# 5 Account inventory (incl. shared/service accounts)

> v0.1.0 · role: Assure · [policy: #10 · #28](https://docs.google.com/spreadsheets/d/1nOztaPd1Y7eNeRSR_hdovYy-ncpx-bAx/edit?usp=sharing&ouid=115159875779023172526&rtpof=true&sd=true)

Unlocks: [№21 Leaver offboarding runbook](leaver-offboarding.md) · [№43 Periodic access recertification](access-recertification.md)

A dated register of every account — human, shared and service — each with a named owner. It is built by exporting users, groups and group memberships with GAM (the command-line Workspace admin tool), then reconciling that against the human register and cross-checking the identities that never appear as ordinary users: GCP service accounts and domain-wide delegation clients. The point of the register is that it makes an unowned account a finding rather than a curiosity, so an orphaned or attacker-created identity has nowhere to hide.

Visual explainer: [A worked example: from raw GAM output to a register with named owners](../explainer/account-inventory/index.html)

## Caveats

- The inventory is only as good as its 'unowned = suspicious' rule — an account with a plausible name and no owner is exactly what an attacker leaves behind.
- Suspended accounts still hold data and can be re-enabled by any super admin — count them in, do not filter them out.

## Setup steps

1. Install and authorize GAM7 (the command-line Workspace admin tool) on an admin workstation — a one-time setup this control and a dozen later runbooks reuse. Budget a real half hour: the installer creates a dedicated Google Cloud project for API access and walks you through two browser authorizations as a super admin. Follow the official install wiki (github.com/GAM-team/GAM) rather than a blog post.

   ```
   bash <(curl -s -S -L https://raw.githubusercontent.com/GAM-team/GAM/main/src/gam-install.sh)
gam oauth info  # proves the authorization stuck
   ```
   Note: GAM holds super-admin-equivalent API credentials — the machine it lives on is an admin workstation from that moment ([№53](privileged-access-workstation.md)), and its config directory belongs in the same custody as any admin credential.

2. Export the authoritative user list with GAM and store it as the dated inventory snapshot.

   ```
   gam print users fields primaryEmail,name,suspended,isAdmin,isDelegatedAdmin,lastLoginTime,creationTime,orgUnitPath > users-$(date +%F).csv
   ```
   Note: lastLoginTime is blank for accounts that have never signed in and for some SSO paths — do not read blank as dormant without checking the audit log.

   docs: [Download a list of users](https://knowledge.workspace.google.com/admin/users/advanced/download-a-list-of-users) · [REST Resource: users | Admin SDK Directory API](https://developers.google.com/workspace/admin/directory/reference/rest/v1/users)

3. Export groups and their membership, so shared mailboxes and service identities are captured too.

   ```
   gam print groups fields email,name,description,adminCreated > groups-$(date +%F).csv  &&  gam print group-members > group-members-$(date +%F).csv
   ```

4. Enumerate non-human identities that never appear as ordinary users: service accounts and domain-wide delegation clients. — `Security › Access and data control › API controls › Manage Domain Wide Delegation`

   Cross-check GCP service accounts and Security > Access and data control > API controls > Manage Domain Wide Delegation against the register

   docs: [Control API access with domain-wide delegation](https://knowledge.workspace.google.com/admin/apps/control-api-access-with-domain-wide-delegation) · [List and edit service accounts](https://docs.cloud.google.com/iam/docs/service-accounts-list-edit)

5. Reconcile the export against the human register (HR list + owned shared accounts) and open the console on any row that has no owner. Group hygiene itself — open posting, external members on access-granting groups — is not inventory work: it is audited and fixed by [№34](groups-exposure-lockdown.md), which starts from the group export this control just produced. — `Directory › Users`

   ```
   Diff = zero unexplained accounts; every account has a named owner and a purpose
   ```

## Ongoing maintenance

- **[automatable: script]** Monthly: re-export users/groups/delegation clients and diff against the register.
- **[requires a human]** Monthly: triage every unowned or unexplained account the diff surfaces.

## How to verify

1. Check the newest inventory snapshot is no older than the cycle, then re-run the export and diff — an inventory is only as good as its date.

   ```
   gam print users fields primaryEmail,suspended,lastLoginTime > users-check.csv && diff <(sort users-check.csv) <(sort users-<last-date>.csv)
   ```

## Settings screens

- Directory > Users
  - console: https://admin.google.com/ac/users
  - screenshot: ../screenshots/admin.google.com/ac/users.png
- Directory > Groups
  - console: https://admin.google.com/ac/groups
  - screenshot: ../screenshots/admin.google.com/ac/groups.png
- Security > Access and data control > API controls > Manage Domain Wide Delegation
  - console: https://admin.google.com/ac/owl/domainwidedelegation
  - screenshot: ../screenshots/admin.google.com/ac/owl/domainwidedelegation.png
