# threats (/docs/reference/api/threats)

> Identity threat detection and response: attacks found in the audit trail, incidents, identity risk, and responses.



Identity threat detection and response: attacks found in the audit trail, incidents, identity risk, and responses.
Detection rules find them, incidents group them for investigation, a risk score per identity feeds policies, and
administrators respond by hand or with playbooks. Password sprays,
guessing, stolen sessions, replayed tokens, account takeover, self-granted administrator access, mass deletions,
weakened security settings, and audit tampering all leave audit events; the `iam.detectThreats()` job reads them,
raises detections, and groups them into one incident per subject. The repository guide is `docs/threat-detection.md`.

## How detection works [#how-detection-works]

Detection is a scheduled job, [`iam.detectThreats()`](/docs/reference/api#detectthreats) (CLI
[`detect-threats`](/docs/reference/cli#detect-threats)), not a hook in the request path. For each active tenant it
reads the audit events recorded since its last run from a per-tenant cursor, verifies their hash chain (a break is an
`audit-tampering` detection), evaluates the tenant's rules over them and the history the rules' windows need, and
records what it finds. Detection is therefore as late as the job's interval and never slows a sign-in, and it sees
every writer's events: the authentication service, SCIM, protocols, and deployment jobs.

* **Rules.** Twenty-one built-in rules, each with a severity, a subject type, a MITRE ATT\&CK technique, and (for counting
  rules) a threshold and window a tenant can tune within bounds. [`rules`](#rules) lists them with the setting in force.
* **Detections** are recorded once each: a rule's unique key is its triggering event or its burst, so re-reading
  events never raises a second one. Each carries evidence: up to 20 audit event ids with counts, the time span, and
  the networks, identities, and actions involved.
* **Incidents** group detections by subject (an identity, a network, the tenant, or a SCIM connection). A subject has
  at most one open or investigating incident; resolving it closes it, and the next detection opens a new one.
* **Subjects and networks.** Networks are client addresses reduced to an IPv4 address or an IPv6 /64. Addresses are
  known only when the deployment records them (`http.clientInfo`). Networks in the tenant's `trustedNetworks` are
  never reported and never blocked.
* **Actors.** Detections, incidents, and playbook responses are recorded by the actor `threat-detection`; everything
  done through this group by the person who did it.

## Identity risk [#identity-risk]

A detection about an identity adds points by severity (low 10, medium 25, high 50, critical 80) that halve every
`riskHalfLifeHours` (24 by default). The score is what remains, at most 100; the level is `none` below 10, `low` from
10, `medium` from 40, and `high` from 70. Scores are computed when read, so they decay without a writer. An
administrator can set a floor with [`setRisk`](#setrisk); dismissing a detection, or resolving its incident as a false
positive or benign, takes its points out. Nobody clears risk raised about themselves: detections about you are
dismissed, and your own false-positive incidents closed, by someone else (root administrators excepted), and nobody
sets their own level.

Policies read the effective values as `principal.riskLevel` and `principal.riskScore` (see
[principal keys](/docs/guides/authorization/conditions#principal-keys)). A delegated session carries the higher of the
person's and the agent's risk; simulated principals (invariants, impact previews, simulation, and package-rule
automation) always see `none` and 0.

## Responses and protections [#responses-and-protections]

The five response actions are `revoke-sessions` (API keys kept unless `keepApiKeys: false`), `forget-devices`,
`contain` (the identity is disabled until [`release`](#release); its API keys are kept but refused meanwhile),
`block-network` (a tenant network block for `durationMs`, one minute to 30 days, one day by default), and `notify`
(the `threat-alert` email to the tenant's `notify` recipients). People take them with [`respond`](#respond);
playbooks take them for new detections that match their trigger.

* People cannot contain themselves or the last owner, contain an owner unless they are an owner of the tenant in
  person or root, end a root administrator's sessions or contain one unless they are root, block a network that
  covers their own address, or block a network on the root tenant unless they are root.
* Playbooks never contain owners or root administrators and never block networks on the root tenant (skipped as
  `protected`), contain at most
  `maxAutomaticContainments` identities per tenant and run (skipped as `braked` beyond it, audited once as
  `threat:response-braked`), and email an incident again only when its severity rose.
* Nobody blocks a trusted network (skipped as `trusted-network`).

Every action taken or skipped is kept as a response on the incident's timeline. Shared Signals receivers get
`threat:revoke-sessions` as CAEP `session-revoked` and `threat:contain` as RISC `account-disabled`.

## Who may call what [#who-may-call-what]

Reading needs `iam:threats:read`, tuning and triage `iam:threats:manage`, and responding `iam:threats:respond`. They
are checked on `iam/threats/rules`, `iam/threats/settings`, `iam/threats/detections`, `iam/threats/incidents`,
`iam/threats/risk`, `iam/threats/playbooks` (with `/{id}` for one record), and `iam/threats/responses`. Every call
is audited under its permission (reads included), and changes also record the `threat:*` event each method names.
[`reportSuspicious`](#reportsuspicious) needs no permission: it acts on the caller's own account only.

| Method                                  | What it does                                                                                                                | Access     |
| --------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | ---------- |
| [`addNote`](#addnote)                   | Adds an investigation note to an incident.                                                                                  | Credential |
| [`configure`](#configure)               | Tunes detection for the tenant: rules, trusted networks, dormancy, risk decay, alert recipients, and the containment brake. | Credential |
| [`createPlaybook`](#createplaybook)     | Defines an automatic response: actions that run when a new detection matches the trigger.                                   | Credential |
| [`deletePlaybook`](#deleteplaybook)     | Deletes a playbook; responses it already took stay on their incidents.                                                      | Credential |
| [`detect`](#detect)                     | Runs detection for the tenant now instead of waiting for the scheduled job, and returns what the run did.                   | Credential |
| [`dismissDetection`](#dismissdetection) | Marks an open detection as a false alarm and takes its points out of the identity's risk.                                   | Credential |
| [`getDetection`](#getdetection)         | Returns one detection with its evidence.                                                                                    | Credential |
| [`getIncident`](#getincident)           | Returns an incident with its detections, notes, responses, and the risk of the identity it concerns.                        | Credential |
| [`getRisk`](#getrisk)                   | Returns one identity's risk: score, level, override, containment, and the contributions behind them.                        | Credential |
| [`getSettings`](#getsettings)           | Returns the tenant's detection settings with every default applied.                                                         | Credential |
| [`listDetections`](#listdetections)     | Lists the tenant's detections, newest first, optionally narrowed by status, rule, severity, identity, incident, and time.   | Credential |
| [`listIncidents`](#listincidents)       | Lists the tenant's incidents, most recently active first, optionally narrowed by status, severity, assignee, and identity.  | Credential |
| [`listPlaybooks`](#listplaybooks)       | Lists the tenant's playbooks by name.                                                                                       | Credential |
| [`listRisk`](#listrisk)                 | Lists the identities at or above a risk level, and contained ones, highest score first.                                     | Credential |
| [`release`](#release)                   | Lifts a containment, so the identity is active again and its API keys work.                                                 | Credential |
| [`reportSuspicious`](#reportsuspicious) | Lets a person report activity on their own account that was not them: it raises an incident and signs out everywhere else.  | Credential |
| [`resolveIncident`](#resolveincident)   | Closes an incident as a true positive, a false positive, or benign.                                                         | Credential |
| [`respond`](#respond)                   | Responds by hand to an incident, an identity, or a network with one to five response actions.                               | Credential |
| [`rules`](#rules)                       | Lists every detection rule with its defaults, tuning bounds, and the setting in force for the tenant.                       | Credential |
| [`setRisk`](#setrisk)                   | Sets an identity's risk level by hand, as a floor that holds until it expires, or clears it.                                | Credential |
| [`summary`](#summary)                   | Returns the tenant's threat posture at a glance for a dashboard.                                                            | Credential |
| [`timeline`](#timeline)                 | Returns an identity's recent audit trail, newest first: what it did and what was done to it.                                | Credential |
| [`updateIncident`](#updateincident)     | Moves an incident between open and investigating, assigns it, or changes its severity.                                      | Credential |
| [`updatePlaybook`](#updateplaybook)     | Changes a playbook's name, description, trigger, actions, or whether it is enabled.                                         | Credential |

## addNote [#addnote]

Adds an investigation note to an incident.

**HTTP:** `POST /api/iam/threats/addNote` (requires a credential) · **Browser client:** `client.threats.addNote()`

* **Permission:** `iam:threats:manage` on `iam/threats/incidents/{id}`.
* **Audited as:** `iam:threats:manage` and `threat:note` (metadata: `noteId`, `length`; never the text).
* **Errors:** `INVALID_INPUT` for a body that is empty or over 4000 characters; `LIMIT_EXCEEDED` (409) past 500 notes on
  the incident; `NOT_FOUND` when the incident is not in this tenant.

Notes may span several lines (line breaks are normalized, other control characters dropped) and can be added to
resolved incidents too. No recent sign-in is needed, so an investigator can keep writing during a long incident.
[`getIncident`](#getincident) returns them oldest first.

```ts title="Signature"
iam.api.threats.addNote(
  credential: CredentialInput,
  input: { tenantId: string; incidentId: string; body: string },
): Promise<ThreatNote>
```

## configure [#configure]

Tunes detection for the tenant: rules, trusted networks, dormancy, risk decay, alert recipients, and the containment brake.

**HTTP:** `POST /api/iam/threats/configure` (requires a credential) · **Browser client:** `client.threats.configure()`

* **Permission:** `iam:threats:manage` on `iam/threats/settings`, and a recent sign-in.
* **Audited as:** `iam:threats:manage` and `threat:settings` (metadata: `changed`, `changedRules`, `disabledRules`,
  `weakened`); turning a rule off or weakening detection otherwise also records a `guardrail-weakened` detection
  against the caller.
* **Errors:** `ACCESS_DENIED` for shortening `riskHalfLifeHours` while detections raise the caller's own risk (root
  administrators excepted); `INVALID_INPUT` for an unknown rule or setting, a `threshold` or `windowMs` outside the
  rule's `tunable` bounds (or for a rule that has none), `everyone` on a rule other than `new-network`, more than 50
  trusted networks or one that is not an address or CIDR block or is wider than /8 (IPv4) or /32 (IPv6), `dormantDays`
  outside 7 to 3650,
  `riskHalfLifeHours` outside 1 to 720, more than 20 or malformed `notify.emails`, or `maxAutomaticContainments`
  outside 0 to 100; `RECENT_AUTH_REQUIRED`.

Fields left out keep their value. `rules` is merged per rule: each rule takes `enabled`, `severity`, `threshold`,
`windowMs`, and (for `new-network`) `everyone`, where `null` restores that field's default and `null` for the whole
rule drops every adjustment. `trustedNetworks` replaces the list. `notify` names who the `notify` response emails:
`owners` (active owners with a verified email) and up to 20 `emails`. The result is the settings with every default
applied, as [`getSettings`](#getsettings) returns them. Because the detection engine never reads the module's own
events, weakening detection is reported here, as a `guardrail-weakened` detection that runs the tenant's playbooks:
turning a rule off; a higher threshold, a shorter window, or a lower severity on a rule that stays on; `new-network`
no longer reporting everyone; a shorter risk half-life; a longer dormancy period; a newly trusted network; a lower
`maxAutomaticContainments`; or fewer incident recipients. Its metadata lists them as `weakened` (for example
`rules.brute-force.threshold`, `trustedNetworks`).

```ts
await iam.api.threats.configure(credential, {
  tenantId,
  rules: { 'brute-force': { threshold: 20 }, 'new-network': { everyone: true } },
  trustedNetworks: ['203.0.113.0/24'],
  notify: { owners: true, emails: ['soc@acme.test'] },
});
```

```ts title="Signature"
iam.api.threats.configure(
  credential: CredentialInput,
  input: ThreatSettingsInput & { tenantId: string },
): Promise<ThreatSettingsView>
```

## createPlaybook [#createplaybook]

Defines an automatic response: actions that run when a new detection matches the trigger.

**HTTP:** `POST /api/iam/threats/createPlaybook` (requires a credential) · **Browser client:** `client.threats.createPlaybook()`

* **Permission:** `iam:threats:manage` on `iam/threats/playbooks`, and a recent sign-in.
* **Audited as:** `iam:threats:manage` and `threat:playbook-create` (metadata: `name`, `enabled`, `trigger`,
  `actions`).
* **Errors:** `INVALID_INPUT` for a missing or over-long name, a missing `trigger`, an unknown rule, severity, or
  subject type, or `actions` that are not one to five known kinds each listed once (`durationMs` only on
  `block-network`, one minute to 30 days; `keepApiKeys` only on `revoke-sessions`); `CONFLICT` (409) when a playbook
  has the same name (case-insensitive); `LIMIT_EXCEEDED` (409) past 50 playbooks; `RECENT_AUTH_REQUIRED`.

The trigger matches when every clause it sets matches: `ruleIds`, `minSeverity`, and `subjectTypes`; `trigger: {}`
matches every detection. Actions run in order as the `threat-detection` actor, under the protections in
[Responses and protections](#responses-and-protections), and playbooks run in the order they were created. Playbooks
are the only automatic source of alert emails. A new playbook is enabled unless `enabled: false`.

```ts
await iam.api.threats.createPlaybook(credential, {
  tenantId,
  name: 'Contain taken-over accounts',
  trigger: { ruleIds: ['session-hijack', 'account-takeover-persistence', 'user-reported'] },
  actions: [{ kind: 'contain' }, { kind: 'notify' }],
});
```

```ts title="Signature"
iam.api.threats.createPlaybook(
  credential: CredentialInput,
  input: ThreatPlaybookInput & { tenantId: string },
): Promise<ThreatPlaybook>
```

## deletePlaybook [#deleteplaybook]

Deletes a playbook; responses it already took stay on their incidents.

**HTTP:** `POST /api/iam/threats/deletePlaybook` (requires a credential) · **Browser client:** `client.threats.deletePlaybook()`

* **Permission:** `iam:threats:manage` on `iam/threats/playbooks/{id}`, and a recent sign-in.
* **Audited as:** `iam:threats:manage` and `threat:playbook-delete` (metadata: `name`).
* **Errors:** `NOT_FOUND` when the playbook is not in this tenant; `RECENT_AUTH_REQUIRED`.

To stop a playbook for a while and keep it, set `enabled: false` with [`updatePlaybook`](#updateplaybook) instead.

```ts title="Signature"
iam.api.threats.deletePlaybook(
  credential: CredentialInput,
  input: { tenantId: string; playbookId: string },
): Promise<{ deleted: true }>
```

## detect [#detect]

Runs detection for the tenant now instead of waiting for the scheduled job, and returns what the run did.

**HTTP:** `POST /api/iam/threats/detect` (requires a credential) · **Browser client:** `client.threats.detect()`

* **Permission:** `iam:threats:manage` on `iam/threats/detections`.
* **Audited as:** `iam:threats:manage`, plus whatever the run records (`threat:detection`, `threat:incident-open`,
  playbook responses) by `threat-detection`.

It is exactly one tenant's share of [`iam.detectThreats()`](/docs/reference/api#detectthreats): it reads the unread
events from the tenant's cursor (at most 2000), so it is safe beside the scheduled job and never records anything
twice. The result is `{ tenants, eventsScanned, detections, incidentsOpened, responses, braked, chainBreaks, pending }`.
Use it behind a "Check now" button or in tests.

```ts title="Signature"
iam.api.threats.detect(
  credential: CredentialInput,
  input: { tenantId: string },
): Promise<ThreatDetectionRun>
```

## dismissDetection [#dismissdetection]

Marks an open detection as a false alarm and takes its points out of the identity's risk.

**HTTP:** `POST /api/iam/threats/dismissDetection` (requires a credential) · **Browser client:** `client.threats.dismissDetection()`

* **Permission:** `iam:threats:manage` on `iam/threats/detections/{id}`, and a recent sign-in.
* **Audited as:** `iam:threats:manage` and `threat:detection-dismiss` (metadata: `ruleId`, `severity`, `reason`,
  `identityId`, `incidentId`), plus `threat:risk-change` when the identity's level drops.
* **Errors:** `INVALID_TRANSITION` (409) when the detection is not open; `ACCESS_DENIED` when it is about the caller
  and the caller is not a root administrator; `INVALID_INPUT` for a missing `reason` or one over 512 characters;
  `NOT_FOUND`; `RECENT_AUTH_REQUIRED`.

The detection keeps its place in its incident, marked `dismissed` with who, when, and why. The incident stays open,
even when nothing open is left in it, until someone resolves it with [`resolveIncident`](#resolveincident).

```ts title="Signature"
iam.api.threats.dismissDetection(
  credential: CredentialInput,
  input: { tenantId: string; detectionId: string; reason: string },
): Promise<ThreatDetection>
```

## getDetection [#getdetection]

Returns one detection with its evidence.

**HTTP:** `POST /api/iam/threats/getDetection` (requires a credential) · **Browser client:** `client.threats.getDetection()`

* **Permission:** `iam:threats:read` on `iam/threats/detections/{id}`.
* **Audited as:** `iam:threats:read`.
* **Errors:** `NOT_FOUND` when the detection is not in this tenant.

A detection names its `ruleId`, `severity`, `title` and one-sentence `summary`, its `subject`, the `identityId` whose
risk it raised and the `network` it came from when there are ones, its `incidentId`, `status` (`open`, `dismissed`,
or `resolved`), when the activity happened (`occurredAt`) and when it was recorded (`detectedAt`), rule-specific
`metadata`, and `evidence` with up to 20 audit event ids to open in the audit log.

```ts title="Signature"
iam.api.threats.getDetection(
  credential: CredentialInput,
  input: { tenantId: string; detectionId: string },
): Promise<ThreatDetection>
```

## getIncident [#getincident]

Returns an incident with its detections, notes, responses, and the risk of the identity it concerns.

**HTTP:** `POST /api/iam/threats/getIncident` (requires a credential) · **Browser client:** `client.threats.getIncident()`

* **Permission:** `iam:threats:read` on `iam/threats/incidents/{id}`.
* **Audited as:** `iam:threats:read`.
* **Errors:** `NOT_FOUND` when the incident is not in this tenant.

The result is `{ incident, detections, notes, responses, risk }`: up to 200 detections newest first, notes oldest
first, responses (applied and skipped, by people and playbooks) newest first, and, for an incident about an identity,
that identity's risk as [`getRisk`](#getrisk) returns it. Everything an investigator needs for one page.

```ts title="Signature"
iam.api.threats.getIncident(
  credential: CredentialInput,
  input: { tenantId: string; incidentId: string },
): Promise<IncidentDetail>
```

## getRisk [#getrisk]

Returns one identity's risk: score, level, override, containment, and the contributions behind them.

**HTTP:** `POST /api/iam/threats/getRisk` (requires a credential) · **Browser client:** `client.threats.getRisk()`

* **Permission:** `iam:threats:read` on `iam/threats/risk/{identityId}`.
* **Audited as:** `iam:threats:read`.
* **Errors:** `NOT_FOUND` when the identity is not in this tenant.

`score` and `level` are the effective values at the time of the read, decay and override included; `level` is
`none` for an identity nothing was ever detected about. `override` shows an administrator's floor with `active: false`
once it has expired, `contained` is set while the threats module holds the identity contained, and `contributions`
list each detection's points with what they still add (`current`), newest first.

```ts title="Signature"
iam.api.threats.getRisk(
  credential: CredentialInput,
  input: { tenantId: string; identityId: string },
): Promise<IdentityRiskView>
```

## getSettings [#getsettings]

Returns the tenant's detection settings with every default applied.

**HTTP:** `POST /api/iam/threats/getSettings` (requires a credential) · **Browser client:** `client.threats.getSettings()`

* **Permission:** `iam:threats:read` on `iam/threats/settings`.
* **Audited as:** `iam:threats:read`.

The result has every rule's `enabled` and `severity` (and `threshold` and `windowMs` where the rule has them),
`trustedNetworks`, `dormantDays`, `riskHalfLifeHours`, `notify`, `maxAutomaticContainments`, and `configured: false`
while the tenant has never saved settings, with `updatedAt` and `updatedBy` once it has.

```ts title="Signature"
iam.api.threats.getSettings(
  credential: CredentialInput,
  input: { tenantId: string },
): Promise<ThreatSettingsView>
```

## listDetections [#listdetections]

Lists the tenant's detections, newest first, optionally narrowed by status, rule, severity, identity, incident, and time.

**HTTP:** `POST /api/iam/threats/listDetections` (requires a credential) · **Browser client:** `client.threats.listDetections()`

* **Permission:** `iam:threats:read` on `iam/threats/detections`.
* **Audited as:** `iam:threats:read`.
* **Errors:** `INVALID_INPUT` for an unknown `status`, `ruleId`, or `severity`, a `limit` outside 1 to 1000, or a
  malformed `offset` or `since`.

`since` (epoch milliseconds) keeps detections recorded from then on. The result is `{ detections, total }`; page with
`limit` (100 by default) and `offset`.

```ts title="Signature"
iam.api.threats.listDetections(
  credential: CredentialInput,
  input: {
    tenantId: string;
    status?: DetectionStatus;
    ruleId?: ThreatRuleId;
    severity?: ThreatSeverity;
    identityId?: string;
    incidentId?: string;
    since?: number;
    limit?: number;
    offset?: number;
  },
): Promise<DetectionPage>
```

## listIncidents [#listincidents]

Lists the tenant's incidents, most recently active first, optionally narrowed by status, severity, assignee, and identity.

**HTTP:** `POST /api/iam/threats/listIncidents` (requires a credential) · **Browser client:** `client.threats.listIncidents()`

* **Permission:** `iam:threats:read` on `iam/threats/incidents`.
* **Audited as:** `iam:threats:read`.
* **Errors:** `INVALID_INPUT` for an unknown `status` or `severity`, or a `limit` outside 1 to 1000.

Incidents are ordered by their latest detection. `status` is `open`, `investigating`, or `resolved`. The result is
`{ incidents, total }`; page with `limit` (100 by default) and `offset`.

```ts title="Signature"
iam.api.threats.listIncidents(
  credential: CredentialInput,
  input: {
    tenantId: string;
    status?: IncidentStatus;
    severity?: ThreatSeverity;
    assigneeId?: string;
    identityId?: string;
    limit?: number;
    offset?: number;
  },
): Promise<IncidentPage>
```

## listPlaybooks [#listplaybooks]

Lists the tenant's playbooks by name.

**HTTP:** `POST /api/iam/threats/listPlaybooks` (requires a credential) · **Browser client:** `client.threats.listPlaybooks()`

* **Permission:** `iam:threats:read` on `iam/threats/playbooks`.
* **Audited as:** `iam:threats:read`.

Each playbook shows its trigger, actions, `enabled`, and how often it ran (`runs`, `lastRunAt`).

```ts title="Signature"
iam.api.threats.listPlaybooks(
  credential: CredentialInput,
  input: { tenantId: string },
): Promise<ThreatPlaybook[]>
```

## listRisk [#listrisk]

Lists the identities at or above a risk level, and contained ones, highest score first.

**HTTP:** `POST /api/iam/threats/listRisk` (requires a credential) · **Browser client:** `client.threats.listRisk()`

* **Permission:** `iam:threats:read` on `iam/threats/risk`.
* **Audited as:** `iam:threats:read`.
* **Errors:** `INVALID_INPUT` for a `minLevel` other than `low`, `medium`, or `high`, or a `limit` outside 1 to 1000.

`minLevel` defaults to `low`, so the list is everyone with any live risk. Each entry has the same shape as
[`getRisk`](#getrisk). The result is `{ identities, total }`; page with `limit` (100 by default) and `offset`. Deleted
identities are left out.

```ts title="Signature"
iam.api.threats.listRisk(
  credential: CredentialInput,
  input: {
    tenantId: string;
    minLevel?: Exclude<RiskLevel, 'none'>;
    limit?: number;
    offset?: number;
  },
): Promise<RiskPage>
```

## release [#release]

Lifts a containment, so the identity is active again and its API keys work.

**HTTP:** `POST /api/iam/threats/release` (requires a credential) · **Browser client:** `client.threats.release()`

* **Permission:** `iam:threats:respond` on `iam/threats/responses`, and a recent sign-in.
* **Audited as:** `iam:threats:respond` and `threat:release` (metadata: `note`, `containedAt`, `incidentId`).
* **Errors:** `INVALID_TRANSITION` (409) when the identity is not contained by the threats module, no longer disabled,
  or disabled, suspended, or re-enabled another way since (setStatus, offboarding, `agents.suspend`), or when it has
  passed its `expiresAt` (extend or clear it first); `ACCESS_DENIED` for a root administrator released by anyone but
  root; `INVALID_INPUT` for a note over 512 characters; `NOT_FOUND`; `RECENT_AUTH_REQUIRED`.

The identity signs in as usual again; the sessions containment ended stay ended, the member invitations it sent stay
revoked, and its risk score is kept. Changing the identity's status any other way (for example `identities.setStatus`,
`identities.offboard`, or `agents.suspend`) ends the containment, so a release never undoes it. Its rule-based access
packages are re-evaluated right after. The result is the `release` response, filed under the incident the containment
was for.

```ts title="Signature"
iam.api.threats.release(
  credential: CredentialInput,
  input: { tenantId: string; identityId: string; note?: string },
): Promise<ThreatResponse>
```

## reportSuspicious [#reportsuspicious]

Lets a person report activity on their own account that was not them: it raises an incident and signs out everywhere else.

**HTTP:** `POST /api/iam/threats/reportSuspicious` (requires a credential) · **Browser client:** `client.threats.reportSuspicious()`

* **Permission:** None beyond an ordinary sign-in session of the account's own tenant.
* **Audited as:** `threat:user-report` (metadata: `detectionId`, `incidentId`, `sessionsEnded`, `devicesForgotten`,
  `withNote`, `sessionId`), with the responses as `threat:revoke-sessions` and `threat:forget-devices`.
* **Errors:** `ACCESS_DENIED` for an API key, role session, session token, delegated session, or a session of another
  tenant; `IMPERSONATION_RESTRICTED` (403) from a "view as" session; `LIMIT_EXCEEDED` (409) after five reports in 24
  hours; `FEATURE_DISABLED` (403) when the tenant turned the `user-reported` rule off; `NOT_FOUND` for a `sessionId`
  that belongs to someone else; `INVALID_INPUT` for a note over 1000 characters.

It records a `user-reported` detection (high), which raises the person's risk and opens an incident administrators
see, then ends every other session of the account except API keys (the reporting session stays), deletes pending
sign-in challenges, forgets remembered devices, and runs the tenant's playbooks. `sessionId` names a session from
`auth.listSessions` the person does not recognize. The result is
`{ detectionId, incidentId, sessionsEnded, devicesForgotten }`. Suggest a password change and a new second factor
next.

```ts
await client.threats.reportSuspicious({ tenantId, note: 'A sign-in email from a city I have never visited.' });
```

```ts title="Signature"
iam.api.threats.reportSuspicious(
  credential: CredentialInput,
  input: { tenantId: string; note?: string; sessionId?: string },
): Promise<SuspiciousActivityReport>
```

## resolveIncident [#resolveincident]

Closes an incident as a true positive, a false positive, or benign.

**HTTP:** `POST /api/iam/threats/resolveIncident` (requires a credential) · **Browser client:** `client.threats.resolveIncident()`

* **Permission:** `iam:threats:manage` on `iam/threats/incidents/{id}`, and a recent sign-in.
* **Audited as:** `iam:threats:manage` and `threat:incident-resolve` (metadata: `resolution`, `severity`,
  `subjectType`, `subjectId`, `detectionsResolved`, `identityId`), plus `threat:risk-change` for identities whose
  level drops.
* **Errors:** `INVALID_TRANSITION` (409) when it is already resolved; `ACCESS_DENIED` when a `false-positive` or
  `benign` resolution would clear risk raised against the caller and the caller is not a root administrator;
  `INVALID_INPUT` for an unknown `resolution` or a note over 4000 characters; `NOT_FOUND`; `RECENT_AUTH_REQUIRED`.

The incident's open detections become `resolved`. A `false-positive` or `benign` resolution also takes their points
out of the identities' risk; `true-positive` keeps them decaying. An optional `note` is added as the closing note.
Resolved incidents are closed to changes, and the next detection about the same subject opens a new incident.

```ts title="Signature"
iam.api.threats.resolveIncident(
  credential: CredentialInput,
  input: {
    tenantId: string;
    incidentId: string;
    resolution: IncidentResolution;
    note?: string;
  },
): Promise<ThreatIncident>
```

## respond [#respond]

Responds by hand to an incident, an identity, or a network with one to five response actions.

**HTTP:** `POST /api/iam/threats/respond` (requires a credential) · **Browser client:** `client.threats.respond()`

* **Permission:** `iam:threats:respond` on `iam/threats/incidents/{id}` (on `iam/threats/responses` without an
  incident), and a recent sign-in.
* **Audited as:** `iam:threats:respond`, and each applied action as `threat:revoke-sessions`,
  `threat:forget-devices`, `threat:contain`, `threat:block-network`, or `threat:notify`.
* **Errors:** `INVALID_INPUT` without exactly one of `incidentId`, `identityId`, and `network`, for a network that is
  not an address or CIDR block, actions that are not one to five known kinds each listed once, a missing `reason`,
  containing yourself, or blocking a network that covers your own address; `ACCESS_DENIED` for ending a root
  administrator's sessions or containing one without being root, containing an owner without being an owner of the
  tenant in person or root, or blocking a network on the root tenant without being root; `LAST_OWNER` (409) for
  containing the last owner; `NOT_FOUND`; `RECENT_AUTH_REQUIRED`.

An incident target acts on the incident's subject (and its identity and network). An incident about the tenant, a
network, or a connection names an identity only while all its detections that name one name the same one; once two
differ (`severalIdentities`), identity actions on it are skipped as `not-identity`, so respond to the person by
`identityId` instead. A response to an identity or
network that has an open incident is filed under it. The result lists one response per action, `applied` or `skipped`
with a reason such as `not-identity`, `no-network`, `trusted-network`, `already-applied`, `inactive`, `no-incident`,
`no-transport`, or `no-recipients`. Revoking your own sessions keeps the one you are responding from. Containment and
network blocks take effect at once; contained identities' rule-based access packages are re-evaluated right after.

```ts
await iam.api.threats.respond(credential, {
  tenantId,
  incidentId,
  actions: [{ kind: 'contain' }, { kind: 'block-network', durationMs: 7 * 86_400_000 }, { kind: 'notify' }],
  reason: 'Password sprayed and reused from a hosting provider',
});
```

```ts title="Signature"
iam.api.threats.respond(
  credential: CredentialInput,
  input: {
    tenantId: string;
    incidentId?: string;
    identityId?: string;
    network?: string;
    actions: ResponseAction[];
    reason: string;
  },
): Promise<ThreatResponse[]>
```

## rules [#rules]

Lists every detection rule with its defaults, tuning bounds, and the setting in force for the tenant.

**HTTP:** `POST /api/iam/threats/rules` (requires a credential) · **Browser client:** `client.threats.rules()`

* **Permission:** `iam:threats:read` on `iam/threats/rules`.
* **Audited as:** `iam:threats:read`.

Each rule has its `id`, `title`, `description`, `category`, subject type, MITRE ATT\&CK `technique`, its `defaults`,
the `tunable` bounds [`configure`](#configure) accepts for `threshold` and `windowMs`, the effective `enabled`,
`severity`, `threshold`, and `windowMs`, `everyone` for `new-network`, and `customized` when the tenant changed
anything about it. Build a settings page from it rather than hard-coding the catalog.

```ts title="Signature"
iam.api.threats.rules(
  credential: CredentialInput,
  input: { tenantId: string },
): Promise<ThreatRuleView[]>
```

## setRisk [#setrisk]

Sets an identity's risk level by hand, as a floor that holds until it expires, or clears it.

**HTTP:** `POST /api/iam/threats/setRisk` (requires a credential) · **Browser client:** `client.threats.setRisk()`

* **Permission:** `iam:threats:respond` on `iam/threats/risk/{identityId}`, and a recent sign-in.
* **Audited as:** `iam:threats:respond` and `threat:risk-override` (metadata: `level`, `reason`, `expiresAt`, and
  `clearedContributions` for `none`), plus `threat:risk-change` when the effective level moves.
* **Errors:** `ACCESS_DENIED` for your own identity; `INVALID_INPUT` for an unknown `level`, a missing `reason`, an
  `expiresInMs` outside one hour to 90 days, or `expiresInMs` with `none`; `NOT_FOUND`; `RECENT_AUTH_REQUIRED`.

`low`, `medium`, or `high` records a floor ("confirmed compromised"): the effective level is never below it until
`expiresInMs` passes, or indefinitely without it. `none` removes the override and every detection's contribution, so
the identity starts from zero. The result is the identity's risk as [`getRisk`](#getrisk) returns it. Policies see
the new level on their next decision.

```ts title="Signature"
iam.api.threats.setRisk(
  credential: CredentialInput,
  input: {
    tenantId: string;
    identityId: string;
    level: RiskLevel;
    reason: string;
    expiresInMs?: number;
  },
): Promise<IdentityRiskView>
```

## summary [#summary]

Returns the tenant's threat posture at a glance for a dashboard.

**HTTP:** `POST /api/iam/threats/summary` (requires a credential) · **Browser client:** `client.threats.summary()`

* **Permission:** `iam:threats:read` on `iam/threats/incidents`.
* **Audited as:** `iam:threats:read`.

The result is `openIncidents` (open incidents by severity), `investigating` (the count under investigation),
`riskyIdentities` (by level), `contained`, `detections24h` with `byRule24h`, and `lastRunAt`, when detection last
read the tenant's audit trail. A `lastRunAt` far in the past means the job is not running.

```ts title="Signature"
iam.api.threats.summary(
  credential: CredentialInput,
  input: { tenantId: string },
): Promise<ThreatSummary>
```

## timeline [#timeline]

Returns an identity's recent audit trail, newest first: what it did and what was done to it.

**HTTP:** `POST /api/iam/threats/timeline` (requires a credential) · **Browser client:** `client.threats.timeline()`

* **Permission:** `iam:threats:read` on `iam/threats/risk/{identityId}`.
* **Audited as:** `iam:threats:read`.
* **Errors:** `NOT_FOUND` when the identity is not in this tenant; `INVALID_INPUT` for a `limit` outside 1 to 500.

It lists events the identity performed and events whose resource is the identity, allowed and denied, from `since` (epoch milliseconds; seven days ago by default), at most `limit` (200 by default).

```ts title="Signature"
iam.api.threats.timeline(
  credential: CredentialInput,
  input: { tenantId: string; identityId: string; since?: number; limit?: number },
): Promise<AuditEvent[]>
```

## updateIncident [#updateincident]

Moves an incident between open and investigating, assigns it, or changes its severity.

**HTTP:** `POST /api/iam/threats/updateIncident` (requires a credential) · **Browser client:** `client.threats.updateIncident()`

* **Permission:** `iam:threats:manage` on `iam/threats/incidents/{id}`.
* **Audited as:** `iam:threats:manage` and `threat:incident-update` (metadata: `changes`, `status`, `severity`,
  `assigneeId`).
* **Errors:** `INVALID_TRANSITION` (409) for a resolved incident; `INVALID_INPUT` for a `status` other than `open`
  or `investigating`, an unknown `severity`, or an assignee who is not active; `NOT_FOUND` for the incident or an
  assignee outside the tenant.

`assigneeId: null` unassigns. Resolve an incident with [`resolveIncident`](#resolveincident). No recent sign-in is
needed, so triage stays quick.

```ts title="Signature"
iam.api.threats.updateIncident(
  credential: CredentialInput,
  input: {
    tenantId: string;
    incidentId: string;
    status?: Exclude<IncidentStatus, 'resolved'>;
    assigneeId?: string | null;
    severity?: ThreatSeverity;
  },
): Promise<ThreatIncident>
```

## updatePlaybook [#updateplaybook]

Changes a playbook's name, description, trigger, actions, or whether it is enabled.

**HTTP:** `POST /api/iam/threats/updatePlaybook` (requires a credential) · **Browser client:** `client.threats.updatePlaybook()`

* **Permission:** `iam:threats:manage` on `iam/threats/playbooks/{id}`, and a recent sign-in.
* **Audited as:** `iam:threats:manage` and `threat:playbook-update` (metadata: the settings `before` and `after`).
* **Errors:** as for [`createPlaybook`](#createplaybook), plus `NOT_FOUND` when the playbook is not in this tenant.

Fields left out keep their value; `description: null` clears the description, and `trigger` and `actions` are
replaced whole. The playbook keeps its run count.

```ts title="Signature"
iam.api.threats.updatePlaybook(
  credential: CredentialInput,
  input: Partial<Omit<ThreatPlaybookInput, 'description'>> & {
    tenantId: string;
    playbookId: string;
    description?: string | null;
  },
): Promise<ThreatPlaybook>
```
