diff --git a/Dockerfile b/Dockerfile index 0af01a4354c..54139e1c716 100644 --- a/Dockerfile +++ b/Dockerfile @@ -28,7 +28,7 @@ RUN apk upgrade --no-cache apk add --no-cache --virtual .build-deps curl && \ apk add --no-cache git ruby npm && \ gem install --no-document bundler-audit && \ - npm install --global --ignore-scripts corepack && \ + npm install --global --ignore-scripts corepack && corepack enable npm && \ unzip dependency-check-${VERSION}-release.zip -d /usr/share/ && \ rm dependency-check-${VERSION}-release.zip && \ cd /usr/share/dependency-check/plugins && \ @@ -50,7 +50,7 @@ USER ${UID} ### Cache pieces needed for the specific run user RUN bundle audit update && \ - corepack prepare pnpm@latest yarn@latest --activate && \ + corepack prepare npm@latest pnpm@latest yarn@latest --activate && \ printf "enableTelemetry: false\nenableScripts: false\n" >> ${HOME}/.yarnrc.yml && \ rm -rf /tmp/* diff --git a/ant/src/main/java/org/owasp/dependencycheck/taskdefs/Check.java b/ant/src/main/java/org/owasp/dependencycheck/taskdefs/Check.java index ee2f777c83f..ca47245aa62 100644 --- a/ant/src/main/java/org/owasp/dependencycheck/taskdefs/Check.java +++ b/ant/src/main/java/org/owasp/dependencycheck/taskdefs/Check.java @@ -184,6 +184,10 @@ public class Check extends Update { * The path to `pnpm`. */ private String pathToPnpm; + /** + * The path to `npm`. + */ + private String pathToNpm; /** * Additional ZIP File extensions to add analyze. This should be a * comma-separated list of file extensions to treat like ZIP files. @@ -1136,6 +1140,15 @@ public void setPathToPnpm(String pathToPnpm) { this.pathToPnpm = pathToPnpm; } + /** + * Set the value of pathToNpm. + * + * @param pathToNpm new value of pathToNpm + */ + public void setPathToNpm(String pathToNpm) { + this.pathToNpm = pathToNpm; + } + /** * Set the value of pathToGo. * @@ -1510,6 +1523,7 @@ protected void populateSettings() throws BuildException { getSettings().setStringIfNotNull(Settings.KEYS.ANALYZER_GOLANG_PATH, pathToGo); getSettings().setStringIfNotNull(Settings.KEYS.ANALYZER_YARN_PATH, pathToYarn); getSettings().setStringIfNotNull(Settings.KEYS.ANALYZER_PNPM_PATH, pathToPnpm); + getSettings().setStringIfNotNull(Settings.KEYS.ANALYZER_NPM_PATH, pathToNpm); getSettings().setBooleanIfNotNull(Settings.KEYS.ANALYZER_MIX_AUDIT_ENABLED, mixAuditAnalyzerEnabled); getSettings().setStringIfNotNull(Settings.KEYS.ANALYZER_MIX_AUDIT_PATH, mixAuditPath); getSettings().setBooleanIfNotNull(Settings.KEYS.ANALYZER_NUSPEC_ENABLED, nuspecAnalyzerEnabled); diff --git a/ant/src/site/markdown/configuration.md b/ant/src/site/markdown/configuration.md index 4a6efa951d2..d2ac9c8602b 100644 --- a/ant/src/site/markdown/configuration.md +++ b/ant/src/site/markdown/configuration.md @@ -117,13 +117,14 @@ be needed. | cpanfileAnalyzerEnabled | Sets whether the [experimental](../analyzers/index.html) Perl CPAN File Analyzer should be used. `enableExperimental` must be set to true. | true | | nodeAnalyzerEnabled | Sets whether the Node Package Analyzer should be used. | true | | nodePackageSkipDevDependencies | Sets whether the Node Package Analyzer will skip devDependencies. | false | -| nodeAuditAnalyzerEnabled | Sets whether the Node Audit Analyzer should be used. This analyzer requires an internet connection. | true | +| nodeAuditAnalyzerEnabled | Sets whether the Node Audit Analyzer should be used. This analyzer requires npm and an internet connection. | true | | nodeAuditAnalyzerUseCache | Sets whether the Node Audit Analyzer will cache results. Cached results expire after 24 hours. | true | | nodeAuditSkipDevDependencies | Sets whether the Node Audit Analyzer will skip devDependencies. | false | | yarnAuditAnalyzerEnabled | Sets whether the Yarn Audit Analyzer should be used. This analyzer requires yarn and an internet connection. Use `nodeAuditSkipDevDependencies` to skip dev dependencies. | true | | pnpmAuditAnalyzerEnabled | Sets whether the Pnpm Audit Analyzer should be used. This analyzer requires pnpm and an internet connection. Use `nodeAuditSkipDevDependencies` to skip dev dependencies. | true | | pathToYarn | The path to `yarn`. |   | | pathToPnpm | The path to `pnpm`. |   | +| pathToNpm | The path to `npm`. |   | | retireJsAnalyzerEnabled | Sets whether the RetireJS Analyzer update and analyzer are enabled. | true | | retireJsFilterNonVulnerable | Configures the RetireJS Analyzer to remove non-vulnerable JS dependencies from the report. | false | | retireJsFilter | A nested configuration that can be specified multple times; The regex defined is used to filter JS files based on content. |   | diff --git a/cli/src/main/java/org/owasp/dependencycheck/App.java b/cli/src/main/java/org/owasp/dependencycheck/App.java index 5e32b29f239..f5afde3f23b 100644 --- a/cli/src/main/java/org/owasp/dependencycheck/App.java +++ b/cli/src/main/java/org/owasp/dependencycheck/App.java @@ -527,6 +527,8 @@ protected void populateSettings(CliParser cli) throws InvalidSettingException { cli.getStringArgument(CliParser.ARGUMENT.PATH_TO_YARN)); settings.setStringIfNotNull(Settings.KEYS.ANALYZER_PNPM_PATH, cli.getStringArgument(CliParser.ARGUMENT.PATH_TO_PNPM)); + settings.setStringIfNotNull(Settings.KEYS.ANALYZER_NPM_PATH, + cli.getStringArgument(CliParser.ARGUMENT.PATH_TO_NPM)); settings.setBooleanIfNotNull(Settings.KEYS.PRETTY_PRINT, cli.hasOption(CliParser.ARGUMENT.PRETTY_PRINT)); settings.setStringIfNotNull(Settings.KEYS.ANALYZER_RETIREJS_REPO_JS_URL, diff --git a/cli/src/main/java/org/owasp/dependencycheck/CliParser.java b/cli/src/main/java/org/owasp/dependencycheck/CliParser.java index 47bdbc418b0..a16779e697c 100644 --- a/cli/src/main/java/org/owasp/dependencycheck/CliParser.java +++ b/cli/src/main/java/org/owasp/dependencycheck/CliParser.java @@ -476,6 +476,8 @@ private void addAdvancedOptions(final Options options) { "The path to the `yarn` executable.")) .addOption(newOptionWithArg(ARGUMENT.PATH_TO_PNPM, "path", "The path to the `pnpm` executable.")) + .addOption(newOptionWithArg(ARGUMENT.PATH_TO_NPM, "path", + "The path to the `npm` executable.")) .addOption(newOptionWithArg(ARGUMENT.RETIRE_JS_FILTERS, "pattern", "Specify Retire JS content filter used to exclude files from analysis based on their content; " + "most commonly used to exclude based on your applications own copyright line. This " @@ -1348,6 +1350,10 @@ public static class ARGUMENT { * The CLI argument name for setting the path to `pnpm`. */ public static final String PATH_TO_PNPM = "pnpm"; + /** + * The CLI argument name for setting the path to `npm`. + */ + public static final String PATH_TO_NPM = "npm"; /** * Disables the Ruby Gemspec Analyzer. */ diff --git a/cli/src/site/markdown/arguments.md b/cli/src/site/markdown/arguments.md index f1093d54032..3e814ae1eb2 100644 --- a/cli/src/site/markdown/arguments.md +++ b/cli/src/site/markdown/arguments.md @@ -55,7 +55,8 @@ Advanced Options | | \-\-yarn | \ | The path to `yarn`. |   | | | \-\-disablePnpmAudit | | Sets whether the pnpm Audit Analyzer will be used. This analyzer requires an internet connection and that pnpm is installed. Use `--nodeAuditSkipDevDependencies` to skip dev dependencies. |   | | | \-\-pnpm | \ | The path to `pnpm`. |   | -| | \-\-disableNodeAudit | | Sets whether the Node Audit Analyzer will be used. This analyzer requires an internet connection. |   | +| | \-\-npm | \ | The path to `npm`. |   | +| | \-\-disableNodeAudit | | Sets whether the Node Audit Analyzer will be used. This analyzer requires an internet connection and that npm is installed. Use `--nodeAuditSkipDevDependencies` to skip dev dependencies. |   | | | \-\-disableNodeAuditCache | | When the argument is present the Node Audit Analyzer will not cache results. By default the results are cached for 24 hours. |   | | | \-\-nodeAuditSkipDevDependencies | | Configures the Node Audit Analyzer to skip devDependencies. |   | | | \-\-disableRetireJs | | Sets whether the RetireJS Analyzer will be used. |   | diff --git a/core/src/main/java/org/owasp/dependencycheck/analyzer/AbstractNpmAnalyzer.java b/core/src/main/java/org/owasp/dependencycheck/analyzer/AbstractNpmAnalyzer.java index a02e3d40d74..3e38e8dacd9 100644 --- a/core/src/main/java/org/owasp/dependencycheck/analyzer/AbstractNpmAnalyzer.java +++ b/core/src/main/java/org/owasp/dependencycheck/analyzer/AbstractNpmAnalyzer.java @@ -21,28 +21,33 @@ import com.github.packageurl.PackageURL; import com.github.packageurl.PackageURL.StandardTypes; import com.github.packageurl.PackageURLBuilder; -import org.semver4j.Semver; -import org.semver4j.SemverException; +import org.apache.commons.collections4.MultiValuedMap; +import org.apache.commons.lang3.StringUtils; import org.owasp.dependencycheck.Engine; +import org.owasp.dependencycheck.analyzer.exception.AnalysisException; +import org.owasp.dependencycheck.analyzer.exception.UnexpectedAnalysisException; import org.owasp.dependencycheck.data.nodeaudit.Advisory; -import org.owasp.dependencycheck.data.nodeaudit.NodeAuditSearch; +import org.owasp.dependencycheck.data.nvd.ecosystem.Ecosystem; import org.owasp.dependencycheck.dependency.Confidence; import org.owasp.dependencycheck.dependency.Dependency; +import org.owasp.dependencycheck.dependency.EvidenceType; import org.owasp.dependencycheck.dependency.Vulnerability; import org.owasp.dependencycheck.dependency.VulnerableSoftware; import org.owasp.dependencycheck.dependency.VulnerableSoftwareBuilder; +import org.owasp.dependencycheck.dependency.naming.GenericIdentifier; +import org.owasp.dependencycheck.dependency.naming.Identifier; +import org.owasp.dependencycheck.dependency.naming.PurlIdentifier; import org.owasp.dependencycheck.exception.InitializationException; +import org.owasp.dependencycheck.utils.Checksum; import org.owasp.dependencycheck.utils.InvalidSettingException; import org.owasp.dependencycheck.utils.Settings; +import org.semver4j.Semver; +import org.semver4j.SemverException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.File; -import java.io.IOException; -import java.net.MalformedURLException; -import java.net.URL; -import java.util.Collection; -import java.util.List; -import java.util.Map; +import us.springett.parsers.cpe.exceptions.CpeValidationException; +import us.springett.parsers.cpe.values.Part; + import javax.annotation.concurrent.ThreadSafe; import jakarta.json.Json; import jakarta.json.JsonArray; @@ -51,18 +56,13 @@ import jakarta.json.JsonString; import jakarta.json.JsonValue; import jakarta.json.JsonValue.ValueType; -import org.apache.commons.collections4.MultiValuedMap; -import org.apache.commons.lang3.StringUtils; -import org.owasp.dependencycheck.analyzer.exception.AnalysisException; -import org.owasp.dependencycheck.analyzer.exception.UnexpectedAnalysisException; -import org.owasp.dependencycheck.data.nvd.ecosystem.Ecosystem; -import org.owasp.dependencycheck.dependency.EvidenceType; -import org.owasp.dependencycheck.dependency.naming.GenericIdentifier; -import org.owasp.dependencycheck.dependency.naming.Identifier; -import org.owasp.dependencycheck.dependency.naming.PurlIdentifier; -import org.owasp.dependencycheck.utils.Checksum; -import us.springett.parsers.cpe.exceptions.CpeValidationException; -import us.springett.parsers.cpe.values.Part; +import java.io.File; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Collection; +import java.util.List; +import java.util.Map; /** * An abstract NPM analyzer that contains common methods for concrete @@ -88,11 +88,6 @@ public abstract class AbstractNpmAnalyzer extends AbstractFileTypeAnalyzer { */ private static final String PACKAGE_JSON = "package.json"; - /** - * The Node Audit Searcher. - */ - private NodeAuditSearch searcher; - /** * Determines if the file can be analyzed by the analyzer. * @@ -414,24 +409,16 @@ protected void prepareFileTypeAnalyzer(Engine engine) throws InitializationExcep this.setEnabled(false); return; } - if (searcher == null) { - LOGGER.debug("Initializing {}", getName()); - try { - searcher = new NodeAuditSearch(getSettings()); - } catch (MalformedURLException ex) { - setEnabled(false); - throw new InitializationException("The configured URL to NPM Audit API is malformed", ex); - } - try { - final Settings settings = engine.getSettings(); - final boolean nodeEnabled = settings.getBoolean(Settings.KEYS.ANALYZER_NODE_PACKAGE_ENABLED); - if (!nodeEnabled) { - LOGGER.warn("The Node Package Analyzer has been disabled; the resulting report will only " - + "contain the known vulnerable dependency - not a bill of materials for the node project."); - } - } catch (InvalidSettingException ex) { - throw new InitializationException("Unable to read configuration settings", ex); + LOGGER.debug("Initializing {}", getName()); + try { + final Settings settings = engine.getSettings(); + final boolean nodeEnabled = settings.getBoolean(Settings.KEYS.ANALYZER_NODE_PACKAGE_ENABLED); + if (!nodeEnabled) { + LOGGER.warn("The Node Package Analyzer has been disabled; the resulting report will only " + + "contain the known vulnerable dependency - not a bill of materials for the node project."); } + } catch (InvalidSettingException ex) { + throw new InitializationException("Unable to read configuration settings", ex); } } @@ -519,15 +506,6 @@ protected void replaceOrAddVulnerability(Dependency dependency, Vulnerability vu } } - /** - * Returns the node audit search utility. - * - * @return the node audit search utility - */ - protected NodeAuditSearch getSearcher() { - return searcher; - } - /** * Give an NPM version range and a collection of versions, this method * attempts to select a specific version from the collection that is in the diff --git a/core/src/main/java/org/owasp/dependencycheck/analyzer/NodeAuditAnalyzer.java b/core/src/main/java/org/owasp/dependencycheck/analyzer/NodeAuditAnalyzer.java index 51307bbf5ac..11de7e73049 100644 --- a/core/src/main/java/org/owasp/dependencycheck/analyzer/NodeAuditAnalyzer.java +++ b/core/src/main/java/org/owasp/dependencycheck/analyzer/NodeAuditAnalyzer.java @@ -19,37 +19,42 @@ import org.apache.commons.collections4.MultiValuedMap; import org.apache.commons.collections4.multimap.HashSetValuedHashMap; +import org.apache.commons.jcs3.access.exception.CacheException; +import org.apache.commons.lang3.StringUtils; +import org.json.JSONException; +import org.json.JSONObject; import org.owasp.dependencycheck.Engine; import org.owasp.dependencycheck.analyzer.exception.AnalysisException; -import org.owasp.dependencycheck.analyzer.exception.SearchException; import org.owasp.dependencycheck.analyzer.exception.UnexpectedAnalysisException; +import org.owasp.dependencycheck.data.cache.DataCache; +import org.owasp.dependencycheck.data.cache.DataCacheFactory; import org.owasp.dependencycheck.data.nodeaudit.Advisory; -import org.owasp.dependencycheck.data.nodeaudit.NpmPayloadBuilder; +import org.owasp.dependencycheck.data.nodeaudit.NpmCliAuditParser; import org.owasp.dependencycheck.data.nvd.ecosystem.Ecosystem; import org.owasp.dependencycheck.dependency.Dependency; +import org.owasp.dependencycheck.exception.InitializationException; +import org.owasp.dependencycheck.utils.Checksum; import org.owasp.dependencycheck.utils.FileFilterBuilder; import org.owasp.dependencycheck.utils.Settings; -import org.owasp.dependencycheck.utils.URLConnectionFailureException; +import org.owasp.dependencycheck.utils.processing.ProcessReader; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import us.springett.parsers.cpe.exceptions.CpeValidationException; import javax.annotation.concurrent.ThreadSafe; -import jakarta.json.Json; -import jakarta.json.JsonException; -import jakarta.json.JsonObject; -import jakarta.json.JsonReader; import java.io.File; import java.io.FileFilter; import java.io.IOException; import java.nio.file.Files; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; import java.util.List; import static org.owasp.dependencycheck.utils.FileUtils.existsWithContent; /** * Used to analyze Node Package Manager (npm) package-lock.json and - * npm-shrinkwrap.json files via NPM Audit API. + * npm-shrinkwrap.json files via the `npm audit` command. * * @author Steve Springett */ @@ -60,10 +65,6 @@ public class NodeAuditAnalyzer extends AbstractNpmAnalyzer { * The logger. */ private static final Logger LOGGER = LoggerFactory.getLogger(NodeAuditAnalyzer.class); - /** - * The default URL to the NPM Audit API. - */ - public static final String DEFAULT_URL = "https://registry.npmjs.org/-/npm/v1/security/audits"; /** * A descriptor for the type of dependencies processed or added by this * analyzer. @@ -85,6 +86,15 @@ public class NodeAuditAnalyzer extends AbstractNpmAnalyzer { private static final FileFilter PACKAGE_JSON_FILTER = FileFilterBuilder.newInstance() .addFilenames(PACKAGE_LOCK_JSON, SHRINKWRAP_JSON).build(); + /** + * The path to the `npm` executable. + */ + private String npmPath; + /** + * Persisted disk cache for `npm audit` results. + */ + private DataCache> cache; + /** * Returns the FileFilter * @@ -126,6 +136,83 @@ protected String getAnalyzerEnabledSettingKey() { return Settings.KEYS.ANALYZER_NODE_AUDIT_ENABLED; } + /** + * Initializes the analyzer once before any analysis is performed. + * + * @param engine a reference to the dependency-check engine + * @throws InitializationException if there's an error during initialization + */ + @Override + protected void prepareFileTypeAnalyzer(Engine engine) throws InitializationException { + super.prepareFileTypeAnalyzer(engine); + if (!isEnabled()) { + LOGGER.debug("{} is disabled - skipping npm executable check", getName()); + return; + } + try { + cacheNpmCommandPath(); + checkNpmExecutable(); + } catch (Exception ex) { + this.setEnabled(false); + LOGGER.warn("The {} has been disabled after failing to find npm. The npm executable was not " + + "found or received a non-zero exit value: {}", getName(), ex.getMessage()); + throw new InitializationException("Unable to determine the npm executable to use.", ex); + } + if (getSettings().getBoolean(Settings.KEYS.ANALYZER_NODE_AUDIT_USE_CACHE, true)) { + try { + final DataCacheFactory factory = new DataCacheFactory(getSettings()); + cache = factory.getNodeAuditCache(); + } catch (CacheException ex) { + getSettings().setBoolean(Settings.KEYS.ANALYZER_NODE_AUDIT_USE_CACHE, false); + LOGGER.debug("Error creating cache, disabling caching", ex); + } + } + } + + /** + * Attempts to determine and cache the path to `npm`. + */ + private void cacheNpmCommandPath() { + String value = getSettings().getString(Settings.KEYS.ANALYZER_NPM_PATH); + if (value == null || value.isBlank()) { + value = "npm"; + } else { + final File fileValue = new File(value); + if (fileValue.isFile()) { + value = fileValue.getAbsolutePath(); + } else { + LOGGER.warn("Provided path to the `npm` executable is invalid; defaulting to `npm`."); + value = "npm"; + } + } + npmPath = value; + } + + /** + * Verifies that the npm executable can be run. + */ + private void checkNpmExecutable() { + final List args = List.of(npmPath, "--version"); + final ProcessBuilder builder = new ProcessBuilder(args); + try { + final Process process = builder.start(); + try (ProcessReader processReader = new ProcessReader(process)) { + processReader.readAll(); + final int exitValue = process.waitFor(); + if (exitValue != 0) { + throw new IllegalStateException(String.format("Unable to run npm, unexpected response " + + "(exit value %s, output: %s, error: %s)", exitValue, + StringUtils.trimToEmpty(processReader.getOutput()), processReader.getError())); + } + } + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + throw new IllegalStateException("Unable to run npm.", ex); + } catch (IOException ex) { + throw new IllegalStateException("Unable to run npm.", ex); + } + } + @Override protected void analyzeDependency(Dependency dependency, Engine engine) throws AnalysisException { if (dependency.getDisplayFileName().equals(dependency.getFileName())) { @@ -140,135 +227,222 @@ protected void analyzeDependency(Dependency dependency, Engine engine) throws An if (!existsWithContent(packageLock) || !shouldProcess(packageLock)) { return; } - final File packageJson = new File(packageLock.getParentFile(), "package.json"); - final List advisories; - final MultiValuedMap dependencyMap = new HashSetValuedHashMap<>(); - //final Map dependencyMap = new HashMap<>(); - if (packageJson.isFile()) { - advisories = analyzePackage(packageLock, packageJson, dependency, dependencyMap); - } else { - advisories = legacyAnalysis(packageLock, dependency, dependencyMap); - } + final boolean skipDevDependencies = getSettings().getBoolean(Settings.KEYS.ANALYZER_NODE_AUDIT_SKIPDEV, false); + final File folder = getDependencyDirectory(packageLock); try { + final List advisories = getAdvisories(packageLock, folder, skipDevDependencies); + final MultiValuedMap dependencyMap = new HashSetValuedHashMap<>(); + collectDependencyVersions(folder, skipDevDependencies, dependency, dependencyMap); processResults(advisories, engine, dependency, dependencyMap); + } catch (JSONException e) { + throw new AnalysisException(String.format("Failed to parse the output of `npm audit` for %s " + + "(NodeAuditAnalyzer).", packageLock.getPath()), e); } catch (CpeValidationException ex) { throw new UnexpectedAnalysisException(ex); } } /** - * Analyzes the package and package-lock files by extracting dependency - * information, creating a payload to submit to the npm audit API, - * submitting the payload, and returning the identified advisories. + * Obtains the advisories for the given lock file - either from the local + * disk cache or by invoking `npm audit`. * - * @param lockFile a reference to the package-lock.json - * @param packageFile a reference to the package.json - * @param dependency a reference to the dependency-object for the - * package-lock.json - * @param dependencyMap a collection of module/version pairs; during - * creation of the payload the dependency map is populated with the - * module/version information. + * @param packageLock the lock file being analyzed + * @param folder the directory containing the lock file + * @param skipDevDependencies whether devDependencies should be skipped * @return a list of advisories - * @throws AnalysisException thrown when there is an error creating or - * submitting the npm audit API payload + * @throws AnalysisException thrown when there is an error running or + * parsing the `npm audit` output */ - private List analyzePackage(final File lockFile, final File packageFile, - Dependency dependency, MultiValuedMap dependencyMap) - throws AnalysisException { - try { - final JsonReader packageReader = Json.createReader(Files.newInputStream(packageFile.toPath())); - final JsonReader lockReader = Json.createReader(Files.newInputStream(lockFile.toPath())); - // Retrieves the contents of package-lock.json from the Dependency - final JsonObject lockJson = lockReader.readObject(); - // Retrieves the contents of package-lock.json from the Dependency - final JsonObject packageJson = packageReader.readObject(); - - // Modify the payload to meet the NPM Audit API requirements - final JsonObject payload = NpmPayloadBuilder.build(lockJson, packageJson, dependencyMap, - getSettings().getBoolean(Settings.KEYS.ANALYZER_NODE_AUDIT_SKIPDEV, false)); - - // Submits the package payload to the nsp check service - return getSearcher().submitPackage(payload); - - } catch (URLConnectionFailureException e) { - this.setEnabled(false); - throw new AnalysisException("Failed to connect to the NPM Audit API (NodeAuditAnalyzer); the analyzer " - + "is being disabled and may result in false negatives.", e); - } catch (IOException e) { - LOGGER.debug("Error reading dependency or connecting to NPM Audit API", e); - this.setEnabled(false); - throw new AnalysisException("Failed to read results from the NPM Audit API (NodeAuditAnalyzer); " - + "the analyzer is being disabled and may result in false negatives.", e); - } catch (JsonException e) { - throw new AnalysisException(String.format("Failed to parse %s file from the NPM Audit API " - + "(NodeAuditAnalyzer).", lockFile.getPath()), e); - } catch (SearchException e) { - final File yarnCheck = new File(lockFile.getParentFile(), "yarn.lock"); - if (yarnCheck.exists()) { - final String msg = "NodeAuditAnalyzer failed on " + dependency.getActualFilePath() - + " - yarn.lock was found; if package-lock.json was generated using synp, it may not be in the correct format."; - LOGGER.error(msg); - throw new AnalysisException(msg, e); + private List getAdvisories(File packageLock, File folder, boolean skipDevDependencies) throws AnalysisException { + String key = null; + if (cache != null) { + try { + key = Checksum.getSHA256Checksum(packageLock) + (skipDevDependencies ? "_prod" : "_all"); + final List cached = cache.get(key); + if (cached != null) { + LOGGER.debug("Cache hit for node audit: {}", key); + return cached; + } + } catch (IOException | NoSuchAlgorithmException ex) { + LOGGER.debug("Error calculating the checksum of the lock file; the audit results will not be cached", ex); + key = null; } - LOGGER.error("NodeAuditAnalyzer failed on {}", dependency.getActualFilePath()); - throw e; } + final JSONObject auditReport = fetchNpmAuditReport(folder, skipDevDependencies); + final List advisories = new NpmCliAuditParser().parse(auditReport); + if (cache != null && key != null) { + cache.put(key, advisories); + } + return advisories; } /** - * Analyzes the package and package-lock files by extracting dependency - * information, creating a payload to submit to the npm audit API, - * submitting the payload, and returning the identified advisories. + * Invokes `npm audit` and returns the parsed JSON report. * - * @param file a reference to the package-lock.json - * @param dependency a reference to the dependency-object for the - * package-lock.json - * @param dependencyMap a collection of module/version pairs; during - * creation of the payload the dependency map is populated with the - * module/version information. - * @return a list of advisories - * @throws AnalysisException thrown when there is an error creating or - * submitting the npm audit API payload + * @param folder the directory containing the lock file to audit + * @param skipDevDependencies whether devDependencies should be skipped + * @return the JSON report produced by `npm audit` + * @throws AnalysisException thrown when there is an error running or + * parsing the `npm audit` output */ - private List legacyAnalysis(final File file, Dependency dependency, MultiValuedMap dependencyMap) - throws AnalysisException { + private JSONObject fetchNpmAuditReport(File folder, boolean skipDevDependencies) throws AnalysisException { + final List args = new ArrayList<>(); + args.add(npmPath); + args.add("audit"); + if (skipDevDependencies) { + args.add("--omit=dev"); + } + //do not require an installed node_modules directory - audit the lock file as-is + args.add("--package-lock-only"); + //vulnerabilities being found must not result in a non-zero exit value + args.add("--audit-level=none"); + args.add("--json"); + final ProcessBuilder builder = new ProcessBuilder(args); + builder.directory(folder); + LOGGER.debug("Launching: {}", args); - try (JsonReader jsonReader = Json.createReader(Files.newInputStream(file.toPath()))) { + final String report = startAndReadStdoutToString(builder, "npm_audit"); + LOGGER.debug("npm audit report: {}", report); + try { + final JSONObject jsonReport = new JSONObject(report); + if (jsonReport.has("error")) { + final JSONObject error = jsonReport.getJSONObject("error"); + throw new AnalysisException(String.format("`npm audit` failed with error code %s: %s", + error.optString("code"), error.optString("summary"))); + } + return jsonReport; + } catch (JSONException e) { + throw new AnalysisException("`npm audit` returned an invalid response.", e); + } + } - // Retrieves the contents of package-lock.json from the Dependency - final JsonObject packageJson = jsonReader.readObject(); + /** + * Invokes `npm ls` to obtain the resolved dependency tree from the lock + * file; populating the given map with each module name and version + * identified and updating the name and version of the dependency being + * analyzed with the project details. + * + *

+ * The advisories returned by `npm audit` do not contain the installed + * version of the affected modules - only the vulnerable version range. The + * name/version map is used to resolve the installed version.

+ * + * @param folder the directory containing the lock file + * @param skipDevDependencies whether devDependencies should be skipped + * @param dependency a reference to the dependency-object for the lock file + * @param dependencyMap a collection of module/version pairs that is + * populated while parsing the dependency tree + * @throws AnalysisException thrown when there is an error running or + * parsing the `npm ls` output + */ + private void collectDependencyVersions(File folder, boolean skipDevDependencies, Dependency dependency, + MultiValuedMap dependencyMap) throws AnalysisException { + final List args = new ArrayList<>(); + args.add(npmPath); + args.add("ls"); + args.add("--all"); + if (skipDevDependencies) { + args.add("--omit=dev"); + } + args.add("--package-lock-only"); + args.add("--json"); + final ProcessBuilder builder = new ProcessBuilder(args); + builder.directory(folder); + LOGGER.debug("Launching: {}", args); - final String projectName = packageJson.getString("name", ""); - final String projectVersion = packageJson.getString("version", ""); + //`npm ls` may exit with a non-zero value for recoverable problems (e.g. + // missing optional modules) while still producing the dependency tree + final String output = startAndReadStdoutToString(builder, "npm_ls"); + try { + final JSONObject tree = new JSONObject(output); + if (tree.has("error") && !tree.has("dependencies")) { + final JSONObject error = tree.getJSONObject("error"); + throw new AnalysisException(String.format("`npm ls` failed with error code %s: %s", + error.optString("code"), error.optString("summary"))); + } + final String projectName = tree.optString("name", ""); + final String projectVersion = tree.optString("version", ""); if (!projectName.isEmpty()) { dependency.setName(projectName); } if (!projectVersion.isEmpty()) { dependency.setVersion(projectVersion); } + collectDependencyVersions(tree, dependencyMap); + } catch (JSONException e) { + throw new AnalysisException("`npm ls` returned an invalid response.", e); + } + } - // Modify the payload to meet the NPM Audit API requirements - final JsonObject payload = NpmPayloadBuilder.build(packageJson, dependencyMap, - getSettings().getBoolean(Settings.KEYS.ANALYZER_NODE_AUDIT_SKIPDEV, false)); + /** + * Recursively walks the dependency tree produced by `npm ls`, adding each + * module name and version to the given map. + * + * @param node a node within the dependency tree + * @param dependencyMap the collection of module/version pairs being built + */ + private void collectDependencyVersions(JSONObject node, MultiValuedMap dependencyMap) { + final JSONObject dependencies = node.optJSONObject("dependencies"); + if (dependencies == null) { + return; + } + for (final String name : dependencies.keySet()) { + final JSONObject child = dependencies.optJSONObject(name); + if (child != null) { + final String version = child.optString("version", null); + if (version != null && !version.isEmpty()) { + dependencyMap.put(name, version); + } + collectDependencyVersions(child, dependencyMap); + } + } + } - // Submits the package payload to the nsp check service - return getSearcher().submitPackage(payload); + /** + * Workaround 64k limitation of InputStream; redirect stdout to a file that + * we will read later instead of reading directly stdout from the Process's + * InputStream which is capped at 64k. + * + * @param builder a reference to the process builder + * @param tmpFilePrefix the prefix for the temporary file the output is + * redirected to + * @return returns the standard out from the process + * @throws AnalysisException thrown when the process cannot be started or + * its output read + */ + private String startAndReadStdoutToString(ProcessBuilder builder, String tmpFilePrefix) throws AnalysisException { + try { + final File tmpFile = getSettings().getTempFile(tmpFilePrefix, "json"); + builder.redirectOutput(tmpFile); + final Process process = builder.start(); + try (ProcessReader processReader = new ProcessReader(process)) { + processReader.readAll(); + final String errOutput = processReader.getError(); + if (!StringUtils.isBlank(errOutput)) { + LOGGER.debug("Process Error Out: {}", errOutput); + } + return Files.readString(tmpFile.toPath()); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + throw new AnalysisException("npm process was interrupted.", ex); + } + } catch (IOException ioe) { + throw new AnalysisException("npm audit failure; this error can be ignored if you are not analyzing " + + "projects with an npm lockfile.", ioe); + } + } - } catch (URLConnectionFailureException e) { - this.setEnabled(false); - throw new AnalysisException("Failed to connect to the NPM Audit API (NodeAuditAnalyzer); the analyzer " - + "is being disabled and may result in false negatives.", e); - } catch (IOException e) { - LOGGER.debug("Error reading dependency or connecting to NPM Audit API", e); - this.setEnabled(false); - throw new AnalysisException("Failed to read results from the NPM Audit API (NodeAuditAnalyzer); " - + "the analyzer is being disabled and may result in false negatives.", e); - } catch (JsonException e) { - throw new AnalysisException(String.format("Failed to parse %s file from the NPM Audit API " - + "(NodeAuditAnalyzer).", file.getPath()), e); - } catch (SearchException ex) { - LOGGER.error("NodeAuditAnalyzer failed on {}", dependency.getActualFilePath()); - throw ex; + /** + * Returns the directory containing the given lock file. + * + * @param lockFile the lock file being analyzed + * @return the directory containing the lock file + */ + private static File getDependencyDirectory(File lockFile) { + final File folder = lockFile.getParentFile(); + if (!folder.isDirectory()) { + throw new IllegalArgumentException(String.format("%s should have been a directory.", folder.getAbsolutePath())); } + return folder; } } diff --git a/core/src/main/java/org/owasp/dependencycheck/analyzer/YarnAuditAnalyzer.java b/core/src/main/java/org/owasp/dependencycheck/analyzer/YarnAuditAnalyzer.java index 20c95c8aafe..809a6a0d229 100644 --- a/core/src/main/java/org/owasp/dependencycheck/analyzer/YarnAuditAnalyzer.java +++ b/core/src/main/java/org/owasp/dependencycheck/analyzer/YarnAuditAnalyzer.java @@ -286,7 +286,7 @@ private static List parseAdvisoryJsons(List advisoryJsons) final var moduleName = advisoryJson.optString("value", null); final var id = object.get("ID"); final var url = object.optString("URL", null); - final var ghsaId = extractGhsaId(url); + final var ghsaId = Advisory.ghsaIdFromUrl(url); final var issue = object.optString("Issue", null); final var severity = object.optString("Severity", null); final var vulnerableVersions = object.optString("Vulnerable Versions", null); @@ -313,15 +313,4 @@ private static List parseAdvisoryJsons(List advisoryJsons) } return advisories; } - - private static String extractGhsaId(String url) { - if (url == null || url.isEmpty()) { - return null; - } - final int lastSlashIndex = url.lastIndexOf('/'); - if (lastSlashIndex == -1 || lastSlashIndex == url.length() - 1) { - return null; - } - return url.substring(lastSlashIndex + 1); - } } diff --git a/core/src/main/java/org/owasp/dependencycheck/data/nodeaudit/Advisory.java b/core/src/main/java/org/owasp/dependencycheck/data/nodeaudit/Advisory.java index 97fab27f268..071d4f83c47 100644 --- a/core/src/main/java/org/owasp/dependencycheck/data/nodeaudit/Advisory.java +++ b/core/src/main/java/org/owasp/dependencycheck/data/nodeaudit/Advisory.java @@ -18,11 +18,11 @@ package org.owasp.dependencycheck.data.nodeaudit; - import io.github.jeremylong.openvulnerability.client.nvd.CvssV3; + +import javax.annotation.concurrent.ThreadSafe; import java.io.Serializable; import java.util.List; -import javax.annotation.concurrent.ThreadSafe; /** * The response from NPM Audit API will respond with 0 or more advisories. This @@ -258,6 +258,28 @@ public void setCwes(List cwes) { this.cwes = cwes; } + /** + * Extracts the GHSA identifier from a GitHub advisory URL such as + * `https://github.com/advisories/GHSA-c9f4-xj24-8jqx`. Audit tooling + * reports advisories with such URLs rather than a discrete GHSA + * identifier. + * + * @param url the advisory URL + * @return the GHSA identifier; or null if the URL does not reference a + * GHSA + */ + public static String ghsaIdFromUrl(String url) { + if (url == null || url.isEmpty()) { + return null; + } + final int lastSlashIndex = url.lastIndexOf('/'); + if (lastSlashIndex == -1 || lastSlashIndex == url.length() - 1) { + return null; + } + final String id = url.substring(lastSlashIndex + 1); + return id.startsWith("GHSA-") ? id : null; + } + public String getGhsaId() { return ghsaId; } diff --git a/core/src/main/java/org/owasp/dependencycheck/data/nodeaudit/NodeAuditSearch.java b/core/src/main/java/org/owasp/dependencycheck/data/nodeaudit/NodeAuditSearch.java deleted file mode 100644 index 01ff8d97962..00000000000 --- a/core/src/main/java/org/owasp/dependencycheck/data/nodeaudit/NodeAuditSearch.java +++ /dev/null @@ -1,225 +0,0 @@ -/* - * This file is part of dependency-check-core. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * Copyright (c) 2018 Steve Springett. All Rights Reserved. - */ -package org.owasp.dependencycheck.data.nodeaudit; - -import java.io.IOException; -import java.net.MalformedURLException; -import java.net.URISyntaxException; -import java.net.URL; -import java.security.SecureRandom; -import java.util.ArrayList; -import java.util.List; -import javax.annotation.concurrent.ThreadSafe; - -import org.apache.hc.client5.http.HttpResponseException; -import org.apache.hc.core5.http.ContentType; -import org.apache.hc.core5.http.Header; -import org.apache.hc.core5.http.HttpHeaders; -import org.apache.hc.core5.http.message.BasicHeader; -import org.json.JSONObject; -import org.owasp.dependencycheck.utils.DownloadFailedException; -import org.owasp.dependencycheck.utils.Downloader; -import org.owasp.dependencycheck.utils.ResourceNotFoundException; -import org.owasp.dependencycheck.utils.Settings; -import org.owasp.dependencycheck.utils.TooManyRequestsException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import jakarta.json.JsonObject; -import org.apache.commons.jcs3.access.exception.CacheException; - -import static org.owasp.dependencycheck.analyzer.NodeAuditAnalyzer.DEFAULT_URL; - -import org.owasp.dependencycheck.analyzer.exception.SearchException; -import org.owasp.dependencycheck.analyzer.exception.UnexpectedAnalysisException; -import org.owasp.dependencycheck.data.cache.DataCache; -import org.owasp.dependencycheck.data.cache.DataCacheFactory; -import org.owasp.dependencycheck.utils.Checksum; - -/** - * Class of methods to search via Node Audit API. - * - * @author Steve Springett - */ -@ThreadSafe -public class NodeAuditSearch { - - /** - * The URL for the public Node Audit API. - */ - private final URL nodeAuditUrl; - - /** - * Whether to use the Proxy when making requests. - */ - private final boolean useProxy; - /** - * The configured settings. - */ - private final Settings settings; - /** - * Used for logging. - */ - private static final Logger LOGGER = LoggerFactory.getLogger(NodeAuditSearch.class); - /** - * Persisted disk cache for `npm audit` results. - */ - private DataCache> cache; - - /** - * Creates a NodeAuditSearch for the given repository URL. - * - * @param settings the configured settings - * @throws java.net.MalformedURLException thrown if the configured URL is - * invalid - */ - public NodeAuditSearch(Settings settings) throws MalformedURLException { - final String searchUrl = settings.getString(Settings.KEYS.ANALYZER_NODE_AUDIT_URL, DEFAULT_URL); - LOGGER.debug("Node Audit Search URL: {}", searchUrl); - this.nodeAuditUrl = new URL(searchUrl); - this.settings = settings; - if (null != settings.getString(Settings.KEYS.PROXY_SERVER)) { - useProxy = true; - LOGGER.debug("Using proxy"); - } else { - useProxy = false; - LOGGER.debug("Not using proxy"); - } - if (settings.getBoolean(Settings.KEYS.ANALYZER_NODE_AUDIT_USE_CACHE, true)) { - try { - final DataCacheFactory factory = new DataCacheFactory(settings); - cache = factory.getNodeAuditCache(); - } catch (CacheException ex) { - settings.setBoolean(Settings.KEYS.ANALYZER_NODE_AUDIT_USE_CACHE, false); - LOGGER.debug("Error creating cache, disabling caching", ex); - } - } - } - - /** - * Submits the package.json file to the Node Audit API and returns a list of - * zero or more Advisories. - * - * @param packageJson the package.json file retrieved from the Dependency - * @return a List of zero or more Advisory object - * @throws SearchException if Node Audit API is unable to analyze the - * package - * @throws IOException if it's unable to connect to Node Audit API - */ - public List submitPackage(JsonObject packageJson) throws SearchException, IOException { - String key = null; - if (cache != null) { - key = Checksum.getSHA256Checksum(packageJson.toString()); - final List cached = cache.get(key); - if (cached != null) { - LOGGER.debug("cache hit for node audit: " + key); - return cached; - } - } - return submitPackage(packageJson, key, 0); - } - - /** - * Submits the package.json file to the Node Audit API and returns a list of - * zero or more Advisories. - * - * @param packageJson the package.json file retrieved from the Dependency - * @param key the key for the cache entry - * @param count the current retry count - * @return a List of zero or more Advisory object - * @throws SearchException if Node Audit API is unable to analyze the - * package - * @throws IOException if it's unable to connect to Node Audit API - */ - private List submitPackage(JsonObject packageJson, String key, int count) throws SearchException, IOException { - if (LOGGER.isTraceEnabled()) { - LOGGER.trace("----------------------------------------"); - LOGGER.trace("Node Audit Payload:"); - LOGGER.trace(packageJson.toString()); - LOGGER.trace("----------------------------------------"); - LOGGER.trace("----------------------------------------"); - } - final List
additionalHeaders = new ArrayList<>(); - additionalHeaders.add(new BasicHeader(HttpHeaders.USER_AGENT, "npm/6.1.0 node/v10.5.0 linux x64")); - additionalHeaders.add(new BasicHeader("npm-in-ci", "false")); - additionalHeaders.add(new BasicHeader("npm-scope", "")); - additionalHeaders.add(new BasicHeader("npm-session", generateRandomSession())); - - try { - final String response = Downloader.getInstance().postBasedFetchContent(nodeAuditUrl.toURI(), - packageJson.toString(), ContentType.APPLICATION_JSON, additionalHeaders); - final JSONObject jsonResponse = new JSONObject(response); - final NpmAuditParser parser = new NpmAuditParser(); - final List advisories = parser.parse(jsonResponse); - if (cache != null) { - cache.put(key, advisories); - } - return advisories; - } catch (RuntimeException | URISyntaxException | TooManyRequestsException | ResourceNotFoundException ex) { - LOGGER.debug("Error connecting to Node Audit API. Error: {}", - ex.getMessage()); - throw new SearchException("Could not connect to Node Audit API: " + ex.getMessage(), ex); - } catch (DownloadFailedException e) { - if (e.getCause() instanceof HttpResponseException) { - final HttpResponseException hre = (HttpResponseException) e.getCause(); - switch (hre.getStatusCode()) { - case 503: - LOGGER.debug("Node Audit API returned `{} {}` - retrying request.", - hre.getStatusCode(), hre.getReasonPhrase()); - if (count < 5) { - final int next = count + 1; - try { - Thread.sleep(1500L * next); - } catch (InterruptedException ex) { - Thread.currentThread().interrupt(); - throw new UnexpectedAnalysisException(ex); - } - return submitPackage(packageJson, key, next); - } - throw new SearchException("Could not perform Node Audit analysis - service returned a 503.", e); - case 400: - LOGGER.debug("Invalid payload submitted to Node Audit API. Received response code: {} {}", - hre.getStatusCode(), hre.getReasonPhrase()); - throw new SearchException("Could not perform Node Audit analysis. Invalid payload submitted to Node Audit API.", e); - default: - LOGGER.debug("Could not connect to Node Audit API. Received response code: {} {}", - hre.getStatusCode(), hre.getReasonPhrase()); - throw new IOException("Could not connect to Node Audit API", e); - } - } else { - LOGGER.debug("Could not connect to Node Audit API. Received generic DownloadException", e); - throw new IOException("Could not connect to Node Audit API", e); - } - } - } - - /** - * Generates a random 16 character lower-case hex string. - * - * @return a random 16 character lower-case hex string - */ - private String generateRandomSession() { - final int length = 16; - final SecureRandom r = new SecureRandom(); - final StringBuilder sb = new StringBuilder(); - while (sb.length() < length) { - sb.append(Integer.toHexString(r.nextInt())); - } - return sb.substring(0, length); - } -} diff --git a/core/src/main/java/org/owasp/dependencycheck/data/nodeaudit/NpmCliAuditParser.java b/core/src/main/java/org/owasp/dependencycheck/data/nodeaudit/NpmCliAuditParser.java new file mode 100644 index 00000000000..08c0c24a4ff --- /dev/null +++ b/core/src/main/java/org/owasp/dependencycheck/data/nodeaudit/NpmCliAuditParser.java @@ -0,0 +1,135 @@ +/* + * This file is part of dependency-check-core. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Copyright (c) 2026 The OWASP Foundation. All Rights Reserved. + */ +package org.owasp.dependencycheck.data.nodeaudit; + +import io.github.jeremylong.openvulnerability.client.nvd.CvssV3; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.owasp.dependencycheck.utils.CvssUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; + +/** + * Parser for the JSON output of `npm audit --json` (auditReportVersion 2, as + * produced by npm 7 and later). + * + *

+ * The report contains a `vulnerabilities` object keyed by module name. Each + * entry's `via` array holds either advisory objects (for modules that are the + * direct subject of an advisory) or strings naming other vulnerable modules + * (for modules that are only affected transitively). Only advisory objects are + * converted into {@link Advisory} instances; transitive references would + * otherwise duplicate the advisory of the module they point at.

+ */ +public class NpmCliAuditParser { + + /** + * The logger. + */ + private static final Logger LOGGER = LoggerFactory.getLogger(NpmCliAuditParser.class); + + /** + * Parses the JSON report from `npm audit --json`. + * + * @param report the JSON report to parse + * @return a list of zero or more Advisory objects + * @throws JSONException thrown if the JSON is not of the expected schema + */ + public List parse(JSONObject report) throws JSONException { + LOGGER.debug("Parsing npm audit report"); + final List advisories = new ArrayList<>(); + final JSONObject vulnerabilities = report.optJSONObject("vulnerabilities"); + if (vulnerabilities == null) { + return advisories; + } + for (final String moduleName : vulnerabilities.keySet()) { + final JSONObject vulnerability = vulnerabilities.getJSONObject(moduleName); + final JSONArray via = vulnerability.optJSONArray("via"); + for (int i = 0; via != null && i < via.length(); i++) { + final Object entry = via.get(i); + if (entry instanceof JSONObject) { + advisories.add(parseAdvisory((JSONObject) entry)); + } + } + } + return advisories; + } + + /** + * Parses a single advisory object from the `via` array of an npm audit + * report entry. + * + * @param object the JSON object containing the advisory + * @return the Advisory object + * @throws JSONException thrown if the JSON is not of the expected schema + */ + private Advisory parseAdvisory(JSONObject object) throws JSONException { + final Advisory advisory = new Advisory(); + final String url = object.optString("url", null); + final String title = object.optString("title", null); + final String ghsaId = Advisory.ghsaIdFromUrl(url); + if (ghsaId != null) { + advisory.setGhsaId(ghsaId); + } else { + //fall back on the numeric GitHub Advisory Database identifier + advisory.setGhsaId(object.optString("source", null)); + } + advisory.setTitle(title); + advisory.setOverview(title); + if (url != null) { + advisory.setReferences("- " + url); + } + advisory.setModuleName(object.optString("dependency", object.optString("name", null))); + advisory.setSeverity(object.optString("severity", null)); + advisory.setVulnerableVersions(object.optString("range", null)); + + final JSONArray jsonCwes = object.optJSONArray("cwe"); + final List stringCwes = new ArrayList<>(); + if (jsonCwes != null) { + for (int j = 0; j < jsonCwes.length(); j++) { + stringCwes.add(jsonCwes.getString(j)); + } + } + advisory.setCwes(stringCwes); + + final JSONObject jsonCvss = object.optJSONObject("cvss"); + if (jsonCvss != null) { + final double baseScore = jsonCvss.optDouble("score", -1.0); + final String vector = jsonCvss.optString("vectorString", null); +if (baseScore >= 0.0 && vector != null && !"null".equals(vector)) { + if (vector.startsWith("CVSS:3")) { + try { + final CvssV3 cvss = CvssUtil.vectorToCvssV3(vector, baseScore); + advisory.setCvssV3(cvss); + } catch (IllegalArgumentException iae) { + LOGGER.warn("Invalid CVSS vector format encountered in npm audit results '{}': {} ", vector, iae.getMessage()); + } + } else { + LOGGER.warn("Unsupported CVSS vector format in npm audit results, please file a feature " + + "request at https://github.com/dependency-check/DependencyCheck/issues/new/choose to " + + "support vector format '{}' ", vector); + } + } + } + return advisory; + } +} diff --git a/core/src/main/java/org/owasp/dependencycheck/data/nodeaudit/NpmPayloadBuilder.java b/core/src/main/java/org/owasp/dependencycheck/data/nodeaudit/NpmPayloadBuilder.java deleted file mode 100644 index 478c1d5f2bc..00000000000 --- a/core/src/main/java/org/owasp/dependencycheck/data/nodeaudit/NpmPayloadBuilder.java +++ /dev/null @@ -1,292 +0,0 @@ -/* - * This file is part of dependency-check-core. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * Copyright (c) 2017 Steve Springett. All Rights Reserved. - */ -package org.owasp.dependencycheck.data.nodeaudit; - -import org.owasp.dependencycheck.analyzer.NodePackageAnalyzer; - -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import java.util.TreeMap; -import java.util.stream.Collectors; -import jakarta.json.Json; -import jakarta.json.JsonObject; -import jakarta.json.JsonObjectBuilder; -import jakarta.json.JsonString; -import jakarta.json.JsonValue; -import javax.annotation.concurrent.ThreadSafe; -import org.apache.commons.collections4.MultiValuedMap; - -/** - * Class used to create the payload to submit to the NPM Audit API service. - * - * @author Steve Springett - * @author Jeremy Long - */ -@ThreadSafe -public final class NpmPayloadBuilder { - - /** - * Private constructor for utility class. - */ - private NpmPayloadBuilder() { - //empty - } - - /** - * Builds an npm audit API payload. - * - * @param lockJson the package-lock.json - * @param packageJson the package.json - * @param dependencyMap a collection of module/version pairs that is - * populated while building the payload - * @param skipDevDependencies whether devDependencies should be skipped - * @return the npm audit API payload - */ - public static JsonObject build(JsonObject lockJson, JsonObject packageJson, - MultiValuedMap dependencyMap, boolean skipDevDependencies) { - final JsonObjectBuilder payloadBuilder = Json.createObjectBuilder(); - addProjectInfo(packageJson, payloadBuilder); - - // NPM Audit expects 'requires' to be an object containing key/value - // pairs corresponding to the module name (key) and version (value). - final JsonObjectBuilder requiresBuilder = Json.createObjectBuilder(); - - if (packageJson.containsKey("dependencies")) { - packageJson.getJsonObject("dependencies").entrySet() - .stream() - .collect(Collectors.toMap( - Map.Entry::getKey, - Map.Entry::getValue, - (oldValue, newValue) -> newValue, TreeMap::new)) - .forEach((key, value) -> { - if (NodePackageAnalyzer.shouldSkipDependency(key, ((JsonString) value).getString())) { - return; - } - requiresBuilder.add(key, value); - dependencyMap.put(key, value.toString()); - }); - } - - if (!skipDevDependencies && packageJson.containsKey("devDependencies")) { - packageJson.getJsonObject("devDependencies").entrySet() - .stream() - .collect(Collectors.toMap( - Map.Entry::getKey, - Map.Entry::getValue, - (oldValue, newValue) -> newValue, TreeMap::new)) - .forEach((key, value) -> { - if (NodePackageAnalyzer.shouldSkipDependency(key, ((JsonString) value).getString())) { - return; - } - requiresBuilder.add(key, value); - dependencyMap.put(key, value.toString()); - }); - } - - payloadBuilder.add("requires", requiresBuilder.build()); - - final JsonObjectBuilder dependenciesBuilder = Json.createObjectBuilder(); - final int lockJsonVersion = lockJson.containsKey("lockfileVersion") ? lockJson.getInt("lockfileVersion") : 1; - JsonObject dependencies = lockJson.getJsonObject("dependencies"); - if (lockJsonVersion >= 2 && dependencies == null) { - dependencies = lockJson.getJsonObject("packages"); - } - - if (dependencies != null) { - dependencies.forEach((k, value) -> { - String key = k; - final int indexOfNodeModule = key.lastIndexOf(NodePackageAnalyzer.NODE_MODULES_DIRNAME + "/"); - if (indexOfNodeModule >= 0) { - key = key.substring(indexOfNodeModule + NodePackageAnalyzer.NODE_MODULES_DIRNAME.length() + 1); - } - - JsonObject dep = ((JsonObject) value); - - //After Version 3, dependencies can't be taken directly from package-lock.json - if (lockJsonVersion > 2 && dep.containsKey("dependencies") && dep.get("dependencies") instanceof JsonObject) { - final JsonObjectBuilder depBuilder = Json.createObjectBuilder(dep); - depBuilder.remove("dependencies"); - depBuilder.add("requires", dep.get("dependencies")); - dep = depBuilder.build(); - } - - final String version = dep.getString("version", ""); - final boolean isDev = dep.getBoolean("dev", false); - if (skipDevDependencies && isDev) { - return; - } - if (NodePackageAnalyzer.shouldSkipDependency(key, version)) { - return; - } - dependencyMap.put(key, version); - dependenciesBuilder.add(key, buildDependencies(dep, dependencyMap)); - }); - } - payloadBuilder.add("dependencies", dependenciesBuilder.build()); - - addConstantElements(payloadBuilder); - return payloadBuilder.build(); - } - - /** - * Attempts to build the request data for NPM Audit API call. This may - * produce a payload that will fail. - * - * @param packageJson a raw package-lock.json file - * @param dependencyMap a collection of module/version pairs that is - * @param skipDevDependencies whether devDependencies should be skipped - * populated while building the payload - * @return the JSON payload for NPN Audit - */ - public static JsonObject build(JsonObject packageJson, MultiValuedMap dependencyMap, - final boolean skipDevDependencies) { - final JsonObjectBuilder payloadBuilder = Json.createObjectBuilder(); - addProjectInfo(packageJson, payloadBuilder); - - // NPM Audit expects 'requires' to be an object containing key/value - // pairs corresponding to the module name (key) and version (value). - final JsonObjectBuilder requiresBuilder = Json.createObjectBuilder(); - final JsonObjectBuilder dependenciesBuilder = Json.createObjectBuilder(); - - final JsonObject dependencies = packageJson.getJsonObject("dependencies"); - if (dependencies != null) { - dependencies.forEach((name, value) -> { - final String version; - if (value.getValueType() == JsonValue.ValueType.OBJECT) { - final JsonObject dep = ((JsonObject) value); - version = Optional.ofNullable(dep.getJsonString("version")) - .map(JsonString::getString) - .orElse(null); - - final boolean isDev = dep.getBoolean("dev", false); - if (skipDevDependencies && isDev) { - return; - } - if (NodePackageAnalyzer.shouldSkipDependency(name, version)) { - return; - } - dependencyMap.put(name, version); - dependenciesBuilder.add(name, buildDependencies(dep, dependencyMap)); - } else { - //TODO I think the following is dead code and no real "dependencies" - // section in a lock file will look like this - final String tmp = value.toString(); - if (tmp.startsWith("\"")) { - version = tmp.substring(1, tmp.length() - 1); - } else { - version = tmp; - } - } - requiresBuilder.add(name, Objects.isNull(version) ? "*" : "^" + version); - }); - } - payloadBuilder.add("requires", requiresBuilder.build()); - - payloadBuilder.add("dependencies", dependenciesBuilder.build()); - - addConstantElements(payloadBuilder); - return payloadBuilder.build(); - } - - /** - * Adds the project name and version to the npm audit API payload. - * - * @param packageJson a reference to the package-lock.json - * @param payloadBuilder a reference to the npm audit API payload builder - */ - private static void addProjectInfo(JsonObject packageJson, final JsonObjectBuilder payloadBuilder) { - final String projectName = packageJson.getString("name", ""); - final String projectVersion = packageJson.getString("version", ""); - if (!projectName.isEmpty()) { - payloadBuilder.add("name", projectName); - } - if (!projectVersion.isEmpty()) { - payloadBuilder.add("version", projectVersion); - } - } - - /** - * Adds the constant data elements to the npm audit API payload. - * - * @param payloadBuilder a reference to the npm audit API payload builder - */ - private static void addConstantElements(final JsonObjectBuilder payloadBuilder) { - payloadBuilder.add("install", Json.createArrayBuilder().build()); - payloadBuilder.add("remove", Json.createArrayBuilder().build()); - payloadBuilder.add("metadata", Json.createObjectBuilder() - .add("npm_version", "6.9.0") - .add("node_version", "v10.5.0") - .add("platform", "linux") - ); - } - - /** - * Recursively builds the dependency structure - copying only the needed - * items from the package-lock.json into the npm audit API payload. - * - * @param dep the parent dependency - * @param dependencyMap the collection of child dependencies - * @return the dependencies structure needed for the npm audit API payload - */ - private static JsonObject buildDependencies(JsonObject dep, MultiValuedMap dependencyMap) { - final JsonObjectBuilder depBuilder = Json.createObjectBuilder(); - Optional.ofNullable(dep.getJsonString("version")) - .map(JsonString::getString) - .ifPresent(version -> depBuilder.add("version", version)); - - //not installed package (like, dependency of an optional dependency) doesn't contains integrity - if (dep.containsKey("integrity")) { - depBuilder.add("integrity", dep.getString("integrity")); - } - if (dep.containsKey("requires")) { - final JsonObjectBuilder requiresBuilder = Json.createObjectBuilder(); - dep.getJsonObject("requires").forEach((key, value) -> { - if (NodePackageAnalyzer.shouldSkipDependency(key, ((JsonString) value).getString())) { - return; - } - - requiresBuilder.add(key, value); - }); - depBuilder.add("requires", requiresBuilder.build()); - } - if (dep.containsKey("dependencies")) { - final JsonObjectBuilder dependeciesBuilder = Json.createObjectBuilder(); - dep.getJsonObject("dependencies").forEach((key, value) -> { - if (value.getValueType() == JsonValue.ValueType.OBJECT) { - final JsonObject currentDep = (JsonObject) value; - final String v = currentDep.getString("version"); - dependencyMap.put(key, v); - dependeciesBuilder.add(key, buildDependencies(currentDep, dependencyMap)); - } else { - final String tmp = value.toString(); - final String v; - if (tmp.startsWith("\"")) { - v = tmp.substring(1, tmp.length() - 1); - } else { - v = tmp; - } - dependencyMap.put(key, v); - dependeciesBuilder.add(key, v); - } - }); - depBuilder.add("dependencies", dependeciesBuilder.build()); - } - return depBuilder.build(); - } -} diff --git a/core/src/main/java/org/owasp/dependencycheck/data/nodeaudit/package-info.java b/core/src/main/java/org/owasp/dependencycheck/data/nodeaudit/package-info.java index 094698c6b7e..cf018de1925 100644 --- a/core/src/main/java/org/owasp/dependencycheck/data/nodeaudit/package-info.java +++ b/core/src/main/java/org/owasp/dependencycheck/data/nodeaudit/package-info.java @@ -1,7 +1,7 @@ /** * - * Contains classes related to searching via Node Audit API.

+ * Contains classes related to parsing the results of npm, yarn, and pnpm audits.

* - * These are used to abstract Node Audit searching away from OWASP Dependency Check so they can be reused elsewhere. + * These are used to abstract npm audit result parsing away from OWASP Dependency Check so they can be reused elsewhere. */ package org.owasp.dependencycheck.data.nodeaudit; diff --git a/core/src/main/resources/dependencycheck.properties b/core/src/main/resources/dependencycheck.properties index 081a8fcdbc9..8efe04715da 100644 --- a/core/src/main/resources/dependencycheck.properties +++ b/core/src/main/resources/dependencycheck.properties @@ -86,8 +86,7 @@ analyzer.ossindex.enabled=true analyzer.ossindex.url=https://api.guide.sonatype.com analyzer.ossindex.use.cache=true -# the URL for searching NPM Audit API -analyzer.node.audit.url=https://registry.npmjs.org/-/npm/v1/security/audits +# whether the results of `npm audit` invocations should be cached analyzer.node.audit.use.cache=true # the number of nested archives that will be searched. diff --git a/core/src/test/java/org/owasp/dependencycheck/data/nodeaudit/NodeAuditSearchTest.java b/core/src/test/java/org/owasp/dependencycheck/data/nodeaudit/NodeAuditSearchTest.java deleted file mode 100644 index 18a4ee726c4..00000000000 --- a/core/src/test/java/org/owasp/dependencycheck/data/nodeaudit/NodeAuditSearchTest.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * This file is part of dependency-check-core. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * Copyright (c) 2017 Steve Springett. All Rights Reserved. - */ -package org.owasp.dependencycheck.data.nodeaudit; - -import org.owasp.dependencycheck.BaseTest; - -class NodeAuditSearchTest extends BaseTest { - -// Tested as part of the NodeAuditAnalyzerIT. Adding this test can cause build failures due to an external service. -// private static final Logger LOGGER = LoggerFactory.getLogger(NodeAuditSearchTest.class); -// private NodeAuditSearch searcher; -// -// @BeforeEach -// @Override -// void setUp() throws Exception { -// super.setUp(); -// searcher = new NodeAuditSearch(getSettings()); -// } -// -// @Test -// void testNodeAuditSearchPositive() throws Exception { -// InputStream in = BaseTest.getResourceAsStream(this, "nodeaudit/package-lock.json"); -// try (JsonReader jsonReader = Json.createReader(in)) { -// final JsonObject packageJson = jsonReader.readObject(); -// final JsonObject payload = SanitizePackage.sanitize(packageJson); -// final List advisories = searcher.submitPackage(payload); -// URLConnectionFailureException ex = assertThrows(URLConnectionFailureException.class, -// () -> searcher.submitPackage(payload)); -// assumeFalse(ex.getMessage().contains("Unable to connect to ")); -// } -// -// //this should result in a cache hit -// in = BaseTest.getResourceAsStream(this, "nodeaudit/package-lock.json"); -// try (JsonReader jsonReader = Json.createReader(in)) { -// final JsonObject packageJson = jsonReader.readObject(); -// final JsonObject payload = SanitizePackage.sanitize(packageJson); -// URLConnectionFailureException ex = assertThrows(URLConnectionFailureException.class, -// () -> searcher.submitPackage(payload)); -// assumeFalse(ex.getMessage().contains("Unable to connect to ")); -// } -// } -// -// void testNodeAuditSearchNegative() throws Exception { -// InputStream in = BaseTest.getResourceAsStream(this, "nodeaudit/package.json"); -// try (JsonReader jsonReader = Json.createReader(in)) { -// final JsonObject packageJson = jsonReader.readObject(); -// final JsonObject sanitizedJson = SanitizePackage.sanitize(packageJson); -// URLConnectionFailureException ex = assertThrows(URLConnectionFailureException.class, -// () -> searcher.submitPackage(sanitizedJson)); -// assumeFalse(ex.getMessage().contains("Unable to connect to ")); -// } -// } -} diff --git a/core/src/test/java/org/owasp/dependencycheck/data/nodeaudit/NpmCliAuditParserTest.java b/core/src/test/java/org/owasp/dependencycheck/data/nodeaudit/NpmCliAuditParserTest.java new file mode 100644 index 00000000000..535b2ab2776 --- /dev/null +++ b/core/src/test/java/org/owasp/dependencycheck/data/nodeaudit/NpmCliAuditParserTest.java @@ -0,0 +1,71 @@ +package org.owasp.dependencycheck.data.nodeaudit; + +import org.json.JSONObject; +import org.junit.jupiter.api.Test; +import org.owasp.dependencycheck.BaseTest; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +class NpmCliAuditParserTest extends BaseTest { + + private JSONObject loadReport() throws IOException { + try (InputStream in = BaseTest.getResourceAsStream(this, "nodeaudit/npm-audit-report.json")) { + return new JSONObject(new String(in.readAllBytes(), StandardCharsets.UTF_8)); + } + } + + @Test + void testParse() throws IOException { + final List advisories = new NpmCliAuditParser().parse(loadReport()); + + //two advisory objects for ajv, one for uglify-js; the transitive + //har-validator entry (string `via`) must not create an advisory + assertEquals(3, advisories.size()); + + final Advisory ajv = findByGhsaId(advisories, "GHSA-v88g-cgmw-v5xw"); + assertEquals("ajv", ajv.getModuleName()); + assertEquals("Prototype Pollution in Ajv", ajv.getTitle()); + assertEquals("Prototype Pollution in Ajv", ajv.getOverview()); + assertEquals("- https://github.com/advisories/GHSA-v88g-cgmw-v5xw", ajv.getReferences()); + assertEquals("moderate", ajv.getSeverity()); + assertEquals("<6.12.3", ajv.getVulnerableVersions()); + assertEquals(List.of("CWE-915", "CWE-1321"), ajv.getCwes()); + assertNotNull(ajv.getCvssV3()); + assertEquals(5.6, ajv.getCvssV3().getCvssData().getBaseScore(), 0.01); + //the installed version is not part of the report; it is resolved later + assertNull(ajv.getVersion()); + + //a zero score with a null vector must not produce a CVSS record + final Advisory ajvRedos = findByGhsaId(advisories, "GHSA-2g4f-4pwh-qvx6"); + assertNull(ajvRedos.getCvssV3()); + + final Advisory uglify = findByGhsaId(advisories, "GHSA-c9f4-xj24-8jqx"); + assertEquals("uglify-js", uglify.getModuleName()); + assertEquals("high", uglify.getSeverity()); + assertEquals("<2.6.0", uglify.getVulnerableVersions()); + assertEquals(List.of("CWE-1333"), uglify.getCwes()); + assertNotNull(uglify.getCvssV3()); + } + + private static Advisory findByGhsaId(List advisories, String ghsaId) { + final Advisory advisory = advisories.stream() + .filter(a -> ghsaId.equals(a.getGhsaId())) + .findFirst().orElse(null); + assertNotNull(advisory, "Advisory " + ghsaId + " not found"); + return advisory; + } + + @Test + void testParseEmptyReport() { + final List advisories = new NpmCliAuditParser().parse( + new JSONObject("{\"auditReportVersion\": 2, \"vulnerabilities\": {}}")); + assertEquals(0, advisories.size()); + } +} diff --git a/core/src/test/java/org/owasp/dependencycheck/data/nodeaudit/NpmPayloadBuilderTest.java b/core/src/test/java/org/owasp/dependencycheck/data/nodeaudit/NpmPayloadBuilderTest.java deleted file mode 100644 index 59c612e6f2e..00000000000 --- a/core/src/test/java/org/owasp/dependencycheck/data/nodeaudit/NpmPayloadBuilderTest.java +++ /dev/null @@ -1,185 +0,0 @@ -/* - * This file is part of dependency-check-core. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * Copyright (c) 2017 Steve Springett. All Rights Reserved. - */ -package org.owasp.dependencycheck.data.nodeaudit; - -import jakarta.json.Json; -import jakarta.json.JsonObject; -import jakarta.json.JsonObjectBuilder; -import jakarta.json.JsonReader; -import org.apache.commons.collections4.MultiValuedMap; -import org.apache.commons.collections4.multimap.HashSetValuedHashMap; -import org.junit.jupiter.api.Test; -import org.owasp.dependencycheck.BaseTest; - -import java.io.InputStream; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; - -class NpmPayloadBuilderTest { - - @Test - void testSanitizer() { - JsonObjectBuilder builder = Json.createObjectBuilder() - .add("name", "my app") - .add("version", "1.0.0") - .add("random", "random") - .add("lockfileVersion", 1) - .add("requires", true) - .add("dependencies", - Json.createObjectBuilder() - .add("abbrev", - Json.createObjectBuilder() - .add("version", "1.1.1") - .add("resolved", "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz") - .add("integrity", "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==") - .add("dev", true) - ) - .add("node_modules/jest-resolve", - Json.createObjectBuilder() - .add("dev", true) - .add("optional", true) - .add("peer", true)) - ); - - JsonObject packageJson = builder.build(); - final MultiValuedMap dependencyMap = new HashSetValuedHashMap<>(); - JsonObject sanitized = NpmPayloadBuilder.build(packageJson, dependencyMap, false); - - assertTrue(sanitized.containsKey("name")); - assertTrue(sanitized.containsKey("version")); - assertTrue(sanitized.containsKey("dependencies")); - assertTrue(sanitized.containsKey("requires")); - - JsonObject dependencies = sanitized.getJsonObject("dependencies"); - assertTrue(dependencies.containsKey("node_modules/jest-resolve")); - - JsonObject requires = sanitized.getJsonObject("requires"); - assertTrue(requires.containsKey("abbrev")); - assertEquals("^1.1.1", requires.getString("abbrev")); - assertEquals("*", requires.getString("node_modules/jest-resolve")); - - assertFalse(sanitized.containsKey("lockfileVersion")); - assertFalse(sanitized.containsKey("random")); - } - - - @Test - void testSkippedDependencies() { - JsonObjectBuilder builder = Json.createObjectBuilder() - .add("name", "my app") - .add("version", "1.0.0") - .add("random", "random") - .add("lockfileVersion", 1) - .add("requires", true) - .add("dependencies", - Json.createObjectBuilder() - .add("abbrev", - Json.createObjectBuilder() - .add("version", "1.1.1") - .add("resolved", "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz") - .add("integrity", "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==") - .add("dev", true) - ) - .add("react-dom", - Json.createObjectBuilder() - .add("version", "npm:@hot-loader/react-dom") - ) - .add("fake_submodule", - Json.createObjectBuilder() - .add("version", "file:fake_submodule") - ) - ); - - JsonObject packageJson = builder.build(); - final MultiValuedMap dependencyMap = new HashSetValuedHashMap<>(); - JsonObject sanitized = NpmPayloadBuilder.build(packageJson, dependencyMap, false); - - assertTrue(sanitized.containsKey("name")); - assertTrue(sanitized.containsKey("version")); - assertTrue(sanitized.containsKey("dependencies")); - assertTrue(sanitized.containsKey("requires")); - - JsonObject requires = sanitized.getJsonObject("requires"); - assertTrue(requires.containsKey("abbrev")); - assertEquals("^1.1.1", requires.getString("abbrev")); - - //local and alias need to be skipped - assertFalse(requires.containsKey("react-dom")); - assertFalse(requires.containsKey("fake_submodule")); - - assertFalse(sanitized.containsKey("lockfileVersion")); - assertFalse(sanitized.containsKey("random")); - } - - @Test - void testSanitizePackage() { - InputStream in = BaseTest.getResourceAsStream(this, "nodeaudit/package-lock.json"); - final MultiValuedMap dependencyMap = new HashSetValuedHashMap<>(); - try (JsonReader jsonReader = Json.createReader(in)) { - JsonObject packageJson = jsonReader.readObject(); - JsonObject sanitized = NpmPayloadBuilder.build(packageJson, dependencyMap, false); - - assertTrue(sanitized.containsKey("name")); - assertTrue(sanitized.containsKey("version")); - assertTrue(sanitized.containsKey("dependencies")); - assertTrue(sanitized.containsKey("requires")); - - JsonObject requires = sanitized.getJsonObject("requires"); - assertTrue(requires.containsKey("bcrypt-nodejs")); - assertEquals("^0.0.3", requires.getString("bcrypt-nodejs")); - - assertFalse(sanitized.containsKey("lockfileVersion")); - assertFalse(sanitized.containsKey("random")); - } - } - - @Test - void testPayloadWithLockAndPackage() { - InputStream lock = BaseTest.getResourceAsStream(this, "nodeaudit/package-lock.json"); - InputStream json = BaseTest.getResourceAsStream(this, "nodeaudit/package.json"); - final MultiValuedMap dependencyMap = new HashSetValuedHashMap<>(); - try (JsonReader jsonReader = Json.createReader(json); JsonReader lockReader = Json.createReader(lock)) { - JsonObject packageJson = jsonReader.readObject(); - JsonObject lockJson = lockReader.readObject(); - JsonObject sanitized = NpmPayloadBuilder.build(lockJson, packageJson, dependencyMap, false); - - assertTrue(sanitized.containsKey("name")); - assertTrue(sanitized.containsKey("version")); - assertTrue(sanitized.containsKey("dependencies")); - assertTrue(sanitized.containsKey("requires")); - - JsonObject requires = sanitized.getJsonObject("requires"); - assertTrue(requires.containsKey("bcrypt-nodejs")); - assertEquals("0.0.3", requires.getString("bcrypt-nodejs")); - - assertFalse(sanitized.containsKey("lockfileVersion")); - assertFalse(sanitized.containsKey("random")); - - assertTrue(sanitized.containsKey("name")); - assertTrue(sanitized.containsKey("version")); - assertTrue(sanitized.containsKey("dependencies")); - assertTrue(sanitized.containsKey("requires")); - - //local and alias need to be skipped - assertFalse(requires.containsKey("react-dom")); - assertFalse(requires.containsKey("fake_submodule")); - } - } -} diff --git a/core/src/test/resources/dependencycheck.properties b/core/src/test/resources/dependencycheck.properties index 5ce9e843d34..61d6bc390c1 100644 --- a/core/src/test/resources/dependencycheck.properties +++ b/core/src/test/resources/dependencycheck.properties @@ -74,8 +74,6 @@ analyzer.central.query=%s?q=1:%s&wt=xml analyzer.central.retry.count=3 analyzer.central.parallel.analysis=false -# the URL for searching NPM Audit API -analyzer.node.audit.url=https://registry.npmjs.org/-/npm/v1/security/audits analyzer.retirejs.enabled=true analyzer.retirejs.repo.validforhours=24 analyzer.retirejs.repo.js.url=https://raw.githubusercontent.com/Retirejs/retire.js/master/repository/jsrepository.json diff --git a/core/src/test/resources/nodeaudit/npm-audit-report.json b/core/src/test/resources/nodeaudit/npm-audit-report.json new file mode 100644 index 00000000000..f2e0eac3bf2 --- /dev/null +++ b/core/src/test/resources/nodeaudit/npm-audit-report.json @@ -0,0 +1,119 @@ +{ + "auditReportVersion": 2, + "vulnerabilities": { + "ajv": { + "name": "ajv", + "severity": "moderate", + "isDirect": false, + "via": [ + { + "source": 1097685, + "name": "ajv", + "dependency": "ajv", + "title": "Prototype Pollution in Ajv", + "url": "https://github.com/advisories/GHSA-v88g-cgmw-v5xw", + "severity": "moderate", + "cwe": [ + "CWE-915", + "CWE-1321" + ], + "cvss": { + "score": 5.6, + "vectorString": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:L" + }, + "range": "<6.12.3" + }, + { + "source": 1113714, + "name": "ajv", + "dependency": "ajv", + "title": "ajv has ReDoS when using `$data` option", + "url": "https://github.com/advisories/GHSA-2g4f-4pwh-qvx6", + "severity": "moderate", + "cwe": [ + "CWE-400", + "CWE-1333" + ], + "cvss": { + "score": 0, + "vectorString": null + }, + "range": "<6.14.0" + } + ], + "effects": [ + "har-validator" + ], + "range": "<=6.12.6", + "nodes": [ + "node_modules/ajv" + ], + "fixAvailable": false + }, + "har-validator": { + "name": "har-validator", + "severity": "moderate", + "isDirect": false, + "via": [ + "ajv" + ], + "effects": [ + "request" + ], + "range": "3.3.0 - 5.1.5", + "nodes": [ + "node_modules/har-validator" + ], + "fixAvailable": false + }, + "uglify-js": { + "name": "uglify-js", + "severity": "high", + "isDirect": false, + "via": [ + { + "source": 1091686, + "name": "uglify-js", + "dependency": "uglify-js", + "title": "Regular Expression Denial of Service in uglify-js", + "url": "https://github.com/advisories/GHSA-c9f4-xj24-8jqx", + "severity": "high", + "cwe": [ + "CWE-1333" + ], + "cvss": { + "score": 7.5, + "vectorString": "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H" + }, + "range": "<2.6.0" + } + ], + "effects": [ + "swig" + ], + "range": "<2.6.0", + "nodes": [ + "node_modules/uglify-js" + ], + "fixAvailable": false + } + }, + "metadata": { + "vulnerabilities": { + "info": 0, + "low": 0, + "moderate": 3, + "high": 1, + "critical": 0, + "total": 4 + }, + "dependencies": { + "prod": 233, + "dev": 851, + "optional": 168, + "peer": 0, + "peerOptional": 0, + "total": 1199 + } + } +} diff --git a/maven/src/main/java/org/owasp/dependencycheck/maven/BaseDependencyCheckMojo.java b/maven/src/main/java/org/owasp/dependencycheck/maven/BaseDependencyCheckMojo.java index ff6511f3c9f..f6e1a3ccaec 100644 --- a/maven/src/main/java/org/owasp/dependencycheck/maven/BaseDependencyCheckMojo.java +++ b/maven/src/main/java/org/owasp/dependencycheck/maven/BaseDependencyCheckMojo.java @@ -281,6 +281,12 @@ public abstract class BaseDependencyCheckMojo extends AbstractMojo implements Ma @SuppressWarnings("CanBeFinal") @Parameter(property = "pathToPnpm") private String pathToPnpm; + /** + * Sets the path to `npm`. + */ + @SuppressWarnings("CanBeFinal") + @Parameter(property = "pathToNpm") + private String pathToNpm; /** * Use pom dependency information for snapshot dependencies that are part of * the Maven reactor while aggregate scanning a multi-module project. @@ -546,13 +552,6 @@ public abstract class BaseDependencyCheckMojo extends AbstractMojo implements Ma @Parameter(property = "nodeAuditAnalyzerEnabled") private Boolean nodeAuditAnalyzerEnabled; - /** - * The Node Audit API URL for the Node Audit Analyzer. - */ - @SuppressWarnings("CanBeFinal") - @Parameter(property = "nodeAuditAnalyzerUrl") - private String nodeAuditAnalyzerUrl; - /** * Sets whether or not the Yarn Audit Analyzer should be used. */ @@ -2320,6 +2319,7 @@ protected void populateSettings() throws MojoFailureException, MojoExecutionExce settings.setStringIfNotNull(Settings.KEYS.ANALYZER_GOLANG_PATH, pathToGo); settings.setStringIfNotNull(Settings.KEYS.ANALYZER_YARN_PATH, pathToYarn); settings.setStringIfNotNull(Settings.KEYS.ANALYZER_PNPM_PATH, pathToPnpm); + settings.setStringIfNotNull(Settings.KEYS.ANALYZER_NPM_PATH, pathToNpm); // use global maven proxy if provided and system properties are not set final Proxy mavenProxyHttp = getMavenProxy(PROTOCOL_HTTP); @@ -2423,7 +2423,6 @@ protected void populateSettings() throws MojoFailureException, MojoExecutionExce settings.setBooleanIfNotNull(Settings.KEYS.ANALYZER_CPANFILE_ENABLED, cpanfileAnalyzerEnabled); settings.setBooleanIfNotNull(Settings.KEYS.ANALYZER_NODE_PACKAGE_ENABLED, nodeAnalyzerEnabled); settings.setBooleanIfNotNull(Settings.KEYS.ANALYZER_NODE_AUDIT_ENABLED, nodeAuditAnalyzerEnabled); - settings.setStringIfNotNull(Settings.KEYS.ANALYZER_NODE_AUDIT_URL, nodeAuditAnalyzerUrl); settings.setBooleanIfNotNull(Settings.KEYS.ANALYZER_NODE_AUDIT_USE_CACHE, nodeAuditAnalyzerUseCache); settings.setBooleanIfNotNull(Settings.KEYS.ANALYZER_NODE_PACKAGE_SKIPDEV, nodePackageSkipDevDependencies); settings.setBooleanIfNotNull(Settings.KEYS.ANALYZER_NODE_AUDIT_SKIPDEV, nodeAuditSkipDevDependencies); diff --git a/maven/src/site/markdown/configuration.md b/maven/src/site/markdown/configuration.md index ba04d4f9696..f496305abf1 100644 --- a/maven/src/site/markdown/configuration.md +++ b/maven/src/site/markdown/configuration.md @@ -96,12 +96,12 @@ be needed. | pnpmAuditAnalyzerEnabled | Sets whether the Pnpm Audit Analyzer should be used. This analyzer requires pnpm and an internet connection. Use `nodeAuditSkipDevDependencies` to skip dev dependencies. | true | | pathToYarn | The path to `yarn`. |   | | pathToPnpm | The path to `pnpm`. |   | +| pathToNpm | The path to `npm`. |   | | nodeAnalyzerEnabled | Sets whether the Node Package Analyzer should be used. | true | | nodePackageSkipDevDependencies | Sets whether the Node Package Analyzer will skip devDependencies. | false | -| nodeAuditAnalyzerEnabled | Sets whether the Node Audit Analyzer should be used. This analyzer requires an internet connection. | true | +| nodeAuditAnalyzerEnabled | Sets whether the Node Audit Analyzer should be used. This analyzer requires npm and an internet connection. | true | | nodeAuditAnalyzerUseCache | Sets whether the Node Audit Analyzer will cache results. Cached results expire after 24 hours. | true | | nodeAuditSkipDevDependencies | Sets whether the Node Audit Analyzer will skip devDependencies. | false | -| nodeAuditAnalyzerUrl | The Node Audit API URL for the Node Audit Analyzer. | https://registry.npmjs.org/-/npm/v1/security/audits | | retireJsAnalyzerEnabled | Sets whether the RetireJS Analyzer should be used. | true | | retireJsForceUpdate | Sets whether the RetireJS Analyzer should update regardless of the `autoupdate` setting. | false | | retireJsUrl | The URL to the Retire JS repository. **Note** the file name must be `jsrepository.json`. | https://raw.githubusercontent.com/Retirejs/retire.js/master/repository/jsrepository.json | diff --git a/src/site/markdown/analyzers/index.md b/src/site/markdown/analyzers/index.md index f4b6f004c68..ba4b88e110b 100644 --- a/src/site/markdown/analyzers/index.md +++ b/src/site/markdown/analyzers/index.md @@ -3,21 +3,21 @@ File Type Analyzers OWASP dependency-check contains several analyzers that are used to extract identification information from the files analyzed. -| Analyzer | File Types Scanned | Analysis Method | -|-------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------| -| [Archive](./archive-analyzer.html) | Zip archive format (\*.zip, \*.ear, \*.war, \*.jar, \*.sar, \*.apk, \*.nupkg); Tape Archive Format (\*.tar); Gzip format (\*.gz, \*.tgz); Bzip2 format (\*.bz2, \*.tbz2); RPM format (\*.rpm) | Extracts archive contents, then scans contents with all available analyzers. | -| [Assembly](./assembly-analyzer.html) | .NET Assemblies (\*.exe, \*.dll) | Uses [GrokAssembly.exe](https://github.com/colezlaw/GrokAssembly); requires the dotnet core 8.0 runtime to be installed. | -| [Jar](./jar-analyzer.html) | Java archive files (\*.jar); Web application archive (\*.war) | Examines archive manifest metadata, and Maven Project Object Model files (pom.xml). | -| [MS Build](./msbuild.html) | MS Build files (\*.csproj, \*.vbproj) | Parses the project files, including related directory build or package properties, to gather dependency information. | -| [Node Package](./nodejs.html) | NPM package specification files (package.json) | Parses the package.json to gather dependency information for a Node JS project. | -| [Node Audit](./node-audit-analyzer.html) | NPM package lock files (package-lock.json, npm-shrinkwrap.json) | Uses the `npm audit` APIs to report on known vulnerable node.js libraries. This analyzer requires an Internet connection. | -| [Nugetconf](./nugetconf-analyzer.html) | Nuget packages.config file | Uses XPath to parse specification XML. | -| [Nuspec](./nuspec-analyzer.html) | Nuget package specification file (\*.nuspec) | Uses XPath to parse specification XML. | -| [OpenSSL](./openssl.html) | OpenSSL Version Source Header File (opensslv.h) | Regex parse of the OPENSSL_VERSION_NUMBER macro definition. | -| [PNPM Audit](./pnpm-audit.html) | PNPM lock files (`pnpm-lock.yaml`) | Uses the PNPM CLI `audit` command to analyze lock files and retrieve vulnerabilities from the NPM Audit APIs. | -| [RetireJS](./retirejs-analyzer.html) | JavaScript files | Analyzes JavaScript files using the [RetireJS](https://github.com/RetireJS/retire.js) database. | -| [Ruby bundler‑audit](./bundle-audit.html) | Ruby `Gemfile.lock` files | Executes bundle-audit and incorporates the results into the dependency-check report. | -| [Yarn Audit](./yarn-audit.html) | Yarn lock files (`yarn.lock`) | Uses the Yarn CLI `audit` command to analyze lock files and retrieve vulnerabilities from the NPM Audit APIs. | +| Analyzer | File Types Scanned | Analysis Method | +|-------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------| +| [Archive](./archive-analyzer.html) | Zip archive format (\*.zip, \*.ear, \*.war, \*.jar, \*.sar, \*.apk, \*.nupkg); Tape Archive Format (\*.tar); Gzip format (\*.gz, \*.tgz); Bzip2 format (\*.bz2, \*.tbz2); RPM format (\*.rpm) | Extracts archive contents, then scans contents with all available analyzers. | +| [Assembly](./assembly-analyzer.html) | .NET Assemblies (\*.exe, \*.dll) | Uses [GrokAssembly.exe](https://github.com/colezlaw/GrokAssembly); requires the dotnet core 8.0 runtime to be installed. | +| [Jar](./jar-analyzer.html) | Java archive files (\*.jar); Web application archive (\*.war) | Examines archive manifest metadata, and Maven Project Object Model files (pom.xml). | +| [MS Build](./msbuild.html) | MS Build files (\*.csproj, \*.vbproj) | Parses the project files, including related directory build or package properties, to gather dependency information. | +| [Node Package](./nodejs.html) | NPM package specification files (package.json) | Parses the package.json to gather dependency information for a Node JS project. | +| [Node Audit](./node-audit-analyzer.html) | NPM package lock files (package-lock.json, npm-shrinkwrap.json) | Uses the NPM CLI `audit` command to analyze lock files and retrieve vulnerabilities from the NPM Audit APIs. | +| [Nugetconf](./nugetconf-analyzer.html) | Nuget packages.config file | Uses XPath to parse specification XML. | +| [Nuspec](./nuspec-analyzer.html) | Nuget package specification file (\*.nuspec) | Uses XPath to parse specification XML. | +| [OpenSSL](./openssl.html) | OpenSSL Version Source Header File (opensslv.h) | Regex parse of the OPENSSL_VERSION_NUMBER macro definition. | +| [PNPM Audit](./pnpm-audit.html) | PNPM lock files (`pnpm-lock.yaml`) | Uses the PNPM CLI `audit` command to analyze lock files and retrieve vulnerabilities from the NPM Audit APIs. | +| [RetireJS](./retirejs-analyzer.html) | JavaScript files | Analyzes JavaScript files using the [RetireJS](https://github.com/RetireJS/retire.js) database. | +| [Ruby bundler‑audit](./bundle-audit.html) | Ruby `Gemfile.lock` files | Executes bundle-audit and incorporates the results into the dependency-check report. | +| [Yarn Audit](./yarn-audit.html) | Yarn lock files (`yarn.lock`) | Uses the Yarn CLI `audit` command to analyze lock files and retrieve vulnerabilities from the NPM Audit APIs. | Augmenting Analyzers ---------------------- diff --git a/src/site/markdown/analyzers/node-audit-analyzer.md b/src/site/markdown/analyzers/node-audit-analyzer.md index 8a477dda93c..2f6ed2de632 100644 --- a/src/site/markdown/analyzers/node-audit-analyzer.md +++ b/src/site/markdown/analyzers/node-audit-analyzer.md @@ -2,11 +2,20 @@ Node Audit Analyzer ================ OWASP dependency-check includes a Node Audit Analyzer that scans `package-lock.json` -files. The analyzer submits the lock files to the [NPM Audit](https://www.npmjs.com/) -API for analysis, returning a list of advisories which get incorporated into the +and `npm-shrinkwrap.json` files. The analyzer runs [`npm audit`](https://docs.npmjs.com/cli/commands/npm-audit) +against the lock file, returning a list of advisories which get incorporated into the dependency check reports. -This analyzer is enabled by default and requires that the machine performing -the analysis can reach out to the Internet. +This analyzer is enabled by default and requires: -Files Types Scanned: [package-lock.json](https://docs.npmjs.com/files/package-lock.json) +- The `npm` command must be available - whether from an npm install, a + [corepack](https://github.com/nodejs/corepack) shim, or the configured + path to npm setting. +- The machine performing the analysis must be able to reach the npm registry + (or the registry configured via `.npmrc`). + +The analysis is performed against the lock file alone (`npm audit --package-lock-only`); +the project's `node_modules` directory does not need to be installed. + +Files Types Scanned: [package-lock.json](https://docs.npmjs.com/files/package-lock.json), +[npm-shrinkwrap.json](https://docs.npmjs.com/cli/configuring-npm/npm-shrinkwrap-json) diff --git a/src/site/markdown/dependency-check-gradle/configuration-aggregate.md b/src/site/markdown/dependency-check-gradle/configuration-aggregate.md index 72a5d6f4f88..d290c249bf6 100644 --- a/src/site/markdown/dependency-check-gradle/configuration-aggregate.md +++ b/src/site/markdown/dependency-check-gradle/configuration-aggregate.md @@ -185,7 +185,7 @@ Within the `analyzers` group, the following sub-groups are configurable. | nodeAudit | yarnPath | Sets the path to the `yarn` executable. |   | | nodeAudit | pnpmEnabled | Sets whether the Pnpm Audit Analyzer should be used. This analyzer requires pnpm and an internet connection. | true | | nodeAudit | pnpmPath | The path to `pnpm`. |   | -| nodeAudit | url | The node audit API url to use. |   | +| nodeAudit | url | **Deprecated** - ignored; the Node Audit Analyzer now uses the local `npm audit` command rather than the retired NPM Audit API. |   | | retirejs | enabled | Sets whether the RetireJS Analyzer should be used. | true | | retirejs | forceupdate | Sets whether the RetireJS Analyzer should update regardless of the `autoupdate` setting. | false | | retirejs | retireJsUrl | The URL to the Retire JS repository. | https://raw.githubusercontent.com/Retirejs/retire.js/master/repository/jsrepository.json | diff --git a/src/site/markdown/dependency-check-gradle/configuration.md b/src/site/markdown/dependency-check-gradle/configuration.md index 5106e07ac88..bd286bc2393 100644 --- a/src/site/markdown/dependency-check-gradle/configuration.md +++ b/src/site/markdown/dependency-check-gradle/configuration.md @@ -185,7 +185,7 @@ Within the `analyzers` group, the following sub-groups are configurable. | nodeAudit | yarnPath | Sets the path to the `yarn` executable. |   | | nodeAudit | pnpmEnabled | Sets whether the Pnpm Audit Analyzer should be used. This analyzer requires pnpm and an internet connection. | true | | nodeAudit | pnpmPath | The path to `pnpm`. |   | -| nodeAudit | url | The node audit API url to use. |   | +| nodeAudit | url | **Deprecated** - ignored; the Node Audit Analyzer now uses the local `npm audit` command rather than the retired NPM Audit API. |   | | retirejs | enabled | Sets whether the RetireJS Analyzer should be used. | true | | retirejs | forceupdate | Sets whether the RetireJS Analyzer should update regardless of the `autoupdate` setting. | false | | retirejs | retireJsUrl | The URL to the Retire JS repository. | https://raw.githubusercontent.com/Retirejs/retire.js/master/repository/jsrepository.json | diff --git a/utils/src/main/java/org/owasp/dependencycheck/utils/Settings.java b/utils/src/main/java/org/owasp/dependencycheck/utils/Settings.java index 29e85101cfa..a97a9ab2a3f 100644 --- a/utils/src/main/java/org/owasp/dependencycheck/utils/Settings.java +++ b/utils/src/main/java/org/owasp/dependencycheck/utils/Settings.java @@ -418,7 +418,12 @@ public static final class KEYS { public static final String ANALYZER_PNPM_AUDIT_REGISTRY = "analyzer.pnpm.audit.registry"; /** * The properties key for supplying the URL to the Node Audit API. + * + * @deprecated the Node Audit analyzer now uses the local `npm audit` + * command instead of directly submitting payloads to the retired NPM + * Audit API; this setting is no longer used. */ + @Deprecated(since = "13.1.0", forRemoval = true) public static final String ANALYZER_NODE_AUDIT_URL = "analyzer.node.audit.url"; /** * The properties key for configure whether the Node Audit analyzer @@ -523,6 +528,10 @@ public static final class KEYS { * The path to pnpm, if available. */ public static final String ANALYZER_PNPM_PATH = "analyzer.pnpm.path"; + /** + * The path to npm, if available. + */ + public static final String ANALYZER_NPM_PATH = "analyzer.npm.path"; /** * The properties key for whether the Golang Dep analyzer is enabled. */ diff --git a/utils/src/test/resources/dependencycheck.properties b/utils/src/test/resources/dependencycheck.properties index 366db0979cb..e8917040dae 100644 --- a/utils/src/test/resources/dependencycheck.properties +++ b/utils/src/test/resources/dependencycheck.properties @@ -74,8 +74,6 @@ analyzer.central.query=%s?q=1:%s&wt=xml analyzer.central.retry.count=3 analyzer.central.parallel.analysis=false -# the URL for searching NPM Audit API -analyzer.node.audit.url=https://registry.npmjs.org/-/npm/v1/security/audits analyzer.retirejs.enabled=true analyzer.retirejs.repo.validforhours=24 analyzer.retirejs.repo.js.url=https://raw.githubusercontent.com/Retirejs/retire.js/master/repository/jsrepository.json