From 5ddce94c041cbb46a5ac01889f1c05db4cbf39f3 Mon Sep 17 00:00:00 2001 From: duph97 Date: Fri, 26 Jul 2024 12:48:11 +0200 Subject: [PATCH] Bugfix/cyclonedx reader license expression (#280) * Add "packageType" parameter and logic to create purl in CSV Reader. Refactor tests and config readers * Add warn message for unknown packagetypes * Improve log message * Add release note * Add Unit Test and format code * Add documentation * Update CSV reader doc * Move switch-case block to dedicated method * Run tests with packageType=null * Check for null or empty packageType. Make logger non static for testing purposes. * Add mockito dependency for tests. * Add tests for npm, pypi and empty packageType * minor improvement * swap position of artifactId and version in config * formatting * Consistent syntax * Add condition to check for expressions * Take expression as it is instead of parsing and splitting the licenses. * Add release note * Remove unused imports * Add unit test for reading an expression --------- Co-authored-by: ohecker <8004361+ohecker@users.noreply.github.com> --- .../reader/cyclonedx/CyclonedxReader.java | 50 ++++++----- .../cyclonedx/CyclonedxReaderTests.java | 30 +++++++ core/src/test/resources/expressionsbom.json | 84 +++++++++++++++++++ documentation/master-solicitor.asciidoc | 1 + 4 files changed, 144 insertions(+), 21 deletions(-) create mode 100644 core/src/test/resources/expressionsbom.json diff --git a/core/src/main/java/com/devonfw/tools/solicitor/reader/cyclonedx/CyclonedxReader.java b/core/src/main/java/com/devonfw/tools/solicitor/reader/cyclonedx/CyclonedxReader.java index e6258cd2..9d8c495e 100644 --- a/core/src/main/java/com/devonfw/tools/solicitor/reader/cyclonedx/CyclonedxReader.java +++ b/core/src/main/java/com/devonfw/tools/solicitor/reader/cyclonedx/CyclonedxReader.java @@ -115,29 +115,37 @@ public void readInventory(String type, String sourceUrl, Application application else if (licensesNode != null && licensesNode.isEmpty()) { addRawLicense(appComponent, null, null, sourceUrl); } - // Case if licenses field exists and contains licenses + // Case if licenses field exists and contains expressions or licenses else if (licensesNode != null && licensesNode.isEmpty() == false) { - // Iterate over each "license" object within the "licenses" array for (JsonNode licenseNode : licensesNode) { - // Declared License can be written either in "id" or "name" field. Prefer "id" as its written in SPDX - // format. - if (licenseNode.get("license").has("id")) { - if (licenseNode.get("license").has("url")) { - licenseCount++; - addRawLicense(appComponent, licenseNode.get("license").get("id").asText(), - licenseNode.get("license").get("url").asText(), sourceUrl); - } else { - licenseCount++; - addRawLicense(appComponent, licenseNode.get("license").get("id").asText(), null, sourceUrl); - } - } else if (licenseNode.get("license").has("name")) { - if (licenseNode.get("license").has("url")) { - licenseCount++; - addRawLicense(appComponent, licenseNode.get("license").get("name").asText(), - licenseNode.get("license").get("url").asText(), sourceUrl); - } else { - licenseCount++; - addRawLicense(appComponent, licenseNode.get("license").get("name").asText(), null, sourceUrl); + // Check for expressions + if (licenseNode.has("expression")) { + licenseCount++; + addRawLicense(appComponent, licenseNode.get("expression").asText(), null, sourceUrl); + } + + // Check for licenses + if (licenseNode.has("license")) { + // Declared License can be written either in "id" or "name" field. Prefer "id" as its written in SPDX + // format. + if (licenseNode.get("license").has("id")) { + if (licenseNode.get("license").has("url")) { + licenseCount++; + addRawLicense(appComponent, licenseNode.get("license").get("id").asText(), + licenseNode.get("license").get("url").asText(), sourceUrl); + } else { + licenseCount++; + addRawLicense(appComponent, licenseNode.get("license").get("id").asText(), null, sourceUrl); + } + } else if (licenseNode.get("license").has("name")) { + if (licenseNode.get("license").has("url")) { + licenseCount++; + addRawLicense(appComponent, licenseNode.get("license").get("name").asText(), + licenseNode.get("license").get("url").asText(), sourceUrl); + } else { + licenseCount++; + addRawLicense(appComponent, licenseNode.get("license").get("name").asText(), null, sourceUrl); + } } } } diff --git a/core/src/test/java/com/devonfw/tools/solicitor/reader/cyclonedx/CyclonedxReaderTests.java b/core/src/test/java/com/devonfw/tools/solicitor/reader/cyclonedx/CyclonedxReaderTests.java index 7e68c505..1b2fda39 100644 --- a/core/src/test/java/com/devonfw/tools/solicitor/reader/cyclonedx/CyclonedxReaderTests.java +++ b/core/src/test/java/com/devonfw/tools/solicitor/reader/cyclonedx/CyclonedxReaderTests.java @@ -185,4 +185,34 @@ public void readNpmFileAndCheckSize() { } assertTrue(found); } + + /** + * Test the {@link CyclonedxReader#readInventory()} method. Input file is an SBOM containing expressions. + */ + @Test + public void readExpression() { + + // Always return a non-empty String for npm purls + Mockito.when(this.delegatingPurlHandler.pathFor(Mockito.startsWith("pkg:maven/"))).thenReturn("foo"); + + Application application = this.modelFactory.newApplication("testApp", "0.0.0.TEST", "1.1.2111", "http://bla.com", + "Angular"); + this.cdxr.setModelFactory(this.modelFactory); + this.cdxr.setInputStreamFactory(new FileInputStreamFactory()); + this.cdxr.setDelegatingPackageURLHandler(this.delegatingPurlHandler); + this.cdxr.readInventory("npm", "src/test/resources/expressionsbom.json", application, UsagePattern.DYNAMIC_LINKING, + "cyclonedx", null, null); + LOG.info(application.toString()); + + boolean found = false; + + for (ApplicationComponent ap : application.getApplicationComponents()) { + if (ap.getArtifactId().equals("hk2-locator") && ap.getVersion().equals("2.5.0-b42")) { + found = true; + assertEquals("(CDDL-1.0 OR GPL-2.0-with-classpath-exception)", ap.getRawLicenses().get(0).getDeclaredLicense()); + + } + } + assertTrue(found); + } } diff --git a/core/src/test/resources/expressionsbom.json b/core/src/test/resources/expressionsbom.json new file mode 100644 index 00000000..3311faa4 --- /dev/null +++ b/core/src/test/resources/expressionsbom.json @@ -0,0 +1,84 @@ +{ + "bomFormat": "CycloneDX", + "specVersion": "1.5", + "serialNumber": "urn:uuid:dd714b36-700a-4a2c-be85-eeb8723c2489", + "version": 1, + "metadata": { + "timestamp": "2024-07-08T17:39:55Z", + "tools": { + "components": [ + { + "group": "@cyclonedx", + "name": "cdxgen", + "version": "10.7.1", + "purl": "pkg:npm/%40cyclonedx/cdxgen@10.7.1", + "type": "application", + "bom-ref": "pkg:npm/@cyclonedx/cdxgen@10.7.1", + "author": "OWASP Foundation", + "publisher": "OWASP Foundation" + } + ] + }, + "authors": [ + { + "name": "OWASP Foundation" + } + ], + "lifecycles": [ + { + "phase": "build" + } + ], + "component": { + "group": "", + "name": "lib", + "version": "latest", + "type": "application", + "bom-ref": "pkg:maven/lib@latest", + "purl": "pkg:maven/lib@latest" + } + }, + "components": [ + { + "publisher": "Oracle Corporation", + "group": "org.glassfish.hk2", + "name": "hk2-locator", + "version": "2.5.0-b42", + "description": "${project.name}", + "licenses": [ + { + "expression": "(CDDL-1.0 OR GPL-2.0-with-classpath-exception)" + } + ], + "purl": "pkg:maven/org.glassfish.hk2/hk2-locator@2.5.0-b42?type=jar", + "externalReferences": [ + { + "type": "vcs", + "url": "https://hk2-project.github.io" + } + ], + "type": "library", + "bom-ref": "pkg:maven/org.glassfish.hk2/hk2-locator@2.5.0-b42?type=jar", + "evidence": { + "identity": { + "field": "purl", + "confidence": 1, + "methods": [ + { + "technique": "manifest-analysis", + "confidence": 1, + "value": "hk2-locator-2.5.0-b42.jar" + } + ] + } + }, + "properties": [ + { + "name": "SrcFile", + "value": "hk2-locator-2.5.0-b42.jar" + } + ] + } + ], + "dependencies": [] +} \ No newline at end of file diff --git a/documentation/master-solicitor.asciidoc b/documentation/master-solicitor.asciidoc index 60cdc923..cdf8bf02 100644 --- a/documentation/master-solicitor.asciidoc +++ b/documentation/master-solicitor.asciidoc @@ -1901,6 +1901,7 @@ Spring beans implementing this interface will be called at certain points in the Changes in 1.25.0:: * https://github.com/devonfw/solicitor/issues/277: When reading content (license texts or notice files) within the scancode adapter files which are greater than 1 million bytes will be skipped. This avoids large memory consumption and resulting instability. * https://github.com/devonfw/solicitor/issues/274: Fixed issue where no packageURL was created when using the CSV reader. Added attribute 'packageType'. +* https://github.com/devonfw/solicitor/issues/279: Fixed issue where the CycloneDX reader could not read licenses declared as 'expression'. Changes in 1.24.2:: * https://github.com/devonfw/solicitor/pull/271: Fixed an incompatibility with JDK 8.