Skip to main content

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/
├── conditional-access/
├── 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.

The config.json file in each auth context folder is operator-defined and never written by the scan pipeline. See Auth Context CA Compliance for the supported fields.

Conditional Access​

CA policies that reference at least one auth context claim are stored here. Only policies with conditions.applications.includeAuthenticationContextClassReferences set are stored. All other CA policies are out of scope.

inventory/conditional-access/{slug}/
└── definition.json
{
"id": "policyId",
"displayName": "PIM - Phishing-resistant MFA + SIF",
"state": "enabled",
"conditions": {
"applications": {
"includeAuthenticationContextClassReferences": ["c2"]
}
},
"grantControls": {
"authenticationStrength": {
"id": "00000000-0000-0000-0000-000000000003"
}
},
"sessionControls": {
"signInFrequency": {
"isEnabled": true,
"frequencyInterval": "everyTime"
}
}
}

Note: The includeAuthenticationContextClassReferences field is only available on the beta endpoint. PIM Monitor uses $GraphBeta/identity/conditionalAccess/policies for this workload.

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 different slug), its inventory folder is not deleted. PIM Monitor moves it 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 first detected the removal.

What is preserved​

The full folder contents (definition, policy, and assignments) are moved as-is. This means:

  • The last known state of the entity is readable on disk without touching git history.
  • The git history of the original folder is preserved. git log inventory/directory-roles/old-role-name/ still shows all changes the entity went through before it was removed.
  • Notifications fire for the removal: it is recorded as a High severity change entry and sent via email or webhook if configured.

Renamed roles​

If a role is renamed in Entra ID, its slug changes (slugs are derived from displayName). PIM Monitor sees this as a removal of the old slug and a creation of the new one. The old folder is archived; a new folder is created on the same scan run.

Browsing archived entities​

ls inventory/archive/directory-roles/
git log --oneline inventory/archive/directory-roles/old-role-name_2026-04-26/
git show HEAD:inventory/archive/directory-roles/old-role-name_2026-04-26/assignments.json

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 id field: sorted by id
  • 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/