From 963ea594c8ae4e294d07148e9f17d1149fe43dfb Mon Sep 17 00:00:00 2001 From: Alex Goodman Date: Fri, 20 Sep 2024 12:50:47 -0400 Subject: [PATCH] Add compliance policy for empty name and version (#3257) * add policy for empty name and version Signed-off-by: Alex Goodman * default stub version Signed-off-by: Alex Goodman * modifying ids requires augmenting relationships Signed-off-by: Alex Goodman --------- Signed-off-by: Alex Goodman --- cmd/syft/internal/options/catalog.go | 10 ++ cmd/syft/internal/options/compliance.go | 35 ++++ internal/relationship/index.go | 56 +++++-- internal/relationship/index_test.go | 177 ++++++++++++++++++++ internal/task/package_task_factory.go | 172 +++++++++++++++---- internal/task/package_task_factory_test.go | 85 ++++++++++ syft/cataloging/compliance.go | 47 ++++++ syft/create_sbom_config.go | 9 + syft/pkg/cataloger/ocaml/parse_opam_test.go | 3 +- 9 files changed, 547 insertions(+), 47 deletions(-) create mode 100644 cmd/syft/internal/options/compliance.go create mode 100644 syft/cataloging/compliance.go diff --git a/cmd/syft/internal/options/catalog.go b/cmd/syft/internal/options/catalog.go index 8d18518605e..ec75d392f91 100644 --- a/cmd/syft/internal/options/catalog.go +++ b/cmd/syft/internal/options/catalog.go @@ -37,6 +37,7 @@ type Catalog struct { Scope string `yaml:"scope" json:"scope" mapstructure:"scope"` Parallelism int `yaml:"parallelism" json:"parallelism" mapstructure:"parallelism"` // the number of catalog workers to run in parallel Relationships relationshipsConfig `yaml:"relationships" json:"relationships" mapstructure:"relationships"` + Compliance complianceConfig `yaml:"compliance" json:"compliance" mapstructure:"compliance"` Enrich []string `yaml:"enrich" json:"enrich" mapstructure:"enrich"` // ecosystem-specific cataloger configuration @@ -62,6 +63,7 @@ var _ interface { func DefaultCatalog() Catalog { return Catalog{ + Compliance: defaultComplianceConfig(), Scope: source.SquashedScope.String(), Package: defaultPackageConfig(), LinuxKernel: defaultLinuxKernelConfig(), @@ -79,6 +81,7 @@ func (cfg Catalog) ToSBOMConfig(id clio.Identification) *syft.CreateSBOMConfig { WithTool(id.Name, id.Version). WithParallelism(cfg.Parallelism). WithRelationshipsConfig(cfg.ToRelationshipsConfig()). + WithComplianceConfig(cfg.ToComplianceConfig()). WithSearchConfig(cfg.ToSearchConfig()). WithPackagesConfig(cfg.ToPackagesConfig()). WithFilesConfig(cfg.ToFilesConfig()). @@ -104,6 +107,13 @@ func (cfg Catalog) ToRelationshipsConfig() cataloging.RelationshipsConfig { } } +func (cfg Catalog) ToComplianceConfig() cataloging.ComplianceConfig { + return cataloging.ComplianceConfig{ + MissingName: cfg.Compliance.MissingName, + MissingVersion: cfg.Compliance.MissingVersion, + } +} + func (cfg Catalog) ToFilesConfig() filecataloging.Config { hashers, err := intFile.Hashers(cfg.File.Metadata.Digests...) if err != nil { diff --git a/cmd/syft/internal/options/compliance.go b/cmd/syft/internal/options/compliance.go new file mode 100644 index 00000000000..a48b214410b --- /dev/null +++ b/cmd/syft/internal/options/compliance.go @@ -0,0 +1,35 @@ +package options + +import ( + "github.com/anchore/fangs" + "github.com/anchore/syft/syft/cataloging" +) + +var ( + _ fangs.FieldDescriber = (*complianceConfig)(nil) + _ fangs.PostLoader = (*complianceConfig)(nil) +) + +type complianceConfig struct { + MissingName cataloging.ComplianceAction `mapstructure:"missing-name" json:"missing-name" yaml:"missing-name"` + MissingVersion cataloging.ComplianceAction `mapstructure:"missing-version" json:"missing-version" yaml:"missing-version"` +} + +func defaultComplianceConfig() complianceConfig { + def := cataloging.DefaultComplianceConfig() + return complianceConfig{ + MissingName: def.MissingName, + MissingVersion: def.MissingVersion, + } +} + +func (r *complianceConfig) DescribeFields(descriptions fangs.FieldDescriptionSet) { + descriptions.Add(&r.MissingName, "action to take when a package is missing a name") + descriptions.Add(&r.MissingVersion, "action to take when a package is missing a version") +} + +func (r *complianceConfig) PostLoad() error { + r.MissingName = r.MissingName.Parse() + r.MissingVersion = r.MissingVersion.Parse() + return nil +} diff --git a/internal/relationship/index.go b/internal/relationship/index.go index a94c84f8537..ab78c62b4f6 100644 --- a/internal/relationship/index.go +++ b/internal/relationship/index.go @@ -17,20 +17,16 @@ type Index struct { // NewIndex returns a new relationship Index func NewIndex(relationships ...artifact.Relationship) *Index { - out := Index{} + out := Index{ + fromID: make(map[artifact.ID]*mappedRelationships), + toID: make(map[artifact.ID]*mappedRelationships), + } out.Add(relationships...) return &out } // Add adds all the given relationships to the index, without adding duplicates func (i *Index) Add(relationships ...artifact.Relationship) { - if i.fromID == nil { - i.fromID = map[artifact.ID]*mappedRelationships{} - } - if i.toID == nil { - i.toID = map[artifact.ID]*mappedRelationships{} - } - // store appropriate indexes for stable ordering to minimize ID() calls for _, r := range relationships { // prevent duplicates @@ -68,6 +64,39 @@ func (i *Index) Add(relationships ...artifact.Relationship) { } } +func (i *Index) Remove(id artifact.ID) { + delete(i.fromID, id) + delete(i.toID, id) + + for idx := 0; idx < len(i.all); { + if i.all[idx].from == id || i.all[idx].to == id { + i.all = append(i.all[:idx], i.all[idx+1:]...) + } else { + idx++ + } + } +} + +func (i *Index) Replace(ogID artifact.ID, replacement artifact.Identifiable) { + for _, mapped := range fromMappedByID(i.fromID, ogID) { + i.Add(artifact.Relationship{ + From: replacement, + To: mapped.relationship.To, + Type: mapped.relationship.Type, + }) + } + + for _, mapped := range fromMappedByID(i.toID, ogID) { + i.Add(artifact.Relationship{ + From: mapped.relationship.From, + To: replacement, + Type: mapped.relationship.Type, + }) + } + + i.Remove(ogID) +} + // From returns all relationships from the given identifiable, with specified types func (i *Index) From(identifiable artifact.Identifiable, types ...artifact.RelationshipType) []artifact.Relationship { return toSortedSlice(fromMapped(i.fromID, identifiable), types) @@ -110,10 +139,17 @@ func (i *Index) All(types ...artifact.RelationshipType) []artifact.Relationship } func fromMapped(idMap map[artifact.ID]*mappedRelationships, identifiable artifact.Identifiable) []*sortableRelationship { - if identifiable == nil || idMap == nil { + if identifiable == nil { + return nil + } + return fromMappedByID(idMap, identifiable.ID()) +} + +func fromMappedByID(idMap map[artifact.ID]*mappedRelationships, id artifact.ID) []*sortableRelationship { + if idMap == nil { return nil } - mapped := idMap[identifiable.ID()] + mapped := idMap[id] if mapped == nil { return nil } diff --git a/internal/relationship/index_test.go b/internal/relationship/index_test.go index 1f4e66c27af..8f0d010f259 100644 --- a/internal/relationship/index_test.go +++ b/internal/relationship/index_test.go @@ -3,6 +3,7 @@ package relationship import ( "testing" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/anchore/syft/syft/artifact" @@ -231,3 +232,179 @@ func (i id) ID() artifact.ID { 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 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) +} diff --git a/internal/task/package_task_factory.go b/internal/task/package_task_factory.go index 8a31c0483a7..4a86aed1237 100644 --- a/internal/task/package_task_factory.go +++ b/internal/task/package_task_factory.go @@ -11,6 +11,7 @@ import ( "github.com/anchore/syft/internal/bus" "github.com/anchore/syft/internal/log" + "github.com/anchore/syft/internal/relationship" "github.com/anchore/syft/internal/sbomsync" "github.com/anchore/syft/syft/artifact" "github.com/anchore/syft/syft/cataloging" @@ -27,6 +28,7 @@ type packageTaskFactory func(cfg CatalogingFactoryConfig) Task type PackageTaskFactories []packageTaskFactory type CatalogingFactoryConfig struct { + ComplianceConfig cataloging.ComplianceConfig SearchConfig cataloging.SearchConfig RelationshipsConfig cataloging.RelationshipsConfig DataGenerationConfig cataloging.DataGenerationConfig @@ -35,6 +37,7 @@ type CatalogingFactoryConfig struct { func DefaultCatalogingFactoryConfig() CatalogingFactoryConfig { return CatalogingFactoryConfig{ + ComplianceConfig: cataloging.DefaultComplianceConfig(), SearchConfig: cataloging.DefaultSearchConfig(), RelationshipsConfig: cataloging.DefaultRelationshipsConfig(), DataGenerationConfig: cataloging.DefaultDataGenerationConfig(), @@ -82,7 +85,7 @@ func (f PackageTaskFactories) Tasks(cfg CatalogingFactoryConfig) ([]Task, error) } // NewPackageTask creates a Task function for a generic pkg.Cataloger, honoring the common configuration options. -func NewPackageTask(cfg CatalogingFactoryConfig, c pkg.Cataloger, tags ...string) Task { //nolint: funlen +func NewPackageTask(cfg CatalogingFactoryConfig, c pkg.Cataloger, tags ...string) Task { fn := func(ctx context.Context, resolver file.Resolver, sbom sbomsync.Builder) error { catalogerName := c.Name() log.WithFields("name", catalogerName).Trace("starting package cataloger") @@ -106,42 +109,9 @@ func NewPackageTask(cfg CatalogingFactoryConfig, c pkg.Cataloger, tags ...string log.WithFields("cataloger", catalogerName).Debugf("discovered %d packages", len(pkgs)) - for i, p := range pkgs { - if p.FoundBy == "" { - p.FoundBy = catalogerName - } - - if cfg.DataGenerationConfig.GenerateCPEs && !hasAuthoritativeCPE(p.CPEs) { - // generate CPEs (note: this is excluded from package ID, so is safe to mutate) - // we might have binary classified CPE already with the package so we want to append here - dictionaryCPEs, ok := cpeutils.DictionaryFind(p) - if ok { - log.Tracef("used CPE dictionary to find CPEs for %s package %q: %s", p.Type, p.Name, dictionaryCPEs) - p.CPEs = append(p.CPEs, dictionaryCPEs...) - } else { - p.CPEs = append(p.CPEs, cpeutils.Generate(p)...) - } - } - - // if we were not able to identify the language we have an opportunity - // to try and get this value from the PURL. Worst case we assert that - // we could not identify the language at either stage and set UnknownLanguage - if p.Language == "" { - p.Language = pkg.LanguageFromPURL(p.PURL) - } - - if cfg.RelationshipsConfig.PackageFileOwnership { - // create file-to-package relationships for files owned by the package - owningRelationships, err := packageFileOwnershipRelationships(p, resolver) - if err != nil { - log.Warnf("unable to create any package-file relationships for package name=%q type=%q: %w", p.Name, p.Type, err) - } else { - relationships = append(relationships, owningRelationships...) - } - } + pkgs, relationships = finalizePkgCatalogerResults(cfg, resolver, catalogerName, pkgs, relationships) - pkgs[i] = p - } + pkgs, relationships = applyCompliance(cfg.ComplianceConfig, pkgs, relationships) sbom.AddPackages(pkgs...) sbom.AddRelationships(relationships...) @@ -157,6 +127,136 @@ func NewPackageTask(cfg CatalogingFactoryConfig, c pkg.Cataloger, tags ...string return NewTask(c.Name(), fn, tags...) } +func finalizePkgCatalogerResults(cfg CatalogingFactoryConfig, resolver file.PathResolver, catalogerName string, pkgs []pkg.Package, relationships []artifact.Relationship) ([]pkg.Package, []artifact.Relationship) { + for i, p := range pkgs { + if p.FoundBy == "" { + p.FoundBy = catalogerName + } + + if cfg.DataGenerationConfig.GenerateCPEs && !hasAuthoritativeCPE(p.CPEs) { + // generate CPEs (note: this is excluded from package ID, so is safe to mutate) + // we might have binary classified CPE already with the package so we want to append here + dictionaryCPEs, ok := cpeutils.DictionaryFind(p) + if ok { + log.Tracef("used CPE dictionary to find CPEs for %s package %q: %s", p.Type, p.Name, dictionaryCPEs) + p.CPEs = append(p.CPEs, dictionaryCPEs...) + } else { + p.CPEs = append(p.CPEs, cpeutils.Generate(p)...) + } + } + + // if we were not able to identify the language we have an opportunity + // to try and get this value from the PURL. Worst case we assert that + // we could not identify the language at either stage and set UnknownLanguage + if p.Language == "" { + p.Language = pkg.LanguageFromPURL(p.PURL) + } + + if cfg.RelationshipsConfig.PackageFileOwnership { + // create file-to-package relationships for files owned by the package + owningRelationships, err := packageFileOwnershipRelationships(p, resolver) + if err != nil { + log.Warnf("unable to create any package-file relationships for package name=%q type=%q: %v", p.Name, p.Type, err) + } else { + relationships = append(relationships, owningRelationships...) + } + } + + pkgs[i] = p + } + return pkgs, relationships +} + +type packageReplacement struct { + original artifact.ID + pkg pkg.Package +} + +func applyCompliance(cfg cataloging.ComplianceConfig, pkgs []pkg.Package, relationships []artifact.Relationship) ([]pkg.Package, []artifact.Relationship) { + remainingPkgs, droppedPkgs, replacements := filterNonCompliantPackages(pkgs, cfg) + + relIdx := relationship.NewIndex(relationships...) + for _, p := range droppedPkgs { + relIdx.Remove(p.ID()) + } + + for _, replacement := range replacements { + relIdx.Replace(replacement.original, replacement.pkg) + } + + return remainingPkgs, relIdx.All() +} + +func filterNonCompliantPackages(pkgs []pkg.Package, cfg cataloging.ComplianceConfig) ([]pkg.Package, []pkg.Package, []packageReplacement) { + var remainingPkgs, droppedPkgs []pkg.Package + var replacements []packageReplacement + for _, p := range pkgs { + keep, replacement := applyComplianceRules(&p, cfg) + if keep { + remainingPkgs = append(remainingPkgs, p) + } else { + droppedPkgs = append(droppedPkgs, p) + } + if replacement != nil { + replacements = append(replacements, *replacement) + } + } + + return remainingPkgs, droppedPkgs, replacements +} + +func applyComplianceRules(p *pkg.Package, cfg cataloging.ComplianceConfig) (bool, *packageReplacement) { + var drop bool + var replacement *packageReplacement + + applyComplianceRule := func(value, fieldName string, action cataloging.ComplianceAction) bool { + if strings.TrimSpace(value) != "" { + return false + } + + loc := "unknown" + locs := p.Locations.ToSlice() + if len(locs) > 0 { + loc = locs[0].Path() + } + switch action { + case cataloging.ComplianceActionDrop: + log.WithFields("pkg", p.String(), "location", loc).Debugf("package with missing %s, dropping", fieldName) + drop = true + + case cataloging.ComplianceActionStub: + log.WithFields("pkg", p.String(), "location", loc).Debugf("package with missing %s, stubbing with default value", fieldName) + return true + + case cataloging.ComplianceActionKeep: + log.WithFields("pkg", p.String(), "location", loc).Tracef("package with missing %s, taking no action", fieldName) + } + return false + } + + ogID := p.ID() + + if applyComplianceRule(p.Name, "name", cfg.MissingName) { + p.Name = cataloging.UnknownStubValue + p.SetID() + } + + if applyComplianceRule(p.Version, "version", cfg.MissingVersion) { + p.Version = cataloging.UnknownStubValue + p.SetID() + } + + newID := p.ID() + if newID != ogID { + replacement = &packageReplacement{ + original: ogID, + pkg: *p, + } + } + + return !drop, replacement +} + func hasAuthoritativeCPE(cpes []cpe.CPE) bool { for _, c := range cpes { if c.Source != cpe.GeneratedSource { diff --git a/internal/task/package_task_factory_test.go b/internal/task/package_task_factory_test.go index 2876afb56c8..592da3c9471 100644 --- a/internal/task/package_task_factory_test.go +++ b/internal/task/package_task_factory_test.go @@ -4,8 +4,13 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/anchore/syft/syft/artifact" + "github.com/anchore/syft/syft/cataloging" "github.com/anchore/syft/syft/cpe" + "github.com/anchore/syft/syft/file" + "github.com/anchore/syft/syft/pkg" ) func Test_hasAuthoritativeCPE(t *testing.T) { @@ -53,3 +58,83 @@ func Test_hasAuthoritativeCPE(t *testing.T) { }) } } + +func TestApplyCompliance(t *testing.T) { + p1 := pkg.Package{Name: "pkg-1", Version: "1.0"} + p2 := pkg.Package{Name: "", Version: "1.0"} // missing name + p3 := pkg.Package{Name: "pkg-3", Version: ""} // missing version + p4 := pkg.Package{Name: "pkg-4", Version: ""} // missing version + c1 := file.Coordinates{RealPath: "/coords/1"} + c2 := file.Coordinates{RealPath: "/coords/2"} + + for _, p := range []*pkg.Package{&p1, &p2, &p3, &p4} { + p.SetID() + } + + r1 := artifact.Relationship{ + From: p1, + To: c1, + Type: artifact.ContainsRelationship, + } + r2 := artifact.Relationship{ + From: p2, + To: c2, + Type: artifact.ContainsRelationship, + } + + cfg := cataloging.ComplianceConfig{ + MissingName: cataloging.ComplianceActionDrop, + MissingVersion: cataloging.ComplianceActionStub, + } + + remainingPkgs, remainingRels := applyCompliance(cfg, []pkg.Package{p1, p2, p3, p4}, []artifact.Relationship{r1, r2}) + + // p2 should be dropped because it has a missing name, p3 and p4 should pass with a warning for the missing version + assert.Len(t, remainingPkgs, 3) // p1, p3, p4 should remain + assert.Len(t, remainingRels, 1) // only r1 should remain (relationship involving p1) +} + +func TestFilterNonCompliantPackages(t *testing.T) { + p1 := pkg.Package{Name: "pkg-1", Version: "1.0"} + p2 := pkg.Package{Name: "", Version: "1.0"} // missing name + p3 := pkg.Package{Name: "pkg-3", Version: ""} // missing version + + for _, p := range []*pkg.Package{&p1, &p2, &p3} { + p.SetID() + } + + cfg := cataloging.ComplianceConfig{ + MissingName: cataloging.ComplianceActionDrop, + MissingVersion: cataloging.ComplianceActionKeep, + } + + remainingPkgs, droppedPkgs, replacement := filterNonCompliantPackages([]pkg.Package{p1, p2, p3}, cfg) + require.Nil(t, replacement) + + // p2 should be dropped because it has a missing name + assert.Len(t, remainingPkgs, 2) + assert.Len(t, droppedPkgs, 1) + assert.Equal(t, p2, droppedPkgs[0]) +} + +func TestApplyComplianceRules_DropAndStub(t *testing.T) { + p := pkg.Package{Name: "", Version: ""} + p.SetID() + ogID := p.ID() + + cfg := cataloging.ComplianceConfig{ + MissingName: cataloging.ComplianceActionDrop, + MissingVersion: cataloging.ComplianceActionStub, + } + + isCompliant, replacement := applyComplianceRules(&p, cfg) + require.NotNil(t, replacement) + assert.Equal(t, packageReplacement{ + original: ogID, + pkg: p, + }, *replacement) + + // the package should be dropped due to missing name (drop action) and its version should be stubbed + assert.False(t, isCompliant) + assert.Equal(t, cataloging.UnknownStubValue, p.Version) +} diff --git a/syft/cataloging/compliance.go b/syft/cataloging/compliance.go new file mode 100644 index 00000000000..180c49d15d7 --- /dev/null +++ b/syft/cataloging/compliance.go @@ -0,0 +1,47 @@ +package cataloging + +import ( + "strings" +) + +const ( + ComplianceActionKeep ComplianceAction = "keep" + ComplianceActionDrop ComplianceAction = "drop" + ComplianceActionStub ComplianceAction = "stub" +) + +const UnknownStubValue = "UNKNOWN" + +type ComplianceAction string + +type ComplianceConfig struct { + MissingName ComplianceAction `yaml:"missing-name" json:"missing-name" mapstructure:"missing-name"` + MissingVersion ComplianceAction `yaml:"missing-version" json:"missing-version" mapstructure:"missing-version"` +} + +func DefaultComplianceConfig() ComplianceConfig { + // Note: name and version are required minimum SBOM elements by NTIA, thus should be the API default + return ComplianceConfig{ + MissingName: ComplianceActionDrop, + MissingVersion: ComplianceActionStub, + } +} + +func (c ComplianceConfig) Parse() ComplianceConfig { + return ComplianceConfig{ + MissingName: c.MissingName.Parse(), + MissingVersion: c.MissingVersion.Parse(), + } +} + +func (c ComplianceAction) Parse() ComplianceAction { + switch strings.ToLower(string(c)) { + case string(ComplianceActionKeep), "include": + return ComplianceActionKeep + case string(ComplianceActionDrop), "exclude": + return ComplianceActionDrop + case string(ComplianceActionStub), "replace": + return ComplianceActionStub + } + return ComplianceActionKeep +} diff --git a/syft/create_sbom_config.go b/syft/create_sbom_config.go index 1f7deb18d72..66c1bb3398b 100644 --- a/syft/create_sbom_config.go +++ b/syft/create_sbom_config.go @@ -19,6 +19,7 @@ import ( // CreateSBOMConfig specifies all parameters needed for creating an SBOM. type CreateSBOMConfig struct { // required configuration input to specify how cataloging should be performed + Compliance cataloging.ComplianceConfig Search cataloging.SearchConfig Relationships cataloging.RelationshipsConfig DataGeneration cataloging.DataGenerationConfig @@ -38,6 +39,7 @@ type CreateSBOMConfig struct { func DefaultCreateSBOMConfig() *CreateSBOMConfig { return &CreateSBOMConfig{ + Compliance: cataloging.DefaultComplianceConfig(), Search: cataloging.DefaultSearchConfig(), Relationships: cataloging.DefaultRelationshipsConfig(), DataGeneration: cataloging.DefaultDataGenerationConfig(), @@ -93,6 +95,12 @@ func (c *CreateSBOMConfig) WithParallelism(p int) *CreateSBOMConfig { return c } +// WithComplianceConfig allows for setting the specific compliance configuration for cataloging. +func (c *CreateSBOMConfig) WithComplianceConfig(cfg cataloging.ComplianceConfig) *CreateSBOMConfig { + c.Compliance = cfg + return c +} + // WithSearchConfig allows for setting the specific search configuration for cataloging. func (c *CreateSBOMConfig) WithSearchConfig(cfg cataloging.SearchConfig) *CreateSBOMConfig { c.Search = cfg @@ -225,6 +233,7 @@ func (c *CreateSBOMConfig) packageTasks(src source.Description) ([]task.Task, *t RelationshipsConfig: c.Relationships, DataGenerationConfig: c.DataGeneration, PackagesConfig: c.Packages, + ComplianceConfig: c.Compliance, } persistentTasks, selectableTasks, err := c.allPackageTasks(cfg) diff --git a/syft/pkg/cataloger/ocaml/parse_opam_test.go b/syft/pkg/cataloger/ocaml/parse_opam_test.go index f25563be4d8..63b5fc9d37b 100644 --- a/syft/pkg/cataloger/ocaml/parse_opam_test.go +++ b/syft/pkg/cataloger/ocaml/parse_opam_test.go @@ -3,11 +3,12 @@ package ocaml import ( "testing" + "github.com/stretchr/testify/assert" + "github.com/anchore/syft/syft/artifact" "github.com/anchore/syft/syft/file" "github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/pkg/cataloger/internal/pkgtest" - "github.com/stretchr/testify/assert" ) func TestParseOpamPackage(t *testing.T) {