Add Active Directory group membership to the audit #1

Merged
hannah-vernon merged 1 commit from dev into main 2026-08-05 14:28:48 -05:00

Summary

Adds Active Directory group membership to the account-usage audit, in response to a
user request: permissions are often granted to AD groups rather than directly to an
account, so scanning for the account alone can miss access granted through a group it
belongs to. Opt-in via --include-groups.

What it does

  • Resolves effective membership via the tokenGroups attribute (nested groups and
    the primary group included), translating each SID to DOMAIN\group. Broad well-known
    groups (Domain Users, Everyone, Authenticated Users, BUILTIN\Users, ...) are excluded
    by default; --include-wellknown-groups overrides. SIDs that cannot be resolved (for
    example from a trusted-but-unreachable domain) are recorded by SID rather than dropped.
  • Reports memberships as AdGroupMembership findings, keyed by subject + group.
  • Matches groups everywhere: each resolved group is added to the account matcher, so
    every scanner also flags artifacts that reference the group (SQL group logins, SSRS
    role assignments, configuration files, local groups). Group-derived findings are
    labelled with a MatchedVia path (e.g. CONTOSO\hannah is a member of CONTOSO\sql-admins) so direct references and group-derived access can be told apart.
  • New localgroup scanner: reports direct membership in machine-local groups (for
    example the local Administrators group) using the WinNT provider - deliberately not
    Win32_GroupUser (WMI), which is very slow on domain-joined machines.

Schema

  • Adds a nullable MatchedVia column to Finding (idempotent migration) and an
    @MatchedVia parameter to usp_UpsertFinding; vUnresolvedFindings exposes it.
  • Preflight now verifies the parameter exists, so an out-of-date database that cannot be
    upgraded automatically fails fast with a clear message.
  • Console / CSV / JSON output and the deployed-agent JSON round-trip MatchedVia.

Deploy mode

The orchestrator resolves groups once and passes them to each agent via an internal
--group-form option, so agents match the same groups without re-querying AD (agents do
not re-report the memberships themselves).

Notes and scope

  • Group-based access is not affected by rotating the account's password; it is
    reported for completeness of the access-surface picture, not as a rotation blocker.
  • Deferred to follow-up work: group membership spanning AD domain/forest trusts, and
    a dedicated NTFS / share / registry ACL and User-Rights-Assignment scan.

Testing

  • Built clean (0 warnings / 0 errors, NuGetAudit enabled).
  • Verified against a SQL Server 2019 instance using a temporary database (created, tested,
    dropped): schema migration and @MatchedVia preflight, membership rows keyed by
    subject, and the local-group scanner (direct Administrators / docker-users membership,
    sub-second) all confirmed. A rubber-duck review was completed and its findings
    addressed (membership row identity, local-vs-domain name disambiguation, cancellation
    durability of membership rows, and the preflight schema-version check).

Assumptions

  • The host running the scan is domain-joined with directory connectivity and read access
    to the account object (the tool already relies on this for name-form resolution).
  • Group expansion targets a single account's effective membership; multi-domain / trust
    traversal is explicitly out of scope for this change (tracked as follow-up).
## Summary Adds **Active Directory group membership** to the account-usage audit, in response to a user request: permissions are often granted to AD groups rather than directly to an account, so scanning for the account alone can miss access granted through a group it belongs to. Opt-in via `--include-groups`. ## What it does - **Resolves effective membership** via the `tokenGroups` attribute (nested groups and the primary group included), translating each SID to `DOMAIN\group`. Broad well-known groups (Domain Users, Everyone, Authenticated Users, BUILTIN\Users, ...) are excluded by default; `--include-wellknown-groups` overrides. SIDs that cannot be resolved (for example from a trusted-but-unreachable domain) are recorded by SID rather than dropped. - **Reports memberships** as `AdGroupMembership` findings, keyed by subject + group. - **Matches groups everywhere**: each resolved group is added to the account matcher, so every scanner also flags artifacts that reference the group (SQL group logins, SSRS role assignments, configuration files, local groups). Group-derived findings are labelled with a `MatchedVia` path (e.g. `CONTOSO\hannah is a member of CONTOSO\sql-admins`) so direct references and group-derived access can be told apart. - **New `localgroup` scanner**: reports direct membership in machine-local groups (for example the local Administrators group) using the WinNT provider - deliberately not `Win32_GroupUser` (WMI), which is very slow on domain-joined machines. ## Schema - Adds a nullable `MatchedVia` column to `Finding` (idempotent migration) and an `@MatchedVia` parameter to `usp_UpsertFinding`; `vUnresolvedFindings` exposes it. - Preflight now verifies the parameter exists, so an out-of-date database that cannot be upgraded automatically fails fast with a clear message. - Console / CSV / JSON output and the deployed-agent JSON round-trip `MatchedVia`. ## Deploy mode The orchestrator resolves groups once and passes them to each agent via an internal `--group-form` option, so agents match the same groups without re-querying AD (agents do not re-report the memberships themselves). ## Notes and scope - Group-based access is **not** affected by rotating the account's password; it is reported for completeness of the access-surface picture, not as a rotation blocker. - Deferred to follow-up work: group membership spanning AD domain/forest **trusts**, and a dedicated NTFS / share / registry **ACL** and User-Rights-Assignment scan. ## Testing - Built clean (0 warnings / 0 errors, NuGetAudit enabled). - Verified against a SQL Server 2019 instance using a temporary database (created, tested, dropped): schema migration and `@MatchedVia` preflight, membership rows keyed by subject, and the local-group scanner (direct Administrators / docker-users membership, sub-second) all confirmed. A rubber-duck review was completed and its findings addressed (membership row identity, local-vs-domain name disambiguation, cancellation durability of membership rows, and the preflight schema-version check). ## Assumptions - The host running the scan is domain-joined with directory connectivity and read access to the account object (the tool already relies on this for name-form resolution). - Group expansion targets a single account's effective membership; multi-domain / trust traversal is explicitly out of scope for this change (tracked as follow-up).
Requested by a user (permissions are often granted to AD groups rather than
directly to an account, so scanning for the account alone can miss group-granted
access). Opt-in via --include-groups.

- GroupResolver reads the account's effective membership via the tokenGroups
  attribute (nested + primary group) and translates each SID to DOMAIN\group.
  Broad well-known groups (Domain Users, Everyone, Authenticated Users,
  BUILTIN\Users, ...) are excluded by default; --include-wellknown-groups
  overrides. Unresolvable (e.g. cross-trust) SIDs fall back to the SID string.
- Memberships are reported as AdGroupMembership findings (keyed by subject+group),
  and each resolved group is added to AccountMatcher so every scanner also flags
  artifacts that reference the group (SQL group logins, SSRS roles, config files).
  Group-derived findings are labelled with a MatchedVia path.
- New LocalGroupScanner (key localgroup) reports direct membership in machine-local
  groups (e.g. local Administrators) via the WinNT provider - deliberately not
  Win32_GroupUser, which is pathologically slow on domain-joined machines.
- Finding gains a MatchedVia field; Schema.sql adds a nullable MatchedVia column
  (idempotent migration) and usp_UpsertFinding an @MatchedVia parameter; preflight
  verifies the parameter exists so an un-upgradable database fails fast.
  Console/CSV/JSON and the agent JSON round-trip MatchedVia.
- Deploy: the orchestrator resolves groups once and passes them to agents via an
  internal --group-form option (agents match groups but do not re-report members).

Group-based access is not affected by a password change; it is reported for
completeness of the access-surface picture. Cross-domain/forest trust membership
and dedicated NTFS/share/registry ACL scanning are tracked as future work.

Docs (README, ARCHITECTURE) and help updated.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
hannah-vernon/ADAccountUsage!1
No description provided.