Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
95731d6
feat(cve-env): integrate cve-env as packages/cve_env (Phase 1 lift-an…
gadievron Jun 12, 2026
e0370e1
test(cve-env): satisfy raptor ruff-pr + fast-tier guard (PR #802 CI)
gadievron Jun 12, 2026
93de23e
fix(cve-env): floor interrupted-exit cost by turns under session auth
gadievron Jun 13, 2026
4ff38fd
fix(cve-env): fire interrupted-exit cost floor on nonzero token stubs
gadievron Jun 13, 2026
d302464
fix(cve-env): merge cumulative ResultMessage.usage via max(), not += …
gadievron Jun 15, 2026
1d24eaf
Merge remote-tracking branch 'origin/main' into feat/cve-env-integration
gadievron Jun 15, 2026
f00d8a6
fix(ci): cover core/threat_model + core/dataflow in subsystem filters
gadievron Jun 15, 2026
20a2129
fix(cve_env): address all CodeQL alerts on the cve-env port
grokjc Jun 16, 2026
87c3833
Refactor denied check for Docker Hub
grokjc Jun 16, 2026
9715465
fix(cve_env): code quality — URL scheme validation, narrowed exceptio…
grokjc Jun 20, 2026
fffc5c4
Merge branch 'main' into feat/cve-env-integration
grokjc Jun 20, 2026
05cc380
fix(cve_env): gate SDK-dependent tests with pytest.importorskip
grokjc Jun 20, 2026
72ec4a3
fix(cve_env): adversarial review fixes — container ownership, env san…
grokjc Jun 20, 2026
ca218f1
fix(cve_env): update TOML config test for CWD-autoload removal
grokjc Jun 20, 2026
bbdc401
fix(cve_env): round-2 test fixes — SDK gating + correctness
grokjc Jun 20, 2026
c06d8b0
fix(cve_env): round-2 adversarial review — 19 bug fixes
grokjc Jun 20, 2026
999f87e
fix(cve_env): audit JSONL blank first line from unconditional newline…
grokjc Jun 20, 2026
f90d3b2
fix(cve_env): audit boundary repair via single binary open
grokjc Jun 20, 2026
3e5d339
fix(cve_env): round-3 adversarial review — 120 issues across security…
grokjc Jun 21, 2026
0d1b4fd
fix(cve_env): fix continuation cost double-count + B2 structural test…
grokjc Jun 21, 2026
2aef5c8
chore(cve_env): remove duplicate `import pytest` in 22 test files
grokjc Jun 21, 2026
bf6fc8e
Merge branch 'main' into feat/cve-env-integration
grokjc Jul 17, 2026
e2c30f2
fix: CI filter coverage for core.build + mock nm in orchestrator test
grokjc Jul 17, 2026
9a088ab
fix(cve_env): harden SSRF, container isolation, and input validation
grokjc Jul 18, 2026
d385622
fix(cve_env): allowlist safe device nodes + allow_devices tool parameter
grokjc Jul 18, 2026
a663e39
fix(cve_env): add allow_devices to tool schema test expectation
grokjc Jul 18, 2026
1936b65
Merge remote-tracking branch 'upstream/main' into feat/cve-env-integr…
grokjc Jul 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 108 additions & 0 deletions .claude/commands/cve-env.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
---
description: CVE → Docker environment builder — agentic build + verify of a vulnerable app at its pre-patch version
dispatch: libexec/raptor-cve-env <subcommand> [args]
---

# /cve-env — Agentic CVE → Docker Environment Builder

Given a CVE ID, builds and verifies a Docker environment running the affected
application at its **pre-patch (vulnerable) version**. Fully agentic — an LLM
tool-use loop (Claude Code session auth, via the agent SDK) researches the CVE,
resolves the affected component + version, acquires it (vulhub compose, upstream
image, or source build), launches a container, and verifies the vulnerable
service is actually running.

## Arguments

If the user provides a CVE ID (e.g. `/cve-env CVE-2018-7600`), build immediately.
If the user types just `/cve-env` with no argument, ask which CVE to build.
If the user types `/cve-env doctor`, run the service-health check instead.

## Execution

Run via the libexec wrapper (not the Python CLI directly):

```bash
libexec/raptor-cve-env build <CVE-ID> [options]
```

The script prints a per-CVE outcome JSON to stdout and a human-readable summary
to stderr (suppress the summary with `--silent`).

**Options (build):**
- `--product P` / `--version V` / `--description D` — optional hints to seed research.
- `--max-turns N` — agent turn cap (default 96).
- `--max-cost-usd F` — per-build USD budget cap (default 1.80).
- `--max-turn-extensions N` / `--turn-extension-pct F` — productive-extension knobs (auto-grant more turns when on a building path; set extensions 0 to disable).
- `--audit-root DIR` — override where the audit JSONL + outcome sidecar are written (default: raptor `out/agentic/`).
- `--silent` — suppress the stderr human summary (for scripts scraping JSON from stdout).
- `--auto-cleanup-containers` / `--auto-prune-images` / `--auto-stop-colima` — opt-in post-build lifecycle teardown (also enabled via `CVE_ENV_AUTO_*` env vars).

**Health check** (pre-flight):

```bash
libexec/raptor-cve-env doctor [--strict]
```

Probes NVD, OSV, GitHub, Docker Hub, and alternate registries; prints a health
table with latency + rate-limit headers. Run before a batch to catch outages or
missing credentials early. `--strict` returns non-zero even on non-critical
(e.g. throttled) failures.

## Output

`build` prints an outcome JSON to stdout. Key fields:

```json
{
"cve_id": "CVE-2018-7600",
"status": "success",
"verify_passed": true,
"num_turns": 23,
"total_cost_usd": 0.41,
"method": "compose",
"audit_path": "out/agentic/manual-<ts>/CVE-2018-7600.jsonl"
}
```

**Artifacts** (under the audit root, default raptor `out/`):

| File | Contents |
|------|----------|
| `out/agentic/<run>/<CVE>.jsonl` | Append-only per-turn audit trail (tools, costs, outcome) |
| `out/agentic/<CVE>.outcome.json` | Outcome sidecar (survives a SIGKILL after build returns) |
| `out/refusals-log.md` | Any LLM AUP refusals encountered, with recovery status |

## The 5 stages

```
research → resolve → acquire → launch → verify
```

1. **research** — NVD/OSV/GitHub lookup to identify the affected component.
2. **resolve** — pin the vulnerable version + a matching base image/arch.
3. **acquire** — vulhub compose, upstream image pull, or source build.
4. **launch** — `docker run` / `docker compose up` with hardened flags (cap-drop, no-new-privileges, localhost-only ports).
5. **verify** — executor DAG (http/log/exec/tcp/stability checks) confirms the vulnerable service is live.

## After the build completes

1. Parse the JSON summary from stdout (last thing printed).
2. Read the human summary on stderr (unless `--silent`) — it states the pathway, outcome, verify check types, registries tried, and credential nudges.
3. Present to the user: status, build method, whether verify passed, turns + cost, and the audit path.
4. On a non-success outcome, the JSON includes `give_up_reason` / `give_up_detail` / `reason` explaining why.

## Error handling

| Exit code | Meaning |
|-----------|---------|
| 0 | build succeeded / doctor healthy |
| 1 | build ended non-success (see `give_up_reason`) / doctor `--strict` non-critical failure |
| 2 | usage error / doctor: a critical service is unreachable |

## Requirements

- Claude Code session auth (the agent SDK) — `ANTHROPIC_API_KEY` is honored if set but not required.
- A working Docker daemon (Colima on macOS) — `cve-env` builds and runs containers.
- `NVD_API_KEY` — optional (raises the NVD rate-limit tier).
- `GITHUB_TOKEN` — recommended (avoids GitHub API rate limits during research).
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,8 @@ Once inside, just say "hi" to get started, or jump straight to a command.
| `/validate` | Multi-stage exploitability validation pipeline (Stages 0-F) | Stable |
| `/codeql` | CodeQL-only deep analysis with SMT dataflow pre-screening | Stable |
| `/sca` | Software composition analysis: dependencies, advisories, supply-chain signals, SBOMs, and fixes | Beta |
| `/cve-diff` | Discover and diff the fix commit for a CVE across OSV, NVD, GitHub, and GitLab | Beta |
| `/cve-env` | Build and verify a Docker environment running a CVE's affected application at its pre-patch version | Experimental |
| `/exploit` | Generate proof-of-concept exploit code | Beta |
| `/patch` | Generate secure patches for confirmed vulnerabilities | Beta |
| `/fuzz` | Binary fuzzing with AFL++ and crash analysis | Stable |
Expand Down
47 changes: 47 additions & 0 deletions bin/cve-env
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
#!/usr/bin/env bash
#
# cve-env — agentic CVE -> Docker environment builder (argparse CLI)
#
# Given a CVE ID, builds and verifies a Docker environment running the
# affected application at its pre-patch version. Mirrors bin/cve-diff's
# launcher contract (env-strip + trust marker + PYTHONPATH).
#
# Install: add this directory to PATH, or symlink to a directory already on PATH.
#

set -euo pipefail

# Resolve symlinks to find the real script location
SCRIPT="$0"
while [ -L "$SCRIPT" ]; do
DIR="$(cd "$(dirname "$SCRIPT")" && pwd)"
SCRIPT="$(readlink "$SCRIPT")"
[[ "$SCRIPT" != /* ]] && SCRIPT="$DIR/$SCRIPT"
done
RAPTOR_DIR="$(cd "$(dirname "$SCRIPT")/.." && pwd)"

if [ ! -d "$RAPTOR_DIR/core" ]; then
echo "cve-env: cannot find RAPTOR installation at $RAPTOR_DIR" >&2
exit 1
fi

# Strip env vars that could inject code into the Python process.
# Sourced from core/security/_dangerous_env_strip.sh — single source of
# truth shared with bin/raptor + bin/cve-diff (DANGEROUS_ENV_VARS).
. "$RAPTOR_DIR/core/security/_dangerous_env_strip.sh"

if ! command -v python3 >/dev/null 2>&1; then
echo "cve-env: python3 not found" >&2
exit 1
fi

export RAPTOR_DIR
export PYTHONPATH="$RAPTOR_DIR:$RAPTOR_DIR/packages/cve_env"
# Artifacts (audit JSONL, outcome sidecars, refusals log) land under
# raptor's out/ tree — consistent with bin/cve-diff. Honors a caller's
# pre-set override.
export CVE_ENV_OUTPUT_ROOT="${CVE_ENV_OUTPUT_ROOT:-$RAPTOR_DIR/out}"
# Trust marker — libexec/ scripts refuse to run without one of
# CLAUDECODE, _RAPTOR_TRUSTED.
export _RAPTOR_TRUSTED=1
exec python3 -c "import sys; sys.argv[0] = 'cve-env'; from cve_env.cli import main; sys.exit(main())" "$@"
3 changes: 3 additions & 0 deletions core/security/prompt_envelope_audit.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,9 @@
"packages/cve_diff/cve_diff/agent/loop.py",
"packages/cve_diff/cve_diff/agent/prompt.py",
"packages/cve_diff/cve_diff/analysis/analyzer.py",
# cve-env agent (claude-agent-sdk loop; interpolates CVE advisory text)
"packages/cve_env/cve_env/agent/prompts.py",
"packages/cve_env/cve_env/agent/loop.py",
)


Expand Down
51 changes: 51 additions & 0 deletions libexec/raptor-cve-env
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
#!/usr/bin/env python3
"""raptor-cve-env — non-interactive CVE -> Docker environment builder.

Subcommands
-----------
build <CVE-ID> [--product P] [--version V] [--description D]
[--max-turns N] [--max-cost-usd F] [--audit-root DIR] [--silent]
[--auto-cleanup-containers] [--auto-prune-images] [--auto-stop-colima]
doctor [--strict]

Thin wrapper over cve-env's own argparse CLI (cve_env.cli.main). ``build``
prints the per-CVE outcome JSON to stdout and a human summary to stderr
(unless --silent); ``doctor`` prints a service-health table.

Exit codes:
0 build: success / doctor: healthy
1 build: non-success outcome / doctor: --strict non-critical failure
2 usage error / doctor: critical-service failure
"""
from __future__ import annotations

import os
import sys
from pathlib import Path

# Path setup: RAPTOR root for core.*, packages/cve_env for cve_env.*
_RAPTOR_DIR = Path(__file__).resolve().parents[1]

# ─── trust-marker check (do not import; inline by design) ───
if not (os.environ.get("CLAUDECODE")
or os.environ.get("_RAPTOR_TRUSTED")):
sys.stderr.write(
f"{sys.argv[0]}: internal dispatch script.\n"
" Run via 'bin/cve-env' instead.\n"
" Tests / power users: set _RAPTOR_TRUSTED=1 to bypass.\n"
)
sys.exit(2)
# ─── end trust-marker check ─────────────────────────────────

# Artifacts land under raptor's out/ tree (consistent with cve-diff).
# Must be set BEFORE importing cve_env.config — OUTPUT_ROOT is import-time.
os.environ.setdefault("CVE_ENV_OUTPUT_ROOT", str(_RAPTOR_DIR / "out"))

sys.path.insert(0, str(_RAPTOR_DIR))
sys.path.insert(0, str(_RAPTOR_DIR / "packages" / "cve_env"))

from cve_env.cli import main # noqa: E402 -- after path + trust setup


if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))
21 changes: 21 additions & 0 deletions packages/cve_env/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2026 Gadi Evron

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
18 changes: 18 additions & 0 deletions packages/cve_env/PROVENANCE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Provenance

This package was imported from the standalone repository **gadievron/cve-env**.

- Source: `gadievron/cve-env` @ `ba9f91c` ("Initial release: cve-env — agentic CVE → Docker environment builder")
- Imported: 2026-06-12
- Layout change on import: `src/cve_env/` → `packages/cve_env/cve_env/` (flat package, mirroring `packages/cve_diff/`). All `cve_env.*` imports are absolute and unchanged.
- Not copied: `pyproject.toml`, `uv.lock`, virtualenvs, caches, `cve-env.toml.example`. Dependencies are declared in the repo-root `requirements.txt` per raptor's "no per-package build config" convention.

Phase 1 of the integration is a behavior-preserving lift-and-shift: cve-env keeps its own agent loop (claude-agent-sdk), Docker tooling, dockerfile generation, config, and HTTP layer. It adopts **zero** raptor `core/` modules in this phase. Selective `core/` adoption is deferred to a later phase behind behavior-equivalence checks.

## Divergences from the imported snapshot

The vendored copy tracks upstream `gadievron/cve-env` with cherry-picked fixes applied on top of the `ba9f91c` snapshot:

- **Cost-floor on interrupted exits** (this PR) — ports upstream cve-env `89917d8` (PR #2): floors `total_cost_usd` by engine turn count when a build ends on an interrupted status, so interrupted runs no longer log ~$0. Files: `cve_env/config.py` (`estimate_cost_from_turns`), `cve_env/agent/loop.py` (`_floor_cost` + `_INTERRUPTED_EXIT_STATUSES`), `tests/unit/test_cost_floor_non_clean_exit.py`.
- **Follow-up (same PR):** ports the upstream session-auth-stub fix — the floor was gated on `input_tokens == 0 and output_tokens == 0`, but Claude Code session auth emits a tiny *nonzero* token stub (`in=10, out=2`), so the gate never matched in production and the floor was dead code. The gate now keys on interrupted-status membership only (the floor is a `max()` bounded by the budget cap, so it only raises). Found by a live 6-CVE smoke (`CVE-2019-11043` turn_cap logged `$0.095` for 97 turns).
- **Token double-count on cumulative ResultMessage.usage** (this PR) — ports upstream cve-env PR #4 (merged `43731d5`): `state.total_*_tokens` accumulated from BOTH per-message `AssistantMessage.usage` AND the session-cumulative `ResultMessage.usage` (SDK `types.py` "Cumulative API usage for the session"), double-counting tokens ~2x. Fix: merge the cumulative RM usage via `max()` (new `_merge_cumulative_tokens`), keeping the per-message `_accum_tokens` += path. Benign for cost under session auth (the token estimate never wins `_floor_cost`'s `max()`); fixes telemetry / API-key over-report. Files: `cve_env/agent/loop.py` (`_merge_cumulative_tokens` + RM-site call), `tests/unit/test_token_double_count.py`. Validated by the 2026-06-14 token A/B bench (benign-equivalent to the remove-RM alternative).
3 changes: 3 additions & 0 deletions packages/cve_env/cve_env/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
"""cve-env: LLM-agentic, self-healing CVE -> Docker environment builder."""

__version__ = "0.1.0"
10 changes: 10 additions & 0 deletions packages/cve_env/cve_env/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
"""Module entry point: ``python -m cve_env`` → the argparse CLI."""

from __future__ import annotations

import sys

from cve_env.cli import main

if __name__ == "__main__":
sys.exit(main())
1 change: 1 addition & 0 deletions packages/cve_env/cve_env/agent/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Agent loop, budget, audit, and LLM client for cve-env."""
77 changes: 77 additions & 0 deletions packages/cve_env/cve_env/agent/_activity.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
"""Tool-activity tracker for the connectivity circuit-breaker.

The SDK is SILENT during a long in-process MCP tool call (``include_partial_
messages`` is off and there is no transport keepalive), so the inter-message
idle-timeout in ``llm._run_query_once`` must EXCLUDE tool-execution time or it
would false-abort legitimate 600-900s builds (docker_build / compose /
image_resolve).

The tool wrappers in ``agent/tools.py`` mark start/end here. The idle watchdog
never fires while a tool is in flight, and otherwise measures idle from the
last tool's END — so it bounds ONLY true API-wait gaps (a dead/unreachable
Anthropic endpoint).

Single-process, single-agent-per-CVE model → a plain module global is correct
(each ``cve-env build`` is its own subprocess; reset() is called per query).

WARNING: All globals are thread-unsafe. Do not call from concurrent tasks sharing this import.
"""

from __future__ import annotations

import time

_in_flight: int = 0
_last_activity: float = 0.0
# Monotonic timestamp when the CURRENT in-flight batch began (the 0→1
# transition); 0.0 when idle. Lets the connectivity breaker bound how long a
# single tool may stay in flight (``inflight_age``) so a WEDGED handler (e.g. a
# docker subprocess stuck on a dead VM socket that run_with_timeout could not
# reap) trips the breaker instead of being exempted to the external wall.
_oldest_start: float = 0.0


def reset() -> None:
"""Reset state at the start of each SDK query (called by _run_query_once)."""
global _in_flight, _last_activity, _oldest_start
_in_flight = 0
_last_activity = time.monotonic()
_oldest_start = 0.0


def tool_start() -> None:
"""Mark that an MCP tool handler has begun executing."""
global _in_flight, _oldest_start
if _in_flight == 0:
_oldest_start = time.monotonic()
_in_flight += 1


def tool_end() -> None:
"""Mark that an MCP tool handler has finished (stamps last-activity)."""
global _in_flight, _last_activity, _oldest_start
_in_flight = max(0, _in_flight - 1)
_last_activity = time.monotonic()
if _in_flight == 0:
_oldest_start = 0.0


def tool_in_flight() -> bool:
"""True iff at least one MCP tool handler is currently executing."""
return _in_flight > 0


def inflight_age() -> float:
"""Seconds the OLDEST currently-in-flight tool has been running (0.0 if idle).

Measured from the 0→1 transition, so nested start/start/end report the age of
the FIRST start until the count returns to zero. Used by the breaker's
tool-in-flight MAX bound."""
if _in_flight <= 0:
return 0.0
return time.monotonic() - _oldest_start


def last_activity() -> float:
"""``time.monotonic()`` timestamp of the most recent tool end / reset."""
return _last_activity
Loading
Loading