diff --git a/internal/testing/testdata/exampledata/small-legal-cyclonedx-no-inline.json b/internal/testing/testdata/exampledata/small-legal-cyclonedx-no-inline.json new file mode 100644 index 0000000000..ca1578e3ed --- /dev/null +++ b/internal/testing/testdata/exampledata/small-legal-cyclonedx-no-inline.json @@ -0,0 +1,280 @@ +{ + "bomFormat" : "CycloneDX", + "specVersion" : "1.4", + "serialNumber" : "urn:uuid:0697952e-9848-4785-95bf-f81ff9731682", + "version" : 1, + "metadata" : { + "timestamp" : "2022-11-09T11:14:31Z", + "tools" : [ + { + "vendor" : "OWASP Foundation", + "name" : "CycloneDX Maven plugin", + "version" : "2.7.1", + "hashes" : [ + { + "alg" : "SHA3-512", + "content" : "72ea0ed8faa3cc4493db96d0223094842e7153890b091ff364040ad3ad89363157fc9d1bd852262124aec83134f0c19aa4fd0fa482031d38a76d74dfd36b7964" + } + ] + } + ], + "component" : { + "group" : "org.acme", + "name" : "getting-started", + "version" : "1.0.0-SNAPSHOT", + "licenses": [ + { + "license": { + "id": "GPL-2.0" + } + }, + { + "license": { + "id": "LGPL-3.0-or-later" + } + } + ], + "hashes" : [ + { + "alg" : "SHA3-512", + "content" : "85240ed8faa3cc4493db96d0223094842e7153890b091ff364040ad3ad89363157fc9d1bd852262124aec83134f0c19aa4fd0fa482031d38a76d74dfd36b7964" + } + ], + "purl" : "pkg:maven/org.acme/getting-started@1.0.0-SNAPSHOT?type=jar", + "type" : "library", + "bom-ref" : "pkg:maven/org.acme/getting-started@1.0.0-SNAPSHOT?type=jar" + } + }, + "components" : [ + { + "publisher" : "JBoss by Red Hat", + "group" : "io.quarkus", + "name" : "quarkus-resteasy-reactive", + "version" : "2.13.4.Final", + "description" : "A JAX-RS implementation utilizing build time processing and Vert.x. This extension is not compatible with the quarkus-resteasy extension, or any of the extensions that depend on it.", + "scope" : "optional", + "hashes" : [ + { + "alg" : "MD5", + "content" : "bf39044af8c6ba66fc3beb034bc82ae8" + }, + { + "alg" : "SHA3-512", + "content" : "615e56bdfeb591af8b5fdeadf019f8fa729643232d7e0768674411a7d959bb00e12e114280a6949f871514e1a86e01e0033372a0a826d15720050d7cffb80e69" + } + ], + "licenses" : [ + { + "license" : { + "id" : "Apache-2.0" + } + } + ], + "purl" : "pkg:maven/io.quarkus/quarkus-resteasy-reactive@2.13.4.Final?type=jar", + "externalReferences" : [ + { + "type" : "distribution", + "url" : "https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/" + }, + { + "type" : "issue-tracker", + "url" : "https://github.com/quarkusio/quarkus/issues/" + }, + { + "type" : "vcs", + "url" : "https://github.com/quarkusio/quarkus" + }, + { + "type" : "website", + "url" : "http://www.jboss.org" + }, + { + "type" : "mailing-list", + "url" : "http://lists.jboss.org/pipermail/jboss-user/" + } + ], + "type" : "library", + "bom-ref" : "pkg:maven/io.quarkus/quarkus-resteasy-reactive@2.13.4.Final?type=jar" + }, + { + "publisher" : "SmallRye", + "group" : "io.smallrye.reactive", + "name" : "smallrye-mutiny-vertx-uri-template", + "version" : "2.27.0", + "description" : "SmallRye Build Parent POM", + "hashes" : [ + { + "alg" : "MD5", + "content" : "8756663af131035a2090d83f5f1b4054" + } + ], + "licenses" : [ + { + "expression" : "Apache-2.0 AND (MIT OR GPL-2.0-only)" + } + ], + "purl" : "pkg:maven/io.smallrye.reactive/smallrye-mutiny-vertx-uri-template@2.27.0?type=jar", + "externalReferences" : [ + { + "type" : "website", + "url" : "https://wwww.smallrye.io" + }, + { + "type" : "issue-tracker", + "url" : "https://github.com/smallrye/smallrye-mutiny-vertx-bindings/issues" + }, + { + "type" : "vcs", + "url" : "https://github.com/smallrye/smallrye-mutiny-vertx-bindings" + }, + { + "type" : "distribution", + "url" : "https://oss.sonatype.org/service/local/staging/deploy/maven2/" + } + ], + "type" : "library", + "bom-ref" : "pkg:maven/io.smallrye.reactive/smallrye-mutiny-vertx-uri-template@2.27.0?type=jar" + }, + { + "publisher" : "JBoss by Red Hat", + "group" : "io.quarkus", + "name" : "quarkus-resteasy-reactive-common", + "version" : "2.13.4.Final", + "description" : "Common runtime parts of Quarkus RESTEasy Reactive", + "hashes" : [ + { + "alg" : "SHA3-512", + "content" : "54ffa51cb2fb25e70871e4b69489814ebb3d23d4f958e83ef1f811c00a8753c6c30c5bbc1b48b6427357eb70e5c35c7b357f5252e246fbfa00b90ee22ad095e1" + } + ], + "licenses" : [ + { + "license": { + "id": "Apache-2.0" + } + }, + { + "license": { + "name": "Custom license", + "text": { + "content": "This is the text of the custom license I wrote" + } + } + }, + { + "license": { + "name": "Custom license 2" + } + } + ], + "purl" : "pkg:maven/io.quarkus/quarkus-resteasy-reactive-common@2.13.4.Final?type=jar", + "externalReferences" : [ + { + "type" : "mailing-list", + "url" : "http://lists.jboss.org/pipermail/jboss-user/" + } + ], + "type" : "library", + "bom-ref" : "pkg:maven/io.quarkus/quarkus-resteasy-reactive-common@2.13.4.Final?type=jar" + }, + { + "publisher" : "JBoss by Red Hat", + "group" : "io.quarkus", + "name" : "netbase", + "version" : ".3", + "description" : "Common runtime parts of Quarkus RESTEasy Reactive", + "hashes" : [ + { + "alg" : "SHA3-512", + "content" : "87gna51cb2fb25e70871e4b69489814ebb3d23d4f958e83ef1f811c00a8753c6c30c5bbc1b48b6427357eb70e5c35c7b357f5252e246fbfa00b90ee22ad095e1" + } + ], + "licenses" : [ + { + "license": { + "id": "Apache-2.0" + } + }, + { + "license": { + "name": "Custom license", + "text": { + "content": "This is the text of the custom license I wrote" + } + } + } + ], + "purl" : "pkg:deb/debian/netbase@6.3?arch=all\u0026distro=debian-11", + "externalReferences" : [ + { + "type" : "mailing-list", + "url" : "http://lists.jboss.org/pipermail/jboss-user/" + } + ], + "type" : "library", + "bom-ref" : "pkg:deb/debian/netbase@6.3?arch=all\u0026distro=debian-11\u0026package-id=913906225fd3778b" + }, + { + "publisher" : "Eclipse Foundation", + "group" : "org.eclipse.microprofile.context-propagation", + "name" : "microprofile-context-propagation-api", + "version" : "1.2", + "description" : "MicroProfile Context Propagation :: API", + "hashes" : [ + { + "alg" : "SHA-256", + "content" : "1576e21f3bf9cc3a3092e7cd40e9c9fef70532223af98a9218c1c9c885a71251" + } + ], + "licenses" : [ + { + "license": { + "name": "Custom license", + "bom-ref" : "LicenseRef-a7fb6b15" + } + }, + { + "license": { + "name": "Custom license 2", + "bom-ref" : "LicenseRef-59a01e67" + } + } + ], + "purl" : "pkg:maven/org.eclipse.microprofile.context-propagation/microprofile-context-propagation-api@1.2?type=jar", + "externalReferences" : [ + { + "type" : "website", + "url" : "http://www.eclipse.org/" + }, + { + "type" : "distribution", + "url" : "https://oss.sonatype.org/service/local/staging/deploy/maven2/" + }, + { + "type" : "issue-tracker", + "url" : "https://github.com/eclipse/microprofile-context-propagation/issues" + }, + { + "type" : "vcs", + "url" : "https://github.com/eclipse/microprofile-context-propagation" + } + ], + "type" : "library", + "bom-ref" : "pkg:maven/org.eclipse.microprofile.context-propagation/microprofile-context-propagation-api@1.2?type=jar" + } + ], + "dependencies" : [ + { + "ref" : "pkg:maven/org.acme/getting-started@1.0.0-SNAPSHOT?type=jar", + "dependsOn" : [ + "pkg:maven/io.quarkus/quarkus-resteasy-reactive@2.13.4.Final?type=jar" + ] + }, + { + "ref" : "pkg:maven/io.quarkus/quarkus-resteasy-reactive@2.13.4.Final?type=jar", + "dependsOn" : [ + "pkg:maven/io.quarkus/quarkus-resteasy-reactive-common@2.13.4.Final?type=jar" + ] + } + ] +} diff --git a/internal/testing/testdata/testdata.go b/internal/testing/testdata/testdata.go index 91a1b97c57..a337f3994e 100644 --- a/internal/testing/testdata/testdata.go +++ b/internal/testing/testdata/testdata.go @@ -176,6 +176,9 @@ var ( //go:embed exampledata/small-legal-cyclonedx.json CycloneDXLegalExample []byte + //go:embed exampledata/small-legal-cyclonedx-no-inline.json + CycloneDXLegalNoInlineExample []byte + // json format json = jsoniter.ConfigCompatibleWithStandardLibrary // CycloneDX VEX testdata unaffected @@ -1403,6 +1406,22 @@ var ( }, } + cdxLegalHasSBOMNoInLine = []assembler.HasSBOMIngest{ + { + Artifact: &model.ArtifactInputSpec{ + Algorithm: "sha3-512", + Digest: "85240ed8faa3cc4493db96d0223094842e7153890b091ff364040ad3ad89363157fc9d1bd852262124aec83134f0c19aa4fd0fa482031d38a76d74dfd36b7964", + }, + HasSBOM: &model.HasSBOMInputSpec{ + Uri: "urn:uuid:0697952e-9848-4785-95bf-f81ff9731682", + Algorithm: "sha256", + Digest: "09522e1c53eb2b919446c2e904f6517482de731dc6e61d7e7ad559675cb9355b", + DownloadLocation: "", + KnownSince: cdxQuarkusTime, + }, + }, + } + CdxQuarkusLegalPredicates = assembler.IngestPredicates{ IsDependency: cdxLegalDeps, IsOccurrence: cdxLegalOccurrence, @@ -1410,6 +1429,120 @@ var ( CertifyLegal: cdxLegalCertifyLegal, } + CdxQuarkusLegalNoInlinePredicates = assembler.IngestPredicates{ + IsDependency: cdxLegalDeps, + IsOccurrence: cdxLegalOccurrence, + HasSBOM: cdxLegalHasSBOMNoInLine, + CertifyLegal: cdxLegalCertifyLegalNoInline, + } + + cdxLegalCertifyLegalNoInline = []assembler.CertifyLegalIngest{ + { + Pkg: cdxNetbasePack, + Declared: []model.LicenseInputSpec{ + { + Name: "Apache-2.0", + ListVersion: &lvUnknown, + }, + { + Name: "LicenseRef-a7fb6b15", + Inline: &customLincenseText, + }, + }, + CertifyLegal: &model.CertifyLegalInputSpec{ + DeclaredLicense: "Apache-2.0 AND LicenseRef-a7fb6b15", + Justification: "Found in CycloneDX document", + TimeScanned: cdxQuarkusTime, + }, + }, + { + Pkg: cdxResteasyPack, + Declared: []model.LicenseInputSpec{ + { + Name: "Apache-2.0", + ListVersion: &lvUnknown, + }, + }, + CertifyLegal: &model.CertifyLegalInputSpec{ + DeclaredLicense: "Apache-2.0", + Justification: "Found in CycloneDX document", + TimeScanned: cdxQuarkusTime, + }, + }, + { + Pkg: cdxReactiveCommonPack, + Declared: []model.LicenseInputSpec{ + { + Name: "Apache-2.0", + ListVersion: &lvUnknown, + }, + { + Name: "LicenseRef-a7fb6b15", + Inline: &customLincenseText, + }, + }, + CertifyLegal: &model.CertifyLegalInputSpec{ + DeclaredLicense: "Apache-2.0 AND LicenseRef-a7fb6b15 AND LicenseRef-59a01e67", + Justification: "Found in CycloneDX document", + TimeScanned: cdxQuarkusTime, + }, + }, + { + Pkg: cdxSmallRye, + Declared: []model.LicenseInputSpec{ + { + Name: "Apache-2.0", + ListVersion: &lvUnknown, + }, + { + Name: "MIT", + ListVersion: &lvUnknown, + }, + { + Name: "GPL-2.0-only", + ListVersion: &lvUnknown, + }, + }, + CertifyLegal: &model.CertifyLegalInputSpec{ + DeclaredLicense: "Apache-2.0 AND (MIT OR GPL-2.0-only)", + Justification: "Found in CycloneDX document", + TimeScanned: cdxQuarkusTime, + }, + }, + { + Pkg: cdxTopQuarkusPack, + Declared: []model.LicenseInputSpec{ + { + Name: "GPL-2.0", + ListVersion: &lvUnknown, + }, + { + Name: "LGPL-3.0-or-later", + ListVersion: &lvUnknown, + }, + }, + CertifyLegal: &model.CertifyLegalInputSpec{ + DeclaredLicense: "GPL-2.0 AND LGPL-3.0-or-later", + Justification: "Found in CycloneDX document", + TimeScanned: cdxQuarkusTime, + }, + }, + { + Pkg: cdxMicroprofilePack, + Declared: []model.LicenseInputSpec{ + { + Name: "LicenseRef-a7fb6b15", + Inline: &customLincenseText, + }, + }, + CertifyLegal: &model.CertifyLegalInputSpec{ + DeclaredLicense: "LicenseRef-a7fb6b15 AND LicenseRef-59a01e67", + Justification: "Found in CycloneDX document", + TimeScanned: cdxQuarkusTime, + }, + }, + } + cdxWebAppPackage, _ = asmhelpers.PurlToPkg("pkg:npm/web-app@1.0.0") cdxBootstrapPackage, _ = asmhelpers.PurlToPkg("pkg:npm/bootstrap@4.0.0-beta.2") diff --git a/pkg/ingestor/parser/common/license.go b/pkg/ingestor/parser/common/license.go index f26bfe07be..3185dca720 100644 --- a/pkg/ingestor/parser/common/license.go +++ b/pkg/ingestor/parser/common/license.go @@ -80,25 +80,30 @@ func ParseLicenses(exp string, lv *string, inLineMap map[string]string) []model. if slices.Contains(ignore, p) { continue } - var license model.LicenseInputSpec + var license *model.LicenseInputSpec if inline, ok := inLineMap[p]; ok { - license = model.LicenseInputSpec{ - Name: p, - Inline: &inline, - ListVersion: lv, - } - } else if lv != nil { - license = model.LicenseInputSpec{ - Name: p, - ListVersion: lv, + license = &model.LicenseInputSpec{ + Name: p, + Inline: &inline, } } else { - license = model.LicenseInputSpec{ - Name: p, - ListVersion: &unknown, + if !strings.HasPrefix(p, "LicenseRef") { + if lv != nil { + license = &model.LicenseInputSpec{ + Name: p, + ListVersion: lv, + } + } else { + license = &model.LicenseInputSpec{ + Name: p, + ListVersion: &unknown, + } + } } } - rv = append(rv, license) + if license != nil { + rv = append(rv, *license) + } } return rv } @@ -113,3 +118,21 @@ func HashLicense(inline string) string { func CombineLicense(licenses []string) string { return strings.Join(licenses, " AND ") } + +func FixSPDXLicenseExpression(licenseExpression string, inLineMap map[string]string) string { + modifiedLicenseExpression := licenseExpression + for _, part := range strings.Split(licenseExpression, " ") { + p := strings.Trim(part, "()+") + if slices.Contains(ignore, p) { + continue + } + if strings.HasPrefix(p, "LicenseRef-") { + if inline, ok := inLineMap[p]; ok { + newLicenseName := HashLicense(inline) + inLineMap[newLicenseName] = inline + modifiedLicenseExpression = strings.ReplaceAll(modifiedLicenseExpression, p, newLicenseName) + } + } + } + return modifiedLicenseExpression +} diff --git a/pkg/ingestor/parser/cyclonedx/parser_cyclonedx.go b/pkg/ingestor/parser/cyclonedx/parser_cyclonedx.go index 97cff5c697..ec55aa1b92 100644 --- a/pkg/ingestor/parser/cyclonedx/parser_cyclonedx.go +++ b/pkg/ingestor/parser/cyclonedx/parser_cyclonedx.go @@ -305,7 +305,6 @@ func getLicenseFromName(c *cyclonedxParser, compLicense cdx.LicenseChoice) strin license = compLicense.License.BOMRef } else { license = common.HashLicense(compLicense.License.Name) - c.licenseInLine[license] = compLicense.License.Name } } else { license = compLicense.License.ID diff --git a/pkg/ingestor/parser/cyclonedx/parser_cyclonedx_test.go b/pkg/ingestor/parser/cyclonedx/parser_cyclonedx_test.go index 28fde0f2bd..cdff1ce7d2 100644 --- a/pkg/ingestor/parser/cyclonedx/parser_cyclonedx_test.go +++ b/pkg/ingestor/parser/cyclonedx/parser_cyclonedx_test.go @@ -136,6 +136,15 @@ func Test_cyclonedxParser(t *testing.T) { }, wantPredicates: &testdata.CdxQuarkusLegalPredicates, wantErr: false, + }, { + name: "valid CycloneDX VEX document with LicenseRef and no inline", + doc: &processor.Document{ + Blob: testdata.CycloneDXLegalNoInlineExample, + Format: processor.FormatJSON, + Type: processor.DocumentCycloneDX, + }, + wantPredicates: &testdata.CdxQuarkusLegalNoInlinePredicates, + wantErr: false, }} for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/pkg/ingestor/parser/spdx/parse_spdx.go b/pkg/ingestor/parser/spdx/parse_spdx.go index dc2fa31a0e..1f8d8b68d3 100644 --- a/pkg/ingestor/parser/spdx/parse_spdx.go +++ b/pkg/ingestor/parser/spdx/parse_spdx.go @@ -42,6 +42,7 @@ type spdxParser struct { packageLegals map[string][]*model.CertifyLegalInputSpec filePackages map[string][]*model.PkgInputSpec fileArtifacts map[string][]*model.ArtifactInputSpec + licenseInLine map[string]string topLevelPackages []*model.PkgInputSpec topLevelArtifacts map[string][]*model.ArtifactInputSpec identifierStrings *common.IdentifierStrings @@ -58,6 +59,7 @@ func NewSpdxParser() common.DocumentParser { filePackages: map[string][]*model.PkgInputSpec{}, fileArtifacts: map[string][]*model.ArtifactInputSpec{}, topLevelArtifacts: make(map[string][]*model.ArtifactInputSpec), + licenseInLine: map[string]string{}, identifierStrings: &common.IdentifierStrings{}, topLevelIsHeuristic: false, } @@ -92,6 +94,11 @@ func (s *spdxParser) Parse(ctx context.Context, doc *processor.Document) error { return err } + // collect SPDX otherLicenses to InLineMap to be used for license predicate creation + for _, o := range s.spdxDoc.OtherLicenses { + s.licenseInLine[o.LicenseIdentifier] = o.ExtractedText + } + return nil } @@ -356,22 +363,15 @@ func (s *spdxParser) GetPredicates(ctx context.Context) *assembler.IngestPredica } for id, cls := range s.packageLegals { for _, cl := range cls { - dec := common.ParseLicenses(cl.DeclaredLicense, &lv, nil) - dis := common.ParseLicenses(cl.DiscoveredLicense, &lv, nil) - for i := range dec { - o, n := fixLicense(ctx, &dec[i], s.spdxDoc.OtherLicenses) - if o != "" { - exp := strings.ReplaceAll(cl.DeclaredLicense, o, n) - cl.DeclaredLicense = exp - } - } - for i := range dis { - o, n := fixLicense(ctx, &dis[i], s.spdxDoc.OtherLicenses) - if o != "" { - exp := strings.ReplaceAll(cl.DiscoveredLicense, o, n) - cl.DiscoveredLicense = exp - } - } + + modifiedDecLicense := common.FixSPDXLicenseExpression(cl.DeclaredLicense, s.licenseInLine) + modifiedDisLicense := common.FixSPDXLicenseExpression(cl.DiscoveredLicense, s.licenseInLine) + + cl.DeclaredLicense = modifiedDecLicense + cl.DiscoveredLicense = modifiedDisLicense + + dec := common.ParseLicenses(modifiedDecLicense, &lv, s.licenseInLine) + dis := common.ParseLicenses(modifiedDisLicense, &lv, s.licenseInLine) for _, pkg := range s.packagePackages[id] { cli := assembler.CertifyLegalIngest{ Pkg: pkg, @@ -412,30 +412,6 @@ func (s *spdxParser) GetPredicates(ctx context.Context) *assembler.IngestPredica return preds } -func fixLicense(ctx context.Context, l *model.LicenseInputSpec, ol []*spdx.OtherLicense) (string, string) { - logger := logging.FromContext(ctx) - if !strings.HasPrefix(l.Name, "LicenseRef-") { - return "", "" - } - oldName := l.Name - l.ListVersion = nil - found := false - for _, o := range ol { - if o.LicenseIdentifier == l.Name { - l.Inline = &o.ExtractedText - found = true - break - } - } - if !found { - logger.Warnf("License identifier %s not found in OtherLicenses", l.Name) - s := "Not found" - l.Inline = &s - } - l.Name = common.HashLicense(*l.Inline) - return oldName, l.Name -} - func isDependency(rel string) bool { return map[string]bool{ spdx_common.TypeRelationshipContains: true,