Skip to content

Commit

Permalink
De-duplicate CPEs in NVD feed file parsing
Browse files Browse the repository at this point in the history
Some CVE records contain duplicate CPEs. The original parsing logic did not de-duplicate those, consequently causing duplicate `VulnerableSoftware` records in the database.

De-duplication was already handled in `NistApiMirrorTask`, but not `NistMirrorTask`.

Relates to DependencyTrack#3663

Signed-off-by: nscuro <[email protected]>
  • Loading branch information
nscuro authored and rkg-mm committed May 5, 2024
1 parent fd5f52c commit 1eaa700
Show file tree
Hide file tree
Showing 4 changed files with 247 additions and 11 deletions.
8 changes: 7 additions & 1 deletion src/main/java/org/dependencytrack/parser/nvd/NvdParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@
import java.util.List;
import java.util.Optional;
import java.util.function.BiConsumer;
import java.util.stream.Collectors;

import static org.dependencytrack.parser.nvd.api20.ModelConverter.distinctIgnoringDatastoreIdentity;

/**
* Parser and processor of NVD data feeds.
Expand Down Expand Up @@ -223,7 +226,10 @@ private void parseCveItem(final ObjectNode cveItem) {
vsList.addAll(reconcile(vulnerableSoftwareInNode, nodeOperator));
}

vulnerabilityConsumer.accept(vulnerability, vsList);
final List<VulnerableSoftware> uniqueVsList = vsList.stream()
.filter(distinctIgnoringDatastoreIdentity())
.collect(Collectors.toList());
vulnerabilityConsumer.accept(vulnerability, uniqueVsList);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ public static List<VulnerableSoftware> convertConfigurations(final String cveId,
.collect(Collectors.toList());
}

private static Predicate<VulnerableSoftware> distinctIgnoringDatastoreIdentity() {
public static Predicate<VulnerableSoftware> distinctIgnoringDatastoreIdentity() {
final var seen = new HashSet<Integer>();
return vs -> seen.add(vs.hashCodeWithoutDatastoreIdentity());
}
Expand Down
61 changes: 52 additions & 9 deletions src/test/java/org/dependencytrack/tasks/NistMirrorTaskTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -68,21 +68,14 @@ public void setUp() {

@Test
public void test() throws Exception {
// Gzip the JSON feed file to match the format returned by the NVD.
// NB: The file is a truncated version of an actual feed file.
// It only contains the first three CVEs. Truncation was done with jq:
// jq 'del(.CVE_Items[3:])' ~/.dependency-track/nist/nvdcve-1.1-2022.json > nvdcve-1.1-2022.json
final var byteArrayOutputStream = new ByteArrayOutputStream();
try (final var gzipOutputStream = new GZIPOutputStream(byteArrayOutputStream)) {
gzipOutputStream.write(resourceToByteArray("/unit/nvd/feed/nvdcve-1.1-2022.json"));
}
final byte[] gzippedFeedFileBytes = gzipResource("/unit/nvd/feed/nvdcve-1.1-2022.json");

wireMock.stubFor(get(anyUrl())
.willReturn(aResponse()
.withStatus(404)));
wireMock.stubFor(get(urlPathEqualTo("/json/cve/1.1/nvdcve-1.1-2022.json.gz"))
.willReturn(aResponse()
.withBody(byteArrayOutputStream.toByteArray())));
.withBody(gzippedFeedFileBytes)));
wireMock.stubFor(get(urlPathEqualTo("/json/cve/1.1/nvdcve-1.1-2022.meta"))
.willReturn(aResponse()
.withBody(resourceToByteArray("/unit/nvd/feed/nvdcve-1.1-2022.meta"))));
Expand Down Expand Up @@ -172,4 +165,54 @@ locks for some Intel(R) Processors in Intel(R) Boot Guard and Intel(R) \
);
}

@Test
public void testWithDuplicateCpes() throws Exception {
final byte[] gzippedFeedFileBytes = gzipResource("/unit/nvd/feed/nvdcve-1.1-2021_duplicate-cpes.json");

wireMock.stubFor(get(anyUrl())
.willReturn(aResponse()
.withStatus(404)));
wireMock.stubFor(get(urlPathEqualTo("/json/cve/1.1/nvdcve-1.1-2021.json.gz"))
.willReturn(aResponse()
.withBody(gzippedFeedFileBytes)));

final Path mirrorDirPath = Files.createTempDirectory(null);
mirrorDirPath.toFile().deleteOnExit();

new NistMirrorTask(mirrorDirPath).inform(new NistMirrorEvent());

final List<Vulnerability> vulns = qm.getVulnerabilities().getList(Vulnerability.class);
assertThat(vulns).hasSize(1);

final Vulnerability vuln = vulns.get(0);
assertThat(vuln.getVulnerableSoftware()).satisfiesExactlyInAnyOrder(
vs -> {
assertThat(vs.getCpe22()).isEqualTo("cpe:/o:intel:ethernet_controller_e810_firmware:::~~~linux~~");
assertThat(vs.getCpe23()).isEqualTo("cpe:2.3:o:intel:ethernet_controller_e810_firmware:*:*:*:*:*:linux:*:*");
},
vs -> {
assertThat(vs.getCpe22()).isEqualTo("cpe:/o:fedoraproject:fedora:33");
assertThat(vs.getCpe23()).isEqualTo("cpe:2.3:o:fedoraproject:fedora:33:*:*:*:*:*:*:*");
},
vs -> {
assertThat(vs.getCpe22()).isEqualTo("cpe:/o:fedoraproject:fedora:34");
assertThat(vs.getCpe23()).isEqualTo("cpe:2.3:o:fedoraproject:fedora:34:*:*:*:*:*:*:*");
},
vs -> {
// This CPE appears twice in the feed file. We must only record it once.
assertThat(vs.getCpe22()).isEqualTo("cpe:/o:fedoraproject:fedora:35");
assertThat(vs.getCpe23()).isEqualTo("cpe:2.3:o:fedoraproject:fedora:35:*:*:*:*:*:*:*");
}
);
}

private byte[] gzipResource(final String resourcePath) throws Exception {
final var byteArrayOutputStream = new ByteArrayOutputStream();
try (final var gzipOutputStream = new GZIPOutputStream(byteArrayOutputStream)) {
gzipOutputStream.write(resourceToByteArray(resourcePath));
}

return byteArrayOutputStream.toByteArray();
}

}
187 changes: 187 additions & 0 deletions src/test/resources/unit/nvd/feed/nvdcve-1.1-2021_duplicate-cpes.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
{
"CVE_data_type": "CVE",
"CVE_data_format": "MITRE",
"CVE_data_version": "4.0",
"CVE_data_numberOfCVEs": "22423",
"CVE_data_timestamp": "2024-04-30T07:00Z",
"CVE_Items": [
{
"cve": {
"data_type": "CVE",
"data_format": "MITRE",
"data_version": "4.0",
"CVE_data_meta": {
"ID": "CVE-2021-0002",
"ASSIGNER": "[email protected]"
},
"problemtype": {
"problemtype_data": [
{
"description": [
{
"lang": "en",
"value": "CWE-754"
}
]
}
]
},
"references": {
"reference_data": [
{
"url": "https://www.intel.com/content/www/us/en/security-center/advisory/intel-sa-00515.html",
"name": "https://www.intel.com/content/www/us/en/security-center/advisory/intel-sa-00515.html",
"refsource": "MISC",
"tags": [
"Vendor Advisory"
]
},
{
"url": "https://security.netapp.com/advisory/ntap-20210827-0008/",
"name": "https://security.netapp.com/advisory/ntap-20210827-0008/",
"refsource": "CONFIRM",
"tags": [
"Third Party Advisory"
]
},
{
"url": "https://lists.fedoraproject.org/archives/list/package-announce%40lists.fedoraproject.org/message/EUZYFCI7N4TFZSIGA7WGZ4Q7V3EK76GH/",
"name": "FEDORA-2021-9807b754d9",
"refsource": "",
"tags": []
},
{
"url": "https://lists.fedoraproject.org/archives/list/package-announce%40lists.fedoraproject.org/message/LKMUMLUH6ENNMLGTJ5AFRF6764ILEMYJ/",
"name": "FEDORA-2021-cbad295a90",
"refsource": "",
"tags": []
},
{
"url": "https://lists.fedoraproject.org/archives/list/package-announce%40lists.fedoraproject.org/message/MFLYHRQPDF6ZMESCI3HRNOP6D6GELPFR/",
"name": "FEDORA-2021-9818cabe0d",
"refsource": "",
"tags": []
}
]
},
"description": {
"description_data": [
{
"lang": "en",
"value": "Improper conditions check in some Intel(R) Ethernet Controllers 800 series Linux drivers before version 1.4.11 may allow an authenticated user to potentially enable information disclosure or denial of service via local access."
}
]
}
},
"configurations": {
"CVE_data_version": "4.0",
"nodes": [
{
"operator": "AND",
"children": [
{
"operator": "OR",
"children": [],
"cpe_match": [
{
"vulnerable": true,
"cpe23Uri": "cpe:2.3:o:intel:ethernet_controller_e810_firmware:*:*:*:*:*:linux:*:*",
"versionEndExcluding": "1.4.11",
"cpe_name": []
}
]
},
{
"operator": "OR",
"children": [],
"cpe_match": [
{
"vulnerable": false,
"cpe23Uri": "cpe:2.3:h:intel:ethernet_controller_e810:-:*:*:*:*:*:*:*",
"cpe_name": []
}
]
}
],
"cpe_match": []
},
{
"operator": "OR",
"children": [],
"cpe_match": [
{
"vulnerable": true,
"cpe23Uri": "cpe:2.3:o:fedoraproject:fedora:33:*:*:*:*:*:*:*",
"cpe_name": []
},
{
"vulnerable": true,
"cpe23Uri": "cpe:2.3:o:fedoraproject:fedora:34:*:*:*:*:*:*:*",
"cpe_name": []
},
{
"vulnerable": true,
"cpe23Uri": "cpe:2.3:o:fedoraproject:fedora:35:*:*:*:*:*:*:*",
"cpe_name": []
}
]
},
{
"operator": "OR",
"children": [],
"cpe_match": [
{
"vulnerable": true,
"cpe23Uri": "cpe:2.3:o:fedoraproject:fedora:35:*:*:*:*:*:*:*",
"cpe_name": []
}
]
}
]
},
"impact": {
"baseMetricV3": {
"cvssV3": {
"version": "3.1",
"vectorString": "CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:H",
"attackVector": "LOCAL",
"attackComplexity": "LOW",
"privilegesRequired": "LOW",
"userInteraction": "NONE",
"scope": "UNCHANGED",
"confidentialityImpact": "HIGH",
"integrityImpact": "NONE",
"availabilityImpact": "HIGH",
"baseScore": 7.1,
"baseSeverity": "HIGH"
},
"exploitabilityScore": 1.8,
"impactScore": 5.2
},
"baseMetricV2": {
"cvssV2": {
"version": "2.0",
"vectorString": "AV:L/AC:L/Au:N/C:P/I:N/A:P",
"accessVector": "LOCAL",
"accessComplexity": "LOW",
"authentication": "NONE",
"confidentialityImpact": "PARTIAL",
"integrityImpact": "NONE",
"availabilityImpact": "PARTIAL",
"baseScore": 3.6
},
"severity": "LOW",
"exploitabilityScore": 3.9,
"impactScore": 4.9,
"acInsufInfo": false,
"obtainAllPrivilege": false,
"obtainUserPrivilege": false,
"obtainOtherPrivilege": false,
"userInteractionRequired": false
}
},
"publishedDate": "2021-08-11T13:15Z",
"lastModifiedDate": "2023-11-07T03:27Z"
}
]
}

0 comments on commit 1eaa700

Please sign in to comment.