mirror of
https://github.com/anchore/syft.git
synced 2026-10-30 22:12:28 +01:00
fix: spdx and cyclonedx
Signed-off-by: Keith Zantow <kzantow@gmail.com>
This commit is contained in:
@@ -6,6 +6,8 @@ import (
|
||||
"crypto/sha1"
|
||||
"fmt"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -52,7 +54,8 @@ func ToFormatModel(s sbom.SBOM) *spdx.Document {
|
||||
rels := relationship.NewIndex(s.Relationships...)
|
||||
packages, otherLicenses := toPackages(rels, s.Artifacts.Packages, s)
|
||||
|
||||
files, archiveRelationships := toFiles(s)
|
||||
sourceFile := toSourceFile(s)
|
||||
files, archiveRelationships := toFiles(s, sourceFile)
|
||||
allRelationships := append(toRelationships(rels.All()), archiveRelationships...)
|
||||
|
||||
// for valid SPDX we need a document describes relationship
|
||||
@@ -62,7 +65,7 @@ func ToFormatModel(s sbom.SBOM) *spdx.Document {
|
||||
if rootPackage != nil {
|
||||
describesID = rootPackage.PackageSPDXIdentifier
|
||||
|
||||
allRelationships = append(allRelationships, toRootRelationships(rootPackage, packages, files, allRelationships)...)
|
||||
allRelationships = append(allRelationships, toRootRelationships(rootPackage, sourceFile, packages, files, allRelationships)...)
|
||||
|
||||
// append the root package
|
||||
packages = append(packages, rootPackage)
|
||||
@@ -160,38 +163,58 @@ func ToFormatModel(s sbom.SBOM) *spdx.Document {
|
||||
}
|
||||
}
|
||||
|
||||
// toRootRelationships relates the root package to each package and file nothing else CONTAINS: what is
|
||||
// within an archive is reached only through the archive's file, and an owned file through its package.
|
||||
func toRootRelationships(rootPackage *spdx.Package, packages []*spdx.Package, files []*spdx.File, relationships []*spdx.Relationship) (out []*spdx.Relationship) {
|
||||
contained := map[spdx.ElementID]bool{}
|
||||
// toRootRelationships relates the root package to every package, and to each file nothing else CONTAINS: what is
|
||||
// within an archive is reached only through the archive's file, and an owned file through its package. When the
|
||||
// source is itself a file, the root package DESCRIBES it, and a scanned directory CONTAINS those files instead. A
|
||||
// package containing files must have analyzed them, so the root package then gets a verification code over them.
|
||||
func toRootRelationships(rootPackage *spdx.Package, sourceFile *file.Coordinates, packages []*spdx.Package, files []*spdx.File, relationships []*spdx.Relationship) (out []*spdx.Relationship) {
|
||||
contained := map[spdx.ElementID]struct{}{}
|
||||
for _, r := range relationships {
|
||||
if r.Relationship == string(helpers.ContainsRelationship) {
|
||||
contained[r.RefB.ElementRefID] = true
|
||||
contained[r.RefB.ElementRefID] = struct{}{}
|
||||
}
|
||||
}
|
||||
relate := func(a spdx.ElementID, typ helpers.RelationshipType, b spdx.ElementID) {
|
||||
out = append(out, &spdx.Relationship{
|
||||
RefA: spdx.DocElementID{ElementRefID: a},
|
||||
Relationship: string(typ),
|
||||
RefB: spdx.DocElementID{ElementRefID: b},
|
||||
})
|
||||
}
|
||||
|
||||
fileContainer := rootPackage.PackageSPDXIdentifier
|
||||
if sourceFile != nil {
|
||||
id := toSPDXID(*sourceFile)
|
||||
relate(rootPackage.PackageSPDXIdentifier, helpers.DescribesRelationship, id)
|
||||
contained[id] = struct{}{}
|
||||
if isScanRoot(*sourceFile) {
|
||||
fileContainer = id
|
||||
}
|
||||
}
|
||||
|
||||
var ids []spdx.ElementID
|
||||
for _, p := range packages {
|
||||
ids = append(ids, p.PackageSPDXIdentifier)
|
||||
relate(rootPackage.PackageSPDXIdentifier, helpers.ContainsRelationship, p.PackageSPDXIdentifier)
|
||||
}
|
||||
var sha1s []string
|
||||
for _, f := range files {
|
||||
ids = append(ids, f.FileSPDXIdentifier)
|
||||
}
|
||||
for _, id := range ids {
|
||||
if contained[id] {
|
||||
if _, has := contained[f.FileSPDXIdentifier]; has {
|
||||
continue
|
||||
}
|
||||
out = append(out, &spdx.Relationship{
|
||||
RefA: spdx.DocElementID{
|
||||
ElementRefID: rootPackage.PackageSPDXIdentifier,
|
||||
},
|
||||
Relationship: string(helpers.ContainsRelationship),
|
||||
RefB: spdx.DocElementID{
|
||||
ElementRefID: id,
|
||||
},
|
||||
})
|
||||
relate(fileContainer, helpers.ContainsRelationship, f.FileSPDXIdentifier)
|
||||
if fileContainer != rootPackage.PackageSPDXIdentifier {
|
||||
continue
|
||||
}
|
||||
for _, c := range f.Checksums {
|
||||
if c.Algorithm == spdx.SHA1 {
|
||||
sha1s = append(sha1s, c.Value)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
|
||||
rootPackage.PackageVerificationCode = toVerificationCode(sha1s)
|
||||
rootPackage.FilesAnalyzed = rootPackage.PackageVerificationCode != nil
|
||||
return out
|
||||
}
|
||||
|
||||
//nolint:funlen
|
||||
@@ -728,10 +751,19 @@ const layerIDCommentPrefix = "layerID: "
|
||||
|
||||
// toFiles describes every file, including each archive holding one. A file within an archive is named by
|
||||
// its path in that archive, and the archive CONTAINS it.
|
||||
func toFiles(s sbom.SBOM) (results []*spdx.File, archiveRelationships []*spdx.Relationship) {
|
||||
func toFiles(s sbom.SBOM, sourceFile *file.Coordinates) (results []*spdx.File, archiveRelationships []*spdx.Relationship) {
|
||||
_, coordinateSorter := formatInternal.GetLocationSorters(s)
|
||||
top, entries := formatInternal.ArchiveEntries(s.AllCoordinates(), coordinateSorter)
|
||||
|
||||
// a file holding entries is an archive, whatever its content starts with (such as a launch script)
|
||||
describe := func(c file.Coordinates, name string) *spdx.File {
|
||||
f := toFile(s.Artifacts, c, name)
|
||||
if len(entries[c]) > 0 {
|
||||
f.FileTypes = withArchiveType(f.FileTypes)
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
var addEntries func(archive file.Coordinates)
|
||||
addEntries = func(archive file.Coordinates) {
|
||||
for _, c := range entries[archive] {
|
||||
@@ -740,7 +772,7 @@ func toFiles(s sbom.SBOM) (results []*spdx.File, archiveRelationships []*spdx.Re
|
||||
Relationship: string(helpers.ContainsRelationship),
|
||||
RefB: spdx.DocElementID{ElementRefID: toSPDXID(c)},
|
||||
})
|
||||
results = append(results, toFile(s.Artifacts, c, strings.TrimPrefix(c.LocalPath(), "/")))
|
||||
results = append(results, describe(c, strings.TrimPrefix(c.LocalPath(), "/")))
|
||||
addEntries(c)
|
||||
}
|
||||
}
|
||||
@@ -750,7 +782,24 @@ func toFiles(s sbom.SBOM) (results []*spdx.File, archiveRelationships []*spdx.Re
|
||||
log.Debugf("unable to convert relative path '%s' to absolute path: %s", c.RealPath, err)
|
||||
name = c.RealPath
|
||||
}
|
||||
results = append(results, toFile(s.Artifacts, c, name))
|
||||
var archive *source.FileMetadata
|
||||
if isScanRoot(c) {
|
||||
// the scanned root is the root package, unless it is the file the source is
|
||||
if sourceFile == nil || c != *sourceFile {
|
||||
continue
|
||||
}
|
||||
name = "."
|
||||
if m, ok := s.Source.Metadata.(source.FileMetadata); ok {
|
||||
archive = &m
|
||||
name = filepath.Base(m.Path)
|
||||
}
|
||||
}
|
||||
f := describe(c, name)
|
||||
if archive != nil {
|
||||
// an extracted archive is the archive file, not the directory it was extracted to
|
||||
f.FileTypes = withArchiveType(toFileTypes(&file.Metadata{MIMEType: archive.MIMEType}))
|
||||
}
|
||||
results = append(results, f)
|
||||
addEntries(c)
|
||||
}
|
||||
|
||||
@@ -764,6 +813,47 @@ func toFiles(s sbom.SBOM) (results []*spdx.File, archiveRelationships []*spdx.Re
|
||||
return results, archiveRelationships
|
||||
}
|
||||
|
||||
// toSourceFile finds the file the scanned source is: a directory, or an extracted archive, is the scanned root,
|
||||
// while a single file is indexed beside its parent directory, which was not scanned.
|
||||
func toSourceFile(s sbom.SBOM) *file.Coordinates {
|
||||
var top []file.Coordinates
|
||||
for _, c := range s.AllCoordinates() {
|
||||
if c.FullNestedPath() == c.RealPath {
|
||||
top = append(top, c)
|
||||
}
|
||||
}
|
||||
match := isScanRoot
|
||||
switch m := s.Source.Metadata.(type) {
|
||||
case source.DirectoryMetadata:
|
||||
case source.FileMetadata:
|
||||
name := filepath.Base(m.Path)
|
||||
single := func(c file.Coordinates) bool { return strings.TrimPrefix(c.RealPath, "/") == name }
|
||||
if slices.ContainsFunc(top, single) {
|
||||
match = single
|
||||
}
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
i := slices.IndexFunc(top, match)
|
||||
if i < 0 {
|
||||
return nil
|
||||
}
|
||||
return &top[i]
|
||||
}
|
||||
|
||||
// withArchiveType adds the archive file type, in place of other
|
||||
func withArchiveType(types []string) []string {
|
||||
if slices.Contains(types, string(helpers.ArchiveFileType)) {
|
||||
return types
|
||||
}
|
||||
types = slices.DeleteFunc(types, func(t string) bool { return t == string(helpers.OtherFileType) })
|
||||
return append(types, string(helpers.ArchiveFileType))
|
||||
}
|
||||
|
||||
func isScanRoot(c file.Coordinates) bool {
|
||||
return c.RealPath == "" || c.RealPath == "/"
|
||||
}
|
||||
|
||||
func toFile(artifacts sbom.Artifacts, c file.Coordinates, name string) *spdx.File {
|
||||
var metadata *file.Metadata
|
||||
if metadataForLocation, exists := artifacts.FileMetadata[c]; exists {
|
||||
@@ -879,7 +969,7 @@ func newPackageVerificationCode(rels *relationship.Index, p pkg.Package, sbom sb
|
||||
// if a sha1 for a file is provided, then the validator will fail if the package does not have
|
||||
// a package verification code
|
||||
coordinates := rels.Coordinates(p, artifact.ContainsRelationship)
|
||||
var digests []file.Digest
|
||||
var sha1s []string
|
||||
for _, c := range coordinates {
|
||||
digest := sbom.Artifacts.FileDigests[c]
|
||||
if len(digest) == 0 {
|
||||
@@ -893,27 +983,23 @@ func newPackageVerificationCode(rels *relationship.Index, p pkg.Package, sbom sb
|
||||
break
|
||||
}
|
||||
}
|
||||
digests = append(digests, d)
|
||||
sha1s = append(sha1s, d.Value)
|
||||
}
|
||||
return toVerificationCode(sha1s)
|
||||
}
|
||||
|
||||
if len(digests) == 0 {
|
||||
func toVerificationCode(sha1s []string) *spdx.PackageVerificationCode {
|
||||
if len(sha1s) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// sort templist in ascending order by SHA1 value
|
||||
sort.SliceStable(digests, func(i, j int) bool {
|
||||
return digests[i].Value < digests[j].Value
|
||||
})
|
||||
sort.Strings(sha1s)
|
||||
|
||||
// filelist = templist with "/n"s removed. /* ordered sequence of SHA1 values with no separators
|
||||
var b strings.Builder
|
||||
for _, digest := range digests {
|
||||
b.WriteString(digest.Value)
|
||||
}
|
||||
|
||||
//nolint:gosec
|
||||
hasher := sha1.New()
|
||||
_, _ = hasher.Write([]byte(b.String()))
|
||||
_, _ = hasher.Write([]byte(strings.Join(sha1s, "")))
|
||||
return &spdx.PackageVerificationCode{
|
||||
// 7.9.1: Package Verification Code Value
|
||||
// Cardinality: mandatory, one
|
||||
@@ -934,10 +1020,6 @@ func trimPatchVersion(semver string) string {
|
||||
// spdx requires that the file name field is a relative filename
|
||||
// with the root of the package archive or directory
|
||||
func convertAbsoluteToRelative(absPath string) (string, error) {
|
||||
// the root itself; an empty file name is not valid SPDX (tag-value omits it and cannot read it back)
|
||||
if absPath == "" || absPath == "/" {
|
||||
return ".", nil
|
||||
}
|
||||
// Ensure the absolute path is absolute (although it should already be)
|
||||
if !path.IsAbs(absPath) {
|
||||
// already relative
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -557,7 +558,7 @@ func Test_toFiles(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
in sbom.SBOM
|
||||
want spdx.File
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
name: "File paths are converted to relative in final SPDX collection",
|
||||
@@ -585,31 +586,56 @@ func Test_toFiles(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
want: spdx.File{
|
||||
FileName: "some/path",
|
||||
want: []string{"some/path"},
|
||||
},
|
||||
{
|
||||
name: "the scanned root is the root package, not a file",
|
||||
in: sbom.SBOM{Artifacts: sbom.Artifacts{FileMetadata: map[file.Coordinates]file.Metadata{
|
||||
{RealPath: ""}: {Type: stfile.TypeDirectory},
|
||||
{RealPath: "/etc"}: {Type: stfile.TypeDirectory},
|
||||
}}},
|
||||
want: []string{"etc"},
|
||||
},
|
||||
{
|
||||
name: "a scanned directory is a file",
|
||||
in: sbom.SBOM{
|
||||
Source: source.Description{Metadata: source.DirectoryMetadata{Path: "/dir"}},
|
||||
Artifacts: sbom.Artifacts{FileMetadata: map[file.Coordinates]file.Metadata{
|
||||
{RealPath: ""}: {Type: stfile.TypeDirectory},
|
||||
{RealPath: "/etc"}: {Type: stfile.TypeDirectory},
|
||||
}},
|
||||
},
|
||||
want: []string{".", "etc"},
|
||||
},
|
||||
{
|
||||
name: "the scanned root has a name",
|
||||
in: sbom.SBOM{Artifacts: sbom.Artifacts{FileMetadata: map[file.Coordinates]file.Metadata{
|
||||
{RealPath: ""}: {Type: stfile.TypeDirectory},
|
||||
}}},
|
||||
want: spdx.File{FileName: "."},
|
||||
name: "a scanned file is indexed beside its parent directory, which was not scanned",
|
||||
in: sbom.SBOM{
|
||||
Source: source.Description{Metadata: source.FileMetadata{Path: "/dir/go.mod"}},
|
||||
Artifacts: sbom.Artifacts{FileMetadata: map[file.Coordinates]file.Metadata{
|
||||
{RealPath: ""}: {Type: stfile.TypeDirectory},
|
||||
{RealPath: "/go.mod"}: {Type: stfile.TypeRegular},
|
||||
}},
|
||||
},
|
||||
want: []string{"go.mod"},
|
||||
},
|
||||
{
|
||||
name: "the absolute root has a name",
|
||||
name: "the absolute root is the root package, not a file",
|
||||
in: sbom.SBOM{Artifacts: sbom.Artifacts{FileMetadata: map[file.Coordinates]file.Metadata{
|
||||
{RealPath: "/"}: {Type: stfile.TypeDirectory},
|
||||
{RealPath: "/"}: {Type: stfile.TypeDirectory},
|
||||
{RealPath: "/etc"}: {Type: stfile.TypeDirectory},
|
||||
}}},
|
||||
want: spdx.File{FileName: "."},
|
||||
want: []string{"etc"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
files, _ := toFiles(test.in)
|
||||
require.Len(t, files, 1)
|
||||
assert.Equal(t, test.want.FileName, files[0].FileName)
|
||||
files, _ := toFiles(test.in, toSourceFile(test.in))
|
||||
var names []string
|
||||
for _, f := range files {
|
||||
names = append(names, f.FileName)
|
||||
}
|
||||
assert.Equal(t, test.want, names)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1179,93 +1205,279 @@ func Test_nestedArchiveFiles(t *testing.T) {
|
||||
inB := file.Coordinates{RealPath: "/app.war", FullPath: "/app.war!/WEB-INF/lib/b.jar!/odd:na%me.txt", FileSystemID: "sha256:layer"}
|
||||
owned := file.NewCoordinates("/usr/bin/tool", "sha256:layer")
|
||||
a := file.Coordinates{RealPath: "/app.war", FullPath: "/app.war!/WEB-INF/lib/a.jar", FileSystemID: "sha256:layer"}
|
||||
war := file.NewCoordinates("/app.war", "sha256:layer")
|
||||
b := file.Coordinates{RealPath: "/app.war", FullPath: "/app.war!/WEB-INF/lib/b.jar", FileSystemID: "sha256:layer"}
|
||||
topPkg := pkg.Package{Name: "top"}
|
||||
topPkg.SetID()
|
||||
nestedPkg := pkg.Package{Name: "nested"}
|
||||
nestedPkg.SetID()
|
||||
s := sbom.SBOM{
|
||||
Artifacts: sbom.Artifacts{
|
||||
Packages: pkg.NewCollection(topPkg, nestedPkg),
|
||||
FileMetadata: map[file.Coordinates]file.Metadata{top: {}, inA: {}, inB: {}, owned: {}},
|
||||
|
||||
sources := []source.Description{
|
||||
{Name: "img", Metadata: source.ImageMetadata{UserInput: "img:latest", ManifestDigest: "sha256:abc"}},
|
||||
{Name: "dir", Metadata: source.DirectoryMetadata{Path: "/dir"}},
|
||||
}
|
||||
for _, src := range sources {
|
||||
t.Run(src.Name, func(t *testing.T) {
|
||||
s := sbom.SBOM{
|
||||
Source: src,
|
||||
Artifacts: sbom.Artifacts{
|
||||
Packages: pkg.NewCollection(topPkg, nestedPkg),
|
||||
FileMetadata: map[file.Coordinates]file.Metadata{top: {}, inA: {}, inB: {}, owned: {}},
|
||||
},
|
||||
Relationships: []artifact.Relationship{
|
||||
{From: topPkg, To: owned, Type: artifact.ContainsRelationship},
|
||||
{From: a, To: nestedPkg, Type: artifact.ContainsRelationship},
|
||||
},
|
||||
}
|
||||
|
||||
doc := ToFormatModel(s)
|
||||
|
||||
// each archive holding a described file is described too, named by its path in its own container
|
||||
names := map[spdx.ElementID]string{}
|
||||
for _, f := range doc.Files {
|
||||
names[f.FileSPDXIdentifier] = f.FileName
|
||||
assert.Equal(t, "layerID: sha256:layer", f.FileComment)
|
||||
}
|
||||
assert.Equal(t, map[spdx.ElementID]string{
|
||||
toSPDXID(top): "etc/os-release",
|
||||
toSPDXID(owned): "usr/bin/tool",
|
||||
toSPDXID(war): "app.war",
|
||||
toSPDXID(a): "WEB-INF/lib/a.jar",
|
||||
toSPDXID(b): "WEB-INF/lib/b.jar",
|
||||
toSPDXID(inA): "META-INF/MANIFEST.MF",
|
||||
toSPDXID(inB): "odd:na%me.txt",
|
||||
}, names)
|
||||
|
||||
var described []spdx.ElementID
|
||||
var contains [][2]spdx.ElementID
|
||||
for _, r := range doc.Relationships {
|
||||
switch r.Relationship {
|
||||
case string(helpers.DescribesRelationship):
|
||||
described = append(described, r.RefB.ElementRefID)
|
||||
case string(helpers.ContainsRelationship):
|
||||
contains = append(contains, [2]spdx.ElementID{r.RefA.ElementRefID, r.RefB.ElementRefID})
|
||||
}
|
||||
}
|
||||
rootPkg := doc.Packages[len(doc.Packages)-1]
|
||||
root := rootPkg.PackageSPDXIdentifier
|
||||
assert.Equal(t, []spdx.ElementID{root}, described)
|
||||
|
||||
// the root contains every package, but only the files nothing else does: not what is within an archive,
|
||||
// nor an owned file
|
||||
assert.ElementsMatch(t, [][2]spdx.ElementID{
|
||||
{root, toSPDXID(topPkg)},
|
||||
{root, toSPDXID(nestedPkg)},
|
||||
{root, toSPDXID(top)},
|
||||
{root, toSPDXID(war)},
|
||||
{toSPDXID(topPkg), toSPDXID(owned)},
|
||||
{toSPDXID(a), toSPDXID(nestedPkg)},
|
||||
{toSPDXID(war), toSPDXID(a)},
|
||||
{toSPDXID(war), toSPDXID(b)},
|
||||
{toSPDXID(a), toSPDXID(inA)},
|
||||
{toSPDXID(b), toSPDXID(inB)},
|
||||
}, contains)
|
||||
|
||||
// a package containing files must have analyzed them
|
||||
assert.True(t, rootPkg.FilesAnalyzed)
|
||||
assert.NotNil(t, rootPkg.PackageVerificationCode)
|
||||
|
||||
// other tools may instead have the document describe the top-level files directly
|
||||
describesFiles := ToFormatModel(s)
|
||||
for _, r := range describesFiles.Relationships {
|
||||
if r.RefA.ElementRefID == root && (r.RefB.ElementRefID == toSPDXID(top) || r.RefB.ElementRefID == toSPDXID(war)) {
|
||||
r.RefA.ElementRefID = "DOCUMENT"
|
||||
r.Relationship = string(helpers.DescribesRelationship)
|
||||
}
|
||||
}
|
||||
|
||||
for _, d := range []struct {
|
||||
name string
|
||||
doc *spdx.Document
|
||||
}{
|
||||
{name: "root contains files", doc: doc},
|
||||
{name: "document describes files", doc: describesFiles},
|
||||
} {
|
||||
t.Run(d.name, func(t *testing.T) {
|
||||
decoded, err := ToSyftModel(d.doc)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, src.Name, decoded.Source.Name)
|
||||
assert.IsType(t, src.Metadata, decoded.Source.Metadata)
|
||||
|
||||
// decoded paths are relative, as SPDX names are
|
||||
relative := func(c file.Coordinates) file.Coordinates {
|
||||
c.RealPath = strings.TrimPrefix(c.RealPath, "/")
|
||||
c.FullPath = strings.TrimPrefix(c.FullPath, "/")
|
||||
return c
|
||||
}
|
||||
var got []file.Coordinates
|
||||
for c := range decoded.Artifacts.FileMetadata {
|
||||
got = append(got, c)
|
||||
}
|
||||
assert.ElementsMatch(t, []file.Coordinates{relative(top), relative(owned), relative(war), relative(a), relative(b), relative(inA), relative(inB)}, got)
|
||||
|
||||
var gotContains []artifact.Relationship
|
||||
for _, r := range decoded.Relationships {
|
||||
_, fromFile := r.From.(file.Coordinates)
|
||||
_, toFile := r.To.(file.Coordinates)
|
||||
if fromFile && toFile {
|
||||
gotContains = append(gotContains, r)
|
||||
}
|
||||
}
|
||||
assert.ElementsMatch(t, []artifact.Relationship{
|
||||
{From: relative(war), To: relative(a), Type: artifact.ContainsRelationship},
|
||||
{From: relative(war), To: relative(b), Type: artifact.ContainsRelationship},
|
||||
{From: relative(a), To: relative(inA), Type: artifact.ContainsRelationship},
|
||||
{From: relative(b), To: relative(inB), Type: artifact.ContainsRelationship},
|
||||
}, gotContains)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_sourceFile(t *testing.T) {
|
||||
root := file.NewCoordinates("", "")
|
||||
war := file.NewCoordinates("/app.war", "")
|
||||
jar := file.Coordinates{RealPath: "/app.war", FullPath: "/app.war!/a.jar"}
|
||||
etc := file.NewCoordinates("/etc/os-release", "")
|
||||
p := pkg.Package{Name: "p"}
|
||||
p.SetID()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
source source.Description
|
||||
files []file.Coordinates
|
||||
// relationships as "from type to", with the root package as "root"
|
||||
want []string
|
||||
wantAnalyzed bool
|
||||
// the file types of the file the root package describes
|
||||
wantTypes []string
|
||||
// decoded files, and the file relationships among them, as "from type to"
|
||||
wantFiles []string
|
||||
wantContains []string
|
||||
}{
|
||||
{
|
||||
name: "a scanned directory is described, and contains what is at its root",
|
||||
source: source.Description{Name: "dir", Metadata: source.DirectoryMetadata{Path: "/dir"}},
|
||||
files: []file.Coordinates{root, etc, war, jar},
|
||||
want: []string{
|
||||
"root DESCRIBES .",
|
||||
"root CONTAINS p",
|
||||
". CONTAINS etc/os-release",
|
||||
". CONTAINS app.war",
|
||||
"app.war CONTAINS a.jar",
|
||||
},
|
||||
// the scanned root decodes back to the scanned root, and holds nothing as an archive would
|
||||
wantFiles: []string{"", "etc/os-release", "app.war", "app.war!/a.jar"},
|
||||
wantContains: []string{"app.war contains app.war!/a.jar"},
|
||||
},
|
||||
Relationships: []artifact.Relationship{
|
||||
{From: topPkg, To: owned, Type: artifact.ContainsRelationship},
|
||||
{From: a, To: nestedPkg, Type: artifact.ContainsRelationship},
|
||||
{
|
||||
name: "an extracted archive is described, and contains what is at its root",
|
||||
source: source.Description{Name: "t.tar", Metadata: source.FileMetadata{Path: "/dir/t.tar"}},
|
||||
files: []file.Coordinates{root, etc},
|
||||
want: []string{
|
||||
"root DESCRIBES t.tar",
|
||||
"root CONTAINS p",
|
||||
"t.tar CONTAINS etc/os-release",
|
||||
},
|
||||
// the scanned root is the directory the archive was extracted to, but describes the archive
|
||||
wantTypes: []string{"ARCHIVE"},
|
||||
// the described archive is the scanned root, holding what is at the root
|
||||
wantFiles: []string{"", "etc/os-release"},
|
||||
},
|
||||
{
|
||||
name: "an extracted archive takes its file types from its MIME type",
|
||||
source: source.Description{Name: "t.zip", Metadata: source.FileMetadata{Path: "/dir/t.zip", MIMEType: "application/zip"}},
|
||||
files: []file.Coordinates{root, etc},
|
||||
want: []string{
|
||||
"root DESCRIBES t.zip",
|
||||
"root CONTAINS p",
|
||||
"t.zip CONTAINS etc/os-release",
|
||||
},
|
||||
wantTypes: []string{"APPLICATION", "ARCHIVE"},
|
||||
wantFiles: []string{"", "etc/os-release"},
|
||||
},
|
||||
{
|
||||
name: "a scanned file is described, but not its parent directory",
|
||||
source: source.Description{Name: "app.war", Metadata: source.FileMetadata{Path: "/dir/app.war"}},
|
||||
files: []file.Coordinates{root, war, jar},
|
||||
want: []string{
|
||||
"root DESCRIBES app.war",
|
||||
"root CONTAINS p",
|
||||
"app.war CONTAINS a.jar",
|
||||
},
|
||||
// an archive read in place is the scanned root too, so decodes as if it had been extracted
|
||||
wantFiles: []string{"", "a.jar"},
|
||||
},
|
||||
{
|
||||
name: "without the scanned root, the root package contains what is at its root",
|
||||
source: source.Description{Name: "dir", Metadata: source.DirectoryMetadata{Path: "/dir"}},
|
||||
files: []file.Coordinates{etc},
|
||||
want: []string{
|
||||
"root CONTAINS p",
|
||||
"root CONTAINS etc/os-release",
|
||||
},
|
||||
wantAnalyzed: true,
|
||||
wantFiles: []string{"etc/os-release"},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
metadata := map[file.Coordinates]file.Metadata{}
|
||||
for _, c := range tt.files {
|
||||
metadata[c] = file.Metadata{}
|
||||
}
|
||||
s := sbom.SBOM{
|
||||
Source: tt.source,
|
||||
Artifacts: sbom.Artifacts{Packages: pkg.NewCollection(p), FileMetadata: metadata},
|
||||
}
|
||||
|
||||
doc := ToFormatModel(s)
|
||||
doc := ToFormatModel(s)
|
||||
|
||||
// each archive holding a described file is described too, named by its path in its own container
|
||||
war := file.NewCoordinates("/app.war", "sha256:layer")
|
||||
b := file.Coordinates{RealPath: "/app.war", FullPath: "/app.war!/WEB-INF/lib/b.jar", FileSystemID: "sha256:layer"}
|
||||
names := map[spdx.ElementID]string{}
|
||||
for _, f := range doc.Files {
|
||||
names[f.FileSPDXIdentifier] = f.FileName
|
||||
assert.Equal(t, "layerID: sha256:layer", f.FileComment)
|
||||
rootPkg := doc.Packages[len(doc.Packages)-1]
|
||||
names := map[spdx.ElementID]string{rootPkg.PackageSPDXIdentifier: "root", toSPDXID(p): "p"}
|
||||
for _, f := range doc.Files {
|
||||
names[f.FileSPDXIdentifier] = f.FileName
|
||||
}
|
||||
var got []string
|
||||
for _, r := range doc.Relationships {
|
||||
if r.RefA.ElementRefID == "DOCUMENT" {
|
||||
continue
|
||||
}
|
||||
got = append(got, names[r.RefA.ElementRefID]+" "+r.Relationship+" "+names[r.RefB.ElementRefID])
|
||||
}
|
||||
assert.ElementsMatch(t, tt.want, got)
|
||||
assert.Equal(t, tt.wantAnalyzed, rootPkg.FilesAnalyzed)
|
||||
if tt.wantTypes != nil {
|
||||
for _, r := range doc.Relationships {
|
||||
if r.RefA.ElementRefID == rootPkg.PackageSPDXIdentifier && r.Relationship == string(helpers.DescribesRelationship) {
|
||||
i := slices.IndexFunc(doc.Files, func(f *spdx.File) bool { return f.FileSPDXIdentifier == r.RefB.ElementRefID })
|
||||
require.GreaterOrEqual(t, i, 0)
|
||||
assert.Equal(t, tt.wantTypes, doc.Files[i].FileTypes)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
decoded, err := ToSyftModel(doc)
|
||||
require.NoError(t, err)
|
||||
|
||||
var gotFiles []string
|
||||
for c := range decoded.Artifacts.FileMetadata {
|
||||
gotFiles = append(gotFiles, c.FullNestedPath())
|
||||
}
|
||||
assert.ElementsMatch(t, tt.wantFiles, gotFiles)
|
||||
|
||||
var gotContains []string
|
||||
for _, r := range decoded.Relationships {
|
||||
if from, ok := r.From.(file.Coordinates); ok {
|
||||
to, _ := r.To.(file.Coordinates)
|
||||
gotContains = append(gotContains, from.FullNestedPath()+" "+string(r.Type)+" "+to.FullNestedPath())
|
||||
}
|
||||
}
|
||||
assert.ElementsMatch(t, tt.wantContains, gotContains)
|
||||
})
|
||||
}
|
||||
assert.Equal(t, map[spdx.ElementID]string{
|
||||
toSPDXID(top): "etc/os-release",
|
||||
toSPDXID(owned): "usr/bin/tool",
|
||||
toSPDXID(war): "app.war",
|
||||
toSPDXID(a): "WEB-INF/lib/a.jar",
|
||||
toSPDXID(b): "WEB-INF/lib/b.jar",
|
||||
toSPDXID(inA): "META-INF/MANIFEST.MF",
|
||||
toSPDXID(inB): "odd:na%me.txt",
|
||||
}, names)
|
||||
|
||||
var root spdx.ElementID
|
||||
var contains [][2]spdx.ElementID
|
||||
for _, r := range doc.Relationships {
|
||||
switch r.Relationship {
|
||||
case string(helpers.DescribesRelationship):
|
||||
root = r.RefB.ElementRefID
|
||||
case string(helpers.ContainsRelationship):
|
||||
contains = append(contains, [2]spdx.ElementID{r.RefA.ElementRefID, r.RefB.ElementRefID})
|
||||
}
|
||||
}
|
||||
// the root contains only what nothing else does: not what is within an archive, nor an owned file
|
||||
assert.ElementsMatch(t, [][2]spdx.ElementID{
|
||||
{root, toSPDXID(topPkg)},
|
||||
{root, toSPDXID(top)},
|
||||
{root, toSPDXID(war)},
|
||||
{toSPDXID(topPkg), toSPDXID(owned)},
|
||||
{toSPDXID(a), toSPDXID(nestedPkg)},
|
||||
{toSPDXID(war), toSPDXID(a)},
|
||||
{toSPDXID(war), toSPDXID(b)},
|
||||
{toSPDXID(a), toSPDXID(inA)},
|
||||
{toSPDXID(b), toSPDXID(inB)},
|
||||
}, contains)
|
||||
|
||||
decoded, err := ToSyftModel(doc)
|
||||
require.NoError(t, err)
|
||||
|
||||
// decoded paths are relative, as SPDX names are
|
||||
relative := func(c file.Coordinates) file.Coordinates {
|
||||
c.RealPath = strings.TrimPrefix(c.RealPath, "/")
|
||||
c.FullPath = strings.TrimPrefix(c.FullPath, "/")
|
||||
return c
|
||||
}
|
||||
var got []file.Coordinates
|
||||
for c := range decoded.Artifacts.FileMetadata {
|
||||
got = append(got, c)
|
||||
}
|
||||
assert.ElementsMatch(t, []file.Coordinates{relative(top), relative(owned), relative(war), relative(a), relative(b), relative(inA), relative(inB)}, got)
|
||||
|
||||
var gotContains []artifact.Relationship
|
||||
for _, r := range decoded.Relationships {
|
||||
_, fromFile := r.From.(file.Coordinates)
|
||||
_, toFile := r.To.(file.Coordinates)
|
||||
if fromFile && toFile {
|
||||
gotContains = append(gotContains, r)
|
||||
}
|
||||
}
|
||||
assert.ElementsMatch(t, []artifact.Relationship{
|
||||
{From: relative(war), To: relative(a), Type: artifact.ContainsRelationship},
|
||||
{From: relative(war), To: relative(b), Type: artifact.ContainsRelationship},
|
||||
{From: relative(a), To: relative(inA), Type: artifact.ContainsRelationship},
|
||||
{From: relative(b), To: relative(inB), Type: artifact.ContainsRelationship},
|
||||
}, gotContains)
|
||||
}
|
||||
|
||||
func Test_toRootPackage_name(t *testing.T) {
|
||||
@@ -1313,3 +1525,27 @@ func Test_toRootPackage_name(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_toFiles_archiveType(t *testing.T) {
|
||||
// a file holding entries is an archive, even when its content starts like something else, such as a Spring Boot
|
||||
// executable jar's launch script
|
||||
jar := file.NewCoordinates("/app.jar", "")
|
||||
s := sbom.SBOM{Artifacts: sbom.Artifacts{FileMetadata: map[file.Coordinates]file.Metadata{
|
||||
jar: {MIMEType: "text/x-shellscript"},
|
||||
file.NewCoordinates("/plain.zip", ""): {MIMEType: "application/zip"},
|
||||
{RealPath: "/app.jar", FullPath: "/app.jar!/lib/b.jar!/c.txt"}: {MIMEType: "text/plain"},
|
||||
}}}
|
||||
|
||||
files, _ := toFiles(s, nil)
|
||||
got := map[string][]string{}
|
||||
for _, f := range files {
|
||||
got[f.FileName] = f.FileTypes
|
||||
}
|
||||
assert.Equal(t, map[string][]string{
|
||||
"app.jar": {"TEXT", "ARCHIVE"},
|
||||
"plain.zip": {"APPLICATION", "ARCHIVE"},
|
||||
// added only to hold its entry, with no metadata of its own
|
||||
"lib/b.jar": {"ARCHIVE"},
|
||||
"c.txt": {"TEXT"},
|
||||
}, got)
|
||||
}
|
||||
|
||||
@@ -38,6 +38,9 @@ func ToSyftModel(doc *spdx.Document) (*sbom.SBOM, error) {
|
||||
|
||||
spdxIDMap := make(map[string]any)
|
||||
|
||||
// found before the root package and its relationships are removed
|
||||
sourceFile := findSourceFile(doc)
|
||||
|
||||
s := &sbom.SBOM{
|
||||
Source: extractSource(spdxIDMap, doc),
|
||||
Artifacts: sbom.Artifacts{
|
||||
@@ -50,7 +53,7 @@ func ToSyftModel(doc *spdx.Document) (*sbom.SBOM, error) {
|
||||
|
||||
collectSyftPackages(s, spdxIDMap, doc)
|
||||
|
||||
collectSyftFiles(s, spdxIDMap, doc)
|
||||
collectSyftFiles(s, spdxIDMap, doc, sourceFile)
|
||||
|
||||
s.Relationships = toSyftRelationships(spdxIDMap, doc)
|
||||
|
||||
@@ -379,7 +382,32 @@ func collectSyftPackages(s *sbom.SBOM, spdxIDMap map[string]any, doc *spdx.Docum
|
||||
}
|
||||
}
|
||||
|
||||
func collectSyftFiles(s *sbom.SBOM, spdxIDMap map[string]any, doc *spdx.Document) {
|
||||
// findSourceFile returns the file the root package DESCRIBES when it CONTAINS files: an archive scanned as the source
|
||||
// is the scanned root, so what it contains is at the root rather than within an archive.
|
||||
func findSourceFile(doc *spdx.Document) spdx.ElementID {
|
||||
roots := findRootPackages(doc)
|
||||
if len(roots) != 1 {
|
||||
return ""
|
||||
}
|
||||
var described spdx.ElementID
|
||||
containers := map[spdx.ElementID]bool{}
|
||||
for _, r := range doc.Relationships {
|
||||
switch helpers.RelationshipType(r.Relationship) {
|
||||
case helpers.DescribesRelationship:
|
||||
if r.RefA.ElementRefID == roots[0].PackageSPDXIdentifier {
|
||||
described = r.RefB.ElementRefID
|
||||
}
|
||||
case helpers.ContainsRelationship:
|
||||
containers[r.RefA.ElementRefID] = true
|
||||
}
|
||||
}
|
||||
if !containers[described] {
|
||||
return ""
|
||||
}
|
||||
return described
|
||||
}
|
||||
|
||||
func collectSyftFiles(s *sbom.SBOM, spdxIDMap map[string]any, doc *spdx.Document, sourceFile spdx.ElementID) {
|
||||
files := map[spdx.ElementID]*spdx.File{}
|
||||
for _, p := range doc.Packages {
|
||||
for _, f := range p.Files {
|
||||
@@ -394,6 +422,11 @@ func collectSyftFiles(s *sbom.SBOM, spdxIDMap map[string]any, doc *spdx.Document
|
||||
for id, f := range files {
|
||||
nested.own[id] = toSyftCoordinates(f)
|
||||
}
|
||||
if c, ok := nested.own[sourceFile]; ok {
|
||||
// the scanned root
|
||||
c.RealPath = ""
|
||||
nested.own[sourceFile] = c
|
||||
}
|
||||
for _, r := range doc.Relationships {
|
||||
container, entry := r.RefA.ElementRefID, r.RefB.ElementRefID
|
||||
switch helpers.RelationshipType(r.Relationship) {
|
||||
@@ -403,7 +436,8 @@ func collectSyftFiles(s *sbom.SBOM, spdxIDMap map[string]any, doc *spdx.Document
|
||||
default:
|
||||
continue
|
||||
}
|
||||
if files[container] != nil && files[entry] != nil {
|
||||
// the scanned root holds what is at the root, not within an archive
|
||||
if files[container] != nil && files[entry] != nil && !isScanRoot(nested.own[container]) {
|
||||
nested.container[entry] = container
|
||||
}
|
||||
}
|
||||
@@ -568,7 +602,7 @@ func fileContainsRelationship(typ helpers.RelationshipType, a, b any) (artifact.
|
||||
return artifact.Relationship{}, false
|
||||
}
|
||||
container, ok := a.(file.Location)
|
||||
if !ok {
|
||||
if !ok || isScanRoot(container.Coordinates) {
|
||||
return artifact.Relationship{}, false
|
||||
}
|
||||
var entry artifact.Identifiable
|
||||
@@ -612,6 +646,10 @@ func toSyftCoordinates(f *spdx.File) file.Coordinates {
|
||||
}
|
||||
|
||||
func coordinatesFromFile(name, comment, id string) file.Coordinates {
|
||||
// the scanned root
|
||||
if name == "." {
|
||||
name = ""
|
||||
}
|
||||
c := file.NewCoordinates(name, "")
|
||||
if v, ok := strings.CutPrefix(comment, layerIDCommentPrefix); ok {
|
||||
c.FileSystemID = v
|
||||
|
||||
@@ -31,6 +31,9 @@ func ToSyftModelV3(doc *spdx.Document) (*sbom.SBOM, error) {
|
||||
|
||||
spdxMap := ptrMap[any]{}
|
||||
|
||||
// found before the root package and its relationships are removed
|
||||
sourceFile := v3findSourceFile(doc)
|
||||
|
||||
s := &sbom.SBOM{
|
||||
Source: v3extractSource(spdxMap, doc),
|
||||
Artifacts: sbom.Artifacts{
|
||||
@@ -45,7 +48,7 @@ func ToSyftModelV3(doc *spdx.Document) (*sbom.SBOM, error) {
|
||||
|
||||
v3collectSyftPackages(s, spdxMap, relationships, doc)
|
||||
|
||||
v3collectSyftFiles(s, spdxMap, doc)
|
||||
v3collectSyftFiles(s, spdxMap, doc, sourceFile)
|
||||
|
||||
s.Relationships = v3toSyftRelationships(spdxMap, doc)
|
||||
|
||||
@@ -282,7 +285,40 @@ func v3collectSyftPackages(s *sbom.SBOM, spdxMap ptrMap[any], relationships ptrM
|
||||
}
|
||||
}
|
||||
|
||||
func v3collectSyftFiles(s *sbom.SBOM, spdxMap ptrMap[any], doc *spdx.Document) {
|
||||
// v3findSourceFile returns the file the root package DESCRIBES when it CONTAINS files: an archive scanned as the
|
||||
// source is the scanned root, so what it contains is at the root rather than within an archive.
|
||||
func v3findSourceFile(doc *spdx.Document) spdx.AnyFile {
|
||||
roots := v3findRootPackages(doc)
|
||||
if len(roots) != 1 {
|
||||
return nil
|
||||
}
|
||||
var described spdx.AnyFile
|
||||
containers := ptrMap[bool]{}
|
||||
for _, r := range doc.Elements.Relationships() {
|
||||
from := r.GetFrom()
|
||||
if from == nil {
|
||||
continue
|
||||
}
|
||||
switch r.GetType() {
|
||||
case spdx.RelationshipType_Describes:
|
||||
if from == spdx.AnyElement(roots[0]) {
|
||||
for _, to := range r.GetTo() {
|
||||
if f, ok := to.(spdx.AnyFile); ok {
|
||||
described = f
|
||||
}
|
||||
}
|
||||
}
|
||||
case spdx.RelationshipType_Contains:
|
||||
containers.Set(from, true)
|
||||
}
|
||||
}
|
||||
if described == nil || !containers.Get(described) {
|
||||
return nil
|
||||
}
|
||||
return described
|
||||
}
|
||||
|
||||
func v3collectSyftFiles(s *sbom.SBOM, spdxMap ptrMap[any], doc *spdx.Document, sourceFile spdx.AnyFile) {
|
||||
nested := newNestedFiles[reflect.Value]()
|
||||
var files []spdx.AnyFile
|
||||
for _, elementList := range []spdx.ElementList{doc.Elements, doc.RootElements} {
|
||||
@@ -294,6 +330,13 @@ func v3collectSyftFiles(s *sbom.SBOM, spdxMap ptrMap[any], doc *spdx.Document) {
|
||||
files = append(files, f)
|
||||
}
|
||||
}
|
||||
if sourceFile != nil {
|
||||
if c, ok := nested.own[ptrTo(sourceFile)]; ok {
|
||||
// the scanned root
|
||||
c.RealPath = ""
|
||||
nested.own[ptrTo(sourceFile)] = c
|
||||
}
|
||||
}
|
||||
for _, r := range doc.Elements.Relationships() {
|
||||
container, ok := r.GetFrom().(spdx.AnyFile)
|
||||
if !ok || r.GetType() != spdx.RelationshipType_Contains {
|
||||
@@ -306,7 +349,7 @@ func v3collectSyftFiles(s *sbom.SBOM, spdxMap ptrMap[any], doc *spdx.Document) {
|
||||
}
|
||||
_, knownContainer := nested.own[ptrTo(container)]
|
||||
_, knownEntry := nested.own[ptrTo(entry)]
|
||||
if knownContainer && knownEntry {
|
||||
if knownContainer && knownEntry && !isScanRoot(nested.own[ptrTo(container)]) {
|
||||
nested.container[ptrTo(entry)] = ptrTo(container)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"github.com/anchore/syft/syft/format/internal/testutil"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/sbom"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
)
|
||||
|
||||
var updateSnapshot = flag.Bool("update-spdx-json", false, "update the *.golden files for spdx-json encoders")
|
||||
@@ -281,3 +282,83 @@ func defaultFormatEncoders() []sbom.FormatEncoder {
|
||||
}
|
||||
return encs
|
||||
}
|
||||
|
||||
func TestEncodeDecodeTopLevelFiles(t *testing.T) {
|
||||
// the root package contains the top-level files, or the scanned root does when it is described
|
||||
root := file.NewCoordinates("", "")
|
||||
etc := file.NewCoordinates("/etc/os-release", "")
|
||||
jar := file.Coordinates{RealPath: "/app.war", FullPath: "/app.war!/WEB-INF/lib/a.jar"}
|
||||
war := file.NewCoordinates("/app.war", "")
|
||||
tests := []struct {
|
||||
name string
|
||||
src source.Description
|
||||
files []file.Coordinates
|
||||
wantPaths []string
|
||||
wantRels []string
|
||||
}{
|
||||
{
|
||||
name: "img",
|
||||
src: source.Description{Name: "img", Metadata: source.ImageMetadata{UserInput: "img:latest", ManifestDigest: "sha256:abc"}},
|
||||
files: []file.Coordinates{etc, jar},
|
||||
wantPaths: []string{"etc/os-release", "app.war", "app.war!/WEB-INF/lib/a.jar"},
|
||||
wantRels: []string{"app.war contains app.war!/WEB-INF/lib/a.jar"},
|
||||
},
|
||||
{
|
||||
name: "dir",
|
||||
src: source.Description{Name: "dir", Metadata: source.DirectoryMetadata{Path: "dir"}},
|
||||
files: []file.Coordinates{etc, jar},
|
||||
wantPaths: []string{"etc/os-release", "app.war", "app.war!/WEB-INF/lib/a.jar"},
|
||||
wantRels: []string{"app.war contains app.war!/WEB-INF/lib/a.jar"},
|
||||
},
|
||||
{
|
||||
name: "dir-root",
|
||||
src: source.Description{Name: "dir", Metadata: source.DirectoryMetadata{Path: "dir"}},
|
||||
files: []file.Coordinates{root, etc, jar},
|
||||
wantPaths: []string{"", "etc/os-release", "app.war", "app.war!/WEB-INF/lib/a.jar"},
|
||||
wantRels: []string{"app.war contains app.war!/WEB-INF/lib/a.jar"},
|
||||
},
|
||||
{
|
||||
// a scanned archive is the scanned root, holding what is at the root
|
||||
name: "file",
|
||||
src: source.Description{Name: "app.war", Metadata: source.FileMetadata{Path: "dir/app.war"}},
|
||||
files: []file.Coordinates{war, jar},
|
||||
wantPaths: []string{"", "WEB-INF/lib/a.jar"},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
src := tt.src
|
||||
for _, enc := range defaultFormatEncoders() {
|
||||
t.Run(tt.name+"_"+enc.Version(), func(t *testing.T) {
|
||||
metadata := map[file.Coordinates]file.Metadata{}
|
||||
for _, c := range tt.files {
|
||||
metadata[c] = file.Metadata{}
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
require.NoError(t, enc.Encode(&buf, sbom.SBOM{
|
||||
Source: src,
|
||||
Artifacts: sbom.Artifacts{FileMetadata: metadata},
|
||||
}))
|
||||
|
||||
s, _, _, err := NewFormatDecoder().Decode(bytes.NewReader(buf.Bytes()))
|
||||
require.NoError(t, err, "encoded document:\n%s", buf.String())
|
||||
|
||||
assert.Equal(t, src.Name, s.Source.Name)
|
||||
assert.IsType(t, src.Metadata, s.Source.Metadata)
|
||||
|
||||
var paths []string
|
||||
for c := range s.Artifacts.FileMetadata {
|
||||
paths = append(paths, c.FullNestedPath())
|
||||
}
|
||||
assert.ElementsMatch(t, tt.wantPaths, paths)
|
||||
|
||||
var rels []string
|
||||
for _, r := range s.Relationships {
|
||||
from, _ := r.From.(file.Coordinates)
|
||||
to, _ := r.To.(file.Coordinates)
|
||||
rels = append(rels, from.FullNestedPath()+" "+string(r.Type)+" "+to.FullNestedPath())
|
||||
}
|
||||
assert.ElementsMatch(t, tt.wantRels, rels)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -231,7 +231,7 @@ func TestEncodeDecodeSourceWithoutName(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestEncodeDecodeRootDirectoryFile(t *testing.T) {
|
||||
// a directory scan with all file metadata describes the scanned root, whose relative path is empty
|
||||
// a directory scan with all file metadata includes the scanned root, which has no relative path of its own
|
||||
root := file.NewCoordinates("", "")
|
||||
etc := file.NewCoordinates("/etc", "")
|
||||
for _, enc := range defaultFormatEncoders() {
|
||||
@@ -252,7 +252,7 @@ func TestEncodeDecodeRootDirectoryFile(t *testing.T) {
|
||||
for c := range s.Artifacts.FileMetadata {
|
||||
names = append(names, c.RealPath)
|
||||
}
|
||||
assert.ElementsMatch(t, []string{".", "etc"}, names)
|
||||
assert.ElementsMatch(t, []string{"", "etc"}, names)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user