Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support multiple modes of operation for vulnerability policies #669

Merged
merged 12 commits into from
Jun 5, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
import org.dependencytrack.persistence.jdbi.NotificationSubjectDao;
import org.dependencytrack.policy.vulnerability.VulnerabilityPolicy;
import org.dependencytrack.policy.vulnerability.VulnerabilityPolicyEvaluator;
import org.dependencytrack.policy.vulnerability.VulnerabilityPolicyOperation;
import org.dependencytrack.policy.vulnerability.VulnerabilityPolicyRating;
import org.dependencytrack.proto.notification.v1.Group;
import org.dependencytrack.proto.vulnanalysis.v1.ScanKey;
Expand Down Expand Up @@ -205,8 +206,16 @@ private void processScannerResult(final QueryManager qm, final Component compone
LOGGER.debug("Identified policy matches for %d/%d vulnerabilities (scanKey: %s)"
.formatted(matchedPoliciesByVulnUuid.size(), syncedVulns.size(), prettyPrint(scanKey)));

// Log the matched policies with operation mode LOG
LOGGER.debug("List of matched vulnerability policies with mode LOG : " + matchedPoliciesByVulnUuid.values().stream().map(matchedPolicy -> matchedPolicy.getName()).toList());
sahibamittal marked this conversation as resolved.
Show resolved Hide resolved

final Map<UUID, VulnerabilityPolicy> actionablePolicies = matchedPoliciesByVulnUuid.entrySet().stream()
.filter(policy -> policy.getValue().getOperationMode() == VulnerabilityPolicyOperation.APPLY)
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));

// Perform analysis for only actionable policies.
final List<Vulnerability> newVulnUuids = synchronizeFindingsAndAnalyses(qm, component, syncedVulns,
scannerResult.getScanner(), matchedPoliciesByVulnUuid);
scannerResult.getScanner(), actionablePolicies);
LOGGER.debug("Identified %d new vulnerabilities for %s with %s (scanKey: %s)"
.formatted(newVulnUuids.size(), scanKey.getComponentUuid(), scannerResult.getScanner(), prettyPrint(scanKey)));

Expand Down Expand Up @@ -446,7 +455,6 @@ private List<Vulnerability> maybeApplyPolicyAnalyses(QueryManager qm, final Dao
.collect(Collectors.toMap(Analysis::getVulnUuid, Function.identity()));

final var analysesToCreateOrUpdate = new ArrayList<Analysis>();
final var projectAuditChangeNotifications = new ArrayList<org.dependencytrack.proto.notification.v1.Notification>();
final var analysisCommentsByVulnId = new MultivaluedHashMap<Long, AnalysisComment>();

for (final Map.Entry<UUID, VulnerabilityPolicy> vulnUuidAndPolicy : policiesByVulnUuid.entrySet()) {
Expand Down
13 changes: 13 additions & 0 deletions src/main/java/org/dependencytrack/model/VulnerabilityPolicy.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import org.dependencytrack.policy.vulnerability.VulnerabilityPolicyAnalysis;
import org.dependencytrack.policy.vulnerability.VulnerabilityPolicyOperation;
import org.dependencytrack.policy.vulnerability.VulnerabilityPolicyRating;

import javax.jdo.annotations.Column;
Expand Down Expand Up @@ -96,6 +97,10 @@ public class VulnerabilityPolicy implements Serializable {
@Persistent(defaultFetchGroup = "true")
private List<VulnerabilityPolicyRating> ratings;

@Column(name = "OPERATION_MODE", allowsNull = "false")
@Persistent(defaultFetchGroup = "true")
private VulnerabilityPolicyOperation operationMode;

public long getId() {
return id;
}
Expand Down Expand Up @@ -183,5 +188,13 @@ public List<VulnerabilityPolicyRating> getRatings() {
public void setRatings(List<VulnerabilityPolicyRating> ratings) {
this.ratings = ratings;
}

public VulnerabilityPolicyOperation getOperationMode() {
return operationMode;
}

public void setOperationMode(VulnerabilityPolicyOperation operationMode) {
this.operationMode = operationMode;
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ public interface VulnerabilityPolicyDao extends SqlObject {
WHERE
("VALID_FROM" IS NULL OR "VALID_FROM" <= NOW())
AND ("VALID_UNTIL" IS NULL OR "VALID_UNTIL" >= NOW())
AND ("OPERATION_MODE" != 'DISABLED')
""")
List<VulnerabilityPolicy> getAllValid();
sahibamittal marked this conversation as resolved.
Show resolved Hide resolved

Expand All @@ -74,6 +75,7 @@ public interface VulnerabilityPolicyDao extends SqlObject {
"UPDATED" AS "updated",
"VALID_FROM" AS "validFrom",
"VALID_UNTIL" AS "validUntil",
"OPERATION_MODE" AS "operationMode",
COUNT(*) OVER() AS "totalCount"
FROM
"VULNERABILITY_POLICY"
Expand All @@ -84,7 +86,7 @@ public interface VulnerabilityPolicyDao extends SqlObject {
${offsetAndLimit!}
""")
@UseRowReducer(PaginatedVulnerabilityPolicyRowReducer.class)
PaginatedResult getPageWithNameLike(@DefineOrdering(allowedColumns = {"id", "author", "created", "name", "updated", "validFrom", "validUntil"}, alsoBy = "id") Ordering ordering,
PaginatedResult getPageWithNameLike(@DefineOrdering(allowedColumns = {"id", "author", "created", "name", "updated", "validFrom", "validUntil", "operationMode"}, alsoBy = "id") Ordering ordering,
@DefinePagination Pagination pagination, @Bind String name);

@SqlQuery("""
Expand All @@ -94,9 +96,9 @@ PaginatedResult getPageWithNameLike(@DefineOrdering(allowedColumns = {"id", "aut

@SqlUpdate("""
INSERT INTO "VULNERABILITY_POLICY"
("ANALYSIS", "AUTHOR", "CONDITIONS", "CREATED", "DESCRIPTION", "NAME", "RATINGS", "VALID_FROM", "VALID_UNTIL")
("ANALYSIS", "AUTHOR", "CONDITIONS", "CREATED", "DESCRIPTION", "NAME", "RATINGS", "VALID_FROM", "VALID_UNTIL", "OPERATION_MODE")
VALUES
((:analysis)::JSONB, :author, :conditions, NOW(), :description, :name, (:ratings)::JSONB, :validFrom, :validUntil)
((:analysis)::JSONB, :author, :conditions, NOW(), :description, :name, (:ratings)::JSONB, :validFrom, :validUntil, :operationMode)
RETURNING *
""")
@GetGeneratedKeys("*")
Expand All @@ -117,7 +119,8 @@ PaginatedResult getPageWithNameLike(@DefineOrdering(allowedColumns = {"id", "aut
"RATINGS" = (:ratings)::JSONB,
"UPDATED" = NOW(),
"VALID_FROM" = :validFrom,
"VALID_UNTIL" = :validUntil
"VALID_UNTIL" = :validUntil,
"OPERATION_MODE" = :operationMode
WHERE
"NAME" = :name AND (
-- Using IS DISTINCT FROM instead of != for nullable columns
Expand All @@ -129,6 +132,7 @@ PaginatedResult getPageWithNameLike(@DefineOrdering(allowedColumns = {"id", "aut
OR "RATINGS" IS DISTINCT FROM (:ratings)::JSONB
OR "VALID_FROM" IS DISTINCT FROM :validFrom
OR "VALID_UNTIL" IS DISTINCT FROM :validUntil
OR "OPERATION_MODE" IS DISTINCT FROM :operationMode
)
RETURNING *
""")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ public class VulnerabilityPolicy implements Serializable {

private List<VulnerabilityPolicyRating> ratings;

private VulnerabilityPolicyOperation operationMode;

public void setName(final String name) {
this.name = name;
}
Expand Down Expand Up @@ -128,4 +130,12 @@ public ZonedDateTime getValidUntil() {
public List<String> getConditions() {
return conditions;
}

public VulnerabilityPolicyOperation getOperationMode() {
return operationMode;
}

public void setOperationMode(VulnerabilityPolicyOperation operationMode) {
this.operationMode = operationMode;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* This file is part of Dependency-Track.
*
* 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.
*
* SPDX-License-Identifier: Apache-2.0
* Copyright (c) OWASP Foundation. All Rights Reserved.
*/
package org.dependencytrack.policy.vulnerability;

public enum VulnerabilityPolicyOperation {

DISABLED,
LOG,
APPLY
}
6 changes: 6 additions & 0 deletions src/main/resources/migration/changelog-v5.5.0.xml
Original file line number Diff line number Diff line change
Expand Up @@ -104,4 +104,10 @@
<changeSet id="v5.5.0-8" author="nscuro">
<dropTable tableName="CWE"/>
</changeSet>

<changeSet id="v5.5.0-9" author="sahibamittal">
<addColumn tableName="VULNERABILITY_POLICY">
<column name="OPERATION_MODE" type="VARCHAR(255)"/>
</addColumn>
</changeSet>
</databaseChangeLog>
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,10 @@
"items": { "$ref": "#/$defs/rating" },
"minItems": 0,
"maxItems": 3
},
"operationMode": {
"description": "Mode of operation for the vulnerability policy.",
"enum": ["DISABLED", "APPLY", "LOG"]
}
},
"$defs": {
Expand Down Expand Up @@ -107,6 +111,6 @@
"required": ["method", "severity"]
}
},
"required": ["apiVersion", "type", "name", "conditions", "analysis"],
"required": ["apiVersion", "type", "name", "conditions", "analysis", "operationMode"],
"additionalProperties": false
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
import org.dependencytrack.policy.vulnerability.DatabaseVulnerabilityPolicyProvider;
import org.dependencytrack.policy.vulnerability.VulnerabilityPolicy;
import org.dependencytrack.policy.vulnerability.VulnerabilityPolicyAnalysis;
import org.dependencytrack.policy.vulnerability.VulnerabilityPolicyOperation;
import org.dependencytrack.policy.vulnerability.VulnerabilityPolicyRating;
import org.dependencytrack.proto.notification.v1.NewVulnerabilitySubject;
import org.dependencytrack.proto.notification.v1.NewVulnerableDependencySubject;
Expand Down Expand Up @@ -645,6 +646,7 @@ public void analysisThroughPolicyNewAnalysisTest() {
policy.setConditions(List.of("has(component.name)", "project.version != \"\""));
policy.setAnalysis(policyAnalysis);
policy.setRatings(List.of(policyRating));
policy.setOperationMode(VulnerabilityPolicyOperation.APPLY);
jdbi(qm).withExtension(VulnerabilityPolicyDao.class, dao -> dao.create(policy));

final var componentUuid = component.getUuid();
Expand Down Expand Up @@ -747,6 +749,7 @@ public void analysisThroughPolicyNewAnalysisSuppressionTest() {
policy.setAuthor("Jane Doe");
policy.setConditions(List.of("has(component.name)", "project.version != \"\""));
policy.setAnalysis(policyAnalysis);
policy.setOperationMode(VulnerabilityPolicyOperation.APPLY);
jdbi(qm).withExtension(VulnerabilityPolicyDao.class, dao -> dao.create(policy));

final var componentUuid = component.getUuid();
Expand Down Expand Up @@ -850,6 +853,7 @@ public void analysisThroughPolicyExistingDifferentAnalysisTest() {
policy.setConditions(List.of("has(component.name)", "project.version != \"\""));
policy.setAnalysis(policyAnalysis);
policy.setRatings(List.of(policyRating));
policy.setOperationMode(VulnerabilityPolicyOperation.APPLY);
jdbi(qm).withExtension(VulnerabilityPolicyDao.class, dao -> dao.create(policy));

final var componentUuid = component.getUuid();
Expand Down Expand Up @@ -970,6 +974,7 @@ public void analysisThroughPolicyExistingEqualAnalysisTest() {
policy.setConditions(List.of("has(component.name)", "project.version != \"\""));
policy.setAnalysis(policyAnalysis);
policy.setRatings(List.of(policyRating));
policy.setOperationMode(VulnerabilityPolicyOperation.APPLY);
jdbi(qm).withExtension(VulnerabilityPolicyDao.class, dao -> dao.create(policy));

final var componentUuid = component.getUuid();
Expand Down Expand Up @@ -1058,6 +1063,7 @@ public void analysisThroughPolicyWithAliasesTest() {
policy.setName("Foo");
policy.setConditions(List.of("vuln.aliases.exists(alias, alias.id == \"GHSA-100\" || alias.id == \"GHSA-200\")"));
policy.setAnalysis(policyAnalysis);
policy.setOperationMode(VulnerabilityPolicyOperation.APPLY);
jdbi(qm).withExtension(VulnerabilityPolicyDao.class, dao -> dao.create(policy));

// Report three vulnerabilities for the component:
Expand Down Expand Up @@ -1140,6 +1146,7 @@ public void analysisThroughPolicyResetOnNoMatchTest() {
policy.setName("Foo");
policy.setConditions(List.of("component.name == \"some-other-name\""));
policy.setAnalysis(policyAnalysis);
policy.setOperationMode(VulnerabilityPolicyOperation.APPLY);
jdbi(qm).withExtension(VulnerabilityPolicyDao.class, dao -> dao.create(policy));

// Create vulnerability with existing analysis that was previously applied by the above policy,
Expand Down Expand Up @@ -1273,6 +1280,7 @@ public void analysisThroughPolicyWithPoliciesNotYetValidOrNotValidAnymoreTest()
notYetValidPolicy.setValidFrom(ZonedDateTime.ofInstant(Instant.now().plusSeconds(180), ZoneOffset.UTC));
notYetValidPolicy.setConditions(List.of("true"));
notYetValidPolicy.setAnalysis(notYetValidPolicyAnalysis);
notYetValidPolicy.setOperationMode(VulnerabilityPolicyOperation.APPLY);
jdbi(qm).withExtension(VulnerabilityPolicyDao.class, dao -> dao.create(notYetValidPolicy));

final var notValidAnymorePolicyAnalysis = new VulnerabilityPolicyAnalysis();
Expand Down Expand Up @@ -1345,6 +1353,7 @@ public void analysisThroughPolicyWithAnalysisUpdateNotOnStateOrSuppressionTest()
policy.setName("Foo");
policy.setConditions(List.of("true"));
policy.setAnalysis(policyAnalysis);
policy.setOperationMode(VulnerabilityPolicyOperation.APPLY);
jdbi(qm).withExtension(VulnerabilityPolicyDao.class, dao -> dao.create(policy));

final var componentUuid = component.getUuid();
Expand All @@ -1368,6 +1377,55 @@ public void analysisThroughPolicyWithAnalysisUpdateNotOnStateOrSuppressionTest()
record -> assertThat(record.topic()).isEqualTo(KafkaTopics.NOTIFICATION_PROJECT_AUDIT_CHANGE.name()));
}

@Test
public void analysisThroughPolicyWithPoliciesNotActionableTest() {
final var project = new Project();
project.setName("acme-app");
project.setVersion("1.0.0");
qm.persist(project);

final var component = new Component();
component.setName("acme-lib");
component.setVersion("1.1.0");
component.setProject(project);
qm.persist(component);

final var policyAnalysis = new VulnerabilityPolicyAnalysis();
policyAnalysis.setState(VulnerabilityPolicyAnalysis.State.FALSE_POSITIVE);
policyAnalysis.setJustification(VulnerabilityPolicyAnalysis.Justification.CODE_NOT_REACHABLE);
policyAnalysis.setVendorResponse(VulnerabilityPolicyAnalysis.Response.WILL_NOT_FIX);
policyAnalysis.setSuppress(true);
final var notActionablePolicy = new VulnerabilityPolicy();
notActionablePolicy.setName("NotActionable");
notActionablePolicy.setConditions(List.of("true"));
notActionablePolicy.setAnalysis(policyAnalysis);
notActionablePolicy.setOperationMode(VulnerabilityPolicyOperation.LOG);
jdbi(qm).withExtension(VulnerabilityPolicyDao.class, dao -> dao.create(notActionablePolicy));

final var vuln = new Vulnerability();
vuln.setVulnId("CVE-100");
vuln.setSource(Vulnerability.Source.NVD);
vuln.setSeverity(Severity.CRITICAL);
qm.persist(vuln);

final var componentUuid = component.getUuid();
final var scanToken = UUID.randomUUID().toString();
final var scanKey = ScanKey.newBuilder().setScanToken(scanToken).setComponentUuid(componentUuid.toString()).build();
final var scanResult = ScanResult.newBuilder()
.setKey(scanKey)
.addScannerResults(ScannerResult.newBuilder()
.setScanner(SCANNER_INTERNAL)
.setStatus(SCAN_STATUS_SUCCESSFUL)
.setBom(Bom.newBuilder().addAllVulnerabilities(List.of(
createVuln(vuln.getVulnId(), vuln.getSource())
))))
.build();
processor.process(aConsumerRecord(scanKey, scanResult).build());

qm.getPersistenceManager().evictAll();
assertThat(qm.getAnalysis(component, vuln)).isNull();
}

private org.cyclonedx.proto.v1_4.Vulnerability createVuln(final String id, final String source) {
return org.cyclonedx.proto.v1_4.Vulnerability.newBuilder()
.setId(id)
Expand Down
Loading