Add PostgreSQL reporting schema and ingest contract
This commit is contained in:
81
infra/postgres/001_ocsentinel.sql
Normal file
81
infra/postgres/001_ocsentinel.sql
Normal file
@@ -0,0 +1,81 @@
|
||||
-- OfficeCom Sentinel central reporting store.
|
||||
-- Apply once as a PostgreSQL administrator to the dedicated ocsentinel database.
|
||||
|
||||
BEGIN;
|
||||
|
||||
CREATE SCHEMA IF NOT EXISTS ocsentinel;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ocsentinel.device (
|
||||
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
machine_name TEXT NOT NULL,
|
||||
machine_name_key TEXT NOT NULL UNIQUE,
|
||||
first_seen_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
last_seen_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
last_client_version TEXT NOT NULL DEFAULT ''
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ocsentinel.scan_report (
|
||||
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
device_id BIGINT NOT NULL REFERENCES ocsentinel.device(id) ON DELETE CASCADE,
|
||||
generated_at_utc TIMESTAMPTZ NOT NULL,
|
||||
received_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
client_version TEXT NOT NULL,
|
||||
alert_state TEXT NOT NULL CHECK (alert_state IN ('ok', 'warning', 'critical', 'unknown')),
|
||||
base_alert_state TEXT NOT NULL CHECK (base_alert_state IN ('ok', 'warning', 'critical', 'unknown')),
|
||||
total_events INTEGER NOT NULL CHECK (total_events >= 0),
|
||||
unique_ip_count INTEGER NOT NULL CHECK (unique_ip_count >= 0),
|
||||
cve_total INTEGER NOT NULL DEFAULT 0 CHECK (cve_total >= 0),
|
||||
cve_critical INTEGER NOT NULL DEFAULT 0 CHECK (cve_critical >= 0),
|
||||
payload_sha256 CHAR(64) NOT NULL,
|
||||
payload JSONB NOT NULL,
|
||||
UNIQUE (device_id, generated_at_utc, payload_sha256)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_ocsentinel_scan_report_device_received
|
||||
ON ocsentinel.scan_report (device_id, received_at DESC);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_ocsentinel_scan_report_alert_received
|
||||
ON ocsentinel.scan_report (alert_state, received_at DESC);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ocsentinel.ingest_nonce (
|
||||
nonce CHAR(32) PRIMARY KEY,
|
||||
device_id BIGINT NOT NULL REFERENCES ocsentinel.device(id) ON DELETE CASCADE,
|
||||
received_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
expires_at TIMESTAMPTZ NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_ocsentinel_ingest_nonce_expires
|
||||
ON ocsentinel.ingest_nonce (expires_at);
|
||||
|
||||
CREATE OR REPLACE VIEW ocsentinel.current_device_status AS
|
||||
SELECT DISTINCT ON (d.id)
|
||||
d.machine_name,
|
||||
d.first_seen_at,
|
||||
d.last_seen_at,
|
||||
d.last_client_version,
|
||||
r.generated_at_utc,
|
||||
r.received_at,
|
||||
r.alert_state,
|
||||
r.base_alert_state,
|
||||
r.total_events,
|
||||
r.unique_ip_count,
|
||||
r.cve_total,
|
||||
r.cve_critical,
|
||||
r.payload
|
||||
FROM ocsentinel.device AS d
|
||||
LEFT JOIN ocsentinel.scan_report AS r ON r.device_id = d.id
|
||||
ORDER BY d.id, r.generated_at_utc DESC NULLS LAST, r.received_at DESC NULLS LAST;
|
||||
|
||||
CREATE OR REPLACE VIEW ocsentinel.organization_summary AS
|
||||
SELECT
|
||||
count(*) FILTER (WHERE generated_at_utc IS NOT NULL) AS devices_reporting,
|
||||
count(*) FILTER (WHERE alert_state = 'warning') AS devices_warning,
|
||||
count(*) FILTER (WHERE alert_state = 'critical') AS devices_critical,
|
||||
coalesce(sum(total_events), 0) AS total_events,
|
||||
coalesce(sum(unique_ip_count), 0) AS total_unique_ips,
|
||||
coalesce(sum(cve_total), 0) AS total_cves,
|
||||
coalesce(sum(cve_critical), 0) AS critical_cves,
|
||||
max(received_at) AS last_report_received_at
|
||||
FROM ocsentinel.current_device_status;
|
||||
|
||||
COMMIT;
|
||||
Reference in New Issue
Block a user