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 @@ -18,6 +18,9 @@

import static io.github.guacsec.trustifyda.impl.ExhortApi.debugLoggingIsNeeded;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.packageurl.MalformedPackageURLException;
import com.github.packageurl.PackageURL;
import io.github.guacsec.trustifyda.Api;
Expand All @@ -40,9 +43,11 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.logging.Logger;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
Expand Down Expand Up @@ -136,16 +141,17 @@ Sbom getDependenciesSbom(Path manifestPath, boolean buildTree) throws IOExceptio
determineMainModuleVersion(manifestPath.getParent());
Sbom sbom;
List<PackageURL> ignoredDeps = getIgnoredDeps(manifestPath);
Set<String> directDepPaths = getDirectDependencyPaths(manifestPath);
boolean matchManifestVersions =
Environment.getBoolean(Provider.PROP_MATCH_MANIFEST_VERSIONS, false);
if (matchManifestVersions) {
String[] goModGraphLines = goModulesResult.split(Operations.GENERIC_LINE_SEPARATOR);
performManifestVersionsCheck(goModGraphLines, manifestPath);
}
if (!buildTree) {
sbom = buildSbomFromList(goModulesResult, ignoredDeps);
sbom = buildSbomFromList(goModulesResult, ignoredDeps, directDepPaths);
} else {
sbom = buildSbomFromGraph(goModulesResult, ignoredDeps, manifestPath);
sbom = buildSbomFromGraph(goModulesResult, ignoredDeps, manifestPath, directDepPaths);
}
return sbom;
}
Expand Down Expand Up @@ -264,7 +270,10 @@ public void determineMainModuleVersion(Path directory) {
}

private Sbom buildSbomFromGraph(
String goModulesResult, List<PackageURL> ignoredDeps, Path manifestPath) {
String goModulesResult,
List<PackageURL> ignoredDeps,
Path manifestPath,
Set<String> directDepPaths) {
// Each entry contains a key of the module, and the list represents the module direct
// dependencies , so
// pairing of the key with each of the dependencies in a list is basically an edge in the graph.
Expand Down Expand Up @@ -300,6 +309,17 @@ private Sbom buildSbomFromGraph(
PackageURL source = toPurl(key, "@");
value.stream()
.filter(dep -> !isGoToolchainEntry(dep))
.filter(
dep -> {
// When processing root-level edges, skip indirect dependencies.
// In Go 1.17+, go mod graph emits edges for all modules in go.mod,
// including those marked // indirect. Only keep edges whose child
// module path is in the direct dependency set.
if (getModulePath(key).equals(getModulePath(rootPackage))) {
return directDepPaths.contains(getModulePath(dep));
}
return true;
})
.forEach(
dep -> {
PackageURL targetPurl = toPurl(dep, "@");
Expand Down Expand Up @@ -422,7 +442,59 @@ private String buildGoModulesDependencies(Path manifestPath) {
return goModulesOutput;
}

private Sbom buildSbomFromList(String golangDeps, List<PackageURL> ignoredDeps) {
/**
* Runs {@code go mod edit -json} and returns the set of module paths that are direct dependencies
* (i.e., entries in the Require array where {@code Indirect} is false or absent). This
* distinguishes direct from indirect dependencies in Go 1.17+ modules, where {@code go mod tidy}
* records all transitively-imported modules in go.mod with an {@code // indirect} marker.
*/
private Set<String> getDirectDependencyPaths(Path manifestPath) {
String[] goModEditCmd = new String[] {goExecutable, "mod", "edit", "-json"};
String goModEditOutput = Operations.runProcessGetOutput(manifestPath.getParent(), goModEditCmd);
if (debugLoggingIsNeeded()) {
log.info(
String.format(
"Package Manager Go Mod Edit -json output : %s%s",
System.lineSeparator(), goModEditOutput));
}
Set<String> directDepPaths = new HashSet<>();
try {
ObjectMapper mapper = new ObjectMapper();
JsonNode root = mapper.readTree(goModEditOutput);
JsonNode requireArray = root.get("Require");
if (requireArray != null && requireArray.isArray()) {
for (JsonNode req : requireArray) {
JsonNode indirectNode = req.get("Indirect");
if (indirectNode == null || !indirectNode.asBoolean()) {
if (req.get("Path") != null) {
directDepPaths.add(req.get("Path").asText());
}
}
}
}
} catch (JsonProcessingException e) {
throw new IllegalStateException("Failed to parse go mod edit -json output", e);
}
if (debugLoggingIsNeeded()) {
log.info(String.format("Direct dependency paths from go mod edit -json: %s", directDepPaths));
}
return directDepPaths;
}

/**
* Extracts the module path from a go mod graph entry by stripping the version suffix. For
* example, {@code "github.com/foo/bar@v1.2.3"} returns {@code "github.com/foo/bar"}.
*/
private static String getModulePath(String graphEntry) {
int atIndex = graphEntry.indexOf("@");
if (atIndex == -1) {
return graphEntry;
}
return graphEntry.substring(0, atIndex);
}

private Sbom buildSbomFromList(
String golangDeps, List<PackageURL> ignoredDeps, Set<String> directDepPaths) {
String[] allModulesFlat = golangDeps.split(Operations.GENERIC_LINE_SEPARATOR);
String parentVertex = getParentVertex(allModulesFlat[0]);
PackageURL root = toPurl(parentVertex, "@");
Expand All @@ -433,6 +505,7 @@ private Sbom buildSbomFromList(String golangDeps, List<PackageURL> ignoredDeps)
sbom.addRoot(root, readLicenseFromManifest());
deps.stream()
.filter(dep -> !isGoToolchainEntry(dep))
.filter(dep -> directDepPaths.contains(getModulePath(dep)))
.forEach(
dep -> {
PackageURL targetPurl = toPurl(dep, "@");
Expand Down
34 changes: 32 additions & 2 deletions src/main/java/io/github/guacsec/trustifyda/sbom/CycloneDXSbom.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@

import static io.github.guacsec.trustifyda.impl.ExhortApi.debugLoggingIsNeeded;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.github.packageurl.MalformedPackageURLException;
import com.github.packageurl.PackageURL;
import io.github.guacsec.trustifyda.logging.LoggersFactory;
Expand Down Expand Up @@ -67,8 +70,8 @@ public CycloneDXSbom() {
Metadata metadata = new Metadata();
metadata.setTimestamp(new Date());
bom.setMetadata(metadata);
bom.setComponents(new ArrayList<>());
bom.setDependencies(new ArrayList<>());
bom.setComponents(new ArrayList<>());
belongingCriteriaBinaryAlgorithm = getBelongingConditionByName();
this.exhortIgnoreMethod = "insensitive";
}
Expand Down Expand Up @@ -230,6 +233,9 @@ private Sbom removeIgnoredDepsFromSbom(List<String> refsToIgnore) {
d.setDependencies(filteredDeps);
}
});
if (bom.getComponents().isEmpty()) {
bom.setDependencies(new ArrayList<>());
}
return this;
}

Expand Down Expand Up @@ -302,12 +308,36 @@ public Sbom addDependency(PackageURL sourceRef, PackageURL targetRef, String s)
public String getAsJsonString() {
try {
var jsonString = BomGeneratorFactory.createJson(VERSION, bom).toJsonString();
jsonString = ensureComponentsField(jsonString);
if (debugLoggingIsNeeded()) {
log.info("Generated Sbom Json:" + System.lineSeparator() + jsonString);
}
return jsonString;
} catch (GeneratorException e) {
throw new RuntimeException("Unable to genenerate JSON from SBOM", e);
throw new RuntimeException("Unable to generate JSON from SBOM", e);
}
}

private String ensureComponentsField(String json) throws GeneratorException {
try {
var mapper = new ObjectMapper();
var rootNode = (ObjectNode) mapper.readTree(json);
if (!rootNode.has("components")) {
var ordered = mapper.createObjectNode();
rootNode
.properties()
.forEach(
field -> {
ordered.set(field.getKey(), field.getValue());
if (field.getKey().equals("metadata")) {
ordered.set("components", mapper.createArrayNode());
}
});
return mapper.writerWithDefaultPrettyPrinter().writeValueAsString(ordered);
}
return json;
} catch (JsonProcessingException e) {
throw new GeneratorException("Unable to ensure components field in JSON", e);
}
}
Comment thread
ruromero marked this conversation as resolved.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3989,51 +3989,13 @@
{
"ref": "pkg:golang/github.com/rhecosystemappeng/saasi/deployer@v0.0.0",
"dependsOn": [
"pkg:golang/github.com/davecgh/go-spew@v1.1.1",
"pkg:golang/github.com/emicklei/go-restful/v3@v3.9.0",
"pkg:golang/github.com/gin-gonic/gin@v1.9.1",
"pkg:golang/github.com/go-logr/logr@v1.2.3",
"pkg:golang/github.com/go-openapi/jsonpointer@v0.19.5",
"pkg:golang/github.com/go-openapi/jsonreference@v0.20.0",
"pkg:golang/github.com/go-openapi/swag@v0.19.14",
"pkg:golang/github.com/gogo/protobuf@v1.3.2",
"pkg:golang/github.com/golang/protobuf@v1.5.2",
"pkg:golang/github.com/google/gnostic@v0.5.7-v3refs",
"pkg:golang/github.com/google/go-cmp@v0.5.9",
"pkg:golang/github.com/google/gofuzz@v1.1.0",
"pkg:golang/github.com/google/uuid@v1.1.2",
"pkg:golang/github.com/imdario/mergo@v0.3.6",
"pkg:golang/github.com/jessevdk/go-flags@v1.5.0",
"pkg:golang/github.com/josharian/intern@v1.0.0",
"pkg:golang/github.com/json-iterator/go@v1.1.12",
"pkg:golang/github.com/kr/pretty@v0.3.1",
"pkg:golang/github.com/kr/text@v0.2.0",
"pkg:golang/github.com/mailru/easyjson@v0.7.6",
"pkg:golang/github.com/modern-go/concurrent@v0.0.0-20180306012644-bacd9c7ef1dd",
"pkg:golang/github.com/modern-go/reflect2@v1.0.2",
"pkg:golang/github.com/munnerz/goautoneg@v0.0.0-20191010083416-a7dc8b61c822",
"pkg:golang/github.com/rogpeppe/go-internal@v1.9.0",
"pkg:golang/github.com/spf13/pflag@v1.0.5",
"pkg:golang/golang.org/x/net@v0.10.0",
"pkg:golang/golang.org/x/oauth2@v0.0.0-20220223155221-ee480838109b",
"pkg:golang/golang.org/x/sys@v0.8.0",
"pkg:golang/golang.org/x/term@v0.8.0",
"pkg:golang/golang.org/x/text@v0.9.0",
"pkg:golang/golang.org/x/time@v0.0.0-20220210224613-90d013bbcef8",
"pkg:golang/google.golang.org/appengine@v1.6.7",
"pkg:golang/google.golang.org/protobuf@v1.30.0",
"pkg:golang/gopkg.in/inf.v0@v0.9.1",
"pkg:golang/gopkg.in/yaml.v2@v2.4.0",
"pkg:golang/gopkg.in/yaml.v3@v3.0.1",
"pkg:golang/k8s.io/api@v0.26.1",
"pkg:golang/k8s.io/apimachinery@v0.26.1",
"pkg:golang/k8s.io/client-go@v0.26.1",
"pkg:golang/k8s.io/klog/v2@v2.80.1",
"pkg:golang/k8s.io/kube-openapi@v0.0.0-20221012153701-172d655c2280",
"pkg:golang/k8s.io/utils@v0.0.0-20221107191617-1a15be271d1d",
"pkg:golang/sigs.k8s.io/json@v0.0.0-20220713155537-f223a00ba0e2",
"pkg:golang/sigs.k8s.io/structured-merge-diff/v4@v4.2.3",
"pkg:golang/sigs.k8s.io/yaml@v1.3.0"
"pkg:golang/k8s.io/client-go@v0.26.1"
]
},
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,23 +29,14 @@
"name" : "tools",
"version" : "v0.0.0-20210112183307-1e6ecd4bf1b0",
"purl" : "pkg:golang/golang.org/x/tools@v0.0.0-20210112183307-1e6ecd4bf1b0"
},
{
"type" : "library",
"bom-ref" : "pkg:golang/gopkg.in/yaml.v3@v3.0.1",
"group" : "gopkg.in",
"name" : "yaml.v3",
"version" : "v3.0.1",
"purl" : "pkg:golang/gopkg.in/yaml.v3@v3.0.1"
}
],
"dependencies" : [
{
"ref" : "pkg:golang/golang.org/x/example@v0.0.0",
"dependsOn" : [
"pkg:golang/github.com/spf13/cobra@v0.0.5",
"pkg:golang/golang.org/x/tools@v0.0.0-20210112183307-1e6ecd4bf1b0",
"pkg:golang/gopkg.in/yaml.v3@v3.0.1"
"pkg:golang/golang.org/x/tools@v0.0.0-20210112183307-1e6ecd4bf1b0"
]
},
{
Expand All @@ -55,10 +46,6 @@
{
"ref" : "pkg:golang/golang.org/x/tools@v0.0.0-20210112183307-1e6ecd4bf1b0",
"dependsOn" : [ ]
},
{
"ref" : "pkg:golang/gopkg.in/yaml.v3@v3.0.1",
"dependsOn" : [ ]
}
]
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -315,8 +315,7 @@
"ref": "pkg:golang/golang.org/x/example@v0.0.0",
"dependsOn": [
"pkg:golang/github.com/spf13/cobra@v0.0.5",
"pkg:golang/golang.org/x/tools@v0.0.0-20210112183307-1e6ecd4bf1b0",
"pkg:golang/gopkg.in/yaml.v3@v3.0.1"
"pkg:golang/golang.org/x/tools@v0.0.0-20210112183307-1e6ecd4bf1b0"
]
},
{
Expand Down
Loading
Loading