Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions .claude/skills/INDEX.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ Run `python3 <skill>/scripts/<name>` (`bash` for `.sh`). `name: purpose — argu
- coverage_ledger.py: completeness check for a machine-report coverage map. — [--domains SPEC] <findings.json>
- derived_numbers.py: bind a doc number to the counter that derives it. — [--fix] [--selftest] [ROOT]
- design_drift.py: zero-token check that UI code still uses the project's design… — [--strict] [--json] [--tokens FILE]…
- dynamic_probe.py: opt-in dynamic probes of a LOCAL running app. — [--rate-endpoint PATH] [--burst N]…
- finding_ground_check.py: mark each gap finding grounded or not. — <findings.json> [--root DIR] [--ref…
- finding_verdict.py: strict verdict gate for machine-report gap rows that carry a… — <findings.json>...
- fix_class_gate.py: every fix commit must touch a pinned test, or say why not. — --base <ref> --head <ref> [--test-glob…
Expand Down
1 change: 1 addition & 0 deletions .claude/skills/deep-code-review/INDEX-scripts.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ Generated by `scripts/skill_index.py` (do not hand-edit). The script half of `IN
| `scripts/coverage_ledger.py` | completeness check for a machine-report coverage map. | coverage_ledger.py [--domains SPEC] <findings.json> |
| `scripts/derived_numbers.py` | bind a doc number to the counter that derives it. | python3 derived_numbers.py [--fix] [--selftest] [ROOT] Exit codes: 0 ok (or fixed); 1 drift or bad marker; 2 usage or s… |
| `scripts/design_drift.py` | zero-token check that UI code still uses the project's design tokens. | design_drift.py [--strict] [--json] [--tokens FILE] [--base REF] [PATH...] |
| `scripts/dynamic_probe.py` | opt-in dynamic probes of a LOCAL running app. | dynamic_probe.py [--rate-endpoint PATH] [--burst N] BASE_URL |
| `scripts/finding_ground_check.py` | mark each gap finding grounded or not. | finding_ground_check.py <findings.json> [--root DIR] [--ref GITREF] |
| `scripts/finding_verdict.py` | strict verdict gate for machine-report gap rows that carry a verdict. | finding_verdict.py <findings.json>... |
| `scripts/fix_class_gate.py` | every fix commit must touch a pinned test, or say why not. | fix_class_gate.py --base <ref> --head <ref> [--test-glob GLOB ...] [--trigger-glob GLOB ...] fix_class_gate.py --selfte… |
Expand Down
7 changes: 4 additions & 3 deletions .claude/skills/deep-code-review/INDEX.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,14 +72,15 @@ Generated by `scripts/skill_index.py` (do not hand-edit; rerun it after changing
| `references/domain-s.md` | domain S (Branches, merges & open-work triage) is applicable in Phase 2 — the coverage ledger marks… | 1179 |
| `references/domain-t.md` | domain T (Multi-tenancy & isolation) is applicable in Phase 2 — the coverage ledger marks it, or a… | 915 |
| `references/domain-w.md` | domain W (Workflows, jobs & scheduling) is applicable in Phase 2 — the coverage ledger marks it, or… | 2053 |
| `references/dynamic-probe.md` | static scanners came back clean (or are unavailable) and the target app runs locally: it probes for… | 472 |
| `references/energy-efficiency.md` | the target runs on a battery or radio (mobile, embedded, wearable, laptop client, browser tab), run… | 1167 |
| `references/example-review-report.md` | you want a finished FULL deliverable shape after install. | 98 |
| `references/frontend-a11y.md` | the target renders UI (web, mobile web, component library, or a server-rendered view). | 4976 |
| `references/frontend-security.md` | the diff sanitizes or renders user-controlled HTML, sets or edits a Content-Security-Policy, adds a… | 1564 |
| `references/full-audit.md` | the scope is FULL (never for DIFF or FILE). | 1646 |
| `references/full-audit.md` | the scope is FULL (never for DIFF or FILE). | 1689 |
| `references/full-map-principles.md` | a finding turns on principle 4-6, 10 or 11 (1-3 and 7-9 stay in full-map.md), or when setting or re… | 765 |
| `references/full-map-report.md` | writing the Phase 5 report or closing a review (Definition of done (a)). | 542 |
| `references/full-map.md` | the review is FILE/FULL scope, a DIFF over 300 lines or with a blast-radius flag, or anything past… | 4746 |
| `references/full-map.md` | the review is FILE/FULL scope, a DIFF over 300 lines or with a blast-radius flag, or anything past… | 4755 |
| `references/i18n-l10n.md` | the target renders user-facing text in more than one language or locale, accepts user-supplied Unic… | 2540 |
| `references/infra-evolution-by-stage.md` | the going-forward roadmap (report-format.md) must advise how a project's infrastructure and archite… | 1971 |
| `references/infra-iac-containers.md` | the target ships Dockerfiles, Kubernetes manifests/Helm, Terraform/Pulumi/CloudFormation, CI/CD wor… | 4342 |
Expand Down Expand Up @@ -151,4 +152,4 @@ Generated by `scripts/skill_index.py` (do not hand-edit; rerun it after changing

## Scripts

30 scripts. Read `INDEX-scripts.md` when you need a script (a trigger and CLI per script). `../INDEX.md`, `### deep-code-review`, has only each one-line purpose and arguments.
31 scripts. Read `INDEX-scripts.md` when you need a script (a trigger and CLI per script). `../INDEX.md`, `### deep-code-review`, has only each one-line purpose and arguments.
19 changes: 19 additions & 0 deletions .claude/skills/deep-code-review/references/dynamic-probe.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
Read this when static scanners came back clean (or are unavailable) and the target app runs locally: it probes for crash-on-bad-input, missing rate limiting, missing security headers and open redirects that source scanners miss. Opt-in; sibling of `app-pentest.md`.

## Run it

`python3 scripts/dynamic_probe.py [--rate-endpoint PATH] [--burst N] http://127.0.0.1:PORT` (beside the skill's other scripts). Only `localhost` and `127.0.0.1` are accepted (exit 2 otherwise; no allowlist override, unlike `app_pentest.sh`). Redirects are never followed. Use a disposable instance: the burst sends up to `--burst` (default 30, max 500) GETs to the named endpoint.

## Output

A JSON array in the `review_checks.sh` row shape (`file`, `line`, `rule`, `message`, `text`, `tool`). `rule` is a finding id, `clean` or `not-checked`:

- `server-error`: any 5xx on malformed percent-encoding (`/%`, `/%zz`, truncated UTF-8) or an oversized path or query. Typical root cause: an unhandled `URIError` / decode exception returning 500 instead of 400.
- `no-rate-limit`: no 429 within the burst on `--rate-endpoint`. Name the error or auth endpoint you care about; without the flag the row is `not-checked`.
- `missing-header`: CSP, `X-Content-Type-Options`, `Referrer-Policy`, `X-Frame-Options` (unless CSP `frame-ancestors`), and HSTS on https.
- `open-redirect`: a 3xx whose `Location` echoes an attacker URL passed via a common redirect parameter, on `/` only.
- `not-checked`: the endpoint was unreachable or not named. Never read it as clean.

## After the run

Findings are leads: reproduce by hand and apply `appsec-verify.md` section 1. `clean` covers only the inputs probed; a limiter keyed per IP or a redirect on another route is not seen. Say so; do not write "no vulnerabilities found". A burst can trip a limiter that exists but sits above `--burst`: raise it before filing `no-rate-limit`.
2 changes: 2 additions & 0 deletions .claude/skills/deep-code-review/references/full-audit.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,5 @@ Run a lens only when its trigger exists; record the grep and hit count either wa
id, status and evidence line in the `machine-report.md` ledger so the next run re-checks it automatically.
On a FULL re-run, load the prior machine report in `code-review/` for the same target by default (`machine-report.md` section 3, no `PRIOR` flag needed) and
emit `fixed` or `still-open` for every prior id before new hunting.

Opt-in dynamic step: read `dynamic-probe.md` when the app runs locally and static scanners found nothing (malformed-input 5xx, missing rate limit, headers, open redirect).
2 changes: 1 addition & 1 deletion .claude/skills/deep-code-review/references/full-map.md
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ applicable domains. Language footguns:
| T | Multi-tenancy & isolation | `domain-t.md` (when one deployment serves multiple tenants) |
| W | Workflows, jobs & scheduling | `domain-w.md` (when the target runs cron, queues, or multi-step workflows) |

Also: `app-pentest.md` (scan running app), `supply-chain-check.md` (advisories, Actions).
Also: `app-pentest.md` (scan running app), `supply-chain-check.md` (advisories, Actions), `dynamic-probe.md` (local probes).

---

Expand Down
134 changes: 134 additions & 0 deletions .claude/skills/deep-code-review/scripts/dynamic_probe.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
#!/usr/bin/env python3
"""dynamic_probe.py [--rate-endpoint PATH] [--burst N] BASE_URL — opt-in dynamic probes of a LOCAL running app.

Refuses (exit 2) any host but localhost / 127.0.0.1, userinfo, or a non-http(s) scheme. Never follows redirects.
Probes: malformed percent-encoding and oversized inputs (any 5xx is a finding), missing rate limiting (burst on
--rate-endpoint; without it that probe is not-checked), security headers, open redirect.
Prints a JSON array in the review_checks.sh row shape: {file,line,rule,message,text,tool}; rule is a finding id,
`clean` or `not-checked`. A clean row proves only what was probed. Read references/dynamic-probe.md before use.
"""
import http.client, json, ssl, sys
from urllib.parse import urlsplit

TOOL = "dynamic_probe"
rows = []


def row(path, rule, msg):
rows.append({"file": path, "line": 0, "rule": rule, "message": msg, "text": f"{rule}: {msg}", "tool": TOOL})


def req(base, path):
"""GET raw path (no normalisation, no redirects) -> (status, headers) or None when the connection fails."""
https = base.scheme == "https"
cls = http.client.HTTPSConnection if https else http.client.HTTPConnection
kw = {"context": ssl._create_unverified_context()} if https else {} # localhost self-signed certs
try:
c = cls(base.hostname, base.port, timeout=5, **kw)
c.request("GET", path)
r = c.getresponse()
r.read(1024)
return r.status, {k.lower(): v for k, v in r.getheaders()}
except (OSError, http.client.HTTPException):
return None


def probe_inputs(b):
cases = ["/%", "/%zz", "/%E0%A4%A", "/?q=%", "/?q=%zz", "/?q=%C0%AF", "/" + "a" * 10000, "/?q=" + "a" * 100000]
bad = dropped = 0
for p in cases:
r = req(b, p)
if r is None:
dropped += 1
elif r[0] >= 500:
bad += 1
row(p[:60], "server-error", f"GET returned {r[0]} for malformed or oversized input")
if dropped:
row("/", "not-checked", f"malformed/oversized inputs: {dropped} of {len(cases)} requests dropped, reset or timed out")
elif bad == 0:
row("/", "clean", f"no 5xx across {len(cases)} malformed/oversized inputs")


def probe_rate(b, ep, n):
if not ep:
return row("", "not-checked", "rate limit: pass --rate-endpoint PATH")
res = [req(b, ep) for _ in range(n)]
codes = [r[0] for r in res if r]
dropped = res.count(None)
if 429 in codes:
row(ep, "clean", f"429 seen within {n} requests")
elif dropped:
row(ep, "not-checked", f"rate limit: {dropped} of {n} requests dropped, reset or timed out, no 429")
else:
row(ep, "no-rate-limit", f"{n} rapid requests, no 429 (statuses seen: {sorted(set(codes))})")


def probe_headers(b):
r = req(b, "/")
if not r:
return row("/", "not-checked", "security headers: / unreachable")
h = r[1]
want = ["content-security-policy", "x-content-type-options", "referrer-policy"]
if "frame-ancestors" not in h.get("content-security-policy", ""):
want.append("x-frame-options")
if b.scheme == "https":
want.append("strict-transport-security")
miss = [w for w in want if w not in h]
for m in miss:
row("/", "missing-header", f"{m} absent")
if not miss:
row("/", "clean", "expected security headers present")


def probe_redirect(b):
evil, hit, dropped = "https://evil.example.com", 0, 0
for k in ("next", "url", "redirect", "redirect_uri", "return", "returnTo", "dest", "continue"):
r = req(b, f"/?{k}={evil}")
dropped += r is None
loc = r[1].get("location", "") if r and 300 <= r[0] < 400 else ""
if loc.startswith((evil, "//evil.example.com")):
hit += 1
row(f"/?{k}=", "open-redirect", f"{r[0]} Location echoes attacker URL")
if dropped:
row("/", "not-checked", f"open redirect: {dropped} requests dropped, reset or timed out")
elif not hit:
row("/", "clean", "no redirect to an off-site URL via common params (only / probed)")


def main(argv):
ep, n, args = "", 30, list(argv)
while len(args) > 1 and args[0] in ("--rate-endpoint", "--burst"):
flag, val = args[0], args[1]
args = args[2:]
if flag == "--rate-endpoint":
ep = val
else:
try:
n = int(val)
assert n > 0 and val.isascii()
except (ValueError, AssertionError):
print("usage: --burst needs a positive ASCII integer", file=sys.stderr)
return 2
n = min(n, 500)
if len(args) != 1:
print("usage: dynamic_probe.py [--rate-endpoint PATH] [--burst N] BASE_URL", file=sys.stderr)
return 2
try:
b = urlsplit(args[0])
b.port
except ValueError:
print("REFUSED: bad URL", file=sys.stderr)
return 2
if b.scheme not in ("http", "https") or b.username or b.password or b.hostname not in ("localhost", "127.0.0.1"):
print("REFUSED: base URL must be http(s)://localhost or 127.0.0.1, no userinfo", file=sys.stderr)
return 2
if ep and not ep.startswith("/"):
print("REFUSED: --rate-endpoint must start with /", file=sys.stderr)
return 2
probe_inputs(b), probe_rate(b, ep, n), probe_headers(b), probe_redirect(b)
print(json.dumps(rows, indent=1))
return 0


if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))
7 changes: 7 additions & 0 deletions changelog.d/wave-dynamic-probe.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
### Added
- deep-code-review `scripts/dynamic_probe.py`: opt-in dynamic step for a locally running app (localhost or 127.0.0.1 only, never follows redirects). Probes malformed percent-encoding and oversized inputs (any 5xx is a finding), a burst on a named endpoint for missing rate limiting, security headers, and open redirect, and prints review_checks-shaped rows (finding, clean or not-checked). Routed from `full-audit.md` via the new `references/dynamic-probe.md`. Tests: `scripts/test_dynamic_probe.py`.
size-budget-raise: .claude/skills/deep-code-review/references/full-audit.md 6584→6757 route line to the dynamic probe step
size-budget-raise: .claude/skills/INDEX.md 14885→14993 dynamic_probe.py script row
size-budget-raise: .claude/skills/deep-code-review/INDEX.md 20406→20551 dynamic-probe.md reference row and script count
size-budget-raise: .claude/skills/deep-code-review/INDEX-scripts.md 5356→5498 dynamic_probe.py script row
size-budget-raise: .claude/skills/deep-code-review/references/full-map.md 18985→19020 route to dynamic-probe.md
3 changes: 3 additions & 0 deletions scripts/ci-tests.d/dynamic-probe.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Registers the dynamic_probe.py stub-server test (localhost http.server only, no real host) with CI.
python3 scripts/test_dynamic_probe.py
python3 scripts/test_dynamic_probe_mock.py
5 changes: 3 additions & 2 deletions scripts/mustload-budgets.tsv
Original file line number Diff line number Diff line change
Expand Up @@ -313,12 +313,13 @@ lib / SDK 9368
# Routing dedupe: domain-checklists.md drops its A-W table (SKILL.md Domain map is the home), language-stack-redflags.md trims a pattern list, branch-and-merge-hygiene.md drops Cross-references and the standards-name list (full only). Floors: phase-floor-light 23115->22810 (-305), phase-floor-full 31334->30786 (-548) (net of +51 bytes in SKILL.md: the M and O Domain-map names echo their domain-m.md / domain-o.md triggers for trigger_lint); no archetype row changed.
# Slim SKILL.md split: the tables moved verbatim to references/full-map.md, which the gate now counts instead of SKILL.md (SKILL.md is the slim small-DIFF fast path, bounded by routing --max-bytes and scripts/test_slim_skill_fast_path.py). Floors: phase-floor-light 22810->22796, phase-floor-full 30786->30772 (-14 each); no archetype row changed.
# full-map.md core split: principles 4-6, 10 and 11 plus the Project stage model moved (principles 7 and 8, the read-only default and fetched-content-is-data guardrails, stay in the core because no finding trigger fires before a write or a fetch) verbatim to references/full-map-principles.md (read only when a finding turns on one of them); the Findings report and Definition of done (a) to references/full-map-report.md (read at Phase 5 / when closing); the Severity rubric stays in the core because every review ranks findings. Honest bytes per review, before->after: 22122 -> 20456 typical (core 18438 + report file 2018, -1666, -7.5%); 18438 core-only (-3684); worst case 23516 when the principles file also loads (+1394). Floors: phase-floor-light 22796->21877, phase-floor-full 30772->29856 (-919 and -916; the core counts, the split files are not Phase 0-2 mandatory loads); no archetype row changed.
phase-floor-light 18617
phase-floor-full 26596
phase-floor-light 18626
phase-floor-full 26605
# Train 2026-10-12: union of merged PRs nets the floors down 1 token each (23359->23358, 31578->31577).
phase-conditional method-situational.md
phase-conditional redflags-situational.md
# security-appsec.md split (web must-load cut): the A01 SSRF-guard-transport footguns and parallel-lane drift, the A06 business-flow detect steps, and the deterministic-corroboration map moved verbatim to appsec-egress.md, appsec-flows.md and appsec-corroboration.md (32790 -> 27956 B), each routed from the parent's index with a trigger and named in full-map.md's domain-B row. The untrusted-egress caller census and the client-trust-fields check stay in the parent because FILE/FULL scope never fires a diff-only trigger. security-appsec.md is must-load for four archetypes, so each drops 1208 tokens: web 20049->18841, mobile 14675->13467, api / service 19589->18381, agent / LLM / MCP 19610->18402. The three route names in full-map.md raise both floors by 17 tokens (phase-floor-light 18534->18551, phase-floor-full 26513->26530), the same routing cost as earlier lazy splits.
# Security verify route: full-map.md's domain-B row names appsec-verify.md with its trigger (+60 bytes) and security-appsec.md's index gains a one-line row for it (+42 bytes, 27998 B under the 28000-byte cap); the reference itself is conditional, off every must-load set. Archetypes web 18841->18851, mobile 13467->13477, api / service 16714->16724, agent / LLM / MCP 18402->18412 (+10 each). Floors: phase-floor-light 18578->18593, phase-floor-full 26557->26572 (+15 each).
# App pentest and supply-chain routes: full-map.md gains one Also line naming app-pentest.md and supply-chain-check.md (+101 bytes); both new references are conditional, off every must-load set. Floors: phase-floor-light 18593->18618, phase-floor-full 26572->26597 (+25 each); web 18851->18870, mobile 13477->13496, api / service 16724->16743, agent / LLM / MCP 18412->18431 (+19 each).
# Tool hub: full-map.md names tool-hub.md (one line), offset by wording trims in unpinned lines. Floors: phase-floor-light 18618->18617, phase-floor-full 26597->26596 (-1 each); no archetype row changed.
# Dynamic probe route: full-map.md Also line names dynamic-probe.md (+35 bytes); the reference is conditional, off every must-load set. Floors: phase-floor-light 18617->18626, phase-floor-full 26596->26605 (+9 each); no archetype row changed.
Loading
Loading