Commit Graph
215 Commits
Author SHA1 Message Date
CyberSecurityUPandClaude Opus 5 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>
2026-09-18 19:20:29 -03:00
CyberSecurityUPandClaude Opus 5 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>
2026-09-14 10:26:06 -03:00
CyberSecurityUPandClaude Opus 5 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>
2026-09-14 01:39:20 -03:00
CyberSecurityUPandClaude Opus 5 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>
2026-09-14 01:35:36 -03:00
CyberSecurityUPandClaude Opus 5 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>
2026-09-14 01:21:49 -03:00
CyberSecurityUPandClaude Opus 5 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>
2026-09-14 01:19:15 -03:00
CyberSecurityUPandClaude Opus 5 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>
2026-09-14 01:14:03 -03:00
CyberSecurityUPandClaude Opus 5 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>
2026-09-14 00:57:45 -03:00
CyberSecurityUPandClaude Opus 5 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>
2026-09-14 00:54:01 -03:00
CyberSecurityUPandClaude Opus 5 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>
2026-09-14 00:51:29 -03:00
CyberSecurityUPandClaude Opus 5 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>
2026-09-14 00:49:52 -03:00
CyberSecurityUPandClaude Opus 5 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>
2026-09-14 00:39:35 -03:00
CyberSecurityUPandClaude Opus 5 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>
2026-09-14 00:35:38 -03:00
CyberSecurityUPandClaude Opus 5 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>
2026-09-14 00:32:31 -03:00
CyberSecurityUPandClaude Opus 5 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>
2026-09-14 00:27:29 -03:00
CyberSecurityUPandClaude Opus 5 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>
2026-09-14 00:22:10 -03:00
CyberSecurityUPandClaude Opus 5 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>
2026-09-13 20:03:57 -03:00
CyberSecurityUPandClaude Opus 5 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>
2026-09-13 19:46:20 -03:00
CyberSecurityUPandClaude Opus 5 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>
2026-09-13 19:27:26 -03:00
CyberSecurityUPandClaude Opus 5 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>
2026-09-13 19:18:32 -03:00
CyberSecurityUPandClaude Opus 5 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>
2026-09-13 17:56:31 -03:00
CyberSecurityUPandClaude Opus 5 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>
2026-09-13 17:48:14 -03:00
CyberSecurityUPandClaude Opus 5 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>
2026-09-13 15:26:50 -03:00
CyberSecurityUPandClaude Opus 5 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>
2026-09-13 15:16:24 -03:00
CyberSecurityUPandClaude Opus 5 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>
2026-09-13 15:10:23 -03:00
CyberSecurityUPandClaude Opus 5 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>
2026-09-13 15:03:21 -03:00
CyberSecurityUP 6e1b73e036 Merge remote-tracking branch 'origin/main' 2026-09-07 16:58:46 -03:00
CyberSecurityUPandClaude Opus 5 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
2026-09-07 15:35:36 -03:00
CyberSecurityUPandClaude Opus 5 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
2026-09-07 13:33:16 -03:00
Joas A Santos 20e2060151 Update README.md 2026-08-29 11:38:05 -03:00
CyberSecurityUPandClaude Sonnet 5 cea73a563a Merge branch 'docs/web-console-tutorial' into main
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0166e1EFP2JrifYsj5bVugut
2026-08-24 11:43:23 -03:00
CyberSecurityUPandClaude Sonnet 5 ec53880298 docs: document web console in README/TUTORIAL, note REPL-backed run
- README: web console section notes the wizard now scripts a real REPL
  session for run/whitebox/greybey so mid-run prompts work
- TUTORIAL.md: new §8 Web console (wizard steps, REPL script, live view,
  links to web/API.md and web/README.md); renumbered §9-17 and §9.1-9.5
- web/README.md: bullet on the REPL-backed run + send-prompt box

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0166e1EFP2JrifYsj5bVugut
2026-08-24 11:43:20 -03:00
CyberSecurityUPandClaude Sonnet 5 4fbe608a7a feat(web): drive run/whitebox/greybox exploitation through a real REPL session
Root cause of "can't send prompts while a run streams": /api/exploit
spawned a plain `neurosploit run ...` subprocess, and that CLI path
(run_mode() in main.rs) never reads stdin - it only waits on the task or
Ctrl-C. The ONLY thing in the harness that keeps accepting input while an
engagement streams is the interactive REPL's background-run loop. So:

- New startJobViaRepl(): for mode run/whitebox/greybox, spawns a bare
  `neurosploit` REPL session and scripts it via stdin (/target or /repo,
  /model, /sub, /mcp, /votes, /chain, /recon, /focus, /objective,
  /scope-out, /creds, /only <agents> or /only clear, then /run) instead
  of building CLI args. Same underlying pipeline, same tagged output
  lines, so all existing parsing (findings/phase/progress/runId) works
  unchanged. host/aitest/skills modes stay on the old one-shot
  startJob() - they need onboarding's scope picker, an interactive
  arrow-key menu that silently skips itself over a piped stdin, so they
  can't be scripted this way.
- New POST /api/exploit/:id/input writes a line to the session's stdin -
  natural language, /status, /continue, anything the REPL accepts - and
  the live run view grows a "send prompt" box (in the Activity log tab)
  for it, shown only when the job reports interactive: true.
- Stop, for an interactive job, now sends the REPL's own graceful
  '/stop\n1\n' (validate what's found, then report) instead of SIGINT -
  the REPL's own input loop has no signal handler, so SIGINT there would
  just kill the process outright and skip the report step. Non-
  interactive jobs still get SIGINT (run_mode() does catch that).
- 'done' can no longer be process-exit only: an interactive session stays
  open after the engagement finishes (for /report, /continue, another
  /run), so ingestLine() now also flags done from the same "phase
  complete" content signal it already used for the phase field.

Verified end-to-end: started an interactive job, confirmed
`interactive: true` and a captured runId, sent /status and /agents mid-
and post-run over the new /input endpoint (both accepted, session stayed
alive and responsive after completion), and confirmed a non-interactive
run is unaffected.

Also: the missing "Activity log" tab a screenshot showed for a "running"
engagement was the sidebar's detail-view fallback (2 tabs, no log) for a
run whose Job object no longer exists in server memory - it happens when
the Node process gets restarted while a spawned neurosploit child is
still alive underneath it (an orphan from testing across many redeploys
this session, not a code bug); the live view itself always had the tab.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0129WdYHccPsH27k5GGuwijd
v4.0.0
2026-08-23 16:04:55 -03:00
CyberSecurityUPandClaude Sonnet 5 07bed42467 feat(harness): /only REPL command; redesign the HTML report (drop attack-chain, match Typst)
/only <agent,...> (app/src/repl.rs): the REPL had no way to pin an exact
agent set the way the CLI's --only flag does - Session gained a `pinned`
field, wired into RunConfig in both start_background() (the live
background-run path) and the blocking run() fallback. Needed so the web
console's exploitation jobs can drive a real interactive REPL session
(for live input while a run streams) without losing lead-pinning, which
only existed as a CLI flag until now. Also usable directly from a
terminal REPL session.

HTML report (crates/harness/src/report.rs, html()): rebuilt to match the
Typst PDF template's design (templates/report.typ) instead of its own
inconsistent styling - violet brand accent, an asset table, a 5-box
executive-summary grid (all severities, zero-count included, matching
Typst's grid exactly), a Vulnerability Summary table, and severity-
left-bordered finding cards with a compact field grid (Criticality /
Status / OWASP-CWE / Confidence / Location / Agent / Auth context) before
Description-Impact / Proof of Concept / Evidence / Remediation - same
field order and labels as the Typst template. Dropped the Mermaid
attack-path/kill-chain section entirely (the web console's live
Generative Attack Path Chaining graph covers that now, interactively).
Also tidied two pre-existing formatting quirks while in there: OWASP/CWE
left a dangling " · " when CWE was empty, and the confidence cell said
"<votes-string> votes" even when the votes field already contained a
compound descriptor like "1/1 · receipt_missing".

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0129WdYHccPsH27k5GGuwijd
2026-08-23 16:04:34 -03:00
CyberSecurityUP ecc2a9ae03 docs: refresh README for v4.0.0 - correct stale counts, expand web console section
- Agent library table was stale (196/12/78/17 = 303 total, missing the
  infra/chains/ai categories entirely). Corrected to the real counts
  (245/78/30/34/23/13/12 = 435), matching the "MD Agents-435" badge and
  neurosploit agents output.
- Provider badge said 16; the harness actually ships 18 (litellm and azure
  were missing from the README's provider table and the API-key export
  block). Added both.
- Web console section was a 3-line stub written before most of the feature
  was built. Expanded to cover the 5-step wizard, the lead board's bulk
  select/category toggles, custom-lead-generates-a-real-agent, the live
  run view's Generative Attack Path Chaining graph and finding/PoC detail,
  the Auth & Keys menu, and F5 persistence - with links to web/API.md and
  web/README.md.
2026-08-23 15:47:59 -03:00
CyberSecurityUPandClaude Sonnet 5 e253b8b291 feat(web): bulk select/clear-all leads; custom lead generates a real agent
- Select all / Clear all buttons in the Leads step toolbar - respects the
  current search filter, so filtering to "sql" then Select all only pins
  those, not all 412 leads. The per-category master switch (already
  select/deselect-all for that category, indeterminate when partial) was
  the only bulk control before; this adds the "everything" case.

- '+ Custom lead' now generates an ACTUAL specialist-agent markdown file
  (agents_md/vulns/custom_<slug>.md, same format every other agent uses)
  via the claude CLI on the operator's Anthropic subscription
  (claude-opus-4-8 by default - matches the harness's own default model),
  instead of folding free text into --focus. The new lead is immediately
  selectable and pinnable via --only like any other agent; verified the
  Rust harness's own agent loader picks it up (agent count went 435 -> 436,
  neurosploit agents confirmed it).

  Two things found and fixed while wiring this up:
  - the skip-permissions flag gave the model file/bash tool access, which
    made it try to write the file itself and narrate doing so instead of
    just returning text. Dropped the flag (pure text completion needs no
    tools) and told it explicitly not to use any.
  - Even so, defensively strip anything before the first '# ' heading
    before saving, in case a model still prepends commentary.

  Falls back to the old free-text-focus behavior if generation fails
  (claude not installed/logged in, malformed output, timeout) so the
  operator's intent isn't lost.

- New "Custom Leads" category, shown first, so generated leads have a
  visible home instead of landing in the catch-all "Other" bucket.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0129WdYHccPsH27k5GGuwijd
2026-08-23 15:43:13 -03:00
CyberSecurityUPandClaude Sonnet 5 8047c66e8f fix(web): finding modal — dedupe repeated attribution text, prose vs code sections
impact and business_impact often carry identical text (both ending with the
reporter's 'Identified and validated by NeuroSploit...' footer), and the
modal concatenated them verbatim — the boilerplate line rendered twice, and
whenever the two fields matched, so did the whole paragraph.

- Strip the attribution sentence out of impact/business_impact/remediation/
  evidence wherever it appears; surface it once, at the bottom of the modal,
  instead of embedded per field.
- Skip business_impact entirely when it's identical to impact (the common
  case) instead of printing the same paragraph twice.
- Split rendering into codeBlock() (endpoint/payload — monospace, looks like
  what it is: a request/curl) and proseBlock() (description/impact/
  remediation — a readable paragraph, not a code box).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0129WdYHccPsH27k5GGuwijd
2026-08-23 15:35:21 -03:00
CyberSecurityUPandClaude Sonnet 5 a083990ce4 fix(web): attack-graph canvas follows the app theme instead of forcing dark
Node/edge colors now use the same --sev-*-fg / --surface / --text / --border
CSS custom properties as the rest of the console (set via inline style=
attributes, since SVG presentation attributes don't resolve var()) — the
graph reads correctly in light mode instead of always rendering as a
dark canvas.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0129WdYHccPsH27k5GGuwijd
2026-08-23 15:33:48 -03:00
CyberSecurityUPandClaude Sonnet 5 7f365b4e88 feat(web): render the attack path as a real node graph, not flat cards
The 'Generative Attack Path Chaining' tab previously showed kill-chain
stages as stacked cards in columns — with 1 finding (the common case
early in a run) it looked like an empty list, nothing like an attack
graph.

Rewritten as an inline SVG node/edge graph on a fixed-dark canvas
(matches attack-graph tools like NodeZero regardless of the app's own
light/dark theme — bright severity colors read better against near-black):
- Root node = the target, always present.
- One node per confirmed finding, positioned in its kill-chain-stage
  column (falls back to a single flat column when no finding has a
  stage yet).
- Edges: from the finding's chains_from parent when the harness set one,
  else fanned directly from root — never invents a specific relationship
  that doesn't exist in the data.
- Per-node icon inferred from title/evidence/cwe/stage (key/shield/
  person/host/db/impact), severity-colored border + corner tick.
- Nodes are clickable — opens the same finding detail modal as the
  findings table (PoC included).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0129WdYHccPsH27k5GGuwijd
2026-08-23 15:21:41 -03:00
CyberSecurityUPandClaude Sonnet 5 d42e9ff8e8 fix(web): global [hidden] bug, progress bar, F5 persistence, finding detail + PoC
Real front-end bugs found and fixed:
- [hidden] never worked on any element whose class also sets 'display'
  (every .btn, .chip, ...): the browser's built-in '[hidden]{display:none}'
  rule and an author rule of equal specificity tie, and the later one in the
  cascade wins — so 'Next' stayed visible on the Review step alongside
  'Start Exploitation', and 'Open report'/'Stop' rendered during 'starting'.
  Fixed with a single global '[hidden]{display:none!important}' override.
- Progress bar was functionally correct but easy to miss (thin, 0%-width,
  low-contrast track) and gave no feedback while the agent count is still
  unknown (recon phase). Added a border for visibility and an indeterminate
  sliding-segment state for the 'agents: ?' window.
- A live run watched in the browser was lost on F5 (jumped back to the
  wizard) even though the job keeps running server-side. The active job id
  now persists in localStorage; on load the app reconnects the SSE stream
  (the server replays its full event buffer) instead of losing the view.

New:
- Findings are now clickable — a detail modal shows every Finding field
  (CWE/CVSS/OWASP/MITRE/stage/exploitability/confidence/votes/review status/
  auth context/account/agent), endpoint+payload, evidence, impact, business
  impact, remediation, and chains_from — in both the live run and past-run
  detail views.
- PoC surfacing: the finding modal looks up any script the run wrote to
  pocs/ that's cited in the finding's evidence (per the harness's own
  doctrine — see pipeline.rs change below), fetches and previews it inline,
  with a link to open the raw file. Live runs poll for new PoC files every
  5s once the run id is known.
- Pinned-leads confirmation: the live run header now states plainly how
  many leads were pinned (and their names) or that selection is auto
  (recon-driven) — this was previously buried in the scrolling activity log
  behind the harness's unconditional 'Loaded 435 agents' library-size line,
  which describes the full agent library, not what will actually run.

Harness doctrine (crates/harness/src/pipeline.rs, pocs_line()):
PoC-writing for black-box findings was previously conditioned on 'when an
issue needs a custom multi-step exploit/script' — vague enough that a
straightforward finding (single-request XSS/SQLi/IDOR) often got no PoC
file at all. Now required for every confirmed Medium+ finding, one
standalone .py/.sh script per finding, and explicit about citing the exact
file name in the finding's evidence field (which is what the web UI now
matches on to link a PoC to its finding).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0129WdYHccPsH27k5GGuwijd
2026-08-23 14:37:28 -03:00
Joas A Santos 51c38c1db3 Merge pull request #43 from JoasASantos/feat/v4.0.0-web-ui
feat(4.0.0): web console — lead board + live findings + real CLI REPL
2026-08-23 14:27:42 -03:00
CyberSecurityUPandClaude Sonnet 5 040170e54a fix(web): category master switch reads as partial, not off, when only some agents are deselected
Unchecking one agent in an 8-agent category (7/8 left on) rendered the
category header switch fully unchecked — visually indistinguishable from
'category disabled', even though 7 of 8 agents were still on. The
checkbox's checked state only had two positions; a partial selection had
nowhere to render but off.

Fix: set the master switch's .indeterminate property when 0 < selected <
total, with its own CSS state (grey track, thumb parked halfway) instead
of the on/off track+thumb. Clicking a checkbox out of indeterminate
selects everything, per browser default — unchanged behavior, just an
honest visual for the in-between state.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0129WdYHccPsH27k5GGuwijd
2026-08-23 14:27:27 -03:00
CyberSecurityUPandClaude Sonnet 5 3dcfeb7377 feat(web): require an engagement name before launch
Wizard's Asset step now opens with a required 'Engagement name' field
(validated before advancing or launching). The name isn't a harness/CLI
concept, so it's persisted server-side as runId -> name in
.neurosploit/web-engagement-names.json (keyed off the CLI's own run id,
captured from its 'run id : ns-...' log line) so the sidebar, live run
header, and run detail can label a run by name instead of the raw
target/run-id, surviving a server restart.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0129WdYHccPsH27k5GGuwijd
2026-08-23 14:20:34 -03:00
CyberSecurityUPandClaude Sonnet 5 bb659412fc feat(web): engagement wizard, model/auth picker, Auth & Keys menu, attack-path graph
Full frontend rewrite following a deliberate visual direction (dense
security-operations console — borders over shadows, two radii, one accent,
no gradients/glassmorphism) and fixing real bugs found in review:

- EventSource on the exploit stream never called es.close() on 'done',
  so the browser silently reconnected and re-streamed the whole job
  (duplicate log lines/findings). Fixed.
- Sidebar 'running' step indicator and openRun() matched ANY running run
  instead of the one belonging to the current job (by runId). Fixed.

New:
- 5-step engagement wizard (Asset -> Scope & Auth -> Leads -> Model & Run
  -> Review) replacing the single flat board — inspired by the
  Discovery/Plan/Exploit/Remediate stage model both a.security and
  terra.security use publicly.
- Model is now a real dropdown sourced from /api/providers (mirrors
  harness::models::providers()), with an API-key vs. subscription toggle
  that disables subscription for API-only providers.
- One Auth & Keys menu: target auth header + named roles (IDOR/BOLA/BFLA
  multi-identity testing) materialize into an ephemeral creds.yaml passed
  via --creds; per-provider API keys live in server memory only (never on
  disk) and are merged into every spawned child's env.
- Generative Attack Path Chaining: findings rendered as kill-chain columns
  (recon -> initial-access -> ... -> impact) with chains_from resolved to
  parent titles, live in the run view and static in run detail.
- Findings are now a proper table (severity/title/endpoint/CWE/agent/
  confidence) instead of stacked cards.
- Explicit light/dark theme toggle persisted in localStorage, defaulting
  to light (previously light only won when the OS wasn't in dark mode).
- All UI strings in English.

Backend additions: GET /api/providers, GET/POST/DELETE /api/keys,
ephemeral creds.yaml generation for auth/roles, env override merged into
every exploit-job and REPL child spawn.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0129WdYHccPsH27k5GGuwijd
2026-08-23 14:16:46 -03:00
CyberSecurityUPandClaude Sonnet 5 d1d1c71e24 feat(4.0.0): web console — lead board + live findings + real CLI REPL
New web/ app (zero npm deps, Node http built-ins only):
- server.js reads agents_md/ to build a categorized lead board (435 agents
  auto-classified into Business Logic / Broken Access Control / Injection /
  LLM Application / Auth & Session / SSRF / API / Cloud & Infra / etc.),
  reads runs/ for history, and spawns the compiled neurosploit CLI binary
  for every exploitation job — structured findings/phase/progress are parsed
  from its stdout (finding_json:/phase lines), same signal the TUI uses.
- REPL drawer spawns `neurosploit` with no subcommand (real interactive
  session, Reader::Plain over the piped stdin) and streams stdin/stdout —
  every /command works exactly as in a terminal, nothing reimplemented.
- SSE endpoints for both job and REPL streams; run/finding/report assets
  served under /api/runs/:id/asset/*.
- public/{index,app.js,style.css}: lead board with category toggles + custom
  leads + Start Exploitation, live run view (progress/findings/log), run
  detail view, REPL drawer — screenshot-inspired layout.
- web/API.md: full endpoint reference. web/README.md: quick start.

Bump version 3.6.9 -> 4.0.0 (Cargo.toml, CLI banners, README/TUTORIAL).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0129WdYHccPsH27k5GGuwijd
2026-08-23 13:59:19 -03:00
Joas A Santos 3ddb22ee25 Merge pull request #42 from JoasASantos/feat/opencode-hermes-providers
feat(3.6.9): OpenCode Zen + Nous Research (Hermes) providers
2026-08-13 12:26:23 -03:00
CyberSecurityUPandClaude Opus 5 69c5e3ddb9 feat(3.6.9): OpenCode Zen + Nous Research (Hermes) providers
Add two new model providers, both usable via API key or --subscription
(local CLI login, no key):

- opencode: OpenCode Zen gateway (OPENCODE_API_KEY, opencode.ai/zen/v1).
  Subscription mode drives the `opencode` CLI (`opencode run --auto`).
  Supports the Playwright MCP (--mcp): our .mcp.json is converted to
  OpenCode's own config schema and injected via OPENCODE_CONFIG.

- nous: Nous Research / Hermes models (NOUS_API_KEY,
  inference-api.nousresearch.com/v1). Subscription mode drives the
  `hermes` CLI (NousResearch/hermes-agent) on the user's Nous Portal
  OAuth login (`hermes setup --portal`), via `hermes chat -q`. No
  CLI-level MCP hook — falls back to Hermes's own built-in toolsets
  (web/terminal/computer-use).

Both wired into cli_binary_for, installed_cli_backends, cli_login_status
(prompt passed as argv, not stdin — neither CLI reads stdin for this).

Bump version 3.6.8 -> 3.6.9 across Cargo.toml, README, TUTORIAL, setup.sh,
install.ps1, and in-binary version strings. README/.env.example updated
with the new provider rows and subscription-login table.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HHFAVCHMvRkTy9Wgw7SayG
v3.6.9
2026-08-11 23:47:12 -03:00
CyberSecurityUPandClaude Opus 4.6 1f8ccb6f9e fix(3.6.8): recon time budget — 5min cap prevents recon from eating entire run
- Add RECON_TOTAL_BUDGET_SECS (300s) total wall-clock cap across all rounds
- Per-round budget directive in prompt: 30-50 commands max, stop early if enough intel
- Elapsed time check between rounds: skip remaining if budget exhausted
- Remaining time communicated to follow-up rounds for self-pacing
- RELEASE.md updated with recon budget section

Previously: subscription CLI recon ran 150+ commands over 15 min, exploitation never started.
Now: recon caps at 5 min total, then proceeds to agent exploitation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-08-08 10:10:43 -03:00
CyberSecurityUPandClaude Opus 4.6 3c49a83578 fix(3.6.8): auth resilience — circuit breaker pauses run on token revocation, preserves findings
- Add is_auth_failure() detector (401, OAuth revoked, session expired, invalid key)
- Circuit breaker: 3 consecutive auth failures auto-pause instead of burning 66 agents
- Auth-aware park_exhausted(): clear message + fallback provider switch via /continue
- No retry burn on auth errors (immediate return like exhaustion)
- Recon preserves HTTP probe facts when model auth fails
- REPL phase tracking: paused (auth) distinct from paused (quota)
- RELEASE.md updated with auth resilience section

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-08-08 09:14:21 -03:00
CyberSecurityUPandClaude Opus 4.6 e956b482b9 fix(3.6.8): JSON parse resilience + diagnostics for local model failures
- extract_findings: log when model output has no JSON (was silent drop)
- extract_findings: auto-fix trailing-comma JSON (common LLM mistake)
- pipeline: emit response tail when agent returns 0 parseable findings
- Helps diagnose why small/local models produce 0 findings on valid targets

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-08-07 09:51:50 -03:00