Skip to content

Commit 22ddec5

Browse files
committed
feat: Implement Yarn Berry Analyser
1 parent 0ba6d4c commit 22ddec5

File tree

16 files changed

+11273
-128
lines changed

16 files changed

+11273
-128
lines changed

core/src/main/java/org/owasp/dependencycheck/analyzer/YarnAuditAnalyzer.java renamed to core/src/main/java/org/owasp/dependencycheck/analyzer/AbstractYarnAuditAnalyzer.java

Lines changed: 56 additions & 116 deletions
Original file line numberDiff line numberDiff line change
@@ -19,45 +19,36 @@
1919

2020
import org.apache.commons.collections4.MultiValuedMap;
2121
import org.apache.commons.collections4.multimap.HashSetValuedHashMap;
22-
import org.apache.commons.io.IOUtils;
2322
import org.apache.commons.lang3.StringUtils;
2423
import org.owasp.dependencycheck.Engine;
2524
import org.owasp.dependencycheck.analyzer.exception.AnalysisException;
26-
import org.owasp.dependencycheck.analyzer.exception.SearchException;
2725
import org.owasp.dependencycheck.analyzer.exception.UnexpectedAnalysisException;
2826
import org.owasp.dependencycheck.data.nodeaudit.Advisory;
29-
import org.owasp.dependencycheck.data.nodeaudit.NpmPayloadBuilder;
3027
import org.owasp.dependencycheck.dependency.Dependency;
3128
import org.owasp.dependencycheck.exception.InitializationException;
3229
import org.owasp.dependencycheck.utils.FileFilterBuilder;
3330
import org.owasp.dependencycheck.utils.Settings;
34-
import org.owasp.dependencycheck.utils.URLConnectionFailureException;
3531
import org.owasp.dependencycheck.utils.processing.ProcessReader;
32+
import org.semver4j.Semver;
33+
import org.semver4j.SemverException;
3634
import org.slf4j.Logger;
3735
import org.slf4j.LoggerFactory;
3836
import us.springett.parsers.cpe.exceptions.CpeValidationException;
3937

4038
import javax.annotation.concurrent.ThreadSafe;
41-
import jakarta.json.Json;
42-
import jakarta.json.JsonException;
43-
import jakarta.json.JsonObject;
44-
import jakarta.json.JsonReader;
4539
import java.io.File;
4640
import java.io.FileFilter;
4741
import java.io.IOException;
4842
import java.nio.charset.StandardCharsets;
4943
import java.nio.file.Files;
5044
import java.util.ArrayList;
51-
import java.util.Arrays;
5245
import java.util.List;
5346

5447
@ThreadSafe
55-
public class YarnAuditAnalyzer extends AbstractNpmAnalyzer {
48+
public abstract class AbstractYarnAuditAnalyzer extends AbstractNpmAnalyzer {
5649

57-
/**
58-
* The logger.
59-
*/
60-
private static final Logger LOGGER = LoggerFactory.getLogger(YarnAuditAnalyzer.class);
50+
private static final Logger LOGGER = LoggerFactory.getLogger(AbstractYarnAuditAnalyzer.class);
51+
protected static final int YARN_CLASSIC_MAJOR_VERSION = 1;
6152

6253
/**
6354
* The file name to scan.
@@ -71,23 +62,39 @@ public class YarnAuditAnalyzer extends AbstractNpmAnalyzer {
7162
.addFilenames(YARN_PACKAGE_LOCK).build();
7263

7364
/**
74-
* An expected error from `yarn audit --offline --verbose --json` that will
75-
* be ignored.
65+
* The path to the `yarn` executable.
7666
*/
77-
private static final String EXPECTED_ERROR = "{\"type\":\"error\",\"data\":\"Can't make a request in "
78-
+ "offline mode (\\\"https://registry.yarnpkg.com/-/npm/v1/security/audits\\\")\"}\n";
67+
private String yarnPath;
7968

8069
/**
81-
* The path to the `yarn` executable.
70+
* The version of the `yarn` executable.
8271
*/
83-
private String yarnPath;
72+
private String yarnVersion;
73+
74+
75+
/**
76+
* Extracts the major version from a version string.
77+
*
78+
* @return the major version (e.g., `4` from "4.2.1")
79+
*/
80+
protected int getYarnMajorVersion() {
81+
if (StringUtils.isBlank(yarnVersion)) {
82+
throw new IllegalArgumentException("Version string cannot be null or empty");
83+
}
84+
try {
85+
var semver = new Semver(yarnVersion);
86+
return semver.getMajor();
87+
} catch (SemverException e) {
88+
throw new IllegalArgumentException("Invalid version string format", e);
89+
}
90+
}
8491

8592
/**
8693
* Analyzes the yarn lock file to determine vulnerable dependencies. Uses
8794
* yarn audit --offline to generate the payload to be sent to the NPM API.
8895
*
8996
* @param dependency the yarn lock file
90-
* @param engine the analysis engine
97+
* @param engine the analysis engine
9198
* @throws AnalysisException thrown if there is an error analyzing the file
9299
*/
93100
@Override
@@ -110,6 +117,20 @@ protected void analyzeDependency(Dependency dependency, Engine engine) throws An
110117
}
111118
}
112119

120+
/**
121+
* Analyzes the package.
122+
*
123+
* @param lockFile a reference to the package-lock.json
124+
* @param packageFile a reference to the package.json
125+
* @param dependency a reference to the dependency-object for the yarn.lock
126+
* @param dependencyMap a collection of module/version pairs; during
127+
* creation of the payload the dependency map is populated with the
128+
* module/version information.
129+
* @return a list of advisories
130+
* @throws AnalysisException thrown when there is an error creating or submitting the npm audit
131+
*/
132+
protected abstract List<Advisory> analyzePackage(File lockFile, File packageFile, Dependency dependency, MultiValuedMap<String, String> dependencyMap) throws AnalysisException;
133+
113134
@Override
114135
protected String getAnalyzerEnabledSettingKey() {
115136
return Settings.KEYS.ANALYZER_YARN_AUDIT_ENABLED;
@@ -120,11 +141,6 @@ protected FileFilter getFileFilter() {
120141
return LOCK_FILE_FILTER;
121142
}
122143

123-
@Override
124-
public String getName() {
125-
return "Yarn Audit Analyzer";
126-
}
127-
128144
@Override
129145
public AnalysisPhase getAnalysisPhase() {
130146
return AnalysisPhase.FINDING_ANALYSIS;
@@ -145,7 +161,7 @@ protected void prepareFileTypeAnalyzer(Engine engine) throws InitializationExcep
145161
}
146162
final List<String> args = new ArrayList<>();
147163
args.add(getYarn());
148-
args.add("--help");
164+
args.add("--version");
149165
final ProcessBuilder builder = new ProcessBuilder(args);
150166
LOGGER.debug("Launching: {}", args);
151167
try {
@@ -158,6 +174,11 @@ protected void prepareFileTypeAnalyzer(Engine engine) throws InitializationExcep
158174
switch (exitValue) {
159175
case expectedExitValue:
160176
LOGGER.debug("{} is enabled.", getName());
177+
yarnVersion = processReader.getOutput();
178+
if (StringUtils.isBlank(yarnVersion)) {
179+
this.setEnabled(false);
180+
LOGGER.warn("The {} has been disabled. Yarn version could not be determined.", getName());
181+
}
161182
break;
162183
case yarnExecutableNotFoundExitValue:
163184
default:
@@ -177,7 +198,7 @@ protected void prepareFileTypeAnalyzer(Engine engine) throws InitializationExcep
177198
*
178199
* @return the path to `yarn`
179200
*/
180-
private String getYarn() {
201+
protected String getYarn() {
181202
final String value;
182203
synchronized (this) {
183204
if (yarnPath == null) {
@@ -199,54 +220,24 @@ private String getYarn() {
199220
return value;
200221
}
201222

202-
private JsonObject fetchYarnAuditJson(Dependency dependency, boolean skipDevDependencies) throws AnalysisException {
203-
final File folder = dependency.getActualFile().getParentFile();
204-
if (!folder.isDirectory()) {
205-
throw new AnalysisException(String.format("%s should have been a directory.", folder.getAbsolutePath()));
206-
}
223+
/**
224+
* Workaround 64k limitation of InputStream, redirect stdout to a file that we will read later
225+
* instead of reading directly stdout from Process's InputStream which is topped at 64k
226+
*/
227+
protected String startAndReadStdoutToString(ProcessBuilder builder) throws AnalysisException {
207228
try {
208-
final List<String> args = new ArrayList<>();
209-
210-
args.add(getYarn());
211-
args.add("audit");
212-
//offline audit is not supported - but the audit request is generated in the verbose output
213-
args.add("--offline");
214-
if (skipDevDependencies) {
215-
args.add("--groups");
216-
args.add("dependencies");
217-
}
218-
args.add("--json");
219-
args.add("--verbose");
220-
final ProcessBuilder builder = new ProcessBuilder(args);
221-
builder.directory(folder);
222-
LOGGER.debug("Launching: {}", args);
223-
// Workaround 64k limitation of InputStream, redirect stdout to a file that we will read later
224-
// instead of reading directly stdout from Process's InputStream which is topped at 64k
225-
226229
final File tmpFile = getSettings().getTempFile("yarn_audit", "json");
227230
builder.redirectOutput(tmpFile);
228231
final Process process = builder.start();
229232
try (ProcessReader processReader = new ProcessReader(process)) {
230233
processReader.readAll();
231234
final String errOutput = processReader.getError();
232235

233-
if (!StringUtils.isBlank(errOutput) && !EXPECTED_ERROR.equals(errOutput)) {
236+
if (!StringUtils.isBlank(errOutput)) {
234237
LOGGER.debug("Process Error Out: {}", errOutput);
235238
LOGGER.debug("Process Out: {}", processReader.getOutput());
236239
}
237-
final String verboseJson = new String(Files.readAllBytes(tmpFile.toPath()), StandardCharsets.UTF_8);
238-
final String auditRequestJson = Arrays.stream(verboseJson.split("\n"))
239-
.filter(line -> line.contains("Audit Request"))
240-
.findFirst().get();
241-
String auditRequest;
242-
try (JsonReader reader = Json.createReader(IOUtils.toInputStream(auditRequestJson, StandardCharsets.UTF_8))) {
243-
final JsonObject jsonObject = reader.readObject();
244-
auditRequest = jsonObject.getString("data");
245-
auditRequest = auditRequest.substring(15);
246-
}
247-
LOGGER.debug("Audit Request: {}", auditRequest);
248-
249-
return Json.createReader(IOUtils.toInputStream(auditRequest, StandardCharsets.UTF_8)).readObject();
240+
return new String(Files.readAllBytes(tmpFile.toPath()), StandardCharsets.UTF_8);
250241
} catch (InterruptedException ex) {
251242
Thread.currentThread().interrupt();
252243
throw new AnalysisException("Yarn audit process was interrupted.", ex);
@@ -255,55 +246,4 @@ private JsonObject fetchYarnAuditJson(Dependency dependency, boolean skipDevDepe
255246
throw new AnalysisException("yarn audit failure; this error can be ignored if you are not analyzing projects with a yarn lockfile.", ioe);
256247
}
257248
}
258-
259-
/**
260-
* Analyzes the package and yarn lock files by extracting dependency
261-
* information, creating a payload to submit to the npm audit API,
262-
* submitting the payload, and returning the identified advisories.
263-
*
264-
* @param lockFile a reference to the package-lock.json
265-
* @param packageFile a reference to the package.json
266-
* @param dependency a reference to the dependency-object for the yarn.lock
267-
* @param dependencyMap a collection of module/version pairs; during
268-
* creation of the payload the dependency map is populated with the
269-
* module/version information.
270-
* @return a list of advisories
271-
* @throws AnalysisException thrown when there is an error creating or
272-
* submitting the npm audit API payload
273-
*/
274-
private List<Advisory> analyzePackage(final File lockFile, final File packageFile,
275-
Dependency dependency, MultiValuedMap<String, String> dependencyMap)
276-
throws AnalysisException {
277-
try {
278-
final boolean skipDevDependencies = getSettings().getBoolean(Settings.KEYS.ANALYZER_NODE_AUDIT_SKIPDEV, false);
279-
// Retrieves the contents of package-lock.json from the Dependency
280-
final JsonObject lockJson = fetchYarnAuditJson(dependency, skipDevDependencies);
281-
// Retrieves the contents of package-lock.json from the Dependency
282-
final JsonObject packageJson;
283-
try (JsonReader packageReader = Json.createReader(Files.newInputStream(packageFile.toPath()))) {
284-
packageJson = packageReader.readObject();
285-
}
286-
// Modify the payload to meet the NPM Audit API requirements
287-
final JsonObject payload = NpmPayloadBuilder.build(lockJson, packageJson, dependencyMap, skipDevDependencies);
288-
289-
// Submits the package payload to the nsp check service
290-
return getSearcher().submitPackage(payload);
291-
292-
} catch (URLConnectionFailureException e) {
293-
this.setEnabled(false);
294-
throw new AnalysisException("Failed to connect to the NPM Audit API (YarnAuditAnalyzer); the analyzer "
295-
+ "is being disabled and may result in false negatives.", e);
296-
} catch (IOException e) {
297-
LOGGER.debug("Error reading dependency or connecting to NPM Audit API", e);
298-
this.setEnabled(false);
299-
throw new AnalysisException("Failed to read results from the NPM Audit API (YarnAuditAnalyzer); "
300-
+ "the analyzer is being disabled and may result in false negatives.", e);
301-
} catch (JsonException e) {
302-
throw new AnalysisException(String.format("Failed to parse %s file from the NPM Audit API "
303-
+ "(YarnAuditAnalyzer).", lockFile.getPath()), e);
304-
} catch (SearchException ex) {
305-
LOGGER.error("YarnAuditAnalyzer failed on {}", dependency.getActualFilePath());
306-
throw ex;
307-
}
308-
}
309249
}

core/src/main/java/org/owasp/dependencycheck/analyzer/AnalysisPhase.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,8 @@ public enum AnalysisPhase {
125125
* {@link NvdCveAnalyzer}
126126
* {@link PnpmAuditAnalyzer}
127127
* {@link RetireJsAnalyzer}
128-
* {@link YarnAuditAnalyzer}
128+
* {@link YarnClassicAuditAnalyzer}
129+
* {@link YarnBerryAuditAnalyzer}
129130
*
130131
*/
131132
FINDING_ANALYSIS,

core/src/main/java/org/owasp/dependencycheck/analyzer/NodePackageAnalyzer.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ protected String getAnalyzerEnabledSettingKey() {
191191
*/
192192
private boolean isNodeAuditEnabled(Engine engine) {
193193
for (Analyzer a : engine.getAnalyzers()) {
194-
if (a instanceof NodeAuditAnalyzer || a instanceof YarnAuditAnalyzer || a instanceof PnpmAuditAnalyzer) {
194+
if (a instanceof NodeAuditAnalyzer || a instanceof AbstractYarnAuditAnalyzer || a instanceof PnpmAuditAnalyzer) {
195195
if (a.isEnabled()) {
196196
try {
197197
((AbstractNpmAnalyzer) a).prepareFileTypeAnalyzer(engine);

0 commit comments

Comments
 (0)