mirror of
https://github.com/anchore/syft.git
synced 2026-07-05 02:28:25 +02:00
* fix relationship rewrites for isolated nodes Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * cover dangling pointers Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> --------- Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
537 lines
13 KiB
Go
537 lines
13 KiB
Go
package relationship
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/anchore/syft/syft/artifact"
|
|
"github.com/anchore/syft/syft/file"
|
|
"github.com/anchore/syft/syft/pkg"
|
|
)
|
|
|
|
func Test_Index(t *testing.T) {
|
|
p1 := pkg.Package{
|
|
Name: "pkg-1",
|
|
}
|
|
p2 := pkg.Package{
|
|
Name: "pkg-2",
|
|
}
|
|
p3 := pkg.Package{
|
|
Name: "pkg-3",
|
|
}
|
|
c1 := file.Coordinates{
|
|
RealPath: "/coords/1",
|
|
}
|
|
c2 := file.Coordinates{
|
|
RealPath: "/coords/2",
|
|
}
|
|
|
|
for _, p := range []*pkg.Package{&p1, &p2, &p3} {
|
|
p.SetID()
|
|
}
|
|
|
|
r1 := artifact.Relationship{
|
|
From: p1,
|
|
To: p2,
|
|
Type: artifact.DependencyOfRelationship,
|
|
}
|
|
r2 := artifact.Relationship{
|
|
From: p1,
|
|
To: p3,
|
|
Type: artifact.DependencyOfRelationship,
|
|
}
|
|
r3 := artifact.Relationship{
|
|
From: p1,
|
|
To: c1,
|
|
Type: artifact.ContainsRelationship,
|
|
}
|
|
r4 := artifact.Relationship{
|
|
From: p2,
|
|
To: c2,
|
|
Type: artifact.ContainsRelationship,
|
|
}
|
|
r5 := artifact.Relationship{
|
|
From: p3,
|
|
To: c2,
|
|
Type: artifact.ContainsRelationship,
|
|
}
|
|
|
|
dup := artifact.Relationship{
|
|
From: p3,
|
|
To: c2,
|
|
Type: artifact.ContainsRelationship,
|
|
}
|
|
|
|
idx := NewIndex(r1, r2, r3, r4, r5, dup)
|
|
require.ElementsMatch(t, slice(r1, r2, r3, r4, r5), idx.All())
|
|
|
|
require.ElementsMatch(t, slice(r1, r4), idx.References(p2))
|
|
require.ElementsMatch(t, slice(r4), idx.References(p2, artifact.ContainsRelationship))
|
|
|
|
require.ElementsMatch(t, slice(r1), idx.To(p2))
|
|
require.ElementsMatch(t, []artifact.Relationship(nil), idx.To(p2, artifact.ContainsRelationship))
|
|
|
|
require.ElementsMatch(t, slice(r4), idx.From(p2))
|
|
require.ElementsMatch(t, slice(r4), idx.From(p2, artifact.ContainsRelationship))
|
|
}
|
|
|
|
func Test_sortOrder(t *testing.T) {
|
|
r1 := artifact.Relationship{
|
|
From: id("1"),
|
|
To: id("2"),
|
|
Type: "1",
|
|
}
|
|
r2 := artifact.Relationship{
|
|
From: id("2"),
|
|
To: id("3"),
|
|
Type: "1",
|
|
}
|
|
r3 := artifact.Relationship{
|
|
From: id("3"),
|
|
To: id("4"),
|
|
Type: "1",
|
|
}
|
|
r4 := artifact.Relationship{
|
|
From: id("1"),
|
|
To: id("2"),
|
|
Type: "2",
|
|
}
|
|
r5 := artifact.Relationship{
|
|
From: id("2"),
|
|
To: id("3"),
|
|
Type: "2",
|
|
}
|
|
dup := artifact.Relationship{
|
|
From: id("2"),
|
|
To: id("3"),
|
|
Type: "2",
|
|
}
|
|
r6 := artifact.Relationship{
|
|
From: id("2"),
|
|
To: id("3"),
|
|
Type: "3",
|
|
}
|
|
|
|
idx := NewIndex(r5, r2, r6, r4, r1, r3, dup)
|
|
require.EqualValues(t, slice(r1, r2, r3, r4, r5, r6), idx.All())
|
|
|
|
require.EqualValues(t, slice(r1, r4), idx.From(id("1")))
|
|
|
|
require.EqualValues(t, slice(r2, r5, r6), idx.To(id("3")))
|
|
|
|
rLast := artifact.Relationship{
|
|
From: id("0"),
|
|
To: id("3"),
|
|
Type: "9999",
|
|
}
|
|
|
|
rFirst := artifact.Relationship{
|
|
From: id("0"),
|
|
To: id("3"),
|
|
Type: "1",
|
|
}
|
|
|
|
rMid := artifact.Relationship{
|
|
From: id("0"),
|
|
To: id("1"),
|
|
Type: "2",
|
|
}
|
|
|
|
idx.Add(rLast, rFirst, rMid)
|
|
|
|
require.EqualValues(t, slice(rFirst, r1, r2, r3, rMid, r4, r5, r6, rLast), idx.All())
|
|
|
|
require.EqualValues(t, slice(rFirst, r2, r5, r6, rLast), idx.To(id("3")))
|
|
}
|
|
|
|
func Test_Coordinates(t *testing.T) {
|
|
p1 := pkg.Package{
|
|
Name: "pkg-1",
|
|
}
|
|
p2 := pkg.Package{
|
|
Name: "pkg-2",
|
|
}
|
|
p3 := pkg.Package{
|
|
Name: "pkg-3",
|
|
}
|
|
c1 := file.Coordinates{
|
|
RealPath: "/coords/1",
|
|
}
|
|
c2 := file.Coordinates{
|
|
RealPath: "/coords/2",
|
|
}
|
|
c3 := file.Coordinates{
|
|
RealPath: "/coords/3",
|
|
}
|
|
c4 := file.Coordinates{
|
|
RealPath: "/coords/4",
|
|
}
|
|
|
|
for _, p := range []*pkg.Package{&p1, &p2, &p3} {
|
|
p.SetID()
|
|
}
|
|
|
|
r1 := artifact.Relationship{
|
|
From: p1,
|
|
To: p2,
|
|
Type: artifact.DependencyOfRelationship,
|
|
}
|
|
r2 := artifact.Relationship{
|
|
From: p1,
|
|
To: p3,
|
|
Type: artifact.DependencyOfRelationship,
|
|
}
|
|
r3 := artifact.Relationship{
|
|
From: p1,
|
|
To: c1,
|
|
Type: artifact.ContainsRelationship,
|
|
}
|
|
r4 := artifact.Relationship{
|
|
From: p2,
|
|
To: c2,
|
|
Type: artifact.ContainsRelationship,
|
|
}
|
|
r5 := artifact.Relationship{
|
|
From: p3,
|
|
To: c1,
|
|
Type: artifact.ContainsRelationship,
|
|
}
|
|
r6 := artifact.Relationship{
|
|
From: p3,
|
|
To: c2,
|
|
Type: artifact.ContainsRelationship,
|
|
}
|
|
r7 := artifact.Relationship{
|
|
From: c1,
|
|
To: c3,
|
|
Type: artifact.ContainsRelationship,
|
|
}
|
|
r8 := artifact.Relationship{
|
|
From: c3,
|
|
To: c4,
|
|
Type: artifact.ContainsRelationship,
|
|
}
|
|
|
|
idx := NewIndex(r1, r2, r3, r4, r5, r6, r7, r8)
|
|
|
|
got := idx.Coordinates(p1)
|
|
require.ElementsMatch(t, slice(c1), got)
|
|
|
|
got = idx.Coordinates(p3)
|
|
require.ElementsMatch(t, slice(c1, c2), got)
|
|
}
|
|
|
|
type id string
|
|
|
|
func (i id) ID() artifact.ID {
|
|
return artifact.ID(i)
|
|
}
|
|
|
|
func slice[T any](values ...T) []T {
|
|
return values
|
|
}
|
|
|
|
func TestRemove(t *testing.T) {
|
|
p1 := pkg.Package{Name: "pkg-1"}
|
|
p2 := pkg.Package{Name: "pkg-2"}
|
|
p3 := pkg.Package{Name: "pkg-3"}
|
|
c1 := file.Coordinates{RealPath: "/coords/1"}
|
|
c2 := file.Coordinates{RealPath: "/coords/2"}
|
|
c3 := file.Coordinates{RealPath: "/coords/3"}
|
|
c4 := file.Coordinates{RealPath: "/coords/4"}
|
|
|
|
for _, p := range []*pkg.Package{&p1, &p2, &p3} {
|
|
p.SetID()
|
|
}
|
|
|
|
r1 := artifact.Relationship{
|
|
From: p1,
|
|
To: p2,
|
|
Type: artifact.DependencyOfRelationship,
|
|
}
|
|
r2 := artifact.Relationship{
|
|
From: p1,
|
|
To: p3,
|
|
Type: artifact.DependencyOfRelationship,
|
|
}
|
|
r3 := artifact.Relationship{
|
|
From: p1,
|
|
To: c1,
|
|
Type: artifact.ContainsRelationship,
|
|
}
|
|
r4 := artifact.Relationship{
|
|
From: p2,
|
|
To: c2,
|
|
Type: artifact.ContainsRelationship,
|
|
}
|
|
r5 := artifact.Relationship{
|
|
From: p3,
|
|
To: c1,
|
|
Type: artifact.ContainsRelationship,
|
|
}
|
|
r6 := artifact.Relationship{
|
|
From: p3,
|
|
To: c2,
|
|
Type: artifact.ContainsRelationship,
|
|
}
|
|
r7 := artifact.Relationship{
|
|
From: c1,
|
|
To: c3,
|
|
Type: artifact.ContainsRelationship,
|
|
}
|
|
r8 := artifact.Relationship{
|
|
From: c3,
|
|
To: c4,
|
|
Type: artifact.ContainsRelationship,
|
|
}
|
|
|
|
index := NewIndex(r1, r2, r3, r4, r5, r6, r7, r8)
|
|
|
|
assert.Equal(t, 8, len(index.All()))
|
|
|
|
// removal of p1 should remove r1, r2, and r3
|
|
index.Remove(p1.ID())
|
|
remaining := index.All()
|
|
|
|
assert.Equal(t, 5, len(remaining))
|
|
assert.NotContains(t, remaining, r1)
|
|
assert.NotContains(t, remaining, r2)
|
|
assert.NotContains(t, remaining, r3)
|
|
|
|
assert.Empty(t, index.From(p1))
|
|
assert.Empty(t, index.To(p1))
|
|
|
|
// removal of c1 should remove r5 and r7
|
|
index.Remove(c1.ID())
|
|
remaining = index.All()
|
|
|
|
// r8 remains since c3->c4 should still exist
|
|
assert.Equal(t, 3, len(remaining))
|
|
assert.NotContains(t, remaining, r5)
|
|
assert.NotContains(t, remaining, r7)
|
|
assert.Contains(t, remaining, r8)
|
|
|
|
assert.Empty(t, index.From(c1))
|
|
assert.Empty(t, index.To(c1))
|
|
|
|
// removal of c3 should remove r8
|
|
index.Remove(c3.ID())
|
|
remaining = index.All()
|
|
|
|
assert.Equal(t, 2, len(remaining))
|
|
assert.Contains(t, remaining, r4)
|
|
assert.Contains(t, remaining, r6)
|
|
|
|
assert.Empty(t, index.From(c3))
|
|
assert.Empty(t, index.To(c3))
|
|
}
|
|
|
|
func TestReplace(t *testing.T) {
|
|
p1 := pkg.Package{Name: "pkg-1"}
|
|
p2 := pkg.Package{Name: "pkg-2"}
|
|
p3 := pkg.Package{Name: "pkg-3"}
|
|
p4 := pkg.Package{Name: "pkg-4"}
|
|
|
|
for _, p := range []*pkg.Package{&p1, &p2, &p3, &p4} {
|
|
p.SetID()
|
|
}
|
|
|
|
r1 := artifact.Relationship{
|
|
From: p1,
|
|
To: p2,
|
|
Type: artifact.DependencyOfRelationship,
|
|
}
|
|
r2 := artifact.Relationship{
|
|
From: p3,
|
|
To: p1,
|
|
Type: artifact.DependencyOfRelationship,
|
|
}
|
|
r3 := artifact.Relationship{
|
|
From: p2,
|
|
To: p3,
|
|
Type: artifact.ContainsRelationship,
|
|
}
|
|
|
|
index := NewIndex(r1, r2, r3)
|
|
|
|
// replace p1 with p4 in the relationships
|
|
index.Replace(p1.ID(), &p4)
|
|
|
|
expectedRels := []artifact.Relationship{
|
|
{
|
|
From: p4, // replaced
|
|
To: p2,
|
|
Type: artifact.DependencyOfRelationship,
|
|
},
|
|
{
|
|
From: p3,
|
|
To: p4, // replaced
|
|
Type: artifact.DependencyOfRelationship,
|
|
},
|
|
{
|
|
From: p2,
|
|
To: p3,
|
|
Type: artifact.ContainsRelationship,
|
|
},
|
|
}
|
|
|
|
compareRelationships(t, expectedRels, index.All())
|
|
}
|
|
|
|
func TestReplace_NodeOnlyOnToSide(t *testing.T) {
|
|
// regression: a node that only appears on the To side of relationships (nothing depends on it, e.g. a go main
|
|
// module that other modules are a dependency-of) must keep its incoming edges when its ID changes (such as when a
|
|
// compliance rule stubs a missing version and recomputes the package ID). previously the To-side remap guard
|
|
// checked ogID itself instead of the surviving From endpoint, so all such edges were dropped.
|
|
main := pkg.Package{Name: "main-module"} // empty version, like a go main module
|
|
dep1 := pkg.Package{Name: "dep-1", Version: "1.0.0"}
|
|
dep2 := pkg.Package{Name: "dep-2", Version: "2.0.0"}
|
|
|
|
for _, p := range []*pkg.Package{&main, &dep1, &dep2} {
|
|
p.SetID()
|
|
}
|
|
|
|
// both deps are a dependency-of main; main has no outgoing edges
|
|
r1 := artifact.Relationship{From: dep1, To: main, Type: artifact.DependencyOfRelationship}
|
|
r2 := artifact.Relationship{From: dep2, To: main, Type: artifact.DependencyOfRelationship}
|
|
|
|
index := NewIndex(r1, r2)
|
|
|
|
// simulate the version stub: main gets a new ID
|
|
stubbedMain := main
|
|
stubbedMain.Version = "UNKNOWN"
|
|
stubbedMain.SetID()
|
|
require.NotEqual(t, main.ID(), stubbedMain.ID())
|
|
|
|
index.Replace(main.ID(), &stubbedMain)
|
|
|
|
expectedRels := []artifact.Relationship{
|
|
{From: dep1, To: stubbedMain, Type: artifact.DependencyOfRelationship},
|
|
{From: dep2, To: stubbedMain, Type: artifact.DependencyOfRelationship},
|
|
}
|
|
|
|
compareRelationships(t, expectedRels, index.All())
|
|
}
|
|
|
|
func compareRelationships(t testing.TB, expected, actual []artifact.Relationship) {
|
|
assert.Equal(t, len(expected), len(actual), "number of relationships should match")
|
|
for _, e := range expected {
|
|
found := false
|
|
for _, a := range actual {
|
|
if a.From.ID() == e.From.ID() && a.To.ID() == e.To.ID() && a.Type == e.Type {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
assert.True(t, found, "expected relationship not found: %+v", e)
|
|
}
|
|
}
|
|
|
|
func TestReplace_NoExistingRelations(t *testing.T) {
|
|
p1 := pkg.Package{Name: "pkg-1"}
|
|
p2 := pkg.Package{Name: "pkg-2"}
|
|
|
|
p1.SetID()
|
|
p2.SetID()
|
|
|
|
index := NewIndex()
|
|
|
|
index.Replace(p1.ID(), &p2)
|
|
|
|
allRels := index.All()
|
|
assert.Len(t, allRels, 0)
|
|
}
|
|
|
|
// Remove must scrub the removed node's edges from adjacent nodes too, otherwise query methods keep returning
|
|
// relationships that no longer exist. the original Remove only deleted the removed node's own maps, leaving
|
|
// dangling pointers in the other endpoints.
|
|
func TestRemove_NoStaleEdgesInAdjacentNodes(t *testing.T) {
|
|
p1 := pkg.Package{Name: "pkg-1", Version: "1"}
|
|
p2 := pkg.Package{Name: "pkg-2", Version: "2"}
|
|
p3 := pkg.Package{Name: "pkg-3", Version: "3"}
|
|
for _, p := range []*pkg.Package{&p1, &p2, &p3} {
|
|
p.SetID()
|
|
}
|
|
|
|
r12 := artifact.Relationship{From: p1, To: p2, Type: artifact.DependencyOfRelationship}
|
|
r32 := artifact.Relationship{From: p3, To: p2, Type: artifact.DependencyOfRelationship}
|
|
index := NewIndex(r12, r32)
|
|
|
|
index.Remove(p1.ID())
|
|
|
|
// p2 was on the To side of the removed edge; its incoming view must no longer include the stale p1 edge,
|
|
// but must still include the live p3 edge.
|
|
compareRelationships(t, []artifact.Relationship{r32}, index.To(p2, artifact.DependencyOfRelationship))
|
|
assert.False(t, index.Contains(r12), "Contains must not report a removed edge")
|
|
assert.Len(t, index.References(p2), 1, "p2 should only reference the surviving p3 edge")
|
|
assert.True(t, index.Contains(r32), "surviving edge should remain")
|
|
compareRelationships(t, []artifact.Relationship{r32}, index.All())
|
|
}
|
|
|
|
// Replace must leave no dangling references to the old ID in adjacent nodes' query views.
|
|
func TestReplace_NoStaleEdgesAfterReplace(t *testing.T) {
|
|
main := pkg.Package{Name: "main-module"} // empty version
|
|
dep := pkg.Package{Name: "dep", Version: "1.0.0"}
|
|
main.SetID()
|
|
dep.SetID()
|
|
|
|
oldEdge := artifact.Relationship{From: dep, To: main, Type: artifact.DependencyOfRelationship}
|
|
index := NewIndex(oldEdge)
|
|
|
|
stubbed := main
|
|
stubbed.Version = "UNKNOWN"
|
|
stubbed.SetID()
|
|
require.NotEqual(t, main.ID(), stubbed.ID())
|
|
|
|
index.Replace(main.ID(), &stubbed)
|
|
|
|
newEdge := artifact.Relationship{From: dep, To: stubbed, Type: artifact.DependencyOfRelationship}
|
|
// the dep's outgoing view must point at the stubbed node, not the old one
|
|
from := index.From(dep, artifact.DependencyOfRelationship)
|
|
require.Len(t, from, 1)
|
|
assert.Equal(t, stubbed.ID(), from[0].To.ID())
|
|
assert.True(t, index.Contains(newEdge))
|
|
assert.False(t, index.Contains(oldEdge), "stale edge to old ID must be gone")
|
|
assert.Empty(t, index.To(main), "old node should have no incoming edges")
|
|
compareRelationships(t, []artifact.Relationship{newEdge}, index.All())
|
|
}
|
|
|
|
// replacing a node with one that has the same ID must be a no-op, not silently wipe its edges.
|
|
func TestReplace_SameIDIsNoOp(t *testing.T) {
|
|
a := pkg.Package{Name: "a", Version: "1"}
|
|
b := pkg.Package{Name: "b", Version: "2"}
|
|
a.SetID()
|
|
b.SetID()
|
|
edge := artifact.Relationship{From: a, To: b, Type: artifact.DependencyOfRelationship}
|
|
index := NewIndex(edge)
|
|
|
|
// same identity object -> same ID
|
|
index.Replace(b.ID(), b)
|
|
|
|
compareRelationships(t, []artifact.Relationship{edge}, index.All())
|
|
assert.True(t, index.Contains(edge))
|
|
}
|
|
|
|
// a self-edge on the replaced node should be remapped to replacement->replacement, not dropped.
|
|
func TestReplace_SelfEdge(t *testing.T) {
|
|
n := pkg.Package{Name: "n"} // empty version
|
|
n.SetID()
|
|
index := NewIndex(artifact.Relationship{From: n, To: n, Type: artifact.DependencyOfRelationship})
|
|
|
|
sn := n
|
|
sn.Version = "UNKNOWN"
|
|
sn.SetID()
|
|
require.NotEqual(t, n.ID(), sn.ID())
|
|
|
|
index.Replace(n.ID(), &sn)
|
|
|
|
all := index.All()
|
|
require.Len(t, all, 1)
|
|
assert.Equal(t, sn.ID(), all[0].From.ID())
|
|
assert.Equal(t, sn.ID(), all[0].To.ID())
|
|
assert.Empty(t, index.From(n), "no edges should reference the old ID")
|
|
assert.Empty(t, index.To(n), "no edges should reference the old ID")
|
|
}
|