From e5ae8a684d9e52c4255b1d8302230cec0ba59eef Mon Sep 17 00:00:00 2001 From: Tomasz Nowak Date: Fri, 24 Nov 2023 16:39:24 +0000 Subject: [PATCH] Ignore (suppress) advisories globally, systemwide Advisories listed in `IGNORED_ADVISORIES` environment variable will not trigger a vulnerability. This allows removing noise from disputed or other invalid advisories. Configuration via an environment variable has been chosen to allow versioning changes in a VCS like Git, when the deployment configuration is versioned. Signed-off-by: Tomasz Nowak --- docs/_docs/triage/suppression.md | 11 +++- .../VulnerabilityQueryManager.java | 18 ++++-- .../util/NotificationUtil.java | 50 ++++++++------- ...ternalAnalysisIgnoredGloballyTaskTest.java | 61 +++++++++++++++++++ 4 files changed, 113 insertions(+), 27 deletions(-) create mode 100644 src/test/java/org/dependencytrack/tasks/scanners/InternalAnalysisIgnoredGloballyTaskTest.java diff --git a/docs/_docs/triage/suppression.md b/docs/_docs/triage/suppression.md index bd947dd688..3aabb6f4f6 100644 --- a/docs/_docs/triage/suppression.md +++ b/docs/_docs/triage/suppression.md @@ -32,4 +32,13 @@ By suppressing findings, external systems will, by default, have the same positi finding will have a positive impact not only on Dependency-Track itself, but the metrics of external systems as well. For example, vulnerability aggregation platforms such as Kenna Security or ThreadFix will become aware of the updated -list of findings and metrics the next time they sync. These systems will assume suppressed findings have been 'fixed'. \ No newline at end of file +list of findings and metrics the next time they sync. These systems will assume suppressed findings have been 'fixed'. + +### Ignoring Advisories Systemwide + +Individual advisories can be ignored by setting the environment variable "IGNORED_ADVISORIES" before starting +Dependency-Track API server. Identifiers of the advisories need to be separated by a space character, for example: + +``` +export IGNORED_ADVISORIES="CVE-2019-123 GHSA-567" +``` diff --git a/src/main/java/org/dependencytrack/persistence/VulnerabilityQueryManager.java b/src/main/java/org/dependencytrack/persistence/VulnerabilityQueryManager.java index 8026a973ae..d3f1067872 100644 --- a/src/main/java/org/dependencytrack/persistence/VulnerabilityQueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/VulnerabilityQueryManager.java @@ -18,10 +18,12 @@ */ package org.dependencytrack.persistence; +import alpine.common.logging.Logger; import alpine.event.framework.Event; import alpine.persistence.PaginatedResult; import alpine.resources.AlpineRequest; import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.SystemUtils; import org.dependencytrack.event.IndexEvent; import org.dependencytrack.model.AffectedVersionAttribution; import org.dependencytrack.model.Analysis; @@ -36,11 +38,13 @@ import javax.jdo.PersistenceManager; import javax.jdo.Query; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.Date; import java.util.HashMap; +import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; @@ -48,6 +52,8 @@ import java.util.stream.Collectors; final class VulnerabilityQueryManager extends QueryManager implements IQueryManager { + private final Logger LOGGER = Logger.getLogger(this.getClass()); + private static final Set ignoredAdvisories = new HashSet(Arrays.asList(SystemUtils.getEnvironmentVariable("IGNORED_ADVISORIES", "").split(" "))); /** * Constructs a new QueryManager. @@ -212,10 +218,14 @@ public void addVulnerability(Vulnerability vulnerability, Component component, A */ public void addVulnerability(Vulnerability vulnerability, Component component, AnalyzerIdentity analyzerIdentity, String alternateIdentifier, String referenceUrl) { - if (!contains(vulnerability, component)) { - component.addVulnerability(vulnerability); - component = persist(component); - persist(new FindingAttribution(component, vulnerability, analyzerIdentity, alternateIdentifier, referenceUrl)); + if (ignoredAdvisories.contains(vulnerability.getVulnId())) { + this.LOGGER.warn("IGNORED_ADVISORIES hit in addVulnerability, skipping: " + vulnerability.getVulnId()); + } else { + if (!contains(vulnerability, component)) { + component.addVulnerability(vulnerability); + component = persist(component); + persist(new FindingAttribution(component, vulnerability, analyzerIdentity, alternateIdentifier, referenceUrl)); + } } } diff --git a/src/main/java/org/dependencytrack/util/NotificationUtil.java b/src/main/java/org/dependencytrack/util/NotificationUtil.java index b244173dfc..431a0deaf0 100644 --- a/src/main/java/org/dependencytrack/util/NotificationUtil.java +++ b/src/main/java/org/dependencytrack/util/NotificationUtil.java @@ -22,6 +22,7 @@ import alpine.notification.Notification; import alpine.notification.NotificationLevel; import org.apache.commons.io.FileUtils; +import org.apache.commons.lang3.SystemUtils; import org.dependencytrack.model.Analysis; import org.dependencytrack.model.Bom; import org.dependencytrack.model.Component; @@ -63,15 +64,18 @@ import java.io.IOException; import java.net.URLDecoder; import java.nio.file.Path; +import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Set; import static java.nio.charset.StandardCharsets.UTF_8; public final class NotificationUtil { + private static final Set ignoredAdvisories = new HashSet(Arrays.asList(SystemUtils.getEnvironmentVariable("IGNORED_ADVISORIES", "").split(" "))); /** * Private constructor. @@ -79,30 +83,32 @@ public final class NotificationUtil { private NotificationUtil() { } public static void analyzeNotificationCriteria(QueryManager qm, Vulnerability vulnerability, Component component, VulnerabilityAnalysisLevel vulnerabilityAnalysisLevel) { - if (!qm.contains(vulnerability, component)) { - // Component did not previously contain this vulnerability. It could be a newly discovered vulnerability - // against an existing component, or it could be a newly added (and vulnerable) component. Either way, - // it warrants a Notification be dispatched. - final Map affectedProjects = new HashMap<>(); - final List components = qm.matchIdentity(new ComponentIdentity(component)); - for (final Component c : components) { - if(!affectedProjects.containsKey(c.getProject().getId())) { - affectedProjects.put(c.getProject().getId(), qm.detach(Project.class, c.getProject().getId())); + if (!ignoredAdvisories.contains(vulnerability.getVulnId())) { + if (!qm.contains(vulnerability, component)) { + // Component did not previously contain this vulnerability. It could be a newly discovered vulnerability + // against an existing component, or it could be a newly added (and vulnerable) component. Either way, + // it warrants a Notification be dispatched. + final Map affectedProjects = new HashMap<>(); + final List components = qm.matchIdentity(new ComponentIdentity(component)); + for (final Component c : components) { + if(!affectedProjects.containsKey(c.getProject().getId())) { + affectedProjects.put(c.getProject().getId(), qm.detach(Project.class, c.getProject().getId())); + } } - } - - final Vulnerability detachedVuln = qm.detach(Vulnerability.class, vulnerability.getId()); - detachedVuln.setAliases(qm.detach(qm.getVulnerabilityAliases(vulnerability))); // Aliases are lost during detach above - final Component detachedComponent = qm.detach(Component.class, component.getId()); - Notification.dispatch(new Notification() - .scope(NotificationScope.PORTFOLIO) - .group(NotificationGroup.NEW_VULNERABILITY) - .title(generateNotificationTitle(NotificationConstants.Title.NEW_VULNERABILITY, component.getProject())) - .level(NotificationLevel.INFORMATIONAL) - .content(generateNotificationContent(detachedVuln)) - .subject(new NewVulnerabilityIdentified(detachedVuln, detachedComponent, new HashSet<>(affectedProjects.values()), vulnerabilityAnalysisLevel)) - ); + final Vulnerability detachedVuln = qm.detach(Vulnerability.class, vulnerability.getId()); + detachedVuln.setAliases(qm.detach(qm.getVulnerabilityAliases(vulnerability))); // Aliases are lost during detach above + final Component detachedComponent = qm.detach(Component.class, component.getId()); + + Notification.dispatch(new Notification() + .scope(NotificationScope.PORTFOLIO) + .group(NotificationGroup.NEW_VULNERABILITY) + .title(generateNotificationTitle(NotificationConstants.Title.NEW_VULNERABILITY, component.getProject())) + .level(NotificationLevel.INFORMATIONAL) + .content(generateNotificationContent(detachedVuln)) + .subject(new NewVulnerabilityIdentified(detachedVuln, detachedComponent, new HashSet<>(affectedProjects.values()), vulnerabilityAnalysisLevel)) + ); + } } } diff --git a/src/test/java/org/dependencytrack/tasks/scanners/InternalAnalysisIgnoredGloballyTaskTest.java b/src/test/java/org/dependencytrack/tasks/scanners/InternalAnalysisIgnoredGloballyTaskTest.java new file mode 100644 index 0000000000..4596879793 --- /dev/null +++ b/src/test/java/org/dependencytrack/tasks/scanners/InternalAnalysisIgnoredGloballyTaskTest.java @@ -0,0 +1,61 @@ +package org.dependencytrack.tasks.scanners; + +import alpine.persistence.PaginatedResult; +import org.dependencytrack.PersistenceCapableTest; +import org.dependencytrack.model.Component; +import org.dependencytrack.model.Project; +import org.dependencytrack.model.Vulnerability; +import org.dependencytrack.model.VulnerableSoftware; +import org.junit.Test; + +import java.lang.reflect.Field; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + +public class InternalAnalysisIgnoredGloballyTaskTest extends PersistenceCapableTest { + + @Test + public void testIssueIgnoredGlobally1574() throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, SecurityException { + final Map env = System.getenv(); + final Field envField = env.getClass().getDeclaredField("m"); + try { + envField.setAccessible(true); + ((Map) envField.get(env)).put("IGNORED_ADVISORIES", "GHSA-wjm3-fq3r-5x46"); + + var project = new Project(); + project.setName("acme-app"); + project = qm.createProject(project, Collections.emptyList(), false); + var component = new Component(); + component.setProject(project); + component.setName("github.com/tidwall/gjson"); + component.setVersion("v1.6.0"); + component.setPurl("pkg:golang/github.com/tidwall/gjson@v1.6.0?type=module"); + component = qm.createComponent(component, false); + + var vulnerableSoftware = new VulnerableSoftware(); + vulnerableSoftware.setPurlType("golang"); + vulnerableSoftware.setPurlNamespace("github.com/tidwall"); + vulnerableSoftware.setPurlName("gjson"); + vulnerableSoftware.setVersionEndExcluding("1.6.5"); + vulnerableSoftware.setVulnerable(true); + vulnerableSoftware = qm.persist(vulnerableSoftware); + + var vulnerability = new Vulnerability(); + vulnerability.setVulnId("GHSA-wjm3-fq3r-5x46"); + vulnerability.setSource(Vulnerability.Source.GITHUB); + vulnerability.setVulnerableSoftware(List.of(vulnerableSoftware)); + qm.createVulnerability(vulnerability, false); + + new InternalAnalysisTask().analyze(List.of(component)); + + final PaginatedResult vulnerabilities = qm.getVulnerabilities(component); + assertThat(vulnerabilities.getTotal()).isEqualTo(0); + } finally { + ((Map) envField.get(env)).remove("IGNORED_ADVISORIES"); // this doesn't work! + envField.setAccessible(false); + } + } +}