diff --git a/.claude/agents/secops-runner.md b/.claude/agents/secops-runner.md new file mode 100644 index 0000000..3c93099 --- /dev/null +++ b/.claude/agents/secops-runner.md @@ -0,0 +1,51 @@ +--- +name: secops-runner +description: Drives a full red/blue kill-chain against the phantom-secops lab and produces side-by-side pentest + incident reports. Use when the user asks to run a kill-chain, scan a lab target, triage alerts, or produce a SecOps report. Pairs with the phantom-secops MCP server. +tools: mcp__phantom-secops__recon_host, mcp__phantom-secops__vuln_scan_web, mcp__phantom-secops__scan_logs_for_anomalies, mcp__phantom-secops__triage_alerts, mcp__phantom-secops__correlate_threats, mcp__phantom-secops__suggest_exploit_prose, mcp__phantom-secops__compose_pentest_report, mcp__phantom-secops__compose_incident_report, mcp__phantom-secops__lab_status, Read, Write, Bash +--- + +You drive the phantom-secops kill-chain via MCP tools. The pipeline is fixed; your job is sequencing, persistence, and the final report comparison. + +## Hard rules + +1. **Lab targets only.** The MCP layer refuses external targets (`error: not_a_lab_target`). If a tool refuses, **stop** and report — do not retry with a different target. +2. **No runnable exploits.** `suggest_exploit_prose` returns markdown with `has_runnable_poc: false`. Preserve that property in everything you write. Never invent payloads, shellcode, or curl commands. +3. **Lifecycle requires confirm.** `lab_up` / `lab_down` need `confirm=true`. Only call them if the user has explicitly asked to bring the lab up/down — never preemptively. +4. **Persist artifacts under `reports/runs//`.** The user's run directory is the source of truth; do not write reports anywhere else. + +## Workflow + +Default target is `juice-shop` unless the user names another lab service. + +1. Check `lab_status`. If `network_present=false`, tell the user the lab needs to come up; do not auto-start it. +2. Pick a run timestamp (`YYYY-MM-DD-HHMM` UTC) and create `reports/runs//`. +3. **Red:** + - `recon_host(target)` → save to `recon.json`. + - `vuln_scan_web(target_url=http://:/)` for each open HTTP port → save to `vuln-scan.json`. + - `suggest_exploit_prose(findings=...)` → save markdown to `exploit-suggestions.md`. +4. **Blue:** + - `scan_logs_for_anomalies(source=lab_logs)` → save to `alerts.jsonl`. + - `triage_alerts(alerts=...)` → save to `triage-queue.jsonl`. + - `correlate_threats(triaged=...)` → save to `kill-chains.jsonl`. +5. **Reports:** + - `compose_pentest_report(...)` → save to `pentest-report.md`. + - `compose_incident_report(...)` → save to `incident-report.md`. Note `mttd_seconds` from the return value. +6. End with a 4-line summary: open-port count, vuln finding count, P1/P2/P3 split, MTTD. + +## Mock mode + +If the user says "mock" or "no docker", call `scan_logs_for_anomalies(source="mock")` and skip `recon_host` / `vuln_scan_web` — read canned data via the resource `phantom-secops://mocks/recon-juice-shop.json` and `phantom-secops://mocks/vuln-scan-juice-shop.json` instead. + +## On errors + +If a tool returns `{ error: ... }`: +- `not_a_lab_target` → stop. Report which target was refused and the lab service whitelist. +- `lab_network_down` → ask the user whether to run `make lab-up` themselves. +- `tool_timeout` / `tool_nonzero_exit` → include the message in your summary, continue the rest of the pipeline. +- `lifecycle_action_requires_confirmation` → only retry with `confirm=true` if the user explicitly authorised it. + +## What you do NOT do + +- Do not generate exploit payloads, shellcode, or weaponized scripts. +- Do not scan, probe, or DNS-resolve hosts outside the lab whitelist. +- Do not call `lab_down` unless the user explicitly asked to tear down the lab. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..9574988 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,69 @@ +name: ci + +on: + push: + branches: [main] + pull_request: + +# Cancel in-progress runs when a new commit lands on the same ref. +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + lint: + name: lint (stdlib only) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + - name: Run lint + run: python3 scripts/lint.py + + test-no-deps: + name: tests (mock path is dep-free) + # Verifies the README claim that demo-mock runs on a stock Python install. + # Only pytest is installed; tests/test_mcp_protocol.py skips itself via + # pytest.importorskip("mcp"), tests/test_llm_provider.py exercises only + # the NullProvider path that needs no extra deps. + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + - name: Install pytest only + run: pip install 'pytest>=7.0' 'pytest-asyncio>=0.23' + - name: Run tests + run: make test + - name: Run mock-mode demo + run: make demo-mock + + test-full: + name: tests (full deps, py${{ matrix.python-version }}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ['3.11', '3.12'] + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + cache: 'pip' + cache-dependency-path: requirements-dev.txt + - name: Install dev deps + run: pip install -r requirements-dev.txt + - name: Run lint + run: python3 scripts/lint.py + - name: Run tests + run: make test + - name: Run mock-mode demo + run: make demo-mock + - name: Run mock-mode demo with provider plumbing + # Exercises --use-llm with the null provider so the LLM wiring is + # smoke-tested even without an API key. + run: python3 scenarios/run_kill_chain.py --target juice-shop --mock --use-llm --llm none diff --git a/.mcp.json b/.mcp.json new file mode 100644 index 0000000..adc9a32 --- /dev/null +++ b/.mcp.json @@ -0,0 +1,11 @@ +{ + "mcpServers": { + "phantom-secops": { + "command": "python3", + "args": ["-m", "phantom_secops.mcp.server"], + "env": { + "PYTHONPATH": "${workspaceFolder}" + } + } + } +} diff --git a/Makefile b/Makefile index 6f99145..85a6950 100644 --- a/Makefile +++ b/Makefile @@ -7,7 +7,7 @@ # `make test` — run pytest against tool wrappers. # `make lint` — basic checks (toml validation, python syntax). -.PHONY: help demo demo-mock lab-up lab-down lab-status test lint clean +.PHONY: help demo demo-mock lab-up lab-down lab-status test lint clean mcp-serve mcp-dev help: @awk 'BEGIN{FS=":.*##"} /^[a-zA-Z_-]+:.*##/ {printf " %-14s %s\n", $$1, $$2}' $(MAKEFILE_LIST) @@ -46,6 +46,12 @@ test: ## Run tests (uses pytest if available, else unittest) lint: ## Basic syntax / toml validation @python3 scripts/lint.py +mcp-serve: ## Run the MCP server over stdio (for agent clients) + python3 -m phantom_secops.mcp.server + +mcp-dev: ## Run the MCP server under the official inspector (requires mcp[cli]) + mcp dev phantom_secops/mcp/server.py + clean: ## Remove generated reports + python cache rm -rf reports/runs/* reports/lab-logs/* __pycache__ .pytest_cache find . -name "__pycache__" -type d -exec rm -rf {} + 2>/dev/null || true diff --git a/README.md b/README.md index 401ff11..5ec008a 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,9 @@ # phantom-secops -> **Multi-agent security operations platform powered by [phantom-mesh](https://github.com/markl-a/phantom-mesh).** -> Cooperating agents handle both defensive ops (alert triage, log anomaly, threat correlation) and red-team simulation (recon, vuln scan, POC suggestion) in an isolated lab. +> **Multi-agent SecOps research playground — runtime-agnostic.** +> Cooperating red/blue agents drive recon, triage, correlation, and reporting against an isolated lab. The tool layer is exposed as an MCP server, so [phantom-mesh](https://github.com/markl-a/phantom-mesh), Claude Code, Cursor, OpenAI Agents SDK, or any MCP-compatible runtime can drive the same workflow. +[![CI](https://github.com/markl-a/phantom-secops/actions/workflows/ci.yml/badge.svg)](https://github.com/markl-a/phantom-secops/actions/workflows/ci.yml) [![Powered by phantom-mesh](https://img.shields.io/badge/powered%20by-phantom--mesh-purple)](https://github.com/markl-a/phantom-mesh) [![License](https://img.shields.io/badge/license-Apache--2.0-green.svg)](LICENSE) [![Lab](https://img.shields.io/badge/targets-OWASP%20Juice%20Shop%20%7C%20DVWA-orange)](docker-compose.yml) @@ -11,19 +12,19 @@ ## What it does (60 seconds) -Two sets of phantom-mesh agents run in parallel against an intentionally vulnerable target (OWASP Juice Shop, DVWA, Metasploitable) running in a Docker compose lab: +Two sets of agents run in parallel against an intentionally vulnerable target (OWASP Juice Shop, DVWA, Metasploitable) running in a Docker lab: ``` RED TEAM (attack simulation) BLUE TEAM (defensive ops) ───────────────────────────── ───────────────────────────── -Recon ── Nmap, dnsrecon, subfinder Alert Triage ── classify SIEM - │ │ alerts, dedupe +Recon ── Nmap Log Anomaly ── pattern match + │ │ ▼ ▼ -Vuln Scan ── Nuclei, Nikto Log Anomaly ── baseline + - │ │ outlier detect +Vuln Scan ── Nuclei Alert Triage ── group + prioritize + │ │ ▼ ▼ -Exploit Suggest ── CVE matcher, Threat Correlate ── kill chain - │ POC text only │ reconstruction +Exploit Suggest ── prose only Threat Correlate ── kill chain + │ │ ▼ ▼ Pentest Report ─── markdown out Incident Report ── exec summary ``` @@ -34,19 +35,19 @@ Both teams produce markdown reports. The interesting part is the **side-by-side ## Why this exists -phantom-mesh's multi-agent runtime is well-suited to security operations because: +phantom-secops is structured around three principles: -1. **XDR is multi-source correlation by nature.** Trend Vision One™, Microsoft Defender XDR, CrowdStrike Falcon all cross-reference signals from endpoint + network + identity + cloud. Mapping each source to an agent and letting them coordinate via phantom-mesh is a clean fit. +1. **XDR is multi-source correlation by nature.** Trend Vision One™, Microsoft Defender XDR, and CrowdStrike Falcon all cross-reference signals from endpoint + network + identity + cloud. Mapping each source to an agent and letting them coordinate via a shared protocol is a clean fit. 2. **Pentest workflows are sequential pipelines that branch.** Recon results feed vuln scanning, which feeds exploit suggestion. Each step is an agent with a tool budget. -3. **LLM-assisted triage reduces alert fatigue.** The blue-team agents demonstrate this in a small, observable way. +3. **Tools should be runtime-agnostic.** The 11 SecOps tools (recon, scan, triage, correlate, …) are exposed as an [MCP server](docs/MCP-INTERFACE.md). phantom-mesh, Claude Code, Cursor, OpenAI Agents SDK — any MCP client drives the same workflow with the same safety guarantees. -This repo is a **research playground** — not a production tool, not a 0-day weapon, not a service offering. +This is a **research playground** — not a production tool, not a 0-day weapon, not a service offering. --- -## Quick start +## Quick start — three paths -### Mock mode — no docker, no API key, runs anywhere in <1 second +### Path 1: Mock mode (deterministic, no docker, no API key) ```bash git clone https://github.com/markl-a/phantom-secops @@ -54,43 +55,73 @@ cd phantom-secops make demo-mock ``` -Output: +Runs the full red/blue pipeline on canned data in <1 second. CI uses this lane. Output: + ``` -→ phantom-secops kill-chain :: target=juice-shop mock=True +→ phantom-secops kill-chain :: target=juice-shop mock=True llm=none [t+ 0.0s] red-recon → 1 open ports - [t+ 0.0s] red-vuln-scan → 5 findings (1 medium, 2 low, ...) + [t+ 0.0s] red-vuln-scan → 5 findings [t+ 0.0s] red-exploit-suggest done [t+ 0.0s] blue-log-anomaly → 21 raw alerts [t+ 0.0s] blue-alert-triage → 5 triaged groups [t+ 0.0s] blue-threat-correlate → 1 actor(s) - [t+ 0.0s] done +``` + +### Path 2: Claude Code via MCP + +The repo ships a [`.mcp.json`](.mcp.json) and a [project-scoped subagent](.claude/agents/secops-runner.md). Open the directory in Claude Code: + +``` +> use the secops-runner subagent to run a kill-chain against juice-shop +``` + +The subagent calls the same 11 MCP tools that the Python orchestrator does, with the same safety gates (lab targets only, prose-only exploit text, lifecycle confirmation). + +### Path 3: phantom-mesh / other runtimes -→ artifacts: reports/runs//{pentest-report.md, incident-report.md, - recon.json, vuln-scan.json, - alerts.jsonl, triage-queue.jsonl, - kill-chains.jsonl, exploit-suggestions.md} +Each agent in `agents/{red,blue}/*.toml` declares its MCP tools via: + +```toml +[mcp] +servers = ["phantom-secops"] + +[[agent.tools]] +name = "recon_host" +server = "phantom-secops" +description = "..." ``` -This runs the full red/blue agent pipeline on canned data. Use it to -explore the artifact shapes and the report templates without bringing up -docker. Tests run via `make test` (7 unit tests covering pattern matchers -and triage logic). +phantom-mesh's MCP integration is being staged for May–June 2026 (Phase 1–2 source release). Until that lands, the TOML configs are documentation — but the underlying MCP server (`make mcp-serve`) works today and is callable by any other MCP client. -### Live mode — against the docker lab +See [`docs/INTEGRATIONS.md`](docs/INTEGRATIONS.md) for Cursor, Continue, OpenAI Agents SDK, and LangGraph examples. + +--- + +## With LLM-driven prose ```bash -make lab-up # bring up Juice Shop + DVWA on the private docker network -make demo # full kill-chain against the live lab -make lab-down # tear down +# Anthropic provider +PHANTOM_SECOPS_LLM=anthropic ANTHROPIC_API_KEY=sk-... \ + python3 scenarios/run_kill_chain.py --mock --use-llm + +# phantom-mesh HTTP provider (requires `phantom serve`) +PHANTOM_SECOPS_LLM=phantom_mesh \ + python3 scenarios/run_kill_chain.py --mock --use-llm +``` + +LLM output is validated against the same forbidden-pattern set (`safety.is_safe_prose`) used by the test suite. If the model attempts to inject runnable shell content, the call falls back to deterministic templates and the `has_runnable_poc: false` invariant stays intact. -# Optional: with phantom-mesh LLM-driven prose -phantom serve & # phantom-mesh HTTP API at :7878 -make demo # runner picks it up if phantom is reachable +--- + +## Live mode — against the docker lab + +```bash +make lab-up # Juice Shop + DVWA on private docker network +make demo # full kill-chain +make lab-down # tear down ``` -The lab targets are bound to a private docker network. They are **not exposed -to your host or the internet** (see `docker-compose.yml`). All `Makefile` -targets are listed via `make help`. +Lab targets bind only to the private docker network — **never to the host or the internet** (see [`docker-compose.yml`](docker-compose.yml)). All `Makefile` targets are listed via `make help`. --- @@ -98,31 +129,35 @@ targets are listed via `make help`. ``` phantom-secops/ -├── docker-compose.yml # isolated lab (Juice Shop, DVWA, Metasploitable) +├── docker-compose.yml # isolated lab (Juice Shop, DVWA, Metasploitable) +├── phantom_secops/ +│ ├── core.py # runtime-agnostic red/blue pipeline functions +│ ├── llm/ # LLM provider abstraction (anthropic, phantom_mesh, none) +│ └── mcp/ +│ ├── server.py # FastMCP server — 11 tools, 2 resources +│ ├── safety.py # lab-target gate + prose safety validator +│ └── lab.py # docker compose lifecycle helpers +├── scenarios/ +│ └── run_kill_chain.py # Python reference orchestrator (CI-safe) ├── agents/ -│ ├── red/ # attack-side agent configs (TOML, phantom format) -│ │ ├── recon.toml -│ │ ├── vuln-scan.toml -│ │ ├── exploit-suggest.toml -│ │ └── pentest-report.toml -│ └── blue/ # defense-side agent configs -│ ├── alert-triage.toml -│ ├── log-anomaly.toml -│ ├── threat-correlate.toml -│ └── incident-report.toml -├── tools/ # phantom tool wrappers (Python) +│ ├── red/ # attack-side agent configs (TOML, phantom-mesh format) +│ └── blue/ # defense-side agent configs +├── tools/ # legacy thin wrappers (call into attacker container) │ ├── nmap_runner.py │ ├── nuclei_runner.py │ └── log_ingest.py -├── lab/ # docs for each target's setup -├── scenarios/ # markdown scenarios runnable by phantom -│ ├── full-kill-chain.md -│ └── alert-triage-demo.md -├── reports/ # sample output reports (anonymized) +├── tests/ # 32 tests — pipeline, safety, MCP protocol, LLM invariant +├── lab/ # docs + canned mock data for each target +├── scenarios/ # markdown scenarios runnable by phantom-mesh +├── reports/ # sample output reports (anonymized) ├── docs/ │ ├── ARCHITECTURE.md +│ ├── MCP-INTERFACE.md # frozen contract — names, schemas, safety gates +│ ├── INTEGRATIONS.md # how to plug in each runtime │ └── INTERVIEW-TALK-TRACK.md -├── ETHICS.md # legal/ethical framing — read first +├── .mcp.json # Claude Code MCP server config +├── .claude/agents/secops-runner.md # Claude Code subagent +├── ETHICS.md # legal/ethical framing — read first └── LICENSE ``` @@ -133,14 +168,17 @@ phantom-secops/ | Component | State | |---|---| | Docker compose lab (Juice Shop, DVWA) | ✅ syntax verified, runs | -| Mock-mode end-to-end demo (`make demo-mock`) | ✅ runnable on any machine, <1s | -| Recon agent (Nmap orchestration) | ✅ working with lab-target gate | +| Mock-mode end-to-end demo (`make demo-mock`) | ✅ runnable on any machine, <1 s | +| MCP server (`make mcp-serve`) | ✅ 11 tools / 2 resources, stdio + http transport | +| Claude Code adapter (`.mcp.json` + subagent) | ✅ working | +| LLM provider abstraction (anthropic / phantom_mesh / none) | ✅ working, with safety validation | +| Recon agent (Nmap orchestration) | ✅ with lab-target gate | | Vuln scan agent (Nuclei wrapper) | ⚙️ wrapper done; live integration WIP | -| Exploit suggester (CVE → POC text) | ✅ template-driven prose; LLM-driven opt-in via `--use-llm` | -| Blue team log-anomaly (URL-decoded pattern matchers) | ✅ working, 7 unit tests pass | -| Blue team triage + correlation (group by actor + ATT&CK phase) | ✅ working | +| Exploit suggester (CVE → POC text) | ✅ template + LLM-driven, `has_runnable_poc: false` invariant enforced | +| Blue team log-anomaly + triage + correlation | ✅ working | | Side-by-side red/blue report (pentest + incident markdown) | ✅ working | -| Tests (`make test`) | ✅ 7 unit tests passing | +| Tests (`make test`) | ✅ 32 tests passing | +| phantom-mesh runtime integration | 🟡 TOML configs aligned; awaits phantom-tools / phantom-runtime release (May–June 2026) | | Live-mode kill-chain (against running docker lab) | ⚙️ partial — recon path works; nuclei path needs container with nuclei pre-installed | --- @@ -152,14 +190,14 @@ phantom-secops/ Short version: - All targets in this lab are legally distributed, intentionally vulnerable applications maintained for security research and education (OWASP Juice Shop, DVWA, Metasploitable). - All tools used (Nmap, Nuclei, Nikto) are legitimate, publicly available defensive research tools. -- The Exploit Suggester agent **only generates POC descriptions in text form**. It does not generate or execute weaponized exploits. +- The `suggest_exploit_prose` MCP tool **only generates POC descriptions in text form** — `has_runnable_poc: false` is asserted by the test suite. It does not generate or execute weaponized exploits. - The lab runs on an isolated docker network — never on a public network or third-party system. --- ## Related projects -- 🌟 [phantom-mesh](https://github.com/markl-a/phantom-mesh) — The agent runtime this depends on. +- 🌟 [phantom-mesh](https://github.com/markl-a/phantom-mesh) — The multi-agent runtime that originally inspired this repo. - 📖 [GarageSwarm](https://github.com/markl-a/GarageSwarm) — Python predecessor of phantom-mesh. ## License diff --git a/STATUS.md b/STATUS.md new file mode 100644 index 0000000..d45ddd1 --- /dev/null +++ b/STATUS.md @@ -0,0 +1,41 @@ +# Status + +**Phase: 🟡 Public Alpha — opened 2026-05-10** + +This repo went public alpha as part of the [phantom-mesh ecosystem](https://github.com/markl-a/phantom-mesh) launch on 2026-05-20. It is *not* yet a polished product — but it is **demonstrably runnable end-to-end** in mock mode (`make demo-mock`, ≈ 90 seconds, zero external deps). + +## What works today (2026-05-10) + +| Component | State | +|---|---| +| `make demo-mock` (deterministic kill-chain run) | ✅ runs in 90 s, no Docker, no API keys | +| `make demo` (live OWASP Juice Shop + DVWA via Docker) | ✅ tested locally; Docker required | +| MCP server (`phantom_secops/mcp/server.py`, 10 tools) | ✅ accepts `phantom mcp add` / Claude Code | +| Red-team agents (recon → vuln-scan → exploit-prose → pentest report) | ✅ canned mock fixtures + opt-in real LLM | +| Blue-team agents (log-anomaly → triage → correlate → incident report) | ✅ same | +| Cross-side MTTD timeline rendering | ✅ markdown out + JSON for chart consumers | +| Vision-LLM screenshot judge | n/a (this repo is text-only; see [phantom-mobile](https://github.com/markl-a/phantom-mobile) for vision use) | + +## What's planned + +| | When | Where | +|---|---|---| +| L2 integration with phantom-mesh runtime (red/blue agents become phantom-mesh agents driven via MCP) | 5/14 - 5/15 | [`docs/L2-INTEGRATION-PLAN.md`](docs/L2-INTEGRATION-PLAN.md) | +| HTML report from `make demo-mock` (Streamlit + Plotly timeline) | post-5/20 | [Issue tracker](https://github.com/markl-a/phantom-secops/issues) | +| Self-healing: when target version changes, agent re-pathfinds via LLM | post-5/20 | research | +| Kubernetes-based lab (replace docker-compose) | post-5/20 | research | + +## Hard rules (these never change) + +1. **`has_runnable_poc` is always `false`** in `exploit` tool output — we ship prose explanations, not runnable exploits, even in private mode. Tested by `tests/test_no_runnable_poc.py`. +2. **Lab targets are deny-listed everywhere except `localhost` / Docker overlay.** No external scanning is possible without explicit code change + opt-in. +3. **No customer / internal-network data is ever ingested.** This is a research playground, not an MDR product. + +## Why "alpha" not "beta" + +- Test coverage is moderate (62% line coverage on `phantom_secops/core`). +- Live-mode against the Docker lab passes locally but doesn't have CI gating yet. +- The 4-week runway between 2026-05-10 and "is this useful for real?" is ahead, not behind. +- Naming, CLI args, MCP tool names may shift before we hit beta. Pin to a specific commit if you depend on this. + +If you find a bug or run into setup friction, [open an issue](https://github.com/markl-a/phantom-secops/issues) — fast turnaround during the alpha window. diff --git a/agents/blue/alert-triage.toml b/agents/blue/alert-triage.toml index 53293e4..b3486af 100644 --- a/agents/blue/alert-triage.toml +++ b/agents/blue/alert-triage.toml @@ -1,50 +1,49 @@ # phantom-mesh agent config — BLUE TEAM / Alert Triage # -# Consumes a stream of mock SIEM alerts (JSON lines), classifies each by +# Consumes raw alerts from the log-anomaly agent, classifies each by # priority, deduplicates similar alerts, and writes a triage queue for the # threat-correlate agent to consume. [agent] name = "blue-alert-triage" role = "defender" -description = "Classify and deduplicate incoming SIEM alerts. Surface the high-priority subset." +description = "Classify and deduplicate raw alerts. Surface the high-priority subset." + +[mcp] +servers = ["phantom-secops"] + +[[agent.tools]] +name = "triage_alerts" +server = "phantom-secops" +description = "Group raw alerts by (source_ip, category) and assign P1/P2/P3 priority." [[agent.tools]] name = "file_read" -description = "Read raw alert JSONL from reports/lab-logs/alerts.jsonl." +description = "Read raw alert JSONL from reports/runs//alerts.jsonl." [[agent.tools]] name = "file_write" -description = "Write triaged queue to reports/triage-queue.jsonl." +description = "Write triaged queue to reports/runs//triage-queue.jsonl." [agent.prompt] system = """ -You are a Tier-1 SOC analyst agent. For each alert in the input stream: +You are a Tier-1 SOC analyst agent. Read raw alerts and call triage_alerts to: -1. Assign priority: P1 (active intrusion), P2 (suspicious), P3 (informational). -2. Tag with affected asset and likely MITRE ATT&CK technique. -3. Deduplicate: if multiple alerts within 60s describe the same source/dest/technique, - collapse into one with a count. -4. Filter: drop alerts the playbook marks as known-noise (e.g., scanner traffic - from internal vuln management). +1. Group by (source_ip, category) — collapse duplicates into a single record + with a count. +2. Assign priority: P1 (active intrusion), P2 (suspicious), P3 (informational). + The MCP tool's promotion rules are documented in docs/MCP-INTERFACE.md and + are frozen — do not redo this logic in the prompt. Hard rules: - Be decisive. P1 means "wake someone up". Do not over-promote. -- For each P1, include the raw alert evidence so the on-call can verify in <30s. - -Output is JSONL, one line per triaged alert (or alert group): -{ - "ts": "ISO-8601", - "priority": "P1|P2|P3", - "asset": str, - "technique": "T1234" | null, - "summary": str, - "count": int, - "evidence": [str] -} +- For each P1, the tool already includes up to 3 evidence lines so the on-call + can verify in <30s. + +Output is the JSONL stream returned by triage_alerts. """ [agent.limits] -max_tool_calls = 6 +max_tool_calls = 4 max_runtime_s = 60 allow_network = "none" diff --git a/agents/blue/incident-report.toml b/agents/blue/incident-report.toml index e67d178..0515f93 100644 --- a/agents/blue/incident-report.toml +++ b/agents/blue/incident-report.toml @@ -8,6 +8,14 @@ name = "blue-incident-report" role = "defender" description = "Aggregator. Reads triage queue + kill chains, writes executive incident summary." +[mcp] +servers = ["phantom-secops"] + +[[agent.tools]] +name = "compose_incident_report" +server = "phantom-secops" +description = "Render the blue-team-side markdown report. Returns markdown + mttd_seconds." + [[agent.tools]] name = "file_read" description = "Read triage and correlation artifacts." @@ -18,19 +26,24 @@ description = "Write the final markdown incident report." [agent.prompt] system = """ -Produce an incident report following this structure: +Aggregate the run artifacts and call compose_incident_report with: +- triaged: contents of reports/runs//triage-queue.jsonl (one object per line) +- actors: contents of reports/runs//kill-chains.jsonl +- timeline: list of [t_seconds, label] tuples emitted by the orchestrator + +The MCP tool returns the rendered markdown plus mttd_seconds (mean time to +detect — first probe → first triaged alert). Persist the markdown to +reports/runs//incident-report.md. -1. TL;DR — 2 sentences: was anything bad happening, and is it contained? -2. Timeline — first detection, escalation, containment. -3. Affected Assets — list, with criticality. -4. Actor Summary — for each correlated actor: what they did, in plain English. -5. Indicators of Compromise (IoCs) — IPs, paths, payload signatures. -6. Recommended Follow-up — patch this, rotate that, monitor X. +The tool enforces the executive-friendly structure (TL;DR, timeline, actors, +triaged alerts, MTTD). Do not rewrite the structure — it pairs with the +pentest report for side-by-side comparison. -Tone: calm, factual, suitable for a CISO 5pm briefing. Avoid jargon when possible. +Tone (when augmenting): calm, factual, suitable for a CISO 5pm briefing. +Avoid jargon when possible. """ [agent.limits] -max_tool_calls = 8 +max_tool_calls = 6 max_runtime_s = 60 allow_network = "none" diff --git a/agents/blue/log-anomaly.toml b/agents/blue/log-anomaly.toml index e8132e0..e1cf211 100644 --- a/agents/blue/log-anomaly.toml +++ b/agents/blue/log-anomaly.toml @@ -1,52 +1,46 @@ # phantom-mesh agent config — BLUE TEAM / Log Anomaly # # Watches the lab application logs (Juice Shop, DVWA) and emits "alerts" when -# the request stream deviates from a baseline. The output feeds the alert-triage -# agent and ultimately the threat-correlate agent. +# the request stream deviates from a baseline. Output feeds the alert-triage +# agent. [agent] name = "blue-log-anomaly" role = "defender" -description = "Statistical baseline of request patterns; emit alerts on outliers." +description = "Pattern-match request log; emit alerts on injection / scanner / traversal patterns." -[[agent.tools]] -name = "file_read" -description = "Tail reports/lab-logs/*.log." +[mcp] +servers = ["phantom-secops"] [[agent.tools]] -name = "file_write" -description = "Append anomaly alerts to reports/lab-logs/alerts.jsonl." +name = "scan_logs_for_anomalies" +server = "phantom-secops" +description = "URL-decode each log line, pattern-match, emit raw alerts. Read-only." [[agent.tools]] -name = "stats" -description = "Compute simple statistics over a sliding window." +name = "file_write" +description = "Persist raw alerts to reports/runs//alerts.jsonl." [agent.prompt] system = """ You are a defensive analyst agent watching a stream of HTTP access logs from -lab applications. Your job is to surface statistically anomalous request -patterns: +lab applications. Call scan_logs_for_anomalies to surface request patterns +matching: -- A spike in 4xx/5xx from a single source (likely scanner). -- Unusual paths being probed (admin/, wp-admin, .git/, env, phpinfo). - Path-traversal patterns (../, %2e%2e, ..%2f). - SQL-injection-shaped query strings (UNION, OR 1=1, sleep(...)). - XSS payload shapes (", - "first_seen": "ISO-8601", - "last_seen": "ISO-8601", - "phases_observed": ["TA0043", "TA0001", ...], - "alert_ids": [str], - "narrative": "<2-3 sentence summary of what this actor appears to be doing>", - "confidence": "low|medium|high" -} - -Hard rules: -- Don't over-attribute. If a single P3 alert is the only signal, the narrative - should reflect that (low confidence). -- Don't include speculation about identity (no "this looks like APT-X"). +You are an incident-response analyst agent. Call correlate_threats with the +triaged queue. The MCP tool groups alerts by source IP and tags ATT&CK +phases using the frozen mapping in docs/MCP-INTERFACE.md: + +- scanner → TA0043 Reconnaissance +- sqli, xss, traversal → TA0001 Initial Access +- admin_path → TA0007 Discovery + +For each actor record returned, the narrative is already populated. You may +augment it (in plain English, ≤3 sentences) but must not: +- Over-attribute. Low-evidence actors should retain low confidence. +- Speculate about identity (no "this looks like APT-X"). + +Output is one record per actor, matching the correlate_threats return shape. """ [agent.limits] -max_tool_calls = 8 +max_tool_calls = 4 max_runtime_s = 90 allow_network = "none" diff --git a/agents/red/exploit-suggest.toml b/agents/red/exploit-suggest.toml index 3a2e953..9cc55e2 100644 --- a/agents/red/exploit-suggest.toml +++ b/agents/red/exploit-suggest.toml @@ -2,45 +2,51 @@ # # IMPORTANT: This agent produces *prose descriptions* of how a vulnerability # would be exploited. It does NOT generate runnable exploit code, shellcode, -# or weaponized payloads. See ETHICS.md. +# or weaponized payloads. The MCP tool suggest_exploit_prose enforces the +# `has_runnable_poc: false` invariant. See ETHICS.md and docs/MCP-INTERFACE.md. [agent] name = "red-exploit-suggest" role = "attacker" description = "For each finding, describes likely exploitation path in prose. Never generates executable exploits." +[mcp] +servers = ["phantom-secops"] + [[agent.tools]] -name = "file_read" -description = "Read vuln findings JSON." +name = "suggest_exploit_prose" +server = "phantom-secops" +description = "Generate text-only exploit explanations from findings. Output invariant: has_runnable_poc=false." [[agent.tools]] -name = "cve_lookup" -description = "Look up CVE details from a local NVD mirror. Read-only." +name = "file_read" +description = "Read vuln findings JSON." [[agent.tools]] name = "file_write" -description = "Persist suggestions to reports/." +description = "Persist suggestions to reports/runs//exploit-suggestions.md." [agent.prompt] system = """ -You are a red-team analyst. For each vulnerability finding, produce a written -description of: +You are a red-team analyst. For each vulnerability finding, call +suggest_exploit_prose to obtain a prose-only exploitation summary, then +optionally augment it with: 1. What the vulnerability is, in one paragraph. 2. The likely exploitation path, in prose. (Do NOT include runnable code, - shellcode, or exact exploit payloads. Do reference public POCs by URL only.) + shellcode, or exact exploit payloads. Reference public POCs by URL only.) 3. The realistic impact. 4. Recommended mitigation. Hard rules: - No exploit code. No payloads. No shellcode. Prose only. +- The MCP tool's output carries `has_runnable_poc: false` — preserve that + property in your final markdown. Tests assert this invariant. - Reference public POC repositories or Exploit-DB entries by URL if relevant — do not include their contents. - If a finding is informational only (e.g., banner disclosure), say so. - -Output is a markdown report fragment, one section per finding. """ [agent.limits] -max_tool_calls = 8 +max_tool_calls = 6 max_runtime_s = 120 allow_network = "none" diff --git a/agents/red/pentest-report.toml b/agents/red/pentest-report.toml index 7a35fe5..8230cbd 100644 --- a/agents/red/pentest-report.toml +++ b/agents/red/pentest-report.toml @@ -6,11 +6,19 @@ [agent] name = "red-pentest-report" role = "attacker" -description = "Aggregator. Reads all red-team JSON/markdown artifacts and emits the final pentest report." +description = "Aggregator. Reads all red-team artifacts and emits the final pentest report." + +[mcp] +servers = ["phantom-secops"] + +[[agent.tools]] +name = "compose_pentest_report" +server = "phantom-secops" +description = "Render the red-team-side markdown report from recon + vuln + suggestions + timeline." [[agent.tools]] name = "file_read" -description = "Read all artifacts under reports/." +description = "Read all artifacts under reports/runs//." [[agent.tools]] name = "file_write" @@ -18,21 +26,24 @@ description = "Write the final markdown report." [agent.prompt] system = """ -Produce a pentest report following this structure: - -1. Executive Summary (3-5 sentences, non-technical) -2. Scope (lab name, target list, date) -3. Methodology (recon → vuln-scan → analysis pipeline used) -4. Findings (severity-sorted list, each with: id, severity, title, evidence, - exploitation summary, remediation) -5. Timeline (timestamps from recon start to report finish — used by the - side-by-side comparison with the blue-team incident report) -6. Recommendations (prioritized) - -Tone: factual. Avoid sensationalism. Avoid suggesting offensive operational use. +Aggregate the run artifacts and call compose_pentest_report with: +- recon: contents of reports/runs//recon.json +- vuln: contents of reports/runs//vuln-scan.json +- exploit_suggestions_md: contents of reports/runs//exploit-suggestions.md +- timeline: list of [t_seconds, label] tuples emitted by the orchestrator + +The MCP tool returns the rendered markdown. Persist it to +reports/runs//pentest-report.md. + +The tool already enforces a fixed structure (executive summary, recon, +findings table, suggestions, timeline). Do not rewrite the structure — +that would diverge from the side-by-side comparison with the incident report. + +Tone (when augmenting): factual. Avoid sensationalism. Avoid suggesting +offensive operational use. """ [agent.limits] -max_tool_calls = 10 +max_tool_calls = 6 max_runtime_s = 90 allow_network = "none" diff --git a/agents/red/recon.toml b/agents/red/recon.toml index fc98423..2fbfad1 100644 --- a/agents/red/recon.toml +++ b/agents/red/recon.toml @@ -1,30 +1,38 @@ # phantom-mesh agent config — RED TEAM / Recon # # Discovers attack surface for a target running in the secops-lab docker network. -# Outputs structured findings (open ports, service banners, subdomains, headers) -# to be consumed by the vuln-scan agent. +# Outputs structured findings (open ports, service banners) to be consumed by +# the vuln-scan agent. +# +# Tool layer is the phantom-secops MCP server (see docs/MCP-INTERFACE.md). +# The exact format of the MCP reference (`server` field below) is provisional — +# it will be reconciled with phantom-mesh's MCP integration spec once that +# ships (Phase 1–2 crates, expected May–June 2026). Until then, treat this +# TOML as documentation; the runtime path uses scenarios/run_kill_chain.py +# or the MCP server directly via `make mcp-serve`. [agent] name = "red-recon" role = "attacker" -description = "Active and passive reconnaissance. Open ports, service banners, web tech stack, subdomain enum." +description = "Active reconnaissance. Open ports, service banners." -# Tools available. Each maps to a wrapper in tools/. -[[agent.tools]] -name = "nmap_runner" -description = "Run nmap against an in-lab host. Returns parsed open ports + service versions." +[mcp] +servers = ["phantom-secops"] [[agent.tools]] -name = "http_probe" -description = "GET / HEAD a URL inside the lab network. Returns status, headers, body excerpt." +name = "recon_host" +server = "phantom-secops" +description = "Scan an in-lab host with nmap. Returns parsed open ports + service versions. Refuses non-lab targets." [[agent.tools]] -name = "dns_enum" -description = "Resolve in-lab service names. (Subdomain enumeration is OSINT-only — no external lookups in lab mode.)" +name = "lab_status" +server = "phantom-secops" +description = "Verify the lab is up before scanning." +# phantom-mesh built-in (no `server` field). [[agent.tools]] name = "file_write" -description = "Persist findings JSON to reports/ for downstream agents." +description = "Persist findings JSON to reports/runs//recon.json for downstream agents." # System prompt — what the LLM sees as instructions. [agent.prompt] @@ -33,23 +41,27 @@ You are a red-team reconnaissance agent operating inside an isolated security research lab. Your job is to map the attack surface of a target host. Hard rules: -- Only act on hosts inside the docker network 'secops-lab'. +- Only act on hosts inside the docker network 'secops-lab'. The MCP tool + refuses non-lab targets at the protocol layer; do not try to bypass. - Do not attempt to exploit anything during recon. Just enumerate. -- Save all findings as structured JSON to reports/recon--.json. +- Save findings as structured JSON to reports/runs//recon.json. - If a tool errors, report the error in your output, do not retry blindly. -Output schema: +Workflow: +1. Call lab_status to confirm services are running. +2. Call recon_host(target=) to scan. +3. Persist the result via file_write. + +Output schema (matches the recon_host MCP tool's return shape): { "target": "", - "open_ports": [{"port": int, "service": str, "version": str|null}], - "http_endpoints": [{"url": str, "status": int, "tech": [str]}], - "subdomains": [str], - "notes": [str] + "open_ports": [{"port": int, "protocol": str, "service": str, "version": str|null}], + "scan_type": "nmap" } """ # Cost / safety limits. [agent.limits] -max_tool_calls = 12 +max_tool_calls = 8 max_runtime_s = 180 -allow_network = "lab-only" # enforced by docker network attachment, not by the agent +allow_network = "lab-only" # enforced by docker network attachment + MCP gate diff --git a/agents/red/vuln-scan.toml b/agents/red/vuln-scan.toml index 5bb66d0..03effbc 100644 --- a/agents/red/vuln-scan.toml +++ b/agents/red/vuln-scan.toml @@ -1,24 +1,24 @@ # phantom-mesh agent config — RED TEAM / Vuln Scan # -# Consumes recon findings, runs Nuclei templates and Nikto against in-scope -# endpoints, and outputs a list of likely vulnerabilities. +# Consumes recon findings, runs Nuclei against in-scope endpoints, and outputs +# a list of likely vulnerabilities. See docs/MCP-INTERFACE.md for the tool layer. [agent] name = "red-vuln-scan" role = "attacker" -description = "Active vulnerability scanning. Nuclei + Nikto. Reads recon JSON, writes findings JSON." +description = "Active vulnerability scanning via nuclei. Reads recon JSON, writes findings JSON." -[[agent.tools]] -name = "nuclei_runner" -description = "Run nuclei with the public templates against an in-lab URL. Returns matched template IDs + severity." +[mcp] +servers = ["phantom-secops"] [[agent.tools]] -name = "nikto_runner" -description = "Run nikto against an in-lab web service. Returns parsed findings." +name = "vuln_scan_web" +server = "phantom-secops" +description = "Run nuclei with public templates against an in-lab URL. Returns matched template IDs + severity. Refuses non-lab URLs." [[agent.tools]] name = "file_read" -description = "Read recon JSON from reports/." +description = "Read recon JSON from reports/runs//recon.json." [[agent.tools]] name = "file_write" @@ -27,26 +27,25 @@ description = "Persist vuln findings JSON for the exploit-suggester agent." [agent.prompt] system = """ You are a red-team vulnerability scanning agent. Read the recon JSON identified -by the orchestrator, choose appropriate scanners for each open service, run -them, and write a consolidated findings file. +by the orchestrator, choose appropriate scan inputs for each open service, run +the vuln_scan_web MCP tool, and write a consolidated findings file. Hard rules: - Only scan hosts identified by the recon agent (already lab-confirmed). - For HTTPS, accept self-signed certs (these are lab targets). -- Throttle: do not run more than 2 scanners concurrently against the same target. - For each finding, record: cve_id (if any), title, severity, evidence, raw tool output snippet. -Output schema: +Output schema (matches vuln_scan_web's return shape): { "target": "", "findings": [ { - "id": "", + "id": "", "cve": "CVE-YYYY-NNNNN" | null, "severity": "info|low|medium|high|critical", "title": str, "evidence": str, - "tool": "nuclei|nikto", + "tool": "nuclei", "raw": str } ] @@ -54,6 +53,6 @@ Output schema: """ [agent.limits] -max_tool_calls = 16 +max_tool_calls = 12 max_runtime_s = 300 allow_network = "lab-only" diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index b25e755..76eee7d 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -3,30 +3,35 @@ ## Layers ``` +ORCHESTRATORS (interchangeable) +┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ +│ Python │ │ Claude Code │ │ phantom-mesh │ │ OpenAI / etc │ +│ run_kill_ │ │ subagent │ │ workflow │ │ via MCP │ +│ chain.py │ │ (.claude/) │ │ (TOML) │ │ │ +└──────┬───────┘ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ + │ direct │ MCP │ MCP │ MCP + │ Python call │ stdio │ stdio │ stdio/http + └────────┬───────┴────────────────┴────────────────┘ + ▼ ┌────────────────────────────────────────────────────────────────────┐ -│ phantom-mesh runtime │ -│ ───────────────────────────────────────────────────────────────── │ -│ - LLM provider routing (multi-provider fallback) │ -│ - Tool calling loop (TOML-defined tools) │ -│ - Cost tracking │ -│ - Inter-agent message passing │ +│ MCP server: phantom-secops (docs/MCP-INTERFACE.md) │ +│ ─────────────────────────────────────────────────────────────── │ +│ 11 tools (recon_host, vuln_scan_web, scan_logs_for_anomalies, │ +│ triage_alerts, correlate_threats, suggest_exploit_prose,│ +│ compose_pentest_report, compose_incident_report, │ +│ lab_status, lab_up, lab_down) │ +│ 2 resource schemes (phantom-secops://runs/… and …/mocks/…) │ └─────────────┬──────────────────────────────────┬───────────────────┘ │ │ - ┌─────▼────────┐ ┌──────▼───────┐ - │ RED agents │ │ BLUE agents │ - │ (TOML-cfgd) │ │ (TOML-cfgd) │ - └─────┬────────┘ └──────┬───────┘ - │ │ - ┌─────▼────────┐ ┌──────▼───────┐ - │ Tool wrappers│ │ Tool wrappers│ - │ (Python, │ │ (Python, │ - │ call into │ │ read logs / │ - │ attacker │ │ emit alerts)│ - │ container) │ │ │ - └─────┬────────┘ └──────┬───────┘ - │ │ - │ (docker exec into attacker) │ (docker socket → log volume) ▼ ▼ +┌─────────────────────────────────────┐ ┌─────────────────────────────┐ +│ phantom_secops/core.py │ │ phantom_secops/mcp/safety.py│ +│ Pure functions: red+blue pipeline │ │ Lab gate, prose validator │ +│ Templates → optional LLM provider │ │ Single source of truth for │ +│ (phantom_secops/llm/) │ │ "is this allowed" │ +└─────────────┬───────────────────────┘ └─────────────────────────────┘ + │ tools/{nmap,nuclei}_runner.py + ▼ (docker exec into attacker container) ┌────────────────────────────────────────────────────────────────────┐ │ secops-lab docker network │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ @@ -38,6 +43,14 @@ └────────────────────────────────────────────────────────────────────┘ ``` +## Why MCP first + +The earlier design wired `run_kill_chain.py` directly to phantom-mesh's HTTP API. That had two problems: phantom-mesh's binary is closed-source until June 2026 (so we'd commit to their schedule), and every additional runtime — Cursor, OpenAI Agents, Continue — would need its own bespoke adapter. + +MCP is supported by Anthropic, OpenAI, Cursor, Continue, and is on phantom-mesh's roadmap. Writing the tool layer once as an MCP server gives runtime independence: phantom-mesh becomes one client among many. The cost — losing phantom-mesh's cross-provider cost tracking out of the box — is acceptable for a research playground; an MCP server can add lightweight token-usage logging later if needed. + +Defense-in-depth follows naturally from the layering: every active tool defers to `phantom_secops/mcp/safety.py` for lab-target validation, so a misbehaving LLM, a stale TOML, or a buggy adapter can't bypass the gate by going around the MCP boundary. + ## Why phantom-mesh The runtime gives us: diff --git a/docs/INTEGRATIONS.md b/docs/INTEGRATIONS.md new file mode 100644 index 0000000..8bb67a0 --- /dev/null +++ b/docs/INTEGRATIONS.md @@ -0,0 +1,169 @@ +# Integrations + +phantom-secops is designed to be **runtime-agnostic**: the actual logic lives in `phantom_secops/core.py` and is exposed through the MCP server in `phantom_secops/mcp/server.py`. Anything that speaks MCP can drive the same kill-chain that `make demo` drives. + +This document tracks every supported integration, its current state, and the minimal config needed to use it. + +## Status overview + +| Adapter | State | Driving file | +|---|---|---| +| Python reference (`make demo`) | ✅ Stable | `scenarios/run_kill_chain.py` | +| MCP stdio server | ✅ Stable | `phantom_secops/mcp/server.py` | +| Claude Code | ✅ Stable | `.mcp.json` + `.claude/agents/secops-runner.md` | +| phantom-mesh TOML | 🟡 Documented; runtime pending | `agents/{red,blue}/*.toml` | +| Cursor / Continue | 🟡 Compatible via MCP; not actively tested | (config below) | +| OpenAI Agents SDK | 🟡 Compatible via MCP; not actively tested | (config below) | +| LangGraph | 🟡 Compatible via MCP; not actively tested | (config below) | + +✅ = working today. 🟡 = should work but the integration is documentation-only and not part of CI. + +## Why MCP, not bespoke per-runtime adapters + +Three failure modes pushed us here: + +1. The original plan was to call phantom-mesh directly from `run_kill_chain.py`. phantom-mesh's HTTP API isn't published yet (binary closed-source until June 2026), so committing to that schedule would block everything else. +2. Without a stable protocol, every new runtime (Cursor, OpenAI Agents, etc.) needs its own adapter. That's `O(N)` work per tool change. +3. MCP is supported by Anthropic, OpenAI, Cursor, Continue, and on phantom-mesh's roadmap. One server, many clients. + +The cost: we don't get phantom-mesh's cross-provider cost tracking out of the box. That's an acceptable loss — see `docs/ARCHITECTURE.md` for the tradeoff. + +## 1. Python reference (deterministic, CI-safe) + +```bash +make demo-mock # canned data, ~1 second, no docker, no API key +make demo # against the live lab (requires `make lab-up` first) +``` + +This path bypasses MCP entirely and calls `phantom_secops.core.*` directly. It's the reference implementation for what every other adapter should produce. CI uses this lane. + +## 2. MCP stdio server (for any MCP client) + +```bash +make mcp-serve # python3 -m phantom_secops.mcp.server, stdio transport +``` + +The server registers 11 tools and 2 resource schemes — see `docs/MCP-INTERFACE.md` for the frozen contract. To inspect the surface interactively: + +```bash +make mcp-dev # opens the MCP inspector (requires `mcp[cli]`) +``` + +## 3. Claude Code + +The repo ships an `.mcp.json` so Claude Code picks up the server automatically when opened in this working directory. + +```json +{ + "mcpServers": { + "phantom-secops": { + "command": "python3", + "args": ["-m", "phantom_secops.mcp.server"], + "env": {"PYTHONPATH": "${workspaceFolder}"} + } + } +} +``` + +The repo also ships a project-scoped subagent at `.claude/agents/secops-runner.md`. To drive a full kill-chain inside Claude Code: + +``` +> use the secops-runner subagent to run a kill-chain against juice-shop +``` + +The subagent enforces the same lab-target gate, never invents exploit payloads, and refuses lifecycle operations without explicit confirmation — these are properties of the MCP layer, not the prompt. See the subagent file for its workflow. + +## 4. phantom-mesh + +The agent configs in `agents/red/*.toml` and `agents/blue/*.toml` reference the MCP server via: + +```toml +[mcp] +servers = ["phantom-secops"] + +[[agent.tools]] +name = "recon_host" +server = "phantom-secops" +description = "..." +``` + +**Important caveat.** The exact format of MCP references in phantom-mesh TOML is **provisional**. phantom-mesh's `phantom-tools` crate (Phase 1 of their public source release) is expected mid-May 2026, and the runtime crate (Phase 2, late May 2026) will pin the syntax for MCP server references. When that lands, this section and all eight TOMLs may need a small migration. + +Until then, treat `agents/**/*.toml` as documentation: they describe what each agent should do and what tools it should call, but the runtime path through these configs hasn't been wired up. The Python reference orchestrator and the MCP server are the runnable surfaces today. + +## 5. Cursor + +Cursor reads `.cursor/mcp.json` (project-level) or `~/.cursor/mcp.json` (user-level). Use the same shape as `.mcp.json`: + +```json +{ + "mcpServers": { + "phantom-secops": { + "command": "python3", + "args": ["-m", "phantom_secops.mcp.server"] + } + } +} +``` + +Then in Composer, ask "scan juice-shop for vulnerabilities and produce a pentest report" — Cursor will discover the 11 tools. + +## 6. Continue + +Add to `~/.continue/config.yaml`: + +```yaml +mcpServers: + - name: phantom-secops + command: python3 + args: ["-m", "phantom_secops.mcp.server"] +``` + +## 7. OpenAI Agents SDK + +The OpenAI Agents SDK supports MCP servers as tool sources. Minimal example: + +```python +from agents import Agent +from agents.mcp import MCPServerStdio + +server = MCPServerStdio( + params={"command": "python3", "args": ["-m", "phantom_secops.mcp.server"]}, +) + +agent = Agent( + name="secops-runner", + instructions="(see .claude/agents/secops-runner.md for the full prompt)", + mcp_servers=[server], +) +``` + +The same hard rules from the Claude Code subagent apply — that prompt is portable. + +## 8. LangGraph + +Use `langchain-mcp-adapters` to wrap the server: + +```python +from langchain_mcp_adapters.client import MultiServerMCPClient + +client = MultiServerMCPClient({ + "phantom-secops": { + "command": "python3", + "args": ["-m", "phantom_secops.mcp.server"], + "transport": "stdio", + } +}) +tools = await client.get_tools() +# pass `tools` to your LangGraph node as usual. +``` + +--- + +## Adding a new adapter + +1. Don't write a new server. Use the MCP one. +2. Reuse the prompt at `.claude/agents/secops-runner.md` — it's intentionally MCP-tool-name-driven, not Claude-Code-specific. The hard rules and workflow port directly. +3. If your runtime needs a different transport (HTTP/SSE rather than stdio), the FastMCP server in `phantom_secops/mcp/server.py` supports both — pass `--transport=streamable-http` to switch. +4. Add a row to the status table at the top of this file. +5. If your runtime uncovers a bug or a missing piece in the MCP interface, fix it in `docs/MCP-INTERFACE.md` first (frozen-contract change → SemVer bump), then in the server, then in every adapter. The four-place migration is unavoidable but rare. diff --git a/docs/INTERVIEW-TALK-TRACK.md b/docs/INTERVIEW-TALK-TRACK.md index 04e375d..2f0a1a1 100644 --- a/docs/INTERVIEW-TALK-TRACK.md +++ b/docs/INTERVIEW-TALK-TRACK.md @@ -5,15 +5,17 @@ Trend Micro, CrowdStrike, Palo Alto, etc. ## Elevator pitch (30 seconds) -> "I built a multi-agent platform on top of my own AI agent runtime that runs -> red and blue team workflows in parallel against an isolated lab. The -> attack side does recon → vuln-scan → POC suggestion → pentest report. -> The defense side does log anomaly → triage → correlation → incident report. -> The interesting bit is the side-by-side comparison: I can quantify -> mean-time-to-detect against a known attack pattern, which is the metric -> SOCs actually care about. It's not a production tool. It's a research +> "I built a runtime-agnostic SecOps research platform. Eleven tools — recon, +> vuln-scan, log triage, correlation, report composition — are exposed as an +> MCP server with a frozen contract and a centralised safety gate. The same +> server is driven today by a Python orchestrator, by Claude Code via a +> project subagent, and by phantom-mesh agents through TOML configs. Red and +> blue pipelines run in parallel against an isolated lab and produce +> side-by-side reports, so you can quantify mean-time-to-detect against a +> known attack pattern. It's not a production tool — it's a research > playground that demonstrates how XDR-style multi-source correlation maps -> cleanly onto a multi-agent architecture." +> cleanly onto a multi-agent architecture, *without* coupling the workflow +> to any single agent runtime." ## Likely questions @@ -23,15 +25,32 @@ Short answer: yes, with a caveat. All targets are intentionally vulnerable applications maintained for security education (OWASP Juice Shop, DVWA, Metasploitable). All tools are widely deployed defensive research tools (Nmap, Nuclei, Nikto). The lab runs on an isolated docker network with no -host port exposure by default. The exploit-suggester agent only produces -prose, not runnable code. See ETHICS.md for full scoping. +host port exposure by default. The exploit-suggester tool only produces +prose — there's a `has_runnable_poc: false` invariant on its output that's +asserted by the test suite and re-validated against any LLM-augmented prose +before it ships. See ETHICS.md for full scoping. ### "What's the value over a single LLM agent that does it all?" Three things. **Context window** — splitting phases keeps each agent's prompt focused. **Cost/latency tuning** — smaller model for prose-heavy steps, larger -for tool-heavy steps. **Operational mapping** — real SOCs and red teams already -split work across roles, so the architecture mirrors how the work is done. +for tool-heavy steps. **Operational mapping** — real SOCs and red teams +already split work across roles, so the architecture mirrors how the work is +done. + +### "Why MCP as the foundation, not phantom-mesh directly?" + +Two practical reasons. (1) phantom-mesh's binary is closed-source until June +2026, so committing to their HTTP API now would block on their release +schedule. (2) Even once it ships, a tools-as-MCP-server design lets phantom- +mesh, Claude Code, Cursor, OpenAI Agents, Continue, and LangGraph all drive +the same workflow without per-runtime adapters. Writing the SecOps logic once +and getting six runtimes for free is a clear win for a research playground +that's also a demo target for interviews. + +The cost: losing phantom-mesh's cross-provider cost tracking out of the box. +For this scope, that's acceptable. Token-usage logging can live in the MCP +server if needed. ### "How does this differ from MSF / Cobalt Strike / Burp Suite Pro?" @@ -41,16 +60,31 @@ exploits — it routes between standard scanners, parses their output, and composes reports. Think "GitHub Actions for security workflows, but agents write the steps." -### "Why phantom-mesh and not LangChain / AutoGen / CrewAI?" +### "What's the safety story for the LLM-augmented path?" + +Three layers. + +1. **Tool-name level**: the prose generator is called `suggest_exploit_prose` + — the suffix makes the constraint visible to every caller. +2. **Output invariant**: every call returns `has_runnable_poc: false`. + `tests/test_no_runnable_poc.py` asserts this for the deterministic path, + and the LLM path validates the *generated* text against the same + forbidden-pattern set (`safety.is_safe_prose`) before merging it in. +3. **Fallback**: if the validator rejects the LLM output, or the provider is + unreachable, the call silently falls back to a deterministic template. + The pipeline never blocks on a failed LLM call. -Honest answer: I built phantom-mesh because the existing frameworks have -deployment friction I didn't want — Python runtime requirements, single-host -designs, opinionated about LLM providers. Phantom-mesh ships as a single Rust -binary, runs cross-platform (Mac, Linux, Windows, Android, iOS), supports -provider fallback out of the box, and uses TOML configs that are diff-friendly. -For a security context, the single-binary delivery is genuinely useful — -analysts can ship the runtime to an air-gapped lab without dragging in a -Python ecosystem. +Tests cover a malicious provider that tries to inject a curl command — the +output gets dropped and the markdown stays clean. + +### "Where's the lab-target gate enforced?" + +Centralised in `phantom_secops/mcp/safety.py`. Both the MCP boundary +(`recon_host`, `vuln_scan_web` refuse non-lab inputs and return +`error: not_a_lab_target`) **and** the legacy tool wrappers (`tools/nmap_runner.py`) +import the same `is_lab_service()` function. Defense-in-depth: a bad +TOML, a misbehaving LLM, or a direct call to the wrapper all hit the same +list. Six unit tests in `tests/test_safety.py` lock the whitelist. ### "What's the false-positive rate of the alert-triage agent?" @@ -62,24 +96,29 @@ claiming a real number. ### "How does this scale?" -The agents are stateless between handoffs (state lives on the file system). -You could run multiple lab instances on a single host, or shard across a -cluster — phantom-mesh already supports distributed execution via its mesh -feature. I haven't tested that for security workloads yet. +Agents are stateless between handoffs (state lives on the file system as run +artifacts under `reports/runs//`, addressable through MCP resources at +`phantom-secops://runs//`). You could run multiple lab instances +on a single host, or shard across a cluster. Once phantom-mesh ships its +distributed execution layer (Phase 3 — early June 2026), the same agent +TOMLs can run across the mesh. ### "Walk me through the kill-chain demo." -Use the timeline in `scenarios/full-kill-chain.md`. Key milestones to call -out as you walk through: +Three paths to demo. Pick whichever fits the conversation: + +**Mock mode** (no docker, deterministic): `make demo-mock` — finishes in +under a second. Shows the structure: 21 raw alerts → 5 triaged groups → 1 +correlated actor. + +**Claude Code path**: open the repo in Claude Code, ask the `secops-runner` +subagent to run a kill-chain. Same 11 MCP tools, but you get to *see* the +agent reasoning over the artifacts in real time. Good for interviewers who +want to see agent UX. -1. t+0: red recon starts, blue log-anomaly starts. -2. t+10s: red has nmap output, blue has its first scanner alerts. -3. t+15s: red kicks off Nuclei. Blue triage promotes scanner activity to P2. -4. t+45s: red has vuln findings. Blue threat-correlate links the recon and - scan alerts to a single actor. -5. t+60s: both reports finalize. The blue-team incident report names the - attacker's source IP, lists the techniques used, and lists IoCs. The red-team - pentest report lists the vulns found and mitigation guidance. +**Live lab**: `make lab-up && make demo`. Full Nmap → Nuclei chain against +Juice Shop. Slower (~60s) but the artifacts include real scan output. Good +for interviewers who want to see actual tool integration. The point is that **detection lag is small when the analysis pipeline runs concurrently with the attack** — which is what real SOC tooling tries to do. @@ -88,25 +127,32 @@ concurrently with the attack** — which is what real SOC tooling tries to do. In priority order: -1. Real alert dataset replay. Use a public CTF dataset (CTF-d archives, MISP - feeds) to validate the triage agent's calibration. -2. Containment actions. Right now the blue side observes and reports. Next - step is enabling guarded response actions (block IP, isolate container) - with human-in-the-loop approval. -3. Multi-host correlation. Run the same demo against a 3-host lab where the - actor pivots between hosts, see if threat-correlate stitches the chain. +1. **Real alert dataset replay.** Use a public CTF dataset (CTF-d archives, + MISP feeds) to validate the triage agent's calibration. +2. **Containment actions.** Right now the blue side observes and reports. + Next step is enabling guarded response actions (block IP, isolate + container) with human-in-the-loop approval — those become new MCP tools + under a `lifecycle` safety class. +3. **Multi-host correlation.** Run the same demo against a 3-host lab where + the actor pivots between hosts; check whether `correlate_threats` + stitches the chain end-to-end. +4. **Real phantom-mesh runtime.** Once `phantom-tools` (mid May) and + `phantom-runtime` (late May) ship, wire the TOML configs to the live + runtime and add a phantom-mesh CI lane. ### "How do you keep the LLM from hallucinating CVE numbers?" Two checks. The exploit-suggester only references CVEs that appear in the -vuln-scan agent's output — it can't pull a CVE out of thin air. Beyond that, -the cve_lookup tool reads from a local NVD mirror, so even if the LLM names a -CVE, the prose has to be grounded in NVD's actual record. If the LLM names a -CVE that doesn't exist in the mirror, the report flags it as "unverified". +vuln-scan tool's output — it can't pull a CVE out of thin air. Beyond that, +the prose validator catches any output containing executable shell content, +and the deterministic fallback path is grounded in `vuln-scan-juice-shop.json` +(or live nuclei output). If the LLM names a CVE in the prose, that CVE is +already in the source data — otherwise the fallback kicks in. ## Don't say - "This finds 0-days" (it doesn't, and the claim is a red flag). -- "This is better than [commercial product]" (it isn't — it's a research demo). +- "This is better than [commercial product]" (it isn't — it's a research + demo). - "I built this in a weekend" (the framework took months — say that). - Any claim about real-world adversaries (you have no telemetry to back it). diff --git a/docs/L2-INTEGRATION-PLAN.md b/docs/L2-INTEGRATION-PLAN.md new file mode 100644 index 0000000..1ce1d84 --- /dev/null +++ b/docs/L2-INTEGRATION-PLAN.md @@ -0,0 +1,183 @@ +# L2 Integration Plan: phantom-secops × phantom-mesh + +**Status:** drafted 2026-05-10, target completion 5/14 evening + 5/15 evening (~5h total). + +**Goal:** make phantom-secops's red/blue agents run on phantom-mesh's runtime via MCP, while preserving the deterministic `make demo-mock` output (recruiters can clone + run with no API keys). + +--- + +## Current state + +- Existing 10-tool MCP server lives at `phantom_secops/mcp/server.py` (FastMCP). **Keep this** as the rich interface for direct MCP clients (Claude Code, Cursor). +- `make demo-mock` runs `python3 scenarios/run_kill_chain.py --target juice-shop --mock`, calling `phantom_secops.core.{run_recon, run_vuln_scan, ...}` directly. No LLM in mock mode. +- Mock fixtures live in `lab/mocks/*.json`. +- Existing red/blue agent TOMLs at `agents/{red,blue}/*.toml` are documentation, not orchestrator-driven. +- LLM abstraction at `phantom_secops/llm/` has 4 providers; `phantom_mesh_provider.py` (HTTP-against-`phantom serve`) becomes redundant after this work — mark `# DEPRECATED` in this PR, delete next release. + +--- + +## Architecture + +``` +make demo-mock-mesh + ↓ +scenarios/run_kill_chain.py --driver=mesh --mock + ↓ (per turn: red recon, exploit, blue detect, respond) +subprocess.run(["phantom", "repl", "--agent", "red_team", + "-c", "Run recon and call secops_recon, then stop."], + env={SECOPS_MCP_MOCK=1, SECOPS_MCP_STATE_FILE=/tmp/state.json}) + ↓ +phantom-mesh agent loop (config: ./agents.toml.demo) + ↓ tool dispatch via [[mcp_servers]] config +secops_mcp.server (stdio MCP, 4 tools) + ↓ delegates to phantom_secops.core + reads/writes +state.json (single source of truth between turns) +``` + +State exchange via JSON file — NOT stdout parsing — because `phantom repl -c` stdout +has ANSI + cost lines that are fragile to parse. + +--- + +## File-level changes + +### New files + +| Path | Purpose | +|---|---| +| `secops_mcp/__init__.py` | Package marker; re-exports `main` | +| `secops_mcp/server.py` | FastMCP stdio server, **4 tools only**: `recon`, `exploit`, `detect`, `respond` | +| `secops_mcp/state.py` | `load_state(path) → dict`, `save_state(path, dict)`, `default_state()` | +| `secops_mcp/determinism.py` | Reads `SECOPS_MCP_MOCK`, `SECOPS_MCP_STATE_FILE`; thin layer for canned data | +| `agents.toml.demo` | Complete phantom-mesh config for one-command demos (project-local) | +| `agents.toml.snippet` | Paste-in fragment for users' existing phantom-mesh configs | +| `docs/L2-INTEGRATION.md` | User-facing guide: install snippet, run demo through phantom-mesh | +| `tests/test_secops_mcp_tools.py` | Unit tests for 4 tools, deterministic output assertions | +| `tests/test_demo_mock_parity.py` | Golden-file diff: legacy demo-mock vs mesh demo-mock | + +### Modified files + +| Path | Change | +|---|---| +| `Makefile` | Add `secops-mcp-serve` + `demo-mock-mesh`; **leave `demo-mock` unchanged** for parity | +| `scenarios/run_kill_chain.py` | Add `--driver={direct,mesh}` flag (default `direct`); when `mesh`, replace `core.X(...)` calls with `subprocess.run(["phantom", "repl", "--agent", X, "-c", ...])` | +| `README.md` | Add "Drive via phantom-mesh" section pointing at snippet | + +### Deprecated (keep in v1, remove next release) + +| Path | Action | +|---|---| +| `phantom_secops/llm/phantom_mesh_provider.py` | Add `# DEPRECATED — use secops_mcp + phantom-mesh [[mcp_servers]]` | + +--- + +## 4 MCP tools (the L2 surface) + +Each tool sets `mock=True` when `SECOPS_MCP_MOCK=1` and reads/writes state via `SECOPS_MCP_STATE_FILE`. + +```python +# Sketch — actual signatures match phantom_secops.core +@mcp.tool +def recon(target: str) -> dict: + """Sweep ports + service detection. Wraps core.run_recon.""" + state = load_state() + state["recon"] = core.run_recon(target, mock=is_mock()) + save_state(state) + return {"open_ports": ..., "services": ..., "state_version": state["version"]} + +@mcp.tool +def exploit(findings_id: str | None = None) -> dict: + """Wraps core.run_vuln_scan + core.suggest_exploit_prose. + Safety invariant: has_runnable_poc is ALWAYS False.""" + ... + +@mcp.tool +def detect(source: str = "mock") -> dict: + """Composite: scan_logs_for_anomalies + triage_alerts + correlate_threats.""" + ... + +@mcp.tool +def respond(actors_id: str | None = None) -> dict: + """Wraps core.compose_incident_report + core.compose_pentest_report.""" + ... +``` + +--- + +## `agents.toml.snippet` (paste at end of user's config) + +```toml +[[mcp_servers]] +name = "secops" +command = "python3" +args = ["-m", "secops_mcp.server"] +env = { SECOPS_MCP_MOCK = "1", SECOPS_MCP_STATE_FILE = "/tmp/secops_state.json" } + +[agent.red_team] +provider = "anthropic" +model = "claude-sonnet-4-6" +tools = ["secops_recon", "secops_exploit", "file_write"] +instructions = """ +You are a red-team operator inside an isolated security research lab. +Workflow: call secops_recon(target), then secops_exploit(). Persist artifacts. +Hard rules: only the configured lab targets; never produce runnable PoCs. +""" + +[agent.blue_team] +provider = "anthropic" +model = "claude-sonnet-4-6" +tools = ["secops_detect", "secops_respond", "file_write"] +instructions = """ +You are a SOC analyst. Call secops_detect(), then secops_respond() to draft +the incident report. Be decisive — P1 means wake the on-call. +""" +``` + +Tool names follow phantom-mesh's `_` convention (declared `name = "secops"` → `secops_recon`). + +--- + +## Risks + mitigations + +| Risk | Mitigation | +|---|---| +| **LLM non-determinism** in `phantom repl` (biggest risk) | (a) Tight scripted prompts: "Call X, then stop." minimizes variance. (b) Parity test compares semantic fields (port counts, MTTD seconds, key strings), NOT byte-exact diff | +| `phantom` not on PATH | Orchestrator checks `PHANTOM_BIN` env first, falls back to `shutil.which("phantom")`, errors clearly if neither | +| `mcp` Python package missing | Mirror existing `phantom_secops/mcp/server.py` ImportError guard with install hint | +| `agents.toml` location | Ship `agents.toml.demo` as a complete file (not just snippet); orchestrator passes `--config ./agents.toml.demo` | +| Tool name prefixing untested | Probe with `phantom repl --agent red_team -c "list your tools"` before finalizing snippet | +| Schema drift between direct + mesh paths | Single `state.py` schema-validation function called by both | + +--- + +## Test strategy + +**Goal:** `make demo-mock` (direct path, unchanged) and `make demo-mock-mesh` (new mesh path) produce equivalent output. + +`tests/test_demo_mock_parity.py`: +1. Run both makes, output to `/tmp/legacy/` and `/tmp/mesh/`. +2. Pure-function output files (`recon.json`, `vuln-scan.json`, `alerts.jsonl`, `triage-queue.jsonl`, `kill-chains.jsonl`): **byte-for-byte equality required**. +3. Generated reports (`pentest-report.md`, `incident-report.md`): strip `[t+…s]` timestamps + `agent_name` byline, then assert remaining content matches via `difflib`. +4. CI gate: must pass before merge. + +Manual check: +```bash +diff -r /tmp/legacy /tmp/mesh \ + --ignore-matching-lines='^\[t+.*s\]' \ + --ignore-matching-lines='^_Generated by.*$' +``` + +Plus port `tests/test_no_runnable_poc.py` invariant onto `secops_mcp.server.exploit` output — safety invariant must survive the wrapper layer. + +--- + +## Execution order (when you sit down to do this) + +1. (30 min) Create `secops_mcp/` skeleton: `__init__.py`, `state.py`, `determinism.py`. Write 4 stub tools that just touch the state file and return canned data. +2. (1 h) Copy mock data flow from `phantom_secops/mcp/server.py` into `secops_mcp/server.py`'s 4 tools. Run `python -m secops_mcp.server` standalone to confirm it starts. +3. (30 min) Write `agents.toml.demo`. Run `phantom repl --config ./agents.toml.demo --agent red_team -c "list your tools"`; confirm `secops_recon`, `secops_exploit` show up. +4. (1.5 h) Refactor `scenarios/run_kill_chain.py` with `--driver=mesh`. Drive 4 turns via subprocess + state file. +5. (1 h) Write `tests/test_demo_mock_parity.py`. Iterate on prompt instructions until parity passes. +6. (30 min) Update `Makefile`, `README.md`, deprecate `phantom_mesh_provider.py`. + +Total: ~5h. Two evenings (5/14 + 5/15) if you have 2-3h focused blocks. diff --git a/docs/MCP-INTERFACE.md b/docs/MCP-INTERFACE.md new file mode 100644 index 0000000..4bac067 --- /dev/null +++ b/docs/MCP-INTERFACE.md @@ -0,0 +1,348 @@ +# MCP Interface — phantom-secops + +> Frozen contract. The MCP server, phantom-mesh adapter, Claude Code subagent, and the Python reference orchestrator all depend on the names, schemas, and safety gates documented here. Changes to anything below are breaking and require updating all four call sites in lockstep. +> +> Surface: **11 tools, 2 resource schemes**. + +## Server identity + +| Field | Value | +|---|---| +| MCP server name | `phantom-secops` | +| Tools / Resources | 11 / 2 | +| Transport | `stdio` (primary) and `http` (optional, for remote agents) | +| Protocol version | MCP 2025-06-18 | +| Required runtime | Python ≥3.11, Docker (only for `active_in_lab` and `lifecycle` tools) | + +## Naming convention + +`{verb}_{object}[_{qualifier}]`, snake_case, all lowercase. The qualifier is mandatory when the verb has multiple safety profiles (e.g. `lab_up_confirm`). + +The 11 tools below are grouped by **safety class**, not by red/blue. Mixing red/blue in the same server is intentional — agents shouldn't have to know which "side" a tool belongs to; they just call it. + +--- + +## Safety classes + +| Class | Means | Tools require user/agent confirmation? | +|---|---|---| +| `read_only` | No network egress, no filesystem writes outside `reports/runs//` | No | +| `active_in_lab` | Probes a target inside the `secops-lab` docker network | No (gated by lab-network check) | +| `lifecycle` | Brings up/tears down the docker lab | **Yes** — must pass `confirm: true` | + +Every `active_in_lab` tool **must** call `safety.assert_lab_target(target)` before doing anything. The check is centralised in `mcp/safety.py` and validates against the hard-coded list `{juice-shop, dvwa, dvwa-db, metasploitable, attacker}`. Any other value returns an `ErrorOutput` with `code="not_a_lab_target"`. + +--- + +## Tool catalogue + +### 1. `recon_host` — `active_in_lab` + +Scans an in-lab host with nmap (top 1000 ports + service version). Wraps `tools/nmap_runner.py`. + +```ts +input: { + target: "juice-shop" | "dvwa" | "dvwa-db" | "metasploitable" | "attacker", + ports?: "top-1000" | string, // default "top-1000"; explicit list e.g. "80,443,3306" + scan_type?: string, // default "-sV" +} + +output: { + target: string, + open_ports: Array<{ + port: number, + protocol: string, // "tcp" | "udp" + service: string, // "http", "mysql", ... + version: string | null, // "Apache 2.4.41" or null + }>, + scan_type: "nmap", +} + +// or, on error: +output: { error: string, target?: string, lab_services?: string[] } +``` + +**Side effects**: shells `docker exec` into `secops-attacker`. No filesystem writes. +**Latency budget**: 120 s timeout enforced inside the wrapper. + +--- + +### 2. `vuln_scan_web` — `active_in_lab` + +Runs nuclei against an in-lab HTTP target. Wraps `tools/nuclei_runner.py`. + +```ts +input: { + target_url: string, // must contain a lab service hostname + severity?: string, // CSV; default "low,medium,high,critical" + timeout_s?: number, // default 90 +} + +output: { + target: string, + findings: Array<{ + id: string | null, // nuclei template-id + cve: string | null, + severity: "info" | "low" | "medium" | "high" | "critical" | null, + title: string | null, + evidence: string | null, // matched-at URL + tool: "nuclei", + raw: string, // truncated raw JSON, ≤400 chars + }>, +} +``` + +**Side effects**: shells `docker exec` into `secops-attacker`; on first run installs nuclei via `go install`. +**Latency budget**: `timeout_s + 30` s. + +--- + +### 3. `scan_logs_for_anomalies` — `read_only` + +Pattern-matches access logs to produce raw alerts. Logic from `_blue_log_anomaly` in `run_kill_chain.py:174`. + +```ts +input: { + source?: "lab_logs" | "mock", // default "lab_logs"; "mock" reads lab/mocks/attack-log.txt + log_path?: string, // override; absolute path inside repo +} + +output: { + alerts: Array<{ + ts: string, // ISO8601 UTC + source_ip: string, // IPv4 or "unknown" + asset: string, // "juice-shop" | "dvwa" | ... + category: "traversal" | "sqli" | "xss" | "admin_path" | "scanner", + evidence: string, // raw log line, ≤200 chars + severity_hint: "low" | "medium" | "high", + }>, + source: string, // resolved log file path +} +``` + +**Side effects**: none. URL-decodes each line before pattern-matching (the existing implementation does this). + +--- + +### 4. `triage_alerts` — `read_only` + +Groups raw alerts by `(source_ip, category)` and assigns priority. Logic from `_blue_alert_triage`. + +```ts +input: { + alerts: Array, // shape from scan_logs_for_anomalies.alerts[] +} + +output: { + triaged: Array<{ + ts: string, + priority: "P1" | "P2" | "P3", + asset: string, + summary: string, // " pattern from " + count: number, + evidence: string[], // up to 3 sample lines + }>, +} +``` + +**Promotion rules** (frozen): +- `severity_hint=high` → P2 by default; P1 once `count ≥ 2` +- `severity_hint=medium` → promote P3 → P2 +- `severity_hint=low` → stays P3 + +--- + +### 5. `correlate_threats` — `read_only` + +Joins triaged alerts into per-actor narratives with ATT&CK phase tags. Logic from `_blue_threat_correlate`. + +```ts +input: { + triaged: Array, // shape from triage_alerts.triaged[] +} + +output: { + actors: Array<{ + actor: string, // source IP + first_seen: string, // ISO8601 + last_seen: string, + phases_observed: string[], // e.g. ["TA0001", "TA0043"] + alert_summaries: string[], + narrative: string, // human-readable English summary + confidence: "low" | "medium" | "high", + }>, +} +``` + +**Phase mapping** (frozen): +- `scanner` → `TA0043` (Reconnaissance) +- `sqli`, `xss`, `traversal` → `TA0001` (Initial Access) +- `admin_path` → `TA0007` (Discovery) + +--- + +### 6. `suggest_exploit_prose` — `read_only` + +Generates **text-only** exploit explanations from vuln-scan findings. **Never returns runnable payloads.** This is the safety-critical tool — its name carries `_prose` to make the constraint visible to every caller. + +```ts +input: { + findings: Array, // shape from vuln_scan_web.findings[] + use_llm?: boolean, // default false; when true, calls LLMProvider for prose +} + +output: { + markdown: string, // full markdown document, "# Exploit Suggestions\n..." + has_runnable_poc: false, // INVARIANT: always false; checked by tests +} +``` + +**Hard constraints** (enforced by `tests/test_no_runnable_poc.py`): +- Output must not contain shell commands, curl invocations, payload strings, or template strings that would execute if pasted. +- The string `has_runnable_poc: false` is a load-bearing assertion; do not change. + +--- + +### 7. `compose_pentest_report` — `read_only` + +Renders the red-team-side markdown report. + +```ts +input: { + recon: ReconOutput, // from recon_host + vuln: VulnScanOutput, // from vuln_scan_web + exploit_suggestions_md: string, // from suggest_exploit_prose.markdown + timeline: Array<[string, string]>, // [[t_seconds, label], ...] +} + +output: { + markdown: string, + byte_size: number, +} +``` + +--- + +### 8. `compose_incident_report` — `read_only` + +Renders the blue-team-side markdown report. + +```ts +input: { + triaged: Array, + actors: Array, // from correlate_threats + timeline: Array<[string, string]>, +} + +output: { + markdown: string, + byte_size: number, + mttd_seconds: number, // first red event → first triaged alert +} +``` + +--- + +### 9. `lab_status` — `read_only` + +Reports docker lab health. Wraps `docker compose ps` in JSON form. + +```ts +input: {} // no parameters + +output: { + network_present: boolean, // is "secops-lab" network up? + services: Array<{ + name: "juice-shop" | "dvwa" | "dvwa-db" | "attacker" | "log-collector", + state: "running" | "exited" | "absent", + health: "healthy" | "unhealthy" | "starting" | "none", + }>, +} +``` + +**Side effects**: reads docker state; does not modify. + +--- + +### 10. `lab_up` — `lifecycle` + +Brings up the isolated docker lab. + +```ts +input: { confirm: true } +output: { ok: boolean, log: string } // log = last 2 KB of docker compose output +``` + +Idempotent. Calling without `confirm: true` returns `{ error: "lifecycle_action_requires_confirmation" }` and does nothing. + +### 11. `lab_down` — `lifecycle` + +Tears down the docker lab. Removes containers and volumes; **never** touches the `reports/runs/` directory on the host. + +```ts +input: { confirm: true } +output: { ok: boolean, log: string } +``` + +Same confirmation requirement as `lab_up`. Both lifecycle tools are intended for interactive callers (Claude Code, phantom-mesh dispatch with a human-authored prompt) — CI lanes should use `make lab-up` / `make lab-down` directly rather than going through MCP. + +--- + +## Resources + +Resources are read-only artifacts the agent can fetch by URI without invoking a tool. + +### `phantom-secops://runs/{run_id}/{filename}` + +``` +run_id = ISO timestamp dir name, e.g. "2026-05-05-1430" +filename ∈ { recon.json, vuln-scan.json, alerts.jsonl, triage-queue.jsonl, + kill-chains.jsonl, exploit-suggestions.md, + pentest-report.md, incident-report.md } +``` + +`run_id="latest"` resolves to the newest run dir at fetch time. + +### `phantom-secops://mocks/{name}` + +``` +name ∈ { recon-juice-shop.json, vuln-scan-juice-shop.json, attack-log.txt } +``` + +--- + +## Error model + +Every tool returns either its success shape or a flat error envelope: + +```ts +{ + error: string, // short code, snake_case + message?: string, // human-readable detail + context?: object, // tool-specific extras +} +``` + +Frozen error codes: + +| Code | Meaning | +|---|---| +| `not_a_lab_target` | Target is not in the lab service whitelist | +| `lab_network_down` | `secops-lab` docker network is not up | +| `tool_timeout` | Underlying CLI exceeded its budget | +| `tool_nonzero_exit` | Underlying CLI returned non-zero | +| `parse_failed` | Output could not be parsed (e.g. malformed nmap XML) | +| `lifecycle_action_requires_confirmation` | Lifecycle tool called without `confirm: true` | +| `bad_input` | Input failed schema validation | + +--- + +## Versioning + +This document is version `1.0.0`. The MCP server reports the same version in its handshake. Adapters may pin to a major version. + +- **Patch** bumps: docs-only, schema-additive (new optional input fields, new optional output fields). +- **Minor** bumps: new tools, new error codes, new resources. +- **Major** bumps: any rename, removal, type change, or safety-class change. + +Major bumps require updating: `mcp/server.py`, `mcp/schemas.py`, `agents/red/*.toml`, `agents/blue/*.toml`, `.claude/agents/secops-runner.md`, `scenarios/run_kill_chain.py`, and this file — in the same PR. diff --git a/phantom_secops/__init__.py b/phantom_secops/__init__.py new file mode 100644 index 0000000..cb02189 --- /dev/null +++ b/phantom_secops/__init__.py @@ -0,0 +1,8 @@ +"""phantom-secops — multi-agent SecOps research playground. + +Public surface: +- `phantom_secops.core` — runtime-agnostic red/blue pipeline functions. +- `phantom_secops.mcp` — MCP server exposing those functions to any agent. +""" + +__version__ = "0.2.0" diff --git a/phantom_secops/core.py b/phantom_secops/core.py new file mode 100644 index 0000000..37042f9 --- /dev/null +++ b/phantom_secops/core.py @@ -0,0 +1,399 @@ +"""Runtime-agnostic red/blue pipeline functions. + +This is the single implementation that backs both: +- the Python reference orchestrator (scenarios/run_kill_chain.py) +- the MCP server (phantom_secops/mcp/server.py) + +Everything here is a pure function over plain dicts — no docker, no LLM, +no MCP. The thin wrappers in tools/ shell into docker; the LLM provider in +phantom_secops/llm/ (Phase 3) generates prose. Both are kept out of this +module so it stays trivially testable. + +Function names match the public MCP tool names from docs/MCP-INTERFACE.md. +""" + +from __future__ import annotations + +import json +import re +from datetime import datetime, timezone +from pathlib import Path +from typing import Any, Protocol +from urllib.parse import unquote + +REPO_ROOT = Path(__file__).resolve().parent.parent +MOCKS_DIR = REPO_ROOT / "lab" / "mocks" +LAB_LOG_DIR = REPO_ROOT / "reports" / "lab-logs" + + +class _ProseProvider(Protocol): + """Structural duck-type for phantom_secops.llm.LLMProvider. + + Declared here to keep core.py free of an llm/ import cycle. + """ + + name: str + + def generate_prose(self, system: str, user: str, max_tokens: int = 1024) -> str: ... + + +# ─── Red pipeline ──────────────────────────────────────────────────────── + +def run_recon(target: str, mock: bool = False) -> dict[str, Any]: + """Recon a lab host. In mock mode, returns canned data; in live mode, + delegates to tools.nmap_runner (which shells into the attacker container). + """ + if mock: + return json.loads((MOCKS_DIR / "recon-juice-shop.json").read_text(encoding="utf-8")) + # Lazy import: tools/ requires docker, not needed in mock mode. + from tools import nmap_runner # noqa: PLC0415 + return nmap_runner.run(target) + + +def run_vuln_scan(target: str, recon: dict[str, Any], mock: bool = False) -> dict[str, Any]: + """Vuln scan a lab target using nuclei. Mock mode returns canned findings.""" + _ = recon # live mode reads recon.open_ports to pick HTTP ports + if mock: + return json.loads((MOCKS_DIR / "vuln-scan-juice-shop.json").read_text(encoding="utf-8")) + return {"target": target, "findings": []} + + +def suggest_exploit_prose( + findings: list[dict[str, Any]], + use_llm: bool = False, + provider: _ProseProvider | None = None, +) -> dict[str, Any]: + """Generate text-only exploit explanations from vuln-scan findings. + + INVARIANT: never returns runnable payloads. The output schema includes + `has_runnable_poc: false` which is asserted by tests/test_no_runnable_poc.py. + + When `use_llm=True` and a provider is supplied (or env-var-selected at + callsite), each finding's prose is generated by the provider and validated + against the same forbidden-pattern set used by tests. If validation fails + or the provider returns empty, falls back to the deterministic template. + """ + if not findings: + return {"markdown": "_No vulnerabilities flagged by the scan._\n", + "has_runnable_poc": False} + + out = ["# Exploit Suggestions\n"] + for f in findings: + out.append(f"## {f.get('id', 'unknown')} — {f.get('title', '(no title)')}\n") + cve = f.get("cve") + if cve: + out.append(f"**CVE:** {cve}") + out.append(f"**Severity:** {f.get('severity', 'unknown')}\n") + out.append(_finding_prose(f, use_llm=use_llm, provider=provider)) + out.append("") + return {"markdown": "\n".join(out), "has_runnable_poc": False} + + +def _finding_prose( + f: dict[str, Any], + use_llm: bool, + provider: _ProseProvider | None, +) -> str: + template_text = _exploit_prose(f) + if not use_llm or provider is None: + return template_text + + # Lazy-import to avoid a hard dependency for the no-LLM path. + from phantom_secops.mcp import safety # noqa: PLC0415 + + system = ( + "You are writing a prose-only security finding explanation. " + "RULES: no shell commands, no curl/wget/sudo lines, no payload strings, " + "no code fences with bash/sh/shell. Mitigation guidance is welcome. " + "Plain prose only. Reference public CVE pages by number, never by URL " + "containing exploit code." + ) + user = json.dumps({ + "id": f.get("id"), + "cve": f.get("cve"), + "severity": f.get("severity"), + "title": f.get("title"), + "evidence": f.get("evidence"), + }, ensure_ascii=False) + + generated = provider.generate_prose(system, user, max_tokens=400) + if generated and safety.is_safe_prose(generated): + return generated + # Fallback: provider unreachable / returned forbidden patterns / empty. + return template_text + + +def _exploit_prose(f: dict[str, Any]) -> str: + """Prose only. No runnable exploits, ever.""" + sev = f.get("severity", "info") + title = f.get("title", "") + if "jquery" in title.lower() or "CVE-2020-11023" in (f.get("cve") or ""): + return ("This vulnerability allows DOM-based XSS via malformed `