mirror of
https://github.com/andreicscs/HoneyWire
synced 2026-06-26 12:39:53 +00:00
cleaning up legacy docs
This commit is contained in:
+1
-1
@@ -1,4 +1,4 @@
|
||||
[](LICENSE)
|
||||
[](LICENSE)
|
||||
|
||||
# Contributing to HoneyWire
|
||||
|
||||
|
||||
@@ -33,4 +33,47 @@ Views act as orchestrators for pages (e.g., `Dashboard.vue`, `NodeDetails.vue`).
|
||||
|---|---|
|
||||
| Own ephemeral UI state (e.g., "is the modal open?") | Perform backend mutations directly |
|
||||
| Delegate heavy logic to Pinia stores | Duplicate error rollback logic |
|
||||
| Orchestrate layout and pass down props | Normalize API responses |
|
||||
| Orchestrate layout and pass down props | Normalize API responses |
|
||||
|
||||
---
|
||||
|
||||
## Design System
|
||||
|
||||
The Hub frontend uses a centralized token system based on the OKLCH color space (located in `src/assets/style.css`).
|
||||
|
||||
### Principles
|
||||
|
||||
- Semantic tokens only — no hardcoded colors in components.
|
||||
- Accessibility-first contrast.
|
||||
- Dark/light parity.
|
||||
- Theme switching swaps root variables only.
|
||||
|
||||
### Token Categories
|
||||
|
||||
| Category | Examples | Usage |
|
||||
|---|---|---|
|
||||
| Structural | `--bg`, `--bg-surface`, `--border-default` | Layout hierarchy |
|
||||
| Interactive | `--primary-main`, `--secondary-main`, `--danger-main` | Actions & states |
|
||||
| Severity | `--sev-critical` through `--sev-info` | Charts, alerts, tables, status |
|
||||
| Typography | `--text-h1`, `--text-base`, `--text-sm` | All text sizing |
|
||||
| Spacing | `--space-card-p`, `--space-flow` | Layout rhythm |
|
||||
| Elevation | `--radius-sm`, `--shadow-md` | Cards, modals, widgets |
|
||||
| Z-index | `--z-dropdown` → `--z-toast` | Stacking hierarchy |
|
||||
|
||||
**No arbitrary values.** Use tokens.
|
||||
|
||||
### Tailwind Integration
|
||||
|
||||
Tokens exposed via `@theme`:
|
||||
|
||||
```css
|
||||
bg-bg-surface → --bg-surface
|
||||
text-text-h → --text-h
|
||||
border-border-default → --border-default
|
||||
```
|
||||
|
||||
### Dark Mode
|
||||
|
||||
```css
|
||||
.dark { /* overrides root variables */ }
|
||||
```
|
||||
@@ -111,3 +111,41 @@ This decision was explicitly chosen to prioritize initial simplicity and speed o
|
||||
|
||||
**Future Migration Path:**
|
||||
This is considered technical debt. If frontend performance bottlenecks arise due to memory overhead (e.g., traversing thousands of events in the browser), or if HoneyWire adopters request it for scale, the event pagination logic will be migrated to the backend to match the architecture of our dashboard projections.
|
||||
# Bootstrap & Lifecycle
|
||||
|
||||
## Cold Boot Sequence
|
||||
|
||||
User loads the app (`onMounted` in App.vue):
|
||||
|
||||
1. Check if setup is required (`checkRequiresSetup`).
|
||||
2. Check if authenticated (`checkSystemState`).
|
||||
3. Load application data in parallel (`fetchFleet`, `fetchEvents`, etc.).
|
||||
4. Connect WebSocket and register handlers.
|
||||
5. **Critical Invariant:** `isAuthenticated = true` is set **last**, after all data has been fetched. This prevents the authenticated shell from rendering before stores are populated.
|
||||
|
||||
---
|
||||
|
||||
# Debugging Guide
|
||||
|
||||
## Blank Dashboard After Login
|
||||
- Check: `loadAppData()` actually completed and all `await Promise.all([...])` calls resolved.
|
||||
- Fix: Ensure `isAuthenticated = true` is set AFTER data loads.
|
||||
|
||||
## UI Not Updating
|
||||
- Check: Array reassignment broke reactivity (`nodes.value = newArray`). Fix by using `splice()`, `push()`, `Object.assign()`.
|
||||
- Check: Watched property is accessed with `.value` in `<script setup>`.
|
||||
|
||||
## WebSocket Not Receiving Updates
|
||||
- Check: WebSocket not connected (check Network tab for "101 Switching Protocols").
|
||||
- Check: Handler not registered before `wsService.connect()`.
|
||||
- Check: Event filtered out inside `handleWsEvent()` due to `selectedNode` / `selectedSensor`.
|
||||
|
||||
## Composite Key Bugs (Sensors)
|
||||
- Symptom: Wrong sensor updated, cross-node collisions.
|
||||
- Fix: Always pass both `nodeId` and `sensorId` to get a sensor, as `sensorId` alone is not unique.
|
||||
|
||||
## Optimistic Update Didn't Rollback
|
||||
- Check: Saved state before optimistic update was not captured or was applied to the wrong object reference.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
# App Store Architecture (`app.ts`)
|
||||
|
||||
## 4-Layer State Model
|
||||
|
||||
State in `app.ts` is divided into four distinct categories:
|
||||
|
||||
1. **Backend-Synced State (Server Truth):** Absolute source of truth for backend settings (e.g., `isArmed`, `version`). Never optimistically mutated.
|
||||
2. **Pure UI State (Frontend Owned):** Ephemeral client-only state (e.g., `viewingArchive`, `sidebarOpen`, `currentView`).
|
||||
3. **Session State Machine:** A three-state model (`'unknown'`, `'authenticated'`, `'unauthenticated'`) to manage identity before UI renders.
|
||||
4. **Explicit Error State:** Dedicated reactive variables for errors (`bootstrapError`, `authError`, `setupError`).
|
||||
|
||||
## Session Transition Authority
|
||||
|
||||
All session changes (login, logout, 401s) must pass through a strict `transitionSession` gatekeeper that blocks invalid state transitions.
|
||||
|
||||
## Decoupled System State Fetching
|
||||
|
||||
`fetchSystemState` only fetches data. If it encounters a `401 Unauthorized` or `403 Forbidden`, it delegates to the Gatekeeper by calling `transitionSession('unauthenticated')`.
|
||||
|
||||
## Reconciled Update Pattern
|
||||
|
||||
For critical system flags (like toggling `isArmed`), optimistic UI updates are forbidden:
|
||||
1. **Dispatch Intent:** Send intended state to server.
|
||||
2. **Reconcile Reality:** Fetch actual resulting state from server.
|
||||
3. **UI Update:** Update UI only when the new snapshot replaces local state.
|
||||
|
||||
## State-First Error Boundaries
|
||||
|
||||
Auth and Setup methods do not return error strings. They act as mutators for dedicated error states (`authError`, `setupError`). Views bind directly to these state variables.
|
||||
|
||||
## Bootstrap Orchestration
|
||||
|
||||
Initialization (`initAppStore()`) fetches independent system requirements concurrently using `Promise.allSettled`. It delegates findings to the Gatekeeper to safely stall, authenticate, or fall back to setup.
|
||||
@@ -0,0 +1,22 @@
|
||||
# Events Store Architecture (`events.ts`)
|
||||
|
||||
## Entity State vs. Projection State
|
||||
The `events.ts` store manages two fundamentally different state models with specific mutation rules:
|
||||
|
||||
### Mutable Entity State (Events List)
|
||||
* **Identity-Preserving:** Uses a Hybrid Update Model. When active filters change, the store refetches the complete history from the backend.
|
||||
* **Real-time Hydration:** When a `NEW_EVENT` arrives via WebSocket, the store evaluates if it belongs to the active view. If so, it is instantly `unshift`ed into the array without issuing a network request.
|
||||
|
||||
### Immutable Projection State (Analytics Charts)
|
||||
* **Backend-Generated:** The UI never loops over local events to aggregate data.
|
||||
* **Atomically Replaced:** When a new projection arrives, the entire object reference is replaced to trigger clean re-renders via shallow watchers.
|
||||
|
||||
## Contextual Invalidation Strategy
|
||||
To prevent race conditions during high-frequency filter changes (e.g., rapidly clicking timeframes), the store uses `AbortController` to cancel in-flight projection requests.
|
||||
When a matching realtime WebSocket event arrives, the store does not manually mutate projections. Instead, it invalidates the current projection timestamp, triggering components to automatically refetch the exact backend snapshot.
|
||||
|
||||
## Composite Key Enforcement
|
||||
Because sensor IDs are not globally unique, filtering and matching logic inside the store strictly evaluates both `nodeId` AND `sensorId` simultaneously to prevent telemetry cross-contamination.
|
||||
|
||||
## Encapsulation
|
||||
State is hidden inside private `ref`s and accessed strictly through exported `computed` getters, physically preventing Vue components from accidentally mutating or clearing the event arrays.
|
||||
@@ -0,0 +1,16 @@
|
||||
# Fleet Store Architecture (`fleet.ts`)
|
||||
|
||||
## Composite Key Model
|
||||
Sensor IDs are only unique within the scope of their parent Node. To prevent nested array traversal, the store uses a flat O(1) read model by hashing them into a **Composite Key** (`${nodeId}:${rawSensorId}`). Store actions must pass both to generate the correct hash.
|
||||
|
||||
## Optimistic Update with Rollback Cache
|
||||
For non-critical metadata changes, `fleet.ts` uses Optimistic UI. Gatekeepers (`patchNode`, `patchSensor`) apply the update immediately and return the *previous* state. If the subsequent backend API call fails, the store uses the previous state to seamlessly rollback.
|
||||
|
||||
## Transport Boundary Normalization
|
||||
Raw backend JSON is strictly normalized (`normalizeNodeData`, `normalizeSensorData`) before entering the State Tree. This insulates Vue components from backend schema changes and missing properties.
|
||||
|
||||
## Partial Invalidation (Subgraph Fetching)
|
||||
Mutations (e.g., deleting a sensor) do not trigger a full `fetchFleet()`. Instead, a **Subgraph Fetch** (`fetchNodeDetails(nodeId)`) executes. It securely deletes the specific node's branch from the `sensorsById` map and merges the fresh payload.
|
||||
|
||||
## Encapsulation
|
||||
Core objects (`nodesById`, `sensorsById`) are hidden behind private `ref`s and exported as read-only `computed` properties, physically preventing Vue components from accidentally executing direct mutations.
|
||||
@@ -10,6 +10,8 @@ HoneyWire uses three primary Pinia stores to manage its domain state. Components
|
||||
- **Actions:** Login/Logout, completing the initial setup, toggling the system arming state.
|
||||
- **Note:** `isAuthenticated` acts as a final gatekeeper. During a cold boot, it is only set to `true` *after* all underlying data (fleet, events) has finished loading.
|
||||
|
||||
> 📖 **[View the detailed App Store Architecture](./store-app.md)**
|
||||
|
||||
## 2. Fleet Store (`fleet.ts`)
|
||||
|
||||
**Ownership:** Infrastructure state (Nodes, sensors, uptime, deployment metadata).
|
||||
@@ -19,6 +21,8 @@ HoneyWire uses three primary Pinia stores to manage its domain state. Components
|
||||
- **Composite Keys:** Sensors are only unique within a node. The store strictly uses the composite key `nodeId + sensorId`.
|
||||
- **Actions:** Fetches node lists, handles sensor creation/deletion, and optimistic updates for toggling sensor silence.
|
||||
|
||||
> 📖 **[View the detailed Fleet Store Architecture](./store-fleet.md)**
|
||||
|
||||
## 3. Events Store (`events.ts`)
|
||||
|
||||
**Ownership:** Telemetry state, intrusion events, and unread tracking.
|
||||
@@ -27,6 +31,8 @@ HoneyWire uses three primary Pinia stores to manage its domain state. Components
|
||||
- **Filtering:** All filtering by archive mode, selected node, or selected sensor occurs *reactively inside the store*. Components do not implement filtering logic.
|
||||
- **Actions:** Fetching events based on context, marking as read, and appending new incoming WebSocket events.
|
||||
|
||||
> 📖 **[View the detailed Events Store Architecture](./store-events.md)**
|
||||
|
||||
## Reactive Identity Preservation
|
||||
|
||||
A critical pattern in the HoneyWire frontend is preserving Vue 3's reactive object identity.
|
||||
|
||||
@@ -1,145 +0,0 @@
|
||||
# HoneyWire Hub Backend Architecture
|
||||
|
||||
This document outlines the architectural design and data flow of the HoneyWire Hub backend. The backend is written in Go and strictly adheres to a **Domain Service Pattern** (a lightweight form of Clean Architecture / Domain-Driven Design) to ensure high testability, predictable state management, and clear separation of concerns.
|
||||
|
||||
---
|
||||
|
||||
## Core Principles
|
||||
|
||||
1. **Thin Transport Layer:** HTTP handlers in the `api` package contain **zero** business logic. They only parse JSON, read context, and format HTTP responses.
|
||||
2. **Isolated Domain Services:** Business rules, side effects (like WebSockets or Notifications), and complex transactions live exclusively in the `services` package.
|
||||
3. **Interface Segregation:** Services define their own narrow `Store` and `Broadcaster` interfaces. They do not know about the concrete `SQLiteStore` or `WebSocketService`.
|
||||
4. **Explicit Composition:** All dependencies are instantiated and wired together precisely once in the Composition Root (`cmd/hub/main.go`). Global state is strictly prohibited.
|
||||
5. **Contextual Authentication:** Middleware authenticates requests and injects the verified identity (e.g., `NodeID`) into the standard `context.Context`.
|
||||
|
||||
---
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```text
|
||||
internal/
|
||||
├── api/ # Transport Layer (HTTP Handlers, Middleware, Router)
|
||||
├── models/ # Core Domain Entities (Structs, JSON tags)
|
||||
├── projections/ # CQRS Read-Models (Analytics, Dashboards)
|
||||
├── services/ # Domain Layer (Business Logic, Orchestration)
|
||||
└── store/ # Persistence Layer (SQLite implementation)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## The Four Layers
|
||||
|
||||
### 1. Transport Layer (`internal/api`)
|
||||
The entry point for all network requests.
|
||||
|
||||
* **Handlers (`nodes.go`, `events.go`, etc.):**
|
||||
* Bound to domain-specific struct receivers (e.g., `NodeHandler`).
|
||||
* Responsible for JSON Unmarshaling, Input Validation, and HTTP Status Codes.
|
||||
* **Rule:** Never interact with the database directly. Always delegate to a Service.
|
||||
* **Middleware (`middleware.go`):**
|
||||
* Handles Authentication (`UIAuthMiddleware`, `AgentAuthMiddleware`, `DualAuthMiddleware`).
|
||||
* Extracts credentials (Cookies, Bearer tokens), validates them via the `auth.Service`, and attaches the resulting `nodeID` to the `http.Request` context.
|
||||
* **Router (`router.go`):**
|
||||
* Maps HTTP routes to specific Handlers and applies Middleware groups.
|
||||
* **SPA Routing & Frontend Embedding:** The router serves the compiled Vue 3 frontend using `go:embed`. It implements a secure Single Page Application fallback:
|
||||
1. **Strict API Protection:** Requests prefixed with `/api/` return strict 404s if unmatched, preventing HTML from leaking into API clients.
|
||||
2. **Static Assets:** Existing static files (e.g., `.css`, `.js`) are served normally from the embedded filesystem.
|
||||
3. **SPA Fallback:** Any unrecognized non-API route transparently falls back to `index.html`, allowing the Vue Router to manage history mode URLs (like `/dashboard`) without server-side 404 errors.
|
||||
|
||||
### 2. Domain Service Layer (`internal/services`)
|
||||
The brain of the application. Everything in `internal/services/*` is framework-agnostic.
|
||||
|
||||
* **Services (`event`, `node`, `config`, etc.):**
|
||||
* Contain the actual "verbs" of the system (`ProcessHeartbeat`, `CreateNode`).
|
||||
* Handle side effects (Dispatching webhooks, queuing SIEM logs, broadcasting WebSockets).
|
||||
* **Dependency Injection:** Services declare their dependencies via narrow interfaces. For example, `event.Service` defines a `Store` interface that `sqlite.go` implicitly satisfies.
|
||||
* **Background Workers:**
|
||||
* Long-running tasks that belong to a specific domain (e.g., `eventSvc.StartRetentionWorker`, `sensorSvc.StartHealthMonitor`) live inside that service's package.
|
||||
|
||||
### 3. Persistence Layer (`internal/store`)
|
||||
The data access layer.
|
||||
|
||||
* **SQLite Store (`sqlite.go`):**
|
||||
* Implements the narrow interfaces required by the Services.
|
||||
* Responsible for SQL queries, transactions, and JSON marshaling into Go structs.
|
||||
* Operates in Write-Ahead Log (WAL) mode with connection pooling for high concurrency.
|
||||
* **Rule:** Contains no business logic or external side effects.
|
||||
|
||||
### 4. Read/Analytics Layer (`internal/projections`)
|
||||
A specialized CQRS pattern for heavy dashboard analytics.
|
||||
|
||||
* Used for high-volume aggregations like Threat Velocity and Severity Distributions.
|
||||
* Instead of raw arrays, these return flat **DTOs** (Data Transfer Objects) derived via pure `calculator.go` functions.
|
||||
* See `projections/severity/SEVERITY_ARCHITECTURE.md` for a deep dive.
|
||||
|
||||
---
|
||||
|
||||
## Data Flow Example (Event Processing)
|
||||
|
||||
When a sensor detects an intrusion and sends an HTTP POST to `/api/v1/event`:
|
||||
|
||||
1. **Router:** Matches the route and invokes `AgentAuthMiddleware`.
|
||||
2. **Middleware:**
|
||||
* Reads the `X-Api-Key` or `Bearer` token.
|
||||
* Calls `authService.AuthenticateNodeRequest()`.
|
||||
* Injects the authenticated `NodeID` into the request `Context`.
|
||||
3. **API Handler (`events.go`):**
|
||||
* Parses the JSON body into a `models.Event`.
|
||||
* Extracts `NodeID` from the context.
|
||||
* Calls `eventService.ProcessEvent(event, nodeID)`.
|
||||
4. **Event Service (`services/event/service.go`):**
|
||||
* Verifies version compatibility.
|
||||
* Calls `store.InsertEvent()`.
|
||||
* Calls `store.UpdateNodeLastHeartbeat()`.
|
||||
* Checks `store.IsSensorSilenced()`. If false -> calls `notifyService.Dispatch()`.
|
||||
* Calls `siemService.QueueEvent()`.
|
||||
* Calls `broadcaster.Broadcast("NEW_EVENT")` to update connected UI clients instantly.
|
||||
5. **API Handler:** Returns `HTTP 200 OK`.
|
||||
|
||||
---
|
||||
|
||||
## Authentication Strategies
|
||||
|
||||
HoneyWire employs a multi-tiered authentication strategy depending on the actor:
|
||||
|
||||
1. **UI Dashboard (Humans):**
|
||||
* Secured via short-lived Sessions and HTTP-Only, Secure, SameSite `hw_auth` Cookies.
|
||||
* Managed by `services/auth` (Brute-force protection, lockout tracking, session invalidation).
|
||||
* Validated by `UIAuthMiddleware`.
|
||||
|
||||
2. **Sensors/Agents (Machines):**
|
||||
* Secured via statically generated, cryptographically random API Keys (`hw_key_...`).
|
||||
* Passed via `Authorization: Bearer` or `X-Api-Key` headers.
|
||||
* Cached in memory via `sync.Map` in `auth.Service` to prevent database bottlenecks during heartbeat storms.
|
||||
* Validated by `AgentAuthMiddleware`.
|
||||
|
||||
3. **Dual-Auth Endpoints:**
|
||||
* Endpoints like `/api/v1/manifests` are accessed by *both* humans (UI Dashboard) and machines (Sensors).
|
||||
* Handled safely via `DualAuthMiddleware`, which attempts Bearer auth first, falling back to Cookie validation.
|
||||
|
||||
---
|
||||
|
||||
## Background Workers
|
||||
|
||||
Workers are decoupled from the HTTP transport layer and managed exclusively via the `context.Context` instantiated in `main.go`.
|
||||
|
||||
| Worker | Location | Purpose |
|
||||
|---|---|---|
|
||||
| **Health Monitor** | `services/sensor` | Polls every 30s. If a sensor misses heartbeats > 60s, updates status to `down` and broadcasts a WS update. |
|
||||
| **Event Retention** | `services/event` | Wakes hourly to delete/archive events older than configured thresholds to prevent DB bloat. |
|
||||
| **Chart Sync** | `services/websocket` | Emits an empty payload every 30s telling the UI to tick its time-series charts forward smoothly. |
|
||||
| **Auth Sweeper** | `services/auth` | Cleans up expired sessions and brute-force IP lockout maps to prevent memory leaks. |
|
||||
| **SIEM Forwarder** | `services/siem` | Drains the in-memory event channel over TCP/UDP to external log aggregators. |
|
||||
| **Notifier** | `services/notify` | Drains the webhook channel to Slack/Discord/Gotify, preventing external API latency from blocking HoneyWire HTTP responses. |
|
||||
|
||||
---
|
||||
|
||||
## Adding a New Feature (Developer Guide)
|
||||
|
||||
To add a new endpoint, follow this strict sequence:
|
||||
|
||||
1. **Models:** Define your data structures in `models/`.
|
||||
2. **Store:** Write the SQL query in `store/`. Update the interface definition at the top of your target Service.
|
||||
3. **Service:** Write the business logic in `services/<domain>/service.go`. Handle errors, validation, and side effects here.
|
||||
4. **API Handler:** Write a thin HTTP wrapper in `api/<domain>.go`. Use `api.RespondError` and `api.SendJSON`.
|
||||
5. **Router & Main:** Register the route in `api/router.go`.
|
||||
@@ -1,28 +0,0 @@
|
||||
# Hub v2.0 Secure Compiler Architecture
|
||||
|
||||
## Overview
|
||||
The `internal/compose` package handles the generation of deterministic, hardened `honeywire-compose.yml` payloads sent to the remote sensor nodes.
|
||||
|
||||
The architecture is driven by the principle of **Secure Defaults by Inversion**. Instead of filtering bad things out of an arbitrary map, the compiler explicitly maps specific, allowed schema primitives into a locked-down Compose base.
|
||||
|
||||
## The Pipeline
|
||||
|
||||
1. **Strict Ingestion**
|
||||
All catalog fetch operations and API preview payloads use `json.NewDecoder` equipped with `DisallowUnknownFields()`. This creates a hard edge boundary—if a payload possesses attributes outside the v2 schema, it is rejected immediately. This enforces structural schema validity, not semantic safety.
|
||||
2. **The Validator (`security/validate.go`)**
|
||||
We assume malicious intent. This module acts as the "bouncer."
|
||||
- Strictly enforces capabilities via an explicit allowlist (e.g. `NET_RAW`, `NET_BIND_SERVICE`, `NET_ADMIN`).
|
||||
- Normalizes path structures (`filepath.Clean()`) and checks them against an absolute denylist (e.g., `/proc`, `/sys`, `/var/run/docker.sock`).
|
||||
- Recursively verifies volume mounts and init-containers.
|
||||
- Blocks untrusted interpolation patterns in fields that are not executed through a safe templating engine.
|
||||
3. **Secure Environment Composition (`env.go`)**
|
||||
The `BuildEnv` pipeline ensures isolated priority overrides. Manifest defaults are loaded, overridden safely by user vars (dropping any attempting to modify forbidden environment fields), and ultimately superseded by statically defined, system-injected constants (e.g., `HW_HUB_KEY`, `HW_SENSOR_ID`).
|
||||
4. **Versioning and Dual-Manifest Resolution**
|
||||
Because nodes now support manual rather than automatic upgrades, a node may run a deprecated legacy version of a sensor. To prevent breaking changes during compose regeneration, the compiler relies on dual-manifest resolution:
|
||||
- **Latest Catalog:** Warmed into cache on startup via `index.json`.
|
||||
- **Historical Schemas:** Lazy-loaded and permanently cached via `FetchSpecificManifest` (e.g. `hw-sensor-tarpit-v1.0.0.json`) when requested by a legacy node.
|
||||
5. **The Builder (`builder.go`)**
|
||||
The `BuildService` routine converts the strictly-validated models into `ComposeFile` structs.
|
||||
- **Immutable Sandboxing**: Unconditionally forces `ReadOnly: true`, `CapDrop: ["ALL"]`, and `SecurityOpt: ["no-new-privileges:true"]`.
|
||||
- Structural templating handles file-bind expansions inherently rather than executing string replacement interpolations on manifest strings.
|
||||
- Output lists (environment pairs, volume mount paths, initialization chains) are explicitly alphabetically sorted. This guarantees bit-for-bit deterministic deployment manifests every time.
|
||||
@@ -1,385 +0,0 @@
|
||||
# Severity Analytics Projection Architecture Guide
|
||||
|
||||
This document describes the current severity analytics architecture used by the dashboard.
|
||||
The system has been refactored away from frontend-side event aggregation and now uses backend-generated analytics projections with contextual filtering.
|
||||
|
||||
# Architecture Overview
|
||||
|
||||
## Backend Projection Model
|
||||
```
|
||||
Raw Events
|
||||
↓
|
||||
Backend Filtering (timeframe/node/sensor/archive)
|
||||
↓
|
||||
Backend Severity Aggregation
|
||||
↓
|
||||
Flat Severity Projection DTO
|
||||
↓
|
||||
Frontend Chart Rendering
|
||||
```
|
||||
|
||||
# Architectural Goals
|
||||
|
||||
- Eliminate frontend traversal of raw event arrays.
|
||||
- Push filtering and aggregation to the backend/database layer.
|
||||
- Keep widgets lightweight and rendering-focused.
|
||||
- Use immutable backend projection snapshots instead of frontend analytics recomputation.
|
||||
- Support contextual filtering by:
|
||||
- timeframe
|
||||
- node
|
||||
- sensor
|
||||
- archive mode
|
||||
- Maintain websocket compatibility through projection invalidation/refetch.
|
||||
|
||||
---
|
||||
|
||||
|
||||
# Directory Structure
|
||||
|
||||
```txt
|
||||
internal/
|
||||
├── projections/
|
||||
│ └── severity/
|
||||
│ ├── dto.go
|
||||
│ ├── calculator.go
|
||||
│ └── projection.go
|
||||
│
|
||||
├── api/
|
||||
│ └── analytics.go
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Backend Components
|
||||
|
||||
# 1. DTO Layer (`dto.go`)
|
||||
|
||||
Defines the flat API contract returned to the frontend.
|
||||
|
||||
## Example
|
||||
|
||||
```go
|
||||
type SeverityProjection struct {
|
||||
Timeframe string `json:"timeframe"`
|
||||
|
||||
Total int `json:"total"`
|
||||
Critical int `json:"critical"`
|
||||
High int `json:"high"`
|
||||
Medium int `json:"medium"`
|
||||
Low int `json:"low"`
|
||||
Info int `json:"info"`
|
||||
}
|
||||
```
|
||||
|
||||
## Design Rules
|
||||
|
||||
- Flat structure only.
|
||||
- No nested traversal required by the frontend.
|
||||
- Represents a complete immutable analytics snapshot.
|
||||
|
||||
---
|
||||
|
||||
# 2. Calculator Layer (`calculator.go`)
|
||||
|
||||
Contains pure aggregation logic.
|
||||
|
||||
## Responsibilities
|
||||
|
||||
- Iterate filtered events.
|
||||
- Count severities.
|
||||
- Return aggregated counts.
|
||||
|
||||
## Rules
|
||||
|
||||
- No HTTP access.
|
||||
- No database access.
|
||||
- No Vue/frontend concerns.
|
||||
- Pure deterministic aggregation only.
|
||||
|
||||
---
|
||||
|
||||
# 3. Projection Layer (`projection.go`)
|
||||
|
||||
Orchestrates the analytics pipeline.
|
||||
|
||||
## Responsibilities
|
||||
|
||||
1. Accept filtering context:
|
||||
- timeframe
|
||||
- node
|
||||
- sensor
|
||||
- archive visibility
|
||||
|
||||
2. Fetch filtered events from repository/storage layer.
|
||||
|
||||
3. Pass events into calculator.
|
||||
|
||||
4. Return the composed projection DTO.
|
||||
|
||||
---
|
||||
|
||||
# 4. API Handler (`analytics_handler.go`)
|
||||
|
||||
Thin HTTP adapter.
|
||||
|
||||
## Responsibilities
|
||||
|
||||
- Parse query parameters.
|
||||
- Invoke projection builder.
|
||||
- Serialize JSON response.
|
||||
|
||||
## Supported Query Parameters
|
||||
|
||||
| Parameter | Description |
|
||||
|---|---|
|
||||
| `timeframe` | Time window (`alltime`, `24h`, etc.) |
|
||||
| `node` | Filter by node ID |
|
||||
| `sensor` | Filter by sensor ID |
|
||||
| `archive` | Include archived events |
|
||||
|
||||
---
|
||||
|
||||
|
||||
# Frontend Integration
|
||||
|
||||
## Data Flow
|
||||
|
||||
### Initial Load / Filter Changes
|
||||
|
||||
1. Widget watches:
|
||||
- selected node
|
||||
- selected sensor
|
||||
- archive mode
|
||||
|
||||
2. Widget triggers:
|
||||
|
||||
```js
|
||||
eventsStore.fetchSeverityProjection(...)
|
||||
```
|
||||
|
||||
3. Store:
|
||||
- aborts stale requests
|
||||
- fetches new projection snapshot
|
||||
- replaces `severityProjection`
|
||||
|
||||
4. Widget reacts to new projection reference and updates chart.
|
||||
|
||||
---
|
||||
|
||||
## WebSocket Flow
|
||||
|
||||
### Event Arrival
|
||||
|
||||
1. New websocket event arrives.
|
||||
|
||||
2. Events store determines whether the event affects the currently active filter context.
|
||||
|
||||
3. If relevant:
|
||||
- invalidate projection
|
||||
- refetch `/api/v1/analytics/severity`
|
||||
|
||||
4. Store replaces snapshot.
|
||||
|
||||
5. Chart rerenders.
|
||||
|
||||
---
|
||||
|
||||
# Important Architectural Rule
|
||||
|
||||
The frontend does NOT:
|
||||
- increment severity counters
|
||||
- aggregate raw events
|
||||
- mutate analytics state manually
|
||||
|
||||
The backend remains the authoritative analytics source.
|
||||
|
||||
WebSockets only trigger invalidation/refetch behavior.
|
||||
|
||||
---
|
||||
|
||||
# Store Responsibilities
|
||||
|
||||
## Events Store
|
||||
|
||||
The store owns:
|
||||
|
||||
- network transport
|
||||
- request cancellation
|
||||
- projection state
|
||||
- websocket invalidation
|
||||
- contextual fetching
|
||||
|
||||
## Example Fetch Flow
|
||||
|
||||
```js
|
||||
async fetchSeverityProjection(timeframe, nodeId, sensorId)
|
||||
```
|
||||
|
||||
### Features
|
||||
|
||||
- Uses `AbortController`
|
||||
- Prevents stale responses
|
||||
- Replaces immutable snapshot references
|
||||
|
||||
---
|
||||
|
||||
# Widget Responsibilities
|
||||
|
||||
# SeverityChart.vue
|
||||
|
||||
The widget is intentionally lightweight.
|
||||
|
||||
## Responsibilities
|
||||
|
||||
- Trigger contextual projection fetches.
|
||||
- Render projection values.
|
||||
- Update Chart.js instance.
|
||||
|
||||
## Forbidden Responsibilities
|
||||
|
||||
The widget must NOT:
|
||||
|
||||
- traverse raw event arrays
|
||||
- aggregate severities
|
||||
- compute percentages from event entities
|
||||
- maintain live analytics state
|
||||
|
||||
---
|
||||
|
||||
# Current Reactivity Model
|
||||
|
||||
The widget watches:
|
||||
|
||||
```js
|
||||
watch(severityProjection, updateData)
|
||||
```
|
||||
|
||||
Because:
|
||||
- projections are immutable snapshots
|
||||
- store replaces object references
|
||||
- deep reactivity is unnecessary
|
||||
|
||||
---
|
||||
|
||||
# Performance Characteristics
|
||||
|
||||
## Backend
|
||||
|
||||
Efficient because:
|
||||
- filtering happens before aggregation
|
||||
- database applies WHERE clauses
|
||||
- aggregation runs on reduced datasets
|
||||
|
||||
## Frontend
|
||||
|
||||
Efficient because:
|
||||
- chart consumes flat primitive values
|
||||
- no array traversal
|
||||
- no deep watchers
|
||||
- no repeated reductions
|
||||
- no raw event recomputation
|
||||
|
||||
---
|
||||
|
||||
# API Endpoint
|
||||
|
||||
### GET /api/v1/events/severity
|
||||
|
||||
Returns severity distribution analytics for the fleet.
|
||||
|
||||
---
|
||||
|
||||
## Query Parameters
|
||||
|
||||
| Parameter | Description |
|
||||
|---|---|
|
||||
| `timeframe` | Timeframe filter (e.g. `alltime`, `24H`) (default: `alltime`) |
|
||||
| `node` | Filter by node ID (optional) |
|
||||
| `sensor` | Filter by sensor ID (optional) |
|
||||
| `viewingArchive` | Include archived events (`true`, `1`, `false`, or `0`) (default: `false`) |
|
||||
|
||||
---
|
||||
|
||||
## Example Requests
|
||||
|
||||
### Global Severity Distribution
|
||||
|
||||
```txt
|
||||
/api/v1/events/severity
|
||||
```
|
||||
|
||||
### Severity Distribution For a Specific Node
|
||||
|
||||
```txt
|
||||
/api/v1/events/severity?node=node-1
|
||||
```
|
||||
|
||||
### Severity Distribution For a Specific Sensor
|
||||
|
||||
```txt
|
||||
/api/v1/events/severity?node=node-1&sensor=sensor-3
|
||||
```
|
||||
|
||||
### Archived Events Only
|
||||
|
||||
```txt
|
||||
/api/v1/events/severity?viewingArchive=true
|
||||
```
|
||||
|
||||
### Combined Filtering
|
||||
|
||||
```txt
|
||||
/api/v1/events/severity?timeframe=24H&node=node-1&sensor=sensor-3&viewingArchive=true
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Example Response
|
||||
|
||||
```json
|
||||
{
|
||||
"timeframe": "alltime",
|
||||
"total": 150,
|
||||
"critical": 10,
|
||||
"high": 40,
|
||||
"medium": 50,
|
||||
"low": 30,
|
||||
"info": 20
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Current Architectural Principles
|
||||
|
||||
## Backend Owns Analytics Truth
|
||||
|
||||
The frontend never computes severity analytics from raw events.
|
||||
|
||||
---
|
||||
|
||||
## Immutable Projection Snapshots
|
||||
|
||||
Projection objects are treated as disposable snapshots:
|
||||
- fetch
|
||||
- replace
|
||||
- render
|
||||
|
||||
No in-place mutation.
|
||||
|
||||
---
|
||||
|
||||
## Contextual Fetching
|
||||
|
||||
Analytics projections are contextual:
|
||||
- node selection changes projection
|
||||
- sensor selection changes projection
|
||||
- archive mode changes projection
|
||||
|
||||
---
|
||||
|
||||
## Lightweight Widgets
|
||||
|
||||
Widgets are rendering-focused, not analytics-focused.
|
||||
|
||||
---
|
||||
@@ -1,342 +0,0 @@
|
||||
# Uptime Projection Architecture Guide
|
||||
|
||||
This document describes the refactored uptime analytics architecture, which moves from frontend-computed UI to backend-generated projections.
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
### Backend Projection Model
|
||||
```
|
||||
Database → Backend Calculations → Typed DTOs → Frontend Strict Rendering
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Strong type contracts between backend and frontend
|
||||
- Single source of truth for all uptime logic
|
||||
- Pure business logic separated from HTTP handlers
|
||||
- Testable calculations with zero external dependencies
|
||||
- Frontend only does shallow hydration for real-time updates
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
internal/
|
||||
├── projections/ # New domain: analytics/UI projection layer
|
||||
│ └── uptime/
|
||||
│ ├── dto.go # API contracts (zero logic)
|
||||
│ ├── calculator.go # Pure business logic
|
||||
│ └── projection.go # Orchestration & mapping
|
||||
├── api/
|
||||
│ ├── analytics.go # Thin HTTP handler
|
||||
│ └── ... (other handlers)
|
||||
└── ... (other packages)
|
||||
```
|
||||
|
||||
## Component Responsibilities
|
||||
|
||||
### 1. DTOs (`dto.go`)
|
||||
|
||||
**Purpose:** Define the exact JSON structure the frontend expects.
|
||||
|
||||
**Rules:**
|
||||
- Zero business logic or methods
|
||||
- Explicit JSON tags on all fields
|
||||
- Immutable once defined (breaking changes require versioning)
|
||||
|
||||
**Types:**
|
||||
- `UptimeResponse`: Root response object
|
||||
- `UptimeSummary`: Fleet-wide statistics
|
||||
- `UptimeGroup`: Sensors grouped by node
|
||||
- `UptimeSensor`: Individual sensor with blocks
|
||||
- `UptimeBlock`: Single time-bucket heatmap cell
|
||||
|
||||
### 2. Calculator (`calculator.go`)
|
||||
|
||||
**Purpose:** Pure business logic isolated from HTTP/database concerns.
|
||||
|
||||
**Rules:**
|
||||
- No database access (data passed as parameters)
|
||||
- No HTTP request context
|
||||
- Functions should be deterministic and testable
|
||||
- Descriptive names that explain intent
|
||||
|
||||
**Key Functions:**
|
||||
- `CalculateParams()`: Determine block count, delta, expected pings per timeframe
|
||||
- `BuildHeartbeatHistory()`: Aggregate heartbeats into time-bucketed map
|
||||
- `CalculateBlockStatus()`: Determine up/down/degraded for a single block
|
||||
- `GenerateBlocks()`: Build heatmap for a sensor
|
||||
- `ResolveWorstStatus()`: Determine worst status from list (down > degraded > up)
|
||||
- `CalculateOverallUptime()`: Fleet-wide uptime percentage
|
||||
|
||||
**Testing Strategy:**
|
||||
```go
|
||||
// All calculator functions can be tested directly without mocking
|
||||
func TestCalculateBlockStatus(t *testing.T) {
|
||||
status := CalculateBlockStatus(start, end, now, firstSeen, pings, params, idx)
|
||||
assert.Equal(t, "up", status.Status)
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Projection (`projection.go`)
|
||||
|
||||
**Purpose:** Orchestrate data flow from storage to DTOs.
|
||||
|
||||
**Responsibilities:**
|
||||
1. Accept `FilterCriteria` (timeframe, now)
|
||||
2. Fetch raw data from store
|
||||
3. Invoke calculators on raw data
|
||||
4. Map results into DTOs
|
||||
5. Return complete `UptimeResponse`
|
||||
|
||||
**Interface Design:**
|
||||
```go
|
||||
type ProjectionStore interface {
|
||||
GetNodes() ([]models.Node, error)
|
||||
GetSensorsForUptime(cutoffStr string) ([]store.SensorUptimeData, error)
|
||||
GetHeartbeatsSince(cutoffStr string) ([]store.HeartbeatData, error)
|
||||
IsSensorSilenced(nodeID, sensorID string) (bool, error)
|
||||
}
|
||||
```
|
||||
|
||||
The minimal interface allows easy testing with mocks.
|
||||
|
||||
### 4. HTTP Handler (`uptime_handler.go`)
|
||||
|
||||
**Purpose:** Parse HTTP request → Call projector → Serialize response.
|
||||
|
||||
**Rules:**
|
||||
- Zero business logic
|
||||
- Validate input early
|
||||
- Delegate to projector
|
||||
- Handle HTTP-specific concerns (status codes, error messages)
|
||||
|
||||
```go
|
||||
func (h *Handler) GetUptime(w http.ResponseWriter, r *http.Request) {
|
||||
// 1. Parse & validate
|
||||
timeframe := r.URL.Query().Get("timeframe")
|
||||
if !isValidTimeframe(timeframe) {
|
||||
RespondError(w, "Invalid timeframe", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// 2. Delegate
|
||||
projector := uptime.NewProjector(h.Store)
|
||||
projection, err := projector.BuildUptimeProjection(uptime.FilterCriteria{...})
|
||||
if err != nil {
|
||||
RespondError(w, "Failed to build projection", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// 3. Serialize
|
||||
SendJSON(w, http.StatusOK, projection)
|
||||
}
|
||||
```
|
||||
|
||||
## Frontend Integration
|
||||
|
||||
### Data Flow
|
||||
|
||||
**Initial Load:**
|
||||
1. Component mounts → Fleet store's `fetchUptime()` calls `GET /api/v1/uptime?timeframe=24H`
|
||||
2. API returns `UptimeResponse` → Stored in `uptimeData`
|
||||
3. Component computes `hydratedGroups` → Shallow hydration with live status from fleet store
|
||||
4. Template renders from `hydratedGroups.groups` (already grouped, no computation)
|
||||
|
||||
**Real-time Updates (WebSocket):**
|
||||
1. `heartbeat` event updates `fleet.nodes[nodeId].installedSensors[sensorId].status`
|
||||
2. Vue reactivity triggers `hydratedGroups` recomputation
|
||||
3. Only the "Current" block's status is updated (shallow hydration)
|
||||
4. Historical blocks are unchanged (important!)
|
||||
|
||||
### Frontend Responsibilities
|
||||
|
||||
**Allowed:**
|
||||
- Rendering the nested structure
|
||||
- Shallow hydration for live status
|
||||
- Filtering/sorting on UI view
|
||||
- Animation/transition effects
|
||||
|
||||
**Forbidden:**
|
||||
- Grouping sensors by node (backend does this)
|
||||
- Calculating worst_status
|
||||
- Recalculating overall_uptime
|
||||
- Inferring historical downtime blocks
|
||||
- Joining data from multiple sources
|
||||
|
||||
### Hydration Function
|
||||
|
||||
```typescript
|
||||
// Only update the "Current" block's live status
|
||||
const hydrateGroupsWithLiveStatus = (groups) => {
|
||||
return groups.map(group => ({
|
||||
...group,
|
||||
sensors: group.sensors.map(sensor => {
|
||||
const blocks = [...sensor.blocks]
|
||||
if (blocks.length > 0) {
|
||||
const lastBlock = blocks[blocks.length - 1]
|
||||
lastBlock.status = isLiveOnline ? 'up' : 'down'
|
||||
}
|
||||
return { ...sensor, blocks }
|
||||
})
|
||||
}))
|
||||
}
|
||||
```
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### GET /api/v1/uptime
|
||||
|
||||
**Query Parameters:**
|
||||
- `timeframe` (string): `1H`, `24H`, `7D`, `30D` (default: `24H`)
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"timeframe": "24H",
|
||||
"generatedAt": "2026-05-23T14:30:00Z",
|
||||
"summary": {
|
||||
"overallUptime": 99.52
|
||||
},
|
||||
"groups": [
|
||||
{
|
||||
"nodeId": "prod-server-1",
|
||||
"nodeAlias": "Production Primary",
|
||||
"worstStatus": "up",
|
||||
"sensors": [
|
||||
{
|
||||
"sensorId": "hw-tcp-tarpit",
|
||||
"displayName": "TCP Tarpit",
|
||||
"status": "up",
|
||||
"isSilenced": false,
|
||||
"blocks": [
|
||||
{
|
||||
"status": "up",
|
||||
"label": "Online",
|
||||
"timeLabel": "Current"
|
||||
},
|
||||
{
|
||||
"status": "up",
|
||||
"label": "Online",
|
||||
"timeLabel": "1 hours ago"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Extending the Architecture
|
||||
|
||||
### Adding a New Calculation
|
||||
|
||||
1. Add function to `calculator.go`:
|
||||
```go
|
||||
func CalculateSLABreach(blocks []UptimeBlock) bool {
|
||||
// Pure logic only
|
||||
}
|
||||
```
|
||||
|
||||
2. Add field to DTO in `dto.go`:
|
||||
```go
|
||||
type UptimeSensor struct {
|
||||
// ... existing fields
|
||||
SLABreached bool `json:"sla_breached"`
|
||||
}
|
||||
```
|
||||
|
||||
3. Invoke calculator in `projection.go`:
|
||||
```go
|
||||
sensorDTO.SLABreached = CalculateSLABreach(blocks)
|
||||
```
|
||||
|
||||
4. Update frontend component to render new field (if needed)
|
||||
|
||||
### Adding a New Timeframe
|
||||
|
||||
1. Add case to `CalculateParams()` in `calculator.go`
|
||||
2. Update `formatTimeLabel()` to handle new granularity
|
||||
3. Update frontend's timeframe dropdown (if needed)
|
||||
4. No handler changes needed!
|
||||
|
||||
### Testing the Projection Layer
|
||||
|
||||
```go
|
||||
// Create a test store mock
|
||||
type MockStore struct {
|
||||
nodes []models.Node
|
||||
sensors []store.SensorUptimeData
|
||||
heartbeats []store.HeartbeatData
|
||||
}
|
||||
|
||||
func (m *MockStore) GetNodes() ([]models.Node, error) {
|
||||
return m.nodes, nil
|
||||
}
|
||||
|
||||
// Run test
|
||||
func TestBuildUptimeProjection(t *testing.T) {
|
||||
store := &MockStore{...}
|
||||
projector := uptime.NewProjector(store)
|
||||
result, err := projector.BuildUptimeProjection(uptime.FilterCriteria{...})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "up", result.Groups[0].WorstStatus)
|
||||
}
|
||||
```
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
### ❌ Adding business logic to the handler
|
||||
```go
|
||||
// WRONG
|
||||
func (h *Handler) GetUptime(w http.ResponseWriter, r *http.Request) {
|
||||
worst := "" // Don't calculate here!
|
||||
}
|
||||
|
||||
// RIGHT
|
||||
func (h *Handler) GetUptime(w http.ResponseWriter, r *http.Request) {
|
||||
projection, _ := projector.BuildUptimeProjection(criteria)
|
||||
SendJSON(w, http.StatusOK, projection)
|
||||
}
|
||||
```
|
||||
|
||||
### ❌ Frontend re-computing projections
|
||||
```javascript
|
||||
// WRONG
|
||||
const groups = computed(() => {
|
||||
return flatten(uptimeData).map(s => ({
|
||||
worst: calculateWorst(s.blocks) // Don't do this!
|
||||
}))
|
||||
})
|
||||
|
||||
// RIGHT
|
||||
const hydratedGroups = computed(() => {
|
||||
return hydrateWithLiveStatus(uptimeData?.groups)
|
||||
})
|
||||
```
|
||||
|
||||
### ❌ Changing DTOs without versioning
|
||||
```go
|
||||
// WRONG: Existing frontend breaks
|
||||
type UptimeSensor struct {
|
||||
// Removed: SensorID string
|
||||
Identifier string // Use new name
|
||||
}
|
||||
|
||||
// RIGHT: Add new field, deprecate old
|
||||
type UptimeSensor struct {
|
||||
SensorID string `json:"sensor_id"` // Keep for compatibility
|
||||
Identifier string `json:"identifier"` // New field
|
||||
}
|
||||
```
|
||||
|
||||
## Maintenance Guidelines
|
||||
|
||||
### When to Update Each Layer
|
||||
|
||||
| Change | Where | Why |
|
||||
|--------|-------|-----|
|
||||
| Fix uptime calculation bug | `calculator.go` | Isolated, testable |
|
||||
| Add time range filter | `projection.go` | Doesn't touch DTOs |
|
||||
| New status type | `calculator.go` + `dto.go` | Pure logic + contract |
|
||||
| Style changes | Frontend component | No backend impact |
|
||||
| Fetch different data | `projection.go` store interface | Fetch layer only |
|
||||
@@ -1,244 +0,0 @@
|
||||
# Threat Velocity Analytics Projection Architecture Guide
|
||||
|
||||
This document describes the threat velocity analytics architecture used by the dashboard's line chart.
|
||||
The system has been refactored away from frontend-side event array traversal and time-bucketing, and now uses backend-generated analytics projections with contextual filtering.
|
||||
|
||||
# Architecture Overview
|
||||
|
||||
## Backend Projection Model
|
||||
```
|
||||
Raw Events
|
||||
↓
|
||||
Backend Filtering (timeframe/node/sensor/archive)
|
||||
↓
|
||||
Backend Time-Bucketing & Aggregation
|
||||
↓
|
||||
Time-Series Velocity Projection DTO
|
||||
↓
|
||||
Frontend Chart Rendering
|
||||
```
|
||||
|
||||
# Architectural Goals
|
||||
|
||||
- Eliminate frontend traversal and date-math over raw event arrays.
|
||||
- Push filtering, time-bucketing, and aggregation to the backend/database layer.
|
||||
- Keep widgets lightweight and strictly focused on chart rendering.
|
||||
- Ensure deterministic time boundaries using precise timestamp math.
|
||||
- Support contextual filtering by:
|
||||
- timeframe
|
||||
- node
|
||||
- sensor
|
||||
- archive mode
|
||||
- Maintain precise real-time synchronization through smart rollover timeouts and websocket invalidation.
|
||||
|
||||
---
|
||||
|
||||
# Directory Structure
|
||||
|
||||
```txt
|
||||
internal/
|
||||
├── projections/
|
||||
│ └── velocity/
|
||||
│ ├── dto.go
|
||||
│ ├── calculator.go
|
||||
│ ├── projection.go
|
||||
│ └── VELOCITY_ARCHITECTURE.md
|
||||
│
|
||||
├── api/
|
||||
│ └── analytics.go (GetVelocityAnalytics)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Backend Components
|
||||
|
||||
# 1. DTO Layer (`dto.go`)
|
||||
|
||||
Defines the flat API contract returned to the frontend. Unlike Severity, Velocity requires time-series arrays.
|
||||
|
||||
## Example
|
||||
|
||||
```go
|
||||
type ThreatVelocityProjection struct {
|
||||
Timeframe string `json:"timeframe"`
|
||||
BucketSizeMs int64 `json:"bucketSizeMs"`
|
||||
GeneratedAt int64 `json:"generatedAt"`
|
||||
|
||||
BucketTimestamps []int64 `json:"bucketTimestamps"` // Raw epoch timestamps
|
||||
Labels []string `json:"labels"` // X-axis labels (e.g., "Now", "-2m")
|
||||
ExactTimes []string `json:"exactTimes"` // Tooltip timestamps
|
||||
|
||||
Series map[string][]int `json:"series"` // Map of severity to bucketed counts
|
||||
RecentEventCount int `json:"recentEventCount"`
|
||||
}
|
||||
```
|
||||
|
||||
## Design Rules
|
||||
|
||||
- Pre-calculated axis arrays (`Labels`, `ExactTimes`).
|
||||
- Ready-to-use dataset map (`Series`).
|
||||
- Contains all necessary metadata for the frontend to know *when* the data expires (`BucketSizeMs`, `GeneratedAt`).
|
||||
|
||||
---
|
||||
|
||||
# 2. Calculator Layer (`calculator.go`)
|
||||
|
||||
Contains pure time-bucketing and aggregation logic.
|
||||
|
||||
## Responsibilities
|
||||
|
||||
- Determine bucket counts and sizes based on `timeframe`.
|
||||
- Generate precise start times and human-readable labels for each bucket.
|
||||
- Iterate filtered events, parse timestamps, and place them in the correct bucket index.
|
||||
- Return the populated multi-series struct.
|
||||
|
||||
## Rules
|
||||
|
||||
- Pure deterministic aggregation only.
|
||||
- Accepts `time.Now()` as an explicit parameter for testability and deterministic generation.
|
||||
|
||||
---
|
||||
|
||||
# 3. Projection Layer (`projection.go`)
|
||||
|
||||
Orchestrates the velocity pipeline.
|
||||
|
||||
## Responsibilities
|
||||
|
||||
1. Accept filtering context (timeframe, node, sensor, archive mode).
|
||||
2. Fetch events from the store.
|
||||
3. Pass events and `time.Now().UTC()` to the calculator.
|
||||
4. Return the composed `ThreatVelocityProjection`.
|
||||
|
||||
---
|
||||
|
||||
# Frontend Integration
|
||||
|
||||
## Data Flow
|
||||
|
||||
### Initial Load / Filter Changes
|
||||
|
||||
1. Widget watches contextual state.
|
||||
2. Widget triggers `eventsStore.fetchThreatVelocityProjection(...)`.
|
||||
3. Store replaces `threatVelocityProjection` with a new immutable snapshot.
|
||||
4. Widget reacts to snapshot replacement and updates Chart.js.
|
||||
|
||||
---
|
||||
|
||||
## Real-Time Synchronization (The Rollover Ticker)
|
||||
|
||||
Because Threat Velocity is a time-series chart, the buckets must shift forward at exact intervals, even if no new events arrive.
|
||||
|
||||
Instead of a naive `setInterval`, the widget dynamically calculates the precise delay until the next time boundary based on `bucket_size_ms`.
|
||||
|
||||
```js
|
||||
const scheduleNextRollover = () => {
|
||||
// Clear any existing timeout so they don't pile up
|
||||
if (rolloverTimeout) clearTimeout(rolloverTimeout)
|
||||
if (!projection.value?.bucket_size_ms) return
|
||||
|
||||
const bucketMs = projection.value.bucket_size_ms
|
||||
const now = Date.now()
|
||||
|
||||
// Find the exact millisecond of the next bucket boundary
|
||||
const nextBoundary = Math.ceil(now / bucketMs) * bucketMs
|
||||
|
||||
// Add a tiny 100ms buffer to ensure we safely crossed the time boundary
|
||||
// before asking the backend for the new data.
|
||||
const delay = nextBoundary - now + 100
|
||||
|
||||
rolloverTimeout = setTimeout(() => {
|
||||
// When the boundary hits, request fresh data
|
||||
fetchContextualProjection()
|
||||
|
||||
// Note: We don't recursively call scheduleNextRollover() here.
|
||||
// Why? Because fetchContextualProjection() will fetch a new projection,
|
||||
// which will trigger the watcher below, which will safely schedule the NEXT tick.
|
||||
}, delay)
|
||||
}
|
||||
|
||||
watch(
|
||||
() => projection.value?.generated_at,
|
||||
(newVal) => {
|
||||
if (newVal) {
|
||||
updateData();
|
||||
scheduleNextRollover();
|
||||
}
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
## WebSocket Flow
|
||||
|
||||
### Event Arrival
|
||||
|
||||
1. New websocket event arrives.
|
||||
2. Store determines if event affects current filter context.
|
||||
3. If relevant, store calls `invalidateThreatVelocityProjection()`.
|
||||
4. Widget watches invalidation state. If not viewing the archive, it triggers `fetchContextualProjection()`.
|
||||
|
||||
---
|
||||
|
||||
# Important Architectural Rule
|
||||
|
||||
The frontend does NOT:
|
||||
- Calculate bucket sizes or relative minutes/hours.
|
||||
- Iterate raw event arrays to build datasets.
|
||||
- Maintain a local ticking clock for chart dataset shifting.
|
||||
|
||||
The backend is the authoritative source for both **data** and **time labels**.
|
||||
|
||||
---
|
||||
|
||||
# Store Responsibilities
|
||||
|
||||
## Events Store
|
||||
|
||||
The store owns:
|
||||
|
||||
- Network transport (using `AbortController`)
|
||||
- `threatVelocityProjection` state
|
||||
- Contextual fetching via query params.
|
||||
- Blind invalidation marker (`lastVelocityInvalidation`).
|
||||
|
||||
---
|
||||
|
||||
# API Endpoint
|
||||
|
||||
### GET /api/v1/events/velocity
|
||||
|
||||
Returns time-bucketed velocity analytics for the fleet.
|
||||
|
||||
---
|
||||
|
||||
## Query Parameters
|
||||
|
||||
| Parameter | Description |
|
||||
|---|---|
|
||||
| `timeframe` | Timeframe filter (`1H`, `24H`, `7D`, `30D`) (default: `24H`) |
|
||||
| `nodeId` | Filter by node ID (optional) |
|
||||
| `sensorId` | Filter by sensor ID (optional) |
|
||||
| `archived` | Include archived events (`true` or `false`) (default: `false`) |
|
||||
|
||||
---
|
||||
|
||||
## Example Response
|
||||
|
||||
```json
|
||||
{
|
||||
"timeframe": "24H",
|
||||
"bucketSizeMs": 3600000,
|
||||
"generatedAt": 1716552000000,
|
||||
"bucketTimestamps": [1716465600000, 1716469200000],
|
||||
"labels": ["-23h", "-22h", "Now"],
|
||||
"exactTimes": ["May 23, 08:00 AM", "May 23, 09:00 AM"],
|
||||
"series": {
|
||||
"critical": [0, 1, 0],
|
||||
"high": [2, 0, 5],
|
||||
"medium": [0, 0, 0],
|
||||
"low": [0, 0, 0],
|
||||
"info": [1, 3, 2]
|
||||
},
|
||||
"recentEventCount": 14
|
||||
}
|
||||
```
|
||||
@@ -1,473 +0,0 @@
|
||||
# HoneyWire Frontend Architecture & Data Flow
|
||||
|
||||
This document explains the structural design, state management, data flow, and real-time update strategy of the HoneyWire frontend. It focuses on how data is stored, updated, rendered, and the distinction between WebSocket (realtime) and API (authoritative) data sources.
|
||||
|
||||
For practical development guidelines, project structure, and component rules, see [Frontend Developer Guide](./Frontend.md).
|
||||
|
||||
---
|
||||
|
||||
# Table of Contents
|
||||
|
||||
1. [Layered Architecture](#layered-architecture)
|
||||
2. [Core Principles](#core-principles)
|
||||
3. [State Storage](#state-storage)
|
||||
4. [Data Flow Lifecycle](#data-flow-lifecycle)
|
||||
5. [API Data vs WebSocket Data](#api-data-vs-websocket-data)
|
||||
6. [Normalization & Reactivity](#normalization--reactivity)
|
||||
7. [WebSocket Integration](#websocket-integration)
|
||||
8. [Persistence Boundaries](#persistence-boundaries)
|
||||
9. [Bootstrap & Lifecycle](#bootstrap--lifecycle)
|
||||
10. [Error Handling & Rollback](#error-handling--rollback)
|
||||
11. [Debugging Guide](#debugging-guide)
|
||||
12. [Analytics Projection Architecture](#analytics-projection-architecture)
|
||||
|
||||
---
|
||||
|
||||
# Layered Architecture
|
||||
|
||||
HoneyWire enforces strict layered architecture with unidirectional dependencies:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ Views & Components │
|
||||
│ (UI rendering + ephemeral state) │
|
||||
├─────────────────────────────────────────┤
|
||||
│ Stores (Pinia) │
|
||||
│ (Business logic + state ownership) │
|
||||
├─────────────────────────────────────────┤
|
||||
│ API Client · WebSocket Service │
|
||||
│ (Transport + error handling) │
|
||||
├─────────────────────────────────────────┤
|
||||
│ Utils & Helpers │
|
||||
│ (Shared functions) │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
No layer reaches above itself.
|
||||
- Views never call APIs directly.
|
||||
- Stores never import Vue or manage UI state.
|
||||
- Services never touch component state.
|
||||
- API Client never implements business logic.
|
||||
|
||||
---
|
||||
|
||||
# Core Principles
|
||||
|
||||
1. **Deterministic rendering** — Same state always produces same output
|
||||
2. **Centralized state ownership** — Stores are the single source of truth
|
||||
3. **Optimistic responsiveness** — UI updates immediately, backend confirms asynchronously
|
||||
4. **Rollback safety** — Every mutation has a clear undo path
|
||||
5. **Reactive identity stability** — Array/object references preserved, never reassigned
|
||||
6. **Normalized boundaries** — Data normalized once at store entry point, never in components
|
||||
7. **Transport abstraction** — API layer decoupled from business logic
|
||||
8. **Predictable data flow** — One direction, one owner per piece of state
|
||||
9. **Backend-owned analytics** — Aggregations and projections are computed server-side, never derived from raw frontend entity traversal
|
||||
10. **Immutable projection snapshots** — Analytics projections replace object references intentionally
|
||||
11. **Contextual analytics orchestration** — Widgets request projections based on active UI context
|
||||
12. **Transport race protection** — Projection fetches use AbortController cancellation to prevent stale overwrites
|
||||
|
||||
## Design Goals
|
||||
|
||||
The frontend architecture is optimized for:
|
||||
- deterministic realtime synchronization
|
||||
- low-latency UI updates
|
||||
- backend-authoritative analytics
|
||||
- predictable Vue reactivity behavior
|
||||
- resilience to websocket disconnects
|
||||
- minimal component-level business logic
|
||||
|
||||
---
|
||||
|
||||
# State Storage
|
||||
|
||||
HoneyWire uses three main Pinia stores, each owning a distinct domain:
|
||||
|
||||
## Store: `app.js` — Application & Auth State
|
||||
|
||||
**Ownership:** UI navigation, authentication, system state
|
||||
|
||||
| State | Purpose | Source |
|
||||
|-------|---------|--------|
|
||||
| `isAuthenticated` | Shell reveal toggle | Set by App.vue after `loadAppData()` completes |
|
||||
| `requiresSetup` | Initial setup flow gate | API: `GET /api/v1/setup/status` |
|
||||
| `currentView` | Active page (dashboard, fleet, settings, etc.) | UI selection |
|
||||
| `sidebarOpen` | Sidebar visibility toggle | UI toggle |
|
||||
| `viewingArchive` | Archive view mode (vs active events) | UI toggle |
|
||||
| `isArmed` | System armed/disarmed state | API: `GET /api/v1/system/state` + WS updates |
|
||||
| `version` | Hub version string | API: `GET /api/v1/version` |
|
||||
| `activeTimeframe` | Dashboard chart timeframe (24H, 7D, 30D, 1H) | UI selection |
|
||||
| `velocityTimeframe` | Threat velocity timeframe | UI selection |
|
||||
|
||||
### Action Patterns
|
||||
- `login(password)` / `logout()` — Session management.
|
||||
- `completeSetup()` / `checkSetupStatus()` — Hub setup coordination.
|
||||
- `toggleArmed()` — Optimistic update of system arming state, with rollback on failure.
|
||||
|
||||
---
|
||||
|
||||
## Store: `fleet.js` — Infrastructure State
|
||||
|
||||
**Ownership:** Nodes, sensors, uptime, deployment metadata
|
||||
|
||||
### State Structure
|
||||
|
||||
```javascript
|
||||
{
|
||||
nodes: [
|
||||
{
|
||||
id: "node-abc",
|
||||
alias: "production-db",
|
||||
tags: ["database", "prod"],
|
||||
status: "up" | "down" | "unknown" | "pending",
|
||||
publicIp: "203.0.113.5",
|
||||
privateIp: "10.0.1.5",
|
||||
lastHeartbeat: 1716345600000,
|
||||
installedSensors: [
|
||||
{
|
||||
id: "tcp-tarpit-1",
|
||||
status: "up",
|
||||
isSilenced: false,
|
||||
lastHeartbeat: 1716345600000,
|
||||
// ...
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
uptimeData: [ ... ],
|
||||
selectedNode: "node-abc" | null,
|
||||
selectedSensor: "tcp-tarpit-1" | null,
|
||||
activeTimeframe: "24H" | "7D" | "30D" | "1H"
|
||||
}
|
||||
```
|
||||
|
||||
### Computed Properties (Indexed Access)
|
||||
|
||||
For O(1) lookup performance, the store maintains computed maps:
|
||||
|
||||
```javascript
|
||||
const getNode = (nodeId) => nodeMap.value[nodeId] || null
|
||||
const getSensor = (nodeId, sensorId) => sensorIndex.value[nodeId]?.[sensorId] || null
|
||||
```
|
||||
|
||||
**Critical:** Always use composite key `node_id + sensor_id`. Sensor IDs are only unique within a node.
|
||||
|
||||
### Action Patterns
|
||||
- **Data Fetching**: The store fetches complete node lists on cold boot, or individual node details after a mutation (like adding/removing a sensor).
|
||||
- **Mutations (Optimistic Update)**: Actions like creating a node, or toggling sensor silence, are applied immediately to the local state, followed by an API request. On error, the state is rolled back.
|
||||
- **Deletions**: Entities are removed locally immediately, and a full refetch is triggered if the backend deletion fails.
|
||||
|
||||
---
|
||||
|
||||
## Store: `events.js` — Telemetry State
|
||||
|
||||
**Ownership:** Intrusion events, event filtering, unread tracking
|
||||
|
||||
### State Structure
|
||||
|
||||
```javascript
|
||||
{
|
||||
events: [
|
||||
{
|
||||
id: "event-xyz",
|
||||
node_id: "node-abc",
|
||||
sensor_id: "tcp-tarpit-1",
|
||||
severity: "critical",
|
||||
is_read: 0 | 1,
|
||||
is_archived: 0 | 1,
|
||||
// ...
|
||||
}
|
||||
],
|
||||
unreadCount: 5,
|
||||
activeEvent: null,
|
||||
isFetching: false
|
||||
}
|
||||
```
|
||||
|
||||
### Filtering Model
|
||||
|
||||
Events are filtered reactively by:
|
||||
- archive mode
|
||||
- selected node
|
||||
- selected sensor
|
||||
|
||||
Filtering occurs entirely inside the store so components never implement filtering logic.
|
||||
|
||||
### Action Patterns
|
||||
- **Fetching**: Retrieves events based on active filters (archived state, node, sensor).
|
||||
- **Mutations**: Marking events as read or archived.
|
||||
- **WebSocket Updates**: Appends new incoming events to the local list immediately.
|
||||
|
||||
---
|
||||
|
||||
# Data Flow Lifecycle
|
||||
|
||||
## 1. User Action → Store → API → Backend
|
||||
|
||||
**Example: User toggles silence on a sensor**
|
||||
|
||||
```
|
||||
View clicks: "Silence Sensor"
|
||||
↓
|
||||
View calls: fleetStore.toggleSilence(nodeId, sensorId, true)
|
||||
↓
|
||||
Store Action starts:
|
||||
1. Save previous state: const previous = sensor.isSilenced
|
||||
2. OPTIMISTIC: sensor.isSilenced = true ← UI updates immediately
|
||||
3. Await API: api.patch(`/api/v1/nodes/${nodeId}/sensors/${sensorId}/silence`, ...)
|
||||
↓
|
||||
Backend processes request
|
||||
↓
|
||||
Success (2xx): Store does nothing (UI already updated)
|
||||
Error (4xx/5xx): ROLLBACK sensor.isSilenced = previous, show toast
|
||||
```
|
||||
|
||||
Pattern: Optimistic first, confirm async, rollback on error.
|
||||
|
||||
---
|
||||
|
||||
## 2. API Fetch → Store → Normalize → Merge → UI Update
|
||||
|
||||
**Example: Fetch fleet on cold boot**
|
||||
|
||||
```
|
||||
App.vue calls: await fleetStore.fetchFleet()
|
||||
↓
|
||||
Store Action:
|
||||
1. API: await api.get('/api/v1/nodes')
|
||||
2. NORMALIZE: raw.map(normalizeNode)
|
||||
3. MERGE with existing (in-place)
|
||||
↓
|
||||
Vue reactivity triggered
|
||||
↓
|
||||
UI re-renders with new data
|
||||
```
|
||||
|
||||
Pattern: Normalize at boundary, preserve array identity, merge existing to prevent watchers breaking.
|
||||
|
||||
---
|
||||
|
||||
## 3. WebSocket Event → Service → Store Handler → UI Update
|
||||
|
||||
**Example: Backend broadcasts NEW_SENSOR event**
|
||||
|
||||
```
|
||||
Backend: Node deployed a sensor
|
||||
↓
|
||||
WS broadcast: { type: "NEW_SENSOR", payload: { ... } }
|
||||
↓
|
||||
ws.js routes to App.vue handler
|
||||
↓
|
||||
fleetStore.handleWsUpdate('NEW_SENSOR', payload)
|
||||
↓
|
||||
NORMALIZE and PUSH to node's array
|
||||
↓
|
||||
Vue reactivity triggered
|
||||
```
|
||||
|
||||
Pattern: WebSocket updates are applied immediately (no rollback), optionally trigger full refetch for authoritative state.
|
||||
|
||||
---
|
||||
|
||||
# API Data vs WebSocket Data
|
||||
|
||||
## API Data (Authoritative)
|
||||
|
||||
**Characteristics:**
|
||||
- Source of truth — represents backend state at fetch time
|
||||
- Complete — includes all fields and nested data
|
||||
- Normalized
|
||||
- Used for: Cold boot, manual refreshes, critical mutations
|
||||
|
||||
## WebSocket Data (Realtime Delta)
|
||||
|
||||
**Characteristics:**
|
||||
- Incremental — only includes changed fields
|
||||
- Immediate
|
||||
- Event-driven
|
||||
- Used for: Heartbeats, new events, config syncs
|
||||
|
||||
Priority: WebSocket updates are applied immediately; API fetches verify and correct state.
|
||||
|
||||
---
|
||||
|
||||
# Normalization & Reactivity
|
||||
|
||||
## Data Normalization
|
||||
|
||||
All backend payloads are normalized at the store boundary using `normalize*` functions. Components never normalize. This absorbs backend schema changes at the boundary and provides components with a consistent frontend schema.
|
||||
|
||||
## Reactive Identity Preservation
|
||||
|
||||
Vue 3 reactivity depends on object identity. If you break the identity, watchers and computed properties fail.
|
||||
|
||||
### ❌ WRONG: Reassignment breaks identity
|
||||
|
||||
```javascript
|
||||
nodes.value = newArray.map(normalizeNode) // New reference = broken watchers
|
||||
```
|
||||
|
||||
### ✅ CORRECT: Mutation preserves identity
|
||||
|
||||
Always mutate arrays/objects in-place to preserve identity. Never reassign. Use `splice()`, `push()`, `Object.assign()`.
|
||||
|
||||
```javascript
|
||||
// Clear without reassigning
|
||||
nodes.value.splice(0, nodes.value.length)
|
||||
incoming.forEach(node => nodes.value.push(node))
|
||||
|
||||
// Mutate in-place
|
||||
Object.assign(existing, { alias: "new alias" })
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# WebSocket Integration
|
||||
|
||||
The WebSocket layer is decoupled from Vue/Pinia via the `HoneyWireWS` class in `services/ws.js`.
|
||||
|
||||
**Responsibilities:**
|
||||
- Establish connection and auto-reconnect with exponential backoff.
|
||||
- Parse incoming JSON messages and dispatch to registered callbacks.
|
||||
- Maintains no state management.
|
||||
|
||||
`App.vue` registers handlers to pass parsed messages to the respective store `handleWsEvent` or `handleWsUpdate` methods.
|
||||
|
||||
---
|
||||
|
||||
# Persistence Boundaries
|
||||
|
||||
Understanding what survives a page refresh is key to the state design:
|
||||
|
||||
| State | Persistent | Source |
|
||||
|-------|------------|--------|
|
||||
| Authentication | Session cookie | Backend |
|
||||
| Fleet state | Ephemeral | Rehydrated via API bootstrap + WS |
|
||||
| Events | Ephemeral | Rehydrated via API bootstrap + WS |
|
||||
| UI selections | Optional localStorage | Frontend |
|
||||
| Projections | Ephemeral | Rehydrated via backend API |
|
||||
|
||||
---
|
||||
|
||||
# Bootstrap & Lifecycle
|
||||
|
||||
## Cold Boot Sequence
|
||||
|
||||
User loads the app (`onMounted` in App.vue):
|
||||
|
||||
1. Check if setup is required (`checkRequiresSetup`).
|
||||
2. Check if authenticated (`checkSystemState`).
|
||||
3. Load application data in parallel (`fetchFleet`, `fetchEvents`, etc.).
|
||||
4. Connect WebSocket and register handlers.
|
||||
5. **Critical Invariant:** `isAuthenticated = true` is set **last**, after all data has been fetched. This prevents the authenticated shell from rendering before stores are populated.
|
||||
|
||||
---
|
||||
|
||||
# Error Handling & Rollback
|
||||
|
||||
All API errors are caught at the store level, not the component level. State mutations use explicit rollback patterns on error.
|
||||
|
||||
```javascript
|
||||
// 1. Save previous state
|
||||
const previous = sensor.isSilenced
|
||||
// 2. OPTIMISTIC update
|
||||
sensor.isSilenced = targetState
|
||||
|
||||
try {
|
||||
// 3. Send to backend
|
||||
await api.patch(...)
|
||||
} catch (err) {
|
||||
// 4. ERROR — ROLLBACK
|
||||
sensor.isSilenced = previous
|
||||
throw err
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Debugging Guide
|
||||
|
||||
## Blank Dashboard After Login
|
||||
- Check: `loadAppData()` actually completed and all `await Promise.all([...])` calls resolved.
|
||||
- Fix: Ensure `isAuthenticated = true` is set AFTER data loads.
|
||||
|
||||
## UI Not Updating
|
||||
- Check: Array reassignment broke reactivity (`nodes.value = newArray`). Fix by using `splice()`, `push()`, `Object.assign()`.
|
||||
- Check: Watched property is accessed with `.value` in `<script setup>`.
|
||||
|
||||
## WebSocket Not Receiving Updates
|
||||
- Check: WebSocket not connected (check Network tab for "101 Switching Protocols").
|
||||
- Check: Handler not registered before `wsService.connect()`.
|
||||
- Check: Event filtered out inside `handleWsEvent()` due to `selectedNode` / `selectedSensor`.
|
||||
|
||||
## Composite Key Bugs (Sensors)
|
||||
- Symptom: Wrong sensor updated, cross-node collisions.
|
||||
- Fix: Always pass both `nodeId` and `sensorId` to get a sensor, as `sensorId` alone is not unique.
|
||||
|
||||
## Optimistic Update Didn't Rollback
|
||||
- Check: Saved state before optimistic update was not captured or was applied to the wrong object reference.
|
||||
|
||||
---
|
||||
|
||||
# Analytics Projection Architecture
|
||||
|
||||
The frontend delegates all analytics aggregations to the backend. This section covers how we manage projection state.
|
||||
|
||||
## Projection vs Entity State
|
||||
|
||||
HoneyWire distinguishes between two fundamentally different frontend state models:
|
||||
|
||||
### 1. Entity State (Mutable)
|
||||
Examples: `nodes[]`, `installedSensors[]`, `events[]`
|
||||
|
||||
Characteristics:
|
||||
- Long-lived
|
||||
- Incrementally mutated
|
||||
- Identity-preserving
|
||||
- Updated directly from WebSocket deltas
|
||||
|
||||
Rules:
|
||||
- Never replace array references
|
||||
- Merge in-place
|
||||
- Use `splice`/`push`/`Object.assign`
|
||||
|
||||
Purpose: Efficient realtime synchronization of raw operational data.
|
||||
|
||||
### 2. Projection State (Immutable)
|
||||
Examples: `severityProjection`, `threatVelocityProjection`
|
||||
|
||||
Characteristics:
|
||||
- Backend-generated
|
||||
- Flat DTO snapshots
|
||||
- Replaced atomically
|
||||
- Filter/context dependent
|
||||
|
||||
Rules:
|
||||
- Reference replacement is intentional
|
||||
- No deep watchers
|
||||
- No local aggregation
|
||||
- No array traversal derivation
|
||||
|
||||
Purpose: Efficient rendering of authoritative backend analytics.
|
||||
|
||||
Example:
|
||||
```javascript
|
||||
// Correct projection replacement
|
||||
this.severityProjection = await res.json()
|
||||
```
|
||||
Projection snapshots intentionally replace references so shallow watchers trigger deterministic chart updates.
|
||||
|
||||
## Projection Invalidation Strategy
|
||||
|
||||
Analytics projections use a different realtime model than raw entities. While raw entity updates from WebSocket mutate local state directly, realtime events for projections trigger conditional invalidation.
|
||||
|
||||
Flow (e.g. `NEW_EVENT`):
|
||||
1. WS event arrives.
|
||||
2. Store evaluates the active filter context.
|
||||
3. If the event affects the current projection:
|
||||
- Abort in-flight projection requests (via `AbortController`).
|
||||
- Refetch the updated projection snapshot from the backend.
|
||||
4. The projection reference is replaced.
|
||||
5. The chart's shallow watcher re-renders.
|
||||
|
||||
This architecture prevents frontend aggregation drift, context desynchronization, and stale filtered analytics.
|
||||
@@ -1,166 +0,0 @@
|
||||
# HoneyWire Frontend Developer Guide
|
||||
|
||||
This document contains practical guidelines, project structure, component organization, and the design system used in the HoneyWire frontend.
|
||||
|
||||
For state management, data flow, real-time updates, and architectural rules, see [Frontend Architecture](../Hub/ui/FRONTEND_ARCHITECTURE.md).
|
||||
|
||||
## Stack
|
||||
|
||||
Vue 3 · Pinia · Vite · TailwindCSS · Native WebSocket · OKLCH Design System
|
||||
|
||||
---
|
||||
|
||||
# Project Structure
|
||||
|
||||
```text
|
||||
src/
|
||||
├── api/
|
||||
│ ├── client.js # Centralized HTTP client
|
||||
│ └── useConfig.js # Config loader composable
|
||||
│
|
||||
├── assets/
|
||||
│ └── style.css # Design system tokens
|
||||
│
|
||||
├── components/
|
||||
│ ├── dashboard/ # Dashboard widgets
|
||||
│ ├── layout/ # App shell components
|
||||
│ └── ui/ # Reusable UI primitives
|
||||
│
|
||||
├── services/
|
||||
│ └── ws.js # WebSocket service
|
||||
│
|
||||
├── stores/
|
||||
│ ├── app.js # App + auth state
|
||||
│ ├── events.js # Event pipeline
|
||||
│ └── fleet.js # Infrastructure state
|
||||
│
|
||||
├── utils/
|
||||
│ ├── chartConfig.js
|
||||
│ ├── theme.js
|
||||
│ └── useDropdown.js
|
||||
│
|
||||
├── views/
|
||||
│ ├── Dashboard.vue
|
||||
│ ├── FleetView.vue
|
||||
│ ├── Login.vue
|
||||
│ ├── NodeDetailView.vue
|
||||
│ ├── Settings.vue
|
||||
│ ├── Setup.vue
|
||||
│ └── Store.vue
|
||||
│
|
||||
├── App.vue # Root orchestrator
|
||||
└── main.js # Bootstrap
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# View Structure
|
||||
|
||||
| View | Purpose |
|
||||
|---|---|
|
||||
| Dashboard | Telemetry & analytics |
|
||||
| FleetView | Node management |
|
||||
| NodeDetailView | Sensor deployment & configuration |
|
||||
| Store | Sensor catalog |
|
||||
| Settings | Hub configuration |
|
||||
| Setup | Initial setup flow |
|
||||
| Login | Authentication |
|
||||
|
||||
---
|
||||
|
||||
# Component System
|
||||
|
||||
## Dashboard Widgets
|
||||
|
||||
`components/dashboard/` — data-driven, stateless when possible:
|
||||
|
||||
- EventTable
|
||||
- SeverityChart
|
||||
- ThreatVelocity
|
||||
- TrafficFilters
|
||||
- UptimeHeatmap
|
||||
|
||||
## UI Primitives
|
||||
|
||||
`components/ui/` — reusable design-system primitives:
|
||||
|
||||
| Category | Contents |
|
||||
|---|---|
|
||||
| branding | Logos, theme elements |
|
||||
| feedback | Alerts, modals, status |
|
||||
| forms | Inputs, buttons, selectors |
|
||||
| layout | Cards, widgets, page shells |
|
||||
| navigation | Menus, sidebar, nav |
|
||||
|
||||
---
|
||||
|
||||
# Design System
|
||||
|
||||
`src/assets/style.css` — centralized token system on OKLCH color space.
|
||||
|
||||
## Principles
|
||||
|
||||
- Semantic tokens only — no hardcoded colors in components
|
||||
- Accessibility-first contrast
|
||||
- Dark/light parity
|
||||
- Theme switching swaps root variables only
|
||||
|
||||
## Token Categories
|
||||
|
||||
| Category | Examples | Usage |
|
||||
|---|---|---|
|
||||
| Structural | `--bg`, `--bg-surface`, `--border-default` | Layout hierarchy |
|
||||
| Interactive | `--primary-main`, `--secondary-main`, `--danger-main` | Actions & states |
|
||||
| Severity | `--sev-critical` through `--sev-info` | Charts, alerts, tables, status |
|
||||
| Typography | `--text-h1`, `--text-base`, `--text-sm` | All text sizing |
|
||||
| Spacing | `--space-card-p`, `--space-flow` | Layout rhythm |
|
||||
| Elevation | `--radius-sm`, `--shadow-md` | Cards, modals, widgets |
|
||||
| Z-index | `--z-dropdown` → `--z-toast` | Stacking hierarchy |
|
||||
|
||||
**No arbitrary values.** Use tokens.
|
||||
|
||||
## Typography
|
||||
|
||||
| Token | Font |
|
||||
|---|---|
|
||||
| `--font-sans` | Inter |
|
||||
| `--font-mono` | JetBrains Mono |
|
||||
|
||||
## Tailwind Integration
|
||||
|
||||
Tokens exposed via `@theme`:
|
||||
|
||||
```css
|
||||
bg-bg-surface → --bg-surface
|
||||
text-text-h → --text-h
|
||||
border-border-default → --border-default
|
||||
```
|
||||
|
||||
## Dark Mode
|
||||
|
||||
```css
|
||||
.dark { /* overrides root variables */ }
|
||||
```
|
||||
|
||||
Components should not implement separate dark styles unless absolutely necessary.
|
||||
|
||||
---
|
||||
|
||||
# Frontend Component Rules
|
||||
|
||||
## Components
|
||||
|
||||
| ✅ Do | ❌ Don't |
|
||||
|---|---|
|
||||
| Stay presentation-focused | Import the API client |
|
||||
| Receive data via props/stores | Call `fetch()` |
|
||||
| Emit actions upward | Own business logic |
|
||||
| Remain reusable | Normalize backend payloads |
|
||||
|
||||
## Views
|
||||
|
||||
| ✅ Do | ❌ Don't |
|
||||
|---|---|
|
||||
| Own ephemeral UI state | Perform backend mutations |
|
||||
| Delegate to stores | Duplicate rollback logic |
|
||||
| Orchestrate layout | Normalize API responses |
|
||||
@@ -1,95 +0,0 @@
|
||||
# Events Store Architecture Guide
|
||||
|
||||
This document describes the design, state management, and projection strategies used in the `events.ts` Pinia store.
|
||||
|
||||
The `events.ts` store manages the telemetry pipeline, bridging the gap between raw backend event logs, high-frequency WebSocket deltas, and deeply contextual UI analytics projections.
|
||||
|
||||
---
|
||||
|
||||
# 1. Entity State vs. Projection State
|
||||
|
||||
The `events.ts` store distinguishes between two fundamentally different frontend state models, which are treated with different mutation rules.
|
||||
|
||||
### Mutable Entity State
|
||||
**Examples:** `events[]`
|
||||
|
||||
* **Long-Lived:** The event list persists across page navigations.
|
||||
* **Identity-Preserving:** When new events arrive over WebSocket, they are `unshift`ed directly into the array without replacing the array reference itself.
|
||||
* **Client-Filtered:** We fetch all events for a given context and use Vue `computed` properties (`filteredEvents`) to narrow down the view based on `appStore.viewingArchive`.
|
||||
|
||||
#### The Hybrid Update Model (Refetch vs. Hydration)
|
||||
The `events[]` array uses a hybrid approach to keep the UI snappy and reduce backend load during high-frequency attacks:
|
||||
|
||||
1. **Initial Load & Filter Changes (Refetch):** When the app first loads or the user changes their active filters (e.g., selects a different node), the store issues an `api.get` request. This completely replaces the `events` array with the authoritative history from the SQLite database.
|
||||
2. **Real-time Delta Updates (Hydration):** When a `NEW_EVENT` arrives via WebSocket, the store does *not* refetch the event list. Instead, it evaluates the active filters locally inside `handleWsEvent`. If the new event belongs to the current view, it is *hydrated* directly into the local array using `unshift()`. This prepends the event to the top of the UI instantly without issuing a network request.
|
||||
|
||||
*(Note: This is the exact opposite of how Immutable Projections work, which rely exclusively on refetching).*
|
||||
|
||||
### Immutable Projection State
|
||||
**Examples:** `severityProjection`, `threatVelocityProjection`
|
||||
|
||||
* **Backend-Generated:** The UI *never* loops over local events to aggregate data. Chart data is calculated entirely by SQLite on the server.
|
||||
* **Atomically Replaced:** When a new projection arrives, the entire object reference is replaced intentionally to trigger clean, deterministic re-renders in Chart.js via Vue's shallow watchers.
|
||||
|
||||
```typescript
|
||||
// Correct projection replacement:
|
||||
state.value.threatVelocityProjection = (await response.json()) as ThreatVelocityProjection
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# 2. Contextual Invalidation Strategy
|
||||
|
||||
Because Projections are server-rendered based on the user's active filters (Timeframe, Selected Node, Selected Sensor), they are highly vulnerable to race conditions if left unmanaged during a high-frequency intrusion.
|
||||
|
||||
The store uses a combination of `AbortController` and selective invalidation to keep the dashboard stable.
|
||||
|
||||
## The Abort Controller Gatekeeper
|
||||
If a user clicks through three different timeframes rapidly, the store will cancel the in-flight HTTP requests for the previous two, guaranteeing that the final resolved promise belongs to the most recently requested context.
|
||||
|
||||
```typescript
|
||||
const fetchThreatVelocityProjection = async (...) => {
|
||||
if (velocityAbortController) velocityAbortController.abort()
|
||||
velocityAbortController = new AbortController()
|
||||
|
||||
// ... execute fetch with velocityAbortController.signal
|
||||
}
|
||||
```
|
||||
|
||||
## The Realtime Invalidation Flow
|
||||
When a `NEW_EVENT` arrives via WebSocket:
|
||||
|
||||
1. **Evaluate Context:** `handleWsEvent` checks if the new event actually belongs to the user's *current view* (e.g., if they are filtering for Node A, and the event happened on Node B, do nothing to the charts).
|
||||
2. **Invalidate Projections:** If the event *does* affect the current view, we don't attempt to manually mutate the local projection (which would cause aggregation drift). Instead, we trigger an invalidation.
|
||||
3. **Refetch:** The component (e.g., `ThreatVelocity.vue`) watches the invalidation timestamp and automatically asks the store to execute `fetchThreatVelocityProjection` again using the new exact context.
|
||||
|
||||
---
|
||||
|
||||
# 3. Composite Key Enforcement
|
||||
|
||||
Sensors in HoneyWire do not possess globally unique UUIDs; their IDs are only unique within the scope of their parent Node (e.g., a `tcp-tarpit` on `Node A` has the exact same string ID as a `tcp-tarpit` on `Node B`).
|
||||
|
||||
The `events.ts` store enforces strict composite key checks before allowing a WebSocket event into the filtered pipeline.
|
||||
|
||||
```typescript
|
||||
const sensorMatch =
|
||||
selectedSensor &&
|
||||
selectedNode &&
|
||||
payload.nodeId === selectedNode?.id &&
|
||||
payload.sensorId === selectedSensor?.sensorId
|
||||
```
|
||||
*Rule:* Filtering or matching logic must always evaluate `nodeId` AND `sensorId` simultaneously to prevent telemetry cross-contamination.
|
||||
|
||||
---
|
||||
|
||||
# 4. Encapsulation & Read-Only Access
|
||||
|
||||
Like all modern stores in the application, the actual `EventsState` is hidden inside a reactive `ref`.
|
||||
|
||||
Components accessing the store only interact with strictly exported `computed` getters. This guarantees that `App.vue` or `ThreatVelocity.vue` can never accidentally clear or overwrite the event arrays directly.
|
||||
|
||||
```typescript
|
||||
// In the store:
|
||||
const events = computed<EventPayload[]>(() => state.value.events)
|
||||
const unreadCount = computed<number>(() => state.value.unreadCount)
|
||||
```
|
||||
@@ -1,95 +0,0 @@
|
||||
# Fleet Store Architecture Guide
|
||||
|
||||
This document describes the design, state management, and normalization strategies used in the `fleet.ts` Pinia store.
|
||||
|
||||
The `fleet.ts` store manages the state of the infrastructure: Nodes, Sensors, their configurations, and their real-time heartbeats. Because this store powers complex CRUD operations, it utilizes specific architectural patterns to maintain UI consistency and speed.
|
||||
|
||||
---
|
||||
|
||||
# 1. The Composite Key Model
|
||||
|
||||
A major architectural decision in HoneyWire is that **Sensor IDs are only unique within the scope of their parent Node.**
|
||||
|
||||
If you deploy a `tcp-tarpit` to `Node A` and a `tcp-tarpit` to `Node B`, their raw `sensorId`s are identical.
|
||||
|
||||
To handle this safely on the frontend without nested, slow array traversal, the store creates a flat O(1) read model by hashing them into a **Composite Key**.
|
||||
|
||||
```typescript
|
||||
// Inside normalizeSensorData()
|
||||
const compositeId = `${nodeId}:${rawSensorId}`
|
||||
```
|
||||
|
||||
*Rule:* Any UI component or store action attempting to fetch, patch, or silence a sensor MUST pass both the `nodeId` and the `rawSensorId` to generate the correct composite hash.
|
||||
|
||||
---
|
||||
|
||||
# 2. Optimistic Update with Rollback Cache
|
||||
|
||||
For non-critical metadata changes (like renaming a Node or adding a Tag), `app.ts`'s "Reconciled Update" (wait for backend, then fetch) feels too sluggish. Instead, `fleet.ts` uses **Optimistic UI with an explicit rollback mechanism.**
|
||||
|
||||
The `patchNode` and `patchSensor` gatekeepers are designed to return the *previous* state of the object right before the patch is applied.
|
||||
|
||||
```typescript
|
||||
const updateNode = async (nodeId: string, payload: Partial<FleetNode>) => {
|
||||
// 1. Store previous state and immediately update UI
|
||||
const previousState = patchNode(nodeId, {
|
||||
alias: payload.alias
|
||||
})
|
||||
|
||||
try {
|
||||
// 2. Await Backend
|
||||
await api.patch(`/api/v1/nodes/${nodeId}`, payload)
|
||||
} catch (err) {
|
||||
// 3. Rollback the exact object on failure
|
||||
if (previousState) patchNode(nodeId, previousState)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
*Rule:* The `nodesById` and `sensorsById` record maps are the absolute truth. Never mutate the array returned by the `nodes` getter directly. Always route through `patchNode` or `patchSensor`.
|
||||
|
||||
---
|
||||
|
||||
# 3. Transport Boundary Normalization
|
||||
|
||||
The shape of the JSON that the backend returns (`rawNode`) is not necessarily the exact shape the Vue components want to consume.
|
||||
|
||||
The store implements a rigid Normalization layer at the ingress boundary (`normalizeNodeData`, `normalizeSensorData`).
|
||||
|
||||
```typescript
|
||||
const normalizeNodeData = (raw: any): FleetNode | null => {
|
||||
// Protect against undefined values and provide safe fallbacks
|
||||
return {
|
||||
id: raw.nodeId,
|
||||
alias: raw.alias || 'Unnamed Node',
|
||||
hasPendingConfig: raw.hasPendingConfig ?? false,
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
*Rule:* A raw backend response MUST pass through a normalizer before touching the State Tree. This means if the Go backend changes a property from `has_pending_config` to `hasPendingConfig`, you only have to fix the frontend in *one* exact line of code.
|
||||
|
||||
---
|
||||
|
||||
# 4. Partial Invalidation (Subgraph Fetching)
|
||||
|
||||
When a user deletes a sensor on a Node, we don't want to call `fetchFleet()` and wipe out the entire network tree just to update one branch.
|
||||
|
||||
Instead, we execute a **Subgraph Fetch** via `fetchNodeDetails(nodeId)`.
|
||||
|
||||
This function securely reaches into the `sensorsById` map, deletes *only* the sensors belonging to that specific Node using our reverse index (`sensorsByNodeId`), and then merges the fresh node payload into the State Tree.
|
||||
|
||||
---
|
||||
|
||||
# 5. Encapsulation
|
||||
|
||||
Like `app.ts` and `events.ts`, the core objects (`nodesById`, `sensorsById`) are hidden behind private `ref`s and exported as read-only `computed` properties.
|
||||
|
||||
This physically prevents Vue components from accidentally executing code like:
|
||||
|
||||
```typescript
|
||||
// Typescript will block this:
|
||||
fleetStore.nodes[0].alias = "My Hacked Node"
|
||||
```
|
||||
@@ -1,153 +0,0 @@
|
||||
# App Store Architecture Guide
|
||||
|
||||
This document describes the architectural design, state segregation, and session management strategies used in the `app.ts` Pinia store.
|
||||
|
||||
The `app.ts` store serves as the global application orchestrator. It manages the boot sequence, identity session, and critical system flags. It has been strictly engineered to separate frontend-owned UI state from backend-owned truths, utilizing defensive data-fetching, explicit error boundaries, and a strict Session Transition Authority.
|
||||
|
||||
---
|
||||
|
||||
# Architecture Overview
|
||||
|
||||
## The 4-Layer State Model
|
||||
|
||||
State within `app.ts` is managed in a single reactive `state` object, but is conceptually divided into categories. It is exposed to components via `computed` getters to prevent direct mutation and encapsulate the store's internal structure.
|
||||
|
||||
### 1. Backend-Synced State (Server Truth)
|
||||
The absolute source of truth for global backend settings. These are **never** optimistically mutated.
|
||||
|
||||
const state = ref<AppState>({
|
||||
isArmed: true,
|
||||
version: '1.0.0',
|
||||
// ...
|
||||
})
|
||||
|
||||
### 2. Pure UI State (Frontend Owned)
|
||||
Ephemeral state owned entirely by the client. Fully synchronous and never requires server validation.
|
||||
|
||||
const state = ref<AppState>({
|
||||
viewingArchive: false,
|
||||
sidebarOpen: true,
|
||||
currentView: 'dashboard', // 'dashboard' | 'fleet' | 'settings' | 'node-details'
|
||||
// ...
|
||||
})
|
||||
|
||||
### 3. Session State Machine & Bootstrap
|
||||
Utilizes a formal three-state session model rather than a naive boolean, allowing the UI to differentiate between "checking identity" and "not logged in."
|
||||
|
||||
const state = ref<AppState>({
|
||||
sessionState: 'unknown', // 'unknown' | 'authenticated' | 'unauthenticated'
|
||||
requiresSetup: false,
|
||||
isInitialized: false,
|
||||
// ...
|
||||
})
|
||||
|
||||
### 4. Explicit Error State
|
||||
Errors are managed state-first. UI components react to these variables rather than parsing ephemeral function return values.
|
||||
|
||||
const state = ref<AppState>({
|
||||
bootstrapError: null,
|
||||
authError: null,
|
||||
setupError: null,
|
||||
})
|
||||
|
||||
---
|
||||
|
||||
# 1. The Session Transition Authority
|
||||
|
||||
Traditional applications often use a simple `isAuthenticated: false` flag, which causes UI layout pop-in because the app assumes the user is logged out before the initial network request finishes, or scatters auth logic across many API fetchers.
|
||||
|
||||
HoneyWire uses a **Session State Machine** governed by a strict Gatekeeper (`transitionSession`):
|
||||
- `sessionState` begins as `'unknown'`.
|
||||
- The app stalls the router/UI initialization while `sessionState` is `'unknown'`.
|
||||
- **All** session changes (login, logout, 401s during background polling) must pass through `transitionSession`. This prevents race conditions, such as a lagging bootstrap overwriting a successful concurrent login.
|
||||
- The transition function uses an explicit transition matrix to block invalid state changes (e.g., authenticated -> authenticated).
|
||||
|
||||
const transitionSession = (nextState: SessionState): void => {
|
||||
if (state.value.sessionState === nextState) return
|
||||
|
||||
const validTransitions: Record<SessionState, SessionState[]> = {
|
||||
unknown: ['authenticated', 'unauthenticated'],
|
||||
authenticated: ['unauthenticated'],
|
||||
unauthenticated: ['authenticated']
|
||||
}
|
||||
|
||||
if (!validTransitions[state.value.sessionState].includes(nextState)) return
|
||||
state.value.sessionState = nextState
|
||||
}
|
||||
|
||||
---
|
||||
|
||||
# 2. Decoupled System State Fetching
|
||||
|
||||
`fetchSystemState` is a pure data-fetching function. It does not dictate application flow or own authentication logic. If it encounters a `401 Unauthorized` or `403 Forbidden` from the backend, it simply reports this to the Gatekeeper.
|
||||
|
||||
This ensures that any API failure drops the user to a safe, unauthenticated state immediately, without crashing the frontend.
|
||||
|
||||
const fetchSystemState = async () => {
|
||||
try {
|
||||
// ... fetch and commit ...
|
||||
} catch (err) {
|
||||
if (err.status === 401 || err.status === 403) {
|
||||
transitionSession('unauthenticated')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
---
|
||||
|
||||
# 3. The "Reconciled Update" Pattern
|
||||
|
||||
For critical system flags (like toggling the `isArmed` status of the entire security ecosystem), the store completely avoids Optimistic UI updates.
|
||||
|
||||
If the UI pretends the system is armed but the backend fails to apply it, the user is left with a false sense of security. Instead, `app.ts` relies on **Reconciled Updates**:
|
||||
|
||||
1. **Dispatch Intent:** Send the user's intended state to the server.
|
||||
2. **Reconcile Reality:** Regardless of success or failure, fetch the *actual* resulting state from the server.
|
||||
3. **UI Update:** The UI only updates when the newly fetched snapshot replaces the local state.
|
||||
|
||||
const toggleArmed = async () => {
|
||||
const targetState = !state.value.isArmed
|
||||
|
||||
try {
|
||||
await api.patch('/api/v1/system/state', { isArmed: targetState })
|
||||
// Reconcile reality
|
||||
await fetchSystemState()
|
||||
} catch (err) {
|
||||
// Reality check fallback
|
||||
await fetchSystemState()
|
||||
}
|
||||
}
|
||||
|
||||
---
|
||||
|
||||
# 4. State-First Error Boundaries
|
||||
|
||||
Authentication and Setup routines do not return complex error strings directly to components. Instead, they act as mutators for dedicated error states.
|
||||
|
||||
This decouples the form submission logic from the error rendering logic:
|
||||
|
||||
// In the Store:
|
||||
const login = async (password) => {
|
||||
state.value.authError = null // Clear previous
|
||||
try {
|
||||
// ...
|
||||
transitionSession('authenticated')
|
||||
return { success: true }
|
||||
} catch (err) {
|
||||
transitionSession('unauthenticated')
|
||||
state.value.authError = 'Invalid credentials'
|
||||
return { success: false }
|
||||
}
|
||||
}
|
||||
|
||||
*Rule:* Views bind directly to `appStore.authError`. The component's `onSubmit` handler only needs to check `{ success: boolean }`.
|
||||
|
||||
---
|
||||
|
||||
# 5. Bootstrap Orchestration
|
||||
|
||||
Initialization is managed by a single orchestrator: `initAppStore()`.
|
||||
|
||||
It fetches multiple independent system requirements concurrently using `Promise.allSettled`. This ensures that a failure in a non-critical endpoint (e.g., a version check) does not fatally crash the critical system state ingestion.
|
||||
|
||||
The bootstrap explicitly delegates its findings to the Gatekeeper, allowing the application to safely stall, authenticate, or fall back to the setup screen.
|
||||
+3
-1
@@ -13,6 +13,8 @@
|
||||
- The potential impact (e.g., DoS, Remote Code Execution, Authentication Bypass).
|
||||
- Any potential mitigation or fix you might suggest.
|
||||
|
||||
As a solo maintainer, I will do my best to acknowledge receipt of your vulnerability report within 48-72 hours and will work with you to patch the issue before a public CVE or advisory is published.
|
||||
Optionally you can send an email at info@honeywire.dev.
|
||||
|
||||
As a solo maintainer, I will do my best to acknowledge receipt of your vulnerability report and will work with you to patch the issue before a public CVE or advisory is published.
|
||||
|
||||
Thank you for helping keep HoneyWire secure!
|
||||
@@ -1,4 +1,4 @@
|
||||
[](../../LICENSE)
|
||||
[](../../LICENSE)
|
||||
|
||||
# Community Sensor Lab
|
||||
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
# HoneyWire Official Sensor: File Canary
|
||||
|
||||
The File Canary is a low-noise Host-Level Tamper and Ransomware detector. It safely provisions discrete decoy files across your system and explicitly maps them into an unprivileged sandbox using individual read-only mounts.
|
||||
|
||||
## Features
|
||||
* **Zero-Setup SDK Integration:** Natively built on the HoneyWire Go SDK.
|
||||
* **Tamper & Ransomware Tripwires:** Triggers on `IN_MODIFY`, `IN_DELETE_SELF`, and `IN_ATTRIB` to instantly alert on encryption routines or cleanup scripts tampering with bait artifacts.
|
||||
* **Strict Initializer Sandbox:** Validates paths, drops symlinks, ensures strict absolute targeting, and automatically creates missing artifacts without altering surrounding directory permissions.
|
||||
* **Explicit File Isolation:** Eschews broad directory tracking in favor of explicit individual file monitoring to dramatically cut false positives.
|
||||
* **Optional Access Detection:** Toggle `HW_ALERT_ON_OPEN` to catch reconnaissance activity like `cat` and `less` via `IN_OPEN` / `IN_ACCESS` events.
|
||||
|
||||
## Configuration
|
||||
|
||||
Configuration is managed through an `.env` file located in the same directory as the `docker-compose.yml`.
|
||||
|
||||
### Core Ecosystem Variables
|
||||
| Variable | Description | Example |
|
||||
|---|---|---|
|
||||
| `HW_HUB_ENDPOINT` | The URL of your central HoneyWire Hub. | `http://127.0.0.1:8080` |
|
||||
| `HW_HUB_KEY` | The Node Key to authenticate with the Hub. | `super_secret_key_123` |
|
||||
| `HW_SENSOR_ID` | A unique identifier for this specific trap. | `file-canary-01` |
|
||||
| `HW_SEVERITY` | Alert severity sent to the Hub (`info` to `critical`). | `critical` |
|
||||
### Sensor-Specific Variables
|
||||
| Variable | Description | Default |
|
||||
|---|---|---|
|
||||
| `HW_DECOY_FILES` | Comma-separated list of absolute file paths to deploy and monitor. | `/var/www/html/.backup-config.php, /opt/.env.backup` |
|
||||
| `HW_ALERT_ON_OPEN` | Generates alerts when files are simply read/opened. | `false` |
|
||||
## Deployment
|
||||
It is highly recommended to deploy this sensor using the provided `docker-compose.yml` configuration and the provided `Dockerfile` (if building from source). The compose file orchestrates the required permission fixers and read-only mounts automatically.
|
||||
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
## Security Architecture
|
||||
|
||||
This sensor is architected for extreme resilience against exploits. By utilizing a minimal attack surface and enforcing strict container sandboxing, it ensures the host filesystem remains protected.
|
||||
|
||||
**Core Defense-in-Depth Measures:**
|
||||
* **Unprivileged Execution:** Runs entirely as a non-root user (`UID 65532`), preventing system-level modifications even in the event of a container breach.
|
||||
* **Explicit Single-File Mounts:** Completely abandons broad directory mounting. Individual files are explicitly mounted with `read_only: true` flags, leaving zero surface area for recursive filesystem traversal.
|
||||
* **Symlink & Traversal Protections:** The initialization provisioner actively refuses to follow symlinks, utilize relative paths, or touch directories, entirely mitigating path traversal injections.
|
||||
* **Kernel Capability Stripping:** Drops all default Linux kernel capabilities (`cap_drop: ALL`) via the Docker Compose configuration, neutralizing advanced kernel exploitation techniques.
|
||||
* **Distroless Isolation:** Built on a statically-linked Distroless image. It completely lacks a shell (`/bin/sh`), package managers, or standard Linux utilities, leaving attackers with zero tools to pivot to the host.
|
||||
|
||||
*Recommendation: For optimal security, always deploy this sensor using the official `docker-compose.yml` and `Dockerfile` to ensure these sandbox protections are strictly enforced by the container runtime.*
|
||||
@@ -1,42 +0,0 @@
|
||||
# HoneyWire Official Sensor: ICMP Canary
|
||||
|
||||
The ICMP Canary (Ping Canary) is a simple, highly effective network tripwire. It listens for ICMP Echo Requests (pings) directed at the host machine. It is best deployed on isolated IPs, darknets, or unused subnets where any inbound ICMP traffic is inherently suspicious.
|
||||
|
||||
## Features
|
||||
* **Zero-Setup SDK Integration:** Natively built on the HoneyWire Go SDK.
|
||||
* **Raw Socket Listening:** Uses pure Go to listen directly for protocol 1 (ICMP) packets without external C-dependencies.
|
||||
* **Low Overhead:** Requires minimal CPU and RAM to operate, making it ideal for widespread deployment.
|
||||
* **Distroless Container:** Compiled as a statically-linked binary running inside a minimal Docker image.
|
||||
|
||||
## Configuration
|
||||
|
||||
Configuration is managed through Environment Variables.
|
||||
|
||||
### Core Ecosystem Variables
|
||||
| Variable | Description | Example |
|
||||
|---|---|---|
|
||||
| `HW_HUB_ENDPOINT` | The URL of your central HoneyWire Hub. | `http://127.0.0.1:8080` |
|
||||
| `HW_HUB_KEY` | The Node Key to authenticate with the Hub. | `super_secret_key_123` |
|
||||
| `HW_SENSOR_ID` | A unique identifier for this specific trap. | `ping-canary-01` |
|
||||
| `HW_SEVERITY` | Alert severity sent to the Hub (`info` to `critical`). | `high` |
|
||||
|
||||
## Deployment
|
||||
|
||||
It is highly recommended to deploy this sensor using the provided `docker-compose.yml` configuration and the provided `Dockerfile` (if building from source). The compose file automatically applies the strict network and kernel capability rules required for safe execution.
|
||||
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
## Security Architecture
|
||||
|
||||
This sensor is architected for extreme resilience against exploits. By utilizing a minimal attack surface and enforcing strict container sandboxing, it safely intercepts raw ICMP traffic.
|
||||
|
||||
**Core Defense-in-Depth Measures:**
|
||||
* **Raw Socket Isolation:** Bypasses heavy NIDS frameworks by interacting directly with network packets in pure Go, eliminating external C-library vulnerabilities.
|
||||
* **Least Privilege Execution:** Runs as container root strictly to bind the raw socket, relying on container boundaries to limit system access.
|
||||
* **Kernel Capability Stripping:** Drops all default Linux kernel capabilities (`cap_drop: ALL`) and only adds back `NET_RAW`, ensuring the sensor can intercept pings but cannot modify the host filesystem or OS.
|
||||
* **Distroless Isolation:** Built on a statically-linked Distroless image. It completely lacks a shell (`/bin/sh`), package managers, or standard Linux utilities, leaving attackers with zero tools to execute secondary payloads.
|
||||
* **In-Memory Operation:** Processes all packet data exclusively in memory, ensuring zero malicious disk I/O operations occur on the host system.
|
||||
|
||||
*Recommendation: For optimal security, always deploy this sensor using the official `docker-compose.yml` and `Dockerfile` to ensure these sandbox protections are strictly enforced by the container runtime.*
|
||||
@@ -1,49 +0,0 @@
|
||||
# HoneyWire Official Sensor: Network Scan Detector
|
||||
|
||||
The Network Scan Detector is a low-overhead network sensor designed to silently detect horizontal port scans. By monitoring raw SYN packets directly on the network interface, it identifies scanning activity aimed at closed or unused ports before it ever reaches a firewall or application log.
|
||||
|
||||
## Features
|
||||
* **Zero-Setup SDK Integration:** Natively built on the HoneyWire Go SDK.
|
||||
* **In-Memory Parsing:** Analyzes raw TCP headers directly in memory.
|
||||
* **Configurable Thresholds:** Easily adjust how many unique ports must be hit within a specific time window to trigger an alert.
|
||||
* **Distroless Container:** Compiled as a statically-linked binary running inside a minimal Docker image.
|
||||
|
||||
## Configuration
|
||||
|
||||
All configuration is handled via Environment Variables.
|
||||
|
||||
### Core Ecosystem Variables
|
||||
| Variable | Description | Example |
|
||||
|---|---|---|
|
||||
| `HW_HUB_ENDPOINT` | The URL of your central HoneyWire Hub. | `http://127.0.0.1:8080` |
|
||||
| `HW_HUB_KEY` | The Node Key to authenticate with the Hub. | `super_secret_key_123` |
|
||||
| `HW_SENSOR_ID` | A unique identifier for this specific trap. | `scan-detector-01` |
|
||||
| `HW_SEVERITY` | Alert severity sent to the Hub (`info` to `critical`). | `critical` |
|
||||
|
||||
### Sensor-Specific Variables
|
||||
| Variable | Description | Default |
|
||||
|---|---|---|
|
||||
| `HW_SCAN_THRESHOLD` | Number of unique ports that must be hit to trigger an alert. | `5` |
|
||||
| `HW_SCAN_WINDOW` | The time window (in seconds) to track the threshold. | `5` |
|
||||
| `HW_IGNORE_PORTS` | Comma-separated ports to ignore (e.g., actual open services). | `80,443` |
|
||||
|
||||
## Deployment
|
||||
|
||||
It is highly recommended to deploy this sensor using the provided docker-compose.yml configuration and the provided Dockerfile (if building from source). The compose file automatically applies the strict network and kernel capability rules required for safe execution.
|
||||
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
## Security Architecture
|
||||
|
||||
This sensor is architected for extreme resilience against exploits. By utilizing a minimal attack surface and enforcing strict container sandboxing, it safely handles raw network traffic.
|
||||
|
||||
**Core Defense-in-Depth Measures:**
|
||||
* **Raw Socket Isolation:** Bypasses heavy NIDS frameworks by interacting directly with network packets in pure Go, eliminating external C-library vulnerabilities.
|
||||
* **Least Privilege Execution:** Runs as container root strictly to bind the raw socket, but relies on capability dropping to prevent privilege escalation.
|
||||
* **Kernel Capability Stripping:** Drops all default Linux kernel capabilities (`cap_drop: ALL`) and only adds back `NET_RAW`, ensuring the sensor can read packets but cannot modify the system.
|
||||
* **Distroless Isolation:** Built on a statically-linked Distroless image. It completely lacks a shell (`/bin/sh`), package managers, or standard Linux utilities, leaving attackers with zero tools to pivot to the host network.
|
||||
* **In-Memory Operation:** Processes all payload data exclusively in memory, ensuring zero malicious disk I/O operations occur on the host system.
|
||||
|
||||
*Recommendation: For optimal security, always deploy this sensor using the official `docker-compose.yml` and `Dockerfile` to ensure these sandbox protections are strictly enforced by the container runtime.*
|
||||
@@ -1,54 +0,0 @@
|
||||
# HoneyWire Official Sensor: TCP Tarpit
|
||||
|
||||
The TCP Tarpit is a high-fidelity, low-interaction honeypot designed to detect network reconnaissance and brute-force attempts. It can act as a "Tarpit," binding to decoy ports and intentionally stalling attackers to waste their time while silently extracting their IP and payload data to report to the HoneyWire Hub, or instantly close the connection and report the IP to the Hub.
|
||||
|
||||
## Features
|
||||
* **Zero-Setup SDK Integration:** Natively built on the HoneyWire Go SDK.
|
||||
* **Massive Concurrency:** Powered by Go routines and channels, capable of trapping thousands of automated bots simultaneously with microscopic memory overhead.
|
||||
* **Tarpit Modes:** Supports `hold` (silent stall), `echo` (repeat data back), or `close` (immediate drop).
|
||||
* **Forensic Capture:** Safely buffers up to 10 lines of payload data without risking memory exhaustion.
|
||||
* **Distroless Container:** Compiled as a statically-linked binary running inside a hardened, unprivileged `:nonroot` Distroless Docker image to prevent container breakouts.
|
||||
|
||||
## Configuration
|
||||
|
||||
All configuration is handled via Environment Variables. Copy the `.env.example` file to `.env` before running.
|
||||
|
||||
### Core Ecosystem Variables (Required)
|
||||
| Variable | Description | Example |
|
||||
|---|---|---|
|
||||
| `HW_HUB_ENDPOINT` | The URL of your central HoneyWire Hub. | `http://127.0.0.1:8080` |
|
||||
| `HW_HUB_KEY` | The Node Key to authenticate with the Hub. | `super_secret_key_123` |
|
||||
| `HW_SENSOR_ID` | A unique identifier for this specific trap. | `ssh-tarpit-01` |
|
||||
| `HW_SEVERITY` | Alert severity sent to the Hub (`info` to `critical`). | `high` |
|
||||
|
||||
### Sensor-Specific Variables
|
||||
| Variable | Description | Default |
|
||||
|---|---|---|
|
||||
| `HW_DECOY_PORTS` | Comma-separated list of TCP ports to monitor. | `2222,3306` |
|
||||
| `HW_TARPIT_MODE` | The behavior of the trap: `hold`, `echo`, or `close`. | `hold` |
|
||||
| `HW_TARPIT_BANNER` | (Optional) A fake service banner to send on connect. | `SSH-2.0-OpenSSH_8.2p1\r\n` |
|
||||
|
||||
## Tarpit Modes Explained
|
||||
* **`hold` (Default):** The sensor accepts the connection but sends nothing. It holds the TCP socket open as long as possible (up to 1 hour), dripping empty bytes to drain the attacker's resources and slow down automated scanners like Nmap or brute-force tools.
|
||||
* **`echo`:** The sensor acts as an echo server, repeating whatever the attacker sends back to them. Useful for confusing automated scripts.
|
||||
* **`close`:** The sensor logs the connection, captures the initial payload, and forcefully closes the socket.
|
||||
|
||||
## Deployment
|
||||
|
||||
It is highly recommended to deploy this sensor using the provided docker-compose.yml configuration and the provided Dockerfile (if building from source). The compose file automatically applies the strict network and kernel capability rules required for safe execution.
|
||||
|
||||
```Bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
## Security Architecture
|
||||
|
||||
This sensor is architected for extreme resilience against exploitation by adhering to the principle of least privilege and enforcing strict resource limits.
|
||||
|
||||
**Core Defense-in-Depth Measures:**
|
||||
* **Kernel Capability Stripping:** Drops all Linux kernel capabilities (`cap_drop: ALL`) via the Docker Compose configuration, neutralizing advanced kernel exploitation techniques.
|
||||
* **Distroless Isolation:** Built on a statically-linked Distroless image. It completely lacks a shell (`/bin/sh`), package managers, or common Linux utilities (like `curl` or `wget`), leaving attackers with zero tools to pivot if they achieve Remote Code Execution.
|
||||
* **Concurrency Capping:** Utilizes a native Go buffered channel (semaphore) to strictly cap concurrent connections at `1000`. This prevents attackers from launching a Denial of Service (DoS) attack designed to exhaust the host machine's File Descriptors or RAM.
|
||||
* **In-Memory Operation:** Processes all payload data exclusively in memory, ensuring zero malicious disk I/O operations occur on the host system.
|
||||
|
||||
*Recommendation: For optimal security, always deploy this sensor using the official `docker-compose.yml` and `Dockerfile` to ensure these sandbox protections are strictly enforced by the container runtime.*
|
||||
@@ -1,46 +0,0 @@
|
||||
# HoneyWire Official Sensor: Web Router Decoy
|
||||
|
||||
The Web Router Decoy is a web honeypot designed to detect credential stuffing, automated web scanners, and targeted administrative panel attacks. It serves a deceptive, router login page. When an attacker attempts to log in, the sensor captures their IP, user agent, and attempted credentials, silently reports them to the HoneyWire Hub, and safely returns a "401 Unauthorized" response to keep them guessing.
|
||||
|
||||
## Features
|
||||
* **Zero-Setup SDK Integration:** Natively built on the HoneyWire Go SDK.
|
||||
* **Dynamic Brand Variable:** Automatically injects the specified router brand (e.g., Cisco, Netgear, ASUS) directly into the HTML template to make the trap more convincing.
|
||||
* **Distroless Container:** Compiled as a statically-linked binary running inside a hardened, unprivileged `:nonroot` Distroless Docker image to prevent container breakouts.
|
||||
|
||||
## Configuration
|
||||
|
||||
All configuration is handled via Environment Variables. Copy the `.env.example` file to `.env` before running.
|
||||
|
||||
### Core Ecosystem Variables (Required)
|
||||
| Variable | Description | Example |
|
||||
|---|---|---|
|
||||
| `HW_HUB_ENDPOINT` | The URL of your central HoneyWire Hub. | `http://127.0.0.1:8080` |
|
||||
| `HW_HUB_KEY` | The Node Key to authenticate with the Hub. | `super_secret_key_123` |
|
||||
| `HW_SENSOR_ID` | A unique identifier for this specific trap. | `web-decoy-01` |
|
||||
| `HW_SEVERITY` | Alert severity sent to the Hub (`info` to `critical`). | `critical` |
|
||||
|
||||
### Sensor-Specific Variables
|
||||
| Variable | Description | Default |
|
||||
|---|---|---|
|
||||
| `HW_BIND_PORT` | The TCP port the fake web server will listen on. | `8080` |
|
||||
| `HW_ROUTER_BRAND` | The brand name injected into the fake login page. | `Netgear` |
|
||||
|
||||
## Deployment
|
||||
|
||||
It is highly recommended to deploy this sensor using the provided docker-compose.yml configuration and the provided Dockerfile (if building from source). The compose file automatically applies the strict network and kernel capability rules required for safe execution.
|
||||
```Bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
## Security Architecture
|
||||
|
||||
This sensor is architected for extreme resilience against web-based exploits by utilizing a minimal attack surface and enforcing strict container sandboxing.
|
||||
|
||||
**Core Defense-in-Depth Measures:**
|
||||
* **Framework-Free Execution:** Built purely on Go's native `net/http` library, eliminating the massive attack surface and supply-chain risks associated with heavy third-party web frameworks (like FastAPI, Flask, or Express).
|
||||
* **Unprivileged Execution:** Runs entirely as a non-root user (`UID 65532`), preventing system-level modifications even in the event of a container breach.
|
||||
* **Kernel Capability Stripping:** Drops all Linux kernel capabilities (`cap_drop: ALL`) via the Docker Compose configuration, neutralizing advanced kernel exploitation techniques.
|
||||
* **Distroless Isolation:** Built on a statically-linked Distroless image. It completely lacks a shell (`/bin/sh`), package managers, or standard Linux utilities (like `curl` or `wget`), leaving attackers with zero tools to download secondary payloads or pivot to the host network.
|
||||
* **In-Memory Operation:** Processes all payload data exclusively in memory, ensuring zero malicious disk I/O operations occur on the host system.
|
||||
|
||||
*Recommendation: For optimal security, always deploy this sensor using the official `docker-compose.yml` and `Dockerfile` to ensure these sandbox protections are strictly enforced by the container runtime.*
|
||||
@@ -1,75 +0,0 @@
|
||||
# HoneyWire Wizard - Intelligent Deception CLI
|
||||
|
||||
## Overview
|
||||
|
||||
The HoneyWire Wizard is a zero-friction, intelligent command-line agent designed to assess a host's attack surface and automatically deploy contextual deception infrastructure (honeypots).
|
||||
|
||||
It uses correlated discovery to map active processes to network sockets and Docker containers, keeping deployments precise and low-noise. The Wizard fetches manifests from the HoneyWire Hub and executes them locally without hardcoding sensor behavior.
|
||||
|
||||
## Architecture
|
||||
|
||||
The current architecture separates runtime orchestration from platform domain logic.
|
||||
|
||||
### Command entry point
|
||||
- `cmd/wizard/main.go` — CLI startup, flags, and command dispatch.
|
||||
|
||||
### Runtime / orchestration layer (`internal/`)
|
||||
- `internal/app/` — runtime session, node config, and app state.
|
||||
- `internal/cli/` — terminal rendering, prompts, and UX helpers.
|
||||
- `internal/commands/` — high-level command orchestrators.
|
||||
- `internal/deploy/` — Docker/Compose deployment and uninstall logic.
|
||||
- `internal/system/` — host health checks and environment readiness.
|
||||
- `internal/util/` — shared application helpers.
|
||||
|
||||
### Core domain layer (`core/`)
|
||||
- `core/api/` — Hub API client.
|
||||
- `core/discovery/` — recommendation engine and discovery pipeline.
|
||||
- `core/scanner/` — host inspection engine for native and containerized services.
|
||||
- `core/schema/` — manifest contracts and deployment models.
|
||||
|
||||
This layout keeps command code thin and concentrates domain systems in `core/`, while `internal/` remains the runtime host and orchestration layer.
|
||||
|
||||
## Core subsystems
|
||||
|
||||
### `core/scanner`
|
||||
Discovers the host's attack surface with OS-native inspection.
|
||||
- parses `/proc/net/tcp(6)` and `/proc/[pid]/fd`
|
||||
- correlates ports with processes and containers
|
||||
- integrates Docker socket data for container-aware discovery
|
||||
|
||||
### `core/discovery`
|
||||
Builds sensor recommendations from manifests and host state.
|
||||
- matches manifests against discovered services
|
||||
- filters already deployed sensors
|
||||
- renders deployment templates for a targeted deception strategy
|
||||
|
||||
### `core/api`
|
||||
Fetches manifest data and interacts with the HoneyWire Hub for node linking and state.
|
||||
|
||||
### `core/schema`
|
||||
Defines shared manifest and deployment contracts used across discovery and deployment.
|
||||
|
||||
## Usage
|
||||
|
||||
Build and run the Wizard on the host:
|
||||
|
||||
```bash
|
||||
make build
|
||||
./build/honeywire
|
||||
```
|
||||
|
||||
Run the module test suite:
|
||||
|
||||
```bash
|
||||
make test
|
||||
```
|
||||
|
||||
## Cleanup
|
||||
|
||||
To tear down managed deception infrastructure:
|
||||
|
||||
```bash
|
||||
./build/honeywire uninstall
|
||||
```
|
||||
|
||||
This safely removes the managed compose deployment and leaves the host in a clean state.
|
||||
Reference in New Issue
Block a user