From aa8d467cc3ce211a3f239075bb8397e55ed422f1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 18 Jan 2026 18:01:31 +0000 Subject: [PATCH 1/3] Initial plan From b49cfc9890dbcddc92082c77cbeda5885f8ed298 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 18 Jan 2026 18:10:41 +0000 Subject: [PATCH 2/3] Add OWASP Risk Rating support from CycloneDX VEX Co-authored-by: fahedouch <11649303+fahedouch@users.noreply.github.com> --- .../cyclonedx/CycloneDXVexImporter.java | 5 + .../parser/cyclonedx/util/ModelConverter.java | 46 ++++++++++ .../cyclonedx/CycloneDXVexImporterTest.java | 60 ++++++++++++ .../resources/vex-with-owasp-ratings.json | 92 +++++++++++++++++++ 4 files changed, 203 insertions(+) create mode 100644 src/test/resources/vex-with-owasp-ratings.json diff --git a/src/main/java/org/dependencytrack/parser/cyclonedx/CycloneDXVexImporter.java b/src/main/java/org/dependencytrack/parser/cyclonedx/CycloneDXVexImporter.java index e7729e55b8..c144b428b0 100644 --- a/src/main/java/org/dependencytrack/parser/cyclonedx/CycloneDXVexImporter.java +++ b/src/main/java/org/dependencytrack/parser/cyclonedx/CycloneDXVexImporter.java @@ -170,5 +170,10 @@ private static void updateAnalysis(final QueryManager qm, final Component compon } } qm.makeAnalysis(component, refreshedVuln, analysisState, analysisJustification, analysisResponse, analysisDetails, suppress); + + // Process OWASP Risk Rating from VEX ratings + if (cdxVuln.getRatings() != null && !cdxVuln.getRatings().isEmpty()) { + ModelConverter.applyOwaspRatingFromCdxRatings(refreshedVuln, cdxVuln.getRatings()); + } } } diff --git a/src/main/java/org/dependencytrack/parser/cyclonedx/util/ModelConverter.java b/src/main/java/org/dependencytrack/parser/cyclonedx/util/ModelConverter.java index 2e7f462b5e..8f97308900 100644 --- a/src/main/java/org/dependencytrack/parser/cyclonedx/util/ModelConverter.java +++ b/src/main/java/org/dependencytrack/parser/cyclonedx/util/ModelConverter.java @@ -1187,5 +1187,51 @@ public static AnalysisJustification convertCdxVulnAnalysisJustificationToDtAnaly return AnalysisJustification.NOT_SET; } } + + /** + * Applies OWASP Risk Rating from CycloneDX vulnerability ratings to a Dependency-Track vulnerability. + * This method searches for an OWASP rating in the provided ratings list and applies it to the vulnerability. + * + * @param vulnerability the Dependency-Track vulnerability to update + * @param ratings the list of CycloneDX ratings from the VEX document + */ + public static void applyOwaspRatingFromCdxRatings(final Vulnerability vulnerability, + final List ratings) { + if (ratings == null || ratings.isEmpty()) { + return; + } + + // Find the OWASP rating + org.cyclonedx.model.vulnerability.Vulnerability.Rating owaspRating = null; + for (org.cyclonedx.model.vulnerability.Vulnerability.Rating rating : ratings) { + if (rating.getMethod() == org.cyclonedx.model.vulnerability.Vulnerability.Rating.Method.OWASP) { + owaspRating = rating; + break; + } + } + + if (owaspRating == null || owaspRating.getVector() == null) { + return; + } + + try { + // Parse the OWASP RR vector and extract scores + final us.springett.owasp.riskrating.OwaspRiskRating rr = + us.springett.owasp.riskrating.OwaspRiskRating.fromVector(owaspRating.getVector()); + final us.springett.owasp.riskrating.Score score = rr.calculateScore(); + + // Set the OWASP RR scores on the vulnerability + vulnerability.setOwaspRRVector(owaspRating.getVector()); + vulnerability.setOwaspRRLikelihoodScore(java.math.BigDecimal.valueOf(score.getLikelihoodScore())); + vulnerability.setOwaspRRTechnicalImpactScore(java.math.BigDecimal.valueOf(score.getTechnicalImpactScore())); + vulnerability.setOwaspRRBusinessImpactScore(java.math.BigDecimal.valueOf(score.getBusinessImpactScore())); + + LOGGER.info("Applied OWASP Risk Rating from VEX: vector=%s, likelihood=%.1f, technical=%.1f, business=%.1f" + .formatted(owaspRating.getVector(), score.getLikelihoodScore(), + score.getTechnicalImpactScore(), score.getBusinessImpactScore())); + } catch (IllegalArgumentException | us.springett.owasp.riskrating.MissingFactorException e) { + LOGGER.warn("Failed to parse OWASP RR vector from VEX: %s - %s".formatted(owaspRating.getVector(), e.getMessage())); + } + } } diff --git a/src/test/java/org/dependencytrack/parser/cyclonedx/CycloneDXVexImporterTest.java b/src/test/java/org/dependencytrack/parser/cyclonedx/CycloneDXVexImporterTest.java index 0b98ddf239..678a40b2c7 100644 --- a/src/test/java/org/dependencytrack/parser/cyclonedx/CycloneDXVexImporterTest.java +++ b/src/test/java/org/dependencytrack/parser/cyclonedx/CycloneDXVexImporterTest.java @@ -128,4 +128,64 @@ void shouldAuditVulnerabilityFromAllSourcesUsingVex() throws URISyntaxException, }); } + @Test + void shouldApplyOwaspRatingsFromVex() throws URISyntaxException, IOException, ParseException { + // Arrange + var project = qm.createProject("Acme Application", null, "2.0", null, null, null, true, false); + + var component = new Component(); + component.setProject(project); + component.setName("Acme Component"); + component.setVersion("2.0"); + component = qm.createComponent(component, false); + + // Create vulnerabilities that will receive OWASP ratings + var vuln1 = new Vulnerability(); + vuln1.setVulnId("CVE-2024-12345"); + vuln1.setSource(Vulnerability.Source.NVD); + vuln1.setSeverity(Severity.HIGH); + vuln1.setComponents(List.of(component)); + vuln1 = qm.createVulnerability(vuln1, false); + qm.addVulnerability(vuln1, component, AnalyzerIdentity.NONE); + + var vuln2 = new Vulnerability(); + vuln2.setVulnId("CVE-2024-54321"); + vuln2.setSource(Vulnerability.Source.NVD); + vuln2.setSeverity(Severity.CRITICAL); + vuln2.setComponents(List.of(component)); + vuln2 = qm.createVulnerability(vuln2, false); + qm.addVulnerability(vuln2, component, AnalyzerIdentity.NONE); + + // Load VEX with OWASP ratings + final byte[] vexBytes = Files.readAllBytes(Paths.get(getClass().getClassLoader().getResource("vex-with-owasp-ratings.json").toURI())); + var parser = BomParserFactory.createParser(vexBytes); + var vex = parser.parse(vexBytes); + + qm.getPersistenceManager().refreshAll(); + + // Act + vexImporter.applyVex(qm, vex, project); + qm.getPersistenceManager().refreshAll(); + + // Assert + var refreshedVuln1 = qm.getVulnerabilityByVulnId(Vulnerability.Source.NVD.name(), "CVE-2024-12345"); + Assertions.assertThat(refreshedVuln1).isNotNull(); + Assertions.assertThat(refreshedVuln1.getOwaspRRVector()).isNotNull(); + Assertions.assertThat(refreshedVuln1.getOwaspRRVector()).isEqualTo("SL:1/M:1/O:0/S:2/ED:1/EE:1/A:1/ID:1/LC:2/LI:1/LAV:1/LAC:1/FD:1/RD:1/NC:2/PV:2"); + Assertions.assertThat(refreshedVuln1.getOwaspRRLikelihoodScore()).isNotNull(); + Assertions.assertThat(refreshedVuln1.getOwaspRRTechnicalImpactScore()).isNotNull(); + Assertions.assertThat(refreshedVuln1.getOwaspRRBusinessImpactScore()).isNotNull(); + + var refreshedVuln2 = qm.getVulnerabilityByVulnId(Vulnerability.Source.NVD.name(), "CVE-2024-54321"); + Assertions.assertThat(refreshedVuln2).isNotNull(); + Assertions.assertThat(refreshedVuln2.getOwaspRRVector()).isNotNull(); + Assertions.assertThat(refreshedVuln2.getOwaspRRVector()).isEqualTo("SL:5/M:5/O:5/S:9/ED:3/EE:3/A:9/ID:9/LC:9/LI:9/LAV:9/LAC:9/FD:9/RD:9/NC:7/PV:9"); + Assertions.assertThat(refreshedVuln2.getOwaspRRLikelihoodScore()).isNotNull(); + Assertions.assertThat(refreshedVuln2.getOwaspRRTechnicalImpactScore()).isNotNull(); + Assertions.assertThat(refreshedVuln2.getOwaspRRBusinessImpactScore()).isNotNull(); + // Verify that scores are higher for the critical vulnerability + Assertions.assertThat(refreshedVuln2.getOwaspRRLikelihoodScore()) + .isGreaterThan(refreshedVuln1.getOwaspRRLikelihoodScore()); + } + } diff --git a/src/test/resources/vex-with-owasp-ratings.json b/src/test/resources/vex-with-owasp-ratings.json new file mode 100644 index 0000000000..645f101055 --- /dev/null +++ b/src/test/resources/vex-with-owasp-ratings.json @@ -0,0 +1,92 @@ +{ + "bomFormat": "CycloneDX", + "specVersion": "1.6", + "serialNumber": "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79", + "version": 1, + "metadata": { + "timestamp": "2024-01-15T00:00:00Z", + "tools": [ + { + "vendor": "VENS", + "name": "Vens CLI", + "version": "1.0.0" + } + ], + "component": { + "name": "Acme Application", + "version": "2.0", + "type": "application", + "bom-ref": "app-2.0" + } + }, + "vulnerabilities": [ + { + "id": "CVE-2024-12345", + "source": { + "name": "NVD", + "url": "https://nvd.nist.gov/vuln/detail/CVE-2024-12345" + }, + "ratings": [ + { + "source": { + "name": "NVD" + }, + "score": 7.5, + "severity": "high", + "method": "CVSSv31", + "vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N" + }, + { + "source": { + "name": "VENS" + }, + "method": "OWASP", + "vector": "SL:1/M:1/O:0/S:2/ED:1/EE:1/A:1/ID:1/LC:2/LI:1/LAV:1/LAC:1/FD:1/RD:1/NC:2/PV:2" + } + ], + "analysis": { + "state": "exploitable", + "detail": "This vulnerability has been analyzed using OWASP Risk Rating methodology" + }, + "affects": [ + { + "ref": "app-2.0" + } + ] + }, + { + "id": "CVE-2024-54321", + "source": { + "name": "NVD", + "url": "https://nvd.nist.gov/vuln/detail/CVE-2024-54321" + }, + "ratings": [ + { + "source": { + "name": "NVD" + }, + "score": 9.8, + "severity": "critical", + "method": "CVSSv31", + "vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H" + }, + { + "source": { + "name": "VENS" + }, + "method": "OWASP", + "vector": "SL:5/M:5/O:5/S:9/ED:3/EE:3/A:9/ID:9/LC:9/LI:9/LAV:9/LAC:9/FD:9/RD:9/NC:7/PV:9" + } + ], + "analysis": { + "state": "in_triage", + "detail": "Critical vulnerability with high OWASP risk rating" + }, + "affects": [ + { + "ref": "app-2.0" + } + ] + } + ] +} From e681efc4cccd4873b7d5dd54d206e21db729aee3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 18 Jan 2026 18:13:00 +0000 Subject: [PATCH 3/3] Address code review feedback - use stream API and DEBUG logging Co-authored-by: fahedouch <11649303+fahedouch@users.noreply.github.com> --- .../parser/cyclonedx/util/ModelConverter.java | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/dependencytrack/parser/cyclonedx/util/ModelConverter.java b/src/main/java/org/dependencytrack/parser/cyclonedx/util/ModelConverter.java index 8f97308900..d017c400f3 100644 --- a/src/main/java/org/dependencytrack/parser/cyclonedx/util/ModelConverter.java +++ b/src/main/java/org/dependencytrack/parser/cyclonedx/util/ModelConverter.java @@ -1202,13 +1202,10 @@ public static void applyOwaspRatingFromCdxRatings(final Vulnerability vulnerabil } // Find the OWASP rating - org.cyclonedx.model.vulnerability.Vulnerability.Rating owaspRating = null; - for (org.cyclonedx.model.vulnerability.Vulnerability.Rating rating : ratings) { - if (rating.getMethod() == org.cyclonedx.model.vulnerability.Vulnerability.Rating.Method.OWASP) { - owaspRating = rating; - break; - } - } + final org.cyclonedx.model.vulnerability.Vulnerability.Rating owaspRating = ratings.stream() + .filter(rating -> rating.getMethod() == org.cyclonedx.model.vulnerability.Vulnerability.Rating.Method.OWASP) + .findFirst() + .orElse(null); if (owaspRating == null || owaspRating.getVector() == null) { return; @@ -1226,7 +1223,7 @@ public static void applyOwaspRatingFromCdxRatings(final Vulnerability vulnerabil vulnerability.setOwaspRRTechnicalImpactScore(java.math.BigDecimal.valueOf(score.getTechnicalImpactScore())); vulnerability.setOwaspRRBusinessImpactScore(java.math.BigDecimal.valueOf(score.getBusinessImpactScore())); - LOGGER.info("Applied OWASP Risk Rating from VEX: vector=%s, likelihood=%.1f, technical=%.1f, business=%.1f" + LOGGER.debug("Applied OWASP Risk Rating from VEX: vector=%s, likelihood=%.1f, technical=%.1f, business=%.1f" .formatted(owaspRating.getVector(), score.getLikelihoodScore(), score.getTechnicalImpactScore(), score.getBusinessImpactScore())); } catch (IllegalArgumentException | us.springett.owasp.riskrating.MissingFactorException e) {