feat: deliver Sense audits to Bell (T-016)
This commit is contained in:
@@ -26,6 +26,7 @@ const (
|
||||
defaultOrphanScanPeriod = time.Minute
|
||||
defaultONVIFMode = "disabled"
|
||||
defaultControlAuthMode = "static-sha256"
|
||||
defaultAuditRelayPeriod = time.Second
|
||||
)
|
||||
|
||||
var instanceIDPattern = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$`)
|
||||
@@ -53,6 +54,11 @@ type Config struct {
|
||||
ControlAuthFile string
|
||||
ControlCursorKeyFile string
|
||||
ControlAllowInsecureHTTP bool
|
||||
AuditRelayEnabled bool
|
||||
AuditRelayURL string
|
||||
AuditRelayKeyFile string
|
||||
AuditRelayKeyID string
|
||||
AuditRelayInterval time.Duration
|
||||
}
|
||||
|
||||
func Load() (Config, error) {
|
||||
@@ -92,6 +98,14 @@ func Load() (Config, error) {
|
||||
if err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
auditRelayEnabled, err := boolEnv("SENSE_AUDIT_RELAY_ENABLED", false)
|
||||
if err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
auditRelayInterval, err := durationEnv("SENSE_AUDIT_RELAY_INTERVAL", defaultAuditRelayPeriod)
|
||||
if err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
metricsEnabled, err := boolEnv("SENSE_METRICS_ENABLED", true)
|
||||
if err != nil {
|
||||
return Config{}, err
|
||||
@@ -130,6 +144,11 @@ func Load() (Config, error) {
|
||||
ControlAuthFile: stringEnv("SENSE_CONTROL_AUTH_FILE", ""),
|
||||
ControlCursorKeyFile: stringEnv("SENSE_CONTROL_CURSOR_KEY_FILE", ""),
|
||||
ControlAllowInsecureHTTP: controlAllowInsecure,
|
||||
AuditRelayEnabled: auditRelayEnabled,
|
||||
AuditRelayURL: stringEnv("SENSE_AUDIT_RELAY_URL", ""),
|
||||
AuditRelayKeyFile: stringEnv("SENSE_AUDIT_RELAY_KEY_FILE", ""),
|
||||
AuditRelayKeyID: stringEnv("SENSE_AUDIT_RELAY_KEY_ID", ""),
|
||||
AuditRelayInterval: auditRelayInterval,
|
||||
}
|
||||
if err := cfg.Validate(); err != nil {
|
||||
return Config{}, err
|
||||
@@ -228,6 +247,31 @@ func (c Config) Validate() error {
|
||||
return fmt.Errorf("non-loopback Control API requires SENSE_CONTROL_ALLOW_INSECURE_HTTP=true")
|
||||
}
|
||||
}
|
||||
if c.AuditRelayEnabled {
|
||||
if databaseDriver != postgresDatabaseDriver {
|
||||
return fmt.Errorf("Sense audit relay requires SENSE_DB_DRIVER=postgres")
|
||||
}
|
||||
if c.AuditRelayKeyFile == "" || !filepath.IsAbs(c.AuditRelayKeyFile) {
|
||||
return fmt.Errorf("SENSE_AUDIT_RELAY_KEY_FILE must be an absolute external path")
|
||||
}
|
||||
if !instanceIDPattern.MatchString(c.AuditRelayKeyID) {
|
||||
return fmt.Errorf("invalid SENSE_AUDIT_RELAY_KEY_ID")
|
||||
}
|
||||
if c.AuditRelayInterval < time.Second {
|
||||
return fmt.Errorf("SENSE_AUDIT_RELAY_INTERVAL must be at least 1s")
|
||||
}
|
||||
relayURL, err := url.Parse(c.AuditRelayURL)
|
||||
if err != nil || relayURL.Host == "" || relayURL.Path != "/internal/v1/audit-events:batch" ||
|
||||
relayURL.RawQuery != "" || relayURL.Fragment != "" || relayURL.User != nil {
|
||||
return fmt.Errorf("invalid SENSE_AUDIT_RELAY_URL")
|
||||
}
|
||||
relayHost := relayURL.Hostname()
|
||||
relayIP := net.ParseIP(relayHost)
|
||||
relayLoopback := relayHost == "localhost" || (relayIP != nil && relayIP.IsLoopback())
|
||||
if relayURL.Scheme != "https" && !(relayURL.Scheme == "http" && relayLoopback) {
|
||||
return fmt.Errorf("SENSE_AUDIT_RELAY_URL requires HTTPS outside loopback")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -166,3 +166,31 @@ func TestValidateControlAPINonLoopbackNeedsSeparateRiskAcceptance(t *testing.T)
|
||||
t.Fatalf("explicit non-loopback Control API risk acceptance failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateAuditRelaySecurityBoundary(t *testing.T) {
|
||||
base := Config{
|
||||
HTTPAddress: "127.0.0.1:8080", DatabaseDriver: "postgres",
|
||||
DatabaseDSN: "postgres://sense-runtime@127.0.0.1/yovision?sslmode=disable",
|
||||
MediaMTXURL: "http://127.0.0.1:9997", ReconcileInterval: time.Second, ProbeInterval: time.Second,
|
||||
AuditRelayEnabled: true, AuditRelayURL: "http://127.0.0.1:8081/internal/v1/audit-events:batch",
|
||||
AuditRelayKeyFile: filepath.Join(t.TempDir(), "relay-keys.json"), AuditRelayKeyID: "sense-a", AuditRelayInterval: time.Second,
|
||||
}
|
||||
if err := base.Validate(); err != nil {
|
||||
t.Fatalf("valid loopback relay rejected: %v", err)
|
||||
}
|
||||
remoteHTTP := base
|
||||
remoteHTTP.AuditRelayURL = "http://bell.example/internal/v1/audit-events:batch"
|
||||
if err := remoteHTTP.Validate(); err == nil {
|
||||
t.Fatal("remote plaintext relay was accepted")
|
||||
}
|
||||
sqlite := base
|
||||
sqlite.DatabaseDriver, sqlite.DatabaseDSN = "sqlite", "file:test.db"
|
||||
if err := sqlite.Validate(); err == nil {
|
||||
t.Fatal("SQLite audit relay was accepted")
|
||||
}
|
||||
relativeKey := base
|
||||
relativeKey.AuditRelayKeyFile = "relay-keys.json"
|
||||
if err := relativeKey.Validate(); err == nil {
|
||||
t.Fatal("repository-relative relay key was accepted")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user