WIP: Save current state - security subsystems, migrations, logging
This commit is contained in:
@@ -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;
|
||||
@@ -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.)';
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
`
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user