Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement SPDX expressions #393

Merged
merged 1 commit into from
Oct 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 18 additions & 5 deletions src/main/java/org/dependencytrack/model/Component.java
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,11 @@ public enum FetchGroup {
@Pattern(regexp = RegexSequence.Definition.PRINTABLE_CHARS, message = "The license may only contain printable characters")
private String license;

@Persistent
@Column(name = "LICENSE_EXPRESSION", jdbcType = "CLOB", allowsNull = "true")
@Pattern(regexp = RegexSequence.Definition.PRINTABLE_CHARS, message = "The license expression may only contain printable characters")
private String licenseExpression;

@Persistent
@Column(name = "LICENSE_URL", jdbcType = "VARCHAR")
@Size(max = 255)
Expand Down Expand Up @@ -352,7 +357,7 @@ public enum FetchGroup {
private UUID uuid;

private transient String bomRef;
private transient String licenseId;
private transient List<org.cyclonedx.model.License> licenseCandidates;
private transient DependencyMetrics metrics;
private transient RepositoryMetaComponent repositoryMeta;

Expand Down Expand Up @@ -629,6 +634,14 @@ public void setLicense(String license) {
this.license = StringUtils.abbreviate(license, 255);
}

public String getLicenseExpression() {
return licenseExpression;
}

public void setLicenseExpression(String licenseExpression) {
this.licenseExpression = licenseExpression;
}

public String getLicenseUrl() {
return licenseUrl;
}
Expand Down Expand Up @@ -775,12 +788,12 @@ public void setBomRef(String bomRef) {
this.bomRef = bomRef;
}

public String getLicenseId() {
return licenseId;
public List<org.cyclonedx.model.License> getLicenseCandidates() {
return licenseCandidates;
}

public void setLicenseId(final String licenseId) {
this.licenseId = licenseId;
public void setLicenseCandidates(final List<org.cyclonedx.model.License> licenseCandidates) {
this.licenseCandidates = licenseCandidates;
}

public int getUsedBy() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@
import org.dependencytrack.model.Vulnerability;
import org.dependencytrack.parser.common.resolver.CweResolver;
import org.dependencytrack.parser.cyclonedx.CycloneDXExporter;
import org.dependencytrack.parser.spdx.expression.SpdxExpressionParser;
import org.dependencytrack.parser.spdx.expression.model.SpdxExpression;
import org.dependencytrack.persistence.QueryManager;
import org.dependencytrack.util.VulnerabilityUtil;

Expand All @@ -56,13 +58,16 @@
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Function;

import static org.apache.commons.lang3.StringUtils.isNotBlank;
import static org.apache.commons.lang3.StringUtils.trim;
import static org.apache.commons.lang3.StringUtils.trimToNull;
import static org.dependencytrack.util.PurlUtil.silentPurlCoordinatesOnly;

Expand Down Expand Up @@ -162,18 +167,33 @@ public static Component convertComponent(final org.cyclonedx.model.Component cdx
}
}

if (cdxComponent.getLicenseChoice() != null
&& cdxComponent.getLicenseChoice().getLicenses() != null
&& !cdxComponent.getLicenseChoice().getLicenses().isEmpty()) {
for (final org.cyclonedx.model.License cdxLicense : cdxComponent.getLicenseChoice().getLicenses()) {
if (cdxLicense != null) {
component.setLicenseId(trimToNull(cdxLicense.getId()));
component.setLicense(trimToNull(cdxLicense.getName()));
component.setLicenseUrl(trimToNull(cdxLicense.getUrl()));
break; // Components in CDX can have multiple licenses, but DT supports only one
final var licenseCandidates = new ArrayList<org.cyclonedx.model.License>();
if (cdxComponent.getLicenseChoice() != null) {
if (cdxComponent.getLicenseChoice().getLicenses() != null) {
cdxComponent.getLicenseChoice().getLicenses().stream()
.filter(license -> isNotBlank(license.getId()) || isNotBlank(license.getName()))
.peek(license -> {
// License text can be large, but we don't need it for further processing. Drop it.
license.setLicenseText(null);
})
.forEach(licenseCandidates::add);
}

if (isNotBlank(cdxComponent.getLicenseChoice().getExpression())) {
component.setLicenseExpression(trim(cdxComponent.getLicenseChoice().getExpression()));

// If the expression consists of just one license ID, add it as another option.
final var expressionParser = new SpdxExpressionParser();
final SpdxExpression expression = expressionParser.parse(component.getLicenseExpression());
if (expression.getSpdxLicenseId() != null) {
final var expressionLicense = new org.cyclonedx.model.License();
expressionLicense.setId(expression.getSpdxLicenseId());
expressionLicense.setName(expression.getSpdxLicenseId());
licenseCandidates.add(expressionLicense);
}
}
}
component.setLicenseCandidates(licenseCandidates);

if (cdxComponent.getComponents() != null && !cdxComponent.getComponents().isEmpty()) {
final var children = new ArrayList<Component>();
Expand Down Expand Up @@ -358,32 +378,33 @@ public static org.cyclonedx.model.Component convert(final QueryManager qm, final
cycloneComponent.addHash(new Hash(Hash.Algorithm.SHA3_512, component.getSha3_512()));
}

final LicenseChoice licenseChoice = new LicenseChoice();
if (component.getResolvedLicense() != null) {
final org.cyclonedx.model.License license = new org.cyclonedx.model.License();
license.setId(component.getResolvedLicense().getLicenseId());
license.setUrl(component.getLicenseUrl());
final LicenseChoice licenseChoice = new LicenseChoice();
licenseChoice.addLicense(license);
cycloneComponent.setLicenseChoice(licenseChoice);
} else if (component.getLicense() != null) {
final org.cyclonedx.model.License license = new org.cyclonedx.model.License();
license.setName(component.getLicense());
license.setUrl(component.getLicenseUrl());
final LicenseChoice licenseChoice = new LicenseChoice();
licenseChoice.addLicense(license);
cycloneComponent.setLicenseChoice(licenseChoice);
} else if (StringUtils.isNotEmpty(component.getLicenseUrl())) {
final org.cyclonedx.model.License license = new org.cyclonedx.model.License();
license.setUrl(component.getLicenseUrl());
final LicenseChoice licenseChoice = new LicenseChoice();
licenseChoice.addLicense(license);
cycloneComponent.setLicenseChoice(licenseChoice);
}

if (component.getLicenseExpression() != null) {
licenseChoice.setExpression(component.getLicenseExpression());
cycloneComponent.setLicenseChoice(licenseChoice);
}

if (component.getExternalReferences() != null && component.getExternalReferences().size() > 0) {
List<org.cyclonedx.model.ExternalReference> references = new ArrayList<>();
for (ExternalReference ref: component.getExternalReferences()) {
for (ExternalReference ref : component.getExternalReferences()) {
org.cyclonedx.model.ExternalReference cdxRef = new org.cyclonedx.model.ExternalReference();
cdxRef.setType(ref.getType());
cdxRef.setUrl(ref.getUrl());
Expand Down Expand Up @@ -737,17 +758,23 @@ private static org.cyclonedx.model.vulnerability.Vulnerability.Source convertDtV
cdxSource.setName(vulnSource.name());
switch (vulnSource) {
case NVD:
cdxSource.setUrl("https://nvd.nist.gov/"); break;
cdxSource.setUrl("https://nvd.nist.gov/");
break;
case NPM:
cdxSource.setUrl("https://www.npmjs.com/"); break;
cdxSource.setUrl("https://www.npmjs.com/");
break;
case GITHUB:
cdxSource.setUrl("https://github.com/advisories"); break;
cdxSource.setUrl("https://github.com/advisories");
break;
case VULNDB:
cdxSource.setUrl("https://vulndb.cyberriskanalytics.com/"); break;
cdxSource.setUrl("https://vulndb.cyberriskanalytics.com/");
break;
case OSSINDEX:
cdxSource.setUrl("https://ossindex.sonatype.org/"); break;
cdxSource.setUrl("https://ossindex.sonatype.org/");
break;
case RETIREJS:
cdxSource.setUrl("https://github.com/RetireJS/retire.js"); break;
cdxSource.setUrl("https://github.com/RetireJS/retire.js");
break;
}
return cdxSource;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
/*
* This file is part of Dependency-Track.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
* Copyright (c) Steve Springett. All Rights Reserved.
*/
package org.dependencytrack.parser.spdx.expression;

import java.util.ArrayDeque;
import java.util.Iterator;
import java.util.List;

import org.dependencytrack.parser.spdx.expression.model.SpdxOperator;
import org.dependencytrack.parser.spdx.expression.model.SpdxExpression;

/**
* This class parses SPDX expressions according to
* https://spdx.github.io/spdx-spec/v2-draft/SPDX-license-expressions/ into a tree of
* SpdxExpressions and SpdxExpressionOperations
*
* @author hborchardt
* @since 4.8.0
*/
public class SpdxExpressionParser {

/**
* Reads in a SPDX expression and returns a parsed tree of SpdxExpressionOperators and license
* ids.
*
* @param spdxExpression
* spdx expression string
* @return parsed SpdxExpression tree, or SpdxExpression.INVALID if an error has occurred during
* parsing
*/
public SpdxExpression parse(final String spdxExpression) {
// operators are surrounded by spaces or brackets. Let's make our life easier and surround brackets by spaces.
var _spdxExpression = spdxExpression.replace("(", " ( ").replace(")", " ) ").split(" ");
if (_spdxExpression.length == 1) {
return new SpdxExpression(spdxExpression);
}

// Shunting yard algorithm to convert SPDX expression to reverse polish notation
// specify list of infix operators
List<String> infixOperators = List.of(SpdxOperator.OR.getToken(), SpdxOperator.AND.getToken(),
SpdxOperator.WITH.getToken());

ArrayDeque<String> operatorStack = new ArrayDeque<>();
ArrayDeque<String> outputQueue = new ArrayDeque<>();
Iterator<String> it = List.of(_spdxExpression).iterator();
while(it.hasNext()) {
var token = it.next();
if (token.length() == 0) {
continue;
}
if (infixOperators.contains(token)) {
int opPrecedence = SpdxOperator.valueOf(token).getPrecedence();
for (String o2; (o2 = operatorStack.peek()) != null && !o2.equals("(")
&& SpdxOperator.valueOf(o2).getPrecedence() > opPrecedence;) {
outputQueue.push(operatorStack.pop());
}
;
operatorStack.push(token);
} else if (token.equals("(")) {
operatorStack.push(token);
} else if (token.equals(")")) {
for (String o2; (o2 = operatorStack.peek()) == null || !o2.equals("(");) {
if (o2 == null) {
// Mismatched parentheses
return SpdxExpression.INVALID;
}
outputQueue.push(operatorStack.pop());
}
;
String leftParens = operatorStack.pop();

if (!"(".equals(leftParens)) {
// Mismatched parentheses
return SpdxExpression.INVALID;
}
// no function tokens implemented
} else {
outputQueue.push(token);
}
}
for (String o2; (o2 = operatorStack.peek()) != null;) {
if ("(".equals(o2)) {
// Mismatched parentheses
return SpdxExpression.INVALID;
}
outputQueue.push(operatorStack.pop());
}

// convert RPN stack into tree
// this is easy because all infix operators have two arguments
ArrayDeque<SpdxExpression> expressions = new ArrayDeque<>();
SpdxExpression expr = null;
while (!outputQueue.isEmpty()) {
var token = outputQueue.pollLast();
if (infixOperators.contains(token)) {
var rhs = expressions.pop();
var lhs = expressions.pop();
expr = new SpdxExpression(SpdxOperator.valueOf(token), List.of(lhs, rhs));
} else {
if (token.endsWith("+")) {
// trailing `+` is not a whitespace-delimited operator - process it separately
expr = new SpdxExpression(SpdxOperator.PLUS,
List.of(new SpdxExpression(token.substring(0, token.length() - 1))));
} else {
expr = new SpdxExpression(token);
}
}
expressions.push(expr);
}
return expr;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
* This file is part of Dependency-Track.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
* Copyright (c) Steve Springett. All Rights Reserved.
*/
package org.dependencytrack.parser.spdx.expression.model;

import java.util.List;

/**
* A node of an SPDX expression tree. If it is a leaf node, it contains a spdxLicenseId. If it is an
* inner node, containss an operation.
*
* @author hborchardt
* @since 4.8.0
*/
public class SpdxExpression {
public static final SpdxExpression INVALID = new SpdxExpression(null);
public SpdxExpression(String spdxLicenseId) {
this.spdxLicenseId = spdxLicenseId;
}

public SpdxExpression(SpdxOperator operator, List<SpdxExpression> arguments) {
this.operation = new SpdxExpressionOperation(operator, arguments);
}

private SpdxExpressionOperation operation;
private String spdxLicenseId;

public SpdxExpressionOperation getOperation() {
return operation;
}

public void setOperation(SpdxExpressionOperation operation) {
this.operation = operation;
}

public String getSpdxLicenseId() {
return spdxLicenseId;
}

public void setSpdxLicenseId(String spdxLicenseId) {
this.spdxLicenseId = spdxLicenseId;
}

@Override
public String toString() {
if (this == INVALID) {
return "INVALID";
}
if (spdxLicenseId != null) {
return spdxLicenseId;
}
return operation.toString();
}
}
Loading