diff --git a/install.ps1 b/install.ps1 index 98da84b..99f03a1 100644 --- a/install.ps1 +++ b/install.ps1 @@ -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 } diff --git a/setup.bat b/setup.bat new file mode 100644 index 0000000..a868241 --- /dev/null +++ b/setup.bat @@ -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% diff --git a/setup.sh b/setup.sh index e61f3c2..2004a36 100755 --- a/setup.sh +++ b/setup.sh @@ -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 + [ -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 + 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 diff --git a/web/public/app.js b/web/public/app.js index 82ba037..d6993eb 100644 --- a/web/public/app.js +++ b/web/public/app.js @@ -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 = '' + state.categories.map((g) => { + const n = g.agents.filter((a) => state.selected.has(a.id)).length; + return ``; + }).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() { ${p.kind === 'cli' ? 'cli+api' : 'api only'} + `; }).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(); })); } diff --git a/web/public/index.html b/web/public/index.html index 5b4ac96..e41fd19 100644 --- a/web/public/index.html +++ b/web/public/index.html @@ -170,6 +170,7 @@
Toggle specific leads to test, or leave everything off to let recon-driven auto-selection choose.
+
@@ -273,13 +274,13 @@
The harness and agent commands route through it. Full HTTPS interception needs one of the tools (own tunnels TLS).
diff --git a/web/public/style.css b/web/public/style.css index b3d50d9..fcf90dc 100644 --- a/web/public/style.css +++ b/web/public/style.css @@ -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 diff --git a/web/server.js b/web/server.js index f31b65d..7cf5106 100644 --- a/web/server.js +++ b/web/server.js @@ -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) {