Insider Threat Behavioral Analytics
27-category anomaly detection engine covering human admin behavior and autonomous agent/A2A threats.
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.
| Group | Detectors | Actor |
|---|---|---|
| Access | 5: off-hours, weekend, new IP, impossible travel, new user agent | Human |
| Privilege | 5: escalation, role modification, shadow admin, API key spree, breakglass abuse | Human |
| Data exfiltration | 4: bulk export, bulk read, PII access spike, audit log tampering | Human |
| Behavioural | 4: velocity spike, dormant reactivation, scope widening, credential hoarding | Human |
| Agent and A2A | 9: scope creep, velocity spike, lateral movement, trust gaming, credential harvesting, delegation abuse, exfiltration tunnel, dormant reactivation, off-schedule execution | Autonomous 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_atis 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_usedvalue 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_atand 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.
| Field | Written by an analysis run | Meaning |
|---|---|---|
normal_hours_start, normal_hours_end | no | Hour range for expected activity, defaults 8 and 18 |
avg_daily_actions | yes | Window action count divided by the window in days |
max_daily_actions | yes | The window's action count |
known_ip_hashes | yes | IP digests seen in this window, capped at 50 |
known_user_agents | yes | User-agent strings seen in this window, capped at 20 |
computed_at | yes | When 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
analyzewith 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.
| Key | Default | Applies to |
|---|---|---|
velocity_sigma | 3.0 | Human velocity spike multiplier |
dormant_days | 30 | Human dormant reactivation |
api_key_spree_threshold | 5 | API key creations per window |
breakglass_abuse_threshold | 2 | Breakglass actions per window |
pii_spike_threshold | 50 | PII decryption events per window |
bulk_export_threshold | 3 | Exports per window |
bulk_read_threshold | 500 | Reads per window |
agent_velocity_threshold | 200 | Agent calls per window |
agent_dormant_days | 14 | Agent dormant reactivation |
agent_credential_threshold | 3 | Credentials 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
- Anatomy of an A2A Handshake: How Two AI Agents Establish Trust Without a Shared Secretsecurity · 13 min read
- Behavioral Fingerprinting for Agents: Detecting Model Substitution in Production Pipelinessecurity · 17 min read
- The Identity Stack for Autonomous Commerce: When Your AI Agent Needs a Credit Linesecurity · 14 min read