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