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
-
-
-
-
-
-
-
Access Denied: Invalid Key
-
-
-
-
-
-
-
-
-"""
-
-
-@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
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Menu
-
-
-
- Dashboard
-
-
-
-
- Sensor Store
-
-
-
-
- Settings
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Purge System Logs
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Live
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Exit
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ 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