Files
Redflag/aggregator-server/internal/models/agent.go
jpetree331 f97d4845af feat(security): A-1 Ed25519 key rotation + A-2 replay attack fixes
Complete RedFlag codebase with two major security audit implementations.

== A-1: Ed25519 Key Rotation Support ==

Server:
- SignCommand sets SignedAt timestamp and KeyID on every signature
- signing_keys database table (migration 020) for multi-key rotation
- InitializePrimaryKey registers active key at startup
- /api/v1/public-keys endpoint for rotation-aware agents
- SigningKeyQueries for key lifecycle management

Agent:
- Key-ID-aware verification via CheckKeyRotation
- FetchAndCacheAllActiveKeys for rotation pre-caching
- Cache metadata with TTL and staleness fallback
- SecurityLogger events for key rotation and command signing

== A-2: Replay Attack Fixes (F-1 through F-7) ==

F-5 CRITICAL - RetryCommand now signs via signAndCreateCommand
F-1 HIGH     - v3 format: "{agent_id}:{cmd_id}:{type}:{hash}:{ts}"
F-7 HIGH     - Migration 026: expires_at column with partial index
F-6 HIGH     - GetPendingCommands/GetStuckCommands filter by expires_at
F-2 HIGH     - Agent-side executedIDs dedup map with cleanup
F-4 HIGH     - commandMaxAge reduced from 24h to 4h
F-3 CRITICAL - Old-format commands rejected after 48h via CreatedAt

Verification fixes: migration idempotency (ETHOS #4), log format
compliance (ETHOS #1), stale comments updated.

All 24 tests passing. Docker --no-cache build verified.
See docs/ for full audit reports and deviation log (DEV-001 to DEV-019).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 21:25:47 -04:00

182 lines
8.2 KiB
Go

package models
import (
"database/sql/driver"
"encoding/json"
"time"
"github.com/google/uuid"
)
// Agent represents a registered update agent
type Agent struct {
ID uuid.UUID `json:"id" db:"id"`
Hostname string `json:"hostname" db:"hostname"`
OSType string `json:"os_type" db:"os_type"`
OSVersion string `json:"os_version" db:"os_version"`
OSArchitecture string `json:"os_architecture" db:"os_architecture"`
AgentVersion string `json:"agent_version" db:"agent_version"` // Version at registration
CurrentVersion string `json:"current_version" db:"current_version"` // Current running version
UpdateAvailable bool `json:"update_available" db:"update_available"` // Whether update is available
LastVersionCheck time.Time `json:"last_version_check" db:"last_version_check"` // Last time version was checked
MachineID *string `json:"machine_id,omitempty" db:"machine_id"` // Unique machine identifier
PublicKeyFingerprint *string `json:"public_key_fingerprint,omitempty" db:"public_key_fingerprint"` // Public key fingerprint
IsUpdating bool `json:"is_updating" db:"is_updating"` // Whether agent is currently updating
UpdatingToVersion *string `json:"updating_to_version,omitempty" db:"updating_to_version"` // Target version for ongoing update
UpdateInitiatedAt *time.Time `json:"update_initiated_at,omitempty" db:"update_initiated_at"` // When update process started
LastSeen time.Time `json:"last_seen" db:"last_seen"`
Status string `json:"status" db:"status"`
Metadata JSONB `json:"metadata" db:"metadata"`
RebootRequired bool `json:"reboot_required" db:"reboot_required"`
LastRebootAt *time.Time `json:"last_reboot_at,omitempty" db:"last_reboot_at"`
RebootReason *string `json:"reboot_reason,omitempty" db:"reboot_reason"`
CreatedAt time.Time `json:"created_at" db:"created_at"`
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
}
// AgentWithLastScan extends Agent with last scan information
type AgentWithLastScan struct {
ID uuid.UUID `json:"id" db:"id"`
Hostname string `json:"hostname" db:"hostname"`
OSType string `json:"os_type" db:"os_type"`
OSVersion string `json:"os_version" db:"os_version"`
OSArchitecture string `json:"os_architecture" db:"os_architecture"`
AgentVersion string `json:"agent_version" db:"agent_version"` // Version at registration
CurrentVersion string `json:"current_version" db:"current_version"` // Current running version
UpdateAvailable bool `json:"update_available" db:"update_available"` // Whether update is available
LastVersionCheck time.Time `json:"last_version_check" db:"last_version_check"` // Last time version was checked
MachineID *string `json:"machine_id,omitempty" db:"machine_id"` // Unique machine identifier
PublicKeyFingerprint *string `json:"public_key_fingerprint,omitempty" db:"public_key_fingerprint"` // Public key fingerprint
IsUpdating bool `json:"is_updating" db:"is_updating"` // Whether agent is currently updating
UpdatingToVersion *string `json:"updating_to_version,omitempty" db:"updating_to_version"` // Target version for ongoing update
UpdateInitiatedAt *time.Time `json:"update_initiated_at,omitempty" db:"update_initiated_at"` // When update process started
LastSeen time.Time `json:"last_seen" db:"last_seen"`
Status string `json:"status" db:"status"`
Metadata JSONB `json:"metadata" db:"metadata"`
RebootRequired bool `json:"reboot_required" db:"reboot_required"`
LastRebootAt *time.Time `json:"last_reboot_at,omitempty" db:"last_reboot_at"`
RebootReason *string `json:"reboot_reason,omitempty" db:"reboot_reason"`
CreatedAt time.Time `json:"created_at" db:"created_at"`
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
LastScan *time.Time `json:"last_scan" db:"last_scan"`
}
// AgentSpecs represents system specifications for an agent
type AgentSpecs struct {
ID uuid.UUID `json:"id" db:"id"`
AgentID uuid.UUID `json:"agent_id" db:"agent_id"`
CPUModel string `json:"cpu_model" db:"cpu_model"`
CPUCores int `json:"cpu_cores" db:"cpu_cores"`
MemoryTotalMB int `json:"memory_total_mb" db:"memory_total_mb"`
DiskTotalGB int `json:"disk_total_gb" db:"disk_total_gb"`
DiskFreeGB int `json:"disk_free_gb" db:"disk_free_gb"`
NetworkInterfaces JSONB `json:"network_interfaces" db:"network_interfaces"`
DockerInstalled bool `json:"docker_installed" db:"docker_installed"`
DockerVersion string `json:"docker_version" db:"docker_version"`
PackageManagers StringArray `json:"package_managers" db:"package_managers"`
CollectedAt time.Time `json:"collected_at" db:"collected_at"`
}
// AgentRegistrationRequest is the payload for agent registration
type AgentRegistrationRequest struct {
Hostname string `json:"hostname" binding:"required"`
OSType string `json:"os_type" binding:"required"`
OSVersion string `json:"os_version"`
OSArchitecture string `json:"os_architecture"`
AgentVersion string `json:"agent_version" binding:"required"`
RegistrationToken string `json:"registration_token"` // Optional, for fallback method
MachineID string `json:"machine_id"` // Unique machine identifier
PublicKeyFingerprint string `json:"public_key_fingerprint"` // Embedded public key fingerprint
Metadata map[string]string `json:"metadata"`
}
// AgentRegistrationResponse is returned after successful registration
type AgentRegistrationResponse struct {
AgentID uuid.UUID `json:"agent_id"`
Token string `json:"token"` // Short-lived access token (24h)
RefreshToken string `json:"refresh_token"` // Long-lived refresh token (90d)
Config map[string]interface{} `json:"config"`
}
// TokenRenewalRequest is the payload for token renewal using refresh token
type TokenRenewalRequest struct {
AgentID uuid.UUID `json:"agent_id" binding:"required"`
RefreshToken string `json:"refresh_token" binding:"required"`
AgentVersion string `json:"agent_version,omitempty"` // Optional: agent's current version for upgrade tracking
}
// TokenRenewalResponse is returned after successful token renewal
type TokenRenewalResponse struct {
Token string `json:"token"` // New short-lived access token (24h)
}
// UTCTime is a time.Time that marshals to ISO format with UTC timezone
type UTCTime time.Time
// MarshalJSON implements json.Marshaler for UTCTime
func (t UTCTime) MarshalJSON() ([]byte, error) {
return json.Marshal(time.Time(t).UTC().Format("2006-01-02T15:04:05.000Z"))
}
// UnmarshalJSON implements json.Unmarshaler for UTCTime
func (t *UTCTime) UnmarshalJSON(data []byte) error {
var s string
if err := json.Unmarshal(data, &s); err != nil {
return err
}
parsed, err := time.Parse("2006-01-02T15:04:05.000Z", s)
if err != nil {
return err
}
*t = UTCTime(parsed)
return nil
}
// JSONB type for PostgreSQL JSONB columns
type JSONB map[string]interface{}
// Value implements driver.Valuer for database storage
func (j JSONB) Value() (driver.Value, error) {
if j == nil {
return nil, nil
}
return json.Marshal(j)
}
// Scan implements sql.Scanner for database retrieval
func (j *JSONB) Scan(value interface{}) error {
if value == nil {
*j = nil
return nil
}
bytes, ok := value.([]byte)
if !ok {
return nil
}
return json.Unmarshal(bytes, j)
}
// StringArray type for PostgreSQL text[] columns
type StringArray []string
// Value implements driver.Valuer
func (s StringArray) Value() (driver.Value, error) {
if s == nil {
return nil, nil
}
return json.Marshal(s)
}
// Scan implements sql.Scanner
func (s *StringArray) Scan(value interface{}) error {
if value == nil {
*s = nil
return nil
}
bytes, ok := value.([]byte)
if !ok {
return nil
}
return json.Unmarshal(bytes, s)
}