browser-driven testing doctrine + 8 SPA/API agents (Juice Shop-ready)

- tool_doctrine: agents now actively DRIVE the browser on JS/SPA targets — use
  the Playwright MCP (render, read live DOM, click client-side routes, watch the
  network to find the real API, screenshot proof); when no MCP, use the Playwright
  CLI (write+run a small script / npx playwright screenshot) to render and capture
  XHR/fetch traffic — complementing curl (which only sees the empty shell).
- probe: detect SPAs (<app-root>, ng-version, near-empty body + linked scripts →
  Angular/React/Vue/SPA) and note in recon that the browser is required, so the
  SPA agents get selected.
- +8 SPA/API agents (library 383): spa_api_discovery, spa_hidden_admin,
  login_sqli_bypass, dom_xss_spa, api_bola_numeric_ids,
  register_privilege_mass_assign, jwt_forgery_spa, spa_business_logic.
- Docs: README/RELEASE/TUTORIAL counts + notes.
This commit is contained in:
CyberSecurityUP
2026-07-05 16:25:34 -03:00
parent 4ac4faec32
commit d931ce09a6
14 changed files with 496 additions and 9 deletions
@@ -51,9 +51,14 @@ fn operator_directives(cfg: &RunConfig) -> String {
/// where these tools are preinstalled.
fn tool_doctrine(mcp_on: bool) -> String {
let browser = if mcp_on {
"A Playwright MCP browser IS available — use it for JS-heavy pages, DOM/JS execution, and to PROVE client-side issues (e.g. XSS firing); capture screenshots as evidence."
"BROWSER (Playwright MCP is available — USE IT, don't rely on curl alone): for any JS-heavy / SPA / Angular / React / Vue target, DRIVE THE REAL BROWSER — navigate, wait for the app to render, read the live DOM, click through client-side routes (e.g. #/admin, #/administration, #/score-board), submit forms, and watch the NETWORK requests the app makes to discover the real REST/GraphQL API. PROVE client-side issues (XSS actually firing, DOM sinks, auth flows) in the browser and capture a screenshot as evidence. Use curl for the API/backend calls you discover; use the browser for anything the SPA renders or executes client-side."
} else {
"No browser MCP is available — use `curl` (and `wget`) for all HTTP interaction; render/inspect responses directly."
"BROWSER (no MCP — use the Playwright CLI to complement curl on JS-heavy targets): curl only sees the initial HTML (an empty SPA shell renders nothing useful). To render/interact, write a small Playwright script and run it, e.g.:\n\
`npx -y playwright@latest install chromium >/dev/null 2>&1; cat > /tmp/pw.js <<'EOF'\n\
const { chromium } = require('playwright');\n(async () => { const b = await chromium.launch(); const p = await b.newPage();\n\
p.on('request', r => console.log('REQ', r.method(), r.url()));\n await p.goto(process.argv[2], {waitUntil:'networkidle'});\n\
console.log(await p.content()); await p.screenshot({path:'/tmp/shot.png'}); await b.close(); })();\nEOF`\n\
then `node /tmp/pw.js <url>` to get the rendered DOM + the XHR/fetch URLs the app calls (that reveals the real API). Use `npx playwright screenshot <url> out.png` for quick proof. Combine with curl for the discovered API endpoints."
};
format!(
"TOOLING (authorized; best on Kali Linux or the kalilinux/kali-rolling Docker image):\n\
+15 -2
View File
@@ -157,16 +157,29 @@ pub async fn probe(target: &str) -> Probe {
}
}
// Tech hints (headers + body keywords).
let hay = format!("{} {} {} {}", p.server, p.powered_by, p.content_type, body.chars().take(20_000).collect::<String>()).to_lowercase();
let hay = format!("{} {} {} {}", p.server, p.powered_by, p.content_type, body.chars().take(30_000).collect::<String>()).to_lowercase();
for (needle, tech) in [
("wp-content", "WordPress"), ("/wp-json", "WordPress"), ("drupal", "Drupal"), ("joomla", "Joomla"),
("x-drupal", "Drupal"), ("laravel_session", "Laravel"), ("csrftoken", "Django"), ("__next", "Next.js"),
("react", "React"), ("vue", "Vue"), ("angular", "Angular"), ("nginx", "nginx"), ("apache", "Apache"),
("react", "React"), ("vue", "Vue"), ("nginx", "nginx"), ("apache", "Apache"),
("microsoft-iis", "IIS"), ("express", "Express"), ("phpsessid", "PHP"), ("jsessionid", "Java"),
("cloudflare", "Cloudflare"), ("swagger", "Swagger/OpenAPI"), ("graphql", "GraphQL"),
// SPA / framework markers (Juice Shop = Angular <app-root>).
("<app-root", "Angular"), ("ng-version", "Angular"), ("angular", "Angular"),
("data-reactroot", "React"), ("id=\"root\"", "SPA"), ("id=\"app\"", "SPA"),
("polyfills", "SPA"), ("runtime.", "SPA"),
] {
if hay.contains(needle) && !p.tech.iter().any(|t| t == tech) { p.tech.push(tech.to_string()); }
}
// Heuristic: a nearly-empty body with several linked scripts is a JS SPA
// (curl sees the shell only — the browser is required to render it).
let text_len = body.chars().filter(|c| !c.is_whitespace()).count();
if p.scripts.len() >= 2 && text_len < 3000 && !p.tech.iter().any(|t| t == "SPA") {
p.tech.push("SPA".to_string());
}
if p.tech.iter().any(|t| t == "SPA" || t == "Angular" || t == "React" || t == "Vue") {
p.notes.push("JS-rendered SPA — curl sees the shell only; use the browser (MCP/Playwright) to render, enumerate routes, and discover the API.".to_string());
}
// CORS reflection probe.
if let Ok(r2) = c.get(target).header("Origin", "https://evil.neurosploit.test").send().await {