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.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   * The ReportGenerator is used to, as the name implies, generate reports.
75   * Internally the generator uses the Velocity Templating Engine. The
76   * ReportGenerator exposes a list of Dependencies to the template when
77   * generating the report.
78   *
79   * @author Jeremy Long
80   */
81  @NotThreadSafe
82  public class ReportGenerator {
83  
84      /**
85       * The logger.
86       */
87      private static final Logger LOGGER = LoggerFactory.getLogger(ReportGenerator.class);
88  
89      /**
90       * An enumeration of the report formats.
91       */
92      public enum Format {
93  
94          /**
95           * Generate all reports.
96           */
97          ALL,
98          /**
99           * Generate XML report.
100          */
101         XML,
102         /**
103          * Generate HTML report.
104          */
105         HTML,
106         /**
107          * Generate JSON report.
108          */
109         JSON,
110         /**
111          * Generate CSV report.
112          */
113         CSV,
114         /**
115          * Generate Sarif report.
116          */
117         SARIF,
118         /**
119          * Generate HTML report without script or non-vulnerable libraries for
120          * Jenkins.
121          */
122         JENKINS,
123         /**
124          * Generate JUNIT report.
125          */
126         JUNIT,
127         /**
128          * Generate Report in GitLab dependency check format.
129          *
130          * @see <a href="https://gitlab.com/gitlab-org/security-products/security-report-schemas/-/blob/master/dist/dependency-scanning-report-format.json">format definition</a>
131          * @see <a href="https://docs.gitlab.com/ee/development/integrations/secure.html">additional explanations on the format</a>
132          */
133         GITLAB
134     }
135 
136     /**
137      * The Velocity Engine.
138      */
139     private final VelocityEngine velocityEngine;
140     /**
141      * The Velocity Engine Context.
142      */
143     private final Context context;
144     /**
145      * The configured settings.
146      */
147     private final Settings settings;
148 
149     //CSOFF: ParameterNumber
150     //CSOFF: LineLength
151 
152     /**
153      * Constructs a new ReportGenerator.
154      *
155      * @param applicationName the application name being analyzed
156      * @param dependencies the list of dependencies
157      * @param analyzers the list of analyzers used
158      * @param properties the database properties (containing timestamps of the
159      * NVD CVE data)
160      * @param settings a reference to the database settings
161      * @deprecated Please use
162      * {@link #ReportGenerator(java.lang.String, java.util.List, java.util.List, DatabaseProperties, Settings, ExceptionCollection)}
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      * Constructs a new ReportGenerator.
172      *
173      * @param applicationName the application name being analyzed
174      * @param dependencies the list of dependencies
175      * @param analyzers the list of analyzers used
176      * @param properties the database properties (containing timestamps of the
177      * NVD CVE data)
178      * @param settings a reference to the database settings
179      * @param exceptions a collection of exceptions that may have occurred
180      * during the analysis
181      * @since 5.1.0
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      * Constructs a new ReportGenerator.
190      *
191      * @param applicationName the application name being analyzed
192      * @param groupID the group id of the project being analyzed
193      * @param artifactID the application id of the project being analyzed
194      * @param version the application version of the project being analyzed
195      * @param dependencies the list of dependencies
196      * @param analyzers the list of analyzers used
197      * @param properties the database properties (containing timestamps of the
198      * NVD CVE data)
199      * @param settings a reference to the database settings
200      * @deprecated Please use
201      * {@link #ReportGenerator(String, String, String, String, List, List, DatabaseProperties, Settings, ExceptionCollection)}
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      * Constructs a new ReportGenerator.
212      *
213      * @param applicationName the application name being analyzed
214      * @param groupID the group id of the project being analyzed
215      * @param artifactID the application id of the project being analyzed
216      * @param version the application version of the project being analyzed
217      * @param dependencies the list of dependencies
218      * @param analyzers the list of analyzers used
219      * @param properties the database properties (containing timestamps of the
220      * NVD CVE data)
221      * @param settings a reference to the database settings
222      * @param exceptions a collection of exceptions that may have occurred
223      * during the analysis
224      * @since 5.1.0
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      * Constructs the velocity context used to generate the dependency-check
238      * reports.
239      *
240      * @param applicationName the application name being analyzed
241      * @param groupID the group id of the project being analyzed
242      * @param artifactID the application id of the project being analyzed
243      * @param version the application version of the project being analyzed
244      * @param dependencies the list of dependencies
245      * @param analyzers the list of analyzers used
246      * @param properties the database properties (containing timestamps of the
247      * NVD CVE data)
248      * @param exceptions a collection of exceptions that may have occurred
249      * during the analysis
250      * @return the velocity context
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     //CSON: ParameterNumber
298     //CSON: LineLength
299 
300     /**
301      * Creates a new Velocity Engine.
302      *
303      * @return a velocity engine
304      */
305     private VelocityEngine createVelocityEngine() {
306         return new VelocityEngine();
307     }
308 
309     /**
310      * Writes the dependency-check report to the given output location.
311      *
312      * @param outputLocation the path where the reports should be written
313      * @param format the format the report should be written in (a valid member
314      * of {@link Format}) or even the path to a custom velocity template
315      * (either fully qualified or the template name on the class path).
316      * @throws ReportException is thrown if there is an error creating out the
317      * reports
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      * Writes the dependency-check report(s).
343      *
344      * @param outputLocation the path where the reports should be written
345      * @param format the format the report should be written in (see
346      * {@link Format})
347      * @throws ReportException is thrown if there is an error creating out the
348      * reports
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      * Determines the report file name based on the give output location and
374      * format. If the output location contains a full file name that has the
375      * correct extension for the given report type then the output location is
376      * returned. However, if the output location is a directory, this method
377      * will generate the correct name for the given output format.
378      *
379      * @param outputLocation the specified output location
380      * @param format the report format
381      * @return the report File
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      * Generates a report from a given Velocity Template. The template name
418      * provided can be the name of a template contained in the jar file, such as
419      * 'XmlReport' or 'HtmlReport', or the template name can be the path to a
420      * template file.
421      *
422      * @param template the name of the template to load
423      * @param file the output file to write the report to
424      * @throws ReportException is thrown when the report cannot be generated
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      * Generates a report from a given Velocity Template. The template name
438      * provided can be the name of a template contained in the jar file, such as
439      * 'XmlReport' or 'HtmlReport', or the template name can be the path to a
440      * template file.
441      *
442      * @param templateName the name of the template to load
443      * @param outputStream the OutputStream to write the report to
444      * @throws ReportException is thrown when an exception occurs
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      * Validates that the given file's parent directory exists. If the directory
494      * does not exist an attempt to create the necessary path is made; if that
495      * fails a ReportException will be raised.
496      *
497      * @param file the file or directory directory
498      * @throws ReportException thrown if the parent directory does not exist and
499      * cannot be created
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      * Reformats the given XML file.
513      *
514      * @param path the path to the XML file to be reformatted
515      * @throws ReportException thrown if the given JSON file is malformed
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      * Reformats the given JSON file.
556      *
557      * @param pathToJson the path to the JSON file to be reformatted
558      * @throws ReportException thrown if the given JSON file is malformed
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 }