mirror of
https://github.com/mblanke/GooseStrike.git
synced 2026-03-01 14:00:21 -05:00
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:
30
Dockerfile.kali
Normal file
30
Dockerfile.kali
Normal 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
196
README.md
@@ -1,2 +1,196 @@
|
|||||||
# GooseStrike
|
# 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.
|
||||||
|
|||||||
0
app/__init__.py
Normal file
0
app/__init__.py
Normal file
32
app/agents/base_agent.py
Normal file
32
app/agents/base_agent.py
Normal 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
28
app/agents/cve_agent.py
Normal 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)
|
||||||
28
app/agents/exploit_agent.py
Normal file
28
app/agents/exploit_agent.py
Normal 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
31
app/agents/plan_agent.py
Normal 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)
|
||||||
29
app/agents/privesc_agent.py
Normal file
29
app/agents/privesc_agent.py
Normal 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
31
app/agents/recon_agent.py
Normal 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
42
docker-compose.kali.yml
Normal 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
40
hydra_runner.py
Normal 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
172
indexer.py
Normal 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
37
metasploit_runner.py
Normal 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
109
mitre_mapping.py
Normal 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
119
password_cracker_runner.py
Normal 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
6
requirements.txt
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
fastapi
|
||||||
|
uvicorn
|
||||||
|
requests
|
||||||
|
pydantic
|
||||||
|
pytest
|
||||||
|
jinja2
|
||||||
47
runner_utils.py
Normal file
47
runner_utils.py
Normal 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
269
scanner.py
Normal 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
34
sqlmap_runner.py
Normal 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
134
task_queue.py
Normal 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())
|
||||||
171
tests/test_scanner_and_api.py
Normal file
171
tests/test_scanner_and_api.py
Normal 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"
|
||||||
16
web/static/goose_flag_logo.svg
Normal file
16
web/static/goose_flag_logo.svg
Normal 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
368
web/static/styles.css
Normal 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);
|
||||||
|
}
|
||||||
0
web/static/uploads/.gitkeep
Normal file
0
web/static/uploads/.gitkeep
Normal file
42
web/static/uploads/official_goosestrike_logo.svg
Normal file
42
web/static/uploads/official_goosestrike_logo.svg
Normal 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
426
web/templates/index.html
Normal 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 & Vulnerabilities</h2>
|
||||||
|
<div id="assets"></div>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<h2>Scan History</h2>
|
||||||
|
<div id="scans"></div>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<h2>MITRE ATT&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>
|
||||||
161
web/templates/mock_dashboard.html
Normal file
161
web/templates/mock_dashboard.html
Normal 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
34
zap_runner.py
Normal 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))
|
||||||
Reference in New Issue
Block a user