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 final VelocityContext ctxt = new VelocityContext();
264 ctxt.put("applicationName", applicationName);
265 dependencies.sort(Dependency.NAME_COMPARATOR);
266 ctxt.put("dependencies", dependencies);
267 ctxt.put("analyzers", analyzers);
268 ctxt.put("properties", properties);
269 ctxt.put("scanDate", scanDate);
270 ctxt.put("scanDateXML", scanDateXML);
271 ctxt.put("scanDateJunit", scanDateJunit);
272 ctxt.put("scanDateGitLab", scanDateGitLab);
273 ctxt.put("enc", new EscapeTool());
274 ctxt.put("rpt", new ReportTool());
275 ctxt.put("checksum", Checksum.class);
276 ctxt.put("WordUtils", new WordUtils());
277 ctxt.put("StringUtils", new StringUtils());
278 ctxt.put("VENDOR", EvidenceType.VENDOR);
279 ctxt.put("PRODUCT", EvidenceType.PRODUCT);
280 ctxt.put("VERSION", EvidenceType.VERSION);
281 ctxt.put("version", settings.getString(Settings.KEYS.APPLICATION_VERSION, "Unknown"));
282 ctxt.put("settings", settings);
283 if (version != null) {
284 ctxt.put("applicationVersion", version);
285 }
286 if (artifactID != null) {
287 ctxt.put("artifactID", artifactID);
288 }
289 if (groupID != null) {
290 ctxt.put("groupID", groupID);
291 }
292 if (exceptions != null) {
293 ctxt.put("exceptions", exceptions.getExceptions());
294 }
295 return ctxt;
296 }
297
298
299
300
301
302
303
304
305 private VelocityEngine createVelocityEngine() {
306 return new VelocityEngine();
307 }
308
309
310
311
312
313
314
315
316
317
318
319 public void write(String outputLocation, String format) throws ReportException {
320 Format reportFormat = null;
321 try {
322 reportFormat = Format.valueOf(format.toUpperCase());
323 } catch (IllegalArgumentException ex) {
324 LOGGER.trace("ignore this exception", ex);
325 }
326
327 if (reportFormat != null) {
328 write(outputLocation, reportFormat);
329 } else {
330 File out = getReportFile(outputLocation, null);
331 if (out.isDirectory()) {
332 out = new File(out, FilenameUtils.getBaseName(format));
333 LOGGER.warn("Writing non-standard VSL output to a directory using template name as file name.");
334 }
335 LOGGER.info("Writing custom report to: {}", out.getAbsolutePath());
336 processTemplate(format, out);
337 }
338
339 }
340
341
342
343
344
345
346
347
348
349
350 public void write(String outputLocation, Format format) throws ReportException {
351 if (format == Format.ALL) {
352 for (Format f : Format.values()) {
353 if (f != Format.ALL) {
354 write(outputLocation, f);
355 }
356 }
357 } else {
358 final File out = getReportFile(outputLocation, format);
359 final String templateName = format.toString().toLowerCase() + "Report";
360 LOGGER.info("Writing {} report to: {}", format, out.getAbsolutePath());
361 processTemplate(templateName, out);
362 if (settings.getBoolean(Settings.KEYS.PRETTY_PRINT, false)) {
363 if (format == Format.JSON || format == Format.SARIF) {
364 pretifyJson(out.getPath());
365 } else if (format == Format.XML || format == Format.JUNIT) {
366 pretifyXml(out.getPath());
367 }
368 }
369 }
370 }
371
372
373
374
375
376
377
378
379
380
381
382
383 public static File getReportFile(String outputLocation, Format format) {
384 File outFile = new File(outputLocation);
385 if (outFile.getParentFile() == null) {
386 outFile = new File(".", outputLocation);
387 }
388 final String pathToCheck = outputLocation.toLowerCase();
389 if (format == Format.XML && !pathToCheck.endsWith(".xml")) {
390 return new File(outFile, "dependency-check-report.xml");
391 }
392 if (format == Format.HTML && !pathToCheck.endsWith(".html") && !pathToCheck.endsWith(".htm")) {
393 return new File(outFile, "dependency-check-report.html");
394 }
395 if (format == Format.JENKINS && !pathToCheck.endsWith(".html") && !pathToCheck.endsWith(".htm")) {
396 return new File(outFile, "dependency-check-jenkins.html");
397 }
398 if (format == Format.JSON && !pathToCheck.endsWith(".json")) {
399 return new File(outFile, "dependency-check-report.json");
400 }
401 if (format == Format.CSV && !pathToCheck.endsWith(".csv")) {
402 return new File(outFile, "dependency-check-report.csv");
403 }
404 if (format == Format.JUNIT && !pathToCheck.endsWith(".xml")) {
405 return new File(outFile, "dependency-check-junit.xml");
406 }
407 if (format == Format.SARIF && !pathToCheck.endsWith(".sarif")) {
408 return new File(outFile, "dependency-check-report.sarif");
409 }
410 if (format == Format.GITLAB && !pathToCheck.endsWith(".json")) {
411 return new File(outFile, "dependency-check-gitlab.json");
412 }
413 return outFile;
414 }
415
416
417
418
419
420
421
422
423
424
425
426 @SuppressFBWarnings(justification = "try with resources will clean up the output stream", value = {"OBL_UNSATISFIED_OBLIGATION"})
427 protected void processTemplate(String template, File file) throws ReportException {
428 ensureParentDirectoryExists(file);
429 try (OutputStream output = new FileOutputStream(file)) {
430 processTemplate(template, output);
431 } catch (IOException ex) {
432 throw new ReportException(String.format("Unable to write to file: %s", file), ex);
433 }
434 }
435
436
437
438
439
440
441
442
443
444
445
446 protected void processTemplate(String templateName, OutputStream outputStream) throws ReportException {
447 InputStream input = null;
448 String logTag;
449 final File f = new File(templateName);
450 try {
451 if (f.isFile()) {
452 try {
453 logTag = templateName;
454 input = new FileInputStream(f);
455 } catch (FileNotFoundException ex) {
456 throw new ReportException("Unable to locate template file: " + templateName, ex);
457 }
458 } else {
459 logTag = "templates/" + templateName + ".vsl";
460 input = FileUtils.getResourceAsStream(logTag);
461 }
462 if (input == null) {
463 logTag = templateName;
464 input = FileUtils.getResourceAsStream(templateName);
465 }
466 if (input == null) {
467 throw new ReportException("Template file doesn't exist: " + logTag);
468 }
469
470 try (InputStreamReader reader = new InputStreamReader(input, StandardCharsets.UTF_8);
471 OutputStreamWriter writer = new OutputStreamWriter(outputStream, StandardCharsets.UTF_8)) {
472 if (!velocityEngine.evaluate(context, writer, logTag, reader)) {
473 throw new ReportException("Failed to convert the template into html.");
474 }
475 writer.flush();
476 } catch (UnsupportedEncodingException ex) {
477 throw new ReportException("Unable to generate the report using UTF-8", ex);
478 }
479 } catch (IOException ex) {
480 throw new ReportException("Unable to write the report", ex);
481 } finally {
482 if (input != null) {
483 try {
484 input.close();
485 } catch (IOException ex) {
486 LOGGER.trace("Error closing input", ex);
487 }
488 }
489 }
490 }
491
492
493
494
495
496
497
498
499
500
501 private void ensureParentDirectoryExists(File file) throws ReportException {
502 if (!file.getParentFile().exists()) {
503 final boolean created = file.getParentFile().mkdirs();
504 if (!created) {
505 final String msg = String.format("Unable to create directory '%s'.", file.getParentFile().getAbsolutePath());
506 throw new ReportException(msg);
507 }
508 }
509 }
510
511
512
513
514
515
516
517 private void pretifyXml(String path) throws ReportException {
518 final String outputPath = path + ".pretty";
519 final File in = new File(path);
520 final File out = new File(outputPath);
521 try (OutputStream os = new FileOutputStream(out)) {
522 final TransformerFactory transformerFactory = SAXTransformerFactory.newInstance();
523 transformerFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
524 final Transformer transformer = transformerFactory.newTransformer();
525 transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
526 transformer.setOutputProperty(OutputKeys.INDENT, "yes");
527 transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2");
528
529 final SAXSource saxs = new SAXSource(new InputSource(path));
530 final XMLReader saxReader = XmlUtils.buildSecureSaxParser().getXMLReader();
531
532 saxs.setXMLReader(saxReader);
533 transformer.transform(saxs, new StreamResult(new OutputStreamWriter(os, StandardCharsets.UTF_8)));
534 } catch (ParserConfigurationException | TransformerConfigurationException ex) {
535 LOGGER.debug("Configuration exception when pretty printing", ex);
536 LOGGER.error("Unable to generate pretty report, caused by: {}", ex.getMessage());
537 } catch (TransformerException | SAXException | IOException ex) {
538 LOGGER.debug("Malformed XML?", ex);
539 LOGGER.error("Unable to generate pretty report, caused by: {}", ex.getMessage());
540 }
541 if (out.isFile() && in.isFile() && in.delete()) {
542 try {
543 Thread.sleep(1000);
544 Files.move(out.toPath(), in.toPath());
545 } catch (IOException ex) {
546 LOGGER.error("Unable to generate pretty report, caused by: {}", ex.getMessage());
547 } catch (InterruptedException ex) {
548 Thread.currentThread().interrupt();
549 LOGGER.error("Unable to generate pretty report, caused by: {}", ex.getMessage());
550 }
551 }
552 }
553
554
555
556
557
558
559
560 private void pretifyJson(String pathToJson) throws ReportException {
561 LOGGER.debug("pretify json: {}", pathToJson);
562 final String outputPath = pathToJson + ".pretty";
563 final File in = new File(pathToJson);
564 final File out = new File(outputPath);
565
566 final JsonFactory factory = new JsonFactory();
567
568 try (InputStream is = new FileInputStream(in); OutputStream os = new FileOutputStream(out)) {
569
570 final JsonParser parser = factory.createParser(is);
571 final JsonGenerator generator = factory.createGenerator(os);
572
573 generator.useDefaultPrettyPrinter();
574
575 while (parser.nextToken() != null) {
576 generator.copyCurrentEvent(parser);
577 }
578 generator.flush();
579 } catch (IOException ex) {
580 LOGGER.debug("Malformed JSON?", ex);
581 throw new ReportException("Unable to generate json report", ex);
582 }
583 if (out.isFile() && in.isFile() && in.delete()) {
584 try {
585 Thread.sleep(1000);
586 Files.move(out.toPath(), in.toPath());
587 } catch (IOException ex) {
588 LOGGER.error("Unable to generate pretty report, caused by: {}", ex.getMessage());
589 } catch (InterruptedException ex) {
590 Thread.currentThread().interrupt();
591 LOGGER.error("Unable to generate pretty report, caused by: {}", ex.getMessage());
592 }
593 }
594 }
595
596 }