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