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..efb3716f1f6 100644 --- a/core/src/main/java/org/owasp/dependencycheck/analyzer/NodeAuditAnalyzer.java +++ b/core/src/main/java/org/owasp/dependencycheck/analyzer/NodeAuditAnalyzer.java @@ -188,7 +188,7 @@ private List analyzePackage(final File lockFile, final File packageFil getSettings().getBoolean(Settings.KEYS.ANALYZER_NODE_AUDIT_SKIPDEV, false)); // Submits the package payload to the nsp check service - return getSearcher().submitPackage(payload); + return getSearcher().submitPackageWithBulkFallback(payload, dependencyMap); } catch (URLConnectionFailureException e) { this.setEnabled(false); @@ -252,7 +252,7 @@ private List legacyAnalysis(final File file, Dependency dependency, Mu getSettings().getBoolean(Settings.KEYS.ANALYZER_NODE_AUDIT_SKIPDEV, false)); // Submits the package payload to the nsp check service - return getSearcher().submitPackage(payload); + return getSearcher().submitPackageWithBulkFallback(payload, dependencyMap); } catch (URLConnectionFailureException e) { this.setEnabled(false); 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 index 01ff8d97962..d60f7188f07 100644 --- a/core/src/main/java/org/owasp/dependencycheck/data/nodeaudit/NodeAuditSearch.java +++ b/core/src/main/java/org/owasp/dependencycheck/data/nodeaudit/NodeAuditSearch.java @@ -26,6 +26,7 @@ import java.util.List; import javax.annotation.concurrent.ThreadSafe; +import org.apache.commons.collections4.MultiValuedMap; import org.apache.hc.client5.http.HttpResponseException; import org.apache.hc.core5.http.ContentType; import org.apache.hc.core5.http.Header; @@ -63,6 +64,10 @@ public class NodeAuditSearch { * The URL for the public Node Audit API. */ private final URL nodeAuditUrl; + /** + * The URL for the npm bulk advisory API. + */ + private final URL nodeAuditBulkUrl; /** * Whether to use the Proxy when making requests. @@ -92,6 +97,7 @@ 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.nodeAuditBulkUrl = deriveBulkUrl(searchUrl); this.settings = settings; if (null != settings.getString(Settings.KEYS.PROXY_SERVER)) { useProxy = true; @@ -111,6 +117,60 @@ public NodeAuditSearch(Settings settings) throws MalformedURLException { } } + /** + * Derives the npm bulk advisory API URL from the configured legacy audit URL. + * + * @param searchUrl the configured legacy audit API URL + * @return the derived bulk advisory URL, or {@code null} + * @throws MalformedURLException thrown if the derived URL is malformed + */ + private URL deriveBulkUrl(String searchUrl) throws MalformedURLException { + final String legacyPath = "/-/npm/v1/security/audits"; + final String bulkPath = "/-/npm/v1/security/advisories/bulk"; + if (searchUrl.endsWith(legacyPath)) { + final String bulkUrl = searchUrl.substring(0, searchUrl.length() - legacyPath.length()) + bulkPath; + LOGGER.debug("Node Audit bulk advisory URL: {}", bulkUrl); + return new URL(bulkUrl); + } + LOGGER.debug("Unable to derive npm bulk advisory URL from configured Node Audit URL: {}", searchUrl); + return null; + } + + /** + * Submits the package to the npm bulk advisory API first, falling back to + * the legacy audit API if the bulk endpoint is unavailable. + * + * @param packageJson the legacy npm audit payload + * @param dependencyMap the dependencies and installed versions collected + * while building the legacy payload + * @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 submitPackageWithBulkFallback(JsonObject packageJson, + MultiValuedMap dependencyMap) 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; + } + } + if (nodeAuditBulkUrl != null) { + final JsonObject bulkPayload = NpmPayloadBuilder.buildBulk(dependencyMap); + if (!bulkPayload.isEmpty()) { + try { + return submitBulkPackage(bulkPayload, key, 0); + } catch (SearchException | IOException ex) { + LOGGER.debug("npm bulk advisory API failed; falling back to legacy Node Audit API", ex); + } + } + } + return submitPackage(packageJson, key, 0); + } + /** * Submits the package.json file to the Node Audit API and returns a list of * zero or more Advisories. @@ -154,11 +214,7 @@ private List submitPackage(JsonObject packageJson, String key, int cou 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())); + final List
additionalHeaders = buildAdditionalHeaders(); try { final String response = Downloader.getInstance().postBasedFetchContent(nodeAuditUrl.toURI(), @@ -208,6 +264,89 @@ private List submitPackage(JsonObject packageJson, String key, int cou } } + /** + * Submits the package data to the npm bulk advisory API. + * + * @param packageJson the bulk advisory payload + * @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 the bulk advisory API is unable to analyze the package + * @throws IOException if it's unable to connect to the bulk advisory API + */ + private List submitBulkPackage(JsonObject packageJson, String key, int count) throws SearchException, IOException { + if (LOGGER.isTraceEnabled()) { + LOGGER.trace("----------------------------------------"); + LOGGER.trace("Node Audit Bulk Payload:"); + LOGGER.trace(packageJson.toString()); + LOGGER.trace("----------------------------------------"); + LOGGER.trace("----------------------------------------"); + } + final List
additionalHeaders = buildAdditionalHeaders(); + + try { + final String response = Downloader.getInstance().postBasedFetchContent(nodeAuditBulkUrl.toURI(), + packageJson.toString(), ContentType.APPLICATION_JSON, additionalHeaders); + final JSONObject jsonResponse = new JSONObject(response); + final NpmAuditParser parser = new NpmAuditParser(); + final List advisories = parser.parseBulk(jsonResponse); + if (cache != null) { + cache.put(key, advisories); + } + return advisories; + } catch (RuntimeException | URISyntaxException | TooManyRequestsException | ResourceNotFoundException ex) { + LOGGER.debug("Error connecting to npm bulk advisory API. Error: {}", + ex.getMessage()); + throw new SearchException("Could not connect to npm bulk advisory 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("npm bulk advisory 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 submitBulkPackage(packageJson, key, next); + } + throw new SearchException("Could not perform npm bulk advisory analysis - service returned a 503.", e); + case 400: + LOGGER.debug("Invalid payload submitted to npm bulk advisory API. Received response code: {} {}", + hre.getStatusCode(), hre.getReasonPhrase()); + throw new SearchException("Could not perform npm bulk advisory analysis. Invalid payload submitted " + + "to npm bulk advisory API.", e); + default: + LOGGER.debug("Could not connect to npm bulk advisory API. Received response code: {} {}", + hre.getStatusCode(), hre.getReasonPhrase()); + throw new IOException("Could not connect to npm bulk advisory API", e); + } + } else { + LOGGER.debug("Could not connect to npm bulk advisory API. Received generic DownloadException", e); + throw new IOException("Could not connect to npm bulk advisory API", e); + } + } + } + + /** + * Builds common npm audit request headers. + * + * @return request headers + */ + private List
buildAdditionalHeaders() { + 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())); + return additionalHeaders; + } + /** * Generates a random 16 character lower-case hex string. * diff --git a/core/src/main/java/org/owasp/dependencycheck/data/nodeaudit/NpmAuditParser.java b/core/src/main/java/org/owasp/dependencycheck/data/nodeaudit/NpmAuditParser.java index 5c5b01fc416..18d6ea72f61 100644 --- a/core/src/main/java/org/owasp/dependencycheck/data/nodeaudit/NpmAuditParser.java +++ b/core/src/main/java/org/owasp/dependencycheck/data/nodeaudit/NpmAuditParser.java @@ -63,6 +63,30 @@ public List parse(JSONObject jsonResponse) throws JSONException { return advisories; } + /** + * Parses the JSON response from the NPM bulk advisory API. + * + * @param jsonResponse the JSON node to parse + * @return a list of advisories + * @throws JSONException thrown if the JSON is not of the expected schema + */ + public List parseBulk(JSONObject jsonResponse) throws JSONException { + LOGGER.debug("Parsing bulk advisory JSON node"); + final List advisories = new ArrayList<>(); + final Iterator keys = jsonResponse.keys(); + while (keys.hasNext()) { + final String moduleName = (String) keys.next(); + final JSONArray moduleAdvisories = jsonResponse.optJSONArray(moduleName); + if (moduleAdvisories == null) { + continue; + } + for (int i = 0; i < moduleAdvisories.length(); i++) { + advisories.add(parseBulkAdvisory(moduleName, moduleAdvisories.getJSONObject(i))); + } + } + return advisories; + } + /** * Parses the advisory from Node Audit. * @@ -150,4 +174,117 @@ private Advisory parseAdvisory(JSONObject object) throws JSONException { } return advisory; } + + /** + * Parses an advisory from the NPM bulk advisory API. + * + * @param moduleName the name of the vulnerable module + * @param object the JSON object containing the advisory + * @return the Advisory object + */ + private Advisory parseBulkAdvisory(String moduleName, JSONObject object) { + final Advisory advisory = new Advisory(); + final String url = object.optString("url", null); + String id = object.optString("github_advisory_id", null); + if (id == null || id.isBlank()) { + id = extractGhsaId(url); + } + if (id == null || id.isBlank()) { + id = object.optString("id", null); + } + advisory.setGhsaId(id); + advisory.setOverview(object.optString("overview", object.optString("title", null))); + advisory.setReferences(url == null || url.isBlank() ? null : "- " + url); + advisory.setCreated(object.optString("created", null)); + advisory.setUpdated(object.optString("updated", null)); + advisory.setRecommendation(object.optString("recommendation", null)); + advisory.setTitle(object.optString("title", null)); + advisory.setModuleName(moduleName); + advisory.setVulnerableVersions(object.optString("vulnerable_versions", object.optString("range", "*"))); + if (advisory.getVulnerableVersions() == null || advisory.getVulnerableVersions().isBlank()) { + advisory.setVulnerableVersions("*"); + } + advisory.setPatchedVersions(object.optString("patched_versions", null)); + advisory.setAccess(object.optString("access", null)); + advisory.setSeverity(object.optString("severity", null)); + advisory.setCwes(parseStringArray(object.opt("cwe"))); + advisory.setCves(parseStringArray(object.opt("cves"))); + parseCvss(object.optJSONObject("cvss"), advisory); + return advisory; + } + + /** + * Parses a JSON string or array of strings. + * + * @param value the value to parse + * @return a list of strings + */ + private List parseStringArray(Object value) { + final List values = new ArrayList<>(); + if (value instanceof JSONArray) { + final JSONArray array = (JSONArray) value; + for (int i = 0; i < array.length(); i++) { + values.add(array.optString(i)); + } + } else if (value instanceof String) { + values.add((String) value); + } + return values; + } + + /** + * Parses CVSSv3 details onto an advisory. + * + * @param jsonCvss the CVSS JSON object + * @param advisory the advisory to update + */ + private void parseCvss(JSONObject jsonCvss, Advisory advisory) { + if (jsonCvss == null) { + return; + } + double baseScore = -1.0; + final String score = jsonCvss.optString("score"); + if (score != null) { + try { + baseScore = Float.parseFloat(score); + } catch (NumberFormatException ignored) { + LOGGER.trace("Swallowed NumberFormatException", ignored); + baseScore = -1.0f; + } + } + if (baseScore >= 0.0) { + final String vector = jsonCvss.optString("vectorString"); + if (vector != null && !"null".equals(vector)) { + if (vector.startsWith("CVSS:3") && baseScore >= 0.0) { + 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); + } + } + } + } + + /** + * Extracts a GHSA identifier from an advisory URL. + * + * @param url the advisory URL + * @return the GHSA identifier, or null + */ + private String extractGhsaId(String url) { + if (url == null || url.isBlank()) { + 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/NpmPayloadBuilder.java b/core/src/main/java/org/owasp/dependencycheck/data/nodeaudit/NpmPayloadBuilder.java index 478c1d5f2bc..62d0ba9b11b 100644 --- a/core/src/main/java/org/owasp/dependencycheck/data/nodeaudit/NpmPayloadBuilder.java +++ b/core/src/main/java/org/owasp/dependencycheck/data/nodeaudit/NpmPayloadBuilder.java @@ -204,6 +204,66 @@ public static JsonObject build(JsonObject packageJson, MultiValuedMap dependencyMap) { + final JsonObjectBuilder payloadBuilder = Json.createObjectBuilder(); + dependencyMap.asMap().entrySet().stream() + .collect(Collectors.toMap( + Map.Entry::getKey, + Map.Entry::getValue, + (oldValue, newValue) -> newValue, + TreeMap::new)) + .forEach((key, versions) -> { + if (key == null || key.isBlank()) { + return; + } + final var versionsBuilder = Json.createArrayBuilder(); + versions.stream() + .map(NpmPayloadBuilder::normalizeVersion) + .filter(Objects::nonNull) + .distinct() + .sorted() + .forEach(versionsBuilder::add); + final var versionArray = versionsBuilder.build(); + if (!versionArray.isEmpty()) { + payloadBuilder.add(key, versionArray); + } + }); + return payloadBuilder.build(); + } + + /** + * Normalizes dependency versions for npm's bulk advisory API, which expects + * exact installed versions rather than ranges, aliases, or local references. + * + * @param version the candidate version + * @return the normalized exact version or {@code null} + */ + private static String normalizeVersion(String version) { + if (version == null) { + return null; + } + String normalized = version.trim(); + if (normalized.length() > 1 && normalized.startsWith("\"") && normalized.endsWith("\"")) { + normalized = normalized.substring(1, normalized.length() - 1); + } + if (normalized.isEmpty() || "*".equals(normalized) + || normalized.startsWith("^") || normalized.startsWith("~") + || normalized.startsWith(">") || normalized.startsWith("<") + || normalized.startsWith("=") || normalized.startsWith("file:") + || normalized.startsWith("link:") || normalized.startsWith("npm:") + || normalized.startsWith("workspace:")) { + return null; + } + return normalized.matches("[0-9]+\\.[0-9]+\\.[0-9]+.*") ? normalized : null; + } + /** * Adds the project name and version to the npm audit API payload. * diff --git a/core/src/main/resources/dependencycheck.properties b/core/src/main/resources/dependencycheck.properties index b0d6516ee61..9717dd2cc58 100644 --- a/core/src/main/resources/dependencycheck.properties +++ b/core/src/main/resources/dependencycheck.properties @@ -86,7 +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 +# the legacy URL for searching NPM Audit API; npm bulk advisory URL is derived from this URL when possible analyzer.node.audit.url=https://registry.npmjs.org/-/npm/v1/security/audits analyzer.node.audit.use.cache=true @@ -172,4 +172,4 @@ hosted.suppressions.validforhours=2 ## The following controls the max query limit used in the CPE searches for each ecosystem odc.ecosystem.maxquerylimit.native=1000 -odc.ecosystem.maxquerylimit.default=100 \ No newline at end of file +odc.ecosystem.maxquerylimit.default=100 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 index 18a4ee726c4..b568dfced1a 100644 --- a/core/src/test/java/org/owasp/dependencycheck/data/nodeaudit/NodeAuditSearchTest.java +++ b/core/src/test/java/org/owasp/dependencycheck/data/nodeaudit/NodeAuditSearchTest.java @@ -17,10 +17,131 @@ */ package org.owasp.dependencycheck.data.nodeaudit; +import com.sun.net.httpserver.HttpServer; +import jakarta.json.Json; +import jakarta.json.JsonObject; +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 org.owasp.dependencycheck.utils.Settings; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.jupiter.api.Assertions.assertEquals; class NodeAuditSearchTest extends BaseTest { + @Test + void testSubmitPackageWithBulkFallbackUsesBulkEndpoint() throws Exception { + final AtomicInteger bulkRequests = new AtomicInteger(); + final AtomicInteger quickRequests = new AtomicInteger(); + final HttpServer server = createServer( + 200, + "{" + + "\"minimist\":[{" + + "\"id\":1097677," + + "\"url\":\"https://github.com/advisories/GHSA-xvch-5gv4-984h\"," + + "\"title\":\"Prototype Pollution in minimist\"," + + "\"severity\":\"critical\"," + + "\"vulnerable_versions\":\"<0.2.4\"" + + "}]" + + "}", + 200, + "{\"advisories\":{}}", + bulkRequests, + quickRequests); + try { + final NodeAuditSearch search = createSearch(server); + + final List advisories = search.submitPackageWithBulkFallback(createLegacyPayload(), createDependencyMap()); + + assertEquals(1, advisories.size()); + assertEquals("minimist", advisories.get(0).getModuleName()); + assertEquals(1, bulkRequests.get()); + assertEquals(0, quickRequests.get()); + } finally { + server.stop(0); + } + } + + @Test + void testSubmitPackageWithBulkFallbackUsesQuickEndpointWhenBulkFails() throws Exception { + final AtomicInteger bulkRequests = new AtomicInteger(); + final AtomicInteger quickRequests = new AtomicInteger(); + final HttpServer server = createServer( + 410, + "{}", + 200, + "{\"advisories\":{}}", + bulkRequests, + quickRequests); + try { + final NodeAuditSearch search = createSearch(server); + + final List advisories = search.submitPackageWithBulkFallback(createLegacyPayload(), createDependencyMap()); + + assertEquals(0, advisories.size()); + assertEquals(1, bulkRequests.get()); + assertEquals(1, quickRequests.get()); + } finally { + server.stop(0); + } + } + + private NodeAuditSearch createSearch(HttpServer server) throws Exception { + final Settings settings = getSettings(); + settings.setString(Settings.KEYS.ANALYZER_NODE_AUDIT_URL, + "http://localhost:" + server.getAddress().getPort() + "/-/npm/v1/security/audits"); + settings.setBoolean(Settings.KEYS.ANALYZER_NODE_AUDIT_USE_CACHE, false); + return new NodeAuditSearch(settings); + } + + private JsonObject createLegacyPayload() { + return Json.createObjectBuilder() + .add("name", "test") + .add("version", "1.0.0") + .add("requires", Json.createObjectBuilder() + .add("minimist", "0.0.8")) + .add("dependencies", Json.createObjectBuilder() + .add("minimist", Json.createObjectBuilder() + .add("version", "0.0.8"))) + .build(); + } + + private MultiValuedMap createDependencyMap() { + final MultiValuedMap dependencyMap = new HashSetValuedHashMap<>(); + dependencyMap.put("minimist", "0.0.8"); + return dependencyMap; + } + + private HttpServer createServer(int bulkStatus, String bulkResponse, int quickStatus, String quickResponse, + AtomicInteger bulkRequests, AtomicInteger quickRequests) throws IOException { + final HttpServer server = HttpServer.create(new InetSocketAddress(0), 0); + server.createContext("/-/npm/v1/security/advisories/bulk", exchange -> { + bulkRequests.incrementAndGet(); + respond(exchange, bulkStatus, bulkResponse); + }); + server.createContext("/-/npm/v1/security/audits", exchange -> { + quickRequests.incrementAndGet(); + respond(exchange, quickStatus, quickResponse); + }); + server.start(); + return server; + } + + private void respond(com.sun.net.httpserver.HttpExchange exchange, int status, String response) throws IOException { + final byte[] bytes = response.getBytes(StandardCharsets.UTF_8); + exchange.getResponseHeaders().add("Content-Type", "application/json"); + exchange.sendResponseHeaders(status, bytes.length); + exchange.getResponseBody().write(bytes); + exchange.close(); + } + // 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; 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 index 59c612e6f2e..9a6139a83e7 100644 --- a/core/src/test/java/org/owasp/dependencycheck/data/nodeaudit/NpmPayloadBuilderTest.java +++ b/core/src/test/java/org/owasp/dependencycheck/data/nodeaudit/NpmPayloadBuilderTest.java @@ -182,4 +182,27 @@ void testPayloadWithLockAndPackage() { assertFalse(requires.containsKey("fake_submodule")); } } + + @Test + void testBulkPayload() { + final MultiValuedMap dependencyMap = new HashSetValuedHashMap<>(); + dependencyMap.put("abbrev", "1.1.1"); + dependencyMap.put("abbrev", "\"1.1.2\""); + dependencyMap.put("abbrev", "^1.1.0"); + dependencyMap.put("react-dom", "npm:@hot-loader/react-dom"); + dependencyMap.put("fake_submodule", "file:fake_submodule"); + dependencyMap.put("workspace-module", "workspace:*"); + dependencyMap.put("", "1.0.0"); + + final JsonObject bulkPayload = NpmPayloadBuilder.buildBulk(dependencyMap); + + assertTrue(bulkPayload.containsKey("abbrev")); + assertEquals(2, bulkPayload.getJsonArray("abbrev").size()); + assertEquals("1.1.1", bulkPayload.getJsonArray("abbrev").getString(0)); + assertEquals("1.1.2", bulkPayload.getJsonArray("abbrev").getString(1)); + assertFalse(bulkPayload.containsKey("react-dom")); + assertFalse(bulkPayload.containsKey("fake_submodule")); + assertFalse(bulkPayload.containsKey("workspace-module")); + assertFalse(bulkPayload.containsKey("")); + } } diff --git a/maven/src/site/markdown/configuration.md b/maven/src/site/markdown/configuration.md index 68a79397c11..3ca263ce1ee 100644 --- a/maven/src/site/markdown/configuration.md +++ b/maven/src/site/markdown/configuration.md @@ -102,7 +102,7 @@ be needed. | nodeAuditAnalyzerEnabled | Sets whether the Node Audit Analyzer should be used. This analyzer requires 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 | +| nodeAuditAnalyzerUrl | The legacy Node Audit API URL for the Node Audit Analyzer. For npm package-lock analysis, the bulk advisory URL is derived from this URL when possible and used before falling back to this legacy endpoint. | 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/dependency-check-gradle/configuration-aggregate.md b/src/site/markdown/dependency-check-gradle/configuration-aggregate.md index a93d5ec9592..034f2b822dc 100644 --- a/src/site/markdown/dependency-check-gradle/configuration-aggregate.md +++ b/src/site/markdown/dependency-check-gradle/configuration-aggregate.md @@ -183,7 +183,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 | The legacy node audit API url to use. For npm package-lock analysis, the bulk advisory url is derived from this url when possible and used before falling back to this legacy endpoint. |   | | 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 8e0d4bdd781..5df45cc0ef6 100644 --- a/src/site/markdown/dependency-check-gradle/configuration.md +++ b/src/site/markdown/dependency-check-gradle/configuration.md @@ -183,7 +183,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 | The legacy node audit API url to use. For npm package-lock analysis, the bulk advisory url is derived from this url when possible and used before falling back to this legacy endpoint. |   | | 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 |