mirror of
https://github.com/CyberSecurityUP/NeuroSploit.git
synced 2026-06-29 23:05:30 +02:00
v3.4.1: CLI-only Rust harness — interactive wizard, smart selection, tool doctrine, Typst, status
- Remove Rust web server (axum/tower-http); CLI-only binary - Verbose logging (-v) + unique run-id output folder runs/ns-<ts>-<target>/ - status.json lifecycle (running → complete) + ✓ COMPLETE summary - Interactive wizard when run with no args; detailed --help with testphp/DVWA examples + Kali tip - Tool-usage doctrine injected into recon/exploit prompts: curl + rustscan/nmap (apt/brew/cargo install guidance) + browser via Playwright when present, else curl - Smart recon-aware selection: map recon signals → agent categories, only run matching agents; heuristic fallback when LLM selection is empty - Cross-model false-positive validation: voting prefers a model other than the finder - Playwright MCP auto-provision (npx) + per-backend support (claude/codex; gemini/grok degrade) - Gemini provider (API + gemini CLI subscription) - Typst report (report.typ + compiled report.pdf) via blank structured template - Lenient finding parsing (confidence as word/number) — fixes empty-results bug - bump version 3.4.0 -> 3.4.1 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Generated
+11
-328
@@ -67,81 +67,12 @@ version = "1.0.102"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
||||
|
||||
[[package]]
|
||||
name = "async-trait"
|
||||
version = "0.1.89"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "atomic-waker"
|
||||
version = "1.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
|
||||
|
||||
[[package]]
|
||||
name = "axum"
|
||||
version = "0.7.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"axum-core",
|
||||
"base64",
|
||||
"bytes",
|
||||
"futures-util",
|
||||
"http",
|
||||
"http-body",
|
||||
"http-body-util",
|
||||
"hyper",
|
||||
"hyper-util",
|
||||
"itoa",
|
||||
"matchit",
|
||||
"memchr",
|
||||
"mime",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"rustversion",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_path_to_error",
|
||||
"serde_urlencoded",
|
||||
"sha1",
|
||||
"sync_wrapper",
|
||||
"tokio",
|
||||
"tokio-tungstenite",
|
||||
"tower",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "axum-core"
|
||||
version = "0.4.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"bytes",
|
||||
"futures-util",
|
||||
"http",
|
||||
"http-body",
|
||||
"http-body-util",
|
||||
"mime",
|
||||
"pin-project-lite",
|
||||
"rustversion",
|
||||
"sync_wrapper",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.22.1"
|
||||
@@ -154,27 +85,12 @@ version = "2.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8"
|
||||
|
||||
[[package]]
|
||||
name = "block-buffer"
|
||||
version = "0.10.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.20.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
|
||||
|
||||
[[package]]
|
||||
name = "byteorder"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.12.0"
|
||||
@@ -249,41 +165,6 @@ version = "1.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crypto-common"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "data-encoding"
|
||||
version = "2.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8"
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.10.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
||||
dependencies = [
|
||||
"block-buffer",
|
||||
"crypto-common",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "displaydoc"
|
||||
version = "0.2.6"
|
||||
@@ -408,16 +289,6 @@ dependencies = [
|
||||
"slab",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "generic-array"
|
||||
version = "0.14.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
|
||||
dependencies = [
|
||||
"typenum",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.2.17"
|
||||
@@ -440,22 +311,11 @@ dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"r-efi 5.3.0",
|
||||
"r-efi",
|
||||
"wasip2",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"r-efi 6.0.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.5.0"
|
||||
@@ -501,12 +361,6 @@ version = "1.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
|
||||
|
||||
[[package]]
|
||||
name = "httpdate"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
|
||||
|
||||
[[package]]
|
||||
name = "hyper"
|
||||
version = "1.10.1"
|
||||
@@ -520,7 +374,6 @@ dependencies = [
|
||||
"http",
|
||||
"http-body",
|
||||
"httparse",
|
||||
"httpdate",
|
||||
"itoa",
|
||||
"pin-project-lite",
|
||||
"smallvec",
|
||||
@@ -732,24 +585,12 @@ version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
|
||||
|
||||
[[package]]
|
||||
name = "matchit"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4"
|
||||
|
||||
[[package]]
|
||||
name = "mime"
|
||||
version = "0.3.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "1.2.1"
|
||||
@@ -763,23 +604,20 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "neurosploit"
|
||||
version = "3.4.0"
|
||||
version = "3.4.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
"clap",
|
||||
"futures",
|
||||
"neurosploit-harness",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
"tower-http 0.5.2",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "neurosploit-harness"
|
||||
version = "3.4.0"
|
||||
version = "3.4.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"futures",
|
||||
@@ -879,7 +717,7 @@ dependencies = [
|
||||
"rustc-hash",
|
||||
"rustls",
|
||||
"socket2",
|
||||
"thiserror 2.0.18",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"web-time",
|
||||
@@ -894,13 +732,13 @@ dependencies = [
|
||||
"bytes",
|
||||
"getrandom 0.3.4",
|
||||
"lru-slab",
|
||||
"rand 0.9.4",
|
||||
"rand",
|
||||
"ring",
|
||||
"rustc-hash",
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"slab",
|
||||
"thiserror 2.0.18",
|
||||
"thiserror",
|
||||
"tinyvec",
|
||||
"tracing",
|
||||
"web-time",
|
||||
@@ -935,41 +773,14 @@ version = "5.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
|
||||
|
||||
[[package]]
|
||||
name = "r-efi"
|
||||
version = "6.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.8.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"rand_chacha 0.3.1",
|
||||
"rand_core 0.6.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.9.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea"
|
||||
dependencies = [
|
||||
"rand_chacha 0.9.0",
|
||||
"rand_core 0.9.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_chacha"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
|
||||
dependencies = [
|
||||
"ppv-lite86",
|
||||
"rand_core 0.6.4",
|
||||
"rand_chacha",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -979,16 +790,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
|
||||
dependencies = [
|
||||
"ppv-lite86",
|
||||
"rand_core 0.9.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.6.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
||||
dependencies = [
|
||||
"getrandom 0.2.17",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1067,7 +869,7 @@ dependencies = [
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tower",
|
||||
"tower-http 0.6.11",
|
||||
"tower-http",
|
||||
"tower-service",
|
||||
"url",
|
||||
"wasm-bindgen",
|
||||
@@ -1201,17 +1003,6 @@ dependencies = [
|
||||
"zmij",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_path_to_error"
|
||||
version = "0.1.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"serde",
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_urlencoded"
|
||||
version = "0.7.1"
|
||||
@@ -1224,17 +1015,6 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha1"
|
||||
version = "0.10.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "2.0.1"
|
||||
@@ -1322,33 +1102,13 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.69"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
|
||||
dependencies = [
|
||||
"thiserror-impl 1.0.69",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "2.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
|
||||
dependencies = [
|
||||
"thiserror-impl 2.0.18",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "1.0.69"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1425,18 +1185,6 @@ dependencies = [
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-tungstenite"
|
||||
version = "0.24.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "edc5f74e248dc973e0dbb7b74c7e0d6fcc301c694ff50049504004ef4d0cdcd9"
|
||||
dependencies = [
|
||||
"futures-util",
|
||||
"log",
|
||||
"tokio",
|
||||
"tungstenite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower"
|
||||
version = "0.5.3"
|
||||
@@ -1450,23 +1198,6 @@ dependencies = [
|
||||
"tokio",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower-http"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bytes",
|
||||
"http",
|
||||
"http-body",
|
||||
"http-body-util",
|
||||
"pin-project-lite",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1505,7 +1236,6 @@ version = "0.1.44"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
|
||||
dependencies = [
|
||||
"log",
|
||||
"pin-project-lite",
|
||||
"tracing-core",
|
||||
]
|
||||
@@ -1525,30 +1255,6 @@ version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
|
||||
|
||||
[[package]]
|
||||
name = "tungstenite"
|
||||
version = "0.24.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "18e5b8366ee7a95b16d32197d0b2604b43a0be89dc5fac9f8e96ccafbaedda8a"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"bytes",
|
||||
"data-encoding",
|
||||
"http",
|
||||
"httparse",
|
||||
"log",
|
||||
"rand 0.8.6",
|
||||
"sha1",
|
||||
"thiserror 1.0.69",
|
||||
"utf-8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typenum"
|
||||
version = "1.20.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.24"
|
||||
@@ -1573,12 +1279,6 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "utf-8"
|
||||
version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
|
||||
|
||||
[[package]]
|
||||
name = "utf8_iter"
|
||||
version = "1.0.4"
|
||||
@@ -1591,23 +1291,6 @@ version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.23.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "144d6b123cef80b301b8f72a9e2ca4370ddec21950d0a103dd22c437006d2db7"
|
||||
dependencies = [
|
||||
"getrandom 0.4.3",
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||
|
||||
[[package]]
|
||||
name = "walkdir"
|
||||
version = "2.5.0"
|
||||
|
||||
@@ -3,7 +3,7 @@ members = ["crates/harness", "app"]
|
||||
resolver = "2"
|
||||
|
||||
[workspace.package]
|
||||
version = "3.4.0"
|
||||
version = "3.4.1"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
repository = "https://github.com/JoasASantos/NeuroSploit"
|
||||
|
||||
@@ -15,7 +15,4 @@ serde_json.workspace = true
|
||||
tokio.workspace = true
|
||||
anyhow.workspace = true
|
||||
futures.workspace = true
|
||||
axum = { version = "0.7", features = ["ws"] }
|
||||
tower-http = { version = "0.5", features = ["cors"] }
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
uuid = { version = "1", features = ["v4"] }
|
||||
|
||||
+138
-42
@@ -1,26 +1,39 @@
|
||||
//! NeuroSploit v3.4.0 — single binary: `serve` (web dashboard) or `run` (CLI).
|
||||
|
||||
mod web;
|
||||
//! NeuroSploit v3.4.1 — CLI: `run` (black-box) / `whitebox` (source) / `agents` / `models`.
|
||||
|
||||
use clap::{Parser, Subcommand};
|
||||
use harness::{agents, models::ModelRef, pool::ModelPool, types::RunConfig, RunOutput};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "neurosploit", version, about = "NeuroSploit v3.4.0 — multi-model autonomous pentest harness")]
|
||||
#[command(
|
||||
name = "neurosploit",
|
||||
version,
|
||||
about = "NeuroSploit v3.4.1 — multi-model autonomous pentest harness",
|
||||
long_about = "NeuroSploit v3.4.1 — a Rust multi-model harness that drives a pool of LLMs \
|
||||
(API key or local subscription: Claude/Codex/Gemini/Grok) to autonomously test a target. \
|
||||
After recon it INTELLIGENTLY selects only the agents matching the discovered surface, runs \
|
||||
them in parallel, then validates every finding by cross-model voting before reporting.\n\n\
|
||||
Run with NO arguments for an interactive wizard.\n\n\
|
||||
EXAMPLES:\n \
|
||||
# Black-box against a known test site (subscription, Opus, browser via Playwright if present)\n \
|
||||
neurosploit run http://testphp.vulnweb.com/ --subscription --model anthropic:claude-opus-4-8 --mcp -v\n\n \
|
||||
# Black-box via API keys with a multi-model voting panel\n \
|
||||
neurosploit run http://testphp.vulnweb.com/ --model anthropic:claude-opus-4-8 --model openai:gpt-5.1 --vote-n 3\n\n \
|
||||
# White-box source review of a cloned repo (DVWA)\n \
|
||||
git clone https://github.com/digininja/DVWA /tmp/DVWA\n \
|
||||
neurosploit whitebox /tmp/DVWA --subscription --model anthropic:claude-opus-4-8 -v\n\n \
|
||||
# Offline pipeline self-test (no keys/login)\n \
|
||||
neurosploit run http://testphp.vulnweb.com/ --offline\n\n\
|
||||
TIP: run inside Kali Linux (or `docker run -it kalilinux/kali-rolling`) so curl/nmap/rustscan/ffuf are available."
|
||||
)]
|
||||
struct Cli {
|
||||
#[command(subcommand)]
|
||||
cmd: Cmd,
|
||||
cmd: Option<Cmd>,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum Cmd {
|
||||
/// Start the web dashboard.
|
||||
Serve {
|
||||
#[arg(long, default_value_t = 8788)]
|
||||
port: u16,
|
||||
},
|
||||
/// Run an engagement from the CLI.
|
||||
/// Black-box: recon → intelligent agent selection → exploit → vote → report.
|
||||
Run {
|
||||
url: String,
|
||||
/// Models as provider:model (repeatable). First is primary; rest fail over + vote.
|
||||
@@ -30,20 +43,21 @@ enum Cmd {
|
||||
max_agents: usize,
|
||||
#[arg(long, default_value_t = 3)]
|
||||
vote_n: usize,
|
||||
/// Exercise the pipeline without calling any model API.
|
||||
#[arg(long)]
|
||||
offline: bool,
|
||||
/// Use local agentic CLI subscriptions (Claude Code / Codex / Grok)
|
||||
/// instead of HTTP API keys.
|
||||
/// Use local agentic CLI subscription (Claude/Codex/Gemini/Grok login).
|
||||
#[arg(long)]
|
||||
subscription: bool,
|
||||
/// Enable Playwright MCP (browser proof) on the subscription/CLI path.
|
||||
/// Enable Playwright MCP (auto-installed if missing; backends that don't
|
||||
/// support MCP fall back to their built-in tools).
|
||||
#[arg(long)]
|
||||
mcp: bool,
|
||||
/// Verbose: log each agent as it launches, recon, and votes.
|
||||
#[arg(short, long)]
|
||||
verbose: bool,
|
||||
},
|
||||
/// White-box: analyse a local repository's source code for vulnerabilities.
|
||||
Whitebox {
|
||||
/// Path to the repository to analyse.
|
||||
path: String,
|
||||
#[arg(long = "model")]
|
||||
models: Vec<String>,
|
||||
@@ -55,6 +69,8 @@ enum Cmd {
|
||||
offline: bool,
|
||||
#[arg(long)]
|
||||
subscription: bool,
|
||||
#[arg(short, long)]
|
||||
verbose: bool,
|
||||
},
|
||||
/// Show agent library counts.
|
||||
Agents,
|
||||
@@ -62,8 +78,7 @@ enum Cmd {
|
||||
Models,
|
||||
}
|
||||
|
||||
/// Locate the repo root that holds `agents_md/` (walk up from CWD, then fall
|
||||
/// back to the crate's compile-time location).
|
||||
/// Locate the repo root that holds `agents_md/`.
|
||||
fn find_base() -> PathBuf {
|
||||
if let Ok(b) = std::env::var("NEUROSPLOIT_BASE") {
|
||||
return PathBuf::from(b);
|
||||
@@ -80,7 +95,6 @@ fn find_base() -> PathBuf {
|
||||
}
|
||||
}
|
||||
}
|
||||
// crate is at <root>/neurosploit-rs/app → root is two levels up
|
||||
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.parent()
|
||||
.and_then(|p| p.parent())
|
||||
@@ -93,10 +107,18 @@ async fn main() -> anyhow::Result<()> {
|
||||
let cli = Cli::parse();
|
||||
let base = find_base();
|
||||
|
||||
match cli.cmd {
|
||||
let cmd = match cli.cmd {
|
||||
Some(c) => c,
|
||||
None => interactive(&base).await?, // no args → wizard
|
||||
};
|
||||
|
||||
match cmd {
|
||||
Cmd::Agents => {
|
||||
let lib = agents::load(&base);
|
||||
println!("{{\"vulns\":{},\"meta\":{},\"total\":{}}}", lib.vulns.len(), lib.meta.len(), lib.total());
|
||||
println!(
|
||||
"{{\"vulns\":{},\"recon\":{},\"code\":{},\"meta\":{},\"total\":{}}}",
|
||||
lib.vulns.len(), lib.recon.len(), lib.code.len(), lib.meta.len(), lib.total()
|
||||
);
|
||||
}
|
||||
Cmd::Models => {
|
||||
for p in harness::providers() {
|
||||
@@ -106,55 +128,76 @@ async fn main() -> anyhow::Result<()> {
|
||||
}
|
||||
}
|
||||
}
|
||||
Cmd::Run { url, models, max_agents, vote_n, offline, subscription, mcp } => {
|
||||
Cmd::Run { url, models, max_agents, vote_n, offline, subscription, mcp, verbose } => {
|
||||
let url = if url.starts_with("http") { url } else { format!("https://{url}") };
|
||||
let mut cfg = RunConfig::new(&url);
|
||||
cfg.max_agents = max_agents;
|
||||
cfg.vote_n = vote_n;
|
||||
cfg.offline = offline;
|
||||
cfg.subscription = subscription;
|
||||
cfg.verbose = verbose;
|
||||
if !models.is_empty() {
|
||||
cfg.models = models;
|
||||
}
|
||||
let out = run_engagement(&base, cfg, mcp, false).await?;
|
||||
print_findings(&out);
|
||||
}
|
||||
Cmd::Whitebox { path, models, max_agents, vote_n, offline, subscription } => {
|
||||
Cmd::Whitebox { path, models, max_agents, vote_n, offline, subscription, verbose } => {
|
||||
let mut cfg = RunConfig::new(&path);
|
||||
cfg.max_agents = max_agents;
|
||||
cfg.vote_n = vote_n;
|
||||
cfg.offline = offline;
|
||||
cfg.subscription = subscription;
|
||||
cfg.verbose = verbose;
|
||||
if !models.is_empty() {
|
||||
cfg.models = models;
|
||||
}
|
||||
let out = run_engagement(&base, cfg, false, true).await?;
|
||||
print_findings(&out);
|
||||
}
|
||||
Cmd::Serve { port } => {
|
||||
web::serve(base, port).await?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Shared engagement runner for CLI `run` / `whitebox`.
|
||||
/// Shared engagement runner for `run` / `whitebox`.
|
||||
async fn run_engagement(base: &Path, mut cfg: RunConfig, mcp: bool, whitebox: bool) -> anyhow::Result<RunOutput> {
|
||||
let lib = agents::load(base);
|
||||
let workdir = base.join("runs").join(format!("{}-{}", sanitize(&cfg.target), now_ts()));
|
||||
|
||||
// Unique, sortable run id → runs/<id>/
|
||||
let run_id = format!("ns-{}-{}", now_ts(), sanitize(&cfg.target));
|
||||
let workdir = base.join("runs").join(&run_id);
|
||||
std::fs::create_dir_all(&workdir).ok();
|
||||
cfg.workdir = Some(workdir.display().to_string());
|
||||
cfg.rl_path = Some(base.join("data").join("rl_state_rs.json").display().to_string());
|
||||
write_status(&workdir, "running", &format!("\"target\":{:?}", cfg.target));
|
||||
|
||||
println!(" ┌─ NeuroSploit v3.4.1");
|
||||
println!(" │ run id : {run_id}");
|
||||
println!(" │ target : {}", cfg.target);
|
||||
println!(" │ models : {}", cfg.models.join(", "));
|
||||
println!(" │ output : {}", workdir.display());
|
||||
println!(" └─ mode : {}{}{}",
|
||||
if whitebox { "white-box" } else { "black-box" },
|
||||
if cfg.subscription { " · subscription" } else { " · api" },
|
||||
if mcp { " · mcp" } else { "" });
|
||||
|
||||
// Playwright MCP: only for backends that support it; auto-provision if asked.
|
||||
let mcp_config = if mcp && cfg.subscription {
|
||||
match harness::write_mcp_config(&workdir) {
|
||||
Ok(p) => {
|
||||
println!(" [*] Playwright MCP enabled → {}", p.display());
|
||||
Some(p.display().to_string())
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!(" [!] MCP config failed: {e}");
|
||||
None
|
||||
let providers: Vec<String> = cfg.models.iter().map(|m| ModelRef::parse(m).provider).collect();
|
||||
if providers.iter().any(|p| harness::mcp_supported(p)) {
|
||||
match harness::ensure_playwright_mcp() {
|
||||
Ok(()) => match harness::write_mcp_config(&workdir) {
|
||||
Ok(p) => {
|
||||
println!(" [*] Playwright MCP ready → {}", p.display());
|
||||
Some(p.display().to_string())
|
||||
}
|
||||
Err(e) => { eprintln!(" [!] MCP config failed: {e}"); None }
|
||||
},
|
||||
Err(e) => { eprintln!(" [!] Playwright MCP unavailable ({e}); using built-in tools"); None }
|
||||
}
|
||||
} else {
|
||||
eprintln!(" [!] selected backend(s) don't support MCP; using built-in tools");
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
@@ -175,6 +218,14 @@ async fn run_engagement(base: &Path, mut cfg: RunConfig, mcp: bool, whitebox: bo
|
||||
harness::run(cfg, &lib, &pool, tx).await
|
||||
};
|
||||
let _ = printer.await;
|
||||
|
||||
// Final report via Typst (PDF if the `typst` binary is present) + HTML/MD already written.
|
||||
match harness::report::typst_report(&out.target, &out.findings, &workdir) {
|
||||
Ok(p) => println!(" [*] report → {}", p.display()),
|
||||
Err(e) => eprintln!(" [!] typst report skipped: {e}"),
|
||||
}
|
||||
write_status(&workdir, "complete", &format!("\"findings\":{},\"agents_ran\":{}", out.findings.len(), out.agents_ran.len()));
|
||||
println!(" ✓ COMPLETE — {} validated finding(s) · status: {}/status.json", out.findings.len(), workdir.display());
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
@@ -189,16 +240,61 @@ fn print_findings(out: &RunOutput) {
|
||||
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();
|
||||
o.truncate(50);
|
||||
o.truncate(40);
|
||||
let o = o.trim_matches('_').to_string();
|
||||
if o.is_empty() {
|
||||
"target".into()
|
||||
} else {
|
||||
o
|
||||
}
|
||||
if o.is_empty() { "target".into() } else { o }
|
||||
}
|
||||
|
||||
fn now_ts() -> u64 {
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0)
|
||||
}
|
||||
|
||||
fn write_status(workdir: &Path, state: &str, extra: &str) {
|
||||
let p = workdir.join("status.json");
|
||||
let _ = std::fs::write(&p, format!("{{\"state\":\"{state}\",\"ts\":{}{}}}", now_ts(),
|
||||
if extra.is_empty() { String::new() } else { format!(",{extra}") }));
|
||||
}
|
||||
|
||||
fn prompt(q: &str, default: &str) -> String {
|
||||
use std::io::Write;
|
||||
print!(" {q}{}: ", if default.is_empty() { String::new() } else { format!(" [{default}]") });
|
||||
std::io::stdout().flush().ok();
|
||||
let mut s = String::new();
|
||||
std::io::stdin().read_line(&mut s).ok();
|
||||
let s = s.trim().to_string();
|
||||
if s.is_empty() { default.to_string() } else { s }
|
||||
}
|
||||
|
||||
/// Interactive wizard launched when `neurosploit` is run with no subcommand.
|
||||
async fn interactive(base: &Path) -> anyhow::Result<Cmd> {
|
||||
let lib = agents::load(base);
|
||||
let backends = harness::installed_cli_backends();
|
||||
println!("\n ┌────────────────────────────────────────────┐");
|
||||
println!(" │ NeuroSploit v3.4.1 — interactive │");
|
||||
println!(" └────────────────────────────────────────────┘");
|
||||
println!(" agents: {} · detected CLI logins: {}\n",
|
||||
lib.total(), if backends.is_empty() { "none".into() } else { backends.join(", ") });
|
||||
|
||||
let mode = prompt("Mode — (b)lack-box URL or (w)hite-box repo?", "b").to_lowercase();
|
||||
let whitebox = mode.starts_with('w');
|
||||
let target = if whitebox {
|
||||
prompt("Repository path", "/tmp/DVWA")
|
||||
} else {
|
||||
prompt("Target URL", "http://testphp.vulnweb.com/")
|
||||
};
|
||||
let model = prompt("Model (provider:model)", "anthropic:claude-opus-4-8");
|
||||
let sub = prompt("Use subscription login (no API key)? (y/n)", "y").to_lowercase().starts_with('y');
|
||||
let mcp = if whitebox { false } else {
|
||||
prompt("Use Playwright MCP browser if available? (y/n)", "y").to_lowercase().starts_with('y')
|
||||
};
|
||||
let max_agents: usize = prompt("Max agents (0 = all matching)", "5").parse().unwrap_or(5);
|
||||
let vote_n: usize = prompt("Validator votes (N)", "3").parse().unwrap_or(3);
|
||||
|
||||
let models = vec![model];
|
||||
Ok(if whitebox {
|
||||
Cmd::Whitebox { path: target, models, max_agents, vote_n, offline: false, subscription: sub, verbose: true }
|
||||
} else {
|
||||
Cmd::Run { url: target, models, max_agents, vote_n, offline: false, subscription: sub, mcp, verbose: true }
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,236 +0,0 @@
|
||||
//! Axum web dashboard for the v3.4.0 harness.
|
||||
|
||||
use axum::{
|
||||
extract::{Path, State},
|
||||
response::Html,
|
||||
routing::{get, post},
|
||||
Json, Router,
|
||||
};
|
||||
use harness::{agents, models::ModelRef, pool::ModelPool, report, types::RunConfig};
|
||||
use serde_json::{json, Value};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
path::PathBuf,
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
struct RunState {
|
||||
log: Vec<String>,
|
||||
done: bool,
|
||||
result: Option<Value>,
|
||||
report: Option<String>,
|
||||
}
|
||||
|
||||
pub struct AppState {
|
||||
base: PathBuf,
|
||||
runs: Mutex<HashMap<String, RunState>>,
|
||||
}
|
||||
|
||||
pub async fn serve(base: PathBuf, port: u16) -> anyhow::Result<()> {
|
||||
let state = Arc::new(AppState { base, runs: Mutex::new(HashMap::new()) });
|
||||
let app = Router::new()
|
||||
.route("/", get(index))
|
||||
.route("/api/info", get(info))
|
||||
.route("/api/agents", get(agents_list))
|
||||
.route("/api/models", get(models_list))
|
||||
.route("/api/run", post(run))
|
||||
.route("/api/status/:id", get(status))
|
||||
.route("/report/:id", get(report_html))
|
||||
.with_state(state);
|
||||
|
||||
let addr = format!("127.0.0.1:{port}");
|
||||
println!("NeuroSploit v3.4.0 dashboard → http://{addr}");
|
||||
let listener = tokio::net::TcpListener::bind(&addr).await?;
|
||||
axum::serve(listener, app).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn index() -> Html<&'static str> {
|
||||
Html(include_str!("../web/index.html"))
|
||||
}
|
||||
|
||||
async fn info(State(st): State<Arc<AppState>>) -> Json<Value> {
|
||||
let lib = agents::load(&st.base);
|
||||
let provs: Vec<Value> = harness::providers()
|
||||
.iter()
|
||||
.map(|p| json!({"key": p.key, "label": p.label, "kind": p.kind, "models": p.models}))
|
||||
.collect();
|
||||
Json(json!({
|
||||
"version": "3.4.0",
|
||||
"agents": {"vulns": lib.vulns.len(), "meta": lib.meta.len(), "recon": lib.recon.len(), "code": lib.code.len(), "total": lib.total()},
|
||||
"providers": provs,
|
||||
"cli_backends": harness::installed_cli_backends(),
|
||||
}))
|
||||
}
|
||||
|
||||
async fn agents_list(State(st): State<Arc<AppState>>) -> Json<Value> {
|
||||
let lib = agents::load(&st.base);
|
||||
let v: Vec<Value> = lib
|
||||
.vulns
|
||||
.iter()
|
||||
.chain(lib.recon.iter())
|
||||
.chain(lib.code.iter())
|
||||
.chain(lib.meta.iter())
|
||||
.map(|a| json!({"name": a.name, "title": a.title, "cwe": a.cwe, "kind": a.kind}))
|
||||
.collect();
|
||||
Json(json!({ "agents": v }))
|
||||
}
|
||||
|
||||
async fn models_list() -> Json<Value> {
|
||||
let provs: Vec<Value> = harness::providers()
|
||||
.iter()
|
||||
.map(|p| json!({"key": p.key, "label": p.label, "kind": p.kind, "models": p.models}))
|
||||
.collect();
|
||||
Json(json!({ "providers": provs }))
|
||||
}
|
||||
|
||||
fn norm(u: &str) -> String {
|
||||
if u.starts_with("http") {
|
||||
u.to_string()
|
||||
} else {
|
||||
format!("https://{u}")
|
||||
}
|
||||
}
|
||||
|
||||
async fn run(State(st): State<Arc<AppState>>, Json(body): Json<Value>) -> Json<Value> {
|
||||
let id = uuid::Uuid::new_v4().to_string();
|
||||
st.runs
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert(id.clone(), RunState { log: vec![], done: false, result: None, report: None });
|
||||
|
||||
let st2 = st.clone();
|
||||
let id2 = id.clone();
|
||||
tokio::spawn(async move {
|
||||
let base = st2.base.clone();
|
||||
|
||||
let mut targets: Vec<String> = Vec::new();
|
||||
if let Some(arr) = body.get("targets").and_then(|v| v.as_array()) {
|
||||
for t in arr {
|
||||
if let Some(s) = t.as_str() {
|
||||
if !s.trim().is_empty() {
|
||||
targets.push(norm(s.trim()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if targets.is_empty() {
|
||||
if let Some(u) = body.get("url").and_then(|v| v.as_str()) {
|
||||
if !u.trim().is_empty() {
|
||||
targets.push(norm(u.trim()));
|
||||
}
|
||||
}
|
||||
}
|
||||
let models: Vec<String> = body
|
||||
.get("models")
|
||||
.and_then(|v| v.as_array())
|
||||
.map(|a| a.iter().filter_map(|x| x.as_str().map(|s| s.to_string())).collect())
|
||||
.unwrap_or_default();
|
||||
let vote_n = body.get("vote_n").and_then(|v| v.as_u64()).unwrap_or(3) as usize;
|
||||
let max_agents = body.get("max_agents").and_then(|v| v.as_u64()).unwrap_or(0) as usize;
|
||||
let offline = body.get("offline").and_then(|v| v.as_bool()).unwrap_or(false);
|
||||
let subscription = body.get("subscription").and_then(|v| v.as_bool()).unwrap_or(false);
|
||||
let mcp = body.get("mcp").and_then(|v| v.as_bool()).unwrap_or(false);
|
||||
let mode = body.get("mode").and_then(|v| v.as_str()).unwrap_or("web").to_string();
|
||||
// Whitebox uses a repo path instead of URLs.
|
||||
if mode == "whitebox" {
|
||||
if let Some(p) = body.get("repo").and_then(|v| v.as_str()) {
|
||||
if !p.trim().is_empty() {
|
||||
targets = vec![p.trim().to_string()];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let lib = agents::load(&base);
|
||||
let refs: Vec<ModelRef> = if models.is_empty() {
|
||||
vec![ModelRef::parse("anthropic:claude-opus-4-8")]
|
||||
} else {
|
||||
models.iter().map(|s| ModelRef::parse(s)).collect()
|
||||
};
|
||||
let mcp_config = if mcp && subscription {
|
||||
harness::write_mcp_config(&base.join("runs").join("_mcp")).ok().map(|p| p.display().to_string())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let pool = ModelPool::with_auth(refs, 8, subscription, mcp_config);
|
||||
let rl_path = base.join("data").join("rl_state_rs.json").display().to_string();
|
||||
|
||||
let (tx, mut rx) = tokio::sync::mpsc::channel::<String>(256);
|
||||
let stf = st2.clone();
|
||||
let idf = id2.clone();
|
||||
let fwd = tokio::spawn(async move {
|
||||
while let Some(line) = rx.recv().await {
|
||||
if let Ok(mut g) = stf.runs.lock() {
|
||||
if let Some(r) = g.get_mut(&idf) {
|
||||
r.log.push(line);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let mut all_findings = Vec::new();
|
||||
let mut all_ran = Vec::new();
|
||||
for url in &targets {
|
||||
let mut cfg = RunConfig::new(url);
|
||||
cfg.models = if models.is_empty() {
|
||||
vec!["anthropic:claude-opus-4-8".into()]
|
||||
} else {
|
||||
models.clone()
|
||||
};
|
||||
cfg.vote_n = vote_n;
|
||||
cfg.max_agents = max_agents;
|
||||
cfg.offline = offline;
|
||||
cfg.subscription = subscription;
|
||||
cfg.rl_path = Some(rl_path.clone());
|
||||
cfg.workdir = Some(base.join("runs").join(format!("{}-{}", slug(url), now_ts())).display().to_string());
|
||||
let _ = tx.send(format!("=== {}: {url} ===", if mode == "whitebox" { "whitebox repo" } else { "target" })).await;
|
||||
let out = if mode == "whitebox" {
|
||||
harness::run_whitebox(cfg, &lib, &pool, tx.clone()).await
|
||||
} else {
|
||||
harness::run(cfg, &lib, &pool, tx.clone()).await
|
||||
};
|
||||
all_findings.extend(out.findings);
|
||||
all_ran.extend(out.agents_ran);
|
||||
}
|
||||
drop(tx);
|
||||
let _ = fwd.await;
|
||||
|
||||
let report_html = report::html(targets.first().map(|s| s.as_str()).unwrap_or(""), &all_findings);
|
||||
let result = json!({"findings": all_findings, "agents_ran": all_ran, "targets": targets});
|
||||
if let Ok(mut g) = st2.runs.lock() {
|
||||
if let Some(r) = g.get_mut(&id2) {
|
||||
r.result = Some(result);
|
||||
r.report = Some(report_html);
|
||||
r.done = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Json(json!({ "run_id": id }))
|
||||
}
|
||||
|
||||
async fn status(Path(id): Path<String>, State(st): State<Arc<AppState>>) -> Json<Value> {
|
||||
let g = st.runs.lock().unwrap();
|
||||
match g.get(&id) {
|
||||
Some(r) => Json(json!({"log": r.log, "done": r.done, "result": r.result, "has_report": r.report.is_some()})),
|
||||
None => Json(json!({"error": "unknown run"})),
|
||||
}
|
||||
}
|
||||
|
||||
async fn report_html(Path(id): Path<String>, State(st): State<Arc<AppState>>) -> Html<String> {
|
||||
let g = st.runs.lock().unwrap();
|
||||
Html(g.get(&id).and_then(|r| r.report.clone()).unwrap_or_else(|| "<h1>no report</h1>".into()))
|
||||
}
|
||||
|
||||
fn slug(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();
|
||||
o.truncate(50);
|
||||
let o = o.trim_matches('_').to_string();
|
||||
if o.is_empty() { "target".into() } else { o }
|
||||
}
|
||||
|
||||
fn now_ts() -> u64 {
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0)
|
||||
}
|
||||
@@ -1,327 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
<title>NeuroSploit</title>
|
||||
<style>
|
||||
:root{
|
||||
--bg:#0a0b0f;--bg2:#0e1016;--panel:#13151d;--panel2:#191c26;--panel3:#1e2230;
|
||||
--line:#242838;--line2:#2e3346;--text:#eef1f6;--muted:#9aa1b4;--dim:#6b7186;
|
||||
--accent:#7c5cff;--accent2:#a855f7;--cy:#2dd4bf;--ok:#34d399;--warn:#fbbf24;
|
||||
--crit:#f5556d;--high:#fb923c;--med:#fbbf24;--low:#38bdf8;--info:#94a3b8;
|
||||
--radius:14px;--shadow:0 16px 50px rgba(0,0,0,.5);
|
||||
}
|
||||
*{box-sizing:border-box;margin:0;padding:0}
|
||||
html{scroll-behavior:smooth}
|
||||
body{background:var(--bg);color:var(--text);font:14px/1.55 ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;
|
||||
-webkit-font-smoothing:antialiased}
|
||||
::-webkit-scrollbar{width:9px;height:9px}::-webkit-scrollbar-thumb{background:var(--line2);border-radius:9px}
|
||||
.app{display:grid;grid-template-columns:240px 1fr;min-height:100vh}
|
||||
/* ===== sidebar ===== */
|
||||
.side{background:linear-gradient(180deg,var(--bg2),#08090d);border-right:1px solid var(--line);
|
||||
padding:22px 16px;display:flex;flex-direction:column;gap:3px;position:sticky;top:0;height:100vh}
|
||||
.brand{display:flex;align-items:center;gap:11px;margin:2px 6px 24px}
|
||||
.logo{width:38px;height:38px;border-radius:11px;background:linear-gradient(135deg,var(--accent),var(--accent2));
|
||||
display:grid;place-items:center;font-weight:800;font-size:19px;color:#fff;box-shadow:0 8px 26px rgba(124,92,255,.45)}
|
||||
.brand .nm{font-size:16px;font-weight:700;letter-spacing:.2px}
|
||||
.brand .vr{font-size:10.5px;color:var(--muted);margin-top:-1px}
|
||||
.badge-rs{font-size:8.5px;font-weight:800;color:#1a1209;background:#e6b673;border-radius:4px;padding:1px 5px;margin-left:6px;vertical-align:middle}
|
||||
.navlabel{font-size:10px;text-transform:uppercase;letter-spacing:1px;color:var(--dim);margin:16px 10px 6px}
|
||||
.nav{display:flex;align-items:center;gap:11px;padding:10px 12px;border-radius:10px;color:var(--muted);
|
||||
cursor:pointer;font-size:13.5px;font-weight:500;transition:.13s}
|
||||
.nav .ic{width:18px;height:18px;opacity:.8;flex:none}
|
||||
.nav:hover{background:var(--panel);color:var(--text)}
|
||||
.nav.on{background:linear-gradient(135deg,rgba(124,92,255,.2),rgba(168,85,247,.08));color:#fff;
|
||||
box-shadow:inset 0 0 0 1px rgba(124,92,255,.32)}
|
||||
.nav.on .ic{opacity:1}
|
||||
.side .foot{margin-top:auto;border-top:1px solid var(--line);padding-top:12px;font-size:11px;color:var(--dim)}
|
||||
.stat{display:flex;justify-content:space-between;padding:3px 8px}.stat b{color:var(--text)}
|
||||
/* ===== main ===== */
|
||||
main{padding:0;overflow:hidden}
|
||||
.topbar{display:flex;align-items:center;justify-content:space-between;padding:18px 32px;border-bottom:1px solid var(--line);
|
||||
background:rgba(14,16,22,.6);backdrop-filter:blur(8px);position:sticky;top:0;z-index:5}
|
||||
.topbar h1{font-size:18px;font-weight:650}.topbar .crumb{color:var(--dim);font-size:12.5px;margin-top:2px}
|
||||
.chipline{display:flex;gap:8px}
|
||||
.mono{font-family:ui-monospace,"SF Mono",Menlo,monospace}
|
||||
.wrap{padding:28px 32px;max-width:1180px}
|
||||
.view{display:none;animation:fade .25s ease}.view.on{display:block}
|
||||
@keyframes fade{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:none}}
|
||||
.grid2{display:grid;grid-template-columns:1.55fr 1fr;gap:20px;align-items:start}
|
||||
@media(max-width:980px){.app{grid-template-columns:1fr}.side{position:static;height:auto;flex-direction:row;flex-wrap:wrap}
|
||||
.grid2{grid-template-columns:1fr}.navlabel{display:none}.side .foot{display:none}}
|
||||
.card{background:var(--panel);border:1px solid var(--line);border-radius:var(--radius);padding:22px;margin-bottom:20px}
|
||||
.card h2{font-size:14px;font-weight:650;margin-bottom:4px;display:flex;align-items:center;gap:8px}
|
||||
.card .desc{color:var(--muted);font-size:12.5px;margin-bottom:16px}
|
||||
label{display:block;font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:.5px;margin:0 0 7px;font-weight:600}
|
||||
.field{margin-bottom:16px}
|
||||
input,select,textarea{width:100%;background:var(--panel2);border:1px solid var(--line2);color:var(--text);
|
||||
border-radius:10px;padding:11px 13px;font-size:13.5px;outline:none;font-family:inherit;transition:.14s}
|
||||
textarea{resize:vertical;min-height:76px;font-family:ui-monospace,Menlo,monospace;font-size:12.5px}
|
||||
input:focus,select:focus,textarea:focus{border-color:var(--accent);box-shadow:0 0 0 3px rgba(124,92,255,.13)}
|
||||
.row{display:flex;gap:13px;flex-wrap:wrap}.row>*{flex:1;min-width:120px}
|
||||
/* segmented mode switch */
|
||||
.seg{display:inline-flex;background:var(--panel2);border:1px solid var(--line2);border-radius:10px;padding:3px;gap:3px;margin-bottom:18px}
|
||||
.seg button{background:transparent;border:0;color:var(--muted);padding:8px 16px;border-radius:8px;font-size:13px;font-weight:600;cursor:pointer;transition:.13s}
|
||||
.seg button.on{background:linear-gradient(135deg,var(--accent),var(--accent2));color:#fff}
|
||||
.toggles{display:flex;gap:10px;flex-wrap:wrap;margin:4px 0 18px}
|
||||
.tg{display:flex;align-items:center;gap:9px;background:var(--panel2);border:1px solid var(--line2);border-radius:10px;
|
||||
padding:10px 13px;cursor:pointer;font-size:12.5px;user-select:none;transition:.13s}
|
||||
.tg.on{border-color:var(--accent);background:rgba(124,92,255,.1);color:#fff}
|
||||
.tg input{accent-color:var(--accent);width:15px;height:15px}
|
||||
.btns{display:flex;gap:11px}
|
||||
button.act{border:0;border-radius:11px;padding:12px 18px;font-size:14px;font-weight:650;cursor:pointer;transition:.13s}
|
||||
.primary{background:linear-gradient(135deg,var(--accent),var(--accent2));color:#fff;flex:1;box-shadow:0 8px 22px rgba(124,92,255,.3)}
|
||||
.primary:hover{filter:brightness(1.09)}
|
||||
.ghost{background:var(--panel2);color:var(--text);border:1px solid var(--line2)}.ghost:hover{border-color:var(--accent)}
|
||||
button:disabled{opacity:.5;cursor:not-allowed}
|
||||
.chip{display:inline-flex;align-items:center;gap:6px;background:var(--panel2);border:1px solid var(--line2);
|
||||
border-radius:999px;padding:4px 11px;font-size:11.5px;color:var(--muted)}
|
||||
.chip b{color:var(--text)}.dot{width:7px;height:7px;border-radius:50%;background:var(--ok);box-shadow:0 0 8px var(--ok)}
|
||||
/* model panel */
|
||||
.mpanel{max-height:230px;overflow:auto;border:1px solid var(--line2);border-radius:10px;padding:5px;background:var(--bg2)}
|
||||
.mopt{display:flex;align-items:center;gap:9px;padding:7px 9px;border-radius:8px;font-size:12.5px;cursor:pointer}
|
||||
.mopt:hover{background:var(--panel2)}.mopt input{accent-color:var(--accent)}
|
||||
.tag{font-size:9px;font-weight:700;padding:2px 6px;border-radius:5px;text-transform:uppercase;letter-spacing:.4px}
|
||||
.tag.cli{background:rgba(124,92,255,.2);color:#c4b5fd}.tag.api{background:rgba(45,212,191,.15);color:var(--cy)}
|
||||
.tag.meta{background:rgba(45,212,191,.15);color:var(--cy)}.tag.recon{background:rgba(56,189,248,.16);color:var(--low)}
|
||||
.tag.code{background:rgba(251,146,60,.16);color:var(--high)}.tag.vuln{background:rgba(245,85,109,.15);color:var(--crit)}
|
||||
/* console */
|
||||
.term{background:#070810;border:1px solid var(--line2);border-radius:12px;padding:14px 16px;
|
||||
font:12px/1.65 ui-monospace,Menlo,monospace;max-height:340px;overflow:auto;white-space:pre-wrap;color:#c3cad8;min-height:120px}
|
||||
.term .h{color:var(--accent2);font-weight:600}.term .ok{color:var(--ok)}.term .e{color:var(--crit)}
|
||||
.term .v{color:var(--cy)}.term .s{color:var(--warn)}
|
||||
.term .empty{color:var(--dim)}
|
||||
/* findings */
|
||||
.sevbar{display:flex;gap:9px;flex-wrap:wrap;margin-bottom:14px}
|
||||
.scount{display:flex;flex-direction:column;align-items:center;background:var(--panel2);border:1px solid var(--line2);
|
||||
border-radius:10px;padding:9px 16px;min-width:74px}
|
||||
.scount .n{font-size:20px;font-weight:750}.scount .l{font-size:10px;text-transform:uppercase;letter-spacing:.5px;color:var(--muted)}
|
||||
.scount.Critical .n{color:var(--crit)}.scount.High .n{color:var(--high)}.scount.Medium .n{color:var(--med)}
|
||||
.scount.Low .n{color:var(--low)}.scount.Info .n{color:var(--info)}
|
||||
.find{border:1px solid var(--line2);border-left-width:3px;border-radius:11px;padding:15px 17px;margin:11px 0;background:var(--panel2)}
|
||||
.find.Critical{border-left-color:var(--crit)}.find.High{border-left-color:var(--high)}.find.Medium{border-left-color:var(--med)}
|
||||
.find.Low{border-left-color:var(--low)}.find.Info{border-left-color:var(--info)}
|
||||
.find h4{font-size:14px;margin-bottom:5px;display:flex;align-items:center;gap:9px}
|
||||
.sev{font-size:10px;font-weight:700;padding:3px 8px;border-radius:6px;text-transform:uppercase}
|
||||
.sev.Critical{background:rgba(245,85,109,.18);color:var(--crit)}.sev.High{background:rgba(251,146,60,.18);color:var(--high)}
|
||||
.sev.Medium{background:rgba(251,191,36,.16);color:var(--med)}.sev.Low{background:rgba(56,189,248,.16);color:var(--low)}
|
||||
.sev.Info{background:rgba(148,163,184,.16);color:var(--info)}
|
||||
.find .m{color:var(--muted);font-size:11.5px;margin-bottom:6px}
|
||||
.find pre{background:#070810;border:1px solid var(--line);border-radius:8px;padding:10px;font-size:11.5px;overflow:auto;margin:7px 0}
|
||||
.empty-state{text-align:center;color:var(--dim);padding:36px 10px;font-size:13px}
|
||||
/* agents list */
|
||||
.toolbar{display:flex;gap:10px;margin-bottom:14px;flex-wrap:wrap}.toolbar input{flex:1;min-width:160px}
|
||||
.fbtn{background:var(--panel2);border:1px solid var(--line2);color:var(--muted);border-radius:8px;padding:8px 13px;font-size:12px;cursor:pointer;font-weight:600}
|
||||
.fbtn.on{border-color:var(--accent);color:#fff;background:rgba(124,92,255,.12)}
|
||||
.alist{max-height:560px;overflow:auto;border:1px solid var(--line2);border-radius:11px}
|
||||
.arow{display:flex;gap:11px;padding:10px 14px;border-bottom:1px solid var(--line);font-size:13px;align-items:center}
|
||||
.arow:last-child{border:0}.arow:hover{background:var(--panel2)}.arow code{color:var(--accent2);font-size:12.5px}
|
||||
.arow .t{color:var(--muted);margin-left:auto;font-size:11.5px;text-align:right}
|
||||
.muted{color:var(--muted)}.dim{color:var(--dim)}a{color:var(--accent2);text-decoration:none}a:hover{text-decoration:underline}
|
||||
.dl{display:inline-flex;gap:8px;background:var(--panel2);border:1px solid var(--line2);border-radius:9px;padding:9px 14px;margin:0 9px 9px 0;color:var(--text);font-size:12.5px}
|
||||
.dl:hover{border-color:var(--accent);text-decoration:none}
|
||||
iframe{width:100%;height:560px;border:1px solid var(--line2);border-radius:11px;background:#fff;margin-top:12px}
|
||||
.mcard{padding:14px 16px;border:1px solid var(--line2);border-radius:11px;margin-bottom:11px;background:var(--panel2)}
|
||||
.mcard h3{font-size:13.5px;margin-bottom:7px;display:flex;align-items:center;gap:8px}
|
||||
.keyrow{display:flex;align-items:center;gap:8px;margin:5px 0;font-size:12px;color:var(--muted)}
|
||||
.progress{height:3px;background:var(--line);border-radius:3px;overflow:hidden;margin-top:14px;display:none}
|
||||
.progress.on{display:block}.progress .bar{height:100%;width:30%;background:linear-gradient(90deg,var(--accent),var(--accent2));
|
||||
border-radius:3px;animation:slide 1.1s infinite ease-in-out}
|
||||
@keyframes slide{0%{margin-left:-30%}100%{margin-left:100%}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="app">
|
||||
<aside class="side">
|
||||
<div class="brand"><div class="logo">N</div><div><div class="nm">NeuroSploit<span class="badge-rs">RUST</span></div><div class="vr">v3.4.0 · Multi-Model Harness</div></div></div>
|
||||
<div class="navlabel">Operate</div>
|
||||
<div class="nav on" data-v="run"><svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="5 3 19 12 5 21 5 3"/></svg> Engagement</div>
|
||||
<div class="nav" data-v="findings"><svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg> Findings</div>
|
||||
<div class="nav" data-v="report"><svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg> Report</div>
|
||||
<div class="navlabel">Library</div>
|
||||
<div class="nav" data-v="agents"><svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M12 1v6m0 6v6m11-7h-6m-6 0H1"/></svg> Agents</div>
|
||||
<div class="nav" data-v="models"><svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M9 9h6v6H9z"/></svg> Models</div>
|
||||
<div class="foot">
|
||||
<div class="stat"><span>Agents</span><b id="sf-agents">—</b></div>
|
||||
<div class="stat"><span>Providers</span><b id="sf-prov">—</b></div>
|
||||
<div class="stat"><span>Backends</span><b id="sf-cli">—</b></div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<main>
|
||||
<div class="topbar">
|
||||
<div><h1 id="bar-title">Engagement</h1><div class="crumb" id="bar-crumb">Configure and launch an autonomous run</div></div>
|
||||
<div class="chipline"><span class="chip"><span class="dot"></span> <b>online</b></span><span class="chip mono" id="chip-models">—</span></div>
|
||||
</div>
|
||||
|
||||
<!-- ENGAGEMENT -->
|
||||
<section class="view on" id="v-run"><div class="wrap">
|
||||
<div class="seg" id="modeSeg">
|
||||
<button class="on" data-m="web">🌐 Black-box (URL)</button>
|
||||
<button data-m="whitebox">📦 White-box (repo)</button>
|
||||
</div>
|
||||
<div class="grid2">
|
||||
<div>
|
||||
<div class="card">
|
||||
<h2>Target</h2>
|
||||
<div class="desc" id="targetDesc">One or more URLs — the harness recons each, then intelligently selects matching agents.</div>
|
||||
<div class="field" id="urlField"><label>Targets (one per line)</label><textarea id="targets" placeholder="https://target-one.example https://target-two.example"></textarea></div>
|
||||
<div class="field" id="repoField" style="display:none"><label>Repository path (local)</label><input id="repo" placeholder="/path/to/repo"/></div>
|
||||
<div class="row">
|
||||
<div class="field"><label>Validator votes (N)</label><input id="voten" type="number" value="3" min="1" max="9"/></div>
|
||||
<div class="field"><label>Max agents (0 = all)</label><input id="maxa" type="number" value="0" min="0"/></div>
|
||||
</div>
|
||||
<div class="toggles">
|
||||
<label class="tg on" id="tg-off"><input type="checkbox" id="offline" checked/> Offline self-test</label>
|
||||
<label class="tg" id="tg-sub"><input type="checkbox" id="subscription"/> Subscription (Claude/Codex/Gemini login)</label>
|
||||
<label class="tg" id="tg-mcp"><input type="checkbox" id="mcp"/> Playwright MCP</label>
|
||||
</div>
|
||||
<div class="btns"><button class="act primary" id="go">▶ Launch engagement</button></div>
|
||||
<div class="progress" id="prog"><div class="bar"></div></div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="card">
|
||||
<h2>Model panel</h2>
|
||||
<div class="desc">1st = primary · others fail over & form the validator jury.</div>
|
||||
<div class="mpanel" id="mpanel"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h2>Live execution</h2>
|
||||
<div class="desc">Recon → intelligent agent selection → parallel exploitation → N-model voting → report. Artifacts saved to <span class="mono">runs/</span>.</div>
|
||||
<div class="term" id="term"><span class="empty">— idle. Launch an engagement to stream activity. —</span></div>
|
||||
</div>
|
||||
</div></section>
|
||||
|
||||
<!-- FINDINGS -->
|
||||
<section class="view" id="v-findings"><div class="wrap">
|
||||
<div class="card">
|
||||
<h2>Validated findings</h2>
|
||||
<div class="desc">Only findings confirmed by multi-model adversarial voting appear here.</div>
|
||||
<div class="sevbar" id="sevbar"></div>
|
||||
<div id="findings"><div class="empty-state">No findings yet — run an engagement.</div></div>
|
||||
</div>
|
||||
</div></section>
|
||||
|
||||
<!-- REPORT -->
|
||||
<section class="view" id="v-report"><div class="wrap">
|
||||
<div class="card">
|
||||
<h2>Report</h2>
|
||||
<div class="desc">HTML report + JSON/MD artifacts for reuse by other tools/AIs.</div>
|
||||
<div id="reportcard"><div class="empty-state">Run an engagement to generate a report.</div></div>
|
||||
</div>
|
||||
</div></section>
|
||||
|
||||
<!-- AGENTS -->
|
||||
<section class="view" id="v-agents"><div class="wrap">
|
||||
<div class="card">
|
||||
<h2>Agent library</h2>
|
||||
<div class="desc" id="agentsub">…</div>
|
||||
<div class="toolbar">
|
||||
<input id="asearch" placeholder="🔎 filter by name / title / CWE"/>
|
||||
<button class="fbtn on" data-k="all">All</button>
|
||||
<button class="fbtn" data-k="vuln">Vuln</button>
|
||||
<button class="fbtn" data-k="recon">Recon</button>
|
||||
<button class="fbtn" data-k="code">Code</button>
|
||||
<button class="fbtn" data-k="meta">Meta</button>
|
||||
</div>
|
||||
<div class="alist" id="alist"></div>
|
||||
</div>
|
||||
</div></section>
|
||||
|
||||
<!-- MODELS -->
|
||||
<section class="view" id="v-models"><div class="wrap">
|
||||
<div class="card">
|
||||
<h2>Providers & models</h2>
|
||||
<div class="desc">Use via <b>API</b> key or <b>subscription</b> (local CLI login). CLI-capable providers are tagged.</div>
|
||||
<div id="modelcard"></div>
|
||||
</div>
|
||||
</div></section>
|
||||
</main>
|
||||
</div>
|
||||
<script>
|
||||
const $=s=>document.querySelector(s),$$=s=>[...document.querySelectorAll(s)];
|
||||
let INFO=null,AGENTS=[],lastRun=null,mode='web',filter='all';
|
||||
|
||||
const TITLES={run:['Engagement','Configure and launch an autonomous run'],findings:['Findings','Validated, multi-model-confirmed results'],
|
||||
report:['Report','Generated deliverables & artifacts'],agents:['Agents','The markdown agent library'],models:['Models','Providers, models & auth']};
|
||||
$$('.nav').forEach(n=>n.onclick=()=>{$$('.nav').forEach(x=>x.classList.remove('on'));n.classList.add('on');
|
||||
$$('.view').forEach(v=>v.classList.remove('on'));$('#v-'+n.dataset.v).classList.add('on');
|
||||
const t=TITLES[n.dataset.v];$('#bar-title').textContent=t[0];$('#bar-crumb').textContent=t[1];});
|
||||
|
||||
// mode switch
|
||||
$$('#modeSeg button').forEach(b=>b.onclick=()=>{$$('#modeSeg button').forEach(x=>x.classList.remove('on'));b.classList.add('on');
|
||||
mode=b.dataset.m;const wb=mode==='whitebox';
|
||||
$('#urlField').style.display=wb?'none':'';$('#repoField').style.display=wb?'':'none';
|
||||
$('#targetDesc').textContent=wb?'A local repository path — code agents review the source for vulnerabilities.':'One or more URLs — the harness recons each, then intelligently selects matching agents.';});
|
||||
// toggles
|
||||
['off','sub','mcp'].forEach(k=>{const map={off:'offline',sub:'subscription',mcp:'mcp'};
|
||||
$('#'+map[k]).onchange=e=>$('#tg-'+k).classList.toggle('on',e.target.checked);});
|
||||
|
||||
async function init(){
|
||||
INFO=await (await fetch('/api/info')).json();
|
||||
const a=INFO.agents;
|
||||
$('#sf-agents').textContent=a.total;$('#sf-prov').textContent=INFO.providers.length;
|
||||
$('#sf-cli').textContent=(INFO.cli_backends||[]).length;
|
||||
$('#chip-models').textContent=(INFO.cli_backends||[]).join(' · ')||'api-only';
|
||||
$('#agentsub').textContent=`${a.vulns} vuln · ${a.recon||0} recon · ${a.code||0} code · ${a.meta} meta — ${a.total} total`;
|
||||
let mh='',first=true;
|
||||
INFO.providers.forEach(p=>p.models.forEach(m=>{const id=p.key+':'+m;
|
||||
mh+=`<label class="mopt"><input type="checkbox" value="${id}" ${first?'checked':''}/> <span class="tag ${p.kind}">${p.kind}</span> <code>${id}</code></label>`;first=false;}));
|
||||
$('#mpanel').innerHTML=mh;
|
||||
$('#modelcard').innerHTML=INFO.providers.map(p=>`<div class="mcard"><h3><span class="tag ${p.kind}">${p.kind}</span> ${p.label}
|
||||
${(INFO.cli_backends||[]).some(b=>['claude','codex','grok','gemini'].includes(b))&&p.kind==='cli'?'<span class="dim" style="font-size:11px">· subscription-capable</span>':''}</h3>
|
||||
<div class="muted" style="font-size:12px">${p.models.map(m=>'<code>'+m+'</code>').join(' · ')}</div></div>`).join('');
|
||||
AGENTS=(await (await fetch('/api/agents')).json()).agents;renderAgents();
|
||||
}
|
||||
function selectedModels(){return $$('#mpanel input:checked').map(i=>i.value);}
|
||||
function logLine(t){const T=$('#term');if(T.querySelector('.empty'))T.innerHTML='';const d=document.createElement('div');
|
||||
d.className=t.startsWith('===')?'h':t.includes('CONFIRMED')||t.includes('validated finding')||t.includes('updated')||t.startsWith('artifacts')?'ok':
|
||||
t.includes('failed')||t.startsWith('ERROR')?'e':t.startsWith('recon')||t.startsWith('exploit')||t.startsWith('analyze')||t.startsWith('intelligently')||t.startsWith('agent selection')?'v':
|
||||
t.startsWith('selected')||t.includes('candidate')?'s':'';
|
||||
d.textContent='› '+t;T.appendChild(d);T.scrollTop=T.scrollHeight;}
|
||||
let seen=0;
|
||||
async function run(){
|
||||
let body={models:selectedModels(),vote_n:+$('#voten').value,max_agents:+$('#maxa').value,
|
||||
offline:$('#offline').checked,subscription:$('#subscription').checked,mcp:$('#mcp').checked,mode};
|
||||
if(mode==='whitebox'){const r=$('#repo').value.trim();if(!r){$('#repo').focus();$('#repo').style.borderColor='var(--crit)';return;}body.repo=r;}
|
||||
else{const t=$('#targets').value.split('\n').map(s=>s.trim()).filter(Boolean);if(!t.length){$('#targets').focus();$('#targets').style.borderColor='var(--crit)';return;}body.targets=t;}
|
||||
$('#go').disabled=true;$('#prog').classList.add('on');$('#term').innerHTML='';seen=0;
|
||||
logLine((mode==='whitebox'?'White-box repo: '+body.repo:'Black-box targets: '+body.targets.length)+' · panel: '+(body.models.join(', ')||'default'));
|
||||
const r=await (await fetch('/api/run',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)})).json();
|
||||
if(r.error){logLine('ERROR: '+r.error);$('#go').disabled=false;$('#prog').classList.remove('on');return;}
|
||||
poll(r.run_id);
|
||||
}
|
||||
async function poll(id){
|
||||
const st=await (await fetch('/api/status/'+id)).json();
|
||||
(st.log||[]).slice(seen).forEach(logLine);seen=(st.log||[]).length;
|
||||
if(!st.done){setTimeout(()=>poll(id),600);return;}
|
||||
$('#go').disabled=false;$('#prog').classList.remove('on');lastRun=id;render(st.result||{});
|
||||
logLine('done.');
|
||||
}
|
||||
function render(res){
|
||||
const f=res.findings||[],by={Critical:0,High:0,Medium:0,Low:0,Info:0};f.forEach(x=>by[x.severity]=(by[x.severity]||0)+1);
|
||||
$('#sevbar').innerHTML=Object.entries(by).map(([k,v])=>`<div class="scount ${k}"><span class="n">${v}</span><span class="l">${k}</span></div>`).join('');
|
||||
$('#findings').innerHTML=f.length?f.map(x=>`<div class="find ${x.severity}"><h4><span class="sev ${x.severity}">${x.severity}</span> ${esc(x.title||'')}</h4>
|
||||
<div class="m mono">${esc(x.agent||'')} · ${esc(x.cwe||'')} · votes ${esc(x.votes||'-')} · conf ${(x.confidence||0).toFixed(2)} · ${esc(x.endpoint||'')}</div>
|
||||
${x.payload?`<pre>${esc(x.payload)}</pre>`:''}${x.evidence?`<div class="m">Evidence: ${esc(x.evidence)}</div>`:''}
|
||||
${x.remediation?`<div class="m">Fix: ${esc(x.remediation)}</div>`:''}</div>`).join('')
|
||||
:`<div class="empty-state">✓ Run complete — ${(res.agents_ran||[]).length} agents ran, 0 validated findings.<br><span class="dim">${$('#offline').checked?'Offline mode performs no exploitation — enable a model (API key or subscription) to find issues.':''}</span></div>`;
|
||||
$('#reportcard').innerHTML=`<a class="dl" href="/report/${lastRun}" target="_blank">⬇ Open HTML report</a><iframe src="/report/${lastRun}"></iframe>`;
|
||||
}
|
||||
function esc(s){return (s+'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');}
|
||||
$$('.fbtn').forEach(b=>b.onclick=()=>{$$('.fbtn').forEach(x=>x.classList.remove('on'));b.classList.add('on');filter=b.dataset.k;renderAgents();});
|
||||
function renderAgents(){const q=$('#asearch').value.toLowerCase();
|
||||
const rows=AGENTS.filter(a=>(filter==='all'||a.kind===filter)&&(!q||(a.name+a.title+a.cwe).toLowerCase().includes(q)));
|
||||
$('#alist').innerHTML=rows.slice(0,500).map(a=>`<div class="arow"><span class="tag ${a.kind}">${a.kind}</span> <code>${a.name}</code>
|
||||
<span class="t">${esc((a.title||'').replace(' Agent',''))} ${a.cwe?'· '+a.cwe:''}</span></div>`).join('')||'<div class="arow muted">no match</div>';}
|
||||
$('#asearch').oninput=renderAgents;
|
||||
$('#go').onclick=run;
|
||||
$('#targets').oninput=()=>$('#targets').style.borderColor='';$('#repo').oninput=()=>$('#repo').style.borderColor='';
|
||||
init();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 78 KiB |
@@ -1,4 +1,4 @@
|
||||
//! NeuroSploit v3.4.0 harness — a robust multi-model runtime for the
|
||||
//! NeuroSploit v3.4.1 harness — a robust multi-model runtime for the
|
||||
//! markdown-driven autonomous pentest engine.
|
||||
//!
|
||||
//! The harness loads the `agents_md/` library, drives a *pool* of LLM models
|
||||
@@ -16,8 +16,8 @@ pub mod types;
|
||||
|
||||
pub use agents::{Agent, Library};
|
||||
pub use models::{
|
||||
cli_binary_for, installed_cli_backends, provider_for, providers, write_mcp_config, ChatClient,
|
||||
ModelRef, Provider,
|
||||
cli_binary_for, ensure_playwright_mcp, installed_cli_backends, mcp_supported, provider_for,
|
||||
providers, write_mcp_config, ChatClient, ModelRef, Provider,
|
||||
};
|
||||
pub use pipeline::{run_whitebox, RunOutput};
|
||||
pub use pipeline::run;
|
||||
|
||||
@@ -146,20 +146,23 @@ impl ChatClient {
|
||||
let mut cmd = Command::new(bin);
|
||||
match bin {
|
||||
// Claude Code headless print mode (uses the Claude subscription login).
|
||||
// Tool autonomy is always enabled so the agent can use its built-in
|
||||
// tools (Bash/curl/etc.) to actually probe the target — Playwright MCP
|
||||
// is an *optional* add-on, not a requirement.
|
||||
"claude" => {
|
||||
cmd.arg("-p").arg("--model").arg(model);
|
||||
cmd.arg("-p").arg("--model").arg(model).arg("--dangerously-skip-permissions");
|
||||
// Required to allow tool autonomy when running as root.
|
||||
cmd.env("IS_SANDBOX", "1");
|
||||
if let Some(mcp) = mcp_config {
|
||||
cmd.arg("--mcp-config").arg(mcp).arg("--dangerously-skip-permissions");
|
||||
// Required to allow tool autonomy when running as root.
|
||||
cmd.env("IS_SANDBOX", "1");
|
||||
cmd.arg("--mcp-config").arg(mcp);
|
||||
}
|
||||
}
|
||||
// Codex non-interactive exec (uses the ChatGPT/Codex login), prompt on stdin.
|
||||
"codex" => {
|
||||
cmd.arg("exec").arg("--model").arg(model);
|
||||
cmd.arg("exec").arg("--model").arg(model)
|
||||
.arg("--dangerously-bypass-approvals-and-sandbox");
|
||||
if let Some(mcp) = mcp_config {
|
||||
cmd.arg("--config").arg(format!("mcp_config_file={mcp}"))
|
||||
.arg("--dangerously-bypass-approvals-and-sandbox");
|
||||
cmd.arg("--config").arg(format!("mcp_config_file={mcp}"));
|
||||
}
|
||||
cmd.arg("-");
|
||||
}
|
||||
@@ -173,13 +176,17 @@ impl ChatClient {
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
cmd.stdin(Stdio::piped()).stdout(Stdio::piped()).stderr(Stdio::piped());
|
||||
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.
|
||||
}
|
||||
let out = child.wait_with_output().await?;
|
||||
// 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 {
|
||||
Ok(r) => r?,
|
||||
Err(_) => return Err(anyhow!("{} subscription CLI timed out after 600s", bin)),
|
||||
};
|
||||
let stdout = String::from_utf8_lossy(&out.stdout).trim().to_string();
|
||||
let stderr = String::from_utf8_lossy(&out.stderr);
|
||||
if !out.status.success() {
|
||||
@@ -229,6 +236,35 @@ pub fn installed_cli_backends() -> Vec<&'static str> {
|
||||
["claude", "codex", "grok", "gemini"].into_iter().filter(|b| binary_in_path(b)).collect()
|
||||
}
|
||||
|
||||
/// 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.
|
||||
pub fn mcp_supported(provider: &str) -> bool {
|
||||
matches!(provider, "anthropic" | "openai")
|
||||
}
|
||||
|
||||
/// Best-effort ensure the Playwright MCP server is available locally. Requires
|
||||
/// `npx`; pre-warms `@playwright/mcp` so the first agent call isn't a cold start.
|
||||
/// Returns Err with a clear reason when it can't be provisioned (caller then
|
||||
/// degrades to built-in tools).
|
||||
pub fn ensure_playwright_mcp() -> Result<()> {
|
||||
if !binary_in_path("npx") {
|
||||
return Err(anyhow!("npx (Node.js) not found — install Node to use Playwright MCP"));
|
||||
}
|
||||
// `npx -y @playwright/mcp@latest --help` installs the package into the npx
|
||||
// cache on first run; ignore non-zero exit (some versions lack --help) as long
|
||||
// as the package resolves.
|
||||
let out = std::process::Command::new("npx")
|
||||
.args(["-y", "@playwright/mcp@latest", "--help"])
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.status();
|
||||
match out {
|
||||
Ok(_) => Ok(()),
|
||||
Err(e) => Err(anyhow!("could not provision @playwright/mcp via npx: {e}")),
|
||||
}
|
||||
}
|
||||
|
||||
/// Write a Playwright `.mcp.json` into `dir` and return its path, so the agentic
|
||||
/// CLI can drive a real browser (DOM/JS/network/screenshots) during execution.
|
||||
pub fn write_mcp_config(dir: &std::path::Path) -> std::io::Result<std::path::PathBuf> {
|
||||
|
||||
@@ -11,6 +11,7 @@ use tokio::sync::mpsc::Sender;
|
||||
/// Result of an engagement run.
|
||||
#[derive(Default, Serialize)]
|
||||
pub struct RunOutput {
|
||||
pub target: String,
|
||||
pub findings: Vec<Finding>,
|
||||
pub agents_ran: Vec<String>,
|
||||
pub candidates: usize,
|
||||
@@ -19,7 +20,28 @@ pub struct RunOutput {
|
||||
pub artifacts: Vec<String>,
|
||||
}
|
||||
|
||||
const RECON_SYS: &str = "You are a web recon specialist. Map the target's attack surface and reply with a compact JSON object (tech, endpoints, auth, apis, ai_features). No prose.";
|
||||
const RECON_SYS: &str = "You are a web recon specialist on an AUTHORIZED engagement. You have shell tools (curl etc.) — actively fetch the target, enumerate pages/params, and map the real attack surface. Do not ask for permission; proceed. Reply with a compact JSON object (tech, endpoints, params, auth, apis). No prose.";
|
||||
|
||||
/// Tool-usage doctrine prepended to recon/exploit prompts so the agent knows
|
||||
/// exactly what it may use. Best run on Kali Linux (or the Kali Docker image),
|
||||
/// 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."
|
||||
} else {
|
||||
"No browser MCP is available — use `curl` (and `wget`) for all HTTP interaction; render/inspect responses directly."
|
||||
};
|
||||
format!(
|
||||
"TOOLING (authorized; best on Kali Linux or the kalilinux/kali-rolling Docker image):\n\
|
||||
- HTTP: `curl` (headers, methods, params, cookies), `wget`.\n\
|
||||
- Ports/services: `rustscan` if present, else `nmap`; if neither is installed you may \
|
||||
install via apt (`apt install -y nmap`), brew, or cargo (`cargo install rustscan`) — \
|
||||
otherwise probe common ports with `curl`/`nc`.\n\
|
||||
- Content/params: `ffuf`, `gobuster`, `gau`, `katana` when available.\n\
|
||||
- {browser}\n\
|
||||
Use only what is installed; degrade gracefully. Never run destructive or DoS actions.\n\n"
|
||||
)
|
||||
}
|
||||
const VOTE_SYS: &str = "You are an adversarial security validator. Decide if the candidate finding is a REAL, reproducible, exploitable vulnerability with proof. Reply with JSON {\"verdict\":\"confirmed\"|\"rejected\",\"reason\":\"...\"}. Default to rejected when uncertain.";
|
||||
const CODE_VOTE_SYS: &str = "You are an adversarial source-code reviewer. Decide if the reported issue is a REAL vulnerability in the provided code (reachable, exploitable, not a false positive). Reply JSON {\"verdict\":\"confirmed\"|\"rejected\",\"reason\":\"...\"}.";
|
||||
|
||||
@@ -40,9 +62,14 @@ pub async fn run(cfg: RunConfig, lib: &Library, pool: &ModelPool, tx: Sender<Str
|
||||
let _ = tx.send("recon: offline mode — skipping model calls".into()).await;
|
||||
"{}".to_string()
|
||||
} else {
|
||||
match pool.complete(RECON_SYS, &format!("Target: {}", cfg.target)).await {
|
||||
let recon_user = format!("{}Target: {}", tool_doctrine(pool.mcp_config.is_some()), cfg.target);
|
||||
match pool.complete(RECON_SYS, &recon_user).await {
|
||||
Ok((m, t)) => {
|
||||
let _ = tx.send(format!("recon complete via {}", m.label())).await;
|
||||
if cfg.verbose {
|
||||
let snip: String = t.chars().take(280).collect();
|
||||
let _ = tx.send(format!(" recon> {}", snip.replace('\n', " "))).await;
|
||||
}
|
||||
t
|
||||
}
|
||||
Err(e) => {
|
||||
@@ -63,22 +90,24 @@ pub async fn run(cfg: RunConfig, lib: &Library, pool: &ModelPool, tx: Sender<Str
|
||||
let _ = tx.send(format!("selected {} specialist agents (RL-ranked)", selected.len())).await;
|
||||
let _ = tx.send("offline: no exploitation performed (provide API keys or --subscription to run live)".into()).await;
|
||||
let artifacts = persist(&cfg, &recon, "", &[]);
|
||||
return RunOutput { findings: vec![], agents_ran: selected.iter().map(|a| a.name.clone()).collect(), candidates: 0, recon, artifacts };
|
||||
return RunOutput { target: cfg.target.clone(), findings: vec![], agents_ran: selected.iter().map(|a| a.name.clone()).collect(), candidates: 0, recon, artifacts };
|
||||
}
|
||||
|
||||
// Use the model to pick the agents whose preconditions match the recon —
|
||||
// the harness reasons about *which* specialists to run, not all of them.
|
||||
let chosen = select_agents(pool, &recon, &ranked, &tx).await;
|
||||
let selected: Vec<Agent> = {
|
||||
let mut sel: Vec<Agent> = if chosen.is_empty() {
|
||||
ranked.clone()
|
||||
} else {
|
||||
ranked.iter().filter(|a| chosen.iter().any(|c| c == &a.name)).cloned().collect()
|
||||
};
|
||||
let selected: Vec<Agent> = if !chosen.is_empty() {
|
||||
let sel: Vec<Agent> =
|
||||
ranked.iter().filter(|a| chosen.iter().any(|c| c == &a.name)).cloned().collect();
|
||||
if sel.is_empty() {
|
||||
sel = ranked.clone();
|
||||
heuristic_select(&ranked, &recon, cap)
|
||||
} else {
|
||||
sel.into_iter().take(cap).collect()
|
||||
}
|
||||
sel.into_iter().take(cap).collect()
|
||||
} else {
|
||||
// LLM selection failed/empty → recon-keyword heuristic, not a blind flat list.
|
||||
let _ = tx.send("selection empty — using recon-keyword heuristic".into()).await;
|
||||
heuristic_select(&ranked, &recon, cap)
|
||||
};
|
||||
let _ = tx
|
||||
.send(format!("intelligently selected {} agent(s) matching recon: {}", selected.len(),
|
||||
@@ -87,6 +116,8 @@ pub async fn run(cfg: RunConfig, lib: &Library, pool: &ModelPool, tx: Sender<Str
|
||||
|
||||
// ---- 3. Exploit (parallel) -----------------------------------------
|
||||
let target = cfg.target.clone();
|
||||
let verbose = cfg.verbose;
|
||||
let mcp_on = pool.mcp_config.is_some();
|
||||
let raw: Vec<(String, String, Vec<Finding>)> = stream::iter(selected.iter().cloned())
|
||||
.map(|ag| {
|
||||
let target = target.clone();
|
||||
@@ -94,10 +125,18 @@ pub async fn run(cfg: RunConfig, lib: &Library, pool: &ModelPool, tx: Sender<Str
|
||||
let txc = tx.clone();
|
||||
async move {
|
||||
let user = format!(
|
||||
"{}\n\nReply ONLY with a JSON array of confirmed findings (may be empty []). \
|
||||
Each item: {{id,title,severity,cwe,endpoint,payload,evidence,impact,remediation,confidence}}.",
|
||||
ag.user.replace("{target}", &target).replace("{recon_json}", &recon)
|
||||
"AUTHORIZED engagement — you have explicit permission to test {target}. \
|
||||
Do not ask for confirmation — proceed and PROVE each issue.\n\n\
|
||||
{doctrine}{body}\n\nWhen done, reply with ONLY a JSON array of confirmed findings (may be empty []). \
|
||||
Each item: {{id,title,severity,cwe,endpoint,payload,evidence,impact,remediation,confidence}}. \
|
||||
`evidence` must contain the concrete proof (request/response excerpt).",
|
||||
target = target,
|
||||
doctrine = tool_doctrine(mcp_on),
|
||||
body = ag.user.replace("{target}", &target).replace("{recon_json}", &recon),
|
||||
);
|
||||
if verbose {
|
||||
let _ = txc.send(format!(" ▶ launching agent: {} ({})", ag.name, ag.title.replace(" Agent", ""))).await;
|
||||
}
|
||||
match pool.complete(&ag.system, &user).await {
|
||||
Ok((m, text)) => {
|
||||
let f = extract_findings(&text, &ag.name);
|
||||
@@ -145,7 +184,7 @@ pub async fn run_whitebox(cfg: RunConfig, lib: &Library, pool: &ModelPool, tx: S
|
||||
|
||||
if cfg.offline || bytes == 0 {
|
||||
let artifacts = persist(&cfg, "{}", &context, &[]);
|
||||
return RunOutput { findings: vec![], agents_ran: selected.iter().map(|a| a.name.clone()).collect(), candidates: 0, recon: String::new(), artifacts };
|
||||
return RunOutput { target: cfg.target.clone(), findings: vec![], agents_ran: selected.iter().map(|a| a.name.clone()).collect(), candidates: 0, recon: String::new(), artifacts };
|
||||
}
|
||||
|
||||
let raw: Vec<(String, String, Vec<Finding>)> = stream::iter(selected.iter().cloned())
|
||||
@@ -200,7 +239,12 @@ async fn select_agents(pool: &ModelPool, recon: &str, catalog: &[Agent], tx: &Se
|
||||
match pool.complete(SELECT_SYS, &user).await {
|
||||
Ok((m, text)) => {
|
||||
let names = parse_string_array(&text);
|
||||
let _ = tx.send(format!("agent selection via {} → {} agent(s) chosen", m.label(), names.len())).await;
|
||||
if names.is_empty() {
|
||||
let preview: String = text.chars().take(120).collect();
|
||||
let _ = tx.send(format!("agent selection via {} returned no parseable list ({} chars): {}", m.label(), text.len(), preview.replace('\n', " "))).await;
|
||||
} else {
|
||||
let _ = tx.send(format!("agent selection via {} → {} agent(s) chosen", m.label(), names.len())).await;
|
||||
}
|
||||
names
|
||||
}
|
||||
Err(e) => {
|
||||
@@ -217,16 +261,88 @@ fn parse_string_array(text: &str) -> Vec<String> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Fallback agent selection when the LLM selector fails: score each agent by
|
||||
/// keyword overlap between its name/title and the recon text, always seed a
|
||||
/// black-box baseline of high-yield web classes, and take the top `cap`.
|
||||
fn heuristic_select(ranked: &[Agent], recon: &str, cap: usize) -> Vec<Agent> {
|
||||
const BASELINE: &[&str] = &[
|
||||
"sqli_error", "sqli_blind", "sqli_union", "xss_reflected", "xss_stored", "xss_dom",
|
||||
"command_injection", "lfi", "path_traversal", "ssrf", "idor", "open_redirect",
|
||||
"auth_bypass", "csrf", "ssti", "file_upload", "xxe", "information_disclosure",
|
||||
"security_headers", "cors_misconfig",
|
||||
];
|
||||
let r = recon.to_lowercase();
|
||||
// Recon signal → agent-name substrings. Only agents whose surface the recon
|
||||
// actually identified get the signal boost; the rest rely on the baseline.
|
||||
let signals: &[(&str, &[&str])] = &[
|
||||
("graphql", &["graphql"]),
|
||||
("jwt", &["jwt"]),
|
||||
("oauth", &["oauth", "oidc", "saml"]),
|
||||
("\"jwt\"", &["jwt"]),
|
||||
("api", &["api_", "bola", "bfla", "idor", "mass_assign", "rate_limit"]),
|
||||
("upload", &["file_upload", "zip_slip"]),
|
||||
("websocket", &["websocket"]),
|
||||
("\"ws\"", &["websocket"]),
|
||||
("graphql", &["graphql"]),
|
||||
("aws", &["aws_", "s3_", "imds", "cloud_"]),
|
||||
("gcp", &["gcp_", "gcs_", "metadata"]),
|
||||
("azure", &["azure_"]),
|
||||
("kubernetes", &["k8s_", "kubelet"]),
|
||||
("docker", &["docker_", "container_"]),
|
||||
("ai_features", &["llm_", "prompt_injection", "rag", "vector_db"]),
|
||||
("chat", &["llm_", "prompt_injection"]),
|
||||
("jinja", &["ssti"]),
|
||||
("flask", &["ssti", "ssrf", "command_injection"]),
|
||||
("php", &["lfi", "rfi", "sqli", "command_injection"]),
|
||||
("template", &["ssti", "csti"]),
|
||||
("redirect", &["open_redirect"]),
|
||||
("login", &["auth_bypass", "brute_force", "sqli", "default_credentials"]),
|
||||
("search", &["xss", "sqli"]),
|
||||
("cache", &["cache", "smuggl"]),
|
||||
];
|
||||
let mut scored: Vec<(i32, &Agent)> = ranked
|
||||
.iter()
|
||||
.map(|a| {
|
||||
let mut score = 0;
|
||||
if BASELINE.contains(&a.name.as_str()) {
|
||||
score += 4;
|
||||
}
|
||||
// recon-signal mapping: boost agents matching identified surface
|
||||
for (sig, names) in signals {
|
||||
if r.contains(sig) && names.iter().any(|n| a.name.contains(n)) {
|
||||
score += 6;
|
||||
}
|
||||
}
|
||||
// direct keyword overlap with recon text
|
||||
for tok in a.name.split('_') {
|
||||
if tok.len() >= 4 && r.contains(tok) {
|
||||
score += 2;
|
||||
}
|
||||
}
|
||||
(score, a)
|
||||
})
|
||||
.collect();
|
||||
scored.sort_by(|x, y| y.0.cmp(&x.0));
|
||||
let mut out: Vec<Agent> = scored.iter().filter(|(s, _)| *s > 0).map(|(_, a)| (*a).clone()).collect();
|
||||
if out.is_empty() {
|
||||
out = ranked.to_vec();
|
||||
}
|
||||
out.into_iter().take(cap).collect()
|
||||
}
|
||||
|
||||
async fn validate(candidates: Vec<Finding>, pool: &ModelPool, sys: &str, vote_n: usize, tx: &Sender<String>) -> Vec<Finding> {
|
||||
// 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.into_iter())
|
||||
.map(|mut f| {
|
||||
let txc = tx.clone();
|
||||
let finder = finder.clone();
|
||||
async move {
|
||||
let q = format!(
|
||||
"Finding: {} | severity {} | {} | at {} | payload {} | evidence {}",
|
||||
f.title, f.severity, f.cwe, f.endpoint, f.payload, f.evidence
|
||||
);
|
||||
let (yes, total) = pool.vote(sys, &q, vote_n).await;
|
||||
let (yes, total) = pool.vote(sys, &q, vote_n, finder.as_deref()).await;
|
||||
f.validated = total > 0 && yes * 2 >= total;
|
||||
f.votes = format!("{yes}/{total}");
|
||||
if f.confidence == 0.0 && total > 0 {
|
||||
@@ -268,6 +384,7 @@ async fn finish(cfg: RunConfig, _lib: &Library, recon: String, transcript: Strin
|
||||
}
|
||||
|
||||
RunOutput {
|
||||
target: cfg.target.clone(),
|
||||
candidates: findings.len(),
|
||||
findings,
|
||||
agents_ran: selected.iter().map(|a| a.name.clone()).collect(),
|
||||
|
||||
@@ -87,8 +87,18 @@ impl ModelPool {
|
||||
/// Ask up to `n` distinct models the same yes/no validation question and
|
||||
/// return (confirmations, total_votes). A model answering "yes"/"confirmed"
|
||||
/// counts as a confirmation. Used to cut false positives.
|
||||
pub async fn vote(&self, system: &str, user: &str, n: usize) -> (usize, usize) {
|
||||
let panel: Vec<ModelRef> = self.candidates.iter().take(n.max(1)).cloned().collect();
|
||||
///
|
||||
/// `skip` names the model that produced the finding; when the panel has more
|
||||
/// than one model, that model is moved to the back so a DIFFERENT model
|
||||
/// adjudicates first (cross-model false-positive validation).
|
||||
pub async fn vote(&self, system: &str, user: &str, n: usize, skip: Option<&str>) -> (usize, usize) {
|
||||
let mut ordered: Vec<ModelRef> = self.candidates.clone();
|
||||
if let Some(finder) = skip {
|
||||
if ordered.len() > 1 {
|
||||
ordered.sort_by_key(|m| m.label() == finder); // finder (true) sorts last
|
||||
}
|
||||
}
|
||||
let panel: Vec<ModelRef> = ordered.into_iter().take(n.max(1)).collect();
|
||||
let mut confirmed = 0usize;
|
||||
let mut total = 0usize;
|
||||
for m in &panel {
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
use crate::types::Finding;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
/// The blank, structured Typst template (rendering logic). Data (`meta`,
|
||||
/// `findings`) is prepended by `typst_report` to make a self-contained file.
|
||||
const TYPST_TEMPLATE: &str = include_str!("../../../templates/report.typ");
|
||||
|
||||
fn sev_rank(s: &str) -> u8 {
|
||||
match s {
|
||||
@@ -74,9 +79,70 @@ pub fn html(target: &str, findings: &[Finding]) -> String {
|
||||
h4{{margin:12px 0 3px;font-size:12px;text-transform:uppercase;letter-spacing:.5px;color:#8b5cf6}}\
|
||||
.b{{color:#8b5cf6;font-weight:800}}</style></head><body>\
|
||||
<h1><span class=b>NeuroSploit</span> Penetration Test Report</h1>\
|
||||
<div class=meta>Target: <b>{t}</b> · v3.4.0 Rust harness · multi-model validated</div>\
|
||||
<div class=meta>Target: <b>{t}</b> · v3.4.1 Rust harness · multi-model validated</div>\
|
||||
<div>{chips}</div><h2>Findings ({n})</h2>{body}\
|
||||
<p class=meta>Authorized testing only. Findings confirmed by multi-model adversarial voting.</p></body></html>",
|
||||
t = esc(target), chips = chips, n = sorted.len(), body = body,
|
||||
)
|
||||
}
|
||||
|
||||
// ===== Typst report =====
|
||||
|
||||
/// Is the `typst` binary available on PATH?
|
||||
fn typst_available() -> bool {
|
||||
std::env::var_os("PATH")
|
||||
.map(|p| std::env::split_paths(&p).any(|d| d.join("typst").is_file()))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn sorted_findings(findings: &[Finding]) -> Vec<Finding> {
|
||||
let mut v = findings.to_vec();
|
||||
v.sort_by_key(|f| sev_rank(&f.severity));
|
||||
v
|
||||
}
|
||||
|
||||
/// Escape a string for embedding inside a Typst `"..."` literal (single line).
|
||||
fn tq(s: &str) -> String {
|
||||
let cleaned: String = s.replace('\\', "\\\\").replace('"', "\\\"").replace(['\n', '\r'], " ");
|
||||
format!("\"{}\"", cleaned)
|
||||
}
|
||||
|
||||
/// Generate a self-contained `report.typ` (data + bundled template) in `dir`
|
||||
/// and compile it to `report.pdf` via the `typst` binary. Falls back to leaving
|
||||
/// the `.typ` when `typst` is unavailable.
|
||||
pub fn typst_report(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 mut data = String::new();
|
||||
data.push_str(&format!(
|
||||
"#let meta = (target: {}, run_id: {}, generated: {}, model: {})\n",
|
||||
tq(target), tq(&run_id), tq("NeuroSploit v3.4.1"), tq("multi-model")
|
||||
));
|
||||
data.push_str("#let findings = (\n");
|
||||
for f in sorted_findings(findings) {
|
||||
data.push_str(&format!(
|
||||
" (severity: {}, title: {}, agent: {}, cwe: {}, cvss: {}, endpoint: {}, payload: {}, evidence: {}, impact: {}, remediation: {}, votes: {}, confidence: {}),\n",
|
||||
tq(&f.severity), tq(&f.title), tq(&f.agent), tq(&f.cwe), tq(&f.cvss),
|
||||
tq(&f.endpoint), tq(&f.payload), tq(&f.evidence), tq(&f.impact),
|
||||
tq(&f.remediation), tq(&f.votes), f.confidence,
|
||||
));
|
||||
}
|
||||
data.push_str(")\n\n");
|
||||
|
||||
let typ_path = dir.join("report.typ");
|
||||
std::fs::write(&typ_path, format!("{data}{TYPST_TEMPLATE}"))?;
|
||||
|
||||
if typst_available() {
|
||||
let pdf_path = dir.join("report.pdf");
|
||||
match std::process::Command::new("typst")
|
||||
.arg("compile").arg(&typ_path).arg(&pdf_path).output()
|
||||
{
|
||||
Ok(o) if o.status.success() && pdf_path.exists() => return Ok(pdf_path),
|
||||
Ok(o) => eprintln!("typst compile failed: {}",
|
||||
String::from_utf8_lossy(&o.stderr).lines().next().unwrap_or("").trim()),
|
||||
Err(e) => eprintln!("typst not runnable: {e}"),
|
||||
}
|
||||
}
|
||||
Ok(typ_path)
|
||||
}
|
||||
|
||||
@@ -80,6 +80,9 @@ pub struct RunConfig {
|
||||
/// Path to the RL reward state file.
|
||||
#[serde(default)]
|
||||
pub rl_path: Option<String>,
|
||||
/// Verbose: log each agent as it launches, recon snippet, and votes.
|
||||
#[serde(default)]
|
||||
pub verbose: bool,
|
||||
}
|
||||
|
||||
fn default_vote() -> usize {
|
||||
@@ -101,6 +104,7 @@ impl RunConfig {
|
||||
subscription: false,
|
||||
workdir: None,
|
||||
rl_path: None,
|
||||
verbose: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
// NeuroSploit v3.4.1 — Typst report template (blank, structured).
|
||||
//
|
||||
// The harness generates `report.typ` per run by prepending a `findings` array
|
||||
// and a `meta` dict, then including this template's rendering logic. This file
|
||||
// is the reference/blank template: it renders a cover, an executive summary with
|
||||
// severity counts, and one section per finding. Compile with:
|
||||
// typst compile report.typ report.pdf
|
||||
//
|
||||
// Expected inputs (defined above this template in the generated file):
|
||||
// #let meta = (target: "", run_id: "", generated: "", model: "")
|
||||
// #let findings = ( (severity: "", title: "", agent: "", cwe: "", cvss: "",
|
||||
// endpoint: "", payload: "", evidence: "", impact: "",
|
||||
// remediation: "", votes: "", confidence: 0.0), ... )
|
||||
|
||||
#let sevcolor = (
|
||||
Critical: rgb("#c0392b"), High: rgb("#e67e22"), Medium: rgb("#f1c40f"),
|
||||
Low: rgb("#3498db"), Info: rgb("#7f8c8d"),
|
||||
)
|
||||
#let sevbadge(s) = box(
|
||||
fill: sevcolor.at(s, default: rgb("#7f8c8d")), inset: (x: 5pt, y: 2pt),
|
||||
radius: 3pt, text(fill: white, weight: "bold", size: 8pt, upper(s)),
|
||||
)
|
||||
#let sevrank(s) = (Critical: 0, High: 1, Medium: 2, Low: 3, Info: 4).at(s, default: 5)
|
||||
|
||||
#set page(margin: 2cm, numbering: "1", footer: context [
|
||||
#set text(size: 8pt, fill: gray)
|
||||
NeuroSploit v3.4.1 · #meta.target · confidential
|
||||
#h(1fr) #counter(page).display()
|
||||
])
|
||||
#set text(font: ("Helvetica Neue", "Helvetica", "Arial"), size: 10pt)
|
||||
#set heading(numbering: none)
|
||||
|
||||
// ---- Cover ----
|
||||
#v(3cm)
|
||||
#align(center)[
|
||||
#text(28pt, weight: "bold")[#text(fill: rgb("#7c5cff"))[Neuro]Sploit]
|
||||
#v(2pt)
|
||||
#text(15pt, fill: gray)[Penetration Test Report]
|
||||
#v(1cm)
|
||||
#text(13pt)[Target: #strong(meta.target)]
|
||||
#v(4pt)
|
||||
#text(10pt, fill: gray)[Run #meta.run_id · #meta.generated · models: #meta.model]
|
||||
]
|
||||
#pagebreak()
|
||||
|
||||
// ---- Executive summary ----
|
||||
= Executive Summary
|
||||
|
||||
#let counts = (:)
|
||||
#for f in findings {
|
||||
counts.insert(f.severity, counts.at(f.severity, default: 0) + 1)
|
||||
}
|
||||
#if findings.len() == 0 [
|
||||
No validated findings were produced for this engagement. All candidate issues
|
||||
were either unproven or rejected by multi-model adversarial validation.
|
||||
] else [
|
||||
This engagement produced #strong(str(findings.len())) validated finding(s),
|
||||
each confirmed by multi-model voting.
|
||||
|
||||
#v(6pt)
|
||||
#grid(columns: 5, gutter: 8pt,
|
||||
..("Critical", "High", "Medium", "Low", "Info").map(s => box(
|
||||
width: 100%, inset: 8pt, radius: 6pt, stroke: 0.5pt + sevcolor.at(s),
|
||||
align(center)[
|
||||
#text(18pt, weight: "bold", fill: sevcolor.at(s))[#str(counts.at(s, default: 0))]
|
||||
#v(-4pt) #text(8pt, upper(s))
|
||||
],
|
||||
))
|
||||
)
|
||||
]
|
||||
|
||||
#v(10pt)
|
||||
#line(length: 100%, stroke: 0.5pt + gray)
|
||||
|
||||
// ---- Findings ----
|
||||
= Findings
|
||||
|
||||
#let sorted = findings.sorted(key: f => sevrank(f.severity))
|
||||
#if sorted.len() == 0 [
|
||||
#text(fill: gray)[_Nothing to report._]
|
||||
]
|
||||
#for (i, f) in sorted.enumerate() [
|
||||
#block(breakable: false, width: 100%, inset: 10pt, radius: 6pt,
|
||||
stroke: (left: 3pt + sevcolor.at(f.severity, default: gray), rest: 0.5pt + rgb("#dddddd")))[
|
||||
#sevbadge(f.severity) #h(6pt) #text(12pt, weight: "bold")[#str(i + 1). #f.title]
|
||||
#v(4pt)
|
||||
#text(9pt, fill: gray)[
|
||||
agent: #raw(f.agent) · CWE: #f.cwe · CVSS: #f.cvss · votes: #f.votes · confidence: #str(f.confidence)
|
||||
]
|
||||
#v(2pt) #text(9pt)[Endpoint: #raw(f.endpoint)]
|
||||
#v(5pt) #strong[Payload] #linebreak() #raw(f.payload)
|
||||
#v(3pt) #strong[Evidence] #linebreak() #raw(f.evidence)
|
||||
#v(3pt) #strong[Impact:] #f.impact
|
||||
#v(2pt) #strong[Remediation:] #f.remediation
|
||||
]
|
||||
#v(8pt)
|
||||
]
|
||||
Reference in New Issue
Block a user