Branch: claude/explore-new-repo-GVf2W
Files changed: 16 | Lines added: 2,421
What This Is
Two optional infrastructure services that bolt onto the medOS hospital stack:
- Metabase — BI analytics dashboards (“medOS Analytics”)
- Wazuh — SIEM/XDR security monitoring (“medOS Security Monitor”)
Both are pre-configured to work with medOS data and branding. Hospital IT
deploys them and everything is ready — no configuration, no learning curve.
Why These Two
| Need |
Solution |
Why not alternatives? |
| Hospital analytics (census, revenue, bed occupancy) |
Metabase |
Open-source, connects directly to MongoDB, non-technical users can browse dashboards without SQL |
| Security monitoring (HIPAA, intrusion detection, audit) |
Wazuh |
Open-source SIEM with built-in file integrity, agent-based monitoring, and compliance mapping |
Removed: OpenCTI was originally included but removed — it’s a threat intelligence platform for SOC teams, not something hospital IT departments would use.
File Structure
infrastructure/platform-services/
├── .env.example # All credentials (MUST change before deploy)
├── README.md # User-facing quick start
├── deploy-platform.sh # Unified deploy script (197 lines)
├── hospital-sizes.env # 4 scaling profiles
│
├── branding/ # Shared medOS brand assets
│ ├── medos-logo.png # Heartbeat circle icon (62 KB)
│ ├── medos-favicon.ico # Browser tab icon
│ ├── medos-favicon.png # PNG variant
│ ├── medos-wordmark.png # EVER logo horizontal (17 KB)
│ └── medos-wordmark.svg # EVER logo vector
│
├── metabase/
│ ├── docker-compose.metabase.yml # Metabase + PostgreSQL (metadata)
│ └── metabase-setup.sh # Auto-setup: branding + 13 dashboards
│
└── wazuh/
├── docker-compose.wazuh.yml # Indexer + Manager + Dashboard
├── medos-decoders.xml # 6 custom log decoders (225 lines)
├── medos-rules.xml # 80+ detection rules (864 lines)
└── wazuh-setup.sh # Auto-setup: rules + branding
Hospital Size Scaling
Every service has resource limits tuned to four deployment sizes. The
deploy-platform.sh script reads hospital-sizes.env and exports the right
variables before running docker compose up.
| Profile |
Beds |
Metabase |
Wazuh (total) |
Combined |
| small |
< 50 |
1 GB |
2 GB |
~3 GB |
| medium |
50–300 |
2 GB |
4 GB |
~6 GB |
| large |
300–1000 |
4 GB |
8 GB |
~12 GB |
| enterprise |
1000+ |
6 GB |
16 GB |
~22 GB |
Controls: Java heap, CPU shares, Docker memory limits, Wazuh indexer JVM,
and (for Metabase) concurrent query capacity.
Docker Architecture
┌─────────────────────────────────────────┐
│ medos-metabase (port 3100) │
│ metabase/metabase:latest │
│ Connects to medOS MongoDB for queries │
│ Stores metadata in its own PostgreSQL │
├─────────────────────────────────────────┤
│ metabase-postgres (port 5433) │
│ postgres:14.5 │
│ Internal metadata only (not patient) │
└─────────────────────────────────────────┘
Branding Applied
| Setting |
Value |
| Application name |
medOS Analytics |
| Brand color |
#009378 (medOS teal) |
| Logo |
Heartbeat circle (medos-logo.png) |
| Favicon |
medos-favicon.ico |
| Loading message |
“Preparing hospital data…” |
| Metabase links |
Hidden |
| Metabot |
Disabled |
| Help link |
Hidden |
Pre-built Dashboards (13 reports)
Created automatically by metabase-setup.sh via the Metabase API.
Clinical Operations:
| Report |
MongoDB Collection |
Query Type |
| Daily Patient Census |
admission |
Group by ward, filter status=Admit/Admitted |
| Admissions Today |
admission |
Filter admitDateTime >= today |
| Discharges This Week |
admission |
Filter dischargeDateTime >= 7 days ago |
| Emergency Visits (30d) |
admission |
Filter treatmentMode=Emergency, group by date |
| Top 20 Diagnoses (30d) |
diagnosis |
Group by icd10Code, sort by count |
Financial & Billing:
| Report |
MongoDB Collection |
Query Type |
| Daily Revenue Summary |
invoice |
Group by docDate, sum totalAmount |
| Outstanding A/R |
account_receivable |
Filter status != Paid, group by payorName |
| Payment Methods |
receipt |
Group by paymentMethod, sum amount |
Hospital Operations:
| Report |
MongoDB Collection |
Query Type |
| Bed Occupancy by Ward |
bed |
Group by ward, calc occupied/total ratio |
| Lab Request Volume (7d) |
lab_request |
Group by date, count |
| Medication Orders by Status |
medication_request |
Group by status |
| Active Staff Today |
user |
Filter lastLogin >= today, group by department |
| Appointment Volume (7d) |
appointment |
Filter start between today and +7d |
Wazuh — “medOS Security Monitor”
Docker Architecture
┌─────────────────────────────────────────┐
│ medos-wazuh-indexer (port 9201) │
│ wazuh/wazuh-indexer:4.9.2 │
│ OpenSearch-based, stores alert data │
├─────────────────────────────────────────┤
│ medos-wazuh-manager │
│ wazuh/wazuh-manager:4.9.2 │
│ Ports: 55000 (API), 1514 (agents), │
│ 1515 (enrollment), 514/udp │
│ Runs rules engine, receives logs │
├─────────────────────────────────────────┤
│ medos-wazuh-dashboard (port 5601) │
│ wazuh/wazuh-dashboard:4.9.2 │
│ Branded as "medOS Security Monitor" │
└─────────────────────────────────────────┘
Branding Applied
| Setting |
Value |
| Application title |
medOS Security Monitor |
| Tab title |
medOS Security |
| App logo |
Heartbeat circle |
| Sidebar logo |
EVER wordmark |
| Health check logo |
Heartbeat circle |
| Favicon |
medos-favicon.ico |
Custom Decoders (9 parsers — medos-decoders.xml)
Each decoder parses a specific medOS log format so Wazuh rules can trigger on
structured fields.
| Decoder |
Source |
Format |
Key Fields Extracted |
medos-gateway |
API Gateway |
JSON (Moleculer) |
level, msg, nodeID, action, caller, requestID, duration |
medos-auth |
AAA Service |
Syslog structured |
action, user, ip, status, role, department |
medos-access |
Audit Trail |
Syslog structured |
user, action, resource, patient ID, breakglass flag |
medos-db |
Database Proxy |
Syslog structured |
user, query_type, table, rows_returned, duration_ms |
medos-device |
Device Gateway |
Syslog structured |
device_id, type, ip, event, target, status |
medos-rbac |
RBAC Events |
Syslog structured |
actor, action, target_user, role, prev_role |
medos-manifest |
Supabase triggers + backend |
Syslog structured |
table, action, event_type, encounter, source, client_type |
medos-orchestrator |
Deno Edge Function |
JSON + syslog |
level, event_id, event_type, latency_ms, status, hmac_valid |
medos-workflow |
Workflow State Machine |
Syslog structured |
event, user, encounter, from, to, transition_valid, server_validated |
Custom Rules (130+ detections — medos-rules.xml)
All rules use ID range 100000–100999 (Wazuh custom range). Severity levels
follow Wazuh convention: 0–4 informational, 5–7 low, 8–10 medium,
11–13 high, 14–15 critical.
Authentication & Access Control (100000–100099)
| Rule ID |
Level |
Detection |
HIPAA Ref |
| 100000 |
3 |
Failed login attempt |
164.312.d |
| 100001 |
10 |
Brute force (5+ failures/2min from same IP) |
164.312.d |
| 100002 |
12 |
Targeted account attack (5+ failures/5min same user) |
164.312.d |
| 100010 |
8 |
After-hours login (22:00–06:00) |
164.312.a.1 |
| 100011 |
6 |
Weekend login |
164.312.a.1 |
| 100012 |
10 |
Login from external IP (outside RFC1918) |
164.312.e.1 |
| 100722 |
6 |
User account disabled |
164.308.a.3 |
| 100723 |
13 |
Disabled/terminated account still logging in |
164.308.a.3 |
| 100730 |
5 |
Password changed |
164.312.d |
| 100731 |
7 |
Password reset by admin |
164.312.d |
| 100732 |
11 |
Bulk password resets (5+/5min by same admin) |
164.312.d |
| 100740 |
9 |
Shared/generic account login |
164.312.a.2.i |
| 100741 |
10 |
Concurrent sessions from different IPs |
164.312.d |
| 100742 |
12 |
Login from Tor exit node / anonymizer |
— |
Patient Record Access — HIPAA (100100–100199)
| Rule ID |
Level |
Detection |
HIPAA Ref |
| 100100 |
2 |
Patient record viewed (baseline) |
164.312.a.1 |
| 100101 |
10 |
Bulk snooping (20+ records/5min) |
164.308.a.5 |
| 100102 |
8 |
After-hours patient record access |
164.312.a.1 |
| 100110 |
10 |
Break-glass emergency override used |
164.308.a.4 |
| 100111 |
8 |
Access without care relationship |
164.312.a.1 |
| 100120 |
4 |
Patient record edited |
164.312.c.1 |
| 100121 |
12 |
Patient record deleted |
164.312.c.1 |
| 100997 |
10 |
VIP/celebrity patient record accessed |
164.312.a.1 |
| 100998 |
13 |
VIP snooping (3+ VIP records/10min) |
164.312.a.1 |
Data Exfiltration (100200–100299)
| Rule ID |
Level |
Detection |
HIPAA Ref |
| 100200 |
6 |
Data export event (baseline) |
164.312.e.1 |
| 100201 |
12 |
Bulk export (100+ records) |
164.308.a.1 |
| 100202 |
13 |
After-hours export |
164.312.e.1 |
| 100203 |
11 |
Repeated exports (3+/10min same user) |
164.312.e.1 |
Database Anomalies (100300–100399)
| Rule ID |
Level |
Detection |
| 100300 |
8 |
Large query result (1000+ rows) |
| 100301 |
11 |
Mass query on clinical tables (patient, diagnosis, vital_sign) |
| 100310 |
6 |
Slow query (>5 seconds) |
| 100320 |
15 |
DROP/TRUNCATE on production tables |
Medical Device Security (100400–100499)
| Rule ID |
Level |
Detection |
| 100400 |
10 |
Device attempting external connection |
| 100401 |
8 |
Blocked connection from device |
| 100410 |
12 |
Unauthorized firmware update |
| 100420 |
9 |
New unknown device on medical network |
| 100421 |
11 |
Device making repeated blocked connections |
Privilege Escalation / RBAC (100500–100599)
| Rule ID |
Level |
Detection |
HIPAA Ref |
| 100500 |
7 |
Role assignment event |
164.312.a.1 |
| 100501 |
12 |
Admin privilege granted |
164.312.a.1 |
| 100502 |
14 |
Self-elevation (user changed own role) |
164.312.a.1 |
| 100510 |
10 |
Break-glass override in RBAC context |
164.312.a.1 |
| 100520 |
13 |
Rapid role changes (3+/5min) |
— |
Application Health (100600–100699)
| Rule ID |
Level |
Detection |
| 100600 |
5 |
API error (single) |
| 100601 |
10 |
API error spike (20+/minute) |
| 100610 |
6 |
Slow API response (>10 seconds) |
Compliance & Audit (100700–100799)
| Rule ID |
Level |
Detection |
HIPAA Ref |
| 100700 |
15 |
Audit log tampering attempt |
164.312.b |
| 100710 |
8 |
System settings changed |
164.312.a.1 |
| 100720 |
6 |
New user account created |
164.308.a.3 |
| 100721 |
10 |
Rapid user creation (5+/5min) |
— |
Pharmacy & Medication Safety (100800–100899)
| Rule ID |
Level |
Detection |
| 100801 |
6 |
Controlled substance order |
| 100802 |
11 |
High-volume controlled substance orders (10+/hour) |
| 100810 |
9 |
Medication cancelled after dispensing (diversion risk) |
| 100811 |
8 |
Medication safety override (CDS bypass) |
| 100812 |
11 |
Frequent safety overrides (5+/hour) |
| 100820 |
10 |
Self-dispensing detected |
| 100830 |
7 |
Pharmacy inventory adjustment |
| 100831 |
10 |
Large inventory decrease |
| 100832 |
11 |
Frequent inventory adjustments (5+/hour) |
| 100840 |
12 |
Medication administered without valid order |
| 100841 |
7 |
High-alert medication ordered (insulin, heparin, chemo) |
Blood Bank & Transfusion Safety (100900–100949)
| Rule ID |
Level |
Detection |
| 100900 |
3 |
Blood product issued (baseline) |
| 100901 |
12 |
Blood issued WITHOUT crossmatch |
| 100902 |
9 |
Emergency blood release (uncrossmatched) |
| 100910 |
10 |
Expired blood product still in inventory |
| 100911 |
5 |
Blood product discarded (waste tracking) |
| 100912 |
9 |
High wastage (5+ discards/24h) |
| 100920 |
14 |
Blood type mismatch — STOP transfusion |
| 100930 |
8 |
Blood stock critically low |
Ransomware & Malware (100950–100979)
| Rule ID |
Level |
Detection |
| 100950 |
14 |
Files renamed with encryption extensions |
| 100951 |
15 |
Ransom note file detected |
| 100952 |
13 |
50+ file modifications in 30 seconds |
| 100955 |
12 |
Credential dumping tool (mimikatz, lazagne, etc.) |
| 100956 |
10 |
Encoded PowerShell execution |
| 100957 |
14 |
Shadow copy deletion (backup destruction) |
| 100958 |
9 |
Suspicious scheduled task creation |
| 100960 |
10 |
Large outbound data transfer |
| 100961 |
12 |
DNS query to known malicious domain |
Session & API Abuse (100980–100999)
| Rule ID |
Level |
Detection |
| 100980 |
8 |
API rate limit exceeded |
| 100981 |
12 |
Sustained API abuse (10+ rate limit hits/minute) |
| 100982 |
10 |
Expired JWT token reuse (replay attack) |
| 100983 |
9 |
Session IP changed (hijack indicator) |
| 100985 |
10 |
Non-admin accessing admin API endpoints |
| 100986 |
12 |
SQL injection attempt |
| 100987 |
11 |
XSS attempt |
| 100988 |
11 |
Path traversal attempt |
| 100990 |
5 |
Unauthorized API access (401) |
| 100991 |
10 |
Authorization probing (15+ 401s/2min) |
| 100992 |
9 |
Untrusted FHIR/interop endpoint access |
| 100993 |
5 |
Report printed (audit) |
| 100994 |
9 |
Bulk printing (10+/10min) |
| 100995 |
7 |
E-form template modified |
| 100996 |
8 |
E-form accessed for discharged patient |
Event System Monitoring (medOS-Specific)
The medOS stack broadcasts events through a specific pipeline. These rules
monitor that pipeline for tampering, forgery, and failures.
Event Flow
Backend API write
↓
hospital_events table (Supabase, append-only)
↓ Postgres trigger
↓ Webhook (HMAC-signed)
Encounter Orchestrator (Deno Edge Function)
↓ projects into
┌────────────────────────┬────────────────────────┬──────────────┐
encounter_journey_cache department_queues event_dlq
(per-encounter state) (operational queues) (failed events)
↓ Supabase realtime
React frontend subscribes
There are 140+ event types flowing through this pipeline. Rules below
catch tampering at each stage.
Manifest / Encounter Journey Integrity (101000–101099)
| Rule ID |
Level |
Detection |
HIPAA Ref |
| 101000 |
2 |
hospital_events INSERT (baseline) |
— |
| 101001 |
13 |
hospital_events UPDATE/DELETE (append-only violation) |
164.312.b, c.2 |
| 101010 |
13 |
encounter_journey_cache modified by non-orchestrator |
— |
| 101011 |
14 |
Frontend attempted direct write to read model |
— |
| 101020 |
10 |
Invalid event_type (possible injection) |
— |
| 101021 |
11 |
Event for non-existent encounter (forgery) |
— |
| 101030 |
10 |
Event storm (50+/minute for single encounter) |
— |
| 101040 |
11 |
Duplicate event_id (replay attack) |
— |
| 101041 |
9 |
Out-of-order event (state inconsistency) |
— |
| 101050 |
10 |
Event for archived encounter (zombie event) |
— |
| 101060 |
13 |
Safety snapshot tampered outside rules engine |
— |
Event Orchestration & Dead Letter Queue (101100–101199)
| Rule ID |
Level |
Detection |
| 101100 |
6 |
Orchestrator error (baseline) |
| 101101 |
11 |
Error spike (5+/minute) |
| 101110 |
8 |
Event sent to DLQ |
| 101111 |
12 |
DLQ growing rapidly (10+/5min — pipeline broken) |
| 101120 |
8 |
Slow orchestrator processing (>1 second) |
| 101130 |
12 |
Orchestrator health check failing |
| 101140 |
13 |
Invalid HMAC on webhook (forged event) |
| 101141 |
14 |
Sustained HMAC forgery campaign (3+/5min) |
| 101150 |
5 |
Cold start / restart |
| 101151 |
10 |
Frequent restarts (5+/15min — unstable) |
| 101160 |
7 |
Clinical alert pruning triggered |
Workflow State Machine (101200–101299)
| Rule ID |
Level |
Detection |
HIPAA Ref |
| 101200 |
3 |
Workflow session started (baseline) |
— |
| 101210 |
12 |
Illegal workflow transition attempt |
— |
| 101211 |
9 |
Out-of-sequence workflow step |
— |
| 101220 |
5 |
Workflow session cancelled |
— |
| 101221 |
9 |
High cancellation rate (10+/hour) |
— |
| 101230 |
9 |
Workflow template modified |
164.312.c.1 |
| 101231 |
13 |
Workflow template DELETED |
— |
| 101240 |
13 |
Transition without server validation |
— |
| 101250 |
6 |
Stale workflow session detected |
— |
| 101260 |
12 |
Workflow session bound to wrong target (hijack) |
— |
NATS / Moleculer Messaging (101300–101399)
| Rule ID |
Level |
Detection |
| 101300 |
8 |
NATS disconnection |
| 101301 |
12 |
NATS broker unstable (5+ disconnects/10min) |
| 101310 |
7 |
Moleculer action timeout / service not found |
| 101311 |
11 |
Timeout spike (20+/2min) |
| 101320 |
10 |
Unauthorized NATS subject subscription |
| 101330 |
8 |
Moleculer node disappeared (service crash) |
| 101340 |
7 |
Stale/lying cache detected |
Supabase Realtime Channels (101400–101499)
| Rule ID |
Level |
Detection |
HIPAA Ref |
| 101400 |
10 |
Unauthorized realtime subscription |
164.312.e.1 |
| 101401 |
11 |
Non-clinical user subscribed to journey cache |
164.312.a.1 |
| 101410 |
10 |
Wildcard channel subscription (data mining) |
— |
| 101420 |
11 |
High-volume realtime consumption (500+/min scraping) |
— |
| 101430 |
10 |
Realtime subscription from external IP |
164.312.e.1 |
Read Model & Queue Integrity (101500–101599)
| Rule ID |
Level |
Detection |
| 101500 |
13 |
department_queues modified by non-orchestrator |
| 101510 |
10 |
Queue priority rules modified |
| 101520 |
12 |
Queue number manually changed (line-jumping) |
| 101521 |
11 |
Queue status changed bypassing workflow |
| 101530 |
12 |
Clinical activity counter reset (metric tampering) |
| 101540 |
9 |
Cache drift detected (orchestrator missed events) |
| 101541 |
12 |
Frequent cache drifts (10+/30min) |
Reverse Proxy Integration
Routes added to the Nginx default.conf.template (first server block):
/metabase/ → medos-metabase:3000
/wazuh/ → medos-wazuh-dashboard:5601 (with WebSocket upgrade)
Environment variables for host override: METABASE_HOST, WAZUH_DASHBOARD_HOST.
Deployment Flow
cd infrastructure/platform-services
cp .env.example .env
vim .env
./deploy-platform.sh up medium
./metabase/metabase-setup.sh
./wazuh/wazuh-setup.sh
./deploy-platform.sh status
After this, hospital IT opens:
http://<host>/metabase/ — dashboards are already there
http://<host>/wazuh/ — security alerts are already flowing
Network Topology
All platform services join the medos-platform Docker bridge network.
The deploy script auto-creates it if missing.
┌─────────────────────────────────────────────────────────┐
│ medos-platform network (Docker bridge) │
│ │
│ ┌──────────┐ ┌──────────────┐ ┌───────────────────┐ │
│ │ Metabase │ │ Wazuh │ │ Wazuh │ │
│ │ :3100 │ │ Manager │ │ Dashboard :5601 │ │
│ └────┬─────┘ │ :55000/1514 │ └────────┬──────────┘ │
│ │ └──────┬───────┘ │ │
│ ┌────┴─────┐ ┌──────┴───────┐ │ │
│ │ Metabase │ │ Wazuh │ │ │
│ │ Postgres │ │ Indexer ├───────────┘ │
│ │ :5433 │ │ :9201 │ │
│ └──────────┘ └──────────────┘ │
│ │
│ ─── Also on this network: ─── │
│ mongo:27017 nats:4222 api-gateway:3001 web:3000 │
└─────────────────────────────────────────────────────────┘
Port Reference
| Port |
Service |
Protocol |
Exposed |
| 3100 |
Metabase UI |
HTTP |
Yes |
| 5433 |
Metabase PostgreSQL |
TCP |
Yes (configurable) |
| 5601 |
Wazuh Dashboard |
HTTP |
Yes |
| 9201 |
Wazuh Indexer |
HTTP |
Yes |
| 55000 |
Wazuh Manager API |
HTTPS |
Yes |
| 1514 |
Wazuh Agent comms |
TCP |
Yes |
| 1515 |
Wazuh Agent enrollment |
TCP |
Yes |
| 514 |
Syslog collector |
UDP |
Yes |
Customization
Swap Logos
Replace files in branding/ with hospital-specific logos:
medos-logo.png — square icon (recommended: 512x512 PNG)
medos-wordmark.png — horizontal logo for sidebars
medos-favicon.ico — browser tab icon
Re-run the setup scripts to apply.
Edit metabase/metabase-setup.sh and add more create_question calls.
The function takes: name, description, collection_id, and a MongoDB
aggregation pipeline as JSON.
Add Wazuh Rules
Add rules to wazuh/medos-rules.xml using IDs in the 100000–100999 range.
Re-run wazuh/wazuh-setup.sh to deploy.
Adjust Scaling
Edit hospital-sizes.env to tune resource limits for your hardware.
The variable naming convention is {SIZE}_{SERVICE}_{SETTING}.
Commits
| Hash |
Description |
c9c7fe68 |
Initial platform services (Metabase, OpenCTI, Wazuh) with hospital-size scaling |
e81f95e2 |
Remove OpenCTI (not needed for hospital IT) |
2d09150e |
Zero-config hospital customizations (13 dashboards, 40+ rules) |
8a6689ef |
Custom Wazuh decoders for medOS log formats |
36fe46f2 |
Expand to 80+ hospital-specific detections |
88e45710 |
Rebrand Metabase and Wazuh with medOS logos and colors |