feat: add HW_HUB_PORT to .env.example and update docker-compose.yml to use it

refactor: enhance agent authentication in main.py to support Bearer tokens

fix: correct setup.py to use packages instead of py_modules

chore: update TcpTripWire sensor to use new environment variable names and improve connection handling

build: modify Dockerfile for better SDK integration and streamline installation process

docs: clean up requirements.txt and remove unnecessary comments
This commit is contained in:
andreicscs
2026-04-03 12:30:42 +02:00
parent c2168144e4
commit a3f4460e1e
10 changed files with 162 additions and 114 deletions
+2
View File
@@ -2,6 +2,8 @@
# HONEYWIRE HUB CONFIGURATION
# ==========================================
HW_HUB_PORT=8080
# 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
+1 -1
View File
@@ -4,7 +4,7 @@ services:
container_name: honeywire-hub
restart: unless-stopped
ports:
- "${HUB_PORT}:8080"
- "${HW_HUB_PORT}:8080"
volumes:
- honeywire_data:/data
env_file:
+10 -3
View File
@@ -269,9 +269,16 @@ def verify_ui_auth(request: Request) -> None:
raise HTTPException(status_code=401, detail="Session Expired")
def verify_agent_auth(x_api_key: str = Header(None)) -> None:
def verify_agent_auth(x_api_key: str = Header(None), authorization: str = Header(None)) -> None:
"""Validates sensor API keys using constant-time comparison."""
if not x_api_key or not secrets.compare_digest(x_api_key, API_SECRET):
token = None
if x_api_key:
token = x_api_key
elif authorization and authorization.startswith("Bearer "):
token = authorization.split(" ", 1)[1].strip()
if not token or not secrets.compare_digest(token, API_SECRET):
raise HTTPException(status_code=401, detail="Invalid API key")
@@ -295,7 +302,7 @@ async def set_system_state(state: SystemState):
return {"status": "success", "is_armed": state.is_armed}
@app.get("/api/v1/version", dependencies=[Depends(verify_ui_auth)])
@app.get("/api/v1/version", dependencies=[Depends(verify_agent_auth)])
async def get_version():
return {"version": HONEYWIRE_VERSION}
+36 -12
View File
@@ -6,27 +6,36 @@ import datetime
import requests
from abc import ABC, abstractmethod
SDK_DEFAULT_AGENT_VERSION = "1.0.0"
HONEYWIRE_SCHEMA_VERSION = "1.0"
HEARTBEAT_INTERVAL_SECONDS = 30
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.agent_version = os.getenv("HONEYWIRE_VERSION", SDK_DEFAULT_AGENT_VERSION)
self.severity = os.getenv("HW_SEVERITY", "4")
self._validate_required_env()
self.headers = self._build_headers()
self.hub_contract_version = "unknown"
def _validate_required_env(self):
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 = {
def _build_headers(self) -> dict:
return {
"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."""
@@ -70,6 +79,10 @@ class HoneyWireSensor(ABC):
print(f"[!] FATAL: Failed to synchronize with Hub. Details: {e}")
sys.exit(1)
def _post_to_hub(self, path: str, payload: dict, timeout: int = 5):
url = f"{self.hub_endpoint}{path}"
return requests.post(url, headers=self.headers, json=payload, timeout=timeout)
def _heartbeat_loop(self) -> None:
"""Background thread to ping the Hub every 30 seconds."""
payload = {
@@ -77,17 +90,26 @@ class HoneyWireSensor(ABC):
"sensor_type": self.sensor_type,
"metadata": {
"agent_version": self.agent_version,
"contract_version": self.hub_contract_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)
resp = self._post_to_hub("/api/v1/heartbeat", payload)
resp.raise_for_status()
except Exception as e:
print(f"[-] Heartbeat error: {e}")
time.sleep(30)
time.sleep(HEARTBEAT_INTERVAL_SECONDS)
def report_event(self, event_type: str, severity, metadata: dict, action_taken: str = "logged") -> bool:
def report_event(
self,
event_type: str,
severity,
metadata: dict,
action_taken: str = "logged",
source: str = "Unknown",
target: str = "Unknown",
) -> bool:
"""Formats and sends the payload enforcing the HoneyWire JSON Schema."""
normalized_severity = self._normalize_severity(severity)
@@ -99,13 +121,15 @@ class HoneyWireSensor(ABC):
"severity": normalized_severity,
"timestamp": datetime.datetime.now(datetime.timezone.utc).isoformat(),
"action_taken": action_taken,
"metadata": metadata
"source": source,
"target": target,
"details": metadata
}
try:
resp = requests.post(f"{self.hub_endpoint}/api/v1/alert", headers=self.headers, json=payload, timeout=5)
resp = self._post_to_hub("/api/v1/event", payload)
resp.raise_for_status()
print(f"[+] Alert sent: {event_type} (Severity: {normalized_severity})")
print(f"[+] Event sent: {event_type} (Severity: {normalized_severity})")
return True
except requests.exceptions.RequestException as e:
print(f"[-] Event report failed: {e}")
+1 -1
View File
@@ -3,5 +3,5 @@ from setuptools import setup, find_packages
setup(
name='honeywire',
version='1.0.0',
py_modules=find_packages(),
packages=find_packages(),
)
+1 -1
View File
@@ -7,7 +7,7 @@
HW_HUB_ENDPOINT=http://127.0.0.1:8080
# Must match the Hub's HW_HUB_KEY
HW_HUB_KEY=super_secret_key_123
HW_HUB_KEY=change_this_to_a_secure_random_string
# Unique name for this specific sensor
HW_SENSOR_ID=ubuntu-vps-01
+19 -20
View File
@@ -2,35 +2,34 @@
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"
# 1. Copy the SDK source
COPY SDKs/python-honeywire /tmp/sdk_src
# 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
# 2. Build a WHEEL (a static redistribution file) instead of installing directly.
# This ensures all code is physically packaged and not symlinked.
RUN pip install --no-cache-dir wheel && \
pip wheel /tmp/sdk_src --wheel-dir /tmp/wheels
# 3. Install the wheel and the requirements into a specific folder
# We use --target to ensure everything is in one flat, movable directory.
COPY Sensors/official/TcpTripWire/requirements.txt .
RUN pip install --no-cache-dir --target /app/lib /tmp/wheels/honeywire-*.whl -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
# 4. Copy the FLAT library folder. No symlinks, just code.
COPY --from=builder /app/lib /app/lib
# 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
# 5. Copy the sensor script
COPY Sensors/official/TcpTripWire/TcpTripWire.py .
# 6. Tell Python to look in our lib folder first.
ENV PYTHONPATH="/app/lib"
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).
# Distroless runs /usr/bin/python3 by default
CMD ["TcpTripWire.py"]
+87 -67
View File
@@ -3,44 +3,66 @@ 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)
HW_DECOY_PORTS Comma-separated ports (default: 2222,3306)
HW_TARPIT_MODE hold | echo | close (default: hold)
HW_TARPIT_BANNER Optional service banner
"""
import asyncio
import os
import sys
import time
from typing import List, Optional
# Import the SDK
from honeywire import HoneyWireSensor
DEFAULT_DECOY_PORTS = [2222, 3306]
DEFAULT_TARPIT_MODE = "hold"
DEFAULT_MAX_BYTES = 1024 * 50
DEFAULT_MAX_LINES = 10
DEFAULT_MAX_DURATION = 3600
DEFAULT_CONCURRENCY = 1000
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)
self.tarpit_mode = os.getenv("HW_TARPIT_MODE", DEFAULT_TARPIT_MODE).lower()
self.decoy_ports = self._parse_ports(os.getenv("HW_DECOY_PORTS", ",".join(map(str, DEFAULT_DECOY_PORTS))))
self.tarpit_banner = self._load_banner(os.getenv("HW_TARPIT_BANNER", ""))
self.max_bytes = DEFAULT_MAX_BYTES
self.max_lines = DEFAULT_MAX_LINES
self.max_duration = DEFAULT_MAX_DURATION
self.semaphore = asyncio.Semaphore(DEFAULT_CONCURRENCY)
@staticmethod
def _parse_ports(raw_ports: str) -> List[int]:
ports = []
for item in raw_ports.split(","):
item = item.strip()
if not item:
continue
try:
ports.append(int(item))
except ValueError:
print(f"[!] Invalid port in HW_DECOY_PORTS: {item}")
return ports or DEFAULT_DECOY_PORTS
@staticmethod
def _load_banner(raw_banner: str) -> bytes:
if not raw_banner:
return b""
return raw_banner.encode("utf-8").decode("unicode_escape").encode("utf-8")
async def _capture_connection(self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter, port: int):
peer = writer.get_extra_info("peername")
source_ip = peer[0] if peer else "Unknown"
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
captured_payload = []
consumed_bytes = 0
try:
if self.tarpit_banner and self.tarpit_mode != "close":
@@ -48,84 +70,82 @@ class TcpTarpitSensor(HoneyWireSensor):
await writer.drain()
if self.tarpit_mode != "close":
while total_bytes < self.max_bytes and (time.time() - start_time) < self.max_duration:
while consumed_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
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 decoded and len(captured_payload) < self.max_lines:
captured_payload.append(decoded)
consumed_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
except Exception as exc:
print(f"[!] Connection processing error from {source_ip}:{port} - {exc}")
finally:
try:
writer.close()
await writer.wait_closed()
except Exception:
pass
if not writer.is_closing():
try:
writer.close()
await writer.wait_closed()
except Exception:
pass
duration = time.time() - start_time
metadata = {
event_payload = {
"source_ip": source_ip,
"target_port": port,
"duration_sec": round(duration, 2),
"payload": payload_data
"payload": captured_payload,
}
# Send event!
# Use asyncio.to_thread because report_event uses blocking requests
await asyncio.to_thread(
self.report_event,
self.report_event,
event_type="tcp_connection",
severity=self.severity,
metadata=metadata,
action_taken=self.tarpit_mode
metadata=event_payload,
action_taken=self.tarpit_mode,
source=source_ip,
target=f"Port {port}",
)
async def connection_wrapper(self, reader, writer, port):
async def _serve_connection(self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter, port: int):
async with self.semaphore:
await self.handle_client(reader, writer, port)
await self._capture_connection(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 = []
listeners = []
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
listener = await asyncio.start_server(
lambda r, w, p=port: self._serve_connection(r, w, p),
"0.0.0.0",
port,
)
servers.append(server.serve_forever())
listeners.append(listener.serve_forever())
print(f"[+] Tarpit listening on port {port}")
await asyncio.gather(*servers)
await asyncio.gather(*listeners)
if __name__ == "__main__":
sensor = TcpTarpitSensor()
try:
# SDK's start() handles heartbeat, version sync, and calls monitor()
asyncio.run(sensor.start())
asyncio.run(sensor.start())
except KeyboardInterrupt:
print("\n[*] Shutting down HoneyWire Tarpit...")
@@ -1,13 +1,12 @@
services:
tcp-tripwire:
# Use 'build: .' if testing locally, or pull from a registry later
build: .
build:
context: ../../../
dockerfile: Sensors/official/TcpTripWire/Dockerfile
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.
# Binds directly to the host machine's network
network_mode: "host"
env_file:
@@ -1,6 +1,3 @@
# 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
requests==2.31.0