Skip to content

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:

  1. Baseline builder — weekly job that computes statistical baselines (p50/p95) per entity per metric and writes them to PostgreSQL
  2. 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 CategoryTableKey Fields
Account logon behaviorSecurityEvent (EventID 4624)TargetUserName, LogonType, Computer, TimeGenerated
After-hours logonsSecurityEvent (EventID 4624)TargetUserName, TimeGenerated (compare hour to 07:00–19:00 PST baseline)
Outbound transfer volumeCommonSecurityLog (Palo Alto CEF)SourceIP, SentBytes, DestinationIP, TimeGenerated
Identity sign-in anomaliesSignInLogsUserPrincipalName, 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):

MetricDescription
logon_count_per_dayTotal network logons (Type 3) per calendar day
unique_hosts_per_dayNumber of distinct hosts logged into per calendar day
after_hours_logons_per_weekLogons outside 07:00–19:00 PST, counted per week

Per host (from SecurityEvent 4624 — received events):

MetricDescription
inbound_connection_count_per_dayDistinct source accounts connecting per day
unique_source_accounts_per_dayDistinct accounts that authenticated to this host per day

Per IP (from CommonSecurityLog — Palo Alto outbound):

MetricDescription
bytes_out_per_dayTotal SentBytes per calendar day
unique_destination_count_per_dayDistinct 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 TargetUserName

Longitudinal 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_multiplier

Threshold Multipliers (initial — tune after 4 weeks)

MetricMultiplierRationale
logon_count_per_day2.0Higher threshold — logon spikes have many benign causes
unique_hosts_per_day1.5Lateral movement signal — tighter threshold
after_hours_logons_per_weekN/A (any)Any after-hours logon when p95 = 0 is flagged
bytes_out_per_day1.5Exfil signal — tighter threshold
unique_destination_count_per_day2.0Higher 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_drift

Integration 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 detection

  • runbooks/uba-operations.md — day-to-day operations, tuning, false positive management
  • architecture/security-monitoring-architecture.md — full agent architecture
  • runbooks/secops-findings-triage.md — how UBA findings appear in SecOps

Internal use only — Cirius Group