BetterIAM
Server API

analysis

Access analysis scans a tenant's configuration for risky or stale access, such as administrators without MFA or dormant accounts that still hold roles.

Access analysis scans a tenant's configuration for risky or stale access, such as administrators without MFA or dormant accounts that still hold roles. It also reports API keys nobody uses, broken manager links, and more. It answers "what should we fix first?" without anyone reading every policy by hand, and a policy linter catches documents that do not do what their author intended. Nothing here changes access: findings are observations you act on through the other groups, or suppress with a recorded reason when a risk is accepted.

What the scan checks

findings runs every check below in one read-only transaction. "Administrator" means a tenant owner or anyone who holds a role that grants every action (* or iam:*) on every resource without conditions. Protected system policies and the Owner role are not reported against.

KindSeverityReported when
unrestricted-admin-policyhighA policy or a role's inline document allows every action on every resource without conditions.
admin-without-mfahighA human administrator has neither an authenticator app nor a passkey, and the tenant does not require MFA.
separation-of-dutieshighAn active person holds roles that a separation-of-duties rule forbids together.
team-maintainers-grant-adminhighA team (or a team above it) holds an administrator role as standing access, and maintainers manage its membership, so they can make anyone an administrator.
broad-action-wildcardmediumA policy allows a service-wide wildcard such as documents:* without conditions, which also grants actions added later.
service-account-adminmediumA service account is an administrator, so a leaked API key would control the organization.
dormant-accessmediumA person holding bindings or ownership has not signed in for dormantDays (an account that never signed in counts from its creation).
stale-api-keymediumAn unexpired API key has not been used for dormantDays.
trust-without-mfamediumA live trust lets its source assume a role without MFA.
standing-privileged-accessmediumA person holds an administrator role through a direct, permanent binding that is not eligible (just-in-time).
manager-cyclemediumA person's manager chain loops back to them.
unattached-policylowA policy is not attached to any role.
unused-rolelowA role is not bound to anyone and no trust uses it.
empty-rolelowA role has no inline document and no attached policies.
empty-group-with-accesslowA group holds role bindings but has no members, so anyone added later inherits them at once.
unused-eligible-bindinglowAn eligible binding has existed for longer than dormantDays without a recorded activation.
orphaned-managerlowA person's manager no longer exists or is not active, so manager approvals cannot reach them.
policy-lintlowA stored policy or a role's inline document has linter warnings of severity warning (see lintPolicy).
team-without-maintainerlowA team has members but no active maintainer (in it or above it), so only administrators can manage it.
department-without-headlowA department has people but no active head, so manager approvals routed through the org chart stop there.

Each finding has a title, a detail that says what to do, and the subject it concerns (a policy, role, identity, group, trust, credential, delegation, team, or department).

Suppressing findings

Some findings describe accepted risk: a break-glass administrator account, or a service account that must be powerful. Suppress them with a reason so they stop cluttering the results. Finding IDs are deterministic: the same condition on the same subject always yields the same 24-character ID, so a suppression keeps applying for as long as the condition holds, and again if it returns later. Suppressed findings are counted in summary.suppressed and listed, with who suppressed them, when, and why, when you pass includeSuppressed: true.

Methods4
Serveriam.api.analysis
Clientclient.analysis
HTTPPOST /api/iam/analysis/*
MethodWhat it doesAccess
findingsRuns every access-analysis check on the tenant and returns the findings, most severe first, with counts per severity.Credential
lintPolicyChecks a policy document, or a stored policy, for errors and for statements that likely do not do what they say.Credential
suppressHides one finding from future results and records why.Credential
unsuppressShows a suppressed finding again.Credential

findings

Runs every access-analysis check on the tenant and returns the findings, most severe first, with counts per severity.

POST/api/iam/analysis/findings
client.analysis.findings()Credential

Used inEnterprise onboarding,Access reviews

  • Permission: iam:analysis:read on iam/analysis/*.
  • Audited as: iam:analysis:read.
  • Errors: INVALID_INPUT when dormantDays is outside 1 to 3650.

dormantDays (default 90) sets when an unused account, API key, or eligible binding is reported. Suppressed findings are left out unless includeSuppressed is true. Run it on a schedule and alert on new high findings; the analyze CLI command does this with --fail-on high, which suits a CI or cron job.

const { summary, findings } = await iam.api.analysis.findings(credential, { tenantId, dormantDays: 60 });
for (const finding of findings.filter((item) => item.severity === 'high'))
  console.log(finding.title, finding.detail);
Input

Prop

Type

Returns

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/analysis/findings" \
  -H "Authorization: Bearer $BETTER_IAM_TOKEN" \
  -H "Content-Type: application/json" \
  -H "X-Better-IAM: 1" \
  -d '{
  "tenantId": "<tenantId>"
}'
Signature
iam.api.analysis.findings(
  credential: CredentialInput,
  input: { tenantId: string; dormantDays?: number; includeSuppressed?: boolean },
): Promise<{
  generatedAt: number;
  dormantDays: number;
  summary: { high: number; medium: number; low: number; suppressed: number };
  findings: AccessFinding[];
}>

lintPolicy

Checks a policy document, or a stored policy, for errors and for statements that likely do not do what they say.

POST/api/iam/analysis/lintPolicy
client.analysis.lintPolicy()Credential
  • Permission: iam:policies:read on the policy (with policyId) or on the tenant (with a candidate document).
  • Audited as: iam:policies:read.
  • Errors: INVALID_INPUT unless exactly one of document and policyId is given; NOT_FOUND when the policy is not in this tenant.

The document is first validated the way storage would validate it, including unknown actions and resource types. A document storage would reject is not thrown as an error: the result has valid: false, the error code and message, and no warnings. A valid document gets warnings, each with a code, a severity (warning: probably a mistake; info: worth a look, often intended), the statement index, and a message. Examples include an allow that makes every holder a full administrator, a condition key the server never sets, a deny that silently never applies when an optional key is missing, an allow that an unconditional deny shadows, and duplicate statements.

Use it in a policy editor before saving, or in CI over documents kept in version control. Pass contextKeys to name keys your application supplies through resolveContext, so they are not reported as unknown. See policies and conditions.

const result = await iam.api.analysis.lintPolicy(credential, {
  tenantId,
  document: {
    version: 1,
    statements: [{ effect: 'allow', actions: ['documents:*'], resources: ['*'] }],
  },
});
// result.valid === true; result.warnings[0].code === 'service-wildcard'
Input

Prop

Type

Returns

A PolicyLintResult 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/analysis/lintPolicy" \
  -H "Authorization: Bearer $BETTER_IAM_TOKEN" \
  -H "Content-Type: application/json" \
  -H "X-Better-IAM: 1" \
  -d '{
  "tenantId": "<tenantId>"
}'
Signature
iam.api.analysis.lintPolicy(
  credential: CredentialInput,
  input: {
    tenantId: string;
    document?: unknown;
    policyId?: string;
    contextKeys?: string[];
  },
): Promise<PolicyLintResult>

suppress

Hides one finding from future results and records why.

POST/api/iam/analysis/suppress
client.analysis.suppress()Credential
  • Permission: iam:analysis:update on iam/analysis/{findingId}.
  • Audited as: iam:analysis:update.
  • Errors: INVALID_INPUT when findingId is not a 24-character finding ID, or reason is empty or longer than 500 characters.

The reason, the caller, and the time are kept and shown to anyone who lists suppressed findings, so reviewers can see who accepted which risk. Suppressing a finding again replaces its reason.

Input

Prop

Type

Returns

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/analysis/suppress" \
  -H "Authorization: Bearer $BETTER_IAM_TOKEN" \
  -H "Content-Type: application/json" \
  -H "X-Better-IAM: 1" \
  -d '{
  "tenantId": "<tenantId>",
  "findingId": "<findingId>",
  "reason": "<reason>"
}'
Signature
iam.api.analysis.suppress(
  credential: CredentialInput,
  input: { tenantId: string; findingId: string; reason: string },
): Promise<{ suppressed: boolean }>

unsuppress

Shows a suppressed finding again.

POST/api/iam/analysis/unsuppress
client.analysis.unsuppress()Credential
  • Permission: iam:analysis:update on iam/analysis/{findingId}.
  • Audited as: iam:analysis:update.

Unsuppressing a finding that is not suppressed succeeds and changes nothing.

Input

Prop

Type

Returns

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/analysis/unsuppress" \
  -H "Authorization: Bearer $BETTER_IAM_TOKEN" \
  -H "Content-Type: application/json" \
  -H "X-Better-IAM: 1" \
  -d '{
  "tenantId": "<tenantId>",
  "findingId": "<findingId>"
}'
Signature
iam.api.analysis.unsuppress(
  credential: CredentialInput,
  input: { tenantId: string; findingId: string },
): Promise<{ suppressed: boolean }>

Was this page helpful?

Better IAM is created by Sean Filimon

Last updated

On this page