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
Detection is a scheduled job, iam.detectThreats() (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.
ruleslists 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'strustedNetworksare 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
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; 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). 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
The five response actions are revoke-sessions (API keys kept unless keepApiKeys: false), forget-devices,
contain (the identity is disabled until 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;
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 mostmaxAutomaticContainmentsidentities per tenant and run (skipped asbrakedbeyond it, audited once asthreat: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
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 needs no permission: it acts on the caller's own account only.
| Method | What it does | Access |
|---|---|---|
addNote | Adds an investigation note to an incident. | Credential |
configure | Tunes detection for the tenant: rules, trusted networks, dormancy, risk decay, alert recipients, and the containment brake. | Credential |
createPlaybook | Defines an automatic response: actions that run when a new detection matches the trigger. | Credential |
deletePlaybook | Deletes a playbook; responses it already took stay on their incidents. | Credential |
detect | Runs detection for the tenant now instead of waiting for the scheduled job, and returns what the run did. | Credential |
dismissDetection | Marks an open detection as a false alarm and takes its points out of the identity's risk. | Credential |
getDetection | Returns one detection with its evidence. | Credential |
getIncident | Returns an incident with its detections, notes, responses, and the risk of the identity it concerns. | Credential |
getRisk | Returns one identity's risk: score, level, override, containment, and the contributions behind them. | Credential |
getSettings | Returns the tenant's detection settings with every default applied. | Credential |
listDetections | Lists the tenant's detections, newest first, optionally narrowed by status, rule, severity, identity, incident, and time. | Credential |
listIncidents | Lists the tenant's incidents, most recently active first, optionally narrowed by status, severity, assignee, and identity. | Credential |
listPlaybooks | Lists the tenant's playbooks by name. | Credential |
listRisk | Lists the identities at or above a risk level, and contained ones, highest score first. | Credential |
release | Lifts a containment, so the identity is active again and its API keys work. | Credential |
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 | Closes an incident as a true positive, a false positive, or benign. | Credential |
respond | Responds by hand to an incident, an identity, or a network with one to five response actions. | Credential |
rules | Lists every detection rule with its defaults, tuning bounds, and the setting in force for the tenant. | Credential |
setRisk | Sets an identity's risk level by hand, as a floor that holds until it expires, or clears it. | Credential |
summary | Returns the tenant's threat posture at a glance for a dashboard. | Credential |
timeline | Returns an identity's recent audit trail, newest first: what it did and what was done to it. | Credential |
updateIncident | Moves an incident between open and investigating, assigns it, or changes its severity. | Credential |
updatePlaybook | Changes a playbook's name, description, trigger, actions, or whether it is enabled. | Credential |
addNote
Adds an investigation note to an incident.
Used inThreat detection
- Permission:
iam:threats:manageoniam/threats/incidents/{id}. - Audited as:
iam:threats:manageandthreat:note(metadata:noteId,length; never the text). - Errors:
INVALID_INPUTfor a body that is empty or over 4000 characters;LIMIT_EXCEEDED(409) past 500 notes on the incident;NOT_FOUNDwhen 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 returns them oldest first.
Prop
Type
A ThreatNote object:
Prop
Type
Example HTTP request
Only the required fields are shown; replace each <placeholder>. The response is { "data": … } on success or { "error": { "code", "message" } }.
curl -X POST "$IAM_URL/api/iam/threats/addNote" \
-H "Authorization: Bearer $BETTER_IAM_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Better-IAM: 1" \
-d '{
"tenantId": "<tenantId>",
"incidentId": "<incidentId>",
"body": "<body>"
}'iam.api.threats.addNote(
credential: CredentialInput,
input: { tenantId: string; incidentId: string; body: string },
): Promise<ThreatNote>configure
Tunes detection for the tenant: rules, trusted networks, dormancy, risk decay, alert recipients, and the containment brake.
Used inThreat detection
- Permission:
iam:threats:manageoniam/threats/settings, and a recent sign-in. - Audited as:
iam:threats:manageandthreat:settings(metadata:changed,changedRules,disabledRules,weakened); turning a rule off or weakening detection otherwise also records aguardrail-weakeneddetection against the caller. - Errors:
ACCESS_DENIEDfor shorteningriskHalfLifeHourswhile detections raise the caller's own risk (root administrators excepted);INVALID_INPUTfor an unknown rule or setting, athresholdorwindowMsoutside the rule'stunablebounds (or for a rule that has none),everyoneon a rule other thannew-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),dormantDaysoutside 7 to 3650,riskHalfLifeHoursoutside 1 to 720, more than 20 or malformednotify.emails, ormaxAutomaticContainmentsoutside 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 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).
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'] },
});Prop
Type
A ThreatSettingsView object:
Prop
Type
Example HTTP request
Only the required fields are shown; replace each <placeholder>. The response is { "data": … } on success or { "error": { "code", "message" } }.
curl -X POST "$IAM_URL/api/iam/threats/configure" \
-H "Authorization: Bearer $BETTER_IAM_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Better-IAM: 1" \
-d '{
"tenantId": "<tenantId>"
}'iam.api.threats.configure(
credential: CredentialInput,
input: ThreatSettingsInput & { tenantId: string },
): Promise<ThreatSettingsView>createPlaybook
Defines an automatic response: actions that run when a new detection matches the trigger.
Used inShared Signals receiver,Threat detection
- Permission:
iam:threats:manageoniam/threats/playbooks, and a recent sign-in. - Audited as:
iam:threats:manageandthreat:playbook-create(metadata:name,enabled,trigger,actions). - Errors:
INVALID_INPUTfor a missing or over-long name, a missingtrigger, an unknown rule, severity, or subject type, oractionsthat are not one to five known kinds each listed once (durationMsonly onblock-network, one minute to 30 days;keepApiKeysonly onrevoke-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, 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.
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' }],
});Prop
Type
A ThreatPlaybook object:
Prop
Type
Example HTTP request
Only the required fields are shown; replace each <placeholder>. The response is { "data": … } on success or { "error": { "code", "message" } }.
curl -X POST "$IAM_URL/api/iam/threats/createPlaybook" \
-H "Authorization: Bearer $BETTER_IAM_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Better-IAM: 1" \
-d '{
"name": "<name>",
"trigger": {},
"actions": [
{
"kind": "notify"
}
],
"tenantId": "<tenantId>"
}'iam.api.threats.createPlaybook(
credential: CredentialInput,
input: ThreatPlaybookInput & { tenantId: string },
): Promise<ThreatPlaybook>deletePlaybook
Deletes a playbook; responses it already took stay on their incidents.
- Permission:
iam:threats:manageoniam/threats/playbooks/{id}, and a recent sign-in. - Audited as:
iam:threats:manageandthreat:playbook-delete(metadata:name). - Errors:
NOT_FOUNDwhen 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 instead.
Prop
Type
Prop
Type
Example HTTP request
Only the required fields are shown; replace each <placeholder>. The response is { "data": … } on success or { "error": { "code", "message" } }.
curl -X POST "$IAM_URL/api/iam/threats/deletePlaybook" \
-H "Authorization: Bearer $BETTER_IAM_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Better-IAM: 1" \
-d '{
"tenantId": "<tenantId>",
"playbookId": "<playbookId>"
}'iam.api.threats.deletePlaybook(
credential: CredentialInput,
input: { tenantId: string; playbookId: string },
): Promise<{ deleted: true }>detect
Runs detection for the tenant now instead of waiting for the scheduled job, and returns what the run did.
- Permission:
iam:threats:manageoniam/threats/detections. - Audited as:
iam:threats:manage, plus whatever the run records (threat:detection,threat:incident-open, playbook responses) bythreat-detection.
It is exactly one tenant's share of iam.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.
Prop
Type
A ThreatDetectionRun object:
Prop
Type
Example HTTP request
Only the required fields are shown; replace each <placeholder>. The response is { "data": … } on success or { "error": { "code", "message" } }.
curl -X POST "$IAM_URL/api/iam/threats/detect" \
-H "Authorization: Bearer $BETTER_IAM_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Better-IAM: 1" \
-d '{
"tenantId": "<tenantId>"
}'iam.api.threats.detect(
credential: CredentialInput,
input: { tenantId: string },
): Promise<ThreatDetectionRun>dismissDetection
Marks an open detection as a false alarm and takes its points out of the identity's risk.
- Permission:
iam:threats:manageoniam/threats/detections/{id}, and a recent sign-in. - Audited as:
iam:threats:manageandthreat:detection-dismiss(metadata:ruleId,severity,reason,identityId,incidentId), plusthreat:risk-changewhen the identity's level drops. - Errors:
INVALID_TRANSITION(409) when the detection is not open;ACCESS_DENIEDwhen it is about the caller and the caller is not a root administrator;INVALID_INPUTfor a missingreasonor 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.
Prop
Type
A ThreatDetection object:
Prop
Type
Example HTTP request
Only the required fields are shown; replace each <placeholder>. The response is { "data": … } on success or { "error": { "code", "message" } }.
curl -X POST "$IAM_URL/api/iam/threats/dismissDetection" \
-H "Authorization: Bearer $BETTER_IAM_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Better-IAM: 1" \
-d '{
"tenantId": "<tenantId>",
"detectionId": "<detectionId>",
"reason": "<reason>"
}'iam.api.threats.dismissDetection(
credential: CredentialInput,
input: { tenantId: string; detectionId: string; reason: string },
): Promise<ThreatDetection>getDetection
Returns one detection with its evidence.
- Permission:
iam:threats:readoniam/threats/detections/{id}. - Audited as:
iam:threats:read. - Errors:
NOT_FOUNDwhen 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.
Prop
Type
A ThreatDetection object:
Prop
Type
Example HTTP request
Only the required fields are shown; replace each <placeholder>. The response is { "data": … } on success or { "error": { "code", "message" } }.
curl -X POST "$IAM_URL/api/iam/threats/getDetection" \
-H "Authorization: Bearer $BETTER_IAM_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Better-IAM: 1" \
-d '{
"tenantId": "<tenantId>",
"detectionId": "<detectionId>"
}'iam.api.threats.getDetection(
credential: CredentialInput,
input: { tenantId: string; detectionId: string },
): Promise<ThreatDetection>getIncident
Returns an incident with its detections, notes, responses, and the risk of the identity it concerns.
Used inThreat detection
- Permission:
iam:threats:readoniam/threats/incidents/{id}. - Audited as:
iam:threats:read. - Errors:
NOT_FOUNDwhen 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 returns it. Everything an investigator needs for one page.
Prop
Type
A IncidentDetail object:
Prop
Type
Example HTTP request
Only the required fields are shown; replace each <placeholder>. The response is { "data": … } on success or { "error": { "code", "message" } }.
curl -X POST "$IAM_URL/api/iam/threats/getIncident" \
-H "Authorization: Bearer $BETTER_IAM_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Better-IAM: 1" \
-d '{
"tenantId": "<tenantId>",
"incidentId": "<incidentId>"
}'iam.api.threats.getIncident(
credential: CredentialInput,
input: { tenantId: string; incidentId: string },
): Promise<IncidentDetail>getRisk
Returns one identity's risk: score, level, override, containment, and the contributions behind them.
- Permission:
iam:threats:readoniam/threats/risk/{identityId}. - Audited as:
iam:threats:read. - Errors:
NOT_FOUNDwhen 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.
Prop
Type
A IdentityRiskView object:
Prop
Type
Example HTTP request
Only the required fields are shown; replace each <placeholder>. The response is { "data": … } on success or { "error": { "code", "message" } }.
curl -X POST "$IAM_URL/api/iam/threats/getRisk" \
-H "Authorization: Bearer $BETTER_IAM_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Better-IAM: 1" \
-d '{
"tenantId": "<tenantId>",
"identityId": "<identityId>"
}'iam.api.threats.getRisk(
credential: CredentialInput,
input: { tenantId: string; identityId: string },
): Promise<IdentityRiskView>getSettings
Returns the tenant's detection settings with every default applied.
- Permission:
iam:threats:readoniam/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.
Prop
Type
A ThreatSettingsView object:
Prop
Type
Example HTTP request
Only the required fields are shown; replace each <placeholder>. The response is { "data": … } on success or { "error": { "code", "message" } }.
curl -X POST "$IAM_URL/api/iam/threats/getSettings" \
-H "Authorization: Bearer $BETTER_IAM_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Better-IAM: 1" \
-d '{
"tenantId": "<tenantId>"
}'iam.api.threats.getSettings(
credential: CredentialInput,
input: { tenantId: string },
): Promise<ThreatSettingsView>listDetections
Lists the tenant's detections, newest first, optionally narrowed by status, rule, severity, identity, incident, and time.
- Permission:
iam:threats:readoniam/threats/detections. - Audited as:
iam:threats:read. - Errors:
INVALID_INPUTfor an unknownstatus,ruleId, orseverity, alimitoutside 1 to 1000, or a malformedoffsetorsince.
since (epoch milliseconds) keeps detections recorded from then on. The result is { detections, total }; page with
limit (100 by default) and offset.
Prop
Type
A DetectionPage object:
Prop
Type
Example HTTP request
Only the required fields are shown; replace each <placeholder>. The response is { "data": … } on success or { "error": { "code", "message" } }.
curl -X POST "$IAM_URL/api/iam/threats/listDetections" \
-H "Authorization: Bearer $BETTER_IAM_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Better-IAM: 1" \
-d '{
"tenantId": "<tenantId>"
}'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
Lists the tenant's incidents, most recently active first, optionally narrowed by status, severity, assignee, and identity.
Used inThreat detection
- Permission:
iam:threats:readoniam/threats/incidents. - Audited as:
iam:threats:read. - Errors:
INVALID_INPUTfor an unknownstatusorseverity, or alimitoutside 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.
Prop
Type
A IncidentPage object:
Prop
Type
Example HTTP request
Only the required fields are shown; replace each <placeholder>. The response is { "data": … } on success or { "error": { "code", "message" } }.
curl -X POST "$IAM_URL/api/iam/threats/listIncidents" \
-H "Authorization: Bearer $BETTER_IAM_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Better-IAM: 1" \
-d '{
"tenantId": "<tenantId>"
}'iam.api.threats.listIncidents(
credential: CredentialInput,
input: {
tenantId: string;
status?: IncidentStatus;
severity?: ThreatSeverity;
assigneeId?: string;
identityId?: string;
limit?: number;
offset?: number;
},
): Promise<IncidentPage>listPlaybooks
Lists the tenant's playbooks by name.
- Permission:
iam:threats:readoniam/threats/playbooks. - Audited as:
iam:threats:read.
Each playbook shows its trigger, actions, enabled, and how often it ran (runs, lastRunAt).
Prop
Type
An array of ThreatPlaybook.
Prop
Type
Example HTTP request
Only the required fields are shown; replace each <placeholder>. The response is { "data": … } on success or { "error": { "code", "message" } }.
curl -X POST "$IAM_URL/api/iam/threats/listPlaybooks" \
-H "Authorization: Bearer $BETTER_IAM_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Better-IAM: 1" \
-d '{
"tenantId": "<tenantId>"
}'iam.api.threats.listPlaybooks(
credential: CredentialInput,
input: { tenantId: string },
): Promise<ThreatPlaybook[]>listRisk
Lists the identities at or above a risk level, and contained ones, highest score first.
- Permission:
iam:threats:readoniam/threats/risk. - Audited as:
iam:threats:read. - Errors:
INVALID_INPUTfor aminLevelother thanlow,medium, orhigh, or alimitoutside 1 to 1000.
minLevel defaults to low, so the list is everyone with any live risk. Each entry has the same shape as
getRisk. The result is { identities, total }; page with limit (100 by default) and offset. Deleted
identities are left out.
Prop
Type
A RiskPage object:
Prop
Type
Example HTTP request
Only the required fields are shown; replace each <placeholder>. The response is { "data": … } on success or { "error": { "code", "message" } }.
curl -X POST "$IAM_URL/api/iam/threats/listRisk" \
-H "Authorization: Bearer $BETTER_IAM_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Better-IAM: 1" \
-d '{
"tenantId": "<tenantId>"
}'iam.api.threats.listRisk(
credential: CredentialInput,
input: {
tenantId: string;
minLevel?: Exclude<RiskLevel, 'none'>;
limit?: number;
offset?: number;
},
): Promise<RiskPage>release
Lifts a containment, so the identity is active again and its API keys work.
- Permission:
iam:threats:respondoniam/threats/responses, and a recent sign-in. - Audited as:
iam:threats:respondandthreat: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 itsexpiresAt(extend or clear it first);ACCESS_DENIEDfor a root administrator released by anyone but root;INVALID_INPUTfor 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.
Prop
Type
A ThreatResponse object:
Prop
Type
Example HTTP request
Only the required fields are shown; replace each <placeholder>. The response is { "data": … } on success or { "error": { "code", "message" } }.
curl -X POST "$IAM_URL/api/iam/threats/release" \
-H "Authorization: Bearer $BETTER_IAM_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Better-IAM: 1" \
-d '{
"tenantId": "<tenantId>",
"identityId": "<identityId>"
}'iam.api.threats.release(
credential: CredentialInput,
input: { tenantId: string; identityId: string; note?: string },
): Promise<ThreatResponse>reportSuspicious
Lets a person report activity on their own account that was not them: it raises an incident and signs out everywhere else.
Used inThreat detection
- 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 asthreat:revoke-sessionsandthreat:forget-devices. - Errors:
ACCESS_DENIEDfor 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 theuser-reportedrule off;NOT_FOUNDfor asessionIdthat belongs to someone else;INVALID_INPUTfor 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.
await client.threats.reportSuspicious({ tenantId, note: 'A sign-in email from a city I have never visited.' });Prop
Type
A SuspiciousActivityReport object:
Prop
Type
Example HTTP request
Only the required fields are shown; replace each <placeholder>. The response is { "data": … } on success or { "error": { "code", "message" } }.
curl -X POST "$IAM_URL/api/iam/threats/reportSuspicious" \
-H "Authorization: Bearer $BETTER_IAM_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Better-IAM: 1" \
-d '{
"tenantId": "<tenantId>"
}'iam.api.threats.reportSuspicious(
credential: CredentialInput,
input: { tenantId: string; note?: string; sessionId?: string },
): Promise<SuspiciousActivityReport>resolveIncident
Closes an incident as a true positive, a false positive, or benign.
Used inThreat detection
- Permission:
iam:threats:manageoniam/threats/incidents/{id}, and a recent sign-in. - Audited as:
iam:threats:manageandthreat:incident-resolve(metadata:resolution,severity,subjectType,subjectId,detectionsResolved,identityId), plusthreat:risk-changefor identities whose level drops. - Errors:
INVALID_TRANSITION(409) when it is already resolved;ACCESS_DENIEDwhen afalse-positiveorbenignresolution would clear risk raised against the caller and the caller is not a root administrator;INVALID_INPUTfor an unknownresolutionor 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.
Prop
Type
A ThreatIncident object:
Prop
Type
Example HTTP request
Only the required fields are shown; replace each <placeholder>. The response is { "data": … } on success or { "error": { "code", "message" } }.
curl -X POST "$IAM_URL/api/iam/threats/resolveIncident" \
-H "Authorization: Bearer $BETTER_IAM_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Better-IAM: 1" \
-d '{
"tenantId": "<tenantId>",
"incidentId": "<incidentId>",
"resolution": "true-positive"
}'iam.api.threats.resolveIncident(
credential: CredentialInput,
input: {
tenantId: string;
incidentId: string;
resolution: IncidentResolution;
note?: string;
},
): Promise<ThreatIncident>respond
Responds by hand to an incident, an identity, or a network with one to five response actions.
Used inThreat detection
- Permission:
iam:threats:respondoniam/threats/incidents/{id}(oniam/threats/responseswithout an incident), and a recent sign-in. - Audited as:
iam:threats:respond, and each applied action asthreat:revoke-sessions,threat:forget-devices,threat:contain,threat:block-network, orthreat:notify. - Errors:
INVALID_INPUTwithout exactly one ofincidentId,identityId, andnetwork, for a network that is not an address or CIDR block, actions that are not one to five known kinds each listed once, a missingreason, containing yourself, or blocking a network that covers your own address;ACCESS_DENIEDfor 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.
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',
});Prop
Type
An array of ThreatResponse.
Prop
Type
Example HTTP request
Only the required fields are shown; replace each <placeholder>. The response is { "data": … } on success or { "error": { "code", "message" } }.
curl -X POST "$IAM_URL/api/iam/threats/respond" \
-H "Authorization: Bearer $BETTER_IAM_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Better-IAM: 1" \
-d '{
"tenantId": "<tenantId>",
"actions": [
{
"kind": "notify"
}
],
"reason": "<reason>"
}'iam.api.threats.respond(
credential: CredentialInput,
input: {
tenantId: string;
incidentId?: string;
identityId?: string;
network?: string;
actions: ResponseAction[];
reason: string;
},
): Promise<ThreatResponse[]>rules
Lists every detection rule with its defaults, tuning bounds, and the setting in force for the tenant.
- Permission:
iam:threats:readoniam/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 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.
Prop
Type
An array of ThreatRuleView.
Prop
Type
Example HTTP request
Only the required fields are shown; replace each <placeholder>. The response is { "data": … } on success or { "error": { "code", "message" } }.
curl -X POST "$IAM_URL/api/iam/threats/rules" \
-H "Authorization: Bearer $BETTER_IAM_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Better-IAM: 1" \
-d '{
"tenantId": "<tenantId>"
}'iam.api.threats.rules(
credential: CredentialInput,
input: { tenantId: string },
): Promise<ThreatRuleView[]>setRisk
Sets an identity's risk level by hand, as a floor that holds until it expires, or clears it.
- Permission:
iam:threats:respondoniam/threats/risk/{identityId}, and a recent sign-in. - Audited as:
iam:threats:respondandthreat:risk-override(metadata:level,reason,expiresAt, andclearedContributionsfornone), plusthreat:risk-changewhen the effective level moves. - Errors:
ACCESS_DENIEDfor your own identity;INVALID_INPUTfor an unknownlevel, a missingreason, anexpiresInMsoutside one hour to 90 days, orexpiresInMswithnone;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 returns it. Policies see
the new level on their next decision.
Prop
Type
A IdentityRiskView object:
Prop
Type
Example HTTP request
Only the required fields are shown; replace each <placeholder>. The response is { "data": … } on success or { "error": { "code", "message" } }.
curl -X POST "$IAM_URL/api/iam/threats/setRisk" \
-H "Authorization: Bearer $BETTER_IAM_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Better-IAM: 1" \
-d '{
"tenantId": "<tenantId>",
"identityId": "<identityId>",
"level": "high",
"reason": "<reason>"
}'iam.api.threats.setRisk(
credential: CredentialInput,
input: {
tenantId: string;
identityId: string;
level: RiskLevel;
reason: string;
expiresInMs?: number;
},
): Promise<IdentityRiskView>summary
Returns the tenant's threat posture at a glance for a dashboard.
Used inThreat detection
- Permission:
iam:threats:readoniam/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.
Prop
Type
A ThreatSummary object:
Prop
Type
Example HTTP request
Only the required fields are shown; replace each <placeholder>. The response is { "data": … } on success or { "error": { "code", "message" } }.
curl -X POST "$IAM_URL/api/iam/threats/summary" \
-H "Authorization: Bearer $BETTER_IAM_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Better-IAM: 1" \
-d '{
"tenantId": "<tenantId>"
}'iam.api.threats.summary(
credential: CredentialInput,
input: { tenantId: string },
): Promise<ThreatSummary>timeline
Returns an identity's recent audit trail, newest first: what it did and what was done to it.
Used inThreat detection
- Permission:
iam:threats:readoniam/threats/risk/{identityId}. - Audited as:
iam:threats:read. - Errors:
NOT_FOUNDwhen the identity is not in this tenant;INVALID_INPUTfor alimitoutside 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).
Prop
Type
An array of AuditEvent.
Prop
Type
Example HTTP request
Only the required fields are shown; replace each <placeholder>. The response is { "data": … } on success or { "error": { "code", "message" } }.
curl -X POST "$IAM_URL/api/iam/threats/timeline" \
-H "Authorization: Bearer $BETTER_IAM_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Better-IAM: 1" \
-d '{
"tenantId": "<tenantId>",
"identityId": "<identityId>"
}'iam.api.threats.timeline(
credential: CredentialInput,
input: { tenantId: string; identityId: string; since?: number; limit?: number },
): Promise<AuditEvent[]>updateIncident
Moves an incident between open and investigating, assigns it, or changes its severity.
Used inThreat detection
- Permission:
iam:threats:manageoniam/threats/incidents/{id}. - Audited as:
iam:threats:manageandthreat:incident-update(metadata:changes,status,severity,assigneeId). - Errors:
INVALID_TRANSITION(409) for a resolved incident;INVALID_INPUTfor astatusother thanopenorinvestigating, an unknownseverity, or an assignee who is not active;NOT_FOUNDfor the incident or an assignee outside the tenant.
assigneeId: null unassigns. Resolve an incident with resolveIncident. No recent sign-in is
needed, so triage stays quick.
Prop
Type
A ThreatIncident object:
Prop
Type
Example HTTP request
Only the required fields are shown; replace each <placeholder>. The response is { "data": … } on success or { "error": { "code", "message" } }.
curl -X POST "$IAM_URL/api/iam/threats/updateIncident" \
-H "Authorization: Bearer $BETTER_IAM_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Better-IAM: 1" \
-d '{
"tenantId": "<tenantId>",
"incidentId": "<incidentId>"
}'iam.api.threats.updateIncident(
credential: CredentialInput,
input: {
tenantId: string;
incidentId: string;
status?: Exclude<IncidentStatus, 'resolved'>;
assigneeId?: string | null;
severity?: ThreatSeverity;
},
): Promise<ThreatIncident>updatePlaybook
Changes a playbook's name, description, trigger, actions, or whether it is enabled.
- Permission:
iam:threats:manageoniam/threats/playbooks/{id}, and a recent sign-in. - Audited as:
iam:threats:manageandthreat:playbook-update(metadata: the settingsbeforeandafter). - Errors: as for
createPlaybook, plusNOT_FOUNDwhen 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.
Prop
Type
A ThreatPlaybook object:
Prop
Type
Example HTTP request
Only the required fields are shown; replace each <placeholder>. The response is { "data": … } on success or { "error": { "code", "message" } }.
curl -X POST "$IAM_URL/api/iam/threats/updatePlaybook" \
-H "Authorization: Bearer $BETTER_IAM_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Better-IAM: 1" \
-d '{
"tenantId": "<tenantId>",
"playbookId": "<playbookId>"
}'iam.api.threats.updatePlaybook(
credential: CredentialInput,
input: Partial<Omit<ThreatPlaybookInput, 'description'>> & {
tenantId: string;
playbookId: string;
description?: string | null;
},
): Promise<ThreatPlaybook>Better IAM is created by Sean Filimon
Last updated