Skip to content

Commit 23daa1c

Browse files
refoo0claude
andcommitted
fix: handle multiple info-source parents in SBOM graph without panicking
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: rafi <refaei.shikho@hotmail.com>
1 parent 5bc7ae5 commit 23daa1c

2 files changed

Lines changed: 44 additions & 3 deletions

File tree

normalize/sbom_graph.go

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import (
2727

2828
cdx "github.com/CycloneDX/cyclonedx-go"
2929
"github.com/google/uuid"
30+
"github.com/l3montree-dev/devguard/monitoring"
3031
"github.com/package-url/packageurl-go"
3132
)
3233

@@ -929,11 +930,14 @@ func (g *SBOMGraph) FindAllComponentOnlyPathsToPURL(purl string, limit int) []Pa
929930
// if not, just discard this path, as it does not belong to the scoped artifact
930931
parentArtifact := reverseEdges[parentID]
931932
if len(parentArtifact) > 1 {
932-
panic("more than one parent, makes no sense")
933+
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)
934+
monitoring.Alert("info source has multiple parents in SBOM graph", fmt.Errorf("infoSourceID=%s parentArtifacts=%v", parentID, parentArtifact))
933935
}
934936

935-
if parentArtifact[0] != g.ScopeID {
936-
// this info source does not belong to the scoped artifact, discard path
937+
if !slices.ContainsFunc(parentArtifact, func(artifact string) bool {
938+
return artifact == g.ScopeID
939+
}) {
940+
// parent artifact is not the one we scoped to, so skip this path
937941
continue
938942
}
939943
}

normalize/sbom_graph_test.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1445,6 +1445,43 @@ func TestFindAllComponentOnlyPathsToPURL_ScopeIsolation(t *testing.T) {
14451445
assert.NotEqual(t, artifact1ID, artifact2ID)
14461446
assert.NotEqual(t, infoSource1, infoSource2)
14471447
})
1448+
1449+
t.Run("info source with two parent artifacts still returns correct scoped paths", func(t *testing.T) {
1450+
// Simulate a malformed (but real-world) graph where the same info source
1451+
// node is reachable from two different artifact nodes. The warning branch
1452+
// (len(parentArtifact) > 1) must still produce correct results: scoping
1453+
// to either artifact must find the path, not drop it.
1454+
g := NewSBOMGraph()
1455+
1456+
artifact1ID := g.AddArtifact("app1")
1457+
artifact2ID := g.AddArtifact("app2")
1458+
1459+
// Create an info source under artifact1.
1460+
infoSourceID := g.AddInfoSource(artifact1ID, "shared/sbom.json", InfoSourceSBOM)
1461+
1462+
// Manually wire artifact2 → the same info source, creating the
1463+
// "multiple parents" condition that triggers the warning.
1464+
g.AddEdge(artifact2ID, infoSourceID)
1465+
1466+
lodash := cdx.Component{BOMRef: "pkg:npm/lodash@4.17.20", PackageURL: "pkg:npm/lodash@4.17.20"}
1467+
lodashID := g.AddComponent(lodash)
1468+
g.AddEdge(infoSourceID, lodashID)
1469+
1470+
// Scoped to artifact1: path must still be returned despite the warning.
1471+
err := g.ScopeToArtifact("app1")
1472+
assert.NoError(t, err)
1473+
paths1 := g.FindAllComponentOnlyPathsToPURL("pkg:npm/lodash@4.17.20", 0)
1474+
assert.Len(t, paths1, 1, "scoped to app1 should return the path even with multiple info-source parents")
1475+
assert.Equal(t, Path([]string{"pkg:npm/lodash@4.17.20"}), paths1[0])
1476+
1477+
// Scoped to artifact2: same expectation.
1478+
g.ClearScope()
1479+
err = g.ScopeToArtifact("app2")
1480+
assert.NoError(t, err)
1481+
paths2 := g.FindAllComponentOnlyPathsToPURL("pkg:npm/lodash@4.17.20", 0)
1482+
assert.Len(t, paths2, 1, "scoped to app2 should return the path even with multiple info-source parents")
1483+
assert.Equal(t, Path([]string{"pkg:npm/lodash@4.17.20"}), paths2[0])
1484+
})
14481485
}
14491486

14501487
func TestVulnerabilities(t *testing.T) {

0 commit comments

Comments
 (0)