diff --git a/dependency-check-ant/pom.xml b/dependency-check-ant/pom.xml index 12d183bb0..2e40f5da4 100644 --- a/dependency-check-ant/pom.xml +++ b/dependency-check-ant/pom.xml @@ -21,7 +21,7 @@ Copyright (c) 2013 - Jeremy Long. All Rights Reserved. org.owasp dependency-check-parent - 1.2.4-SNAPSHOT + 1.2.5-SNAPSHOT dependency-check-ant diff --git a/dependency-check-cli/pom.xml b/dependency-check-cli/pom.xml index 8af381366..fd6ee5220 100644 --- a/dependency-check-cli/pom.xml +++ b/dependency-check-cli/pom.xml @@ -21,7 +21,7 @@ Copyright (c) 2012 - Jeremy Long. All Rights Reserved. org.owasp dependency-check-parent - 1.2.4-SNAPSHOT + 1.2.5-SNAPSHOT dependency-check-cli diff --git a/dependency-check-core/pom.xml b/dependency-check-core/pom.xml index e760639d4..ef9aae6cb 100644 --- a/dependency-check-core/pom.xml +++ b/dependency-check-core/pom.xml @@ -20,7 +20,7 @@ Copyright (c) 2012 Jeremy Long. All Rights Reserved. org.owasp dependency-check-parent - 1.2.4-SNAPSHOT + 1.2.5-SNAPSHOT dependency-check-core @@ -621,6 +621,13 @@ Copyright (c) 2012 Jeremy Long. All Rights Reserved. provided true + + org.springframework.retry + spring-retry + 1.1.0.RELEASE + provided + true + diff --git a/dependency-check-core/src/main/java/org/owasp/dependencycheck/Engine.java b/dependency-check-core/src/main/java/org/owasp/dependencycheck/Engine.java index 09f12d9ba..3dfb16489 100644 --- a/dependency-check-core/src/main/java/org/owasp/dependencycheck/Engine.java +++ b/dependency-check-core/src/main/java/org/owasp/dependencycheck/Engine.java @@ -18,6 +18,7 @@ package org.owasp.dependencycheck; import java.io.File; +import java.io.Serializable; import java.util.ArrayList; import java.util.EnumMap; import java.util.HashSet; @@ -52,7 +53,7 @@ import org.owasp.dependencycheck.utils.Settings; * * @author Jeremy Long */ -public class Engine { +public class Engine implements Serializable { /** * The list of dependencies. @@ -61,19 +62,19 @@ public class Engine { /** * A Map of analyzers grouped by Analysis phase. */ - private final EnumMap> analyzers; + private transient final EnumMap> analyzers; /** * A Map of analyzers grouped by Analysis phase. */ - private final Set fileTypeAnalyzers; + private transient final Set fileTypeAnalyzers; /** * The ClassLoader to use when dynamically loading Analyzer and Update services. */ - private ClassLoader serviceClassLoader; + private transient ClassLoader serviceClassLoader; /** * The Logger for use throughout the class. */ - private static final Logger LOGGER = Logger.getLogger(Engine.class.getName()); + private transient static final Logger LOGGER = Logger.getLogger(Engine.class.getName()); /** * Creates a new Engine. diff --git a/dependency-check-core/src/main/java/org/owasp/dependencycheck/analyzer/CPEAnalyzer.java b/dependency-check-core/src/main/java/org/owasp/dependencycheck/analyzer/CPEAnalyzer.java index bb75da624..97779a515 100644 --- a/dependency-check-core/src/main/java/org/owasp/dependencycheck/analyzer/CPEAnalyzer.java +++ b/dependency-check-core/src/main/java/org/owasp/dependencycheck/analyzer/CPEAnalyzer.java @@ -170,29 +170,10 @@ public class CPEAnalyzer implements Analyzer { * @throws ParseException is thrown when the Lucene query cannot be parsed. */ protected void determineCPE(Dependency dependency) throws CorruptIndexException, IOException, ParseException { - Confidence confidence = Confidence.HIGHEST; - - String vendors = addEvidenceWithoutDuplicateTerms("", dependency.getVendorEvidence(), confidence); - String products = addEvidenceWithoutDuplicateTerms("", dependency.getProductEvidence(), confidence); - /* bug fix for #40 - version evidence is not showing up as "used" in the reports if there is no - * CPE identified. As such, we are "using" the evidence and ignoring the results. */ - addEvidenceWithoutDuplicateTerms("", dependency.getVersionEvidence(), confidence); - - int ctr = 0; - do { - if (!vendors.isEmpty() && !products.isEmpty()) { - final List entries = searchCPE(vendors, products, dependency.getProductEvidence().getWeighting(), - dependency.getVendorEvidence().getWeighting()); - - for (IndexEntry e : entries) { - if (verifyEntry(e, dependency)) { - final String vendor = e.getVendor(); - final String product = e.getProduct(); - determineIdentifiers(dependency, vendor, product); - } - } - } - confidence = reduceConfidence(confidence); + //TODO test dojo-war against this. we shold get dojo-toolkit:dojo-toolkit AND dojo-toolkit:toolkit + String vendors = ""; + String products = ""; + for (Confidence confidence : Confidence.values()) { if (dependency.getVendorEvidence().contains(confidence)) { vendors = addEvidenceWithoutDuplicateTerms(vendors, dependency.getVendorEvidence(), confidence); } @@ -201,10 +182,26 @@ public class CPEAnalyzer implements Analyzer { } /* bug fix for #40 - version evidence is not showing up as "used" in the reports if there is no * CPE identified. As such, we are "using" the evidence and ignoring the results. */ - if (dependency.getVersionEvidence().contains(confidence)) { - addEvidenceWithoutDuplicateTerms("", dependency.getVersionEvidence(), confidence); +// if (dependency.getVersionEvidence().contains(confidence)) { +// addEvidenceWithoutDuplicateTerms("", dependency.getVersionEvidence(), confidence); +// } + if (!vendors.isEmpty() && !products.isEmpty()) { + final List entries = searchCPE(vendors, products, dependency.getProductEvidence().getWeighting(), + dependency.getVendorEvidence().getWeighting()); + + boolean identifierAdded = false; + for (IndexEntry e : entries) { + if (verifyEntry(e, dependency)) { + final String vendor = e.getVendor(); + final String product = e.getProduct(); + identifierAdded |= determineIdentifiers(dependency, vendor, product, confidence); + } + } + if (identifierAdded) { + break; + } } - } while ((++ctr) < 4); + } } /** @@ -239,22 +236,6 @@ public class CPEAnalyzer implements Analyzer { return sb.toString().trim(); } - /** - * Reduces the given confidence by one level. This returns LOW if the confidence passed in is not HIGH. - * - * @param c the confidence to reduce. - * @return One less then the confidence passed in. - */ - private Confidence reduceConfidence(final Confidence c) { - if (c == Confidence.HIGHEST) { - return Confidence.HIGH; - } else if (c == Confidence.HIGH) { - return Confidence.MEDIUM; - } else { - return Confidence.LOW; - } - } - /** *

* Searches the Lucene CPE index to identify possible CPE entries associated with the supplied vendor, product, and @@ -508,14 +489,19 @@ public class CPEAnalyzer implements Analyzer { * @param dependency the Dependency being analyzed * @param vendor the vendor for the CPE being analyzed * @param product the product for the CPE being analyzed + * @return true if an identifier was added to the dependency; otherwise false * @throws UnsupportedEncodingException is thrown if UTF-8 is not supported */ - private void determineIdentifiers(Dependency dependency, String vendor, String product) throws UnsupportedEncodingException { + private boolean determineIdentifiers(Dependency dependency, String vendor, String product, Confidence currentConfidence) throws UnsupportedEncodingException { final Set cpes = cve.getCPEs(vendor, product); DependencyVersion bestGuess = new DependencyVersion("-"); Confidence bestGuessConf = null; + boolean hasBroadMatch = false; final List collected = new ArrayList(); for (Confidence conf : Confidence.values()) { +// if (conf.compareTo(currentConfidence) > 0) { +// break; +// } for (Evidence evidence : dependency.getVersionEvidence().iterator(conf)) { final DependencyVersion evVer = DependencyVersionUtil.parseVersion(evidence.getValue()); if (evVer == null) { @@ -528,9 +514,12 @@ public class CPEAnalyzer implements Analyzer { } else { dbVer = DependencyVersionUtil.parseVersion(vs.getVersion()); } - if (dbVer == null //special case, no version specified - everything is vulnerable - || evVer.equals(dbVer)) { //yeah! exact match - + if (dbVer == null) { //special case, no version specified - everything is vulnerable + hasBroadMatch = true; + final String url = String.format(NVD_SEARCH_URL, URLEncoder.encode(vs.getName(), "UTF-8")); + final IdentifierMatch match = new IdentifierMatch("cpe", vs.getName(), url, IdentifierConfidence.BROAD_MATCH, conf); + collected.add(match); + } else if (evVer.equals(dbVer)) { //yeah! exact match final String url = String.format(NVD_SEARCH_URL, URLEncoder.encode(vs.getName(), "UTF-8")); final IdentifierMatch match = new IdentifierMatch("cpe", vs.getName(), url, IdentifierConfidence.EXACT_MATCH, conf); collected.add(match); @@ -556,7 +545,11 @@ public class CPEAnalyzer implements Analyzer { } } final String cpeName = String.format("cpe:/a:%s:%s:%s", vendor, product, bestGuess.toString()); - final String url = null; + String url = null; + if (hasBroadMatch) { //if we have a broad match we can add the URL to the best guess. + final String cpeUrlName = String.format("cpe:/a:%s:%s", vendor, product); + url = String.format(NVD_SEARCH_URL, URLEncoder.encode(cpeUrlName, "UTF-8")); + } if (bestGuessConf == null) { bestGuessConf = Confidence.LOW; } @@ -566,6 +559,7 @@ public class CPEAnalyzer implements Analyzer { Collections.sort(collected); final IdentifierConfidence bestIdentifierQuality = collected.get(0).getConfidence(); final Confidence bestEvidenceQuality = collected.get(0).getEvidenceConfidence(); + boolean identifierAdded = false; for (IdentifierMatch m : collected) { if (bestIdentifierQuality.equals(m.getConfidence()) && bestEvidenceQuality.equals(m.getEvidenceConfidence())) { @@ -576,8 +570,10 @@ public class CPEAnalyzer implements Analyzer { i.setConfidence(bestEvidenceQuality); } dependency.addIdentifier(i); + identifierAdded = true; } } + return identifierAdded; } /** @@ -592,7 +588,12 @@ public class CPEAnalyzer implements Analyzer { /** * A best guess for the CPE. */ - BEST_GUESS + BEST_GUESS, + /** + * The entire vendor/product group must be added (without a guess at version) because there is a CVE with a VS + * that only specifies vendor/product. + */ + BROAD_MATCH } /** diff --git a/dependency-check-core/src/main/java/org/owasp/dependencycheck/analyzer/FalsePositiveAnalyzer.java b/dependency-check-core/src/main/java/org/owasp/dependencycheck/analyzer/FalsePositiveAnalyzer.java index 20f8c3f41..8f592e73a 100644 --- a/dependency-check-core/src/main/java/org/owasp/dependencycheck/analyzer/FalsePositiveAnalyzer.java +++ b/dependency-check-core/src/main/java/org/owasp/dependencycheck/analyzer/FalsePositiveAnalyzer.java @@ -86,12 +86,41 @@ public class FalsePositiveAnalyzer extends AbstractAnalyzer { public void analyze(Dependency dependency, Engine engine) throws AnalysisException { removeJreEntries(dependency); removeBadMatches(dependency); + removeBadSpringMatches(dependency); removeWrongVersionMatches(dependency); removeSpuriousCPE(dependency); removeDuplicativeEntriesFromJar(dependency, engine); addFalseNegativeCPEs(dependency); } + private void removeBadSpringMatches(Dependency dependency) { + String mustContain = null; + for (Identifier i : dependency.getIdentifiers()) { + if ("maven".contains(i.getType())) { + if (i.getValue() != null && i.getValue().startsWith("org.springframework.")) { + int endPoint = i.getValue().indexOf(":", 19); + if (endPoint >= 0) { + mustContain = i.getValue().substring(19, endPoint).toLowerCase(); + break; + } + } + } + } + if (mustContain != null) { + Iterator itr = dependency.getIdentifiers().iterator(); + while (itr.hasNext()) { + Identifier i = itr.next(); + if ("cpe".contains(i.getType()) + && i.getValue() != null + && i.getValue().startsWith("cpe:/a:springsource:") + && !i.getValue().toLowerCase().contains(mustContain)) { + dependency.getIdentifiers().remove(i); + } + + } + } + } + /** *

* Intended to remove spurious CPE entries. By spurious we mean duplicate, less specific CPE entries.

diff --git a/dependency-check-core/src/main/java/org/owasp/dependencycheck/analyzer/JarAnalyzer.java b/dependency-check-core/src/main/java/org/owasp/dependencycheck/analyzer/JarAnalyzer.java index 2895bf6e2..97e934840 100644 --- a/dependency-check-core/src/main/java/org/owasp/dependencycheck/analyzer/JarAnalyzer.java +++ b/dependency-check-core/src/main/java/org/owasp/dependencycheck/analyzer/JarAnalyzer.java @@ -587,7 +587,7 @@ public class JarAnalyzer extends AbstractFileTypeAnalyzer { groupid = groupid.substring(4); } foundSomething = true; - dependency.getVendorEvidence().addEvidence("pom", "groupid", groupid, Confidence.HIGH); + dependency.getVendorEvidence().addEvidence("pom", "groupid", groupid, Confidence.HIGHEST); dependency.getProductEvidence().addEvidence("pom", "groupid", groupid, Confidence.LOW); addMatchingValues(classes, groupid, dependency.getVendorEvidence()); addMatchingValues(classes, groupid, dependency.getProductEvidence()); @@ -616,7 +616,7 @@ public class JarAnalyzer extends AbstractFileTypeAnalyzer { artifactid = artifactid.substring(4); } foundSomething = true; - dependency.getProductEvidence().addEvidence("pom", "artifactid", artifactid, Confidence.HIGH); + dependency.getProductEvidence().addEvidence("pom", "artifactid", artifactid, Confidence.HIGHEST); dependency.getVendorEvidence().addEvidence("pom", "artifactid", artifactid, Confidence.LOW); addMatchingValues(classes, artifactid, dependency.getVendorEvidence()); addMatchingValues(classes, artifactid, dependency.getProductEvidence()); diff --git a/dependency-check-core/src/main/java/org/owasp/dependencycheck/dependency/Dependency.java b/dependency-check-core/src/main/java/org/owasp/dependencycheck/dependency/Dependency.java index 08ddd9819..9ce32d38d 100644 --- a/dependency-check-core/src/main/java/org/owasp/dependencycheck/dependency/Dependency.java +++ b/dependency-check-core/src/main/java/org/owasp/dependencycheck/dependency/Dependency.java @@ -19,6 +19,7 @@ package org.owasp.dependencycheck.dependency; import java.io.File; import java.io.IOException; +import java.io.Serializable; import java.security.NoSuchAlgorithmException; import java.util.Set; import java.util.SortedSet; @@ -35,7 +36,7 @@ import org.owasp.dependencycheck.utils.FileUtils; * * @author Jeremy Long */ -public class Dependency implements Comparable { +public class Dependency implements Serializable, Comparable { /** * The logger. diff --git a/dependency-check-core/src/main/java/org/owasp/dependencycheck/dependency/Evidence.java b/dependency-check-core/src/main/java/org/owasp/dependencycheck/dependency/Evidence.java index 6b8ad8bd6..594ffb613 100644 --- a/dependency-check-core/src/main/java/org/owasp/dependencycheck/dependency/Evidence.java +++ b/dependency-check-core/src/main/java/org/owasp/dependencycheck/dependency/Evidence.java @@ -17,12 +17,14 @@ */ package org.owasp.dependencycheck.dependency; +import java.io.Serializable; + /** * Evidence is a piece of information about a Dependency. * * @author Jeremy Long */ -public class Evidence implements Comparable { +public class Evidence implements Serializable, Comparable { /** * Creates a new Evidence object. diff --git a/dependency-check-core/src/main/java/org/owasp/dependencycheck/dependency/EvidenceCollection.java b/dependency-check-core/src/main/java/org/owasp/dependencycheck/dependency/EvidenceCollection.java index 141370ab1..eae3f9750 100644 --- a/dependency-check-core/src/main/java/org/owasp/dependencycheck/dependency/EvidenceCollection.java +++ b/dependency-check-core/src/main/java/org/owasp/dependencycheck/dependency/EvidenceCollection.java @@ -17,6 +17,7 @@ */ package org.owasp.dependencycheck.dependency; +import java.io.Serializable; import java.net.MalformedURLException; import java.util.HashSet; import java.util.Iterator; @@ -36,7 +37,7 @@ import org.owasp.dependencycheck.utils.UrlStringUtils; * * @author Jeremy Long */ -public class EvidenceCollection implements Iterable { +public class EvidenceCollection implements Serializable, Iterable { /** * The logger. diff --git a/dependency-check-core/src/main/java/org/owasp/dependencycheck/dependency/Identifier.java b/dependency-check-core/src/main/java/org/owasp/dependencycheck/dependency/Identifier.java index 6e44ec613..3f64b2faa 100644 --- a/dependency-check-core/src/main/java/org/owasp/dependencycheck/dependency/Identifier.java +++ b/dependency-check-core/src/main/java/org/owasp/dependencycheck/dependency/Identifier.java @@ -17,11 +17,13 @@ */ package org.owasp.dependencycheck.dependency; +import java.io.Serializable; + /** * * @author Jeremy Long */ -public class Identifier implements Comparable { +public class Identifier implements Serializable, Comparable { /** * Constructs a new Identifier with the specified data. diff --git a/dependency-check-core/src/main/java/org/owasp/dependencycheck/utils/DependencyVersion.java b/dependency-check-core/src/main/java/org/owasp/dependencycheck/utils/DependencyVersion.java index a11ca9123..b0ba88311 100644 --- a/dependency-check-core/src/main/java/org/owasp/dependencycheck/utils/DependencyVersion.java +++ b/dependency-check-core/src/main/java/org/owasp/dependencycheck/utils/DependencyVersion.java @@ -189,17 +189,23 @@ public class DependencyVersion implements Iterable, Comparable 3) { - max = 3; + if (Math.abs(this.versionParts.size() - version.versionParts.size()) >= 3) { + return false; } + final int max = (this.versionParts.size() < version.versionParts.size()) + ? this.versionParts.size() : version.versionParts.size(); + + boolean ret = true; for (int i = 0; i < max; i++) { - if (this.versionParts.get(i) == null || !this.versionParts.get(i).equals(version.versionParts.get(i))) { + String thisVersion = this.versionParts.get(i); + String otherVersion = version.getVersionParts().get(i); + if (i >= 3) { + if (thisVersion.compareToIgnoreCase(otherVersion) >= 0) { + ret = false; + break; + } + } else if (!thisVersion.equals(otherVersion)) { ret = false; break; } diff --git a/dependency-check-core/src/main/resources/dependencycheck-base-suppression.xml b/dependency-check-core/src/main/resources/dependencycheck-base-suppression.xml index 3f469b64d..24bdb883a 100644 --- a/dependency-check-core/src/main/resources/dependencycheck-base-suppression.xml +++ b/dependency-check-core/src/main/resources/dependencycheck-base-suppression.xml @@ -17,4 +17,18 @@ com\.thoughtworks\.xstream:xstream:.* cpe:/a:springsource:spring_framework + + + org.apache.velocity:velocity-tools:.* + cpe:/a:apache:struts + + + + .*\.(jar|dll|exe|ear|war|pom) + cpe:/a:sandbox:sandbox + \ No newline at end of file diff --git a/dependency-check-core/src/main/resources/templates/HtmlReport.vsl b/dependency-check-core/src/main/resources/templates/HtmlReport.vsl index 52062ca6f..6c6d88120 100644 --- a/dependency-check-core/src/main/resources/templates/HtmlReport.vsl +++ b/dependency-check-core/src/main/resources/templates/HtmlReport.vsl @@ -33,8 +33,8 @@ Copyright (c) 2012 Jeremy Long. All Rights Reserved. "); - sink.section1(); - sink.sectionTitle1(); - sink.text("Project: " + projectName); - sink.sectionTitle1_(); - sink.date(); - final Date now = new Date(); - sink.text(DateFormat.getDateTimeInstance().format(now)); - sink.date_(); - sink.section1_(); - } - // - - /** - * Returns the maven settings proxy server. - * - * @param proxy the maven proxy - * @return the proxy url - */ - private String getMavenSettingsProxyServer(Proxy proxy) { - return new StringBuilder(proxy.getProtocol()).append("://").append(proxy.getHost()).toString(); - } - - /** - * Returns the maven proxy. - * - * @return the maven proxy - */ - private Proxy getMavenProxy() { - if (mavenSettings != null) { - final List proxies = mavenSettings.getProxies(); - if (proxies != null && proxies.size() > 0) { - if (mavenSettingsProxyId != null) { - for (Proxy proxy : proxies) { - if (mavenSettingsProxyId.equalsIgnoreCase(proxy.getId())) { - return proxy; - } - } - } else if (proxies.size() == 1) { - return proxies.get(0); - } else { - throw new IllegalStateException("Ambiguous proxy definition"); - } - } - } - return null; + return false; } + // /** * Takes the properties supplied and updates the dependency-check settings. Additionally, this sets the system * properties required to change the proxy url, port, and connection timeout. @@ -796,34 +377,41 @@ public class DependencyCheckMojo extends AbstractMojo implements MavenMultiPageR mojoProperties = this.getClass().getClassLoader().getResourceAsStream(PROPERTIES_FILE); Settings.mergeProperties(mojoProperties); } catch (IOException ex) { - logger.log(Level.WARNING, "Unable to load the dependency-check ant task.properties file."); - logger.log(Level.FINE, null, ex); + LOGGER.log(Level.WARNING, "Unable to load the dependency-check ant task.properties file."); + LOGGER.log(Level.FINE, null, ex); } finally { if (mojoProperties != null) { try { mojoProperties.close(); } catch (IOException ex) { - logger.log(Level.FINEST, null, ex); + LOGGER.log(Level.FINEST, null, ex); } } } Settings.setBoolean(Settings.KEYS.AUTO_UPDATE, autoUpdate); - - if (proxyUrl != null && !proxyUrl.isEmpty()) { - logger.warning("Deprecated configuration detected, proxyUrl will be ignored; use the maven settings to configure the proxy instead"); + if (externalReport != null) { + LOGGER.warning("The 'externalReport' option was set; this configuration option has been removed. " + + "Please update the dependency-check-maven plugin's configuration"); } + if (proxyUrl != null && !proxyUrl.isEmpty()) { + LOGGER.warning("Deprecated configuration detected, proxyUrl will be ignored; use the maven settings " + + "to configure the proxy instead"); + } final Proxy proxy = getMavenProxy(); if (proxy != null) { - Settings.setString(Settings.KEYS.PROXY_SERVER, getMavenSettingsProxyServer(proxy)); + Settings.setString(Settings.KEYS.PROXY_SERVER, proxy.getHost()); Settings.setString(Settings.KEYS.PROXY_PORT, Integer.toString(proxy.getPort())); final String userName = proxy.getUsername(); final String password = proxy.getPassword(); - if (userName != null && password != null) { + if (userName != null) { Settings.setString(Settings.KEYS.PROXY_USERNAME, userName); + } + if (password != null) { Settings.setString(Settings.KEYS.PROXY_PASSWORD, password); } + } if (connectionTimeout != null && !connectionTimeout.isEmpty()) { @@ -894,20 +482,47 @@ public class DependencyCheckMojo extends AbstractMojo implements MavenMultiPageR if (cveUrl20Base != null && !cveUrl20Base.isEmpty()) { Settings.setString(Settings.KEYS.CVE_SCHEMA_2_0, cveUrl20Base); } - } + /** + * Returns the maven proxy. + * + * @return the maven proxy + */ + private Proxy getMavenProxy() { + if (mavenSettings != null) { + final List proxies = mavenSettings.getProxies(); + if (proxies != null && proxies.size() > 0) { + if (mavenSettingsProxyId != null) { + for (Proxy proxy : proxies) { + if (mavenSettingsProxyId.equalsIgnoreCase(proxy.getId())) { + return proxy; + } + } + } else if (proxies.size() == 1) { + return proxies.get(0); + } else { + LOGGER.warning("Multiple proxy defentiions exist in the Maven settings. In the dependency-check " + + "configuration set the maveSettingsProxyId so that the correct proxy will be used."); + throw new IllegalStateException("Ambiguous proxy definition"); + } + } + } + return null; + } + // + /** * Executes the dependency-check and generates the report. * * @throws MojoExecutionException if a maven exception occurs * @throws MojoFailureException thrown if a CVSS score is found that is higher then the configured level */ - public void execute() throws MojoExecutionException, MojoFailureException { - Engine engine = null; + @Override + protected void performExecute() throws MojoExecutionException, MojoFailureException { try { engine = executeDependencyCheck(); - generateExternalReports(engine, outputDirectory); + ReportingUtil.generateExternalReports(engine, outputDirectory, getProject().getName(), format); if (this.showSummary) { showSummary(engine.getDependencies()); } @@ -915,59 +530,116 @@ public class DependencyCheckMojo extends AbstractMojo implements MavenMultiPageR checkForFailure(engine.getDependencies()); } } catch (DatabaseException ex) { - logger.log(Level.SEVERE, + LOGGER.log(Level.SEVERE, "Unable to connect to the dependency-check database; analysis has stopped"); - logger.log(Level.FINE, "", ex); + LOGGER.log(Level.FINE, "", ex); + } + } + + @Override + protected void postExecute() throws MojoExecutionException, MojoFailureException { + try { + super.postExecute(); } finally { - Settings.cleanup(true); - if (engine != null) { - engine.cleanup(); - } + cleanupEngine(); + } + } + + @Override + protected void postGenerate() throws MavenReportException { + try { + super.postGenerate(); + } finally { + cleanupEngine(); } } /** - * Generates the Dependency-Check Site Report. - * - * @param sink the sink to write the report to - * @param locale the locale to use when generating the report - * @throws MavenReportException if a Maven report exception occurs + * Calls engine.cleanup() to release resources. */ - public void generate(@SuppressWarnings("deprecation") org.codehaus.doxia.sink.Sink sink, - Locale locale) throws MavenReportException { - generate((Sink) sink, null, locale); + private void cleanupEngine() { + if (engine != null) { + engine.cleanup(); + engine = null; + } + Settings.cleanup(true); } /** * Generates the Dependency-Check Site Report. * - * @param sink the sink to write the report to - * @param sinkFactory the sink factory * @param locale the locale to use when generating the report * @throws MavenReportException if a maven report exception occurs */ - public void generate(Sink sink, SinkFactory sinkFactory, Locale locale) throws MavenReportException { - Engine engine = null; - try { - engine = executeDependencyCheck(); - if (this.externalReport) { - generateExternalReports(engine, reportOutputDirectory); + @Override + protected void executeNonAggregateReport(Locale locale) throws MavenReportException { + + final List deps = readDataFile(); + if (deps != null) { + try { + engine = initializeEngine(); + engine.getDependencies().addAll(deps); + } catch (DatabaseException ex) { + final String msg = String.format("An unrecoverable exception with the dependency-check initialization occured while scanning %s", + getProject().getName()); + throw new MavenReportException(msg, ex); + } + } else { + try { + engine = executeDependencyCheck(); + } catch (DatabaseException ex) { + final String msg = String.format("An unrecoverable exception with the dependency-check scan occured while scanning %s", + getProject().getName()); + throw new MavenReportException(msg, ex); + } + } + ReportingUtil.generateExternalReports(engine, getReportOutputDirectory(), getProject().getName(), format); + } + + @Override + protected void executeAggregateReport(MavenProject project, Locale locale) throws MavenReportException { + List deps = readDataFile(project); + if (deps != null) { + try { + engine = initializeEngine(); + engine.getDependencies().addAll(deps); + } catch (DatabaseException ex) { + final String msg = String.format("An unrecoverable exception with the dependency-check initialization occured while scanning %s", + project.getName()); + throw new MavenReportException(msg, ex); + } + } else { + try { + engine = executeDependencyCheck(project); + } catch (DatabaseException ex) { + final String msg = String.format("An unrecoverable exception with the dependency-check scan occured while scanning %s", + project.getName()); + throw new MavenReportException(msg, ex); + } + } + for (MavenProject child : getAllChildren(project)) { + deps = readDataFile(child); + if (deps == null) { + final String msg = String.format("Unable to include information on %s in the dependency-check aggregate report", child.getName()); + LOGGER.severe(msg); } else { - generateMavenSiteReport(engine, sink); - } - } catch (DatabaseException ex) { - logger.log(Level.SEVERE, - "Unable to connect to the dependency-check database; analysis has stopped"); - logger.log(Level.FINE, "", ex); - } finally { - Settings.cleanup(true); - if (engine != null) { - engine.cleanup(); + engine.getDependencies().addAll(deps); } } + final DependencyBundlingAnalyzer bundler = new DependencyBundlingAnalyzer(); + try { + bundler.analyze(null, engine); + } catch (AnalysisException ex) { + LOGGER.log(Level.WARNING, "An error occured grouping the dependencies; duplicate entries may exist in the report", ex); + LOGGER.log(Level.FINE, "Bundling Exception", ex); + } + final File outputDir = getReportOutputDirectory(project); + if (outputDir != null) { + ReportingUtil.generateExternalReports(engine, outputDir, project.getName(), format); + } } - // + // /** * Returns the output name. * @@ -982,7 +654,7 @@ public class DependencyCheckMojo extends AbstractMojo implements MavenMultiPageR } else if ("VULN".equalsIgnoreCase(this.format)) { return "dependency-check-vulnerability"; } else { - logger.log(Level.WARNING, "Unknown report format used during site generation."); + LOGGER.log(Level.WARNING, "Unknown report format used during site generation."); return "dependency-check-report"; } } @@ -1003,25 +675,7 @@ public class DependencyCheckMojo extends AbstractMojo implements MavenMultiPageR * @return the report name */ public String getName(Locale locale) { - return name; - } - - /** - * Sets the Reporting output directory. - * - * @param directory the output directory - */ - public void setReportOutputDirectory(File directory) { - reportOutputDirectory = directory; - } - - /** - * Returns the output directory. - * - * @return the output directory - */ - public File getReportOutputDirectory() { - return reportOutputDirectory; + return "dependency-check"; } /** @@ -1031,28 +685,64 @@ public class DependencyCheckMojo extends AbstractMojo implements MavenMultiPageR * @return the description */ public String getDescription(Locale locale) { - return description; + return "A report providing details on any published " + + "vulnerabilities within project dependencies. This report is a best effort but may contain " + + "false positives and false negatives."; } /** - * Returns whether this is an external report. + * Returns whether or not a report can be generated. * - * @return true or false; - */ - public boolean isExternalReport() { - return externalReport; - } - - /** - * Returns whether or not the plugin can generate a report. - * - * @return true + * @return true if a report can be generated; otherwise false */ public boolean canGenerateReport() { - return true; + if (canGenerateAggregateReport() || (isAggregate() && isMultiModule())) { + return true; + } + if (canGenerateNonAggregateReport()) { + return true; + } else { + final String msg; + if (getProject().getArtifacts().size() > 0) { + msg = "No project dependencies exist in the included scope - dependency-check:check is unable to generate a report."; + } else { + msg = "No project dependencies exist - dependency-check:check is unable to generate a report."; + } + LOGGER.warning(msg); + } + + return false; + } + + /** + * Returns whether or not a non-aggregate report can be generated. + * + * @return true if a non-aggregate report can be generated; otherwise false + */ + @Override + protected boolean canGenerateNonAggregateReport() { + boolean ability = false; + for (Artifact a : getProject().getArtifacts()) { + if (!excludeFromScan(a)) { + ability = true; + break; + } + } + return ability; + } + + /** + * Returns whether or not an aggregate report can be generated. + * + * @return true if an aggregate report can be generated; otherwise false + */ + @Override + protected boolean canGenerateAggregateReport() { + return isAggregate() && isLastProject(); } // + // /** * Checks to see if a vulnerability has been identified with a CVSS score that is above the threshold set in the * configuration. @@ -1120,7 +810,114 @@ public class DependencyCheckMojo extends AbstractMojo implements MavenMultiPageR final String msg = String.format("%n%n" + "One or more dependencies were identified with known vulnerabilities:%n%n%s" + "%n%nSee the dependency-check report for more details.%n%n", summary.toString()); - logger.log(Level.WARNING, msg); + LOGGER.log(Level.WARNING, msg); } } + // + + // + /** + * Writes the scan data to disk. This is used to serialize the scan data between the "check" and "aggregate" phase. + * + * @return the File object referencing the data file that was written + */ + @Override + protected File writeDataFile() { + File file = null; + if (engine != null && getProject().getContextValue(this.getDataFileContextKey()) == null) { + file = new File(getProject().getBuild().getDirectory(), getDataFileName()); + OutputStream os = null; + OutputStream bos = null; + ObjectOutputStream out = null; + try { + os = new FileOutputStream(file); + bos = new BufferedOutputStream(os); + out = new ObjectOutputStream(bos); + out.writeObject(engine.getDependencies()); + out.flush(); + + //call reset to prevent resource leaks per + //https://www.securecoding.cert.org/confluence/display/java/SER10-J.+Avoid+memory+and+resource+leaks+during+serialization + out.reset(); + + } catch (IOException ex) { + LOGGER.log(Level.WARNING, "Unable to create data file used for report aggregation; " + + "if report aggregation is being used the results may be incomplete."); + LOGGER.log(Level.FINE, ex.getMessage(), ex); + } finally { + if (out != null) { + try { + out.close(); + } catch (IOException ex) { + LOGGER.log(Level.FINEST, "ignore", ex); + } + } + if (bos != null) { + try { + bos.close(); + } catch (IOException ex) { + LOGGER.log(Level.FINEST, "ignore", ex); + } + } + if (os != null) { + try { + os.close(); + } catch (IOException ex) { + LOGGER.log(Level.FINEST, "ignore", ex); + } + } + } + } + return file; + } + + /** + * Reads the serialized scan data from disk. This is used to serialize the scan data between the "check" and + * "aggregate" phase. + * + * @return a Engine object populated with dependencies if the serialized data file exists; otherwise + * null is returned + */ + protected List readDataFile() { + return readDataFile(getProject()); + } + + /** + * Reads the serialized scan data from disk. This is used to serialize the scan data between the "check" and + * "aggregate" phase. + * + * @param project the Maven project to read the data file from + * @return a Engine object populated with dependencies if the serialized data file exists; otherwise + * null is returned + */ + protected List readDataFile(MavenProject project) { + final Object oPath = project.getContextValue(this.getDataFileContextKey()); + if (oPath == null) { + return null; + } + List ret = null; + final String path = (String) oPath; + ObjectInputStream ois = null; + try { + ois = new ObjectInputStream(new FileInputStream(path)); + ret = (List) ois.readObject(); + } catch (FileNotFoundException ex) { + //TODO fix logging + LOGGER.log(Level.SEVERE, null, ex); + } catch (IOException ex) { + LOGGER.log(Level.SEVERE, null, ex); + } catch (ClassNotFoundException ex) { + LOGGER.log(Level.SEVERE, null, ex); + } finally { + if (ois != null) { + try { + ois.close(); + } catch (IOException ex) { + LOGGER.log(Level.SEVERE, null, ex); + } + } + } + return ret; + } + // } diff --git a/dependency-check-maven/src/main/java/org/owasp/dependencycheck/maven/ReportAggregationMojo.java b/dependency-check-maven/src/main/java/org/owasp/dependencycheck/maven/ReportAggregationMojo.java new file mode 100644 index 000000000..34dd4f750 --- /dev/null +++ b/dependency-check-maven/src/main/java/org/owasp/dependencycheck/maven/ReportAggregationMojo.java @@ -0,0 +1,462 @@ +/* + * This file is part of dependency-check-maven. + * + * 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) 2014 Jeremy Long. All Rights Reserved. + */ +package org.owasp.dependencycheck.maven; + +import java.io.File; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.logging.Logger; +import org.apache.maven.doxia.sink.Sink; +import org.apache.maven.plugin.AbstractMojo; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugin.MojoFailureException; +import org.apache.maven.plugins.annotations.Component; +import org.apache.maven.plugins.annotations.Parameter; +import org.apache.maven.project.MavenProject; +import org.apache.maven.reporting.MavenReport; +import org.apache.maven.reporting.MavenReportException; + +/** + *

+ * This is an abstract reporting mojo that enables report aggregation. Some of the code in the this class was copied + * from the CoberturaReportMojo (http://mojo.codehaus.org/cobertura-maven-plugin/, version 2.6). The authors of the + * CoberturaReportMojo were Will Gwaltney and + * Joakim Erdfelt. There working example of how to do report aggregation was + * invaluable.

+ *

+ * An important point about using this abstract class is that it is intended for one to write some form of serialized + * data (via the {@link org.owasp.dependencycheck.maven.ReportAggregationMojo#writeDataFile() }; note that the + * writeDataFile() function is called automatically after either {@link org.owasp.dependencycheck.maven.ReportAggregationMojo#executeNonAggregateReport(org.apache.maven.doxia.sink.Sink, + * org.apache.maven.doxia.sink.SinkFactory, java.util.Locale) + * } or {@link org.owasp.dependencycheck.maven.ReportAggregationMojo#executeAggregateReport(org.apache.maven.doxia.sink.Sink, + * org.apache.maven.doxia.sink.SinkFactory, java.util.Locale) + * } are called. When executeAggregateReport() is implemented, one can call {@link org.owasp.dependencycheck.maven.ReportAggregationMojo#getChildDataFiles() + * } to obtain a list of the data files to aggregate.

+ * + * + * @author Jeremy Long + */ +public abstract class ReportAggregationMojo extends AbstractMojo implements MavenReport { + + /** + * The Maven Project Object. + */ + @Component + private MavenProject project; + + /** + * Logger field reference. + */ + private static final Logger LOGGER = Logger.getLogger(ReportAggregationMojo.class.getName()); + + /** + * List of Maven project of the current build + */ + @Parameter(readonly = true, required = true, property = "reactorProjects") + private List reactorProjects; + + /** + * Generate aggregate reports in multi-module projects. + */ + @Parameter(property = "aggregate", defaultValue = "false") + private boolean aggregate; + + /** + * Sets whether or not the external report format should be used. + */ + @Parameter(property = "metaFileName", defaultValue = "dependency-check.ser", required = true) + private String dataFileName; + /** + * Specifies the destination directory for the generated Dependency-Check report. This generally maps to + * "target/site". + */ + @Parameter(property = "reportOutputDirectory", defaultValue = "${project.reporting.outputDirectory}", required = true) + private File reportOutputDirectory; + + /** + * Sets the Reporting output directory. + * + * @param directory the output directory + */ + @Override + public void setReportOutputDirectory(File directory) { + reportOutputDirectory = directory; + } + + /** + * Returns the output directory. + * + * @return the output directory + */ + @Override + public File getReportOutputDirectory() { + return reportOutputDirectory; + } + + /** + * Returns the output directory for the given project. + * + * @param project the Maven project to get the output directory for + * @return the output directory for the given project + */ + public File getReportOutputDirectory(MavenProject project) { + final Object o = project.getContextValue(getOutputDirectoryContextKey()); + if (o != null && o instanceof File) { + return (File) o; + } + return null; + } + + /** + * Returns whether this is an external report. This method always returns true. + * + * @return true + */ + @Override + public final boolean isExternalReport() { + return true; + } + + /** + * The collection of child projects. + */ + private final Map> projectChildren = new HashMap>(); + + /** + * Called before execute; allows for any setup that is needed. If this is overridden you must call + * super.preExecute(). + * + * @throws MojoExecutionException thrown if there is an issue executing the mojo + * @throws MojoFailureException thrown if there is an issue executing the mojo + */ + protected void preExecute() throws MojoExecutionException, MojoFailureException { + buildAggregateInfo(); + } + + /** + * Called when the mojo is being executed. + * + * @throws MojoExecutionException thrown if there is an issue executing the mojo + * @throws MojoFailureException thrown if there is an issue executing the mojo + */ + protected abstract void performExecute() throws MojoExecutionException, MojoFailureException; + + /** + * Runs after the mojo has executed. This implementation will call writeDataFile(). As such, it is + * important that if this method is overriden that super.postExecute() is called. + * + * @throws MojoExecutionException thrown if there is an issue executing the mojo + * @throws MojoFailureException thrown if there is an issue executing the mojo + */ + protected void postExecute() throws MojoExecutionException, MojoFailureException { + final File written = writeDataFile(); + if (written != null) { + project.setContextValue(getDataFileContextKey(), written.getAbsolutePath()); + } + } + + /** + * Returns the key used to store the path to the data file that is saved by writeDataFile(). This key + * is used in the MavenProject.(set|get)ContextValue. + * + * @return the key used to store the path to the data file + */ + protected String getDataFileContextKey() { + return "dependency-check-path-" + this.getDataFileName(); + } + + /** + * Returns the key used to store the path to the output directory. When generating the report in the + * executeAggregateReport() the output directory should be obtained by using this key. + * + * @return the key used to store the path to the output directory + */ + protected String getOutputDirectoryContextKey() { + return "dependency-output-dir-" + this.getDataFileName(); + } + + /** + * Is called by Maven to execute the mojo. + * + * @throws MojoExecutionException thrown if there is an issue executing the mojo + * @throws MojoFailureException thrown if there is an issue executing the mojo + */ + public final void execute() throws MojoExecutionException, MojoFailureException { + try { + preExecute(); + performExecute(); + } finally { + postExecute(); + } + } + + /** + * Runs prior to the site report generation. + * + * @throws MavenReportException if a maven report exception occurs + */ + protected void preGenerate() throws MavenReportException { + buildAggregateInfo(); + + project.setContextValue(getOutputDirectoryContextKey(), getReportOutputDirectory()); + } + + /** + * Executes after the site report has been generated. + * + * @throws MavenReportException if a maven report exception occurs + */ + protected void postGenerate() throws MavenReportException { + final File written = writeDataFile(); + if (written != null) { + project.setContextValue(getDataFileContextKey(), written.getAbsolutePath()); + } + } + + /** + * Generates the non aggregate report. + * + * @param locale the locale to use when generating the report + * @throws MavenReportException if a maven report exception occurs + */ + protected abstract void executeNonAggregateReport(Locale locale) throws MavenReportException; + + /** + * Generates the aggregate Site Report. + * + * @param project the maven project used to generate the aggregate report + * @param locale the locale to use when generating the report + * @throws MavenReportException if a maven report exception occurs + */ + protected abstract void executeAggregateReport(MavenProject project, Locale locale) throws MavenReportException; + + /** + * Generates the Dependency-Check Site Report. + * + * @param sink the sink to write the report to + * @param locale the locale to use when generating the report + * @throws MavenReportException if a maven report exception occurs + * @deprecated use {@link #generate(org.apache.maven.doxia.sink.Sink, java.util.Locale) instead. + */ + @Deprecated + public final void generate(@SuppressWarnings("deprecation") org.codehaus.doxia.sink.Sink sink, Locale locale) throws MavenReportException { + generate((Sink) sink, locale); + } + + /** + * Generates the Dependency-Check Site Report. + * + * @param sink the sink to write the report to + * @param locale the locale to use when generating the report + * @throws MavenReportException if a maven report exception occurs + */ + public final void generate(Sink sink, Locale locale) throws MavenReportException { + try { + preGenerate(); + if (canGenerateNonAggregateReport()) { + executeNonAggregateReport(locale); + } + + if (canGenerateAggregateReport()) { + for (MavenProject proj : reactorProjects) { + if (!isMultiModule(proj)) { + continue; + } + executeAggregateReport(proj, locale); + } + } + } finally { + postGenerate(); + } + } + + /** + * Returns whether or not the mojo can generate a non-aggregate report for this project. + * + * @return true if a non-aggregate report can be generated, otherwise false + */ + protected abstract boolean canGenerateNonAggregateReport(); + + /** + * Returns whether or not we can generate any aggregate reports at this time. + * + * @return true if an aggregate report can be generated, otherwise false + */ + protected abstract boolean canGenerateAggregateReport(); + + /** + * Returns the name of the data file that contains the serialized data. + * + * @return the name of the data file that contains the serialized data + */ + protected String getDataFileName() { + return dataFileName; + } + + /** + * Writes the data file to disk in the target directory. + * + * @return the File object referencing the data file that was written + */ + protected abstract File writeDataFile(); + + /** + * Collects the information needed for building aggregate reports. + */ + private void buildAggregateInfo() { + // build parent-child map + for (MavenProject proj : reactorProjects) { + Set depList = projectChildren.get(proj.getParent()); + if (depList == null) { + depList = new HashSet(); + projectChildren.put(proj.getParent(), depList); + } + depList.add(proj); + } + } + + /** + * Returns a list containing all the recursive, non-pom children of the given project, never null. + * + * @return a list of child projects + */ + protected List getAllChildren() { + return getAllChildren(project); + } + + /** + * Returns a list containing all the recursive, non-pom children of the given project, never null. + * + * @param parentProject the parent project to collect the child project references + * @return a list of child projects + */ + protected List getAllChildren(MavenProject parentProject) { + final Set children = projectChildren.get(parentProject); + if (children == null) { + return Collections.emptyList(); + } + + final List result = new ArrayList(); + for (MavenProject child : children) { + if (isMultiModule(child)) { + result.addAll(getAllChildren(child)); + } else { + result.add(child); + } + } + return result; + } + + /** + * Returns a list of data files that were produced by the direct children of the given MavenProject. + * + * @param project the Maven project to obtain the child data files from + * @return a list of the data files + */ + protected List getAllChildDataFiles(MavenProject project) { + final List children = getAllChildren(project); + return getDataFiles(children); + } + + /** + * Returns any existing output files from the given list of projects. + * + * @param projects the list of projects to obtain the output files from + * @return a list of output files + */ + protected List getDataFiles(List projects) { + final List files = new ArrayList(); + for (MavenProject proj : projects) { + final Object path = project.getContextValue(getDataFileContextKey()); + if (path == null) { + final String msg = String.format("Unable to aggregate data for '%s' - aggregate data file was not generated", + proj.getName()); + LOGGER.warning(msg); + } else { + final File outputFile = new File((String) path); + if (outputFile.exists()) { + files.add(outputFile); + } else { + if (!isMultiModule(project)) { + final String msg = String.format("Unable to aggregate data for '%s' - missing data file '%s'", + proj.getName(), outputFile.getPath()); + LOGGER.warning(msg); + } + } + } + } + return files; + } + + /** + * Test if the project has pom packaging + * + * @param mavenProject Project to test + * @return true if it has a pom packaging; otherwise false + */ + protected boolean isMultiModule(MavenProject mavenProject) { + return "pom".equals(mavenProject.getPackaging()); + } + + /** + * Test if the current project has pom packaging + * + * @return true if it has a pom packaging; otherwise false + */ + protected boolean isMultiModule() { + return isMultiModule(project); + } + + /** + * Check whether the current project is the last project in a multi-module build. If the maven build is not a + * multi-module project then this will always return true. + * + * @return true if the current project is the last project in a multi-module build; otherwise + * false + */ + protected boolean isLastProject() { + return project.equals(reactorProjects.get(reactorProjects.size() - 1)); + } + + /** + * Returns whether or not the mojo is configured to perform report aggregation. + * + * @return true if report aggregation is enabled; otherwise false + */ + public boolean isAggregate() { + return aggregate; + } + + /** + * Returns a reference to the current project. This method is used instead of auto-binding the project via component + * annotation in concrete implementations of this. If the child has a @Component MavenProject project; + * defined then the abstract class (i.e. this class) will not have access to the current project (just the way Maven + * works with the binding). + * + * @return returns a reference to the current project + */ + protected MavenProject getProject() { + return project; + } +} diff --git a/dependency-check-maven/src/main/java/org/owasp/dependencycheck/maven/ReportingUtil.java b/dependency-check-maven/src/main/java/org/owasp/dependencycheck/maven/ReportingUtil.java new file mode 100644 index 000000000..d26e9b18e --- /dev/null +++ b/dependency-check-maven/src/main/java/org/owasp/dependencycheck/maven/ReportingUtil.java @@ -0,0 +1,456 @@ +/* + * This file is part of dependency-check-maven. + * + * 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) 2014 Jeremy Long. All Rights Reserved. + */ +package org.owasp.dependencycheck.maven; + +import java.io.File; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.text.DateFormat; +import java.util.Date; +import java.util.List; +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.apache.maven.doxia.sink.Sink; +import org.owasp.dependencycheck.Engine; +import org.owasp.dependencycheck.data.nvdcve.CveDB; +import org.owasp.dependencycheck.data.nvdcve.DatabaseException; +import org.owasp.dependencycheck.data.nvdcve.DatabaseProperties; +import org.owasp.dependencycheck.dependency.Dependency; +import org.owasp.dependencycheck.dependency.Evidence; +import org.owasp.dependencycheck.dependency.Identifier; +import org.owasp.dependencycheck.dependency.Reference; +import org.owasp.dependencycheck.dependency.Vulnerability; +import org.owasp.dependencycheck.dependency.VulnerableSoftware; +import org.owasp.dependencycheck.reporting.ReportGenerator; + +/** + * A utility class that encapsulates the report generation for dependency-check-maven. + * + * @author Jeremy Long + */ +final class ReportingUtil { + + /** + * Logger field reference. + */ + private static final Logger LOGGER = Logger.getLogger(ReportingUtil.class.getName()); + + /** + * Empty private constructor for this utility class. + */ + private ReportingUtil() { + } + + /** + * Generates the reports for a given dependency-check engine. + * + * @param engine a dependency-check engine + * @param outDirectory the directory to write the reports to + * @param projectName the name of the project that a report is being generated for + * @param format the format of the report to generate + */ + static void generateExternalReports(Engine engine, File outDirectory, String projectName, String format) { + DatabaseProperties prop = null; + CveDB cve = null; + try { + cve = new CveDB(); + cve.open(); + prop = cve.getDatabaseProperties(); + } catch (DatabaseException ex) { + LOGGER.log(Level.FINE, "Unable to retrieve DB Properties", ex); + } finally { + if (cve != null) { + cve.close(); + } + } + final ReportGenerator r = new ReportGenerator(projectName, engine.getDependencies(), engine.getAnalyzers(), prop); + try { + r.generateReports(outDirectory.getCanonicalPath(), format); + } catch (IOException ex) { + LOGGER.log(Level.SEVERE, + "Unexpected exception occurred during analysis; please see the verbose error log for more details."); + LOGGER.log(Level.FINE, null, ex); + } catch (Throwable ex) { + LOGGER.log(Level.SEVERE, + "Unexpected exception occurred during analysis; please see the verbose error log for more details."); + LOGGER.log(Level.FINE, null, ex); + } + } + + /** + * Generates a dependency-check report using the Maven Site format. + * + * @param engine the engine used to scan the dependencies + * @param sink the sink to write the data to + * @param projectName the name of the project + */ + static void generateMavenSiteReport(final Engine engine, Sink sink, String projectName) { + final List dependencies = engine.getDependencies(); + + writeSiteReportHeader(sink, projectName); + writeSiteReportTOC(sink, dependencies); + + int cnt = 0; + for (Dependency d : dependencies) { + writeSiteReportDependencyHeader(sink, d); + cnt = writeSiteReportDependencyEvidenceUsed(d, cnt, sink); + cnt = writeSiteReportDependencyRelatedDependencies(d, cnt, sink); + writeSiteReportDependencyIdentifiers(d, sink); + writeSiteReportDependencyVulnerabilities(d, sink, cnt); + } + sink.body_(); + } + + // + /** + * Writes the vulnerabilities to the site report. + * + * @param d the dependency + * @param sink the sink to write the data to + * @param collapsibleHeaderCount the collapsible header count + */ + private static void writeSiteReportDependencyVulnerabilities(Dependency d, Sink sink, int collapsibleHeaderCount) { + int cnt = collapsibleHeaderCount; + if (d.getVulnerabilities() != null && !d.getVulnerabilities().isEmpty()) { + for (Vulnerability v : d.getVulnerabilities()) { + + sink.paragraph(); + sink.bold(); + try { + sink.link("http://web.nvd.nist.gov/view/vuln/detail?vulnId=" + URLEncoder.encode(v.getName(), "US-ASCII")); + sink.text(v.getName()); + sink.link_(); + sink.bold_(); + } catch (UnsupportedEncodingException ex) { + sink.text(v.getName()); + sink.bold_(); + sink.lineBreak(); + sink.text("http://web.nvd.nist.gov/view/vuln/detail?vulnId=" + v.getName()); + } + sink.paragraph_(); + sink.paragraph(); + sink.text("Severity: "); + if (v.getCvssScore() < 4.0) { + sink.text("Low"); + } else { + if (v.getCvssScore() >= 7.0) { + sink.text("High"); + } else { + sink.text("Medium"); + } + } + sink.lineBreak(); + sink.text("CVSS Score: " + v.getCvssScore()); + if (v.getCwe() != null && !v.getCwe().isEmpty()) { + sink.lineBreak(); + sink.text("CWE: "); + sink.text(v.getCwe()); + } + sink.paragraph_(); + sink.paragraph(); + sink.text(v.getDescription()); + if (v.getReferences() != null && !v.getReferences().isEmpty()) { + sink.list(); + for (Reference ref : v.getReferences()) { + sink.listItem(); + sink.text(ref.getSource()); + sink.text(" - "); + sink.link(ref.getUrl()); + sink.text(ref.getName()); + sink.link_(); + sink.listItem_(); + } + sink.list_(); + } + sink.paragraph_(); + if (v.getVulnerableSoftware() != null && !v.getVulnerableSoftware().isEmpty()) { + sink.paragraph(); + + cnt += 1; + sink.rawText("Vulnerable Software [-]"); + sink.rawText("
"); + sink.list(); + for (VulnerableSoftware vs : v.getVulnerableSoftware()) { + sink.listItem(); + try { + sink.link("http://web.nvd.nist.gov/view/vuln/search-results?cpe=" + URLEncoder.encode(vs.getName(), "US-ASCII")); + sink.text(vs.getName()); + sink.link_(); + if (vs.hasPreviousVersion()) { + sink.text(" and all previous versions."); + } + } catch (UnsupportedEncodingException ex) { + sink.text(vs.getName()); + if (vs.hasPreviousVersion()) { + sink.text(" and all previous versions."); + } + sink.text(" (http://web.nvd.nist.gov/view/vuln/search-results?cpe=" + vs.getName() + ")"); + } + + sink.listItem_(); + } + sink.list_(); + sink.rawText("
"); + sink.paragraph_(); + } + } + } + } + + /** + * Writes the identifiers to the site report. + * + * @param d the dependency + * @param sink the sink to write the data to + */ + private static void writeSiteReportDependencyIdentifiers(Dependency d, Sink sink) { + if (d.getIdentifiers() != null && !d.getIdentifiers().isEmpty()) { + sink.sectionTitle4(); + sink.text("Identifiers"); + sink.sectionTitle4_(); + sink.list(); + for (Identifier i : d.getIdentifiers()) { + sink.listItem(); + sink.text(i.getType()); + sink.text(": "); + if (i.getUrl() != null && i.getUrl().length() > 0) { + sink.link(i.getUrl()); + sink.text(i.getValue()); + sink.link_(); + } else { + sink.text(i.getValue()); + } + if (i.getDescription() != null && i.getDescription().length() > 0) { + sink.lineBreak(); + sink.text(i.getDescription()); + } + sink.listItem_(); + } + sink.list_(); + } + } + + /** + * Writes the related dependencies to the site report. + * + * @param d the dependency + * @param sink the sink to write the data to + * @param collapsibleHeaderCount the collapsible header count + * @return the collapsible header count + */ + private static int writeSiteReportDependencyRelatedDependencies(Dependency d, int collapsibleHeaderCount, Sink sink) { + int cnt = collapsibleHeaderCount; + if (d.getRelatedDependencies() != null && !d.getRelatedDependencies().isEmpty()) { + cnt += 1; + sink.sectionTitle4(); + sink.rawText("Related Dependencies [+]"); + sink.sectionTitle4_(); + sink.rawText("
"); + sink.list(); + for (Dependency r : d.getRelatedDependencies()) { + sink.listItem(); + sink.text(r.getFileName()); + sink.list(); + writeListItem(sink, "File Path: " + r.getFilePath()); + writeListItem(sink, "SHA1: " + r.getSha1sum()); + writeListItem(sink, "MD5: " + r.getMd5sum()); + sink.list_(); + sink.listItem_(); + } + sink.list_(); + sink.rawText("
"); + } + return cnt; + } + + /** + * Writes the evidence used to the site report. + * + * @param d the dependency + * @param sink the sink to write the data to + * @param collapsibleHeaderCount the collapsible header count + * @return the collapsible header count + */ + private static int writeSiteReportDependencyEvidenceUsed(Dependency d, int collapsibleHeaderCount, Sink sink) { + int cnt = collapsibleHeaderCount; + final Set evidence = d.getEvidenceForDisplay(); + if (evidence != null && evidence.size() > 0) { + cnt += 1; + sink.sectionTitle4(); + sink.rawText("Evidence Collected [+]"); + sink.sectionTitle4_(); + sink.rawText("
"); + sink.table(); + sink.tableRow(); + writeTableHeaderCell(sink, "Source"); + writeTableHeaderCell(sink, "Name"); + writeTableHeaderCell(sink, "Value"); + sink.tableRow_(); + for (Evidence e : evidence) { + sink.tableRow(); + writeTableCell(sink, e.getSource()); + writeTableCell(sink, e.getName()); + writeTableCell(sink, e.getValue()); + sink.tableRow_(); + } + sink.table_(); + sink.rawText("
"); + } + return cnt; + } + + /** + * Writes the dependency header to the site report. + * + * @param d the dependency + * @param sink the sink to write the data to + */ + private static void writeSiteReportDependencyHeader(Sink sink, Dependency d) { + sink.sectionTitle2(); + sink.anchor("sha1" + d.getSha1sum()); + sink.text(d.getFileName()); + sink.anchor_(); + sink.sectionTitle2_(); + if (d.getDescription() != null && d.getDescription().length() > 0) { + sink.paragraph(); + sink.bold(); + sink.text("Description: "); + sink.bold_(); + sink.text(d.getDescription()); + sink.paragraph_(); + } + if (d.getLicense() != null && d.getLicense().length() > 0) { + sink.paragraph(); + sink.bold(); + sink.text("License: "); + sink.bold_(); + if (d.getLicense().startsWith("http://") && !d.getLicense().contains(" ")) { + sink.link(d.getLicense()); + sink.text(d.getLicense()); + sink.link_(); + } else { + sink.text(d.getLicense()); + } + sink.paragraph_(); + } + } + + /** + * Adds a list item to the site report. + * + * @param sink the sink to write the data to + * @param text the text to write + */ + private static void writeListItem(Sink sink, String text) { + sink.listItem(); + sink.text(text); + sink.listItem_(); + } + + /** + * Adds a table cell to the site report. + * + * @param sink the sink to write the data to + * @param text the text to write + */ + private static void writeTableCell(Sink sink, String text) { + sink.tableCell(); + sink.text(text); + sink.tableCell_(); + } + + /** + * Adds a table header cell to the site report. + * + * @param sink the sink to write the data to + * @param text the text to write + */ + private static void writeTableHeaderCell(Sink sink, String text) { + sink.tableHeaderCell(); + sink.text(text); + sink.tableHeaderCell_(); + } + + /** + * Writes the TOC for the site report. + * + * @param sink the sink to write the data to + * @param dependencies the dependencies that are being reported on + */ + private static void writeSiteReportTOC(Sink sink, final List dependencies) { + sink.list(); + for (Dependency d : dependencies) { + sink.listItem(); + sink.link("#sha1" + d.getSha1sum()); + sink.text(d.getFileName()); + sink.link_(); + if (!d.getVulnerabilities().isEmpty()) { + sink.rawText(" "); + } + if (!d.getRelatedDependencies().isEmpty()) { + sink.list(); + for (Dependency r : d.getRelatedDependencies()) { + writeListItem(sink, r.getFileName()); + } + sink.list_(); + } + sink.listItem_(); + } + sink.list_(); + } + + /** + * Writes the site report header. + * + * @param sink the sink to write the data to + * @param projectName the name of the project + */ + private static void writeSiteReportHeader(Sink sink, String projectName) { + sink.head(); + sink.title(); + sink.text("Dependency-Check Report: " + projectName); + sink.title_(); + sink.head_(); + sink.body(); + sink.rawText(""); + sink.section1(); + sink.sectionTitle1(); + sink.text("Project: " + projectName); + sink.sectionTitle1_(); + sink.date(); + final Date now = new Date(); + sink.text(DateFormat.getDateTimeInstance().format(now)); + sink.date_(); + sink.section1_(); + } + //
+ +} diff --git a/dependency-check-maven/src/site/markdown/configuration.md b/dependency-check-maven/src/site/markdown/configuration.md index 672d4c774..cccc733fb 100644 --- a/dependency-check-maven/src/site/markdown/configuration.md +++ b/dependency-check-maven/src/site/markdown/configuration.md @@ -4,8 +4,8 @@ The following properties can be set on the dependency-check-maven plugin. Property | Description | Default Value ---------------------|------------------------------------|------------------ +aggregate | Sets whether report aggregation will be performed for multi-module site reports. This option only affects the report generation when configured within the reporting section. | false autoUpdate | Sets whether auto-updating of the NVD CVE/CPE data is enabled. It is not recommended that this be turned to false. | true -externalReport | When using as a Site plugin this parameter sets whether or not the external report format should be used. | false outputDirectory | The location to write the report(s). Note, this is not used if generating the report as part of a `mvn site` build | 'target' failBuildOnCVSS | Specifies if the build should be failed if a CVSS score above a specified level is identified. The default is 11 which means since the CVSS scores are 0-10, by default the build will never fail. | 11 format | The report format to be generated (HTML, XML, VULN, ALL). This configuration option has no affect if using this within the Site plugin unless the externalReport is set to true. | HTML @@ -31,7 +31,7 @@ jarAnalyzer | Sets whether Jar Analyzer will be used. nexusAnalyzerEnabled | Sets whether Nexus Analyzer will be used. | true nexusUrl | Defines the Nexus URL. | https://repository.sonatype.org/service/local/ nexusUsesProxy | Whether or not the defined proxy should be used when connecting to Nexus. | true -nuspecAnalyzerEnabled | Sets whether or not the .NET Nuget Nuspec Analyzer will be used. | true +nuspecAnalyzerEnabled | Sets whether or not the .NET Nuget Nuspec Analyzer will be used. | true assemblyAnalyzerEnabled | Sets whether or not the .NET Assembly Analyzer should be used. | true pathToMono | The path to Mono for .NET assembly analysis on non-windows systems |   @@ -40,21 +40,27 @@ Advanced Configuration The following properties can be configured in the plugin. However, they are less frequently changed. One exception may be the cvedUrl properties, which can be used to host a mirror of the NVD within an enterprise environment. -Property | Description | Default Value ----------------------|-------------------------------------------------------------------------|------------------ -cveUrl12Modified | URL for the modified CVE 1.2 | http://nvd.nist.gov/download/nvdcve-modified.xml -cveUrl20Modified | URL for the modified CVE 2.0 | http://static.nvd.nist.gov/feeds/xml/cve/nvdcve-2.0-modified.xml -cveUrl12Base | Base URL for each year's CVE 1.2, the %d will be replaced with the year | http://nvd.nist.gov/download/nvdcve-%d.xml -cveUrl20Base | Base URL for each year's CVE 2.0, the %d will be replaced with the year | http://static.nvd.nist.gov/feeds/xml/cve/nvdcve-2.0-%d.xml -connectionTimeout | The URL Connection Timeout. |   -dataDirectory | Data directory to hold SQL CVEs contents. This should generally not be changed. |   +Property | Description | Default Value +---------------------|--------------------------------------------------------------------------|------------------ +cveUrl12Modified | URL for the modified CVE 1.2. | http://nvd.nist.gov/download/nvdcve-modified.xml +cveUrl20Modified | URL for the modified CVE 2.0. | http://static.nvd.nist.gov/feeds/xml/cve/nvdcve-2.0-modified.xml +cveUrl12Base | Base URL for each year's CVE 1.2, the %d will be replaced with the year. | http://nvd.nist.gov/download/nvdcve-%d.xml +cveUrl20Base | Base URL for each year's CVE 2.0, the %d will be replaced with the year. | http://static.nvd.nist.gov/feeds/xml/cve/nvdcve-2.0-%d.xml +connectionTimeout | Sets the URL Connection Timeout used when downloading external data. |   +dataDirectory | Sets the data directory to hold SQL CVEs contents. This should generally not be changed. |   databaseDriverName | The name of the database driver. Example: org.h2.Driver. |   databaseDriverPath | The path to the database driver JAR file; only used if the driver is not in the class path. |   connectionString | The connection string used to connect to the database. |   databaseUser | The username used when connecting to the database. |   databasePassword | The password used when connecting to the database. |   - +metaFileName | Sets the name of the file to use for storing the metadata about the project. | dependency-check.ser Proxy Configuration ==================== -Use [Maven's settings](https://maven.apache.org/settings.html#Proxies) to configure a proxy server. +Use [Maven's settings](https://maven.apache.org/settings.html#Proxies) to configure a proxy server. If multiple proxies +are configured in the Maven settings file you must tell dependency-check which proxy to use with the following property: + +Property | Description | Default Value +---------------------|--------------------------------------------------------------------------------------|------------------ +mavenSettingsProxyId | The id for the proxy, configured via settings.xml, that dependency-check should use. |   + diff --git a/dependency-check-maven/src/site/markdown/usage.md.vm b/dependency-check-maven/src/site/markdown/usage.md.vm index c04b26e9f..6c2c7e10c 100644 --- a/dependency-check-maven/src/site/markdown/usage.md.vm +++ b/dependency-check-maven/src/site/markdown/usage.md.vm @@ -17,7 +17,9 @@ Create the DependencyCheck-report.html in the target directory ```xml + ... + ... ... @@ -41,11 +43,48 @@ Create the DependencyCheck-report.html in the target directory ``` $H$H$H Example 2: +Create an aggregated dependency-check report within the site + +```xml + + ... + + ... + + ... + + + org.owasp + dependency-check-maven + ${project.version} + + true + + + + + check + + + + + + ... + + ... + + ... + +``` + +$H$H$H Example 3: Create the DependencyCheck-report.html and fail the build for CVSS greater then 8 ```xml + ... + ... ... @@ -71,44 +110,14 @@ Create the DependencyCheck-report.html and fail the build for CVSS greater then ``` -$H$H$H Example 3: -Create the dependency-check report within the site - -```xml - - - - ... - - org.apache.maven.plugins - maven-site-plugin - - - - org.owasp - dependency-check-maven - ${project.version} - - false - - - - - - ... - - ... - - ... - -``` - $H$H$H Example 4: -Create the DependencyCheck-report.html and skip artifacts no bundled in distribution (Provided and Runtime scope) +Create the DependencyCheck-report.html and skip artifacts not bundled in distribution (Provided and Runtime scope) ```xml + ... + ... ... @@ -140,7 +149,9 @@ Create the DependencyCheck-report.html and use internal mirroring of CVE content ```xml + ... + ... ... diff --git a/dependency-check-utils/pom.xml b/dependency-check-utils/pom.xml index 1200559cf..a71065a60 100644 --- a/dependency-check-utils/pom.xml +++ b/dependency-check-utils/pom.xml @@ -21,7 +21,7 @@ Copyright (c) 2014 - Jeremy Long. All Rights Reserved. org.owasp dependency-check-parent - 1.2.4-SNAPSHOT + 1.2.5-SNAPSHOT dependency-check-utils diff --git a/dependency-check-utils/src/main/java/org/owasp/dependencycheck/utils/Checksum.java b/dependency-check-utils/src/main/java/org/owasp/dependencycheck/utils/Checksum.java index bc497b8d3..0224943a2 100644 --- a/dependency-check-utils/src/main/java/org/owasp/dependencycheck/utils/Checksum.java +++ b/dependency-check-utils/src/main/java/org/owasp/dependencycheck/utils/Checksum.java @@ -63,8 +63,22 @@ public final class Checksum { try { fis = new FileInputStream(file); FileChannel ch = fis.getChannel(); - MappedByteBuffer byteBuffer = ch.map(FileChannel.MapMode.READ_ONLY, 0, file.length()); - digest.update(byteBuffer); + long remainingToRead = file.length(); + long start = 0; + while (remainingToRead > 0) { + long amountToRead; + if (remainingToRead > Integer.MAX_VALUE) { + remainingToRead -= Integer.MAX_VALUE; + amountToRead = Integer.MAX_VALUE; + } else { + amountToRead = remainingToRead; + remainingToRead = 0; + } + MappedByteBuffer byteBuffer = ch.map(FileChannel.MapMode.READ_ONLY, start, amountToRead); + digest.update(byteBuffer); + start += amountToRead; + } + // BufferedInputStream bis = new BufferedInputStream(fis); // DigestInputStream dis = new DigestInputStream(bis, digest); // //yes, we are reading in a buffer for performance reasons - 1 byte at a time is SLOW diff --git a/pom.xml b/pom.xml index e1b968423..751fefd83 100644 --- a/pom.xml +++ b/pom.xml @@ -1,4 +1,3 @@ -