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 @@

- Latest Release + Latest Release - License: GPLv3 + License: GPLv3 Risky Bulletin 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)