mirror of
https://github.com/anchore/syft.git
synced 2025-11-17 16:33:21 +01:00
Stabilize SPDX JSON output sorting (#1216)
This commit is contained in:
parent
0f99215b2c
commit
c2005fad8d
@ -5,7 +5,10 @@ import (
|
|||||||
"regexp"
|
"regexp"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/syft/artifact"
|
||||||
"github.com/anchore/syft/syft/formats/common/testutils"
|
"github.com/anchore/syft/syft/formats/common/testutils"
|
||||||
|
"github.com/anchore/syft/syft/sbom"
|
||||||
|
"github.com/anchore/syft/syft/source"
|
||||||
)
|
)
|
||||||
|
|
||||||
var updateSpdxJson = flag.Bool("update-spdx-json", false, "update the *.golden files for spdx-json encoders")
|
var updateSpdxJson = flag.Bool("update-spdx-json", false, "update the *.golden files for spdx-json encoders")
|
||||||
@ -30,6 +33,36 @@ func TestSPDXJSONImageEncoder(t *testing.T) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSPDXRelationshipOrder(t *testing.T) {
|
||||||
|
testImage := "image-simple"
|
||||||
|
s := testutils.ImageInput(t, testImage, testutils.FromSnapshot())
|
||||||
|
addRelationships(&s)
|
||||||
|
testutils.AssertEncoderAgainstGoldenImageSnapshot(t,
|
||||||
|
Format(),
|
||||||
|
s,
|
||||||
|
testImage,
|
||||||
|
*updateSpdxJson,
|
||||||
|
spdxJsonRedactor,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func addRelationships(s *sbom.SBOM) {
|
||||||
|
catalog := s.Artifacts.PackageCatalog.Sorted()
|
||||||
|
s.Artifacts.FileMetadata = map[source.Coordinates]source.FileMetadata{}
|
||||||
|
|
||||||
|
for _, f := range []string{"/f1", "/f2", "/d1/f3", "/d2/f4", "/z1/f5", "/a1/f6"} {
|
||||||
|
meta := source.FileMetadata{}
|
||||||
|
coords := source.Coordinates{RealPath: f}
|
||||||
|
s.Artifacts.FileMetadata[coords] = meta
|
||||||
|
|
||||||
|
s.Relationships = append(s.Relationships, artifact.Relationship{
|
||||||
|
From: catalog[0],
|
||||||
|
To: coords,
|
||||||
|
Type: artifact.ContainsRelationship,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func spdxJsonRedactor(s []byte) []byte {
|
func spdxJsonRedactor(s []byte) []byte {
|
||||||
// each SBOM reports the time it was generated, which is not useful during snapshot testing
|
// each SBOM reports the time it was generated, which is not useful during snapshot testing
|
||||||
s = regexp.MustCompile(`"created": .*`).ReplaceAll(s, []byte("redacted"))
|
s = regexp.MustCompile(`"created": .*`).ReplaceAll(s, []byte("redacted"))
|
||||||
|
|||||||
@ -0,0 +1,151 @@
|
|||||||
|
{
|
||||||
|
"SPDXID": "SPDXRef-DOCUMENT",
|
||||||
|
"name": "user-image-input",
|
||||||
|
"spdxVersion": "SPDX-2.2",
|
||||||
|
"creationInfo": {
|
||||||
|
"created": "2022-09-19T18:39:05.841331Z",
|
||||||
|
"creators": [
|
||||||
|
"Organization: Anchore, Inc",
|
||||||
|
"Tool: syft-v0.42.0-bogus"
|
||||||
|
],
|
||||||
|
"licenseListVersion": "3.18"
|
||||||
|
},
|
||||||
|
"dataLicense": "CC0-1.0",
|
||||||
|
"documentNamespace": "https://anchore.com/syft/image/user-image-input-6cf0595e-7d69-4990-aef5-8183b52023b9",
|
||||||
|
"packages": [
|
||||||
|
{
|
||||||
|
"SPDXID": "SPDXRef-2a46171f91c8d4bc",
|
||||||
|
"name": "package-1",
|
||||||
|
"licenseConcluded": "MIT",
|
||||||
|
"downloadLocation": "NOASSERTION",
|
||||||
|
"externalRefs": [
|
||||||
|
{
|
||||||
|
"referenceCategory": "SECURITY",
|
||||||
|
"referenceLocator": "cpe:2.3:*:some:package:1:*:*:*:*:*:*:*",
|
||||||
|
"referenceType": "cpe23Type"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"referenceCategory": "PACKAGE_MANAGER",
|
||||||
|
"referenceLocator": "a-purl-1",
|
||||||
|
"referenceType": "purl"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"filesAnalyzed": false,
|
||||||
|
"hasFiles": [
|
||||||
|
"SPDXRef-5265a4dde3edbf7c",
|
||||||
|
"SPDXRef-839d99ee67d9d174",
|
||||||
|
"SPDXRef-9c2f7510199b17f6",
|
||||||
|
"SPDXRef-c641caa71518099f",
|
||||||
|
"SPDXRef-c6f5b29dca12661f",
|
||||||
|
"SPDXRef-f9e49132a4b96ccd"
|
||||||
|
],
|
||||||
|
"licenseDeclared": "MIT",
|
||||||
|
"sourceInfo": "acquired package info from installed python package manifest file: /somefile-1.txt",
|
||||||
|
"versionInfo": "1.0.1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"SPDXID": "SPDXRef-ae77680e9b1d087e",
|
||||||
|
"name": "package-2",
|
||||||
|
"licenseConcluded": "NONE",
|
||||||
|
"downloadLocation": "NOASSERTION",
|
||||||
|
"externalRefs": [
|
||||||
|
{
|
||||||
|
"referenceCategory": "SECURITY",
|
||||||
|
"referenceLocator": "cpe:2.3:*:some:package:2:*:*:*:*:*:*:*",
|
||||||
|
"referenceType": "cpe23Type"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"referenceCategory": "PACKAGE_MANAGER",
|
||||||
|
"referenceLocator": "pkg:deb/debian/package-2@2.0.1",
|
||||||
|
"referenceType": "purl"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"filesAnalyzed": false,
|
||||||
|
"licenseDeclared": "NONE",
|
||||||
|
"sourceInfo": "acquired package info from DPKG DB: /somefile-2.txt",
|
||||||
|
"versionInfo": "2.0.1"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"files": [
|
||||||
|
{
|
||||||
|
"SPDXID": "SPDXRef-9c2f7510199b17f6",
|
||||||
|
"licenseConcluded": "NOASSERTION",
|
||||||
|
"fileName": "/a1/f6",
|
||||||
|
"fileTypes": [
|
||||||
|
"OTHER"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"SPDXID": "SPDXRef-c6f5b29dca12661f",
|
||||||
|
"licenseConcluded": "NOASSERTION",
|
||||||
|
"fileName": "/d1/f3",
|
||||||
|
"fileTypes": [
|
||||||
|
"OTHER"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"SPDXID": "SPDXRef-c641caa71518099f",
|
||||||
|
"licenseConcluded": "NOASSERTION",
|
||||||
|
"fileName": "/d2/f4",
|
||||||
|
"fileTypes": [
|
||||||
|
"OTHER"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"SPDXID": "SPDXRef-5265a4dde3edbf7c",
|
||||||
|
"licenseConcluded": "NOASSERTION",
|
||||||
|
"fileName": "/f1",
|
||||||
|
"fileTypes": [
|
||||||
|
"OTHER"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"SPDXID": "SPDXRef-f9e49132a4b96ccd",
|
||||||
|
"licenseConcluded": "NOASSERTION",
|
||||||
|
"fileName": "/f2",
|
||||||
|
"fileTypes": [
|
||||||
|
"OTHER"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"SPDXID": "SPDXRef-839d99ee67d9d174",
|
||||||
|
"licenseConcluded": "NOASSERTION",
|
||||||
|
"fileName": "/z1/f5",
|
||||||
|
"fileTypes": [
|
||||||
|
"OTHER"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"relationships": [
|
||||||
|
{
|
||||||
|
"spdxElementId": "SPDXRef-2a46171f91c8d4bc",
|
||||||
|
"relationshipType": "CONTAINS",
|
||||||
|
"relatedSpdxElement": "SPDXRef-5265a4dde3edbf7c"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"spdxElementId": "SPDXRef-2a46171f91c8d4bc",
|
||||||
|
"relationshipType": "CONTAINS",
|
||||||
|
"relatedSpdxElement": "SPDXRef-839d99ee67d9d174"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"spdxElementId": "SPDXRef-2a46171f91c8d4bc",
|
||||||
|
"relationshipType": "CONTAINS",
|
||||||
|
"relatedSpdxElement": "SPDXRef-9c2f7510199b17f6"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"spdxElementId": "SPDXRef-2a46171f91c8d4bc",
|
||||||
|
"relationshipType": "CONTAINS",
|
||||||
|
"relatedSpdxElement": "SPDXRef-c641caa71518099f"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"spdxElementId": "SPDXRef-2a46171f91c8d4bc",
|
||||||
|
"relationshipType": "CONTAINS",
|
||||||
|
"relatedSpdxElement": "SPDXRef-c6f5b29dca12661f"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"spdxElementId": "SPDXRef-2a46171f91c8d4bc",
|
||||||
|
"relationshipType": "CONTAINS",
|
||||||
|
"relatedSpdxElement": "SPDXRef-f9e49132a4b96ccd"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@ -22,6 +22,8 @@ import (
|
|||||||
func toFormatModel(s sbom.SBOM) *model.Document {
|
func toFormatModel(s sbom.SBOM) *model.Document {
|
||||||
name, namespace := spdxhelpers.DocumentNameAndNamespace(s.Source)
|
name, namespace := spdxhelpers.DocumentNameAndNamespace(s.Source)
|
||||||
|
|
||||||
|
relationships := s.RelationshipsSorted()
|
||||||
|
|
||||||
return &model.Document{
|
return &model.Document{
|
||||||
Element: model.Element{
|
Element: model.Element{
|
||||||
SPDXID: model.ElementID("DOCUMENT").String(),
|
SPDXID: model.ElementID("DOCUMENT").String(),
|
||||||
@ -39,9 +41,9 @@ func toFormatModel(s sbom.SBOM) *model.Document {
|
|||||||
},
|
},
|
||||||
DataLicense: "CC0-1.0",
|
DataLicense: "CC0-1.0",
|
||||||
DocumentNamespace: namespace,
|
DocumentNamespace: namespace,
|
||||||
Packages: toPackages(s.Artifacts.PackageCatalog, s.Relationships),
|
Packages: toPackages(s.Artifacts.PackageCatalog, relationships),
|
||||||
Files: toFiles(s),
|
Files: toFiles(s),
|
||||||
Relationships: toRelationships(s.Relationships),
|
Relationships: toRelationships(relationships),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -113,8 +115,8 @@ func fileIDsForPackage(packageSpdxID string, relationships []artifact.Relationsh
|
|||||||
}
|
}
|
||||||
|
|
||||||
from := model.ElementID(relationship.From.ID()).String()
|
from := model.ElementID(relationship.From.ID()).String()
|
||||||
to := model.ElementID(relationship.To.ID()).String()
|
|
||||||
if from == packageSpdxID {
|
if from == packageSpdxID {
|
||||||
|
to := model.ElementID(relationship.To.ID()).String()
|
||||||
fileIDs = append(fileIDs, to)
|
fileIDs = append(fileIDs, to)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -125,7 +127,7 @@ func toFiles(s sbom.SBOM) []model.File {
|
|||||||
results := make([]model.File, 0)
|
results := make([]model.File, 0)
|
||||||
artifacts := s.Artifacts
|
artifacts := s.Artifacts
|
||||||
|
|
||||||
for _, coordinates := range sbom.AllCoordinates(s) {
|
for _, coordinates := range s.AllCoordinates() {
|
||||||
var metadata *source.FileMetadata
|
var metadata *source.FileMetadata
|
||||||
if metadataForLocation, exists := artifacts.FileMetadata[coordinates]; exists {
|
if metadataForLocation, exists := artifacts.FileMetadata[coordinates]; exists {
|
||||||
metadata = &metadataForLocation
|
metadata = &metadataForLocation
|
||||||
@ -160,6 +162,9 @@ func toFiles(s sbom.SBOM) []model.File {
|
|||||||
|
|
||||||
// sort by real path then virtual path to ensure the result is stable across multiple runs
|
// sort by real path then virtual path to ensure the result is stable across multiple runs
|
||||||
sort.SliceStable(results, func(i, j int) bool {
|
sort.SliceStable(results, func(i, j int) bool {
|
||||||
|
if results[i].FileName == results[j].FileName {
|
||||||
|
return results[i].SPDXID < results[j].SPDXID
|
||||||
|
}
|
||||||
return results[i].FileName < results[j].FileName
|
return results[i].FileName < results[j].FileName
|
||||||
})
|
})
|
||||||
return results
|
return results
|
||||||
|
|||||||
@ -93,7 +93,7 @@ func toFile(s sbom.SBOM) []model.File {
|
|||||||
results := make([]model.File, 0)
|
results := make([]model.File, 0)
|
||||||
artifacts := s.Artifacts
|
artifacts := s.Artifacts
|
||||||
|
|
||||||
for _, coordinates := range sbom.AllCoordinates(s) {
|
for _, coordinates := range s.AllCoordinates() {
|
||||||
var metadata *source.FileMetadata
|
var metadata *source.FileMetadata
|
||||||
if metadataForLocation, exists := artifacts.FileMetadata[coordinates]; exists {
|
if metadataForLocation, exists := artifacts.FileMetadata[coordinates]; exists {
|
||||||
metadata = &metadataForLocation
|
metadata = &metadataForLocation
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
package sbom
|
package sbom
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"sort"
|
||||||
|
|
||||||
"github.com/anchore/syft/syft/artifact"
|
"github.com/anchore/syft/syft/artifact"
|
||||||
"github.com/anchore/syft/syft/file"
|
"github.com/anchore/syft/syft/file"
|
||||||
"github.com/anchore/syft/syft/linux"
|
"github.com/anchore/syft/syft/linux"
|
||||||
@ -31,21 +33,35 @@ type Descriptor struct {
|
|||||||
Configuration interface{}
|
Configuration interface{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func AllCoordinates(sbom SBOM) []source.Coordinates {
|
func (s SBOM) RelationshipsSorted() []artifact.Relationship {
|
||||||
|
relationships := s.Relationships
|
||||||
|
sort.SliceStable(relationships, func(i, j int) bool {
|
||||||
|
if relationships[i].From.ID() == relationships[j].From.ID() {
|
||||||
|
if relationships[i].To.ID() == relationships[j].To.ID() {
|
||||||
|
return relationships[i].Type < relationships[j].Type
|
||||||
|
}
|
||||||
|
return relationships[i].To.ID() < relationships[j].To.ID()
|
||||||
|
}
|
||||||
|
return relationships[i].From.ID() < relationships[j].From.ID()
|
||||||
|
})
|
||||||
|
return relationships
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s SBOM) AllCoordinates() []source.Coordinates {
|
||||||
set := source.NewCoordinateSet()
|
set := source.NewCoordinateSet()
|
||||||
for coordinates := range sbom.Artifacts.FileMetadata {
|
for coordinates := range s.Artifacts.FileMetadata {
|
||||||
set.Add(coordinates)
|
set.Add(coordinates)
|
||||||
}
|
}
|
||||||
for coordinates := range sbom.Artifacts.FileContents {
|
for coordinates := range s.Artifacts.FileContents {
|
||||||
set.Add(coordinates)
|
set.Add(coordinates)
|
||||||
}
|
}
|
||||||
for coordinates := range sbom.Artifacts.FileClassifications {
|
for coordinates := range s.Artifacts.FileClassifications {
|
||||||
set.Add(coordinates)
|
set.Add(coordinates)
|
||||||
}
|
}
|
||||||
for coordinates := range sbom.Artifacts.FileDigests {
|
for coordinates := range s.Artifacts.FileDigests {
|
||||||
set.Add(coordinates)
|
set.Add(coordinates)
|
||||||
}
|
}
|
||||||
for _, relationship := range sbom.Relationships {
|
for _, relationship := range s.Relationships {
|
||||||
for _, coordinates := range extractCoordinates(relationship) {
|
for _, coordinates := range extractCoordinates(relationship) {
|
||||||
set.Add(coordinates)
|
set.Add(coordinates)
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user