feat: key verification, category filter, installer deps/run, UI polish

Web console
- LLM API keys: Save now verifies the key with a cheap authenticated call per
  provider (anthropic/openai/xai/gemini/deepseek/mistral/groq/together/openrouter/
  moonshot/qwen/nvidia) and shows ✓ valid / ✗ rejected / • saved; new
  POST /api/keys/verify. Keys still save when a provider isn't auto-verifiable.
- Leads: a single "category" dropdown filters the board to one category.
- Provider/Model row alignment fixed (stacked-group top margin no longer applies
  inside a field-row).
- "own interceptor" renamed to "NeuroSploit Interceptor" in the proxy picker.

Installers
- setup.sh: v4.3.0; detects a package manager (apt/brew/dnf/pacman/apk) and
  installs missing deps (curl, git, nmap, node; rustscan + typst via cargo);
  optional launch at the end (NEUROSPLOIT_RUN=1 or a prompt). Skip with
  NEUROSPLOIT_NO_DEPS=1.
- install.ps1: v4.3.0; installs deps via winget (Node, Nmap, Git; typst via
  cargo); optional launch at the end.
- setup.bat (new): Windows CMD entry point that bootstraps install.ps1 (local or
  from GitHub).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
CyberSecurityUPandClaude Opus 4.8 committed 2026-10-14 20:09:32 -03:00
1 parent dc397f1b50
commit 1a2e1791e3
7 files changed
+254 -20

No files matched your search

+33 -2
View File
@@ -14,7 +14,7 @@ function Ok ($m) { Write-Host " + $m" -ForegroundColor Green }
function Warn($m){ Write-Host " ! $m" -ForegroundColor Yellow }
Write-Host ""
Write-Host " NeuroSploit installer (Windows) — v3.6.9" -ForegroundColor Cyan
Write-Host " NeuroSploit installer (Windows) — v4.3.0" -ForegroundColor Cyan
# arch → asset arch (only x64 prebuilt today; arm64 falls back to source)
$rawArch = $env:PROCESSOR_ARCHITECTURE
@@ -29,7 +29,7 @@ $ref = $env:NEUROSPLOIT_REF
if (-not $ref) {
try { $ref = (Invoke-RestMethod "https://api.github.com/repos/$slug/releases/latest").tag_name } catch { }
}
if (-not $ref) { $ref = "v3.6.9" }
if (-not $ref) { $ref = "v4.3.0" }
Say "Release: $ref"
New-Item -ItemType Directory -Force -Path $dir | Out-Null
@@ -104,9 +104,40 @@ Ok "Set NEUROSPLOIT_BASE=$dir (User)."
$env:Path = "$dir;$env:Path"; $env:NEUROSPLOIT_BASE = $dir
Ok ("Version: " + (& $exePath --version))
# ---- dependencies: install what winget can (best-effort, never fatal) ----
if ($env:NEUROSPLOIT_NO_DEPS -ne "1") {
Say "Checking dependencies..."
$have = { param($c) [bool](Get-Command $c -ErrorAction SilentlyContinue) }
$deps = @(
@{ cmd = "node"; id = "OpenJS.NodeJS"; why = "web console" },
@{ cmd = "nmap"; id = "Insecure.Nmap"; why = "recon (optional)" },
@{ cmd = "git"; id = "Git.Git"; why = "source build / updates" }
)
foreach ($d in $deps) {
if (& $have $d.cmd) { Ok ("{0} present" -f $d.cmd) }
elseif (Get-Command winget -ErrorAction SilentlyContinue) {
Say ("installing {0} ({1})..." -f $d.cmd, $d.why)
try { winget install -e --id $d.id --accept-source-agreements --accept-package-agreements | Out-Null; Ok ("{0} installed" -f $d.cmd) }
catch { Warn ("could not install {0} automatically — install it manually" -f $d.cmd) }
} else { Warn ("{0} missing (winget unavailable) — install manually for: {1}" -f $d.cmd, $d.why) }
}
if (& $have cargo) {
if (-not (& $have typst)) { Say "installing typst (cargo, PDF reports)..."; try { cargo install typst-cli | Out-Null; Ok "typst installed" } catch { Warn "typst optional — skipped" } }
}
}
Write-Host ""
Ok "Installed. Open a NEW terminal, then from ANY folder:"
Write-Host " neurosploit # interactive session"
Write-Host " neurosploit run http://testphp.vulnweb.com/ --subscription --model anthropic:claude-opus-4-8 -v"
Write-Host " neurosploit --help"
Warn "Update later: just re-run this script."
# ---- optionally launch now (NEUROSPLOIT_RUN=1, or confirm when interactive) ----
$runNow = $env:NEUROSPLOIT_RUN
if (-not $runNow -and [Environment]::UserInteractive) {
$ans = Read-Host "`n Launch the interactive session now? [y/N]"
if ($ans -match '^(y|yes)$') { $runNow = "1" }
}
if ($runNow -eq "1") { Say "Starting NeuroSploit..."; & $exePath }
+40
View File
@@ -0,0 +1,40 @@
@echo off
REM NeuroSploit installer for Windows (CMD) — by Joas A Santos & Red Team Leaders
REM
REM Double-click setup.bat, or run it from a terminal. It bootstraps the
REM PowerShell installer (install.ps1), which detects the platform, installs
REM Rust if missing, downloads or builds the binary, finds and installs
REM dependencies (Node, Nmap, Git, typst), configures PATH + NEUROSPLOIT_BASE,
REM and can launch NeuroSploit at the end.
REM
REM Env (optional): NEUROSPLOIT_REF, NEUROSPLOIT_DIR, NEUROSPLOIT_BUILD=1,
REM NEUROSPLOIT_NO_DEPS=1, NEUROSPLOIT_RUN=1
setlocal
echo.
echo NeuroSploit installer (Windows) v4.3.0
echo --------------------------------------
where powershell >nul 2>&1
if errorlevel 1 (
echo [x] PowerShell not found. Install PowerShell, or use WSL with setup.sh.
exit /b 1
)
REM Prefer the install.ps1 sitting next to this file; fall back to the published one.
set "PS1=%~dp0install.ps1"
if exist "%PS1%" (
echo Running local installer: %PS1%
powershell -NoProfile -ExecutionPolicy Bypass -File "%PS1%"
) else (
echo Fetching installer from GitHub...
powershell -NoProfile -ExecutionPolicy Bypass -Command "irm https://raw.githubusercontent.com/JoasASantos/NeuroSploit/main/install.ps1 | iex"
)
set "RC=%ERRORLEVEL%"
echo.
if "%RC%"=="0" (
echo [ok] Done. Open a NEW terminal, then run: neurosploit
) else (
echo [x] Installer exited with code %RC%.
)
endlocal & exit /b %RC%
+69 -8
View File
@@ -28,10 +28,10 @@ cat <<'BANNER'
███╗ ██╗███████╗██╗ ██╗██████╗ ██████╗
████╗ ██║██╔════╝██║ ██║██╔══██╗██╔═══██╗ NeuroSploit installer
██╔██╗ ██║█████╗ ██║ ██║██████╔╝██║ ██║ v3.6.9 — Rust harness
██╔██╗ ██║█████╗ ██║ ██║██████╔╝██║ ██║ v4.3.0 — Rust harness
██║╚██╗██║██╔══╝ ██║ ██║██╔══██╗██║ ██║ by Joas A Santos
██║ ╚████║███████╗╚██████╔╝██║ ██║╚██████╔╝ & Red Team Leaders
╚═╝ ╚═══╝╚══════╝ ╚═════╝ ╚═╝ ╚═╝ ╚═════╝
╚═╝ ╚═══╝╚══════╝ ╚═════╝ ╚═╝ ╚═╝ ╚═════╝ v4.3.0
BANNER
# ---- platform detection (Linux / macOS / Windows-WSL · x64 / arm64) ----
@@ -63,7 +63,7 @@ if [ -z "$REF" ]; then
REF="$(dl "https://api.github.com/repos/${REPO_SLUG}/releases/latest" /dev/stdout 2>/dev/null \
| grep -m1 '"tag_name"' | sed -E 's/.*"tag_name" *: *"([^"]+)".*/\1/' || true)"
fi
[ -z "$REF" ] && REF="v3.6.9"
[ -z "$REF" ] && REF="v4.3.0"
say "Release: $REF"
installed=0
@@ -154,11 +154,59 @@ if [ -d "$HOME/.config/fish" ]; then
fi
fi
# ---- optional tooling hints ----
say "Recommended tools (optional): curl nmap rustscan ffuf node npx typst"
for t in curl nmap rustscan ffuf node npx typst; do
command -v "$t" >/dev/null 2>&1 && ok "$t present" || warn "$t missing"
done
# ---- dependencies: detect, and install the ones a package manager can ----
# Core recon/report tools. Rust is already handled above. Auto-install is
# best-effort: it never fails the install, and is skipped with NEUROSPLOIT_NO_DEPS=1.
PKG=""; SUDO=""
if command -v apt-get >/dev/null 2>&1; then PKG="apt"
elif command -v brew >/dev/null 2>&1; then PKG="brew"
elif command -v dnf >/dev/null 2>&1; then PKG="dnf"
elif command -v pacman >/dev/null 2>&1; then PKG="pacman"
elif command -v apk >/dev/null 2>&1; then PKG="apk"; fi
[ "$(id -u)" = "0" ] || command -v sudo >/dev/null 2>&1 && [ "$(id -u)" != "0" ] && SUDO="sudo"
pkg_install() { # pkg_install <name...>
[ -n "$PKG" ] || { warn "no supported package manager — install manually: $*"; return 0; }
case "$PKG" in
apt) $SUDO apt-get update -qq >/dev/null 2>&1; $SUDO apt-get install -y "$@" >/dev/null 2>&1 ;;
brew) brew install "$@" >/dev/null 2>&1 ;;
dnf) $SUDO dnf install -y "$@" >/dev/null 2>&1 ;;
pacman) $SUDO pacman -Sy --noconfirm "$@" >/dev/null 2>&1 ;;
apk) $SUDO apk add "$@" >/dev/null 2>&1 ;;
esac
}
# Map a required command to its package name on the detected manager.
pkg_name() { # pkg_name <cmd>
case "$1" in
npx) echo "node" ;; # npx ships with node
node) [ "$PKG" = "apt" ] && echo "nodejs" || echo "node" ;;
*) echo "$1" ;;
esac
}
if [ "${NEUROSPLOIT_NO_DEPS:-0}" != "1" ]; then
say "Checking dependencies (install with the detected package manager: ${PKG:-none})…"
# Essential for the web console + recon; typst is for PDF reports (optional).
for t in curl git nmap node; do
if command -v "$t" >/dev/null 2>&1; then ok "$t present"; else
warn "$t missing — installing…"; pkg_install "$(pkg_name "$t")"
command -v "$t" >/dev/null 2>&1 && ok "$t installed" || warn "could not install $t automatically — install it manually"
fi
done
# Rust-cargo extras + report engine: install via cargo when present (no root).
if command -v cargo >/dev/null 2>&1; then
command -v rustscan >/dev/null 2>&1 || { say "installing rustscan (cargo)…"; cargo install rustscan >/dev/null 2>&1 && ok "rustscan installed" || warn "rustscan optional — skipped"; }
command -v typst >/dev/null 2>&1 || { say "installing typst (cargo)…"; cargo install typst-cli >/dev/null 2>&1 && ok "typst installed" || warn "typst optional (PDF reports) — skipped"; }
fi
# ffuf is Go; only hint it.
command -v ffuf >/dev/null 2>&1 && ok "ffuf present" || warn "ffuf optional (fuzzing) — install via your package manager or Go"
else
say "Dependency auto-install skipped (NEUROSPLOIT_NO_DEPS=1)."
for t in curl git nmap node rustscan ffuf typst; do
command -v "$t" >/dev/null 2>&1 && ok "$t present" || warn "$t missing"
done
fi
echo
ok "Installed. Open a NEW terminal — or activate now with:"
@@ -170,3 +218,16 @@ echo " neurosploit run http://testphp.vulnweb.com/ --subscription --model a
echo " neurosploit --help"
echo
warn "Update later: just re-run this script. Best runtime: Kali (docker run -it kalilinux/kali-rolling)."
# ---- optionally launch right away ----
# `curl | bash` has no TTY, so auto-run only when asked (NEUROSPLOIT_RUN=1) or
# when a real terminal is attached and the operator confirms.
RUN_NOW="${NEUROSPLOIT_RUN:-}"
if [ -z "$RUN_NOW" ] && [ -t 0 ]; then
printf '\n Launch the interactive session now? [y/N] '; read -r ans
case "$ans" in y|Y|yes) RUN_NOW=1 ;; esac
fi
if [ "$RUN_NOW" = "1" ]; then
say "Starting NeuroSploit…"
exec env NEUROSPLOIT_BASE="$DIR" "$DIR/neurosploit"
fi
+47 -5
View File
@@ -22,6 +22,7 @@ const state = {
selected: new Set(),
customLeads: [],
filter: 'all',
catFilter: '',
search: '',
expandedCats: new Set(),
providers: [],
@@ -212,6 +213,7 @@ async function loadAgents() {
function renderBoard() {
const root = $('#categories');
root.innerHTML = '';
populateCatFilter();
for (const group of state.categories) {
const selCount = group.agents.filter((a) => state.selected.has(a.id)).length;
const card = document.createElement('div');
@@ -297,7 +299,8 @@ function updateChips() {
function applyFilters() {
const q = state.search.trim().toLowerCase();
const narrowing = !!q || state.filter !== 'all';
const cat = state.catFilter || '';
const narrowing = !!q || state.filter !== 'all' || !!cat;
$$('.agent-row').forEach((row) => {
const isSel = state.selected.has(row.dataset.id);
let visible = true;
@@ -308,6 +311,7 @@ function applyFilters() {
});
let anyCardVisible = false;
$$('.cat-card').forEach((card) => {
if (cat && card.dataset.category !== cat) { card.style.display = 'none'; return; }
const shown = $$('.agent-row', card).filter((r) => !r.classList.contains('hidden-by-search'));
card.style.display = shown.length ? '' : 'none';
if (shown.length) anyCardVisible = true;
@@ -324,6 +328,20 @@ function applyFilters() {
show($('#leadsEmpty'), !anyCardVisible);
}
// One place to pick a category: the select lists every category (with its
// selected/total count); choosing one shows just that card, "All" resets.
function populateCatFilter() {
const sel = $('#catFilter');
if (!sel) return;
const cur = state.catFilter || '';
sel.innerHTML = '<option value="">All categories</option>' + state.categories.map((g) => {
const n = g.agents.filter((a) => state.selected.has(a.id)).length;
return `<option value="${esc(g.category)}">${esc(g.category)} (${n}/${g.agents.length})</option>`;
}).join('');
sel.value = cur;
}
$('#catFilter')?.addEventListener('change', (e) => { state.catFilter = e.target.value; applyFilters(); });
$$('.chip').forEach((chip) => chip.addEventListener('click', () => {
$$('.chip').forEach((c) => c.classList.remove('chip-active'));
chip.classList.add('chip-active');
@@ -1748,17 +1766,41 @@ async function refreshKeyStatus() {
<span class="p-kind">${p.kind === 'cli' ? 'cli+api' : 'api only'}</span>
<input type="password" data-provider="${esc(p.key)}" placeholder="${set ? '•••••••• (set — enter to replace)' : 'paste API key'}" />
<button class="btn btn-sm" data-save="${esc(p.key)}">Save</button>
<span class="key-verify" data-verify="${esc(p.key)}"></span>
</div>
`;
}).join('');
$$('[data-save]', root).forEach((btn) => btn.addEventListener('click', async () => {
const doSave = async (btn) => {
const provider = btn.dataset.save;
const input = root.querySelector(`input[data-provider="${provider}"]`);
const vtag = root.querySelector(`[data-verify="${provider}"]`);
const key = input.value.trim();
if (!key) return;
await api('/api/keys', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ provider, key }) });
input.value = '';
refreshKeyStatus();
btn.disabled = true; const label = btn.textContent; btn.textContent = 'Saving…';
if (vtag) { vtag.className = 'key-verify checking'; vtag.textContent = '· verifying…'; }
try {
// Save verifies the key server-side with a cheap authenticated call.
const r = await api('/api/keys', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ provider, key }) });
input.value = '';
const v = r.verify || { ok: null, message: 'saved' };
if (vtag) {
vtag.className = `key-verify ${v.ok === true ? 'ok' : v.ok === false ? 'bad' : 'unknown'}`;
vtag.textContent = v.ok === true ? '✓ valid' : v.ok === false ? '✗ ' + v.message : '• ' + v.message;
}
toast(v.ok === true ? `${provider}: key valid` : v.ok === false ? `${provider}: ${v.message}` : `${provider}: ${v.message}`, v.ok === false ? 'error' : 'ok');
// Refresh the set-dots but keep the verify tag we just set.
state.keys = await api('/api/keys').catch(() => state.keys);
const dot = root.querySelector(`[data-save="${provider}"]`)?.closest('.provider-row')?.querySelector('.dot');
if (dot) dot.classList.add('set');
} catch (e) {
if (vtag) { vtag.className = 'key-verify bad'; vtag.textContent = '✗ ' + e.message; }
} finally {
btn.disabled = false; btn.textContent = label;
}
};
$$('[data-save]', root).forEach((btn) => btn.addEventListener('click', () => doSave(btn)));
$$('input[data-provider]', root).forEach((inp) => inp.addEventListener('keydown', (e) => {
if (e.key === 'Enter') root.querySelector(`[data-save="${inp.dataset.provider}"]`)?.click();
}));
}
+4 -3
View File
@@ -170,6 +170,7 @@
<div class="section-desc">Toggle specific leads to test, or leave everything off to let recon-driven auto-selection choose.</div>
</div>
<div class="lead-toolbar">
<select class="input-sm" id="catFilter" title="Show one category"><option value="">All categories</option></select>
<div class="search-wrap">
<span class="search-icon" aria-hidden="true">⌕</span>
<input id="leadSearch" type="text" placeholder="Search leads by name or CWE" />
@@ -273,13 +274,13 @@
<div class="field-group"><label class="field-label" for="fieldIntercept">Intercepting proxy</label>
<select class="narrow" id="fieldIntercept">
<option value="off" selected>off · direct</option>
<option value="own">own interceptor (record + passive discovery)</option>
<option value="own">NeuroSploit Interceptor (record + passive discovery)</option>
<option value="burp">Burp Suite</option>
<option value="caido">Caido</option>
<option value="zap">OWASP ZAP</option>
<option value="mitmproxy">mitmproxy</option>
<option value="own+burp">own + Burp</option>
<option value="own+caido">own + Caido</option>
<option value="own+burp">NeuroSploit Interceptor + Burp</option>
<option value="own+caido">NeuroSploit Interceptor + Caido</option>
</select>
<div class="field-help">The harness and agent commands route through it. Full HTTPS interception needs one of the tools (own tunnels TLS).</div>
</div>
+9 -1
View File
@@ -363,8 +363,11 @@ a.btn:disabled, a.btn[aria-disabled="true"] { pointer-events: none; }
.field-error::before { content: "⚠ "; }
.field-group:has(.field-error:not([hidden])) input,
.field-group:has(.field-error:not([hidden])) textarea { border-color: var(--sev-critical-fg); }
.field-row { display: flex; gap: var(--sp-4); flex-wrap: wrap; }
.field-row { display: flex; gap: var(--sp-4); flex-wrap: wrap; align-items: flex-start; }
.field-row > * { flex: 1; min-width: 160px; }
/* inside a row the groups sit side by side — the stacked-group top margin would
push every group after the first one down and misalign the controls */
.field-row > .field-group + .field-group { margin-top: 0; }
.section-title { font-size: var(--fs-base); font-weight: 600; }
.section-desc { font-size: var(--fs-sm); color: var(--text-dim); margin-top: 2px; }
@@ -591,6 +594,11 @@ textarea { resize: vertical; min-height: 72px; }
.provider-row input { flex: 1; font-family: var(--mono); font-size: var(--fs-sm); }
.provider-row .dot { width: 7px; height: 7px; border-radius: 50%; background: var(--border-strong); flex: none; }
.provider-row .dot.set { background: var(--sev-low-fg); }
.key-verify { flex: none; font-size: var(--fs-2xs); font-weight: 700; min-width: 60px; }
.key-verify.ok { color: var(--sev-low-fg); }
.key-verify.bad { color: var(--sev-critical-fg); }
.key-verify.unknown { color: var(--text-faint); font-weight: 500; }
.key-verify.checking { color: var(--text-dim); font-weight: 500; }
/* ============================================================ Terminal dock
+52 -1
View File
@@ -144,6 +144,45 @@ const PROVIDERS = [
// In-memory only — never written to disk. Cleared on server restart.
const apiKeys = new Map(); // provider key -> secret
// A cheap authenticated endpoint per provider, to confirm a key works. GET a
// models/account list — a 200 means the key authenticates, 401/403 means it does
// not. Providers without a reachable check (local/proxy, or no public endpoint)
// return ok:null — the key is saved, just not auto-verified.
function keyProbe(provider, key) {
const bearer = { Authorization: `Bearer ${key}` };
switch (provider) {
case 'anthropic': return { url: 'https://api.anthropic.com/v1/models', headers: { 'x-api-key': key, 'anthropic-version': '2023-06-01' } };
case 'openai': return { url: 'https://api.openai.com/v1/models', headers: bearer };
case 'xai': return { url: 'https://api.x.ai/v1/models', headers: bearer };
case 'gemini': return { url: `https://generativelanguage.googleapis.com/v1beta/models?key=${encodeURIComponent(key)}`, headers: {} };
case 'deepseek': return { url: 'https://api.deepseek.com/models', headers: bearer };
case 'mistral': return { url: 'https://api.mistral.ai/v1/models', headers: bearer };
case 'groq': return { url: 'https://api.groq.com/openai/v1/models', headers: bearer };
case 'together': return { url: 'https://api.together.xyz/v1/models', headers: bearer };
case 'openrouter': return { url: 'https://openrouter.ai/api/v1/key', headers: bearer };
case 'moonshot': return { url: 'https://api.moonshot.ai/v1/models', headers: bearer };
case 'qwen': return { url: 'https://dashscope-intl.aliyuncs.com/compatible-mode/v1/models', headers: bearer };
case 'nvidia_nim': return { url: 'https://integrate.api.nvidia.com/v1/models', headers: bearer };
default: return null; // opencode, nous, zai, litellm, azure, ollama, llamacpp — not auto-verifiable here
}
}
async function verifyKey(provider, key) {
const probe = keyProbe(provider, key);
if (!probe) return { ok: null, message: 'saved — this provider is not auto-verifiable here' };
try {
const ctrl = new AbortController();
const timer = setTimeout(() => ctrl.abort(), 10000);
const r = await fetch(probe.url, { headers: probe.headers, signal: ctrl.signal }).finally(() => clearTimeout(timer));
if (r.ok) return { ok: true, message: 'key is valid' };
if (r.status === 401 || r.status === 403) return { ok: false, message: `key rejected (HTTP ${r.status})` };
if (r.status === 429) return { ok: true, message: 'key valid (rate-limited)' };
return { ok: null, message: `saved — provider returned HTTP ${r.status}` };
} catch (e) {
return { ok: null, message: `saved — could not reach provider (${e.name === 'AbortError' ? 'timeout' : e.message})` };
}
}
function envOverrides() {
const env = {};
for (const [key, secret] of apiKeys) {
@@ -1838,7 +1877,19 @@ const server = http.createServer(async (req, res) => {
}
if (body.key) apiKeys.set(body.provider, body.key);
else apiKeys.delete(body.provider);
return sendJson(res, 200, { ok: true });
// Save also verifies the key with a cheap authenticated call, so the
// operator knows it works before a run depends on it. The key is saved
// either way (some providers can't be auto-verified from here).
let verify = { ok: null, message: 'saved' };
if (body.key && body.verify !== false) verify = await verifyKey(body.provider, body.key);
return sendJson(res, 200, { ok: true, verify });
}
if (req.method === 'POST' && p === '/api/keys/verify') {
const body = await readBody(req);
const key = body.key || apiKeys.get(body.provider);
if (!body.provider || !PROVIDERS.some((pr) => pr.key === body.provider)) return sendJson(res, 400, { error: 'unknown provider' });
if (!key) return sendJson(res, 400, { error: 'no key to verify' });
return sendJson(res, 200, await verifyKey(body.provider, key));
}
m = p.match(/^\/api\/keys\/([^/]+)$/);
if (req.method === 'DELETE' && m) {