mirror of
https://github.com/andreicscs/HoneyWire
synced 2026-06-26 12:39:53 +00:00
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:
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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}
|
||||
|
||||
|
||||
@@ -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}")
|
||||
|
||||
@@ -3,5 +3,5 @@ from setuptools import setup, find_packages
|
||||
setup(
|
||||
name='honeywire',
|
||||
version='1.0.0',
|
||||
py_modules=find_packages(),
|
||||
packages=find_packages(),
|
||||
)
|
||||
@@ -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
|
||||
|
||||
@@ -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"]
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user