Skip to content

UBA Operations — User Behavioral Analytics

Purpose: Day-to-day operations for the UBA baseline builder and longitudinal anomaly detection agent.

Audience: Rory Related architecture: architecture/uba-architecture.md


Overview

UBA produces HIGH-severity findings in SecOps when an account, host, or IP deviates significantly from its historical baseline (p50/p95 over the prior 30 days). Findings are behavioral signals — correlate them with kill chain findings before escalating.

The two components:

  • Baseline builder — runs Sunday 02:00 UTC, writes p50/p95 per entity to uba_baselines table in psql-secops-prod
  • Longitudinal agent — runs daily 02:30 UTC, compares last 24 hours against baselines, posts findings

Checking Baseline Health

Connect to PostgreSQL:

bash
psql -h psql-secops-prod.postgres.database.azure.com \
     -U secops_admin -d secops

Credentials in Keeper → Service Accounts → psql-secops-prod.

sql
-- Recent baselines for PROD accounts
SELECT entity_value, metric, p50, p95, sample_days, computed_at
FROM uba_baselines
WHERE source_system = 'PROD'
  AND entity_type = 'account'
ORDER BY computed_at DESC
LIMIT 50;

-- Entities with insufficient data (< 10 days)
SELECT entity_type, entity_value, metric, sample_days, source_system
FROM uba_baselines
WHERE sample_days < 10
ORDER BY sample_days;

-- Check when baselines were last computed
SELECT source_system, MAX(computed_at) AS last_run
FROM uba_baselines
GROUP BY source_system;

If last_run is more than 8 days ago, the baseline builder failed on last Sunday's run. Check Container Apps Job logs (below).


Checking Agent Logs

Container Apps Job logs (Azure Portal):

  1. Azure Portal → Resource Group rg-logging-logs → Container App ca-secops-prod
  2. Logs → query for the relevant job execution:
kql
ContainerAppConsoleLogs
| where TimeGenerated > ago(2d)
| where ContainerName contains "uba"
| project TimeGenerated, ContainerName, Log
| order by TimeGenerated desc

Or from az CLI:

bash
az containerapp logs show \
  --name ca-secops-prod \
  --resource-group rg-logging-logs \
  --follow

Responding to UBA Findings

UBA findings arrive in SecOps with source = uba_longitudinal_agent. The description states which metric exceeded the threshold and by how much (e.g., "Account jsmith: unique_hosts_per_day = 12, baseline p95 = 4.2, threshold exceeded by 2.9×").

Triage steps:

  1. Open the finding — note entity (account/host/IP), metric, current value, baseline p95
  2. Check for a CM ticket covering that entity: GET /api/changes/recent or SecOps → Change Management
  3. Check Entra SignInLogs for the account — is the logon from a known device and location?
    kql
    SignInLogs
    | where TimeGenerated > ago(24h)
    | where UserPrincipalName == "<account>"
    | project TimeGenerated, UserPrincipalName, AppDisplayName, DeviceDetail,
        Location, ConditionalAccessStatus, RiskLevelDuringSignIn
    | order by TimeGenerated desc
  4. Check whether any kill chain agents fired on the same entity in the same cycle. If yes — treat as compound evidence and escalate to Rory
  5. If the deviation has a plausible legitimate explanation (user returned from vacation, mass provisioning, software deployment): add a known-good rule with an expiry date

Tuning Thresholds

Thresholds live in agents/uba/uba_longitudinal_agent.py as module-level constants. To change a threshold:

  1. Edit the constant in the agent file
  2. Open a PR against bedrock-hub main
  3. CI runs and validates the change
  4. Rory reviews and merges
  5. The new threshold takes effect on the next daily run

Do not raise thresholds globally to suppress a specific noisy account — use a known-good rule for that account instead. Global threshold changes affect everyone.

Review thresholds after the first 4 weeks of production data. Adjust based on:

  • False positive rate (too many benign deviations flagged)
  • False negative rate (check whether the agent would have caught a real attack in historical data)

Resetting a Polluted Baseline

If a major change polluted the baseline (mass user onboarding, infrastructure migration, a past attack that ran for weeks):

sql
-- Delete baselines for a specific entity
DELETE FROM uba_baselines
WHERE entity_value = 'jsmith@ciriusgroup.com'
  AND source_system = 'PROD';

-- Delete all baselines for a source system (full rebuild)
DELETE FROM uba_baselines
WHERE source_system = 'PROD';

After deletion, trigger the baseline builder manually to recompute:

bash
az containerapp job start \
  --name job-orchestrator-cirius \
  --resource-group rg-logging-logs \
  --subscription <logging-subscription-id>

The manual run uses the same 30-day lookback window (excluding last 7 days).


Manually Triggering a Daily Run

bash
az containerapp job start \
  --name job-orchestrator-cirius \
  --resource-group rg-logging-logs \
  --subscription <logging-subscription-id>

The job runs all agents including uba_longitudinal_agent. Results appear in SecOps within ~10 minutes of the job starting.


False Positive Management

If a specific account consistently triggers UBA findings for legitimate reasons (a heavy-use IT account, a batch job account):

  1. Do NOT raise the global threshold
  2. Add a known-good rule in SecOps scoped to that account's ioc_key pattern:
    • Pattern: PROD:account:jsmith@ciriusgroup.com:uba_*
    • Expiry: set an expiry — if behavior genuinely changes, you want to know
  3. Document the known-good rule rationale in SecOps

New Accounts and Devices (Insufficient Baseline)

New accounts will have sample_days < 10 and the longitudinal agent will skip them. After the entity has been active for 30+ days, the next Sunday baseline run will compute a valid baseline.

There is no UBA coverage for new accounts in their first 30 days. For high-risk new accounts (privileged users, service accounts), monitor manually via Entra SignInLogs and the kill chain agents until a baseline is established.


  • architecture/uba-architecture.md — schema, KQL, agent design
  • runbooks/secops-findings-triage.md — SecOps triage workflow
  • runbooks/known-good-rule-management.md — managing suppression rules

Internal use only — Cirius Group