Compliance center
Automated identity checks mapped to SOC 2, ISO 27001, NIST 800-53 and GDPR, with exceptions and signed evidence for auditors.
Every audit asks the same questions about identity: must people use a second factor, and do they? Do sessions and API keys end? Do leavers lose their access? Is access reviewed, and how many people can do everything? Can anyone edit the logs? Answering them usually means screenshots and spreadsheets pulled together once a year.
The compliance center answers them continuously. Built-in checks read the tenant's identity and access state; a tenant turns them into controls, evaluates them every day, accepts individual findings with time-boxed exceptions that a second person approves, sees the status of each framework requirement, and hands its auditors a signed evidence pack they can verify offline.
Evidence, not coverage
Each check is mapped to requirements of SOC 2, ISO 27001, NIST SP 800-53 and GDPR as evidence for them, not as full coverage. Those requirements also cover systems, processes and documents Better IAM never sees, so a passing requirement passes only for the part the checks measure, and your auditor still assesses the rest.
await iam.api.compliance.adoptFramework(admin, { tenantId, framework: 'soc2' });
const { run, results } = await iam.api.compliance.evaluate(admin, { tenantId });
const [soc2] = await iam.api.compliance.status(admin, { tenantId, framework: 'soc2' });Routes: POST /api/iam/compliance/adoptFramework, POST /api/iam/compliance/evaluate, POST /api/iam/compliance/status
Checks
| Check | Measures | Parameters (default) | Evidence for |
|---|---|---|---|
mfa-enforced | The sign-in policy requires a second factor (warn when only owners must). | none | SOC 2 CC6.1, ISO 8.5, NIST IA-2(1) |
mfa-coverage | The share of active people with an authenticator app (or a passkey, when the deployment enables passkeys). | minimumPercent (100) | SOC 2 CC6.1, ISO 8.5, NIST IA-2(1), GDPR 32 |
password-length | The enforced minimum password length, or no passwords at all. | minimum (12) | SOC 2 CC6.1, ISO 5.17, NIST IA-5 |
session-lifetime | The longest session and idle time before sign-out. | maxHours (168), idleMinutes (1440) | SOC 2 CC6.1, ISO 8.5, NIST AC-12 |
inactive-accounts | Active people without a sign-in for a while. | days (90) | SOC 2 CC6.2, ISO 5.16, NIST AC-2 |
leaver-access | Disabled or expired accounts still holding sessions, keys, bindings or groups. | none | SOC 2 CC6.2, ISO 5.16, NIST AC-2, GDPR 32 |
stale-api-keys | Live API keys unused for a while. | days (90) | SOC 2 CC6.3, ISO 5.18, NIST IA-5 |
api-key-lifetime | Live API keys that live too long. | maxDays (365) | SOC 2 CC6.1, ISO 5.17, NIST IA-5 |
access-reviews | A review that reviewers mostly decided closed recently and covered the roles people hold; none is overdue. | intervalDays (90), minDecidedPercent (80), minCoveragePercent (100) | SOC 2 CC6.3, ISO 5.18, NIST AC-6(7) |
privileged-access | Everyone who can administer the organization: owners, root administrators, holders of admin roles by any path (groups, inherited roles, activated or approval-free eligible bindings), and trusts that assume them. | maxHolders (5) | SOC 2 CC6.3, ISO 8.2, NIST AC-6 |
owner-redundancy | At least two active owners (warn with one, fail with none). | none | ISO 8.2 |
separation-of-duties | Nobody breaks a separation-of-duties rule (warn without rules). | none | SOC 2 CC6.3, ISO 5.3, NIST AC-5 |
audit-integrity | The audit hash chain verifies, continuing each time from the last checkpoint. Its findings cannot be excepted. | none | SOC 2 CC7.2, ISO 8.15, NIST AU-9, GDPR 32 |
privacy-deadlines | No data-subject request is past its deadline. | none | ISO 5.34, GDPR 12 |
Each check returns a status (pass, fail, warn or not-applicable), metrics, and findings that name what needs
attention: identity:ID, session:ID (an API key), trust:ID, role:ID, campaign:ID, request:ID, or
tenant:ID:condition for an organization-wide setting (tenant:ID:mfa-optional). Stored findings identify people
only by ID; people's names are added when results are read, for callers who may read the directory
(iam:identities:read). The API reference and catalog list every parameter
with its range, and @better-iam/server exports the checks and mappings as complianceChecks and
complianceFrameworks.
Controls
A control is a check as your organization runs it: a key, a name, parameters, and the requirements it evidences.
adoptFramework(soc2,iso27001,nist-800-53orgdpr) creates one control per check the framework uses, keyed by the check's id and mapped asframework:requirement(soc2:CC6.1). Frameworks share controls: adopting a second one adds its mappings to the controls that exist.createControlruns a check with your own parameters and mappings, for example inactive accounts after 30 days, mapped to an internal policy as well as to SOC 2.
await iam.api.compliance.createControl(admin, {
tenantId,
key: 'inactive-30',
name: 'Accounts unused for 30 days are reviewed',
checkId: 'inactive-accounts',
params: { days: 30 },
mappings: ['internal:POL-7', 'soc2:CC6.2'],
});Route: POST /api/iam/compliance/createControl
A tenant holds at most 200 controls. updateControl changes parameters, mappings or enabled; disabled controls are
not evaluated, and keep the requirements they are mapped to from passing. deleteControl revokes the control's
exceptions, which stay as history.
Evaluating
evaluate (iam:compliance:evaluate) runs the enabled controls, at most once a minute per organization
(RATE_LIMITED otherwise). The checks read outside any transaction, so evaluating a large directory never holds the
store's write lock; the results are then written in one short transaction. Each result records the status, the status before
exceptions (rawStatus), a summary, metrics and up to 200 findings; evaluate returns the statuses and counts, and
listResults the findings. A check that cannot run fails its control rather than passing by omission. The run keeps
counts per status and a SHA-256 digest of its results, which is also written to the tamper-evident audit log as
compliance:evaluate. When a control starts or stops failing, the run records compliance:control:fail or
compliance:control:recover: subscribe a to hear about regressions.
Results more than three days old are marked stale, and a stale control counts as not evaluated in the requirement
status, so a scheduler that stopped running cannot leave old passes standing.
Exceptions
Some findings are accepted for a while: a person on leave, a service account with a compensating control, a fix that
is planned. createException names the control, the finding's subject, a reason, and an expiry at most a year away.
The exception starts pending; once a second person approves it with approveException, and until it expires, the
finding is marked excepted and no longer counts toward its control's status. There are no permanent exceptions.
const exception = await iam.api.compliance.createException(admin, {
tenantId,
controlKey: 'inactive-30',
subject: `identity:${aliceId}`,
reason: 'On parental leave until December',
expiresAt: Date.parse('2026-12-31T00:00:00Z'),
});
// Someone else with iam:compliance:manage approves it.
await iam.api.compliance.approveException(otherAdmin, { tenantId, exceptionId: exception.id });Routes: POST /api/iam/compliance/createException, POST /api/iam/compliance/approveException
Nobody may propose or approve an exception for a finding about themselves, and audit-integrity findings cannot be
excepted at all. revokeException ends an exception or withdraws a pending one; revoked exceptions stay as history,
in listExceptions and in evidence packs, until the retention sweep removes them 400 days after they expire.
Requirement status
status shows each requirement of a framework with its controls: no-control when nothing evidences it, and
otherwise the worst of its controls, where a disabled control counts as disabled and one without a fresh result as
not-evaluated, so neither lets the requirement pass. total, covered and passing count the requirements, those
with a control, and those that pass.
Evidence for auditors
exportEvidence returns a JSON pack with the controls (of one framework, or all; disabled ones marked), their
parameters, mappings, latest results and findings, every exception with its approval and revocation history, the
latest 30 runs with their digests, and the audit chain head. The pack carries a SHA-256 digest of its content and
an Ed25519 signature by a key derived from the deployment secret; the digest is also recorded in the audit chain
when the pack is exported.
import { verifyEvidencePack } from '@better-iam/server';
const pack = await iam.api.compliance.exportEvidence(admin, { tenantId, framework: 'soc2' });
const jwks = await iam.api.compliance.evidenceKeys(admin, { tenantId });
// The auditor, offline, with no access to your deployment:
const { valid, reason } = verifyEvidencePack(pack, jwks);Routes: POST /api/iam/compliance/exportEvidence, POST /api/iam/compliance/evidenceKeys
evidenceKeys publishes the public keys as a JWKS, the current secret's first and then those of
previousSecrets, so packs keep verifying across secret rotations. verifyEvidence makes the same check on the
server, and also accepts version 1 packs, which were signed with an HMAC before evidence keys existed. Packs name
people by ID, and by email address for callers who may read the directory, so handle them as personal data.
Permissions
iam:compliance:read covers the readers, status, evidence and its keys; iam:compliance:evaluate running
evaluations; and iam:compliance:manage adopting frameworks, controls, and proposing, approving and revoking
exceptions. Evaluations read the whole directory whoever starts them, so readers see findings about people they might
not otherwise read, by ID unless they may also read the directory. The console page is Governance › Compliance.
Options
The compliance center has no deployment option: the compliance group is always available and its collections are
part of the ordinary migration (npx better-iam migrate or iam.initialize()). Evidence packs are signed with a key
derived from the deployment secret. A control takes these fields in createControl and updateControl:
| Field | Type | Default | Description |
|---|---|---|---|
key | string | none | Required. Lowercase letters, digits, dots, underscores or hyphens; unique per tenant. |
name | string | none | Required. What reports show. |
checkId | string | none | Required. A built-in check from Checks. |
params | Record<string, number> | Check defaults | The check's parameters, such as { days: 30 }. |
mappings | string[] | [] | Up to 50 requirements as framework:requirement, such as soc2:CC6.1. |
enabled | boolean | true | Disabled controls are not evaluated (updateControl). |
adoptFramework takes framework: soc2, iso27001, nist-800-53 or gdpr. A tenant holds at most 200 controls.
Schema
| Collection | One record per | Key fields |
|---|---|---|
complianceControls | Control | key, name, description, checkId, params, mappings, enabled, lastStatus, lastEvaluatedAt, lastResultId |
complianceExceptions | Exception | controlKey, subject, reason, expiresAt, createdBy, status (pending, approved, revoked), approvedBy, approvedAt, revokedBy, revokedAt |
complianceResults | Control result of one run | runId, controlId, controlKey, checkId, status, rawStatus, summary, metrics, findings (up to 200), findingsTotal, excepted, evaluatedAt, expiresAt |
complianceRuns | Evaluation run | evaluatedAt, counts (per status), digest, triggeredBy, expiresAt |
complianceCheckpoints | Tenant (the audit-integrity check's verified place in the chain) | sequence, hash, verifiedAt |
Results and runs carry an expiresAt 400 days out, after which iam.sweepExpired() removes them.
Routes
Every method is a POST under the handler's base path (default /api/iam) and needs a credential. The browser client
calls the same routes as client.compliance.<method>.
| Method | Route | Method | Route |
|---|---|---|---|
compliance.catalog | POST /api/iam/compliance/catalog | compliance.adoptFramework | POST /api/iam/compliance/adoptFramework |
compliance.createControl | POST /api/iam/compliance/createControl | compliance.updateControl | POST /api/iam/compliance/updateControl |
compliance.deleteControl | POST /api/iam/compliance/deleteControl | compliance.listControls | POST /api/iam/compliance/listControls |
compliance.evaluate | POST /api/iam/compliance/evaluate | compliance.listRuns | POST /api/iam/compliance/listRuns |
compliance.listResults | POST /api/iam/compliance/listResults | compliance.status | POST /api/iam/compliance/status |
compliance.createException | POST /api/iam/compliance/createException | compliance.approveException | POST /api/iam/compliance/approveException |
compliance.revokeException | POST /api/iam/compliance/revokeException | compliance.listExceptions | POST /api/iam/compliance/listExceptions |
compliance.exportEvidence | POST /api/iam/compliance/exportEvidence | compliance.evidenceKeys | POST /api/iam/compliance/evidenceKeys |
compliance.verifyEvidence | POST /api/iam/compliance/verifyEvidence |
iam.compliance.evaluateAll() is the scheduled job, and verifyEvidencePack (from @better-iam/server) the offline
verifier; neither is a route.
Errors
| Code | Status | When |
|---|---|---|
INVALID_INPUT | 400 | An unknown framework or check, a malformed control key, more than 50 mappings, a non-boolean enabled, or an exception for findings that cannot be excepted |
CONFLICT | 409 | A control with the key exists (or the key belongs to another check), or approving an exception that is not pending or has expired |
LIMIT_EXCEEDED | 409 | The tenant would hold more than 200 controls |
RATE_LIMITED | 429 | evaluate less than a minute after the last evaluation |
ACCESS_DENIED | 403 | Proposing or approving an exception for a finding about yourself |
NOT_FOUND | 404 | An unknown control or exception |
Better IAM is created by Sean Filimon
Last updated
Lifecycle workflowsnew
Joiner, mover and leaver automation that runs steps for each person a trigger fires for, with the owner's rights checked at every step.
Application catalognew
A My apps launcher for your organization's tools, with assignments to people and groups, access requests, audited launches, and sign-in enforcement.