Inventory Structure
The inventory/ folder contains the current PIM state as JSON files, organized by workload and entity.
Top-level layout
inventory/
├ ── directory-roles/
├── pim-groups/
├── authentication-contexts/
├── administrative-units/
├── activation-events/
└── archive/
Each entity gets its own subfolder using a slugified name. For example: inventory/directory-roles/global-administrator/.
Directory Roles
inventory/directory-roles/{slug}/
├── definition.json
├── policy.json
└── assignments.json
definition.json
{
"id": "62e90394-69f5-4237-9190-012177145e10",
"displayName": "Global Administrator",
"description": "...",
"isBuiltIn": true,
"isEnabled": true,
"rolePermissions": [
{
"allowedResourceActions": ["*"],
"condition": null
}
]
}
policy.json
PIM policy assignment with expanded rules:
{
"id": "policyAssignmentId",
"scopeId": "/",
"scopeType": "Directory",
"roleDefinitionId": "roleId",
"policy": {
"id": "policyId",
"rules": [
{
"id": "Enablement_EndUser_Assignment",
"enabledRules": ["MultiFactorAuthentication"],
"notificationLevel": "All"
},
{
"id": "Expiration_EndUser_Assignment",
"maximumDuration": "PT8H"
}
]
}
}
assignments.json
{
"permanent": [
{
"id": "assignmentId1",
"principalId": "userId1",
"principal": {
"id": "userId1",
"displayName": "Alice Admin",
"userPrincipalName": "alice@contoso.com"
},
"directoryScopeId": "/",
"createdDateTime": "2024-01-15T10:00:00Z"
}
],
"eligible": [
{
"id": "eligibleId1",
"principalId": "userId2",
"principal": { "id": "userId2", "displayName": "Bob User" },
"directoryScopeId": "/",
"scheduleInfo": {
"expiration": {
"endDateTime": "2027-04-20T00:00:00Z"
}
}
}
],
"active": [
{
"id": "activeId1",
"principalId": "userId3",
"scheduleInfo": {
"expiration": {
"endDateTime": "2026-04-21T15:30:00Z"
}
}
}
]
}
PIM Groups
inventory/pim-groups/{slug}/
├── definition.json
├── policy.json
└── assignments.json
definition.json
{
"id": "groupId",
"displayName": "Finance Admins",
"description": "...",
"groupTypes": [],
"visibility": "Private"
}
policy.json
Two policy assignments, one per access type:
{
"member": {
"id": "policyAssignment_member",
"scopeId": "groupId",
"scopeType": "Group",
"roleDefinitionId": "member",
"policy": {
"rules": [
{
"id": "Enablement_EndUser_Assignment",
"enabledRules": ["MultiFactorAuthentication"]
}
]
}
},
"owner": {
"id": "policyAssignment_owner",
"scopeId": "groupId",
"scopeType": "Group",
"roleDefinitionId": "owner",
"policy": {
"rules": [
{
"id": "Enablement_EndUser_Assignment",
"enabledRules": ["MultiFactorAuthentication"]
}
]
}
}
}
assignments.json
{
"eligible": [
{
"id": "scheduleId1",
"principalId": "userId1",
"groupId": "groupId",
"accessId": "member",
"principal": {
"id": "userId1",
"displayName": "Alice Admin"
},
"scheduleInfo": {
"expiration": {
"endDateTime": "2027-04-20T00:00:00Z"
}
}
}
],
"active": []
}
Each entry has an accessId field (member or owner).
Lookups
Lookups are read-only catalogs of IDs to display names. They contain only definition.json and are never diffed for severity.
authentication-contexts
inventory/authentication-contexts/{slug}/
└── definition.json
{
"id": "c1",
"displayName": "Require MFA",
"description": "..."
}
Resolves claimValue references in policy rules like AuthenticationContext_EndUser_Assignment.
administrative-units
inventory/administrative-units/{slug}/
└── definition.json
{
"id": "auId",
"displayName": "Finance Department",
"description": "..."
}
Resolves directoryScopeId in assignments scoped to an AU instead of the full tenant.
Archive
When a role or group disappears from PIM (removed, offboarded, or renamed to a new slug), its inventory folder is not deleted. It is moved to inventory/archive/{workload}/{slug}_{date}:
inventory/archive/
├── directory-roles/
│ └── old-role-name_2026-04-26/
│ ├── definition.json
│ ├── policy.json
│ └── assignments.json
└── pim-groups/
└── disbanded-group_2026-03-01/
├── definition.json
├── policy.json
└── assignments.json
The date suffix is the UTC date of the scan that detected the removal. The full contents of the folder are preserved, so the last known state of the entity remains in git history and on disk.
The removal is also recorded as a High severity change entry and included in notifications.
Deterministic serialization
All JSON files are serialized the same way every time. Same data always produces identical output. This keeps git diff clean and meaningful.
- Objects with an
idfield: sorted byid - Strings: sorted alphabetically
@odata.context,@odata.type, and other metadata fields are stripped
Git history
Every inventory change becomes a commit:
git log --oneline inventory/
Output:
a1b2c3d scan: 2026-04-20T15:30:00Z
e5f6g7h scan: 2026-04-20T15:00:00Z
To see what changed between two scans:
git diff a1b2c3d~1 a1b2c3d -- inventory/