diff --git a/src/main/java/org/dependencytrack/model/Vulnerability.java b/src/main/java/org/dependencytrack/model/Vulnerability.java index cf56b3c8c4..9fca02e150 100644 --- a/src/main/java/org/dependencytrack/model/Vulnerability.java +++ b/src/main/java/org/dependencytrack/model/Vulnerability.java @@ -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; @@ -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 diff --git a/src/main/java/org/dependencytrack/parser/cyclonedx/CycloneDXVexImporter.java b/src/main/java/org/dependencytrack/parser/cyclonedx/CycloneDXVexImporter.java index ca469e205a..2fa8519086 100644 --- a/src/main/java/org/dependencytrack/parser/cyclonedx/CycloneDXVexImporter.java +++ b/src/main/java/org/dependencytrack/parser/cyclonedx/CycloneDXVexImporter.java @@ -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 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 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()) { @@ -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. diff --git a/src/test/java/org/dependencytrack/parser/cyclonedx/CycloneDXVexImporterTest.java b/src/test/java/org/dependencytrack/parser/cyclonedx/CycloneDXVexImporterTest.java new file mode 100644 index 0000000000..82a956fdeb --- /dev/null +++ b/src/test/java/org/dependencytrack/parser/cyclonedx/CycloneDXVexImporterTest.java @@ -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 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 query = qm.getPersistenceManager().newQuery(Analysis.class, "project == :project"); + var analyses = (List) 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"); + }); + } + +} diff --git a/src/test/resources/vex-1.json b/src/test/resources/vex-1.json new file mode 100644 index 0000000000..807fdacccb --- /dev/null +++ b/src/test/resources/vex-1.json @@ -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" + } + ] + } + ] +} \ No newline at end of file