From dbfdf1df900d8c8ccd9fadf6fa4741143db0da20 Mon Sep 17 00:00:00 2001 From: andreicscs Date: Fri, 3 Apr 2026 11:38:16 +0200 Subject: [PATCH] refactor: implement V1.0 contract and unified SDK architecture - Migrated to HW_ environment variable namespace - Created installable python-honeywire SDK package - Standardized sensor templates with Distroless Docker support - Added CI/CD pipeline for PR security scanning and functional testing - Updated all internal documentation to match V1.0 specs --- .github/scripts/mock_hub.py | 53 +++++ .github/workflows/docker-publish.yml | 48 ---- .github/workflows/pr_sensor_test.yml | 102 ++++++++ .gitignore | 22 +- Agent/.env.example | 23 -- Agent/Dockerfile | 26 -- Agent/agent.py | 222 ------------------ Agent/docker-compose.yml | 9 - CONTRIBUTING.md | 50 ++++ API.md => Docs/API.md | 5 +- Hub/.env.example | 28 ++- Hub/main.py | 24 +- Hub/requirements.txt | 20 +- README.md | 73 +++--- SDKs/python-honeywire/honeywire/__init__.py | 146 ++++++++++++ SDKs/python-honeywire/setup.py | 7 + {screenshots => Screenshots}/dashboard.png | Bin .../payload-inspector.png | Bin Sensors/official/TcpTripWire/.env.example | 29 +++ Sensors/official/TcpTripWire/Dockerfile | 36 +++ Sensors/official/TcpTripWire/README.md | 70 ++++++ Sensors/official/TcpTripWire/TcpTripWire.py | 131 +++++++++++ .../official/TcpTripWire/docker-compose.yml | 14 ++ Sensors/official/TcpTripWire/requirements.txt | 6 + Sensors/templates/python-sensor/.env.example | 15 ++ Sensors/templates/python-sensor/Dockerfile | 32 +++ Sensors/templates/python-sensor/README.md | 26 ++ .../templates/python-sensor/requirements.txt | 8 + Sensors/templates/python-sensor/sensor.py | 60 +++++ 29 files changed, 892 insertions(+), 393 deletions(-) create mode 100644 .github/scripts/mock_hub.py delete mode 100644 .github/workflows/docker-publish.yml create mode 100644 .github/workflows/pr_sensor_test.yml delete mode 100644 Agent/.env.example delete mode 100644 Agent/Dockerfile delete mode 100644 Agent/agent.py delete mode 100644 Agent/docker-compose.yml create mode 100644 CONTRIBUTING.md rename API.md => Docs/API.md (96%) create mode 100644 SDKs/python-honeywire/honeywire/__init__.py create mode 100644 SDKs/python-honeywire/setup.py rename {screenshots => Screenshots}/dashboard.png (100%) rename {screenshots => Screenshots}/payload-inspector.png (100%) create mode 100644 Sensors/official/TcpTripWire/.env.example create mode 100644 Sensors/official/TcpTripWire/Dockerfile create mode 100644 Sensors/official/TcpTripWire/README.md create mode 100644 Sensors/official/TcpTripWire/TcpTripWire.py create mode 100644 Sensors/official/TcpTripWire/docker-compose.yml create mode 100644 Sensors/official/TcpTripWire/requirements.txt create mode 100644 Sensors/templates/python-sensor/.env.example create mode 100644 Sensors/templates/python-sensor/Dockerfile create mode 100644 Sensors/templates/python-sensor/README.md create mode 100644 Sensors/templates/python-sensor/requirements.txt create mode 100644 Sensors/templates/python-sensor/sensor.py diff --git a/.github/scripts/mock_hub.py b/.github/scripts/mock_hub.py new file mode 100644 index 0000000..b04ab0e --- /dev/null +++ b/.github/scripts/mock_hub.py @@ -0,0 +1,53 @@ +import http.server +import json +import sys + +class MockHubHandler(http.server.BaseHTTPRequestHandler): + def do_POST(self): + content_length = int(self.headers['Content-Length']) + post_data = self.rfile.read(content_length) + + try: + payload = json.loads(post_data.decode('utf-8')) + + # 1. Enforce the V1.0 Contract + required_keys = [ + "contract_version", "sensor_id", "sensor_type", + "event_type", "severity", "timestamp", "metadata" + ] + + for key in required_keys: + if key not in payload: + print(f"❌ FAILED: Missing required key '{key}'") + sys.exit(1) + + # 2. Enforce severity type + if not isinstance(payload['severity'], int) and payload['severity'] not in ["info", "low", "medium", "high", "critical"]: + print("❌ FAILED: 'severity' must be an int or valid enum.") + sys.exit(1) + + print("✅ SUCCESS: Valid HoneyWire V1.0 payload received.") + print(json.dumps(payload, indent=2)) + + self.send_response(200) + self.end_headers() + + # Write a success flag for the CI runner + with open('/tmp/test_passed', 'w') as f: + f.write('success') + + except json.JSONDecodeError: + print("❌ FAILED: Payload is not valid JSON.") + sys.exit(1) + except Exception as e: + print(f"❌ FAILED: Unexpected error - {e}") + sys.exit(1) + + def log_message(self, format, *args): + # Suppress default HTTP logging to keep CI logs clean + pass + +if __name__ == '__main__': + print("🛡️ Mock Hub listening on port 8080...") + server = http.server.HTTPServer(('0.0.0.0', 8080), MockHubHandler) + server.handle_request() # Process exactly one request then exit \ No newline at end of file diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml deleted file mode 100644 index 63e1115..0000000 --- a/.github/workflows/docker-publish.yml +++ /dev/null @@ -1,48 +0,0 @@ -name: Build and Publish HoneyWire - -on: - push: - branches: [ "main" ] - release: - types: [published] - -jobs: - build-and-push: - runs-on: ubuntu-latest - permissions: - contents: read - packages: write - - strategy: - matrix: - component: [Hub, Agent] - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Log in to GHCR - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - # --- THIS IS WHERE THE TAG RULES BELONG --- - - name: Extract metadata - id: meta - uses: docker/metadata-action@v5 - with: - images: ghcr.io/${{ github.repository }}-${{ matrix.component }} - tags: | - type=ref,event=branch - type=raw,value=latest,enable={{is_default_branch}} - - # --- THIS IS THE BUILDER THAT USES THE RULES --- - - name: Build and push - uses: docker/build-push-action@v5 - with: - context: ./${{ matrix.component }} - push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} \ No newline at end of file diff --git a/.github/workflows/pr_sensor_test.yml b/.github/workflows/pr_sensor_test.yml new file mode 100644 index 0000000..dbf3758 --- /dev/null +++ b/.github/workflows/pr_sensor_test.yml @@ -0,0 +1,102 @@ +name: Sensor PR Check & Security Scan + +on: + pull_request: + paths: + - 'Sensors/community/**' + - 'Sensors/official/**' + +jobs: + static-analysis: + name: CodeQL Security Scan + runs-on: ubuntu-latest + permissions: + security-events: write + actions: read + contents: read + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: python + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + + docker-security-scan: + name: Build & Trivy Image Scan + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + # We need to find which sensor changed to build the right Dockerfile + - name: Get changed files + id: changed-files + uses: tj-actions/changed-files@v42 + with: + files: Sensors/** + + - name: Build and Scan Changed Sensors + run: | + for file in ${{ steps.changed-files.outputs.all_changed_files }}; do + SENSOR_DIR=$(dirname "$file") + + # Only build if there is a Dockerfile + if [ -f "$SENSOR_DIR/Dockerfile" ]; then + echo "Building $SENSOR_DIR..." + docker build -t test-sensor:latest "$SENSOR_DIR" + + echo "Running Trivy Vulnerability Scanner..." + curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin + trivy image --severity HIGH,CRITICAL --exit-code 1 --no-progress test-sensor:latest + fi + done + + functional-test: + name: Mock Hub Contract Test + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Start Mock Hub + run: | + python .github/scripts/mock_hub.py & + sleep 3 # Give the server a second to start + + - name: Get changed files + id: changed-files + uses: tj-actions/changed-files@v42 + with: + files: Sensors/** + + - name: Run Test Mode on Changed Sensors + run: | + for file in ${{ steps.changed-files.outputs.all_changed_files }}; do + SENSOR_DIR=$(dirname "$file") + + if [ -f "$SENSOR_DIR/Dockerfile" ]; then + docker build -t test-sensor:latest "$SENSOR_DIR" + + # Run the container with test mode enabled + docker run --rm \ + -e HW_HUB_ENDPOINT="http://172.17.0.1:8080" \ + -e HW_HUB_KEY="mock_key" \ + -e HW_SENSOR_ID="ci-test-node" \ + -e HW_TEST_MODE="true" \ + test-sensor:latest + + # Check if the Mock Hub received the success signal + if [ -f /tmp/test_passed ]; then + echo "✅ Sensor successfully communicated with Mock Hub." + rm /tmp/test_passed + else + echo "❌ Sensor failed to contact Mock Hub or sent invalid JSON." + exit 1 + fi + fi + done \ No newline at end of file diff --git a/.gitignore b/.gitignore index f91fb33..b0fc001 100644 --- a/.gitignore +++ b/.gitignore @@ -1,16 +1,32 @@ +# ========================= # Secrets and Config +# ========================= .env -**/ .env +**/.env +# (Note: We do NOT ignore .env.example so users have a template) +# ========================= # Database +# ========================= *.db *.sqlite3 +# ========================= # Python +# ========================= __pycache__/ *.py[cod] venv/ .venv/ +*.egg-info/ +build/ +dist/ -# Docker -.dockerignore \ No newline at end of file +# ========================= +# OS & IDE Clutter +# ========================= +.DS_Store +Thumbs.db +.vscode/ + +# (Notice: .dockerignore is intentionally NOT here. It must be tracked by Git!) \ No newline at end of file diff --git a/Agent/.env.example b/Agent/.env.example deleted file mode 100644 index f6c2289..0000000 --- a/Agent/.env.example +++ /dev/null @@ -1,23 +0,0 @@ -# Where is the Hub located? (Use your Hub's public/LAN IP if on a different machine) -HUB_URL=http://127.0.0.1:8080 - -# Must match the Hub's secret -API_SECRET=super_secret_key_123 - -# Unique name and IP for this specific sensor -SENSOR_ID=ubuntu-vps-02 -SENSOR_IP=192.168.1.51 - -# The fake ports this agent will listen on -DECOY_PORTS=21,2222,3306 - -# Modes: 'echo' (repeat data), 'hold' (do nothing, keep open), 'close' (drop immediately) -TARPIT_MODE=hold - -# UI Color Coding: info|low|medium|high|critical -SEVERITY=high - -# The fake service banner. -# Example for SSH: SSH-2.0-OpenSSH_8.2p1 Ubuntu-4ubuntu0.1\r\n -# Example for FTP: 220 (vsFTPd 3.0.3)\r\n -TARPIT_BANNER=SSH-2.0-OpenSSH_8.2p1 Ubuntu-4ubuntu0.1\r\n \ No newline at end of file diff --git a/Agent/Dockerfile b/Agent/Dockerfile deleted file mode 100644 index 6b9adbd..0000000 --- a/Agent/Dockerfile +++ /dev/null @@ -1,26 +0,0 @@ -# --- Stage 1: Builder --- -FROM python:3.11-slim AS builder -WORKDIR /app - -# Create a virtual environment and install dependencies -RUN python -m venv /opt/venv -ENV PATH="/opt/venv/bin:$PATH" -RUN pip install --no-cache-dir requests - -# --- Stage 2: Distroless (Production) --- -FROM gcr.io/distroless/python3-debian12 -WORKDIR /app - -# Copy the virtual environment from the builder -COPY --from=builder /opt/venv /opt/venv - -ENV PYTHONPATH="/opt/venv/lib/python3.11/site-packages" -ENV PATH="/opt/venv/bin:$PATH" - -# Copy your agent script -COPY agent.py . - -# Notice: We intentionally do NOT use a nonroot user here. -# We remain 'root' to allow binding to privileged decoy ports (1-1024). - -CMD ["agent.py"] \ No newline at end of file diff --git a/Agent/agent.py b/Agent/agent.py deleted file mode 100644 index 211b1ca..0000000 --- a/Agent/agent.py +++ /dev/null @@ -1,222 +0,0 @@ -""" -HoneyWire Tarpit Agent (Standard Port Tripwire) -Sensor type: tarpit - -Environment variables: - HUB_URL Hub base URL (default: http://localhost:8080) - API_SECRET Shared API key (default: super_secret_key_123) - SENSOR_ID Unique name for this node (default: alpha-node-01) - DECOY_PORTS Comma-separated ports (default: 2222,3306) - SEVERITY info|low|medium|high|critical (default: high) - TARPIT_MODE hold | echo | close (default: hold) -""" - -import asyncio -import os -import sys -import time -import threading -import requests - -# --------------------------------------------------------------------------- -# Configuration -# --------------------------------------------------------------------------- -HUB_URL = os.getenv("HUB_URL", "http://localhost:8080") -API_SECRET = os.getenv("API_SECRET", "super_secret_key_123") -SENSOR_ID = os.getenv("SENSOR_ID", "alpha-node-01") -SEVERITY = os.getenv("SEVERITY", "high").lower() - -raw_ports = os.getenv("DECOY_PORTS", "2222,3306") or "2222,3306" -DECOY_PORTS = [int(p.strip()) for p in raw_ports.split(",") if p.strip()] - -TARPIT_MODE = os.getenv("TARPIT_MODE", "hold").lower() - -raw_banner = os.getenv("TARPIT_BANNER", "") -TARPIT_BANNER = ( - raw_banner.encode("utf-8").decode("unicode_escape").encode("utf-8") - if raw_banner else b"" -) - -DEFAULT_AGENT_VERSION = "1.0.0" -VERSION_FILE_PATH = os.path.join(os.path.dirname(__file__), "..", "VERSION") -try: - with open(os.path.abspath(VERSION_FILE_PATH), "r") as f: - file_version = f.read().strip() -except FileNotFoundError: - file_version = DEFAULT_AGENT_VERSION - -AGENT_VERSION = os.getenv("HONEYWIRE_VERSION", file_version) -MAX_BYTES = 1024 * 50 # 50 KB safety cap per connection -MAX_LINES = 10 # Max lines to store in memory for forensics -MAX_DURATION = 3600 # Force disconnect after 1 hour to free File Descriptors -CONCURRENCY_LIMIT = 1000 # Max simultaneous connections to prevent FD exhaustion - -_HEADERS = {"x-api-key": API_SECRET} - -# Dynamically set upon startup synchronization -HUB_CONTRACT_VERSION = "unknown" - -# Global semaphore to limit concurrent TCP sockets -connection_semaphore = asyncio.Semaphore(CONCURRENCY_LIMIT) - - -# --------------------------------------------------------------------------- -# Hub Communication -# --------------------------------------------------------------------------- -def sync_hub_version() -> None: - """Fetches the Hub's contract version on startup to ensure compatibility.""" - global HUB_CONTRACT_VERSION - try: - print(f"[*] Synchronizing with Hub at {HUB_URL}...") - resp = requests.get(f"{HUB_URL}/api/v1/version", headers=_HEADERS, timeout=5) - resp.raise_for_status() - - HUB_CONTRACT_VERSION = resp.json()["version"] - - # Semantic Versioning Check (Major Version) - hub_major = HUB_CONTRACT_VERSION.split('.')[0] - agent_major = AGENT_VERSION.split('.')[0] - - if hub_major != agent_major: - print(f"[!] FATAL: Version mismatch. Hub (v{HUB_CONTRACT_VERSION}) vs Agent (v{AGENT_VERSION})") - sys.exit(1) - - print(f"[+] Synchronized successfully. Operating on contract v{HUB_CONTRACT_VERSION}") - - except requests.exceptions.RequestException as e: - print(f"[!] FATAL: Failed to synchronize with Hub. Is it offline? Details: {e}") - sys.exit(1) - - -def send_heartbeat() -> None: - """Pings the Hub every 30 seconds to stay 'online'.""" - payload = { - "sensor_id": SENSOR_ID, - "sensor_type": "tarpit", - "metadata": { - "agent_version": AGENT_VERSION, - "contract_version": HUB_CONTRACT_VERSION, - "mode": TARPIT_MODE, - "ports": DECOY_PORTS, - "severity_config": SEVERITY - } - } - while True: - try: - requests.post(f"{HUB_URL}/api/v1/heartbeat", headers=_HEADERS, json=payload, timeout=5) - except Exception as e: - print(f[-] Heartbeat error: {e}") - time.sleep(30) - - -def report_event(source_ip: str, port: int, duration: float, payload: list[str]) -> None: - """Reports connection to Hub using the Universal Event Standard.""" - event = { - "contract_version": HUB_CONTRACT_VERSION, # Integrated dynamic version - "sensor_id": SENSOR_ID, - "sensor_type": "tarpit", - "event_type": "tcp_connection", - "severity": SEVERITY, - "source": source_ip, - "target": f"Port {port}", - "action_taken": TARPIT_MODE, - "details": { - "duration_sec": round(duration, 2), - "payload": payload, - } - } - try: - requests.post(f"{HUB_URL}/api/v1/event", headers=_HEADERS, json=event, timeout=5) - print(f"[+] Alert sent: {source_ip} -> Port {port} ({SEVERITY.upper()})") - except Exception as e: - print(f"[-] Event report failed: {e}") - - -# --------------------------------------------------------------------------- -# Core Logic -# --------------------------------------------------------------------------- -async def handle_client(reader: asyncio.StreamReader, writer: asyncio.StreamWriter, port: int) -> None: - addr = writer.get_extra_info("peername") - source_ip = addr[0] if addr else "Unknown" - - start_time = time.time() - payload = [] - total_bytes = 0 - - try: - if TARPIT_BANNER and TARPIT_MODE != "close": - writer.write(TARPIT_BANNER) - await writer.drain() - - if TARPIT_MODE != "close": - while total_bytes < MAX_BYTES and (time.time() - start_time) < MAX_DURATION: - try: - # Added a default timeout even for "hold" to allow TTL checks - timeout = 60.0 if TARPIT_MODE == "echo" else 300.0 - data = await asyncio.wait_for(reader.read(1024), timeout=timeout) - - if not data: break - - decoded = data.decode("utf-8", errors="replace").strip() - if decoded: - # 🔒 SECURITY: Only store the first 10 lines to prevent memory bloat - if len(payload) < MAX_LINES: - payload.append(decoded) - - total_bytes += len(data) - - if TARPIT_MODE == "echo": - writer.write(data) - await writer.drain() - - await asyncio.sleep(0.5) # Anti-spam delay - - except asyncio.TimeoutError: - if not writer.is_closing(): - writer.write(b"\0") # Heartbeat byte to keep connection active - await writer.drain() - continue - - except Exception: - pass # Expected when attackers forcefully close the socket - finally: - # 🔒 SECURITY: Try/Except prevents Ghost Bypass on TCP RST - try: - writer.close() - await writer.wait_closed() - except Exception: - pass - - duration = time.time() - start_time - await asyncio.to_thread(report_event, source_ip, port, duration, payload) - - -async def connection_wrapper(reader: asyncio.StreamReader, writer: asyncio.StreamWriter, port: int) -> None: - """Wraps handle_client in a Semaphore to prevent File Descriptor exhaustion.""" - async with connection_semaphore: - await handle_client(reader, writer, port) - - -async def main() -> None: - print(f"[*] HoneyWire Agent v{AGENT_VERSION} | Severity: {SEVERITY.upper()}") - - # 1. Sync version with Hub before opening any ports - sync_hub_version() - - # 2. Start heartbeat thread - threading.Thread(target=send_heartbeat, daemon=True).start() - - # 3. Start decoy listeners - servers = [] - for port in DECOY_PORTS: - server = await asyncio.start_server(lambda r, w, p=port: connection_wrapper(r, w, p), "0.0.0.0", port) - servers.append(server.serve_forever()) - print(f"[+] Listening on port {port}") - - await asyncio.gather(*servers) - -if __name__ == "__main__": - try: - asyncio.run(main()) - except KeyboardInterrupt: - print("\n[*] Shutting down HoneyWire Agent...") \ No newline at end of file diff --git a/Agent/docker-compose.yml b/Agent/docker-compose.yml deleted file mode 100644 index a007610..0000000 --- a/Agent/docker-compose.yml +++ /dev/null @@ -1,9 +0,0 @@ -services: - agent: - build: . - container_name: honeywire-agent - restart: unless-stopped - # Binds directly to the host machine's network to catch real port scans - network_mode: "host" - env_file: - - .env \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..91c8b14 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,50 @@ +# Contributing to HoneyWire 🍯🕸️ + +Welcome to HoneyWire! We are building a centralized, high-fidelity security ecosystem for homelabs and SMBs. + +To keep the ecosystem stable, all community-submitted sensors must adhere to a strict set of rules. We treat sensors as **isolated microservices**. + +## The Golden Rules of Sensors + +1. **Self-Contained (Docker Only):** Every sensor must include a `Dockerfile`. Users should not need to install Python, Node, Go, or Rust on their host machine to run your sensor. +2. **Zero Blast Radius:** Your sensor must not crash the main Hub. All communication must happen via HTTP POST requests containing JSON. +3. **No Hardcoding:** All configurations (Ports, API keys, file paths) must be handled via environment variables inside a `.env` file. + +## How to Submit a New Sensor + +### 1. Use the Template +Copy the `Sensors/templates/python-sensor/` folder and rename it to your sensor's name inside the `Sensors/community/` directory. + +### 2. Follow the JSON Contract (v1.0) +Your sensor must POST a payload to the Hub (`HW_HUB_ENDPOINT`) matching this exact schema: + +```json +{ + "contract_version": "1.0", + "sensor_id": "provided-by-env", + "sensor_type": "your_sensor_category", + "event_type": "what_just_happened", + "severity": "critical", + "timestamp": "2026-04-03T01:24:18Z", + "action_taken": "logged", + "metadata": { + "ip": "192.168.1.5", + "custom_data": "anything you want" + } +} +``` +*(Note: If you use the official HoneyWire SDK provided in the template, this formatting is handled for you automatically).* + +### 3. Implement Test Mode (Required for CI/CD) +To ensure your code works before merging, our GitHub Actions will build your Docker container and pass `HW_TEST_MODE=true` as an environment variable. + +If this variable is present, your sensor **must immediately send a dummy payload to the Hub and exit**. (The Python SDK handles this out-of-the-box). + +### 4. Provide Documentation +Your sensor folder must contain a `README.md` that explicitly lists all required environment variables and provides instructions for the user. + +## Review Process +Once you open a Pull Request: +1. **Functional Testing:** GitHub Actions will automatically build your Docker container and test it against a Mock Hub using `HW_TEST_MODE=true`. +2. **Automated Security Scanning:** GitHub Actions will run **Trivy** to scan your Docker image for OS and library vulnerabilities, and **CodeQL** to perform static code analysis for security flaws. +3. **Manual Review:** A core maintainer will manually review the code for malicious intent or blast-radius risks before merging. PRs that fail automated testing or scanning will not be reviewed. \ No newline at end of file diff --git a/API.md b/Docs/API.md similarity index 96% rename from API.md rename to Docs/API.md index 9dae766..c0e31bd 100644 --- a/API.md +++ b/Docs/API.md @@ -70,6 +70,7 @@ This document describes the HTTP API for the HoneyWire Hub backend. ```json [ { + "contract_version": "1.0.0", "id": 123, "timestamp": "2026-04-02 15:25:12", "sensor_id": "alpha-node-01", @@ -130,6 +131,7 @@ This document describes the HTTP API for the HoneyWire Hub backend. - Request: ```json { + "contract_version": "1.0.0", "sensor_id": "alpha-node-01", "sensor_type": "tarpit", "event_type": "tcp_connection", @@ -139,8 +141,7 @@ This document describes the HTTP API for the HoneyWire Hub backend. "action_taken": "hold", "details": { "duration_sec": 12.3, - "payload_sample": ["sudo rm -rf /"], - "total_lines": 7 + "payload": ["sudo rm -rf /"], } } ``` diff --git a/Hub/.env.example b/Hub/.env.example index a9173d0..d658fb1 100644 --- a/Hub/.env.example +++ b/Hub/.env.example @@ -1,12 +1,22 @@ -# The port the dashboard will be accessible on -HUB_PORT=8080 +# ========================================== +# HONEYWIRE HUB CONFIGURATION +# ========================================== -# The master password for your fleet -API_SECRET=super_secret_key_123 +# The secret key your sensors use to talk to the Hub. Make this long and random! +HW_HUB_KEY=change_this_to_a_secure_random_string -# Optional: Get push notifications to your phone -NTFY_URL=https://ntfy.sh/supersecretnotificationendpoint -GOTIFY_URL=http://your-gotify-server.local/message -GOTIFY_TOKEN=AxxBxxCxxDxx +# The password to log into the Web Dashboard +HW_DASHBOARD_PASSWORD=admin -DASHBOARD_PASSWORD=my_secure_password \ No newline at end of file +# Where should the SQLite database be stored? (Inside the container) +HW_DB_PATH=/data/honeywire.db + +# ========================================== +# NOTIFICATIONS (Optional) +# ========================================== +# Push alerts directly to your phone via Ntfy (e.g., https://ntfy.sh/my_secret_topic) +HW_NTFY_URL= + +# Push alerts to a self-hosted Gotify instance +HW_GOTIFY_URL= +HW_GOTIFY_TOKEN= \ No newline at end of file diff --git a/Hub/main.py b/Hub/main.py index 078efa5..1edd512 100755 --- a/Hub/main.py +++ b/Hub/main.py @@ -33,14 +33,21 @@ logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(me log = logging.getLogger("honeywire") # --------------------------------------------------------------------------- -# Configuration (all values come from environment variables) +# Configuration (all values come from HW_ environment variables) # --------------------------------------------------------------------------- -API_SECRET = os.getenv("API_SECRET", "super_secret_key_123") -DASHBOARD_PASSWORD = os.getenv("DASHBOARD_PASSWORD", "") -NTFY_URL = os.getenv("NTFY_URL", "") -GOTIFY_URL = os.getenv("GOTIFY_URL", "") -GOTIFY_TOKEN = os.getenv("GOTIFY_TOKEN", "") -DB_PATH = os.getenv("DB_PATH", "/data/honeywire.db") +# The API Key sensors use to authenticate to the Hub +API_SECRET = os.getenv("HW_HUB_KEY", "super_secret_key_123") + +# Dashboard UI Password +DASHBOARD_PASSWORD = os.getenv("HW_DASHBOARD_PASSWORD", "") + +# Notification Endpoints +NTFY_URL = os.getenv("HW_NTFY_URL", "") +GOTIFY_URL = os.getenv("HW_GOTIFY_URL", "") +GOTIFY_TOKEN = os.getenv("HW_GOTIFY_TOKEN", "") + +# Database Location +DB_PATH = os.getenv("HW_DB_PATH", "/data/honeywire.db") # Versioning: one source of truth stored in root file + env override. DEFAULT_VERSION = "1.0.0" @@ -51,7 +58,8 @@ try: except FileNotFoundError: FILE_VERSION = DEFAULT_VERSION -HONEYWIRE_VERSION = os.getenv("HONEYWIRE_VERSION", FILE_VERSION) +# Use HW_VERSION to allow users to override it if strictly necessary +HONEYWIRE_VERSION = os.getenv("HW_VERSION", FILE_VERSION) AUTH_COOKIE_NAME = "hw_auth" diff --git a/Hub/requirements.txt b/Hub/requirements.txt index 8fc8031..6ac7be8 100755 --- a/Hub/requirements.txt +++ b/Hub/requirements.txt @@ -1,5 +1,15 @@ -fastapi -uvicorn -pydantic -requests -jinja2 \ No newline at end of file +# Web Framework +fastapi==0.110.0 +uvicorn[standard]==0.29.0 + +# HTML Templating for the Dashboard +jinja2==3.1.3 + +# Data Validation (Required by FastAPI) +pydantic==2.6.4 + +# Used for Login Forms (FastAPI OAuth2PasswordRequestForm needs this) +python-multipart==0.0.9 + +# Used for pushing outbound notifications (Ntfy, Gotify, Discord etc.) +requests==2.31.0 \ No newline at end of file diff --git a/README.md b/README.md index 8b21b97..44dec63 100644 --- a/README.md +++ b/README.md @@ -37,25 +37,26 @@ There are existing lightweight honeypots, but none feature a clean, SaaS-grade d ![Payload Inspector](screenshots/payload-inspector.png) --- - ## 🔌 The Universal Event Standard (Bring Your Own Sensor) The true power of HoneyWire is that the Hub is **completely sensor-agnostic**. You are not limited to the included Tarpit agent. -By adhering to the **HoneyWire Event Standard**, you can write a script in *any* language (Bash, Go, Rust, Python) to monitor *anything*, and the Sentinel UI will dynamically parse, syntax-highlight, and render your forensic data perfectly. +By adhering to the **HoneyWire Event Standard V1.0**, you can write a script in *any* language (Bash, Go, Rust, Python) to monitor *anything*, and the Sentinel UI will dynamically parse, syntax-highlight, and render your forensic data perfectly. Whether it is a **Deep Packet Inspection (DPI)** engine, a **DNS sinkhole**, a **Canary Token** embedded in a PDF, an **Email Honeypot**, or a simple **TCP Port Tripwire**, just POST this JSON to the Hub: ```json { + "contract_version": "1.0", "sensor_id": "core-dpi-engine", "sensor_type": "deep_packet_inspection", "event_type": "malformed_jwt_detected", "severity": "critical", - "source": "104.28.19.12", - "target": "Auth Gateway", + "timestamp": "2026-04-03T11:30:00Z", "action_taken": "ip_banned", - "details": { + "metadata": { + "source_ip": "104.28.19.12", + "target": "Auth Gateway", "protocol": "TCP", "headers_stripped": true, "payload_sample": [ @@ -65,6 +66,8 @@ Whether it is a **Deep Packet Inspection (DPI)** engine, a **DNS sinkhole**, a * } } ``` +> Note: If you build your sensor using the official HoneyWire Python SDK, this JSON formatting and delivery is handled for you automatically! + *The Hub's Alpine.js frontend will automatically translate arrays into syntax-highlighted code blocks and primitive values into clean metadata tags.* --- @@ -87,8 +90,9 @@ Whether it is a **Deep Packet Inspection (DPI)** engine, a **DNS sinkhole**, a * HoneyWire is split into two independent microservices: -1. **The Hub (`/Hub`)**: The central brain. It runs a FastAPI backend, an SQLite database, and the web dashboard. It runs as a `nonroot` user inside a Distroless container, safely mounting data to a dedicated volume. -2. **The Agent (`/Agent`)**: The decoy sensor (or any custom script you write). It listens on vulnerable ports, traps attackers, and securely POSTs the intrusion data back to the Hub using an `API_SECRET`. +1. `/Hub`: The central brain. It runs a FastAPI backend, an SQLite database, and the web dashboard. It runs as a nonroot user inside a Distroless container, safely mounting data to a dedicated volume. +2. `/Sensors`: The decoy nodes (like the included TCP Tripwire or your custom scripts). They listen on vulnerable ports, trap attackers, and securely POST intrusion data back to the Hub. +3. `/SDKs`: Official libraries (like python-honeywire) that handle secure Hub communication so community developers can easily build new sensors. --- @@ -113,9 +117,9 @@ services: env_file: - .env - agent: - image: ghcr.io/andreicscs/honeywire-agent:latest - container_name: honeywire-agent + tcp-tripwire: + image: ghcr.io/andreicscs/honeywire-tcptripwire:latest + container_name: hw-tcp-tripwire restart: unless-stopped network_mode: "host" # Required to accurately capture port scans against the physical machine env_file: @@ -130,43 +134,37 @@ volumes: # HUB CONFIGURATION # ========================================== # The master password for your fleet to communicate -API_SECRET=super_secret_key_123 +HW_HUB_KEY=super_secret_key_123 # Protect your Web UI (Leave blank for no password) -DASHBOARD_PASSWORD=my_secure_password +HW_DASHBOARD_PASSWORD=my_secure_password # Optional: Push Notifications -NTFY_URL=https://ntfy.sh/your_private_topic -GOTIFY_URL=https://gotify.yourdomain.com/message -GOTIFY_TOKEN=your_app_token -``` -``` +HW_NTFY_URL=[https://ntfy.sh/your_private_topic](https://ntfy.sh/your_private_topic) +HW_GOTIFY_URL=[https://gotify.yourdomain.com/message](https://gotify.yourdomain.com/message) +HW_GOTIFY_TOKEN=your_app_token + + # ========================================== -# AGENT CONFIGURATION +# SENSOR CONFIGURATION # ========================================== # Point this to your Hub's IP address and Port -HUB_URL=http://127.0.0.1:8080 +HW_HUB_ENDPOINT=[http://127.0.0.1:8080](http://127.0.0.1:8080) -# Must match the Hub's secret -API_SECRET=super_secret_key_123 - -# Identify this specific sensor and its IP -SENSOR_ID=dmz-node-01 -SENSOR_IP=192.168.1.50 +# Identify this specific sensor +HW_SENSOR_ID=dmz-node-01 # A comma-separated list of fake ports to open -DECOY_PORTS=21,22,2222,3306,8080 +HW_DECOY_PORTS=21,22,2222,3306,8080 # Tarpit Behavior: 'hold', 'echo', or 'close' -TARPIT_MODE=hold +HW_TARPIT_MODE=hold # UI Color Coding: info|low|medium|high|critical -SEVERITY=high +HW_SEVERITY=high # Fake Service Banner (Use \r\n for line breaks) -# Example SSH: SSH-2.0-OpenSSH_8.2p1 Ubuntu-4ubuntu0.1\r\n -# Example FTP: 220 (vsFTPd 3.0.3)\r\n -TARPIT_BANNER=SSH-2.0-OpenSSH_8.2p1 Ubuntu-4ubuntu0.1\r\n +HW_TARPIT_BANNER=SSH-2.0-OpenSSH_8.2p1 Ubuntu-4ubuntu0.1\r\n ``` ### 3. Start the Trap Run the following command to pull the images and start the honeypot: @@ -210,23 +208,22 @@ nc 2222 ## 📦 Versioning and API reference - HoneyWire now uses a single source of truth version file: `VERSION` in the repo root. -- Runtime version is exposed via env override: `HONEYWIRE_VERSION` (Hub + Agent), and defaults to `VERSION`. +- Runtime version is exposed via env override: HW_VERSION (Hub + Sensors), and defaults to VERSION. - `Hub` endpoint: - `GET /api/v1/version` → returns `{ "version": "1.0.0" }` -- API docs file added: [📖 API.md](./API.md). with full backend route reference and sample payloads. +- API docs file added: [📖 API.md](./Docs/API.md). with full backend route reference and sample payloads. ### API endpoints to know - `GET /api/v1/system/state` / `PATCH /api/v1/system/state` - `GET /api/v1/sensors` - `GET /api/v1/events` - `PATCH /api/v1/events/read`, `PATCH /api/v1/events/{event_id}/read`, `DELETE /api/v1/events` -- `POST /api/v1/heartbeat` (agent heartbeat) -- `POST /api/v1/event` (agent event reports) +- `POST /api/v1/heartbeat` (Sensor heartbeat) +- `POST /api/v1/event` (Sensor event reports) --- ## 🧪 Operational checklist -- set `API_SECRET` for all components -- set optional `DASHBOARD_PASSWORD` -- optionally set `HONEYWIRE_VERSION` to track deployment version +- set `HW_HUB_KEY` for all components +- set optional `HW_DASHBOARD_PASSWORD` - build/redeploy containers after any version bump in `VERSION` or env value diff --git a/SDKs/python-honeywire/honeywire/__init__.py b/SDKs/python-honeywire/honeywire/__init__.py new file mode 100644 index 0000000..ad79c6b --- /dev/null +++ b/SDKs/python-honeywire/honeywire/__init__.py @@ -0,0 +1,146 @@ +import os +import sys +import time +import threading +import datetime +import requests +from abc import ABC, abstractmethod + +class HoneyWireSensor(ABC): + def __init__(self, sensor_type: str): + self.sensor_type = sensor_type + + self.hub_endpoint = os.getenv("HW_HUB_ENDPOINT") + self.hub_key = os.getenv("HW_HUB_KEY") + self.sensor_id = os.getenv("HW_SENSOR_ID") + self.test_mode = os.getenv("HW_TEST_MODE", "false").lower() == "true" + self.agent_version = os.getenv("HONEYWIRE_VERSION", "1.0.0") + self.severity = os.getenv("HW_SEVERITY", "4") + + if not all([self.hub_endpoint, self.hub_key, self.sensor_id]): + print("[!] FATAL: Missing required environment variables (HW_HUB_ENDPOINT, HW_HUB_KEY, HW_SENSOR_ID).") + sys.exit(1) + + self.headers = { + "Authorization": f"Bearer {self.hub_key}", + "Content-Type": "application/json" + } + + self.hub_contract_version = "unknown" + + def _normalize_severity(self, raw_severity) -> str: + """Converts 1-5 or strings into the official schema enum.""" + mapping = { + "1": "info", + "2": "low", + "3": "medium", + "4": "high", + "5": "critical" + } + + val = str(raw_severity).lower().strip() + + if val in mapping: + return mapping[val] + elif val in ["info", "low", "medium", "high", "critical"]: + return val + else: + print(f"[!] Warning: Invalid severity '{raw_severity}'. Defaulting to 'info'.") + return "info" + + def _sync_hub_version(self) -> None: + """Fetches the Hub's contract version synchronously on startup.""" + print(f"[*] Synchronizing with Hub at {self.hub_endpoint}...") + try: + resp = requests.get(f"{self.hub_endpoint}/api/v1/version", headers=self.headers, timeout=5) + resp.raise_for_status() + + self.hub_contract_version = resp.json().get("version", "unknown") + + # Semantic Versioning Check + hub_major = str(self.hub_contract_version).split('.')[0] + agent_major = str(self.agent_version).split('.')[0] + + if hub_major != agent_major and hub_major != "unknown": + print(f"[!] FATAL: Version mismatch. Hub (v{self.hub_contract_version}) vs Agent (v{self.agent_version})") + sys.exit(1) + + print(f"[+] Synchronized successfully. Operating on contract v{self.hub_contract_version}") + except requests.exceptions.RequestException as e: + print(f"[!] FATAL: Failed to synchronize with Hub. Details: {e}") + sys.exit(1) + + def _heartbeat_loop(self) -> None: + """Background thread to ping the Hub every 30 seconds.""" + payload = { + "sensor_id": self.sensor_id, + "sensor_type": self.sensor_type, + "metadata": { + "agent_version": self.agent_version, + "contract_version": self.hub_contract_version + } + } + while True: + try: + requests.post(f"{self.hub_endpoint}/api/v1/heartbeat", headers=self.headers, json=payload, timeout=5) + except Exception as e: + print(f"[-] Heartbeat error: {e}") + time.sleep(30) + + def report_event(self, event_type: str, severity, metadata: dict, action_taken: str = "logged") -> bool: + """Formats and sends the payload enforcing the HoneyWire JSON Schema.""" + normalized_severity = self._normalize_severity(severity) + + payload = { + "contract_version": "1.0", + "sensor_id": self.sensor_id, + "sensor_type": self.sensor_type, + "event_type": event_type, + "severity": normalized_severity, + "timestamp": datetime.datetime.now(datetime.timezone.utc).isoformat(), + "action_taken": action_taken, + "metadata": metadata + } + + try: + resp = requests.post(f"{self.hub_endpoint}/api/v1/alert", headers=self.headers, json=payload, timeout=5) + resp.raise_for_status() + print(f"[+] Alert sent: {event_type} (Severity: {normalized_severity})") + return True + except requests.exceptions.RequestException as e: + print(f"[-] Event report failed: {e}") + return False + + def _run_test_mode(self): + """Used by CI/CD to verify sensor works and exits cleanly.""" + print("🛠️ TEST MODE ACTIVE: Sending synthetic payload...") + success = self.report_event( + event_type="test_mode_synthetic_alert", + severity="info", + metadata={"test_message": "Automated CI/CD check."}, + action_taken="ignored" + ) + if success: + print("✅ Test mode complete. Exiting gracefully.") + sys.exit(0) + else: + print("❌ Test mode failed to contact Hub.") + sys.exit(1) + + @abstractmethod + async def monitor(self): + """The specific sensor logic to be implemented by the creator.""" + pass + + async def start(self): + """Initializes the sensor, runs background threads, and starts the async monitor.""" + self._sync_hub_version() + + if self.test_mode: + self._run_test_mode() + + # Start heartbeat in a standard daemon thread (avoids blocking the async loop) + threading.Thread(target=self._heartbeat_loop, daemon=True).start() + + # Await the creator's async logic + await self.monitor() \ No newline at end of file diff --git a/SDKs/python-honeywire/setup.py b/SDKs/python-honeywire/setup.py new file mode 100644 index 0000000..24887b9 --- /dev/null +++ b/SDKs/python-honeywire/setup.py @@ -0,0 +1,7 @@ +from setuptools import setup, find_packages + +setup( + name='honeywire', + version='1.0.0', + py_modules=find_packages(), +) \ No newline at end of file diff --git a/screenshots/dashboard.png b/Screenshots/dashboard.png similarity index 100% rename from screenshots/dashboard.png rename to Screenshots/dashboard.png diff --git a/screenshots/payload-inspector.png b/Screenshots/payload-inspector.png similarity index 100% rename from screenshots/payload-inspector.png rename to Screenshots/payload-inspector.png diff --git a/Sensors/official/TcpTripWire/.env.example b/Sensors/official/TcpTripWire/.env.example new file mode 100644 index 0000000..a670985 --- /dev/null +++ b/Sensors/official/TcpTripWire/.env.example @@ -0,0 +1,29 @@ +# ========================================== +# HONEYWIRE SENSOR: TCP TARPIT +# ========================================== + +# --- CORE ECOSYSTEM SETTINGS (Required) --- +# Where is the Hub located? (Use your Hub's IP if on a different machine) +HW_HUB_ENDPOINT=http://127.0.0.1:8080 + +# Must match the Hub's HW_HUB_KEY +HW_HUB_KEY=super_secret_key_123 + +# Unique name for this specific sensor +HW_SENSOR_ID=ubuntu-vps-01 + +# Alert Severity: 1-5 OR info|low|medium|high|critical +HW_SEVERITY=high + + +# --- SENSOR SPECIFIC SETTINGS --- +# The fake ports this agent will listen on +HW_DECOY_PORTS=21,2222,3306 + +# Modes: 'echo' (repeat data), 'hold' (do nothing, keep open), 'close' (drop immediately) +HW_TARPIT_MODE=hold + +# The fake service banner. +# Example for SSH: SSH-2.0-OpenSSH_8.2p1 Ubuntu-4ubuntu0.1\r\n +# Example for FTP: 220 (vsFTPd 3.0.3)\r\n +HW_TARPIT_BANNER=SSH-2.0-OpenSSH_8.2p1 Ubuntu-4ubuntu0.1\r\n \ No newline at end of file diff --git a/Sensors/official/TcpTripWire/Dockerfile b/Sensors/official/TcpTripWire/Dockerfile new file mode 100644 index 0000000..a7153fa --- /dev/null +++ b/Sensors/official/TcpTripWire/Dockerfile @@ -0,0 +1,36 @@ +# --- Stage 1: Builder --- +FROM python:3.11-slim AS builder +WORKDIR /app + +# 1. Install git (Required for pip to pull the HoneyWire SDK from GitHub) +RUN apt-get update && apt-get install -y git && rm -rf /var/lib/apt/lists/* + +# 2. Create a virtual environment +RUN python -m venv /opt/venv +ENV PATH="/opt/venv/bin:$PATH" + +# 3. Copy requirements and install +# (This fetches requests, aiohttp, and the SDK directly into the venv) +COPY requirements.txt . +RUN pip install --no-cache-dir --upgrade pip && \ + pip install --no-cache-dir -r requirements.txt + +# --- Stage 2: Distroless (Production) --- +FROM gcr.io/distroless/python3-debian12 +WORKDIR /app + +# 1. Copy the compiled virtual environment from the builder +COPY --from=builder /opt/venv /opt/venv + +# 2. Set environment variables so Python knows where the packages are +ENV PYTHONPATH="/opt/venv/lib/python3.11/site-packages" +ENV PATH="/opt/venv/bin:$PATH" +# Ensures Python print() statements go straight to Docker logs without delay +ENV PYTHONUNBUFFERED=1 + +# 3. Copy your specific sensor script +COPY TcpTripWire.py . + +# Notice: We intentionally do NOT use a nonroot user here. +# We remain 'root' to allow binding to privileged decoy ports (e.g., Port 22, 80). +CMD ["TcpTripWire.py"] \ No newline at end of file diff --git a/Sensors/official/TcpTripWire/README.md b/Sensors/official/TcpTripWire/README.md new file mode 100644 index 0000000..27d1ee2 --- /dev/null +++ b/Sensors/official/TcpTripWire/README.md @@ -0,0 +1,70 @@ +# HoneyWire Official Sensor: TCP TripWire 🕸️ + +The TCP TripWire is a high-fidelity, low-interaction honeypot designed to detect network reconnaissance and brute-force attempts. It acts as a "Tarpit," binding to decoy ports and intentionally stalling attackers to waste their time while silently extracting their IP and payload data to report to the HoneyWire Hub. + +## Features +* **Zero-Setup SDK Integration:** Natively built on the HoneyWire Python SDK. +* **Tarpit Modes:** Supports `hold` (silent stall), `echo` (repeat data back), or `close` (immediate drop). +* **Forensic Capture:** Safely buffers up to 10 lines of payload data without risking memory exhaustion. +* **Distroless Container:** Runs as a hardened, Distroless Docker image to prevent container breakouts. + +## Configuration + +All configuration is handled via Environment Variables. Copy the `.env.example` file to `.env` before running. + +### Core Ecosystem Variables (Required) +| Variable | Description | Example | +|---|---|---| +| `HW_HUB_ENDPOINT` | The URL of your central HoneyWire Hub. | `http://192.168.1.100:8080` | +| `HW_HUB_KEY` | The shared secret API key to authenticate with the Hub. | `super_secret_key_123` | +| `HW_SENSOR_ID` | A unique identifier for this specific trap. | `dmz-ssh-tarpit-01` | + +### Sensor-Specific Variables +| Variable | Description | Default | +|---|---|---| +| `HW_DECOY_PORTS` | Comma-separated list of TCP ports to monitor. | `2222,3306` | +| `HW_TARPIT_MODE` | The behavior of the trap: `hold`, `echo`, or `close`. | `hold` | +| `HW_SEVERITY` | Alert severity sent to the Hub (`info` to `critical`). | `high` | +| `HW_TARPIT_BANNER` | (Optional) A fake service banner to send on connect. | `SSH-2.0-OpenSSH_8.2p1` | + +## Tarpit Modes Explained +* **`hold` (Default):** The sensor accepts the connection but sends nothing. It holds the TCP socket open as long as possible, draining the attacker's resources and slowing down automated scanners like Nmap. +* **`echo`:** The sensor acts as an echo server, repeating whatever the attacker sends back to them. Useful for confusing automated scripts. +* **`close`:** The sensor logs the connection, captures the initial payload, and forcefully closes the socket. + +## Deployment + +The easiest way to deploy this sensor is via Docker Compose. Note that if you intend to bind to privileged ports (any port under 1024, like Port 22 or 80), the container must run as `root` (which is the default). + +### 1. Using Docker Compose +Create a `docker-compose.yml` file: + +```yaml +services: + tcp-tripwire: + image: ghcr.io/andreicscs/honeywire-tcptripwire:latest + container_name: hw-tcp-tripwire + restart: unless-stopped + + # CRITICAL: Binds directly to the host machine's network. + # 1. Preserves the real source IP of the attacker. + # 2. Automatically exposes whatever ports are set in HW_DECOY_PORTS. + network_mode: "host" + + env_file: + - .env +``` +Run it in the background: +```Bash +docker-compose up -d +``` + +### 2. Testing Locally (Build from Source) +If you are developing or testing locally: +```Bash +docker build -t honeywire-tcptripwire . +docker run --rm --network host --env-file .env honeywire-tcptripwire +``` + +## Security Note +This sensor uses asyncio with strict memory limits (50KB or 10 lines per connection) and a global semaphore to limit concurrent connections. This ensures the sensor cannot be easily DOS'd or used to exhaust the host machine's File Descriptors. \ No newline at end of file diff --git a/Sensors/official/TcpTripWire/TcpTripWire.py b/Sensors/official/TcpTripWire/TcpTripWire.py new file mode 100644 index 0000000..be4c23b --- /dev/null +++ b/Sensors/official/TcpTripWire/TcpTripWire.py @@ -0,0 +1,131 @@ +""" +HoneyWire Tarpit Agent (Standard Port Tripwire) +Sensor type: tarpit + +Custom Environment variables: + DECOY_PORTS Comma-separated ports (default: 2222,3306) + TARPIT_MODE hold | echo | close (default: hold) +""" +import asyncio +import os +import sys +import time + +# Import the SDK +from honeywire import HoneyWireSensor + + +class TcpTarpitSensor(HoneyWireSensor): + def __init__(self): + # Initialize the base class with the sensor type + super().__init__(sensor_type="tarpit") + + # Sensor-specific configuration + self.tarpit_mode = os.getenv("HW_TARPIT_MODE", "hold").lower() + + raw_ports = os.getenv("HW_DECOY_PORTS", "2222,3306") + self.decoy_ports = [int(p.strip()) for p in raw_ports.split(",") if p.strip()] + + raw_banner = os.getenv("HW_TARPIT_BANNER", "") + self.tarpit_banner = raw_banner.encode("utf-8").decode("unicode_escape").encode("utf-8") if raw_banner else b"" + + self.max_bytes = 1024 * 50 + self.max_lines = 10 + self.max_duration = 3600 + self.semaphore = asyncio.Semaphore(1000) + + async def handle_client(self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter, port: int) -> None: + addr = writer.get_extra_info("peername") + source_ip = addr[0] if addr else "Unknown" + + start_time = time.time() + payload_data = [] + total_bytes = 0 + + try: + if self.tarpit_banner and self.tarpit_mode != "close": + writer.write(self.tarpit_banner) + await writer.drain() + + if self.tarpit_mode != "close": + while total_bytes < self.max_bytes and (time.time() - start_time) < self.max_duration: + try: + timeout = 300.0 + data = await asyncio.wait_for(reader.read(1024), timeout=timeout) + + if not data: break + + decoded = data.decode("utf-8", errors="replace").strip() + if decoded and len(payload_data) < self.max_lines: + payload_data.append(decoded) + + total_bytes += len(data) + + if self.tarpit_mode == "echo": + writer.write(data) + await writer.drain() + + await asyncio.sleep(0.5) + + except asyncio.TimeoutError: + if not writer.is_closing(): + writer.write(b"\0") + await writer.drain() + continue + + except Exception: + pass + finally: + try: + writer.close() + await writer.wait_closed() + except Exception: + pass + + duration = time.time() - start_time + + metadata = { + "source_ip": source_ip, + "target_port": port, + "duration_sec": round(duration, 2), + "payload": payload_data + } + + # Send event! + # Use asyncio.to_thread because report_event uses blocking requests + await asyncio.to_thread( + self.report_event, + event_type="tcp_connection", + severity=self.severity, + metadata=metadata, + action_taken=self.tarpit_mode + ) + + async def connection_wrapper(self, reader, writer, port): + async with self.semaphore: + await self.handle_client(reader, writer, port) + + async def monitor(self): + """This is the required method from the SDK.""" + print(f"[*] HoneyWire Agent | Mode: {self.tarpit_mode.upper()} | Severity: {self.severity}") + + servers = [] + for port in self.decoy_ports: + server = await asyncio.start_server( + lambda r, w, p=port: self.connection_wrapper(r, w, p), + "0.0.0.0", + port + ) + servers.append(server.serve_forever()) + print(f"[+] Tarpit listening on port {port}") + + await asyncio.gather(*servers) + + +if __name__ == "__main__": + sensor = TcpTarpitSensor() + try: + # SDK's start() handles heartbeat, version sync, and calls monitor() + asyncio.run(sensor.start()) + except KeyboardInterrupt: + print("\n[*] Shutting down HoneyWire Tarpit...") \ No newline at end of file diff --git a/Sensors/official/TcpTripWire/docker-compose.yml b/Sensors/official/TcpTripWire/docker-compose.yml new file mode 100644 index 0000000..b73cfe0 --- /dev/null +++ b/Sensors/official/TcpTripWire/docker-compose.yml @@ -0,0 +1,14 @@ +services: + tcp-tripwire: + # Use 'build: .' if testing locally, or pull from a registry later + build: . + container_name: hw-tcp-tripwire + restart: unless-stopped + + # CRITICAL: Binds directly to the host machine's network. + # 1. Preserves the real source IP of the attacker. + # 2. Automatically exposes whatever ports are set in HW_DECOY_PORTS. + network_mode: "host" + + env_file: + - .env \ No newline at end of file diff --git a/Sensors/official/TcpTripWire/requirements.txt b/Sensors/official/TcpTripWire/requirements.txt new file mode 100644 index 0000000..1ecaaa7 --- /dev/null +++ b/Sensors/official/TcpTripWire/requirements.txt @@ -0,0 +1,6 @@ +# requirements.txt +aiohttp==3.9.3 +requests==2.31.0 + +# Install the official HoneyWire SDK directly from the main branch +git+https://github.com/andreicscs/HoneyWire.git@main#subdirectory=SDKs/python-honeywire \ No newline at end of file diff --git a/Sensors/templates/python-sensor/.env.example b/Sensors/templates/python-sensor/.env.example new file mode 100644 index 0000000..4990c2c --- /dev/null +++ b/Sensors/templates/python-sensor/.env.example @@ -0,0 +1,15 @@ +# ========================================== +# HONEYWIRE SENSOR CONFIGURATION +# ========================================== + +# --- REQUIRED ECOSYSTEM SETTINGS --- +HW_HUB_ENDPOINT=http://192.168.1.100:8080 +HW_HUB_KEY=super_secret_key_123 +HW_SENSOR_ID=my-custom-sensor-01 + +# --- CUSTOM SENSOR SETTINGS --- +# Alert Severity: 1-5 OR info|low|medium|high|critical +HW_SEVERITY=high + +# Add your custom variables here: +# HW_TARGET_PATH=/tmp/honey \ No newline at end of file diff --git a/Sensors/templates/python-sensor/Dockerfile b/Sensors/templates/python-sensor/Dockerfile new file mode 100644 index 0000000..44afd3b --- /dev/null +++ b/Sensors/templates/python-sensor/Dockerfile @@ -0,0 +1,32 @@ +# --- Stage 1: Builder --- +FROM python:3.11-slim AS builder +WORKDIR /app + +# Install git (Required to fetch the SDK from GitHub) +RUN apt-get update && apt-get install -y git && rm -rf /var/lib/apt/lists/* + +# Create virtual environment +RUN python -m venv /opt/venv +ENV PATH="/opt/venv/bin:$PATH" + +# Install requirements (This pulls the SDK + your custom libraries) +COPY requirements.txt . +RUN pip install --no-cache-dir --upgrade pip && \ + pip install --no-cache-dir -r requirements.txt + +# --- Stage 2: Distroless (Production) --- +FROM gcr.io/distroless/python3-debian12 +WORKDIR /app + +# Copy the compiled virtual environment from the builder +COPY --from=builder /opt/venv /opt/venv + +ENV PYTHONPATH="/opt/venv/lib/python3.11/site-packages" +ENV PATH="/opt/venv/bin:$PATH" +ENV PYTHONUNBUFFERED=1 + +# Copy the custom sensor script +COPY sensor.py . + +# Run the sensor +CMD ["sensor.py"] \ No newline at end of file diff --git a/Sensors/templates/python-sensor/README.md b/Sensors/templates/python-sensor/README.md new file mode 100644 index 0000000..68fca60 --- /dev/null +++ b/Sensors/templates/python-sensor/README.md @@ -0,0 +1,26 @@ +# HoneyWire Python Sensor Template + +Welcome to the HoneyWire ecosystem! This template contains everything you need to build a custom, Dockerized security sensor that natively reports to the HoneyWire Hub. + +## How to Build Your Sensor + +1. **Copy this folder** and rename it to your sensor's name (e.g., `ssh_watcher`). +2. **Write your logic** inside `sensor.py`. The file is heavily commented and shows you exactly where to put your code. +3. **Add dependencies**: If your script needs extra Python libraries (like `scapy` or `paramiko`), add them to `requirements.txt`. +4. **Update `.env.example`**: Add any custom `HW_` variables your sensor needs. + +## Testing Your Sensor + +You can test your sensor locally by running: + +```bash +# Copy the example environment file and fill in your Hub details +cp .env.example .env + +# Build and run the Distroless Docker container +docker build -t honeywire-custom-sensor . +docker run --rm --env-file .env honeywire-custom-sensor +``` + +## CI/CD Requirement +To ensure your sensor works, our GitHub Actions will run it with HW_TEST_MODE=true. The base SDK handles this automatically, so you don't need to write any test logic! Just ensure you don't override the start() method in the base class. \ No newline at end of file diff --git a/Sensors/templates/python-sensor/requirements.txt b/Sensors/templates/python-sensor/requirements.txt new file mode 100644 index 0000000..61c4abf --- /dev/null +++ b/Sensors/templates/python-sensor/requirements.txt @@ -0,0 +1,8 @@ +# This single line installs the official HoneyWire SDK directly from GitHub. +# It automatically fetches required libraries like 'requests' and 'aiohttp'. +git+https://github.com/andreicscs/HoneyWire.git@main#subdirectory=SDKs/python-honeywire + +# --- ADD YOUR SENSOR's SPECIFIC LIBRARIES BELOW --- +# Example: +# watchdog==4.0.0 +# scapy==2.5.0 \ No newline at end of file diff --git a/Sensors/templates/python-sensor/sensor.py b/Sensors/templates/python-sensor/sensor.py new file mode 100644 index 0000000..ab93689 --- /dev/null +++ b/Sensors/templates/python-sensor/sensor.py @@ -0,0 +1,60 @@ +import os +import asyncio +import time + +# Import the official HoneyWire SDK +from honeywire import HoneyWireSensor + +class MyCustomSensor(HoneyWireSensor): + def __init__(self): + # Initialize the base class. + # Change 'custom' to whatever category fits your sensor (e.g., 'network', 'file', 'auth') + super().__init__(sensor_type="custom") + + # Add any custom configuration from your .env file here + # Example: self.target_path = os.getenv("HW_TARGET_PATH", "/tmp/honey") + self.severity = os.getenv("HW_SEVERITY", "medium") + + async def monitor(self): + """ + REQUIRED METHOD: This is the main loop of your sensor. + Write your detection logic here. Do NOT block the async loop. + """ + print(f"[*] Starting Custom Sensor | Severity: {self.severity}") + + try: + while True: + # --- YOUR SENSOR LOGIC GOES HERE --- + # Example: Wait for an event... + await asyncio.sleep(60) + + # Assume an attack happened! + print("[!] Attack detected! Gathering forensics...") + + # Format your specific forensic data + metadata = { + "source_ip": "192.168.1.100", # Replace with actual data + "attack_type": "example_probe", + "raw_payload": "GET /etc/passwd HTTP/1.1" + } + + # Send the alert to the Hub using the SDK's built-in method. + # Wrap it in asyncio.to_thread to prevent blocking your async monitor loop. + await asyncio.to_thread( + self.report_event, + event_type="custom_anomaly_detected", + severity=self.severity, + metadata=metadata, + action_taken="logged" + ) + + except asyncio.CancelledError: + print("[*] Monitor loop cancelled. Shutting down gracefully.") + +if __name__ == "__main__": + sensor = MyCustomSensor() + try: + # The SDK's start() method handles connecting to the Hub and running your monitor() + asyncio.run(sensor.start()) + except KeyboardInterrupt: + print("\n[*] Shutting down custom sensor...") \ No newline at end of file