mirror of
https://github.com/andreicscs/HoneyWire
synced 2026-06-26 12:39:53 +00:00
implemented manual updates, fixed versioning bugs, updated docs to reflect changes
This commit is contained in:
+15
@@ -104,6 +104,13 @@ Common message types:
|
||||
|
||||
Fetches the sensor manifest catalog from `RegistryURL` or the default public manifest registry.
|
||||
|
||||
### GET /api/v1/manifests/{sensorId}/versions
|
||||
|
||||
Fetches a specific historical version schema of a sensor manifest from the registry.
|
||||
|
||||
**Query parameters:**
|
||||
- `version` — The exact version string to fetch (e.g. `2.0.6`).
|
||||
|
||||
### POST /api/v1/compose/generate
|
||||
|
||||
Generates a `docker-compose.yml` preview from the selected sensor manifests and UI-provided environment values.
|
||||
@@ -193,6 +200,10 @@ Updates a node's alias, tags, public IP, or private IP.
|
||||
}
|
||||
```
|
||||
|
||||
#### POST /api/v1/nodes/{nodeId}/upgrade
|
||||
|
||||
Triggers a fleet-wide sync command over WebSockets to force the node to pull the newest compose definitions and automatically pull the latest manifest versions for all of its installed sensors.
|
||||
|
||||
#### DELETE /api/v1/nodes/{nodeId}
|
||||
|
||||
Deletes a node and cascades to remove its sensors, event history, and heartbeat records.
|
||||
@@ -220,6 +231,10 @@ Updates a sensor's custom name and config values and marks the parent node pendi
|
||||
|
||||
Removes a sensor from the node and marks the node pending sync.
|
||||
|
||||
#### POST /api/v1/nodes/{nodeId}/sensors/{sensorId}/upgrade
|
||||
|
||||
Upgrades a specific sensor to the latest available manifest version listed in the registry, and marks the node pending sync.
|
||||
|
||||
#### PATCH /api/v1/nodes/{nodeId}/sensors/{sensorId}/silence
|
||||
|
||||
Toggles a sensor's silence state. Silenced sensors still log events but suppress push notifications.
|
||||
|
||||
@@ -27,7 +27,7 @@ HoneyWire
|
||||
|
||||
### Hub
|
||||
The central orchestrator and data aggregator.
|
||||
*For deep-dives into the Hub, see the [Backend Architecture](./hub/backend/overview.md) and [Frontend Architecture](./hub/frontend/overview.md).*
|
||||
*For deep-dives into the Hub, see the [Backend Architecture](./hub/backend/overview.md), and [Frontend Architecture](./hub/frontend/overview.md).*
|
||||
|
||||
**Responsibilities:**
|
||||
- Stores events
|
||||
@@ -219,6 +219,7 @@ A high-level view of internal subsystem boundaries:
|
||||
**Hub**
|
||||
|
||||
├── API Layer
|
||||
├── Compose Compiler
|
||||
├── Services
|
||||
├── Store
|
||||
├── Projections
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
# Hub Secure Compose Compiler Architecture
|
||||
|
||||
The `internal/compose` package is responsible for compiling deterministic, hardened `honeywire-compose.yml` configurations served to remote edge nodes.
|
||||
|
||||
To prevent privilege escalation or container escape vulnerabilities, the compiler operates on the principle of **Secure Defaults by Inversion**. Instead of attempt-filtering bad configurations out of an arbitrary map, the compiler explicitly maps specific allowed schema primitives into a locked-down, pre-configured Compose base.
|
||||
|
||||
---
|
||||
|
||||
## The Compilation Pipeline
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[Raw Manifest JSON / Preview Payload] -->|Strict Ingestion: DisallowUnknownFields| B(Ingestion & Validation)
|
||||
B -->|Normalizes paths & cleans capabilities| C(Validator: security/validate.go)
|
||||
C -->|BuildEnv overrides client & injects system variables| D(Secure Env Composition: env.go)
|
||||
D -->|Lazy-loads & caches historical tags| E(Dual-Manifest Version Resolution)
|
||||
E -->|Forces Read-Only, drops all capabilities, sorts arrays| F(The Builder: builder.go)
|
||||
F -->|Outputs bit-for-bit deterministic file| G(honeywire-compose.yml)
|
||||
```
|
||||
|
||||
### 1. Strict Ingestion
|
||||
All API endpoints receiving compose preview payloads, as well as catalog fetch operations, ingest JSON using `json.NewDecoder` configured with `.DisallowUnknownFields()`. This establishes a hard boundary at the API layer: if a manifest or override payload contains attributes outside the schema, the request is immediately rejected before any compilation logic executes.
|
||||
|
||||
### 2. The Validator (`internal/compose/security/validate.go`)
|
||||
The validation module acts as the security gateway for all manifests and user-provided configuration overrides. It performs:
|
||||
* **Capability Restriction:** Enforces a strict allowlist of Linux capabilities (e.g. `NET_RAW`, `NET_BIND_SERVICE`, `NET_ADMIN`). Any other capabilities are blocked.
|
||||
* **Volume Path Normalization:** Cleans host and container path structures (`filepath.Clean()`) and rejects paths matching a forbidden directory denylist (e.g., `/proc`, `/sys`, `/var/run/docker.sock`).
|
||||
* **Safe Interpolation Checking:** Rejects common interpolation patterns (such as `${` or `{{`) on raw fields to block injection vectors, relying instead on structural builder templates.
|
||||
|
||||
### 3. Secure Environment Composition (`internal/compose/env.go`)
|
||||
The environment generation pipeline merges variables with a strict priority order:
|
||||
1. **Manifest Defaults:** Base variables defined in the sensor manifest.
|
||||
2. **User/Node Overrides:** Safe environment variables requested via the UI or Wizard (ignoring any attempting to override forbidden system environment fields).
|
||||
3. **System-Injected Constants:** Hardcoded, system-injected values (such as `HW_HUB_KEY`, `HW_SENSOR_ID`) that are unconditionally set by the Hub.
|
||||
|
||||
### 4. Versioning and Dual-Manifest Resolution
|
||||
To support manual upgrades and historical rollbacks, nodes often run versions of a sensor other than the absolute latest catalog version. To prevent compiling incompatible or broken configurations, the compiler uses dual-manifest resolution:
|
||||
* **Latest Catalog:** Warmed into cache on startup via the central `index.json`.
|
||||
* **Historical Schemas:** Lazy-loaded and permanently cached via `FetchSpecificManifest` from the configured registry when a legacy node requests its deployed version. If a node is deployed with a legacy tag, the compose preview will render configurations specific to that historical manifest tag rather than defaulting to the newest catalog entry.
|
||||
|
||||
### 5. The Builder (`internal/compose/builder.go`)
|
||||
The builder converts the strictly validated model inputs into a `ComposeFile` struct:
|
||||
* **Immutable Sandboxing:** Unconditionally forces secure runtime defaults, including `ReadOnly: true`, `CapDrop: ["ALL"]`, and `SecurityOpt: ["no-new-privileges:true"]`.
|
||||
* **Bit-for-Bit Determinism:** Output arrays (environment pairs, volume mount paths, initialization chains) are explicitly alphabetically sorted. This ensures that the generated YAML string is perfectly deterministic and predictable.
|
||||
@@ -0,0 +1,80 @@
|
||||
# HoneyWire Hub Backend Overview
|
||||
|
||||
The HoneyWire Hub backend is the central orchestrator of the system, written entirely in Go. It strictly adheres to a **Domain Service Pattern** (a lightweight form of Clean Architecture / Domain-Driven Design). This design ensures high testability, predictable state management, and clear separation of concerns.
|
||||
|
||||
## Technology Stack
|
||||
|
||||
Go (Golang) was chosen as the primary language for the Hub backend for several strategic reasons:
|
||||
- **Concurrency & WebSockets:** Go's lightweight goroutines make it exceptionally well-suited for handling thousands of simultaneous, long-lived WebSocket connections without the overhead of traditional threading models.
|
||||
- **Single Static Binary:** Go compiles to a single, statically linked executable. This allows the Hub to be deployed in a highly secure, minimal Distroless container sandbox with no runtime dependencies.
|
||||
- **Performance:** As a distributed security system, the Hub must ingest high-velocity telemetry from sensors across the network without dropping packets. Go's performance characteristics ensure low latency and high throughput.
|
||||
- **Standard Library:** Go's robust `net/http` standard library allowed us to build the entire API and router without relying on heavy third-party web frameworks, reducing the attack surface.
|
||||
- **Embedded Database Compatibility:** Go's ability to seamlessly integrate with CGO-free, pure-Go SQLite drivers enables a zero-configuration, single-node persistence layer perfectly suited for edge deployments.
|
||||
|
||||
## Core Principles
|
||||
|
||||
The backend architecture is built upon five foundational principles:
|
||||
|
||||
1. **Thin Transport Layer:** HTTP handlers in the `api` package contain **zero** business logic. They are strictly responsible for parsing JSON, reading context, and formatting HTTP responses.
|
||||
2. **Isolated Domain Services:** All business rules, side effects (like WebSockets or Push Notifications), and complex transactions live exclusively in the `services` package.
|
||||
3. **Interface Segregation:** Services define their own narrow interfaces (e.g., `Store` and `Broadcaster`). They do not depend on concrete implementations like `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` to be used by the layers below.
|
||||
|
||||
## Directory Structure
|
||||
|
||||
The backend source code is structured as follows:
|
||||
|
||||
```text
|
||||
internal/
|
||||
├── api/ # Transport Layer (HTTP Handlers, Middleware, Router)
|
||||
├── compose/ # Secure Compose Compiler & Validation Engine
|
||||
├── models/ # Core Domain Entities (Structs, JSON tags)
|
||||
├── projections/ # CQRS Read-Models (Analytics, Dashboards)
|
||||
├── services/ # Domain Layer (Business Logic, Orchestration)
|
||||
└── store/ # Persistence Layer (SQLite implementation)
|
||||
```
|
||||
|
||||
## Core Subsystems & Layers
|
||||
|
||||
### 1. Transport Layer (`internal/api`)
|
||||
The entry point for all network requests.
|
||||
- **Handlers:** Bound to domain-specific struct receivers. They handle JSON marshaling/unmarshaling, input validation, and HTTP status codes. They **never** interact with the database directly.
|
||||
- **Middleware:** Handles Authentication, extracts credentials, validates them, and attaches identity to the request context.
|
||||
- **Router:** Maps HTTP routes to specific Handlers and applies Middleware groups.
|
||||
- **SPA Routing & Frontend Embedding:** The router securely serves the embedded Vue 3 frontend. It enforces strict API protection (returning pure 404 JSON for unmatched `/api/` routes) while providing a transparent SPA fallback (serving `index.html`) for unrecognized paths, enabling seamless frontend History API navigation (e.g., `/dashboard`) without reload errors.
|
||||
|
||||
### 2. Secure Compose Compiler (`internal/compose`)
|
||||
Responsible for compiling deterministic, hardened `honeywire-compose.yml` configurations served to remote edge nodes. See the [Compose Compiler Architecture](./compose.md) for more details.
|
||||
- **Secure Defaults by Inversion:** Explicitly maps specific allowed schema primitives (e.g. strict Linux capabilities, normalized volume paths) into a locked-down Compose base instead of attempting to filter arbitrary config maps.
|
||||
- **Validation Engine:** Sanitizes configurations, normalizes directory paths via `filepath.Clean()`, enforces a capability allowlist, and rejects potentially unsafe interpolation patterns.
|
||||
- **Dual-Manifest Version Resolution:** Supports manual updates and historical rollbacks. When a node requests its configuration, the compiler resolves the specific deployed version:
|
||||
- If the node is running the latest version, it pulls from the in-memory registry cache.
|
||||
- If the node is running an older/historical version, the compiler lazy-loads and caches that specific tagged manifest from the registry (e.g., `hw-sensor-tarpit-v1.0.0.json`) to perform compilation matching the node's deployed state.
|
||||
|
||||
### 3. Domain Service Layer (`internal/services`)
|
||||
The brain of the application. Everything in `internal/services/*` is framework-agnostic.
|
||||
- Contains the actual "verbs" of the system (e.g., `ProcessHeartbeat`, `CreateNode`).
|
||||
- Handles all side effects.
|
||||
- Relies on Dependency Injection.
|
||||
|
||||
### 4. Persistence Layer (`internal/store`)
|
||||
The data access layer.
|
||||
- **SQLite Store:** 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.
|
||||
- Contains **no** business logic.
|
||||
|
||||
### 5. Read/Analytics Layer (`internal/projections`)
|
||||
A specialized CQRS (Command Query Responsibility Segregation) pattern used for heavy dashboard analytics.
|
||||
- Used for high-volume aggregations like Threat Velocity and Threat Severity Distributions.
|
||||
- Returns flat **DTOs** (Data Transfer Objects) derived via pure functions, preventing the frontend from traversing large data arrays.
|
||||
|
||||
|
||||
## 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. Handled via `UIAuthMiddleware`.
|
||||
2. **Sensors/Agents (Machines):** Secured via statically generated, cryptographically random API Keys (`hw_key_...`). Passed via `Authorization: Bearer` or `X-Api-Key` headers and validated by `AgentAuthMiddleware`. These are aggressively cached in memory to prevent database bottlenecks during heartbeat storms.
|
||||
3. **Dual-Auth Endpoints:** Shared endpoints (like `/api/v1/manifests`) are protected by `DualAuthMiddleware`, which attempts Bearer authentication first before falling back to Cookie validation.
|
||||
@@ -112,6 +112,9 @@ git tag sensor/file-canary/v1.2.0
|
||||
git push origin sensor/file-canary/v1.2.0
|
||||
```
|
||||
|
||||
> [!WARNING]
|
||||
> **Immutability Rule:** In standard GitOps workflows, release tags are immutable. If you push `v1.2.0`, realize it's broken, and rollback your fleet, you **must not** overwrite the `v1.2.0` tag with a fix. Doing so causes cache poisoning in the Hub backend and Docker registries. Always bump the version (e.g. `v1.2.1`) and leave the rolled-back tag untouched.
|
||||
|
||||
### Step 3: CI Handles the Rest
|
||||
The `publish-sensor-registry` Gitea Action will:
|
||||
1. Read `Sensors/official/FileCanary/file-canary.json` at the tagged commit
|
||||
@@ -125,5 +128,7 @@ Check the `registry-pages` branch to confirm:
|
||||
- `index.json` lists the new version
|
||||
- The `latest` field points to the new version
|
||||
|
||||
### Step 5: Dashboard Sync
|
||||
Refresh your HoneyWire dashboard or wait for the automatic UI sync. The Hub's event-driven catalog hook will instantly detect the registry mutation, generate the new cryptographic hashes for your nodes, lock them into 'Pending Sync' state, and immediately broadcast the update to all active UI sessions without latency. You do not need to wait for a 5-minute background loop.
|
||||
### Step 5: Dashboard Sync & Manual Upgrades
|
||||
Refresh your HoneyWire dashboard or wait for the automatic UI sync. The Hub's event-driven catalog hook will instantly detect the registry mutation and compare it against deployed sensors.
|
||||
|
||||
Instead of forcefully upgrading production edge nodes automatically, the Hub will flag nodes with an **"Update Available"** indicator. Users must manually trigger the `/api/v1/nodes/{id}/upgrade` endpoint (via the UI) to instruct the node to pull the new version schema and execute a compose restart.
|
||||
|
||||
@@ -168,6 +168,19 @@ func (h *NodeHandler) EditNodeSensor(w http.ResponseWriter, r *http.Request) {
|
||||
SendJSON(w, http.StatusOK, map[string]string{"status": "success"})
|
||||
}
|
||||
|
||||
// UpgradeNodeSensor handles POST /api/v1/nodes/{id}/sensors/{sensorId}/upgrade
|
||||
func (h *NodeHandler) UpgradeNodeSensor(w http.ResponseWriter, r *http.Request) {
|
||||
nodeID := chi.URLParam(r, "nodeId")
|
||||
sensorID := chi.URLParam(r, "sensorId")
|
||||
|
||||
if err := h.service.UpgradeSensor(nodeID, sensorID); err != nil {
|
||||
RespondError(w, "Failed to upgrade sensor: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
SendJSON(w, http.StatusOK, map[string]string{"status": "success", "message": "Sensor upgraded, node pending sync"})
|
||||
}
|
||||
|
||||
// DeleteNodeSensor handles DELETE /api/v1/nodes/{id}/sensors/{sensorId}
|
||||
func (h *NodeHandler) DeleteNodeSensor(w http.ResponseWriter, r *http.Request) {
|
||||
nodeID := chi.URLParam(r, "nodeId")
|
||||
@@ -193,6 +206,18 @@ func (h *NodeHandler) DeleteNode(w http.ResponseWriter, r *http.Request) {
|
||||
SendJSON(w, http.StatusOK, map[string]string{"status": "success"})
|
||||
}
|
||||
|
||||
// UpgradeNode handles POST /api/v1/nodes/{id}/upgrade
|
||||
func (h *NodeHandler) UpgradeNode(w http.ResponseWriter, r *http.Request) {
|
||||
nodeID := chi.URLParam(r, "nodeId")
|
||||
|
||||
if err := h.service.UpgradeNode(nodeID); err != nil {
|
||||
RespondError(w, "Failed to upgrade node: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
SendJSON(w, http.StatusOK, map[string]string{"status": "success", "message": "Node upgraded, pending sync"})
|
||||
}
|
||||
|
||||
// GetCurrentNode handles GET /api/v1/nodes/me
|
||||
// Used by wizard agents authenticated via Bearer token
|
||||
func (h *NodeHandler) GetCurrentNode(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@@ -70,11 +70,13 @@ func SetupRouter(cfg RouterConfig) (*chi.Mux, error) {
|
||||
r.Get("/api/v1/nodes", cfg.Nodes.GetNodes)
|
||||
r.Get("/api/v1/nodes/{nodeId}", cfg.Nodes.GetNodeDetails)
|
||||
r.Patch("/api/v1/nodes/{nodeId}", cfg.Nodes.UpdateNode)
|
||||
r.Post("/api/v1/nodes/{nodeId}/upgrade", cfg.Nodes.UpgradeNode)
|
||||
r.Delete("/api/v1/nodes/{nodeId}", cfg.Nodes.DeleteNode)
|
||||
|
||||
// --- Sensor Management ---
|
||||
r.Post("/api/v1/nodes/{nodeId}/sensors", cfg.Nodes.AddNodeSensor)
|
||||
r.Put("/api/v1/nodes/{nodeId}/sensors/{sensorId}", cfg.Nodes.EditNodeSensor)
|
||||
r.Post("/api/v1/nodes/{nodeId}/sensors/{sensorId}/upgrade", cfg.Nodes.UpgradeNodeSensor)
|
||||
r.Delete("/api/v1/nodes/{nodeId}/sensors/{sensorId}", cfg.Nodes.DeleteNodeSensor)
|
||||
r.Patch("/api/v1/nodes/{nodeId}/sensors/{sensorId}/silence", cfg.Sensors.ToggleSilence)
|
||||
|
||||
@@ -112,7 +114,10 @@ func SetupRouter(cfg RouterConfig) (*chi.Mux, error) {
|
||||
r.Post("/api/v1/event", cfg.Events.ReceiveEvent)
|
||||
})
|
||||
|
||||
r.With(DualAuthMiddleware(cfg.SessionValidator, cfg.NodeAuthenticator, rateLimiter)).Get("/api/v1/manifests", cfg.Sensors.GetManifests)
|
||||
r.With(DualAuthMiddleware(cfg.SessionValidator, cfg.NodeAuthenticator, rateLimiter)).Group(func(r chi.Router) {
|
||||
r.Get("/api/v1/manifests", cfg.Sensors.GetManifests)
|
||||
r.Get("/api/v1/manifests/{sensorId}/versions", cfg.Sensors.GetSpecificManifest)
|
||||
})
|
||||
|
||||
// --- Serve the Vue Frontend ---
|
||||
distFS, err := fs.Sub(ui.StaticFiles, "dist")
|
||||
|
||||
@@ -120,3 +120,22 @@ func (h *SensorHandler) GetManifests(w http.ResponseWriter, r *http.Request) {
|
||||
// nosemgrep: go.lang.security.audit.xss.no-direct-write-to-responsewriter.no-direct-write-to-responsewriter
|
||||
w.Write(body)
|
||||
}
|
||||
|
||||
// GetSpecificManifest fetches the exact historical manifest for a specific version.
|
||||
func (h *SensorHandler) GetSpecificManifest(w http.ResponseWriter, r *http.Request) {
|
||||
sensorID := chi.URLParam(r, "sensorId")
|
||||
version := r.URL.Query().Get("version")
|
||||
|
||||
if sensorID == "" || version == "" {
|
||||
RespondError(w, "sensorId and version are required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
manifest, err := h.composeService.FetchSpecificManifest(sensorID, version)
|
||||
if err != nil {
|
||||
RespondError(w, "Failed to fetch manifest version: "+err.Error(), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
SendJSON(w, http.StatusOK, manifest)
|
||||
}
|
||||
|
||||
@@ -17,7 +17,11 @@ The architecture is driven by the principle of **Secure Defaults by Inversion**.
|
||||
- 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. **The Builder (`builder.go`)**
|
||||
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.
|
||||
|
||||
@@ -69,7 +69,7 @@ func (s *Service) FetchManifestBytes(currentHubVersion string) ([]byte, error) {
|
||||
func (s *Service) fetchStrictCatalogManifests(currentHubVersion string) ([]models.SensorManifest, error) {
|
||||
registryURL, err := s.store.GetConfigValue("registry_url")
|
||||
if err != nil || registryURL == "" {
|
||||
registryURL = "https://raw.githubusercontent.com/andreicscs/HoneyWire/registry-pages"
|
||||
return nil, fmt.Errorf("registry_url is not configured in database")
|
||||
}
|
||||
|
||||
if err := s.catalog.RefreshIndex(); err != nil {
|
||||
@@ -131,6 +131,51 @@ func (s *Service) fetchStrictCatalogManifests(currentHubVersion string) ([]model
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *Service) FetchSpecificManifest(sensorID, targetVersion string) (*models.SensorManifest, error) {
|
||||
cleanVersion := strings.TrimPrefix(targetVersion, "v")
|
||||
|
||||
registryURL, err := s.store.GetConfigValue("registry_url")
|
||||
if err != nil || registryURL == "" {
|
||||
return nil, fmt.Errorf("registry_url is not configured in database")
|
||||
}
|
||||
|
||||
sensorName := strings.TrimPrefix(sensorID, "hw-sensor-")
|
||||
manifestURL := fmt.Sprintf("%s/%s-v%s.json", strings.TrimRight(registryURL, "/"), sensorName, cleanVersion)
|
||||
|
||||
cacheKey := sensorID + "-v" + cleanVersion
|
||||
s.mu.RLock()
|
||||
cached, ok := s.cache[cacheKey]
|
||||
s.mu.RUnlock()
|
||||
|
||||
if ok {
|
||||
return &cached, nil
|
||||
}
|
||||
|
||||
mResp, fetchErr := http.Get(manifestURL)
|
||||
if fetchErr != nil {
|
||||
log.Printf("[ERROR] FetchSpecificManifest network error: %v", fetchErr)
|
||||
return nil, fetchErr
|
||||
}
|
||||
defer mResp.Body.Close()
|
||||
|
||||
if mResp.StatusCode != 200 {
|
||||
log.Printf("[ERROR] FetchSpecificManifest received HTTP %d from %s", mResp.StatusCode, manifestURL)
|
||||
return nil, fmt.Errorf("unexpected status %d", mResp.StatusCode)
|
||||
}
|
||||
|
||||
var specific models.SensorManifest
|
||||
if dErr := json.NewDecoder(mResp.Body).Decode(&specific); dErr != nil {
|
||||
log.Printf("[ERROR] FetchSpecificManifest failed to decode JSON: %v", dErr)
|
||||
return nil, dErr
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
s.cache[cacheKey] = specific
|
||||
s.mu.Unlock()
|
||||
|
||||
return &specific, nil
|
||||
}
|
||||
|
||||
// --- GENERATION LOGIC ---
|
||||
|
||||
func (s *Service) GetNodeCompose(token, hostFallback string, currentHubVersion string) ([]byte, error) {
|
||||
@@ -161,25 +206,20 @@ func (s *Service) GetNodeCompose(token, hostFallback string, currentHubVersion s
|
||||
_ = s.store.ApplyNodeRevision(nodeID, effectiveRevision)
|
||||
}
|
||||
|
||||
manifests, fetchErr := s.fetchStrictCatalogManifests(currentHubVersion)
|
||||
if fetchErr != nil {
|
||||
log.Printf("[ERROR] fetchStrictCatalogManifests failed: %v", fetchErr)
|
||||
return nil, fmt.Errorf("failed_to_fetch")
|
||||
}
|
||||
|
||||
manifestByID := make(map[string]models.SensorManifest)
|
||||
for _, m := range manifests {
|
||||
manifestByID[m.ID] = m
|
||||
}
|
||||
|
||||
var finalCompose composeEngine.ComposeFile
|
||||
|
||||
for _, sensor := range nodeDetails.InstalledSensors {
|
||||
manifest, ok := manifestByID[sensor.ID]
|
||||
if !ok {
|
||||
log.Printf("[WARNING] Manifest for sensor %s not found in catalog, skipping.", sensor.ID)
|
||||
targetVersion := sensor.DeployedVersion
|
||||
if targetVersion == "" {
|
||||
targetVersion, _ = s.catalog.GetLatestCompatibleVersion(sensor.ID, currentHubVersion)
|
||||
}
|
||||
|
||||
manifestPtr, err := s.FetchSpecificManifest(sensor.ID, targetVersion)
|
||||
if err != nil || manifestPtr == nil {
|
||||
log.Printf("[WARNING] Manifest for sensor %s (v%s) not found in catalog, skipping.", sensor.ID, targetVersion)
|
||||
continue
|
||||
}
|
||||
manifest := *manifestPtr
|
||||
|
||||
// The Hub will strictly wait for evaluateNodeSyncState to verify the new hash before considering this version deployed!
|
||||
if valErr := security.ValidateManifest(manifest); valErr != nil {
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"log"
|
||||
"sort"
|
||||
"time"
|
||||
"fmt"
|
||||
|
||||
"github.com/honeywire/hub/internal/catalog"
|
||||
"github.com/honeywire/hub/internal/models"
|
||||
@@ -127,11 +128,6 @@ func (s *Service) GetNodes() ([]models.Node, error) {
|
||||
if err == nil && latest != "" {
|
||||
deployedVer := sensor.DeployedVersion
|
||||
if deployedVer != latest {
|
||||
newHash := GenerateRevisionHash(nodes[i].InstalledSensors, s.catalog, models.HubVersion)
|
||||
if nodes[i].ActiveRevision != "" && nodes[i].ActiveRevision == newHash {
|
||||
_ = s.store.SetNodeSensorDeployedVersion(nodes[i].ID, sensor.ID, latest)
|
||||
continue
|
||||
}
|
||||
nodes[i].HasUpdateAvailable = true
|
||||
break
|
||||
}
|
||||
@@ -156,12 +152,6 @@ func (s *Service) GetNodeDetails(nodeID string) (*models.Node, error) {
|
||||
if err == nil && latest != "" {
|
||||
deployedVer := sensor.DeployedVersion
|
||||
if deployedVer != latest {
|
||||
newHash := GenerateRevisionHash(node.InstalledSensors, s.catalog, models.HubVersion)
|
||||
if node.ActiveRevision != "" && node.ActiveRevision == newHash {
|
||||
_ = s.store.SetNodeSensorDeployedVersion(node.ID, sensor.ID, latest)
|
||||
node.InstalledSensors[i].DeployedVersion = latest
|
||||
continue
|
||||
}
|
||||
node.InstalledSensors[i].UpdateAvailable = true
|
||||
node.HasUpdateAvailable = true
|
||||
}
|
||||
@@ -188,6 +178,46 @@ func (s *Service) EditSensor(nodeID, sensorID, customName string, configValues m
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) UpgradeSensor(nodeID, sensorID string) error {
|
||||
if s.catalog == nil {
|
||||
return fmt.Errorf("catalog unavailable")
|
||||
}
|
||||
latest, err := s.catalog.GetLatestCompatibleVersion(sensorID, models.HubVersion)
|
||||
if err != nil || latest == "" {
|
||||
return fmt.Errorf("no update available")
|
||||
}
|
||||
if err := s.store.SetNodeSensorDeployedVersion(nodeID, sensorID, latest); err != nil {
|
||||
return err
|
||||
}
|
||||
s.evaluateNodeSyncState(nodeID)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) UpgradeNode(nodeID string) error {
|
||||
if s.catalog == nil {
|
||||
return fmt.Errorf("catalog unavailable")
|
||||
}
|
||||
nodeDetails, err := s.store.GetNodeDetails(nodeID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
updatedAny := false
|
||||
for _, sensor := range nodeDetails.InstalledSensors {
|
||||
latest, err := s.catalog.GetLatestCompatibleVersion(sensor.ID, models.HubVersion)
|
||||
if err == nil && latest != "" && sensor.DeployedVersion != latest {
|
||||
_ = s.store.SetNodeSensorDeployedVersion(nodeID, sensor.ID, latest)
|
||||
updatedAny = true
|
||||
}
|
||||
}
|
||||
|
||||
if updatedAny {
|
||||
s.evaluateNodeSyncState(nodeID)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) DeleteSensor(nodeID, sensorID string) error {
|
||||
if err := s.store.RemoveNodeSensor(nodeID, sensorID); err != nil {
|
||||
return err
|
||||
@@ -213,9 +243,11 @@ func (s *Service) evaluateNodeSyncState(nodeID string) {
|
||||
|
||||
if s.catalog != nil {
|
||||
for _, sensor := range nodeDetails.InstalledSensors {
|
||||
latest, _ := s.catalog.GetLatestCompatibleVersion(sensor.ID, models.HubVersion)
|
||||
if latest != "" && sensor.DeployedVersion != latest {
|
||||
_ = s.store.SetNodeSensorDeployedVersion(nodeID, sensor.ID, latest)
|
||||
if sensor.DeployedVersion == "" {
|
||||
latest, _ := s.catalog.GetLatestCompatibleVersion(sensor.ID, models.HubVersion)
|
||||
if latest != "" {
|
||||
_ = s.store.SetNodeSensorDeployedVersion(nodeID, sensor.ID, latest)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -236,11 +268,11 @@ func GenerateRevisionHash(sensors []models.NodeSensor, catSvc *catalog.Service,
|
||||
}
|
||||
var configs []sensorConfig
|
||||
for _, s := range sensors {
|
||||
latestVersion := ""
|
||||
if catSvc != nil {
|
||||
latestVersion, _ = catSvc.GetLatestCompatibleVersion(s.ID, currentHubVersion)
|
||||
targetVersion := s.DeployedVersion
|
||||
if targetVersion == "" && catSvc != nil {
|
||||
targetVersion, _ = catSvc.GetLatestCompatibleVersion(s.ID, currentHubVersion)
|
||||
}
|
||||
configs = append(configs, sensorConfig{ID: s.ID, Version: latestVersion, EnvVars: s.EnvVars})
|
||||
configs = append(configs, sensorConfig{ID: s.ID, Version: targetVersion, EnvVars: s.EnvVars})
|
||||
}
|
||||
sort.Slice(configs, func(i, j int) bool { return configs[i].ID < configs[j].ID })
|
||||
b, _ := json.Marshal(configs)
|
||||
|
||||
@@ -136,6 +136,7 @@ func initializeDefaultConfigTx(tx *sql.Tx) error {
|
||||
"auto_purge_days": "0",
|
||||
"siem_address": "",
|
||||
"siem_protocol": "tcp",
|
||||
// TODO: Add "registry_url": "https://raw.githubusercontent.com/andreicscs/HoneyWire/registry-pages" default
|
||||
}
|
||||
|
||||
stmt, err := tx.Prepare("INSERT OR IGNORE INTO config (key, value) VALUES (?, ?)")
|
||||
|
||||
@@ -15,7 +15,8 @@ const emit = defineEmits<{
|
||||
(e: 'update', updates: any): void,
|
||||
(e: 'silence'): void,
|
||||
(e: 'delete'): void,
|
||||
(e: 'openDetail'): void
|
||||
(e: 'openDetail'): void,
|
||||
(e: 'upgradeAll'): void
|
||||
}>()
|
||||
|
||||
const { copiedStates, handleCopy } = useClipboard() as any
|
||||
@@ -114,6 +115,10 @@ const removeTag = (index: number | string) => {
|
||||
</svg>
|
||||
{{ node.isSilenced ? 'Unsilence Node' : 'Silence Node' }}
|
||||
</button>
|
||||
<button v-if="node.hasUpdateAvailable" @click="$emit('upgradeAll')" class="w-full text-left px-3 py-2 text-sm font-medium text-low flex items-center gap-2 hover:bg-low/10 transition-colors group">
|
||||
<svg class="w-3.5 h-3.5 transition-transform duration-normal group-hover:rotate-180" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" /></svg>
|
||||
Update Node
|
||||
</button>
|
||||
<button @click="$emit('delete')" class="w-full text-left px-3 py-2 text-sm font-medium text-danger-text flex items-center gap-2 hover:bg-danger-bg transition-colors group border-t border-border-default mt-1 pt-2">
|
||||
<svg class="w-3.5 h-3.5 transition-transform duration-normal group-hover:scale-110" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 6v14a2 2 0 002 2h10a2 2 0 002-2V6M10 11v6M14 11v6" /><path class="origin-bottom-right transition-transform duration-normal group-hover:-rotate-[15deg] group-hover:-translate-y-0.5" d="M3 6h18M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2" /></svg>
|
||||
<span v-if="isDeleting">Deleting...</span><span v-else>Delete</span>
|
||||
|
||||
@@ -10,11 +10,6 @@ const getInstalled = (manifest: any) => {
|
||||
return props.installedSensors?.find((sensor: any) => sensor.id === manifest.id || sensor.sensorId === manifest.id || sensor.name === manifest.id)
|
||||
}
|
||||
|
||||
const hasUpdate = (manifest: any) => {
|
||||
const installed = getInstalled(manifest)
|
||||
return installed?.updateAvailable || false
|
||||
}
|
||||
|
||||
const handleSensorClick = (s: any) => {
|
||||
const installed = getInstalled(s)
|
||||
if (installed) {
|
||||
@@ -46,7 +41,6 @@ const handleSensorClick = (s: any) => {
|
||||
<div class="flex justify-between items-start mb-3">
|
||||
<div class="w-10 h-10 rounded-md bg-bg-base border border-border-default/50 text-text-h flex items-center justify-center shrink-0 group-hover:scale-105 transition-transform duration-normal"><svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" :d="s.icon_svg"></path></svg></div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span v-if="hasUpdate(s)" class="w-2 h-2 rounded-full bg-low/70 shadow-[0_0_8px_rgba(var(--color-low),0.5)]" title="Sensor Update Available"></span>
|
||||
<span class="px-2 py-0.5 rounded text-sm font-medium tracking-wider bg-bg-inset text-text-m border border-border-default/50">{{ s.osi_layer }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -16,7 +16,8 @@ const emit = defineEmits<{
|
||||
(e: 'silence'): void,
|
||||
(e: 'delete'): void,
|
||||
(e: 'sync'): void,
|
||||
(e: 'manageKey'): void
|
||||
(e: 'manageKey'): void,
|
||||
(e: 'upgradeAll'): void
|
||||
}>()
|
||||
|
||||
const { copiedStates, handleCopy } = useClipboard() as any
|
||||
@@ -99,6 +100,7 @@ const removeTag = (index: number) => {
|
||||
<span v-if="node.hasPendingConfig" class="shrink-0 text-high" title="Pending sync — click Sync Node below to apply changes">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/></svg>
|
||||
</span>
|
||||
<span v-if="node.hasUpdateAvailable" class="w-2.5 h-2.5 rounded-full bg-low/70 shadow-[0_0_8px_rgba(var(--color-low),0.5)] shrink-0" title="Updates available for installed sensors"></span>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-x-6 gap-y-2 text-sm text-text-m">
|
||||
@@ -149,6 +151,11 @@ const removeTag = (index: number) => {
|
||||
{{ node.isSilenced ? 'Unsilence Node' : 'Silence Node' }}
|
||||
</button>
|
||||
|
||||
<button v-if="node.hasUpdateAvailable" @click="$emit('upgradeAll')" class="w-full text-left px-3 py-2 text-sm font-medium text-low flex items-center gap-2 hover:bg-low/10 transition-colors group">
|
||||
<svg class="w-3.5 h-3.5 transition-transform duration-normal group-hover:rotate-180" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" /></svg>
|
||||
Update Node
|
||||
</button>
|
||||
|
||||
<button @click="$emit('delete')" class="w-full text-left px-3 py-2 text-sm font-medium text-danger-text flex items-center gap-2 hover:bg-danger-bg transition-colors group border-t border-border-default mt-1 pt-2">
|
||||
<svg class="w-3.5 h-3.5 transition-transform duration-normal group-hover:scale-110" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M5 6v14a2 2 0 002 2h10a2 2 0 002-2V6M10 11v6M14 11v6" />
|
||||
|
||||
@@ -3,7 +3,7 @@ import BaseMeatballMenu from '../ui/navigation/BaseMeatballMenu.vue'
|
||||
import { formatSensorId } from '../../utils/formatSensorId'
|
||||
|
||||
const props = defineProps<{ sensors: any[] }>()
|
||||
defineEmits<{ (e: 'edit', sensor: any): void, (e: 'toggleSilence', sensor: any): void, (e: 'remove', sensor: any): void }>()
|
||||
defineEmits<{ (e: 'edit', sensor: any): void, (e: 'toggleSilence', sensor: any): void, (e: 'remove', sensor: any): void, (e: 'upgrade', sensor: any): void }>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -24,6 +24,7 @@ defineEmits<{ (e: 'edit', sensor: any): void, (e: 'toggleSilence', sensor: any):
|
||||
</div>
|
||||
<BaseMeatballMenu :id="`sensor-menu-${sensor.id}`">
|
||||
<button @click="$emit('edit', sensor)" class="w-full text-left px-3 py-2 text-sm font-medium flex items-center gap-2 text-text-m hover:bg-secondary-hover hover:text-text-h transition-colors group"><svg class="w-3.5 h-3.5 overflow-visible" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5" /><path class="transition-transform duration-normal group-hover:-translate-y-0.5 group-hover:translate-x-0.5 group-hover:-rotate-6" d="M17.586 3.586a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" /></svg>Edit</button>
|
||||
<button v-if="sensor.updateAvailable" @click="$emit('upgrade', sensor)" class="w-full text-left px-3 py-2 text-sm font-medium text-low flex items-center gap-2 hover:bg-low/10 transition-colors group"><svg class="w-3.5 h-3.5 transition-transform duration-normal group-hover:rotate-180" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" /></svg>Update Sensor</button>
|
||||
<button @click="$emit('toggleSilence', sensor)" class="w-full text-left px-3 py-2 text-sm font-medium flex items-center gap-2 text-text-m hover:bg-secondary-hover transition-colors group" :class="sensor.isSilenced ? 'text-archive-text hover:bg-archive-bg' : ' hover:text-text-h'"><svg class="w-3.5 h-3.5 transition-transform duration-normal group-hover:rotate-12 group-active:-rotate-12 origin-top" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path v-if="!sensor.isSilenced" d="M18 8A6 6 0 006 8c0 7-3 9-3 9h18s-3-2-3-9M13.73 21a2 2 0 01-3.46 0"/><path v-if="sensor.isSilenced" d="M13.73 21a2 2 0 01-3.46 0m-3.9-3.9a2.032 2.032 0 01-2.37.5L4 17h12.59l3.12 3.12M3 3l18 18M18 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341c-.5.186-.967.447-1.385.772"/></svg>{{ sensor.isSilenced ? 'Unsilence Alerts' : 'Silence Alerts' }}</button>
|
||||
<button @click="$emit('remove', sensor)" class="w-full text-left px-3 py-2 text-sm font-medium text-danger-text flex items-center gap-2 hover:bg-danger-bg transition-colors group border-t border-border-default mt-1 pt-2"><svg class="w-3.5 h-3.5 transition-transform duration-normal group-hover:scale-110" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 6v14a2 2 0 002 2h10a2 2 0 002-2V6M10 11v6M14 11v6" /><path class="origin-bottom-right transition-transform duration-normal group-hover:-rotate-[15deg] group-hover:-translate-y-0.5" d="M3 6h18M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2" /></svg>Remove Sensor</button>
|
||||
</BaseMeatballMenu>
|
||||
|
||||
@@ -26,9 +26,11 @@ watch(() => props.show, (shown) => {
|
||||
if (shown && props.sensor) {
|
||||
activeTab.value = props.isEditing ? 'config' : 'readme'
|
||||
envVarValues.value = { ...props.initialEnvVars }
|
||||
fetchYamlFromHub()
|
||||
} else {
|
||||
envVarValues.value = {}; rawCompose.value = ''; highlightedCompose.value = ''; activeEnvVar.value = null
|
||||
envVarValues.value = {}
|
||||
rawCompose.value = ''
|
||||
highlightedCompose.value = ''
|
||||
activeEnvVar.value = null
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import { api } from '../../api/client'
|
||||
|
||||
// --- TYPES & INTERFACES ---
|
||||
export interface RawSensorPayload {
|
||||
sensorId: string
|
||||
@@ -10,6 +9,7 @@ export interface RawSensorPayload {
|
||||
isSilenced?: boolean
|
||||
envVars?: Record<string, any>
|
||||
metadata?: Record<string, any>
|
||||
deployedVersion?: string
|
||||
updateAvailable?: boolean
|
||||
lastHeartbeat?: string | null
|
||||
}
|
||||
@@ -62,6 +62,7 @@ export interface InstalledSensor {
|
||||
isSilenced: boolean
|
||||
envVars: Record<string, any>
|
||||
metadata: Record<string, any>
|
||||
deployedVersion?: string
|
||||
updateAvailable?: boolean
|
||||
lastHeartbeat: string | null
|
||||
}
|
||||
@@ -368,6 +369,7 @@ export const useFleetStore = defineStore('fleet', () => {
|
||||
display: raw.customName || rawId,
|
||||
status: raw.status || 'down',
|
||||
isSilenced: raw.isSilenced ?? false,
|
||||
deployedVersion: raw.deployedVersion || '',
|
||||
updateAvailable: raw.updateAvailable || false,
|
||||
envVars: raw.envVars || {},
|
||||
metadata: raw.metadata || {},
|
||||
@@ -592,6 +594,16 @@ export const useFleetStore = defineStore('fleet', () => {
|
||||
}
|
||||
}
|
||||
|
||||
const upgradeNode = async (nodeId: string): Promise<void> => {
|
||||
try {
|
||||
await api.post(`/api/v1/nodes/${nodeId}/upgrade`)
|
||||
fetchNodeDetails(nodeId)
|
||||
} catch (err: any) {
|
||||
console.error('Node Upgrade Failed:', err.message || 'Failed to upgrade node')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
const deleteNode = async (nodeId: string): Promise<{ success: boolean; error?: string }> => {
|
||||
markNodeAction(nodeId, 'deleting')
|
||||
|
||||
@@ -627,6 +639,18 @@ export const useFleetStore = defineStore('fleet', () => {
|
||||
}
|
||||
}
|
||||
|
||||
const upgradeSensor = async (nodeId: string, rawSensorId: string) => {
|
||||
if (!nodeId || !rawSensorId) throw new Error('Missing required parameters')
|
||||
|
||||
try {
|
||||
await api.post(`/api/v1/nodes/${encodeURIComponent(nodeId)}/sensors/${encodeURIComponent(rawSensorId)}/upgrade`)
|
||||
fetchNodeDetails(nodeId)
|
||||
} catch (err: any) {
|
||||
console.error('Sensor Upgrade Failed:', err.message || 'Failed to upgrade sensor')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
const updateSensor = async (nodeId: string, rawSensorId: string, { customName, configValues }: { customName?: string, configValues?: Record<string, string> }): Promise<void> => {
|
||||
const sensor = getSensor(nodeId, rawSensorId)
|
||||
if (!sensor) return
|
||||
@@ -715,8 +739,8 @@ export const useFleetStore = defineStore('fleet', () => {
|
||||
isNodeActionPending, isNodeSilenced,
|
||||
fetchFleet, fetchNodeDetails, fetchUptime, fetchManifests,
|
||||
selectTarget, clearSelection, setActiveTimeframe,
|
||||
createNode, deleteNode, updateNode,
|
||||
addSensor, updateSensor, removeSensor, toggleSilence, silenceNode,
|
||||
createNode, deleteNode, updateNode, upgradeNode,
|
||||
addSensor, updateSensor, upgradeSensor, removeSensor, toggleSilence, silenceNode,
|
||||
fetchCompose, generateCompose, syncNode,
|
||||
handleWsUpdate,
|
||||
}
|
||||
|
||||
@@ -77,6 +77,10 @@ const handleForgetNode = async (nodeId: string) => {
|
||||
if (!res.success) alert(res.error)
|
||||
}
|
||||
|
||||
const handleUpgradeNode = async (nodeId: string) => {
|
||||
await fleetStore.upgradeNode(nodeId)
|
||||
}
|
||||
|
||||
const handleOpenNodeDetail = (nodeId: string) => {
|
||||
fleetStore.selectTarget(nodeId, null, false)
|
||||
router.push({ name: 'node-details', params: { id: nodeId } })
|
||||
@@ -125,6 +129,7 @@ const handleOpenNodeDetail = (nodeId: string) => {
|
||||
@silence="handleSilenceNode(node.id)"
|
||||
@delete="handleForgetNode(node.id)"
|
||||
@openDetail="handleOpenNodeDetail(node.id)"
|
||||
@upgradeAll="handleUpgradeNode(node.id)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useEventsStore } from '../stores/Events/events.ts'
|
||||
import { useAppStore } from '../stores/System/app.ts'
|
||||
import { useConfigStore } from '../stores/Config/config.ts'
|
||||
import type { FleetNode } from '../stores/Fleet/fleet.ts'
|
||||
import { api } from '../api/client.ts'
|
||||
|
||||
import NodeDetailHeader from '../components/nodedetails/NodeDetailHeader.vue'
|
||||
import NodeStatWidgets from '../components/nodedetails/NodeStatWidgets.vue'
|
||||
@@ -75,12 +76,22 @@ const getUIDefault = (def: any) => {
|
||||
// --- SENSOR ACTIONS (delegated to store) ---
|
||||
|
||||
const handleToggleSensorSilence = async (sensor: any) => {
|
||||
if (!node.value?.id || !sensor.sensorId) return
|
||||
try {
|
||||
await fleetStore.toggleSilence(node.value.id, sensor.sensorId, !sensor.isSilenced)
|
||||
} catch (err) {
|
||||
alert('Unable to change sensor silence state. Please try again.')
|
||||
}
|
||||
if (!node.value) return
|
||||
try {
|
||||
await fleetStore.toggleSilence(node.value.id, sensor.sensorId, !sensor.isSilenced)
|
||||
} catch (e) {
|
||||
// Error handled in store
|
||||
}
|
||||
}
|
||||
|
||||
const handleUpgradeSensor = async (sensor: any) => {
|
||||
if (!node.value) return
|
||||
await fleetStore.upgradeSensor(node.value.id, sensor.sensorId)
|
||||
}
|
||||
|
||||
const handleUpgradeAll = async () => {
|
||||
if (!node.value) return
|
||||
await fleetStore.upgradeNode(node.value.id)
|
||||
}
|
||||
|
||||
const handleRemoveSensor = async (sensor: any) => {
|
||||
@@ -237,8 +248,18 @@ const openSensor = (sensor: any) => {
|
||||
showSensorModal.value = true
|
||||
}
|
||||
|
||||
const editSensor = (installedSensor: any) => {
|
||||
const manifest = manifests.value.find((m: any) => m.id === installedSensor.id || m.id === installedSensor.sensorId || m.id === installedSensor.name)
|
||||
const editSensor = async (installedSensor: any) => {
|
||||
let manifest = manifests.value.find((m: any) => m.id === installedSensor.id || m.id === installedSensor.sensorId || m.id === installedSensor.name)
|
||||
|
||||
if (installedSensor.deployedVersion) {
|
||||
try {
|
||||
const res = await api.get(`/api/v1/manifests/${encodeURIComponent(installedSensor.sensorId || installedSensor.id || installedSensor.name)}/versions?version=${encodeURIComponent(installedSensor.deployedVersion)}`)
|
||||
manifest = await res.json()
|
||||
} catch (err) {
|
||||
console.error("Failed to fetch deployed manifest version schema:", err)
|
||||
}
|
||||
}
|
||||
|
||||
if (!manifest) {
|
||||
alert('Sensor manifest not found')
|
||||
return
|
||||
@@ -298,6 +319,7 @@ const closeSensor = () => {
|
||||
@delete="handleDeleteNode"
|
||||
@sync="triggerManualSync"
|
||||
@manage-key="showKeyModal = true"
|
||||
@upgradeAll="handleUpgradeAll"
|
||||
/>
|
||||
|
||||
<NodeStatWidgets :node="node" :recent-activity="recentActivity" @view-all-events="viewAllEvents" />
|
||||
@@ -307,6 +329,7 @@ const closeSensor = () => {
|
||||
@edit="editSensor"
|
||||
@toggleSilence="handleToggleSensorSilence"
|
||||
@remove="handleRemoveSensor"
|
||||
@upgrade="handleUpgradeSensor"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
+2
-1
@@ -42,8 +42,9 @@ Sensors execute as isolated containers.
|
||||
* Strict typed schema enforcement (`DisallowUnknownFields` JSON decoding).
|
||||
* Hub-side normalization prior to compilation.
|
||||
* Interpolation rejection (blocks `${`, `{{`, and `$` patterns).
|
||||
* Strict version pinning for historical manifests (prevents tag-overwriting and cache poisoning).
|
||||
* Image digest pinning (ensures cryptographically verified, immutable container pulls).
|
||||
* **Gaps (NOT IMPLEMENTED):**
|
||||
* Image digest pinning (currently relying on image tags).
|
||||
* Cryptographic manifest signature verification.
|
||||
* Provenance tracking.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user