Skip to content

Commit

Permalink
tests: update tests with PR feedback and overhaul file format model
Browse files Browse the repository at this point in the history
Signed-off-by: Christopher Phillips <[email protected]>
  • Loading branch information
spiffcs committed Jan 30, 2025
1 parent c8aea92 commit 97359b1
Show file tree
Hide file tree
Showing 2 changed files with 185 additions and 17 deletions.
52 changes: 38 additions & 14 deletions syft/format/common/cyclonedxhelpers/to_format_model.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package cyclonedxhelpers

import (
"fmt"
"os"
"slices"
"strings"
"time"
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -75,31 +91,39 @@ 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
// "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 {
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 {
Expand Down
150 changes: 147 additions & 3 deletions syft/format/common/cyclonedxhelpers/to_format_model_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ package cyclonedxhelpers

import (
"fmt"
"os"
"testing"
"time"

"github.com/CycloneDX/cyclonedx-go"
"github.com/google/go-cmp/cmp"
Expand Down Expand Up @@ -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"}: {
Expand All @@ -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) {
Expand All @@ -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
Expand Down

0 comments on commit 97359b1

Please sign in to comment.