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 old Rule#evaluate license logic) — ee/lib/security/dependency_firewall_policies/license_rule.rb
  • Security::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: LicenseRuleEvaluatorPolicyEvaluator

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 (returns allowed/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 as 0 (below any threshold).
  • For an allowed rule (severity threshold T), a package is allowed only if every remaining vulnerability has severity <= T.
  • For a denied rule (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 allOf block so denied / allowed items are validated as { name: ... } for license rules and as a single { severity: ... } entry (one of critical|high|medium|low|info|unknown) for vulnerability rules.
  • Allow exception entries to specify either purl (package exception) or id (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: 123

References

  • 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

  1. Setup a new dependency firewall policy as mentioned here: !225976 (merged). Use the above sample YAML.
  2. Open a Rails console gdk rails c
  3. 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"}]
  4. Call the policy evaluator's evaluate method: e.evaluate("my-internal2-lib", purl_type: "npm", licenses: licenses, vulnerabilities: vulnerabilities)
  5. 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

Edited by Arpit Gogia

Merge request reports

Loading