Skip to content

Commit

Permalink
feat: Implement Yarn Berry Analyser
Browse files Browse the repository at this point in the history
  • Loading branch information
segovia committed Jan 15, 2025
1 parent 0ec89c5 commit 7551e42
Show file tree
Hide file tree
Showing 10 changed files with 398 additions and 67 deletions.
14 changes: 14 additions & 0 deletions ant/src/main/java/org/owasp/dependencycheck/taskdefs/Check.java
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@ public class Check extends Update {
* Whether or not the Yarn Audit Analyzer is enabled.
*/
private Boolean yarnAuditAnalyzerEnabled;
/**
* Whether or not the Yarn Berry Audit Analyzer is enabled.
*/
private Boolean yarnBerryAuditAnalyzerEnabled;
/**
* Whether or not the Pnpm Audit Analyzer is enabled.
*/
Expand Down Expand Up @@ -941,6 +945,15 @@ public void setYarnAuditAnalyzerEnabled(Boolean yarnAuditAnalyzerEnabled) {
this.yarnAuditAnalyzerEnabled = yarnAuditAnalyzerEnabled;
}

/**
* Set the value of yarnBerryAuditAnalyzerEnabled.
*
* @param yarnBerryAuditAnalyzerEnabled new value of yarnBerryAuditAnalyzerEnabled
*/
public void setYarnBerryAuditAnalyzerEnabled(Boolean yarnBerryAuditAnalyzerEnabled) {
this.yarnBerryAuditAnalyzerEnabled = yarnBerryAuditAnalyzerEnabled;
}

/**
* Set the value of pnpmAuditAnalyzerEnabled.
*
Expand Down Expand Up @@ -1471,6 +1484,7 @@ protected void populateSettings() throws BuildException {
getSettings().setBooleanIfNotNull(Settings.KEYS.ANALYZER_NODE_PACKAGE_SKIPDEV, nodePackageSkipDevDependencies);
getSettings().setBooleanIfNotNull(Settings.KEYS.ANALYZER_NODE_AUDIT_ENABLED, nodeAuditAnalyzerEnabled);
getSettings().setBooleanIfNotNull(Settings.KEYS.ANALYZER_YARN_AUDIT_ENABLED, yarnAuditAnalyzerEnabled);
getSettings().setBooleanIfNotNull(Settings.KEYS.ANALYZER_YARN_BERRY_AUDIT_ENABLED, yarnBerryAuditAnalyzerEnabled);
getSettings().setBooleanIfNotNull(Settings.KEYS.ANALYZER_PNPM_AUDIT_ENABLED, pnpmAuditAnalyzerEnabled);
getSettings().setBooleanIfNotNull(Settings.KEYS.ANALYZER_NODE_AUDIT_USE_CACHE, nodeAuditAnalyzerUseCache);
getSettings().setBooleanIfNotNull(Settings.KEYS.ANALYZER_NODE_AUDIT_SKIPDEV, nodeAuditSkipDevDependencies);
Expand Down
1 change: 1 addition & 0 deletions ant/src/site/markdown/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ be needed.
nodeAuditSkipDevDependencies | Sets whether the Node Audit Analyzer will skip devDependencies. | false
nodePackageSkipDevDependencies | Sets whether the Node Package 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
yarnBerryAuditAnalyzerEnabled | Sets whether the Yarn Berry 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`. |  
Expand Down
2 changes: 2 additions & 0 deletions cli/src/main/java/org/owasp/dependencycheck/App.java
Original file line number Diff line number Diff line change
Expand Up @@ -597,6 +597,8 @@ protected void populateSettings(CliParser cli) throws InvalidSettingException {
!cli.isNodeAuditDisabled());
settings.setBoolean(Settings.KEYS.ANALYZER_YARN_AUDIT_ENABLED,
!cli.isYarnAuditDisabled());
settings.setBoolean(Settings.KEYS.ANALYZER_YARN_BERRY_AUDIT_ENABLED,
!cli.isYarnBerryAuditDisabled());
settings.setBoolean(Settings.KEYS.ANALYZER_PNPM_AUDIT_ENABLED,
!cli.isPnpmAuditDisabled());
settings.setBoolean(Settings.KEYS.ANALYZER_NODE_AUDIT_USE_CACHE,
Expand Down
15 changes: 15 additions & 0 deletions cli/src/main/java/org/owasp/dependencycheck/CliParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -537,6 +537,7 @@ private void addAdvancedOptions(final Options options) {
.addOption(newOption(ARGUMENT.DISABLE_NODE_AUDIT, "Disable the Node Audit Analyzer."))
.addOption(newOption(ARGUMENT.DISABLE_PNPM_AUDIT, "Disable the Pnpm Audit Analyzer."))
.addOption(newOption(ARGUMENT.DISABLE_YARN_AUDIT, "Disable the Yarn Audit Analyzer."))
.addOption(newOption(ARGUMENT.DISABLE_YARN_BERRY_AUDIT, "Disable the Yarn Berry Audit Analyzer."))
.addOption(newOption(ARGUMENT.DISABLE_NODE_AUDIT_CACHE, "Disallow the Node Audit Analyzer from caching results"))
.addOption(newOption(ARGUMENT.DISABLE_NODE_AUDIT_SKIPDEV, "Configures the Node Audit Analyzer to skip devDependencies"))
.addOption(newOption(ARGUMENT.DISABLE_RETIRE_JS, "Disable the RetireJS Analyzer."))
Expand Down Expand Up @@ -666,6 +667,16 @@ public boolean isYarnAuditDisabled() {
return isDisabled(ARGUMENT.DISABLE_YARN_AUDIT, Settings.KEYS.ANALYZER_YARN_AUDIT_ENABLED);
}

/**
* Returns true if the disableYarnBerryAudit command line argument was specified.
*
* @return true if the disableYarnBerryAudit command line argument was specified;
* otherwise false
*/
public boolean isYarnBerryAuditDisabled() {
return isDisabled(ARGUMENT.DISABLE_YARN_BERRY_AUDIT, Settings.KEYS.ANALYZER_YARN_BERRY_AUDIT_ENABLED);
}

/**
* Returns true if the disablePnpmAudit command line argument was specified.
*
Expand Down Expand Up @@ -1474,6 +1485,10 @@ public static class ARGUMENT {
* Disables the Yarn Audit Analyzer.
*/
public static final String DISABLE_YARN_AUDIT = "disableYarnAudit";
/**
* Disables the Yarn Berry Audit Analyzer.
*/
public static final String DISABLE_YARN_BERRY_AUDIT = "disableYarnBerryAudit";
/**
* Disables the Pnpm Audit Analyzer.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,284 @@
/*
* This file is part of dependency-check-ant.
*
* 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) 2021 The OWASP Foundation. All Rights Reserved.
*/
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
import us.springett.parsers.cpe.exceptions.CpeValidationException;

import javax.annotation.concurrent.ThreadSafe;
import javax.json.Json;
import javax.json.JsonException;
import javax.json.JsonObject;
import javax.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;

@ThreadSafe
public class YarnBerryAuditAnalyzer extends AbstractNpmAnalyzer {

/**
* The logger.
*/
private static final Logger LOGGER = LoggerFactory.getLogger(YarnBerryAuditAnalyzer.class);

/**
* The file name to scan.
*/
public static final String YARN_PACKAGE_LOCK = "yarn.lock";

/**
* Filter that detects files named "yarn.lock"
*/
private static final FileFilter LOCK_FILE_FILTER = FileFilterBuilder.newInstance()
.addFilenames(YARN_PACKAGE_LOCK).build();

/**
* The path to the `yarn` executable.
*/
private String yarnPath;

/**
* Analyzes the yarn lock file to determine vulnerable dependencies. Uses
* yarn npm audit to generate the advisories.
*
* @param dependency the yarn lock file
* @param engine the analysis engine
* @throws AnalysisException thrown if there is an error analyzing the file
*/
@Override
protected void analyzeDependency(Dependency dependency, Engine engine) throws AnalysisException {
if (dependency.getDisplayFileName().equals(dependency.getFileName())) {
engine.removeDependency(dependency);
}
final File packageLock = dependency.getActualFile();
if (!packageLock.isFile() || packageLock.length() == 0 || !shouldProcess(packageLock)) {
return;
}
final List<Advisory> advisories;
final MultiValuedMap<String, String> dependencyMap = new HashSetValuedHashMap<>();
advisories = analyzePackage(dependency);
try {
processResults(advisories, engine, dependency, dependencyMap);
} catch (CpeValidationException ex) {
throw new UnexpectedAnalysisException(ex);
}
}

@Override
protected String getAnalyzerEnabledSettingKey() {
return Settings.KEYS.ANALYZER_YARN_BERRY_AUDIT_ENABLED;
}

@Override
protected FileFilter getFileFilter() {
return LOCK_FILE_FILTER;
}

@Override
public String getName() {
return "Yarn Audit Analyzer";
}

@Override
public AnalysisPhase getAnalysisPhase() {
return AnalysisPhase.FINDING_ANALYSIS;
}

/**
* 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("{} Analyzer is disabled skipping yarn executable check", getName());
return;
}
final List<String> args = new ArrayList<>();
args.add(getYarn());
args.add("--help");
final ProcessBuilder builder = new ProcessBuilder(args);
LOGGER.debug("Launching: {}", args);
try {
final Process process = builder.start();
try (ProcessReader processReader = new ProcessReader(process)) {
processReader.readAll();
final int exitValue = process.waitFor();
final int expectedExitValue = 0;
final int yarnExecutableNotFoundExitValue = 127;
switch (exitValue) {
case expectedExitValue:
LOGGER.debug("{} is enabled.", getName());
break;
case yarnExecutableNotFoundExitValue:
default:
this.setEnabled(false);
LOGGER.warn("The {} has been disabled. Yarn executable was not found.", getName());
}
}
} catch (Exception ex) {
this.setEnabled(false);
LOGGER.warn("The {} has been disabled. Yarn executable was not found.", getName());
throw new InitializationException("Unable to read yarn audit output.", ex);
}
}

/**
* Attempts to determine the path to `yarn`.
*
* @return the path to `yarn`
*/
private String getYarn() {
final String value;
synchronized (this) {
if (yarnPath == null) {
final String path = getSettings().getString(Settings.KEYS.ANALYZER_YARN_PATH);
if (path == null) {
yarnPath = "yarn";
} else {
final File yarnFile = new File(path);
if (yarnFile.isFile()) {
yarnPath = yarnFile.getAbsolutePath();
} else {
LOGGER.warn("Provided path to `yarn` executable is invalid.");
yarnPath = "yarn";
}
}
}
value = yarnPath;
}
return value;
}

private List<JSONObject> fetchYarnAdvisories(Dependency dependency, boolean skipDevDependencies) throws AnalysisException {
final File folder = dependency.getActualFile().getParentFile();
if (!folder.isDirectory()) {
throw new AnalysisException(String.format("%s should have been a directory.", folder.getAbsolutePath()));
}
try {
final List<String> args = new ArrayList<>();

args.add(getYarn());
args.add("npm");
args.add("audit");
if (skipDevDependencies) {
args.add("--environment");
args.add("production");
}
args.add("--all");
args.add("--recursive");
args.add("--json");
final ProcessBuilder builder = new ProcessBuilder(args);
builder.directory(folder);
LOGGER.debug("Launching: {}", args);
// Workaround 64k limitation of InputStream, redirect stdout to a file that we will read later
// instead of reading directly stdout from Process's InputStream which is topped at 64k

final File tmpFile = getSettings().getTempFile("yarn_audit", "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);
LOGGER.debug("Process Out: {}", processReader.getOutput());
}
final String advisoriesJsons = new String(Files.readAllBytes(tmpFile.toPath()), StandardCharsets.UTF_8);
LOGGER.debug("Advisories JSON: {}", advisoriesJsons);
String[] advisoriesJsonArray = advisoriesJsons.split("\n");
List<JSONObject> advisories = new ArrayList<>();
for (String advisoriesJson : advisoriesJsonArray) {
advisories.add(new JSONObject(advisoriesJson));
}

return advisories;
} catch (JSONException e) {
throw new AnalysisException("Failed to parse the response from NPM Audit API "
+ "(YarnBerryAuditAnalyzer).", e);
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
throw new AnalysisException("Yarn audit process was interrupted.", ex);
}
} catch (IOException ioe) {
throw new AnalysisException("yarn audit failure; this error can be ignored if you are not analyzing projects with a yarn lockfile.", ioe);
}
}

/**
* 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> analyzePackage(Dependency dependency) throws AnalysisException {
try {
final boolean skipDevDependencies = getSettings().getBoolean(Settings.KEYS.ANALYZER_NODE_AUDIT_SKIPDEV, false);
final List<JSONObject> advisoriesJson = fetchYarnAdvisories(dependency, skipDevDependencies);
final List<Advisory> advisories = new ArrayList<>();
for (JSONObject advisoryJson : advisoriesJson) {
final Advisory advisory = new Advisory();
JSONObject object = advisoryJson.getJSONObject("children");
advisory.setGhsaId(object.getString("ID"));
advisory.setTitle(object.optString("Issue", null));
advisory.setOverview(object.optString("URL", null));
advisory.setSeverity(object.optString("Severity", null));
advisory.setVulnerableVersions(object.optString("Vulnerable Versions", null));
advisory.setModuleName(advisoryJson.optString("value", null));
advisory.setCwes(new ArrayList<>());
advisories.add(advisory);
}
return advisories;
} 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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ org.owasp.dependencycheck.analyzer.CMakeAnalyzer
org.owasp.dependencycheck.analyzer.NodePackageAnalyzer
org.owasp.dependencycheck.analyzer.NodeAuditAnalyzer
org.owasp.dependencycheck.analyzer.YarnAuditAnalyzer
org.owasp.dependencycheck.analyzer.YarnBerryAuditAnalyzer
org.owasp.dependencycheck.analyzer.PnpmAuditAnalyzer
org.owasp.dependencycheck.analyzer.GolangModAnalyzer
org.owasp.dependencycheck.analyzer.GolangDepAnalyzer
Expand Down
Loading

0 comments on commit 7551e42

Please sign in to comment.