From 23daa1c698f007bb4201ebca3b2c88b4f459ff45 Mon Sep 17 00:00:00 2001 From: rafi Date: Wed, 20 May 2026 15:06:39 +0200 Subject: [PATCH 1/3] fix: handle multiple info-source parents in SBOM graph without panicking Co-Authored-By: Claude Sonnet 4.6 Signed-off-by: rafi --- normalize/sbom_graph.go | 10 +++++++--- normalize/sbom_graph_test.go | 37 ++++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/normalize/sbom_graph.go b/normalize/sbom_graph.go index 2f45edc1b..a2b02967c 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,14 @@ 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) + monitoring.Alert("info source has multiple parents in SBOM graph", fmt.Errorf("infoSourceID=%s parentArtifacts=%v", parentID, parentArtifact)) } - if parentArtifact[0] != g.ScopeID { - // this info source does not belong to the scoped artifact, discard path + if !slices.ContainsFunc(parentArtifact, func(artifact string) bool { + return artifact == 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) { From e9055302e08f4ab67782721dbaa24bdb6c6e3206 Mon Sep 17 00:00:00 2001 From: Tim Bastin <38261809+timbastin@users.noreply.github.com> Date: Wed, 20 May 2026 15:20:54 +0200 Subject: [PATCH 2/3] Update normalize/sbom_graph.go Signed-off-by: Tim Bastin <38261809+timbastin@users.noreply.github.com> --- normalize/sbom_graph.go | 1 - 1 file changed, 1 deletion(-) diff --git a/normalize/sbom_graph.go b/normalize/sbom_graph.go index a2b02967c..0a72d285d 100644 --- a/normalize/sbom_graph.go +++ b/normalize/sbom_graph.go @@ -931,7 +931,6 @@ func (g *SBOMGraph) FindAllComponentOnlyPathsToPURL(purl string, limit int) []Pa parentArtifact := reverseEdges[parentID] if len(parentArtifact) > 1 { 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) - monitoring.Alert("info source has multiple parents in SBOM graph", fmt.Errorf("infoSourceID=%s parentArtifacts=%v", parentID, parentArtifact)) } if !slices.ContainsFunc(parentArtifact, func(artifact string) bool { From 8a96751f768b639ba5d500c2aef5f434305967f6 Mon Sep 17 00:00:00 2001 From: Tim Bastin <38261809+timbastin@users.noreply.github.com> Date: Wed, 20 May 2026 15:21:09 +0200 Subject: [PATCH 3/3] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Signed-off-by: Tim Bastin <38261809+timbastin@users.noreply.github.com> --- normalize/sbom_graph.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/normalize/sbom_graph.go b/normalize/sbom_graph.go index 0a72d285d..042fb8d61 100644 --- a/normalize/sbom_graph.go +++ b/normalize/sbom_graph.go @@ -933,9 +933,7 @@ func (g *SBOMGraph) FindAllComponentOnlyPathsToPURL(purl string, limit int) []Pa 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 !slices.ContainsFunc(parentArtifact, func(artifact string) bool { - return artifact == g.ScopeID - }) { + if !slices.Contains(parentArtifact, g.ScopeID) { // parent artifact is not the one we scoped to, so skip this path continue }