main.sql 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612
  1. CREATE EXTENSION IF NOT EXISTS pgcrypto;
  2. CREATE TYPE user_role AS ENUM (
  3. 'admin',
  4. 'operator',
  5. 'viewer'
  6. );
  7. CREATE TYPE agent_status AS ENUM (
  8. 'online',
  9. 'offline',
  10. 'busy',
  11. 'error'
  12. );
  13. CREATE TYPE command_status AS ENUM (
  14. 'pending',
  15. 'running',
  16. 'completed',
  17. 'failed',
  18. 'cancelled'
  19. );
  20. CREATE TYPE transfer_direction AS ENUM (
  21. 'upload',
  22. 'download'
  23. );
  24. CREATE TYPE actor_type_enum AS ENUM (
  25. 'user',
  26. 'agent',
  27. 'system'
  28. );
  29. -- USERS
  30. CREATE TABLE users (
  31. id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  32. username TEXT NOT NULL UNIQUE,
  33. password_hash TEXT NOT NULL,
  34. role user_role NOT NULL DEFAULT 'viewer',
  35. is_active BOOLEAN NOT NULL DEFAULT TRUE,
  36. created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  37. updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
  38. );
  39. CREATE INDEX idx_users_role
  40. ON users(role);
  41. CREATE INDEX idx_users_active
  42. ON users(is_active);
  43. -- AGENTS
  44. CREATE TABLE agents (
  45. id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  46. hostname TEXT NOT NULL UNIQUE,
  47. token_hash TEXT NOT NULL,
  48. cert_fingerprint TEXT,
  49. os_info JSONB,
  50. status agent_status NOT NULL DEFAULT 'offline',
  51. is_active BOOLEAN NOT NULL DEFAULT TRUE,
  52. last_seen TIMESTAMPTZ,
  53. registered_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  54. updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
  55. );
  56. CREATE INDEX idx_agents_status
  57. ON agents(status);
  58. CREATE INDEX idx_agents_last_seen
  59. ON agents(last_seen);
  60. CREATE INDEX idx_agents_active
  61. ON agents(is_active);
  62. CREATE INDEX idx_agents_os_info_gin
  63. ON agents
  64. USING GIN(os_info);
  65. -- COMMAND TYPES
  66. CREATE TABLE command_types (
  67. id SMALLSERIAL PRIMARY KEY,
  68. name TEXT NOT NULL UNIQUE
  69. );
  70. INSERT INTO command_types(name)
  71. VALUES
  72. ('shell'),
  73. ('powershell'),
  74. ('upload'),
  75. ('download'),
  76. ('system'),
  77. ('custom');
  78. -- COMMANDS
  79. CREATE TABLE commands (
  80. id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  81. agent_id UUID NOT NULL
  82. REFERENCES agents(id)
  83. ON DELETE CASCADE,
  84. issued_by UUID
  85. REFERENCES users(id)
  86. ON DELETE SET NULL,
  87. command_type_id SMALLINT
  88. REFERENCES command_types(id),
  89. command TEXT NOT NULL,
  90. status command_status NOT NULL DEFAULT 'pending',
  91. output TEXT,
  92. exit_code INTEGER,
  93. created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  94. completed_at TIMESTAMPTZ,
  95. updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  96. CHECK (
  97. completed_at IS NULL
  98. OR completed_at >= created_at
  99. ),
  100. CHECK (
  101. exit_code IS NULL
  102. OR exit_code BETWEEN -255 AND 255
  103. )
  104. );
  105. CREATE INDEX idx_commands_agent
  106. ON commands(agent_id);
  107. CREATE INDEX idx_commands_issued_by
  108. ON commands(issued_by);
  109. CREATE INDEX idx_commands_status
  110. ON commands(status);
  111. CREATE INDEX idx_commands_created_at
  112. ON commands(created_at DESC);
  113. CREATE INDEX idx_commands_pending
  114. ON commands(status)
  115. WHERE status = 'pending';
  116. CREATE INDEX idx_commands_running
  117. ON commands(status)
  118. WHERE status = 'running';
  119. -- FILE TRANSFERS
  120. CREATE TABLE file_transfers (
  121. id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  122. agent_id UUID
  123. REFERENCES agents(id)
  124. ON DELETE CASCADE,
  125. initiated_by UUID
  126. REFERENCES users(id)
  127. ON DELETE SET NULL,
  128. command_id UUID
  129. REFERENCES commands(id)
  130. ON DELETE SET NULL,
  131. direction transfer_direction NOT NULL,
  132. filename TEXT NOT NULL,
  133. sha256 CHAR(64),
  134. size_bytes BIGINT,
  135. status command_status NOT NULL DEFAULT 'pending',
  136. created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  137. updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  138. CHECK (
  139. size_bytes IS NULL
  140. OR size_bytes >= 0
  141. )
  142. );
  143. CREATE INDEX idx_file_transfers_agent
  144. ON file_transfers(agent_id);
  145. CREATE INDEX idx_file_transfers_status
  146. ON file_transfers(status);
  147. CREATE INDEX idx_file_transfers_sha256
  148. ON file_transfers(sha256);
  149. CREATE INDEX idx_file_transfers_created
  150. ON file_transfers(created_at DESC);
  151. -- AUDIT LOGS
  152. CREATE TABLE audit_logs (
  153. id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  154. actor_id UUID,
  155. actor_type actor_type_enum NOT NULL,
  156. action TEXT NOT NULL,
  157. target TEXT,
  158. details JSONB,
  159. ip_address INET,
  160. created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
  161. );
  162. CREATE INDEX idx_audit_actor
  163. ON audit_logs(actor_id);
  164. CREATE INDEX idx_audit_action
  165. ON audit_logs(action);
  166. CREATE INDEX idx_audit_created
  167. ON audit_logs(created_at DESC);
  168. CREATE INDEX idx_audit_details_gin
  169. ON audit_logs
  170. USING GIN(details);
  171. -- AGENT HEARTBEATS
  172. CREATE TABLE agent_heartbeats (
  173. id BIGSERIAL PRIMARY KEY,
  174. agent_id UUID NOT NULL
  175. REFERENCES agents(id)
  176. ON DELETE CASCADE,
  177. received_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  178. metadata JSONB
  179. );
  180. CREATE INDEX idx_heartbeats_agent
  181. ON agent_heartbeats(agent_id);
  182. CREATE INDEX idx_heartbeats_received
  183. ON agent_heartbeats(received_at DESC);
  184. -- USER SESSIONS
  185. CREATE TABLE user_sessions (
  186. id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  187. user_id UUID NOT NULL
  188. REFERENCES users(id)
  189. ON DELETE CASCADE,
  190. refresh_token_hash TEXT NOT NULL,
  191. user_agent TEXT,
  192. ip_address INET,
  193. expires_at TIMESTAMPTZ NOT NULL,
  194. created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
  195. );
  196. CREATE INDEX idx_sessions_user
  197. ON user_sessions(user_id);
  198. CREATE INDEX idx_sessions_expires
  199. ON user_sessions(expires_at);
  200. -- AGENT GROUPS
  201. CREATE TABLE agent_groups (
  202. id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  203. name TEXT NOT NULL UNIQUE,
  204. description TEXT,
  205. created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
  206. );
  207. CREATE TABLE agent_group_members (
  208. agent_id UUID NOT NULL
  209. REFERENCES agents(id)
  210. ON DELETE CASCADE,
  211. group_id UUID NOT NULL
  212. REFERENCES agent_groups(id)
  213. ON DELETE CASCADE,
  214. PRIMARY KEY (agent_id, group_id)
  215. );
  216. -- UPDATE updated_at FUNCTION
  217. CREATE OR REPLACE FUNCTION update_updated_at_column()
  218. RETURNS TRIGGER AS
  219. $$
  220. BEGIN
  221. NEW.updated_at = NOW();
  222. RETURN NEW;
  223. END;
  224. $$ LANGUAGE plpgsql;
  225. -- updated_at TRIGGERS
  226. CREATE TRIGGER trg_users_updated_at
  227. BEFORE UPDATE ON users
  228. FOR EACH ROW
  229. EXECUTE FUNCTION update_updated_at_column();
  230. CREATE TRIGGER trg_agents_updated_at
  231. BEFORE UPDATE ON agents
  232. FOR EACH ROW
  233. EXECUTE FUNCTION update_updated_at_column();
  234. CREATE TRIGGER trg_commands_updated_at
  235. BEFORE UPDATE ON commands
  236. FOR EACH ROW
  237. EXECUTE FUNCTION update_updated_at_column();
  238. CREATE TRIGGER trg_file_transfers_updated_at
  239. BEFORE UPDATE ON file_transfers
  240. FOR EACH ROW
  241. EXECUTE FUNCTION update_updated_at_column();
  242. -- COMMAND COMPLETION TRIGGER
  243. CREATE OR REPLACE FUNCTION update_agent_after_command()
  244. RETURNS TRIGGER AS
  245. $$
  246. BEGIN
  247. IF NEW.status = 'completed'
  248. AND OLD.status IS DISTINCT FROM 'completed'
  249. THEN
  250. UPDATE agents
  251. SET
  252. last_seen = NOW(),
  253. status = 'online'
  254. WHERE id = NEW.agent_id;
  255. END IF;
  256. RETURN NEW;
  257. END;
  258. $$ LANGUAGE plpgsql;
  259. CREATE TRIGGER trg_update_agent_after_command
  260. AFTER UPDATE ON commands
  261. FOR EACH ROW
  262. EXECUTE FUNCTION update_agent_after_command();
  263. -- AUDIT LOG COMMAND CREATION
  264. CREATE OR REPLACE FUNCTION log_command_creation()
  265. RETURNS TRIGGER AS
  266. $$
  267. BEGIN
  268. INSERT INTO audit_logs (
  269. actor_id,
  270. actor_type,
  271. action,
  272. target,
  273. details
  274. )
  275. VALUES (
  276. NEW.issued_by,
  277. 'user',
  278. 'command_created',
  279. NEW.id::TEXT,
  280. jsonb_build_object(
  281. 'command_length', length(NEW.command),
  282. 'status', NEW.status
  283. )
  284. );
  285. RETURN NEW;
  286. END;
  287. $$ LANGUAGE plpgsql;
  288. CREATE TRIGGER trg_log_command_creation
  289. AFTER INSERT ON commands
  290. FOR EACH ROW
  291. EXECUTE FUNCTION log_command_creation();
  292. -- AGENT STATUS CHANGE LOGGING
  293. CREATE OR REPLACE FUNCTION log_agent_status_change()
  294. RETURNS TRIGGER AS
  295. $$
  296. BEGIN
  297. IF OLD.status IS DISTINCT FROM NEW.status THEN
  298. INSERT INTO audit_logs (
  299. actor_id,
  300. actor_type,
  301. action,
  302. target,
  303. details
  304. )
  305. VALUES (
  306. NEW.id,
  307. 'agent',
  308. 'status_changed',
  309. NEW.hostname,
  310. jsonb_build_object(
  311. 'old_status', OLD.status,
  312. 'new_status', NEW.status
  313. )
  314. );
  315. END IF;
  316. RETURN NEW;
  317. END;
  318. $$ LANGUAGE plpgsql;
  319. CREATE TRIGGER trg_log_agent_status_change
  320. AFTER UPDATE ON agents
  321. FOR EACH ROW
  322. EXECUTE FUNCTION log_agent_status_change();
  323. -- IMMUTABLE AUDIT LOGS
  324. CREATE OR REPLACE FUNCTION prevent_audit_log_modification()
  325. RETURNS TRIGGER AS
  326. $$
  327. BEGIN
  328. RAISE EXCEPTION 'audit_logs table is immutable';
  329. END;
  330. $$ LANGUAGE plpgsql;
  331. CREATE TRIGGER trg_prevent_audit_update
  332. BEFORE UPDATE ON audit_logs
  333. FOR EACH ROW
  334. EXECUTE FUNCTION prevent_audit_log_modification();
  335. CREATE TRIGGER trg_prevent_audit_delete
  336. BEFORE DELETE ON audit_logs
  337. FOR EACH ROW
  338. EXECUTE FUNCTION prevent_audit_log_modification();
  339. -- VIEWS
  340. CREATE VIEW active_agents_view AS
  341. SELECT
  342. a.id,
  343. a.hostname,
  344. a.status,
  345. a.last_seen,
  346. a.registered_at,
  347. COUNT(c.id) AS total_commands,
  348. COUNT(*) FILTER (
  349. WHERE c.status = 'completed'
  350. ) AS completed_commands,
  351. COUNT(*) FILTER (
  352. WHERE c.status = 'pending'
  353. ) AS pending_commands
  354. FROM agents a
  355. LEFT JOIN commands c
  356. ON a.id = c.agent_id
  357. WHERE a.is_active = TRUE
  358. GROUP BY
  359. a.id,
  360. a.hostname,
  361. a.status,
  362. a.last_seen,
  363. a.registered_at;
  364. -- RECENT COMMANDS VIEW
  365. CREATE VIEW recent_commands_view AS
  366. SELECT
  367. c.id,
  368. c.command,
  369. c.status,
  370. c.created_at,
  371. c.completed_at,
  372. a.hostname AS agent_hostname,
  373. u.username AS issued_by
  374. FROM commands c
  375. JOIN agents a
  376. ON c.agent_id = a.id
  377. LEFT JOIN users u
  378. ON c.issued_by = u.id;
  379. -- AUDIT LOGS VIEW
  380. CREATE VIEW audit_logs_view AS
  381. SELECT
  382. al.id,
  383. al.action,
  384. al.target,
  385. al.created_at,
  386. al.ip_address,
  387. u.username AS actor_username,
  388. a.hostname AS agent_hostname
  389. FROM audit_logs al
  390. LEFT JOIN users u
  391. ON al.actor_id = u.id
  392. AND al.actor_type = 'user'
  393. LEFT JOIN agents a
  394. ON al.actor_id = a.id
  395. AND al.actor_type = 'agent';
  396. -- CLEANUP
  397. CREATE OR REPLACE PROCEDURE cleanup_old_commands(
  398. days_to_keep INTEGER
  399. )
  400. LANGUAGE plpgsql
  401. AS
  402. $$
  403. DECLARE
  404. deleted_rows INTEGER;
  405. BEGIN
  406. DELETE FROM commands
  407. WHERE status = 'completed'
  408. AND completed_at < NOW() - (days_to_keep || ' days')::INTERVAL;
  409. GET DIAGNOSTICS deleted_rows = ROW_COUNT;
  410. INSERT INTO audit_logs (
  411. actor_type,
  412. action,
  413. target,
  414. details
  415. )
  416. VALUES (
  417. 'system',
  418. 'cleanup_completed_commands',
  419. 'commands',
  420. jsonb_build_object(
  421. 'days_to_keep', days_to_keep,
  422. 'deleted_rows', deleted_rows
  423. )
  424. );
  425. END;
  426. $$;
  427. -- SYSTEM STATS
  428. CREATE OR REPLACE FUNCTION get_system_stats()
  429. RETURNS TABLE (
  430. total_users BIGINT,
  431. total_agents BIGINT,
  432. online_agents BIGINT,
  433. pending_commands BIGINT
  434. )
  435. LANGUAGE plpgsql
  436. AS
  437. $$
  438. BEGIN
  439. RETURN QUERY
  440. SELECT
  441. (SELECT COUNT(*) FROM users WHERE is_active = TRUE),
  442. (SELECT COUNT(*) FROM agents),
  443. (SELECT COUNT(*) FROM agents WHERE status = 'online'),
  444. (SELECT COUNT(*) FROM commands WHERE status = 'pending');
  445. END;
  446. $$;