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:
AndReicscs
2026-06-17 11:49:18 +00:00
parent 0ea36027c7
commit 3c760c5942
31 changed files with 133 additions and 156 deletions
+10 -1
View File
@@ -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:
-3
View File
@@ -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.
+6 -2
View File
@@ -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.
-5
View File
@@ -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)
+6 -6
View File
@@ -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
}
}
+4 -4
View File
@@ -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)
+8 -8
View File
@@ -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
}
}
-2
View File
@@ -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"`
+2
View File
@@ -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()
+7 -7
View File
@@ -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)
}
}
+21 -19
View File
@@ -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))
}
})
}
+3 -1
View File
@@ -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")
+3 -22
View File
@@ -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) {
+6 -11
View File
@@ -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")
}
}
+2 -2
View File
@@ -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
+1 -1
View File
@@ -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{
+4 -3
View File
@@ -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
}
+2 -2
View File
@@ -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 -3
View File
@@ -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 -3
View File
@@ -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 -3
View File
@@ -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",
+5 -15
View File
@@ -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",
+1 -1
View File
@@ -1 +1 @@
1.1.1
2.0.0
+2 -3
View File
@@ -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
View File
@@ -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)
-2
View File
@@ -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"`
+4
View File
@@ -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)