Security Events
Security Events is a single, query-time view over everything security-relevant that happens in your workspace — policy decisions, egress blocks, approvals, exceptions, content-safety findings, and behavioral alerts — in one timeline instead of scattered across separate pages.
Where it lives
Security → Activity → Security in the admin console shows security events, filterable by event type, identity, severity, and time range. All activity correlates security events with gateway requests, sessions, and configuration changes. Gateway shows every evaluated request, with decision, tool or model, and request-type filters.
Selecting a row opens its detail drawer. If the full details cannot be loaded, the drawer shows a short note and the row's listed fields instead of an error.
Links to the old /audit URL redirect to Gateway
(/security/activity?tab=gateway) automatically. Legacy /security-events links
open the matching tab under Security → Activity.
Security incidents
GET /api/v1/security-incidents is a read-only Portal endpoint for administrators
and viewers that groups the same findings as GET /api/v1/security-findings into
incidents. It accepts the finding scope parameters (from, to, user, agent,
session, workload with cluster, source, detector, eventType, plane,
severity, search) plus status (open, the default, which is new and acknowledged;
new; acknowledged; resolved; or all), assignee (an admin email, me or
unassigned), disposition (true_positive, false_positive or benign), sortBy
(lastSeen, firstSeen, count, severity), sortOrder (asc or desc),
limit (1 to 100, default 20) and offset. Cursors are not supported. The range
is at most 31 days, the default is the preceding 24 hours, and an unknown value
returns 400. Scope and facet filters apply to member findings before grouping;
severity applies to an incident's highest severity. sortBy accepts lastSeen, firstSeen, count, severity and verdict; the default is verdict descending, which lists act_now first and likely_benign last, newest first within each. An explicitly empty or whitespace-only assignee filter returns 400; omit it to include all assignees.
The response has items, total (incidents), findingCount (findings in those
incidents), windowSeconds, sources and completeness. Each incident has:
| Field | Meaning |
|---|---|
id | Stable and URL-safe (inc1.). It encodes the actor, pattern, target and first finding time, so a new finding in the window does not change it. Treat it as carrying those values. |
groupingVersion | incident-window-v3. |
status | new, acknowledged or resolved. An incident nobody has acted on is new. |
triage | The stored triage state: status, assignee, disposition, acknowledgedAt, resolvedAt, version and follows. See Incident triage. |
verdict | What to do with the incident: kind (likely_benign, needs_review or act_now), a one-sentence reason (the Judge's own reasoning on the detail read when it decided the verdict), basis (the inputs used) and confidence (0 to 1). Derived from the Judge verdicts on member sequence matches (an injection-tainted verdict is never trusted), the highest severity, whether a control stopped the calls (a sequence match counts as stopped when every step that recorded a gateway decision was denied or held (deny, require_approval or warn_confirm); a step with no decision carried no action, and unreadable step records are never a stop), and, on the detail read when the loaded members are the whole incident (at most 10 findings), whether their evidence is readable, which lowers confidence only. A response_blocked finding is not counted as a control stop, because the upstream call already ran. act_now: a Judge escalation, critical severity, or high severity with no control stop. likely_benign: every trusted Judge verdict downgraded the match, none escalated, every member is a sequence match, severity medium or lower. Everything else is needs_review. |
actor | kind (user, agent, session, unknown), id (the display identity) and key, the full grouping actor, which keeps the cluster-qualified agent a user acts through: one user through agents in two clusters shares an id but not a key. |
resolvedActor | The newest finding's resolved actor, as on a finding: kind (user, agent_for_user, service_account, workload, unresolved), label, the user, agent, workload, cluster and session when known, and unknownReason when unresolved. actor is only the grouping key. A recorded cluster-qualified agent keeps its cluster in the key, including when a user delegates to it, so the same user or same-named agents in two clusters form separate incidents. agentLabel is the display form of agent (namespace/name for a cluster-qualified key). |
pattern | name, eventType, policy, plugin, entity, detectorKind and key, the full pattern dimension of the grouping key. Equal keys are the same pattern; the catalog title in knowledge is only a label and spans patterns with different policies. |
target | kind (workload, server), id and cluster, or null. |
firstSeen, lastSeen, findingCount | Bounds and size of the group. |
severity | Highest member severity, after the severity rules. |
latestFinding, latestFindingId, summary | The newest member, usable with the finding evidence routes. |
knowledge | Plain-English title, why, action and MITRE techniques for the pattern, or null. See the Security Finding Catalog. |
GET /api/v1/security-incidents/{id} returns the incident, latestControls (the control outcomes of its newest member finding, the same on every page of members), its related incidents (other incidents of the same grouping actor within 7 days, same pattern first, at most 5; a user acting through an agent in another cluster is a different actor and is never related) and its member findings,
newest first, with total. It accepts limit and offset and the same scope
parameters as the list (severity is ignored), so pass the list's scope and to to get
exactly the findings its row counted. The id supplies the start of the incident,
so from is not accepted. An id whose first finding is no longer retained returns 404.
The response also carries the list's sources and completeness for the same scope, and history, the incident's triage events, newest first (at most 100). A member's id opens
/api/v1/security-findings/{id}/evidence. An unknown or malformed id returns 404
or 400.
Bulk resolve
POST /api/v1/security-incidents/bulk-resolve is administrator-only, enforced by route middleware. The body has disposition (benign or false_positive), a required note (up to 2000 characters), and exactly one of incidentIds (1 to 200 ids, with an optional patternKey: an id of another pattern is skipped, so a pattern-scoped action cannot be widened) or filter (exactly one of pattern, an incident's full pattern.key (event type, policy, plugin, entity and template; never the catalog key, which two incidents of different policies share), or template, a sequence template id, which is what a security briefing noise group carries; optional actor, an incident's full actor.key (for example user:alice@acme.com, or user:alice@acme.com via k8s-cluster://c1/prod/runner for a user acting through an agent in one cluster; the display actor.id alone matches nothing); optional from and to, at most 31 days, default the last 7). A filter scans open incidents for the pattern; it resolves only incidents whose verdict is likely_benign and resolves at most 200 per call. The report's matched is how many likely-benign incidents the filter found and remaining how many are left for another call; the resolved ones leave the open list, so repeating the same request takes the next batch. The scan covers at most 5,000 open incidents in the range; narrow from and to or actor if a pattern is not reached. Incidents named by id are read first and skipped unless they are open and likely_benign.
Each incident is resolved by its own triage transaction, which writes the state, the history entry and the security_incident.resolved configuration audit event together, pinned to the version just read. One failure never rolls back the others. The 200 response has requested, resolved, skipped, failed, truncated, matched, remaining and results, one {id, status, reason} per incident (resolved, skipped or failed). A malformed body, a disposition other than the two allowed, a missing note, more than 200 ids, or both or neither of incidentIds and filter returns 400.
POST /api/v1/security-findings/{id}/notify-user is administrator-only and, like bulk resolve, requires Content-Type: application/json (otherwise 415). A subject-scoped incident read omits the Judge's reasoning from the verdict reason, because it summarizes context outside the scope. It sends one in-app notification, with the catalog's advice and no evidence, to the user the finding resolved to. It returns 422 when the finding has no recorded user and 409 (not_delivered) when no notice was delivered because in-app notices are disabled for the event or the same notice was just sent.
Incident summary
GET /api/v1/security-incidents/{id}/analyst-context is a read-only Portal endpoint that
gathers the context for one incident summary without a provider call. It requires to, the end of the range that produced the incident row, so a summary never reads later findings, and accepts
user, agent, session, workload, cluster, source, detector, eventType,
plane and search, the same scope the incident detail read uses. from and severity
are rejected. The response carries the incident, up to eight of its newest member
findings with a citation alias each, a totalMembers count, server-generated facts,
contextRevision and the Analyst generation state. Each member has an outcome (denied, blocked, held, redacted, flagged, allowed, withheld, unread or unrecorded), controlSteps (the persisted audit steps that recorded an outcome, at most 100) and controlsTruncated (more steps existed than were read, so a stronger outcome may be unread). A sequence finding's outcome is the strongest of its steps; withheld means a subject-scoped view cannot show its step outcomes and unread means its stored steps could not be fully read. incident.severity is the highest severity among the findings read, and recordedSeverity is the incident's recorded aggregate, which the summary did not verify. Each citation carries an opaque findingID handle and an opaque source alias, never a raw finding or source ID;
the incident's latestFindingId is blank. A control citation is the audit event that recorded a
member's control outcome. Open a cited member with GET /api/v1/security-findings/{handle}/evidence, passing the
same scope: the server resolves the handle under the caller's own incident read and returns 409
when the member has shifted. It requires to and ignores from; the subject, source, detector and search filters stay and severity is dropped. The evidence is read from the incident's own
first-finding time to to, clipped to the 31-day incident cap, so a request cannot read outside the incident's range.
POST /api/v1/security-incidents/{id}/analyst is administrator-only and makes one provider call, plus one automatic retry with a stricter prompt when the first answer fails validation. The body is
{ scope, expectedContextRevision, expectedConfigurationID, allowPaidGeneration: true }
with expectedEvidenceRevision: null. It returns 409 context_changed if the incident's
sampled members changed since the context was read. The model's output has a recommendation
(action of close_benign, escalate or contain, with text and citationIDs), a required confidence
(low, medium or high, capped at medium when not every finding was read), three
observations (what happened, blast radius, whether a control worked) and a nextStep;
output that fails validation, such as citing an ID outside the returned citations, is rejected
as invalid_output after the retry. The 502 body then carries result with the computed context,
failureReason (a validation code), retried, and a receipt costing both attempts under one invocation ID.
The context's generation.maxOutputTokens is the limit used for the call.
The prompt carries counts, times and fixed labels only, never finding summaries, subject or
policy names, or source identifiers.
A workload finding without a recorded cluster groups by namespace, kind and name. A sequence finding's pattern is its template and its recorded exemption flag, so an exempted detection forms its own incident.
MCP server inventory and runtime findings carry their latest observation time, so an incident made of them gets a new id when the observation advances; an old id then returns 404.
Findings join while consecutive findings with the same key are at most
windowSeconds (1800) apart. Grouping is computed when the request is read and
adds no stored data. Incident boundaries are computed within the requested range.
Resolving an incident also ends it: the first finding with the same key after the
resolve opens a new incident, even inside the window. Triage state is the only stored
data.
Incident triage
Administrators assign, acknowledge and resolve incidents. All three are POST
routes under /api/v1/security-incidents/{id} and are Admin-only: a viewer can read
the state but gets 403 on every action, and builder tokens are blocked at the portal
boundary. Each takes a JSON body and returns the incident's new triage object.
An optional expectedVersion (the triage.version you last read, 0 for an
incident nobody has acted on) makes the call fail with 409 if someone else changed
the incident first.
| Route | Body | Rules |
|---|---|---|
/assign | assignee (an admin email, or "" to unassign) | The assignee must uniquely identify an active admin, otherwise 400. Whitespace-only values return 400; unassignment requires the explicit empty string. Not allowed once resolved. Assigning to someone other than yourself adds an in-app notification and queues a best-effort email, subject to notification routing. SMTP delivery does not delay the assignment response; failure or a full delivery queue does not undo the assignment. |
/acknowledge | none | Only a new incident can be acknowledged. |
/resolve | disposition (true_positive, false_positive or benign), note and optional disputed (detection, guard_override or both) | The note is required for every disposition, at most 2,000 characters. disputed says what a false_positive or benign review disputes: the sequence detection, the Judge guard override, or both. It is rejected with true_positive and on the other routes. It is recorded in the history event, the configuration audit event and the triage record export, and changes nothing else: no guard, exemption, enforcement or suppression. A resolved incident cannot be changed. |
An action that does not apply to the incident's current state returns 409. An unknown field returns 400.
A stored triage record matches an incident of the same key whose span overlaps it, so a range-clipped read, whose incident starts later than the real first finding, still reports the recorded status and assignee. An unresolved record also carries over to a new incident that starts after its last triage action, for up to 31 days (the longest readable range); an incident that already existed before the action is never matched this way. A resolved record matches only findings up to its resolve time, plus 30 minutes for late arrivals.
A resolved incident is never reopened. Findings recorded at or before the resolve time stay in it, even if they arrive late. Later findings with the same actor, pattern
and target form a new incident. Its triage.follows names the resolved incident (id,
resolvedAt, disposition) only when its first finding is within the 30-minute grouping window of the resolved incident's last finding. Incidents separated by a longer natural gap are independent and have no follow-up link.
Every action is recorded in the incident's history and in the configuration
audit trail as security_incident.assigned, security_incident.unassigned,
security_incident.acknowledged or security_incident.resolved, with the actor, request and the before and after
status, assignee and disposition. Resolve notes appear only in history, not in the
configuration audit trail. Audit targets and assignment notification links use the same persisted incident ID as history, even when the selected time range clips the incident's beginning. See Configuration change evidence.
Exporting an incident to a SIEM is recorded as security_incident.exported, with the
trigger, destination host and outcome. See Security Incident OCSF Mapping.
Creating a Jira or ServiceNow ticket for an incident is recorded as security_incident.ticket_created, with the
trigger, destination host, outcome and, on success, the ticket key. See Security Incident Tickets.
Triage history is kept indefinitely by default. Set storage.securityTriageRetentionDays to
delete triage events and resolved incident records older than that many days (a
positive value below 62 is raised to 62).
Suppression rules
A suppression rule hides the findings it matches before incidents are formed.
| Route | Access | Notes |
|---|---|---|
GET /api/v1/security-suppressions?state=&limit=&offset= | Portal (admin and viewer) | state is active, expired or revoked; empty lists all, newest first. Each rule carries suppressedFindings, the findings it hid in the retained 31 day window. |
POST /api/v1/security-suppressions/preview | Admin-only | Body {matcher}. Returns matchedFindings and matchedGroups over the last 7 days. Writes nothing. |
POST /api/v1/security-suppressions | Admin-only | Body {matcher, reason, expiresAt} or {matcher, reason, durationDays}. Returns 201 with the rule. |
POST /api/v1/security-suppressions/{id}/revoke | Admin-only | Body {reason}, optional. 409 if the rule is not active. |
matcher has the string fields actor, eventType, policy, plugin, entity and target. At least one must be set, and every set field must match. actor and target are kind:id or kind:*; a user key also matches that user acting through a cluster agent. The matcher compares the incident's computed fields, and entity falls back to the detector kind when the finding has no entity.
A rule starts when it is created. reason is required, at most 500 characters. The expiry is required, must be in the future, and is at most 90 days from creation, else 400. At most 200 rules are active at once, and at most 500 may be active or have ended within the last 31 days, else 409. actor and target allow * only as the whole id (kind:*), else 400. A rule matches findings recorded in [startsAt, min(expiresAt, revokedAt)). Findings recorded in that window stay hidden after the rule ends; later findings form incidents as usual. Rule rows are never purged.
Each create and revoke is recorded as security_suppression.created or security_suppression.revoked in the configuration audit trail, in the same transaction. Builder and developer tokens get 403 on every route.
Triage metrics
GET /api/v1/security-triage/metrics?from=&to= is a Portal read. from and to are whole-second RFC 3339 timestamps, default the last 30 days, at most 366 days apart. It reports, for incidents first seen in [from, to):
| Field | Meaning |
|---|---|
mtta | First acknowledgement minus first finding, over incidents that were acknowledged. |
mttr | Resolve minus first finding, over resolved incidents. |
incidents, excludedSuppressed | Triaged incidents measured, and those left out because a suppression rule covered them while they were open. |
open, oldestOpenSeconds, openFrom | New and Acknowledged incidents read from the findings, over the most recent 31 days of the range, the age of the oldest, and the start of the window counted (the range start, or 31 days before its end for a longer range). |
mtta and mttr each have count, medianSeconds, p90Seconds (nearest rank) and meanSeconds, computed in the server. A range with more than 50,000 triaged incidents sets truncated.
Triage record export
GET /api/v1/security-triage/export?from=&to=&format=json|csv&includeNotes= is Admin-only, for SOX, HIPAA and DORA reviews. from and to are required whole-second RFC 3339 timestamps (a fractional second returns 400), at most 366 days apart. The routes refuse any other query parameter, a repeated one, and a credential in the URL (token), with 400. Both formats return the X-Triage-Record-Events and X-Triage-Record-Sha256 headers with the integrity values. A range with more than 50,000 incidents or 250,000 triage events returns 413.
The record holds incidents with a triage action whose first finding is in [from, to), with every action, and the suppression rules whose window overlaps the range. includeNotes defaults to false; without it resolve notes and revoke reasons are omitted. The JSON document has schemaVersion, generatedAt, generatedBy, from, to, groupingVersion, notesIncluded, incidents, suppressionRules and integrity. The CSV has one row per action: subject_type, subject_id, actor, pattern, target, first_seen, action, from_status, to_status, disposition, assignee, actor_email, occurred_at, rule_reason and rule_expires_at (filled for rule rows), history_purged (for incident rows), disputed (what a resolve disputed), and note only when notes are included. An incident with no remaining action has one row with an empty action. On a rule row, pattern is a JSON object holding the rule's eventType, policy, plugin and entity. Cells that could run as spreadsheet formulas are prefixed with an apostrophe.
integrity.events counts the events and integrity.sha256 is the SHA-256 of each event's compact JSON, one per line in file order (incidents' events, then rules' events), without HTML escaping. Each event carries subjectType and subjectId, and a rule's created event carries the rule's matcher, reason and expiresAt, so the hash covers which incident or rule an event belongs to. Incident summary fields and a rule's state are not hashed; they derive from the events. Retention records the latest cutoff at which it deleted events. An incident still open after a deletion appears with historyPurged: true when its first action is older than that cutoff, or when its state names an acknowledgement, assignment or resolve that has no event; its events then lack the deleted actions. It detects edits to the recorded history (the actions, their order, who took them and what each rule matched), not to the rest of the file; it is not a signature. The record is read in one snapshot. The export is recorded as security_triage.exported with the range, format, notes flag, counts and hash, and is refused if that record cannot be written.
Revoke agent access
Administrators can block the agent an incident or finding resolved to. All four routes are Admin-only, enforced by route middleware; the preview is Admin-only too because it lists credentials. See Contain an agent from an incident.
| Route | Purpose |
|---|---|
GET /api/v1/security-incidents/{id}/revoke-access/preview | What the revocation would cut off. |
POST /api/v1/security-incidents/{id}/revoke-access | Apply it. |
GET /api/v1/security-findings/{id}/revoke-access/preview | The same for a finding. Pass the drawer's from and to, as for the finding read. |
POST /api/v1/security-findings/{id}/revoke-access | Apply it to a finding. |
The server reads the incident or finding itself and resolves the identity from its stored actor. The incident routes take the same scope parameters as the incident detail read (to, user, agent, session, workload, cluster, source, detector, eventType, plane, search; not from, paging or cursors, which return 400), because the actor is the newest member's and must be the one the drawer shows. The finding routes take the finding read's scope. The body of a POST is identityId, the identity.id of the preview, agentId (the preview's identity.agentId) and an optional reason (at most 280 characters, shown to the blocked agent) an optional revokeKeys (default false) and, with it, keyIds, the virtualKeys ids from the preview: key revocation is permanent, so it covers exactly the keys shown and a different set returns 409 (preview_changed). identityId and agentId only confirm the preview: if either differs from what the server resolves now, the call returns 409 (preview_changed), and any other field is rejected with 400. The request needs Content-Type: application/json, otherwise 415.
The preview has supported, and when false a reasonCode and a reason sentence: person_account (a person's own account or credential), workload_only, no_identity, identity_not_found, ambiguous_identity, partial_block_exists (the agent already has a narrower Identities-page block, which is never widened in place), existing_block (a switched-off Identities-page block exists and is never taken over), cluster_unproven (the incident recorded a cluster but the identity row cannot be tied to it), or shared_agent_id (an agent with no recorded cluster shares its id with a cluster, or any spelling is used in another namespace, or a matching identity has unknown cluster provenance; listed in sharedClusters). When supported it has kind (agent, client for a Cross-App Access governed agent, or workload), the identity (id, the Identities page route; label; agentId; cluster), the active virtualKeys and agent-owned activeSessions (using the same activity, expiry, degraded-state and idle-window rules as the session list; XAA client sessions cannot be attributed to a single client and are not counted), sameAgentIdentities (other identities of the cluster that carry the agent id and are cut off too), delegating and user for an agent acting for a person, clientIds for a client block, boundAgents for a workload block, restoreOnPolicy when the block has no identity page and is restored from the Policies page, and blocked with the block policy name. A POST for an actor that is not supported returns 422 with the same code and preview; an agent that is already blocked returns 409 (already_revoked).
The action creates the Identities page Block: an enabled, enforce-mode policy named identity-block-<slug>-<hash> (category Identity, source security:security_incident:<id> or security:security_finding:<id>) that denies every MCP and LLM call, and HTTP egress where network egress filtering is on, whose object.agent.agentId is any spelling of the identity's agent id (runner and prod/runner are one identity, so both are matched), or whose object.agent.clientId is the governed agent's client id and object.agent.agentId the delegating person (the same client for another person is untouched), and, for an agent with a recorded cluster, whose object.agent.clusterId is that cluster. A workload block matches the agents bound to the workload in its cluster (seen in the last 24 hours with a pod identity) and is named identity-block- with a hash of the workload. A block this action created and someone disabled is turned back on; a block made from the Identities page, narrower or switched off, is never rewritten or taken over, because Unblock would then delete it. Credentials are never revoked or deleted. Unblock on the Identities page, or deleting the policy, restores the agent. This replica applies the policy at once; other replicas apply it on their next policy sync (policy.cacheTTLSeconds, 60 seconds by default).
With revokeKeys true the agent's active virtual keys in that cluster are revoked in the same transaction, each with its own virtual_key.revoked event, and the response lists them in revokedKeys. A revoked key is not restored by Unblock.
The policy, its revision, the incident's triage history entry (revoke_access, incidents only) and one configuration audit event commit in one transaction. The event is security_incident.access_revoked or security_finding.access_revoked, targeted at the incident or finding, with the identity, agent id and policy name in after. It changes no triage status or version, but creates the incident's stored triage record when it has none.
Recorded findings needing attention
GET /api/v1/security-findings/attention-summary is a read-only Portal endpoint
for administrators and viewers. It returns whole-set counts and at most three
prioritized retained findings; it does not invoke the Analyst or fetch payload
blobs. Developer credentials cannot read organization-wide evidence.
Optional from and to are whole-second UTC timestamps with an exclusive end;
the default is the preceding 24 hours. Exact user, agent, session, or
workload with cluster scopes are echoed in the response. Unsupported source
scope is reported explicitly and never falls back to a broader identity query.
Pagination, caller-selected limits and event facets are not accepted here.
counts.recorded counts source-preserving findings, not unique attacks or
requests. It equals eligible + followUp + suppressed. Sequence exemptions,
dismissals and valid untainted persisted downgrades suppress default attention;
acknowledgment moves to follow-up. Baseline accepted-normal reviews suppress
attention, while accepted risk and unclassified acknowledgment remain follow-up.
Missing or inconsistent native review facts remain eligible with explicit
unknown reasons. These dispositions do not establish that behavior was resolved.
outcomes partitions the same recorded findings into recorded request denials,
response blocks, historical holds, latest runtime-series blocks and unknown
outcomes. A historical hold is not a current pending approval. A runtime series
counter is not a count of individually blocked requests. Contradictory retained
facts remain unknown; source limitations and ingestion freshness are separate.
Ranking considers the complete scoped set before selecting three: high or
critical findings without a recorded block or hold, then those with one, then
other severities. Severity, source timestamp and stable source-qualified IDs
break ties. The response supplies rankingVersion, native review facts and
reason codes. Failed or timed-out reads return an error without counts, rather
than reporting zero findings.
Security briefing
GET /api/v1/security-briefing?from=<time>&to=<time> is a read-only Portal endpoint for
administrators and viewers. It returns the computed security briefing
for one window and makes no model call. from and to are required whole-second UTC
timestamps with an exclusive end, up to 31 days apart. Other parameters, including user,
agent and facet filters, are rejected: the briefing always covers the whole deployment.
The response contains:
| Field | Meaning |
|---|---|
verdict | The headline, a detail line, the number of items that need attention, the findings in noise groups, open incident counts, and incidentsRead, which is false when the incident read failed. |
needsAttention | Up to five items, each with incidentId, findingId, a plain-English title, the calibrated severity, reason, blocked, likelyBenign, the Judge counts in judge, and one action. moreNeedsAttention counts the rest. |
noise | Groups of probably-benign patterns, at most four. Each has kind (judge_benign or baseline), counts, the Judge's judged and benign counts, the top actors, a filter for the incidents list, disposition, and up to 100 incidentIds. moreNoise counts the groups not listed. |
changes | findings, requests and denials, each with current, previous, change and an explanation, and policyChanges with a count that is null when the configuration audit could not be read. |
controls | denied, responsesBlocked, held, topDenials, and runtime with a state of not_applicable, available, degraded, unavailable or unknown, and warning. |
coverage | Plain-language limits that affect a conclusion. An empty list means none does. |
Each section is read independently. When a read fails, its directly sourced nullable values are null (for example the affected change/control metrics and policyChanges.count), while incident-derived verdict counts remain zero with verdict.incidentsRead: false; lists are empty and the reason is added to coverage. A failed read never fails the response or reports zero as a successful read. The Judge counts are those of each incident's own attack-pattern matches in the window, and count a downgrade only when the evidence is not
injection-tainted. The briefing reads at most 1,000 open incidents, highest severity first, and
says so in coverage when it stops short. The response is never cached.
Events routed elsewhere
Correlated findings carry security signals only. Setup problems and spend events are recorded where they belong and never listed as findings, including rows stored before this rule.
| Source | Reason code or kind | Destination |
|---|---|---|
| Gateway denial | no-upstream-key (and its older name pat-no-upstream-key), passthrough-not-allowed, passthrough-auth-mismatch, model-provider-mismatch, model-alias-missing, upstream-ambiguous, upstream-format-mismatch | An issue on the Gateway tab of Health, with the count of refused requests in the last 15 minutes. Source gateway:config:<reason>. The denied calls stay in the audit log |
| Gateway denial | model-budget, token-budget, session-budget, session-token-limit, session-request-limit, virtual-key-spend-cap, budget.model-cap | Cost and budget views. The denied calls stay in the audit log. In Activity, model-budget and budget.model-cap appear as Budget rule and link to Budgets → Rules, with no policy-edit action |
| Session alert | session_budget_warning, session_cost_alert | Cost, and the in-app notification sent to the developer and admins. The alert stays in the session's anomaly list |
Correlated finding evidence fields
Each correlated finding reports whether its retained evidence can be read back.
| Field | Meaning |
|---|---|
evidenceStatus | readable, partial, or unavailable. A sequence finding is unavailable when no steps were stored, the stored evidence cannot be decoded, or no step holds readable content. |
evidenceReason | The reason code for partial or unavailable, or an empty string. |
sourceSeverity | The calibrated severity before the evidence cap, when the finding's severity was capped to medium, or an empty string. |
controls[].policy | The policy that decided the step, or an empty string when none was recorded. |
controls[].decision | The recorded gateway decision, such as allow, deny, or require_approval, or an empty string. |
An unavailable finding is a signal, not a confirmed incident. Findings recorded
before these fields existed have no stored decision or policy for their steps.
The severity filter, Home attention candidate selection and counts, incident severity and ordering, and the Analyst aggregates use the same capped severity as the finding, for attack patterns, audit findings and Content Safety detections. A finding whose evidence cannot be read back does not match a high-severity filter and does not outrank a readable finding. A finding recorded before the evidence tracking existed is capped until a background job qualifies it; its evidenceReason is evidence_qualification_pending.
All sequence rows share one list evidence-read byte budget. Rows that cannot fit are marked partial until opened individually. Shortened retained request/response content is also partial. Direct audit control outcomes use the response policy for blocked, flagged or redacted responses unless the request was denied; a missing response policy remains unknown.
Judge guard explanation
When a guard replaces a Judge downgraded recommendation, the finding's effective verdict is confirmed and its assessment carries a guard object (judgeGuard on the session, agent and workload attack-pattern matches). It is null when no guard replaced the recommendation. The assessment's verdict and confidence are the effective outcome and the model's own confidence. The original recommendation is in guard.
| Field | Meaning |
|---|---|
recorded | false for a hold from before explanations were recorded: only guard, originalVerdict, originalConfidence and originalReasoning are filled, and the rule, step and excerpt are empty. They are never reconstructed from evidence that may have changed. |
guard | approval_directive_downgrade (an approval-style claim or text addressed to the reviewer in the evidence, or an approval claim in the model's reasoning) or injection_taint (the evidence or the stored match carries an injection finding). |
path | Where the override happened: initial_assessment, stored_downgrade_reguard (a stored downgrade checked again on a later sweep) or persisted_injection_taint. |
rule, term | The matcher: approval_claim, reviewer_directive, reasoning_approval_claim, injection_findings or persisted_injection_taint, and the kind of match inside it, such as change_or_incident_ticket or classify_as_benign. |
step, field | The 1-based evidence step (0 when not tied to one) and the field that matched, such as arguments, response_excerpt or destination. |
before, match, after | A short excerpt around the match. It is bounded, and credentials and personal data are masked. Injection findings have no excerpt. |
encodings | The decoding layers applied before the match, percent and base64, outermost first. Empty when the match was in plain text. |
originalVerdict, originalConfidence, originalReasoning | What the model recommended before the guard replaced it. |
GET /api/v1/security-findings/{id}/evidence also returns incidentId for a sequence finding: the incident that holds it, found with the same grouping, suppression rules and resolve cuts the incident views use, over the 31 days up to the finding. It is null when no incident holds the finding, and for other finding types. The id starts at the incident's first finding in that range, and its triage state matches by overlap, as for any range-clipped incident id.
The explanation is stored when the guard decides. Like the rationale, guard is null on a subject-scoped evidence read (a user, agent, session or workload filter), because the model's reasoning and the excerpt come from context the scope can't prove. The session, agent and workload match endpoints are already scoped to their subject and return judgeGuard. It is never read back to decide anything. An incident whose Judge downgrade was held has the needs_review verdict with a reason that says a guard held it, not that the Judge confirmed the pattern.
Detection spans on evidence items
Each evidence item returned by GET /api/v1/security-findings/{id}/evidence carries spans and spanState, so a client can highlight why a detector fired inside text.
| Field | Meaning |
|---|---|
spans[].offset, spans[].length | Position of the span in text, counted in Unicode code points (not bytes or UTF-16 units). Spans never overlap and always lie inside text. |
spans[].detector, spans[].rule | The detector that fired, such as injection-guard, and its rule or entity, such as JAILBREAK_PATTERN or AWS_KEY. |
spans[].label | A short label combining the detector and rule. |
spans[].kind | matched_text when the span is the detector's own match (injection, jailbreak), or masked when it is a placeholder for a secret or PII value. The original value is never returned. |
spanState | recorded (spans present), not_recorded (the finding predates span recording), not_located (no location was recorded, for example a detector that judges the whole text), not_retained (no excerpt was stored because sample retention was off or the scan was past its limit of 32 excerpts), or not_applicable (the item is not a detection excerpt). |
spans is always an array. Content-safety findings store the spans the detector reported, measured against the stored, redacted excerpt. For gateway events and attack-pattern steps no detector offsets are stored, so spans lists the redaction markers found in the text, with the redacted type as the rule. A marker is text in the redactor's placeholder format, so text a user typed in that format is listed too; the value behind a real marker is not recoverable.
Event types
Each event's severity follows the severity rules.
| Event type | Meaning |
|---|---|
policy_audit | An Audit-mode violation with a selected intent-policy name. A name match does not verify built-in policy provenance. Shown as Policy finding (Audit); the finding does not establish blocking or execution. |
policy_denial | An MCP tool call or LLM request was blocked by a policy. Gateway configuration denials and budget denials are not listed; see Events routed elsewhere |
egress_denied | An outbound HTTP(S) call was blocked by network egress filtering — the always-on SSRF floor or an egress policy |
approval_required | A request was paused and parked in the Approvals queue pending human review |
hitl_approved | A reviewer approved a held request in the Approvals queue. Records who decided, not how the call itself was later dispositioned |
hitl_denied | A reviewer denied a held request in the Approvals queue, or a self-approval attempt was rejected |
exception_requested | A developer requested a config exception (a temporary deviation from an enforced setting) |
exception_granted | An exception request was approved, in full or in part |
exception_denied | An exception request was rejected |
exception_revoked | A previously granted exception was revoked before it expired |
exception_expired | A granted exception reached its expiry without being revoked |
content_safety_finding | The content-safety scanner detected PII, a credential, or a prompt-injection pattern in a prompt, tool argument, or response |
session_anomaly | A session's behavior deviated from its established baseline (unusual tool sequence, volume, or timing). Budget and cost alerts are not listed |
baseline_alert | An identity's behavioral baseline itself changed enough to raise an alert (drift in normal usage patterns) |
ungoverned_mcp_server | An MCP server received traffic through the proxy but was never registered — see MCP Governance |
unregistered_workload_blocked | The gateway denied MCP or LLM calls from a Kubernetes workload that has no registration. Shown as Unregistered workload blocked. See Unregistered workloads |
unregistered_workload_shadowed | The gateway allowed MCP or LLM calls from an unregistered Kubernetes workload because the mode is Audit. Shown as Unregistered workload (Audit) |
runtime_dns | The kernel observed a workload resolve a DNS name. See the caveats below — a resolution is not a connection |
runtime_network | The kernel observed a workload open a network connection to a destination |
runtime_open | The kernel observed a workload open a file path |
runtime_exec | The kernel observed a workload execute a binary |
runtime_network_blocked | An enforce-mode runtime policy denied a workload's network connection. Shown as Network connection blocked |
runtime_open_blocked | An enforce-mode runtime policy denied a workload's file access. Shown as File access blocked |
runtime_exec_blocked | An enforce-mode runtime policy denied a workload's binary execution before it ran. Shown as Execution blocked |
runtime_blocked | An enforce-mode runtime policy denied an attempt for a sensor behavior that has no dedicated blocked type |
runtime_sensor_outage | The runtime sensor's continuity broke: the sensor was unhealthy on a node, AIControls couldn't list every node, or runtime events weren't reaching AIControls. One event per outage, dated at its start, in the Runtime facet. The summary says (ongoing) or (recovered after N min), so the event records both the start and the recovery. Severity is high while the outage is open and medium after recovery. See Runtime sensor outages |
Runtime sensor outages
runtime_sensor_outage isn't a kernel observation. It records a gap in sensor coverage: while the sensor is down, traffic that bypasses the proxy can't be seen. Workload readiness rule R6 fails closed for workloads whose current instance overlaps the outage, and the outage also appears as a Runtime sensor row on the Health page. AIControls sends admins a Runtime Sensor Outage notification when the outage starts and a Runtime Sensor Recovered notification when it ends. Both use the Runtime Sensor Outage event in Settings → Notifications, where you choose the in-app, email, Slack, and Teams routes. See Make a workload ready to register.
Runtime observations
The runtime_dns, runtime_network, runtime_open, and runtime_exec types are kernel observations, not policy decisions. Nothing was denied; the sensor recorded that something happened. They appear only when the runtime integration is enabled, and they carry the Runtime scope facet.
The *_blocked types record attempts that an enforce-mode runtime policy denied: the finding has mode=enforce and result=fail, and the kernel refused the operation (for example, with Operation not permitted). AIControls never labels a blocked attempt with the allowed-activity type, so a denied exec reads Execution blocked, not Binary executed. The row shows a Deny badge and a blocked tag, and the detail drawer's header and Outcome field say Blocked. A monitor-mode finding, which records what an enforcing policy would have denied, keeps the allowed-activity type and reads Allowed in the drawer. Severity still ranks the behavior's evidence strength; the outcome is carried in the event type.
In the Security tab's table, the Subject column shows who or what the event is about. For gateway events it shows the agent, with the user's email beneath it. For runtime events it shows the workload as a link to its detail page, or unattributed when AIControls couldn't resolve the pod's owner. The Target field in a runtime row is the file path, destination, or binary the sensor saw.
In the detail drawer of a runtime event, administrators can select Exclude namespace to stop observing that namespace. AIControls drops its future runtime events and deletes the runtime events already stored for it. Manage the list in Settings → Workloads. A workload's Runtime tab also lists its recent runtime events, with a View in Activity link.
Each runtime row is a finding series, and its time and label describe the series' latest occurrence. A series keeps its identity when its policy switches from monitor to enforce, so it can hold both outcomes. When it does, the drawer labels the field Latest outcome and adds All occurrences, for example "2 blocked, 4 allowed."
Three things about them are easy to misread:
- A resolution is not a connection.
runtime_dnsproves a workload looked a name up. It does not prove any traffic followed. It is strictly weaker evidence thanruntime_network, and the UI marks it as such — never treat the two as interchangeable. - Suppressed destinations are not bypasses. Cluster DNS, the API server, in-cluster traffic, package registries and service-mesh control planes are suppressed from bypass accounting, and each suppressed row states its reason in the detail drawer. AIControls' own upstream calls are excluded too — the gateway is an in-cluster hop, not a workload bypassing governance.
- No findings is not "clean". When the detail drawer reports a non-zero ingest lag, kernel observations recorded by the sensor are not yet (or never will be) reflected in stored findings. A lost observation is indistinguishable from a workload that did nothing, so treat quiet workloads as possibly incomplete.
When you list pod label keys in runtimeControls.findingLabels, each runtime event carries those labels as workloadMetadata, read by AIControls from the Kubernetes API. The detail drawer shows them as Workload labels, and the Workload label box on the Security tab filters by one, such as ebay-app-id=checkout.
Aggregate views — per-node sensor coverage, and the per-workload kernel-vs-gateway reconciliation — are not events and stay on their own page. Clicking a workload there opens Security → Activity → Security filtered to that workload's observations.
Unregistered workloads
AIControls records new unregistered workload events only in self-managed installations, because Workload Trust isn't available on Nirmata-hosted workspaces. Events recorded before a change to hosted mode can still appear. See Feature availability.
When the unregistered workload mode applies to a call, AIControls records it under the Inventory scope facet. Calls roll up: one event covers every call from the same workload and identity, to the same model or tool, with the same outcome, on the same UTC day. The event's time is the first such call, and its summary counts them, for example "Blocked LLM call to openai/gpt-4o from unregistered workload shop/Deployment/recommender (42 calls)". A workload that retries in a loop therefore adds one event a day, not one per call.
Each event carries:
| Field | Value |
|---|---|
| Workload | <namespace>/<kind>/<name> of the calling workload, in the cluster the gateway attributed it to |
| Identity | The calling ServiceAccount identity |
| Target | provider/model for an LLM call, or the server and tool for an MCP call |
| Outcome | Blocked in Enforce, or Not blocked (Audit) in Audit; both are medium severity. Another policy can still deny the call |
| Policy | workload_not_registered |
AIControls keeps these rolled-up rows for 90 days after the last call. The per-call deny audit rows stay on Gateway and in the audit log, so the full record is kept. They don't also appear as policy_denial events. Admins can open an event and select Register workload to start the registration wizard for that workload.
Exception lifecycle events (exception_*) and approval decisions (hitl_approved, hitl_denied) are workflow bookkeeping, not call dispositions — see Quiet confidence for how these are excluded from that metric. In particular, a reviewer denying a held request is not counted as a denial against that reviewer in deny-rate figures or the "most-denied user" ranking in the admin digest.
API
curl "$YOUR_WORKSPACE_URL/api/v1/security-events" \
-H "Authorization: Bearer $ADMIN_TOKEN"
Requires an admin or viewer role. Supports the same identity, decision, event-type, and time-range filters as the UI.
| Query parameter | Effect |
|---|---|
eventType | Comma-separated event types from the table above |
plane | Comma-separated scope facets: mcp, llm, egress, content, session, inventory, runtime |
severity | Comma-separated severities |
agent, session | Exact-match identity filters |
workload | Exact-match Kubernetes workload key (<namespace>/<kind>/<name>). Runtime events, unregistered-workload rollups, workload sequence matches, and audit findings with a recorded workload tuple can carry one. Audit findings are restricted to the gateway's trusted cluster; a missing cluster does not broaden their scope |
label | Workload label as <key>=<value>, such as label=ebay-app-id=checkout. Repeat it to require several labels. Matches only runtime_* events whose workload carries every label, so events from other planes are left out while it is set. Only labels listed in runtimeControls.findingLabels are recorded |
unregistered | Set to current to keep only unregistered-workload events (unregistered_workload_blocked and unregistered_workload_shadowed) whose workload has no active registration now. The match uses the workload and its cluster, so a registration in one cluster doesn't hide the same workload name in another. An automatic registration marked inactive doesn't count. Events of other types are left out. Any other value, including an empty one, returns 400. The Home page uses it for the unregistered-agent callout |
search | Case-insensitive substring match across the summary and policy name |
from, to | ISO date (2026-08-01) or RFC 3339 timestamp |