Files
syft/syft/create_sbom_config_test.go
T
4b8a3db5f3 Add nested archive cataloging (#5260)
Signed-off-by: Kudryavcev Nikolay <kydry.nikolau@gmail.com>
Signed-off-by: Thomas Bechtold <thomasbechtold@jpberlin.de>
Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
Signed-off-by: Keith Zantow <kzantow@gmail.com>
Co-authored-by: Kudryavcev Nikolay <kydry.nikolau@gmail.com>
Co-authored-by: Thomas Bechtold <thomasbechtold@jpberlin.de>
Co-authored-by: Alex Goodman <wagoodman@users.noreply.github.com>
2026-10-29 17:33:07 -04:00

654 lines
19 KiB
Go

package syft
import (
"context"
"sort"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/scylladb/go-set/strset"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/anchore/syft/internal/task"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/cataloging"
"github.com/anchore/syft/syft/cataloging/filecataloging"
"github.com/anchore/syft/syft/cataloging/pkgcataloging"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/pkg/cataloger/java"
"github.com/anchore/syft/syft/source"
)
var _ pkg.Cataloger = (*dummyCataloger)(nil)
type dummyCataloger struct {
name string
}
func newDummyCataloger(name string) pkg.Cataloger {
return dummyCataloger{name: name}
}
func (d dummyCataloger) Name() string {
return d.name
}
func (d dummyCataloger) Catalog(_ context.Context, _ file.Resolver) ([]pkg.Package, []artifact.Relationship, error) {
return nil, nil, nil
}
func TestCreateSBOMConfig_makeTaskGroups(t *testing.T) {
pkgIntersect := func(intersect ...string) []string {
var sets []*strset.Set
for _, s := range intersect {
sets = append(sets, strset.New(pkgCatalogerNamesWithTagOrName(t, s)...))
}
intersectSet := strset.Intersection(sets...)
slice := intersectSet.List()
sort.Strings(slice)
return slice
}
addTo := func(slice []string, add ...string) []string {
slice = append(slice, add...)
sort.Strings(slice)
return slice
}
imgSrc := source.Description{
Metadata: source.ImageMetadata{},
}
dirSrc := source.Description{
Metadata: source.DirectoryMetadata{},
}
fileSrc := source.Description{
Metadata: source.FileMetadata{},
}
tests := []struct {
name string
src source.Description
cfg *CreateSBOMConfig
wantTaskNames [][]string
wantManifest *catalogerManifest
wantErr require.ErrorAssertionFunc
}{
{
name: "default catalogers for image source",
src: imgSrc,
cfg: DefaultCreateSBOMConfig(),
wantTaskNames: [][]string{
environmentCatalogerNames(),
pkgCatalogerNamesWithTagOrName(t, "image"),
fileCatalogerNames(),
relationshipCatalogerNames(),
unknownsTaskNames(),
osFeatureDetectionTaskNames(),
},
wantManifest: &catalogerManifest{
Requested: cataloging.SelectionRequest{
DefaultNamesOrTags: []string{"image", "file"},
},
Used: flatten(pkgCatalogerNamesWithTagOrName(t, "image"), fileCatalogerNames()),
},
wantErr: require.NoError,
},
{
name: "default catalogers for directory source",
src: dirSrc,
cfg: DefaultCreateSBOMConfig(),
wantTaskNames: [][]string{
environmentCatalogerNames(),
pkgCatalogerNamesWithTagOrName(t, "directory"),
fileCatalogerNames(),
relationshipCatalogerNames(),
unknownsTaskNames(),
osFeatureDetectionTaskNames(),
},
wantManifest: &catalogerManifest{
Requested: cataloging.SelectionRequest{
DefaultNamesOrTags: []string{"directory", "file"},
},
Used: flatten(pkgCatalogerNamesWithTagOrName(t, "directory"), fileCatalogerNames()),
},
wantErr: require.NoError,
},
{
// note, the file source acts like a directory scan
name: "default catalogers for file source",
src: fileSrc,
cfg: DefaultCreateSBOMConfig(),
wantTaskNames: [][]string{
environmentCatalogerNames(),
pkgCatalogerNamesWithTagOrName(t, "directory"),
fileCatalogerNames(),
relationshipCatalogerNames(),
unknownsTaskNames(),
osFeatureDetectionTaskNames(),
},
wantManifest: &catalogerManifest{
Requested: cataloging.SelectionRequest{
DefaultNamesOrTags: []string{"directory", "file"},
},
Used: flatten(pkgCatalogerNamesWithTagOrName(t, "directory"), fileCatalogerNames()),
},
wantErr: require.NoError,
},
{
name: "no file digest cataloger",
src: imgSrc,
cfg: DefaultCreateSBOMConfig().WithCatalogerSelection(cataloging.NewSelectionRequest().WithRemovals("digest")),
wantTaskNames: [][]string{
environmentCatalogerNames(),
pkgCatalogerNamesWithTagOrName(t, "image"),
fileCatalogerNames("file-metadata", "content", "binary-metadata"),
relationshipCatalogerNames(),
unknownsTaskNames(),
osFeatureDetectionTaskNames(),
},
wantManifest: &catalogerManifest{
Requested: cataloging.SelectionRequest{
DefaultNamesOrTags: []string{"image", "file"},
RemoveNamesOrTags: []string{"digest"},
},
Used: flatten(pkgCatalogerNamesWithTagOrName(t, "image"), fileCatalogerNames("file-metadata", "content", "binary-metadata")),
},
wantErr: require.NoError,
},
{
name: "select no file catalogers",
src: imgSrc,
cfg: DefaultCreateSBOMConfig().WithCatalogerSelection(cataloging.NewSelectionRequest().WithRemovals("file")),
wantTaskNames: [][]string{
environmentCatalogerNames(),
pkgCatalogerNamesWithTagOrName(t, "image"),
nil, // note: there is a file cataloging group, with no items in it
relationshipCatalogerNames(),
unknownsTaskNames(),
osFeatureDetectionTaskNames(),
},
wantManifest: &catalogerManifest{
Requested: cataloging.SelectionRequest{
DefaultNamesOrTags: []string{"image", "file"},
RemoveNamesOrTags: []string{"file"},
},
Used: pkgCatalogerNamesWithTagOrName(t, "image"),
},
wantErr: require.NoError,
},
{
name: "select all file catalogers",
src: imgSrc,
cfg: DefaultCreateSBOMConfig().WithFilesConfig(filecataloging.DefaultConfig().WithSelection(file.AllFilesSelection)),
wantTaskNames: [][]string{
environmentCatalogerNames(),
// note: there is a single group of catalogers for pkgs and files
append(
pkgCatalogerNamesWithTagOrName(t, "image"),
fileCatalogerNames()...,
),
relationshipCatalogerNames(),
unknownsTaskNames(),
osFeatureDetectionTaskNames(),
},
wantManifest: &catalogerManifest{
Requested: cataloging.SelectionRequest{
DefaultNamesOrTags: []string{"image", "file"},
},
Used: flatten(pkgCatalogerNamesWithTagOrName(t, "image"), fileCatalogerNames()),
},
wantErr: require.NoError,
},
{
name: "user-provided persistent cataloger is always run (image)",
src: imgSrc,
cfg: DefaultCreateSBOMConfig().WithCatalogers(
pkgcataloging.NewAlwaysEnabledCatalogerReference(newDummyCataloger("persistent")),
),
wantTaskNames: [][]string{
environmentCatalogerNames(),
addTo(pkgCatalogerNamesWithTagOrName(t, "image"), "persistent"),
fileCatalogerNames(),
relationshipCatalogerNames(),
unknownsTaskNames(),
osFeatureDetectionTaskNames(),
},
wantManifest: &catalogerManifest{
Requested: cataloging.SelectionRequest{
DefaultNamesOrTags: []string{"image", "file"},
},
Used: flatten(addTo(pkgCatalogerNamesWithTagOrName(t, "image"), "persistent"), fileCatalogerNames()),
},
wantErr: require.NoError,
},
{
name: "user-provided persistent cataloger is always run (directory)",
src: dirSrc,
cfg: DefaultCreateSBOMConfig().WithCatalogers(
pkgcataloging.NewAlwaysEnabledCatalogerReference(newDummyCataloger("persistent")),
),
wantTaskNames: [][]string{
environmentCatalogerNames(),
addTo(pkgCatalogerNamesWithTagOrName(t, "directory"), "persistent"),
fileCatalogerNames(),
relationshipCatalogerNames(),
unknownsTaskNames(),
osFeatureDetectionTaskNames(),
},
wantManifest: &catalogerManifest{
Requested: cataloging.SelectionRequest{
DefaultNamesOrTags: []string{"directory", "file"},
},
Used: flatten(addTo(pkgCatalogerNamesWithTagOrName(t, "directory"), "persistent"), fileCatalogerNames()),
},
wantErr: require.NoError,
},
{
name: "user-provided persistent cataloger is always run (user selection does not affect this)",
src: imgSrc,
cfg: DefaultCreateSBOMConfig().WithCatalogers(
pkgcataloging.NewAlwaysEnabledCatalogerReference(newDummyCataloger("persistent")),
).WithCatalogerSelection(cataloging.NewSelectionRequest().WithSubSelections("javascript")),
wantTaskNames: [][]string{
environmentCatalogerNames(),
addTo(pkgIntersect("image", "javascript"), "persistent"),
fileCatalogerNames(),
relationshipCatalogerNames(),
unknownsTaskNames(),
osFeatureDetectionTaskNames(),
},
wantManifest: &catalogerManifest{
Requested: cataloging.SelectionRequest{
DefaultNamesOrTags: []string{"image", "file"},
SubSelectTags: []string{"javascript"},
},
Used: flatten(addTo(pkgIntersect("image", "javascript"), "persistent"), fileCatalogerNames()),
},
wantErr: require.NoError,
},
{
name: "user-provided cataloger runs when selected",
src: imgSrc,
cfg: DefaultCreateSBOMConfig().WithCatalogers(
pkgcataloging.NewCatalogerReference(newDummyCataloger("user-provided"), []string{"image"}),
),
wantTaskNames: [][]string{
environmentCatalogerNames(),
addTo(pkgCatalogerNamesWithTagOrName(t, "image"), "user-provided"),
fileCatalogerNames(),
relationshipCatalogerNames(),
unknownsTaskNames(),
osFeatureDetectionTaskNames(),
},
wantManifest: &catalogerManifest{
Requested: cataloging.SelectionRequest{
DefaultNamesOrTags: []string{"image", "file"},
},
Used: flatten(addTo(pkgCatalogerNamesWithTagOrName(t, "image"), "user-provided"), fileCatalogerNames()),
},
wantErr: require.NoError,
},
{
name: "user-provided cataloger NOT run when NOT selected",
src: imgSrc,
cfg: DefaultCreateSBOMConfig().WithCatalogers(
pkgcataloging.NewCatalogerReference(newDummyCataloger("user-provided"), []string{"bogus-selector-will-never-be-used"}),
),
wantTaskNames: [][]string{
environmentCatalogerNames(),
pkgCatalogerNamesWithTagOrName(t, "image"),
fileCatalogerNames(),
relationshipCatalogerNames(),
unknownsTaskNames(),
osFeatureDetectionTaskNames(),
},
wantManifest: &catalogerManifest{
Requested: cataloging.SelectionRequest{
DefaultNamesOrTags: []string{"image", "file"},
},
Used: flatten(pkgCatalogerNamesWithTagOrName(t, "image"), fileCatalogerNames()),
},
wantErr: require.NoError,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.wantErr == nil {
tt.wantErr = require.NoError
}
// sanity check
require.NotEmpty(t, tt.wantTaskNames)
// test the subject
gotTasks, gotManifest, err := tt.cfg.makeTaskGroups(tt.src, nil)
tt.wantErr(t, err)
if err != nil {
return
}
gotNames := taskGroupNames(gotTasks)
if d := cmp.Diff(
tt.wantTaskNames,
gotNames,
// order within a group does not matter
cmpopts.SortSlices(func(a, b string) bool {
return a < b
}),
); d != "" {
t.Errorf("mismatched task group names (-want +got):\n%s", d)
}
if d := cmp.Diff(tt.wantManifest, gotManifest); d != "" {
t.Errorf("mismatched cataloger manifest (-want +got):\n%s", d)
}
})
}
}
func pkgCatalogerNamesWithTagOrName(t *testing.T, token string) []string {
var names []string
cfg := task.DefaultCatalogingFactoryConfig()
for _, factory := range task.DefaultPackageTaskFactories() {
cat := factory(cfg)
name := cat.Name()
if selector, ok := cat.(task.Selector); ok {
if selector.HasAllSelectors(token) {
names = append(names, name)
continue
}
}
if name == token {
names = append(names, name)
}
}
// these thresholds are arbitrary but should be large enough to catch any major changes
switch token {
case "image":
require.Greater(t, len(names), 18, "minimum cataloger sanity check failed token")
case "directory":
require.Greater(t, len(names), 25, "minimum cataloger sanity check failed token")
default:
require.Greater(t, len(names), 0, "minimum cataloger sanity check failed token")
}
sort.Strings(names)
return names
}
func fileCatalogerNames(tokens ...string) []string {
var names []string
cfg := task.DefaultCatalogingFactoryConfig()
topLoop:
for _, factory := range task.DefaultFileTaskFactories() {
cat := factory(cfg)
if cat == nil {
continue
}
name := cat.Name()
if len(tokens) == 0 {
names = append(names, name)
continue
}
for _, token := range tokens {
if selector, ok := cat.(task.Selector); ok {
if selector.HasAllSelectors(token) {
names = append(names, name)
continue topLoop
}
}
if name == token {
names = append(names, name)
}
}
}
sort.Strings(names)
return names
}
func flatten(lists ...[]string) []string {
var final []string
for _, lst := range lists {
final = append(final, lst...)
}
sort.Strings(final)
return final
}
func relationshipCatalogerNames() []string {
return []string{"relationships-cataloger"}
}
func unknownsTaskNames() []string {
return []string{"unknowns-labeler"}
}
func osFeatureDetectionTaskNames() []string {
return []string{"os-feature-detection"}
}
func environmentCatalogerNames() []string {
return []string{"environment-cataloger"}
}
func taskGroupNames(groups [][]task.Task) [][]string {
var names [][]string
for _, group := range groups {
var groupNames []string
for _, tsk := range group {
groupNames = append(groupNames, tsk.Name())
}
names = append(names, groupNames)
}
return names
}
func Test_replaceDefaultTagReferences(t *testing.T) {
tests := []struct {
name string
lst []string
want []string
}{
{
name: "no default tag",
lst: []string{"foo", "bar"},
want: []string{"foo", "bar"},
},
{
name: "replace default tag",
lst: []string{"foo", "default", "bar"},
want: []string{"foo", "replacement", "bar"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.want, replaceDefaultTagReferences([]string{"replacement"}, tt.lst))
})
}
}
func Test_findDefaultTag(t *testing.T) {
tests := []struct {
name string
src source.Description
want []string
wantErr require.ErrorAssertionFunc
}{
{
name: "image",
src: source.Description{
Metadata: source.ImageMetadata{},
},
want: []string{pkgcataloging.ImageTag, filecataloging.FileTag},
},
{
name: "directory",
src: source.Description{
Metadata: source.DirectoryMetadata{},
},
want: []string{pkgcataloging.DirectoryTag, filecataloging.FileTag},
},
{
name: "file",
src: source.Description{
Metadata: source.FileMetadata{},
},
want: []string{pkgcataloging.DirectoryTag, filecataloging.FileTag}, // not a mistake...
},
{
name: "unknown",
src: source.Description{
Metadata: struct{}{},
},
wantErr: require.Error,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.wantErr == nil {
tt.wantErr = require.NoError
}
got, err := findDefaultTags(tt.src)
tt.wantErr(t, err)
if err != nil {
return
}
assert.Equal(t, tt.want, got)
})
}
}
func TestCreateSBOMConfig_validate(t *testing.T) {
tests := []struct {
name string
cfg *CreateSBOMConfig
wantErr assert.ErrorAssertionFunc
}{
{
name: "incompatible ExcludeBinaryPackagesWithFileOwnershipOverlap selection",
cfg: DefaultCreateSBOMConfig().
WithRelationshipsConfig(
cataloging.DefaultRelationshipsConfig().
WithExcludeBinaryPackagesWithFileOwnershipOverlap(true).
WithPackageFileOwnershipOverlap(false),
),
wantErr: assert.Error,
},
{
name: "nested archives enabled with no memory or disk",
cfg: DefaultCreateSBOMConfig().WithArchiveConfig(
cataloging.DefaultArchiveSearchConfig().WithMaxDepth(1).WithMaxMemoryBytes(0).WithMaxDiskBytes(0),
),
wantErr: assert.Error,
},
{
name: "nested archives enabled with memory only",
cfg: DefaultCreateSBOMConfig().WithArchiveConfig(
cataloging.DefaultArchiveSearchConfig().WithMaxDepth(1).WithMaxDiskBytes(0),
),
},
{
name: "nested archives enabled with disk only",
cfg: DefaultCreateSBOMConfig().WithArchiveConfig(
cataloging.DefaultArchiveSearchConfig().WithMaxDepth(1).WithMaxMemoryBytes(0),
),
},
{
name: "nested archives disabled with no memory or disk",
cfg: DefaultCreateSBOMConfig().WithArchiveConfig(
cataloging.DefaultArchiveSearchConfig().WithMaxMemoryBytes(0).WithMaxDiskBytes(0),
),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.wantErr == nil {
tt.wantErr = assert.NoError
}
tt.wantErr(t, tt.cfg.validate())
})
}
}
func TestCreateSBOMConfig_archiveTaskGroup(t *testing.T) {
src := source.Description{Metadata: source.DirectoryMetadata{}}
archiveTaskIn := func(groups [][]task.Task) bool {
for _, group := range groups {
for _, tsk := range group {
if tsk.Name() == task.ArchiveCatalogerTaskName {
return true
}
}
}
return false
}
t.Run("no archive task by default", func(t *testing.T) {
// the feature is off by default, and off must mean no task at all rather than a registered
// task that does nothing: a registered task is reported as a selected cataloger
groups, manifest, err := DefaultCreateSBOMConfig().makeTaskGroups(src, nil)
require.NoError(t, err)
assert.False(t, archiveTaskIn(groups))
assert.NotContains(t, manifest.Used, task.ArchiveCatalogerTaskName)
})
t.Run("archive task is present at negative depth", func(t *testing.T) {
cfg := DefaultCreateSBOMConfig().
WithArchiveConfig(cataloging.DefaultArchiveSearchConfig().WithMaxDepth(-1))
groups, _, err := cfg.makeTaskGroups(src, nil)
require.NoError(t, err)
assert.True(t, archiveTaskIn(groups))
})
t.Run("archive task is present when enabled", func(t *testing.T) {
cfg := DefaultCreateSBOMConfig().
WithArchiveConfig(cataloging.DefaultArchiveSearchConfig().WithMaxDepth(2))
groups, manifest, err := cfg.makeTaskGroups(src, nil)
require.NoError(t, err)
assert.True(t, archiveTaskIn(groups))
assert.Contains(t, manifest.Used, task.ArchiveCatalogerTaskName, "the manifest must say archive contents were searched")
})
t.Run("enabling the feature does not mutate the caller's package config", func(t *testing.T) {
// the java side learns the archive task is running from the context, not from its config: a
// consumer who handed us a package config gets it back exactly as supplied.
supplied := java.DefaultArchiveCatalogerConfig().WithUseNetwork(true)
cfg := DefaultCreateSBOMConfig().
WithArchiveConfig(cataloging.DefaultArchiveSearchConfig().WithMaxDepth(2)).
WithPackagesConfig(pkgcataloging.Config{JavaArchive: supplied})
_, _, err := cfg.makeTaskGroups(src, nil)
require.NoError(t, err)
assert.Equal(t, supplied, cfg.Packages.JavaArchive,
"the caller's java config must come back byte-identical")
assert.True(t, cfg.Packages.JavaArchive.UseNetwork,
"a caller-supplied field must survive untouched")
assert.Zero(t, cfg.Packages.JavaArchive.MaxDepth,
"java's own squash-inlined depth must not be written to either")
})
}