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
Original file line number Diff line number Diff line change
Expand Up @@ -159,20 +159,29 @@ private Sbom getDependencySbom() throws IOException {
var sbom = SbomFactory.newInstance();
sbom.addRoot(manifest.root, readLicenseFromManifest());
addDependenciesToSbom(sbom, depTree);
ensurePeerAndOptionalDeps(sbom);
sbom.filterIgnoredDeps(manifest.ignored);
return sbom;
}

protected void addDependenciesToSbom(Sbom sbom, JsonNode depTree) {
var deps = depTree.get("dependencies");
addDependenciesFromKey(sbom, depTree, "dependencies");
addDependenciesFromKey(sbom, depTree, "optionalDependencies");
}

private void addDependenciesFromKey(Sbom sbom, JsonNode depTree, String key) {
var deps = depTree.get(key);
if (deps == null) {
return;
}
deps.fields()
.forEachRemaining(
e -> {
var version = e.getValue().get("version").asText();
var target = toPurl(e.getKey(), version);
JsonNode versionNode = e.getValue().get("version");
if (versionNode == null || versionNode.isNull()) {
return; // skip entries without a resolved version
}
var target = toPurl(e.getKey(), versionNode.asText());
sbom.addDependency(manifest.root, target, null);
addDependenciesOf(sbom, target, e.getValue());
});
Expand All @@ -187,6 +196,7 @@ private Sbom getDirectDependencySbom() throws IOException {
.filter(e -> manifest.dependencies.contains(e.getKey()))
.map(Entry::getValue)
.forEach(p -> sbom.addDependency(manifest.root, p, null));
ensurePeerAndOptionalDeps(sbom);
sbom.filterIgnoredDeps(manifest.ignored);
return sbom;
}
Expand All @@ -195,9 +205,18 @@ private Sbom getDirectDependencySbom() throws IOException {
// axios -> pkg:npm/axios@0.19.2
protected Map<String, PackageURL> getRootDependencies(JsonNode depTree) {
Map<String, PackageURL> direct = new TreeMap<>();
depTree
.get("dependencies")
.fields()
addRootDependenciesFromKey(direct, depTree, "dependencies");
addRootDependenciesFromKey(direct, depTree, "optionalDependencies");
return direct;
}

private void addRootDependenciesFromKey(
Map<String, PackageURL> direct, JsonNode depTree, String key) {
var node = depTree.get(key);
if (node == null) {
return;
}
node.fields()
.forEachRemaining(
e -> {
String name = e.getKey();
Expand All @@ -208,7 +227,21 @@ protected Map<String, PackageURL> getRootDependencies(JsonNode depTree) {
direct.put(name, purl);
}
});
return direct;
}

private void ensurePeerAndOptionalDeps(Sbom sbom) {
var rootComponent = sbom.getRoot();
var depSources = new Map[] {manifest.peerDependencies, manifest.optionalDependencies};
for (var source : depSources) {
@SuppressWarnings("unchecked")
Map<String, String> deps = source;
deps.forEach(
(name, version) -> {
if (!sbom.checkIfPackageInsideDependsOnList(rootComponent, name)) {
sbom.addDependency(manifest.root, toPurl(name, version), null);
}
});
Comment thread
qodo-code-review[bot] marked this conversation as resolved.
}
}

protected JsonNode buildDependencyTree(boolean includeTransitive) throws JsonProcessingException {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,11 @@
import io.github.guacsec.trustifyda.sbom.Sbom;
import io.github.guacsec.trustifyda.tools.Operations;
import java.nio.file.Path;
import java.util.ArrayDeque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.regex.Pattern;

Expand Down Expand Up @@ -108,15 +112,77 @@ void addDependenciesToSbom(Sbom sbom, JsonNode depTree) {
if (depTree == null) {
return;
}

Map<String, JsonNode> nodeIndex = new HashMap<>();
depTree.forEach(n -> nodeIndex.put(n.get("value").asText(), n));

Set<String> prodDeps = manifest.dependencies;
Set<String> reachable = new HashSet<>();
var queue = new ArrayDeque<String>();

for (JsonNode n : depTree) {
var depName = n.get("value").asText();
if (isRoot(depName)) {
var deps = (ArrayNode) n.get("children").get("Dependencies");
if (deps != null) {
for (JsonNode d : deps) {
var locator = d.get("locator").asText();
var target = purlFromlocator(locator);
if (target != null) {
var fullName = purlToFullName(target);
if (prodDeps.contains(fullName)) {
queue.add(locator);
}
}
}
}
break;
}
}

Set<String> reachableNodeValues = new HashSet<>();
while (!queue.isEmpty()) {
var locator = queue.poll();
if (reachable.contains(locator)) {
continue;
}
reachable.add(locator);

var nodeValue = nodeValueFromLocator(locator);
reachableNodeValues.add(nodeValue);

var node = nodeIndex.get(nodeValue);
if (node != null) {
var deps = (ArrayNode) node.get("children").get("Dependencies");
if (deps != null) {
for (JsonNode d : deps) {
var childLocator = d.get("locator").asText();
if (!reachable.contains(childLocator)) {
queue.add(childLocator);
}
}
}
}
}

depTree.forEach(
n -> {
var depName = n.get("value").asText();
var from = isRoot(depName) ? sbom.getRoot() : purlFromNode(depName, n);
var isRootNode = isRoot(depName);
if (!isRootNode && !reachableNodeValues.contains(depName)) {
return;
}

var from = isRootNode ? sbom.getRoot() : purlFromNode(depName, n);
var deps = (ArrayNode) n.get("children").get("Dependencies");
if (deps != null && !deps.isEmpty()) {
deps.forEach(
d -> {
var target = purlFromlocator(d.get("locator").asText());
var locator = d.get("locator").asText();
if (!reachable.contains(locator)) {
return;
}
var target = purlFromlocator(locator);
if (target != null) {
sbom.addDependency(from, target, null);
}
Expand All @@ -125,6 +191,20 @@ void addDependenciesToSbom(Sbom sbom, JsonNode depTree) {
});
}

private String nodeValueFromLocator(String locator) {
var matcher = VIRTUAL_LOCATOR_PATTERN.matcher(locator);
if (matcher.matches()) {
return matcher.group(1) + "@npm:" + matcher.group(2);
}
return locator;
}

private static String purlToFullName(PackageURL purl) {
return purl.getNamespace() != null
? purl.getNamespace() + "/" + purl.getName()
: purl.getName();
}

private PackageURL purlFromlocator(String locator) {
if (locator == null) return null;
var matcher = LOCATOR_PATTERN.matcher(locator);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

public class Manifest {
Expand All @@ -35,6 +37,8 @@ public class Manifest {
public final String license;
public final PackageURL root;
public final Set<String> dependencies;
public final Map<String, String> peerDependencies;
public final Map<String, String> optionalDependencies;
public final Set<String> ignored;
public final Path path;

Expand All @@ -45,6 +49,8 @@ public Manifest(Path manifestPath) throws IOException {
}
var content = loadManifest(manifestPath);
this.dependencies = loadDependencies(content);
this.peerDependencies = loadDependencyMap(content, "peerDependencies");
this.optionalDependencies = loadDependencyMap(content, "optionalDependencies");
this.name = content.get("name").asText();
this.version = content.get("version").asText();
this.license = loadLicense(content);
Expand All @@ -63,12 +69,29 @@ private JsonNode loadManifest(Path manifestPath) throws IOException {

private Set<String> loadDependencies(JsonNode content) {
var names = new HashSet<String>();
if (content != null && content.has("dependencies")) {
content.get("dependencies").fieldNames().forEachRemaining(names::add);
if (content != null) {
addFieldNames(names, content, "dependencies");
addFieldNames(names, content, "optionalDependencies");
addFieldNames(names, content, "peerDependencies");
}
return Collections.unmodifiableSet(names);
}

private void addFieldNames(Set<String> names, JsonNode content, String field) {
if (content.has(field)) {
content.get(field).fieldNames().forEachRemaining(names::add);
}
}

private Map<String, String> loadDependencyMap(JsonNode content, String field) {
if (content == null || !content.has(field)) {
return Collections.emptyMap();
}
var map = new HashMap<String, String>();
content.get(field).fields().forEachRemaining(e -> map.put(e.getKey(), e.getValue().asText()));
return Collections.unmodifiableMap(map);
}

private String loadLicense(JsonNode content) {
if (content == null) {
return null;
Expand Down
35 changes: 24 additions & 11 deletions src/main/java/io/github/guacsec/trustifyda/sbom/CycloneDXSbom.java
Original file line number Diff line number Diff line change
Expand Up @@ -337,18 +337,31 @@ public boolean checkIfPackageInsideDependsOnList(PackageURL component, String na
List<Dependency> deps = targetComponent.getDependencies();
List<PackageURL> allDirectDeps = Collections.emptyList();
if (deps != null) {
deps.stream()
.map(
dep -> {
try {
return new PackageURL(dep.getRef());
} catch (MalformedPackageURLException e) {
throw new RuntimeException(e);
}
})
.collect(Collectors.toList());
allDirectDeps =
deps.stream()
.map(
dep -> {
try {
return new PackageURL(dep.getRef());
} catch (MalformedPackageURLException e) {
throw new RuntimeException(e);
}
})
.collect(Collectors.toList());
}
result = allDirectDeps.stream().anyMatch(dep -> dep.getName().equals(name));
result =
allDirectDeps.stream()
.anyMatch(
dep -> {
if (dep.getName().equals(name)) {
return true;
}
var fullName =
dep.getNamespace() != null
? dep.getNamespace() + "/" + dep.getName()
: dep.getName();
return fullName.equals(name);
});
Comment thread
soul2zimate marked this conversation as resolved.
}
return result;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ class Javascript_Provider_Test extends ExhortTest {
// - package.json: the target manifest for testing
// - expected_sbom.json: the SBOM expected to be provided
static Stream<String> testFolders() {
return Stream.of("deps_with_ignore", "deps_with_no_ignore");
return Stream.of("deps_with_ignore", "deps_with_no_ignore", "deps_with_mixed_dep_types");
}

static Stream<String> providers() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* Copyright 2023-2025 Trustify Dependency Analytics Authors
*
* 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.
*/
package io.github.guacsec.trustifyda.providers;

import static org.assertj.core.api.Assertions.assertThat;

import io.github.guacsec.trustifyda.ExhortTest;
import io.github.guacsec.trustifyda.providers.javascript.model.Manifest;
import java.io.IOException;
import java.util.Map;
import org.junit.jupiter.api.Test;

class ManifestTest extends ExhortTest {

@Test
void loads_manifest_with_mixed_dependency_types() throws IOException {
var manifestPath = resolveFile("tst_manifests/npm/deps_with_mixed_dep_types/package.json");
var m = new Manifest(manifestPath);

assertThat(m.name).isEqualTo("mixed-deps-test");
assertThat(m.version).isEqualTo("1.0.0");

// dependencies should include deps, peerDeps, and optionalDeps but NOT devDeps
assertThat(m.dependencies).containsExactlyInAnyOrder("express", "axios", "minimist", "lodash");
assertThat(m.dependencies).doesNotContain("jest", "eslint");

// peerDependencies and optionalDependencies maps
assertThat(m.peerDependencies).isEqualTo(Map.of("minimist", "1.2.0"));
assertThat(m.optionalDependencies).isEqualTo(Map.of("lodash", "4.17.19"));

assertThat(m.ignored).isEmpty();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,26 +12,10 @@
"purl": "pkg:golang/github.com/devfile-samples/devfile-sample-go-basic@v0.0.0"
}
},
"components": [
{
"type": "library",
"bom-ref": "pkg:golang/github.com/miekg/dns@v1.1.12",
"group": "github.com/miekg",
"name": "dns",
"version": "v1.1.12",
"purl": "pkg:golang/github.com/miekg/dns@v1.1.12"
}
],
"dependencies": [
{
"ref": "pkg:golang/github.com/devfile-samples/devfile-sample-go-basic@v0.0.0",
"dependsOn": [
"pkg:golang/github.com/miekg/dns@v1.1.12"
]
},
{
"ref": "pkg:golang/github.com/miekg/dns@v1.1.12",
"dependsOn": []
"dependsOn": [ ]
}
]
}
Loading
Loading