implemented phase 2 of versioning

This commit is contained in:
AndReicscs
2026-06-15 13:37:27 +00:00
parent c813554a42
commit f89d025ae4
12 changed files with 127 additions and 46 deletions
+1 -1
View File
@@ -102,7 +102,7 @@ Common message types:
### GET /api/v1/manifests
Fetches the sensor manifest catalog from `HW_MANIFEST_URL` or the default public manifest registry.
Fetches the sensor manifest catalog from `RegistryURL` or the default public manifest registry.
### POST /api/v1/compose/generate
+1 -1
View File
@@ -67,7 +67,7 @@ Run `docker compose -f docker-compose.test-infra.yml up -d`.
Start the Hub in development mode. Leave this running in a dedicated terminal pane so you can monitor the logs for HTTP requests and compilation errors.
```bash
cd Hub
HW_ENV=development HW_PORT=8080 HW_MANIFEST_URL=http://localhost:3000/your/path/to/the/gitea/hosted/manifest.dev.json go run cmd/hub/main.go
HW_ENV=development HW_PORT=8080 go run cmd/hub/main.go
```
*(Ensure the UI is also running via `npm run dev` in `Hub/ui` if you are working on the ui, you can also use the hub's embedded ui at `http://localhost:8080`if you need to use the dashboard to provision API keys).*
+15 -2
View File
@@ -19,6 +19,19 @@ var CoreEnvVars = []string{
"HW_SEVERITY",
}
func buildImageString(repo, tag, digest string) string {
img := repo
if tag != "" {
img += ":" + tag
} else {
img += ":latest"
}
if digest != "" {
img += "@" + digest
}
return img
}
func BuildService(sensorID string, m models.SensorManifest, envMap map[string]string) (*ComposeFile, error) {
var compose ComposeFile
@@ -34,7 +47,7 @@ func BuildService(sensorID string, m models.SensorManifest, envMap map[string]st
for _, ic := range initContainers {
initSvc := &ComposeService{
Image: ic.Image,
Image: buildImageString(ic.ImageRepository, ic.ImageTag, ic.ImageDigest),
Command: ic.Command,
}
@@ -155,7 +168,7 @@ func BuildService(sensorID string, m models.SensorManifest, envMap map[string]st
}
svc := &ComposeService{
Image: m.Deployment.Image,
Image: buildImageString(m.Deployment.ImageRepository, m.Deployment.ImageTag, m.Deployment.ImageDigest),
ContainerName: containerName,
Restart: "unless-stopped",
+6 -6
View File
@@ -11,7 +11,7 @@ import (
func TestBuildService_SecurityDefaults(t *testing.T) {
manifest := models.SensorManifest{
Deployment: models.Deployment{
Image: "test-image",
ImageRepository: "test-image",
},
}
compose, err := BuildService("test-sensor", manifest, map[string]string{})
@@ -28,7 +28,7 @@ func TestBuildService_SecurityDefaults(t *testing.T) {
func TestBuildService_CapabilityFiltering(t *testing.T) {
manifest := models.SensorManifest{
Deployment: models.Deployment{
Image: "test-image",
ImageRepository: "test-image",
CapAdd: []string{"NET_RAW", "SYS_ADMIN"}, // SYS_ADMIN is not allowed
},
}
@@ -44,7 +44,7 @@ func TestBuildService_DynamicVolumeExpansion(t *testing.T) {
t.Run("Dynamic File Bind", func(t *testing.T) {
manifest := models.SensorManifest{
Deployment: models.Deployment{
Image: "test-image",
ImageRepository: "test-image",
VolumeMounts: []models.VolumeMount{
{Type: models.DynamicFileBind, SourceEnv: "HW_FILES", TargetPrefix: "/watch/"},
},
@@ -102,10 +102,10 @@ func TestBuildService_DynamicVolumeExpansion(t *testing.T) {
func TestBuildService_InitContainers(t *testing.T) {
manifest := models.SensorManifest{
Deployment: models.Deployment{
Image: "main-image",
ImageRepository: "main-image",
InitContainers: []models.InitContainer{
{Name: "z-init", Image: "init-z"},
{Name: "a-init", Image: "init-a"},
{Name: "z-init", ImageRepository: "init-z"},
{Name: "a-init", ImageRepository: "init-a"},
},
},
}
+4 -4
View File
@@ -54,8 +54,8 @@ func ValidateManifest(m models.SensorManifest) error {
}
// Interpolation Checks
if containsInterpolation(m.Deployment.Image) {
return fmt.Errorf("SECURITY REJECT: interpolation not allowed in image")
if containsInterpolation(m.Deployment.ImageRepository) || containsInterpolation(m.Deployment.ImageTag) || containsInterpolation(m.Deployment.ImageDigest) {
return fmt.Errorf("SECURITY REJECT: interpolation not allowed in image fields")
}
if containsInterpolation(m.Deployment.NetworkMode) {
return fmt.Errorf("SECURITY REJECT: interpolation not allowed in network_mode")
@@ -66,8 +66,8 @@ func ValidateManifest(m models.SensorManifest) error {
}
}
for _, ic := range m.Deployment.InitContainers {
if containsInterpolation(ic.Image) {
return fmt.Errorf("SECURITY REJECT: interpolation not allowed in init container image")
if containsInterpolation(ic.ImageRepository) || containsInterpolation(ic.ImageTag) || containsInterpolation(ic.ImageDigest) {
return fmt.Errorf("SECURITY REJECT: interpolation not allowed in init container image fields")
}
if containsInterpolation(ic.Command) {
return fmt.Errorf("SECURITY REJECT: interpolation not allowed in init container command")
@@ -46,7 +46,7 @@ func TestValidateManifest(t *testing.T) {
return models.SensorManifest{
SchemaVersion: "1.0",
Deployment: models.Deployment{
Image: "safe-image:latest",
ImageRepository: "safe-image:latest",
},
}
}
@@ -100,7 +100,7 @@ func TestValidateManifest(t *testing.T) {
{
name: "Interpolation in Main Image",
modifier: func(m *models.SensorManifest) {
m.Deployment.Image = "image:${TAG}"
m.Deployment.ImageRepository = "image:${TAG}"
},
expectError: true,
errorMsg: "interpolation not allowed in image",
@@ -168,7 +168,7 @@ func TestDecodeManifestStrict(t *testing.T) {
})
t.Run("Valid Decode", func(t *testing.T) {
goodJson := `{"schema_version": "1.0", "deployment": {"image": "test"}}`
goodJson := `{"schema_version": "1.0", "deployment": {"image_repository": "test"}}`
_, err := DecodeManifestStrict(strings.NewReader(goodJson))
assert.NoError(t, err)
})
+1
View File
@@ -10,6 +10,7 @@ type SetupPayload struct {
// ConfigPayload represents the runtime configuration of the Hub
type ConfigPayload struct {
HubEndpoint string `json:"hubEndpoint"`
RegistryURL string `json:"registryUrl"`
AutoArchiveDays int `json:"autoArchiveDays"`
AutoPurgeDays int `json:"autoPurgeDays"`
WebhookURL string `json:"webhookUrl"`
+71 -27
View File
@@ -1,13 +1,13 @@
package composesvc
import (
"bytes"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"strings"
"sync"
composeEngine "github.com/honeywire/hub/internal/compose"
"github.com/honeywire/hub/internal/compose/security"
@@ -25,10 +25,15 @@ type Store interface {
type Service struct {
store Store
cache map[string]models.SensorManifest
mu sync.RWMutex
}
func NewService(store Store) *Service {
return &Service{store: store}
return &Service{
store: store,
cache: make(map[string]models.SensorManifest),
}
}
// --- DTOs ---
@@ -47,46 +52,85 @@ type PreviewRequest struct {
// --- MANIFEST FETCHING ---
func (s *Service) FetchManifestBytes() ([]byte, error) {
manifestURL := os.Getenv("HW_MANIFEST_URL")
// --- MANIFEST FETCHING ---
if manifestURL == "" {
manifestURL = "https://raw.githubusercontent.com/andreicscs/HoneyWire/main/Sensors/official/manifests.json"
func (s *Service) FetchManifestBytes() ([]byte, error) {
manifests, err := s.fetchStrictCatalogManifests()
if err != nil {
return nil, err
}
return json.Marshal(manifests)
}
func (s *Service) fetchStrictCatalogManifests() ([]models.SensorManifest, error) {
registryURL, err := s.store.GetConfigValue("registry_url")
if err != nil || registryURL == "" {
registryURL = "https://raw.githubusercontent.com/andreicscs/HoneyWire/registry-pages"
}
resp, err := http.Get(manifestURL)
indexURL := strings.TrimRight(registryURL, "/") + "/index.json"
resp, err := http.Get(indexURL)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("manifest registry returned status %d", resp.StatusCode)
return nil, fmt.Errorf("registry returned status %d", resp.StatusCode)
}
return io.ReadAll(resp.Body)
}
func (s *Service) fetchStrictCatalogManifests() ([]models.SensorManifest, error) {
body, err := s.FetchManifestBytes()
if err != nil {
var idx struct {
Sensors []struct {
ID string `json:"id"`
Latest string `json:"latest"`
} `json:"sensors"`
}
if err := json.NewDecoder(resp.Body).Decode(&idx); err != nil {
return nil, err
}
var rawManifests []json.RawMessage
if err := json.Unmarshal(body, &rawManifests); err != nil {
return nil, err
}
var result []models.SensorManifest
var manifests []models.SensorManifest
for _, raw := range rawManifests {
manifest, err := security.DecodeManifestStrict(bytes.NewReader(raw))
if err != nil {
return nil, err
for _, sensor := range idx.Sensors {
cacheKey := sensor.ID + "-v" + sensor.Latest
s.mu.RLock()
cached, ok := s.cache[cacheKey]
s.mu.RUnlock()
if ok {
result = append(result, cached)
continue
}
manifests = append(manifests, manifest)
sensorName := strings.TrimPrefix(sensor.ID, "hw-sensor-")
manifestURL := fmt.Sprintf("%s/%s-v%s.json", strings.TrimRight(registryURL, "/"), sensorName, sensor.Latest)
mResp, fetchErr := http.Get(manifestURL)
if fetchErr != nil {
log.Printf("[WARNING] Failed to fetch %s: %v", manifestURL, fetchErr)
continue
}
if mResp.StatusCode != http.StatusOK {
mResp.Body.Close()
continue
}
manifest, decodeErr := security.DecodeManifestStrict(mResp.Body)
mResp.Body.Close()
if decodeErr != nil {
log.Printf("[WARNING] Failed to decode %s: %v", manifestURL, decodeErr)
continue
}
s.mu.Lock()
s.cache[cacheKey] = manifest
s.mu.Unlock()
result = append(result, manifest)
}
return manifests, nil
return result, nil
}
// --- GENERATION LOGIC ---
+2
View File
@@ -127,6 +127,7 @@ func (s *Service) GetConfig() (models.ConfigPayload, error) {
return models.ConfigPayload{
HubEndpoint: kv["hub_endpoint"],
RegistryURL: kv["registry_url"],
AutoArchiveDays: archiveDays,
AutoPurgeDays: purgeDays,
WebhookURL: kv["webhook_url"],
@@ -142,6 +143,7 @@ func (s *Service) UpdateConfig(req map[string]interface{}) error {
dbUpdates := make(map[string]interface{})
mapping := map[string]string{
"hubEndpoint": "hub_endpoint",
"registryUrl": "registry_url",
"autoArchiveDays": "auto_archive_days",
"autoPurgeDays": "auto_purge_days",
"webhookType": "webhook_type",
+1
View File
@@ -50,6 +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))
for k, v := range headers {
req.Header.Set(k, v)
}
+11 -1
View File
@@ -50,8 +50,18 @@ func (e *Engine) GetRecommendations(hostState *scanner.HostState, systemState *s
// Iterate through manifests and find correlated matches
for _, manifest := range e.manifests {
imageStr := manifest.Deployment.ImageRepository
if manifest.Deployment.ImageTag != "" {
imageStr += ":" + manifest.Deployment.ImageTag
} else {
imageStr += ":latest"
}
if manifest.Deployment.ImageDigest != "" {
imageStr += "@" + manifest.Deployment.ImageDigest
}
// Idempotency check: skip if this sensor is already deployed
if deployedImageSet[manifest.Deployment.Image] {
if deployedImageSet[imageStr] {
continue
}
+11 -1
View File
@@ -27,7 +27,17 @@ func PrintDeploymentPlan(recommendations []*discovery.Recommendation) {
for _, rec := range recommendations {
fmt.Printf(" %s+%s %s%s%s %s(%s)%s\n", Green, Reset, Bold, rec.SensorName, Reset, Gray, rec.SensorID, Reset)
fmt.Printf(" %sReason:%s %s\n", Cyan, Reset, rec.Reason)
fmt.Printf(" %sImage:%s %s\n", Cyan, Reset, rec.DeploymentTemplate.Image)
imageStr := rec.DeploymentTemplate.ImageRepository
if rec.DeploymentTemplate.ImageTag != "" {
imageStr += ":" + rec.DeploymentTemplate.ImageTag
} else {
imageStr += ":latest"
}
if rec.DeploymentTemplate.ImageDigest != "" {
imageStr += "@" + rec.DeploymentTemplate.ImageDigest
}
fmt.Printf(" %sImage:%s %s\n", Cyan, Reset, imageStr)
if len(rec.DeploymentTemplate.VolumeMounts) > 0 {
fmt.Printf(" %sVolume Mounts:%s\n", Cyan, Reset)