diff --git a/dependency-check-core/src/main/java/org/owasp/dependencycheck/analyzer/DependencyBundlingAnalyzer.java b/dependency-check-core/src/main/java/org/owasp/dependencycheck/analyzer/DependencyBundlingAnalyzer.java index 30125e419..02cf7d7d3 100644 --- a/dependency-check-core/src/main/java/org/owasp/dependencycheck/analyzer/DependencyBundlingAnalyzer.java +++ b/dependency-check-core/src/main/java/org/owasp/dependencycheck/analyzer/DependencyBundlingAnalyzer.java @@ -135,6 +135,15 @@ public class DependencyBundlingAnalyzer extends AbstractDependencyComparingAnaly mergeDependencies(nextDependency, dependency, dependenciesToRemove); return true; //since we merged into the next dependency - skip forward to the next in mainIterator } + } else if (ecoSystemIs(NspAnalyzer.DEPENDENCY_ECOSYSTEM, dependency, nextDependency) + && namesAreEqual(dependency, nextDependency) + && versionsAreEqual(dependency, nextDependency)) { + if (dependency.isVirtual()) { + DependencyMergingAnalyzer.mergeDependencies(dependency, nextDependency, dependenciesToRemove); + } else { + DependencyMergingAnalyzer.mergeDependencies(nextDependency, dependency, dependenciesToRemove); + return true; + } } return false; } @@ -452,4 +461,16 @@ public class DependencyBundlingAnalyzer extends AbstractDependencyComparingAnaly return filePath != null && filePath.matches(".*\\.(ear|war)[\\\\/].*"); } + private boolean ecoSystemIs(String ecoSystem, Dependency dependency, Dependency nextDependency) { + return ecoSystem.equals(dependency.getEcosystem()) && ecoSystem.equals(nextDependency.getEcosystem()); + } + + private boolean namesAreEqual(Dependency dependency, Dependency nextDependency) { + return dependency.getName() != null && dependency.getName().equals(nextDependency.getName()); + } + + private boolean versionsAreEqual(Dependency dependency, Dependency nextDependency) { + return dependency.getVersion() != null && dependency.getVersion().equals(nextDependency.getVersion()); + } + } diff --git a/dependency-check-core/src/main/java/org/owasp/dependencycheck/analyzer/DependencyMergingAnalyzer.java b/dependency-check-core/src/main/java/org/owasp/dependencycheck/analyzer/DependencyMergingAnalyzer.java index 49db7fd7c..5a21f2cad 100644 --- a/dependency-check-core/src/main/java/org/owasp/dependencycheck/analyzer/DependencyMergingAnalyzer.java +++ b/dependency-check-core/src/main/java/org/owasp/dependencycheck/analyzer/DependencyMergingAnalyzer.java @@ -118,7 +118,7 @@ public class DependencyMergingAnalyzer extends AbstractDependencyComparingAnalyz * removed from the main analysis loop, this function adds to this * collection */ - private void mergeDependencies(final Dependency dependency, final Dependency relatedDependency, final Set dependenciesToRemove) { + public static void mergeDependencies(final Dependency dependency, final Dependency relatedDependency, final Set dependenciesToRemove) { LOGGER.debug("Merging '{}' into '{}'", relatedDependency.getFilePath(), dependency.getFilePath()); dependency.addRelatedDependency(relatedDependency); for (Evidence e : relatedDependency.getEvidence(EvidenceType.VENDOR)) { @@ -135,9 +135,8 @@ public class DependencyMergingAnalyzer extends AbstractDependencyComparingAnalyz dependency.addRelatedDependency(d); relatedDependency.removeRelatedDependencies(d); } - if (dependency.getSha1sum().equals(relatedDependency.getSha1sum())) { - dependency.addAllProjectReferences(relatedDependency.getProjectReferences()); - } + dependency.addAllProjectReferences(relatedDependency.getProjectReferences()); + dependenciesToRemove.add(relatedDependency); } diff --git a/dependency-check-core/src/main/java/org/owasp/dependencycheck/analyzer/NodePackageAnalyzer.java b/dependency-check-core/src/main/java/org/owasp/dependencycheck/analyzer/NodePackageAnalyzer.java index f9b83008d..1f9c0d734 100644 --- a/dependency-check-core/src/main/java/org/owasp/dependencycheck/analyzer/NodePackageAnalyzer.java +++ b/dependency-check-core/src/main/java/org/owasp/dependencycheck/analyzer/NodePackageAnalyzer.java @@ -48,7 +48,6 @@ import org.owasp.dependencycheck.dependency.EvidenceType; * @author Dale Visser */ @ThreadSafe -@Retired public class NodePackageAnalyzer extends AbstractFileTypeAnalyzer { /** @@ -133,22 +132,9 @@ public class NodePackageAnalyzer extends AbstractFileTypeAnalyzer { } try (JsonReader jsonReader = Json.createReader(FileUtils.openInputStream(file))) { final JsonObject json = jsonReader.readObject(); - if (json.containsKey("name")) { - final Object value = json.get("name"); - if (value instanceof JsonString) { - final String valueString = ((JsonString) value).getString(); - dependency.setName(valueString); - dependency.addEvidence(EvidenceType.PRODUCT, PACKAGE_JSON, "name", valueString, Confidence.HIGHEST); - dependency.addEvidence(EvidenceType.VENDOR, PACKAGE_JSON, "name_project", - String.format("%s_project", valueString), Confidence.LOW); - } else { - LOGGER.warn("JSON value not string as expected: {}", value); - } - } - addToEvidence(dependency, EvidenceType.PRODUCT, json, "description"); - addToEvidence(dependency, EvidenceType.VENDOR, json, "author"); - final String version = addToEvidence(dependency, EvidenceType.VERSION, json, "version"); - dependency.setVersion(version); + + gatherEvidence(json, dependency); + } catch (JsonException e) { LOGGER.warn("Failed to parse package.json file.", e); } catch (IOException e) { @@ -156,6 +142,38 @@ public class NodePackageAnalyzer extends AbstractFileTypeAnalyzer { } } + public static void gatherEvidence(final JsonObject json, Dependency dependency) { + if (json.containsKey("name")) { + final Object value = json.get("name"); + if (value instanceof JsonString) { + final String valueString = ((JsonString) value).getString(); + dependency.setName(valueString); + dependency.setPackagePath(valueString); + dependency.addEvidence(EvidenceType.PRODUCT, PACKAGE_JSON, "name", valueString, Confidence.HIGHEST); + dependency.addEvidence(EvidenceType.VENDOR, PACKAGE_JSON, "name", valueString, Confidence.HIGH); + } else { + LOGGER.warn("JSON value not string as expected: {}", value); + } + } + addToEvidence(dependency, EvidenceType.PRODUCT, json, "description"); + addToEvidence(dependency, EvidenceType.VENDOR, json, "author"); + final String version = addToEvidence(dependency, EvidenceType.VERSION, json, "version"); + if (version != null) { + dependency.setVersion(version); + dependency.addIdentifier("npm", String.format("%s:%s", dependency.getName(), version), null, Confidence.HIGHEST); + } + + // Adds the license if defined in package.json + if (json.containsKey("license")) { + final Object value = json.get("license"); + if (value instanceof JsonString) { + dependency.setLicense(json.getString("license")); + } else { + dependency.setLicense(json.getJsonObject("license").getString("type")); + } + } + } + /** * Adds information to an evidence collection from the node json * configuration. @@ -166,7 +184,7 @@ public class NodePackageAnalyzer extends AbstractFileTypeAnalyzer { * @return the actual string set into evidence * @param key the key to obtain the data from the json information */ - private String addToEvidence(Dependency dep, EvidenceType t, JsonObject json, String key) { + private static String addToEvidence(Dependency dep, EvidenceType t, JsonObject json, String key) { String evidenceStr = null; if (json.containsKey(key)) { final JsonValue value = json.get(key); diff --git a/dependency-check-core/src/main/java/org/owasp/dependencycheck/analyzer/NspAnalyzer.java b/dependency-check-core/src/main/java/org/owasp/dependencycheck/analyzer/NspAnalyzer.java index da33c61a6..f36f23f4e 100644 --- a/dependency-check-core/src/main/java/org/owasp/dependencycheck/analyzer/NspAnalyzer.java +++ b/dependency-check-core/src/main/java/org/owasp/dependencycheck/analyzer/NspAnalyzer.java @@ -36,6 +36,7 @@ import java.io.File; import java.io.FileFilter; import java.io.IOException; import java.net.MalformedURLException; +import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.List; @@ -71,7 +72,11 @@ public class NspAnalyzer extends AbstractFileTypeAnalyzer { * The default URL to the NSP check API. */ public static final String DEFAULT_URL = "https://api.nodesecurity.io/check"; - + /** + * A descriptor for the type of dependencies processed or added by this + * analyzer. + */ + public static final String DEPENDENCY_ECOSYSTEM = "npm"; /** * The file name to scan. */ @@ -136,10 +141,10 @@ public class NspAnalyzer extends AbstractFileTypeAnalyzer { } /** - * Returns the key used in the properties file to reference the analyzer's - * enabled property.x + * Returns the key used in the properties file to determine if the analyzer + * is enabled. * - * @return the analyzer's enabled property setting key + * @return the enabled property setting key for the analyzer */ @Override protected String getAnalyzerEnabledSettingKey() { @@ -152,16 +157,48 @@ public class NspAnalyzer extends AbstractFileTypeAnalyzer { if (!file.isFile() || file.length() == 0) { return; } + try (JsonReader jsonReader = Json.createReader(FileUtils.openInputStream(file))) { + // Retrieves the contents of package.json from the Dependency + final JsonObject packageJson = jsonReader.readObject(); + + if (dependency.getEcosystem() == null || dependency.getName() == null) { + NodePackageAnalyzer.gatherEvidence(packageJson, dependency); + dependency.setEcosystem(DEPENDENCY_ECOSYSTEM); + } + // Do not scan the node_modules directory if (file.getCanonicalPath().contains(File.separator + "node_modules" + File.separator)) { LOGGER.debug("Skipping analysis of node module: " + file.getCanonicalPath()); return; } - // Retrieves the contents of package.json from the Dependency - final JsonObject packageJson = jsonReader.readObject(); + //Processes the dependencies objects in package.json and adds all the modules as dependencies + if (packageJson.containsKey("dependencies")) { + final JsonObject dependencies = packageJson.getJsonObject("dependencies"); + processPackage(engine, dependency, dependencies, "dependencies"); + } + if (packageJson.containsKey("devDependencies")) { + final JsonObject dependencies = packageJson.getJsonObject("devDependencies"); + processPackage(engine, dependency, dependencies, "devDependencies"); + } + if (packageJson.containsKey("optionalDependencies")) { + final JsonObject dependencies = packageJson.getJsonObject("optionalDependencies"); + processPackage(engine, dependency, dependencies, "optionalDependencies"); + } + if (packageJson.containsKey("peerDependencies")) { + final JsonObject dependencies = packageJson.getJsonObject("peerDependencies"); + processPackage(engine, dependency, dependencies, "peerDependencies"); + } + if (packageJson.containsKey("bundleDependencies")) { + final JsonArray dependencies = packageJson.getJsonArray("bundleDependencies"); + processPackage(engine, dependency, dependencies, "bundleDependencies"); + } + if (packageJson.containsKey("bundledDependencies")) { + final JsonArray dependencies = packageJson.getJsonArray("bundledDependencies"); + processPackage(engine, dependency, dependencies, "bundledDependencies"); + } // Create a sanitized version of the package.json final JsonObject sanitizedJson = SanitizePackage.sanitize(packageJson); @@ -192,77 +229,21 @@ public class NspAnalyzer extends AbstractFileTypeAnalyzer { * Create a single vulnerable software object - these do not use CPEs unlike the NVD. */ final VulnerableSoftware vs = new VulnerableSoftware(); - //vs.setVersion(advisory.getVulnerableVersions()); - vs.setUpdate(advisory.getPatchedVersions()); + //TODO consider changing this to available versions on the dependency + //vs.setUpdate(advisory.getPatchedVersions()); + vs.setName(advisory.getModule() + ":" + advisory.getVulnerableVersions()); vuln.setVulnerableSoftware(new HashSet<>(Arrays.asList(vs))); - // Add the vulnerability to package.json - dependency.addVulnerability(vuln); - } - - /* - * Adds evidence about the node package itself, not any of the modules. - */ - if (packageJson.containsKey("name")) { - final Object value = packageJson.get("name"); - if (value instanceof JsonString) { - final String valueString = ((JsonString) value).getString(); - dependency.addEvidence(EvidenceType.PRODUCT, PACKAGE_JSON, "name", valueString, Confidence.HIGHEST); - dependency.addEvidence(EvidenceType.VENDOR, PACKAGE_JSON, "name_project", - String.format("%s_project", valueString), Confidence.LOW); + Dependency existing = findDependency(engine, advisory.getModule(), advisory.getVersion()); + if (existing == null) { + Dependency nodeModule = createDependency(dependency, advisory.getModule(), advisory.getVersion(), "transitive"); + nodeModule.addVulnerability(vuln); + engine.addDependency(nodeModule); } else { - LOGGER.warn("JSON value not string as expected: {}", value); + existing.addVulnerability(vuln); } } - - /* - * Processes the dependencies objects in package.json and adds all the modules as related dependencies - */ - if (packageJson.containsKey("dependencies")) { - final JsonObject dependencies = packageJson.getJsonObject("dependencies"); - processPackage(dependency, dependencies, "dependencies"); - } - if (packageJson.containsKey("devDependencies")) { - final JsonObject dependencies = packageJson.getJsonObject("devDependencies"); - processPackage(dependency, dependencies, "devDependencies"); - } - if (packageJson.containsKey("optionalDependencies")) { - final JsonObject dependencies = packageJson.getJsonObject("optionalDependencies"); - processPackage(dependency, dependencies, "optionalDependencies"); - } - if (packageJson.containsKey("peerDependencies")) { - final JsonObject dependencies = packageJson.getJsonObject("peerDependencies"); - processPackage(dependency, dependencies, "peerDependencies"); - } - if (packageJson.containsKey("bundleDependencies")) { - final JsonArray dependencies = packageJson.getJsonArray("bundleDependencies"); - processPackage(dependency, dependencies, "bundleDependencies"); - } - if (packageJson.containsKey("bundledDependencies")) { - final JsonArray dependencies = packageJson.getJsonArray("bundledDependencies"); - processPackage(dependency, dependencies, "bundledDependencies"); - } - - /* - * Adds the license if defined in package.json - */ - if (packageJson.containsKey("license")) { - final Object value = packageJson.get("license"); - if (value instanceof JsonString) { - dependency.setLicense(packageJson.getString("license")); - } else { - dependency.setLicense(packageJson.getJsonObject("license").getString("type")); - } - } - - /* - * Adds general evidence to about the package. - */ - addToEvidence(dependency, EvidenceType.PRODUCT, packageJson, "description"); - addToEvidence(dependency, EvidenceType.VENDOR, packageJson, "author"); - addToEvidence(dependency, EvidenceType.VERSION, packageJson, "version"); - dependency.setDisplayFileName(String.format("%s/%s", file.getParentFile().getName(), file.getName())); } catch (URLConnectionFailureException e) { this.setEnabled(false); throw new AnalysisException(e.getMessage(), e); @@ -275,62 +256,62 @@ public class NspAnalyzer extends AbstractFileTypeAnalyzer { } } + private Dependency createDependency(Dependency dependency, String name, String version, String scope) { + final Dependency nodeModule = new Dependency(new File(dependency.getActualFile() + "?" + name), true); + nodeModule.setEcosystem(DEPENDENCY_ECOSYSTEM); + nodeModule.addEvidence(EvidenceType.PRODUCT, "package.json", "name", name, Confidence.HIGHEST); + nodeModule.addEvidence(EvidenceType.VENDOR, "package.json", "name", name, Confidence.HIGH); + nodeModule.addEvidence(EvidenceType.VERSION, "package.json", "version", version, Confidence.HIGHEST); + nodeModule.addProjectReference(dependency.getName() + ": " + scope); + nodeModule.setName(name); + nodeModule.setVersion(version); + nodeModule.addIdentifier("npm", String.format("%s:%s", name, version), null, Confidence.HIGHEST); + return nodeModule; + } + /** * Processes a part of package.json (as defined by JsonArray) and update the * specified dependency with relevant info. * + * @param engine the dependency-check engine * @param dependency the Dependency to update * @param jsonArray the jsonArray to parse * @param depType the dependency type */ - private void processPackage(Dependency dependency, JsonArray jsonArray, String depType) { + private void processPackage(Engine engine, Dependency dependency, JsonArray jsonArray, String depType) { final JsonObjectBuilder builder = Json.createObjectBuilder(); for (JsonString str : jsonArray.getValuesAs(JsonString.class)) { builder.add(str.toString(), ""); } final JsonObject jsonObject = builder.build(); - processPackage(dependency, jsonObject, depType); + processPackage(engine, dependency, jsonObject, depType); } /** * Processes a part of package.json (as defined by JsonObject) and update * the specified dependency with relevant info. * + * @param engine the dependency-check engine * @param dependency the Dependency to update * @param jsonObject the jsonObject to parse * @param depType the dependency type */ - private void processPackage(Dependency dependency, JsonObject jsonObject, String depType) { + private void processPackage(Engine engine, Dependency dependency, JsonObject jsonObject, String depType) { for (int i = 0; i < jsonObject.size(); i++) { for (Map.Entry entry : jsonObject.entrySet()) { - /* - * Create identifies that include the npm module and version. Since these are defined, - * assign the highest confidence. - */ - final Identifier moduleName = new Identifier("npm", "Module", null, entry.getKey()); - moduleName.setConfidence(Confidence.HIGHEST); + + final String name = entry.getKey(); String version = ""; if (entry.getValue() != null && entry.getValue().getValueType() == JsonValue.ValueType.STRING) { version = ((JsonString) entry.getValue()).getString(); } - final Identifier moduleVersion = new Identifier("npm", "Version", null, version); - moduleVersion.setConfidence(Confidence.HIGHEST); - - final Identifier moduleDepType = new Identifier("npm", "Scope", null, depType); - moduleVersion.setConfidence(Confidence.HIGHEST); - - /* - * Create related dependencies for each module defined in package.json. The path to the related - * dependency will not actually exist but needs to be unique (due to the use of Set in Dependency). - * The use of related dependencies is a way to specify the actual software BOM in package.json. - */ - //TODO is this actually correct? or should these be transitive dependencies? - final Dependency nodeModule = new Dependency(new File(dependency.getActualFile() + "#" + entry.getKey()), true); - nodeModule.setDisplayFileName(entry.getKey()); - nodeModule.addIdentifier(moduleName); - nodeModule.addIdentifier(moduleVersion); - nodeModule.addIdentifier(moduleDepType); - dependency.addRelatedDependency(nodeModule); + Dependency existing = findDependency(engine, name, version); + if (existing == null) { + final Dependency nodeModule = createDependency(dependency, name, version, depType); + engine.addDependency(nodeModule); + } else { + existing.addProjectReference(dependency.getName() + ": " + depType); + } } } } @@ -368,4 +349,43 @@ public class NspAnalyzer extends AbstractFileTypeAnalyzer { } } } + + private Dependency findDependency(Engine engine, String name, String version) { + for (Dependency d : engine.getDependencies()) { + if (DEPENDENCY_ECOSYSTEM.equals(d.getEcosystem()) && name.equals(d.getName()) && version != null && d.getVersion() != null) { + String dependencyVersion = d.getVersion(); + if (dependencyVersion.startsWith("^") || dependencyVersion.startsWith("~")) { + dependencyVersion = dependencyVersion.substring(1); + } + + if (version.equals(dependencyVersion)) { + return d; + } + if (version.startsWith("^") || version.startsWith("~") || version.contains("*")) { + String type; + String tmp; + if (version.startsWith("^") || version.startsWith("~")) { + type = version.substring(0, 1); + tmp = version.substring(1); + } else { + type = "*"; + tmp = version; + } + String[] v = tmp.split(" ")[0].split("\\."); + String[] depVersion = dependencyVersion.split("\\."); + + if ("^".equals(type) && v[0].equals(depVersion[0])) { + return d; + } else if ("~".equals(type) && v.length >= 2 && depVersion.length >= 2 && v[0].equals(depVersion[0]) && v[1].equals(depVersion[1])) { + return d; + } else if (v[0].equals("*") + || (v.length >= 2 && v[0].equals(depVersion[0]) && v[1].equals("*")) + || (v.length >= 3 && depVersion.length >= 2 && v[0].equals(depVersion[0]) && v[1].equals(depVersion[1]) && v[2].equals("*"))) { + return d; + } + } + } + } + return null; + } } diff --git a/dependency-check-core/src/main/java/org/owasp/dependencycheck/data/nsp/NspSearch.java b/dependency-check-core/src/main/java/org/owasp/dependencycheck/data/nsp/NspSearch.java index 327952a25..52bdd15b8 100644 --- a/dependency-check-core/src/main/java/org/owasp/dependencycheck/data/nsp/NspSearch.java +++ b/dependency-check-core/src/main/java/org/owasp/dependencycheck/data/nsp/NspSearch.java @@ -123,6 +123,7 @@ public class NspSearch { try (InputStream in = new BufferedInputStream(conn.getInputStream()); JsonReader jsonReader = Json.createReader(in)) { final JsonArray array = jsonReader.readArray(); + if (array != null) { for (int i = 0; i < array.size(); i++) { final JsonObject object = array.getJsonObject(i);