mirror of
https://github.com/anchore/syft.git
synced 2025-11-17 08:23:15 +01:00
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:
parent
a16e374a50
commit
9a9195e5c4
@ -6,12 +6,14 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/CycloneDX/cyclonedx-go"
|
||||
cyclonedx "github.com/CycloneDX/cyclonedx-go"
|
||||
"github.com/google/uuid"
|
||||
|
||||
stfile "github.com/anchore/stereoscope/pkg/file"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/syft/artifact"
|
||||
"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/linux"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
@ -19,6 +21,18 @@ import (
|
||||
"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 {
|
||||
cdxBOM := cyclonedx.NewBOM()
|
||||
|
||||
@ -28,12 +42,49 @@ func ToFormatModel(s sbom.SBOM) *cyclonedx.BOM {
|
||||
cdxBOM.SerialNumber = uuid.New().URN()
|
||||
cdxBOM.Metadata = toBomDescriptor(s.Descriptor.Name, s.Descriptor.Version, s.Source)
|
||||
|
||||
// Packages
|
||||
packages := s.Artifacts.Packages.Sorted()
|
||||
components := make([]cyclonedx.Component, len(packages))
|
||||
for i, p := range packages {
|
||||
components[i] = helpers.EncodeComponent(p)
|
||||
}
|
||||
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
|
||||
|
||||
dependencies := toDependencies(s.Relationships)
|
||||
@ -44,6 +95,22 @@ func ToFormatModel(s sbom.SBOM) *cyclonedx.BOM {
|
||||
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 {
|
||||
if distro == nil {
|
||||
return []cyclonedx.Component{}
|
||||
|
||||
@ -9,7 +9,9 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
stfile "github.com/anchore/stereoscope/pkg/file"
|
||||
"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/linux"
|
||||
"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) {
|
||||
type args struct {
|
||||
name string
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user