Appearance
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_baselinestable 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 secopsCredentials 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):
- Azure Portal → Resource Group
rg-logging-logs→ Container Appca-secops-prod - Logs → query for the relevant job execution:
kql
ContainerAppConsoleLogs
| where TimeGenerated > ago(2d)
| where ContainerName contains "uba"
| project TimeGenerated, ContainerName, Log
| order by TimeGenerated descOr from az CLI:
bash
az containerapp logs show \
--name ca-secops-prod \
--resource-group rg-logging-logs \
--followResponding 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:
- Open the finding — note entity (account/host/IP), metric, current value, baseline p95
- Check for a CM ticket covering that entity:
GET /api/changes/recentor SecOps → Change Management - 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 - 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
- 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:
- Edit the constant in the agent file
- Open a PR against
bedrock-hubmain - CI runs and validates the change
- Rory reviews and merges
- 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):
- Do NOT raise the global threshold
- 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
- Pattern:
- 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.
Related Documents
architecture/uba-architecture.md— schema, KQL, agent designrunbooks/secops-findings-triage.md— SecOps triage workflowrunbooks/known-good-rule-management.md— managing suppression rules