diff --git a/src/main/java/org/dependencytrack/event/kafka/processor/VulnerabilityScanResultProcessor.java b/src/main/java/org/dependencytrack/event/kafka/processor/VulnerabilityScanResultProcessor.java index 7e01409be..3ad371b95 100644 --- a/src/main/java/org/dependencytrack/event/kafka/processor/VulnerabilityScanResultProcessor.java +++ b/src/main/java/org/dependencytrack/event/kafka/processor/VulnerabilityScanResultProcessor.java @@ -77,6 +77,7 @@ import static org.dependencytrack.persistence.jdbi.JdbiFactory.jdbi; import static org.dependencytrack.proto.notification.v1.Group.GROUP_NEW_VULNERABILITY; import static org.dependencytrack.proto.notification.v1.Group.GROUP_NEW_VULNERABLE_DEPENDENCY; +import static org.dependencytrack.proto.notification.v1.Group.GROUP_PROJECT_AUDIT_CHANGE; import static org.dependencytrack.proto.notification.v1.Level.LEVEL_INFORMATIONAL; import static org.dependencytrack.proto.notification.v1.Scope.SCOPE_PORTFOLIO; import static org.dependencytrack.proto.vulnanalysis.v1.ScanStatus.SCAN_STATUS_FAILED; @@ -626,12 +627,12 @@ private List maybeApplyPolicyAnalyses(QueryManager qm, final Dao private org.dependencytrack.proto.notification.v1.Notification createAndAddProjectAuditChangeNotification(QueryManager qm, Component component, Vulnerability vuln, Analysis policyAnalysis, boolean analysisStateChange, boolean suppressionChange) { final Timestamp notificationTimestamp = Timestamps.now(); - AtomicReference notification = null; - jdbi(qm).useExtension(NotificationSubjectDao.class, dao -> dao.getForProjectAuditChange(component.uuid(), vuln.getUuid()) + AtomicReference notification = new AtomicReference<>(); + jdbi(qm).useExtension(NotificationSubjectDao.class, dao -> dao.getForProjectAuditChange(component.uuid(), vuln.getUuid(), policyAnalysis.state, policyAnalysis.suppressed) .map(subject -> { notification.set(org.dependencytrack.proto.notification.v1.Notification.newBuilder() .setScope(SCOPE_PORTFOLIO) - .setGroup(GROUP_NEW_VULNERABILITY) + .setGroup(GROUP_PROJECT_AUDIT_CHANGE) .setLevel(LEVEL_INFORMATIONAL) .setTimestamp(notificationTimestamp) .setTitle(generateTitle(policyAnalysis.getState(), policyAnalysis.getSuppressed(), analysisStateChange, suppressionChange)) diff --git a/src/main/java/org/dependencytrack/persistence/jdbi/NotificationSubjectDao.java b/src/main/java/org/dependencytrack/persistence/jdbi/NotificationSubjectDao.java index e0bc1f6f0..843ea6896 100644 --- a/src/main/java/org/dependencytrack/persistence/jdbi/NotificationSubjectDao.java +++ b/src/main/java/org/dependencytrack/persistence/jdbi/NotificationSubjectDao.java @@ -1,5 +1,6 @@ package org.dependencytrack.persistence.jdbi; +import org.dependencytrack.model.AnalysisState; import org.dependencytrack.model.VulnerabilityAnalysisLevel; import org.dependencytrack.persistence.jdbi.mapping.NotificationComponentRowMapper; import org.dependencytrack.persistence.jdbi.mapping.NotificationProjectRowMapper; @@ -299,8 +300,8 @@ LEFT JOIN LATERAL ( ) AS "vulnSeverity", STRING_TO_ARRAY("V"."CWES", ',') AS "vulnCwes", "vulnAliasesJson", - "A"."SUPPRESSED" AS "isVulnAnalysisSuppressed", - "A"."STATE" AS "vulnAnalysisState", + :isSuppressed AS "isVulnAnalysisSuppressed", + :analysisState AS "vulnAnalysisState", '/api/v1/vulnerability/source/' || "V"."SOURCE" || '/vuln/' || "V"."VULNID" || '/projects' AS "affectedProjectsApiUrl", '/vulnerabilities/' || "V"."SOURCE" || '/' || "V"."VULNID" || '/affectedProjects' AS "affectedProjectsFrontendUrl" FROM @@ -341,5 +342,6 @@ LEFT JOIN LATERAL ( "C"."UUID" = (:componentUuid)::TEXT AND "V"."UUID" = (:vulnUuid)::TEXT """) @RegisterRowMapper(NotificationSubjectProjectAuditChangeRowMapper.class) - Optional getForProjectAuditChange(final UUID componentUuid, final UUID vulnUuid); + + Optional getForProjectAuditChange(final UUID componentUuid, final UUID vulnUuid, AnalysisState analysisState, boolean isSuppressed); } diff --git a/src/test/java/org/dependencytrack/event/kafka/processor/VulnerabilityScanResultProcessorTest.java b/src/test/java/org/dependencytrack/event/kafka/processor/VulnerabilityScanResultProcessorTest.java index 4945ca0b4..913cbc6fb 100644 --- a/src/test/java/org/dependencytrack/event/kafka/processor/VulnerabilityScanResultProcessorTest.java +++ b/src/test/java/org/dependencytrack/event/kafka/processor/VulnerabilityScanResultProcessorTest.java @@ -52,6 +52,7 @@ import org.dependencytrack.proto.notification.v1.NewVulnerabilitySubject; import org.dependencytrack.proto.notification.v1.NewVulnerableDependencySubject; import org.dependencytrack.proto.notification.v1.Notification; +import org.dependencytrack.proto.notification.v1.VulnerabilityAnalysisDecisionChangeSubject; import org.dependencytrack.proto.vulnanalysis.v1.ScanKey; import org.dependencytrack.proto.vulnanalysis.v1.ScanResult; import org.dependencytrack.proto.vulnanalysis.v1.Scanner; @@ -75,6 +76,7 @@ import static org.dependencytrack.proto.notification.v1.Group.GROUP_ANALYZER; import static org.dependencytrack.proto.notification.v1.Group.GROUP_NEW_VULNERABILITY; import static org.dependencytrack.proto.notification.v1.Group.GROUP_NEW_VULNERABLE_DEPENDENCY; +import static org.dependencytrack.proto.notification.v1.Group.GROUP_PROJECT_AUDIT_CHANGE; import static org.dependencytrack.proto.notification.v1.Level.LEVEL_ERROR; import static org.dependencytrack.proto.notification.v1.Level.LEVEL_INFORMATIONAL; import static org.dependencytrack.proto.notification.v1.Scope.SCOPE_PORTFOLIO; @@ -895,8 +897,19 @@ public void analysisThroughPolicyExistingDifferentAnalysisTest() { }); // The vulnerability already existed, so no notifications to be expected. - // TODO: There should be PROJECT_AUDIT_CHANGE notifications. - assertThat(kafkaMockProducer.history()).isEmpty(); + // There should be PROJECT_AUDIT_CHANGE notification. + assertThat(kafkaMockProducer.history()).satisfiesExactly( + record -> { + assertThat(record.topic()).isEqualTo(KafkaTopics.NOTIFICATION_PROJECT_AUDIT_CHANGE.name()); + final Notification notification = deserializeValue(KafkaTopics.NOTIFICATION_PROJECT_AUDIT_CHANGE, record); + assertThat(notification.getScope()).isEqualTo(SCOPE_PORTFOLIO); + assertThat(notification.getLevel()).isEqualTo(LEVEL_INFORMATIONAL); + assertThat(notification.getGroup()).isEqualTo(GROUP_PROJECT_AUDIT_CHANGE); + assertThat(notification.getSubject().is(VulnerabilityAnalysisDecisionChangeSubject.class)).isTrue(); + final var subject = notification.getSubject().unpack(VulnerabilityAnalysisDecisionChangeSubject.class); + assertThat(subject.getAnalysis().getState()).isEqualTo("NOT_AFFECTED"); + } + ); } @Test diff --git a/src/test/java/org/dependencytrack/persistence/jdbi/NotificationSubjectDaoTest.java b/src/test/java/org/dependencytrack/persistence/jdbi/NotificationSubjectDaoTest.java index 8e6638dc4..0156f567f 100644 --- a/src/test/java/org/dependencytrack/persistence/jdbi/NotificationSubjectDaoTest.java +++ b/src/test/java/org/dependencytrack/persistence/jdbi/NotificationSubjectDaoTest.java @@ -499,10 +499,10 @@ public void testGetForProjectAuditChange() { qm.addVulnerability(vulnA, component, AnalyzerIdentity.INTERNAL_ANALYZER); // Suppress vulnB, it should not appear in the query results. - qm.makeAnalysis(component, vulnA, AnalysisState.NOT_AFFECTED, null, null, null, false); + var policyAnalysis = qm.makeAnalysis(component, vulnA, AnalysisState.NOT_AFFECTED, null, null, null, false); final Optional optionalSubject = JdbiFactory.jdbi(qm).withExtension(NotificationSubjectDao.class, - dao -> dao.getForProjectAuditChange(component.getUuid(), vulnA.getUuid())); + dao -> dao.getForProjectAuditChange(component.getUuid(), vulnA.getUuid(), policyAnalysis.getAnalysisState(), policyAnalysis.isSuppressed())); assertThat(optionalSubject.get()).satisfies(subject -> assertThatJson(JsonFormat.printer().print(subject))