diff --git a/Docs/architecture/README.md b/Docs/architecture/README.md index a59d927..6ef8a85 100644 --- a/Docs/architecture/README.md +++ b/Docs/architecture/README.md @@ -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: diff --git a/Docs/architecture/dataContracts.md b/Docs/architecture/dataContracts.md index 160cb75..b5cf6f5 100644 --- a/Docs/architecture/dataContracts.md +++ b/Docs/architecture/dataContracts.md @@ -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. diff --git a/Docs/development/maintainer-workflow.md b/Docs/development/maintainer-workflow.md index dfa92ad..877838d 100644 --- a/Docs/development/maintainer-workflow.md +++ b/Docs/development/maintainer-workflow.md @@ -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. diff --git a/Hub/cmd/hub/main.go b/Hub/cmd/hub/main.go index 736d64d..8aba676 100644 --- a/Hub/cmd/hub/main.go +++ b/Hub/cmd/hub/main.go @@ -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) diff --git a/Hub/internal/api/middleware.go b/Hub/internal/api/middleware.go index 316ebd1..6e15443 100644 --- a/Hub/internal/api/middleware.go +++ b/Hub/internal/api/middleware.go @@ -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 } } diff --git a/Hub/internal/api/middleware_test.go b/Hub/internal/api/middleware_test.go index d314e55..49b9043 100644 --- a/Hub/internal/api/middleware_test.go +++ b/Hub/internal/api/middleware_test.go @@ -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) diff --git a/Hub/internal/catalog/service.go b/Hub/internal/catalog/service.go index 711c97c..93a22e7 100644 --- a/Hub/internal/catalog/service.go +++ b/Hub/internal/catalog/service.go @@ -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 } } diff --git a/Hub/internal/models/models.go b/Hub/internal/models/models.go index 3119b7e..a7eb512 100644 --- a/Hub/internal/models/models.go +++ b/Hub/internal/models/models.go @@ -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"` diff --git a/Hub/internal/services/auth/service.go b/Hub/internal/services/auth/service.go index 867654c..a044b62 100644 --- a/Hub/internal/services/auth/service.go +++ b/Hub/internal/services/auth/service.go @@ -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() diff --git a/Hub/internal/services/compose/service.go b/Hub/internal/services/compose/service.go index d01a666..0f4aed9 100644 --- a/Hub/internal/services/compose/service.go +++ b/Hub/internal/services/compose/service.go @@ -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) } } diff --git a/Hub/internal/services/compose/service_test.go b/Hub/internal/services/compose/service_test.go index d505c84..f0303ed 100644 --- a/Hub/internal/services/compose/service_test.go +++ b/Hub/internal/services/compose/service_test.go @@ -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)) } }) } diff --git a/Hub/internal/services/event/service.go b/Hub/internal/services/event/service.go index 464ef2f..0077baf 100644 --- a/Hub/internal/services/event/service.go +++ b/Hub/internal/services/event/service.go @@ -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") diff --git a/Hub/internal/services/node/service.go b/Hub/internal/services/node/service.go index 9debb38..fc44f92 100644 --- a/Hub/internal/services/node/service.go +++ b/Hub/internal/services/node/service.go @@ -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) { diff --git a/Hub/internal/services/node/service_test.go b/Hub/internal/services/node/service_test.go index 474151c..ef123a3 100644 --- a/Hub/internal/services/node/service_test.go +++ b/Hub/internal/services/node/service_test.go @@ -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") } } diff --git a/Hub/internal/services/sensor/service.go b/Hub/internal/services/sensor/service.go index e3bf1b4..6de5494 100644 --- a/Hub/internal/services/sensor/service.go +++ b/Hub/internal/services/sensor/service.go @@ -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 diff --git a/Hub/internal/services/siem/service.go b/Hub/internal/services/siem/service.go index 53e60c0..537bdb4 100644 --- a/Hub/internal/services/siem/service.go +++ b/Hub/internal/services/siem/service.go @@ -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{ diff --git a/Hub/ui/src/stores/Fleet/fleet.ts b/Hub/ui/src/stores/Fleet/fleet.ts index e0c70e1..6b4528c 100644 --- a/Hub/ui/src/stores/Fleet/fleet.ts +++ b/Hub/ui/src/stores/Fleet/fleet.ts @@ -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 } diff --git a/README.md b/README.md index 3ed2cac..4b4986d 100644 --- a/README.md +++ b/README.md @@ -5,10 +5,10 @@
-
+
-
+
diff --git a/Sensors/official/FileCanary/file-canary.json b/Sensors/official/FileCanary/file-canary.json
index 44411e8..88e6cae 100644
--- a/Sensors/official/FileCanary/file-canary.json
+++ b/Sensors/official/FileCanary/file-canary.json
@@ -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",
diff --git a/Sensors/official/IcmpCanary/icmp-canary.json b/Sensors/official/IcmpCanary/icmp-canary.json
index 1473157..9202d76 100644
--- a/Sensors/official/IcmpCanary/icmp-canary.json
+++ b/Sensors/official/IcmpCanary/icmp-canary.json
@@ -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",
diff --git a/Sensors/official/NetworkScanDetector/network-scan-detector.json b/Sensors/official/NetworkScanDetector/network-scan-detector.json
index 0a3fc7e..1350800 100644
--- a/Sensors/official/NetworkScanDetector/network-scan-detector.json
+++ b/Sensors/official/NetworkScanDetector/network-scan-detector.json
@@ -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",
diff --git a/Sensors/official/TcpTarpit/tcp-tarpit.json b/Sensors/official/TcpTarpit/tcp-tarpit.json
index 188f8c2..c8e4dbb 100644
--- a/Sensors/official/TcpTarpit/tcp-tarpit.json
+++ b/Sensors/official/TcpTarpit/tcp-tarpit.json
@@ -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",
diff --git a/Sensors/official/WebRouterDecoy/web-router-decoy.json b/Sensors/official/WebRouterDecoy/web-router-decoy.json
index cecdaa8..ace4e5a 100644
--- a/Sensors/official/WebRouterDecoy/web-router-decoy.json
+++ b/Sensors/official/WebRouterDecoy/web-router-decoy.json
@@ -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",
diff --git a/Sensors/official/manifests.dev.json b/Sensors/official/manifests.dev.json
index ad88f4f..07333df 100644
--- a/Sensors/official/manifests.dev.json
+++ b/Sensors/official/manifests.dev.json
@@ -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",
diff --git a/Sensors/templates/go-sensor/manifest.example.json b/Sensors/templates/go-sensor/manifest.example.json
index 13e39e7..d714db0 100644
--- a/Sensors/templates/go-sensor/manifest.example.json
+++ b/Sensors/templates/go-sensor/manifest.example.json
@@ -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",
diff --git a/Sensors/templates/python-sensor/manifest.example.json b/Sensors/templates/python-sensor/manifest.example.json
index 8470b5a..2ded580 100644
--- a/Sensors/templates/python-sensor/manifest.example.json
+++ b/Sensors/templates/python-sensor/manifest.example.json
@@ -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",
diff --git a/VERSION b/VERSION
index 8cfbc90..359a5b9 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-1.1.1
\ No newline at end of file
+2.0.0
\ No newline at end of file
diff --git a/scripts/build-registry-index.sh b/scripts/build-registry-index.sh
index 2e9a991..a9489c1 100755
--- a/scripts/build-registry-index.sh
+++ b/scripts/build-registry-index.sh
@@ -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)
diff --git a/wizard/core/api/client.go b/wizard/core/api/client.go
index 60808f9..4b6049a 100644
--- a/wizard/core/api/client.go
+++ b/wizard/core/api/client.go
@@ -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)
diff --git a/wizard/core/schema/manifest.go b/wizard/core/schema/manifest.go
index f0103f3..51b6302 100644
--- a/wizard/core/schema/manifest.go
+++ b/wizard/core/schema/manifest.go
@@ -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"`
diff --git a/wizard/internal/commands/status.go b/wizard/internal/commands/status.go
index 3d360d6..537be37 100644
--- a/wizard/internal/commands/status.go
+++ b/wizard/internal/commands/status.go
@@ -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)