View Javadoc
1   /*
2    * This file is part of dependency-check-core.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *     http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   *
16   * Copyright (c) 2012 Jeremy Long. All Rights Reserved.
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   * The ReportGenerator is used to, as the name implies, generate reports.
74   * Internally the generator uses the Velocity Templating Engine. The
75   * ReportGenerator exposes a list of Dependencies to the template when
76   * generating the report.
77   *
78   * @author Jeremy Long
79   */
80  @NotThreadSafe
81  public class ReportGenerator {
82  
83      /**
84       * The logger.
85       */
86      private static final Logger LOGGER = LoggerFactory.getLogger(ReportGenerator.class);
87  
88      /**
89       * An enumeration of the report formats.
90       */
91      public enum Format {
92  
93          /**
94           * Generate all reports.
95           */
96          ALL,
97          /**
98           * Generate XML report.
99           */
100         XML,
101         /**
102          * Generate HTML report.
103          */
104         HTML,
105         /**
106          * Generate JSON report.
107          */
108         JSON,
109         /**
110          * Generate CSV report.
111          */
112         CSV,
113         /**
114          * Generate Sarif report.
115          */
116         SARIF,
117         /**
118          * Generate HTML report without script or non-vulnerable libraries for
119          * Jenkins.
120          */
121         JENKINS,
122         /**
123          * Generate JUNIT report.
124          */
125         JUNIT,
126         /**
127          * Generate Report in GitLab dependency check format:
128          *
129          * @see <a href="https://gitlab.com/gitlab-org/security-products/security-report-schemas/-/blob/master/dist/dependency-scanning-report-format.json">format definition</a>
130          * @see <a href="https://docs.gitlab.com/ee/development/integrations/secure.html">additional explantions on the format</a>
131          */
132         GITLAB
133     }
134 
135     /**
136      * The Velocity Engine.
137      */
138     private final VelocityEngine velocityEngine;
139     /**
140      * The Velocity Engine Context.
141      */
142     private final Context context;
143     /**
144      * The configured settings.
145      */
146     private final Settings settings;
147 
148     //CSOFF: ParameterNumber
149     //CSOFF: LineLength
150 
151     /**
152      * Constructs a new ReportGenerator.
153      *
154      * @param applicationName the application name being analyzed
155      * @param dependencies the list of dependencies
156      * @param analyzers the list of analyzers used
157      * @param properties the database properties (containing timestamps of the
158      * NVD CVE data)
159      * @param settings a reference to the database settings
160      * @deprecated Please use
161      * {@link #ReportGenerator(java.lang.String, java.util.List, java.util.List, DatabaseProperties, Settings, ExceptionCollection)}
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      * Constructs a new ReportGenerator.
171      *
172      * @param applicationName the application name being analyzed
173      * @param dependencies the list of dependencies
174      * @param analyzers the list of analyzers used
175      * @param properties the database properties (containing timestamps of the
176      * NVD CVE data)
177      * @param settings a reference to the database settings
178      * @param exceptions a collection of exceptions that may have occurred
179      * during the analysis
180      * @since 5.1.0
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      * Constructs a new ReportGenerator.
189      *
190      * @param applicationName the application name being analyzed
191      * @param groupID the group id of the project being analyzed
192      * @param artifactID the application id of the project being analyzed
193      * @param version the application version of the project being analyzed
194      * @param dependencies the list of dependencies
195      * @param analyzers the list of analyzers used
196      * @param properties the database properties (containing timestamps of the
197      * NVD CVE data)
198      * @param settings a reference to the database settings
199      * @deprecated Please use
200      * {@link #ReportGenerator(String, String, String, String, List, List, DatabaseProperties, Settings, ExceptionCollection)}
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      * Constructs a new ReportGenerator.
211      *
212      * @param applicationName the application name being analyzed
213      * @param groupID the group id of the project being analyzed
214      * @param artifactID the application id of the project being analyzed
215      * @param version the application version of the project being analyzed
216      * @param dependencies the list of dependencies
217      * @param analyzers the list of analyzers used
218      * @param properties the database properties (containing timestamps of the
219      * NVD CVE data)
220      * @param settings a reference to the database settings
221      * @param exceptions a collection of exceptions that may have occurred
222      * during the analysis
223      * @since 5.1.0
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      * Constructs the velocity context used to generate the dependency-check
237      * reports.
238      *
239      * @param applicationName the application name being analyzed
240      * @param groupID the group id of the project being analyzed
241      * @param artifactID the application id of the project being analyzed
242      * @param version the application version of the project being analyzed
243      * @param dependencies the list of dependencies
244      * @param analyzers the list of analyzers used
245      * @param properties the database properties (containing timestamps of the
246      * NVD CVE data)
247      * @param exceptions a collection of exceptions that may have occurred
248      * during the analysis
249      * @return the velocity context
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     //CSON: ParameterNumber
296     //CSON: LineLength
297 
298     /**
299      * Creates a new Velocity Engine.
300      *
301      * @return a velocity engine
302      */
303     private VelocityEngine createVelocityEngine() {
304         return new VelocityEngine();
305     }
306 
307     /**
308      * Writes the dependency-check report to the given output location.
309      *
310      * @param outputLocation the path where the reports should be written
311      * @param format the format the report should be written in (a valid member
312      * of {@link Format}) or even the path to a custom velocity template
313      * (either fully qualified or the template name on the class path).
314      * @throws ReportException is thrown if there is an error creating out the
315      * reports
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      * Writes the dependency-check report(s).
341      *
342      * @param outputLocation the path where the reports should be written
343      * @param format the format the report should be written in (see
344      * {@link Format})
345      * @throws ReportException is thrown if there is an error creating out the
346      * reports
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      * Determines the report file name based on the give output location and
372      * format. If the output location contains a full file name that has the
373      * correct extension for the given report type then the output location is
374      * returned. However, if the output location is a directory, this method
375      * will generate the correct name for the given output format.
376      *
377      * @param outputLocation the specified output location
378      * @param format the report format
379      * @return the report File
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      * Generates a report from a given Velocity Template. The template name
416      * provided can be the name of a template contained in the jar file, such as
417      * 'XmlReport' or 'HtmlReport', or the template name can be the path to a
418      * template file.
419      *
420      * @param template the name of the template to load
421      * @param file the output file to write the report to
422      * @throws ReportException is thrown when the report cannot be generated
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      * Generates a report from a given Velocity Template. The template name
436      * provided can be the name of a template contained in the jar file, such as
437      * 'XmlReport' or 'HtmlReport', or the template name can be the path to a
438      * template file.
439      *
440      * @param templateName the name of the template to load
441      * @param outputStream the OutputStream to write the report to
442      * @throws ReportException is thrown when an exception occurs
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      * Validates that the given file's parent directory exists. If the directory
492      * does not exist an attempt to create the necessary path is made; if that
493      * fails a ReportException will be raised.
494      *
495      * @param file the file or directory directory
496      * @throws ReportException thrown if the parent directory does not exist and
497      * cannot be created
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      * Reformats the given XML file.
511      *
512      * @param path the path to the XML file to be reformatted
513      * @throws ReportException thrown if the given JSON file is malformed
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      * Reformats the given JSON file.
554      *
555      * @param pathToJson the path to the JSON file to be reformatted
556      * @throws ReportException thrown if the given JSON file is malformed
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 }