Compare commits

...
28 Commits
Author SHA1 Message Date
CyberSecurityUP e5c607f467 v3.5.4 — Robust attack chaining & false-positive reduction
Bundles the multi-round post-exploitation attack-chaining engine (attack_chain:
per-foothold decisions, loot carried forward, validate-before-pivot, loop-until-
dry, --chain-depth) and the false-positive controls (robust verdict parsing,
severity-aware quorum, adversarial refute pass, stronger validator prompt).
Version bumped 3.5.3 -> 3.5.4; README/RELEASE updated.
2026-07-01 19:01:27 -03:00
CyberSecurityUP ea61ab1fdf harness: robust multi-round attack chaining (decision-driven post-exploitation)
Replaces the single-shot chain_round with attack_chain(): an iterative,
per-foothold pivot engine.
- Each round takes the newest confirmed footholds (best-first, capped) and, for
  EACH one, an agent DECIDES which directions to expand — post-exploitation
  (loot creds/keys/config/source), credential reuse, horizontal+vertical
  privesc, lateral movement to adjacent services/hosts, data exfiltration, and
  new attack surface the foothold exposes — proving each step with a receipt.
- LOOT (creds/tokens/hosts/endpoints) discovered in one round is carried forward
  and reused by later rounds (parsed from a {"findings":[...],"loot":[...]} reply).
- New findings are validated each round (never pivot off a false positive) and
  become the next round's footholds. Loop-until-dry or chain_depth rounds.
- New RunConfig.chain_depth (default 2) + --chain-depth flag on all engagement
  commands (0 disables). CHAIN_SYS rewritten for decision/post-ex framing.
2026-07-01 18:22:00 -03:00
CyberSecurityUPandClaude Opus 4.8 e9f81c164d harness: reduce false positives (robust verdicts, severity quorum, refute pass)
- Robust verdict parsing (pool::parse_verdict): whitespace-insensitive, checks
  explicit rejection first, counts only explicit confirmations; ambiguous →
  Unclear (not confirmed). Replaces the fragile exact-JSON / loose "yes" match.
- Severity-aware quorum (pool::quorum_confirmed): High/Critical now need ≥2
  validators AND ≥2/3 agreement (a single vote can no longer confirm a
  Critical); lower severities need a strict majority (>half, was ≥half). Single-
  model panels fall back to majority so they aren't nuked.
- Adversarial refute pass (REFUTE_SYS): every confirmed High/Critical is
  re-examined by a skeptical panel that assumes false-positive; findings that
  can't withstand a majority of skeptics are dropped. Survives on infra failure.
- Strengthened VOTE_SYS with an explicit false-positive checklist (reflected-not-
  executed, version/banner guesses, self-XSS, error-as-injection, thin evidence,
  inflated severity); validator query now also includes impact.
- Unit tests for parse_verdict + quorum_confirmed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 17:33:15 -03:00
CyberSecurityUPandClaude Opus 4.8 0a181782a4 Add MIT LICENSE
Credits: Joas A Santos & Red Team Leaders.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 17:14:42 -03:00
CyberSecurityUPandClaude Opus 4.8 669ab44cef ci: cross-build macOS x64 on Apple-Silicon runner (avoid scarce macos-13)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 09:53:30 -03:00
CyberSecurityUPandClaude Opus 4.8 64decada3e v3.5.3 — Integrations (GitHub · GitLab · Jira)
New harness module `integrations` (+ app commands) wiring NeuroSploit into the
SDLC. Config persists per-project to .neurosploit/integrations.json; secrets are
NEVER stored — only the env-var name is saved, values read from the environment.

GitHub:
- private-repo clone (token injected into the clone URL for whitebox/greybox/tui)
- `neurosploit pr <owner/repo> <n>`: clone the PR head (refs/pull/N/head),
  white-box review, optional `--comment` (PR summary) and `--jira` (cards)
- `neurosploit watch <owner/repo> --branch --interval`: re-review on each new commit
GitLab:
- private-repo clone (oauth2 token) for whitebox/greybox (gitlab.com or self-hosted)
Jira:
- `--jira` on any engagement opens one card per finding (REST /issue, basic auth)

Control:
- `/integrations` (REPL): show · enable/disable · setup jira|gitlab|github
- `neurosploit integrations [show|enable|disable] [github|gitlab|jira]` (CLI)

Docs: README "Integrations" section + new TUTORIAL-INTEGRATION.md (per-tool setup,
scopes, recipes, troubleshooting). Version bumped 3.5.2 → 3.5.3.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 01:56:49 -03:00
CyberSecurityUPandClaude Opus 4.8 ae5bb247a3 ci: cross-platform release builds (linux x64/arm64, macos x64/arm64, windows)
GitHub Actions workflow that, on a pushed v* tag (or manual dispatch), builds a
self-contained NeuroSploit (binary + agents_md/) for every OS/arch and uploads
the archives to the matching release. macOS builds are also attached manually.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 14:25:22 -03:00
CyberSecurityUPandClaude Opus 4.8 d957429c09 feat(models): add Azure OpenAI provider + GOOGLE_API_KEY alias for Gemini
Resolves the only two open issues that still apply to the Rust build:
- #21 Azure OpenAI: new `azure` provider (OpenAI-compatible). Endpoint comes
  from AZURE_OPENAI_ENDPOINT, api-version from AZURE_OPENAI_API_VERSION
  (default 2024-10-21); the model name is the Azure deployment; auth uses the
  `api-key` header instead of Bearer. Use `--model azure:<deployment>`.
- #25 Gemini key confusion: GEMINI_API_KEY now also accepts GOOGLE_API_KEY
  (Google's standard env var) as an alias; local providers (ollama/litellm)
  require no key. .env.example documents both.

Kept under the v3.5.2 line (additive provider support).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 14:17:25 -03:00
CyberSecurityUPandClaude Opus 4.8 761d3df444 feat: whitebox/greybox/repl accept a GitHub URL (auto-clone)
`whitebox <arg>`, `greybox --repo <arg>`, `tui --repo`, and the REPL `/repo`
now accept a git URL (https://github.com/owner/repo[.git], git@…, ssh://, *.git)
or an `owner/repo` shorthand. A new resolve_source() shallow-clones it into
<base>/repos/<name> (cached, .gitignored) and reviews it; existing local paths
are used unchanged. Works identically with API-key (--model) and --subscription.

Verified: `neurosploit whitebox https://github.com/digininja/DVWA --offline`
clones DVWA and runs the 78 code agents over 120KB of source.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 13:52:51 -03:00
Joas A SantosandGitHub 489b3abd3f Merge pull request #31 from leebaird/main
Revise DNS reconnaissance methodology details.
2026-06-26 13:39:28 -03:00
CyberSecurityUPandClaude Opus 4.8 e4efa9bbb0 v3.5.2 — Exploitation Depth & Report Hygiene
Distilled from reviewing real AI-pentest output that kept stopping at "exposed"
instead of "exploited". Pure-additive, back-compatible.

Behavior (injected into black/grey/chain exploit prompts via DEPTH_DOCTRINE):
- Exposed → exploited: any info-disclosure / exposed service/WSDL / leaked
  credential|token / reachable dev host MUST be used before it's a finding;
  otherwise it's a lead, not a confirmed High/Critical.
- Chain across modules: reuse obtained session/JWT/cookie/credential and pivot
  to IDOR/privesc/exfil; report the chain, not isolated parts.
- Decode & fingerprint → CVE; audit tokens (alg-confusion/none/kid/JWKS, weak
  HS256 secret cracking, lifecycle).

Deterministic post-pass (new crates/harness/src/hygiene.rs, wired into finish()):
- calibrate severity to PROVEN impact — unproven High/Critical (hedged, no
  payload, thin evidence) capped to Medium and re-titled "(potential)";
- depth_audit — flag exposures on a host with no real exploit;
- hygiene_summary — advise consolidating hygiene classes repeated across assets.
Unit tests cover calibration + depth audit.

5 new doctrine meta-agents (scripts/build_methodology_v352.py → agents_md/meta/):
exploit_depth_doctrine, finding_chainer, artifact_decoder, token_auditor,
report_calibrator (meta 17→22, total 343→348).

Version bumped 3.5.1 → 3.5.2 across crates/app/installers/docs; RELEASE/README
updated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 11:31:11 -03:00
Lee BairdandGitHub 9d261b45e7 Revise DNS reconnaissance methodology details.
Updated methodology section for DNS reconnaissance.
2026-06-25 20:22:45 -05:00
CyberSecurityUPandClaude Opus 4.8 ac84db024c docs: add v3.5.1 release notes to RELEASE.md
Prepend the 3.5.x entry: interactive REPL, POMDP belief/grounding, infra/host
(SSH + Windows/AD), attack-chain & app-stack/CVE agents, LiteLLM, Mission-Control
TUI, structured Typst report, and the new run control (background /run, 3-way
/stop, crash recovery, pause-on-quota /continue).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 09:28:16 -03:00
CyberSecurityUPandClaude Opus 4.8 734af8d839 chore: stop tracking per-project .neurosploit/ test state
These session/runs/history files are runtime state generated during local
testing; .neurosploit/ is already in .gitignore. Untrack them so the repo
doesn't carry test artifacts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 09:26:09 -03:00
CyberSecurityUPandClaude Opus 4.8 49dde7c637 feat(repl): pause-on-exhaustion + live findings checkpoint + instant stop
Token/quota exhaustion no longer silently drops agents. When every candidate
model is rate-limited / out of quota, the run PARKS (keeping all state) and
prints "⏸ token/quota exhausted … PAUSED". The user can:
  - wait for renewal and /continue (retry same model), or
  - /model <provider:model> (or the /model selector) then /continue to switch.
Implemented via ModelPool: is_exhaustion() detection, park_exhausted() that
awaits a resume Notify, and a fallback-model slot tried first on retry. /model
queues the chosen models into a paused run's fallback so a plain /continue
resumes on them.

Findings now survive a crash/quit: each finding is checkpointed live to
.neurosploit/active_run.json; on next launch an interrupted run is recovered
into /runs (a raw report is materialized) so /results, /finding and /report
keep working.

/stop now actually halts immediately on raw/discard: one() races the in-flight
model call against the hard-cancel flag, so the CLI child (kill_on_drop) is
terminated at once instead of finishing its whole command sequence. The
validate path still soft-stops (lets validation run).

Docs: TUTORIAL documents the 3-way /stop, crash recovery and pause/continue;
/help lists /continue and the new behaviors.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 00:41:22 -03:00
CyberSecurityUPandClaude Opus 4.8 7dba912d3f chore: slim .env.example to the v3.5.1 Rust providers
Drop the legacy Python-stack settings (DATABASE_URL, HOST/PORT, RAG,
Kali sandbox, Discord/Telegram/Twilio, feature flags) that no longer
exist in the Rust harness. Keep only the provider API-key env vars the
model pool actually reads, plus the Ollama/LiteLLM base-URL overrides.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 23:59:25 -03:00
CyberSecurityUPandClaude Opus 4.8 79f20b1456 docs: detailed white-box & grey-box instructions (TUTORIAL + README + /help)
- TUTORIAL 5.2 white-box: how source review works (context collection, agent
  selection, source→sink dataflow, file:line symbolic grounding, validation),
  examples and tips.
- TUTORIAL 5.3 grey-box: code review leads → live exploitation flow, auth via
  creds.yaml, MCP, REPL repo+target = greybox.
- README quick-start gains white-box / grey-box / host one-liners + tutorial link.
- REPL /help shows the MODES line (black/white/grey/host) and Ctrl-O hint.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 23:26:57 -03:00
CyberSecurityUPandClaude Opus 4.8 c69546c145 v3.5.1: LiteLLM support (OpenAI-compatible proxy)
- New `litellm` provider (kind=api). Use `litellm:<model>` — model names pass
  through to your gateway. No hardcoded key required (proxy may be open).
- Env-configurable base URL: LITELLM_BASE_URL (default http://localhost:4000/v1),
  LITELLM_API_KEY. OLLAMA_BASE_URL override added too.
- TUTORIAL documents the LiteLLM env config.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 23:24:16 -03:00
CyberSecurityUPandClaude Opus 4.8 eb4e13efea v3.5.1: live findings + /finding + Ctrl+O/expand + 3-way /stop (soft validate) + report URL + structured Typst + IIS/CMS/CVE agents
REPL interactivity & findings:
- Live findings registered during a run: /results shows them accumulating;
  /finding opens a selection menu with FULL details (PoC, command, evidence,
  CVSS, OWASP/CWE, remediation). Past runs too.
- /expand (and Ctrl+O) dump the last full, untruncated commands.
- Findings colored by severity in the feed (not all-yellow); confirmed vote = green.

Stop & report:
- CRITICAL: /stop no longer kills validation. New SOFT stop (pool.soft) halts
  launching new agents but lets in-flight + VALIDATION finish — so confirmed
  findings are kept. /stop now asks 3 ways: [1] validate then report,
  [2] report raw (no validation), [3] discard.
- Report file:// URL printed on completion/stop.

Report:
- Typst report restructured: executive summary, a Vulnerability Summary TABLE
  (#, vuln, severity, CVSS, OWASP/CWE), and per-finding sections with criticality,
  CVSS, OWASP/CWE, description/impact, PoC, evidence, remediation. owasp passed through.

Agents: +14 app-stack/CVE (IIS tilde/WebDAV/ViewState/debug/handler-bypass,
CMS fingerprint + WordPress/Joomla/Drupal/default-admin, app-server consoles,
exposed VCS, known-CVE & outdated-component exploitation) → 343 total.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 23:21:43 -03:00
CyberSecurityUPandClaude Opus 4.8 df73c0e134 v3.5.1 fix: critical char-boundary panic (was dropping findings) + background runs, progress bar, severity colors, /help
CRITICAL BUG: truncate()/source-context slices cut strings by BYTE, panicking on
a multibyte char (e.g. '—'). The panic crashed agent tasks → task.await returned
JoinError → unwrap_or_default() → empty RunOutput. Result: real confirmed findings
(win.ini traversal, HTML injection) were silently lost, workdir was empty, report
missing. Now all string truncation is char-safe (models.rs, pipeline.rs, repl.rs).

Also:
- Background runs: /run now runs in the BACKGROUND via rustyline's ExternalPrinter
  — the REPL keeps accepting commands while the engagement streams live. New
  /status (live phase + progress bar + findings) and /stop (graceful). Findings
  persist to history + report on completion (finalize_run ensures workdir is set
  even on abort, fixing "no report file in ").
- Progress bar: agents-done/total with %, shown in /status.
- Severity colors in the live feed (Critical=red…Info=grey); confirmed vote = green.
- /help reformatted into clear aligned sections.
- TUTORIAL: document non-blocking runs, /status progress, /stop, colors.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 23:04:50 -03:00
CyberSecurityUPandClaude Opus 4.8 ab0161ee53 v3.5.1 fix: view inserted config + clear REPL run boundaries
- BUG: /auth (and /creds /focus /target /repo) with no argument CLEARED the value
  instead of showing it — so typing /auth to view wiped your credential. Now no-arg
  prints the current value; clear only with an explicit `clear`.
- /show now also displays API-key status (set/missing) for the selected models'
  providers, and a hint of which commands edit config.
- REPL /run prints a clear "▶ RUNNING (prompt returns when done; use tui for live)"
  banner before and "◀ back to the NeuroSploit REPL" after, so it's obvious the
  REPL didn't disappear during a run.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 22:45:39 -03:00
CyberSecurityUPandClaude Opus 4.8 16e45eb0a3 v3.5.1: robust README + detailed TUTORIAL.md + cross-platform install (Linux/macOS/Windows · x64/arm64)
- README rewritten: engagement-modes table, highlights, supported-platforms
  matrix, agents 329, links to the tutorial.
- TUTORIAL.md: full user guide — concepts, install, auth (API/subscription),
  models, all modes (black/white/grey/host), REPL, TUI, creds.yaml, steering,
  outputs/reports, per-project memory, POMDP/grounding/chaining, agent library,
  MCP, troubleshooting, command/flag reference.
- setup.sh: detect OS (Linux/macOS/Windows) + arch (x64/arm64); v3.5.1 banner.
- install.ps1: native Windows PowerShell one-liner (winget/rustup, build, PATH).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 22:39:10 -03:00
CyberSecurityUPandClaude Opus 4.8 3f78a2b686 v3.5.1: REPL quick-wins — @ list-completion menu, /diff (what-changed), /retest
- Claude-Code-style @ menu: rustyline CompletionType::List so @path shows a
  file/folder selection list (Tab), not inline cycling.
- /diff (/changed): shows new (+) / gone (-) findings between the last two runs.
- /retest [n]: loads a past run's target/repo and seeds a re-verify focus on its
  findings → /run to check if they're fixed.
- Both added to Tab-complete and /help.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 22:32:26 -03:00
CyberSecurityUPandClaude Opus 4.8 639c2209f7 v3.5.1: attack-chain agents (12) + per-project .neurosploit/ persistence & resume
Chaining:
- agents_md/chains/ (12 multi-stage exploitation playbooks): SQLi→RCE→LPE,
  SSRF→AWS-creds, SSRF→RCE, upload→RCE, upload→LFI→RCE→LPE, XSS→ATO, IDOR→ATO,
  SSTI→RCE→cloud, default-creds→domain, deserialization→RCE, exposed-git→RCE,
  subdomain-takeover→trusted-abuse. Each stage proven by a tool receipt before
  advancing; reports chains_from edges.
- Loaded as a `chains` category (→ 329 agents). chain_round now injects the chain
  recipes as a menu so the LLM applies proven multi-stage paths.

Persistence (no DB — structured state):
- Per-project `<cwd>/.neurosploit/` holding session.json (config), runs.json
  (history), history.txt (readline). REPL resumes target/repo/auth/focus/models
  on reopen; saves on /run and /quit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 22:30:22 -03:00
CyberSecurityUPandClaude Opus 4.8 f8d70ce9c5 v3.5.1: infra/host engagements — IP + SSH/Windows-AD creds + Linux/Win/AD agents + REPL context bar
Infra:
- creds.yaml gains `ssh:` (host/port/user/password/key) and `windows:`/`ad:`
  (host/user/password/domain/ntlm-hash) blocks; multi-block YAML parser.
  host_instruction() tells agents how to authenticate to the host.
- 14 infra agents (agents_md/infra/): port/service scan, SMB enum, Linux privesc/
  sudo/cron/SSH, Windows privesc/SMB-signing/WinRM, AD kerberoast/asreproast/ACL/
  DCSync/default-creds. Loader gains `infra` category → 317 agents total.
- run_host pipeline + `neurosploit host <ip> --creds creds.yaml` (and Mode::Host
  in run_mode/TUI): host recon (nmap/netexec) → infra agent selection → test →
  validate → chain → report, with host tooling doctrine + supplied creds.

REPL:
- Context/status bar above the prompt: "model auth · cwd · mode▸target"
  (e.g. claude-opus-4-8 sub · /opt/projeto · black-box▸app.acme.com).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 22:17:14 -03:00
CyberSecurityUPandClaude Opus 4.8 969af20a8e v3.5.1: Mission Control TUI (ratatui) — concurrent panels + composer active during run
- `neurosploit tui <url> [--repo ..] [--model ..] [--subscription] [--mcp] [--focus ..]`
- Concurrent ratatui UI driven by the engagement's live event stream:
  * fixed status header: target · mode · model · phase · elapsed · token/cost · findings · ⏸
  * live activity feed (color-coded: commands, recon, findings, errors)
  * live Findings panel (severity-styled) and a Targets map (hosts → state)
  * composer input that stays active WHILE the runner streams — local, non-blocking
    answers: `summary`/`what` (partial summary), `pause` (graceful stop), `errors`
    (filter), `clear`, or free-text notes.
- Engagement runs as a tokio task; UI drains an mpsc channel each ~120ms tick.
  Esc/Ctrl-C requests a graceful stop; report is generated on exit (status stopped/complete).
- Terminal setup before task spawn → clean error on non-TTY, no detached run.
- README documents the TUI mode.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 21:52:53 -03:00
CyberSecurityUPandClaude Opus 4.8 78653e45cd v3.5.1: live findings feed + 🔔 notifications + automatic partial summary
- Live findings feed: each candidate is surfaced (✦ possible finding [sev] title
  @ endpoint) the moment an agent returns it, not only at the end.
- 🔔 notifications in the feed: evidence saved, phase complete (with severity
  breakdown = automatic partial summary). Renderer styles notify/finding tags.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 21:43:24 -03:00
CyberSecurityUPandClaude Opus 4.8 a8676fee0a v3.5.1: POMDP belief-state + value-of-information planner + grounded anti-hallucination
Partial observability is now first-class:

- belief.rs — property-graph world model; nodes (host/service/vuln/exploit/cred)
  carry a probability, not a boolean. Bayesian observation updates; per-node
  Shannon entropy; mean-uncertainty + recon-frontier. Black-box = diffuse priors
  that sharpen with observation; white-box collapses toward deterministic (MDP).
- pomdp.rs — value_of_information(), decide() (recon vs exploit falls out of
  belief entropy), and may_assert() — the mathematical anti-hallucination gate:
  no exploitability claim while the belief is diffuse (high entropy) → observe first.
- grounding.rs — verification engine, hard rule "no claim without a tool receipt":
  empirical grounding for black-box (raw HTTP/OOB/error markers), symbolic for
  white-box (file:line into reviewed source). Ungrounded claims demoted + flagged
  receipt_missing (feeds future reward shaping).
- pipeline.finish(): grounding gate before reporting + belief-uncertainty readout.
- bump 3.5.0 → 3.5.1; README documents the v3.5.1 belief/grounding architecture
  and the infra/bandit/reward roadmap.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 21:41:18 -03:00
81 changed files with 7071 additions and 429 deletions
+42 -170
View File
@@ -1,188 +1,60 @@
# NeuroSploit v3 Environment Variables
# =====================================
# Copy this file to .env and configure your API keys
# NeuroSploit v3.5.1 — environment / API keys (optional)
# ------------------------------------------------------------------
# You only need this for the API-key auth path. If you log in with a
# local subscription CLI instead (--subscription with Claude / Codex /
# Gemini / Grok), you don't need any key here.
#
# IMPORTANT: You MUST set at least one LLM API key for the AI agent to work!
# Set the key(s) for the providers you use, then load and run:
# set -a; . ./.env; set +a
# neurosploit run http://target --model anthropic:claude-opus-4-8 -v
#
# Provider prefix -> env var (use as `--model <prefix>:<model>`).
# =============================================================================
# LLM API Keys (REQUIRED - at least one must be set)
# =============================================================================
# Get your Claude API key at: https://console.anthropic.com/
# anthropic: https://console.anthropic.com/
ANTHROPIC_API_KEY=
# OpenAI: https://platform.openai.com/api-keys
# openai: https://platform.openai.com/api-keys
OPENAI_API_KEY=
# Google Gemini: https://aistudio.google.com/app/apikey
# gemini: https://aistudio.google.com/app/apikey
# (GOOGLE_API_KEY is also accepted as an alias if GEMINI_API_KEY is unset)
GEMINI_API_KEY=
#GOOGLE_API_KEY=
# OpenRouter (multi-model): https://openrouter.ai/keys
OPENROUTER_API_KEY=
# azure: Azure OpenAI (OpenAI-compatible). Use `--model azure:<deployment>`
# (the model name is your Azure *deployment* name).
#AZURE_OPENAI_API_KEY=
#AZURE_OPENAI_ENDPOINT=https://your-resource.openai.azure.com
#AZURE_OPENAI_API_VERSION=2024-10-21
# xAI Grok: https://console.x.ai/ (used by the Grok CLI backend)
# xai: https://console.x.ai/
XAI_API_KEY=
# NVIDIA NIM (PR #28): https://build.nvidia.com/ keys look like `nvapi-...`
# OpenAI-compatible endpoint at https://integrate.api.nvidia.com/v1
# nvidia_nim: https://build.nvidia.com/ (keys look like nvapi-...)
NVIDIA_NIM_API_KEY=
# Together AI: https://api.together.xyz/settings/api-keys
# deepseek: https://platform.deepseek.com/
DEEPSEEK_API_KEY=
# mistral: https://console.mistral.ai/
MISTRAL_API_KEY=
# qwen: https://dashscope-intl.aliyuncs.com/ (Alibaba DashScope)
DASHSCOPE_API_KEY=
# groq: https://console.groq.com/keys
GROQ_API_KEY=
# together: https://api.together.xyz/settings/api-keys
TOGETHER_API_KEY=
# Fireworks AI: https://fireworks.ai/account/api-keys
FIREWORKS_API_KEY=
# openrouter: https://openrouter.ai/keys
OPENROUTER_API_KEY=
# Azure OpenAI: https://portal.azure.com/
#AZURE_OPENAI_API_KEY=
#AZURE_OPENAI_ENDPOINT=https://your-resource.openai.azure.com/
#AZURE_OPENAI_API_VERSION=2024-02-01
#AZURE_OPENAI_DEPLOYMENT=gpt-4o
# ollama: local, no key needed. Override the endpoint if not default:
#OLLAMA_BASE_URL=http://localhost:11434/v1
# =============================================================================
# Local LLM (optional - no API key needed)
# =============================================================================
# Ollama: https://ollama.ai
#OLLAMA_BASE_URL=http://localhost:11434
# LM Studio: https://lmstudio.ai
#LMSTUDIO_BASE_URL=http://localhost:1234
# =============================================================================
# LLM Configuration
# =============================================================================
# Max output tokens (up to 64000 for Claude). Comment out for profile defaults.
#MAX_OUTPUT_TOKENS=64000
# Select specific model name (e.g., claude-sonnet-4-20250514, gpt-4o, llama3.2, qwen2.5)
# Leave empty for provider default
#DEFAULT_LLM_MODEL=
# Enable task-type model routing (routes to different LLM profiles per task)
ENABLE_MODEL_ROUTING=false
# =============================================================================
# Feature Flags
# =============================================================================
# Bug bounty dataset cognitive augmentation
ENABLE_KNOWLEDGE_AUGMENTATION=false
# Playwright browser-based validation + screenshot capture
ENABLE_BROWSER_VALIDATION=false
# =============================================================================
# Agent Autonomy (Phase 1-5 modules)
# =============================================================================
# Token budget per scan (limits total LLM tokens). Comment out for unlimited.
#TOKEN_BUDGET=100000
# Enable AI reasoning engine (think/plan/reflect at checkpoints)
ENABLE_REASONING=true
# Enable CVE/exploit search (NVD API + GitHub)
ENABLE_CVE_HUNT=true
# NVD API key for higher rate limits: https://nvd.nist.gov/developers/request-an-api-key
#NVD_API_KEY=
# NVIDIA NIM API key for free 40 RPM endpoint
NIM_API_KEY=
# NVIDIA NIM Model (optional - defaults to openai/gpt-oss-120b)
#NIM_MODEL=
# GitHub token for exploit search (optional, increases rate limit)
#GITHUB_TOKEN=
# Enable multi-agent orchestration (replaces default 3-stream architecture)
# WARNING: Experimental - uses specialist agents instead of parallel streams
ENABLE_MULTI_AGENT=false
# Enable AI Researcher agent (0-day discovery with Kali sandbox)
# Requires enable_kali_sandbox=true per scan (frontend checkbox)
ENABLE_RESEARCHER_AI=true
# CLI Agent (AI CLI tools inside Kali sandbox)
# Runs Claude Code / Gemini CLI / Codex CLI inside Kali container as pentest engine
#ENABLE_CLI_AGENT=true
#CLI_AGENT_MAX_RUNTIME=1800
#CLI_AGENT_DEFAULT_PROVIDER=claude_code
# Kali sandbox Docker image name
#KALI_SANDBOX_IMAGE=neurosploit-kali:latest
# =============================================================================
# Smart Router (OAuth + API provider routing)
# =============================================================================
# Enable Smart Router for automatic provider failover and CLI OAuth token reuse
#ENABLE_SMART_ROUTER=true
# =============================================================================
# RAG System (Retrieval-Augmented Generation)
# =============================================================================
# Enable RAG for semantic search over vuln knowledge, bug bounty data, etc.
ENABLE_RAG=true
# RAG backend: auto (best available), chromadb, tfidf, bm25
RAG_BACKEND=auto
# =============================================================================
# Methodology File (deep injection into agent prompts)
# =============================================================================
# Path to .md methodology file (FASE-based pentest methodology)
#METHODOLOGY_FILE=/opt/Prompts-PenTest/pentestcompleto_en.md
# =============================================================================
# Vuln Type Agents (per-vuln parallel orchestration)
# =============================================================================
# Enable parallel per-vuln-type specialist agents
ENABLE_VULN_AGENTS=false
# =============================================================================
# Notifications (multi-channel scan alerts)
# =============================================================================
#ENABLE_NOTIFICATIONS=false
#NOTIFICATION_SEVERITY_FILTER=critical,high
# Discord webhook for scan alerts
#DISCORD_WEBHOOK_URL=
# Telegram bot alerts
#TELEGRAM_BOT_TOKEN=
#TELEGRAM_CHAT_ID=
# WhatsApp/Twilio alerts
#TWILIO_ACCOUNT_SID=
#TWILIO_AUTH_TOKEN=
#TWILIO_FROM_NUMBER=
#TWILIO_TO_NUMBER=
# =============================================================================
# Database (default is SQLite - no config needed)
# =============================================================================
DATABASE_URL=sqlite+aiosqlite:///./data/neurosploit.db
# =============================================================================
# Server Configuration
# =============================================================================
HOST=0.0.0.0
PORT=8000
DEBUG=false
# =============================================================================
# NeuroSploit v3.3.0 — Autonomous MD-Agent Engine
# =============================================================================
# The engine delegates execution to a locally-installed agentic CLI backend.
# Default backend (claude | codex | grok). First installed is used if unset.
NEUROSPLOIT_BACKEND=claude
# Default provider/model (see neurosploit_agent/models.py)
NEUROSPLOIT_PROVIDER=anthropic
NEUROSPLOIT_MODEL=claude-opus-4-8
# OOB collaborator host for blind/SSRF/XXE proof (optional)
NEUROSPLOIT_COLLABORATOR=
# Reinforcement-learning loop (1=on). State persists to data/rl_state.json
NEUROSPLOIT_RL=1
# Playwright MCP for browser-based proof of execution (1=on; needs npx)
NEUROSPLOIT_MCP=1
# OpenAI-compatible base URL override (set automatically per provider)
#OPENAI_BASE_URL=
# litellm: point at your LiteLLM proxy (OpenAI-compatible). Route any
# model through it as `--model litellm:<model>`.
#LITELLM_BASE_URL=http://localhost:4000/v1
LITELLM_API_KEY=
+96
View File
@@ -0,0 +1,96 @@
name: Release builds
# Builds self-contained NeuroSploit binaries for every OS/arch and uploads them
# to the matching GitHub Release. Fires automatically on a pushed `v*` tag, or
# manually via "Run workflow" (provide the tag).
on:
push:
tags: ["v*"]
workflow_dispatch:
inputs:
tag:
description: "Release tag to build & attach (e.g. v3.5.2)"
required: true
permissions:
contents: write
jobs:
build:
name: ${{ matrix.label }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- { os: ubuntu-22.04, label: linux-x64, ext: tar.gz, target: "" }
- { os: ubuntu-24.04-arm, label: linux-arm64, ext: tar.gz, target: "" }
# macOS x64 is cross-built on an Apple-Silicon runner (no scarce Intel runner).
- { os: macos-14, label: macos-x64, ext: tar.gz, target: x86_64-apple-darwin }
- { os: macos-14, label: macos-arm64, ext: tar.gz, target: "" }
- { os: windows-latest, label: windows-x64, ext: zip, target: "" }
steps:
- uses: actions/checkout@v4
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- name: Cache cargo
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
neurosploit-rs/target
key: ${{ matrix.label }}-cargo-${{ hashFiles('neurosploit-rs/Cargo.lock') }}
- name: Build (release)
working-directory: neurosploit-rs
shell: bash
run: |
if [ -n "${{ matrix.target }}" ]; then
cargo build --release --target "${{ matrix.target }}"
else
cargo build --release
fi
- name: Resolve tag
id: tag
shell: bash
run: echo "tag=${{ github.event.inputs.tag || github.ref_name }}" >> "$GITHUB_OUTPUT"
- name: Package
shell: bash
run: |
set -e
TAG="${{ steps.tag.outputs.tag }}"
NAME="neurosploit-${TAG}-${{ matrix.label }}"
mkdir -p "dist/$NAME"
cp -R agents_md "dist/$NAME/"
cat > "dist/$NAME/README.txt" <<EOF
NeuroSploit ${TAG} — ${{ matrix.label }}
Run from inside this folder so it finds agents_md/, e.g.:
./neurosploit --version
./neurosploit run http://testphp.vulnweb.com/ --model anthropic:claude-opus-4-8 -v
Or set NEUROSPLOIT_BASE to this folder and run neurosploit from anywhere.
EOF
BINDIR="neurosploit-rs/target/release"
if [ -n "${{ matrix.target }}" ]; then BINDIR="neurosploit-rs/target/${{ matrix.target }}/release"; fi
if [ "${{ runner.os }}" = "Windows" ]; then
cp "$BINDIR/neurosploit.exe" "dist/$NAME/"
(cd dist && 7z a "${NAME}.zip" "$NAME" >/dev/null)
else
cp "$BINDIR/neurosploit" "dist/$NAME/"
(cd dist && tar -czf "${NAME}.tar.gz" "$NAME")
fi
- name: Upload to release
shell: bash
env:
GH_TOKEN: ${{ github.token }}
run: |
TAG="${{ steps.tag.outputs.tag }}"
gh release upload "$TAG" dist/neurosploit-*.${{ matrix.ext }} --clobber
+6
View File
@@ -102,3 +102,9 @@ neurosploit-rs/runs/
v34_gui.png
data/repl_runs.json
data/repl_history.txt
.neurosploit/
/tmp/*
# Cloned source repos (whitebox/greybox from a git URL)
repos/
neurosploit-rs/repos/
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Joas A Santos & Red Team Leaders
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+129 -12
View File
@@ -1,4 +1,4 @@
<h1 align="center">🧠 NeuroSploit v3.5.0</h1>
<h1 align="center">🧠 NeuroSploit v3.5.4</h1>
<p align="center">
<a href="https://github.com/JoasASantos/NeuroSploit/stargazers"><img src="https://img.shields.io/github/stars/JoasASantos/NeuroSploit?style=for-the-badge&logo=github&color=8b5cf6" alt="Stars"></a>
@@ -8,11 +8,12 @@
</p>
<p align="center">
<img src="https://img.shields.io/badge/Version-3.5.0-blue?style=flat-square">
<img src="https://img.shields.io/badge/Version-3.5.4-blue?style=flat-square">
<img src="https://img.shields.io/badge/Harness-Rust%20%7C%20tokio-e6b673?style=flat-square">
<img src="https://img.shields.io/badge/License-MIT-green?style=flat-square">
<img src="https://img.shields.io/badge/MD%20Agents-303-red?style=flat-square">
<img src="https://img.shields.io/badge/MD%20Agents-329-red?style=flat-square">
<img src="https://img.shields.io/badge/Models-12%20providers-success?style=flat-square">
<img src="https://img.shields.io/badge/Modes-Black%20%7C%20White%20%7C%20Grey%20%7C%20Host-9cf?style=flat-square">
<img src="https://img.shields.io/badge/Auth-API%20key%20%7C%20Subscription-orange?style=flat-square">
</p>
@@ -20,29 +21,93 @@
<i>by Joas A Santos &amp; Red Team Leaders</i></p>
> ⭐ If this is useful, **star the repo** — it helps a lot.
>
> 📖 **New here? Read the [full Tutorial & User Guide →](TUTORIAL.md)** — every mode, flag, config and example explained.
> 🆕 **New in v3.5.4 — Robust attack chaining + fewer false positives:** a
> multi-round, decision-driven **post-exploitation** engine takes each confirmed
> foothold and expands new directions (cred reuse, privesc, lateral movement,
> exfil, new surface), carrying **loot** forward across rounds (`--chain-depth`).
> Validation is now **severity-aware** (High/Critical need ≥2 validators & ≥2/3
> agreement) with an **adversarial refute pass** that drops findings that can't
> withstand a skeptic.
> *(v3.5.3 added GitHub/GitLab/Jira **[integrations](TUTORIAL-INTEGRATION.md)**; v3.5.2 the DEPTH doctrine + report-hygiene pass — see [RELEASE.md](RELEASE.md).)*
---
**Autonomous, multi-model penetration-testing harness — Rust, CLI-only.**
**NeuroSploit** turns a URL, a source repository, a running app, or a host/IP into
an autonomous security engagement. A Rust harness (`tokio`) drives a **pool of
LLMs** — via **API key** or local **subscription** (Claude Code / Codex / Gemini /
Grok) — recons the target, **intelligently selects only the agents that match the
discovered surface**, runs them in parallel, **chains** findings into deeper
impact, and **validates every claim by cross-model voting + tool-receipt
grounding** before reporting. It ships **329 markdown agents** and a **Mission
Control TUI**.
This branch is the **slim, Rust-only** distribution: the `neurosploit-rs/` workspace
plus the `agents_md/` agent library. It turns a URL (black-box) or a code
repository (white-box) into an autonomous engagement that drives a pool of LLMs
— via **API key** or local **subscription** (Claude Code / Codex / Gemini / Grok)
— recons the target, **intelligently selects only the agents matching the
discovered surface**, runs them in parallel, then validates every finding by
**cross-model voting** before reporting.
### Engagement modes
> The full project (Python engine, web GUIs, history) lives on the `main` branch.
| Mode | Command | What it does |
|------|---------|-------------|
| **Black-box** | `neurosploit run <url>` | recon → select → exploit → vote → report |
| **White-box** | `neurosploit whitebox <repo>` | source/SAST review (file:line evidence) |
| **Grey-box** | `neurosploit greybox <repo> --url <app>` | code review **+** live exploitation together |
| **Host/Infra** | `neurosploit host <ip> --creds creds.yaml` | Linux / Windows / Active Directory testing |
| **Mission Control** | `neurosploit tui <url>` | live TUI panels + composer during the run |
| **Interactive** | `neurosploit` | persistent REPL session (resumes per project) |
### Highlights
- 🧠 **POMDP belief + value-of-information** — the target is partially observable,
so findings aren't booleans: a property-graph **belief** carries probabilities,
and "scan more vs exploit now" falls out of belief entropy. The `may_assert`
gate is a **mathematical anti-hallucination rule** (don't claim exploitability
while the belief is diffuse).
- 🧾 **Grounding** — hard rule: **no claim without a tool receipt** (raw tool
output, not paraphrase). Empirical for black-box, symbolic (`file:line`) for
white-box; ungrounded claims are demoted.
- 🔗 **Attack chaining** — 12 multi-stage chain agents (SQLi→RCE→LPE, SSRF→AWS
creds, upload→LFI→RCE→LPE, default-creds→domain, …); each stage proven before
advancing.
- 🗺️ **Attack graph & kill chain** — findings mapped to OWASP / CWE / MITRE
ATT&CK / stage; rendered as a Mermaid graph in the report.
-**Cross-model validation** — a different model adjudicates each finding;
RL-weighted, recon-aware agent selection.
- 🛰️ **Mission Control TUI** — live header/feed/findings/targets panels + a
composer you can type in *while the run streams* (`summary`, `pause`, …).
- 💾 **Per-project memory**`<cwd>/.neurosploit/` keeps session, run history and
command history; the REPL **resumes** on reopen. No database required.
- 🪙 **Token/cost telemetry**, per-agent attribution, graceful Ctrl-C → report or
discard, Typst/HTML/JSON/MD reports.
> This is the **slim, Rust-only** distribution (`neurosploit-rs/` + `agents_md/`).
> The earlier Python engine and web GUIs live on the older `v3.4.0` branch.
---
## 📦 Install (one line)
**Linux / macOS** (x64 & arm64):
```bash
curl -fsSL https://raw.githubusercontent.com/JoasASantos/NeuroSploit/main/setup.sh | bash
```
**Windows** (PowerShell, x64 & arm64):
```powershell
irm https://raw.githubusercontent.com/JoasASantos/NeuroSploit/main/install.ps1 | iex
```
### Supported platforms
| OS | x64 | arm64 |
|----|-----|-------|
| **Linux** (Kali recommended) | ✅ | ✅ |
| **macOS** | ✅ | ✅ (Apple Silicon) |
| **Windows** | ✅ | ✅ |
Pure Rust + stdlib, so it builds natively everywhere a stable Rust toolchain runs.
The installer auto-detects OS/arch and installs Rust if missing. On native Windows
use `install.ps1`; under WSL2 / Git Bash the `setup.sh` one-liner also works.
The installer auto-installs Rust if needed, clones the repo to `~/.neurosploit`,
builds the release binary, and links `neurosploit` into `~/.local/bin`. Re-run it
any time to update. Tweak with env vars: `NEUROSPLOIT_REF` (branch/tag),
@@ -63,12 +128,64 @@ neurosploit
# or one-liner (subscription login, no API key needed):
neurosploit run http://testphp.vulnweb.com/ --subscription --model anthropic:claude-opus-4-8 -v
# white-box — review a source repository (SAST agents, file:line evidence):
git clone https://github.com/digininja/DVWA /tmp/DVWA
neurosploit whitebox /tmp/DVWA --subscription --model anthropic:claude-opus-4-8 -v
# grey-box — review the code AND exploit the running app together:
neurosploit greybox /tmp/DVWA --url http://localhost:8080/ --creds creds.yaml \
--subscription --model anthropic:claude-opus-4-8 --mcp -v
# host / infra — Linux / Windows / Active Directory (SSH/Win creds in creds.yaml):
neurosploit host 10.0.0.10 --creds creds.yaml --subscription --model anthropic:claude-opus-4-8 -v
# 🛰 Mission Control TUI — live panels (header/feed/findings/targets) + a composer
# you can type in WHILE the run streams (summary · pause · errors · notes):
neurosploit tui http://testphp.vulnweb.com/ --subscription --model anthropic:claude-opus-4-8 --mcp
```
> Full step-by-step for every mode (black/white/grey/host) is in **[TUTORIAL.md](TUTORIAL.md)**.
No login? Use an **API key** instead — see [Authentication](#authentication--run-via-api-key-or-subscription).
---
## 🔌 Integrations (GitHub · GitLab · Jira)
Wire NeuroSploit into your SDLC. Toggle from the REPL (`/integrations`) or the CLI
(`neurosploit integrations enable github|gitlab|jira`). **Tokens are never stored**
— only the *name* of the env var is saved; the value is read from your environment.
```bash
export GITHUB_TOKEN=ghp_... # PAT with `repo` scope (private repos)
neurosploit integrations enable github
# Review a Pull Request's code (clones the PR head, white-box) and comment back:
neurosploit pr digininja/DVWA 42 --subscription --model anthropic:claude-opus-4-8 --comment
# Watch a branch and re-review on every new commit:
neurosploit watch myorg/private-app --branch main --subscription --model anthropic:claude-opus-4-8
# Private GitLab repo (token-injected clone) — works in whitebox/greybox:
export GITLAB_TOKEN=glpat-... ; neurosploit integrations enable gitlab
neurosploit whitebox https://gitlab.com/myorg/private-svc --subscription --model anthropic:claude-opus-4-8
# Open a Jira card per finding (any engagement):
export JIRA_EMAIL=you@org.com JIRA_API_TOKEN=... # set base/project once: /integrations setup jira
neurosploit whitebox https://github.com/myorg/app --jira --subscription --model anthropic:claude-opus-4-8
```
| Integration | What you get | Env vars |
|-------------|--------------|----------|
| **GitHub** | private clone · `pr` review + comment · `watch` branch | `GITHUB_TOKEN` |
| **GitLab** | private clone for whitebox/greybox | `GITLAB_TOKEN` |
| **Jira** | one card per finding (`--jira`) | `JIRA_EMAIL`, `JIRA_API_TOKEN` |
📖 Step-by-step setup for each tool: **[TUTORIAL-INTEGRATION.md](TUTORIAL-INTEGRATION.md)**.
---
## Build
```bash
+278
View File
@@ -1,3 +1,281 @@
# NeuroSploit v3.5.4 — Release Notes
**Release Date:** July 2026
**Codename:** Robust Attack Chaining & False-Positive Reduction
**License:** MIT
**Credits:** Joas A Santos & Red Team Leaders
---
## TL;DR
v3.5.4 makes NeuroSploit both **deeper** and **more precise**: a real multi-round
**post-exploitation attack-chaining** engine that expands each foothold in new
directions, plus stronger **false-positive** controls so what it reports is
trustworthy.
## Attack chaining (robust, decision-driven)
Replaces the old single-shot chainer with **`attack_chain()`** — an iterative,
per-foothold pivot engine:
- **Per-foothold decisions.** Each round takes the newest confirmed footholds
(best-first, capped per round) and, for **each one**, an agent decides which
directions to expand and proves new impact: **post-exploitation** (loot
creds/keys/config/source), **credential reuse**, **privilege escalation**
(horizontal & vertical), **lateral movement** to adjacent services/hosts,
**data exfiltration**, and **new attack surface** the foothold exposes.
- **Loot carried forward.** Credentials/tokens/hosts/endpoints discovered in one
round are passed to later rounds and reused (agent returns
`{"findings":[...],"loot":[...]}`), so the engine genuinely pivots in new
directions instead of re-testing the same spot.
- **No pivoting off false positives.** Each round's new findings are validated
before they become the next round's footholds.
- **Convergence.** Runs up to `chain_depth` rounds **or** stops when a round finds
nothing new (loop-until-dry).
- **Control.** New `RunConfig.chain_depth` (default **2**) and a `--chain-depth`
flag on every engagement command (`0` disables).
## False-positive reduction
- **Robust verdict parsing** (`pool::parse_verdict`) — whitespace-insensitive,
checks explicit rejection first, counts only explicit confirmations; ambiguous
replies are *not* counted as confirmed. Replaces the fragile exact-JSON /
loose-`yes` matching.
- **Severity-aware quorum** (`pool::quorum_confirmed`) — **High/Critical now need
≥2 validators AND ≥2/3 agreement** (a single vote can no longer confirm a
Critical); lower severities need a strict majority. Single-model panels fall
back to majority so they aren't nuked.
- **Adversarial refute pass** — every confirmed High/Critical is re-examined by a
skeptical panel that assumes false-positive; findings that can't withstand a
majority of skeptics are dropped.
- **Stronger validator prompt** with an explicit false-positive checklist
(reflected-not-executed, version/banner guesses, self-XSS, error-as-injection,
thin evidence, inflated severity).
## Notes
- Additive and back-compatible; defaults keep behavior sensible if you change
nothing. Unit tests cover verdict parsing, quorum, and report-hygiene logic.
---
# NeuroSploit v3.5.3 — Release Notes
**Release Date:** June 2026
**Codename:** Integrations (GitHub · GitLab · Jira)
**License:** MIT
**Credits:** Joas A Santos & Red Team Leaders
---
## TL;DR
v3.5.3 plugs NeuroSploit into your SDLC: review **private** GitHub/GitLab repos
and **Pull Requests**, **watch** a branch and re-review on every commit, and open
a **Jira card per finding** — all toggleable via a new `/integrations` command.
## Highlights
- **GitHub integration**
- **Private repos**: when enabled, `whitebox` / `greybox --repo` / `tui --repo`
inject your `GITHUB_TOKEN` into the clone URL (token never printed/stored).
- **`neurosploit pr <owner/repo> <number>`** — clones the **PR head**
(`refs/pull/N/head`), runs a white-box review, optionally **posts a summary
comment** back on the PR (`--comment`) and/or **opens Jira cards** (`--jira`).
- **`neurosploit watch <owner/repo> --branch <b> --interval <s>`** — polls the
branch and runs a white-box review **each time a new commit lands**.
- **GitLab integration** — private clone (token-injected) for `whitebox`/`greybox`
against `gitlab.com` or a self-hosted base.
- **Jira integration** — `--jira` on any engagement (or `pr`/`watch`) opens **one
card per finding** (summary, severity, CVSS, CWE, location, PoC, evidence,
remediation) in your project via the Jira REST API.
- **`/integrations` (REPL) + `neurosploit integrations` (CLI)** — `show`,
`enable`/`disable <github|gitlab|jira>`, and `setup <jira|gitlab|github>`
(interactive). Config persists to `<project>/.neurosploit/integrations.json`.
**Secrets are never stored** — only the env-var *name* is saved; values come
from the environment at use time.
- New harness module `integrations` + app commands `pr` / `watch` /
`integrations`, plus a `--jira` flag on `run` / `whitebox`.
## Setup
Step-by-step for tokens, scopes and configuration is in
**[TUTORIAL-INTEGRATION.md](TUTORIAL-INTEGRATION.md)** and summarized in the README.
## Notes
- Additive and back-compatible: all existing modes/flags are unchanged; if no
integration is enabled the behavior is identical to v3.5.2.
- Tokens use env vars: `GITHUB_TOKEN`, `GITLAB_TOKEN`, `JIRA_EMAIL` +
`JIRA_API_TOKEN` (names configurable per integration).
---
# NeuroSploit v3.5.2 — Release Notes
**Release Date:** June 2026
**Codename:** Exploitation Depth & Report Hygiene
**License:** MIT
**Credits:** Joas A Santos & Red Team Leaders
---
## TL;DR
v3.5.2 hard-codes the discipline that separates a great pentest from a noisy
one — distilled from reviewing real AI-pentest output that kept stopping at
*"exposed"* instead of *"exploited"*. The engine now pushes every exposure to
demonstrated impact, **chains** findings, decodes/fingerprints artifacts and
correlates CVEs, audits tokens, and keeps the final report honest (deduplicated
and severity-calibrated).
## Highlights
- **DEPTH doctrine (exploit, don't just expose).** A new doctrine is injected
into every exploitation prompt (black/grey/chain): any info-disclosure,
exposed service/catalog/WSDL, leaked credential/token, or reachable dev host
**must be USED** before it can be a finding — call it, decode it, log in, hit
the dev host. If it was only observed, it's reported as a **lead**, not a
confirmed High/Critical.
- **Finding chaining.** Reuse any session/JWT/cookie/credential obtained in one
step across all other modules; pivot access into IDOR/privesc/exfil and report
the **chain**, not isolated parts (e.g. captcha-bypass→admin JWT→authenticated
surface; enum + no-rate-limit→password spraying).
- **Decode & fingerprint → CVE.** Decode opaque tokens/paths (base64/JSON/marshal)
and pin exact library/gem/plugin/CMS versions, then correlate to known CVEs and
attempt a safe PoC.
- **Token auditor.** JWT alg-confusion (RS→HS), `alg:none`, kid/jku injection,
real signature verification, **weak HS256 secret cracking**, and token
lifecycle (logout/expiry/refresh).
- **Report-hygiene & depth pass (deterministic, in the harness).** After
validation the run now:
- **calibrates severity to proven impact** — an unproven High/Critical
(hedged language, no payload, thin evidence) is capped to Medium and
re-titled "(potential)";
- flags **"exposed → exploited" gaps** — exposures on a host with no actual
exploit get an advisory to go use them;
- advises **consolidating hygiene** classes (headers/cookies/TLS/HSTS/
clickjacking/disclosure) repeated across many assets into ONE finding with
an affected-asset table, instead of inflating the count one-per-host.
- **5 new doctrine meta-agents** (`agents_md/meta/`): `exploit_depth_doctrine`,
`finding_chainer`, `artifact_decoder`, `token_auditor`, `report_calibrator`
(meta agents 17 → 22; total library 343 → 348).
- **Source from a GitHub URL.** `whitebox` / `greybox --repo` (and the REPL
`/repo`) now accept a **git URL** (`https://github.com/owner/repo[.git]`) or an
`owner/repo` shorthand — the repo is cloned (shallow) into `<base>/repos/` and
reviewed automatically, no manual `git clone` needed:
```bash
neurosploit whitebox https://github.com/digininja/DVWA \
--subscription --model anthropic:claude-opus-4-8 -v
```
- **Azure OpenAI provider** (resolves #21). OpenAI-compatible: set
`AZURE_OPENAI_ENDPOINT` (+ optional `AZURE_OPENAI_API_VERSION`, default
`2024-10-21`) and `AZURE_OPENAI_API_KEY`, then `--model azure:<deployment>`
(the model name is your Azure *deployment* name; auth via the `api-key`
header).
- **`GOOGLE_API_KEY` alias for Gemini** (resolves #25 confusion). Gemini's API
path reads `GEMINI_API_KEY`, and now also accepts `GOOGLE_API_KEY` (Google's
standard env var) when the former is unset. Local providers (ollama/litellm)
still need **no** key at all.
## Notes
- Pure-additive and back-compatible: existing modes, REPL, TUI, pause/continue,
crash-recovery and reports are unchanged. The hygiene pass only annotates and
down-calibrates unproven severities — it never invents or drops findings.
- New unit tests cover the calibration and depth-audit logic
(`harness::hygiene`).
---
# NeuroSploit v3.5.1 — Release Notes
**Release Date:** June 2026
**Codename:** Interactive POMDP Harness
**License:** MIT
**Credits:** Joas A Santos & Red Team Leaders
---
## TL;DR
The 3.5.x line turns the Rust harness into a full **interactive REPL** (Claude
Code / Codex / Cursor-CLI style) on top of the multi-model engine: pick models
with arrow-keys, configure API keys per provider, set target/repo/auth/creds and
free-text instructions that steer the agents, then `/run` engagements **in the
background** while you keep typing. v3.5.1 adds a **POMDP belief spine** with
anti-hallucination grounding ("no claim without a tool receipt"), **infra/host**
testing (IP + SSH + Windows/AD) with Linux/Windows/AD agents, **attack-chain
agents**, a **Mission-Control TUI**, structured **Typst** reports, and resilient
run control (live checkpointing, pause-on-quota, instant stop).
## Highlights
- **Interactive REPL** (`neurosploit` with no subcommand): real line editing
(history ↑/↓, Ctrl-A/E/K, multiline), Tab-completion of `/commands` and
`@filesystem-paths` (Claude-Code-style file menu), arrow-key model multi-select,
per-provider API-key config, and a live context bar (`model · cwd · mode▸target`).
- **Engagement modes**: **black-box** (`run`), **white-box** SAST (`whitebox`,
set `/repo`), **grey-box** (`greybox`, `/repo` + `/target`), **host/infra**
(`/target <ip>` + `/creds` for SSH / Windows / AD), plus the **TUI** dashboard.
- **POMDP belief state** (`belief.rs`, `pomdp.rs`): a property-graph with
probabilities + Bayesian update + Shannon-entropy uncertainty, a
value-of-information planner, and a **grounding gate** (`grounding.rs`,
`may_assert`) — findings must carry an empirical/symbolic **tool receipt**.
- **Infra / credentials** (`creds.rs`): multi-block YAML (jwt/header/cookie,
HTTP login, SSH, Windows/AD); real automated login; Linux/Windows/AD agents.
- **Attack-chain agents**: sqli→rce→lpe, ssrf→aws, upload→lfi→rce, and more —
injected as chain recipes during exploitation.
- **App-stack & CVE hunting**: IIS/.NET (tilde shortname, WebDAV, ViewState),
CMS (WordPress/Joomla/Drupal), app-server consoles, known-CVE exploitation.
- **13 providers** incl. **LiteLLM** proxy and Gemini/xAI alongside the existing
OpenAI-compatible set; **subscription mode** drives local agentic CLIs
(claude/codex/gemini/grok) via stream-json.
- **Mission-Control TUI** (`ratatui`): concurrent activity/findings/targets panels
with a non-blocking composer active during the run.
- **Structured Typst report**: executive summary, vulnerability-summary table,
and per-finding sections (criticality, CVSS, OWASP/CWE, PoC, evidence,
remediation) + an attack-graph / kill-chain mapping (OWASP/CWE/MITRE).
- **Per-project persistence** (`.neurosploit/`, no database): `session.json`,
`runs.json`, `history.txt` — resumes automatically on reopen.
## Run control (new in 3.5.1)
- **Background `/run`** with a live progress bar, severity-colored findings, and
the full `file://` report URL on completion/stop.
- **3-way `/stop`**: **[1]** validate findings so far → report · **[2]** raw
report **now** without validating · **[3]** discard. Raw/discard abort
in-flight agents immediately (running CLI children are killed via
`kill_on_drop`); validate soft-stops so the validator still runs.
- **Crash/quit recovery**: every finding is checkpointed live to
`.neurosploit/active_run.json`; an interrupted run is recovered into `/runs`
on the next launch, so `/results`, `/finding` and `/report` keep working.
- **Pause-on-exhaustion**: when all models are rate-limited / out of quota the
run **parks** (state kept) and prints `⏸ token/quota exhausted … PAUSED`.
Resume with **`/continue`** when your quota renews, or switch with
**`/model <provider:model>`** (or the `/model` selector) then **`/continue`**.
- **Inspection**: `/results` (live findings), `/finding` (pick one → full
command + PoC + evidence), `/expand` / Ctrl-O (full untruncated commands),
`/status`, `/diff`, `/retest`.
## Usage
```bash
cd neurosploit-rs && cargo build --release
./target/release/neurosploit # interactive REPL
./target/release/neurosploit run http://target -v --model anthropic:claude-opus-4-8
./target/release/neurosploit whitebox --repo /path/to/code # white-box SAST
./target/release/neurosploit greybox --repo /path --target http://target # grey-box
./target/release/neurosploit run <ip> --creds creds.yaml # host / infra
./target/release/neurosploit tui http://target --subscription --mcp
```
Cross-platform install (Linux / macOS / Windows, x64 + arm64) via `setup.sh` and
`install.ps1`. See **README.md** and **TUTORIAL.md** for the full walkthrough.
---
# NeuroSploit v3.4.0 — Release Notes
**Release Date:** June 2026
+210
View File
@@ -0,0 +1,210 @@
# NeuroSploit — Integrations Setup Guide (v3.5.3)
Connect NeuroSploit to **GitHub**, **GitLab** and **Jira** so it can review private
repositories and Pull Requests, watch branches for new code, and file a Jira
**card per vulnerability**.
> ⚠️ **Authorized testing only.** Use integrations against code/projects you own or
> are explicitly permitted to test.
---
## Table of contents
1. [How it works (config & secrets)](#1-how-it-works)
2. [The `/integrations` command](#2-the-integrations-command)
3. [GitHub](#3-github)
4. [GitLab](#4-gitlab)
5. [Jira](#5-jira)
6. [Recipes](#6-recipes)
7. [Troubleshooting](#7-troubleshooting)
---
## 1. How it works
- Integration config is **per project**, stored at
`<cwd>/.neurosploit/integrations.json`.
- **Secrets are never written to disk.** The config only stores the **name** of
the environment variable that holds each token (e.g. `GITHUB_TOKEN`). The real
value is read from your environment at use time. Keep tokens in your shell /
secret manager, not in the repo.
- Enable/disable per integration; each is independent.
Default env-var names (configurable):
| Integration | Token env var(s) |
|-------------|------------------|
| GitHub | `GITHUB_TOKEN` |
| GitLab | `GITLAB_TOKEN` |
| Jira | `JIRA_EMAIL` + `JIRA_API_TOKEN` |
---
## 2. The `/integrations` command
In the **REPL** (`neurosploit` with no args):
```
/integrations # show status of all three
/integrations enable github # toggle on (also: gitlab | jira)
/integrations disable jira # toggle off
/integrations setup jira # interactive: base URL, project key, issue type
/integrations setup gitlab # set the GitLab base (gitlab.com or self-hosted)
/integrations setup github # set the API base (change only for GitHub Enterprise)
```
From the **CLI**:
```bash
neurosploit integrations # show status
neurosploit integrations enable github # enable / disable <github|gitlab|jira>
```
`show` prints whether each is on and whether the token env var is currently set
(`✓ token` / `⚠ token env not set`).
---
## 3. GitHub
**a. Create a token.** GitHub → *Settings → Developer settings → Personal access
tokens*. A classic PAT with the **`repo`** scope (read access to the private repos
you'll test) is enough. Fine-grained tokens also work (grant *Contents: Read* and,
for PR comments, *Pull requests: Read & write*).
**b. Export it and enable:**
```bash
export GITHUB_TOKEN=ghp_xxxxxxxxxxxxxxxxxxxx
neurosploit integrations enable github
```
**c. What you can now do:**
- **Clone & review a private repo** (token is injected into the clone URL,
never printed):
```bash
neurosploit whitebox https://github.com/myorg/private-app \
--subscription --model anthropic:claude-opus-4-8 -v
```
- **Review a Pull Request's code** — clones the PR head (`refs/pull/N/head`):
```bash
neurosploit pr myorg/private-app 128 \
--subscription --model anthropic:claude-opus-4-8 --comment
```
- `--comment` posts a Markdown findings summary back on the PR.
- `--jira` also opens a card per finding (needs Jira configured).
- **Watch a branch** and re-review on every new commit:
```bash
neurosploit watch myorg/private-app --branch main --interval 300 \
--subscription --model anthropic:claude-opus-4-8
```
It polls the branch tip via the GitHub API and runs a white-box review whenever
the SHA changes (Ctrl-C to stop).
**GitHub Enterprise:** `/integrations setup github` and set the API base to your
GHE URL (e.g. `https://ghe.mycorp.com/api/v3`).
---
## 4. GitLab
**a. Create a token.** GitLab → *Preferences → Access Tokens* (or a project/group
token) with the **`read_repository`** scope (add `api` if you want more later).
**b. Export it and enable:**
```bash
export GITLAB_TOKEN=glpat-xxxxxxxxxxxxxxxxxxxx
neurosploit integrations enable gitlab
# self-hosted? set the base:
# /integrations setup gitlab → https://gitlab.mycorp.com
```
**c. Review a private GitLab repo** (token-injected clone, works in whitebox &
greybox):
```bash
neurosploit whitebox https://gitlab.com/myorg/private-svc \
--subscription --model anthropic:claude-opus-4-8 -v
```
> To review a specific Merge Request, check out its source branch and point
> `whitebox` at that clone, or pass the MR source branch URL.
---
## 5. Jira
**a. Create an API token.** https://id.atlassian.com/manage-profile/security/api-tokens
→ *Create API token*. Note the email of the Atlassian account that owns it.
**b. Export credentials:**
```bash
export JIRA_EMAIL=you@yourorg.com
export JIRA_API_TOKEN=xxxxxxxxxxxxxxxxxxxx
```
**c. Configure base URL + project (once):**
```
# in the REPL:
/integrations setup jira
Jira base URL (https://your-org.atlassian.net): https://yourorg.atlassian.net
Jira project key (e.g. SEC): SEC
Issue type [Bug]: Bug
```
This enables Jira and saves the base URL / project key / issue type to
`.neurosploit/integrations.json` (no secrets).
**d. Open cards.** Add `--jira` to any engagement (or `pr` / `watch`). One card is
created per **validated** finding, with severity, CVSS, CWE, location, PoC,
evidence and remediation:
```bash
neurosploit whitebox https://github.com/myorg/app --jira \
--subscription --model anthropic:claude-opus-4-8 -v
```
The created issue keys are printed (e.g. `🪪 Jira cards opened: SEC-481, SEC-482`).
> Uses the Jira REST API (`POST /rest/api/2/issue`) with Basic auth
> (`JIRA_EMAIL` : `JIRA_API_TOKEN`). The `issuetype` must exist in your project
> (use `Vulnerability` if your project defines it).
---
## 6. Recipes
**PR gate in CI** (block a PR if Critical/High findings appear):
```bash
export GITHUB_TOKEN=... # CI secret
neurosploit integrations enable github
neurosploit pr "$REPO" "$PR_NUMBER" --model anthropic:claude-opus-4-8 --comment --jira
```
**Nightly drift review** of a private app, filing Jira cards:
```bash
neurosploit integrations enable github
neurosploit integrations enable jira
neurosploit watch myorg/app --branch main --interval 3600 --jira \
--model anthropic:claude-opus-4-8
```
**Local private-repo audit** (no PR), cards to Jira:
```bash
neurosploit whitebox https://github.com/myorg/app --jira \
--subscription --model anthropic:claude-opus-4-8 -v
```
---
## 7. Troubleshooting
- **`⚠ token env not set`** — the integration is enabled but the env var isn't
exported in this shell. Export it (`export GITHUB_TOKEN=...`) and re-run.
- **`git clone failed` on a private repo** — confirm the token scope (`repo` /
`read_repository`) and that the integration is enabled (`neurosploit
integrations`). The token is only injected when the matching integration is on.
- **`jira create failed: 400`** — the `issuetype` name doesn't exist in the
project, or a required field is enforced. Try `Bug`, or set your project's type
via `/integrations setup jira`.
- **`jira ... not set`** — export `JIRA_EMAIL` and `JIRA_API_TOKEN`.
- **GitHub comment fails (403/404)** — the token needs *Pull requests: write*
(fine-grained) or `repo` (classic), and you must have access to the repo.
- **Tokens in CI** — pass them as masked secrets; NeuroSploit never logs or
stores token values.
+544
View File
@@ -0,0 +1,544 @@
# NeuroSploit — Tutorial & User Guide (v3.5.4)
A complete, hands-on guide to installing, configuring and running NeuroSploit —
the autonomous, multi-model penetration-testing harness.
> ⚠️ **Authorized testing only.** Every agent is instructed to stay in scope and
> never run destructive/DoS actions. You are responsible for having written
> permission for any target you point it at.
---
## Table of contents
1. [Concepts in 60 seconds](#1-concepts-in-60-seconds)
2. [Install](#2-install)
3. [Authentication: API key vs subscription](#3-authentication-api-key-vs-subscription)
4. [Choosing models](#4-choosing-models)
5. [Engagement modes](#5-engagement-modes)
- [Black-box (URL)](#51-black-box-url)
- [White-box (source repo)](#52-white-box-source-repo)
- [Grey-box (code + live app)](#53-grey-box-code--live-app)
- [Host / Infra (Linux / Windows / AD)](#54-host--infra-linux--windows--ad)
6. [The interactive REPL](#6-the-interactive-repl)
7. [Mission Control TUI](#7-mission-control-tui)
8. [Credentials (`creds.yaml`)](#8-credentials-credsyaml)
9. [Steering the tests (focus & instructions)](#9-steering-the-tests)
10. [Outputs, reports & artifacts](#10-outputs-reports--artifacts)
11. [Per-project memory & resume](#11-per-project-memory--resume)
12. [How it decides: POMDP, grounding, chaining](#12-how-it-decides)
13. [The agent library](#13-the-agent-library)
14. [Playwright MCP & extra tools](#14-playwright-mcp--extra-tools)
15. [Tips, tuning & troubleshooting](#15-tips-tuning--troubleshooting)
16. [Command & flag reference](#16-command--flag-reference)
---
## 1. Concepts in 60 seconds
You give NeuroSploit a **target** (URL, repo, app, or host/IP). It:
1. **Recons** the target with real tools (curl/nmap/…).
2. **Intelligently selects** only the agents whose preconditions match the recon
(it does *not* blindly run all 329).
3. **Exploits** in parallel — each agent works in a ReAct loop and must prove its
claim with a **tool receipt** (raw output).
4. **Validates** every candidate by **cross-model voting** (a different model
adjudicates) and a **grounding gate** (no claim without a receipt).
5. **Chains** confirmed findings into deeper impact (SQLi→RCE→LPE, SSRF→cloud…).
6. **Reports** — HTML + Typst PDF + JSON/MD, with an attack-graph / kill-chain
mapped to OWASP / CWE / MITRE ATT&CK.
It runs on a **pool of LLMs** you choose, authenticated either by **API key** or
your local **subscription** (Claude Code / Codex / Gemini / Grok CLI).
---
## 2. Install
### One-liner
**Linux / macOS** (x64 & arm64):
```bash
curl -fsSL https://raw.githubusercontent.com/JoasASantos/NeuroSploit/main/setup.sh | bash
```
**Windows** (PowerShell, x64 & arm64):
```powershell
irm https://raw.githubusercontent.com/JoasASantos/NeuroSploit/main/install.ps1 | iex
```
The installer detects your OS/arch, installs the Rust toolchain if needed, clones
the repo, builds the release binary and puts `neurosploit` on your PATH. Re-run it
any time to update. Env knobs: `NEUROSPLOIT_REF` (branch/tag), `NEUROSPLOIT_DIR`,
`PREFIX`.
### Manual build
```bash
git clone https://github.com/JoasASantos/NeuroSploit
cd NeuroSploit/neurosploit-rs
cargo build --release # → target/release/neurosploit
```
### Recommended runtime
Run inside **Kali Linux** (or the Docker image) so the offensive tools the agents
use are already present:
```bash
docker run -it --rm kalilinux/kali-rolling
apt update && apt install -y curl nmap ffuf nodejs npm
# optional: cargo install rustscan ; cargo install typst-cli
```
Agents **degrade gracefully**: if `rustscan` is absent they use `nmap`; if neither,
`curl`. With Playwright MCP present they drive a real browser; otherwise `curl`.
### Verify
```bash
neurosploit --version # neurosploit 3.5.4
neurosploit agents # {"vulns":196,...,"chains":12,"total":329}
neurosploit models # all providers & models
```
---
## 3. Authentication: API key vs subscription
You pick **per run**. They're independent.
### A) Via API key
Export the key for each provider you'll use, then run **without** `--subscription`:
```bash
export ANTHROPIC_API_KEY=sk-ant-... # anthropic:claude-*
export OPENAI_API_KEY=sk-... # openai:gpt-*
export GEMINI_API_KEY=AIza... # gemini:gemini-*
export XAI_API_KEY=xai-... # xai:grok-*
export NVIDIA_NIM_API_KEY=nvapi-... # nvidia_nim:*
export DEEPSEEK_API_KEY=... # deepseek:*
export MISTRAL_API_KEY=... # mistral:*
export DASHSCOPE_API_KEY=... # qwen:* (Alibaba DashScope)
export GROQ_API_KEY=... # groq:*
export TOGETHER_API_KEY=... # together:*
export OPENROUTER_API_KEY=... # openrouter:*
# ollama: no key (local)
# LiteLLM proxy: point at your gateway and route any model through it:
export LITELLM_BASE_URL=http://localhost:4000/v1 # your LiteLLM proxy
export LITELLM_API_KEY=sk-... # litellm:<model the proxy routes>
neurosploit run http://testphp.vulnweb.com/ --model anthropic:claude-opus-4-8 --vote-n 3 -v
```
Or put them in a `.env` and source it (`cp .env.example .env`; edit; `set -a; . ./.env; set +a`).
In the REPL you can also run `/key anthropic sk-ant-...` (it lists which providers
your selected models need).
### B) Via subscription (no API key)
Install and log into a local agentic CLI, then pass `--subscription`:
| `--model` prefix | CLI | Login |
|------------------|-----|-------|
| `anthropic:` | Claude Code (`claude`) | `claude``/login` |
| `openai:` | Codex (`codex`) | codex login |
| `gemini:` | Gemini (`gemini`) | gemini login |
| `xai:` | Grok (`grok`) | grok login |
```bash
neurosploit run http://testphp.vulnweb.com/ --subscription --model anthropic:claude-opus-4-8 --mcp -v
```
---
## 4. Choosing models
`--model provider:model` is **repeatable**. The **first** model is the primary
(does recon & exploitation); the **rest fail over** if it errors **and** form the
**validator voting jury** (a different model adjudicates each finding → fewer false
positives).
```bash
# single model
--model anthropic:claude-opus-4-8
# voting panel (Opus finds, GPT-5.5 + Gemini-3 adjudicate)
--model anthropic:claude-opus-4-8 --model openai:gpt-5.5 --model gemini:gemini-3-pro
```
A built-in **router** sends fast/cheap models to recon & triage and the strongest
to exploitation, to save tokens. See `neurosploit models` for the full list
(Claude 4.x, GPT-5.x incl. Codex, Gemini 3/2.5, Grok, NVIDIA NIM, DeepSeek,
Mistral, Qwen, Groq, Together, OpenRouter, Ollama).
---
## 5. Engagement modes
### 5.1 Black-box (URL)
```bash
neurosploit run http://testphp.vulnweb.com/ \
--subscription --model anthropic:claude-opus-4-8 \
--focus "injection and broken access control" --mcp -v
```
### 5.2 White-box (source repo)
Reviews a **local code repository** with the 78 source-review (SAST) agents:
SQLi, command injection, SSRF, XSS, path traversal, insecure deserialization,
hardcoded secrets, weak crypto, auth/IDOR, XXE, SSTI, language-specific sinks
(PHP/Java/.NET/Go/Node/Python), and more.
```bash
# 1. clone or point at the code you own
git clone https://github.com/digininja/DVWA /tmp/DVWA
# 2. review it (subscription or --model with an API key)
neurosploit whitebox /tmp/DVWA --subscription --model anthropic:claude-opus-4-8 -v
# focus a specific class, cap agents, raise the voting bar:
neurosploit whitebox /tmp/DVWA --focus "injection and access control" \
--max-agents 8 --vote-n 2 --model openai:gpt-5.5
```
**How it works**
1. **Collects source context** — walks the repo (skips `.git/node_modules/target/
vendor`), reads supported source files into a bounded review context.
2. **Selects code agents** for the languages/frameworks it sees.
3. Each agent traces **source → sink** dataflow and must quote the **exact
vulnerable lines as `file:line`**.
4. **Grounding is symbolic**: a finding is only kept if its `file:line` / quoted
code actually exists in the reviewed source (no hallucinated locations).
5. **Validated** by cross-model voting, then reported with the code reference,
CWE/OWASP, PoC and remediation.
**Tips**
- No `--mcp` is used in white-box (there's no live app to browse).
- For huge repos, narrow with `--focus` or point at a subdirectory.
- Each finding's `endpoint` field is the `file:line`; `evidence` quotes the code;
`payload` is the PoC / vulnerable snippet — view it all with `/finding`.
### 5.3 Grey-box (code + live app)
The strongest mode: review the **source** *and* exploit the **running app**
together. Code-review findings become **leads** that the live agents confirm
against the deployed application (so a SQLi spotted in code is proven exploitable
on the running endpoint).
```bash
# code repo + the URL where that code is actually running
neurosploit greybox /tmp/DVWA --url http://localhost:8080/ \
--creds creds.yaml --focus "auth and IDOR" \
--subscription --model anthropic:claude-opus-4-8 --mcp -v
```
**How it works**
1. **Recon** the live app (`--url`).
2. **Review the source** with the code agents → produces a list of *leads*
(suspected vulns with file:line).
3. **Live exploitation** runs with those leads injected as context, so agents go
straight for the proven-in-code weaknesses and **prove them on the live app**
(empirical receipt: real request/response).
4. Validate (cross-model) → chain → report.
**Notes**
- Pass `--creds creds.yaml` so agents test **authenticated** flows (login / JWT /
cookie) — essential for IDOR/BOLA/auth findings.
- `--mcp` enables the Playwright browser for client-side proof (e.g. XSS firing).
- In the REPL: set **both** `/repo <path>` and `/target <url>` → grey-box is
auto-selected; `/show` displays `mode: greybox (code + live)`.
### 5.4 Host / Infra (Linux / Windows / AD)
Target an IP/host with SSH or Windows/AD credentials from `creds.yaml`:
```bash
neurosploit host 10.0.0.10 --creds creds.yaml \
--focus "privilege escalation and AD" --subscription --model anthropic:claude-opus-4-8 -v
```
Runs infra agents: port/service scan, SMB enum, Linux privesc/sudo/cron/SSH,
Windows privesc/SMB-signing/WinRM, and AD kerberoasting / AS-REP / ACL abuse /
DCSync / default-creds.
---
## 6. The interactive REPL
Run with **no arguments** for a persistent session:
```bash
neurosploit
```
A context bar shows `model auth · cwd · mode▸target`. Key commands:
```
/model [a:b,..] set models (no arg → arrow-key multi-select)
/key [prov key] configure API keys for your models (no arg → guided)
/sub on|off use subscription login instead of API key
/target <url> black-box target /repo <path> add a repo (repo+target = greybox)
/auth <value> send an auth header /creds <file> load creds.yaml
/focus <text> steer the tests (or just type the instruction)
@path @dir @f:1-20 attach a file/folder/line-range to context (Tab → menu)
/mcp on|off /offline on|off /votes <n> /agents <n> /theme color|mono
/run launch the engagement
/runs /results [n] /report [n] /status [n]
/diff what changed vs the previous run
/retest [n] re-verify a past run's findings
/quit
```
Line editing: **↑/↓** history, **Tab** completes commands & `@paths`, **Ctrl-A/E/K**,
end a line with **`\`** for multiline.
### Runs are non-blocking
`/run` launches the engagement **in the background** and immediately returns the
prompt — you keep typing while it streams live above the prompt. While it runs:
- **`/status`** — live phase, a **progress bar** (agents done / total), elapsed
time, token/cost and the possible findings so far.
- **`/stop`** — stop with a 3-way choice: **[1]** validate the findings found so
far, then report · **[2]** raw report **now** without validating · **[3]**
discard. Choices 2 and 3 abort in-flight agents immediately (running commands
are killed); choice 1 stops launching new agents but lets validation finish.
- Findings are color-coded by severity (Critical = red … Info = grey), and a
confirmed vote shows green ✓.
- When it finishes you get `◀ run #n done — N validated finding(s) · /results n · /report n`.
**Findings survive a crash/quit.** Every finding is checkpointed live to
`.neurosploit/active_run.json`. If the REPL is closed (or crashes) mid-run, the
next launch recovers them into `/runs` automatically (`↻ recovered interrupted
run …`), so `/results`, `/finding` and `/report` still work.
**If your tokens/quota run out, the run pauses instead of dying.** When every
candidate model is rate-limited/out of quota, the run **parks** (keeping all
state) and prints `⏸ token/quota exhausted … PAUSED`. Then either:
- wait for your quota to renew and type **`/continue`** to retry the same model, or
- switch model first — **`/model <provider:model>`** (or `/model` for the
arrow-select menu) — then **`/continue`** to resume on the new model.
(When stdin is piped/non-interactive, `/run` falls back to blocking mode.)
---
## 7. Mission Control TUI
A live dashboard with concurrent panels and a composer you can type in **while the
run streams**:
```bash
neurosploit tui http://testphp.vulnweb.com/ --subscription --model anthropic:claude-opus-4-8 --mcp
# greybox: add --repo /path/to/repo
```
- **Header**: target · mode · model · phase · elapsed · 🪙 tokens/cost · findings · ⏸
- **Activity feed** (color-coded), **Findings** panel (live), **Targets** map
- **Composer** (non-blocking): `summary` (partial summary), `pause` (graceful
stop), `errors` (filter), `clear`, or a free-text note
- **Esc / Ctrl-C** → graceful stop; the report is generated on exit
---
## 8. Credentials (`creds.yaml`)
One file covers web auth, SSH and Windows/AD. See `neurosploit-rs/creds.example.yaml`.
```yaml
# --- web auth (pick one) ---
jwt: eyJhbGciOi... # → Authorization: Bearer <jwt>
# header: "X-Api-Key: abc123"
# cookie: "session=deadbeef"
# --- OR an automated login the harness performs to capture a live session ---
login:
url: http://localhost:8080/login
method: POST
username_field: username
password_field: password
username: admin
password: password
success: Logout # text shown on a successful login
# --- Linux host (SSH) ---
ssh:
host: 10.0.0.5
port: 22
user: ubuntu
password: s3cret # or:
key: /home/op/id_ed25519
# --- Windows / Active Directory ---
windows:
host: 10.0.0.10
domain: CORP
user: jdoe
password: Winter2026! # or pass-the-hash:
hash: aad3b435b51404eeaad3b435b51404ee:NThashhere
```
- `jwt`/`header`/`cookie` are used as-is.
- A `login:` block is **executed** (real HTTP) to capture a live session
cookie/token; if it fails, agents are told to authenticate themselves.
- `ssh:` / `windows:` tell host agents how to authenticate.
Use with `--creds creds.yaml` on `run` / `greybox` / `host`, or `/creds` in the REPL.
---
## 9. Steering the tests
Tell the harness what to prioritise — it biases both agent **selection** and
**execution**:
```bash
--focus "find injection and broken access control"
```
In the REPL just type the instruction (no slash) or use `/focus`. Attach scope or a
stack trace with `@file`, `@folder`, or `@file:10-40`.
---
## 10. Outputs, reports & artifacts
Every run writes a self-contained folder `runs/ns-<ts>-<target>/`:
| File | Contents |
|------|----------|
| `status.json` | `running``complete`/`stopped` with a summary |
| `recon.json` / `recon.md` | mapped attack surface |
| `exploitation.md` | raw per-agent transcript (the receipts) |
| `findings.json` / `findings.md` | validated findings (reuse by other tools/AIs) |
| `report.html` | HTML report **+ Mermaid attack-graph / kill-chain** |
| `report.typ` / `report.pdf` | Typst source + compiled PDF (if `typst` installed) |
The CLI prints a severity summary, an ASCII kill-chain, and the token/cost total.
---
## 11. Per-project memory & resume
When you launch the REPL in a project directory, NeuroSploit creates
`<cwd>/.neurosploit/`:
```
.neurosploit/
session.json # your config (models, target, repo, auth, focus)
runs.json # run history (for /runs, /results, /report, /diff, /retest)
active_run.json # live checkpoint of an in-flight run (auto-recovered if interrupted)
history.txt # command history (↑/↓)
```
Close and reopen in the same folder → it **resumes** automatically
(`↻ resumed project session`). If a run was interrupted mid-flight, its
checkpointed findings are recovered into `/runs` (`↻ recovered interrupted run`).
No database needed — it's structured state.
---
## 12. How it decides
NeuroSploit treats the target as **partially observable** (a POMDP):
- **Belief world model** — a property graph whose nodes (host/service/vuln/
exploit/credential) carry *probabilities*, updated by observations.
- **Value-of-information** — "scan more vs exploit now" falls out of belief
entropy: when a node's belief is diffuse, recon is worth more than exploiting.
- **Anti-hallucination gate** (`may_assert`) — the agent may **not** claim
exploitability while the belief is diffuse; it must observe more first.
- **Grounding** — **no claim without a tool receipt**: empirical for black-box
(real HTTP/OOB/error output), symbolic (`file:line`) for white-box. Ungrounded
claims are demoted and flagged.
- **Chaining** — confirmed findings are chained into deeper impact, each stage
proven before advancing.
White-box collapses the POMDP toward a near-deterministic MDP (the world model is
built from SAST/dataflow), so uncertainty becomes *path reachability*, not state.
---
## 13. The agent library
`agents_md/` holds **329** markdown agents in categories:
| Category | Dir | Count | Purpose |
|----------|-----|-------|---------|
| Vulnerability specialists | `vulns/` | 196 | exploit a specific class |
| Recon | `recon/` | 12 | information gathering |
| Code (SAST) | `code/` | 78 | white-box source review |
| Infra | `infra/` | 14 | Linux / Windows / AD host testing |
| Chains | `chains/` | 12 | multi-stage exploitation chains |
| Meta | `meta/` | 17 | orchestrator, validator, scorers, reporter, RL |
Each agent is a self-contained playbook (`## User Prompt` methodology + `## System
Prompt` strict anti-false-positive rules). **Add your own** by dropping a `.md` into
the matching folder — it's picked up automatically.
---
## 14. Playwright MCP & extra tools
`--mcp` (subscription path) drives a real **Playwright** browser for JS-heavy pages
and to *prove* client-side issues (XSS firing, DOM, screenshots). It's
auto-provisioned via `npx` when available; backends that don't support MCP fall
back to `curl`. You can add more MCP servers by placing a `mcp.servers.json`
(`{ "mcpServers": { ... } }`) in the project root — they're merged into the run.
---
## 15. Tips, tuning & troubleshooting
- **No findings on a live target?** It may be unreachable from your network, or the
app is genuinely static — the harness refuses to fabricate. Check `recon.md`.
- **Quick smoke test:** `neurosploit run http://x --offline` exercises the pipeline
without calling any model.
- **Cost control:** start with `--max-agents 4 --vote-n 1`; scale up later. The
router already routes cheap models to recon.
- **Rate limits (subscription):** the harness retries with backoff and caps
parallel CLI processes; if you hit your 5-hour quota, add more models to the
panel or switch to an API key.
- **Run as root:** the harness sets `IS_SANDBOX=1` so Claude Code's autonomy works.
- **Stuck?** Ctrl-C once for a graceful stop (→ keep/discard report); twice aborts.
---
## 16. Command & flag reference
```
neurosploit # interactive REPL (resumes per project)
neurosploit run <url> # black-box
neurosploit whitebox <repo> # white-box source review
neurosploit greybox <repo> --url <app> # code + live
neurosploit host <ip> # Linux/Windows/AD (with --creds)
neurosploit tui <url> # Mission Control TUI (--repo for greybox)
neurosploit agents # library counts
neurosploit models # providers & models
neurosploit --help # full help
```
Common flags (run / greybox / host / tui):
```
--model provider:model repeatable; 1st = primary, rest = failover + voting jury
--subscription use local CLI login instead of an API key
--mcp enable Playwright MCP browser (subscription path)
--creds <file.yaml> jwt/header/cookie/login + ssh/windows credentials
--focus "<text>" steer agent selection & execution
--vote-n <n> validator votes per finding (default 3)
--max-agents <n> cap agents (0 = all matching)
--offline pipeline self-test, no model calls
-v, --verbose log each agent, recon, votes
```
---
*NeuroSploit — by Joas A Santos & Red Team Leaders. MIT licensed. Authorized testing only.*
@@ -0,0 +1,42 @@
# Default Creds → Foothold → Domain Compromise Chain Agent
## User Prompt
You are executing a multi-stage ATTACK CHAIN against **{target}**: default/weak creds → host foothold → AD escalation → domain dominance.
**Recon Context / prior findings:**
{recon_json}
**GOAL:** Chain an exposed credential into Active Directory domain compromise.
**CHAIN — advance stage by stage; each stage's output is the next stage's input. Use the ReAct loop and PROVE every stage with raw tool output before advancing:**
### Stage 1. Get the foothold
- Authenticate with the default/weak/reused credential (SSH/WinRM/SMB/web)
### Stage 2. Enumerate AD
- From the foothold, run BloodHound/netexec; map attack paths, roastable accounts, ACLs
### Stage 3. Escalate in AD
- Kerberoast/AS-REP-roast, abuse an ACL edge, or relay — recover higher-priv creds
### Stage 4. Reach domain dominance
- Demonstrate DCSync or DA-equivalent access (single test account) proving the path
### 5. Report Format
Report the chain as ONE finding (plus per-stage evidence):
```
FINDING:
- Title: Default Creds → Foothold → Domain Compromise Chain
- Severity: Critical
- CWE: CWE-798
- Endpoint: [entry point]
- Vector: [the full chain, stage by stage]
- Payload: [the key payloads/commands per stage]
- Evidence: [raw output proving EACH stage actually executed]
- Impact: Domain compromise from a single weak/default credential
- Remediation: Rotate defaults; unique strong passwords; tiered admin; monitor
- chains_from: [ids of the prerequisite findings this builds on]
```
## System Prompt
You are an exploit-chaining specialist. Only advance a stage after the PREVIOUS one is proven with a real tool receipt (raw output) — never assume a stage worked. If a stage can't be proven, stop and report the chain up to the last proven stage; do not claim the full chain. AUTHORIZED engagement; no destructive/DoS actions. Each reported stage must carry its own evidence. Credits: Joas A Santos & Red Team Leaders.
@@ -0,0 +1,42 @@
# Insecure Deserialization → RCE Chain Agent
## User Prompt
You are executing a multi-stage ATTACK CHAIN against **{target}**: untrusted deserialization → gadget chain → remote code execution.
**Recon Context / prior findings:**
{recon_json}
**GOAL:** Turn a deserialization sink into reliable code execution.
**CHAIN — advance stage by stage; each stage's output is the next stage's input. Use the ReAct loop and PROVE every stage with raw tool output before advancing:**
### Stage 1. Locate the sink
- Identify where attacker data is deserialized (cookie/param/file/RPC); fingerprint the format/library
### Stage 2. Build the gadget
- Select a working gadget chain (ysoserial/ysoserial.net/PyYAML/pickle) for the target stack
### Stage 3. Execute
- Deliver the payload to the sink
### Stage 4. Confirm
- Prove execution via OOB callback or command output with a unique marker
### 5. Report Format
Report the chain as ONE finding (plus per-stage evidence):
```
FINDING:
- Title: Insecure Deserialization → RCE Chain
- Severity: Critical
- CWE: CWE-502
- Endpoint: [entry point]
- Vector: [the full chain, stage by stage]
- Payload: [the key payloads/commands per stage]
- Evidence: [raw output proving EACH stage actually executed]
- Impact: Remote code execution via unsafe object deserialization
- Remediation: Never deserialize untrusted data; allowlist types; safe formats
- chains_from: [ids of the prerequisite findings this builds on]
```
## System Prompt
You are an exploit-chaining specialist. Only advance a stage after the PREVIOUS one is proven with a real tool receipt (raw output) — never assume a stage worked. If a stage can't be proven, stop and report the chain up to the last proven stage; do not claim the full chain. AUTHORIZED engagement; no destructive/DoS actions. Each reported stage must carry its own evidence. Credits: Joas A Santos & Red Team Leaders.
@@ -0,0 +1,42 @@
# Exposed .git/.env → Secret → RCE Chain Agent
## User Prompt
You are executing a multi-stage ATTACK CHAIN against **{target}**: exposed source/secrets → recovered credentials → authenticated RCE.
**Recon Context / prior findings:**
{recon_json}
**GOAL:** Chain leaked source/secrets into authenticated code execution.
**CHAIN — advance stage by stage; each stage's output is the next stage's input. Use the ReAct loop and PROVE every stage with raw tool output before advancing:**
### Stage 1. Recover the source/secrets
- Dump exposed `.git` (git-dumper) or read `.env`/config; extract keys/creds/tokens
### Stage 2. Validate the secrets
- Confirm a recovered credential/key is live (admin panel, cloud, DB, CI)
### Stage 3. Gain execution
- Use the access to deploy code / run a CI job / write a webshell / exec via admin feature
### Stage 4. Confirm RCE
- Prove command execution with output
### 5. Report Format
Report the chain as ONE finding (plus per-stage evidence):
```
FINDING:
- Title: Exposed .git/.env → Secret → RCE Chain
- Severity: High
- CWE: CWE-527
- Endpoint: [entry point]
- Vector: [the full chain, stage by stage]
- Payload: [the key payloads/commands per stage]
- Evidence: [raw output proving EACH stage actually executed]
- Impact: Code execution using credentials recovered from exposed source/secrets
- Remediation: Block dotfiles from web; rotate leaked secrets; vault storage
- chains_from: [ids of the prerequisite findings this builds on]
```
## System Prompt
You are an exploit-chaining specialist. Only advance a stage after the PREVIOUS one is proven with a real tool receipt (raw output) — never assume a stage worked. If a stage can't be proven, stop and report the chain up to the last proven stage; do not claim the full chain. AUTHORIZED engagement; no destructive/DoS actions. Each reported stage must carry its own evidence. Credits: Joas A Santos & Red Team Leaders.
@@ -0,0 +1,42 @@
# IDOR → Mass Account Takeover Chain Agent
## User Prompt
You are executing a multi-stage ATTACK CHAIN against **{target}**: IDOR → cross-account data → credential/role manipulation → takeover.
**Recon Context / prior findings:**
{recon_json}
**GOAL:** Chain object-level authz failure into taking over arbitrary accounts.
**CHAIN — advance stage by stage; each stage's output is the next stage's input. Use the ReAct loop and PROVE every stage with raw tool output before advancing:**
### Stage 1. Confirm the IDOR
- Access another user's object with your session, proven by their data
### Stage 2. Find a state-changing IDOR
- Locate IDOR on email/password/role/API-key endpoints
### Stage 3. Manipulate the victim account
- Change a victim's email or reset token / elevate role via the IDOR
### Stage 4. Confirm takeover
- Log in as / act as the victim; demonstrate control
### 5. Report Format
Report the chain as ONE finding (plus per-stage evidence):
```
FINDING:
- Title: IDOR → Mass Account Takeover Chain
- Severity: High
- CWE: CWE-639
- Endpoint: [entry point]
- Vector: [the full chain, stage by stage]
- Payload: [the key payloads/commands per stage]
- Evidence: [raw output proving EACH stage actually executed]
- Impact: Mass account takeover via broken object-level authorization
- Remediation: Enforce per-object ownership on every endpoint; indirect references
- chains_from: [ids of the prerequisite findings this builds on]
```
## System Prompt
You are an exploit-chaining specialist. Only advance a stage after the PREVIOUS one is proven with a real tool receipt (raw output) — never assume a stage worked. If a stage can't be proven, stop and report the chain up to the last proven stage; do not claim the full chain. AUTHORIZED engagement; no destructive/DoS actions. Each reported stage must carry its own evidence. Credits: Joas A Santos & Red Team Leaders.
@@ -0,0 +1,45 @@
# SQLi → RCE → Local PrivEsc Chain Agent
## User Prompt
You are executing a multi-stage ATTACK CHAIN against **{target}**: SQL injection → command execution → local privilege escalation.
**Recon Context / prior findings:**
{recon_json}
**GOAL:** Turn a database-layer injection into root/SYSTEM on the host.
**CHAIN — advance stage by stage; each stage's output is the next stage's input. Use the ReAct loop and PROVE every stage with raw tool output before advancing:**
### Stage 1. Exploit the SQL injection
- Confirm injection (error/boolean/time); identify DBMS and privileges
- Enumerate whether stacked queries / FILE / xp_cmdshell / INTO OUTFILE are available
### Stage 2. Pivot SQLi → RCE
- MSSQL: enable & use `xp_cmdshell`; MySQL: `INTO OUTFILE` a webshell to a known web path; PostgreSQL: `COPY ... PROGRAM`
- Confirm OS command execution with `id`/`whoami` output
### Stage 3. Establish a foothold
- Drop/upgrade to a stable shell as the web/db service user
### Stage 4. Local privilege escalation
- Enumerate SUID/sudo/cron/kernel (Linux) or token/service/unquoted-path (Windows)
- Escalate to root/SYSTEM and prove with a privileged command output
### 5. Report Format
Report the chain as ONE finding (plus per-stage evidence):
```
FINDING:
- Title: SQLi → RCE → Local PrivEsc Chain
- Severity: Critical
- CWE: CWE-89
- Endpoint: [entry point]
- Vector: [the full chain, stage by stage]
- Payload: [the key payloads/commands per stage]
- Evidence: [raw output proving EACH stage actually executed]
- Impact: Full host compromise originating from a web injection
- Remediation: Parameterize queries; least-privilege DB account; harden host; patch local vectors
- chains_from: [ids of the prerequisite findings this builds on]
```
## System Prompt
You are an exploit-chaining specialist. Only advance a stage after the PREVIOUS one is proven with a real tool receipt (raw output) — never assume a stage worked. If a stage can't be proven, stop and report the chain up to the last proven stage; do not claim the full chain. AUTHORIZED engagement; no destructive/DoS actions. Each reported stage must carry its own evidence. Credits: Joas A Santos & Red Team Leaders.
@@ -0,0 +1,45 @@
# SSRF → AWS Credential Compromise Chain Agent
## User Prompt
You are executing a multi-stage ATTACK CHAIN against **{target}**: SSRF → cloud metadata → IAM credentials → cloud account access.
**Recon Context / prior findings:**
{recon_json}
**GOAL:** Convert a server-side request forgery into valid AWS credentials and account access.
**CHAIN — advance stage by stage; each stage's output is the next stage's input. Use the ReAct loop and PROVE every stage with raw tool output before advancing:**
### Stage 1. Confirm the SSRF primitive
- Find a server-side fetch you control (url/webhook/import/pdf/image param)
- Prove it reaches an attacker-controlled / internal host
### Stage 2. Reach the metadata service
- IMDSv2: PUT `/latest/api/token` then GET with the token header; else IMDSv1 GET
- Retrieve `/latest/meta-data/iam/security-credentials/<role>`
### Stage 3. Harvest IAM credentials
- Capture AccessKeyId/SecretAccessKey/Token from the metadata response
### Stage 4. Use the credentials (in scope)
- `aws sts get-caller-identity` to confirm; enumerate permitted actions read-only
- Prove access to at least one resource the role can reach
### 5. Report Format
Report the chain as ONE finding (plus per-stage evidence):
```
FINDING:
- Title: SSRF → AWS Credential Compromise Chain
- Severity: Critical
- CWE: CWE-918
- Endpoint: [entry point]
- Vector: [the full chain, stage by stage]
- Payload: [the key payloads/commands per stage]
- Evidence: [raw output proving EACH stage actually executed]
- Impact: Cloud account compromise via stolen IAM role credentials
- Remediation: Enforce IMDSv2 hop-limit=1; egress allowlists; SSRF input validation; scoped IAM roles
- chains_from: [ids of the prerequisite findings this builds on]
```
## System Prompt
You are an exploit-chaining specialist. Only advance a stage after the PREVIOUS one is proven with a real tool receipt (raw output) — never assume a stage worked. If a stage can't be proven, stop and report the chain up to the last proven stage; do not claim the full chain. AUTHORIZED engagement; no destructive/DoS actions. Each reported stage must carry its own evidence. Credits: Joas A Santos & Red Team Leaders.
+43
View File
@@ -0,0 +1,43 @@
# SSRF → RCE Chain Agent
## User Prompt
You are executing a multi-stage ATTACK CHAIN against **{target}**: SSRF → internal service abuse → remote code execution.
**Recon Context / prior findings:**
{recon_json}
**GOAL:** Escalate an SSRF into code execution via a reachable internal service.
**CHAIN — advance stage by stage; each stage's output is the next stage's input. Use the ReAct loop and PROVE every stage with raw tool output before advancing:**
### Stage 1. Confirm SSRF + map internals
- Prove the SSRF; port-scan internal hosts through it (gopher/http)
- Identify exploitable internal services (Redis, unauth admin, CI, internal API)
### Stage 2. Weaponize the internal service
- e.g. Redis → write SSH key/cron/module; internal Jenkins/Actuator → job/exec; gopher:// to craft raw protocol payloads
### Stage 3. Achieve RCE
- Trigger command execution on the internal/back-end host
### Stage 4. Confirm
- Prove execution with an OOB callback or command output tied to a unique marker
### 5. Report Format
Report the chain as ONE finding (plus per-stage evidence):
```
FINDING:
- Title: SSRF → RCE Chain
- Severity: Critical
- CWE: CWE-918
- Endpoint: [entry point]
- Vector: [the full chain, stage by stage]
- Payload: [the key payloads/commands per stage]
- Evidence: [raw output proving EACH stage actually executed]
- Impact: Remote code execution pivoted through an internal service
- Remediation: Egress controls; authenticate internal services; SSRF allowlists
- chains_from: [ids of the prerequisite findings this builds on]
```
## System Prompt
You are an exploit-chaining specialist. Only advance a stage after the PREVIOUS one is proven with a real tool receipt (raw output) — never assume a stage worked. If a stage can't be proven, stop and report the chain up to the last proven stage; do not claim the full chain. AUTHORIZED engagement; no destructive/DoS actions. Each reported stage must carry its own evidence. Credits: Joas A Santos & Red Team Leaders.
@@ -0,0 +1,42 @@
# SSTI → RCE → Cloud Pivot Chain Agent
## User Prompt
You are executing a multi-stage ATTACK CHAIN against **{target}**: template injection → RCE → host creds → cloud/lateral movement.
**Recon Context / prior findings:**
{recon_json}
**GOAL:** Go from template injection to code execution to cloud or lateral access.
**CHAIN — advance stage by stage; each stage's output is the next stage's input. Use the ReAct loop and PROVE every stage with raw tool output before advancing:**
### Stage 1. Confirm SSTI → RCE
- Fingerprint the engine (`{{7*7}}` etc.); use the gadget to execute a command; prove with output
### Stage 2. Loot the host
- Read env/config/instance metadata for cloud creds, DB creds, tokens
### Stage 3. Pivot
- Use recovered creds against cloud APIs or adjacent internal hosts
### Stage 4. Confirm impact
- Prove access to a cloud resource or a second host with evidence
### 5. Report Format
Report the chain as ONE finding (plus per-stage evidence):
```
FINDING:
- Title: SSTI → RCE → Cloud Pivot Chain
- Severity: Critical
- CWE: CWE-1336
- Endpoint: [entry point]
- Vector: [the full chain, stage by stage]
- Payload: [the key payloads/commands per stage]
- Evidence: [raw output proving EACH stage actually executed]
- Impact: Cloud/lateral compromise originating from template injection
- Remediation: Never render user input as templates; sandbox; scope host IAM/creds
- chains_from: [ids of the prerequisite findings this builds on]
```
## System Prompt
You are an exploit-chaining specialist. Only advance a stage after the PREVIOUS one is proven with a real tool receipt (raw output) — never assume a stage worked. If a stage can't be proven, stop and report the chain up to the last proven stage; do not claim the full chain. AUTHORIZED engagement; no destructive/DoS actions. Each reported stage must carry its own evidence. Credits: Joas A Santos & Red Team Leaders.
@@ -0,0 +1,42 @@
# Subdomain Takeover → Trusted Phishing/Cookie Chain Agent
## User Prompt
You are executing a multi-stage ATTACK CHAIN against **{target}**: dangling DNS → subdomain takeover → trusted-origin abuse.
**Recon Context / prior findings:**
{recon_json}
**GOAL:** Chain a dangling record into hosting attacker content on a trusted subdomain.
**CHAIN — advance stage by stage; each stage's output is the next stage's input. Use the ReAct loop and PROVE every stage with raw tool output before advancing:**
### Stage 1. Find the dangling record
- Identify a CNAME/A pointing to an unclaimed provider resource
### Stage 2. Claim it
- Register the resource so the subdomain serves your content (benign PoC)
### Stage 3. Abuse the trust
- Show impact: wildcard-cookie capture, OAuth redirect trust, or CSP allowlist bypass
### Stage 4. Confirm
- Demonstrate the concrete trusted-origin abuse with evidence
### 5. Report Format
Report the chain as ONE finding (plus per-stage evidence):
```
FINDING:
- Title: Subdomain Takeover → Trusted Phishing/Cookie Chain
- Severity: High
- CWE: CWE-350
- Endpoint: [entry point]
- Vector: [the full chain, stage by stage]
- Payload: [the key payloads/commands per stage]
- Evidence: [raw output proving EACH stage actually executed]
- Impact: Trusted-origin abuse (cookie theft / phishing / OAuth) via a taken-over subdomain
- Remediation: Remove dangling DNS; monitor; scope cookies/CSP per-host
- chains_from: [ids of the prerequisite findings this builds on]
```
## System Prompt
You are an exploit-chaining specialist. Only advance a stage after the PREVIOUS one is proven with a real tool receipt (raw output) — never assume a stage worked. If a stage can't be proven, stop and report the chain up to the last proven stage; do not claim the full chain. AUTHORIZED engagement; no destructive/DoS actions. Each reported stage must carry its own evidence. Credits: Joas A Santos & Red Team Leaders.
@@ -0,0 +1,42 @@
# Upload → LFI → RCE → LPE Chain Agent
## User Prompt
You are executing a multi-stage ATTACK CHAIN against **{target}**: file upload + local file inclusion → log/session poisoning → RCE → privilege escalation.
**Recon Context / prior findings:**
{recon_json}
**GOAL:** Chain a benign upload and an LFI into code execution and then root.
**CHAIN — advance stage by stage; each stage's output is the next stage's input. Use the ReAct loop and PROVE every stage with raw tool output before advancing:**
### Stage 1. Confirm the LFI
- Prove local file inclusion (read /etc/passwd or app config); identify wrappers (php://, data://, zip://)
### Stage 2. Plant controllable content via upload
- Upload a file whose path/content you can later include (image with PHP, zip for zip:// , or use the LFI to read your uploaded file)
### Stage 3. LFI → RCE
- Include the planted file, or poison logs/session/`/proc/self/environ` then include it to execute code
### Stage 4. Confirm RCE then escalate
- Prove command execution; then enumerate and perform local privilege escalation to root/SYSTEM
### 5. Report Format
Report the chain as ONE finding (plus per-stage evidence):
```
FINDING:
- Title: Upload → LFI → RCE → LPE Chain
- Severity: Critical
- CWE: CWE-98
- Endpoint: [entry point]
- Vector: [the full chain, stage by stage]
- Payload: [the key payloads/commands per stage]
- Evidence: [raw output proving EACH stage actually executed]
- Impact: Host compromise from a non-executable upload chained through LFI
- Remediation: Fix LFI (allowlist includes); validate uploads; harden host
- chains_from: [ids of the prerequisite findings this builds on]
```
## System Prompt
You are an exploit-chaining specialist. Only advance a stage after the PREVIOUS one is proven with a real tool receipt (raw output) — never assume a stage worked. If a stage can't be proven, stop and report the chain up to the last proven stage; do not claim the full chain. AUTHORIZED engagement; no destructive/DoS actions. Each reported stage must carry its own evidence. Credits: Joas A Santos & Red Team Leaders.
+43
View File
@@ -0,0 +1,43 @@
# File Upload → RCE Chain Agent
## User Prompt
You are executing a multi-stage ATTACK CHAIN against **{target}**: insecure file upload → webshell → remote code execution.
**Recon Context / prior findings:**
{recon_json}
**GOAL:** Turn an unrestricted/insecure upload into code execution.
**CHAIN — advance stage by stage; each stage's output is the next stage's input. Use the ReAct loop and PROVE every stage with raw tool output before advancing:**
### Stage 1. Probe the upload
- Map accepted types/extensions, storage path, and how files are served
- Test bypasses: double extension, content-type spoof, magic-byte prefix, null byte, .htaccess/.phar
### Stage 2. Upload a payload
- Place a minimal webshell/handler in a web-served, executable location
### Stage 3. Locate & trigger
- Find the served URL of the upload; request it to execute
### Stage 4. Confirm RCE
- Run `id`/`whoami`; capture output proving execution
### 5. Report Format
Report the chain as ONE finding (plus per-stage evidence):
```
FINDING:
- Title: File Upload → RCE Chain
- Severity: Critical
- CWE: CWE-434
- Endpoint: [entry point]
- Vector: [the full chain, stage by stage]
- Payload: [the key payloads/commands per stage]
- Evidence: [raw output proving EACH stage actually executed]
- Impact: Remote code execution via uploaded executable content
- Remediation: Validate type by content; randomize names; store outside webroot; non-exec storage
- chains_from: [ids of the prerequisite findings this builds on]
```
## System Prompt
You are an exploit-chaining specialist. Only advance a stage after the PREVIOUS one is proven with a real tool receipt (raw output) — never assume a stage worked. If a stage can't be proven, stop and report the chain up to the last proven stage; do not claim the full chain. AUTHORIZED engagement; no destructive/DoS actions. Each reported stage must carry its own evidence. Credits: Joas A Santos & Red Team Leaders.
@@ -0,0 +1,42 @@
# XSS → Session/Account Takeover Chain Agent
## User Prompt
You are executing a multi-stage ATTACK CHAIN against **{target}**: stored/reflected XSS → session or token theft → account takeover.
**Recon Context / prior findings:**
{recon_json}
**GOAL:** Escalate XSS into full takeover of a victim (incl. admin) account.
**CHAIN — advance stage by stage; each stage's output is the next stage's input. Use the ReAct loop and PROVE every stage with raw tool output before advancing:**
### Stage 1. Prove execution
- Confirm the payload executes in the victim's browser context (Playwright: alert/DOM), not just reflects
### Stage 2. Steal the session
- Exfiltrate the session cookie/JWT/CSRF token to a collaborator, or perform actions in-context if HttpOnly
### Stage 3. Take over the account
- Replay the stolen session, or change email/password/MFA via in-context requests
### Stage 4. Confirm + escalate
- Prove control of the victim account; target an admin for privilege escalation
### 5. Report Format
Report the chain as ONE finding (plus per-stage evidence):
```
FINDING:
- Title: XSS → Session/Account Takeover Chain
- Severity: High
- CWE: CWE-79
- Endpoint: [entry point]
- Vector: [the full chain, stage by stage]
- Payload: [the key payloads/commands per stage]
- Evidence: [raw output proving EACH stage actually executed]
- Impact: Account takeover (incl. privileged) via client-side execution
- Remediation: Output encoding + CSP; HttpOnly/SameSite cookies; rotate tokens
- chains_from: [ids of the prerequisite findings this builds on]
```
## System Prompt
You are an exploit-chaining specialist. Only advance a stage after the PREVIOUS one is proven with a real tool receipt (raw output) — never assume a stage worked. If a stage can't be proven, stop and report the chain up to the last proven stage; do not claim the full chain. AUTHORIZED engagement; no destructive/DoS actions. Each reported stage must carry its own evidence. Credits: Joas A Santos & Red Team Leaders.
+35
View File
@@ -0,0 +1,35 @@
# AD ACL / DACL Abuse Agent
## User Prompt
You are testing **{target}** (a host/infrastructure target) for dangerous Active Directory ACLs.
**Recon Context:**
{recon_json}
Authentication/credentials, if provided, are described in the operator directives above.
**METHODOLOGY:**
### 1. Map
- Collect with bloodhound-python/SharpHound; find GenericAll/WriteDACL/ForceChangePassword edges
### 2. Confirm
- Demonstrate one safe, reversible control step (e.g. shadow-cred / targeted password reset in a lab) proving the path
### 3. Report Format
For each CONFIRMED finding:
```
FINDING:
- Title: AD ACL / DACL Abuse on [host]
- Severity: High
- CWE: CWE-269
- Endpoint: [host/service]
- Vector: [how]
- Payload: [command/PoC]
- Evidence: [raw tool output proving it]
- Impact: Domain privilege escalation
- Remediation: Tighten ACLs; tiered admin model
```
## System Prompt
You are an infrastructure pentest specialist for dangerous Active Directory ACLs. AUTHORIZED engagement. Report ONLY what you proved with raw tool output (the receipt) — never a paraphrase or assumption. If you lack access/observation to confirm, say so and gather more first. Stay in scope; never run destructive or DoS actions. Credits: Joas A Santos & Red Team Leaders.
+35
View File
@@ -0,0 +1,35 @@
# AD AS-REP Roasting Agent
## User Prompt
You are testing **{target}** (a host/infrastructure target) for accounts with Kerberos pre-auth disabled.
**Recon Context:**
{recon_json}
Authentication/credentials, if provided, are described in the operator directives above.
**METHODOLOGY:**
### 1. Enumerate
- impacket GetNPUsers / `netexec ldap {target} --asreproast out.txt` for DONT_REQ_PREAUTH accounts
### 2. Crack & confirm
- Crack the AS-REP (hashcat -m 18200); confirm a recovered password
### 3. Report Format
For each CONFIRMED finding:
```
FINDING:
- Title: AD AS-REP Roasting on [host]
- Severity: High
- CWE: CWE-522
- Endpoint: [host/service]
- Vector: [how]
- Payload: [command/PoC]
- Evidence: [raw tool output proving it]
- Impact: Account compromise
- Remediation: Require Kerberos pre-auth; strong passwords
```
## System Prompt
You are an infrastructure pentest specialist for accounts with Kerberos pre-auth disabled. AUTHORIZED engagement. Report ONLY what you proved with raw tool output (the receipt) — never a paraphrase or assumption. If you lack access/observation to confirm, say so and gather more first. Stay in scope; never run destructive or DoS actions. Credits: Joas A Santos & Red Team Leaders.
+35
View File
@@ -0,0 +1,35 @@
# AD DCSync Exposure Agent
## User Prompt
You are testing **{target}** (a host/infrastructure target) for replication rights enabling DCSync.
**Recon Context:**
{recon_json}
Authentication/credentials, if provided, are described in the operator directives above.
**METHODOLOGY:**
### 1. Check rights
- Identify principals with DS-Replication-Get-Changes(-All) via BloodHound/ACL review
### 2. Confirm
- With authorized creds, prove replication right (e.g. impacket secretsdump -just-dc-user for a single test account)
### 3. Report Format
For each CONFIRMED finding:
```
FINDING:
- Title: AD DCSync Exposure on [host]
- Severity: Critical
- CWE: CWE-269
- Endpoint: [host/service]
- Vector: [how]
- Payload: [command/PoC]
- Evidence: [raw tool output proving it]
- Impact: Full domain credential compromise
- Remediation: Remove replication rights from non-DC principals
```
## System Prompt
You are an infrastructure pentest specialist for replication rights enabling DCSync. AUTHORIZED engagement. Report ONLY what you proved with raw tool output (the receipt) — never a paraphrase or assumption. If you lack access/observation to confirm, say so and gather more first. Stay in scope; never run destructive or DoS actions. Credits: Joas A Santos & Red Team Leaders.
+35
View File
@@ -0,0 +1,35 @@
# AD/Host Default & Reused Credentials Agent
## User Prompt
You are testing **{target}** (a host/infrastructure target) for default or reused credentials across the domain.
**Recon Context:**
{recon_json}
Authentication/credentials, if provided, are described in the operator directives above.
**METHODOLOGY:**
### 1. Spray (authorized, throttled)
- With supplied account list, `netexec smb {target} -u users -p pass --continue-on-success` within ROE
### 2. Confirm
- Show a successful authentication that should not have worked (reused/default cred)
### 3. Report Format
For each CONFIRMED finding:
```
FINDING:
- Title: AD/Host Default & Reused Credentials on [host]
- Severity: High
- CWE: CWE-798
- Endpoint: [host/service]
- Vector: [how]
- Payload: [command/PoC]
- Evidence: [raw tool output proving it]
- Impact: Lateral movement, domain access
- Remediation: Rotate defaults; enforce unique strong passwords; lockout
```
## System Prompt
You are an infrastructure pentest specialist for default or reused credentials across the domain. AUTHORIZED engagement. Report ONLY what you proved with raw tool output (the receipt) — never a paraphrase or assumption. If you lack access/observation to confirm, say so and gather more first. Stay in scope; never run destructive or DoS actions. Credits: Joas A Santos & Red Team Leaders.
+35
View File
@@ -0,0 +1,35 @@
# AD Kerberoasting Agent
## User Prompt
You are testing **{target}** (a host/infrastructure target) for service accounts with crackable SPNs.
**Recon Context:**
{recon_json}
Authentication/credentials, if provided, are described in the operator directives above.
**METHODOLOGY:**
### 1. Request
- `netexec ldap {target} -u <user> -p <pass> --kerberoasting out.txt` or impacket GetUserSPNs
### 2. Crack & confirm
- Crack the TGS hash offline (hashcat -m 13100); confirm a recovered service-account password
### 3. Report Format
For each CONFIRMED finding:
```
FINDING:
- Title: AD Kerberoasting on [host]
- Severity: High
- CWE: CWE-522
- Endpoint: [host/service]
- Vector: [how]
- Payload: [command/PoC]
- Evidence: [raw tool output proving it]
- Impact: Service-account compromise, lateral movement
- Remediation: Strong/long service-account passwords; gMSA
```
## System Prompt
You are an infrastructure pentest specialist for service accounts with crackable SPNs. AUTHORIZED engagement. Report ONLY what you proved with raw tool output (the receipt) — never a paraphrase or assumption. If you lack access/observation to confirm, say so and gather more first. Stay in scope; never run destructive or DoS actions. Credits: Joas A Santos & Red Team Leaders.
@@ -0,0 +1,37 @@
# Host Port & Service Scan Agent
## User Prompt
You are testing **{target}** (a host/infrastructure target) for open ports and service/version discovery.
**Recon Context:**
{recon_json}
Authentication/credentials, if provided, are described in the operator directives above.
**METHODOLOGY:**
### 1. Scan
- `rustscan -a {target} -- -sV` if present, else `nmap -sV -sC -Pn {target}`
- Identify open TCP/UDP ports, service banners and versions
### 2. Triage
- Flag risky services (SMB, RDP, SSH, WinRM, LDAP, databases) and outdated versions
- Correlate versions to known CVEs for downstream agents
### 3. Report Format
For each CONFIRMED finding:
```
FINDING:
- Title: Host Port & Service Scan on [host]
- Severity: Info
- CWE: CWE-200
- Endpoint: [host/service]
- Vector: [how]
- Payload: [command/PoC]
- Evidence: [raw tool output proving it]
- Impact: Attack-surface mapping
- Remediation: Close/patch exposed services; restrict by firewall
```
## System Prompt
You are an infrastructure pentest specialist for open ports and service/version discovery. AUTHORIZED engagement. Report ONLY what you proved with raw tool output (the receipt) — never a paraphrase or assumption. If you lack access/observation to confirm, say so and gather more first. Stay in scope; never run destructive or DoS actions. Credits: Joas A Santos & Red Team Leaders.
+36
View File
@@ -0,0 +1,36 @@
# SMB/NetBIOS Enumeration Agent
## User Prompt
You are testing **{target}** (a host/infrastructure target) for SMB shares, sessions and misconfigurations.
**Recon Context:**
{recon_json}
Authentication/credentials, if provided, are described in the operator directives above.
**METHODOLOGY:**
### 1. Enumerate
- `netexec smb {target}` / `crackmapexec smb {target}` for hosts, signing, null sessions
- `smbclient -L //{target}/ -N` to list shares; check anonymous read/write
### 2. Assess
- Flag SMB signing disabled (relay risk), guest/anonymous access, writable shares
### 3. Report Format
For each CONFIRMED finding:
```
FINDING:
- Title: SMB/NetBIOS Enumeration on [host]
- Severity: Medium
- CWE: CWE-200
- Endpoint: [host/service]
- Vector: [how]
- Payload: [command/PoC]
- Evidence: [raw tool output proving it]
- Impact: Lateral movement, credential relay
- Remediation: Require SMB signing; disable guest; restrict shares
```
## System Prompt
You are an infrastructure pentest specialist for SMB shares, sessions and misconfigurations. AUTHORIZED engagement. Report ONLY what you proved with raw tool output (the receipt) — never a paraphrase or assumption. If you lack access/observation to confirm, say so and gather more first. Stay in scope; never run destructive or DoS actions. Credits: Joas A Santos & Red Team Leaders.
+35
View File
@@ -0,0 +1,35 @@
# Writable Cron / Service Abuse Agent
## User Prompt
You are testing **{target}** (a host/infrastructure target) for world-writable cron jobs or unit files.
**Recon Context:**
{recon_json}
Authentication/credentials, if provided, are described in the operator directives above.
**METHODOLOGY:**
### 1. Find
- Inspect /etc/cron*, systemd units, and scripts they call for writable paths
### 2. Confirm
- Plant a benign marker that the privileged job executes, proving control
### 3. Report Format
For each CONFIRMED finding:
```
FINDING:
- Title: Writable Cron / Service Abuse on [host]
- Severity: High
- CWE: CWE-732
- Endpoint: [host/service]
- Vector: [how]
- Payload: [command/PoC]
- Evidence: [raw tool output proving it]
- Impact: Privilege escalation
- Remediation: Fix permissions on jobs and their targets
```
## System Prompt
You are an infrastructure pentest specialist for world-writable cron jobs or unit files. AUTHORIZED engagement. Report ONLY what you proved with raw tool output (the receipt) — never a paraphrase or assumption. If you lack access/observation to confirm, say so and gather more first. Stay in scope; never run destructive or DoS actions. Credits: Joas A Santos & Red Team Leaders.
+36
View File
@@ -0,0 +1,36 @@
# Linux Privilege Escalation Agent
## User Prompt
You are testing **{target}** (a host/infrastructure target) for local privilege-escalation paths on a Linux host.
**Recon Context:**
{recon_json}
Authentication/credentials, if provided, are described in the operator directives above.
**METHODOLOGY:**
### 1. Enumerate (authenticated via SSH)
- Run linpeas/`sudo -l`, SUID/SGID (`find / -perm -4000`), cron, capabilities, writable PATH
- Check kernel version for known local exploits
### 2. Confirm
- Demonstrate an actual escalation to root (or a clear, reachable path) with command output
### 3. Report Format
For each CONFIRMED finding:
```
FINDING:
- Title: Linux Privilege Escalation on [host]
- Severity: High
- CWE: CWE-269
- Endpoint: [host/service]
- Vector: [how]
- Payload: [command/PoC]
- Evidence: [raw tool output proving it]
- Impact: Full host compromise
- Remediation: Patch kernel; fix sudo/SUID/cron/permission issues
```
## System Prompt
You are an infrastructure pentest specialist for local privilege-escalation paths on a Linux host. AUTHORIZED engagement. Report ONLY what you proved with raw tool output (the receipt) — never a paraphrase or assumption. If you lack access/observation to confirm, say so and gather more first. Stay in scope; never run destructive or DoS actions. Credits: Joas A Santos & Red Team Leaders.
+36
View File
@@ -0,0 +1,36 @@
# SSH Weak Authentication Agent
## User Prompt
You are testing **{target}** (a host/infrastructure target) for weak/guessable SSH credentials or misconfig.
**Recon Context:**
{recon_json}
Authentication/credentials, if provided, are described in the operator directives above.
**METHODOLOGY:**
### 1. Assess
- Check allowed auth methods; test provided creds with `ssh`/`sshpass`
- Only test supplied credentials — never brute force out of scope
### 2. Confirm
- Show authenticated shell access with the credentials, capturing the session banner
### 3. Report Format
For each CONFIRMED finding:
```
FINDING:
- Title: SSH Weak Authentication on [host]
- Severity: High
- CWE: CWE-1391
- Endpoint: [host/service]
- Vector: [how]
- Payload: [command/PoC]
- Evidence: [raw tool output proving it]
- Impact: Unauthorized host access
- Remediation: Key-only auth; strong passwords; fail2ban
```
## System Prompt
You are an infrastructure pentest specialist for weak/guessable SSH credentials or misconfig. AUTHORIZED engagement. Report ONLY what you proved with raw tool output (the receipt) — never a paraphrase or assumption. If you lack access/observation to confirm, say so and gather more first. Stay in scope; never run destructive or DoS actions. Credits: Joas A Santos & Red Team Leaders.
+35
View File
@@ -0,0 +1,35 @@
# Linux Sudo Misconfiguration Agent
## User Prompt
You are testing **{target}** (a host/infrastructure target) for exploitable sudo rules.
**Recon Context:**
{recon_json}
Authentication/credentials, if provided, are described in the operator directives above.
**METHODOLOGY:**
### 1. Enumerate
- `sudo -l`; look for NOPASSWD binaries and GTFObins-exploitable entries
### 2. Confirm
- Escalate via a permitted binary and show `id`=root output
### 3. Report Format
For each CONFIRMED finding:
```
FINDING:
- Title: Linux Sudo Misconfiguration on [host]
- Severity: High
- CWE: CWE-250
- Endpoint: [host/service]
- Vector: [how]
- Payload: [command/PoC]
- Evidence: [raw tool output proving it]
- Impact: Privilege escalation to root
- Remediation: Restrict sudo to least privilege; avoid shell-capable binaries
```
## System Prompt
You are an infrastructure pentest specialist for exploitable sudo rules. AUTHORIZED engagement. Report ONLY what you proved with raw tool output (the receipt) — never a paraphrase or assumption. If you lack access/observation to confirm, say so and gather more first. Stay in scope; never run destructive or DoS actions. Credits: Joas A Santos & Red Team Leaders.
+35
View File
@@ -0,0 +1,35 @@
# Windows Privilege Escalation Agent
## User Prompt
You are testing **{target}** (a host/infrastructure target) for local privilege escalation on a Windows host.
**Recon Context:**
{recon_json}
Authentication/credentials, if provided, are described in the operator directives above.
**METHODOLOGY:**
### 1. Enumerate (authenticated)
- Run winPEAS/`whoami /priv`; check unquoted service paths, weak service perms, AlwaysInstallElevated, token privileges (SeImpersonate)
### 2. Confirm
- Demonstrate escalation to SYSTEM/admin with command output (e.g. via a Potato technique where applicable)
### 3. Report Format
For each CONFIRMED finding:
```
FINDING:
- Title: Windows Privilege Escalation on [host]
- Severity: High
- CWE: CWE-269
- Endpoint: [host/service]
- Vector: [how]
- Payload: [command/PoC]
- Evidence: [raw tool output proving it]
- Impact: Full host compromise
- Remediation: Patch; fix service perms; remove dangerous privileges
```
## System Prompt
You are an infrastructure pentest specialist for local privilege escalation on a Windows host. AUTHORIZED engagement. Report ONLY what you proved with raw tool output (the receipt) — never a paraphrase or assumption. If you lack access/observation to confirm, say so and gather more first. Stay in scope; never run destructive or DoS actions. Credits: Joas A Santos & Red Team Leaders.
+35
View File
@@ -0,0 +1,35 @@
# SMB Signing & Relay Exposure Agent
## User Prompt
You are testing **{target}** (a host/infrastructure target) for SMB signing not required (NTLM relay risk).
**Recon Context:**
{recon_json}
Authentication/credentials, if provided, are described in the operator directives above.
**METHODOLOGY:**
### 1. Detect
- `netexec smb {target}` — note `signing:False`
### 2. Assess
- Explain the NTLM-relay exposure; confirm a coercible auth path only if in scope
### 3. Report Format
For each CONFIRMED finding:
```
FINDING:
- Title: SMB Signing & Relay Exposure on [host]
- Severity: Medium
- CWE: CWE-294
- Endpoint: [host/service]
- Vector: [how]
- Payload: [command/PoC]
- Evidence: [raw tool output proving it]
- Impact: Credential relay, lateral movement
- Remediation: Enforce SMB signing; disable NTLM where possible
```
## System Prompt
You are an infrastructure pentest specialist for SMB signing not required (NTLM relay risk). AUTHORIZED engagement. Report ONLY what you proved with raw tool output (the receipt) — never a paraphrase or assumption. If you lack access/observation to confirm, say so and gather more first. Stay in scope; never run destructive or DoS actions. Credits: Joas A Santos & Red Team Leaders.
+35
View File
@@ -0,0 +1,35 @@
# WinRM Authenticated Access Agent
## User Prompt
You are testing **{target}** (a host/infrastructure target) for remote management access via WinRM.
**Recon Context:**
{recon_json}
Authentication/credentials, if provided, are described in the operator directives above.
**METHODOLOGY:**
### 1. Connect
- `evil-winrm -i {target} -u <user> -p <pass>` (or -H <hash>) with supplied creds/hash
### 2. Confirm
- Show an authenticated remote shell and the host context (`whoami`, hostname)
### 3. Report Format
For each CONFIRMED finding:
```
FINDING:
- Title: WinRM Authenticated Access on [host]
- Severity: Medium
- CWE: CWE-287
- Endpoint: [host/service]
- Vector: [how]
- Payload: [command/PoC]
- Evidence: [raw tool output proving it]
- Impact: Remote host control
- Remediation: Restrict WinRM; strong creds; network segmentation
```
## System Prompt
You are an infrastructure pentest specialist for remote management access via WinRM. AUTHORIZED engagement. Report ONLY what you proved with raw tool output (the receipt) — never a paraphrase or assumption. If you lack access/observation to confirm, say so and gather more first. Stay in scope; never run destructive or DoS actions. Credits: Joas A Santos & Red Team Leaders.
+27
View File
@@ -0,0 +1,27 @@
# Artifact Decoder & CVE Correlator Agent
> Meta-agent (v3.5.2 doctrine). Decodes opaque tokens/paths, fingerprints the stack, and maps versions to CVEs.
## User Prompt
For **{target}**, inspect every opaque or technology-revealing artifact seen in
recon and responses:
1. **Decode** opaque tokens, IDs and URL paths (base64 / base64url / JSON /
marshal / JWT segments). A decoded value often reveals the framework or an
internal file path (e.g. a Dragonfly job `[["f","...file"]]`, a signed-URL
structure, a serialized object).
2. **Fingerprint** the stack: server, framework, language, and exact library /
gem / plugin / CMS versions (headers, asset paths, readme/changelog, error
pages, manifests).
3. **Correlate to CVEs**: map each exact version to known CVEs; prioritize
unauth RCE / SQLi / auth-bypass with a reliable, non-destructive PoC, and
attempt a safe confirmation (version/echo/OOB), never a destructive payload.
Output JSON: {decoded:[{artifact, decoded_value, implication}],
stack:[{component, version}], cves:[{component, version, cve, cvss, exploitable, poc}]}.
## System Prompt
You decode the opaque and correlate the obvious. Base64/JSON/marshal blobs and
version banners are leads, not noise — you decode them, fingerprint exact
versions, and check them against known CVEs, confirming only with a safe PoC and
a real receipt. Authorized engagement; no destructive or DoS actions. Credits: Joas A Santos and Red Team Leaders.
+30
View File
@@ -0,0 +1,30 @@
# Exploitation Depth Doctrine Agent
> Meta-agent (v3.5.2 doctrine). Turns every exposure into an exploitation attempt before it becomes a finding.
## User Prompt
You are reviewing the candidate findings and live transcript for **{target}**.
For EACH candidate that merely *exposes* something (information disclosure,
exposed service/catalog/WSDL, leaked credential or token, reachable dev/staging
host, permissive CORS, open .git), drive it one step further BEFORE it is
reported:
1. **Use what was exposed.** Call the exposed endpoint, decode the leaked
artifact, log in with the leaked credential, hit the dev host, send the
cross-origin request. Capture the real request/response.
2. **Decide honestly.** If using it proved impact → keep/raise severity with the
new evidence. If it could not be used → down-rate to a LEAD (low confidence),
never a confirmed High/Critical.
3. **Report the gap.** List any exposure you could not yet exploit, with the
exact next command to try, so the next round (or the human) can finish it.
Output JSON: {"escalations":[{id, action_taken, new_evidence, new_severity}],
"leads":[{id, why_not_proven, next_command}]}.
## System Prompt
You are a senior exploitation lead. Detection is not a finding — impact is. You
never let an info-disclosure, exposed service, leaked secret or reachable
non-prod host be reported as confirmed without an attempt to actually use it,
backed by a real tool receipt. Unproven impact is a lead, not a High. Authorized
engagement; no destructive or DoS actions. Credits: Joas A Santos and Red Team Leaders.
+25
View File
@@ -0,0 +1,25 @@
# Finding Chainer Agent
> Meta-agent (v3.5.2 doctrine). Reuses obtained access across modules and reports the chain, not the parts.
## User Prompt
Given the confirmed findings and any sessions/tokens/credentials obtained during
the engagement on **{target}**, build exploitation CHAINS:
- Reuse every session/JWT/cookie/credential from one step against ALL other
modules and hosts in scope (a captcha/login bypass that yields a token unlocks
the entire authenticated surface — use it).
- Pivot access into higher impact: IDOR/BOLA, horizontal/vertical privesc, mass
assignment, data exfiltration, account takeover.
- Combine separate weaknesses (e.g. user-enumeration + missing rate-limit =
password spraying; token-in-URL + no throttle = mass exfil).
For each chain output: {chain_id, steps:[{finding_id, action}], combined_impact,
combined_severity, evidence}. Prefer ONE well-evidenced chain over several
isolated low-severity items.
## System Prompt
You are an exploit-chaining specialist. Isolated findings understate risk; the
real story is the chain. You always try to reuse obtained access across the
whole scope and escalate to business impact, reporting the combined chain with
concrete evidence. Authorized engagement; no destructive or DoS actions. Credits: Joas A Santos and Red Team Leaders.
+30
View File
@@ -0,0 +1,30 @@
# Report Calibrator Agent
> Meta-agent (v3.5.2 doctrine). Dedups by class, calibrates severity to proven impact, demands evidence per claim.
## User Prompt
Before the final report for **{target}**, clean and calibrate the findings:
1. **Consolidate hygiene by class.** Merge repeated hygiene findings (missing
security headers, clickjacking, cookie flags, weak TLS, HSTS, version/banner
disclosure) into ONE finding per class with an affected-asset TABLE — do not
inflate the count one-per-host.
2. **Calibrate severity to PROVEN impact.** High/Critical requires demonstrated
impact with evidence. Unproven DoS/abuse, "could/may/potential" language, or a
finding with no concrete payload/PoC → cap to Low/Medium or mark
"(potential)". Recompute the CVSS vector to match the proven impact.
3. **Evidence per claim.** Every finding — and every item in the "tests
performed" log — must carry a concrete request/response receipt; flag any
claim that has none, and any contradiction between the test log and the
findings.
Output JSON: {merged:[{class, severity, assets:[...]}],
recalibrated:[{id, old_severity, new_severity, reason}],
unevidenced:[{id_or_test, missing}]}.
## System Prompt
You are a meticulous report editor. You group hygiene by class with an
asset table, calibrate every severity to demonstrated impact (no inflated
High/Critical, no padding the count with duplicates), and require a real
receipt behind every claim — including each line of the tests-performed log.
Honest, deduplicated, evidence-backed reporting only. Credits: Joas A Santos and Red Team Leaders.
+26
View File
@@ -0,0 +1,26 @@
# Token & JWT Auditor Agent
> Meta-agent (v3.5.2 doctrine). Attacks tokens: alg-confusion, none, kid/jku, signature checks, weak HS256 secrets.
## User Prompt
For any session token or JWT issued by **{target}**, run a full auth-token audit:
1. **Decode** the header/payload; note alg (HS*/RS*/none), kid, jku, exp, claims.
2. **Algorithm attacks**: try `alg:none`, RS→HS confusion (sign with the public
key as HMAC secret), and kid/jku injection. Confirm whether the server
actually verifies the signature (tamper a claim and replay).
3. **Weak secret**: for HS256, attempt to crack the signing secret offline
(wordlist/rules); a static or guessable shared secret (e.g. an `x-auth-*`
header value) is a strong lead — if cracked, forge a token for any user.
4. **Lifecycle**: test reuse after logout, expiry enforcement, and refresh-token
revocation.
Output JSON: {token_type, alg, verified:true|false,
attacks:[{name, result, evidence}], forged_token_possible:true|false}.
## System Prompt
You are a token-security specialist. Every JWT/session token gets audited for
algorithm confusion, none, kid/jku injection, real signature verification, weak
HS256 secrets, and lifecycle (logout/expiry/refresh). A forged or replayable
token is account takeover — you prove it with a real receipt. Authorized
engagement; no destructive or DoS actions. Credits: Joas A Santos and Red Team Leaders.
+4 -3
View File
@@ -9,10 +9,11 @@ You are performing reconnaissance on **{target}** to map DNS records and infrast
**METHODOLOGY:**
### 1. Records
- Enumerate A/AAAA/CNAME/MX/TXT/NS/SOA; check SPF/DMARC/DKIM
- Enumerate A/AAAA/CNAME/MX/NS/SOA/SRV/TXT
- Check DKIM/DMARC/SPF
### 2. Misconfig
- Test zone transfer (AXFR), wildcard records, dangling CNAMEs
- Test dangling CNAMEs, wildcard records, AND zone transfer (AXFR)
### 3. Relate
- Cluster shared infrastructure and providers
@@ -33,4 +34,4 @@ FINDING:
```
## System Prompt
You are a DNS-recon specialist. Report only records you actually resolved, with the query evidence.
You are a DNS recon specialist. Report only records you actually resolved, with the query evidence.
+36
View File
@@ -0,0 +1,36 @@
# App-Server Console Exposure Agent
## User Prompt
You are testing **{target}** for exposed Tomcat/JBoss/Jenkins/Actuator consoles.
**Recon Context:**
{recon_json}
**METHODOLOGY:**
### 1. Discover
- Probe `/manager/html`, `/jmx-console`, `/jenkins`, `/actuator`, `/console`, `/admin`
### 2. Assess
- Test default/weak creds (in scope); check unauth-exposed management endpoints
### 3. Confirm
- Demonstrate a management action / deploy / info-leak proving exposure (→ often RCE)
### 4. Report Format
For each CONFIRMED finding:
```
FINDING:
- Title: App-Server Console Exposure at [endpoint]
- Severity: High
- CWE: CWE-1188
- Endpoint: [full URL]
- Vector: [what/where]
- Payload: [exact payload/command]
- Evidence: [raw tool output proving it]
- Impact: Remote code execution / takeover
- Remediation: Authenticate & network-restrict consoles; remove defaults
```
## System Prompt
You are a specialist in exposed Tomcat/JBoss/Jenkins/Actuator consoles. AUTHORIZED engagement. Report ONLY what you proved with a real tool receipt (raw output) — never a paraphrase or assumption. Confirm the component/version before claiming a version-specific CVE is exploitable; if you cannot reach a working PoC, report it as a lower-confidence exposure, not a confirmed exploit. No destructive/DoS actions. Credits: Joas A Santos and Red Team Leaders.
+36
View File
@@ -0,0 +1,36 @@
# ASP.NET Debug/Trace Exposure Agent
## User Prompt
You are testing **{target}** for debug/trace enabled in production ASP.NET.
**Recon Context:**
{recon_json}
**METHODOLOGY:**
### 1. Probe
- Request `trace.axd`; send `DEBUG` verb; check `<compilation debug=...>` leakage via errors
### 2. Assess
- Harvest request/session data, stack traces, app internals from trace output
### 3. Confirm
- Show sensitive runtime data exposed
### 4. Report Format
For each CONFIRMED finding:
```
FINDING:
- Title: ASP.NET Debug/Trace Exposure at [endpoint]
- Severity: Medium
- CWE: CWE-489
- Endpoint: [full URL]
- Vector: [what/where]
- Payload: [exact payload/command]
- Evidence: [raw tool output proving it]
- Impact: Information disclosure
- Remediation: Disable debug/trace; custom errors
```
## System Prompt
You are a specialist in debug/trace enabled in production ASP.NET. AUTHORIZED engagement. Report ONLY what you proved with a real tool receipt (raw output) — never a paraphrase or assumption. Confirm the component/version before claiming a version-specific CVE is exploitable; if you cannot reach a working PoC, report it as a lower-confidence exposure, not a confirmed exploit. No destructive/DoS actions. Credits: Joas A Santos and Red Team Leaders.
+36
View File
@@ -0,0 +1,36 @@
# ASP.NET ViewState Deserialization Agent
## User Prompt
You are testing **{target}** for unprotected/known-key __VIEWSTATE deserialization.
**Recon Context:**
{recon_json}
**METHODOLOGY:**
### 1. Inspect
- Capture __VIEWSTATE; check if MAC is disabled (enableViewStateMac=false) or a known/leaked machineKey is in play
### 2. Weaponize
- With a known/guessed machineKey, craft a ysoserial.net ViewState gadget
### 3. Confirm
- Prove code execution via OOB callback or command output tied to a unique marker
### 4. Report Format
For each CONFIRMED finding:
```
FINDING:
- Title: ASP.NET ViewState Deserialization at [endpoint]
- Severity: Critical
- CWE: CWE-502
- Endpoint: [full URL]
- Vector: [what/where]
- Payload: [exact payload/command]
- Evidence: [raw tool output proving it]
- Impact: Remote code execution
- Remediation: Enable ViewState MAC; rotate machineKey; patch
```
## System Prompt
You are a specialist in unprotected/known-key __VIEWSTATE deserialization. AUTHORIZED engagement. Report ONLY what you proved with a real tool receipt (raw output) — never a paraphrase or assumption. Confirm the component/version before claiming a version-specific CVE is exploitable; if you cannot reach a working PoC, report it as a lower-confidence exposure, not a confirmed exploit. No destructive/DoS actions. Credits: Joas A Santos and Red Team Leaders.
+36
View File
@@ -0,0 +1,36 @@
# CMS Admin Panel & Default Creds Agent
## User Prompt
You are testing **{target}** for exposed CMS admin with weak/default credentials.
**Recon Context:**
{recon_json}
**METHODOLOGY:**
### 1. Locate
- Find admin (`/wp-admin`, `/administrator`, `/user/login`, `/admin`)
### 2. Test (in scope)
- Try supplied/default credentials; respect lockout/ROE — no out-of-scope brute force
### 3. Confirm
- Show authenticated admin access
### 4. Report Format
For each CONFIRMED finding:
```
FINDING:
- Title: CMS Admin Panel & Default Creds at [endpoint]
- Severity: High
- CWE: CWE-1392
- Endpoint: [full URL]
- Vector: [what/where]
- Payload: [exact payload/command]
- Evidence: [raw tool output proving it]
- Impact: Full CMS compromise
- Remediation: Remove defaults; strong creds + MFA; restrict admin
```
## System Prompt
You are a specialist in exposed CMS admin with weak/default credentials. AUTHORIZED engagement. Report ONLY what you proved with a real tool receipt (raw output) — never a paraphrase or assumption. Confirm the component/version before claiming a version-specific CVE is exploitable; if you cannot reach a working PoC, report it as a lower-confidence exposure, not a confirmed exploit. No destructive/DoS actions. Credits: Joas A Santos and Red Team Leaders.
+37
View File
@@ -0,0 +1,37 @@
# CMS Fingerprint & Version Agent
## User Prompt
You are testing **{target}** for CMS identification and version disclosure.
**Recon Context:**
{recon_json}
**METHODOLOGY:**
### 1. Identify
- Detect CMS via meta generator, paths (`/wp-`, `/sites/`, `/administrator/`), headers, favicon hash
- Run whatweb/wpscan-style detection without auth
### 2. Version
- Pin exact version from readme/changelog/asset hashes
### 3. Map
- List plugins/themes/modules and their versions for CVE correlation
### 4. Report Format
For each CONFIRMED finding:
```
FINDING:
- Title: CMS Fingerprint & Version at [endpoint]
- Severity: Info
- CWE: CWE-200
- Endpoint: [full URL]
- Vector: [what/where]
- Payload: [exact payload/command]
- Evidence: [raw tool output proving it]
- Impact: Targeted exploitation surface
- Remediation: Hide version/generator; keep components updated
```
## System Prompt
You are a specialist in CMS identification and version disclosure. AUTHORIZED engagement. Report ONLY what you proved with a real tool receipt (raw output) — never a paraphrase or assumption. Confirm the component/version before claiming a version-specific CVE is exploitable; if you cannot reach a working PoC, report it as a lower-confidence exposure, not a confirmed exploit. No destructive/DoS actions. Credits: Joas A Santos and Red Team Leaders.
+40
View File
@@ -0,0 +1,40 @@
# Known-CVE Exploitation Specialist Agent
## User Prompt
You are testing **{target}** for exploiting known CVEs for the detected stack.
**Recon Context:**
{recon_json}
**METHODOLOGY:**
### 1. Identify versions
- From recon, list each component + exact version (server, framework, CMS, plugins, libs)
### 2. Map to CVEs
- Match versions to known CVEs; prioritise unauth RCE/SQLi/auth-bypass; note CVE id + CVSS
- Prefer issues with a reliable, non-destructive PoC
### 3. Reproduce safely
- Run a benign PoC (e.g. a version/echo check or OOB callback) to confirm the CVE is actually present and exploitable — never a destructive payload
### 4. Confirm
- Report the CVE only when the PoC produced concrete proof (output/OOB); otherwise report it as 'potentially vulnerable (version match, unconfirmed)'
### 5. Report Format
For each CONFIRMED finding:
```
FINDING:
- Title: Known-CVE Exploitation Specialist at [endpoint]
- Severity: Critical
- CWE: CWE-1395
- Endpoint: [full URL]
- Vector: [what/where]
- Payload: [exact payload/command]
- Evidence: [raw tool output proving it]
- Impact: Depends on CVE — up to full compromise
- Remediation: Patch/upgrade the affected components; apply vendor advisories
```
## System Prompt
You are a specialist in exploiting known CVEs for the detected stack. AUTHORIZED engagement. Report ONLY what you proved with a real tool receipt (raw output) — never a paraphrase or assumption. Confirm the component/version before claiming a version-specific CVE is exploitable; if you cannot reach a working PoC, report it as a lower-confidence exposure, not a confirmed exploit. No destructive/DoS actions. Credits: Joas A Santos and Red Team Leaders.
+36
View File
@@ -0,0 +1,36 @@
# Drupal Security Audit Agent
## User Prompt
You are testing **{target}** for Drupal core/module weaknesses (e.g. Drupalgeddon class).
**Recon Context:**
{recon_json}
**METHODOLOGY:**
### 1. Enumerate
- Version (CHANGELOG, headers), enabled modules
### 2. Correlate CVEs
- Map to known Drupal RCE/SQLi (e.g. SA-CORE highly-critical classes)
### 3. Confirm
- Reproduce with an OOB/output proof where applicable
### 4. Report Format
For each CONFIRMED finding:
```
FINDING:
- Title: Drupal Security Audit at [endpoint]
- Severity: Critical
- CWE: CWE-1395
- Endpoint: [full URL]
- Vector: [what/where]
- Payload: [exact payload/command]
- Evidence: [raw tool output proving it]
- Impact: Remote code execution
- Remediation: Patch core/modules promptly
```
## System Prompt
You are a specialist in Drupal core/module weaknesses (e.g. Drupalgeddon class). AUTHORIZED engagement. Report ONLY what you proved with a real tool receipt (raw output) — never a paraphrase or assumption. Confirm the component/version before claiming a version-specific CVE is exploitable; if you cannot reach a working PoC, report it as a lower-confidence exposure, not a confirmed exploit. No destructive/DoS actions. Credits: Joas A Santos and Red Team Leaders.
+36
View File
@@ -0,0 +1,36 @@
# Exposed VCS / Build Artifacts Agent
## User Prompt
You are testing **{target}** for exposed .git/.svn/CI artifacts on the app host.
**Recon Context:**
{recon_json}
**METHODOLOGY:**
### 1. Probe
- Request `/.git/HEAD`, `/.svn/entries`, `/.env`, build/CI artifact paths
### 2. Recover
- Dump source (git-dumper) / read secrets
### 3. Confirm
- Show recovered source or live secret
### 4. Report Format
For each CONFIRMED finding:
```
FINDING:
- Title: Exposed VCS / Build Artifacts at [endpoint]
- Severity: High
- CWE: CWE-527
- Endpoint: [full URL]
- Vector: [what/where]
- Payload: [exact payload/command]
- Evidence: [raw tool output proving it]
- Impact: Source/secret disclosure → RCE
- Remediation: Block VCS/dotfiles from web; rotate secrets
```
## System Prompt
You are a specialist in exposed .git/.svn/CI artifacts on the app host. AUTHORIZED engagement. Report ONLY what you proved with a real tool receipt (raw output) — never a paraphrase or assumption. Confirm the component/version before claiming a version-specific CVE is exploitable; if you cannot reach a working PoC, report it as a lower-confidence exposure, not a confirmed exploit. No destructive/DoS actions. Credits: Joas A Santos and Red Team Leaders.
+36
View File
@@ -0,0 +1,36 @@
# IIS Handler/Extension Bypass Agent
## User Prompt
You are testing **{target}** for auth or filter bypass via IIS handler quirks.
**Recon Context:**
{recon_json}
**METHODOLOGY:**
### 1. Probe
- Test path/extension tricks: `;.asp`, `::$DATA`, trailing dot, `%20`, case, `/admin/.`/`..%2f`
### 2. Bypass
- Reach a protected handler/endpoint via a normalization or handler-mapping quirk
### 3. Confirm
- Show access to a resource that should be blocked
### 4. Report Format
For each CONFIRMED finding:
```
FINDING:
- Title: IIS Handler/Extension Bypass at [endpoint]
- Severity: High
- CWE: CWE-288
- Endpoint: [full URL]
- Vector: [what/where]
- Payload: [exact payload/command]
- Evidence: [raw tool output proving it]
- Impact: Auth/control bypass
- Remediation: Consistent normalization; patch; tighten ACLs
```
## System Prompt
You are a specialist in auth or filter bypass via IIS handler quirks. AUTHORIZED engagement. Report ONLY what you proved with a real tool receipt (raw output) — never a paraphrase or assumption. Confirm the component/version before claiming a version-specific CVE is exploitable; if you cannot reach a working PoC, report it as a lower-confidence exposure, not a confirmed exploit. No destructive/DoS actions. Credits: Joas A Santos and Red Team Leaders.
+37
View File
@@ -0,0 +1,37 @@
# IIS Tilde (~) Short-Name Enumeration Agent
## User Prompt
You are testing **{target}** for IIS 8.3 short-name disclosure.
**Recon Context:**
{recon_json}
**METHODOLOGY:**
### 1. Detect
- Probe `GET /*~1*/.aspx` style requests; a 404-vs-error differential reveals 8.3 short names
- Confirm IIS version from Server header
### 2. Enumerate
- Brute the short names char by char to reveal hidden files/dirs
### 3. Confirm
- Show recovered short names mapping to real sensitive files
### 4. Report Format
For each CONFIRMED finding:
```
FINDING:
- Title: IIS Tilde (~) Short-Name Enumeration at [endpoint]
- Severity: Medium
- CWE: CWE-200
- Endpoint: [full URL]
- Vector: [what/where]
- Payload: [exact payload/command]
- Evidence: [raw tool output proving it]
- Impact: Discovery of hidden files/backups/configs
- Remediation: Disable 8.3 name creation; patch IIS
```
## System Prompt
You are a specialist in IIS 8.3 short-name disclosure. AUTHORIZED engagement. Report ONLY what you proved with a real tool receipt (raw output) — never a paraphrase or assumption. Confirm the component/version before claiming a version-specific CVE is exploitable; if you cannot reach a working PoC, report it as a lower-confidence exposure, not a confirmed exploit. No destructive/DoS actions. Credits: Joas A Santos and Red Team Leaders.
+36
View File
@@ -0,0 +1,36 @@
# IIS WebDAV Misconfiguration Agent
## User Prompt
You are testing **{target}** for exposed/unsafe WebDAV on IIS.
**Recon Context:**
{recon_json}
**METHODOLOGY:**
### 1. Detect
- `OPTIONS /` — look for DAV header / PUT/MOVE/COPY allowed
### 2. Test write
- Attempt PUT of a benign file; if blocked, try `.txt`→MOVE→`.asp` trick
### 3. Confirm
- Show an uploaded file is served (and if executable → RCE)
### 4. Report Format
For each CONFIRMED finding:
```
FINDING:
- Title: IIS WebDAV Misconfiguration at [endpoint]
- Severity: High
- CWE: CWE-650
- Endpoint: [full URL]
- Vector: [what/where]
- Payload: [exact payload/command]
- Evidence: [raw tool output proving it]
- Impact: Arbitrary upload, potential RCE
- Remediation: Disable WebDAV or restrict methods/authn
```
## System Prompt
You are a specialist in exposed/unsafe WebDAV on IIS. AUTHORIZED engagement. Report ONLY what you proved with a real tool receipt (raw output) — never a paraphrase or assumption. Confirm the component/version before claiming a version-specific CVE is exploitable; if you cannot reach a working PoC, report it as a lower-confidence exposure, not a confirmed exploit. No destructive/DoS actions. Credits: Joas A Santos and Red Team Leaders.
+36
View File
@@ -0,0 +1,36 @@
# Joomla Security Audit Agent
## User Prompt
You are testing **{target}** for Joomla core/extension weaknesses.
**Recon Context:**
{recon_json}
**METHODOLOGY:**
### 1. Enumerate
- Version (`administrator/manifests/files/joomla.xml`), components/extensions + versions
### 2. Correlate CVEs
- Map to known Joomla/extension CVEs (SQLi, LFI, object injection)
### 3. Confirm
- Reproduce one with proof
### 4. Report Format
For each CONFIRMED finding:
```
FINDING:
- Title: Joomla Security Audit at [endpoint]
- Severity: High
- CWE: CWE-1395
- Endpoint: [full URL]
- Vector: [what/where]
- Payload: [exact payload/command]
- Evidence: [raw tool output proving it]
- Impact: Site takeover / data breach
- Remediation: Update core/extensions; harden admin
```
## System Prompt
You are a specialist in Joomla core/extension weaknesses. AUTHORIZED engagement. Report ONLY what you proved with a real tool receipt (raw output) — never a paraphrase or assumption. Confirm the component/version before claiming a version-specific CVE is exploitable; if you cannot reach a working PoC, report it as a lower-confidence exposure, not a confirmed exploit. No destructive/DoS actions. Credits: Joas A Santos and Red Team Leaders.
@@ -0,0 +1,36 @@
# Outdated Component CVE Specialist Agent
## User Prompt
You are testing **{target}** for outdated front-end/back-end components with known CVEs.
**Recon Context:**
{recon_json}
**METHODOLOGY:**
### 1. Inventory
- Extract JS libs (jQuery, Angular, etc.), server modules, framework versions from responses/JS/headers
### 2. Correlate
- Map each to known CVEs; flag the exploitable, reachable ones
### 3. Confirm
- Prove exploitability where a safe PoC exists; else report as version-based exposure
### 4. Report Format
For each CONFIRMED finding:
```
FINDING:
- Title: Outdated Component CVE Specialist at [endpoint]
- Severity: High
- CWE: CWE-1104
- Endpoint: [full URL]
- Vector: [what/where]
- Payload: [exact payload/command]
- Evidence: [raw tool output proving it]
- Impact: Varies — XSS/RCE/info-leak
- Remediation: Upgrade components; dependency scanning in CI
```
## System Prompt
You are a specialist in outdated front-end/back-end components with known CVEs. AUTHORIZED engagement. Report ONLY what you proved with a real tool receipt (raw output) — never a paraphrase or assumption. Confirm the component/version before claiming a version-specific CVE is exploitable; if you cannot reach a working PoC, report it as a lower-confidence exposure, not a confirmed exploit. No destructive/DoS actions. Credits: Joas A Santos and Red Team Leaders.
+36
View File
@@ -0,0 +1,36 @@
# WordPress Security Audit Agent
## User Prompt
You are testing **{target}** for WordPress core/plugin/theme weaknesses.
**Recon Context:**
{recon_json}
**METHODOLOGY:**
### 1. Enumerate
- Users (`/?author=`, REST `/wp-json/wp/v2/users`), plugins/themes + versions, `xmlrpc.php`
### 2. Correlate CVEs
- Map plugin/theme versions to known vulns (arbitrary upload, SQLi, auth bypass, LFI)
### 3. Confirm
- Reproduce one concrete issue (e.g. unauth arbitrary file upload) with proof
### 4. Report Format
For each CONFIRMED finding:
```
FINDING:
- Title: WordPress Security Audit at [endpoint]
- Severity: High
- CWE: CWE-1395
- Endpoint: [full URL]
- Vector: [what/where]
- Payload: [exact payload/command]
- Evidence: [raw tool output proving it]
- Impact: Site takeover / RCE
- Remediation: Update core/plugins/themes; harden; disable xmlrpc
```
## System Prompt
You are a specialist in WordPress core/plugin/theme weaknesses. AUTHORIZED engagement. Report ONLY what you proved with a real tool receipt (raw output) — never a paraphrase or assumption. Confirm the component/version before claiming a version-specific CVE is exploitable; if you cannot reach a working PoC, report it as a lower-confidence exposure, not a confirmed exploit. No destructive/DoS actions. Credits: Joas A Santos and Red Team Leaders.
+61
View File
@@ -0,0 +1,61 @@
# NeuroSploit installer for Windows (PowerShell) — by Joas A Santos & Red Team Leaders
#
# irm https://raw.githubusercontent.com/JoasASantos/NeuroSploit/main/install.ps1 | iex
#
# Installs the Rust toolchain if needed, clones the repo, builds the release
# binary, and adds it to your PATH. Works on x64 and arm64.
$ErrorActionPreference = "Stop"
function Say($m) { Write-Host " > $m" -ForegroundColor Magenta }
function Ok ($m) { Write-Host " + $m" -ForegroundColor Green }
function Warn($m){ Write-Host " ! $m" -ForegroundColor Yellow }
Write-Host ""
Write-Host " NeuroSploit installer (Windows) — v3.5.4" -ForegroundColor Cyan
$arch = $env:PROCESSOR_ARCHITECTURE
Say "Platform: Windows / $arch"
$dir = if ($env:NEUROSPLOIT_DIR) { $env:NEUROSPLOIT_DIR } else { Join-Path $HOME ".neurosploit-src" }
$ref = if ($env:NEUROSPLOIT_REF) { $env:NEUROSPLOIT_REF } else { "main" }
# 1) git
if (-not (Get-Command git -ErrorAction SilentlyContinue)) { throw "git is required (install Git for Windows) and re-run." }
# 2) Rust (rustup) — winget if available, else the rustup-init bootstrap
if (-not (Get-Command cargo -ErrorAction SilentlyContinue)) {
Say "Rust not found — installing rustup..."
if (Get-Command winget -ErrorAction SilentlyContinue) {
winget install -e --id Rustlang.Rustup --accept-source-agreements --accept-package-agreements
} else {
$ri = Join-Path $env:TEMP "rustup-init.exe"
Invoke-WebRequest "https://win.rustup.rs/$arch" -OutFile $ri
& $ri -y --default-toolchain stable --profile minimal
}
$env:Path = "$HOME\.cargo\bin;$env:Path"
}
Ok ("Rust: " + (cargo --version))
# 3) clone or update
if (Test-Path (Join-Path $dir ".git")) {
Say "Updating $dir..."; git -C $dir fetch --depth 1 origin $ref; git -C $dir reset --hard "origin/$ref"
} else {
Say "Cloning to $dir..."; git clone --depth 1 --branch $ref "https://github.com/JoasASantos/NeuroSploit.git" $dir
}
# 4) build
Say "Building release binary (first build downloads crates)..."
Push-Location (Join-Path $dir "neurosploit-rs"); cargo build --release; Pop-Location
$bin = Join-Path $dir "neurosploit-rs\target\release\neurosploit.exe"
if (-not (Test-Path $bin)) { throw "build did not produce $bin" }
Ok ("Built: " + (& $bin --version))
# 5) add to PATH (user)
$binDir = Split-Path $bin
$userPath = [Environment]::GetEnvironmentVariable("Path", "User")
if ($userPath -notlike "*$binDir*") {
[Environment]::SetEnvironmentVariable("Path", "$userPath;$binDir", "User")
Ok "Added $binDir to your PATH (open a new terminal)."
}
Write-Host ""
Ok "Done. Launch: neurosploit"
Write-Host " neurosploit run http://testphp.vulnweb.com/ --subscription --model anthropic:claude-opus-4-8 -v"
+307 -7
View File
@@ -11,6 +11,12 @@ dependencies = [
"memchr",
]
[[package]]
name = "allocator-api2"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
[[package]]
name = "anstream"
version = "1.0.0"
@@ -97,6 +103,21 @@ version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593"
[[package]]
name = "cassowary"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53"
[[package]]
name = "castaway"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a"
dependencies = [
"rustversion",
]
[[package]]
name = "cc"
version = "1.2.65"
@@ -180,6 +201,20 @@ version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
[[package]]
name = "compact_str"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fd622ebbb56a5b2ccb651b32b911cdeb2a9b4b11776b2473bf26a26a286244e"
dependencies = [
"castaway",
"cfg-if",
"itoa",
"rustversion",
"ryu",
"static_assertions",
]
[[package]]
name = "console"
version = "0.15.11"
@@ -193,6 +228,65 @@ dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "crossterm"
version = "0.28.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6"
dependencies = [
"bitflags",
"crossterm_winapi",
"mio",
"parking_lot",
"rustix 0.38.44",
"signal-hook",
"signal-hook-mio",
"winapi",
]
[[package]]
name = "crossterm_winapi"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b"
dependencies = [
"winapi",
]
[[package]]
name = "darling"
version = "0.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d"
dependencies = [
"darling_core",
"darling_macro",
]
[[package]]
name = "darling_core"
version = "0.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0"
dependencies = [
"ident_case",
"proc-macro2",
"quote",
"strsim",
"syn",
]
[[package]]
name = "darling_macro"
version = "0.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d"
dependencies = [
"darling_core",
"quote",
"syn",
]
[[package]]
name = "dialoguer"
version = "0.11.0"
@@ -217,6 +311,12 @@ dependencies = [
"syn",
]
[[package]]
name = "either"
version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e"
[[package]]
name = "encode_unicode"
version = "1.0.0"
@@ -229,6 +329,12 @@ version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d"
[[package]]
name = "equivalent"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "errno"
version = "0.3.14"
@@ -258,8 +364,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ce92ff622d6dadf7349484f42c93271a0d49b7cc4d466a936405bacbe10aa78"
dependencies = [
"cfg-if",
"rustix",
"windows-sys 0.52.0",
"rustix 1.1.4",
"windows-sys 0.59.0",
]
[[package]]
@@ -268,6 +374,12 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
[[package]]
name = "foldhash"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
[[package]]
name = "form_urlencoded"
version = "1.2.2"
@@ -392,6 +504,17 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "hashbrown"
version = "0.15.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
dependencies = [
"allocator-api2",
"equivalent",
"foldhash",
]
[[package]]
name = "heck"
version = "0.5.0"
@@ -587,6 +710,12 @@ dependencies = [
"zerovec",
]
[[package]]
name = "ident_case"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
[[package]]
name = "idna"
version = "1.1.0"
@@ -608,6 +737,28 @@ dependencies = [
"icu_properties",
]
[[package]]
name = "indoc"
version = "2.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706"
dependencies = [
"rustversion",
]
[[package]]
name = "instability"
version = "0.3.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5eb2d60ef19920a3a9193c3e371f726ec1dafc045dac788d0fb3704272458971"
dependencies = [
"darling",
"indoc",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "ipnet"
version = "2.12.0"
@@ -620,6 +771,15 @@ version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
[[package]]
name = "itertools"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
dependencies = [
"either",
]
[[package]]
name = "itoa"
version = "1.0.18"
@@ -643,6 +803,12 @@ version = "0.2.186"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
[[package]]
name = "linux-raw-sys"
version = "0.4.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab"
[[package]]
name = "linux-raw-sys"
version = "0.12.1"
@@ -670,6 +836,15 @@ version = "0.4.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
[[package]]
name = "lru"
version = "0.12.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38"
dependencies = [
"hashbrown",
]
[[package]]
name = "lru-slab"
version = "0.1.2"
@@ -689,19 +864,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda"
dependencies = [
"libc",
"log",
"wasi",
"windows-sys 0.61.2",
]
[[package]]
name = "neurosploit"
version = "3.5.0"
version = "3.5.4"
dependencies = [
"anyhow",
"clap",
"crossterm",
"dialoguer",
"futures",
"neurosploit-harness",
"ratatui",
"rustyline",
"serde",
"serde_json",
@@ -710,7 +888,7 @@ dependencies = [
[[package]]
name = "neurosploit-harness"
version = "3.5.0"
version = "3.5.4"
dependencies = [
"anyhow",
"futures",
@@ -778,6 +956,12 @@ dependencies = [
"windows-link",
]
[[package]]
name = "paste"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
[[package]]
name = "percent-encoding"
version = "2.3.2"
@@ -926,6 +1110,27 @@ dependencies = [
"getrandom 0.3.4",
]
[[package]]
name = "ratatui"
version = "0.28.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fdef7f9be5c0122f890d58bdf4d964349ba6a6161f705907526d891efabba57d"
dependencies = [
"bitflags",
"cassowary",
"compact_str",
"crossterm",
"instability",
"itertools",
"lru",
"paste",
"strum",
"strum_macros",
"unicode-segmentation",
"unicode-truncate",
"unicode-width 0.1.14",
]
[[package]]
name = "redox_syscall"
version = "0.5.18"
@@ -1022,6 +1227,19 @@ version = "2.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe"
[[package]]
name = "rustix"
version = "0.38.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"
dependencies = [
"bitflags",
"errno",
"libc",
"linux-raw-sys 0.4.15",
"windows-sys 0.59.0",
]
[[package]]
name = "rustix"
version = "1.1.4"
@@ -1031,7 +1249,7 @@ dependencies = [
"bitflags",
"errno",
"libc",
"linux-raw-sys",
"linux-raw-sys 0.12.1",
"windows-sys 0.61.2",
]
@@ -1186,6 +1404,27 @@ version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
[[package]]
name = "signal-hook"
version = "0.3.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2"
dependencies = [
"libc",
"signal-hook-registry",
]
[[package]]
name = "signal-hook-mio"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc"
dependencies = [
"libc",
"mio",
"signal-hook",
]
[[package]]
name = "signal-hook-registry"
version = "1.4.8"
@@ -1224,12 +1463,40 @@ version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
[[package]]
name = "static_assertions"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]]
name = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "strum"
version = "0.26.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06"
dependencies = [
"strum_macros",
]
[[package]]
name = "strum_macros"
version = "0.26.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be"
dependencies = [
"heck",
"proc-macro2",
"quote",
"rustversion",
"syn",
]
[[package]]
name = "subtle"
version = "2.6.1"
@@ -1276,8 +1543,8 @@ dependencies = [
"fastrand",
"getrandom 0.3.4",
"once_cell",
"rustix",
"windows-sys 0.52.0",
"rustix 1.1.4",
"windows-sys 0.61.2",
]
[[package]]
@@ -1465,6 +1732,17 @@ version = "1.13.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8"
[[package]]
name = "unicode-truncate"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf"
dependencies = [
"itertools",
"unicode-segmentation",
"unicode-width 0.1.14",
]
[[package]]
name = "unicode-width"
version = "0.1.14"
@@ -1625,6 +1903,22 @@ dependencies = [
"rustls-pki-types",
]
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-util"
version = "0.1.11"
@@ -1634,6 +1928,12 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows-link"
version = "0.2.1"
+1 -1
View File
@@ -3,7 +3,7 @@ members = ["crates/harness", "app"]
resolver = "2"
[workspace.package]
version = "3.5.0"
version = "3.5.4"
edition = "2021"
license = "MIT"
repository = "https://github.com/JoasASantos/NeuroSploit"
+2
View File
@@ -18,3 +18,5 @@ futures.workspace = true
clap = { version = "4", features = ["derive"] }
rustyline = "14"
dialoguer = "0.11"
ratatui = "0.28"
crossterm = "0.28"
+507 -47
View File
@@ -1,6 +1,7 @@
//! NeuroSploit v3.5.0 — interactive harness + CLI (`run` / `whitebox` / `agents` / `models`).
//! NeuroSploit v3.5.4 — interactive harness + CLI (`run` / `whitebox` / `agents` / `models`).
mod repl;
mod tui;
use clap::{Parser, Subcommand};
use harness::{agents, models::ModelRef, pool::ModelPool, types::RunConfig, RunOutput};
@@ -10,8 +11,8 @@ use std::path::{Path, PathBuf};
#[command(
name = "neurosploit",
version,
about = "NeuroSploit v3.5.0 — multi-model autonomous pentest harness",
long_about = "NeuroSploit v3.5.0 — a Rust multi-model harness that drives a pool of LLMs \
about = "NeuroSploit v3.5.4 — multi-model autonomous pentest harness",
long_about = "NeuroSploit v3.5.4 — a Rust multi-model harness that drives a pool of LLMs \
(API key or local subscription: Claude/Codex/Gemini/Grok) to autonomously test a target. \
After recon it INTELLIGENTLY selects only the agents matching the discovered surface, runs \
them in parallel, then validates every finding by cross-model voting before reporting.\n\n\
@@ -45,6 +46,9 @@ enum Cmd {
max_agents: usize,
#[arg(long, default_value_t = 3)]
vote_n: usize,
/// Attack-chaining rounds (post-exploitation pivots; 0 disables).
#[arg(long, default_value_t = 2)]
chain_depth: usize,
#[arg(long)]
offline: bool,
/// Use local agentic CLI subscription (Claude/Codex/Gemini/Grok login).
@@ -60,12 +64,17 @@ enum Cmd {
/// Free-text focus, e.g. "injection and broken access control".
#[arg(long)]
focus: Option<String>,
/// Open a Jira card per finding (needs the jira integration enabled).
#[arg(long)]
jira: bool,
/// Verbose: log each agent as it launches, recon, and votes.
#[arg(short, long)]
verbose: bool,
},
/// White-box: analyse a local repository's source code for vulnerabilities.
/// White-box: analyse a repository's source code for vulnerabilities.
Whitebox {
/// Local path, a GitHub URL (https://github.com/owner/repo[.git]) or an
/// `owner/repo` shorthand — git URLs are cloned automatically.
path: String,
#[arg(long = "model")]
models: Vec<String>,
@@ -73,16 +82,22 @@ enum Cmd {
max_agents: usize,
#[arg(long, default_value_t = 2)]
vote_n: usize,
/// Attack-chaining rounds (post-exploitation pivots; 0 disables).
#[arg(long, default_value_t = 2)]
chain_depth: usize,
#[arg(long)]
offline: bool,
#[arg(long)]
subscription: bool,
/// Open a Jira card per finding (needs the jira integration enabled).
#[arg(long)]
jira: bool,
#[arg(short, long)]
verbose: bool,
},
/// Greybox: review a repo's source AND exploit the running app together.
Greybox {
/// Path to the source repository.
/// Source repo: local path, a GitHub URL, or `owner/repo` (cloned if a URL).
repo: String,
/// URL of the running application.
#[arg(long)]
@@ -99,6 +114,9 @@ enum Cmd {
max_agents: usize,
#[arg(long, default_value_t = 3)]
vote_n: usize,
/// Attack-chaining rounds (post-exploitation pivots; 0 disables).
#[arg(long, default_value_t = 2)]
chain_depth: usize,
#[arg(long)]
offline: bool,
#[arg(long)]
@@ -108,6 +126,105 @@ enum Cmd {
#[arg(short, long)]
verbose: bool,
},
/// Mission Control TUI: concurrent panels (header/feed/findings/targets) with
/// a composer active during the run. Black-box (URL) or, with --repo, greybox.
Tui {
url: String,
#[arg(long = "model")]
models: Vec<String>,
#[arg(long)]
repo: Option<String>,
#[arg(long)]
creds: Option<String>,
#[arg(long)]
focus: Option<String>,
#[arg(long, default_value_t = 0)]
max_agents: usize,
#[arg(long, default_value_t = 3)]
vote_n: usize,
/// Attack-chaining rounds (post-exploitation pivots; 0 disables).
#[arg(long, default_value_t = 2)]
chain_depth: usize,
#[arg(long)]
subscription: bool,
#[arg(long)]
mcp: bool,
},
/// Infra/host: scan an IP/host and run Linux/Windows/AD agents. SSH/Windows
/// credentials come from --creds (creds.yaml ssh:/windows: blocks).
Host {
/// Target host or IP.
target: String,
#[arg(long = "model")]
models: Vec<String>,
/// Credentials YAML (ssh / windows / ad blocks).
#[arg(long)]
creds: Option<String>,
#[arg(long)]
focus: Option<String>,
#[arg(long, default_value_t = 0)]
max_agents: usize,
#[arg(long, default_value_t = 3)]
vote_n: usize,
/// Attack-chaining rounds (post-exploitation pivots; 0 disables).
#[arg(long, default_value_t = 2)]
chain_depth: usize,
#[arg(long)]
offline: bool,
#[arg(long)]
subscription: bool,
#[arg(short, long)]
verbose: bool,
},
/// Review a GitHub Pull Request's code (clones the PR head, white-box).
/// Optionally comments back on the PR and/or opens Jira cards per finding.
Pr {
/// `owner/repo` or a GitHub URL.
repo: String,
/// Pull request number.
number: u64,
#[arg(long = "model")]
models: Vec<String>,
#[arg(long, default_value_t = 2)]
vote_n: usize,
/// Attack-chaining rounds (post-exploitation pivots; 0 disables).
#[arg(long, default_value_t = 2)]
chain_depth: usize,
#[arg(long)]
subscription: bool,
/// Post a summary comment back on the PR (needs github integration on).
#[arg(long)]
comment: bool,
/// Open a Jira card per finding (needs jira integration on).
#[arg(long)]
jira: bool,
#[arg(short, long)]
verbose: bool,
},
/// Watch a GitHub repo branch; white-box review each time a new commit lands.
Watch {
/// `owner/repo` or a GitHub URL.
repo: String,
#[arg(long, default_value = "main")]
branch: String,
/// Poll interval in seconds.
#[arg(long, default_value_t = 300)]
interval: u64,
#[arg(long = "model")]
models: Vec<String>,
#[arg(long)]
subscription: bool,
#[arg(long)]
jira: bool,
#[arg(short, long)]
verbose: bool,
},
/// Manage integrations: `integrations [show|enable|disable] [github|gitlab|jira]`.
Integrations {
#[arg(default_value = "show")]
action: String,
name: Option<String>,
},
/// Show agent library counts.
Agents,
/// List providers and models.
@@ -156,8 +273,8 @@ async fn main() -> anyhow::Result<()> {
Cmd::Agents => {
let lib = agents::load(&base);
println!(
"{{\"vulns\":{},\"recon\":{},\"code\":{},\"meta\":{},\"total\":{}}}",
lib.vulns.len(), lib.recon.len(), lib.code.len(), lib.meta.len(), lib.total()
"{{\"vulns\":{},\"recon\":{},\"code\":{},\"infra\":{},\"chains\":{},\"meta\":{},\"total\":{}}}",
lib.vulns.len(), lib.recon.len(), lib.code.len(), lib.infra.len(), lib.chains.len(), lib.meta.len(), lib.total()
);
}
Cmd::Models => {
@@ -168,11 +285,12 @@ async fn main() -> anyhow::Result<()> {
}
}
}
Cmd::Run { url, models, max_agents, vote_n, offline, subscription, mcp, creds, focus, verbose } => {
Cmd::Run { url, models, max_agents, vote_n, chain_depth, offline, subscription, mcp, creds, focus, jira, verbose } => {
let url = if url.starts_with("http") { url } else { format!("https://{url}") };
let mut cfg = RunConfig::new(&url);
cfg.max_agents = max_agents;
cfg.vote_n = vote_n;
cfg.chain_depth = chain_depth;
cfg.offline = offline;
cfg.subscription = subscription;
cfg.verbose = verbose;
@@ -183,11 +301,15 @@ async fn main() -> anyhow::Result<()> {
apply_creds(&mut cfg, creds.as_deref()).await;
let out = run_engagement(&base, cfg, mcp, false).await?;
print_findings(&out);
let ig = harness::integrations::Integrations::load(&repl::proj_dir());
post_integrations(&ig, &url, &out, jira, false, None).await;
}
Cmd::Whitebox { path, models, max_agents, vote_n, offline, subscription, verbose } => {
Cmd::Whitebox { path, models, max_agents, vote_n, chain_depth, offline, subscription, jira, verbose } => {
let path = resolve_source(&base, &path)?; // local path OR github URL/owner/repo
let mut cfg = RunConfig::new(&path);
cfg.max_agents = max_agents;
cfg.vote_n = vote_n;
cfg.chain_depth = chain_depth;
cfg.offline = offline;
cfg.subscription = subscription;
cfg.verbose = verbose;
@@ -196,13 +318,17 @@ async fn main() -> anyhow::Result<()> {
}
let out = run_engagement(&base, cfg, false, true).await?;
print_findings(&out);
let ig = harness::integrations::Integrations::load(&repl::proj_dir());
post_integrations(&ig, &path, &out, jira, false, None).await;
}
Cmd::Greybox { repo, url, models, creds, focus, max_agents, vote_n, offline, subscription, mcp, verbose } => {
Cmd::Greybox { repo, url, models, creds, focus, max_agents, vote_n, chain_depth, offline, subscription, mcp, verbose } => {
let repo = resolve_source(&base, &repo)?; // local path OR github URL/owner/repo
let url = if url.starts_with("http") { url } else { format!("https://{url}") };
let mut cfg = RunConfig::new(&url);
cfg.repo = Some(repo);
cfg.max_agents = max_agents;
cfg.vote_n = vote_n;
cfg.chain_depth = chain_depth;
cfg.offline = offline;
cfg.subscription = subscription;
cfg.verbose = verbose;
@@ -214,10 +340,119 @@ async fn main() -> anyhow::Result<()> {
let out = run_greybox_engagement(&base, cfg, mcp).await?;
print_findings(&out);
}
Cmd::Tui { url, models, repo, creds, focus, max_agents, vote_n, chain_depth, subscription, mcp } => {
let repo = match repo { Some(r) => Some(resolve_source(&base, &r)?), None => None }; // github URL ok
let url = if url.starts_with("http") { url } else { format!("https://{url}") };
let mut cfg = RunConfig::new(&url);
cfg.max_agents = max_agents;
cfg.vote_n = vote_n;
cfg.chain_depth = chain_depth;
cfg.subscription = subscription;
cfg.instructions = focus;
cfg.repo = repo.clone();
if !models.is_empty() {
cfg.models = models;
}
apply_creds(&mut cfg, creds.as_deref()).await;
let mode = if repo.is_some() { Mode::Grey } else { Mode::Black };
tui::run(&base, cfg, mcp, mode).await?;
}
Cmd::Host { target, models, creds, focus, max_agents, vote_n, chain_depth, offline, subscription, verbose } => {
let mut cfg = RunConfig::new(&target);
cfg.max_agents = max_agents;
cfg.vote_n = vote_n;
cfg.chain_depth = chain_depth;
cfg.offline = offline;
cfg.subscription = subscription;
cfg.verbose = verbose;
cfg.instructions = focus;
if !models.is_empty() {
cfg.models = models;
}
apply_creds(&mut cfg, creds.as_deref()).await;
let out = run_mode(&base, cfg, false, Mode::Host).await?;
print_findings(&out);
}
Cmd::Pr { repo, number, models, vote_n, chain_depth, subscription, comment, jira, verbose } => {
let ig = harness::integrations::Integrations::load(&repl::proj_dir());
let owner_repo = normalize_repo(&repo);
let path = clone_pr(&base, &ig, &owner_repo, number)?;
println!(" 🔍 white-box review of {owner_repo} PR #{number}");
let mut cfg = RunConfig::new(&path);
cfg.vote_n = vote_n;
cfg.chain_depth = chain_depth;
cfg.subscription = subscription;
cfg.verbose = verbose;
cfg.instructions = Some(format!("This is the code of pull request #{number} of {owner_repo}. Focus on vulnerabilities introduced or touched by this change."));
if !models.is_empty() { cfg.models = models; }
let out = run_engagement(&base, cfg, false, true).await?;
print_findings(&out);
post_integrations(&ig, &format!("{owner_repo}#{number}"), &out, jira, comment, Some((&owner_repo, number))).await;
}
Cmd::Watch { repo, branch, interval, models, subscription, jira, verbose } => {
let ig = harness::integrations::Integrations::load(&repl::proj_dir());
let owner_repo = normalize_repo(&repo);
println!(" 👀 watching {owner_repo}@{branch} every {interval}s — Ctrl-C to stop");
let mut last = String::new();
loop {
match ig.github_latest_sha(&owner_repo, &branch).await {
Ok(sha) if sha != last => {
let short = &sha[..7.min(sha.len())];
println!("\n 🔔 {} commit {short} on {owner_repo}@{branch} — reviewing",
if last.is_empty() { "current" } else { "new" });
// fresh clone of the branch tip
let dest = base.join("repos").join(sanitize(&format!("{owner_repo}-{branch}")));
std::fs::remove_dir_all(&dest).ok();
let url = ig.authed_clone_url(&format!("https://github.com/{owner_repo}"));
if run_git(&["clone", "--depth", "1", "--branch", &branch, &url, &dest.display().to_string()]).is_ok() {
let mut cfg = RunConfig::new(&dest.display().to_string());
cfg.subscription = subscription;
cfg.verbose = verbose;
if !models.is_empty() { cfg.models = models.clone(); }
if let Ok(out) = run_engagement(&base, cfg, false, true).await {
print_findings(&out);
post_integrations(&ig, &format!("{owner_repo}@{short}"), &out, jira, false, None).await;
}
}
last = sha;
}
Ok(_) => {}
Err(e) => eprintln!(" watch: {e}"),
}
tokio::time::sleep(std::time::Duration::from_secs(interval.max(15))).await;
}
}
Cmd::Integrations { action, name } => {
let dir = repl::proj_dir();
let mut ig = harness::integrations::Integrations::load(&dir);
match action.as_str() {
"enable" | "disable" => {
let on = action == "enable";
match name.as_deref() {
Some("github") => ig.github.enabled = on,
Some("gitlab") => ig.gitlab.enabled = on,
Some("jira") => ig.jira.enabled = on,
_ => { eprintln!(" usage: integrations {action} <github|gitlab|jira>"); return Ok(()); }
}
ig.save(&dir)?;
println!(" {} {}", name.unwrap_or_default(), if on { "enabled ✓" } else { "disabled" });
}
_ => {
println!(" integrations · {}", dir.display());
for l in ig.status_lines() { println!(" {l}"); }
println!(" toggle: `neurosploit integrations enable github|gitlab|jira` · full setup in the REPL: /integrations");
}
}
}
}
Ok(())
}
// Helpers the TUI module reuses.
pub(crate) fn now_ts_pub() -> u64 { now_ts() }
pub(crate) fn sanitize_pub(s: &str) -> String { sanitize(s) }
pub(crate) fn write_status_pub(workdir: &Path, state: &str, extra: &str) { write_status(workdir, state, extra); }
/// Load a creds.yaml into the run config. Direct material (jwt/header/cookie) is
/// used as-is; a `login:` flow is EXECUTED now (real HTTP) to capture a live
/// session cookie/token. If the auto-login fails, fall back to instructing the
@@ -232,6 +467,13 @@ pub(crate) async fn apply_creds(cfg: &mut RunConfig, path: Option<&str>) {
if cfg.auth.is_none() {
cfg.auth = c.auth_header();
}
// Host credentials (SSH / Windows-AD) → tell the agents how to authenticate
// to the host so they can run on-host enumeration / privesc / AD checks.
if let Some(hi) = c.host_instruction() {
let base = cfg.instructions.clone().unwrap_or_default();
cfg.instructions = Some(format!("{hi}\n{base}"));
println!(" [*] host credentials loaded (SSH/Windows-AD)");
}
// No direct material but a login flow → perform it now.
if cfg.auth.is_none() {
if let Some(login) = &c.login {
@@ -254,7 +496,7 @@ pub(crate) async fn apply_creds(cfg: &mut RunConfig, path: Option<&str>) {
}
#[derive(Clone, Copy, PartialEq)]
pub(crate) enum Mode { Black, White, Grey }
pub(crate) enum Mode { Black, White, Grey, Host }
pub(crate) async fn run_greybox_engagement(base: &Path, cfg: RunConfig, mcp: bool) -> anyhow::Result<RunOutput> {
run_mode(base, cfg, mcp, Mode::Grey).await
@@ -265,10 +507,26 @@ pub(crate) async fn run_engagement(base: &Path, cfg: RunConfig, mcp: bool, white
run_mode(base, cfg, mcp, if whitebox { Mode::White } else { Mode::Black }).await
}
async fn run_mode(base: &Path, mut cfg: RunConfig, mcp: bool, mode: Mode) -> anyhow::Result<RunOutput> {
let lib = agents::load(base);
/// A spawned engagement: the running task, its live event stream, a cancel
/// handle, and the run's output dir. Lets callers drive it blocking (run_mode)
/// or in the background (the REPL), and finalize with `finalize_run`.
pub(crate) struct Spawned {
pub task: tokio::task::JoinHandle<RunOutput>,
pub rx: tokio::sync::mpsc::Receiver<String>,
pub cancel: std::sync::Arc<std::sync::atomic::AtomicBool>,
pub soft: std::sync::Arc<std::sync::atomic::AtomicBool>,
/// Set when the run is parked on token/quota exhaustion (awaiting /continue).
pub paused: std::sync::Arc<std::sync::atomic::AtomicBool>,
/// Wakes a parked run when the user runs /continue.
pub resume: std::sync::Arc<tokio::sync::Notify>,
/// Fallback models pushed by /continue <provider:model> before resuming.
pub fallback: std::sync::Arc<std::sync::Mutex<Vec<ModelRef>>>,
pub workdir: PathBuf,
}
// Unique, sortable run id → runs/<id>/
/// Set up + start an engagement (synchronous setup; the work runs in the task).
pub(crate) fn spawn_engagement(base: &Path, mut cfg: RunConfig, mcp: bool, mode: Mode) -> Spawned {
let lib = agents::load(base);
let run_id = format!("ns-{}-{}", now_ts(), sanitize(&cfg.target));
let workdir = base.join("runs").join(&run_id);
std::fs::create_dir_all(&workdir).ok();
@@ -276,7 +534,7 @@ async fn run_mode(base: &Path, mut cfg: RunConfig, mcp: bool, mode: Mode) -> any
cfg.rl_path = Some(base.join("data").join("rl_state_rs.json").display().to_string());
write_status(&workdir, "running", &format!("\"target\":{:?}", cfg.target));
println!(" ┌─ NeuroSploit v3.5.0 · by Joas A Santos & Red Team Leaders");
println!(" ┌─ NeuroSploit v3.5.4 · by Joas A Santos & Red Team Leaders");
println!(" │ run id : {run_id}");
println!(" │ target : {}", cfg.target);
println!(" │ models : {}", cfg.models.join(", "));
@@ -285,25 +543,19 @@ async fn run_mode(base: &Path, mut cfg: RunConfig, mcp: bool, mode: Mode) -> any
println!(" │ repo : {}", cfg.repo.clone().unwrap_or_default());
}
println!(" └─ mode : {}{}{}",
match mode { Mode::White => "white-box", Mode::Grey => "greybox", Mode::Black => "black-box" },
match mode { Mode::White => "white-box", Mode::Grey => "greybox", Mode::Host => "host/infra", Mode::Black => "black-box" },
if cfg.subscription { " · subscription" } else { " · api" },
if mcp { " · mcp" } else { "" });
// Playwright MCP: only for backends that support it; auto-provision if asked.
let mcp_config = if mcp && cfg.subscription {
let providers: Vec<String> = cfg.models.iter().map(|m| ModelRef::parse(m).provider).collect();
if providers.iter().any(|p| harness::mcp_supported(p)) {
match harness::ensure_playwright_mcp() {
Ok(()) => {
// Optional user-supplied extra MCP servers merged into the pipeline.
let extra = base.join("mcp.servers.json");
let extra_ref = if extra.is_file() { Some(extra.as_path()) } else { None };
match harness::write_mcp_config(&workdir, extra_ref) {
Ok(p) => {
if extra_ref.is_some() { println!(" [*] merged extra MCP servers from mcp.servers.json"); }
println!(" [*] Playwright MCP ready → {}", p.display());
Some(p.display().to_string())
}
Ok(p) => { println!(" [*] Playwright MCP ready → {}", p.display()); Some(p.display().to_string()) }
Err(e) => { eprintln!(" [!] MCP config failed: {e}"); None }
}
}
@@ -313,30 +565,61 @@ async fn run_mode(base: &Path, mut cfg: RunConfig, mcp: bool, mode: Mode) -> any
eprintln!(" [!] selected backend(s) don't support MCP; using built-in tools");
None
}
} else {
None
};
} else { None };
let refs: Vec<ModelRef> = cfg.models.iter().map(|s| ModelRef::parse(s)).collect();
let pool = ModelPool::with_auth(refs, cfg.concurrency, cfg.subscription, mcp_config);
let cancel = pool.cancel_handle();
let (tx, mut rx) = tokio::sync::mpsc::channel::<String>(256);
let printer = tokio::spawn(async move {
while let Some(line) = rx.recv().await {
render_line(&line);
}
});
// Run the engagement as a task so Ctrl-C can stop it gracefully (the AI's
// in-flight CLI/subprocesses are bounded; no new agents launch once cancelled).
let mut task = tokio::spawn(async move {
let out = match mode {
let soft = pool.soft_handle();
let paused = pool.pause_handle();
let resume = pool.resume_handle();
let fallback = pool.fallback_handle();
let (tx, rx) = tokio::sync::mpsc::channel::<String>(256);
let task = tokio::spawn(async move {
match mode {
Mode::White => harness::run_whitebox(cfg, &lib, &pool, tx).await,
Mode::Grey => harness::run_greybox(cfg, &lib, &pool, tx).await,
Mode::Host => harness::run_host(cfg, &lib, &pool, tx).await,
Mode::Black => harness::run(cfg, &lib, &pool, tx).await,
};
}
});
Spawned { task, rx, cancel, soft, paused, resume, fallback, workdir }
}
/// Absolute file:// URL of a run's report (PDF if present, else HTML).
pub(crate) fn report_url(workdir: &Path) -> String {
let pdf = workdir.join("report.pdf");
let f = if pdf.is_file() { pdf } else { workdir.join("report.html") };
let abs = f.canonicalize().unwrap_or(f);
format!("file://{}", abs.display())
}
/// Generate a report directly from raw (unvalidated) findings — used by the REPL
/// when the user chooses "report without validating" on /stop.
pub(crate) fn report_raw(target: &str, findings: &[harness::types::Finding], workdir: &Path) {
let mut fs = findings.to_vec();
harness::attack_graph::enrich(&mut fs);
std::fs::write(workdir.join("findings.json"), serde_json::to_string_pretty(&fs).unwrap_or_default()).ok();
let _ = harness::report::typst_report(target, &fs, workdir);
write_status(workdir, "stopped-raw", &format!("\"findings\":{}", fs.len()));
}
/// Generate the report + final status for a finished run, ensuring the workdir
/// is always recorded (even on an aborted/partial run).
pub(crate) fn finalize_run(mut out: RunOutput, workdir: &Path) -> RunOutput {
if out.workdir.is_empty() { out.workdir = workdir.display().to_string(); }
if out.target.is_empty() {
out.target = workdir.file_name().and_then(|s| s.to_str()).unwrap_or("").to_string();
}
let _ = harness::report::typst_report(&out.target, &out.findings, workdir);
write_status(workdir, "complete", &format!("\"findings\":{},\"agents_ran\":{}", out.findings.len(), out.agents_ran.len()));
out
}
async fn run_mode(base: &Path, cfg: RunConfig, mcp: bool, mode: Mode) -> anyhow::Result<RunOutput> {
let Spawned { mut task, mut rx, cancel, workdir, .. } = spawn_engagement(base, cfg, mcp, mode);
let printer = tokio::spawn(async move {
while let Some(line) = rx.recv().await { render_line(&line); }
});
let mut cancelled = false;
@@ -365,13 +648,9 @@ async fn run_mode(base: &Path, mut cfg: RunConfig, mcp: bool, mode: Mode) -> any
}
}
// Final report via Typst (PDF if the `typst` binary is present) + HTML/MD already written.
match harness::report::typst_report(&out.target, &out.findings, &workdir) {
Ok(p) => println!(" [*] report {}", p.display()),
Err(e) => eprintln!(" [!] typst report skipped: {e}"),
}
write_status(&workdir, "complete", &format!("\"findings\":{},\"agents_ran\":{}", out.findings.len(), out.agents_ran.len()));
println!(" ✓ COMPLETE — {} validated finding(s) · status: {}/status.json", out.findings.len(), workdir.display());
let out = finalize_run(out, &workdir);
println!(" ✓ COMPLETE — {} validated finding(s)", out.findings.len());
println!(" \x1b[36mreport: {}\x1b[0m", report_url(&workdir));
Ok(out)
}
@@ -408,6 +687,131 @@ fn now_ts() -> u64 {
SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0)
}
/// Resolve a source argument (white-box `path` / grey-box `--repo`) to a local
/// directory. A git URL (`https://…`, `git@…`, `ssh://…`, `*.git`) or a GitHub
/// `owner/repo` shorthand is **cloned** (shallow) into `<base>/repos/<name>` and
/// that path is returned; an existing local path is returned unchanged.
pub(crate) fn resolve_source(base: &Path, arg: &str) -> anyhow::Result<String> {
let is_url = arg.starts_with("http://") || arg.starts_with("https://")
|| arg.starts_with("git@") || arg.starts_with("ssh://") || arg.ends_with(".git");
// `owner/repo` GitHub shorthand: no scheme, exactly one slash, not a real path.
let is_shorthand = !is_url
&& !Path::new(arg).exists()
&& arg.matches('/').count() == 1
&& !arg.starts_with('.') && !arg.starts_with('/') && !arg.starts_with('~')
&& arg.chars().all(|c| c.is_ascii_alphanumeric() || "._-/".contains(c));
if !is_url && !is_shorthand {
return Ok(arg.to_string()); // already a local path
}
let url = if is_shorthand { format!("https://github.com/{arg}") } else { arg.to_string() };
let name = sanitize(url.trim_end_matches('/').trim_end_matches(".git").rsplit('/').next().unwrap_or("repo"));
let repos_dir = base.join("repos");
std::fs::create_dir_all(&repos_dir).ok();
let dest = repos_dir.join(&name);
if dest.join(".git").is_dir() {
println!(" [*] repo cache hit → {} (delete it to re-clone)", dest.display());
return Ok(dest.display().to_string());
}
// If a GitHub/GitLab integration is enabled, inject its token so PRIVATE
// repos clone without an interactive prompt (token never printed).
let ig = harness::integrations::Integrations::load(&repl::proj_dir());
let clone_url = ig.authed_clone_url(&url);
let private = clone_url != url;
println!(" [*] cloning {url}{}{}", if private { " (private, via token)" } else { "" }, dest.display());
let status = std::process::Command::new("git")
.args(["clone", "--depth", "1", &clone_url, &dest.display().to_string()])
.status()
.map_err(|e| anyhow::anyhow!("could not start `git clone` (is git installed?): {e}"))?;
if !status.success() {
std::fs::remove_dir_all(&dest).ok();
anyhow::bail!("git clone failed for {url}");
}
Ok(dest.display().to_string())
}
/// Normalize a GitHub repo reference to `owner/name`.
fn normalize_repo(s: &str) -> String {
s.trim()
.trim_end_matches('/')
.trim_end_matches(".git")
.replace("https://github.com/", "")
.replace("http://github.com/", "")
.replace("git@github.com:", "")
}
/// Run a git command, returning Ok(()) on success.
fn run_git(args: &[&str]) -> anyhow::Result<()> {
let status = std::process::Command::new("git").args(args).status()
.map_err(|e| anyhow::anyhow!("could not run git (is it installed?): {e}"))?;
if !status.success() { anyhow::bail!("git {:?} failed", args.first().unwrap_or(&"")); }
Ok(())
}
/// Clone a repo and check out a Pull Request's HEAD (`refs/pull/N/head`).
fn clone_pr(base: &Path, ig: &harness::integrations::Integrations, owner_repo: &str, number: u64) -> anyhow::Result<String> {
let dest = base.join("repos").join(sanitize(&format!("{owner_repo}-pr{number}")));
std::fs::create_dir_all(base.join("repos")).ok();
std::fs::remove_dir_all(&dest).ok(); // always fresh — PR code changes
let url = ig.authed_clone_url(&format!("https://github.com/{owner_repo}"));
let private = url.contains('@');
println!(" [*] cloning {owner_repo}{} + PR #{number} head → {}", if private { " (private)" } else { "" }, dest.display());
let d = dest.display().to_string();
run_git(&["clone", "--depth", "1", &url, &d])?;
run_git(&["-C", &d, "fetch", "--depth", "1", "origin", &format!("pull/{number}/head:pr-{number}")])?;
run_git(&["-C", &d, "checkout", &format!("pr-{number}")])?;
Ok(d)
}
/// After a run, optionally open Jira cards and/or comment on a GitHub PR.
async fn post_integrations(
ig: &harness::integrations::Integrations,
target: &str,
out: &RunOutput,
jira: bool,
comment: bool,
gh_pr: Option<(&str, u64)>,
) {
if jira && ig.jira.enabled && !out.findings.is_empty() {
let (keys, errs) = ig.jira_cards_for(target, &out.findings).await;
if !keys.is_empty() { println!(" 🪪 Jira cards opened: {}", keys.join(", ")); }
for e in errs { eprintln!(" jira: {e}"); }
}
if comment && ig.github.enabled {
if let Some((repo, number)) = gh_pr {
match ig.github_comment(repo, number, &pr_comment_body(out)).await {
Ok(()) => println!(" 💬 commented results on {repo}#{number}"),
Err(e) => eprintln!(" github comment: {e}"),
}
}
}
}
/// Markdown summary of a run, for a PR comment.
fn pr_comment_body(out: &RunOutput) -> String {
let mut by = std::collections::BTreeMap::new();
for f in &out.findings { *by.entry(f.severity.as_str()).or_insert(0) += 1; }
let chips: Vec<String> = by.iter().map(|(k, v)| format!("{k}: {v}")).collect();
let mut s = format!(
"### 🧠 NeuroSploit white-box review\n\n**{} validated finding(s)** — {}\n\n",
out.findings.len(),
if chips.is_empty() { "none".into() } else { chips.join(" · ") }
);
if out.findings.is_empty() {
s.push_str("_No vulnerabilities confirmed in the reviewed code._\n");
} else {
s.push_str("| Severity | Finding | CWE | Location |\n|---|---|---|---|\n");
for f in &out.findings {
s.push_str(&format!("| {} | {} | {} | {} |\n",
f.severity, f.title.replace('|', "\\|"), f.cwe,
f.endpoint.replace('|', "\\|")));
}
s.push_str("\n_Findings validated by multi-model voting. Authorized testing only._\n");
}
s
}
/// Blocking yes/no prompt (default yes). Used after a graceful Ctrl-C.
fn ask_yes_no(q: &str) -> bool {
use std::io::Write;
@@ -437,10 +841,12 @@ fn render_line(raw: &str) {
}
}
let (tag, rest) = match line.split_once(": ") {
Some((t, r)) if matches!(t, "exec" | "danger" | "read" | "edit" | "tool" | "net" | "ai" | "plan" | "tokens") => (t, r),
Some((t, r)) if matches!(t, "exec" | "danger" | "read" | "edit" | "tool" | "net" | "ai" | "plan" | "tokens" | "notify" | "finding") => (t, r),
_ => ("", line),
};
match tag {
"notify" => println!(" \x1b[1;36m🔔 {}\x1b[0m", rest.trim()),
"finding" => println!(" \x1b[1;33m✦ possible finding\x1b[0m {who}{}", rest.trim()),
"exec" => card(&format!("{who}⌘ command"), rest, "\x1b[33m"),
"danger" => card(&format!("{who}⚠ DANGEROUS command"), rest, "\x1b[1;31m"),
"read" => state("📄", "reading", &format!("{who}{rest}"), "\x1b[34m"),
@@ -454,6 +860,60 @@ fn render_line(raw: &str) {
}
}
/// One-line styled rendering of a stream event — used by the background REPL run
/// (via rustyline's external printer) where multi-line cards would fight the
/// prompt. Returns None for events that shouldn't clutter the background feed.
pub(crate) fn render_compact(raw: &str) -> Option<String> {
let mut line = raw.trim_end();
let mut who = String::new();
if let Some(stripped) = line.strip_prefix('@') {
if let Some((label, rest)) = stripped.split_once(' ') { who = format!("[{label}] "); line = rest; }
}
let (tag, rest) = line.split_once(": ").unwrap_or(("", line));
if tag == "finding_json" { return None; } // captured for /results & /finding, not shown
let s = match tag {
"exec" | "danger" => format!("\x1b[33m ⌘ {who}{}\x1b[0m", trunc1(rest, 110)),
"net" => format!("\x1b[36m 🌐 {who}{}\x1b[0m", trunc1(rest, 110)),
"read" => format!("\x1b[34m 📄 {who}{}\x1b[0m", rest),
"tokens" => { track_tokens(rest); return None; } // counted, shown in /status
// Candidate finding — color by severity (not all-yellow).
"finding" => {
let sev = rest.strip_prefix('[').and_then(|b| b.split_once(']')).map(|(s, _)| s).unwrap_or("");
format!(" {}{who}{}\x1b[0m", sev_color(sev), rest)
}
"notify" => format!("\x1b[1;36m 🔔 {}\x1b[0m", rest),
"ai" => return None, // skip verbose model chatter in background feed
_ => {
let low = line.to_lowercase();
if low.contains("recon complete") { "\x1b[36m 🔍 recon complete\x1b[0m".into() }
else if low.contains("selected") && low.contains("agent") { format!("\x1b[36m 🧭 {}\x1b[0m", trunc1(line, 110)) }
else if low.starts_with("vote") && low.contains("confirmed") { format!("\x1b[1;32m ✓ {}\x1b[0m", trunc1(line, 110)) }
else if low.starts_with("exploit") || low.starts_with("test ") || low.contains("launching agent") { format!("\x1b[35m 🧪 {}\x1b[0m", trunc1(line, 110)) }
else if low.starts_with("vote") { format!("\x1b[2m · {}\x1b[0m", trunc1(line, 110)) }
else if low.contains("fail") || low.contains("error") { format!("\x1b[31m ✗ {}\x1b[0m", trunc1(line, 110)) }
else { return None; }
}
};
Some(s)
}
/// ANSI color per severity — so confirmed/critical findings stand out instead of
/// everything being yellow.
fn sev_color(sev: &str) -> &'static str {
match sev.trim() {
"Critical" => "\x1b[1;31m", // bold red
"High" => "\x1b[38;5;208m", // orange
"Medium" => "\x1b[33m", // yellow
"Low" => "\x1b[36m", // cyan
_ => "\x1b[37m", // info/grey
}
}
fn trunc1(s: &str, n: usize) -> String {
let one = s.replace('\n', " ");
if one.chars().count() <= n { one } else { format!("{}", one.chars().take(n).collect::<String>()) }
}
// Running token/cost total across the engagement (shown in the summary).
static TOK_IN: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
static TOK_OUT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
+666 -61
View File
@@ -1,4 +1,4 @@
//! NeuroSploit v3.5.0 — interactive session (Claude-Code / Codex / Cursor-CLI style).
//! NeuroSploit v3.5.4 — interactive session (Claude-Code / Codex / Cursor-CLI style).
//!
//! Launched when `neurosploit` runs with no subcommand. A persistent REPL with
//! real line editing (arrow-key history recall, Ctrl-A/E/K, paste), model
@@ -6,24 +6,121 @@
//! models, target/repo/auth/instructions, run history, and reports.
use dialoguer::{theme::ColorfulTheme, MultiSelect};
use harness::{agents, types::Finding, types::RunConfig};
use harness::{agents, models::ModelRef, types::Finding, types::RunConfig};
use rustyline::completion::{Completer, Pair};
use rustyline::error::ReadlineError;
use rustyline::highlight::Highlighter;
use rustyline::hint::Hinter;
use rustyline::history::FileHistory;
use rustyline::validate::{ValidationContext, ValidationResult, Validator};
use rustyline::{Config, Context, Editor, Helper};
use rustyline::{Cmd, CompletionType, Config, Context, Editor, ExternalPrinter, Helper, KeyEvent};
use serde::{Deserialize, Serialize};
use std::io::IsTerminal;
use std::path::Path;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
use std::time::Instant;
/// Live state of a background run, updated from the engagement stream so the
/// composer can answer /status while the runner works.
struct RunLive {
target: String,
mode: &'static str,
phase: String,
started: Instant,
findings: Vec<(String, String)>, // sev, title (summary)
full: Vec<Finding>, // full candidate findings (PoC, evidence) for /finding
commands: Vec<String>, // full untruncated commands for /expand & Ctrl+O
agents: usize,
agents_done: usize,
}
impl RunLive {
/// progress fraction in [0,1] (agents completed / total selected).
fn progress(&self) -> f64 {
if self.agents == 0 { return 0.0; }
(self.agents_done as f64 / self.agents as f64).clamp(0.0, 1.0)
}
fn bar(&self, width: usize) -> String {
let filled = (self.progress() * width as f64).round() as usize;
format!("[{}{}] {}/{} ({:.0}%)",
"".repeat(filled), "".repeat(width.saturating_sub(filled)),
self.agents_done, self.agents, self.progress() * 100.0)
}
fn ingest(&mut self, line: &str) {
let low = line.to_lowercase();
if low.contains("token/quota exhausted") || low.contains("run is paused") { self.phase = "paused (quota)".into(); }
else if low.contains("resumed — retrying") { self.phase = "exploiting".into(); }
else if low.contains("recon complete") { self.phase = "recon".into(); }
else if low.contains("selected") && low.contains("agent") {
self.phase = "planning".into();
if let Some(n) = line.split_whitespace().find_map(|t| t.parse::<usize>().ok()) { self.agents = n; }
}
else if low.starts_with("exploit") || low.starts_with("test ") || low.contains("launching agent") { self.phase = "exploiting".into(); }
else if low.starts_with("vote") || low.contains("validating") { self.phase = "validating".into(); }
else if low.starts_with("chain") { self.phase = "chaining".into(); }
else if low.contains("phase complete") || low.contains("validated finding(s)") { self.phase = "complete".into(); }
// count completed agents (each emits "... via <model> → N candidate(s)")
if low.contains("candidate(s)") && (low.starts_with("exploit ") || low.starts_with("test ") || low.starts_with("analyze ") || low.starts_with("review ")) {
self.agents_done += 1;
}
if let Some(rest) = line.strip_prefix("finding: ") {
if let Some(b) = rest.strip_prefix('[') {
if let Some((sev, tail)) = b.split_once(']') {
let title = tail.trim().split(" @ ").next().unwrap_or(tail.trim());
self.findings.push((sev.to_string(), title.to_string()));
}
}
}
// Full candidate finding (with PoC/evidence) for /results & /finding.
if let Some(j) = line.strip_prefix("finding_json: ") {
if let Ok(f) = serde_json::from_str::<Finding>(j) { self.full.push(f); }
}
// Full untruncated command for /expand & Ctrl+O.
let cmd_part = line.strip_prefix('@').and_then(|s| s.split_once(' ').map(|(_, r)| r)).unwrap_or(line);
if let Some(c) = cmd_part.strip_prefix("exec: ").or_else(|| cmd_part.strip_prefix("danger: ")) {
self.commands.push(c.to_string());
if self.commands.len() > 100 { self.commands.remove(0); }
}
}
}
/// What to do when the user stops a run.
#[derive(Clone, Copy, PartialEq)]
enum StopMode { Run, Validate, Raw, Discard }
/// A run executing in the background of the REPL.
struct ActiveRun {
live: Arc<Mutex<RunLive>>,
cancel: Arc<AtomicBool>,
soft: Arc<AtomicBool>,
done: Arc<AtomicBool>,
choice: Arc<Mutex<StopMode>>,
/// Set when the run is parked on token/quota exhaustion (awaiting /continue).
paused: Arc<AtomicBool>,
/// Wakes the parked run when the user runs /continue.
resume: Arc<tokio::sync::Notify>,
/// Fallback models to try first, pushed by /continue <provider:model>.
fallback: Arc<Mutex<Vec<ModelRef>>>,
}
/// On-disk checkpoint of an in-flight run's findings/commands, written live so a
/// run survives quitting/crashing — recovered into /runs on the next launch.
#[derive(Serialize, Deserialize, Clone, Default)]
struct LiveCheckpoint {
target: String,
mode: String,
phase: String,
workdir: String,
findings: Vec<Finding>,
commands: Vec<String>,
}
/// All slash-commands, for Tab completion.
const COMMANDS: &[&str] = &[
"/help", "/show", "/config", "/providers", "/model", "/key", "/sub", "/target",
"/repo", "/auth", "/creds", "/focus", "/attach", "/context", "/mcp", "/offline",
"/votes", "/agents", "/theme", "/clear", "/run", "/runs", "/results", "/report",
"/status", "/quit",
"/votes", "/agents", "/theme", "/clear", "/run", "/stop", "/continue", "/runs", "/results", "/report",
"/status", "/diff", "/retest", "/integrations", "/quit",
];
/// rustyline helper: Tab-completes `/commands` and `@filesystem-paths`,
@@ -131,8 +228,6 @@ impl Default for Session {
}
}
const PROMPT: &str = "\x1b[35mneurosploit\x1b[0m ";
/// Line reader: full rustyline editing (Tab-complete, history, multiline) when
/// interactive, plain stdin when piped.
enum Reader {
@@ -141,13 +236,16 @@ enum Reader {
}
impl Reader {
fn new(base: &Path) -> Reader {
fn new(_base: &Path) -> Reader {
if std::io::stdin().is_terminal() {
let cfg = Config::builder().auto_add_history(false).build();
// List completion → @path shows a file/folder menu (Claude-Code-style).
let cfg = Config::builder().auto_add_history(false)
.completion_type(CompletionType::List).build();
if let Ok(mut ed) = Editor::<NsHelper, FileHistory>::with_config(cfg) {
ed.set_helper(Some(NsHelper));
let hist = base.join("data").join("repl_history.txt");
std::fs::create_dir_all(hist.parent().unwrap()).ok();
// Ctrl+O pre-fills /expand to dump the last full (untruncated) commands.
ed.bind_sequence(KeyEvent::ctrl('o'), Cmd::Insert(1, "/expand".to_string()));
let hist = proj_dir().join("history.txt");
let _ = ed.load_history(&hist);
return Reader::Rl(Box::new(ed), hist);
}
@@ -155,11 +253,21 @@ impl Reader {
Reader::Plain(std::io::stdin())
}
/// An external printer that can write *above* the prompt from another task —
/// this is what lets a background run stream live while you keep typing.
fn external_printer(&mut self) -> Option<Box<dyn ExternalPrinter + Send>> {
match self {
Reader::Rl(ed, _) => ed.create_external_printer().ok().map(|p| Box::new(p) as Box<dyn ExternalPrinter + Send>),
Reader::Plain(_) => None,
}
}
/// Returns None to exit (EOF / Ctrl-D), Some(line) otherwise. Ctrl-C cancels
/// the current line (returns an empty string) instead of exiting.
fn read(&mut self) -> Option<String> {
/// `prompt` is the dynamic context bar + prompt to show.
fn read(&mut self, prompt: &str) -> Option<String> {
match self {
Reader::Rl(ed, hist) => match ed.readline(PROMPT) {
Reader::Rl(ed, hist) => match ed.readline(prompt) {
Ok(l) => {
// Join multiline input: a trailing `\` continued the line.
let l = l.replace("\\\n", " ").replace('\n', " ");
@@ -174,7 +282,7 @@ impl Reader {
},
Reader::Plain(stdin) => {
use std::io::Write;
print!("{PROMPT}");
print!("{prompt}");
std::io::stdout().flush().ok();
let mut s = String::new();
match stdin.read_line(&mut s) {
@@ -191,7 +299,7 @@ pub async fn repl(base: &Path) -> anyhow::Result<()> {
let backends = harness::installed_cli_backends();
println!("\x1b[1m");
println!(" ███╗ ██╗███████╗██╗ ██╗██████╗ ██████╗");
println!(" ████╗ ██║██╔════╝██║ ██║██╔══██╗██╔═══██╗ NeuroSploit v3.5.0");
println!(" ████╗ ██║██╔════╝██║ ██║██╔══██╗██╔═══██╗ NeuroSploit v3.5.4");
println!(" ██╔██╗ ██║█████╗ ██║ ██║██████╔╝██║ ██║ interactive harness");
println!(" ██║╚██╗██║██╔══╝ ██║ ██║██╔══██╗██║ ██║ by Joas A Santos");
println!(" ██║ ╚████║███████╗╚██████╔╝██║ ██║╚██████╔╝ & Red Team Leaders");
@@ -201,15 +309,37 @@ pub async fn repl(base: &Path) -> anyhow::Result<()> {
println!(" Type \x1b[36m/help\x1b[0m to start, \x1b[36m/run\x1b[0m to launch, \x1b[36m/quit\x1b[0m to exit. (↑/↓ recalls commands)\n");
let mut s = Session::default();
let mut history: Vec<RunRecord> = load_runs(base);
if !history.is_empty() {
println!(" loaded {} past run(s) — /runs to list\n", history.len());
let resumed = load_session(&mut s);
// Shared so a background run's forwarder task can append to it.
let history: Arc<Mutex<Vec<RunRecord>>> = Arc::new(Mutex::new(load_runs(base)));
let past = history.lock().unwrap().len();
if resumed || past > 0 {
println!(" ↻ resumed project session from {}{} past run(s)", proj_dir().display(), past);
}
// Recover an interrupted run (REPL was quit/crashed mid-engagement): its
// live findings were checkpointed to disk — fold them into /runs so
// /results, /finding and /report still work.
if let Some(cp) = load_checkpoint() {
if !cp.findings.is_empty() {
let wd = std::path::PathBuf::from(&cp.workdir);
std::fs::create_dir_all(&wd).ok();
crate::report_raw(&cp.target, &cp.findings, &wd); // materialize a report so /report works
let mut h = history.lock().unwrap();
let id = h.len() + 1;
h.push(RunRecord { id, mode: cp.mode.clone(), target: cp.target.clone(), workdir: cp.workdir.clone(), findings: cp.findings.clone() });
save_runs(base, &h);
println!(" \x1b[1;33m↻ recovered interrupted run on {}{} finding(s) saved as run #{}\x1b[0m (/results {id} · /report {id})",
cp.target, cp.findings.len(), id);
}
clear_checkpoint();
}
println!();
let mut reader = Reader::new(base);
let mut active: Option<ActiveRun> = None;
show(&s);
loop {
let Some(line) = reader.read() else { println!("\n bye."); break };
let Some(line) = reader.read(&context_prompt(&s)) else { println!("\n bye."); break };
let line = line.trim();
if line.is_empty() {
continue;
@@ -240,6 +370,15 @@ pub async fn repl(base: &Path) -> anyhow::Result<()> {
s.models = arg.split([',', ' ']).filter(|x| !x.is_empty()).map(String::from).collect();
println!(" models: {}", s.models.join(", "));
}
// If a run is paused on exhaustion, queue the newly-chosen models
// as its fallback so a plain /continue picks them up.
if let Some(a) = &active {
if a.paused.load(Ordering::Relaxed) {
let mut fb = a.fallback.lock().unwrap();
for id in &s.models { fb.push(ModelRef::parse(id)); }
println!(" \x1b[2m↪ queued for the paused run — /continue to resume on these model(s)\x1b[0m");
}
}
}
"/key" => key_cmd(&mut s, arg, &mut reader),
"/sub" | "/subscription" => {
@@ -247,24 +386,36 @@ pub async fn repl(base: &Path) -> anyhow::Result<()> {
println!(" subscription: {}", onoff(s.subscription));
}
"/target" | "/url" => {
let t = if arg.starts_with("http") || arg.is_empty() { arg.to_string() } else { format!("https://{arg}") };
s.target = if t.is_empty() { None } else { Some(t) };
println!(" target: {}", s.target.clone().unwrap_or_else(|| "(none)".into()));
if arg.is_empty() { println!(" target: {}", s.target.clone().unwrap_or_else(|| "(none) — set with /target <url>, clear with /target clear".into())); }
else if arg == "clear" { s.target = None; println!(" target cleared"); }
else { let t = if arg.starts_with("http") { arg.to_string() } else { format!("https://{arg}") };
s.target = Some(t.clone()); println!(" target: {t}"); }
}
"/repo" => {
s.repo = if arg.is_empty() { None } else { Some(arg.to_string()) };
println!(" repo: {}", s.repo.clone().unwrap_or_else(|| "(none)".into()));
if arg.is_empty() { println!(" repo: {}", s.repo.clone().unwrap_or_else(|| "(none) — set with /repo <path | github-url | owner/repo>, clear with /repo clear".into())); }
else if arg == "clear" { s.repo = None; println!(" repo cleared"); }
else {
// Accept a local path OR a GitHub URL / owner-repo shorthand (cloned on set).
match crate::resolve_source(base, arg) {
Ok(p) => { s.repo = Some(p.clone()); println!(" repo: {p}"); }
Err(e) => println!(" \x1b[31mcould not resolve repo: {e}\x1b[0m"),
}
}
}
"/auth" => {
s.auth = if arg.is_empty() { None } else { Some(arg.to_string()) };
println!(" auth: {}", s.auth.clone().unwrap_or_else(|| "(none)".into()));
if arg.is_empty() { println!(" auth: {}", s.auth.clone().unwrap_or_else(|| "(none) — set with /auth <header>, clear with /auth clear".into())); }
else if arg == "clear" { s.auth = None; println!(" auth cleared"); }
else { s.auth = Some(arg.to_string()); println!(" auth set: {arg}"); }
}
"/creds" => {
s.creds = if arg.is_empty() { None } else { Some(arg.to_string()) };
println!(" creds file: {}", s.creds.clone().unwrap_or_else(|| "(none)".into()));
if arg.is_empty() { println!(" creds file: {}", s.creds.clone().unwrap_or_else(|| "(none) — set with /creds <file.yaml>".into())); }
else if arg == "clear" { s.creds = None; println!(" creds cleared"); }
else { s.creds = Some(arg.to_string()); println!(" creds file: {arg}"); }
}
"/focus" | "/instructions" => {
s.instructions = if arg.is_empty() { None } else { Some(arg.to_string()) };
if arg == "clear" { s.instructions = None; println!(" focus cleared"); continue; }
if arg.is_empty() { println!(" focus: {}", s.instructions.clone().unwrap_or_else(|| "(none)".into())); continue; }
s.instructions = Some(arg.to_string());
println!(" focus: {}", s.instructions.clone().unwrap_or_else(|| "(none)".into()));
}
"/attach" => { let n = attach_path(arg.trim_start_matches('@'), &mut s); if n > 0 { println!(" attached ({} total)", s.attachments.len()); } }
@@ -279,15 +430,141 @@ pub async fn repl(base: &Path) -> anyhow::Result<()> {
}
"/mcp" => { s.mcp = !matches!(arg, "off" | "false" | "0" | "no"); println!(" Playwright MCP: {}", onoff(s.mcp)); }
"/offline" => { s.offline = !matches!(arg, "off" | "false" | "0" | "no"); println!(" offline: {}", onoff(s.offline)); }
"/integrations" | "/integration" => integrations_cmd(arg),
"/votes" => { s.vote_n = arg.parse().unwrap_or(s.vote_n); println!(" votes: {}", s.vote_n); }
"/agents" => { s.max_agents = arg.parse().unwrap_or(s.max_agents); println!(" max agents: {}", s.max_agents); }
"/clear" => { print!("\x1b[2J\x1b[H"); }
"/run" | "/go" => { run(base, &s, &mut history).await; save_runs(base, &history); }
"/runs" | "/history" => list_runs(&history),
"/results" => results(&history, arg),
"/report" => open_report(&history, arg),
"/status" => run_status(&history, arg),
"/quit" | "/exit" | "/q" => { println!(" bye."); break; }
"/run" | "/go" => {
if active.as_ref().map(|a| !a.done.load(Ordering::Relaxed)).unwrap_or(false) {
println!(" a run is already active — /status to check, /stop to halt it.");
} else {
save_session(&s);
match start_background(base, &s, &mut reader, history.clone()).await {
Some(a) => { active = Some(a); println!(" \x1b[1;35m▶ running in background\x1b[0m — keep typing · \x1b[36m/status\x1b[0m · \x1b[36m/stop\x1b[0m"); }
None => { // no external printer (piped) → blocking fallback
let mut h = history.lock().unwrap();
run(base, &s, &mut h).await; save_runs(base, &h);
}
}
}
}
"/stop" => {
match &active {
Some(a) if !a.done.load(Ordering::Relaxed) => {
println!(" \x1b[1mStop the run — choose:\x1b[0m");
println!(" \x1b[36m1\x1b[0m validate the findings found so far, then report \x1b[2m(recommended)\x1b[0m");
println!(" \x1b[36m2\x1b[0m report NOW without validating (raw findings)");
println!(" \x1b[36m3\x1b[0m discard (no report)");
let ans = ask_line(" choice [1/2/3]:");
match ans.trim() {
"2" => { *a.choice.lock().unwrap() = StopMode::Raw; a.cancel.store(true, Ordering::Relaxed);
println!(" ⏹ stopping — generating a RAW report from what was found…"); }
"3" => { *a.choice.lock().unwrap() = StopMode::Discard; a.cancel.store(true, Ordering::Relaxed);
println!(" 🗑 stopping — discarding this run."); }
_ => { *a.choice.lock().unwrap() = StopMode::Validate; a.soft.store(true, Ordering::Relaxed);
println!(" ⏸ stopping exploitation — validating what was found, then reporting…"); }
}
}
_ => println!(" no active run."),
}
}
"/continue" | "/resume" => {
match &active {
Some(a) if a.paused.load(Ordering::Relaxed) => {
if !arg.is_empty() {
let m = ModelRef::parse(arg);
println!(" \x1b[1;35m▶ resuming with fallback model\x1b[0m {}:{}", m.provider, m.model);
a.fallback.lock().unwrap().push(m);
} else {
println!(" \x1b[1;35m▶ resuming\x1b[0m — retrying with the current model(s).");
}
a.paused.store(false, Ordering::Relaxed);
a.resume.notify_waiters();
}
Some(a) if !a.done.load(Ordering::Relaxed) => println!(" run is not paused — it's still working. /status to check."),
_ => println!(" no paused run. (a run pauses automatically if your tokens/quota run out)"),
}
}
"/runs" | "/history" => list_runs(&history.lock().unwrap()),
"/diff" | "/changed" => diff_runs(&history.lock().unwrap()),
"/retest" => {
let h = history.lock().unwrap();
if let Some(r) = pick(&h, arg) {
if r.target.starts_with('/') { s.repo = Some(r.target.clone()); s.target = None; }
else { s.target = Some(r.target.clone()); }
let titles: Vec<String> = r.findings.iter().map(|f| f.title.clone()).collect();
if !titles.is_empty() {
s.instructions = Some(format!("RETEST — re-verify whether these prior findings are now fixed: {}", titles.join("; ")));
}
println!(" ↻ retest set up for {} ({} prior finding(s)) — /run to launch", r.target, titles.len());
}
}
"/results" => {
// Live findings while a run is active (no arg), else a past run.
match &active {
Some(a) if arg.is_empty() && !a.done.load(Ordering::Relaxed) => {
let l = a.live.lock().unwrap();
println!(" ▶ live — {} possible finding(s) so far ({})", l.full.len(), l.phase);
let mut f = l.full.clone();
f.sort_by_key(|x| sev_rank(&x.severity));
for x in &f { println!(" • [{}] {} \x1b[2m({} · {})\x1b[0m", x.severity, x.title, x.agent, x.endpoint); }
if !f.is_empty() { println!(" \x1b[2m/finding — pick one to see the command & PoC\x1b[0m"); }
}
_ => results(&history.lock().unwrap(), arg),
}
}
"/finding" | "/findings" => {
// Build the finding pool: live run if active, else a past run.
let pool: Vec<Finding> = match &active {
Some(a) if arg.is_empty() && !a.done.load(Ordering::Relaxed) => a.live.lock().unwrap().full.clone(),
_ => { let h = history.lock().unwrap(); pick(&h, arg).map(|r| r.findings.clone()).unwrap_or_default() }
};
finding_detail(&pool);
}
"/expand" | "/full" => {
// Show full untruncated commands from the active run.
match &active {
Some(a) => {
let l = a.live.lock().unwrap();
let n: usize = arg.trim().parse().unwrap_or(5);
let cmds = &l.commands;
if cmds.is_empty() { println!(" no commands captured yet."); }
else {
println!(" ── last {} command(s) (full) ──", n.min(cmds.len()));
for c in cmds.iter().rev().take(n).rev() { println!(" \x1b[33m$ {c}\x1b[0m"); }
}
}
None => println!(" no active run — /expand shows full commands while a run streams."),
}
}
"/report" => open_report(&history.lock().unwrap(), arg),
"/status" => {
// Live status if a run is active, else a past run's status.json.
match &active {
Some(a) if arg.is_empty() && !a.done.load(Ordering::Relaxed) => {
let l = a.live.lock().unwrap();
let el = l.started.elapsed().as_secs();
let mut by: std::collections::BTreeMap<&str, usize> = Default::default();
for (sv, _) in &l.findings { *by.entry(sv.as_str()).or_insert(0) += 1; }
let sev = if by.is_empty() { "0".into() } else { by.iter().map(|(k, v)| format!("{k}:{v}")).collect::<Vec<_>>().join(" ") };
println!(" \x1b[1m▶ live\x1b[0m {} ({}) · phase {} · {:02}:{:02} · {} possible finding(s) [{}]",
l.target, l.mode, l.phase, el / 60, el % 60, l.findings.len(), sev);
if a.paused.load(Ordering::Relaxed) {
println!(" \x1b[1;33m⏸ PAUSED — token/quota exhausted. /continue to resume, or /model <provider:model> then /continue to switch.\x1b[0m");
}
if l.agents > 0 { println!(" progress \x1b[36m{}\x1b[0m", l.bar(24)); }
for (sv, t) in l.findings.iter().rev().take(5) { println!(" ✦ [{sv}] {t}"); }
}
_ => run_status(&history.lock().unwrap(), arg),
}
}
"/quit" | "/exit" | "/q" => {
if active.as_ref().map(|a| !a.done.load(Ordering::Relaxed)).unwrap_or(false) {
if let Some(a) = &active { a.cancel.store(true, Ordering::Relaxed); }
println!(" ⏸ a run is active — requested stop; quitting.");
}
save_session(&s); println!(" session saved → {} · bye.", proj_dir().display()); break;
}
other => println!(" unknown command '{other}' — try /help"),
}
}
@@ -424,20 +701,171 @@ async fn run(base: &Path, s: &Session, history: &mut Vec<RunRecord>) {
}
}
fn runs_path(base: &Path) -> std::path::PathBuf {
base.join("data").join("repl_runs.json")
/// Launch an engagement in the BACKGROUND: it streams live via the editor's
/// external printer while the REPL keeps accepting commands (/status, /stop).
/// Returns None when no external printer is available (piped) → caller blocks.
async fn start_background(base: &Path, s: &Session, reader: &mut Reader,
history: Arc<Mutex<Vec<RunRecord>>>) -> Option<ActiveRun> {
let (target, mode_s, mode_e, mcp) = match (&s.repo, &s.target) {
(Some(_), Some(t)) => (t.clone(), "greybox", crate::Mode::Grey, s.mcp),
(Some(r), None) => (r.clone(), "white-box", crate::Mode::White, false),
(None, Some(t)) => (t.clone(), "black-box", crate::Mode::Black, s.mcp),
_ => { println!(" \x1b[31m✗ set a /target <url> and/or /repo <path> first.\x1b[0m"); return None; }
};
let mut cfg = RunConfig::new(&target);
cfg.models = s.models.clone();
cfg.subscription = s.subscription;
cfg.vote_n = s.vote_n;
cfg.max_agents = s.max_agents;
cfg.verbose = true;
cfg.offline = s.offline;
cfg.instructions = if s.attachments.is_empty() { s.instructions.clone() }
else { Some(format!("{}\n\nATTACHED CONTEXT:\n{}", s.instructions.clone().unwrap_or_default(), s.attachments.join("\n\n"))) };
cfg.auth = s.auth.clone();
if matches!(mode_e, crate::Mode::Grey) { cfg.repo = s.repo.clone(); }
crate::apply_creds(&mut cfg, s.creds.as_deref()).await;
let mut printer = reader.external_printer()?; // None on piped stdin → blocking fallback
let sp = crate::spawn_engagement(base, cfg, mcp, mode_e);
let live = Arc::new(Mutex::new(RunLive {
target: target.clone(), mode: mode_s, phase: "starting".into(),
started: Instant::now(), findings: vec![], full: vec![], commands: vec![],
agents: 0, agents_done: 0,
}));
let cancel = sp.cancel.clone();
let soft = sp.soft.clone();
let paused = sp.paused.clone();
let resume = sp.resume.clone();
let fallback = sp.fallback.clone();
let done = Arc::new(AtomicBool::new(false));
let choice = Arc::new(Mutex::new(StopMode::Run));
let (live2, done2, hist2, choice2) = (live.clone(), done.clone(), history, choice.clone());
tokio::spawn(async move {
let crate::Spawned { task, mut rx, workdir, .. } = sp;
let mut last_saved = 0usize;
while let Some(line) = rx.recv().await {
live2.lock().unwrap().ingest(&line);
if let Some(out) = crate::render_compact(&line) { let _ = printer.print(out); }
// Checkpoint live findings to disk whenever a new one lands, so the
// run survives a quit/crash and is recovered on next launch.
let snap = {
let l = live2.lock().unwrap();
if l.full.len() != last_saved {
last_saved = l.full.len();
Some(LiveCheckpoint {
target: l.target.clone(), mode: l.mode.into(), phase: l.phase.clone(),
workdir: workdir.display().to_string(),
findings: l.full.clone(), commands: l.commands.clone(),
})
} else { None }
};
if let Some(c) = snap { save_checkpoint(&c); }
}
fn load_runs(base: &Path) -> Vec<RunRecord> {
std::fs::read_to_string(runs_path(base)).ok()
let task_out = task.await.unwrap_or_default();
let mode_choice = *choice2.lock().unwrap();
if mode_choice == StopMode::Discard {
std::fs::remove_dir_all(&workdir).ok();
clear_checkpoint();
let _ = printer.print(format!("\x1b[33m🗑 run discarded — {}\x1b[0m", workdir.display()));
done2.store(true, Ordering::Relaxed);
return;
}
// Raw → report from the unvalidated candidates we captured live.
let (findings, validated_word) = if mode_choice == StopMode::Raw {
let raw = live2.lock().unwrap().full.clone();
crate::report_raw(&target, &raw, &workdir);
(raw, "unvalidated")
} else {
let out = crate::finalize_run(task_out, &workdir);
(out.findings, "validated")
};
let id = {
let mut h = hist2.lock().unwrap();
let id = h.len() + 1;
h.push(RunRecord { id, mode: mode_s.into(), target, workdir: workdir.display().to_string(), findings: findings.clone() });
if let Ok(j) = serde_json::to_string_pretty(&*h) { std::fs::write(proj_dir().join("runs.json"), j).ok(); }
id
};
clear_checkpoint(); // run is now a completed RunRecord
let _ = printer.print(format!(
"\x1b[1;32m◀ run #{id} done — {} {} finding(s)\x1b[0m · /results {id} · /finding",
findings.len(), validated_word));
let _ = printer.print(format!("\x1b[36m report: {}\x1b[0m", crate::report_url(&workdir)));
done2.store(true, Ordering::Relaxed);
});
Some(ActiveRun { live, cancel, soft, done, choice, paused, resume, fallback })
}
/// Project-local store: `<cwd>/.neurosploit/` so each project keeps its own
/// session, run history and command history (resume on reopen). No DB needed —
/// it's structured state, not semantic search.
pub(crate) fn proj_dir() -> std::path::PathBuf {
let d = std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from(".")).join(".neurosploit");
std::fs::create_dir_all(&d).ok();
d
}
fn runs_path(_base: &Path) -> std::path::PathBuf { proj_dir().join("runs.json") }
fn load_runs(_base: &Path) -> Vec<RunRecord> {
std::fs::read_to_string(runs_path(_base)).ok()
.and_then(|t| serde_json::from_str(&t).ok())
.unwrap_or_default()
}
fn save_runs(base: &Path, history: &[RunRecord]) {
let p = runs_path(base);
if let Some(dir) = p.parent() { std::fs::create_dir_all(dir).ok(); }
fn save_runs(_base: &Path, history: &[RunRecord]) {
let p = runs_path(_base);
if let Ok(j) = serde_json::to_string_pretty(history) { std::fs::write(p, j).ok(); }
}
/// Live-run checkpoint file (one in-flight run at a time).
fn checkpoint_path() -> std::path::PathBuf { proj_dir().join("active_run.json") }
fn save_checkpoint(c: &LiveCheckpoint) {
if let Ok(j) = serde_json::to_string_pretty(c) { std::fs::write(checkpoint_path(), j).ok(); }
}
fn clear_checkpoint() { std::fs::remove_file(checkpoint_path()).ok(); }
fn load_checkpoint() -> Option<LiveCheckpoint> {
std::fs::read_to_string(checkpoint_path()).ok().and_then(|t| serde_json::from_str(&t).ok())
}
/// Persistable snapshot of the session config (resume across restarts).
#[derive(Serialize, Deserialize, Default)]
struct Snapshot {
models: Vec<String>,
subscription: bool,
mcp: bool,
vote_n: usize,
max_agents: usize,
target: Option<String>,
repo: Option<String>,
auth: Option<String>,
creds: Option<String>,
instructions: Option<String>,
}
fn session_path() -> std::path::PathBuf { proj_dir().join("session.json") }
fn save_session(s: &Session) {
let snap = Snapshot {
models: s.models.clone(), subscription: s.subscription, mcp: s.mcp,
vote_n: s.vote_n, max_agents: s.max_agents, target: s.target.clone(),
repo: s.repo.clone(), auth: s.auth.clone(), creds: s.creds.clone(),
instructions: s.instructions.clone(),
};
if let Ok(j) = serde_json::to_string_pretty(&snap) { std::fs::write(session_path(), j).ok(); }
}
fn load_session(s: &mut Session) -> bool {
let Ok(txt) = std::fs::read_to_string(session_path()) else { return false };
let Ok(snap) = serde_json::from_str::<Snapshot>(&txt) else { return false };
if !snap.models.is_empty() { s.models = snap.models; }
s.subscription = snap.subscription; s.mcp = snap.mcp;
if snap.vote_n > 0 { s.vote_n = snap.vote_n; }
s.max_agents = snap.max_agents;
s.target = snap.target; s.repo = snap.repo; s.auth = snap.auth;
s.creds = snap.creds; s.instructions = snap.instructions;
true
}
fn pick<'a>(history: &'a [RunRecord], arg: &str) -> Option<&'a RunRecord> {
if history.is_empty() { println!(" no runs yet — /run first."); return None; }
if arg.trim().is_empty() { return history.last(); }
@@ -491,6 +919,125 @@ fn open_report(history: &[RunRecord], arg: &str) {
}
}
/// What changed between the last two runs (by finding title).
fn diff_runs(history: &[RunRecord]) {
if history.len() < 2 {
println!(" need at least 2 runs to diff (/runs).");
return;
}
let prev = &history[history.len() - 2];
let cur = &history[history.len() - 1];
let set = |r: &RunRecord| r.findings.iter().map(|f| f.title.clone()).collect::<std::collections::HashSet<_>>();
let (a, b) = (set(prev), set(cur));
println!(" ── what changed: run #{} → #{} ({}{}) ──", prev.id, cur.id, prev.findings.len(), cur.findings.len());
for t in b.difference(&a) { println!(" \x1b[32m+ new\x1b[0m {t}"); }
for t in a.difference(&b) { println!(" \x1b[31m- gone\x1b[0m {t}"); }
if a == b { println!(" (no change in finding titles)"); }
}
fn sev_rank(s: &str) -> u8 {
match s { "Critical" => 0, "High" => 1, "Medium" => 2, "Low" => 3, _ => 4 }
}
/// Read one line synchronously (for the /stop choice prompt).
/// `/integrations` — show / enable / disable / setup GitHub, GitLab, Jira.
fn integrations_cmd(arg: &str) {
let dir = proj_dir();
let mut ig = harness::integrations::Integrations::load(&dir);
let mut parts = arg.splitn(2, char::is_whitespace);
let sub = parts.next().unwrap_or("").trim();
let name = parts.next().unwrap_or("").trim();
match sub {
"" | "show" | "status" => {
println!(" \x1b[1mintegrations\x1b[0m · {}", dir.display());
for l in ig.status_lines() { println!(" {l}"); }
println!(" \x1b[2m/integrations enable|disable <github|gitlab|jira> · /integrations setup <jira|gitlab|github>\x1b[0m");
println!(" \x1b[2mtokens come from env vars (never stored): GITHUB_TOKEN · GITLAB_TOKEN · JIRA_EMAIL + JIRA_API_TOKEN\x1b[0m");
}
"enable" | "disable" => {
let on = sub == "enable";
match name {
"github" => ig.github.enabled = on,
"gitlab" => ig.gitlab.enabled = on,
"jira" => ig.jira.enabled = on,
_ => { println!(" usage: /integrations {sub} <github|gitlab|jira>"); return; }
}
let _ = ig.save(&dir);
println!(" {name} {}", if on { "enabled ✓" } else { "disabled" });
}
"setup" => match name {
"jira" => {
let base = ask_line(" Jira base URL (https://your-org.atlassian.net):");
if !base.trim().is_empty() { ig.jira.base_url = base.trim().trim_end_matches('/').to_string(); }
let proj = ask_line(" Jira project key (e.g. SEC):");
if !proj.trim().is_empty() { ig.jira.project_key = proj.trim().to_string(); }
let it = ask_line(" Issue type [Bug]:");
if !it.trim().is_empty() { ig.jira.issue_type = it.trim().to_string(); }
ig.jira.enabled = true;
let _ = ig.save(&dir);
println!(" ✓ jira configured (project {}, {}). Now export {} and {} in your shell.",
ig.jira.project_key, ig.jira.base_url, ig.jira.email_env, ig.jira.token_env);
}
"gitlab" => {
let b = ask_line(" GitLab base [https://gitlab.com]:");
if !b.trim().is_empty() { ig.gitlab.base = b.trim().trim_end_matches('/').to_string(); }
ig.gitlab.enabled = true;
let _ = ig.save(&dir);
println!(" ✓ gitlab enabled (base {}). Export {} (PAT with read_repository).", ig.gitlab.base, ig.gitlab.token_env);
}
"github" => {
let a = ask_line(" GitHub API base [https://api.github.com] (change for GHE):");
if !a.trim().is_empty() { ig.github.api = a.trim().trim_end_matches('/').to_string(); }
ig.github.enabled = true;
let _ = ig.save(&dir);
println!(" ✓ github enabled (api {}). Export {} (PAT with repo scope).", ig.github.api, ig.github.token_env);
}
_ => println!(" usage: /integrations setup <jira|gitlab|github>"),
},
_ => println!(" usage: /integrations [show | enable <name> | disable <name> | setup <name>]"),
}
}
fn ask_line(prompt: &str) -> String {
use std::io::Write;
print!("{prompt} ");
std::io::stdout().flush().ok();
let mut s = String::new();
std::io::stdin().read_line(&mut s).ok();
s
}
/// Arrow-key selection menu over findings; prints EVERYTHING about the chosen one
/// (command/PoC, evidence, impact, remediation, votes, confidence).
fn finding_detail(pool: &[Finding]) {
if pool.is_empty() { println!(" no findings to inspect yet."); return; }
let mut f = pool.to_vec();
f.sort_by_key(|x| sev_rank(&x.severity));
let items: Vec<String> = f.iter().map(|x| format!("[{}] {}{}", x.severity, x.title, x.cwe)).collect();
let idx = if std::io::stdin().is_terminal() {
match dialoguer::Select::with_theme(&ColorfulTheme::default())
.with_prompt("Select a finding (↑/↓, enter)").items(&items).default(0).interact_opt() {
Ok(Some(i)) => i, _ => return,
}
} else { 0 };
let x = &f[idx];
println!("\n ┌─ \x1b[1m{}\x1b[0m", x.title);
println!(" │ severity : {}", x.severity);
println!(" │ cwe / cvss : {} · {}", x.cwe, x.cvss);
println!(" │ agent : {}", x.agent);
println!(" │ endpoint : {}", x.endpoint);
println!(" │ votes/conf : {} · {:.2}", x.votes, x.confidence);
println!(" ├─ \x1b[33mPayload / PoC\x1b[0m");
for l in x.payload.lines() { println!("{l}"); }
println!(" ├─ \x1b[36mEvidence (tool output)\x1b[0m");
for l in x.evidence.lines() { println!("{l}"); }
println!(" ├─ Impact");
for l in x.impact.lines() { println!("{l}"); }
println!(" ├─ Remediation");
for l in x.remediation.lines() { println!("{l}"); }
println!(" └─────");
}
fn run_status(history: &[RunRecord], arg: &str) {
let Some(r) = pick(history, arg) else { return };
match std::fs::read_to_string(Path::new(&r.workdir).join("status.json")) {
@@ -516,27 +1063,61 @@ fn show(s: &Session) {
println!(" │ creds : {}", s.creds.clone().unwrap_or_else(|| "(none)".into()));
println!(" │ focus : {}", s.instructions.clone().unwrap_or_else(|| "(none — tests everything)".into()));
println!(" │ opts : mcp={} offline={} votes={} max-agents={}", onoff(s.mcp), onoff(s.offline), s.vote_n, s.max_agents);
println!(" └─ /run to launch");
// API-key status for the providers your selected models need.
if !s.subscription {
let provs: std::collections::BTreeSet<String> = s.models.iter()
.map(|m| m.split(':').next().unwrap_or("").to_string()).collect();
let mut keys = Vec::new();
for p in &provs {
if let Some(pr) = harness::provider_for(p) {
let set = std::env::var(pr.env_key).map(|v| !v.is_empty()).unwrap_or(false);
keys.push(format!("{p}={}", if set { "" } else { "" }));
}
}
if !keys.is_empty() { println!(" │ api keys : {}", keys.join(" ")); }
}
println!(" └─ /run to launch · edit with /target /repo /auth /creds /focus /model");
}
fn help() {
println!(" Commands (↑/↓ recall history · Ctrl-A/E/K edit · Ctrl-C cancels line):");
println!(" /model [a:b,..] set models; with no arg → arrow-key multi-select");
println!(" /providers list providers & models");
println!(" /key [prov key] configure API keys for your models (no arg → guided)");
println!(" /sub on|off use local subscription login instead of API key");
println!(" /target <url> black-box target URL");
println!(" /repo <path> analyse a repo (repo+target = greybox: code + live)");
println!(" /auth <value> auth header (e.g. 'Authorization: Bearer <jwt>')");
println!(" /creds <file.yaml> credentials (jwt/header/cookie/login) for authed tests");
println!(" /focus <text> steer the tests (or just type it); e.g. injection + access control");
println!(" @path @dir @f:1-20 attach a file/folder/line-range to context (Tab-completes)");
println!(" /attach <path> attach context · /context list attachments");
println!(" /mcp on|off Playwright MCP browser /offline on|off self-test");
println!(" /theme color|mono /config (=/show) /votes <n> /agents <n>");
println!(" Tab completes commands & @paths · ↑/↓ history · end a line with \\ for multiline");
println!(" /run launch · /runs /results [n] /report [n] /status [n]");
println!(" /quit exit");
let h = |c: &str, d: &str| println!(" \x1b[36m{c:<20}\x1b[0m {d}");
println!("\n \x1b[1mNeuroSploit REPL — commands\x1b[0m");
println!("\n \x1b[2mTARGET & SCOPE\x1b[0m");
h("/target <url>", "black-box target URL");
h("/repo <path>", "analyse a repo (repo + target = greybox: code + live)");
h("/auth <value>", "auth header, e.g. 'Authorization: Bearer <jwt>' (no arg = show)");
h("/creds <file.yaml>", "credentials: jwt/header/cookie/login + ssh/windows");
h("/focus <text>", "steer the tests (or just type the instruction)");
h("@path @dir @f:1-20", "attach a file/folder/line-range to context (Tab → menu)");
h("/attach /context", "attach a path · list attachments");
println!("\n \x1b[2mMODELS & AUTH\x1b[0m");
h("/model [a:b,..]", "set models (no arg → arrow-key multi-select)");
h("/providers", "list providers & models");
h("/key [prov key]", "configure API keys for your models (no arg → guided)");
h("/sub on|off", "use local subscription login instead of an API key");
println!("\n \x1b[2mRUN & MONITOR\x1b[0m");
h("/run", "launch (runs in the BACKGROUND — keep typing)");
h("/status", "live progress + findings while running (or a past run #)");
h("/stop", "stop: [1] validate+report [2] raw report now [3] discard");
h("/continue", "resume a run paused on token/quota (change /model first to switch)");
h("/runs", "list runs · /results [n] · /report [n]");
h("/diff /retest [n]", "what changed vs last run · re-verify a past run");
println!("\n \x1b[2mINTEGRATIONS\x1b[0m");
h("/integrations", "show · enable/disable github|gitlab|jira · setup <name>");
println!("\n \x1b[2mOPTIONS\x1b[0m");
h("/mcp on|off", "Playwright MCP browser /offline on|off self-test");
h("/votes <n>", "validator votes /agents <n> cap agents");
h("/theme color|mono", "/show (config) /clear /quit");
println!("\n \x1b[2mMODES — black-box: set /target · white-box: set /repo · grey-box: set BOTH /repo + /target · host: /target <ip> + /creds\x1b[0m");
println!(" \x1b[2mFindings are checkpointed live to .neurosploit/ — quit/crash mid-run and they're recovered into /runs next launch.\x1b[0m");
println!(" \x1b[2mIf tokens/quota run out the run PAUSES (state kept) — /continue to resume, or switch with /model then /continue.\x1b[0m");
println!(" \x1b[2m↑/↓ history · Tab completes commands & @paths · Ctrl-A/E/K edit · Ctrl-O full cmd · \\ for multiline\x1b[0m\n");
}
/// Scan a line for @path tokens, attach each referenced file/dir to context.
@@ -590,5 +1171,29 @@ fn parse_range(r: &str) -> Option<(usize, usize)> {
}
}
/// Context/status bar shown above the prompt — model · cwd · mode/target,
/// e.g. "claude-opus-4-8 · /opt/projeto · black-box▸target".
fn context_prompt(s: &Session) -> String {
let model = s.models.first().map(|m| m.split(':').next_back().unwrap_or(m)).unwrap_or("?");
let auth = if s.subscription { "sub" } else { "api" };
let cwd = std::env::current_dir().ok()
.map(|p| p.display().to_string())
.unwrap_or_else(|| ".".into());
let mode = match (&s.repo, &s.target) {
(Some(_), Some(_)) => "greybox",
(Some(_), None) => "white-box",
(None, Some(_)) => "black-box",
_ => "idle",
};
let tgt = s.target.clone().or_else(|| s.repo.clone()).unwrap_or_default();
let tgt = if tgt.is_empty() { String::new() } else { format!("{}", tgt.replace("https://", "").replace("http://", "")) };
format!(
"\x1b[2m{model} {auth} · {cwd} · {mode}{tgt}\x1b[0m\n\x1b[35mneurosploit\x1b[0m "
)
}
fn onoff(b: bool) -> &'static str { if b { "on" } else { "off" } }
fn trunc(s: &str, n: usize) -> String { if s.len() <= n { s.to_string() } else { format!("{}", &s[..n.saturating_sub(1)]) } }
fn trunc(s: &str, n: usize) -> String {
if s.chars().count() <= n { s.to_string() }
else { format!("{}", s.chars().take(n.saturating_sub(1)).collect::<String>()) }
}
+304
View File
@@ -0,0 +1,304 @@
//! NeuroSploit v3.5.4 — TUI "Mission Control" mode.
//!
//! Concurrent panels that update live while the engagement runs in the
//! background, with a composer input that stays active during execution:
//!
//! ┌ status header (target · mode · phase · elapsed · tokens · findings) ┐
//! │ live activity feed │ findings (live) │
//! │ (recon/exploit/tool/command) ├───────────────────────────────────┤
//! │ │ targets / queue │
//! └ composer: ask 'summary', 'pause', 'errors', or notes … ────────────┘
//!
//! The engagement runs as a tokio task streaming tagged events over an mpsc
//! channel; the UI drains them each tick. The composer answers locally
//! (summary / what-found / errors / pause) WITHOUT stopping the runner.
use crate::Mode;
use crossterm::event::{self, Event, KeyCode, KeyModifiers};
use crossterm::{execute, terminal};
use harness::{agents, models::ModelRef, pool::ModelPool, types::RunConfig};
use ratatui::prelude::*;
use ratatui::widgets::{Block, Borders, List, ListItem, Paragraph, Wrap};
use std::collections::VecDeque;
use std::io::stdout;
use std::path::Path;
use std::sync::atomic::Ordering;
use std::time::{Duration, Instant};
struct Ui {
target: String,
models: String,
mode: &'static str,
phase: String,
started: Instant,
feed: VecDeque<String>,
findings: Vec<(String, String, String)>, // sev, title, endpoint
targets: Vec<(String, String)>, // host, state
tin: u64,
tout: u64,
cost: f64,
input: String,
filter_errors: bool,
done: bool,
paused: bool,
}
impl Ui {
fn new(target: &str, models: &str, mode: &'static str) -> Self {
let host = target.replace("https://", "").replace("http://", "");
let host = host.split('/').next().unwrap_or(&host).to_string();
Ui {
target: target.into(), models: models.into(), mode,
phase: "starting".into(), started: Instant::now(),
feed: VecDeque::new(), findings: vec![],
targets: vec![(host, "🔄 running".into())],
tin: 0, tout: 0, cost: 0.0, input: String::new(),
filter_errors: false, done: false, paused: false,
}
}
fn ingest(&mut self, raw: String) {
let line = raw.trim_end().to_string();
let low = line.to_lowercase();
// phase tracking
if low.contains("recon") { self.phase = "🔍 recon".into(); }
else if low.contains("planning") || low.contains("selected") || low.contains("selection") { self.phase = "🧭 planning".into(); }
else if low.starts_with("exploit") || low.contains("launching agent") || low.starts_with("analyze") { self.phase = "🧪 exploiting".into(); }
else if low.starts_with("vote") || low.contains("validating") { self.phase = "✓ validating".into(); }
else if low.starts_with("chain") { self.phase = "🔗 chaining".into(); }
else if low.contains("phase complete") || low.contains("validated finding(s)") { self.phase = "✓ complete".into(); }
// live findings
if let Some(rest) = line.strip_prefix("finding: ") {
// "[sev] title @ endpoint"
if let Some(b) = rest.strip_prefix('[') {
if let Some((sev, tail)) = b.split_once(']') {
let (title, ep) = tail.trim().split_once(" @ ").unwrap_or((tail.trim(), ""));
self.findings.push((sev.to_string(), title.to_string(), ep.to_string()));
self.note_target_from(ep);
}
}
return;
}
// token telemetry
if let Some(rest) = line.strip_prefix("@").and_then(|s| s.split_once(' ')).map(|(_, r)| r).filter(|r| r.starts_with("tokens:")).or_else(|| line.strip_prefix("tokens: ").map(|_| line.as_str())) {
for part in rest.split_whitespace() {
if let Some(v) = part.strip_prefix("in=") { self.tin += v.parse().unwrap_or(0); }
else if let Some(v) = part.strip_prefix("out=") { self.tout += v.parse().unwrap_or(0); }
else if let Some(v) = part.strip_prefix("cost=$") { self.cost += v.parse().unwrap_or(0.0); }
}
}
let is_err = low.contains("fail") || low.contains("error") || low.starts_with('✗');
if self.filter_errors && !is_err { return; }
self.feed.push_back(line);
while self.feed.len() > 500 { self.feed.pop_front(); }
}
fn note_target_from(&mut self, endpoint: &str) {
let host = endpoint.replace("https://", "").replace("http://", "");
let host = host.split('/').next().unwrap_or("").to_string();
if !host.is_empty() && !self.targets.iter().any(|(h, _)| h == &host) {
self.targets.push((host, "🔄 testing".into()));
}
}
/// Composer command (local, non-blocking). Returns feed lines to show.
fn composer(&mut self, cmd: &str) -> Vec<String> {
let c = cmd.trim().to_lowercase();
match c.as_str() {
"" => vec![],
"pause" | "/pause" | "stop" | "/stop" => { self.paused = true; vec!["⏸ pausing — finishing in-flight work, no new agents".into()] }
"errors" | "/errors" => { self.filter_errors = !self.filter_errors; vec![format!("filter errors: {}", self.filter_errors)] }
"clear" | "/clear" => { self.feed.clear(); vec![] }
"summary" | "/summary" | "what" | "o que" | "resumo" => self.summary(),
"findings" | "/findings" => self.summary(),
"quit" | "/quit" | "exit" => { self.done = true; vec![] }
other => vec![format!("noted: {other}")],
}
}
fn summary(&self) -> Vec<String> {
let mut by: std::collections::BTreeMap<&str, usize> = Default::default();
for (s, _, _) in &self.findings { *by.entry(s.as_str()).or_insert(0) += 1; }
let sev = if by.is_empty() { "0".into() } else { by.iter().map(|(k, v)| format!("{k}:{v}")).collect::<Vec<_>>().join(" ") };
let mut out = vec![format!("── partial summary: {} finding(s) [{}] · phase {} ──", self.findings.len(), sev, self.phase)];
for (s, t, _) in self.findings.iter().rev().take(5) { out.push(format!(" • [{s}] {t}")); }
out
}
}
/// Run the Mission-Control TUI for an engagement.
pub async fn run(base: &Path, mut cfg: RunConfig, mcp: bool, mode: Mode) -> anyhow::Result<()> {
let lib = agents::load(base);
let run_id = format!("ns-{}-{}", crate::now_ts_pub(), crate::sanitize_pub(&cfg.target));
let workdir = base.join("runs").join(&run_id);
std::fs::create_dir_all(&workdir).ok();
cfg.workdir = Some(workdir.display().to_string());
cfg.rl_path = Some(base.join("data").join("rl_state_rs.json").display().to_string());
cfg.verbose = true;
let mcp_config = if mcp && cfg.subscription {
harness::ensure_playwright_mcp().ok().and_then(|_| harness::write_mcp_config(&workdir, None).ok())
.map(|p| p.display().to_string())
} else { None };
let refs: Vec<ModelRef> = cfg.models.iter().map(|s| ModelRef::parse(s)).collect();
let pool = ModelPool::with_auth(refs, cfg.concurrency, cfg.subscription, mcp_config);
let cancel = pool.cancel_handle();
let (tx, mut rx) = tokio::sync::mpsc::channel::<String>(512);
let models = cfg.models.join(", ");
let mode_s = match mode { Mode::White => "white-box", Mode::Grey => "greybox", Mode::Host => "host/infra", Mode::Black => "black-box" };
let target_s = cfg.target.clone();
// ---- terminal setup FIRST: on a non-TTY this errors before we spawn any
// live engagement, so we never detach a running task. ----
terminal::enable_raw_mode()?;
execute!(stdout(), terminal::EnterAlternateScreen)?;
let mut term = Terminal::new(CrosstermBackend::new(stdout()))?;
let mut ui = Ui::new(&target_s, &models, mode_s);
let mut task = tokio::spawn(async move {
match mode {
Mode::White => harness::run_whitebox(cfg, &lib, &pool, tx).await,
Mode::Grey => harness::run_greybox(cfg, &lib, &pool, tx).await,
Mode::Host => harness::run_host(cfg, &lib, &pool, tx).await,
Mode::Black => harness::run(cfg, &lib, &pool, tx).await,
}
});
let out;
loop {
// drain engagement events
while let Ok(line) = rx.try_recv() { ui.ingest(line); }
// engagement finished?
if task.is_finished() {
ui.done = true;
ui.phase = "✓ complete".into();
if let Some((_, st)) = ui.targets.get_mut(0) { *st = "✅ done".into(); }
}
draw(&mut term, &ui)?;
// input (100ms tick keeps the UI live while the runner works)
if event::poll(Duration::from_millis(120))? {
if let Event::Key(k) = event::read()? {
let ctrl_c = k.modifiers.contains(KeyModifiers::CONTROL) && k.code == KeyCode::Char('c');
match k.code {
KeyCode::Esc => { cancel.store(true, Ordering::Relaxed); if ui.done { break; } ui.paused = true; }
KeyCode::Char('c') if ctrl_c => { cancel.store(true, Ordering::Relaxed); if ui.done { break; } ui.paused = true; }
KeyCode::Enter => {
let line = std::mem::take(&mut ui.input);
if matches!(line.trim(), "quit" | "/quit" | "exit") && ui.done { break; }
let lines = ui.composer(&line);
if ui.paused { cancel.store(true, Ordering::Relaxed); }
for l in lines { ui.feed.push_back(l); }
}
KeyCode::Backspace => { ui.input.pop(); }
KeyCode::Char(c) => { ui.input.push(c); }
_ => {}
}
}
}
if ui.done && task.is_finished() && ui.input.is_empty() {
// brief grace so the final frame is visible; exit on next Esc/Enter handled above
}
}
out = (&mut task).await.unwrap_or_default();
// ---- restore terminal ----
execute!(stdout(), terminal::LeaveAlternateScreen)?;
terminal::disable_raw_mode()?;
// generate report unless discarded; print a plain summary after leaving the TUI
match harness::report::typst_report(&out.target, &out.findings, &workdir) {
Ok(p) => println!(" report → {}", p.display()),
Err(_) => {}
}
crate::write_status_pub(&workdir, if cancel.load(Ordering::Relaxed) { "stopped" } else { "complete" }, "");
println!("{} validated finding(s) · {}", out.findings.len(), workdir.display());
Ok(())
}
fn sevstyle(s: &str) -> Style {
match s {
"Critical" => Style::new().fg(Color::Red).bold(),
"High" => Style::new().fg(Color::Rgb(251, 146, 60)),
"Medium" => Style::new().fg(Color::Yellow),
"Low" => Style::new().fg(Color::Cyan),
_ => Style::new().fg(Color::Gray),
}
}
fn draw(term: &mut Terminal<CrosstermBackend<std::io::Stdout>>, ui: &Ui) -> anyhow::Result<()> {
term.draw(|f| {
let root = Layout::vertical([
Constraint::Length(3), // header
Constraint::Min(5), // body
Constraint::Length(3), // composer
]).split(f.area());
// ── header ──
let el = ui.started.elapsed().as_secs();
let accent = Style::new().fg(Color::Rgb(139, 92, 246)).bold();
let header = Line::from(vec![
Span::styled(" 🧠 NeuroSploit ", accent),
Span::raw(format!("{} ", ui.target)),
Span::styled(format!("{} ", ui.mode), Style::new().fg(Color::Magenta)),
Span::styled(format!("{} ", short_models(&ui.models)), Style::new().fg(Color::DarkGray)),
Span::styled(format!("{} ", ui.phase), Style::new().fg(Color::Cyan)),
Span::raw(format!("{:02}:{:02} ", el / 60, el % 60)),
Span::styled(format!("{} findings ", ui.findings.len()), Style::new().fg(Color::Yellow)),
Span::raw(format!("│ 🪙 {}/{} ${:.3} ", ui.tin, ui.tout, ui.cost)),
if ui.paused { Span::styled("│ ⏸ stopping ", Style::new().fg(Color::Red)) } else { Span::raw("") },
]);
f.render_widget(Paragraph::new(header).block(Block::default().borders(Borders::ALL)
.title(" Mission Control ").border_style(accent)), root[0]);
// ── body: feed | (findings / targets) ──
let body = Layout::horizontal([Constraint::Percentage(60), Constraint::Percentage(40)]).split(root[1]);
let feed_h = body[0].height.saturating_sub(2) as usize;
let feed: Vec<ListItem> = ui.feed.iter().rev().take(feed_h).rev()
.map(|l| ListItem::new(feed_span(l))).collect();
f.render_widget(List::new(feed).block(Block::default().borders(Borders::ALL)
.title(format!(" Activity{} ", if ui.filter_errors { " [errors]" } else { "" }))), body[0]);
let right = Layout::vertical([Constraint::Percentage(60), Constraint::Percentage(40)]).split(body[1]);
let finds: Vec<ListItem> = ui.findings.iter().rev().take(right[0].height.saturating_sub(2) as usize)
.map(|(s, t, _)| ListItem::new(Line::from(vec![
Span::styled(format!("[{s}] "), sevstyle(s)), Span::raw(t.clone())]))).collect();
f.render_widget(List::new(finds).block(Block::default().borders(Borders::ALL)
.title(format!(" Findings ({}) ", ui.findings.len()))), right[0]);
let tg: Vec<ListItem> = ui.targets.iter()
.map(|(h, st)| ListItem::new(format!("{st} {h}"))).collect();
f.render_widget(List::new(tg).block(Block::default().borders(Borders::ALL).title(" Targets ")), right[1]);
// ── composer ──
let hint = if ui.done { "engagement done — type quit/Esc to exit · summary" }
else { "composer (runner active): summary · pause · errors · clear · or a note" };
let comp = Paragraph::new(Line::from(vec![
Span::styled(" ", accent), Span::raw(&ui.input),
Span::styled("", Style::new().fg(Color::Rgb(139, 92, 246))),
])).block(Block::default().borders(Borders::ALL).title(format!(" {hint} "))).wrap(Wrap { trim: false });
f.render_widget(comp, root[2]);
})?;
Ok(())
}
fn short_models(m: &str) -> String {
// show just the first model's name, compactly
m.split(',').next().unwrap_or(m).split(':').next_back().unwrap_or(m).trim().to_string()
}
fn feed_span(l: &str) -> Line<'static> {
let low = l.to_lowercase();
let (color, s) = if l.starts_with("finding:") || l.contains("possible finding") { (Color::Yellow, l) }
else if l.starts_with("notify:") || l.contains('🔔') { (Color::Cyan, l) }
else if low.contains("fail") || low.contains("error") || l.starts_with('✗') { (Color::Red, l) }
else if low.contains("exec:") || low.contains("command") || low.contains("curl") { (Color::Rgb(230, 180, 100), l) }
else if low.contains("recon") || low.contains("vote") || low.contains("chain") { (Color::Cyan, l) }
else { (Color::Gray, l) };
Line::from(Span::styled(s.to_string(), Style::new().fg(color)))
}
@@ -23,11 +23,14 @@ pub struct Library {
pub meta: Vec<Agent>,
pub recon: Vec<Agent>,
pub code: Vec<Agent>,
pub infra: Vec<Agent>,
pub chains: Vec<Agent>,
}
impl Library {
pub fn total(&self) -> usize {
self.vulns.len() + self.meta.len() + self.recon.len() + self.code.len()
+ self.infra.len() + self.chains.len()
}
}
@@ -39,6 +42,8 @@ pub fn load(base: &Path) -> Library {
meta: load_dir(&root.join("meta"), "meta"),
recon: load_dir(&root.join("recon"), "recon"),
code: load_dir(&root.join("code"), "code"),
infra: load_dir(&root.join("infra"), "infra"),
chains: load_dir(&root.join("chains"), "chain"),
}
}
+146
View File
@@ -0,0 +1,146 @@
//! POMDP belief-state world model (v3.5.4).
//!
//! The target is only partially observable, so we don't track booleans — we
//! track a **belief**: a property graph whose nodes (host / service / vuln /
//! credential) each carry a probability that the proposition is true. Recon
//! produces *observations* that update those beliefs via a Bayesian step; the
//! per-node Shannon entropy measures how diffuse the belief still is.
//!
//! - **Black-box**: beliefs start uncertain (~0.5) and sharpen with observation.
//! - **White-box**: the world model is built (near-)deterministically from
//! source/SAST, so beliefs collapse toward 0/1 — the POMDP degenerates into an
//! MDP and uncertainty migrates to *path reachability*, not state.
//!
//! This is the substrate for value-of-information planning (see `pomdp.rs`): when
//! a node's belief is diffuse, gathering an observation about it is worth more
//! than acting on it — which is also the anti-hallucination criterion.
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
/// What a belief node is about.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum Kind {
Host, // a host exists / is reachable
Service, // a service/endpoint is present
Vuln, // a specific weakness is present
Exploit, // the weakness is actually exploitable
Credential, // a credential is valid
}
/// A single proposition with a probability of being true and the evidence count
/// behind it (used for confidence/entropy).
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Node {
pub id: String,
pub kind: Kind,
pub label: String,
/// P(proposition is true) ∈ [0,1].
pub p: f64,
/// number of independent observations folded in.
pub obs: u32,
}
impl Node {
/// Shannon entropy in bits of the Bernoulli(p) belief — 1.0 = maximally
/// uncertain (p=0.5), 0.0 = certain.
pub fn entropy(&self) -> f64 {
let p = self.p.clamp(1e-6, 1.0 - 1e-6);
-(p * p.log2() + (1.0 - p) * (1.0 - p).log2())
}
}
/// A directed edge: "from enables/leads-to to" with a transition probability.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Edge {
pub from: String,
pub to: String,
pub p: f64,
}
/// The belief: a property graph over the partially-observed target.
#[derive(Default, Clone, Serialize, Deserialize)]
pub struct WorldModel {
pub nodes: HashMap<String, Node>,
pub edges: Vec<Edge>,
/// true once beliefs were built deterministically (white-box → MDP regime).
pub deterministic: bool,
}
/// A sensed observation about a node: P(observation | true) vs P(observation | false).
/// `positive` true means the observation supports the proposition.
pub struct Observation<'a> {
pub node: &'a str,
pub positive: bool,
/// sensor reliability ∈ (0.5, 1.0]; how much one observation moves the belief.
pub reliability: f64,
}
impl WorldModel {
pub fn new() -> Self {
WorldModel::default()
}
/// Seed a node with a prior. Black-box priors are ~0.5 (unknown); white-box
/// callers pass priors near 0/1.
pub fn add(&mut self, id: &str, kind: Kind, label: &str, prior: f64) {
self.nodes.entry(id.to_string()).or_insert_with(|| Node {
id: id.to_string(),
kind,
label: label.to_string(),
p: prior.clamp(0.0, 1.0),
obs: 0,
});
}
pub fn link(&mut self, from: &str, to: &str, p: f64) {
self.edges.push(Edge { from: from.into(), to: to.into(), p: p.clamp(0.0, 1.0) });
}
/// Bayesian update of a node's belief from one observation. With sensor
/// reliability r: a positive obs multiplies the odds by r/(1-r), a negative
/// one by (1-r)/r.
pub fn observe(&mut self, o: Observation) {
let r = o.reliability.clamp(0.5 + 1e-6, 1.0 - 1e-6);
if let Some(n) = self.nodes.get_mut(o.node) {
let p = n.p.clamp(1e-6, 1.0 - 1e-6);
let prior_odds = p / (1.0 - p);
let lr = if o.positive { r / (1.0 - r) } else { (1.0 - r) / r };
let post_odds = prior_odds * lr;
n.p = post_odds / (1.0 + post_odds);
n.obs += 1;
}
}
/// Collapse a node to (near-)certainty — used by white-box when SAST/dataflow
/// determines the proposition deterministically.
pub fn set_known(&mut self, id: &str, truth: bool) {
if let Some(n) = self.nodes.get_mut(id) {
n.p = if truth { 0.98 } else { 0.02 };
n.obs += 3;
}
}
/// Mean entropy across nodes of a kind (or all). 1.0 = totally diffuse.
pub fn uncertainty(&self, kind: Option<Kind>) -> f64 {
let rel: Vec<&Node> = self.nodes.values()
.filter(|n| kind.map(|k| n.kind == k).unwrap_or(true)).collect();
if rel.is_empty() {
return 1.0;
}
rel.iter().map(|n| n.entropy()).sum::<f64>() / rel.len() as f64
}
/// Nodes whose belief is still diffuse (entropy above `thresh`) — the recon
/// frontier: where collecting an observation has the highest value.
pub fn frontier(&self, thresh: f64) -> Vec<&Node> {
let mut v: Vec<&Node> = self.nodes.values().filter(|n| n.entropy() > thresh).collect();
v.sort_by(|a, b| b.entropy().partial_cmp(&a.entropy()).unwrap_or(std::cmp::Ordering::Equal));
v
}
/// Is a proposition confident enough to *act/assert* on? (low entropy + high p)
pub fn is_confident(&self, id: &str, min_p: f64, max_entropy: f64) -> bool {
self.nodes.get(id).map(|n| n.p >= min_p && n.entropy() <= max_entropy).unwrap_or(false)
}
}
+82 -12
View File
@@ -31,12 +31,34 @@ pub struct Login {
pub success: String,
}
/// SSH credentials for Linux host testing.
#[derive(Default, Debug, Clone)]
pub struct Ssh {
pub host: String,
pub port: String, // default 22
pub user: String,
pub password: String,
pub key: String, // path to a private key
}
/// Windows / Active Directory credentials.
#[derive(Default, Debug, Clone)]
pub struct Win {
pub host: String,
pub user: String,
pub password: String,
pub domain: String,
pub hash: String, // NTLM hash for pass-the-hash (LM:NT or NT)
}
#[derive(Default, Debug, Clone)]
pub struct Creds {
pub jwt: Option<String>,
pub header: Option<String>,
pub cookie: Option<String>,
pub login: Option<Login>,
pub ssh: Option<Ssh>,
pub win: Option<Win>,
}
impl Creds {
@@ -44,8 +66,10 @@ impl Creds {
let text = std::fs::read_to_string(path).ok()?;
let mut c = Creds::default();
let mut login = Login { method: "POST".into(), ..Default::default() };
let mut in_login = false;
let mut have_login = false;
let mut ssh = Ssh { port: "22".into(), ..Default::default() };
let mut win = Win::default();
let (mut have_login, mut have_ssh, mut have_win) = (false, false, false);
let mut block = ""; // "", "login", "ssh", "windows"
for raw in text.lines() {
let line = raw.split('#').next().unwrap_or("");
if line.trim().is_empty() {
@@ -56,13 +80,19 @@ impl Creds {
Some((k, v)) => (k.trim().to_string(), unquote(v.trim())),
None => continue,
};
if k == "login" && v.is_empty() {
in_login = true;
have_login = true;
// Enter a nested block (header line with empty value).
if v.is_empty() && !indented {
block = match k.as_str() {
"login" => { have_login = true; "login" }
"ssh" => { have_ssh = true; "ssh" }
"windows" | "win" | "ad" => { have_win = true; "windows" }
_ => "",
};
continue;
}
if in_login && indented {
match k.as_str() {
if indented {
match block {
"login" => match k.as_str() {
"url" => login.url = v,
"method" => login.method = v.to_uppercase(),
"username_field" => login.username_field = v,
@@ -71,10 +101,28 @@ impl Creds {
"password" | "pass" => login.password = v,
"success" => login.success = v,
_ => {}
},
"ssh" => match k.as_str() {
"host" | "ip" => ssh.host = v,
"port" => ssh.port = v,
"user" | "username" => ssh.user = v,
"password" | "pass" => ssh.password = v,
"key" | "keyfile" | "identity" => ssh.key = v,
_ => {}
},
"windows" => match k.as_str() {
"host" | "ip" => win.host = v,
"user" | "username" => win.user = v,
"password" | "pass" => win.password = v,
"domain" => win.domain = v,
"hash" | "ntlm" => win.hash = v,
_ => {}
},
_ => {}
}
continue;
}
in_login = false;
block = "";
match k.as_str() {
"jwt" | "token" => c.jwt = Some(v),
"header" => c.header = Some(v),
@@ -82,15 +130,37 @@ impl Creds {
_ => {}
}
}
if have_login && !login.url.is_empty() {
c.login = Some(login);
}
if c.jwt.is_none() && c.header.is_none() && c.cookie.is_none() && c.login.is_none() {
if have_login && !login.url.is_empty() { c.login = Some(login); }
if have_ssh && !ssh.host.is_empty() { c.ssh = Some(ssh); }
if have_win && !win.host.is_empty() { c.win = Some(win); }
if c.jwt.is_none() && c.header.is_none() && c.cookie.is_none()
&& c.login.is_none() && c.ssh.is_none() && c.win.is_none() {
return None;
}
Some(c)
}
/// A directive describing the host credentials available to the agents, so
/// they can authenticate to Linux (SSH) / Windows (AD) hosts.
pub fn host_instruction(&self) -> Option<String> {
let mut s = String::new();
if let Some(h) = &self.ssh {
let auth = if !h.key.is_empty() { format!("private key {}", h.key) } else { "password (provided)".into() };
s.push_str(&format!(
"SSH ACCESS (Linux): host {}:{} as user '{}' via {}. Use `ssh`/`sshpass` to run \
enumeration and privilege-escalation checks on the host.\n",
h.host, h.port, h.user, auth));
}
if let Some(w) = &self.win {
let auth = if !w.hash.is_empty() { "NTLM hash (pass-the-hash)".to_string() } else { "password".into() };
s.push_str(&format!(
"WINDOWS/AD ACCESS: host {} domain '{}' as user '{}' via {}. Use tools like \
crackmapexec/netexec, impacket, evil-winrm, bloodhound-python for host and AD checks.\n",
w.host, if w.domain.is_empty() { "(workgroup)" } else { &w.domain }, w.user, auth));
}
if s.is_empty() { None } else { Some(s) }
}
/// The auth material to send with each request, as a header line.
pub fn auth_header(&self) -> Option<String> {
if let Some(h) = &self.header {
@@ -0,0 +1,87 @@
//! Verification / grounding engine (v3.5.4).
//!
//! Hard rule: **no claim enters the world model without a tool receipt** — raw
//! tool output, not the LLM's paraphrase. This is the empirical anti-hallucination
//! anchor that complements the POMDP belief gate:
//!
//! - **Black-box**: grounding is empirical — the finding's evidence must look
//! like raw tool output (an HTTP response, an OOB callback, an error oracle),
//! not prose.
//! - **White-box**: grounding is symbolic — a file:line reference into the
//! reviewed source (reachability/taint), checked against the collected context.
//!
//! Ungrounded claims are flagged (`receipt_missing`) so the reward layer can
//! penalize them (the "claim without receipt" term).
use crate::types::Finding;
/// Verdict of grounding a single finding.
pub struct Grounded {
pub ok: bool,
pub kind: &'static str, // "empirical" | "symbolic" | "missing"
pub reason: String,
}
/// Markers that suggest the evidence is a real tool receipt rather than prose.
fn looks_empirical(evidence: &str) -> bool {
let e = evidence.to_lowercase();
let markers = [
"http/", "status", "200", "301", "302", "401", "403", "500",
"set-cookie", "location:", "content-type", "<html", "<script",
"server:", "x-", "alert(", "uid=", "root:", "sql", "error", "stack",
"callback", "oob", "collaborator", "$ ", "# ", "curl", "nmap",
];
evidence.len() >= 24 && markers.iter().filter(|m| e.contains(*m)).count() >= 2
}
/// White-box: evidence should reference a source location present in `context`.
fn looks_symbolic(f: &Finding, context: &str) -> bool {
// endpoint like file.ext:line, and the file appears in the reviewed source.
let loc = &f.endpoint;
if let Some((file, _)) = loc.rsplit_once(':') {
let base = file.rsplit('/').next().unwrap_or(file);
if !base.is_empty() && context.contains(base) {
return true;
}
}
// or the evidence quotes code that is actually in the context
!f.evidence.trim().is_empty()
&& f.evidence.split_whitespace().take(6).collect::<Vec<_>>().join(" ")
.split_whitespace()
.filter(|t| t.len() > 4 && context.contains(*t))
.count()
>= 2
}
/// Ground a finding. `context` is the reviewed source for white-box (empty for
/// black-box). Returns whether it has a valid receipt and of what kind.
pub fn ground(f: &Finding, context: &str, whitebox: bool) -> Grounded {
if whitebox && !context.is_empty() {
if looks_symbolic(f, context) {
return Grounded { ok: true, kind: "symbolic", reason: "source location/quote matches reviewed code".into() };
}
return Grounded { ok: false, kind: "missing", reason: "no source reference into reviewed code".into() };
}
if looks_empirical(&f.evidence) {
Grounded { ok: true, kind: "empirical", reason: "evidence resembles raw tool output".into() }
} else {
Grounded { ok: false, kind: "missing", reason: "evidence is paraphrase, not a tool receipt".into() }
}
}
/// Apply the grounding gate to a finding set. Ungrounded findings are flagged
/// (receipt recorded in `votes`) and demoted to unvalidated so they never get
/// reported as confirmed. Returns (kept, demoted_count).
pub fn gate(mut findings: Vec<Finding>, context: &str, whitebox: bool) -> (Vec<Finding>, usize) {
let mut demoted = 0;
for f in findings.iter_mut() {
let g = ground(f, context, whitebox);
if !g.ok {
f.validated = false;
f.votes = format!("{} · receipt_missing", f.votes);
demoted += 1;
}
}
findings.retain(|f| f.validated);
(findings, demoted)
}
@@ -0,0 +1,186 @@
//! Report-hygiene & exploitation-depth pass (v3.5.2).
//!
//! Encodes the post-engagement discipline learned from reviewing real
//! AI-pentest output, applied deterministically after validation:
//! 1. **Calibrate severity to PROVEN impact** — an unproven High/Critical
//! (hedged language, no payload, thin evidence) is capped to Medium and
//! re-titled "(potential)". No inflated severities.
//! 2. **Exposed → exploited** — flag info-disclosure / exposed-service /
//! leaked-credential findings on a host that has no actual exploit, so the
//! operator knows to *use* what was exposed (or down-rate it to a lead).
//! 3. **Consolidate hygiene** — when the same hygiene class (missing headers,
//! clickjacking, cookie flags, TLS, info-disclosure…) repeats across many
//! assets, advise merging into ONE finding with an affected-asset table,
//! instead of inflating the count one-per-host.
//!
//! All functions are pure/deterministic; only `calibrate` mutates findings
//! (severity/title/confidence). The rest return advisory strings streamed to
//! the operator and recorded with the run.
use crate::types::Finding;
fn host_of(endpoint: &str) -> String {
let s = endpoint.trim();
let s = s.split("://").last().unwrap_or(s);
let s = s.split('/').next().unwrap_or(s);
s.split('?').next().unwrap_or(s).to_lowercase()
}
fn sev_rank(s: &str) -> u8 {
match s.to_lowercase().as_str() {
x if x.starts_with("crit") => 4,
x if x.starts_with("high") => 3,
x if x.starts_with("med") => 2,
x if x.starts_with("low") => 1,
_ => 0,
}
}
fn short(s: &str) -> String {
s.chars().take(64).collect()
}
/// Hedging words that signal an impact was described but not demonstrated
/// (English + Portuguese, since engagements are bilingual).
const WEASEL: &[&str] = &[
"could ", "may ", "might ", "potential", "possible", "possibly", "teóric", "theoret",
"poderia", "possív", "potencial", "if the ", "caso o", "caso a", "would allow", "permitiria",
];
/// A finding that *exposes* something (recon/disclosure) rather than being an
/// exploit with demonstrated impact.
fn is_exposure(f: &Finding) -> bool {
let cwe = f.cwe.to_lowercase();
let t = f.title.to_lowercase();
["200", "527", "538", "942", "497", "209", "548", "16"].iter().any(|c| cwe.contains(c))
|| [
"disclosure", "exposed", "exposi", "exposure", "catalog", "catálogo", "cors",
"banner", "version", "versão", "header", "cabeçalho", ".git", "enumerat",
"fingerprint", "wsdl", "swagger", "missing security", "outdated", "eol",
]
.iter()
.any(|k| t.contains(k))
}
/// Reads as unproven: hedged or thin evidence AND no concrete payload.
fn looks_unproven(f: &Finding) -> bool {
let blob = format!("{} {} {}", f.title, f.impact, f.evidence).to_lowercase();
let hedged = WEASEL.iter().any(|w| blob.contains(w));
let weak_ev = f.evidence.trim().chars().count() < 40;
let no_payload = f.payload.trim().is_empty();
(hedged || weak_ev) && no_payload
}
/// Normalized hygiene class, for consolidation advice.
fn class_of(f: &Finding) -> &'static str {
let t = f.title.to_lowercase();
if t.contains("header") || t.contains("cabeçalho") { "missing-security-headers" }
else if t.contains("clickjack") || t.contains("frame") { "clickjacking" }
else if t.contains("hsts") || t.contains("strict-transport") { "missing-hsts" }
else if t.contains("cookie") { "cookie-flags" }
else if t.contains("tls") || t.contains("ssl") { "weak-tls" }
else if t.contains("cors") { "cors-misconfig" }
else if t.contains("version") || t.contains("versão") || t.contains("banner") || t.contains("eol") || t.contains("outdated") { "version-disclosure" }
else { "information-disclosure" }
}
/// Cap inflated, unproven High/Critical findings to Medium. Returns advisories.
pub fn calibrate(findings: &mut [Finding]) -> Vec<String> {
let mut notes = Vec::new();
for f in findings.iter_mut() {
if sev_rank(&f.severity) >= 3 && looks_unproven(f) {
let old = f.severity.clone();
f.severity = "Medium".into();
f.confidence = f.confidence.min(0.5);
let low = f.title.to_lowercase();
if !low.contains("potential") && !low.contains("potencial") {
f.title = format!("{} (potential — impact not demonstrated)", f.title);
}
notes.push(format!(
"severity calibrated: \"{}\" {old} → Medium (impact not demonstrated)",
short(&f.title)
));
}
}
notes
}
/// "Exposed → exploited": exposures on a host with no real exploit get flagged.
pub fn depth_audit(findings: &[Finding]) -> Vec<String> {
let exploited: std::collections::HashSet<String> = findings
.iter()
.filter(|f| !is_exposure(f) && sev_rank(&f.severity) >= 2)
.map(|f| host_of(&f.endpoint))
.collect();
let mut notes = Vec::new();
for f in findings.iter().filter(|f| is_exposure(f)) {
if !exploited.contains(&host_of(&f.endpoint)) {
notes.push(format!(
"depth gap: \"{}\" exposed but not exploited — USE it (call the endpoint / decode the artifact / log in / hit the dev host) to prove impact, or down-rate to a lead",
short(&f.title)
));
}
}
notes.truncate(8);
notes
}
/// Advise consolidating hygiene classes that repeat across multiple assets.
pub fn hygiene_summary(findings: &[Finding]) -> Vec<String> {
use std::collections::{BTreeMap, BTreeSet};
let mut groups: BTreeMap<&'static str, BTreeSet<String>> = BTreeMap::new();
for f in findings.iter().filter(|f| is_exposure(f)) {
groups.entry(class_of(f)).or_default().insert(host_of(&f.endpoint));
}
let mut notes = Vec::new();
for (class, hosts) in groups {
if hosts.len() > 1 {
notes.push(format!(
"hygiene: '{class}' affects {} assets — consolidate into ONE finding with an affected-asset table (don't inflate the count one-per-host)",
hosts.len()
));
}
}
notes
}
#[cfg(test)]
mod tests {
use super::*;
fn f(title: &str, sev: &str, cwe: &str, ep: &str, ev: &str, payload: &str) -> Finding {
let mut x = Finding::default();
x.title = title.into(); x.severity = sev.into(); x.cwe = cwe.into();
x.endpoint = ep.into(); x.evidence = ev.into(); x.payload = payload.into();
x
}
#[test]
fn unproven_high_is_capped() {
let mut v = vec![f("Flooding DoS", "High", "CWE-770", "https://a/x", "could overload", "")];
let notes = calibrate(&mut v);
assert_eq!(v[0].severity, "Medium");
assert_eq!(notes.len(), 1);
}
#[test]
fn proven_high_is_kept() {
let mut v = vec![f("SQLi", "High", "CWE-89", "https://a/x",
"id=1' UNION SELECT version()-- returned 8.0.32 in the response body, proving injection", "1' OR '1'='1")];
calibrate(&mut v);
assert_eq!(v[0].severity, "High");
}
#[test]
fn exposure_without_exploit_flagged() {
let v = vec![f("Information Disclosure - .git exposed", "Low", "CWE-527", "https://a/.git", "leaked", "")];
assert_eq!(depth_audit(&v).len(), 1);
}
#[test]
fn exposure_with_exploit_on_same_host_not_flagged() {
let v = vec![
f("Information Disclosure - banner", "Low", "CWE-200", "https://a/x", "Server: IIS", ""),
f("SQL Injection", "High", "CWE-89", "https://a/login", "dumped users", "1'--"),
];
assert!(depth_audit(&v).is_empty());
}
}
@@ -0,0 +1,199 @@
//! External integrations (v3.5.3): GitHub / GitLab (private repos, PR/MR code
//! review, commit watching) and Jira (open one vulnerability card per finding).
//!
//! Config persists to `<project>/.neurosploit/integrations.json`. **Secrets are
//! never stored** — only the *name* of the env var holding each token is saved;
//! the value is read from the environment at use time.
use crate::types::Finding;
use anyhow::{anyhow, Result};
use serde::{Deserialize, Serialize};
use std::path::Path;
#[derive(Serialize, Deserialize, Clone)]
pub struct GithubCfg {
pub enabled: bool,
pub token_env: String, // e.g. GITHUB_TOKEN (a PAT with `repo` scope for private repos)
pub api: String, // https://api.github.com (or GHE base)
}
impl Default for GithubCfg {
fn default() -> Self { Self { enabled: false, token_env: "GITHUB_TOKEN".into(), api: "https://api.github.com".into() } }
}
#[derive(Serialize, Deserialize, Clone)]
pub struct GitlabCfg {
pub enabled: bool,
pub token_env: String, // GITLAB_TOKEN
pub base: String, // https://gitlab.com (or self-hosted)
}
impl Default for GitlabCfg {
fn default() -> Self { Self { enabled: false, token_env: "GITLAB_TOKEN".into(), base: "https://gitlab.com".into() } }
}
#[derive(Serialize, Deserialize, Clone)]
pub struct JiraCfg {
pub enabled: bool,
pub base_url: String, // https://your-org.atlassian.net
pub email_env: String, // JIRA_EMAIL
pub token_env: String, // JIRA_API_TOKEN
pub project_key: String,
pub issue_type: String, // Bug / Vulnerability / Task
}
impl Default for JiraCfg {
fn default() -> Self {
Self { enabled: false, base_url: String::new(), email_env: "JIRA_EMAIL".into(),
token_env: "JIRA_API_TOKEN".into(), project_key: String::new(), issue_type: "Bug".into() }
}
}
#[derive(Serialize, Deserialize, Clone, Default)]
pub struct Integrations {
pub github: GithubCfg,
pub gitlab: GitlabCfg,
pub jira: JiraCfg,
}
fn env(name: &str) -> Option<String> {
std::env::var(name).ok().filter(|v| !v.trim().is_empty())
}
fn client() -> reqwest::Client {
reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(30))
.build()
.unwrap_or_default()
}
impl Integrations {
pub fn path(dir: &Path) -> std::path::PathBuf { dir.join("integrations.json") }
pub fn load(dir: &Path) -> Self {
std::fs::read_to_string(Self::path(dir))
.ok()
.and_then(|t| serde_json::from_str(&t).ok())
.unwrap_or_default()
}
pub fn save(&self, dir: &Path) -> Result<()> {
std::fs::create_dir_all(dir).ok();
std::fs::write(Self::path(dir), serde_json::to_string_pretty(self)?)?;
Ok(())
}
pub fn github_token(&self) -> Option<String> { env(&self.github.token_env) }
pub fn gitlab_token(&self) -> Option<String> { env(&self.gitlab.token_env) }
/// Inject a token into an https git URL so private repos can be cloned.
/// No-op if the matching integration is off, the token env is unset, or the
/// URL doesn't match the configured host.
pub fn authed_clone_url(&self, url: &str) -> String {
if self.github.enabled {
if let Some(rest) = url.strip_prefix("https://github.com/") {
if let Some(tok) = self.github_token() {
return format!("https://x-access-token:{tok}@github.com/{rest}");
}
}
}
if self.gitlab.enabled {
let host = self.gitlab.base.trim_start_matches("https://").trim_start_matches("http://").trim_end_matches('/');
let prefix = format!("https://{host}/");
if let Some(rest) = url.strip_prefix(&prefix) {
if let Some(tok) = self.gitlab_token() {
return format!("https://oauth2:{tok}@{host}/{rest}");
}
}
}
url.to_string()
}
/// Post a comment on a GitHub PR/issue (`repo` = `owner/name`).
pub async fn github_comment(&self, repo: &str, number: u64, body: &str) -> Result<()> {
let tok = self.github_token().ok_or_else(|| anyhow!("{} not set", self.github.token_env))?;
let url = format!("{}/repos/{}/issues/{}/comments", self.github.api.trim_end_matches('/'), repo, number);
let resp = client().post(&url)
.header("User-Agent", "NeuroSploit")
.header("Accept", "application/vnd.github+json")
.bearer_auth(tok)
.json(&serde_json::json!({ "body": body }))
.send().await?;
if !resp.status().is_success() {
return Err(anyhow!("github comment failed: {} {}", resp.status(), resp.text().await.unwrap_or_default()));
}
Ok(())
}
/// Latest commit SHA of a branch via the GitHub API (for `watch`).
pub async fn github_latest_sha(&self, repo: &str, branch: &str) -> Result<String> {
let url = format!("{}/repos/{}/commits/{}", self.github.api.trim_end_matches('/'), repo, branch);
let mut req = client().get(&url)
.header("User-Agent", "NeuroSploit")
.header("Accept", "application/vnd.github+json");
if let Some(t) = self.github_token() { req = req.bearer_auth(t); }
let resp = req.send().await?;
if !resp.status().is_success() {
return Err(anyhow!("github commits API {}: {}", resp.status(), resp.text().await.unwrap_or_default()));
}
let v: serde_json::Value = resp.json().await?;
v["sha"].as_str().map(|s| s.to_string()).ok_or_else(|| anyhow!("no sha in response"))
}
/// Create one Jira issue. Returns the issue key (e.g. SEC-123).
pub async fn jira_card(&self, summary: &str, description: &str) -> Result<String> {
let email = env(&self.jira.email_env).ok_or_else(|| anyhow!("{} not set", self.jira.email_env))?;
let token = env(&self.jira.token_env).ok_or_else(|| anyhow!("{} not set", self.jira.token_env))?;
if self.jira.base_url.is_empty() || self.jira.project_key.is_empty() {
return Err(anyhow!("jira base_url/project_key not configured (run /integrations setup jira)"));
}
let url = format!("{}/rest/api/2/issue", self.jira.base_url.trim_end_matches('/'));
let payload = serde_json::json!({
"fields": {
"project": { "key": self.jira.project_key },
"summary": summary,
"description": description,
"issuetype": { "name": self.jira.issue_type },
}
});
let resp = client().post(&url)
.basic_auth(email, Some(token))
.header("Accept", "application/json")
.json(&payload)
.send().await?;
let status = resp.status();
let text = resp.text().await.unwrap_or_default();
if !status.is_success() {
return Err(anyhow!("jira create failed: {} {}", status, text));
}
let v: serde_json::Value = serde_json::from_str(&text)?;
Ok(v["key"].as_str().unwrap_or("?").to_string())
}
/// Open one Jira card per finding. Returns (created keys, errors).
pub async fn jira_cards_for(&self, target: &str, findings: &[Finding]) -> (Vec<String>, Vec<String>) {
let (mut keys, mut errs) = (Vec::new(), Vec::new());
for f in findings {
let summary = format!("[{}] {}{}", f.severity, f.title, target);
let description = format!(
"*Target:* {target}\n*Severity:* {} | *CVSS:* {} | *CWE:* {}\n*Location:* {}\n\n*Impact:*\n{}\n\n*PoC / payload:*\n{{code}}{}{{code}}\n\n*Evidence:*\n{{code}}{}{{code}}\n\n*Remediation:*\n{}\n\n_Filed automatically by NeuroSploit._",
f.severity, f.cvss, f.cwe, f.endpoint, f.impact, f.payload, f.evidence, f.remediation
);
match self.jira_card(&summary, &description).await {
Ok(k) => keys.push(k),
Err(e) => errs.push(format!("{}: {e}", f.title)),
}
}
(keys, errs)
}
/// Human-readable status (for `/integrations` and the CLI).
pub fn status_lines(&self) -> Vec<String> {
let badge = |on: bool, tok: bool| if !on { "off".to_string() }
else if tok { "on ✓ token".to_string() } else { "on ⚠ token env not set".to_string() };
vec![
format!("github : {:<18} (clone private repos · PR review · watch) env={}", badge(self.github.enabled, self.github_token().is_some()), self.github.token_env),
format!("gitlab : {:<18} (clone private repos · MR review) env={}", badge(self.gitlab.enabled, self.gitlab_token().is_some()), self.gitlab.token_env),
format!("jira : {:<18} (open a card per finding) project={} base={}",
badge(self.jira.enabled, env(&self.jira.token_env).is_some()),
if self.jira.project_key.is_empty() { "-" } else { &self.jira.project_key },
if self.jira.base_url.is_empty() { "-" } else { &self.jira.base_url }),
]
}
}
+7 -2
View File
@@ -1,4 +1,4 @@
//! NeuroSploit v3.5.0 harness — a robust multi-model runtime for the
//! NeuroSploit v3.5.4 harness — a robust multi-model runtime for the
//! markdown-driven autonomous pentest engine.
//!
//! The harness loads the `agents_md/` library, drives a *pool* of LLM models
@@ -8,7 +8,12 @@
pub mod agents;
pub mod attack_graph;
pub mod belief;
pub mod creds;
pub mod grounding;
pub mod hygiene;
pub mod integrations;
pub mod pomdp;
pub mod models;
pub mod pipeline;
pub mod pool;
@@ -21,7 +26,7 @@ pub use models::{
cli_binary_for, ensure_playwright_mcp, installed_cli_backends, mcp_supported, provider_for,
providers, write_mcp_config, ChatClient, ModelRef, Provider,
};
pub use pipeline::{run_greybox, run_whitebox, RunOutput};
pub use pipeline::{run_greybox, run_host, run_whitebox, RunOutput};
pub use pipeline::run;
pub use pool::{ModelPool, Task};
pub use types::{Finding, RunConfig};
+53 -7
View File
@@ -42,8 +42,19 @@ pub fn providers() -> Vec<Provider> {
models: vec!["llama-3.3-70b-versatile", "qwen-2.5-coder-32b"] },
Provider { key: "together", label: "Together AI", base_url: "https://api.together.xyz/v1", env_key: "TOGETHER_API_KEY", kind: "api",
models: vec!["Qwen/Qwen2.5-Coder-32B-Instruct", "deepseek-ai/DeepSeek-R1", "meta-llama/Llama-3.3-70B-Instruct-Turbo"] },
// LiteLLM proxy (OpenAI-compatible). Point at your gateway with
// LITELLM_BASE_URL (default http://localhost:4000/v1); key = LITELLM_API_KEY.
// Use `litellm:<any-model-the-proxy-routes>` — model names pass through.
Provider { key: "litellm", label: "LiteLLM (proxy)", base_url: "http://localhost:4000/v1", env_key: "LITELLM_API_KEY", kind: "api",
models: vec!["gpt-4o", "claude-3-7-sonnet", "gemini/gemini-2.5-pro"] },
Provider { key: "openrouter", label: "OpenRouter", base_url: "https://openrouter.ai/api/v1", env_key: "OPENROUTER_API_KEY", kind: "api",
models: vec!["anthropic/claude-opus-4-8", "qwen/qwen-2.5-coder-32b-instruct", "deepseek/deepseek-r1", "meta-llama/llama-3.3-70b-instruct"] },
// Azure OpenAI (OpenAI-compatible). Set AZURE_OPENAI_ENDPOINT (e.g.
// https://<resource>.openai.azure.com), optionally AZURE_OPENAI_API_VERSION
// (default 2024-10-21), and use `azure:<your-deployment-name>` as the model.
// base_url is resolved from the endpoint at call time; auth uses an api-key header.
Provider { key: "azure", label: "Azure OpenAI", base_url: "", env_key: "AZURE_OPENAI_API_KEY", kind: "api",
models: vec!["gpt-4o", "gpt-4o-mini", "gpt-5.1", "o4-mini"] },
Provider { key: "ollama", label: "Ollama (local)", base_url: "http://localhost:11434/v1", env_key: "OLLAMA_API_KEY", kind: "api",
models: vec!["qwen2.5-coder:32b", "qwq:32b", "deepseek-r1:32b", "llama3.3:70b"] },
]
@@ -53,6 +64,17 @@ pub fn provider_for(key: &str) -> Option<Provider> {
providers().into_iter().find(|p| p.key == key)
}
/// Resolve a provider's API key from the environment, honoring common aliases.
/// For Gemini we also accept `GOOGLE_API_KEY` (Google's standard env var name)
/// when `GEMINI_API_KEY` is unset.
fn resolve_key(p: &Provider) -> String {
let mut k = std::env::var(p.env_key).unwrap_or_default();
if k.is_empty() && p.key == "gemini" {
k = std::env::var("GOOGLE_API_KEY").unwrap_or_default();
}
k
}
/// A `provider:model` selection.
#[derive(Clone, Debug)]
pub struct ModelRef {
@@ -92,11 +114,32 @@ impl ChatClient {
pub async fn chat(&self, m: &ModelRef, system: &str, user: &str) -> Result<String> {
let p = provider_for(&m.provider)
.ok_or_else(|| anyhow!("unknown provider '{}'", m.provider))?;
let key = std::env::var(p.env_key).unwrap_or_default();
if key.is_empty() && p.key != "ollama" {
return Err(anyhow!("no API key ({}) for provider '{}'", p.env_key, p.key));
let key = resolve_key(&p);
if key.is_empty() && p.key != "ollama" && p.key != "litellm" {
let hint = if p.key == "gemini" { format!("{} (or GOOGLE_API_KEY)", p.env_key) } else { p.env_key.to_string() };
return Err(anyhow!("no API key ({}) for provider '{}'", hint, p.key));
}
let url = format!("{}/chat/completions", p.base_url.trim_end_matches('/'));
// Azure OpenAI uses a per-resource endpoint + deployment + api-version,
// and authenticates with an `api-key` header instead of Bearer.
let azure = p.key == "azure";
let url = if azure {
let endpoint = std::env::var("AZURE_OPENAI_ENDPOINT").unwrap_or_default();
if endpoint.is_empty() {
return Err(anyhow!("set AZURE_OPENAI_ENDPOINT (e.g. https://<resource>.openai.azure.com) for the azure provider"));
}
let ver = std::env::var("AZURE_OPENAI_API_VERSION").unwrap_or_else(|_| "2024-10-21".to_string());
// `model` is the Azure DEPLOYMENT name (use `azure:<deployment>`).
format!("{}/openai/deployments/{}/chat/completions?api-version={}",
endpoint.trim_end_matches('/'), m.model, ver)
} else {
// Allow an env base-URL override (LiteLLM gateway, self-hosted proxies, …).
let base = match p.key {
"litellm" => std::env::var("LITELLM_BASE_URL").unwrap_or_else(|_| p.base_url.to_string()),
"ollama" => std::env::var("OLLAMA_BASE_URL").unwrap_or_else(|_| p.base_url.to_string()),
_ => p.base_url.to_string(),
};
format!("{}/chat/completions", base.trim_end_matches('/'))
};
let body = serde_json::json!({
"model": m.model,
"max_tokens": 4096,
@@ -108,7 +151,7 @@ impl ChatClient {
});
let mut req = self.http.post(&url).json(&body);
if !key.is_empty() {
req = req.bearer_auth(&key);
if azure { req = req.header("api-key", &key); } else { req = req.bearer_auth(&key); }
}
let resp = req.send().await?;
let status = resp.status();
@@ -416,9 +459,12 @@ impl Default for ChatClient {
}
fn truncate(s: &str, n: usize) -> String {
if s.len() <= n {
// Truncate by CHARACTERS, never bytes — slicing `&s[..n]` panics when `n`
// lands inside a multi-byte char (e.g. '—'). That panic was crashing agent
// tasks and silently dropping their findings.
if s.chars().count() <= n {
s.to_string()
} else {
format!("{}", &s[..n])
format!("{}", s.chars().take(n).collect::<String>())
}
}
+362 -49
View File
@@ -60,7 +60,11 @@ fn tool_doctrine(mcp_on: bool) -> String {
Use only what is installed; degrade gracefully. Never run destructive or DoS actions.\n\n"
)
}
const VOTE_SYS: &str = "You are an adversarial security validator. Decide if the candidate finding is a REAL, reproducible, exploitable vulnerability with proof. Reply with JSON {\"verdict\":\"confirmed\"|\"rejected\",\"reason\":\"...\"}. Default to rejected when uncertain.";
const VOTE_SYS: &str = "You are an adversarial security validator. Decide if the candidate finding is a REAL, reproducible, exploitable vulnerability whose EVIDENCE actually proves impact. Reject common false positives: input merely reflected but not executed; version/banner guesses with no working PoC; self-XSS; theoretical issues; an error message or stack trace mistaken for injection; missing, generic, or non-reproducible evidence; severity inflated beyond what the evidence demonstrates. Confirm only if the provided evidence (request/response) concretely proves the vulnerability. Reply with JSON {\"verdict\":\"confirmed\"|\"rejected\",\"reason\":\"...\"}. Default to rejected when uncertain.";
/// Adversarial second pass for High/Critical findings: assume false positive
/// until the evidence forces otherwise. A finding that can't withstand the
/// skeptics is dropped.
const REFUTE_SYS: &str = "You are a skeptical senior reviewer trying to DISPROVE a reported vulnerability. Assume it is a FALSE POSITIVE unless the evidence forces otherwise. Scrutinize: does the evidence PROVE execution/impact, or only that input was reflected/accepted? Is there a real working PoC, or just a version/banner/theory? Could it be self-XSS, an error message, or an unreachable path? Reply JSON {\"verdict\":\"confirmed\"|\"rejected\",\"reason\":\"...\"} where confirmed means the vulnerability is REAL and proven by the evidence. When in doubt, reject.";
const CODE_VOTE_SYS: &str = "You are an adversarial source-code reviewer. Decide if the reported issue is a REAL vulnerability in the provided code (reachable, exploitable, not a false positive). Reply JSON {\"verdict\":\"confirmed\"|\"rejected\",\"reason\":\"...\"}.";
/// ReAct loop directive: make the agent reason → act with a tool → observe →
@@ -69,6 +73,16 @@ const REACT_DOCTRINE: &str = "METHOD (ReAct): work in explicit Thought → Actio
Each Action runs ONE concrete tool command (e.g. a curl request); read its real Observation before the next Thought. \
Base every claim on an actual observed response never assume. Stop when you've either proven an issue or exhausted reasonable checks. Be token-efficient: no filler, no repetition.\n\n";
/// DEPTH doctrine (v3.5.2): push past detection to demonstrated impact, and
/// chain. Distilled from reviewing real AI-pentest output that kept stopping at
/// "exposed" instead of "exploited".
const DEPTH_DOCTRINE: &str = "DEPTH (exploit, don't just expose):\n\
- Exposed exploited: any info-disclosure, exposed service/catalog/WSDL, leaked credential/token, or non-prod (dev/staging) host you find MUST be USED before you report it call the exposed endpoint, decode the leaked artifact, log in with the leaked credential, hit the dev host. If you only observed it but never used it, report it as a LEAD (low confidence), not a confirmed finding.\n\
- Chain across steps: reuse any session/JWT/cookie/credential you obtain in one step against every other module; if one bug yields access, pivot it into IDOR/privesc/data-exfil and report the CHAIN, not isolated parts.\n\
- Decode & fingerprint CVE: decode opaque tokens/paths (base64/JSON/marshal) and fingerprint the stack (server, framework, library/gem/plugin versions); map exact versions to known CVEs and attempt a safe, non-destructive PoC.\n\
- Audit tokens: for any JWT, check alg-confusion (RSHS), alg:none, kid/jku injection, whether the signature is actually verified, and weak/guessable HS256 secrets.\n\
- Calibrate honestly: claim High/Critical ONLY when impact is DEMONSTRATED; unproven DoS/abuse is Low/Info or a lead, never inflated.\n\n";
/// Black-box web engagement: recon → parallel exploit → N-model vote → report.
pub async fn run(cfg: RunConfig, lib: &Library, pool: &ModelPool, tx: Sender<String>) -> RunOutput {
pool.set_progress(tx.clone());
@@ -159,7 +173,7 @@ pub async fn run(cfg: RunConfig, lib: &Library, pool: &ModelPool, tx: Sender<Str
let directives = directives.clone();
let txc = tx.clone();
async move {
if pool.is_cancelled() {
if pool.stop_exploiting() {
return (ag.name.clone(), String::new(), vec![]);
}
if verbose {
@@ -168,12 +182,13 @@ pub async fn run(cfg: RunConfig, lib: &Library, pool: &ModelPool, tx: Sender<Str
let user = format!(
"AUTHORIZED engagement — you have explicit permission to test {target}. \
Do not ask for confirmation proceed and PROVE each issue.\n\n\
{directives}{react}{doctrine}{body}\n\nWhen done, reply with ONLY a JSON array of confirmed findings (may be empty []). \
{directives}{react}{depth}{doctrine}{body}\n\nWhen done, reply with ONLY a JSON array of confirmed findings (may be empty []). \
Each item: {{id,title,severity,cwe,endpoint,payload,evidence,impact,remediation,confidence}}. \
`evidence` must contain the concrete proof (request/response excerpt).",
target = target,
directives = directives,
react = REACT_DOCTRINE,
depth = DEPTH_DOCTRINE,
doctrine = tool_doctrine(mcp_on),
body = ag.user.replace("{target}", &target).replace("{recon_json}", &recon),
);
@@ -181,6 +196,11 @@ pub async fn run(cfg: RunConfig, lib: &Library, pool: &ModelPool, tx: Sender<Str
Ok((m, text)) => {
let f = extract_findings(&text, &ag.name);
let _ = txc.send(format!("exploit {} via {}{} candidate(s)", ag.name, m.label(), f.len())).await;
// Live findings feed: surface each candidate the moment it appears.
for c in &f {
let _ = txc.send(format!("finding: [{}] {} @ {}", c.severity, c.title, c.endpoint)).await;
if let Ok(j) = serde_json::to_string(c) { let _ = txc.send(format!("finding_json: {j}")).await; }
}
(ag.name.clone(), text, f)
}
Err(e) => {
@@ -201,14 +221,11 @@ pub async fn run(cfg: RunConfig, lib: &Library, pool: &ModelPool, tx: Sender<Str
// ---- 4. Validate by N-model voting ---------------------------------
let mut findings = validate(candidates, pool, VOTE_SYS, cfg.vote_n, &tx).await;
// ---- 5. Chain confirmed findings into deeper impact ----------------
let chained = chain_round(pool, &cfg.target, &recon, &operator_directives(&cfg), &findings, &tx).await;
if !chained.is_empty() {
let extra = validate(dedup_findings(chained), pool, VOTE_SYS, cfg.vote_n, &tx).await;
let _ = tx.send(format!("chaining added {} validated finding(s)", extra.len())).await;
findings.extend(extra);
// ---- 5. Attack chaining: multi-round post-exploitation pivots ------
let chained = attack_chain(pool, &cfg, &recon, &findings, &lib.chains, &tx).await;
findings.extend(chained);
findings = dedup_findings(findings);
}
let findings = refute_pass(findings, pool, cfg.vote_n, &tx).await;
finish(cfg, lib, recon, transcript, findings, selected, &mut rl, tx).await
}
@@ -270,6 +287,7 @@ pub async fn run_whitebox(cfg: RunConfig, lib: &Library, pool: &ModelPool, tx: S
let candidates = dedup_findings(raw.iter().flat_map(|(_, _, f)| f.clone()).collect());
let _ = tx.send(format!("{} candidate finding(s) (deduped) — validating", candidates.len())).await;
let findings = validate(candidates, pool, CODE_VOTE_SYS, cfg.vote_n, &tx).await;
let findings = refute_pass(findings, pool, cfg.vote_n, &tx).await;
finish(cfg, lib, "{}".into(), transcript, findings, selected, &mut rl, tx).await
}
@@ -374,7 +392,7 @@ pub async fn run_greybox(cfg: RunConfig, lib: &Library, pool: &ModelPool, tx: Se
let leads = leads_ctx.clone();
let txc = tx.clone();
async move {
if pool.is_cancelled() {
if pool.stop_exploiting() {
return (ag.name.clone(), String::new(), vec![]);
}
if verbose {
@@ -382,11 +400,11 @@ pub async fn run_greybox(cfg: RunConfig, lib: &Library, pool: &ModelPool, tx: Se
}
let user = format!(
"AUTHORIZED greybox engagement on {target} — you also have the source review below. \
Proceed and PROVE each issue against the LIVE app.\n\n{directives}{leads}{react}{doctrine}{body}\n\n\
Proceed and PROVE each issue against the LIVE app.\n\n{directives}{leads}{react}{depth}{doctrine}{body}\n\n\
Reply ONLY a JSON array of confirmed findings (may be []): \
{{id,title,severity,cwe,endpoint,payload,evidence,impact,remediation,confidence}}.",
target = target, directives = directives, leads = leads,
react = REACT_DOCTRINE, doctrine = tool_doctrine(mcp_on),
react = REACT_DOCTRINE, depth = DEPTH_DOCTRINE, doctrine = tool_doctrine(mcp_on),
body = ag.user.replace("{target}", &target).replace("{recon_json}", &recon),
);
match pool.complete_routed(Task::Exploit, &ag.name, &ag.system, &user).await {
@@ -405,46 +423,172 @@ pub async fn run_greybox(cfg: RunConfig, lib: &Library, pool: &ModelPool, tx: Se
let candidates = dedup_findings(raw.iter().flat_map(|(_, _, f)| f.clone()).collect());
let _ = tx.send(format!("{} candidate finding(s) (deduped) — validating", candidates.len())).await;
let mut findings = validate(candidates, pool, VOTE_SYS, cfg.vote_n, &tx).await;
let chained = chain_round(pool, &cfg.target, &recon, &operator_directives(&cfg), &findings, &tx).await;
if !chained.is_empty() {
let extra = validate(dedup_findings(chained), pool, VOTE_SYS, cfg.vote_n, &tx).await;
let _ = tx.send(format!("chaining added {} validated finding(s)", extra.len())).await;
findings.extend(extra);
let chained = attack_chain(pool, &cfg, &recon, &findings, &lib.chains, &tx).await;
findings.extend(chained);
findings = dedup_findings(findings);
}
let findings = refute_pass(findings, pool, cfg.vote_n, &tx).await;
finish(cfg, lib, recon, transcript, findings, selected, &mut rl, tx).await
}
const CHAIN_SYS: &str = "You are an exploit-chaining specialist. Given already-CONFIRMED findings, chain them into deeper impact — e.g. SSRF→cloud metadata creds, SQLi→DB dump→credential reuse, IDOR→account takeover, arbitrary file read→secrets→RCE, auth bypass→admin. Use your tools to actually carry the chain forward and PROVE the escalated impact. Report ONLY NEW findings beyond the inputs.";
const CHAIN_SYS: &str = "You are a post-exploitation & attack-chaining specialist. You are given ONE confirmed foothold plus any loot already gathered. DECIDE the most promising directions to expand from THIS foothold and pursue them with real tools: post-exploitation (loot credentials/tokens/keys/config/source), credential reuse, privilege escalation (horizontal AND vertical), lateral movement to adjacent services/hosts, data exfiltration, and reaching NEW attack surface the foothold exposes (e.g. SSRF→cloud metadata creds→IAM, SQLi→DB dump→credential reuse→admin, arbitrary file read→secrets→RCE, IDOR→account takeover, auth bypass→internal APIs). PROVE each escalated step with a real tool receipt. Report ONLY NEW findings beyond the input, plus any new loot you discovered (creds, tokens, hosts, internal endpoints) so later stages can reuse it. Authorized engagement; never destructive/DoS.";
/// One orchestration round: take the confirmed findings and try to chain them
/// into higher-impact follow-ups, reusing the recon/auth context. Returns the
/// (unvalidated) new candidate findings produced by chaining.
async fn chain_round(pool: &ModelPool, target: &str, recon: &str, directives: &str,
confirmed: &[Finding], tx: &Sender<String>) -> Vec<Finding> {
if confirmed.is_empty() {
/// Dedup / identity key for a finding (cwe|endpoint|title-prefix).
fn finding_key(f: &Finding) -> String {
format!("{}|{}|{}", f.cwe.to_lowercase(), f.endpoint.to_lowercase(),
f.title.to_lowercase().chars().take(40).collect::<String>())
}
fn sev_rank(sev: &str) -> u8 {
match sev.to_lowercase().as_str() {
x if x.starts_with("crit") => 4,
x if x.starts_with("high") => 3,
x if x.starts_with("med") => 2,
x if x.starts_with("low") => 1,
_ => 0,
}
}
/// Max footholds expanded per round (keeps token cost bounded).
const CHAIN_SEEDS_PER_ROUND: usize = 6;
/// Robust attack-chaining engine (v3.5.4): iterative, decision-driven,
/// post-exploitation pivoting. Each round takes the newest confirmed footholds,
/// and for EACH one an agent decides which directions to expand (post-ex, cred
/// reuse, privesc, lateral, exfil, new surface), proves new impact, and reports
/// new findings + **loot** (creds/tokens/hosts/endpoints). Loot is carried
/// forward so later rounds reuse it. New validated findings become the next
/// round's footholds; the loop stops at `chain_depth` rounds or when a round
/// yields nothing new (loop-until-dry). Findings are validated each round so we
/// never pivot off a false positive.
async fn attack_chain(pool: &ModelPool, cfg: &RunConfig, recon: &str,
confirmed: &[Finding], chains: &[Agent], tx: &Sender<String>) -> Vec<Finding> {
let max_rounds = cfg.chain_depth;
if max_rounds == 0 || confirmed.is_empty() || pool.stop_exploiting() {
return vec![];
}
let summary: String = confirmed.iter().take(20)
.map(|f| format!("- [{}] {} @ {} ({})", f.severity, f.title, f.endpoint, f.cwe))
.collect::<Vec<_>>().join("\n");
let _ = tx.send(format!("chaining {} confirmed finding(s) for deeper impact…", confirmed.len())).await;
let recon_ctx: String = recon.chars().take(2500).collect();
let recipes: String = chains.iter().map(|a| format!("- {}", a.title.replace(" Agent", ""))).collect::<Vec<_>>().join("\n");
let recipe_block = if recipes.is_empty() { String::new() } else { format!("KNOWN CHAIN RECIPES (apply any that fit):\n{recipes}\n\n") };
let recon_ctx: String = recon.chars().take(2000).collect();
let directives = operator_directives(cfg);
let mut all_new: Vec<Finding> = Vec::new();
let mut loot: Vec<String> = Vec::new();
let mut seen: std::collections::HashSet<String> = confirmed.iter().map(finding_key).collect();
// Frontier = footholds to expand this round; start with confirmed, best-first.
let mut frontier: Vec<Finding> = confirmed.to_vec();
frontier.sort_by(|a, b| sev_rank(&b.severity).cmp(&sev_rank(&a.severity)));
for round in 1..=max_rounds {
if pool.stop_exploiting() || frontier.is_empty() {
break;
}
let seeds: Vec<Finding> = frontier.iter().take(CHAIN_SEEDS_PER_ROUND).cloned().collect();
let _ = tx.send(format!("⛓ attack-chain round {round}/{max_rounds} — expanding {} foothold(s), {} loot item(s)", seeds.len(), loot.len())).await;
let loot_snapshot = loot.clone();
let results: Vec<(Vec<Finding>, Vec<String>)> = stream::iter(seeds.into_iter())
.map(|seed| {
let (dir, rc, rb, ls, txc) = (directives.clone(), recon_ctx.clone(), recipe_block.clone(), loot_snapshot.clone(), tx.clone());
async move { chain_from_seed(pool, &cfg.target, &dir, &rc, &rb, &seed, &ls, round, max_rounds, &txc).await }
})
.buffer_unordered(4)
.collect()
.await;
// Merge round output: accumulate loot, gather candidate findings.
let mut round_cands: Vec<Finding> = Vec::new();
for (fs, lt) in results {
for l in lt {
if !loot.iter().any(|x| x.eq_ignore_ascii_case(&l)) { loot.push(l); }
}
round_cands.extend(fs);
}
// Keep only genuinely NEW findings (unseen key).
let fresh: Vec<Finding> = dedup_findings(round_cands)
.into_iter()
.filter(|f| seen.insert(finding_key(f)))
.collect();
if fresh.is_empty() {
let _ = tx.send("⛓ no new paths this round — chain exhausted".into()).await;
break;
}
// Validate before pivoting further (don't chain off false positives).
let validated = validate(fresh, pool, VOTE_SYS, cfg.vote_n, tx).await;
let _ = tx.send(format!("⛓ round {round}: +{} validated finding(s), {} loot item(s) total", validated.len(), loot.len())).await;
if validated.is_empty() {
break;
}
all_new.extend(validated.clone());
// Next round expands the freshly-validated footholds, best-first.
frontier = validated;
frontier.sort_by(|a, b| sev_rank(&b.severity).cmp(&sev_rank(&a.severity)));
}
if !all_new.is_empty() {
let _ = tx.send(format!("⛓ attack-chaining added {} finding(s) across pivots", all_new.len())).await;
}
all_new
}
/// Expand ONE foothold: the agent decides directions, does post-exploitation and
/// pivots, and returns new findings + discovered loot.
async fn chain_from_seed(pool: &ModelPool, target: &str, directives: &str, recon_ctx: &str,
recipe_block: &str, seed: &Finding, loot: &[String],
round: usize, max: usize, tx: &Sender<String>) -> (Vec<Finding>, Vec<String>) {
if pool.stop_exploiting() {
return (vec![], vec![]);
}
let loot_block = if loot.is_empty() {
"(none yet)".to_string()
} else {
loot.iter().take(30).map(|l| format!("- {l}")).collect::<Vec<_>>().join("\n")
};
let short: String = seed.title.chars().take(28).collect();
let user = format!(
"AUTHORIZED engagement on {target}.\n\n{directives}{react}{doctrine}\
CONFIRMED FINDINGS TO CHAIN:\n{summary}\n\nRecon:\n{recon_ctx}\n\n\
Chain these into deeper impact and PROVE it. Reply ONLY a JSON array of NEW findings \
(may be []): {{id,title,severity,cwe,endpoint,payload,evidence,impact,remediation,confidence}}.",
react = REACT_DOCTRINE, doctrine = tool_doctrine(pool.mcp_config.is_some()),
"AUTHORIZED engagement on {target}.\n\n{directives}{react}{depth}{doctrine}\
FOOTHOLD TO EXPAND (round {round}/{max}):\n- [{}] {} @ {} ({})\n payload: {}\n evidence: {}\n\n\
LOOT GATHERED (reuse it):\n{loot_block}\n\n{recipe_block}RECON:\n{recon_ctx}\n\n\
From THIS foothold, DECIDE the best directions and PROVE new impact post-exploitation (loot creds/keys/config/source), credential reuse, privilege escalation (horizontal & vertical), lateral movement to adjacent services/hosts, data exfiltration, and NEW attack surface it exposes. Every claim needs a real tool receipt.\n\n\
Reply ONLY JSON: {{\"findings\":[{{id,title,severity,cwe,endpoint,payload,evidence,impact,remediation,confidence}}],\"loot\":[\"cred:user:pass@host\",\"token:...\",\"host:10.0.0.5\",\"endpoint:/internal/api\"]}} (empty arrays are fine).",
seed.severity, seed.title, seed.endpoint, seed.cwe, seed.payload, seed.evidence,
react = REACT_DOCTRINE, depth = DEPTH_DOCTRINE, doctrine = tool_doctrine(pool.mcp_config.is_some()),
);
match pool.complete_routed(Task::Exploit, "chain", CHAIN_SYS, &user).await {
let label = format!("chain:{short}");
match pool.complete_routed(Task::Exploit, &label, CHAIN_SYS, &user).await {
Ok((m, text)) => {
let f = extract_findings(&text, "chain");
let _ = tx.send(format!("chain via {}{} new candidate(s)", m.label(), f.len())).await;
f
let (f, lt) = extract_chain(&text, "chain");
if !f.is_empty() || !lt.is_empty() {
let _ = tx.send(format!("chain[{short}] via {}{} new finding(s), {} loot", m.label(), f.len(), lt.len())).await;
}
Err(e) => { let _ = tx.send(format!("chaining failed: {e}")).await; vec![] }
(f, lt)
}
Err(e) => {
let _ = tx.send(format!("chain[{short}] failed: {e}")).await;
(vec![], vec![])
}
}
}
/// Parse a chain agent reply into (new findings, loot). Accepts the object form
/// `{"findings":[...],"loot":[...]}` and falls back to a bare findings array.
fn extract_chain(text: &str, agent: &str) -> (Vec<Finding>, Vec<String>) {
if let (Some(a), Some(b)) = (text.find('{'), text.rfind('}')) {
if b > a {
if let Ok(serde_json::Value::Object(o)) = serde_json::from_str::<serde_json::Value>(&text[a..=b]) {
if o.contains_key("findings") {
let findings = o.get("findings").map(|v| extract_findings(&v.to_string(), agent)).unwrap_or_default();
let loot = o.get("loot").and_then(|v| v.as_array())
.map(|arr| arr.iter().filter_map(|x| x.as_str().map(|s| s.to_string())).collect())
.unwrap_or_default();
return (findings, loot);
}
}
}
}
(extract_findings(text, agent), vec![])
}
// --------------------------------------------------------------------------- shared
@@ -583,11 +727,11 @@ async fn validate(candidates: Vec<Finding>, pool: &ModelPool, sys: &str, vote_n:
let finder = finder.clone();
async move {
let q = format!(
"Finding: {} | severity {} | {} | at {} | payload {} | evidence {}",
f.title, f.severity, f.cwe, f.endpoint, f.payload, f.evidence
"Finding: {} | severity {} | {} | at {} | payload {} | evidence {} | impact {}",
f.title, f.severity, f.cwe, f.endpoint, f.payload, f.evidence, f.impact
);
let (yes, total) = pool.vote(sys, &q, vote_n, finder.as_deref()).await;
f.validated = total > 0 && yes * 2 >= total;
f.validated = crate::pool::quorum_confirmed(&f.severity, yes, total);
f.votes = format!("{yes}/{total}");
if f.confidence == 0.0 && total > 0 {
f.confidence = yes as f64 / total as f64;
@@ -602,8 +746,74 @@ async fn validate(candidates: Vec<Finding>, pool: &ModelPool, sys: &str, vote_n:
validated.into_iter().filter(|f| f.validated).collect()
}
/// Adversarial refutation pass: every confirmed **High/Critical** finding is
/// re-examined by a skeptical panel that tries to prove it's a false positive.
/// A finding that fails to withstand a majority of skeptics is dropped. Lower
/// severities pass through unchanged. Runs only when a real panel exists.
async fn refute_pass(findings: Vec<Finding>, pool: &ModelPool, vote_n: usize, tx: &Sender<String>) -> Vec<Finding> {
let finder = pool.candidates.first().map(|m| m.label());
let mut kept = Vec::new();
for mut f in findings {
let s = f.severity.to_lowercase();
let high = s.starts_with("crit") || s.starts_with("high");
if !high || pool.stop_exploiting() {
kept.push(f);
continue;
}
let q = format!(
"Finding: {} | severity {} | {} | at {} | payload {} | evidence {} | impact {}",
f.title, f.severity, f.cwe, f.endpoint, f.payload, f.evidence, f.impact
);
let (yes, total) = pool.vote(REFUTE_SYS, &q, vote_n.max(2), finder.as_deref()).await;
// Survive on no-response (infra failure) or a surviving majority.
let survives = total == 0 || yes * 2 > total;
if survives {
if total > 0 { f.votes = format!("{} · refute {yes}/{total}", f.votes); }
kept.push(f);
} else {
let _ = tx.send(format!("vote {} → dropped by adversarial refute ({yes}/{total})", f.title)).await;
}
}
kept
}
async fn finish(cfg: RunConfig, _lib: &Library, recon: String, transcript: String, mut findings: Vec<Finding>,
selected: Vec<Agent>, rl: &mut RlState, tx: Sender<String>) -> RunOutput {
// --- Grounding gate: no claim without a tool receipt (anti-hallucination) ---
// White/grey carry source context; black-box is verified empirically.
let whitebox = cfg.repo.is_some() && cfg.target.starts_with('/');
let before = findings.len();
let (kept, demoted) = crate::grounding::gate(findings, &transcript, whitebox);
findings = kept;
if demoted > 0 {
let _ = tx.send(format!("grounding gate: demoted {demoted}/{before} ungrounded claim(s) (no tool receipt)")).await;
}
// --- v3.5.2 report-hygiene & exploitation-depth pass ---
// Calibrate inflated/unproven High-Critical to Medium, flag exposures that
// were never exploited ("exposed → exploited"), and advise consolidating
// hygiene findings duplicated across many assets.
for n in crate::hygiene::calibrate(&mut findings) {
let _ = tx.send(format!("calibrate: {n}")).await;
}
for n in crate::hygiene::depth_audit(&findings) {
let _ = tx.send(format!("notify: {n}")).await;
}
for n in crate::hygiene::hygiene_summary(&findings) {
let _ = tx.send(format!("notify: {n}")).await;
}
// --- POMDP belief: build from grounded findings, report residual uncertainty ---
let mut wm = crate::belief::WorldModel::new();
wm.deterministic = whitebox;
for f in &findings {
wm.add(&f.id, crate::belief::Kind::Exploit, &f.title, f.confidence.max(0.05).min(0.99));
}
let unc = wm.uncertainty(None);
if !findings.is_empty() {
let _ = tx.send(format!("belief uncertainty over confirmed findings: {:.2} (0=sharp,1=diffuse)", unc)).await;
}
let _ = tx.send(format!("{} validated finding(s)", findings.len())).await;
// Map findings to OWASP / MITRE / kill-chain stage for the attack graph.
crate::attack_graph::enrich(&mut findings);
@@ -626,8 +836,17 @@ async fn finish(cfg: RunConfig, _lib: &Library, recon: String, transcript: Strin
let artifacts = persist(&cfg, &recon, &transcript, &findings);
if !artifacts.is_empty() {
let _ = tx.send(format!("notify: evidence saved → {}", cfg.workdir.clone().unwrap_or_default())).await;
let _ = tx.send(format!("artifacts saved: {}", artifacts.join(", "))).await;
}
// Automatic partial summary (phase complete).
{
let mut by: std::collections::BTreeMap<&str, usize> = Default::default();
for f in &findings { *by.entry(f.severity.as_str()).or_insert(0) += 1; }
let sev = if by.is_empty() { "none".to_string() }
else { by.iter().map(|(k, v)| format!("{k}:{v}")).collect::<Vec<_>>().join(" ") };
let _ = tx.send(format!("notify: phase complete — {} validated finding(s) [{}]", findings.len(), sev)).await;
}
RunOutput {
target: cfg.target.clone(),
@@ -775,13 +994,7 @@ fn conf(v: Option<&serde_json::Value>) -> f64 {
fn dedup_findings(mut v: Vec<Finding>) -> Vec<Finding> {
v.sort_by(|a, b| b.confidence.partial_cmp(&a.confidence).unwrap_or(std::cmp::Ordering::Equal));
let mut seen = std::collections::HashSet::new();
v.into_iter()
.filter(|f| {
let key = format!("{}|{}|{}", f.cwe.to_lowercase(), f.endpoint.to_lowercase(),
f.title.to_lowercase().chars().take(40).collect::<String>());
seen.insert(key)
})
.collect()
v.into_iter().filter(|f| seen.insert(finding_key(f))).collect()
}
fn norm_sev(s: &str) -> String {
@@ -824,9 +1037,109 @@ fn collect_repo_context(root: &Path, max_files: usize, max_bytes: usize) -> Stri
let rel = path.strip_prefix(root).unwrap_or(path).to_string_lossy();
let budget = max_bytes.saturating_sub(out.len());
let take = content.len().min(budget).min(8_000);
out.push_str(&format!("\n// ===== file: {} =====\n{}\n", rel, &content[..take]));
// Char-safe slice: back off to the nearest char boundary so multibyte
// source files (UTF-8) never panic.
let mut end = take.min(content.len());
while end > 0 && !content.is_char_boundary(end) { end -= 1; }
out.push_str(&format!("\n// ===== file: {} =====\n{}\n", rel, &content[..end]));
files += 1;
}
}
out
}
const HOST_RECON_SYS: &str = "You are an infrastructure recon specialist on an AUTHORIZED engagement against a HOST/IP. Actively scan with rustscan/nmap (and netexec/smbclient where relevant) to map open ports, services, versions and auth surfaces. Use any provided SSH/Windows credentials to enumerate from inside. Do not ask permission; proceed. Reply with a compact JSON object (host, os, ports, services, auth, ad). No prose.";
const HOST_TOOLING: &str = "TOOLING (best on Kali): nmap/rustscan (ports), netexec/crackmapexec + smbclient (SMB/AD), ssh/sshpass + linpeas (Linux), evil-winrm + winPEAS + impacket (Windows), bloodhound-python/SharpHound (AD), hashcat (offline cracking). Use only supplied credentials; never brute force or run destructive/DoS actions.\n\n";
/// Infrastructure engagement: scan/enumerate an IP/host and run Linux/Windows/AD
/// agents. Mirrors the web pipeline but selects from the `infra` agent set.
pub async fn run_host(cfg: RunConfig, lib: &Library, pool: &ModelPool, tx: Sender<String>) -> RunOutput {
pool.set_progress(tx.clone());
let _ = tx.send(format!("HOST · target: {} · {} infra agents · models: {}", cfg.target, lib.infra.len(),
pool.candidates.iter().map(|m| m.label()).collect::<Vec<_>>().join(", "))).await;
let recon = if cfg.offline {
"{}".to_string()
} else {
let user = format!("{}{}Target host: {}", operator_directives(&cfg), HOST_TOOLING, cfg.target);
match pool.complete_routed(Task::Recon, "recon", HOST_RECON_SYS, &user).await {
Ok((m, t)) => { let _ = tx.send(format!("recon complete via {}", m.label())).await; t }
Err(e) => { let _ = tx.send(format!("recon failed ({e})")).await; "{}".to_string() }
}
};
let mut rl = cfg.rl_path.as_ref().map(|p| RlState::load(Path::new(p))).unwrap_or_default();
let mut ranked: Vec<Agent> = lib.infra.clone();
ranked.sort_by(|a, b| rl.weight(&b.name).partial_cmp(&rl.weight(&a.name)).unwrap_or(std::cmp::Ordering::Equal));
let cap = if cfg.max_agents > 0 { cfg.max_agents.min(ranked.len()) } else { ranked.len() };
let focus = cfg.instructions.clone().unwrap_or_default();
if cfg.offline {
let selected: Vec<Agent> = ranked.into_iter().take(cap).collect();
let _ = tx.send(format!("offline: selected {} infra agent(s); no live testing", selected.len())).await;
let artifacts = persist(&cfg, &recon, "", &[]);
return RunOutput { target: cfg.target.clone(), workdir: cfg.workdir.clone().unwrap_or_default(), findings: vec![],
agents_ran: selected.iter().map(|a| a.name.clone()).collect(), candidates: 0, recon, artifacts };
}
let chosen = select_agents(pool, &recon, &focus, &ranked, &tx).await;
let selected: Vec<Agent> = if !chosen.is_empty() {
let sel: Vec<Agent> = ranked.iter().filter(|a| chosen.iter().any(|c| c == &a.name)).cloned().collect();
if sel.is_empty() { ranked.iter().take(cap).cloned().collect() } else { sel.into_iter().take(cap).collect() }
} else {
ranked.iter().take(cap).cloned().collect()
};
let selected: Vec<Agent> = { let mut seen = std::collections::HashSet::new();
selected.into_iter().filter(|a| seen.insert(a.name.clone())).collect() };
let _ = tx.send(format!("selected {} infra agent(s): {}", selected.len(),
selected.iter().map(|a| a.name.clone()).collect::<Vec<_>>().join(", "))).await;
let target = cfg.target.clone();
let verbose = cfg.verbose;
let directives = operator_directives(&cfg);
let recon_ctx: String = recon.chars().take(3000).collect();
let raw: Vec<(String, String, Vec<Finding>)> = stream::iter(selected.iter().cloned())
.map(|ag| {
let target = target.clone();
let recon = recon_ctx.clone();
let directives = directives.clone();
let txc = tx.clone();
async move {
if pool.stop_exploiting() { return (ag.name.clone(), String::new(), vec![]); }
if verbose {
let _ = txc.send(format!(" ▶ launching agent: {} ({})", ag.name, ag.title.replace(" Agent", ""))).await;
}
let user = format!(
"AUTHORIZED host engagement on {target}. Proceed and PROVE each issue with raw tool output.\n\n{directives}{tooling}{react}{body}\n\nReply ONLY a JSON array of confirmed findings (may be []): {{id,title,severity,cwe,endpoint,payload,evidence,impact,remediation,confidence}}.",
target = target, directives = directives, tooling = HOST_TOOLING, react = REACT_DOCTRINE,
body = ag.user.replace("{target}", &target).replace("{recon_json}", &recon),
);
match pool.complete_routed(Task::Exploit, &ag.name, &ag.system, &user).await {
Ok((m, text)) => {
let f = extract_findings(&text, &ag.name);
let _ = txc.send(format!("test {} via {}{} candidate(s)", ag.name, m.label(), f.len())).await;
for c in &f {
let _ = txc.send(format!("finding: [{}] {} @ {}", c.severity, c.title, c.endpoint)).await;
if let Ok(j) = serde_json::to_string(c) { let _ = txc.send(format!("finding_json: {j}")).await; }
}
(ag.name.clone(), text, f)
}
Err(e) => { let _ = txc.send(format!("test {} failed: {e}", ag.name)).await;
(ag.name.clone(), format!("ERROR: {e}"), vec![]) }
}
}
})
.buffer_unordered(cfg.concurrency)
.collect::<Vec<_>>().await;
let transcript = transcript_of(&raw);
let candidates = dedup_findings(raw.iter().flat_map(|(_, _, f)| f.clone()).collect());
let _ = tx.send(format!("{} candidate finding(s) (deduped) — validating", candidates.len())).await;
let mut findings = validate(candidates, pool, VOTE_SYS, cfg.vote_n, &tx).await;
let chained = attack_chain(pool, &cfg, &recon, &findings, &lib.chains, &tx).await;
findings.extend(chained);
findings = dedup_findings(findings);
let findings = refute_pass(findings, pool, cfg.vote_n, &tx).await;
finish(cfg, lib, recon, transcript, findings, selected, &mut rl, tx).await
}
+109
View File
@@ -0,0 +1,109 @@
//! POMDP decision layer (v3.5.4): value-of-information planning + the
//! anti-hallucination gate.
//!
//! The choice "scan more vs exploit now" is **not** a heuristic here — it falls
//! out of the belief. When a target node's belief is diffuse (high entropy), the
//! expected value of an observation (recon) exceeds that of an exploit, because
//! the observation is expected to sharpen the belief by more than the exploit's
//! risk-adjusted payoff. That same criterion is the anti-hallucination rule: the
//! agent must not assert exploitability while the belief about the target state
//! is diffuse — it must collect more observation first.
use crate::belief::{Kind, WorldModel};
/// What the planner recommends doing next.
#[derive(Debug, Clone, PartialEq)]
pub enum Action {
/// Gather an observation about a still-diffuse node (recon).
Recon { node: String, voi: f64 },
/// Act on a node the belief is confident about (exploit/report).
Exploit { node: String, ev: f64 },
/// Belief is sharp and nothing actionable remains.
Stop,
}
/// Decision thresholds (tunable; could be learned later).
pub struct Policy {
/// Above this belief entropy, recon dominates exploit (value-of-information).
pub explore_entropy: f64,
/// Minimum P(true) to allow asserting/acting.
pub assert_min_p: f64,
/// Maximum entropy to allow asserting/acting (the anti-hallucination ceiling).
pub assert_max_entropy: f64,
}
impl Default for Policy {
fn default() -> Self {
Policy { explore_entropy: 0.6, assert_min_p: 0.7, assert_max_entropy: 0.4 }
}
}
/// Expected value of an observation about a node ≈ how much entropy it can
/// remove, weighted by the node's relevance (Exploit/Credential nodes matter
/// most). A sharp belief has ~0 VoI; a diffuse one has VoI≈1×weight.
pub fn value_of_information(wm: &WorldModel, node_id: &str) -> f64 {
let Some(n) = wm.nodes.get(node_id) else { return 0.0 };
let weight = match n.kind {
Kind::Exploit | Kind::Credential => 1.0,
Kind::Vuln => 0.8,
Kind::Service => 0.5,
Kind::Host => 0.4,
};
n.entropy() * weight
}
/// Risk-adjusted expected value of exploiting a node now: only worthwhile when
/// the belief is both high and sharp.
fn exploit_ev(wm: &WorldModel, node_id: &str, pol: &Policy) -> f64 {
let Some(n) = wm.nodes.get(node_id) else { return 0.0 };
if n.entropy() > pol.assert_max_entropy {
return 0.0; // too uncertain — exploiting now is gambling
}
n.p
}
/// Decide the next macro-action from the current belief: recon the highest-VoI
/// diffuse node, or exploit the most-confident node, whichever wins.
pub fn decide(wm: &WorldModel, pol: &Policy) -> Action {
// Best recon candidate by value-of-information.
let best_recon = wm.nodes.keys()
.map(|id| (id.clone(), value_of_information(wm, id)))
.max_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
// Best exploit candidate by risk-adjusted EV.
let best_exploit = wm.nodes.values()
.filter(|n| matches!(n.kind, Kind::Exploit | Kind::Vuln | Kind::Credential))
.map(|n| (n.id.clone(), exploit_ev(wm, &n.id, pol)))
.max_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
match (best_recon, best_exploit) {
(Some((rid, voi)), exp) => {
let ev = exp.as_ref().map(|(_, e)| *e).unwrap_or(0.0);
// Value-of-information dominates while the belief is diffuse.
if voi >= ev && voi > (1.0 - pol.explore_entropy) {
Action::Recon { node: rid, voi }
} else if let Some((eid, e)) = exp.filter(|(_, e)| *e > 0.0) {
Action::Exploit { node: eid, ev: e }
} else {
Action::Recon { node: rid, voi }
}
}
(None, Some((eid, e))) if e > 0.0 => Action::Exploit { node: eid, ev: e },
_ => Action::Stop,
}
}
/// Anti-hallucination gate. A claim of exploitability about `node` may only be
/// asserted when the belief is confident AND sharp. Returns Ok(()) to allow the
/// claim, or Err(reason) to force "collect more observation first".
pub fn may_assert(wm: &WorldModel, node_id: &str, pol: &Policy) -> Result<(), String> {
match wm.nodes.get(node_id) {
None => Err("no belief about this target — observe first".into()),
Some(n) if n.entropy() > pol.assert_max_entropy =>
Err(format!("belief diffuse (entropy {:.2} > {:.2}) — recon before asserting exploitability",
n.entropy(), pol.assert_max_entropy)),
Some(n) if n.p < pol.assert_min_p =>
Err(format!("belief too low (p {:.2} < {:.2}) — not exploitable on current evidence",
n.p, pol.assert_min_p)),
Some(_) => Ok(()),
}
}
+245 -15
View File
@@ -1,7 +1,24 @@
use crate::models::{cli_binary_for, ChatClient, ModelRef};
use anyhow::{anyhow, Result};
use std::sync::Arc;
use tokio::sync::Semaphore;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
use std::time::Duration;
use tokio::sync::{Notify, Semaphore};
/// Does this error look like token/quota/rate-limit exhaustion (as opposed to a
/// transient network blip)? Used to PAUSE the run instead of silently dropping
/// the agent, so the user can /continue (wait for renewal) or switch model.
pub fn is_exhaustion(e: &anyhow::Error) -> bool {
let s = format!("{e:#}").to_lowercase();
[
"rate limit", "rate_limit", "ratelimit", "429", "too many requests",
"quota", "insufficient_quota", "insufficient quota", "out of credit",
"credit balance", "billing", "exhausted", "overloaded", "capacity",
"usage limit", "resource_exhausted", "resource exhausted",
]
.iter()
.any(|k| s.contains(k))
}
/// Task type used by the model router to pick the best model for the step.
#[derive(Clone, Copy, Debug)]
@@ -34,9 +51,19 @@ pub struct ModelPool {
/// Progress channel: when set, the subscription CLI streams structured
/// activity (tools called, commands run, files read) here live.
progress: std::sync::Mutex<Option<tokio::sync::mpsc::Sender<String>>>,
/// Cooperative cancellation: when set, in-flight model calls short-circuit
/// and the pipeline stops launching new agents (graceful stop).
/// HARD cancellation: when set, in-flight model calls short-circuit (abort).
cancel: std::sync::Arc<std::sync::atomic::AtomicBool>,
/// SOFT stop: stop launching new EXPLOIT agents, but let in-flight finish and
/// VALIDATION still run — so "stop and validate what was found" works.
soft: std::sync::Arc<std::sync::atomic::AtomicBool>,
/// PAUSE: set when every candidate model is token/quota-exhausted. The run
/// parks (keeping all state) until the user runs /continue.
paused: Arc<AtomicBool>,
/// Wakes the parked task when the user runs /continue.
resume: Arc<Notify>,
/// Fallback models the user added via `/continue <provider:model>` while
/// paused — tried first on the next attempt.
fallback: Arc<Mutex<Vec<ModelRef>>>,
}
impl ModelPool {
@@ -65,6 +92,10 @@ impl ModelPool {
mcp_config,
progress: std::sync::Mutex::new(None),
cancel: Arc::new(std::sync::atomic::AtomicBool::new(false)),
soft: Arc::new(std::sync::atomic::AtomicBool::new(false)),
paused: Arc::new(AtomicBool::new(false)),
resume: Arc::new(Notify::new()),
fallback: Arc::new(Mutex::new(Vec::new())),
}
}
@@ -80,13 +111,69 @@ impl ModelPool {
self.progress.lock().ok().and_then(|g| g.clone())
}
/// Handle to request graceful cancellation of an in-progress engagement.
/// Handle to request HARD cancellation (abort all model calls).
pub fn cancel_handle(&self) -> Arc<std::sync::atomic::AtomicBool> {
self.cancel.clone()
}
/// Handle to request a SOFT stop (stop launching new exploit agents; keep
/// validation running).
pub fn soft_handle(&self) -> Arc<std::sync::atomic::AtomicBool> {
self.soft.clone()
}
pub fn is_cancelled(&self) -> bool {
self.cancel.load(std::sync::atomic::Ordering::Relaxed)
}
/// Should the exploit phase stop launching new agents? (hard OR soft stop)
pub fn stop_exploiting(&self) -> bool {
self.cancel.load(std::sync::atomic::Ordering::Relaxed)
|| self.soft.load(std::sync::atomic::Ordering::Relaxed)
}
/// Handle to the PAUSE flag (observe whether the run is parked on exhaustion).
pub fn pause_handle(&self) -> Arc<AtomicBool> {
self.paused.clone()
}
/// Handle used by the REPL to wake a parked run (`/continue`).
pub fn resume_handle(&self) -> Arc<Notify> {
self.resume.clone()
}
/// Slot the REPL pushes a fallback model into before resuming
/// (`/continue <provider:model>`).
pub fn fallback_handle(&self) -> Arc<Mutex<Vec<ModelRef>>> {
self.fallback.clone()
}
pub fn is_paused(&self) -> bool {
self.paused.load(Ordering::Relaxed)
}
/// Park the run on token/quota exhaustion: keep ALL state, emit a notice,
/// and wait until the user runs `/continue` (or cancels). Returns when the
/// run should retry (pause cleared) or give up (cancelled).
async fn park_exhausted(&self, err: &anyhow::Error) {
self.paused.store(true, Ordering::Relaxed);
if let Some(tx) = self.progress() {
let msg = format!("{err:#}");
let short = msg.lines().next().unwrap_or(&msg);
let _ = tx
.send(format!(
"notify: ⏸ token/quota exhausted ({}). Run is PAUSED — type /continue when your quota renews, or switch with /model <provider:model> then /continue.",
short.chars().take(120).collect::<String>()
))
.await;
}
while self.paused.load(Ordering::Relaxed) && !self.is_cancelled() {
let notified = self.resume.notified();
tokio::select! {
_ = notified => {}
_ = tokio::time::sleep(Duration::from_millis(500)) => {}
}
}
if !self.is_cancelled() {
if let Some(tx) = self.progress() {
let _ = tx.send("notify: ▶ resumed — retrying exhausted step.".to_string()).await;
}
}
}
/// One completion for a model, via subscription CLI (optionally with MCP) or
/// HTTP API, with a short retry/backoff. `label` (e.g. the agent name) tags
@@ -99,18 +186,35 @@ impl ModelPool {
let progress = self.progress();
let mut last = anyhow::anyhow!("no attempt");
for attempt in 0..3u64 {
if self.is_cancelled() {
return Err(anyhow!("cancelled"));
}
if attempt > 0 {
tokio::time::sleep(std::time::Duration::from_millis(1500 * attempt * attempt.max(1))).await;
}
let r = if use_cli {
let call = async {
if use_cli {
self.client
.chat_cli(label, &m.provider, &m.model, system, user, self.mcp_config.as_deref(), progress.clone())
.await
} else {
self.client.chat(m, system, user).await
}
};
// Race the in-flight call against a HARD cancel: when the user picks
// "report raw" / "discard" on /stop, drop the call future so the
// CLI child (spawned with kill_on_drop) is terminated immediately
// instead of finishing its whole command sequence.
let r = tokio::select! {
biased;
_ = wait_cancelled(&self.cancel) => return Err(anyhow!("cancelled")),
r = call => r,
};
match r {
Ok(t) => return Ok(t),
// Don't burn retries on exhaustion — surface it so the caller
// can park and let the user /continue.
Err(e) if is_exhaustion(&e) => return Err(e),
Err(e) => last = e,
}
}
@@ -125,15 +229,44 @@ impl ModelPool {
/// Router-aware completion. `label` tags streamed activity (agent name).
pub async fn complete_routed(&self, task: Task, label: &str, system: &str, user: &str) -> Result<(ModelRef, String)> {
let _permit = self.sem.acquire().await.expect("semaphore closed");
let order = self.route(task);
loop {
if self.is_cancelled() {
return Err(anyhow!("cancelled"));
}
// User-supplied fallback models (via /continue) are tried first.
let mut order = self.route(task);
if let Ok(fb) = self.fallback.lock() {
for m in fb.iter().rev() {
if !order.iter().any(|o| o.provider == m.provider && o.model == m.model) {
order.insert(0, m.clone());
}
}
}
let mut last = anyhow!("no candidate models");
let mut exhausted = false;
for m in &order {
if self.is_cancelled() {
return Err(anyhow!("cancelled"));
}
match self.one(label, m, system, user).await {
Ok(text) => return Ok((m.clone(), text)),
Err(e) => last = e,
Err(e) => {
if is_exhaustion(&e) {
exhausted = true;
}
last = e;
}
}
Err(last)
}
// Every candidate failed. If it was token/quota exhaustion, park the
// run until the user runs /continue, then retry the whole order (now
// including any fallback model they added). Otherwise, give up.
if exhausted && !self.is_cancelled() {
self.park_exhausted(&last).await;
continue;
}
return Err(last);
}
}
/// Reorder candidates for a task. With a single-model panel this is a no-op.
@@ -179,12 +312,7 @@ impl ModelPool {
};
if let Ok(text) = self.one("validate", m, system, user).await {
total += 1;
let t = text.to_lowercase();
if t.contains("\"verdict\": \"confirmed\"")
|| t.trim_start().starts_with("yes")
|| t.contains("confirmed: true")
|| t.contains("is_real\": true")
{
if parse_verdict(&text) == Verdict::Confirmed {
confirmed += 1;
}
}
@@ -192,3 +320,105 @@ impl ModelPool {
(confirmed, total)
}
}
/// Resolve once the HARD-cancel flag flips. Lets `tokio::select!` race an
/// in-flight model call against cancellation and drop it on the spot.
async fn wait_cancelled(flag: &Arc<AtomicBool>) {
while !flag.load(Ordering::Relaxed) {
tokio::time::sleep(Duration::from_millis(120)).await;
}
}
/// A validator's verdict on a candidate finding.
#[derive(Debug, PartialEq, Eq)]
pub enum Verdict {
Confirmed,
Rejected,
/// No clear yes/no — treated conservatively as NOT confirmed.
Unclear,
}
/// Robustly parse a validator reply into a verdict. Whitespace-insensitive
/// (so `{"verdict":"confirmed"}` and `{ "verdict": "confirmed" }` both match),
/// checks explicit rejection first, and only counts an *explicit* confirmation.
/// Anything ambiguous is `Unclear` (does not count as confirmed) — biasing the
/// pipeline against false positives.
pub fn parse_verdict(text: &str) -> Verdict {
let lower = text.to_lowercase();
let dense: String = lower.chars().filter(|c| !c.is_whitespace()).collect();
// Explicit rejection wins (conservative).
let rejected = [
"\"verdict\":\"rejected\"", "\"verdict\":\"reject\"", "verdict:rejected",
"\"is_real\":false", "\"isreal\":false", "\"confirmed\":false", "\"real\":false",
"\"exploitable\":false", "\"valid\":false",
];
if rejected.iter().any(|k| dense.contains(k)) {
return Verdict::Rejected;
}
// Explicit confirmation.
let confirmed = [
"\"verdict\":\"confirmed\"", "verdict:confirmed",
"\"is_real\":true", "\"isreal\":true", "\"confirmed\":true", "\"real\":true",
"\"exploitable\":true", "\"valid\":true",
];
if confirmed.iter().any(|k| dense.contains(k)) {
return Verdict::Confirmed;
}
// Fallback: only a leading, unambiguous "yes" counts as confirmation.
if lower.trim_start().starts_with("yes") {
return Verdict::Confirmed;
}
Verdict::Unclear
}
#[cfg(test)]
mod verdict_tests {
use super::*;
#[test]
fn parses_json_and_prose() {
assert_eq!(parse_verdict(r#"{"verdict":"confirmed","reason":"x"}"#), Verdict::Confirmed);
assert_eq!(parse_verdict(r#"{ "verdict": "confirmed" }"#), Verdict::Confirmed);
assert_eq!(parse_verdict(r#"{ "verdict": "rejected" }"#), Verdict::Rejected);
assert_eq!(parse_verdict(r#"{"is_real": false}"#), Verdict::Rejected);
assert_eq!(parse_verdict("Yes, the evidence proves RCE."), Verdict::Confirmed);
assert_eq!(parse_verdict("This looks theoretical."), Verdict::Unclear); // not counted
}
#[test]
fn rejection_beats_confirmation_when_both_present() {
// an answer that says confirmed:false must not be read as confirmed
assert_eq!(parse_verdict(r#"{"confirmed": false, "note": "verdict was confirmed earlier"}"#), Verdict::Rejected);
}
#[test]
fn quorum_is_severity_aware() {
// high/critical: need >=2 votes AND >=2/3
assert!(!quorum_confirmed("High", 1, 2));
assert!(quorum_confirmed("High", 2, 2));
assert!(quorum_confirmed("Critical", 2, 3));
assert!(!quorum_confirmed("Critical", 1, 3));
// single validator: majority applies to all
assert!(quorum_confirmed("Critical", 1, 1));
// low/medium: strict majority (more than half)
assert!(quorum_confirmed("Low", 1, 1));
assert!(!quorum_confirmed("Medium", 1, 2));
assert!(quorum_confirmed("Low", 2, 3));
assert!(!quorum_confirmed("Low", 0, 2));
}
}
/// Severity-aware confirmation quorum. False High/Critical findings are the most
/// costly, so they require ≥2 validators AND ≥2/3 agreement; lower severities
/// pass on a strict majority (more than half). With only one validator available
/// (single-model panel) the majority rule applies to all severities.
pub fn quorum_confirmed(severity: &str, yes: usize, total: usize) -> bool {
if total == 0 {
return false;
}
let s = severity.to_lowercase();
let high = s.starts_with("crit") || s.starts_with("high");
if high && total >= 2 {
yes * 3 >= total * 2 // ≥ two-thirds
} else {
yes * 2 > total // strict majority
}
}
+6 -5
View File
@@ -97,9 +97,9 @@ pub fn html(target: &str, findings: &[Finding]) -> String {
h4{{margin:12px 0 3px;font-size:12px;text-transform:uppercase;letter-spacing:.5px;color:#8b5cf6}}\
.b{{color:#8b5cf6;font-weight:800}}</style></head><body>\
<h1><span class=b>NeuroSploit</span> Penetration Test Report</h1>\
<div class=meta>Target: <b>{t}</b> · v3.5.0 Rust harness · multi-model validated</div>\
<div class=meta>Target: <b>{t}</b> · v3.5.4 Rust harness · multi-model validated</div>\
<div>{chips}</div>{graph_block}<h2>Findings ({n})</h2>{body}\
<p class=meta>Authorized testing only. Findings confirmed by multi-model adversarial voting.<br>NeuroSploit v3.5.0 · by <b>Joas A Santos</b> &amp; <b>Red Team Leaders</b></p></body></html>",
<p class=meta>Authorized testing only. Findings confirmed by multi-model adversarial voting.<br>NeuroSploit v3.5.4 · by <b>Joas A Santos</b> &amp; <b>Red Team Leaders</b></p></body></html>",
t = esc(target), chips = chips, n = sorted.len(), body = body, graph_block = graph_block,
)
}
@@ -135,13 +135,14 @@ pub fn typst_report(target: &str, findings: &[Finding], dir: &Path) -> std::io::
let mut data = String::new();
data.push_str(&format!(
"#let meta = (target: {}, run_id: {}, generated: {}, model: {})\n",
tq(target), tq(&run_id), tq("NeuroSploit v3.5.0"), tq("multi-model")
tq(target), tq(&run_id), tq("NeuroSploit v3.5.4"), tq("multi-model")
));
data.push_str("#let findings = (\n");
for f in sorted_findings(findings) {
let owasp = if f.owasp.is_empty() { f.cwe.clone() } else { f.owasp.clone() };
data.push_str(&format!(
" (severity: {}, title: {}, agent: {}, cwe: {}, cvss: {}, endpoint: {}, payload: {}, evidence: {}, impact: {}, remediation: {}, votes: {}, confidence: {}),\n",
tq(&f.severity), tq(&f.title), tq(&f.agent), tq(&f.cwe), tq(&f.cvss),
" (severity: {}, title: {}, agent: {}, cwe: {}, owasp: {}, cvss: {}, endpoint: {}, payload: {}, evidence: {}, impact: {}, remediation: {}, votes: {}, confidence: {}),\n",
tq(&f.severity), tq(&f.title), tq(&f.agent), tq(&f.cwe), tq(&owasp), tq(&f.cvss),
tq(&f.endpoint), tq(&f.payload), tq(&f.evidence), tq(&f.impact),
tq(&f.remediation), tq(&f.votes), f.confidence,
));
@@ -123,11 +123,20 @@ pub struct RunConfig {
/// agents (skipping recon-based selection) — used by the category picker.
#[serde(default)]
pub pinned: Vec<String>,
/// Attack-chaining depth: how many post-exploitation pivot rounds to run
/// from confirmed findings (0 disables chaining). Each round expands the
/// newest footholds in new directions, carrying discovered loot forward.
#[serde(default = "default_chain_depth")]
pub chain_depth: usize,
}
fn default_vote() -> usize {
3
}
fn default_chain_depth() -> usize {
2
}
fn default_concurrency() -> usize {
8
}
@@ -149,6 +158,7 @@ impl RunConfig {
auth: None,
repo: None,
pinned: Vec::new(),
chain_depth: 2,
}
}
}
+15
View File
@@ -20,3 +20,18 @@ login:
username: admin
password: password
success: Logout # text that appears on a successful login
# --- infra/host credentials (used by `neurosploit host <ip> --creds creds.yaml`) ---
ssh:
host: 10.0.0.5
port: 22
user: ubuntu
password: s3cret # or:
key: /home/op/id_ed25519
windows: # also used for Active Directory
host: 10.0.0.10
domain: CORP
user: jdoe
password: Winter2026! # or pass-the-hash:
hash: aad3b435b51404eeaad3b435b51404ee:NThashhere
+37 -12
View File
@@ -1,4 +1,4 @@
// NeuroSploit v3.5.0 — Typst report template (blank, structured).
// NeuroSploit v3.5.1 — Typst report template (blank, structured).
//
// The harness generates `report.typ` per run by prepending a `findings` array
// and a `meta` dict, then including this template's rendering logic. This file
@@ -24,7 +24,7 @@
#set page(margin: 2cm, numbering: "1", footer: context [
#set text(size: 8pt, fill: gray)
NeuroSploit v3.5.0 · #meta.target · confidential
NeuroSploit v3.5.1 · #meta.target · confidential
#h(1fr) #counter(page).display()
])
#set text(font: ("Helvetica Neue", "Helvetica", "Arial"), size: 10pt)
@@ -71,13 +71,32 @@
)
]
#let sorted = findings.sorted(key: f => sevrank(f.severity))
// ---- Vulnerability summary table ----
#if sorted.len() > 0 [
#v(8pt)
== Vulnerability Summary
#v(4pt)
#table(
columns: (auto, 1fr, auto, auto, auto),
inset: 6pt, align: (left + horizon, left + horizon, center + horizon, center + horizon, center + horizon),
stroke: 0.5pt + rgb("#dddddd"),
table.header(
text(weight: "bold")[\#], text(weight: "bold")[Vulnerability],
text(weight: "bold")[Severity], text(weight: "bold")[CVSS], text(weight: "bold")[OWASP / CWE],
),
..sorted.enumerate().map(((i, f)) => (
str(i + 1), f.title, sevbadge(f.severity), f.cvss, f.owasp,
)).flatten()
)
]
#v(10pt)
#line(length: 100%, stroke: 0.5pt + gray)
// ---- Findings ----
// ---- Detailed findings ----
= Findings
#let sorted = findings.sorted(key: f => sevrank(f.severity))
#if sorted.len() == 0 [
#text(fill: gray)[_Nothing to report._]
]
@@ -86,14 +105,20 @@
stroke: (left: 3pt + sevcolor.at(f.severity, default: gray), rest: 0.5pt + rgb("#dddddd")))[
#sevbadge(f.severity) #h(6pt) #text(12pt, weight: "bold")[#str(i + 1). #f.title]
#v(4pt)
#text(9pt, fill: gray)[
agent: #raw(f.agent) · CWE: #f.cwe · CVSS: #f.cvss · votes: #f.votes · confidence: #str(f.confidence)
]
#v(2pt) #text(9pt)[Endpoint: #raw(f.endpoint)]
#v(5pt) #strong[Payload] #linebreak() #raw(f.payload)
#table(
columns: (auto, 1fr, auto, 1fr),
inset: 4pt, stroke: none, align: left + horizon,
text(8pt, fill: gray)[Criticality], text(8pt)[#f.severity],
text(8pt, fill: gray)[CVSS], text(8pt)[#f.cvss],
text(8pt, fill: gray)[OWASP/CWE], text(8pt)[#f.owasp · #f.cwe],
text(8pt, fill: gray)[Confidence], text(8pt)[#f.votes votes · #str(f.confidence)],
text(8pt, fill: gray)[Location], text(8pt)[#raw(f.endpoint)],
text(8pt, fill: gray)[Agent], text(8pt)[#raw(f.agent)],
)
#v(4pt) #strong[Description / Impact] #linebreak() #text(9pt)[#f.impact]
#v(4pt) #strong[Proof of Concept] #linebreak() #raw(f.payload)
#v(3pt) #strong[Evidence] #linebreak() #raw(f.evidence)
#v(3pt) #strong[Impact:] #f.impact
#v(2pt) #strong[Remediation:] #f.remediation
#v(3pt) #strong[Remediation] #linebreak() #text(9pt)[#f.remediation]
]
#v(8pt)
]
+143
View File
@@ -0,0 +1,143 @@
#!/usr/bin/env python3
"""
NeuroSploit v3.5.1 application-stack & CVE-hunting agents.
Adds IIS/.NET, CMS (WordPress/Drupal/Joomla/etc.), app-server and known-CVE
exploitation agents to agents_md/vulns/. Credits: Joas A Santos & Red Team Leaders.
"""
import os
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
OUT = os.path.join(ROOT, "agents_md", "vulns")
def render(a):
L = [f"# {a['title']} Agent\n", "## User Prompt",
f"You are testing **{{target}}** for {a['for']}.\n",
"**Recon Context:**\n{recon_json}\n", "**METHODOLOGY:**\n"]
for i, (s, bs) in enumerate(a["steps"], 1):
L.append(f"### {i}. {s}")
L += [f"- {b}" for b in bs]
L.append("")
n = len(a["steps"]) + 1
L += [f"### {n}. Report Format", "For each CONFIRMED finding:", "```", "FINDING:",
f"- Title: {a['title']} at [endpoint]", f"- Severity: {a['sev']}", f"- CWE: {a['cwe']}",
"- Endpoint: [full URL]", "- Vector: [what/where]", "- Payload: [exact payload/command]",
"- Evidence: [raw tool output proving it]", f"- Impact: {a['impact']}",
f"- Remediation: {a['fix']}", "```\n", "## System Prompt", a["system"]]
return "\n".join(L) + "\n"
def A(name, title, vc, cwe, sev, steps, fix, impact):
return {"name": name, "title": title, "for": vc, "sev": sev, "cwe": cwe, "impact": impact,
"fix": fix, "steps": steps,
"system": (f"You are a specialist in {vc}. AUTHORIZED engagement. Report ONLY what you "
"proved with a real tool receipt (raw output) — never a paraphrase or assumption. "
"Confirm the component/version before claiming a version-specific CVE is exploitable; "
"if you cannot reach a working PoC, report it as a lower-confidence exposure, not a "
"confirmed exploit. No destructive/DoS actions. Credits: Joas A Santos and Red Team Leaders.")}
AGENTS = [
# ---- IIS / ASP.NET ----
A("iis_tilde_shortname", "IIS Tilde (~) Short-Name Enumeration", "IIS 8.3 short-name disclosure",
"CWE-200", "Medium",
[("Detect", ["Probe `GET /*~1*/.aspx` style requests; a 404-vs-error differential reveals 8.3 short names",
"Confirm IIS version from Server header"]),
("Enumerate", ["Brute the short names char by char to reveal hidden files/dirs"]),
("Confirm", ["Show recovered short names mapping to real sensitive files"])],
"Disable 8.3 name creation; patch IIS", "Discovery of hidden files/backups/configs"),
A("iis_webdav", "IIS WebDAV Misconfiguration", "exposed/unsafe WebDAV on IIS",
"CWE-650", "High",
[("Detect", ["`OPTIONS /` — look for DAV header / PUT/MOVE/COPY allowed"]),
("Test write", ["Attempt PUT of a benign file; if blocked, try `.txt`→MOVE→`.asp` trick"]),
("Confirm", ["Show an uploaded file is served (and if executable → RCE)"])],
"Disable WebDAV or restrict methods/authn", "Arbitrary upload, potential RCE"),
A("aspnet_viewstate", "ASP.NET ViewState Deserialization", "unprotected/known-key __VIEWSTATE deserialization",
"CWE-502", "Critical",
[("Inspect", ["Capture __VIEWSTATE; check if MAC is disabled (enableViewStateMac=false) or a known/leaked machineKey is in play"]),
("Weaponize", ["With a known/guessed machineKey, craft a ysoserial.net ViewState gadget"]),
("Confirm", ["Prove code execution via OOB callback or command output tied to a unique marker"])],
"Enable ViewState MAC; rotate machineKey; patch", "Remote code execution"),
A("aspnet_debug_trace", "ASP.NET Debug/Trace Exposure", "debug/trace enabled in production ASP.NET",
"CWE-489", "Medium",
[("Probe", ["Request `trace.axd`; send `DEBUG` verb; check `<compilation debug=...>` leakage via errors"]),
("Assess", ["Harvest request/session data, stack traces, app internals from trace output"]),
("Confirm", ["Show sensitive runtime data exposed"])],
"Disable debug/trace; custom errors", "Information disclosure"),
A("iis_handler_bypass", "IIS Handler/Extension Bypass", "auth or filter bypass via IIS handler quirks",
"CWE-288", "High",
[("Probe", ["Test path/extension tricks: `;.asp`, `::$DATA`, trailing dot, `%20`, case, `/admin/.`/`..%2f`"]),
("Bypass", ["Reach a protected handler/endpoint via a normalization or handler-mapping quirk"]),
("Confirm", ["Show access to a resource that should be blocked"])],
"Consistent normalization; patch; tighten ACLs", "Auth/control bypass"),
# ---- CMS general & specific ----
A("cms_fingerprint", "CMS Fingerprint & Version", "CMS identification and version disclosure",
"CWE-200", "Info",
[("Identify", ["Detect CMS via meta generator, paths (`/wp-`, `/sites/`, `/administrator/`), headers, favicon hash",
"Run whatweb/wpscan-style detection without auth"]),
("Version", ["Pin exact version from readme/changelog/asset hashes"]),
("Map", ["List plugins/themes/modules and their versions for CVE correlation"])],
"Hide version/generator; keep components updated", "Targeted exploitation surface"),
A("wordpress_audit", "WordPress Security Audit", "WordPress core/plugin/theme weaknesses",
"CWE-1395", "High",
[("Enumerate", ["Users (`/?author=`, REST `/wp-json/wp/v2/users`), plugins/themes + versions, `xmlrpc.php`"]),
("Correlate CVEs", ["Map plugin/theme versions to known vulns (arbitrary upload, SQLi, auth bypass, LFI)"]),
("Confirm", ["Reproduce one concrete issue (e.g. unauth arbitrary file upload) with proof"])],
"Update core/plugins/themes; harden; disable xmlrpc", "Site takeover / RCE"),
A("joomla_audit", "Joomla Security Audit", "Joomla core/extension weaknesses",
"CWE-1395", "High",
[("Enumerate", ["Version (`administrator/manifests/files/joomla.xml`), components/extensions + versions"]),
("Correlate CVEs", ["Map to known Joomla/extension CVEs (SQLi, LFI, object injection)"]),
("Confirm", ["Reproduce one with proof"])],
"Update core/extensions; harden admin", "Site takeover / data breach"),
A("drupal_audit", "Drupal Security Audit", "Drupal core/module weaknesses (e.g. Drupalgeddon class)",
"CWE-1395", "Critical",
[("Enumerate", ["Version (CHANGELOG, headers), enabled modules"]),
("Correlate CVEs", ["Map to known Drupal RCE/SQLi (e.g. SA-CORE highly-critical classes)"]),
("Confirm", ["Reproduce with an OOB/output proof where applicable"])],
"Patch core/modules promptly", "Remote code execution"),
A("cms_default_admin", "CMS Admin Panel & Default Creds", "exposed CMS admin with weak/default credentials",
"CWE-1392", "High",
[("Locate", ["Find admin (`/wp-admin`, `/administrator`, `/user/login`, `/admin`)"]),
("Test (in scope)", ["Try supplied/default credentials; respect lockout/ROE — no out-of-scope brute force"]),
("Confirm", ["Show authenticated admin access"])],
"Remove defaults; strong creds + MFA; restrict admin", "Full CMS compromise"),
# ---- app servers / panels ----
A("appserver_exposure", "App-Server Console Exposure", "exposed Tomcat/JBoss/Jenkins/Actuator consoles",
"CWE-1188", "High",
[("Discover", ["Probe `/manager/html`, `/jmx-console`, `/jenkins`, `/actuator`, `/console`, `/admin`"]),
("Assess", ["Test default/weak creds (in scope); check unauth-exposed management endpoints"]),
("Confirm", ["Demonstrate a management action / deploy / info-leak proving exposure (→ often RCE)"])],
"Authenticate & network-restrict consoles; remove defaults", "Remote code execution / takeover"),
A("git_svn_exposure_app", "Exposed VCS / Build Artifacts", "exposed .git/.svn/CI artifacts on the app host",
"CWE-527", "High",
[("Probe", ["Request `/.git/HEAD`, `/.svn/entries`, `/.env`, build/CI artifact paths"]),
("Recover", ["Dump source (git-dumper) / read secrets"]),
("Confirm", ["Show recovered source or live secret"])],
"Block VCS/dotfiles from web; rotate secrets", "Source/secret disclosure → RCE"),
# ---- CVE hunting ----
A("cve_known_exploitation", "Known-CVE Exploitation Specialist", "exploiting known CVEs for the detected stack",
"CWE-1395", "Critical",
[("Identify versions", ["From recon, list each component + exact version (server, framework, CMS, plugins, libs)"]),
("Map to CVEs", ["Match versions to known CVEs; prioritise unauth RCE/SQLi/auth-bypass; note CVE id + CVSS",
"Prefer issues with a reliable, non-destructive PoC"]),
("Reproduce safely", ["Run a benign PoC (e.g. a version/echo check or OOB callback) to confirm the CVE is actually present and exploitable — never a destructive payload"]),
("Confirm", ["Report the CVE only when the PoC produced concrete proof (output/OOB); otherwise report it as 'potentially vulnerable (version match, unconfirmed)'"])],
"Patch/upgrade the affected components; apply vendor advisories", "Depends on CVE — up to full compromise"),
A("outdated_dependency_cve", "Outdated Component CVE Specialist", "outdated front-end/back-end components with known CVEs",
"CWE-1104", "High",
[("Inventory", ["Extract JS libs (jQuery, Angular, etc.), server modules, framework versions from responses/JS/headers"]),
("Correlate", ["Map each to known CVEs; flag the exploitable, reachable ones"]),
("Confirm", ["Prove exploitability where a safe PoC exists; else report as version-based exposure"])],
"Upgrade components; dependency scanning in CI", "Varies — XSS/RCE/info-leak"),
]
def main():
os.makedirs(OUT, exist_ok=True)
for a in AGENTS:
open(os.path.join(OUT, a["name"] + ".md"), "w").write(render(a))
print(f"wrote {len(AGENTS)} app-stack/CVE agents to {OUT}")
if __name__ == "__main__":
main()
+201
View File
@@ -0,0 +1,201 @@
#!/usr/bin/env python3
"""
NeuroSploit v3.5.1 attack-chain agents.
Each agent is a multi-stage exploitation-chaining playbook: take a confirmed
entry-point weakness and escalate it through concrete stages to deeper impact
(e.g. SQLi RCE local privilege escalation). Writes agents_md/chains/*.md.
Credits: Joas A Santos & Red Team Leaders.
"""
import os
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
OUT = os.path.join(ROOT, "agents_md", "chains")
def render(a):
L = [f"# {a['title']} Agent\n", "## User Prompt",
f"You are executing a multi-stage ATTACK CHAIN against **{{target}}**: {a['chain']}.\n",
"**Recon Context / prior findings:**\n{recon_json}\n",
f"**GOAL:** {a['goal']}\n",
"**CHAIN — advance stage by stage; each stage's output is the next stage's input. "
"Use the ReAct loop and PROVE every stage with raw tool output before advancing:**\n"]
for i, (stage, bs) in enumerate(a["stages"], 1):
L.append(f"### Stage {i}. {stage}")
L += [f"- {b}" for b in bs]
L.append("")
n = len(a["stages"]) + 1
L += [f"### {n}. Report Format",
"Report the chain as ONE finding (plus per-stage evidence):", "```", "FINDING:",
f"- Title: {a['title']}", f"- Severity: {a['sev']}", f"- CWE: {a['cwe']}",
"- Endpoint: [entry point]", "- Vector: [the full chain, stage by stage]",
"- Payload: [the key payloads/commands per stage]",
"- Evidence: [raw output proving EACH stage actually executed]",
f"- Impact: {a['impact']}", f"- Remediation: {a['fix']}",
"- chains_from: [ids of the prerequisite findings this builds on]", "```\n",
"## System Prompt", a["system"]]
return "\n".join(L) + "\n"
def A(name, title, chain, goal, cwe, sev, impact, fix, stages):
return {"name": name, "title": title, "chain": chain, "goal": goal, "cwe": cwe,
"sev": sev, "impact": impact, "fix": fix, "stages": stages,
"system": ("You are an exploit-chaining specialist. Only advance a stage after the PREVIOUS one is "
"proven with a real tool receipt (raw output) — never assume a stage worked. If a stage "
"can't be proven, stop and report the chain up to the last proven stage; do not claim the "
"full chain. AUTHORIZED engagement; no destructive/DoS actions. Each reported stage must "
"carry its own evidence. Credits: Joas A Santos & Red Team Leaders.")}
CHAINS = [
A("chain_sqli_to_rce_to_lpe",
"SQLi → RCE → Local PrivEsc Chain",
"SQL injection → command execution → local privilege escalation",
"Turn a database-layer injection into root/SYSTEM on the host.",
"CWE-89", "Critical",
"Full host compromise originating from a web injection",
"Parameterize queries; least-privilege DB account; harden host; patch local vectors",
[("Exploit the SQL injection", ["Confirm injection (error/boolean/time); identify DBMS and privileges",
"Enumerate whether stacked queries / FILE / xp_cmdshell / INTO OUTFILE are available"]),
("Pivot SQLi → RCE", ["MSSQL: enable & use `xp_cmdshell`; MySQL: `INTO OUTFILE` a webshell to a known web path; PostgreSQL: `COPY ... PROGRAM`",
"Confirm OS command execution with `id`/`whoami` output"]),
("Establish a foothold", ["Drop/upgrade to a stable shell as the web/db service user"]),
("Local privilege escalation", ["Enumerate SUID/sudo/cron/kernel (Linux) or token/service/unquoted-path (Windows)",
"Escalate to root/SYSTEM and prove with a privileged command output"])]),
A("chain_ssrf_to_aws_compromise",
"SSRF → AWS Credential Compromise Chain",
"SSRF → cloud metadata → IAM credentials → cloud account access",
"Convert a server-side request forgery into valid AWS credentials and account access.",
"CWE-918", "Critical",
"Cloud account compromise via stolen IAM role credentials",
"Enforce IMDSv2 hop-limit=1; egress allowlists; SSRF input validation; scoped IAM roles",
[("Confirm the SSRF primitive", ["Find a server-side fetch you control (url/webhook/import/pdf/image param)",
"Prove it reaches an attacker-controlled / internal host"]),
("Reach the metadata service", ["IMDSv2: PUT `/latest/api/token` then GET with the token header; else IMDSv1 GET",
"Retrieve `/latest/meta-data/iam/security-credentials/<role>`"]),
("Harvest IAM credentials", ["Capture AccessKeyId/SecretAccessKey/Token from the metadata response"]),
("Use the credentials (in scope)", ["`aws sts get-caller-identity` to confirm; enumerate permitted actions read-only",
"Prove access to at least one resource the role can reach"])]),
A("chain_ssrf_to_rce",
"SSRF → RCE Chain",
"SSRF → internal service abuse → remote code execution",
"Escalate an SSRF into code execution via a reachable internal service.",
"CWE-918", "Critical",
"Remote code execution pivoted through an internal service",
"Egress controls; authenticate internal services; SSRF allowlists",
[("Confirm SSRF + map internals", ["Prove the SSRF; port-scan internal hosts through it (gopher/http)",
"Identify exploitable internal services (Redis, unauth admin, CI, internal API)"]),
("Weaponize the internal service", ["e.g. Redis → write SSH key/cron/module; internal Jenkins/Actuator → job/exec; gopher:// to craft raw protocol payloads"]),
("Achieve RCE", ["Trigger command execution on the internal/back-end host"]),
("Confirm", ["Prove execution with an OOB callback or command output tied to a unique marker"])]),
A("chain_upload_to_rce",
"File Upload → RCE Chain",
"insecure file upload → webshell → remote code execution",
"Turn an unrestricted/insecure upload into code execution.",
"CWE-434", "Critical",
"Remote code execution via uploaded executable content",
"Validate type by content; randomize names; store outside webroot; non-exec storage",
[("Probe the upload", ["Map accepted types/extensions, storage path, and how files are served",
"Test bypasses: double extension, content-type spoof, magic-byte prefix, null byte, .htaccess/.phar"]),
("Upload a payload", ["Place a minimal webshell/handler in a web-served, executable location"]),
("Locate & trigger", ["Find the served URL of the upload; request it to execute"]),
("Confirm RCE", ["Run `id`/`whoami`; capture output proving execution"])]),
A("chain_upload_lfi_rce_lpe",
"Upload → LFI → RCE → LPE Chain",
"file upload + local file inclusion → log/session poisoning → RCE → privilege escalation",
"Chain a benign upload and an LFI into code execution and then root.",
"CWE-98", "Critical",
"Host compromise from a non-executable upload chained through LFI",
"Fix LFI (allowlist includes); validate uploads; harden host",
[("Confirm the LFI", ["Prove local file inclusion (read /etc/passwd or app config); identify wrappers (php://, data://, zip://)"]),
("Plant controllable content via upload", ["Upload a file whose path/content you can later include (image with PHP, zip for zip:// , or use the LFI to read your uploaded file)"]),
("LFI → RCE", ["Include the planted file, or poison logs/session/`/proc/self/environ` then include it to execute code"]),
("Confirm RCE then escalate", ["Prove command execution; then enumerate and perform local privilege escalation to root/SYSTEM"])]),
A("chain_xss_to_account_takeover",
"XSS → Session/Account Takeover Chain",
"stored/reflected XSS → session or token theft → account takeover",
"Escalate XSS into full takeover of a victim (incl. admin) account.",
"CWE-79", "High",
"Account takeover (incl. privileged) via client-side execution",
"Output encoding + CSP; HttpOnly/SameSite cookies; rotate tokens",
[("Prove execution", ["Confirm the payload executes in the victim's browser context (Playwright: alert/DOM), not just reflects"]),
("Steal the session", ["Exfiltrate the session cookie/JWT/CSRF token to a collaborator, or perform actions in-context if HttpOnly"]),
("Take over the account", ["Replay the stolen session, or change email/password/MFA via in-context requests"]),
("Confirm + escalate", ["Prove control of the victim account; target an admin for privilege escalation"])]),
A("chain_idor_to_takeover",
"IDOR → Mass Account Takeover Chain",
"IDOR → cross-account data → credential/role manipulation → takeover",
"Chain object-level authz failure into taking over arbitrary accounts.",
"CWE-639", "High",
"Mass account takeover via broken object-level authorization",
"Enforce per-object ownership on every endpoint; indirect references",
[("Confirm the IDOR", ["Access another user's object with your session, proven by their data"]),
("Find a state-changing IDOR", ["Locate IDOR on email/password/role/API-key endpoints"]),
("Manipulate the victim account", ["Change a victim's email or reset token / elevate role via the IDOR"]),
("Confirm takeover", ["Log in as / act as the victim; demonstrate control"])]),
A("chain_ssti_to_rce_to_cloud",
"SSTI → RCE → Cloud Pivot Chain",
"template injection → RCE → host creds → cloud/lateral movement",
"Go from template injection to code execution to cloud or lateral access.",
"CWE-1336", "Critical",
"Cloud/lateral compromise originating from template injection",
"Never render user input as templates; sandbox; scope host IAM/creds",
[("Confirm SSTI → RCE", ["Fingerprint the engine (`{{7*7}}` etc.); use the gadget to execute a command; prove with output"]),
("Loot the host", ["Read env/config/instance metadata for cloud creds, DB creds, tokens"]),
("Pivot", ["Use recovered creds against cloud APIs or adjacent internal hosts"]),
("Confirm impact", ["Prove access to a cloud resource or a second host with evidence"])]),
A("chain_default_creds_to_domain",
"Default Creds → Foothold → Domain Compromise Chain",
"default/weak creds → host foothold → AD escalation → domain dominance",
"Chain an exposed credential into Active Directory domain compromise.",
"CWE-798", "Critical",
"Domain compromise from a single weak/default credential",
"Rotate defaults; unique strong passwords; tiered admin; monitor",
[("Get the foothold", ["Authenticate with the default/weak/reused credential (SSH/WinRM/SMB/web)"]),
("Enumerate AD", ["From the foothold, run BloodHound/netexec; map attack paths, roastable accounts, ACLs"]),
("Escalate in AD", ["Kerberoast/AS-REP-roast, abuse an ACL edge, or relay — recover higher-priv creds"]),
("Reach domain dominance", ["Demonstrate DCSync or DA-equivalent access (single test account) proving the path"])]),
A("chain_deserialization_to_rce",
"Insecure Deserialization → RCE Chain",
"untrusted deserialization → gadget chain → remote code execution",
"Turn a deserialization sink into reliable code execution.",
"CWE-502", "Critical",
"Remote code execution via unsafe object deserialization",
"Never deserialize untrusted data; allowlist types; safe formats",
[("Locate the sink", ["Identify where attacker data is deserialized (cookie/param/file/RPC); fingerprint the format/library"]),
("Build the gadget", ["Select a working gadget chain (ysoserial/ysoserial.net/PyYAML/pickle) for the target stack"]),
("Execute", ["Deliver the payload to the sink"]),
("Confirm", ["Prove execution via OOB callback or command output with a unique marker"])]),
A("chain_exposed_git_to_rce",
"Exposed .git/.env → Secret → RCE Chain",
"exposed source/secrets → recovered credentials → authenticated RCE",
"Chain leaked source/secrets into authenticated code execution.",
"CWE-527", "High",
"Code execution using credentials recovered from exposed source/secrets",
"Block dotfiles from web; rotate leaked secrets; vault storage",
[("Recover the source/secrets", ["Dump exposed `.git` (git-dumper) or read `.env`/config; extract keys/creds/tokens"]),
("Validate the secrets", ["Confirm a recovered credential/key is live (admin panel, cloud, DB, CI)"]),
("Gain execution", ["Use the access to deploy code / run a CI job / write a webshell / exec via admin feature"]),
("Confirm RCE", ["Prove command execution with output"])]),
A("chain_subdomain_takeover_to_phishing",
"Subdomain Takeover → Trusted Phishing/Cookie Chain",
"dangling DNS → subdomain takeover → trusted-origin abuse",
"Chain a dangling record into hosting attacker content on a trusted subdomain.",
"CWE-350", "High",
"Trusted-origin abuse (cookie theft / phishing / OAuth) via a taken-over subdomain",
"Remove dangling DNS; monitor; scope cookies/CSP per-host",
[("Find the dangling record", ["Identify a CNAME/A pointing to an unclaimed provider resource"]),
("Claim it", ["Register the resource so the subdomain serves your content (benign PoC)"]),
("Abuse the trust", ["Show impact: wildcard-cookie capture, OAuth redirect trust, or CSP allowlist bypass"]),
("Confirm", ["Demonstrate the concrete trusted-origin abuse with evidence"])]),
]
def main():
os.makedirs(OUT, exist_ok=True)
for a in CHAINS:
open(os.path.join(OUT, a["name"] + ".md"), "w").write(render(a))
print(f"wrote {len(CHAINS)} chain agents to {OUT}")
if __name__ == "__main__":
main()
+117
View File
@@ -0,0 +1,117 @@
#!/usr/bin/env python3
"""
NeuroSploit v3.5.1 infrastructure host agents (Linux / Windows / Active Directory).
Writes agents_md/infra/*.md. Credits: Joas A Santos & Red Team Leaders.
"""
import os
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
OUT = os.path.join(ROOT, "agents_md", "infra")
def render(a):
L = [f"# {a['title']} Agent\n", "## User Prompt",
f"You are testing **{{target}}** (a host/infrastructure target) for {a['for']}.\n",
"**Recon Context:**\n{recon_json}\n",
"Authentication/credentials, if provided, are described in the operator directives above.\n",
"**METHODOLOGY:**\n"]
for i, (s, bs) in enumerate(a["steps"], 1):
L.append(f"### {i}. {s}")
L += [f"- {b}" for b in bs]
L.append("")
n = len(a["steps"]) + 1
L += [f"### {n}. Report Format", "For each CONFIRMED finding:", "```", "FINDING:",
f"- Title: {a['title']} on [host]", f"- Severity: {a['sev']}", f"- CWE: {a['cwe']}",
"- Endpoint: [host/service]", "- Vector: [how]", "- Payload: [command/PoC]",
"- Evidence: [raw tool output proving it]", f"- Impact: {a['impact']}",
f"- Remediation: {a['fix']}", "```\n",
"## System Prompt", a["system"]]
return "\n".join(L) + "\n"
def A(name, title, vc, cwe, sev, steps, fix, impact):
return {"name": name, "title": title, "for": vc, "sev": sev, "cwe": cwe, "impact": impact,
"fix": fix, "steps": steps,
"system": f"You are an infrastructure pentest specialist for {vc}. AUTHORIZED engagement. "
"Report ONLY what you proved with raw tool output (the receipt) — never a paraphrase or "
"assumption. If you lack access/observation to confirm, say so and gather more first. "
"Stay in scope; never run destructive or DoS actions. Credits: Joas A Santos & Red Team Leaders."}
INFRA = [
# ---- recon / network ----
A("infra_port_service_scan", "Host Port & Service Scan", "open ports and service/version discovery", "CWE-200", "Info",
[("Scan", ["`rustscan -a {target} -- -sV` if present, else `nmap -sV -sC -Pn {target}`",
"Identify open TCP/UDP ports, service banners and versions"]),
("Triage", ["Flag risky services (SMB, RDP, SSH, WinRM, LDAP, databases) and outdated versions",
"Correlate versions to known CVEs for downstream agents"])],
"Close/patch exposed services; restrict by firewall", "Attack-surface mapping"),
A("infra_smb_enum", "SMB/NetBIOS Enumeration", "SMB shares, sessions and misconfigurations", "CWE-200", "Medium",
[("Enumerate", ["`netexec smb {target}` / `crackmapexec smb {target}` for hosts, signing, null sessions",
"`smbclient -L //{target}/ -N` to list shares; check anonymous read/write"]),
("Assess", ["Flag SMB signing disabled (relay risk), guest/anonymous access, writable shares"])],
"Require SMB signing; disable guest; restrict shares", "Lateral movement, credential relay"),
# ---- linux ----
A("linux_priv_esc", "Linux Privilege Escalation", "local privilege-escalation paths on a Linux host", "CWE-269", "High",
[("Enumerate (authenticated via SSH)", ["Run linpeas/`sudo -l`, SUID/SGID (`find / -perm -4000`), cron, capabilities, writable PATH",
"Check kernel version for known local exploits"]),
("Confirm", ["Demonstrate an actual escalation to root (or a clear, reachable path) with command output"])],
"Patch kernel; fix sudo/SUID/cron/permission issues", "Full host compromise"),
A("linux_ssh_weak_auth", "SSH Weak Authentication", "weak/guessable SSH credentials or misconfig", "CWE-1391", "High",
[("Assess", ["Check allowed auth methods; test provided creds with `ssh`/`sshpass`",
"Only test supplied credentials — never brute force out of scope"]),
("Confirm", ["Show authenticated shell access with the credentials, capturing the session banner"])],
"Key-only auth; strong passwords; fail2ban", "Unauthorized host access"),
A("linux_sudo_misconfig", "Linux Sudo Misconfiguration", "exploitable sudo rules", "CWE-250", "High",
[("Enumerate", ["`sudo -l`; look for NOPASSWD binaries and GTFObins-exploitable entries"]),
("Confirm", ["Escalate via a permitted binary and show `id`=root output"])],
"Restrict sudo to least privilege; avoid shell-capable binaries", "Privilege escalation to root"),
A("linux_cron_writable", "Writable Cron / Service Abuse", "world-writable cron jobs or unit files", "CWE-732", "High",
[("Find", ["Inspect /etc/cron*, systemd units, and scripts they call for writable paths"]),
("Confirm", ["Plant a benign marker that the privileged job executes, proving control"])],
"Fix permissions on jobs and their targets", "Privilege escalation"),
# ---- windows ----
A("windows_priv_esc", "Windows Privilege Escalation", "local privilege escalation on a Windows host", "CWE-269", "High",
[("Enumerate (authenticated)", ["Run winPEAS/`whoami /priv`; check unquoted service paths, weak service perms, AlwaysInstallElevated, token privileges (SeImpersonate)"]),
("Confirm", ["Demonstrate escalation to SYSTEM/admin with command output (e.g. via a Potato technique where applicable)"])],
"Patch; fix service perms; remove dangerous privileges", "Full host compromise"),
A("windows_smb_signing", "SMB Signing & Relay Exposure", "SMB signing not required (NTLM relay risk)", "CWE-294", "Medium",
[("Detect", ["`netexec smb {target}` — note `signing:False`"]),
("Assess", ["Explain the NTLM-relay exposure; confirm a coercible auth path only if in scope"])],
"Enforce SMB signing; disable NTLM where possible", "Credential relay, lateral movement"),
A("windows_winrm_access", "WinRM Authenticated Access", "remote management access via WinRM", "CWE-287", "Medium",
[("Connect", ["`evil-winrm -i {target} -u <user> -p <pass>` (or -H <hash>) with supplied creds/hash"]),
("Confirm", ["Show an authenticated remote shell and the host context (`whoami`, hostname)"])],
"Restrict WinRM; strong creds; network segmentation", "Remote host control"),
# ---- active directory ----
A("ad_kerberoasting", "AD Kerberoasting", "service accounts with crackable SPNs", "CWE-522", "High",
[("Request", ["`netexec ldap {target} -u <user> -p <pass> --kerberoasting out.txt` or impacket GetUserSPNs"]),
("Crack & confirm", ["Crack the TGS hash offline (hashcat -m 13100); confirm a recovered service-account password"])],
"Strong/long service-account passwords; gMSA", "Service-account compromise, lateral movement"),
A("ad_asreproasting", "AD AS-REP Roasting", "accounts with Kerberos pre-auth disabled", "CWE-522", "High",
[("Enumerate", ["impacket GetNPUsers / `netexec ldap {target} --asreproast out.txt` for DONT_REQ_PREAUTH accounts"]),
("Crack & confirm", ["Crack the AS-REP (hashcat -m 18200); confirm a recovered password"])],
"Require Kerberos pre-auth; strong passwords", "Account compromise"),
A("ad_acl_privesc", "AD ACL / DACL Abuse", "dangerous Active Directory ACLs", "CWE-269", "High",
[("Map", ["Collect with bloodhound-python/SharpHound; find GenericAll/WriteDACL/ForceChangePassword edges"]),
("Confirm", ["Demonstrate one safe, reversible control step (e.g. shadow-cred / targeted password reset in a lab) proving the path"])],
"Tighten ACLs; tiered admin model", "Domain privilege escalation"),
A("ad_dcsync", "AD DCSync Exposure", "replication rights enabling DCSync", "CWE-269", "Critical",
[("Check rights", ["Identify principals with DS-Replication-Get-Changes(-All) via BloodHound/ACL review"]),
("Confirm", ["With authorized creds, prove replication right (e.g. impacket secretsdump -just-dc-user for a single test account)"])],
"Remove replication rights from non-DC principals", "Full domain credential compromise"),
A("ad_default_creds", "AD/Host Default & Reused Credentials", "default or reused credentials across the domain", "CWE-798", "High",
[("Spray (authorized, throttled)", ["With supplied account list, `netexec smb {target} -u users -p pass --continue-on-success` within ROE"]),
("Confirm", ["Show a successful authentication that should not have worked (reused/default cred)"])],
"Rotate defaults; enforce unique strong passwords; lockout", "Lateral movement, domain access"),
]
def main():
os.makedirs(OUT, exist_ok=True)
for a in INFRA:
open(os.path.join(OUT, a["name"] + ".md"), "w").write(render(a))
print(f"wrote {len(INFRA)} infra agents to {OUT}")
if __name__ == "__main__":
main()
+183
View File
@@ -0,0 +1,183 @@
#!/usr/bin/env python3
"""
NeuroSploit v3.5.2 exploitation-depth & report-hygiene doctrine agents.
Distilled from reviewing real AI-pentest output that kept stopping at
"exposed" instead of "exploited". Emits meta-agents to agents_md/meta/ that
push the engine past detection to demonstrated impact, chain findings, decode
artifacts/correlate CVEs, audit tokens, and keep the report honest (dedup +
severity calibration). Credits: Joas A Santos & Red Team Leaders.
"""
import os
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
OUT = os.path.join(ROOT, "agents_md", "meta")
CREDITS = "Credits: Joas A Santos and Red Team Leaders."
def render(a):
L = [f"# {a['title']}\n",
f"> Meta-agent (v3.5.2 doctrine). {a['tagline']}\n",
"## User Prompt",
a["user"].strip(), "",
"## System Prompt",
a["system"].strip() + " " + CREDITS]
return "\n".join(L) + "\n"
AGENTS = [
{"name": "exploit_depth_doctrine",
"title": "Exploitation Depth Doctrine Agent",
"tagline": "Turns every exposure into an exploitation attempt before it becomes a finding.",
"user": """
You are reviewing the candidate findings and live transcript for **{target}**.
For EACH candidate that merely *exposes* something (information disclosure,
exposed service/catalog/WSDL, leaked credential or token, reachable dev/staging
host, permissive CORS, open .git), drive it one step further BEFORE it is
reported:
1. **Use what was exposed.** Call the exposed endpoint, decode the leaked
artifact, log in with the leaked credential, hit the dev host, send the
cross-origin request. Capture the real request/response.
2. **Decide honestly.** If using it proved impact keep/raise severity with the
new evidence. If it could not be used down-rate to a LEAD (low confidence),
never a confirmed High/Critical.
3. **Report the gap.** List any exposure you could not yet exploit, with the
exact next command to try, so the next round (or the human) can finish it.
Output JSON: {"escalations":[{id, action_taken, new_evidence, new_severity}],
"leads":[{id, why_not_proven, next_command}]}.
""",
"system": """
You are a senior exploitation lead. Detection is not a finding impact is. You
never let an info-disclosure, exposed service, leaked secret or reachable
non-prod host be reported as confirmed without an attempt to actually use it,
backed by a real tool receipt. Unproven impact is a lead, not a High. Authorized
engagement; no destructive or DoS actions.
"""},
{"name": "finding_chainer",
"title": "Finding Chainer Agent",
"tagline": "Reuses obtained access across modules and reports the chain, not the parts.",
"user": """
Given the confirmed findings and any sessions/tokens/credentials obtained during
the engagement on **{target}**, build exploitation CHAINS:
- Reuse every session/JWT/cookie/credential from one step against ALL other
modules and hosts in scope (a captcha/login bypass that yields a token unlocks
the entire authenticated surface use it).
- Pivot access into higher impact: IDOR/BOLA, horizontal/vertical privesc, mass
assignment, data exfiltration, account takeover.
- Combine separate weaknesses (e.g. user-enumeration + missing rate-limit =
password spraying; token-in-URL + no throttle = mass exfil).
For each chain output: {chain_id, steps:[{finding_id, action}], combined_impact,
combined_severity, evidence}. Prefer ONE well-evidenced chain over several
isolated low-severity items.
""",
"system": """
You are an exploit-chaining specialist. Isolated findings understate risk; the
real story is the chain. You always try to reuse obtained access across the
whole scope and escalate to business impact, reporting the combined chain with
concrete evidence. Authorized engagement; no destructive or DoS actions.
"""},
{"name": "artifact_decoder",
"title": "Artifact Decoder & CVE Correlator Agent",
"tagline": "Decodes opaque tokens/paths, fingerprints the stack, and maps versions to CVEs.",
"user": """
For **{target}**, inspect every opaque or technology-revealing artifact seen in
recon and responses:
1. **Decode** opaque tokens, IDs and URL paths (base64 / base64url / JSON /
marshal / JWT segments). A decoded value often reveals the framework or an
internal file path (e.g. a Dragonfly job `[["f","...file"]]`, a signed-URL
structure, a serialized object).
2. **Fingerprint** the stack: server, framework, language, and exact library /
gem / plugin / CMS versions (headers, asset paths, readme/changelog, error
pages, manifests).
3. **Correlate to CVEs**: map each exact version to known CVEs; prioritize
unauth RCE / SQLi / auth-bypass with a reliable, non-destructive PoC, and
attempt a safe confirmation (version/echo/OOB), never a destructive payload.
Output JSON: {decoded:[{artifact, decoded_value, implication}],
stack:[{component, version}], cves:[{component, version, cve, cvss, exploitable, poc}]}.
""",
"system": """
You decode the opaque and correlate the obvious. Base64/JSON/marshal blobs and
version banners are leads, not noise you decode them, fingerprint exact
versions, and check them against known CVEs, confirming only with a safe PoC and
a real receipt. Authorized engagement; no destructive or DoS actions.
"""},
{"name": "token_auditor",
"title": "Token & JWT Auditor Agent",
"tagline": "Attacks tokens: alg-confusion, none, kid/jku, signature checks, weak HS256 secrets.",
"user": """
For any session token or JWT issued by **{target}**, run a full auth-token audit:
1. **Decode** the header/payload; note alg (HS*/RS*/none), kid, jku, exp, claims.
2. **Algorithm attacks**: try `alg:none`, RSHS confusion (sign with the public
key as HMAC secret), and kid/jku injection. Confirm whether the server
actually verifies the signature (tamper a claim and replay).
3. **Weak secret**: for HS256, attempt to crack the signing secret offline
(wordlist/rules); a static or guessable shared secret (e.g. an `x-auth-*`
header value) is a strong lead if cracked, forge a token for any user.
4. **Lifecycle**: test reuse after logout, expiry enforcement, and refresh-token
revocation.
Output JSON: {token_type, alg, verified:true|false,
attacks:[{name, result, evidence}], forged_token_possible:true|false}.
""",
"system": """
You are a token-security specialist. Every JWT/session token gets audited for
algorithm confusion, none, kid/jku injection, real signature verification, weak
HS256 secrets, and lifecycle (logout/expiry/refresh). A forged or replayable
token is account takeover you prove it with a real receipt. Authorized
engagement; no destructive or DoS actions.
"""},
{"name": "report_calibrator",
"title": "Report Calibrator Agent",
"tagline": "Dedups by class, calibrates severity to proven impact, demands evidence per claim.",
"user": """
Before the final report for **{target}**, clean and calibrate the findings:
1. **Consolidate hygiene by class.** Merge repeated hygiene findings (missing
security headers, clickjacking, cookie flags, weak TLS, HSTS, version/banner
disclosure) into ONE finding per class with an affected-asset TABLE do not
inflate the count one-per-host.
2. **Calibrate severity to PROVEN impact.** High/Critical requires demonstrated
impact with evidence. Unproven DoS/abuse, "could/may/potential" language, or a
finding with no concrete payload/PoC cap to Low/Medium or mark
"(potential)". Recompute the CVSS vector to match the proven impact.
3. **Evidence per claim.** Every finding and every item in the "tests
performed" log — must carry a concrete request/response receipt; flag any
claim that has none, and any contradiction between the test log and the
findings.
Output JSON: {merged:[{class, severity, assets:[...]}],
recalibrated:[{id, old_severity, new_severity, reason}],
unevidenced:[{id_or_test, missing}]}.
""",
"system": """
You are a meticulous report editor. You group hygiene by class with an
asset table, calibrate every severity to demonstrated impact (no inflated
High/Critical, no padding the count with duplicates), and require a real
receipt behind every claim including each line of the tests-performed log.
Honest, deduplicated, evidence-backed reporting only.
"""},
]
def main():
os.makedirs(OUT, exist_ok=True)
for a in AGENTS:
open(os.path.join(OUT, a["name"] + ".md"), "w").write(render(a))
print(f"wrote {len(AGENTS)} v3.5.2 doctrine meta-agents to {OUT}")
if __name__ == "__main__":
main()
+22 -3
View File
@@ -25,14 +25,33 @@ cat <<'BANNER'
███╗ ██╗███████╗██╗ ██╗██████╗ ██████╗
████╗ ██║██╔════╝██║ ██║██╔══██╗██╔═══██╗ NeuroSploit installer
██╔██╗ ██║█████╗ ██║ ██║██████╔╝██║ ██║ v3.5.0 — Rust harness
██╔██╗ ██║█████╗ ██║ ██║██████╔╝██║ ██║ v3.5.4 — Rust harness
██║╚██╗██║██╔══╝ ██║ ██║██╔══██╗██║ ██║ by Joas A Santos
██║ ╚████║███████╗╚██████╔╝██║ ██║╚██████╔╝ & Red Team Leaders
╚═╝ ╚═══╝╚══════╝ ╚═════╝ ╚═╝ ╚═╝ ╚═════╝
BANNER
OS="$(uname -s)"
say "Detected OS: $OS"
# ---- platform detection (Linux / macOS / Windows-via-WSL/MSYS · x64 / arm64) ----
OS_RAW="$(uname -s)"
ARCH_RAW="$(uname -m)"
case "$OS_RAW" in
Linux*) OS="Linux" ;;
Darwin*) OS="macOS" ;;
MINGW*|MSYS*|CYGWIN*) OS="Windows" ;;
*) OS="$OS_RAW" ;;
esac
case "$ARCH_RAW" in
x86_64|amd64) ARCH="x64" ;;
arm64|aarch64) ARCH="arm64" ;;
*) ARCH="$ARCH_RAW" ;;
esac
say "Platform: $OS / $ARCH"
if [ "$OS" = "Windows" ]; then
warn "On native Windows, run this in WSL2, Git Bash or MSYS2. (Or build with: cargo build --release)"
fi
if [ "$OS" != "Linux" ] && [ "$OS" != "macOS" ] && [ "$OS" != "Windows" ]; then
warn "Unrecognized OS '$OS_RAW' — attempting a generic Rust build anyway."
fi
# 1) git
command -v git >/dev/null 2>&1 || die "git is required. Install git and re-run."