Extracts enterprise users and enriches them with:
- organization membership
- SAML/SCIM external identity details
- authoritative Microsoft Entra ID user principal names (UPNs)
- Copilot seat assignment details (Business/Enterprise), including last activity metadata
The tool is built in TypeScript and uses both GitHub GraphQL and REST APIs. It supports enterprise-level data collection first, with org-level fallback where needed.
The exporter:
- Loads an Entra ID extract containing
id,userPrincipalName,displayName, selected extension attributes, and primarySMTP - Fetches organizations and enterprise members from GitHub
- Fetches linked external identities through GitHub GraphQL
- Fetches SCIM records, including
externalId, through GitHub REST - Fetches Copilot billing seats
- Resolves UPNs from Entra ID and merges everything by GitHub login
- Exports results to CSV or JSON
It also prints useful run statistics, including total users, users with external identity, and Copilot seat counts by plan.
- Node.js (ESM)
- TypeScript
tsxfor executiondotenvfor configurationcsv-stringifyfor CSV generationundicifor proxy-aware HTTP requests
src/index.ts: orchestration, merge logic, filtering, exportsrc/api/github.ts: GraphQL/REST calls, pagination, mappingsrc/identity/entra-extract.ts: Entra JSON extract loadingsrc/identity/upn.ts: deterministic GitHub/SCIM/Entra matchingsrc/types/index.ts: domain and config typesscripts/export-entra-users.ps1: read-only Entra ID export
- Node.js 20+
- Access token with enterprise/org/copilot-billing read access
- Enterprise slug (e.g. from
https://github.com/enterprises/<slug>) - PowerShell 7 and Microsoft Graph PowerShell for the Entra export
- Delegated Microsoft Graph
User.Read.Allpermission
- Install dependencies:
npm install- Create your local env file from the template:
cp .env.example .env-
Fill
.envwith your values (GITHUB_TOKEN,ENTERPRISE_SLUG, etc.). -
Install the two Microsoft Graph PowerShell modules used by the extract:
Install-Module Microsoft.Graph.Authentication,Microsoft.Graph.Users -Scope CurrentUser- Sign in and create the local Entra extract:
npm run export:entraThe extract is written to input/entra-users.json, which is excluded from Git.
Run CSV export (default):
npm run export:csvRun JSON export:
npm run export:jsonRefresh the authoritative Entra UPN extract before an export:
npm run export:entraBuild TypeScript:
npm run buildEnvironment variables:
GITHUB_TOKEN: PAT used for API callsSCIM_TOKEN: optional PAT for read-only SCIM REST calls; falls back toGITHUB_TOKENENTRA_USERS_FILE: optional Entra JSON extract path; defaults to./input/entra-users.jsonENTERPRISE_SLUG: enterprise identifierGRAPHQL_URL: GraphQL endpointAPI_BASE_URL: REST base URLOUTPUT_FORMAT:csvorjsonOUTPUT_FILE: output pathFILTER_STRICT: whentrue, exports only users that match all three conditions:- has external identity
- has Copilot seat
- has at least one organization
For Enterprise Managed Users, GitHub recommends a classic PAT associated with the setup user and scoped to scim:enterprise. An enterprise owner can use a classic PAT with admin:enterprise for read-only SCIM GET requests. For organization-level SCIM, the token must belong to an organization owner, have admin:org, and be authorized for the SAML SSO organization.
The CSV writer does not truncate values. Live validation in this enterprise showed that both GitHub GraphQL scimIdentity.username and REST SCIM userName contain the same truncated value. GitHub therefore is not treated as the authoritative source for UPN.
The authoritative value is Entra ID userPrincipalName. The primary match follows this chain:
GitHub login → GraphQL SCIM username → exact REST SCIM userName → SCIM externalId → Entra id → Entra userPrincipalName
For Microsoft Entra provisioning, SCIM externalId is the Entra object ID. This exact identifier join is used even when names differ.
If the object-ID path is unavailable, display name is a secondary key. A display-name candidate is accepted only when exactly one Entra record also has a userPrincipalName for which the GitHub SCIM value is a case-insensitive strict prefix. Duplicate or conflicting candidates remain empty and are reported as ambiguous. Email is never used as a UPN substitute.
The raw GitHub value remains in scim_username for traceability. The authoritative result is written separately to user_principal_name, with upn_match_method set to external_id or display_name.
Includes core identity and licensing fields such as:
login,name,organizationsuser_principal_name,upn_match_methodextension_c77e68a23a6a4f91af48a93b63f95e0f_AMCOMPANYCODE,extension_c77e68a23a6a4f91af48a93b63f95e0f_AMBUCODE,extension_c77e68a23a6a4f91af48a93b63f95e0f_AMSEGMENTCODE,SMTPscim_username(raw GitHub value),external_idcopilot_assigned,copilot_plan_type,copilot_org,copilot_assigning_teamcopilot_last_activity,copilot_last_editor,copilot_pending_cancellation
- The exporter prefers enterprise-level endpoints where available.
- For constrained environments, HTTP(S) proxy variables (
HTTPS_PROXY,HTTP_PROXY) are supported. - Keep
.envprivate and do not commit the Entra extract or other identity data.