mirror of
https://github.com/Micke-K/IntuneManagement.git
synced 2026-09-28 19:05:38 +02:00
IntuneManagement 4.0.0-beta1
This commit is contained in:
@@ -0,0 +1,404 @@
|
||||
# Per-object documentation context. Replaces all of the $script:* accumulators
|
||||
# the old Documentation.psm1 reset at the top of every Get-ObjectDocumentation
|
||||
# call. Input providers and *DocHandler classes mutate the lists via Add*()
|
||||
# methods; the engine calls ToResult() to produce the per-object PSCustomObject
|
||||
# the output providers consume.
|
||||
#
|
||||
# Substitution map (from DocumentationMigration.md):
|
||||
# $global:chkIncludeScripts.IsChecked -> Options.IncludeScripts
|
||||
# $global:chkExcludeScriptSignature.IsChecked -> Options.ExcludeScriptSignature
|
||||
# $global:documentationLanguage -> Language
|
||||
# $script:objectBasicInfo -> BasicInfo
|
||||
# $script:objectSettingsData -> SettingsData
|
||||
# $script:objectComplianceActionData -> ComplianceActions
|
||||
# $script:applicabilityRules -> ApplicabilityRules
|
||||
# $script:objectAssignments -> Assignments
|
||||
# $script:objectScripts -> Scripts
|
||||
# $script:customTables -> CustomTables
|
||||
# $script:admxCategories -> ADMXCategories
|
||||
# $script:ObjectTypeFullTable -> ObjectTypeFullTable
|
||||
# $script:scopeTags -> ScopeTags
|
||||
# $script:languageStrings -> LanguageStrings
|
||||
# $script:offlineDocumentation -> SourceTenantUnavailable (formerly OfflineDocumentation)
|
||||
# $script:settingsProperties, CurrentSubCategory, ValueOutputProperty -> per-call
|
||||
# fields used during input-provider walks; added incrementally as providers land
|
||||
|
||||
class DocumentationContext {
|
||||
# ---- Inputs ----
|
||||
[object] $PolicyObject
|
||||
[string] $Language
|
||||
[hashtable]$Options
|
||||
|
||||
# ---- Cross-batch caches (lazily populated; survive ResetForObject so they
|
||||
# amortize across all policies in one bulk run, matching old code's
|
||||
# $global:* caches) ----
|
||||
[hashtable]$LanguageStrings = @{}
|
||||
[object[]] $ScopeTags = @() # /deviceManagement/roleScopeTags
|
||||
[hashtable]$CachedCfgSettings = @{} # SettingsCatalog: settingDefinitionId -> definition
|
||||
[object[]] $CfgCategories = @() # /deviceManagement/configurationCategories + /complianceCategories
|
||||
# Resolved assignment-filter display names (filterId -> displayName, or $null
|
||||
# for IDs the directory didn't return on lookup so we don't keep retrying).
|
||||
# Survives ResetForObject so a bulk-doc run resolves each filter exactly
|
||||
# once across all policies.
|
||||
[hashtable]$FilterNamesById = @{}
|
||||
# True once the tenant-wide assignment-filter list has been loaded into
|
||||
# FilterNamesById (from the login-time dependency cache, the run prefetch
|
||||
# batch, or the lazy fallback in Add-AssignmentsForObject). Acts as a
|
||||
# negative-cache stamp too, so a failed tenant-wide fetch isn't retried
|
||||
# per policy.
|
||||
[bool] $FiltersLoaded = $false
|
||||
# Resolved assignment-group display names (groupId -> displayName, or $null
|
||||
# for IDs the directory didn't return on lookup so we don't keep retrying).
|
||||
# Survives ResetForObject so one bulk-doc run resolves each group at most
|
||||
# once across all policies. Front-loaded in one getByIds batch by the prefetch
|
||||
# (when the "Use Batch API" setting is on); otherwise filled lazily per-object
|
||||
# by Add-AssignmentsForObject. Same negative-cache semantics as FilterNamesById.
|
||||
[hashtable]$GroupNamesById = @{}
|
||||
# Per-run prefetch of policy sub-resources, populated by
|
||||
# Initialize-DocumentationRunPrefetch in one Graph $batch before the output
|
||||
# loop: policyId -> settings[] (with settingDefinitions expanded) for
|
||||
# Settings Catalog / Compliance V2 policies. Cleared at the start of every
|
||||
# run so a re-documented policy reflects current tenant state.
|
||||
[hashtable]$PrefetchedPolicySettings = @{}
|
||||
# When true, the SOURCE tenant the export came from is not reachable, so
|
||||
# source-tenant-specific lookups (assignments->groups, scope-tag/filter/app
|
||||
# names, named locations, ToU, linked certs, reusable settings, per-policy
|
||||
# settings-by-id) are skipped. It does NOT mean "no tenant at all": generic
|
||||
# Intune schema (setting definitions/categories, ADMX, intent templates,
|
||||
# resourceOperations) is identical on every tenant and is still fetched from
|
||||
# whatever tenant is connected, gated by Test-DocumentationGraphAvailable.
|
||||
[bool] $SourceTenantUnavailable
|
||||
|
||||
# Title the document by something other than the policy's display name, and
|
||||
# fold the Basics rows into the settings table instead of emitting a second
|
||||
# table. Both exist for the default enrollment policies: Windows Hello for
|
||||
# Business, Windows Restore, the device limit, the platform restrictions and
|
||||
# the enrollment status page all ship under the SAME display name, "All users
|
||||
# and all devices", so a document headed by that name does not say which
|
||||
# policy it is, and the two tables between them hold only a handful of rows.
|
||||
# Set per object by the ObjectInfo customizer; both reset between objects.
|
||||
[string] $DocumentName = $null
|
||||
[bool] $MergeBasicInfo = $false
|
||||
|
||||
# ---- Per-object accumulators (drained by ToResult) ----
|
||||
[System.Collections.Generic.List[object]] $BasicInfo = [System.Collections.Generic.List[object]]::new()
|
||||
[System.Collections.Generic.List[object]] $SettingsData = [System.Collections.Generic.List[object]]::new()
|
||||
[System.Collections.Generic.List[object]] $ComplianceActions = [System.Collections.Generic.List[object]]::new()
|
||||
[System.Collections.Generic.List[object]] $ApplicabilityRules = [System.Collections.Generic.List[object]]::new()
|
||||
[System.Collections.Generic.List[object]] $Assignments = [System.Collections.Generic.List[object]]::new()
|
||||
[System.Collections.Generic.List[object]] $Scripts = [System.Collections.Generic.List[object]]::new()
|
||||
[System.Collections.Generic.List[object]] $CustomTables = [System.Collections.Generic.List[object]]::new()
|
||||
[object[]] $ADMXCategories = @() # /deviceManagement/groupPolicyCategories?$expand=parent,definitions — batch cache, survives ResetForObject
|
||||
|
||||
# Intent template caches (deviceManagementIntent input provider). Keyed by
|
||||
# templateId / categoryId; survive ResetForObject so a bulk run of N intents
|
||||
# against the same template only pays one round-trip per category.
|
||||
[hashtable] $IntentCategories = @{} # templateId -> categories[] (with $expand=settingDefinitions)
|
||||
[hashtable] $IntentCatRecommendedSettings = @{} # categoryId -> recommendedSettings[]
|
||||
|
||||
# ---- Cross-type accumulator (for ScopeTags consolidated table at end of batch) ----
|
||||
[hashtable] $ObjectTypeFullTable = @{}
|
||||
|
||||
# ---- Result fields populated by input providers / handlers ----
|
||||
[string[]] $DefaultDocumentationProperties = @('Name','Value')
|
||||
[object[]] $DisplayProperties = @()
|
||||
[string] $ErrorText
|
||||
[string] $InputType
|
||||
[bool] $UpdateFilteredObject
|
||||
[object[]] $UnconfiguredProperties = @()
|
||||
|
||||
# ---- Walker / translate-primitive state (replaces $script:currentObject,
|
||||
# $script:CurrentSubCategory, $script:propLevel, $script:propertySeparator,
|
||||
# $script:objectSeparator) ----
|
||||
[object] $CurrentObject
|
||||
[string] $CurrentSubCategory
|
||||
[int] $PropLevel = 0
|
||||
[string] $PropertySeparator = ','
|
||||
[string] $ObjectSeparator = [System.Environment]::NewLine
|
||||
|
||||
DocumentationContext() {
|
||||
$this.Options = [DocumentationContext]::DefaultOptions()
|
||||
$this.Language = 'en'
|
||||
}
|
||||
|
||||
DocumentationContext([object]$PolicyObject, [string]$Language, [hashtable]$Options) {
|
||||
$this.PolicyObject = $PolicyObject
|
||||
$this.Language = if ($Language) { $Language } else { 'en' }
|
||||
$this.Options = if ($Options) { $Options } else { [DocumentationContext]::DefaultOptions() }
|
||||
}
|
||||
|
||||
# Default Options hashtable. Keys recognised by the engine:
|
||||
# IncludeScripts [bool] emit Scripts collection (default true)
|
||||
# ExcludeScriptSignature [bool] strip script signing blocks (default false)
|
||||
# IncludePolicyId [bool] engine appends an 'Id' BasicInfo row after
|
||||
# handler/input dispatch when true (default false)
|
||||
static [hashtable] DefaultOptions() {
|
||||
return @{
|
||||
IncludeScripts = $true
|
||||
ExcludeScriptSignature = $false
|
||||
IncludePolicyId = $false
|
||||
Language = 'en'
|
||||
PropertySeparator = ';'
|
||||
ObjectSeparator = [System.Environment]::NewLine
|
||||
# When true, the engine post-step that translates $obj.assignments
|
||||
# into Assignment rows is skipped. Old engine gated this on
|
||||
# $global:chkExcludeAssignments (default off — assignments included).
|
||||
ExcludeAssignments = $false
|
||||
# ObjectInfo walker: when a property is null on the input, the walker
|
||||
# can either skip emitting a row OR substitute the prop's declared
|
||||
# unconfiguredValue / defaultValue / emptyValueResourceKey. Old engine
|
||||
# gates these on UI checkboxes. Legacy defaults substitute an explicit
|
||||
# unconfigured value but do not substitute a declared default value.
|
||||
SetUnconfiguredValue = $true
|
||||
SetDefaultValue = $false
|
||||
SkipNotConfigured = $false
|
||||
SkipDefaultValues = $false
|
||||
SkipDisabled = $true
|
||||
NotConfiguredText = 'notConfigured'
|
||||
ValueOutputProperty = 'value'
|
||||
SourceTenantUnavailable = $false
|
||||
# When true, output providers skip the document-info header they emit
|
||||
# at the top of a Full document (Organization / Generated by / Generated
|
||||
# date). Generic because every provider writes the same block; read via
|
||||
# Get-DocumentationOption so all providers honour it consistently.
|
||||
SkipDocumentInfo = $false
|
||||
# A policy type that no handler and no input provider claims is
|
||||
# documented by the generic fallback - basic info plus one row per
|
||||
# property - instead of producing a page with nothing on it. On by
|
||||
# default: a rough table beats a blank page, and the alternative
|
||||
# (Enrollment Notifications, Windows Hello for Business and friends
|
||||
# documenting to nothing) reads as a bug to everyone who hits it.
|
||||
# Off means those types are logged and skipped entirely, not emitted
|
||||
# as an empty document. Overridden per run via -Options, and per
|
||||
# user by the Output Settings checkbox, which saves to this key.
|
||||
FallbackDocumentation = $true
|
||||
Outputs = @{}
|
||||
}
|
||||
}
|
||||
|
||||
# Reset per-object accumulators so the same context instance can be reused
|
||||
# across objects in a bulk run (cheaper than constructing a new one).
|
||||
[void] ResetForObject([object]$PolicyObject) {
|
||||
$this.PolicyObject = $PolicyObject
|
||||
$this.CurrentObject = if ($PolicyObject -and $PolicyObject.PSObject.Properties['JsonObject']) { $PolicyObject.JsonObject } else { $PolicyObject }
|
||||
$this.BasicInfo.Clear()
|
||||
$this.SettingsData.Clear()
|
||||
$this.ComplianceActions.Clear()
|
||||
$this.ApplicabilityRules.Clear()
|
||||
$this.Assignments.Clear()
|
||||
$this.Scripts.Clear()
|
||||
$this.CustomTables.Clear()
|
||||
# ADMXCategories deliberately not cleared — batch-scoped cache (matches old $script:admxCategories)
|
||||
$this.DefaultDocumentationProperties = @('Name','Value')
|
||||
$this.DisplayProperties = @()
|
||||
$this.ErrorText = $null
|
||||
$this.InputType = $null
|
||||
$this.UpdateFilteredObject = $false
|
||||
$this.UnconfiguredProperties = @()
|
||||
$this.CurrentSubCategory = $null
|
||||
$this.PropLevel = 0
|
||||
$this.DocumentName = $null
|
||||
$this.MergeBasicInfo = $false
|
||||
}
|
||||
|
||||
# ---- Add* methods (used by input providers / handlers) ----
|
||||
|
||||
[void] AddBasic([string]$Name, [object]$Value) {
|
||||
$this.AddBasic($Name, $Value, $null)
|
||||
}
|
||||
|
||||
# Preferred overload — pass the source field name (e.g. 'displayName',
|
||||
# 'state', 'createdDateTime') as EntityKey. Compare logic and other
|
||||
# downstream tools key rows by EntityKey rather than the localized Name.
|
||||
[void] AddBasic([string]$Name, [object]$Value, [string]$EntityKey) {
|
||||
$this.BasicInfo.Add([PSCustomObject]@{
|
||||
Name = $Name; Value = $Value; EntityKey = $EntityKey
|
||||
})
|
||||
}
|
||||
|
||||
[void] AddProperty([string]$Name, [object]$Value) {
|
||||
$this.SettingsData.Add([PSCustomObject]@{ Name = $Name; Value = $Value })
|
||||
}
|
||||
|
||||
[void] AddProperty([string]$Name, [object]$Value, [string]$Category) {
|
||||
$this.SettingsData.Add([PSCustomObject]@{ Name = $Name; Value = $Value; Category = $Category })
|
||||
}
|
||||
|
||||
[void] AddProperty([string]$Name, [object]$Value, [string]$Category, [string]$SubCategory) {
|
||||
$this.SettingsData.Add([PSCustomObject]@{ Name = $Name; Value = $Value; Category = $Category; SubCategory = $SubCategory })
|
||||
}
|
||||
|
||||
[void] AddSetting([object]$Setting) {
|
||||
# Every settings row must carry a stable, language-independent
|
||||
# EntityKey — it is the join key for documentation-based compare (and
|
||||
# any other downstream tooling). Manifest/Profile/handler rows set it
|
||||
# themselves; Settings Catalog / Intent walker rows carry the identity
|
||||
# as SettingId (+ ParentSettingId / RowIndex), so derive it here:
|
||||
# [<ParentSettingId>/]<SettingId>[#<RowIndex>]
|
||||
# RowIndex > 0 disambiguates repeated group-collection rows (e.g.
|
||||
# firewall rule lists) — positional matching, same as the old project.
|
||||
if ($Setting -and (-not $Setting.PSObject.Properties['EntityKey'] -or
|
||||
[string]::IsNullOrEmpty([string]$Setting.EntityKey))) {
|
||||
$key = $null
|
||||
if ($Setting.PSObject.Properties['SettingId'] -and $Setting.SettingId) {
|
||||
$key = [string]$Setting.SettingId
|
||||
if ($Setting.PSObject.Properties['ParentSettingId'] -and $Setting.ParentSettingId) {
|
||||
$key = "$($Setting.ParentSettingId)/$key"
|
||||
}
|
||||
if ($Setting.PSObject.Properties['RowIndex'] -and [int]$Setting.RowIndex -gt 0) {
|
||||
$key = "$key#$($Setting.RowIndex)"
|
||||
}
|
||||
}
|
||||
if ($key) {
|
||||
$Setting | Add-Member -MemberType NoteProperty -Name 'EntityKey' -Value $key -Force
|
||||
}
|
||||
else {
|
||||
Write-LogDebug "Documentation row without EntityKey: '$($Setting.Name)' ($($this.InputType))"
|
||||
}
|
||||
}
|
||||
$this.SettingsData.Add($Setting)
|
||||
}
|
||||
|
||||
[void] AddComplianceAction([string]$Action, [string]$Schedule, [string]$MessageTemplate, [string]$EmailCC) {
|
||||
$this.ComplianceActions.Add([PSCustomObject]@{
|
||||
Action = $Action
|
||||
Schedule = $Schedule
|
||||
MessageTemplate = $MessageTemplate
|
||||
EmailCC = $EmailCC
|
||||
})
|
||||
}
|
||||
|
||||
[void] AddApplicabilityRule([string]$Rule, [string]$Property, [object]$Value) {
|
||||
$this.ApplicabilityRules.Add([PSCustomObject]@{
|
||||
Rule = $Rule; Property = $Property; Value = $Value
|
||||
})
|
||||
}
|
||||
|
||||
[void] AddAssignment([object]$Assignment) {
|
||||
$this.Assignments.Add($Assignment)
|
||||
}
|
||||
|
||||
[void] AddCustomTable([object]$Table) {
|
||||
$this.CustomTables.Add($Table)
|
||||
}
|
||||
|
||||
# AddScript honors Options.IncludeScripts so output providers don't need to gate
|
||||
# on $global:chkIncludeScripts anymore.
|
||||
[void] AddScript([string]$Header, [string]$Caption, [string]$Content) {
|
||||
if (-not $this.Options.IncludeScripts) { return }
|
||||
if (-not $Content) { return }
|
||||
if ($this.Options.ExcludeScriptSignature) {
|
||||
$Content = [regex]::Replace(
|
||||
$Content,
|
||||
'(?ms)^\s*# SIG # Begin signature block.*?# SIG # End signature block\s*$',
|
||||
''
|
||||
).TrimEnd()
|
||||
}
|
||||
$this.Scripts.Add([PSCustomObject]@{
|
||||
Header = $Header
|
||||
Caption = $Caption
|
||||
ScriptContent = $Content
|
||||
})
|
||||
}
|
||||
|
||||
# Produce the per-object result PSCustomObject that output providers consume.
|
||||
# Output-provider contract: this is the exact set of fields outputs may read.
|
||||
[PSCustomObject] ToResult() {
|
||||
$updateNotConfigured = $true
|
||||
$notConfiguredLoc = Get-LanguageString 'SettingDetails.notConfigured'
|
||||
$notConfiguredText = ''
|
||||
if($this.Options.NotConfiguredText -eq 'notConfigured') {
|
||||
$notConfiguredText = $notConfiguredLoc
|
||||
}
|
||||
elseif($this.Options.NotConfiguredText -eq 'asis') {
|
||||
$updateNotConfigured = $false
|
||||
}
|
||||
|
||||
$settings = @($this.SettingsData | Where-Object {
|
||||
if((-not ($_.PSObject.Properties | Where-Object Name -eq "RawValue")) -or
|
||||
($_.AlwaysAddValue -eq $true))
|
||||
{ return $true }
|
||||
|
||||
if ($this.Options.SkipDisabled -and $_.Enabled -is [bool] -and -not $_.Enabled) { return $false }
|
||||
if ($this.Options.SkipDefaultValues -and
|
||||
$null -ne $_.RawValue -and
|
||||
(($null -ne $_.DefaultValue -and $_.RawValue -eq $_.DefaultValue) -or
|
||||
($null -ne $_.UnconfiguredValue -and $_.RawValue -eq $_.UnconfiguredValue))) { return $false }
|
||||
if ($this.Options.SkipNotConfigured -and
|
||||
(($_.RawValue -isnot [array] -and (
|
||||
$null -eq $_.RawValue -or
|
||||
"$($_.RawValue)" -eq "" -or
|
||||
"$($_.RawValue)" -eq "notConfigured")) -or
|
||||
#($null -ne $_.UnconfiguredValue -and $_.RawValue -eq $_.UnconfiguredValue)) -or
|
||||
($_.RawValue -is [array] -and $_.RawValue.Count -eq 0) -or
|
||||
($this.UnconfiguredProperties | Where-Object EntityKey -eq $_.EntityKey))) { return $false }
|
||||
|
||||
|
||||
if($updateNotConfigured -and (($_.RawValue -isnot [array] -and ($null -eq $_.RawValue -or "$($_.RawValue)" -eq "" -or "$($_.RawValue)" -eq "notConfigured") -and [String]::IsNullOrEmpty($_.Value)) -or ($_.RawValue -is [array] -and ($_.RawValue | Measure-Object).Count -eq 0)))
|
||||
{
|
||||
$_.Value = $notConfiguredText
|
||||
}
|
||||
|
||||
if ($this.Options.SkipNotConfigured -and $_.Value -eq $notConfiguredLoc) {
|
||||
Write-Log "Skipping property $($_.Name) based on '$($notConfiguredLoc)' string value" 2
|
||||
return $false
|
||||
}
|
||||
return $true
|
||||
})
|
||||
|
||||
if ($this.Options.NotConfiguredText -eq 'empty') {
|
||||
foreach ($setting in $settings) {
|
||||
if ($setting.Value -eq 'notConfigured') { $setting.Value = '' }
|
||||
}
|
||||
}
|
||||
|
||||
# Basic-info rows carry only a localized display Value (no RawValue), so
|
||||
# apply SkipNotConfigured the same way settings do at their final check
|
||||
# above: drop any row whose value is a "not configured" string. Handlers
|
||||
# emit that value from more than one language key (SettingDetails.notConfigured
|
||||
# for the settings path, Inputs.notConfigured for basic toggles like
|
||||
# connectedAppsEnabled / credentialProviderRoleState), so match against both
|
||||
# so this stays correct if a locale ever diverges the two.
|
||||
$basicRows = @($this.BasicInfo)
|
||||
if ($this.Options.SkipNotConfigured) {
|
||||
$notConfiguredValues = @(
|
||||
$notConfiguredLoc
|
||||
Get-LanguageString 'Inputs.notConfigured'
|
||||
) | Where-Object { $_ } | Select-Object -Unique
|
||||
$basicRows = @($basicRows | Where-Object {
|
||||
if ($_.Value -in $notConfiguredValues) {
|
||||
Write-Log "Skipping basic property $($_.Name) based on '$($_.Value)' string value" 2
|
||||
return $false
|
||||
}
|
||||
return $true
|
||||
})
|
||||
}
|
||||
|
||||
# One table instead of two: the basic rows lead, then the settings, in the
|
||||
# order a reader meets them on the portal blade. Done here rather than in
|
||||
# each output provider so every format gets the same shape, and done AFTER
|
||||
# the engine's post-steps so the scope-tag and assignment rows they append
|
||||
# to BasicInfo come along too.
|
||||
if ($this.MergeBasicInfo) {
|
||||
$settings = @($basicRows) + @($settings)
|
||||
$basicRows = @()
|
||||
}
|
||||
|
||||
return [PSCustomObject]@{
|
||||
BasicInfo = $basicRows
|
||||
FilteredSettings = $settings
|
||||
DocumentName = $this.DocumentName
|
||||
ComplianceActions = @($this.ComplianceActions)
|
||||
ApplicabilityRules = @($this.ApplicabilityRules)
|
||||
Assignments = @($this.Assignments)
|
||||
Scripts = @($this.Scripts)
|
||||
CustomTables = @($this.CustomTables)
|
||||
DisplayProperties = $this.DisplayProperties
|
||||
DefaultDocumentationProperties = $this.DefaultDocumentationProperties
|
||||
ErrorText = $this.ErrorText
|
||||
InputType = $this.InputType
|
||||
UpdateFilteredObject = $this.UpdateFilteredObject
|
||||
UnconfiguredProperties = $this.UnconfiguredProperties
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user