Skip to content

Commit 226279c

Browse files
soul2zimateclaude
andcommitted
fix: exclude transitive devDependencies from Yarn Berry SBOM using BFS reachability
The previous implementation only filtered devDependencies at the root edge, but still processed all non-root nodes unconditionally. This meant transitive dependencies of devDeps (e.g., jest -> some-test-util) would leak into the SBOM. Uses BFS from root production deps to compute the reachable set, then skips unreachable nodes and edges during SBOM emission. Also adds a test fixture with a transitive devDep to expose the issue. Co-Authored-By: Claude Sonnet 4 <noreply@anthropic.com>
1 parent 31a5cab commit 226279c

File tree

2 files changed

+74
-9
lines changed

2 files changed

+74
-9
lines changed

src/main/java/io/github/guacsec/trustifyda/providers/YarnBerryProcessor.java

Lines changed: 72 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@
2323
import io.github.guacsec.trustifyda.sbom.Sbom;
2424
import io.github.guacsec.trustifyda.tools.Operations;
2525
import java.nio.file.Path;
26+
import java.util.ArrayDeque;
27+
import java.util.HashMap;
28+
import java.util.HashSet;
2629
import java.util.Map;
2730
import java.util.Set;
2831
import java.util.TreeMap;
@@ -109,32 +112,93 @@ void addDependenciesToSbom(Sbom sbom, JsonNode depTree) {
109112
if (depTree == null) {
110113
return;
111114
}
115+
116+
Map<String, JsonNode> nodeIndex = new HashMap<>();
117+
depTree.forEach(n -> nodeIndex.put(n.get("value").asText(), n));
118+
112119
Set<String> prodDeps = manifest.dependencies;
120+
Set<String> reachable = new HashSet<>();
121+
var queue = new ArrayDeque<String>();
122+
123+
for (JsonNode n : depTree) {
124+
var depName = n.get("value").asText();
125+
if (isRoot(depName)) {
126+
var deps = (ArrayNode) n.get("children").get("Dependencies");
127+
if (deps != null) {
128+
for (JsonNode d : deps) {
129+
var locator = d.get("locator").asText();
130+
var target = purlFromlocator(locator);
131+
if (target != null) {
132+
var fullName = purlToFullName(target);
133+
if (prodDeps.contains(fullName)) {
134+
queue.add(locator);
135+
}
136+
}
137+
}
138+
}
139+
break;
140+
}
141+
}
142+
143+
Set<String> reachableNodeValues = new HashSet<>();
144+
while (!queue.isEmpty()) {
145+
var locator = queue.poll();
146+
if (reachable.contains(locator)) {
147+
continue;
148+
}
149+
reachable.add(locator);
150+
151+
var nodeValue = nodeValueFromLocator(locator);
152+
reachableNodeValues.add(nodeValue);
153+
154+
var node = nodeIndex.get(nodeValue);
155+
if (node != null) {
156+
var deps = (ArrayNode) node.get("children").get("Dependencies");
157+
if (deps != null) {
158+
for (JsonNode d : deps) {
159+
var childLocator = d.get("locator").asText();
160+
if (!reachable.contains(childLocator)) {
161+
queue.add(childLocator);
162+
}
163+
}
164+
}
165+
}
166+
}
167+
113168
depTree.forEach(
114169
n -> {
115170
var depName = n.get("value").asText();
116171
var isRootNode = isRoot(depName);
172+
if (!isRootNode && !reachableNodeValues.contains(depName)) {
173+
return;
174+
}
175+
117176
var from = isRootNode ? sbom.getRoot() : purlFromNode(depName, n);
118177
var deps = (ArrayNode) n.get("children").get("Dependencies");
119178
if (deps != null && !deps.isEmpty()) {
120179
deps.forEach(
121180
d -> {
122-
var target = purlFromlocator(d.get("locator").asText());
181+
var locator = d.get("locator").asText();
182+
if (!reachable.contains(locator)) {
183+
return;
184+
}
185+
var target = purlFromlocator(locator);
123186
if (target != null) {
124-
// For root node, only include production dependencies
125-
if (isRootNode) {
126-
var fullName = purlToFullName(target);
127-
if (!prodDeps.contains(fullName)) {
128-
return;
129-
}
130-
}
131187
sbom.addDependency(from, target, null);
132188
}
133189
});
134190
}
135191
});
136192
}
137193

194+
private String nodeValueFromLocator(String locator) {
195+
var matcher = VIRTUAL_LOCATOR_PATTERN.matcher(locator);
196+
if (matcher.matches()) {
197+
return matcher.group(1) + "@npm:" + matcher.group(2);
198+
}
199+
return locator;
200+
}
201+
138202
private static String purlToFullName(PackageURL purl) {
139203
return purl.getNamespace() != null
140204
? purl.getNamespace() + "/" + purl.getName()

src/test/resources/tst_manifests/yarn-berry/deps_with_mixed_dep_types/yarn-berry-ls-stack.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
{"value":"eslint@npm:7.0.0","children":{"Version":"7.0.0"}}
44
{"value":"express@npm:4.17.1","children":{"Version":"4.17.1","Dependencies":[{"descriptor":"body-parser@npm:1.19.0","locator":"body-parser@npm:1.19.0"}]}}
55
{"value":"follow-redirects@npm:1.5.10","children":{"Version":"1.5.10"}}
6-
{"value":"jest@npm:26.0.0","children":{"Version":"26.0.0"}}
6+
{"value":"jest@npm:26.0.0","children":{"Version":"26.0.0","Dependencies":[{"descriptor":"some-test-util@npm:1.0.0","locator":"some-test-util@npm:1.0.0"}]}}
7+
{"value":"some-test-util@npm:1.0.0","children":{"Version":"1.0.0"}}
78
{"value":"lodash@npm:4.17.19","children":{"Version":"4.17.19"}}
89
{"value":"mixed-deps-test@workspace:.","children":{"Version":"1.0.0","Dependencies":[{"descriptor":"axios@npm:0.19.0","locator":"axios@npm:0.19.0"},{"descriptor":"eslint@npm:7.0.0","locator":"eslint@npm:7.0.0"},{"descriptor":"express@npm:4.17.1","locator":"express@npm:4.17.1"},{"descriptor":"jest@npm:26.0.0","locator":"jest@npm:26.0.0"},{"descriptor":"lodash@npm:4.17.19","locator":"lodash@npm:4.17.19"}]}}

0 commit comments

Comments
 (0)