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.lang3.StringUtils;
26 import org.apache.commons.text.WordUtils;
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
163 public ReportGenerator(String applicationName, List<Dependency> dependencies, List<Analyzer> analyzers,
164 DatabaseProperties properties, Settings settings, ExceptionCollection exceptions) {
165 this(applicationName, null, null, null, dependencies, analyzers, properties, settings, exceptions);
166 }
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184 public ReportGenerator(String applicationName, String groupID, String artifactID, String version,
185 List<Dependency> dependencies, List<Analyzer> analyzers, DatabaseProperties properties,
186 Settings settings, ExceptionCollection exceptions) {
187 this.settings = settings;
188 velocityEngine = createVelocityEngine();
189 velocityEngine.init();
190 context = createContext(applicationName, dependencies, analyzers, properties, groupID,
191 artifactID, version, exceptions);
192 }
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210 @SuppressWarnings("JavaTimeDefaultTimeZone")
211 private VelocityContext createContext(String applicationName, List<Dependency> dependencies,
212 List<Analyzer> analyzers, DatabaseProperties properties, String groupID,
213 String artifactID, String version, ExceptionCollection exceptions) {
214
215 final ZonedDateTime dt = ZonedDateTime.now();
216 final String scanDate = DateTimeFormatter.RFC_1123_DATE_TIME.format(dt);
217 final String scanDateXML = DateTimeFormatter.ISO_INSTANT.format(dt);
218 final String scanDateJunit = DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(dt);
219 final String scanDateGitLab = DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(dt.withNano(0));
220
221
222 final VelocityContext ctxt = new VelocityContext();
223 ctxt.put("applicationName", applicationName);
224 dependencies.sort(Dependency.NAME_COMPARATOR);
225 ctxt.put("dependencies", dependencies);
226 ctxt.put("analyzers", analyzers);
227 ctxt.put("properties", properties);
228 ctxt.put("scanDate", scanDate);
229 ctxt.put("scanDateXML", scanDateXML);
230 ctxt.put("scanDateJunit", scanDateJunit);
231 ctxt.put("scanDateGitLab", scanDateGitLab);
232 ctxt.put("enc", new EscapeTool());
233 ctxt.put("rpt", new ReportTool());
234 ctxt.put("checksum", Checksum.class);
235 ctxt.put("WordUtils", new WordUtils());
236 ctxt.put("StringUtils", new StringUtils());
237 ctxt.put("VENDOR", EvidenceType.VENDOR);
238 ctxt.put("PRODUCT", EvidenceType.PRODUCT);
239 ctxt.put("VERSION", EvidenceType.VERSION);
240 ctxt.put("version", settings.getString(Settings.KEYS.APPLICATION_VERSION, "Unknown"));
241 ctxt.put("settings", settings);
242 if (version != null) {
243 ctxt.put("applicationVersion", version);
244 }
245 if (artifactID != null) {
246 ctxt.put("artifactID", artifactID);
247 }
248 if (groupID != null) {
249 ctxt.put("groupID", groupID);
250 }
251 if (exceptions != null) {
252 ctxt.put("exceptions", exceptions.getExceptions());
253 }
254 return ctxt;
255 }
256
257
258
259
260
261
262
263
264 private VelocityEngine createVelocityEngine() {
265 return new VelocityEngine();
266 }
267
268
269
270
271
272
273
274
275
276
277
278 public void write(String outputLocation, String format) throws ReportException {
279 Format reportFormat = null;
280 try {
281 reportFormat = Format.valueOf(format.toUpperCase());
282 } catch (IllegalArgumentException ex) {
283 LOGGER.trace("ignore this exception", ex);
284 }
285
286 if (reportFormat != null) {
287 write(outputLocation, reportFormat);
288 } else {
289 File out = getReportFile(outputLocation, null);
290 if (out.isDirectory()) {
291 out = new File(out, FilenameUtils.getBaseName(format));
292 LOGGER.warn("Writing non-standard VSL output to a directory using template name as file name.");
293 }
294 LOGGER.info("Writing custom report to: {}", out.getAbsolutePath());
295 processTemplate(format, out);
296 }
297
298 }
299
300
301
302
303
304
305
306
307
308
309 public void write(String outputLocation, Format format) throws ReportException {
310 if (format == Format.ALL) {
311 for (Format f : Format.values()) {
312 if (f != Format.ALL) {
313 write(outputLocation, f);
314 }
315 }
316 } else {
317 final File out = getReportFile(outputLocation, format);
318 final String templateName = format.toString().toLowerCase() + "Report";
319 LOGGER.info("Writing {} report to: {}", format, out.getAbsolutePath());
320 processTemplate(templateName, out);
321 if (settings.getBoolean(Settings.KEYS.PRETTY_PRINT, false)) {
322 if (format == Format.JSON || format == Format.SARIF) {
323 pretifyJson(out.getPath());
324 } else if (format == Format.XML || format == Format.JUNIT) {
325 pretifyXml(out.getPath());
326 }
327 }
328 }
329 }
330
331
332
333
334
335
336
337
338
339
340
341
342 public static File getReportFile(String outputLocation, Format format) {
343 File outFile = new File(outputLocation);
344 if (outFile.getParentFile() == null) {
345 outFile = new File(".", outputLocation);
346 }
347 final String pathToCheck = outputLocation.toLowerCase();
348 if (format == Format.XML && !pathToCheck.endsWith(".xml")) {
349 return new File(outFile, "dependency-check-report.xml");
350 }
351 if (format == Format.HTML && !pathToCheck.endsWith(".html") && !pathToCheck.endsWith(".htm")) {
352 return new File(outFile, "dependency-check-report.html");
353 }
354 if (format == Format.JENKINS && !pathToCheck.endsWith(".html") && !pathToCheck.endsWith(".htm")) {
355 return new File(outFile, "dependency-check-jenkins.html");
356 }
357 if (format == Format.JSON && !pathToCheck.endsWith(".json")) {
358 return new File(outFile, "dependency-check-report.json");
359 }
360 if (format == Format.CSV && !pathToCheck.endsWith(".csv")) {
361 return new File(outFile, "dependency-check-report.csv");
362 }
363 if (format == Format.JUNIT && !pathToCheck.endsWith(".xml")) {
364 return new File(outFile, "dependency-check-junit.xml");
365 }
366 if (format == Format.SARIF && !pathToCheck.endsWith(".sarif")) {
367 return new File(outFile, "dependency-check-report.sarif");
368 }
369 if (format == Format.GITLAB && !pathToCheck.endsWith(".json")) {
370 return new File(outFile, "dependency-check-gitlab.json");
371 }
372 return outFile;
373 }
374
375
376
377
378
379
380
381
382
383
384
385 @SuppressFBWarnings(justification = "try with resources will clean up the output stream", value = {"OBL_UNSATISFIED_OBLIGATION"})
386 protected void processTemplate(String template, File file) throws ReportException {
387 ensureParentDirectoryExists(file);
388 try (OutputStream output = new FileOutputStream(file)) {
389 processTemplate(template, output);
390 } catch (IOException ex) {
391 throw new ReportException(String.format("Unable to write to file: %s", file), ex);
392 }
393 }
394
395
396
397
398
399
400
401
402
403
404
405 protected void processTemplate(String templateName, OutputStream outputStream) throws ReportException {
406 try {
407 String logTag;
408 InputStream input;
409 final File f = new File(templateName);
410 if (f.isFile()) {
411 logTag = templateName;
412 input = new FileInputStream(f);
413 } else {
414 logTag = "templates/" + templateName + ".vsl";
415 input = FileUtils.getResourceAsStream(logTag);
416 }
417
418 try (InputStreamReader reader = new InputStreamReader(input, StandardCharsets.UTF_8);
419 OutputStreamWriter writer = new OutputStreamWriter(outputStream, StandardCharsets.UTF_8)) {
420 if (!velocityEngine.evaluate(context, writer, logTag, reader)) {
421 throw new ReportException("Failed to convert the template into html.");
422 }
423 writer.flush();
424 }
425 } catch (FileNotFoundException ex) {
426 throw new ReportException("Unable to locate template file: " + templateName, ex);
427 } catch (IOException ex) {
428 throw new ReportException("Unable to write the report", ex);
429 }
430 }
431
432
433
434
435
436
437
438
439
440
441 private void ensureParentDirectoryExists(File file) throws ReportException {
442 if (!file.getParentFile().exists()) {
443 final boolean created = file.getParentFile().mkdirs();
444 if (!created) {
445 final String msg = String.format("Unable to create directory '%s'.", file.getParentFile().getAbsolutePath());
446 throw new ReportException(msg);
447 }
448 }
449 }
450
451
452
453
454
455
456 private void pretifyXml(String path) {
457 final String outputPath = path + ".pretty";
458 final File in = new File(path);
459 final File out = new File(outputPath);
460 try (OutputStream os = new FileOutputStream(out)) {
461 final TransformerFactory transformerFactory = SAXTransformerFactory.newInstance();
462 transformerFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
463 final Transformer transformer = transformerFactory.newTransformer();
464 transformer.setOutputProperty(OutputKeys.ENCODING, StandardCharsets.UTF_8.name());
465 transformer.setOutputProperty(OutputKeys.INDENT, "yes");
466 transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2");
467
468 final SAXSource saxs = new SAXSource(XmlUtils.buildSecureXmlReader(), new InputSource(path));
469 transformer.transform(saxs, new StreamResult(new OutputStreamWriter(os, StandardCharsets.UTF_8)));
470 } catch (ParserConfigurationException | TransformerConfigurationException ex) {
471 LOGGER.debug("Configuration exception when pretty printing", ex);
472 LOGGER.error("Unable to generate pretty report, caused by: {}", ex.getMessage());
473 } catch (TransformerException | SAXException | IOException ex) {
474 LOGGER.debug("Malformed XML?", ex);
475 LOGGER.error("Unable to generate pretty report, caused by: {}", ex.getMessage());
476 }
477 replaceWithPrettified(out, in);
478 }
479
480
481
482
483
484
485
486 private void pretifyJson(String pathToJson) throws ReportException {
487 LOGGER.debug("pretify json: {}", pathToJson);
488 final String outputPath = pathToJson + ".pretty";
489 final File in = new File(pathToJson);
490 final File out = new File(outputPath);
491
492 final JsonFactory factory = new JsonFactory();
493
494 try (InputStream is = new FileInputStream(in); OutputStream os = new FileOutputStream(out)) {
495
496 final JsonParser parser = factory.createParser(is);
497 final JsonGenerator generator = factory.createGenerator(os);
498
499 generator.useDefaultPrettyPrinter();
500
501 while (parser.nextToken() != null) {
502 generator.copyCurrentEvent(parser);
503 }
504 generator.flush();
505 } catch (IOException ex) {
506 LOGGER.debug("Malformed JSON?", ex);
507 throw new ReportException("Unable to generate json report", ex);
508 }
509 replaceWithPrettified(out, in);
510 }
511
512 private void replaceWithPrettified(File prettified, File original) {
513 if (prettified.isFile() && original.isFile() && original.delete()) {
514 try {
515 Thread.sleep(1000);
516 Files.move(prettified.toPath(), original.toPath());
517 } catch (IOException ex) {
518 LOGGER.error("Unable to generate pretty report, caused by: {}", ex.getMessage());
519 } catch (InterruptedException ex) {
520 Thread.currentThread().interrupt();
521 LOGGER.error("Unable to generate pretty report, caused by: {}", ex.getMessage());
522 }
523 }
524 }
525
526 }