mirror of
https://github.com/CyberSecurityUP/NeuroSploit.git
synced 2026-10-18 21:47:32 +02:00
release: v4.3.2 — bulk collection import, full MCP server, web console overhaul
Collection import: - parse_path() auto-detects file vs directory; parse_dir() walks a folder of .json collections (Postman/OpenAPI/Swagger/HAR), merges and deduplicates endpoints across all files. walkdir (already a dep) now used in collection.rs. - CLI --collection, REPL /collection, pipeline injection, and web console all accept a directory path transparently. MCP server (neurosploit mcp): - Expanded from 8 to 24 tools: added whitebox, greybox, host, aitest, mobile, skills, pr, graph, audit, poc, traffic, assurance, agents, models, and import_collection. neurosploit_run gained --collection and --objective params. - Clients: Claude Code, Codex CLI, Cursor, Windsurf, VS Code — all documented in tool descriptions. Web console: - Full sidebar navigation: Dashboard, Pentests, PR Reviews, Issues, Repositories, Domains, Networks, Integrations, Settings. Chat removed. - switchView() navigation system with lazy-loading per page. - Pentests page: tabular list of all engagements with click-to-detail. - PR Reviews page: filtered view of PR review runs. - Issues page: aggregate findings across every run, filterable by severity. - Repositories page: connect/manage source repos (persisted in .neurosploit/). - Domains page: manage target domains for black-box scans. - Networks page: manage network ranges for infra scanning. - Integrations page: GitHub, GitLab, Jira, Slack, MCP status cards. - Settings page: default model, environment, TypeSafe, network, proxy config. - Server: CRUD API endpoints for repos/domains/networks/integrations/settings with JSON file persistence in .neurosploit/ directory. - New CSS: nav items, config cards, data tables, stat cards, integration cards, form modals, accent buttons. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UqW6BbQgqGkEDn3RYuCVoM
This commit is contained in:
1 parent
e8b7f61890
commit
71339a9ddb
11 files changed
+1072
-93
No files matched your search
Generated
+2
-2
@@ -940,7 +940,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "neurosploit"
|
||||
version = "4.3.1"
|
||||
version = "4.3.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
@@ -957,7 +957,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "neurosploit-harness"
|
||||
version = "4.3.1"
|
||||
version = "4.3.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64",
|
||||
|
||||
@@ -3,7 +3,7 @@ members = ["crates/harness", "app"]
|
||||
resolver = "2"
|
||||
|
||||
[workspace.package]
|
||||
version = "4.3.1"
|
||||
version = "4.3.2"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
repository = "https://github.com/JoasASantos/NeuroSploit"
|
||||
|
||||
@@ -13,8 +13,8 @@ use std::path::{Path, PathBuf};
|
||||
#[command(
|
||||
name = "neurosploit",
|
||||
version,
|
||||
about = "NeuroSploit v4.3.1 — multi-model autonomous pentest harness",
|
||||
long_about = "NeuroSploit v4.3.1 — a Rust multi-model harness that drives a pool of LLMs \
|
||||
about = "NeuroSploit v4.3.2 — multi-model autonomous pentest harness",
|
||||
long_about = "NeuroSploit v4.3.2 — a Rust multi-model harness that drives a pool of LLMs \
|
||||
(API key or local subscription: Claude/Codex/Gemini/Grok/OpenCode/Hermes) 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\
|
||||
@@ -2252,9 +2252,10 @@ fn apply_quick(cfg: &mut RunConfig) {
|
||||
/// sees how many endpoints were seeded.
|
||||
fn apply_collection(cfg: &mut RunConfig, path: Option<&str>) -> anyhow::Result<()> {
|
||||
let Some(path) = path.filter(|p| !p.trim().is_empty()) else { return Ok(()) };
|
||||
let eps = harness::collection::parse_file(path)
|
||||
let eps = harness::collection::parse_path(path)
|
||||
.map_err(|e| anyhow::anyhow!("--collection {path}: {e}"))?;
|
||||
println!(" \x1b[2mcollection: imported {} endpoint(s) from {} — tested as known surface\x1b[0m", eps.len(), path);
|
||||
let kind = if std::path::Path::new(path).is_dir() { "directory" } else { "file" };
|
||||
println!(" \x1b[2mcollection: imported {} endpoint(s) from {kind} {} — tested as known surface\x1b[0m", eps.len(), path);
|
||||
cfg.api_collection = Some(path.to_string());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
+431
-50
@@ -1,19 +1,36 @@
|
||||
//! NeuroSploit as an MCP server.
|
||||
//!
|
||||
//! Speaks the Model Context Protocol over stdio (JSON-RPC 2.0, line-delimited),
|
||||
//! so Claude Code, Codex, Cursor and any MCP client can drive NeuroSploit as a
|
||||
//! set of tools. It does not re-implement the harness: each tool shells out to
|
||||
//! the same `neurosploit` binary the operator already uses, so behaviour,
|
||||
//! authorization and safety are identical to the CLI.
|
||||
//! so Claude Code, Codex, Cursor, Windsurf and any MCP client can drive
|
||||
//! NeuroSploit as a set of tools. It does not re-implement the harness: each
|
||||
//! tool shells out to the same `neurosploit` binary the operator already uses,
|
||||
//! so behaviour, authorization and safety are identical to the CLI.
|
||||
//!
|
||||
//! Exposed tools:
|
||||
//! - `neurosploit_run` launch an engagement (returns the run id + summary)
|
||||
//! - `neurosploit_list_runs` list finished runs
|
||||
//! - `neurosploit_findings` read a run's findings.json
|
||||
//! - `neurosploit_report` read a run's markdown report
|
||||
//! - `neurosploit_rebuild` rebuild a run's report from its findings
|
||||
//! - `neurosploit_internal` internal/AD attack-graph analysis
|
||||
//! - `neurosploit_compliance` map a run onto PCI-DSS / HIPAA / SOC 2
|
||||
//! - neurosploit_run launch a black-box engagement
|
||||
//! - neurosploit_whitebox white-box source review
|
||||
//! - neurosploit_greybox grey-box (source + running app)
|
||||
//! - neurosploit_host infra/host scan (Linux/Windows/AD)
|
||||
//! - neurosploit_aitest AI/LLM red-teaming (OWASP LLM Top 10)
|
||||
//! - neurosploit_mobile mobile binary analysis (APK/IPA)
|
||||
//! - neurosploit_container OCI container scan + SBOM
|
||||
//! - neurosploit_skills skill/plugin/n8n audit
|
||||
//! - neurosploit_pr GitHub PR security review
|
||||
//! - neurosploit_internal internal/AD attack-graph analysis
|
||||
//! - neurosploit_list_runs list finished runs
|
||||
//! - neurosploit_findings read a run's findings.json
|
||||
//! - neurosploit_report read a run's markdown report
|
||||
//! - neurosploit_rebuild rebuild report from findings
|
||||
//! - neurosploit_sarif emit SARIF 2.1.0
|
||||
//! - neurosploit_compliance map findings onto PCI-DSS/HIPAA/SOC2
|
||||
//! - neurosploit_graph attack graph (web + internal)
|
||||
//! - neurosploit_audit verify audit trail
|
||||
//! - neurosploit_poc re-validate PoCs
|
||||
//! - neurosploit_traffic export HTTP traffic
|
||||
//! - neurosploit_assurance assurance bundle
|
||||
//! - neurosploit_agents list the agent library
|
||||
//! - neurosploit_models list providers and models
|
||||
//! - neurosploit_import_collection validate/preview a collection (file or dir)
|
||||
//!
|
||||
//! Kept dependency-free: JSON-RPC framing is hand-rolled over stdin/stdout with
|
||||
//! serde_json, and tools run via std::process::Command.
|
||||
@@ -37,12 +54,11 @@ pub fn serve() -> anyhow::Result<()> {
|
||||
};
|
||||
let req: Value = match serde_json::from_str(&line) {
|
||||
Ok(v) => v,
|
||||
Err(_) => continue, // ignore malformed frames
|
||||
Err(_) => continue,
|
||||
};
|
||||
let id = req.get("id").cloned();
|
||||
let method = req.get("method").and_then(|m| m.as_str()).unwrap_or("");
|
||||
|
||||
// Notifications (no id) get no response.
|
||||
let response = match method {
|
||||
"initialize" => Some(ok(id, json!({
|
||||
"protocolVersion": PROTOCOL_VERSION,
|
||||
@@ -73,63 +89,372 @@ fn err(id: Option<Value>, code: i64, msg: &str) -> Value {
|
||||
json!({ "jsonrpc": "2.0", "id": id, "error": { "code": code, "message": msg } })
|
||||
}
|
||||
|
||||
/// The tool catalogue advertised to the client.
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tool catalogue
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn tool_list() -> Value {
|
||||
json!([
|
||||
{
|
||||
"name": "neurosploit_run",
|
||||
"description": "Launch a NeuroSploit engagement against an authorized target. Black-box by default. Returns the run id, the finding summary, and the report path. Only test targets you are authorized to test.",
|
||||
"description": "Launch a NeuroSploit black-box engagement against an authorized target. Returns the run id, finding summary, and report path. Only test targets you are authorized to test.",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"target": { "type": "string", "description": "URL or host, e.g. https://app.example.com" },
|
||||
"mode": { "type": "string", "enum": ["run","whitebox","greybox","host"], "description": "Engagement mode (default run)" },
|
||||
"model": { "type": "string", "description": "provider:model, e.g. anthropic:claude-opus-4-8" },
|
||||
"target": { "type": "string", "description": "URL or host, e.g. https://app.example.com" },
|
||||
"model": { "type": "string", "description": "provider:model, e.g. anthropic:claude-opus-4-8" },
|
||||
"subscription": { "type": "boolean", "description": "Use the local CLI login instead of an API key" },
|
||||
"focus": { "type": "string", "description": "What to prioritise" },
|
||||
"scope_file": { "type": "string", "description": "Path to a scope YAML (hard boundary)" },
|
||||
"sandbox": { "type": "boolean", "description": "Run tool commands in the Kali container" },
|
||||
"typesafe": { "type": "string", "enum": ["on","off","auto"] },
|
||||
"max_agents": { "type": "integer" }
|
||||
"focus": { "type": "string", "description": "What to prioritise" },
|
||||
"objective": { "type": "string", "description": "Engagement goal / context" },
|
||||
"scope_file": { "type": "string", "description": "Path to a scope YAML (hard boundary)" },
|
||||
"collection": { "type": "string", "description": "API collection file or directory (Postman/OpenAPI/HAR, JSON)" },
|
||||
"sandbox": { "type": "boolean", "description": "Run tool commands in the Kali container" },
|
||||
"typesafe": { "type": "string", "enum": ["on","off","auto"] },
|
||||
"max_agents": { "type": "integer" },
|
||||
"quick": { "type": "boolean", "description": "Economy preset: light recon, fewer agents, lower cost" }
|
||||
},
|
||||
"required": ["target"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "neurosploit_whitebox",
|
||||
"description": "White-box: analyse a repository's source code for vulnerabilities. Accepts a local path, GitHub URL, or owner/repo shorthand.",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": { "type": "string", "description": "Local path, GitHub URL, or owner/repo" },
|
||||
"model": { "type": "string" },
|
||||
"subscription": { "type": "boolean" },
|
||||
"focus": { "type": "string" },
|
||||
"research": { "type": "boolean", "description": "Hunt novel CVE-reportable bugs" },
|
||||
"max_agents": { "type": "integer" },
|
||||
"quick": { "type": "boolean" }
|
||||
},
|
||||
"required": ["path"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "neurosploit_greybox",
|
||||
"description": "Greybox: review source AND exploit the running app together.",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"repo": { "type": "string", "description": "Source repo (local path, GitHub URL, or owner/repo)" },
|
||||
"url": { "type": "string", "description": "URL of the running application" },
|
||||
"model": { "type": "string" },
|
||||
"subscription": { "type": "boolean" },
|
||||
"creds": { "type": "string", "description": "Credentials YAML path" },
|
||||
"focus": { "type": "string" },
|
||||
"collection": { "type": "string", "description": "API collection file or directory" },
|
||||
"research": { "type": "boolean" },
|
||||
"max_agents": { "type": "integer" },
|
||||
"quick": { "type": "boolean" }
|
||||
},
|
||||
"required": ["repo", "url"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "neurosploit_host",
|
||||
"description": "Infra/host: scan an IP/host and run Linux/Windows/AD agents. SSH/Windows credentials come from --creds.",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"target": { "type": "string", "description": "IP or hostname" },
|
||||
"model": { "type": "string" },
|
||||
"subscription": { "type": "boolean" },
|
||||
"creds": { "type": "string", "description": "Credentials YAML (ssh/windows/ad blocks)" },
|
||||
"focus": { "type": "string" },
|
||||
"max_agents": { "type": "integer" }
|
||||
},
|
||||
"required": ["target"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "neurosploit_aitest",
|
||||
"description": "AI/LLM red-team: test a live AI agent / LLM app / MCP endpoint (OWASP LLM Top 10 + MCP risks).",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"url": { "type": "string", "description": "URL of the AI agent / LLM endpoint" },
|
||||
"model": { "type": "string" },
|
||||
"subscription": { "type": "boolean" },
|
||||
"auth": { "type": "string", "description": "Auth header for the AI endpoint" },
|
||||
"focus": { "type": "string" },
|
||||
"max_agents": { "type": "integer" }
|
||||
},
|
||||
"required": ["url"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "neurosploit_mobile",
|
||||
"description": "Mobile/binary: analyse a local artifact (APK/IPA/binary) with mobile RE agents (Ghidra, MobSF, Frida, apktool/jadx).",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": { "type": "string", "description": "Path to the artifact (.apk/.ipa/binary)" },
|
||||
"model": { "type": "string" },
|
||||
"subscription": { "type": "boolean" },
|
||||
"focus": { "type": "string" },
|
||||
"max_agents": { "type": "integer" }
|
||||
},
|
||||
"required": ["path"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "neurosploit_container",
|
||||
"description": "Scan an OCI container image (repo:tag / tar / Dockerfile) for vulnerable packages, secrets, misconfig and emit an SBOM.",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"image": { "type": "string", "description": "Image reference, local tar, or Dockerfile path" },
|
||||
"model": { "type": "string" },
|
||||
"subscription": { "type": "boolean" },
|
||||
"focus": { "type": "string" }
|
||||
},
|
||||
"required": ["image"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "neurosploit_skills",
|
||||
"description": "Audit AI skills/plugins or exported n8n workflows (white-box .md/.json file or folder).",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": { "type": "string", "description": "Path to a skill/plugin/n8n file or folder" },
|
||||
"model": { "type": "string" },
|
||||
"subscription": { "type": "boolean" }
|
||||
},
|
||||
"required": ["path"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "neurosploit_pr",
|
||||
"description": "Review a GitHub Pull Request's code for security vulnerabilities. Optionally comments back on the PR.",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"repo": { "type": "string", "description": "owner/repo or GitHub URL" },
|
||||
"number": { "type": "integer", "description": "Pull request number" },
|
||||
"model": { "type": "string" },
|
||||
"subscription": { "type": "boolean" },
|
||||
"comment": { "type": "boolean", "description": "Post a summary comment back on the PR" },
|
||||
"fail_on": { "type": "string", "enum": ["critical","high","medium","low"], "description": "Block PR at this severity or worse" },
|
||||
"jira": { "type": "boolean", "description": "Open a Jira card per finding" }
|
||||
},
|
||||
"required": ["repo", "number"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "neurosploit_internal",
|
||||
"description": "Internal-network / Active Directory attack-graph analysis: paths to crown jewels and the choke point to fix first.",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"graph": { "type": "string", "description": "Path to a graph JSON" },
|
||||
"scaffold": { "type": "string", "description": "Domain to scaffold, e.g. corp.local" },
|
||||
"from": { "type": "string", "description": "Foothold node id" }
|
||||
}
|
||||
}
|
||||
},
|
||||
{ "name": "neurosploit_list_runs", "description": "List finished NeuroSploit runs (ids and targets).", "inputSchema": { "type": "object", "properties": {} } },
|
||||
{ "name": "neurosploit_findings", "description": "Read a finished run's findings as JSON.", "inputSchema": { "type": "object", "properties": { "run": { "type": "string", "description": "Run id or path" } }, "required": ["run"] } },
|
||||
{ "name": "neurosploit_report", "description": "Read a finished run's Markdown report.", "inputSchema": { "type": "object", "properties": { "run": { "type": "string" } }, "required": ["run"] } },
|
||||
{ "name": "neurosploit_rebuild", "description": "Rebuild a run's report artifacts from its findings (no model calls).", "inputSchema": { "type": "object", "properties": { "run": { "type": "string" } }, "required": ["run"] } },
|
||||
{ "name": "neurosploit_sarif", "description": "Emit SARIF 2.1.0 for a finished run (report.sarif) so CI code-scanning can ingest the findings.", "inputSchema": { "type": "object", "properties": { "run": { "type": "string" } }, "required": ["run"] } },
|
||||
{ "name": "neurosploit_internal", "description": "Internal-network / Active Directory attack-graph analysis: paths to crown jewels and the choke point to fix first.", "inputSchema": { "type": "object", "properties": { "graph": { "type": "string", "description": "Path to a graph JSON" }, "scaffold": { "type": "string", "description": "Domain to scaffold, e.g. corp.local" }, "from": { "type": "string", "description": "Foothold node id" } } } },
|
||||
{ "name": "neurosploit_sarif", "description": "Emit SARIF 2.1.0 for a finished run so CI code-scanning can ingest the findings.", "inputSchema": { "type": "object", "properties": { "run": { "type": "string" } }, "required": ["run"] } },
|
||||
{ "name": "neurosploit_compliance", "description": "Map a finished run's findings onto PCI-DSS, HIPAA or SOC 2 controls.", "inputSchema": { "type": "object", "properties": { "run": { "type": "string" }, "framework": { "type": "string", "enum": ["pci-dss","hipaa","soc2"] } }, "required": ["run"] } },
|
||||
{ "name": "neurosploit_container", "description": "Scan an OCI container image (repo:tag / tar / Dockerfile) for vulnerable packages, secrets, misconfig and emit an SBOM.", "inputSchema": { "type": "object", "properties": { "image": { "type": "string" }, "model": { "type": "string" }, "subscription": { "type": "boolean" } }, "required": ["image"] } }
|
||||
{
|
||||
"name": "neurosploit_graph",
|
||||
"description": "Attack graph for a finished run: attack chains with severity, next moves, Cypher export. With --push merges into Neo4j.",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"run": { "type": "string", "description": "Run id or path" },
|
||||
"internal": { "type": "string", "description": "Internal-network graph JSON to merge" },
|
||||
"push": { "type": "boolean", "description": "Push to Neo4j" },
|
||||
"top": { "type": "integer", "description": "How many chains to print (default 5)" }
|
||||
},
|
||||
"required": ["run"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "neurosploit_audit",
|
||||
"description": "Verify a finished run's audit trail — the hash chain and, with --anchor, the signed anchors.",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"run": { "type": "string" },
|
||||
"anchor": { "type": "boolean", "description": "Also verify anchors (P4)" }
|
||||
},
|
||||
"required": ["run"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "neurosploit_poc",
|
||||
"description": "Re-validate a finished run's PoCs: re-run each finding's proof and report which still reproduce.",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"run": { "type": "string" },
|
||||
"repeats": { "type": "integer", "description": "Re-runs per finding (default 2)" },
|
||||
"apply": { "type": "boolean", "description": "Write demoted findings back" },
|
||||
"only": { "type": "string", "description": "Re-test only this finding id" }
|
||||
},
|
||||
"required": ["run"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "neurosploit_traffic",
|
||||
"description": "Export a run's archived HTTP traffic as a .http file for external inspection.",
|
||||
"inputSchema": { "type": "object", "properties": { "run": { "type": "string" } }, "required": ["run"] }
|
||||
},
|
||||
{
|
||||
"name": "neurosploit_assurance",
|
||||
"description": "Assemble or verify a run's assurance bundle (P1-P5 manifest).",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"run": { "type": "string" },
|
||||
"verify": { "type": "boolean", "description": "Verify instead of assembling" }
|
||||
},
|
||||
"required": ["run"]
|
||||
}
|
||||
},
|
||||
{ "name": "neurosploit_agents", "description": "List the NeuroSploit agent library (names, categories, counts).", "inputSchema": { "type": "object", "properties": {} } },
|
||||
{ "name": "neurosploit_models", "description": "List available providers and models.", "inputSchema": { "type": "object", "properties": {} } },
|
||||
{
|
||||
"name": "neurosploit_import_collection",
|
||||
"description": "Validate and preview an API collection (Postman/OpenAPI/Swagger/HAR). Accepts a single JSON file or a directory of .json files. Returns the parsed endpoint list.",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": { "type": "string", "description": "Path to a collection file or a directory of .json collection files" }
|
||||
},
|
||||
"required": ["path"]
|
||||
}
|
||||
}
|
||||
])
|
||||
}
|
||||
|
||||
/// Dispatch a `tools/call`.
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tool dispatch
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn handle_call(id: Option<Value>, req: &Value, exe: &std::path::Path) -> Value {
|
||||
let params = req.get("params").cloned().unwrap_or(json!({}));
|
||||
let name = params.get("name").and_then(|n| n.as_str()).unwrap_or("");
|
||||
let a = params.get("arguments").cloned().unwrap_or(json!({}));
|
||||
let s = |k: &str| a.get(k).and_then(|v| v.as_str()).map(|x| x.to_string());
|
||||
let b = |k: &str| a.get(k).and_then(|v| v.as_bool()).unwrap_or(false);
|
||||
let i = |k: &str| a.get(k).and_then(|v| v.as_i64());
|
||||
|
||||
let mut argv: Vec<String> = Vec::new();
|
||||
match name {
|
||||
// ---- engagements ----------------------------------------------------
|
||||
"neurosploit_run" => {
|
||||
let Some(target) = s("target") else { return tool_err(id, "target is required") };
|
||||
argv.push(s("mode").unwrap_or_else(|| "run".into()));
|
||||
argv.push(target);
|
||||
argv.push("run".into()); argv.push(target);
|
||||
if let Some(m) = s("model") { argv.push("--model".into()); argv.push(m); }
|
||||
if b("subscription") { argv.push("--subscription".into()); }
|
||||
if let Some(f) = s("focus") { argv.push("--focus".into()); argv.push(f); }
|
||||
if let Some(o) = s("objective") { argv.push("--objective".into()); argv.push(o); }
|
||||
if let Some(sf) = s("scope_file") { argv.push("--scope-file".into()); argv.push(sf); }
|
||||
if let Some(c) = s("collection") { argv.push("--collection".into()); argv.push(c); }
|
||||
if b("sandbox") { argv.push("--sandbox".into()); }
|
||||
if let Some(ts) = s("typesafe") { argv.push("--typesafe".into()); argv.push(ts); }
|
||||
if let Some(n) = a.get("max_agents").and_then(|v| v.as_i64()) { argv.push("--max-agents".into()); argv.push(n.to_string()); }
|
||||
if let Some(n) = i("max_agents") { argv.push("--max-agents".into()); argv.push(n.to_string()); }
|
||||
if b("quick") { argv.push("--quick".into()); }
|
||||
argv.push("-v".into());
|
||||
}
|
||||
"neurosploit_list_runs" => { argv.push("runs".into()); }
|
||||
"neurosploit_whitebox" => {
|
||||
let Some(path) = s("path") else { return tool_err(id, "path is required") };
|
||||
argv.push("whitebox".into()); argv.push(path);
|
||||
if let Some(m) = s("model") { argv.push("--model".into()); argv.push(m); }
|
||||
if b("subscription") { argv.push("--subscription".into()); }
|
||||
if let Some(f) = s("focus") { argv.push("--focus".into()); argv.push(f); }
|
||||
if b("research") { argv.push("--research".into()); }
|
||||
if let Some(n) = i("max_agents") { argv.push("--max-agents".into()); argv.push(n.to_string()); }
|
||||
if b("quick") { argv.push("--quick".into()); }
|
||||
argv.push("-v".into());
|
||||
}
|
||||
"neurosploit_greybox" => {
|
||||
let Some(repo) = s("repo") else { return tool_err(id, "repo is required") };
|
||||
let Some(url) = s("url") else { return tool_err(id, "url is required") };
|
||||
argv.push("greybox".into()); argv.push(repo);
|
||||
argv.push("--url".into()); argv.push(url);
|
||||
if let Some(m) = s("model") { argv.push("--model".into()); argv.push(m); }
|
||||
if b("subscription") { argv.push("--subscription".into()); }
|
||||
if let Some(c) = s("creds") { argv.push("--creds".into()); argv.push(c); }
|
||||
if let Some(f) = s("focus") { argv.push("--focus".into()); argv.push(f); }
|
||||
if let Some(col) = s("collection") { argv.push("--collection".into()); argv.push(col); }
|
||||
if b("research") { argv.push("--research".into()); }
|
||||
if let Some(n) = i("max_agents") { argv.push("--max-agents".into()); argv.push(n.to_string()); }
|
||||
if b("quick") { argv.push("--quick".into()); }
|
||||
argv.push("-v".into());
|
||||
}
|
||||
"neurosploit_host" => {
|
||||
let Some(target) = s("target") else { return tool_err(id, "target is required") };
|
||||
argv.push("host".into()); argv.push(target);
|
||||
if let Some(m) = s("model") { argv.push("--model".into()); argv.push(m); }
|
||||
if b("subscription") { argv.push("--subscription".into()); }
|
||||
if let Some(c) = s("creds") { argv.push("--creds".into()); argv.push(c); }
|
||||
if let Some(f) = s("focus") { argv.push("--focus".into()); argv.push(f); }
|
||||
if let Some(n) = i("max_agents") { argv.push("--max-agents".into()); argv.push(n.to_string()); }
|
||||
argv.push("-v".into());
|
||||
}
|
||||
"neurosploit_aitest" => {
|
||||
let Some(url) = s("url") else { return tool_err(id, "url is required") };
|
||||
argv.push("aitest".into()); argv.push(url);
|
||||
if let Some(m) = s("model") { argv.push("--model".into()); argv.push(m); }
|
||||
if b("subscription") { argv.push("--subscription".into()); }
|
||||
if let Some(auth) = s("auth") { argv.push("--auth".into()); argv.push(auth); }
|
||||
if let Some(f) = s("focus") { argv.push("--focus".into()); argv.push(f); }
|
||||
if let Some(n) = i("max_agents") { argv.push("--max-agents".into()); argv.push(n.to_string()); }
|
||||
argv.push("-v".into());
|
||||
}
|
||||
"neurosploit_mobile" => {
|
||||
let Some(path) = s("path") else { return tool_err(id, "path is required") };
|
||||
argv.push("mobile".into()); argv.push(path);
|
||||
if let Some(m) = s("model") { argv.push("--model".into()); argv.push(m); }
|
||||
if b("subscription") { argv.push("--subscription".into()); }
|
||||
if let Some(f) = s("focus") { argv.push("--focus".into()); argv.push(f); }
|
||||
if let Some(n) = i("max_agents") { argv.push("--max-agents".into()); argv.push(n.to_string()); }
|
||||
argv.push("-v".into());
|
||||
}
|
||||
"neurosploit_container" => {
|
||||
let Some(image) = s("image") else { return tool_err(id, "image is required") };
|
||||
argv.push("container".into()); argv.push(image);
|
||||
if let Some(m) = s("model") { argv.push("--model".into()); argv.push(m); }
|
||||
if b("subscription") { argv.push("--subscription".into()); }
|
||||
if let Some(f) = s("focus") { argv.push("--focus".into()); argv.push(f); }
|
||||
argv.push("-v".into());
|
||||
}
|
||||
"neurosploit_skills" => {
|
||||
let Some(path) = s("path") else { return tool_err(id, "path is required") };
|
||||
argv.push("skills".into()); argv.push(path);
|
||||
if let Some(m) = s("model") { argv.push("--model".into()); argv.push(m); }
|
||||
if b("subscription") { argv.push("--subscription".into()); }
|
||||
argv.push("-v".into());
|
||||
}
|
||||
"neurosploit_pr" => {
|
||||
let Some(repo) = s("repo") else { return tool_err(id, "repo is required") };
|
||||
let Some(number) = i("number") else { return tool_err(id, "number is required") };
|
||||
argv.push("pr".into()); argv.push(repo); argv.push(number.to_string());
|
||||
if let Some(m) = s("model") { argv.push("--model".into()); argv.push(m); }
|
||||
if b("subscription") { argv.push("--subscription".into()); }
|
||||
if b("comment") { argv.push("--comment".into()); }
|
||||
if let Some(fo) = s("fail_on") { argv.push("--fail-on".into()); argv.push(fo); }
|
||||
if b("jira") { argv.push("--jira".into()); }
|
||||
argv.push("-v".into());
|
||||
}
|
||||
"neurosploit_internal" => {
|
||||
argv.push("internal".into());
|
||||
if let Some(g) = s("graph") { argv.push("--graph".into()); argv.push(g); }
|
||||
if let Some(sc) = s("scaffold") { argv.push("--scaffold".into()); argv.push(sc); }
|
||||
if let Some(fr) = s("from") { argv.push("--from".into()); argv.push(fr); }
|
||||
}
|
||||
|
||||
// ---- post-run tools -------------------------------------------------
|
||||
"neurosploit_list_runs" => return list_runs(id),
|
||||
"neurosploit_findings" => {
|
||||
let Some(run) = s("run") else { return tool_err(id, "run is required") };
|
||||
return read_run_file(id, &run, "findings.json");
|
||||
@@ -138,35 +463,71 @@ fn handle_call(id: Option<Value>, req: &Value, exe: &std::path::Path) -> Value {
|
||||
let Some(run) = s("run") else { return tool_err(id, "run is required") };
|
||||
return read_run_file(id, &run, "report.md");
|
||||
}
|
||||
"neurosploit_rebuild" => { let Some(run) = s("run") else { return tool_err(id, "run is required") }; argv.push("rebuild".into()); argv.push(run); }
|
||||
"neurosploit_sarif" => { let Some(run) = s("run") else { return tool_err(id, "run is required") }; argv.push("sarif".into()); argv.push(run); }
|
||||
"neurosploit_internal" => {
|
||||
argv.push("internal".into());
|
||||
if let Some(g) = s("graph") { argv.push("--graph".into()); argv.push(g); }
|
||||
if let Some(sc) = s("scaffold") { argv.push("--scaffold".into()); argv.push(sc); }
|
||||
if let Some(fr) = s("from") { argv.push("--from".into()); argv.push(fr); }
|
||||
"neurosploit_rebuild" => {
|
||||
let Some(run) = s("run") else { return tool_err(id, "run is required") };
|
||||
argv.push("rebuild".into()); argv.push(run);
|
||||
}
|
||||
"neurosploit_container" => {
|
||||
let Some(image) = s("image") else { return tool_err(id, "image is required") };
|
||||
argv.push("container".into()); argv.push(image);
|
||||
if let Some(m) = s("model") { argv.push("--model".into()); argv.push(m); }
|
||||
if b("subscription") { argv.push("--subscription".into()); }
|
||||
argv.push("-v".into());
|
||||
"neurosploit_sarif" => {
|
||||
let Some(run) = s("run") else { return tool_err(id, "run is required") };
|
||||
argv.push("sarif".into()); argv.push(run);
|
||||
}
|
||||
"neurosploit_compliance" => {
|
||||
let Some(run) = s("run") else { return tool_err(id, "run is required") };
|
||||
argv.push("compliance".into()); argv.push(run);
|
||||
if let Some(fw) = s("framework") { argv.push("--framework".into()); argv.push(fw); }
|
||||
}
|
||||
"neurosploit_graph" => {
|
||||
let Some(run) = s("run") else { return tool_err(id, "run is required") };
|
||||
argv.push("graph".into()); argv.push(run);
|
||||
if let Some(ig) = s("internal") { argv.push("--internal".into()); argv.push(ig); }
|
||||
if b("push") { argv.push("--push".into()); }
|
||||
if let Some(t) = i("top") { argv.push("--top".into()); argv.push(t.to_string()); }
|
||||
}
|
||||
"neurosploit_audit" => {
|
||||
let Some(run) = s("run") else { return tool_err(id, "run is required") };
|
||||
argv.push("audit".into()); argv.push(run);
|
||||
if b("anchor") { argv.push("--anchor".into()); }
|
||||
}
|
||||
"neurosploit_poc" => {
|
||||
let Some(run) = s("run") else { return tool_err(id, "run is required") };
|
||||
argv.push("poc".into()); argv.push(run);
|
||||
if let Some(r) = i("repeats") { argv.push("--repeats".into()); argv.push(r.to_string()); }
|
||||
if b("apply") { argv.push("--apply".into()); }
|
||||
if let Some(o) = s("only") { argv.push("--only".into()); argv.push(o); }
|
||||
argv.push("--json".into());
|
||||
}
|
||||
"neurosploit_traffic" => {
|
||||
let Some(run) = s("run") else { return tool_err(id, "run is required") };
|
||||
argv.push("traffic".into()); argv.push(run);
|
||||
}
|
||||
"neurosploit_assurance" => {
|
||||
let Some(run) = s("run") else { return tool_err(id, "run is required") };
|
||||
argv.push("assurance".into()); argv.push(run);
|
||||
if b("verify") { argv.push("--verify".into()); }
|
||||
}
|
||||
|
||||
// ---- info -----------------------------------------------------------
|
||||
"neurosploit_agents" => { argv.push("agents".into()); }
|
||||
"neurosploit_models" => { argv.push("models".into()); }
|
||||
|
||||
// ---- collection preview ---------------------------------------------
|
||||
"neurosploit_import_collection" => {
|
||||
let Some(path) = s("path") else { return tool_err(id, "path is required") };
|
||||
return import_collection(id, &path);
|
||||
}
|
||||
|
||||
_ => return tool_err(id, &format!("unknown tool: {name}")),
|
||||
}
|
||||
|
||||
// `runs` subcommand doesn't exist as a bare CLI verb; list from the runs dir.
|
||||
if name == "neurosploit_list_runs" {
|
||||
return list_runs(id);
|
||||
}
|
||||
run_cli(id, exe, &argv)
|
||||
}
|
||||
|
||||
let out = std::process::Command::new(exe).args(&argv).output();
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn run_cli(id: Option<Value>, exe: &std::path::Path, argv: &[String]) -> Value {
|
||||
let out = std::process::Command::new(exe).args(argv).output();
|
||||
match out {
|
||||
Ok(o) => {
|
||||
let mut text = String::from_utf8_lossy(&o.stdout).to_string();
|
||||
@@ -181,6 +542,27 @@ fn handle_call(id: Option<Value>, req: &Value, exe: &std::path::Path) -> Value {
|
||||
}
|
||||
}
|
||||
|
||||
fn import_collection(id: Option<Value>, path: &str) -> Value {
|
||||
match harness::collection::parse_path(path) {
|
||||
Ok(eps) => {
|
||||
let kind = if std::path::Path::new(path).is_dir() { "directory" } else { "file" };
|
||||
let mut lines = vec![format!("Parsed {kind} {path}: {} endpoint(s)\n", eps.len())];
|
||||
for (i, ep) in eps.iter().enumerate().take(200) {
|
||||
if ep.note.is_empty() {
|
||||
lines.push(format!(" {}. {} {}", i + 1, ep.method, ep.url));
|
||||
} else {
|
||||
lines.push(format!(" {}. {} {} ({})", i + 1, ep.method, ep.url, ep.note));
|
||||
}
|
||||
}
|
||||
if eps.len() > 200 {
|
||||
lines.push(format!(" … and {} more", eps.len() - 200));
|
||||
}
|
||||
tool_text(id, &lines.join("\n"))
|
||||
}
|
||||
Err(e) => tool_err(id, &format!("{e}")),
|
||||
}
|
||||
}
|
||||
|
||||
fn read_run_file(id: Option<Value>, run: &str, file: &str) -> Value {
|
||||
let base = std::env::current_dir().unwrap_or_default();
|
||||
let dir = if std::path::Path::new(run).is_dir() { std::path::PathBuf::from(run) } else { base.join("runs").join(run) };
|
||||
@@ -201,7 +583,6 @@ fn list_runs(id: Option<Value>) -> Value {
|
||||
}
|
||||
|
||||
fn tool_text(id: Option<Value>, text: &str) -> Value {
|
||||
// Cap the payload so a huge report doesn't blow the client's context.
|
||||
let capped: String = text.chars().take(60_000).collect();
|
||||
ok(id, json!({ "content": [ { "type": "text", "text": capped } ], "isError": false }))
|
||||
}
|
||||
|
||||
@@ -863,8 +863,12 @@ pub async fn repl(base: &Path, auth: SessionAuth) -> anyhow::Result<()> {
|
||||
} else if arg == "clear" {
|
||||
s.collection = None; println!(" api collection cleared");
|
||||
} else {
|
||||
match harness::collection::parse_file(arg) {
|
||||
Ok(eps) => { s.collection = Some(arg.to_string()); println!(" api collection: imported {} endpoint(s) from {arg} — tested as known surface", eps.len()); }
|
||||
match harness::collection::parse_path(arg) {
|
||||
Ok(eps) => {
|
||||
let kind = if std::path::Path::new(arg).is_dir() { "directory" } else { "file" };
|
||||
s.collection = Some(arg.to_string());
|
||||
println!(" api collection: imported {} endpoint(s) from {kind} {arg} — tested as known surface", eps.len());
|
||||
}
|
||||
Err(e) => println!(" \x1b[33m⚠ {e}\x1b[0m"),
|
||||
}
|
||||
}
|
||||
@@ -2724,7 +2728,7 @@ fn help() {
|
||||
h("/repo <path|url>", "source repo (greybox) OR skill/n8n file/folder to audit (skills scope)");
|
||||
h("/auth <value>", "auth header (Bearer/cookie/key). Roles: /auth admin <hdr> · /auth user <hdr>");
|
||||
h("/creds <file.yaml>", "creds: jwt/header/cookie/login + ssh/windows + aws/gcp/azure + roles");
|
||||
h("/collection <file>", "import an API collection (Postman/OpenAPI/HAR, JSON) as known surface to test");
|
||||
h("/collection <path>", "import API collection(s) — file or directory of .json (Postman/OpenAPI/HAR)");
|
||||
h("/focus <text>", "steer the tests (or just type the instruction)");
|
||||
h("/objective <text>", "engagement goal/context — shapes what agents prioritise & count as impact");
|
||||
h("/scope-out <text>", "out-of-scope exclusions — host-shaped entries become ENFORCED denials");
|
||||
|
||||
@@ -39,7 +39,50 @@ fn detect(v: &Value) -> &'static str {
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse a collection file into a de-duplicated endpoint list.
|
||||
/// Parse a path that is either a single collection file OR a directory of
|
||||
/// `.json` collection files. When it is a directory every `.json` inside it
|
||||
/// (recursively) is parsed and the endpoints are merged and de-duplicated.
|
||||
pub fn parse_path(path: &str) -> Result<Vec<SeedEndpoint>> {
|
||||
let p = std::path::Path::new(path);
|
||||
if p.is_dir() {
|
||||
parse_dir(path)
|
||||
} else {
|
||||
parse_file(path)
|
||||
}
|
||||
}
|
||||
|
||||
/// Walk a directory tree, parse every `.json` file as a collection, merge and
|
||||
/// de-duplicate. Files that are not valid collections are reported on stderr
|
||||
/// but do not abort the import — at least one file must yield endpoints.
|
||||
pub fn parse_dir(dir: &str) -> Result<Vec<SeedEndpoint>> {
|
||||
let mut all = Vec::new();
|
||||
let mut ok_count = 0usize;
|
||||
let mut err_count = 0usize;
|
||||
for entry in walkdir::WalkDir::new(dir).follow_links(true).into_iter().filter_map(|e| e.ok()) {
|
||||
let p = entry.path();
|
||||
if !p.is_file() { continue; }
|
||||
match p.extension().and_then(|x| x.to_str()) {
|
||||
Some("json" | "har") => {}
|
||||
_ => continue,
|
||||
}
|
||||
match parse_file(&p.to_string_lossy()) {
|
||||
Ok(eps) => { ok_count += 1; all.extend(eps); }
|
||||
Err(e) => { err_count += 1; eprintln!(" \x1b[33m⚠ skipping {}: {e}\x1b[0m", p.display()); }
|
||||
}
|
||||
}
|
||||
if ok_count == 0 {
|
||||
return Err(anyhow!(
|
||||
"no valid collection files found in '{dir}' ({err_count} file(s) skipped)"
|
||||
));
|
||||
}
|
||||
dedup(&mut all);
|
||||
if all.is_empty() {
|
||||
return Err(anyhow!("collections in '{dir}' contained no endpoints"));
|
||||
}
|
||||
Ok(all)
|
||||
}
|
||||
|
||||
/// Parse a single collection file into a de-duplicated endpoint list.
|
||||
pub fn parse_file(path: &str) -> Result<Vec<SeedEndpoint>> {
|
||||
let raw = std::fs::read_to_string(path)
|
||||
.map_err(|e| anyhow!("cannot read collection '{path}': {e}"))?;
|
||||
|
||||
@@ -180,7 +180,7 @@ fn operator_directives(cfg: &RunConfig) -> String {
|
||||
// endpoints as surface to test directly. Parse errors are reported once at
|
||||
// launch (see the CLI), so render silently here.
|
||||
if let Some(col) = cfg.api_collection.as_deref().filter(|x| !x.trim().is_empty()) {
|
||||
if let Ok(eps) = crate::collection::parse_file(col) {
|
||||
if let Ok(eps) = crate::collection::parse_path(col) {
|
||||
s.push_str(&crate::collection::render_block(&eps, 200));
|
||||
}
|
||||
}
|
||||
|
||||
+253
-15
@@ -1,5 +1,5 @@
|
||||
'use strict';
|
||||
/* NeuroSploit v4.2.1 — web console frontend. Vanilla JS, no build step. */
|
||||
/* NeuroSploit v4.3.2 — web console frontend. Vanilla JS, no build step. */
|
||||
|
||||
const $ = (sel, root = document) => root.querySelector(sel);
|
||||
const $$ = (sel, root = document) => Array.from(root.querySelectorAll(sel));
|
||||
@@ -43,6 +43,7 @@ const state = {
|
||||
live: { sort: 'severity', dir: 1, query: '', sev: null },
|
||||
detail: { sort: 'severity', dir: 1, query: '', sev: null },
|
||||
},
|
||||
activeView: 'wizard',
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -96,6 +97,33 @@ function fieldError(id, msg) {
|
||||
}
|
||||
function clearFieldErrors() { $$('.field-error').forEach((el) => { el.textContent = ''; el.hidden = true; }); }
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// view navigation
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const ALL_VIEWS = ['dashView','wizardView','liveView','detailView','pentestsView','prreviewsView','issuesView','reposView','domainsView','networksView','integrationsView','settingsView'];
|
||||
|
||||
function switchView(viewName) {
|
||||
state.activeView = viewName;
|
||||
ALL_VIEWS.forEach(id => { const el = $(`#${id}`); if (el) el.hidden = true; });
|
||||
const target = $(`#${viewName}View`);
|
||||
if (target) target.hidden = false;
|
||||
$$('.sb-nav-item').forEach(btn => {
|
||||
btn.classList.toggle('active', btn.dataset.view === viewName);
|
||||
});
|
||||
const runHist = $('#sbRunHistory');
|
||||
if (runHist) runHist.style.display = ['dash','pentests','wizard'].includes(viewName) ? '' : 'none';
|
||||
if (viewName === 'pentests') loadPentests();
|
||||
if (viewName === 'issues') loadIssues();
|
||||
if (viewName === 'repos') loadRepos();
|
||||
if (viewName === 'domains') loadDomains();
|
||||
if (viewName === 'networks') loadNetworks();
|
||||
if (viewName === 'integrations') loadIntegrations();
|
||||
if (viewName === 'settings') loadSettings();
|
||||
if (viewName === 'prreviews') loadPrReviews();
|
||||
if (viewName === 'dash') renderDashboard();
|
||||
}
|
||||
|
||||
function timeAgo(ts) {
|
||||
if (!ts) return '';
|
||||
const secs = Math.max(0, Math.floor(Date.now() / 1000 - ts));
|
||||
@@ -1029,7 +1057,7 @@ function leaveLiveJob() {
|
||||
state.currentJob = null;
|
||||
termSyncTargets();
|
||||
}
|
||||
$('#btnBackToBoard').addEventListener('click', () => { leaveLiveJob(); show($('#liveView'), false); show($('#dashView'), false); show($('#wizardView'), true); });
|
||||
$('#btnBackToBoard').addEventListener('click', () => { leaveLiveJob(); switchView('wizard'); });
|
||||
// Regenerating from the evidence already on disk, rather than re-running the
|
||||
// engagement: a run whose PDF was never produced (no `typst` at the time, or a
|
||||
// since-improved template) would otherwise be unreportable.
|
||||
@@ -1074,8 +1102,9 @@ $('#btnDeleteRun').addEventListener('click', async () => {
|
||||
}
|
||||
});
|
||||
|
||||
$('#btnDetailBack').addEventListener('click', () => { clearInterval(state.detailPoll); show($('#detailView'), false); show($('#dashView'), false); show($('#wizardView'), true); });
|
||||
$('#btnNewEngagement').addEventListener('click', () => { leaveLiveJob(); clearInterval(state.detailPoll); show($('#detailView'), false); show($('#liveView'), false); show($('#dashView'), false); show($('#wizardView'), true); });
|
||||
$('#btnDetailBack').addEventListener('click', () => { clearInterval(state.detailPoll); switchView('wizard'); });
|
||||
$('#btnNewEngagement')?.addEventListener('click', () => { leaveLiveJob(); clearInterval(state.detailPoll); switchView('wizard'); });
|
||||
if ($('#btnNewPentest2')) $('#btnNewPentest2').addEventListener('click', () => { leaveLiveJob(); clearInterval(state.detailPoll); switchView('wizard'); });
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Finding detail modal — full evidence/impact/remediation + any PoC script
|
||||
@@ -2611,16 +2640,19 @@ function openFairParams(params) {
|
||||
function showDashboard() {
|
||||
leaveLiveJob();
|
||||
clearInterval(state.detailPoll);
|
||||
show($('#wizardView'), false);
|
||||
show($('#liveView'), false);
|
||||
show($('#detailView'), false);
|
||||
show($('#dashView'), true);
|
||||
renderDashboard();
|
||||
switchView('dash');
|
||||
}
|
||||
$('#btnDashboard').addEventListener('click', showDashboard);
|
||||
$('#btnDashRefresh').addEventListener('click', renderDashboard);
|
||||
$('#dashRange').addEventListener('change', renderDashboard);
|
||||
$('#runFilter').addEventListener('input', (e) => { state.runFilter = e.target.value; renderSidebar(); });
|
||||
// Sidebar nav items
|
||||
$$('.sb-nav-item').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
if (btn.dataset.view === 'wizard') { leaveLiveJob(); clearInterval(state.detailPoll); }
|
||||
switchView(btn.dataset.view);
|
||||
if (window.matchMedia(NARROW).matches) setSidebar(false);
|
||||
});
|
||||
});
|
||||
$('#btnDashRefresh')?.addEventListener('click', renderDashboard);
|
||||
$('#dashRange')?.addEventListener('change', renderDashboard);
|
||||
$('#runFilter')?.addEventListener('input', (e) => { state.runFilter = e.target.value; renderSidebar(); });
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// boot
|
||||
@@ -2641,8 +2673,7 @@ $('#sidebarScrim').addEventListener('click', () => setSidebar(false));
|
||||
$('#sbGroups').addEventListener('click', (e) => {
|
||||
if (e.target.closest('.sb-run') && window.matchMedia(NARROW).matches) setSidebar(false);
|
||||
});
|
||||
$('#btnDashboard').addEventListener('click', () => { if (window.matchMedia(NARROW).matches) setSidebar(false); });
|
||||
$('#btnNewEngagement').addEventListener('click', () => { if (window.matchMedia(NARROW).matches) setSidebar(false); });
|
||||
$('#btnNewEngagement')?.addEventListener('click', () => { if (window.matchMedia(NARROW).matches) setSidebar(false); });
|
||||
// Growing the window past the breakpoint leaves a scrim over a sidebar that is
|
||||
// no longer a drawer.
|
||||
window.matchMedia(NARROW).addEventListener('change', (m) => { if (!m.matches) setSidebar(false); });
|
||||
@@ -3365,3 +3396,210 @@ const adoptGraph = (r) => {
|
||||
$('#btnGraphRecompute')?.addEventListener('click', (e) => cliAction(e.currentTarget, 'Recompute graph', '/graph', {}, adoptGraph));
|
||||
$('#btnGraphPush')?.addEventListener('click', (e) => cliAction(e.currentTarget, 'Push to Neo4j', '/graph', { push: true }, adoptGraph));
|
||||
$('#btnRetest')?.addEventListener('click', (e) => cliAction(e.currentTarget, 'Re-test PoCs', '/retest', {}));
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// new pages — pentests / PR reviews / issues / repos / domains / networks /
|
||||
// integrations / settings
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function loadPentests() {
|
||||
const body = $('#pentestsBody');
|
||||
if (!body) return;
|
||||
try {
|
||||
const runs = await api('/api/runs');
|
||||
if (!runs.length) { body.innerHTML = '<div class="empty-state">No pentests yet. Click <strong>+ New Pentest</strong> to start.</div>'; return; }
|
||||
const rows = runs.map(r => {
|
||||
const mode = (r.id || '').startsWith('ns-wb-') ? 'whitebox' : (r.id || '').startsWith('ns-gb-') ? 'greybox' : (r.id || '').startsWith('ns-host-') ? 'host' : 'blackbox';
|
||||
const when = r.when ? timeAgo(Math.floor(new Date(r.when).getTime() / 1000)) : '';
|
||||
return `<tr class="pentest-row" data-id="${esc(r.id)}">
|
||||
<td class="mono">${esc(r.id)}</td>
|
||||
<td>${esc(r.target || r.name || '')}</td>
|
||||
<td>${esc(mode)}</td>
|
||||
<td>${r.findings ?? 0}</td>
|
||||
<td>${esc(r.state || 'done')}</td>
|
||||
<td>${when}</td>
|
||||
</tr>`;
|
||||
}).join('');
|
||||
body.innerHTML = `<table class="data-table"><thead><tr><th>ID</th><th>Target</th><th>Mode</th><th>Findings</th><th>Status</th><th>When</th></tr></thead><tbody>${rows}</tbody></table>`;
|
||||
$$('.pentest-row', body).forEach(row => {
|
||||
row.style.cursor = 'pointer';
|
||||
row.addEventListener('click', () => { const run = runs.find(r => r.id === row.dataset.id); if (run) openRun(run); });
|
||||
});
|
||||
} catch (e) { body.innerHTML = `<div class="empty-state">Failed to load: ${esc(e.message)}</div>`; }
|
||||
}
|
||||
|
||||
async function loadPrReviews() {
|
||||
const body = $('#prreviewsBody');
|
||||
if (!body) return;
|
||||
try {
|
||||
const runs = await api('/api/runs');
|
||||
const prs = runs.filter(r => r.id && r.id.includes('-pr-'));
|
||||
if (!prs.length) { body.innerHTML = '<div class="empty-state">No PR reviews yet. Run <code>neurosploit pr owner/repo 123</code> or use the MCP server.</div>'; return; }
|
||||
const rows = prs.map(r => {
|
||||
const when = r.when ? timeAgo(Math.floor(new Date(r.when).getTime() / 1000)) : '';
|
||||
return `<tr class="pentest-row" data-id="${esc(r.id)}"><td class="mono">${esc(r.id)}</td><td>${esc(r.target || r.name || '')}</td><td>${r.findings ?? 0}</td><td>${esc(r.state || 'done')}</td><td>${when}</td></tr>`;
|
||||
}).join('');
|
||||
body.innerHTML = `<table class="data-table"><thead><tr><th>ID</th><th>Repository</th><th>Findings</th><th>Status</th><th>When</th></tr></thead><tbody>${rows}</tbody></table>`;
|
||||
} catch (e) { body.innerHTML = `<div class="empty-state">Failed to load: ${esc(e.message)}</div>`; }
|
||||
}
|
||||
|
||||
async function loadIssues() {
|
||||
const body = $('#issuesBody');
|
||||
if (!body) return;
|
||||
try {
|
||||
const data = await api('/api/issues');
|
||||
if (!data.length) { body.innerHTML = '<div class="empty-state">No findings across any engagement yet.</div>'; return; }
|
||||
const rows = data.map(f => {
|
||||
const sc = sevClass(f.severity);
|
||||
return `<tr><td><span class="pill ${sc}">${esc(f.severity)}</span></td><td>${esc(f.title)}</td><td class="mono">${esc(f.target || '')}</td><td class="mono">${esc(f.run || '')}</td><td>${esc(f.status || 'open')}</td></tr>`;
|
||||
}).join('');
|
||||
body.innerHTML = `<table class="data-table"><thead><tr><th>Severity</th><th>Title</th><th>Target</th><th>Run</th><th>Status</th></tr></thead><tbody>${rows}</tbody></table>`;
|
||||
} catch (e) { body.innerHTML = `<div class="empty-state">Failed to load: ${esc(e.message)}</div>`; }
|
||||
}
|
||||
|
||||
async function loadRepos() {
|
||||
const body = $('#reposBody');
|
||||
if (!body) return;
|
||||
try {
|
||||
const repos = await api('/api/config/repos');
|
||||
if (!repos.length) { body.innerHTML = '<div class="empty-state">No repositories connected yet. Click <strong>+ Add Repository</strong> to connect one.</div>'; return; }
|
||||
body.innerHTML = '<div class="config-grid">' + repos.map(r => `<div class="config-card">
|
||||
<div class="config-card-title">${esc(r.name || r.url)}</div>
|
||||
<div class="config-card-desc">${esc(r.description || r.url)}</div>
|
||||
<div class="config-card-meta">${esc(r.provider || 'git')} · ${esc(r.branch || 'main')}</div>
|
||||
<div class="config-card-actions"><button class="btn btn-danger" onclick="deleteRepo('${esc(r.id)}')">Remove</button></div>
|
||||
</div>`).join('') + '</div>';
|
||||
} catch { body.innerHTML = '<div class="empty-state">No repositories connected yet. Click <strong>+ Add Repository</strong> to connect one.</div>'; }
|
||||
}
|
||||
|
||||
async function loadDomains() {
|
||||
const body = $('#domainsBody');
|
||||
if (!body) return;
|
||||
try {
|
||||
const domains = await api('/api/config/domains');
|
||||
if (!domains.length) { body.innerHTML = '<div class="empty-state">No domains configured yet. Click <strong>+ Add Domain</strong> to add one.</div>'; return; }
|
||||
body.innerHTML = '<div class="config-grid">' + domains.map(d => `<div class="config-card">
|
||||
<div class="config-card-title">${esc(d.domain)}</div>
|
||||
<div class="config-card-desc">${esc(d.description || 'Target domain')}</div>
|
||||
<div class="config-card-meta">${d.subdomains ? d.subdomains.length + ' subdomains' : 'No subdomains'} · ${esc(d.scope || 'in-scope')}</div>
|
||||
<div class="config-card-actions"><button class="btn btn-danger" onclick="deleteDomain('${esc(d.id)}')">Remove</button></div>
|
||||
</div>`).join('') + '</div>';
|
||||
} catch { body.innerHTML = '<div class="empty-state">No domains configured yet. Click <strong>+ Add Domain</strong> to add one.</div>'; }
|
||||
}
|
||||
|
||||
async function loadNetworks() {
|
||||
const body = $('#networksBody');
|
||||
if (!body) return;
|
||||
try {
|
||||
const nets = await api('/api/config/networks');
|
||||
if (!nets.length) { body.innerHTML = '<div class="empty-state">No networks configured yet. Click <strong>+ Add Network</strong> to add one.</div>'; return; }
|
||||
body.innerHTML = '<div class="config-grid">' + nets.map(n => `<div class="config-card">
|
||||
<div class="config-card-title">${esc(n.name || n.cidr)}</div>
|
||||
<div class="config-card-desc">${esc(n.description || n.cidr)}</div>
|
||||
<div class="config-card-meta">${esc(n.cidr)} · ${esc(n.transport || 'direct')}</div>
|
||||
<div class="config-card-actions"><button class="btn btn-danger" onclick="deleteNetwork('${esc(n.id)}')">Remove</button></div>
|
||||
</div>`).join('') + '</div>';
|
||||
} catch { body.innerHTML = '<div class="empty-state">No networks configured yet. Click <strong>+ Add Network</strong> to add one.</div>'; }
|
||||
}
|
||||
|
||||
async function loadIntegrations() {
|
||||
const body = $('#integrationsBody');
|
||||
if (!body) return;
|
||||
try {
|
||||
const integs = await api('/api/config/integrations');
|
||||
const defs = [
|
||||
{ id: 'github', name: 'GitHub', desc: 'Pull request reviews, commit status checks, and repository scanning' },
|
||||
{ id: 'gitlab', name: 'GitLab', desc: 'Merge request reviews and repository scanning' },
|
||||
{ id: 'jira', name: 'Jira', desc: 'Create Jira tickets for confirmed findings' },
|
||||
{ id: 'slack', name: 'Slack', desc: 'Notifications for completed engagements and critical findings' },
|
||||
{ id: 'mcp', name: 'MCP Server', desc: 'Model Context Protocol — drive NeuroSploit from Claude Code, Codex, Cursor, Windsurf' },
|
||||
];
|
||||
body.innerHTML = '<div class="config-grid">' + defs.map(c => {
|
||||
const cfg = integs.find(i => i.id === c.id) || {};
|
||||
const on = cfg.enabled;
|
||||
return `<div class="config-card">
|
||||
<div class="config-card-title">${esc(c.name)}</div>
|
||||
<div class="config-card-desc">${esc(c.desc)}</div>
|
||||
<div class="config-card-meta"><span class="integration-dot ${on ? 'connected' : 'disconnected'}"></span> ${on ? 'Connected' : 'Not configured'}</div>
|
||||
<div class="config-card-actions"><button class="btn" onclick="configureIntegration('${c.id}')">${on ? 'Configure' : 'Connect'}</button></div>
|
||||
</div>`;
|
||||
}).join('') + '</div>';
|
||||
} catch { body.innerHTML = '<div class="empty-state">Failed to load integrations.</div>'; }
|
||||
}
|
||||
|
||||
async function loadSettings() {
|
||||
const body = $('#settingsBody');
|
||||
if (!body) return;
|
||||
try {
|
||||
const cfg = await api('/api/config/settings');
|
||||
body.innerHTML = `<div class="config-grid">
|
||||
<div class="config-card">
|
||||
<div class="config-card-title">General</div>
|
||||
<div class="form-group"><label>Default model</label><input type="text" id="settingModel" value="${esc(cfg.defaultModel || '')}" placeholder="anthropic:claude-opus-4-8" /></div>
|
||||
<div class="form-group"><label>Default environment</label><select id="settingEnv">
|
||||
<option value="lab" ${cfg.environment === 'lab' ? 'selected' : ''}>Lab</option>
|
||||
<option value="development" ${cfg.environment === 'development' ? 'selected' : ''}>Development</option>
|
||||
<option value="staging" ${cfg.environment === 'staging' ? 'selected' : ''}>Staging</option>
|
||||
<option value="production" ${!cfg.environment || cfg.environment === 'production' ? 'selected' : ''}>Production</option>
|
||||
</select></div>
|
||||
<div class="form-group"><label>TypeSafe</label><select id="settingTypesafe">
|
||||
<option value="auto" ${!cfg.typesafe || cfg.typesafe === 'auto' ? 'selected' : ''}>Auto</option>
|
||||
<option value="on" ${cfg.typesafe === 'on' ? 'selected' : ''}>On</option>
|
||||
<option value="off" ${cfg.typesafe === 'off' ? 'selected' : ''}>Off</option>
|
||||
</select></div>
|
||||
<div class="config-card-actions"><button class="btn btn-accent" onclick="saveSettings()">Save</button></div>
|
||||
</div>
|
||||
<div class="config-card">
|
||||
<div class="config-card-title">Network</div>
|
||||
<div class="form-group"><label>Default transport</label><input type="text" id="settingTransport" value="${esc(cfg.transport || '')}" placeholder="direct / socks5://host:port" /></div>
|
||||
<div class="form-group"><label>OOB domain</label><input type="text" id="settingOob" value="${esc(cfg.oobDomain || '')}" placeholder="oob.yourdomain.com" /></div>
|
||||
<div class="form-group"><label>Intercept proxy</label><input type="text" id="settingIntercept" value="${esc(cfg.intercept || '')}" placeholder="burp / caido / http://host:port" /></div>
|
||||
<div class="config-card-actions"><button class="btn btn-accent" onclick="saveSettings()">Save</button></div>
|
||||
</div>
|
||||
<div class="config-card">
|
||||
<div class="config-card-title">MCP Server</div>
|
||||
<div class="config-card-desc">Run NeuroSploit as an MCP server for Claude Code, Codex, Cursor, Windsurf and other MCP clients.</div>
|
||||
<div class="config-card-meta" style="margin-top:var(--sp-2)"><code>neurosploit mcp</code><br/>Config: <code>{"command":"neurosploit","args":["mcp"]}</code></div>
|
||||
</div>
|
||||
</div>`;
|
||||
} catch { body.innerHTML = '<div class="empty-state">Failed to load settings.</div>'; }
|
||||
}
|
||||
|
||||
function deleteRepo(id) { api(`/api/config/repos/${id}`, { method: 'DELETE' }).then(() => loadRepos()).catch(e => toast(e.message, 'error')); }
|
||||
function deleteDomain(id) { api(`/api/config/domains/${id}`, { method: 'DELETE' }).then(() => loadDomains()).catch(e => toast(e.message, 'error')); }
|
||||
function deleteNetwork(id) { api(`/api/config/networks/${id}`, { method: 'DELETE' }).then(() => loadNetworks()).catch(e => toast(e.message, 'error')); }
|
||||
function configureIntegration(id) { toast('Integration configuration — coming soon', 'info'); }
|
||||
async function saveSettings() {
|
||||
const data = {
|
||||
defaultModel: $('#settingModel')?.value || '',
|
||||
environment: $('#settingEnv')?.value || 'production',
|
||||
typesafe: $('#settingTypesafe')?.value || 'auto',
|
||||
transport: $('#settingTransport')?.value || '',
|
||||
oobDomain: $('#settingOob')?.value || '',
|
||||
intercept: $('#settingIntercept')?.value || '',
|
||||
};
|
||||
try { await api('/api/config/settings', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); toast('Settings saved', 'ok'); }
|
||||
catch (e) { toast(`Failed: ${e.message}`, 'error'); }
|
||||
}
|
||||
|
||||
// Add-item modals
|
||||
function addRepoPrompt() {
|
||||
const url = prompt('Repository URL or owner/repo:');
|
||||
if (!url) return;
|
||||
const name = prompt('Name (optional):') || '';
|
||||
api('/api/config/repos', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ url, name, branch: 'main' }) })
|
||||
.then(() => loadRepos()).catch(e => toast(e.message, 'error'));
|
||||
}
|
||||
function addDomainPrompt() {
|
||||
const domain = prompt('Domain (e.g. app.example.com):');
|
||||
if (!domain) return;
|
||||
api('/api/config/domains', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ domain }) })
|
||||
.then(() => loadDomains()).catch(e => toast(e.message, 'error'));
|
||||
}
|
||||
function addNetworkPrompt() {
|
||||
const cidr = prompt('Network CIDR or host (e.g. 10.0.0.0/24):');
|
||||
if (!cidr) return;
|
||||
const name = prompt('Name (optional):') || '';
|
||||
api('/api/config/networks', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ cidr, name }) })
|
||||
.then(() => loadNetworks()).catch(e => toast(e.message, 'error'));
|
||||
}
|
||||
+145
-15
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>NeuroSploit v4.3.1 — Console</title>
|
||||
<title>NeuroSploit v4.3.2 — Console</title>
|
||||
<link rel="icon" type="image/svg+xml" href="logo.svg">
|
||||
<link rel="stylesheet" href="/vendor/xterm.css" />
|
||||
<link rel="stylesheet" href="/style.css" />
|
||||
@@ -35,18 +35,33 @@
|
||||
<button class="icon-btn" id="btnThemeToggle" title="Toggle light / dark theme">☾</button>
|
||||
</div>
|
||||
|
||||
<button class="sb-new" id="btnNewEngagement">+ New engagement</button>
|
||||
<button class="sb-link" id="btnDashboard">▤ Dashboard</button>
|
||||
<button class="sb-new" id="btnNewEngagement">+ New Pentest</button>
|
||||
|
||||
<div class="sb-search">
|
||||
<span class="search-icon" aria-hidden="true">⌕</span>
|
||||
<input id="runFilter" type="text" autocomplete="off" placeholder="Filter runs" aria-label="Filter runs" />
|
||||
<nav class="sb-nav" id="sbNav">
|
||||
<button class="sb-nav-item active" data-view="dash"><span class="sb-nav-icon">▤</span> Dashboard</button>
|
||||
<button class="sb-nav-item" data-view="pentests"><span class="sb-nav-icon">⊞</span> Pentests</button>
|
||||
<button class="sb-nav-item" data-view="prreviews"><span class="sb-nav-icon">⎇</span> PR Reviews</button>
|
||||
<button class="sb-nav-item" data-view="issues"><span class="sb-nav-icon">⚑</span> Issues</button>
|
||||
<div class="sb-nav-separator"></div>
|
||||
<button class="sb-nav-item" data-view="repos"><span class="sb-nav-icon">◎</span> Repositories</button>
|
||||
<button class="sb-nav-item" data-view="domains"><span class="sb-nav-icon">◇</span> Domains</button>
|
||||
<button class="sb-nav-item" data-view="networks"><span class="sb-nav-icon">⬡</span> Networks</button>
|
||||
<div class="sb-nav-separator"></div>
|
||||
<button class="sb-nav-item" data-view="integrations"><span class="sb-nav-icon">⚙</span> Integrations</button>
|
||||
<button class="sb-nav-item" data-view="settings"><span class="sb-nav-icon">☰</span> Settings</button>
|
||||
</nav>
|
||||
|
||||
<div class="sb-run-history" id="sbRunHistory">
|
||||
<div class="sb-search">
|
||||
<span class="search-icon" aria-hidden="true">⌕</span>
|
||||
<input id="runFilter" type="text" autocomplete="off" placeholder="Filter runs" aria-label="Filter runs" />
|
||||
</div>
|
||||
|
||||
<div class="sb-groups" id="sbGroups"><!-- populated by app.js --></div>
|
||||
</div>
|
||||
|
||||
<div class="sb-groups" id="sbGroups"><!-- populated by app.js --></div>
|
||||
|
||||
<div class="sb-bottom">
|
||||
<span class="sb-version" id="sbVersion">v4.3.1</span>
|
||||
<span class="sb-version" id="sbVersion">v4.3.2</span>
|
||||
<div class="sb-bottom-actions">
|
||||
<button class="icon-btn" id="btnOpenAuth" title="Auth & API keys">🔑</button>
|
||||
<button class="icon-btn" id="btnOpenRepl" title="Open terminal (Ctrl+`)">❭_</button>
|
||||
@@ -78,11 +93,126 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============ PENTESTS ============ -->
|
||||
<section class="page-view" id="pentestsView" hidden>
|
||||
<header class="topbar">
|
||||
<div>
|
||||
<div class="topbar-title">Pentests</div>
|
||||
<div class="topbar-sub">All engagements — black-box, white-box, greybox, host, AI, mobile, container</div>
|
||||
</div>
|
||||
<div class="topbar-spacer"></div>
|
||||
<input type="text" id="pentestSearch" class="topbar-search" placeholder="Search pentests…" />
|
||||
<button class="btn btn-accent" id="btnNewPentest2">+ New Pentest</button>
|
||||
</header>
|
||||
<div class="page-body" id="pentestsBody"><div class="empty-state">Loading…</div></div>
|
||||
</section>
|
||||
|
||||
<!-- ============ PR REVIEWS ============ -->
|
||||
<section class="page-view" id="prreviewsView" hidden>
|
||||
<header class="topbar">
|
||||
<div>
|
||||
<div class="topbar-title">PR Reviews</div>
|
||||
<div class="topbar-sub">Security reviews of pull requests</div>
|
||||
</div>
|
||||
<div class="topbar-spacer"></div>
|
||||
<button class="btn btn-accent" id="btnNewPrReview">+ New Review</button>
|
||||
</header>
|
||||
<div class="page-body" id="prreviewsBody"><div class="empty-state">No PR reviews yet. Run <code>neurosploit pr owner/repo 123</code> or use the MCP server.</div></div>
|
||||
</section>
|
||||
|
||||
<!-- ============ ISSUES ============ -->
|
||||
<section class="page-view" id="issuesView" hidden>
|
||||
<header class="topbar">
|
||||
<div>
|
||||
<div class="topbar-title">Issues</div>
|
||||
<div class="topbar-sub">All findings across every engagement</div>
|
||||
</div>
|
||||
<div class="topbar-spacer"></div>
|
||||
<select id="issuesSevFilter" class="dash-range">
|
||||
<option value="">All severities</option>
|
||||
<option value="critical">Critical</option>
|
||||
<option value="high">High</option>
|
||||
<option value="medium">Medium</option>
|
||||
<option value="low">Low</option>
|
||||
</select>
|
||||
<select id="issuesStatusFilter" class="dash-range">
|
||||
<option value="">All statuses</option>
|
||||
<option value="open">Open</option>
|
||||
<option value="fixed">Fixed</option>
|
||||
</select>
|
||||
<input type="text" id="issuesSearch" class="topbar-search" placeholder="Search issues…" />
|
||||
</header>
|
||||
<div class="page-body" id="issuesBody"><div class="empty-state">Loading…</div></div>
|
||||
</section>
|
||||
|
||||
<!-- ============ REPOSITORIES ============ -->
|
||||
<section class="page-view" id="reposView" hidden>
|
||||
<header class="topbar">
|
||||
<div>
|
||||
<div class="topbar-title">Repositories</div>
|
||||
<div class="topbar-sub">Connected source repositories for white-box, greybox, and PR reviews</div>
|
||||
</div>
|
||||
<div class="topbar-spacer"></div>
|
||||
<button class="btn btn-accent" id="btnAddRepo">+ Add Repository</button>
|
||||
</header>
|
||||
<div class="page-body" id="reposBody"><div class="empty-state">No repositories connected yet.</div></div>
|
||||
</section>
|
||||
|
||||
<!-- ============ DOMAINS ============ -->
|
||||
<section class="page-view" id="domainsView" hidden>
|
||||
<header class="topbar">
|
||||
<div>
|
||||
<div class="topbar-title">Domains</div>
|
||||
<div class="topbar-sub">Target domains and subdomains for black-box engagements</div>
|
||||
</div>
|
||||
<div class="topbar-spacer"></div>
|
||||
<button class="btn btn-accent" id="btnAddDomain">+ Add Domain</button>
|
||||
</header>
|
||||
<div class="page-body" id="domainsBody"><div class="empty-state">No domains configured yet.</div></div>
|
||||
</section>
|
||||
|
||||
<!-- ============ NETWORKS ============ -->
|
||||
<section class="page-view" id="networksView" hidden>
|
||||
<header class="topbar">
|
||||
<div>
|
||||
<div class="topbar-title">Networks</div>
|
||||
<div class="topbar-sub">Network ranges and hosts for infrastructure scanning</div>
|
||||
</div>
|
||||
<div class="topbar-spacer"></div>
|
||||
<button class="btn btn-accent" id="btnAddNetwork">+ Add Network</button>
|
||||
</header>
|
||||
<div class="page-body" id="networksBody"><div class="empty-state">No networks configured yet.</div></div>
|
||||
</section>
|
||||
|
||||
<!-- ============ INTEGRATIONS ============ -->
|
||||
<section class="page-view" id="integrationsView" hidden>
|
||||
<header class="topbar">
|
||||
<div>
|
||||
<div class="topbar-title">Integrations</div>
|
||||
<div class="topbar-sub">Connect external services — GitHub, GitLab, Jira, Slack, CI/CD</div>
|
||||
</div>
|
||||
<div class="topbar-spacer"></div>
|
||||
</header>
|
||||
<div class="page-body" id="integrationsBody"><div class="empty-state">Loading…</div></div>
|
||||
</section>
|
||||
|
||||
<!-- ============ SETTINGS ============ -->
|
||||
<section class="page-view" id="settingsView" hidden>
|
||||
<header class="topbar">
|
||||
<div>
|
||||
<div class="topbar-title">Settings</div>
|
||||
<div class="topbar-sub">Console configuration, API keys, and preferences</div>
|
||||
</div>
|
||||
<div class="topbar-spacer"></div>
|
||||
</header>
|
||||
<div class="page-body" id="settingsBody"><div class="empty-state">Loading…</div></div>
|
||||
</section>
|
||||
|
||||
<!-- ============ WIZARD (new engagement) ============ -->
|
||||
<section class="wizard" id="wizardView">
|
||||
<header class="topbar">
|
||||
<div>
|
||||
<div class="topbar-title">New engagement</div>
|
||||
<div class="topbar-title">New Pentest</div>
|
||||
<div class="topbar-sub">Asset → Scope & Auth → Leads → Model & Run → Review</div>
|
||||
</div>
|
||||
<div class="topbar-spacer"></div>
|
||||
@@ -350,7 +480,7 @@
|
||||
<a class="btn" id="btnDownloadLog" download>Download log</a>
|
||||
<button class="btn" id="btnReportNow" title="Write the report from what has been found so far">Report so far</button>
|
||||
<button class="btn btn-danger" id="btnStopRun">Stop</button>
|
||||
<button class="btn" id="btnBackToBoard">← New engagement</button>
|
||||
<button class="btn" id="btnBackToBoard">← Back</button>
|
||||
</div>
|
||||
</header>
|
||||
<div class="progress-wrap">
|
||||
@@ -416,7 +546,7 @@
|
||||
<button class="btn" id="btnRetest" title="Re-run every finding's recorded proof (neurosploit poc) and see what still reproduces">⟳ Re-test</button>
|
||||
<a class="btn" id="detailOpenAudit" target="_blank" hidden title="Every action this run took, hash-chained">Audit trail</a>
|
||||
<button class="btn btn-danger" id="btnDeleteRun" title="Permanently delete this session and its report">🗑 Delete</button>
|
||||
<button class="btn" id="btnDetailBack">← New engagement</button>
|
||||
<button class="btn" id="btnDetailBack">← Back</button>
|
||||
</div>
|
||||
</header>
|
||||
<nav class="run-tabs">
|
||||
@@ -639,9 +769,9 @@
|
||||
</div>
|
||||
<div class="field-group">
|
||||
<label class="field-label" for="collectionFile">API collection (Postman / OpenAPI / Swagger / HAR)</label>
|
||||
<input id="collectionFile" type="file" accept=".json,application/json,.har" />
|
||||
<input id="collectionPath" type="text" placeholder="…or a path on disk: api.postman_collection.json" />
|
||||
<div class="field-help" id="collectionHelp">Imports the endpoints as known surface — every route is tested directly, alongside recon. JSON only (convert YAML specs first).</div>
|
||||
<input id="collectionFile" type="file" accept=".json,application/json,.har" multiple />
|
||||
<input id="collectionPath" type="text" placeholder="…or a path on disk: file.json or a directory of .json files" />
|
||||
<div class="field-help" id="collectionHelp">Imports endpoints as known surface — file or directory of .json files. Every route is tested directly, alongside recon.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -207,6 +207,79 @@ a { color: var(--accent); text-decoration: none; }
|
||||
}
|
||||
.sb-link:hover { background: var(--surface-3); color: var(--text); }
|
||||
|
||||
.sb-nav { display: flex; flex-direction: column; gap: 1px; padding: 0 var(--sp-2) var(--sp-3); }
|
||||
.sb-nav-item {
|
||||
display: flex; align-items: center; gap: var(--sp-3); padding: var(--sp-2) var(--sp-3);
|
||||
border: none; border-radius: var(--radius-sm);
|
||||
background: transparent; color: var(--text-dim); font-size: var(--fs-sm); text-align: left;
|
||||
cursor: pointer; transition: background var(--t-fast), color var(--t-fast);
|
||||
}
|
||||
.sb-nav-item:hover { background: var(--surface-3); color: var(--text); }
|
||||
.sb-nav-item.active { background: var(--accent-soft); color: var(--accent); font-weight: 600; }
|
||||
.sb-nav-icon { width: 18px; text-align: center; font-size: var(--fs-base); flex: none; }
|
||||
.sb-nav-separator { height: 1px; background: var(--border); margin: var(--sp-2) var(--sp-3); }
|
||||
|
||||
.sb-run-history { display: flex; flex-direction: column; flex: 1; overflow: hidden; }
|
||||
|
||||
.page-view { display: none; flex-direction: column; flex: 1; overflow: hidden; }
|
||||
.page-view:not([hidden]) { display: flex; }
|
||||
.page-body { flex: 1; overflow-y: auto; padding: var(--sp-5); }
|
||||
|
||||
.topbar-search {
|
||||
padding: var(--sp-2) var(--sp-3); border: 1px solid var(--border); border-radius: var(--radius-sm);
|
||||
background: var(--surface); color: var(--text); font-size: var(--fs-sm); width: 220px;
|
||||
}
|
||||
|
||||
.data-table { width: 100%; border-collapse: collapse; font-size: var(--fs-sm); }
|
||||
.data-table th { text-align: left; padding: var(--sp-2) var(--sp-3); font-weight: 600; color: var(--text-dim); border-bottom: 2px solid var(--border); font-size: var(--fs-xs); text-transform: uppercase; letter-spacing: .04em; }
|
||||
.data-table td { padding: var(--sp-2) var(--sp-3); border-bottom: 1px solid var(--border); vertical-align: top; }
|
||||
.data-table tr:hover td { background: var(--surface-2); }
|
||||
.data-table .mono { font-family: var(--mono); font-size: var(--fs-xs); }
|
||||
|
||||
.config-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: var(--sp-4); }
|
||||
.config-card {
|
||||
border: 1px solid var(--border); border-radius: var(--radius-md); padding: var(--sp-4);
|
||||
background: var(--surface); transition: border-color var(--t-fast);
|
||||
}
|
||||
.config-card:hover { border-color: var(--border-strong); }
|
||||
.config-card-title { font-size: var(--fs-lg); font-weight: 600; margin-bottom: var(--sp-2); display: flex; align-items: center; gap: var(--sp-2); }
|
||||
.config-card-desc { font-size: var(--fs-sm); color: var(--text-dim); margin-bottom: var(--sp-3); }
|
||||
.config-card-meta { font-size: var(--fs-xs); color: var(--text-faint); font-family: var(--mono); }
|
||||
.config-card-actions { display: flex; gap: var(--sp-2); margin-top: var(--sp-3); padding-top: var(--sp-3); border-top: 1px solid var(--border); }
|
||||
|
||||
.btn-accent {
|
||||
background: var(--accent); color: var(--accent-contrast); border: 1px solid var(--accent);
|
||||
padding: var(--sp-2) var(--sp-4); border-radius: var(--radius-sm); font-size: var(--fs-sm); font-weight: 600;
|
||||
}
|
||||
.btn-accent:hover { background: var(--accent-hover); border-color: var(--accent-hover); }
|
||||
|
||||
.integration-card { display: flex; align-items: center; gap: var(--sp-4); }
|
||||
.integration-icon { width: 40px; height: 40px; border-radius: var(--radius-sm); background: var(--surface-3); display: flex; align-items: center; justify-content: center; font-size: var(--fs-xl); flex: none; }
|
||||
.integration-info { flex: 1; }
|
||||
.integration-status { font-size: var(--fs-xs); display: flex; align-items: center; gap: var(--sp-1); }
|
||||
.integration-dot { width: 8px; height: 8px; border-radius: 50%; }
|
||||
.integration-dot.connected { background: var(--sev-low-fg); }
|
||||
.integration-dot.disconnected { background: var(--text-faint); }
|
||||
|
||||
.stats-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: var(--sp-4); margin-bottom: var(--sp-5); }
|
||||
.stat-card {
|
||||
border: 1px solid var(--border); border-radius: var(--radius-md); padding: var(--sp-4);
|
||||
background: var(--surface);
|
||||
}
|
||||
.stat-label { font-size: var(--fs-xs); color: var(--text-dim); text-transform: uppercase; letter-spacing: .04em; font-weight: 600; }
|
||||
.stat-value { font-size: var(--fs-2xl); font-weight: 700; margin-top: var(--sp-1); }
|
||||
.stat-sub { font-size: var(--fs-xs); color: var(--text-faint); margin-top: var(--sp-1); }
|
||||
|
||||
.form-modal { max-width: 520px; }
|
||||
.form-group { margin-bottom: var(--sp-4); }
|
||||
.form-group label { display: block; font-size: var(--fs-sm); font-weight: 600; margin-bottom: var(--sp-1); }
|
||||
.form-group input, .form-group select, .form-group textarea {
|
||||
width: 100%; padding: var(--sp-2) var(--sp-3); border: 1px solid var(--border); border-radius: var(--radius-sm);
|
||||
background: var(--surface); color: var(--text); font-size: var(--fs-sm);
|
||||
}
|
||||
.form-group .field-help { font-size: var(--fs-xs); color: var(--text-faint); margin-top: var(--sp-1); }
|
||||
.form-actions { display: flex; gap: var(--sp-2); justify-content: flex-end; margin-top: var(--sp-5); }
|
||||
|
||||
.sb-search { position: relative; margin: 0 var(--sp-4) var(--sp-3); }
|
||||
.sb-search input { padding-left: 26px; font-size: var(--fs-sm); }
|
||||
.sb-search .search-icon { left: 8px; }
|
||||
|
||||
+110
-1
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env node
|
||||
'use strict';
|
||||
/**
|
||||
* NeuroSploit v4.3.1 — web console backend.
|
||||
* NeuroSploit v4.3.2 — web console backend.
|
||||
*
|
||||
* Zero-dependency Node HTTP server that:
|
||||
* - serves the static SPA in ./public
|
||||
@@ -57,6 +57,22 @@ function saveEngagementName(runId, name) {
|
||||
// interrupted run/whitebox/greybox job, RESUME it (relaunch the REPL, which
|
||||
// auto-recovers the harness's own on-disk checkpoint and carries findings
|
||||
// forward). Secrets (API keys, creds-file contents) are never written here.
|
||||
const CONFIG_DIR = path.join(ROOT, '.neurosploit');
|
||||
function configFile(name) { return path.join(CONFIG_DIR, `${name}.json`); }
|
||||
async function ensureConfigDir() { await fsp.mkdir(CONFIG_DIR, { recursive: true }); }
|
||||
async function readConfig(name) {
|
||||
try { return JSON.parse(await fsp.readFile(configFile(name), 'utf8')); }
|
||||
catch { return []; }
|
||||
}
|
||||
async function readConfigObj(name) {
|
||||
try { return JSON.parse(await fsp.readFile(configFile(name), 'utf8')); }
|
||||
catch { return {}; }
|
||||
}
|
||||
async function writeConfig(name, data) {
|
||||
await ensureConfigDir();
|
||||
await fsp.writeFile(configFile(name), JSON.stringify(data, null, 2));
|
||||
}
|
||||
|
||||
const JOBS_DIR = path.join(ROOT, '.neurosploit', 'web-jobs');
|
||||
function jobFile(id) { return path.join(JOBS_DIR, `${id}.json`); }
|
||||
function persistJob(job) {
|
||||
@@ -1942,6 +1958,99 @@ const server = http.createServer(async (req, res) => {
|
||||
return sendJson(res, 200, { ok: true });
|
||||
}
|
||||
|
||||
// ---- Issues (aggregate findings across all runs) ----
|
||||
if (req.method === 'GET' && p === '/api/issues') {
|
||||
const dirs = await fsp.readdir(RUNS_DIR).catch(() => []);
|
||||
const all = [];
|
||||
for (const d of dirs) {
|
||||
const fp = path.join(RUNS_DIR, d, 'findings.json');
|
||||
try {
|
||||
const raw = await fsp.readFile(fp, 'utf8');
|
||||
const findings = JSON.parse(raw);
|
||||
for (const f of findings) all.push({ ...f, run: d, target: f.target || d });
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
const sRank = (s) => { s = (s || '').toLowerCase(); return s.includes('crit') ? 0 : s.includes('high') ? 1 : s.includes('med') ? 2 : s.includes('low') ? 3 : 4; };
|
||||
all.sort((a, b) => sRank(a.severity) - sRank(b.severity));
|
||||
return sendJson(res, 200, all);
|
||||
}
|
||||
|
||||
// ---- Config CRUD: repos ----
|
||||
if (req.method === 'GET' && p === '/api/config/repos') return sendJson(res, 200, await readConfig('repos'));
|
||||
if (req.method === 'POST' && p === '/api/config/repos') {
|
||||
const body = await readBody(req);
|
||||
const repos = await readConfig('repos');
|
||||
const item = { id: crypto.randomUUID(), ...body, createdAt: new Date().toISOString() };
|
||||
repos.push(item);
|
||||
await writeConfig('repos', repos);
|
||||
return sendJson(res, 200, item);
|
||||
}
|
||||
m = p.match(/^\/api\/config\/repos\/([^/]+)$/);
|
||||
if (req.method === 'DELETE' && m) {
|
||||
let repos = await readConfig('repos');
|
||||
repos = repos.filter(r => r.id !== decodeURIComponent(m[1]));
|
||||
await writeConfig('repos', repos);
|
||||
return sendJson(res, 200, { ok: true });
|
||||
}
|
||||
|
||||
// ---- Config CRUD: domains ----
|
||||
if (req.method === 'GET' && p === '/api/config/domains') return sendJson(res, 200, await readConfig('domains'));
|
||||
if (req.method === 'POST' && p === '/api/config/domains') {
|
||||
const body = await readBody(req);
|
||||
const domains = await readConfig('domains');
|
||||
const item = { id: crypto.randomUUID(), ...body, createdAt: new Date().toISOString() };
|
||||
domains.push(item);
|
||||
await writeConfig('domains', domains);
|
||||
return sendJson(res, 200, item);
|
||||
}
|
||||
m = p.match(/^\/api\/config\/domains\/([^/]+)$/);
|
||||
if (req.method === 'DELETE' && m) {
|
||||
let domains = await readConfig('domains');
|
||||
domains = domains.filter(d => d.id !== decodeURIComponent(m[1]));
|
||||
await writeConfig('domains', domains);
|
||||
return sendJson(res, 200, { ok: true });
|
||||
}
|
||||
|
||||
// ---- Config CRUD: networks ----
|
||||
if (req.method === 'GET' && p === '/api/config/networks') return sendJson(res, 200, await readConfig('networks'));
|
||||
if (req.method === 'POST' && p === '/api/config/networks') {
|
||||
const body = await readBody(req);
|
||||
const networks = await readConfig('networks');
|
||||
const item = { id: crypto.randomUUID(), ...body, createdAt: new Date().toISOString() };
|
||||
networks.push(item);
|
||||
await writeConfig('networks', networks);
|
||||
return sendJson(res, 200, item);
|
||||
}
|
||||
m = p.match(/^\/api\/config\/networks\/([^/]+)$/);
|
||||
if (req.method === 'DELETE' && m) {
|
||||
let networks = await readConfig('networks');
|
||||
networks = networks.filter(n => n.id !== decodeURIComponent(m[1]));
|
||||
await writeConfig('networks', networks);
|
||||
return sendJson(res, 200, { ok: true });
|
||||
}
|
||||
|
||||
// ---- Config CRUD: integrations ----
|
||||
if (req.method === 'GET' && p === '/api/config/integrations') return sendJson(res, 200, await readConfig('integrations'));
|
||||
m = p.match(/^\/api\/config\/integrations\/([^/]+)$/);
|
||||
if (req.method === 'PUT' && m) {
|
||||
const body = await readBody(req);
|
||||
let integs = await readConfig('integrations');
|
||||
const id = decodeURIComponent(m[1]);
|
||||
const idx = integs.findIndex(i => i.id === id);
|
||||
if (idx >= 0) integs[idx] = { ...integs[idx], ...body };
|
||||
else integs.push({ id, ...body });
|
||||
await writeConfig('integrations', integs);
|
||||
return sendJson(res, 200, integs.find(i => i.id === id));
|
||||
}
|
||||
|
||||
// ---- Config: settings ----
|
||||
if (req.method === 'GET' && p === '/api/config/settings') return sendJson(res, 200, await readConfigObj('settings'));
|
||||
if (req.method === 'PUT' && p === '/api/config/settings') {
|
||||
const body = await readBody(req);
|
||||
await writeConfig('settings', body);
|
||||
return sendJson(res, 200, body);
|
||||
}
|
||||
|
||||
sendJson(res, 404, { error: 'not found' });
|
||||
} catch (err) {
|
||||
sendJson(res, 500, { error: err.message });
|
||||
|
||||
Reference in new issue
Block a user