Skip to content

Commit 281086b

Browse files
committed
Include KEV flag in notifications
Adds an `is_kev` flag to vulnerability messages in notifications. Besides the information being useful to receivers, it also allows users to leverage notification filter expressions to, for example, only receive notifications for KEV vulns. Signed-off-by: nscuro <nscuro@protonmail.com>
1 parent e4880f9 commit 281086b

12 files changed

Lines changed: 111 additions & 30 deletions

File tree

apiserver/src/main/java/org/dependencytrack/notification/NotificationModelConverter.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -250,7 +250,8 @@ public static Vulnerability convert(org.dependencytrack.model.Vulnerability vuln
250250
final Vulnerability.Builder builder = Vulnerability.newBuilder()
251251
.setUuid(vuln.getUuid().toString())
252252
.setVulnId(vuln.getVulnId())
253-
.setSource(vuln.getSource());
253+
.setSource(vuln.getSource())
254+
.setIsKev(vuln.isKev());
254255

255256
if (vuln.getAliases() != null) {
256257
getUniqueAliases(vuln).stream()

apiserver/src/main/java/org/dependencytrack/persistence/AnalysisQueryManager.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import org.dependencytrack.model.AnalysisState;
2929
import org.dependencytrack.model.Component;
3030
import org.dependencytrack.model.Vulnerability;
31+
import org.dependencytrack.model.VulnerabilityKey;
3132
import org.dependencytrack.notification.JdoNotificationEmitter;
3233
import org.dependencytrack.notification.NotificationModelConverter;
3334
import org.dependencytrack.persistence.command.MakeAnalysisCommand;
@@ -148,6 +149,12 @@ public long makeAnalysis(final MakeAnalysisCommand command) {
148149

149150
if (!command.options().contains(MakeAnalysisCommand.Option.OMIT_NOTIFICATION)
150151
&& (stateChanged || suppressionChanged)) {
152+
// NB: KEV is a transient field and must be computed ad-hoc.
153+
final boolean isKev = !super.getKev(
154+
List.of(VulnerabilityKey.of(
155+
analysis.getVulnerability()))).isEmpty();
156+
analysis.getVulnerability().setKev(isKev);
157+
151158
new JdoNotificationEmitter(this).emit(
152159
createVulnerabilityAnalysisDecisionChangeNotification(
153160
NotificationModelConverter.convert(analysis.getProject()),

apiserver/src/main/java/org/dependencytrack/persistence/QueryManager.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -809,6 +809,10 @@ public List<VulnerabilityAlias> getVulnerabilityAliases(Vulnerability vulnerabil
809809
return getVulnerabilityQueryManager().getVulnerabilityAliases(vulnerability);
810810
}
811811

812+
public Set<VulnerabilityKey> getKev(Collection<VulnerabilityKey> keys) {
813+
return getVulnerabilityQueryManager().getKev(keys);
814+
}
815+
812816
public Analysis getAnalysis(Component component, Vulnerability vulnerability) {
813817
return getAnalysisQueryManager().getAnalysis(component, vulnerability);
814818
}

apiserver/src/main/java/org/dependencytrack/persistence/VulnerabilityQueryManager.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,7 @@ public PaginatedResult getVulnerabilities() {
339339
return result;
340340
}
341341

342+
@Override
342343
public Set<VulnerabilityKey> getKev(Collection<VulnerabilityKey> keys) {
343344
if (keys.isEmpty()) {
344345
return Set.of();

apiserver/src/main/java/org/dependencytrack/persistence/jdbi/NotificationSubjectDao.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ SELECT ARRAY_AGG(DISTINCT t."NAME")
145145
, COALESCE(a."SEVERITY", v."SEVERITY") AS "vulnSeverity"
146146
, STRING_TO_ARRAY(v."CWES", ',') AS "vulnCwes"
147147
, JSONB_VULN_ALIASES(v."SOURCE", v."VULNID") AS "vulnAliasesJson"
148+
, <@sql.isKev vulnSource='v."SOURCE"' vulnId='v."VULNID"'/> AS "vulnIsKev"
148149
FROM UNNEST(:componentIds, :vulnerabilityIds)
149150
AS req(component_id, vulnerability_id)
150151
INNER JOIN "COMPONENTS_VULNERABILITIES" AS cv
@@ -259,6 +260,7 @@ SELECT ARRAY_AGG(DISTINCT t."NAME")
259260
, COALESCE(a."SEVERITY", v."SEVERITY") AS "vulnSeverity"
260261
, STRING_TO_ARRAY(v."CWES", ',') AS "vulnCwes"
261262
, JSONB_VULN_ALIASES(v."SOURCE", v."VULNID") AS "vulnAliasesJson"
263+
, <@sql.isKev vulnSource='v."SOURCE"' vulnId='v."VULNID"'/> AS "vulnIsKev"
262264
FROM "COMPONENT" AS c
263265
INNER JOIN "PROJECT" AS p
264266
ON p."ID" = c."PROJECT_ID"
@@ -405,6 +407,7 @@ SELECT ARRAY_AGG(DISTINCT t."NAME")
405407
, COALESCE(a."SEVERITY", v."SEVERITY") AS "vulnSeverity"
406408
, STRING_TO_ARRAY(v."CWES", ',') AS "vulnCwes"
407409
, JSONB_VULN_ALIASES(v."SOURCE", v."VULNID") AS "vulnAliasesJson"
410+
, <@sql.isKev vulnSource='v."SOURCE"' vulnId='v."VULNID"'/> AS "vulnIsKev"
408411
, req.analysis_state AS "vulnAnalysisState"
409412
, req.suppressed AS "isVulnAnalysisSuppressed"
410413
, format('/api/v1/vulnerability/source/%s/vuln/%s/projects', v."SOURCE", v."VULNID") AS "affectedProjectsApiUrl"
@@ -595,6 +598,7 @@ default Map<FindingKey, Vulnerability> getVulnsByFindingKey(Collection<FindingKe
595598
, COALESCE(a."SEVERITY", v."SEVERITY") AS "vulnSeverity"
596599
, STRING_TO_ARRAY(v."CWES", ',') AS "vulnCwes"
597600
, JSONB_VULN_ALIASES(v."SOURCE", v."VULNID") AS "vulnAliasesJson"
601+
, <@sql.isKev vulnSource='v."SOURCE"' vulnId='v."VULNID"'/> AS "vulnIsKev"
598602
FROM UNNEST(:componentIds, :vulnDbIds)
599603
AS t(component_id, vuln_db_id)
600604
INNER JOIN "VULNERABILITY" AS v

apiserver/src/main/java/org/dependencytrack/persistence/jdbi/mapping/NotificationVulnerabilityRowMapper.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ public Vulnerability map(final ResultSet rs, final StatementContext ctx) throws
6363
maybeSet(rs, "vulnSeverity", ResultSet::getString, builder::setSeverity);
6464
maybeSet(rs, "vulnCwes", NotificationVulnerabilityRowMapper::maybeConvertCwes, builder::addAllCwes);
6565
maybeSet(rs, "vulnAliasesJson", NotificationVulnerabilityRowMapper::maybeConvertAliases, builder::addAllAliases);
66+
maybeSet(rs, "vulnIsKev", ResultSet::getBoolean, builder::setIsKev);
6667
return builder.build();
6768
}
6869

apiserver/src/test/java/org/dependencytrack/persistence/jdbi/NotificationSubjectDaoTest.java

Lines changed: 74 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
import com.google.protobuf.util.JsonFormat;
2222
import org.dependencytrack.PersistenceCapableTest;
23+
import org.dependencytrack.kevdatasource.api.KevAssertion;
2324
import org.dependencytrack.model.Analysis;
2425
import org.dependencytrack.model.AnalysisState;
2526
import org.dependencytrack.model.Component;
@@ -103,9 +104,24 @@ public void testGetForNewVulnerabilities() {
103104
vulnB.setSource(Vulnerability.Source.NVD);
104105
qm.persist(vulnB);
105106

106-
useJdbiTransaction(handle -> new VulnerabilityAliasDao(handle)
107-
.syncAssertions("TEST", new VulnerabilityKey("CVE-100", Vulnerability.Source.NVD),
108-
Set.of(new VulnerabilityKey("GHSA-100", Vulnerability.Source.GITHUB))));
107+
useJdbiTransaction(handle -> {
108+
new VulnerabilityAliasDao(handle)
109+
.syncAssertions(
110+
"TEST",
111+
new VulnerabilityKey("CVE-100", Vulnerability.Source.NVD),
112+
Set.of(new VulnerabilityKey("GHSA-100", Vulnerability.Source.GITHUB)));
113+
handle
114+
.attach(KevDao.class)
115+
.upsertBatch("cisa", List.of(
116+
new KevAssertion(
117+
"NVD",
118+
"CVE-100",
119+
null,
120+
null,
121+
null,
122+
null,
123+
null)));
124+
});
109125

110126
qm.addVulnerability(vulnA, component, "internal");
111127
qm.addVulnerability(vulnB, component, "internal");
@@ -125,7 +141,7 @@ public void testGetForNewVulnerabilities() {
125141
.withMatcher("projectUuid", equalTo(project.getUuid().toString()))
126142
.withMatcher("componentUuid", equalTo(component.getUuid().toString()))
127143
.withMatcher("vulnUuid", equalTo(vulnA.getUuid().toString()))
128-
.isEqualTo("""
144+
.isEqualTo(/* language=JSON */ """
129145
{
130146
"affectedProjects": [
131147
{
@@ -193,7 +209,8 @@ public void testGetForNewVulnerabilities() {
193209
"vulnId": "CVE-100",
194210
"cvssV2Vector": "(AV:N/AC:M/Au:S/C:P/I:P/A:P)",
195211
"cvssV3Vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H",
196-
"owaspRRVector": "(SL:5/M:5/O:2/S:9/ED:4/EE:2/A:7/ID:2/LC:2/LI:2/LAV:7/LAC:9/FD:3/RD:5/NC:0/PV:7)"
212+
"owaspRRVector": "(SL:5/M:5/O:2/S:9/ED:4/EE:2/A:7/ID:2/LC:2/LI:2/LAV:7/LAC:9/FD:3/RD:5/NC:0/PV:7)",
213+
"isKev": true
197214
}
198215
}
199216
"""));
@@ -247,7 +264,7 @@ public void testGetForNewVulnerabilityWithAnalysisRatingOverwrite() throws Excep
247264
.withMatcher("projectUuid", equalTo(project.getUuid().toString()))
248265
.withMatcher("componentUuid", equalTo(component.getUuid().toString()))
249266
.withMatcher("vulnUuid", equalTo(vuln.getUuid().toString()))
250-
.isEqualTo("""
267+
.isEqualTo(/* language=JSON */ """
251268
{
252269
"component": {
253270
"uuid": "${json-unit.matches:componentUuid}",
@@ -270,7 +287,8 @@ public void testGetForNewVulnerabilityWithAnalysisRatingOverwrite() throws Excep
270287
"severity": "CRITICAL",
271288
"cvssV2Vector": "",
272289
"cvssV3Vector": "cvssV3VectorOverwrite",
273-
"owaspRRVector": "owaspRrVector"
290+
"owaspRRVector": "owaspRrVector",
291+
"isKev": false
274292
},
275293
"affectedProjects": [
276294
{
@@ -332,11 +350,25 @@ public void testGetForNewVulnerableDependency() throws Exception {
332350
vulnB.setSource(Vulnerability.Source.NVD);
333351
qm.persist(vulnB);
334352

335-
useJdbiTransaction(handle -> new VulnerabilityAliasDao(handle)
336-
.syncAssertions(
337-
"TEST",
338-
new VulnerabilityKey("CVE-100", Vulnerability.Source.NVD),
339-
Set.of(new VulnerabilityKey("GHSA-100", Vulnerability.Source.GITHUB))));
353+
useJdbiTransaction(handle -> {
354+
new VulnerabilityAliasDao(handle)
355+
.syncAssertions(
356+
"TEST",
357+
new VulnerabilityKey("CVE-100", Vulnerability.Source.NVD),
358+
Set.of(new VulnerabilityKey("GHSA-100", Vulnerability.Source.GITHUB)));
359+
360+
handle
361+
.attach(KevDao.class)
362+
.upsertBatch("cisa", List.of(
363+
new KevAssertion(
364+
"NVD",
365+
"CVE-100",
366+
null,
367+
null,
368+
null,
369+
null,
370+
null)));
371+
});
340372

341373
qm.addVulnerability(vulnA, component, "internal");
342374
qm.addVulnerability(vulnB, component, "internal");
@@ -355,7 +387,7 @@ public void testGetForNewVulnerableDependency() throws Exception {
355387
.withMatcher("projectUuid", equalTo(project.getUuid().toString()))
356388
.withMatcher("componentUuid", equalTo(component.getUuid().toString()))
357389
.withMatcher("vulnUuid", equalTo(vulnA.getUuid().toString()))
358-
.isEqualTo("""
390+
.isEqualTo(/* language=JSON */ """
359391
{
360392
"component": {
361393
"uuid": "${json-unit.matches:componentUuid}",
@@ -410,7 +442,8 @@ public void testGetForNewVulnerableDependency() throws Exception {
410442
"owaspRRVector": "(SL:5/M:5/O:2/S:9/ED:4/EE:2/A:7/ID:2/LC:2/LI:2/LAV:7/LAC:9/FD:3/RD:5/NC:0/PV:7)",
411443
"aliases": [
412444
{"vulnId": "GHSA-100", "source": "GITHUB"}
413-
]
445+
],
446+
"isKev": true
414447
}
415448
]
416449
}
@@ -465,7 +498,7 @@ public void testGetForNewVulnerableDependencyWithAnalysisRatingOverwrite() throw
465498
.withMatcher("projectUuid", equalTo(project.getUuid().toString()))
466499
.withMatcher("componentUuid", equalTo(component.getUuid().toString()))
467500
.withMatcher("vulnUuid", equalTo(vuln.getUuid().toString()))
468-
.isEqualTo("""
501+
.isEqualTo(/* language=JSON */ """
469502
{
470503
"component": {
471504
"uuid": "${json-unit.matches:componentUuid}",
@@ -489,7 +522,8 @@ public void testGetForNewVulnerableDependencyWithAnalysisRatingOverwrite() throw
489522
"severity": "CRITICAL",
490523
"cvssV2Vector": "",
491524
"cvssV3Vector": "cvssV3VectorOverwrite",
492-
"owaspRRVector": "owaspRrVector"
525+
"owaspRRVector": "owaspRrVector",
526+
"isKev": false
493527
}
494528
]
495529
}
@@ -540,9 +574,25 @@ public void testGetForProjectAuditChange() {
540574
vulnA.setCwes(List.of(666, 777));
541575
qm.persist(vulnA);
542576

543-
useJdbiTransaction(handle -> new VulnerabilityAliasDao(handle)
544-
.syncAssertions("TEST", new VulnerabilityKey("CVE-100", Vulnerability.Source.NVD),
545-
Set.of(new VulnerabilityKey("GHSA-100", Vulnerability.Source.GITHUB))));
577+
useJdbiTransaction(handle -> {
578+
new VulnerabilityAliasDao(handle)
579+
.syncAssertions(
580+
"TEST",
581+
new VulnerabilityKey("CVE-100", Vulnerability.Source.NVD),
582+
Set.of(new VulnerabilityKey("GHSA-100", Vulnerability.Source.GITHUB)));
583+
584+
handle
585+
.attach(KevDao.class)
586+
.upsertBatch("cisa", List.of(
587+
new KevAssertion(
588+
"NVD",
589+
"CVE-100",
590+
null,
591+
null,
592+
null,
593+
null,
594+
null)));
595+
});
546596

547597
qm.addVulnerability(vulnA, component, "internal");
548598

@@ -565,7 +615,7 @@ public void testGetForProjectAuditChange() {
565615
.withMatcher("projectUuid", equalTo(project.getUuid().toString()))
566616
.withMatcher("componentUuid", equalTo(component.getUuid().toString()))
567617
.withMatcher("vulnUuid", equalTo(vulnA.getUuid().toString()))
568-
.isEqualTo("""
618+
.isEqualTo(/* language=JSON */ """
569619
{
570620
"component": {
571621
"uuid": "${json-unit.matches:componentUuid}",
@@ -622,7 +672,8 @@ public void testGetForProjectAuditChange() {
622672
],
623673
"cvssV2Vector": "(AV:N/AC:M/Au:S/C:P/I:P/A:P)",
624674
"cvssV3Vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H",
625-
"owaspRRVector": "(SL:5/M:5/O:2/S:9/ED:4/EE:2/A:7/ID:2/LC:2/LI:2/LAV:7/LAC:9/FD:3/RD:5/NC:0/PV:7)"
675+
"owaspRRVector": "(SL:5/M:5/O:2/S:9/ED:4/EE:2/A:7/ID:2/LC:2/LI:2/LAV:7/LAC:9/FD:3/RD:5/NC:0/PV:7)",
676+
"isKev": true
626677
},
627678
"analysis": {
628679
"component": {
@@ -680,7 +731,8 @@ public void testGetForProjectAuditChange() {
680731
],
681732
"cvssV2Vector": "(AV:N/AC:M/Au:S/C:P/I:P/A:P)",
682733
"cvssV3Vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H",
683-
"owaspRRVector": "(SL:5/M:5/O:2/S:9/ED:4/EE:2/A:7/ID:2/LC:2/LI:2/LAV:7/LAC:9/FD:3/RD:5/NC:0/PV:7)"
734+
"owaspRRVector": "(SL:5/M:5/O:2/S:9/ED:4/EE:2/A:7/ID:2/LC:2/LI:2/LAV:7/LAC:9/FD:3/RD:5/NC:0/PV:7)",
735+
"isKev": true
684736
},
685737
"state": "NOT_AFFECTED",
686738
"suppressed": false

e2e/src/test/java/org/dependencytrack/e2e/BomUploadProcessingE2ET.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,8 @@ private void verifyWebhookNotification() {
239239
"source": "INTERNAL",
240240
"cvssv3" : "${json-unit.any-number}",
241241
"severity": "CRITICAL",
242-
"cvssV3Vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H"
242+
"cvssV3Vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H",
243+
"isKev": false
243244
},
244245
"affectedProjectsReference": {
245246
"apiUri": "/api/v1/vulnerability/source/INTERNAL/vuln/INT-123/projects",

e2e/src/test/java/org/dependencytrack/e2e/VulnerabilityPolicyE2ET.java

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -454,7 +454,8 @@ private void verifyNewVulnerableDependencyNotification() {
454454
"source": "INTERNAL",
455455
"cvssv3": 7.1,
456456
"severity": "HIGH",
457-
"cvssV3Vector" : "CVSS:3.0/AV:N/AC:H/PR:L/UI:R/S:U/C:H/I:H/A:H"
457+
"cvssV3Vector" : "CVSS:3.0/AV:N/AC:H/PR:L/UI:R/S:U/C:H/I:H/A:H",
458+
"isKev": false
458459
},
459460
{
460461
"uuid": "${json-unit.any-string}",
@@ -463,7 +464,8 @@ private void verifyNewVulnerableDependencyNotification() {
463464
"cvssv2": 2.6,
464465
"cvssv3": 9.8,
465466
"severity": "LOW",
466-
"cvssV3Vector": "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H"
467+
"cvssV3Vector": "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
468+
"isKev": false
467469
}
468470
]
469471
}
@@ -512,7 +514,8 @@ private void verifyNewVulnerabilityNotifications() {
512514
"source": "INTERNAL",
513515
"cvssv3": 7.1,
514516
"severity": "HIGH",
515-
"cvssV3Vector" : "CVSS:3.0/AV:N/AC:H/PR:L/UI:R/S:U/C:H/I:H/A:H"
517+
"cvssV3Vector" : "CVSS:3.0/AV:N/AC:H/PR:L/UI:R/S:U/C:H/I:H/A:H",
518+
"isKev": false
516519
},
517520
"analysisTrigger" : "ANALYSIS_TRIGGER_BOM_UPLOAD",
518521
"vulnerabilityAnalysisLevel": "BOM_UPLOAD_ANALYSIS",
@@ -574,7 +577,8 @@ private void verifyNewVulnerabilityNotifications() {
574577
"cvssv2": 2.6,
575578
"cvssv3": 9.8,
576579
"severity": "LOW",
577-
"cvssV3Vector": "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H"
580+
"cvssV3Vector": "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
581+
"isKev": false
578582
},
579583
"analysisTrigger" : "ANALYSIS_TRIGGER_BOM_UPLOAD",
580584
"vulnerabilityAnalysisLevel": "BOM_UPLOAD_ANALYSIS",

notification/api/src/main/java/org/dependencytrack/notification/api/TestNotificationFactory.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -446,6 +446,7 @@ private static Vulnerability createVulnerability() {
446446
.addCwes(Vulnerability.Cwe.newBuilder()
447447
.setCweId(777)
448448
.setName("Regular Expression without Anchors"))
449+
.setIsKev(false)
449450
.build();
450451
}
451452

0 commit comments

Comments
 (0)