Merge pull request #1 from mblanke/codex/create-all-in-one-security-toolkit-with-gui

Expose roadmap APIs and mock dashboard
This commit is contained in:
2025-11-13 15:06:40 -05:00
committed by GitHub
29 changed files with 3717 additions and 1 deletions

30
Dockerfile.kali Normal file
View File

@@ -0,0 +1,30 @@
FROM kalilinux/kali-rolling
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && \
apt-get install -y --no-install-recommends \
python3 \
python3-pip \
python3-venv \
git \
nmap \
masscan \
sqlmap \
hydra \
metasploit-framework \
hashcat \
john \
rainbowcrack \
curl \
ca-certificates && \
apt-get clean && rm -rf /var/lib/apt/lists/*
WORKDIR /opt/goosestrike
COPY requirements.txt requirements.txt
RUN pip3 install --no-cache-dir -r requirements.txt
COPY . /opt/goosestrike
EXPOSE 8000
CMD ["uvicorn", "api:app", "--host", "0.0.0.0", "--port", "8000"]

196
README.md
View File

@@ -1,2 +1,196 @@
# GooseStrike
Mix of Nessus and OCO for CTF events
GooseStrike is an AI-assisted, Canadian-themed offensive security and CTF operations toolkit. It blends subnet discovery, CVE/exploit correlation, task orchestration, and agent-driven planning into one cohesive platform designed **only** for authorized lab environments.
## Features
- **Scanner** wraps `nmap`, preserves MAC/OUI data, captures timestamps/notes, and automatically ingests results with a scan UUID for replay-grade history.
- **Indexer** parses NVD + Exploit-DB + PacketStorm data into `db/exploits.db`, ensuring CVEs, severities, and exploit metadata always live in SQLite for offline ops.
- **FastAPI backend** tracks assets, services, CVEs, scan runs, MITRE ATT&CK suggestions, and alerts while exposing webhook hooks for n8n automations.
- **Task queue + runners** enqueue work for Metasploit, SQLMap, Hydra, OWASP ZAP, the password cracking helper, and now manage every job directly from the dashboard.
- **Password cracking automation** orchestrate Hashcat, John the Ripper, or rainbow-table (`rcrack`) jobs with consistent logging.
- **LLM agents** structured recon / CVE / exploit / privilege escalation / planning agents for high-level guidance.
- **Web UI** Canadian-themed dashboard that now shows assets, scan history, MITRE recommendations, the task queue, and inline forms to submit tool runs or password-cracking jobs inspired by OWASP Nettacker & Exploitivator playbooks.
- **Roadmap + mock data** `/core_snapshot`, `/roadmap`, and `/mock/dashboard-data` feed both the live UI and a static mock dashboard so you can preview GooseStrike with fake sample data (served at `/mockup`).
## GooseStrike Core snapshot
| Highlight | Details |
| --- | --- |
| 🔧 Stack | Nmap, Metasploit, SQLMap, Hydra, OWASP ZAP (all wired into runners) |
| 🧠 AI-ready | External LLM exploit assistant hooks for Claude / HackGPT / Ollama |
| 📚 Offline CVE mirroring | `update_cve.sh` keeps the SQLite CVE/exploit mirror fresh when air-gapped |
| 🗂 Branding kit | ASCII banner, official crest, and PDF-ready branding pack for your ops briefings |
| 📜 CVE helpers | Scan-to-CVE JSON matching scripts pulled from Nettacker / Exploitivator inspirations |
| 📦 Artifact drops | `goosestrike-cve-enabled.zip` & `hackgpt-ai-stack.zip` ship with READMEs + architecture notes |
### Coming next (roadmap you requested)
| Task | Status |
| --- | --- |
| 🐳 Build `docker-compose.goosestrike-full.yml` | ⏳ In progress |
| 🧠 HackGPT API container (linked to n8n) | ⏳ Next up |
| 🌐 Local CVE API server | Pending |
| 🧬 Claude + HackGPT fallback system | Pending |
| 🔄 n8n workflow `.json` import | Pending |
| 🎯 Target "prioritizer" AI agent | Pending |
| 🧭 SVG architecture diagram | Pending |
| 🖥 Dashboard frontend (Armitage-style) | Optional |
| 🔐 C2 bridging to Mythic/Sliver | Optional |
You can query the same table programmatically at `GET /roadmap` or fetch the bullet list at `GET /core_snapshot`.
## Architecture Overview
```
scanner.py -> /ingest/scan ---->
FastAPI (api.py) ---> db/goosestrike.db
| ├─ assets / services / service_cves
| ├─ scan_runs + scan_services (historical state)
| └─ attack_suggestions + alerts
indexer.py -> db/exploits.db --/ |
REST/JSON + Web UI (assets, scans, MITRE)
|
+-> task_queue.py -> runners (metasploit/sqlmap/hydra/zap) -> logs/
+-> app/agents/* (LLM guidance)
+-> n8n webhooks (/webhook/n8n/*)
```
## Quickstart
1. **Clone & install dependencies**
```bash
git clone <repo>
cd GooseStrike
pip install -r requirements.txt # create your own env if desired
```
2. **Run the API + UI**
```bash
uvicorn api:app --reload
```
Visit http://localhost:8000/ for the themed dashboard.
3. **Index CVEs & exploits (required for CVE severity + MITRE context)**
```bash
python indexer.py --nvd data/nvd --exploitdb data/exploitdb --packetstorm data/packetstorm.xml
```
4. **Scan a subnet**
```bash
python scanner.py 192.168.1.0/24 --fast --api http://localhost:8000 --notes "Lab validation"
```
Every run stores MAC/OUI data, timestamps, the CLI metadata, and the raw payload so `/scans` keeps a tamper-evident trail.
5. **Enqueue tool runs**
```bash
python task_queue.py enqueue sqlmap "http://example" '{"level": 2}'
```
Then invoke the appropriate runner (e.g., `python sqlmap_runner.py`) inside your own automation glue.
6. **Crack passwords (hashcat / John / rainbow tables)**
```bash
python task_queue.py enqueue password_cracker hashes '{"crack_tool": "hashcat", "hash_file": "hashes.txt", "wordlist": "/wordlists/rockyou.txt", "mode": 0}'
python password_cracker_runner.py
```
Adjust the JSON for `crack_tool` (`hashcat`, `john`, or `rainbow`) plus specific options like masks, rules, or rainbow-table paths. Prefer the dashboard forms if you want to queue these jobs without hand-writing JSON.
## Customizing the dashboard logo
Drop the exact artwork you want to display into `web/static/uploads/` (PNG/SVG/JPG/WebP). The UI auto-loads the first supported file it finds at startup, so the logo you uploaded appears at the top-right of the header instead of the default crest. If you need to host the logo elsewhere, set `GOOSESTRIKE_LOGO` to a reachable URL (or another `/static/...` path) before launching `uvicorn`.
## API Examples
- **Ingest a host**
```bash
curl -X POST http://localhost:8000/ingest/scan \
-H 'Content-Type: application/json' \
-d '{
"ip": "10.0.0.5",
"mac_address": "00:11:22:33:44:55",
"mac_vendor": "Acme Labs",
"scan": {"scan_id": "demo-001", "scanner": "GooseStrike", "mode": "fast"},
"services": [
{"port": 80, "proto": "tcp", "product": "nginx", "version": "1.23", "cves": ["CVE-2023-12345"]}
]
}'
```
- **List assets**
```bash
curl http://localhost:8000/assets
```
- **Get CVE + exploit context**
```bash
curl http://localhost:8000/cve/CVE-2023-12345
```
- **Review scan history + MITRE suggestions**
```bash
curl http://localhost:8000/scans
curl http://localhost:8000/attack_suggestions
```
- **Roadmap + mock data**
```bash
curl http://localhost:8000/core_snapshot
curl http://localhost:8000/roadmap
curl http://localhost:8000/mock/dashboard-data
```
Preview the populated UI without touching production data at http://localhost:8000/mockup .
- **Queue & review tasks**
```bash
curl -X POST http://localhost:8000/tasks \
-H 'Content-Type: application/json' \
-d '{
"tool": "password_cracker",
"target": "lab-hash",
"params": {"crack_tool": "hashcat", "hash_file": "hashes.txt", "wordlist": "rockyou.txt"}
}'
curl http://localhost:8000/tasks
```
Workers can update entries through `POST /tasks/{task_id}/status` once a run completes.
- **n8n webhook**
```bash
curl -X POST http://localhost:8000/webhook/n8n/new_cve \
-H 'Content-Type: application/json' \
-d '{"cve_id": "CVE-2023-12345", "critical": true}'
```
## Password cracking runner
`password_cracker_runner.py` centralizes cracking workflows:
- **Hashcat** supply `hash_file`, `wordlist` or `mask`, and optional `mode`, `attack_mode`, `rules`, `workload`, or arbitrary `extra_args`.
- **John the Ripper** provide `hash_file` plus switches like `wordlist`, `format`, `rules`, `incremental`, or `potfile`.
- **Rainbow tables** call `rcrack` by specifying `tables_path` along with either `hash_value` or `hash_file` and optional thread counts.
All runs land in `logs/` with timestamped records so you can prove what was attempted during an engagement.
## Kali Linux Docker stack
Need everything preloaded inside Kali? Use the included `Dockerfile.kali` and `docker-compose.kali.yml`:
```bash
docker compose -f docker-compose.kali.yml build
docker compose -f docker-compose.kali.yml up -d api
# run scanners or runners inside dedicated containers
docker compose -f docker-compose.kali.yml run --rm scanner python scanner.py 10.0.0.0/24 --fast --api http://api:8000
docker compose -f docker-compose.kali.yml run --rm worker python password_cracker_runner.py
```
The image layers the GooseStrike codebase on top of `kalilinux/kali-rolling`, installs `nmap`, `masscan`, `sqlmap`, `hydra`, `metasploit-framework`, `hashcat`, `john`, and `rainbowcrack`, and exposes persistent `db/`, `logs/`, and `data/` volumes so scan history and cracking outputs survive container restarts.
## Extending GooseStrike
- **Add a new runner** by following the `runner_utils.run_subprocess` pattern and placing a `<tool>_runner.py` file that interprets task dictionaries safely.
- **Add more agents** by subclassing `app.agents.base_agent.BaseAgent` and exposing a simple `run(context)` helper similar to the existing agents.
- **Enhance the UI** by editing `web/templates/index.html` + `web/static/styles.css` and creating dedicated JS components that consume `/assets`, `/scans`, and `/attack_suggestions`.
- **Integrate orchestration** tools (n8n, Celery, etc.) by interacting with `task_queue.py` and the FastAPI webhook endpoints.
## Safety & Legal Notice
GooseStrike is intended for **authorized security assessments, CTF competitions, and lab research only**. You are responsible for obtaining written permission before scanning, exploiting, or otherwise interacting with any system. The maintainers provide no warranty, and misuse may be illegal.

1086
api.py Normal file

File diff suppressed because it is too large Load Diff

0
app/__init__.py Normal file
View File

32
app/agents/base_agent.py Normal file
View File

@@ -0,0 +1,32 @@
"""Base LLM agent scaffolding for GooseStrike."""
from __future__ import annotations
from dataclasses import dataclass
from typing import Any, Dict
def llm_call(prompt: str) -> str:
"""Placeholder LLM call."""
return "LLM response placeholder. Configure llm_call() to talk to your provider."
@dataclass
class AgentResult:
prompt: str
raw_response: str
recommendations: Dict[str, Any]
class BaseAgent:
name = "base"
def run(self, context: Dict[str, Any]) -> AgentResult:
prompt = self.build_prompt(context)
raw = llm_call(prompt)
return AgentResult(prompt=prompt, raw_response=raw, recommendations=self.parse(raw))
def build_prompt(self, context: Dict[str, Any]) -> str:
raise NotImplementedError
def parse(self, raw: str) -> Dict[str, Any]:
return {"notes": raw.strip()}

28
app/agents/cve_agent.py Normal file
View File

@@ -0,0 +1,28 @@
"""CVE triage agent."""
from __future__ import annotations
from typing import Any, Dict
from .base_agent import AgentResult, BaseAgent
class CVEAgent(BaseAgent):
name = "cve"
def build_prompt(self, context: Dict[str, Any]) -> str:
cves = context.get("cves", [])
lines = ["You are prioritizing CVEs for a legal assessment."]
for cve in cves:
lines.append(
f"{cve.get('cve_id')}: severity={cve.get('severity')} score={cve.get('score')} desc={cve.get('description','')[:120]}"
)
lines.append("Provide prioritized actions and validation steps. No exploit code.")
return "\n".join(lines)
def parse(self, raw: str) -> Dict[str, Any]:
recommendations = [line.strip() for line in raw.split('\n') if line.strip()]
return {"cve_actions": recommendations}
def run(context: Dict[str, Any]) -> AgentResult:
return CVEAgent().run(context)

View File

@@ -0,0 +1,28 @@
"""Exploit correlation agent."""
from __future__ import annotations
from typing import Any, Dict
from .base_agent import AgentResult, BaseAgent
class ExploitAgent(BaseAgent):
name = "exploit"
def build_prompt(self, context: Dict[str, Any]) -> str:
exploits = context.get("exploits", [])
lines = ["Summarize how existing public exploits might apply."]
for exploit in exploits:
lines.append(
f"{exploit.get('source')} -> {exploit.get('title')} references {exploit.get('cve_id')}"
)
lines.append("Provide validation ideas and defensive considerations only.")
return "\n".join(lines)
def parse(self, raw: str) -> Dict[str, Any]:
notes = [line.strip() for line in raw.split('\n') if line.strip()]
return {"exploit_notes": notes}
def run(context: Dict[str, Any]) -> AgentResult:
return ExploitAgent().run(context)

31
app/agents/plan_agent.py Normal file
View File

@@ -0,0 +1,31 @@
"""High level planning agent."""
from __future__ import annotations
from typing import Any, Dict
from .base_agent import AgentResult, BaseAgent
class PlanAgent(BaseAgent):
name = "plan"
def build_prompt(self, context: Dict[str, Any]) -> str:
objectives = context.get("objectives", [])
intel = context.get("intel", [])
lines = ["Create a prioritized plan for the GooseStrike assessment."]
if objectives:
lines.append("Objectives:")
lines.extend(f"- {objective}" for objective in objectives)
if intel:
lines.append("Intel:")
lines.extend(f"- {item}" for item in intel)
lines.append("Return a numbered plan with legal, defensive-minded suggestions.")
return "\n".join(lines)
def parse(self, raw: str) -> Dict[str, Any]:
steps = [line.strip() for line in raw.split('\n') if line.strip()]
return {"plan": steps}
def run(context: Dict[str, Any]) -> AgentResult:
return PlanAgent().run(context)

View File

@@ -0,0 +1,29 @@
"""Privilege escalation agent."""
from __future__ import annotations
from typing import Any, Dict
from .base_agent import AgentResult, BaseAgent
class PrivEscAgent(BaseAgent):
name = "privesc"
def build_prompt(self, context: Dict[str, Any]) -> str:
host = context.get("host")
findings = context.get("findings", [])
lines = ["Suggest legal privilege escalation checks for a lab machine."]
if host:
lines.append(f"Host: {host}")
for finding in findings:
lines.append(f"Finding: {finding}")
lines.append("Provide checklists only; no exploit payloads.")
return "\n".join(lines)
def parse(self, raw: str) -> Dict[str, Any]:
steps = [line.strip() for line in raw.split('\n') if line.strip()]
return {"privesc_checks": steps}
def run(context: Dict[str, Any]) -> AgentResult:
return PrivEscAgent().run(context)

31
app/agents/recon_agent.py Normal file
View File

@@ -0,0 +1,31 @@
"""Reconnaissance agent."""
from __future__ import annotations
from typing import Any, Dict
from .base_agent import AgentResult, BaseAgent
class ReconAgent(BaseAgent):
name = "recon"
def build_prompt(self, context: Dict[str, Any]) -> str:
hosts = context.get("hosts", [])
lines = ["You are advising a legal CTF recon team."]
for host in hosts:
services = host.get("services", [])
service_lines = ", ".join(
f"{svc.get('proto')}/{svc.get('port')} {svc.get('product','?')} {svc.get('version','')}"
for svc in services
)
lines.append(f"Host {host.get('ip')} services: {service_lines}")
lines.append("Suggest safe recon next steps without exploit code.")
return "\n".join(lines)
def parse(self, raw: str) -> Dict[str, Any]:
bullets = [line.strip('- ') for line in raw.split('\n') if line.strip()]
return {"recon_steps": bullets}
def run(context: Dict[str, Any]) -> AgentResult:
return ReconAgent().run(context)

42
docker-compose.kali.yml Normal file
View File

@@ -0,0 +1,42 @@
version: "3.9"
x-goosestrike-service: &goosestrike-service
image: goosestrike-kali:latest
volumes:
- ./db:/opt/goosestrike/db
- ./logs:/opt/goosestrike/logs
- ./data:/opt/goosestrike/data
networks:
- goosenet
services:
api:
<<: *goosestrike-service
build:
context: .
dockerfile: Dockerfile.kali
container_name: goosestrike_api
command: ["uvicorn", "api:app", "--host", "0.0.0.0", "--port", "8000"]
ports:
- "8000:8000"
restart: unless-stopped
scanner:
<<: *goosestrike-service
container_name: goosestrike_scanner
depends_on:
- api
command: ["sleep", "infinity"]
restart: unless-stopped
worker:
<<: *goosestrike-service
container_name: goosestrike_worker
depends_on:
- api
command: ["sleep", "infinity"]
restart: unless-stopped
networks:
goosenet:
driver: bridge

40
hydra_runner.py Normal file
View File

@@ -0,0 +1,40 @@
"""Wrapper for Hydra tasks."""
from __future__ import annotations
from typing import Any, Dict, List
from runner_utils import run_subprocess
def build_command(task: Dict[str, Any]) -> List[str]:
service = task.get("service")
target = task.get("target")
if not service or not target:
raise ValueError("service and target are required")
command = ["hydra", "-t", str(task.get("threads", 4))]
if task.get("username"):
command.extend(["-l", task["username"]])
if task.get("password"):
command.extend(["-p", task["password"]])
if task.get("username_list"):
command.extend(["-L", task["username_list"]])
if task.get("password_list"):
command.extend(["-P", task["password_list"]])
if task.get("options"):
for opt in task["options"]:
command.append(opt)
command.extend([f"{target}", service])
return command
def run_task(task: Dict[str, Any]) -> Dict[str, Any]:
try:
command = build_command(task)
except ValueError as exc:
return {"status": "error", "exit_code": None, "error": str(exc)}
return run_subprocess(command, "hydra")
if __name__ == "__main__":
example = {"service": "ssh", "target": "10.0.0.5", "username": "root", "password_list": "rockyou.txt"}
print(run_task(example))

172
indexer.py Normal file
View File

@@ -0,0 +1,172 @@
"""Index CVEs and public exploit references into SQLite for GooseStrike."""
from __future__ import annotations
import argparse
import json
import re
import sqlite3
from pathlib import Path
from typing import List
CVE_REGEX = re.compile(r"CVE-\d{4}-\d{4,7}")
DB_PATH = Path("db/exploits.db")
def ensure_tables(conn: sqlite3.Connection) -> None:
conn.execute(
"""
CREATE TABLE IF NOT EXISTS cves (
cve_id TEXT PRIMARY KEY,
description TEXT,
severity TEXT,
score REAL
)
"""
)
conn.execute(
"""
CREATE TABLE IF NOT EXISTS exploits (
id INTEGER PRIMARY KEY AUTOINCREMENT,
source TEXT NOT NULL,
title TEXT NOT NULL,
reference TEXT,
path TEXT,
cve_id TEXT,
FOREIGN KEY(cve_id) REFERENCES cves(cve_id)
)
"""
)
conn.commit()
def ingest_nvd(directory: Path, conn: sqlite3.Connection) -> int:
if not directory.exists():
return 0
count = 0
for json_file in sorted(directory.glob("*.json")):
with json_file.open("r", encoding="utf-8") as handle:
data = json.load(handle)
items = data.get("CVE_Items", [])
for item in items:
cve_id = item.get("cve", {}).get("CVE_data_meta", {}).get("ID")
if not cve_id:
continue
description_nodes = item.get("cve", {}).get("description", {}).get("description_data", [])
description = description_nodes[0]["value"] if description_nodes else ""
metrics = item.get("impact", {})
severity = None
score = None
for metric in (metrics.get("baseMetricV3"), metrics.get("baseMetricV2")):
if metric:
data_metric = metric.get("cvssV3" if "cvssV3" in metric else "cvssV2", {})
severity = data_metric.get("baseSeverity") or metric.get("severity")
score = data_metric.get("baseScore") or metric.get("cvssV2", {}).get("baseScore")
break
conn.execute(
"""
INSERT INTO cves(cve_id, description, severity, score)
VALUES(?, ?, ?, ?)
ON CONFLICT(cve_id) DO UPDATE SET
description=excluded.description,
severity=excluded.severity,
score=excluded.score
""",
(cve_id, description, severity, score),
)
count += 1
conn.commit()
return count
def extract_cves_from_text(text: str) -> List[str]:
return list(set(CVE_REGEX.findall(text)))
def ingest_directory(source: str, directory: Path, conn: sqlite3.Connection) -> int:
if not directory.exists():
return 0
count = 0
for file_path in directory.rglob("*"):
if not file_path.is_file():
continue
try:
content = file_path.read_text(encoding="utf-8", errors="ignore")
except OSError:
continue
cves = extract_cves_from_text(content)
title = file_path.stem.replace("_", " ")
reference = str(file_path.relative_to(directory))
if not cves:
conn.execute(
"INSERT INTO exploits(source, title, reference, path, cve_id) VALUES(?,?,?,?,?)",
(source, title, reference, str(file_path), None),
)
count += 1
continue
for cve_id in cves:
conn.execute(
"INSERT INTO exploits(source, title, reference, path, cve_id) VALUES(?,?,?,?,?)",
(source, title, reference, str(file_path), cve_id),
)
count += 1
conn.commit()
return count
def ingest_packetstorm(xml_file: Path, conn: sqlite3.Connection) -> int:
if not xml_file.exists():
return 0
import xml.etree.ElementTree as ET
tree = ET.parse(xml_file)
root = tree.getroot()
count = 0
for item in root.findall("channel/item"):
title = item.findtext("title") or "PacketStorm entry"
link = item.findtext("link")
description = item.findtext("description") or ""
cves = extract_cves_from_text(description)
if not cves:
conn.execute(
"INSERT INTO exploits(source, title, reference, path, cve_id) VALUES(?,?,?,?,?)",
("packetstorm", title, link, None, None),
)
count += 1
continue
for cve_id in cves:
conn.execute(
"INSERT INTO exploits(source, title, reference, path, cve_id) VALUES(?,?,?,?,?)",
("packetstorm", title, link, None, cve_id),
)
count += 1
conn.commit()
return count
def main(argv: Optional[List[str]] = None) -> int:
parser = argparse.ArgumentParser(description="Index CVEs and exploits into SQLite")
parser.add_argument("--nvd", default="data/nvd", help="Directory with NVD JSON dumps")
parser.add_argument(
"--exploitdb", default="data/exploitdb", help="Directory with Exploit-DB entries"
)
parser.add_argument(
"--packetstorm", default="data/packetstorm.xml", help="PacketStorm RSS/Atom file"
)
args = parser.parse_args(argv)
DB_PATH.parent.mkdir(parents=True, exist_ok=True)
conn = sqlite3.connect(DB_PATH)
with conn:
ensure_tables(conn)
nvd_count = ingest_nvd(Path(args.nvd), conn)
edb_count = ingest_directory("exploitdb", Path(args.exploitdb), conn)
ps_count = ingest_packetstorm(Path(args.packetstorm), conn)
print(
f"Indexed {nvd_count} CVEs, {edb_count} Exploit-DB entries, "
f"{ps_count} PacketStorm entries"
)
return 0
if __name__ == "__main__":
raise SystemExit(main())

37
metasploit_runner.py Normal file
View File

@@ -0,0 +1,37 @@
"""Run Metasploit tasks in a controlled way for GooseStrike."""
from __future__ import annotations
from datetime import datetime
from typing import Any, Dict
from runner_utils import LOG_DIR, run_subprocess
def run_task(task: Dict[str, Any]) -> Dict[str, Any]:
module = task.get("module") or task.get("options", {}).get("module")
target = task.get("target") or task.get("options", {}).get("rhosts")
if not module or not target:
return {"status": "error", "exit_code": None, "error": "module and target required"}
opts = task.get("options", {})
rc_lines = [f"use {module}", f"set RHOSTS {target}"]
for key, value in opts.items():
if key.lower() == "module" or key.lower() == "rhosts":
continue
rc_lines.append(f"set {key.upper()} {value}")
rc_lines.extend(["run", "exit"])
rc_path = LOG_DIR / f"metasploit_{datetime.utcnow().strftime('%Y%m%d-%H%M%S')}.rc"
rc_path.write_text("\n".join(rc_lines), encoding="utf-8")
command = ["msfconsole", "-q", "-r", str(rc_path)]
return run_subprocess(command, "metasploit")
if __name__ == "__main__":
example = {
"module": "auxiliary/scanner/portscan/tcp",
"target": "127.0.0.1",
"options": {"THREADS": 4},
}
print(run_task(example))

109
mitre_mapping.py Normal file
View File

@@ -0,0 +1,109 @@
"""Lightweight MITRE ATT&CK suggestion helpers.
This module borrows ideas from community tooling such as OWASP Nettacker
and Exploitivator by correlating discovered services and CVEs with the
most relevant ATT&CK techniques. It does not attempt to be exhaustive;
instead it provides explainable heuristics that can be stored alongside
scan records so analysts always have context for next steps.
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import Iterable, List, Optional
@dataclass
class MitreSuggestion:
technique_id: str
tactic: str
name: str
description: str
related_cve: Optional[str]
severity: str
@dataclass
class MitreRule:
technique_id: str
tactic: str
name: str
description: str
ports: Optional[Iterable[int]] = None
protocols: Optional[Iterable[str]] = None
product_keywords: Optional[Iterable[str]] = None
cve_required: bool = False
MITRE_RULES: List[MitreRule] = [
MitreRule(
technique_id="T1190",
tactic="Initial Access",
name="Exploit Public-Facing Application",
description="HTTP/S service exposes attack surface that mirrors the"
" public exploitation stage emphasized by Nettacker's web modules",
ports={80, 443, 8080, 8443},
protocols={"tcp"},
),
MitreRule(
technique_id="T1133",
tactic="Initial Access",
name="External Remote Services",
description="SSH/RDP/VNC listeners enable credential attacks similar"
" to Exploitivator's service runners.",
ports={22, 3389, 5900},
protocols={"tcp"},
),
MitreRule(
technique_id="T1047",
tactic="Execution",
name="Windows Management Instrumentation",
description="SMB/RPC services align with remote execution and lateral"
" movement playbooks.",
ports={135, 139, 445},
protocols={"tcp"},
),
MitreRule(
technique_id="T1068",
tactic="Privilege Escalation",
name="Exploitation for Privilege Escalation",
description="Service exposes CVEs that can deliver privilege gains.",
cve_required=True,
),
]
def _match_rule(rule: MitreRule, service: object, has_cve: bool) -> bool:
proto = getattr(service, "proto", None)
port = getattr(service, "port", None)
product = (getattr(service, "product", None) or "").lower()
if rule.protocols and proto not in rule.protocols:
return False
if rule.ports and port not in rule.ports:
return False
if rule.cve_required and not has_cve:
return False
if rule.product_keywords and not any(keyword in product for keyword in rule.product_keywords):
return False
return True
def generate_attack_suggestions(asset_label: str, services: Iterable[object]) -> List[MitreSuggestion]:
"""Return MITRE suggestions based on service metadata and CVE presence."""
suggestions: List[MitreSuggestion] = []
for service in services:
cves = getattr(service, "cves", None) or []
severity = "critical" if cves else "info"
for rule in MITRE_RULES:
if not _match_rule(rule, service, bool(cves)):
continue
suggestions.append(
MitreSuggestion(
technique_id=rule.technique_id,
tactic=rule.tactic,
name=rule.name,
description=f"{rule.description} Observed on {asset_label}.",
related_cve=cves[0] if cves else None,
severity=severity,
)
)
return suggestions

119
password_cracker_runner.py Normal file
View File

@@ -0,0 +1,119 @@
"""Password cracking task runner for hashcat, John the Ripper, and rainbow tables."""
from __future__ import annotations
from typing import Any, Callable, Dict, List
from runner_utils import run_subprocess
def build_hashcat_command(task: Dict[str, Any]) -> List[str]:
hash_file = task.get("hash_file")
if not hash_file:
raise ValueError("hash_file is required for hashcat tasks")
command: List[str] = ["hashcat"]
if task.get("mode") is not None:
command.extend(["-m", str(task["mode"])])
if task.get("attack_mode") is not None:
command.extend(["-a", str(task["attack_mode"])])
if task.get("workload") is not None:
command.extend(["-w", str(task["workload"])])
if task.get("session"):
command.extend(["--session", task["session"]])
if task.get("potfile"):
command.extend(["--potfile-path", task["potfile"]])
if task.get("rules"):
for rule in task["rules"]:
command.extend(["-r", rule])
if task.get("extra_args"):
command.extend(task["extra_args"])
command.append(hash_file)
if task.get("wordlist"):
command.append(task["wordlist"])
elif task.get("mask"):
command.append(task["mask"])
else:
raise ValueError("hashcat tasks require either a wordlist or mask")
return command
def build_john_command(task: Dict[str, Any]) -> List[str]:
hash_file = task.get("hash_file")
if not hash_file:
raise ValueError("hash_file is required for john tasks")
command: List[str] = ["john"]
if task.get("wordlist"):
command.append(f"--wordlist={task['wordlist']}")
if task.get("format"):
command.append(f"--format={task['format']}")
if task.get("rules"):
command.append(f"--rules={task['rules']}")
if task.get("session"):
command.append(f"--session={task['session']}")
if task.get("potfile"):
command.append(f"--pot={task['potfile']}")
if task.get("incremental"):
command.append("--incremental")
if task.get("extra_args"):
command.extend(task["extra_args"])
command.append(hash_file)
return command
def build_rainbow_command(task: Dict[str, Any]) -> List[str]:
tables_path = task.get("tables_path")
if not tables_path:
raise ValueError("tables_path is required for rainbow table tasks")
command: List[str] = ["rcrack", tables_path]
if task.get("hash_value"):
command.append(task["hash_value"])
elif task.get("hash_file"):
command.extend(["-f", task["hash_file"]])
else:
raise ValueError("Provide hash_value or hash_file for rainbow table tasks")
if task.get("threads"):
command.extend(["-t", str(task["threads"])])
if task.get("extra_args"):
command.extend(task["extra_args"])
return command
COMMAND_BUILDERS: Dict[str, Callable[[Dict[str, Any]], List[str]]] = {
"hashcat": build_hashcat_command,
"john": build_john_command,
"johntheripper": build_john_command,
"rainbow": build_rainbow_command,
"rcrack": build_rainbow_command,
}
def run_task(task: Dict[str, Any]) -> Dict[str, Any]:
tool = task.get("crack_tool", task.get("tool", "hashcat")).lower()
builder = COMMAND_BUILDERS.get(tool)
if builder is None:
return {
"status": "error",
"exit_code": None,
"error": f"Unsupported password cracking tool: {tool}",
}
try:
command = builder(task)
except ValueError as exc:
return {"status": "error", "exit_code": None, "error": str(exc)}
log_prefix = f"crack_{tool}"
return run_subprocess(command, log_prefix)
if __name__ == "__main__":
demo_task = {
"crack_tool": "hashcat",
"hash_file": "hashes.txt",
"wordlist": "/usr/share/wordlists/rockyou.txt",
"mode": 0,
"attack_mode": 0,
}
print(run_task(demo_task))

6
requirements.txt Normal file
View File

@@ -0,0 +1,6 @@
fastapi
uvicorn
requests
pydantic
pytest
jinja2

47
runner_utils.py Normal file
View File

@@ -0,0 +1,47 @@
"""Shared helpers for GooseStrike tool runners."""
from __future__ import annotations
import subprocess
from datetime import datetime
from pathlib import Path
from typing import Any, Dict, List, Optional
LOG_DIR = Path("logs")
LOG_DIR.mkdir(parents=True, exist_ok=True)
def run_subprocess(command: List[str], log_prefix: str, stdin: Optional[str] = None) -> Dict[str, Any]:
timestamp = datetime.utcnow().strftime("%Y%m%d-%H%M%S")
log_path = LOG_DIR / f"{log_prefix}_{timestamp}.log"
with log_path.open("w", encoding="utf-8") as log_file:
log_file.write(f"COMMAND: {' '.join(command)}\n")
if stdin:
log_file.write("STDIN:\n")
log_file.write(stdin)
log_file.write("\n--- END STDIN ---\n")
try:
proc = subprocess.run(
command,
input=stdin,
text=True,
capture_output=True,
check=False,
)
except FileNotFoundError:
log_file.write(f"ERROR: command not found: {command[0]}\n")
return {
"status": "error",
"exit_code": None,
"error": f"Command not found: {command[0]}",
"log_path": str(log_path),
}
log_file.write("STDOUT:\n")
log_file.write(proc.stdout)
log_file.write("\nSTDERR:\n")
log_file.write(proc.stderr)
status = "success" if proc.returncode == 0 else "failed"
return {
"status": status,
"exit_code": proc.returncode,
"log_path": str(log_path),
}

269
scanner.py Normal file
View File

@@ -0,0 +1,269 @@
"""Subnet scanning utility for GooseStrike.
This module wraps nmap (and optionally masscan) to inventory services
across a CIDR range and forward the parsed results to the GooseStrike
FastAPI backend.
"""
from __future__ import annotations
import argparse
import json
import shlex
import subprocess
import sys
import uuid
from dataclasses import dataclass, field
from typing import Dict, List, Optional, Tuple
import xml.etree.ElementTree as ET
import requests
API_DEFAULT = "http://localhost:8000"
def run_command(cmd: List[str]) -> str:
"""Execute a command and return stdout, raising on errors."""
try:
completed = subprocess.run(
cmd,
check=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)
except FileNotFoundError as exc:
raise RuntimeError(f"Required command not found: {cmd[0]}") from exc
except subprocess.CalledProcessError as exc:
raise RuntimeError(
f"Command failed ({cmd}): {exc.stderr.strip() or exc.stdout.strip()}"
) from exc
return completed.stdout
@dataclass
class ServiceResult:
port: int
proto: str
product: Optional[str] = None
version: Optional[str] = None
extra: Dict[str, str] = field(default_factory=dict)
@dataclass
class HostResult:
ip: str
hostname: Optional[str]
services: List[ServiceResult] = field(default_factory=list)
mac_address: Optional[str] = None
mac_vendor: Optional[str] = None
@dataclass
class ScanMetadata:
scan_id: str
started_at: Optional[str]
finished_at: Optional[str]
scanner: str
mode: str
notes: Optional[str]
def build_nmap_command(
cidr: str, mode: str, rate: Optional[int], extra_args: Optional[List[str]] = None
) -> List[str]:
"""Construct the nmap command with optional mode/rate/custom arguments."""
cmd: List[str] = ["nmap"]
if mode == "fast":
cmd.extend(["-T4", "-F"])
elif mode == "full":
cmd.extend(["-T4", "-p-"])
if rate:
cmd.extend(["--min-rate", str(rate)])
if extra_args:
cmd.extend(extra_args)
cmd.extend(["-sV", "-oX", "-", cidr])
return cmd
def parse_nmap_xml(xml_content: str) -> Tuple[List[HostResult], Optional[str], Optional[str]]:
root = ET.fromstring(xml_content)
hosts: List[HostResult] = []
started_at = root.get("startstr")
finished_el = root.find("runstats/finished")
finished_at = finished_el.get("timestr") if finished_el is not None else None
for host in root.findall("host"):
status = host.find("status")
if status is not None and status.get("state") != "up":
continue
address = host.find("address[@addrtype='ipv4']") or host.find("address[@addrtype='ipv6']")
if address is None:
continue
ip = address.get("addr")
hostname_el = host.find("hostnames/hostname")
hostname = hostname_el.get("name") if hostname_el is not None else None
mac_el = host.find("address[@addrtype='mac']")
mac_address = mac_el.get("addr") if mac_el is not None else None
mac_vendor = mac_el.get("vendor") if mac_el is not None else None
services: List[ServiceResult] = []
for port in host.findall("ports/port"):
state = port.find("state")
if state is None or state.get("state") != "open":
continue
service_el = port.find("service")
service = ServiceResult(
port=int(port.get("portid", 0)),
proto=port.get("protocol", "tcp"),
product=service_el.get("product") if service_el is not None else None,
version=service_el.get("version") if service_el is not None else None,
extra={},
)
if service_el is not None:
for key in ("name", "extrainfo", "ostype"):
value = service_el.get(key)
if value:
service.extra[key] = value
services.append(service)
if services:
hosts.append(
HostResult(
ip=ip,
hostname=hostname,
services=services,
mac_address=mac_address,
mac_vendor=mac_vendor,
)
)
return hosts, started_at, finished_at
def send_to_api(hosts: List[HostResult], api_base: str, scan_meta: ScanMetadata) -> None:
ingest_url = f"{api_base.rstrip('/')}/ingest/scan"
session = requests.Session()
for host in hosts:
payload = {
"ip": host.ip,
"hostname": host.hostname,
"mac_address": host.mac_address,
"mac_vendor": host.mac_vendor,
"scan": {
"scan_id": scan_meta.scan_id,
"scanner": scan_meta.scanner,
"mode": scan_meta.mode,
"started_at": scan_meta.started_at,
"completed_at": scan_meta.finished_at,
"notes": scan_meta.notes,
},
"services": [
{
"port": s.port,
"proto": s.proto,
"product": s.product,
"version": s.version,
"extra": s.extra,
}
for s in host.services
],
}
response = session.post(ingest_url, json=payload, timeout=30)
response.raise_for_status()
def scan_and_ingest(
cidr: str,
mode: str,
rate: Optional[int],
api_base: str,
notes: Optional[str],
scanner_name: str,
scan_id: Optional[str],
extra_args: Optional[List[str]] = None,
) -> None:
cmd = build_nmap_command(cidr, mode, rate, extra_args)
print(f"[+] Running {' '.join(cmd)}")
xml_result = run_command(cmd)
hosts, started_at, finished_at = parse_nmap_xml(xml_result)
print(f"[+] Parsed {len(hosts)} hosts with open services")
if not hosts:
return
meta = ScanMetadata(
scan_id=scan_id or str(uuid.uuid4()),
started_at=started_at,
finished_at=finished_at,
scanner=scanner_name,
mode=mode,
notes=notes,
)
send_to_api(hosts, api_base, meta)
print("[+] Results sent to API")
def main(argv: Optional[List[str]] = None) -> int:
parser = argparse.ArgumentParser(description="GooseStrike subnet scanner")
parser.add_argument("cidr", help="CIDR range to scan, e.g. 10.0.0.0/24")
mode_group = parser.add_mutually_exclusive_group()
mode_group.add_argument("--fast", action="store_true", help="Fast top-port scan")
mode_group.add_argument("--full", action="store_true", help="Full port scan")
parser.add_argument("--rate", type=int, help="Optional nmap min rate")
parser.add_argument("--api", default=API_DEFAULT, help="API base URL")
parser.add_argument(
"--no-upload",
action="store_true",
help="Skip uploading results (useful for local testing)",
)
parser.add_argument("--notes", help="Optional operator notes recorded with the scan")
parser.add_argument("--scanner-name", default="GooseStrike nmap", help="Logical scanner identifier")
parser.add_argument("--scan-id", help="Optional scan UUID for correlating uploads")
parser.add_argument(
"--nmap-args",
help=(
"Extra nmap arguments (quoted) to mirror advanced Recorded Future command "
"examples, e.g. \"-sC --script vuln -Pn\""
),
)
args = parser.parse_args(argv)
mode = "standard"
if args.fast:
mode = "fast"
elif args.full:
mode = "full"
extra_args: Optional[List[str]] = None
if args.nmap_args:
extra_args = shlex.split(args.nmap_args)
try:
cmd = build_nmap_command(args.cidr, mode, args.rate, extra_args)
xml_result = run_command(cmd)
hosts, started_at, finished_at = parse_nmap_xml(xml_result)
meta = ScanMetadata(
scan_id=args.scan_id or str(uuid.uuid4()),
started_at=started_at,
finished_at=finished_at,
scanner=args.scanner_name,
mode=mode,
notes=args.notes,
)
print(
json.dumps(
[
{
**host.__dict__,
"services": [service.__dict__ for service in host.services],
}
for host in hosts
],
indent=2,
)
)
if not args.no_upload and hosts:
send_to_api(hosts, args.api, meta)
print("[+] Uploaded results to API")
return 0
except Exception as exc: # pylint: disable=broad-except
print(f"[-] {exc}", file=sys.stderr)
return 1
if __name__ == "__main__":
raise SystemExit(main())

34
sqlmap_runner.py Normal file
View File

@@ -0,0 +1,34 @@
"""Wrapper for sqlmap tasks."""
from __future__ import annotations
from typing import Any, Dict, List
from runner_utils import run_subprocess
def build_command(task: Dict[str, Any]) -> List[str]:
if not task.get("target_url"):
raise ValueError("target_url is required")
command = ["sqlmap", "-u", task["target_url"], "--batch"]
options = task.get("options", {})
for key, value in options.items():
flag = f"--{key.replace('_', '-')}"
if isinstance(value, bool):
if value:
command.append(flag)
else:
command.extend([flag, str(value)])
return command
def run_task(task: Dict[str, Any]) -> Dict[str, Any]:
try:
command = build_command(task)
except ValueError as exc:
return {"status": "error", "exit_code": None, "error": str(exc)}
return run_subprocess(command, "sqlmap")
if __name__ == "__main__":
example = {"target_url": "http://example.com/vuln.php?id=1", "options": {"level": 2}}
print(run_task(example))

134
task_queue.py Normal file
View File

@@ -0,0 +1,134 @@
"""SQLite-backed task queue for GooseStrike."""
from __future__ import annotations
import argparse
import json
import sqlite3
from datetime import datetime
from pathlib import Path
from typing import Any, Dict, Optional
DB_PATH = Path("db/tasks.db")
DB_PATH.parent.mkdir(parents=True, exist_ok=True)
def get_conn() -> sqlite3.Connection:
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
return conn
def ensure_tables() -> None:
with get_conn() as conn:
conn.execute(
"""
CREATE TABLE IF NOT EXISTS tasks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
tool TEXT NOT NULL,
target TEXT,
params_json TEXT,
status TEXT NOT NULL DEFAULT 'pending',
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
started_at TEXT,
finished_at TEXT,
result_json TEXT
)
"""
)
conn.commit()
def enqueue_task(tool: str, target: Optional[str], params: Dict[str, Any]) -> int:
ensure_tables()
with get_conn() as conn:
cur = conn.execute(
"INSERT INTO tasks(tool, target, params_json) VALUES(?,?,?)",
(tool, target, json.dumps(params)),
)
conn.commit()
return cur.lastrowid
def fetch_next_task(tool: Optional[str] = None) -> Optional[Dict[str, Any]]:
ensure_tables()
with get_conn() as conn:
query = "SELECT * FROM tasks WHERE status='pending'"
params: tuple = ()
if tool:
query += " AND tool=?"
params = (tool,)
query += " ORDER BY created_at LIMIT 1"
row = conn.execute(query, params).fetchone()
if not row:
return None
conn.execute(
"UPDATE tasks SET status='running', started_at=? WHERE id=?",
(datetime.utcnow().isoformat(), row["id"]),
)
conn.commit()
return dict(row)
def update_task_status(task_id: int, status: str, result: Optional[Dict[str, Any]] = None) -> None:
ensure_tables()
with get_conn() as conn:
conn.execute(
"UPDATE tasks SET status=?, finished_at=?, result_json=? WHERE id=?",
(
status,
datetime.utcnow().isoformat(),
json.dumps(result) if result is not None else None,
task_id,
),
)
conn.commit()
def list_tasks() -> None:
ensure_tables()
with get_conn() as conn:
rows = conn.execute(
"SELECT id, tool, target, status, created_at, started_at, finished_at FROM tasks ORDER BY id"
).fetchall()
for row in rows:
print(dict(row))
def cli(argv: Optional[list] = None) -> int:
parser = argparse.ArgumentParser(description="Manage GooseStrike task queue")
sub = parser.add_subparsers(dest="cmd", required=True)
enqueue_cmd = sub.add_parser("enqueue", help="Enqueue a new task")
enqueue_cmd.add_argument("tool")
enqueue_cmd.add_argument("target", nargs="?")
enqueue_cmd.add_argument("params", help="JSON string with parameters")
sub.add_parser("list", help="List tasks")
fetch_cmd = sub.add_parser("next", help="Fetch next task")
fetch_cmd.add_argument("--tool")
update_cmd = sub.add_parser("update", help="Update task status")
update_cmd.add_argument("task_id", type=int)
update_cmd.add_argument("status")
update_cmd.add_argument("result", nargs="?", help="JSON result payload")
args = parser.parse_args(argv)
if args.cmd == "enqueue":
params = json.loads(args.params)
task_id = enqueue_task(args.tool, args.target, params)
print(f"Enqueued task {task_id}")
elif args.cmd == "list":
list_tasks()
elif args.cmd == "next":
task = fetch_next_task(args.tool)
print(task or "No pending tasks")
elif args.cmd == "update":
result = json.loads(args.result) if args.result else None
update_task_status(args.task_id, args.status, result)
print("Task updated")
return 0
if __name__ == "__main__":
raise SystemExit(cli())

View File

@@ -0,0 +1,171 @@
from __future__ import annotations
import json
import sqlite3
from pathlib import Path
from typing import Any
import pytest
from fastapi.testclient import TestClient
import api
import scanner
NMAP_XML = """<?xml version='1.0'?>
<nmaprun startstr="2024-01-01 10:00 UTC">
<host>
<status state="up" />
<address addr="192.168.1.10" addrtype="ipv4" />
<address addr="00:11:22:33:44:55" addrtype="mac" vendor="TestVendor" />
<hostnames>
<hostname name="web" />
</hostnames>
<ports>
<port protocol="tcp" portid="80">
<state state="open" />
<service name="http" product="Apache httpd" version="2.4.57" />
</port>
</ports>
</host>
<runstats>
<finished timestr="2024-01-01 10:01 UTC" />
</runstats>
</nmaprun>
"""
def test_parse_nmap_xml():
hosts, started, finished = scanner.parse_nmap_xml(NMAP_XML)
assert len(hosts) == 1
assert hosts[0].ip == "192.168.1.10"
assert hosts[0].mac_address == "00:11:22:33:44:55"
assert hosts[0].services[0].port == 80
assert started.startswith("2024")
assert finished.endswith("UTC")
def test_scanner_main_monkeypatched(monkeypatch):
class DummyProcess:
def __init__(self, stdout: str):
self.stdout = stdout
self.stderr = ""
self.returncode = 0
captured: dict[str, Any] = {}
def fake_run(cmd: Any, check: bool, stdout, stderr, text): # pylint: disable=unused-argument
captured["cmd"] = cmd
return DummyProcess(NMAP_XML)
monkeypatch.setattr(scanner, "subprocess", type("S", (), {"run": staticmethod(fake_run)}))
exit_code = scanner.main(
["192.168.1.0/24", "--no-upload", "--nmap-args", "-Pn --script vuln"]
)
assert exit_code == 0
assert "-Pn" in captured["cmd"]
assert "--script" in captured["cmd"]
def test_build_nmap_command_accepts_custom_args():
cmd = scanner.build_nmap_command(
"10.0.0.0/24", "standard", None, ["-Pn", "--script", "vuln"]
)
assert cmd[0] == "nmap"
assert cmd[-1] == "10.0.0.0/24"
assert cmd[1:4] == ["-Pn", "--script", "vuln"]
@pytest.fixture()
def temp_db(tmp_path: Path, monkeypatch):
db_path = tmp_path / "api.db"
exploit_db = tmp_path / "exploits.db"
tasks_db = tmp_path / "tasks.db"
monkeypatch.setattr(api, "DB_PATH", db_path)
monkeypatch.setattr(api, "EXPLOIT_DB_PATH", exploit_db)
monkeypatch.setattr(api.task_queue, "DB_PATH", tasks_db)
api.initialize_db()
api.task_queue.ensure_tables()
conn = sqlite3.connect(exploit_db)
conn.execute(
"CREATE TABLE IF NOT EXISTS cves (cve_id TEXT PRIMARY KEY, description TEXT, severity TEXT, score REAL)"
)
conn.execute(
"INSERT OR REPLACE INTO cves(cve_id, description, severity, score) VALUES(?,?,?,?)",
("CVE-2023-12345", "Test vuln", "HIGH", 8.8),
)
conn.close()
return db_path
def test_ingest_and_list_assets(temp_db):
client = TestClient(api.app)
payload = {
"ip": "192.168.1.10",
"hostname": "web",
"mac_address": "00:11:22:33:44:55",
"mac_vendor": "TestVendor",
"scan": {"scan_id": "unit-test-scan", "scanner": "pytest", "mode": "fast"},
"services": [
{
"port": 80,
"proto": "tcp",
"product": "Apache",
"version": "2.4.57",
"extra": {"name": "http"},
"cves": ["CVE-2023-12345"],
}
],
}
response = client.post("/ingest/scan", json=payload)
assert response.status_code == 200
asset = response.json()
assert asset["mac_address"] == "00:11:22:33:44:55"
list_response = client.get("/assets")
assert list_response.status_code == 200
assets = list_response.json()
assert assets[0]["services"][0]["cves"] == ["CVE-2023-12345"]
vuln = assets[0]["services"][0]["vulnerabilities"][0]
assert vuln["severity"] == "HIGH"
conn = sqlite3.connect(temp_db)
rows = conn.execute("SELECT COUNT(*) FROM services").fetchone()[0]
assert rows == 1
scan_runs = conn.execute("SELECT COUNT(*) FROM scan_runs").fetchone()[0]
assert scan_runs == 1
conn.close()
scans = client.get("/scans").json()
assert scans[0]["scan_id"] == "unit-test-scan"
suggestions = client.get("/attack_suggestions").json()
assert any(item["technique_id"] == "T1068" for item in suggestions)
def test_task_queue_endpoints(temp_db):
client = TestClient(api.app)
response = client.post(
"/tasks",
json={
"tool": "password_cracker",
"target": "lab-hashes",
"params": {"hash_file": "hashes.txt", "wordlist": "rockyou.txt"},
},
)
assert response.status_code == 201
task = response.json()
assert task["tool"] == "password_cracker"
list_response = client.get("/tasks")
assert list_response.status_code == 200
tasks = list_response.json()
assert len(tasks) == 1
assert tasks[0]["target"] == "lab-hashes"
update_response = client.post(
f"/tasks/{task['id']}/status",
json={"status": "completed", "result": {"exit_code": 0}},
)
assert update_response.status_code == 200
assert update_response.json()["status"] == "completed"

View File

@@ -0,0 +1,16 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 300 200" role="img" aria-label="GooseStrike mark">
<defs>
<style>
.goose-fill { fill: #0b0c0d; }
.accent-red { fill: #d90429; }
.leaf-fill { fill: #d90429; }
.type { font-family: 'Oswald', 'Arial Black', sans-serif; font-size: 32px; font-weight: 700; letter-spacing: 2px; fill: #0b0c0d; }
</style>
</defs>
<rect width="300" height="200" fill="none" />
<path class="goose-fill" d="M206 33c-32.7-14.7-78-16.4-104.1 8.4-12.9 12.4-19.7 29.6-16.5 48.5 2.7 16 11.8 29.6 24.8 38.8-11.9 6.4-19.4 17.5-19.4 29.6 0 22 21.5 39.1 56.7 39.1 20.6 0 37.6-4.8 50.7-13.7l-9.5-15c-8.9 6.3-22 10.6-37.5 10.6-19.6 0-32.8-8.2-32.8-20 0-7.7 8.1-15.5 22.8-17.7l26.6-4c36.4-5.5 63.3-28.2 63.3-66.1-.1-28.4-17.1-51.7-45.1-62.5zm-12.6 101.8-24.9 3.7c-15.5-9.4-26-25.8-26-43.4 0-32.7 34.2-52 68.5-43.9 23.3 5.5 36.7 22.5 36.7 43.6 0 27.3-21.3 34.4-54.3 40z"/>
<path class="goose-fill" d="M210.7 48.4c9.9 2.7 19.1 8.3 25.6 16.1 4.6-6.5 5.2-14.8 1.2-20.8-5.3-7.8-17.5-11.9-30-10.4-5.7.7-10.5 2.5-14 5 6.9 1.2 12.8 3.4 17.2 6.1z"/>
<path class="accent-red" d="M227.4 60.4c-2.9 3.8-7.5 6.7-13.2 8.2 4 4 9.2 6.6 14.1 7.3 6.3.9 12.5-1 16.4-5.3 1.9-2.1 3.1-4.6 3.3-7.2-6.2 1.6-13.1 1.2-20.6-3z"/>
<polygon class="leaf-fill" points="122 88 132 88 135 72 142 88 152 88 144 101 154 112 140 113 135 128 128 112 116 114 125 101"/>
<text class="type" x="40" y="182">GOOSE STRIKE</text>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

368
web/static/styles.css Normal file
View File

@@ -0,0 +1,368 @@
:root {
--black: #0b0c0d;
--grey: #4b4f58;
--white: #f5f5f5;
--red: #d90429;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: var(--black);
color: var(--white);
min-height: 100vh;
display: flex;
flex-direction: column;
}
header {
display: flex;
align-items: center;
gap: 1rem;
padding: 1.5rem;
background: linear-gradient(135deg, var(--black), var(--grey));
border-bottom: 4px solid var(--red);
}
section.hero-panel {
background: linear-gradient(120deg, rgba(0, 0, 0, 0.95), rgba(11, 12, 13, 0.85), rgba(75, 79, 88, 0.85));
border: 1px solid rgba(255, 255, 255, 0.2);
display: grid;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
gap: 2rem;
box-shadow: 0 25px 45px rgba(0, 0, 0, 0.55);
}
.hero-text h2 {
margin-top: 0;
font-size: 1.6rem;
}
.hero-list {
list-style: none;
padding-left: 0;
margin: 0 0 1.5rem 0;
display: flex;
flex-direction: column;
gap: 0.4rem;
}
.hero-list li {
font-size: 1rem;
letter-spacing: 0.02em;
}
.hero-meta h3 {
margin-bottom: 0.5rem;
color: var(--white);
}
.hero-meta table {
width: 100%;
border-collapse: collapse;
font-size: 0.95rem;
}
.hero-meta th,
.hero-meta td {
border: 1px solid rgba(255, 255, 255, 0.2);
padding: 0.45rem 0.6rem;
text-align: left;
}
.hero-meta th {
background: rgba(255, 255, 255, 0.08);
}
.hero-visual {
display: flex;
align-items: center;
justify-content: center;
}
.hero-visual-inner {
width: min(320px, 100%);
border-radius: 18px;
background: radial-gradient(circle at top, rgba(255, 255, 255, 0.12), rgba(0, 0, 0, 0.75));
padding: 1.5rem;
box-shadow: 0 30px 60px rgba(0, 0, 0, 0.65);
}
.hero-visual img {
width: 100%;
height: auto;
display: block;
}
.canadian-flag {
width: 80px;
height: 48px;
border: 2px solid var(--white);
border-radius: 4px;
background: linear-gradient(90deg, var(--red) 0 25%, var(--white) 25% 75%, var(--red) 75% 100%);
box-shadow: 0 0 12px rgba(0, 0, 0, 0.45);
}
.goose-logo {
margin-left: auto;
width: 120px;
height: 120px;
border-radius: 12px;
border: 2px solid rgba(255, 255, 255, 0.85);
background: linear-gradient(145deg, rgba(255, 255, 255, 0.15), rgba(0, 0, 0, 0.45));
padding: 0.4rem;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 10px 28px rgba(0, 0, 0, 0.65);
}
.goose-logo img {
width: 100%;
height: 100%;
object-fit: contain;
filter: drop-shadow(0 6px 18px rgba(0, 0, 0, 0.75));
}
main {
flex: 1;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 1.5rem;
padding: 1.5rem;
}
section {
background: rgba(255, 255, 255, 0.05);
border: 1px solid var(--grey);
border-radius: 8px;
padding: 1rem;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.35);
}
.wide {
grid-column: span 2;
}
#assets article {
border: 1px solid var(--grey);
border-radius: 6px;
padding: 0.75rem;
margin-bottom: 0.75rem;
background: rgba(0, 0, 0, 0.3);
}
#assets .meta {
font-size: 0.85rem;
color: rgba(255, 255, 255, 0.7);
margin-bottom: 0.35rem;
}
#scans,
#mitre {
max-height: 60vh;
overflow-y: auto;
}
#scans article,
#mitre article {
border: 1px solid rgba(255, 255, 255, 0.15);
border-radius: 6px;
padding: 0.75rem;
margin-bottom: 0.75rem;
background: rgba(255, 255, 255, 0.03);
}
#mitre h3,
#scans h3 {
margin-top: 0;
color: var(--red);
}
.service-line {
display: flex;
justify-content: space-between;
align-items: center;
}
.vuln-list {
margin-top: 0.5rem;
display: flex;
flex-wrap: wrap;
gap: 0.35rem;
}
.badge {
display: inline-block;
padding: 0.2rem 0.45rem;
border-radius: 4px;
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.severity-critical {
background: var(--red);
color: var(--white);
}
.severity-high {
background: #ff6b6b;
color: var(--black);
}
.severity-medium {
background: #ffba08;
color: var(--black);
}
.severity-low,
.severity-info,
.severity-unknown {
background: rgba(255, 255, 255, 0.2);
color: var(--white);
}
.scan-card .meta,
#mitre .meta {
font-size: 0.85rem;
color: rgba(255, 255, 255, 0.75);
}
#assets h3 {
margin: 0 0 0.5rem 0;
color: var(--red);
}
#assets ul {
list-style: none;
padding: 0;
margin: 0;
}
#assets li {
display: flex;
flex-direction: column;
margin-bottom: 0.5rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
#assets li strong {
font-size: 1.1rem;
}
footer {
text-align: center;
padding: 1rem;
background: var(--grey);
color: var(--white);
}
form.card-form {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.form-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 0.75rem;
}
label {
font-size: 0.9rem;
display: flex;
flex-direction: column;
gap: 0.35rem;
}
input,
textarea,
select {
background: rgba(0, 0, 0, 0.6);
border: 1px solid rgba(255, 255, 255, 0.2);
color: var(--white);
padding: 0.5rem;
border-radius: 4px;
font-size: 1rem;
}
textarea {
resize: vertical;
}
.form-actions {
display: flex;
align-items: center;
gap: 1rem;
}
button {
background: var(--red);
color: var(--white);
border: none;
padding: 0.6rem 1.25rem;
border-radius: 4px;
font-weight: 600;
cursor: pointer;
}
button:hover {
background: #ff4d4d;
}
.helper {
font-size: 0.85rem;
color: rgba(255, 255, 255, 0.7);
}
.helper.error {
color: #ff8c8c;
}
#tasks {
max-height: 60vh;
overflow-y: auto;
}
.task-card {
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 6px;
padding: 0.75rem;
margin-bottom: 0.75rem;
background: rgba(0, 0, 0, 0.3);
}
.task-line {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.35rem;
}
.status-pill {
background: rgba(255, 255, 255, 0.2);
color: var(--white);
}
.status-running {
background: #ffba08;
color: var(--black);
}
.status-completed {
background: #06d6a0;
color: var(--black);
}
.status-failed,
.status-error {
background: #ef476f;
color: var(--white);
}

View File

View File

@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="640" height="640" viewBox="0 0 640 640" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="GooseStrike official crest">
<defs>
<linearGradient id="bg" x1="0%" x2="100%" y1="0%" y2="100%">
<stop offset="0%" stop-color="#101112" />
<stop offset="55%" stop-color="#1c1e20" />
<stop offset="100%" stop-color="#2b2f36" />
</linearGradient>
<linearGradient id="beak" x1="0%" x2="100%" y1="0%" y2="100%">
<stop offset="0%" stop-color="#e43d3d" />
<stop offset="100%" stop-color="#a10b0b" />
</linearGradient>
<filter id="shadow" x="-20%" y="-20%" width="140%" height="140%">
<feDropShadow dx="0" dy="12" stdDeviation="12" flood-color="#000" flood-opacity="0.65" />
</filter>
</defs>
<rect width="640" height="640" rx="64" fill="url(#bg)" />
<g filter="url(#shadow)">
<path d="M365 120c-88 0-162 78-162 174 0 73 43 122 96 138-4 18-38 58-68 76 0 0 120-18 152-142 44-1 112-44 125-112 20-106-82-134-143-134z" fill="#f7f7f2"/>
<path d="M274 178c-12 8-18 18-18 30 0 20 22 36 54 36 38 0 94-24 142-78-40 8-78-2-106-10-34-10-58-4-72 22z" fill="#050505"/>
<path d="M360 210c-18 0-32 10-32 24s14 24 32 24 32-10 32-24-14-24-32-24z" fill="#f7f7f2"/>
<path d="M418 204c48-22 90-22 122 0-6-54-58-88-128-88-42 0-92 10-142 32 42 26 82 34 148 56z" fill="#050505"/>
<path d="M468 232c-16 14-20 34-10 46 12 16 38 14 58-4 22-20 22-46 2-64-18-16-44-18-64-4-10 8-10 16-4 26 10-8 22-8 34 0 14 10 12 24-4 36-12 10-26 12-36 2 0-6 2-12 10-18z" fill="#050505"/>
<path d="M270 374c-26 62-70 104-120 126 72 10 142-12 186-66 32-40 46-94 46-124-36 26-78 44-112 64z" fill="#050505"/>
<path d="M322 330l-42 60 62-28c28-14 50-30 66-54-34 10-64 16-86 22z" fill="#050505"/>
<path d="M256 426 320 512l60-86z" fill="#050505"/>
<path d="M320 500l-80 32 20 32 88-12z" fill="#050505" opacity="0.8"/>
<path d="M320 328 208 448l116-56 100 50z" fill="#050505" opacity="0.85"/>
<path d="M326 360c-30 0-56 24-56 52s26 52 56 52 56-24 56-52-26-52-56-52z" fill="#050505" opacity="0.9"/>
<path d="M346 370c-18 0-32 12-32 28s14 28 32 28 32-12 32-28-14-28-32-28z" fill="#f5f5f5"/>
<path d="M320 260c-58 58-88 118-88 178l62-32 26-76 24 76 68 30c0-96-26-160-92-176z" fill="#d90429"/>
<path d="M320 272c-42 44-62 90-62 138l44-22 18-50 16 50 46 20c0-68-18-114-62-136z" fill="#fdf7f7"/>
<path d="M448 132c-10 0-20 4-28 12 28 0 52 10 70 24 10 8 18 18 24 28 4-4 6-10 6-18 0-26-32-46-72-46z" fill="#050505"/>
<path d="M480 188c-6 0-12 2-18 6 10 8 18 18 22 32 8-2 12-6 12-14 0-12-8-24-16-24z" fill="#050505"/>
<path d="M430 220c-10 0-18 6-18 14 0 8 8 14 18 14s18-6 18-14c0-8-8-14-18-14z" fill="#fefefe"/>
<path d="M512 248c-6 0-10 2-14 6 6 4 10 12 10 20 8-2 12-6 12-12 0-8-4-14-8-14z" fill="#fefefe"/>
<path d="M392 190c-10 0-18 4-18 10s8 10 18 10 18-4 18-10-8-10-18-10z" fill="#fefefe"/>
<path d="M360 144c-10 0-18 6-18 12s8 12 18 12 18-6 18-12-8-12-18-12z" fill="#fefefe"/>
<path d="M428 144c-6 0-12 2-16 6 12 4 22 10 30 18 2-2 2-4 2-6 0-10-8-18-16-18z" fill="#be1e2d"/>
<path d="M448 176c-6 0-12 2-16 6 10 6 18 14 22 22 4-2 6-6 6-10 0-10-6-18-12-18z" fill="url(#beak)"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

426
web/templates/index.html Normal file
View File

@@ -0,0 +1,426 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>GooseStrike Command Deck</title>
<link rel="stylesheet" href="/static/styles.css" />
</head>
<body>
<header>
<div class="canadian-flag" role="img" aria-label="Canadian flag motif"></div>
<div>
<h1>GooseStrike</h1>
<p>Canadian-themed, AI-assisted offensive security toolbox for authorized testing.</p>
</div>
<div class="goose-logo" role="img" aria-label="Uploaded GooseStrike logo">
<img src="{{ logo_url }}" alt="Uploaded GooseStrike logo" />
</div>
</header>
<main>
<section class="wide hero-panel" aria-label="GooseStrike Core overview">
<div class="hero-text">
<h2>GooseStrike Core (Docker-ready)</h2>
<ul class="hero-list">
<li>🔧 Nmap, Metasploit, SQLMap, Hydra, ZAP</li>
<li>🧠 AI exploit assistant (Claude, HackGPT-ready)</li>
<li>📚 Offline CVE mirroring with <code>update_cve.sh</code></li>
<li>🗂 ASCII banner, logo, branding kit (PDF)</li>
<li>📜 CVE scan + JSON match script</li>
<li>📦 <strong>goosestrike-cve-enabled.zip</strong> (download link)</li>
<li>🧠 <strong>hackgpt-ai-stack.zip</strong> with README + architecture</li>
</ul>
<div class="hero-meta">
<h3>Coming next (roadmap you requested)</h3>
<table>
<thead>
<tr>
<th>Task</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<tr>
<td>🐳 Build <code>docker-compose.goosestrike-full.yml</code></td>
<td>⏳ In progress</td>
</tr>
<tr>
<td>🧠 HackGPT API container (linked to n8n)</td>
<td>⏳ Next up</td>
</tr>
<tr>
<td>🌐 Local CVE API server</td>
<td>Pending</td>
</tr>
<tr>
<td>🧬 Claude + HackGPT fallback system</td>
<td>Pending</td>
</tr>
<tr>
<td>🔄 n8n workflow <code>.json</code> import</td>
<td>Pending</td>
</tr>
<tr>
<td>🎯 Target "prioritizer" AI agent</td>
<td>Pending</td>
</tr>
<tr>
<td>🧭 SVG architecture diagram</td>
<td>Pending</td>
</tr>
<tr>
<td>🖥 Dashboard frontend (Armitage-style)</td>
<td>Optional</td>
</tr>
<tr>
<td>🔐 C2 bridging to Mythic/Sliver</td>
<td>Optional</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="hero-visual" role="img" aria-label="GooseStrike crest render">
<div class="hero-visual-inner">
<img src="{{ spotlight_logo_url }}" alt="GooseStrike crest" />
</div>
</div>
</section>
<section>
<h2>Assets &amp; Vulnerabilities</h2>
<div id="assets"></div>
</section>
<section>
<h2>Scan History</h2>
<div id="scans"></div>
</section>
<section>
<h2>MITRE ATT&amp;CK Suggestions</h2>
<div id="mitre"></div>
</section>
<section>
<h2>Task Queue</h2>
<div id="tasks"></div>
</section>
<section>
<h2>Alerts</h2>
<pre id="alerts">Waiting for n8n webhooks...</pre>
</section>
<section class="wide">
<h2>Queue Tool Task</h2>
<form id="task-form" class="card-form">
<div class="form-grid">
<label>
Tool
<input type="text" id="task-tool" list="tool-presets" placeholder="sqlmap" required />
<datalist id="tool-presets">
<option value="metasploit"></option>
<option value="sqlmap"></option>
<option value="hydra"></option>
<option value="zap"></option>
<option value="password_cracker"></option>
<option value="hashcat"></option>
<option value="john"></option>
<option value="rainbow"></option>
</datalist>
</label>
<label>
Target / Notes
<input type="text" id="task-target" placeholder="10.0.0.5:80" />
</label>
</div>
<label>
Params (JSON)
<textarea id="task-params" rows="4" placeholder='{"level": 3, "risk": 1}'></textarea>
</label>
<div class="form-actions">
<button type="submit">Queue Task</button>
<span class="helper">Tasks land in db/tasks.db and runners pick them up.</span>
</div>
<div id="task-message" class="helper"></div>
</form>
</section>
<section class="wide">
<h2>Password Cracking Helper</h2>
<form id="password-form" class="card-form">
<div class="form-grid">
<label>
Cracking Tool
<select id="password-tool">
<option value="hashcat">Hashcat</option>
<option value="john">John the Ripper</option>
<option value="rainbow">Rainbow (rcrack)</option>
</select>
</label>
<label>
Target Label
<input type="text" id="password-target" placeholder="lab-hashes" />
</label>
</div>
<label>
Hash / Tables file
<input type="text" id="password-file" placeholder="/data/hashes.txt" required />
</label>
<label>
Wordlist / Mask / Hash value
<input type="text" id="password-wordlist" placeholder="/usr/share/wordlists/rockyou.txt or ?l?d?d?d" />
</label>
<label>
Extra options (JSON)
<textarea id="password-extra" rows="3" placeholder='{"mode": 0, "attack_mode": 0}'></textarea>
</label>
<div class="form-actions">
<button type="submit">Queue Password Crack</button>
<span class="helper">Hashcat/John/Rainbow jobs reuse the password_cracker runner.</span>
</div>
<div id="password-message" class="helper"></div>
</form>
</section>
</main>
<footer>
Built for red, grey, black, and white team operations. Use responsibly.
</footer>
<script>
async function fetchAssets() {
const response = await fetch('/assets');
const container = document.getElementById('assets');
if (!response.ok) {
container.innerText = 'Unable to load assets yet.';
return;
}
const assets = await response.json();
if (!assets.length) {
container.innerText = 'No assets ingested yet.';
return;
}
container.innerHTML = '';
assets.forEach(asset => {
const card = document.createElement('article');
const macLine = asset.mac_address ? `<div class="meta">MAC: ${asset.mac_address} ${asset.mac_vendor ? '(' + asset.mac_vendor + ')' : ''}</div>` : '';
card.innerHTML = `
<h3>${asset.ip} ${asset.hostname ? '(' + asset.hostname + ')' : ''}</h3>
${macLine}
<ul>
${asset.services.map(service => `
<li>
<div class="service-line">
<strong>${service.proto}/${service.port}</strong>
<span>${service.product || 'unknown'} ${service.version || ''}</span>
</div>
${service.vulnerabilities && service.vulnerabilities.length ? `
<div class="vuln-list">
${service.vulnerabilities.map(vuln => `
<span class="badge severity-${(vuln.severity || 'unknown').toLowerCase()}">${vuln.cve_id}${vuln.severity ? ' · ' + vuln.severity : ''}</span>
`).join(' ')}
</div>
` : '<span class="badge severity-info">No CVEs linked</span>'}
</li>`).join('')}
</ul>
`;
container.appendChild(card);
});
}
async function fetchScans() {
const response = await fetch('/scans');
const container = document.getElementById('scans');
if (!response.ok) {
container.innerText = 'Unable to load scan history.';
return;
}
const scans = await response.json();
if (!scans.length) {
container.innerText = 'No scans stored yet.';
return;
}
container.innerHTML = '';
scans.forEach(scan => {
const card = document.createElement('article');
card.classList.add('scan-card');
card.innerHTML = `
<h3>${scan.asset_ip}</h3>
<div class="meta">Scan ${scan.scan_id || scan.id} via ${scan.scanner || 'unknown'} (${scan.mode || 'standard'})</div>
<div class="meta">${scan.started_at || 'unknown start'}${scan.completed_at || 'unknown finish'}</div>
<div class="meta">Services: ${scan.services.length}</div>
`;
container.appendChild(card);
});
}
async function fetchMitre() {
const response = await fetch('/attack_suggestions');
const container = document.getElementById('mitre');
if (!response.ok) {
container.innerText = 'Unable to load ATT&CK guidance.';
return;
}
const suggestions = await response.json();
if (!suggestions.length) {
container.innerText = 'Suggestions populate after a scan.';
return;
}
container.innerHTML = '';
suggestions.forEach(suggestion => {
const card = document.createElement('article');
card.innerHTML = `
<h3>${suggestion.technique_id} · ${suggestion.name}</h3>
<p>${suggestion.description}</p>
<div class="meta">${suggestion.tactic} | Severity: ${suggestion.severity || 'info'}</div>
${suggestion.related_cve ? `<div class="badge severity-critical">Linked CVE ${suggestion.related_cve}</div>` : ''}
`;
container.appendChild(card);
});
}
function refreshAll() {
fetchAssets();
fetchScans();
fetchMitre();
fetchTasks();
}
refreshAll();
setInterval(refreshAll, 15000);
async function fetchTasks() {
const response = await fetch('/tasks');
const container = document.getElementById('tasks');
if (!response.ok) {
container.innerText = 'Unable to load tasks.';
return;
}
const tasks = await response.json();
if (!tasks.length) {
container.innerText = 'Queue a task to populate this view.';
return;
}
container.innerHTML = '';
tasks.forEach(task => {
const card = document.createElement('article');
card.classList.add('task-card');
const params = JSON.stringify(task.params || {}, null, 2);
card.innerHTML = `
<div class="task-line">
<strong>${task.tool}</strong>
<span class="badge status-pill status-${task.status.toLowerCase()}">${task.status}</span>
</div>
<div class="meta">Target: ${task.target || 'n/a'}</div>
<div class="meta">Created: ${task.created_at}</div>
${task.started_at ? `<div class="meta">Started: ${task.started_at}</div>` : ''}
${task.finished_at ? `<div class="meta">Finished: ${task.finished_at}</div>` : ''}
<details>
<summary>Parameters</summary>
<pre>${params}</pre>
</details>
${task.result ? `<details><summary>Result</summary><pre>${JSON.stringify(task.result, null, 2)}</pre></details>` : ''}
`;
container.appendChild(card);
});
}
async function queueTask(payload, messageEl) {
try {
const response = await fetch('/tasks', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
if (!response.ok) {
const text = await response.text();
throw new Error(text || 'Unable to queue task');
}
const data = await response.json();
messageEl.textContent = `Queued task #${data.id}`;
messageEl.classList.remove('error');
fetchTasks();
} catch (err) {
messageEl.textContent = err.message;
messageEl.classList.add('error');
}
}
const taskForm = document.getElementById('task-form');
if (taskForm) {
taskForm.addEventListener('submit', async (event) => {
event.preventDefault();
const toolInput = document.getElementById('task-tool');
const targetInput = document.getElementById('task-target');
const paramsInput = document.getElementById('task-params');
const messageEl = document.getElementById('task-message');
let params = {};
if (paramsInput.value.trim()) {
try {
params = JSON.parse(paramsInput.value);
} catch (err) {
messageEl.textContent = 'Params must be valid JSON.';
messageEl.classList.add('error');
return;
}
}
await queueTask({
tool: toolInput.value.trim(),
target: targetInput.value.trim() || null,
params
}, messageEl);
taskForm.reset();
});
}
const passwordForm = document.getElementById('password-form');
if (passwordForm) {
passwordForm.addEventListener('submit', async (event) => {
event.preventDefault();
const messageEl = document.getElementById('password-message');
const tool = document.getElementById('password-tool').value;
const target = document.getElementById('password-target').value;
const hashFile = document.getElementById('password-file').value;
const wordlist = document.getElementById('password-wordlist').value;
const extraField = document.getElementById('password-extra');
let extra = {};
if (extraField.value.trim()) {
try {
extra = JSON.parse(extraField.value);
} catch (err) {
messageEl.textContent = 'Extra options must be valid JSON.';
messageEl.classList.add('error');
return;
}
}
const params = {
crack_tool: tool,
...extra
};
if (tool === 'rainbow') {
params.tables_path = hashFile;
if (wordlist) {
if (wordlist.includes('/') || wordlist.endsWith('.txt')) {
params.hash_file = wordlist;
} else {
params.hash_value = wordlist;
}
}
} else {
params.hash_file = hashFile;
if (wordlist) {
if (tool === 'hashcat' && wordlist.includes('?') && !wordlist.includes('/')) {
params.mask = wordlist;
} else {
params.wordlist = wordlist;
}
}
}
await queueTask({
tool: 'password_cracker',
target: target || hashFile,
params
}, messageEl);
passwordForm.reset();
});
}
</script>
</body>
</html>

View File

@@ -0,0 +1,161 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>GooseStrike Mock Dashboard</title>
<link rel="stylesheet" href="/static/styles.css" />
</head>
<body>
<header>
<div class="canadian-flag" role="img" aria-label="Canadian flag motif"></div>
<div>
<h1>GooseStrike Mock Dashboard</h1>
<p>Pre-filled sample data so you can preview the UI without running scans.</p>
</div>
<div class="goose-logo" role="img" aria-label="Uploaded GooseStrike logo">
<img src="{{ logo_url }}" alt="Uploaded GooseStrike logo" />
</div>
</header>
<main>
<section class="wide hero-panel" aria-label="Mock GooseStrike Core overview">
<div class="hero-text">
<h2>{{ mock.core_snapshot.title }}</h2>
<ul class="hero-list">
{% for bullet in mock.core_snapshot.bullets %}
<li>{{ bullet }}</li>
{% endfor %}
</ul>
<h3>Artifact drops</h3>
<ul class="hero-list">
{% for download in mock.core_snapshot.downloads %}
<li>{{ download }}</li>
{% endfor %}
</ul>
<div class="hero-meta">
<h3>Coming next (roadmap you requested)</h3>
<table>
<thead>
<tr>
<th>Task</th>
<th>Status</th>
</tr>
</thead>
<tbody>
{% for item in mock.roadmap %}
<tr>
<td>{{ item.task | safe }}</td>
<td>{{ item.status }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<div class="hero-visual" role="img" aria-label="GooseStrike crest mockup">
<div class="hero-visual-inner">
<img src="{{ spotlight_logo_url }}" alt="GooseStrike crest" />
</div>
</div>
</section>
<section>
<h2>Assets (Sample)</h2>
<div class="scan-list">
{% for asset in mock.assets %}
<article class="scan-card">
<h3>{{ asset.ip }}{% if asset.hostname %} · {{ asset.hostname }}{% endif %}</h3>
<p class="meta">MAC {{ asset.mac_address }} · {{ asset.mac_vendor }}</p>
<ul>
{% for service in asset.services %}
<li>
<strong>{{ service.port }}/{{ service.proto }}</strong>
<span>{{ service.product }} {{ service.version }}</span>
<div>
{% for vuln in service.vulnerabilities %}
<span class="severity-pill severity-{{ vuln.severity | lower if vuln.severity else 'unknown' }}">
{{ vuln.cve_id }} ({{ vuln.severity or 'Unknown' }})
</span>
{% endfor %}
</div>
</li>
{% endfor %}
</ul>
</article>
{% endfor %}
</div>
</section>
<section>
<h2>Scan History (Sample)</h2>
<div class="scan-list">
{% for scan in mock.scans %}
<article class="scan-card">
<h3>{{ scan.asset_ip }} · {{ scan.mode }} ({{ scan.scan_id }})</h3>
<p class="meta">{{ scan.started_at }} → {{ scan.completed_at }} · {{ scan.notes }}</p>
<ul>
{% for svc in scan.services %}
<li>
<strong>{{ svc.port }}/{{ svc.proto }}</strong> — {{ svc.product }} {{ svc.version }}
<div>
{% for cve in svc.cves %}
<span class="severity-pill severity-info">{{ cve }}</span>
{% endfor %}
</div>
</li>
{% endfor %}
</ul>
</article>
{% endfor %}
</div>
</section>
<section>
<h2>MITRE ATT&CK Suggestions (Sample)</h2>
<div id="mitre">
{% for suggestion in mock.attack_suggestions %}
<article class="scan-card">
<h3>{{ suggestion.technique_id }} · {{ suggestion.name }}</h3>
<p class="meta">{{ suggestion.tactic }} · Related to {{ suggestion.related_cve }}</p>
<p>{{ suggestion.description }}</p>
</article>
{% endfor %}
</div>
</section>
<section>
<h2>Task Queue (Sample)</h2>
<div id="tasks">
{% for task in mock.tasks %}
<article class="task-card">
<div class="task-line">
<strong>{{ task.tool }}</strong>
<span class="severity-pill status-{{ task.status }}">{{ task.status }}</span>
</div>
<p>{{ task.target }}</p>
<pre>{{ task.params | tojson(indent=2) }}</pre>
</article>
{% endfor %}
</div>
</section>
<section>
<h2>Alerts (Sample)</h2>
<div id="alerts">
{% for alert in mock.alerts %}
<article class="scan-card">
<h3>{{ alert.source }}</h3>
<p class="meta">{{ alert.created_at }}</p>
<pre>{{ alert.payload | tojson(indent=2) }}</pre>
</article>
{% endfor %}
</div>
</section>
</main>
<footer>
Mock data only — run the real dashboard at <code>/</code> once you have live scans.
</footer>
</body>
</html>

34
zap_runner.py Normal file
View File

@@ -0,0 +1,34 @@
"""Wrapper around OWASP ZAP baseline scan."""
from __future__ import annotations
from typing import Any, Dict, List
from runner_utils import run_subprocess
def build_command(task: Dict[str, Any]) -> List[str]:
target = task.get("target_url")
if not target:
raise ValueError("target_url is required")
command = ["zap-baseline.py", "-t", target, "-m", "5", "-r", task.get("report", "zap_report.html")]
for key, value in task.get("options", {}).items():
flag = f"-{key}" if len(key) == 1 else f"--{key}"
if isinstance(value, bool):
if value:
command.append(flag)
else:
command.extend([flag, str(value)])
return command
def run_task(task: Dict[str, Any]) -> Dict[str, Any]:
try:
command = build_command(task)
except ValueError as exc:
return {"status": "error", "exit_code": None, "error": str(exc)}
return run_subprocess(command, "zap")
if __name__ == "__main__":
example = {"target_url": "http://example.com", "report": "example.html"}
print(run_task(example))