Skip to content

feat(simulate): shadow simulation — what a band-aid would block before it is applied (G2) - #4

Merged
henleda merged 2 commits into
mainfrom
feat-g2-shadow-simulation
Jul 27, 2026
Merged

feat(simulate): shadow simulation — what a band-aid would block before it is applied (G2)#4
henleda merged 2 commits into
mainfrom
feat-g2-shadow-simulation

Conversation

@henleda

@henleda henleda commented Jul 27, 2026

Copy link
Copy Markdown
Owner

Roadmap G2. The safety spine proves a policy blocks the finding's exploit and passes one legit
request. It says nothing about the other million requests a day — and that gap is why security teams
leave controls in monitor mode. vpcopilot simulate replays a recorded sample against each candidate
on a spare LB and reports the would-block set before anything reaches the human gate.

Replay runs on a real LB rather than an offline evaluator (G1, deferred by decision): XC's own
enforcement is the only oracle that cannot silently disagree with XC.

Proven live on vpcopilot-lab, not asserted

policy evaluated would block rate verdict
deny-negative-pay-amount 200 1 — the exploit 0.5% within threshold
deny-too-broad (/api prefix DENY) 200 154 77.0% over threshold

Both acceptance criteria: the negative-amount policy blocked the exploit and none of the 60
legitimate transfers; a deliberately overbroad policy tripped. Enforcement was confirmed by canary
before counting; the LB was restored and the throwaway policy deleted after each run.

Three defects only the live run could find

Each is now pinned by a test. Unit tests with a stubbed transport could not have caught any of them.

  1. Counting before the edge enforced. Attach-then-replay measured an unenforced policy as
    harmless — 0/200 blocked, including the exploit. A canary built from the finding's own probe now
    confirms enforcement first, and an unconfirmed run says so rather than reading as safe.
  2. One TLS hiccup killed a whole simulation. Failures are now per-request and excluded from the
    rate rather than guessed as blocked or allowed.
  3. Attaching by name assumed the object existed. A fresh candidate has none, and referencing a
    missing policy broke the LB config outright — every replayed request failed in transit, not
    just the matched ones. Simulation now creates a throwaway <name>-vpcsim object and deletes it,
    so it never rides on, or mutates, the operator's own policies.

The gate: warn, don't veto

Over-threshold warns and requires an explicit override, which writes a simulate_override audit
record with the rate, threshold and actor. A hard block would be code refusing a human, inverting
DESIGN.md's "the model proposes, code disposes, a human approves". Silent when nothing was
simulated — this adds a check, never a prerequisite.

Redaction is a correctness requirement here

The redacted sample ships inside the evidence bundle, so redaction runs at ingest, not export:
Authorization / Cookie / api-key headers dropped, secret-looking body fields replaced (keeping
the document shape so matchers still see it), src_ip never carried into a record, and the counts
surfaced in simulation.json.

Honesty carried in the artifact, not only the docs

  • XC access logs capture no request bodybodies_available: false says a body_matcher can't
    be judged from them.
  • Zero evaluated reports as "zero evidence, not a clean result" rather than a comfortable 0%.
  • Caveats state the sample is one window, that replay is one-policy-at-a-time, and how many requests
    failed in transit.

Surfaces

vpcopilot simulate [--logs <path> | --from-tenant --source-lb <lb> --since 6h] ·
GET+POST /api/simulate · a ③ Simulate console step · a Blast radius panel in the HTML
report · simulation.json in the evidence bundle · bench/fixtures/traffic/nimbus-sample.jsonl
(200 records, one negative-amount transfer among 60 legitimate ones).

No-regression

Per the invariant added last change: apply is unchanged when nothing was simulated, a dry run is
never gated, FakeXC gained delete_service_policy to match the real client, and the console
renumber (④ Mitigate … ⑦ Benchmark) was swept across the UI and eight docs.

34 new tests · 245 pass · coverage 73% · ruff clean.

Deliberately not in scope

Simulation is not wired into run_pipeline: replay attaches a policy to an LB, and scan is
documented as performing no XC writes and being safe to run anywhere. Folding it in would break that.
G3 (feeding the refiner) stays its own item.

🤖 Generated with Claude Code

henleda and others added 2 commits July 27, 2026 10:45
…e it is applied (G2)

The safety spine proves a policy blocks the finding's exploit and passes one legit request. It says
nothing about the other million requests a day, and that gap is why security teams leave controls in
monitor mode. `vpcopilot simulate` replays a recorded sample against each candidate on a SPARE LB and
reports the would-block set before anything reaches the human gate.

Replay runs on a real LB rather than an offline evaluator (roadmap G1, deferred by decision): XC's
own enforcement is the only oracle that cannot silently disagree with XC.

- traffic.py — one RequestRecord shape from a HAR, a JSONL sample, or XC access logs (new read-only
  `XC.access_logs`, the only /data endpoint this client touches). Redaction runs at INGEST because
  the redacted sample ships in the evidence bundle: Authorization/Cookie/api-key headers are
  dropped, secret-looking body fields are replaced (keeping the document shape so matchers still
  see it), src_ip is never carried, and the counts reach simulation.json.
- simulate.py — snapshot -> create a throwaway policy -> attach -> confirm enforcement -> replay ->
  restore -> delete. One policy at a time: two candidates share FIRST_MATCH ordering, so a batched
  result would describe neither.
- Gate: over-threshold WARNS and requires an explicit override, which writes a `simulate_override`
  audit record. A machine veto would invert "the model proposes, code disposes, a human approves".
  Silent when nothing was simulated — this adds a check, never a prerequisite.
- Surfaces: `vpcopilot simulate [--logs | --from-tenant]`, GET+POST /api/simulate, a ③ Simulate
  console step, a Blast radius panel in the HTML report, simulation.json in the evidence bundle.

Live on vpcopilot-lab: the negative-amount policy blocked 1 of 200 recorded requests — the exploit —
and none of the 60 legitimate transfers (0.5%, under threshold); a deliberately overbroad /api DENY
blocked 154 of 200 (77%) and tripped it. Both acceptance criteria, measured rather than asserted.

Three defects only the live run could find, each now pinned by a test:
  1. Attaching and replaying immediately counted an UNENFORCED policy as harmless — 0/200 blocked,
     including the exploit. A canary built from the finding's own probe now confirms enforcement
     before counting, and an unconfirmed run says so rather than reading as safe.
  2. One transient TLS error aborted a whole policy's simulation. Failures are now per-request and
     excluded from the rate rather than guessed as blocked or allowed.
  3. Attaching by name assumed the policy object already existed. A fresh candidate has none, and
     referencing a missing policy broke the LB config outright — every replayed request failed in
     transit, not just the matched ones. Simulation now creates a throwaway `<name>-vpcsim` object
     and deletes it, so it never rides on, or mutates, the operator's own policies.

Honesty carried in the artifact, not just the docs: XC access logs capture no request body, so
`bodies_available` says a body_matcher cannot be judged from them; zero evaluated reports as "zero
evidence, not a clean result"; caveats state the sample is one window.

No-regression pins: apply is unchanged when nothing was simulated, a dry run is never gated, and
FakeXC gained `delete_service_policy` to match the real client. Console renumbered ④ Mitigate
through ⑦ Benchmark across the UI and eight docs. 34 new tests; 245 pass, coverage 73%.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…shots

The Simulate step rendered its empty state in the offline demo, and every screenshot still showed
the six-step nav.

demo/build_demo_out.py writes a curated `simulation.json`: the SQLi service policy replayed against
500 recorded requests, blocking 3 — all attacks on the login endpoint — for 0.6%, under the
threshold. That is the honest picture for this dataset: only `service_policy` is replayable (the
other six controls are parameterized by the engine rather than matched per request), so one policy
is the whole story, and the result shown is the surgical outcome the demo is about. Redaction counts
and the caveats ride along so the demo shows the honesty, not just the number.

Screenshots regenerated at 1200px/2x against a credential-free .env, and renamed to match the new
step numbers: 3-mitigate.png -> 4-mitigate.png, 5-retire.png -> 6-retire.png, plus a new
3-simulate.png. README and DEMO.md references updated; no stale names remain. report.png grew by the
Blast radius panel.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@henleda
henleda merged commit 3bd2c28 into main Jul 27, 2026
4 checks passed
@henleda
henleda deleted the feat-g2-shadow-simulation branch July 27, 2026 15:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant