-
Notifications
You must be signed in to change notification settings - Fork 139
Expand file tree
/
Copy path009_responder_pwa.sql
More file actions
187 lines (170 loc) · 8.6 KB
/
Copy path009_responder_pwa.sql
File metadata and controls
187 lines (170 loc) · 8.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
-- AiSOC Responder PWA migration (Phase 4B)
-- Adds the operational tables the mobile responder PWA needs:
--
-- passkey_credentials — WebAuthn passkeys per user (passwordless auth)
-- passkey_challenges — short-lived register/auth challenges
-- oncall_status — current on-call snapshot per user (available|busy|offline)
-- agent_approvals — agent-blocking approval requests with one-tap
-- approve/deny actions from the PWA
--
-- Plus a `snoozed_until` column on `alerts` so the queue view can hide
-- alerts the responder asked to defer for an hour without losing them.
--
-- All tables enforce tenant isolation via Row-Level Security so the
-- responder PWA respects the same boundaries as alerts/cases.
-- ============================================================
-- 0. Alerts: snooze support for the mobile triage queue
-- ============================================================
ALTER TABLE alerts
ADD COLUMN IF NOT EXISTS snoozed_until TIMESTAMPTZ;
ALTER TABLE alerts
ADD COLUMN IF NOT EXISTS snoozed_by_id UUID REFERENCES users(id) ON DELETE SET NULL;
CREATE INDEX IF NOT EXISTS idx_alerts_tenant_snoozed
ON alerts(tenant_id, snoozed_until)
WHERE snoozed_until IS NOT NULL;
-- ============================================================
-- 1. passkey_credentials (WebAuthn / FIDO2)
-- ============================================================
CREATE TABLE IF NOT EXISTS passkey_credentials (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
-- Raw WebAuthn credential ID (base64url-encoded for portability).
credential_id TEXT NOT NULL UNIQUE,
-- COSE public key, base64url-encoded.
public_key TEXT NOT NULL,
-- Anti-clone counter from the authenticator.
sign_count BIGINT NOT NULL DEFAULT 0,
-- Authenticator transports (usb, ble, nfc, internal, hybrid).
transports JSONB NOT NULL DEFAULT '[]'::jsonb,
-- Friendly name like "iPhone 15 Pro" or "YubiKey 5C NFC".
device_name VARCHAR(120) NOT NULL,
-- Optional AAGUID for telemetry / device-class display.
aaguid UUID,
-- True if the authenticator says it's a discoverable credential.
is_discoverable BOOLEAN NOT NULL DEFAULT TRUE,
last_used_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
revoked_at TIMESTAMPTZ
);
CREATE INDEX IF NOT EXISTS idx_passkeys_user
ON passkey_credentials(user_id)
WHERE revoked_at IS NULL;
CREATE INDEX IF NOT EXISTS idx_passkeys_tenant
ON passkey_credentials(tenant_id);
ALTER TABLE passkey_credentials ENABLE ROW LEVEL SECURITY;
ALTER TABLE passkey_credentials FORCE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS passkeys_tenant ON passkey_credentials;
CREATE POLICY passkeys_tenant ON passkey_credentials
USING (tenant_id = current_tenant_id() OR current_tenant_id() IS NULL);
-- ============================================================
-- 2. passkey_challenges (short-lived registration & auth challenges)
-- ============================================================
CREATE TABLE IF NOT EXISTS passkey_challenges (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
-- Challenge type: 'register' or 'authenticate'.
purpose VARCHAR(20) NOT NULL,
-- The pre-shared random challenge bytes, base64url-encoded.
challenge TEXT NOT NULL UNIQUE,
-- For registration we know the user; for authentication we may not
-- (discoverable creds), in which case both fields are NULL.
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
tenant_id UUID REFERENCES tenants(id) ON DELETE CASCADE,
-- For passwordless conditional UI flows we record the email hint.
email_hint VARCHAR(255),
-- The hostname/origin that asked for the challenge (RP ID).
rp_id VARCHAR(255) NOT NULL,
expires_at TIMESTAMPTZ NOT NULL,
consumed_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_passkey_challenges_expiry
ON passkey_challenges(expires_at);
CREATE INDEX IF NOT EXISTS idx_passkey_challenges_user
ON passkey_challenges(user_id);
-- Challenges deliberately do NOT enforce RLS: the registration/auth start
-- endpoints insert rows on behalf of unauthenticated users (auth flow) or
-- in the tenant context of the requesting user (registration flow). The
-- challenge value itself is the secret.
-- ============================================================
-- 3. oncall_status (current on-call snapshot per user)
-- ============================================================
CREATE TABLE IF NOT EXISTS oncall_status (
user_id UUID PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
-- 'available' | 'busy' | 'offline'.
status VARCHAR(20) NOT NULL DEFAULT 'offline',
-- ISO-8601 schedule reference (PagerDuty/OpsGenie/manual/etc).
schedule_ref VARCHAR(200),
-- Optional rotation handle ("primary-soc", "ir-secondary", …).
rotation VARCHAR(80),
note TEXT,
until TIMESTAMPTZ,
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_oncall_tenant_status
ON oncall_status(tenant_id, status);
ALTER TABLE oncall_status ENABLE ROW LEVEL SECURITY;
ALTER TABLE oncall_status FORCE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS oncall_tenant ON oncall_status;
CREATE POLICY oncall_tenant ON oncall_status
USING (tenant_id = current_tenant_id() OR current_tenant_id() IS NULL);
-- ============================================================
-- 4. agent_approvals (one-tap approval requests blocking agent runs)
-- ============================================================
CREATE TABLE IF NOT EXISTS agent_approvals (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
-- Optional pointers back to the originating run/case/alert.
run_id UUID REFERENCES investigation_runs(id) ON DELETE SET NULL,
case_id VARCHAR(200),
alert_id UUID,
-- Who created the approval (always the agents service).
requested_by VARCHAR(120) NOT NULL DEFAULT 'agent',
-- Who must approve. Either a specific user or a rotation/topic.
required_user_id UUID REFERENCES users(id) ON DELETE SET NULL,
required_topic VARCHAR(80),
-- Human-readable summary surfaced in the PWA card.
title VARCHAR(200) NOT NULL,
summary TEXT NOT NULL,
risk_level VARCHAR(20) NOT NULL DEFAULT 'medium',
-- Structured action the agent intends to take if approved
-- ({ kind: "isolate_host", host: "WIN-…" }, etc).
action JSONB NOT NULL,
-- Approval lifecycle.
status VARCHAR(20) NOT NULL DEFAULT 'pending',
-- pending | approved | denied | expired | cancelled
decided_by_id UUID REFERENCES users(id) ON DELETE SET NULL,
decided_at TIMESTAMPTZ,
decision_comment TEXT,
expires_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_agent_approvals_tenant_status
ON agent_approvals(tenant_id, status);
CREATE INDEX IF NOT EXISTS idx_agent_approvals_user
ON agent_approvals(required_user_id)
WHERE status = 'pending';
CREATE INDEX IF NOT EXISTS idx_agent_approvals_run
ON agent_approvals(run_id);
ALTER TABLE agent_approvals ENABLE ROW LEVEL SECURITY;
ALTER TABLE agent_approvals FORCE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS agent_approvals_tenant ON agent_approvals;
CREATE POLICY agent_approvals_tenant ON agent_approvals
USING (tenant_id = current_tenant_id() OR current_tenant_id() IS NULL);
-- ============================================================
-- 5. House-keeping: keep updated_at fresh on agent_approvals
-- ============================================================
CREATE OR REPLACE FUNCTION agent_approvals_touch_updated()
RETURNS TRIGGER LANGUAGE plpgsql AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$;
DROP TRIGGER IF EXISTS trg_agent_approvals_touch ON agent_approvals;
CREATE TRIGGER trg_agent_approvals_touch
BEFORE UPDATE ON agent_approvals
FOR EACH ROW EXECUTE FUNCTION agent_approvals_touch_updated();