Skip to content

feat: RuleGenerationAgent — auto-generate OpenGrep reachability rules for CVEs #17

Description

@varun-polpakkara

Summary

The Stage 3 reachability analyser uses static, hand-written OpenGrep rule files (e.g. tests/fixtures/reachability/rules/log4j-reachability.yaml). For the pipeline to scale beyond a small watch list of well-known CVEs, rules need to be generated automatically for any CVE that Grype/OSV surfaces in Stage 2.

This issue tracks the design and implementation of a RuleGenerationAgent and the surrounding plumbing to store, validate, and consume generated rules.


Background

OpenGrep (Semgrep-compatible) rules are YAML files that describe two code patterns per CVE:

  • import rule (kind: import) — detects that the vulnerable library is referenced in the source tree (necessary but not sufficient for reachability).
  • usage/sink rule (kind: usage) — detects a call into the specific vulnerable API (sufficient for a REACHABLE verdict).

Stage 3 already parses these two kind values from rule metadata and maps them to verdicts (REACHABLE / UNCERTAIN / NOT_REACHABLE). No changes to Stage 3 are needed — only rule files need to exist on disk before a scan runs.


Proposed Design

1. Rule storage

cyberguard/
└── rules/
    ├── community/        # rules pulled from Semgrep registry (auto-downloaded, gitignored)
    └── generated/        # LLM-drafted rules (committed after human review)
        ├── CVE-2021-44228.yaml
        ├── CVE-2014-0160.yaml
        └── ...

Stage 3 should pass --config rules/generated/ --config rules/community/ instead of the current single _OPENGREP_CONFIG constant.

2. Community rule lookup (free, no LLM)

Before calling the agent, check the Semgrep registry for an existing rule:

GET https://semgrep.dev/api/registry/rules?query=CVE-2021-44228&lang=java

High-profile CVEs (Log4Shell, Spring4Shell, Heartbleed) often already have community rules. If one is found, download it to rules/community/ and skip the agent call entirely.

3. RuleGenerationAgent

File: cyberguard/packages/agents/agents/rule_generation_agent.py

Constructor:

class RuleGenerationAgent:
    def __init__(self, llm: BaseChatModel, *, max_iterations: int = 10) -> None: ...

Primary method:

def generate_rule(self, cve_context: CVERuleContext) -> GeneratedRule: ...

CVERuleContext (input dataclass):

@dataclass
class CVERuleContext:
    cve_id: str                  # e.g. "CVE-2021-44228"
    purl: str                    # e.g. "pkg:maven/org.apache.logging.log4j/log4j-core@2.14.1"
    ecosystem: str               # e.g. "maven"
    language: str                # e.g. "java"  (derived from ecosystem)
    affected_versions: str       # e.g. ">= 2.0-beta9, < 2.15.0"
    fixed_version: str           # e.g. "2.15.0"
    description: str             # CVE advisory text from NVD/OSV
    cvss_score: float
    references: list[str]        # advisory URLs for the agent to fetch

GeneratedRule (output dataclass):

@dataclass
class GeneratedRule:
    cve_id: str
    rule_yaml: str               # complete OpenGrep-compatible YAML content
    confidence: Literal["high", "medium", "low"]
    rationale: str               # agent's explanation of the patterns chosen
    has_import_rule: bool
    has_usage_rule: bool

Agent tools:

  • FetchURLTool — reads advisory pages and PoC write-ups from references
  • WebSearchTool (Tavily) — finds PoC code examples showing the vulnerable call site

Prompt strategy:
The agent is given the CVE context and asked to produce a YAML rule with:

  1. An import pattern matching how the vulnerable package is referenced in source
  2. A sink/usage pattern matching the specific API call that triggers the vulnerability
  3. Correct metadata.purl and metadata.kind fields (parsed by Stage 3)
  4. Language set to the correct Semgrep language identifier (java, python, c, etc.)

4. Validation step

Generated rules must be validated before use:

  1. Run opengrep scan --config <rule_yaml_path> <reachable_fixture> — expects ≥ 1 match.
  2. Run opengrep scan --config <rule_yaml_path> <non_reachable_fixture> — expects 0 kind: usage matches.

If validation fails, the rule is written to rules/generated/pending/ with a # validation_failed: true header comment for human review. It is not used in production scans until it passes or is manually approved.

For CVEs where no reachability fixture exists, validation is skipped and the rule goes directly to pending/.

5. CLI commands

# Generate a rule for one CVE (fetches advisory from OSV, calls agent if no community rule found)
cyberguard db generate-rule CVE-2021-44228

# Batch: generate rules for all CVEs in the most recent scan that have no rule yet
cyberguard db refresh-rules [--scan-run-id UUID] [--force-regenerate]

# Show rule status across all CVEs (community vs generated, validated vs pending)
cyberguard db rules-status [--format table|json]

6. Optional DB table — CVERule

Tracks rule provenance and review status (the file is still the source of truth):

Column Type Notes
cve_id VARCHAR PK
rule_path TEXT relative path to YAML file
source VARCHAR community | generated | manual
validation_status VARCHAR passed | failed | skipped | pending_review
generated_at TIMESTAMPTZ
reviewed_by VARCHAR nullable; set when a human approves a pending rule

Key constraints

  • One rule file per CVECVE-2021-44228 and CVE-2021-45046 share the same Log4j sink; the agent should check whether an existing rule already covers the new CVE before generating a new file.
  • Not all CVEs have call-site patterns — passive vulnerabilities (triggered by receiving a malformed packet rather than calling a specific function) cannot be expressed as usage rules. The agent should set has_usage_rule: false and confidence: low; Stage 3 will default to UNCERTAIN and escalate to the ReachabilityAgent as today.
  • Language coverage — derive language from PURL ecosystem: mavenjava, pypipython, npmjavascript, cargorust, conanc. Unsupported ecosystems fall back to pattern-regex rules or skip generation entirely.
  • Rate limiting — Semgrep registry and Tavily both rate-limit. refresh-rules should back off on 429 responses.

Implementation order

  1. Add rules/generated/ and rules/community/ directories; update .gitignore to exclude rules/community/.
  2. Update _OPENGREP_CONFIG in reachability_analyzer.py to accept a list of config paths instead of a single string.
  3. Implement community rule lookup (HTTP fetch only, no agent).
  4. Implement RuleGenerationAgent with FetchURLTool + WebSearchTool.
  5. Implement the validation runner.
  6. Wire up the three CLI commands.
  7. Unit tests (mock the agent) + one e2e test that generates a rule for CVE-2021-44228 and validates it against the existing reachability fixtures.

Related files

  • cyberguard/packages/pipeline/pipeline/stages/reachability_analyzer.py_OPENGREP_CONFIG constant (~line 296); _parse_opengrep_output() already handles kind: import / kind: usage
  • cyberguard/packages/agents/agents/exploit_agent.py — reference implementation for an agent using FetchURLTool + WebSearchTool
  • cyberguard/tests/fixtures/reachability/rules/log4j-reachability.yaml — canonical example of the target YAML output
  • cyberguard/tests/fixtures/reachability/reachable/App.java — validation fixture (must match usage rule)
  • cyberguard/tests/fixtures/reachability/non_reachable/App.java — validation fixture (must NOT match usage rule)

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions