158 lines
7.4 KiB
MySQL
158 lines
7.4 KiB
MySQL
|
|
-- 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()
|
||
|
|
);
|