mirror of
https://github.com/andreicscs/HoneyWire
synced 2026-06-26 12:39:53 +00:00
implemented phase 2 of versioning
This commit is contained in:
+1
-1
@@ -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
|
||||
|
||||
|
||||
@@ -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).*
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
|
||||
@@ -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"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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 ---
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user