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

181 lines
5.3 KiB
Go

package queries
import (
"context"
"database/sql"
"fmt"
"github.com/Fimeg/RedFlag/aggregator-server/internal/models"
"github.com/google/uuid"
)
// StorageMetricsQueries handles storage metrics database operations
type StorageMetricsQueries struct {
db *sql.DB
}
// NewStorageMetricsQueries creates a new storage metrics queries instance
func NewStorageMetricsQueries(db *sql.DB) *StorageMetricsQueries {
return &StorageMetricsQueries{db: db}
}
// InsertStorageMetric inserts a new storage metric
func (q *StorageMetricsQueries) InsertStorageMetric(ctx context.Context, metric models.StorageMetric) error {
query := `
INSERT INTO storage_metrics (
id, agent_id, mountpoint, device, disk_type, filesystem,
total_bytes, used_bytes, available_bytes, used_percent,
severity, metadata, created_at
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
`
_, err := q.db.ExecContext(ctx, query,
metric.ID, metric.AgentID, metric.Mountpoint, metric.Device,
metric.DiskType, metric.Filesystem, metric.TotalBytes,
metric.UsedBytes, metric.AvailableBytes, metric.UsedPercent,
metric.Severity, metric.Metadata, metric.CreatedAt,
)
if err != nil {
return fmt.Errorf("failed to insert storage metric: %w", err)
}
return nil
}
// GetStorageMetricsByAgentID retrieves storage metrics for an agent
func (q *StorageMetricsQueries) GetStorageMetricsByAgentID(ctx context.Context, agentID uuid.UUID, limit, offset int) ([]models.StorageMetric, error) {
query := `
SELECT id, agent_id, mountpoint, device, disk_type, filesystem,
total_bytes, used_bytes, available_bytes, used_percent,
severity, metadata, created_at
FROM storage_metrics
WHERE agent_id = $1
ORDER BY created_at DESC
LIMIT $2 OFFSET $3
`
rows, err := q.db.QueryContext(ctx, query, agentID, limit, offset)
if err != nil {
return nil, fmt.Errorf("failed to query storage metrics: %w", err)
}
defer rows.Close()
var metrics []models.StorageMetric
for rows.Next() {
var metric models.StorageMetric
err := rows.Scan(
&metric.ID, &metric.AgentID, &metric.Mountpoint, &metric.Device,
&metric.DiskType, &metric.Filesystem, &metric.TotalBytes,
&metric.UsedBytes, &metric.AvailableBytes, &metric.UsedPercent,
&metric.Severity, &metric.Metadata, &metric.CreatedAt,
)
if err != nil {
return nil, fmt.Errorf("failed to scan storage metric: %w", err)
}
metrics = append(metrics, metric)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("error iterating storage metrics: %w", err)
}
return metrics, nil
}
// GetLatestStorageMetrics retrieves the most recent storage metrics per mountpoint
func (q *StorageMetricsQueries) GetLatestStorageMetrics(ctx context.Context, agentID uuid.UUID) ([]models.StorageMetric, error) {
query := `
SELECT DISTINCT ON (mountpoint)
id, agent_id, mountpoint, device, disk_type, filesystem,
total_bytes, used_bytes, available_bytes, used_percent,
severity, metadata, created_at
FROM storage_metrics
WHERE agent_id = $1
ORDER BY mountpoint, created_at DESC
`
rows, err := q.db.QueryContext(ctx, query, agentID)
if err != nil {
return nil, fmt.Errorf("failed to query latest storage metrics: %w", err)
}
defer rows.Close()
var metrics []models.StorageMetric
for rows.Next() {
var metric models.StorageMetric
err := rows.Scan(
&metric.ID, &metric.AgentID, &metric.Mountpoint, &metric.Device,
&metric.DiskType, &metric.Filesystem, &metric.TotalBytes,
&metric.UsedBytes, &metric.AvailableBytes, &metric.UsedPercent,
&metric.Severity, &metric.Metadata, &metric.CreatedAt,
)
if err != nil {
return nil, fmt.Errorf("failed to scan storage metric: %w", err)
}
metrics = append(metrics, metric)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("error iterating latest storage metrics: %w", err)
}
return metrics, nil
}
// GetStorageMetricsSummary returns summary statistics for an agent
func (q *StorageMetricsQueries) GetStorageMetricsSummary(ctx context.Context, agentID uuid.UUID) (map[string]interface{}, error) {
query := `
SELECT
COUNT(*) as total_disks,
COUNT(CASE WHEN severity = 'critical' THEN 1 END) as critical_disks,
COUNT(CASE WHEN severity = 'important' THEN 1 END) as important_disks,
AVG(used_percent) as avg_used_percent,
MAX(used_percent) as max_used_percent,
MIN(created_at) as first_collected_at,
MAX(created_at) as last_collected_at
FROM storage_metrics
WHERE agent_id = $1
AND created_at >= NOW() - INTERVAL '24 hours'
`
var (
totalDisks int
criticalDisks int
importantDisks int
avgUsedPercent sql.NullFloat64
maxUsedPercent sql.NullFloat64
firstCollectedAt sql.NullTime
lastCollectedAt sql.NullTime
)
err := q.db.QueryRowContext(ctx, query, agentID).Scan(
&totalDisks,
&criticalDisks,
&importantDisks,
&avgUsedPercent,
&maxUsedPercent,
&firstCollectedAt,
&lastCollectedAt,
)
if err != nil {
return nil, fmt.Errorf("failed to get storage metrics summary: %w", err)
}
summary := map[string]interface{}{
"total_disks": totalDisks,
"critical_disks": criticalDisks,
"important_disks": importantDisks,
"avg_used_percent": avgUsedPercent.Float64,
"max_used_percent": maxUsedPercent.Float64,
"first_collected_at": firstCollectedAt.Time,
"last_collected_at": lastCollectedAt.Time,
}
return summary, nil
}