diff --git a/sca/bom/buildinfo/technologies/java/sbom_regression_test.go b/sca/bom/buildinfo/technologies/java/sbom_regression_test.go new file mode 100644 index 000000000..be8b8d7aa --- /dev/null +++ b/sca/bom/buildinfo/technologies/java/sbom_regression_test.go @@ -0,0 +1,42 @@ +package java + +import ( + "testing" + + "github.com/jfrog/jfrog-cli-security/utils/results" + "github.com/jfrog/jfrog-cli-security/utils/techutils" + "github.com/stretchr/testify/require" +) + +// Exercise the Maven plugin JSON -> module graphs -> SBOM pipeline without a server. +func TestMavenModuleExclusionSbom(t *testing.T) { + excluded := `{"root":"example:a:1.0","nodes":{ + "example:a:1.0":{"children":["example:x:1.0"]}, + "example:x:1.0":{"children":[]}}}` + included := `{"root":"example:b:1.0","nodes":{ + "example:b:1.0":{"children":["example:x:1.0"]}, + "example:x:1.0":{"children":["example:y:1.0"]}, + "example:y:1.0":{"children":[]}}}` + for _, modules := range [][]string{{excluded, included}, {included, excluded}} { + trees, unique, _, _, err := getGraphAndPluginDepsFromDepTree(writeDepTreeModuleFiles(t, modules...)) + require.NoError(t, err) + components, dependencies := results.DepsTreeToSbom(trees...) + require.NotNil(t, components) + refs := []string{} + for _, component := range *components { + refs = append(refs, component.BOMRef) + } + expected := []string{} + for id := range unique { + expected = append(expected, techutils.XrayComponentIdToCdxComponentRef(id)) + } + require.Len(t, expected, 4) + require.ElementsMatch(t, expected, refs) + require.NotNil(t, dependencies) + edges := map[string][]string{} + for _, dep := range *dependencies { + edges[dep.Ref] = *dep.Dependencies + } + require.ElementsMatch(t, []string{techutils.XrayComponentIdToCdxComponentRef("gav://example:y:1.0")}, edges[techutils.XrayComponentIdToCdxComponentRef("gav://example:x:1.0")]) + } +} diff --git a/utils/results/common.go b/utils/results/common.go index 82e073112..5a15870dc 100644 --- a/utils/results/common.go +++ b/utils/results/common.go @@ -948,7 +948,9 @@ func SearchTargetResultsByRelativePath(relativeTarget string, resultsToCompare * } func DepsTreeToSbom(trees ...*xrayUtils.GraphNode) (components *[]cyclonedx.Component, dependencies *[]cyclonedx.Dependency) { - parsed := datastructures.MakeSet[string]() + // Distinct occurrences of the same component can have different children. + // Visit each node object; AppendComponents and AppendDependencies merge by ref. + parsed := datastructures.MakeSet[*xrayUtils.GraphNode]() bom := cyclonedx.NewBOM() bom.Components = &[]cyclonedx.Component{} bom.Dependencies = &[]cyclonedx.Dependency{} @@ -966,15 +968,15 @@ func DepsTreeToSbom(trees ...*xrayUtils.GraphNode) (components *[]cyclonedx.Comp return bom.Components, bom.Dependencies } -func getDataFromNode(node *xrayUtils.GraphNode, parsed *datastructures.Set[string]) (*[]cyclonedx.Component, *[]cyclonedx.Dependency) { +func getDataFromNode(node *xrayUtils.GraphNode, parsed *datastructures.Set[*xrayUtils.GraphNode]) (*[]cyclonedx.Component, *[]cyclonedx.Dependency) { bom := cyclonedx.NewBOM() bom.Components = &[]cyclonedx.Component{} bom.Dependencies = &[]cyclonedx.Dependency{} - if parsed.Exists(node.Id) { - // The node was already parsed, no need to parse it again + if parsed.Exists(node) { + // This exact node was already traversed (including shared subgraphs and cycles). return bom.Components, bom.Dependencies } - parsed.Add(node.Id) + parsed.Add(node) // Create a new component and add it to the sbom *bom.Components = append(*bom.Components, CreateScaComponentFromXrayCompId(node.Id)) if len(node.Nodes) > 0 { diff --git a/utils/results/sbom_occurrences_test.go b/utils/results/sbom_occurrences_test.go new file mode 100644 index 000000000..573754cdf --- /dev/null +++ b/utils/results/sbom_occurrences_test.go @@ -0,0 +1,41 @@ +package results + +import ( + "testing" + + "github.com/jfrog/jfrog-cli-security/utils/techutils" + xrayUtils "github.com/jfrog/jfrog-client-go/xray/services/utils" + "github.com/stretchr/testify/require" +) + +func TestDepsTreeToSbomDistinctOccurrences(t *testing.T) { + node := func(name string, children ...*xrayUtils.GraphNode) *xrayUtils.GraphNode { + return &xrayUtils.GraphNode{Id: "gav://example:" + name + ":1.0", Nodes: children} + } + // The same edge occurs twice, but only its later occurrence has a grandchild. + a := node("a", node("x", node("y"))) + b := node("b", node("x", node("y", node("z")))) + c := node("c", node("x", node("w"))) + for _, roots := range [][]*xrayUtils.GraphNode{{a, b, c}, {c, b, a}, {b, a, c}} { + components, dependencies := DepsTreeToSbom(roots...) + require.NotNil(t, components) + require.Len(t, *components, 7) + edges := map[string][]string{} + for _, dep := range *dependencies { + edges[dep.Ref] = *dep.Dependencies + } + ref := func(name string) string { return techutils.XrayComponentIdToCdxComponentRef(node(name).Id) } + require.ElementsMatch(t, []string{ref("y"), ref("w")}, edges[ref("x")]) + require.ElementsMatch(t, []string{ref("z")}, edges[ref("y")]) + require.Len(t, *dependencies, 5) + } +} + +func TestDepsTreeToSbomPointerCycle(t *testing.T) { + a := &xrayUtils.GraphNode{Id: "gav://example:a:1.0"} + b := &xrayUtils.GraphNode{Id: "gav://example:b:1.0", Nodes: []*xrayUtils.GraphNode{a}} + a.Nodes = []*xrayUtils.GraphNode{b} + components, dependencies := DepsTreeToSbom(a, b) + require.Len(t, *components, 2) + require.Len(t, *dependencies, 2) +}