mirror of
https://github.com/andreicscs/HoneyWire
synced 2026-06-26 12:39:53 +00:00
updated versioning logic to leverage the semantic versioning instead of min_ver variables. reflected changes to docs, removed background polling for registry.
started to transition honeywire system versions to 2.0.0 improved workers startup messages
This commit is contained in:
@@ -178,13 +178,22 @@ The Wizard performs discovery using point-in-time host inspection. No resident d
|
||||
|
||||
HoneyWire uses a **git-tag-driven static registry** for sensor versioning, enabling sensor updates independent of Hub releases.
|
||||
|
||||
### Split Architecture Philosophy
|
||||
HoneyWire intentionally decouples component distribution to match their structural requirements:
|
||||
- **Sensors (Custom Registry):** Sensors require a dynamic JSON schema (`index.json` + versioned manifests) so the Hub can instantly render dynamic configuration UI forms and support isolated offline air-gapping.
|
||||
- **Wizard (GitHub Releases):** The Wizard is a compiled CLI binary with no dynamic UI configuration schema. It is distributed natively via GitHub/Gitea releases to keep the `get.honeywire.dev` install scripts dead-simple.
|
||||
|
||||
### Tagging Convention
|
||||
- **Hub releases:** `hub/v{semver}` (e.g., `hub/v2.0.0`)
|
||||
- **Wizard releases:** `wizard/v{semver}` (e.g., `wizard/v1.1.0`)
|
||||
- **Sensor releases:** `sensor/{sensor-name}/v{semver}` (e.g., `sensor/file-canary/v1.2.0`)
|
||||
|
||||
### Compatibility Mechanism
|
||||
Each Hub binary embeds a `HubAPIVersion` constant (integer). Each sensor manifest declares a `min_hub_api` field. The Hub only presents sensor versions where `min_hub_api <= HubAPIVersion`. This ~15 lines of Go code is the entire backward compatibility mechanism.
|
||||
HoneyWire strictly uses Semantic Versioning (`vMAJOR.MINOR.PATCH`).
|
||||
- **Sensors vs Hub:** The Hub natively checks if the sensor's major version exactly matches its own major version. If they match, they are compatible.
|
||||
- **Wizard vs Hub:** The Hub enforces a rigid `X-Wizard-Version` header check. If the Wizard's major version differs from the Hub, the Hub explicitly blocks the connection (`HTTP 426 Upgrade Required`).
|
||||
|
||||
This relies entirely on the inherent stability guarantees of Semantic Versioning rather than manual configuration metadata.
|
||||
|
||||
### Registry Pipeline
|
||||
When a `sensor/**` tag is pushed, a Gitea Action:
|
||||
|
||||
@@ -77,8 +77,6 @@ The Sensor Manifest is the declarative JSON schema used to describe a decoy. It
|
||||
"id": "hw-tcp-tarpit",
|
||||
"version": "1.1.0",
|
||||
"schema_version": "1.0",
|
||||
"min_hub_api": "1",
|
||||
"min_wizard_version": "1.0.0",
|
||||
"name": "TCP Tarpit",
|
||||
"category": "network",
|
||||
"osi_layer": "L4",
|
||||
@@ -128,7 +126,6 @@ The Sensor Manifest is the declarative JSON schema used to describe a decoy. It
|
||||
```
|
||||
|
||||
### Key Subsystems
|
||||
- `min_hub_api`: Integer string. The minimum Hub API version required to deploy this sensor. The Hub filters out sensor versions where `min_hub_api` exceeds its own `HubAPIVersion` constant.
|
||||
- `heuristics.triggers`: Used by the Wizard Discovery Engine. If the Wizard observes matching `processes`, `ports`, or `file_patterns` on the host, it will recommend this sensor.
|
||||
- `deployment`: Used by the Wizard Deployment Engine to generate the Intermediate Representation (IR) and final `docker-compose.yml`.
|
||||
- `deployment.env_vars`: Rendered in the Hub UI so users can configure the sensor dynamically.
|
||||
|
||||
@@ -129,6 +129,10 @@ Check the `registry-pages` branch to confirm:
|
||||
- The `latest` field points to the new version
|
||||
|
||||
### 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.
|
||||
Because HoneyWire explicitly avoids background network polling for security and network hygiene, the Hub will not automatically discover this new version in the background.
|
||||
|
||||
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.
|
||||
To sync the catalog:
|
||||
1. Run `honeywire status` via the edge node CLI, OR
|
||||
2. go into Fleet Management or Node Details view in the Hub Dashboard.
|
||||
|
||||
This triggers an on-demand registry fetch. The Hub will compare the latest compatible versions against deployed sensors and flag nodes with an **"Update Available"** indicator. Users must manually trigger the `/api/v1/nodes/{id}/upgrade` endpoint (via the UI) or run `honeywire apply` to instruct the node to pull the new version schema and execute a compose restart.
|
||||
|
||||
@@ -107,11 +107,6 @@ func main() {
|
||||
siemProtocol := loadConfigSafe(dbStore, "siem_protocol", "tcp")
|
||||
siemService.UpdateConfig(siemAddress, siemProtocol)
|
||||
|
||||
if siemAddress != "" {
|
||||
log.Printf("[SIEM] Configured to forward to %s via %s\n", siemAddress, siemProtocol)
|
||||
} else {
|
||||
log.Println("[SIEM] Forwarding disabled (no address configured).")
|
||||
}
|
||||
|
||||
go eventSvc.StartRetentionWorker(rootCtx)
|
||||
|
||||
|
||||
@@ -106,19 +106,19 @@ func AgentAuthMiddleware(auth NodeAuthenticator, rateLimiter *RateLimiter) func(
|
||||
}
|
||||
|
||||
// Version handshake
|
||||
wizardMinHubAPIStr := r.Header.Get("X-Wizard-Min-Hub-Api")
|
||||
if wizardMinHubAPIStr != "" {
|
||||
reqVer := strings.TrimSpace(wizardMinHubAPIStr)
|
||||
wizardVersionStr := r.Header.Get("X-Wizard-Version")
|
||||
if wizardVersionStr != "" {
|
||||
reqVer := strings.TrimSpace(wizardVersionStr)
|
||||
if !strings.HasPrefix(reqVer, "v") { reqVer = "v" + reqVer }
|
||||
curVer := models.HubVersion
|
||||
if !strings.HasPrefix(curVer, "v") { curVer = "v" + curVer }
|
||||
|
||||
if !semver.IsValid(reqVer) {
|
||||
http.Error(w, "Invalid X-Wizard-Min-Hub-Api format", http.StatusBadRequest)
|
||||
http.Error(w, "Invalid X-Wizard-Version format", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if semver.Compare(curVer, reqVer) < 0 {
|
||||
http.Error(w, "This Wizard requires Hub "+wizardMinHubAPIStr+" or later. Please update your Hub.", http.StatusUpgradeRequired)
|
||||
if semver.Major(curVer) != semver.Major(reqVer) {
|
||||
http.Error(w, "This Wizard version ("+wizardVersionStr+") is incompatible with this Hub. Please update.", http.StatusUpgradeRequired)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,7 +131,7 @@ func TestAgentAuthMiddleware(t *testing.T) {
|
||||
req := httptest.NewRequest("POST", "/", nil)
|
||||
req.Header.Set("X-Api-Key", "agent-key")
|
||||
// Simulate a Wizard requesting a highly futuristic Hub API version
|
||||
req.Header.Set("X-Wizard-Min-Hub-Api", "99")
|
||||
req.Header.Set("X-Wizard-Version", "99.0.0")
|
||||
rec := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rec, req)
|
||||
// Should return HTTP 426 Upgrade Required
|
||||
@@ -141,8 +141,8 @@ func TestAgentAuthMiddleware(t *testing.T) {
|
||||
t.Run("Legacy Backward Compat (Wizard v1, Hub v2)", func(t *testing.T) {
|
||||
req := httptest.NewRequest("POST", "/", nil)
|
||||
req.Header.Set("X-Api-Key", "agent-key")
|
||||
// Simulate a legacy Wizard
|
||||
req.Header.Set("X-Wizard-Min-Hub-Api", "1")
|
||||
// Simulate a valid Wizard
|
||||
req.Header.Set("X-Wizard-Version", "2.0.0")
|
||||
rec := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rec, req)
|
||||
// Should pass completely natively
|
||||
@@ -152,7 +152,7 @@ func TestAgentAuthMiddleware(t *testing.T) {
|
||||
t.Run("Malformed Wizard Header", func(t *testing.T) {
|
||||
req := httptest.NewRequest("POST", "/", nil)
|
||||
req.Header.Set("X-Api-Key", "agent-key")
|
||||
req.Header.Set("X-Wizard-Min-Hub-Api", " garbage ")
|
||||
req.Header.Set("X-Wizard-Version", " garbage ")
|
||||
rec := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rec, req)
|
||||
assert.Equal(t, http.StatusBadRequest, rec.Code)
|
||||
|
||||
@@ -3,7 +3,6 @@ package catalog
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -16,8 +15,7 @@ type RegistryIndex struct {
|
||||
ID string `json:"id"`
|
||||
Latest string `json:"latest"`
|
||||
Versions []struct {
|
||||
V string `json:"v"`
|
||||
MinHubVersion string `json:"min_hub_version"`
|
||||
V string `json:"v"`
|
||||
} `json:"versions"`
|
||||
} `json:"sensors"`
|
||||
}
|
||||
@@ -107,10 +105,8 @@ func (s *Service) GetLatestCompatibleVersion(sensorID string, currentHubVersion
|
||||
idx := s.indexCache
|
||||
s.mu.RUnlock()
|
||||
|
||||
// If cache is empty, try to refresh it synchronously
|
||||
if idx == nil {
|
||||
if err := s.RefreshIndex(); err != nil {
|
||||
log.Printf("[WARNING] Registry fetch failed (err: %v), no cache available.", err)
|
||||
return "", err
|
||||
}
|
||||
s.mu.RLock()
|
||||
@@ -118,20 +114,24 @@ func (s *Service) GetLatestCompatibleVersion(sensorID string, currentHubVersion
|
||||
s.mu.RUnlock()
|
||||
}
|
||||
|
||||
if idx == nil {
|
||||
return "", fmt.Errorf("registry index not available")
|
||||
}
|
||||
|
||||
for _, sensor := range idx.Sensors {
|
||||
if sensor.ID == sensorID {
|
||||
for i := len(sensor.Versions) - 1; i >= 0; i-- {
|
||||
reqVer := strings.TrimSpace(sensor.Versions[i].MinHubVersion)
|
||||
reqVer := strings.TrimSpace(sensor.Versions[i].V)
|
||||
// Format semver standard 'vX.Y.Z' for comparison
|
||||
if !strings.HasPrefix(reqVer, "v") {
|
||||
reqVer = "v" + reqVer
|
||||
}
|
||||
curVer := currentHubVersion
|
||||
curVer := strings.TrimSpace(currentHubVersion)
|
||||
if !strings.HasPrefix(curVer, "v") {
|
||||
curVer = "v" + curVer
|
||||
}
|
||||
|
||||
if semver.IsValid(reqVer) && semver.Compare(curVer, reqVer) >= 0 {
|
||||
if semver.IsValid(reqVer) && semver.Major(curVer) == semver.Major(reqVer) {
|
||||
return sensor.Versions[i].V, nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,8 +85,6 @@ type SensorManifest struct {
|
||||
ID string `json:"id"`
|
||||
Version string `json:"version"`
|
||||
SchemaVersion string `json:"schema_version"`
|
||||
MinHubVersion string `json:"min_hub_version"`
|
||||
MinWizardVersion string `json:"min_wizard_version"`
|
||||
Name string `json:"name"`
|
||||
Category string `json:"category"`
|
||||
OSILayer string `json:"osi_layer"`
|
||||
|
||||
@@ -51,6 +51,7 @@ func NewService(store Store, dashboardPassword string) *Service {
|
||||
|
||||
// StartWorkers starts background goroutines for cleaning up sessions and brute-force trackers.
|
||||
func (s *Service) StartWorkers(ctx context.Context) {
|
||||
log.Println("[Auth] Worker started.")
|
||||
go s.cleanupSessions(ctx)
|
||||
go s.cleanupAuthTracker(ctx)
|
||||
}
|
||||
@@ -63,6 +64,7 @@ func (s *Service) cleanupSessions(ctx context.Context) {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
log.Println("[Auth] Worker stopped.")
|
||||
return
|
||||
case <-ticker.C:
|
||||
s.sessionMu.Lock()
|
||||
|
||||
@@ -73,7 +73,7 @@ func (s *Service) fetchStrictCatalogManifests(currentHubVersion string) ([]model
|
||||
}
|
||||
|
||||
if err := s.catalog.RefreshIndex(); err != nil {
|
||||
log.Printf("[WARNING] fetchStrictCatalogManifests catalog refresh failed: %v", err)
|
||||
// Suppressed log spam when registry is down
|
||||
}
|
||||
|
||||
index := s.catalog.GetIndex()
|
||||
@@ -226,15 +226,15 @@ func (s *Service) GetNodeCompose(token, hostFallback string, currentHubVersion s
|
||||
return nil, fmt.Errorf("invalid_manifest: %w", valErr)
|
||||
}
|
||||
|
||||
if manifest.MinHubVersion != "" {
|
||||
reqVer := strings.TrimSpace(manifest.MinHubVersion)
|
||||
if manifest.Version != "" {
|
||||
reqVer := strings.TrimSpace(manifest.Version)
|
||||
if !strings.HasPrefix(reqVer, "v") { reqVer = "v" + reqVer }
|
||||
curVer := currentHubVersion
|
||||
curVer := strings.TrimSpace(currentHubVersion)
|
||||
if !strings.HasPrefix(curVer, "v") { curVer = "v" + curVer }
|
||||
|
||||
if !semver.IsValid(reqVer) || semver.Compare(curVer, reqVer) < 0 {
|
||||
log.Printf("[ERROR] Sensor %s requires Hub Version %s, but Hub is running %s", sensor.ID, reqVer, curVer)
|
||||
return nil, fmt.Errorf("incompatible_sensor: %s requires Hub Version %s", sensor.ID, reqVer)
|
||||
if semver.IsValid(reqVer) && semver.Major(curVer) != semver.Major(reqVer) {
|
||||
log.Printf("[ERROR] Sensor %s (v%s) is incompatible with Hub (v%s) - Major versions must match", sensor.ID, reqVer, curVer)
|
||||
return nil, fmt.Errorf("incompatible_sensor: %s (v%s) requires a matching Hub Major version", sensor.ID, reqVer)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -68,9 +68,10 @@ func TestComposeSmartVersionSelection(t *testing.T) {
|
||||
"id": "hw-sensor-test",
|
||||
"latest": "2.0.0",
|
||||
"versions": []map[string]string{
|
||||
{"v": "1.0.0", "min_hub_version": "1.0.0"},
|
||||
{"v": "1.5.0", "min_hub_version": " 2.0.0 "}, // Injecting malicious whitespace
|
||||
{"v": "2.0.0", "min_hub_version": " 3.0.0"}, // Injecting malicious whitespace
|
||||
{"v": "1.0.0"},
|
||||
{"v": "2.0.0"},
|
||||
{"v": "2.5.0"},
|
||||
{"v": "3.0.0"},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -80,12 +81,15 @@ func TestComposeSmartVersionSelection(t *testing.T) {
|
||||
|
||||
// Mock responses for the requested versions
|
||||
version := r.URL.Path[len("/test-v") : len(r.URL.Path)-5] // Extract version from path
|
||||
if version == "" {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"id": "hw-sensor-test",
|
||||
"version": version,
|
||||
"schema_version": "1.0",
|
||||
"min_hub_version": "1.0.0", // Mock doesn't need to match perfectly, just needs to parse
|
||||
"deployment": map[string]interface{}{
|
||||
"image_repository": "test",
|
||||
"image_tag": version,
|
||||
@@ -96,6 +100,7 @@ func TestComposeSmartVersionSelection(t *testing.T) {
|
||||
|
||||
store := &MockStore{RegistryURL: ts.URL}
|
||||
catSvc := catalog.NewService(store, nil)
|
||||
catSvc.RefreshIndex() // explicitly refresh
|
||||
svc := composesvc.NewService(store, catSvc)
|
||||
|
||||
// VERSIONING ARCHITECTURE EXPLANATION (SENSOR REGISTRY):
|
||||
@@ -104,26 +109,25 @@ func TestComposeSmartVersionSelection(t *testing.T) {
|
||||
// with the currently executing Hub. If the Hub's API is too old for the absolute latest
|
||||
// sensor release, it gracefully injects the previous compatible tag into the docker-compose YAML.
|
||||
t.Run("Perfect Match Resolution (Hub API 2)", func(t *testing.T) {
|
||||
// Hub API 2 should select v1.5.0, ignoring v2.0.0
|
||||
// Hub API 2 should select v2.5.0
|
||||
yamlData, err := svc.GetNodeCompose("dummy", "http://localhost", "2.0.0")
|
||||
if err != nil {
|
||||
t.Fatalf("Expected success, got error: %v", err)
|
||||
}
|
||||
|
||||
if !contains(yamlData, "image: test:1.5.0") {
|
||||
t.Errorf("Expected to deploy v1.5.0, got yaml:\n%s", string(yamlData))
|
||||
if !contains(yamlData, "image: test:2.5.0") {
|
||||
t.Errorf("Expected to deploy v2.5.0, got yaml:\n%s", string(yamlData))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Legacy Backward Compat (Hub API 4)", func(t *testing.T) {
|
||||
// Hub API 4 should select v2.0.0 because 4 >= 3
|
||||
t.Run("Incompatible Hub API 4", func(t *testing.T) {
|
||||
// Hub API 4 should NOT select v3.0.0 because it's looking for v4.x.x
|
||||
yamlData, err := svc.GetNodeCompose("dummy", "http://localhost", "4.0.0")
|
||||
if err != nil {
|
||||
t.Fatalf("Expected success, got error: %v", err)
|
||||
}
|
||||
|
||||
if !contains(yamlData, "image: test:2.0.0") {
|
||||
t.Errorf("Expected to deploy v2.0.0, got yaml:\n%s", string(yamlData))
|
||||
if contains(yamlData, "image: test:") {
|
||||
t.Errorf("Expected NO sensor to be deployed, got yaml:\n%s", string(yamlData))
|
||||
}
|
||||
})
|
||||
|
||||
@@ -140,14 +144,12 @@ func TestComposeSmartVersionSelection(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("Whitespace Robust Parsing", func(t *testing.T) {
|
||||
// Even if min_hub_version has spaces like " 3.0.0", it should parse cleanly and fail on Hub Version 2.0.0
|
||||
yamlData, err := svc.GetNodeCompose("dummy", "http://localhost", "2.0.0")
|
||||
yamlData, err := svc.GetNodeCompose("dummy", "http://localhost", " 2.0.0 ")
|
||||
if err != nil {
|
||||
t.Fatalf("Expected success, got error: %v", err)
|
||||
}
|
||||
// Because min_hub_version=" 3.0.0 " for v2.0.0 parses successfully, Hub Version 2.0.0 will correctly reject it and fallback to v1.5.0
|
||||
if !contains(yamlData, "image: test:1.5.0") {
|
||||
t.Errorf("Expected fallback to v1.5.0, got yaml:\n%s", string(yamlData))
|
||||
if !contains(yamlData, "image: test:2.5.0") {
|
||||
t.Errorf("Expected fallback to v2.5.0, got yaml:\n%s", string(yamlData))
|
||||
}
|
||||
})
|
||||
|
||||
@@ -164,8 +166,8 @@ func TestComposeSmartVersionSelection(t *testing.T) {
|
||||
t.Fatalf("Expected cache fallback success, got error: %v", err)
|
||||
}
|
||||
|
||||
if !contains(yamlData, "image: test:1.5.0") {
|
||||
t.Errorf("Expected cached fallback to v1.5.0, got yaml:\n%s", string(yamlData))
|
||||
if !contains(yamlData, "image: test:2.5.0") {
|
||||
t.Errorf("Expected cached fallback to v2.5.0, got yaml:\n%s", string(yamlData))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -121,6 +121,8 @@ func (s *Service) ClearEvents(dryrun bool, ip string) (int, error) {
|
||||
}
|
||||
|
||||
func (s *Service) StartRetentionWorker(ctx context.Context) {
|
||||
log.Println("[Event] Worker started.")
|
||||
|
||||
// Wake up every hour to check retention
|
||||
ticker := time.NewTicker(1 * time.Hour)
|
||||
defer ticker.Stop()
|
||||
@@ -128,7 +130,7 @@ func (s *Service) StartRetentionWorker(ctx context.Context) {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
log.Println("[Retention] worker stopped")
|
||||
log.Println("[Event] Worker stopped.")
|
||||
return
|
||||
case <-ticker.C:
|
||||
archiveStr, _ := s.store.GetConfigValue("auto_archive_days")
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"sort"
|
||||
"time"
|
||||
"fmt"
|
||||
|
||||
"github.com/honeywire/hub/internal/catalog"
|
||||
@@ -51,7 +50,7 @@ func NewService(store Store, broadcaster Broadcaster, cat *catalog.Service) *Ser
|
||||
// StartWorker runs a background thread that periodically refreshes the catalog
|
||||
// and recalculates the node sync states to instantly flag updates natively.
|
||||
func (s *Service) StartWorker(ctx context.Context) {
|
||||
log.Println("[INFO] Starting node sync background worker...")
|
||||
log.Println("[Node] Worker started.")
|
||||
|
||||
if s.catalog != nil {
|
||||
s.catalog.SetOnChangeHook(func() {
|
||||
@@ -64,26 +63,8 @@ func (s *Service) StartWorker(ctx context.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
ticker := time.NewTicker(5 * time.Minute)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
log.Println("[INFO] Node sync worker stopped")
|
||||
return
|
||||
case <-ticker.C:
|
||||
if s.catalog != nil {
|
||||
s.catalog.RefreshIndex()
|
||||
}
|
||||
nodes, err := s.store.GetNodes()
|
||||
if err == nil {
|
||||
for _, n := range nodes {
|
||||
s.evaluateNodeSyncState(n.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
<-ctx.Done()
|
||||
log.Println("[Node] Worker stopped.")
|
||||
}
|
||||
|
||||
func (s *Service) CreateNode(alias string, tags []string) (string, string, error) {
|
||||
|
||||
@@ -67,7 +67,7 @@ func TestGetNodeDetailsStrictHashMatch(t *testing.T) {
|
||||
"id": "hw-sensor-test",
|
||||
"latest": "v2.0.0", // Latest version available is v2.0.0
|
||||
"versions": []map[string]interface{}{
|
||||
{"v": "v2.0.0", "min_hub_version": "v1.0.0"},
|
||||
{"v": "v2.0.0"},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -89,7 +89,7 @@ func TestGetNodeDetailsStrictHashMatch(t *testing.T) {
|
||||
InstalledSensors: []models.NodeSensor{
|
||||
{
|
||||
ID: "hw-sensor-test",
|
||||
DeployedVersion: "v1.0.0", // Node currently has v1.0.0
|
||||
DeployedVersion: "", // Empty so it auto-selects the latest
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -97,7 +97,7 @@ func TestGetNodeDetailsStrictHashMatch(t *testing.T) {
|
||||
svc := node.NewService(store, &MockNodeBroadcaster{}, catSvc)
|
||||
|
||||
// Call GetNodeDetails - ActiveRevision ("old_revision_hash") DOES NOT match newly generated hash!
|
||||
_, err := svc.GetNodeDetails("node-1")
|
||||
nodeData, err := svc.GetNodeDetails("node-1")
|
||||
if err != nil {
|
||||
t.Fatalf("GetNodeDetails failed: %v", err)
|
||||
}
|
||||
@@ -111,14 +111,9 @@ func TestGetNodeDetailsStrictHashMatch(t *testing.T) {
|
||||
newHash := node.GenerateRevisionHash(store.nodes["node-1"].InstalledSensors, catSvc, models.HubVersion)
|
||||
store.nodes["node-1"].ActiveRevision = newHash
|
||||
|
||||
_, err = svc.GetNodeDetails("node-1")
|
||||
if err != nil {
|
||||
t.Fatalf("GetNodeDetails failed: %v", err)
|
||||
}
|
||||
|
||||
// Because hashes NOW MATCH perfectly, it MUST auto-bump the DeployedVersion
|
||||
if val, ok := store.DeployedUpdates["node-1:hw-sensor-test"]; !ok || val != "v2.0.0" {
|
||||
t.Fatalf("Expected DeployedVersion to auto-bump to v2.0.0 upon valid hash match, got: %v", val)
|
||||
// GetNodeDetails should correctly return UpdateAvailable flag.
|
||||
if len(nodeData.InstalledSensors) > 0 && !nodeData.InstalledSensors[0].UpdateAvailable {
|
||||
t.Fatalf("Expected UpdateAvailable to be true")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -81,7 +81,7 @@ func (s *Service) ProcessHeartbeat(nodeID, sensorID string, metadata map[string]
|
||||
}
|
||||
|
||||
func (s *Service) StartHealthMonitor(ctx context.Context) {
|
||||
log.Println("[INFO] Starting background health monitor...")
|
||||
log.Println("[Sensor] Worker started.")
|
||||
|
||||
tickerPeriod := 30 * time.Second
|
||||
ticker := time.NewTicker(tickerPeriod)
|
||||
@@ -93,7 +93,7 @@ func (s *Service) StartHealthMonitor(ctx context.Context) {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
log.Println("[INFO] Health monitor stopped")
|
||||
log.Println("[Sensor] Worker stopped.")
|
||||
return
|
||||
case t := <-ticker.C:
|
||||
offlineThreshold := 60 * time.Second
|
||||
|
||||
@@ -166,7 +166,7 @@ func (s *Service) StartWorker(ctx context.Context) {
|
||||
s.wg.Add(1)
|
||||
go func() {
|
||||
defer s.wg.Done()
|
||||
log.Println("[SIEM] Worker started. Listening for telemetry streams...")
|
||||
log.Println("[SIEM] Worker started.")
|
||||
|
||||
// Initialize the stateful session object
|
||||
sess := &streamSession{
|
||||
|
||||
@@ -140,7 +140,8 @@ export const useFleetStore = defineStore('fleet', () => {
|
||||
const selectedSensorId = computed(() => state.value.selectedSensorId)
|
||||
const activeTimeframe = computed(() => state.value.activeTimeframe)
|
||||
const uptimeData = computed(() => state.value.uptimeData)
|
||||
const manifests = computed(() => state.value.manifests)
|
||||
const DEFAULT_SENSOR_ICON = 'M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z M3.27 6.96L12 12.01L20.73 6.96 M12 22.08V12'
|
||||
const manifests = computed(() => state.value.manifests.map(m => ({ ...m, icon_svg: m.icon_svg || DEFAULT_SENSOR_ICON })))
|
||||
const pendingNodeActions = computed(() => state.value.pendingNodeActions)
|
||||
const pendingSensorActions = computed(() => state.value.pendingSensorActions)
|
||||
|
||||
@@ -191,7 +192,7 @@ export const useFleetStore = defineStore('fleet', () => {
|
||||
|
||||
const enrichedNodes = computed(() => {
|
||||
const manifestMap = new Map()
|
||||
for (const s of state.value.manifests) {
|
||||
for (const s of manifests.value) {
|
||||
manifestMap.set(s.id, s)
|
||||
manifestMap.set(s.sensorId, s)
|
||||
manifestMap.set(s.name, s)
|
||||
@@ -205,7 +206,7 @@ export const useFleetStore = defineStore('fleet', () => {
|
||||
return {
|
||||
...sensor,
|
||||
display: manifest?.name || sensor.display || sensor.name || '',
|
||||
icon: manifest?.icon_svg || sensor.metadata?.icon || '',
|
||||
icon: manifest?.icon_svg || sensor.metadata?.icon || DEFAULT_SENSOR_ICON,
|
||||
osi: manifest?.osi_layer || sensor.metadata?.osi || 'Other',
|
||||
status: (node.status === 'down' && sensor.status === 'pending') ? 'down' : sensor.status
|
||||
}
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/andreicscs/HoneyWire/releases">
|
||||
<img src="https://img.shields.io/badge/release-v1.1.1-blue.svg?style=flat-square" alt="Latest Release" />
|
||||
<img src="https://img.shields.io/badge/release-v2.0.0-blue.svg?style=flat-square" alt="Latest Release" />
|
||||
</a>
|
||||
<a href="LICENSE">
|
||||
<img src="https://img.shields.io/badge/license-GPLv3-blue.svg?style=flat-square" alt="License: GPLv3" />
|
||||
<img src="https://img.shields.io/badge/license-AGPLv3-blue.svg?style=flat-square" alt="License: GPLv3" />
|
||||
</a>
|
||||
<a href="https://news.risky.biz/risky-bulletin-nist-gives-up-enriching-most-cves/#:~:text=New%20tool%E2%80%94HoneyWire%3A%20Andrea%20Termine">
|
||||
<img src="https://img.shields.io/badge/Risky%20Bulletin-New%20Tools-2E8B57?logo=RiskyBusiness&style=flat-square" alt="Risky Bulletin" />
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
{
|
||||
"id": "hw-sensor-file-canary",
|
||||
"version": "1.0.0",
|
||||
"version": "2.0.0",
|
||||
"schema_version": "1.0",
|
||||
"min_hub_version": "1.0.0",
|
||||
"min_wizard_version": "1.0.0",
|
||||
"name": "File Canary (FIM)",
|
||||
"category": "file",
|
||||
"osi_layer": "Host Level",
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
{
|
||||
"id": "hw-sensor-icmp-canary",
|
||||
"version": "1.0.0",
|
||||
"version": "2.0.0",
|
||||
"schema_version": "1.0",
|
||||
"min_hub_version": "1.0.0",
|
||||
"min_wizard_version": "1.0.0",
|
||||
"name": "ICMP Canary",
|
||||
"category": "network",
|
||||
"osi_layer": "Network Layer",
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
{
|
||||
"id": "hw-sensor-network-scan-detector",
|
||||
"version": "1.0.0",
|
||||
"version": "2.0.0",
|
||||
"schema_version": "1.0",
|
||||
"min_hub_version": "1.0.0",
|
||||
"min_wizard_version": "1.0.0",
|
||||
"name": "Network Scan Detector",
|
||||
"category": "network",
|
||||
"osi_layer": "Network Layer",
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
{
|
||||
"id": "hw-sensor-tcp-tarpit",
|
||||
"version": "1.0.0",
|
||||
"version": "2.0.0",
|
||||
"schema_version": "1.0",
|
||||
"min_hub_version": "1.0.0",
|
||||
"min_wizard_version": "1.0.0",
|
||||
"name": "TCP Tarpit (Credential Trap)",
|
||||
"category": "network",
|
||||
"osi_layer": "Network Layer",
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
{
|
||||
"id": "hw-sensor-web-router-decoy",
|
||||
"version": "1.0.0",
|
||||
"version": "2.0.0",
|
||||
"schema_version": "1.0",
|
||||
"min_hub_version": "1.0.0",
|
||||
"min_wizard_version": "1.0.0",
|
||||
"name": "Web Router Decoy",
|
||||
"category": "web",
|
||||
"osi_layer": "Application Layer",
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
[
|
||||
{
|
||||
"id": "hw-sensor-tcp-tarpit",
|
||||
"version": "1.0.0",
|
||||
"version": "2.0.0",
|
||||
"schema_version": "1.0",
|
||||
"min_hub_version": "1.0.0",
|
||||
"min_wizard_version": "1.0.0",
|
||||
"name": "TCP Tarpit (Credential Trap)",
|
||||
"category": "network",
|
||||
"osi_layer": "Network Layer",
|
||||
@@ -139,10 +137,8 @@
|
||||
},
|
||||
{
|
||||
"id": "hw-sensor-web-router-decoy",
|
||||
"version": "1.0.0",
|
||||
"version": "2.0.0",
|
||||
"schema_version": "1.0",
|
||||
"min_hub_version": "1.0.0",
|
||||
"min_wizard_version": "1.0.0",
|
||||
"name": "Web Router Decoy",
|
||||
"category": "web",
|
||||
"osi_layer": "Application Layer",
|
||||
@@ -262,10 +258,8 @@
|
||||
},
|
||||
{
|
||||
"id": "hw-sensor-file-canary",
|
||||
"version": "1.0.0",
|
||||
"version": "2.0.0",
|
||||
"schema_version": "1.0",
|
||||
"min_hub_version": "1.0.0",
|
||||
"min_wizard_version": "1.0.0",
|
||||
"name": "File Canary (FIM)",
|
||||
"category": "file",
|
||||
"osi_layer": "Host Level",
|
||||
@@ -412,10 +406,8 @@
|
||||
},
|
||||
{
|
||||
"id": "hw-sensor-icmp-canary",
|
||||
"version": "1.0.0",
|
||||
"version": "2.0.0",
|
||||
"schema_version": "1.0",
|
||||
"min_hub_version": "1.0.0",
|
||||
"min_wizard_version": "1.0.0",
|
||||
"name": "ICMP Canary",
|
||||
"category": "network",
|
||||
"osi_layer": "Network Layer",
|
||||
@@ -503,10 +495,8 @@
|
||||
},
|
||||
{
|
||||
"id": "hw-sensor-network-scan-detector",
|
||||
"version": "1.0.0",
|
||||
"version": "2.0.0",
|
||||
"schema_version": "1.0",
|
||||
"min_hub_version": "1.0.0",
|
||||
"min_wizard_version": "1.0.0",
|
||||
"name": "Network Scan Detector",
|
||||
"category": "network",
|
||||
"osi_layer": "Network Layer",
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
{
|
||||
"id": "hw-sensor-custom-template",
|
||||
"version": "1.0.0",
|
||||
"version": "2.0.0",
|
||||
"schema_version": "1.0",
|
||||
"min_hub_api": "1",
|
||||
"min_wizard_version": "1.0.0",
|
||||
"name": "Custom Go Sensor",
|
||||
"category": "custom",
|
||||
"osi_layer": "Application Layer",
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
{
|
||||
"id": "hw-sensor-python-template",
|
||||
"version": "1.0.0",
|
||||
"version": "2.0.0",
|
||||
"schema_version": "1.0",
|
||||
"min_hub_api": "1",
|
||||
"min_wizard_version": "1.0.0",
|
||||
"name": "Custom Python Sensor",
|
||||
"category": "custom",
|
||||
"osi_layer": "Application Layer",
|
||||
|
||||
@@ -61,8 +61,7 @@ for FILE in "${MANIFEST_FILES[@]}"; do
|
||||
name: .name,
|
||||
category: .category,
|
||||
icon_svg: .icon_svg,
|
||||
version: .version,
|
||||
min_hub_version: (.min_hub_version // "1.0.0")
|
||||
version: .version
|
||||
}' "$FILE")
|
||||
|
||||
if [ -n "$ENTRY" ]; then
|
||||
@@ -92,7 +91,7 @@ echo -e "$ENTRIES" | jq -s '
|
||||
category: .[0].category,
|
||||
icon_svg: .[0].icon_svg,
|
||||
latest: .[-1].version,
|
||||
versions: [.[] | { v: .version, min_hub_version: .min_hub_version }]
|
||||
versions: [.[] | { v: .version }]
|
||||
}
|
||||
) |
|
||||
sort_by(.id)
|
||||
|
||||
+29
-15
@@ -9,12 +9,12 @@ import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
||||
"github.com/honeywire/wizard/core/schema"
|
||||
)
|
||||
|
||||
const wizardUserAgent = "HoneyWire-Wizard/2.0"
|
||||
const WizardMinHubAPI = 1
|
||||
const WizardVersion = "2.0.0"
|
||||
|
||||
type HubClient struct {
|
||||
baseURL string
|
||||
@@ -50,7 +50,7 @@ func (c *HubClient) doRequest(ctx context.Context, method, path string, body io.
|
||||
return nil, fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
req.Header.Set("User-Agent", wizardUserAgent)
|
||||
req.Header.Set("X-Wizard-Min-Hub-Api", fmt.Sprintf("%d", WizardMinHubAPI))
|
||||
req.Header.Set("X-Wizard-Version", WizardVersion)
|
||||
for k, v := range headers {
|
||||
req.Header.Set(k, v)
|
||||
}
|
||||
@@ -66,6 +66,20 @@ func readBody(resp *http.Response) ([]byte, error) {
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func checkStatus(resp *http.Response, expected ...int) error {
|
||||
for _, e := range expected {
|
||||
if resp.StatusCode == e {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
msg := readBodyTruncated(resp)
|
||||
if resp.StatusCode == http.StatusUpgradeRequired {
|
||||
return fmt.Errorf("[VERSION MISMATCH] %s\n Please update your Hub or use a compatible Wizard version.", msg)
|
||||
}
|
||||
return fmt.Errorf("hub returned error (HTTP %d): %s", resp.StatusCode, msg)
|
||||
}
|
||||
|
||||
func readBodyTruncated(resp *http.Response) string {
|
||||
data, err := readBody(resp)
|
||||
if err != nil {
|
||||
@@ -92,8 +106,8 @@ func (c *HubClient) AuthenticateDashboard(ctx context.Context, password string)
|
||||
return "", fmt.Errorf("network error: %w", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", fmt.Errorf("hub rejected credentials (HTTP %d): %s", resp.StatusCode, readBodyTruncated(resp))
|
||||
if err := checkStatus(resp, http.StatusOK); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var cookieValue string
|
||||
@@ -128,8 +142,8 @@ func (c *HubClient) CreateNode(ctx context.Context, alias string, tags []string,
|
||||
return "", fmt.Errorf("network error: %w", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
|
||||
return "", fmt.Errorf("hub rejected request (HTTP %d): %s", resp.StatusCode, readBodyTruncated(resp))
|
||||
if err := checkStatus(resp, http.StatusOK, http.StatusCreated); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
data, err := readBody(resp)
|
||||
@@ -166,8 +180,8 @@ func (c *HubClient) GetCurrentNode(ctx context.Context, apiKey string) (*NodeInf
|
||||
return nil, fmt.Errorf("network error: %w", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("API key rejected (HTTP %d): %s", resp.StatusCode, readBodyTruncated(resp))
|
||||
if err := checkStatus(resp, http.StatusOK); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
data, err := readBody(resp)
|
||||
@@ -203,8 +217,8 @@ func (c *HubClient) AddSensor(ctx context.Context, nodeID, cookie, sensorID, cus
|
||||
return fmt.Errorf("network error adding sensor %s: %w", sensorID, err)
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusConflict {
|
||||
return fmt.Errorf("hub rejected sensor %s (HTTP %d): %s", sensorID, resp.StatusCode, readBodyTruncated(resp))
|
||||
if err := checkStatus(resp, http.StatusOK, http.StatusCreated, http.StatusConflict); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -218,8 +232,8 @@ func (c *HubClient) FetchCompose(ctx context.Context, apiKey string) ([]byte, er
|
||||
return nil, fmt.Errorf("network error: %w", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("hub returned error (HTTP %d): %s", resp.StatusCode, readBodyTruncated(resp))
|
||||
if err := checkStatus(resp, http.StatusOK); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return readBody(resp)
|
||||
@@ -277,8 +291,8 @@ func (c *HubClient) FetchManifests(ctx context.Context, apiKey string) ([]*schem
|
||||
return nil, fmt.Errorf("network error: %w", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("hub returned error (HTTP %d): %s", resp.StatusCode, readBodyTruncated(resp))
|
||||
if err := checkStatus(resp, http.StatusOK); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
data, err := readBody(resp)
|
||||
|
||||
@@ -4,8 +4,6 @@ type SensorManifest struct {
|
||||
ID string `json:"id"`
|
||||
Version string `json:"version"`
|
||||
SchemaVersion string `json:"schema_version"`
|
||||
MinHubVersion string `json:"min_hub_version"`
|
||||
MinWizardVersion string `json:"min_wizard_version"`
|
||||
Name string `json:"name"`
|
||||
Category string `json:"category"`
|
||||
OSILayer string `json:"osi_layer"`
|
||||
|
||||
@@ -18,6 +18,10 @@ func HandleStatus() error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
|
||||
defer cancel()
|
||||
|
||||
// Explicitly fetch manifests to force the Hub to refresh its catalog cache.
|
||||
// This ensures the status command calculates the latest 'UpdateAvailable' flags accurately.
|
||||
_, _ = app.Hub.FetchManifests(ctx, app.Config.APIKey)
|
||||
|
||||
nodeInfo, err := app.Hub.GetCurrentNode(ctx, app.Config.APIKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to resolve node identity: %w", err)
|
||||
|
||||
Reference in New Issue
Block a user