feat: Add support for new Copyrights and schema updates

- **Added**: New JSON schema version `16.0.16` with support for the new `Copyrights`.
- **Modified**: Updated the `JSONSchemaVersion` parameter to use the new schema.

- **Added**: New `Copyrights` field to the `Package` and `PackageBasicData` structs, similar to the existing `Licenses` field.
- **Added**: New `Copyright` struct.
- **Implemented**: Sorting methods for the `Copyright` struct.

- **Changed**: Updated the `PackageCopyrightText` to use `helpers.GetCopyrights(p.Copyrights)`, which formats the copyright text and returns a string. Example output: "Copyright 2014-2014 Matt Zabriskie & Collaborators".

- **Added**: `Copyrights` assignment to the `toSyftPackage` function.

Signed-off-by: dor-hayun <dor.hayun@mend.io>
This commit is contained in:
dor-hayun 2024-08-22 16:40:50 +03:00
parent f2caf45695
commit b75dd28a62
35 changed files with 3132 additions and 103 deletions

View file

@ -9,10 +9,11 @@ import (
)
func DefaultCommonOptions() []cmp.Option {
return CommonOptions(nil, nil)
return CommonOptions(nil, nil, nil)
}
func CommonOptions(licenseCmp LicenseComparer, locationCmp LocationComparer) []cmp.Option {
//nolint:funlen
func CommonOptions(licenseCmp LicenseComparer, locationCmp LocationComparer, copyrightCmp CopyrightComparer) []cmp.Option {
if licenseCmp == nil {
licenseCmp = DefaultLicenseComparer
}
@ -21,6 +22,10 @@ func CommonOptions(licenseCmp LicenseComparer, locationCmp LocationComparer) []c
locationCmp = DefaultLocationComparer
}
if copyrightCmp == nil {
copyrightCmp = DefaultCopyrightComparer
}
return []cmp.Option{
cmpopts.IgnoreFields(pkg.Package{}, "id"), // note: ID is not deterministic for test purposes
cmpopts.SortSlices(pkg.Less),
@ -61,11 +66,31 @@ func CommonOptions(licenseCmp LicenseComparer, locationCmp LocationComparer) []c
return true
},
),
cmp.Comparer(
func(x, y pkg.CopyrightsSet) bool {
xs := x.ToSlice()
ys := y.ToSlice()
if len(xs) != len(ys) {
return false
}
for i, xe := range xs {
ye := ys[i]
if !copyrightCmp(xe, ye) {
return false
}
}
return true
},
),
cmp.Comparer(
locationCmp,
),
cmp.Comparer(
licenseCmp,
),
cmp.Comparer(
copyrightCmp,
),
}
}

View file

@ -0,0 +1,17 @@
package cmptest
import (
"github.com/google/go-cmp/cmp"
"github.com/anchore/syft/syft/pkg"
)
type CopyrightComparer func(x, y pkg.Copyright) bool
func DefaultCopyrightComparer(x, y pkg.Copyright) bool {
return cmp.Equal(x, y, cmp.Comparer(
func(x, y string) bool {
return x == y
},
))
}

View file

@ -3,5 +3,5 @@ package internal
const (
// JSONSchemaVersion is the current schema version output by the JSON encoder
// This is roughly following the "SchemaVer" guidelines for versioning the JSON schema. Please see schema/json/README.md for details on how to increment.
JSONSchemaVersion = "16.0.15"
JSONSchemaVersion = "16.0.16"
)

View file

@ -351,6 +351,7 @@ func relationshipComparer(x, y []artifact.Relationship) string {
artifact.Relationship{},
file.LocationSet{},
pkg.LicenseSet{},
pkg.CopyrightsSet{},
), cmpopts.SortSlices(lessRelationships))
}

View file

@ -7,9 +7,9 @@ import (
"sync"
"time"
"github.com/anchore/syft/internal/log"
"github.com/hashicorp/go-multierror"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/internal/sbomsync"
"github.com/anchore/syft/syft/event/monitor"
"github.com/anchore/syft/syft/file"

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "anchore.io/schema/syft/json/16.0.15/document",
"$id": "anchore.io/schema/syft/json/16.0.16/document",
"$ref": "#/$defs/Document",
"$defs": {
"AlpmDbEntry": {
@ -370,6 +370,28 @@
"path"
]
},
"Copyright": {
"properties": {
"url": {
"type": "string"
},
"author": {
"type": "string"
},
"startYear": {
"type": "string"
},
"endYear": {
"type": "string"
}
},
"type": "object",
"required": [
"author",
"startYear",
"endYear"
]
},
"DartPubspecLockEntry": {
"properties": {
"name": {
@ -1462,6 +1484,9 @@
"licenses": {
"$ref": "#/$defs/licenses"
},
"copyrights": {
"$ref": "#/$defs/copyrights"
},
"language": {
"type": "string"
},
@ -1626,6 +1651,7 @@
"foundBy",
"locations",
"licenses",
"copyrights",
"language",
"cpes",
"purl"
@ -2566,6 +2592,12 @@
"pluginInstallDirectory"
]
},
"copyrights": {
"items": {
"$ref": "#/$defs/Copyright"
},
"type": "array"
},
"cpes": {
"items": {
"$ref": "#/$defs/CPE"

View file

@ -444,8 +444,8 @@ func toPackages(rels *relationship.Index, catalog *pkg.Collection, sbom sbom.SBO
// NOASSERTION, if
// (i) the SPDX document creator has made no attempt to determine this field; or
// (ii) the SPDX document creator has intentionally provided no information (no meaning should be implied by doing so).
//
PackageCopyrightText: noAssertion,
// (iii) Get the formatted copyright text if available, otherwise return NOASSERTION
PackageCopyrightText: helpers.GetCopyrights(p.Copyrights),
// 7.18: Package Summary Description
// Cardinality: optional, one

View file

@ -595,6 +595,7 @@ func Test_convertToAndFromFormat(t *testing.T) {
cmpopts.IgnoreUnexported(pkg.Collection{}),
cmpopts.IgnoreUnexported(pkg.Package{}),
cmpopts.IgnoreUnexported(pkg.LicenseSet{}),
cmpopts.IgnoreUnexported(pkg.CopyrightsSet{}),
cmpopts.IgnoreFields(sbom.Artifacts{}, "FileMetadata", "FileDigests"),
); diff != "" {
t.Fatalf("packages do not match:\n%s", diff)

View file

@ -48,6 +48,7 @@ func EncodeComponent(p pkg.Package) cyclonedx.Component {
Version: p.Version,
PackageURL: p.PURL,
Licenses: encodeLicenses(p),
Copyright: encodeCopyrights(p),
CPE: encodeSingleCPE(p),
Author: encodeAuthor(p),
Publisher: encodePublisher(p),

View file

@ -187,6 +187,7 @@ func Test_encodeCompomentType(t *testing.T) {
Value: "go-module",
},
},
Copyright: "",
},
},
{
@ -206,6 +207,8 @@ func Test_encodeCompomentType(t *testing.T) {
Value: "binary",
},
},
Copyright: "",
},
},
}

View file

@ -10,6 +10,11 @@ import (
"github.com/anchore/syft/syft/pkg"
)
const (
noAssertion = "NOASSERTION"
copyrightPrefix = "Copyright"
)
// This should be a function that just surfaces licenses already validated in the package struct
func encodeLicenses(p pkg.Package) *cyclonedx.Licenses {
spdx, other, ex := separateLicenses(p)
@ -195,3 +200,31 @@ func reduceOuter(expression string) string {
return sb.String()
}
func encodeCopyrights(p pkg.Package) string {
if p.Copyrights.Empty() {
return ""
}
var strArr []string
for _, c := range p.Copyrights.ToSlice() {
var sb strings.Builder
sb.WriteString(copyrightPrefix)
// Construct the string with Start Year, End Year, and Author
if c.StartYear != "" {
sb.WriteString(" " + c.StartYear)
}
if c.EndYear != "" {
sb.WriteString("-" + c.EndYear)
}
if c.Author != "" {
sb.WriteString(" " + c.Author)
}
strArr = append(strArr, sb.String())
}
return strings.Join(strArr, ", ")
}

View file

@ -0,0 +1,45 @@
package helpers
import (
"strings"
"github.com/anchore/syft/syft/pkg"
)
const (
noAssertion = "NOASSERTION"
copyrightPrefix = "Copyright"
)
func GetCopyrights(copyrights pkg.CopyrightsSet) string {
result := noAssertion
for _, c := range copyrights.ToSlice() {
var sb strings.Builder
sb.WriteString(copyrightPrefix)
// Start Year
if c.StartYear != "" {
sb.WriteString(" ")
sb.WriteString(c.StartYear)
}
// End Year
if c.EndYear != "" {
sb.WriteString("-")
sb.WriteString(c.EndYear)
}
// Author
if c.Author != "" {
sb.WriteString(" ")
sb.WriteString(c.Author)
}
// Assign the formatted string to result
result = sb.String()
}
return result
}

View file

@ -14,7 +14,7 @@
},
"packages": [
{
"SPDXID": "SPDXRef-Package-files-analyzed-false-7d37ba9d2f7c574b",
"SPDXID": "SPDXRef-Package-files-analyzed-false-0950a383541717dc",
"copyrightText": "NOASSERTION",
"downloadLocation": "NOASSERTION",
"filesAnalyzed": false,
@ -27,7 +27,7 @@
},
{
"name": "files-analyzed-true",
"SPDXID": "SPDXRef-Package-files-analyzed-true-035066c2086b8bb4",
"SPDXID": "SPDXRef-Package-files-analyzed-true-1d0a8d923f0cd238",
"versionInfo": "v1",
"supplier": "NOASSERTION",
"downloadLocation": "NOASSERTION",
@ -77,18 +77,18 @@
],
"relationships": [
{
"spdxElementId": "SPDXRef-Package-files-analyzed-true-035066c2086b8bb4",
"spdxElementId": "SPDXRef-Package-files-analyzed-true-1d0a8d923f0cd238",
"relatedSpdxElement": "SPDXRef-File-some-file-2c5bc344430decac",
"relationshipType": "CONTAINS"
},
{
"spdxElementId": "SPDXRef-DocumentRoot-Unknown-",
"relatedSpdxElement": "SPDXRef-Package-files-analyzed-false-7d37ba9d2f7c574b",
"relatedSpdxElement": "SPDXRef-Package-files-analyzed-false-0950a383541717dc",
"relationshipType": "CONTAINS"
},
{
"spdxElementId": "SPDXRef-DocumentRoot-Unknown-",
"relatedSpdxElement": "SPDXRef-Package-files-analyzed-true-035066c2086b8bb4",
"relatedSpdxElement": "SPDXRef-Package-files-analyzed-true-1d0a8d923f0cd238",
"relationshipType": "CONTAINS"
},
{

View file

@ -15,7 +15,7 @@
"packages": [
{
"name": "package-1",
"SPDXID": "SPDXRef-Package-python-package-1-5a2b1ae000fcb51e",
"SPDXID": "SPDXRef-Package-python-package-1-f7fdfcfa4ca6e742",
"versionInfo": "1.0.1",
"supplier": "NOASSERTION",
"downloadLocation": "NOASSERTION",
@ -39,7 +39,7 @@
},
{
"name": "package-2",
"SPDXID": "SPDXRef-Package-deb-package-2-39392bb5e270f669",
"SPDXID": "SPDXRef-Package-deb-package-2-062f404587213e8b",
"versionInfo": "2.0.1",
"supplier": "NOASSERTION",
"downloadLocation": "NOASSERTION",
@ -75,12 +75,12 @@
"relationships": [
{
"spdxElementId": "SPDXRef-DocumentRoot-Directory-some-path",
"relatedSpdxElement": "SPDXRef-Package-python-package-1-5a2b1ae000fcb51e",
"relatedSpdxElement": "SPDXRef-Package-python-package-1-f7fdfcfa4ca6e742",
"relationshipType": "CONTAINS"
},
{
"spdxElementId": "SPDXRef-DocumentRoot-Directory-some-path",
"relatedSpdxElement": "SPDXRef-Package-deb-package-2-39392bb5e270f669",
"relatedSpdxElement": "SPDXRef-Package-deb-package-2-062f404587213e8b",
"relationshipType": "CONTAINS"
},
{

View file

@ -15,7 +15,7 @@
"packages": [
{
"name": "package-1",
"SPDXID": "SPDXRef-Package-python-package-1-c5cf7ac34cbca450",
"SPDXID": "SPDXRef-Package-python-package-1-69910a93dc37ffb4",
"versionInfo": "1.0.1",
"supplier": "NOASSERTION",
"downloadLocation": "NOASSERTION",
@ -39,7 +39,7 @@
},
{
"name": "package-2",
"SPDXID": "SPDXRef-Package-deb-package-2-4b756c6f6fb127a3",
"SPDXID": "SPDXRef-Package-deb-package-2-fe989317bb1cbb62",
"versionInfo": "2.0.1",
"supplier": "NOASSERTION",
"downloadLocation": "NOASSERTION",
@ -89,12 +89,12 @@
"relationships": [
{
"spdxElementId": "SPDXRef-DocumentRoot-Image-user-image-input",
"relatedSpdxElement": "SPDXRef-Package-python-package-1-c5cf7ac34cbca450",
"relatedSpdxElement": "SPDXRef-Package-python-package-1-69910a93dc37ffb4",
"relationshipType": "CONTAINS"
},
{
"spdxElementId": "SPDXRef-DocumentRoot-Image-user-image-input",
"relatedSpdxElement": "SPDXRef-Package-deb-package-2-4b756c6f6fb127a3",
"relatedSpdxElement": "SPDXRef-Package-deb-package-2-fe989317bb1cbb62",
"relationshipType": "CONTAINS"
},
{

View file

@ -15,7 +15,7 @@
"packages": [
{
"name": "package-1",
"SPDXID": "SPDXRef-Package-python-package-1-c5cf7ac34cbca450",
"SPDXID": "SPDXRef-Package-python-package-1-69910a93dc37ffb4",
"versionInfo": "1.0.1",
"supplier": "NOASSERTION",
"downloadLocation": "NOASSERTION",
@ -39,7 +39,7 @@
},
{
"name": "package-2",
"SPDXID": "SPDXRef-Package-deb-package-2-4b756c6f6fb127a3",
"SPDXID": "SPDXRef-Package-deb-package-2-fe989317bb1cbb62",
"versionInfo": "2.0.1",
"supplier": "NOASSERTION",
"downloadLocation": "NOASSERTION",
@ -198,43 +198,43 @@
],
"relationships": [
{
"spdxElementId": "SPDXRef-Package-python-package-1-c5cf7ac34cbca450",
"spdxElementId": "SPDXRef-Package-python-package-1-69910a93dc37ffb4",
"relatedSpdxElement": "SPDXRef-File-f1-5265a4dde3edbf7c",
"relationshipType": "CONTAINS"
},
{
"spdxElementId": "SPDXRef-Package-python-package-1-c5cf7ac34cbca450",
"spdxElementId": "SPDXRef-Package-python-package-1-69910a93dc37ffb4",
"relatedSpdxElement": "SPDXRef-File-z1-f5-839d99ee67d9d174",
"relationshipType": "CONTAINS"
},
{
"spdxElementId": "SPDXRef-Package-python-package-1-c5cf7ac34cbca450",
"spdxElementId": "SPDXRef-Package-python-package-1-69910a93dc37ffb4",
"relatedSpdxElement": "SPDXRef-File-a1-f6-9c2f7510199b17f6",
"relationshipType": "CONTAINS"
},
{
"spdxElementId": "SPDXRef-Package-python-package-1-c5cf7ac34cbca450",
"spdxElementId": "SPDXRef-Package-python-package-1-69910a93dc37ffb4",
"relatedSpdxElement": "SPDXRef-File-d2-f4-c641caa71518099f",
"relationshipType": "CONTAINS"
},
{
"spdxElementId": "SPDXRef-Package-python-package-1-c5cf7ac34cbca450",
"spdxElementId": "SPDXRef-Package-python-package-1-69910a93dc37ffb4",
"relatedSpdxElement": "SPDXRef-File-d1-f3-c6f5b29dca12661f",
"relationshipType": "CONTAINS"
},
{
"spdxElementId": "SPDXRef-Package-python-package-1-c5cf7ac34cbca450",
"spdxElementId": "SPDXRef-Package-python-package-1-69910a93dc37ffb4",
"relatedSpdxElement": "SPDXRef-File-f2-f9e49132a4b96ccd",
"relationshipType": "CONTAINS"
},
{
"spdxElementId": "SPDXRef-DocumentRoot-Image-user-image-input",
"relatedSpdxElement": "SPDXRef-Package-python-package-1-c5cf7ac34cbca450",
"relatedSpdxElement": "SPDXRef-Package-python-package-1-69910a93dc37ffb4",
"relationshipType": "CONTAINS"
},
{
"spdxElementId": "SPDXRef-DocumentRoot-Image-user-image-input",
"relatedSpdxElement": "SPDXRef-Package-deb-package-2-4b756c6f6fb127a3",
"relatedSpdxElement": "SPDXRef-Package-deb-package-2-fe989317bb1cbb62",
"relationshipType": "CONTAINS"
},
{

View file

@ -22,7 +22,7 @@ PackageLicenseDeclared: NOASSERTION
##### Package: @at-sign
PackageName: @at-sign
SPDXID: SPDXRef-Package--at-sign-1c8c811ea5b1cd46
SPDXID: SPDXRef-Package--at-sign-ec109f3d122ef1db
PackageSupplier: NOASSERTION
PackageDownloadLocation: NOASSERTION
FilesAnalyzed: false
@ -34,7 +34,7 @@ PackageCopyrightText: NOASSERTION
##### Package: some/slashes
PackageName: some/slashes
SPDXID: SPDXRef-Package-some-slashes-8a8e95924316c66b
SPDXID: SPDXRef-Package-some-slashes-8a21771e3392022f
PackageSupplier: NOASSERTION
PackageDownloadLocation: NOASSERTION
FilesAnalyzed: false
@ -46,7 +46,7 @@ PackageCopyrightText: NOASSERTION
##### Package: under_scores
PackageName: under_scores
SPDXID: SPDXRef-Package-under-scores-883703d950ec00f3
SPDXID: SPDXRef-Package-under-scores-5db453bf3f332f99
PackageSupplier: NOASSERTION
PackageDownloadLocation: NOASSERTION
FilesAnalyzed: false
@ -57,8 +57,8 @@ PackageCopyrightText: NOASSERTION
##### Relationships
Relationship: SPDXRef-DocumentRoot-Directory-foobar-baz CONTAINS SPDXRef-Package--at-sign-1c8c811ea5b1cd46
Relationship: SPDXRef-DocumentRoot-Directory-foobar-baz CONTAINS SPDXRef-Package-some-slashes-8a8e95924316c66b
Relationship: SPDXRef-DocumentRoot-Directory-foobar-baz CONTAINS SPDXRef-Package-under-scores-883703d950ec00f3
Relationship: SPDXRef-DocumentRoot-Directory-foobar-baz CONTAINS SPDXRef-Package--at-sign-ec109f3d122ef1db
Relationship: SPDXRef-DocumentRoot-Directory-foobar-baz CONTAINS SPDXRef-Package-some-slashes-8a21771e3392022f
Relationship: SPDXRef-DocumentRoot-Directory-foobar-baz CONTAINS SPDXRef-Package-under-scores-5db453bf3f332f99
Relationship: SPDXRef-DOCUMENT DESCRIBES SPDXRef-DocumentRoot-Directory-foobar-baz

View file

@ -69,7 +69,7 @@ ExternalRef: PACKAGE-MANAGER purl pkg:oci/user-image-input@sha256:2731251dc34951
##### Package: package-2
PackageName: package-2
SPDXID: SPDXRef-Package-deb-package-2-4b756c6f6fb127a3
SPDXID: SPDXRef-Package-deb-package-2-fe989317bb1cbb62
PackageVersion: 2.0.1
PackageSupplier: NOASSERTION
PackageDownloadLocation: NOASSERTION
@ -84,7 +84,7 @@ ExternalRef: PACKAGE-MANAGER purl pkg:deb/debian/package-2@2.0.1
##### Package: package-1
PackageName: package-1
SPDXID: SPDXRef-Package-python-package-1-c5cf7ac34cbca450
SPDXID: SPDXRef-Package-python-package-1-69910a93dc37ffb4
PackageVersion: 1.0.1
PackageSupplier: NOASSERTION
PackageDownloadLocation: NOASSERTION
@ -98,13 +98,13 @@ ExternalRef: PACKAGE-MANAGER purl a-purl-1
##### Relationships
Relationship: SPDXRef-Package-python-package-1-c5cf7ac34cbca450 CONTAINS SPDXRef-File-f1-5265a4dde3edbf7c
Relationship: SPDXRef-Package-python-package-1-c5cf7ac34cbca450 CONTAINS SPDXRef-File-z1-f5-839d99ee67d9d174
Relationship: SPDXRef-Package-python-package-1-c5cf7ac34cbca450 CONTAINS SPDXRef-File-a1-f6-9c2f7510199b17f6
Relationship: SPDXRef-Package-python-package-1-c5cf7ac34cbca450 CONTAINS SPDXRef-File-d2-f4-c641caa71518099f
Relationship: SPDXRef-Package-python-package-1-c5cf7ac34cbca450 CONTAINS SPDXRef-File-d1-f3-c6f5b29dca12661f
Relationship: SPDXRef-Package-python-package-1-c5cf7ac34cbca450 CONTAINS SPDXRef-File-f2-f9e49132a4b96ccd
Relationship: SPDXRef-DocumentRoot-Image-user-image-input CONTAINS SPDXRef-Package-python-package-1-c5cf7ac34cbca450
Relationship: SPDXRef-DocumentRoot-Image-user-image-input CONTAINS SPDXRef-Package-deb-package-2-4b756c6f6fb127a3
Relationship: SPDXRef-Package-python-package-1-69910a93dc37ffb4 CONTAINS SPDXRef-File-f1-5265a4dde3edbf7c
Relationship: SPDXRef-Package-python-package-1-69910a93dc37ffb4 CONTAINS SPDXRef-File-z1-f5-839d99ee67d9d174
Relationship: SPDXRef-Package-python-package-1-69910a93dc37ffb4 CONTAINS SPDXRef-File-a1-f6-9c2f7510199b17f6
Relationship: SPDXRef-Package-python-package-1-69910a93dc37ffb4 CONTAINS SPDXRef-File-d2-f4-c641caa71518099f
Relationship: SPDXRef-Package-python-package-1-69910a93dc37ffb4 CONTAINS SPDXRef-File-d1-f3-c6f5b29dca12661f
Relationship: SPDXRef-Package-python-package-1-69910a93dc37ffb4 CONTAINS SPDXRef-File-f2-f9e49132a4b96ccd
Relationship: SPDXRef-DocumentRoot-Image-user-image-input CONTAINS SPDXRef-Package-python-package-1-69910a93dc37ffb4
Relationship: SPDXRef-DocumentRoot-Image-user-image-input CONTAINS SPDXRef-Package-deb-package-2-fe989317bb1cbb62
Relationship: SPDXRef-DOCUMENT DESCRIBES SPDXRef-DocumentRoot-Image-user-image-input

View file

@ -22,7 +22,7 @@ PackageLicenseDeclared: NOASSERTION
##### Package: package-2
PackageName: package-2
SPDXID: SPDXRef-Package-deb-package-2-39392bb5e270f669
SPDXID: SPDXRef-Package-deb-package-2-062f404587213e8b
PackageVersion: 2.0.1
PackageSupplier: NOASSERTION
PackageDownloadLocation: NOASSERTION
@ -37,7 +37,7 @@ ExternalRef: PACKAGE-MANAGER purl pkg:deb/debian/package-2@2.0.1
##### Package: package-1
PackageName: package-1
SPDXID: SPDXRef-Package-python-package-1-5a2b1ae000fcb51e
SPDXID: SPDXRef-Package-python-package-1-f7fdfcfa4ca6e742
PackageVersion: 1.0.1
PackageSupplier: NOASSERTION
PackageDownloadLocation: NOASSERTION
@ -51,7 +51,7 @@ ExternalRef: PACKAGE-MANAGER purl a-purl-2
##### Relationships
Relationship: SPDXRef-DocumentRoot-Directory-some-path CONTAINS SPDXRef-Package-python-package-1-5a2b1ae000fcb51e
Relationship: SPDXRef-DocumentRoot-Directory-some-path CONTAINS SPDXRef-Package-deb-package-2-39392bb5e270f669
Relationship: SPDXRef-DocumentRoot-Directory-some-path CONTAINS SPDXRef-Package-python-package-1-f7fdfcfa4ca6e742
Relationship: SPDXRef-DocumentRoot-Directory-some-path CONTAINS SPDXRef-Package-deb-package-2-062f404587213e8b
Relationship: SPDXRef-DOCUMENT DESCRIBES SPDXRef-DocumentRoot-Directory-some-path

View file

@ -25,7 +25,7 @@ ExternalRef: PACKAGE-MANAGER purl pkg:oci/user-image-input@sha256:2731251dc34951
##### Package: package-2
PackageName: package-2
SPDXID: SPDXRef-Package-deb-package-2-4b756c6f6fb127a3
SPDXID: SPDXRef-Package-deb-package-2-fe989317bb1cbb62
PackageVersion: 2.0.1
PackageSupplier: NOASSERTION
PackageDownloadLocation: NOASSERTION
@ -40,7 +40,7 @@ ExternalRef: PACKAGE-MANAGER purl pkg:deb/debian/package-2@2.0.1
##### Package: package-1
PackageName: package-1
SPDXID: SPDXRef-Package-python-package-1-c5cf7ac34cbca450
SPDXID: SPDXRef-Package-python-package-1-69910a93dc37ffb4
PackageVersion: 1.0.1
PackageSupplier: NOASSERTION
PackageDownloadLocation: NOASSERTION
@ -54,7 +54,7 @@ ExternalRef: PACKAGE-MANAGER purl a-purl-1
##### Relationships
Relationship: SPDXRef-DocumentRoot-Image-user-image-input CONTAINS SPDXRef-Package-python-package-1-c5cf7ac34cbca450
Relationship: SPDXRef-DocumentRoot-Image-user-image-input CONTAINS SPDXRef-Package-deb-package-2-4b756c6f6fb127a3
Relationship: SPDXRef-DocumentRoot-Image-user-image-input CONTAINS SPDXRef-Package-python-package-1-69910a93dc37ffb4
Relationship: SPDXRef-DocumentRoot-Image-user-image-input CONTAINS SPDXRef-Package-deb-package-2-fe989317bb1cbb62
Relationship: SPDXRef-DOCUMENT DESCRIBES SPDXRef-DocumentRoot-Image-user-image-input

View file

@ -24,16 +24,17 @@ type Package struct {
// PackageBasicData contains non-ambiguous values (type-wise) from pkg.Package.
type PackageBasicData struct {
ID string `json:"id"`
Name string `json:"name"`
Version string `json:"version"`
Type pkg.Type `json:"type"`
FoundBy string `json:"foundBy"`
Locations []file.Location `json:"locations"`
Licenses licenses `json:"licenses"`
Language pkg.Language `json:"language"`
CPEs cpes `json:"cpes"`
PURL string `json:"purl"`
ID string `json:"id"`
Name string `json:"name"`
Version string `json:"version"`
Type pkg.Type `json:"type"`
FoundBy string `json:"foundBy"`
Locations []file.Location `json:"locations"`
Licenses licenses `json:"licenses"`
Copyrights copyrights `json:"copyrights"`
Language pkg.Language `json:"language"`
CPEs cpes `json:"cpes"`
PURL string `json:"purl"`
}
type cpes []CPE
@ -53,6 +54,15 @@ type License struct {
Locations []file.Location `json:"locations"`
}
type copyrights []Copyright
type Copyright struct {
URL string `json:"url,omitempty"`
Author string `json:"author"`
StartYear string `json:"startYear"`
EndYear string `json:"endYear"`
}
func newModelLicensesFromValues(licenses []string) (ml []License) {
for _, v := range licenses {
expression, err := license.ParseExpression(v)

View file

@ -1,7 +1,7 @@
{
"artifacts": [
{
"id": "5a2b1ae000fcb51e",
"id": "f7fdfcfa4ca6e742",
"name": "package-1",
"version": "1.0.1",
"type": "python",
@ -21,6 +21,7 @@
"locations": []
}
],
"copyrights": [],
"language": "python",
"cpes": [
{
@ -44,7 +45,7 @@
}
},
{
"id": "39392bb5e270f669",
"id": "062f404587213e8b",
"name": "package-2",
"version": "2.0.1",
"type": "deb",
@ -56,6 +57,7 @@
}
],
"licenses": [],
"copyrights": [],
"language": "",
"cpes": [
{

View file

@ -1,7 +1,7 @@
{
"artifacts": [
{
"id": "ad3ecac55fe1c30f",
"id": "ecf423ccf313f850",
"name": "package-1",
"version": "1.0.1",
"type": "python",
@ -21,6 +21,7 @@
"locations": []
}
],
"copyrights": [],
"language": "python",
"cpes": [
{
@ -40,7 +41,7 @@
}
},
{
"id": "fa4ec37eccd65756",
"id": "b4d209e1bb8d83cb",
"name": "package-2",
"version": "2.0.1",
"type": "deb",
@ -52,6 +53,7 @@
}
],
"licenses": [],
"copyrights": [],
"language": "",
"cpes": [
{

View file

@ -1,7 +1,7 @@
{
"artifacts": [
{
"id": "c5cf7ac34cbca450",
"id": "69910a93dc37ffb4",
"name": "package-1",
"version": "1.0.1",
"type": "python",
@ -22,6 +22,7 @@
"locations": []
}
],
"copyrights": [],
"language": "python",
"cpes": [
{
@ -41,7 +42,7 @@
}
},
{
"id": "4b756c6f6fb127a3",
"id": "fe989317bb1cbb62",
"name": "package-2",
"version": "2.0.1",
"type": "deb",
@ -54,6 +55,7 @@
}
],
"licenses": [],
"copyrights": [],
"language": "",
"cpes": [
{

View file

@ -233,6 +233,18 @@ func toLicenseModel(pkgLicenses []pkg.License) (modelLicenses []model.License) {
return
}
func toCopyrightModel(pkgCopyrights []pkg.Copyright) (modelCopyrights []model.Copyright) {
for _, l := range pkgCopyrights {
modelCopyrights = append(modelCopyrights, model.Copyright{
URL: l.URL,
Author: l.Author,
StartYear: l.StartYear,
EndYear: l.EndYear,
})
}
return
}
// toPackageModel crates a new Package from the given pkg.Package.
func toPackageModel(p pkg.Package, cfg EncoderConfig) model.Package {
var cpes = make([]model.CPE, len(p.CPEs))
@ -251,18 +263,24 @@ func toPackageModel(p pkg.Package, cfg EncoderConfig) model.Package {
licenses = toLicenseModel(p.Licenses.ToSlice())
}
var copyrights = make([]model.Copyright, 0)
if !p.Copyrights.Empty() {
copyrights = toCopyrightModel(p.Copyrights.ToSlice())
}
return model.Package{
PackageBasicData: model.PackageBasicData{
ID: string(p.ID()),
Name: p.Name,
Version: p.Version,
Type: p.Type,
FoundBy: p.FoundBy,
Locations: p.Locations.ToSlice(),
Licenses: licenses,
Language: p.Language,
CPEs: cpes,
PURL: p.PURL,
ID: string(p.ID()),
Name: p.Name,
Version: p.Version,
Type: p.Type,
FoundBy: p.FoundBy,
Locations: p.Locations.ToSlice(),
Licenses: licenses,
Copyrights: copyrights,
Language: p.Language,
CPEs: cpes,
PURL: p.PURL,
},
PackageCustomData: model.PackageCustomData{
MetadataType: metadataType(p.Metadata, cfg.Legacy),

View file

@ -162,6 +162,19 @@ func toSyftLicenses(m []model.License) (p []pkg.License) {
return
}
func toSyftCopyrights(m []model.Copyright) (p []pkg.Copyright) {
for _, l := range m {
p = append(p, pkg.Copyright{
URL: l.URL,
Author: l.Author,
StartYear: l.StartYear,
EndYear: l.EndYear,
})
}
return
}
func toSyftFileType(ty string) stereoscopeFile.Type {
switch ty {
case "SymbolicLink":
@ -331,16 +344,17 @@ func toSyftPackage(p model.Package, idAliases map[string]string) pkg.Package {
}
out := pkg.Package{
Name: p.Name,
Version: p.Version,
FoundBy: p.FoundBy,
Locations: file.NewLocationSet(p.Locations...),
Licenses: pkg.NewLicenseSet(toSyftLicenses(p.Licenses)...),
Language: p.Language,
Type: p.Type,
CPEs: cpes,
PURL: p.PURL,
Metadata: p.Metadata,
Name: p.Name,
Version: p.Version,
FoundBy: p.FoundBy,
Locations: file.NewLocationSet(p.Locations...),
Licenses: pkg.NewLicenseSet(toSyftLicenses(p.Licenses)...),
Copyrights: pkg.NewCopyrightSet(toSyftCopyrights(p.Copyrights)...),
Language: p.Language,
Type: p.Type,
CPEs: cpes,
PURL: p.PURL,
Metadata: p.Metadata,
}
// we don't know if this package ID is truly unique, however, we need to trust the user input in case there are

View file

@ -71,6 +71,8 @@ func findMetadataDefinitionNames(paths ...string) ([]string, error) {
// remove known exceptions, that is, types exported in the pkg Package that are not used
// in a metadata type but are not metadata types themselves.
names.Remove("Licenses", "KeyValue")
names.Remove("Copyrights", "KeyValue")
names.Remove("CopyrightsSet", "KeyValue")
strNames := names.List()
sort.Strings(strNames)

View file

@ -157,7 +157,7 @@ func Test_newELFPackage(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
actual := newELFPackage(test.metadata, file.NewLocationSet())
if diff := cmp.Diff(test.expected, actual, cmpopts.IgnoreFields(pkg.Package{}, "id"), cmpopts.IgnoreUnexported(pkg.Package{}, file.LocationSet{}, pkg.LicenseSet{})); diff != "" {
if diff := cmp.Diff(test.expected, actual, cmpopts.IgnoreFields(pkg.Package{}, "id"), cmpopts.IgnoreUnexported(pkg.Package{}, file.LocationSet{}, pkg.LicenseSet{}, pkg.CopyrightsSet{})); diff != "" {
t.Errorf("newELFPackage() mismatch (-want +got):\n%s", diff)
}
})

View file

@ -42,6 +42,7 @@ type CatalogTester struct {
compareOptions []cmp.Option
locationComparer cmptest.LocationComparer
licenseComparer cmptest.LicenseComparer
copyrightComparer cmptest.CopyrightComparer
packageStringer func(pkg.Package) string
customAssertions []func(t *testing.T, pkgs []pkg.Package, relationships []artifact.Relationship)
}
@ -267,7 +268,7 @@ func (p *CatalogTester) TestCataloger(t *testing.T, cataloger pkg.Cataloger) {
func (p *CatalogTester) assertPkgs(t *testing.T, pkgs []pkg.Package, relationships []artifact.Relationship) {
t.Helper()
p.compareOptions = append(p.compareOptions, cmptest.CommonOptions(p.licenseComparer, p.locationComparer)...)
p.compareOptions = append(p.compareOptions, cmptest.CommonOptions(p.licenseComparer, p.locationComparer, p.copyrightComparer)...)
{
r := cmptest.NewDiffReporter()
@ -320,6 +321,7 @@ func TestFileParserWithEnv(t *testing.T, fixturePath string, parser generic.Pars
NewCatalogTester().FromFile(t, fixturePath).WithEnv(env).Expects(expectedPkgs, expectedRelationships).TestParser(t, parser)
}
//nolint:funlen
func AssertPackagesEqual(t *testing.T, a, b pkg.Package) {
t.Helper()
opts := []cmp.Option{
@ -360,12 +362,33 @@ func AssertPackagesEqual(t *testing.T, a, b pkg.Package) {
return true
},
),
cmp.Comparer(
func(x, y pkg.CopyrightsSet) bool {
xs := x.ToSlice()
ys := y.ToSlice()
if len(xs) != len(ys) {
return false
}
for i, xe := range xs {
ye := ys[i]
if !cmptest.DefaultCopyrightComparer(xe, ye) {
return false
}
}
return true
},
),
cmp.Comparer(
cmptest.DefaultLocationComparer,
),
cmp.Comparer(
cmptest.DefaultLicenseComparer,
),
cmp.Comparer(
cmptest.DefaultCopyrightComparer,
),
}
if diff := cmp.Diff(a, b, opts...); diff != "" {

70
syft/pkg/copyright.go Normal file
View file

@ -0,0 +1,70 @@
package pkg
import (
"fmt"
"sort"
"github.com/scylladb/go-set/strset"
)
type Copyright struct {
URL string `json:"url,omitempty"`
Author string `json:"author"`
StartYear string `json:"startYear"`
EndYear string `json:"endYear"`
}
type Copyrights []Copyright
func (c Copyrights) Len() int {
return len(c)
}
func (c Copyrights) Swap(i, j int) {
c[i], c[j] = c[j], c[i]
}
func (c Copyrights) Less(i, j int) bool {
return c[i].Author < c[j].Author
}
// Merge attempts to merge two Copyright instances. It merges URLs if the Author,
// StartYear, and EndYear are the same or compatible.
func (s Copyright) Merge(c Copyright) (*Copyright, error) {
// Check if the Author is the same
if s.Author != c.Author {
return nil, fmt.Errorf("cannot merge copyrights with different authors: %s vs %s", s.Author, c.Author)
}
// Check if the StartYear and EndYear are compatible
if s.StartYear != c.StartYear || s.EndYear != c.EndYear {
return nil, fmt.Errorf("cannot merge copyrights with different years: %s-%s vs %s-%s", s.StartYear, s.EndYear, c.StartYear, c.EndYear)
}
// Merge URLs
if c.URL != "" {
s.URL = mergeURLs(s.URL, c.URL)
}
return &s, nil
}
// mergeURLs merges two URL strings, deduplicates, and sorts them.
func mergeURLs(sURL, cURL string) string {
var urls []string
if sURL != "" {
urls = append(urls, sURL)
}
if cURL != "" {
urls = append(urls, cURL)
}
if len(urls) > 0 {
// Deduplicate and sort URLs
urlsSet := strset.New(urls...)
sortedURLs := urlsSet.List()
sort.Strings(sortedURLs)
return sortedURLs[0] // Assuming we return the first one or join them into a single string
}
return ""
}

89
syft/pkg/copyright_set.go Normal file
View file

@ -0,0 +1,89 @@
//nolint:dupl
package pkg
import (
"fmt"
"sort"
"github.com/mitchellh/hashstructure/v2"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/artifact"
)
type CopyrightsSet struct {
set map[artifact.ID]Copyright
}
func NewCopyrightSet(copyrights ...Copyright) (c CopyrightsSet) {
for _, l := range copyrights {
c.Add(l)
}
return c
}
func (c *CopyrightsSet) addToExisting(copyright Copyright) (id artifact.ID, merged bool, err error) {
id, err = artifact.IDByHash(copyright)
if err != nil {
return id, false, fmt.Errorf("could not get the hash for a copyright: %w", err)
}
v, ok := c.set[id]
if !ok {
// doesn't exist safe to add
return id, false, nil
}
// we got the same id; we want to merge the URLs and Location data
// URLs/Location are not considered when taking the Hash
m, err := v.Merge(copyright)
if err != nil {
return id, false, fmt.Errorf("could not merge license into map: %w", err)
}
c.set[id] = *m
return id, true, nil
}
func (c *CopyrightsSet) Add(copyrights ...Copyright) {
if c.set == nil {
c.set = make(map[artifact.ID]Copyright)
}
for _, l := range copyrights {
// we only want to add copyrights that have a value
// note, this check should be moved to the license constructor in the future
if l.Author != "" {
if id, merged, err := c.addToExisting(l); err == nil && !merged {
// doesn't exist, add it
c.set[id] = l
} else if err != nil {
log.Trace("copyright set failed to add copyright %#v: %+v", l, err)
}
}
}
}
func (c CopyrightsSet) ToSlice() []Copyright {
if c.set == nil {
return nil
}
var copyrights []Copyright
for _, v := range c.set {
copyrights = append(copyrights, v)
}
sort.Sort(Copyrights(copyrights))
return copyrights
}
func (c CopyrightsSet) Hash() (uint64, error) {
// access paths and filesystem IDs are not considered when hashing a copyright set, only the real paths
return hashstructure.Hash(c.ToSlice(), hashstructure.FormatV2, &hashstructure.HashOptions{
ZeroNil: true,
SlicesAsSets: true,
})
}
func (c CopyrightsSet) Empty() bool {
return len(c.set) < 1
}

View file

@ -1,3 +1,4 @@
//nolint:dupl
package pkg
import (

View file

@ -17,17 +17,19 @@ import (
// Package represents an application or library that has been bundled into a distributable format.
// TODO: if we ignore FoundBy for ID generation should we merge the field to show it was found in two places?
type Package struct {
id artifact.ID `hash:"ignore"`
Name string // the package name
Version string // the version of the package
FoundBy string `hash:"ignore" cyclonedx:"foundBy"` // the specific cataloger that discovered this package
Locations file.LocationSet // the locations that lead to the discovery of this package (note: this is not necessarily the locations that make up this package)
Licenses LicenseSet // licenses discovered with the package metadata
Language Language `hash:"ignore" cyclonedx:"language"` // the language ecosystem this package belongs to (e.g. JavaScript, Python, etc)
Type Type `cyclonedx:"type"` // the package type (e.g. Npm, Yarn, Python, Rpm, Deb, etc)
CPEs []cpe.CPE `hash:"ignore"` // all possible Common Platform Enumerators (note: this is NOT included in the definition of the ID since all fields on a CPE are derived from other fields)
PURL string `hash:"ignore"` // the Package URL (see https://github.com/package-url/purl-spec)
Metadata interface{} // additional data found while parsing the package source
id artifact.ID `hash:"ignore"`
Name string // the package name
Version string // the version of the package
FoundBy string `hash:"ignore" cyclonedx:"foundBy"` // the specific cataloger that discovered this package
Locations file.LocationSet // the locations that lead to the discovery of this package (note: this is not necessarily the locations that make up this package)
Licenses LicenseSet // licenses discovered with the package metadata
Copyrights CopyrightsSet // copyrights discovered with the package metadata
Language Language `hash:"ignore" cyclonedx:"language"` // the language ecosystem this package belongs to (e.g. JavaScript, Python, etc)
Type Type `cyclonedx:"type"` // the package type (e.g. Npm, Yarn, Python, Rpm, Deb, etc)
CPEs []cpe.CPE `hash:"ignore"` // all possible Common Platform Enumerators (note: this is NOT included in the definition of the ID since all fields on a CPE are derived from other fields)
PURL string `hash:"ignore"` // the Package URL (see https://github.com/package-url/purl-spec)
Metadata interface{} // additional data found while parsing the package source
}
func (p *Package) OverrideID(id artifact.ID) {

View file

@ -416,6 +416,24 @@ func TestPackage_Merge(t *testing.T) {
return true
},
),
cmp.Comparer(
func(x, y CopyrightsSet) bool {
xs := x.ToSlice()
ys := y.ToSlice()
if len(xs) != len(ys) {
return false
}
for i, xe := range xs {
ye := ys[i]
if !copyrightComparer(xe, ye) {
return false
}
}
return true
},
),
cmp.Comparer(locationComparer),
); diff != "" {
t.Errorf("unexpected result from parsing (-expected +actual)\n%s", diff)
@ -428,6 +446,10 @@ func licenseComparer(x, y License) bool {
return cmp.Equal(x, y, cmp.Comparer(locationComparer))
}
func copyrightComparer(x, y Copyright) bool {
return cmp.Equal(x, y, cmp.Comparer(copyrightComparer))
}
func locationComparer(x, y file.Location) bool {
return cmp.Equal(x.Coordinates, y.Coordinates) && cmp.Equal(x.AccessPath, y.AccessPath)
}