diff --git a/internal/cmptest/common_options.go b/internal/cmptest/common_options.go
index ecfb54edfde..ce23f71839b 100644
--- a/internal/cmptest/common_options.go
+++ b/internal/cmptest/common_options.go
@@ -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,
+ ),
}
}
diff --git a/internal/cmptest/copyright.go b/internal/cmptest/copyright.go
new file mode 100644
index 00000000000..789db8c2afc
--- /dev/null
+++ b/internal/cmptest/copyright.go
@@ -0,0 +1,16 @@
+package cmptest
+
+import (
+ "github.com/anchore/syft/syft/pkg"
+ "github.com/google/go-cmp/cmp"
+)
+
+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
+ },
+ ))
+}
diff --git a/internal/constants.go b/internal/constants.go
index 6d1dd197439..93b0093ab6a 100644
--- a/internal/constants.go
+++ b/internal/constants.go
@@ -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"
)
diff --git a/internal/relationship/binary/binary_dependencies_test.go b/internal/relationship/binary/binary_dependencies_test.go
index ea524fa1f7c..602139e9ad5 100644
--- a/internal/relationship/binary/binary_dependencies_test.go
+++ b/internal/relationship/binary/binary_dependencies_test.go
@@ -351,6 +351,7 @@ func relationshipComparer(x, y []artifact.Relationship) string {
artifact.Relationship{},
file.LocationSet{},
pkg.LicenseSet{},
+ pkg.CopyrightsSet{},
), cmpopts.SortSlices(lessRelationships))
}
diff --git a/schema/json/schema-latest.json b/schema/json/schema-latest.json
index 1bab78aa6ba..537f41e98ff 100644
--- a/schema/json/schema-latest.json
+++ b/schema/json/schema-latest.json
@@ -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"
diff --git a/syft/format/common/spdxhelpers/to_format_model.go b/syft/format/common/spdxhelpers/to_format_model.go
index 3fd136e84df..6e03d3fad54 100644
--- a/syft/format/common/spdxhelpers/to_format_model.go
+++ b/syft/format/common/spdxhelpers/to_format_model.go
@@ -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
diff --git a/syft/format/common/spdxhelpers/to_syft_model_test.go b/syft/format/common/spdxhelpers/to_syft_model_test.go
index e8526bab588..897fe2abba1 100644
--- a/syft/format/common/spdxhelpers/to_syft_model_test.go
+++ b/syft/format/common/spdxhelpers/to_syft_model_test.go
@@ -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)
diff --git a/syft/format/cyclonedxjson/test-fixtures/snapshot/TestCycloneDxDirectoryEncoder.golden b/syft/format/cyclonedxjson/test-fixtures/snapshot/TestCycloneDxDirectoryEncoder.golden
index e4e7bd7521b..8311adb2857 100644
--- a/syft/format/cyclonedxjson/test-fixtures/snapshot/TestCycloneDxDirectoryEncoder.golden
+++ b/syft/format/cyclonedxjson/test-fixtures/snapshot/TestCycloneDxDirectoryEncoder.golden
@@ -35,6 +35,7 @@
}
}
],
+ "copyright":"NOASSERTION",
"cpe": "cpe:2.3:*:some:package:2:*:*:*:*:*:*:*",
"purl": "a-purl-2",
"properties": [
@@ -62,6 +63,7 @@
},
{
"bom-ref":"redacted",
+ "copyright":"NOASSERTION",
"type": "library",
"name": "package-2",
"version": "2.0.1",
diff --git a/syft/format/cyclonedxjson/test-fixtures/snapshot/TestCycloneDxImageEncoder.golden b/syft/format/cyclonedxjson/test-fixtures/snapshot/TestCycloneDxImageEncoder.golden
index 62750e9e68a..3f1d5f844a8 100644
--- a/syft/format/cyclonedxjson/test-fixtures/snapshot/TestCycloneDxImageEncoder.golden
+++ b/syft/format/cyclonedxjson/test-fixtures/snapshot/TestCycloneDxImageEncoder.golden
@@ -36,6 +36,7 @@
}
}
],
+ "copyright":"NOASSERTION",
"cpe": "cpe:2.3:*:some:package:1:*:*:*:*:*:*:*",
"purl": "a-purl-1",
"properties": [
@@ -67,6 +68,7 @@
},
{
"bom-ref":"redacted",
+ "copyright":"NOASSERTION",
"type": "library",
"name": "package-2",
"version": "2.0.1",
diff --git a/syft/format/cyclonedxxml/test-fixtures/snapshot/TestCycloneDxDirectoryEncoder.golden b/syft/format/cyclonedxxml/test-fixtures/snapshot/TestCycloneDxDirectoryEncoder.golden
index 9a9f7bce8a6..1752c19a14a 100644
--- a/syft/format/cyclonedxxml/test-fixtures/snapshot/TestCycloneDxDirectoryEncoder.golden
+++ b/syft/format/cyclonedxxml/test-fixtures/snapshot/TestCycloneDxDirectoryEncoder.golden
@@ -24,6 +24,7 @@
MIT
+ NOASSERTION
cpe:2.3:*:some:package:2:*:*:*:*:*:*:*
a-purl-2
@@ -37,6 +38,7 @@
package-2
2.0.1
+ NOASSERTION
cpe:2.3:*:some:package:2:*:*:*:*:*:*:*
pkg:deb/debian/package-2@2.0.1
diff --git a/syft/format/cyclonedxxml/test-fixtures/snapshot/TestCycloneDxImageEncoder.golden b/syft/format/cyclonedxxml/test-fixtures/snapshot/TestCycloneDxImageEncoder.golden
index 12c99a5fa5d..793fc2c885e 100644
--- a/syft/format/cyclonedxxml/test-fixtures/snapshot/TestCycloneDxImageEncoder.golden
+++ b/syft/format/cyclonedxxml/test-fixtures/snapshot/TestCycloneDxImageEncoder.golden
@@ -25,6 +25,7 @@
MIT
+ NOASSERTION
cpe:2.3:*:some:package:1:*:*:*:*:*:*:*
a-purl-1
@@ -39,6 +40,7 @@
package-2
2.0.1
+ NOASSERTION
cpe:2.3:*:some:package:2:*:*:*:*:*:*:*
pkg:deb/debian/package-2@2.0.1
diff --git a/syft/format/internal/cyclonedxutil/helpers/component.go b/syft/format/internal/cyclonedxutil/helpers/component.go
index 526094ac5c3..d64a91cfa4a 100644
--- a/syft/format/internal/cyclonedxutil/helpers/component.go
+++ b/syft/format/internal/cyclonedxutil/helpers/component.go
@@ -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),
diff --git a/syft/format/internal/cyclonedxutil/helpers/component_test.go b/syft/format/internal/cyclonedxutil/helpers/component_test.go
index 7f2b9d24525..7de350d4160 100644
--- a/syft/format/internal/cyclonedxutil/helpers/component_test.go
+++ b/syft/format/internal/cyclonedxutil/helpers/component_test.go
@@ -187,6 +187,7 @@ func Test_encodeCompomentType(t *testing.T) {
Value: "go-module",
},
},
+ Copyright: noAssertion,
},
},
{
@@ -206,6 +207,8 @@ func Test_encodeCompomentType(t *testing.T) {
Value: "binary",
},
},
+
+ Copyright: noAssertion,
},
},
}
diff --git a/syft/format/internal/cyclonedxutil/helpers/licenses.go b/syft/format/internal/cyclonedxutil/helpers/licenses.go
index a092d3abdf3..3ac7ba0135f 100644
--- a/syft/format/internal/cyclonedxutil/helpers/licenses.go
+++ b/syft/format/internal/cyclonedxutil/helpers/licenses.go
@@ -5,11 +5,15 @@ import (
"strings"
"github.com/CycloneDX/cyclonedx-go"
-
"github.com/anchore/syft/internal/spdxlicense"
"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)
@@ -198,3 +202,31 @@ func reduceOuter(expression string) string {
return sb.String()
}
+
+func encodeCopyrights(p pkg.Package) string {
+ if p.Copyrights.Empty() {
+ return noAssertion
+ }
+
+ 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, ", ")
+}
diff --git a/syft/format/internal/spdxutil/helpers/copyright.go b/syft/format/internal/spdxutil/helpers/copyright.go
new file mode 100644
index 00000000000..f269e421853
--- /dev/null
+++ b/syft/format/internal/spdxutil/helpers/copyright.go
@@ -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
+}
diff --git a/syft/format/spdxjson/test-fixtures/snapshot/TestSPDX22JSONRequredProperties.golden b/syft/format/spdxjson/test-fixtures/snapshot/TestSPDX22JSONRequredProperties.golden
index 118247b1dc0..0ca78b8b9da 100644
--- a/syft/format/spdxjson/test-fixtures/snapshot/TestSPDX22JSONRequredProperties.golden
+++ b/syft/format/spdxjson/test-fixtures/snapshot/TestSPDX22JSONRequredProperties.golden
@@ -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"
},
{
diff --git a/syft/format/spdxjson/test-fixtures/snapshot/TestSPDXJSONDirectoryEncoder.golden b/syft/format/spdxjson/test-fixtures/snapshot/TestSPDXJSONDirectoryEncoder.golden
index 35433f6f547..6d0237dc5e0 100644
--- a/syft/format/spdxjson/test-fixtures/snapshot/TestSPDXJSONDirectoryEncoder.golden
+++ b/syft/format/spdxjson/test-fixtures/snapshot/TestSPDXJSONDirectoryEncoder.golden
@@ -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"
},
{
diff --git a/syft/format/spdxjson/test-fixtures/snapshot/TestSPDXJSONImageEncoder.golden b/syft/format/spdxjson/test-fixtures/snapshot/TestSPDXJSONImageEncoder.golden
index 737aed46893..857ab51628c 100644
--- a/syft/format/spdxjson/test-fixtures/snapshot/TestSPDXJSONImageEncoder.golden
+++ b/syft/format/spdxjson/test-fixtures/snapshot/TestSPDXJSONImageEncoder.golden
@@ -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"
},
{
diff --git a/syft/format/spdxjson/test-fixtures/snapshot/TestSPDXRelationshipOrder.golden b/syft/format/spdxjson/test-fixtures/snapshot/TestSPDXRelationshipOrder.golden
index 54533ae4ccf..ebf1ff55bad 100644
--- a/syft/format/spdxjson/test-fixtures/snapshot/TestSPDXRelationshipOrder.golden
+++ b/syft/format/spdxjson/test-fixtures/snapshot/TestSPDXRelationshipOrder.golden
@@ -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"
},
{
diff --git a/syft/format/spdxtagvalue/test-fixtures/snapshot/TestSPDXJSONSPDXIDs.golden b/syft/format/spdxtagvalue/test-fixtures/snapshot/TestSPDXJSONSPDXIDs.golden
index dd946aa235b..4a6cd08c23e 100644
--- a/syft/format/spdxtagvalue/test-fixtures/snapshot/TestSPDXJSONSPDXIDs.golden
+++ b/syft/format/spdxtagvalue/test-fixtures/snapshot/TestSPDXJSONSPDXIDs.golden
@@ -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
diff --git a/syft/format/spdxtagvalue/test-fixtures/snapshot/TestSPDXRelationshipOrder.golden b/syft/format/spdxtagvalue/test-fixtures/snapshot/TestSPDXRelationshipOrder.golden
index 75cab71eb79..168c1262474 100644
--- a/syft/format/spdxtagvalue/test-fixtures/snapshot/TestSPDXRelationshipOrder.golden
+++ b/syft/format/spdxtagvalue/test-fixtures/snapshot/TestSPDXRelationshipOrder.golden
@@ -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
diff --git a/syft/format/spdxtagvalue/test-fixtures/snapshot/TestSPDXTagValueDirectoryEncoder.golden b/syft/format/spdxtagvalue/test-fixtures/snapshot/TestSPDXTagValueDirectoryEncoder.golden
index bccd8acc0f2..9bbc6473d27 100644
--- a/syft/format/spdxtagvalue/test-fixtures/snapshot/TestSPDXTagValueDirectoryEncoder.golden
+++ b/syft/format/spdxtagvalue/test-fixtures/snapshot/TestSPDXTagValueDirectoryEncoder.golden
@@ -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
diff --git a/syft/format/spdxtagvalue/test-fixtures/snapshot/TestSPDXTagValueImageEncoder.golden b/syft/format/spdxtagvalue/test-fixtures/snapshot/TestSPDXTagValueImageEncoder.golden
index c93fb63298f..6eff49f246f 100644
--- a/syft/format/spdxtagvalue/test-fixtures/snapshot/TestSPDXTagValueImageEncoder.golden
+++ b/syft/format/spdxtagvalue/test-fixtures/snapshot/TestSPDXTagValueImageEncoder.golden
@@ -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
diff --git a/syft/format/syftjson/model/package.go b/syft/format/syftjson/model/package.go
index 503709d1f16..ca252b00a1b 100644
--- a/syft/format/syftjson/model/package.go
+++ b/syft/format/syftjson/model/package.go
@@ -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)
diff --git a/syft/format/syftjson/test-fixtures/snapshot/TestDirectoryEncoder.golden b/syft/format/syftjson/test-fixtures/snapshot/TestDirectoryEncoder.golden
index 0ae05a3aa5f..a778afff863 100644
--- a/syft/format/syftjson/test-fixtures/snapshot/TestDirectoryEncoder.golden
+++ b/syft/format/syftjson/test-fixtures/snapshot/TestDirectoryEncoder.golden
@@ -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": [
{
diff --git a/syft/format/syftjson/test-fixtures/snapshot/TestEncodeFullJSONDocument.golden b/syft/format/syftjson/test-fixtures/snapshot/TestEncodeFullJSONDocument.golden
index 6fc9041e9f1..ca774d826b9 100644
--- a/syft/format/syftjson/test-fixtures/snapshot/TestEncodeFullJSONDocument.golden
+++ b/syft/format/syftjson/test-fixtures/snapshot/TestEncodeFullJSONDocument.golden
@@ -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": [
{
diff --git a/syft/format/syftjson/test-fixtures/snapshot/TestImageEncoder.golden b/syft/format/syftjson/test-fixtures/snapshot/TestImageEncoder.golden
index f013f2026a0..517eebab208 100644
--- a/syft/format/syftjson/test-fixtures/snapshot/TestImageEncoder.golden
+++ b/syft/format/syftjson/test-fixtures/snapshot/TestImageEncoder.golden
@@ -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": [
{
diff --git a/syft/format/syftjson/to_format_model.go b/syft/format/syftjson/to_format_model.go
index 42ec48f77d2..7b3ef8247d7 100644
--- a/syft/format/syftjson/to_format_model.go
+++ b/syft/format/syftjson/to_format_model.go
@@ -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),
diff --git a/syft/format/syftjson/to_syft_model.go b/syft/format/syftjson/to_syft_model.go
index 289e91fda52..241a4a15979 100644
--- a/syft/format/syftjson/to_syft_model.go
+++ b/syft/format/syftjson/to_syft_model.go
@@ -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
diff --git a/syft/internal/packagemetadata/discover_type_names.go b/syft/internal/packagemetadata/discover_type_names.go
index 03f8a4cc62b..ed8c4701c89 100644
--- a/syft/internal/packagemetadata/discover_type_names.go
+++ b/syft/internal/packagemetadata/discover_type_names.go
@@ -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)
diff --git a/syft/pkg/cataloger/binary/elf_package_test.go b/syft/pkg/cataloger/binary/elf_package_test.go
index 85fa422211f..93e16343b1b 100644
--- a/syft/pkg/cataloger/binary/elf_package_test.go
+++ b/syft/pkg/cataloger/binary/elf_package_test.go
@@ -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)
}
})
diff --git a/syft/pkg/cataloger/internal/pkgtest/test_generic_parser.go b/syft/pkg/cataloger/internal/pkgtest/test_generic_parser.go
index ae26bb6d13b..ce743bec5d6 100644
--- a/syft/pkg/cataloger/internal/pkgtest/test_generic_parser.go
+++ b/syft/pkg/cataloger/internal/pkgtest/test_generic_parser.go
@@ -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 != "" {
diff --git a/syft/pkg/copyright.go b/syft/pkg/copyright.go
new file mode 100644
index 00000000000..a81fa617a0a
--- /dev/null
+++ b/syft/pkg/copyright.go
@@ -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 ""
+}
diff --git a/syft/pkg/copyright_set.go b/syft/pkg/copyright_set.go
new file mode 100644
index 00000000000..8c4c4ab69ff
--- /dev/null
+++ b/syft/pkg/copyright_set.go
@@ -0,0 +1,88 @@
+//nolint:dupl
+package pkg
+
+import (
+ "fmt"
+ "sort"
+
+ "github.com/anchore/syft/internal/log"
+ "github.com/anchore/syft/syft/artifact"
+ "github.com/mitchellh/hashstructure/v2"
+)
+
+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
+}
diff --git a/syft/pkg/license_set.go b/syft/pkg/license_set.go
index 99593fae2a2..fdf104f4da8 100644
--- a/syft/pkg/license_set.go
+++ b/syft/pkg/license_set.go
@@ -1,13 +1,13 @@
+//nolint:dupl
package pkg
import (
"fmt"
"sort"
- "github.com/mitchellh/hashstructure/v2"
-
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/artifact"
+ "github.com/mitchellh/hashstructure/v2"
)
type LicenseSet struct {
diff --git a/syft/pkg/package.go b/syft/pkg/package.go
index 8ee8d969fae..8b52cfc93b6 100644
--- a/syft/pkg/package.go
+++ b/syft/pkg/package.go
@@ -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) {
diff --git a/syft/pkg/package_test.go b/syft/pkg/package_test.go
index 94896d652ed..c3e5397ea24 100644
--- a/syft/pkg/package_test.go
+++ b/syft/pkg/package_test.go
@@ -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)
}