From 86ec383084910ad1df040ef1c440c9907354e9a6 Mon Sep 17 00:00:00 2001 From: henleda Date: Wed, 5 Aug 2026 21:46:14 +0530 Subject: [PATCH] docs(demo): rewrite DEMO.md as a console-driven walkthrough of everything MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The old doc predated roughly half the product. It had no Simulate step, no CVE / manifest / OpenAPI inputs, no declarative-WAF emit, no BIG-IP lab, no audit sink, no CWE/OWASP mapping, no benchmark and no MCP — and it described two paths when there are now three meaningfully different levels of proof. Rewritten as a GUI walkthrough: A — offline, no cloud and no keys, from the curated dataset B — live on F5 XC: a real exploit blocked at a real edge C — the declarative WAF: the same policy emitted for BIG-IP AS3 and NGINX App Protect, answering "we are not an F5 shop" Each says up front what it needs and what it proves, with a note to pick one and finish it rather than run half of each. Adds a "when something declines mid-demo" table. Every decline in this tool is a deliberate answer — no band-aid fits, MITRE prohibits the obvious CWE, the replay could not be measured, the cure has not merged — and in front of a security audience those are the most credible moments available. The table gives the line to say for each. FIXTURE FIX, found while verifying the doc rather than writing it from memory: the curated summary has always claimed "9 candidates, 6 verified, 2 refuted, 1 dropped" while shipping only the 6 verified findings. So the report could not render its "Candidates the verify agent did not confirm" section at all — the demo advertised a funnel it was structurally unable to show, and the dataset contradicted its own metrics. demo/build_demo_out.py now ships the three non-verified candidates in findings.json but NOT in triage.json, which is exactly how a real run represents them. They are realistic false positives of the kind verify actually catches: an eval() sink that is not reachable, a "hardcoded password" that is a test fixture, and a missing rate limit that already exists upstream. The report now renders "3 of 9 candidates", the severity and OWASP charts still count 6, and the console's Review table still shows 6 rows. Verified by running Demo A end to end in a browser, not from memory: the console renders the CWE badges (`CWE-89 · mapped`), the chip row reads 9/6/5/6, the OWASP chart carries its `(no category)` bar, and the refuted section appears below the fold. Co-Authored-By: Claude Opus 5 (1M context) --- demo/build_demo_out.py | 31 +++- demo/out/audit.log | 26 +-- demo/out/findings.json | 40 +++++ demo/out/ledger.json | 20 +-- demo/out/report.html | 6 +- demo/out/run.json | 4 +- demo/out/simulation.json | 2 +- docs/DEMO.md | 338 ++++++++++++++++++++++++++++----------- 8 files changed, 339 insertions(+), 128 deletions(-) diff --git a/demo/build_demo_out.py b/demo/build_demo_out.py index 520ee15..5ced4d4 100644 --- a/demo/build_demo_out.py +++ b/demo/build_demo_out.py @@ -48,6 +48,30 @@ "exploit_sketch": "Diff the 'already registered' vs 'ok' responses to enumerate users."}, ] +# The three candidates the verify agent did NOT confirm. The curated summary has always claimed +# "9 candidates, 6 verified, 2 refuted, 1 dropped" while shipping only the 6 — so the report could +# not render the refuted section at all, and the dataset advertised a funnel it could not show. +# These are real-looking false positives of the kind verify actually catches: a sink that is not +# reachable, a "secret" that is a test fixture, and a rate limit that already exists upstream. +REFUTED = [ + {"id": "crapi-eval-007", "title": "Remote code execution via eval in report renderer", + "vuln_class": "command_injection", "severity": "critical", + "file": "services/community/render.js", "line": 118, + "description": "A template string reaches eval() in the report renderer.", + "exploit_sketch": "Inject a template expression that closes the string and calls process.exit.", + "snippet": "const out = eval('`' + tpl + '`'); // flagged"}, + {"id": "crapi-hardcoded-008", "title": "Hardcoded database password", + "vuln_class": "sensitive_data", "severity": "high", + "file": "services/identity/test/fixtures.js", "line": 9, + "description": "A database password appears as a literal in the source.", + "exploit_sketch": "Read the credential from the repository and connect directly."}, + {"id": "crapi-nolimit-009", "title": "No rate limit on password reset", + "vuln_class": "rate_abuse", "severity": "medium", + "file": "services/identity/reset.js", "line": 47, + "description": "The password-reset endpoint has no throttle in application code.", + "exploit_sketch": "Loop reset requests to enumerate accounts or exhaust the mail quota."}, +] + # control per finding (005 is code-cure-only) BANDAID = {"crapi-sqli-001": "service_policy", "crapi-bola-002": "api_schema", "crapi-mass-003": "waf", "crapi-bruteforce-004": "rate_limit", "crapi-tokenleak-006": "waf_data_guard"} @@ -138,10 +162,13 @@ def main(): # it never passes the pipeline choke point; without this the demo dataset would render as # "never classified" while a real run of the same findings classified fine. from vpcopilot.weakness import classify - for _f in FINDINGS: + for _f in FINDINGS + REFUTED: _g = classify(_f.get("vuln_class", "")) _f["cwe"], _f["owasp"], _f["cwe_source"] = _g["cwe"], _g["owasp"], _g["cwe_source"] - (OUT / "findings.json").write_text(json.dumps(FINDINGS, indent=2)) + # Refuted candidates ship in findings.json but NOT in triage.json — which is exactly how a + # real run represents them, and what makes the report render its "did not confirm" + # section. They carry no band-aid and are excluded from every count. + (OUT / "findings.json").write_text(json.dumps(FINDINGS + REFUTED, indent=2)) (OUT / "triage.json").write_text(json.dumps(TRIAGE, indent=2)) (OUT / "remediations.json").write_text(json.dumps(REMEDIATIONS, indent=2)) (OUT / "policies.json").write_text(json.dumps(POLICIES, indent=2)) diff --git a/demo/out/audit.log b/demo/out/audit.log index 01b8862..4b14428 100644 --- a/demo/out/audit.log +++ b/demo/out/audit.log @@ -1,13 +1,13 @@ -{"ts": "2026-08-05T13:06:09.732288+00:00", "action": "refine_apply", "run_id": "cfc003c6cfb6", "actor": "security-oncall", "host": "vpcopilot-demo", "tool_version": "0.1.0", "finding_id": "crapi-sqli-001", "namespace": "crapi-demo", "control": "service_policy", "policy": "deny-login-sqli", "lb": "crapi-lab", "passed": true, "attempts": 2, "before_after": {"before": {"exploit_status": 200, "exploit_blocked": false, "legit_ok": true}, "after": {"exploit_status": 403, "exploit_blocked": true, "legit_ok": true}}} -{"ts": "2026-08-05T13:06:13.732288+00:00", "action": "apply_timing", "run_id": "cfc003c6cfb6", "actor": "security-oncall", "host": "vpcopilot-demo", "tool_version": "0.1.0", "control": "service_policy", "finding_id": "crapi-sqli-001", "passed": true, "elapsed_s": 48.0, "attempts": 2, "before_after": {"before": {"exploit_status": 200, "exploit_blocked": false, "legit_ok": true}, "after": {"exploit_status": 403, "exploit_blocked": true, "legit_ok": true}}} -{"ts": "2026-08-05T13:07:05.732288+00:00", "action": "create_api_definition", "run_id": "cfc003c6cfb6", "actor": "security-oncall", "host": "vpcopilot-demo", "tool_version": "0.1.0", "finding_id": "crapi-bola-002", "namespace": "crapi-demo", "name": "crapi-lab-apidef", "swagger": "crapi-lab-swagger"} -{"ts": "2026-08-05T13:07:09.732288+00:00", "action": "apply_api_schema", "run_id": "cfc003c6cfb6", "actor": "security-oncall", "host": "vpcopilot-demo", "tool_version": "0.1.0", "finding_id": "crapi-bola-002", "namespace": "crapi-demo", "apidef": "crapi-lab-apidef", "lb": "crapi-lab", "passed": true, "kept": true, "before_after": {"before": {"exploit_status": 200, "exploit_blocked": false, "legit_ok": true}, "after": {"exploit_status": 403, "exploit_blocked": true, "legit_ok": true}}} -{"ts": "2026-08-05T13:07:13.732288+00:00", "action": "apply_timing", "run_id": "cfc003c6cfb6", "actor": "security-oncall", "host": "vpcopilot-demo", "tool_version": "0.1.0", "control": "api_schema", "finding_id": "crapi-bola-002", "passed": true, "elapsed_s": 33.0, "attempts": 1, "before_after": {"before": {"exploit_status": 200, "exploit_blocked": false, "legit_ok": true}, "after": {"exploit_status": 403, "exploit_blocked": true, "legit_ok": true}}} -{"ts": "2026-08-05T13:07:50.732288+00:00", "action": "apply_waf", "run_id": "cfc003c6cfb6", "actor": "security-oncall", "host": "vpcopilot-demo", "tool_version": "0.1.0", "finding_id": "crapi-mass-003", "namespace": "crapi-demo", "app_firewall": "crapi-lab-waf", "lb": "crapi-lab", "config_enabled": true, "kept": true, "before_after": {"before": {"exploit_status": 200, "exploit_blocked": false, "legit_ok": true}, "after": {"exploit_status": 403, "exploit_blocked": true, "legit_ok": true}}} -{"ts": "2026-08-05T13:07:54.732288+00:00", "action": "apply_timing", "run_id": "cfc003c6cfb6", "actor": "security-oncall", "host": "vpcopilot-demo", "tool_version": "0.1.0", "control": "waf", "finding_id": "crapi-mass-003", "passed": true, "elapsed_s": 21.0, "attempts": 1, "before_after": {"before": {"exploit_status": 200, "exploit_blocked": false, "legit_ok": true}, "after": {"exploit_status": 403, "exploit_blocked": true, "legit_ok": true}}} -{"ts": "2026-08-05T13:08:19.732288+00:00", "action": "apply_rate_limit", "run_id": "cfc003c6cfb6", "actor": "security-oncall", "host": "vpcopilot-demo", "tool_version": "0.1.0", "finding_id": "crapi-bruteforce-004", "namespace": "crapi-demo", "rate": "5/MINUTE", "lb": "crapi-lab", "passed": true, "kept": true, "behavioral": {"sent": 30, "limited": 25, "passed": 5, "codes": {"200": 5, "429": 25}}} -{"ts": "2026-08-05T13:08:23.732288+00:00", "action": "apply_timing", "run_id": "cfc003c6cfb6", "actor": "security-oncall", "host": "vpcopilot-demo", "tool_version": "0.1.0", "control": "rate_limit", "finding_id": "crapi-bruteforce-004", "passed": true, "elapsed_s": 27.0, "attempts": 1} -{"ts": "2026-08-05T13:08:54.732288+00:00", "action": "apply_data_guard", "run_id": "cfc003c6cfb6", "actor": "security-oncall", "host": "vpcopilot-demo", "tool_version": "0.1.0", "finding_id": "crapi-tokenleak-006", "namespace": "crapi-demo", "app_firewall": "crapi-lab-waf", "lb": "crapi-lab", "enabled": true, "kept": true} -{"ts": "2026-08-05T13:08:58.732288+00:00", "action": "apply_timing", "run_id": "cfc003c6cfb6", "actor": "security-oncall", "host": "vpcopilot-demo", "tool_version": "0.1.0", "control": "waf_data_guard", "finding_id": "crapi-tokenleak-006", "passed": true, "elapsed_s": 19.0, "attempts": 1} -{"ts": "2026-08-05T13:09:21.732288+00:00", "action": "open_pr", "run_id": "cfc003c6cfb6", "actor": "security-oncall", "host": "vpcopilot-demo", "tool_version": "0.1.0", "finding_id": "crapi-sqli-001", "finding": "crapi-sqli-001", "repo": "acme/crapi", "url": "https://github.com/acme/crapi/pull/311", "number": 311} -{"ts": "2026-08-05T13:09:25.732288+00:00", "action": "retire", "run_id": "cfc003c6cfb6", "actor": "security-oncall", "host": "vpcopilot-demo", "tool_version": "0.1.0", "finding_id": "crapi-sqli-001", "namespace": "crapi-demo", "control": "service_policy", "lb": "crapi-lab", "forced": false} +{"ts": "2026-08-05T16:05:09.359184+00:00", "action": "refine_apply", "run_id": "7bc42a95f23d", "actor": "security-oncall", "host": "vpcopilot-demo", "tool_version": "0.1.0", "finding_id": "crapi-sqli-001", "namespace": "crapi-demo", "control": "service_policy", "policy": "deny-login-sqli", "lb": "crapi-lab", "passed": true, "attempts": 2, "before_after": {"before": {"exploit_status": 200, "exploit_blocked": false, "legit_ok": true}, "after": {"exploit_status": 403, "exploit_blocked": true, "legit_ok": true}}} +{"ts": "2026-08-05T16:05:13.359184+00:00", "action": "apply_timing", "run_id": "7bc42a95f23d", "actor": "security-oncall", "host": "vpcopilot-demo", "tool_version": "0.1.0", "control": "service_policy", "finding_id": "crapi-sqli-001", "passed": true, "elapsed_s": 48.0, "attempts": 2, "before_after": {"before": {"exploit_status": 200, "exploit_blocked": false, "legit_ok": true}, "after": {"exploit_status": 403, "exploit_blocked": true, "legit_ok": true}}} +{"ts": "2026-08-05T16:06:05.359184+00:00", "action": "create_api_definition", "run_id": "7bc42a95f23d", "actor": "security-oncall", "host": "vpcopilot-demo", "tool_version": "0.1.0", "finding_id": "crapi-bola-002", "namespace": "crapi-demo", "name": "crapi-lab-apidef", "swagger": "crapi-lab-swagger"} +{"ts": "2026-08-05T16:06:09.359184+00:00", "action": "apply_api_schema", "run_id": "7bc42a95f23d", "actor": "security-oncall", "host": "vpcopilot-demo", "tool_version": "0.1.0", "finding_id": "crapi-bola-002", "namespace": "crapi-demo", "apidef": "crapi-lab-apidef", "lb": "crapi-lab", "passed": true, "kept": true, "before_after": {"before": {"exploit_status": 200, "exploit_blocked": false, "legit_ok": true}, "after": {"exploit_status": 403, "exploit_blocked": true, "legit_ok": true}}} +{"ts": "2026-08-05T16:06:13.359184+00:00", "action": "apply_timing", "run_id": "7bc42a95f23d", "actor": "security-oncall", "host": "vpcopilot-demo", "tool_version": "0.1.0", "control": "api_schema", "finding_id": "crapi-bola-002", "passed": true, "elapsed_s": 33.0, "attempts": 1, "before_after": {"before": {"exploit_status": 200, "exploit_blocked": false, "legit_ok": true}, "after": {"exploit_status": 403, "exploit_blocked": true, "legit_ok": true}}} +{"ts": "2026-08-05T16:06:50.359184+00:00", "action": "apply_waf", "run_id": "7bc42a95f23d", "actor": "security-oncall", "host": "vpcopilot-demo", "tool_version": "0.1.0", "finding_id": "crapi-mass-003", "namespace": "crapi-demo", "app_firewall": "crapi-lab-waf", "lb": "crapi-lab", "config_enabled": true, "kept": true, "before_after": {"before": {"exploit_status": 200, "exploit_blocked": false, "legit_ok": true}, "after": {"exploit_status": 403, "exploit_blocked": true, "legit_ok": true}}} +{"ts": "2026-08-05T16:06:54.359184+00:00", "action": "apply_timing", "run_id": "7bc42a95f23d", "actor": "security-oncall", "host": "vpcopilot-demo", "tool_version": "0.1.0", "control": "waf", "finding_id": "crapi-mass-003", "passed": true, "elapsed_s": 21.0, "attempts": 1, "before_after": {"before": {"exploit_status": 200, "exploit_blocked": false, "legit_ok": true}, "after": {"exploit_status": 403, "exploit_blocked": true, "legit_ok": true}}} +{"ts": "2026-08-05T16:07:19.359184+00:00", "action": "apply_rate_limit", "run_id": "7bc42a95f23d", "actor": "security-oncall", "host": "vpcopilot-demo", "tool_version": "0.1.0", "finding_id": "crapi-bruteforce-004", "namespace": "crapi-demo", "rate": "5/MINUTE", "lb": "crapi-lab", "passed": true, "kept": true, "behavioral": {"sent": 30, "limited": 25, "passed": 5, "codes": {"200": 5, "429": 25}}} +{"ts": "2026-08-05T16:07:23.359184+00:00", "action": "apply_timing", "run_id": "7bc42a95f23d", "actor": "security-oncall", "host": "vpcopilot-demo", "tool_version": "0.1.0", "control": "rate_limit", "finding_id": "crapi-bruteforce-004", "passed": true, "elapsed_s": 27.0, "attempts": 1} +{"ts": "2026-08-05T16:07:54.359184+00:00", "action": "apply_data_guard", "run_id": "7bc42a95f23d", "actor": "security-oncall", "host": "vpcopilot-demo", "tool_version": "0.1.0", "finding_id": "crapi-tokenleak-006", "namespace": "crapi-demo", "app_firewall": "crapi-lab-waf", "lb": "crapi-lab", "enabled": true, "kept": true} +{"ts": "2026-08-05T16:07:58.359184+00:00", "action": "apply_timing", "run_id": "7bc42a95f23d", "actor": "security-oncall", "host": "vpcopilot-demo", "tool_version": "0.1.0", "control": "waf_data_guard", "finding_id": "crapi-tokenleak-006", "passed": true, "elapsed_s": 19.0, "attempts": 1} +{"ts": "2026-08-05T16:08:21.359184+00:00", "action": "open_pr", "run_id": "7bc42a95f23d", "actor": "security-oncall", "host": "vpcopilot-demo", "tool_version": "0.1.0", "finding_id": "crapi-sqli-001", "finding": "crapi-sqli-001", "repo": "acme/crapi", "url": "https://github.com/acme/crapi/pull/311", "number": 311} +{"ts": "2026-08-05T16:08:25.359184+00:00", "action": "retire", "run_id": "7bc42a95f23d", "actor": "security-oncall", "host": "vpcopilot-demo", "tool_version": "0.1.0", "finding_id": "crapi-sqli-001", "namespace": "crapi-demo", "control": "service_policy", "lb": "crapi-lab", "forced": false} diff --git a/demo/out/findings.json b/demo/out/findings.json index 0b99d03..7f72b79 100644 --- a/demo/out/findings.json +++ b/demo/out/findings.json @@ -89,5 +89,45 @@ "cwe": "", "owasp": "API2:2023", "cwe_source": "" + }, + { + "id": "crapi-eval-007", + "title": "Remote code execution via eval in report renderer", + "vuln_class": "command_injection", + "severity": "critical", + "file": "services/community/render.js", + "line": 118, + "description": "A template string reaches eval() in the report renderer.", + "exploit_sketch": "Inject a template expression that closes the string and calls process.exit.", + "snippet": "const out = eval('`' + tpl + '`'); // flagged", + "cwe": "CWE-77", + "owasp": "", + "cwe_source": "mapped" + }, + { + "id": "crapi-hardcoded-008", + "title": "Hardcoded database password", + "vuln_class": "sensitive_data", + "severity": "high", + "file": "services/identity/test/fixtures.js", + "line": 9, + "description": "A database password appears as a literal in the source.", + "exploit_sketch": "Read the credential from the repository and connect directly.", + "cwe": "", + "owasp": "API3:2023", + "cwe_source": "" + }, + { + "id": "crapi-nolimit-009", + "title": "No rate limit on password reset", + "vuln_class": "rate_abuse", + "severity": "medium", + "file": "services/identity/reset.js", + "line": 47, + "description": "The password-reset endpoint has no throttle in application code.", + "exploit_sketch": "Loop reset requests to enumerate accounts or exhaust the mail quota.", + "cwe": "CWE-799", + "owasp": "API4:2023", + "cwe_source": "mapped" } ] \ No newline at end of file diff --git a/demo/out/ledger.json b/demo/out/ledger.json index be54930..41e5932 100644 --- a/demo/out/ledger.json +++ b/demo/out/ledger.json @@ -24,9 +24,9 @@ "no_bandaid": false, "has_cure": true, "ttl": { - "applied_at": "2026-08-05T13:15:09.715566+00:00", + "applied_at": "2026-08-05T16:14:09.347397+00:00", "ttl_hours": 168, - "expires_at": "2026-08-12T13:15:09.715566+00:00" + "expires_at": "2026-08-12T16:14:09.347397+00:00" } }, "crapi-bola-002": { @@ -54,9 +54,9 @@ "no_bandaid": false, "has_cure": true, "ttl": { - "applied_at": "2026-08-05T13:15:09.716815+00:00", + "applied_at": "2026-08-05T16:14:09.348285+00:00", "ttl_hours": 168, - "expires_at": "2026-08-12T13:15:09.716815+00:00" + "expires_at": "2026-08-12T16:14:09.348285+00:00" } }, "crapi-mass-003": { @@ -81,9 +81,9 @@ "no_bandaid": false, "has_cure": true, "ttl": { - "applied_at": "2026-08-05T13:15:09.717778+00:00", + "applied_at": "2026-08-05T16:14:09.348948+00:00", "ttl_hours": 168, - "expires_at": "2026-08-12T13:15:09.717778+00:00" + "expires_at": "2026-08-12T16:14:09.348948+00:00" } }, "crapi-bruteforce-004": { @@ -108,9 +108,9 @@ "no_bandaid": false, "has_cure": true, "ttl": { - "applied_at": "2026-08-05T13:15:09.720633+00:00", + "applied_at": "2026-08-05T16:14:09.349658+00:00", "ttl_hours": 168, - "expires_at": "2026-08-12T13:15:09.720633+00:00" + "expires_at": "2026-08-12T16:14:09.349658+00:00" } }, "crapi-tokenleak-006": { @@ -138,9 +138,9 @@ "no_bandaid": false, "has_cure": true, "ttl": { - "applied_at": "2026-08-05T13:15:09.721386+00:00", + "applied_at": "2026-08-05T16:14:09.350331+00:00", "ttl_hours": 168, - "expires_at": "2026-08-12T13:15:09.721386+00:00" + "expires_at": "2026-08-12T16:14:09.350331+00:00" } }, "crapi-userenum-005": { diff --git a/demo/out/report.html b/demo/out/report.html index 1048633..4ba5f41 100644 --- a/demo/out/report.html +++ b/demo/out/report.html @@ -62,7 +62,7 @@ .model .a{color:var(--grey)}.model .m{font-family:ui-monospace,Menlo,monospace;color:var(--f5)}

virtual-patch·copilot — scan report

-
target: crapi-lab · generated 2026-08-05 13:15 UTC
+
target: crapi-lab · generated 2026-08-05 16:14 UTC
6exploitable vulns
5mitigated live by XC
29.6stime to mitigate · 72,973× faster
vs
25 daysnormal change control
6code-fix PRs (the cure)

Run summary

9candidates
6verified
5band-aided
1code-cure only
5XC policies
6code-fix PRs
@@ -70,9 +70,9 @@

At a glance

each agent's model is set per-agent in config/agents.yaml — swap Claude / OpenAI / Gemini / Ollama with no code change
resolve · anthropic/claude-opus-4-8discover · anthropic/claude-opus-4-8verify · anthropic/claude-opus-4-8triage · anthropic/claude-opus-4-8generate · anthropic/claude-opus-4-8remediate · anthropic/claude-opus-4-8probe · anthropic/claude-opus-4-8refine · anthropic/claude-opus-4-8

Pipeline metrics

19.2stotal time
6.4sdiscover
4.1sverify
8.7ssynthesize
67%verify confirm-rate
0.86avg confidence
1dupe band-aids collapsed

verify: 9 candidates → 6 verified, 2 refuted, 1 dropped < 0.5 confidence

-

Findings & band-aid coverage

criticalSQL injection in logincrapi-sqli-001sqliservices/identity/login.js:42
service_policy · full✓ code fix drafted
Details

Description: The email field is concatenated straight into the auth query.

Exploit: email=" OR 1=1 -- lets any password through and dumps the users table.

Code cure: Fix sql injection in login

highBOLA on vehicle locationcrapi-bola-002broken_object_authzservices/identity/vehicle.js:88
api_schema · full✓ code fix drafted
Details

Description: Any authenticated user can read another user's vehicle GPS by id.

Exploit: Swap {id} to another user's vehicle uuid; server returns their live location.

Code cure: Fix bola on vehicle location

highMass assignment on profilecrapi-mass-003mass_assignmentservices/identity/dashboard.js:61
waf · partial✓ code fix drafted
Details

Description: The update handler binds the whole body, so `role` and `credit` are writable.

Exploit: POST {"role":"admin","available_credit":9999} — privilege + balance escalation.

Code cure: Fix mass assignment on profile

highJWT + card data in responsecrapi-tokenleak-006sensitive_dataservices/workshop/mechanic.js:120
waf_data_guard · partial✓ code fix drafted
Details

Description: The receipt payload echoes the full PAN and a signed service token.

Exploit: GET a receipt; response body contains the 16-digit card number in cleartext.

Code cure: Fix jwt + card data in response

mediumNo rate limit on OTP verifycrapi-bruteforce-004rate_abuseservices/identity/otp.js:30
rate_limit · full✓ code fix drafted
Details

Description: The 4-digit OTP endpoint has no throttle — brute-forceable in minutes.

Exploit: Fire all 10k OTPs; no lockout, no delay.

Code cure: Fix no rate limit on otp verify

mediumUsername enumeration on signupcrapi-userenum-005broken_authservices/identity/signup.js:25
no band-aid — code cure only✓ code fix drafted
Residual risk: no positive-security band-aid fits; ships as code-only.
Details

Description: Distinct errors for taken vs free emails leak which accounts exist.

Exploit: Diff the 'already registered' vs 'ok' responses to enumerate users.

Code cure: Fix username enumeration on signup

+

Findings & band-aid coverage

criticalSQL injection in logincrapi-sqli-001sqliservices/identity/login.js:42
service_policy · full✓ code fix drafted
Details

Description: The email field is concatenated straight into the auth query.

Exploit: email=" OR 1=1 -- lets any password through and dumps the users table.

Code cure: Fix sql injection in login

highBOLA on vehicle locationcrapi-bola-002broken_object_authzservices/identity/vehicle.js:88
api_schema · full✓ code fix drafted
Details

Description: Any authenticated user can read another user's vehicle GPS by id.

Exploit: Swap {id} to another user's vehicle uuid; server returns their live location.

Code cure: Fix bola on vehicle location

highMass assignment on profilecrapi-mass-003mass_assignmentservices/identity/dashboard.js:61
waf · partial✓ code fix drafted
Details

Description: The update handler binds the whole body, so `role` and `credit` are writable.

Exploit: POST {"role":"admin","available_credit":9999} — privilege + balance escalation.

Code cure: Fix mass assignment on profile

highJWT + card data in responsecrapi-tokenleak-006sensitive_dataservices/workshop/mechanic.js:120
waf_data_guard · partial✓ code fix drafted
Details

Description: The receipt payload echoes the full PAN and a signed service token.

Exploit: GET a receipt; response body contains the 16-digit card number in cleartext.

Code cure: Fix jwt + card data in response

mediumNo rate limit on OTP verifycrapi-bruteforce-004rate_abuseservices/identity/otp.js:30
rate_limit · full✓ code fix drafted
Details

Description: The 4-digit OTP endpoint has no throttle — brute-forceable in minutes.

Exploit: Fire all 10k OTPs; no lockout, no delay.

Code cure: Fix no rate limit on otp verify

mediumUsername enumeration on signupcrapi-userenum-005broken_authservices/identity/signup.js:25
no band-aid — code cure only✓ code fix drafted
Residual risk: no positive-security band-aid fits; ships as code-only.
Details

Description: Distinct errors for taken vs free emails leak which accounts exist.

Exploit: Diff the 'already registered' vs 'ok' responses to enumerate users.

Code cure: Fix username enumeration on signup

Candidates the verify agent did not confirm

3 of 9 candidates. The discover agent proposed these; the adversarial verify step could not confirm them, so they carry no band-aid and are not counted anywhere else on this page. They are shown because "considered and rejected" is a result worth seeing — not because they are findings.

criticalRemote code execution via eval in report renderercrapi-eval-007command_injectionservices/community/render.js:118
Details

Description: A template string reaches eval() in the report renderer.

Exploit: Inject a template expression that closes the string and calls process.exit.

highHardcoded database passwordcrapi-hardcoded-008sensitive_dataservices/identity/test/fixtures.js:9
Details

Description: A database password appears as a literal in the source.

Exploit: Read the credential from the repository and connect directly.

mediumNo rate limit on password resetcrapi-nolimit-009rate_abuseservices/identity/reset.js:47
Details

Description: The password-reset endpoint has no throttle in application code.

Exploit: Loop reset requests to enumerate accounts or exhaust the mail quota.

Generated XC band-aid policies

api_schema crapi-lab-apidef
rate_limit otp-throttle
service_policy deny-login-sqli
waf crapi-lab-waf
waf_data_guard mask-pan
-

Band-aid impact exploit before → after (live validation)

controlpolicyexploit beforeexploit afterlegitresultwhen
service_policy self-healed ×2deny-login-sqli200 allowed403 blockedokPASS2026-08-05T13:06:09
apply_timing self-healed ×2200 allowed403 blockedokPASS2026-08-05T13:06:13
api_schemacrapi-lab-apidef200 allowed403 blockedokPASS2026-08-05T13:07:09
apply_timing200 allowed403 blockedokPASS2026-08-05T13:07:13
wafcrapi-lab-waf200 allowed403 blockedokfail2026-08-05T13:07:50
apply_timing200 allowed403 blockedokPASS2026-08-05T13:07:54
rate_limit5/MINUTEburst 30 allowed25/30 rate-limited (429)PASS2026-08-05T13:08:19

Blast radius what each band-aid would block in recorded traffic — 500 of 500 recorded request(s) replayed through crapi-lab · window 2026-07-20T09:00:00Z..2026-07-20T10:00:00Z

policyevaluatedwould blockrateverdicttop blocked pathscaveats
deny-login-sqli50030.6%within threshold/identity/api/auth/login ×3
+

Band-aid impact exploit before → after (live validation)

controlpolicyexploit beforeexploit afterlegitresultwhen
service_policy self-healed ×2deny-login-sqli200 allowed403 blockedokPASS2026-08-05T16:05:09
apply_timing self-healed ×2200 allowed403 blockedokPASS2026-08-05T16:05:13
api_schemacrapi-lab-apidef200 allowed403 blockedokPASS2026-08-05T16:06:09
apply_timing200 allowed403 blockedokPASS2026-08-05T16:06:13
wafcrapi-lab-waf200 allowed403 blockedokfail2026-08-05T16:06:50
apply_timing200 allowed403 blockedokPASS2026-08-05T16:06:54
rate_limit5/MINUTEburst 30 allowed25/30 rate-limited (429)PASS2026-08-05T16:07:19

Blast radius what each band-aid would block in recorded traffic — 500 of 500 recorded request(s) replayed through crapi-lab · window 2026-07-20T09:00:00Z..2026-07-20T10:00:00Z

policyevaluatedwould blockrateverdicttop blocked pathscaveats
deny-login-sqli50030.6%within threshold/identity/api/auth/login ×3

Remediation ledger found → mitigated → remediated → retired

findingstateband-aidcode cure
crapi-sqli-001retired{'control': 'service_policy', 'policy_name': 'deny-login-sqli', 'lb': 'crapi-lab'}{'pr_url': 'https://github.com/acme/crapi/pull/311', 'pr_number': 311}
crapi-bola-002remediated{'control': 'api_schema', 'policy_name': 'crapi-lab-apidef', 'lb': 'crapi-lab'}{'pr_url': 'https://github.com/acme/crapi/pull/312', 'pr_number': 312}
crapi-mass-003mitigated{'control': 'waf', 'policy_name': 'crapi-lab-waf', 'lb': 'crapi-lab'}
crapi-bruteforce-004mitigated{'control': 'rate_limit', 'policy_name': 'otp-throttle', 'lb': 'crapi-lab'}
crapi-tokenleak-006remediated{'control': 'waf_data_guard', 'policy_name': 'mask-pan', 'lb': 'crapi-lab'}{'pr_url': 'https://github.com/acme/crapi/pull/313', 'pr_number': 313}
crapi-userenum-005found
diff --git a/demo/out/run.json b/demo/out/run.json index 02fd888..c1f0c1f 100644 --- a/demo/out/run.json +++ b/demo/out/run.json @@ -1,6 +1,6 @@ { - "run_id": "cfc003c6cfb6", - "created": "2026-08-05T13:15:09.728200+00:00", + "run_id": "7bc42a95f23d", + "created": "2026-08-05T16:14:09.354173+00:00", "repo": "/src/crapi", "config_path": "config/agents.yaml", "models": { diff --git a/demo/out/simulation.json b/demo/out/simulation.json index e98faf6..ae8f738 100644 --- a/demo/out/simulation.json +++ b/demo/out/simulation.json @@ -1,5 +1,5 @@ { - "ts": "2026-08-05T13:15:09.733157+00:00", + "ts": "2026-08-05T16:14:09.360187+00:00", "lb": "crapi-lab", "source": "xc:crapi-lab", "records": 500, diff --git a/docs/DEMO.md b/docs/DEMO.md index c7450c2..2fc87dd 100644 --- a/docs/DEMO.md +++ b/docs/DEMO.md @@ -1,96 +1,231 @@ -# DEMO — virtual-patch-copilot in five minutes +# DEMO — the whole product, from the console -Two ways to run it. **Path A** needs no cloud and no API keys — it tells the whole story from a -curated dataset, ideal for a laptop walkthrough or a screen recording. **Path B** is the live -end-to-end: a real app behind F5 Distributed Cloud (XC), a real exploit blocked in minutes. +Everything here is driven from the **ops console** in a browser. No terminal is needed once the +server is up. CLI equivalents are given where you might want them, but the demo is the GUI. The one-line story to keep in mind: -> A frontier model finds exploitable vulns. The code fix needs 20–30 days of change control. -> The copilot puts an **XC band-aid in front of the app in minutes** — exploit blocked, legit -> traffic untouched — and opens the **code-fix PR (the cure)** in the same pass. When the cure -> ships, it **retires** the band-aid. Every step is gated, validated, and reversible. +> A frontier model finds exploitable vulns. The code fix needs 20–30 days of change control. The +> copilot puts an **F5 band-aid in front of the app in minutes** — exploit blocked, legit traffic +> untouched — and opens the **code-fix PR (the cure)** in the same pass. When the cure ships, it +> **retires** the band-aid. Every step is gated, validated, reversible and recorded. + +There are three demos, in increasing order of what they need. **Pick one and finish it** — they +tell the same story at different levels of proof, and running half of each is worse than running +one whole. + +| | needs | proves | wall clock | +|---|---|---|---| +| **A — Offline** | nothing | the whole arc, from a curated dataset | 5 min | +| **B — Live on F5 XC** | XC tenant + a model key | a real exploit blocked at a real edge | 12 min | +| **C — Declarative WAF** | B, plus a BIG-IP | the same policy, emitted for someone else's WAF | +6 min | --- -## Path A — the offline walkthrough (no XC, no keys) +## The console at a glance -```bash -pip install -e ".[console]" -python3 demo/build_demo_out.py # writes a curated demo/out (crAPI-flavoured) -VPCOPILOT_OUT=demo/out vpcopilot console # http://127.0.0.1:8787 ``` +1 Scan → 2 Review → 3 Simulate → 4 Mitigate → 5 Cure → 6 Retire → 7 Benchmark ⚙ Setup +``` + +The seven numbered steps are the arc, left to right. **⚙ Setup** holds credentials, the XC and +BIG-IP status panels, the audit sink and the per-agent model table. A **Run settings** bar +(`dry-run · rollback · LB · refine×3`) appears on the four steps that can change something — +Simulate, Mitigate, Cure, Retire — and never on the ones that cannot. + +The **hero band** across the top is the headline: exploitable vulns → mitigated live → time to +mitigate, against your change-control window. + +--- -Walk the steps top to bottom — the whole arc is already in the data: - -1. **② Review → hero band.** "6 exploitable vulns → mitigated live in ~30s, vs a 25-day change - window." Five XC control families are in play (service_policy, api_schema, waf, rate_limit, - waf_data_guard). One finding ships code-only (no band-aid fits) — honesty, not theatre. -2. **② Review → findings.** Click any row to inspect the exploit, the vulnerable code, the - generated band-aid, and the code cure. Note the SQLi row. -3. **② Review → Open HTML report ↗** (one click, right there in the step — no hunting in Setup). - The same story as a shareable, self-contained `report.html`: hero, severity/coverage bars, - model-independence, and the band-aid impact table where the SQLi service policy shows - **self-healed ×2** — the refiner's first policy didn't block, it diagnosed and retried until the - exploit actually returned 403 — and rate-limit shows the behavioral proof (25/30 requests 429'd). - **Download** grabs a stamped copy. It is rebuilt from the current out dir every time you open - it, so it is never a stale file. -4. **⑥ Retire → ledger.** The four-state track: `found → mitigated → remediated → retired`. - `crapi-sqli-001` is walked all the way to **retired** — its cure PR merged, so the band-aid was - detached. -5. **⑥ Retire → audit trail.** *Every change made to a load balancer*, one row each: when (UTC) · - action · justified by (the finding + severity) · control (+ the XC object) · load balancer - (+ namespace) · outcome (with the `200 allowed → 403 blocked` proof and a self-heal ×N badge) · - by. Filter it, expand `▸` for the raw JSON, then **Export evidence bundle (.zip)** — the - normalized `audit.csv`, the raw `audit.log`, the exact XC configs pushed, the pre-change LB - snapshots, and a manifest that SHA-256s every member. **All runs** does the same for every run - dir on disk. The curated log is hand-built, so a couple of rows carry no finding or actor; a - live apply (Path B) stamps both on every record. Detail: [AUDIT.md](AUDIT.md). - -The report also lives at `demo/out/report.html` — open it directly with no server. The same bundle -is available from the CLI: +## Demo A — offline (no cloud, no keys) ```bash -vpcopilot export --out demo/out # -> demo/out/audit-bundle.zip +pip install -e ".[console]" +python3 demo/build_demo_out.py # writes a curated demo/out (crAPI-flavoured) +VPCOPILOT_OUT=demo/out vpcopilot console # http://127.0.0.1:8787 ``` +Nine candidates, six verified — the whole arc already in the data. Walk the steps left to right. + +### ② Review — what was found, and what was not + +The chip row is the funnel: **candidates: 9 → verified: 6 → band-aids: 5 → code-fix PRs: 6**. + +The gap between 9 and 6 is the point. An adversarial verify step refuted three candidates — an +`eval()` sink that is not reachable, a "hardcoded password" that is a test fixture, a missing rate +limit that already exists upstream — and they are **not** silently dropped. In the HTML report they +render below the fold in their own section, labelled *"Candidates the verify agent did not +confirm"*, excluded from every count and every chart on the page. + +Say this out loud: *the tool argues with itself, and shows you the argument.* + +Click any finding row to expand the exploit, the vulnerable code, the generated policy and the +code cure. + +Each row carries its **CWE and OWASP API category**, with the tier spelled out — `CWE-89 · mapped` +versus `CWE-22 · advisory`. That distinction is the honesty: `advisory` means the OSV record named +the weakness and we are quoting it; `mapped` means the copilot classified it. Two findings carry +**no CWE at all**, and that is the correct answer — `CWE-840` is *prohibited* by MITRE for mapping +to real vulnerabilities, and username enumeration is not "weak authentication". A blank you can +explain beats a guess. + +### ② Review → **Open HTML report ↗** + +The shareable artifact, rebuilt from the run directory every time you open it, so it is never +stale. **Download** takes a stamped copy. + +Point at three things: + +1. **Findings by OWASP API Top 10** — and the `(no category)` bar. Injection was *removed* from the + 2023 API list, so `sqli` carries a CWE and no category. The chart accounts for every finding + rather than quietly charting a subset. +2. **The blast-radius table** — a verdict per policy, with a caveats column. A replay that could + not be measured says *not measured*, not a green 0.0%. +3. **Self-healed ×2** on the SQLi policy — the first generated policy did not block; the refiner + diagnosed it and retried until the exploit actually returned 403. + +### ③ Simulate — the blast radius, before anything is applied + +The step people do not expect, and the one that lands with security teams. The safety spine proves +a band-aid 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 controls sit in monitor mode. + +Simulate replays a recorded traffic sample against each candidate policy through a **spare** load +balancer and reports what each one *would* block. Nothing is applied; the spare LB is snapshotted +and restored. + +### ⑥ Retire — the ledger and the audit trail + +The four-state track: `found → mitigated → remediated → retired`. `crapi-sqli-001` is walked all +the way — its cure PR merged, so the band-aid was detached. + +Below it, **every change ever made to a load balancer**, one row each: when (UTC) · action · +justified by (finding + severity) · control · load balancer + namespace · outcome (with the +`200 allowed → 403 blocked` proof) · by whom. Expand `▸` for the raw JSON. + +**Export evidence bundle (.zip)** — the normalized `audit.csv`, the raw `audit.log`, the exact +configs pushed, the pre-change LB snapshots, and a manifest that SHA-256s every member. **All +runs** does the same across every run directory on disk. + +Two things worth saying while it downloads: + +- Dry runs are deliberately **absent** from the trail. Nothing changed, so there is nothing to + answer for. +- Overriding a protected load balancer writes its **own** audit event, so crossing a rail is + visible to anyone scanning the trail rather than buried in a field of an ordinary record. + +### ⑦ Benchmark + +Compare models per agent. The point is that *nothing here is Anthropic-specific* — every agent's +model is set in `config/agents.yaml`, and ⚙ Setup shows the current assignment for all eight. + --- -## Path B — live, behind XC (the real thing) +## Demo B — live on F5 Distributed Cloud -Prereqs in `.env`: `XC_API_URL`, `XC_API_TOKEN`, `XC_NAMESPACE`, a model key (e.g. -`ANTHROPIC_API_KEY`), and a `GITHUB_TOKEN` for PRs. Optional: `XC_DASHBOARD_URL` for the -"XC security dashboard ↗" deep link; `CHANGE_CONTROL_DAYS` to match the customer's number. +**⚙ Setup** first. Fill in `XC_API_URL`, `XC_API_TOKEN`, `XC_NAMESPACE` and a model key, then +**Save to .env**. A value already supplied through the environment shows as **(set in +environment)** — saving `.env` will not override it until the console restarts, and the page says +so rather than letting you believe a change took effect. -```bash -vpcopilot console # http://127.0.0.1:8787 -``` +Then `vpcopilot console`, and pick a lab load balancer in **Run settings**. + +### ① Scan — four kinds of input -1. **① Scan** a vulnerable app repo (VAmPI / crAPI / Nimbus). Watch discover → verify → triage → - generate → remediate stream live. The log box is scrollable and holds the **whole** transcript — - scroll up mid-scan to re-read the discover output and it stays put; a **↓ follow** chip and a - line counter appear until you scroll back to the bottom. Long scans no longer push the page down. -2. **② Review** the findings, and hit **Open HTML report ↗** if someone wants the artifact now. -3. **④ Mitigate.** With `dry-run` OFF and `keep live` ON (**Run settings** — the collapsible bar at the top of the Mitigate step), click - **Mitigate service_policy ▶** on a finding. The refiner streams in the row: attach → validate → - (refine → retry)* → **before 200 through → after 403 BLOCKED · legit ok**, with a *self-healed in - N attempts* badge if it took more than one try. It never claims success unless the live exploit - is actually blocked. -4. **XC security dashboard ↗** (hero band) — jump to the native WAF/API-Security telemetry to show - the block landing in XC. -5. **⑤ Cure → Open PR** on the same finding to draft the real code fix against your repo. -6. **⑥ Retire** once the cure merges — the band-aid is detached and the finding goes `retired`. The - loop is closed. Below the ledger, the **audit trail** now has a row per live change: which - finding justified it, which LB and namespace it touched, whether it stuck, and who ran it - (`VPCOPILOT_ACTOR`, else the OS user). **Export evidence bundle (.zip)** hands that to whoever - asks why the load balancer changed — see [AUDIT.md](AUDIT.md). - -Dry runs are deliberately *not* in the trail: nothing changed, so there is nothing to answer for. -The bundle is evidence for a human reviewer, not a compliance certification. - -Guardrails hold throughout: protected LBs (`VPCOPILOT_PROTECTED_LBS`, default `nimbus-www`) and -`nimbus-*` policies refuse mutation unless you explicitly opt in; every apply snapshots first and -rolls back on failure. +The step takes any of these, and the last three are the ones people have not seen before: + +| Field | What it does | +|---|---| +| **Target repo** | source scan — discover → verify → triage → generate → remediate | +| **…or a security advisory** | a CVE / GHSA / PYSEC id, resolved against OSV. No repo, no credentials | +| **…or an OpenAPI spec** | scans the *contract*. With a repo, also cross-checks the spec against the code | +| **…or dependency manifests** | every pinned package resolved against OSV — `requirements.txt`, `package-lock.json`, `pom.xml` | + +**Preview (no model calls)** surveys the dependencies without spending a token — a good answer when +someone asks what a run costs. + +The run log is the whole transcript and stays where you scroll it; a **↓ follow** chip appears if +you scroll up mid-scan. + +### ④ Mitigate — the moment that sells it + +**Run settings**: `dry-run` OFF, `keep live` ON. Then **Mitigate ▶** on a finding. + +The row streams: attach → validate → *(refine → retry)* → **before 200 through → after 403 +BLOCKED · legit ok**. It never reports success unless the live exploit is actually blocked, and +never reports a block without also confirming a legitimate request still passes. + +Seven control families are available — `service_policy`, `waf`, `waf_data_guard`, `api_schema`, +`rate_limit`, `malicious_user`, `bot_defense` — and triage picks per finding. Some findings get +**no band-aid at all**: plaintext password storage is a data-at-rest problem the edge never sees, +and the tool says "code cure only" rather than inventing a control for it. + +Then **XC security dashboard ↗** in the hero band, to show the block landing in F5's own telemetry. + +### ⑤ Cure → ⑥ Retire + +**Open PR** drafts the real fix against your repo. When it merges, **Reconcile & retire proven +fixes** re-fires the exploit at the app's **origin** — around the band-aid, so a blocked request +cannot be mistaken for a fixed bug — and only then detaches the control. + +If the cure has not merged, the console **refuses and tells you why**, with an explicit *retire +anyway* button. That is the pattern throughout: warn, explain, let a human override, record the +override. + +--- + +## Demo C — the declarative WAF (not just F5 XC) + +The objection you will hear is *"we are not an F5 shop"*. This answers it. + +### ② Review → **Emit** + +Pick a target and emit the same finding's policy as a declarative WAF policy for: + +- `xc` — F5 Distributed Cloud +- `bigip-awaf` — BIG-IP Advanced WAF, as an AS3 declaration +- `nginx-app-protect` — NGINX App Protect + +Same finding, same evidence, three vendors' syntax. Where a target genuinely cannot express a +control, it is listed as **unsupported** rather than emitted as something approximate. + +### ⚙ Setup → BIG-IP lab + +With an appliance configured, the panel shows the AS3 version, the tenants it can see, and the +protected ones. `/Common` is refused outright — not overridable, not even on a dry run, because +previewing the deletion of the appliance's own configuration is previewing an outage. + +The emitted policy is validated against the vendor's own published schema and — in the reference +lab — pushed to a real BIG-IP, where the exploit is fired and the account balance is checked +afterwards. The assertion is the **balance**, not the status code: BIG-IP's blocking page returns +HTTP 200, so a status-code assertion would pass on a policy that blocked nothing. + +--- + +## The safety rails, if someone asks + +All of these hold on **all three surfaces** — CLI, console and the MCP server: + +- **Protected load balancers** refuse mutation unless explicitly overridden, and the name is parsed + before the check, so `./nimbus-www` is refused too. +- **Every apply snapshots first** and rolls back on failure — and the rollback is *verified*, not + assumed. A control plane that accepts the restore and applies nothing raises loudly. +- **The blast-radius gate** blocks promotion of a policy that would block too much real traffic, + and honours the threshold you set rather than silently substituting a default. +- **Secrets never reach an artifact.** Headers, bodies *and query strings* are redacted before a + request sample is written, and the redaction is counted, so a sample cannot report itself clean. +- **Refusing to guess.** "We could not check this" and "this is clean" never render alike, anywhere. + An unreadable snapshot is not "no drift". A scan of a path that does not exist is refused, not + answered with an empty report. + +## For an agent audience: MCP + +`vpcopilot mcp` serves the pipeline over stdio as 15 tools — `scan_start`, `scan_status`, +`scan_result`, `impact`, `patches_list`, `deps`, `drift`, `simulate`, `ledger`, `reconcile`, +`verify_bundle` and more. Tools that would change a load balancer, open a PR or retire a control +are **absent** unless the operator started the server with writes enabled. The human gate is not +something a tool call can satisfy on its own. --- @@ -99,37 +234,46 @@ rolls back on failure. - **Band-aids, not cures.** Every mitigated finding also gets a code-fix PR. The band-aid buys the 20–30 days; the PR is the fix. The ledger tracks both to `retired`. - **Self-healing.** The copilot validates its own policy against the finding's real exploit and - refines until it works — so it never ships a band-aid that doesn't block. -- **Model-independent.** Every agent's model is set per-agent in `config/agents.yaml` (Workflow / - Model independence panels show it) — Claude, OpenAI, Gemini, or local Ollama, no code change. -- **Reversible + gated.** Snapshot → self-test → attach → validate → keep or rollback. A human - approves every live change in the console. -- **Auditable.** Every live change is recorded with the finding that justified it, the LB + - namespace it touched, whether it stuck, and who ran it — exportable as a .zip with a SHA-256 - manifest ([AUDIT.md](AUDIT.md)). It is evidence for a human reviewer, not a compliance - certification. + refines until it works, so it never ships a band-aid that does not block. +- **Model-independent.** Eight agents, each with its own model in `config/agents.yaml` — Claude, + OpenAI, Gemini or local Ollama, with no code change. +- **Reversible and gated.** Snapshot → self-test → attach → validate → keep or roll back. A human + approves every live change. +- **Auditable.** Every live change carries the finding that justified it, the LB and namespace it + touched, whether it stuck, and who ran it — exportable with a SHA-256 manifest + ([AUDIT.md](AUDIT.md)). Evidence for a human reviewer, **not** a compliance certification. + +## When something declines mid-demo + +Do not treat it as a failure — it is the product working, and in front of a security audience it is +the most credible thing that can happen. Read the reason aloud. + +| You see | Say | +|---|---| +| a finding with **no band-aid** | "the edge cannot see this one — it gets a code fix, and it says so" | +| a finding with **no CWE** | "MITRE prohibits the obvious mapping here; a blank we can explain beats a guess" | +| **"not measured"** in the blast radius | "that replay could not be measured — it will not pretend it came back clean" | +| a **refused retire** | "the cure has not merged; the band-aid is the only thing holding this shut" | +| a **refused protected LB** | "that one is off-limits, and the override would be recorded" | ## Screenshots -Captured from `demo/out` (Path A) and checked in under [`docs/images/`](images/) — they carry the -story on their own: +Checked in under [`docs/images/`](images/): | Shot | File | |---|---| | Scan — the target form and its scrollable run log | [`1-scan.png`](images/1-scan.png) | -| Review — hero band + findings + the HTML-report buttons | [`2-review.png`](images/2-review.png) | -| Simulate — blast radius of each candidate before it is applied | [`3-simulate.png`](images/3-simulate.png) | +| Review — hero band + findings + report buttons | [`2-review.png`](images/2-review.png) | +| Simulate — blast radius before anything is applied | [`3-simulate.png`](images/3-simulate.png) | | Mitigate — per-finding live apply | [`4-mitigate.png`](images/4-mitigate.png) | -| Retire — four-state ledger (`crapi-sqli-001` at *retired*) + the audit trail | [`6-retire.png`](images/6-retire.png) | -| The shareable HTML report (self-heal ×2 + rate-limit proof) | [`report.png`](images/report.png) | +| Retire — four-state ledger + audit trail | [`6-retire.png`](images/6-retire.png) | +| The shareable HTML report | [`report.png`](images/report.png) | -To regenerate them: rebuild the dataset with `python3 demo/build_demo_out.py`, run -`VPCOPILOT_OUT=demo/out vpcopilot console`, then capture the `#scan`, `#review`, `#mitigate` and -`#simulate`, `#mitigate` and `#retire` steps plus `demo/out/report.html` at 1200px wide / 2× device pixel ratio. +To regenerate: `python3 demo/build_demo_out.py`, then `VPCOPILOT_OUT=demo/out vpcopilot console`, +and capture each step at 1200px wide / 2× device pixel ratio. -Point the console at a **credential-free** `.env` when you do (`VPCOPILOT_ENV=…`): with XC creds +Point the console at a **credential-free** `.env` when you do (`VPCOPILOT_ENV=…`). With XC creds loaded, the hero band renders a deep link carrying your tenant hostname and namespace, and the -Mitigate step's drift strip renders the target LB's live control set — both would ship in the image. -Without creds the drift strip renders nothing at all, which is why it does not appear in -`4-mitigate.png`. `build_demo_out.py` curates `actor`/`host`/`out_dir` in the fixture for the -same reason — no real machine identity in a shared dataset. +BIG-IP panel renders your appliance's tenants — both would ship in the image. +`build_demo_out.py` curates `actor`/`host`/`out_dir` in the fixture for the same reason: no real +machine identity in a shared dataset.