feat: add package supplier flag (#4131)

---------

Signed-off-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com>
This commit is contained in:
Christopher Angelo Phillips 2025-08-12 14:49:41 -04:00 committed by GitHub
parent 89470ecdd3
commit 6b48bd4b5e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
29 changed files with 3453 additions and 73 deletions

View File

@ -220,6 +220,7 @@ func getSource(ctx context.Context, opts *options.Catalog, userInput string, sou
WithAlias(source.Alias{ WithAlias(source.Alias{
Name: opts.Source.Name, Name: opts.Source.Name,
Version: opts.Source.Version, Version: opts.Source.Version,
Supplier: opts.Source.Supplier,
}). }).
WithExcludeConfig(source.ExcludeConfig{ WithExcludeConfig(source.ExcludeConfig{
Paths: opts.Exclusions, Paths: opts.Exclusions,

View File

@ -259,6 +259,9 @@ func (cfg *Catalog) AddFlags(flags clio.FlagSet) {
flags.StringVarP(&cfg.Source.BasePath, "base-path", "", flags.StringVarP(&cfg.Source.BasePath, "base-path", "",
"base directory for scanning, no links will be followed above this directory, and all paths will be reported relative to this directory") "base directory for scanning, no links will be followed above this directory, and all paths will be reported relative to this directory")
flags.StringVarP(&cfg.Source.Supplier, "source-supplier", "",
"the organization that supplied the component, which often may be the manufacturer, distributor, or repackager")
} }
func (cfg *Catalog) DescribeFields(descriptions fangs.FieldDescriptionSet) { func (cfg *Catalog) DescribeFields(descriptions fangs.FieldDescriptionSet) {

View File

@ -16,6 +16,8 @@ import (
type sourceConfig struct { type sourceConfig struct {
Name string `json:"name" yaml:"name" mapstructure:"name"` Name string `json:"name" yaml:"name" mapstructure:"name"`
Version string `json:"version" yaml:"version" mapstructure:"version"` Version string `json:"version" yaml:"version" mapstructure:"version"`
Supplier string `json:"supplier" yaml:"supplier" mapstructure:"supplier"`
Source string `json:"source" yaml:"source" mapstructure:"source"`
BasePath string `yaml:"base-path" json:"base-path" mapstructure:"base-path"` // specify base path for all file paths BasePath string `yaml:"base-path" json:"base-path" mapstructure:"base-path"` // specify base path for all file paths
File fileSource `json:"file" yaml:"file" mapstructure:"file"` File fileSource `json:"file" yaml:"file" mapstructure:"file"`
Image imageSource `json:"image" yaml:"image" mapstructure:"image"` Image imageSource `json:"image" yaml:"image" mapstructure:"image"`

View File

@ -78,7 +78,10 @@ func TestEncodeDecodeEncodeCycleComparison(t *testing.T) {
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
for _, image := range images { for _, image := range images {
originalSBOM, _ := catalogFixtureImage(t, image, source.SquashedScope) originalSBOM, _ := catalogFixtureImage(t, image, source.SquashedScope)
// we need a way to inject supplier into this test
// supplier is not available as part of the SBOM Config API since the flag
// is used in conjunction with the SourceConfig which is injected into generateSBOM during scan
originalSBOM.Source.Supplier = "anchore"
f := encoders.GetByString(test.name) f := encoders.GetByString(test.name)
require.NotNil(t, f) require.NotNil(t, f)

View File

@ -3,5 +3,5 @@ package internal
const ( const (
// JSONSchemaVersion is the current schema version output by the JSON encoder // JSONSchemaVersion is the current schema version output by the JSON encoder
// This is roughly following the "SchemaVer" guidelines for versioning the JSON schema. Please see schema/json/README.md for details on how to increment. // This is roughly following the "SchemaVer" guidelines for versioning the JSON schema. Please see schema/json/README.md for details on how to increment.
JSONSchemaVersion = "16.0.36" JSONSchemaVersion = "16.0.37"
) )

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{ {
"$schema": "https://json-schema.org/draft/2020-12/schema", "$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "anchore.io/schema/syft/json/16.0.36/document", "$id": "anchore.io/schema/syft/json/16.0.37/document",
"$ref": "#/$defs/Document", "$ref": "#/$defs/Document",
"$defs": { "$defs": {
"AlpmDbEntry": { "AlpmDbEntry": {
@ -3047,6 +3047,9 @@
"version": { "version": {
"type": "string" "type": "string"
}, },
"supplier": {
"type": "string"
},
"type": { "type": {
"type": "string" "type": "string"
}, },

View File

@ -48,7 +48,7 @@ func ToFormatModel(s sbom.SBOM) *cyclonedx.BOM {
packages := s.Artifacts.Packages.Sorted() packages := s.Artifacts.Packages.Sorted()
components := make([]cyclonedx.Component, len(packages)) components := make([]cyclonedx.Component, len(packages))
for i, p := range packages { for i, p := range packages {
components[i] = helpers.EncodeComponent(p, locationSorter) components[i] = helpers.EncodeComponent(p, s.Source.Supplier, locationSorter)
} }
components = append(components, toOSComponent(s.Artifacts.LinuxDistribution)...) components = append(components, toOSComponent(s.Artifacts.LinuxDistribution)...)
@ -220,11 +220,22 @@ func toBomDescriptor(name, version string, srcMetadata source.Description) *cycl
}, },
}, },
}, },
Supplier: toBomSupplier(srcMetadata),
Properties: toBomProperties(srcMetadata), Properties: toBomProperties(srcMetadata),
Component: toBomDescriptorComponent(srcMetadata), Component: toBomDescriptorComponent(srcMetadata),
} }
} }
func toBomSupplier(srcMetadata source.Description) *cyclonedx.OrganizationalEntity {
if srcMetadata.Supplier != "" {
return &cyclonedx.OrganizationalEntity{
Name: srcMetadata.Supplier,
}
}
return nil
}
// used to indicate that a relationship listed under the syft artifact package can be represented as a cyclonedx dependency. // used to indicate that a relationship listed under the syft artifact package can be represented as a cyclonedx dependency.
// NOTE: CycloneDX provides the ability to describe components and their dependency on other components. // NOTE: CycloneDX provides the ability to describe components and their dependency on other components.
// The dependency graph is capable of representing both direct and transitive relationships. // The dependency graph is capable of representing both direct and transitive relationships.
@ -325,6 +336,7 @@ func toBomDescriptorComponent(srcMetadata source.Description) *cyclonedx.Compone
Type: cyclonedx.ComponentTypeContainer, Type: cyclonedx.ComponentTypeContainer,
Name: name, Name: name,
Version: version, Version: version,
Supplier: toBomSupplier(srcMetadata),
} }
case source.DirectoryMetadata: case source.DirectoryMetadata:
if name == "" { if name == "" {
@ -340,6 +352,7 @@ func toBomDescriptorComponent(srcMetadata source.Description) *cyclonedx.Compone
Type: cyclonedx.ComponentTypeFile, Type: cyclonedx.ComponentTypeFile,
Name: name, Name: name,
Version: version, Version: version,
Supplier: toBomSupplier(srcMetadata),
} }
case source.FileMetadata: case source.FileMetadata:
if name == "" { if name == "" {
@ -355,6 +368,7 @@ func toBomDescriptorComponent(srcMetadata source.Description) *cyclonedx.Compone
Type: cyclonedx.ComponentTypeFile, Type: cyclonedx.ComponentTypeFile,
Name: name, Name: name,
Version: version, Version: version,
Supplier: toBomSupplier(srcMetadata),
} }
} }

View File

@ -389,7 +389,69 @@ func Test_toBomDescriptor(t *testing.T) {
Name: "syft:image:labels:key1", Name: "syft:image:labels:key1",
Value: "value1", Value: "value1",
}, },
}}, },
},
},
{
name: "with optional supplier is on the root component and bom metadata",
args: args{
name: "test-image",
version: "1.0.0",
srcMetadata: source.Description{
Name: "test-image",
Version: "1.0.0",
Supplier: "optional-supplier",
Metadata: source.ImageMetadata{},
},
},
want: &cyclonedx.Metadata{
Timestamp: "",
Lifecycles: nil,
Tools: &cyclonedx.ToolsChoice{
Components: &[]cyclonedx.Component{
{
Type: cyclonedx.ComponentTypeApplication,
Author: "anchore",
Name: "test-image",
Version: "1.0.0",
},
},
},
Authors: nil,
Component: &cyclonedx.Component{
BOMRef: "",
MIMEType: "",
Type: "container",
Supplier: &cyclonedx.OrganizationalEntity{
Name: "optional-supplier",
},
Author: "",
Publisher: "",
Group: "",
Name: "test-image",
Version: "1.0.0",
Description: "",
Scope: "",
Hashes: nil,
Licenses: nil,
Copyright: "",
CPE: "",
PackageURL: "",
SWID: nil,
Modified: nil,
Pedigree: nil,
ExternalReferences: nil,
Properties: nil,
Components: nil,
Evidence: nil,
ReleaseNotes: nil,
},
Manufacture: nil,
Supplier: &cyclonedx.OrganizationalEntity{
Name: "optional-supplier",
},
Licenses: nil,
},
}, },
} }
for _, tt := range tests { for _, tt := range tests {

View File

@ -257,9 +257,7 @@ func toRootPackage(s source.Description) *spdx.Package {
PackageChecksums: checksums, PackageChecksums: checksums,
PackageExternalReferences: nil, PackageExternalReferences: nil,
PrimaryPackagePurpose: purpose, PrimaryPackagePurpose: purpose,
PackageSupplier: &spdx.Supplier{ PackageSupplier: toSPDXSupplier(s),
Supplier: helpers.NOASSERTION,
},
PackageCopyrightText: helpers.NOASSERTION, PackageCopyrightText: helpers.NOASSERTION,
PackageDownloadLocation: helpers.NOASSERTION, PackageDownloadLocation: helpers.NOASSERTION,
PackageLicenseConcluded: helpers.NOASSERTION, PackageLicenseConcluded: helpers.NOASSERTION,
@ -327,6 +325,23 @@ func toSPDXID(identifiable artifact.Identifiable) spdx.ElementID {
return spdx.ElementID(helpers.SanitizeElementID(id)) return spdx.ElementID(helpers.SanitizeElementID(id))
} }
func toSPDXSupplier(s source.Description) *spdx.Supplier {
supplier := helpers.NOASSERTION
if s.Supplier != "" {
supplier = s.Supplier
}
supplierType := ""
if supplier != helpers.NOASSERTION {
supplierType = helpers.SUPPLIERORG
}
return &spdx.Supplier{
Supplier: supplier,
SupplierType: supplierType,
}
}
// packages populates all Package Information from the package Collection (see https://spdx.github.io/spdx-spec/3-package-information/) // packages populates all Package Information from the package Collection (see https://spdx.github.io/spdx-spec/3-package-information/)
// //
//nolint:funlen //nolint:funlen
@ -391,7 +406,7 @@ func toPackages(rels *relationship.Index, catalog *pkg.Collection, sbom sbom.SBO
// 7.6: Package Originator: may have single result for either Person or Organization, // 7.6: Package Originator: may have single result for either Person or Organization,
// or NOASSERTION // or NOASSERTION
// Cardinality: optional, one // Cardinality: optional, one
PackageSupplier: toPackageSupplier(p), PackageSupplier: toPackageSupplier(p, sbom.Source.Supplier),
PackageOriginator: toPackageOriginator(p), PackageOriginator: toPackageOriginator(p),
@ -556,11 +571,18 @@ func toPackageOriginator(p pkg.Package) *spdx.Originator {
} }
} }
func toPackageSupplier(p pkg.Package) *spdx.Supplier { func toPackageSupplier(p pkg.Package, sbomSupplier string) *spdx.Supplier {
kind, supplier := helpers.Supplier(p) kind, supplier := helpers.Supplier(p)
if kind == "" || supplier == "" { if kind == "" || supplier == "" {
supplier := helpers.NOASSERTION
supplierType := ""
if sbomSupplier != "" {
supplier = sbomSupplier
supplierType = helpers.SUPPLIERORG
}
return &spdx.Supplier{ return &spdx.Supplier{
Supplier: helpers.NOASSERTION, Supplier: supplier,
SupplierType: supplierType,
} }
} }
return &spdx.Supplier{ return &spdx.Supplier{

View File

@ -38,6 +38,7 @@ func Test_toFormatModel(t *testing.T) {
Source: source.Description{ Source: source.Description{
Name: "alpine", Name: "alpine",
Version: "sha256:d34db33f", Version: "sha256:d34db33f",
Supplier: "Alpine Linux",
Metadata: source.ImageMetadata{ Metadata: source.ImageMetadata{
UserInput: "alpine:latest", UserInput: "alpine:latest",
ManifestDigest: "sha256:d34db33f", ManifestDigest: "sha256:d34db33f",
@ -61,7 +62,8 @@ func Test_toFormatModel(t *testing.T) {
PackageName: "pkg-1", PackageName: "pkg-1",
PackageVersion: "version-1", PackageVersion: "version-1",
PackageSupplier: &spdx.Supplier{ PackageSupplier: &spdx.Supplier{
Supplier: "NOASSERTION", Supplier: "Alpine Linux",
SupplierType: "Organization",
}, },
}, },
{ {
@ -78,7 +80,8 @@ func Test_toFormatModel(t *testing.T) {
}, },
}, },
PackageSupplier: &spdx.Supplier{ PackageSupplier: &spdx.Supplier{
Supplier: "NOASSERTION", Supplier: "Alpine Linux",
SupplierType: "Organization",
}, },
}, },
}, },

View File

@ -145,10 +145,21 @@ func containerSource(p *spdx.Package) source.Description {
c := p.PackageChecksums[0] c := p.PackageChecksums[0]
digest = fmt.Sprintf("%s:%s", fromChecksumAlgorithm(c.Algorithm), c.Value) digest = fmt.Sprintf("%s:%s", fromChecksumAlgorithm(c.Algorithm), c.Value)
} }
supplier := ""
if p.PackageSupplier != nil {
// we also don't want NOASSERTION transferred to the syft format
// NOASSERTION == ""
if p.PackageSupplier.Supplier != helpers.NOASSERTION && p.PackageSupplier.SupplierType == helpers.SUPPLIERORG {
supplier = p.PackageSupplier.Supplier
}
}
return source.Description{ return source.Description{
ID: id, ID: id,
Name: p.PackageName, Name: p.PackageName,
Version: p.PackageVersion, Version: p.PackageVersion,
Supplier: supplier,
Metadata: source.ImageMetadata{ Metadata: source.ImageMetadata{
UserInput: container, UserInput: container,
ID: id, ID: id,
@ -179,10 +190,16 @@ func fileSource(p *spdx.Package) source.Description {
metadata, version = fileSourceMetadata(p) metadata, version = fileSourceMetadata(p)
} }
supplier := ""
if p.PackageSupplier.Supplier != helpers.NOASSERTION {
supplier = p.PackageSupplier.Supplier
}
return source.Description{ return source.Description{
ID: string(p.PackageSPDXIdentifier), ID: string(p.PackageSPDXIdentifier),
Name: p.PackageName, Name: p.PackageName,
Version: version, Version: version,
Supplier: supplier,
Metadata: metadata, Metadata: metadata,
} }
} }

View File

@ -527,15 +527,17 @@ func Test_convertToAndFromFormat(t *testing.T) {
}, },
Name: "some-image", Name: "some-image",
Version: "some-tag", Version: "some-tag",
Supplier: "some-supplier",
}, },
packages: packages, packages: packages,
relationships: relationships, relationships: relationships,
}, },
{ {
name: ". directory source", name: ". directory source with supplier",
source: source.Description{ source: source.Description{
ID: "DocumentRoot-Directory-.", ID: "DocumentRoot-Directory-.",
Name: ".", Name: ".",
Supplier: "some-supplier",
Metadata: source.DirectoryMetadata{ Metadata: source.DirectoryMetadata{
Path: ".", Path: ".",
}, },
@ -544,7 +546,7 @@ func Test_convertToAndFromFormat(t *testing.T) {
relationships: relationships, relationships: relationships,
}, },
{ {
name: "directory source", name: "directory source without supplier",
source: source.Description{ source: source.Description{
ID: "DocumentRoot-Directory-my-app", ID: "DocumentRoot-Directory-my-app",
Name: "my-app", Name: "my-app",

View File

@ -14,7 +14,7 @@ import (
"github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/pkg"
) )
func EncodeComponent(p pkg.Package, locationSorter func(a, b file.Location) int) cyclonedx.Component { func EncodeComponent(p pkg.Package, supplier string, locationSorter func(a, b file.Location) int) cyclonedx.Component {
props := EncodeProperties(p, "syft:package") props := EncodeProperties(p, "syft:package")
if p.Metadata != nil { if p.Metadata != nil {
@ -49,6 +49,7 @@ func EncodeComponent(p pkg.Package, locationSorter func(a, b file.Location) int)
Name: p.Name, Name: p.Name,
Group: encodeGroup(p), Group: encodeGroup(p),
Version: p.Version, Version: p.Version,
Supplier: encodeSupplier(p, supplier),
PackageURL: p.PURL, PackageURL: p.PURL,
Licenses: encodeLicenses(p), Licenses: encodeLicenses(p),
CPE: encodeSingleCPE(p), CPE: encodeSingleCPE(p),
@ -61,6 +62,16 @@ func EncodeComponent(p pkg.Package, locationSorter func(a, b file.Location) int)
} }
} }
// TODO: we eventually want to update this so that we can read "supplier" from different syft metadata
func encodeSupplier(_ pkg.Package, sbomSupplier string) *cyclonedx.OrganizationalEntity {
if sbomSupplier != "" {
return &cyclonedx.OrganizationalEntity{
Name: sbomSupplier,
}
}
return nil
}
func DeriveBomRef(p pkg.Package) string { func DeriveBomRef(p pkg.Package) string {
// try and parse the PURL if possible and append syft id to it, to make // try and parse the PURL if possible and append syft id to it, to make
// the purl unique in the BOM. // the purl unique in the BOM.

View File

@ -152,7 +152,8 @@ func Test_encodeComponentProperties(t *testing.T) {
} }
for _, test := range tests { for _, test := range tests {
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
c := EncodeComponent(test.input, file.LocationSorter(nil)) sbomSupplier := ""
c := EncodeComponent(test.input, sbomSupplier, file.LocationSorter(nil))
if test.expected == nil { if test.expected == nil {
if c.Properties != nil { if c.Properties != nil {
t.Fatalf("expected no properties, got: %+v", *c.Properties) t.Fatalf("expected no properties, got: %+v", *c.Properties)
@ -212,7 +213,8 @@ func Test_encodeCompomentType(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
tt.pkg.ID() tt.pkg.ID()
p := EncodeComponent(tt.pkg, file.LocationSorter(nil)) sbomSupplier := ""
p := EncodeComponent(tt.pkg, sbomSupplier, file.LocationSorter(nil))
assert.Equal(t, tt.want, p) assert.Equal(t, tt.want, p)
}) })
} }

View File

@ -211,6 +211,16 @@ func extractComponents(meta *cyclonedx.Metadata) source.Description {
} }
c := meta.Component c := meta.Component
supplier := ""
// First check component-level supplier
if c.Supplier != nil && c.Supplier.Name != "" {
supplier = c.Supplier.Name
}
// Fall back to metadata-level supplier if component supplier is not set
if supplier == "" && meta.Supplier != nil && meta.Supplier.Name != "" {
supplier = meta.Supplier.Name
}
switch c.Type { switch c.Type {
case cyclonedx.ComponentTypeContainer: case cyclonedx.ComponentTypeContainer:
var labels map[string]string var labels map[string]string
@ -221,6 +231,7 @@ func extractComponents(meta *cyclonedx.Metadata) source.Description {
return source.Description{ return source.Description{
ID: "", ID: "",
Supplier: supplier,
// TODO: can we decode alias name-version somehow? (it isn't be encoded in the first place yet) // TODO: can we decode alias name-version somehow? (it isn't be encoded in the first place yet)
Metadata: source.ImageMetadata{ Metadata: source.ImageMetadata{
@ -236,6 +247,7 @@ func extractComponents(meta *cyclonedx.Metadata) source.Description {
// TODO: this is lossy... we can't know if this is a file or a directory // TODO: this is lossy... we can't know if this is a file or a directory
return source.Description{ return source.Description{
ID: "", ID: "",
Supplier: supplier,
Metadata: source.FileMetadata{Path: c.Name}, Metadata: source.FileMetadata{Path: c.Name},
} }
} }

View File

@ -10,6 +10,7 @@ import (
const NONE = "NONE" const NONE = "NONE"
const NOASSERTION = "NOASSERTION" const NOASSERTION = "NOASSERTION"
const SUPPLIERORG = "Organization"
func DownloadLocation(p pkg.Package) string { func DownloadLocation(p pkg.Package) string {
// 3.7: Package Download Location // 3.7: Package Download Location

View File

@ -12,10 +12,16 @@ import (
) )
// Source object represents the thing that was cataloged // Source object represents the thing that was cataloged
// Note: syft currently makes no claims or runs any logic to determine the Supplier field below
// Instead, the Supplier can be determined by the user of syft and passed as a config or flag to help fulfill
// the NTIA minimum elements. For mor information see the NTIA framing document below
// https://www.ntia.gov/files/ntia/publications/framingsbom_20191112.pdf
type Source struct { type Source struct {
ID string `json:"id"` ID string `json:"id"`
Name string `json:"name"` Name string `json:"name"`
Version string `json:"version"` Version string `json:"version"`
Supplier string `json:"supplier,omitempty"`
Type string `json:"type"` Type string `json:"type"`
Metadata interface{} `json:"metadata"` Metadata interface{} `json:"metadata"`
} }
@ -25,6 +31,7 @@ type sourceUnpacker struct {
ID string `json:"id,omitempty"` ID string `json:"id,omitempty"`
Name string `json:"name"` Name string `json:"name"`
Version string `json:"version"` Version string `json:"version"`
Supplier string `json:"supplier,omitempty"`
Type string `json:"type"` Type string `json:"type"`
Metadata json.RawMessage `json:"metadata"` Metadata json.RawMessage `json:"metadata"`
Target json.RawMessage `json:"target"` // pre-v9 schema support Target json.RawMessage `json:"target"` // pre-v9 schema support
@ -40,6 +47,7 @@ func (s *Source) UnmarshalJSON(b []byte) error {
s.Name = unpacker.Name s.Name = unpacker.Name
s.Version = unpacker.Version s.Version = unpacker.Version
s.Supplier = unpacker.Supplier
s.Type = unpacker.Type s.Type = unpacker.Type
s.ID = unpacker.ID s.ID = unpacker.ID

View File

@ -320,6 +320,7 @@ func toSourceModel(src source.Description) model.Source {
ID: src.ID, ID: src.ID,
Name: src.Name, Name: src.Name,
Version: src.Version, Version: src.Version,
Supplier: src.Supplier,
Type: sourcemetadata.JSONName(src.Metadata), Type: sourcemetadata.JSONName(src.Metadata),
Metadata: src.Metadata, Metadata: src.Metadata,
} }

View File

@ -59,6 +59,7 @@ func Test_toSourceModel(t *testing.T) {
ID: "test-id", ID: "test-id",
Name: "some-name", Name: "some-name",
Version: "some-version", Version: "some-version",
Supplier: "optional-supplier",
Metadata: source.DirectoryMetadata{ Metadata: source.DirectoryMetadata{
Path: "some/path", Path: "some/path",
Base: "some/base", Base: "some/base",
@ -68,6 +69,7 @@ func Test_toSourceModel(t *testing.T) {
ID: "test-id", ID: "test-id",
Name: "some-name", Name: "some-name",
Version: "some-version", Version: "some-version",
Supplier: "optional-supplier",
Type: "directory", Type: "directory",
Metadata: source.DirectoryMetadata{ Metadata: source.DirectoryMetadata{
Path: "some/path", Path: "some/path",
@ -81,6 +83,7 @@ func Test_toSourceModel(t *testing.T) {
ID: "test-id", ID: "test-id",
Name: "some-name", Name: "some-name",
Version: "some-version", Version: "some-version",
Supplier: "optional-supplier",
Metadata: source.FileMetadata{ Metadata: source.FileMetadata{
Path: "some/path", Path: "some/path",
Digests: []file.Digest{{Algorithm: "sha256", Value: "some-digest"}}, Digests: []file.Digest{{Algorithm: "sha256", Value: "some-digest"}},
@ -91,6 +94,7 @@ func Test_toSourceModel(t *testing.T) {
ID: "test-id", ID: "test-id",
Name: "some-name", Name: "some-name",
Version: "some-version", Version: "some-version",
Supplier: "optional-supplier",
Type: "file", Type: "file",
Metadata: source.FileMetadata{ Metadata: source.FileMetadata{
Path: "some/path", Path: "some/path",

View File

@ -315,6 +315,7 @@ func toSyftSourceData(s model.Source) *source.Description {
ID: s.ID, ID: s.ID,
Name: s.Name, Name: s.Name,
Version: s.Version, Version: s.Version,
Supplier: s.Supplier,
Metadata: s.Metadata, Metadata: s.Metadata,
} }
} }

View File

@ -3,6 +3,7 @@ package source
type Alias struct { type Alias struct {
Name string `json:"name" yaml:"name" mapstructure:"name"` Name string `json:"name" yaml:"name" mapstructure:"name"`
Version string `json:"version" yaml:"version" mapstructure:"version"` Version string `json:"version" yaml:"version" mapstructure:"version"`
Supplier string `json:"supplier" yaml:"supplier" mapstructure:"supplier"`
} }
func (a *Alias) IsEmpty() bool { func (a *Alias) IsEmpty() bool {

View File

@ -5,5 +5,6 @@ type Description struct {
ID string `hash:"ignore"` // the id generated from the parent source struct ID string `hash:"ignore"` // the id generated from the parent source struct
Name string `hash:"ignore"` Name string `hash:"ignore"`
Version string `hash:"ignore"` Version string `hash:"ignore"`
Supplier string `hash:"ignore"`
Metadata interface{} Metadata interface{}
} }

View File

@ -112,6 +112,7 @@ func (s directorySource) ID() artifact.ID {
func (s directorySource) Describe() source.Description { func (s directorySource) Describe() source.Description {
name := cleanDirPath(s.config.Path, s.config.Base) name := cleanDirPath(s.config.Path, s.config.Base)
version := "" version := ""
supplier := ""
if !s.config.Alias.IsEmpty() { if !s.config.Alias.IsEmpty() {
a := s.config.Alias a := s.config.Alias
if a.Name != "" { if a.Name != "" {
@ -120,11 +121,15 @@ func (s directorySource) Describe() source.Description {
if a.Version != "" { if a.Version != "" {
version = a.Version version = a.Version
} }
if a.Supplier != "" {
supplier = a.Supplier
}
} }
return source.Description{ return source.Description{
ID: string(s.id), ID: string(s.id),
Name: name, Name: name,
Version: version, Version: version,
Supplier: supplier,
Metadata: source.DirectoryMetadata{ Metadata: source.DirectoryMetadata{
Path: s.config.Path, Path: s.config.Path,
Base: s.config.Base, Base: s.config.Base,

View File

@ -475,12 +475,13 @@ func Test_DirectorySource_ID(t *testing.T) {
wantErr: require.Error, wantErr: require.Error,
}, },
{ {
name: "to dir with name and version", name: "to dir with name and version and supplier",
cfg: Config{ cfg: Config{
Path: "./test-fixtures", Path: "./test-fixtures",
Alias: source.Alias{ Alias: source.Alias{
Name: "name-me-that!", Name: "name-me-that!",
Version: "version-me-this!", Version: "version-me-this!",
Supplier: "some-supplier",
}, },
}, },
want: artifact.ID("51a5f2a1536cf4b5220d4247814b07eec5862ab0547050f90e9ae216548ded7e"), want: artifact.ID("51a5f2a1536cf4b5220d4247814b07eec5862ab0547050f90e9ae216548ded7e"),
@ -492,6 +493,7 @@ func Test_DirectorySource_ID(t *testing.T) {
Alias: source.Alias{ Alias: source.Alias{
Name: "name-me-that!", Name: "name-me-that!",
Version: "version-me-this!", Version: "version-me-this!",
Supplier: "some-supplier",
}, },
}, },
// note: this must match the previous value because the alias should trump the path info // note: this must match the previous value because the alias should trump the path info

View File

@ -123,6 +123,7 @@ func (s fileSource) ID() artifact.ID {
func (s fileSource) Describe() source.Description { func (s fileSource) Describe() source.Description {
name := path.Base(s.config.Path) name := path.Base(s.config.Path)
version := s.digestForVersion version := s.digestForVersion
supplier := ""
if !s.config.Alias.IsEmpty() { if !s.config.Alias.IsEmpty() {
a := s.config.Alias a := s.config.Alias
if a.Name != "" { if a.Name != "" {
@ -132,11 +133,16 @@ func (s fileSource) Describe() source.Description {
if a.Version != "" { if a.Version != "" {
version = a.Version version = a.Version
} }
if a.Supplier != "" {
supplier = a.Supplier
}
} }
return source.Description{ return source.Description{
ID: string(s.id), ID: string(s.id),
Name: name, Name: name,
Version: version, Version: version,
Supplier: supplier,
Metadata: source.FileMetadata{ Metadata: source.FileMetadata{
Path: s.config.Path, Path: s.config.Path,
Digests: s.digests, Digests: s.digests,

View File

@ -51,6 +51,7 @@ func (s stereoscopeImageSource) Describe() source.Description {
a := s.config.Alias a := s.config.Alias
name := a.Name name := a.Name
supplier := a.Supplier
nameIfUnset := func(n string) { nameIfUnset := func(n string) {
if name != "" { if name != "" {
return return
@ -90,6 +91,7 @@ func (s stereoscopeImageSource) Describe() source.Description {
ID: string(s.id), ID: string(s.id),
Name: name, Name: name,
Version: version, Version: version,
Supplier: supplier,
Metadata: s.metadata, Metadata: s.metadata,
} }
} }

View File

@ -71,6 +71,21 @@ func TestPackagesCmdFlags(t *testing.T) {
assertSuccessfulReturnCode, assertSuccessfulReturnCode,
}, },
}, },
{
name: "source flags override bom metadata",
args: []string{
"scan",
"--source-name", "custom-name",
"--source-version", "custom-version",
"--source-supplier", "custom-supplier",
"-o", "json", coverageImage},
assertions: []traitAssertion{
assertInOutput("custom-name"),
assertInOutput("custom-version"),
assertInOutput("custom-supplier"),
assertSuccessfulReturnCode,
},
},
// I haven't been able to reproduce locally yet, but in CI this has proven to be unstable: // I haven't been able to reproduce locally yet, but in CI this has proven to be unstable:
// For the same commit: // For the same commit:
// pass: https://github.com/anchore/syft/runs/4611344142?check_suite_focus=true // pass: https://github.com/anchore/syft/runs/4611344142?check_suite_focus=true