From e721dac3891a16c2f705c90826b6e8c460e3e8f3 Mon Sep 17 00:00:00 2001 From: Jeremy Long Date: Mon, 8 May 2017 07:43:39 -0400 Subject: [PATCH] implemented CSV reports per #675 --- .../owasp/dependencycheck/taskdefs/Check.java | 4 +- .../org/owasp/dependencycheck/CliParser.java | 2 +- .../dependencycheck/reporting/EscapeTool.java | 69 +++++ .../reporting/ReportGenerator.java | 19 +- .../main/resources/templates/CsvReport.vsl | 27 ++ .../reporting/EscapeToolTest.java | 235 ++++++++++++++++++ .../maven/BaseDependencyCheckMojo.java | 4 + 7 files changed, 356 insertions(+), 4 deletions(-) create mode 100644 dependency-check-core/src/main/resources/templates/CsvReport.vsl create mode 100644 dependency-check-core/src/test/java/org/owasp/dependencycheck/reporting/EscapeToolTest.java diff --git a/dependency-check-ant/src/main/java/org/owasp/dependencycheck/taskdefs/Check.java b/dependency-check-ant/src/main/java/org/owasp/dependencycheck/taskdefs/Check.java index 5cc1e8226..d69b3ecbe 100644 --- a/dependency-check-ant/src/main/java/org/owasp/dependencycheck/taskdefs/Check.java +++ b/dependency-check-ant/src/main/java/org/owasp/dependencycheck/taskdefs/Check.java @@ -146,7 +146,7 @@ public class Check extends Update { private boolean updateOnly = false; /** - * The report format to be generated (HTML, XML, VULN, ALL). Default is + * The report format to be generated (HTML, XML, VULN, CSV, JSON, ALL). Default is * HTML. */ private String reportFormat = "HTML"; @@ -1102,7 +1102,7 @@ public class Check extends Update { } /** - * An enumeration of supported report formats: "ALL", "HTML", "XML", "VULN", + * An enumeration of supported report formats: "ALL", "HTML", "XML", "CSV", "JSON", "VULN", * etc.. */ public static class ReportFormats extends EnumeratedAttribute { diff --git a/dependency-check-cli/src/main/java/org/owasp/dependencycheck/CliParser.java b/dependency-check-cli/src/main/java/org/owasp/dependencycheck/CliParser.java index 6413017c1..c259e50d0 100644 --- a/dependency-check-cli/src/main/java/org/owasp/dependencycheck/CliParser.java +++ b/dependency-check-cli/src/main/java/org/owasp/dependencycheck/CliParser.java @@ -120,7 +120,7 @@ public final class CliParser { Format.valueOf(format); } catch (IllegalArgumentException ex) { final String msg = String.format("An invalid 'format' of '%s' was specified. " - + "Supported output formats are XML, JSON, HTML, VULN, or ALL", format); + + "Supported output formats are HTML, XML, CSV, JSON, VULN, or ALL", format); throw new ParseException(msg); } } diff --git a/dependency-check-core/src/main/java/org/owasp/dependencycheck/reporting/EscapeTool.java b/dependency-check-core/src/main/java/org/owasp/dependencycheck/reporting/EscapeTool.java index 43e0ea230..48014183a 100644 --- a/dependency-check-core/src/main/java/org/owasp/dependencycheck/reporting/EscapeTool.java +++ b/dependency-check-core/src/main/java/org/owasp/dependencycheck/reporting/EscapeTool.java @@ -19,7 +19,9 @@ package org.owasp.dependencycheck.reporting; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; +import java.util.Set; import org.apache.commons.lang3.StringEscapeUtils; +import org.owasp.dependencycheck.dependency.Identifier; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -94,4 +96,71 @@ public class EscapeTool { } return StringEscapeUtils.escapeJson(text); } + + /** + * Formats text for CSV format. This includes trimming whitespace, replace + * line breaks with spaces, and if necessary quotes the text and/or escapes + * contained quotes. + * + * @param text the text to escape and quote + * @return the escaped and quoted text + */ + public String csv(String text) { + if (text == null || text.isEmpty()) { + return text; + } + return StringEscapeUtils.escapeCsv(text.trim().replace("\n", " ")); + } + + /** + * Takes a set of Identifiers, filters them to none CPE, and formats them + * for display in a CSV. + * + * @param ids the set of identifiers + * @return the formated list of none CPE identifiers + */ + public String csvIdentifiers(Set ids) { + if (ids == null || ids.isEmpty()) { + return ""; + } + boolean addComma = false; + StringBuilder sb = new StringBuilder(); + for (Identifier id : ids) { + if (!"cpe".equals(id.getType())) { + if (addComma) { + sb.append(", "); + } else { + addComma = true; + } + sb.append(id.getValue()); + } + } + return StringEscapeUtils.escapeCsv(sb.toString()); + } + + /** + * Takes a set of Identifiers, filters them to just CPEs, and formats them + * for display in a CSV. + * + * @param ids the set of identifiers + * @return the formated list of CPE identifiers + */ + public String csvCpe(Set ids) { + if (ids == null || ids.isEmpty()) { + return ""; + } + boolean addComma = false; + StringBuilder sb = new StringBuilder(); + for (Identifier id : ids) { + if ("cpe".equals(id.getType())) { + if (addComma) { + sb.append(", "); + } else { + addComma = true; + } + sb.append(id.getValue()); + } + } + return StringEscapeUtils.escapeCsv(sb.toString()); + } } diff --git a/dependency-check-core/src/main/java/org/owasp/dependencycheck/reporting/ReportGenerator.java b/dependency-check-core/src/main/java/org/owasp/dependencycheck/reporting/ReportGenerator.java index da4fa5429..b8acd2557 100644 --- a/dependency-check-core/src/main/java/org/owasp/dependencycheck/reporting/ReportGenerator.java +++ b/dependency-check-core/src/main/java/org/owasp/dependencycheck/reporting/ReportGenerator.java @@ -81,7 +81,11 @@ public class ReportGenerator { /** * Generate JSON report. */ - JSON + JSON, + /** + * Generate CSV report. + */ + CSV } /** * The Velocity Engine. @@ -191,6 +195,9 @@ public class ReportGenerator { if (format == Format.JSON || format == Format.ALL) { generateReport("JsonReport", outputStream); } + if (format == Format.CSV || format == Format.ALL) { + generateReport("CsvReport", outputStream); + } } /** @@ -209,6 +216,9 @@ public class ReportGenerator { generateReport("JsonReport", outputDir + File.separator + "dependency-check-report.json"); pretifyJson(outputDir + File.separator + "dependency-check-report.json"); } + if (format == Format.CSV || format == Format.ALL) { + generateReport("CsvReport", outputDir + File.separator + "dependency-check-report.csv"); + } if (format == Format.HTML || format == Format.ALL) { generateReport("HtmlReport", outputDir + File.separator + "dependency-check-report.html"); } @@ -344,6 +354,13 @@ public class ReportGenerator { generateReports(outputDir, Format.JSON); } } + if ("CSV".equalsIgnoreCase(format)) { + if (pathToCheck.endsWith(".csv")) { + generateReport("CsvReport", outputDir); + } else { + generateReports(outputDir, Format.JSON); + } + } if ("ALL".equalsIgnoreCase(format)) { generateReports(outputDir, Format.ALL); } diff --git a/dependency-check-core/src/main/resources/templates/CsvReport.vsl b/dependency-check-core/src/main/resources/templates/CsvReport.vsl new file mode 100644 index 000000000..99c2758a1 --- /dev/null +++ b/dependency-check-core/src/main/resources/templates/CsvReport.vsl @@ -0,0 +1,27 @@ +#** +This file is part of Dependency-Check. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +Copyright (c) 2017 Jeremy Long. All Rights Reserved. + +@author Jeremy Long +@version 1 *### +"Project","ScanDate","DependencyName","DependencyPath","Description","License","Md5","Sha1","Identifiers","CPE","CVE","CWE","Vulnerability","Severity","CVSSv2" +#macro(writeSev $score)#if($score<4.0)"Low"#elseif($score>=7.0)"High"#else"Medium"#end#end +#foreach($dependency in $dependencies)#if($dependency.getVulnerabilities().size()>0) +#foreach($vuln in $dependency.getVulnerabilities()) +$enc.csv($applicationName),$enc.csv($scanDate),$enc.csv($dependency.DisplayFileName),#if($dependency.FilePath)$enc.csv($dependency.FilePath)#end,#if($dependency.description)$enc.csv($dependency.description)#end,#if($dependency.license)$enc.csv($dependency.license)#end,#if($dependency.Md5sum)$enc.csv($dependency.Md5sum)#end,#if($dependency.Sha1sum)$enc.csv($dependency.Sha1sum)#end,#if($dependency.identifiers)$enc.csvIdentifiers($dependency.identifiers)#end,#if($dependency.identifiers)$enc.csvCpe($dependency.identifiers)#end,#if($vuln.name)$enc.csv($vuln.name)#end,#if($dependency.cwe)$enc.csv($vuln.cwe)#end,#if($vuln.description)$enc.csv($vuln.description)#end,#writeSev($vuln.cvssScore),$vuln.cvssScore +#end +#end +#end \ No newline at end of file diff --git a/dependency-check-core/src/test/java/org/owasp/dependencycheck/reporting/EscapeToolTest.java b/dependency-check-core/src/test/java/org/owasp/dependencycheck/reporting/EscapeToolTest.java new file mode 100644 index 000000000..f49f060b4 --- /dev/null +++ b/dependency-check-core/src/test/java/org/owasp/dependencycheck/reporting/EscapeToolTest.java @@ -0,0 +1,235 @@ +/* + * This file is part of dependency-check-core. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Copyright (c) 2017 Jeremy Long. All Rights Reserved. + */ +package org.owasp.dependencycheck.reporting; + +import java.util.HashSet; +import java.util.Set; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import static org.junit.Assert.*; +import org.owasp.dependencycheck.dependency.Identifier; + +/** + * + * @author jerem + */ +public class EscapeToolTest { + + /** + * Test of url method, of class EscapeTool. + */ + @Test + public void testUrl() { + String text = null; + EscapeTool instance = new EscapeTool(); + String expResult = null; + String result = instance.url(text); + assertEquals(expResult, result); + + text = ""; + expResult = ""; + result = instance.url(text); + assertEquals(expResult, result); + + text = " "; + expResult = "+"; + result = instance.url(text); + assertEquals(expResult, result); + } + + /** + * Test of html method, of class EscapeTool. + */ + @Test + public void testHtml() { + EscapeTool instance = new EscapeTool(); + String text = null; + String expResult = null; + String result = instance.html(text); + assertEquals(expResult, result); + + text = ""; + expResult = ""; + result = instance.html(text); + assertEquals(expResult, result); + + text = "
"; + expResult = "<div>"; + result = instance.html(text); + assertEquals(expResult, result); + } + + /** + * Test of xml method, of class EscapeTool. + */ + @Test + public void testXml() { + EscapeTool instance = new EscapeTool(); + String text = null; + String expResult = null; + String result = instance.xml(text); + assertEquals(expResult, result); + + text = ""; + expResult = ""; + result = instance.xml(text); + assertEquals(expResult, result); + + text = "
"; + expResult = "<div>"; + result = instance.xml(text); + assertEquals(expResult, result); + } + + /** + * Test of json method, of class EscapeTool. + */ + @Test + public void testJson() { + String text = null; + EscapeTool instance = new EscapeTool(); + String expResult = null; + String result = instance.json(text); + assertEquals(expResult, result); + + text = ""; + expResult = ""; + result = instance.json(text); + assertEquals(expResult, result); + + text = "test \"quote\"\""; + expResult = "test \\\"quote\\\"\\\""; + result = instance.json(text); + assertEquals(expResult, result); + } + + /** + * Test of csv method, of class EscapeTool. + */ + @Test + public void testCsv() { + String text = null; + EscapeTool instance = new EscapeTool(); + String expResult = null; + String result = instance.csv(text); + assertEquals(expResult, result); + + text = ""; + expResult = ""; + result = instance.csv(text); + assertEquals(expResult, result); + + text = "one, two"; + expResult = "\"one, two\""; + result = instance.csv(text); + assertEquals(expResult, result); + } + + /** + * Test of csvIdentifiers method, of class EscapeTool. + */ + @Test + public void testCsvIdentifiers() { + EscapeTool instance = new EscapeTool(); + Set ids = null; + String expResult = ""; + String result = instance.csvIdentifiers(ids); + assertEquals(expResult, result); + + ids = new HashSet<>(); + expResult = ""; + result = instance.csvIdentifiers(ids); + assertEquals(expResult, result); + + ids = new HashSet<>(); + ids.add(new Identifier("cpe", "cpe:/a:somegroup:something:1.0", "")); + expResult = ""; + result = instance.csvIdentifiers(ids); + assertEquals(expResult, result); + + ids = new HashSet<>(); + ids.add(new Identifier("gav", "somegroup:something:1.0", "")); + expResult = "somegroup:something:1.0"; + result = instance.csvIdentifiers(ids); + assertEquals(expResult, result); + + ids = new HashSet<>(); + ids.add(new Identifier("cpe", "cpe:/a:somegroup:something:1.0", "")); + ids.add(new Identifier("gav", "somegroup:something:1.0", "")); + expResult = "somegroup:something:1.0"; + result = instance.csvIdentifiers(ids); + assertEquals(expResult, result); + + ids = new HashSet<>(); + ids.add(new Identifier("cpe", "cpe:/a:somegroup:something:1.0", "")); + ids.add(new Identifier("gav", "somegroup:something:1.0", "")); + ids.add(new Identifier("gav", "somegroup2:something:1.2", "")); + expResult = "\"somegroup:something:1.0, somegroup2:something:1.2\""; + String expResult2 = "\"somegroup2:something:1.2, somegroup:something:1.0\""; + result = instance.csvIdentifiers(ids); + assertTrue(expResult.equals(result) || expResult2.equals(result)); + } + + /** + * Test of csvCpe method, of class EscapeTool. + */ + @Test + public void testCsvCpe() { + EscapeTool instance = new EscapeTool(); + Set ids = null; + String expResult = ""; + String result = instance.csvCpe(ids); + assertEquals(expResult, result); + + ids = new HashSet<>(); + expResult = ""; + result = instance.csvCpe(ids); + assertEquals(expResult, result); + + ids = new HashSet<>(); + ids.add(new Identifier("gav", "somegroup:something:1.0", "")); + expResult = ""; + result = instance.csvCpe(ids); + assertEquals(expResult, result); + + ids = new HashSet<>(); + ids.add(new Identifier("cpe", "cpe:/a:somegroup:something:1.0", "")); + expResult = "cpe:/a:somegroup:something:1.0"; + result = instance.csvCpe(ids); + assertEquals(expResult, result); + + ids = new HashSet<>(); + ids.add(new Identifier("cpe", "cpe:/a:somegroup:something:1.0", "")); + ids.add(new Identifier("gav", "somegroup:something:1.0", "")); + expResult = "cpe:/a:somegroup:something:1.0"; + result = instance.csvCpe(ids); + assertEquals(expResult, result); + + ids = new HashSet<>(); + ids.add(new Identifier("cpe", "cpe:/a:somegroup:something:1.0", "")); + ids.add(new Identifier("gav", "somegroup:something:1.0", "")); + ids.add(new Identifier("cpe", "cpe:/a:somegroup2:something:1.2", "")); + expResult = "\"cpe:/a:somegroup:something:1.0, cpe:/a:somegroup2:something:1.2\""; + String expResult2 = "\"cpe:/a:somegroup2:something:1.2, cpe:/a:somegroup:something:1.0\""; + result = instance.csvCpe(ids); + assertTrue(expResult.equals(result) || expResult2.equals(result)); + } +} diff --git a/dependency-check-maven/src/main/java/org/owasp/dependencycheck/maven/BaseDependencyCheckMojo.java b/dependency-check-maven/src/main/java/org/owasp/dependencycheck/maven/BaseDependencyCheckMojo.java index 1a923a698..9dc7dc799 100644 --- a/dependency-check-maven/src/main/java/org/owasp/dependencycheck/maven/BaseDependencyCheckMojo.java +++ b/dependency-check-maven/src/main/java/org/owasp/dependencycheck/maven/BaseDependencyCheckMojo.java @@ -757,6 +757,10 @@ public abstract class BaseDependencyCheckMojo extends AbstractMojo implements Ma return "dependency-check-report.xml#"; } else if ("VULN".equalsIgnoreCase(this.format)) { return "dependency-check-vulnerability"; + } else if ("JSON".equalsIgnoreCase(this.format)) { + return "dependency-check-report.json"; + } else if ("CSV".equalsIgnoreCase(this.format)) { + return "dependency-check-report.csv"; } else { getLog().warn("Unknown report format used during site generation."); return "dependency-check-report";