# Access review

> Who has access to what across your connected systems: dormant accounts, over-privilege, and shared credentials.

Source: https://triagic.com/checkups/access-review

## Scope [#scope]

A periodic "who can do what" review across every connected system this run can
read. Enumerate and compare only — never create, modify, or revoke access, and
never read credential values. The output is a proposal for the access owner. The
subject is the org's own systems; do not review or report on this application's
own accounts or configuration.

## Procedure [#procedure]

1. **Enumerate per-system accounts, for every connected system.** Use only
   read-only enumeration:
   * **GitHub / GitLab** — organization or group members, their role, outside
     collaborators, teams and their repository permissions, deploy keys, and
     personal-access or project tokens where the API exposes them. Admin/owner
     rights and outside collaborators on private repositories are the two lists to
     read closely.
   * **Snowflake** — `SHOW USERS`, `SHOW ROLES`, `SHOW GRANTS TO ROLE`, and
     `ACCOUNT_USAGE.GRANTS_TO_USERS` / `GRANTS_TO_ROLES`; users with
     `ACCOUNTADMIN` or `SECURITYADMIN`, users with no MFA or with password
     authentication where key-pair is the standard, and `LAST_SUCCESS_LOGIN`
     older than 90 days.
   * **Postgres** — `pg_roles` (`rolsuper`, `rolcreaterole`,
     `rolbypassrls`) and `information_schema.role_table_grants`; anything
     granted to `PUBLIC` is granted to everyone and is almost always a finding.
   * **MySQL** — `SELECT user, host FROM mysql.user` for the account list, then
     `SHOW GRANTS FOR '<user>'@'<host>'` per account; `WITH GRANT OPTION`,
     `ALL PRIVILEGES ON *.*`, and any account with host `%` are the three to
     read closely. Anonymous accounts (empty `user`) are a finding on sight.
   * **Cloud and SaaS providers generally** — whatever member/role listing the
     provider's read-only tools expose. If a system is connected but exposes no
     way to enumerate access, record that as a coverage gap rather than skipping it
     silently.
     With nothing enumerable connected, say so plainly in the report, emit a single
     info finding recording that the review had no reviewable surface, and stop —
     do not substitute another surface to review.
2. **Find the dormant.** Cross-reference every principal against its last activity:
   90 days with no login or action is the usual dormancy line, 180 days is
   indefensible. A dormant account with elevated privileges is the highest-value
   finding this checkup produces, because it is both the likeliest to be forgotten
   and the likeliest to be abused. Where a system exposes no last-activity
   timestamp, say so rather than assuming the account is in use.
3. **Find the over-privileged.** Compare each principal's grants against what their
   observable work requires: a support member with production write grants, a
   contractor with organization-owner rights, a service account with a role far
   wider than the queries it actually runs (check query or audit history where the
   system exposes it). Look for privilege that was granted for a one-off task and
   never removed — the pattern is a broad grant with a narrow, old usage history.
4. **Find the unattributable.** Shared logins, service accounts several humans use,
   API tokens with no named owner, and any credential where the system's own audit
   trail cannot say which person acted. These break attribution for every other
   control, so report them even when the access itself is appropriate.
5. **Compare against the last review.** Findings that are still open from the
   previous run are the real story of an access review — a dormant account reported
   three months running is a process failure, not an access failure, and should be
   said that way in the report.

## Finding keys [#finding-keys]

The `key` identifies the *principal and the problem*, not this run, so a person
who is still over-privileged next month lands on the same ledger row and a revoked
grant that reappears is flagged as a regression. Use
`<system>:<principal>:<issue-slug>` with the principal identified the way its own
system does (login, username, email local part, token name). Do not put dates,
counts, or role names that change into the key.

* `github:acme-org/octocat:dormant-admin`
* `snowflake:ETL_SVC:accountadmin-grant`
* `postgres:public:grant-to-public`
* `shared:deploy-bot:no-named-owner`

## Severity rubric [#severity-rubric]

* **critical** — active access for someone who has left the organization; a
  privileged credential shared by several people with no attribution; production
  write or admin access granted to `PUBLIC` or to everyone by default.
* **high** — admin or owner privileges that are dormant, unexplained, or far beyond
  the holder's role; an unaccepted or unused elevated invitation; a service account
  with account-level administrative rights.
* **medium** — ordinary over-privilege with a plausible history, dormant
  non-privileged accounts.
* **low** — tidiness: unused teams, stale invitations without privilege, naming
  that obscures ownership.
* **info** — the access map itself: principal counts by role and system, recorded so
  the next run can see what changed.

## Output guidance [#output-guidance]

Open with a one-paragraph executive summary: how many principals
across how many systems, how many are privileged, how many are dormant, and the
single most exposed grant. Then one section per connected system reviewed, each
opening with a small access table
(Principal | Role or grants | Last activity | Assessment). Add a "Not reviewed"
section naming systems that are connected but expose no enumeration, and systems
the org uses that are not connected at all. Close with a recommendations table
(Principal | System | Recommended action | Why | Owner to action it), revocations
first. Flag every finding that was already open at the last run as a repeat, since
repeats are a process finding rather than an access finding. Never propose a
revocation as if you performed it.

<!-- generated by apps/server/scripts/export-checkups.ts, do not edit -->
