mirror of
https://github.com/anchore/syft.git
synced 2025-11-17 08:23:15 +01:00
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:
parent
60bbd24031
commit
963ea594c8
@ -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 {
|
||||||
|
|||||||
35
cmd/syft/internal/options/compliance.go
Normal file
35
cmd/syft/internal/options/compliance.go
Normal 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
|
||||||
|
}
|
||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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)
|
||||||
|
}
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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)
|
||||||
|
}
|
||||||
|
|||||||
47
syft/cataloging/compliance.go
Normal file
47
syft/cataloging/compliance.go
Normal 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
|
||||||
|
}
|
||||||
@ -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)
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user