Insider Threat Behavioral Analytics

27-category anomaly detection engine covering human admin behavior and autonomous agent/A2A threats.

Last reviewed September 16, 2026Fresh

Overview

Insider threat analytics scans a tenant's audit log and agent activity against stored baselines and reports anomalies for investigation. It runs when someone asks it to: there is no continuous stream and no scheduled job.

Twenty-seven detectors in five groups. The first eighteen read audit log rows and describe human actors; the last nine read agent activity and delegate records.

GroupDetectorsActor
Access5: off-hours, weekend, new IP, impossible travel, new user agentHuman
Privilege5: escalation, role modification, shadow admin, API key spree, breakglass abuseHuman
Data exfiltration4: bulk export, bulk read, PII access spike, audit log tamperingHuman
Behavioural4: velocity spike, dormant reactivation, scope widening, credential hoardingHuman
Agent and A2A9: scope creep, velocity spike, lateral movement, trust gaming, credential harvesting, delegation abuse, exfiltration tunnel, dormant reactivation, off-schedule executionAutonomous agent

Reachability. Console only. shield-insider-threat is not in the public allowlist, so https://api.passkeybridge.io/v1/shield-insider-threat answers 404 with x-pb-reason: unknown-function. The dashboard calls it with the session JWT.

Authorization. A user JWT whose membership row for the tenant has the admin role, then an Enterprise plan check. The tenant is resolved from that admin membership rather than from the request body, and the lookup expects exactly one row: a user who is an admin of more than one tenant resolves to none and gets 403 Admin access required.

Detection engine. The detectors are pure functions that take rows and return findings. All reads and writes happen in the edge function, which is why each detector is independently testable and why the thresholds below are the whole of the logic.

Human insider threat detectors (1–18)

These read shield_audit_log rows for the window, grouped per actor, and compare against that actor's baseline where one exists. Rows with no actor are skipped.

Access anomalies.

  • off_hours_access. Actions outside the baseline's hour range. Fires at 3 or more, critical at 10 or more. The range defaults to 08:00 to 18:00 UTC.
  • weekend_access. Actions on a Saturday or Sunday in UTC. Fires at 3 or more, severity info, critical at 15 or more.
  • new_ip_address. IP digests absent from the baseline's known set. Requires an existing baseline with at least one known digest, so it never fires on an actor's first analysis. Critical at 3 or more new digests.
  • impossible_travel. Two consecutive actions whose names contain login, from different IP digests, less than five minutes apart. Always critical, risk 0.95, and at most one finding per actor per run.
  • new_user_agent. User-agent strings absent from the baseline. Warning at 3 or more, otherwise info.

Privilege anomalies.

  • privilege_escalation. A permission change whose target is the actor themselves. Always critical, risk 0.95.
  • role_modification. Three or more permission changes in the window that were not self-targeted. Warning.
  • shadow_admin_creation. Creating a member or role record whose recorded change sets the role to admin. Always critical, risk 0.9.
  • api_key_spree. API key creations at or above the threshold, default 5. Critical at twice the threshold.
  • breakglass_abuse. Breakglass actions at or above the threshold, default 2, counted over the analysis window rather than a fixed day. Always critical.

Data exfiltration.

  • bulk_export. Export or download actions at or above the threshold, default 3.
  • bulk_read. Read, select or query actions at or above the threshold, default 500.
  • pii_access_spike. Decryption or PII access events at or above the threshold, default 50. Always critical, risk 0.9.
  • audit_log_tampering. Any attempt to update, delete, purge or truncate audit log records. Always critical, risk 1.0.

Behavioural anomalies.

  • velocity_spike. Window action count above the baseline daily average multiplied by the sigma setting, default 3. Requires a baseline with a positive average.
  • dormant_reactivation. Fires when the baseline's computed_at is older than the dormant threshold, default 30 days, and the actor has any activity now. It measures how long since this actor was last analysed, so a tenant that runs analysis rarely will see it on actors who were never dormant.
  • scope_widening. Eight or more distinct resource types touched in the window, critical at 12.
  • credential_hoarding. Five or more passkeys, credentials, API keys or agent delegates created, critical at 10.

Agent / A2A threat detectors (19–27)

These read shield_agent_activity and shield_agent_delegates. Two are structural and need no activity at all.

Structural, from delegate records.

  • agent_scope_creep. An active delegate holding scopes absent from its original_scopes. Critical at 3 or more added scopes.
  • agent_delegation_abuse. Two findings from one detector. A sub-delegate holding a scope its parent lacks is critical and breaks delegation monotonicity, at most one per parent. Separately, a parent with 5 or more sub-delegates is a warning about an opaque trust graph.

From activity rows.

  • agent_velocity_spike. Calls by one delegate at or above the threshold, default 200 for the window. Critical at triple. The evidence names the top five action types driving it.
  • agent_lateral_move. A delegate using a scope_used value absent from its current scopes. Always critical, risk 0.95, one finding per delegate listing every unauthorised scope.
  • agent_trust_gaming. Five or more attestation, negotiation, renewal or trust actions, and at least three of them less than 60 seconds apart. Always critical.
  • agent_credential_harvest. Create, register or generate actions at or above the threshold, default 3. Critical at triple.
  • agent_exfil_tunnel. Three or more handshake, A2A or negotiate actions where either more than half failed or at least two targeted a delegate outside the tenant's known set. Always critical.
  • agent_dormant_reactivation. The gap between a delegate's last_used_at and its first activity in this window is at or above the threshold, default 14 days. Critical at double.
  • agent_off_schedule. Five or more actions between 01:00 and 05:00 UTC. Critical at 20 or more.

Agents have no baseline table. Their expected behaviour is the delegation record itself: scopes, original scopes, last use and trust score, compared against what the activity rows show.

Behavioral baselines

One row per actor per tenant in shield_insider_baselines, upserted at the end of every analysis run.

FieldWritten by an analysis runMeaning
normal_hours_start, normal_hours_endnoHour range for expected activity, defaults 8 and 18
avg_daily_actionsyesWindow action count divided by the window in days
max_daily_actionsyesThe window's action count
known_ip_hashesyesIP digests seen in this window, capped at 50
known_user_agentsyesUser-agent strings seen in this window, capped at 20
computed_atyesWhen the run wrote this row

Two properties follow from that table and both change how the detectors behave.

The hour range is fixed unless you edit the row. No action writes normal_hours_start or normal_hours_end, so every baseline keeps the 08:00 to 18:00 UTC default and off-hours findings are measured against it. Changing it means updating the row directly.

The known sets are replaced, not accumulated. Each run overwrites known_ip_hashes and known_user_agents with the values from that window alone. An address seen in yesterday's run and absent from today's is no longer known, so it will read as new the next time it appears. Run analysis over a window wide enough to cover normal activity if you want the new-IP and new-user-agent detectors to be quiet.

The upsert keys on tenant and actor, so there is exactly one baseline per actor per tenant. Baselines are counted on the summary and listed, 50 at a time, by the baselines action.

Edge function: shield-insider-threat

Six actions, all POST with the session JWT through supabase.functions.invoke("shield-insider-threat", { body }). With no action the function defaults to summary.

`analyze` takes window_hours, default 24, capped at 168. It fetches five things in parallel: audit log rows for the window, agent activity for the window, all delegates, all baselines and the tenant's active policy. The two activity sources are each ordered oldest first and capped at 1,000 rows, so on a busy tenant the cap truncates the most recent end of the window. Analyse a shorter window rather than trusting a 7 day one to see everything.

New findings are deduplicated against existing open or investigating anomalies for the same actor and type: a match is updated in place with the fresh evidence and risk score, and only a genuinely new pair is inserted. Baselines are then recomputed, and the run is audit-logged as insider_threat_analyze.

{
  "anomalies_detected": 4,
  "human_anomalies": 3,
  "agent_anomalies": 1,
  "anomalies_inserted": 2,
  "entries_scanned": 812,
  "window_hours": 24,
  "results": []
}

results carries every finding in full, including ones that updated an existing row, which is why it can exceed anomalies_inserted.

`anomalies` returns up to limit rows, default 100, ordered by risk score descending, with optional status and severity filters. An unrecognised filter value is ignored rather than rejected.

`resolve` takes anomaly_id, a status of open, investigating, resolved or dismissed, and optional notes. Resolving or dismissing stamps the resolver and the time. Every transition is audit-logged as insider_anomaly_[status].

`baselines` returns up to 50 baselines, newest computation first. `policies` returns every policy row for the tenant. `summary` returns the counts the dashboard tiles show:

{
  "total_anomalies": 37,
  "open_anomalies": 9,
  "avg_risk_score": 0.742,
  "by_severity": { "info": 4, "warning": 21, "critical": 12 },
  "by_status": { "open": 7, "investigating": 2, "resolved": 26, "dismissed": 2 },
  "baselines_computed": 14
}

avg_risk_score averages open and investigating anomalies only, so it does not move when historical findings are resolved.

Errors. 401 Missing authorization header with no bearer token; 401 Unauthorized when the token does not resolve; 403 Admin access required when the caller has no single admin membership; 403 with plan_required below Enterprise, or 402 payment_required on an Enterprise plan whose billing entitlement is revoked; 400 Invalid JSON body; 400 anomaly_id required and 400 status must be one of: open, investigating, resolved, dismissed on resolve; 400 Unknown action; 500 Failed to fetch audit log, Query failed or Update failed when a database call fails.

Insider threat tab

Dashboard, Insider threat. Enterprise only; a lower plan sees the upgrade gate instead of the contents, and the tab makes no network call in that state.

  • Run analysis. The header button. It calls analyze with a fixed 24 hour window; the window is not adjustable from the dashboard. The toast reports findings and entries scanned.
  • Tiles. Open anomalies, Critical, Average risk score as a percentage, and Baselines.
  • Filters. Two rows of buttons, one for status and one for severity, applied client side to the 100 rows already loaded.
  • Anomaly table. Severity badge, the finding's title with its category label underneath, the first 8 characters of the actor id, a risk bar, status, detection date, and per-row controls to view or update. Columns hide progressively on narrow screens.
  • Detail dialog. Category, full description, risk score, actor, detection time, and a collapsible Raw evidence block with the detector's evidence JSON.
  • Update status dialog. A status selector limited to Investigating, Resolved and Dismissed, plus a notes field. Confirm calls resolve.

Every anomaly type has a human label, so agent_lateral_move reads as Agent Lateral Movement. An unmapped type would fall back to its raw identifier.

Verify a run landed. After Run analysis, the toast reports the count, the Open anomalies and Baselines tiles move, and new rows appear at the top of the table ordered by risk. In the audit log, an insider_threat_analyze row records the window, entries scanned, and the human and agent counts.

Configuration

Thresholds live in shield_insider_policies as a thresholds JSON object. Any key you omit keeps its default.

KeyDefaultApplies to
velocity_sigma3.0Human velocity spike multiplier
dormant_days30Human dormant reactivation
api_key_spree_threshold5API key creations per window
breakglass_abuse_threshold2Breakglass actions per window
pii_spike_threshold50PII decryption events per window
bulk_export_threshold3Exports per window
bulk_read_threshold500Reads per window
agent_velocity_threshold200Agent calls per window
agent_dormant_days14Agent dormant reactivation
agent_credential_threshold3Credentials an agent creates per window

One policy is used. The analysis reads a single active policy row for the tenant and takes its thresholds. If several rows have is_active set, the extra rows are ignored rather than merged, and which one wins is not specified. Keep exactly one active policy per tenant.

There is no write action. policies reads; nothing in the function creates or updates a policy. Insert or update the row directly, which row-level security permits for tenant admins on their own tenant.

Every threshold above is a count within the analysis window rather than a rate per day or per hour. Because the window is 24 hours from the dashboard and up to 168 hours through the API, the same policy is effectively stricter on a longer window. The breakglass threshold in particular reads as 2 per window rather than 2 per day.

Related from the blog