Plan: Add Experimental OpenGrep SAST Integration
Context
ReviewCerberus currently reviews code using only an LLM agent. Adding a SAST
pre-scan via OpenGrep (a Semgrep fork)
can provide the LLM with concrete security signals to verify or dismiss. This is
experimental, disabled by default, and enabled only with --sast.
Key design decisions:
- SAST runs before the agent as a pre-processing step
- Findings go into the user message (alongside diffs/commits)
- SAST skepticism guidance is appended to the system prompt — the original
full_review.mdis never modified - Works naturally with
--verify— SAST data flows throughuser_messageandsystem_promptwhich verification already receives (seesrc/agent/verification/runner.py:20-27) - SAST errors are fatal — if
--sastwas requested and fails, the run fails (user explicitly opted in, they should know it's broken)
Step 0: Validate OpenGrep CLI Assumptions ✅ DONE
Validated with opengrep 1.16.0 on macOS arm64:
--baseline-commit main— accepts branch names directly (usesgit cat-file -einternally). No need forgit merge-base. Only reports findings introduced by the current branch. ✅JSON output structure — confirmed field names:
results[].check_id— rule ID (e.g.,python.lang.security.audit.eval-detected.eval-detected)results[].path— file pathresults[].start.line,results[].end.line— line rangeresults[].extra.message— descriptionresults[].extra.severity—"WARNING","ERROR", or"INFO"results[].extra.lines— matched source code- Drop:
extra.metadata,extra.fingerprint,extra.metavars,extra.is_ignored,extra.validation_state,extra.engine_kind - Drop top-level:
version,errors,paths,interfile_languages_used,skipped_rules
Exit codes — ⚠️ different from semgrep convention:
- 0 = success (with or without findings — findings don't change exit code)
- non-zero (2, 123, 124, 125) = error
--errorflag makes it exit 1 on findings, but we don't use it- This means we can use
check=Truein subprocess — non-zero always means a real error
--config auto— downloads ~1064 community rules from Semgrep Registry. Rules cached between runs. Second run ~4s. ✅Fallback — not needed,
--baseline-commitworks with branch names. ✅Binary distribution — all platforms are standalone binaries (no tarballs):
opengrep_manylinux_x86(Linux x86_64)opengrep_manylinux_aarch64(Linux aarch64)opengrep_osx_x86(macOS x86_64)opengrep_osx_arm64(macOS arm64)
Step 1: Create SAST Module — Installer
New files: src/agent/sast/__init__.py, src/agent/sast/installer.py
The __init__.py re-exports the public API (follow the pattern in
src/agent/verification/__init__.py).
The installer downloads the opengrep binary on demand with a pinned version:
- Pinned version as a constant (e.g.,
OPENGREP_VERSION = "1.16.0") - Cache path:
~/.cache/reviewcerberus/opengrep-v{VERSION}/opengrep - Platform detection via
platform.system()+platform.machine()to pick the right binary from GitHub releases. All platforms are standalone binaries (no extraction needed):opengrep_manylinux_x86(Linux x86_64)opengrep_manylinux_aarch64(Linux aarch64)opengrep_osx_x86(macOS x86_64)opengrep_osx_arm64(macOS arm64)
- Download URL:
https://github.com/opengrep/opengrep/releases/download/v{VERSION}/{binary_name} - Download method:
urllib.request.urlretrieve(stdlib, no extra deps)
The main function ensure_opengrep_binary() -> str checks these in order:
OPENGREP_BINARY_PATHenv var (manual override)- Cached binary at the cache path (version-pinned, no system lookup to avoid version mismatch)
- Download from GitHub releases
Include an if __name__ == "__main__" block so the Dockerfile can reuse the
same logic: RUN python -m src.agent.sast.installer.
Verify: Run poetry run python -m src.agent.sast.installer twice — first
downloads, second finds cached binary.
Step 2: Create SAST Module — Scanner
New file: src/agent/sast/scanner.py
No need for a detailed dataclass or structured parsing — the output goes straight to the LLM, which can understand any format. The goal is just to keep it short for token efficiency.
One public function:
run_sast_scan(repo_path, target_branch) -> str | None— the public entry point. Gets the binary viaensure_opengrep_binary(), passestarget_branchdirectly to--baseline-commit(same approach asget_file_diff.py:29which passes{target_branch}...HEADto git without computing merge-base). Runs[binary, "scan", "--config", "auto", "--json", "--baseline-commit", target_branch, "."]withcwd=repo_path. Usecheck=True— opengrep returns 0 even when findings exist (unlike git grep). Non-zero always means a real error.After getting JSON output, strip it down to only the fields the LLM needs and drop everything else. Fields to keep per finding (validated in Step 0):
check_id,path,start.line,end.line,extra.message,extra.severity,extra.lines. Drop:extra.metadata,extra.fingerprint,extra.metavars,extra.is_ignored,extra.validation_state,extra.engine_kind. Drop top-level:version,errors,paths,interfile_languages_used,skipped_rules.Also return the finding count separately (or print it directly) for the progress display.
Verify: Call on a repo with a known vulnerability on a feature branch. Confirm only new findings appear.
Step 3: Add SAST Prompt Guidance
New file: src/agent/prompts/sast_guidance.md
This file is appended to the system prompt when --sast is used. The original
full_review.md stays untouched. Content should cover two themes:
Be VERY skeptical of SAST findings — verify independently, don't blindly report, explain reasoning, dismiss false positives silently
Go beyond SAST — focus on what SAST cannot detect: logic errors, race conditions, design problems, context-dependent security, side effects. SAST findings are supplementary hints, not the focus.
Step 4: Wire SAST into Existing Code
Thread the SAST data through 5 existing files. Each gets one new optional parameter — no existing behavior changes.
4a. src/agent/formatting/build_review_context.py
Add sast_findings: str | None = None param. If present, append it as an
additional section after the existing Commits/Changed Files/Diffs sections.
4b. src/agent/prompts/__init__.py
Add include_sast_guidance: bool = False param to build_review_system_prompt.
When true, load and append sast_guidance.md before additional_instructions
(so user instructions have final say). Follow the existing pattern at line
44-49.
4c. src/agent/agent.py
Add include_sast_guidance: bool = False param to create_review_agent. Pass
through to build_review_system_prompt (line 32).
4d. src/agent/runner.py
Add sast_findings: str | None = None param to run_review. Pass to
build_review_context (line 52) and derive include_sast_guidance from
sast_findings is not None for both build_review_system_prompt (line 55) and
create_review_agent (line 58-61).
4e. src/main.py
Add --sast flag in parse_arguments() (follow --verify pattern at line
41-45). In main(), after print_changed_files_summary() (line 133) and before
run_review() (line 148):
- Lazy import the sast module (avoids loading installer logic for normal runs)
- Call
run_sast_scan(repo_path, args.target_branch)— returns trimmed string orNone - Print finding count:
🔍 SAST: Found N potential issuesorNo findings - Pass result directly as
sast_findingstorun_review()
--verify compatibility: No changes needed to the verification pipeline.
review_result.user_message and review_result.system_prompt already carry
SAST data through — see src/main.py:161-167 where they're passed to
run_verification().
Verify: Run poetry run reviewcerberus --sast --target-branch main and
confirm SAST findings appear in the review. Test --sast --verify too.
Step 5: Write Tests
New files: tests/agent/sast/__init__.py, test_scanner.py,
test_installer.py
Scanner tests — test JSON trimming logic with sample opengrep output. Test
run_sast_scan() error paths with mocked ensure_opengrep_binary and
subprocess.run (follow mocking pattern from
tests/agent/verification/test_runner.py).
Installer tests — test platform detection logic, lookup order (mock
shutil.which, Path.is_file, env vars).
Run make test and make lint to verify.
Step 6: Update GitHub Action
Thread the --sast flag through 4 action files (follow the existing --verify
pattern exactly):
action/action.yml— addsastinput with default"false"(alongsideverifyat line 57-59)action/src/config.ts— addsast: booleantoActionInputs(line 90-95), read viacore.getInput("sast") === "true"ingetActionInputs()(line 100-125)action/src/review.ts— addsast: booleantoReviewConfig(line 9-15), push"--sast"to Docker args when true (after--verifyat line 71)action/src/index.ts— passsast: inputs.sastin config object (line 47-53)
Rebuild with cd action && npm run build and commit dist/index.js.
Step 7: Update Dockerfile
Pre-install opengrep to avoid download on every container run. Add after
COPY src ./src and before useradd:
- Set
PYTHONPATHandPATHearly (currently at lines 39-40, need to move up or duplicate) so the installer module can run RUN python -m src.agent.sast.installer- Copy the downloaded binary to
/usr/local/bin/opengrepsoshutil.which("opengrep")finds it regardless of which user runs the container
This way the runtime ensure_opengrep_binary() hits lookup step 2
(shutil.which) and never downloads.
Step 8: Update Documentation
README.md
- Add
--sastto CLI usage examples (after--verifyat line 101) - Add "SAST Integration (Experimental)" subsection in "What's Included" (after Verification Mode at line 143)
- Add
sastrow to GitHub Action inputs table (around line 280) - Add action example with
sast: "true"(after verification example at line 300)
DOCKERHUB.md
- Add
--sastto Command-Line Options (after--verifyat line 118)
spec/project-description.md
- Add SAST bullet to Core Features section
spec/implementation-summary.md
- Add "14. SAST Integration (Experimental)" section covering design, architecture, binary management, prompt approach, error handling
- Update project structure tree to include
sast/module
Files Summary
New files (7)
| File | Purpose |
|---|---|
src/agent/sast/__init__.py |
Module exports |
src/agent/sast/installer.py |
Binary download, caching, platform detection |
src/agent/sast/scanner.py |
Run opengrep, trim JSON output for LLM |
src/agent/prompts/sast_guidance.md |
Skepticism + go-beyond-SAST guidance |
tests/agent/sast/__init__.py |
Test package |
tests/agent/sast/test_scanner.py |
Test JSON trimming, error handling |
tests/agent/sast/test_installer.py |
Test platform detection, lookup order |
Modified files (11)
| File | Change |
|---|---|
src/main.py |
Add --sast flag, SAST orchestration block |
src/agent/runner.py |
Add sast_findings param |
src/agent/agent.py |
Add include_sast_guidance param |
src/agent/formatting/build_review_context.py |
Append SAST section |
src/agent/prompts/__init__.py |
Load SAST guidance when enabled |
Dockerfile |
Pre-install opengrep, move ENV up |
action/action.yml |
Add sast input |
action/src/config.ts |
Add sast to ActionInputs |
action/src/review.ts |
Add sast to ReviewConfig, pass --sast flag |
action/src/index.ts |
Wire sast through config |
README.md |
Document SAST feature |
Also update (docs)
| File | Change |
|---|---|
DOCKERHUB.md |
Add --sast to CLI options |
spec/project-description.md |
Add SAST to Core Features |
spec/implementation-summary.md |
Add section 14, update structure tree |
Edge Cases & Error Handling
| Scenario | Behavior |
|---|---|
| opengrep download fails | Fatal error, run aborts |
| opengrep crashes (exit code != 0) | Fatal error, run aborts (check=True) |
| no findings | Print "No findings", review runs normally |
| JSON trimming fails | Fatal error, run aborts |
--sast with --verify |
Works — SAST data flows through automatically |
--sast with --instructions |
Both work — instructions appended after SAST guidance |
--sast with --json |
Works — SAST doesn't change output schema |
no --sast flag |
Zero impact — SAST module never imported |
| unsupported platform | Fatal error, run aborts |
Verification Plan
- Step 0: Manually test opengrep CLI assumptions
- Per-step: Each step has its own verify checkpoint
- Unit tests:
make testpasses - Lint:
make lintpasses (mypy, isort, black) - Integration:
poetry run reviewcerberus --sast --target-branch main - Combined flags:
poetry run reviewcerberus --sast --verify - Action build:
cd action && npm test && npm run build - Docker: Build image, run with
--sast, confirm opengrep pre-installed