Agentic C/C++ vulnerability scanner powered by Claude.
Lacuna (a gap or missing portion) autonomously scans C/C++ libraries for security vulnerabilities. A single Claude agent uses a rich tool set — code review, git history analysis, fuzzing with AFL++/libFuzzer, and sanitizer-instrumented compilation — to find what's missing in a library's security posture.
The agent runs in a sandboxed Docker container with no network access, ensuring the target code cannot exfiltrate data or phone home during analysis.
lacuna scan targets/libpng.yaml
│
├─ Stage source on HOST (git clone / tarball / local copy)
│
├─ Start Docker sandbox (network_mode: none from start)
│ └─ /workspace/<target>/ ← bind-mounted from host
│
└─ VulnerabilityAgent loop (Anthropic tool-use API)
├─ think — internal reasoning scratchpad
├─ bash — shell execution in sandbox
├─ read_file / write_file / list_directory
├─ search_code — ripgrep across the codebase
├─ git_log / git_show / git_blame
├─ compile — builds with ASan + UBSan
├─ run_fuzzer — AFL++ or libFuzzer
├─ read_crash — inspects fuzzer crash inputs
└─ emit_finding → structured finding → Markdown report
The agent decides its own strategy and iterates until it either exhausts its budget or reaches end_turn. Every scan writes a Markdown report and a full conversation JSON for replay and audit.
- Python 3.11+
- Docker
- An Anthropic API key
# Clone the repo
git clone <repo-url>
cd lacuna
# Configure environment
cp .env.example .env
# Edit .env and set ANTHROPIC_API_KEY=sk-ant-...
# Install (editable)
pip install -e ".[dev]"
# Build the sandbox image (one-time, ~5 min)
./scripts/build_sandbox.sh# Smoke test against the bundled tiny C library
lacuna scan targets/test_tiny.yaml --verbose
# Scan libpng
lacuna scan targets/example_libpng.yaml
# Cheaper dev model
lacuna scan targets/example_libpng.yaml --model claude-haiku-4-5-20251001
# Full deep-dive with Opus
lacuna scan targets/example_openssl.yaml --full-run
# Clean up after a scan
lacuna cleanReports are written to reports/<target>_<YYYYMMDD_HHMMSS>.md.
lacuna scan <target.yaml> Run a vulnerability scan
lacuna clean Stop sandbox, remove container, clear workspace/
lacuna report <scan-id> Re-render a report from a saved messages JSON
| Flag | Default | Description |
|---|---|---|
--model TEXT |
claude-sonnet-4-6 |
Model to use |
--max-iterations INT |
50 |
Max agent iterations |
--full-run |
off | Use claude-opus-4-6 (expensive, thorough) |
--verbose |
off | Stream every tool call/result to terminal |
--budget-awareness |
off | Inject remaining-iterations reminders into the agent's conversation |
name: libpng
version: "1.6.40"
language: c
source:
type: git # git | tarball | local
url: https://github.com/pnggroup/libpng
ref: v1.6.40 # tag, branch, or commit
description: "PNG reference library"
attack_surface_hint: "Focus on image parsing: png_read_image(), png_process_data()"
build_hint: "cmake -DPNG_TESTS=OFF . && make"For local sources (type: local), put the source tree under workspace_src/ and set path: workspace_src/<dir>. See targets/test_tiny.yaml for a working example.
The bundled workspace_src/tiny/ library is an intentionally vulnerable C target used for smoke testing. A correct scan should identify all three deliberate vulnerabilities:
| Function | Vulnerability |
|---|---|
parse_input() |
Stack buffer overflow — strcpy into a 64-byte stack buffer with no bounds check |
format_output() |
Format string vulnerability — caller-supplied string passed directly to snprintf as the format argument |
resize_buffer() |
Integer overflow — n * elem_size wraps on large inputs, causing malloc to allocate a far smaller buffer than expected |
| Tool | Description |
|---|---|
think |
Internal reasoning scratchpad — never executes anything |
bash |
Shell command execution inside the sandbox |
read_file |
Read a file from the sandbox filesystem |
write_file |
Write a file in the sandbox |
list_directory |
List a directory tree in the sandbox |
search_code |
Ripgrep-based code search |
git_log |
Commit history, filterable by path/date |
git_show |
Show a specific commit diff or file at a ref |
git_blame |
Attribute lines to commits |
compile |
Compile C/C++ with specified flags and sanitizers |
run_fuzzer |
Launch AFL++ or libFuzzer; returns structured summary |
read_crash |
Read a crash input from fuzzer output directory |
emit_finding |
Record a structured vulnerability finding |
| Model | Use Case | Est. Cost / Scan |
|---|---|---|
claude-haiku-4-5-20251001 |
Development, iteration, testing | ~$0.10–$0.50 |
claude-sonnet-4-6 |
Default production scans | ~$1–$5 |
claude-opus-4-6 |
Full deep-dive (--full-run) |
~$10–$50 |
The CLI prints total token counts and estimated cost after every scan.
The Docker sandbox enforces strict isolation:
network_mode: none— no internet access, evercap_drop: ALL— minimal Linux capabilitiescap_add: SYS_PTRACE— required for sanitizers and gdb onlyno-new-privileges: true- Workspace is bind-mounted read-write; no other host paths are mounted
Source staging (git clone, tarball download) always happens on the host before the container starts. The sandbox never has network access — not even briefly.
# Unit tests (no Docker required)
pytest tests/unit/ -v
# Integration tests (requires Docker + lacuna-sandbox image)
pytest tests/ -v
# Lint + type check
.venv/bin/ruff check lacuna/ --fix
.venv/bin/mypy lacuna/ --ignore-missing-imports- Create
lacuna/tools/my_tool.py, subclassBaseTool. - Add to
build_tool_registry()inlacuna/tools/__init__.py. - Write a unit test in
tests/unit/test_tools.py.
No other files need to change.
lacuna/
├── lacuna/
│ ├── agent.py # Core agent loop + tool dispatch
│ ├── cli.py # Entry point
│ ├── config.py # ScanConfig, TargetSpec
│ ├── context.py # Context window trimming
│ ├── prompts.py # System prompt construction
│ ├── report.py # Markdown + JSON report writer
│ ├── tools/ # 13 tool implementations
│ └── sandbox/ # Docker sandbox lifecycle
├── docker/sandbox/ # Sandbox Dockerfile (AFL++, clang, sanitizers)
├── targets/ # Declarative scan targets (YAML)
├── workspace_src/ # Committed source for local targets
├── workspace/ # Bind-mounted into sandbox at /workspace/
└── reports/ # Generated reports (gitignored)
| Variable | Required | Description |
|---|---|---|
ANTHROPIC_API_KEY |
Yes | Anthropic API key |
LACUNA_DEFAULT_MODEL |
No | Override default model |
LACUNA_MAX_ITERATIONS |
No | Override default max iterations (50) |
LACUNA_REPORTS_DIR |
No | Override default ./reports |
--verbosestreams every tool call and result to the terminal.- After a scan the sandbox container stays alive:
docker exec -it lacuna-sandbox bash - Every scan writes
reports/<target>_<timestamp>_messages.json— replay it withlacuna report <scan-id>to understand agent decisions. - To reset everything:
lacuna clean