| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612 |
- CREATE EXTENSION IF NOT EXISTS pgcrypto;
- CREATE TYPE user_role AS ENUM (
- 'admin',
- 'operator',
- 'viewer'
- );
- CREATE TYPE agent_status AS ENUM (
- 'online',
- 'offline',
- 'busy',
- 'error'
- );
- CREATE TYPE command_status AS ENUM (
- 'pending',
- 'running',
- 'completed',
- 'failed',
- 'cancelled'
- );
- CREATE TYPE transfer_direction AS ENUM (
- 'upload',
- 'download'
- );
- CREATE TYPE actor_type_enum AS ENUM (
- 'user',
- 'agent',
- 'system'
- );
- -- USERS
- CREATE TABLE users (
- id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
- username TEXT NOT NULL UNIQUE,
- password_hash TEXT NOT NULL,
- role user_role NOT NULL DEFAULT 'viewer',
- is_active BOOLEAN NOT NULL DEFAULT TRUE,
- created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
- updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
- );
- CREATE INDEX idx_users_role
- ON users(role);
- CREATE INDEX idx_users_active
- ON users(is_active);
- -- AGENTS
- CREATE TABLE agents (
- id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
- hostname TEXT NOT NULL UNIQUE,
- token_hash TEXT NOT NULL,
- cert_fingerprint TEXT,
- os_info JSONB,
- status agent_status NOT NULL DEFAULT 'offline',
- is_active BOOLEAN NOT NULL DEFAULT TRUE,
- last_seen TIMESTAMPTZ,
- registered_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
- updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
- );
- CREATE INDEX idx_agents_status
- ON agents(status);
- CREATE INDEX idx_agents_last_seen
- ON agents(last_seen);
- CREATE INDEX idx_agents_active
- ON agents(is_active);
- CREATE INDEX idx_agents_os_info_gin
- ON agents
- USING GIN(os_info);
- -- COMMAND TYPES
- CREATE TABLE command_types (
- id SMALLSERIAL PRIMARY KEY,
- name TEXT NOT NULL UNIQUE
- );
- INSERT INTO command_types(name)
- VALUES
- ('shell'),
- ('powershell'),
- ('upload'),
- ('download'),
- ('system'),
- ('custom');
- -- COMMANDS
- CREATE TABLE commands (
- id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
- agent_id UUID NOT NULL
- REFERENCES agents(id)
- ON DELETE CASCADE,
- issued_by UUID
- REFERENCES users(id)
- ON DELETE SET NULL,
- command_type_id SMALLINT
- REFERENCES command_types(id),
- command TEXT NOT NULL,
- status command_status NOT NULL DEFAULT 'pending',
- output TEXT,
- exit_code INTEGER,
- created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
- completed_at TIMESTAMPTZ,
- updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
- CHECK (
- completed_at IS NULL
- OR completed_at >= created_at
- ),
- CHECK (
- exit_code IS NULL
- OR exit_code BETWEEN -255 AND 255
- )
- );
- CREATE INDEX idx_commands_agent
- ON commands(agent_id);
- CREATE INDEX idx_commands_issued_by
- ON commands(issued_by);
- CREATE INDEX idx_commands_status
- ON commands(status);
- CREATE INDEX idx_commands_created_at
- ON commands(created_at DESC);
- CREATE INDEX idx_commands_pending
- ON commands(status)
- WHERE status = 'pending';
- CREATE INDEX idx_commands_running
- ON commands(status)
- WHERE status = 'running';
- -- FILE TRANSFERS
- CREATE TABLE file_transfers (
- id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
- agent_id UUID
- REFERENCES agents(id)
- ON DELETE CASCADE,
- initiated_by UUID
- REFERENCES users(id)
- ON DELETE SET NULL,
- command_id UUID
- REFERENCES commands(id)
- ON DELETE SET NULL,
- direction transfer_direction NOT NULL,
- filename TEXT NOT NULL,
- sha256 CHAR(64),
- size_bytes BIGINT,
- status command_status NOT NULL DEFAULT 'pending',
- created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
- updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
- CHECK (
- size_bytes IS NULL
- OR size_bytes >= 0
- )
- );
- CREATE INDEX idx_file_transfers_agent
- ON file_transfers(agent_id);
- CREATE INDEX idx_file_transfers_status
- ON file_transfers(status);
- CREATE INDEX idx_file_transfers_sha256
- ON file_transfers(sha256);
- CREATE INDEX idx_file_transfers_created
- ON file_transfers(created_at DESC);
- -- AUDIT LOGS
- CREATE TABLE audit_logs (
- id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
- actor_id UUID,
- actor_type actor_type_enum NOT NULL,
- action TEXT NOT NULL,
- target TEXT,
- details JSONB,
- ip_address INET,
- created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
- );
- CREATE INDEX idx_audit_actor
- ON audit_logs(actor_id);
- CREATE INDEX idx_audit_action
- ON audit_logs(action);
- CREATE INDEX idx_audit_created
- ON audit_logs(created_at DESC);
- CREATE INDEX idx_audit_details_gin
- ON audit_logs
- USING GIN(details);
- -- AGENT HEARTBEATS
- CREATE TABLE agent_heartbeats (
- id BIGSERIAL PRIMARY KEY,
- agent_id UUID NOT NULL
- REFERENCES agents(id)
- ON DELETE CASCADE,
- received_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
- metadata JSONB
- );
- CREATE INDEX idx_heartbeats_agent
- ON agent_heartbeats(agent_id);
- CREATE INDEX idx_heartbeats_received
- ON agent_heartbeats(received_at DESC);
- -- USER SESSIONS
- CREATE TABLE user_sessions (
- id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
- user_id UUID NOT NULL
- REFERENCES users(id)
- ON DELETE CASCADE,
- refresh_token_hash TEXT NOT NULL,
- user_agent TEXT,
- ip_address INET,
- expires_at TIMESTAMPTZ NOT NULL,
- created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
- );
- CREATE INDEX idx_sessions_user
- ON user_sessions(user_id);
- CREATE INDEX idx_sessions_expires
- ON user_sessions(expires_at);
- -- AGENT GROUPS
- CREATE TABLE agent_groups (
- id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
- name TEXT NOT NULL UNIQUE,
- description TEXT,
- created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
- );
- CREATE TABLE agent_group_members (
- agent_id UUID NOT NULL
- REFERENCES agents(id)
- ON DELETE CASCADE,
- group_id UUID NOT NULL
- REFERENCES agent_groups(id)
- ON DELETE CASCADE,
- PRIMARY KEY (agent_id, group_id)
- );
- -- UPDATE updated_at FUNCTION
- CREATE OR REPLACE FUNCTION update_updated_at_column()
- RETURNS TRIGGER AS
- $$
- BEGIN
- NEW.updated_at = NOW();
- RETURN NEW;
- END;
- $$ LANGUAGE plpgsql;
- -- updated_at TRIGGERS
- CREATE TRIGGER trg_users_updated_at
- BEFORE UPDATE ON users
- FOR EACH ROW
- EXECUTE FUNCTION update_updated_at_column();
- CREATE TRIGGER trg_agents_updated_at
- BEFORE UPDATE ON agents
- FOR EACH ROW
- EXECUTE FUNCTION update_updated_at_column();
- CREATE TRIGGER trg_commands_updated_at
- BEFORE UPDATE ON commands
- FOR EACH ROW
- EXECUTE FUNCTION update_updated_at_column();
- CREATE TRIGGER trg_file_transfers_updated_at
- BEFORE UPDATE ON file_transfers
- FOR EACH ROW
- EXECUTE FUNCTION update_updated_at_column();
- -- COMMAND COMPLETION TRIGGER
- CREATE OR REPLACE FUNCTION update_agent_after_command()
- RETURNS TRIGGER AS
- $$
- BEGIN
- IF NEW.status = 'completed'
- AND OLD.status IS DISTINCT FROM 'completed'
- THEN
- UPDATE agents
- SET
- last_seen = NOW(),
- status = 'online'
- WHERE id = NEW.agent_id;
- END IF;
- RETURN NEW;
- END;
- $$ LANGUAGE plpgsql;
- CREATE TRIGGER trg_update_agent_after_command
- AFTER UPDATE ON commands
- FOR EACH ROW
- EXECUTE FUNCTION update_agent_after_command();
- -- AUDIT LOG COMMAND CREATION
- CREATE OR REPLACE FUNCTION log_command_creation()
- RETURNS TRIGGER AS
- $$
- BEGIN
- INSERT INTO audit_logs (
- actor_id,
- actor_type,
- action,
- target,
- details
- )
- VALUES (
- NEW.issued_by,
- 'user',
- 'command_created',
- NEW.id::TEXT,
- jsonb_build_object(
- 'command_length', length(NEW.command),
- 'status', NEW.status
- )
- );
- RETURN NEW;
- END;
- $$ LANGUAGE plpgsql;
- CREATE TRIGGER trg_log_command_creation
- AFTER INSERT ON commands
- FOR EACH ROW
- EXECUTE FUNCTION log_command_creation();
- -- AGENT STATUS CHANGE LOGGING
- CREATE OR REPLACE FUNCTION log_agent_status_change()
- RETURNS TRIGGER AS
- $$
- BEGIN
- IF OLD.status IS DISTINCT FROM NEW.status THEN
- INSERT INTO audit_logs (
- actor_id,
- actor_type,
- action,
- target,
- details
- )
- VALUES (
- NEW.id,
- 'agent',
- 'status_changed',
- NEW.hostname,
- jsonb_build_object(
- 'old_status', OLD.status,
- 'new_status', NEW.status
- )
- );
- END IF;
- RETURN NEW;
- END;
- $$ LANGUAGE plpgsql;
- CREATE TRIGGER trg_log_agent_status_change
- AFTER UPDATE ON agents
- FOR EACH ROW
- EXECUTE FUNCTION log_agent_status_change();
- -- IMMUTABLE AUDIT LOGS
- CREATE OR REPLACE FUNCTION prevent_audit_log_modification()
- RETURNS TRIGGER AS
- $$
- BEGIN
- RAISE EXCEPTION 'audit_logs table is immutable';
- END;
- $$ LANGUAGE plpgsql;
- CREATE TRIGGER trg_prevent_audit_update
- BEFORE UPDATE ON audit_logs
- FOR EACH ROW
- EXECUTE FUNCTION prevent_audit_log_modification();
- CREATE TRIGGER trg_prevent_audit_delete
- BEFORE DELETE ON audit_logs
- FOR EACH ROW
- EXECUTE FUNCTION prevent_audit_log_modification();
- -- VIEWS
- CREATE VIEW active_agents_view AS
- SELECT
- a.id,
- a.hostname,
- a.status,
- a.last_seen,
- a.registered_at,
- COUNT(c.id) AS total_commands,
- COUNT(*) FILTER (
- WHERE c.status = 'completed'
- ) AS completed_commands,
- COUNT(*) FILTER (
- WHERE c.status = 'pending'
- ) AS pending_commands
- FROM agents a
- LEFT JOIN commands c
- ON a.id = c.agent_id
- WHERE a.is_active = TRUE
- GROUP BY
- a.id,
- a.hostname,
- a.status,
- a.last_seen,
- a.registered_at;
- -- RECENT COMMANDS VIEW
- CREATE VIEW recent_commands_view AS
- SELECT
- c.id,
- c.command,
- c.status,
- c.created_at,
- c.completed_at,
- a.hostname AS agent_hostname,
- u.username AS issued_by
- FROM commands c
- JOIN agents a
- ON c.agent_id = a.id
- LEFT JOIN users u
- ON c.issued_by = u.id;
- -- AUDIT LOGS VIEW
- CREATE VIEW audit_logs_view AS
- SELECT
- al.id,
- al.action,
- al.target,
- al.created_at,
- al.ip_address,
- u.username AS actor_username,
- a.hostname AS agent_hostname
- FROM audit_logs al
- LEFT JOIN users u
- ON al.actor_id = u.id
- AND al.actor_type = 'user'
- LEFT JOIN agents a
- ON al.actor_id = a.id
- AND al.actor_type = 'agent';
- -- CLEANUP
- CREATE OR REPLACE PROCEDURE cleanup_old_commands(
- days_to_keep INTEGER
- )
- LANGUAGE plpgsql
- AS
- $$
- DECLARE
- deleted_rows INTEGER;
- BEGIN
- DELETE FROM commands
- WHERE status = 'completed'
- AND completed_at < NOW() - (days_to_keep || ' days')::INTERVAL;
- GET DIAGNOSTICS deleted_rows = ROW_COUNT;
- INSERT INTO audit_logs (
- actor_type,
- action,
- target,
- details
- )
- VALUES (
- 'system',
- 'cleanup_completed_commands',
- 'commands',
- jsonb_build_object(
- 'days_to_keep', days_to_keep,
- 'deleted_rows', deleted_rows
- )
- );
- END;
- $$;
- -- SYSTEM STATS
- CREATE OR REPLACE FUNCTION get_system_stats()
- RETURNS TABLE (
- total_users BIGINT,
- total_agents BIGINT,
- online_agents BIGINT,
- pending_commands BIGINT
- )
- LANGUAGE plpgsql
- AS
- $$
- BEGIN
- RETURN QUERY
- SELECT
- (SELECT COUNT(*) FROM users WHERE is_active = TRUE),
- (SELECT COUNT(*) FROM agents),
- (SELECT COUNT(*) FROM agents WHERE status = 'online'),
- (SELECT COUNT(*) FROM commands WHERE status = 'pending');
- END;
- $$;
|