Skip to content
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
7 changes: 6 additions & 1 deletion src/main/java/org/dependencytrack/model/Vulnerability.java
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Objects;
Expand Down Expand Up @@ -107,7 +108,11 @@ public enum Source {
RETIREJS, // Retire.js
INTERNAL, // Internally-managed (and manually entered) vulnerability
OSV, // Google OSV Advisories
SNYK, // Snyk Purl Vulnerability
SNYK; // Snyk Purl Vulnerability

public static boolean isKnownSource(String source) {
return Arrays.stream(values()).anyMatch(enumSource -> enumSource.name().equalsIgnoreCase(source));
}
}

@PrimaryKey
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,17 +42,16 @@ public class CycloneDXVexImporter {

public void applyVex(final QueryManager qm, final Bom bom, final Project project) {
if (bom.getVulnerabilities() == null) return;
for (org.cyclonedx.model.vulnerability.Vulnerability cdxVuln: bom.getVulnerabilities()) {
List<org.cyclonedx.model.vulnerability.Vulnerability> auditableVulnerabilities = bom.getVulnerabilities().stream().filter(
bomVuln -> bomVuln.getSource() == null || Vulnerability.Source.isKnownSource(bomVuln.getSource().getName())
).toList();
for (org.cyclonedx.model.vulnerability.Vulnerability cdxVuln: auditableVulnerabilities) {
if (cdxVuln.getAnalysis() == null) continue;
final List<Vulnerability> vulns = qm.getVulnerabilities(project, true);
if (vulns == null) continue;
for (final Vulnerability vuln: vulns) {
// NOTE: These vulnerability objects are detached
if ((vuln.getSource().equals(Vulnerability.Source.NVD.name())
|| vuln.getSource().equals(Vulnerability.Source.OSSINDEX.name())
|| vuln.getSource().equals(Vulnerability.Source.GITHUB.name())
|| vuln.getSource().equals(Vulnerability.Source.INTERNAL.name()))
&& vuln.getVulnId().equals(cdxVuln.getId())) {
if (shouldAuditVulnerability(cdxVuln, vuln)) {

if (cdxVuln.getAffects() == null) continue;
for (org.cyclonedx.model.vulnerability.Vulnerability.Affect affect: cdxVuln.getAffects()) {
Expand Down Expand Up @@ -81,6 +80,14 @@ public void applyVex(final QueryManager qm, final Bom bom, final Project project
}
}

private boolean shouldAuditVulnerability(org.cyclonedx.model.vulnerability.Vulnerability bomVulnerability, Vulnerability dtVulnerability) {
boolean result = true;
result = result && bomVulnerability.getSource() != null;
result = result && dtVulnerability.getVulnId().equals(bomVulnerability.getId());
result = result && dtVulnerability.getSource().equalsIgnoreCase(bomVulnerability.getSource().getName());
return result;
}

private void updateAnalysis(final QueryManager qm, final Component component, final Vulnerability vuln,
final org.cyclonedx.model.vulnerability.Vulnerability cdxVuln) {
// The vulnerability object is detached, so refresh it.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
package org.dependencytrack.parser.cyclonedx;

import org.assertj.core.api.Assertions;
import org.cyclonedx.BomParserFactory;
import org.cyclonedx.exception.ParseException;
import org.dependencytrack.PersistenceCapableTest;
import org.dependencytrack.model.Analysis;
import org.dependencytrack.model.AnalysisJustification;
import org.dependencytrack.model.AnalysisState;
import org.dependencytrack.model.Component;
import org.dependencytrack.model.Severity;
import org.dependencytrack.model.Vulnerability;
import org.dependencytrack.tasks.scanners.AnalyzerIdentity;
import org.junit.Assert;
import org.junit.Test;

import javax.jdo.Query;
import java.io.IOException;
import java.net.URISyntaxException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.UUID;

public class CycloneDXVexImporterTest extends PersistenceCapableTest {

private CycloneDXVexImporter vexImporter = new CycloneDXVexImporter();

@Test
public void shouldAuditVulnerabilityFromAllSourcesUsingVex() throws URISyntaxException, IOException, ParseException {
// Arrange
var sources = Arrays.asList(Vulnerability.Source.values());
var project = qm.createProject("Acme Example", null, "1.0", null, null, null, true, false);

var component = new Component();
component.setProject(project);
component.setName("Acme Component");
component.setVersion("1.0");
component = qm.createComponent(component, false);

final byte[] vexBytes = Files.readAllBytes(Paths.get(getClass().getClassLoader().getResource("vex-1.json").toURI()));
var parser = BomParserFactory.createParser(vexBytes);
var vex = parser.parse(vexBytes);

List<org.cyclonedx.model.vulnerability.Vulnerability> audits = new LinkedList<>();

var unknownVexSourceVulnerability = new Vulnerability();
unknownVexSourceVulnerability.setVulnId("CVE-2020-25649");
unknownVexSourceVulnerability.setSource(Vulnerability.Source.NVD);
unknownVexSourceVulnerability.setSeverity(Severity.HIGH);
unknownVexSourceVulnerability.setComponents(List.of(component));
unknownVexSourceVulnerability = qm.createVulnerability(unknownVexSourceVulnerability, false);
qm.addVulnerability(unknownVexSourceVulnerability, component, AnalyzerIdentity.NONE);

var mismatchVexSourceVulnerability = new Vulnerability();
mismatchVexSourceVulnerability.setVulnId("CVE-2020-25650");
mismatchVexSourceVulnerability.setSource(Vulnerability.Source.NVD);
mismatchVexSourceVulnerability.setSeverity(Severity.HIGH);
mismatchVexSourceVulnerability.setComponents(List.of(component));
mismatchVexSourceVulnerability = qm.createVulnerability(mismatchVexSourceVulnerability, false);
qm.addVulnerability(mismatchVexSourceVulnerability, component, AnalyzerIdentity.NONE);

var noVexSourceVulnerability = new Vulnerability();
noVexSourceVulnerability.setVulnId("CVE-2020-25651");
noVexSourceVulnerability.setSource(Vulnerability.Source.GITHUB);
noVexSourceVulnerability.setSeverity(Severity.HIGH);
noVexSourceVulnerability.setComponents(List.of(component));
noVexSourceVulnerability = qm.createVulnerability(noVexSourceVulnerability, false);
qm.addVulnerability(noVexSourceVulnerability, component, AnalyzerIdentity.NONE);

// Build vulnerabilities for each available and known vulnerability source
for (var source : sources) {
var vulnId = source.name().toUpperCase()+"-001";
var vulnerability = new Vulnerability();
vulnerability.setVulnId(vulnId);
vulnerability.setSource(source);
vulnerability.setSeverity(Severity.HIGH);
vulnerability.setComponents(List.of(component));
vulnerability = qm.createVulnerability(vulnerability, false);
qm.addVulnerability(vulnerability, component, AnalyzerIdentity.NONE);

var audit = new org.cyclonedx.model.vulnerability.Vulnerability();
audit.setBomRef(UUID.randomUUID().toString());
audit.setId(vulnId);
var auditSource = new org.cyclonedx.model.vulnerability.Vulnerability.Source();
auditSource.setName(source.name());
audit.setSource(auditSource);
var analysis = new org.cyclonedx.model.vulnerability.Vulnerability.Analysis();
analysis.setState(org.cyclonedx.model.vulnerability.Vulnerability.Analysis.State.FALSE_POSITIVE);
analysis.setDetail("Unit test");
analysis.setJustification(org.cyclonedx.model.vulnerability.Vulnerability.Analysis.Justification.PROTECTED_BY_MITIGATING_CONTROL);
audit.setAnalysis(analysis);
var affect = new org.cyclonedx.model.vulnerability.Vulnerability.Affect();
affect.setRef(vex.getMetadata().getComponent().getBomRef());
audit.setAffects(List.of(affect));
audits.add(audit);
}
audits.addAll(vex.getVulnerabilities());
vex.setVulnerabilities(audits);
qm.getPersistenceManager().refreshAll();

// Act
vexImporter.applyVex(qm, vex, project);

// Assert
final Query<Analysis> query = qm.getPersistenceManager().newQuery(Analysis.class, "project == :project");
var analyses = (List<Analysis>) query.execute(project);
// CVE-2020-256[49|50|51] are not audited otherwise analyses.size would have been equal to sources.size()+3
Assert.assertEquals(sources.size(), analyses.size());
Assertions.assertThat(analyses).allSatisfy(analysis -> {
Assertions.assertThat(analysis.getVulnerability().getVulnId()).isNotEqualTo("CVE-2020-25649");
Assertions.assertThat(analysis.getVulnerability().getVulnId()).isNotEqualTo("CVE-2020-25650");
Assertions.assertThat(analysis.isSuppressed()).isTrue();
Assertions.assertThat(analysis.getAnalysisComments().size()).isEqualTo(3);
Assertions.assertThat(analysis.getAnalysisComments()).satisfiesExactlyInAnyOrder(comment -> {
Assertions.assertThat(comment.getCommenter()).isEqualTo("CycloneDX VEX");
Assertions.assertThat(comment.getComment()).isEqualTo(String.format("Analysis: %s → %s", AnalysisState.NOT_SET, AnalysisState.FALSE_POSITIVE));
}, comment -> {
Assertions.assertThat(comment.getCommenter()).isEqualTo("CycloneDX VEX");
Assertions.assertThat(comment.getComment()).isEqualTo("Details: Unit test");
}, comment -> {
Assertions.assertThat(comment.getCommenter()).isEqualTo("CycloneDX VEX");
Assertions.assertThat(comment.getComment()).isEqualTo(String.format("Justification: %s → %s", AnalysisJustification.NOT_SET, AnalysisJustification.PROTECTED_BY_MITIGATING_CONTROL));
});
Assertions.assertThat(analysis.getAnalysisDetails()).isEqualTo("Unit test");
});
}

}
99 changes: 99 additions & 0 deletions src/test/resources/vex-1.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
{
"bomFormat": "CycloneDX",
"specVersion": "1.4",
"serialNumber": "urn:uuid:50f729f5-1e3c-4d10-b0da-d2f1c02ca257",
"version": 1,
"metadata": {
"timestamp": "2023-03-01T00:00:00Z",
"tools": [
{
"vendor": "OWASP",
"name": "Dependency-Track",
"version": "latest"
}
],
"component": {
"name": "Acme example",
"version": "1.0",
"type": "application",
"bom-ref": "7f2ee811-6b35-4c24-83ec-605d7939005c"
}
},
"vulnerabilities": [
{
"id": "CVE-2020-25649",
"source": {
"name": "National Vulnerability Database",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2020-25649"
},
"ratings": [
{
"source": {
"name": "NVD",
"url": "https://nvd.nist.gov/vuln-metrics/cvss/v3-calculator?vector=AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N&version=3.1"
},
"score": 7.5,
"severity": "high",
"method": "CVSSv31",
"vector": "AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N"
}
],
"analysis": {
"state": "not_affected"
},
"affects": [
{
"ref": "7f2ee811-6b35-4c24-83ec-605d7939005c"
}
]
},
{
"id": "CVE-2020-25650",
"source": {
"name": "OSSINDEX",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2020-25650"
},
"ratings": [
{
"source": {
"name": "OSSINDEX"
},
"score": 7.5,
"severity": "high",
"method": "CVSSv31",
"vector": "AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N"
}
],
"analysis": {
"state": "false_positive"
},
"affects": [
{
"ref": "7f2ee811-6b35-4c24-83ec-605d7939005c"
}
]
},
{
"id": "CVE-2020-25651",
"ratings": [
{
"source": {
"name": "OSSINDEX"
},
"score": 7.6,
"severity": "high",
"method": "CVSSv31",
"vector": "AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N"
}
],
"analysis": {
"state": "false_positive"
},
"affects": [
{
"ref": "7f2ee811-6b35-4c24-83ec-605d7939005c"
}
]
}
]
}