Skip to content

Commit 9c07e40

Browse files
committed
fix: improve consistency of results from YarnAuditAnalyzer
Signed-off-by: Chad Wilson <29788154+chadlwilson@users.noreply.github.com>
1 parent 02bb473 commit 9c07e40

19 files changed

Lines changed: 379 additions & 16528 deletions

File tree

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

Lines changed: 32 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import org.apache.commons.lang3.StringUtils;
2222
import org.json.JSONException;
2323
import org.json.JSONObject;
24+
import org.jspecify.annotations.NonNull;
2425
import org.owasp.dependencycheck.Engine;
2526
import org.owasp.dependencycheck.analyzer.exception.AnalysisException;
2627
import org.owasp.dependencycheck.analyzer.exception.UnexpectedAnalysisException;
@@ -42,9 +43,14 @@
4243
import java.io.IOException;
4344
import java.nio.file.Files;
4445
import java.util.ArrayList;
46+
import java.util.Arrays;
4547
import java.util.List;
48+
import java.util.Map;
49+
import java.util.stream.Collectors;
4650
import java.util.stream.Stream;
4751

52+
import static org.apache.commons.lang3.StringUtils.defaultIfBlank;
53+
import static org.apache.commons.lang3.SystemUtils.getEnvironmentVariable;
4854
import static org.owasp.dependencycheck.utils.FileUtils.existsWithContent;
4955

5056
@ThreadSafe
@@ -55,12 +61,17 @@ public class YarnAuditAnalyzer extends AbstractNpmAnalyzer {
5561
*/
5662
private static final Logger LOGGER = LoggerFactory.getLogger(YarnAuditAnalyzer.class);
5763

58-
private static final int YARN_BERRY_MAJOR_VERSION_MIN = 2;
64+
/***
65+
* Minimum Yarn major version supported. Only in Yarn v4 was support for the npm audit bulk API added.
66+
*/
67+
private static final int YARN_MAJOR_VERSION_MIN = 4;
5968

6069
/**
6170
* The file name to scan.
6271
*/
6372
public static final String YARN_PACKAGE_LOCK = "yarn.lock";
73+
static final String YARN_ENV_IGNORE_PATH = "YARN_IGNORE_PATH";
74+
static final String YARN_ENV_ENABLE_TELEMETRY = "YARN_ENABLE_TELEMETRY";
6475

6576
/**
6677
* Filter that detects files named "yarn.lock"
@@ -101,8 +112,7 @@ public AnalysisPhase getAnalysisPhase() {
101112
*/
102113
private Semver getYarnVersion(File dependencyDirectory) {
103114
List<String> args = List.of(yarnPath, "--version");
104-
final ProcessBuilder builder = new ProcessBuilder(args);
105-
builder.directory(dependencyDirectory);
115+
final ProcessBuilder builder = createYarnBuilder(dependencyDirectory, args);
106116
try {
107117
final Process process = builder.start();
108118
try (ProcessReader processReader = new ProcessReader(process)) {
@@ -217,8 +227,8 @@ protected void analyzeDependency(Dependency dependency, Engine engine) throws An
217227
}
218228
File dependencyDirectory = getDependencyDirectory(packageLock);
219229
final var yarnVersion = getYarnVersion(dependencyDirectory);
220-
if (yarnVersion.getMajor() < YARN_BERRY_MAJOR_VERSION_MIN) {
221-
LOGGER.warn("Yarn dependency skipped: {} - Yarn Classic (v{}) is not supported.", dependency.getActualFile(), yarnVersion);
230+
if (yarnVersion.getMajor() < YARN_MAJOR_VERSION_MIN) {
231+
LOGGER.warn("Yarn dependency skipped: {} - Yarn v{} (prior to v{}) is not supported.", dependency.getActualFile(), yarnVersion, YARN_MAJOR_VERSION_MIN);
222232
return;
223233
}
224234

@@ -229,7 +239,7 @@ protected void analyzeDependency(Dependency dependency, Engine engine) throws An
229239
List<Advisory> advisories = parseAdvisoryJsons(advisoryJsons);
230240
processResults(advisories, engine, dependency, new HashSetValuedHashMap<>());
231241
} catch (JSONException e) {
232-
throw new AnalysisException("Failed to parse the response from NPM Audit API (YarnAuditAnalyzer).", e);
242+
throw new AnalysisException("Failed to parse the advisories from `yarn npm audit` (YarnAuditAnalyzer).", e);
233243
} catch (CpeValidationException e) {
234244
throw new UnexpectedAnalysisException(e);
235245
}
@@ -257,25 +267,26 @@ private List<JSONObject> fetchYarnAdvisories(Dependency dependency, boolean skip
257267
args.add("--recursive");
258268
args.add("--no-deprecations");
259269
args.add("--json");
260-
final ProcessBuilder builder = new ProcessBuilder(args);
261-
builder.directory(getDependencyDirectory(dependency.getActualFile()));
262-
263-
final String advisoriesJsons = startAndReadStdoutToString(builder);
270+
final String advisoriesJsons = startAndReadStdoutToString(createYarnBuilder(getDependencyDirectory(dependency.getActualFile()), args));
264271

265272
LOGGER.debug("Advisories JSON: {}", advisoriesJsons);
266-
final String[] advisoriesJsonArray = Stream.of(advisoriesJsons.split("\n"))
273+
return Arrays.stream(Stream.of(advisoriesJsons.split("\n"))
267274
.filter(s -> !s.isBlank())
268-
.toArray(String[]::new);
269-
try {
270-
final List<JSONObject> advisories = new ArrayList<>();
271-
for (String advisoriesJson : advisoriesJsonArray) {
272-
advisories.add(new JSONObject(advisoriesJson));
273-
}
275+
.toArray(String[]::new))
276+
.map(JSONObject::new)
277+
.collect(Collectors.toList());
278+
}
274279

275-
return advisories;
276-
} catch (JSONException e) {
277-
throw new AnalysisException("Failed to parse the response from NPM Audit API (YarnAuditAnalyzer).", e);
278-
}
280+
private static @NonNull ProcessBuilder createYarnBuilder(File dependencyDirectory, List<String> args) {
281+
final ProcessBuilder builder = new ProcessBuilder(args).directory(dependencyDirectory);
282+
283+
builder.environment().putAll(Map.of(
284+
// Default to disable use of yarnPath
285+
YARN_ENV_IGNORE_PATH, defaultIfBlank(getEnvironmentVariable(YARN_ENV_IGNORE_PATH, null), "true"),
286+
// Force disable telemetry
287+
YARN_ENV_ENABLE_TELEMETRY, "false"
288+
));
289+
return builder;
279290
}
280291

281292
private static List<Advisory> parseAdvisoryJsons(List<JSONObject> advisoryJsons) throws JSONException {

core/src/test/java/org/owasp/dependencycheck/analyzer/YarnAuditAnalyzerIT.java

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,16 @@
1717
*/
1818
package org.owasp.dependencycheck.analyzer;
1919

20+
import org.apache.commons.lang3.SystemUtils;
2021
import org.jspecify.annotations.NonNull;
2122
import org.junit.jupiter.api.AfterEach;
2223
import org.junit.jupiter.api.BeforeEach;
2324
import org.junit.jupiter.api.Nested;
2425
import org.junit.jupiter.api.Test;
26+
import org.junit.jupiter.params.ParameterizedTest;
27+
import org.junit.jupiter.params.provider.NullSource;
28+
import org.junit.jupiter.params.provider.ValueSource;
29+
import org.mockito.MockedStatic;
2530
import org.owasp.dependencycheck.BaseTest;
2631
import org.owasp.dependencycheck.Engine;
2732
import org.owasp.dependencycheck.dependency.Dependency;
@@ -35,6 +40,7 @@
3540
import static org.junit.jupiter.api.Assertions.assertEquals;
3641
import static org.junit.jupiter.api.Assertions.assertThrows;
3742
import static org.junit.jupiter.api.Assertions.assertTrue;
43+
import static org.mockito.Mockito.mockStatic;
3844

3945
class YarnAuditAnalyzerIT extends BaseTest {
4046

@@ -55,13 +61,20 @@ void cleanup() {
5561
}
5662

5763
@Nested
58-
class Classic {
64+
class UnsupportedYarnVersion {
5965
@Test
6066
void testAnalyzePackageYarnClassic() throws Exception {
6167
final Dependency toScan = new Dependency(BaseTest.getResourceAsFile(YarnAuditAnalyzerIT.this, "yarn/yarn-classic-audit/yarn.lock"));
6268
analyzer.analyze(toScan, engine);
6369
assertEquals(0, engine.getDependencies().length, "No dependencies should be identified");
6470
}
71+
72+
@Test
73+
void testAnalyzePackageYarnBerry() throws Exception {
74+
final Dependency toScan = new Dependency(BaseTest.getResourceAsFile(YarnAuditAnalyzerIT.this, "yarn/yarn-berry-audit-unsupported/yarn.lock"));
75+
analyzer.analyze(toScan, engine);
76+
assertEquals(0, engine.getDependencies().length, "No dependencies should be identified");
77+
}
6578
}
6679

6780
@Nested
@@ -77,10 +90,39 @@ void testAnalyzeWithBadYarnConfiguration() {
7790
assertThat(exception.getMessage(), containsString("Unable to determine yarn version"));
7891
assertThat(exception.getCause().getMessage(), allOf(
7992
containsString("exit value 1"),
80-
containsString("bad-path-to-yarn.js")
93+
containsString("Couldn't parse \"bad-value\" as a boolean")
8194
));
8295
}
8396

97+
@ParameterizedTest
98+
@NullSource
99+
@ValueSource(strings = {" ", "1", "true"})
100+
void testAnalyzeIgnoresBadYarnPath(String envValue) throws Exception {
101+
try (MockedStatic<SystemUtils> systemMock = mockStatic(SystemUtils.class)) {
102+
systemMock.when(() -> SystemUtils.getEnvironmentVariable("YARN_IGNORE_PATH", null)).thenReturn(envValue);
103+
104+
final Dependency toScan = new Dependency(BaseTest.getResourceAsFile(YarnAuditAnalyzerIT.this, "yarn/yarn-berry-audit-bad-path/yarn.lock"));
105+
analyzer.analyze(toScan, engine);
106+
assertEquals(0, engine.getDependencies().length, "No dependency should be identified");
107+
}
108+
}
109+
110+
@ParameterizedTest
111+
@ValueSource(strings = {"0", "false"})
112+
void testAnalyzeAllowsYarnPath(String envValue) {
113+
try (MockedStatic<SystemUtils> systemMock = mockStatic(SystemUtils.class)) {
114+
systemMock.when(() -> SystemUtils.getEnvironmentVariable("YARN_IGNORE_PATH", null)).thenReturn(envValue);
115+
116+
final Dependency toScan = new Dependency(BaseTest.getResourceAsFile(YarnAuditAnalyzerIT.this, "yarn/yarn-berry-audit-bad-path/yarn.lock"));
117+
IllegalStateException exception = assertThrows(IllegalStateException.class, () -> analyzer.analyze(toScan, engine));
118+
assertThat(exception.getMessage(), containsString("Unable to determine yarn version"));
119+
assertThat(exception.getCause().getMessage(), allOf(
120+
containsString("no such file or directory"), // yarnrc yarnPath points to non-existent path so we can detect usage
121+
containsString("does-not-exist/yarn.js")
122+
));
123+
}
124+
}
125+
84126
@Test
85127
void testAnalyzeWithBadPackageManagerConfiguration() {
86128
IllegalStateException exception = assertThrows(IllegalStateException.class, () -> testAnalyzeForUglifyJs("yarn/yarn-berry-audit-bad-package-manager/yarn.lock"));
Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
11
enableGlobalCache: true
22

3-
enableTelemetry: false
4-
53
nodeLinker: node-modules
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
enableGlobalCache: true
2+
3+
nodeLinker: node-modules
4+
5+
yarnPath: does-not-exist/yarn.js
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"name": "owasp-nodejs-goat",
3+
"private": true,
4+
"version": "1.3.0",
5+
"description": "A tool to learn OWASP Top 10 for node.js developers",
6+
"main": "server.js",
7+
"comments": {
8+
"//": "a9 insecure components"
9+
},
10+
"scripts": {
11+
"start": "node server.js",
12+
"test": "node node_modules/grunt-cli/bin/grunt test",
13+
"db:seed": "grunt db-reset",
14+
"precommit": "grunt precommit"
15+
},
16+
"repository": "https://github.com/OWASP/NodejsGoat",
17+
"license": "Apache 2.0",
18+
"packageManager": "yarn@4.13.0"
19+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# This file is generated by running "yarn install" inside your project.
2+
# Manual changes might be lost - proceed with caution!
3+
4+
__metadata:
5+
version: 8
6+
cacheKey: 10c0
7+
8+
"owasp-nodejs-goat@workspace:.":
9+
version: 0.0.0-use.local
10+
resolution: "owasp-nodejs-goat@workspace:."
11+
languageName: unknown
12+
linkType: soft
Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,3 @@
1-
enableGlobalCache: true
2-
3-
enableTelemetry: false
1+
enableGlobalCache: "bad-value"
42

53
nodeLinker: node-modules
6-
7-
yarnPath: bad-path-to-yarn.js
Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
11
enableGlobalCache: true
22

3-
enableTelemetry: false
4-
53
nodeLinker: node-modules
Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
11
enableGlobalCache: true
22

3-
enableTelemetry: false
4-
53
nodeLinker: node-modules
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
enableGlobalCache: true
2+
3+
nodeLinker: node-modules

0 commit comments

Comments
 (0)