-- Phishing Triage — Postgres schema (v3.0). -- -- Schema-only migration (Phase 15) — lays down the full durable schema every -- v3.0 phase reads and writes, before any service writes to it. -- -- • Campaign grouping (dedupe multiple reports of the same lure) -> campaigns -- • One row per reported/triaged ticket (Phase 15 populates this) -> reports -- • Parsed email content per report -> messages (stub, Phase 16) -- • Extracted URLs/senders/hashes per message -> indicators (stub, Phase 16) -- • Campaign verdict + recommended actions -> classifications (stub, Phase 19) -- • Proposed/approved remediation actions per campaign -> remediation_actions (stub, Phase 20) -- • Audit trail of triage/remediation events -> audit_events (stub, Phase 20) -- -- Phase 15 only populates `reports` (via the Plan 02 phishing-detector -- service). `campaigns`, `messages`, `indicators`, `classifications`, -- `remediation_actions`, and `audit_events` are laid down now as stubs so -- Phases 16-21 have their schema ready and never need a second foundation -- migration. Postgres 16 has built-in gen_random_uuid() — no extension needed. -- --------------------------------------------------------------------------- -- 1. campaigns — groups multiple reports of the same phishing lure -- --------------------------------------------------------------------------- -- Populated starting Phase 18 (campaign grouping). reports.campaign_id is -- nullable until then. CREATE TABLE IF NOT EXISTS campaigns ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), campaign_key TEXT, group_method TEXT, first_seen_at TIMESTAMPTZ, last_seen_at TIMESTAMPTZ, report_count INTEGER NOT NULL DEFAULT 0, status TEXT NOT NULL DEFAULT 'open', created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_campaigns_campaign_key ON campaigns(campaign_key); -- --------------------------------------------------------------------------- -- 2. reports — one row per reported/triaged ticket (the only table Phase 15 -- populates; fully designed here) -- --------------------------------------------------------------------------- -- ticket_id is a hard FK to tickets(id) — every report originates from an -- Autotask ticket already synced into Postgres. content_hash is the D-04 -- idempotency key (hash over title+description) the Plan 02 detector uses to -- avoid re-processing an unchanged ticket. matched_patterns captures the -- DETECT-01 pattern strings that matched. evidence captures the EVID-01 -- notes/time_entries/attachments snapshot at detection time. campaign_id is -- nullable — Phase 18's campaign-grouping logic links it later. -- uq_reports_ticket_id lets the Plan 02 detector upsert with -- ON CONFLICT (ticket_id) — one report per ticket. CREATE TABLE IF NOT EXISTS reports ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), ticket_id BIGINT NOT NULL REFERENCES tickets(id), ticket_number VARCHAR(100), company_id BIGINT, company_name VARCHAR(255), requester_contact_id BIGINT, created_by_contact_id BIGINT, title VARCHAR(255), description TEXT, matched_patterns JSONB NOT NULL DEFAULT '[]'::jsonb, content_hash TEXT NOT NULL, evidence JSONB NOT NULL DEFAULT '{}'::jsonb, campaign_id UUID REFERENCES campaigns(id), created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), CONSTRAINT uq_reports_ticket_id UNIQUE (ticket_id) ); CREATE INDEX IF NOT EXISTS idx_reports_ticket_id ON reports(ticket_id); CREATE INDEX IF NOT EXISTS idx_reports_content_hash ON reports(content_hash); CREATE INDEX IF NOT EXISTS idx_reports_campaign_id ON reports(campaign_id); -- --------------------------------------------------------------------------- -- 3. messages — parsed email content per report (stub for Phase 16) -- --------------------------------------------------------------------------- CREATE TABLE IF NOT EXISTS messages ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), report_id UUID REFERENCES reports(id), message_id TEXT, headers JSONB, urls JSONB, attachments JSONB, body_preview TEXT, raw_ref TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_messages_report_id ON messages(report_id); CREATE INDEX IF NOT EXISTS idx_messages_message_id ON messages(message_id); -- --------------------------------------------------------------------------- -- 4. indicators — extracted URLs/senders/hashes per message (stub for Phase 16) -- --------------------------------------------------------------------------- CREATE TABLE IF NOT EXISTS indicators ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), message_id UUID REFERENCES messages(id), indicator_type TEXT NOT NULL, value TEXT NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_indicators_message_id ON indicators(message_id); -- --------------------------------------------------------------------------- -- 5. classifications — campaign verdict + recommended actions (stub for Phase 19) -- --------------------------------------------------------------------------- CREATE TABLE IF NOT EXISTS classifications ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), campaign_id UUID REFERENCES campaigns(id), verdict TEXT, confidence NUMERIC, summary TEXT, reasons JSONB, recommended_actions JSONB, requires_approval BOOLEAN NOT NULL DEFAULT false, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_classifications_campaign_id ON classifications(campaign_id); -- --------------------------------------------------------------------------- -- 6. remediation_actions — proposed/approved remediation per campaign (stub -- for Phase 20) -- --------------------------------------------------------------------------- CREATE TABLE IF NOT EXISTS remediation_actions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), campaign_id UUID REFERENCES campaigns(id), action_type TEXT, status TEXT NOT NULL DEFAULT 'proposed', params JSONB, approved_by TEXT, approved_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_remediation_actions_campaign_id ON remediation_actions(campaign_id); -- --------------------------------------------------------------------------- -- 7. audit_events — audit trail of triage/remediation events (stub for Phase 20) -- --------------------------------------------------------------------------- CREATE TABLE IF NOT EXISTS audit_events ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), campaign_id UUID, actor TEXT, event_type TEXT NOT NULL, payload JSONB, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() );