1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 package org.owasp.dependencycheck.reporting;
19
20 import com.fasterxml.jackson.core.JsonFactory;
21 import com.fasterxml.jackson.core.JsonGenerator;
22 import com.fasterxml.jackson.core.JsonParser;
23 import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
24 import org.apache.commons.io.FilenameUtils;
25 import org.apache.commons.text.WordUtils;
26 import org.apache.velocity.VelocityContext;
27 import org.apache.velocity.app.VelocityEngine;
28 import org.apache.velocity.context.Context;
29 import org.owasp.dependencycheck.analyzer.Analyzer;
30 import org.owasp.dependencycheck.data.nvdcve.DatabaseProperties;
31 import org.owasp.dependencycheck.dependency.Dependency;
32 import org.owasp.dependencycheck.dependency.EvidenceType;
33 import org.owasp.dependencycheck.exception.ExceptionCollection;
34 import org.owasp.dependencycheck.exception.ReportException;
35 import org.owasp.dependencycheck.utils.Checksum;
36 import org.owasp.dependencycheck.utils.FileUtils;
37 import org.owasp.dependencycheck.utils.Settings;
38 import org.owasp.dependencycheck.utils.XmlUtils;
39 import org.slf4j.Logger;
40 import org.slf4j.LoggerFactory;
41 import org.xml.sax.InputSource;
42 import org.xml.sax.SAXException;
43 import org.xml.sax.XMLReader;
44
45 import javax.annotation.concurrent.NotThreadSafe;
46 import javax.xml.XMLConstants;
47 import javax.xml.parsers.ParserConfigurationException;
48 import javax.xml.transform.OutputKeys;
49 import javax.xml.transform.Transformer;
50 import javax.xml.transform.TransformerConfigurationException;
51 import javax.xml.transform.TransformerException;
52 import javax.xml.transform.TransformerFactory;
53 import javax.xml.transform.sax.SAXSource;
54 import javax.xml.transform.sax.SAXTransformerFactory;
55 import javax.xml.transform.stream.StreamResult;
56 import java.io.File;
57 import java.io.FileInputStream;
58 import java.io.FileNotFoundException;
59 import java.io.FileOutputStream;
60 import java.io.IOException;
61 import java.io.InputStream;
62 import java.io.InputStreamReader;
63 import java.io.OutputStream;
64 import java.io.OutputStreamWriter;
65 import java.io.UnsupportedEncodingException;
66 import java.nio.charset.StandardCharsets;
67 import java.nio.file.Files;
68 import java.time.ZonedDateTime;
69 import java.time.format.DateTimeFormatter;
70 import java.util.List;
71
72
73
74
75
76
77
78
79
80 @NotThreadSafe
81 public class ReportGenerator {
82
83
84
85
86 private static final Logger LOGGER = LoggerFactory.getLogger(ReportGenerator.class);
87
88
89
90
91 public enum Format {
92
93
94
95
96 ALL,
97
98
99
100 XML,
101
102
103
104 HTML,
105
106
107
108 JSON,
109
110
111
112 CSV,
113
114
115
116 SARIF,
117
118
119
120
121 JENKINS,
122
123
124
125 JUNIT,
126
127
128
129
130
131
132 GITLAB
133 }
134
135
136
137
138 private final VelocityEngine velocityEngine;
139
140
141
142 private final Context context;
143
144
145
146 private final Settings settings;
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163 @Deprecated
164 public ReportGenerator(String applicationName, List<Dependency> dependencies, List<Analyzer> analyzers,
165 DatabaseProperties properties, Settings settings) {
166 this(applicationName, dependencies, analyzers, properties, settings, null);
167 }
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182 public ReportGenerator(String applicationName, List<Dependency> dependencies, List<Analyzer> analyzers,
183 DatabaseProperties properties, Settings settings, ExceptionCollection exceptions) {
184 this(applicationName, null, null, null, dependencies, analyzers, properties, settings, exceptions);
185 }
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202 @Deprecated
203 public ReportGenerator(String applicationName, String groupID, String artifactID, String version,
204 List<Dependency> dependencies, List<Analyzer> analyzers, DatabaseProperties properties,
205 Settings settings) {
206 this(applicationName, groupID, artifactID, version, dependencies, analyzers, properties, settings, null);
207 }
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225 public ReportGenerator(String applicationName, String groupID, String artifactID, String version,
226 List<Dependency> dependencies, List<Analyzer> analyzers, DatabaseProperties properties,
227 Settings settings, ExceptionCollection exceptions) {
228 this.settings = settings;
229 velocityEngine = createVelocityEngine();
230 velocityEngine.init();
231 context = createContext(applicationName, dependencies, analyzers, properties, groupID,
232 artifactID, version, exceptions);
233 }
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251 @SuppressWarnings("JavaTimeDefaultTimeZone")
252 private VelocityContext createContext(String applicationName, List<Dependency> dependencies,
253 List<Analyzer> analyzers, DatabaseProperties properties, String groupID,
254 String artifactID, String version, ExceptionCollection exceptions) {
255
256 final ZonedDateTime dt = ZonedDateTime.now();
257 final String scanDate = DateTimeFormatter.RFC_1123_DATE_TIME.format(dt);
258 final String scanDateXML = DateTimeFormatter.ISO_INSTANT.format(dt);
259 final String scanDateJunit = DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(dt);
260 final String scanDateGitLab = DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(dt.withNano(0));
261
262 final VelocityContext ctxt = new VelocityContext();
263 ctxt.put("applicationName", applicationName);
264 dependencies.sort(Dependency.NAME_COMPARATOR);
265 ctxt.put("dependencies", dependencies);
266 ctxt.put("analyzers", analyzers);
267 ctxt.put("properties", properties);
268 ctxt.put("scanDate", scanDate);
269 ctxt.put("scanDateXML", scanDateXML);
270 ctxt.put("scanDateJunit", scanDateJunit);
271 ctxt.put("scanDateGitLab", scanDateGitLab);
272 ctxt.put("enc", new EscapeTool());
273 ctxt.put("rpt", new ReportTool());
274 ctxt.put("checksum", Checksum.class);
275 ctxt.put("WordUtils", new WordUtils());
276 ctxt.put("VENDOR", EvidenceType.VENDOR);
277 ctxt.put("PRODUCT", EvidenceType.PRODUCT);
278 ctxt.put("VERSION", EvidenceType.VERSION);
279 ctxt.put("version", settings.getString(Settings.KEYS.APPLICATION_VERSION, "Unknown"));
280 ctxt.put("settings", settings);
281 if (version != null) {
282 ctxt.put("applicationVersion", version);
283 }
284 if (artifactID != null) {
285 ctxt.put("artifactID", artifactID);
286 }
287 if (groupID != null) {
288 ctxt.put("groupID", groupID);
289 }
290 if (exceptions != null) {
291 ctxt.put("exceptions", exceptions.getExceptions());
292 }
293 return ctxt;
294 }
295
296
297
298
299
300
301
302
303 private VelocityEngine createVelocityEngine() {
304 return new VelocityEngine();
305 }
306
307
308
309
310
311
312
313
314
315
316
317 public void write(String outputLocation, String format) throws ReportException {
318 Format reportFormat = null;
319 try {
320 reportFormat = Format.valueOf(format.toUpperCase());
321 } catch (IllegalArgumentException ex) {
322 LOGGER.trace("ignore this exception", ex);
323 }
324
325 if (reportFormat != null) {
326 write(outputLocation, reportFormat);
327 } else {
328 File out = getReportFile(outputLocation, null);
329 if (out.isDirectory()) {
330 out = new File(out, FilenameUtils.getBaseName(format));
331 LOGGER.warn("Writing non-standard VSL output to a directory using template name as file name.");
332 }
333 LOGGER.info("Writing custom report to: {}", out.getAbsolutePath());
334 processTemplate(format, out);
335 }
336
337 }
338
339
340
341
342
343
344
345
346
347
348 public void write(String outputLocation, Format format) throws ReportException {
349 if (format == Format.ALL) {
350 for (Format f : Format.values()) {
351 if (f != Format.ALL) {
352 write(outputLocation, f);
353 }
354 }
355 } else {
356 final File out = getReportFile(outputLocation, format);
357 final String templateName = format.toString().toLowerCase() + "Report";
358 LOGGER.info("Writing {} report to: {}", format, out.getAbsolutePath());
359 processTemplate(templateName, out);
360 if (settings.getBoolean(Settings.KEYS.PRETTY_PRINT, false)) {
361 if (format == Format.JSON || format == Format.SARIF) {
362 pretifyJson(out.getPath());
363 } else if (format == Format.XML || format == Format.JUNIT) {
364 pretifyXml(out.getPath());
365 }
366 }
367 }
368 }
369
370
371
372
373
374
375
376
377
378
379
380
381 public static File getReportFile(String outputLocation, Format format) {
382 File outFile = new File(outputLocation);
383 if (outFile.getParentFile() == null) {
384 outFile = new File(".", outputLocation);
385 }
386 final String pathToCheck = outputLocation.toLowerCase();
387 if (format == Format.XML && !pathToCheck.endsWith(".xml")) {
388 return new File(outFile, "dependency-check-report.xml");
389 }
390 if (format == Format.HTML && !pathToCheck.endsWith(".html") && !pathToCheck.endsWith(".htm")) {
391 return new File(outFile, "dependency-check-report.html");
392 }
393 if (format == Format.JENKINS && !pathToCheck.endsWith(".html") && !pathToCheck.endsWith(".htm")) {
394 return new File(outFile, "dependency-check-jenkins.html");
395 }
396 if (format == Format.JSON && !pathToCheck.endsWith(".json")) {
397 return new File(outFile, "dependency-check-report.json");
398 }
399 if (format == Format.CSV && !pathToCheck.endsWith(".csv")) {
400 return new File(outFile, "dependency-check-report.csv");
401 }
402 if (format == Format.JUNIT && !pathToCheck.endsWith(".xml")) {
403 return new File(outFile, "dependency-check-junit.xml");
404 }
405 if (format == Format.SARIF && !pathToCheck.endsWith(".sarif")) {
406 return new File(outFile, "dependency-check-report.sarif");
407 }
408 if (format == Format.GITLAB && !pathToCheck.endsWith(".json")) {
409 return new File(outFile, "dependency-check-gitlab.json");
410 }
411 return outFile;
412 }
413
414
415
416
417
418
419
420
421
422
423
424 @SuppressFBWarnings(justification = "try with resources will clean up the output stream", value = {"OBL_UNSATISFIED_OBLIGATION"})
425 protected void processTemplate(String template, File file) throws ReportException {
426 ensureParentDirectoryExists(file);
427 try (OutputStream output = new FileOutputStream(file)) {
428 processTemplate(template, output);
429 } catch (IOException ex) {
430 throw new ReportException(String.format("Unable to write to file: %s", file), ex);
431 }
432 }
433
434
435
436
437
438
439
440
441
442
443
444 protected void processTemplate(String templateName, OutputStream outputStream) throws ReportException {
445 InputStream input = null;
446 String logTag;
447 final File f = new File(templateName);
448 try {
449 if (f.isFile()) {
450 try {
451 logTag = templateName;
452 input = new FileInputStream(f);
453 } catch (FileNotFoundException ex) {
454 throw new ReportException("Unable to locate template file: " + templateName, ex);
455 }
456 } else {
457 logTag = "templates/" + templateName + ".vsl";
458 input = FileUtils.getResourceAsStream(logTag);
459 }
460 if (input == null) {
461 logTag = templateName;
462 input = FileUtils.getResourceAsStream(templateName);
463 }
464 if (input == null) {
465 throw new ReportException("Template file doesn't exist: " + logTag);
466 }
467
468 try (InputStreamReader reader = new InputStreamReader(input, StandardCharsets.UTF_8);
469 OutputStreamWriter writer = new OutputStreamWriter(outputStream, StandardCharsets.UTF_8)) {
470 if (!velocityEngine.evaluate(context, writer, logTag, reader)) {
471 throw new ReportException("Failed to convert the template into html.");
472 }
473 writer.flush();
474 } catch (UnsupportedEncodingException ex) {
475 throw new ReportException("Unable to generate the report using UTF-8", ex);
476 }
477 } catch (IOException ex) {
478 throw new ReportException("Unable to write the report", ex);
479 } finally {
480 if (input != null) {
481 try {
482 input.close();
483 } catch (IOException ex) {
484 LOGGER.trace("Error closing input", ex);
485 }
486 }
487 }
488 }
489
490
491
492
493
494
495
496
497
498
499 private void ensureParentDirectoryExists(File file) throws ReportException {
500 if (!file.getParentFile().exists()) {
501 final boolean created = file.getParentFile().mkdirs();
502 if (!created) {
503 final String msg = String.format("Unable to create directory '%s'.", file.getParentFile().getAbsolutePath());
504 throw new ReportException(msg);
505 }
506 }
507 }
508
509
510
511
512
513
514
515 private void pretifyXml(String path) throws ReportException {
516 final String outputPath = path + ".pretty";
517 final File in = new File(path);
518 final File out = new File(outputPath);
519 try (OutputStream os = new FileOutputStream(out)) {
520 final TransformerFactory transformerFactory = SAXTransformerFactory.newInstance();
521 transformerFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
522 final Transformer transformer = transformerFactory.newTransformer();
523 transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
524 transformer.setOutputProperty(OutputKeys.INDENT, "yes");
525 transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2");
526
527 final SAXSource saxs = new SAXSource(new InputSource(path));
528 final XMLReader saxReader = XmlUtils.buildSecureSaxParser().getXMLReader();
529
530 saxs.setXMLReader(saxReader);
531 transformer.transform(saxs, new StreamResult(new OutputStreamWriter(os, StandardCharsets.UTF_8)));
532 } catch (ParserConfigurationException | TransformerConfigurationException ex) {
533 LOGGER.debug("Configuration exception when pretty printing", ex);
534 LOGGER.error("Unable to generate pretty report, caused by: {}", ex.getMessage());
535 } catch (TransformerException | SAXException | IOException ex) {
536 LOGGER.debug("Malformed XML?", ex);
537 LOGGER.error("Unable to generate pretty report, caused by: {}", ex.getMessage());
538 }
539 if (out.isFile() && in.isFile() && in.delete()) {
540 try {
541 Thread.sleep(1000);
542 Files.move(out.toPath(), in.toPath());
543 } catch (IOException ex) {
544 LOGGER.error("Unable to generate pretty report, caused by: {}", ex.getMessage());
545 } catch (InterruptedException ex) {
546 Thread.currentThread().interrupt();
547 LOGGER.error("Unable to generate pretty report, caused by: {}", ex.getMessage());
548 }
549 }
550 }
551
552
553
554
555
556
557
558 private void pretifyJson(String pathToJson) throws ReportException {
559 LOGGER.debug("pretify json: {}", pathToJson);
560 final String outputPath = pathToJson + ".pretty";
561 final File in = new File(pathToJson);
562 final File out = new File(outputPath);
563
564 final JsonFactory factory = new JsonFactory();
565
566 try (InputStream is = new FileInputStream(in); OutputStream os = new FileOutputStream(out)) {
567
568 final JsonParser parser = factory.createParser(is);
569 final JsonGenerator generator = factory.createGenerator(os);
570
571 generator.useDefaultPrettyPrinter();
572
573 while (parser.nextToken() != null) {
574 generator.copyCurrentEvent(parser);
575 }
576 generator.flush();
577 } catch (IOException ex) {
578 LOGGER.debug("Malformed JSON?", ex);
579 throw new ReportException("Unable to generate json report", ex);
580 }
581 if (out.isFile() && in.isFile() && in.delete()) {
582 try {
583 Thread.sleep(1000);
584 Files.move(out.toPath(), in.toPath());
585 } catch (IOException ex) {
586 LOGGER.error("Unable to generate pretty report, caused by: {}", ex.getMessage());
587 } catch (InterruptedException ex) {
588 Thread.currentThread().interrupt();
589 LOGGER.error("Unable to generate pretty report, caused by: {}", ex.getMessage());
590 }
591 }
592 }
593
594 }