mirror of
https://github.com/anchore/syft.git
synced 2026-11-01 15:06:36 +01:00
feat: add package supplier flag (#4131)
--------- Signed-off-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com>
This commit is contained in:
@@ -48,7 +48,7 @@ func ToFormatModel(s sbom.SBOM) *cyclonedx.BOM {
|
||||
packages := s.Artifacts.Packages.Sorted()
|
||||
components := make([]cyclonedx.Component, len(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)...)
|
||||
|
||||
@@ -220,11 +220,22 @@ func toBomDescriptor(name, version string, srcMetadata source.Description) *cycl
|
||||
},
|
||||
},
|
||||
},
|
||||
Supplier: toBomSupplier(srcMetadata),
|
||||
Properties: toBomProperties(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.
|
||||
// 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.
|
||||
@@ -321,10 +332,11 @@ func toBomDescriptorComponent(srcMetadata source.Description) *cyclonedx.Compone
|
||||
log.Debugf("unable to get fingerprint of source image metadata=%s: %+v", metadata.ID, err)
|
||||
}
|
||||
return &cyclonedx.Component{
|
||||
BOMRef: string(bomRef),
|
||||
Type: cyclonedx.ComponentTypeContainer,
|
||||
Name: name,
|
||||
Version: version,
|
||||
BOMRef: string(bomRef),
|
||||
Type: cyclonedx.ComponentTypeContainer,
|
||||
Name: name,
|
||||
Version: version,
|
||||
Supplier: toBomSupplier(srcMetadata),
|
||||
}
|
||||
case source.DirectoryMetadata:
|
||||
if name == "" {
|
||||
@@ -337,9 +349,10 @@ func toBomDescriptorComponent(srcMetadata source.Description) *cyclonedx.Compone
|
||||
return &cyclonedx.Component{
|
||||
BOMRef: string(bomRef),
|
||||
// TODO: this is lossy... we can't know if this is a file or a directory
|
||||
Type: cyclonedx.ComponentTypeFile,
|
||||
Name: name,
|
||||
Version: version,
|
||||
Type: cyclonedx.ComponentTypeFile,
|
||||
Name: name,
|
||||
Version: version,
|
||||
Supplier: toBomSupplier(srcMetadata),
|
||||
}
|
||||
case source.FileMetadata:
|
||||
if name == "" {
|
||||
@@ -352,9 +365,10 @@ func toBomDescriptorComponent(srcMetadata source.Description) *cyclonedx.Compone
|
||||
return &cyclonedx.Component{
|
||||
BOMRef: string(bomRef),
|
||||
// TODO: this is lossy... we can't know if this is a file or a directory
|
||||
Type: cyclonedx.ComponentTypeFile,
|
||||
Name: name,
|
||||
Version: version,
|
||||
Type: cyclonedx.ComponentTypeFile,
|
||||
Name: name,
|
||||
Version: version,
|
||||
Supplier: toBomSupplier(srcMetadata),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -389,7 +389,69 @@ func Test_toBomDescriptor(t *testing.T) {
|
||||
Name: "syft:image:labels:key1",
|
||||
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 {
|
||||
|
||||
@@ -257,13 +257,11 @@ func toRootPackage(s source.Description) *spdx.Package {
|
||||
PackageChecksums: checksums,
|
||||
PackageExternalReferences: nil,
|
||||
PrimaryPackagePurpose: purpose,
|
||||
PackageSupplier: &spdx.Supplier{
|
||||
Supplier: helpers.NOASSERTION,
|
||||
},
|
||||
PackageCopyrightText: helpers.NOASSERTION,
|
||||
PackageDownloadLocation: helpers.NOASSERTION,
|
||||
PackageLicenseConcluded: helpers.NOASSERTION,
|
||||
PackageLicenseDeclared: helpers.NOASSERTION,
|
||||
PackageSupplier: toSPDXSupplier(s),
|
||||
PackageCopyrightText: helpers.NOASSERTION,
|
||||
PackageDownloadLocation: helpers.NOASSERTION,
|
||||
PackageLicenseConcluded: helpers.NOASSERTION,
|
||||
PackageLicenseDeclared: helpers.NOASSERTION,
|
||||
}
|
||||
|
||||
if purl != nil {
|
||||
@@ -327,6 +325,23 @@ func toSPDXID(identifiable artifact.Identifiable) spdx.ElementID {
|
||||
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/)
|
||||
//
|
||||
//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,
|
||||
// or NOASSERTION
|
||||
// Cardinality: optional, one
|
||||
PackageSupplier: toPackageSupplier(p),
|
||||
PackageSupplier: toPackageSupplier(p, sbom.Source.Supplier),
|
||||
|
||||
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)
|
||||
if kind == "" || supplier == "" {
|
||||
supplier := helpers.NOASSERTION
|
||||
supplierType := ""
|
||||
if sbomSupplier != "" {
|
||||
supplier = sbomSupplier
|
||||
supplierType = helpers.SUPPLIERORG
|
||||
}
|
||||
return &spdx.Supplier{
|
||||
Supplier: helpers.NOASSERTION,
|
||||
Supplier: supplier,
|
||||
SupplierType: supplierType,
|
||||
}
|
||||
}
|
||||
return &spdx.Supplier{
|
||||
|
||||
@@ -36,8 +36,9 @@ func Test_toFormatModel(t *testing.T) {
|
||||
name: "container",
|
||||
in: sbom.SBOM{
|
||||
Source: source.Description{
|
||||
Name: "alpine",
|
||||
Version: "sha256:d34db33f",
|
||||
Name: "alpine",
|
||||
Version: "sha256:d34db33f",
|
||||
Supplier: "Alpine Linux",
|
||||
Metadata: source.ImageMetadata{
|
||||
UserInput: "alpine:latest",
|
||||
ManifestDigest: "sha256:d34db33f",
|
||||
@@ -61,7 +62,8 @@ func Test_toFormatModel(t *testing.T) {
|
||||
PackageName: "pkg-1",
|
||||
PackageVersion: "version-1",
|
||||
PackageSupplier: &spdx.Supplier{
|
||||
Supplier: "NOASSERTION",
|
||||
Supplier: "Alpine Linux",
|
||||
SupplierType: "Organization",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -78,7 +80,8 @@ func Test_toFormatModel(t *testing.T) {
|
||||
},
|
||||
},
|
||||
PackageSupplier: &spdx.Supplier{
|
||||
Supplier: "NOASSERTION",
|
||||
Supplier: "Alpine Linux",
|
||||
SupplierType: "Organization",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -145,10 +145,21 @@ func containerSource(p *spdx.Package) source.Description {
|
||||
c := p.PackageChecksums[0]
|
||||
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{
|
||||
ID: id,
|
||||
Name: p.PackageName,
|
||||
Version: p.PackageVersion,
|
||||
ID: id,
|
||||
Name: p.PackageName,
|
||||
Version: p.PackageVersion,
|
||||
Supplier: supplier,
|
||||
Metadata: source.ImageMetadata{
|
||||
UserInput: container,
|
||||
ID: id,
|
||||
@@ -179,10 +190,16 @@ func fileSource(p *spdx.Package) source.Description {
|
||||
metadata, version = fileSourceMetadata(p)
|
||||
}
|
||||
|
||||
supplier := ""
|
||||
if p.PackageSupplier.Supplier != helpers.NOASSERTION {
|
||||
supplier = p.PackageSupplier.Supplier
|
||||
}
|
||||
|
||||
return source.Description{
|
||||
ID: string(p.PackageSPDXIdentifier),
|
||||
Name: p.PackageName,
|
||||
Version: version,
|
||||
Supplier: supplier,
|
||||
Metadata: metadata,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -525,17 +525,19 @@ func Test_convertToAndFromFormat(t *testing.T) {
|
||||
UserInput: "some-image:some-tag",
|
||||
ManifestDigest: "sha256:ab8b83234bc28f28d8e",
|
||||
},
|
||||
Name: "some-image",
|
||||
Version: "some-tag",
|
||||
Name: "some-image",
|
||||
Version: "some-tag",
|
||||
Supplier: "some-supplier",
|
||||
},
|
||||
packages: packages,
|
||||
relationships: relationships,
|
||||
},
|
||||
{
|
||||
name: ". directory source",
|
||||
name: ". directory source with supplier",
|
||||
source: source.Description{
|
||||
ID: "DocumentRoot-Directory-.",
|
||||
Name: ".",
|
||||
ID: "DocumentRoot-Directory-.",
|
||||
Name: ".",
|
||||
Supplier: "some-supplier",
|
||||
Metadata: source.DirectoryMetadata{
|
||||
Path: ".",
|
||||
},
|
||||
@@ -544,7 +546,7 @@ func Test_convertToAndFromFormat(t *testing.T) {
|
||||
relationships: relationships,
|
||||
},
|
||||
{
|
||||
name: "directory source",
|
||||
name: "directory source without supplier",
|
||||
source: source.Description{
|
||||
ID: "DocumentRoot-Directory-my-app",
|
||||
Name: "my-app",
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
"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")
|
||||
|
||||
if p.Metadata != nil {
|
||||
@@ -49,6 +49,7 @@ func EncodeComponent(p pkg.Package, locationSorter func(a, b file.Location) int)
|
||||
Name: p.Name,
|
||||
Group: encodeGroup(p),
|
||||
Version: p.Version,
|
||||
Supplier: encodeSupplier(p, supplier),
|
||||
PackageURL: p.PURL,
|
||||
Licenses: encodeLicenses(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 {
|
||||
// try and parse the PURL if possible and append syft id to it, to make
|
||||
// the purl unique in the BOM.
|
||||
|
||||
@@ -152,7 +152,8 @@ func Test_encodeComponentProperties(t *testing.T) {
|
||||
}
|
||||
for _, test := range tests {
|
||||
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 c.Properties != nil {
|
||||
t.Fatalf("expected no properties, got: %+v", *c.Properties)
|
||||
@@ -212,7 +213,8 @@ func Test_encodeCompomentType(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
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)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -211,6 +211,16 @@ func extractComponents(meta *cyclonedx.Metadata) source.Description {
|
||||
}
|
||||
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 {
|
||||
case cyclonedx.ComponentTypeContainer:
|
||||
var labels map[string]string
|
||||
@@ -220,7 +230,8 @@ func extractComponents(meta *cyclonedx.Metadata) 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)
|
||||
|
||||
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
|
||||
return source.Description{
|
||||
ID: "",
|
||||
Supplier: supplier,
|
||||
Metadata: source.FileMetadata{Path: c.Name},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
|
||||
const NONE = "NONE"
|
||||
const NOASSERTION = "NOASSERTION"
|
||||
const SUPPLIERORG = "Organization"
|
||||
|
||||
func DownloadLocation(p pkg.Package) string {
|
||||
// 3.7: Package Download Location
|
||||
|
||||
@@ -12,10 +12,16 @@ import (
|
||||
)
|
||||
|
||||
// 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 {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
Supplier string `json:"supplier,omitempty"`
|
||||
Type string `json:"type"`
|
||||
Metadata interface{} `json:"metadata"`
|
||||
}
|
||||
@@ -25,6 +31,7 @@ type sourceUnpacker struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
Supplier string `json:"supplier,omitempty"`
|
||||
Type string `json:"type"`
|
||||
Metadata json.RawMessage `json:"metadata"`
|
||||
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.Version = unpacker.Version
|
||||
s.Supplier = unpacker.Supplier
|
||||
s.Type = unpacker.Type
|
||||
s.ID = unpacker.ID
|
||||
|
||||
|
||||
@@ -320,6 +320,7 @@ func toSourceModel(src source.Description) model.Source {
|
||||
ID: src.ID,
|
||||
Name: src.Name,
|
||||
Version: src.Version,
|
||||
Supplier: src.Supplier,
|
||||
Type: sourcemetadata.JSONName(src.Metadata),
|
||||
Metadata: src.Metadata,
|
||||
}
|
||||
|
||||
@@ -56,19 +56,21 @@ func Test_toSourceModel(t *testing.T) {
|
||||
{
|
||||
name: "directory",
|
||||
src: source.Description{
|
||||
ID: "test-id",
|
||||
Name: "some-name",
|
||||
Version: "some-version",
|
||||
ID: "test-id",
|
||||
Name: "some-name",
|
||||
Version: "some-version",
|
||||
Supplier: "optional-supplier",
|
||||
Metadata: source.DirectoryMetadata{
|
||||
Path: "some/path",
|
||||
Base: "some/base",
|
||||
},
|
||||
},
|
||||
expected: model.Source{
|
||||
ID: "test-id",
|
||||
Name: "some-name",
|
||||
Version: "some-version",
|
||||
Type: "directory",
|
||||
ID: "test-id",
|
||||
Name: "some-name",
|
||||
Version: "some-version",
|
||||
Supplier: "optional-supplier",
|
||||
Type: "directory",
|
||||
Metadata: source.DirectoryMetadata{
|
||||
Path: "some/path",
|
||||
Base: "some/base",
|
||||
@@ -78,9 +80,10 @@ func Test_toSourceModel(t *testing.T) {
|
||||
{
|
||||
name: "file",
|
||||
src: source.Description{
|
||||
ID: "test-id",
|
||||
Name: "some-name",
|
||||
Version: "some-version",
|
||||
ID: "test-id",
|
||||
Name: "some-name",
|
||||
Version: "some-version",
|
||||
Supplier: "optional-supplier",
|
||||
Metadata: source.FileMetadata{
|
||||
Path: "some/path",
|
||||
Digests: []file.Digest{{Algorithm: "sha256", Value: "some-digest"}},
|
||||
@@ -88,10 +91,11 @@ func Test_toSourceModel(t *testing.T) {
|
||||
},
|
||||
},
|
||||
expected: model.Source{
|
||||
ID: "test-id",
|
||||
Name: "some-name",
|
||||
Version: "some-version",
|
||||
Type: "file",
|
||||
ID: "test-id",
|
||||
Name: "some-name",
|
||||
Version: "some-version",
|
||||
Supplier: "optional-supplier",
|
||||
Type: "file",
|
||||
Metadata: source.FileMetadata{
|
||||
Path: "some/path",
|
||||
Digests: []file.Digest{{Algorithm: "sha256", Value: "some-digest"}},
|
||||
|
||||
@@ -315,6 +315,7 @@ func toSyftSourceData(s model.Source) *source.Description {
|
||||
ID: s.ID,
|
||||
Name: s.Name,
|
||||
Version: s.Version,
|
||||
Supplier: s.Supplier,
|
||||
Metadata: s.Metadata,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user