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
Original file line number Diff line number Diff line change
Expand Up @@ -100,13 +100,15 @@ private List<Issue> toIssues(String ref, ArrayNode response) {
return issues;
}

// Parse only V3.1 and V3.0 CVSS vectors
// Parse only V3.1, V3.0 and V2 CVSS vectors
private void setMetrics(JsonNode metrics, Issue issue) {
ArrayNode metricsNode = null;
if (metrics.has("cvssMetricV31")) {
metricsNode = (ArrayNode) metrics.get("cvssMetricV31");
} else if (metrics.has("cvssMetricV30")) {
metricsNode = (ArrayNode) metrics.get("cvssMetricV30");
} else if (metrics.has("cvssMetricV2")) {
metricsNode = (ArrayNode) metrics.get("cvssMetricV2");
}
if (metricsNode == null) {
return;
Expand Down
62 changes: 58 additions & 4 deletions src/main/java/com/redhat/exhort/model/CvssParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ public class CvssParser {
private static final record IndexItem(
BiConsumer<String, CvssVector> setter, Map<String, String> parameters) {}

private static final String V3_1 = "CVSS:3.1";
private static final String V3_0 = "CVSS:3.0";

private static final Map<String, IndexItem> INDEX = new HashMap<>();
private static final Map<String, String> ATTACK_VECTORS = new HashMap<>();
private static final Map<String, String> ATTACK_COMPLEXITY = new HashMap<>();
Expand All @@ -44,6 +47,13 @@ private static final record IndexItem(
private static final Map<String, String> REMEDIATION_LEVEL = new HashMap<>();
private static final Map<String, String> REPORT_CONFIDENCE = new HashMap<>();

private static final Map<String, IndexItem> INDEX_V2 = new HashMap<>();
private static final Map<String, String> AUTHENTICATION_V2 = new HashMap<>();
private static final Map<String, String> CONFIDENTIALITY_V2 = new HashMap<>();
private static final Map<String, String> EXPLOIT_CODE_MATURITY_V2 = new HashMap<>();
private static final Map<String, String> REMEDIATION_LEVEL_V2 = new HashMap<>();
private static final Map<String, String> REPORT_CONFIDENCE_V2 = new HashMap<>();

static {
INDEX.put("AV", new IndexItem((v, b) -> b.attackVector(v), ATTACK_VECTORS));
INDEX.put("AC", new IndexItem((v, b) -> b.attackComplexity(v), ATTACK_COMPLEXITY));
Expand Down Expand Up @@ -95,6 +105,41 @@ private static final record IndexItem(
REPORT_CONFIDENCE.put("U", "Unknown");
REPORT_CONFIDENCE.put("R", "Reasonable");
REPORT_CONFIDENCE.put("C", "Confirmed");

INDEX_V2.put("AV", INDEX.get("AV"));
INDEX_V2.put("AC", INDEX.get("AC"));
INDEX_V2.put("Au", new IndexItem((v, b) -> b.privilegesRequired(v), AUTHENTICATION_V2));
INDEX_V2.put("C", new IndexItem((v, b) -> b.confidentialityImpact(v), CONFIDENTIALITY_V2));
INDEX_V2.put("I", new IndexItem((v, b) -> b.integrityImpact(v), CONFIDENTIALITY_V2));
INDEX_V2.put("A", new IndexItem((v, b) -> b.availabilityImpact(v), CONFIDENTIALITY_V2));
INDEX_V2.put("E", new IndexItem((v, b) -> b.exploitCodeMaturity(v), EXPLOIT_CODE_MATURITY_V2));
INDEX_V2.put("RL", new IndexItem((v, b) -> b.remediationLevel(v), REMEDIATION_LEVEL_V2));
INDEX_V2.put("RC", new IndexItem((v, b) -> b.reportConfidence(v), REPORT_CONFIDENCE_V2));

AUTHENTICATION_V2.put("M", "High"); // Multiple -> High
AUTHENTICATION_V2.put("S", "Low"); // Simple -> Low
AUTHENTICATION_V2.put("N", "None");

CONFIDENTIALITY_V2.put("P", "Low"); // Partial -> Low
CONFIDENTIALITY_V2.put("C", "High"); // Complete -> High
CONFIDENTIALITY_V2.put("N", "None");

EXPLOIT_CODE_MATURITY_V2.put("ND", "Not Defined");
EXPLOIT_CODE_MATURITY_V2.put("U", "Unproven that exploit exists");
EXPLOIT_CODE_MATURITY_V2.put("P", "Proof of concept code");
EXPLOIT_CODE_MATURITY_V2.put("F", "Functional exploit exists");
EXPLOIT_CODE_MATURITY_V2.put("H", "High");

REMEDIATION_LEVEL_V2.put("ND", "Not Defined");
REMEDIATION_LEVEL_V2.put("OF", "Official fix");
REMEDIATION_LEVEL_V2.put("TF", "Temporary fix");
REMEDIATION_LEVEL_V2.put("W", "Workaround");
REMEDIATION_LEVEL_V2.put("U", "Unavailable");

REPORT_CONFIDENCE_V2.put("ND", "Not Defined");
REPORT_CONFIDENCE_V2.put("UC", "Unknown");
REPORT_CONFIDENCE_V2.put("UR", "Reasonable");
REPORT_CONFIDENCE_V2.put("C", "Confirmed");
}

public static CvssVector fromVectorString(String vector) {
Expand All @@ -103,10 +148,19 @@ public static CvssVector fromVectorString(String vector) {
var parts = vector.split("/");
for (int i = 0; i < parts.length; i++) {
var metrics = parts[i].split(":");
if (metrics.length == 2 && INDEX.containsKey(metrics[0])) {
var item = INDEX.get(metrics[0]);
var value = item.parameters().get(metrics[1]);
item.setter().accept(value, result);
if (vector.startsWith(V3_1) || vector.startsWith(V3_0)) {
if (metrics.length == 2 && INDEX.containsKey(metrics[0])) {
var item = INDEX.get(metrics[0]);
var value = item.parameters().get(metrics[1]);
item.setter().accept(value, result);
}
} else {
// Parse CVSS 2.0
if (metrics.length == 2 && INDEX_V2.containsKey(metrics[0])) {
var item = INDEX_V2.get(metrics[0]);
var value = item.parameters().get(metrics[1]);
item.setter().accept(value, result);
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/*
* Copyright 2024 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* 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.
*/

package com.redhat.exhort.integration.providers.osvnvd;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.io.IOException;
import java.net.URISyntaxException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashMap;

import org.junit.jupiter.api.Test;

import com.redhat.exhort.api.PackageRef;
import com.redhat.exhort.model.DependencyTree;
import com.redhat.exhort.model.DirectDependency;

import io.quarkus.test.junit.QuarkusTest;

import jakarta.inject.Inject;

@QuarkusTest
public class OsvNvdResponseHandlerTest {

@Inject OsvNvdResponseHandler handler;

@Test
void testVectors() throws IOException, URISyntaxException {
var providerResponse = getProviderResponse("osvnvd/maven_report.json");
var postgresRef =
PackageRef.builder().purl("pkg:maven/org.postgresql/[email protected]?type=jar").build();
var jacksonRef =
PackageRef.builder()
.purl("pkg:maven/com.fasterxml.jackson.core/[email protected]?type=jar")
.build();
var deps = new HashMap<PackageRef, DirectDependency>();
deps.put(postgresRef, new DirectDependency(postgresRef, null));
deps.put(jacksonRef, new DirectDependency(jacksonRef, null));
var dependencyTree = new DependencyTree(deps);

var report = handler.responseToIssues(providerResponse, null, dependencyTree);

assertFalse(report.issues().isEmpty());
assertEquals(2, report.issues().size());
var jacksonIssues = report.issues().get(jacksonRef.ref());
assertEquals(3, jacksonIssues.size());

// Test V3.1 vector.
var issue =
jacksonIssues.stream().filter(i -> i.getCves().contains("CVE-2022-42004")).findFirst();
assertTrue(issue.isPresent());
assertEquals(7.5f, issue.get().getCvssScore());
assertEquals("CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H", issue.get().getCvss().getCvss());

// Test V3.0 vector.
issue = jacksonIssues.stream().filter(i -> i.getCves().contains("CVE-2022-42003")).findFirst();
assertTrue(issue.isPresent());
assertEquals(7.5f, issue.get().getCvssScore());
assertEquals("CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H", issue.get().getCvss().getCvss());

// Test V2.0 vector.
issue = jacksonIssues.stream().filter(i -> i.getCves().contains("CVE-2020-36518")).findFirst();
assertTrue(issue.isPresent());
assertEquals(5.0f, issue.get().getCvssScore());
assertEquals("AV:N/AC:L/Au:N/C:N/I:N/A:P", issue.get().getCvss().getCvss());
}

private byte[] getProviderResponse(String fileName) throws IOException, URISyntaxException {
return Files.readAllBytes(
Path.of(this.getClass().getClassLoader().getResource("__files/" + fileName).toURI()));
}
}
47 changes: 44 additions & 3 deletions src/test/java/com/redhat/exhort/model/CvssParserTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ public class CvssParserTest {

private static final String[] INPUTS = {
"CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:H",
"CVSS:3.1/AV:A/AC:H/PR:L/UI:R/S:U/C:H/I:L/A:L/E:U/RL:U/RC:R"
"CVSS:3.1/AV:A/AC:H/PR:L/UI:R/S:U/C:H/I:L/A:L/E:U/RL:U/RC:R",
"CVSS:3.0/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:H",
"CVSS:3.0/AV:A/AC:H/PR:L/UI:R/S:U/C:H/I:L/A:L/E:U/RL:U/RC:R",
};

private static final CvssVector[] EXPECTATIONS = {
Expand Down Expand Up @@ -58,9 +60,48 @@ public class CvssParserTest {
};

@Test
void testVectors() {
void testVectorsV3() {
for (int i = 0; i < INPUTS.length; i++) {
assertEquals(EXPECTATIONS[i], CvssParser.fromVectorString(INPUTS[i]), "Failed: " + INPUTS[i]);
var expectation = EXPECTATIONS[i % 2].cvss(INPUTS[i]);
assertEquals(expectation, CvssParser.fromVectorString(INPUTS[i]), "Failed: " + INPUTS[i]);
}
}

private static final String[] INPUTS_V2 = {
"AV:N/AC:L/Au:N/C:N/I:N/A:P", "AV:A/AC:H/Au:S/C:P/I:P/A:C"
};

private static final CvssVector[] EXPECTATIONS_V2 = {
new CvssVector()
.attackVector("Network")
.attackComplexity("Low")
.privilegesRequired("None")
.confidentialityImpact("None")
.integrityImpact("None")
.availabilityImpact("Low")
.userInteraction(null)
.scope(null)
.cvss(INPUTS_V2[0]),
new CvssVector()
.attackVector("Adjacent Network")
.attackComplexity("High")
.privilegesRequired("Low")
.confidentialityImpact("Low")
.integrityImpact("Low")
.availabilityImpact("High")
.userInteraction(null)
.scope(null)
.exploitCodeMaturity(null)
.remediationLevel(null)
.reportConfidence(null)
.cvss(INPUTS_V2[1])
};

@Test
void testVectorsV2() {
for (int i = 0; i < INPUTS_V2.length; i++) {
assertEquals(
EXPECTATIONS_V2[i], CvssParser.fromVectorString(INPUTS_V2[i]), "Failed: " + INPUTS_V2[i]);
}
}
}
28 changes: 3 additions & 25 deletions src/test/resources/__files/osvnvd/maven_report.json
Original file line number Diff line number Diff line change
Expand Up @@ -408,28 +408,6 @@
}
],
"metrics": {
"cvssMetricV31": [
{
"source": "[email protected]",
"type": "Primary",
"cvssData": {
"version": "3.1",
"vectorString": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H",
"attackVector": "NETWORK",
"attackComplexity": "LOW",
"privilegesRequired": "NONE",
"userInteraction": "NONE",
"scope": "UNCHANGED",
"confidentialityImpact": "NONE",
"integrityImpact": "NONE",
"availabilityImpact": "HIGH",
"baseScore": 7.5,
"baseSeverity": "HIGH"
},
"exploitabilityScore": 3.9,
"impactScore": 3.6
}
],
"cvssMetricV2": [
{
"source": "[email protected]",
Expand Down Expand Up @@ -635,13 +613,13 @@
}
],
"metrics": {
"cvssMetricV31": [
"cvssMetricV30": [
{
"source": "[email protected]",
"type": "Primary",
"cvssData": {
"version": "3.1",
"vectorString": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H",
"version": "3.0",
"vectorString": "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H",
"attackVector": "NETWORK",
"attackComplexity": "LOW",
"privilegesRequired": "NONE",
Expand Down
Loading