mirror of
https://github.com/CyberSecurityUP/NeuroSploit.git
synced 2026-08-15 06:00:28 +02:00
feat: account registration, form analysis, credential vault + cleanup (v3.6.5)
- New agent account_registration_and_forms (+1 → 430): analyzes the app's forms and self-registers a benign test account (curl or Playwright) to reach the authenticated surface when no creds are given. - Probe extracts form details (action/method/fields/kind/CSRF) so form analysis is grounded; shown in the probe summary and recon JSON. - Hard anti-flood guardrail in SAFETY_DOCTRINE + the agent: at most 2 accounts per engagement, never loop/script/batch the register endpoint or flood the DB; reuse the account made; a test needing many sign-ups is a lead, not mass-creation. - Credential vault: engagement_ops directive tells agents to append created accounts to <run-dir>/vault.jsonl; finish() consolidates to vault.json, masks secrets in the report, and adds a 'Test accounts created (DELETE after)' cleanup finding listing each account and how it was created. - Finding tagging: new auth_context (authenticated/unauthenticated) and account fields, rendered per-finding in the HTML report. - Opt-in disposable email (off by default): /tempmail on + RunConfig.temp_email; agents may use the free mail.tm API to read a registration confirmation code. - Tests: parse_forms unit tests; docs updated (README/TUTORIAL/RELEASE), counts 430.
This commit is contained in:
@@ -15,7 +15,7 @@
|
||||
<img src="https://img.shields.io/badge/Version-3.6.5-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-429-red?style=flat-square">
|
||||
<img src="https://img.shields.io/badge/MD%20Agents-430-red?style=flat-square">
|
||||
<img src="https://img.shields.io/badge/Models-15%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">
|
||||
@@ -38,7 +38,13 @@
|
||||
> runs an attacker→**LLM-judge** loop — capture the baseline refusal, apply the
|
||||
> technique across variants, judge whether the guardrail was truly bypassed —
|
||||
> proving it with a **benign, redacted** receipt. `neurosploit aitest <ai-url>`.
|
||||
> Also adds **Claude Opus 5**, **Claude Sonnet 5**, and **Kimi K3** (new Moonshot
|
||||
> Also: **self-service test-account registration** — analyzes the app's forms and
|
||||
> creates a benign account (curl or Playwright) to reach the **authenticated**
|
||||
> surface, with a hard **anti-flood guardrail** (≤2 accounts), a per-run
|
||||
> **credential vault** (`vault.json`, secrets masked in the report), a **"delete
|
||||
> these accounts" cleanup** section, findings tagged **authenticated /
|
||||
> unauthenticated**, and **opt-in disposable email** (`/tempmail`, free mail.tm).
|
||||
> New models: **Claude Opus 5**, **Claude Sonnet 5**, **Kimi K3** (new Moonshot
|
||||
> provider → 15 providers). Full history in [RELEASE.md](RELEASE.md).
|
||||
|
||||
---
|
||||
@@ -49,7 +55,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 **429 markdown agents** and a **Mission
|
||||
grounding** before reporting. It ships **430 markdown agents** and a **Mission
|
||||
Control TUI**.
|
||||
|
||||
### Engagement modes
|
||||
|
||||
+22
@@ -37,6 +37,28 @@
|
||||
as a finder or in the validator voting panel, e.g.
|
||||
`--model anthropic:claude-opus-5 --model moonshot:kimi-k3`.
|
||||
|
||||
- **Account registration & form analysis (+1 agent → total 430).** A new
|
||||
`account_registration_and_forms` agent lets NeuroSploit reach the authenticated
|
||||
surface on its own: it analyzes the app's forms (the deterministic probe now
|
||||
extracts each `<form>`'s action/method/fields/kind/CSRF) and creates a benign
|
||||
test account with **curl** or the **Playwright browser** when no creds are given.
|
||||
- **Anti-flood guardrail (hard):** at most **2 accounts per engagement**, never
|
||||
looping/scripting/batching the register endpoint or flooding the database —
|
||||
reuse the account made; a test needing many sign-ups is reported as a lead and
|
||||
stopped. Enforced in `SAFETY_DOCTRINE` (all flows) and the agent.
|
||||
- **Credential vault:** every generated credential is saved to
|
||||
**`<run-dir>/vault.json`** for the operator to consult; secrets are **masked in
|
||||
the report**. The report adds a **"Test accounts created (DELETE after)"**
|
||||
cleanup section listing each account and how it was created.
|
||||
- **Finding labels:** findings are tagged **`auth_context`**
|
||||
(authenticated/unauthenticated) and **`account`** (which test user/role proved
|
||||
it) — so grey-box shows which findings needed a login, and black-box records how
|
||||
the user was created.
|
||||
- **Disposable email (opt-in, off by default):** `/tempmail on` (or `temp_email`)
|
||||
lets agents use the free **mail.tm** API (no key) to read a registration
|
||||
confirmation code; off by default, a required confirmation is reported as a
|
||||
blocker rather than bypassed.
|
||||
|
||||
## Previously in v3.6.4
|
||||
|
||||
|
||||
|
||||
+41
-4
@@ -40,7 +40,7 @@ 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 429).
|
||||
(it does *not* blindly run all 430).
|
||||
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
|
||||
@@ -99,7 +99,7 @@ Agents **degrade gracefully**: if `rustscan` is absent they use `nmap`; if neith
|
||||
|
||||
```bash
|
||||
neurosploit --version # neurosploit 3.6.5
|
||||
neurosploit agents # {"vulns":240,...,"ai":30,...,"total":429}
|
||||
neurosploit agents # {"vulns":241,...,"ai":30,...,"total":430}
|
||||
neurosploit models # all providers & models
|
||||
```
|
||||
|
||||
@@ -311,6 +311,42 @@ execution.
|
||||
All AI testing is **authorized, non-destructive** — demonstrations stay benign and
|
||||
redacted; the goal is to prove the guardrail bypass, not to cause harm.
|
||||
|
||||
### 5.6 Test accounts, form analysis & the credential vault
|
||||
|
||||
To reach the high-impact **authenticated** surface, NeuroSploit can **analyze the
|
||||
app's forms and create its own test account** when you don't supply credentials —
|
||||
with **curl** (plain HTML/API forms: GET for CSRF+cookies, then POST) or the
|
||||
**Playwright browser** (JS-rendered / multi-step forms, e.g. Juice Shop). The
|
||||
deterministic probe now extracts each `<form>`'s action/method/fields/kind, so the
|
||||
agents know exactly what to submit.
|
||||
|
||||
- **Anti-flood guardrail (hard):** at most **2 accounts per engagement** (1 user; a
|
||||
2nd only when a test needs two users, e.g. horizontal IDOR). Agents never loop /
|
||||
script / batch the register endpoint or flood the database; they reuse the
|
||||
account they made. A test that would need many sign-ups is reported as a lead and
|
||||
stopped.
|
||||
- **Credential vault:** every account/credential the run generates is written to
|
||||
**`<run-dir>/vault.json`** so you can consult the passwords later. Secrets are
|
||||
**masked in the report** and live only in the vault.
|
||||
- **Cleanup list:** the report includes an Info finding **"Test accounts created
|
||||
(DELETE after)"** listing each account and exactly **how it was created** — so you
|
||||
can remove them when done.
|
||||
- **Finding labels:** every finding is tagged **`Auth: authenticated`** /
|
||||
**`unauthenticated`** and **`Account:`** (which test user/role proved it) — so in
|
||||
grey-box you see which findings needed a login, and in black-box you see what the
|
||||
agent did to create the user.
|
||||
- **Disposable email (opt-in, off by default):** if registration requires an email
|
||||
confirmation code, enable **`/tempmail on`** (REPL) — agents may then use the free
|
||||
**mail.tm** API (no key) to create a throwaway inbox and read the code. Off by
|
||||
default: a required confirmation is otherwise reported as a blocker, not bypassed.
|
||||
|
||||
```
|
||||
neurosploit› /target http://localhost:3001 # e.g. a local Juice Shop
|
||||
neurosploit› /tempmail on # only if signup needs email confirmation
|
||||
neurosploit› /run # analyzes forms, self-registers, tests authenticated
|
||||
neurosploit› /report # see the vault-backed "Test accounts (DELETE after)" section
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. The interactive REPL
|
||||
@@ -332,6 +368,7 @@ A context bar shows `model auth · cwd · mode▸target`. Key commands:
|
||||
/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
|
||||
/tempmail on|off opt-in disposable inbox (mail.tm) for a register confirmation code
|
||||
/run launch the engagement
|
||||
/runs /results [n] /report [n] /status [n]
|
||||
/diff what changed vs the previous run
|
||||
@@ -581,11 +618,11 @@ built from SAST/dataflow), so uncertainty becomes *path reachability*, not state
|
||||
|
||||
## 13. The agent library
|
||||
|
||||
`agents_md/` holds **429** markdown agents in categories:
|
||||
`agents_md/` holds **430** markdown agents in categories:
|
||||
|
||||
| Category | Dir | Count | Purpose |
|
||||
|----------|-----|-------|---------|
|
||||
| Vulnerability specialists | `vulns/` | 240 | exploit a specific class |
|
||||
| Vulnerability specialists | `vulns/` | 241 | exploit a specific class · incl. account registration & form analysis |
|
||||
| Recon | `recon/` | 12 | information gathering |
|
||||
| Code (SAST) | `code/` | 78 | white-box source review |
|
||||
| Infra | `infra/` | 34 | Linux / Windows / AD host testing |
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
# Account Registration & Form Analysis Agent
|
||||
## User Prompt
|
||||
You are testing **{target}**. Your job: ANALYZE the app's forms and, when no credentials were provided, CREATE a legitimate test account so the rest of the engagement can test the AUTHENTICATED surface. Authorized, non-destructive.
|
||||
|
||||
**Recon Context (includes `form_details`: action/method/fields/kind/has_csrf):**
|
||||
{recon_json}
|
||||
|
||||
**METHODOLOGY:**
|
||||
|
||||
### 1. Analyze every form
|
||||
- From the probe's `form_details` (and by fetching the page), map each `<form>`: its `action`, `method`, every input `name`/`type`, hidden fields, and any CSRF/anti-forgery token.
|
||||
- Classify each form: **register / login / search / password-reset / other**. Note required fields (email, username, password, confirm-password, phone, DOB, security question), client-side validation, and the exact POST body shape (`application/x-www-form-urlencoded` vs `application/json`).
|
||||
- For SPAs (Angular/React/Vue — e.g. Juice Shop) the register/login form posts to a JSON REST endpoint (e.g. `POST /api/Users`, `/rest/user/login`). Discover it from the network calls (browser/MCP) or JS, not just the HTML.
|
||||
|
||||
### 2. Register a test account
|
||||
- Prefer **curl** for a plain HTML/API form: GET the form first to collect any CSRF token + cookies, then POST the fields. Use a clearly-marked, unique, benign identity — e.g. `nrsplt_<rand>@example.test` / username `nrsplt_<rand>` / a strong throwaway password. Satisfy validation (matching confirm-password, valid email format, required security question/answer).
|
||||
- Use the **browser (Playwright MCP)** when the form is JS-rendered / multi-step / has client-side validation or captcha-like flow: navigate, fill fields, submit, and read the result.
|
||||
- Honor server rules: one account is enough. Do NOT mass-register, brute-force, or spam. If self-registration is disabled, say so and stop (report it as an observation, not a vuln).
|
||||
|
||||
### 3. Verify & capture the session
|
||||
- Confirm the account exists: log in with it and capture the auth material (Set-Cookie session, JWT/Bearer, CSRF token). Show the exact request + the success response as the receipt.
|
||||
- Hand the working session forward so authenticated agents (IDOR, access-control, authenticated_surface_exploit, business-logic) can reuse it. Register a SECOND account when a test needs two users (horizontal IDOR).
|
||||
|
||||
### 4. Probe the registration/login logic itself (report real issues only)
|
||||
- Mass-assignment / privilege escalation at signup: add unexpected fields (`role=admin`, `isAdmin=true`, `type`, `group`) to the register request and check if the server accepts them → account created with elevated role.
|
||||
- Weak password policy, username/email enumeration (different response for existing vs new), missing rate-limiting on register/login, verbose validation errors, and no email verification when the app implies it.
|
||||
- CSRF on register/login if no token is required.
|
||||
|
||||
### 5. Report
|
||||
```
|
||||
FINDING:
|
||||
- Title: [e.g. "Mass-assignment at registration grants admin role" / "Test account self-registration (capability used for authenticated testing)"]
|
||||
- Severity: [High for privesc/mass-assignment; Info for a benign account created as a testing capability]
|
||||
- CWE: [CWE-915 mass-assignment / CWE-306 / CWE-620 / CWE-352 as applicable]
|
||||
- Endpoint: [register/login endpoint]
|
||||
- Payload: [exact request that created/escalated the account]
|
||||
- Evidence: [request + response proving the account exists / the role was set]
|
||||
- Impact: [what the flaw allows]
|
||||
- Remediation: [allow-list bindable fields; server-set roles; verify email; rate-limit; strong password policy; CSRF tokens]
|
||||
```
|
||||
|
||||
## System Prompt
|
||||
You are an account-provisioning and form-analysis specialist on an AUTHORIZED, non-destructive engagement. Your primary goal is enabling authenticated testing: analyze the target's forms (curl for plain HTML/API forms, the Playwright MCP browser for JS-rendered/multi-step ones), then create ONE clearly-marked benign test account (`nrsplt_*@example.test`) and capture a working session to reuse. HARD GUARDRAIL: create AT MOST 2 accounts for the whole engagement (1 user; a 2nd only if a test needs two users), and REUSE them — never loop/script/batch/fuzz the register endpoint or flood the database with sign-ups. To test the register endpoint itself, send only a few controlled requests. If a test would need many registrations, report it as a lead and stop. If self-registration is disabled, report that as an observation and stop. Separately, report GENUINE registration/login flaws (mass-assignment/privilege escalation, missing rate-limit, user enumeration, CSRF, weak policy) only when proven with a real request+response receipt. A created test account is reported as an Info capability, not a vulnerability. Credits: Joas A Santos and Red Team Leaders.
|
||||
@@ -142,7 +142,7 @@ struct LiveCheckpoint {
|
||||
const COMMANDS: &[&str] = &[
|
||||
"/help", "/onboard", "/show", "/config", "/providers", "/model", "/key", "/sub", "/target",
|
||||
"/repo", "/auth", "/creds", "/focus", "/attach", "/context", "/mcp", "/offline",
|
||||
"/votes", "/chain", "/recon", "/timeout", "/proxy", "/burp", "/ua", "/agents", "/theme", "/clear", "/run", "/stop", "/continue", "/runs", "/results", "/report",
|
||||
"/votes", "/chain", "/recon", "/tempmail", "/timeout", "/proxy", "/burp", "/ua", "/agents", "/theme", "/clear", "/run", "/stop", "/continue", "/runs", "/results", "/report",
|
||||
"/status", "/logs", "/diff", "/retest", "/validate", "/finding", "/expand", "/integrations", "/quit",
|
||||
];
|
||||
|
||||
@@ -238,6 +238,8 @@ struct Session {
|
||||
max_agents: usize,
|
||||
chain_depth: usize,
|
||||
recon_intensity: usize,
|
||||
/// Opt-in disposable email (mail.tm) for register flows needing a confirmation code.
|
||||
temp_email: bool,
|
||||
/// Idle guardrail: stop a run if no NEW finding lands in this many seconds
|
||||
/// (0 = disabled). Set in minutes via `/timeout <mins>`.
|
||||
idle_secs: u64,
|
||||
@@ -269,6 +271,7 @@ impl Default for Session {
|
||||
max_agents: 0,
|
||||
chain_depth: 2,
|
||||
recon_intensity: 3,
|
||||
temp_email: false,
|
||||
idle_secs: 300, // 5-minute idle guardrail by default
|
||||
proxy: None,
|
||||
user_agent: None,
|
||||
@@ -602,6 +605,13 @@ pub async fn repl(base: &Path) -> anyhow::Result<()> {
|
||||
if arg.is_empty() { println!(" recon intensity: {} ({}) — set with /recon <1-4> [1 quick · 2 standard · 3 deep · 4 exhaustive]", s.recon_intensity, lvl(s.recon_intensity)); }
|
||||
else { s.recon_intensity = arg.parse::<usize>().unwrap_or(s.recon_intensity).clamp(1, 4); println!(" recon intensity: {} ({}) — more rounds, more enumeration, auto-installs tools", s.recon_intensity, lvl(s.recon_intensity)); }
|
||||
}
|
||||
"/tempmail" | "/temp-email" => {
|
||||
match arg.trim() {
|
||||
"on" | "true" | "1" => { s.temp_email = true; println!(" temp-email: \x1b[32mon\x1b[0m — register flows may use the free mail.tm inbox to read a confirmation code"); }
|
||||
"off" | "false" | "0" => { s.temp_email = false; println!(" temp-email: \x1b[2moff\x1b[0m — a register step that requires email confirmation is reported as a blocker"); }
|
||||
_ => println!(" temp-email: {} — /tempmail on|off (opt-in disposable inbox for register confirmation)", if s.temp_email { "\x1b[32mon\x1b[0m" } else { "\x1b[2moff\x1b[0m" }),
|
||||
}
|
||||
}
|
||||
"/agents" => {
|
||||
if arg == "list" || arg == "ls" {
|
||||
let lib = agents::load(base);
|
||||
@@ -1028,6 +1038,7 @@ async fn run(base: &Path, s: &Session, history: &mut Vec<RunRecord>) {
|
||||
cfg.vote_n = s.vote_n;
|
||||
cfg.chain_depth = s.chain_depth;
|
||||
cfg.recon_intensity = s.recon_intensity;
|
||||
cfg.temp_email = s.temp_email;
|
||||
cfg.proxy = s.proxy.clone();
|
||||
cfg.user_agent = s.user_agent.clone();
|
||||
cfg.max_agents = s.max_agents;
|
||||
@@ -1105,6 +1116,7 @@ async fn start_background(base: &Path, s: &Session, reader: &mut Reader,
|
||||
cfg.vote_n = s.vote_n;
|
||||
cfg.chain_depth = s.chain_depth;
|
||||
cfg.recon_intensity = s.recon_intensity;
|
||||
cfg.temp_email = s.temp_email;
|
||||
cfg.proxy = s.proxy.clone();
|
||||
cfg.user_agent = s.user_agent.clone();
|
||||
cfg.max_agents = s.max_agents;
|
||||
@@ -1599,9 +1611,9 @@ fn show(s: &Session) {
|
||||
println!(" │ proxy : {}", s.proxy.clone().unwrap_or_else(|| "(none — /proxy for Burp/ZAP)".into()));
|
||||
println!(" │ user-agent: {}", s.user_agent.clone().unwrap_or_else(|| "NeuroSploit (default)".into()));
|
||||
println!(" │ focus : {}", s.instructions.clone().unwrap_or_else(|| "(none — tests everything)".into()));
|
||||
println!(" │ opts : mcp={} offline={} votes={} recon={} chain-depth={} max-agents={} idle-stop={}",
|
||||
println!(" │ opts : mcp={} offline={} votes={} recon={} chain-depth={} max-agents={} idle-stop={} temp-email={}",
|
||||
onoff(s.mcp), onoff(s.offline), s.vote_n, s.recon_intensity, s.chain_depth, s.max_agents,
|
||||
if s.idle_secs == 0 { "off".to_string() } else { format!("{}m", s.idle_secs / 60) });
|
||||
if s.idle_secs == 0 { "off".to_string() } else { format!("{}m", s.idle_secs / 60) }, onoff(s.temp_email));
|
||||
// Integrations at a glance (see /integrations for detail).
|
||||
{
|
||||
let ig = harness::integrations::Integrations::load(&proj_dir());
|
||||
@@ -1669,6 +1681,7 @@ fn help() {
|
||||
h("/votes <n>", "number of validator votes per finding");
|
||||
h("/chain <n>", "attack-chain depth (post-exploitation pivots; 0 = off)");
|
||||
h("/recon <1-4>", "recon intensity: 1 quick · 2 standard · 3 deep · 4 exhaustive (installs tools)");
|
||||
h("/tempmail on|off", "opt-in disposable inbox (mail.tm) to read a register confirmation code");
|
||||
h("/timeout <min>", "idle guardrail: stop if no new finding in <min> (0 = off)");
|
||||
h("/proxy <url>|off", "route agent HTTP through Burp/ZAP (/burp = default :8080)");
|
||||
h("/ua <string>", "identifying User-Agent for NeuroSploit traffic (default = NeuroSploit)");
|
||||
|
||||
@@ -4,7 +4,7 @@ use crate::rl::{severity_reward, RlState};
|
||||
use crate::types::{Finding, RunConfig};
|
||||
use crate::report;
|
||||
use futures::stream::{self, StreamExt};
|
||||
use serde::Serialize;
|
||||
use serde::{Serialize, Deserialize};
|
||||
use std::path::{Path, PathBuf};
|
||||
use tokio::sync::mpsc::Sender;
|
||||
|
||||
@@ -181,7 +181,80 @@ operator has explicitly authorized that specific action. Read-only, minimal proo
|
||||
emails, CPF/SSN, phones, cards, tokens): confirm access with the SMALLEST possible sample and REDACT it in the \
|
||||
report (e.g. show 1 masked record + a count) — never dump, store, or transmit the dataset. Prefer benign markers \
|
||||
and OOB/echo checks over any state-changing payload. When unsure whether an action is safe, don't do it — report \
|
||||
it as reachable and stop.\n\n";
|
||||
it as reachable and stop.\n\
|
||||
ACCOUNT-CREATION GUARDRAIL (hard limit): creating accounts is state-changing — do it ONLY to enable authenticated \
|
||||
testing, and create AT MOST 2 accounts for the ENTIRE engagement (1 normal user; a 2nd only when a test genuinely \
|
||||
needs two users, e.g. horizontal IDOR). NEVER loop, script, fuzz, or batch the register endpoint; do not write a \
|
||||
tool/PoC that submits it repeatedly; do not flood or stress the database with sign-ups. Each account must be a \
|
||||
single, clearly-marked benign identity (`nrsplt_<rand>@example.test`). REUSE the account you already made instead of \
|
||||
making new ones. To TEST the register endpoint itself (rate-limit, mass-assignment, enumeration, CSRF, weak policy), \
|
||||
send only a FEW controlled requests and prove the flaw from those — never a high-volume run. If a test would require \
|
||||
many registrations, report it as a LEAD and STOP rather than mass-creating accounts.\n\n";
|
||||
/// Per-run operational directive: the credential VAULT path, the account cap,
|
||||
/// finding-tagging rules, and (opt-in) disposable-email use. Injected into web
|
||||
/// engagement prompts so created accounts are logged for cleanup and findings
|
||||
/// are labelled authenticated vs unauthenticated.
|
||||
fn engagement_ops(cfg: &RunConfig) -> String {
|
||||
let vault = cfg.workdir.as_deref()
|
||||
.map(|d| format!("{}/vault.jsonl", d.trim_end_matches('/')))
|
||||
.unwrap_or_else(|| "the run directory's vault.jsonl".into());
|
||||
let temp = if cfg.temp_email {
|
||||
"DISPOSABLE EMAIL (enabled): if registration requires an email confirmation code/link, you MAY use the free \
|
||||
mail.tm API (no key) — `POST https://api.mail.tm/accounts` {address,password} to create an inbox (get a \
|
||||
valid domain from `GET https://api.mail.tm/domains`), `POST https://api.mail.tm/token` for a JWT, then poll \
|
||||
`GET https://api.mail.tm/messages` (Bearer JWT) to read the confirmation code/link. Use the mail.tm address \
|
||||
as the account email. Guerrilla Mail's API is a fallback. "
|
||||
} else {
|
||||
"DISPOSABLE EMAIL (disabled): if registration REQUIRES an email confirmation you cannot receive, stop and \
|
||||
report it as a blocker (do not attempt to bypass it). "
|
||||
};
|
||||
format!(
|
||||
"ENGAGEMENT OPS — TEST ACCOUNTS & VAULT:\n\
|
||||
- CREDENTIAL VAULT: whenever you create a test account or generate any credential, APPEND one JSON line to \
|
||||
`{vault}` (create the file if missing) of the form \
|
||||
{{\"account\":\"<email/username>\",\"secret\":\"<password>\",\"role\":\"<role>\",\"endpoint\":\"<register endpoint>\",\"how\":\"<curl|browser + the exact steps you used>\",\"auth_flow\":\"<how you logged in / got the session>\"}}. \
|
||||
This vault is the single place secrets are stored so you (and the operator) can consult them later; also \
|
||||
set the finding's `secret` field so it is captured even if the file write fails.\n\
|
||||
- CLEANUP: every account you create MUST be reported so it can be deleted afterwards — the run report lists \
|
||||
them from the vault. Do not leave undocumented accounts.\n\
|
||||
- LABEL FINDINGS: set `auth_context` to \"authenticated\" (proven while logged in with a test/given account) \
|
||||
or \"unauthenticated\" (proven with no session), and `account` to which user/role you used. In grey-box, be \
|
||||
explicit about which findings needed a login. In black-box, record in `how`/evidence exactly what you did \
|
||||
to create the user.\n\
|
||||
- {temp}\n\n"
|
||||
)
|
||||
}
|
||||
/// One credential the run generated (a created test account). Stored in the run
|
||||
/// vault so the operator can consult it and later delete the account.
|
||||
#[derive(Serialize, Deserialize, Default, Clone)]
|
||||
struct VaultEntry {
|
||||
#[serde(default)] account: String,
|
||||
#[serde(default)] secret: String,
|
||||
#[serde(default)] role: String,
|
||||
#[serde(default)] endpoint: String,
|
||||
#[serde(default)] how: String,
|
||||
#[serde(default)] auth_flow: String,
|
||||
}
|
||||
|
||||
/// Read the agent-appended `vault.jsonl` (one JSON object per created account)
|
||||
/// from the run dir. Best-effort: skips malformed lines. `_findings` reserved for
|
||||
/// future correlation. Deduped by account identity.
|
||||
fn collect_vault(dir: &str, _findings: &[Finding]) -> Vec<VaultEntry> {
|
||||
let path = format!("{}/vault.jsonl", dir.trim_end_matches('/'));
|
||||
let mut out: Vec<VaultEntry> = Vec::new();
|
||||
if let Ok(txt) = std::fs::read_to_string(&path) {
|
||||
for line in txt.lines() {
|
||||
let line = line.trim();
|
||||
if line.is_empty() { continue; }
|
||||
if let Ok(v) = serde_json::from_str::<VaultEntry>(line) {
|
||||
if v.account.is_empty() { continue; }
|
||||
if !out.iter().any(|e| e.account == v.account) { out.push(v); }
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
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
|
||||
@@ -214,6 +287,7 @@ const DECISION_DOCTRINE: &str = "DECIDE WHERE TO ATTACK (analyse, then act):\n\
|
||||
- Mine PARAMETERS: enumerate query/body/header/cookie params (incl. hidden ones from JS/source maps); for each, reason about what it does and test the fitting attack (IDOR, injection, path traversal, mass-assignment, open-redirect, SSRF). Add plausible params the API might accept (id, user, role, admin, debug, redirect, file, callback).\n\
|
||||
- MOCK realistic data: when a request needs valid-looking input to reach deeper logic, synthesize believable test data (emails, names, CPFs/SSNs with valid checksums, phone numbers, UUIDs, tokens, JSON bodies) so the flow proceeds — never use real PII.\n\
|
||||
- Authenticated testing: if you can authenticate (given creds/roles or a login you performed), REUSE the session and exploit the AUTHENTICATED surface — the endpoints/params only reachable while logged in are where the high-impact bugs live. Test as EACH role you have (e.g. normal user AND admin) and compare.\n\
|
||||
- Self-register when no creds are given: if the app allows sign-up, ANALYZE the register form (probe `form_details` has action/method/fields) and CREATE one clearly-marked benign test account (`nrsplt_<rand>@example.test`) — with curl (GET for CSRF+cookies, then POST the fields) or the Playwright browser for JS-rendered/multi-step forms — then log in and REUSE that session for authenticated testing. Register a second account only when a test needs two users. Non-destructive: one account, no mass-registration/spam; at signup also try mass-assignment (`role=admin`/`isAdmin`) and report it if accepted.\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";
|
||||
|
||||
@@ -294,6 +368,7 @@ pub async fn run(cfg: RunConfig, lib: &Library, pool: &ModelPool, tx: Sender<Str
|
||||
let verbose = cfg.verbose;
|
||||
let mcp_on = pool.mcp_config.is_some();
|
||||
let directives = operator_directives(&cfg);
|
||||
let ops = engagement_ops(&cfg);
|
||||
// Token economy: each agent gets a capped recon context, not the full blob.
|
||||
let recon_ctx: String = recon.chars().take(3500).collect();
|
||||
let raw: Vec<(String, String, Vec<Finding>)> = stream::iter(selected.iter().cloned())
|
||||
@@ -301,6 +376,7 @@ pub async fn run(cfg: RunConfig, lib: &Library, pool: &ModelPool, tx: Sender<Str
|
||||
let target = target.clone();
|
||||
let recon = recon_ctx.clone();
|
||||
let directives = directives.clone();
|
||||
let ops = ops.clone();
|
||||
let txc = tx.clone();
|
||||
async move {
|
||||
if pool.stop_exploiting() {
|
||||
@@ -312,13 +388,16 @@ 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}{depth}{decision}{safety}{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).",
|
||||
{directives}{react}{depth}{decision}{safety}{ops}{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,auth_context,account,secret}}. \
|
||||
`evidence` must contain the concrete proof (request/response excerpt). \
|
||||
Set `auth_context` to \"authenticated\" or \"unauthenticated\"; set `account` to the test user/role you used (if any); \
|
||||
for a created test account set `secret` to its generated password (it is stored in the run vault and masked in the report).",
|
||||
target = target,
|
||||
directives = directives,
|
||||
react = REACT_DOCTRINE,
|
||||
depth = DEPTH_DOCTRINE, decision = DECISION_DOCTRINE, safety = SAFETY_DOCTRINE,
|
||||
ops = ops,
|
||||
doctrine = tool_doctrine(mcp_on),
|
||||
body = ag.user.replace("{target}", &target).replace("{recon_json}", &recon),
|
||||
);
|
||||
@@ -516,6 +595,7 @@ pub async fn run_greybox(cfg: RunConfig, lib: &Library, pool: &ModelPool, tx: Se
|
||||
let verbose = cfg.verbose;
|
||||
let mcp_on = pool.mcp_config.is_some();
|
||||
let directives = operator_directives(&cfg);
|
||||
let ops = engagement_ops(&cfg);
|
||||
let recon_ctx: String = recon.chars().take(3000).collect();
|
||||
let leads_ctx = code_leads.clone();
|
||||
let raw: Vec<(String, String, Vec<Finding>)> = stream::iter(selected.iter().cloned())
|
||||
@@ -523,6 +603,7 @@ pub async fn run_greybox(cfg: RunConfig, lib: &Library, pool: &ModelPool, tx: Se
|
||||
let target = target.clone();
|
||||
let recon = recon_ctx.clone();
|
||||
let directives = directives.clone();
|
||||
let ops = ops.clone();
|
||||
let leads = leads_ctx.clone();
|
||||
let txc = tx.clone();
|
||||
async move {
|
||||
@@ -534,11 +615,12 @@ 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}{depth}{decision}{safety}{doctrine}{body}\n\n\
|
||||
Proceed and PROVE each issue against the LIVE app.\n\n{directives}{leads}{react}{depth}{decision}{safety}{ops}{doctrine}{body}\n\n\
|
||||
Reply ONLY a JSON array of confirmed findings (may be []): \
|
||||
{{id,title,severity,cwe,endpoint,payload,evidence,impact,remediation,confidence}}.",
|
||||
{{id,title,severity,cwe,endpoint,payload,evidence,impact,remediation,confidence,auth_context,account,secret}}. \
|
||||
Set `auth_context` (authenticated/unauthenticated) and `account` (test user/role used); for a created account set `secret`.",
|
||||
target = target, directives = directives, leads = leads,
|
||||
react = REACT_DOCTRINE, depth = DEPTH_DOCTRINE, decision = DECISION_DOCTRINE, safety = SAFETY_DOCTRINE, doctrine = tool_doctrine(mcp_on),
|
||||
react = REACT_DOCTRINE, depth = DEPTH_DOCTRINE, decision = DECISION_DOCTRINE, safety = SAFETY_DOCTRINE, ops = ops, 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 {
|
||||
@@ -935,6 +1017,53 @@ async fn finish(cfg: RunConfig, _lib: &Library, recon: String, transcript: Strin
|
||||
// White-box/skills are symbolic → deterministic belief; grey-box carries source too.
|
||||
let whitebox = matches!(gmode, GroundMode::Symbolic | GroundMode::Either);
|
||||
|
||||
// --- Credential vault & test-account cleanup ---------------------------
|
||||
// Consolidate every test account created this run (from the agent-appended
|
||||
// vault.jsonl AND any finding that carried a generated `secret`) into a single
|
||||
// vault.json the operator can consult, then MASK the secret in the report and
|
||||
// add a cleanup summary listing the accounts to delete.
|
||||
if let Some(dir) = cfg.workdir.clone() {
|
||||
let mut vault = collect_vault(&dir, &findings);
|
||||
// Fold in secrets captured on findings (dedup by account identity).
|
||||
for f in &findings {
|
||||
if !f.secret.is_empty() && !f.account.is_empty()
|
||||
&& !vault.iter().any(|v| v.account == f.account) {
|
||||
vault.push(VaultEntry {
|
||||
account: f.account.clone(), secret: f.secret.clone(), role: String::new(),
|
||||
endpoint: f.endpoint.clone(), how: f.payload.clone(), auth_flow: String::new(),
|
||||
});
|
||||
}
|
||||
}
|
||||
if !vault.is_empty() {
|
||||
let path = format!("{}/vault.json", dir.trim_end_matches('/'));
|
||||
if let Ok(j) = serde_json::to_string_pretty(&vault) { let _ = std::fs::write(&path, j); }
|
||||
let _ = tx.send(format!(
|
||||
"notify: 🔐 vault: {} test account(s) saved → {} — DELETE these after the engagement",
|
||||
vault.len(), path)).await;
|
||||
// Cleanup summary finding (secrets live only in the vault, masked here).
|
||||
let list = vault.iter()
|
||||
.map(|v| format!("• {}{} — created via {}", v.account,
|
||||
if v.role.is_empty() { String::new() } else { format!(" [{}]", v.role) },
|
||||
if v.how.is_empty() { "the registration flow".to_string() } else { v.how.chars().take(160).collect::<String>() }))
|
||||
.collect::<Vec<_>>().join("\n");
|
||||
findings.push(Finding {
|
||||
id: "test-accounts".into(), agent: "account_registration_and_forms".into(),
|
||||
title: "Test accounts created during the engagement (DELETE after)".into(),
|
||||
severity: "Info".into(), endpoint: cfg.target.clone(),
|
||||
evidence: format!("{} account(s) created for authenticated testing. Credentials are in vault.json (not shown here).\n{}", vault.len(), list),
|
||||
impact: "Operational cleanup: remove these accounts once testing is complete.".into(),
|
||||
remediation: "Delete the listed test accounts; rotate anything they touched.".into(),
|
||||
validated: true, confidence: 1.0, auth_context: "n/a".into(),
|
||||
account: format!("{} test account(s)", vault.len()),
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
// Mask any generated secret so it never appears in the human report.
|
||||
for f in findings.iter_mut() {
|
||||
if !f.secret.is_empty() { f.secret = "•••• (see vault.json)".into(); }
|
||||
}
|
||||
}
|
||||
|
||||
// --- 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
|
||||
|
||||
@@ -47,6 +47,20 @@ pub struct PathHit {
|
||||
pub len: usize,
|
||||
}
|
||||
|
||||
/// A parsed HTML `<form>` — enough for an agent to auto-submit it (e.g. register
|
||||
/// an account) with curl or the browser, without re-parsing the page.
|
||||
#[derive(Serialize, Default, Clone)]
|
||||
pub struct FormInfo {
|
||||
pub action: String,
|
||||
pub method: String,
|
||||
/// input/select/textarea field names with their type (name → type).
|
||||
pub fields: Vec<(String, String)>,
|
||||
/// Best-effort role guess: "register" | "login" | "search" | "other".
|
||||
pub kind: String,
|
||||
/// True if a CSRF/anti-forgery hidden token was seen in the form.
|
||||
pub has_csrf: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Default)]
|
||||
pub struct Probe {
|
||||
pub url: String,
|
||||
@@ -63,6 +77,9 @@ pub struct Probe {
|
||||
pub cors: Cors,
|
||||
pub scripts: Vec<String>,
|
||||
pub forms: usize,
|
||||
/// Parsed forms (action/method/fields) so an agent can auto-submit them —
|
||||
/// e.g. register a test account to reach the authenticated surface.
|
||||
pub form_details: Vec<FormInfo>,
|
||||
pub interesting_paths: Vec<PathHit>,
|
||||
/// Baseline for a random non-existent path (status + body length), so agents
|
||||
/// can tell a real hit from a soft-404 catch-all.
|
||||
@@ -97,6 +114,66 @@ fn between<'a>(s: &'a str, a: &str, b: &str) -> Option<&'a str> {
|
||||
Some(&s[i..j])
|
||||
}
|
||||
|
||||
/// Read one HTML attribute value (double- or single-quoted) from a tag slice.
|
||||
fn attr(tag: &str, name: &str) -> String {
|
||||
for q in ["\"", "'"] {
|
||||
if let Some(v) = between(tag, &format!("{name}={q}"), q) {
|
||||
return v.trim().to_string();
|
||||
}
|
||||
}
|
||||
String::new()
|
||||
}
|
||||
|
||||
/// Best-effort parse of the HTML `<form>`s on a page so agents can auto-submit
|
||||
/// them (e.g. register a test account) without re-parsing. Non-destructive: this
|
||||
/// only READS the markup. Bounded to the first few forms.
|
||||
fn parse_forms(body: &str) -> Vec<FormInfo> {
|
||||
let mut out = Vec::new();
|
||||
for chunk in body.split("<form").skip(1).take(8) {
|
||||
// The form's own attributes live before the first '>'.
|
||||
let head = chunk.split('>').next().unwrap_or("");
|
||||
let inner = chunk.split("</form").next().unwrap_or(chunk);
|
||||
let mut f = FormInfo {
|
||||
action: attr(head, "action"),
|
||||
method: {
|
||||
let m = attr(head, "method");
|
||||
if m.is_empty() { "get".into() } else { m.to_lowercase() }
|
||||
},
|
||||
..Default::default()
|
||||
};
|
||||
// Fields: <input>, <select>, <textarea> — capture name + type.
|
||||
for tag in ["<input", "<select", "<textarea"] {
|
||||
for seg in inner.split(tag).skip(1) {
|
||||
let t = seg.split('>').next().unwrap_or("");
|
||||
let name = attr(t, "name");
|
||||
if name.is_empty() { continue; }
|
||||
let ty = if tag == "<input" {
|
||||
let ty = attr(t, "type");
|
||||
if ty.is_empty() { "text".into() } else { ty.to_lowercase() }
|
||||
} else { tag.trim_start_matches('<').into() };
|
||||
if ty == "hidden" && (t.to_lowercase().contains("csrf") || t.to_lowercase().contains("token") || name.to_lowercase().contains("csrf") || name.to_lowercase().contains("_token")) {
|
||||
f.has_csrf = true;
|
||||
}
|
||||
if f.fields.len() < 25 && !f.fields.iter().any(|(n, _)| *n == name) {
|
||||
f.fields.push((name, ty));
|
||||
}
|
||||
}
|
||||
}
|
||||
// Guess the form's role from action + field names.
|
||||
let hay = format!("{} {}", f.action, f.fields.iter().map(|(n, _)| n.as_str()).collect::<Vec<_>>().join(" ")).to_lowercase();
|
||||
let has_pw = f.fields.iter().any(|(_, t)| t == "password");
|
||||
f.kind = if hay.contains("regist") || hay.contains("signup") || hay.contains("sign-up") || hay.contains("create") || (has_pw && (hay.contains("confirm") || hay.contains("repeat"))) {
|
||||
"register".into()
|
||||
} else if hay.contains("login") || hay.contains("signin") || hay.contains("sign-in") || hay.contains("auth") || has_pw {
|
||||
"login".into()
|
||||
} else if hay.contains("search") || hay.contains("query") || f.fields.iter().any(|(n, _)| n == "q") {
|
||||
"search".into()
|
||||
} else { "other".into() };
|
||||
out.push(f);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Run the probe. Never panics; on total failure returns a Probe with a note.
|
||||
pub async fn probe(target: &str) -> Probe {
|
||||
let mut p = Probe { url: target.to_string(), ..Default::default() };
|
||||
@@ -148,6 +225,7 @@ pub async fn probe(target: &str) -> Probe {
|
||||
p.title = t.trim().chars().take(120).collect();
|
||||
}
|
||||
p.forms = body.matches("<form").count();
|
||||
p.form_details = parse_forms(&body);
|
||||
// linked scripts (src="...")
|
||||
for cap in body.split("<script").skip(1) {
|
||||
if let Some(src) = between(cap, "src=\"", "\"").or_else(|| between(cap, "src='", "'")) {
|
||||
@@ -220,7 +298,7 @@ pub fn probe_json(p: &Probe) -> String {
|
||||
/// One-line human summary for the live feed.
|
||||
pub fn probe_summary(p: &Probe) -> String {
|
||||
format!(
|
||||
"probe: HTTP {} {}{} · {}{} · sec-headers {}/6 · {} cookie(s) · {} script(s){}{}",
|
||||
"probe: HTTP {} {}{} · {}{} · sec-headers {}/6 · {} cookie(s) · {} script(s){}{}{}",
|
||||
p.status,
|
||||
if p.server.is_empty() { "".into() } else { format!("{} ", p.server) },
|
||||
if p.tech.is_empty() { "".to_string() } else { format!("[{}]", p.tech.join(",")) },
|
||||
@@ -229,7 +307,47 @@ pub fn probe_summary(p: &Probe) -> String {
|
||||
p.security_headers.present,
|
||||
p.cookies.len(),
|
||||
p.scripts.len(),
|
||||
{
|
||||
let kinds: Vec<&str> = p.form_details.iter().map(|f| f.kind.as_str()).filter(|k| *k == "register" || *k == "login").collect();
|
||||
if kinds.is_empty() { String::new() } else { format!(" · forms: {}", kinds.join(",")) }
|
||||
},
|
||||
if p.cors.reflects_origin { " · CORS reflects origin!" } else { "" },
|
||||
if p.interesting_paths.is_empty() { String::new() } else { format!(" · hits: {}", p.interesting_paths.iter().map(|h| h.path.clone()).collect::<Vec<_>>().join(",")) },
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::parse_forms;
|
||||
|
||||
#[test]
|
||||
fn parses_register_form_fields_and_kind() {
|
||||
let html = r#"<html><body>
|
||||
<form action="/api/register" method="post">
|
||||
<input type="text" name="username">
|
||||
<input type="email" name="email">
|
||||
<input type="password" name="password">
|
||||
<input type="password" name="confirmPassword">
|
||||
<input type="hidden" name="csrf_token" value="abc">
|
||||
<button>Sign up</button>
|
||||
</form>
|
||||
<form action="/search" method="get"><input name="q"></form>
|
||||
</body></html>"#;
|
||||
let forms = parse_forms(html);
|
||||
assert_eq!(forms.len(), 2);
|
||||
let reg = &forms[0];
|
||||
assert_eq!(reg.action, "/api/register");
|
||||
assert_eq!(reg.method, "post");
|
||||
assert_eq!(reg.kind, "register");
|
||||
assert!(reg.has_csrf, "hidden csrf_token should be detected");
|
||||
assert!(reg.fields.iter().any(|(n, t)| n == "password" && t == "password"));
|
||||
assert_eq!(forms[1].kind, "search");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn login_form_detected_by_password() {
|
||||
let html = r#"<form action="/login"><input name="user"><input type="password" name="pw"></form>"#;
|
||||
let f = parse_forms(html);
|
||||
assert_eq!(f[0].kind, "login");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,12 +54,21 @@ pub fn html(target: &str, findings: &[Finding]) -> String {
|
||||
format!(
|
||||
"<section class=finding><h3><span class=sev style=background:{}>{}</span> {}. {}</h3>\
|
||||
<div class=m>{} · {} · CVSS {} · votes {} · conf {:.2}</div>\
|
||||
<div class=m>Endpoint: {}</div>\
|
||||
<div class=m>Endpoint: {}</div>{authline}\
|
||||
<h4>Payload</h4><pre>{}</pre><h4>Evidence</h4><pre>{}</pre>\
|
||||
<h4>Impact</h4><p>{}</p><h4>Remediation</h4><p>{}</p></section>",
|
||||
sev_color(&f.severity), esc(&f.severity), i + 1, esc(&f.title),
|
||||
esc(&f.agent), esc(&f.cwe), esc(&f.cvss), esc(&f.votes), f.confidence,
|
||||
esc(&f.endpoint), esc(&f.payload), esc(&f.evidence), esc(&f.impact), esc(&f.remediation),
|
||||
authline = {
|
||||
// Show the auth context and which test account proved this finding.
|
||||
if f.auth_context.is_empty() && f.account.is_empty() { String::new() }
|
||||
else {
|
||||
let ac = if f.auth_context.is_empty() { String::new() } else { format!("Auth: <b>{}</b>", esc(&f.auth_context)) };
|
||||
let acct = if f.account.is_empty() { String::new() } else { format!("{}Account: {}", if ac.is_empty() { "" } else { " · " }, esc(&f.account)) };
|
||||
format!("<div class=m>{ac}{acct}</div>")
|
||||
}
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
@@ -47,6 +47,20 @@ pub struct Finding {
|
||||
/// IDs of findings this one chains from (attack-path edges).
|
||||
#[serde(default)]
|
||||
pub chains_from: Vec<String>,
|
||||
/// Auth context in which this was proven: "authenticated" | "unauthenticated"
|
||||
/// | "" (unknown). Lets a report distinguish pre- and post-login findings —
|
||||
/// important in grey/black-box where the agent self-registered to test.
|
||||
#[serde(default)]
|
||||
pub auth_context: String,
|
||||
/// The test account/role used to prove this finding (e.g. "user1 · nrsplt_x@example.test"
|
||||
/// or "admin"). Empty when the finding needed no account.
|
||||
#[serde(default)]
|
||||
pub account: String,
|
||||
/// A credential generated during the run (password/token) for a created test
|
||||
/// account. Captured here transiently, moved to the run's vault, and MASKED in
|
||||
/// the human report. Only set on "test account created" capability findings.
|
||||
#[serde(default)]
|
||||
pub secret: String,
|
||||
}
|
||||
|
||||
impl Default for Finding {
|
||||
@@ -72,6 +86,9 @@ impl Default for Finding {
|
||||
exploitability: String::new(),
|
||||
business_impact: String::new(),
|
||||
chains_from: Vec::new(),
|
||||
auth_context: String::new(),
|
||||
account: String::new(),
|
||||
secret: String::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -141,6 +158,11 @@ pub struct RunConfig {
|
||||
/// more recon rounds, more active enumeration, and auto-installing tools.
|
||||
#[serde(default = "default_recon")]
|
||||
pub recon_intensity: usize,
|
||||
/// Opt-in: when the app requires email confirmation to register, allow the
|
||||
/// agent to use a free disposable-inbox API (mail.tm) to read the code/link.
|
||||
/// Off by default. Account creation is still capped by the safety guardrail.
|
||||
#[serde(default)]
|
||||
pub temp_email: bool,
|
||||
}
|
||||
|
||||
fn default_vote() -> usize {
|
||||
@@ -179,6 +201,7 @@ impl RunConfig {
|
||||
proxy: None,
|
||||
user_agent: None,
|
||||
recon_intensity: 3,
|
||||
temp_email: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user