RUDS - Rogue User Detection
Platform-wide behavioral threat detection: unified action-event hypertable, rules engine, two-tier scorer, LLM second-opinion, SOC dashboard.
Status: Phases 1–3 landed + AI integration · Phases 4–8 pending
Branch: claude/rogue-user-detection-K9rRd
Scope: Platform-wide (all 16 services + web) behavioral threat detection, prevention, and alerting.
Audience: Engineering, security/compliance officers, hospital admins.
Landed (commits on this branch)
Phase 1 — foundation
infrastructure/medbase/migrations/20260522b_ruds_foundation.sql— 5 tables + RLS + tier triggerpackages/security-kit/src/ruds/{types,evaluate}.ts+ jest spec — pure rule evaluatorservices/gateway/src/ruds/rudsEmitter.ts+ApiGatewayService.tshook — buffered event emission
Phase 2 — scoring + prevention
infrastructure/medbase/migrations/20260522c_ruds_seed_rules.sql— 17 baseline rules + ack subject type + 8 cron jobspackages/security-kit/src/ruds/rulesCache.ts— TTL-cached rule snapshotpackages/security-kit/src/middleware/moleculerRudsGuard.ts— per-service lockout middleware factoryservices/gateway/src/ruds/rudsScorer.ts— inline scorer + lockout cache + in-memory window ringservices/gateway/src/ApiGatewayService.ts— lockout precheck inauthorize(); scoreEvent call inonAfterCall()
Phase 3 — alerting
packages/security-kit/src/ruds/dispatch.ts— applies EvaluationResults: writes user_risk_scores delta + security_alerts + AcknowledgementRequest with severity-appropriate channels/deadline/escalation
AI integration
infrastructure/medbase/functions/ruds-ai-analyst/index.ts— Claude Haiku/Sonnet second-pass with prompt caching, anonymization (SHA-256 truncated hashes), strict guardrails (cap=84, confidence≥0.7, no block/lockout from AI, severity hard-clipped to ≤high)
Phase 4 — baselines + batch sweeper
infrastructure/medbase/functions/ruds-baseline-recompute/index.ts— nightly p10/p90 of usual hours, top-3 geos, hashed devices, p95 read/export rates, peer-group inferenceinfrastructure/medbase/functions/ruds-batch-scorer/index.ts— 4 modes: impossible-travel, rbac-audit, orphan-tokens, peer-drift
Phase 6 — admin UI
/admin/security/detection-rules— list every rule, toggle enabled, view predicate JSON, recent-alerts panel (auto-refresh 30s)/admin/security/dashboard— SOC dashboard with severity counters (open critical/high/medium/low), recent-alerts table, elevated-users sidebar (above-green tier), top-firing-rules-24h. Realtime viauseRudsRealtime./admin/security/users?userId=<uuid>— per-user drill-down: current risk + tier badge, lockout status with manual clear + manual 30-min lockout actions, behavioral baseline panel (usual hours/geos/devices/read+export rates), alerts for this user, last 100 events with matched-rules badges. Auto-refreshes via realtime./admin/security/live— streaming live tail: realtime feed of user_action_events, filter by all / risky (≥30) / rule-matches-only, pause/resume, click any user id to open drill-down.web/src/hooks/useRudsRealtime.ts— singleton/refcount realtime hook mirroringusePolicyGatesRealtime(debounced invalidation across 7 query keys spanning the 3 surfaces).- Routes registered in
web/src/routes/AdminRoutes.tsx./security/rudsand/security/ruds/livepaths from the original spec are deferred to a future security-area top-level shell; today they live under/admin/security/*since the admin route prefix already enforces the role gate.
Phase 7 — cross-region seed packs
infrastructure/market-packs/medos-japan/seed-ruds-rules.sql— excludenurse_nightfrom off-hours, narrow new-country to JP/SG/US/GB, APPI-tighter 2.5× read ratioinfrastructure/market-packs/medos-philippines/seed-ruds-rules.sql— disable new-country (multi-island normal spread), narrow off-hours window, tighter bill-void threshold (12 vs 20)
Phase 2.5 — per-service middleware (full platform rollout)
packages/platform-api-schema/src/common/middlewares/ruds-guard.middleware.ts— self-bootstrapping wrapper aroundsecurity-kit.makeMoleculerRudsGuard. ReadsSUPABASE_URL+SUPABASE_SERVICE_ROLE_KEYfrom env on first call. Toggle off withRUDS_GUARD_ENABLED=false.- Mounted in all 15 Moleculer services (both dev + prod broker configs):
medication,financial,clinical,auth,administration,diagnostic,foundation,global-sequence,eform,filestore,messaging,interoperability,patient-relationship,public-api,llm. gatewayuses its own inline scorer (already protected viaauthorize()precheck) — no Moleculer middleware needed there.migrationis a data-loader service, no end-user actions to guard.
Phase 6 follow-up — JSON editor + dry-run tester
/admin/security/detection-rulesnow has an Edit button per row opening a tabbed dialog: Edit (JSON for predicate/action/scope + description + score_weight, syntax-validated) and Test (sample event JSON + Run-test button calling theruds-test-ruleedge fn).infrastructure/medbase/functions/ruds-test-rule/index.ts— self-contained Deno port of the security-kit evaluator. Same predicate DSL; the jest spec on the Node side keeps the canonical behavior pinned.
Phase 8 — compliance hardening
infrastructure/medbase/migrations/20260522d_ruds_compliance.sql:- Immutability triggers on
user_action_eventsandsecurity_alerts. UAE rows are append-only after 5-min back-fill window; SA create-time fields (rule, severity, evidence, etc.) are frozen forever, only status/resolution/false_positive may change. DELETE is blocked outside the retention sweep, which setsapp.ruds_retention_sweep=truesession var to pass the trigger. user_action_events_archive(weekly aggregate, 7y retention) +security_alerts_archive(full alert copy).ruds_archive_sweep(days)function — rolls non-flagged events older than N days into weekly aggregates, copies resolved alerts into the alerts archive, deletes originals.ruds_hard_delete_after_7y()— drops everything older than 7y across all 4 tables.- Two new cron jobs (disabled):
ruds-archive-sweep(Sun 04:00),ruds-hard-delete-7y(monthly 2nd, 05:00). - Evidence export RPCs (
SECURITY DEFINER, role-gated):ruds_export_user_evidence(user_id, from, to)returns a single JSONB packet (baseline + risk + detail events + aggregate events + live alerts + archive alerts) for HIPAA / NHSO / APPI audit responses.ruds_export_tenant_summary(tenant_id, from, to)returns aggregate counts only (no PHI) for periodic compliance reports. Event enrichment + bot/API detection (Phase 9)
- Immutability triggers on
services/gateway/src/ruds/eventEnricher.ts— per-request metadata enrichment (no I/O, in-memory counters). Produces: resourceType, actionCategory, userRoleCategory, localHour, authKind, isExport, hourlyReadCount, requestsPerMinuteFromIp, userAgentCategory, distinctUsersFromIp, sequential404s, missingAcceptLang, withinChangeWindow.infrastructure/medbase/migrations/20260522e_ruds_bot_api_detection.sql— 8 bot/API-abuse rules:ruds.bot.high_velocity_ip_v1— >300 req/min from one IP → block 15mruds.bot.credential_stuffing_v1— 20+ distinct userIds from same IP in 10m → block 60m + alert CRITICALruds.bot.scanner_ua_v1— nikto/sqlmap/nuclei/burp etc. UA → block immediately + alert CRITICALruds.bot.bot_ua_authed_v1— python-requests/curl on authed endpoints → alert MEDIUMruds.bot.sequential_404_v1— 10+ consecutive 404s from one IP → block 30mruds.bot.no_accept_lang_velocity_v1— no Accept-Language + >60 req/min → alert MEDIUMruds.bot.unauthed_flood_v1— anonymous IP >100 req/min → block 30mruds.bot.service_token_off_hours_v1— service token active outside Mon–Fri 09:00–17:00 → alert MEDIUM
Operator steps to deploy
- Manual migration apply — per CLAUDE.md,
supabase db pushis blocked; apply 20260522b through 20260522e via SQL editor orpsql -f - Edge fn deploy —
supabase functions deploy ruds-ai-analyst ruds-baseline-recompute ruds-batch-scorer ruds-test-rule - Cron enable — cron jobs ship
enabled=FALSE; admin enables in/super-admin/cron-jobsafter the edge fns are deployed andANTHROPIC_API_KEYis set - Legal review — APPI/GDPR/HIPAA sign-off before activation in production (spec §14 open question)
1. Problem
medOS-ultra has fragments, not a system:
| Fragment | Location | Limitation |
|---|---|---|
| Kaigo login lockout | web/src/services/kaigo/kaigoSessionGuards.service.ts |
Client-only (localStorage), Japan-only, no backend correlation |
| Auth audit log | services/auth/src/api/aaa/modules/auditLog/ |
Capture only — no detection runs on top |
| Public-API audit log | services/public-api/.../modules/auditlog/ |
Same — separate silo, no cross-correlation |
| LLM call audit | services/llm/src/api/llm/modules/auditLog/ |
Same — separate silo |
| Patient action audit | packages/platform-api-schema/.../patientAuditLog/ |
Per-record only, not per-user behavior |
| FHIR subscription rate limit | 100/min per subscription |
Resource protection, not behavior |
| FHIR DetectedIssue | services/public-api/.../detectedIssue.controller.ts |
Clinical (drug interactions), not user behavior |
| Z-score outlier detection | packages/health-data-platform/.../health-intelligence.ts |
Patient vitals, not user behavior |
What’s missing: a single layer that ingests every user action across every service, scores it against configurable rules + an LLM analyst, blocks the obviously-malicious ones inline, alerts the suspicious ones to a security officer, and periodically re-baselines every user.
This document specifies that layer.
2. Threat model
What we want to catch:
| Category | Example | Default response |
|---|---|---|
| Credential abuse | 50 failed logins in 10 min; impossible-travel (Bangkok → Manila in 5 min); login from a new country | Block + alert |
| Mass exfiltration | Single user opens 500 patient charts in 1 hour; bulk-export to CSV at 2 AM | Alert + require step-up auth; block if score > 90 |
| Snooping / VIP access | Non-care-team user reads a VIP/celebrity/colleague chart | Alert (always) |
| Privilege escalation | RBAC grant added outside change-window; self-grant; new admin role assigned | Block + alert |
| Off-hours access | Cleaner / cashier / clerical role active 02:00–05:00 local | Alert |
| Concurrent sessions | Same user account active from 2+ IPs simultaneously | Alert; block second session if score > 70 |
| Workflow abuse | Cashier voids 20+ paid bills in a shift; pharmacist dispenses controlled substances without an order | Block + alert |
| API abuse | Service token used outside its declared scope; FHIR write API called from an IP not in allowlist | Block |
| Data tampering | Backdated note edits; retroactive diagnosis changes after billing closed | Alert (always — HIPAA / NHSO audit risk) |
| LLM abuse | Same user generates 200+ LLM completions in an hour; prompt-injection patterns in input | Rate-limit + alert |
Out of scope (handled elsewhere): clinical decision support (cds_rules), drug interactions (DetectedIssue), patient vitals anomalies (health-data-platform), workflow gates (policy_gates).
3. Architecture overview
┌─────────────────────────────────────────────────────────────────────────┐
│ All services (gateway, auth, clinical, financial, medication, ...) │
│ emit a UserActionEvent on every authenticated request │
└────────────────────────┬────────────────────────────────────────────────┘
│ NATS: ruds.events.*
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ ruds-collector (NestJS service, new) │
│ • normalizes the 4 audit-log silos + live NATS feed │
│ • writes to user_action_events (Supabase, hypertable) │
│ • forwards to inline scorer │
└────────────────────────┬────────────────────────────────────────────────┘
│
┌───────────────┴────────────────┐
▼ ▼
┌─────────────────────┐ ┌─────────────────────────────┐
│ INLINE scorer │ │ BATCH scorer (cron) │
│ (sync, < 50 ms) │ │ • baseline recompute │
│ • deterministic │ │ • AI sweep over 24h window │
│ detection_rules │ │ • peer-group comparison │
│ • cache hot rules │ │ • drift / new-user check │
└─────────┬───────────┘ └──────────────┬──────────────┘
│ │
│ ┌────────────────────────┘
▼ ▼
┌─────────────────────────────────────────────────────────────────────────┐
│ user_risk_scores (current score per user) │
│ security_alerts (one row per detection hit) │
│ user_baselines (learned per-user behavior fingerprint) │
└─────────────────────────┬───────────────────────────────────────────────┘
│
┌────────────────┼────────────────────┐
▼ ▼ ▼
┌──────────────┐ ┌────────────────┐ ┌──────────────────────┐
│ PREVENTION │ │ ALERTING │ │ DASHBOARD │
│ • gateway │ │ AcknowledgementRequest │ /security/ruds │
│ middleware │ │ to security officer │ widget rail │
│ • per-service│ │ • multi-channel │ live tail │
│ guard │ │ (APP/EMAIL/SMS/PUSH) │ │
└──────────────┘ └────────────────┘ └──────────────────────┘
Key design choices:
| Choice | Rationale |
|---|---|
Rules as JSON in detection_rules (mirrors policy_gates, cds_rules) |
Admin-editable, realtime-propagated, no redeploy |
| Two-tier scorer (inline + batch) | Inline catches the obvious in < 50 ms; batch catches the subtle patterns (impossible travel needs prior login, bulk-export needs an hour of context) |
| Per-user baselines, not a global rule | A radiologist reading 200 charts/hour is normal; a cashier doing it isn’t |
| AI is a second opinion, not the primary gate | LLMs are non-deterministic. Deterministic rules block; AI only raises alerts. Never the other way. |
Reuse AcknowledgementRequest for alerting |
Multi-channel delivery, escalation, RRULE reminders already shipped (docs/architecture/acknowledgement-system.md) |
Reuse cron_jobs registry for batch sweeps |
Visible, auditable, toggleable from admin UI — no “rogue cron” (docs/architecture/cron-jobs-registry.md) |
| Append-only event log (TimescaleDB hypertable) | Forensic trail; required for HIPAA / NHSO / ISO 27001 audits. 7-year retention. |
| Fail-open at the prevention layer | If the scorer is down, requests pass. RUDS must never break clinical care. Alerts fire on scorer-down too. |
4. Data model
4.1 user_action_events (Supabase, TimescaleDB hypertable)
The unified event stream. Replaces / supplements the 4 audit-log silos.
| Column | Type | Notes |
|---|---|---|
event_id |
uuid PK | |
occurred_at |
timestamptz | hypertable partition key |
user_id |
uuid | references app_users |
session_id |
uuid | from JWT sid claim |
service |
text | gateway, clinical, financial, … |
action |
text | LOGIN, READ_PATIENT, EXPORT_CSV, RBAC_GRANT, … |
resource_type |
text | Patient, Encounter, Invoice, … |
resource_id |
text | nullable |
tenant_id |
uuid | hospital / market pack |
ip |
inet | |
geo |
jsonb | {country, region, city, lat, lng} (from MaxMind on the gateway) |
user_agent |
text | |
http_status |
int | nullable for non-HTTP actions |
elapsed_ms |
int | |
metadata |
jsonb | action-specific payload |
risk_score |
int | 0–100, set inline by scorer |
matched_rules |
text[] | rule ids that fired |
Retention: 7 years for compliance; downsample to weekly aggregates after 90 days for non-flagged events.
4.2 detection_rules (mirrors policy_gates)
| Column | Notes |
|---|---|
rule_id |
text PK, e.g. ruds.login.brute_force_v1 |
name, description |
|
category |
credential | exfiltration | snooping | escalation | off_hours | concurrent | workflow | api | tampering | llm_abuse |
tier |
inline (sync, hot path) or batch (cron sweep) |
predicate_json |
event-matching expression — see §5 |
window |
sliding window for aggregation, e.g. 10m, 1h, 24h |
threshold_json |
{ count: 50, distinct: 'ip' } etc. |
action_json |
{ block: true, alert: { severity: 'HIGH', recipient: 'role:security_officer' }, requireStepUp: false } |
score_weight |
0–100, added to user risk score on match |
scope_json |
role / tenant / market-pack scoping (same shape as policy_gates) |
enabled, active, managed_by |
4.3 user_risk_scores
One row per user. Updated by inline scorer (delta) and batch scorer (full recompute).
| Column | Notes |
|---|---|
user_id |
PK |
score |
0–100, capped |
tier |
green (0–29), yellow (30–59), orange (60–84), red (85–100) |
last_event_at, last_alert_at |
|
recent_rules |
text[] — last 10 matched rules |
step_up_required_until |
timestamptz — gateway forces re-auth + MFA |
lockout_until |
timestamptz — set when tier=red and rule has block: true |
4.4 user_baselines
The learned-normal fingerprint per user. Populated by nightly batch job.
| Column | Notes |
|---|---|
user_id |
PK |
usual_hours |
jsonb — {mon: [8, 18], tue: [8, 18], ...} |
usual_geos |
jsonb — [{country: 'TH', region: 'Bangkok', lastSeen: ...}] |
usual_devices |
jsonb — UA fingerprints + frequency |
usual_read_rate |
numeric — patients/hour, p95 |
peer_group |
text — role + department (e.g. nurse:ward_a) |
last_recomputed_at |
timestamptz |
4.5 security_alerts
One row per detection hit that triggered an alert.
| Column | Notes |
|---|---|
alert_id |
uuid PK |
created_at |
|
user_id |
subject |
rule_id |
detection_rules.rule_id |
severity |
low | medium | high | critical |
event_window |
jsonb — start, end, sample event ids |
summary |
text — AI-generated narrative (≤ 200 chars) |
evidence_json |
jsonb — feeding events, counts, geo, etc. |
ack_request_id |
uuid — FK to AcknowledgementRequest |
status |
OPEN | ACKNOWLEDGED | DISMISSED | ESCALATED | RESOLVED |
resolution_notes |
text |
false_positive |
bool — feeds back into AI tuning |
5. Predicate DSL (rule expressions)
The same shape as policy_gates.predicate_json, extended for time-window aggregation.
{
"all": [
{ "event.action": "LOGIN_FAILED" },
{ "_aggregate": {
"window": "10m",
"groupBy": ["user_id"],
"count": { "gte": 10 }
}
}
]
}
Supported operators: all, any, not, eq, neq, in, gte, lte, between, regex, geoDistanceKm, peerCompare (score vs user_baselines.usual_*), _aggregate (windowed count/distinct/sum).
A pure evaluateDetection(rule, eventStream, baseline) → {matched, evidence, score} function lives in packages/security-kit/src/ruds/evaluate.ts. Testable without Supabase; reused by inline scorer and batch sweeper.
6. AI / LLM layer
Deterministic rules always run first. The AI layer is a second pass over the batch window.
nightly cron ──▶ ruds-ai-analyst (Supabase edge fn, Deno)
│
├─ pulls last 24h of events per user
├─ pulls user_baselines fingerprint
├─ calls Claude with structured prompt + JSON schema response
│ (model: claude-haiku-4-5-20251001 for cost; escalate
│ to claude-sonnet-4-6 for users already at tier ≥ orange)
├─ schema: { risk_delta, narrative, suggested_action,
│ novel_pattern, confidence }
└─ writes back to user_risk_scores + security_alerts
(only when confidence ≥ 0.7)
Strict rules for the AI layer:
- AI cannot block. Only deterministic rules with
action_json.block = truecan block requests. AI output flows through alerts. - AI cannot raise score above 84 (orange tier max). Pushing a user into red (85+) requires a deterministic rule hit.
- Confidence < 0.7 → discard. No alert raised.
- Every AI alert carries a
novel_patternboolean. When true, an admin can promote it to a newdetection_rulesrow (with a “from AI suggestion” provenance tag), reviewed before activation. - Prompt caching is mandatory — the baseline fingerprint + rule catalog go into the cached prefix; only the 24h event sample is in the dynamic suffix. (Pattern:
claude-apiskill.) - No PHI in prompts. Events are anonymized server-side: patient ids hashed, free-text note bodies dropped. Only metadata + counts + timestamps.
- Audit log — every LLM call is written to
services/llm/.../auditLogwith the prompt hash, response, cost, latency.
7. Prevention layer
7.1 Gateway middleware (inline)
Every request hits services/gateway/.../middleware/ruds-guard.ts before the route handler:
on request:
1. fetch user_risk_scores for current user_id (cached 30s)
2. if lockout_until > now → 423 Locked, alert security officer
3. if step_up_required_until > now → 401 + WWW-Authenticate: step-up
4. emit UserActionEvent to NATS (fire-and-forget)
5. inline scorer runs deterministic hot rules against this event
6. if any matched rule has action_json.block = true → reject
7. otherwise pass through; risk_score delta written async
Performance budget: p95 < 50 ms added latency. If the score cache misses and Supabase is slow, fail-open (request passes, alert fires on scorer-down).
7.2 Per-service guards (defense in depth)
The gateway can be bypassed (internal NATS calls between services). Each service mounts a thin Moleculer middleware that checks user_risk_scores.lockout_until on every ctx.meta.user.id. Same fail-open behavior.
7.3 Step-up auth
When step_up_required_until is set, the next request returns 401 with WWW-Authenticate: step-up realm="ruds". Frontend handles by showing the existing MFA / re-auth modal. On success, the gateway clears the flag and emits a STEP_UP_PASSED event.
8. Alerting layer
Every security_alerts row creates an AcknowledgementRequest:
| Severity | Recipient | Channels | Escalation |
|---|---|---|---|
low |
role:security_officer |
APP | none (just inbox) |
medium |
role:security_officer |
APP, EMAIL | after 60m → security manager |
high |
role:security_officer + role:ciso |
APP, EMAIL, PUSH | after 15m → CISO |
critical |
role:ciso + role:hospital_admin |
APP, EMAIL, SMS, PUSH | after 5m → on-call + page |
The acknowledgement’s subject points back at security_alerts/{alert_id}. Acknowledging marks the alert ACKNOWLEDGED; declining marks it DISMISSED (and false_positive = true, feeding AI tuning).
Alerts are also surfaced via:
- WidgetRail widget
SecurityAlertsWidget(icon dock + flyout, seedocs/architecture/widget-rail-surface-system.md) — visible to security-officer role only. - Live tail at
/security/ruds/livefor the SOC view. - Dashboard at
/security/ruds— heat map by user/department, score trends, top firing rules, false-positive rate per rule.
9. Periodic checks (cron jobs)
Registered in cron_jobs so they’re visible and toggleable at /super-admin/cron-jobs:
| Job | Schedule | Purpose |
|---|---|---|
ruds-baseline-recompute |
0 2 * * * (02:00 UTC daily) |
Recompute every active user’s user_baselines from last 30 days |
ruds-ai-sweep |
15 2 * * * (02:15 UTC daily) |
AI second-pass over last 24h per user |
ruds-decay-scores |
0 * * * * (hourly) |
Decay risk scores: −2 per quiet hour, floor at 0 |
ruds-peer-drift-check |
30 3 * * 0 (weekly Sun 03:30) |
Compare each user to peer group median; alert on outliers |
ruds-orphan-token-scan |
0 4 * * * (daily) |
Find service tokens unused 30+ days; auto-disable |
ruds-rbac-change-audit |
0 */6 * * * |
Sweep RBAC grants in last 6h; flag those outside change-window |
ruds-impossible-travel-sweep |
*/10 * * * * |
10-min sweep for geo-impossible login pairs (belt-and-suspenders alongside inline rule) |
ruds-retention-prune |
0 5 1 * * (monthly) |
Drop event detail older than 90 days for non-flagged users; keep aggregates |
10. Baseline rule library (seed)
Ships as a migration; admin can edit / disable any of them.
| Rule id | Category | Tier | Default action | Notes |
|---|---|---|---|---|
ruds.login.brute_force_v1 |
credential | inline | block 30m, alert HIGH | ≥10 LOGIN_FAILED in 10m |
ruds.login.impossible_travel_v1 |
credential | inline | block, alert CRITICAL | two successful logins > 500km apart in < geo_speed_kmh window |
ruds.login.new_country_v1 |
credential | inline | step-up, alert MEDIUM | first login from a country not in baseline |
ruds.session.concurrent_v1 |
concurrent | inline | alert HIGH, block 2nd if score ≥ 70 | same user_id, 2+ active session_ids, different IPs |
ruds.read.bulk_patient_v1 |
exfiltration | batch | alert HIGH, step-up if > 200 | reads > 3× user’s p95 in 1h |
ruds.export.bulk_csv_v1 |
exfiltration | inline | block, alert CRITICAL | any CSV export > 100 rows requires data_export role |
ruds.read.vip_chart_v1 |
snooping | inline | alert HIGH (always) | non-care-team user reads a chart flagged vip=true |
ruds.read.colleague_chart_v1 |
snooping | inline | alert MEDIUM | user reads a chart whose patient_id matches a staff record |
ruds.access.off_hours_v1 |
off_hours | inline | alert MEDIUM | role in clerical_roles active outside 02:00–05:00 local |
ruds.rbac.self_grant_v1 |
escalation | inline | block, alert CRITICAL | RBAC_GRANT where granted_to == granted_by |
ruds.rbac.outside_window_v1 |
escalation | batch | alert HIGH | RBAC change outside Mon–Fri 09:00–17:00 |
ruds.workflow.bill_void_storm_v1 |
workflow | batch | alert HIGH | cashier voids ≥ 20 bills/shift |
ruds.workflow.controlled_no_order_v1 |
workflow | inline | block, alert CRITICAL | controlled-substance dispense without matching MedicationRequest |
ruds.note.backdated_edit_v1 |
tampering | inline | alert HIGH (always) | note effectiveAt updated > 24h after first save |
ruds.api.token_scope_breach_v1 |
api | inline | block, alert HIGH | service-token call to route outside declared scopes |
ruds.llm.prompt_storm_v1 |
llm_abuse | batch | rate-limit, alert MEDIUM | > 200 LLM completions / user / hour |
ruds.llm.injection_pattern_v1 |
llm_abuse | inline | alert MEDIUM | prompt matches known injection regex set |
11. Admin UI
/admin/security/detection-rules — list/edit/toggle rules (same shape as /admin/policy-gates).
/admin/security/users/:userId — per-user view: risk score, baseline, recent events, alerts, manual lockout/clear.
/security/ruds — SOC dashboard (officer view).
/security/ruds/live — live tail of events + alerts.
/super-admin/ruds-config — global config: AI model selection, retention windows, default channels, escalation chains.
All four pages role-gated to security_officer / ciso / super_admin.
12. Cross-region considerations
Same pattern as cross-region-policy-gates-deployment.md:
- The migration ships an empty
detection_rulestable; the 17-rule seed is per region. - Off-hours bounds, geo-distance thresholds, and peer-group definitions vary:
- Japan: shift system + Tokuyou night staff means off-hours rule must exclude
nurse_nightrole. - Philippines: multi-shift + barangay outreach means geo-distance baseline is wider.
- Thailand: default seed.
- Japan: shift system + Tokuyou night staff means off-hours rule must exclude
- Locale: alert messages bilingual (local + English), enforced by the same i18n pipeline as the rest of the platform (CLAUDE.md rule 7).
- Currency-sensitive workflow rules (bill voids, refunds) use the region’s currency threshold.
13. Implementation phases
| Phase | Scope | Effort | Dependency |
|---|---|---|---|
| 0. Spec sign-off | This doc reviewed by engineering + security + compliance | 1 wk | — |
| 1. Event collector | ruds-collector service, user_action_events hypertable, gateway middleware emitting events (no scoring yet) |
2 wks | — |
| 2. Inline scorer + 6 rules | detection_rules table, evaluate engine, ship rules 1–6 (login + concurrent + bulk export) with prevention |
2 wks | phase 1 |
| 3. Alerting wire-up | security_alerts table, AcknowledgementRequest integration, SOC dashboard v1, widget rail |
2 wks | phase 2 |
| 4. Baselines + batch sweeper | user_baselines table, nightly cron, peer-group comparison, rules 7–17 |
2 wks | phase 3 |
| 5. AI analyst | ruds-ai-analyst edge fn, prompt-caching, novel-pattern promotion flow |
2 wks | phase 4 |
| 6. Admin UI v2 | Rule editor, per-user drill-down, false-positive feedback loop | 1 wk | phase 5 |
| 7. Cross-region seeds | Per-market-pack rule packs (JP/PH/TH), locale-aware messages | 1 wk | phase 6 |
| 8. Compliance hardening | 7-year retention policies, immutability constraints, NHSO/HIPAA evidence exports | 2 wks | phase 7 |
Total: ~15 weeks single-track; ~10 weeks with two engineers.
14. Open questions
- TimescaleDB vs vanilla Postgres partitioning — TimescaleDB extension status on Supabase? If unavailable, fall back to monthly-partitioned tables.
- Event volume budget — at ~5 hospitals × ~2k users × ~200 events/user/day, we’re at ~2M events/day. Confirm Supabase write throughput + the NATS topic doesn’t drown other consumers.
- AI cost ceiling — Haiku for routine sweeps + Sonnet for tier ≥ orange. Set a per-tenant monthly cost cap; alert security officer when 80% consumed.
- Service-account events — do internal NATS calls between services emit RUDS events? Spec assumes yes for compromise detection, but the volume needs review.
- Federation with hospital SIEM — many hospitals already run Splunk / Sentinel. Should RUDS push alerts via syslog / CEF or stay closed-loop?
- EU / PDPA / HIPAA legal review — passive behavior monitoring of clinical staff is regulated. Need legal sign-off per region before phase 1 ships, especially in Japan (APPI) and EU (GDPR Art. 22 auto-decision restrictions).
15. Non-goals
- Not a replacement for network IDS / WAF — RUDS sees application events, not raw packets.
- Not a DLP product — we detect access patterns, not file-content classification.
- Not a clinical-decision engine —
cds_rulesowns that surface. - Not a workflow gate —
policy_gatesowns that surface.