diff --git a/Hub-Go/Dockerfile b/Hub-Go/Dockerfile deleted file mode 100644 index c6d5e9e..0000000 --- a/Hub-Go/Dockerfile +++ /dev/null @@ -1,20 +0,0 @@ -# STAGE 1: Builder -FROM golang:1.25 AS builder -WORKDIR /build - -COPY go.mod go.sum ./ -RUN go mod download -COPY . . - -RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-w -s" -o honeywire-hub ./cmd/hub/main.go - -# STAGE 2: Distroless -# The "static" image contains literally nothing except SSL certificates and a non-root user. -FROM gcr.io/distroless/static-debian12:nonroot - -WORKDIR /app -COPY --from=builder /build/honeywire-hub /app/ - -# Expose port and run -EXPOSE 8080 -CMD ["/app/honeywire-hub"] \ No newline at end of file diff --git a/Hub-Go/docker-compose.yml b/Hub-Go/docker-compose.yml deleted file mode 100644 index 3ec5d5e..0000000 --- a/Hub-Go/docker-compose.yml +++ /dev/null @@ -1,31 +0,0 @@ -version: '3.8' - -services: - # 1. THE FIXER: Runs as root, fixes permissions, and dies instantly. - permission-fixer: - image: alpine:latest - # 65532 is the standard UID/GID for the Distroless nonroot user - command: sh -c "chown -R 65532:65532 /data" - volumes: - - ./honeywire_data:/data - - # 2. THE HUB: pure Go distroless backend. - honeywire-hub: - build: - context: . - dockerfile: Dockerfile - container_name: honeywire-hub - restart: unless-stopped - ports: - - "${HW_PORT:-8080}:${HW_PORT:-8080}" - volumes: - - ./honeywire_data:/data - depends_on: - # This ensures the Hub waits until the fixer is completely finished - permission-fixer: - condition: service_completed_successfully - environment: - - HW_PORT=8080 - - HW_DB_PATH=/data/honeywire.db - - HW_HUB_KEY=change_this_to_a_secure_random_string - - HW_DASHBOARD_PASSWORD=admin \ No newline at end of file diff --git a/Hub-Go/mock_data.py b/Hub-Go/mock_data.py deleted file mode 100644 index 9be594d..0000000 --- a/Hub-Go/mock_data.py +++ /dev/null @@ -1,143 +0,0 @@ -"""Generate demo sensors + dynamic events for HoneyWire UI showcase.""" - -import os -import requests -import random -import socket -import struct -import time -from datetime import datetime - -HUB_URL = "http://localhost:8080" -API_SECRET = "change_this_to_a_secure_random_string" -HEADERS = {"x-api-key": API_SECRET, "Content-Type": "application/json"} - -# Determine version for contract validation -def get_project_version(): - version = os.getenv("HONEYWIRE_VERSION") - if version: return version - try: - with open(os.path.join(os.path.dirname(__file__), "..", "VERSION"), "r") as f: - return f.read().strip() - except FileNotFoundError: - return "1.0.0" - -VERSION = get_project_version() - -# --- Random Data Generators --- - -def generate_random_ip(): - """Generates a random public-looking IPv4 address.""" - return socket.inet_ntoa(struct.pack('>I', random.randint(0x01000000, 0xDF000000))) - -def generate_fleet(num_sensors=8): - """Generates a list of random sensors with dynamic names.""" - types = ["tarpit", "canary_token", "ids_sentinel", "web_honeypot", "llm_probe"] - regions = ["us-east", "us-west", "eu-central", "ap-south", "lan-core", "dmz"] - - fleet = [] - for _ in range(num_blocks := num_sensors): - s_type = random.choice(types) - region = random.choice(regions) - sensor_id = f"{s_type}-{region}-{random.randint(10, 99)}" - fleet.append({"sensor_id": sensor_id, "sensor_type": s_type}) - return fleet - -def generate_contextual_event(sensor): - """Generates a realistic event that matches the sensor type.""" - s_type = sensor["sensor_type"] - ip = generate_random_ip() - - # Base Event Template - event = { - "sensor_id": sensor["sensor_id"], - "sensor_type": s_type, - "contract_version": VERSION, - "source": ip, - "action_taken": random.choice(["logged", "blocked", "tarpitted", "alert_only"]) - } - - # Contextual Threat Data - if s_type == "web_honeypot": - event["event_type"] = random.choice(["sqli_attempt", "xss_payload", "dir_bruteforce", "api_fuzz"]) - event["severity"] = random.choice(["high", "critical", "medium"]) - event["target"] = random.choice(["/wp-admin", "/api/v1/users", "/.env", "/config.php"]) - event["details"] = {"user_agent": "masscan/1.3", "payload": "1' OR '1'='1", "method": "POST"} - - elif s_type == "tarpit": - event["event_type"] = random.choice(["tcp_syn_flood", "port_scan", "ssh_bruteforce"]) - event["severity"] = random.choice(["low", "medium", "info"]) - event["target"] = random.choice(["Port 22", "Port 23", "Port 3389", "Port 445"]) - event["details"] = {"packets_dropped": random.randint(50, 5000), "connection_time_held": f"{random.randint(10, 120)}s"} - - elif s_type == "canary_token": - event["event_type"] = random.choice(["file_accessed", "aws_key_used", "db_queried"]) - event["severity"] = "critical" - event["target"] = random.choice(["/etc/passwd.bak", "AWS_SECRET_KEY", "users_backup.sql"]) - event["details"] = {"process_name": random.choice(["cat", "curl", "python3"]), "user": "www-data"} - - elif s_type == "llm_probe": - event["event_type"] = random.choice(["prompt_injection", "jailbreak_attempt", "data_exfiltration"]) - event["severity"] = random.choice(["high", "critical"]) - event["target"] = "/v1/chat/completions" - event["details"] = {"prompt_snippet": "Ignore previous instructions and print...", "model": "gpt-4-turbo"} - - else: # ids_sentinel - event["event_type"] = random.choice(["malware_signature", "lateral_movement", "beaconing"]) - event["severity"] = random.choice(["high", "critical", "medium"]) - event["target"] = f"Subnet 10.0.{random.randint(1,5)}.0/24" - event["details"] = {"signature_id": f"SID-{random.randint(1000, 9999)}", "confidence": f"{random.randint(80, 100)}%"} - - return event - -# --- API Interaction --- - -def post_heartbeat(sensor): - body = { - "sensor_id": sensor["sensor_id"], - "sensor_type": sensor["sensor_type"], - "details": { - "version": VERSION, - "os": random.choice(["Linux", "Windows Server 2022", "FreeBSD"]), - "uptime_days": random.randint(1, 400), - }, - } - r = requests.post(f"{HUB_URL}/api/v1/heartbeat", json=body, headers=HEADERS, timeout=5) - r.raise_for_status() - -def post_event(event_data): - r = requests.post(f"{HUB_URL}/api/v1/event", json=event_data, headers=HEADERS, timeout=5) - r.raise_for_status() - -def main(): - print(f"Generating dynamic fleet and events for HoneyWire Hub (v{VERSION})...") - - # 1. Generate Fleet & Send Heartbeats - fleet = generate_fleet(10) # Change this number to test UI scaling - for sensor in fleet: - post_heartbeat(sensor) - print(f"[+] Heartbeat registered: {sensor['sensor_id']}") - time.sleep(0.1) - - print("\nSimulating threat activity...") - - # 2. Fire 40 random, contextual events - for _ in range(40): - sensor = random.choice(fleet) - - # Occasionally simulate an offline sensor by NOT sending a heartbeat, - # but 90% of the time, refresh the heartbeat with the event. - if random.random() > 0.1: - post_heartbeat(sensor) - - event_data = generate_contextual_event(sensor) - post_event(event_data) - - # Print a tiny log to the terminal so it looks cool running - print(f" ↳ {event_data['severity'].upper().ljust(8)} | {event_data['sensor_id']} | {event_data['event_type']}") - time.sleep(random.uniform(0.05, 0.2)) - - print("\n✅ Done. Refresh http://localhost:8080 to view the SOC dashboard.") - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/Hub/.env.example b/Hub/.env.example deleted file mode 100644 index 3daaa0c..0000000 --- a/Hub/.env.example +++ /dev/null @@ -1,24 +0,0 @@ -# ========================================== -# 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 - -# The password to log into the Web Dashboard -HW_DASHBOARD_PASSWORD=admin - -# 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/Dockerfile b/Hub/Dockerfile index a0bc5a5..c6d5e9e 100644 --- a/Hub/Dockerfile +++ b/Hub/Dockerfile @@ -1,38 +1,20 @@ -# --- Stage 1: Builder --- -FROM python:3.11-slim AS builder +# STAGE 1: Builder +FROM golang:1.25 AS builder +WORKDIR /build + +COPY go.mod go.sum ./ +RUN go mod download +COPY . . + +RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-w -s" -o honeywire-hub ./cmd/hub/main.go + +# STAGE 2: Distroless +# The "static" image contains literally nothing except SSL certificates and a non-root user. +FROM gcr.io/distroless/static-debian12:nonroot + WORKDIR /app +COPY --from=builder /build/honeywire-hub /app/ -# 1. Patch the OS (This is why your Debian vulnerabilities are now 0!) -RUN apt-get update && apt-get upgrade -y && \ - rm -rf /var/lib/apt/lists/* - -# 2. Create the virtual environment -RUN python -m venv /opt/venv -ENV PATH="/opt/venv/bin:$PATH" - -# 3. CRITICAL: Upgrade core tools to fix the 'jaraco' and 'wheel' vulnerabilities -# This pulls the fixed versions (6.1.0+ and 0.46.2+) identified in your scan -RUN pip install --no-cache-dir --upgrade pip setuptools wheel - -# 4. Install your pinned requirements -COPY requirements.txt . -RUN pip install --no-cache-dir -r requirements.txt - -# 5. Prepare data directory for Distroless -RUN mkdir /data && chown 65532:65532 /data - -# --- Stage 2: Distroless & Rootless --- -FROM gcr.io/distroless/python3-debian12 -WORKDIR /app - -COPY --from=builder /opt/venv /opt/venv -ENV PYTHONPATH="/opt/venv/lib/python3.11/site-packages" -ENV PATH="/opt/venv/bin:$PATH" - -COPY --from=builder --chown=65532:65532 /data /data -COPY --chown=65532:65532 main.py /app/main.py -COPY --chown=65532:65532 templates/ /app/templates/ - -USER nonroot:nonroot +# Expose port and run EXPOSE 8080 -CMD ["/app/main.py"] \ No newline at end of file +CMD ["/app/honeywire-hub"] \ No newline at end of file diff --git a/Hub-Go/cmd/hub/main.go b/Hub/cmd/hub/main.go similarity index 93% rename from Hub-Go/cmd/hub/main.go rename to Hub/cmd/hub/main.go index 06db8ab..052027a 100644 --- a/Hub-Go/cmd/hub/main.go +++ b/Hub/cmd/hub/main.go @@ -18,7 +18,7 @@ func main() { cfg := config.Load() - dbStore, err := store.NewStore("test_honeywire.db") + dbStore, err := store.NewStore(cfg.DBPath) if err != nil { log.Fatalf("Failed to initialize database: %v", err) } diff --git a/Hub/docker-compose.yml b/Hub/docker-compose.yml index d299780..3ec5d5e 100644 --- a/Hub/docker-compose.yml +++ b/Hub/docker-compose.yml @@ -1,14 +1,31 @@ +version: '3.8' + services: - hub: - build: . + # 1. THE FIXER: Runs as root, fixes permissions, and dies instantly. + permission-fixer: + image: alpine:latest + # 65532 is the standard UID/GID for the Distroless nonroot user + command: sh -c "chown -R 65532:65532 /data" + volumes: + - ./honeywire_data:/data + + # 2. THE HUB: pure Go distroless backend. + honeywire-hub: + build: + context: . + dockerfile: Dockerfile container_name: honeywire-hub restart: unless-stopped ports: - - "${HW_HUB_PORT}:8080" + - "${HW_PORT:-8080}:${HW_PORT:-8080}" volumes: - - honeywire_data:/data - env_file: - - .env - -volumes: - honeywire_data: \ No newline at end of file + - ./honeywire_data:/data + depends_on: + # This ensures the Hub waits until the fixer is completely finished + permission-fixer: + condition: service_completed_successfully + environment: + - HW_PORT=8080 + - HW_DB_PATH=/data/honeywire.db + - HW_HUB_KEY=change_this_to_a_secure_random_string + - HW_DASHBOARD_PASSWORD=admin \ No newline at end of file diff --git a/Hub-Go/go.mod b/Hub/go.mod similarity index 100% rename from Hub-Go/go.mod rename to Hub/go.mod diff --git a/Hub-Go/go.sum b/Hub/go.sum similarity index 100% rename from Hub-Go/go.sum rename to Hub/go.sum diff --git a/Hub-Go/internal/api/handlers.go b/Hub/internal/api/handlers.go similarity index 100% rename from Hub-Go/internal/api/handlers.go rename to Hub/internal/api/handlers.go diff --git a/Hub-Go/internal/api/middleware.go b/Hub/internal/api/middleware.go similarity index 100% rename from Hub-Go/internal/api/middleware.go rename to Hub/internal/api/middleware.go diff --git a/Hub-Go/internal/api/router.go b/Hub/internal/api/router.go similarity index 100% rename from Hub-Go/internal/api/router.go rename to Hub/internal/api/router.go diff --git a/Hub-Go/internal/auth/session.go b/Hub/internal/auth/session.go similarity index 100% rename from Hub-Go/internal/auth/session.go rename to Hub/internal/auth/session.go diff --git a/Hub-Go/internal/config/config.go b/Hub/internal/config/config.go similarity index 100% rename from Hub-Go/internal/config/config.go rename to Hub/internal/config/config.go diff --git a/Hub-Go/internal/models/models.go b/Hub/internal/models/models.go similarity index 100% rename from Hub-Go/internal/models/models.go rename to Hub/internal/models/models.go diff --git a/Hub-Go/internal/notify/notify.go b/Hub/internal/notify/notify.go similarity index 100% rename from Hub-Go/internal/notify/notify.go rename to Hub/internal/notify/notify.go diff --git a/Hub-Go/internal/store/sqlite.go b/Hub/internal/store/sqlite.go similarity index 100% rename from Hub-Go/internal/store/sqlite.go rename to Hub/internal/store/sqlite.go diff --git a/Hub/main.py b/Hub/main.py deleted file mode 100755 index 3960af6..0000000 --- a/Hub/main.py +++ /dev/null @@ -1,737 +0,0 @@ -""" -HoneyWire Hub — main.py -Universal sensor hub supporting arbitrary sensor types via the HoneyWire Event Standard. - -Architecture notes: - - DB schema is versioned via PRAGMA user_version; add new migrations to MIGRATIONS list. - - Notifications are dispatched through notify(); add new backends to NOTIFICATION_BACKENDS. - - Auth is centralised in verify_ui_auth(); cookie logic lives in one place. - - The Event model is intentionally sensor-agnostic: source, target, details cover every - conceivable sensor (network tarpit, file canary, LLM probe, email honeypot, …). -""" - -import secrets -import sqlite3 -import os -import json -import hashlib -import logging -from contextlib import contextmanager -from datetime import datetime, timedelta, timezone -from typing import Any, Callable - -import requests -from fastapi import Depends, FastAPI, Header, HTTPException, Request, BackgroundTasks -from fastapi.responses import HTMLResponse, JSONResponse -from fastapi.templating import Jinja2Templates -from pydantic import BaseModel, field_validator -from datetime import datetime, timedelta, timezone -from fastapi import FastAPI, Request, HTTPException, Depends, Header, BackgroundTasks, Query -from fastapi import Query -from datetime import timedelta - -# --------------------------------------------------------------------------- -# Logging -# --------------------------------------------------------------------------- -logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") -log = logging.getLogger("honeywire") - -# --------------------------------------------------------------------------- -# Configuration (all values come from HW_ environment variables) -# --------------------------------------------------------------------------- -# The API Key sensors use to authenticate to the Hub -API_SECRET = os.getenv("HW_HUB_KEY", "change_this_to_a_secure_random_string") - -# Dashboard UI Password -DASHBOARD_PASSWORD = os.getenv("HW_DASHBOARD_PASSWORD", "admin") - -# 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" -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_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" - -# Store active sessions in memory to prevent Pass-the-Hash vulnerabilities -# Format: { "session_token": expiration_datetime } -ACTIVE_SESSIONS: dict[str, datetime] = {} -# --------------------------------------------------------------------------- -# Database — versioned migrations -# --------------------------------------------------------------------------- -# We use one clean initialization string for new deployments -INIT_SCHEMA = """ -CREATE TABLE IF NOT EXISTS events ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - timestamp TEXT NOT NULL, - contract_version TEXT NOT NULL DEFAULT '1.0.0', - sensor_id TEXT NOT NULL, - sensor_type TEXT NOT NULL DEFAULT 'generic', - event_type TEXT NOT NULL DEFAULT 'alert', - severity TEXT NOT NULL DEFAULT 'medium', - source TEXT NOT NULL DEFAULT 'Unknown', - target TEXT NOT NULL DEFAULT 'Unknown', - action_taken TEXT NOT NULL DEFAULT 'logged', - details TEXT NOT NULL DEFAULT '{}', - is_read INTEGER NOT NULL DEFAULT 0, - is_archived INTEGER NOT NULL DEFAULT 0 -); - -CREATE TABLE IF NOT EXISTS sensors ( - sensor_id TEXT PRIMARY KEY, - first_seen TEXT, - last_seen TEXT NOT NULL, - sensor_type TEXT NOT NULL DEFAULT 'generic', - metadata TEXT NOT NULL DEFAULT '{}', - is_silenced INTEGER NOT NULL DEFAULT 0 -); - -CREATE TABLE IF NOT EXISTS config ( - key TEXT PRIMARY KEY, - value TEXT NOT NULL -); -INSERT OR IGNORE INTO config (key, value) VALUES ('is_armed', 'true'); - -CREATE TABLE IF NOT EXISTS sensor_heartbeats ( - sensor_id TEXT NOT NULL, - time_bucket TEXT NOT NULL, - PRIMARY KEY (sensor_id, time_bucket) -); -CREATE INDEX IF NOT EXISTS idx_heartbeats_time ON sensor_heartbeats(time_bucket); -""" - -def _get_db_version(conn: sqlite3.Connection) -> int: - return conn.execute("PRAGMA user_version").fetchone()[0] - -def _set_db_version(conn: sqlite3.Connection, version: int) -> None: - conn.execute(f"PRAGMA user_version = {version}") - -def init_db(): - """Run exactly once on startup to ensure tables exist.""" - conn = sqlite3.connect(DB_PATH) - try: - conn.executescript(INIT_SCHEMA) - conn.commit() - log.info("Database initialized successfully.") - except Exception as e: - log.error(f"Database initialization failed: {e}") - finally: - conn.close() - -@contextmanager -def get_db(): - """Context manager that yields a sqlite3 connection and closes it on exit.""" - conn = sqlite3.connect(DB_PATH) - conn.row_factory = sqlite3.Row - try: - yield conn - conn.commit() - except Exception: - conn.rollback() - raise - finally: - conn.close() - -init_db() - -# --------------------------------------------------------------------------- -# Notification dispatcher -# --------------------------------------------------------------------------- -def _notify_ntfy(title: str, message: str, severity: str) -> None: - if not NTFY_URL: - return - - # Ntfy scale: 1 (min) to 5 (max) - priorities = {"info": 1, "low": 2, "medium": 3, "high": 4, "critical": 5} - priority = priorities.get(severity.lower(), 3) # Default to medium - - headers = {"Title": title, "Priority": str(priority), "Tags": "rotating_light"} - - NTFY_TOKEN = os.getenv("NTFY_TOKEN", "") - if NTFY_TOKEN: - headers["Authorization"] = f"Bearer {NTFY_TOKEN}" - - try: - response = requests.post(NTFY_URL, data=message.encode('utf-8'), headers=headers, timeout=5) - response.raise_for_status() - - except requests.exceptions.HTTPError as exc: - log.warning("Ntfy rejected the request: %s | Details: %s", exc, response.text) - except Exception as exc: - log.warning("Ntfy failed to connect entirely: %s", exc) - - -def _notify_gotify(title: str, message: str, severity: str) -> None: - if not (GOTIFY_URL and GOTIFY_TOKEN): - return - - # Gotify scale: 0-3 (min/silent), 4-7 (default/sound), 8-10 (high/interrupt) - priorities = {"info": 1, "low": 3, "medium": 5, "high": 8, "critical": 10} - priority = priorities.get(severity.lower(), 5) # Default to medium - - try: - response = requests.post( - GOTIFY_URL, - headers={"X-Gotify-App-Token": GOTIFY_TOKEN}, - json={"title": title, "message": message, "priority": priority}, - timeout=5, - ) - response.raise_for_status() - - except requests.exceptions.HTTPError as exc: - log.warning("Gotify rejected the request: %s | Details: %s", exc, response.text) - except Exception as exc: - log.warning("Gotify failed to connect entirely: %s", exc) - - -NOTIFICATION_BACKENDS: list[Callable[[str, str, str], None]] = [ - _notify_ntfy, - _notify_gotify, -] - - -def notify(title: str, message: str, severity: str) -> None: - """Dispatch a notification to all configured backends. - NOTE: This should always be called via BackgroundTasks to prevent blocking.""" - for backend in NOTIFICATION_BACKENDS: - backend(title, message, severity) - - -# --------------------------------------------------------------------------- -# Pydantic models -# --------------------------------------------------------------------------- -class Event(BaseModel): - contract_version: str - sensor_id: str - sensor_type: str = "generic" - event_type: str = "alert" - severity: str = "medium" - source: str = "Unknown" - target: str = "Unknown" - action_taken: str = "logged" - metadata: dict = {} - - @field_validator("severity") - @classmethod - def validate_severity(cls, v: str) -> str: - allowed = {"info", "low", "medium", "high", "critical"} - if v.lower() not in allowed: - raise ValueError(f"severity must be one of {allowed}") - return v.lower() - - -class Heartbeat(BaseModel): - sensor_id: str - sensor_type: str = "generic" - metadata: Any = {} - - -class SystemState(BaseModel): - is_armed: bool - - -class LoginRequest(BaseModel): - password: str - - -# --------------------------------------------------------------------------- -# FastAPI app -# --------------------------------------------------------------------------- -app = FastAPI(title="HoneyWire Hub") -templates = Jinja2Templates(directory="templates") - - -# --------------------------------------------------------------------------- -# Auth — Centralized Security Logic -# --------------------------------------------------------------------------- -def verify_ui_auth(request: Request) -> None: - """Validates the presence of an active, unexpired session token.""" - if DASHBOARD_PASSWORD: - cookie_val = request.cookies.get(AUTH_COOKIE_NAME) - if not cookie_val or cookie_val not in ACTIVE_SESSIONS: - raise HTTPException(status_code=401, detail="Unauthorized") - if datetime.now() > ACTIVE_SESSIONS[cookie_val]: - del ACTIVE_SESSIONS[cookie_val] - raise HTTPException(status_code=401, detail="Session Expired") - - -def verify_agent_auth(x_api_key: str = Header(None), authorization: str = Header(None)): - """Verifies the secret key sent by the sensor (Supports X-Api-Key and Bearer).""" - token = x_api_key - if not token and authorization and authorization.startswith("Bearer "): - token = authorization.split(" ", 1)[1].strip() - - if not token or not secrets.compare_digest(token, API_SECRET): - log.warning("Unauthorized access attempt detected.") - raise HTTPException(status_code=401, detail="Unauthorized") - -# --------------------------------------------------------------------------- -# Frontend API -# --------------------------------------------------------------------------- -@app.get("/api/v1/system/state", dependencies=[Depends(verify_ui_auth)]) -async def get_system_state(): - with get_db() as conn: - row = conn.execute("SELECT value FROM config WHERE key='is_armed'").fetchone() - return {"is_armed": row["value"] == "true"} - - -@app.patch("/api/v1/system/state", dependencies=[Depends(verify_ui_auth)]) -async def set_system_state(state: SystemState): - with get_db() as conn: - conn.execute( - "UPDATE config SET value=? WHERE key='is_armed'", - ("true" if state.is_armed else "false",), - ) - return {"status": "success", "is_armed": state.is_armed} - - -@app.get("/api/v1/version") -async def get_version(): - return {"version": HONEYWIRE_VERSION} - - -@app.get("/api/v1/sensors", dependencies=[Depends(verify_ui_auth)]) -async def get_sensors(): - with get_db() as conn: - # FIX 1: Add is_silenced to the SQL SELECT statement - rows = conn.execute( - "SELECT sensor_id, sensor_type, last_seen, metadata, is_silenced FROM sensors ORDER BY sensor_id" - ).fetchall() - - now = datetime.now() - fleet = [] - for r in rows: - last_seen_dt = datetime.strptime(r["last_seen"], "%Y-%m-%d %H:%M:%S") - fleet.append({ - "sensor_id": r["sensor_id"], - "sensor_type": r["sensor_type"], - "last_seen": r["last_seen"], - "metadata": json.loads(r["metadata"]), - "status": "online" if (now - last_seen_dt) < timedelta(seconds=90) else "offline", - # FIX 2: Pass the boolean value to the frontend - "is_silenced": bool(r["is_silenced"]) - }) - return fleet - - -@app.get("/api/v1/events", dependencies=[Depends(verify_ui_auth)]) -async def get_events(archived: bool = Query(False)): - with get_db() as conn: - rows = conn.execute( - "SELECT * FROM events WHERE is_archived = ? ORDER BY id DESC", - (1 if archived else 0,) - ).fetchall() - - return [ - { - "contract_version": r["contract_version"], - "id": r["id"], - "timestamp": r["timestamp"], - "sensor_id": r["sensor_id"], - "sensor_type": r["sensor_type"], - "event_type": r["event_type"], - "severity": r["severity"], - "source": r["source"], - "target": r["target"], - "action_taken": r["action_taken"], - "details": json.loads(r["details"]) if r["details"] else {}, - "is_read": bool(r["is_read"]), - "is_archived": bool(r["is_archived"]), - } - for r in rows - ] - - -@app.patch("/api/v1/events/read", dependencies=[Depends(verify_ui_auth)]) -async def mark_events_read(): - with get_db() as conn: - conn.execute("UPDATE events SET is_read = 1 WHERE is_read = 0") - return {"status": "success"} - - -@app.patch("/api/v1/events/{event_id}/read", dependencies=[Depends(verify_ui_auth)]) -async def mark_single_event_read(event_id: int): - with get_db() as conn: - conn.execute("UPDATE events SET is_read = 1 WHERE id = ?", (event_id,)) - return {"status": "success"} - - -@app.delete("/api/v1/events", dependencies=[Depends(verify_ui_auth)]) -async def clear_events(): - with get_db() as conn: - conn.execute("DELETE FROM events") - return {"status": "success"} - -@app.get("/api/v1/uptime", dependencies=[Depends(verify_ui_auth)]) -async def get_uptime_data(timeframe: str = Query("24H")): - now = datetime.now(timezone.utc) - - if timeframe == "1H": - num_blocks = 60 - delta = timedelta(minutes=1) - fmt = "%Y-%m-%d %H:%M" - expected_pings = 1 - elif timeframe == "24H": - num_blocks = 24 - delta = timedelta(hours=1) - fmt = "%Y-%m-%d %H" - expected_pings = 60 - elif timeframe == "7D": - num_blocks = 7 - delta = timedelta(days=1) - fmt = "%Y-%m-%d" - expected_pings = 1440 - else: # 30D - num_blocks = 30 - delta = timedelta(days=1) - fmt = "%Y-%m-%d" - expected_pings = 1440 - - cutoff = now - (delta * num_blocks) - cutoff_str = cutoff.strftime("%Y-%m-%d %H:%M:%S") - - with get_db() as conn: - sensors = conn.execute("SELECT sensor_id, last_seen, first_seen FROM sensors ORDER BY sensor_id").fetchall() - rows = conn.execute( - "SELECT sensor_id, time_bucket FROM sensor_heartbeats WHERE time_bucket >= ?", - (cutoff_str,) - ).fetchall() - - history = {s["sensor_id"]: {} for s in sensors} - for r in rows: - bucket_dt = datetime.strptime(r["time_bucket"], "%Y-%m-%d %H:%M:00") - time_key = bucket_dt.strftime(fmt) - history[r["sensor_id"]][time_key] = history[r["sensor_id"]].get(time_key, 0) + 1 - - result = [] - for s in sensors: - sensor_id = s["sensor_id"] - blocks = [] - - # Safely parse first_seen (fallback to 'now' if missing) - first_seen_str = s["first_seen"] or now.strftime("%Y-%m-%d %H:%M:%S") - first_seen_dt = datetime.strptime(first_seen_str, "%Y-%m-%d %H:%M:%S").replace(tzinfo=timezone.utc) - - # CRITICAL FIX: Convert first_seen to the exact same bucket format as time_key - first_seen_key = first_seen_dt.strftime(fmt) - - for i in range(num_blocks - 1, -1, -1): - block_time = now - (delta * i) - time_key = block_time.strftime(fmt) - - time_label = "Current" if i == 0 else f"{i} " + ("mins ago" if timeframe == "1H" else "hours ago" if timeframe == "24H" else "days ago") - - # 1. NO DATA CHECK: Elegant string comparison - if time_key < first_seen_key: - status, label = "nodata", "No Data (Not Deployed Yet)" - else: - # 2. DEGRADED / UP / DOWN CHECK - ping_count = history[sensor_id].get(time_key, 0) - - if ping_count == 0: - status, label = "down", "Offline" - elif ping_count < (expected_pings * 0.85): - status, label = "degraded", f"Degraded ({ping_count}/{expected_pings} pings)" - else: - status, label = "up", "Online" - - blocks.append({ - "status": status, - "timeLabel": time_label, - "label": label - }) - - # Override the very last block to strictly match live last_seen status - last_seen_dt = datetime.strptime(s["last_seen"], "%Y-%m-%d %H:%M:%S").replace(tzinfo=timezone.utc) - is_live = (now - last_seen_dt) < timedelta(seconds=90) - blocks[-1] = { - "status": "up" if is_live else "down", - "timeLabel": "Current", - "label": "Online (Live)" if is_live else "Offline (Live)" - } - - result.append({ - "id": sensor_id, - "name": sensor_id, - "isOnline": is_live, - "blocks": blocks - }) - - return result - -@app.patch("/api/v1/events/{event_id}/archive", dependencies=[Depends(verify_ui_auth)]) -async def archive_single_event(event_id: int): - with get_db() as conn: - conn.execute("UPDATE events SET is_archived = 1, is_read = 1 WHERE id = ?", (event_id,)) - return {"status": "success"} - -@app.patch("/api/v1/events/archive-all", dependencies=[Depends(verify_ui_auth)]) -async def archive_all_events(): - with get_db() as conn: - # Only archive currently active events - conn.execute("UPDATE events SET is_archived = 1, is_read = 1 WHERE is_archived = 0") - return {"status": "success"} - - - -class SilenceRequest(BaseModel): - is_silenced: bool - -@app.patch("/api/v1/sensors/{sensor_id}/silence", dependencies=[Depends(verify_ui_auth)]) -async def toggle_sensor_silence(sensor_id: str, req: SilenceRequest): - with get_db() as conn: - conn.execute( - "UPDATE sensors SET is_silenced = ? WHERE sensor_id = ?", - (1 if req.is_silenced else 0, sensor_id) - ) - return {"status": "success", "sensor_id": sensor_id, "is_silenced": req.is_silenced} - -# --------------------------------------------------------------------------- -# Sensor API -# --------------------------------------------------------------------------- -@app.post("/api/v1/heartbeat", dependencies=[Depends(verify_agent_auth)]) -async def receive_heartbeat(hb: Heartbeat): - now = datetime.now(timezone.utc) - now_str = now.strftime("%Y-%m-%d %H:%M:%S") - - # Round down to the current minute (e.g., "2026-04-05 21:05:00") - minute_bucket = now.strftime("%Y-%m-%d %H:%M:00") - - metadata_json = json.dumps(hb.metadata) if isinstance(hb.metadata, (dict, list)) else "{}" - - with get_db() as conn: - # 1. Update current live status & explicitly set first_seen if new - conn.execute( - """INSERT INTO sensors (sensor_id, first_seen, last_seen, sensor_type, metadata) - VALUES (?, ?, ?, ?, ?) - ON CONFLICT(sensor_id) DO UPDATE SET last_seen=?, sensor_type=?, metadata=?""", - (hb.sensor_id, now_str, now_str, hb.sensor_type, metadata_json, - now_str, hb.sensor_type, metadata_json), - ) - - # 2. Log historical bucket (Ignores duplicates within the same minute) - conn.execute( - "INSERT OR IGNORE INTO sensor_heartbeats (sensor_id, time_bucket) VALUES (?, ?)", - (hb.sensor_id, minute_bucket) - ) - - return {"status": "alive"} - - -@app.post("/api/v1/event", dependencies=[Depends(verify_agent_auth)]) -async def receive_event(event: Event, bg_tasks: BackgroundTasks): - # Optional but recommended: Semantic versioning check - hub_major_version = HONEYWIRE_VERSION.split('.')[0] - agent_major_version = event.contract_version.split('.')[0] - - if hub_major_version != agent_major_version: - log.warning(f"Version mismatch rejected: Hub is {HONEYWIRE_VERSION}, Agent sent {event.contract_version}") - raise HTTPException( - status_code=426, - detail=f"Upgrade Required: Hub is on v{HONEYWIRE_VERSION}, but agent uses v{event.contract_version}" - ) - - timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S") - details_payload = event.metadata if hasattr(event, 'metadata') else {} - details_json = json.dumps(details_payload) if isinstance(details_payload, (dict, list)) else str(details_payload) - - with get_db() as conn: - conn.execute( - """INSERT INTO events - (timestamp, contract_version, sensor_id, sensor_type, event_type, severity, - source, target, action_taken, details, is_read) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0)""", - (timestamp, event.contract_version, event.sensor_id, event.sensor_type, event.event_type, - event.severity, event.source, event.target, event.action_taken, details_json), - ) - # 1. Check Global Arm State - is_armed = conn.execute( - "SELECT value FROM config WHERE key='is_armed'" - ).fetchone()["value"] == "true" - - # 2. Check Specific Sensor Silence State - sensor_row = conn.execute( - "SELECT is_silenced FROM sensors WHERE sensor_id = ?", - (event.sensor_id,) - ).fetchone() - is_silenced = bool(sensor_row["is_silenced"]) if sensor_row else False - - msg = ( - f"[{event.sensor_id}] {event.event_type.upper()} — " - f"{event.source} → {event.target} | action: {event.action_taken}" - ) - - if is_armed and not is_silenced: - bg_tasks.add_task(notify, title=f"HoneyWire Alert ({event.severity.upper()})", message=msg, severity=event.severity) - - log.info("Event received (v%s): %s", event.contract_version, msg if is_armed else f"{event.sensor_id}/{event.event_type}") - return {"status": "success"} - - -# --------------------------------------------------------------------------- -# Web UI & Vault Door (Uncodixified) -# --------------------------------------------------------------------------- -LOGIN_HTML = """ - - - - - HoneyWire Sentinel | Authentication - - - - - - - -
-
-
🕸️
-

HoneyWire Sentinel

-

Authorized personnel only

-
- -
-
-
- - -
- - -
- - -
- -
- -
-
- - - -""" - - -@app.post("/login") -async def login(req: LoginRequest): - if DASHBOARD_PASSWORD and secrets.compare_digest(req.password, DASHBOARD_PASSWORD): - session_token = secrets.token_hex(32) - ACTIVE_SESSIONS[session_token] = datetime.now() + timedelta(days=30) - - response = JSONResponse(content={"status": "ok"}) - response.set_cookie( - key=AUTH_COOKIE_NAME, value=session_token, - max_age=2_592_000, httponly=True, samesite="strict", - ) - return response - raise HTTPException(status_code=401, detail="Invalid Password") - - -@app.get("/logout") -async def logout(): - response = HTMLResponse(content="") - response.delete_cookie(AUTH_COOKIE_NAME) - return response - - -@app.get("/", response_class=HTMLResponse) -async def serve_dashboard(request: Request): - if DASHBOARD_PASSWORD: - cookie_val = request.cookies.get(AUTH_COOKIE_NAME) - if not cookie_val or cookie_val not in ACTIVE_SESSIONS or datetime.now() > ACTIVE_SESSIONS[cookie_val]: - return HTMLResponse(content=LOGIN_HTML) - return templates.TemplateResponse(request=request, name="index.html") - - -if __name__ == "__main__": - import uvicorn - uvicorn.run(app, host="0.0.0.0", port=8080) \ No newline at end of file diff --git a/Hub/mock_data.py b/Hub/mock_data.py index 0d8b561..9be594d 100644 --- a/Hub/mock_data.py +++ b/Hub/mock_data.py @@ -62,31 +62,31 @@ def generate_contextual_event(sensor): event["event_type"] = random.choice(["sqli_attempt", "xss_payload", "dir_bruteforce", "api_fuzz"]) event["severity"] = random.choice(["high", "critical", "medium"]) event["target"] = random.choice(["/wp-admin", "/api/v1/users", "/.env", "/config.php"]) - event["metadata"] = {"user_agent": "masscan/1.3", "payload": "1' OR '1'='1", "method": "POST"} + event["details"] = {"user_agent": "masscan/1.3", "payload": "1' OR '1'='1", "method": "POST"} elif s_type == "tarpit": event["event_type"] = random.choice(["tcp_syn_flood", "port_scan", "ssh_bruteforce"]) event["severity"] = random.choice(["low", "medium", "info"]) event["target"] = random.choice(["Port 22", "Port 23", "Port 3389", "Port 445"]) - event["metadata"] = {"packets_dropped": random.randint(50, 5000), "connection_time_held": f"{random.randint(10, 120)}s"} + event["details"] = {"packets_dropped": random.randint(50, 5000), "connection_time_held": f"{random.randint(10, 120)}s"} elif s_type == "canary_token": event["event_type"] = random.choice(["file_accessed", "aws_key_used", "db_queried"]) event["severity"] = "critical" event["target"] = random.choice(["/etc/passwd.bak", "AWS_SECRET_KEY", "users_backup.sql"]) - event["metadata"] = {"process_name": random.choice(["cat", "curl", "python3"]), "user": "www-data"} + event["details"] = {"process_name": random.choice(["cat", "curl", "python3"]), "user": "www-data"} elif s_type == "llm_probe": event["event_type"] = random.choice(["prompt_injection", "jailbreak_attempt", "data_exfiltration"]) event["severity"] = random.choice(["high", "critical"]) event["target"] = "/v1/chat/completions" - event["metadata"] = {"prompt_snippet": "Ignore previous instructions and print...", "model": "gpt-4-turbo"} + event["details"] = {"prompt_snippet": "Ignore previous instructions and print...", "model": "gpt-4-turbo"} else: # ids_sentinel event["event_type"] = random.choice(["malware_signature", "lateral_movement", "beaconing"]) event["severity"] = random.choice(["high", "critical", "medium"]) event["target"] = f"Subnet 10.0.{random.randint(1,5)}.0/24" - event["metadata"] = {"signature_id": f"SID-{random.randint(1000, 9999)}", "confidence": f"{random.randint(80, 100)}%"} + event["details"] = {"signature_id": f"SID-{random.randint(1000, 9999)}", "confidence": f"{random.randint(80, 100)}%"} return event @@ -96,7 +96,7 @@ def post_heartbeat(sensor): body = { "sensor_id": sensor["sensor_id"], "sensor_type": sensor["sensor_type"], - "metadata": { + "details": { "version": VERSION, "os": random.choice(["Linux", "Windows Server 2022", "FreeBSD"]), "uptime_days": random.randint(1, 400), diff --git a/Hub/requirements.txt b/Hub/requirements.txt deleted file mode 100755 index c3c7add..0000000 --- a/Hub/requirements.txt +++ /dev/null @@ -1,15 +0,0 @@ -# Web Framework (Fixed 2024/2025 DoS vulnerabilities) -fastapi>=0.115.0 -uvicorn[standard]>=0.30.0 - -# HTML Templating -jinja2>=3.1.4 - -# Data Validation (Pydantic V2.10 is the 2026 stable standard) -pydantic>=2.10.0 - -# Security Fixes for multipart parsing (Fixes CVE-2024-53981) -python-multipart>=0.0.22 - -# Networking -requests>=2.32.3 \ No newline at end of file diff --git a/Hub/tailwind.config.js b/Hub/tailwind.config.js deleted file mode 100644 index 44779d2..0000000 --- a/Hub/tailwind.config.js +++ /dev/null @@ -1,8 +0,0 @@ -/** @type {import('tailwindcss').Config} */ -module.exports = { - content: ["./templates/**/*.html"], - theme: { - extend: {}, - }, - plugins: [], -} diff --git a/Hub/templates/index.html b/Hub/templates/index.html deleted file mode 100755 index cd8323c..0000000 --- a/Hub/templates/index.html +++ /dev/null @@ -1,736 +0,0 @@ - - - - - - HoneyWire Sentinel - - - - - - - - - - - -
- -
- -
- - -
- -
- - - - - - - - - -
- - Exit -
-
- -
- - - - - - -
-
- -
-
- -
- -
-
-

-

-
- -
- -
-
-
-
Sensor Node
-
-
-
-
Source
-
-
-
-
Target
-
-
-
- -
- -
- - -
-
- - - - -
- - -
-
-
- - - - \ No newline at end of file diff --git a/Hub-Go/web/embed.go b/Hub/web/embed.go similarity index 100% rename from Hub-Go/web/embed.go rename to Hub/web/embed.go diff --git a/Hub-Go/web/static/login.html b/Hub/web/static/login.html similarity index 100% rename from Hub-Go/web/static/login.html rename to Hub/web/static/login.html diff --git a/Hub-Go/web/templates/index.html b/Hub/web/templates/index.html similarity index 100% rename from Hub-Go/web/templates/index.html rename to Hub/web/templates/index.html