Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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/*

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

Expand All @@ -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.
Expand All @@ -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.
*/
Expand Down Expand Up @@ -145,7 +124,6 @@ private Semver getYarnVersion(File dependencyDirectory) {
}
}


/**
* Initializes the analyzer once before any analysis is performed.
*
Expand Down Expand Up @@ -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());
}
Expand Down Expand Up @@ -239,54 +217,24 @@ protected void analyzeDependency(Dependency dependency, Engine engine) throws An
}
File dependencyDirectory = getDependencyDirectory(packageLock);
final var yarnVersion = getYarnVersion(dependencyDirectory);
final List<Advisory> advisories;
final MultiValuedMap<String, String> 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<Advisory> 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<String> 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()) {
Expand All @@ -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<Advisory> analyzePackageWithYarnClassic(final File lockFile, Dependency dependency,
MultiValuedMap<String, String> 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<JSONObject> fetchYarnAdvisories(Dependency dependency, boolean skipDevDependencies) throws AnalysisException {
final List<String> args = new ArrayList<>();

Expand Down Expand Up @@ -376,28 +274,7 @@ private List<JSONObject> 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<Advisory> 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);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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");
}
}

Expand Down

This file was deleted.

This file was deleted.

2 changes: 1 addition & 1 deletion src/site/markdown/analyzers/yarn-audit.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading