diff --git a/Dockerfile b/Dockerfile index e29f77c7cf3..0af01a4354c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -50,7 +50,7 @@ USER ${UID} ### Cache pieces needed for the specific run user RUN bundle audit update && \ - corepack prepare pnpm@latest yarn@latest yarn@1 --activate && \ + corepack prepare pnpm@latest yarn@latest --activate && \ printf "enableTelemetry: false\nenableScripts: false\n" >> ${HOME}/.yarnrc.yml && \ rm -rf /tmp/* diff --git a/README.md b/README.md index fd7d7160d9f..44461acef28 100644 --- a/README.md +++ b/README.md @@ -191,7 +191,7 @@ Tests cases require: * dotnet core version 8.0 * Go: `go version` 1.12 and higher * Ruby [bundler-audit](https://github.com/rubysec/bundler-audit#install) -* [Yarn](https://classic.yarnpkg.com/en/docs/install/) +* [Yarn](https://yarnpkg.com/getting-started/install) * [pnpm](https://pnpm.io/installation) ## Development Usage 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 a8a9f8d94fe..20c95c8aafe 100644 --- a/core/src/main/java/org/owasp/dependencycheck/analyzer/YarnAuditAnalyzer.java +++ b/core/src/main/java/org/owasp/dependencycheck/analyzer/YarnAuditAnalyzer.java @@ -17,23 +17,18 @@ */ package org.owasp.dependencycheck.analyzer; -import org.apache.commons.collections4.MultiValuedMap; import org.apache.commons.collections4.multimap.HashSetValuedHashMap; -import org.apache.commons.io.IOUtils; 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.nodeaudit.Advisory; -import org.owasp.dependencycheck.data.nodeaudit.NpmPayloadBuilder; import org.owasp.dependencycheck.dependency.Dependency; import org.owasp.dependencycheck.exception.InitializationException; 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.semver4j.Semver; import org.semver4j.SemverException; @@ -42,17 +37,11 @@ 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.charset.StandardCharsets; import java.nio.file.Files; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; import java.util.stream.Stream; @@ -66,10 +55,7 @@ public class YarnAuditAnalyzer extends AbstractNpmAnalyzer { */ private static final Logger LOGGER = LoggerFactory.getLogger(YarnAuditAnalyzer.class); - /** - * The major version of the Yarn Classic CLI. - */ - private static final int YARN_CLASSIC_MAJOR_VERSION = 1; + private static final int YARN_BERRY_MAJOR_VERSION_MIN = 2; /** * The file name to scan. @@ -82,13 +68,6 @@ public class YarnAuditAnalyzer extends AbstractNpmAnalyzer { private static final FileFilter LOCK_FILE_FILTER = FileFilterBuilder.newInstance() .addFilenames(YARN_PACKAGE_LOCK).build(); - /** - * An expected error from `yarn audit --offline --verbose --json` that will - * be ignored. - */ - private static final String EXPECTED_ERROR = "{\"type\":\"error\",\"data\":\"Can't make a request in " - + "offline mode (\\\"https://registry.yarnpkg.com/-/npm/v1/security/audits\\\")\"}\n"; - /** * The path to the `yarn` executable. */ @@ -145,7 +124,6 @@ private Semver getYarnVersion(File dependencyDirectory) { } } - /** * Initializes the analyzer once before any analysis is performed. * @@ -206,7 +184,7 @@ private String startAndReadStdoutToString(ProcessBuilder builder) throws Analysi processReader.readAll(); final String errOutput = processReader.getError(); - if (!StringUtils.isBlank(errOutput) && !EXPECTED_ERROR.equals(errOutput)) { + if (!StringUtils.isBlank(errOutput)) { LOGGER.debug("Process Error Out: {}", errOutput); LOGGER.debug("Process Out: {}", processReader.getOutput()); } @@ -239,54 +217,24 @@ protected void analyzeDependency(Dependency dependency, Engine engine) throws An } File dependencyDirectory = getDependencyDirectory(packageLock); final var yarnVersion = getYarnVersion(dependencyDirectory); - final List advisories; - final MultiValuedMap dependencyMap = new HashSetValuedHashMap<>(); - if (YARN_CLASSIC_MAJOR_VERSION < yarnVersion.getMajor()) { - LOGGER.info("Analyzing using Yarn Berry ({}) audit for {}", yarnVersion, dependency.getActualFilePath()); - advisories = analyzePackageWithYarnBerry(dependency); - } else { - LOGGER.info("Analyzing using Yarn Classic ({}) audit for {}", yarnVersion, dependency.getActualFilePath()); - advisories = analyzePackageWithYarnClassic(packageLock, dependency, dependencyMap); + if (yarnVersion.getMajor() < YARN_BERRY_MAJOR_VERSION_MIN) { + LOGGER.warn("Yarn dependency skipped: {} - Yarn Classic (v{}) is not supported.", dependency.getActualFile(), yarnVersion); + return; } + + LOGGER.info("Analyzing using Yarn Berry ({}) audit for {}", yarnVersion, dependency.getActualFilePath()); try { - processResults(advisories, engine, dependency, dependencyMap); - } catch (CpeValidationException ex) { - throw new UnexpectedAnalysisException(ex); + final var skipDevDependencies = getSettings().getBoolean(Settings.KEYS.ANALYZER_NODE_AUDIT_SKIPDEV, false); + final var advisoryJsons = fetchYarnAdvisories(dependency, skipDevDependencies); + List advisories = parseAdvisoryJsons(advisoryJsons); + processResults(advisories, engine, dependency, new HashSetValuedHashMap<>()); + } catch (JSONException e) { + throw new AnalysisException("Failed to parse the response from NPM Audit API (YarnAuditAnalyzer).", e); + } catch (CpeValidationException e) { + throw new UnexpectedAnalysisException(e); } } - private JsonObject fetchYarnAuditJson(File dependencyDirectory, boolean skipDevDependencies) throws AnalysisException { - final List args = new ArrayList<>(); - args.add(yarnPath); - args.add("audit"); - //offline audit is not supported - but the audit request is generated in the verbose output - args.add("--offline"); - if (skipDevDependencies) { - args.add("--groups"); - args.add("dependencies"); - } - args.add("--json"); - args.add("--verbose"); - final ProcessBuilder builder = new ProcessBuilder(args); - builder.directory(dependencyDirectory); - LOGGER.debug("Launching: {}", args); - - final String verboseJson = startAndReadStdoutToString(builder); - final String auditRequestJson = Arrays.stream(verboseJson.split("\n")) - .filter(line -> line.contains("Audit Request")) - .findFirst() - .orElseThrow(() -> new AnalysisException("No results from Yarn Classic (offline step) - possibly trying to use classic analyzer on Yarn Berry lockfile")); - String auditRequest; - try (JsonReader reader = Json.createReader(IOUtils.toInputStream(auditRequestJson, StandardCharsets.UTF_8))) { - final JsonObject jsonObject = reader.readObject(); - auditRequest = jsonObject.getString("data"); - auditRequest = auditRequest.substring(15); - } - LOGGER.debug("Audit Request: {}", auditRequest); - - return Json.createReader(IOUtils.toInputStream(auditRequest, StandardCharsets.UTF_8)).readObject(); - } - private static File getDependencyDirectory(File lockFile) { final File folder = lockFile.getParentFile(); if (!folder.isDirectory()) { @@ -295,56 +243,6 @@ private static File getDependencyDirectory(File lockFile) { return folder; } - /** - * Analyzes the package and yarn lock files by extracting dependency - * information, creating a payload to submit to the npm audit API, - * submitting the payload, and returning the identified advisories. - * - * @param lockFile a reference to the package-lock.json - * @param dependency a reference to the dependency-object for the yarn.lock - * @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 - */ - private List analyzePackageWithYarnClassic(final File lockFile, Dependency dependency, - MultiValuedMap dependencyMap) - throws AnalysisException { - try { - final boolean skipDevDependencies = getSettings().getBoolean(Settings.KEYS.ANALYZER_NODE_AUDIT_SKIPDEV, false); - // Retrieves the contents of package-lock.json from the Dependency - final JsonObject lockJson = fetchYarnAuditJson(getDependencyDirectory(lockFile), skipDevDependencies); - // Retrieves the contents of package-lock.json from the Dependency - final JsonObject packageJson; - try (JsonReader packageReader = Json.createReader(Files.newInputStream(lockFile.getParentFile().toPath().resolve("package.json")))) { - packageJson = packageReader.readObject(); - } - // Modify the payload to meet the NPM Audit API requirements - final JsonObject payload = NpmPayloadBuilder.build(lockJson, packageJson, dependencyMap, skipDevDependencies); - - // 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 (YarnAuditAnalyzer); 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 (YarnAuditAnalyzer); " - + "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 " - + "(YarnAuditAnalyzer).", lockFile.getPath()), e); - } catch (SearchException ex) { - LOGGER.error("YarnAuditAnalyzer failed on {}", dependency.getActualFilePath()); - throw ex; - } - } - private List fetchYarnAdvisories(Dependency dependency, boolean skipDevDependencies) throws AnalysisException { final List args = new ArrayList<>(); @@ -376,28 +274,7 @@ private List fetchYarnAdvisories(Dependency dependency, boolean skip return advisories; } catch (JSONException e) { - throw new AnalysisException("Failed to parse the response from NPM Audit API " - + "(YarnBerryAuditAnalyzer).", e); - } - } - - /** - * Analyzes the package and yarn lock files by calling yarn npm audit and returning the identified advisories. - * - * @param dependency a reference to the dependency-object for the yarn.lock - * @return a list of advisories - */ - private List analyzePackageWithYarnBerry(Dependency dependency) throws AnalysisException { - try { - final var skipDevDependencies = getSettings().getBoolean(Settings.KEYS.ANALYZER_NODE_AUDIT_SKIPDEV, false); - final var advisoryJsons = fetchYarnAdvisories(dependency, skipDevDependencies); - return parseAdvisoryJsons(advisoryJsons); - } catch (JSONException e) { - throw new AnalysisException("Failed to parse the response from NPM Audit API " - + "(YarnBerryAuditAnalyzer).", e); - } catch (SearchException ex) { - LOGGER.error("YarnBerryAuditAnalyzer failed on {}", dependency.getActualFilePath()); - throw ex; + throw new AnalysisException("Failed to parse the response from NPM Audit API (YarnAuditAnalyzer).", e); } } diff --git a/core/src/test/java/org/owasp/dependencycheck/analyzer/YarnAuditAnalyzerIT.java b/core/src/test/java/org/owasp/dependencycheck/analyzer/YarnAuditAnalyzerIT.java index c3474e96086..6dc967e42b5 100644 --- a/core/src/test/java/org/owasp/dependencycheck/analyzer/YarnAuditAnalyzerIT.java +++ b/core/src/test/java/org/owasp/dependencycheck/analyzer/YarnAuditAnalyzerIT.java @@ -20,12 +20,10 @@ import org.jspecify.annotations.NonNull; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.owasp.dependencycheck.BaseTest; import org.owasp.dependencycheck.Engine; -import org.owasp.dependencycheck.analyzer.exception.AnalysisException; import org.owasp.dependencycheck.dependency.Dependency; import org.owasp.dependencycheck.dependency.EvidenceType; import org.owasp.dependencycheck.exception.InitializationException; @@ -60,13 +58,9 @@ void cleanup() { class Classic { @Test void testAnalyzePackageYarnClassic() throws Exception { - testAnalyzeForUglifyJs("yarn/yarn-classic-audit/yarn.lock"); - } - - @Test - void testAnalyzePackageYarnClassicOnYarnBerryLockfile() { - AnalysisException exception = assertThrows(AnalysisException.class, () -> testAnalyzeForUglifyJs("yarn/yarn-classic-audit-bad-berry-lockfile/yarn.lock")); - assertThat(exception.getMessage(), containsString("No results from Yarn Classic (offline step) - possibly trying to use classic analyzer on Yarn Berry lockfile")); + final Dependency toScan = new Dependency(BaseTest.getResourceAsFile(YarnAuditAnalyzerIT.this, "yarn/yarn-classic-audit/yarn.lock")); + analyzer.analyze(toScan, engine); + assertEquals(0, engine.getDependencies().length, "No dependencies should be identified"); } } diff --git a/core/src/test/resources/yarn/yarn-classic-audit-bad-berry-lockfile/package.json b/core/src/test/resources/yarn/yarn-classic-audit-bad-berry-lockfile/package.json deleted file mode 100644 index fb923d5d14d..00000000000 --- a/core/src/test/resources/yarn/yarn-classic-audit-bad-berry-lockfile/package.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "name": "owasp-nodejs-goat", - "private": true, - "version": "1.3.0", - "description": "A tool to learn OWASP Top 10 for node.js developers", - "main": "server.js", - "dependencies": { - "does-not-exist": "file:../does-not-exist" - }, - "scripts": { - "start": "node server.js", - "test": "node node_modules/grunt-cli/bin/grunt test", - "db:seed": "grunt db-reset", - "precommit": "grunt precommit" - }, - "repository": "https://github.com/OWASP/NodejsGoat", - "license": "Apache 2.0", - "packageManager": "yarn@1.22.22" -} diff --git a/core/src/test/resources/yarn/yarn-classic-audit-bad-berry-lockfile/yarn.lock b/core/src/test/resources/yarn/yarn-classic-audit-bad-berry-lockfile/yarn.lock deleted file mode 100644 index d924e840ce2..00000000000 --- a/core/src/test/resources/yarn/yarn-classic-audit-bad-berry-lockfile/yarn.lock +++ /dev/null @@ -1,21 +0,0 @@ -# This file is generated by running "yarn install" inside your project. -# Manual changes might be lost - proceed with caution! - -__metadata: - version: 8 - cacheKey: 10c0 - -"mime-db@npm:^1.52.0": - version: 1.54.0 - resolution: "mime-db@npm:1.54.0" - checksum: 10c0/8d907917bc2a90fa2df842cdf5dfeaf509adc15fe0531e07bb2f6ab15992416479015828d6a74200041c492e42cce3ebf78e5ce714388a0a538ea9c53eece284 - languageName: node - linkType: hard - -"owasp-nodejs-goat@workspace:.": - version: 0.0.0-use.local - resolution: "owasp-nodejs-goat@workspace:." - dependencies: - mime-db: "npm:^1.52.0" - languageName: unknown - linkType: soft diff --git a/src/site/markdown/analyzers/yarn-audit.md b/src/site/markdown/analyzers/yarn-audit.md index 289505f93a9..9179e3197ef 100644 --- a/src/site/markdown/analyzers/yarn-audit.md +++ b/src/site/markdown/analyzers/yarn-audit.md @@ -3,6 +3,6 @@ Yarn Audit Analyzer Uses the Yarn CLI `audit` command to analyze `yarn.lock` files and retrieve vulnerabilities from the [NPM Audit](https://www.npmjs.com/) APIs. -Supports Yarn v1 and Yarn v2+ (Berry) and is corepack-aware. +Supports Yarn v2+ (Berry) and is corepack-aware. Yarn v1 (Classic) is no longer supported due to its use of a removed NPM Audit API. Files Types Scanned: package.json, yarn.lock