Skip to content

Commit

Permalink
feat: syft 3435 - add file components to cyclonedx bom output when fi…
Browse files Browse the repository at this point in the history
…le metadata is available (anchore#3539)

---------
Signed-off-by: Christopher Phillips <[email protected]>
  • Loading branch information
spiffcs authored and juan131 committed Feb 14, 2025
1 parent 5e09183 commit 3978879
Show file tree
Hide file tree
Showing 2 changed files with 242 additions and 1 deletion.
69 changes: 68 additions & 1 deletion syft/format/common/cyclonedxhelpers/to_format_model.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,33 @@ 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"
"github.com/anchore/syft/syft/sbom"
"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()

Expand All @@ -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)
Expand All @@ -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{}
Expand Down
174 changes: 174 additions & 0 deletions syft/format/common/cyclonedxhelpers/to_format_model_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 3978879

Please sign in to comment.