mirror of
https://github.com/CyberSecurityUP/NeuroSploit.git
synced 2026-08-14 21:50:21 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3ddb22ee25 | ||
|
|
69c5e3ddb9 | ||
|
|
1f8ccb6f9e | ||
|
|
3c49a83578 | ||
|
|
e956b482b9 | ||
|
|
105c62af61 | ||
|
|
a0a477a2bf | ||
|
|
cb19e2194d | ||
|
|
76b56898d1 |
@@ -51,6 +51,16 @@ TOGETHER_API_KEY=
|
||||
# openrouter: https://openrouter.ai/keys
|
||||
OPENROUTER_API_KEY=
|
||||
|
||||
# opencode: https://opencode.ai/auth (OpenCode Zen gateway)
|
||||
# Or skip the key entirely and use --subscription with the
|
||||
# `opencode` CLI logged into your own Zen/plan account.
|
||||
OPENCODE_API_KEY=
|
||||
|
||||
# nous: Nous Portal (https://portal.nousresearch.com) — Hermes models.
|
||||
# Or skip the key entirely and use --subscription with the
|
||||
# `hermes` CLI (`hermes setup --portal` for OAuth login).
|
||||
NOUS_API_KEY=
|
||||
|
||||
# ollama: local, no key needed. Override the endpoint if not default:
|
||||
#OLLAMA_BASE_URL=http://localhost:11434/v1
|
||||
|
||||
|
||||
@@ -108,3 +108,4 @@ data/repl_history.txt
|
||||
# Cloned source repos (whitebox/greybox from a git URL)
|
||||
repos/
|
||||
neurosploit-rs/repos/
|
||||
target/
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<h1 align="center">🧠 NeuroSploit v3.6.6</h1>
|
||||
<h1 align="center">🧠 NeuroSploit v3.6.9</h1>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://trendshift.io/repositories/22624?utm_source=trendshift-badge&utm_medium=badge&utm_campaign=badge-trendshift-22624" target="_blank" rel="noopener noreferrer"><img src="https://trendshift.io/api/badge/trendshift/repositories/22624/daily?language=Python" alt="JoasASantos%2FNeuroSploit | Trendshift" width="250" height="55"/></a>
|
||||
@@ -12,10 +12,10 @@
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="https://img.shields.io/badge/Version-3.6.6-blue?style=flat-square">
|
||||
<img src="https://img.shields.io/badge/Version-3.6.9-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-430-red?style=flat-square">
|
||||
<img src="https://img.shields.io/badge/MD%20Agents-435-red?style=flat-square">
|
||||
<img src="https://img.shields.io/badge/Models-16%20providers-success?style=flat-square">
|
||||
<img src="https://img.shields.io/badge/Modes-Black%20%7C%20White%20%7C%20Grey%20%7C%20Host%20%7C%20AI-9cf?style=flat-square">
|
||||
<img src="https://img.shields.io/badge/Auth-API%20key%20%7C%20Subscription-orange?style=flat-square">
|
||||
@@ -36,7 +36,7 @@ LLMs** — via **API key** or local **subscription** (Claude Code / Codex / Gemi
|
||||
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 **430 markdown agents** and a **Mission
|
||||
grounding** before reporting. It ships **435 markdown agents** and a **Mission
|
||||
Control TUI**.
|
||||
|
||||
### Engagement modes
|
||||
@@ -69,9 +69,14 @@ Control TUI**.
|
||||
flags, CORS reflection, tech fingerprint, linked JS, 404 baseline, high-signal
|
||||
paths) and feeds those observed facts into recon, so agent selection and
|
||||
exploitation decisions are grounded in evidence — not the model's guess.
|
||||
- 🔗 **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 chaining — any primitive pivots.** 13 multi-stage chain agents
|
||||
(SQLi→RCE→LPE, SSRF→cloud creds, upload→LFI→RCE→LPE, CVE→RCE→pivot, …) **plus a
|
||||
chaining doctrine** that turns *any* confirmed foothold into the next step:
|
||||
reduce it to a primitive (exec / read / write / request-forgery / identity /
|
||||
secret) and pivot — file-upload→RCE, SSRF→metadata creds, IDOR→takeover — reusing
|
||||
looted creds and reasoning about **business logic** (payment/tenancy/workflow
|
||||
abuse). Each stage proven; strictly non-destructive (no data loss, no DB
|
||||
overwrite, no DoS).
|
||||
- ☁️ **Cloud testing** — AWS / GCP / Azure agents that drive the provider CLIs
|
||||
(`aws`/`gcloud`/`az`). Connect via `creds.yaml`: AWS keys, a Google
|
||||
service-account JSON, or an Azure service principal — see
|
||||
@@ -84,12 +89,23 @@ Control TUI**.
|
||||
attacker→**LLM-judge** loop (baseline refusal → technique → verdict) and proves
|
||||
the bypass with a **benign, redacted** receipt. Maps to OWASP LLM Top 10 (2025),
|
||||
MCP threats & OWASP AI Exchange; Skill/plugin & **n8n** files audited white-box.
|
||||
- 🧰 **Misconfig & CVE hunting, safely** — dedicated agents for absurd
|
||||
misconfigs (exposed `.git`/`.env`, debug/actuator, default creds, dashboards,
|
||||
CORS), a **CVE Hunter** (smart, targeted `nuclei`), a **PoC Developer** (writes
|
||||
reproducible scripts to the run's `pocs/`), and **rate-limit** testing — all
|
||||
under a strict **data-safety/PII guardrail** (no destructive or state-changing
|
||||
- 🧰 **Misconfig & CVE hunting → exploitation, safely** — a full CVE pipeline:
|
||||
**version fingerprint** (pin exact versions) → **research analyst** (map to
|
||||
NVD/GHSA CVEs, judge reachability) → **PoC finder** (locate/vet/adapt a public
|
||||
PoC) → **exploit scripter** (write a custom exploit when none exists). Every PoC
|
||||
is written to the run's **`pocs/` folder and referenced in the report** so
|
||||
findings are reproducible. Plus absurd-misconfig agents (exposed `.git`/`.env`,
|
||||
debug/actuator, default creds, dashboards, CORS) and rate-limit testing — all
|
||||
under a strict **data-safety/PII guardrail** (no destructive/state-changing
|
||||
actions; PII proven with a masked sample, never dumped).
|
||||
- 🎯 **Re-test one vulnerability** — `--only <agent>` (repeatable /
|
||||
comma-separated) runs exactly the agent(s) you name and skips recon-based
|
||||
selection — re-test a single finding fast. Works on `run` / `whitebox` /
|
||||
`greybox`; `neurosploit agents` lists the names.
|
||||
- 🔬 **White-box stays white-box** — code agents run under a static-review
|
||||
doctrine (symbolic `file:line` receipts, source-to-sink taint tracing, manifest
|
||||
version→CVE) that forbids hallucinated live/black-box network actions, and can
|
||||
emit a repro PoC to `pocs/`.
|
||||
- 🗣️ **Natural-language REPL** — in the interactive session, just describe what
|
||||
you want, in any language: *"testa https://loja.com com opus, foco em SQLi,
|
||||
fora de escopo /admin, roda"*. A hybrid parser sets target/models/focus/
|
||||
@@ -417,6 +433,8 @@ export GROQ_API_KEY=... # groq:*
|
||||
export TOGETHER_API_KEY=... # together:*
|
||||
export MOONSHOT_API_KEY=... # moonshot:* (Kimi K3/K2)
|
||||
export OPENROUTER_API_KEY=... # openrouter:*
|
||||
export OPENCODE_API_KEY=... # opencode:* (OpenCode Zen gateway)
|
||||
export NOUS_API_KEY=... # nous:* (Nous Portal — Hermes)
|
||||
# ollama / llamacpp need no key (local)
|
||||
|
||||
# then run via API (note: NO --subscription)
|
||||
@@ -446,6 +464,8 @@ Or put the keys in a `.env` and source it (`cp .env.example .env`; edit; `set -a
|
||||
| `together:` | `TOGETHER_API_KEY` | api.together.xyz |
|
||||
| `moonshot:` | `MOONSHOT_API_KEY` | api.moonshot.ai |
|
||||
| `openrouter:` | `OPENROUTER_API_KEY` | openrouter.ai |
|
||||
| `opencode:` | `OPENCODE_API_KEY` | opencode.ai/zen (OpenCode Zen gateway) |
|
||||
| `nous:` | `NOUS_API_KEY` | inference-api.nousresearch.com (Hermes 4) |
|
||||
| `ollama:` | _(none)_ | localhost:11434 |
|
||||
| `llamacpp:` | _(none)_ | localhost:8080 |
|
||||
|
||||
@@ -468,6 +488,12 @@ install and log into one of the CLIs first:
|
||||
| `openai:` | `codex` | `codex` login |
|
||||
| `gemini:` | `gemini` | `gemini` login |
|
||||
| `xai:` | `grok` | `grok` login |
|
||||
| `opencode:` | `opencode` | `opencode auth login` (or `/connect` in the TUI) — Zen/plan account |
|
||||
| `nous:` | `hermes` | `hermes setup --portal` — Nous Portal OAuth |
|
||||
|
||||
`opencode:` also gets the Playwright MCP (`--mcp`) like anthropic/openai do.
|
||||
`nous:` relies on Hermes's own built-in toolsets (web/terminal/computer-use)
|
||||
instead — it has no CLI-level MCP hook.
|
||||
|
||||
```bash
|
||||
./target/release/neurosploit run http://testphp.vulnweb.com/ \
|
||||
|
||||
+116
@@ -1,3 +1,119 @@
|
||||
# NeuroSploit v3.6.8 — Release Notes
|
||||
|
||||
**Release Date:** August 2026
|
||||
**Codename:** Chain & Exploit
|
||||
**License:** MIT
|
||||
**Credits:** Joas A Santos & Red Team Leaders
|
||||
|
||||
## v3.6.8 — Auth resilience, circuit breaker, recon budget, Ollama error handling, empty-evidence validation
|
||||
|
||||
### Recon Time Budget (NEW)
|
||||
|
||||
- **5-minute total recon budget.** Recon phase is now time-boxed to 300 seconds
|
||||
across ALL rounds. Previously, a single recon round could run 150+ commands
|
||||
over 15 minutes via subscription CLI, leaving no time for exploitation.
|
||||
- **Per-round budget directive.** Each recon round receives a prompt instruction
|
||||
with its share of the time budget (e.g. "~100 seconds for this round") and a
|
||||
command count guideline (30-50 commands max). The model is instructed to
|
||||
prioritise high-signal actions and stop early when enough intel is gathered.
|
||||
- **Elapsed time check between rounds.** Before starting each follow-up round,
|
||||
the pipeline checks elapsed time. If the budget is exhausted, recon stops
|
||||
immediately and proceeds to exploitation with the intelligence gathered so far.
|
||||
|
||||
### Auth Resilience & Circuit Breaker (NEW)
|
||||
|
||||
- **`is_auth_failure()` detector.** New function recognises OAuth token revocation
|
||||
(401), session expiry, invalid/revoked API keys, and "not logged in" errors from
|
||||
subscription CLIs. Distinct from `is_exhaustion()` (quota/rate-limit) — auth
|
||||
failures are non-recoverable without re-login or provider switch.
|
||||
- **Circuit breaker (3 consecutive auth failures → auto-pause).** A shared atomic
|
||||
counter tracks consecutive auth failures across ALL agents. After 3 failures the
|
||||
pool pauses the run BEFORE burning through the remaining agents on a dead token.
|
||||
Previously, a revoked OAuth token caused all 66+ agents to silently return 0
|
||||
findings with no pause or warning.
|
||||
- **Auth-aware park: findings preserved, fallback offered.** When auth fails the
|
||||
run parks with a clear message:
|
||||
`⏸ authentication failed (...). Run is PAUSED — all findings so far are SAFE.`
|
||||
The user can `/continue openai:gpt-5.1` (or any provider) to switch and resume.
|
||||
All `LiveCheckpoint` findings on disk are preserved across the pause.
|
||||
- **No retry burn on auth failure.** `one()` returns immediately on auth errors
|
||||
instead of retrying 3 times against a dead token (same as quota exhaustion).
|
||||
- **Recon preserves probe facts on auth failure.** When model recon fails with an
|
||||
auth error, the HTTP probe data is still returned and the pipeline continues
|
||||
with probe-only intelligence instead of silently dropping everything.
|
||||
- **Phase tracking for auth pauses.** The REPL status line shows `paused (auth)`
|
||||
(distinct from `paused (quota)`) so the operator knows the root cause at a glance.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
- **Better Ollama/local provider error messages.** Connection-refused and timeout
|
||||
errors now name the provider, URL, and suggest checking if the server is running.
|
||||
Previously showed raw reqwest errors.
|
||||
- **Empty-evidence findings skip the vote and go straight to `needs-review`.**
|
||||
Findings with no evidence are unverifiable by the adversarial validator (which
|
||||
always rejects "no evidence" per its system prompt). Now they bypass the vote
|
||||
and are flagged for human review instead of being silently dropped.
|
||||
- **Single-model + vote_n=1 warning.** When only one model is configured and
|
||||
vote_n is 1, the pipeline emits a warning that validation is weaker (same model
|
||||
validates its own findings).
|
||||
- **JSON parse resilience for local models.** `extract_findings` now logs when a
|
||||
model returns text but no parseable JSON (previously silent drop — 0 findings
|
||||
with no diagnostic). Also auto-fixes trailing-comma JSON (`[...,]`) which small
|
||||
models commonly produce.
|
||||
- **Visible diagnostics when agents return 0 findings.** Pipeline emits the
|
||||
response tail so the operator can see what the model actually returned (helps
|
||||
debug model quality issues with local/small models).
|
||||
|
||||
---
|
||||
|
||||
## v3.6.7 Highlights
|
||||
|
||||
- **CVE exploitation pipeline — 4 new agents.** `cve_version_fingerprint` (pin
|
||||
exact versions) → `cve_research_analyst` (map to NVD/GHSA, judge reachability) →
|
||||
`cve_poc_finder` (locate/vet/adapt a public PoC) → `cve_exploit_scripter` (write
|
||||
a custom exploit when none exists). Focus: actually exploiting vulns that have
|
||||
CVEs, not just flagging versions.
|
||||
- **PoCs land in the run's `pocs/` folder and are listed in the report.** Every
|
||||
agent writes runnable proofs to `$NEUROSPLOIT_POCS`; the report gains a
|
||||
**"Reproduction — PoC scripts"** section so findings replay end-to-end.
|
||||
- **Chaining for any primitive.** New `CHAIN_DOCTRINE` + a `chain_cve_to_rce_to_pivot`
|
||||
recipe turn any confirmed foothold into the next step (upload→RCE, SSRF→cloud
|
||||
creds, IDOR→takeover, CVE→RCE→pivot), reusing looted creds and reasoning about
|
||||
**business logic** — strictly non-destructive (no data loss / DB overwrite / DoS).
|
||||
- **`--only <agent>` — re-test a single vulnerability.** Runs exactly the named
|
||||
agent(s), skipping recon selection. On `run` / `whitebox` / `greybox`; repeatable
|
||||
or comma/semicolon-separated. (Implements the previously-dead `pinned` allowlist.)
|
||||
- **White-box stays white-box.** A `WHITEBOX_DOCTRINE` keeps code agents in static
|
||||
source-review mode (symbolic `file:line` receipts, source→sink taint, manifest
|
||||
version→CVE) and blocks hallucinated live/black-box network actions; agents can
|
||||
emit a repro PoC.
|
||||
- **435 markdown agents** (was 430).
|
||||
|
||||
**Full changelog:** https://github.com/JoasASantos/NeuroSploit/compare/v3.6.6...v3.6.7
|
||||
|
||||
---
|
||||
|
||||
# NeuroSploit v3.6.6 — Release Notes
|
||||
|
||||
**Release Date:** August 2026
|
||||
**Codename:** Local & Uncensored
|
||||
**License:** MIT
|
||||
**Credits:** Joas A Santos & Red Team Leaders
|
||||
|
||||
## Highlights
|
||||
|
||||
- **Local, uncensored & CPU-only — new `llamacpp:` provider.** Drives a
|
||||
`llama-server` OpenAI-compatible endpoint (`localhost:8080`), **no API key**,
|
||||
no data off-host, CPU-only or GPU-offloaded. Override with `LLAMACPP_BASE_URL`;
|
||||
`model` = the gguf you loaded (pass-through). **15 → 16 providers.**
|
||||
- **clippy clean under `-D warnings`** across the workspace.
|
||||
- **Rust CI template** — `examples/github-actions/ci.yml` (build / test / clippy)
|
||||
for the `neurosploit-rs/` workspace.
|
||||
|
||||
**Full changelog:** https://github.com/JoasASantos/NeuroSploit/compare/v3.6.5...v3.6.6
|
||||
|
||||
---
|
||||
|
||||
# NeuroSploit v3.6.5 — Release Notes
|
||||
|
||||
**Release Date:** July 2026
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
# NeuroSploit — Tutorial & User Guide (v3.6.5)
|
||||
# NeuroSploit — Tutorial & User Guide (v3.6.9)
|
||||
|
||||
A complete, hands-on guide to installing, configuring and running NeuroSploit —
|
||||
the autonomous, multi-model penetration-testing harness.
|
||||
@@ -98,7 +98,7 @@ Agents **degrade gracefully**: if `rustscan` is absent they use `nmap`; if neith
|
||||
### Verify
|
||||
|
||||
```bash
|
||||
neurosploit --version # neurosploit 3.6.5
|
||||
neurosploit --version # neurosploit 3.6.9
|
||||
neurosploit agents # {"vulns":241,...,"ai":30,...,"total":430}
|
||||
neurosploit models # all providers & models
|
||||
```
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
# Known-CVE → RCE → Pivot Chain Agent
|
||||
|
||||
## User Prompt
|
||||
You are executing a multi-stage ATTACK CHAIN against **{target}**: a known CVE in a fingerprinted component → code execution → post-exploitation pivot.
|
||||
|
||||
**Recon Context / prior findings:**
|
||||
{recon_json}
|
||||
|
||||
**GOAL:** Turn a version-matched, reachable CVE into demonstrated RCE/access, then pivot — safely.
|
||||
|
||||
**CHAIN — advance stage by stage; PROVE every stage with raw tool output before advancing:**
|
||||
|
||||
### Stage 1. Pin the target CVE
|
||||
- From the component+version inventory, pick the highest-impact reachable CVE (unauth RCE/SQLi/SSRF/deserialization first). Confirm preconditions are met
|
||||
|
||||
### Stage 2. Obtain a safe PoC
|
||||
- Reuse a vetted public PoC or write one to `$NEUROSPLOIT_POCS`. STRIP any destructive payload; use a benign marker (`id`, unique echo, OOB callback)
|
||||
|
||||
### Stage 3. Execute & confirm
|
||||
- Run it non-destructively against the authorized target; capture output proving exploitation (marker/OOB/leak)
|
||||
|
||||
### Stage 4. Pivot
|
||||
- From the foothold: loot creds/keys/config/source, reuse them, escalate privileges, reach internal services/cloud metadata, or expand to adjacent hosts — each step proven, none destructive
|
||||
|
||||
### 5. Report Format
|
||||
Report the chain as ONE finding (plus per-stage evidence):
|
||||
```
|
||||
FINDING:
|
||||
- Title: [CVE-id] → RCE → Pivot Chain
|
||||
- Severity: Critical
|
||||
- CWE: CWE-1395
|
||||
- Endpoint: [entry point]
|
||||
- Vector: [full chain, stage by stage]
|
||||
- Payload: [PoC path in $NEUROSPLOIT_POCS + key commands per stage]
|
||||
- Evidence: [raw output proving EACH stage]
|
||||
- Impact: [demonstrated compromise + what the pivot reached]
|
||||
- Remediation: Patch to the fixed version; segment/limit blast radius; rotate exposed secrets
|
||||
- chains_from: [ids of the prerequisite findings this builds on]
|
||||
```
|
||||
|
||||
## System Prompt
|
||||
You are an exploit-chaining specialist for known CVEs. Only advance a stage after the previous one is proven with a real tool receipt — never assume. Save any PoC to $NEUROSPLOIT_POCS and cite it. If a stage can't be proven, stop and report the chain up to the last proven stage. AUTHORIZED engagement. DATA SAFETY: benign proof only — never destroy/overwrite/encrypt/mass-exfiltrate data, drop databases, or DoS; mask PII; reuse looted creds only against the authorized target. Credits: Joas A Santos & Red Team Leaders.
|
||||
@@ -0,0 +1,39 @@
|
||||
# CVE Exploit Scripter Agent
|
||||
|
||||
## User Prompt
|
||||
You are testing **{target}**: when no clean public PoC exists for a confirmed-candidate CVE, WRITE a custom exploitation script and prove it safely.
|
||||
|
||||
**Recon Context:**
|
||||
{recon_json}
|
||||
|
||||
**METHODOLOGY:**
|
||||
|
||||
### 1. Decide
|
||||
- Use this when the CVE is reachable but there's no usable public PoC, or the public one is destructive/unsuitable and must be rebuilt safely
|
||||
|
||||
### 2. Build from the advisory
|
||||
- From the CVE/advisory and the component's behaviour, derive the exact request/steps that trigger the bug. Write a runnable script (python/bash/curl) to `$NEUROSPLOIT_POCS` with a header comment: target, CVE id, what it proves, usage
|
||||
|
||||
### 3. Make it safe by construction
|
||||
- Use a BENIGN proof: echo a unique marker, trigger an OOB DNS/HTTP callback, read a non-sensitive indicator, or run `id`/version — never a payload that deletes/overwrites data, drops the DB, or DoSes. Idempotent and minimal
|
||||
|
||||
### 4. Run & confirm
|
||||
- Execute against the authorized target; capture raw output proving exploitation. Keep the script in `$NEUROSPLOIT_POCS` and reference its path so the finding is fully reproducible
|
||||
|
||||
### 5. Report Format
|
||||
For each CONFIRMED finding:
|
||||
```
|
||||
FINDING:
|
||||
- Title: [CVE-id] exploited via custom script on [component]
|
||||
- Severity: [CVSS/impact]
|
||||
- CWE: [CVE's CWE]
|
||||
- Endpoint: [full URL/resource]
|
||||
- Vector: [technique built from the advisory]
|
||||
- Payload: [script path in $NEUROSPLOIT_POCS + exact invocation]
|
||||
- Evidence: [raw output proving exploitation - marker/OOB/leak]
|
||||
- Impact: [demonstrated impact - up to full compromise]
|
||||
- Remediation: Upgrade to the fixed version; apply advisory mitigations
|
||||
```
|
||||
|
||||
## System Prompt
|
||||
You are a custom-exploit developer for known CVEs. AUTHORIZED engagement. Build the exploit from the advisory and PROVE it with a benign, non-destructive marker only. ALWAYS write the script to $NEUROSPLOIT_POCS with a header comment and cite its path — reproducibility is mandatory. Report ONLY what a real tool receipt proves; if you cannot reach a working benign PoC, report the CVE as a reachable exposure, not a confirmed exploit. DATA SAFETY: never destroy/overwrite/encrypt/mass-exfiltrate data or change state beyond the minimal proof; mask PII; no destructive/DoS. Credits: Joas A Santos and Red Team Leaders.
|
||||
@@ -0,0 +1,39 @@
|
||||
# CVE PoC Finder Agent
|
||||
|
||||
## User Prompt
|
||||
You are testing **{target}**: find, vet and run a PUBLIC proof-of-concept for a confirmed-candidate CVE, safely.
|
||||
|
||||
**Recon Context:**
|
||||
{recon_json}
|
||||
|
||||
**METHODOLOGY:**
|
||||
|
||||
### 1. Locate a PoC
|
||||
- Search `searchsploit`/Exploit-DB, GitHub (CVE id + component), NVD references, `nuclei` templates (`-t` for the CVE/tech tags — targeted, not a blind full scan), packet-storm, vendor advisories
|
||||
|
||||
### 2. Vet before you run
|
||||
- READ the PoC first. Reject/neutralise anything destructive (drops tables, wipes files, ransomware-style, mass requests/DoS, backdoors). Understand exactly what it does and what it proves
|
||||
|
||||
### 3. Adapt & stage
|
||||
- `git clone`/download into the run's `$NEUROSPLOIT_POCS` directory. Parameterise it for THIS target (URL, port, path, auth). Replace any harmful payload with a benign marker (`id`, unique echo string, OOB DNS/HTTP callback)
|
||||
|
||||
### 4. Run & confirm
|
||||
- Execute non-destructively against the authorized target; capture raw output that proves the CVE (marker echoed, OOB hit, expected leak). Keep the exact script in `$NEUROSPLOIT_POCS` so the finding is reproducible
|
||||
|
||||
### 5. Report Format
|
||||
For each CONFIRMED finding:
|
||||
```
|
||||
FINDING:
|
||||
- Title: [CVE-id] exploited via public PoC on [component]
|
||||
- Severity: [CVSS/impact]
|
||||
- CWE: [CVE's CWE]
|
||||
- Endpoint: [full URL/resource]
|
||||
- Vector: [technique + PoC source]
|
||||
- Payload: [PoC path in $NEUROSPLOIT_POCS + exact invocation]
|
||||
- Evidence: [raw output proving exploitation - marker/OOB/leak]
|
||||
- Impact: [demonstrated impact]
|
||||
- Remediation: Upgrade to the fixed version; apply advisory mitigations
|
||||
```
|
||||
|
||||
## System Prompt
|
||||
You are a public-PoC exploitation specialist. AUTHORIZED engagement. ALWAYS read a third-party PoC before running it and STRIP any destructive/DoS/backdoor behaviour — swap harmful payloads for benign markers. Save the adapted PoC to $NEUROSPLOIT_POCS and cite its path so the result is reproducible. Report ONLY what a real tool receipt proves. DATA SAFETY: never modify/delete/overwrite/exfiltrate data or change state beyond the minimal benign proof; mask PII; no destructive/DoS. Credits: Joas A Santos and Red Team Leaders.
|
||||
@@ -0,0 +1,40 @@
|
||||
# CVE Research Analyst Agent
|
||||
|
||||
## User Prompt
|
||||
You are testing **{target}**: research known CVEs for the fingerprinted components and decide which are actually exploitable HERE.
|
||||
|
||||
**Recon Context:**
|
||||
{recon_json}
|
||||
|
||||
**METHODOLOGY:**
|
||||
|
||||
### 1. Map versions → CVEs
|
||||
- For each component+version, enumerate CVEs (NVD, GitHub Security Advisories/GHSA, vendor advisories, distro trackers, `searchsploit`). Record CVE id, CVSS, affected/fixed versions, vulnerability class
|
||||
|
||||
### 2. Assess exploitability HERE
|
||||
- Filter to CVEs whose preconditions the target actually meets (reachable endpoint/feature, required config/module enabled, auth level you can reach). Prioritise unauth **RCE / SQLi / auth-bypass / SSRF / deserialization**
|
||||
- Note whether a public PoC/exploit exists (feeds `cve_poc_finder`) or a custom script is needed (feeds `cve_exploit_scripter`)
|
||||
|
||||
### 3. Rank
|
||||
- Order candidates by (impact × exploitability × reachability). Discard theoretical/unreachable CVEs
|
||||
|
||||
### 4. Confirm safely
|
||||
- Where a benign version/behaviour check can confirm the CVE is present (without exploiting), run it and cite the output
|
||||
|
||||
### 5. Report Format
|
||||
For each candidate (Confirmed if a benign check proves presence, else a version-match lead):
|
||||
```
|
||||
FINDING:
|
||||
- Title: [CVE-id] in [component] [version]
|
||||
- Severity: [map from CVSS/impact]
|
||||
- CWE: [CVE's CWE, e.g. CWE-1395]
|
||||
- Endpoint: [reachable resource]
|
||||
- Vector: [class + preconditions met]
|
||||
- Payload: [benign confirmation check, if run]
|
||||
- Evidence: [raw output / advisory + version match]
|
||||
- Impact: [what the CVE yields — up to full compromise]
|
||||
- Remediation: Upgrade to [fixed version]; apply advisory mitigations
|
||||
```
|
||||
|
||||
## System Prompt
|
||||
You are a CVE research analyst. AUTHORIZED engagement. Distinguish "version matches a CVE" (lead) from "CVE is present and reachable here" (confirmed by a benign check) — never inflate a version match into a confirmed exploit. Cite the advisory and the exact affected/fixed version. Hand exploitation to the PoC finder / exploit scripter. DATA SAFETY: read-only research + benign checks only; no state change; mask PII; no destructive/DoS. Credits: Joas A Santos and Red Team Leaders.
|
||||
@@ -0,0 +1,37 @@
|
||||
# CVE Version Fingerprint Agent
|
||||
|
||||
## User Prompt
|
||||
You are testing **{target}** to pin the EXACT version of every component so known CVEs can be mapped precisely.
|
||||
|
||||
**Recon Context:**
|
||||
{recon_json}
|
||||
|
||||
**METHODOLOGY:**
|
||||
|
||||
### 1. Fingerprint every layer
|
||||
- Server/proxy (`Server`, `Via`, `X-Powered-By`), app framework, CMS + plugins/themes, JS libraries (from `<script>` src, source maps, `/package.json`, bundle comments), API framework, TLS stack
|
||||
- Pull versions from: response headers, default/readme/changelog files (`/readme.html`, `/CHANGELOG.md`, `/*.txt`), favicon hash, static asset hashes, error pages, `/.well-known`, `robots.txt`, JS build manifests
|
||||
|
||||
### 2. Disambiguate
|
||||
- When only a range is visible, narrow it: compare asset hashes/behaviour between adjacent releases, check feature/endpoint presence, read embedded build ids/commit hashes
|
||||
|
||||
### 3. Build the inventory
|
||||
- Produce a component → EXACT version table; mark confidence (exact vs range). This inventory feeds `cve_research_analyst` / `cve_hunter`
|
||||
|
||||
### 4. Report Format
|
||||
For each identified component (report as a finding only when the version has known CVEs; otherwise fold into the inventory):
|
||||
```
|
||||
FINDING:
|
||||
- Title: Version Fingerprint - [component] [version]
|
||||
- Severity: Info
|
||||
- CWE: CWE-200
|
||||
- Endpoint: [source header/file/asset]
|
||||
- Vector: [how the version was determined]
|
||||
- Payload: [exact request/hash used]
|
||||
- Evidence: [raw header/file snippet proving the version]
|
||||
- Impact: Enables precise CVE mapping and targeted exploitation
|
||||
- Remediation: Suppress version banners; keep components patched
|
||||
```
|
||||
|
||||
## System Prompt
|
||||
You are a software version-fingerprinting specialist. AUTHORIZED engagement. Report ONLY versions you proved from a real receipt (raw header/file/hash) — never guess a version. Prefer EXACT versions; state confidence when only a range is provable. Your inventory is the input to CVE mapping, so accuracy matters more than volume. DATA SAFETY: read-only; no state change; mask any PII. No destructive/DoS actions. Credits: Joas A Santos and Red Team Leaders.
|
||||
+2
-2
@@ -14,7 +14,7 @@ function Ok ($m) { Write-Host " + $m" -ForegroundColor Green }
|
||||
function Warn($m){ Write-Host " ! $m" -ForegroundColor Yellow }
|
||||
|
||||
Write-Host ""
|
||||
Write-Host " NeuroSploit installer (Windows) — v3.6.1" -ForegroundColor Cyan
|
||||
Write-Host " NeuroSploit installer (Windows) — v3.6.9" -ForegroundColor Cyan
|
||||
|
||||
# arch → asset arch (only x64 prebuilt today; arm64 falls back to source)
|
||||
$rawArch = $env:PROCESSOR_ARCHITECTURE
|
||||
@@ -29,7 +29,7 @@ $ref = $env:NEUROSPLOIT_REF
|
||||
if (-not $ref) {
|
||||
try { $ref = (Invoke-RestMethod "https://api.github.com/repos/$slug/releases/latest").tag_name } catch { }
|
||||
}
|
||||
if (-not $ref) { $ref = "v3.6.1" }
|
||||
if (-not $ref) { $ref = "v3.6.9" }
|
||||
Say "Release: $ref"
|
||||
|
||||
New-Item -ItemType Directory -Force -Path $dir | Out-Null
|
||||
|
||||
Generated
+2
-2
@@ -871,7 +871,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "neurosploit"
|
||||
version = "3.6.6"
|
||||
version = "3.6.9"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
@@ -888,7 +888,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "neurosploit-harness"
|
||||
version = "3.6.6"
|
||||
version = "3.6.9"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"futures",
|
||||
|
||||
@@ -3,7 +3,7 @@ members = ["crates/harness", "app"]
|
||||
resolver = "2"
|
||||
|
||||
[workspace.package]
|
||||
version = "3.6.6"
|
||||
version = "3.6.9"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
repository = "https://github.com/JoasASantos/NeuroSploit"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! NeuroSploit v3.6.6 — interactive harness + CLI (`run` / `whitebox` / `agents` / `models`).
|
||||
//! NeuroSploit v3.6.9 — interactive harness + CLI (`run` / `whitebox` / `agents` / `models`).
|
||||
|
||||
mod repl;
|
||||
mod tui;
|
||||
@@ -11,9 +11,9 @@ use std::path::{Path, PathBuf};
|
||||
#[command(
|
||||
name = "neurosploit",
|
||||
version,
|
||||
about = "NeuroSploit v3.6.6 — multi-model autonomous pentest harness",
|
||||
long_about = "NeuroSploit v3.6.6 — 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. \
|
||||
about = "NeuroSploit v3.6.9 — multi-model autonomous pentest harness",
|
||||
long_about = "NeuroSploit v3.6.9 — a Rust multi-model harness that drives a pool of LLMs \
|
||||
(API key or local subscription: Claude/Codex/Gemini/Grok/OpenCode/Hermes) 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\
|
||||
Run with NO arguments for an interactive wizard.\n\n\
|
||||
@@ -54,7 +54,7 @@ enum Cmd {
|
||||
recon: usize,
|
||||
#[arg(long)]
|
||||
offline: bool,
|
||||
/// Use local agentic CLI subscription (Claude/Codex/Gemini/Grok login).
|
||||
/// Use local agentic CLI subscription (Claude/Codex/Gemini/Grok/OpenCode/Hermes login).
|
||||
#[arg(long)]
|
||||
subscription: bool,
|
||||
/// Enable Playwright MCP (auto-installed if missing; backends that don't
|
||||
@@ -77,6 +77,11 @@ enum Cmd {
|
||||
/// Open a Jira card per finding (needs the jira integration enabled).
|
||||
#[arg(long)]
|
||||
jira: bool,
|
||||
/// Re-test ONLY these agent(s), skipping recon-based selection — repeatable
|
||||
/// or comma/semicolon-separated (e.g. `--only sqli --only cve_hunter`).
|
||||
/// Run `neurosploit agents` for the names.
|
||||
#[arg(long = "only")]
|
||||
only: Vec<String>,
|
||||
/// Verbose: log each agent as it launches, recon, and votes.
|
||||
#[arg(short, long)]
|
||||
verbose: bool,
|
||||
@@ -105,6 +110,9 @@ enum Cmd {
|
||||
/// Open a Jira card per finding (needs the jira integration enabled).
|
||||
#[arg(long)]
|
||||
jira: bool,
|
||||
/// Re-test ONLY these code agent(s) — repeatable or comma/semicolon-separated.
|
||||
#[arg(long = "only")]
|
||||
only: Vec<String>,
|
||||
#[arg(short, long)]
|
||||
verbose: bool,
|
||||
},
|
||||
@@ -139,6 +147,9 @@ enum Cmd {
|
||||
subscription: bool,
|
||||
#[arg(long)]
|
||||
mcp: bool,
|
||||
/// Re-test ONLY these agent(s) — repeatable or comma/semicolon-separated.
|
||||
#[arg(long = "only")]
|
||||
only: Vec<String>,
|
||||
#[arg(short, long)]
|
||||
verbose: bool,
|
||||
},
|
||||
@@ -379,7 +390,7 @@ async fn main() -> anyhow::Result<()> {
|
||||
}
|
||||
}
|
||||
}
|
||||
Cmd::Run { url, models, max_agents, vote_n, chain_depth, recon, offline, subscription, mcp, creds, focus, objective, out_of_scope, jira, verbose } => {
|
||||
Cmd::Run { url, models, max_agents, vote_n, chain_depth, recon, offline, subscription, mcp, creds, focus, objective, out_of_scope, jira, only, verbose } => {
|
||||
let url = if url.starts_with("http") { url } else { format!("https://{url}") };
|
||||
let mut cfg = RunConfig::new(&url);
|
||||
cfg.max_agents = max_agents;
|
||||
@@ -392,6 +403,7 @@ async fn main() -> anyhow::Result<()> {
|
||||
cfg.instructions = focus;
|
||||
cfg.objective = objective;
|
||||
cfg.out_of_scope = out_of_scope;
|
||||
cfg.pinned = parse_only(&only);
|
||||
if !models.is_empty() {
|
||||
cfg.models = models;
|
||||
}
|
||||
@@ -401,7 +413,7 @@ async fn main() -> anyhow::Result<()> {
|
||||
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, chain_depth, recon, offline, subscription, jira, verbose } => {
|
||||
Cmd::Whitebox { path, models, max_agents, vote_n, chain_depth, recon, offline, subscription, jira, only, 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;
|
||||
@@ -411,6 +423,7 @@ async fn main() -> anyhow::Result<()> {
|
||||
cfg.offline = offline;
|
||||
cfg.subscription = subscription;
|
||||
cfg.verbose = verbose;
|
||||
cfg.pinned = parse_only(&only);
|
||||
if !models.is_empty() {
|
||||
cfg.models = models;
|
||||
}
|
||||
@@ -419,7 +432,7 @@ async fn main() -> anyhow::Result<()> {
|
||||
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, chain_depth, recon, offline, subscription, mcp, verbose } => {
|
||||
Cmd::Greybox { repo, url, models, creds, focus, max_agents, vote_n, chain_depth, recon, offline, subscription, mcp, only, 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);
|
||||
@@ -432,6 +445,7 @@ async fn main() -> anyhow::Result<()> {
|
||||
cfg.subscription = subscription;
|
||||
cfg.verbose = verbose;
|
||||
cfg.instructions = focus;
|
||||
cfg.pinned = parse_only(&only);
|
||||
if !models.is_empty() {
|
||||
cfg.models = models;
|
||||
}
|
||||
@@ -751,7 +765,7 @@ pub(crate) fn spawn_engagement(base: &Path, mut cfg: RunConfig, mcp: bool, mode:
|
||||
println!(" │ ua : {ua}");
|
||||
write_status(&workdir, "running", &format!("\"target\":{:?}", cfg.target));
|
||||
|
||||
println!(" ┌─ NeuroSploit v3.6.6 · by Joas A Santos & Red Team Leaders");
|
||||
println!(" ┌─ NeuroSploit v3.6.9 · by Joas A Santos & Red Team Leaders");
|
||||
println!(" │ run id : {run_id}");
|
||||
println!(" │ target : {}", cfg.target);
|
||||
println!(" │ models : {}", cfg.models.join(", "));
|
||||
@@ -896,6 +910,21 @@ pub(crate) fn print_findings(out: &RunOutput) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse repeated `--only` values into a clean agent allowlist. Accepts repeats
|
||||
/// and comma/semicolon-separated lists (`--only sqli,xss` == `--only sqli --only xss`).
|
||||
fn parse_only(vals: &[String]) -> Vec<String> {
|
||||
let mut out: Vec<String> = Vec::new();
|
||||
for v in vals {
|
||||
for part in v.split([',', ';']) {
|
||||
let name = part.trim();
|
||||
if !name.is_empty() && !out.iter().any(|x| x == name) {
|
||||
out.push(name.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn sanitize(s: &str) -> String {
|
||||
let s = s.replace("https://", "").replace("http://", "");
|
||||
let mut o: String = s.chars().map(|c| if c.is_alphanumeric() { c } else { '_' }).collect();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! NeuroSploit v3.6.6 — interactive session (Claude-Code / Codex / Cursor-CLI style).
|
||||
//! NeuroSploit v3.6.9 — 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
|
||||
@@ -68,6 +68,7 @@ impl RunLive {
|
||||
if self.feed.len() > 200 { self.feed.remove(0); }
|
||||
}
|
||||
if low.contains("token/quota exhausted") || low.contains("run is paused") { self.phase = "paused (quota)".into(); }
|
||||
else if low.contains("authentication failed") || low.contains("auth failed") || low.contains("circuit breaker") { self.phase = "paused (auth)".into(); }
|
||||
else if low.contains("resumed — retrying") { self.phase = "exploiting".into(); }
|
||||
else if low.starts_with("recon") || low.starts_with("ai-recon") || low.contains("recon round") || low.contains("intensity") || low.starts_with("probe:") { self.phase = "recon".into(); }
|
||||
else if low.contains("selected") && low.contains("agent") {
|
||||
@@ -370,7 +371,7 @@ pub async fn repl(base: &Path) -> anyhow::Result<()> {
|
||||
let backends = harness::installed_cli_backends();
|
||||
println!("\x1b[1m");
|
||||
println!(" ███╗ ██╗███████╗██╗ ██╗██████╗ ██████╗");
|
||||
println!(" ████╗ ██║██╔════╝██║ ██║██╔══██╗██╔═══██╗ NeuroSploit v3.6.6");
|
||||
println!(" ████╗ ██║██╔════╝██║ ██║██╔══██╗██╔═══██╗ NeuroSploit v3.6.9");
|
||||
println!(" ██╔██╗ ██║█████╗ ██║ ██║██████╔╝██║ ██║ interactive harness");
|
||||
println!(" ██║╚██╗██║██╔══╝ ██║ ██║██╔══██╗██║ ██║ by Joas A Santos");
|
||||
println!(" ██║ ╚████║███████╗╚██████╔╝██║ ██║╚██████╔╝ & Red Team Leaders");
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! NeuroSploit v3.6.6 — TUI "Mission Control" mode.
|
||||
//! NeuroSploit v3.6.9 — 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:
|
||||
|
||||
@@ -52,6 +52,21 @@ pub fn providers() -> Vec<Provider> {
|
||||
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"] },
|
||||
// OpenCode Zen — the curated OpenAI-compatible gateway behind the
|
||||
// `opencode` CLI (https://opencode.ai/zen). Works two ways, like
|
||||
// anthropic/openai/xai/gemini above: as a plain API-key provider here,
|
||||
// or (with --subscription) driven through the locally-installed
|
||||
// `opencode` agentic CLI on the user's own Zen/plan login — no key
|
||||
// needed in that mode. `kind: "cli"` reflects the latter.
|
||||
Provider { key: "opencode", label: "OpenCode Zen", base_url: "https://opencode.ai/zen/v1", env_key: "OPENCODE_API_KEY", kind: "cli",
|
||||
models: vec!["claude-opus-5", "claude-sonnet-5", "gpt-5.6-sol", "gpt-5.5", "gemini-3-pro", "grok-4.5", "deepseek-v4-pro", "qwen3.7-max", "kimi-k3"] },
|
||||
// Nous Research — Hermes models via the Nous Portal. As an API-key
|
||||
// provider here (OpenAI-compatible `inference-api.nousresearch.com`),
|
||||
// or (with --subscription) driven through the `hermes` CLI
|
||||
// (NousResearch/hermes-agent) on the user's OAuth Portal login
|
||||
// (`hermes setup --portal`) — 300+ routed frontier models, no key.
|
||||
Provider { key: "nous", label: "Nous Research (Hermes)", base_url: "https://inference-api.nousresearch.com/v1", env_key: "NOUS_API_KEY", kind: "cli",
|
||||
models: vec!["Hermes-4-405B", "Hermes-4-70B", "DeepHermes-3-Mistral-24B-Preview"] },
|
||||
// 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.
|
||||
@@ -163,7 +178,20 @@ impl ChatClient {
|
||||
if !key.is_empty() {
|
||||
if azure { req = req.header("api-key", &key); } else { req = req.bearer_auth(&key); }
|
||||
}
|
||||
let resp = req.send().await?;
|
||||
let resp = req.send().await.map_err(|e| {
|
||||
if e.is_connect() {
|
||||
let local = matches!(p.key, "ollama" | "litellm" | "llamacpp");
|
||||
if local {
|
||||
anyhow!("{} connection refused at {} — is the server running? ({})", p.key, url, e)
|
||||
} else {
|
||||
anyhow!("{} connection error: {}", p.key, e)
|
||||
}
|
||||
} else if e.is_timeout() {
|
||||
anyhow!("{} request timed out (120s) for model '{}' — model may be too large for available memory", p.key, m.model)
|
||||
} else {
|
||||
anyhow!("{} request error: {}", p.key, e)
|
||||
}
|
||||
})?;
|
||||
let status = resp.status();
|
||||
let text = resp.text().await.unwrap_or_default();
|
||||
if !status.is_success() {
|
||||
@@ -213,6 +241,11 @@ impl ChatClient {
|
||||
}
|
||||
|
||||
let mut cmd = Command::new(bin);
|
||||
// Most agentic CLIs here take the prompt on stdin; opencode and hermes
|
||||
// take it as a trailing positional argument instead — track which so we
|
||||
// don't also pipe it into stdin below (that would just hang the child
|
||||
// waiting on a request it already got as an argv value).
|
||||
let mut prompt_via_stdin = true;
|
||||
match bin {
|
||||
// Codex non-interactive exec (uses the ChatGPT/Codex login), prompt on stdin.
|
||||
"codex" => {
|
||||
@@ -237,13 +270,46 @@ impl ChatClient {
|
||||
"grok" => {
|
||||
cmd.arg("--model").arg(model);
|
||||
}
|
||||
// OpenCode CLI (`opencode run`) — non-interactive one-shot, prompt
|
||||
// as a positional arg, not stdin. `--auto` auto-approves anything
|
||||
// not explicitly denied (our equivalent of --dangerously-skip-permissions).
|
||||
// MCP (Playwright) is injected via a generated opencode.json pointed
|
||||
// at through OPENCODE_CONFIG rather than a CLI flag (opencode has none).
|
||||
"opencode" => {
|
||||
prompt_via_stdin = false;
|
||||
cmd.arg("run").arg("--model").arg(model).arg("--auto");
|
||||
if let Some(mcp) = mcp_config {
|
||||
match write_opencode_mcp_config(mcp) {
|
||||
Ok(cfg) => { cmd.env("OPENCODE_CONFIG", cfg); }
|
||||
Err(e) => eprintln!(" [!] opencode MCP config failed: {e}"),
|
||||
}
|
||||
}
|
||||
cmd.arg(&prompt);
|
||||
}
|
||||
// Hermes Agent CLI (NousResearch/hermes-agent) — single-query mode.
|
||||
// `-q` is the prompt-supplying flag (not a stdin read); `--provider
|
||||
// nous` pins the Nous Portal OAuth login; `-Q` quiets banner/spinner
|
||||
// for programmatic use; `--yolo` bypasses dangerous-command prompts.
|
||||
// No CLI-level MCP hook — Hermes falls back to its own built-in
|
||||
// toolsets (web/terminal/computer-use) rather than our Playwright MCP.
|
||||
"hermes" => {
|
||||
prompt_via_stdin = false;
|
||||
cmd.arg("chat").arg("-m").arg(model).arg("--provider").arg("nous")
|
||||
.arg("-Q").arg("--yolo").arg("-q").arg(&prompt);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
cmd.stdin(Stdio::piped()).stdout(Stdio::piped()).stderr(Stdio::piped()).kill_on_drop(true);
|
||||
let mut child = cmd.spawn().map_err(|e| anyhow!("spawn {} failed: {}", bin, e))?;
|
||||
if let Some(mut stdin) = child.stdin.take() {
|
||||
stdin.write_all(prompt.as_bytes()).await?;
|
||||
// Drop closes stdin so the CLI processes the prompt and exits.
|
||||
if prompt_via_stdin {
|
||||
if let Some(mut stdin) = child.stdin.take() {
|
||||
stdin.write_all(prompt.as_bytes()).await?;
|
||||
// Drop closes stdin so the CLI processes the prompt and exits.
|
||||
}
|
||||
} else {
|
||||
// Prompt went in as an argv value; close stdin immediately so
|
||||
// nothing lingers waiting on it (opencode/hermes never read it).
|
||||
drop(child.stdin.take());
|
||||
}
|
||||
// Cap a single agentic CLI turn so a stuck tool-loop can't hang the run.
|
||||
let out = match tokio::time::timeout(Duration::from_secs(600), child.wait_with_output()).await {
|
||||
@@ -564,6 +630,8 @@ pub fn cli_binary_for(provider: &str) -> Option<&'static str> {
|
||||
"openai" => Some("codex"),
|
||||
"xai" => Some("grok"),
|
||||
"gemini" => Some("gemini"),
|
||||
"opencode" => Some("opencode"),
|
||||
"nous" => Some("hermes"),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -577,7 +645,7 @@ pub fn binary_in_path(name: &str) -> bool {
|
||||
|
||||
/// Which subscription CLI backends are installed locally.
|
||||
pub fn installed_cli_backends() -> Vec<&'static str> {
|
||||
["claude", "codex", "grok", "gemini"].into_iter().filter(|b| binary_in_path(b)).collect()
|
||||
["claude", "codex", "grok", "gemini", "opencode", "hermes"].into_iter().filter(|b| binary_in_path(b)).collect()
|
||||
}
|
||||
|
||||
/// Login state of a subscription CLI backend.
|
||||
@@ -597,15 +665,23 @@ pub async fn cli_login_status(provider: &str) -> LoginStatus {
|
||||
let Some(bin) = cli_binary_for(provider) else { return LoginStatus::NotInstalled };
|
||||
if !binary_in_path(bin) { return LoginStatus::NotInstalled; }
|
||||
let mut cmd = Command::new(bin);
|
||||
// opencode/hermes take the probe prompt as an argv value, not stdin.
|
||||
let prompt_via_stdin = !matches!(bin, "opencode" | "hermes");
|
||||
match bin {
|
||||
"claude" => { cmd.arg("-p").arg("--output-format").arg("text").arg("--dangerously-skip-permissions"); }
|
||||
"codex" => { cmd.arg("exec").arg("--dangerously-bypass-approvals-and-sandbox").arg("-"); }
|
||||
"opencode" => { cmd.arg("run").arg("--auto").arg("Reply with exactly: OK"); }
|
||||
"hermes" => { cmd.arg("chat").arg("--provider").arg("nous").arg("-Q").arg("--yolo").arg("-q").arg("Reply with exactly: OK"); }
|
||||
_ => { cmd.arg("-p"); } // grok / gemini: prompt on stdin
|
||||
}
|
||||
cmd.stdin(Stdio::piped()).stdout(Stdio::piped()).stderr(Stdio::piped()).kill_on_drop(true);
|
||||
let mut child = match cmd.spawn() { Ok(c) => c, Err(_) => return LoginStatus::Unknown };
|
||||
if let Some(mut stdin) = child.stdin.take() {
|
||||
let _ = stdin.write_all(b"Reply with exactly: OK").await;
|
||||
if prompt_via_stdin {
|
||||
if let Some(mut stdin) = child.stdin.take() {
|
||||
let _ = stdin.write_all(b"Reply with exactly: OK").await;
|
||||
}
|
||||
} else {
|
||||
drop(child.stdin.take());
|
||||
}
|
||||
let out = match tokio::time::timeout(Duration::from_secs(45), child.wait_with_output()).await {
|
||||
Ok(Ok(o)) => o,
|
||||
@@ -629,10 +705,50 @@ pub async fn cli_login_status(provider: &str) -> LoginStatus {
|
||||
}
|
||||
|
||||
/// Does this provider's agentic CLI accept a Playwright MCP config?
|
||||
/// Claude Code and Codex do; Gemini/Grok CLIs don't take an MCP-config flag, so
|
||||
/// they fall back to their own built-in tools.
|
||||
/// Claude Code, Codex, and OpenCode do (OpenCode via a generated
|
||||
/// `opencode.json` + `OPENCODE_CONFIG`, see `write_opencode_mcp_config`).
|
||||
/// Gemini/Grok/Hermes have no CLI-level MCP hook, so they fall back to their
|
||||
/// own built-in tools (Hermes ships web/terminal/computer-use natively).
|
||||
pub fn mcp_supported(provider: &str) -> bool {
|
||||
matches!(provider, "anthropic" | "openai")
|
||||
matches!(provider, "anthropic" | "openai" | "opencode")
|
||||
}
|
||||
|
||||
/// Convert our `.mcp.json` (`{"mcpServers": {name: {command, args}}}`) into
|
||||
/// OpenCode's own config schema (`{"mcp": {name: {"type":"local","command":
|
||||
/// [command, ...args], "enabled": true}}}`) and write it next to the source
|
||||
/// file. OpenCode has no `--mcp-config` flag; it's pointed at a config file
|
||||
/// via the `OPENCODE_CONFIG` env var instead (set by the `opencode` arm of
|
||||
/// `chat_cli`), so this doesn't touch the user's own `opencode.json`.
|
||||
fn write_opencode_mcp_config(mcp_json_path: &str) -> Result<std::path::PathBuf> {
|
||||
let txt = std::fs::read_to_string(mcp_json_path)
|
||||
.map_err(|e| anyhow!("read {mcp_json_path}: {e}"))?;
|
||||
let v: serde_json::Value = serde_json::from_str(&txt)
|
||||
.map_err(|e| anyhow!("parse {mcp_json_path}: {e}"))?;
|
||||
let servers = v.get("mcpServers").cloned().unwrap_or(v);
|
||||
let mut mcp = serde_json::Map::new();
|
||||
if let Some(obj) = servers.as_object() {
|
||||
for (name, s) in obj {
|
||||
let command = s.get("command").and_then(|c| c.as_str()).unwrap_or("").to_string();
|
||||
if command.is_empty() { continue; }
|
||||
let mut argv = vec![serde_json::Value::String(command)];
|
||||
if let Some(args) = s.get("args").and_then(|a| a.as_array()) {
|
||||
argv.extend(args.iter().cloned());
|
||||
}
|
||||
mcp.insert(name.clone(), serde_json::json!({
|
||||
"type": "local",
|
||||
"command": argv,
|
||||
"enabled": true
|
||||
}));
|
||||
}
|
||||
}
|
||||
let cfg = serde_json::json!({
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"mcp": mcp
|
||||
});
|
||||
let path = std::path::Path::new(mcp_json_path).with_file_name("opencode.json");
|
||||
std::fs::write(&path, serde_json::to_string_pretty(&cfg).unwrap_or_default())
|
||||
.map_err(|e| anyhow!("write {}: {e}", path.display()))?;
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
/// Best-effort ensure the Playwright MCP server is available locally. Requires
|
||||
|
||||
@@ -313,6 +313,37 @@ const DECISION_DOCTRINE: &str = "DECIDE WHERE TO ATTACK (analyse, then act):\n\
|
||||
- Build PoCs when needed: for issues that need an artifact to prove (clickjacking → an HTML page that frames the target; CSRF → an auto-submitting HTML form; a multi-step or timing exploit → a script), WRITE the PoC to the run's PoC dir, run/validate it, and cite the file in the evidence.\n\
|
||||
- Test control BYPASSES: when something returns 401/403/redirect or is 'blocked', try to bypass it (verb tampering, path/case/encoding normalization, X-Original-URL / X-Rewrite-URL / X-Forwarded-* headers, missing-vs-invalid token, direct object/API access) and confirm the bypass with the two requests.\n\n";
|
||||
|
||||
/// CHAIN doctrine: turn ANY foothold into the next step. A primitive→next-step
|
||||
/// playbook (not an exhaustive script) so the agent always has a concrete pivot
|
||||
/// to reason about, plus a push to chain toward BUSINESS impact — all under the
|
||||
/// non-destructive SAFETY_DOCTRINE (prove RCE/access with a benign marker, never
|
||||
/// harm data or state).
|
||||
const CHAIN_DOCTRINE: &str = "CHAIN THE FOOTHOLD (pivot to deeper, provable impact — any primitive can chain):\n\
|
||||
- Think in primitives, not labels: reduce the foothold to what it GIVES you (code exec, file read, file write, request forgery, a trusted identity, a leaked secret, arbitrary object access) and pick the next step from that.\n\
|
||||
- Pivot playbook (attempt the fitting ones, prove each with a benign receipt):\n\
|
||||
· File upload / write → RCE: upload a webshell/handler to an executable path or poison a config/`.htaccess`/cron/serialized file; prove with a benign marker (`id`, unique echo, OOB DNS), not damage.\n\
|
||||
· SSRF → cloud/host takeover: hit `169.254.169.254` (IMDSv1/v2), GCP/Azure metadata, internal admin/actuator, `file://`/`gopher://`; loot temp creds/tokens and REUSE them.\n\
|
||||
· SQLi → RCE/LPE: stacked queries, `INTO OUTFILE`/`COPY … TO`, UDF, `xp_cmdshell`, read secrets/creds; then reuse creds to log in and escalate.\n\
|
||||
· LFI/path traversal → RCE: log/session/wrapper poisoning, `/proc/self/environ`, read source & secrets; combine with an upload for exec.\n\
|
||||
· XXE → SSRF/file read → creds; deserialization/SSTI → RCE via a gadget/template sink; prove exec with a marker.\n\
|
||||
· IDOR/BOLA/mass-assignment → account/tenant takeover or role escalation (`role=admin`); open-redirect/XSS/CORS → token/session theft → ATO.\n\
|
||||
· Exposed `.git`/backup/`.env`/secrets → reconstruct source & keys → auth to internal APIs, cloud, DB; default/leaked creds → domain/service compromise.\n\
|
||||
- Reuse loot relentlessly: every credential/JWT/cookie/API key/host you obtain is input to the next step — carry it forward across modules and try it everywhere it might be accepted.\n\
|
||||
- Understand the BUSINESS & LOGIC: reason about what the app is FOR (payments, orders, tenancy, KYC, entitlements) and chain toward business impact — payment/price/coupon abuse, cross-tenant data access, entitlement/limit bypass, workflow/state-machine skips (skip approval/verification steps), race conditions on balance/stock. These compound: each finding updates your model of the app for the next probe.\n\
|
||||
- Stop at proof: demonstrate the impact with the SMALLEST safe step and report the CHAIN end-to-end; never destroy, overwrite, encrypt, mass-exfiltrate, or DoS to 'prove' it.\n\n";
|
||||
|
||||
/// WHITEBOX doctrine: this is a STATIC source review — keep the agent in the code,
|
||||
/// not on the wire. Prevents whitebox runs from hallucinating black-box network
|
||||
/// actions (curl/nuclei/live requests) they cannot perform here, and pushes for
|
||||
/// a symbolic `file:line` receipt plus a runnable repro PoC where it adds value.
|
||||
const WHITEBOX_DOCTRINE: &str = "MODE: WHITE-BOX STATIC SOURCE REVIEW. You are reading source code, NOT a live target.\n\
|
||||
- Source-only: reason strictly about the provided code. Do NOT curl, run nuclei, browse, or claim any live/HTTP/network result — there is no running app here. Any \"I sent a request / got a response\" claim is a hallucination and will be rejected.\n\
|
||||
- Symbolic receipt: EVERY finding's evidence is a `file:line` citation plus the exact vulnerable code quoted verbatim. The code citation IS the proof. No `file:line` + code quote ⇒ do not report it.\n\
|
||||
- Trace, don't guess: follow tainted input from its SOURCE (request param, env, deserialization, file) to a dangerous SINK (SQL/exec/eval/template/path/SSRF/deserialize). Report only when source reaches sink without effective sanitization; note the path (`entry → … → sink`).\n\
|
||||
- Version → CVE (static): read dependency manifests (package.json, requirements.txt, go.mod, pom.xml, Gemfile.lock, Cargo.lock) and pin exact versions; map to known CVEs and cite the manifest line. Flag reachable, exploitable ones over merely-outdated ones.\n\
|
||||
- Repro PoC (optional but valued): when a finding warrants it, WRITE a proof/repro script to $NEUROSPLOIT_POCS — e.g. the exact malicious input + the request/CLI call that would trigger the sink, or a unit-style harness exercising the vulnerable function — with a header comment (file:line it proves, how to run). Cite the PoC path in the evidence. Mark clearly that it demonstrates the code path (static-derived), not a live hit.\n\
|
||||
- Calibrate: High/Critical only when the sink is reachable and exploitable from untrusted input; guarded/unreachable code is Low or a lead.\n\n";
|
||||
|
||||
/// Methodology directions for a modern JS SPA backed by a REST/GraphQL API
|
||||
/// (Angular/React/Vue front + Node/Express-style API — the shape of OWASP Juice
|
||||
/// Shop and many real apps). These are DIRECTIONS on HOW to hunt each vuln class,
|
||||
@@ -456,20 +487,37 @@ pub async fn run(cfg: RunConfig, lib: &Library, pool: &ModelPool, tx: Sender<Str
|
||||
|
||||
// Use the model to pick the agents whose preconditions match the recon —
|
||||
// the harness reasons about *which* specialists to run, not all of them.
|
||||
// Exception: when the operator pinned an explicit set (--only), run EXACTLY
|
||||
// those and skip recon-based selection — used to re-test a single vuln.
|
||||
let focus = cfg.instructions.clone().unwrap_or_default();
|
||||
let chosen = select_agents(pool, &recon, &focus, &ranked, &tx).await;
|
||||
let selected: Vec<Agent> = if !chosen.is_empty() {
|
||||
let selected: Vec<Agent> = if !cfg.pinned.is_empty() {
|
||||
let sel: Vec<Agent> =
|
||||
ranked.iter().filter(|a| chosen.iter().any(|c| c == &a.name)).cloned().collect();
|
||||
ranked.iter().filter(|a| cfg.pinned.iter().any(|p| p == &a.name)).cloned().collect();
|
||||
if sel.is_empty() {
|
||||
heuristic_select(&ranked, &recon, &focus, cap)
|
||||
let _ = tx.send(format!("--only matched no agent ({}) — falling back to recon selection",
|
||||
cfg.pinned.join(", "))).await;
|
||||
let chosen = select_agents(pool, &recon, &focus, &ranked, &tx).await;
|
||||
ranked.iter().filter(|a| chosen.iter().any(|c| c == &a.name)).take(cap).cloned().collect()
|
||||
} else {
|
||||
sel.into_iter().take(cap).collect()
|
||||
let _ = tx.send(format!("--only: running exactly {} pinned agent(s): {}", sel.len(),
|
||||
sel.iter().map(|a| a.name.clone()).collect::<Vec<_>>().join(", "))).await;
|
||||
sel
|
||||
}
|
||||
} else {
|
||||
// LLM selection failed/empty → recon+focus keyword heuristic, not a blind flat list.
|
||||
let _ = tx.send("selection empty — using recon-keyword heuristic".into()).await;
|
||||
heuristic_select(&ranked, &recon, &focus, cap)
|
||||
let chosen = select_agents(pool, &recon, &focus, &ranked, &tx).await;
|
||||
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() {
|
||||
heuristic_select(&ranked, &recon, &focus, cap)
|
||||
} else {
|
||||
sel.into_iter().take(cap).collect()
|
||||
}
|
||||
} else {
|
||||
// LLM selection failed/empty → recon+focus keyword heuristic, not a blind flat list.
|
||||
let _ = tx.send("selection empty — using recon-keyword heuristic".into()).await;
|
||||
heuristic_select(&ranked, &recon, &focus, cap)
|
||||
}
|
||||
};
|
||||
// Dedup: never run the same agent twice in one engagement.
|
||||
let mut selected: Vec<Agent> = {
|
||||
@@ -479,7 +527,7 @@ pub async fn run(cfg: RunConfig, lib: &Library, pool: &ModelPool, tx: Sender<Str
|
||||
// No creds given → always run the registration/form agent FIRST so the run
|
||||
// reaches the authenticated surface (and the operator sees it happen). It
|
||||
// self-registers one test account under the anti-flood guardrail.
|
||||
if cfg.auth.as_deref().unwrap_or("").trim().is_empty() {
|
||||
if cfg.pinned.is_empty() && cfg.auth.as_deref().unwrap_or("").trim().is_empty() {
|
||||
if let Some(reg) = lib.vulns.iter().find(|a| a.name == "account_registration_and_forms") {
|
||||
if !selected.iter().any(|a| a.name == reg.name) {
|
||||
let _ = tx.send("no creds set — running account_registration_and_forms first to reach the authenticated surface".into()).await;
|
||||
@@ -550,6 +598,10 @@ 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;
|
||||
if f.is_empty() && !text.trim().is_empty() && text.trim() != "[]" {
|
||||
let tail: String = text.chars().rev().take(120).collect::<String>().chars().rev().collect();
|
||||
let _ = txc.send(format!("⚠ agent {} returned text but 0 parseable findings (model may have produced malformed JSON). Tail: {:?}", ag.name, tail)).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;
|
||||
@@ -558,7 +610,12 @@ pub async fn run(cfg: RunConfig, lib: &Library, pool: &ModelPool, tx: Sender<Str
|
||||
(ag.name.clone(), text, f)
|
||||
}
|
||||
Err(e) => {
|
||||
let _ = txc.send(format!("exploit {} failed: {e}", ag.name)).await;
|
||||
let is_auth = crate::pool::is_auth_failure(&e);
|
||||
if is_auth {
|
||||
let _ = txc.send(format!("⚠ exploit {} auth failed — findings so far are SAFE, run is pausing: {e}", ag.name)).await;
|
||||
} else {
|
||||
let _ = txc.send(format!("exploit {} failed: {e}", ag.name)).await;
|
||||
}
|
||||
(ag.name.clone(), format!("ERROR: {e}"), vec![])
|
||||
}
|
||||
}
|
||||
@@ -571,6 +628,9 @@ pub async fn run(cfg: RunConfig, lib: &Library, pool: &ModelPool, tx: Sender<Str
|
||||
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 by {}-model vote", candidates.len(), cfg.vote_n)).await;
|
||||
if pool.candidates.len() == 1 && cfg.vote_n <= 1 {
|
||||
let _ = tx.send("⚠ single-model panel with vote_n=1 — validation is weaker (same model validates its own findings). Consider --vote-n 2 or adding a second model for cross-validation.".into()).await;
|
||||
}
|
||||
|
||||
// ---- 4. Validate by N-model voting ---------------------------------
|
||||
let mut findings = validate(candidates, pool, VOTE_SYS, cfg.vote_n, &tx).await;
|
||||
@@ -597,7 +657,22 @@ pub async fn run_whitebox(cfg: RunConfig, lib: &Library, pool: &ModelPool, tx: S
|
||||
}
|
||||
|
||||
let mut rl = cfg.rl_path.as_ref().map(|p| RlState::load(Path::new(p))).unwrap_or_default();
|
||||
let mut ranked: Vec<Agent> = if lib.code.is_empty() { lib.vulns.clone() } else { lib.code.clone() };
|
||||
let pool_agents: Vec<Agent> = if lib.code.is_empty() { lib.vulns.clone() } else { lib.code.clone() };
|
||||
let mut ranked: Vec<Agent> = if cfg.pinned.is_empty() {
|
||||
pool_agents
|
||||
} else {
|
||||
// Operator pinned an explicit agent set (--only): re-test exactly those.
|
||||
let sel: Vec<Agent> = pool_agents.iter()
|
||||
.filter(|a| cfg.pinned.iter().any(|p| p == &a.name)).cloned().collect();
|
||||
if sel.is_empty() {
|
||||
let _ = tx.send(format!("--only matched no code agent ({}) — reviewing with the full set",
|
||||
cfg.pinned.join(", "))).await;
|
||||
pool_agents
|
||||
} else {
|
||||
let _ = tx.send(format!("--only: reviewing with exactly {} pinned agent(s)", sel.len())).await;
|
||||
sel
|
||||
}
|
||||
};
|
||||
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 selected: Vec<Agent> = ranked.into_iter().take(cap).collect();
|
||||
@@ -616,11 +691,15 @@ pub async fn run_whitebox(cfg: RunConfig, lib: &Library, pool: &ModelPool, tx: S
|
||||
let user = format!(
|
||||
"{}\n\nSOURCE CODE TO REVIEW:\n```\n{}\n```\n\nReply ONLY with a JSON array of findings (may be empty []). \
|
||||
Each item: {{id,title,severity,cwe,endpoint,payload,evidence,impact,remediation,confidence}} \
|
||||
where `endpoint` is the file:line and `evidence` quotes the vulnerable code.",
|
||||
where `endpoint` is the file:line and `evidence` quotes the vulnerable code. \
|
||||
When a finding warrants a runnable proof, write a repro script to $NEUROSPLOIT_POCS and put its path in `payload`.",
|
||||
ag.user.replace("{target}", "the provided repository").replace("{recon_json}", "{}"),
|
||||
ctx
|
||||
);
|
||||
match pool.complete_routed(Task::Exploit, &ag.name, &ag.system, &user).await {
|
||||
// Prepend the white-box doctrine so code agents stay in static
|
||||
// source-review mode and never hallucinate live/black-box actions.
|
||||
let sys = format!("{}{}", WHITEBOX_DOCTRINE, ag.system);
|
||||
match pool.complete_routed(Task::Exploit, &ag.name, &sys, &user).await {
|
||||
Ok((m, text)) => {
|
||||
let f = extract_findings(&text, &ag.name);
|
||||
let _ = txc.send(format!("analyze {} via {} → {} candidate(s)", ag.name, m.label(), f.len())).await;
|
||||
@@ -910,13 +989,13 @@ async fn chain_from_seed(pool: &ModelPool, target: &str, directives: &str, recon
|
||||
};
|
||||
let short: String = seed.title.chars().take(28).collect();
|
||||
let user = format!(
|
||||
"AUTHORIZED engagement on {target}.\n\n{directives}{react}{depth}{decision}{safety}{doctrine}\
|
||||
"AUTHORIZED engagement on {target}.\n\n{directives}{react}{depth}{decision}{chain}{safety}{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, decision = DECISION_DOCTRINE, safety = SAFETY_DOCTRINE, doctrine = tool_doctrine(pool.mcp_config.is_some()),
|
||||
react = REACT_DOCTRINE, depth = DEPTH_DOCTRINE, decision = DECISION_DOCTRINE, chain = CHAIN_DOCTRINE, safety = SAFETY_DOCTRINE, doctrine = tool_doctrine(pool.mcp_config.is_some()),
|
||||
);
|
||||
let label = format!("chain:{short}");
|
||||
match pool.complete_routed(Task::Exploit, &label, CHAIN_SYS, &user).await {
|
||||
@@ -1081,9 +1160,26 @@ fn heuristic_select(ranked: &[Agent], recon: &str, focus: &str, cap: usize) -> V
|
||||
}
|
||||
|
||||
async fn validate(candidates: Vec<Finding>, pool: &ModelPool, sys: &str, vote_n: usize, tx: &Sender<String>) -> Vec<Finding> {
|
||||
// Fast-track: findings with no evidence are unverifiable — skip the vote
|
||||
// and flag for human review instead of wasting a validator call that will
|
||||
// always reject ("default to rejected when uncertain" + empty evidence).
|
||||
let (have_evidence, no_evidence): (Vec<_>, Vec<_>) = candidates.into_iter().partition(|f| {
|
||||
let e = f.evidence.trim();
|
||||
!e.is_empty() && e != "N/A" && e != "n/a" && e != "none" && e != "-"
|
||||
});
|
||||
let mut flagged: Vec<Finding> = no_evidence.into_iter().map(|mut f| {
|
||||
f.validated = false;
|
||||
f.review_status = "needs-review".into();
|
||||
f.review_reason = "no concrete evidence provided by agent — manual verification required".into();
|
||||
f.votes = "0/0".into();
|
||||
f
|
||||
}).collect();
|
||||
for f in &flagged {
|
||||
let _ = tx.send(format!("vote {} → needs-review (no evidence)", f.title)).await;
|
||||
}
|
||||
// Prefer a model other than the primary (likely finder) to adjudicate.
|
||||
let finder = pool.candidates.first().map(|m| m.label());
|
||||
let validated: Vec<Finding> = stream::iter(candidates)
|
||||
let validated: Vec<Finding> = stream::iter(have_evidence)
|
||||
.map(|mut f| {
|
||||
let txc = tx.clone();
|
||||
let finder = finder.clone();
|
||||
@@ -1117,7 +1213,9 @@ async fn validate(candidates: Vec<Finding>, pool: &ModelPool, sys: &str, vote_n:
|
||||
.collect()
|
||||
.await;
|
||||
// Keep confirmed AND needs-review (human decides); drop only zero-support noise.
|
||||
validated.into_iter().filter(|f| f.validated || f.review_status == "needs-review").collect()
|
||||
// Include no-evidence flagged findings so the human loop sees them.
|
||||
flagged.extend(validated.into_iter().filter(|f| f.validated || f.review_status == "needs-review"));
|
||||
flagged
|
||||
}
|
||||
|
||||
/// Adversarial refutation pass: every confirmed **High/Critical** finding is
|
||||
@@ -1369,12 +1467,27 @@ fn extract_findings(text: &str, agent: &str) -> Vec<Finding> {
|
||||
(Some(a), Some(b)) if b > a => &text[a..=b],
|
||||
_ => match (text.find('{'), text.rfind('}')) {
|
||||
(Some(a), Some(b)) if b > a => &text[a..=b],
|
||||
_ => return vec![],
|
||||
_ => {
|
||||
if !text.trim().is_empty() && text.trim() != "[]" {
|
||||
eprintln!("[extract_findings] agent {agent}: model returned text but no JSON array/object found (len={}); raw tail: {:?}",
|
||||
text.len(), &text[text.len().saturating_sub(200)..]);
|
||||
}
|
||||
return vec![];
|
||||
}
|
||||
},
|
||||
};
|
||||
let val: serde_json::Value = match serde_json::from_str(slice) {
|
||||
Ok(v) => v,
|
||||
Err(_) => return vec![],
|
||||
Err(e) => {
|
||||
eprintln!("[extract_findings] agent {agent}: JSON parse failed: {e}; slice head: {:?}",
|
||||
&slice[..slice.len().min(300)]);
|
||||
// Attempt to salvage: strip trailing comma before ] (common LLM mistake)
|
||||
let fixed = slice.replace(",]", "]").replace(",}", "}");
|
||||
match serde_json::from_str(&fixed) {
|
||||
Ok(v) => v,
|
||||
Err(_) => return vec![],
|
||||
}
|
||||
}
|
||||
};
|
||||
let items: Vec<serde_json::Value> = match val {
|
||||
serde_json::Value::Array(a) => a,
|
||||
@@ -1775,6 +1888,11 @@ fn recon_intensity_directive(level: usize) -> String {
|
||||
(8) TLS/headers/cookies. Report counts (how many subdomains/urls/params/endpoints you actually found).\n\n")
|
||||
}
|
||||
|
||||
/// Max wall-clock seconds for the ENTIRE recon phase (all rounds combined).
|
||||
/// This prevents recon from eating the whole run — exploitation must start.
|
||||
/// Per-round cap = total / (rounds + 1) so later rounds get equal time.
|
||||
const RECON_TOTAL_BUDGET_SECS: u64 = 300; // 5 minutes total
|
||||
|
||||
/// Intense, multi-round recon: an initial deep pass, then follow-up rounds that
|
||||
/// EXPAND the surface (chase discovered subdomains/endpoints/params, install
|
||||
/// tools, dig where the previous round found signal). Returns the merged recon
|
||||
@@ -1786,22 +1904,53 @@ async fn deep_recon(cfg: &RunConfig, pool: &ModelPool, probe_facts: &str, tx: &S
|
||||
let intensity_dir = recon_intensity_directive(intensity);
|
||||
let dir = operator_directives(cfg);
|
||||
let mut accum = format!("OBSERVED HTTP PROBE:\n{probe_facts}");
|
||||
let recon_start = std::time::Instant::now();
|
||||
let total_rounds = 1 + extra_rounds;
|
||||
|
||||
// Time-budget directive: subscription CLIs run commands autonomously, so they
|
||||
// need an explicit cap to avoid running 150+ commands in a single round.
|
||||
let budget_dir = format!(
|
||||
"TIME BUDGET: you have ~{budget_secs} seconds for THIS recon round. Be EFFICIENT: \
|
||||
prioritise high-signal actions (JS analysis, API mapping, SQLi/auth probes) over \
|
||||
exhaustive crawling. AIM for 30-50 commands max per round — enough to map the surface, \
|
||||
not so many that exploitation never starts. STOP EARLY if you have enough intel to \
|
||||
select agents. When done, EMIT YOUR RESULTS IMMEDIATELY — do not start another pass.\n\n",
|
||||
budget_secs = RECON_TOTAL_BUDGET_SECS / total_rounds as u64,
|
||||
);
|
||||
|
||||
// Initial deep pass.
|
||||
let user = format!("{dir}{intensity_dir}{doctrine}OBSERVED HTTP PROBE (build on these, verify, go deeper):\n{probe_facts}\n\nTarget: {}", cfg.target);
|
||||
let _ = tx.send(format!("recon: intensity {} — actively enumerating (installing tools as needed)…", intensity)).await;
|
||||
let user = format!("{dir}{budget_dir}{intensity_dir}{doctrine}OBSERVED HTTP PROBE (build on these, verify, go deeper):\n{probe_facts}\n\nTarget: {}", cfg.target);
|
||||
let _ = tx.send(format!("recon: intensity {} — actively enumerating (budget {}s total, {} round(s))…", intensity, RECON_TOTAL_BUDGET_SECS, total_rounds)).await;
|
||||
match pool.complete_routed(Task::Recon, "recon", RECON_SYS, &user).await {
|
||||
Ok((m, t)) => { let _ = tx.send(format!("recon round 1 complete via {}", m.label())).await; accum.push_str(&format!("\n\nMODEL RECON (round 1):\n{t}")); }
|
||||
Err(e) => { let _ = tx.send(format!("recon round 1 failed ({e}) — probe facts only")).await; return accum; }
|
||||
Err(e) => {
|
||||
let is_auth = crate::pool::is_auth_failure(&e);
|
||||
if is_auth {
|
||||
let _ = tx.send(format!("recon round 1 auth failed ({e}) — continuing with probe facts; run will pause before exploit phase")).await;
|
||||
accum.push_str(&format!("\n\nMODEL RECON (round 1):\n{e}"));
|
||||
} else {
|
||||
let _ = tx.send(format!("recon round 1 failed ({e}) — probe facts only")).await;
|
||||
}
|
||||
return accum;
|
||||
}
|
||||
}
|
||||
|
||||
// Follow-up expansion rounds — each digs further using what's known so far.
|
||||
for r in 0..extra_rounds {
|
||||
if pool.stop_exploiting() { break; }
|
||||
// Time budget check: if recon has already consumed the total budget, stop
|
||||
// and proceed to exploitation with whatever intelligence we gathered.
|
||||
let elapsed = recon_start.elapsed().as_secs();
|
||||
if elapsed >= RECON_TOTAL_BUDGET_SECS {
|
||||
let _ = tx.send(format!("recon: time budget exhausted ({elapsed}s/{RECON_TOTAL_BUDGET_SECS}s) — proceeding to exploitation with current intel")).await;
|
||||
break;
|
||||
}
|
||||
let remaining = RECON_TOTAL_BUDGET_SECS - elapsed;
|
||||
let round = r + 2;
|
||||
let known: String = accum.chars().rev().take(3000).collect::<String>().chars().rev().collect();
|
||||
let follow = format!(
|
||||
"{dir}{intensity_dir}{doctrine}CONTINUE the recon — this is round {round}. Here is what recon has found so far:\n{known}\n\n\
|
||||
"{dir}TIME BUDGET: you have ~{remaining} seconds remaining for recon. Be CONCISE — focus only on the highest-value leads.\n\n\
|
||||
{intensity_dir}{doctrine}CONTINUE the recon — this is round {round}. Here is what recon has found so far:\n{known}\n\n\
|
||||
Now EXPAND: pick the most promising leads and go deeper — resolve & probe any NEW subdomains/hosts, crawl \
|
||||
and harvest URLs for endpoints not yet mapped, run content/parameter discovery where you saw interesting \
|
||||
paths, fingerprint exact versions of anything unclear, and enumerate the API/GraphQL further. Install any \
|
||||
@@ -1813,7 +1962,11 @@ async fn deep_recon(cfg: &RunConfig, pool: &ModelPool, probe_facts: &str, tx: &S
|
||||
if novel.len() > 20 { let _ = tx.send(format!("recon round {round} via {} — expanded surface", m.label())).await; accum.push_str(&format!("\n\nMODEL RECON (round {round}):\n{novel}")); }
|
||||
else { let _ = tx.send(format!("recon round {round}: no new surface — recon converged")).await; break; }
|
||||
}
|
||||
Err(e) => { let _ = tx.send(format!("recon round {round} failed ({e})")).await; break; }
|
||||
Err(e) => {
|
||||
let is_auth = crate::pool::is_auth_failure(&e);
|
||||
let _ = tx.send(format!("recon round {round} {} ({e})", if is_auth { "auth failed" } else { "failed" })).await;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
accum
|
||||
|
||||
@@ -20,6 +20,24 @@ pub fn is_exhaustion(e: &anyhow::Error) -> bool {
|
||||
.any(|k| s.contains(k))
|
||||
}
|
||||
|
||||
/// Does this error look like an **authentication / authorization failure**
|
||||
/// (revoked OAuth, expired session, invalid API key) — distinct from transient
|
||||
/// quota/rate issues? Auth failures are non-recoverable without re-login, so
|
||||
/// the run should pause immediately and offer fallback providers.
|
||||
pub fn is_auth_failure(e: &anyhow::Error) -> bool {
|
||||
let s = format!("{e:#}").to_lowercase();
|
||||
[
|
||||
"401", "403", "unauthorized", "token has been revoked",
|
||||
"token revoked", "access token", "oauth", "session expired",
|
||||
"not authenticated", "not logged in", "please log in",
|
||||
"please login", "invalid api key", "invalid_api_key",
|
||||
"api key expired", "authentication failed", "failed to authenticate",
|
||||
"run /login",
|
||||
]
|
||||
.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)]
|
||||
pub enum Task {
|
||||
@@ -64,6 +82,10 @@ pub struct ModelPool {
|
||||
/// Fallback models the user added via `/continue <provider:model>` while
|
||||
/// paused — tried first on the next attempt.
|
||||
fallback: Arc<Mutex<Vec<ModelRef>>>,
|
||||
/// Circuit breaker: consecutive auth/exhaustion failures across agents.
|
||||
/// When this exceeds `AUTH_FAIL_THRESHOLD`, the pool auto-pauses instead of
|
||||
/// burning through the remaining agents on a dead token.
|
||||
consecutive_auth_fails: Arc<std::sync::atomic::AtomicUsize>,
|
||||
}
|
||||
|
||||
impl ModelPool {
|
||||
@@ -96,9 +118,20 @@ impl ModelPool {
|
||||
paused: Arc::new(AtomicBool::new(false)),
|
||||
resume: Arc::new(Notify::new()),
|
||||
fallback: Arc::new(Mutex::new(Vec::new())),
|
||||
consecutive_auth_fails: Arc::new(std::sync::atomic::AtomicUsize::new(0)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Reset the consecutive auth-failure counter (called on any successful completion).
|
||||
fn reset_auth_fails(&self) {
|
||||
self.consecutive_auth_fails.store(0, std::sync::atomic::Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// Increment the consecutive auth-failure counter and return the new count.
|
||||
fn inc_auth_fails(&self) -> usize {
|
||||
self.consecutive_auth_fails.fetch_add(1, std::sync::atomic::Ordering::Relaxed) + 1
|
||||
}
|
||||
|
||||
/// Attach a progress channel so the subscription CLI streams structured
|
||||
/// activity (commands run, files read, tools called) live.
|
||||
pub fn set_progress(&self, tx: tokio::sync::mpsc::Sender<String>) {
|
||||
@@ -146,20 +179,32 @@ impl ModelPool {
|
||||
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) {
|
||||
/// Consecutive auth/quota failures needed to trip the circuit breaker and
|
||||
/// auto-pause the run. Low threshold: 3 consecutive failures on the same
|
||||
/// provider is enough signal that the token is dead.
|
||||
const AUTH_FAIL_THRESHOLD: usize = 3;
|
||||
|
||||
/// Park the run on token/quota exhaustion or auth failure: 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, is_auth: bool) {
|
||||
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!(
|
||||
let notice = if is_auth {
|
||||
format!(
|
||||
"notify: ⏸ authentication failed ({}). Run is PAUSED — all findings so far are SAFE. \
|
||||
Fix: /continue <provider:model> to switch provider, or re-login and /continue.",
|
||||
short.chars().take(120).collect::<String>()
|
||||
)
|
||||
} else {
|
||||
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;
|
||||
)
|
||||
};
|
||||
let _ = tx.send(notice).await;
|
||||
}
|
||||
while self.paused.load(Ordering::Relaxed) && !self.is_cancelled() {
|
||||
let notified = self.resume.notified();
|
||||
@@ -169,8 +214,9 @@ impl ModelPool {
|
||||
}
|
||||
}
|
||||
if !self.is_cancelled() {
|
||||
self.reset_auth_fails(); // user resumed, reset counter
|
||||
if let Some(tx) = self.progress() {
|
||||
let _ = tx.send("notify: ▶ resumed — retrying exhausted step.".to_string()).await;
|
||||
let _ = tx.send("notify: ▶ resumed — retrying with updated credentials/model.".to_string()).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -212,9 +258,9 @@ impl ModelPool {
|
||||
};
|
||||
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),
|
||||
// Don't burn retries on exhaustion or auth failure — surface
|
||||
// immediately so the caller can park and let the user /continue.
|
||||
Err(e) if is_exhaustion(&e) || is_auth_failure(&e) => return Err(e),
|
||||
Err(e) => last = e,
|
||||
}
|
||||
}
|
||||
@@ -233,6 +279,20 @@ impl ModelPool {
|
||||
if self.is_cancelled() {
|
||||
return Err(anyhow!("cancelled"));
|
||||
}
|
||||
// Circuit breaker: if we've seen N consecutive auth failures across
|
||||
// agents, pause immediately — don't burn another agent on a dead token.
|
||||
let fail_count = self.consecutive_auth_fails.load(std::sync::atomic::Ordering::Relaxed);
|
||||
if fail_count >= Self::AUTH_FAIL_THRESHOLD && !self.is_cancelled() {
|
||||
self.park_exhausted(
|
||||
&anyhow!("circuit breaker: {} consecutive auth failures — token/session likely dead", fail_count),
|
||||
true,
|
||||
).await;
|
||||
if self.is_cancelled() {
|
||||
return Err(anyhow!("cancelled"));
|
||||
}
|
||||
// After resume, retry with potentially new fallback models.
|
||||
continue;
|
||||
}
|
||||
// User-supplied fallback models (via /continue) are tried first.
|
||||
let mut order = self.route(task);
|
||||
if let Ok(fb) = self.fallback.lock() {
|
||||
@@ -244,25 +304,31 @@ impl ModelPool {
|
||||
}
|
||||
let mut last = anyhow!("no candidate models");
|
||||
let mut exhausted = false;
|
||||
let mut auth_failed = 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)),
|
||||
Ok(text) => {
|
||||
self.reset_auth_fails(); // success resets circuit breaker
|
||||
return Ok((m.clone(), text));
|
||||
}
|
||||
Err(e) => {
|
||||
if is_exhaustion(&e) {
|
||||
if is_auth_failure(&e) {
|
||||
auth_failed = true;
|
||||
self.inc_auth_fails();
|
||||
} else if is_exhaustion(&e) {
|
||||
exhausted = true;
|
||||
}
|
||||
last = e;
|
||||
}
|
||||
}
|
||||
}
|
||||
// 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;
|
||||
// Every candidate failed. Park the run (keeping all state) so the user
|
||||
// can fix auth or wait for quota renewal, then /continue.
|
||||
if (auth_failed || exhausted) && !self.is_cancelled() {
|
||||
self.park_exhausted(&last, auth_failed).await;
|
||||
continue;
|
||||
}
|
||||
return Err(last);
|
||||
|
||||
@@ -413,11 +413,51 @@ pub fn json_report(target: &str, findings: &[Finding], run_id: &str, meta: &Enga
|
||||
|
||||
/// Write the full report bundle: Markdown, JSON, HTML, and the Typst/PDF.
|
||||
/// Returns the primary artifact path (PDF if typst present, else the .typ).
|
||||
/// A "## Reproduction — PoC scripts" section listing the runnable proof-of-concept
|
||||
/// scripts agents wrote to `<run>/pocs/`. Each is a self-contained artifact the
|
||||
/// operator can re-run to replicate a finding, so the report ships with a live
|
||||
/// reproduction kit — not just prose. Empty string when no PoCs were produced.
|
||||
pub fn pocs_section(dir: &Path) -> String {
|
||||
let pocs = dir.join("pocs");
|
||||
let mut entries: Vec<(String, String)> = Vec::new();
|
||||
if let Ok(rd) = std::fs::read_dir(&pocs) {
|
||||
for e in rd.flatten() {
|
||||
let p = e.path();
|
||||
if !p.is_file() { continue; }
|
||||
let name = match p.file_name().and_then(|s| s.to_str()) { Some(n) => n.to_string(), None => continue };
|
||||
// First non-empty comment line doubles as a one-line description.
|
||||
let desc = std::fs::read_to_string(&p).ok()
|
||||
.and_then(|t| t.lines()
|
||||
.map(|l| l.trim())
|
||||
.find(|l| l.starts_with('#') || l.starts_with("//") || l.starts_with("/*"))
|
||||
.map(|l| l.trim_start_matches(['#', '/', '*', ' ']).trim().to_string()))
|
||||
.unwrap_or_default();
|
||||
entries.push((name, desc));
|
||||
}
|
||||
}
|
||||
if entries.is_empty() { return String::new(); }
|
||||
entries.sort();
|
||||
let mut s = String::from("## Reproduction — PoC scripts\n\n");
|
||||
s.push_str("Runnable proofs written to `pocs/` during the engagement. Re-run any of \
|
||||
them to replicate the corresponding finding.\n\n");
|
||||
for (name, desc) in entries {
|
||||
if desc.is_empty() {
|
||||
s.push_str(&format!("- `pocs/{name}`\n"));
|
||||
} else {
|
||||
s.push_str(&format!("- `pocs/{name}` — {desc}\n"));
|
||||
}
|
||||
}
|
||||
s.push('\n');
|
||||
s
|
||||
}
|
||||
|
||||
pub fn write_all(target: &str, findings: &[Finding], dir: &Path) -> std::io::Result<PathBuf> {
|
||||
std::fs::create_dir_all(dir)?;
|
||||
let run_id = dir.file_name().and_then(|s| s.to_str()).unwrap_or("run").to_string();
|
||||
let meta = read_meta(dir);
|
||||
std::fs::write(dir.join("report.md"), markdown(target, findings, &meta))?;
|
||||
let mut md = markdown(target, findings, &meta);
|
||||
md.push_str(&pocs_section(dir));
|
||||
std::fs::write(dir.join("report.md"), md)?;
|
||||
std::fs::write(dir.join("report.json"), json_report(target, findings, &run_id, &meta))?;
|
||||
std::fs::write(dir.join("report.html"), html(target, findings, &meta))?;
|
||||
typst_report(target, findings, dir)
|
||||
|
||||
@@ -28,7 +28,7 @@ cat <<'BANNER'
|
||||
|
||||
███╗ ██╗███████╗██╗ ██╗██████╗ ██████╗
|
||||
████╗ ██║██╔════╝██║ ██║██╔══██╗██╔═══██╗ NeuroSploit installer
|
||||
██╔██╗ ██║█████╗ ██║ ██║██████╔╝██║ ██║ v3.6.1 — Rust harness
|
||||
██╔██╗ ██║█████╗ ██║ ██║██████╔╝██║ ██║ v3.6.9 — Rust harness
|
||||
██║╚██╗██║██╔══╝ ██║ ██║██╔══██╗██║ ██║ by Joas A Santos
|
||||
██║ ╚████║███████╗╚██████╔╝██║ ██║╚██████╔╝ & Red Team Leaders
|
||||
╚═╝ ╚═══╝╚══════╝ ╚═════╝ ╚═╝ ╚═╝ ╚═════╝
|
||||
@@ -63,7 +63,7 @@ if [ -z "$REF" ]; then
|
||||
REF="$(dl "https://api.github.com/repos/${REPO_SLUG}/releases/latest" /dev/stdout 2>/dev/null \
|
||||
| grep -m1 '"tag_name"' | sed -E 's/.*"tag_name" *: *"([^"]+)".*/\1/' || true)"
|
||||
fi
|
||||
[ -z "$REF" ] && REF="v3.6.1"
|
||||
[ -z "$REF" ] && REF="v3.6.9"
|
||||
say "Release: $REF"
|
||||
|
||||
installed=0
|
||||
|
||||
Reference in New Issue
Block a user