IntuneManagement 4.0.0-beta1

This commit is contained in:
Mikael Karlsson
2026-09-23 19:13:09 +10:00
commit 7869619510
892 changed files with 577109 additions and 0 deletions
+112
View File
@@ -0,0 +1,112 @@
# Bulk Assignments
## Purpose
`Set-GraphBulkAssignments` adds, replaces, or removes assignment targets across many policies. It is public and UI-independent; the WPF bulk assignment form builds an `IntuneManagerAssignmentSettings` object and calls it.
## Main Files
| File | Responsibility |
| --- | --- |
| `Public/Set-GraphBulkAssignments.ps1` | assignment orchestration and body generation. |
| `Classes/IntuneBaseClasses.ps1` | `IntuneManagerAssignmentSettings` and policy type assignment metadata. |
| `Public/Get-GraphPolicies.ps1` | list policies and include current assignments. |
| `UI/Extensions/IntuneManagerUI.ps1` | bulk assignment UI, group picker, app settings dialog. |
| `UI/XAML/BulkAssignments*.xaml` | forms for assignment targets and settings. |
## Supported Actions
| Action | Behavior |
| --- | --- |
| `Add` | union current assignments with selected assignment rows. |
| `Replace` | replace all current assignments with selected rows. |
| `Remove` | remove selected rows from current assignments. |
Graph `/assign` endpoints replace the full assignment collection, so `Add` and `Remove` must first load current assignments and compute the final collection.
## Target Types
Supported target descriptors:
| Target type | Required fields |
| --- | --- |
| `groupAssignmentTarget` | `GroupId` |
| `exclusionGroupAssignmentTarget` | `GroupId` |
| `allDevicesAssignmentTarget` | none |
| `allLicensedUsersAssignmentTarget` | none |
Group targets can include assignment filters:
| Field | Meaning |
| --- | --- |
| `FilterId` | assignment filter ID. |
| `FilterType` | `include` or `exclude`. |
## Assignment Shapes
Different Graph APIs use different assignment body shapes.
| Shape | Detection | Body |
| --- | --- | --- |
| simple | default | `{ target }` |
| app | `AssignmentsType = mobileAppAssignments` | `{ target, intent, settings? }` |
| script | `AssignmentsType = deviceHealthScriptAssignments` | `{ target, runRemediationScript?, runSchedule? }` |
The function rejects policy types that cannot be represented by one of these shapes.
## Policy Type Gating
`Test-BulkAssignmentSupported` checks:
1. `SupportsAssignments = true`;
2. `AssignmentsType` exists;
3. the type is not a known non-policy assignment API;
4. `Get-BulkAssignmentShape` returns a supported shape.
`Get-BulkAssignmentObjectType` determines assignment entry `@odata.type`. It first checks `PolicyType.AssignmentObjectType`, then uses built-in heuristics for known types.
## No-Op Detection
The command builds tuple signatures for assignment comparison.
| Signature | Used for | Fields |
| --- | --- | --- |
| target signature | Add/Remove identity and dedupe | target type, group ID, filter ID/type, app intent |
| full signature | no-op detection | target signature plus app settings, health-script schedule, remediation flag |
This distinction matters because `Add` should not create duplicates, but `Replace` must still update settings for an existing target.
## App Settings
The UI stores app settings by settings type name, for example `win32LobAppAssignmentSettings`. At execution time:
1. the policy object's `@odata.type` is mapped to the assignment settings type;
2. the selected row's matching settings hashtable is converted to Graph-shaped objects;
3. nested hashtables and arrays are converted recursively for JSON serialization;
4. settings are omitted when not configured so Graph defaults apply.
## Health Script Schedule
Health script settings are stored under the synthetic key `deviceHealthScriptAssignment`. The command turns those values into:
| UI value | Graph type |
| --- | --- |
| `Hourly` | `deviceHealthScriptHourlySchedule` |
| `Daily` | `deviceHealthScriptDailySchedule` |
| `Once` | `deviceHealthScriptRunOnceSchedule` |
## Summary Output
The command returns:
| Field | Meaning |
| --- | --- |
| `Types` | eligible policy types. |
| `PoliciesScanned` | policies loaded. |
| `PoliciesMatched` | policies matching name filter. |
| `PoliciesUpdated` | successful assignment POSTs. |
| `PoliciesSkipped` | no changes required. |
| `PoliciesFailed` | failed POSTs or missing batch responses. |
| `UnsupportedTypes` | selected types skipped by support gate. |
| `Duration` | elapsed time. |
+120
View File
@@ -0,0 +1,120 @@
# Bulk Export
## Purpose
`Start-GraphBulkExport` is the UI-independent bulk export driver. It can be called from the WPF UI, a scheduled task, or automation. It wraps listing, hydration, extra-data synchronization, file writing, and migration-table generation.
## Main Files
| File | Responsibility |
| --- | --- |
| `Public/Start-GraphBulkExport.ps1` | bulk export orchestration and helper functions. |
| `Public/Export-GraphPolicy.ps1` | per-policy file writing and migration hooks. |
| `Public/Get-GraphPolicies.ps1` | listing and assignment loading. |
| `Internal/MSGraph.ps1` | batch execution, migration table functions, navigation properties. |
| `UI/Extensions/IntuneManagerUI.ps1` | bulk export form and settings save UI. |
## Settings Resolution
Parameter precedence:
```text
IntuneManagerExportSettings defaults
<- SettingsFile JSON
<- explicit parameters
```
This allows saved scheduled-task settings while still making command-line overrides possible.
`ExportFullMembershipPrefixes` ("Get full membership") was removed: every group
path is batched now, and its only output was `#DirectMembers` /
`#DirectMemberCount` on the group sidecars, which nothing in the module read.
`Clear-BulkExportLegacyMembershipSetting` (`Internal/BulkExport.ps1`) runs at the
start of every export so a value that outlived the removal is never dropped in
silence: a non-empty value in a settings file is logged as a warning each run
(the file belongs to the caller and is not rewritten), and a value in the
settings store is warned about once and then cleared. The store sweep follows the
same precedence `Get-SettingValue` used, so it covers the per-tenant paths
(`<tenantId>\IntuneManager`, for the connected organization and for the export
token's tenant) as well as the global `IntuneManager` one.
## Target Resolution
Targets are resolved from:
| Input | Behavior |
| --- | --- |
| `-PolicyType` | exact type IDs. |
| `-PolicyGroup` | group IDs expanded to member policy types. |
| neither | all exportable groups and types. |
Unknown types/groups are logged and skipped.
## Parallel Pipeline
When `UseParallelBatchAPI` is enabled and PowerShell 7+ is running, bulk export uses a three-phase pipeline:
```text
Phase 1: list all selected types through Get-GraphPolicies
Phase 2: batch-fetch full policy bodies via Invoke-PolicyHydrate
Phase 2.5: sync extra data and prefetch assignment groups
Phase 3: write policy files type by type
```
When parallel mode is disabled, it processes each type sequentially:
```text
list type -> hydrate full objects -> sync extra data -> write type
```
## Extra Data Synchronization
Some Graph list/detail endpoints do not include all export data. `Invoke-PolicyExtraData` (a Phase-A wrapper that will shrink to nothing as helpers migrate to the per-class `_HasSubResourceBatch` contract) fills the remaining gaps before file writing.
| Helper | Data added | Status |
| --- | --- | --- |
| `Sync-BulkExportReusableSettings` | reusable setting instances. | Phase-A wrapper; migrates to class contract in Phase B. |
| `Sync-BulkExportBrandingImages` | branding image payloads. | Phase-A wrapper; migrates to `IntuneBrandingObject` contract. |
| `Sync-BulkExportAppConfigurationTargetApps` | targeted mobile app reference info. | Phase-A wrapper; migrates to AppConfig object contracts. |
| `Sync-BulkExportRoleAssignmentDetails` | role assignment expanded details. | Phase-A wrapper; migrates to `RoleDefinitionObject` Phase 2. |
| `Sync-BulkExportTermsOfUseFiles` | terms of use file data. | Phase-A wrapper; migrates to `TermsOfUseObject` contract. |
| `Sync-BulkExportMigrationGroups` | migration-table group resolution. | Cross-cutting; will be renamed `Invoke-MigrationGroupResolution`. |
| `Sync-BulkExportNestedGroupHierarchy` | nested-group hierarchy expansion. | Cross-cutting; will be renamed `Invoke-NestedGroupResolution`. |
ADMX definition values, presentation values, app dependencies, supersedence, and Win32 scripts are no longer handled by `Sync-Bulk*` helpers — those moved to the per-class `GetSubResourceBatchRequests` / `ApplySubResourceBatchResult` contract on `AdminTemplateObject` and `ApplicationObject`.
## Migration Performance
Bulk export resets migration caches at the start:
| Cache | Purpose |
| --- | --- |
| `_migFileCache` | in-memory `MigrationTable.json` objects. |
| `_migFileObjectsIndex` | duplicate prevention. |
| `_migFileDirty` | list of migration files to flush once. |
| `_migFilePathCache` | avoid repeated path resolution. |
| `_appConfigTargetAppCache` | avoid repeated app target lookups. |
`Sync-BulkExportMigrationGroups` prefetches assigned groups in one batch. `Add-GraphMigrationObject` can still fetch non-prefetched references such as Conditional Access users/groups and nested groups.
## Output
Returns a summary object:
| Field | Meaning |
| --- | --- |
| `Types` | number of policy types processed. |
| `Policies` | number of policies exported. |
| `Failed` | number of failed type operations. |
| `Duration` | elapsed time. |
## Extension Points
To make a policy type export fully:
1. ensure the list endpoint returns enough data or full hydration works;
2. add full-object URL handling when a type uses polymorphic endpoints;
3. add extra-data sync if the data is not part of the normal object body;
4. add `PostExportCommand` for references that must be included in migration data;
5. set properties-to-remove for import/update round trips.
+714
View File
@@ -0,0 +1,714 @@
# Command reference
Every command the module exports, with parameters, examples and what it returns.
Recipes that combine them are in [Examples.md](Examples.md).
## Conventions
- **Prefix.** The module exports with `DefaultCommandPrefix = 'IM'`, so `Get-GraphPolicies`
is called as `Get-IMGraphPolicies`. The prefix goes after the verb: `Invoke-MSGraphAPI`
becomes **`Invoke-IMMSGraphAPI`** (double M). Override with `Import-Module -Prefix`.
- **`-TokenId`.** Several tenants can be signed in at once. Commands that talk to Graph take
`-TokenId` (from `Get-IMAuthToken`) to say which; omitted, they use the default token -
the first sign-in, or the one connected with `-DefaultToken`.
- **Policy type and group ids** (`-PolicyType`, `-PolicyGroup`) are tab-completed, not
validated: an unknown id is reported, not rejected at bind time. The ids are the ones in
the policy table in [README.md](../README.md#supported-policy-types) - `DeviceConfiguration`,
`SettingsCatalog`, `CompliancePolicies`, `DeviceEnrollments`, and so on.
- **`-WhatIf` / `-Confirm`** are supported on `Remove-IMGraphPolicy`, `Set-IMSetting`,
`Remove-IMSetting`, `Use-IMSettingsStore`, `Export-IMSettingsStore` and
`Import-IMSettingsStore`. **`Start-IMGraphBulkDelete` has neither** - the caller owns the
confirmation.
- **Read-only** commands, safe for reporting and CI: `Get-IMAuthToken`, `Get-IMAccessibleTenant`,
`Get-IMGraphEffectivePermissions`, `Get-IMGraphPolicies`, `Get-IMGraphPolicyFromFile`,
`Compare-IMGraphPolicy`, `Get-IMGraphDocumentation`, `Get-IMDocumentationOutput`,
`Get-IMSetting`, `Get-IMSettingDefinition`, `Get-IMSettingsStore`.
## Contents
| Area | Commands |
|---|---|
| [Signing in and tokens](#signing-in-and-tokens) | `Connect-IMIntuneManagement`, `Get-IMAuthToken`, `Get-IMAccessibleTenant`, `Get-IMGraphEffectivePermissions`, `Invoke-IMMSGraphAPI` |
| [Working with policies](#working-with-policies) | `Get-IMGraphPolicies`, `Get-IMGraphPolicyFromFile`, `Import-IMGraphPolicy`, `Export-IMGraphPolicy`, `Copy-IMGraphPolicy`, `Remove-IMGraphPolicy`, `Compare-IMGraphPolicy` |
| [Bulk operations](#bulk-operations) | `Start-IMGraphBulkExport`, `Start-IMGraphBulkImport`, `Start-IMGraphBulkCopy`, `Start-IMGraphBulkDelete`, `Set-IMGraphBulkAssignments`, `Set-IMGraphBulkScopeTags`, `Save-IMGraphBulkExportSettings` |
| [Documentation](#documentation) | `Get-IMGraphDocumentation`, `Start-IMGraphBulkDocumentation`, `Get-IMDocumentationOutput` |
| [Settings](#settings) | `Get-IMSetting`, `Set-IMSetting`, `Remove-IMSetting`, `Get-IMSettingDefinition`, `Use-IMSettingsStore`, `Get-IMSettingsStore`, `Export-IMSettingsStore`, `Import-IMSettingsStore` |
| [UI](#ui) | `Show-IMMainWindow` |
---
## Signing in and tokens
### Connect-IMIntuneManagement
Authenticate to Microsoft Graph. Each call registers a token; several tenants can be
live at once. Returns the token as a `PSCustomObject` (`TokenId`, `TenantId`,
`TenantName`, `Provider`, `Account`, `ExpiresOn`).
The parameter set chooses the credential. `-Provider` chooses the implementation:
`MSAL` (default) or `OAuth`; the saved *Active authentication provider* setting is the
fallback.
| Set | Parameters | Notes |
|---|---|---|
| Interactive (default) | `-Interactive` `[-TenantId]` `[-User]` `[-ForceInteractive]` `[-AuthenticationBroker]` `[-Browser]` | Silent from cache first, then browser. `-AuthenticationBroker` = WAM, Windows + PS 7 only. With `-Provider OAuth` this routes to device code. |
| DeviceCode | `-DeviceCode` `[-TenantId]` `[-AppId]` | Code shown here, browser step on any device. MFA / FIDO2 capable. |
| Secret | `-TenantId` `-AppId` `-Secret` | App registration with client secret. |
| Certificate | `-TenantId` `-AppId` `-Certificate <thumbprint or X509Certificate2>` | Looked up in `Cert:\CurrentUser\My`, then `Cert:\LocalMachine\My`. |
| CertificatePath | `-TenantId` `-AppId` `-CertificatePath` `[-CertificatePassword <SecureString>]` | `.pfx` file. |
| Token | `-Token` | Bring your own Graph bearer token. Cannot be refreshed. |
| ManagedIdentity | `-ManagedIdentity` `[-AppId]` | System-assigned, or user-assigned via `-AppId`. `-Provider OAuth`. |
| OAuthFederated | `-TenantId` `-AppId` `-FederatedTokenFile` or `-FederatedToken` | Workload identity federation (AKS, GitHub Actions OIDC). `-Provider OAuth`. |
| OAuthCredential | `-TenantId` `-AppId` `-Credential <PSCredential>` | ROPC. Non-MFA accounts only. `-Provider OAuth`. |
Common to every set: `-Cloud Public|USGov|USGovDOD|China` (default from the
*DefaultCloud* setting), `-DefaultToken` (make this the default token), `-Provider`.
```powershell
# Interactive, resumes silently from the cache when it can
Connect-IMIntuneManagement -Interactive
# A specific tenant, and force a fresh prompt
Connect-IMIntuneManagement -Interactive -TenantId contoso.onmicrosoft.com -ForceInteractive
# Unattended: client secret
Connect-IMIntuneManagement -TenantId contoso.onmicrosoft.com -AppId $appId -Secret $secret
# Unattended: certificate thumbprint
Connect-IMIntuneManagement -TenantId contoso.onmicrosoft.com -AppId $appId -Certificate 'A1B2C3...'
# Unattended: .pfx
Connect-IMIntuneManagement -TenantId $t -AppId $a -CertificatePath C:\certs\app.pfx `
-CertificatePassword (ConvertTo-SecureString $pw -AsPlainText -Force)
# Device code - headless box, sign in from a phone
Connect-IMIntuneManagement -DeviceCode
# OAuth provider: managed identity on an Azure VM / Function / Automation account
Connect-IMIntuneManagement -Provider OAuth -ManagedIdentity
# OAuth provider: GitHub Actions OIDC / AKS workload identity
Connect-IMIntuneManagement -Provider OAuth -TenantId $t -AppId $a -FederatedTokenFile $env:AZURE_FEDERATED_TOKEN_FILE
# Bring your own token (the only way to reach APIs closed to public clients)
Connect-IMIntuneManagement -Token $bearer
# Sovereign cloud
Connect-IMIntuneManagement -Interactive -Cloud USGov
```
### Get-IMAuthToken
List the tokens currently held, across providers. Returns `IMAuthToken[]` -
`TokenId`, `TenantId`, `TenantName`, `Provider`, `Account`/`UPN`, `AppId`, `ExpiresOn`,
`IsDefault`.
| Parameter | Type | Notes |
|---|---|---|
| `-TokenId` | int | One token. |
| `-Provider` | string | Filter by provider. |
| `-TenantId` | string | Filter by tenant. |
```powershell
Get-IMAuthToken
# Pick a tenant's token for a later command
$prod = Get-IMAuthToken | Where-Object TenantName -eq 'Contoso Prod'
Get-IMGraphPolicies -PolicyType CompliancePolicies -TokenId $prod.TokenId
```
### Get-IMAccessibleTenant
List the tenants the signed-in account can reach - its home tenant and every tenant it
is a guest in. Graph cannot answer this; the list comes from Azure Resource Manager, so
the app registration needs the delegated permission *Azure Service Management /
user_impersonation*, and only the MSAL provider implements it. Without either, the
command warns and returns nothing.
| Parameter | Type | Notes |
|---|---|---|
| `-TokenId` | int | Ask the provider that owns this token. |
```powershell
Get-IMAccessibleTenant
# Confirm a guest tenant is reachable, then connect to it silently
$guest = Get-IMAccessibleTenant | Where-Object displayName -eq 'Fabrikam'
Connect-IMIntuneManagement -Interactive -TenantId $guest.tenantId
```
### Get-IMGraphEffectivePermissions
What the signed-in identity can actually do, per policy type: the app's token scopes
combined with the user's Intune RBAC or Entra directory roles. Lets a script learn it is
read-only for a type *before* a bulk import, instead of collecting 403s halfway through.
One row per policy type: `Id`, `Required`, `TokenAccess`, `RoleAccess`,
`EffectiveAccess`, `Result` (Match / Read-only / No access), plus the raw
`TokenLevel` / `RbacLevel` / `EffectiveLevel` (Full / Limited / None) and `Reason`.
| Parameter | Type | Notes |
|---|---|---|
| `-TokenId` | int | Evaluate this token. |
| `-PolicyType` | string[] | Only these type ids. |
| `-Raw` | switch | Return the RBAC context itself (allowed resource actions, the catalog, the raw response) instead of the table. |
App-only tokens bypass Intune RBAC, so `RbacLevel` is `$null` and the token level is the
answer. Scope tags are not modelled. The answer is cached per token and refreshed when
the token is re-issued.
```powershell
# What can this user not change, and why?
Get-IMGraphEffectivePermissions | Where-Object EffectiveLevel -ne Full |
Format-Table Id, TokenLevel, RbacLevel, EffectiveLevel, Reason
# Gate a bulk import on write access to the types it touches
$blocked = Get-IMGraphEffectivePermissions -PolicyType DeviceConfiguration, SettingsCatalog |
Where-Object EffectiveLevel -ne Full
if ($blocked) { throw "Read-only for: $($blocked.Id -join ', ')" }
```
### Invoke-IMMSGraphAPI
The low-level Graph call every other command uses: resolves the token, adds headers,
handles throttling, paging, batching and claims challenges. Use it for anything the
policy commands do not cover.
| Parameter | Type | Default | Notes |
|---|---|---|---|
| `-Url` | string | required | Relative (`deviceManagement/deviceCategories`) or absolute. |
| `-HttpMethod` (`-Method`) | string | `GET` | `GET`, `POST`, `PATCH`, `PUT`, `DELETE`, `OPTIONS`. |
| `-Content` (`-Body`) | string | | Request body, JSON. |
| `-Headers`, `-AdditionalHeaders` | hashtable | | |
| `-GraphVersion` | string | `beta` | `beta` or `v1.0`. The *UseGraphV1* setting flips the default. |
| `-ODataMetadata` | string | `full` | `full`, `minimal`, `none`, `skip`. |
| `-AllPages` | switch | | Follow `@odata.nextLink` to the end. |
| `-PageSize` | int | | `$top` for the first page. |
| `-Batch` | switch | | Queue into the current `$batch` instead of sending. |
| `-Outfile` | string | | Save the response body to a file. |
| `-FullResponseObject` | switch | | Return status code and headers as well as the body. |
| `-NoError` | switch | | Return `$null` on failure instead of logging an error. |
| `-SkipAuthentication` | switch | | |
| `-TokenId` | int | default token | |
Returns the parsed body (collections under `.value`). A Multi Admin Approval 412 comes
back with `ApprovalPending = $true` and the `ApprovalCode` on the result.
```powershell
# List with paging
(Invoke-IMMSGraphAPI -Url 'deviceManagement/deviceCategories' -AllPages).value
# Create
Invoke-IMMSGraphAPI -Url 'deviceManagement/deviceCategories' -HttpMethod POST `
-Content '{"displayName":"Kiosks","description":"Shared kiosk devices"}'
# Status code as well as body
$r = Invoke-IMMSGraphAPI -Url "deviceManagement/deviceCategories/$id" -HttpMethod DELETE -FullResponseObject
$r.StatusCode
```
---
## Working with policies
The policy commands pass **policy objects** (`IntunePolicyBase`) down the pipeline. Get
them from a tenant with `Get-IMGraphPolicies` or from files with
`Get-IMGraphPolicyFromFile`; `Import`, `Export`, `Copy`, `Remove` and `Compare` consume
them.
### Get-IMGraphPolicies
List policies of one or more types or groups. Shared list URLs are coalesced and
batched. Returns `IntunePolicyBase[]` - each with `Name`, `Id`, `PolicyType`, `Object`
(the Graph JSON) and, with `-IncludeAssignments`, `Object.assignments`.
| Set | Parameters | Notes |
|---|---|---|
| PolicyType | `-PolicyType <string[]>` (position 0, pipeline) | |
| PolicyGroup | `-PolicyGroup <string[]>` (position 0, pipeline) | Every type in the group. |
| Paging | `-Paging NextPage\|AllRemainingPages` | Continue a paged listing. |
Common: `-NameFilter <string>` (server-side where the endpoint allows, always re-checked
client-side), `-IncludeAssignments`, `-SinglePage`, `-TokenId`.
```powershell
Get-IMGraphPolicies -PolicyType CompliancePolicies
Get-IMGraphPolicies -PolicyGroup DeviceConfiguration -IncludeAssignments
Get-IMGraphPolicies -PolicyType SettingsCatalog -NameFilter 'Baseline'
'CompliancePolicies', 'ConditionalAccess' | Get-IMGraphPolicies
```
### Get-IMGraphPolicyFromFile
Load exported json files as policy objects, resolving each file's policy type from its
`@odata.type` and folder. The result is what `Import-IMGraphPolicy` and
`Compare-IMGraphPolicy` accept.
| Parameter | Type | Notes |
|---|---|---|
| `-InputObject` (`-FileInfo`) | `IO.FileInfo[]`, pipeline | The files. |
| `-FromPolicyTypes` | `IntunePolicyTypeBase[]` | Narrow the candidate types (e.g. when the folder name does not match). |
| `-TenantId` | string | Record the source tenant on the objects. |
```powershell
Get-ChildItem C:\IntuneExport\CompliancePolicies\*.json | Get-IMGraphPolicyFromFile
```
### Import-IMGraphPolicy
Create the piped policies in the tenant. Types are imported in dependency order (scope
tags and filters first, policy sets last), references are translated where the export
carries the information to do so - assignment groups, scope tags, targeted apps, ADMX
setting ids - and per-type hooks run (Win32 content upload, ADMX/ADML files, Terms of
Use PDF). Returns one result per policy with `ImportedObject` (the created policy),
`SourceObject` and `Success`.
| Parameter | Type | Notes |
|---|---|---|
| `-InputObject` | `IntunePolicyBase[]`, pipeline | From `Get-IMGraphPolicyFromFile` or another tenant's `Get-IMGraphPolicies`. |
| `-TokenId` | int | Destination tenant. |
```powershell
# Everything under a folder
Get-ChildItem C:\IntuneExport -Recurse -Filter *.json | Get-IMGraphPolicyFromFile | Import-IMGraphPolicy
# Tenant to tenant without touching disk
$src = (Get-IMAuthToken | Where-Object TenantName -eq 'Lab').TokenId
$dst = (Get-IMAuthToken | Where-Object TenantName -eq 'Prod').TokenId
Get-IMGraphPolicies -PolicyType CompliancePolicies -TokenId $src | Import-IMGraphPolicy -TokenId $dst
```
### Export-IMGraphPolicy
Write the piped policies to json under the export folder, one file per policy, in the
type's subfolder. Assignments, scope-tag names and organization tokens follow the
`IntuneManagerExportSettings` passed in.
| Parameter | Type | Notes |
|---|---|---|
| `-InputObject` | `IntunePolicyBase[]`, pipeline | |
| `-ExportSettings` | `IntuneManagerExportSettings` | Required. `[IntuneManagerExportSettings]::new()` starts from the saved settings. |
| `-PassThru` | switch | Emit the full path of each written file. |
```powershell
$s = [IntuneManagerExportSettings]::new()
$s.ExportFolder = 'C:\IntuneExport'
$s.ExportAssignments = $true
Get-IMGraphPolicies -PolicyType CompliancePolicies | Export-IMGraphPolicy -ExportSettings $s -PassThru
```
### Copy-IMGraphPolicy
Create a copy of each piped policy - in the same tenant, or in another with `-TokenId`.
Returns the new policies.
| Parameter | Type | Notes |
|---|---|---|
| `-InputObject` | `IntunePolicyBase[]`, pipeline | |
| `-Name` | string | Required. Name of the copy. With several inputs, use the patterns instead. |
| `-Description` | string | |
| `-CopyFromPatternName` / `-CopyFromPatternDescription` | string | Substring in the source name/description replaced by `-Name` / `-Description` - for copying many at once. |
| `-ScopeTagIds` | string[] | Scope tags for the copy. Omitted, the copy inherits the source's; an empty array clears them. |
| `-TokenId` | int | Destination tenant. |
The Copy dialog in the UI pre-fills the name from the type's `CopyDefaultName` template
where one is set (`%Name% Copy`); from a script the name is always what you pass.
```powershell
Get-IMGraphPolicies -PolicyType CompliancePolicies -NameFilter 'Pilot - W11' |
Copy-IMGraphPolicy -Name 'Prod - W11'
# Many at once: "Pilot - X" -> "Prod - X"
Get-IMGraphPolicies -PolicyGroup DeviceConfiguration -NameFilter 'Pilot - ' |
Copy-IMGraphPolicy -CopyFromPatternName 'Pilot - ' -Name 'Prod - '
```
### Remove-IMGraphPolicy
Delete the piped policies. Supports `-WhatIf` and `-Confirm` (ConfirmImpact Medium).
Batched when batching is on. Returns the deleted policies.
| Parameter | Type | Notes |
|---|---|---|
| `-InputObject` | `IntunePolicyBase[]`, pipeline | |
| `-TokenId` | int | |
```powershell
Get-IMGraphPolicies -PolicyType DeviceCategories -NameFilter '[Test]' | Remove-IMGraphPolicy -WhatIf
Get-IMGraphPolicies -PolicyType DeviceCategories -NameFilter '[Test]' | Remove-IMGraphPolicy -Confirm:$false
```
### Compare-IMGraphPolicy
Compare two or more policies property by property, or run one of the compare providers
over pairs. Returns rows of `Property`, `Value1`, `Value2`, `Match`.
| Set | Parameters | Notes |
|---|---|---|
| Direct | `-Policies <object[]>` (position 0, pipeline) | Two or more policy objects. |
| ExportFiles | `-ExportFiles <CompareExportFilesProvider>` | Each tenant policy vs its exported file. |
| IntuneWithExport | `-IntuneWithExport <CompareIntuneWithExportProvider>` | |
| NamedObjects | `-NamedObjects <CompareNamedObjectsProvider>` | Pairs matched by name pattern. |
| ExportedFolders | `-ExportedFolders <CompareExportedFoldersProvider>` | Two export folders. |
The provider sets take `-PolicyGroupIds <string[]>` to limit the groups compared.
Provider classes are in `Classes/CompareClasses.ps1`; see [Compare.md](Compare.md).
```powershell
$a, $b = Get-IMGraphPolicies -PolicyType CompliancePolicies -NameFilter 'W11' | Select-Object -First 2
Compare-IMGraphPolicy -Policies @($a, $b) | Where-Object Match -eq $false
# A tenant against last night's export
$p = [CompareIntuneWithExportProvider]::new()
$p.ExportPath = 'C:\IntuneExport'
Compare-IMGraphPolicy -IntuneWithExport $p -PolicyGroupIds DeviceConfiguration
```
---
## Bulk operations
The bulk commands are the headless form of the Bulk menu. Each returns a summary object
with counts and `Duration`. `-PolicyType` and `-PolicyGroup` select what to operate on;
`-Filter` is a name filter: a literal, case-insensitive substring of the policy name, the
same rule as 3.x. `-Filter '[Test]'` selects names containing the text `[Test]`; there is no
regex or wildcard syntax.
### Start-IMGraphBulkExport
Export whole policy groups or types to disk. Precedence: `-SettingsFile` < `-ExportSettings`
< explicit parameters. Returns `Types`, `Policies`, `Failed`, `Duration`.
| Parameter | Type | Notes |
|---|---|---|
| `-ExportFolder` | string | Root folder. Required unless a settings source provides it. |
| `-PolicyType` / `-PolicyGroup` | string[] | Default: every type whose group allows export. |
| `-Filter` | string | Name filter. |
| `-ExportAssignments` | bool | |
| `-AddCompanyName` | bool | Add a tenant-name folder level. |
| `-ExportSettings` | `IntuneManagerExportSettings` | A settings instance. |
| `-SettingsFile` | string | A file written by `Save-IMGraphBulkExportSettings`. |
| `-TokenId` | int | |
```powershell
Start-IMGraphBulkExport -ExportFolder C:\IntuneExport -PolicyGroup DeviceConfiguration
Start-IMGraphBulkExport -ExportFolder C:\IntuneExport -ExportAssignments $true -Filter 'Baseline'
Start-IMGraphBulkExport -SettingsFile .\nightly-export.json
```
### Start-IMGraphBulkImport
Import an export folder. Groups are processed in dependency order. Returns `Groups`,
`Imported`, `Duration`.
| Parameter | Type | Notes |
|---|---|---|
| `-ImportFolder` | string | Required. |
| `-PolicyGroup` | string[] | Default: every group that allows import. |
| `-Filter` | string | |
| `-ImportType` | string | `alwaysImport` (default), `skipIfExist`, `update`, `replace`, `replace_with_assignments`. |
| `-ImportAssignments`, `-ImportScopeTags`, `-ReplaceDependencyIDs` | bool | Persisted as the like-named settings for the session. |
| `-TokenId` | int | |
```powershell
Start-IMGraphBulkImport -ImportFolder C:\IntuneExport
Start-IMGraphBulkImport -ImportFolder C:\IntuneExport -PolicyGroup DeviceConfiguration -Filter 'Baseline' -ImportType update
```
### Start-IMGraphBulkCopy
Copy every policy whose name contains a pattern, to the same name with the pattern
replaced. Returns `Types`, `Copied`, `Skipped`, `FailedTypes`, `UnknownSelectors`, `Duration`.
| Parameter | Type | Notes |
|---|---|---|
| `-CopyFromPattern` | string | Required. |
| `-CopyToPattern` | string | Required. |
| `-PolicyType` / `-PolicyGroup` | string[] | Default: every type whose group allows copy. |
| `-TokenId` | int | |
```powershell
Start-IMGraphBulkCopy -CopyFromPattern 'Pilot - ' -CopyToPattern 'Prod - ' -PolicyGroup DeviceConfiguration
```
### Start-IMGraphBulkDelete
Delete every policy in the selected groups that matches the filter. **No `-WhatIf`, no
confirmation** - list first with `Get-IMGraphPolicies` using the same filter. Groups are
deleted in reverse dependency order. Returns `Groups`, `Deleted`, `Duration`.
| Parameter | Type | Notes |
|---|---|---|
| `-PolicyGroup` | string[] | Required. |
| `-Filter` | string | Empty means every object in the group. |
| `-TokenId` | int | |
```powershell
Get-IMGraphPolicies -PolicyGroup DeviceConfiguration -NameFilter '[Test]' | Select-Object Name # look first
Start-IMGraphBulkDelete -PolicyGroup DeviceConfiguration -Filter '[Test]'
```
### Set-IMGraphBulkAssignments
Add, replace or remove assignments across types or groups. Handles the three assignment
shapes - simple targets, app assignments with intent and per-platform settings, health
scripts with schedules. App types that cannot take a filter (web apps) are assigned
without it, with a log line. Returns `Types`, `PoliciesScanned`, `PoliciesMatched`,
`PoliciesUpdated`, `PoliciesSkipped`, `PoliciesFailed`, `PoliciesUnsupported`, `Duration`.
| Parameter | Type | Notes |
|---|---|---|
| `-Action` | string | `Add`, `Replace`, `Remove`. |
| `-Assignments` | `PSCustomObject[]` | Descriptors: `TargetType` (`groupAssignmentTarget`, `exclusionGroupAssignmentTarget`, `allDevicesAssignmentTarget`, `allLicensedUsersAssignmentTarget`), `GroupId`, `GroupName`, `FilterId`, `FilterType` (`include`/`exclude`), `Intent` (apps), `Settings` (hashtable per platform). |
| `-Filter` | string | Name filter. |
| `-PolicyType` / `-PolicyGroup` | string[] | |
| `-AssignmentSettings` | `IntuneManagerAssignmentSettings` | Alternative to the three above. |
| `-TokenId` | int | |
```powershell
$a = [PSCustomObject]@{
TargetType = 'groupAssignmentTarget'
GroupId = '<entra-group-id>'
GroupName = 'All Helpdesk Devices'
FilterId = '<assignment-filter-id>'
FilterType = 'include'
}
Set-IMGraphBulkAssignments -Action Add -Assignments @($a) -PolicyGroup DeviceConfiguration -Filter 'Baseline'
# Apps: required install to a group
$app = [PSCustomObject]@{ TargetType = 'groupAssignmentTarget'; GroupId = $gid; GroupName = 'Pilot'; Intent = 'required' }
Set-IMGraphBulkAssignments -Action Add -Assignments @($app) -PolicyType Applications -Filter 'Office'
Set-IMGraphBulkAssignments -Action Remove -Assignments @($a) -PolicyGroup DeviceConfiguration
```
See [BulkAssignments.md](BulkAssignments.md) for the settings hashtables.
### Set-IMGraphBulkScopeTags
Add, replace or remove scope tags across types or groups; `-CleanupOrphans` removes
references to tags that no longer exist. Returns `Types`, `PoliciesScanned`,
`PoliciesMatched`, `PoliciesUpdated`, `PoliciesSkipped`, `PoliciesFailed`, `Duration`.
| Parameter | Type | Notes |
|---|---|---|
| `-Action` | string | `Add`, `Replace`, `Remove`. |
| `-ScopeTagIds` | string[] | Tag ids (`0` is the default tag). |
| `-Filter` | string | |
| `-CleanupOrphans` | bool | |
| `-PolicyType` / `-PolicyGroup` | string[] | |
| `-ScopeTagSettings` | `IntuneManagerScopeTagSettings` | Alternative to the above. |
| `-TokenId` | int | |
```powershell
Set-IMGraphBulkScopeTags -Action Add -ScopeTagIds 3, 4 -PolicyGroup DeviceConfiguration
Set-IMGraphBulkScopeTags -CleanupOrphans $true
```
### Save-IMGraphBulkExportSettings
Write an export configuration to a json file that `Start-IMGraphBulkExport -SettingsFile`
reads - the way to schedule the same export nightly.
| Parameter | Type | Notes |
|---|---|---|
| `-Path` | string | Required. |
| `-ExportSettings` | `IntuneManagerExportSettings` | Required. |
| `-PolicyGroup` / `-PolicyType` | string[] | What the file selects. |
```powershell
$s = [IntuneManagerExportSettings]::new()
$s.ExportFolder = '\\server\intune\exports'; $s.ExportAssignments = $true
Save-IMGraphBulkExportSettings -Path .\nightly-export.json -ExportSettings $s -PolicyGroup DeviceConfiguration, Compliance
Start-IMGraphBulkExport -SettingsFile .\nightly-export.json
```
---
## Documentation
### Get-IMGraphDocumentation
Document one policy and return the result object - `BasicInfo`, `FilteredSettings`,
`Assignments`, `Scripts`, `CustomTables` and the rest - without writing a file. Useful
for building your own report.
| Parameter | Type | Notes |
|---|---|---|
| `-PolicyObject` | pipeline | A policy from `Get-IMGraphPolicies` or `Get-IMGraphPolicyFromFile`. |
| `-Language` | string | `en` default; any language the strings ship in. |
| `-Options` | hashtable | See [Documentation.md](Documentation.md#options) for every key. |
```powershell
$p = Get-IMGraphPolicies -PolicyType ConditionalAccess | Select-Object -First 1
$doc = Get-IMGraphDocumentation -PolicyObject $p
$doc.FilteredSettings | Format-Table Name, Value
```
### Start-IMGraphBulkDocumentation
Document many policies through one or more output providers. Selects by object, type,
group, or an export folder. Returns the run summary; files land where each output's
options say.
| Set | Parameters |
|---|---|
| PolicyObject | `-PolicyObject` (pipeline) |
| PolicyType | `-PolicyType <string[]>` |
| PolicyGroup | `-PolicyGroup <string[]>` |
| Folder | `-SourceFolder <string>` - an export folder. Still needs a signed-in tenant (any tenant) for setting definitions and templates; source-tenant names come from the export's migration table or stay as ids. |
Common: `-OutputFormat <string>` (required, position 0; comma-separated: `html`, `md`,
`word`, `json`, `csv`, `atlassian`), `-Language`, `-Options`.
```powershell
Start-IMGraphBulkDocumentation -OutputFormat html -PolicyGroup Compliance
Start-IMGraphBulkDocumentation -OutputFormat 'md,json' -SourceFolder C:\IntuneExport
Get-IMGraphPolicies -PolicyType SettingsCatalog -NameFilter 'Baseline' | Start-IMGraphBulkDocumentation -OutputFormat word
```
### Get-IMDocumentationOutput
List the registered output providers - `Name` and `Value` (what `-OutputFormat` matches).
No parameters.
```powershell
Get-IMDocumentationOutput | Format-Table Name, Value
```
---
## Settings
Settings are read by key. Three stores: the registry (Windows default), a json file, or
memory. Effective value = tenant override, else global, else the registered default.
See [Settings.md](Settings.md).
### Get-IMSetting
| Parameter | Type | Notes |
|---|---|---|
| `-Key` | string, position 0, pipeline | Omit for every registered setting (implies `-Detailed`). |
| `-Scope` | string | `Effective` (default), `Global`, `Tenant`. |
| `-TenantID` | string | Default: the connected tenant. |
| `-SubPath` | string | For keys stored under a sub-path (none for most). |
| `-Detailed` | switch | Include `Source` - Tenant, Global or Default. |
```powershell
Get-IMSetting ExportFolder
Get-IMSetting UseBatchAPI -Detailed
Get-IMSetting | Where-Object Source -ne 'Default' # everything actually configured
```
### Set-IMSetting
Supports `-WhatIf`.
| Parameter | Type | Notes |
|---|---|---|
| `-Key` | string, position 0 | Required. |
| `-Value` | position 1 | Required; `$null` removes the value. |
| `-Scope` | string | `Global` (default) or `Tenant`. |
| `-TenantID`, `-SubPath`, `-PassThru` | | |
```powershell
Set-IMSetting ExportFolder 'C:\Intune\Export'
Set-IMSetting ExportFolder '\\server\intune\contoso' -Scope Tenant
Set-IMSetting UseParallelBatchAPI $true
```
### Remove-IMSetting
Remove a stored value so the next level applies. Supports `-WhatIf`.
| Parameter | Type | Notes |
|---|---|---|
| `-Key` | string, position 0 | Required. |
| `-Scope` | string | `Global` (default) or `Tenant`. |
| `-TenantID`, `-SubPath` | | |
```powershell
Remove-IMSetting ExportFolder -Scope Tenant
```
### Get-IMSettingDefinition
What the module knows about its settings: `Key`, `Title`, `Section`, `Type`,
`DefaultValue`, `Description`. Wildcards on both parameters.
| Parameter | Type |
|---|---|
| `-Key` | string, position 0 |
| `-Section` | string |
```powershell
Get-IMSettingDefinition | Format-Table Key, Section, Type, DefaultValue
Get-IMSettingDefinition -Key *Export*
```
### Use-IMSettingsStore
Choose the store for the rest of the session. Supports `-WhatIf`.
| Set | Parameters | Notes |
|---|---|---|
| Memory (default) | `-Memory` | Nothing is read from or written to the machine. |
| Json | `-Path <file>` (position 0) | Created if missing. |
| Registry | `-Registry` | Windows only. |
`-Seed` copies the persisted settings into the new store; `-PassThru` returns the store.
```powershell
# The runbook pattern: an empty store, then the run's configuration from source control
Use-IMSettingsStore -Memory
Import-IMSettingsStore -Path .\runbook-settings.json
Use-IMSettingsStore -Path 'D:\shared\IntuneManagement.json'
```
### Get-IMSettingsStore
Which store is active and whether it persists: `Mode`, `Persisted`, `Path`.
`-IncludeValues` adds every value.
```powershell
Get-IMSettingsStore
(Get-IMSettingsStore -IncludeValues).Values | Format-Table
```
### Export-IMSettingsStore
Write the whole active store to a json file - capture a working configuration once and
commit it. Supports `-WhatIf`. Takes `-Path` (position 0, required); the folder is
created and an existing file replaced.
```powershell
Export-IMSettingsStore -Path .\intune-settings.json
```
### Import-IMSettingsStore
Merge a json settings file into the active store (existing keys are overwritten, others
kept). Supports `-WhatIf`. Takes `-Path` (position 0, required). A missing file is
reported, not silently ignored.
```powershell
Use-IMSettingsStore -Memory
Import-IMSettingsStore -Path .\runbook-settings.json
Import-IMSettingsStore -Path .\baseline.json -WhatIf # which store would this land in?
```
---
## UI
### Show-IMMainWindow
Show the application window for the active UI backend (WPF on Windows, Avalonia
elsewhere). Takes an optional `-View` to open on. Not available when the `UI` folder has
been removed from the deployment, and not for use from an ordinary `pwsh` session on
macOS - launch with `Start-Avalonia.command` there.
```powershell
Import-Module .\IntuneManagement.psd1
Show-IMMainWindow
```
+105
View File
@@ -0,0 +1,105 @@
# Compare
## Purpose
The compare feature compares policies from Intune, export folders, selected objects, or named providers. It is used by the UI to show differences and by bulk compare workflows to generate structured output.
## Main Files
| File | Responsibility |
| --- | --- |
| `Public/Compare-GraphPolicy.ps1` | public entry point into compare UI. |
| `Internal/Compare.ps1` | compare engine and output providers. |
| `Classes/CompareClasses.ps1` | compare provider classes. |
| `UI/Extensions/CompareUI.ps1` | compare forms and UI orchestration. |
## Compare Providers
Compare providers abstract the source of objects:
| Provider | Source |
| --- | --- |
| `CompareExportFilesProvider` | exported JSON files. |
| `CompareIntuneWithExportProvider` | live Intune versus export folder. |
| `CompareNamedObjectsProvider` | named live objects. |
| `CompareExportedFoldersProvider` | two export folders. |
## Object Normalization
Before comparing, policies are converted to a compare-friendly JSON/object form:
1. full policy object is resolved when needed;
2. ignored core properties are removed;
3. settings catalog values can be flattened into readable keys;
4. documentation plugin output can be used when available;
5. output rows are generated with property paths and differences.
## Compare Providers - options
Each provider is a class whose properties are the options. Construct it, set the
properties, pass it to `Compare-IMGraphPolicy` through the matching parameter (or select
it in the Bulk > Compare form, which shows the same fields).
| Provider | Parameter | Properties |
|---|---|---|
| `CompareExportFilesProvider` | `-ExportFiles` | `ExportPath` - the export root; `NameFilter` - only policies whose name contains this. |
| `CompareIntuneWithExportProvider` | `-IntuneWithExport` | `ExportPath`, `NameFilter` - as above; each live policy is compared with its exported file. |
| `CompareNamedObjectsProvider` | `-NamedObjects` | `SourcePattern`, `ComparePattern` - name patterns that pair objects (`Pilot - X` with `Prod - X`); `SavePath` - where the result is written; `RemoveProperties` - properties dropped before comparing, default `@('Id')`. |
| `CompareExportedFoldersProvider` | `-ExportedFolders` | `SourcePath`, `ComparePath` - two export roots; `NameFilter`. |
All four take `-PolicyGroupIds <string[]>` on the command to limit the groups compared.
```powershell
$p = [CompareNamedObjectsProvider]::new()
$p.SourcePattern = 'Pilot - '
$p.ComparePattern = 'Prod - '
$p.SavePath = 'C:\Reports\pilot-vs-prod.csv'
Compare-IMGraphPolicy -NamedObjects $p -PolicyGroupIds DeviceConfiguration, Compliance
```
## Run options
Set once per run with `Set-CompareRuntimeOptions` (the Bulk > Compare form does this for
you); the compare functions read them.
| Option | Values | Effect |
|---|---|---|
| `CompareType` | `Property` (default), `Documentation` | Which comparison type - see the strategies below. `Documentation` compares what the documentation engine renders, so two policies that document the same are equal even if raw json differs. |
| `IgnoreCoreProperties` | bool | Skip id, timestamps, version and the other server-side properties. |
| `SaveType` | `objectType`, `all` | One output file per object type, or one file for everything. |
| `OutputProvider` | CSV or Json provider instance | Chosen by the output file's extension when saving from the form (`.csv` / `.json`). |
| `CsvDelimiter` | string | CSV delimiter; default is the culture's list separator. |
| `ObjectSeparator` | string | Separator between items of a multi-value property in the output. |
| `SkipAssignments` | bool | Leave assignments out of the comparison. |
The single-object form adds a result filter (All / Mismatch / Match) that only affects the
grid, not the saved file.
## Compare Strategies
| Strategy | Function | Notes |
| --- | --- | --- |
| property compare | `Compare-ObjectsBasedonProperty` | compares raw property paths. |
| settings compare | `Compare-ObjectsBasedonSettings` | extracts settings catalog and intent settings. |
| documentation compare | `Compare-ObjectsBasedonDocumentation` | uses `Invoke-ObjectDocumentation` when present. |
## Output
Bulk compare writes through an output provider. Two ship: **CSV** (`CompareCSVOutputProvider`,
honours `CsvDelimiter`) and **JSON** (`CompareJsonOutputProvider`). The provider is picked
from the output file's extension, or selected in the form.
Typical output fields include:
| Field | Meaning |
| --- | --- |
| object name/type | source policy identity. |
| property path | location of difference. |
| source value | value in left/source object. |
| target value | value in right/target object. |
| result type | same, different, missing, extra, etc. |
## Extension Notes
Add new compare behavior in `Internal/Compare.ps1` when the comparison semantics change. Add new source behavior by creating a provider class in `Classes/CompareClasses.ps1`. Keep UI code limited to collecting options and displaying results.
+173
View File
@@ -0,0 +1,173 @@
# Running on macOS and Linux (experimental)
IntuneManagement.Next runs outside Windows through the **Avalonia** UI backend. The
engine is the same one the Windows build uses; only the presentation layer differs.
> **Experimental.** Linux has been exercised during development. macOS has been
> started end to end (module import, sign-in, policy browsing) on Apple Silicon with
> PowerShell 7.6.6, but the native GUI paths have had far less mileage than Windows.
> Treat a macOS run as a bug hunt.
The app says so itself: on the first non-Windows launch it shows a notice with a
**Do not show this message again** checkbox. See [Turning the notice back on](#turning-the-notice-back-on).
## Requirements
| | |
|---|---|
| PowerShell | **7.4 or newer** (`pwsh`) on Linux and macOS - 7.6 / 7.7 included; nothing is pinned to a particular 7.x. pwsh bundles its own .NET runtime, so **no separate .NET install is needed** on either platform. Windows PowerShell 5.1 remains supported by WPF, not Avalonia. |
| Display | A desktop session. X11 or Wayland on Linux, Aqua on macOS. |
## Launching
```sh
./Start-Avalonia.command # macOS (also double-clickable in Finder) and Linux
```
or directly:
```sh
pwsh -NoProfile -File ./UI/Avalonia/Start-Avalonia.ps1
```
`-ThemeVariant Dark` and `-Provider OAuth` (or `MSAL`, `MgGraph`: the authentication
provider for this session only, without touching the saved setting) are supported by
both entry points.
On macOS both entry points run the script through the **main-thread hook**
(`Bin/MainThreadHook/IntuneManagement.MainThreadHook.dll`, ~10 KB). Cocoa only allows
the GUI on the process's first thread, and a normal `pwsh` pipeline runs on a worker
thread - even when single-threaded. The hook is a .NET *startup hook*: `pwsh` loads it
before its own `Main` runs, on the main thread, and it opens a `UseCurrentThread`
runspace there and runs `Start-Avalonia.ps1` inside the very same `pwsh`. No second
engine, no second runtime: whatever pwsh you have brings its engine, its .NET and its
`$PSHOME/ref` compile references, all consistent with each other. Direct script
invocation re-launches itself through the hook when it notices it is not on the main
thread. See [The macOS main-thread hook](#the-macos-main-thread-hook).
`Start-Avalonia.ps1` sets `IM_UI_BACKEND=Avalonia` for you. On **Linux or Windows**,
setting that variable and importing the module by hand works too (on Windows use
`pwsh -STA`):
```powershell
$env:IM_UI_BACKEND = 'Avalonia'
Import-Module ./IntuneManagement.psd1 -Force
Show-IMMainWindow -View 'IntuneManagement'
```
On macOS do not import the GUI manually in an ordinary `pwsh` session: the native
host rejects initialization off the main thread. Use one of the entry points above.
Headless imports with the `None` backend are unaffected.
Without `IM_UI_BACKEND`, the module defaults to the headless `None` backend off
Windows - useful for automation, and the reason `Connect-IMIntuneManagement` and the
bulk cmdlets work fine on a Mac or a Linux box with no display at all.
## What does not work off Windows
These degrade with a log message rather than an error:
| Feature | Why |
|---|---|
| **Word documentation output** | Needs `Microsoft.Office.Interop.Word` COM automation. HTML, Markdown, CSV and JSON output are unaffected. |
| **MSI property extraction** on app import | Needs the `WindowsInstaller.Installer` COM object. Other app types import normally. |
| **WAM / broker sign-in** | Windows-only. Authentication falls back to the system browser. |
The `Default` theme follows the OS on all three platforms: the Windows app theme, the
macOS appearance setting, and the GNOME colour scheme on Linux. Linux desktops that are
not GNOME have no common way to report a preference and resolve to Light; pick Light or
Dark explicitly in Settings there.
Token cache persistence *is* implemented on both platforms - macOS uses the Keychain
and Linux uses libsecret (gnome-keyring / KWallet), via `MsalCacheHelper`. On a Linux
box with no keyring daemon, expect to sign in every session.
## Turning the notice back on
The startup notice is a normal setting. Clear **Settings -> General -> Hide
experimental platform notice** to see it again.
To preview it on Windows, where it never appears on its own:
```powershell
$env:IM_EXPERIMENTAL_NOTICE = '1'
./UI/Avalonia/Start-Avalonia.ps1
```
## Rebuilding the Avalonia binaries
`Bin/Avalonia` is committed and holds the natives for all three platforms side by
side - `.dll` for Windows, `.so` for Linux, `.dylib` for macOS - plus the managed
Avalonia assemblies, which are platform-neutral.
To rebuild for the machine you are on:
```powershell
./UI/Avalonia/Bootstrap/Restore-AvaloniaBinaries.ps1
```
To build another platform's natives **without that platform** - how the committed
macOS binaries were produced, from Windows:
```powershell
./UI/Avalonia/Bootstrap/Restore-AvaloniaBinaries.ps1 -RuntimeIdentifier osx-arm64
```
NuGet serves the RID-specific native packages regardless of the host OS. The macOS
dylibs are **universal binaries** (x86_64 + arm64 slices), so `osx-arm64` and
`osx-x64` produce identical files and either one covers every Mac.
`dotnet publish` does not clean its output directory, which is what lets one
`Bin/Avalonia` hold all three platforms at once. It does overwrite
`AvaloniaPayload.deps.json` with the last RID published; that file is inert here,
because the module loads the assemblies with `Add-Type -Path` rather than through
the `dotnet` host.
## The macOS main-thread hook
Source: [`UI/Avalonia/Bootstrap/MainThreadHook/StartupHook.cs`](../UI/Avalonia/Bootstrap/MainThreadHook/StartupHook.cs).
Binary: `Bin/MainThreadHook/IntuneManagement.MainThreadHook.dll` - **committed**, so a
plain clone or source ZIP is a complete, runnable download.
`Start-Avalonia.command` sets two environment variables and runs `pwsh -File`:
| Variable | Purpose |
|---|---|
| `DOTNET_STARTUP_HOOKS` | Path of the hook DLL. The .NET runtime calls its `StartupHook.Initialize()` on the main thread before `pwsh`'s own `Main`. |
| `IM_MAIN_THREAD_HOOK=1` | Engages the hook. Without it `Initialize` returns at once and pwsh starts normally, so child processes are unaffected (the hook also clears both variables from its own environment). |
`Initialize` then parses `-File <script> [args]` from the command line, opens a
`UseCurrentThread` runspace, runs the script and terminates the process with the
script's exit code. The PowerShell side recognises an engaged hook by the presence of
the `[IntuneManagement.MainThreadHook.MainThread]` type; `::Verify()` throws when called
off the main thread, which `Tests/MainThreadHook.Tests.ps1` and the UI smoke test use.
Compatibility comes from two choices in the project file: it targets `net8.0` (the
runtime of pwsh 7.4, the oldest supported engine) and references
`Microsoft.PowerShell.SDK` **7.4.x at compile time only**. .NET binds a reference to a
lower `System.Management.Automation` version against whatever newer one pwsh loaded,
so the one DLL works in 7.4, 7.5, 7.6, 7.7 and later on .NET 8, 9, 10 and later
without a rebuild. Keep it that way: do not bump the SDK reference to "latest".
Rebuild only when `StartupHook.cs` changes (needs the .NET 8+ SDK and NuGet access),
then commit the DLL:
```powershell
./UI/Avalonia/Bootstrap/Publish-MainThreadHook.ps1
```
The project's own tests then run the hook inside a child `pwsh` on every OS (the
mechanism is not macOS-specific; only Cocoa needs it) and check thread ownership,
parameter forwarding, exit codes, module import and that an un-engaged hook is inert,
plus a native-backend smoke test on a Mac desktop session that initializes Avalonia and
exercises a dispatcher callback and button event.
Before calling macOS supported, test on Intel and Apple Silicon: startup, sign-in
and browser return, message boxes, Bulk Compare pickers, file dialogs, clipboard,
closing child windows and quitting/relaunching.
## Reporting problems
Include the platform and architecture (`[System.Runtime.InteropServices.RuntimeInformation]::OSDescription`
and `::ProcessArchitecture`), the PowerShell version, and `IntuneManagement.log` from
the app data folder.
+315
View File
@@ -0,0 +1,315 @@
# Documenting policies
How to produce documentation from a script or a pipeline: the outputs, how to select
what gets documented, every option, and what each output provider expects. The UI's
Bulk > Document form drives exactly the same engine, so anything set there can be set
from `-Options`.
For the engine's internals - handlers, input providers, the customizer hooks and how to
add a renderer for a new type - read the source under `Internal/Documentation/`.
## Contents
- [The two commands](#the-two-commands)
- [Outputs](#outputs)
- [Selecting what to document](#selecting-what-to-document)
- [Documenting from an export folder](#documenting-from-an-export-folder)
- [Options](#options)
- [Per-output options](#per-output-options)
- [Where files land and how they are named](#where-files-land-and-how-they-are-named)
- [Languages](#languages)
- [Which types document, and how](#which-types-document-and-how)
- [Pipeline examples](#pipeline-examples)
## The two commands
| Command | Use when |
|---|---|
| `Start-IMGraphBulkDocumentation -OutputFormat <formats> ...` | You want files. Selects by type, group, object or export folder, runs every selected policy through the chosen outputs. |
| `Get-IMGraphDocumentation -PolicyObject <policy>` | You want the data. Returns one policy's documentation as an object - `BasicInfo`, `FilteredSettings`, `Assignments`, `Scripts`, `CustomTables` - to build your own report from. |
Both take `-Language` and `-Options`. Full parameter tables are in
[CommandReference.md](CommandReference.md#documentation).
## Outputs
`-OutputFormat` names one or more providers, comma-separated. `Get-IMDocumentationOutput`
lists them.
| Value | Produces | Notes |
|---|---|---|
| `html` | one `.html`, or one per policy | Self-contained; the CSS is inlined. |
| `md` | one `.md`, or one per policy | Optionally with the CSS inlined for renderers that honour it. |
| `word` | one `.docx` | **Windows only** - needs Word installed (COM automation). Silently degrades elsewhere. |
| `json` | one `.json`, or one per policy | The raw documentation objects - the same data `Get-IMGraphDocumentation` returns. |
| `csv` | one `.csv` per policy type | Flat rows; good for spreadsheets and diffing. |
| `atlassian` | one file, or one per policy, in Confluence storage format | Paste into a Confluence page, or push through the REST API. Headings carry stable anchors that downstream tooling can link to - see [DocumentationAtlassianOutput.md](DocumentationAtlassianOutput.md). |
```powershell
Start-IMGraphBulkDocumentation -OutputFormat html -PolicyGroup Compliance
Start-IMGraphBulkDocumentation -OutputFormat 'html,md,json' -PolicyGroup Compliance
```
## Selecting what to document
One parameter set per way of choosing:
```powershell
# Every policy of one or more types
Start-IMGraphBulkDocumentation -OutputFormat html -PolicyType CompliancePolicies, ConditionalAccess
# Every type in one or more groups
Start-IMGraphBulkDocumentation -OutputFormat html -PolicyGroup DeviceConfiguration, EndpointSecurity
# Specific policies, from the pipeline
Get-IMGraphPolicies -PolicyType SettingsCatalog -NameFilter 'Baseline' |
Start-IMGraphBulkDocumentation -OutputFormat html
# Everything in an export folder (see the next section)
Start-IMGraphBulkDocumentation -OutputFormat html -SourceFolder C:\IntuneExport
```
Type and group ids are the ones in the README's policy table. Nothing is documented that
the signed-in identity cannot read; a type it has no access to is skipped with a log line.
## Documenting from an export folder
`-SourceFolder` documents the json files of an export instead of live policies. Two
things to know:
- **It still needs a signed-in tenant - but any tenant, not the source.** Setting
definitions, templates and category names are generic Intune data and are looked up
live from whatever tenant is connected. The policies themselves come from the files.
- **Source-tenant names are not looked up.** The mode sets
`SourceTenantUnavailable = $true`, which skips every lookup only the source tenant could
answer: assignment group names, scope tag names, filter names, app names. Those are
resolved from the export's migration table when the export was made with one, and
shown as ids otherwise.
This is the mode for documenting a tenant you no longer have access to, or for
generating documentation in a pipeline from an export artifact rather than from a live
sign-in with broad read rights.
## Options
`-Options` is a hashtable. Anything not given falls back to the value saved by the UI's
documentation form, then to the default below. All keys are optional.
| Key | Default | Effect |
|---|---|---|
| `IncludeScripts` | `$true` | Include script bodies (PowerShell, shell, remediation) in the output. |
| `ExcludeScriptSignature` | `$false` | Strip Authenticode signature blocks from included scripts. |
| `IncludePolicyId` | `$false` | Add the policy's id to its basic information. |
| `ExcludeAssignments` | `$false` | Leave assignments out. |
| `SkipNotConfigured` | `$false` | Omit settings that are not configured. |
| `SkipDefaultValues` | `$false` | Omit settings still at their default. |
| `SkipDisabled` | `$true` | Omit settings that are disabled. |
| `SetUnconfiguredValue` | `$true` | Render unconfigured settings with the text below instead of blank. |
| `SetDefaultValue` | `$false` | Render settings at default with their default value. |
| `NotConfiguredText` | `notConfigured` | Text for an unconfigured setting: `notConfigured` (the language string), `empty`, or `asis`. |
| `ValueOutputProperty` | `value` | For ADMX settings: `value`, or `valueWithLabel` to include the setting's label. |
| `PropertySeparator` | `;` | Separator between values of a multi-value property. |
| `ObjectSeparator` | newline | Separator between items of a collection. |
| `SkipDocumentInfo` | `$false` | Omit the "documented by / on" block. |
| `FallbackDocumentation` | `$true` | Types with no dedicated renderer are documented as basic information plus one row per property. Off, they are skipped with a warning. (The UI calls this *Document unsupported types*.) |
| `SourceTenantUnavailable` | `$false` | Set automatically by `-SourceFolder`; see above. The old name `OfflineDocumentation` still works. |
| `Outputs` | `@{}` | Per-output options - next section. |
```powershell
$o = @{
SkipNotConfigured = $true
SkipDefaultValues = $true
IncludePolicyId = $true
ExcludeAssignments = $true
}
Start-IMGraphBulkDocumentation -OutputFormat html -PolicyGroup Compliance -Options $o
```
## Per-output options
Under `Outputs`, one hashtable per provider value. A key given here wins over the saved
UI setting, which wins over the default.
```powershell
$o = @{
Outputs = @{
html = @{ HTMLDocumentName = 'C:\Reports\Intune-%Date%.html'; HTMLDocumentFileType = 'Object' }
md = @{ MDDocumentName = 'C:\Reports\Intune.md'; MDIncludeCSS = $false; MDDocumentSkipDate = $true }
}
}
Start-IMGraphBulkDocumentation -OutputFormat 'html,md' -PolicyGroup Compliance -Options $o
```
### `html`
| Key | Default | Effect |
|---|---|---|
| `HTMLDocumentName` | `%MyDocuments%\%Organization%-%Date%.html` | Output file. Placeholders below. |
| `HTMLDocumentFileType` | `Full` | `Full` - one file; `Object` - one file per policy, named after it, in the same folder. |
| `HTMLTitleProperty` | `Intune documentation` | Page title. |
| `HTMLCSSFile` | the shipped `DefaultHTMLStyle.css` | Your own stylesheet, inlined. |
| `HTMLOpenFile` | `$true` | Open the result when done - set `$false` in a pipeline. |
### `md`
| Key | Default | Effect |
|---|---|---|
| `MDDocumentName` | `%MyDocuments%\%Organization%-%Date%.md` | Output file. |
| `MDDocumentFileType` | `Full` | `Full` or `Object`, as for html. |
| `MDTitleProperty` | `Intune documentation` | Document title. |
| `MDCSSFile` | the shipped `DefaultMDStyle.css` | Stylesheet for `MDIncludeCSS`. |
| `MDIncludeCSS` | `$true` | Inline the CSS (renderers that honour `<style>` in Markdown). `$false` for plain Markdown. |
| `MDDocumentSkipDate` | `$false` | Leave the date out - keeps a committed file from changing on every run. |
| `MDOpenFile` | `$true` | Open when done. |
### `json`
| Key | Default | Effect |
|---|---|---|
| `JSONDocumentName` | `%MyDocuments%\%Organization%-%Date%.json` | Output file. |
| `JSONOutputFileType` | `Full` | `Full` or `Object`. |
| `JSONOpenFile` | `$true` | Open when done. |
### `csv`
| Key | Default | Effect |
|---|---|---|
| `CSVDocumentationPath` | *(none)* | Root folder for the files. Always set it - with no value the files are written relative to the current directory. One file per policy type, in a subfolder named after the type when `CSVAddObjectType` is on. |
| `CSVExportProperties` | `simple` | `simple` - the standard columns; `custom` - the list below. |
| `CSVCustomDisplayProperties` | `Name,Value,Category` | Columns for `custom`. |
| `CSVDelimiter` | *(culture list separator)* | Override, e.g. `;`. |
| `CSVAddObjectType` | `$true` | Object type column. |
| `CSVAddCompanyName` | `$false` | Tenant name column. |
### `atlassian`
| Key | Default | Effect |
|---|---|---|
| `AtlassianDocumentName` | `%MyDocuments%\%Organization%-%Date%.html` | Output file (Confluence storage format, despite the extension). |
| `AtlassianDocumentFileType` | `Full` | `Full` or `Object`. |
| `AtlassianTitleProperty` | `Intune documentation` | Page title. |
| `AtlassianOpenFile` | `$true` | Open when done. |
### `word`
Windows only.
| Key | Default | Effect |
|---|---|---|
| `WordDocumentName` | `%MyDocuments%\%Organization%-%Date%.docx` | Output file. |
| `WordDocumentTemplate` | *(none)* | A `.dotx` to build on. |
| `WordDocumentFormat` | `wdFormatDocumentDefault` | Any `WdSaveFormat` name, e.g. `wdFormatPDF`. |
| `WordDocumentationLevel` | `full` | How much to include. |
| `WordExportProperties` | `simple` | `simple` or `custom`; `WordCustomDisplayProperties` lists the columns for `custom`. |
| `WordAddCategories` / `WordAddSubCategories` | `$true` / `$true` | Category and sub-category headings. |
| `WordTitleProperty` / `WordSubjectProperty` | `Intune documentation` | Document properties. |
| `WordCoverPage` | `Ion (Dark)` | A cover page from the template's gallery. |
| `WordContentControls` | *(none)* | Content controls to fill. |
| `WordHeader1Style`, `WordHeader2Style`, `WordHeader3Style` | template defaults | Heading style names. |
| `WordTableStyle` | `Grid table 4 - Accent 3` | Style for settings tables. |
| `WordTableHeaderStyle`, `WordTableTextStyle` | template defaults | Styles for table header row and cell text. |
| `WordCategoryHeaderStyle`, `WordSubCategoryHeaderStyle` | template defaults | Styles for the category and sub-category headings. |
| `WordScriptTableStyle`, `WordScriptStyle` | template defaults | Styles for the table around an included script and the script text. |
| `WordTableCaptionPosition` | `below` | `above` or `below`. |
| `WordDocumentationLimitMaxLength` / `WordDocumentationLimitTruncateLength` | *(none)* | Truncate very long values; `WordDocumentationLimitAttach` (`$false`) attaches the full value instead. |
| `WordAttachJsonFile` | `$false` | Embed the policy json. |
| `WordOpenDocument` | `$true` | Open when done. |
## Where files land and how they are named
Document names accept placeholders, expanded at run time:
| Placeholder | Value |
|---|---|
| `%MyDocuments%` | The user's Documents folder. |
| `%Organization%` | The connected tenant's display name. |
| `%Date%` | `yyyy-MM-dd`. |
| `%DateTime%` | `yyyyMMdd-HHmm`. |
Placeholders are ordinary environment-variable expansion, so any `%VARIABLE%` in the
process environment works as well - `%BUILD_ARTIFACTSTAGINGDIRECTORY%` on an Azure DevOps
agent, for example.
With `*DocumentFileType = 'Object'` the name's folder is used and one file is written per
policy, named after the policy (invalid filename characters removed).
In a pipeline set every `*OpenFile` / `WordOpenDocument` to `$false`, and give absolute
paths - `%MyDocuments%` on a build agent is rarely where you want the artifact.
## Languages
`-Language` picks the strings file: 23 languages ship under `Config/LanguageStrings/`
(`Strings-<code>.json`): `cs de en es fr hu id it ja ko nl pl pt ru sv tr zh zh-chs zh-cht zh-hans zh-hant`. Setting names,
categories and values are localized; UI text is always English. The files are generated
and must not be edited by hand.
```powershell
Start-IMGraphBulkDocumentation -OutputFormat html -PolicyGroup Compliance -Language sv
```
## Which types document, and how
A policy is documented by the first of these that claims it:
1. A **handler** for its exact `@odata.type` (Conditional Access, Named Locations,
Scope Tags, Role Definitions, Policy Sets, Kiosk, custom OMA-URI, ...).
2. An **input provider**: Settings Catalog policies walk their setting definitions;
administrative templates their definition values; endpoint security intents their
templates; Linux compliance its settings; and the profile providers use the
per-type manifest files under `Config/ObjectInfo/`.
3. The **generic fallback** (when `FallbackDocumentation` is on): basic information plus
one row per property.
The README's policy table says which of these each type gets (**yes** = 1 or 2,
**generic** = 3). Read-only types under *Intune Info*, ADMX Files and Multi Admin
Approval policies are not offered for documentation at all.
## Pipeline examples
Nightly HTML and Markdown from a service principal, no interactive state on the agent:
```powershell
Import-Module .\IntuneManagement.psd1
Use-IMSettingsStore -Memory
Connect-IMIntuneManagement -TenantId $env:TENANT_ID -AppId $env:APP_ID -Secret $env:APP_SECRET
$o = @{
SkipNotConfigured = $true
Outputs = @{
html = @{ HTMLDocumentName = "$env:BUILD_ARTIFACTSTAGINGDIRECTORY\Intune-%Date%.html"; HTMLOpenFile = $false }
md = @{ MDDocumentName = "$env:BUILD_ARTIFACTSTAGINGDIRECTORY\Intune.md"; MDDocumentSkipDate = $true; MDIncludeCSS = $false; MDOpenFile = $false }
}
}
Start-IMGraphBulkDocumentation -OutputFormat 'html,md' -PolicyGroup DeviceConfiguration, Compliance, EndpointSecurity -Options $o
```
Document an export artifact produced by an earlier stage, one Markdown file per policy,
without the source tenant:
```powershell
Connect-IMIntuneManagement -Provider OAuth -ManagedIdentity # any tenant the identity can read
$o = @{ Outputs = @{ md = @{ MDDocumentName = 'D:\docs\intune\index.md'; MDDocumentFileType = 'Object'; MDOpenFile = $false } } }
Start-IMGraphBulkDocumentation -OutputFormat md -SourceFolder D:\artifacts\IntuneExport -Options $o
```
Your own report from the data:
```powershell
Get-IMGraphPolicies -PolicyType CompliancePolicies |
ForEach-Object {
$d = Get-IMGraphDocumentation -PolicyObject $_ -Options @{ ExcludeAssignments = $true }
[PSCustomObject]@{ Policy = $_.Name; Settings = $d.FilteredSettings.Count }
} | Sort-Object Settings -Descending
```
Confluence, one page per policy, pushed with the REST API:
```powershell
$o = @{ Outputs = @{ atlassian = @{ AtlassianDocumentName = 'D:\out\intune.html'; AtlassianDocumentFileType = 'Object'; AtlassianOpenFile = $false } } }
Start-IMGraphBulkDocumentation -OutputFormat atlassian -PolicyGroup Compliance -Options $o
Get-ChildItem D:\out\*.html | ForEach-Object {
$body = @{ type = 'page'; title = $_.BaseName; space = @{ key = 'INTUNE' }
body = @{ storage = @{ value = (Get-Content $_ -Raw); representation = 'storage' } } } | ConvertTo-Json -Depth 6
Invoke-RestMethod -Uri "$confluence/rest/api/content" -Method Post -Headers $auth -ContentType 'application/json' -Body $body
}
```
+119
View File
@@ -0,0 +1,119 @@
# Atlassian (Confluence) documentation output
[`Internal/Documentation/OutputProviders/DocumentationOutputAtlassian.ps1`](../Internal/Documentation/OutputProviders/DocumentationOutputAtlassian.ps1)
Emits Confluence **storage format** (XHTML plus `<ac:*>` macros) for a documentation
run: paste it into a page's *Rich Text -> Source* view, or POST it to the Confluence
REST API as `representation=storage`. Structurally it mirrors the HTML provider
(BasicInfo / FilteredSettings / ComplianceActions / ApplicabilityRules / Assignments /
CustomTables plus a per-run table of contents); only the markup differs.
Select it with `-OutputFormat atlassian`.
| Option (`$Options.Outputs.atlassian`) | Default | Meaning |
| --- | --- | --- |
| `AtlassianDocumentName` | `%MyDocuments%\%Organization%-%Date%.html` | Output path. Supports `Expand-FileName` tokens. |
| `AtlassianDocumentFileType` | `Full` | `Full` = one file; `Object` = one file per policy plus a TOC index file. |
| `AtlassianTitleProperty` | `Intune documentation` | `<h1>` of the index page. |
| `AtlassianOpenFile` | `$true` | Launch the file with the OS default handler. Set `$false` for CI. |
## Heading anchors (a consumer contract)
Every heading is emitted with an inline anchor macro:
```xml
<h4 id='section-42'><ac:structured-macro ac:name='anchor' ac:schema-version='1'><ac:parameter ac:name=''>section-42</ac:parameter></ac:structured-macro>Contoso Reader</h4>
```
The anchor name has appeared as literal `<!--#section-N-->` text downstream of the
generated file. The source of that rewrite is still unproven: the publishing runbook
parses the body with mshtml before POSTing, and Confluence also transforms storage
format. The inline placement remains the known baseline until those paths are measured
separately.
Three properties are guaranteed, and downstream tooling depends on them:
1. **The `anchor` macro carries the link target.** Confluence discards author-specified
`id=` attributes when it converts storage format to ADF, so `#section-42` can only
resolve to an anchor macro or to Confluence's own heading-text-derived anchor.
2. **`id=` always equals the macro name, byte for byte.** Confluence strips it, but
automation that parses the *generated file* before publishing reads the anchor from
it - for example to walk an assignment table cell back to its policy heading and
build a deep link into the published page.
3. **The table of contents links these names, never the heading text.** Anchor names
contain no character that came from tenant data.
**Naming scheme (breaking-change surface):** `section-N` for every heading, numbered
in emission order over the whole run. `section-N` is unique across a run even in
`Object` mode, because the counter is reset only in
`Invoke-AtlassianPreProcessItems`. Numbering is *not contiguous within the TOC*: a
heading consumes a number whenever it is written to the document, including the
level-6 table captions that the TOC's level cap (4) filters out and the `-SkipTOC`
script captions it never lists. Do not assume contiguity - and do not reconstruct the
numbers by counting TOC entries.
`table-N` is reserved for the `-ToT` caption form of `Add-AtlassianHeader`, which no
call site in this provider uses: table captions here are plain level-6 headings, as
in the HTML provider. Only the Markdown provider passes `-ToT` (and so is the only
output with visible `Table N.` numbering). Enabling it for Atlassian would change
rendered captions, so it belongs with the HTML provider as one formatting decision
rather than a port detail.
The scheme is positional, so inserting one policy shifts every later anchor: links a
user saved from an earlier run of a scheduled export therefore move. A content-derived
scheme (`policy-<objectId>` from the Graph GUID) would be stable and is the natural
follow-up; it needs `Add-AtlassianHeader` to accept an explicit anchor from
`Invoke-AtlassianProcessItem`, with the positional counter kept as the fallback for
headers that have no natural identifier.
**Deployment ordering:** update the module wherever the export runs *before* a consumer
switches to reading `id=` / linking `#section-N`, or its links will point at anchors
that do not exist yet.
## Markup constraints
- **Single-quote every attribute.** Consumers JSON-escape the document body before a
Confluence client serialises it again; a double-quoted attribute arrives as
`ac:name=\"anchor\"` and breaks the macro. Every macro in the provider follows this,
so the body survives a JSON round-trip byte-identically.
- **Storage format is strict XML** and declares only the five XML built-in entities.
Use numeric references (`&#160;`, never `&nbsp;`), close every macro, and escape
text that comes from the tenant - `Get-AtlassianXmlText` for headings, TOC labels,
the title and the document-info lines; `Set-AtlassianText` for table cell values
(it also wraps XML-looking values in a `code` macro and long text in an `expand`).
A single bare `&` in a policy name invalidates the whole page body, not one heading,
and Confluence rejects the upload.
- **In `Object` mode the TOC's `href` carries a file name derived from a policy
name.** `Get-AtlassianObjectFileName` strips only path-invalid characters, so `&`,
`'` and `#` survive into it. `Get-AtlassianHref` percent-encodes the file-name
component (never the `#` that introduces the fragment) and then XML-escapes the
result; use it rather than interpolating a file name into an attribute.
## Change history
Unreleased (part of 4.0, no shipped version has the earlier behaviour):
- Table of contents entries link the anchor macro each heading emits. They used to
target a fragment re-derived from the heading text, so duplicate policy names all
jumped to the first occurrence and characters other than a plain space (`.`, `(`,
`)`, `:`, `&`, `+`, `,`, U+00A0) leaked into the href unencoded.
- Headings carry an inline `anchor` macro; previously they carried only an `id=` that
Confluence discards, which nothing could link to. The *shape* of an anchor name is
unchanged (`section-N`), so a consumer reading `id=` needs no update. A provisional
preceding-paragraph placement was reverted before release because its downstream
behavior and blank-line cost had not been measured.
- A heading kept out of the TOC now consumes an anchor number. It used to reuse the
next listed heading's number, so a document containing script captions (Detection
script, Requirement scripts) emitted two `section-N` anchors with one name and the
TOC entry landed on the caption. This shifts the numbers in such documents: another
reason to read `id=` rather than compute `section-N` from a position.
- Object-mode `href` file names are percent-encoded. A policy name containing `&`
or `'` used to produce a malformed body, and one containing `#` a link to the wrong
fragment.
- In the `-ToT` caption path (present but unused, see above) the `Table N. ` prefix
moved from the id to the visible text, where the Markdown provider puts it.
- Heading text, TOC labels, the title and the document-info lines are XML-escaped.
- Confluence keeps generating its own text-derived anchors, so externally saved
`#Policy-Name` links still resolve.
The three guarantees above are covered by the project's own test suite.
+165
View File
@@ -0,0 +1,165 @@
# Effective Permissions
What the signed-in identity can actually do per policy type, and how the left-nav
access marking, the Profile popup's **Permissions** dialog and
`Get-IMGraphEffectivePermissions` derive it. This page is the "how it works now".
## The two halves of a delegated login
| Half | Where it lives | What it says |
| --- | --- | --- |
| App consent | token `scp` (delegated) / `roles` (app-only) | which Graph scopes the *application* was granted |
| User authorization | Intune RBAC role assignments (+ scope tags) and Entra directory roles | what the *user* may do |
The token carries only the first half (plus `wids`, the directory role template
ids). A user holding just the built-in *Read Only Operator* Intune role signs in with
a token that says `DeviceManagementConfiguration.ReadWrite.All`, and every write is
refused with 403. Effective access is the intersection, so the marking has two layers:
```
Update-IntuneAccessLevels Internal/AccessLevel.ps1
Layer 1 scp/roles vs _Permissions -> Full / Limited / None
Layer 2 Get-IntuneRbacContext -> Full / Limited / None / Unknown Internal/EffectivePermissions.ps1
stamp AccessType = worst(L1, L2), AccessInfo = both reasons
```
Layer 2 can only make a type worse. Unknown never colours: Layer 1 stands.
## Layer 2 in detail
1. **Applies only to delegated tokens.** `idtyp = app` (or no `scp`) means application
permissions, which bypass Intune RBAC - the `roles` claim is the whole answer.
2. **Directory-role short-circuit.** Intune Administrator
(`3a2c62db-5318-420d-8d74-23affee5d9d5`) or Global Administrator
(`62e90394-69f5-4237-9190-012177145e10`) in `wids` grants complete Intune RBAC, so
every Intune category is Full and Graph is not asked. Global Reader and the partial
roles are *not* shortcuts - the user may also hold an Intune role.
3. **Otherwise two Graph calls:**
- `GET /beta/deviceManagement/getEffectivePermissions(scope='*')` - the same function
the Intune portal uses to enable its buttons. It answers with the **allowed**
actions only: `notAllowedResourceActions` comes back empty on every response, so
on its own it cannot tell "the role denies this action" from "no such action
exists for this category".
- `GET /beta/deviceManagement/resourceOperations` - the **catalogue**: every resource
action Intune defines, one entry per action, its `id` being exactly the
`Microsoft.Intune_<Category>_<Action>` name the first call uses. That is the
existence answer the first call cannot give (260 actions over 53 resources on the
tenant this was built against). It describes the service, not the user, so it is
cached per tenant and survives a token refresh. When it cannot be read, existence
is *unknown* and the verdicts degrade as shown below.
4. **Per type:** the `_API` path is mapped to an Intune resource category
(`deviceManagement/deviceConfigurations` -> `DeviceConfigurations`, ...; table in
`Internal/EffectivePermissions.ps1`, overridable per type with `_ResourceCategory`).
Only actions that exist for the category are required, so a category with no
`_Assign` (Roles) or one that spells it `Modify` (ManagedGooglePlay) is not marked
down for the actions it never had.
| For the category | Level |
| --- | --- |
| `_Read` is not in the catalogue | Unknown - no such category exists; say nothing |
| `_Read` exists, not allowed | None |
| `_Read` allowed, type declares only Read scopes | Full (read-only by design) |
| `_Read` allowed, an existing write action (`Create/Update/Delete/Assign/Modify`) not allowed | Limited, tooltip lists the missing actions |
| every existing action is allowed | Full |
The Limited tooltip distinguishes the two shapes of it: "read-only for
`<Category>`" when no write action at all is allowed, and "partial write access to
`<Category>`" when some are (a role that can create and update but not delete or
assign is still Limited, but it is not read-only). The Permissions popup's Role,
Effective and Result columns show "Partial write" for that second shape rather
than "Read" / "Read-only" - Effective and Result only when the token can write
too, since a read-only token over a partial-write role really is read-only.
With no catalogue, the same type yields Unknown when `_Read` is not allowed (it may
not exist), Limited when no write action at all is allowed, and Full otherwise - a
coarser answer that never invents an action name.
APIs that Intune RBAC does not govern (`identity/*`, `identityGovernance/*`,
`organization/*`) and the few listed in `$script:RbacUnmappedApis` are Unknown. A
test fails if a new `deviceManagement/` or `deviceAppManagement/` type is in
neither table.
## Caching and refresh
The user's answer is cached per tenant under the **token fingerprint** (`tid|oid|iat`)
with no TTL; the action catalogue is cached per tenant only, so a refresh re-asks
`getEffectivePermissions` and reuses the catalogue. Both are dropped on disconnect. Any newly minted token - routine renewal, a new sign-in, or **Refresh in the
Profile popup** - has a new `iat`, misses the cache and re-asks. That single user
action therefore covers both kinds of change:
| Change | Visible in the token? | Caught by |
| --- | --- | --- |
| Directory role via PIM (Intune Administrator, Global Admin) | only in a newly issued token; the routine silent acquire returns the cached one until near expiry | Refresh mints a token with the current `wids` |
| Intune RBAC assignment (a role added, PIM for Groups) | never - the token is identical | Refresh still produces a new `iat`, so Layer 2 re-asks Graph |
A failed lookup is remembered for the same fingerprint (no retry on every menu
rebuild) and retried after a refresh. Refresh is provider-agnostic: MSAL
(`WithForceRefresh`), delegated OAuth (`refresh_token` grant) and MgGraph
(`Connect-MgGraph` re-run) all mint a new token; BYO bearer tokens cannot refresh and
the button is already disabled for them.
**Scope tags are not modelled.** `getEffectivePermissions` is the global answer; a
user limited to some tags can still be refused on individual objects.
## Surfaces
- **Left nav** - orange (Limited) / red (None) with the reason in the tooltip; the
existing `HideNoAccess` setting hides red rows. A summary line is logged at sign-in.
- **Profile popup -> Permissions** - one row per policy type: token level, Intune-role
level, effective level, reason, plus a header saying where the Intune half came from
and when the token was issued. WPF: `UI/WPF/Extensions/EffectivePermissionsUIWPF.ps1`;
Avalonia: `UI/Avalonia/Extensions/EffectivePermissionsUIAvalonia.ps1` with
`UI/Avalonia/Classes/EffectivePermissionRowItem.ps1`.
- **`Get-IMGraphEffectivePermissions`** - the same rows for scripts
(`-PolicyType`, `-TokenId`), or `-Raw` for the context (source, the allowed action
set, the catalogue of actions that exist, raw response). Runs even when the setting
below is off.
## Setting
`UseRbacAccessMarking` (General, default on). Off restores the token-only marking
exactly. Registered in `Internal/EffectivePermissions.ps1` because engine code reads
it.
## Verifying against a tenant
Both endpoints, the response shapes and every **category name** in the table have been
run against a live tenant, and `Tests/EffectivePermissions.Tests.ps1` asserts the names
against the catalogue fixture, so a typo fails the suite. What is still unverified is
the other half of each entry - that a given `_API` really is governed by the category it
is mapped to. Those are marked `UNVERIFIED`, and **an unverified mapping is not a safe
mapping**: only a category name that does not exist at all degrades to Unknown. A mapping
to a name that exists but governs a *different* resource produces a confident verdict
derived from the wrong role permissions - a type marked Full because the user's role
covers the category it was mistakenly mapped to, or None because it does not. Both are
wrong, and neither shows as Unknown. The typo test cannot catch this: the wrong name is a
real one. Only a role that grants exactly one category can, so to check on a lab tenant:
```powershell
Import-Module .\IntuneManagement.psd1 -Force
Connect-IMIntuneManagement ... # delegated, as a NON-admin test user
Get-IMGraphEffectivePermissions -Raw | Select-Object Source, TenantId, AsOf
(Get-IMGraphEffectivePermissions -Raw).Allowed | Sort-Object # what the user's role grants
(Get-IMGraphEffectivePermissions -Raw).Catalog | Sort-Object # every action Intune defines
Get-IMGraphEffectivePermissions | Where-Object RbacLevel | Format-Table Id, ResourceCategory, TokenLevel, RbacLevel, EffectiveLevel
```
Assign the test user a custom role granting exactly one category, refresh the token, and
check that the types mapped to it are the ones that move. Move confirmed entries out of
`UNVERIFIED`, and add `_ResourceCategory` overrides where a type maps elsewhere.
## Tests
`Tests/EffectivePermissions.Tests.ps1` (offline, fixtures under
`Tests/Fixtures/EffectivePermissions/`): category resolution, completeness and every
category name against the catalogue, response parsing, per-type verdicts for a full
admin / read-only operator / custom role and for a missing catalogue, the never-upgrade
property, app-only skip, directory-role short-circuit, fingerprint caching, catalogue
caching across a refresh, negative caching, the setting gate, disconnect clearing,
`Update-IntuneAccessLevels` integration and the cmdlet's rows.
The fixtures are recorded responses, not hand-written: `ReadOnlyOperator.json` is the
built-in role's 55 actions, `ResourceOperations.json` the whole catalogue, and every
`notAllowedResourceActions` is empty because that is what Graph returns. A verdict test
that fails is a bug in the code or the mapping - do not "fix" it by inventing
not-allowed data the API never sends.
+214
View File
@@ -0,0 +1,214 @@
# Automation Examples
One page of copy-paste recipes for the exported cmdlets. Everything the UI
does runs through these same commands, so anything shown here can be
scheduled, piped, or scripted.
```powershell
Import-Module .\IntuneManagement.psd1
```
All exported commands carry the `IM` prefix (`Connect-IMIntuneManagement`,
`Start-IMGraphBulkExport`, ...). Most commands take `-TokenId`; when omitted
they use the default token from the last `-DefaultToken` connect.
## Connect
```powershell
# Interactive sign-in (browser / WAM). Cached sessions resume silently.
Connect-IMIntuneManagement -DefaultToken
# Specific user / force a fresh prompt
Connect-IMIntuneManagement -Interactive -User admin@contoso.com -ForceInteractive
# Unattended: client secret (app registration)
Connect-IMIntuneManagement -TenantId $tid -AppId $appId -Secret $secret -DefaultToken
# Unattended: certificate
Connect-IMIntuneManagement -TenantId $tid -AppId $appId -CertificatePath .\auth.pfx
# Device code (headless box, sign in from another device)
Connect-IMIntuneManagement -DeviceCode
# Sovereign clouds
Connect-IMIntuneManagement -Cloud USGov -GCCType High -DefaultToken
```
Several tenants can be connected at once; each connect returns a token whose
`Id` you pass as `-TokenId` to target that tenant.
## Settings
```powershell
# Read and write by KEY - no storage paths, no internal functions
Get-IMSetting GraphPageSize
Get-IMSetting ExportFolder -Detailed # value + where it came from
Set-IMSetting GraphPageSize 999
Remove-IMSetting ExportFolder # back to the registered default
# What is configurable at all
Get-IMSettingDefinition -Section IntuneManager
# Per-tenant override (reads prefer it over the global value)
Set-IMSetting ExportFolder '\\server\intune\contoso' -Scope Tenant -TenantID $tid
```
Writes persist to the registry (Windows) or the settings file. On a shared
worker, where neither should be touched, run against an in-memory store:
```powershell
Use-IMSettingsStore -Memory # nothing from here on hits disk
Import-IMSettingsStore .\config\run.json # a checked-in configuration
Set-IMSetting UseBatchAPI $true
Get-IMSettingsStore # assert the store before trusting it
```
`IM_SETTINGS_STORE=Memory` does the same from the first line of the module
load. See [Settings](Settings.md) for the store modes and precedence rules.
## Export
```powershell
# Everything, to one folder, assignments included
Start-IMGraphBulkExport -ExportFolder C:\IntuneExport -ExportAssignments $true
# Only some types / groups, name filter
Start-IMGraphBulkExport -ExportFolder C:\IntuneExport -PolicyType SettingsCatalog,CompliancePolicies
Start-IMGraphBulkExport -ExportFolder C:\IntuneExport -PolicyGroup DeviceConfiguration -Filter 'PROD-*'
# Reusable settings: build once, save, schedule the file
$s = [IntuneManagerExportSettings]::new()
$s.ExportFolder = 'C:\IntuneExport'
$s.ExportAssignments = $true
$s.AddCompanyName = $false
Save-IMGraphBulkExportSettings -Path C:\Jobs\nightly-export.json -ExportSettings $s
Start-IMGraphBulkExport -SettingsFile C:\Jobs\nightly-export.json
# Single policies via the pipeline
Get-IMGraphPolicies -PolicyType SettingsCatalog | Where-Object Name -like 'Win11*' |
Export-IMGraphPolicy -ExportSettings $s
```
## Import
```powershell
# Import a full export folder (assignments + scope tags translated)
Start-IMGraphBulkImport -ImportFolder C:\IntuneExport -ImportAssignments $true -ImportScopeTags $true
# Only matching files / only some groups
Start-IMGraphBulkImport -ImportFolder C:\IntuneExport -Filter 'PROD-*' -PolicyGroup DeviceConfiguration
# Import behavior when the object already exists (-ImportType):
# alwaysImport (default) | skipIfExist | update | replace | replace_with_assignments
Start-IMGraphBulkImport -ImportFolder C:\IntuneExport -ImportType update
```
Cross-tenant: the export folder's `MigrationTable.json` translates group and
dependency references automatically. Groups that do not exist in the target
tenant are created during import (settings `CreateGroupOnImport` and
`ConvertSyncedGroupOnImport`, both on by default). Policy sets re-point their
member references by display name.
## Documentation
```powershell
# One object -> result object (inspect or serialize yourself)
$p = Get-IMGraphPolicies -PolicyType CompliancePolicies | Select-Object -First 1
Get-IMGraphDocumentation -PolicyObject $p -Language en
# Bulk: whole policy groups to a single HTML file
Start-IMGraphBulkDocumentation -OutputFormat html -PolicyGroup DeviceConfiguration -Options @{
Outputs = @{ html = @{ HTMLDocumentName = 'C:\Docs\Intune.html'; HTMLOpenFile = $false } }
}
# Several formats in one run, each with its own settings
Start-IMGraphBulkDocumentation -OutputFormat 'html,word,csv' -PolicyType SettingsCatalog -Options @{
Outputs = @{
html = @{ HTMLDocumentName = 'C:\Docs\Intune.html'; HTMLOpenFile = $false }
word = @{ WordDocumentName = 'C:\Docs\Intune.docx'; WordOpenDocument = 'false' }
csv = @{ CSVDocumentationPath = 'C:\Docs\CSV' }
}
}
# Localized output
Start-IMGraphBulkDocumentation -OutputFormat html -PolicyGroup CompliancePolicies -Language sv
# Document an EXPORT folder instead of the live tenant
Start-IMGraphBulkDocumentation -OutputFormat md -SourceFolder C:\IntuneExport
# Pipeline: document exactly the policies you select
Get-IMGraphPolicies -PolicyType ConditionalAccess | Start-IMGraphBulkDocumentation -OutputFormat json
```
Formats: `html`, `word`, `csv`, `md`, `json`, `atlassian`. Frequently used
per-format option keys (full list: `Get-IMDocumentationOutput`):
| Format | Keys |
| --- | --- |
| html | `HTMLDocumentName`, `HTMLDocumentFileType` (`Full`/`Object`), `HTMLCSSFile`, `HTMLOpenFile` |
| word | `WordDocumentName`, `WordDocumentTemplate`, `WordCoverPage`, `WordOpenDocument` |
| csv | `CSVDocumentationPath`, `CSVDelimiter`, `CSVAddObjectType` |
| md | `MDDocumentName`, `MDDocumentFileType`, `MDIncludeCSS` |
| json | `JSONDocumentName`, `JSONOutputFileType` |
`*DocumentFileType = 'Object'` writes one file per policy instead of one big
document.
## Compare
```powershell
# Live tenant vs an export folder (drift detection)
$provider = [CompareIntuneWithExportProvider]::new()
$provider.ExportPath = 'C:\IntuneExport'
Compare-IMGraphPolicy -IntuneWithExport $provider -PolicyGroupIds DeviceConfiguration
# Two selected policies against each other
$two = Get-IMGraphPolicies -PolicyType SettingsCatalog | Where-Object Name -in 'Baseline v1','Baseline v2'
Compare-IMGraphPolicy -Policies $two
```
## Bulk assignments
```powershell
# Add a group assignment (with an assignment filter) across a policy group
$s = [IntuneManagerAssignmentSettings]::new()
$s.Action = 'Add' # Add | Replace | Remove
$s.Assignments = @([PSCustomObject]@{
TargetType = 'groupAssignmentTarget' # or exclusionGroupAssignmentTarget,
GroupId = '<entra-group-id>' # allDevicesAssignmentTarget,
FilterId = '<filter-id>' # allLicensedUsersAssignmentTarget
FilterType = 'include'
})
Set-IMGraphBulkAssignments -AssignmentSettings $s -PolicyGroup DeviceConfiguration -Filter 'PROD-*'
# Remove the same assignment again
Set-IMGraphBulkAssignments -AssignmentSettings $s -Action Remove -PolicyGroup DeviceConfiguration
```
## Bulk scope tags, copy, delete
```powershell
# Tag everything matching a name pattern
Set-IMGraphBulkScopeTags -Action Add -ScopeTagIds $tagId -PolicyType SettingsCatalog -Filter 'PROD-*'
# Copy every policy whose name contains the pattern, replacing it in the copy
# ("Test - Baseline" -> "Prod - Baseline"); re-run safe (existing names skipped)
Start-IMGraphBulkCopy -CopyFromPattern 'Test - ' -CopyToPattern 'Prod - ' -PolicyGroup DeviceConfiguration
# Delete by filter - test-prefix your filter, this is destructive
Start-IMGraphBulkDelete -Filter '[Test]*' -PolicyGroup DeviceConfiguration
```
## Lower-level building blocks
```powershell
# List objects (optionally with assignments)
Get-IMGraphPolicies -PolicyType SettingsCatalog -IncludeAssignments
# Load exported files back into policy objects (for selective import)
Get-ChildItem C:\IntuneExport\SettingsCatalog\*.json |
Get-IMGraphPolicyFromFile | Import-IMGraphPolicy
# Raw Graph, with the module's auth, throttling, paging and batching
Invoke-IMMSGraphAPI -Url 'deviceManagement/managedDevices?$top=5' -AllPages
```
+129
View File
@@ -0,0 +1,129 @@
# Graph batching and parallelism
Two settings control how the module talks to Microsoft Graph, and since the
2026-09-07 rework each one means exactly what its name says:
| setting key | title in Settings | meaning |
| --- | --- | --- |
| `UseBatchAPI` | Combine requests into batch calls | Combine logical requests into `POST /$batch`, twenty per call, on **every** path that can batch. Off means `$batch` is never used: each request is its own HTTP call. Default on. |
| `UseParallelBatchAPI` | Send batch calls in parallel (experimental) | Send those `$batch` POSTs concurrently instead of one at a time. Concurrency and nothing else. PowerShell 7 only. Default off. |
Working definitions used on this page:
- **Batched** - several logical requests combined into one `POST /$batch`.
- **Parallel** - more than one HTTP request in flight at once. In this module
that is always a set of `$batch` POSTs; there is no parallel direct-call path.
Both keys are read in exactly one place each, the two predicates at the top of
`Internal/MSGraph.ps1`:
```powershell
Test-GraphBatchEnabled # (Get-SettingValue "UseBatchAPI") -eq $true
Test-GraphParallelEnabled # (Get-SettingValue "UseParallelBatchAPI") -eq $true -and PS 7+
```
`Tests/Static.Tests.ps1` fails the build if either key is consulted anywhere
else. That rule is what keeps the two concerns from re-tangling: before it,
`UseBatchAPI` was checked at four call sites and ignored by the dispatcher
itself, and `UseParallelBatchAPI` silently selected an entire export pipeline.
## Where the decisions are made
`Invoke-GraphBatchRequest` (`Internal/MSGraph.ps1`) is the only place that
decides between batched, direct, serial and parallel dispatch. Every caller
hands it a queue of sub-requests and gets back the batch response shape
(`id`, `status`, `headers`, `body`) whichever way the requests went out.
1. Paced sub-requests (see below) are split into their own queue first.
2. **Batching off, or exactly one non-paced request**: every sub-request goes
out as its own `Invoke-MSGraphAPI` call through
`Invoke-GraphBatchRequestDirect`. The sub-request's `Accept` header becomes
`-ODataMetadata`, other headers ride on `-AdditionalHeaders`, `-AllPages`
is forwarded for GETs, and the status comes from the telemetry row the
wrapper records for every call. A lone request takes this path even with
batching on, because a `$batch` envelope around one GET is a whole extra
round-trip for nothing.
3. **Batching on**: the paced queue drains first, one sub-request per POST,
each gated on the tenant clock. Then the normal queue goes twenty per POST.
4. **Parallel on** and the normal queue has more than
`$script:GraphParallelBatchMinQueue` (20) sub-requests: the chunks are
dispatched concurrently through `Invoke-ParallelGraphBatchPosts`, up to
`ParallelBatchThrottle` at a time. At or below the threshold the queue goes
out serially and the log says so once:
`Batch <type>: parallel requested but the queue is N (threshold 20) - dispatching serially`.
## Every Graph call path
| path | entry point | batches? | can be parallel? | controlled by |
| --- | --- | --- | --- | --- |
| Policy listing | `Get-GraphPolicies` | yes, list requests coalesced by URL; types without a batch object list directly | yes | both settings |
| Policy bodies (hydrate) | `Invoke-PolicyHydrateBodyBatch` | yes, chunks of `ParallelBatchThrottle` x 20; a single policy is a direct GET | yes | both |
| Sub-resources (branding images, app relationships, ToU files, ...) | `Invoke-PolicySubresourceFetch` | yes | yes | both |
| Assignments | `Add-GraphPolicyAssignments` | yes | yes | both |
| Bulk export group prefetch | `Sync-BulkExportMigrationGroups` | no `$batch`: one `directoryObjects/getByIds` POST per thousand ids | no | always runs; not a batching decision |
| Nested group hierarchy | `Sync-BulkExportNestedGroupHierarchy` | yes | yes | both |
| Migration objects queued as cache misses | `Resolve-GraphMigrationObjectsPending` | getByIds for groups/users/devices/service principals; `$batch` for assignment filters and anything else | for the `$batch` part | `UseBatchAPI` for the non-directory part |
| Documentation prefetch | `Initialize-DocumentationRunPrefetch` | scope tags, filters and category lists in one `$batch`; assignment groups through one getByIds preload | yes | both for the `$batch`; the preload always runs |
| Import | `Import-GraphPolicy` | bulk: POSTs queued then batched; direct: each object's own `ImportObject` | yes (bulk) | `UseBatchAPI` chooses bulk versus direct - they are different code, the direct path runs class overrides and carries `PreImportCommand` headers |
| Delete | `Remove-GraphPolicy` | same shape as import | yes (bulk) | `UseBatchAPI` chooses |
| Everything single-request | `Invoke-MSGraphAPI` | no | no | not a batching path |
### The two overrides
**Paced endpoints.** Conditional Access policies, named locations,
authentication strengths, authentication context references, risk detections
and risky users are limited by Graph to one request per second per tenant,
counted across every application, with no `Retry-After` on throttle. Requests
to them are always one sub-request per POST, never parallel, each gated on a
per-tenant clock, whatever the two settings say. The rules live in
`Internal/GraphRateLimits.ps1` and the `GraphPaceIdentityEndpoints` setting
switches only the pacing off. Pacing legitimately wins over both settings.
**Single requests.** A queue of one non-paced request goes direct regardless
of `UseBatchAPI`. See step 2 above.
## Bulk export specifically
`Start-GraphBulkExport` runs one pipeline whatever the settings say: list every
type in one call, hydrate every body across types, prefetch every referenced
group, walk the nested hierarchy when nested export is on, then write per
type. Concurrency takes effect only inside the dispatcher. The old per-type
interleaved loop that ran with parallel off is gone; it skipped the group
prefetch, which on a 963-object tenant cost 22 of a measured 26m 35s (one
direct GET per assignment group at ~1.6 s, against ~2 s for twenty of them in
one `$batch`).
`Add-GraphMigrationObject` never calls Graph. A cache miss is queued and
resolved in bulk when the migration tables are flushed, so a single-policy
export and a bulk export both pay one getByIds POST for all their groups, not
one GET each.
## Benchmark protocol
Export to a fresh scratch folder, never over a previous run, then compare
against a reference export:
| configuration | expectation |
| --- | --- |
| batch on, parallel off (the default) | close to the parallel time, not 6x slower |
| batch on, parallel on | fastest; no regression |
| batch off, parallel off | slowest; zero `Invoke batch` log lines; identical output |
Output equivalence means the same file list, content differences limited to
`lastModifiedDateTime` on objects genuinely edited between runs, and the same
`(Id, Type)` set in `MigrationTable.json` (its ordering may differ).
## Reading the log
| log line | meaning |
| --- | --- |
| `Invoke batch N <type> (M requests)` | one `$batch` POST, serial path |
| `<type>: dispatching N request(s) in parallel batches` | the parallel dispatcher took the queue |
| `Batch <type>: parallel requested but the queue is N (threshold 20) - dispatching serially` | parallel is on but the queue was too small |
| `Direct dispatch <type> (N requests, batching off)` | `UseBatchAPI` is off; each request is its own call |
| `Bulk export: pre-fetching N AAD group(s) ...` / `... prefetch complete - R resolved, M missing/deleted` | the getByIds group prefetch |
| `Migration objects: resolving N queued <kind> id(s) for tenant ...` | the deferred migration-object queue draining |
| `<type>: N of M requests - Conditional Access allows one per second` | the paced queue |
Design record: `Docs/superpowers/specs/2026-09-07-graph-batching-parallelism-control-design.md`.
Tests: `Tests/GraphBatching.Tests.ps1`, `Tests/GraphRateLimits.Tests.ps1`.
+268
View File
@@ -0,0 +1,268 @@
# Settings
Settings are the tool's configuration: everything the Settings dialog shows, plus a
handful of hidden keys and per-feature state. This page is about how they are stored,
how they are addressed, and how to drive them from automation without touching the
machine they run on.
For cache and logging, see [Settings Cache Logging](SettingsCacheLogging.md).
Every registered key, with type, default and description, is in
[SettingsReference.md](SettingsReference.md) - generated from the code, so it is always current.
## Three layers
Addressing a setting used to mean knowing where it lives. It does not any more:
| Layer | Where | Addressed by | Functions |
| --- | --- | --- | --- |
| storage | `Internal/Core.ps1` | **path** (`SubPath` + key) | `Get-SettingStoreValue`, `Save-SettingStoreValue`, `Remove-SettingStoreValue` |
| resolver | `Internal/Settings.ps1` | **key** | `Get-SettingValue`, `Set-SettingValue`, `Remove-SettingValue`, `Test-SettingValueConfigured`, `Resolve-SettingValue` |
| public | `Public/*Setting*.ps1` | **key** | `Get-IMSetting`, `Set-IMSetting`, `Remove-IMSetting`, `Get-IMSettingDefinition`, plus the store cmdlets below |
The dependency direction is one-way: public calls the resolver, the resolver calls
storage. Nothing goes the other way.
**Prefer the resolver.** It looks the key up in the registered definitions and derives
the `SubPath` from the registration, so a write cannot land somewhere a read never
looks. Every SubPath bug the project has had - `GraphPageSize` resolving differently in
headless sessions, the bulk-export remember-last-used round trip being silently dead,
`DefaultCloud` - was a hand-written path on one side only. `Tools/Audit-Settings.ps1`
reports remaining path-addressed access to a registered key as a warning.
`Get-SettingValue` lives in `Internal/Core.ps1` rather than with the rest of the
resolver because `Write-Log` reads `LogFile` through it during preload, long before
`Internal/` is dot-sourced.
### Registration
A setting exists because some file called `Add-SettingsObject`:
```powershell
Add-SettingsObject -Key "GraphPageSize" -Section "IntuneManager" -SubPath "IntuneManager" `
-Title "Page size" -Type "List" -DefaultValue "0" -ItemsSource $sizes
```
`-SubPath` is the storage path. With no `-SubPath` the `-Section` is used, except
`General`, which stores at the root of the store. `-Type` is a **UI editor hint**
(`Boolean`, `Int`, `File`, `List`, `Text`), not a storage type - everything on disk is
a string.
`Get-IMSettingDefinition` lists the registrations; `Get-IMSettingDefinition -Section
IntuneManager` narrows to one section.
## Three store modes
The mode is decided at the very top of `Internal/Core.ps1`, before anything can log:
| Mode | Backing | Default when |
| --- | --- | --- |
| `Registry` | `HKCU:\Software\IntuneManagement` | on Windows |
| `Json` | `IM_SETTINGS_FILE`, else `LocalApplicationData/IntuneManagement/Settings.json` | off Windows, or `IM_SETTINGS_FILE` is set |
| `Memory` | a tree with no file behind it - reads and writes work, nothing touches disk | never; opt in explicitly |
Environment variables, read once at load:
| Variable | Effect |
| --- | --- |
| `IM_SETTINGS_STORE` | `Memory`, `Json` or `Registry`. `Registry` off Windows falls back to `Json`. An unrecognized value falls back to the platform default and is warned about once logging works. |
| `IM_SETTINGS_FILE` | the `Json` store's file. Created if missing. Implies `Json`. |
Memory mode is not a fourth code path: the storage primitives take their JSON branch
whenever a settings **object** exists, and persist only when a settings **file** exists
too. Memory mode is an object with no file.
`Get-IMSettingsStore` reports what the store actually is - which is not always what was
requested, because a `Json` store whose file cannot be read falls back to the registry,
and off Windows that leaves no store at all (`Mode = "None"`: reads return registered
defaults, writes go nowhere).
```powershell
Get-IMSettingsStore # Mode / Path / Persisted / ValueCount / RequestedMode
Get-IMSettingsStore -IncludeValues # ... plus every SubPath/Key/Value row
```
## Public API
```powershell
Get-IMSetting GraphPageSize # the effective value
Get-IMSetting GraphPageSize -Detailed # value + Source (Default/Global/Tenant) + Type + Default + ...
Get-IMSetting ExportFolder -Scope Global # ignore any tenant override
Get-IMSetting ExportFolder -Scope Tenant -TenantID <guid>
Set-IMSetting GraphPageSize 999
Set-IMSetting ExportFolder '\\server\intune\contoso' -Scope Tenant
Set-IMSetting UseBatchAPI $false -PassThru # returns the resolved value afterwards
Remove-IMSetting ExportFolder -Scope Tenant # revert to the global value
Remove-IMSetting ExportFolder # revert to the registered default
```
`Set-IMSetting` and `Remove-IMSetting` **persist by default** - to the registry or the
settings file, whichever the active store is. There is no opt-in switch. They support
`-WhatIf`, they log the store and path they wrote to, and `Get-IMSettingsStore` lets a
script assert on the store before writing anything. If a script must not write to the
machine it runs on, switch to a memory store first.
`Get-IMSetting` accepts a key from the pipeline, so `Get-IMSettingDefinition -Section
IntuneManager | Get-IMSetting -Detailed` dumps a whole section with provenance.
### Scope and precedence
A setting can be written globally or for one tenant. Reads take the tenant value first:
```text
tenant value -> global value -> registered DefaultValue
```
`-Scope Tenant` with no `-TenantID` uses the connected tenant, and **fails loudly** if
there is none - writing the global value instead would change every tenant when the
caller asked for one.
`Get-IMSetting -Detailed` reports which level answered in `Source`. That is computed
fresh from the store on every call; the `.Value` cached on a definition object by
`Get-SettingValue` is a per-session artifact of whoever read it last.
A **scoped** read (`-Scope Global` / `-Scope Tenant`) reports the absence of a value at
that level as `$null` with `Source = 'NotSet'`. It does not fall back to the registered
default, because "this tenant does not override the setting" and "this tenant overrides
it to the same value as the default" are different facts. Only the default `-Scope
Effective` falls back, which is what the application itself resolves.
### Keys with no registration
The hidden keys (`ExportReplaceTokens`) and the per-feature state namespaces are not
registered with `Add-SettingsObject`, so they have no path to take from a registration.
`-SubPath` supplies it, and is accepted on **all three** cmdlets - a key that can be
written this way can be read and removed the same way:
```powershell
Set-IMSetting ExportReplaceTokens 'TenantId' -SubPath 'IntuneManager'
Get-IMSetting ExportReplaceTokens -SubPath 'IntuneManager'
Remove-IMSetting ExportReplaceTokens -SubPath 'IntuneManager'
```
Such a key has no registered default and no declared type, so its `Source` is `Tenant`,
`Global` or `NotSet` - never `Default` - and the value comes back as the string the
store holds. Tenant precedence works as usual (`-Scope Tenant`, `-TenantID`), which is
how a per-tenant `ExportReplaceTokens` is set.
For a **registered** key `-SubPath` is reported in the log and ignored: the registration
decides the path, so a write cannot be aimed somewhere the application never reads.
## Automation: a run that reads and writes nothing on the worker
The problem: a runbook on a shared Hybrid Worker has no business reading, let alone
writing, that worker's `HKCU` hive or settings file. A memory store solves it, and a
checked-in settings file makes the configuration reviewable:
```powershell
Import-Module .\IntuneManagement.psd1
# Nothing from here on touches the worker's registry or settings file.
Use-IMSettingsStore -Memory
Import-IMSettingsStore .\config\documentation-run.json
# Whatever the file did not cover.
Set-IMSetting GraphPageSize 999
Set-IMSetting UseBatchAPI $true
Connect-IMIntuneManagement -Provider OAuth -TenantId $tid -AppId $appId -Certificate $cert
Start-IMGraphBulkDocumentation -OutputFormat html -PolicyGroup DeviceConfiguration
```
`IM_SETTINGS_STORE=Memory` in the environment does the same thing from the first line
of the module load, which matters if anything the module logs during load would
otherwise resolve against the real store.
| Cmdlet | Use |
| --- | --- |
| `Use-IMSettingsStore -Memory [-Seed]` | switch to memory. `-Seed` copies the persisted values in first, so the session starts from the real configuration and then diverges without writing back. |
| `Use-IMSettingsStore <path>` | switch to a `Json` store at that file, creating it if missing. |
| `Use-IMSettingsStore -Registry` | switch back to `HKCU`. Windows only. |
| `Export-IMSettingsStore <path>` | write the whole current store to a JSON file, whatever mode it is in - including a registry store. |
| `Import-IMSettingsStore <path>` | load a settings file into the **active** store, value by value. |
Two things about `Import-IMSettingsStore` worth knowing:
- It **merges** into what is already there rather than replacing it. For a clean slate,
`Use-IMSettingsStore -Memory` (without `-Seed`) first, then import into that.
- It goes through the storage layer, so one implementation is correct for all three
modes: values persist in `Json` mode, land in the registry in `Registry` mode, stay in
memory in `Memory` mode. Which also means importing into a registry store **writes to
the registry**.
- Keys with no registration are imported anyway - the hidden keys and per-feature state
namespaces are real - but they are listed in a warning, because a typo looks
identical and would otherwise silently do nothing.
## Stored shape
Everything is a string. Booleans are PascalCase `"True"`/`"False"`, which is what
`$true.ToString()` produces and what every existing store contains.
`Set-SettingValue` coerces through `Format-SettingStoreValue`, so `$true`, `"true"` and
`"TRUE"` all land in the one canonical shape, and a value written through the resolver
is indistinguishable from one written by the Settings dialog.
Watch out for `[bool]"False"` - it is `$true` in PowerShell, as any non-empty string is.
Stored Booleans are compared to `"true"`, never cast. Both the reader and the writer do
this the same way on purpose.
An **empty string** counts as "not set" on read, so clearing a text box in the Settings
dialog restores the registered default rather than persisting `""`. "Is it configured"
is therefore a different question from "does it have a value", and
`Test-SettingValueConfigured` (`Get-IMSetting` does not expose it) answers it by asking
whether the value exists at that path at all.
## Legacy layout note
A `SubPath` with more than one level (`<tenantid>\IntuneManager`) used to be stored by
the JSON/memory store as a **single flat property** literally named
`"<tenantid>\IntuneManager"`, while the registry stored the same path as nested keys.
The cause was `"a\b".Split(@('/','\'))` not splitting at all - PowerShell binds a string
array to a different `String.Split` overload and hands back the whole path as one
element. It is now `[char[]]`-cast and splits properly.
Existing settings files keep working: `Get-SettingsTreeNode` resolves a flat property
first and writes back to it when it finds one, so no tenant-specific value is orphaned
by the fix. New values nest.
## Testing
Settings leak from the developer machine into tests, and tests leak into the developer
machine. `Tests/TestBootstrap.ps1` has the two helpers:
```powershell
Describe 'x' {
BeforeEach { $script:saved = Use-TestSettingsStore }
AfterEach { Restore-TestSettingsStore -State $script:saved }
InModuleScope IntuneManagement { It 'y' { ... } }
}
```
`Use-TestSettingsStore` swaps in an empty memory store (so every key resolves to its
registered default) and returns the previous state for `Restore-TestSettingsStore`.
**The hooks must be declared at `Describe` level, wrapping `InModuleScope`.** Pester 4
silently never runs a `BeforeEach`/`AfterEach` declared *inside* an `InModuleScope`
block - nothing fails, the tests just run against the developer's real store and write
to it. `Static.Tests.ps1` has a gate for this with a shrink-only allowlist of the files
that still get it wrong.
`Tests/SettingsStore.Tests.ps1` covers the store modes, path resolution, value shape,
provenance, portability and the public surface.
## Auditing
`Tools/Audit-Settings.ps1` cross-references every registration against every read and
write. It is wired into `Static.Tests.ps1` as a gate; see its help for the full
taxonomy. Errors: orphan registrations, engine-consumed-but-UI-registered (breaks
headless sessions), SubPath-mismatched access, duplicate registrations. Warnings:
UI-only-but-engine-registered, and registered keys addressed by path instead of through
the resolver.
```powershell
.\Tools\Audit-Settings.ps1
.\Tools\Audit-Settings.ps1 -IncludeUndefined # also keys read but never registered
```
+79
View File
@@ -0,0 +1,79 @@
# Settings Cache Logging
## Settings
**[Settings](Settings.md) is the reference** - the three layers, the three store modes
(registry / JSON file / in-memory), scope and precedence, the public `*-IMSetting`
cmdlets, portable settings files, and the testing helpers. In short:
```text
Add-SettingsObject
-> setting metadata registered in a section
-> Get-SettingValue / Get-IMSetting
-> tenant-specific persisted value
-> global persisted value
-> setting DefaultValue
```
Important setting groups include:
| Area | Examples |
| --- | --- |
| Graph/auth | `ActiveAuthProvider`, app ID/secret/cert settings, cloud settings. |
| performance | `UseBatchAPI`, `UseParallelBatchAPI`, `ParallelBatchThrottle`, `GraphPaceIdentityEndpoints`. |
| import/export | `ImportType`, assignment/scope tag import defaults, matching settings. |
| UI | delete/bulk delete visibility, expand assignments, formatting. |
| cache | clear cache before import/export, clear all cache. |
## Cache
Core cache functions:
| Function | Purpose |
| --- | --- |
| `Set-CacheObject` | writes in-memory or persistent cache values. |
| `Get-CacheObject` | reads cache values with optional default. |
| `Clear-CacheObject` | removes cache values. |
| `Get-CacheStats` | diagnostics. |
Common cache keys:
| Key pattern | Meaning |
| --- | --- |
| `AADObjectCache_<tenant>` | resolved groups/users/service principals for migration/import. |
| `TenantCache_<tenant>` | persistent backing file for tenant cache values. |
| `_migFileCache` script variables | per-bulk-export migration table buffers. |
| `_appConfigTargetAppCache` | app config target app lookup cache. |
## Logging
Main functions:
| Function | Use |
| --- | --- |
| `Write-Log` | normal and warning/error messages. |
| `Write-LogDebug` | debug-only diagnostics. |
| `Write-LogError` | exception-aware logging. |
| `Write-Status` | UI status line updates for long operations. |
Log behavior is influenced by settings such as `Debug`, `LogFile`, `LogFileSize`, and `LogOutputError`.
## Graph Call Telemetry
`Invoke-MSGraphAPI` and batch helpers populate `$script:AllGraphCalls`. The Graph Calls UI uses it to show:
| Field | Meaning |
| --- | --- |
| request URL/method | what was called. |
| provider | MSAL, MgGraph, etc. |
| status/error | result and parsed Graph error. |
| duration/KB/object count/page count | performance and response shape. |
| batch sub-requests | item-level status inside `$batch`. |
## Testing Notes
Settings can leak from the developer machine into tests, and tests can write into the
developer's real store. Use `Use-TestSettingsStore` / `Restore-TestSettingsStore` from
`Tests/TestBootstrap.ps1` - and declare the hooks at `Describe` level, not inside
`InModuleScope`. See [Settings: Testing](Settings.md#testing).
+148
View File
@@ -0,0 +1,148 @@
# Settings reference
Every setting the module registers, by the section it appears under in the Settings
dialog. **Setting** is the name shown in the dialog; **Key** is what `Get-IMSetting` /
`Set-IMSetting` and a settings file use. Where values are stored and how tenant and global
values combine is in [Settings.md](Settings.md).
**Generated** by `Tools/Export-SettingsReference.ps1` from `Get-IMSettingDefinition` - do not edit by hand. 83 settings.
A runbook settings file for `Import-IMSettingsStore` is a json object keyed by these names:
```json
{ "ExportFolder": "D:\\exports", "ExportAssignments": true, "UseBatchAPI": true, "UseParallelBatchAPI": true }
```
## Contents
- [Authentication](#authentication) (8)
- [General](#general) (13)
- [Import/Export](#importexport) (30)
- [Intune](#intune) (8)
- [Intune Tools](#intune-tools) (1)
- [MS Graph General](#ms-graph-general) (5)
- [MSAL](#msal) (14)
- [OAuth](#oauth) (4)
## Authentication
| Setting | Key | Type | Default | Description |
|---|---|---|---|---|
| Active authentication provider | `ActiveAuthProvider` | List | `MSAL` | Which authentication backend to use. MSAL is the built-in default. MgGraph requires the Microsoft.Graph.Authentication PowerShell module to be installed. OAuth is a pure-PowerShell provider for automation (CI / scheduled tasks / managed identity / workload identity federation) - no SDK required. Values: `Microsoft Authentication Library`, `Microsoft Graph PowerShell SDK`, `Direct OAuth (no SDK)`. |
| Application Id | `EntraCustomAppId` | String | | Custom Entra application (client) id used by MSAL and OAuth sign-in when no built-in application is selected above. The app registration needs the http://localhost redirect URI for browser-based login. |
| Authority | `EntraCustomAuthority` | String | | |
| Default cloud | `DefaultCloud` | List | `Public` | Microsoft cloud this tool signs in to by default. Public covers commercial + GCC commercial; USGov is GCC High; USGovDOD is GCC DoD; China is the Vianet cloud. Values: `Public (Global)`, `US Government (GCC High)`, `US Government (DoD)`, `China (Vianet)`. |
| Entra application | `EntraApp` | List | | Built-in Entra application used for interactive sign-in by both the MSAL and OAuth providers. Leave empty to use the custom Application Id below, or the default Microsoft Graph PowerShell app. Values: `*** Do NOT use *** Microsoft Intune PowerShell`, `Microsoft Graph PowerShell`. |
| Interactive login timeout (seconds) | `MSGraphInteractiveTimeoutSec` | Int | `600` | Maximum time to wait for an interactive login to complete (MSAL embedded/broker and OAuth browser flows). Only applies to flows a user is waiting in front of; silent and app-secret token requests have their own much shorter cap. The default was 180 s, which killed legitimate sign-ins that needed an account picker plus an MFA approval - and the sign-in status has a Cancel button, so an abandoned window does not depend on this timeout. |
| Redirect URL | `EntraCustomAppRedirect` | String | | |
| Tenant Id | `EntraCustomTenantId` | String | | |
## General
| Setting | Key | Type | Default | Description |
|---|---|---|---|---|
| Add errors to PowerShell output | `LogOutputError` | Boolean | `true` | Write errors to the Error Output of the PS Host. If disabled, errors will be written as a Warning. Eg. disable this if automation should skip logging PowerShell errors. |
| Check for updates | `CheckForUpdates` | Boolean | `true` | Check GitHub if there is a later version available |
| Debug | `Debug` | Boolean | `false` | |
| Environment color | `EnvironmentColor` | List | | Background color of the environment badge. Text color is auto-calculated for contrast. |
| Environment name | `EnvironmentText` | Text | | Label shown as a badge in the toolbar (e.g. Production, Lab). Leave empty to hide. |
| Hide experimental platform notice | `HideExperimentalPlatformNotice` | Boolean | `false` | Stop showing the startup notice that macOS and Linux support is experimental. Clear this to see it again. |
| Hide No-access items | `HideNoAccess` | Boolean | `false` | Remove items from the menu if object permissions is missing. Default is to mark them with red |
| Log file | `LogFile` | File | | |
| Max log file size | `LogFileSize` | Int | `1024` | |
| Proxy URI | `ProxyURI` | | | Specify the URI for the proxy eg http://&lt;server&gt;:&lt;port&gt; |
| Show tenant name | `MenuShowOrganizationName` | Boolean | `true` | Adds the organization name next to the login info on the menu bar |
| Theme | `AppTheme` | List | `Default` | Application color theme. Default follows the Windows app theme. Values: `Default (follow Windows)`, `Light`, `Dark`. |
| Use Intune role permissions for access marking | `UseRbacAccessMarking` | Boolean | `true` | Also ask Intune which resource actions the signed-in user's role allows, and mark menu items the user cannot change (orange) or read (red). Off = mark from the app's token scopes only. Refresh the token from the Profile popup after a role change. |
## Import/Export
| Setting | Key | Type | Default | Description |
|---|---|---|---|---|
| Add company name | `AddCompanyName` | Boolean | `true` | Default setting for adding company name to the export folder |
| Add ID to export file | `AddIDToExportFile` | Boolean | `true` | This will add object ID to the export file to support objects with the same name e.g. ObjectName_ObjectId.json |
| Add object type | `AddObjectType` | Boolean | `true` | Default setting for adding object type to the export folder |
| Clear All Objects From Cache | `ClearAllObjectsFromCache` | Boolean | `false` | Whether to clear all cached objects (default is to keep assignments and scopes cached). Use this option if you experience stale cache issues. |
| Clear Cache Before Export/Import | `ClearCacheBeforeExportImport` | List | `1` | Automatically clear object cache before export/import operations (useful when re-downloading profiles to get latest data) Values: `Bulk export only`, `Bulk and manual export`, `Bulk export and import`, `All operations`. |
| Combine requests into batch calls | `UseBatchAPI` | Boolean | `true` | Combine Graph requests into $batch calls, up to 20 per call, on every path that can batch: listing, policy bodies, sub-resources, assignments, import and delete. Turn off to send every request on its own - slower, but each call shows individually in the Graph log. See Docs/GraphBatching.md. |
| Convert synced groups | `ConvertSyncedGroupOnImport` | Boolean | `true` | When a group referenced by an imported policy was AD-synced in the source tenant and does not exist in the target, recreate it as a cloud Entra group. When off, the group is skipped and the reference is left untranslated. |
| Create groups and filters | `CreateGroupOnImport` | Boolean | `true` | Create Entra groups and assignment filters referenced by an imported policy when they do not exist in the target tenant. Groups are created from the export's Groups sidecar (dynamic groups keep their membership rule) or as a default cloud security group; filters from the AssignmentFilters sidecar (platform + rule preserved). |
| Default Conditional Access Policy State | `ConditionalAccessState` | List | `disabled` | Define the state imported Conditional Access policies get. It is recommended to keep this Off (disabled) to avoid accidental tenant lock out. Values: `As Exported - Change On to Report-only`, `As Exported`, `Report-only`, `Off`. |
| Export Assignments | `ExportAssignments` | Boolean | `true` | Default setting for exporting assignments |
| Export file encoding | `ExportFileEncoding` | List | `utf8` | Character encoding for exported JSON files. UTF-8 is recommended - it is what Intune, git and other tools expect. Change it only if an existing pipeline depends on another encoding. Values: `UTF-8`, `UTF-8 with BOM`, `Unicode (UTF-16 LE)`. |
| Export Json format | `ExportJsonFormat` | List | `indented` | Layout of exported JSON. Indented is readable but PowerShell 5.1 and 7 indent differently, so files exported from different hosts differ even when the data is identical. Compact is identical on both - use it with 'Sort Json Properties' if the export is stored in git or compared by automation. Values: `Indented (readable)`, `Compact (single line)`. |
| Export nested group levels | `ExportNestedGroupLevels` | String | `1` | Depth of group-membership recursion when exporting groups. 1 (default) only exports the group assigned to the policy. 2 also exports groups that are direct members of the assigned group. 3 goes one level deeper, and so on. Higher values mean more Graph calls and may hit throttling on large group trees. |
| Graph request timeout (seconds) | `MSGraphRequestTimeoutSec` | Int | `100` | Maximum time in seconds to wait for a single Graph request before it is aborted. Bounds how long a stalled request can block the UI. Default 100. |
| Import Assignments | `ImportAssignments` | Boolean | `true` | Default value for Import assignments when importing objects |
| Import match normalized name | `ImportMatchEnableNormalizedName` | Boolean | `true` | Allow update and skip-if-exists import matching after removing organization-specific prefixes or variables from names. |
| Import match organization tokens | `ImportMatchOrganizationTokens` | String | | Extra organization-specific words or prefixes to remove before normalized-name matching. Separate values with comma, semicolon, or new lines. |
| Import match policy reference | `ImportMatchEnablePolicyToken` | Boolean | `true` | Allow update and skip-if-exists import matching by policy reference token in the object name, e.g. [SEC-1053]. Exact name and same-tenant ID matching are always available. |
| Import match policy reference regex | `ImportMatchPolicyReferenceRegex` | String | `(?i)(?:\[(?<ref>[A-Z][A-Z0-9]{1,15}-\d{2,10})\])|(?<ref>\b[A-Z][A-Z0-9]{1,15}-\d{2,10}\b)` | Regex used to find policy reference tokens in names. It should include a named capture group called ref. Default matches values like [SEC-1053] or SEC-1053. |
| Import Scope (Tags) | `ImportScopeTags` | Boolean | `true` | Default value for Import Scope (Tags) when importing objects |
| Import type | `ImportType` | List | `alwaysImport` | How files are imported. Always import: no detection of existing objects. Skip if object exists: skip when a matching object is found. Replace: import the file, copy the existing object's assignments to it, then delete the existing object. Replace with assignments: same but assignments come from the import file. Update: settings on the existing object are replaced from the file. Values: `Always import`, `Skip if object exists`, `Replace`, `Replace with assignments`, `Update`. |
| Multi Admin Approval justification | `MultiAdminApprovalJustification` | String | | Reason sent with every create/update/delete when the tenant requires Multi Admin Approval. Leave empty unless Tenant administration > Multi Admin Approval has an access policy covering what you are changing. With a justification set, protected changes are queued for a second administrator to approve instead of failing. |
| Pace one-per-second Graph endpoints | `GraphPaceIdentityEndpoints` | Boolean | `true` | Graph allows one request per second per tenant, across all applications, on Conditional Access policies, named locations, authentication strengths and identity protection - and sends no Retry-After when it throttles them. When enabled, requests to those endpoints are sent one at a time, one second apart, and are kept out of parallel batch dispatch. Turn off only if Microsoft has raised the limit for your tenant. |
| Parallel batch throttle limit | `ParallelBatchThrottle` | String | `4` | Maximum number of concurrent $batch POST requests when 'Parallel batch dispatch' is enabled. Higher values are faster but more likely to trigger HTTP 429 throttling. Recommended range: 2-8. |
| Portable file names | `PortableFileNames` | Boolean | `false` | Remove characters that are invalid on ANY supported platform from exported file names, not just the ones invalid on the current one. Windows rejects " < > \| : * ? \ / while Linux and macOS reject only /, so an export made on Linux can contain file names Windows cannot open. Enable this when exports are shared between platforms. Changes the file names of new exports. |
| Replace organization values in export files | `ExportReplaceOrganizationValues` | Boolean | `true` | Replace tenant-specific values with placeholders in exported JSON. By default the tenant id becomes %OrganizationId%; the organization name is left as written unless you opt in. Turn this off to export the raw values - readable and diff-friendly, but the file is then tied to the tenant it came from. Importing a file that already contains placeholders always resolves them, whatever this is set to. Which values are replaced can be changed, see Docs/ExportImportAndCopy.md. |
| Resolve reference info | `ResolveReferenceInfo` | Boolean | `true` | This will export/import info for referenced/navigation properties eg certificates in VPN profiles etc. |
| Root folder | `RootFolder` | Folder | | Root folder for exporting/importing objects |
| Send batch calls in parallel (experimental) | `UseParallelBatchAPI` | Boolean | `false` | Send $batch calls concurrently instead of one at a time. Controls concurrency only - whether requests are batched at all is 'Combine requests into batch calls'. Requires PowerShell 7+. Speeds up large queries but raises the chance of HTTP 429 throttling; queues of 20 requests or fewer still go out one at a time. Leave off unless you've tested it in your tenant. |
| Sort Json Properties | `SortJsonProperties` | Boolean | `false` | Sort JSON properties alphabetically when exporting to improve file readability and consistency |
## Intune
| Setting | Key | Type | Default | Description |
|---|---|---|---|---|
| App download folder | `IntuneAppDownloadFolder` | Folder | | Folder where app packages will be downloaded and where encryption files will be saved |
| App packages folder | `IntuneAppPackagesFolder` | Folder | | Root folder where intune app packages are located |
| Get all pages | `GetAllPages` | Boolean | `true` | Get all pages when getting items in the UI. Note: This can take long time in environments with lots of policies and apps. |
| Graph Page Size | `GraphPageSize` | List | `0` | How many items load at a time Values: `Graph Default`, `5`, `20`, `50`, `100`, `1000`, `All`. |
| Max characters per cell | `ObjectListMaxCellLength` | Int | `50` | With single-line values on, cut a cell's text at this many characters so one long description cannot push the other columns out of view. Hover the cell for the full text; sorting and the filter still use the whole value. 0 = no limit. |
| Menu Object Type | `ObjectViewType` | List | `Group` | Specify object type for the menu. Group: Groups items together like the portal. Type - Single item based on API. Some APIs are split into multiple menu items. Values: `Group`, `Type (API)`. |
| Save Encryption File | `IntuneSaveEncryptionFile` | Boolean | | Save encryption file when uploading an app. This can then be used to when downloading the app file. |
| Single-line values in object list | `ObjectListFirstLineOnly` | Boolean | `true` | Show only the first line of multi-line values (e.g. store app descriptions) in the object list, so every row is one line high. Hover a cell for the full text. Takes effect when the list is next loaded. |
## Intune Tools
| Setting | Key | Type | Default | Description |
|---|---|---|---|---|
| Format OMA-URI Settings | `FormatOMAURI` | Boolean | `false` | Automatically clean up XML formatting in OMA-URI and ADMX registry policies for consistent output |
## MS Graph General
| Setting | Key | Type | Default | Description |
|---|---|---|---|---|
| Expand assignments | `ExpandAssignments` | Boolean | `true` | Expand assignments when listing objects. This can be used in custom columns based on assignment info |
| Refresh Objects after copy | `RefreshObjectsAfterCopy` | Boolean | `true` | Reload the object list after copying an object |
| Show Bulk Delete | `AllowBulkDelete` | Boolean | `true` | Allow using bulk delete to delete all objects of selected types |
| Show Delete button | `AllowDelete` | Boolean | `false` | Allow deleting individual objectes |
| Use Graph 1.0 (Not Recommended) | `UseGraphV1` | Boolean | `false` | This will use production verionof graph, v1.0. Note: Thot officially supported since this can have unpredicted results. Some parts will require Beta version of Graph. |
## MSAL
| Setting | Key | Type | Default | Description |
|---|---|---|---|---|
| Enable Continuous Access Evaluation (CAE) | `EnableCAE` | Boolean | `true` | Adds the 'cp1' client capability so Entra ID can revoke this session in near-real-time when admin policies change. Takes effect after an app restart / fresh login. |
| Get Tenant List | `GetTenantList` | Boolean | `false` | Get a list of all tenants the current user has access to. Only used when the user has access to multiple tenants. This may cause duplicate login/consent prompts first time |
| Log MSAL correlation IDs | `MSALCorrelationLogging` | Boolean | `false` | Generate and log a correlation Guid per AcquireToken call. Useful when working with Microsoft support; off by default to keep logs quiet. |
| MSAL log level | `MSALLogLevel` | List | `Info` | Minimum severity of MSAL log messages to capture. Verbose is noisy. Values: `Error`, `Warning`, `Info`, `Verbose`. |
| MSAL logging | `MSALEnableLogging` | Boolean | `true` | Write MSAL's internal log messages to msal.log under %LOCALAPPDATA%\IntuneManagement. Useful for diagnosing authentication failures. |
| MSAL logging: include personal data (PII) | `MSALEnablePiiLogging` | Boolean | `false` | Include MSAL's PII log messages in msal.log and unredact broker/WAM error text (logged as '(pii)' otherwise). The log will then contain user names, tenant and object ids and token details - review it before sharing. Requires app restart. |
| MSAL region (optional) | `MSALRegion` | String | | Force a regional ESTS endpoint (e.g. 'westeurope'). Leave empty for automatic. Applied via MSAL_FORCE_REGION; requires app restart. |
| Remember Login | `CacheMSALToken` | Boolean | `true` | Store the MSAL token in an encrypted file and automatically log on when the script starts. The token is stored in the users profile and can only be decrypted by the user that created it. Note: Requires restart |
| Sort Account List | `SortAccountList` | Boolean | `false` | Sort the list of cached accounts based on user name. Updated at restart or account change |
| Sort Tenant List | `SortTenantList` | Boolean | `false` | Sort the list of available tenants based on Tenant name. Updated at restart or account change |
| Use MsalCacheHelper library | `MSALUseCacheHelperLib` | Boolean | `true` | Use Microsoft.Identity.Client.Extensions.Msal.MsalCacheHelper for the token cache. Provides cross-process file locking and is the supported library. Disable to fall back to the legacy TokenCacheHelperEx (DPAPI, process-local lock). |
| Use system browser for login | `UseSystemBrowser` | Boolean | `true` | Sign in using the default web browser instead of the embedded view or WAM. Enables passkey/FIDO2 login and browser extensions. Takes precedence over WAM. Custom app registrations need the http://localhost redirect URI (Mobile and desktop applications). Turn off to use the embedded window. Requires app restart. |
| Use Web Account Manager (WAM) for login | `UseWAM` | Boolean | `false` | Use the Windows Web Account Manager broker (Windows Hello, device compliance claims). Turn this on only when the account you sign in with is your Windows account or one added under Settings > Accounts - for any other account WAM cannot keep the session alive and you are prompted again about every hour. Requires PowerShell 7 and an app restart. |
| WAM: list OS accounts | `WAMListOSAccounts` | Boolean | `true` | When WAM is enabled, also surface machine-joined Entra accounts in the account picker (PS7+). Off-by-default on PS5. |
## OAuth
| Setting | Key | Type | Default | Description |
|---|---|---|---|---|
| OAuth browser prompt | `OAuthPrompt` | List | `select_account` | OAuth /authorize prompt behaviour. 'Force login' re-authenticates even with an active browser session (equivalent to force-interactive); 'None' fails if interaction would be required. Values: `Select account`, `Force login`, `Consent`, `None (silent)`. |
| OAuth login hint (UPN) | `OAuthLoginHint` | String | | Optional UPN to pre-fill on the sign-in page (login_hint). |
| OAuth redirect port | `OAuthRedirectPort` | Int | `0` | Fixed loopback port for the browser redirect (http://localhost:<port>). 0 = pick a free port automatically. Set a fixed port only if your app registration requires a specific http://localhost:<port> redirect. |
| Remember login (cache token) | `OAuthCacheToken` | Boolean | `false` | Persist the OAuth refresh token (DPAPI-encrypted, current user) so the app silently resumes the browser session after a restart. When off, you sign in again after each restart (usually a quick browser redirect via existing SSO). |
Binary file not shown.

After

Width:  |  Height:  |  Size: 396 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 177 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 272 KiB