Appearance
UBA Architecture — User Behavioral Analytics
Purpose: Design specification for the UBA baseline builder and longitudinal anomaly detection agent.
Audience: Developer building or operating UBA agents for bedrock-hub. Status: Planned — not yet deployed. See runbooks/uba-operations.md for operations once deployed.
Overview
UBA operates in two phases:
- Baseline builder — weekly job that computes statistical baselines (p50/p95) per entity per metric and writes them to PostgreSQL
- Longitudinal agent — daily agent that reads baselines and flags current activity that exceeds the p95 threshold by a configurable multiplier
UBA findings are behavioral signals, not direct attack indicators. They are severity HIGH at most. SecurityAgent correlates UBA findings with kill chain findings to amplify severity — a UBA anomaly on the same account that the lateral movement agent flagged is a strong compound signal.
Data Sources
All data lives in cirius-logging-law-central (workspace ID 5d76d1f2, rg-logging-logs, logging subscription).
| Metric Category | Table | Key Fields |
|---|---|---|
| Account logon behavior | SecurityEvent (EventID 4624) | TargetUserName, LogonType, Computer, TimeGenerated |
| After-hours logons | SecurityEvent (EventID 4624) | TargetUserName, TimeGenerated (compare hour to 07:00–19:00 PST baseline) |
| Outbound transfer volume | CommonSecurityLog (Palo Alto CEF) | SourceIP, SentBytes, DestinationIP, TimeGenerated |
| Identity sign-in anomalies | SignInLogs | UserPrincipalName, Location, DeviceDetail, ConditionalAccessStatus |
Baseline Schema
Table: uba_baselines in psql-secops-prod
sql
CREATE TABLE uba_baselines (
id SERIAL PRIMARY KEY,
entity_type VARCHAR(20) NOT NULL, -- 'account' | 'host' | 'ip'
entity_value VARCHAR(255) NOT NULL,
metric VARCHAR(50) NOT NULL,
p50 FLOAT NOT NULL,
p95 FLOAT NOT NULL,
sample_days INTEGER NOT NULL, -- days used to compute; < 10 means unreliable
computed_at TIMESTAMP NOT NULL DEFAULT NOW(),
source_system VARCHAR(10) NOT NULL -- 'PROD' | 'DDE' | 'AWS'
);
CREATE UNIQUE INDEX uba_baselines_entity_metric_source
ON uba_baselines (entity_type, entity_value, metric, source_system);The unique index allows INSERT ... ON CONFLICT DO UPDATE (upsert) — each weekly run overwrites the prior baseline for the same entity/metric/source.
Baseline Builder
File: agents/uba/uba_baseline_builder.pySchedule: Weekly, Sunday 02:00 UTC via Container Apps Job Data window: Last 30 days, excluding the most recent 7 days
The 7-day exclusion prevents an active attack from polluting the baseline. If lateral movement is ongoing for a week, you do not want the baseline to normalize to "attacker behavior."
Metrics Computed
Per account (from SecurityEvent 4624):
| Metric | Description |
|---|---|
logon_count_per_day | Total network logons (Type 3) per calendar day |
unique_hosts_per_day | Number of distinct hosts logged into per calendar day |
after_hours_logons_per_week | Logons outside 07:00–19:00 PST, counted per week |
Per host (from SecurityEvent 4624 — received events):
| Metric | Description |
|---|---|
inbound_connection_count_per_day | Distinct source accounts connecting per day |
unique_source_accounts_per_day | Distinct accounts that authenticated to this host per day |
Per IP (from CommonSecurityLog — Palo Alto outbound):
| Metric | Description |
|---|---|
bytes_out_per_day | Total SentBytes per calendar day |
unique_destination_count_per_day | Distinct destination IPs per day |
Minimum Data Requirement
Entities with fewer than 10 data points (10 days with at least 1 event) are skipped — insufficient data for a meaningful statistical baseline. The longitudinal agent also skips entities where sample_days < 10.
KQL Skeleton (account logon_count_per_day)
kql
SecurityEvent
| where TimeGenerated > ago(30d) and TimeGenerated < ago(7d)
| where EventID == 4624
| where LogonType == 3
| where TargetUserName !endswith "$"
| extend Day = bin(TimeGenerated, 1d)
| summarize LogonCount = count() by TargetUserName, Day
| summarize p50 = percentile(LogonCount, 50), p95 = percentile(LogonCount, 95),
SampleDays = dcount(Day)
by TargetUserNameLongitudinal Agent
File: agents/uba/uba_longitudinal_agent.pySchedule: Daily, 02:30 UTC via Container Apps Job
The agent queries the last 24 hours of activity, computes the current value for each metric, and compares against the stored baseline. A finding is raised when:
current_value > p95 * threshold_multiplierThreshold Multipliers (initial — tune after 4 weeks)
| Metric | Multiplier | Rationale |
|---|---|---|
logon_count_per_day | 2.0 | Higher threshold — logon spikes have many benign causes |
unique_hosts_per_day | 1.5 | Lateral movement signal — tighter threshold |
after_hours_logons_per_week | N/A (any) | Any after-hours logon when p95 = 0 is flagged |
bytes_out_per_day | 1.5 | Exfil signal — tighter threshold |
unique_destination_count_per_day | 2.0 | Higher threshold — new destinations common |
Severity
All UBA findings are severity HIGH. UBA is a behavioral signal, not a direct attack indicator. SecurityAgent correlates UBA with kill chain findings — a UBA HIGH combined with a kill chain CRITICAL on the same entity becomes compound evidence.
ioc_key Format
{source_system}:account:{entity_value}:uba_logon_count_drift
{source_system}:account:{entity_value}:uba_unique_hosts_drift
{source_system}:ip:{entity_value}:uba_bytes_out_drift
{source_system}:host:{entity_value}:uba_inbound_connection_driftIntegration With Kill Chain
When SecurityAgent processes a cycle where both a kill chain agent and the UBA agent flagged the same entity (same entity_value in the ioc_key), SecurityAgent should treat the combination as amplified evidence. This is currently handled in SecurityAgent's prompt context — no code change needed in the UBA agent.
The correlation logic: if the evidence field of a kill chain CRITICAL finding and a UBA HIGH finding both reference the same account or host, SecurityAgent will escalate the compound finding even if it would have held the UBA finding in the analyst queue otherwise.
Known Limitations
- Baselines are meaningless for entities with fewer than 10 sample days (new accounts, newly enrolled devices). These are explicitly skipped
- The 7-day exclusion window means a very recent attack that normalized behavior over several weeks will still have a polluted baseline — this is a known gap, accepted in favor of reducing false positives
- UBA does not cover SaaS-native events (SharePoint access patterns, M365 DLP) — those are handled by Arctic Wolf MDR
- DDE environment (Medicare app) uses the same UBA infrastructure but with separate baselines (source_system='DDE')
File Structure
agents/
uba/
__init__.py
uba_baseline_builder.py — weekly baseline computation
uba_longitudinal_agent.py — daily drift detectionRelated Documents
runbooks/uba-operations.md— day-to-day operations, tuning, false positive managementarchitecture/security-monitoring-architecture.md— full agent architecturerunbooks/secops-findings-triage.md— how UBA findings appear in SecOps