mirror of
https://github.com/anchore/syft.git
synced 2025-11-17 16:33:21 +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"
|
"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{}
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user