Add compliance policy for empty name and version (#3257)

* add policy for empty name and version

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* default stub version

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* modifying ids requires augmenting relationships

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

---------

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
This commit is contained in:
Alex Goodman 2024-09-20 12:50:47 -04:00 committed by GitHub
parent 60bbd24031
commit 963ea594c8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 547 additions and 47 deletions

View File

@ -37,6 +37,7 @@ type Catalog struct {
Scope string `yaml:"scope" json:"scope" mapstructure:"scope"` 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 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"` Relationships relationshipsConfig `yaml:"relationships" json:"relationships" mapstructure:"relationships"`
Compliance complianceConfig `yaml:"compliance" json:"compliance" mapstructure:"compliance"`
Enrich []string `yaml:"enrich" json:"enrich" mapstructure:"enrich"` Enrich []string `yaml:"enrich" json:"enrich" mapstructure:"enrich"`
// ecosystem-specific cataloger configuration // ecosystem-specific cataloger configuration
@ -62,6 +63,7 @@ var _ interface {
func DefaultCatalog() Catalog { func DefaultCatalog() Catalog {
return Catalog{ return Catalog{
Compliance: defaultComplianceConfig(),
Scope: source.SquashedScope.String(), Scope: source.SquashedScope.String(),
Package: defaultPackageConfig(), Package: defaultPackageConfig(),
LinuxKernel: defaultLinuxKernelConfig(), LinuxKernel: defaultLinuxKernelConfig(),
@ -79,6 +81,7 @@ func (cfg Catalog) ToSBOMConfig(id clio.Identification) *syft.CreateSBOMConfig {
WithTool(id.Name, id.Version). WithTool(id.Name, id.Version).
WithParallelism(cfg.Parallelism). WithParallelism(cfg.Parallelism).
WithRelationshipsConfig(cfg.ToRelationshipsConfig()). WithRelationshipsConfig(cfg.ToRelationshipsConfig()).
WithComplianceConfig(cfg.ToComplianceConfig()).
WithSearchConfig(cfg.ToSearchConfig()). WithSearchConfig(cfg.ToSearchConfig()).
WithPackagesConfig(cfg.ToPackagesConfig()). WithPackagesConfig(cfg.ToPackagesConfig()).
WithFilesConfig(cfg.ToFilesConfig()). 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 { func (cfg Catalog) ToFilesConfig() filecataloging.Config {
hashers, err := intFile.Hashers(cfg.File.Metadata.Digests...) hashers, err := intFile.Hashers(cfg.File.Metadata.Digests...)
if err != nil { if err != nil {

View File

@ -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
}

View File

@ -17,20 +17,16 @@ type Index struct {
// NewIndex returns a new relationship Index // NewIndex returns a new relationship Index
func NewIndex(relationships ...artifact.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...) out.Add(relationships...)
return &out return &out
} }
// Add adds all the given relationships to the index, without adding duplicates // Add adds all the given relationships to the index, without adding duplicates
func (i *Index) Add(relationships ...artifact.Relationship) { 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 // store appropriate indexes for stable ordering to minimize ID() calls
for _, r := range relationships { for _, r := range relationships {
// prevent duplicates // 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 // From returns all relationships from the given identifiable, with specified types
func (i *Index) From(identifiable artifact.Identifiable, types ...artifact.RelationshipType) []artifact.Relationship { func (i *Index) From(identifiable artifact.Identifiable, types ...artifact.RelationshipType) []artifact.Relationship {
return toSortedSlice(fromMapped(i.fromID, identifiable), types) 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 { func fromMapped(idMap map[artifact.ID]*mappedRelationships, identifiable artifact.Identifiable) []*sortableRelationship {
if identifiable == nil || idMap == nil { if identifiable == nil {
return nil return nil
} }
mapped := idMap[identifiable.ID()] return fromMappedByID(idMap, identifiable.ID())
}
func fromMappedByID(idMap map[artifact.ID]*mappedRelationships, id artifact.ID) []*sortableRelationship {
if idMap == nil {
return nil
}
mapped := idMap[id]
if mapped == nil { if mapped == nil {
return nil return nil
} }

View File

@ -3,6 +3,7 @@ package relationship
import ( import (
"testing" "testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/anchore/syft/syft/artifact" "github.com/anchore/syft/syft/artifact"
@ -231,3 +232,179 @@ func (i id) ID() artifact.ID {
func slice[T any](values ...T) []T { func slice[T any](values ...T) []T {
return values 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)
}

View File

@ -11,6 +11,7 @@ import (
"github.com/anchore/syft/internal/bus" "github.com/anchore/syft/internal/bus"
"github.com/anchore/syft/internal/log" "github.com/anchore/syft/internal/log"
"github.com/anchore/syft/internal/relationship"
"github.com/anchore/syft/internal/sbomsync" "github.com/anchore/syft/internal/sbomsync"
"github.com/anchore/syft/syft/artifact" "github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/cataloging" "github.com/anchore/syft/syft/cataloging"
@ -27,6 +28,7 @@ type packageTaskFactory func(cfg CatalogingFactoryConfig) Task
type PackageTaskFactories []packageTaskFactory type PackageTaskFactories []packageTaskFactory
type CatalogingFactoryConfig struct { type CatalogingFactoryConfig struct {
ComplianceConfig cataloging.ComplianceConfig
SearchConfig cataloging.SearchConfig SearchConfig cataloging.SearchConfig
RelationshipsConfig cataloging.RelationshipsConfig RelationshipsConfig cataloging.RelationshipsConfig
DataGenerationConfig cataloging.DataGenerationConfig DataGenerationConfig cataloging.DataGenerationConfig
@ -35,6 +37,7 @@ type CatalogingFactoryConfig struct {
func DefaultCatalogingFactoryConfig() CatalogingFactoryConfig { func DefaultCatalogingFactoryConfig() CatalogingFactoryConfig {
return CatalogingFactoryConfig{ return CatalogingFactoryConfig{
ComplianceConfig: cataloging.DefaultComplianceConfig(),
SearchConfig: cataloging.DefaultSearchConfig(), SearchConfig: cataloging.DefaultSearchConfig(),
RelationshipsConfig: cataloging.DefaultRelationshipsConfig(), RelationshipsConfig: cataloging.DefaultRelationshipsConfig(),
DataGenerationConfig: cataloging.DefaultDataGenerationConfig(), 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. // 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 { fn := func(ctx context.Context, resolver file.Resolver, sbom sbomsync.Builder) error {
catalogerName := c.Name() catalogerName := c.Name()
log.WithFields("name", catalogerName).Trace("starting package cataloger") log.WithFields("name", catalogerName).Trace("starting package cataloger")
@ -106,6 +109,25 @@ func NewPackageTask(cfg CatalogingFactoryConfig, c pkg.Cataloger, tags ...string
log.WithFields("cataloger", catalogerName).Debugf("discovered %d packages", len(pkgs)) log.WithFields("cataloger", catalogerName).Debugf("discovered %d packages", len(pkgs))
pkgs, relationships = finalizePkgCatalogerResults(cfg, resolver, catalogerName, pkgs, relationships)
pkgs, relationships = applyCompliance(cfg.ComplianceConfig, pkgs, relationships)
sbom.AddPackages(pkgs...)
sbom.AddRelationships(relationships...)
t.Add(int64(len(pkgs)))
t.SetCompleted()
log.WithFields("name", catalogerName).Trace("package cataloger completed")
return nil
}
tags = append(tags, pkgcataloging.PackageTag)
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 { for i, p := range pkgs {
if p.FoundBy == "" { if p.FoundBy == "" {
p.FoundBy = catalogerName p.FoundBy = catalogerName
@ -134,7 +156,7 @@ func NewPackageTask(cfg CatalogingFactoryConfig, c pkg.Cataloger, tags ...string
// create file-to-package relationships for files owned by the package // create file-to-package relationships for files owned by the package
owningRelationships, err := packageFileOwnershipRelationships(p, resolver) owningRelationships, err := packageFileOwnershipRelationships(p, resolver)
if err != nil { if err != nil {
log.Warnf("unable to create any package-file relationships for package name=%q type=%q: %w", p.Name, p.Type, err) log.Warnf("unable to create any package-file relationships for package name=%q type=%q: %v", p.Name, p.Type, err)
} else { } else {
relationships = append(relationships, owningRelationships...) relationships = append(relationships, owningRelationships...)
} }
@ -142,19 +164,97 @@ func NewPackageTask(cfg CatalogingFactoryConfig, c pkg.Cataloger, tags ...string
pkgs[i] = p pkgs[i] = p
} }
return pkgs, relationships
}
sbom.AddPackages(pkgs...) type packageReplacement struct {
sbom.AddRelationships(relationships...) original artifact.ID
t.Add(int64(len(pkgs))) pkg pkg.Package
}
t.SetCompleted() func applyCompliance(cfg cataloging.ComplianceConfig, pkgs []pkg.Package, relationships []artifact.Relationship) ([]pkg.Package, []artifact.Relationship) {
log.WithFields("name", catalogerName).Trace("package cataloger completed") remainingPkgs, droppedPkgs, replacements := filterNonCompliantPackages(pkgs, cfg)
return nil relIdx := relationship.NewIndex(relationships...)
for _, p := range droppedPkgs {
relIdx.Remove(p.ID())
} }
tags = append(tags, pkgcataloging.PackageTag)
return NewTask(c.Name(), fn, tags...) 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 { func hasAuthoritativeCPE(cpes []cpe.CPE) bool {

View File

@ -4,8 +4,13 @@ import (
"testing" "testing"
"github.com/stretchr/testify/assert" "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/cpe"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg"
) )
func Test_hasAuthoritativeCPE(t *testing.T) { 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)
}

View File

@ -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
}

View File

@ -19,6 +19,7 @@ import (
// CreateSBOMConfig specifies all parameters needed for creating an SBOM. // CreateSBOMConfig specifies all parameters needed for creating an SBOM.
type CreateSBOMConfig struct { type CreateSBOMConfig struct {
// required configuration input to specify how cataloging should be performed // required configuration input to specify how cataloging should be performed
Compliance cataloging.ComplianceConfig
Search cataloging.SearchConfig Search cataloging.SearchConfig
Relationships cataloging.RelationshipsConfig Relationships cataloging.RelationshipsConfig
DataGeneration cataloging.DataGenerationConfig DataGeneration cataloging.DataGenerationConfig
@ -38,6 +39,7 @@ type CreateSBOMConfig struct {
func DefaultCreateSBOMConfig() *CreateSBOMConfig { func DefaultCreateSBOMConfig() *CreateSBOMConfig {
return &CreateSBOMConfig{ return &CreateSBOMConfig{
Compliance: cataloging.DefaultComplianceConfig(),
Search: cataloging.DefaultSearchConfig(), Search: cataloging.DefaultSearchConfig(),
Relationships: cataloging.DefaultRelationshipsConfig(), Relationships: cataloging.DefaultRelationshipsConfig(),
DataGeneration: cataloging.DefaultDataGenerationConfig(), DataGeneration: cataloging.DefaultDataGenerationConfig(),
@ -93,6 +95,12 @@ func (c *CreateSBOMConfig) WithParallelism(p int) *CreateSBOMConfig {
return c 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. // WithSearchConfig allows for setting the specific search configuration for cataloging.
func (c *CreateSBOMConfig) WithSearchConfig(cfg cataloging.SearchConfig) *CreateSBOMConfig { func (c *CreateSBOMConfig) WithSearchConfig(cfg cataloging.SearchConfig) *CreateSBOMConfig {
c.Search = cfg c.Search = cfg
@ -225,6 +233,7 @@ func (c *CreateSBOMConfig) packageTasks(src source.Description) ([]task.Task, *t
RelationshipsConfig: c.Relationships, RelationshipsConfig: c.Relationships,
DataGenerationConfig: c.DataGeneration, DataGenerationConfig: c.DataGeneration,
PackagesConfig: c.Packages, PackagesConfig: c.Packages,
ComplianceConfig: c.Compliance,
} }
persistentTasks, selectableTasks, err := c.allPackageTasks(cfg) persistentTasks, selectableTasks, err := c.allPackageTasks(cfg)

View File

@ -3,11 +3,12 @@ package ocaml
import ( import (
"testing" "testing"
"github.com/stretchr/testify/assert"
"github.com/anchore/syft/syft/artifact" "github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file" "github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/pkg/cataloger/internal/pkgtest" "github.com/anchore/syft/syft/pkg/cataloger/internal/pkgtest"
"github.com/stretchr/testify/assert"
) )
func TestParseOpamPackage(t *testing.T) { func TestParseOpamPackage(t *testing.T) {