WIP: Save current state - security subsystems, migrations, logging

This commit is contained in:
Fimeg
2025-12-16 14:19:59 -05:00
parent f792ab23c7
commit f7c8d23c5d
89 changed files with 8884 additions and 1394 deletions

View File

@@ -0,0 +1,34 @@
-- migration 018: Create scanner_config table for user-configurable scanner timeouts
-- This enables admin users to adjust scanner timeouts per subsystem via web UI
CREATE TABLE IF NOT EXISTS scanner_config (
scanner_name VARCHAR(50) PRIMARY KEY,
timeout_ms BIGINT NOT NULL, -- Timeout in milliseconds
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
CHECK (timeout_ms > 0 AND timeout_ms <= 7200000) -- Max 2 hours (7200000ms)
);
COMMENT ON TABLE scanner_config IS 'Stores user-configurable scanner timeout values';
COMMENT ON COLUMN scanner_config.scanner_name IS 'Name of the scanner (dnf, apt, docker, etc.)';
COMMENT ON COLUMN scanner_config.timeout_ms IS 'Timeout in milliseconds (1s = 1000ms)';
COMMENT ON COLUMN scanner_config.updated_at IS 'When this configuration was last modified';
-- Create index on updated_at for efficient querying of recently changed configs
CREATE INDEX IF NOT EXISTS idx_scanner_config_updated_at ON scanner_config(updated_at);
-- Insert default timeout values for all scanners
-- 30 minutes (1800000ms) is the new default for package scanners
INSERT INTO scanner_config (scanner_name, timeout_ms) VALUES
('system', 10000), -- 10 seconds for system metrics
('storage', 10000), -- 10 seconds for storage scan
('apt', 1800000), -- 30 minutes for APT
('dnf', 1800000), -- 30 minutes for DNF
('docker', 60000), -- 60 seconds for Docker
('windows', 600000), -- 10 minutes for Windows Updates
('winget', 120000), -- 2 minutes for Winget
('updates', 30000) -- 30 seconds for virtual update subsystem
ON CONFLICT (scanner_name) DO NOTHING;
-- Grant permissions
GRANT SELECT, INSERT, UPDATE, DELETE ON scanner_config TO redflag_user;

View File

@@ -0,0 +1,39 @@
-- Migration: Create system_events table for unified event logging
-- Reference: docs/ERROR_FLOW_AUDIT.md
CREATE TABLE IF NOT EXISTS system_events (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
agent_id UUID REFERENCES agents(id) ON DELETE CASCADE,
event_type VARCHAR(50) NOT NULL, -- 'agent_update', 'agent_startup', 'agent_scan', 'server_build', etc.
event_subtype VARCHAR(50) NOT NULL, -- 'success', 'failed', 'info', 'warning', 'critical'
severity VARCHAR(20) NOT NULL, -- 'info', 'warning', 'error', 'critical'
component VARCHAR(50) NOT NULL, -- 'agent', 'server', 'build', 'download', 'config', etc.
message TEXT,
metadata JSONB DEFAULT '{}', -- Structured event data (stack traces, HTTP codes, etc.)
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Performance indexes for common query patterns
CREATE INDEX idx_system_events_agent_id ON system_events(agent_id);
CREATE INDEX idx_system_events_type_subtype ON system_events(event_type, event_subtype);
CREATE INDEX idx_system_events_created_at ON system_events(created_at DESC);
CREATE INDEX idx_system_events_severity ON system_events(severity);
CREATE INDEX idx_system_events_component ON system_events(component);
-- Composite index for agent timeline queries (agent + time range)
CREATE INDEX idx_system_events_agent_timeline ON system_events(agent_id, created_at DESC);
-- Partial index for error events (faster error dashboard queries)
CREATE INDEX idx_system_events_errors ON system_events(severity, created_at DESC)
WHERE severity IN ('error', 'critical');
-- GIN index for metadata JSONB queries (allows searching event metadata)
CREATE INDEX idx_system_events_metadata_gin ON system_events USING GIN(metadata);
-- Comment for documentation
COMMENT ON TABLE system_events IS 'Unified event logging table for all system events (agent + server)';
COMMENT ON COLUMN system_events.event_type IS 'High-level event category (e.g., agent_update, agent_startup)';
COMMENT ON COLUMN system_events.event_subtype IS 'Event outcome/status (e.g., success, failed, info, warning)';
COMMENT ON COLUMN system_events.severity IS 'Event severity level for filtering and alerting';
COMMENT ON COLUMN system_events.component IS 'System component that generated the event';
COMMENT ON COLUMN system_events.metadata IS 'JSONB field for structured event data (stack traces, HTTP codes, etc.)';

View File

@@ -0,0 +1,26 @@
-- Down Migration: Remove security features for RedFlag v0.2.x
-- Purpose: Rollback migration 020 - remove security-related tables and columns
-- Drop indexes first
DROP INDEX IF EXISTS idx_security_settings_category;
DROP INDEX IF EXISTS idx_security_settings_restart;
DROP INDEX IF EXISTS idx_security_audit_timestamp;
DROP INDEX IF EXISTS idx_security_incidents_type;
DROP INDEX IF EXISTS idx_security_incidents_severity;
DROP INDEX IF EXISTS idx_security_incidents_resolved;
DROP INDEX IF EXISTS idx_signing_keys_active;
DROP INDEX IF EXISTS idx_signing_keys_algorithm;
-- Drop check constraints
ALTER TABLE security_settings DROP CONSTRAINT IF EXISTS chk_value_type;
ALTER TABLE security_incidents DROP CONSTRAINT IF EXISTS chk_incident_severity;
ALTER TABLE signing_keys DROP CONSTRAINT IF EXISTS chk_algorithm;
-- Drop tables in reverse order to avoid foreign key constraints
DROP TABLE IF EXISTS signing_keys;
DROP TABLE IF EXISTS security_incidents;
DROP TABLE IF EXISTS security_settings_audit;
DROP TABLE IF EXISTS security_settings;
-- Remove signature column from agent_commands table
ALTER TABLE agent_commands DROP COLUMN IF EXISTS signature;

View File

@@ -0,0 +1,106 @@
-- Migration: Add security features for RedFlag v0.2.x
-- Purpose: Add command signatures, security settings, audit trail, incidents tracking, and signing keys
-- Add signature column to agent_commands table
ALTER TABLE agent_commands ADD COLUMN IF NOT EXISTS signature VARCHAR(128);
-- Create security_settings table for user-configurable settings
CREATE TABLE IF NOT EXISTS security_settings (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
category VARCHAR(50) NOT NULL,
key VARCHAR(100) NOT NULL,
value JSONB NOT NULL,
value_type VARCHAR(20) NOT NULL,
requires_restart BOOLEAN DEFAULT false,
updated_at TIMESTAMP DEFAULT NOW(),
updated_by UUID REFERENCES users(id),
is_encrypted BOOLEAN DEFAULT false,
description TEXT,
validation_rules JSONB,
UNIQUE(category, key)
);
-- Create security_settings_audit table for audit trail
CREATE TABLE IF NOT EXISTS security_settings_audit (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
setting_id UUID REFERENCES security_settings(id),
previous_value JSONB,
new_value JSONB,
changed_by UUID REFERENCES users(id),
changed_at TIMESTAMP DEFAULT NOW(),
ip_address INET,
user_agent TEXT,
reason TEXT
);
-- Create security_incidents table for tracking security events
CREATE TABLE IF NOT EXISTS security_incidents (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
incident_type VARCHAR(50) NOT NULL,
severity VARCHAR(20) NOT NULL,
agent_id UUID REFERENCES agents(id),
description TEXT NOT NULL,
metadata JSONB,
resolved BOOLEAN DEFAULT false,
resolved_at TIMESTAMP,
resolved_by UUID REFERENCES users(id),
created_at TIMESTAMP DEFAULT NOW()
);
-- Create signing_keys table for public key rotation
CREATE TABLE IF NOT EXISTS signing_keys (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
key_id VARCHAR(64) UNIQUE NOT NULL,
public_key TEXT NOT NULL,
algorithm VARCHAR(20) DEFAULT 'ed25519',
is_active BOOLEAN DEFAULT true,
is_primary BOOLEAN DEFAULT false,
created_at TIMESTAMP DEFAULT NOW(),
deprecated_at TIMESTAMP,
version INTEGER DEFAULT 1
);
-- Create indexes for security_settings
CREATE INDEX IF NOT EXISTS idx_security_settings_category ON security_settings(category);
CREATE INDEX IF NOT EXISTS idx_security_settings_restart ON security_settings(requires_restart);
-- Create indexes for security_settings_audit
CREATE INDEX IF NOT EXISTS idx_security_audit_timestamp ON security_settings_audit(changed_at DESC);
-- Create indexes for security_incidents
CREATE INDEX IF NOT EXISTS idx_security_incidents_type ON security_incidents(incident_type);
CREATE INDEX IF NOT EXISTS idx_security_incidents_severity ON security_incidents(severity);
CREATE INDEX IF NOT EXISTS idx_security_incidents_resolved ON security_incidents(resolved);
-- Create indexes for signing_keys
CREATE INDEX IF NOT EXISTS idx_signing_keys_active ON signing_keys(is_active, is_primary);
CREATE INDEX IF NOT EXISTS idx_signing_keys_algorithm ON signing_keys(algorithm);
-- Add comments for documentation
COMMENT ON TABLE security_settings IS 'Stores user-configurable security settings for the RedFlag system';
COMMENT ON TABLE security_settings_audit IS 'Audit trail for all changes to security settings';
COMMENT ON TABLE security_incidents IS 'Tracks security incidents and events in the system';
COMMENT ON TABLE signing_keys IS 'Stores public signing keys with support for key rotation';
COMMENT ON COLUMN agent_commands.signature IS 'Digital signature of the command for verification';
COMMENT ON COLUMN security_settings.is_encrypted IS 'Indicates if the setting value should be encrypted at rest';
COMMENT ON COLUMN security_settings.validation_rules IS 'JSON schema for validating the setting value';
COMMENT ON COLUMN security_settings_audit.ip_address IS 'IP address of the user who made the change';
COMMENT ON COLUMN security_settings_audit.reason IS 'Optional reason for the configuration change';
COMMENT ON COLUMN security_incidents.metadata IS 'Additional structured data about the incident';
COMMENT ON COLUMN signing_keys.key_id IS 'Unique identifier for the signing key (e.g., fingerprint)';
COMMENT ON COLUMN signing_keys.version IS 'Version number for tracking key iterations';
-- Add check constraints for data integrity
ALTER TABLE security_settings ADD CONSTRAINT chk_value_type CHECK (value_type IN ('string', 'number', 'boolean', 'array', 'object'));
ALTER TABLE security_incidents ADD CONSTRAINT chk_incident_severity CHECK (severity IN ('low', 'medium', 'high', 'critical'));
ALTER TABLE signing_keys ADD CONSTRAINT chk_algorithm CHECK (algorithm IN ('ed25519', 'rsa', 'ecdsa', 'rsa-pss'));
-- Grant permissions (adjust as needed for your setup)
-- GRANT ALL PRIVILEGES ON TABLE security_settings TO redflag_user;
-- GRANT ALL PRIVILEGES ON TABLE security_settings_audit TO redflag_user;
-- GRANT ALL PRIVILEGES ON TABLE security_incidents TO redflag_user;
-- GRANT ALL PRIVILEGES ON TABLE signing_keys TO redflag_user;
-- GRANT USAGE ON SCHEMA public TO redflag_user;

View File

@@ -22,9 +22,9 @@ func NewCommandQueries(db *sqlx.DB) *CommandQueries {
func (q *CommandQueries) CreateCommand(cmd *models.AgentCommand) error {
query := `
INSERT INTO agent_commands (
id, agent_id, command_type, params, status, source, retried_from_id
id, agent_id, command_type, params, status, source, signature, retried_from_id
) VALUES (
:id, :agent_id, :command_type, :params, :status, :source, :retried_from_id
:id, :agent_id, :command_type, :params, :status, :source, :signature, :retried_from_id
)
`
_, err := q.db.NamedExec(query, cmd)
@@ -200,6 +200,7 @@ func (q *CommandQueries) GetActiveCommands() ([]models.ActiveCommandInfo, error)
c.params,
c.status,
c.source,
c.signature,
c.created_at,
c.sent_at,
c.result,
@@ -262,6 +263,7 @@ func (q *CommandQueries) GetRecentCommands(limit int) ([]models.ActiveCommandInf
c.command_type,
c.status,
c.source,
c.signature,
c.created_at,
c.sent_at,
c.completed_at,

View File

@@ -116,7 +116,7 @@ func (q *RegistrationTokenQueries) MarkTokenUsed(token string, agentID uuid.UUID
return nil
}
// GetActiveRegistrationTokens returns all active tokens
// GetActiveRegistrationTokens returns all active tokens that haven't expired
func (q *RegistrationTokenQueries) GetActiveRegistrationTokens() ([]RegistrationToken, error) {
var tokens []RegistrationToken
query := `
@@ -124,7 +124,7 @@ func (q *RegistrationTokenQueries) GetActiveRegistrationTokens() ([]Registration
revoked, revoked_at, revoked_reason, status, created_by, metadata,
max_seats, seats_used
FROM registration_tokens
WHERE status = 'active'
WHERE status = 'active' AND expires_at > NOW()
ORDER BY created_at DESC
`

View File

@@ -1,123 +0,0 @@
package queries
import (
"time"
"github.com/Fimeg/RedFlag/aggregator-server/internal/models"
"github.com/google/uuid"
"github.com/jmoiron/sqlx"
"golang.org/x/crypto/bcrypt"
)
type UserQueries struct {
db *sqlx.DB
}
func NewUserQueries(db *sqlx.DB) *UserQueries {
return &UserQueries{db: db}
}
// CreateUser inserts a new user into the database with password hashing
func (q *UserQueries) CreateUser(username, email, password, role string) (*models.User, error) {
// Hash the password
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return nil, err
}
user := &models.User{
ID: uuid.New(),
Username: username,
Email: email,
PasswordHash: string(hashedPassword),
Role: role,
CreatedAt: time.Now().UTC(),
}
query := `
INSERT INTO users (
id, username, email, password_hash, role, created_at
) VALUES (
:id, :username, :email, :password_hash, :role, :created_at
)
RETURNING *
`
rows, err := q.db.NamedQuery(query, user)
if err != nil {
return nil, err
}
defer rows.Close()
if rows.Next() {
if err := rows.StructScan(user); err != nil {
return nil, err
}
return user, nil
}
return nil, nil
}
// GetUserByUsername retrieves a user by username
func (q *UserQueries) GetUserByUsername(username string) (*models.User, error) {
var user models.User
query := `SELECT * FROM users WHERE username = $1`
err := q.db.Get(&user, query, username)
if err != nil {
return nil, err
}
return &user, nil
}
// VerifyCredentials checks if the provided username and password are valid
func (q *UserQueries) VerifyCredentials(username, password string) (*models.User, error) {
user, err := q.GetUserByUsername(username)
if err != nil {
return nil, err
}
// Compare the provided password with the stored hash
err = bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(password))
if err != nil {
return nil, err // Invalid password
}
// Update last login time
q.UpdateLastLogin(user.ID)
// Don't return password hash
user.PasswordHash = ""
return user, nil
}
// UpdateLastLogin updates the user's last login timestamp
func (q *UserQueries) UpdateLastLogin(id uuid.UUID) error {
query := `UPDATE users SET last_login = $1 WHERE id = $2`
_, err := q.db.Exec(query, time.Now().UTC(), id)
return err
}
// GetUserByID retrieves a user by ID
func (q *UserQueries) GetUserByID(id uuid.UUID) (*models.User, error) {
var user models.User
query := `SELECT id, username, email, role, created_at, last_login FROM users WHERE id = $1`
err := q.db.Get(&user, query, id)
if err != nil {
return nil, err
}
return &user, nil
}
// EnsureAdminUser creates an admin user if one doesn't exist
func (q *UserQueries) EnsureAdminUser(username, email, password string) error {
// Check if admin user already exists
existingUser, err := q.GetUserByUsername(username)
if err == nil && existingUser != nil {
return nil // Admin user already exists
}
// Create admin user
_, err = q.CreateUser(username, email, password, "admin")
return err
}