mirror of
https://github.com/mblanke/GooseStrike.git
synced 2026-03-01 05:50:22 -05:00
Add integration test endpoints for n8n and Ollama
This commit is contained in:
17
Dockerfile
Normal file
17
Dockerfile
Normal file
@@ -0,0 +1,17 @@
|
||||
FROM python:3.11-slim
|
||||
|
||||
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||
PYTHONUNBUFFERED=1
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends nmap masscan sqlmap hydra \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY requirements.txt ./
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY . .
|
||||
|
||||
CMD ["uvicorn", "api:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||
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"]
|
||||
291
README.md
291
README.md
@@ -1,2 +1,291 @@
|
||||
# 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.
|
||||
- **Claude + HackGPT + Ollama fallback** – the LLM router cycles through Claude, HackGPT, then your local Ollama node so targeting recommendations keep flowing even offline.
|
||||
- **Local CVE API server** – a standalone FastAPI microservice (`cve_api.py`) serves CVE + exploit matches for air-gapped use.
|
||||
- **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`).
|
||||
- **Armitage-style frontend** – `/armitage` renders a radial host/service/CVE graph with mock or live data for briefing-style screenshots.
|
||||
- **Integration health checks** – `/integrations/test/n8n` and `/integrations/test/ollama` verify automation + AI endpoints from the backend without leaving the UI.
|
||||
|
||||
## Delivery status
|
||||
|
||||
The table below summarizes what has landed versus what remains from the running request list.
|
||||
|
||||
| Area | Status | Notes |
|
||||
| --- | --- | --- |
|
||||
| Core scanner + history | ✅ Complete | CIDR scanning with MAC/OUI capture, scan UUIDs, and `/scans` history APIs are live.
|
||||
| CVE / exploit indexer | ✅ Complete | `indexer.py` hydrates `db/exploits.db` from NVD, Exploit-DB, and PacketStorm mirrors.
|
||||
| FastAPI backend + web UI | ✅ Complete | Canadian-themed dashboard, `/assets`, `/roadmap`, `/mock/dashboard-data`, `/mockup`, webhook, and task endpoints shipped.
|
||||
| Task queue + tool runners | ✅ Complete | SQLite-backed queue plus Metasploit, SQLMap, Hydra, ZAP, and password-cracking runners.
|
||||
| MITRE/ATT&CK suggestions | ✅ Complete | `mitre_mapping.py` drives `/attack_suggestions` and the UI cards.
|
||||
| Password cracking helper | ✅ Complete | `password_cracker_runner.py` supports Hashcat, John, and rainbow tables with log capture.
|
||||
| Mock dashboard data | ✅ Complete | `GET /mock/dashboard-data` and `/mockup` render the full interface with sample data for demos.
|
||||
| Roadmap + core snapshot APIs | ✅ Complete | `/core_snapshot` + `/roadmap` expose the same copy used in the UI.
|
||||
| docker-compose goosestrike full stack | ✅ Complete | `docker-compose.goosestrike-full.yml` now launches API, scanner, indexer, CVE API, HackGPT relay, and n8n together.
|
||||
| HackGPT API container | ✅ Complete | `hackgpt_api.py` relays prompts to n8n webhooks and ships as its own container.
|
||||
| Local CVE API server | ✅ Complete | `cve_api.py` exposes the offline CVE mirror over FastAPI/uvicorn.
|
||||
| Claude + HackGPT fallback system | ✅ Complete | `app/agents/llm_router.py` automatically fails over between providers.
|
||||
| n8n workflow `.json` import | ✅ Complete | `/n8n/workflows/import` persists exports and `/n8n/workflows` lists them.
|
||||
| Target prioritizer AI agent | ✅ Complete | `app/agents/prioritizer_agent.py` powers the `/agents/prioritize` endpoint.
|
||||
| SVG architecture diagram | ✅ Complete | `web/static/architecture.svg` is linked below.
|
||||
| Armitage-style dashboard view | ✅ Complete | `/armitage` renders the radial host/service/CVE graph with live data.
|
||||
| Mythic/Sliver C2 bridge | 🔸 Optional | C2 integration untouched pending scoping.
|
||||
|
||||
## 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` | ✅ Complete |
|
||||
| 🧠 HackGPT API container (linked to n8n) | ✅ Complete |
|
||||
| 🌐 Local CVE API server | ✅ Complete |
|
||||
| 🧬 Claude + HackGPT fallback system | ✅ Complete |
|
||||
| 🔄 n8n workflow `.json` import | ✅ Complete |
|
||||
| 🎯 Target "prioritizer" AI agent | ✅ Complete |
|
||||
| 🧭 SVG architecture diagram | ✅ Complete |
|
||||
| 🖥 Dashboard frontend (Armitage-style) | ✅ Complete |
|
||||
| 🔐 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 .
|
||||
|
||||
- **Import + list n8n workflows**
|
||||
```bash
|
||||
curl -X POST http://localhost:8000/n8n/workflows/import \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"name": "drop-scan", "workflow": {"nodes": ["scan", "notify"]}}'
|
||||
curl http://localhost:8000/n8n/workflows
|
||||
```
|
||||
|
||||
- **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}'
|
||||
```
|
||||
|
||||
- **Armitage data + prioritizer agent**
|
||||
```bash
|
||||
curl http://localhost:8000/armitage/data
|
||||
curl -X POST http://localhost:8000/agents/prioritize \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"assets": [{"ip": "10.0.0.21", "services": []}]}'
|
||||
```
|
||||
|
||||
## Full-stack Docker Compose
|
||||
|
||||
Bring everything (API, scanner, indexer, HackGPT relay, CVE API, and n8n) up in one go:
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.goosestrike-full.yml build
|
||||
docker compose -f docker-compose.goosestrike-full.yml up -d api hackgpt cve-api n8n
|
||||
# open http://localhost:8000 for the UI and http://localhost:8500/prompt for the HackGPT relay
|
||||
```
|
||||
|
||||
Use `docker compose ... run --rm scanner python scanner.py ...` or `... run --rm indexer python indexer.py` to execute jobs from dedicated containers.
|
||||
|
||||
## HackGPT relay + Claude / HackGPT / Ollama fallback
|
||||
|
||||
- `hackgpt_api.py` exposes `POST /prompt` (default on port 8500) and forwards every call to `N8N_WEBHOOK_URL` so your automation workflows can react instantly.
|
||||
- Set any combination of `CLAUDE_API_URL`, `HACKGPT_API_URL`, and `OLLAMA_BASE_URL`/`OLLAMA_API_URL` (with optional `CLAUDE_API_KEY`, `HACKGPT_API_KEY`, and `OLLAMA_MODEL`). The router in `app/agents/llm_router.py` will try Claude first, then HackGPT, and finally fall back to the local Ollama instance.
|
||||
- The new `/integrations/test/ollama` route helps you confirm GooseStrike can reach your Ollama node before relying on it for agent prompts.
|
||||
|
||||
## Local CVE API server
|
||||
|
||||
Run `uvicorn cve_api:app --host 0.0.0.0 --port 8600` (or use the compose file) to offer offline CVE lookups:
|
||||
|
||||
```bash
|
||||
curl http://localhost:8600/cve/CVE-2023-12345
|
||||
curl "http://localhost:8600/search?q=openssl"
|
||||
```
|
||||
|
||||
The service reads the same `db/exploits.db` populated by `indexer.py`.
|
||||
|
||||
## Armitage-style dashboard + mock graph
|
||||
|
||||
- Browse `http://localhost:8000/armitage` for the live radial graph.
|
||||
- `GET /armitage/data` returns the same JSON used by the view, while `GET /mock/armitage-data` exposes the sample payload for demos.
|
||||
|
||||
## Target prioritizer agent
|
||||
|
||||
Send any subset of `AssetOut` structures to `POST /agents/prioritize` to receive ranked notes backed by the LLM fallback path. Use it in tandem with the MITRE cards to plan next actions.
|
||||
|
||||
## 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.
|
||||
- **Test n8n & Ollama integrations**
|
||||
```bash
|
||||
curl -X POST http://localhost:8000/integrations/test/n8n \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"url": "http://n8n.local/webhook/goose", "payload": {"ping": "demo"}}'
|
||||
|
||||
curl -X POST http://localhost:8000/integrations/test/ollama \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"url": "http://localhost:11434", "model": "llama3", "prompt": "ping"}'
|
||||
```
|
||||
Both endpoints accept optional overrides and default to the `N8N_WEBHOOK_TEST_URL`, `OLLAMA_BASE_URL`, and `OLLAMA_MODEL` environment variables so you can store safe defaults in compose files.
|
||||
|
||||
0
app/__init__.py
Normal file
0
app/__init__.py
Normal file
38
app/agents/base_agent.py
Normal file
38
app/agents/base_agent.py
Normal file
@@ -0,0 +1,38 @@
|
||||
"""Base LLM agent scaffolding for GooseStrike."""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Dict
|
||||
|
||||
from .llm_router import LLMProviderError, call_llm_with_fallback
|
||||
|
||||
|
||||
def llm_call(prompt: str) -> str:
|
||||
"""Call the configured LLM providers with fallback behavior."""
|
||||
|
||||
try:
|
||||
return call_llm_with_fallback(prompt)
|
||||
except LLMProviderError as exc:
|
||||
return f"LLM providers unavailable: {exc}"
|
||||
|
||||
|
||||
@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)
|
||||
78
app/agents/llm_router.py
Normal file
78
app/agents/llm_router.py
Normal file
@@ -0,0 +1,78 @@
|
||||
"""LLM routing helpers with Claude -> HackGPT fallback."""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from typing import Dict, List, Tuple
|
||||
|
||||
import requests
|
||||
|
||||
|
||||
class LLMProviderError(RuntimeError):
|
||||
"""Raised when a downstream LLM provider fails."""
|
||||
|
||||
|
||||
def _call_provider(name: str, url: str, prompt: str) -> str:
|
||||
payload = {"prompt": prompt}
|
||||
api_key = os.getenv(f"{name.upper()}_API_KEY")
|
||||
headers = {"Content-Type": "application/json"}
|
||||
if api_key:
|
||||
headers["Authorization"] = f"Bearer {api_key}"
|
||||
response = requests.post(url, json=payload, headers=headers, timeout=30)
|
||||
response.raise_for_status()
|
||||
data: Dict[str, str] = response.json() if response.headers.get("content-type", "").startswith("application/json") else {}
|
||||
return data.get("response") or data.get("answer") or data.get("text") or response.text
|
||||
|
||||
|
||||
def normalize_ollama_url(base_url: str) -> str:
|
||||
"""Return a usable Ollama generate endpoint for the supplied base URL."""
|
||||
|
||||
base_url = base_url.rstrip("/")
|
||||
if "/api" in base_url:
|
||||
if base_url.endswith("/generate"):
|
||||
return base_url
|
||||
return f"{base_url}/generate"
|
||||
return f"{base_url}/api/generate"
|
||||
|
||||
|
||||
def _call_ollama(base_url: str, prompt: str) -> str:
|
||||
"""Invoke a local Ollama instance using the configured model."""
|
||||
|
||||
url = normalize_ollama_url(base_url)
|
||||
model = os.getenv("OLLAMA_MODEL", "llama3")
|
||||
payload = {"model": model, "prompt": prompt, "stream": False}
|
||||
response = requests.post(url, json=payload, timeout=30)
|
||||
response.raise_for_status()
|
||||
data: Dict[str, str] = (
|
||||
response.json() if response.headers.get("content-type", "").startswith("application/json") else {}
|
||||
)
|
||||
return data.get("response") or data.get("output") or response.text
|
||||
|
||||
|
||||
def call_llm_with_fallback(prompt: str) -> str:
|
||||
"""Try Claude first, then HackGPT, finally return a placeholder."""
|
||||
|
||||
order: List[Tuple[str, str]] = []
|
||||
claude_url = os.getenv("CLAUDE_API_URL")
|
||||
hackgpt_url = os.getenv("HACKGPT_API_URL")
|
||||
ollama_base = os.getenv("OLLAMA_API_URL") or os.getenv("OLLAMA_BASE_URL")
|
||||
if claude_url:
|
||||
order.append(("claude", claude_url))
|
||||
if hackgpt_url:
|
||||
order.append(("hackgpt", hackgpt_url))
|
||||
if ollama_base:
|
||||
order.append(("ollama", ollama_base))
|
||||
|
||||
errors: List[str] = []
|
||||
for name, url in order:
|
||||
try:
|
||||
if name == "ollama":
|
||||
return _call_ollama(url, prompt)
|
||||
return _call_provider(name, url, prompt)
|
||||
except Exception as exc: # pragma: no cover - network dependent
|
||||
errors.append(f"{name} failed: {exc}")
|
||||
continue
|
||||
|
||||
if errors:
|
||||
raise LLMProviderError("; ".join(errors))
|
||||
|
||||
return "LLM response placeholder. Configure CLAUDE_API_URL or HACKGPT_API_URL to enable live replies."
|
||||
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)
|
||||
35
app/agents/prioritizer_agent.py
Normal file
35
app/agents/prioritizer_agent.py
Normal file
@@ -0,0 +1,35 @@
|
||||
"""Target prioritizer AI agent."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from .base_agent import AgentResult, BaseAgent
|
||||
|
||||
|
||||
class PrioritizerAgent(BaseAgent):
|
||||
name = "prioritizer"
|
||||
|
||||
def build_prompt(self, context: Dict[str, Any]) -> str:
|
||||
hosts: List[Dict[str, Any]] = context.get("assets", [])
|
||||
findings = []
|
||||
for asset in hosts:
|
||||
ip = asset.get("ip")
|
||||
severity = max(
|
||||
(vuln.get("severity", "") for svc in asset.get("services", []) for vuln in svc.get("vulnerabilities", [])),
|
||||
default="",
|
||||
)
|
||||
findings.append(f"Host {ip} exposes {len(asset.get('services', []))} services (max severity: {severity}).")
|
||||
prompt_lines = [
|
||||
"You are GooseStrike's targeting aide.",
|
||||
"Rank the following hosts for next actions using MITRE ATT&CK tactics.",
|
||||
]
|
||||
prompt_lines.extend(findings or ["No assets supplied; recommend intel-gathering tasks."])
|
||||
prompt_lines.append("Return JSON with priorities, rationale, and suggested tactic per host.")
|
||||
return "\n".join(prompt_lines)
|
||||
|
||||
def parse(self, raw: str) -> Dict[str, Any]:
|
||||
return {"priorities": raw.strip()}
|
||||
|
||||
|
||||
def prioritize_targets(context: Dict[str, Any]) -> AgentResult:
|
||||
return PrioritizerAgent().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)
|
||||
85
cve_api.py
Normal file
85
cve_api.py
Normal file
@@ -0,0 +1,85 @@
|
||||
"""Standalone FastAPI service exposing the CVE/exploit mirror."""
|
||||
from __future__ import annotations
|
||||
|
||||
import sqlite3
|
||||
from pathlib import Path
|
||||
from typing import List, Optional
|
||||
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from pydantic import BaseModel
|
||||
|
||||
DB_PATH = Path("db/exploits.db")
|
||||
DB_PATH.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
app = FastAPI(title="GooseStrike CVE API", version="0.1.0")
|
||||
|
||||
|
||||
class CVEEntry(BaseModel):
|
||||
cve_id: str
|
||||
description: Optional[str]
|
||||
severity: Optional[str]
|
||||
score: Optional[float]
|
||||
exploits: List[dict]
|
||||
|
||||
|
||||
class CVESearchResult(BaseModel):
|
||||
results: List[CVEEntry]
|
||||
|
||||
|
||||
def _get_conn() -> sqlite3.Connection:
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
conn.row_factory = sqlite3.Row
|
||||
return conn
|
||||
|
||||
|
||||
@app.get("/cve/{cve_id}", response_model=CVEEntry)
|
||||
async def fetch_cve(cve_id: str) -> CVEEntry:
|
||||
if not DB_PATH.exists():
|
||||
raise HTTPException(status_code=404, detail="CVE database missing")
|
||||
with _get_conn() as conn:
|
||||
row = conn.execute("SELECT * FROM cves WHERE cve_id=?", (cve_id,)).fetchone()
|
||||
if not row:
|
||||
raise HTTPException(status_code=404, detail="CVE not found")
|
||||
exploits = conn.execute(
|
||||
"SELECT source, title, reference, path FROM exploits WHERE cve_id=?", (cve_id,)
|
||||
).fetchall()
|
||||
return CVEEntry(
|
||||
cve_id=row["cve_id"],
|
||||
description=row["description"],
|
||||
severity=row["severity"],
|
||||
score=row["score"],
|
||||
exploits=[dict(exp) for exp in exploits],
|
||||
)
|
||||
|
||||
|
||||
@app.get("/search", response_model=CVESearchResult)
|
||||
async def search_cves(q: str) -> CVESearchResult:
|
||||
if not DB_PATH.exists():
|
||||
return CVESearchResult(results=[])
|
||||
like = f"%{q}%"
|
||||
with _get_conn() as conn:
|
||||
rows = conn.execute(
|
||||
"SELECT * FROM cves WHERE cve_id LIKE ? OR description LIKE ? LIMIT 25",
|
||||
(like, like),
|
||||
).fetchall()
|
||||
entries = []
|
||||
for row in rows:
|
||||
exploits = conn.execute(
|
||||
"SELECT source, title, reference, path FROM exploits WHERE cve_id=?",
|
||||
(row["cve_id"],),
|
||||
).fetchall()
|
||||
entries.append(
|
||||
CVEEntry(
|
||||
cve_id=row["cve_id"],
|
||||
description=row["description"],
|
||||
severity=row["severity"],
|
||||
score=row["score"],
|
||||
exploits=[dict(exp) for exp in exploits],
|
||||
)
|
||||
)
|
||||
return CVESearchResult(results=entries)
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
async def health() -> dict:
|
||||
return {"ok": True, "ready": DB_PATH.exists()}
|
||||
79
docker-compose.goosestrike-full.yml
Normal file
79
docker-compose.goosestrike-full.yml
Normal file
@@ -0,0 +1,79 @@
|
||||
version: "3.9"
|
||||
|
||||
services:
|
||||
api:
|
||||
build: .
|
||||
container_name: goosestrike-api
|
||||
command: ["uvicorn", "api:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||
volumes:
|
||||
- ./db:/app/db
|
||||
- ./logs:/app/logs
|
||||
- ./web/static/uploads:/app/web/static/uploads
|
||||
environment:
|
||||
- GOOSESTRIKE_LOGO=${GOOSESTRIKE_LOGO:-}
|
||||
- CLAUDE_API_URL=${CLAUDE_API_URL:-}
|
||||
- HACKGPT_API_URL=${HACKGPT_API_URL:-http://hackgpt:8500/prompt}
|
||||
- HACKGPT_API_KEY=${HACKGPT_API_KEY:-}
|
||||
- CLAUDE_API_KEY=${CLAUDE_API_KEY:-}
|
||||
ports:
|
||||
- "8000:8000"
|
||||
depends_on:
|
||||
- cve-api
|
||||
- hackgpt
|
||||
|
||||
scanner:
|
||||
build: .
|
||||
container_name: goosestrike-scanner
|
||||
command: ["sleep", "infinity"]
|
||||
volumes:
|
||||
- ./db:/app/db
|
||||
- ./logs:/app/logs
|
||||
depends_on:
|
||||
- api
|
||||
|
||||
indexer:
|
||||
build: .
|
||||
container_name: goosestrike-indexer
|
||||
command: ["sleep", "infinity"]
|
||||
volumes:
|
||||
- ./db:/app/db
|
||||
- ./data:/app/data
|
||||
|
||||
task-runner:
|
||||
build: .
|
||||
container_name: goosestrike-task-runner
|
||||
command: ["sleep", "infinity"]
|
||||
volumes:
|
||||
- ./db:/app/db
|
||||
- ./logs:/app/logs
|
||||
|
||||
hackgpt:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
container_name: goosestrike-hackgpt
|
||||
command: ["uvicorn", "hackgpt_api:app", "--host", "0.0.0.0", "--port", "8500"]
|
||||
environment:
|
||||
- N8N_WEBHOOK_URL=http://n8n:5678/webhook/hackgpt
|
||||
ports:
|
||||
- "8500:8500"
|
||||
|
||||
cve-api:
|
||||
build: .
|
||||
container_name: goosestrike-cve-api
|
||||
command: ["uvicorn", "cve_api:app", "--host", "0.0.0.0", "--port", "8600"]
|
||||
volumes:
|
||||
- ./db:/app/db
|
||||
ports:
|
||||
- "8600:8600"
|
||||
|
||||
n8n:
|
||||
image: n8nio/n8n:1.53.0
|
||||
container_name: goosestrike-n8n
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- GENERIC_TIMEZONE=UTC
|
||||
ports:
|
||||
- "5678:5678"
|
||||
volumes:
|
||||
- ./n8n-data:/home/node/.n8n
|
||||
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
|
||||
54
hackgpt_api.py
Normal file
54
hackgpt_api.py
Normal file
@@ -0,0 +1,54 @@
|
||||
"""Minimal HackGPT relay API wired for n8n webhooks."""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import uuid
|
||||
from typing import Optional
|
||||
|
||||
import requests
|
||||
from fastapi import FastAPI
|
||||
from pydantic import BaseModel
|
||||
|
||||
app = FastAPI(title="GooseStrike HackGPT Relay")
|
||||
|
||||
|
||||
class PromptRequest(BaseModel):
|
||||
prompt: str
|
||||
context: Optional[dict] = None
|
||||
|
||||
|
||||
class PromptResponse(BaseModel):
|
||||
request_id: str
|
||||
prompt: str
|
||||
echoed_context: Optional[dict]
|
||||
forwarded: bool
|
||||
recommendation: str
|
||||
|
||||
|
||||
@app.post("/prompt", response_model=PromptResponse)
|
||||
async def handle_prompt(body: PromptRequest) -> PromptResponse:
|
||||
request_id = str(uuid.uuid4())
|
||||
forwarded = False
|
||||
webhook = os.getenv("N8N_WEBHOOK_URL")
|
||||
if webhook:
|
||||
try:
|
||||
requests.post(
|
||||
webhook,
|
||||
json={"request_id": request_id, "prompt": body.prompt, "context": body.context},
|
||||
timeout=10,
|
||||
)
|
||||
forwarded = True
|
||||
except requests.RequestException:
|
||||
forwarded = False
|
||||
return PromptResponse(
|
||||
request_id=request_id,
|
||||
prompt=body.prompt,
|
||||
echoed_context=body.context,
|
||||
forwarded=forwarded,
|
||||
recommendation="HackGPT relay placeholder – plug in your model pipeline.",
|
||||
)
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
async def health() -> dict:
|
||||
return {"ok": True}
|
||||
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())
|
||||
294
tests/test_scanner_and_api.py
Normal file
294
tests/test_scanner_and_api.py
Normal file
@@ -0,0 +1,294 @@
|
||||
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
|
||||
from app.agents import llm_router
|
||||
|
||||
|
||||
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"
|
||||
|
||||
|
||||
def test_n8n_workflow_import_and_list(temp_db):
|
||||
client = TestClient(api.app)
|
||||
payload = {"name": "demo-workflow", "workflow": {"nodes": ["scan", "alert"]}}
|
||||
response = client.post("/n8n/workflows/import", json=payload)
|
||||
assert response.status_code == 201
|
||||
body = response.json()
|
||||
assert body["name"] == "demo-workflow"
|
||||
listing = client.get("/n8n/workflows")
|
||||
assert listing.status_code == 200
|
||||
assert listing.json()[0]["name"] == "demo-workflow"
|
||||
|
||||
|
||||
def test_armitage_data_endpoint_returns_graph(temp_db):
|
||||
client = TestClient(api.app)
|
||||
data = client.get("/armitage/data").json()
|
||||
assert "nodes" in data and "edges" in data
|
||||
assert any(node["type"] == "asset" for node in data["nodes"])
|
||||
|
||||
|
||||
def test_n8n_integration_test_endpoint(temp_db, monkeypatch):
|
||||
client = TestClient(api.app)
|
||||
|
||||
class DummyResponse:
|
||||
ok = True
|
||||
status_code = 200
|
||||
headers = {"content-type": "application/json"}
|
||||
|
||||
def json(self):
|
||||
return {"echo": True}
|
||||
|
||||
text = ""
|
||||
|
||||
captured = {}
|
||||
|
||||
def fake_post(url, json=None, timeout=None): # pylint: disable=redefined-outer-name
|
||||
captured["url"] = url
|
||||
captured["json"] = json
|
||||
captured["timeout"] = timeout
|
||||
return DummyResponse()
|
||||
|
||||
monkeypatch.setattr(api.requests, "post", fake_post)
|
||||
|
||||
response = client.post(
|
||||
"/integrations/test/n8n",
|
||||
json={"url": "https://n8n.example/webhook", "payload": {"hello": "world"}, "timeout": 5},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
body = response.json()
|
||||
assert body["ok"] is True
|
||||
assert captured["json"]["hello"] == "world"
|
||||
assert captured["timeout"] == 5
|
||||
|
||||
|
||||
def test_ollama_integration_test_endpoint(temp_db, monkeypatch):
|
||||
client = TestClient(api.app)
|
||||
|
||||
class DummyResponse:
|
||||
ok = True
|
||||
status_code = 200
|
||||
headers = {"content-type": "application/json"}
|
||||
|
||||
def json(self):
|
||||
return {"response": "pong"}
|
||||
|
||||
text = ""
|
||||
|
||||
captured = {}
|
||||
|
||||
def fake_post(url, json=None, timeout=None):
|
||||
captured["url"] = url
|
||||
captured["json"] = json
|
||||
return DummyResponse()
|
||||
|
||||
monkeypatch.setattr(api.requests, "post", fake_post)
|
||||
|
||||
response = client.post(
|
||||
"/integrations/test/ollama",
|
||||
json={"url": "http://ollama.internal:11434", "model": "mock", "prompt": "ping"},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json()["ok"] is True
|
||||
assert captured["json"]["model"] == "mock"
|
||||
assert captured["json"]["prompt"] == "ping"
|
||||
assert captured["url"].endswith("/api/generate")
|
||||
|
||||
|
||||
def test_llm_router_falls_back_to_ollama(monkeypatch):
|
||||
monkeypatch.delenv("CLAUDE_API_URL", raising=False)
|
||||
monkeypatch.delenv("HACKGPT_API_URL", raising=False)
|
||||
monkeypatch.setenv("OLLAMA_BASE_URL", "http://ollama.fleet:11434")
|
||||
monkeypatch.setenv("OLLAMA_MODEL", "mock-llm")
|
||||
|
||||
class DummyResponse:
|
||||
headers = {"content-type": "application/json"}
|
||||
|
||||
def __init__(self):
|
||||
self.ok = True
|
||||
self.status_code = 200
|
||||
|
||||
def json(self):
|
||||
return {"response": "hello from ollama"}
|
||||
|
||||
text = "hello"
|
||||
|
||||
def raise_for_status(self):
|
||||
return None
|
||||
|
||||
captured = {}
|
||||
|
||||
def fake_post(url, json=None, headers=None, timeout=None):
|
||||
captured["url"] = url
|
||||
captured["json"] = json
|
||||
return DummyResponse()
|
||||
|
||||
monkeypatch.setattr(llm_router.requests, "post", fake_post)
|
||||
|
||||
result = llm_router.call_llm_with_fallback("status?")
|
||||
assert "ollama" in captured["url"]
|
||||
assert captured["json"]["model"] == "mock-llm"
|
||||
assert "hello from ollama" in result
|
||||
37
web/static/architecture.svg
Normal file
37
web/static/architecture.svg
Normal file
@@ -0,0 +1,37 @@
|
||||
<svg width="800" height="420" viewBox="0 0 800 420" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="grad" x1="0%" y1="0%" x2="100%" y2="0%">
|
||||
<stop offset="0%" style="stop-color:#0b0b0b;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#1f1f1f;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="800" height="420" fill="url(#grad)" rx="32" />
|
||||
<text x="400" y="50" font-size="28" fill="#fff" text-anchor="middle">GooseStrike Flow</text>
|
||||
<rect x="50" y="90" width="160" height="70" rx="14" fill="#b81b29" />
|
||||
<text x="130" y="135" text-anchor="middle" fill="#fff" font-size="16">scanner.py</text>
|
||||
<rect x="300" y="90" width="200" height="70" rx="14" fill="#d63b45" />
|
||||
<text x="400" y="120" text-anchor="middle" fill="#fff" font-size="16">FastAPI / UI</text>
|
||||
<text x="400" y="140" text-anchor="middle" fill="#fff" font-size="12">assets • scans • tasks</text>
|
||||
<rect x="580" y="90" width="160" height="70" rx="14" fill="#f5a623" />
|
||||
<text x="660" y="125" text-anchor="middle" fill="#111" font-size="16">Agents</text>
|
||||
<rect x="50" y="230" width="200" height="70" rx="14" fill="#222" stroke="#fff" stroke-width="2" />
|
||||
<text x="150" y="260" text-anchor="middle" fill="#fff" font-size="14">db/goosestrike.db</text>
|
||||
<rect x="300" y="230" width="200" height="70" rx="14" fill="#333" stroke="#fff" stroke-width="2" />
|
||||
<text x="400" y="250" text-anchor="middle" fill="#fff" font-size="14">db/exploits.db</text>
|
||||
<text x="400" y="270" text-anchor="middle" fill="#fff" font-size="12">CVE API</text>
|
||||
<rect x="580" y="230" width="160" height="70" rx="14" fill="#444" stroke="#fff" stroke-width="2" />
|
||||
<text x="660" y="260" text-anchor="middle" fill="#fff" font-size="14">HackGPT Relay</text>
|
||||
<rect x="300" y="330" width="200" height="60" rx="14" fill="#555" stroke="#fff" stroke-width="2" />
|
||||
<text x="400" y="360" text-anchor="middle" fill="#fff" font-size="14">n8n workflows</text>
|
||||
<line x1="210" y1="125" x2="300" y2="125" stroke="#fff" stroke-width="2" marker-end="url(#arrow)" />
|
||||
<line x1="500" y1="125" x2="580" y2="125" stroke="#fff" stroke-width="2" marker-end="url(#arrow)" />
|
||||
<line x1="400" y1="160" x2="400" y2="230" stroke="#fff" stroke-width="2" marker-end="url(#arrow)" />
|
||||
<line x1="130" y1="160" x2="130" y2="230" stroke="#fff" stroke-width="2" marker-end="url(#arrow)" />
|
||||
<line x1="660" y1="160" x2="660" y2="230" stroke="#fff" stroke-width="2" marker-end="url(#arrow)" />
|
||||
<line x1="400" y1="300" x2="400" y2="330" stroke="#fff" stroke-width="2" marker-end="url(#arrow)" />
|
||||
<defs>
|
||||
<marker id="arrow" markerWidth="10" markerHeight="7" refX="10" refY="3.5" orient="auto">
|
||||
<polygon points="0 0, 10 3.5, 0 7" fill="#fff" />
|
||||
</marker>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.7 KiB |
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 |
449
web/static/styles.css
Normal file
449
web/static/styles.css
Normal file
@@ -0,0 +1,449 @@
|
||||
: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);
|
||||
}
|
||||
|
||||
/* Armitage-style view */
|
||||
.armitage-body {
|
||||
background: radial-gradient(circle at top, #1f1f1f, #0b0b0b);
|
||||
color: #f4f4f4;
|
||||
min-height: 100vh;
|
||||
margin: 0;
|
||||
font-family: 'Inter', 'Segoe UI', sans-serif;
|
||||
}
|
||||
|
||||
.armitage-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 1.5rem 2rem;
|
||||
background: linear-gradient(90deg, #111, #222);
|
||||
border-bottom: 2px solid #b81b29;
|
||||
}
|
||||
|
||||
.armitage-header h1 {
|
||||
margin: 0;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.armitage-header nav a {
|
||||
color: #fff;
|
||||
margin-left: 1rem;
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.armitage-canvas {
|
||||
position: relative;
|
||||
margin: 2rem;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 16px;
|
||||
background: radial-gradient(circle at center, rgba(255, 255, 255, 0.05), rgba(0, 0, 0, 0.9));
|
||||
min-height: 70vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.armitage-edges {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
}
|
||||
|
||||
.armitage-nodes {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.armitage-node {
|
||||
position: absolute;
|
||||
transform: translate(-50%, -50%);
|
||||
padding: 0.35rem 0.8rem;
|
||||
border-radius: 999px;
|
||||
font-size: 0.85rem;
|
||||
text-align: center;
|
||||
pointer-events: auto;
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.armitage-node.asset {
|
||||
background: rgba(184, 27, 41, 0.85);
|
||||
}
|
||||
|
||||
.armitage-node.service {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.armitage-node.cve {
|
||||
background: rgba(255, 205, 41, 0.8);
|
||||
color: #111;
|
||||
}
|
||||
|
||||
.armitage-edge {
|
||||
stroke: rgba(255, 255, 255, 0.25);
|
||||
stroke-width: 2;
|
||||
}
|
||||
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 |
68
web/templates/armitage.html
Normal file
68
web/templates/armitage.html
Normal file
@@ -0,0 +1,68 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>GooseStrike Armitage View</title>
|
||||
<link rel="stylesheet" href="/static/styles.css" />
|
||||
</head>
|
||||
<body class="armitage-body">
|
||||
<header class="armitage-header">
|
||||
<h1>GooseStrike – Armitage-style graph</h1>
|
||||
<nav>
|
||||
<a href="/">Dashboard</a>
|
||||
<a href="/mockup">Mock data</a>
|
||||
</nav>
|
||||
</header>
|
||||
<section class="armitage-canvas" id="armitage-canvas">
|
||||
<svg id="edge-layer" class="armitage-edges" preserveAspectRatio="none"></svg>
|
||||
<div id="armitage-nodes" class="armitage-nodes"></div>
|
||||
</section>
|
||||
<script>
|
||||
const graph = {{ graph_json | safe }};
|
||||
const canvas = document.getElementById('armitage-canvas');
|
||||
const nodesLayer = document.getElementById('armitage-nodes');
|
||||
const edgeLayer = document.getElementById('edge-layer');
|
||||
|
||||
function renderGraph() {
|
||||
const width = canvas.clientWidth;
|
||||
const height = canvas.clientHeight;
|
||||
edgeLayer.setAttribute('viewBox', `0 0 ${width} ${height}`);
|
||||
edgeLayer.setAttribute('width', width);
|
||||
edgeLayer.setAttribute('height', height);
|
||||
nodesLayer.innerHTML = '';
|
||||
edgeLayer.innerHTML = '';
|
||||
const radius = Math.min(width, height) / 2 - 80;
|
||||
const positions = {};
|
||||
graph.nodes.forEach((node, idx) => {
|
||||
const angle = (idx / graph.nodes.length) * Math.PI * 2;
|
||||
const cx = width / 2 + radius * Math.cos(angle);
|
||||
const cy = height / 2 + radius * Math.sin(angle);
|
||||
positions[node.id] = { x: cx, y: cy };
|
||||
const el = document.createElement('div');
|
||||
el.className = `armitage-node ${node.type}`;
|
||||
el.style.left = `${cx}px`;
|
||||
el.style.top = `${cy}px`;
|
||||
el.textContent = node.label;
|
||||
nodesLayer.appendChild(el);
|
||||
});
|
||||
graph.edges.forEach((edge) => {
|
||||
const source = positions[edge.source];
|
||||
const target = positions[edge.target];
|
||||
if (!source || !target) {
|
||||
return;
|
||||
}
|
||||
const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
|
||||
line.setAttribute('x1', source.x);
|
||||
line.setAttribute('y1', source.y);
|
||||
line.setAttribute('x2', target.x);
|
||||
line.setAttribute('y2', target.y);
|
||||
line.classList.add('armitage-edge');
|
||||
edgeLayer.appendChild(line);
|
||||
});
|
||||
}
|
||||
|
||||
window.addEventListener('resize', () => requestAnimationFrame(renderGraph));
|
||||
renderGraph();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
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