mirror of
https://github.com/CyberSecurityUP/NeuroSploit.git
synced 2026-09-29 20:41:51 +02:00
bc00fa61eb07e63d91cf33f4cbfe78dca4a297ae
235
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
bc00fa61eb |
feat(models,pocs): Grok 4.7 support; save every exploit/Frida script to the run's pocs/
Models: xai:grok-4.7 added as the preferred xAI model (API via XAI_API_KEY at api.x.ai/v1, and subscription via the local `grok` CLI) and to the OpenCode Zen list. `--model xai:grok-4.7` or `--subscription --model xai:grok-4.7`. PoC persistence hardened so exploitation artifacts are retrievable after a run: the POCS doctrine now explicitly requires saving repro scripts, Frida hook scripts (<slug>.frida.js), custom exploit code/source, compiled PoCs, request collections and adapted public PoCs into the run's pocs/ folder with a run command in the header — and the mobile mode now carries that directive too, so Frida bypass/hook scripts written during APK/IPA analysis are kept and re-runnable. 383 tests. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
4b71ac63a0 |
feat(mobile): binary/APK/IPA testing mode + 12 RE skills — v4.2.0
New `mobile` engagement mode: `neurosploit mobile <app.apk|app.ipa|binary>` reverse-engineers a local artifact with a dedicated `mobile` agent set, all headless and provisioned on demand (Ghidra analyzeHeadless, MobSF REST/Docker, Frida, apktool/jadx, radare2). Twelve original, generic skills (agents_md/mobile/, English): static binary triage, APK static analysis, IPA static analysis, RASP & anti-tamper mapping, root/jailbreak detection + bypass, TLS pinning detection + bypass, anti-debug detection + bypass, obfuscation analysis & deobfuscation, code-integrity / tamper-check bypass, hardcoded-secrets extraction, insecure local storage, and mobile network traffic analysis. Findings are proven from the artifact (decompilation or Frida trace), non-destructively. - agents.rs: new `mobile` Library category (loaded, counted). - pipeline.rs: run_mobile() mirroring the host pipeline with a mobile recon and headless tooling doctrine; exported from the crate. - CLI: `Cmd::Mobile` + `Mode::Mobile`, wired in main and the TUI. - README + TUTORIAL document the new test type; engagement-modes badge + table updated; "New in v4.2.0" note. Version bumped to 4.2.0 across the workspace. 383 tests. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
a4afd784c7 |
feat(mcp,tooling): NeuroSploit as an MCP server; tool-discovery + CVE-PoC + headless doctrine
MCP server (app/src/mcp.rs): `neurosploit mcp` speaks Model Context Protocol over stdio (JSON-RPC 2.0), exposing run / list_runs / findings / report / rebuild / internal / compliance as tools. Each shells out to the same binary, so scope, safety and authorization match the CLI. Install with `claude mcp add neurosploit -- neurosploit mcp`. Handshake, tools/list and a live call verified. TUTORIAL section 8 + README document setup for Claude Code, Codex and Cursor. Tooling doctrine expanded so the agent researches and provisions the BEST tool for the context instead of being limited to a fixed list: - context toolboxes (AD: netexec/impacket/bloodhound-python/certipy/kerbrute/ responder/evil-winrm; web recon; cloud; exploitation frameworks incl. metasploit/msfvenom; cracking) — provision on demand. - CVE -> PoC sourcing as a core capability: on a fingerprinted version (WordPress/plugin/CMS/OS package/service) go to searchsploit, Exploit-DB, GitHub, PacketStorm/Vulners, wpscan; clone/fetch, compile (gcc/go/cargo) and run the PoC non-destructively, vetted and time-boxed. - headless-only rule for GUI tools: mobsf (REST/Docker), ghidra analyzeHeadless, jadx/apktool/frida, radare2 — never require an X display. 383 tests. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>v4.1.0 |
||
|
|
56b2c80ae4 |
feat(decision): pluggable System One backend — TypeSafe (hosted) or Laya (local)
Laya (github.com/NandhaKishorM/laya) is the same System One abstraction as TypeSafe — identical choice/score/noul primitives — but local, open-source (Apache 2.0) and free. Added it as a swappable backend, entirely additively: the hosted TypeSafe path is byte-for-byte unchanged (key alone → same endpoint, model, bearer as before). - typesafe.rs: endpoint/model/bearer are now instance fields with env overrides (NEUROSPLOIT_DECISION_ENDPOINT / _MODEL). Defaults are the hosted TypeSafe API. from_env() now also activates when a local endpoint is configured (no key). backend_label() names the active backend in the run banner. - tools/laya_shim.py: a stdlib HTTP shim that loads Laya and exposes the exact POST /systemone contract the client already speaks. Model downloads on first use (HF cache); no key; evidence stays on the box. - CLI: --decision-backend typesafe|laya. `laya` installs laya if missing, starts the shim, waits for readiness, and points the client at it — all optional, only when the operator selects it. 383 tests; the hosted TypeSafe behaviour is untouched. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
d752e252e6 |
docs: drop links to the internal benchmark folder
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
d5d136ef34 |
chore: stop tracking benchmarks/ (internal only, not for the public repo)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
651b2bfc81 |
bench: A vs B·TS gap re-test — both close the gaps, TypeSafe calibrates severity
Re-ran the previously-missed scenarios on the current build without TypeSafe (A) and with (B). Both arms now confirm CRLF-on-Location, second-order SQLi, UNION SQLi, blind-time, IDOR and BOLA — the chaining/skill fixes are prompt-level, not TypeSafe-gated. TypeSafe's contribution is the severity shape: it consolidates A's long Low tail (10) into fewer, better-justified High findings (8 vs 3) and keeps the credential-dump BOLA at Critical via data-type grading. Artifact grid restored to A vs B·TS columns; both run arms stored. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
d334946915 |
bench: refresh the TypeSafe benchmark — 13/13 coverage, data-type-aware severity
Current-build run against the 13-scenario target with TypeSafe on: every seeded class confirmed with a live receipt, chained beyond the set into full admin takeover, GraphQL authz bypass, a config secret leak and an authenticated RCE. The credential-dump BOLA holds Critical because severity is graded on the kind of data exposed, not the class. report.html + run artifacts + README refreshed; no secrets committed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
fce86522ca |
feat(chain,skills): close benchmark misses — CRLF-on-Location, second-order precondition; condense BENCHMARK
The 13-target benchmark left 3 misses. Root-caused and fixed the two that were coverage gaps (the third was single-run variance, already handled by the session-limit fix): - CRLF header injection (web_crlf_header_go): the agent confirmed the open redirect on /go?url= and stopped; the CRLF payload was never generated. The open_redirect skill now tests %0d%0a header injection on the SAME param, and CHAIN_DOCTRINE says a param landing in a Location header must also be tested for response splitting. chain.rs: CWE-113/93/644 now provide capabilities; attack_graph maps their kill-chain stage. - Second-order SQLi (web_sqli_second_order): the sink was behind /admin, which the customer account could not reach. CHAIN_DOCTRINE now teaches the precondition pattern (store the payload, trigger from every identity, escalate first if the trigger page needs a role you lack, else report as a chained lead). chain.rs: CWE-564 requires PrivilegedContext so it chains after privesc. BENCHMARK.md: added the TypeSafe calibrated-adjudication row; dropped the "genuinely ahead" prose (the table is the summary); condensed the rest 188 -> 89 lines; refreshed scale (27 validators, 47 modules, 383 tests). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
dff2e3c0f0 |
chore: stop tracking articles/ (local-only, not for the public repo)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
c9e1f74e23 |
feat(cvss,typesafe): data-type-aware impact + evidence back-fill; LinkedIn article
Addresses the benchmark's honest edge (a genuine BOLA credential dump graded Low because evidence_data was null). Two fixes so criticals like it are not recalibrated away: - attack_graph::backfill_evidence — when evidence_data is null but the agent recorded a proof in prose, copy that text into the structured slot the grader reads (no fabrication, just relocation). Called first in enrich(). - attack_graph::data_class — classifies the demonstrated data (none/data/ sensitive) by scanning every evidence slot for credential/key/PII/payment signatures. cvss_graded now grants the confidentiality receipt when sensitive data was shown, even on a thin structured receipt — the KIND of data is itself the impact. - TypeSafe adjudication adds a `data_sensitivity` Score (public → PII → secrets), carried on Adjudication. The pipeline regrade only strips impact when the model was unconvinced AND no sensitive data was shown AND data_sensitivity is low; a demonstrated credential/PII exposure keeps its severity. articles/ — LinkedIn article (PT, no em-dashes) in Markdown + DOCX: explains TypeSafe/System One/Jev, NeuroSploit, how to configure TypeSafe, the step-by-step benchmark, results, the refinements this forced, and offensive-security use cases. 383 tests. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
088d133c80 |
release: v4.1.0 — assurance layer, TypeSafe, hardening + benchmark
Version bumped to 4.1.0 across the workspace, binaries, web console and Typst template. README: new "New in v4.1.0" summary; trimmed the verbose highlight bullets and the TypeSafe section; removed the anti-plagiarism/provenance section (provenance stays in the code, just not front-and-centre in the README); TypeSafe promoted to its own top-level section; agent count 446. TUTORIAL: new section 17 "Assurance & authorization" covering the target gate, --scope-file, evidence-graded CVSS, audit anchoring + assurance bundle, sandbox, intercept proxy, PoC re-validation, compliance mapping, TypeSafe, and the internal/AD graph + budget governor. benchmarks/typesafe-2026-09-20/: the with/without TypeSafe measurement — report.html, scorer, both runs' findings/assurance/meta/logs, and a README. No secrets committed (env-only during the runs, verified clean). 381 tests. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
d2ec0a112d |
fix(models): treat subscription session/usage-limit stdout as exhaustion
Subscription CLIs (claude) report a hit session limit as ordinary stdout with a ZERO exit code — 'You've hit your session limit · resets …'. Left as Ok it became a 'response' each agent then failed to parse, and the run burned every remaining agent against a dead session instead of pausing. Now the sentinel is caught (length-guarded so a real finding mentioning 'rate limit' is not misread) and surfaced as exhaustion, so the pool parks the run for /continue — the pause-on-quota path that already existed but this case never reached. Found during a live benchmark when run B collapsed to 0 findings mid-run. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
b23fae7318 |
fix(transport): exempt loopback from the fail-closed gate (unambiguous host)
127.0.0.1/localhost/::1 always mean this host regardless of any VPN, so the network-position ambiguity the gate guards against does not apply. Needed to run against a local benchmark target. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
7661b5eb4e |
fix(wiring): resolve the last two placebos — pomdp belief gate + harness-owned inbox
pomdp.rs was dead (belief.rs was on the path, pomdp wasn't). Now pomdp::may_assert runs as an anti-hallucination gate over the belief WorldModel: a finding asserted confirmed while the belief about it is diffuse or weak is held for review. Advisory — never deletes. inbox.rs was unused (mail.tm happened via agent prompt instructions). Now when temp-email is enabled the HARNESS creates the mail.tm inbox via crate::inbox and hands the agent that exact address, so the inbox is one we own and record rather than an unrecorded one the agent conjures. Best-effort; falls back to the old prompt path on failure. Every module is now on a real runtime path. 381 tests. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
8e84656f4f |
feat(typesafe): confirmation-loop agent + --typesafe flag (on/off/auto) for A/B
TypeSafe cannot BE an LLM agent — System One does not generate text or call tools. But it can be the decision brain of a code-owned confirmation loop, and that is what typesafe_agent.rs is: an ADDITIONAL confirmation strategy. typesafe_agent.rs — for enumerable classes (XSS, SQLi, open-redirect, path traversal, SSRF, IDOR): code lists candidate payloads, a TypeSafe Choice picks the next one given what's been tried, the replay engine sends it for real, a TypeSafe Noul judges the response, loop until confirmed or exhausted. Edge/WAF answers are refused. Pure parts (class table, payload templating, id-swap, OAST substitution, query encoding) are unit-tested; the networked loop is integration. Wired as a pipeline pass that runs ONLY on findings the LLM path left unconfirmed or in needs-review (the recall lever) — it can raise a finding to confirmed with a calibrated probability, never downgrades (the deterministic layer owns that). --typesafe on|off|auto (global flag) resolves into the env the pipeline reads, governing adjudication, CVSS re-grade, agent pruning and this loop together. `off` runs the identical pipeline without TypeSafe; meta.json records "typesafe": true|false so a with/without pair is a clean A/B measurement. Web console gets the same toggle. 381 tests. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
48c38d4798 |
fix(wiring): activate cvss.rs + waf.rs (were shelf-ware); TypeSafe into CVSS + agent selection
Honesty audit found four modules written but not on the runtime path. Two mattered and are now wired; two are noted. cvss.rs — was NOT called; finding.cvss came from the old attack_graph ladder. Now attack_graph::cvss_graded() bridges the class shape + demonstrated rung into crate::cvss::grade (the FIRST-verbatim v3.1 equation), and enrich() sets finding.cvss from the demonstrated vector, recording the potential ceiling in the impact text. The class ladder remains only as a fallback for findings with no evidence to grade. waf.rs — the deterministic classifier was NOT run on any real exchange (only WAF_OPS prompt text reached the agent). Now poc.rs classifies each re-run: a PoC answered by a WAF/CDN is Unverifiable, not "gone" — closing a false-demotion where an edge block looked like a fix. TypeSafe (System One) extended per the build-with docs: - CVSS via System One: when impact_demonstrated < 0.5, the finding's CVSS is re-graded with impact receipts stripped — the calibrated judgment, not just the rung, decides the demonstrated number. - Agent selection: typesafe_prune_agents() asks one batched request (the fan-out pattern), a Noul per chosen agent, and drops only those it calibrates as clearly irrelevant (p < 0.25), never prunes to empty. Additive over the LLM selection; skipped without a key. Still shelf-ware, flagged honestly (not wired): inbox.rs (mail.tm/SMS happens via agent prompt instructions, the Rust client is unused) and pomdp.rs (redundant — belief.rs is the one on the path). 374 tests. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
c3de51d508 |
feat(typesafe): System One calibrated adjudication (RLCD tier)
Integrates TypeSafe's System One model (Jev) as an optional, calibrated
adjudicator — the RLCD (Reinforcement Learning for Calibrated Decisions) tier:
typed judgments with probabilities where the harness needs a number, not prose.
- typesafe.rs: HTTP client for POST /v1/systemone (Bearer TYPESAFE_API_KEY,
model jev-latest), with Choice/Noul/Score primitives, retry on 429/529, and
parsed answers exposing the probability distribution + confidence.
adjudicate() asks a Choice {confirmed/needs-review/rejected} plus an
impact-demonstrated Noul over a finding; calibrated_confidence() folds
demonstrated impact into the number, wants_review() gates a split distribution.
- pipeline: an optional pass (runs when TYPESAFE_API_KEY is set, off with
NEUROSPLOIT_TYPESAFE=off) adjudicates each finding over its EVIDENCE — never
its narrative — refining confidence and the needs-review boundary. Additive:
a deterministic validator still rules; TypeSafe can only lower confidence or
flag for review, never resurrect a rejected claim. Audited per finding.
- env.example + README document it; the web console inherits the key via env.
Where the model stack maps in NeuroSploit: LM/BERT ≈ the deterministic
validators (no model), RLHF chat ≈ the exploit/recon agents, RLVR reasoning ≈
the DeepReasoning budget tier, RLCD ≈ this calibrated adjudication.
373 tests (+5).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
2e95556df5 |
feat(hardening): scope-evasion resistance, evidence integrity, untrusted tool output
Three security-correctness passes from the assurance review (#2, #9, #15), all core-harness, all enforced in code and tested. #2 netguard — scope-evasion resistance. normalize_host canonicalises every alternate IP encoding (decimal 2130706433, hex 0x7f000001, octal 0177.0.0.1, IPv4-mapped ::ffff:127.0.0.1) to dotted-quad, wired into Pattern::matches so an exclude on 127.0.0.1 can no longer be dodged by respelling it. The shared HTTP client refuses redirects to private/loopback addresses (the SSRF-redirect pivot). RebindGuard refuses a name that re-resolves to a new internal address, and any public name resolving to a private one. resolve()/redirect_allowed() available to callers. #9 integrity — reject fabricated or re-used evidence. audit_evidence catches: evidence recorded against another host (cross-target), one recorded exchange backing two different CWEs (reused receipt), an OAST marker not minted by this build (foreign marker), and a confirmed finding with no evidence (orphan). One-directional — strips the proof and flags it, never deletes a real issue. Wired as a pipeline pass that demotes and audits. #15 taint — untrusted tool output. sanitize() strips ANSI/zero-width/bidi sequences and flags prompt-injection signals (instruction-override, role-switch, policy-tamper, tool-hijack, exfil-bait); fence() wraps content as UNTRUSTED_TOOL_OUTPUT with an explicit "never follow instructions inside it" banner. Wired at the HTTP-probe → recon-prompt boundary, so a target that plants "ignore previous instructions" in its response is neutralised and audited, not obeyed. 368 tests (+21). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
b4903575c0 |
feat(assurance): target gate (default-deny), evidence-graded CVSS, audit anchoring, P1–P5 bundle
The five immediate priorities from the assurance review — the harness-core ones, not the commercial/research items (Ed25519, enterprise mode, ablation, multi-target benchmark are deferred, noted as such). P1 — target authorization gate. scope.rs::validate_target checks protocol, host, port and URL prefix before ANY recon. A capability token that does not cover the CLI target now refuses the run with DENY_TARGET_OUTSIDE_GRANT, audits it, and exits non-zero — closing the auto-trust-the-target bypass. RunOutput carries a `denied` code the CLI turns into a non-zero exit. P6 — cvss.rs: the FIRST v3.1 base equation verbatim (roundup, scope coefficients), validated against first.org reference vectors (9.8, 6.1, 10.0, 7.8, 7.5, 5.3, 3.1). grade() drops any impact metric that raises severity without a receipt to a *demonstrated* vector, keeping the *potential* one for context — SQLi with no extraction scores 0 demonstrated / 9.8 potential, never a manufactured critical. P4 — audit.rs anchoring: signed checkpoints of the chain head, local and (with NEUROSPLOIT_ANCHOR_DIR) external append-only. verify_anchored() catches truncation (chain shorter than an anchor) and silent rebuilds (head hash no longer matches), and forged anchors via signature. `neurosploit audit --anchor`. P5 — assurance.rs bundle: one assurance.json per run — every artifact with its SHA-256, which of P1–P5 it evidenced (present/partial/absent, never flattered), a bundle hash and a signature. `neurosploit assurance <run> [--verify]`. Also +8 deterministic validators earlier this session (19→27). Deferred and documented: Ed25519 tokens (#3), enterprise mode (#25), benchmark/ablation (#20/#21), model pinning + reproducibility (#22/#23), README claims taxonomy (#24), per-agent seccomp (#14). 347 tests. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
8894649ccb |
feat(scope): --scope-file YAML loader + web Scoping/Guardrails UI
Hard scoping was already enforced in code (every request passes
ScopePolicy::check_request; exclude beats allowlist; capability token caps
it; out-of-scope findings withheld + audited). What was missing was a way to
author that boundary from a file or the web form instead of only CLI flags.
- scope.rs: ScopePolicy::from_yaml / from_file — a dependency-free parser for
the friendly string format (app.example.com, *.wildcard, CIDR, url-prefix),
the same strings Pattern::parse already takes, NOT the raw serde {kind,value}
shape. Strict in one direction: an unreadable file errors, an empty hard list
authorizes nothing (a safe failure, but the operator's choice, not a typo).
- CLI: --scope-file <yaml>. Loaded before authorization so --in-scope adds to
it and the capability grant still caps it.
- Web: a full Scoping & Guardrails section in the Authorization tab — hard
scope, exclusions, observe-only, destructive-method + account-creation
toggles, max accounts, rate limit, forbidden payloads, notes. The server
materializes a scope YAML and passes --scope-file; notes stay labelled
"guidance, NOT enforced" so prose is never mistaken for a control.
- examples/scope.example.yaml documents the format.
End-to-end verified: web form -> YAML -> Rust loader -> enforced boundary.
332 tests (+4).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
f1fb6b8bc7 |
feat: PoC validator, Kali sandbox, intercept proxy, compliance, +8 validators
Closes the three benchmark gaps and adds the two the user asked for. poc.rs — re-runs each finding's recorded proof and sorts it into reproduced / changed / gone / unverifiable. The last two are kept apart deliberately: a PoC that could not be tested (out of scope now, state-changing, nothing recorded) is never reported as one that failed. Never re-runs a mutating request to "confirm" it. Can only lower a finding's standing, never raise it. Wired as a run pass (--revalidate-poc) and a subcommand (neurosploit poc <run> --apply). proxy.rs — own recording forward proxy (HTTP in full; HTTPS tunnelled with honest metadata, no fake CA) that chains upstream to Burp / Caido / ZAP / mitmproxy. A bare tool routes straight through it; own+tool records here and forwards for full TLS interception. Flows -> flows.jsonl, distinct hosts become passive-discovery leads. Harness and agent child commands share one route. sandbox.rs — Kali docker/podman container: no host network, no mounted socket, no-new-privileges, workdir mounted, proxy/transport env inherited. A missing runtime is an explicit error, never a silent fallback to host execution — the whole point being to keep attack payloads off the operator's host. Subcommands sandbox up|exec|install|down. compliance.rs — maps confirmed findings onto PCI-DSS v4.0, HIPAA Security Rule and SOC 2 controls. Phrased as "bears on control X", never "compliant/non- compliant"; the disclaimer is rendered on top and absence of a finding is never presented as compliance. Report section + `neurosploit compliance <run>`. validation.rs — 8 new deterministic validators (19 -> 27 classes): verbose errors/stack traces (CWE-209), cleartext/HSTS (319), CRLF response splitting (113), dangerous HTTP methods (650), GraphQL introspection, exposed backup files (530), Host header injection (644), cacheable private responses (525). Each names exactly what it saw and rejects the classic false positives (a block page echoing a payload, the SPA served under a bogus path, a copyright year mistaken for a code). All wired through RunConfig, the CLI (global --intercept/--sandbox; run-level --revalidate-poc/--compliance) and the web console's Tooling & assurance block. 328 tests. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
64d6efa8c3 |
feat(waf): tell the edge apart from the application
A WAF breaks inference in both directions and agents make both mistakes: a 403 from Cloudflare read as "tested, not vulnerable" (the expensive one — the app may be wide open and simply never reached), and a block page that echoes the payload read as reflection (the embarrassing one). classify() answers one question: did the application see this request? Proxy markers and enforcement markers are separate lists, because cf-ray is on every response Cloudflare proxies — treating that as a block would discard every finding on every CDN-fronted site, including the ordinary authorization 403s that are often the finding itself. Coverage::summary() says how many probes actually reached the application, so a clean result on a WAF-fronted target cannot be read as a clean app. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
8aa776665a |
feat(net): fail-closed egress, self-hosted OOB channel, inbound SMS
transport.rs — internal engagements happen through a VPN, a bastion or a tunnel, and the dangerous failure is silent: with the VPN down, 10.20.0.15 is a machine on the operator's own network and the scan succeeds against the wrong host. So an internal target with no transport is refused before any traffic leaves, and a transport that is up must prove it (the apparent source address has to change) rather than be assumed. Supports SOCKS, HTTP proxy, OpenVPN, SSH bastion (dynamic or single-host forward) and cloudflared. oob.rs — our own Collaborator, self-hosted by default because callbacks are engagement data (internal hostnames, resolver addresses, sometimes the exfiltrated value). HTTP and DNS listeners written on tokio directly, no new dependency. The two levels of proof are separated in code: an HTTP callback proves egress, a DNS query proves only that a resolver saw the name — the overclaim this channel otherwise invites. inbox.rs — mail.tm and inbound SMS (Twilio or webhook). extract_code() scores candidates by surrounding text and returns nothing rather than a guess, so a copyright year never gets submitted as an OTP. A throttling claim requires delivered messages carrying DISTINCT codes, not HTTP 200s. Wired through RunConfig, the CLI (global flags, so a session cannot re-route itself mid-engagement), the REPL and the web console's Authorization tab. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
90b4614d94 |
docs: benchmark vs Strix/Shannon/Penligent, README for budget, provenance, AD graph
BENCHMARK.md is a capability comparison, not a scored result — and it says so. It names the three places NeuroSploit is genuinely behind (no container isolation, no intercepting proxy, no benchmark anyone has actually run) as plainly as the places it is ahead. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
8d44e4cf4a |
feat(internal): internal network / AD attack graph with choke-point analysis
An internal engagement's result is a path, not a list of findings. This models it as one: Asset → Exposure → Weakness → Credential → Privilege → Movement → Crown Jewel, with business impact, detection and remediation hanging off the edges — because what a client fixes is a relationship, not a host. - the credential → identity → permission → machine → new credential loop, distance-bounded so a real chain turns it and an assumption-chain does not run forever - paths() separates what was walked from what is believed; one assumed hop caps the chain at informational instead of laundering it to critical - choke_points(): the single edge whose removal cuts the most value to crown jewels — the question a CVSS-sorted finding list cannot answer - detection_gaps(): unchecked reported as unchecked, never as unmonitored - dangling edges refused; re-adding a node upgrades rather than duplicates - `neurosploit internal --graph g.json --scaffold corp.local --from <node>` Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
408350539f |
feat(budget,provenance): reasoning budget modes and JOASNSCOPE provenance
Budget (opt-in, unlimited by default so an un-budgeted run is unchanged):
- crates/harness/src/budget.rs — modes, phase shares, Token Governor
- CLI: --budget/--token-limit/--deep-test-limit/--coverage-first/
--depth-first/--sample-per-route; same controls in the web wizard
- pipeline honours it: vote_n narrows, evidence rounds are capped
Run control parity in the web console:
- /pause in the REPL, backed by a pause gate in the model pool: in-flight
agents finish, then the run holds with every finding kept
- POST /api/exploit/:id/{pause,continue,report} + GET .../log
Provenance (crates/harness/src/provenance.rs):
- JOASNSCOPE sigil leads every canary, so a marker found in a response,
a log or someone else's report extracts whole and names its build
- per-build fingerprint, per-run id, optional per-customer build id
- findings.json stamped with _engine; signed provenance.json manifest
- structural signature survives rewording but not a changed result set
- prompts watermarked at the single pool chokepoint
- `neurosploit provenance show|scan|verify`
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
40b047b9e7 |
feat(agents): 10 skills for techniques the catalogue was missing
Written against what disclosed bug-bounty reports and public write-ups actually turn up, and chosen by diffing the existing 245 skills rather than restating them. Each one is built around the same discipline the harness now enforces: the client-side discovery is a lead, and the finding is what the SERVER did. Browser instrumentation and client trust: - browser_runtime_hooking — hook fetch/XHR/postMessage/storage/WebCrypto at document_start to find what the client is trusted to decide, then prove the server accepts the tampered value with an independent read-back. - prototype_pollution_gadget_hunt — pollution is a precondition, not a finding. Hunt the gadget with a getter on Object.prototype that breaks into the debugger, quote it as file:line from the bundle, and prove the end effect. - js_source_deep_analysis — recover original sources from source maps, extract the API surface the UI never shows, and pair every client-side discovery with the server request that confirms or refutes it. - client_side_path_traversal — the evidence is which URL left the browser, and it is Low until chained to something otherwise unreachable. Authentication: - webauthn_passkey_downgrade — passkeys as a system: the usual finding is a weaker factor nobody removed, or enrollment needing only a session. - email_verification_bypass — the gate is normally on the login screen, not the API; address normalisation is where pre-account-takeover lives. - jwt_jku_x5u_injection — whether the token gets to choose its own verifier. Server-side reach and money: - ssrf_render_pipeline — PDF/screenshot/unfurl renderers browse on the server's behalf, usually with no egress restrictions and often with JS enabled; the returned document is the exfiltration channel. - payment_webhook_forgery — prove the ORDER changed state, not that the endpoint returned 200; idempotency failures are their own finding. - presigned_url_abuse — the bug is what the API is willing to SIGN, which is an IDOR with a cloud signature on top. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
36c9e05ea7 |
feat(uncertainty): collect more evidence instead of handing a human a thin verdict
Everything upstream produced a verdict and stopped. When the evidence was thin the answer was needs-review — which hands a reviewer the same thin evidence and asks them to do the collecting. That is the wrong party: the harness still has the target, the session and the tooling; the reviewer has a paragraph. The uncertainty engine scores how undecided each candidate is by counting what its class needs against what it has, which makes "how sure are we?" a measurement rather than another model's opinion. A finding that is undecided AND whose gap is obtainable gets one more collection round before judgement. Two rules keep the loop from becoming a treadmill: - Only obtainable gaps trigger a round. A missing baseline is one request away; a confirmed account behind an email gate is not, and retrying it forever burns budget while nothing changes. Unreachable gaps are recorded, never retried. - Rounds are bounded per finding (two by default) and the remaining gap is written into the review reason, so a needs-review says exactly what was missing instead of sending a human looking from scratch. Actions are ordered by cost: a baseline is one request, a browser run costs seconds and a process. Spending the expensive step before the cheap one has had a chance to decide it is how a budget goes without buying anything. Merging is monotonic — a later run that did not see the marker does not unsee an earlier one that did. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
170ed3808e |
fix(pool): a voting panel of distinct providers, not one model asked n times
Every finding in the Arena engagement carried votes reading 1/1 while the run was configured for a 3-model vote. The panel was candidates.take(n), so a run with one configured model produced a "multi-model adversarial validation" that was one model agreeing with itself — the single most important thing the engagement revealed about the harness. Filling the panel by repeating that model would not fix it: its errors are correlated with themselves, and three confident repetitions of one mistake are indistinguishable from a consensus. Anthropic checking Anthropic is not independent; a second vendor is. build_panel() now takes at most one model per provider from the configured candidates, then fills any shortfall from backends this machine can actually reach (an installed subscription CLI, or a provider whose API key is in the environment). When nothing else is available the panel stays small and the yes/total the caller prints tells the truth about it, rather than being padded to look like a quorum. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
fb65074cab |
feat(chain): attack paths derived per vulnerability, after every agent has reported
The Arena engagement produced 24 findings, a graph with 83 edges, and exactly one chain edge. Two defects, both a step nobody was doing rather than a model reasoning badly. chains_from came back empty on every finding. An agent works one vulnerability and has no view of what the other twelve found, so asking it to link its result to findings it never saw was asking for something it cannot know. Chaining now happens after the whole set is visible, on rules about ENABLEMENT: what one weakness yields that another needs. Account enumeration yields valid identities; absent throttling turns them into unlimited guesses; a permissive password policy makes the guessing land. None is severe alone, and that sequence is how accounts get taken over — on the real data it now reads CWE-307 <- CWE-204, CWE-208 and CWE-614 <- CWE-319. The CWE->stage fallback sent 23 of 24 findings to initial-access, so the kill chain had one populated column and drew a star. Enumeration and side channels are discovery; missing throttling, password policy, cookie flags and session fixation are credential-access; hardening headers are recon. The same run now spreads across credential-access 13, discovery 6, initial-access 5. Two bugs the tests and the real data caught: - CWE-614 both yields session material and needs it, so a class chained to itself: duplicates formed a circular "attack path" from a cookie flag to the same cookie flag. A weakness class no longer enables itself. - apply_links only fills an empty chains_from, which is right for asserted chains and wrong for links written by an older version of these rules — a report kept the circular link through two rebuilds because nothing was allowed to touch it. repair() now drops links that cannot be true whoever wrote them: self-references, same-class links, dangling ids, cross-host links. enrich() still only fills empty fields during a run (an agent's judgement should survive), but a rebuild applies the current mappings via remap_stages — otherwise a finished run is frozen with whatever taxonomy existed that day. path_for() gives the per-vulnerability view: what precedes this finding, what it enables, and the narrative to print beside it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
0422b8dc41 |
feat(cvss): grade by the impact actually demonstrated, not by the class name
"SQLi = Critical" was the shortcut. The same weakness is a different finding
depending on how far the evidence took it, and the report has to be able to
defend the difference.
A ladder is read off the recorded observations:
reached the component → the mechanic is proven, impact is not
read data → confidentiality impact is real
read SENSITIVE data → and it is high
wrote (read-back) → integrity impact is real
executed code → the system is compromised
crossed to a second system → scope changes
The class now sets the CEILING and the evidence sets the score, so an injection
that reached the interpreter and extracted nothing no longer scores like one
that returned credentials.
Only observations climb it. "Could lead to remote code execution" stays at the
bottom rung — a test asserts exactly that, because prose is where inflation
enters.
Temporal metrics come from facts the engagement owns: E from whether a runnable
PoC exists, RC from the validation verdict (needs-review is Reasonable, never
Confirmed). They only ever lower the score.
A bug the tests caught: the first rung kept the class's availability impact, so
"reached" scored ABOVE "read data" — the ladder inverted at its first step.
Two older tests encoded the behaviour this replaces ("a bare CWE-89 must be
critical"). They now assert the new contract instead: a class name alone earns
no critical, and command execution scores like command execution only when
execution was observed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
9c6b2a3c54 |
feat(prosecutor): a second judge that shrinks claims instead of deleting findings
The existing voters answer "is this finding real?", which invites judging the whole narrative at once — and that is how a proven missing control got deleted for having an overstated headline. One lever, one verdict, observation gone. The Evidence Prosecutor has a narrower brief and four questions in order: what exactly was observed, which sentences go beyond that, what would have to be observed for the claimed impact to be factual, and — the one that decides retain-versus-reject — would anything security-relevant remain if the unsupported sentences were removed. It cannot pass sentence. There is no verdict field in its contract (a test asserts the prompt never offers one), and apply() can only narrow: the minimal supported statement replaces the mechanic, the asserted impact is demoted to potential with the conditions that would make it factual attached. Nothing it returns can raise a severity, add an impact, or drop a finding. A self-contradicting verdict — "nothing survives" alongside a minimal claim — is detected and the reading that keeps the observation wins: the claim is a concrete artifact, the boolean is an opinion about it. Findings from before the claim contract get a ledger reconstructed from what they recorded (structured evidence where present, quoted evidence lines otherwise), so the back catalogue is judged rather than silently skipped. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
3800b029f2 |
feat: browser validator for XSS, and replay that separates request from effect
Two halves of the same problem — proving what actually happened rather than what a response suggested. browser.rs — a payload echoed into HTML is reflection; it is XSS only when a browser parses that response and runs it. The gap between the two is where most XSS false positives live: the value lands in an attribute that is never evaluated, inside a <textarea>, HTML-encoded on the way out, or blocked by CSP. All four look identical to a string match on the body. So a real Chromium loads the URL and reports whether a marker THE HARNESS CHOSE came back through a channel only executing code can reach: a dialog message, a document.title assignment, a window global. A console line is watched too but never treated as decisive on its own — a page can log the value it reflected without ever running it. Payloads are self-reporting rather than generic (alert(1) proves nothing attributable) and cover the contexts a reflected value lands in: raw HTML, attribute break-out, event handler, URL, raw-text element, template expression. When the marker is reflected but does not execute, that is recorded as a note: it tells the operator the input reaches the response and the context is what stopped it. Missing node or playwright yields available:false and confirms nothing. A missing tool must never read as a missing vulnerability — or as a present one. replay.rs — the engagement recorded 25 accepted POSTs and concluded "reset email flooding". Those are facts at different layers and the pipeline could not say so. observe_effects() now records three: request_effect the response: status, headers, latency, size application_effect a read-back showing state actually changed external_effect something left the building (mail, webhook, job) Without a verification request the application layer is reported as unexamined rather than inferred from a 200 — APIs accept and ignore writes routinely. The external layer is honestly reported as unobserved until the harness owns a mailbox or callback listener. deepest_observed() gives the ceiling an impact claim may be built on, which is exactly the line the agent crossed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
22f2a3894d |
feat(claims): separate mechanic from impact so an overstatement stops deleting the observation
The Arena engagement rejected "no rate limiting on the password-reset flow" outright. The agent had proved 25 requests accepted with no 429, no Retry-After, no RateLimit-* — and then titled it "reset-email flooding". The voter judged the claimed impact unproven and discarded everything, so a real missing control never reached the report. That was structural, not a bad call. The judge got a prose paragraph and one accept/reject lever, while agents reliably walk: control absent -> abuse possible -> impact plausible -> impact written as fact. The chain has to break at step two, and that needs the finding to arrive as separable claims. claims.rs adds: - An evidence ledger (E01, E02, …) so a verdict is auditable: "supported by E01-E27" is checkable, "the evidence looks convincing" is not. A claim citing an id that was never recorded is REJECT_INVALID_EVIDENCE — worse than citing nothing, because it looks supported. - Mechanic and impact as separate claims, each with its own citations. An asserted status never outruns its evidence: a model may downgrade itself and can never upgrade past what it cited. - Six structured decisions instead of accept/reject. DOWNGRADE_UNPROVEN_IMPACT and DOWNGRADE_SCOPE_LIMITATION cannot discard — that is enforced by Decision::discards(), not by an instruction a model could reinterpret. - Impact preconditions: "email flooding" needs account_exists + account_confirmed + email_delivery_observed. 25 accepted requests prove throttling was not observed; they do not prove mail was delivered. The difference is now computed, not argued. - A rewriter, because lowering severity is not enough: a report headed "Password Reset Email Flooding" still asserts flooding whatever number sits beside it. The title is rebuilt from the mechanic, the impact prose becomes Observed / Not demonstrated / Potential impact, and the claimed consequence survives only as clearly labelled potential. - still_security_relevant(): strip the unproven impact and ask whether anything remains. "The reset endpoint has no observable rate limiting" does; "the application responded" does not. That decides retain-vs-reject. Wired into the pipeline ahead of the voters, and the voter can no longer delete a finding that arrived with claims — it can only mark the narrative rejected while the mechanic stands. A test caught an inverted comparison in the severity cap that silently left a High finding at High: a cap must lower and never raise. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
53a6a45448 |
feat(report): CVSS v3.1 with vectors, a PDF that survives long payloads, capped over-claims
Driven by the Arena Hockey engagement, where all 24 findings shipped with an
empty CVSS field and the PDF ran payloads off the page edge.
CVSS
- Derived deterministically from what the harness knows: the weakness class
sets the impact shape, the PROVEN exploitability sets attack complexity, and
the auth context sets privileges required. The vector is emitted with the
score, because a score without its vector cannot be checked and an unchecked
score is just a bigger adjective.
- The base equation is the v3.1 specification verbatim, including round-up and
the scope-changed privileges table. Tests anchor it against known values
(9.8 unauthenticated RCE, 10.0 with scope change, 6.1 reflected XSS, 0.0 for
no impact).
- An unknown weakness stays conservative — guessing high impact from a class
nobody mapped is how reports get inflated. An agent-supplied score is never
overwritten.
PDF
- Steps are passed as an ARRAY and rendered as a real numbered list, one command
per box. The previous template flattened them into a single `raw` block, which
rendered five separate commands as one run-on paragraph.
- Finding blocks are breakable, so a long evidence dump flows to the next page
instead of off the bottom of this one.
- `wrappable()` inserts zero-width breaks so encoded payloads wrap. The first
attempt broke prose mid-word ("rota ted", "lockoutOnFailu re=false") by
breaking every N characters regardless of context; it now works per token and
leaves anything that fits on a line exactly as it was.
- rebuild() re-enriches before rendering, so a run that finished before a
mapping existed picks it up instead of reprinting the gap forever.
Over-claimed findings are capped, not deleted
- The engagement rejected "no rate limiting on the password-reset flow" because
the agent claimed inbox flooding and only proved 25 unthrottled requests. The
claim was inflated; the measurement was real, and dropping it hid a genuine
gap. A unanimously rejected finding that still carries a checkable receipt is
now capped to Low and flagged for review, with the validator's reason
attached — the reader gets the fact without the story built on it.
- The agent contract now says impact must be what was MEASURED, and warns that
inflating it costs the whole finding.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
936e358850 |
fix(pipeline): dedupe findings by what they are, not by how they were worded
A live black-box engagement returned 21 "deduped" candidates for about ten actual issues. One cookie problem came back five times and one missing header four, because the key was `cwe|endpoint|title[..40]` and every agent writes those differently: `CWE-614` vs `CWE-614 (Sensitive Cookie in HTTPS Session Without Secure Attribute)`, `https://host/` vs `GET https://host/ (and /Account/Login)`. Worse than the inflated count, the severities disagreed — the same issue arrived Low from one agent and Medium from another, which is indefensible in front of a client. The key is now (CWE number, normalized endpoint) plus a title-similarity check, because grouping on the first two alone over-merges: missing `nosniff`, `Referrer-Policy` and `Permissions-Policy` are all CWE-693 on `/` and are three separate fixes. Titles merge at Jaccard >= 0.4 over meaningful words — calibrated on this run's real output, where two phrasings of the cookie issue score 0.44 and the two header findings score 0.33. The survivor keeps the HIGHEST severity with the fullest evidence, and inherits whatever the duplicates knew that it did not (remediation, repro steps, structured evidence). Agreement between independent agents is recorded as "corroborated by …" and nudges confidence up: several agents reaching the same conclusion separately is a reason to trust a finding, not a reason to print it five times. Tests use the actual titles, CWEs and endpoints from the engagement, including the case that must NOT merge (three rate-limit findings on three different endpoints — login spraying and reset-email flooding are different problems). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
b4da49fae4 |
fix(harness): stop discarding findings when an agent narrates before answering
Both bugs surfaced during a live black-box engagement.
1. extract_findings took the span from the first '[' to the last ']'. One
agent's reply opened with the prose line "[low] Antiforgery cookie missing
Secure flag" and put its real findings in a fenced ```json block further
down — so the span started inside prose, failed to parse, and every finding
that agent had proven was thrown away. Fenced blocks are now tried first
(last one wins: models narrate, then answer), with the span kept only as a
fallback and the trailing-comma salvage preserved.
2. "no findings" was reported as malformed JSON. The guard compared the raw
text to "[]", but models wrap the empty array in a fence, so every honest
negative result was logged as a parse failure — which teaches an operator to
ignore a warning that sometimes means a real one. reported_nothing() now
recognises a bare [], a fenced [], and {"findings": []}.
The second bug made the first one harder to see: the log was already full of
"malformed JSON" warnings for agents that had simply found nothing, so the one
warning that meant a genuine loss looked like more of the same.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
61ae3bc74d |
feat(report): rebuild reports on demand, from the CLI and the web
A PDF was only ever produced while a run was finishing. If `typst` was missing at that moment — or the template improved afterwards — the operator had no way to get one without re-running the whole engagement against the target. report::rebuild() regenerates every artifact (md · json · html · pdf) from the findings already on disk, exposed as `neurosploit rebuild <run-id|dir>` and as POST /api/runs/:id/report with a "Generate report" button in the run view. The endpoint shells out to the harness rather than reimplementing report generation in JavaScript, so there is one implementation instead of two that drift, and it says plainly when the PDF was skipped for want of `typst` instead of handing back a link to a file that was never produced. Also fixes write_all() to pass the run's pocs/ listing into the HTML report, so a rebuilt report links the scripts each finding cites — the run-time path already did this and the rebuild path silently did not. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
481a4eb1b9 |
feat: wire capability tokens and the audit trail through CLI, REPL and web
The risk model, grants and hash-chained trail existed as modules nothing called. Now every engagement runs under them. Capability - `neurosploit capability issue|verify` mints and inspects grants. - `--capability-token` (global, so the REPL takes it too), `--in-scope`, `--environment`, `--policy` on `run`; verification happens at the command line, so an invalid grant fails with a readable message instead of halfway through an engagement. - The pipeline verifies before anything else and REFUSES to run on a token that does not verify — proceeding would mean acting on an authorization nobody can prove was issued. `effective_scope` then applies the grant as a ceiling. - Web: an Authorization tab carrying the token, extra hosts, environment and policy profile. The browser decodes the claims for display and says plainly that it is not verifying them — a "valid" badge from a party without the key would be the UI vouching for something it cannot check. A hole the smoke test found: `/inscope evil.test` inside a session under a grant WIDENED the scope past it — the one thing a capability token exists to prevent. The run itself would still have been constrained (the pipeline re-applies the grant), but `/policy` reported a boundary that was not real, and a tool that misreports its own limits is worse than one with none. Scope mutations now re-apply the ceiling and name what it refused. Session authorization also arrives from argv rather than a `/`-command, because a session that can widen its own grant is not constrained by one. Audit - One hash-chained record per action in `<run>/audit.jsonl`, in the specified shape, covering engagement start/end, validator rejections, findings that reach the report (with the hash of the evidence behind them) and findings withheld for being out of scope. - The run verifies its own chain at the end and says loudly if it is broken. - `/audit [n]` tails the trail and verifies it; the web offers it as a download next to the report, so "show me what the tool did" is a link. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
3456c32f4d |
feat(harness): risk model, engagement policies, capability tokens, audit trail
Four pieces that together answer "may this action happen, under whose authority, and can we prove afterwards what we did". policy.rs — effective_risk per action, exactly as specified: (action_risk + asset_criticality + protocol_risk + privilege_level + blast_radius) × environment_multiplier Every term is named and kept on the result, so the number can be explained rather than argued with. Three policies sit on it: SafetyPolicy (ceilings, approval thresholds, hard prohibitions), ReasoningPolicy (baseline before payload, bounded hypotheses, evidence before escalation, explicit stop conditions) and ProofOfImpactPolicy (what a severity must carry before it may be that severity). OT/ICS/SCADA is treated as its own regime, not web testing on odd ports. Industrial protocols authenticate nothing — a Modbus write is the protocol working as intended, addressed to a device that may be holding a valve — and scanners crash PLCs by sending unexpected data at line rate. So the OT profile blocks writes, disruptive actions, fuzzing and exploit payloads outright, caps the rate at ~1 req/s, and refuses the function codes that stop a CPU (Modbus 5/6/8/15/16/22/23/43, S7 start/stop, DNP3 restart/stop). Safety instrumented systems are off limits in every profile. A test caught a calibration error worth keeping: a plain READ of a critical PLC scores 3.6 on this formula, so the obvious tight ceiling would have refused exactly the observation OT findings come from. In an industrial environment it is the KIND of action that is forbidden, not the arithmetic — the ceiling catches extremes and the low approval threshold makes anything past trivial observation a human's decision. capability.rs — HMAC-signed grants: who authorized what, against which hosts, in which environment, until when. The harness verifies the signature before reading a single claim (a well-formed token from the wrong key must never get to influence what the harness believes), refuses expired and not-yet-valid tokens, and treats the grant as a CEILING: constrain() intersects it with local configuration, so config can narrow authorization and never widen it. Tokens carry no secrets — the payload is readable by anyone holding it. audit.rs — one structured record per action, in the specified shape (timestamp, agent, hypothesis, action, target, policy_decision, operator, tool, result, evidence_hash, capability_token). Two things make it worth having: it is hash-chained, so removing or editing an entry breaks every hash that follows and verify() says which one; and it records REFUSALS, because a trail containing only what happened cannot demonstrate restraint. Only the grant's id is recorded, never the token — the trail gets shared. Hard kill conditions end a run outright: target unresponsive after our traffic, sustained 5xx, out-of-scope request, forbidden industrial function code, safety system addressed, capability expired mid-run, repeated policy violations, budget exhausted, operator stop. Failures BEFORE the target ever answered do not count — nothing listening is not the same as knocked over. The OT switch trips far sooner: a PLC missing two requests already warrants stopping. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
c3a6132153 |
feat(report): findings that answer where / why / how to fix, with a pasteable PoC
A finding is useful only if the reader can find the problem, see why it matters, fix it, and reproduce it without trusting us. The report answered the last one badly and the other three not at all: it printed a payload blob and an evidence blob, and "payload: ' OR 1=1--" tells a developer nothing about WHERE to look. A PoC script attached as a file is a black box unless you run it. Findings are now rendered in the order a reader works through them — where the problem is, what it means, how to fix it, then the proof — in the HTML report, the Markdown, the Typst/PDF and the web console's finding modal. The proof is numbered, pasteable steps: baseline request, attack request, how to read the result, with the real URL and the real payload. They come from the agent's `repro_steps` when it recorded them, and are derived from the endpoint/payload/identity pair otherwise, so every finding carries something runnable. The generated curl redacts Authorization/Cookie/API-key headers — a report gets shared, and a live session cookie inside one is a new bug. A PoC script is now offered as an extra artifact that automates the steps, never as the proof itself. Technical evidence is the measured difference, not a paraphrase: baseline vs attack status, size, timing and delta; how many repeats reproduced it; the controlled marker and whether a browser or a callback observed it; then each recorded exchange with the headers that decide a class (Location, Set-Cookie, Access-Control-*, X-Frame-Options, CSP, Retry-After) and a body excerpt. Finding gains `location` (the parameter/field/flow step, not just the URL) and `repro_steps`, and the agent contract now asks for them explicitly, along with impact tied to this app's data and remediation that names the control rather than saying "sanitise input". The web console offers the run's PDF when Typst produced one — and only then, since a dead download button is worse than none. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
d8ebe05507 |
feat(harness): replay engine — the harness re-runs the interaction itself
Validators decide from recorded artifacts, and the weakest link was who recorded them: "the payload returned a 500" is still an agent's account of what happened. Replay produces the part that matters most in practice — reproducibility — by sending the request again through the harness's own client, with the scope guard in front of it. Three properties it is built around: - every request passes ScopePolicy::check_request before a socket is opened, so replay cannot be the thing that wanders off-scope while verifying a finding; - it never mutates: a finding proven with DELETE is not re-proven by deleting the record again, so non-idempotent verbs are refused and repeats of them are refused outright; - bodies are truncated at 96KB and SAY they were truncated — a silently clipped body makes a length differential meaningless. enrich() fills in repeats and re-measures a recorded baseline (comparing a fresh attack against an hour-old baseline attributes ordinary drift to the payload). It deliberately does NOT synthesize a baseline from an attack request: removing "the payload" from an arbitrary URL is guesswork, and a guessed baseline would silently decide the verdict. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
4f277838c6 |
fix(web): responsive pass — real device sizes, scroll in the right containers
Audited at 390×844, 844×390 (phone landscape), 768×1024, 1024×768, 1440×900
and ≥1600px. What was actually broken:
- **The run header collapsed.** `justify-content: space-between` let the two
action buttons take the whole row, so at 390px the title wrapped one
character per line ("Test AspNe / t") and the facts line wrapped one word per
line. It now stacks below 680px, with the title block sized to its content
instead of stretching (the desktop `flex: 1` was what left a 180px void above
the buttons in the stacked layout).
- **The terminal header overflowed** its dock at 390px (557px of content in a
390px box) — it wraps now, and the status text drops out on narrow screens
where the coloured dot already carries it.
- **`100vh` is wrong on mobile.** It measures the viewport without the
collapsing address bar, so the wizard footer and its CTA sit underneath it.
Switched to `dvh` with the `vh` line kept as the fallback.
- **The dock took 82% of a phone in landscape** at its fixed 320px. It now
tracks the viewport (`clamp(180px, 42dvh, 340px)`, tighter still under
500px of height).
- **The off-canvas drawer had no way out but the button that opened it.**
Added a scrim that closes it, Esc, and auto-close when a run is picked —
and it closes itself if the window grows past the breakpoint, which
otherwise left a scrim over a sidebar that was no longer a drawer.
- **The stepper scrolls horizontally on a phone**, so advancing to an
off-screen step looked like nothing happened; the active step is scrolled
into view.
Device-type rules rather than width alone: `pointer: coarse` gets 38-44px hit
targets and 16px inputs (under 16px, iOS zooms the page on focus and breaks the
layout the user is typing into); `prefers-reduced-motion` drops the drawer
slide and the progress animation, which are decoration.
Scrolling stays where it belongs — one scroller per pane (`.wizard-body`,
`.run-body`, `.dash-body`, `.sb-groups`, `.modal-body`, `.term-host`), wide
tables scroll inside `.table-wrap`, and the page itself never scrolls
horizontally at any tested size.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
6475dba752 |
feat(validation): 13 more CWE validators, each with a rejection rule
Six classes had deterministic rules; the rest of a run still rested on models
voting. These thirteen cover the classes that produce the most false positives
in AI-driven testing, and each one is written around what *disproves* the
claim, because that is the part a language model skips:
SSTI an expression evaluated server-side whose result was never
sent — the payload echoing its own "result" is rejected
XXE entity content or an OOB callback; a parser error mentioning
entities shows the DTD was read, not that anything resolved
Open redirect 3xx WITH a Location off-site; a rendered link is not a redirect
CORS reflected Origin PLUS credentials; ACAO:* without credentials
exposes only what an anonymous client could already read, and
ACAO:* WITH credentials is refused by browsers anyway
Cookie flags fully decidable from Set-Cookie + scheme
Clickjacking neither X-Frame-Options nor CSP frame-ancestors
Auth bypass protected content with NO credentials sent — a "bypass" whose
request still carried a cookie is rejected, as is a redirect
to login
JWT forged token accepted AND privileged content returned
Rate limiting >= 20 attempts with no 429/Retry-After; five attempts prove
nothing about a limit that was never reached
Session fix. the session id surviving login unchanged
Mass assign. a read-back proving the field persisted — a 200 on the write
means nothing, APIs accept and ignore extra fields routinely
CSRF a cross-origin state change read back; a SameSite session
cookie means a browser would never attach it cross-site
Exposure a real secret/listing signature the baseline lacked; a
soft-404 mirroring the baseline page is rejected
Exchange gains response and request headers, because several of these classes
are decided by a header (Location, Set-Cookie, Access-Control-Allow-*) and the
body alone is not evidence for them.
A test asserts no two validators claim the same CWE — ambiguous ownership would
make routing depend on registration order, which is how a class silently gets
the wrong rule.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
093c87fbc6 |
feat(harness): enforced scope guard + deterministic Evidence & Validation Engine
Two gaps this closes, both found by reading what the code actually did.
Scope was never enforced
------------------------
`out_of_scope` was rendered into the prompt as "HARD CONSTRAINT — do NOT test…"
and nothing checked it. That is a request to a model, not a control: an agent
that decided a discovered subdomain was interesting, or that followed a
redirect off-target, was free to act and the operator found out by reading the
report.
scope.rs adds a guard in code:
- Hard scope: allowlist of hosts, *.wildcards, IPv4 CIDRs, URL prefixes, with
exclusions that always win. Defaults to the engagement's own target, so
discovery cannot widen authorization — finding a host is not permission to
attack it. An unconfigured policy is closed, not open.
- Soft scope: observe-only zones, destructive verbs (off by default), an
account-creation cap, a rate guard that warns rather than silently dropping
requests (a dropped request reads as "target unreachable"), and payload
classes refused even in scope because they damage the target instead of
demonstrating a bug.
- Enforced at the harness's own chokepoint (probe) and as a post-run audit:
findings proven against an unauthorized host are withheld from the report and
written to out-of-scope-findings.json as an incident to disclose, because
shipping one would launder the mistake.
- REPL: /inscope, /observe, /guardrail, /policy; /scope-out now promotes
host-shaped entries into enforced rules immediately, and says plainly when an
entry is prose the guard cannot enforce.
Validation was models checking models
-------------------------------------
N-model voting plus an adversarial refute pass share the failure mode of the
thing they check — agreement is not evidence, and a confident hallucination
survives a vote by being confident. grounding.rs helps but matches keywords
("http/", "status", "alert(") and cannot tell a real response from a plausible
transcript of one.
validation.rs asks a different question — does the recorded evidence
demonstrate THIS class? — with per-CWE rules and no model in the loop:
SQLi baseline/attack difference that reproduces >= 2x
XSS a browser executed a harness-chosen marker; reflection is not proof
IDOR identity B reads A's resource AND the body matches (a 200 returning a
login page is rejected, which is the classic false positive)
SSRF controlled callback or canary retrieval
LFI controlled marker or a file signature the baseline lacked
RCE a unique nonce in output/callback; reflected input is rejected
Absent evidence is never a pass, and a class with no rule is never
auto-confirmed. NEUROSPLOIT_VALIDATION=advisory (default) rejects
contradictions without demoting voted findings for missing artifacts;
enforcing makes the verdict the status. The evidence contract is injected into
exploit prompts so agents collect the artifacts while they still hold the
target.
Finding gains evidence_data so agents can emit structured artifacts alongside
the finding JSON.
Two bugs the tests caught while writing this: the scope guard treated a SAST
`src/auth.rs:42` endpoint as a host and quarantined valid source findings, and
two canaries minted in the same clock tick came out identical — a marker that
repeats would let a stale token vouch for a new finding.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
6e1b73e036 | Merge remote-tracking branch 'origin/main' | ||
|
|
9d83cb6e30 |
feat: attack knowledge graph, layered memory, command rectification, FAIR dashboard
Backend ------- - knowledge_graph.rs — the durable structure under attack_graph's per-run view: typed entities (asset/endpoint/weakness/technique/finding/account/credential/ impact) joined by typed, weighted, provenance-carrying edges, accumulated across runs in .neurosploit/graph.json plus a per-run copy the report and web console can draw. Answers what a finding list can't: ranked attack paths, and the frontier of entities observed but never proven — where chaining should look next. Agents only sometimes fill chains_from, so progression is also inferred between adjacent kill-chain stages; those edges are marked inferred, weighted lower, and drawn dashed, because presenting a hypothesis as evidence is the graph lying about itself. Secrets stay in the vault, never the graph. - memory.rs — four tiers scoped by lifetime, not importance: working (one run), engagement (one target), technique (one agent/CWE), reusable (generalized). Promotion is evidence-gated and needs independent evidence at each step: a claim repeated within a run becomes engagement knowledge; one confirmed across runs becomes technique knowledge; one that held on two DIFFERENT targets is generalized into a reusable lesson with host-specific tokens stripped. Nothing is promoted on a single observation, which is exactly what a hallucination looks like. Recall is scored (overlap × past success × recency) and injected into recon/exploit prompts as leads to verify. Recalled memos are credited only when the run they informed actually found something. - rectify.rs — a mistyped command cost a full round trip through /help, at the worst possible moment during a live run. Accepted-as-typed wins over everything (so the /url alias is never "corrected" to /ua), then unique prefix, then Damerau-Levenshtein with a length-scaled budget, and a tie is reported rather than resolved. Arguments too: a bare host gets its scheme, an out-of-range count is clamped with a note instead of silently reverting, a near-miss model id is matched against the live catalog. - pool.rs — when every configured model is exhausted or its token is dead, try whatever else this machine can actually reach (an installed CLI subscription, or a provider whose key is in the environment) before parking. A run that stops on a box with three other usable backends stopped for no reason. - repl.rs — /memory, /forget, /graph; a recovered run resumes by itself where nobody is watching (piped stdin — the web console — or NEUROSPLOIT_AUTO_RESUME), since a `/continue` prompt there waits forever. Web --- - Attack path: the stage list was seven hardcoded values, so findings the harness staged outside it were silently dropped — 5 of 27 on a real run. Rewritten against the harness's own stage list with unknown stages kept, two-line labels (every node used to read "SQL Injection Authent…"), stage column headers, pan/zoom/fit, path highlighting, severity filter, and the run's graph.json used when present. - Dashboard: coverage, findings by severity, top weaknesses, and annualized loss exposure via FAIR — frequency from exploitability × validation confidence, magnitude from assumptions shown on screen and editable, reported as a range. The posture score saturates instead of subtracting, so it keeps discriminating past the first critical. - Run history groups into one folder per target with a filter, instead of one flat list that grows forever. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BvdGy9XtVWSdXDTa3FFLJv |
||
|
|
0ef0ce8d94 |
feat(web): xterm.js terminal dock + front-end QA pass
Replaces the floating REPL drawer with a docked terminal, and fixes the usability problems a screenshot audit of the console turned up. Terminal (the reason for the change): - The drawer rendered the harness into a <div>, so the server had to strip ANSI before sending it: colour, the box-drawn /status panel and the banner all arrived flattened, and long lines rewrapped mid-glyph. The stream is now sent verbatim and rendered by xterm.js (vendored, nothing fetched at runtime), decoded with a streaming UTF-8 decoder so a multi-byte character split across two reads survives. - The drawer floated bottom-right, directly over "Next →" and "Start Exploitation" — the wizard's primary buttons. The dock is a flex child of .main, so opening it shortens the view instead of covering it. Drag its top edge to resize; the height is remembered. - The child is spawned over a pipe, not a PTY, so it never echoes: line editing is local — echo, ←/→, Home/End, history, Tab completion over the slash commands, Ctrl+C/L/U/K/A/E. Ctrl-C is delivered as SIGINT by the server, since a raw 0x03 byte over a pipe interrupts nothing. - A target picker switches the terminal between a standalone REPL session and the engagement currently running, so mid-run instructions go to the same process doing the testing. QA fixes: - Findings tables sorted by severity (a LOW above a CRITICAL made a 27-row result unreadable), with sortable headers, a severity summary that doubles as a filter, a text filter, a sticky header, and horizontal scroll confined to the table instead of the whole page. - alert()/prompt() replaced by inline field errors, a custom-lead modal and toasts — a modal alert hid the very field it was complaining about. - Lead categories start collapsed (412 leads over ~30 categories); search auto-expands what it matches and shows per-category hit counts. - Sidebar rows truncate inside the rail (a long target URL used to spill past its border), and carry a worst-severity dot, finding count and age. - Past-run header shows when it ran, how many agents ran, the recon asset, PoC count and run id — two runs of one target were indistinguishable. - Off-canvas sidebar below 768px had no way to be opened; added the toggle. - Long evidence values (cookies, tokens) now wrap instead of running under the finding modal's edge. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BvdGy9XtVWSdXDTa3FFLJv |
||
|
|
20e2060151 | Update README.md |