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