From 41ad8ed205220bfacf0c478fd0166ddd170bfc35 Mon Sep 17 00:00:00 2001 From: Christopher Phillips <32073428+spiffcs@users.noreply.github.com> Date: Wed, 18 Dec 2024 14:11:57 -0500 Subject: [PATCH 1/7] feat: add files to components list when file metadata is found Signed-off-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com> --- .../cyclonedxhelpers/to_format_model.go | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/syft/format/common/cyclonedxhelpers/to_format_model.go b/syft/format/common/cyclonedxhelpers/to_format_model.go index ba93a367e56..95686f1d2f5 100644 --- a/syft/format/common/cyclonedxhelpers/to_format_model.go +++ b/syft/format/common/cyclonedxhelpers/to_format_model.go @@ -12,6 +12,7 @@ import ( "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" @@ -28,12 +29,42 @@ 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() + fileComponents := make([]cyclonedx.Component, len(coordinates)) + for i, coordinate := range coordinates { + var metadata *file.Metadata + // File Info + fileMetadata, exists := artifacts.FileMetadata[coordinate] + // no file metadata then don't include in SBOM + if !exists { + continue + } + metadata = &fileMetadata + + // Digests + var digests []file.Digest + if digestsForLocation, exists := artifacts.FileDigests[coordinate]; exists { + digests = digestsForLocation + } + + fileComponents[i] = cyclonedx.Component{ + BOMRef: string(coordinate.ID()), + Type: cyclonedx.ComponentTypeFile, + Name: metadata.Path, + Hashes: digestsToHashes(digests), + } + } + components = append(components, fileComponents...) cdxBOM.Components = &components dependencies := toDependencies(s.Relationships) @@ -44,6 +75,33 @@ func ToFormatModel(s sbom.SBOM) *cyclonedx.BOM { return cdxBOM } +func digestsToHashes(digests []file.Digest) *[]cyclonedx.Hash { + hashes := make([]cyclonedx.Hash, len(digests)) + for i, digest := range digests { + cdxAlgo := toCycloneDXAlgorithm(digest.Algorithm) + hashes[i] = cyclonedx.Hash{ + Algorithm: cdxAlgo, + Value: digest.Value, + } + } + return &hashes +} + +// supported algorithm in cycloneDX as of 1.4 +// "MD5", "SHA-1", "SHA-256", "SHA-384", "SHA-512", +// "SHA3-256", "SHA3-384", "SHA3-512", "BLAKE2b-256", "BLAKE2b-384", "BLAKE2b-512", "BLAKE3" +// syft supported digests: cmd/syft/cli/eventloop/tasks.go +// MD5, SHA1, SHA256 +func toCycloneDXAlgorithm(algorithm string) cyclonedx.HashAlgorithm { + validMap := map[string]cyclonedx.HashAlgorithm{ + "sha1": cyclonedx.HashAlgoSHA1, + "md5": cyclonedx.HashAlgoMD5, + "sha256": cyclonedx.HashAlgoSHA256, + } + + return validMap[strings.ToLower(algorithm)] +} + func toOSComponent(distro *linux.Release) []cyclonedx.Component { if distro == nil { return []cyclonedx.Component{} From 860e245f143d58757e5e7e43473366d85109f6d4 Mon Sep 17 00:00:00 2001 From: Christopher Phillips <32073428+spiffcs@users.noreply.github.com> Date: Wed, 18 Dec 2024 15:49:43 -0500 Subject: [PATCH 2/7] test: add file component test to format model Signed-off-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com> --- .../cyclonedxhelpers/to_format_model_test.go | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/syft/format/common/cyclonedxhelpers/to_format_model_test.go b/syft/format/common/cyclonedxhelpers/to_format_model_test.go index c3ac1f3b8f7..85d4a3ca7b3 100644 --- a/syft/format/common/cyclonedxhelpers/to_format_model_test.go +++ b/syft/format/common/cyclonedxhelpers/to_format_model_test.go @@ -10,6 +10,7 @@ import ( "github.com/stretchr/testify/require" "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 +144,52 @@ func Test_relationships(t *testing.T) { } } +func Test_fileComponents(t *testing.T) { + tests := []struct { + name string + sbom sbom.SBOM + want []cyclonedx.Component + }{ + { + name: "sbom coordinates with file metadata are serialized to cdx", + sbom: sbom.SBOM{ + Artifacts: sbom.Artifacts{ + FileMetadata: map[file.Coordinates]file.Metadata{ + {RealPath: "/test"}: {Path: "/test"}, + }, + 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"}, + }, + }, + }, + }, + } + 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 From 47a1c46980b08916f43cf9adcd218c9766bafe6e Mon Sep 17 00:00:00 2001 From: Christopher Phillips <32073428+spiffcs@users.noreply.github.com> Date: Thu, 30 Jan 2025 13:00:44 -0500 Subject: [PATCH 3/7] tests: update tests with PR feedback and overhaul file format model Signed-off-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com> --- .../cyclonedxhelpers/to_format_model.go | 52 ++++-- .../cyclonedxhelpers/to_format_model_test.go | 150 +++++++++++++++++- 2 files changed, 185 insertions(+), 17 deletions(-) diff --git a/syft/format/common/cyclonedxhelpers/to_format_model.go b/syft/format/common/cyclonedxhelpers/to_format_model.go index 95686f1d2f5..ab0a83ed906 100644 --- a/syft/format/common/cyclonedxhelpers/to_format_model.go +++ b/syft/format/common/cyclonedxhelpers/to_format_model.go @@ -2,6 +2,7 @@ package cyclonedxhelpers import ( "fmt" + "os" "slices" "strings" "time" @@ -40,15 +41,23 @@ func ToFormatModel(s sbom.SBOM) *cyclonedx.BOM { // Files artifacts := s.Artifacts coordinates := s.AllCoordinates() - fileComponents := make([]cyclonedx.Component, len(coordinates)) - for i, coordinate := range coordinates { + fileComponents := make([]cyclonedx.Component, 0) + 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.IsDir() || + fileMetadata.Mode() == os.ModeSymlink || + fileMetadata.Mode() == os.ModeSocket { + // skip dir, symlinks and sockets for the final bom + continue + } metadata = &fileMetadata // Digests @@ -57,12 +66,19 @@ func ToFormatModel(s sbom.SBOM) *cyclonedx.BOM { digests = digestsForLocation } - fileComponents[i] = cyclonedx.Component{ + // if cdx doesn't have an algorithm for the SBOM we need to drop the components + // since an empty hash field is not allowed: https://cyclonedx.org/docs/1.6/json/#components_items_hashes_items_alg + cdxHashes, err := digestsToHashes(digests) + if err != nil { + continue + } + + fileComponents = append(fileComponents, cyclonedx.Component{ BOMRef: string(coordinate.ID()), Type: cyclonedx.ComponentTypeFile, Name: metadata.Path, - Hashes: digestsToHashes(digests), - } + Hashes: cdxHashes, + }) } components = append(components, fileComponents...) cdxBOM.Components = &components @@ -75,16 +91,19 @@ func ToFormatModel(s sbom.SBOM) *cyclonedx.BOM { return cdxBOM } -func digestsToHashes(digests []file.Digest) *[]cyclonedx.Hash { - hashes := make([]cyclonedx.Hash, len(digests)) - for i, digest := range digests { - cdxAlgo := toCycloneDXAlgorithm(digest.Algorithm) - hashes[i] = cyclonedx.Hash{ +func digestsToHashes(digests []file.Digest) (*[]cyclonedx.Hash, error) { + hashes := make([]cyclonedx.Hash, 0) + for _, digest := range digests { + cdxAlgo, err := toCycloneDXAlgorithm(digest.Algorithm) + if err != nil { + return nil, err + } + hashes = append(hashes, cyclonedx.Hash{ Algorithm: cdxAlgo, Value: digest.Value, - } + }) } - return &hashes + return &hashes, nil } // supported algorithm in cycloneDX as of 1.4 @@ -92,14 +111,19 @@ func digestsToHashes(digests []file.Digest) *[]cyclonedx.Hash { // "SHA3-256", "SHA3-384", "SHA3-512", "BLAKE2b-256", "BLAKE2b-384", "BLAKE2b-512", "BLAKE3" // syft supported digests: cmd/syft/cli/eventloop/tasks.go // MD5, SHA1, SHA256 -func toCycloneDXAlgorithm(algorithm string) cyclonedx.HashAlgorithm { +func toCycloneDXAlgorithm(algorithm string) (cyclonedx.HashAlgorithm, error) { validMap := map[string]cyclonedx.HashAlgorithm{ "sha1": cyclonedx.HashAlgoSHA1, "md5": cyclonedx.HashAlgoMD5, "sha256": cyclonedx.HashAlgoSHA256, } + lookup := strings.ToLower(algorithm) + cdxAlgo, exists := validMap[lookup] + if !exists { + return "", fmt.Errorf("could not find valid cdx algorithm for %s", lookup) + } - return validMap[strings.ToLower(algorithm)] + return cdxAlgo, nil } func toOSComponent(distro *linux.Release) []cyclonedx.Component { diff --git a/syft/format/common/cyclonedxhelpers/to_format_model_test.go b/syft/format/common/cyclonedxhelpers/to_format_model_test.go index 85d4a3ca7b3..bd7c50c10b1 100644 --- a/syft/format/common/cyclonedxhelpers/to_format_model_test.go +++ b/syft/format/common/cyclonedxhelpers/to_format_model_test.go @@ -2,7 +2,9 @@ package cyclonedxhelpers import ( "fmt" + "os" "testing" + "time" "github.com/CycloneDX/cyclonedx-go" "github.com/google/go-cmp/cmp" @@ -144,18 +146,116 @@ func Test_relationships(t *testing.T) { } } -func Test_fileComponents(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", + 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", FileInfo: newMockFileInfo(false, false)}, // Embed the mock that always returns IsDir() = true + }, + 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", FileInfo: newMockFileInfo(false, false)}, + }, + 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 are not added to the final output", + sbom: sbom.SBOM{ + Artifacts: sbom.Artifacts{ + FileMetadata: map[file.Coordinates]file.Metadata{ + {RealPath: "/test"}: {Path: "/test", FileInfo: newMockFileInfo(false, false)}, + }, + FileDigests: map[file.Coordinates][]file.Digest{ + {RealPath: "/test"}: { + { + Algorithm: "xxh64", + Value: "xyz12345", + }, + }, + }, + }, + }, + want: []cyclonedx.Component{}, + }, + { + 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: "/test"}: {Path: "/test"}, + {RealPath: "/testdir"}: { + Path: "/testdir", + FileInfo: newMockFileInfo(true, false), + }, + {RealPath: "/testsym"}: { + Path: "/testsym", + FileInfo: newMockFileInfo(false, true), + }, + {RealPath: "/test"}: {Path: "/test", FileInfo: newMockFileInfo(false, false)}, }, FileDigests: map[file.Coordinates][]file.Digest{ {RealPath: "/test"}: { @@ -178,6 +278,21 @@ func Test_fileComponents(t *testing.T) { }, }, }, + { + 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) { @@ -190,6 +305,35 @@ func Test_fileComponents(t *testing.T) { } } +// mockFileInfo is a test struct that simulates fs.FileInfo +type mockFileInfo struct { + isDir bool + isSymlink bool +} + +func newMockFileInfo(isDir, isSym bool) mockFileInfo { + return mockFileInfo{ + isDir, + isSym, + } +} + +// Implement os.FileInfo interface methods +func (m mockFileInfo) Name() string { return "mockDir" } +func (m mockFileInfo) Size() int64 { return 0 } +func (m mockFileInfo) Mode() os.FileMode { + if m.isSymlink { + return os.ModeSymlink + } + if m.isDir { + return os.ModeDir + } + return os.ModeType +} // Mark as directory +func (m mockFileInfo) ModTime() time.Time { return time.Now() } +func (m mockFileInfo) IsDir() bool { return m.isDir } +func (m mockFileInfo) Sys() any { return nil } + func Test_toBomDescriptor(t *testing.T) { type args struct { name string From 13ef836af8d89bf4e64a8962ab890bd7b107ba4e Mon Sep 17 00:00:00 2001 From: Christopher Phillips <32073428+spiffcs@users.noreply.github.com> Date: Thu, 30 Jan 2025 13:33:01 -0500 Subject: [PATCH 4/7] chore: fix type for tests Signed-off-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com> --- .../cyclonedxhelpers/to_format_model.go | 8 ++-- .../cyclonedxhelpers/to_format_model_test.go | 42 ++++--------------- 2 files changed, 13 insertions(+), 37 deletions(-) diff --git a/syft/format/common/cyclonedxhelpers/to_format_model.go b/syft/format/common/cyclonedxhelpers/to_format_model.go index ab0a83ed906..aedb3e4a6e6 100644 --- a/syft/format/common/cyclonedxhelpers/to_format_model.go +++ b/syft/format/common/cyclonedxhelpers/to_format_model.go @@ -2,7 +2,6 @@ package cyclonedxhelpers import ( "fmt" - "os" "slices" "strings" "time" @@ -10,6 +9,7 @@ import ( "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" @@ -52,9 +52,9 @@ func ToFormatModel(s sbom.SBOM) *cyclonedx.BOM { if !exists { continue } - if fileMetadata.IsDir() || - fileMetadata.Mode() == os.ModeSymlink || - fileMetadata.Mode() == os.ModeSocket { + if fileMetadata.Type == stfile.TypeDirectory || + fileMetadata.Type == stfile.TypeSocket || + fileMetadata.Type == stfile.TypeSymLink { // skip dir, symlinks and sockets for the final bom continue } diff --git a/syft/format/common/cyclonedxhelpers/to_format_model_test.go b/syft/format/common/cyclonedxhelpers/to_format_model_test.go index bd7c50c10b1..7529f21e8c0 100644 --- a/syft/format/common/cyclonedxhelpers/to_format_model_test.go +++ b/syft/format/common/cyclonedxhelpers/to_format_model_test.go @@ -2,15 +2,14 @@ package cyclonedxhelpers import ( "fmt" - "os" "testing" - "time" "github.com/CycloneDX/cyclonedx-go" "github.com/google/go-cmp/cmp" "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" @@ -161,7 +160,7 @@ func Test_FileComponents(t *testing.T) { Artifacts: sbom.Artifacts{ Packages: pkg.NewCollection(p1), FileMetadata: map[file.Coordinates]file.Metadata{ - {RealPath: "/test"}: {Path: "/test", FileInfo: newMockFileInfo(false, false)}, // Embed the mock that always returns IsDir() = true + {RealPath: "/test"}: {Path: "/test", Type: stfile.TypeRegular}, }, FileDigests: map[file.Coordinates][]file.Digest{ {RealPath: "/test"}: { @@ -194,7 +193,7 @@ func Test_FileComponents(t *testing.T) { sbom: sbom.SBOM{ Artifacts: sbom.Artifacts{ FileMetadata: map[file.Coordinates]file.Metadata{ - {RealPath: "/test"}: {Path: "/test", FileInfo: newMockFileInfo(false, false)}, + {RealPath: "/test"}: {Path: "/test", Type: stfile.TypeRegular}, }, FileDigests: map[file.Coordinates][]file.Digest{ {RealPath: "/test"}: { @@ -228,7 +227,7 @@ func Test_FileComponents(t *testing.T) { sbom: sbom.SBOM{ Artifacts: sbom.Artifacts{ FileMetadata: map[file.Coordinates]file.Metadata{ - {RealPath: "/test"}: {Path: "/test", FileInfo: newMockFileInfo(false, false)}, + {RealPath: "/test"}: {Path: "/test", Type: stfile.TypeRegular}, }, FileDigests: map[file.Coordinates][]file.Digest{ {RealPath: "/test"}: { @@ -248,14 +247,14 @@ func Test_FileComponents(t *testing.T) { Artifacts: sbom.Artifacts{ FileMetadata: map[file.Coordinates]file.Metadata{ {RealPath: "/testdir"}: { - Path: "/testdir", - FileInfo: newMockFileInfo(true, false), + Path: "/testdir", + Type: stfile.TypeDirectory, }, {RealPath: "/testsym"}: { - Path: "/testsym", - FileInfo: newMockFileInfo(false, true), + Path: "/testsym", + Type: stfile.TypeSymLink, }, - {RealPath: "/test"}: {Path: "/test", FileInfo: newMockFileInfo(false, false)}, + {RealPath: "/test"}: {Path: "/test", Type: stfile.TypeRegular}, }, FileDigests: map[file.Coordinates][]file.Digest{ {RealPath: "/test"}: { @@ -311,29 +310,6 @@ type mockFileInfo struct { isSymlink bool } -func newMockFileInfo(isDir, isSym bool) mockFileInfo { - return mockFileInfo{ - isDir, - isSym, - } -} - -// Implement os.FileInfo interface methods -func (m mockFileInfo) Name() string { return "mockDir" } -func (m mockFileInfo) Size() int64 { return 0 } -func (m mockFileInfo) Mode() os.FileMode { - if m.isSymlink { - return os.ModeSymlink - } - if m.isDir { - return os.ModeDir - } - return os.ModeType -} // Mark as directory -func (m mockFileInfo) ModTime() time.Time { return time.Now() } -func (m mockFileInfo) IsDir() bool { return m.isDir } -func (m mockFileInfo) Sys() any { return nil } - func Test_toBomDescriptor(t *testing.T) { type args struct { name string From 240f782f3457461999885b50e4a914e85f0bd631 Mon Sep 17 00:00:00 2001 From: Christopher Phillips <32073428+spiffcs@users.noreply.github.com> Date: Fri, 31 Jan 2025 12:28:16 -0500 Subject: [PATCH 5/7] chore: code review sync Signed-off-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com> --- .../cyclonedxhelpers/to_format_model.go | 54 +++++++++---------- .../cyclonedxhelpers/to_format_model_test.go | 17 +++++- 2 files changed, 39 insertions(+), 32 deletions(-) diff --git a/syft/format/common/cyclonedxhelpers/to_format_model.go b/syft/format/common/cyclonedxhelpers/to_format_model.go index aedb3e4a6e6..a20a13a3cd9 100644 --- a/syft/format/common/cyclonedxhelpers/to_format_model.go +++ b/syft/format/common/cyclonedxhelpers/to_format_model.go @@ -6,7 +6,7 @@ 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" @@ -21,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() @@ -41,7 +53,6 @@ func ToFormatModel(s sbom.SBOM) *cyclonedx.BOM { // Files artifacts := s.Artifacts coordinates := s.AllCoordinates() - fileComponents := make([]cyclonedx.Component, 0) for _, coordinate := range coordinates { var metadata *file.Metadata // File Info @@ -68,19 +79,14 @@ func ToFormatModel(s sbom.SBOM) *cyclonedx.BOM { // if cdx doesn't have an algorithm for the SBOM we need to drop the components // since an empty hash field is not allowed: https://cyclonedx.org/docs/1.6/json/#components_items_hashes_items_alg - cdxHashes, err := digestsToHashes(digests) - if err != nil { - continue - } - - fileComponents = append(fileComponents, cyclonedx.Component{ + cdxHashes := digestsToHashes(digests) + components = append(components, cyclonedx.Component{ BOMRef: string(coordinate.ID()), Type: cyclonedx.ComponentTypeFile, Name: metadata.Path, - Hashes: cdxHashes, + Hashes: &cdxHashes, }) } - components = append(components, fileComponents...) cdxBOM.Components = &components dependencies := toDependencies(s.Relationships) @@ -91,40 +97,28 @@ func ToFormatModel(s sbom.SBOM) *cyclonedx.BOM { return cdxBOM } -func digestsToHashes(digests []file.Digest) (*[]cyclonedx.Hash, error) { - hashes := make([]cyclonedx.Hash, 0) +func digestsToHashes(digests []file.Digest) []cyclonedx.Hash { + var hashes []cyclonedx.Hash for _, digest := range digests { - cdxAlgo, err := toCycloneDXAlgorithm(digest.Algorithm) - if err != nil { - return nil, err + lookup := strings.ToLower(digest.Algorithm) + cdxAlgo, exists := cycloneDXValidHash[lookup] + if !exists { + continue } hashes = append(hashes, cyclonedx.Hash{ Algorithm: cdxAlgo, Value: digest.Value, }) } - return &hashes, nil + return hashes } +// Not pulling it's weight, let's just do strings to lower on the map // supported algorithm in cycloneDX as of 1.4 // "MD5", "SHA-1", "SHA-256", "SHA-384", "SHA-512", // "SHA3-256", "SHA3-384", "SHA3-512", "BLAKE2b-256", "BLAKE2b-384", "BLAKE2b-512", "BLAKE3" // syft supported digests: cmd/syft/cli/eventloop/tasks.go // MD5, SHA1, SHA256 -func toCycloneDXAlgorithm(algorithm string) (cyclonedx.HashAlgorithm, error) { - validMap := map[string]cyclonedx.HashAlgorithm{ - "sha1": cyclonedx.HashAlgoSHA1, - "md5": cyclonedx.HashAlgoMD5, - "sha256": cyclonedx.HashAlgoSHA256, - } - lookup := strings.ToLower(algorithm) - cdxAlgo, exists := validMap[lookup] - if !exists { - return "", fmt.Errorf("could not find valid cdx algorithm for %s", lookup) - } - - return cdxAlgo, nil -} func toOSComponent(distro *linux.Release) []cyclonedx.Component { if distro == nil { diff --git a/syft/format/common/cyclonedxhelpers/to_format_model_test.go b/syft/format/common/cyclonedxhelpers/to_format_model_test.go index 7529f21e8c0..936e7fc7564 100644 --- a/syft/format/common/cyclonedxhelpers/to_format_model_test.go +++ b/syft/format/common/cyclonedxhelpers/to_format_model_test.go @@ -223,7 +223,7 @@ func Test_FileComponents(t *testing.T) { }, }, { - name: "sbom coordinates that return hashes not covered by cdx are not added to the final output", + 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{ @@ -235,11 +235,24 @@ func Test_FileComponents(t *testing.T) { Algorithm: "xxh64", Value: "xyz12345", }, + { + Algorithm: "sha256", + Value: "xyz678910", + }, }, }, }, }, - want: []cyclonedx.Component{}, + 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", From 1560c020958d6914b274be40d22b4052f51fd2df Mon Sep 17 00:00:00 2001 From: Christopher Phillips <32073428+spiffcs@users.noreply.github.com> Date: Fri, 31 Jan 2025 14:04:40 -0500 Subject: [PATCH 6/7] chore: remove comments Signed-off-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com> --- syft/format/common/cyclonedxhelpers/to_format_model.go | 9 --------- 1 file changed, 9 deletions(-) diff --git a/syft/format/common/cyclonedxhelpers/to_format_model.go b/syft/format/common/cyclonedxhelpers/to_format_model.go index a20a13a3cd9..4f4a8ccbd52 100644 --- a/syft/format/common/cyclonedxhelpers/to_format_model.go +++ b/syft/format/common/cyclonedxhelpers/to_format_model.go @@ -77,8 +77,6 @@ func ToFormatModel(s sbom.SBOM) *cyclonedx.BOM { digests = digestsForLocation } - // if cdx doesn't have an algorithm for the SBOM we need to drop the components - // since an empty hash field is not allowed: https://cyclonedx.org/docs/1.6/json/#components_items_hashes_items_alg cdxHashes := digestsToHashes(digests) components = append(components, cyclonedx.Component{ BOMRef: string(coordinate.ID()), @@ -113,13 +111,6 @@ func digestsToHashes(digests []file.Digest) []cyclonedx.Hash { return hashes } -// Not pulling it's weight, let's just do strings to lower on the map -// supported algorithm in cycloneDX as of 1.4 -// "MD5", "SHA-1", "SHA-256", "SHA-384", "SHA-512", -// "SHA3-256", "SHA3-384", "SHA3-512", "BLAKE2b-256", "BLAKE2b-384", "BLAKE2b-512", "BLAKE3" -// syft supported digests: cmd/syft/cli/eventloop/tasks.go -// MD5, SHA1, SHA256 - func toOSComponent(distro *linux.Release) []cyclonedx.Component { if distro == nil { return []cyclonedx.Component{} From ee76d07304f9b9d26c6ef5336f90442dba3bec83 Mon Sep 17 00:00:00 2001 From: Christopher Phillips <32073428+spiffcs@users.noreply.github.com> Date: Fri, 31 Jan 2025 14:36:12 -0500 Subject: [PATCH 7/7] chore: remove old info Signed-off-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com> --- syft/format/common/cyclonedxhelpers/to_format_model_test.go | 6 ------ 1 file changed, 6 deletions(-) diff --git a/syft/format/common/cyclonedxhelpers/to_format_model_test.go b/syft/format/common/cyclonedxhelpers/to_format_model_test.go index 936e7fc7564..74f0ffbf3c0 100644 --- a/syft/format/common/cyclonedxhelpers/to_format_model_test.go +++ b/syft/format/common/cyclonedxhelpers/to_format_model_test.go @@ -317,12 +317,6 @@ func Test_FileComponents(t *testing.T) { } } -// mockFileInfo is a test struct that simulates fs.FileInfo -type mockFileInfo struct { - isDir bool - isSymlink bool -} - func Test_toBomDescriptor(t *testing.T) { type args struct { name string