mirror of
https://github.com/andreicscs/HoneyWire
synced 2026-06-26 12:39:53 +00:00
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
This commit is contained in:
@@ -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
|
||||
@@ -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 }}
|
||||
@@ -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
|
||||
+19
-3
@@ -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
|
||||
# =========================
|
||||
# OS & IDE Clutter
|
||||
# =========================
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
.vscode/
|
||||
|
||||
# (Notice: .dockerignore is intentionally NOT here. It must be tracked by Git!)
|
||||
@@ -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
|
||||
@@ -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"]
|
||||
-222
@@ -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...")
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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 /"],
|
||||
}
|
||||
}
|
||||
```
|
||||
+19
-9
@@ -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
|
||||
# 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=
|
||||
+16
-8
@@ -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"
|
||||
|
||||
|
||||
+15
-5
@@ -1,5 +1,15 @@
|
||||
fastapi
|
||||
uvicorn
|
||||
pydantic
|
||||
requests
|
||||
jinja2
|
||||
# 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
|
||||
@@ -37,25 +37,26 @@ There are existing lightweight honeypots, but none feature a clean, SaaS-grade d
|
||||

|
||||
|
||||
---
|
||||
|
||||
## 🔌 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 <agent-ip> 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
|
||||
|
||||
@@ -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()
|
||||
@@ -0,0 +1,7 @@
|
||||
from setuptools import setup, find_packages
|
||||
|
||||
setup(
|
||||
name='honeywire',
|
||||
version='1.0.0',
|
||||
py_modules=find_packages(),
|
||||
)
|
||||
|
Before Width: | Height: | Size: 368 KiB After Width: | Height: | Size: 368 KiB |
|
Before Width: | Height: | Size: 103 KiB After Width: | Height: | Size: 103 KiB |
@@ -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
|
||||
@@ -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"]
|
||||
@@ -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.
|
||||
@@ -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...")
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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"]
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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...")
|
||||
Reference in New Issue
Block a user