diff --git a/apiserver/src/main/java/org/dependencytrack/policy/cel/compat/VulnerabilityIdCelPolicyScriptSourceBuilder.java b/apiserver/src/main/java/org/dependencytrack/policy/cel/compat/VulnerabilityIdCelPolicyScriptSourceBuilder.java index b485aa02f7..ebb83d8a74 100644 --- a/apiserver/src/main/java/org/dependencytrack/policy/cel/compat/VulnerabilityIdCelPolicyScriptSourceBuilder.java +++ b/apiserver/src/main/java/org/dependencytrack/policy/cel/compat/VulnerabilityIdCelPolicyScriptSourceBuilder.java @@ -23,19 +23,45 @@ import static org.dependencytrack.policy.cel.compat.CelPolicyScriptSourceBuilder.escapeQuotes; public class VulnerabilityIdCelPolicyScriptSourceBuilder implements CelPolicyScriptSourceBuilder { + @Override public String apply(final PolicyCondition policyCondition) { - final String scriptSrc = """ - vulns.exists(v, v.id == "%s") - """.formatted(escapeQuotes(policyCondition.getValue())); + final String value = policyCondition.getValue(); - if (policyCondition.getOperator() == PolicyCondition.Operator.IS) { - return scriptSrc; - } else if (policyCondition.getOperator() == PolicyCondition.Operator.IS_NOT) { - return "!" + scriptSrc; - } + return switch (policyCondition.getOperator()) { + case IS -> """ + vulns.exists(v, v.id == "%s") + """.formatted(escapeQuotes(value)); + case IS_NOT -> """ + !vulns.exists(v, v.id == "%s") + """.formatted(escapeQuotes(value)); + case MATCHES -> """ + vulns.exists(v, v.id.matches("%s")) + """.formatted(escapeQuotes(toRegex(value))); + case NO_MATCH -> """ + !vulns.exists(v, v.id.matches("%s")) + """.formatted(escapeQuotes(toRegex(value))); + default -> null; + }; + } - return null; + /** + * Normalizes a condition value into a regular expression, mirroring the behavior of + * {@code org.dependencytrack.policy.Matcher} in Dependency-Track v4. A bare {@code *} + * is treated as a wildcard ({@code .*}), and the pattern is anchored with {@code .*} on + * either end when it is not already anchored, so the match behaves as a substring match. + * This preserves the semantics the {@code MATCHES} / {@code NO_MATCH} operators had for + * vulnerability-id conditions in v4. + */ + static String toRegex(final String conditionString) { + String regex = conditionString.replace("*", ".*").replace("..*", ".*"); + if (!regex.startsWith("^") && !regex.startsWith(".*")) { + regex = ".*" + regex; + } + if (!regex.endsWith("$") && !regex.endsWith(".*")) { + regex = regex + ".*"; + } + return regex; } } diff --git a/apiserver/src/test/java/org/dependencytrack/policy/cel/compat/VulnerabilityIdConditionTest.java b/apiserver/src/test/java/org/dependencytrack/policy/cel/compat/VulnerabilityIdConditionTest.java index 7fd56aa7b6..46b09bb4d5 100644 --- a/apiserver/src/test/java/org/dependencytrack/policy/cel/compat/VulnerabilityIdConditionTest.java +++ b/apiserver/src/test/java/org/dependencytrack/policy/cel/compat/VulnerabilityIdConditionTest.java @@ -45,7 +45,19 @@ private static Object[] parameters() { // IS_NOT with exact match new Object[]{PolicyCondition.Operator.IS_NOT, "CVE-123", "CVE-123", false}, // IS with quotes - new Object[]{PolicyCondition.Operator.IS, "\"CVE-123", "\"CVE-123", true} + new Object[]{PolicyCondition.Operator.IS, "\"CVE-123", "\"CVE-123", true}, + // MATCHES with wildcard catches malware advisories (MAL-*) + new Object[]{PolicyCondition.Operator.MATCHES, "MAL-*", "MAL-2026-3463", true}, + // MATCHES wildcard with no match + new Object[]{PolicyCondition.Operator.MATCHES, "MAL-*", "CVE-2026-1234", false}, + // MATCHES with a plain regex (CVE-2024-*) + new Object[]{PolicyCondition.Operator.MATCHES, "CVE-2024-*", "CVE-2024-9999", true}, + // MATCHES with a regular expression using alternation and char classes + new Object[]{PolicyCondition.Operator.MATCHES, "^CVE-202[34]-", "CVE-2023-1", true}, + // NO_MATCH excludes allowlisted advisories via regex alternation + new Object[]{PolicyCondition.Operator.NO_MATCH, "(MAL-2026-(3463|3432)|MAL-2025-(5870|5869))", "MAL-2026-9999", true}, + // NO_MATCH: allowlisted advisory produces no violation + new Object[]{PolicyCondition.Operator.NO_MATCH, "(MAL-2026-(3463|3432)|MAL-2025-(5870|5869))", "MAL-2026-3463", false} }; }