├── src ├── test │ ├── resources │ │ ├── test.txt │ │ ├── .spdx-builder.yml │ │ ├── custom_formats.yml │ │ ├── ort_with_issue.yml │ │ └── ort_sample.yml │ └── java │ │ └── com │ │ └── philips │ │ └── research │ │ └── spdxbuilder │ │ ├── persistence │ │ ├── spdx │ │ │ ├── SpdxRefTest.java │ │ │ ├── ExternalReferenceTest.java │ │ │ ├── SpdxPartyTest.java │ │ │ └── TagValueDocumentTest.java │ │ ├── tree │ │ │ ├── TreeFormatterTest.java │ │ │ └── TreeReaderTest.java │ │ ├── ort │ │ │ ├── OrtReaderTest.java │ │ │ └── OrtJsonTest.java │ │ ├── blackduck │ │ │ ├── UriHelperTest.java │ │ │ ├── PackageIdentifierTest.java │ │ │ └── BlackDuckApiTest.java │ │ ├── bom_base │ │ │ ├── BomBaseClientTest.java │ │ │ └── BomBaseApiTest.java │ │ └── license_scanner │ │ │ ├── LicenseKnowledgeBaseTest.java │ │ │ └── LicenseScannerClientTest.java │ │ ├── core │ │ └── domain │ │ │ ├── RelationTest.java │ │ │ ├── BillOfMaterialsTest.java │ │ │ ├── PackageTest.java │ │ │ ├── PurlGlobTest.java │ │ │ └── LicenseDictionaryTest.java │ │ └── controller │ │ ├── TreeConfigurationTest.java │ │ ├── UploadClientTest.java │ │ └── OrtConfigurationTest.java └── main │ ├── java │ └── com │ │ └── philips │ │ └── research │ │ └── spdxbuilder │ │ ├── package-info.java │ │ ├── core │ │ ├── package-info.java │ │ ├── domain │ │ │ ├── package-info.java │ │ │ ├── LicenseException.java │ │ │ ├── domain.puml │ │ │ ├── licenses.puml │ │ │ ├── Party.java │ │ │ ├── Relation.java │ │ │ ├── BillOfMaterials.java │ │ │ ├── PurlGlob.java │ │ │ ├── ConversionInteractor.java │ │ │ ├── LicenseParser.java │ │ │ └── License.java │ │ ├── BomReader.java │ │ ├── BomProcessor.java │ │ ├── BusinessException.java │ │ ├── KnowledgeBase.java │ │ └── ConversionService.java │ │ ├── controller │ │ ├── package-info.java │ │ ├── BlackDuckCommand.java │ │ ├── UploadClient.java │ │ ├── TreeConfiguration.java │ │ ├── TreeCommand.java │ │ ├── AbstractCommand.java │ │ ├── OrtCommand.java │ │ └── OrtConfiguration.java │ │ ├── persistence │ │ ├── package-info.java │ │ ├── ort │ │ │ ├── package-info.java │ │ │ └── OrtReaderException.java │ │ ├── spdx │ │ │ ├── package-info.java │ │ │ ├── SpdxException.java │ │ │ ├── SpdxRef.java │ │ │ ├── ExternalReference.java │ │ │ ├── SpdxLicense.java │ │ │ ├── TagValueDocument.java │ │ │ └── SpdxParty.java │ │ ├── tree │ │ │ ├── package-info.java │ │ │ ├── TreeException.java │ │ │ ├── TreeFormatter.java │ │ │ ├── TreeReader.java │ │ │ └── TreeWriter.java │ │ ├── blackduck │ │ │ ├── package-info.java │ │ │ ├── BlackDuckComponentDetails.java │ │ │ ├── BlackDuckException.java │ │ │ ├── BlackDuckProduct.java │ │ │ ├── BlackDuckComponent.java │ │ │ ├── UriHelper.java │ │ │ ├── blackduck.puml │ │ │ └── PackageIdentifier.java │ │ ├── bom_base │ │ │ ├── package-info.java │ │ │ ├── BomBaseException.java │ │ │ ├── PackageMetadata.java │ │ │ ├── BomBaseKnowledgeBase.java │ │ │ ├── BomBaseClient.java │ │ │ └── BomBaseApi.java │ │ └── license_scanner │ │ │ ├── package-info.java │ │ │ ├── ResultJson.java │ │ │ ├── LicenseScannerException.java │ │ │ ├── LicenseScannerApi.java │ │ │ ├── LicenseKnowledgeBase.java │ │ │ └── LicenseScannerClient.java │ │ ├── layers.puml │ │ └── SpdxBuilder.java │ └── resources │ └── treeformats.yml ├── docs ├── domain.png ├── layers.png ├── blackduck.png └── usage_with_black_duck.md ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── .gitattributes ├── cosign.pub ├── .spdx-builder.yml ├── .gitignore ├── CODEOWNERS ├── gradle.properties ├── settings.gradle ├── .github ├── dependabot.yml └── workflows │ ├── gradle.yml │ ├── licenses.yml │ └── release.yml ├── LICENSE.md ├── CONTRIBUTING.md ├── gradlew.bat └── README.md /src/test/resources/test.txt: -------------------------------------------------------------------------------- 1 | This is just a test file. 2 | -------------------------------------------------------------------------------- /docs/domain.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/philips-software/spdx-builder/HEAD/docs/domain.png -------------------------------------------------------------------------------- /docs/layers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/philips-software/spdx-builder/HEAD/docs/layers.png -------------------------------------------------------------------------------- /docs/blackduck.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/philips-software/spdx-builder/HEAD/docs/blackduck.png -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/philips-software/spdx-builder/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # 2 | # https://help.github.com/articles/dealing-with-line-endings/ 3 | # 4 | # These are explicitly windows files and should use crlf 5 | *.bat text eol=crlf 6 | 7 | -------------------------------------------------------------------------------- /cosign.pub: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEEiaj4r6jL7tog9x0l92idFd9xQI4 3 | vI0Rqtjxr3alz4i2NvpOHsSe/vV8tKje434bCKXPDtuWwANcD2GHSW35Cg== 4 | -----END PUBLIC KEY----- 5 | -------------------------------------------------------------------------------- /src/test/resources/.spdx-builder.yml: -------------------------------------------------------------------------------- 1 | document: 2 | title: "Sample file" 3 | organization: "SPDX-Builder development" 4 | comment: "Test file of SPDX-Builder" 5 | projects: 6 | - id: 'NPM::mime-types:2.1.18' 7 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.3-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /.spdx-builder.yml: -------------------------------------------------------------------------------- 1 | document: 2 | title: "SPDX-Builder command line tool" 3 | organization: "Philips Research" 4 | comment: "This is an experimental tool" 5 | key: 6 | namespace: "https://research.philips.com/spdx-builder" 7 | internal: 8 | - com.philips.research:* 9 | -------------------------------------------------------------------------------- /src/main/java/com/philips/research/spdxbuilder/package-info.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2021, Koninklijke Philips N.V., https://www.philips.com 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | @pl.tlinkowski.annotation.basic.NonNullPackage 7 | package com.philips.research.spdxbuilder; 8 | -------------------------------------------------------------------------------- /src/main/java/com/philips/research/spdxbuilder/core/package-info.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2021, Koninklijke Philips N.V., https://www.philips.com 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | @pl.tlinkowski.annotation.basic.NonNullPackage 6 | package com.philips.research.spdxbuilder.core; 7 | -------------------------------------------------------------------------------- /src/main/java/com/philips/research/spdxbuilder/controller/package-info.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2021, Koninklijke Philips N.V., https://www.philips.com 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | @pl.tlinkowski.annotation.basic.NonNullPackage 7 | package com.philips.research.spdxbuilder.controller; 8 | -------------------------------------------------------------------------------- /src/main/java/com/philips/research/spdxbuilder/core/domain/package-info.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2021, Koninklijke Philips N.V., https://www.philips.com 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | @pl.tlinkowski.annotation.basic.NonNullPackage 7 | package com.philips.research.spdxbuilder.core.domain; 8 | -------------------------------------------------------------------------------- /src/main/java/com/philips/research/spdxbuilder/persistence/package-info.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2021, Koninklijke Philips N.V., https://www.philips.com 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | @pl.tlinkowski.annotation.basic.NonNullPackage 7 | package com.philips.research.spdxbuilder.persistence; 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Maven ### 2 | target/ 3 | 4 | ### Intellij ### 5 | .idea/ 6 | *.iml 7 | 8 | ### Ignore all generated SPDX files 9 | *.spdx 10 | 11 | # Ignore Gradle project-specific cache directory 12 | .gradle 13 | 14 | # Ignore Gradle build output directory 15 | build/ 16 | 17 | # Ignore ORT results 18 | ort/ 19 | -------------------------------------------------------------------------------- /src/main/java/com/philips/research/spdxbuilder/persistence/ort/package-info.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2021, Koninklijke Philips N.V., https://www.philips.com 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | @pl.tlinkowski.annotation.basic.NonNullPackage 7 | package com.philips.research.spdxbuilder.persistence.ort; 8 | -------------------------------------------------------------------------------- /src/main/java/com/philips/research/spdxbuilder/persistence/spdx/package-info.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2021, Koninklijke Philips N.V., https://www.philips.com 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | @pl.tlinkowski.annotation.basic.NonNullPackage 7 | package com.philips.research.spdxbuilder.persistence.spdx; 8 | -------------------------------------------------------------------------------- /src/main/java/com/philips/research/spdxbuilder/persistence/tree/package-info.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2021, Koninklijke Philips N.V., https://www.philips.com 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | @pl.tlinkowski.annotation.basic.NonNullPackage 7 | package com.philips.research.spdxbuilder.persistence.tree; 8 | -------------------------------------------------------------------------------- /src/main/java/com/philips/research/spdxbuilder/persistence/blackduck/package-info.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2021, Koninklijke Philips N.V., https://www.philips.com 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | @pl.tlinkowski.annotation.basic.NonNullPackage 7 | package com.philips.research.spdxbuilder.persistence.blackduck; 8 | -------------------------------------------------------------------------------- /src/main/java/com/philips/research/spdxbuilder/persistence/bom_base/package-info.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2021, Koninklijke Philips N.V., https://www.philips.com 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | @pl.tlinkowski.annotation.basic.NonNullPackage 7 | package com.philips.research.spdxbuilder.persistence.bom_base; 8 | -------------------------------------------------------------------------------- /src/main/java/com/philips/research/spdxbuilder/persistence/license_scanner/package-info.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2021, Koninklijke Philips N.V., https://www.philips.com 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | @pl.tlinkowski.annotation.basic.NonNullPackage 7 | package com.philips.research.spdxbuilder.persistence.license_scanner; 8 | -------------------------------------------------------------------------------- /src/test/resources/custom_formats.yml: -------------------------------------------------------------------------------- 1 | formats: 2 | - format: custom 3 | types: 4 | "" : "custom" 5 | namespace: 6 | regex: "^([^/]*)/([^/]*)@([^/]*)" 7 | group: 1 8 | name: 9 | regex: "^([^/]*)/([^/]*)@([^/]*)" 10 | group: 2 11 | version: 12 | regex: "^([^/]*)/([^/]*)@([^/]*)" 13 | group: 3 14 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # These owners will be the default owners for everything in 2 | # the repo. Unless a later match takes precedence, 3 | # they will be requested for review when someone opens a 4 | # pull request. 5 | * @philips-software/license-scanning-framework 6 | 7 | # See CODEOWNERS syntax here: https://help.github.com/articles/about-codeowners/#codeowners-syntax 8 | -------------------------------------------------------------------------------- /src/test/resources/ort_with_issue.yml: -------------------------------------------------------------------------------- 1 | # Test resource to see if it has issues. 2 | # The analyzer result. 3 | analyzer: 4 | result: 5 | # Metadata about all found projects, in this case only the mime-types package defined by the package.json file. 6 | projects: 7 | - id: "NPM::mime-types:2.1.18" 8 | # A field to quickly check if the analyzer result contains any issues. 9 | has_issues: true 10 | -------------------------------------------------------------------------------- /src/main/java/com/philips/research/spdxbuilder/persistence/license_scanner/ResultJson.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2021, Koninklijke Philips N.V., https://www.philips.com 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | package com.philips.research.spdxbuilder.persistence.license_scanner; 7 | 8 | import pl.tlinkowski.annotation.basic.NullOr; 9 | 10 | class ResultJson { 11 | String id; 12 | @NullOr String license; 13 | boolean confirmed; 14 | } 15 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2021, Koninklijke Philips N.V., https://www.philips.com 3 | # SPDX-License-Identifier: MIT 4 | # 5 | 6 | group=com.philips.research.spdxbuilder 7 | 8 | repoBaseUrl=https://artifactory-ehv.ta.philips.com/artifactory 9 | repoUrl=https://artifactory-ehv.ta.philips.com/artifactory/dl-innersource-mvn 10 | 11 | repoKey=dl-innersource-mvn 12 | repoKeySnapshot=dl-innersource-mvn-snapshot 13 | repoKeyRelease=dl-innersource-mvn-release 14 | -------------------------------------------------------------------------------- /src/main/java/com/philips/research/spdxbuilder/core/BomReader.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2021, Koninklijke Philips N.V., https://www.philips.com 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | package com.philips.research.spdxbuilder.core; 7 | 8 | import com.philips.research.spdxbuilder.core.domain.BillOfMaterials; 9 | 10 | /** 11 | * Interface for reading a bill-of-materials 12 | */ 13 | public interface BomReader { 14 | void read(BillOfMaterials bom); 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/philips/research/spdxbuilder/core/domain/LicenseException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2021, Koninklijke Philips N.V., https://www.philips.com 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | package com.philips.research.spdxbuilder.core.domain; 7 | 8 | import com.philips.research.spdxbuilder.core.BusinessException; 9 | 10 | public class LicenseException extends BusinessException { 11 | public LicenseException(String message) { 12 | super(message); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/philips/research/spdxbuilder/persistence/blackduck/BlackDuckComponentDetails.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2021, Koninklijke Philips N.V., https://www.philips.com 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | package com.philips.research.spdxbuilder.persistence.blackduck; 7 | 8 | import java.net.URL; 9 | import java.util.Optional; 10 | 11 | public interface BlackDuckComponentDetails { 12 | Optional getDescription(); 13 | 14 | Optional getHomepage(); 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/philips/research/spdxbuilder/persistence/spdx/SpdxException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2021, Koninklijke Philips N.V., https://www.philips.com 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | package com.philips.research.spdxbuilder.persistence.spdx; 7 | 8 | import com.philips.research.spdxbuilder.core.BusinessException; 9 | 10 | public class SpdxException extends BusinessException { 11 | public SpdxException(String message) { 12 | super(message); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/philips/research/spdxbuilder/persistence/bom_base/BomBaseException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2021, Koninklijke Philips N.V., https://www.philips.com 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | package com.philips.research.spdxbuilder.persistence.bom_base; 7 | 8 | import com.philips.research.spdxbuilder.core.BusinessException; 9 | 10 | public class BomBaseException extends BusinessException { 11 | public BomBaseException(String message) { 12 | super(message); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/philips/research/spdxbuilder/persistence/ort/OrtReaderException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2021, Koninklijke Philips N.V., https://www.philips.com 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | package com.philips.research.spdxbuilder.persistence.ort; 7 | 8 | import com.philips.research.spdxbuilder.core.BusinessException; 9 | 10 | public class OrtReaderException extends BusinessException { 11 | public OrtReaderException(String message) { 12 | super(message); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2021, Koninklijke Philips N.V., https://www.philips.com 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | /* 7 | * This file was generated by the Gradle 'init' task. 8 | * 9 | * The settings file is used to specify which projects to include in your build. 10 | * 11 | * Detailed information about configuring a multi-project build in Gradle can be found 12 | * in the user manual at https://docs.gradle.org/6.7/userguide/multi_project_builds.html 13 | */ 14 | 15 | rootProject.name = 'spdx-builder' 16 | -------------------------------------------------------------------------------- /src/main/java/com/philips/research/spdxbuilder/core/BomProcessor.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2021, Koninklijke Philips N.V., https://www.philips.com 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | package com.philips.research.spdxbuilder.core; 7 | 8 | import com.philips.research.spdxbuilder.core.domain.BillOfMaterials; 9 | 10 | import java.io.Closeable; 11 | 12 | /** 13 | * Interface for persisting a bill-of-materials. 14 | */ 15 | public interface BomProcessor extends Closeable { 16 | void process(BillOfMaterials bom); 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/philips/research/spdxbuilder/core/domain/domain.puml: -------------------------------------------------------------------------------- 1 | @startuml 2 | 3 | class Package { 4 | type:string 5 | namespace:string 6 | name:string 7 | version:string 8 | purl:URI 9 | etc... 10 | } 11 | 12 | class Relation { 13 | type:{static,dynamic,depends_on,descendant_of} 14 | } 15 | Relation -> Package:from 16 | Relation -> Package:to 17 | 18 | class BillOfMaterials { 19 | title:string 20 | etc... 21 | } 22 | BillOfMaterials *--> "*" Package:packages 23 | BillOfMaterials *--> "*" Relation:relations 24 | 25 | @enduml 26 | -------------------------------------------------------------------------------- /src/main/java/com/philips/research/spdxbuilder/persistence/blackduck/BlackDuckException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2021, Koninklijke Philips N.V., https://www.philips.com 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | package com.philips.research.spdxbuilder.persistence.blackduck; 7 | 8 | public class BlackDuckException extends RuntimeException { 9 | public BlackDuckException(String message) { 10 | super(message); 11 | } 12 | 13 | public BlackDuckException(String message, Throwable cause) { 14 | super(message, cause); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/philips/research/spdxbuilder/persistence/spdx/SpdxRef.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2021, Koninklijke Philips N.V., https://www.philips.com 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | package com.philips.research.spdxbuilder.persistence.spdx; 7 | 8 | /** 9 | * Generic SPDX reference. 10 | */ 11 | public class SpdxRef { 12 | private final String ref; 13 | 14 | public SpdxRef(String ref) { 15 | this.ref = ref; 16 | } 17 | 18 | @Override 19 | public String toString() { 20 | return "SPDXRef-" + ref; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/com/philips/research/spdxbuilder/core/BusinessException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2021, Koninklijke Philips N.V., https://www.philips.com 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | package com.philips.research.spdxbuilder.core; 7 | 8 | /** 9 | * Exception thrown when a business rule is violated. 10 | */ 11 | public class BusinessException extends RuntimeException { 12 | public BusinessException(String message) { 13 | super(message); 14 | } 15 | 16 | public BusinessException(String message, Throwable cause) { 17 | super(message, cause); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/philips/research/spdxbuilder/persistence/tree/TreeException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2021, Koninklijke Philips N.V., https://www.philips.com 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | package com.philips.research.spdxbuilder.persistence.tree; 7 | 8 | import com.philips.research.spdxbuilder.core.BusinessException; 9 | 10 | public class TreeException extends BusinessException { 11 | public TreeException(String message) { 12 | super(message); 13 | } 14 | 15 | public TreeException(String message, Throwable cause) { 16 | super(message, cause); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/philips/research/spdxbuilder/persistence/license_scanner/LicenseScannerException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2021, Koninklijke Philips N.V., https://www.philips.com 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | package com.philips.research.spdxbuilder.persistence.license_scanner; 7 | 8 | import com.philips.research.spdxbuilder.core.BusinessException; 9 | 10 | /** 11 | * Exception thrown in case of an error while obtaining license information. 12 | */ 13 | public class LicenseScannerException extends BusinessException { 14 | public LicenseScannerException(String message) { 15 | super(message); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/philips/research/spdxbuilder/persistence/tree/TreeFormatter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2021, Koninklijke Philips N.V., https://www.philips.com 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | package com.philips.research.spdxbuilder.persistence.tree; 7 | 8 | public class TreeFormatter { 9 | private static final String INDENT = " "; 10 | 11 | private int indent = 0; 12 | 13 | public String node(String name) { 14 | return INDENT.repeat(indent) + name; 15 | } 16 | 17 | public void indent() { 18 | indent++; 19 | } 20 | 21 | public void unindent() { 22 | indent--; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/test/java/com/philips/research/spdxbuilder/persistence/spdx/SpdxRefTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2021, Koninklijke Philips N.V., https://www.philips.com 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | package com.philips.research.spdxbuilder.persistence.spdx; 7 | 8 | import org.junit.jupiter.api.Test; 9 | 10 | import static org.assertj.core.api.Assertions.assertThat; 11 | 12 | class SpdxRefTest { 13 | private static final String VALUE = "Value"; 14 | 15 | @Test 16 | void createsInstance() { 17 | final var ref = new SpdxRef(VALUE); 18 | 19 | assertThat(ref.toString()).isEqualTo("SPDXRef-" + VALUE); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "gradle" 9 | directory: "/" 10 | schedule: 11 | interval: "weekly" 12 | 13 | - package-ecosystem: "github-actions" 14 | # Workflow files stored in the 15 | # default location of `.github/workflows` 16 | directory: "/" 17 | schedule: 18 | interval: "weekly" 19 | 20 | -------------------------------------------------------------------------------- /src/main/java/com/philips/research/spdxbuilder/core/domain/licenses.puml: -------------------------------------------------------------------------------- 1 | @startuml 2 | 3 | abstract class License { 4 | {static} of(string):License 5 | with(string):License 6 | and(string):License 7 | or(string):License 8 | } 9 | 10 | class NoLicense 11 | License <|.. NoLicense 12 | class SingleLicense 13 | License <|.. SingleLicense 14 | class ComboLicense 15 | License <|.. ComboLicense 16 | class AndLicense 17 | ComboLicense <|.. AndLicense 18 | class OrLicense 19 | ComboLicense <|.. OrLicense 20 | 21 | class Detection { 22 | score:int 23 | confirmations:int 24 | filePath:file 25 | startLine:int 26 | endLine:int 27 | ignored:boolean 28 | } 29 | Detection -> "1" License:license 30 | 31 | @enduml 32 | -------------------------------------------------------------------------------- /.github/workflows/gradle.yml: -------------------------------------------------------------------------------- 1 | name: Java CI with Gradle 2 | 3 | on: 4 | push: 5 | pull_request: 6 | branches: [ main ] 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Checkout code 14 | uses: actions/checkout@v4 15 | - uses: actions/setup-java@v4 16 | with: 17 | java-version: '11.0.1' 18 | distribution: 'zulu' 19 | - name: Build project 20 | run: ./gradlew build -x test 21 | - name: Grant execute permission for gradlew 22 | run: chmod +x gradlew 23 | - name: Build 24 | run: ./gradlew build -x test 25 | - name: Tests 26 | run: ./gradlew test 27 | - name: Show version 28 | run: ./gradlew -q run --args='--help' 29 | -------------------------------------------------------------------------------- /src/main/java/com/philips/research/spdxbuilder/persistence/blackduck/BlackDuckProduct.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2021, Koninklijke Philips N.V., https://www.philips.com 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | package com.philips.research.spdxbuilder.persistence.blackduck; 7 | 8 | import com.philips.research.spdxbuilder.core.domain.License; 9 | import pl.tlinkowski.annotation.basic.NullOr; 10 | 11 | import java.time.LocalDateTime; 12 | import java.util.Optional; 13 | import java.util.UUID; 14 | 15 | public interface BlackDuckProduct { 16 | UUID getId(); 17 | 18 | String getName(); 19 | 20 | Optional getDescription(); 21 | 22 | Optional getLicense(); 23 | 24 | Optional getCreatedAt(); 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/philips/research/spdxbuilder/core/domain/Party.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2021, Koninklijke Philips N.V., https://www.philips.com 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | package com.philips.research.spdxbuilder.core.domain; 7 | 8 | /** 9 | * Named person, tool or organization. 10 | */ 11 | public class Party { 12 | private final Type type; 13 | private final String name; 14 | 15 | public Party(Type type, String name) { 16 | this.type = type; 17 | this.name = name; 18 | } 19 | 20 | public Type getType() { 21 | return type; 22 | } 23 | 24 | public String getName() { 25 | return name; 26 | } 27 | 28 | public enum Type {NONE, PERSON, ORGANIZATION, TOOL} 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/test/java/com/philips/research/spdxbuilder/persistence/spdx/ExternalReferenceTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2021, Koninklijke Philips N.V., https://www.philips.com 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | package com.philips.research.spdxbuilder.persistence.spdx; 7 | 8 | import com.github.packageurl.PackageURL; 9 | import org.junit.jupiter.api.Test; 10 | 11 | import static org.assertj.core.api.Assertions.assertThat; 12 | 13 | class ExternalReferenceTest { 14 | @Test 15 | void createsPurlFromPackageURL() throws Exception { 16 | final var purl = new PackageURL("pkg:npm/name@version"); 17 | 18 | final var ref = new ExternalReference(purl); 19 | 20 | assertThat(ref.toString()).isEqualTo("PACKAGE-MANAGER purl " + purl); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/com/philips/research/spdxbuilder/persistence/spdx/ExternalReference.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2021, Koninklijke Philips N.V., https://www.philips.com 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | package com.philips.research.spdxbuilder.persistence.spdx; 7 | 8 | import com.github.packageurl.PackageURL; 9 | 10 | public class ExternalReference { 11 | private final String category; 12 | private final String type; 13 | private final Object locator; 14 | 15 | public ExternalReference(PackageURL purl) { 16 | this.category = "PACKAGE-MANAGER"; 17 | this.type = "purl"; 18 | this.locator = purl.canonicalize(); 19 | } 20 | 21 | @Override 22 | public String toString() { 23 | return String.format("%s %s %s", category, type, locator); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/philips/research/spdxbuilder/persistence/spdx/SpdxLicense.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2021, Koninklijke Philips N.V., https://www.philips.com 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | package com.philips.research.spdxbuilder.persistence.spdx; 7 | 8 | class SpdxLicense { 9 | static final String VERSION = "3.8"; 10 | 11 | //TODO Only allow valid identifiers 12 | private final String identifier; 13 | 14 | private SpdxLicense(String identifier) { 15 | this.identifier = identifier; 16 | } 17 | 18 | /** 19 | * @return SPDX license for the provided textual description 20 | */ 21 | static SpdxLicense of(String text) { 22 | // TODO Parse into structure of classes 23 | return new SpdxLicense(text); 24 | } 25 | 26 | @Override 27 | public String toString() { 28 | return identifier; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/philips/research/spdxbuilder/persistence/blackduck/BlackDuckComponent.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2021, Koninklijke Philips N.V., https://www.philips.com 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | package com.philips.research.spdxbuilder.persistence.blackduck; 7 | 8 | import com.github.packageurl.PackageURL; 9 | import com.philips.research.spdxbuilder.core.domain.License; 10 | 11 | import java.util.List; 12 | import java.util.Optional; 13 | import java.util.UUID; 14 | 15 | interface BlackDuckComponent { 16 | String getName(); 17 | 18 | UUID getId(); 19 | 20 | String getVersion(); 21 | 22 | UUID getVersionId(); 23 | 24 | List getPackageUrls(); 25 | 26 | List getUsages(); 27 | 28 | Optional getLicense(); 29 | 30 | long getHierarchicalId(); 31 | 32 | boolean isAdditionalComponent(); 33 | 34 | boolean isSubproject(); 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/com/philips/research/spdxbuilder/persistence/bom_base/PackageMetadata.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2021, Koninklijke Philips N.V., https://www.philips.com 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | package com.philips.research.spdxbuilder.persistence.bom_base; 7 | 8 | import java.net.URI; 9 | import java.net.URL; 10 | import java.util.List; 11 | import java.util.Optional; 12 | 13 | public interface PackageMetadata { 14 | Optional getTitle(); 15 | 16 | Optional getDescription(); 17 | 18 | Optional getHomePage(); 19 | 20 | Optional getAttribution(); 21 | 22 | Optional getSupplier(); 23 | 24 | Optional getOriginator(); 25 | 26 | Optional getDownloadLocation(); 27 | 28 | Optional getSha1(); 29 | 30 | Optional getSha256(); 31 | 32 | Optional getSourceLocation(); 33 | 34 | Optional getDeclaredLicense(); 35 | 36 | List getDetectedLicenses(); 37 | } 38 | -------------------------------------------------------------------------------- /src/test/java/com/philips/research/spdxbuilder/core/domain/RelationTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2021, Koninklijke Philips N.V., https://www.philips.com 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | package com.philips.research.spdxbuilder.core.domain; 7 | 8 | import nl.jqno.equalsverifier.EqualsVerifier; 9 | import org.junit.jupiter.api.Test; 10 | 11 | import static org.assertj.core.api.Assertions.assertThat; 12 | 13 | class RelationTest { 14 | private static final Package FROM = new Package("NS", "Name", "Version"); 15 | private static final Package TO = new Package("NS", "Name", "Version"); 16 | 17 | @Test 18 | void createsInstance() { 19 | final var relation = new Relation(FROM, TO, Relation.Type.DYNAMICALLY_LINKS); 20 | 21 | assertThat(relation.getFrom()).isEqualTo(FROM); 22 | assertThat(relation.getTo()).isEqualTo(TO); 23 | assertThat(relation.getType()).isEqualTo(Relation.Type.DYNAMICALLY_LINKS); 24 | } 25 | 26 | @Test 27 | void implementsEquals() { 28 | EqualsVerifier.forClass(Relation.class).verify(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021 Koninklijke Philips N.V., https://www.philips.com 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the Software), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /.github/workflows/licenses.yml: -------------------------------------------------------------------------------- 1 | name: Get Licenses 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - develop 8 | - use-tree-format 9 | pull_request: 10 | branches: 11 | - main 12 | - develop 13 | 14 | jobs: 15 | scan: 16 | 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | - uses: actions/checkout@v4 21 | - uses: actions/setup-java@v4 22 | with: 23 | java-version: '11.0.13' 24 | distribution: 'zulu' 25 | - name: Download asset 26 | uses: fabriciobastian/download-release-asset-action@v1.0.6 27 | with: 28 | repository: philips-software/spdx-builder 29 | file: spdx-builder.jar 30 | token: ${{ secrets.GITHUB_TOKEN }} 31 | - name: Create dependency list 32 | run: | 33 | ./gradlew -q dependencies --configuration runtimeClasspath > dependencies.txt 34 | - name: Create SPDX file 35 | run: | 36 | cat dependencies.txt | java -jar spdx-builder.jar tree -f gradle -c .spdx-builder.yml -o spdx-builder.spdx 37 | - uses: actions/upload-artifact@v4 38 | with: 39 | name: licenses 40 | path: | 41 | spdx-builder.spdx 42 | -------------------------------------------------------------------------------- /src/main/java/com/philips/research/spdxbuilder/layers.puml: -------------------------------------------------------------------------------- 1 | @startuml 2 | 3 | title 4 | Application layering 5 | end title 6 | 7 | left to right direction 8 | 9 | package controller { 10 | class FooCommand 11 | note right 12 | Command line invocation 13 | implementation 14 | end note 15 | } 16 | 17 | package core { 18 | interface ConversionService 19 | FooCommand -l-> ConversionService 20 | 21 | package domain { 22 | class ConversionInteractor 23 | note bottom 24 | Use Case implementations 25 | end note 26 | ConversionService <|.. ConversionInteractor 27 | 28 | class DomainClass 29 | ConversionInteractor ..> DomainClass 30 | } 31 | 32 | interface BomReader 33 | ConversionInteractor -> BomReader 34 | 35 | interface BomWriter 36 | ConversionInteractor -> BomWriter 37 | 38 | interface KnowledgeBase 39 | ConversionInteractor -> KnowledgeBase 40 | 41 | BomReader -[hidden]- BomWriter 42 | BomWriter -[hidden]- KnowledgeBase 43 | } 44 | 45 | package persistence { 46 | class XyzReader 47 | BomReader <|-- XyzReader 48 | 49 | class XyzWriter 50 | BomWriter <|-- XyzWriter 51 | 52 | class XyzKnowledgeBase 53 | KnowledgeBase <|-- XyzKnowledgeBase 54 | } 55 | 56 | @enduml 57 | -------------------------------------------------------------------------------- /src/main/java/com/philips/research/spdxbuilder/core/KnowledgeBase.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2021, Koninklijke Philips N.V., https://www.philips.com 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | package com.philips.research.spdxbuilder.core; 7 | 8 | import com.philips.research.spdxbuilder.core.domain.BillOfMaterials; 9 | import com.philips.research.spdxbuilder.core.domain.Package; 10 | 11 | import java.util.concurrent.atomic.AtomicBoolean; 12 | 13 | public abstract class KnowledgeBase { 14 | /** 15 | * Enhances all packages of a bill-of-materials. 16 | * 17 | * @param bom bill-of-materials 18 | * @return true if no packages failed 19 | */ 20 | public boolean enhance(BillOfMaterials bom) { 21 | final var success = new AtomicBoolean(true); 22 | bom.getPackages().stream() 23 | .filter(pkg -> !pkg.isInternal()) 24 | .forEach(pkg -> { 25 | final var found = enhance(pkg); 26 | if (!found) { 27 | System.err.println("WARNING: No metadata for " + pkg); 28 | success.set(false); 29 | } 30 | }); 31 | return success.get(); 32 | } 33 | 34 | /** 35 | * Enhances a single package. 36 | * 37 | * @param pkg the package to enhance 38 | * @return true if for success, or false if enhancement failed 39 | */ 40 | public abstract boolean enhance(Package pkg); 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/com/philips/research/spdxbuilder/persistence/blackduck/UriHelper.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2021, Koninklijke Philips N.V., https://www.philips.com 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | package com.philips.research.spdxbuilder.persistence.blackduck; 7 | 8 | import pl.tlinkowski.annotation.basic.NullOr; 9 | 10 | import java.net.URI; 11 | import java.util.UUID; 12 | 13 | abstract class UriHelper { 14 | static UUID uuidFromUri(URI uri, int fromEnd) { 15 | final var part = getPart(uri, fromEnd); 16 | return UUID.fromString(part); 17 | } 18 | 19 | static long longFromUri(URI uri, int fromEnd) { 20 | final var part = getPart(uri, fromEnd); 21 | return toLong(part); 22 | } 23 | 24 | private static long toLong(String part) { 25 | try { 26 | return Long.parseLong(part); 27 | } catch (Exception e) { 28 | throw new IllegalArgumentException("Invalid integer value: " + part); 29 | } 30 | } 31 | 32 | private static String getPart(@NullOr URI uri, int fromEnd) { 33 | if (uri == null) { 34 | throw new NullPointerException("No URI provided"); 35 | } 36 | final var parts = uri.getPath().split("/"); 37 | if (fromEnd >= parts.length) { 38 | throw new IllegalArgumentException("Expected path of '" + uri + "' to have at least " + (fromEnd + 1) + " parts"); 39 | } 40 | return parts[parts.length - fromEnd - 1]; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/test/java/com/philips/research/spdxbuilder/persistence/tree/TreeFormatterTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2021, Koninklijke Philips N.V., https://www.philips.com 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | package com.philips.research.spdxbuilder.persistence.tree; 7 | 8 | import org.junit.jupiter.api.Test; 9 | 10 | import static org.assertj.core.api.Assertions.assertThat; 11 | 12 | class TreeFormatterTest { 13 | private static final String ONE = "One"; 14 | private static final String TWO = "Two"; 15 | private static final String THREE = "Three"; 16 | private static final String INDENT = " "; 17 | 18 | private final TreeFormatter formatter = new TreeFormatter(); 19 | 20 | @Test 21 | void listsTopLevel() { 22 | assertThat(formatter.node(ONE)).isEqualTo(ONE); 23 | assertThat(formatter.node(TWO)).isEqualTo(TWO); 24 | assertThat(formatter.node(THREE)).isEqualTo(THREE); 25 | } 26 | 27 | @Test 28 | void indentsChildren() { 29 | assertThat(formatter.node(ONE)).isEqualTo(ONE); 30 | formatter.indent(); 31 | assertThat(formatter.node(TWO)).isEqualTo(INDENT + TWO); 32 | assertThat(formatter.node(THREE)).isEqualTo(INDENT + THREE); 33 | } 34 | 35 | @Test 36 | void unindentsBackToParent() { 37 | assertThat(formatter.node(ONE)).isEqualTo(ONE); 38 | formatter.indent(); 39 | assertThat(formatter.node(TWO)).isEqualTo(INDENT + TWO); 40 | formatter.unindent(); 41 | assertThat(formatter.node(THREE)).isEqualTo(THREE); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/com/philips/research/spdxbuilder/persistence/blackduck/blackduck.puml: -------------------------------------------------------------------------------- 1 | @startuml 2 | 3 | title Black Duck BOM representation 4 | 5 | class License { 6 | UUID 7 | licenseType:{CONJUNCTIVE,DISJUNCTIVE} 8 | licenseDisplay:String 9 | spdxId:String 10 | } 11 | License --> "*" License: licenses 12 | note bottom of License 13 | Nested grouping 14 | of AND/OR terms. 15 | end note 16 | 17 | class Origin { 18 | UUID 19 | externalNamespace:String 20 | externalId:String 21 | } 22 | note left of Origin 23 | Exact format of 24 | externalId 25 | depends on the 26 | externalNamespace 27 | end note 28 | 29 | class ComponentVersion { 30 | UUID 31 | componentName:String 32 | componentVersionName:String 33 | usages:{see note}[] 34 | } 35 | ComponentVersion o--> "*" License: licenses\n 36 | ComponentVersion o-d-> "*" Origin: origins 37 | ComponentVersion --> "*" ComponentVersion: children 38 | note right of ComponentVersion 39 | Possible usages values: SOURCE_CODE, 40 | STATICALLY_LINKED, DYNAMICALLY_LINKED, 41 | SEPARATE_WORK, MERELY_AGGREGATED, 42 | IMPLEMENTATION_OF_STANDARD, PREREQUISITE, 43 | DEV_TOOL_EXCLUDED, UNSPECIFIED 44 | end note 45 | 46 | class Component { 47 | UUID 48 | description:String 49 | homepage:URI 50 | } 51 | ComponentVersion -u-> "1" Component: component 52 | 53 | class ProjectVersion{ 54 | UUID 55 | versionName:String 56 | } 57 | ProjectVersion *-> "*" ComponentVersion: BOM 58 | 59 | class Project { 60 | UUID 61 | name:String 62 | } 63 | Project *-> "*" ProjectVersion 64 | 65 | @enduml 66 | -------------------------------------------------------------------------------- /src/main/java/com/philips/research/spdxbuilder/SpdxBuilder.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2021, Koninklijke Philips N.V., https://www.philips.com 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | package com.philips.research.spdxbuilder; 7 | 8 | import com.philips.research.spdxbuilder.controller.BlackDuckCommand; 9 | import com.philips.research.spdxbuilder.controller.OrtCommand; 10 | import com.philips.research.spdxbuilder.controller.TreeCommand; 11 | import com.philips.research.spdxbuilder.core.BusinessException; 12 | import picocli.CommandLine; 13 | 14 | public class SpdxBuilder { 15 | public static void main(String... args) { 16 | new CommandLine(new Runner()) 17 | .setExecutionExceptionHandler(SpdxBuilder::exceptionHandler) 18 | .execute(args); 19 | } 20 | 21 | private static int exceptionHandler(Exception e, CommandLine cmd, CommandLine.ParseResult parseResult) { 22 | if (e instanceof BusinessException) { 23 | printError(cmd, "Conversion aborted: " + e.getMessage()); 24 | return 1; 25 | } 26 | 27 | printError(cmd, "An internal error occurred: " + e); 28 | e.printStackTrace(); 29 | return 1; 30 | } 31 | 32 | private static void printError(CommandLine cmd, String message) { 33 | cmd.getErr().println(cmd.getColorScheme().errorText(message)); 34 | } 35 | 36 | @CommandLine.Command(subcommands = {OrtCommand.class, TreeCommand.class, BlackDuckCommand.class}, 37 | description = "Builds SPDX bill-of-materials files from various sources") 38 | static class Runner { 39 | } 40 | } 41 | 42 | -------------------------------------------------------------------------------- /src/main/java/com/philips/research/spdxbuilder/core/domain/Relation.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2021, Koninklijke Philips N.V., https://www.philips.com 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | package com.philips.research.spdxbuilder.core.domain; 7 | 8 | import pl.tlinkowski.annotation.basic.NullOr; 9 | 10 | import java.util.Objects; 11 | 12 | public final class Relation { 13 | private final Package from; 14 | private final Package to; 15 | private final Type type; 16 | 17 | public Relation(Package from, Package to, Type type) { 18 | this.from = from; 19 | this.to = to; 20 | this.type = type; 21 | } 22 | 23 | public Package getFrom() { 24 | return from; 25 | } 26 | 27 | public Package getTo() { 28 | return to; 29 | } 30 | 31 | public Type getType() { 32 | return type; 33 | } 34 | 35 | @Override 36 | public boolean equals(@NullOr Object o) { 37 | if (this == o) return true; 38 | if (o == null || getClass() != o.getClass()) return false; 39 | Relation relation = (Relation) o; 40 | return Objects.equals(from, relation.from) && 41 | Objects.equals(to, relation.to) && 42 | type == relation.type; 43 | } 44 | 45 | @Override 46 | public int hashCode() { 47 | return Objects.hash(from, to, type); 48 | } 49 | 50 | @Override 51 | public String toString() { 52 | return String.format("%s -> %s: %s", from, to, type); 53 | } 54 | 55 | // Sorted from strongest to weakest binding 56 | public enum Type {DESCENDANT_OF, STATICALLY_LINKS, DYNAMICALLY_LINKS, DEPENDS_ON, CONTAINS, DEVELOPED_USING} 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/com/philips/research/spdxbuilder/persistence/license_scanner/LicenseScannerApi.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2021, Koninklijke Philips N.V., https://www.philips.com 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | package com.philips.research.spdxbuilder.persistence.license_scanner; 7 | 8 | import com.fasterxml.jackson.annotation.JsonInclude; 9 | import pl.tlinkowski.annotation.basic.NullOr; 10 | import retrofit2.Call; 11 | import retrofit2.http.Body; 12 | import retrofit2.http.POST; 13 | import retrofit2.http.Path; 14 | 15 | import java.net.URI; 16 | 17 | /** 18 | * Retrofit REST API declaration. 19 | */ 20 | interface LicenseScannerApi { 21 | /** 22 | * Start scanning a package or retrieve result from an earlier scan. 23 | * 24 | * @return scan result with or without a concluded license 25 | */ 26 | @POST("/packages") 27 | Call scan(@Body RequestJson body); 28 | 29 | /** 30 | * Contests an existing scan. 31 | * 32 | * @param scanId UUID of the scan. 33 | */ 34 | @POST("/scans/{scanId}/contest") 35 | Call contest(@Path("scanId") String scanId, @Body ContestJson body); 36 | 37 | class RequestJson { 38 | String purl; 39 | @JsonInclude(JsonInclude.Include.NON_NULL) 40 | @NullOr String location; 41 | 42 | public RequestJson(String purl, @NullOr URI location) { 43 | this.purl = purl; 44 | if (location != null) { 45 | this.location = location.toASCIIString(); 46 | } 47 | } 48 | } 49 | 50 | class ContestJson { 51 | String license; 52 | 53 | public ContestJson(String license) { 54 | this.license = license; 55 | } 56 | } 57 | } 58 | 59 | -------------------------------------------------------------------------------- /src/test/java/com/philips/research/spdxbuilder/persistence/ort/OrtReaderTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2021, Koninklijke Philips N.V., https://www.philips.com 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | package com.philips.research.spdxbuilder.persistence.ort; 7 | 8 | import com.philips.research.spdxbuilder.core.domain.BillOfMaterials; 9 | import org.junit.jupiter.api.Test; 10 | 11 | import java.io.File; 12 | import java.net.URI; 13 | import java.nio.file.Path; 14 | import java.util.List; 15 | 16 | import static org.assertj.core.api.Assertions.assertThat; 17 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 18 | 19 | 20 | class OrtReaderTest { 21 | 22 | private static final Path SAMPLES_DIR = Path.of("src", "test", "resources"); 23 | private static final File ORT_SAMPLE = SAMPLES_DIR.resolve("ort_sample.yml").toFile(); 24 | private static final File ORT_SAMPLE_WITH_ISSUE = SAMPLES_DIR.resolve("ort_with_issue.yml").toFile(); 25 | 26 | private final BillOfMaterials bom = new BillOfMaterials(); 27 | 28 | void createBOM(File file) { 29 | OrtReader ortSample = new OrtReader(file); 30 | ortSample.defineProjectPackage("NPM::mime-types:2.1.18", URI.create("pkg:npm/mime-types@2.1.18")) 31 | .excludeScopes("NPM::mime-types:2.1.18", List.of("test*")) 32 | .read(bom); 33 | } 34 | 35 | @Test 36 | void loadsOrtSample() { 37 | createBOM(ORT_SAMPLE); 38 | assertThat(bom.getPackages()).hasSize(1 + 2); 39 | } 40 | 41 | @Test() 42 | void abortsOnAnalyzerIssues() { 43 | assertThatThrownBy(() -> createBOM(ORT_SAMPLE_WITH_ISSUE)) 44 | .isInstanceOf(OrtReaderException.class) 45 | .hasMessageContaining("The analyzed ORT file has issues"); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /docs/usage_with_black_duck.md: -------------------------------------------------------------------------------- 1 | # Building a Bill-of-Materials from a Black Duck project version 2 | 3 | _NOTE: This function requires access to the "Hierarchical BOM" REST API of 4 | a [Synoptic Black Duck SCA server](https://www.synopsys.com/software-integrity/security-testing/software-composition-analysis.html) 5 | . (See below for instructions on enabling this option on a self-managed 6 | server.)_ 7 | 8 | ## Usage 9 | SPDX-Builder can extract a bill-of-materials from a Black Duck server for a 10 | specified project version. 11 | 12 | A project version in Black Duck is exported to an SPDX file by: 13 | 14 | ```shell 15 | spdx-builder blackduck -o --url --token 16 | ``` 17 | 18 | _Note: If no "output_file" is specified, the output is written to a file named 19 | `bom.spdx` in the current directory. If the file has no extension, `.spdx` 20 | is automatically appended._ 21 | 22 | _Note: The "project" and "version" can be limited to the unique prefixes for a 23 | project version._ 24 | 25 | _Note: The server URL and access token default to values found in 26 | the `BLACKDUCK_URL` and `BLACKDUCK_API_TOKEN` environment variables._ 27 | 28 | ## Enabling the "Hierarchical BOM API" on the server 29 | 30 | To enable the Hierarchical BOM in the Black Duck server in case of a Docker 31 | Swarm installation: 32 | 33 | - Add the `HUB_HIERARCHICAL_BOM` environment variable to an `.env` file. Set the 34 | value to `true`. 35 | 36 | or alternatively: 37 | 38 | - Edit the webapp service in the `docker-compose.local-overrides.yml` 39 | file located in the docker-swarm directory: `webapp:environment: 40 | {HUB_HIERARCHICAL_BOM: "true"}`. 41 | 42 | In case of a Kubernetes or OpenShift installation: 43 | 44 | - Add the following to your environs 45 | flag: `--environs HUB_HIERARCHICAL_BOM:true` 46 | 47 | -------------------------------------------------------------------------------- /src/main/java/com/philips/research/spdxbuilder/persistence/bom_base/BomBaseKnowledgeBase.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2021, Koninklijke Philips N.V., https://www.philips.com 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | package com.philips.research.spdxbuilder.persistence.bom_base; 7 | 8 | import com.philips.research.spdxbuilder.core.KnowledgeBase; 9 | import com.philips.research.spdxbuilder.core.domain.LicenseParser; 10 | import com.philips.research.spdxbuilder.core.domain.Package; 11 | 12 | import java.net.URI; 13 | 14 | public class BomBaseKnowledgeBase extends KnowledgeBase { 15 | private final BomBaseClient client; 16 | 17 | public BomBaseKnowledgeBase(URI serverUri) { 18 | this(new BomBaseClient(serverUri)); 19 | } 20 | 21 | public BomBaseKnowledgeBase(BomBaseClient client) { 22 | this.client = client; 23 | } 24 | 25 | @Override 26 | public boolean enhance(Package pkg) { 27 | return pkg.getPurl().flatMap(client::readPackage) 28 | .map(meta -> { 29 | meta.getTitle().ifPresent(pkg::setSummary); 30 | meta.getDescription().ifPresent(pkg::setDescription); 31 | meta.getHomePage().ifPresent(pkg::setHomePage); 32 | meta.getSourceLocation().ifPresent(pkg::setSourceLocation); 33 | meta.getDownloadLocation().ifPresent(pkg::setDownloadLocation); 34 | meta.getSha1().ifPresent(hash -> pkg.addHash("SHA1", hash)); 35 | meta.getSha256().ifPresent(hash -> pkg.addHash("SHA256", hash)); 36 | meta.getDeclaredLicense().map(LicenseParser::parse).ifPresent(pkg::setDeclaredLicense); 37 | meta.getDetectedLicenses().stream().map(LicenseParser::parse).forEach(pkg::addDetectedLicense); 38 | return meta; 39 | }).isPresent(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/test/java/com/philips/research/spdxbuilder/core/domain/BillOfMaterialsTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2021, Koninklijke Philips N.V., https://www.philips.com 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | package com.philips.research.spdxbuilder.core.domain; 7 | 8 | import org.junit.jupiter.api.Test; 9 | 10 | import static org.assertj.core.api.Assertions.assertThat; 11 | 12 | class BillOfMaterialsTest { 13 | private static final String TYPE = "Type"; 14 | private static final String NAMESPACE = "Namespace"; 15 | private static final String NAME = "Name"; 16 | private static final String VERSION = "Version"; 17 | 18 | final BillOfMaterials bom = new BillOfMaterials(); 19 | final Package pkg = new Package(NAMESPACE, NAME, VERSION); 20 | final Package other = new Package(NAMESPACE, "Other", VERSION); 21 | 22 | @Test 23 | void createsInstance() { 24 | assertThat(bom.getTitle()).isEmpty(); 25 | assertThat(bom.getComment()).isEmpty(); 26 | assertThat(bom.getOrganization()).isEmpty(); 27 | assertThat(bom.getIdentifier()).isEmpty(); 28 | assertThat(bom.getNamespace()).isEmpty(); 29 | } 30 | 31 | @Test 32 | void blankDocumentReferenceIsIgnored() { 33 | bom.setIdentifier(" "); 34 | 35 | assertThat(bom.getIdentifier()).isEmpty(); 36 | } 37 | 38 | @Test 39 | void addsUniqueRelationOnlyOnce() { 40 | bom.addRelation(pkg, other, Relation.Type.DEPENDS_ON); 41 | bom.addRelation(pkg, other, Relation.Type.DEPENDS_ON); 42 | 43 | assertThat(bom.getRelations()).hasSize(1); 44 | assertThat(bom.getRelations()).containsExactly(new Relation(pkg, other, Relation.Type.DEPENDS_ON)); 45 | } 46 | 47 | @Test 48 | void defaultsTitleToFirstProject() { 49 | bom.addPackage(pkg); 50 | 51 | assertThat(bom.getTitle()).isEqualTo(NAME); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/test/java/com/philips/research/spdxbuilder/persistence/spdx/SpdxPartyTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2021, Koninklijke Philips N.V., https://www.philips.com 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | package com.philips.research.spdxbuilder.persistence.spdx; 7 | 8 | import com.philips.research.spdxbuilder.core.domain.Party; 9 | import nl.jqno.equalsverifier.EqualsVerifier; 10 | import org.junit.jupiter.api.Test; 11 | 12 | import static org.assertj.core.api.Assertions.assertThat; 13 | 14 | class SpdxPartyTest { 15 | static private final String NAME = "Name"; 16 | static private final String VERSION = "Version"; 17 | static private final String EMAIL = "e@mail.com"; 18 | 19 | @Test 20 | void createsTool() { 21 | assertThat(SpdxParty.tool(NAME, VERSION).toString()).isEqualTo("Tool: " + NAME + '-' + VERSION); 22 | assertThat(SpdxParty.tool(NAME, null).toString()).isEqualTo("Tool: " + NAME); 23 | } 24 | 25 | @Test 26 | void createsOrganization() { 27 | assertThat(SpdxParty.organization(NAME).toString()).isEqualTo("Organization: " + NAME); 28 | } 29 | 30 | @Test 31 | void createsPerson() { 32 | assertThat(SpdxParty.person(NAME, EMAIL).toString()).isEqualTo("Person: " + NAME + " (" + EMAIL + ")"); 33 | } 34 | 35 | @Test 36 | void createsFromParty() { 37 | assertThat(SpdxParty.from(new Party(Party.Type.PERSON, NAME))).isEqualTo(SpdxParty.person(NAME, null)); 38 | assertThat(SpdxParty.from(new Party(Party.Type.ORGANIZATION, NAME))).isEqualTo(SpdxParty.organization(NAME)); 39 | assertThat(SpdxParty.from(new Party(Party.Type.TOOL, NAME))).isEqualTo(SpdxParty.tool(NAME, null)); 40 | assertThat(SpdxParty.from(new Party(Party.Type.NONE, NAME))).isNull(); 41 | } 42 | 43 | @Test 44 | void implementsEquals() { 45 | EqualsVerifier.forClass(SpdxParty.class).verify(); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to SPDX-builder 2 | 3 | - [Question or Problem?](#question) 4 | - [Issues and Bugs](#issue) 5 | - [Releasing](#release) 6 | 7 | ## Got a Question or Problem? 8 | Please raise an issue for now. We don't have other official communication channels in place right now. If you really want to know more, you can contact the contributors through the standard Philips communication channel. 9 | 10 | ## Found an Issue? 11 | If you find a bug in the source code or a mistake in the documentation, you can help us by submitting an issue to our [Github Repository][github]. Even better you can submit a Pull Request with a fix. 12 | 13 | ## Create a release? 14 | We're using gitflow to release. In versioning we're using a prefix `v` for example: `v0.2.1`. 15 | 16 | ### Initial setup 17 | You need setup git flow once on your local machine: 18 | 19 | ``` 20 | git flow init 21 | ``` 22 | 23 | ### Make sure your local environment is correct 24 | ``` 25 | git checkout main 26 | git pull origin main 27 | git checkout develop 28 | git pull origin develop 29 | ``` 30 | 31 | ### Start a release 32 | ``` 33 | git flow release start vx.x.x 34 | ``` 35 | 36 | ### Change documentation if wanted 37 | 38 | Change documentation if wanted. Versions will be changed right after the release on `develop` 39 | 40 | 41 | Commit changes: 42 | ``` 43 | git commit -m "Prepare for release vx.x.x" 44 | ``` 45 | 46 | ### Finish a release 47 | ``` 48 | git flow release finish vx.x.x 49 | git push origin develop 50 | git checkout main 51 | git push origin main --tags 52 | ``` 53 | 54 | ### Change versions on various places after a release 55 | This needs to be improved in the future, but for now: 56 | 57 | Change version into new version in file / linenumber: 58 | - `build.gradle` : line 68. 59 | - `build.gradle` : line 81. 60 | 61 | Commit these changes to `develop`: 62 | ``` 63 | git commit -m "Prepare for next release" 64 | ``` 65 | 66 | [github]: https://github.com/philips-software/license-scanner/issues 67 | 68 | -------------------------------------------------------------------------------- /src/main/java/com/philips/research/spdxbuilder/controller/BlackDuckCommand.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2021, Koninklijke Philips N.V., https://www.philips.com 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | package com.philips.research.spdxbuilder.controller; 7 | 8 | import com.philips.research.spdxbuilder.core.BomProcessor; 9 | import com.philips.research.spdxbuilder.core.BomReader; 10 | import com.philips.research.spdxbuilder.core.ConversionService; 11 | import com.philips.research.spdxbuilder.core.domain.ConversionInteractor; 12 | import com.philips.research.spdxbuilder.persistence.blackduck.BlackDuckReader; 13 | import com.philips.research.spdxbuilder.persistence.spdx.SpdxWriter; 14 | import picocli.CommandLine.Command; 15 | import picocli.CommandLine.Option; 16 | import picocli.CommandLine.Parameters; 17 | 18 | import java.net.URL; 19 | 20 | /** 21 | * CLI command to export the SBOM from Black Duck to an SPDX file. 22 | */ 23 | @Command(name = "blackduck", aliases = {"bd"}, description = "Extracts a bill-of-materials from a project version in Synoptic Black Duck.") 24 | public class BlackDuckCommand extends AbstractCommand { 25 | @Parameters(index = "0", description = "Project name") 26 | String project; 27 | 28 | @Parameters(index = "1", description = "Version") 29 | String version; 30 | 31 | @Option(names = {"--url"}, description = "Black Duck server URL (defaults to BLACKDUCK_URL environment variable)", 32 | defaultValue = "${env:BLACKDUCK_URL}", required = true) 33 | URL url; 34 | 35 | @Option(names = {"--insecure"}, description = "Disable SSL certificate checking") 36 | boolean insecure = false; 37 | 38 | @Option(names = {"--token"}, description = "Black Duck authorization token (defaults to BLACKDUCK_API_TOKEN environment variable)", 39 | defaultValue = "${env:BLACKDUCK_API_TOKEN}", required = true) 40 | String token; 41 | 42 | @Override 43 | protected ConversionService createService() { 44 | final BomReader reader = new BlackDuckReader(url, token, project, version, insecure); 45 | final BomProcessor writer = new SpdxWriter(spdxStream); 46 | 47 | return new ConversionInteractor(reader, writer); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/com/philips/research/spdxbuilder/core/ConversionService.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2021, Koninklijke Philips N.V., https://www.philips.com 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | package com.philips.research.spdxbuilder.core; 7 | 8 | import com.github.packageurl.PackageURL; 9 | 10 | import java.net.URI; 11 | 12 | /** 13 | * Conversion use cases. 14 | */ 15 | public interface ConversionService { 16 | /** 17 | * Configure general document properties. 18 | * 19 | * @param title 20 | * @param organization 21 | */ 22 | void setDocument(String title, String organization); 23 | 24 | /** 25 | * Configure document comment. 26 | * 27 | * @param comment 28 | */ 29 | void setComment(String comment); 30 | 31 | /** 32 | * Configure SPDX reference. 33 | * 34 | * @param spdxId Document identifier 35 | */ 36 | void setDocReference(String spdxId); 37 | 38 | /** 39 | * Configure document namespace. 40 | * 41 | * @param namespace Namespace URL 42 | */ 43 | void setDocNamespace(URI namespace); 44 | 45 | /** 46 | * Set alternative license for a package. 47 | * 48 | * @param purl identification of the package 49 | * @param license curated license 50 | */ 51 | void curatePackageLicense(PackageURL purl, String license); 52 | 53 | /** 54 | * Set alternative source for a package. 55 | * 56 | * @param purl identification of the package 57 | * @param source source location 58 | */ 59 | void curatePackageSource(PackageURL purl, URI source); 60 | 61 | /** 62 | * Reads the bill-of-materials from the configured source. 63 | */ 64 | void read(); 65 | 66 | /** 67 | * Applies the processor to the bill-of-materials. 68 | * 69 | * @param processor 70 | */ 71 | void apply(BomProcessor processor); 72 | 73 | /** 74 | * Extends the bill-of-matrials with metadata from the knowledge base (if configured), 75 | * and writes it as a document. 76 | * 77 | * @param continueWhenIncomplete writes the SBOM even if the conversion is incomplete 78 | */ 79 | void convert(boolean continueWhenIncomplete); 80 | } 81 | -------------------------------------------------------------------------------- /src/test/java/com/philips/research/spdxbuilder/controller/TreeConfigurationTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2021, Koninklijke Philips N.V., https://www.philips.com 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | package com.philips.research.spdxbuilder.controller; 7 | 8 | import com.philips.research.spdxbuilder.core.ConversionService; 9 | import org.junit.jupiter.api.Test; 10 | 11 | import java.io.ByteArrayInputStream; 12 | import java.net.URI; 13 | 14 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 15 | import static org.mockito.Mockito.mock; 16 | import static org.mockito.Mockito.verify; 17 | 18 | class TreeConfigurationTest { 19 | private static final String TITLE = "Title"; 20 | private static final String ORGANIZATION = "Organization"; 21 | private static final String COMMENT = "Comment"; 22 | private static final String KEY = "Key"; 23 | private static final URI NAMESPACE = URI.create("https://example.com/namespace"); 24 | 25 | private final ConversionService service = mock(ConversionService.class); 26 | 27 | @Test 28 | void parsesConfiguration() { 29 | final var config = read("document:", 30 | " Title: " + TITLE, 31 | " Organization: " + ORGANIZATION, 32 | " comment: " + COMMENT, 33 | " key: " + KEY, 34 | " NAMESPACE: " + NAMESPACE); 35 | 36 | config.apply(service); 37 | 38 | verify(service).setDocument(TITLE, ORGANIZATION); 39 | verify(service).setComment(COMMENT); 40 | verify(service).setDocReference(KEY); 41 | verify(service).setDocNamespace(NAMESPACE); 42 | } 43 | 44 | @Test 45 | void parsesEmptyConfigurationWithoutException() { 46 | read("document:").apply(service); 47 | 48 | verify(service).setDocument("", ""); 49 | } 50 | 51 | @Test 52 | void throws_malformedConfiguration() { 53 | assertThatThrownBy(() -> read("NoValidSection")) 54 | .isInstanceOf(IllegalArgumentException.class) 55 | .hasMessageContaining("format error"); 56 | } 57 | 58 | private TreeConfiguration read(String... lines) { 59 | final var file = String.join("\n", lines); 60 | return TreeConfiguration.parse(new ByteArrayInputStream(file.getBytes())); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/test/java/com/philips/research/spdxbuilder/persistence/blackduck/UriHelperTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2021, Koninklijke Philips N.V., https://www.philips.com 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | package com.philips.research.spdxbuilder.persistence.blackduck; 7 | 8 | import org.junit.jupiter.api.Test; 9 | 10 | import java.net.URI; 11 | import java.util.UUID; 12 | 13 | import static org.assertj.core.api.Assertions.assertThat; 14 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 15 | 16 | class UriHelperTest { 17 | private static final UUID UUID = java.util.UUID.randomUUID(); 18 | private static final long LONG = 123456789; 19 | 20 | @Test 21 | void extractsUuidFromURI() { 22 | final var uri = URI.create("blah/" + UUID + "/something/else"); 23 | 24 | assertThat(UriHelper.uuidFromUri(uri, 2)).isEqualTo(UUID); 25 | } 26 | 27 | @Test 28 | void throws_notValidUuid() { 29 | final var uri = URI.create("notValid"); 30 | 31 | assertThatThrownBy(() -> UriHelper.uuidFromUri(uri, 0)) 32 | .isInstanceOf(IllegalArgumentException.class) 33 | .hasMessageContaining("Invalid UUID string"); 34 | } 35 | 36 | @Test 37 | void extractsLongFromURI() { 38 | final var uri = URI.create("blah/" + LONG + "/something/else"); 39 | 40 | assertThat(UriHelper.longFromUri(uri, 2)).isEqualTo(LONG); 41 | } 42 | 43 | @Test 44 | void throws_notValidLongValue() { 45 | final var uri = URI.create("notValid"); 46 | 47 | assertThatThrownBy(() -> UriHelper.longFromUri(uri, 0)) 48 | .isInstanceOf(IllegalArgumentException.class) 49 | .hasMessageContaining("Invalid integer value"); 50 | } 51 | 52 | @Test 53 | void throws_insufficientPathSize() { 54 | final var uri = URI.create("short"); 55 | 56 | assertThatThrownBy(() -> UriHelper.uuidFromUri(uri, 1)) 57 | .isInstanceOf(IllegalArgumentException.class) 58 | .hasMessageContaining("to have at least 2 parts"); 59 | } 60 | 61 | @Test 62 | void throws_nullUri() { 63 | //noinspection ConstantConditions 64 | assertThatThrownBy(() -> UriHelper.uuidFromUri(null, 0)) 65 | .isInstanceOf(NullPointerException.class) 66 | .hasMessageContaining("No URI provided"); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/main/java/com/philips/research/spdxbuilder/persistence/tree/TreeReader.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2021, Koninklijke Philips N.V., https://www.philips.com 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | package com.philips.research.spdxbuilder.persistence.tree; 7 | 8 | import com.philips.research.spdxbuilder.core.BomReader; 9 | import com.philips.research.spdxbuilder.core.domain.BillOfMaterials; 10 | import com.philips.research.spdxbuilder.core.domain.PurlGlob; 11 | import pl.tlinkowski.annotation.basic.NullOr; 12 | 13 | import java.io.*; 14 | import java.util.List; 15 | 16 | public class TreeReader implements BomReader { 17 | private final TreeFormats formats; 18 | private final String format; 19 | private final InputStream stream; 20 | private final List internalGlobs; 21 | private boolean isRelease; 22 | 23 | public TreeReader(InputStream stream, String format, @NullOr File extension, List internalGlobs) { 24 | this.internalGlobs = internalGlobs; 25 | formats = new TreeFormats(); 26 | if (extension != null) { 27 | formats.extend(extension); 28 | } 29 | this.format = format; 30 | this.stream = stream; 31 | } 32 | 33 | public TreeReader setRelease(boolean enable) { 34 | this.isRelease = enable; 35 | return this; 36 | } 37 | 38 | @Override 39 | public void read(BillOfMaterials bom) { 40 | try (final var reader = new BufferedReader(new InputStreamReader(stream))) { 41 | final var parser = new TreeParser(bom); 42 | if (isRelease) { 43 | parser.withRelease(); 44 | } 45 | internalGlobs.forEach(pattern -> parser.withInternal(new PurlGlob(pattern))); 46 | formats.configure(parser, format); 47 | 48 | @NullOr String line = reader.readLine(); 49 | while (line != null) { 50 | parse(parser, line); 51 | line = reader.readLine(); 52 | } 53 | } catch (IOException e) { 54 | throw new TreeException("Failed to read the tree data"); 55 | } 56 | } 57 | 58 | private void parse(TreeParser parser, String line) { 59 | try { 60 | parser.parse(line) 61 | .ifPresent(format -> formats.configure(parser.clearFormat(), format)); 62 | } catch (TreeException e) { 63 | System.err.println(line); 64 | throw e; 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/main/java/com/philips/research/spdxbuilder/controller/UploadClient.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2021, Koninklijke Philips N.V., https://www.philips.com 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | package com.philips.research.spdxbuilder.controller; 7 | 8 | import com.philips.research.spdxbuilder.core.BusinessException; 9 | import com.philips.research.spdxbuilder.persistence.license_scanner.LicenseScannerException; 10 | import okhttp3.MediaType; 11 | import okhttp3.MultipartBody; 12 | import okhttp3.OkHttpClient; 13 | import okhttp3.RequestBody; 14 | import retrofit2.Call; 15 | import retrofit2.Retrofit; 16 | import retrofit2.http.Multipart; 17 | import retrofit2.http.POST; 18 | import retrofit2.http.Part; 19 | import retrofit2.http.Url; 20 | 21 | import java.io.File; 22 | import java.io.IOException; 23 | import java.net.URI; 24 | import java.time.Duration; 25 | 26 | interface UploadApi { 27 | @Multipart 28 | @POST 29 | Call uploadFile(@Url String path, @Part MultipartBody.Part filePart); 30 | } 31 | 32 | public class UploadClient { 33 | private static final Duration MAX_UPLOAD_DURATION = Duration.ofMinutes(5); 34 | private static final OkHttpClient CLIENT = new OkHttpClient.Builder() 35 | .writeTimeout(MAX_UPLOAD_DURATION) 36 | .readTimeout(MAX_UPLOAD_DURATION) 37 | .build(); 38 | private final UploadApi rest; 39 | private final URI uploadUrl; 40 | 41 | UploadClient(URI uploadUrl) { 42 | this.uploadUrl = uploadUrl; 43 | var uploadPath = uploadUrl.toASCIIString(); 44 | if (!uploadPath.endsWith("/")) { 45 | uploadPath += '/'; 46 | } 47 | final var retrofit = new Retrofit.Builder() 48 | .client(CLIENT) 49 | .baseUrl(uploadPath) 50 | .build(); 51 | rest = retrofit.create(UploadApi.class); 52 | } 53 | 54 | void upload(File file) { 55 | try { 56 | final var reqBody = RequestBody.create(MediaType.parse("text/plain;charset=UTF-8"), file); 57 | final var filePart = MultipartBody.Part.createFormData("file", "sbom.spdx", reqBody); 58 | final var response = rest.uploadFile(uploadUrl.getPath(), filePart).execute(); 59 | if (!response.isSuccessful()) { 60 | throw new BusinessException("SPDX upload responded with status " + response.code()); 61 | } 62 | } catch (IOException e) { 63 | throw new LicenseScannerException("The SPDX upload server is not reachable at " + uploadUrl); 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/test/java/com/philips/research/spdxbuilder/controller/UploadClientTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2021, Koninklijke Philips N.V., https://www.philips.com 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | package com.philips.research.spdxbuilder.controller; 7 | 8 | import com.philips.research.spdxbuilder.core.BusinessException; 9 | import okhttp3.mockwebserver.MockResponse; 10 | import okhttp3.mockwebserver.MockWebServer; 11 | import org.junit.jupiter.api.AfterEach; 12 | import org.junit.jupiter.api.BeforeEach; 13 | import org.junit.jupiter.api.Test; 14 | 15 | import java.io.File; 16 | import java.io.IOException; 17 | import java.net.URI; 18 | 19 | import static org.assertj.core.api.Assertions.assertThat; 20 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 21 | 22 | class UploadClientTest { 23 | private static final File FILE = new File("src/test/resources/test.txt"); 24 | private static final int PORT = 1080; 25 | private static final String PATH = "/5path/to/upload/"; 26 | 27 | private final MockWebServer mockServer = new MockWebServer(); 28 | 29 | @BeforeEach 30 | void setUp() throws IOException { 31 | mockServer.start(PORT); 32 | } 33 | 34 | @AfterEach 35 | void tearDown() throws IOException { 36 | mockServer.shutdown(); 37 | } 38 | 39 | @Test 40 | void uploadsFile() throws Exception { 41 | mockServer.enqueue(new MockResponse()); 42 | final var client = new UploadClient(mockServer.url(PATH).uri()); 43 | 44 | client.upload(FILE); 45 | 46 | final var request = mockServer.takeRequest(); 47 | assertThat(request.getMethod()).isEqualTo("POST"); 48 | assertThat(request.getPath()).isEqualTo(PATH); 49 | assertThat(request.getHeader("Content-Type")).contains("multipart/form-data"); 50 | } 51 | 52 | @Test 53 | void ignores_serverNotReachable() { 54 | var serverlessClient = new UploadClient(URI.create("http://localhost:1234")); 55 | 56 | assertThatThrownBy(() -> serverlessClient.upload(FILE)) 57 | .isInstanceOf(BusinessException.class) 58 | .hasMessageContaining("not reachable"); 59 | } 60 | 61 | @Test 62 | void throws_unexpectedResponseFromServer() throws Exception { 63 | mockServer.enqueue(new MockResponse().setResponseCode(404)); 64 | final var client = new UploadClient(mockServer.url(PATH).uri()); 65 | 66 | // Default not-found response 67 | assertThatThrownBy(() -> client.upload(FILE)) 68 | .isInstanceOf(BusinessException.class) 69 | .hasMessageContaining("status 404"); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/test/java/com/philips/research/spdxbuilder/persistence/bom_base/BomBaseClientTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2021, Koninklijke Philips N.V., https://www.philips.com 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | package com.philips.research.spdxbuilder.persistence.bom_base; 7 | 8 | import com.github.packageurl.PackageURL; 9 | import okhttp3.mockwebserver.MockResponse; 10 | import okhttp3.mockwebserver.MockWebServer; 11 | import org.json.JSONObject; 12 | import org.junit.jupiter.api.AfterEach; 13 | import org.junit.jupiter.api.BeforeEach; 14 | import org.junit.jupiter.api.Test; 15 | 16 | import java.io.IOException; 17 | import java.net.URI; 18 | 19 | import static org.assertj.core.api.Assertions.assertThat; 20 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 21 | 22 | class BomBaseClientTest { 23 | private static final String PURL = "pkg:namespace/name@version"; 24 | private static final String TITLE = "Title"; 25 | private static final int PORT = 1080; 26 | private final MockWebServer mockServer = new MockWebServer(); 27 | 28 | private final BomBaseClient client = new BomBaseClient(URI.create("http://localhost:" + PORT)); 29 | 30 | @BeforeEach 31 | void setUp() throws IOException { 32 | mockServer.start(PORT); 33 | } 34 | 35 | @AfterEach 36 | void tearDown() throws IOException { 37 | mockServer.shutdown(); 38 | } 39 | 40 | @Test 41 | void getPackageMetadata() throws Exception { 42 | mockServer.enqueue(new MockResponse().setBody(new JSONObject() 43 | .put("attributes", new JSONObject() 44 | .put("title", TITLE)).toString())); 45 | 46 | final var meta = client.readPackage(new PackageURL(PURL)).orElseThrow(); 47 | 48 | assertThat(meta.getTitle()).contains(TITLE); 49 | final var request = mockServer.takeRequest(); 50 | assertThat(request.getMethod()).isEqualTo("GET"); 51 | assertThat(request.getPath()).isEqualTo("/packages/pkg%253Anamespace%252Fname%2540version"); 52 | } 53 | 54 | @Test 55 | void empty_packageNotFound() throws Exception { 56 | mockServer.enqueue(new MockResponse().setResponseCode(404)); 57 | 58 | final var meta = client.readPackage(new PackageURL(PURL)); 59 | 60 | assertThat(meta).isEmpty(); 61 | } 62 | 63 | @Test 64 | void throws_errorStatus() { 65 | mockServer.enqueue(new MockResponse().setResponseCode(500)); 66 | 67 | assertThatThrownBy(() -> client.readPackage(new PackageURL(PURL))) 68 | .isInstanceOf(BomBaseException.class) 69 | .hasMessageContaining("responded with status 500"); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/main/java/com/philips/research/spdxbuilder/persistence/license_scanner/LicenseKnowledgeBase.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2021, Koninklijke Philips N.V., https://www.philips.com 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | package com.philips.research.spdxbuilder.persistence.license_scanner; 7 | 8 | import com.philips.research.spdxbuilder.core.KnowledgeBase; 9 | import com.philips.research.spdxbuilder.core.domain.LicenseDictionary; 10 | import com.philips.research.spdxbuilder.core.domain.LicenseParser; 11 | import com.philips.research.spdxbuilder.core.domain.Package; 12 | 13 | import java.net.URI; 14 | import java.util.Optional; 15 | 16 | /** 17 | * Knowledge base implementation for the License Scanner service. 18 | * See https://github.com/philips-software/license-scanner 19 | */ 20 | public class LicenseKnowledgeBase extends KnowledgeBase { 21 | final LicenseScannerClient licenseClient; 22 | 23 | public LicenseKnowledgeBase(URI uri) { 24 | this(new LicenseScannerClient(uri)); 25 | } 26 | 27 | LicenseKnowledgeBase(LicenseScannerClient client) { 28 | this.licenseClient = client; 29 | } 30 | 31 | @Override 32 | public boolean enhance(Package pkg) { 33 | final var purl = pkg.getPurl(); 34 | if (purl.isEmpty()) { 35 | return false; 36 | } 37 | 38 | return detectLicense(pkg) 39 | .map(l -> { 40 | final var scanned = LicenseParser.parse(l.getLicense()); 41 | final var declared = pkg.getDeclaredLicense().orElse(scanned); 42 | pkg.addDetectedLicense(scanned); 43 | if (l.isConfirmed()) { 44 | pkg.setConcludedLicense(scanned); 45 | } else { 46 | final var dictionary = LicenseDictionary.getInstance(); 47 | final var scannedText = dictionary.expand(scanned); 48 | final var declaredText = dictionary.expand(declared); 49 | if (!scannedText.equals(declaredText)) { 50 | //noinspection OptionalGetWithoutIsPresent 51 | licenseClient.contest(pkg.getPurl().get(), declaredText); 52 | } 53 | } 54 | return l; 55 | }).isPresent(); 56 | } 57 | 58 | private Optional detectLicense(Package pkg) { 59 | try { 60 | //noinspection OptionalGetWithoutIsPresent 61 | return licenseClient.scanLicense(pkg.getPurl().get(), pkg.getSourceLocation().orElse(null)); 62 | } catch (LicenseScannerException e) { 63 | System.err.println("ERROR: " + e.getMessage()); 64 | return Optional.empty(); 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/main/java/com/philips/research/spdxbuilder/persistence/bom_base/BomBaseClient.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2021, Koninklijke Philips N.V., https://www.philips.com 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | package com.philips.research.spdxbuilder.persistence.bom_base; 7 | 8 | import com.fasterxml.jackson.annotation.JsonAutoDetect; 9 | import com.fasterxml.jackson.annotation.PropertyAccessor; 10 | import com.fasterxml.jackson.core.JsonProcessingException; 11 | import com.fasterxml.jackson.databind.DeserializationFeature; 12 | import com.fasterxml.jackson.databind.ObjectMapper; 13 | import com.fasterxml.jackson.databind.PropertyNamingStrategies; 14 | import com.github.packageurl.PackageURL; 15 | import retrofit2.Call; 16 | import retrofit2.Retrofit; 17 | import retrofit2.converter.jackson.JacksonConverterFactory; 18 | 19 | import java.io.IOException; 20 | import java.net.URI; 21 | import java.net.URLEncoder; 22 | import java.nio.charset.StandardCharsets; 23 | import java.util.Optional; 24 | 25 | class BomBaseClient { 26 | private static final ObjectMapper MAPPER = new ObjectMapper() 27 | .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) 28 | .setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.NON_PRIVATE) 29 | .setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE); 30 | 31 | private final URI server; 32 | private final BomBaseApi rest; 33 | 34 | BomBaseClient(URI server) { 35 | this.server = server; 36 | final var retrofit = new Retrofit.Builder() 37 | .baseUrl(server.toASCIIString()) 38 | .addConverterFactory(JacksonConverterFactory.create(MAPPER)) 39 | .build(); 40 | rest = retrofit.create(BomBaseApi.class); 41 | } 42 | 43 | Optional readPackage(PackageURL purl) { 44 | final var param = encode(purl.canonicalize()); 45 | return query(rest.getPackage(param)).map(meta -> meta); 46 | } 47 | 48 | private String encode(String uri) { 49 | return URLEncoder.encode(uri, StandardCharsets.UTF_8); 50 | } 51 | 52 | private Optional query(Call query) { 53 | try { 54 | final var response = query.execute(); 55 | if (response.code() == 404) { 56 | return Optional.empty(); 57 | } 58 | if (!response.isSuccessful()) { 59 | throw new BomBaseException("BOM-base server responded with status " + response.code()); 60 | } 61 | return Optional.ofNullable(response.body()); 62 | } catch (JsonProcessingException e) { 63 | throw new IllegalArgumentException("JSON formatting error", e); 64 | } catch (IOException e) { 65 | throw new BomBaseException("The BOM-base knowledge base is not reachable at " + server); 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/main/java/com/philips/research/spdxbuilder/persistence/spdx/TagValueDocument.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2021, Koninklijke Philips N.V., https://www.philips.com 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | package com.philips.research.spdxbuilder.persistence.spdx; 7 | 8 | import pl.tlinkowski.annotation.basic.NullOr; 9 | 10 | import java.io.*; 11 | import java.util.Optional; 12 | 13 | /** 14 | * SPDX Tag-value format implementation. 15 | */ 16 | public class TagValueDocument implements Closeable { 17 | @SuppressWarnings("SpellCheckingInspection") 18 | private static final String NO_ASSERTION = "NOASSERTION"; 19 | private static final String NONE = "NONE"; 20 | 21 | private final Writer writer; 22 | 23 | /** 24 | * Starts a new tag-value document. 25 | */ 26 | public TagValueDocument(OutputStream stream) { 27 | writer = new OutputStreamWriter(stream); 28 | } 29 | 30 | /** 31 | * Writes a tag only if a value is present. 32 | * 33 | * @param tag type of the value 34 | * @param value optional value 35 | */ 36 | public void optionallyAddValue(String tag, @SuppressWarnings("OptionalUsedAsFieldOrParameterType") Optional value) throws IOException { 37 | if (value.isPresent()) { 38 | addValue(tag, value); 39 | } 40 | } 41 | 42 | /** 43 | * Writes a tag with a value, converting a null or empty Optional to "NOASSERTION" 44 | * and an empty string to "NONE". 45 | * 46 | * @param tag type of the value 47 | * @param value Optional or Object value 48 | */ 49 | public void addValue(String tag, @NullOr Object value) throws IOException { 50 | if (value instanceof Optional) { 51 | value = ((Optional) value).orElse(null); 52 | } 53 | 54 | if (value == null) { 55 | value = NO_ASSERTION; 56 | } 57 | 58 | final var string = value.toString(); 59 | if (string.isBlank()) { 60 | value = NONE; 61 | } 62 | final var isMultiline = string.contains("\r") || string.contains("\n") || string.contains("text>"); 63 | final var escaped = value.toString().replaceAll("", " "); 64 | writeLine(tag + ": " + (isMultiline ? "" + escaped + "" : value)); 65 | } 66 | 67 | /** 68 | * Writes an empty separator line. 69 | */ 70 | public void addEmptyLine() throws IOException { 71 | writeLine(""); 72 | } 73 | 74 | /** 75 | * Writes a comment line. 76 | */ 77 | public void addComment(String comment) throws IOException { 78 | writeLine("## " + comment); 79 | } 80 | 81 | private void writeLine(String line) throws IOException { 82 | writer.write(line); 83 | writer.write('\n'); 84 | } 85 | 86 | @Override 87 | public void close() throws IOException { 88 | if (writer != null) { 89 | writer.close(); 90 | } 91 | } 92 | 93 | } 94 | 95 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /src/main/java/com/philips/research/spdxbuilder/core/domain/BillOfMaterials.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2021, Koninklijke Philips N.V., https://www.philips.com 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | package com.philips.research.spdxbuilder.core.domain; 7 | 8 | import pl.tlinkowski.annotation.basic.NullOr; 9 | 10 | import java.net.URI; 11 | import java.time.LocalDateTime; 12 | import java.util.*; 13 | 14 | /** 15 | * Report on the composition of a product 16 | */ 17 | public class BillOfMaterials { 18 | private final List packages = new ArrayList<>(); 19 | private final Set relations = new HashSet<>(); 20 | private @NullOr String title; 21 | private @NullOr String comment; 22 | private @NullOr Party organization; 23 | private @NullOr String identifier; 24 | private @NullOr URI namespace; 25 | private @NullOr LocalDateTime createdAt; 26 | 27 | public Optional getCreatedAt() { 28 | return Optional.ofNullable(createdAt); 29 | } 30 | 31 | public BillOfMaterials setCreatedAt(LocalDateTime createdTime) { 32 | this.createdAt = createdTime; 33 | return this; 34 | } 35 | 36 | public List getPackages() { 37 | return packages; 38 | } 39 | 40 | public BillOfMaterials addPackage(Package pkg) { 41 | packages.add(pkg); 42 | return this; 43 | } 44 | 45 | public BillOfMaterials addRelation(Package from, Package to, Relation.Type type) { 46 | relations.add(new Relation(from, to, type)); 47 | return this; 48 | } 49 | 50 | public Collection getRelations() { 51 | return relations; 52 | } 53 | 54 | public String getTitle() { 55 | if (title == null) { 56 | return packages.stream() 57 | .findFirst().map(Package::getName) 58 | .orElse(""); 59 | } 60 | return title; 61 | } 62 | 63 | public BillOfMaterials setTitle(String title) { 64 | this.title = title; 65 | return this; 66 | } 67 | 68 | public Optional getComment() { 69 | return Optional.ofNullable(comment); 70 | } 71 | 72 | public BillOfMaterials setComment(String comment) { 73 | this.comment = comment; 74 | return this; 75 | } 76 | 77 | public Optional getOrganization() { 78 | return Optional.ofNullable(organization); 79 | } 80 | 81 | public BillOfMaterials setOrganization(Party organization) { 82 | this.organization = organization; 83 | return this; 84 | } 85 | 86 | public Optional getIdentifier() { 87 | return Optional.ofNullable(identifier); 88 | } 89 | 90 | public BillOfMaterials setIdentifier(@NullOr String identifier) { 91 | this.identifier = (identifier != null && !identifier.isBlank()) ? identifier : null; 92 | return this; 93 | } 94 | 95 | public Optional getNamespace() { 96 | return Optional.ofNullable(namespace); 97 | } 98 | 99 | public BillOfMaterials setNamespace(@NullOr URI namespace) { 100 | this.namespace = namespace; 101 | return this; 102 | } 103 | } 104 | 105 | -------------------------------------------------------------------------------- /src/test/java/com/philips/research/spdxbuilder/core/domain/PackageTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2021, Koninklijke Philips N.V., https://www.philips.com 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | package com.philips.research.spdxbuilder.core.domain; 7 | 8 | import com.github.packageurl.PackageURL; 9 | import nl.jqno.equalsverifier.EqualsVerifier; 10 | import org.junit.jupiter.api.Test; 11 | 12 | import static org.assertj.core.api.Assertions.assertThat; 13 | 14 | class PackageTest { 15 | private static final String TYPE = "type"; 16 | private static final String NAMESPACE = "Namespace"; 17 | private static final String NAME = "Name"; 18 | private static final String VERSION = "Version"; 19 | private static final License LICENSE = License.of("MIT"); 20 | 21 | final Package pkg = new Package(NAMESPACE, NAME, VERSION); 22 | 23 | @Test 24 | void createsAnonymousInstance() { 25 | assertThat(pkg.getNamespace()).isEqualTo(NAMESPACE); 26 | assertThat(pkg.getName()).isEqualTo(NAME); 27 | assertThat(pkg.getFullName()).isEqualTo(NAMESPACE + '/' + NAME); 28 | assertThat(pkg.getVersion()).isEqualTo(VERSION); 29 | assertThat(pkg.isInternal()).isFalse(); 30 | assertThat(pkg.getPurl()).isEmpty(); 31 | assertThat(pkg.getSourceLocation()).isEmpty(); 32 | assertThat(pkg.getDownloadLocation()).isEmpty(); 33 | assertThat(pkg.getConcludedLicense()).isEmpty(); 34 | assertThat(pkg.getDeclaredLicense()).isEmpty(); 35 | assertThat(pkg.getDetectedLicenses()).isEmpty(); 36 | } 37 | 38 | @Test 39 | void createsInstanceWithoutNamespace() { 40 | final var pkg = new Package(null, NAME, VERSION); 41 | 42 | assertThat(pkg.getNamespace()).isEmpty(); 43 | assertThat(pkg.getFullName()).isEqualTo(NAME); 44 | } 45 | 46 | @Test 47 | void createsInstanceFromPackageUrl() throws Exception { 48 | final var purl = new PackageURL("pkg:" + TYPE + '/' + NAMESPACE + '/' + NAME + '@' + VERSION); 49 | final var fromPurl = new Package(purl); 50 | 51 | assertThat(fromPurl.getNamespace()).isEqualTo(NAMESPACE); 52 | assertThat(fromPurl.getName()).isEqualTo(NAME); 53 | assertThat(fromPurl.getVersion()).isEqualTo(VERSION); 54 | assertThat(fromPurl.getPurl()).contains(purl); 55 | } 56 | 57 | @Test 58 | void overridesExplicitPackageUrl() throws Exception { 59 | final var purl = new PackageURL("pkg:type/custom@1.2.3"); 60 | pkg.setPurl(purl); 61 | 62 | assertThat(pkg.getPurl()).contains(purl); 63 | } 64 | 65 | @Test 66 | void tracksInternalPackages() { 67 | pkg.setInternal(true); 68 | 69 | assertThat(pkg.isInternal()).isTrue(); 70 | } 71 | 72 | @Test 73 | void addsDetectedLicenses() { 74 | pkg.addDetectedLicense(LICENSE); 75 | pkg.addDetectedLicense(LICENSE); 76 | pkg.addDetectedLicense(License.of("Something custom")); 77 | pkg.addDetectedLicense(License.NONE); 78 | 79 | assertThat(pkg.getDetectedLicenses()).hasSize(2); 80 | } 81 | 82 | @Test 83 | void implementsEquals() { 84 | EqualsVerifier.forClass(Package.class) 85 | .withOnlyTheseFields("namespace", "name", "version") 86 | .verify(); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/main/java/com/philips/research/spdxbuilder/core/domain/PurlGlob.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2021, Koninklijke Philips N.V., https://www.philips.com 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | package com.philips.research.spdxbuilder.core.domain; 7 | 8 | import com.github.packageurl.PackageURL; 9 | import pl.tlinkowski.annotation.basic.NullOr; 10 | 11 | public class PurlGlob { 12 | private static final String ANY = "*"; 13 | 14 | private final String type; 15 | private final String namespace; 16 | private final String name; 17 | private final String version; 18 | 19 | public PurlGlob(String pattern) { 20 | final var sanitized = sanitized(pattern); 21 | 22 | final var versionPos = posOrLength(sanitized, '@'); 23 | version = versionPos < sanitized.length() ? sanitized.substring(versionPos + 1) : ANY; 24 | 25 | final var path = sanitized.substring(0, versionPos).split("/"); 26 | if (path.length == 1 && !path[0].isEmpty()) { 27 | type = ANY; 28 | namespace = ANY; 29 | name = path[0]; 30 | } else if (path.length == 2) { 31 | namespace = ANY; 32 | type = path[0]; 33 | name = path[1]; 34 | } else if (path.length == 3) { 35 | type = path[0]; 36 | namespace = path[1]; 37 | name = path[2]; 38 | } else { 39 | throw new IllegalArgumentException("Invalid package URL glob: " + pattern); 40 | } 41 | } 42 | 43 | private String sanitized(String purl) { 44 | final var start = purl.startsWith("pkg:") ? 4 : 0; 45 | return purl.substring(start, posOrLength(purl, '#', '?')); 46 | } 47 | 48 | private int posOrLength(String string, char... chars) { 49 | int pos = string.length(); 50 | for (var ch : chars) { 51 | final var index = string.indexOf(ch); 52 | if (index >= 0 && index < pos) { 53 | pos = index; 54 | } 55 | } 56 | return pos; 57 | } 58 | 59 | public boolean matches(PackageURL purl) { 60 | return matches(type, purl.getType()) 61 | && matches(namespace, purl.getNamespace()) 62 | && matches(name, purl.getName()) 63 | && matches(version, purl.getVersion()); 64 | } 65 | 66 | private boolean matches(String pattern, @NullOr String string) { 67 | string = string == null ? "" : string; 68 | var pos = 0; 69 | for (var i = 0; i < pattern.length(); i++) { 70 | if (pattern.substring(i).equals(string.substring(pos))) { 71 | return true; 72 | } 73 | if (pattern.charAt(i) == '*') { 74 | final var remaining = pattern.substring(i + 1); 75 | for (var sub = pos; sub <= string.length(); sub++) { 76 | if (matches(remaining, string.substring(sub))) { 77 | return true; 78 | } 79 | } 80 | return false; 81 | } 82 | if (pos == string.length() || pattern.charAt(i) != string.charAt(pos)) { 83 | return false; 84 | } 85 | pos++; 86 | } 87 | return pattern.isEmpty() && string.isEmpty(); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/test/java/com/philips/research/spdxbuilder/core/domain/PurlGlobTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2021, Koninklijke Philips N.V., https://www.philips.com 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | package com.philips.research.spdxbuilder.core.domain; 7 | 8 | import com.github.packageurl.MalformedPackageURLException; 9 | import com.github.packageurl.PackageURL; 10 | import org.junit.jupiter.api.Test; 11 | 12 | import static org.assertj.core.api.Assertions.assertThat; 13 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 14 | 15 | class PurlGlobTest { 16 | private static final String TYPE = "type"; 17 | private static final String NAMESPACE = "namespace"; 18 | private static final String NAME = "name"; 19 | private static final String VERSION = "version"; 20 | private static final String OTHER = "other"; 21 | private static final String BASE = String.format("%s/%s/%s", TYPE, NAMESPACE, NAME); 22 | private static final String FULL_PURL = String.format("pkg:%s@%s", BASE, VERSION); 23 | private static final PackageURL PURL = toPurl(FULL_PURL); 24 | 25 | static PackageURL toPurl(String purl) { 26 | try { 27 | return new PackageURL(purl); 28 | } catch (MalformedPackageURLException e) { 29 | throw new IllegalArgumentException(e); 30 | } 31 | } 32 | 33 | @Test 34 | void matchesPerPart() { 35 | assertThat(new PurlGlob(FULL_PURL).matches(PURL)).isTrue(); 36 | assertThat(new PurlGlob(TYPE + '/' + NAMESPACE + '/' + NAME).matches(PURL)).isTrue(); 37 | assertThat(new PurlGlob(TYPE + '/' + NAME).matches(PURL)).isTrue(); 38 | assertThat(new PurlGlob(NAME).matches(PURL)).isTrue(); 39 | } 40 | 41 | @Test 42 | void failsPerPart() { 43 | assertThat(new PurlGlob(OTHER + '/' + NAMESPACE + '/' + NAME).matches(PURL)).isFalse(); 44 | assertThat(new PurlGlob(TYPE + '/' + OTHER + '/' + NAME).matches(PURL)).isFalse(); 45 | assertThat(new PurlGlob(TYPE + '/' + NAMESPACE + '/' + OTHER).matches(PURL)).isFalse(); 46 | } 47 | 48 | @Test 49 | void ignoresExtraParts() { 50 | assertThat(new PurlGlob(FULL_PURL).matches(toPurl(FULL_PURL + "#subpath"))).isTrue(); 51 | assertThat(new PurlGlob(FULL_PURL).matches(toPurl(FULL_PURL + "?qualifiers"))).isTrue(); 52 | } 53 | 54 | @Test 55 | void throws_missingNamePart() { 56 | assertThatThrownBy(() -> new PurlGlob("@version")) 57 | .isInstanceOf(IllegalArgumentException.class) 58 | .hasMessageContaining("glob"); 59 | } 60 | 61 | @Test 62 | void throws_invalidPackageUrl() { 63 | assertThatThrownBy(() -> new PurlGlob("not/a/valid/purl")) 64 | .isInstanceOf(IllegalArgumentException.class) 65 | .hasMessageContaining("glob"); 66 | } 67 | 68 | @Test 69 | void matchesWildcard() { 70 | final var glob = new PurlGlob("A*B"); 71 | 72 | assertThat(glob.matches(toPurl("pkg:type/AB"))).isTrue(); 73 | assertThat(glob.matches(toPurl("pkg:type/AsomethingB"))).isTrue(); 74 | assertThat(glob.matches(toPurl("pkg:type/AB@version"))).isTrue(); 75 | assertThat(glob.matches(toPurl("pkg:type/xAsomethingB"))).isFalse(); 76 | assertThat(glob.matches(toPurl("pkg:type/AsomethingBx"))).isFalse(); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/main/java/com/philips/research/spdxbuilder/persistence/spdx/SpdxParty.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2021, Koninklijke Philips N.V., https://www.philips.com 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | package com.philips.research.spdxbuilder.persistence.spdx; 7 | 8 | import com.philips.research.spdxbuilder.core.domain.Party; 9 | import pl.tlinkowski.annotation.basic.NullOr; 10 | 11 | import java.util.Objects; 12 | import java.util.Optional; 13 | 14 | /** 15 | * Named organization or individual. 16 | */ 17 | final class SpdxParty { 18 | private final String type; 19 | private final String name; 20 | private final @NullOr String email; 21 | 22 | private SpdxParty(String type, String name) { 23 | this(type, name, null); 24 | } 25 | 26 | private SpdxParty(String type, String name, @NullOr String email) { 27 | this.type = type; 28 | this.name = name; 29 | this.email = email; 30 | } 31 | 32 | /** 33 | * @return tool description 34 | */ 35 | static SpdxParty tool(String name, @NullOr String version) { 36 | if (version != null) { 37 | name += '-' + version; 38 | } 39 | return new SpdxParty("Tool", name); 40 | } 41 | 42 | /** 43 | * @return organization description 44 | */ 45 | static SpdxParty organization(String name) { 46 | return new SpdxParty("Organization", name); 47 | } 48 | 49 | /** 50 | * @return individual description 51 | */ 52 | static SpdxParty person(String name, @NullOr String email) { 53 | return new SpdxParty("Person", name, email); 54 | } 55 | 56 | /** 57 | * @return description from an optional party entity or null if none was provided 58 | */ 59 | @SuppressWarnings("OptionalUsedAsFieldOrParameterType") 60 | static @NullOr SpdxParty from(Optional party) { 61 | return party.map(SpdxParty::from).orElse(null); 62 | } 63 | 64 | /** 65 | * @return description from a party entity 66 | */ 67 | static @NullOr SpdxParty from(Party party) { 68 | switch (party.getType()) { 69 | case PERSON: 70 | return person(party.getName(), null); 71 | case TOOL: 72 | return tool(party.getName(), null); 73 | case ORGANIZATION: 74 | return organization(party.getName()); 75 | default: 76 | case NONE: 77 | return null; 78 | } 79 | } 80 | 81 | @Override 82 | public boolean equals(Object o) { 83 | if (this == o) return true; 84 | if (!(o instanceof SpdxParty)) return false; 85 | SpdxParty spdxParty = (SpdxParty) o; 86 | return Objects.equals(type, spdxParty.type) && 87 | Objects.equals(name, spdxParty.name) && 88 | Objects.equals(email, spdxParty.email); 89 | } 90 | 91 | @Override 92 | public int hashCode() { 93 | return Objects.hash(type, name, email); 94 | } 95 | 96 | @Override 97 | public String toString() { 98 | var string = type + ": " + name; 99 | if (email != null) { 100 | string += " (" + email + ')'; 101 | } 102 | return string; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/test/java/com/philips/research/spdxbuilder/persistence/license_scanner/LicenseKnowledgeBaseTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2021, Koninklijke Philips N.V., https://www.philips.com 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | package com.philips.research.spdxbuilder.persistence.license_scanner; 7 | 8 | import com.github.packageurl.MalformedPackageURLException; 9 | import com.github.packageurl.PackageURL; 10 | import com.philips.research.spdxbuilder.core.domain.BillOfMaterials; 11 | import com.philips.research.spdxbuilder.core.domain.License; 12 | import com.philips.research.spdxbuilder.core.domain.Package; 13 | import org.junit.jupiter.api.Test; 14 | 15 | import java.net.URI; 16 | import java.util.Optional; 17 | 18 | import static org.assertj.core.api.Assertions.assertThat; 19 | import static org.mockito.Mockito.*; 20 | 21 | class LicenseKnowledgeBaseTest { 22 | private static final URI LOCATION = URI.create("https://example.com"); 23 | private static final License LICENSE = License.of("MIT"); 24 | private static final PackageURL PURL = purlFrom("pkg:maven/namespace/name@version"); 25 | 26 | private final Package pkg = new Package("Namespace", "Name", "Version").setPurl(PURL); 27 | private final BillOfMaterials bom = new BillOfMaterials().addPackage(pkg); 28 | private final LicenseScannerClient client = mock(LicenseScannerClient.class); 29 | private final LicenseKnowledgeBase knowledgeBase = new LicenseKnowledgeBase(client); 30 | 31 | static PackageURL purlFrom(String purl) { 32 | try { 33 | return new PackageURL(purl); 34 | } catch (MalformedPackageURLException e) { 35 | throw new IllegalArgumentException(e); 36 | } 37 | } 38 | 39 | @Test 40 | void checksLicenses() { 41 | pkg.setSourceLocation(LOCATION); 42 | final var info = new LicenseScannerClient.LicenseInfo(LICENSE.toString(), false); 43 | when(client.scanLicense(PURL, LOCATION)).thenReturn(Optional.of(info)); 44 | 45 | knowledgeBase.enhance(bom); 46 | 47 | assertThat(pkg.getDetectedLicenses()).contains(LICENSE); 48 | verify(client, never()).contest(any(), any()); 49 | } 50 | 51 | @Test 52 | void contestsUnconfirmedLicense() { 53 | pkg.setDeclaredLicense(LICENSE); 54 | final var info = new LicenseScannerClient.LicenseInfo("Other", false); 55 | when(client.scanLicense(eq(PURL), any())).thenReturn(Optional.of(info)); 56 | 57 | knowledgeBase.enhance(bom); 58 | 59 | assertThat(pkg.getConcludedLicense()).isEmpty(); 60 | verify(client).contest(PURL, LICENSE.toString()); 61 | } 62 | 63 | @Test 64 | void acceptsConfirmedLicense() { 65 | pkg.setDeclaredLicense(License.of("Other")); 66 | final var info = new LicenseScannerClient.LicenseInfo(LICENSE.toString(), true); 67 | when(client.scanLicense(eq(PURL), any())).thenReturn(Optional.of(info)); 68 | 69 | knowledgeBase.enhance(bom); 70 | 71 | assertThat(pkg.getConcludedLicense()).contains(LICENSE); 72 | verify(client, never()).contest(any(), any()); 73 | } 74 | 75 | @Test 76 | void ignoresCommunicationExceptions() { 77 | pkg.setDeclaredLicense(License.of("Other")); 78 | when(client.scanLicense(any(), any())).thenThrow(new LicenseScannerException("Test")); 79 | 80 | knowledgeBase.enhance(bom); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/main/java/com/philips/research/spdxbuilder/controller/TreeConfiguration.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2021, Koninklijke Philips N.V., https://www.philips.com 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | package com.philips.research.spdxbuilder.controller; 7 | 8 | import com.fasterxml.jackson.annotation.JsonAutoDetect; 9 | import com.fasterxml.jackson.annotation.PropertyAccessor; 10 | import com.fasterxml.jackson.databind.MapperFeature; 11 | import com.fasterxml.jackson.databind.ObjectMapper; 12 | import com.fasterxml.jackson.databind.exc.MismatchedInputException; 13 | import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; 14 | import com.philips.research.spdxbuilder.core.ConversionService; 15 | import pl.tlinkowski.annotation.basic.NullOr; 16 | 17 | import java.io.IOException; 18 | import java.io.InputStream; 19 | import java.io.InputStreamReader; 20 | import java.net.URI; 21 | import java.util.ArrayList; 22 | import java.util.List; 23 | 24 | public class TreeConfiguration { 25 | private static final ObjectMapper MAPPER = new ObjectMapper(new YAMLFactory()) 26 | .setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.NON_PRIVATE) 27 | .configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true); 28 | 29 | private final Configuration config; 30 | 31 | private TreeConfiguration(Configuration config) { 32 | this.config = config; 33 | } 34 | 35 | /** 36 | * Parses a configuration from the provided YAML input stream. 37 | * 38 | * @param stream YAML text stream 39 | * @return configuration 40 | */ 41 | static TreeConfiguration parse(InputStream stream) { 42 | try (final var reader = new InputStreamReader(stream)) { 43 | final var config = MAPPER.readValue(reader, Configuration.class); 44 | return new TreeConfiguration(config); 45 | } catch (MismatchedInputException e) { 46 | throw new IllegalArgumentException("Configuration format error", e); 47 | } catch (IOException e) { 48 | throw new IllegalArgumentException("Malformed configuration file: ", e); 49 | } 50 | } 51 | 52 | /** 53 | * Applies the generic document configuration. 54 | * 55 | * @param service target 56 | */ 57 | void apply(ConversionService service) { 58 | final var document = config.getDocument(); 59 | service.setDocument(document.title, document.organization); 60 | if (document.comment != null) { 61 | service.setComment(document.comment); 62 | } 63 | if (document.key != null) { 64 | service.setDocReference(document.key); 65 | } 66 | if (document.namespace != null) { 67 | service.setDocNamespace(document.namespace); 68 | } 69 | } 70 | 71 | List getInternalGlobs() { 72 | return config.internal; 73 | } 74 | 75 | private static class Configuration { 76 | @NullOr Document document; 77 | List internal = new ArrayList<>(); 78 | 79 | Document getDocument() { 80 | return (document != null) ? document : new Document(); 81 | } 82 | } 83 | 84 | private static class Document { 85 | String title = ""; 86 | String organization = ""; 87 | @NullOr String comment; 88 | @NullOr String key; 89 | @NullOr URI namespace; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/main/java/com/philips/research/spdxbuilder/core/domain/ConversionInteractor.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2021, Koninklijke Philips N.V., https://www.philips.com 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | package com.philips.research.spdxbuilder.core.domain; 7 | 8 | import com.github.packageurl.PackageURL; 9 | import com.philips.research.spdxbuilder.core.*; 10 | import pl.tlinkowski.annotation.basic.NullOr; 11 | 12 | import java.net.URI; 13 | import java.util.Objects; 14 | import java.util.function.Consumer; 15 | 16 | /** 17 | * Implementation of conversion use cases. 18 | */ 19 | public class ConversionInteractor implements ConversionService, AutoCloseable { 20 | private final BomReader reader; 21 | private final BomProcessor writer; 22 | private final BillOfMaterials bom; 23 | 24 | private @NullOr KnowledgeBase knowledgeBase; 25 | 26 | public ConversionInteractor(BomReader reader, BomProcessor writer) { 27 | this(reader, writer, new BillOfMaterials()); 28 | } 29 | 30 | ConversionInteractor(BomReader reader, BomProcessor writer, BillOfMaterials bom) { 31 | this.reader = reader; 32 | this.writer = writer; 33 | this.bom = bom; 34 | } 35 | 36 | public ConversionInteractor setKnowledgeBase(KnowledgeBase knowledgeBase) { 37 | this.knowledgeBase = knowledgeBase; 38 | return this; 39 | } 40 | 41 | @Override 42 | public void setDocument(String title, String organization) { 43 | bom.setTitle(title); 44 | bom.setOrganization(new Party(Party.Type.ORGANIZATION, organization)); 45 | } 46 | 47 | @Override 48 | public void setComment(String comment) { 49 | bom.setComment(comment); 50 | } 51 | 52 | @Override 53 | public void setDocReference(String spdxId) { 54 | bom.setIdentifier(spdxId); 55 | } 56 | 57 | @Override 58 | public void setDocNamespace(URI namespace) { 59 | bom.setNamespace(namespace); 60 | } 61 | 62 | @Override 63 | public void curatePackageLicense(PackageURL purl, String license) { 64 | //FIXME Should be stored first 65 | curate(purl, pkg -> pkg.setConcludedLicense(LicenseParser.parse(license))); 66 | } 67 | 68 | @Override 69 | public void curatePackageSource(PackageURL purl, URI source) { 70 | //FIXME Should be stored first 71 | curate(purl, pkg -> pkg.setSourceLocation(source)); 72 | } 73 | 74 | @Override 75 | public void read() { 76 | reader.read(bom); 77 | } 78 | 79 | @Override 80 | public void apply(BomProcessor processor) { 81 | processor.process(bom); 82 | } 83 | 84 | @Override 85 | public void convert(boolean continueIfIncomplete) { 86 | if (knowledgeBase != null) { 87 | final var success = knowledgeBase.enhance(bom); 88 | if (!success && !continueIfIncomplete) { 89 | throw new BusinessException("Enhancement of metadata failed"); 90 | } 91 | } 92 | //TODO Curate before writing 93 | writer.process(bom); 94 | } 95 | 96 | private void curate(PackageURL purl, Consumer curate) { 97 | bom.getPackages().stream() 98 | //FIXME Will never be equal!? 99 | .filter(pkg -> Objects.equals(purl, pkg.getPurl())) 100 | .forEach(curate); 101 | } 102 | 103 | @Override 104 | public void close() throws Exception { 105 | if (this.writer != null) { 106 | this.writer.close(); 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/main/java/com/philips/research/spdxbuilder/controller/TreeCommand.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2021, Koninklijke Philips N.V., https://www.philips.com 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | package com.philips.research.spdxbuilder.controller; 7 | 8 | import com.philips.research.spdxbuilder.core.BomProcessor; 9 | import com.philips.research.spdxbuilder.core.BomReader; 10 | import com.philips.research.spdxbuilder.core.BusinessException; 11 | import com.philips.research.spdxbuilder.core.ConversionService; 12 | import com.philips.research.spdxbuilder.core.domain.ConversionInteractor; 13 | import com.philips.research.spdxbuilder.persistence.bom_base.BomBaseKnowledgeBase; 14 | import com.philips.research.spdxbuilder.persistence.spdx.SpdxWriter; 15 | import com.philips.research.spdxbuilder.persistence.tree.TreeFormats; 16 | import com.philips.research.spdxbuilder.persistence.tree.TreeReader; 17 | import picocli.CommandLine; 18 | import picocli.CommandLine.Command; 19 | import pl.tlinkowski.annotation.basic.NullOr; 20 | 21 | import java.io.File; 22 | import java.io.FileInputStream; 23 | import java.io.IOException; 24 | import java.net.URI; 25 | 26 | /** 27 | * CLI command to export the SBOM from a textual tree representation to an SPDX file. 28 | */ 29 | @Command(name = "tree", description = "Converts the package tree from your build tool into a bill-of-materials.") 30 | public class TreeCommand extends AbstractCommand { 31 | @CommandLine.Option(names = {"--format", "-f"}, description = "Format of the tree to parse") 32 | @NullOr String format; 33 | 34 | @CommandLine.Option(names = {"--custom"}, description = "Custom formats extension file") 35 | @NullOr File formatExtension; 36 | 37 | @CommandLine.Option(names = {"--config", "-c"}, description = "Configuration YAML file", paramLabel = "FILE", defaultValue = ".spdx-builder.yml") 38 | @SuppressWarnings("NotNullFieldNotInitialized") 39 | File configFile; 40 | 41 | @CommandLine.Option(names = {"--kb", "--bombase"}, description = "Add package metadata from BOM-base knowledge base", paramLabel = "SERVER_URL") 42 | @NullOr URI bomBase; 43 | 44 | @CommandLine.Option(names = {"--release"}, description = "Root packages expose their package URL", defaultValue = "false") 45 | boolean isRelease; 46 | 47 | @Override 48 | public void run() { 49 | if (format == null) { 50 | System.err.println("Missing required format specification. Available formats are:\n"); 51 | new TreeFormats().printFormats(); 52 | throw new BusinessException("No tree format specification provided"); 53 | } 54 | super.run(); 55 | } 56 | 57 | @Override 58 | protected ConversionService createService() { 59 | final var config = readConfiguration(); 60 | final BomReader reader = new TreeReader(System.in, format, formatExtension, config.getInternalGlobs()) 61 | .setRelease(isRelease); 62 | final BomProcessor writer = new SpdxWriter(spdxStream); 63 | 64 | final var service = bomBase != null 65 | ? new ConversionInteractor(reader, writer).setKnowledgeBase(new BomBaseKnowledgeBase(bomBase)) 66 | : new ConversionInteractor(reader, writer); 67 | 68 | config.apply(service); 69 | 70 | return service; 71 | } 72 | 73 | private TreeConfiguration readConfiguration() { 74 | try (final var stream = new FileInputStream(configFile)) { 75 | return TreeConfiguration.parse(stream); 76 | } catch (IOException e) { 77 | throw new BusinessException("Failed to read configuration file from " + configFile); 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/test/java/com/philips/research/spdxbuilder/persistence/spdx/TagValueDocumentTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2021, Koninklijke Philips N.V., https://www.philips.com 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | package com.philips.research.spdxbuilder.persistence.spdx; 7 | 8 | import org.junit.jupiter.api.Test; 9 | 10 | import java.io.ByteArrayOutputStream; 11 | import java.io.IOException; 12 | import java.util.Optional; 13 | 14 | import static org.assertj.core.api.Assertions.assertThat; 15 | 16 | @FunctionalInterface 17 | interface DocumentModification { 18 | void invoke(TagValueDocument doc) throws IOException; 19 | } 20 | 21 | class TagValueDocumentTest { 22 | public static final String COMMENT = "My comment"; 23 | private static final String TAG = "Tag"; 24 | private static final String VALUE = "Value"; 25 | private static final String MULTI_VALUE = "Line1\nLine2"; 26 | private static final String TEMPLATE = "%s: %s\n"; 27 | private static final String MULTI_LINE_TEMPLATE = "%s: %s\n"; 28 | 29 | private static void assertOutput(String expected, DocumentModification test) throws IOException { 30 | final var stream = new ByteArrayOutputStream(); 31 | try (final var doc = new TagValueDocument(stream)) { 32 | test.invoke(doc); 33 | } finally { 34 | assertThat(stream.toString()).isEqualTo(expected); 35 | } 36 | } 37 | 38 | @Test 39 | void writesEmptyLine() throws Exception { 40 | assertOutput("\n", TagValueDocument::addEmptyLine); 41 | } 42 | 43 | @Test 44 | void writesCommentLine() throws Exception { 45 | assertOutput("## " + COMMENT + "\n", tagValueDocument -> tagValueDocument.addComment(COMMENT)); 46 | } 47 | 48 | @Test 49 | void writesTagValue() throws Exception { 50 | assertOutput(String.format(TEMPLATE, TAG, VALUE), (doc) -> doc.addValue(TAG, VALUE)); 51 | } 52 | 53 | @Test 54 | void writesOptionalTagValue() throws Exception { 55 | assertOutput(String.format(TEMPLATE, TAG, VALUE), (doc) -> doc.addValue(TAG, Optional.of(VALUE))); 56 | } 57 | 58 | @Test 59 | void writesNoAssertion_EmptyOptionalTagValue() throws Exception { 60 | assertOutput(String.format(TEMPLATE, TAG, "NOASSERTION"), (doc) -> doc.addValue(TAG, Optional.empty())); 61 | } 62 | 63 | @Test 64 | void writeNoAssertion_NullTagValue() throws Exception { 65 | assertOutput(String.format(TEMPLATE, TAG, "NOASSERTION"), (doc) -> doc.addValue(TAG, null)); 66 | } 67 | 68 | @Test 69 | void writesNone_emptyStringValue() throws Exception { 70 | assertOutput(String.format(TEMPLATE, TAG, "NONE"), (doc) -> doc.addValue(TAG, "")); 71 | } 72 | 73 | @Test 74 | void writesOptionalTag() throws Exception { 75 | assertOutput(String.format(TEMPLATE, TAG, VALUE), (doc) -> doc.optionallyAddValue(TAG, Optional.of(VALUE))); 76 | } 77 | 78 | @Test 79 | void skipsOptionalTag() throws Exception { 80 | assertOutput("", (doc) -> doc.optionallyAddValue(TAG, Optional.empty())); 81 | } 82 | 83 | @Test 84 | void writesMultiLineTextValue() throws Exception { 85 | assertOutput(String.format(MULTI_LINE_TEMPLATE, TAG, MULTI_VALUE), (doc) -> doc.addValue(TAG, MULTI_VALUE)); 86 | } 87 | 88 | @Test 89 | void escapesMultiLineStartTag() throws Exception { 90 | assertOutput(String.format(MULTI_LINE_TEMPLATE, TAG, ""), (doc) -> doc.addValue(TAG, "")); 91 | } 92 | 93 | @Test 94 | void escapesMultiLineEndTag() throws Exception { 95 | assertOutput(String.format(MULTI_LINE_TEMPLATE, TAG, " \nX"), (doc) -> doc.addValue(TAG, "\nX")); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/test/java/com/philips/research/spdxbuilder/controller/OrtConfigurationTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2021, Koninklijke Philips N.V., https://www.philips.com 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | package com.philips.research.spdxbuilder.controller; 7 | 8 | import org.junit.jupiter.api.Test; 9 | 10 | import java.io.ByteArrayInputStream; 11 | import java.net.URI; 12 | 13 | import static org.assertj.core.api.Assertions.assertThat; 14 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 15 | 16 | class OrtConfigurationTest { 17 | @Test 18 | void readsMinimalConfiguration() { 19 | final var config = read("---\n" + 20 | "document:\n" + 21 | " title: Title"); 22 | 23 | assertThat(config.document).isNotNull(); 24 | assertThat(config.projects).isEmpty(); 25 | assertThat(config.curations).isEmpty(); 26 | } 27 | 28 | @Test 29 | void throws_malformedConfiguration() { 30 | assertThatThrownBy(() -> read("Not a valid file")) 31 | .isInstanceOf(IllegalArgumentException.class) 32 | .hasMessageContaining("format error"); 33 | } 34 | 35 | @Test 36 | void throws_emptyConfiguration() { 37 | assertThatThrownBy(() -> read("---\n")) 38 | .isInstanceOf(IllegalArgumentException.class) 39 | .hasMessageContaining("is empty"); 40 | } 41 | 42 | @Test 43 | void readsDocumentProperties() { 44 | final var config = read("---\n" 45 | + "document:\n" 46 | + " title: Title\n" 47 | + " key: Key\n" 48 | + " namespace: http://name/space\n" 49 | + " comment: Comment\n" 50 | + " organization: Organization\n" 51 | ); 52 | 53 | assertThat(config.document.title).isEqualTo("Title"); 54 | assertThat(config.document.key).isEqualTo("Key"); 55 | assertThat(config.document.namespace).isEqualTo(URI.create("http://name/space")); 56 | assertThat(config.document.comment).isEqualTo("Comment"); 57 | assertThat(config.document.organization).isEqualTo("Organization"); 58 | } 59 | 60 | @Test 61 | void readsProjectMappings() { 62 | final var config = read("---\n" 63 | + "projects:\n" 64 | + "- id: ProjectId\n" 65 | + " purl: pkg:/group/name\n" 66 | ); 67 | 68 | assertThat(config.projects).hasSize(1); 69 | final var project = config.projects.get(0); 70 | assertThat(project.id).isEqualTo("ProjectId"); 71 | assertThat(project.purl).isEqualTo(URI.create("pkg:/group/name")); 72 | } 73 | 74 | @Test 75 | void readsCurations() { 76 | final var config = read("---\n" 77 | + "curations:\n" 78 | + "- purl: pkg:/group/name\n" 79 | + " source: https://example/source\n" 80 | + " license: License\n" 81 | ); 82 | 83 | assertThat(config.curations).hasSize(1); 84 | final var curation = config.curations.get(0); 85 | assertThat(curation.purl).isEqualTo(URI.create("pkg:/group/name")); 86 | assertThat(curation.source).isEqualTo(URI.create("https://example/source")); 87 | assertThat(curation.license).isEqualTo("License"); 88 | } 89 | 90 | @Test 91 | void providesSampleFormat() { 92 | final var sample = OrtConfiguration.example(); 93 | 94 | assertThat(sample).contains("Document").contains("identifier").contains("License"); 95 | } 96 | 97 | private OrtConfiguration read(String string) { 98 | return OrtConfiguration.parse(new ByteArrayInputStream(string.getBytes())); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/main/java/com/philips/research/spdxbuilder/controller/AbstractCommand.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2021, Koninklijke Philips N.V., https://www.philips.com 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | package com.philips.research.spdxbuilder.controller; 7 | 8 | import com.philips.research.spdxbuilder.core.ConversionService; 9 | import com.philips.research.spdxbuilder.persistence.tree.TreeWriter; 10 | import picocli.CommandLine.Option; 11 | import pl.tlinkowski.annotation.basic.NullOr; 12 | 13 | import java.io.File; 14 | import java.io.FileOutputStream; 15 | import java.io.IOException; 16 | import java.net.URI; 17 | 18 | /** 19 | * Shared generic part of CLI commands. 20 | */ 21 | public abstract class AbstractCommand implements Runnable { 22 | @Option(names = {"--version", "-V"}, description = "Show version info and exit") 23 | boolean showVersion; 24 | 25 | @Option(names = {"--help", "-H"}, usageHelp = true, description = "Show this message and exit") 26 | @SuppressWarnings("unused") 27 | boolean showUsage; 28 | 29 | @SuppressWarnings("NotNullFieldNotInitialized") 30 | @Option(names = {"--output", "-o"}, description = "Output SPDX tag-value file", paramLabel = "FILE", defaultValue = "bom.spdx") 31 | File spdxFile; 32 | 33 | @NullOr FileOutputStream spdxStream; 34 | 35 | @Option(names = {"--tree"}, description = "Print dependency tree") 36 | boolean printTree; 37 | 38 | @Option(names = {"--upload"}, description = "Upload SPDX file", paramLabel = "SERVER_URL") 39 | @NullOr URI uploadUrl; 40 | 41 | @Option(names = {"--force"}, description = "Create output if metadata is incomplete") 42 | boolean forceContinue; 43 | 44 | /** 45 | * @return instantiated service for the provided parameters and options 46 | */ 47 | abstract protected ConversionService createService(); 48 | 49 | @Override 50 | public void run() { 51 | showBanner(); 52 | 53 | if (showVersion) { 54 | final var app = getClass().getPackage().getImplementationTitle(); 55 | final var version = getClass().getPackage().getImplementationVersion(); 56 | System.out.println(app + ", Version " + version); 57 | System.exit(0); 58 | } 59 | 60 | String filePathName = spdxFile.getPath() + (spdxFile.getName().contains(".") ? "" : ".spdx"); 61 | 62 | try { 63 | spdxFile = new File(filePathName); 64 | System.out.println("Writing SBOM to '" + spdxFile.getName() + "'"); 65 | spdxStream = new FileOutputStream(spdxFile); 66 | 67 | final var service = createService(); 68 | service.read(); 69 | if (printTree) { 70 | service.apply(new TreeWriter()); 71 | } 72 | service.convert(forceContinue); 73 | 74 | if (uploadUrl != null) { 75 | System.out.println("Uploading '" + spdxFile.getName() + "' to " + uploadUrl); 76 | new UploadClient(uploadUrl).upload(spdxFile); 77 | } 78 | 79 | } catch (Exception e) { 80 | e.printStackTrace(); 81 | System.exit(1); 82 | } finally { 83 | try { 84 | if (spdxStream != null) { 85 | spdxStream.close(); 86 | System.exit(0); 87 | } 88 | } catch (IOException e) { 89 | e.printStackTrace(); 90 | System.exit(1); 91 | } 92 | } 93 | } 94 | 95 | private void showBanner() { 96 | System.out.println(" ___ ___ _____ __ ___ _ _ _ "); 97 | System.out.println("/ __| _ \\ \\ \\/ /__| _ )_ _(_) |__| |___ _ _ "); 98 | System.out.println("\\__ \\ _/ |) > <___| _ \\ || | | / _` / -_) '_|"); 99 | System.out.println("|___/_| |___/_/\\_\\ |___/\\_,_|_|_\\__,_\\___|_|"); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/test/java/com/philips/research/spdxbuilder/persistence/bom_base/BomBaseApiTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2021, Koninklijke Philips N.V., https://www.philips.com 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | package com.philips.research.spdxbuilder.persistence.bom_base; 7 | 8 | import com.philips.research.spdxbuilder.persistence.bom_base.BomBaseApi.PackageJson; 9 | import org.junit.jupiter.api.Nested; 10 | import org.junit.jupiter.api.Test; 11 | 12 | import java.net.URI; 13 | import java.net.URL; 14 | import java.util.List; 15 | 16 | import static org.assertj.core.api.Assertions.assertThat; 17 | 18 | class BomBaseApiTest { 19 | @Nested 20 | class AttributeConversion { 21 | public static final String HOMEPAGE_URL = "https://example.com/home"; 22 | public static final String DOWNLOAD_URI = "http:example.com/download"; 23 | public static final String SOURCE_URI = "http://example.com/sources"; 24 | private static final String TITLE = "title"; 25 | private static final String DESCRIPTION = "description"; 26 | private static final String HOMEPAGE = "home_page"; 27 | private static final String SUPPLIER = "supplier"; 28 | private static final String ORIGINATOR = "originator"; 29 | private static final String ATTRIBUTION = "attribution"; 30 | private static final String DOWNLOAD_LOCATION = "download_location"; 31 | private static final String SHA1 = "sha1"; 32 | private static final String SHA256 = "sha256"; 33 | private static final String SOURCE_LOCATION = "source_location"; 34 | private static final String DECLARED_LICENSE = "declared_license"; 35 | private static final String DETECTED_LICENSES = "detected_licenses"; 36 | private static final String DETECTED_LICENSE1 = "license1"; 37 | private static final String DETECTED_LICENSE2 = "license2"; 38 | 39 | final PackageJson meta = new PackageJson(); 40 | 41 | @Test 42 | void extractsAttributeValues() throws Exception { 43 | meta.attributes.put(TITLE, TITLE); 44 | meta.attributes.put(DESCRIPTION, DESCRIPTION); 45 | meta.attributes.put(HOMEPAGE, HOMEPAGE_URL); 46 | meta.attributes.put(ATTRIBUTION, ATTRIBUTION); 47 | meta.attributes.put(SUPPLIER, SUPPLIER); 48 | meta.attributes.put(ORIGINATOR, ORIGINATOR); 49 | meta.attributes.put(DOWNLOAD_LOCATION, DOWNLOAD_URI); 50 | meta.attributes.put(SHA1, SHA1); 51 | meta.attributes.put(SHA256, SHA256); 52 | meta.attributes.put(SOURCE_LOCATION, SOURCE_URI); 53 | meta.attributes.put(DECLARED_LICENSE, DECLARED_LICENSE); 54 | meta.attributes.put(DETECTED_LICENSES, List.of(DETECTED_LICENSE1, DETECTED_LICENSE2)); 55 | 56 | assertThat(meta.getTitle()).contains(TITLE); 57 | assertThat(meta.getDescription()).contains(DESCRIPTION); 58 | assertThat(meta.getHomePage()).contains(new URL(HOMEPAGE_URL)); 59 | assertThat(meta.getAttribution()).contains(ATTRIBUTION); 60 | assertThat(meta.getSupplier()).contains(SUPPLIER); 61 | assertThat(meta.getOriginator()).contains(ORIGINATOR); 62 | assertThat(meta.getDownloadLocation()).contains(URI.create(DOWNLOAD_URI)); 63 | assertThat(meta.getSha1()).contains(SHA1); 64 | assertThat(meta.getSha256()).contains(SHA256); 65 | assertThat(meta.getSourceLocation()).contains(URI.create(SOURCE_URI)); 66 | assertThat(meta.getDeclaredLicense()).contains(DECLARED_LICENSE); 67 | assertThat(meta.getDetectedLicenses()).containsExactly(DETECTED_LICENSE1, DETECTED_LICENSE2); 68 | } 69 | 70 | @Test 71 | void ignoresMalformedValue() { 72 | meta.attributes.put(TITLE, 123); 73 | 74 | assertThat(meta.getTitle()).isEmpty(); 75 | } 76 | 77 | @Test 78 | void ignoresInvalidURL() { 79 | meta.attributes.put(HOMEPAGE, "ssh:/not/a/homepage"); 80 | 81 | assertThat(meta.getHomePage()).isEmpty(); 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/main/java/com/philips/research/spdxbuilder/persistence/license_scanner/LicenseScannerClient.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2021, Koninklijke Philips N.V., https://www.philips.com 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | package com.philips.research.spdxbuilder.persistence.license_scanner; 7 | 8 | import com.fasterxml.jackson.annotation.JsonAutoDetect; 9 | import com.fasterxml.jackson.annotation.PropertyAccessor; 10 | import com.fasterxml.jackson.core.JsonProcessingException; 11 | import com.fasterxml.jackson.databind.DeserializationFeature; 12 | import com.fasterxml.jackson.databind.ObjectMapper; 13 | import com.fasterxml.jackson.databind.PropertyNamingStrategies; 14 | import com.github.packageurl.PackageURL; 15 | import com.philips.research.spdxbuilder.persistence.license_scanner.LicenseScannerApi.ContestJson; 16 | import com.philips.research.spdxbuilder.persistence.license_scanner.LicenseScannerApi.RequestJson; 17 | import pl.tlinkowski.annotation.basic.NullOr; 18 | import retrofit2.Call; 19 | import retrofit2.Retrofit; 20 | import retrofit2.converter.jackson.JacksonConverterFactory; 21 | 22 | import java.io.IOException; 23 | import java.net.URI; 24 | import java.net.URLEncoder; 25 | import java.nio.charset.StandardCharsets; 26 | import java.util.Optional; 27 | 28 | /** 29 | * REST client for the License Scanner Service. 30 | * 31 | * @see License Scanner Service 32 | */ 33 | public class LicenseScannerClient { 34 | private static final ObjectMapper MAPPER = new ObjectMapper() 35 | .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) 36 | .setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.NON_PRIVATE) 37 | .setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE); 38 | 39 | private final URI licenseServer; 40 | private final LicenseScannerApi rest; 41 | 42 | public LicenseScannerClient(URI licenseServer) { 43 | this.licenseServer = licenseServer; 44 | final var retrofit = new Retrofit.Builder() 45 | .baseUrl(licenseServer.toASCIIString()) 46 | .addConverterFactory(JacksonConverterFactory.create(MAPPER)) 47 | .build(); 48 | rest = retrofit.create(LicenseScannerApi.class); 49 | } 50 | 51 | /** 52 | * Queries the licenses for a single package. 53 | * 54 | * @return detected license for the package 55 | */ 56 | public Optional scanLicense(PackageURL purl, @NullOr URI location) { 57 | final var body = new RequestJson(purl.canonicalize(), location); 58 | 59 | return query(rest.scan(body)) 60 | .filter(r -> r.license != null) 61 | .map(r -> new LicenseInfo(r.license, r.confirmed)); 62 | } 63 | 64 | public void contest(PackageURL purl, String license) { 65 | final var scanId = URLEncoder.encode(purl.canonicalize(), StandardCharsets.UTF_8); 66 | query(rest.contest(scanId, new ContestJson(license))); 67 | } 68 | 69 | private Optional query(Call query) { 70 | try { 71 | final var response = query.execute(); 72 | if (!response.isSuccessful()) { 73 | throw new LicenseScannerException("License scanner responded with status " + response.code()); 74 | } 75 | return Optional.ofNullable(response.body()); 76 | } catch (JsonProcessingException e) { 77 | throw new IllegalArgumentException("JSON formatting error", e); 78 | } catch (IOException e) { 79 | throw new LicenseScannerException("The license scanner is not reachable at " + licenseServer); 80 | } 81 | } 82 | 83 | static class LicenseInfo { 84 | private final String license; 85 | private final boolean confirmed; 86 | 87 | public LicenseInfo(String license, boolean confirmed) { 88 | this.license = license; 89 | this.confirmed = confirmed; 90 | } 91 | 92 | public String getLicense() { 93 | return license; 94 | } 95 | 96 | public boolean isConfirmed() { 97 | return confirmed; 98 | } 99 | } 100 | } 101 | 102 | -------------------------------------------------------------------------------- /src/main/java/com/philips/research/spdxbuilder/controller/OrtCommand.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2021, Koninklijke Philips N.V., https://www.philips.com 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | package com.philips.research.spdxbuilder.controller; 7 | 8 | import com.philips.research.spdxbuilder.core.BomProcessor; 9 | import com.philips.research.spdxbuilder.core.BusinessException; 10 | import com.philips.research.spdxbuilder.core.ConversionService; 11 | import com.philips.research.spdxbuilder.core.domain.ConversionInteractor; 12 | import com.philips.research.spdxbuilder.persistence.license_scanner.LicenseKnowledgeBase; 13 | import com.philips.research.spdxbuilder.persistence.ort.OrtReader; 14 | import com.philips.research.spdxbuilder.persistence.spdx.SpdxWriter; 15 | import picocli.CommandLine.Command; 16 | import picocli.CommandLine.Option; 17 | import picocli.CommandLine.Parameters; 18 | import pl.tlinkowski.annotation.basic.NullOr; 19 | 20 | import java.io.File; 21 | import java.io.FileInputStream; 22 | import java.io.IOException; 23 | import java.net.URI; 24 | 25 | /** 26 | * CLI command to generate an SPDX file from an ORT Analyzer YAML. 27 | */ 28 | @Command(name = "ort", description = "Converts the output of the OSS Review Toolkit Analyzer into a bill-of-materials.") 29 | public class OrtCommand extends AbstractCommand { 30 | @Parameters(index = "0", description = "ORT Analyzer YAML file to read", paramLabel = "FILE", defaultValue = "analyzer-result.yml") 31 | @SuppressWarnings("NotNullFieldNotInitialized") 32 | File ortFile; 33 | 34 | @Option(names = {"--config", "-c"}, description = "Configuration YAML file", paramLabel = "FILE", defaultValue = ".spdx-builder.yml") 35 | @SuppressWarnings("NotNullFieldNotInitialized") 36 | File configFile; 37 | 38 | @Option(names = {"--scanner"}, description = "Add licenses from license scanner service", paramLabel = "SERVER_URL") 39 | @NullOr URI licenseScanner; 40 | 41 | @Override 42 | protected ConversionService createService() { 43 | final OrtReader reader = new OrtReader(ortFile); 44 | final BomProcessor writer = new SpdxWriter(spdxStream); 45 | ConversionService service = licenseScanner != null 46 | ? new ConversionInteractor(reader, writer).setKnowledgeBase(new LicenseKnowledgeBase(licenseScanner)) 47 | : new ConversionInteractor(reader, writer); 48 | 49 | final var config = readConfiguration(); 50 | prepareReader(reader, config); 51 | prepareConversion(service, config); 52 | 53 | return service; 54 | } 55 | 56 | private OrtConfiguration readConfiguration() { 57 | try (final var stream = new FileInputStream(configFile)) { 58 | return OrtConfiguration.parse(stream); 59 | } catch (IOException e) { 60 | System.err.println("Configuration error: " + e.getMessage()); 61 | System.err.println("Supported YAML configuration file format is:"); 62 | System.err.println(OrtConfiguration.example()); 63 | 64 | throw new BusinessException("Failed to read configuration"); 65 | } 66 | } 67 | 68 | private void prepareReader(OrtReader reader, OrtConfiguration config) { 69 | config.projects.forEach(project -> { 70 | reader.defineProjectPackage(project.id, project.purl); 71 | if (project.excluded != null) { 72 | reader.excludeScopes(project.id, project.excluded); 73 | } 74 | }); 75 | } 76 | 77 | private void prepareConversion(ConversionService service, OrtConfiguration config) { 78 | service.setDocument(config.document.title, config.document.organization); 79 | service.setComment(config.document.comment); 80 | if (config.document.key != null) { 81 | service.setDocReference(config.document.key); 82 | } 83 | if (config.document.namespace != null) { 84 | service.setDocNamespace(config.document.namespace); 85 | } 86 | 87 | config.curations.forEach(curation -> { 88 | if (curation.license != null) { 89 | service.curatePackageLicense(curation.getPurl(), curation.license); 90 | } 91 | if (curation.source != null) { 92 | service.curatePackageSource(curation.getPurl(), curation.source); 93 | } 94 | }); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/test/java/com/philips/research/spdxbuilder/persistence/ort/OrtJsonTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2021, Koninklijke Philips N.V., https://www.philips.com 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | package com.philips.research.spdxbuilder.persistence.ort; 7 | 8 | import com.philips.research.spdxbuilder.core.domain.Package; 9 | import org.junit.jupiter.api.BeforeEach; 10 | import org.junit.jupiter.api.Nested; 11 | import org.junit.jupiter.api.Test; 12 | 13 | import java.net.URI; 14 | import java.util.List; 15 | 16 | import static org.assertj.core.api.Assertions.assertThat; 17 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 18 | 19 | class OrtJsonTest { 20 | private static final String TYPE = "Type"; 21 | private static final String NAMESPACE = "Namespace"; 22 | private static final String NAME = "Name"; 23 | private static final String VERSION = "Version"; 24 | private static final String FILENAME = "file.name"; 25 | private static final String VALID_URL = "http://example.com/path/to/" + FILENAME; 26 | 27 | @Nested 28 | class PackageJsonTest { 29 | private static final String HASH_VALUE = "abc123"; 30 | private final PackageJson pkg = new PackageJson(); 31 | 32 | @BeforeEach 33 | void beforeEach() { 34 | pkg.id = String.join(":", List.of(TYPE, NAMESPACE, NAME, VERSION)); 35 | } 36 | 37 | @Test 38 | void createsPackage() { 39 | final var result = pkg.createPackage(); 40 | 41 | assertThat(result.getNamespace()).isEqualTo(NAMESPACE); 42 | assertThat(result.getName()).isEqualTo(NAME); 43 | assertThat(result.getVersion()).isEqualTo(VERSION); 44 | } 45 | 46 | @Test 47 | void throws_invalidPackageUrlFormat() { 48 | pkg.purl = URI.create("Not_a_valid_PURL"); 49 | 50 | assertThatThrownBy(pkg::createPackage) 51 | .isInstanceOf(IllegalArgumentException.class) 52 | .hasMessageContaining("not a valid Package URL"); 53 | } 54 | 55 | @Test 56 | void extractsSourceLocation() { 57 | pkg.sourceArtifact = new LocationJson(); 58 | pkg.sourceArtifact.url = URI.create(VALID_URL); 59 | 60 | final var result = pkg.createPackage(); 61 | 62 | assertThat(result.getSourceLocation()).contains(URI.create(VALID_URL)); 63 | } 64 | 65 | @Test 66 | void addsBinaryFileHash() { 67 | var locationJson = new LocationJson(); 68 | var hashJson = new HashJson(); 69 | hashJson.algorithm = "SHA-1"; 70 | hashJson.value = HASH_VALUE; 71 | locationJson.hash = hashJson; 72 | locationJson.url = URI.create(VALID_URL); 73 | pkg.binaryArtifact = locationJson; 74 | 75 | final var result = pkg.createPackage(); 76 | 77 | assertThat(result.getFilename()).contains(FILENAME); 78 | assertThat(result.getHashes()).containsEntry("SHA1", HASH_VALUE); 79 | } 80 | } 81 | 82 | @Nested 83 | class VcsJsonTest { 84 | final Package result = new Package(NAMESPACE, NAME, VERSION); 85 | 86 | @Test 87 | void noLocation_emptyUrlField() { 88 | final var json = new VcsJson(); 89 | 90 | json.addSourceLocation(result); 91 | 92 | assertThat(result.getSourceLocation()).isEmpty(); 93 | } 94 | 95 | @Test 96 | void vcsLocationWithRevisionAndPath() { 97 | final var json = new VcsJson(); 98 | json.type = "Git"; 99 | json.url = VALID_URL; 100 | json.path = "the?path"; 101 | json.revision = "the?revision"; 102 | 103 | json.addSourceLocation(result); 104 | 105 | assertThat(result.getSourceLocation()).contains(URI.create("git+" + VALID_URL + "@the%3Frevision#the%3Fpath")); 106 | } 107 | 108 | @Test 109 | void vcsLocationWithProvidedVersion() { 110 | final var json = new VcsJson(); 111 | json.url = VALID_URL; 112 | 113 | json.addSourceLocation(result); 114 | 115 | assertThat(result.getSourceLocation()).contains(URI.create(VALID_URL + "@" + VERSION)); 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/main/java/com/philips/research/spdxbuilder/persistence/bom_base/BomBaseApi.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2021, Koninklijke Philips N.V., https://www.philips.com 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | package com.philips.research.spdxbuilder.persistence.bom_base; 7 | 8 | import pl.tlinkowski.annotation.basic.NullOr; 9 | import retrofit2.Call; 10 | import retrofit2.http.GET; 11 | import retrofit2.http.Path; 12 | 13 | import java.net.MalformedURLException; 14 | import java.net.URI; 15 | import java.net.URL; 16 | import java.util.HashMap; 17 | import java.util.List; 18 | import java.util.Map; 19 | import java.util.Optional; 20 | import java.util.function.Function; 21 | 22 | public interface BomBaseApi { 23 | @GET("/packages/{purl}") 24 | Call getPackage(@Path("purl") String purl); 25 | 26 | class PackageJson implements PackageMetadata { 27 | Map attributes = new HashMap<>(); 28 | 29 | @Override 30 | public Optional getTitle() { 31 | return getStringAttribute("title"); 32 | } 33 | 34 | @Override 35 | public Optional getDescription() { 36 | return getStringAttribute("description"); 37 | } 38 | 39 | @Override 40 | public Optional getHomePage() { 41 | return getUrlAttribute("home_page"); 42 | } 43 | 44 | @Override 45 | public Optional getAttribution() { 46 | return getStringAttribute("attribution"); 47 | } 48 | 49 | @Override 50 | public Optional getSupplier() { 51 | return getStringAttribute("supplier"); 52 | } 53 | 54 | @Override 55 | public Optional getOriginator() { 56 | return getStringAttribute("originator"); 57 | } 58 | 59 | @Override 60 | public Optional getDownloadLocation() { 61 | return getUriAttribute("download_location"); 62 | } 63 | 64 | @Override 65 | public Optional getSha1() { 66 | return getStringAttribute("sha1"); 67 | } 68 | 69 | @Override 70 | public Optional getSha256() { 71 | return getStringAttribute("sha256"); 72 | } 73 | 74 | @Override 75 | public Optional getSourceLocation() { 76 | return getUriAttribute("source_location"); 77 | } 78 | 79 | @Override 80 | public Optional getDeclaredLicense() { 81 | return getStringAttribute("declared_license"); 82 | } 83 | 84 | @Override 85 | public List getDetectedLicenses() { 86 | return getStringListAttribute("detected_licenses"); 87 | } 88 | 89 | private Optional getStringAttribute(String tag) { 90 | return getAttribute(tag, str -> (String) str); 91 | } 92 | 93 | private List getStringListAttribute(String tag) { 94 | //noinspection unchecked 95 | return getAttribute(tag, list -> (List) list) 96 | .orElse(List.of()); 97 | } 98 | 99 | private Optional getUriAttribute(String tag) { 100 | return getAttribute(tag, str -> URI.create((String) str)); 101 | } 102 | 103 | private Optional getUrlAttribute(String tag) { 104 | return getAttribute(tag, str -> { 105 | try { 106 | return URI.create((String) str).toURL(); 107 | } catch (MalformedURLException e) { 108 | throw new IllegalArgumentException(e.getMessage()); 109 | } 110 | }); 111 | } 112 | 113 | private Optional getAttribute(String tag, Function converter) { 114 | final @NullOr Object value = attributes.get(tag); 115 | try { 116 | if (value == null) { 117 | return Optional.empty(); 118 | } 119 | return Optional.of(converter.apply(value)); 120 | } catch (Exception e) { 121 | System.err.println("WARNING: Attribute " + tag + " value '" + value + "' has an incompatible format"); 122 | return Optional.empty(); 123 | } 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | # Sequence of patterns matched against refs/tags 4 | tags: 5 | - '*' # Push events to matching all 6 | 7 | name: Upload Release Asset 8 | 9 | jobs: 10 | build: 11 | name: Upload Release Asset 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: Checkout code 16 | uses: actions/checkout@v4 17 | - uses: actions/setup-java@v4 18 | with: 19 | java-version: '11.0.1' 20 | distribution: 'zulu' 21 | - name: Build project 22 | run: ./gradlew build -x test 23 | - name: Create Release 24 | id: create_release 25 | uses: actions/create-release@v1 26 | env: 27 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 28 | with: 29 | tag_name: ${{ github.ref }} 30 | release_name: Release ${{ github.ref }} 31 | draft: false 32 | prerelease: false 33 | - name: Copy jar 34 | run: cp ./build/libs/spdx-builder*.jar ./build/libs/spdx-builder.jar 35 | - name: Upload Release Asset 36 | id: upload-release-asset 37 | uses: actions/upload-release-asset@v1 38 | env: 39 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 40 | with: 41 | upload_url: ${{ steps.create_release.outputs.upload_url }} 42 | asset_path: ./build/libs/spdx-builder.jar 43 | asset_name: spdx-builder.jar 44 | asset_content_type: application/java-archive 45 | 46 | - name: Install cosign 47 | uses: sigstore/cosign-installer@v3.3.0 48 | with: 49 | cosign-release: 'v1.13.1' 50 | 51 | - name: Sign release 52 | run: | 53 | echo '${{ secrets.COSIGN_PRIVATE_KEY }}' > cosign.key 54 | cosign sign-blob --key cosign.key --output-signature "${SIGNATURE}" ./build/libs/spdx-builder.jar 55 | cat "${SIGNATURE}" 56 | curl_args=(-s -H "Authorization: token ${GITHUB_TOKEN}") 57 | curl_args+=(-H "Accept: application/vnd.github.v3+json") 58 | release_id="$(curl "${curl_args[@]}" "${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/releases?per_page=10" | jq "map(select(.tag_name == \"${GITHUB_REF_NAME}\"))" | jq -r '.[0].id')" 59 | echo "Upload ${SIGNATURE} to release with id ${release_id}…" 60 | curl_args+=(-H "Content-Type: $(file -b --mime-type "${SIGNATURE}")") 61 | curl "${curl_args[@]}" \ 62 | --data-binary @"${SIGNATURE}" \ 63 | "https://uploads.github.com/repos/${GITHUB_REPOSITORY}/releases/${release_id}/assets?name=${SIGNATURE}" 64 | env: 65 | GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" 66 | COSIGN_PASSWORD: "${{ secrets.COSIGN_PASSWORD }}" 67 | SIGNATURE: spdx-builder.jar.sig 68 | 69 | provenance: 70 | name: Generate provenance 71 | runs-on: ubuntu-20.04 72 | needs: [build] 73 | if: startsWith(github.ref, 'refs/tags/') 74 | 75 | steps: 76 | - name: Install cosign 77 | uses: sigstore/cosign-installer@v3.3.0 78 | with: 79 | cosign-release: 'v1.13.1' 80 | 81 | - name: Generate provenance for Release 82 | uses: philips-labs/slsa-provenance-action@v0.9.0 83 | with: 84 | command: generate 85 | subcommand: github-release 86 | arguments: --artifact-path release-assets --output-path provenance.att --tag-name ${{ github.ref_name }} 87 | env: 88 | GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" 89 | 90 | - name: Sign provenance 91 | run: | 92 | echo '${{ secrets.COSIGN_PRIVATE_KEY }}' > cosign.key 93 | cosign sign-blob --key cosign.key --output-signature "${SIGNATURE}" provenance.att 94 | cat "${SIGNATURE}" 95 | curl_args=(-s -H "Authorization: token ${GITHUB_TOKEN}") 96 | curl_args+=(-H "Accept: application/vnd.github.v3+json") 97 | release_id="$(curl "${curl_args[@]}" "${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/releases?per_page=10" | jq "map(select(.tag_name == \"${GITHUB_REF_NAME}\"))" | jq -r '.[0].id')" 98 | echo "Upload ${SIGNATURE} to release with id ${release_id}…" 99 | curl_args+=(-H "Content-Type: $(file -b --mime-type "${SIGNATURE}")") 100 | curl "${curl_args[@]}" \ 101 | --data-binary @"${SIGNATURE}" \ 102 | "https://uploads.github.com/repos/${GITHUB_REPOSITORY}/releases/${release_id}/assets?name=${SIGNATURE}" 103 | env: 104 | GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" 105 | COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }} 106 | SIGNATURE: provenance.att.sig 107 | -------------------------------------------------------------------------------- /src/main/java/com/philips/research/spdxbuilder/persistence/tree/TreeWriter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2021, Koninklijke Philips N.V., https://www.philips.com 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | package com.philips.research.spdxbuilder.persistence.tree; 7 | 8 | import com.github.packageurl.MalformedPackageURLException; 9 | import com.github.packageurl.PackageURL; 10 | import com.philips.research.spdxbuilder.core.BomProcessor; 11 | import com.philips.research.spdxbuilder.core.domain.BillOfMaterials; 12 | import com.philips.research.spdxbuilder.core.domain.Package; 13 | import com.philips.research.spdxbuilder.core.domain.Relation; 14 | 15 | import java.io.IOException; 16 | import java.util.*; 17 | 18 | /** 19 | * Writes the packages of a bill-of-materials as a tree to the console. 20 | */ 21 | public class TreeWriter implements BomProcessor { 22 | private static final String SNIP = "-".repeat(10) + "8<" + "-".repeat(10); 23 | 24 | private final Map> nodes = new HashMap<>(); 25 | private final List roots = new ArrayList<>(); 26 | private final Set done = new HashSet<>(); 27 | private final TreeFormatter formatter; 28 | 29 | public TreeWriter() { 30 | this(new TreeFormatter()); 31 | } 32 | 33 | TreeWriter(TreeFormatter formatter) { 34 | this.formatter = formatter; 35 | } 36 | 37 | @Override 38 | public void process(BillOfMaterials bom) { 39 | buildNodes(bom); 40 | 41 | System.out.println("TREE start " + SNIP); 42 | roots.forEach(pkg -> { 43 | System.out.println(formatter.node(name(pkg))); 44 | writeRelationsOf(pkg); 45 | }); 46 | System.out.println("TREE end " + SNIP); 47 | } 48 | 49 | private void buildNodes(BillOfMaterials bom) { 50 | nodes.clear(); 51 | bom.getPackages().forEach(pkg -> nodes.put(pkg, new ArrayList<>())); 52 | 53 | roots.clear(); 54 | roots.addAll(nodes.keySet()); 55 | roots.sort(Comparator.comparing(this::name)); 56 | 57 | bom.getRelations().forEach(rel -> { 58 | roots.remove(rel.getTo()); 59 | nodes.get(rel.getFrom()).add(rel); 60 | }); 61 | nodes.values().forEach(list -> list.sort(Comparator.comparing(rel -> name(rel.getTo())))); 62 | 63 | done.clear(); 64 | done.addAll(roots); 65 | } 66 | 67 | private void writeRelationsOf(Package pkg) { 68 | done.add(pkg); 69 | final var relations = nodes.get(pkg); 70 | if (!relations.isEmpty()) { 71 | formatter.indent(); 72 | relations.forEach(this::writeRelation); 73 | formatter.unindent(); 74 | } 75 | } 76 | 77 | private void writeRelation(Relation relation) { 78 | Package pkg = relation.getTo(); 79 | final var name = name(pkg); 80 | final var type = type(relation); 81 | if (!done.contains(pkg)) { 82 | System.out.println(formatter.node(name + type)); 83 | writeRelationsOf(pkg); 84 | } else { 85 | final var omitted = nodes.get(pkg).isEmpty() ? "" : " (*)"; 86 | System.out.println(formatter.node(name + type + omitted)); 87 | } 88 | } 89 | 90 | private String type(Relation relation) { 91 | switch (relation.getType()) { 92 | case DESCENDANT_OF: 93 | return " [derived]"; 94 | case DYNAMICALLY_LINKS: 95 | return " [dynamic]"; 96 | case STATICALLY_LINKS: 97 | return " [static]"; 98 | case CONTAINS: 99 | return " [contained]"; 100 | case DEVELOPED_USING: 101 | return " [dev]"; 102 | case DEPENDS_ON: 103 | return ""; 104 | default: 105 | System.err.println("WARNING: Unmapped tree relation type:" + relation.getType()); 106 | return ""; 107 | } 108 | } 109 | 110 | private String name(Package pkg) { 111 | return pkg.getPurl().orElseGet(() -> { 112 | try { 113 | return new PackageURL("generic", pkg.getNamespace(), pkg.getName(), pkg.getVersion(), null, null); 114 | } catch (MalformedPackageURLException e) { 115 | throw new IllegalArgumentException("Failed to create generic package URL for " + pkg); 116 | } 117 | }).toString(); 118 | } 119 | 120 | @Override 121 | public void close() throws IOException { 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/test/java/com/philips/research/spdxbuilder/persistence/tree/TreeReaderTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2021, Koninklijke Philips N.V., https://www.philips.com 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | package com.philips.research.spdxbuilder.persistence.tree; 7 | 8 | import com.philips.research.spdxbuilder.core.domain.BillOfMaterials; 9 | import com.philips.research.spdxbuilder.core.domain.Package; 10 | import com.philips.research.spdxbuilder.core.domain.Relation; 11 | import org.jetbrains.annotations.NotNull; 12 | import org.junit.jupiter.api.Nested; 13 | import org.junit.jupiter.api.Test; 14 | 15 | import java.io.ByteArrayInputStream; 16 | import java.io.IOException; 17 | import java.io.InputStream; 18 | import java.nio.file.Path; 19 | import java.util.List; 20 | 21 | import static org.assertj.core.api.Assertions.assertThat; 22 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 23 | import static org.mockito.Mockito.mock; 24 | import static org.mockito.Mockito.when; 25 | 26 | class TreeReaderTest { 27 | private final BillOfMaterials bom = new BillOfMaterials(); 28 | 29 | @Test 30 | void readsTreeFromStream() { 31 | final var stream = stream( 32 | "ns/main@1", 33 | "+-> ns/sub@2"); 34 | 35 | new TreeReader(stream, "custom", Path.of("src", "test", "resources", "custom_formats.yml").toFile(), List.of()) 36 | .read(bom); 37 | 38 | assertThat(bom.getPackages()).containsExactly( 39 | new Package("ns", "main", "1"), 40 | new Package("ns", "sub", "2")); 41 | } 42 | 43 | @Test 44 | void switchesFormats() { 45 | final var stream = stream( 46 | "ns/main@1", 47 | "### rust", 48 | "├── sub v2"); 49 | 50 | new TreeReader(stream, "npm", null, List.of()).read(bom); 51 | 52 | assertThat(bom.getPackages()).containsExactly( 53 | new Package("ns", "main", "1"), 54 | new Package("", "sub", "2")); 55 | final var pkg1 = bom.getPackages().get(0); 56 | final var pkg2 = bom.getPackages().get(1); 57 | assertThat(bom.getRelations()).containsExactly( 58 | new Relation(pkg1, pkg2, Relation.Type.DYNAMICALLY_LINKS)); 59 | } 60 | 61 | @Test 62 | void throws_streamFailure() throws Exception { 63 | final var stream = mock(InputStream.class); 64 | when(stream.available()).thenThrow(new IOException("Failing stream")); 65 | final var reader = new TreeReader(stream, "maven", null, List.of()); 66 | 67 | assertThatThrownBy(() -> reader.read(bom)) 68 | .isInstanceOf(TreeException.class) 69 | .hasMessageContaining("read the tree data"); 70 | } 71 | 72 | @Test 73 | void throws_parsingFailure() { 74 | final var stream = stream("Not a valid package"); 75 | final var reader = new TreeReader(stream, "npm", null, List.of()); 76 | 77 | assertThatThrownBy(() -> reader.read(bom)) 78 | .isInstanceOf(TreeException.class) 79 | .hasMessageContaining("package identifier"); 80 | } 81 | 82 | @NotNull 83 | private InputStream stream(String... lines) { 84 | return new ByteArrayInputStream(String.join("\n", lines).getBytes()); 85 | } 86 | 87 | @Nested 88 | class InternalPackages { 89 | @Test 90 | void defaultsToInternalPackagesAtRoot() { 91 | final var stream = stream("ns/main@1"); 92 | 93 | new TreeReader(stream, "npm", null, List.of()).read(bom); 94 | 95 | assertThat(bom.getPackages().get(0).isInternal()).isTrue(); 96 | } 97 | 98 | @Test 99 | void flagsProductReleaseOutput() { 100 | final var stream = stream("ns/main@1"); 101 | 102 | new TreeReader(stream, "npm", null, List.of()) 103 | .setRelease(true) 104 | .read(bom); 105 | 106 | assertThat(bom.getPackages().get(0).isInternal()).isFalse(); 107 | } 108 | 109 | @Test 110 | void passesInternalGlobPatterns() { 111 | final var stream = stream("ns/release@1", " ns/internal@2"); 112 | 113 | new TreeReader(stream, "npm", null, List.of("*/int*")) 114 | .setRelease(true) 115 | .read(bom); 116 | 117 | assertThat(bom.getPackages().get(0).isInternal()).isFalse(); 118 | assertThat(bom.getPackages().get(1).isInternal()).isTrue(); 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/test/resources/ort_sample.yml: -------------------------------------------------------------------------------- 1 | # VCS information about the input directory. 2 | repository: 3 | vcs: 4 | type: "Git" 5 | url: "https://github.com/jshttp/mime-types.git" 6 | revision: "7c4ce23d7354fbf64c69d7b7be8413c4ba2add78" 7 | path: "" 8 | vcs_processed: 9 | type: "git" 10 | url: "https://github.com/jshttp/mime-types.git" 11 | revision: "7c4ce23d7354fbf64c69d7b7be8413c4ba2add78" 12 | path: "" 13 | config: 14 | excludes: 15 | paths: 16 | - pattern: "subdir/**" 17 | reason: "WHATEVER" 18 | comment: "Skip projects on excluded paths using glob" 19 | scopes: 20 | - pattern: "skip*" 21 | reason: "DEV_DEPENDENCY_OF" 22 | comment: "Skip excluded scopes using glob" 23 | # The analyzer result. 24 | analyzer: 25 | # The time when the analyzer was executed. 26 | start_time: "2019-02-19T10:03:07.269Z" 27 | end_time: "2019-02-19T10:03:19.932Z" 28 | # Information about the environment the analyzer was run in. 29 | environment: 30 | ort_version: "331c32d" 31 | os: "Linux" 32 | variables: 33 | SHELL: "/bin/bash" 34 | TERM: "xterm-256color" 35 | JAVA_HOME: "/usr/lib/jvm/java-8-oracle" 36 | tool_versions: { } 37 | # Configuration options of the analyzer. 38 | config: 39 | ignore_tool_versions: false 40 | allow_dynamic_versions: true 41 | # The result of the dependency analysis. 42 | result: 43 | # Metadata about all found projects, in this case only the mime-types package defined by the package.json file. 44 | projects: 45 | - id: "NPM::mime-types:2.1.18" 46 | purl: "pkg:npm/mime-types@2.1.18" 47 | definition_file_path: "package.json" 48 | declared_licenses: 49 | - "MIT" 50 | declared_licenses_processed: 51 | spdx_expression: "MIT" 52 | vcs: 53 | type: "" 54 | url: "https://github.com/jshttp/mime-types.git" 55 | revision: "" 56 | path: "" 57 | vcs_processed: 58 | type: "git" 59 | url: "https://github.com/jshttp/mime-types.git" 60 | revision: "076f7902e3a730970ea96cd0b9c09bb6110f1127" 61 | path: "" 62 | homepage_url: "" 63 | # The dependency trees by scope. 64 | scopes: 65 | - name: "dependencies" 66 | dependencies: 67 | - id: "NPM::dependency:1.0" 68 | - id: "NPM::static_dependency:2.0" 69 | linkage: "STATIC" 70 | - name: "skipMe" 71 | dependencies: 72 | - id: "NPM::skip:1.0" 73 | - id: "NPM::skip:2.0" 74 | dependencies: 75 | - id: "NPM::skip:2.1" 76 | # If an issue occurred during the dependency analysis of this package there would be an additional "issues" 77 | # array. 78 | - name: "testStuff" 79 | dependencies: 80 | - id: "NPM::skip:3.0" 81 | - id: "NPM::skipme:1.2.3" 82 | definition_file_path: "subdir/package.json" 83 | # ... 84 | # Detailed metadata about each package from the dependency trees. 85 | packages: 86 | - package: 87 | id: "NPM::dependency:1.0" 88 | purl: "pkg:npm/dependency@1.0" 89 | declared_licenses: 90 | - "ISC" 91 | declared_licenses_processed: 92 | spdx_expression: "ISC" 93 | description: "Some dependent module" 94 | homepage_url: "https://github.com/example" 95 | binary_artifact: 96 | url: "" 97 | hash: 98 | value: "" 99 | algorithm: "" 100 | source_artifact: 101 | url: "https://registry.npmjs.org/dependency/-/dependency-1.0.tgz" 102 | hash: 103 | value: "91b4792588a7738c25f35dd6f63752a2f8776135" 104 | algorithm: "SHA-1" 105 | - package: 106 | id: "NPM::static_dependency:2.0" 107 | - package: 108 | id: "NPM::skip:1.0" 109 | purl: "pkg:npm/skip@1.0" 110 | - package: 111 | id: "NPM::skip:2.0" 112 | purl: "pkg:npm/skip@2.0" 113 | - package: 114 | id: "NPM::skip:2.1" 115 | purl: "pkg:npm/skip@2.1" 116 | - package: 117 | id: "NPM::skip:3.0" 118 | purl: "pkg:npm/skip@3.0" 119 | # ... 120 | # Finally a list of project related issues that happened during dependency analysis. Fortunately empty in this case. 121 | issues: { } 122 | # A field to quickly check if the analyzer result contains any issues. 123 | has_issues: false 124 | -------------------------------------------------------------------------------- /src/test/java/com/philips/research/spdxbuilder/persistence/license_scanner/LicenseScannerClientTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2021, Koninklijke Philips N.V., https://www.philips.com 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | package com.philips.research.spdxbuilder.persistence.license_scanner; 7 | 8 | import com.fasterxml.jackson.databind.ObjectMapper; 9 | import com.github.packageurl.PackageURL; 10 | import com.philips.research.spdxbuilder.core.BusinessException; 11 | import okhttp3.mockwebserver.MockResponse; 12 | import okhttp3.mockwebserver.MockWebServer; 13 | import org.json.JSONObject; 14 | import org.junit.jupiter.api.AfterEach; 15 | import org.junit.jupiter.api.BeforeEach; 16 | import org.junit.jupiter.api.Test; 17 | 18 | import java.io.IOException; 19 | import java.net.URI; 20 | 21 | import static org.assertj.core.api.Assertions.assertThat; 22 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 23 | 24 | class LicenseScannerClientTest { 25 | private static final int PORT = 1080; 26 | private static final String PURL = "pkg:namespace/name@version"; 27 | private static final String SCAN_ID = "pkg%253Anamespace%252Fname%2540version"; 28 | private static final URI LOCATION = URI.create("http://example.com"); 29 | private static final String LICENSE = "Apache-2.0"; 30 | private static final ObjectMapper MAPPER = new ObjectMapper(); 31 | 32 | private final MockWebServer mockServer = new MockWebServer(); 33 | 34 | private final LicenseScannerClient client = new LicenseScannerClient(URI.create("http://localhost:" + PORT)); 35 | 36 | @BeforeEach 37 | void setUp() throws IOException { 38 | mockServer.start(PORT); 39 | } 40 | 41 | @AfterEach 42 | void tearDown() throws IOException { 43 | mockServer.shutdown(); 44 | } 45 | 46 | @Test 47 | @SuppressWarnings("OptionalGetWithoutIsPresent") 48 | void queriesLicense() throws Exception { 49 | mockServer.enqueue(new MockResponse().setBody( 50 | new JSONObject() 51 | .put("license", LICENSE) 52 | .put("confirmed", true).toString())); 53 | 54 | final var license = client.scanLicense(new PackageURL(PURL), LOCATION).get(); 55 | 56 | final var request = mockServer.takeRequest(); 57 | assertThat(request.getMethod()).isEqualTo("POST"); 58 | assertThat(request.getPath()).isEqualTo("/packages"); 59 | assertThat(MAPPER.readTree(request.getBody().readUtf8())) 60 | .isEqualTo(MAPPER.readTree(new JSONObject() 61 | .put("purl", PURL) 62 | .put("location", LOCATION).toString())); 63 | assertThat(license.getLicense()).contains(LICENSE); 64 | assertThat(license.isConfirmed()).isTrue(); 65 | } 66 | 67 | @Test 68 | void ignoresEmptyLicense() throws Exception { 69 | mockServer.enqueue(new MockResponse().setBody("{}")); 70 | 71 | final var license = client.scanLicense(new PackageURL(PURL), LOCATION); 72 | 73 | assertThat(license).isEmpty(); 74 | } 75 | 76 | @Test 77 | void contestsLicense() throws Exception { 78 | mockServer.enqueue(new MockResponse()); 79 | 80 | client.contest(new PackageURL(PURL), LICENSE); 81 | 82 | final var contestRequest = mockServer.takeRequest(); 83 | assertThat(contestRequest.getMethod()).isEqualTo("POST"); 84 | assertThat(contestRequest.getPath()).isEqualTo(String.format("/scans/%s/contest", SCAN_ID)); 85 | assertThat(contestRequest.getBody().readUtf8()).isEqualTo(new JSONObject().put("license", LICENSE).toString()); 86 | } 87 | 88 | @Test 89 | void throws_serverNotReachable() throws Exception { 90 | var serverlessClient = new LicenseScannerClient(URI.create("http://localhost:1234")); 91 | 92 | assertThatThrownBy(() -> serverlessClient.scanLicense(new PackageURL(PURL), LOCATION)) 93 | .isInstanceOf(BusinessException.class) 94 | .hasMessageContaining("not reachable"); 95 | } 96 | 97 | @Test 98 | void throws_unexpectedResponseFromServer() throws Exception { 99 | mockServer.enqueue(new MockResponse().setResponseCode(404)); 100 | 101 | assertThatThrownBy(() -> client.scanLicense(new PackageURL(PURL), LOCATION)) 102 | .isInstanceOf(BusinessException.class) 103 | .hasMessageContaining("status 404"); 104 | } 105 | 106 | @Test 107 | void throws_malformedResponseFromServer() throws Exception { 108 | mockServer.enqueue(new MockResponse().setBody("Not a JSON response")); 109 | 110 | assertThatThrownBy(() -> client.scanLicense(new PackageURL(PURL), LOCATION)) 111 | .isInstanceOf(IllegalArgumentException.class); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/test/java/com/philips/research/spdxbuilder/persistence/blackduck/PackageIdentifierTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2021, Koninklijke Philips N.V., https://www.philips.com 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | package com.philips.research.spdxbuilder.persistence.blackduck; 7 | 8 | import com.github.packageurl.MalformedPackageURLException; 9 | import com.github.packageurl.PackageURL; 10 | import org.junit.jupiter.api.Test; 11 | 12 | import static org.assertj.core.api.Assertions.assertThat; 13 | 14 | class PackageIdentifierTest { 15 | @Test 16 | void decodesFormats() { 17 | // (Formats are based on external namespaces list provided by Black Duck.) 18 | assertIdentifier("alpine", "name/version/architecture", "alpine/name@version"); 19 | // alt_linux 20 | // anaconda 21 | // android 22 | // android_sdk 23 | // apache_software 24 | assertIdentifier("arch_linux", "name/version/architecture", "arch/name@version"); 25 | // automotive_linux 26 | assertIdentifier("bitbucket", "org/name:version", "bitbucket/org/name@version"); 27 | assertIdentifier("bitbucket", "org/name", "bitbucket/org/name"); 28 | assertIdentifier("bower", "name/version", "bower/name@version"); 29 | assertIdentifier("bower", "@scope/name/version", "bower/%40scope/name@version"); 30 | assertIdentifier("centos", "name/version/architecture", "rpm/centos/name@version"); 31 | // clearlinux 32 | assertIdentifier("cocoapods", "name/version", "cocoapods/name@version"); 33 | // codeplex 34 | // codeplex_group 35 | assertIdentifier("conan", "name/version", "conan/name@version"); 36 | assertIdentifier("cpan", "name/version", "cpan/name@version"); 37 | // cpe 38 | assertIdentifier("cran", "name/version", "cran/name@version"); 39 | assertIdentifier("crates", "name/version", "cargo/name@version"); 40 | assertIdentifier("dart", "name/version", "pub/name@version"); 41 | assertIdentifier("debian", "name/version/architecture", "deb/name@version"); 42 | // eclipse 43 | // efisbot 44 | assertIdentifier("fedora", "name/version/architecture", "rpm/fedora/name@version"); 45 | // freedesktop_org 46 | // gitcafe 47 | assertIdentifier("github", "org/name:version", "github/org/name@version"); 48 | assertIdentifier("github", "org/name", "github/org/name"); 49 | // github_gist 50 | assertIdentifier("gitlab", "org/name:version", "gitlab/org/name@version"); 51 | assertIdentifier("gitlab", "org/name", "gitlab/org/name"); 52 | // gitorious 53 | // gnu 54 | assertIdentifier("golang", "name/version", "golang/name@version"); 55 | assertIdentifier("golang", "domain/name/version", "golang/domain/name@version"); 56 | // googlecode 57 | // hackage 58 | assertIdentifier("hex", "name/version", "hex/name@version"); 59 | assertIdentifier("hex", "namespace/name/version", "hex/namespace/name@version"); 60 | // java_net 61 | // kb_classic 62 | // launchpad 63 | // long_tail 64 | assertIdentifier("maven", "group:name:version", "maven/group/name@version"); 65 | // mongo_db 66 | assertIdentifier("npmjs", "name/version", "npm/name@version"); 67 | assertIdentifier("npmjs", "@scope/name/version", "npm/%40scope/name@version"); 68 | assertIdentifier("nuget", "name/version", "nuget/name@version"); 69 | // openembedded 70 | // openjdk 71 | assertIdentifier("opensuse", "name/version/architecture", "rpm/opensuse/name@version"); 72 | // oracle_linux 73 | // packagist 74 | // pear 75 | // photon 76 | // protocode_sc 77 | assertIdentifier("pypi", "name/version", "pypi/name@version"); 78 | // raspberry_pi 79 | assertIdentifier("redhat", "dist/name/version", "rpm/dist/name@version"); 80 | assertIdentifier("redhat", "name/version", "rpm/name@version"); 81 | // ros 82 | // rubyforge 83 | assertIdentifier("rubygems", "name/version", "gem/name@version"); 84 | // runtime 85 | // sourceforge 86 | // sourceforge_ip 87 | // tianocore 88 | assertIdentifier("ubuntu", "name/version/architecture", "deb/ubuntu/name@version"); 89 | // yokto 90 | } 91 | 92 | private void assertIdentifier(String namespace, String identifier, String expected) { 93 | try { 94 | final var id = new PackageIdentifier(namespace, identifier); 95 | assertThat(id.getPurl()).contains(new PackageURL("pkg:" + expected)); 96 | } catch (MalformedPackageURLException e) { 97 | throw new IllegalArgumentException(e); 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/main/java/com/philips/research/spdxbuilder/persistence/blackduck/PackageIdentifier.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2021, Koninklijke Philips N.V., https://www.philips.com 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | package com.philips.research.spdxbuilder.persistence.blackduck; 7 | 8 | import com.github.packageurl.MalformedPackageURLException; 9 | import com.github.packageurl.PackageURL; 10 | import com.github.packageurl.PackageURLBuilder; 11 | import pl.tlinkowski.annotation.basic.NullOr; 12 | 13 | import java.util.Optional; 14 | 15 | public class PackageIdentifier { 16 | String externalNamespace; 17 | String externalId; 18 | 19 | PackageIdentifier(String namespace, String id) { 20 | this.externalNamespace = namespace; 21 | this.externalId = id; 22 | } 23 | 24 | Optional getPurl() { 25 | try { 26 | return Optional.of(PackageURLBuilder.aPackageURL() 27 | .withType(type()) 28 | .withNamespace(namespace()) 29 | .withName(name()) 30 | .withVersion(version()) 31 | .build()); 32 | } catch (MalformedPackageURLException e) { 33 | System.err.println("Invalid package URL for namespace '" + externalNamespace + "', id '" + externalId + "': " + e); 34 | return Optional.empty(); 35 | } 36 | } 37 | 38 | private String type() { 39 | switch (externalNamespace) { 40 | case "arch_linux": 41 | return "arch"; 42 | case "centos": 43 | case "fedora": 44 | case "redhat": 45 | case "opensuse": 46 | return "rpm"; 47 | case "crates": 48 | return "cargo"; 49 | case "dart": 50 | return "pub"; 51 | case "debian": 52 | case "ubuntu": 53 | return "deb"; 54 | case "npmjs": 55 | return "npm"; 56 | case "rubygems": 57 | return "gem"; 58 | default: 59 | return externalNamespace; 60 | } 61 | } 62 | 63 | private @NullOr String namespace() { 64 | switch (externalNamespace) { 65 | case "maven": 66 | return fromEnd(':', 2); 67 | case "github": 68 | case "gitlab": 69 | case "bitbucket": 70 | return fromEnd('/', 1); 71 | case "centos": 72 | case "fedora": 73 | case "opensuse": 74 | case "ubuntu": 75 | return externalNamespace; 76 | case "alpine": 77 | case "arch_linux": 78 | case "debian": 79 | return null; 80 | default: 81 | return fromEnd('/', 2); 82 | } 83 | } 84 | 85 | private @NullOr String name() { 86 | switch (externalNamespace) { 87 | case "maven": 88 | return fromEnd(':', 1); 89 | case "github": 90 | case "gitlab": 91 | case "bitbucket": 92 | @SuppressWarnings("ConstantConditions") final var name = fromStart(':', 0); 93 | @SuppressWarnings("ConstantConditions") final var pos = name.indexOf('/'); 94 | return (pos < 0) ? name : name.substring(pos + 1); 95 | case "alpine": 96 | case "arch_linux": 97 | case "centos": 98 | case "debian": 99 | case "fedora": 100 | case "opensuse": 101 | case "ubuntu": 102 | return fromEnd('/', 2); 103 | default: 104 | return fromEnd('/', 1); 105 | } 106 | } 107 | 108 | private @NullOr String version() { 109 | switch (externalNamespace) { 110 | case "maven": 111 | return fromStart(':', 2); 112 | case "github": 113 | case "gitlab": 114 | case "bitbucket": 115 | return fromStart(':', 1); 116 | case "alpine": 117 | case "arch_linux": 118 | case "centos": 119 | case "debian": 120 | case "fedora": 121 | case "opensuse": 122 | case "ubuntu": 123 | return fromEnd('/', 1); 124 | default: 125 | return fromEnd('/', 0); 126 | } 127 | } 128 | 129 | private @NullOr String fromStart(char sep, int offset) { 130 | final var parts = externalId.split(String.valueOf(sep)); 131 | return (offset < parts.length) ? parts[offset] : null; 132 | } 133 | 134 | private @NullOr String fromEnd(char sep, int offset) { 135 | final var parts = externalId.split(String.valueOf(sep)); 136 | return (offset < parts.length) ? parts[parts.length - offset - 1] : null; 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/test/java/com/philips/research/spdxbuilder/persistence/blackduck/BlackDuckApiTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2021, Koninklijke Philips N.V., https://www.philips.com 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | package com.philips.research.spdxbuilder.persistence.blackduck; 7 | 8 | import com.philips.research.spdxbuilder.core.domain.License; 9 | import com.philips.research.spdxbuilder.persistence.blackduck.BlackDuckApi.ComponentVersionJson; 10 | import com.philips.research.spdxbuilder.persistence.blackduck.BlackDuckApi.LicenseJson; 11 | import org.junit.jupiter.api.BeforeEach; 12 | import org.junit.jupiter.api.Nested; 13 | import org.junit.jupiter.api.Test; 14 | 15 | import java.util.List; 16 | 17 | import static org.assertj.core.api.Assertions.assertThat; 18 | 19 | class BlackDuckApiTest { 20 | private static final String NAME = "Name"; 21 | 22 | @Nested 23 | class ComponentLicense { 24 | private static final String LICENSE = "Apache-2.0"; 25 | private static final String LICENSE2 = "MIT"; 26 | 27 | private final ComponentVersionJson component = new ComponentVersionJson(); 28 | 29 | @BeforeEach 30 | void beforeEach() { 31 | final var licenseJson = new LicenseJson(); 32 | licenseJson.spdxId = LICENSE; 33 | licenseJson.licenseDisplay = NAME; 34 | 35 | component.licenses.add(licenseJson); 36 | } 37 | 38 | @Test 39 | void prefersSpdxLicense() { 40 | assertThat(component.getLicense()).contains(License.of(LICENSE)); 41 | } 42 | 43 | @Test 44 | void fallsBackToDisplayedLicense() { 45 | component.licenses.get(0).spdxId = null; 46 | 47 | assertThat(component.getLicense().toString()).contains("LicenseRef"); 48 | } 49 | 50 | @Test 51 | void ignoresUnknownLicense() { 52 | component.licenses.get(0).spdxId = null; 53 | component.licenses.get(0).licenseDisplay = "Unknown License"; 54 | 55 | assertThat(component.getLicense()).isEmpty(); 56 | } 57 | 58 | @Test 59 | void combinesConjunctiveByDefault() { 60 | final var json1 = new LicenseJson(); 61 | json1.spdxId = LICENSE; 62 | final var json2 = new LicenseJson(); 63 | json2.spdxId = LICENSE2; 64 | component.licenses.get(0).licenses = List.of(json1, json2); 65 | 66 | final var license = component.getLicense(); 67 | 68 | assertThat(license).contains(License.of(LICENSE).and(License.of(LICENSE2))); 69 | } 70 | 71 | @Test 72 | void combinesDisjunctive() { 73 | final var json1 = new LicenseJson(); 74 | json1.spdxId = LICENSE; 75 | final var json2 = new LicenseJson(); 76 | json2.spdxId = LICENSE2; 77 | component.licenses.get(0).licenseType = "DISJUNCTIVE"; 78 | component.licenses.get(0).licenses = List.of(json1, json2); 79 | 80 | final var license = component.getLicense(); 81 | 82 | assertThat(license).contains(License.of(LICENSE).or(License.of(LICENSE2))); 83 | } 84 | 85 | @Test 86 | void ignoresUnknownElementsInCombinedLicense() { 87 | final var json1 = new LicenseJson(); 88 | json1.spdxId = LICENSE; 89 | final var json2 = new LicenseJson(); 90 | json2.licenseDisplay = "Unknown License"; 91 | component.licenses.get(0).licenseType = "DISJUNCTIVE"; 92 | component.licenses.get(0).licenses = List.of(json1, json2); 93 | 94 | final var license = component.getLicense(); 95 | 96 | assertThat(license).contains(License.of(LICENSE)); 97 | } 98 | 99 | @Test 100 | void ignoresEffectivelyEmptyCombinedLicense() { 101 | final var json = new LicenseJson(); 102 | json.licenseDisplay = "Unknown License"; 103 | component.licenses.get(0).licenseType = "DISJUNCTIVE"; 104 | component.licenses.get(0).licenses = List.of(json); 105 | 106 | final var license = component.getLicense(); 107 | 108 | assertThat(license).isEmpty(); 109 | } 110 | } 111 | 112 | @Nested 113 | class Origin { 114 | @Test 115 | void defaultsToDecodingExternalId() { 116 | final var json = new BlackDuckApi.OriginJson(); 117 | json.externalNamespace = "maven"; 118 | json.externalId = "group:name:version"; 119 | 120 | assertThat(json.getPurl()).isNotEmpty(); 121 | } 122 | 123 | @Test 124 | void noPackageUrl_unknownExternalId() { 125 | final var json = new BlackDuckApi.OriginJson(); 126 | json.externalNamespace = "unknown"; 127 | 128 | assertThat(json.getPurl()).isEmpty(); 129 | } 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/main/java/com/philips/research/spdxbuilder/core/domain/LicenseParser.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2021, Koninklijke Philips N.V., https://www.philips.com 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | package com.philips.research.spdxbuilder.core.domain; 7 | 8 | import pl.tlinkowski.annotation.basic.NullOr; 9 | 10 | /** 11 | * Parser for SPDX-like license statements containing AND, OR, WITH clauses and braces. 12 | */ 13 | public class LicenseParser { 14 | private final LicenseDictionary dictionary; 15 | private StringBuilder buffer = new StringBuilder(); 16 | private License license = License.NONE; 17 | private License current = License.NONE; 18 | private String identifier = ""; 19 | private Mode mode = Mode.NONE; 20 | private boolean parsingWith = false; 21 | 22 | private LicenseParser() { 23 | this(LicenseDictionary.getInstance()); 24 | } 25 | 26 | private LicenseParser(LicenseDictionary dictionary) { 27 | this.dictionary = dictionary; 28 | } 29 | 30 | /** 31 | * @return the license matching the provided text 32 | */ 33 | public static License parse(@NullOr String text) { 34 | if (text == null || text.isBlank()) { 35 | return License.NONE; 36 | } 37 | //TODO Catch any exceptions and convert to single plain license via dictionary 38 | return new LicenseParser().decode(text); 39 | } 40 | 41 | private License decode(String text) { 42 | for (var i = 0; i < text.length(); i++) { 43 | final var ch = text.charAt(i); 44 | switch (ch) { 45 | case '(': 46 | updateCurrent(); 47 | if (!current.equals(License.NONE)) { 48 | appendCurrent(); 49 | } 50 | final var sub = bracketSubstring(text, i + 1); 51 | current = new LicenseParser(dictionary).decode(sub); 52 | i += sub.length() + 1; 53 | break; 54 | case ')': 55 | case ' ': 56 | addToken(); 57 | break; 58 | default: 59 | buffer.append(ch); 60 | } 61 | } 62 | addToken(); 63 | appendCurrent(); 64 | return license; 65 | } 66 | 67 | private String bracketSubstring(String text, int start) { 68 | var nested = 0; 69 | for (var i = start; i < text.length(); i++) { 70 | final var ch = text.charAt(i); 71 | if (ch == '(') { 72 | nested++; 73 | } else if (ch == ')') { 74 | if (nested == 0) { 75 | return text.substring(start, i); 76 | } 77 | nested--; 78 | } 79 | } 80 | return text.substring(start); 81 | } 82 | 83 | private void addToken() { 84 | final var token = buffer.toString().trim(); 85 | switch (token) { 86 | case "WITH": 87 | if (current.equals(License.NONE) && !identifier.isBlank() && !parsingWith) { 88 | current = dictionary.licenseFor(identifier); 89 | identifier = ""; 90 | parsingWith = true; 91 | } else { 92 | identifier += ' ' + token; 93 | } 94 | break; 95 | case "AND": 96 | appendCurrent(); 97 | mode = Mode.AND; 98 | break; 99 | case "OR": 100 | appendCurrent(); 101 | mode = Mode.OR; 102 | break; 103 | default: 104 | if (!token.isBlank()) { 105 | identifier += ' ' + token; 106 | } 107 | } 108 | buffer = new StringBuilder(); 109 | } 110 | 111 | private void appendCurrent() { 112 | updateCurrent(); 113 | switch (mode) { 114 | case AND: 115 | license = license.and(current); 116 | break; 117 | case OR: 118 | license = license.or(current); 119 | break; 120 | default: 121 | if (!current.equals(License.NONE)) { 122 | if (license.equals(License.NONE)) { 123 | license = current; 124 | } else { 125 | license = license.and(current); 126 | } 127 | } 128 | } 129 | current = License.NONE; 130 | mode = Mode.NONE; 131 | } 132 | 133 | private void updateCurrent() { 134 | if (identifier.isBlank()) { 135 | parsingWith = false; 136 | return; 137 | } 138 | if (parsingWith) { 139 | current = dictionary.withException(current, identifier); 140 | parsingWith = false; 141 | } else { 142 | current = dictionary.licenseFor(identifier); 143 | } 144 | identifier = ""; 145 | } 146 | 147 | enum Mode {NONE, AND, OR} 148 | } 149 | 150 | -------------------------------------------------------------------------------- /src/main/java/com/philips/research/spdxbuilder/controller/OrtConfiguration.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2021, Koninklijke Philips N.V., https://www.philips.com 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | package com.philips.research.spdxbuilder.controller; 7 | 8 | import com.fasterxml.jackson.annotation.JsonAutoDetect; 9 | import com.fasterxml.jackson.annotation.PropertyAccessor; 10 | import com.fasterxml.jackson.core.JsonProcessingException; 11 | import com.fasterxml.jackson.databind.ObjectMapper; 12 | import com.fasterxml.jackson.databind.exc.MismatchedInputException; 13 | import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; 14 | import com.github.packageurl.MalformedPackageURLException; 15 | import com.github.packageurl.PackageURL; 16 | import pl.tlinkowski.annotation.basic.NullOr; 17 | 18 | import java.io.IOException; 19 | import java.io.InputStream; 20 | import java.io.InputStreamReader; 21 | import java.net.URI; 22 | import java.util.ArrayList; 23 | import java.util.List; 24 | 25 | class OrtConfiguration { 26 | private static final ObjectMapper MAPPER = new ObjectMapper(new YAMLFactory()) 27 | .setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.NON_PRIVATE); 28 | 29 | Document document = new Document(); 30 | List projects = new ArrayList<>(); 31 | List curations = new ArrayList<>(); 32 | 33 | static OrtConfiguration parse(InputStream stream) { 34 | try (final var reader = new InputStreamReader(stream)) { 35 | final OrtConfiguration ortConfiguration = MAPPER.readValue(reader, OrtConfiguration.class); 36 | validate(ortConfiguration); 37 | return ortConfiguration; 38 | } catch (MismatchedInputException e) { 39 | final var location = e.getLocation(); 40 | throw new IllegalArgumentException("Configuration format error at line " + location.getLineNr() 41 | + ", column " + location.getColumnNr()); 42 | } catch (IOException e) { 43 | throw new IllegalArgumentException("Malformed configuration file: ", e); 44 | } 45 | } 46 | 47 | @SuppressWarnings("ConstantConditions") 48 | private static void validate(OrtConfiguration ortConfiguration) { 49 | if (ortConfiguration == null) { 50 | throw new IllegalArgumentException("Configuration is empty"); 51 | } 52 | if (ortConfiguration.document == null) { 53 | throw new IllegalArgumentException("Configuration contains empty 'document' section"); 54 | } 55 | if (ortConfiguration.projects == null) { 56 | throw new IllegalArgumentException("Configuration contains empty 'projects' section"); 57 | } 58 | if (ortConfiguration.curations == null) { 59 | throw new IllegalArgumentException("Configuration contains empty 'curations' section"); 60 | } 61 | } 62 | 63 | static String example() { 64 | final var config = new OrtConfiguration(); 65 | config.document.title = "<(Optional) Document title>"; 66 | config.document.comment = "<(Optional) Document comment>"; 67 | config.document.namespace = URI.create("http://optional/document/namespace/uri"); 68 | config.document.organization = "<(Optional) Organization name>"; 69 | config.document.key = "<(Optional) Document key>"; 70 | 71 | final var project = new Project(); 72 | project.id = ""; 73 | project.purl = URI.create("pkg:type/namespace/name@version"); 74 | project.excluded = List.of("scope", "test*"); 75 | config.projects.add(project); 76 | 77 | final var curation = new Curation(); 78 | curation.purl = URI.create("pkg:type/namespace/name@version"); 79 | curation.source = URI.create("https://optional/source/location/uri"); 80 | curation.license = "<(Optional) License>"; 81 | config.curations.add(curation); 82 | 83 | try { 84 | return MAPPER.writeValueAsString(config); 85 | } catch (JsonProcessingException e) { 86 | throw new RuntimeException("Failed to generate configuration example", e); 87 | } 88 | } 89 | 90 | static class Document { 91 | String title = ""; 92 | String organization = ""; 93 | String comment = ""; 94 | @NullOr String key = ""; 95 | @NullOr URI namespace; 96 | } 97 | 98 | //TODO This seems only relevant for ORT import 99 | static class Project { 100 | String id; 101 | @NullOr URI purl; 102 | @NullOr List excluded; 103 | } 104 | 105 | //TODO Add way to mark internal packages using wildcard 106 | static class Curation { 107 | URI purl; 108 | //FIXME Is the source code location even appropriate here? (drop) 109 | @NullOr URI source; 110 | @NullOr String license; 111 | 112 | PackageURL getPurl() { 113 | try { 114 | return new PackageURL(purl.toASCIIString()); 115 | } catch (MalformedPackageURLException e) { 116 | throw new IllegalArgumentException("Not a valid package URL: " + purl); 117 | } 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | # SPDX-Builder 4 | 5 | [![Release](https://img.shields.io/github/release/philips-software/spdx-builder.svg)](https://github.com/philips-software/spdx-builder/releases) 6 | 7 | CI/CD tool to generate Bill-of-Materials reports in SPDX format. 8 | > **Status**: Experimental research prototype 9 | 10 |
11 | 12 | ## Contents 13 | 14 | - [Description](#Description) 15 | - [Installation](#installation) 16 | - [Usage](#usage) 17 | - [How to test the software](#how-to-test-the-software) 18 | - [Known issues](#known-issues) 19 | - [Contact / Getting help](#contact--getting-help) 20 | - [License](#license) 21 | - [Credits and references](#credits-and-references) 22 | 23 | ## Description 24 | 25 | Converts project dependencies into a standard 26 | [SPDX](https://spdx.github.io/spdx-spec) tag-value Software Bill-of-Materials 27 | file, optionally integrating externally collected and curated license details. 28 | 29 | A Bill-of-Materials can be generated from various types of inputs: 30 | 31 | 1. From the output of 32 | the [OSS Review Toolkit](https://github.com/oss-review-toolkit/ort) (ORT) 33 | Analyzer tool, optionally in combination with scanned licences provided by 34 | [License Scanning Service](https://github.com/philips-software/license-scanner) 35 | or the [BOM-Base](https://github.com/philips-software/bom-base) metadata 36 | harvesting service. (See [ORT mode usage](docs/usage_with_ort.md)) 37 | 38 | 2. From the REST API of 39 | a [Synoptic Black Duck](https://www.synopsys.com/software-integrity/security-testing/software-composition-analysis.html) 40 | SCA server. (See [Black Duck mode usage](docs/usage_with_black_duck.md)) 41 | 42 | 3. From the "tree" output of many build environments, in combination with 43 | metadata from a [BOM-Base](https://github.com/philips-software/bom-base) 44 | metadata harvesting service. (See [Tree mode usage](docs/usage_with_tree.md)) 45 | 46 | ## Installation 47 | 48 | Build the application using the standard gradle command: 49 | 50 | ```shell 51 | ./gradlew clean install 52 | ``` 53 | 54 | Then make the resulting files from the `build/install/spdx-builder/bin` 55 | available in the path. 56 | 57 | Alternatively the application can be run directly from Gradle: 58 | 59 | ```shell 60 | ./gradlew run --args="ort -c .spdx-builder.yml " 61 | ``` 62 | 63 | ## Usage 64 | 65 | The commandline application has usage instructions built-in 66 | 67 | ```shell 68 | spdx-builder --help 69 | ``` 70 | 71 | Separate usage details are found per mode for: [ort mode](docs/usage_with_ort.md) 72 | ,[blackduck mode](docs/usage_with_black_duck.md), 73 | and [tree mode](docs/usage_with_tree.md). 74 | 75 | _NOTE: This application requires Java 11 or higher._ 76 | 77 | ### Uploading the resulting SPDX file 78 | 79 | It is possible to automatically upload the generated SDPX file to a server. This 80 | will POST the SPDX file using a multi-part file upload in the `file` parameter . 81 | 82 | To upload the extracted bill-of-materials from an ORT file 83 | to [BOM-bar](https://github.com/philips-software/bom-bar), the invocation 84 | becomes: 85 | 86 | ```shell 87 | spdx-builder ort -c -upload=https://:8080/projects//upload 88 | ``` 89 | 90 | ### GitHub actions 91 | 92 | You can use the SPDX-builder in a GitHub Action. This can be found on 93 | . The Action performs an ORT 94 | scan, pushes the data to SPDX-builder and can use a self-hosted license scanner 95 | service and upload service like BOM-Bar. 96 | 97 | ## How to test the software 98 | 99 | The unit test suite is run via the standard Gradle command: 100 | 101 | ```shell 102 | ./gradlew clean test 103 | ``` 104 | 105 | A local ORT-based self-test (if ORT is installed locally) can be run by: 106 | 107 | ```shell 108 | ./gradlew run --args="ort -c src/test/resources/.spdx-builder.yml src/test/resources/ort_sample.yml" 109 | ``` 110 | 111 | ## Known issues 112 | 113 | (Ticked checkboxes indicate topics currently under development.) 114 | 115 | Must-have: 116 | 117 | - [ ] Abort if ORT Analyzer raised errors. 118 | - [ ] Support the new (more compact) ORT tree structure. (Currently breaks Gradle projects.) 119 | - [ ] Add hashes of build results (where possible). 120 | - [ ] (Optionally) Add source artefacts as "GENERATED_FROM" relationship. 121 | 122 | Should-have: 123 | 124 | - [ ] Treat internal (=non-OSS) packages differently for output SBOM. 125 | - [ ] Support output "flavors" for the purpose of the generated SBOM. 126 | 127 | Other ideas: 128 | 129 | - [ ] Integration with [Quartermaster (QMSTR)](https://qmstr.org/). 130 | 131 | ## Contact / Getting help 132 | 133 | Submit tickets to 134 | the [issue tracker](https://github.com/philips-software/spdx-builder/issues). 135 | 136 | See the [architecture document](docs/architecture.md) for a detailed technical 137 | description. 138 | 139 | ## License 140 | 141 | See [LICENSE.md](LICENSE.md). 142 | 143 | ## Credits and references 144 | 145 | 1. [The SPDX Specification](https://spdx.github.io/spdx-spec) documents the SPDX 146 | file standard. 147 | 2. [The ORT Project](https://github.com/oss-review-toolkit) provides a toolset 148 | for generating and analyzing various aspects of the Bill-of-Materials. 149 | -------------------------------------------------------------------------------- /src/test/java/com/philips/research/spdxbuilder/core/domain/LicenseDictionaryTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2021, Koninklijke Philips N.V., https://www.philips.com 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | package com.philips.research.spdxbuilder.core.domain; 7 | 8 | import org.junit.jupiter.api.Test; 9 | 10 | import java.util.Map; 11 | 12 | import static org.assertj.core.api.Assertions.assertThat; 13 | 14 | class LicenseDictionaryTest { 15 | private final LicenseDictionary dictionary = new LicenseDictionary(); 16 | 17 | @Test 18 | void createsSingleton() { 19 | final var first = LicenseDictionary.getInstance(); 20 | final var second = LicenseDictionary.getInstance(); 21 | 22 | assertThat(first).isInstanceOf(LicenseDictionary.class); 23 | assertThat(second).isSameAs(first); 24 | } 25 | 26 | @Test 27 | void ignoresEmptyLicense() { 28 | final var license = dictionary.licenseFor(" \t"); 29 | 30 | assertThat(license).isEqualTo(License.NONE); 31 | } 32 | 33 | @Test 34 | void acceptsVoidLicenses() { 35 | final var license = dictionary.licenseFor("noassertion"); 36 | 37 | assertThat(license.isDefined()).isFalse(); 38 | assertThat(dictionary.getCustomLicenses()).isEmpty(); 39 | } 40 | 41 | @Test 42 | void normalizesSpdxLicenses() { 43 | final var license = dictionary.licenseFor(" mit "); 44 | 45 | assertThat(license).isEqualTo(License.of("MIT")); 46 | } 47 | 48 | @Test 49 | void detectsSpdxLicensesByName() { 50 | final var license = dictionary.licenseFor("MIT License"); 51 | 52 | assertThat(license).isEqualTo(License.of("MIT")); 53 | } 54 | 55 | @Test 56 | void createsCustomLicenseIdentifier() { 57 | final var custom1 = dictionary.licenseFor("First"); 58 | final var custom2 = dictionary.licenseFor("Second"); 59 | 60 | assertThat(custom1).isEqualTo(License.of("LicenseRef-1")); 61 | assertThat(custom2).isEqualTo(License.of("LicenseRef-2")); 62 | } 63 | 64 | @Test 65 | void clearsDictionaryForTestingPurposes() { 66 | dictionary.licenseFor("First"); 67 | dictionary.clear(); 68 | dictionary.licenseFor("Second"); 69 | 70 | assertThat(dictionary.getCustomLicenses()).containsOnlyKeys("LicenseRef-1"); 71 | } 72 | 73 | @Test 74 | void upgradesDeprecatedSpdxLicenses() { 75 | final var deprecated = dictionary.licenseFor("GPL-2.0-with-classpath-exception"); 76 | 77 | assertThat(deprecated).isEqualTo(License.of("GPL-2.0-only").with("Classpath-exception-2.0")); 78 | } 79 | 80 | @Test 81 | void reusesCustomLicenseIdentifier() { 82 | final var custom1 = dictionary.licenseFor("Custom"); 83 | final var custom2 = dictionary.licenseFor("CUSTOM"); 84 | 85 | assertThat(custom1).isEqualTo(custom2); 86 | } 87 | 88 | @Test 89 | void listsCustomLicenses() { 90 | dictionary.licenseFor(" First "); 91 | dictionary.licenseFor("Second"); 92 | 93 | assertThat(dictionary.getCustomLicenses()).isEqualTo(Map.of("LicenseRef-1", "First", "LicenseRef-2", "Second")); 94 | } 95 | 96 | @Test 97 | void ignoresEmptyException() { 98 | final var base = License.of("MIT"); 99 | 100 | final var license = dictionary.withException(base, " \t"); 101 | 102 | assertThat(license).isSameAs(base); 103 | } 104 | 105 | @Test 106 | void extendsLicenseWithSpdxException() { 107 | final var base = License.of("GPL-3.0-only"); 108 | 109 | final var license = dictionary.withException(base, " Classpath-exception-2.0 "); 110 | 111 | assertThat(license).isEqualTo(base.with("Classpath-exception-2.0")); 112 | } 113 | 114 | @Test 115 | void detectsSpdxExceptionsByName() { 116 | final var base = License.of("GPL-3.0-only"); 117 | 118 | final var license = dictionary.withException(base, "Classpath exception 2.0"); 119 | 120 | assertThat(license).isEqualTo(base.with("Classpath-exception-2.0")); 121 | } 122 | 123 | @Test 124 | void customLicenseForCustomException() { 125 | final var base = License.of("MIT"); 126 | 127 | final var license = dictionary.withException(base, " Not an exception "); 128 | 129 | assertThat(license).isEqualTo(License.of("LicenseRef-1")); 130 | assertThat(dictionary.getCustomLicenses()).containsEntry("LicenseRef-1", "MIT WITH Not an exception"); 131 | } 132 | 133 | @Test 134 | void expandsCustomLicenseElements() { 135 | final var license = License.of("MIT") 136 | .and(dictionary.licenseFor("First")) 137 | .or(dictionary.licenseFor("Second")); 138 | 139 | final var string = dictionary.expand(license); 140 | 141 | assertThat(string).isEqualTo(License.of("MIT").and(License.of("First")).or(License.of("Second")).toString()); 142 | } 143 | 144 | @Test 145 | void expandsUnknownCustomElements() { 146 | final var license = License.of("MIT") 147 | .and(License.of("LicenseRef-abc123")) 148 | .and(License.of("Unknown")); 149 | 150 | final var string = dictionary.expand(license); 151 | 152 | assertThat(string).isEqualTo(License.of("MIT").and(License.of("?")).and(License.of("Unknown")).toString()); 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/main/resources/treeformats.yml: -------------------------------------------------------------------------------- 1 | formats: 2 | - format: java 3 | description: "Base definitions for Java group and artifact identifiers." 4 | identifier: "[\\w]" 5 | types: 6 | "" : "maven" 7 | - format: maven 8 | parent: java 9 | description: "Default Maven Java tree output" 10 | tool: "mvn dependency:tree" 11 | cleanup: "^\\[INFO]\\s" 12 | skip: ":test$" 13 | namespace: 14 | regex: "^([^:]*):([^:]*):([^:]*):([^:]*)" 15 | group: 1 16 | name: 17 | regex: "^([^:]*):([^:]*):([^:]*):([^:]*)" 18 | group: 2 19 | version: 20 | regex: "^([^:]*):([^:]*):([^:]*):([^:]*)" 21 | group: 4 22 | start: "^\\[INFO]\\s-+\\smaven-dependency-plugin" 23 | end: "^\\[INFO]\\s-+$" 24 | - format: gradle 25 | parent: java 26 | description: "Default Gradle Java tree output" 27 | tool: "gradlew -q dependencies --configuration runtimeClasspath" 28 | namespace: 29 | regex: "^([^:]*):([^:]*):" 30 | group: 1 31 | name: 32 | regex: "^([^:]*):([^:]*):" 33 | group: 2 34 | version: 35 | regex: "[:\\s]([^:\\s]+)\\s*(\\(.+\\))?$" 36 | group: 1 37 | start: "^runtimeClasspath" # Only runtime dependencies 38 | end: "^\\s*$" # Empty line 39 | - format: npm 40 | description: "Default NPM tree output" 41 | tool: "npm list --all --production" 42 | identifier: "[@\\w]" 43 | types: 44 | "": "npm" 45 | namespace: 46 | regex: "^(\\S+)/" 47 | group: 1 48 | name: 49 | regex: "^(\\S+/)?(.+)@[^@]+$" 50 | group: 2 51 | version: 52 | regex: "@([^@\\s]+)(\\s\\S+)?$" 53 | group: 1 54 | - format: nuget 55 | description: "Tree output for NuGet based on tooling found in https://github.com/Brend-Smits/NugetTree" 56 | tool: "dotnet src/NugetTree/bin/Debug/netcoreapp3.1/NugetTree.dll \"NugetTree.sln\" -t" 57 | types: 58 | "": "nuget" 59 | name: 60 | regex: "(^\\S+)" 61 | version: 62 | regex: "\\s(\\S+)" 63 | - format: rust 64 | description: "Default Rust (=Cargo) tree output" 65 | tool: "cargo tree -e no-dev,no-build --locked" 66 | types: 67 | "" : "cargo" 68 | identifier: "[-\\[\\w]" 69 | skip: "^\\[.+]$" 70 | internal: "\\([^*]+\\)" 71 | name: 72 | regex: "^([-\\w]+)\\s" 73 | group: 1 74 | version: 75 | regex: "^\\S+\\sv(\\S+)(\\s.+)?$" 76 | group: 1 77 | - format: python 78 | description: "Base definitions for Python group and artifact identifiers." 79 | identifier: "[\\w]" 80 | types: 81 | "" : "pypi" 82 | - format: pip 83 | parent: python 84 | description: "Non-hierarchical Python pip freeze format." 85 | tool: "pip freeze" 86 | name: 87 | regex: "^([^=]+)=" 88 | group: 1 89 | version: 90 | regex: "=([^=]+)$" 91 | group: 1 92 | - format: pipenv 93 | parent: python 94 | description: "Default Python pipenv tree output" 95 | tool: "pipenv graph --bare" 96 | skip: ":test$" 97 | name: 98 | regex: "^(.+?)[=\\s]" 99 | group: 1 100 | version: 101 | regex: "(\\S==|installed:\\s)([^\\]]+)\\]?$" 102 | group: 2 103 | - format: purl 104 | description: "Tree output of SPDX-Builder itself." 105 | tool: "SPDX-Builder --tree" 106 | type: 107 | regex: "^pkg:([^/]+)/" 108 | group: 1 109 | namespace: 110 | regex: "^pkg:([^/]+)/([^/@]+)/.+@" 111 | group: 2 112 | replace: 113 | "%21": "!" 114 | "%23": "#" 115 | "%24": "$" 116 | "%25": "%" 117 | "%26": "&" 118 | "%27": "'" 119 | "%28": "{" 120 | "%29": "}" 121 | "%2[Aa]": "*" 122 | "%2[Bb]": "+" 123 | "%2[Cc]": "," 124 | "%2[Ff]": "/" 125 | "%3[Aa]": ":" 126 | "%3[Bb]": ";" 127 | "%3[Dd]": "=" 128 | "%3[Ff]": "?" 129 | "%40": "@" 130 | "%5[Bb]": "[" 131 | "%5[Dd]": "]" 132 | name: 133 | regex: "/([^/]+)@" 134 | group: 1 135 | replace: 136 | "%21": "!" 137 | "%23": "#" 138 | "%24": "$" 139 | "%25": "%" 140 | "%26": "&" 141 | "%27": "'" 142 | "%28": "{" 143 | "%29": "}" 144 | "%2[Aa]": "*" 145 | "%2[Bb]": "+" 146 | "%2[Cc]": "," 147 | "%2[Ff]": "/" 148 | "%3[Aa]": ":" 149 | "%3[Bb]": ";" 150 | "%3[Dd]": "=" 151 | "%3[Ff]": "?" 152 | "%40": "@" 153 | "%5[Bb]": "[" 154 | "%5[Dd]": "]" 155 | version: 156 | regex: "@(\\S+)($|\\s)" 157 | group: 1 158 | replace: 159 | "%21": "!" 160 | "%23": "#" 161 | "%24": "$" 162 | "%25": "%" 163 | "%26": "&" 164 | "%27": "'" 165 | "%28": "{" 166 | "%29": "}" 167 | "%2[Aa]": "*" 168 | "%2[Bb]": "+" 169 | "%2[Cc]": "," 170 | "%2[Ff]": "/" 171 | "%3[Aa]": ":" 172 | "%3[Bb]": ";" 173 | "%3[Dd]": "=" 174 | "%3[Ff]": "?" 175 | "%40": "@" 176 | "%5[Bb]": "[" 177 | "%5[Dd]": "]" 178 | relationship: 179 | regex: "\\s\\[(.+)]" 180 | group: 1 181 | relationships: 182 | "": DEPENDS_ON 183 | "dynamic": DYNAMICALLY_LINKS 184 | "static": STATICALLY_LINKS 185 | "derived": DESCENDANT_OF 186 | "contained": CONTAINS 187 | "dev": DEVELOPED_USING 188 | start: "^TREE start" 189 | end: "^TREE end" 190 | -------------------------------------------------------------------------------- /src/main/java/com/philips/research/spdxbuilder/core/domain/License.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2021, Koninklijke Philips N.V., https://www.philips.com 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | 6 | package com.philips.research.spdxbuilder.core.domain; 7 | 8 | import pl.tlinkowski.annotation.basic.NullOr; 9 | 10 | import java.util.HashSet; 11 | import java.util.Set; 12 | import java.util.stream.Collectors; 13 | 14 | public abstract class License { 15 | @SuppressWarnings("StaticInitializerReferencesSubClass") 16 | public static final License NONE = new NoLicense(); 17 | 18 | public static License of(String identifier) { 19 | if (identifier.isBlank()) { 20 | return NONE; 21 | } 22 | return new SingleLicense(identifier.trim()); 23 | } 24 | 25 | public License with(String exception) { 26 | throw new LicenseException("Cannot add WITH clause to '" + this + "'"); 27 | } 28 | 29 | public License and(License license) { 30 | if (license instanceof NoLicense || this.equals(license)) { 31 | return this; 32 | } 33 | return new AndLicense(this).and(license); 34 | } 35 | 36 | public License or(License license) { 37 | if (license instanceof NoLicense || this.equals(license)) { 38 | return this; 39 | } 40 | return new OrLicense(this).or(license); 41 | } 42 | 43 | public boolean isDefined() { 44 | return true; 45 | } 46 | 47 | @Override 48 | public final int hashCode() { 49 | return toString().toLowerCase().hashCode(); 50 | } 51 | 52 | @Override 53 | public final boolean equals(Object obj) { 54 | if (!(obj instanceof License)) { 55 | return false; 56 | } 57 | return this.getClass() == obj.getClass() 58 | && toString().equalsIgnoreCase(obj.toString()); 59 | } 60 | 61 | @Override 62 | public String toString() { 63 | return "License{}"; 64 | } 65 | 66 | private static class NoLicense extends License { 67 | @Override 68 | public boolean isDefined() { 69 | return false; 70 | } 71 | 72 | @Override 73 | public License and(License license) { 74 | return license; 75 | } 76 | 77 | @Override 78 | public License or(License license) { 79 | return license; 80 | } 81 | 82 | @Override 83 | public String toString() { 84 | return ""; 85 | } 86 | } 87 | 88 | private static class SingleLicense extends License { 89 | private final String identifier; 90 | 91 | private @NullOr String exception = null; 92 | 93 | SingleLicense(String identifier) { 94 | this.identifier = identifier; 95 | } 96 | 97 | @Override 98 | public License with(String exception) { 99 | if (this.exception != null) { 100 | throw new LicenseException("Adding a second exception is not allowed"); 101 | } 102 | final var license = new SingleLicense(identifier); 103 | license.exception = exception.trim(); 104 | return license; 105 | } 106 | 107 | @Override 108 | public boolean isDefined() { 109 | return !identifier.equals("NOASSERTION"); 110 | } 111 | 112 | @Override 113 | public String toString() { 114 | return (exception != null) 115 | ? String.format("%s WITH %s", identifier, exception) 116 | : identifier; 117 | } 118 | } 119 | 120 | private static class ComboLicense extends License { 121 | private final String operation; 122 | private final Set licenses = new HashSet<>(); 123 | 124 | public ComboLicense(String operation, License license) { 125 | this.operation = String.format(" %s ", operation); 126 | licenses.add(license); 127 | } 128 | 129 | License merge(License license) { 130 | if (license.getClass() == this.getClass()) { 131 | licenses.addAll(((ComboLicense) license).licenses); 132 | } else if (!(license instanceof NoLicense)) { 133 | licenses.add(license); 134 | } 135 | return this; 136 | } 137 | 138 | @Override 139 | public String toString() { 140 | return licenses.stream() 141 | .filter(License::isDefined) 142 | .map(license -> (license instanceof ComboLicense) 143 | ? String.format("(%s)", license) 144 | : license.toString()) 145 | .sorted(String::compareToIgnoreCase) 146 | .collect(Collectors.joining(operation)); 147 | } 148 | } 149 | 150 | private static class OrLicense extends ComboLicense { 151 | public OrLicense(License license) { 152 | super("OR", license); 153 | } 154 | 155 | @Override 156 | public License or(License license) { 157 | return merge(license); 158 | } 159 | } 160 | 161 | private static class AndLicense extends ComboLicense { 162 | public AndLicense(License license) { 163 | super("AND", license); 164 | } 165 | 166 | @Override 167 | public License and(License license) { 168 | return merge(license); 169 | } 170 | } 171 | } 172 | --------------------------------------------------------------------------------