Files
Redflag/aggregator-server/internal/database/queries/subsystems.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

311 lines
8.5 KiB
Go

package queries
import (
"database/sql"
"fmt"
"github.com/Fimeg/RedFlag/aggregator-server/internal/models"
"github.com/google/uuid"
"github.com/jmoiron/sqlx"
)
type SubsystemQueries struct {
db *sqlx.DB
}
func NewSubsystemQueries(db *sqlx.DB) *SubsystemQueries {
return &SubsystemQueries{db: db}
}
// GetSubsystems retrieves all subsystems for an agent
func (q *SubsystemQueries) GetSubsystems(agentID uuid.UUID) ([]models.AgentSubsystem, error) {
query := `
SELECT id, agent_id, subsystem, enabled, interval_minutes, auto_run,
last_run_at, next_run_at, created_at, updated_at
FROM agent_subsystems
WHERE agent_id = $1
ORDER BY subsystem
`
var subsystems []models.AgentSubsystem
err := q.db.Select(&subsystems, query, agentID)
if err != nil {
return nil, fmt.Errorf("failed to get subsystems: %w", err)
}
return subsystems, nil
}
// GetSubsystem retrieves a specific subsystem for an agent
func (q *SubsystemQueries) GetSubsystem(agentID uuid.UUID, subsystem string) (*models.AgentSubsystem, error) {
query := `
SELECT id, agent_id, subsystem, enabled, interval_minutes, auto_run,
last_run_at, next_run_at, created_at, updated_at
FROM agent_subsystems
WHERE agent_id = $1 AND subsystem = $2
`
var sub models.AgentSubsystem
err := q.db.Get(&sub, query, agentID, subsystem)
if err == sql.ErrNoRows {
return nil, nil
}
if err != nil {
return nil, fmt.Errorf("failed to get subsystem: %w", err)
}
return &sub, nil
}
// UpdateSubsystem updates a subsystem configuration
func (q *SubsystemQueries) UpdateSubsystem(agentID uuid.UUID, subsystem string, config models.SubsystemConfig) error {
// Build dynamic update query based on provided fields
updates := []string{}
args := []interface{}{agentID, subsystem}
argIdx := 3
if config.Enabled != nil {
updates = append(updates, fmt.Sprintf("enabled = $%d", argIdx))
args = append(args, *config.Enabled)
argIdx++
}
if config.IntervalMinutes != nil {
updates = append(updates, fmt.Sprintf("interval_minutes = $%d", argIdx))
args = append(args, *config.IntervalMinutes)
argIdx++
}
if config.AutoRun != nil {
updates = append(updates, fmt.Sprintf("auto_run = $%d", argIdx))
args = append(args, *config.AutoRun)
argIdx++
// If enabling auto_run, calculate next_run_at
if *config.AutoRun {
updates = append(updates, fmt.Sprintf("next_run_at = NOW() + INTERVAL '%d minutes'", argIdx))
}
}
if len(updates) == 0 {
return fmt.Errorf("no fields to update")
}
updates = append(updates, "updated_at = NOW()")
query := fmt.Sprintf(`
UPDATE agent_subsystems
SET %s
WHERE agent_id = $1 AND subsystem = $2
`, joinUpdates(updates))
result, err := q.db.Exec(query, args...)
if err != nil {
return fmt.Errorf("failed to update subsystem: %w", err)
}
rows, err := result.RowsAffected()
if err != nil {
return fmt.Errorf("failed to get rows affected: %w", err)
}
if rows == 0 {
return fmt.Errorf("subsystem not found")
}
return nil
}
// UpdateLastRun updates the last_run_at timestamp for a subsystem
func (q *SubsystemQueries) UpdateLastRun(agentID uuid.UUID, subsystem string) error {
query := `
UPDATE agent_subsystems
SET last_run_at = NOW(),
next_run_at = CASE
WHEN auto_run THEN NOW() + (interval_minutes || ' minutes')::INTERVAL
ELSE next_run_at
END,
updated_at = NOW()
WHERE agent_id = $1 AND subsystem = $2
`
result, err := q.db.Exec(query, agentID, subsystem)
if err != nil {
return fmt.Errorf("failed to update last run: %w", err)
}
rows, err := result.RowsAffected()
if err != nil {
return fmt.Errorf("failed to get rows affected: %w", err)
}
if rows == 0 {
return fmt.Errorf("subsystem not found")
}
return nil
}
// GetDueSubsystems retrieves all subsystems that are due to run
func (q *SubsystemQueries) GetDueSubsystems() ([]models.AgentSubsystem, error) {
query := `
SELECT id, agent_id, subsystem, enabled, interval_minutes, auto_run,
last_run_at, next_run_at, created_at, updated_at
FROM agent_subsystems
WHERE enabled = true
AND auto_run = true
AND (next_run_at IS NULL OR next_run_at <= NOW())
ORDER BY next_run_at ASC NULLS FIRST
LIMIT 1000
`
var subsystems []models.AgentSubsystem
err := q.db.Select(&subsystems, query)
if err != nil {
return nil, fmt.Errorf("failed to get due subsystems: %w", err)
}
return subsystems, nil
}
// GetSubsystemStats retrieves statistics for a subsystem
func (q *SubsystemQueries) GetSubsystemStats(agentID uuid.UUID, subsystem string) (*models.SubsystemStats, error) {
query := `
SELECT
s.subsystem,
s.enabled,
s.last_run_at,
s.next_run_at,
s.interval_minutes,
s.auto_run,
COUNT(c.id) FILTER (WHERE c.command_type = 'scan_' || s.subsystem) as run_count,
MAX(c.status) FILTER (WHERE c.command_type = 'scan_' || s.subsystem) as last_status,
MAX(al.duration_seconds) FILTER (WHERE al.action = 'scan_' || s.subsystem) as last_duration
FROM agent_subsystems s
LEFT JOIN agent_commands c ON c.agent_id = s.agent_id
LEFT JOIN agent_logs al ON al.command_id = c.id
WHERE s.agent_id = $1 AND s.subsystem = $2
GROUP BY s.subsystem, s.enabled, s.last_run_at, s.next_run_at, s.interval_minutes, s.auto_run
`
var stats models.SubsystemStats
err := q.db.Get(&stats, query, agentID, subsystem)
if err == sql.ErrNoRows {
return nil, nil
}
if err != nil {
return nil, fmt.Errorf("failed to get subsystem stats: %w", err)
}
return &stats, nil
}
// EnableSubsystem enables a subsystem
func (q *SubsystemQueries) EnableSubsystem(agentID uuid.UUID, subsystem string) error {
enabled := true
return q.UpdateSubsystem(agentID, subsystem, models.SubsystemConfig{
Enabled: &enabled,
})
}
// DisableSubsystem disables a subsystem
func (q *SubsystemQueries) DisableSubsystem(agentID uuid.UUID, subsystem string) error {
enabled := false
return q.UpdateSubsystem(agentID, subsystem, models.SubsystemConfig{
Enabled: &enabled,
})
}
// SetAutoRun enables or disables auto-run for a subsystem
func (q *SubsystemQueries) SetAutoRun(agentID uuid.UUID, subsystem string, autoRun bool) error {
return q.UpdateSubsystem(agentID, subsystem, models.SubsystemConfig{
AutoRun: &autoRun,
})
}
// SetInterval sets the interval for a subsystem
func (q *SubsystemQueries) SetInterval(agentID uuid.UUID, subsystem string, intervalMinutes int) error {
return q.UpdateSubsystem(agentID, subsystem, models.SubsystemConfig{
IntervalMinutes: &intervalMinutes,
})
}
// CreateSubsystem creates a new subsystem configuration (used for custom subsystems)
func (q *SubsystemQueries) CreateSubsystem(sub *models.AgentSubsystem) error {
query := `
INSERT INTO agent_subsystems (agent_id, subsystem, enabled, interval_minutes, auto_run, last_run_at, next_run_at)
VALUES ($1, $2, $3, $4, $5, $6, $7)
RETURNING id, created_at, updated_at
`
err := q.db.QueryRow(
query,
sub.AgentID,
sub.Subsystem,
sub.Enabled,
sub.IntervalMinutes,
sub.AutoRun,
sub.LastRunAt,
sub.NextRunAt,
).Scan(&sub.ID, &sub.CreatedAt, &sub.UpdatedAt)
if err != nil {
return fmt.Errorf("failed to create subsystem: %w", err)
}
return nil
}
// DeleteSubsystem deletes a subsystem configuration
func (q *SubsystemQueries) DeleteSubsystem(agentID uuid.UUID, subsystem string) error {
query := `
DELETE FROM agent_subsystems
WHERE agent_id = $1 AND subsystem = $2
`
result, err := q.db.Exec(query, agentID, subsystem)
if err != nil {
return fmt.Errorf("failed to delete subsystem: %w", err)
}
rows, err := result.RowsAffected()
if err != nil {
return fmt.Errorf("failed to get rows affected: %w", err)
}
if rows == 0 {
return fmt.Errorf("subsystem not found")
}
return nil
}
// CreateDefaultSubsystems creates default subsystems for a new agent
func (q *SubsystemQueries) CreateDefaultSubsystems(agentID uuid.UUID) error {
defaults := []models.AgentSubsystem{
{AgentID: agentID, Subsystem: "updates", Enabled: true, AutoRun: true, IntervalMinutes: 60},
{AgentID: agentID, Subsystem: "storage", Enabled: true, AutoRun: true, IntervalMinutes: 5},
{AgentID: agentID, Subsystem: "system", Enabled: true, AutoRun: true, IntervalMinutes: 5},
{AgentID: agentID, Subsystem: "docker", Enabled: true, AutoRun: true, IntervalMinutes: 15},
}
for _, sub := range defaults {
if err := q.CreateSubsystem(&sub); err != nil {
return fmt.Errorf("failed to create subsystem %s: %w", sub.Subsystem, err)
}
}
return nil
}
// Helper function to join update statements
func joinUpdates(updates []string) string {
result := ""
for i, update := range updates {
if i > 0 {
result += ", "
}
result += update
}
return result
}