ServiceEM creates groups with a predictable naming convention, but classification reports require explicit tier metadata. Since groups do not support Custom Security Attributes (CSA), the only automatic classification path is AlternateObjectTierLevelAttributes.
This design adds an optional -UpdateClassificationConfig switch to landing-zone creation that automatically generates AlternateObjectTierLevelAttributes.Group filters from the ServiceEM naming convention and persists them to EntraOpsConfig.json. The implementation supports multiple runs with varying prefixes, merges without destroying existing filters, and an opt-in force overwrite for intentional replacement.
- ServiceEM creates EAM-aligned groups with names like
SG-Rg-Management-ControlPlane-Admins. - Classification reports show these groups as Unclassified unless manually tagged.
- Groups cannot use CSAs (Microsoft Entra limitation).
- The Object Classification File requires per-object maintenance.
- Users must manually discover and write
AlternateObjectTierLevelAttributes.Groupfilter expressions. - Multi-service deployments may use different
GroupPrefixvalues across teams or landing zones. - Existing custom filters must not be silently destroyed by a ServiceEM run.
Generate wildcard-based AlternateObjectTierLevelAttributes.Group filter expressions that match the ServiceEM naming convention. On each run with -UpdateClassificationConfig:
- Read the existing
Groupfilter for each tier. - Detect whether the existing filter is empty, already contains ServiceEM-like
-likepatterns, or is a custom expression. - Merge new patterns with
-orwhen safe; skip when custom filters are detected. - Persist the merged result to both
$Global:EntraOpsConfigandEntraOpsConfig.jsonon disk.
Each tier (ControlPlane, ManagementPlane, WorkloadPlane) is evaluated independently.
| Existing Filter State | Action | Example Result |
|---|---|---|
| Empty | Write the new pattern directly. | $Object.ObjectDisplayName -like "SG-*-ControlPlane-*" |
Contains ServiceEM-like -like patterns |
Append new pattern with -or if not already present. |
$Object.ObjectDisplayName -like "SG-*-ControlPlane-*" -or $Object.ObjectDisplayName -like "PRG-*-ControlPlane-*" |
| Already contains the exact new pattern | Skip with verbose log. | No change. |
Custom expression (regex, complex logic, no -like on ObjectDisplayName) |
Skip with warning. Use -ForceClassificationConfigUpdate to replace. |
No change unless forced. |
A filter is considered "ServiceEM-like" when it contains at least one substring matching:
\$Object\.ObjectDisplayName\s+-like\s+"[^"]*"Anything else (e.g. $Object.ObjectDisplayName -match "Tier0-.*", $Object.AssignedAdministrativeUnits.displayName -contains "...", or compound logic without -like on display name) is treated as custom and protected from overwrite.
Before appending, the helper checks if the new pattern string already exists as a substring in the existing filter. This prevents -or bloat from repeated runs with the same prefix.
Available on:
New-EntraOpsServiceBootstrapNew-EntraOpsSubscriptionLandingZone
When set, triggers the classification config helper after the deployment report is assembled.
Available alongside -UpdateClassificationConfig.
When set, the helper replaces existing custom filters instead of skipping them. Use this when:
- You are intentionally standardising on a single naming convention.
- You want to migrate from a custom filter to the ServiceEM-generated approach.
- You accept that your previous custom expression will be lost.
New-EntraOpsSubscriptionLandingZone `
-DeploymentPrefix "Management" `
-AzureRegion "westeurope" `
-SubscriptionId "<subscription-id>" `
-UpdateClassificationConfigResult:
"Group": {
"Enabled": true,
"ControlPlane": "$Object.ObjectDisplayName -like \"SG-*-ControlPlane-*\"",
"ManagementPlane": "$Object.ObjectDisplayName -like \"SG-*-ManagementPlane-*\"",
"WorkloadPlane": "$Object.ObjectDisplayName -like \"SG-*-WorkloadPlane-*\"",
"UserAccess": ""
}New-EntraOpsSubscriptionLandingZone `
-DeploymentPrefix "Connectivity" `
-GroupPrefix "PRG" `
-AzureRegion "northeurope" `
-SubscriptionId "<subscription-id>" `
-UpdateClassificationConfigResult:
"Group": {
"Enabled": true,
"ControlPlane": "$Object.ObjectDisplayName -like \"SG-*-ControlPlane-*\" -or $Object.ObjectDisplayName -like \"PRG-*-ControlPlane-*\"",
"ManagementPlane": "$Object.ObjectDisplayName -like \"SG-*-ManagementPlane-*\" -or $Object.ObjectDisplayName -like \"PRG-*-ManagementPlane-*\"",
"WorkloadPlane": "$Object.ObjectDisplayName -like \"SG-*-WorkloadPlane-*\" -or $Object.ObjectDisplayName -like \"PRG-*-WorkloadPlane-*\"",
"UserAccess": ""
}New-EntraOpsSubscriptionLandingZone `
-DeploymentPrefix "Dev" `
-AzureRegion "westeurope" `
-SubscriptionId "<subscription-id>" `
-UpdateClassificationConfigResult: No change. The SG-* pattern is already present; deduplication skips the append.
A user has manually configured:
"ControlPlane": "$Object.ObjectDisplayName -match \"^Tier0-.*\""Running with -UpdateClassificationConfig:
New-EntraOpsServiceBootstrap -ServiceName "MyService" -UpdateClassificationConfigResult:
WARNING: Custom Group classification filter detected for ControlPlane. Skipping to avoid overwrite. Use -ForceClassificationConfigUpdate to replace.
The ManagementPlane and WorkloadPlane tiers are still updated if they are empty or ServiceEM-like.
New-EntraOpsServiceBootstrap `
-ServiceName "MyService" `
-UpdateClassificationConfig `
-ForceClassificationConfigUpdateResult: All three tiers are replaced with the ServiceEM-generated patterns. The previous -match filter is lost.
For environments where naming conventions vary but you want minimal config complexity, a broader wildcard can be used instead of prefix-specific patterns:
"ControlPlane": "$Object.ObjectDisplayName -like \"*-ControlPlane-*\""This matches any prefix, any delimiter, any service name — as long as the plane name appears between delimiters.
| Aspect | Prefix-Specific Merge | Broader Wildcard |
|---|---|---|
| Precision | High — only matches intended prefixes | Medium — could match non-ServiceEM groups containing "ControlPlane" |
| Maintenance | Low — automatic merge handles new prefixes | None — one filter forever |
| Multi-service support | Native via -or chaining |
Native — single expression |
| Custom filter coexistence | Supported via merge heuristic | Would overwrite custom filters unless -Force is used |
Recommendation: Use the prefix-specific merge approach as the default. It is precise, safe with custom filters, and scales automatically. Offer the broader wildcard as a documented manual alternative for simple environments.
The implementation leverages the existing EntraOps config infrastructure:
$configJson = Get-Content -Path $ConfigFilePath -Raw | ConvertFrom-Json -Depth 10 -AsHashtableConvertFrom-Json -AsHashtablecreates case-sensitiveOrderedHashtableobjects.- The helper accesses properties via
$configJson.AlternateObjectTierLevelAttributes['Group'](dictionary-style) to remain compatible with both hashtable and PSCustomObject representations.
$configJson | ConvertTo-Json -Depth 10 | Set-Content -Path $ConfigFilePath -Encoding UTF8- Round-trips the entire config with preserved structure and depth.
[ordered]@{}is used when creating new sections to maintain key order for readability.
$Global:EntraOpsConfig.AlternateObjectTierLevelAttributes['Group'] = $mergedFilters- Updated simultaneously with the disk file so subsequent cmdlets in the same session see the new filters immediately.
- No reload required before running
Save-EntraOpsPrivilegedEAMJsonorNew-EntraOpsReportingData.
ConvertFrom-Json -AsHashtablekeys are case-sensitive, but EntraOps consistently uses PascalCase (AlternateObjectTierLevelAttributes,ControlPlane, etc.).- The helper normalises key lookups to match the casing used by
New-EntraOpsConfigFile. - If
$Global:EntraOpsConfigwas loaded viaConnect-EntraOps(which may deliver case-insensitive hashtables), both.Propertyand['Property']access patterns work.
When -UpdateClassificationConfig is used, the deployment report includes a ClassificationConfig property:
$report = New-EntraOpsSubscriptionLandingZone -DeploymentPrefix "Mgmt" -UpdateClassificationConfig
$report.ClassificationConfigName Value
---- -----
ControlPlane Merged (2 patterns)
ManagementPlane Merged (2 patterns)
WorkloadPlane Merged (2 patterns)
UserAccess (empty)
This allows callers to inspect what changed without parsing the config file.
| Object Type | Classification Source | Notes |
|---|---|---|
| Groups | Generated AlternateObjectTierLevelAttributes.Group filters |
Automatic for all ServiceEM-created groups matching the naming convention. |
| Users / Service Principals | Existing CSA or alternate filters | Unchanged by this feature. |
| Access Packages / Catalogs / Resource Groups | Role assignment classification | These are not object-classified via AlternateObjectTierLevelAttributes; their classification comes from RBAC role/scope analysis as before. |
| File | Role |
|---|---|
EntraOps/Private/Update-EntraOpsServiceEMClassificationConfig.ps1 |
Helper — reads existing filters, detects pattern type, merges or skips, persists to config. |
EntraOps/Public/ServiceEM/New-EntraOpsServiceBootstrap.ps1 |
Entry point — accepts -UpdateClassificationConfig and -ForceClassificationConfigUpdate, calls helper after deployment. |
EntraOps/Public/ServiceEM/New-EntraOpsSubscriptionLandingZone.ps1 |
Entry point — accepts same switches, passes through to bootstrap. |
- Multi-team environments often use different prefixes per team or workload.
- Existing custom filters represent deliberate user configuration that should not be silently lost.
- Idempotency — repeated runs with the same prefix produce no duplicate
-orclauses.
A user might have a sophisticated ControlPlane filter (e.g. based on Administrative Unit membership) while leaving ManagementPlane empty. Tier-by-tier merging allows ServiceEM to fill the gaps without touching the custom logic.
Config file mutation should always be a deliberate action. -Force makes the destructive path explicit and auditable.
- Explicit opt-in avoids surprising users who already have custom classification strategies.
- Config file mutation should remain a deliberate action in production pipelines.
Thanks for the detailed design. I've chosen a different approach for classifying ServiceEM groups. Here's why, and what is implemented now.
Why I didn't go with naming-convention filters
AlternateObjectTierLevelAttributesexpressions are executed as PowerShell. Having ServiceEM rewrite them automatically, with merge rules and a force-overwrite switch, makesEntraOpsConfig.jsonharder to review as trusted code.-orclause, and the filters would break if the group name delimiter ever becomes configurable.What's implemented
ServiceEM now classifies only the groups it creates for the workload plane. Both are found by mail nickname (
<ServiceName>.WorkloadPlane.Admins/.Users), not by display name:WorkloadPlaneUserAccessThere are two options, and they can be used together:
ServiceEM.AdministrativeUnits.{WorkloadPlane, UserAccess}$Object.AssignedAdministrativeUnits.id -contains "<id>". Unlike a name, AU membership can't be changed by renaming the group.ServiceEM.UpdateObjectClassificationFileRemove-EntraOpsServiceCatalogremoves the entries of the groups it deletes.Both options are in the Configuration Wizard and
New-EntraOpsConfigFile.ControlPlane-Admins, ManagementPlane-Admins and the administrator group (CatalogPlane-Members) are deliberately left out. In the Centralized model they're tenant-wide groups and should be classified separately, e.g. through the Object Classification File.
Related changes:
ServiceEM.RoleAssignableWorkloadPlaneAdmins.New-EntraOpsSubscriptionLandingZonecan override this with-RoleAssignableWorkloadPlaneAdmins.