Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
hexstrike_env/
hexstrike-env/
.git/
*.log
*.jsonl
*.pid
__pycache__/
*.pyc
.DS_Store
scope.json
10 changes: 10 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
__pycache__/
*.pyc
hexstrike-env/
hexstrike_env/
*.log
*.jsonl
*.pid
.DS_Store
scope.json
.env
156 changes: 156 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
FROM kalilinux/kali-rolling

LABEL maintainer="thesaint"
LABEL description="HexStrike AI MCP server, hardened + containerized for isolated per-engagement use"

ENV DEBIAN_FRONTEND=noninteractive
ENV PYTHONUNBUFFERED=1

# ── Security tools ───────────────────────────────────────────────────────
# Kali's apt repos cover most of the "Core Tools (Essential)" set from the
# README directly. This is NOT the full 150+ tool list (many of the
# ProjectDiscovery/Go-based recon tools — nuclei, subfinder, httpx, katana,
# dalfox, ffuf — aren't in apt and need `go install`; add them below in the
# "Go-based tools" block as needed). Extend this list rather than installing
# tools ad-hoc inside a running container, so the image stays reproducible.
RUN apt-get update && apt-get install -y --no-install-recommends \
python3 python3-pip python3-venv python3-dev build-essential \
nmap masscan \
gobuster nikto whatweb wafw00f sslscan \
sqlmap wpscan \
hydra john hashcat medusa \
smbmap enum4linux responder \
radare2 binwalk foremost steghide exiftool checksec \
golang-go libcap2-bin \
git curl wget ca-certificates \
arsenal-ng gef wpprobe xsstrike sstimap python3-atomic-operator \
amass feroxbuster python3-impacket netexec certipy-ad \
&& rm -rf /var/lib/apt/lists/*

# RustScan isn't in Kali's apt repo (checked the live index directly) —
# it ships prebuilt releases on GitHub instead, as rustscan.deb.zip (a
# .deb wrapped in a zip, not a raw .deb — worth knowing, the naming
# isn't what you'd guess). Using GitHub's stable /latest/download/ redirect
# rather than the releases API, which is rate-limited per-IP and this
# build already got throttled by it once this session. python3 (already
# installed above) handles the unzip so this doesn't need a new apt
# package just for one archive.
RUN curl -sL https://github.com/RustScan/RustScan/releases/latest/download/rustscan.deb.zip -o /tmp/rustscan.zip \
&& python3 -m zipfile -e /tmp/rustscan.zip /tmp/rustscan_extracted/ \
&& dpkg -i /tmp/rustscan_extracted/*.deb \
&& rm -rf /tmp/rustscan.zip /tmp/rustscan_extracted

# ── Go-based tools (ProjectDiscovery etc.) ──────────────────────────────
# Each install is capped at 3min and non-fatal: a single stalled module
# proxy fetch shouldn't block the whole image build. Missing tools are
# reported in the build log — rerun `docker build` later to retry them,
# or add more here with the same pattern.
ENV GOPATH=/opt/go
# /opt/go/bin goes FIRST, not appended. apt/pip install same-named CLIs for
# unrelated tools (pip's httpx[cli] HTTP client vs. ProjectDiscovery's httpx
# recon scanner is the concrete case that shipped broken — appended-PATH
# order let the pip shim silently shadow the real binary). Putting our Go
# tools first makes every `go install` binary in this list authoritative,
# not just httpx.
ENV PATH=/opt/go/bin:$PATH
ENV GOPROXY=https://proxy.golang.org,direct
RUN --mount=type=cache,target=/root/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
for pkg in \
"github.com/projectdiscovery/httpx/cmd/httpx" \
"github.com/ffuf/ffuf/v2" \
"github.com/projectdiscovery/subfinder/v2/cmd/subfinder" \
"github.com/projectdiscovery/nuclei/v3/cmd/nuclei" \
"github.com/tomnomnom/waybackurls" \
; do \
echo "→ go install $pkg"; \
timeout 300 go install "${pkg}@latest" 2>&1 | tail -20 || echo "⚠️ SKIPPED (timeout/failed): $pkg"; \
done

# ── Non-root execution ───────────────────────────────────────────────────
# Run as an unprivileged user; grant raw-socket capabilities directly to
# the two binaries that need them (SYN scans, packet crafting) instead of
# running the whole container as root or with --privileged.
RUN useradd -m -u 1000 -s /bin/bash hexstrike && \
setcap cap_net_raw,cap_net_admin+eip /usr/bin/nmap && \
setcap cap_net_raw,cap_net_admin+eip /usr/bin/masscan

# gef (GDB Enhanced Features): the apt package only drops gef.py at
# /usr/share/gdb/gef.py, it doesn't wire it into gdb's startup — normally
# gef's own installer appends a `source` line to ~/.gdbinit, apt doesn't.
RUN echo "source /usr/share/gdb/gef.py" >> /etc/gdb/gdbinit

# amass's apt wrapper (/usr/bin/amass) checks for libpostal's data dir and,
# if missing, shells out to `sudo libpostal_data download all` — which
# fails outright under the non-root hexstrike user, and even as root would
# pull ~1-2GB just for one optional address-normalization feature amass
# barely uses (its actual recon functionality — DNS enum, ASN lookups,
# cert-transparency, subdomain brute-force — doesn't touch libpostal at
# all). Satisfying the wrapper's existence check with an empty sentinel
# avoids both the sudo failure and the multi-GB download; only the
# address-parsing-specific amass features would be affected, not the
# ones this is actually installed for.
RUN mkdir -p /var/lib/libpostal && touch /usr/share/libpostal/transliteration

WORKDIR /app
COPY requirements.txt .
# venv, not system pip — Kali ships several Python packages (bcrypt etc.)
# as dpkg-managed system packages that pip can't safely uninstall/upgrade
# even with --break-system-packages (PEP 668). Isolating into a venv
# avoids that whole conflict class.
#
# IMPORTANT: deliberately NOT prepending /opt/venv/bin to the global PATH
# here (as an earlier version of this file did). Several apt-packaged
# tools (confirmed: impacket-secretsdump and friends) ship thin shell
# wrappers that call bare `python3` rather than an absolute path — with
# venv-first PATH, those silently resolve to THIS isolated venv instead of
# system python3, and since venvs don't see system dist-packages, they
# fail with ModuleNotFoundError for a library that's actually installed
# and present. Root-caused by hand: verified the wrapper's shebang/exec
# line, verified the library was genuinely on disk, verified system
# python3's sys.path included dist-packages, isolated it to the PATH
# shadowing. Fix: reference the venv by absolute path (/opt/venv/bin/pip,
# /opt/venv/bin/python3 in entrypoint.sh) everywhere WE need it, and leave
# bare `python3` on PATH resolving to the system interpreter so every
# other apt-packaged tool's wrapper keeps working as its maintainer
# intended. This likely isn't impacket-specific — any Kali tool with a
# similar wrapper pattern was at risk before this fix.
RUN python3 -m venv /opt/venv
RUN /opt/venv/bin/pip install --no-cache-dir -r requirements.txt
# xsstrike pulls this in at runtime on first use if missing — bake it in
# so a per-engagement container doesn't need outbound pip access mid-test.
RUN /opt/venv/bin/pip install --no-cache-dir fuzzywuzzy
# atomic-operator itself comes from apt (python3-atomic-operator, above) —
# NOT pip. Its dependency chain (atomic-operator-runner pins pydantic 1.x;
# its other dependency `fire` still imports the stdlib `pipes` module,
# removed in Python 3.13/Kali-rolling's default python3 — a known,
# still-open upstream bug, google/python-fire#444) would conflict badly
# with mcp/fastmcp's pydantic 2.x requirement if installed into this venv
# via pip — confirmed by hand, it silently downgrades pydantic and breaks
# hexstrike_mcp.py's imports at runtime. Kali's own apt packaging already
# resolves both problems correctly (confirmed by hand: apt installs into
# system dist-packages, completely separate from this venv, and imports
# clean with no patching needed) — apt is the correct install path here,
# not pip.

COPY hexstrike_server.py hexstrike_mcp.py scope.example.json ./
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh

# /tmp is where the server's existing tool-output code writes by default
# (hexstrike_files, autorecon_*, prowler_* etc — see FileOperationsManager
# and per-tool output_dir defaults). Bind-mounting the HOST's per-engagement
# directory over this container's /tmp is what gives each engagement its
# own isolated, inspectable output — see hex-engage in .zshrc.
RUN mkdir -p /tmp/hexstrike_files && chown -R hexstrike:hexstrike /app /tmp/hexstrike_files

USER hexstrike
EXPOSE 8888

# NOTE: binding 0.0.0.0 here is intentional and NOT a regression of the
# 127.0.0.1-only rule from the bare-metal setup. Inside Docker's isolated
# network namespace, 0.0.0.0 just means "listen on this container's
# interface" — actual host/LAN exposure is controlled entirely by the
# `-p` flag on `docker run`. hex-engage always maps `-p 127.0.0.1:PORT:8888`,
# so the net effect on the host is identical: loopback-only.
ENTRYPOINT ["/entrypoint.sh"]
61 changes: 57 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -190,28 +190,67 @@ sudo apt update && sudo apt install google-chrome-stable
### Start the Server

```bash
# Start the MCP server
# Required: set an auth token — the server refuses to start without one
export HEXSTRIKE_API_TOKEN=$(python3 -c "import secrets; print(secrets.token_urlsafe(32))")

# Start the MCP server (loopback-only by default)
python3 hexstrike_server.py

# Optional: Start with debug mode
python3 hexstrike_server.py --debug

# Optional: Custom port configuration
python3 hexstrike_server.py --port 8888

# Optional: allowlist authorized engagement targets (see scope.example.json)
export HEXSTRIKE_SCOPE_FILE=/path/to/scope.json
```

### Verify Installation

```bash
# Test server health
curl http://localhost:8888/health
# Test server liveness (fast, no tool sweep)
curl -H "X-HexStrike-Token: $HEXSTRIKE_API_TOKEN" http://localhost:8888/ping

# Test server health (full tool-availability sweep, ~30s)
curl -H "X-HexStrike-Token: $HEXSTRIKE_API_TOKEN" http://localhost:8888/health

# Test AI agent capabilities
curl -X POST http://localhost:8888/api/intelligence/analyze-target \
-H "Content-Type: application/json" \
-H "X-HexStrike-Token: $HEXSTRIKE_API_TOKEN" \
-d '{"target": "example.com", "analysis_type": "comprehensive"}'
```

### Docker (recommended for real engagements)

The included `Dockerfile` builds a self-contained image (Kali base + the core tool set + the Go-based recon tools) and isolates the server from your host filesystem. This is the safer path if you're pointing this at anything beyond your own lab — the server executes arbitrary commands (`/api/command`), so contain the blast radius rather than running it bare-metal.

```bash
docker build -t hexstrike-ai:latest .
```

Run one container per engagement, each with its own scope file and its own output directory, so concurrent engagements can't cross-contaminate and every engagement has an inspectable audit trail:

```bash
mkdir -p ./engagements/acme-corp/workspace
cp scope.example.json ./engagements/acme-corp/scope.json # edit: authorized domains/CIDRs only

docker run -d --name hexstrike-acme-corp \
-p 127.0.0.1:8888:8888 \
--cap-add=NET_RAW --cap-add=NET_ADMIN \
-v "$(pwd)/engagements/acme-corp/workspace:/tmp/hexstrike_files" \
-v "$(pwd)/engagements/acme-corp/scope.json:/app/scope.json:ro" \
-e HEXSTRIKE_API_TOKEN="$HEXSTRIKE_API_TOKEN" \
-e HEXSTRIKE_SCOPE_FILE=/app/scope.json \
-e HEXSTRIKE_AUDIT_LOG=/tmp/hexstrike_files/audit.jsonl \
hexstrike-ai:latest
```

Tool output and the audit log both land in `./engagements/acme-corp/workspace` on the host — inspectable, and gone (well, archived, not scanning) as soon as you `docker rm` the container. `--cap-add=NET_RAW --cap-add=NET_ADMIN` is what lets `nmap`/`masscan` do raw-socket scans (SYN scans etc.) despite the container running as a non-root user — the image doesn't grant that capability container-wide, it's set directly on those two binaries via `setcap`.

The `0.0.0.0` bind you'll see if you inspect the image is intentional and not a contradiction of the loopback-only default described above — Docker's network namespace means it's harmless in isolation; the `-p 127.0.0.1:8888:8888` mapping is what actually determines host-level exposure, and that's loopback-only here too.

---

## AI Client Integration Setup
Expand Down Expand Up @@ -507,6 +546,7 @@ Configure VS Code settings in `.vscode/settings.json`:
| Endpoint | Method | Description |
|----------|--------|-------------|
| `/health` | GET | Server health check with tool availability |
| `/ping` | GET | Lightweight liveness check (no tool sweep, instant) |
| `/api/command` | POST | Execute arbitrary commands with caching |
| `/api/telemetry` | GET | System performance metrics |
| `/api/cache/stats` | GET | Cache performance statistics |
Expand Down Expand Up @@ -647,7 +687,20 @@ python3 hexstrike_mcp.py --debug
- Run in isolated environments or dedicated security testing VMs
- AI agents can execute arbitrary security tools - ensure proper oversight
- Monitor AI agent activities through the real-time dashboard
- Consider implementing authentication for production deployments

### Built-in Hardening

The server ships with the following on by default or available via env vars — set these up before pointing this at anything real:

| Control | Env Var | Default | Notes |
|---|---|---|---|
| Bind address | `HEXSTRIKE_HOST` / `--host` | `127.0.0.1` | Server **refuses to run open on `0.0.0.0`** unless you explicitly pass a different `--host`. Loopback-only by default. |
| Shared-secret auth | `HEXSTRIKE_API_TOKEN` | **required** | Server exits at startup if unset — it will not run unauthenticated. Every route (including `/health`, `/ping`) requires header `X-HexStrike-Token: <token>`. Generate with `python3 -c "import secrets; print(secrets.token_urlsafe(32))"`. |
| Liveness check | — | `/ping` | Lightweight, instant. `/health` does a full ~30s tool-availability sweep — use `/ping` for polling/monitoring, `/health` for diagnostics. |
| Engagement scope | `HEXSTRIKE_SCOPE_FILE` | off (opt-in) | Points to a JSON file (`{"domains": [...], "networks": ["CIDR", ...]}` — see `scope.example.json`) allowlisting authorized targets. Requests referencing an out-of-scope host — via structured params **or** parsed out of a raw command string — get `403`. Set this per engagement; it is the single most important control if you're running this against client-owned infrastructure. |
| Audit log | `HEXSTRIKE_AUDIT_LOG` | `hexstrike_audit.jsonl` | JSONL, one line per authenticated request: timestamp, source IP, method, path, extracted targets, response status. Keep it for engagement records. |

None of this replaces running the server on isolated infrastructure — it reduces the blast radius of the server being reachable or misused, it doesn't sandbox the 150+ tools it invokes.

### Legal & Ethical Use

Expand Down
19 changes: 19 additions & 0 deletions entrypoint.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#!/bin/bash
set -euo pipefail

# HEXSTRIKE_API_TOKEN is validated by the server itself (fails closed at
# import time if unset) — no need to duplicate that check here. This just
# gives a clearer error before Python even starts, and reports the scope
# state so a misconfigured engagement is obvious from `docker logs`.
if [ -z "${HEXSTRIKE_API_TOKEN:-}" ]; then
echo "❌ HEXSTRIKE_API_TOKEN not set — refusing to start. Pass it via -e HEXSTRIKE_API_TOKEN=..." >&2
exit 1
fi

if [ -z "${HEXSTRIKE_SCOPE_FILE:-}" ]; then
echo "⚠️ HEXSTRIKE_SCOPE_FILE not set — scope enforcement is OFF. Do not point this at a real client engagement without it."
else
echo "🎯 Scope enforcement active: ${HEXSTRIKE_SCOPE_FILE}"
fi

exec /opt/venv/bin/python3 hexstrike_server.py --host 0.0.0.0 --port 8888
17 changes: 15 additions & 2 deletions hexstrike_mcp.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,14 +159,27 @@ def __init__(self, server_url: str, timeout: int = DEFAULT_REQUEST_TIMEOUT):
self.timeout = timeout
self.session = requests.Session()

# The server has required auth (X-HexStrike-Token) since the
# security-hardening work — this client predates that and never
# sent it, meaning every single request would 401 silently.
# Fail-closed here rather than sending unauthenticated requests
# that are guaranteed to fail, matching the server's own posture.
api_token = os.environ.get("HEXSTRIKE_API_TOKEN")
if not api_token:
logger.error("❌ HEXSTRIKE_API_TOKEN is not set — the server will reject every request. Set it in the MCP client's env config.")
else:
self.session.headers.update({"X-HexStrike-Token": api_token})

# Try to connect to server with retries
connected = False
for i in range(MAX_RETRIES):
try:
logger.info(f"🔗 Attempting to connect to HexStrike AI API at {server_url} (attempt {i+1}/{MAX_RETRIES})")
# First try a direct connection test before using the health endpoint
# /ping, not /health — health runs a ~30s full tool-availability
# sweep, which would stall every MCP client startup for no
# reason; /ping is the fast liveness check for exactly this.
try:
test_response = self.session.get(f"{self.server_url}/health", timeout=5)
test_response = self.session.get(f"{self.server_url}/ping", timeout=5)
test_response.raise_for_status()
health_check = test_response.json()
connected = True
Expand Down
Loading