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:
- An import pattern matching how the vulnerable package is referenced in source
- A sink/usage pattern matching the specific API call that triggers the vulnerability
- Correct
metadata.purl and metadata.kind fields (parsed by Stage 3)
- Language set to the correct Semgrep language identifier (
java, python, c, etc.)
4. Validation step
Generated rules must be validated before use:
- Run
opengrep scan --config <rule_yaml_path> <reachable_fixture> — expects ≥ 1 match.
- 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 CVE — CVE-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:
maven → java, pypi → python, npm → javascript, cargo → rust, conan → c. 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
- Add
rules/generated/ and rules/community/ directories; update .gitignore to exclude rules/community/.
- Update
_OPENGREP_CONFIG in reachability_analyzer.py to accept a list of config paths instead of a single string.
- Implement community rule lookup (HTTP fetch only, no agent).
- Implement
RuleGenerationAgent with FetchURLTool + WebSearchTool.
- Implement the validation runner.
- Wire up the three CLI commands.
- 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)
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
RuleGenerationAgentand 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:
kind: import) — detects that the vulnerable library is referenced in the source tree (necessary but not sufficient for reachability).kind: usage) — detects a call into the specific vulnerable API (sufficient for a REACHABLE verdict).Stage 3 already parses these two
kindvalues 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
Stage 3 should pass
--config rules/generated/ --config rules/community/instead of the current single_OPENGREP_CONFIGconstant.2. Community rule lookup (free, no LLM)
Before calling the agent, check the Semgrep registry for an existing rule:
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.
RuleGenerationAgentFile:
cyberguard/packages/agents/agents/rule_generation_agent.pyConstructor:
Primary method:
CVERuleContext(input dataclass):GeneratedRule(output dataclass):Agent tools:
FetchURLTool— reads advisory pages and PoC write-ups fromreferencesWebSearchTool(Tavily) — finds PoC code examples showing the vulnerable call sitePrompt strategy:
The agent is given the CVE context and asked to produce a YAML rule with:
metadata.purlandmetadata.kindfields (parsed by Stage 3)java,python,c, etc.)4. Validation step
Generated rules must be validated before use:
opengrep scan --config <rule_yaml_path> <reachable_fixture>— expects ≥ 1 match.opengrep scan --config <rule_yaml_path> <non_reachable_fixture>— expects 0kind: usagematches.If validation fails, the rule is written to
rules/generated/pending/with a# validation_failed: trueheader 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
6. Optional DB table —
CVERuleTracks rule provenance and review status (the file is still the source of truth):
community|generated|manualpassed|failed|skipped|pending_reviewKey constraints
has_usage_rule: falseandconfidence: low; Stage 3 will default toUNCERTAINand escalate to theReachabilityAgentas today.maven→java,pypi→python,npm→javascript,cargo→rust,conan→c. Unsupported ecosystems fall back topattern-regexrules or skip generation entirely.refresh-rulesshould back off on 429 responses.Implementation order
rules/generated/andrules/community/directories; update.gitignoreto excluderules/community/._OPENGREP_CONFIGinreachability_analyzer.pyto accept a list of config paths instead of a single string.RuleGenerationAgentwithFetchURLTool+WebSearchTool.Related files
cyberguard/packages/pipeline/pipeline/stages/reachability_analyzer.py—_OPENGREP_CONFIGconstant (~line 296);_parse_opengrep_output()already handleskind: import/kind: usagecyberguard/packages/agents/agents/exploit_agent.py— reference implementation for an agent usingFetchURLTool+WebSearchToolcyberguard/tests/fixtures/reachability/rules/log4j-reachability.yaml— canonical example of the target YAML outputcyberguard/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)