Draft: Dependency firewall - vulnerability rule evaluator
Assisted by AI
What does this MR do and why?
This MR implements the vulnerability rule evaluator for the Dependency Firewall policy and refactors the existing rule/evaluator architecture to support multiple rule types cleanly.
1. Refactor: introduce a parent Rule class with type-specific subclasses
Security::DependencyFirewallPolicies::Rule is reshaped into a generic parent class that handles concerns shared by every rule type (type detection, PURL exception handling, result envelope construction). Type-specific evaluation logic now lives in dedicated subclasses:
Security::DependencyFirewallPolicies::LicenseRule(extracted from the oldRule#evaluatelicense logic) —ee/lib/security/dependency_firewall_policies/license_rule.rbSecurity::DependencyFirewallPolicies::VulnerabilityRule(new) —ee/lib/security/dependency_firewall_policies/vulnerability_rule.rb
A factory method Rule.by_type(rule_hash) instantiates the correct subclass based on the rule's type field, and Rules now uses it when wrapping raw rule hashes. The base Rule#evaluate short-circuits on the PURL exception check and then delegates to #apply(metadata:), which each subclass overrides. The previous licenses positional argument has been replaced by a metadata: keyword hash so that future rule types can carry their own context (e.g. licenses, vulnerabilities, ...) without further signature churn.
2. Rename: LicenseRuleEvaluator → PolicyEvaluator
Now that the evaluator dispatches across multiple rule types, Security::DependencyFirewallPolicy::LicenseRuleEvaluator is renamed to Security::DependencyFirewallPolicy::PolicyEvaluator (ee/app/services/security/dependency_firewall/policy_evaluator.rb). Its #evaluate signature is extended with a vulnerabilities: keyword and forwards both licenses and vulnerabilities to each rule via the new metadata: parameter. EnforcementService is updated to use the new class.
3. Implement: VulnerabilityRule per the decision tree from #592787
The new VulnerabilityRule#apply follows the agreed decision tree:
flowchart TD
LRE(["Vulnerability Rule Evaluation Input(rule, package, vulnerabilities)"]) --> M{PURL in Exception?}
M -- True --> N([Allowed])
M -- False --> Z("vulnerabilities = vulnerabilities - [CVEs in `exceptions`]")
Z --> O([:allowed rule?])
O -- True --> P{for each vulnerability:<br>`vulnerability.severity` <= `rule.severity`?}
P -- True --> Q([Allowed])
P -- False --> R([Denied])
O -- False --> S{:denied rule?}
S --> T{for any vulnerability:<br>`vulnerability.severity` >= `rule.severity`?}
T -- True --> U([Denied])
T -- False --> V([Allowed])Implementation notes:
- PURL-level exceptions are handled by the parent
Rule#evaluate(returnsallowed/exception). - CVE-level exceptions are filtered out of the vulnerability list before threshold evaluation. If every vulnerability is excepted by id, the rule returns
allowed/exception. - Severities are compared via an ordinal map:
info < unknown < low < medium < high < critical. Unknown strings fall through as0(below any threshold). - For an
allowedrule (severity threshold T), a package is allowed only if every remaining vulnerability has severity<= T. - For a
deniedrule (severity threshold T), a package is denied if any remaining vulnerability has severity>= T. - An empty vulnerability list returns
allowed/evaluation.
4. Schema updates
dependency_firewall_policy_content.json and security_orchestration_policy.json are extended to:
- Allow
type: "vulnerability"in addition to"license". - Use a conditional
allOfblock sodenied/alloweditems are validated as{ name: ... }for license rules and as a single{ severity: ... }entry (one ofcritical|high|medium|low|info|unknown) for vulnerability rules. - Allow exception entries to specify either
purl(package exception) orid(vulnerability identifier such as a CVE).
5. Sample YAML
---
vulnerability_management_policy:
- name: Vuln Policy
description: Vuln Policy
enabled: true
rules:
- type: no_longer_detected
scanners: []
severity_levels: []
actions:
- type: auto_resolve
dependency_firewall_policy:
- name: "Block copyleft"
description: "Prevent copyleft-licensed packages from being downloaded"
enabled: true
enforcement_type: enforced # blocks the download when a rule matches
rules:
- type: license
denied:
- name: NIST Software License
- name: NTP License
exceptions:
- purl: "pkg:npm/my-internal-lib"
- name: "Warning rule"
description: "Warn on certain vulnerabilities"
enabled: true
enforcement_type: warn
rules:
- type: vulnerability
allowed:
- severity: "low"
exceptions:
- purl: "pkg:npm/my-internal-lib"
- id: "CVE-2026-0001"
bypass_settings:
users:
- id: 123
- name: "Denied rule"
description: "Deny any package with >= high severity vulnerabilities"
enabled: true
enforcement_type: enforced
rules:
- type: vulnerability
denied:
- severity: "high"
exceptions:
- purl: "pkg:npm/my-internal-lib"
- id: "CVE-2026-0003"
bypass_settings:
users:
- id: 123References
- Closes #589625 — Dependency firewall policy vulnerability rule evaluator
- Implements decision tree and YAML schema from #592787 — Policy: vulnerability advisory rule schema definition
- Parent epic: &21273 — Define dependency firewall policy rules schema Phase 1
How to set up and validate locally
- Setup a new dependency firewall policy as mentioned here: !225976 (merged). Use the above sample YAML.
- Open a Rails console
gdk rails c - Fetch the project, create an evaluator instance:
project = Project.find(19); e = Security::DependencyFirewall::PolicyEvaluator.new(project, User.find(2)); licenses = ["NIST Software License"]; vulnerabilities = [{id: "CVE-2026-0003", severity: "critical"}] - Call the policy evaluator's
evaluatemethod:e.evaluate("my-internal2-lib", purl_type: "npm", licenses: licenses, vulnerabilities: vulnerabilities) - Vary the arguments to test various scenarios.
MR acceptance checklist
Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.
Related to #589625