diff --git a/normalize/sbom_graph.go b/normalize/sbom_graph.go index 2f45edc1b..042fb8d61 100644 --- a/normalize/sbom_graph.go +++ b/normalize/sbom_graph.go @@ -27,6 +27,7 @@ import ( cdx "github.com/CycloneDX/cyclonedx-go" "github.com/google/uuid" + "github.com/l3montree-dev/devguard/monitoring" "github.com/package-url/packageurl-go" ) @@ -929,11 +930,11 @@ func (g *SBOMGraph) FindAllComponentOnlyPathsToPURL(purl string, limit int) []Pa // if not, just discard this path, as it does not belong to the scoped artifact parentArtifact := reverseEdges[parentID] if len(parentArtifact) > 1 { - panic("more than one parent, makes no sense") + slog.Warn("Info source has multiple parents, which should not happen in a well-formed graph. This may lead to incorrect path results.", "infoSourceID", parentID, "parentArtifacts", parentArtifact) } - if parentArtifact[0] != g.ScopeID { - // this info source does not belong to the scoped artifact, discard path + if !slices.Contains(parentArtifact, g.ScopeID) { + // parent artifact is not the one we scoped to, so skip this path continue } } diff --git a/normalize/sbom_graph_test.go b/normalize/sbom_graph_test.go index 3a4b619e2..2e3e58957 100644 --- a/normalize/sbom_graph_test.go +++ b/normalize/sbom_graph_test.go @@ -1445,6 +1445,43 @@ func TestFindAllComponentOnlyPathsToPURL_ScopeIsolation(t *testing.T) { assert.NotEqual(t, artifact1ID, artifact2ID) assert.NotEqual(t, infoSource1, infoSource2) }) + + t.Run("info source with two parent artifacts still returns correct scoped paths", func(t *testing.T) { + // Simulate a malformed (but real-world) graph where the same info source + // node is reachable from two different artifact nodes. The warning branch + // (len(parentArtifact) > 1) must still produce correct results: scoping + // to either artifact must find the path, not drop it. + g := NewSBOMGraph() + + artifact1ID := g.AddArtifact("app1") + artifact2ID := g.AddArtifact("app2") + + // Create an info source under artifact1. + infoSourceID := g.AddInfoSource(artifact1ID, "shared/sbom.json", InfoSourceSBOM) + + // Manually wire artifact2 → the same info source, creating the + // "multiple parents" condition that triggers the warning. + g.AddEdge(artifact2ID, infoSourceID) + + lodash := cdx.Component{BOMRef: "pkg:npm/lodash@4.17.20", PackageURL: "pkg:npm/lodash@4.17.20"} + lodashID := g.AddComponent(lodash) + g.AddEdge(infoSourceID, lodashID) + + // Scoped to artifact1: path must still be returned despite the warning. + err := g.ScopeToArtifact("app1") + assert.NoError(t, err) + paths1 := g.FindAllComponentOnlyPathsToPURL("pkg:npm/lodash@4.17.20", 0) + assert.Len(t, paths1, 1, "scoped to app1 should return the path even with multiple info-source parents") + assert.Equal(t, Path([]string{"pkg:npm/lodash@4.17.20"}), paths1[0]) + + // Scoped to artifact2: same expectation. + g.ClearScope() + err = g.ScopeToArtifact("app2") + assert.NoError(t, err) + paths2 := g.FindAllComponentOnlyPathsToPURL("pkg:npm/lodash@4.17.20", 0) + assert.Len(t, paths2, 1, "scoped to app2 should return the path even with multiple info-source parents") + assert.Equal(t, Path([]string{"pkg:npm/lodash@4.17.20"}), paths2[0]) + }) } func TestVulnerabilities(t *testing.T) {