feat: syft 3435 - add file components to cyclonedx bom output when file metadata is available (#3539)

---------
Signed-off-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com>
This commit is contained in:
Christopher Angelo Phillips 2025-01-31 15:09:15 -05:00 committed by GitHub
parent a16e374a50
commit 9a9195e5c4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 242 additions and 1 deletions

View File

@ -6,12 +6,14 @@ import (
"strings" "strings"
"time" "time"
"github.com/CycloneDX/cyclonedx-go" cyclonedx "github.com/CycloneDX/cyclonedx-go"
"github.com/google/uuid" "github.com/google/uuid"
stfile "github.com/anchore/stereoscope/pkg/file"
"github.com/anchore/syft/internal/log" "github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/artifact" "github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/cpe" "github.com/anchore/syft/syft/cpe"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/format/internal/cyclonedxutil/helpers" "github.com/anchore/syft/syft/format/internal/cyclonedxutil/helpers"
"github.com/anchore/syft/syft/linux" "github.com/anchore/syft/syft/linux"
"github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/pkg"
@ -19,6 +21,18 @@ import (
"github.com/anchore/syft/syft/source" "github.com/anchore/syft/syft/source"
) )
var cycloneDXValidHash = map[string]cyclonedx.HashAlgorithm{
"sha1": cyclonedx.HashAlgoSHA1,
"md5": cyclonedx.HashAlgoMD5,
"sha256": cyclonedx.HashAlgoSHA256,
"sha384": cyclonedx.HashAlgoSHA384,
"sha512": cyclonedx.HashAlgoSHA512,
"blake2b256": cyclonedx.HashAlgoBlake2b_256,
"blake2b384": cyclonedx.HashAlgoBlake2b_384,
"blake2b512": cyclonedx.HashAlgoBlake2b_512,
"blake3": cyclonedx.HashAlgoBlake3,
}
func ToFormatModel(s sbom.SBOM) *cyclonedx.BOM { func ToFormatModel(s sbom.SBOM) *cyclonedx.BOM {
cdxBOM := cyclonedx.NewBOM() cdxBOM := cyclonedx.NewBOM()
@ -28,12 +42,49 @@ func ToFormatModel(s sbom.SBOM) *cyclonedx.BOM {
cdxBOM.SerialNumber = uuid.New().URN() cdxBOM.SerialNumber = uuid.New().URN()
cdxBOM.Metadata = toBomDescriptor(s.Descriptor.Name, s.Descriptor.Version, s.Source) cdxBOM.Metadata = toBomDescriptor(s.Descriptor.Name, s.Descriptor.Version, s.Source)
// Packages
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) components[i] = helpers.EncodeComponent(p)
} }
components = append(components, toOSComponent(s.Artifacts.LinuxDistribution)...) components = append(components, toOSComponent(s.Artifacts.LinuxDistribution)...)
// Files
artifacts := s.Artifacts
coordinates := s.AllCoordinates()
for _, coordinate := range coordinates {
var metadata *file.Metadata
// File Info
fileMetadata, exists := artifacts.FileMetadata[coordinate]
// no file metadata then don't include in SBOM
// the syft config allows for sometimes only capturing files owned by packages
// so there can be a map miss here where we have less metadata than all coordinates
if !exists {
continue
}
if fileMetadata.Type == stfile.TypeDirectory ||
fileMetadata.Type == stfile.TypeSocket ||
fileMetadata.Type == stfile.TypeSymLink {
// skip dir, symlinks and sockets for the final bom
continue
}
metadata = &fileMetadata
// Digests
var digests []file.Digest
if digestsForLocation, exists := artifacts.FileDigests[coordinate]; exists {
digests = digestsForLocation
}
cdxHashes := digestsToHashes(digests)
components = append(components, cyclonedx.Component{
BOMRef: string(coordinate.ID()),
Type: cyclonedx.ComponentTypeFile,
Name: metadata.Path,
Hashes: &cdxHashes,
})
}
cdxBOM.Components = &components cdxBOM.Components = &components
dependencies := toDependencies(s.Relationships) dependencies := toDependencies(s.Relationships)
@ -44,6 +95,22 @@ func ToFormatModel(s sbom.SBOM) *cyclonedx.BOM {
return cdxBOM return cdxBOM
} }
func digestsToHashes(digests []file.Digest) []cyclonedx.Hash {
var hashes []cyclonedx.Hash
for _, digest := range digests {
lookup := strings.ToLower(digest.Algorithm)
cdxAlgo, exists := cycloneDXValidHash[lookup]
if !exists {
continue
}
hashes = append(hashes, cyclonedx.Hash{
Algorithm: cdxAlgo,
Value: digest.Value,
})
}
return hashes
}
func toOSComponent(distro *linux.Release) []cyclonedx.Component { func toOSComponent(distro *linux.Release) []cyclonedx.Component {
if distro == nil { if distro == nil {
return []cyclonedx.Component{} return []cyclonedx.Component{}

View File

@ -9,7 +9,9 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
stfile "github.com/anchore/stereoscope/pkg/file"
"github.com/anchore/syft/syft/artifact" "github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/format/internal/cyclonedxutil/helpers" "github.com/anchore/syft/syft/format/internal/cyclonedxutil/helpers"
"github.com/anchore/syft/syft/linux" "github.com/anchore/syft/syft/linux"
"github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/pkg"
@ -143,6 +145,178 @@ func Test_relationships(t *testing.T) {
} }
} }
func Test_FileComponents(t *testing.T) {
p1 := pkg.Package{
Name: "p1",
}
tests := []struct {
name string
sbom sbom.SBOM
want []cyclonedx.Component
}{
{
name: "sbom coordinates with file metadata are serialized to cdx along with packages",
sbom: sbom.SBOM{
Artifacts: sbom.Artifacts{
Packages: pkg.NewCollection(p1),
FileMetadata: map[file.Coordinates]file.Metadata{
{RealPath: "/test"}: {Path: "/test", Type: stfile.TypeRegular},
},
FileDigests: map[file.Coordinates][]file.Digest{
{RealPath: "/test"}: {
{
Algorithm: "sha256",
Value: "xyz12345",
},
},
},
},
},
want: []cyclonedx.Component{
{
BOMRef: "2a1fc74ade23e357",
Type: cyclonedx.ComponentTypeLibrary,
Name: "p1",
},
{
BOMRef: "3f31cb2d98be6c1e",
Name: "/test",
Type: cyclonedx.ComponentTypeFile,
Hashes: &[]cyclonedx.Hash{
{Algorithm: "SHA-256", Value: "xyz12345"},
},
},
},
},
{
name: "sbom coordinates that don't contain metadata are not added to the final output",
sbom: sbom.SBOM{
Artifacts: sbom.Artifacts{
FileMetadata: map[file.Coordinates]file.Metadata{
{RealPath: "/test"}: {Path: "/test", Type: stfile.TypeRegular},
},
FileDigests: map[file.Coordinates][]file.Digest{
{RealPath: "/test"}: {
{
Algorithm: "sha256",
Value: "xyz12345",
},
},
{RealPath: "/test-2"}: {
{
Algorithm: "sha256",
Value: "xyz678910",
},
},
},
},
},
want: []cyclonedx.Component{
{
BOMRef: "3f31cb2d98be6c1e",
Name: "/test",
Type: cyclonedx.ComponentTypeFile,
Hashes: &[]cyclonedx.Hash{
{Algorithm: "SHA-256", Value: "xyz12345"},
},
},
},
},
{
name: "sbom coordinates that return hashes not covered by cdx only include valid digests",
sbom: sbom.SBOM{
Artifacts: sbom.Artifacts{
FileMetadata: map[file.Coordinates]file.Metadata{
{RealPath: "/test"}: {Path: "/test", Type: stfile.TypeRegular},
},
FileDigests: map[file.Coordinates][]file.Digest{
{RealPath: "/test"}: {
{
Algorithm: "xxh64",
Value: "xyz12345",
},
{
Algorithm: "sha256",
Value: "xyz678910",
},
},
},
},
},
want: []cyclonedx.Component{
{
BOMRef: "3f31cb2d98be6c1e",
Name: "/test",
Type: cyclonedx.ComponentTypeFile,
Hashes: &[]cyclonedx.Hash{
{Algorithm: "SHA-256", Value: "xyz678910"},
},
},
},
},
{
name: "sbom coordinates who's metadata is directory or symlink are skipped",
sbom: sbom.SBOM{
Artifacts: sbom.Artifacts{
FileMetadata: map[file.Coordinates]file.Metadata{
{RealPath: "/testdir"}: {
Path: "/testdir",
Type: stfile.TypeDirectory,
},
{RealPath: "/testsym"}: {
Path: "/testsym",
Type: stfile.TypeSymLink,
},
{RealPath: "/test"}: {Path: "/test", Type: stfile.TypeRegular},
},
FileDigests: map[file.Coordinates][]file.Digest{
{RealPath: "/test"}: {
{
Algorithm: "sha256",
Value: "xyz12345",
},
},
},
},
},
want: []cyclonedx.Component{
{
BOMRef: "3f31cb2d98be6c1e",
Name: "/test",
Type: cyclonedx.ComponentTypeFile,
Hashes: &[]cyclonedx.Hash{
{Algorithm: "SHA-256", Value: "xyz12345"},
},
},
},
},
{
name: "sbom with no files serialized correctly",
sbom: sbom.SBOM{
Artifacts: sbom.Artifacts{
Packages: pkg.NewCollection(p1),
},
},
want: []cyclonedx.Component{
{
BOMRef: "2a1fc74ade23e357",
Type: cyclonedx.ComponentTypeLibrary,
Name: "p1",
},
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
cdx := ToFormatModel(test.sbom)
got := *cdx.Components
if diff := cmp.Diff(test.want, got); diff != "" {
t.Errorf("cdx file components mismatch (-want +got):\n%s", diff)
}
})
}
}
func Test_toBomDescriptor(t *testing.T) { func Test_toBomDescriptor(t *testing.T) {
type args struct { type args struct {
name string name string