Files
IntuneManagement/Internal/Documentation/InputProviders/DocumentationInputIntent.ps1
T

516 lines
25 KiB
PowerShell

# Intent input provider — deviceManagementIntent (Endpoint Security baselines
# and templates).
#
# Ported from old Extensions/Documentation.psm1:1580 (Invoke-TranslateIntent-
# Object + helpers). Claims @odata.type='#microsoft.graph.deviceManagementIntent'.
#
# Intent settings live under /deviceManagement/templates/{templateId}/categories
# (with $expand=settingDefinitions) and the per-intent values come from
# /deviceManagement/intents/{intentId}/categories/{catId}/settings. Each setting
# may be Simple / Collection / Complex / AbstractComplex with recursive children
# and dependency constraints that hide settings whose parents aren't configured.
#
# Live Graph dependencies (resolved via Invoke-MSGraphAPI):
# /deviceManagement/templates/{tid}/categories?$expand=settingDefinitions
# /deviceManagement/intents/{iid}/categories/{cid}/settings?$expand=...
# /deviceManagement/templates/{tid}/categories/{cid}/RecommendedSettings
#
# Batch-cached on the [DocumentationContext] ($ctx.IntentCategories,
# $ctx.IntentCatRecommendedSettings) so a bulk run of N intents against the
# same template only pays the round-trips once.
function Invoke-InitializeIntentInput {
Add-DocumentationInputProvider ([PSCustomObject]@{
Name = 'Intent'
Order = 40
Match = { param($PolicyObject) $PolicyObject.JsonObject.'@odata.type' -eq '#microsoft.graph.deviceManagementIntent' }
Translate = { param($PolicyObject, $Context) Invoke-TranslateIntentObject $PolicyObject $Context }
})
}
function Invoke-TranslateIntentObject {
param($PolicyObject, [DocumentationContext]$Context)
$Context.DefaultDocumentationProperties = @('Name','Value','RecommendedValue')
$obj = $PolicyObject.JsonObject
Add-BasicDefaultValues $PolicyObject
$baseLineTemplates = Get-CacheObject "BaseLineTemplates"
if(-not $baseLineTemplates)
{
$baseLineTemplates = (Invoke-MSGraphAPI -Url "/deviceManagement/templates").Value
Set-CacheObject "BaseLineTemplates" $baseLineTemplates -Persistent
}
$baseLineTemplate = $baseLineTemplates | Where-Object Id -eq $obj.templateId
if(-not $baseLineTemplate)
{
Write-Log "Could not find Baseline Template with Id $($obj.templateId)" 3
}
else {
$platformType = Get-LanguageString "Platform.$($baseLineTemplate.platformType)"
if($platformType) { Add-BasicPropertyValue (Get-LanguageString "SettingDetails.platformSupported") $platformType 'platformSupported'}
if ($baseLineTemplate.templateSubtype -eq "none")
{
$templateCategoory = $baseLineTemplate.templateType
} else {
$templateCategoory = $baseLineTemplate.templateSubtype
}
Add-BasicPropertyValue (Get-LanguageString "TableHeaders.Category") (Get-IntentCategoryFromTemplateType $templateCategoory) "basicCategory"
Add-BasicPropertyValue (Get-LanguageString "TableHeaders.policyType") $baseLineTemplate.displayName "basicPolicyType"
}
Add-BasicAdditionalValues $PolicyObject
if (-not $obj.templateId) {
Write-Log "Intent: no templateId on '$($obj.displayName)' - cannot translate settings" 2
return
}
# Built-in ES template schema is generic. Seed/share the session-persistent
# caches by reference so per-templateId/per-category writes below warm the
# cache automatically and survive across runs (and tenant switches).
$Context.IntentCategories = Get-CacheObject "DocIntentCategories" $Context.IntentCategories
Set-CacheObject "DocIntentCategories" $Context.IntentCategories -Persistent
$Context.IntentCatRecommendedSettings = Get-CacheObject "DocIntentRecommendedSettings" $Context.IntentCatRecommendedSettings
Set-CacheObject "DocIntentRecommendedSettings" $Context.IntentCatRecommendedSettings -Persistent
# --- Template categories (batch-cached per templateId) ---
$categories = $Context.IntentCategories[$obj.templateId]
if (-not $categories) {
# Built-in Endpoint Security template schema (by templateId) is generic -
# same on every tenant - so resolved from any connected tenant, even when
# the source tenant of the export is gone.
if (-not (Test-DocumentationGraphAvailable)) {
Write-Log "Intent: no tenant connected and no cached template categories for $($obj.templateId) - settings will not render" 2
return
}
try {
$headers = @{}
if ($Context.Language -and $Context.Language -ne 'en') { $headers['Accept-Language'] = $Context.Language }
$resp = Invoke-MSGraphAPI -Url "/deviceManagement/templates/$($obj.templateId)/categories?`$expand=settingDefinitions" -AdditionalHeaders $headers
$categories = @($resp.Value)
$Context.IntentCategories[$obj.templateId] = $categories
}
catch {
Write-LogError "Intent: failed to fetch template categories for $($obj.templateId)" $_.Exception
return
}
}
# Per-object setting buffer (drained at the end into Context.SettingsData
# in dependency-respecting order via Add-IntentSettingObjectToList).
$script:_intentObjectSettings = [System.Collections.Generic.List[object]]::new()
$script:_intentEmittedIds = @{}
foreach ($category in ($categories | Sort-Object -Property displayName)) {
# Per-intent settings for this category (skipped when the input is an
# offline file with .settings inlined).
$settings = $null
if ($obj.'@ObjectFromFile' -eq $true) {
$settings = $obj.settings
}
elseif (-not $Context.SourceTenantUnavailable -and (Test-DocumentationGraphAvailable)) {
# Source-tenant-specific: this intent's configured values by id (404s elsewhere).
# Export path is the @ObjectFromFile branch above.
try {
$headers = @{}
if ($Context.Language -and $Context.Language -ne 'en') { $headers['Accept-Language'] = $Context.Language }
$resp = Invoke-MSGraphAPI -Url "/deviceManagement/intents/$($obj.Id)/categories/$($category.Id)/settings?`$expand=Microsoft.Graph.DeviceManagementComplexSettingInstance/Value" -AdditionalHeaders $headers
$settings = $resp.Value
}
catch {
Write-LogError "Intent: failed to fetch settings for intent=$($obj.Id) category=$($category.Id)" $_.Exception
continue
}
}
if (-not $settings) { continue }
# Recommended settings (template-level, also batch-cached per categoryId)
if (-not $Context.IntentCatRecommendedSettings.ContainsKey($category.Id)) {
# Template-level recommended settings (by templateId) are generic schema.
if (Test-DocumentationGraphAvailable) {
try {
$headers = @{}
if ($Context.Language -and $Context.Language -ne 'en') { $headers['Accept-Language'] = $Context.Language }
$resp = Invoke-MSGraphAPI -Url "/deviceManagement/templates/$($obj.templateId)/categories/$($category.Id)/RecommendedSettings" -AdditionalHeaders $headers
$Context.IntentCatRecommendedSettings[$category.Id] = @($resp.Value)
}
catch {
Write-LogError "Intent: failed to fetch recommended settings for template=$($obj.templateId) category=$($category.Id)" $_.Exception
$Context.IntentCatRecommendedSettings[$category.Id] = @()
}
}
else {
$Context.IntentCatRecommendedSettings[$category.Id] = @()
}
}
foreach ($settingObj in $settings) {
Get-IntentSettingInfo $settingObj $category $settingObj.definitionId $settings $Context | Out-Null
}
}
# Drain top-level settings (those with no parent and no dependencies).
# Children/dependents get visited recursively by Add-IntentSettingObjectToList.
$tops = $script:_intentObjectSettings | Where-Object {
$null -eq $_.ParentId -and (($_.Dependencies | Measure-Object).Count -eq 0)
}
foreach ($s in $tops) {
Add-IntentSettingObjectToList $s $Context
}
}
# Ordered emit: respects dependency constraints (parents resolve to permitted
# values before dependent children are added) and recurses to children of any
# emitted setting.
function Add-IntentSettingObjectToList {
param($objSetting, [DocumentationContext]$Context)
if ($script:_intentEmittedIds.ContainsKey([string]$objSetting.Id)) { return }
$passConstraint = $true
$hasConstraint = $false
foreach ($dependencyObj in $objSetting.SettingDefinition.dependencies) {
$dependencyItemObj = $script:_intentObjectSettings | Where-Object { $_.SettingDefinition.Id -eq $dependencyObj.definitionId } | Select-Object -First 1
if ($dependencyObj.constraints.Count -gt 0) {
$hasConstraint = $true
foreach ($constraint in $dependencyObj.constraints) {
switch ($constraint.'@odata.type') {
'#microsoft.graph.deviceManagementSettingBooleanConstraint' {
if (($null -eq $dependencyItemObj.RawValue -and $constraint.value -eq $false) -or
($dependencyItemObj.RawValue -and "$($dependencyItemObj.RawValue)" -ne "$($constraint.value)")) {
$passConstraint = $false
}
}
'#microsoft.graph.deviceManagementEnumConstraint' {
if (-not ($constraint.values | Where-Object Value -EQ $dependencyItemObj.RawValue)) {
$passConstraint = $false
}
}
'#microsoft.graph.deviceManagementSettingIntegerConstraint' {
# Old code inverts the comparison — passes when value is OUT of range.
# Preserving the (buggy?) behavior for golden parity.
if ($dependencyItemObj.RawValue -ge $constraint.minimumValue -and
$dependencyItemObj.RawValue -le $constraint.maximumValue) {
$passConstraint = $false
}
}
}
if (-not $passConstraint) { break }
}
}
else {
# No explicit constraint — dependency just has to be "set"
$passConstraint = ($null -ne $dependencyItemObj.RawValue -and
"$($dependencyItemObj.RawValue)" -ne 'NotConfigured' -and
"$($dependencyItemObj.RawValue)" -ne 'False')
}
if (-not $passConstraint) { break }
}
if (-not $passConstraint) { return }
if ($hasConstraint) { $objSetting.Level = $objSetting.Level + 1 }
# Attach recommended-value comparison (purely informational on the emitted row)
$recommendedSetting = $Context.IntentCatRecommendedSettings[$objSetting.CategoryObject.Id] |
Where-Object definitionId -EQ $objSetting.SettingId | Select-Object -First 1
if ($recommendedSetting.valueJson -and ($objSetting.ValueSet -eq $false -or
$recommendedSetting.valueJson -ne ($objSetting.RawValue | ConvertTo-Json -Depth 50 -Compress))) {
$objSetting | Add-Member -MemberType NoteProperty -Name 'RecommendedValue' `
-Value ($recommendedSetting.valueJson | ConvertFrom-Json) -Force
}
$Context.AddSetting($objSetting)
$script:_intentEmittedIds[[string]$objSetting.Id] = $true
if ($objSetting.ValueSet -eq $false) { return }
# Recurse: dependents (settings whose dependencies include this one)
foreach ($depObj in ($script:_intentObjectSettings | Where-Object {
$_.Dependencies.definitionId -eq $objSetting.SettingDefinition.Id
})) {
Add-IntentSettingObjectToList $depObj $Context
}
# Recurse: children (settings with ParentId pointing at this one and no deps)
foreach ($depObj in ($script:_intentObjectSettings | Where-Object {
$_.ParentId -eq $objSetting.Id -and (($_.Dependencies | Measure-Object).Count -eq 0)
})) {
Add-IntentSettingObjectToList $depObj $Context
}
}
# Recursive setting parser. Builds a per-setting PSCustomObject with all the
# metadata the emit step needs, pushes it onto $script:_intentObjectSettings,
# and recurses into Complex / AbstractComplex / Collection children.
function Get-IntentSettingInfo {
param(
$valueObj, $category, $defId, $allSettings, [DocumentationContext]$Context,
[switch]$SkipConvertValue, [switch]$PassThru, $parentDef = $null
)
$defObj = $category.settingDefinitions | Where-Object id -EQ $defId | Select-Object -First 1
if (-not $defObj) { return }
$itemValue = $null
$itemFullValue = $null
$rawValue = if ($SkipConvertValue) { $valueObj } else { $valueObj.valueJson | ConvertFrom-Json }
$valueSet = Get-IsIntentObjectConfigured $rawValue
if ($valueSet -eq $false) {
# Skip child settings
}
elseif ($valueObj.'@odata.type' -eq '#microsoft.graph.deviceManagementCollectionSettingInstance' -or
$defObj.'@odata.type' -eq '#microsoft.graph.deviceManagementComplexSettingDefinition' -or
$defObj.valueType -eq 'collection') {
$valueArr = @()
$elementDefObj = if ($defObj.elementDefinitionId) {
$category.settingDefinitions | Where-Object id -EQ $defObj.elementDefinitionId | Select-Object -First 1
} else { $defObj }
if ($elementDefObj.propertyDefinitionIds) {
# Each element is itself a record of N properties — emit the
# FullValueTable so output providers can render it as a table.
$itemFullValue = @()
foreach ($tmpValue in $rawValue) {
$htFullPropInfo = [ordered]@{}
$arrValue = ''
foreach ($propertyDefinitionId in $elementDefObj.propertyDefinitionIds) {
$propDefObj = $category.settingDefinitions | Where-Object id -EQ $propertyDefinitionId | Select-Object -First 1
if ($propDefObj.elementDefinitionId) {
$propDefObj = $category.settingDefinitions | Where-Object id -EQ $propDefObj.elementDefinitionId | Select-Object -First 1
}
if ($arrValue) { $arrValue = $arrValue + $Context.PropertySeparator }
$propName = $propertyDefinitionId.Split('_')[-1]
$propValue = @()
foreach ($childTmpValue in $tmpValue.$propName) {
$propValue += Get-IntentObjectValue $propDefObj $childTmpValue
}
$colName = if ($propDefObj.displayName) { $propDefObj.displayName } else { $propName }
$htFullPropInfo.Add($colName, $tmpValue.$propName)
$arrValue = $arrValue + ($propValue -join $Context.PropertySeparator)
}
$itemFullValue += [PSCustomObject]$htFullPropInfo
$valueArr += $arrValue
}
}
elseif ($rawValue) {
foreach ($tmpValue in $rawValue) {
$valueArr += (Get-IntentObjectValue $elementDefObj $tmpValue)
}
}
if ($valueArr.Count -gt 0) {
$itemValue = $valueArr -join $Context.ObjectSeparator
}
$valueSet = $valueArr.Count -gt 0
}
elseif ($valueObj.'@odata.type' -eq '#microsoft.graph.deviceManagementAbstractComplexSettingInstance' -or
$defObj.'@odata.type' -eq '#microsoft.graph.deviceManagementAbstractComplexSettingDefinition') {
$tmpDef = $category.settingDefinitions | Where-Object {
$_.id -eq $rawValue.implementationId -or $_.id -eq $rawValue.'$implementationId'
} | Select-Object -First 1
if ($tmpDef) {
$itemValue = $tmpDef.displayName
}
else {
$valueSet = $false
}
}
else {
$itemValue = Get-IntentObjectValue $defObj $rawValue
if (-not $itemValue) { $valueSet = $false }
}
if ($valueSet -eq $false) {
$itemValue = Get-LanguageString 'SettingDetails.notConfigured'
$rawValue = $null
}
elseif (-not $itemValue) {
$itemValue = $rawValue
}
$curObjectInfo = [PSCustomObject]@{
Name = $defObj.displayName
Description = $defObj.description
Category = $category.displayName
CategoryDescription = $category.description
CategoryObject = $category
Value = $itemValue
FullValueTable = $itemFullValue
RawValue = $rawValue
SettingDefinition = $defObj
Dependencies = $defObj.dependencies
ValueSet = $valueSet
Id = [Guid]::NewGuid()
ParentId = $null
SettingId = $defObj.Id
ParentSettingId = $parentDef.Id
Level = 0
}
$script:_intentObjectSettings.Add($curObjectInfo)
if ($valueSet -eq $false) {
# Skip children if value not set
}
elseif ($valueObj.'@odata.type' -eq '#microsoft.graph.deviceManagementComplexSettingInstance' -or
$defObj.'@odata.type' -eq '#microsoft.graph.deviceManagementComplexSettingDefinition') {
if ($valueObj.Value) {
$isValueSet = $false
if ($defObj.propertyDefinitionIds) {
foreach ($childDefId in $defObj.propertyDefinitionIds) {
$childSetting = $valueObj.Value | Where-Object DefinitionId -EQ $childDefId | Select-Object -First 1
if ($childSetting) {
$objValueInfo = Get-IntentSettingInfo $childSetting $category $childSetting.definitionId $allSettings $Context -PassThru -parentDef $defObj
$objValueInfo.ParentId = $curObjectInfo.Id
if (($objValueInfo.RawValue -is [bool] -and $objValueInfo.RawValue -eq $true) -or
($objValueInfo.RawValue -is [string] -and -not [string]::IsNullOrEmpty($objValueInfo.RawValue) -and
$objValueInfo.RawValue -ne 'notConfigured' -and -not [string]::IsNullOrEmpty($objValueInfo.Value)) -or
($objValueInfo.RawValue -isnot [bool] -and $objValueInfo.RawValue -isnot [string])) {
$isValueSet = $true
}
}
}
}
else {
foreach ($childSetting in $valueObj.Value) {
$objValueInfo = Get-IntentSettingInfo $childSetting $category $childSetting.definitionId $allSettings $Context -PassThru -parentDef $defObj
$objValueInfo.ParentId = $curObjectInfo.Id
}
$isValueSet = $true
}
}
elseif ($rawValue -and $defObj.propertyDefinitionIds) {
$isValueSet = $false
$isDefault = $true
foreach ($childDefId in $defObj.propertyDefinitionIds) {
$propName = $childDefId.Split('_')[-1]
$objValueInfo = Get-IntentSettingInfo $rawValue.$propName $category $childDefId $allSettings $Context -SkipConvertValue -PassThru -parentDef $defObj
if ($objValueInfo.ValueSet -eq $true) { $isValueSet = $true }
if ($objValueInfo.SettingDefinition.constraints -and
$objValueInfo.SettingDefinition.constraints[0].'@odata.type' -eq '#microsoft.graph.deviceManagementEnumConstraint' -and
($objValueInfo.SettingDefinition.constraints[0].values | Measure-Object).Count -gt 0) {
if ($objValueInfo.SettingDefinition.constraints[0].values[0].value -ne $rawValue.$propName) {
$isDefault = $false
}
}
elseif ($objValueInfo.SettingDefinition.valueType -eq 'string') {
if ($null -ne $rawValue.$propName) { $isDefault = $false }
}
elseif ($objValueInfo.SettingDefinition.valueType -eq 'boolean') {
if ($false -ne $rawValue.$propName) { $isDefault = $false }
}
$objValueInfo.ParentId = $curObjectInfo.Id
}
if ($isDefault) { $isValueSet = $false }
}
else {
$isValueSet = $false
}
$curObjectInfo.Value = if ($isValueSet) { 'Configure' } else { Get-LanguageString 'SettingDetails.notConfigured' }
$curObjectInfo.ValueSet = $isValueSet
$curObjectInfo.FullValueTable = $null
}
elseif (($valueObj.'@odata.type' -eq '#microsoft.graph.deviceManagementAbstractComplexSettingInstance' -or
$defObj.'@odata.type' -eq '#microsoft.graph.deviceManagementAbstractComplexSettingDefinition') -and
$rawValue -and $tmpDef) {
foreach ($childDefId in $tmpDef.propertyDefinitionIds) {
$propName = $childDefId.Split('_')[-1]
$objValueInfo = Get-IntentSettingInfo $rawValue.$propName $category $childDefId $allSettings $Context -SkipConvertValue -PassThru -parentDef $defObj
$objValueInfo.ParentId = $curObjectInfo.Id
}
}
if ($PassThru) { $curObjectInfo }
}
# Translates a raw setting value via its definition (enum / boolean / raw passthrough).
function Get-IntentObjectValue {
param($defObj, $rawValue)
if ($defObj.constraints.'@odata.type' -eq '#microsoft.graph.deviceManagementEnumConstraint') {
$tmpOption = $defObj.constraints.Values | Where-Object value -EQ $rawValue | Select-Object -First 1
if (-not $tmpOption -and $null -eq $rawValue) {
# No defaultValue on the setting definition — fall back to first option.
# Old-code wart preserved for golden parity.
$tmpOption = $defObj.constraints.Values[0]
}
return $tmpOption.displayName
}
elseif ($defObj.valueType -eq 'boolean') {
if ($rawValue -eq 'True') { return (Get-LanguageString 'SettingDetails.yes') }
return $null
}
return $rawValue
}
# Hook for custom "is configured?" rules. Old code always returns true; kept as
# a function so type-specific overrides can be wired in later.
function Get-IsIntentObjectConfigured {
param($obj)
return $true
}
# Template-type to friendly category-name lookup. Used by BasicInfo "Type"
# row when the input provider lands templateType resolution in v2; for now
# only exported so handlers can reuse the mapping.
function Get-IntentCategoryFromTemplateType {
param([string]$TemplateType)
if (-not $TemplateType) {
Write-Log 'Get-IntentCategoryFromTemplateType called with empty TemplateType' 2
return $null
}
# Captured before the prefix is stripped: whether the family was security-shaped
# is what decides if failing to map it is worth reporting (see the default arm).
$isSecurityFamily = $TemplateType.StartsWith('endpointSecurity') -or $TemplateType -match 'baseline'
if ($TemplateType.StartsWith('endpointSecurity')) {
$TemplateType = $TemplateType.Substring(16)
}
switch ($TemplateType) {
'accountProtection' { return (Get-LanguageString 'SecurityTemplate.accountProtection') }
'antivirus' { return (Get-LanguageString 'SecurityTemplate.antivirus') }
'diskEncryption' { return (Get-LanguageString 'SecurityTemplate.diskEncryption') }
'endpointDetectionReponse' { return (Get-LanguageString 'SecurityTemplate.eDR') }
'attackSurfaceReduction' { return (Get-LanguageString 'SecurityTemplate.aSR') }
'firewall' { return (Get-LanguageString 'SecurityTemplate.firewall') }
{ $_ -in @('securityBaseline','baseline','advancedThreatProtectionSecurityBaseline','microsoftEdgeSecurityBaseline') } {
return (Get-LanguageString 'Titles.securityBaselines')
}
# Not a security template, but it reaches this mapper the same way: the
# Settings Catalog provider asks for a category name for every family it
# documents, and the Apple ADE enrollment policies are this one. Without an
# arm here the row read 'enrollmentConfiguration'. PolicySet.deviceEnrollment
# is an existing key, so the label localizes with everything else.
'enrollmentConfiguration' { return (Get-LanguageString 'PolicySet.deviceEnrollment') }
default {
# Only a security-shaped family is expected to resolve here. The Settings
# Catalog provider (Get-IntentCategoryName) calls this for EVERY
# templateFamily and documents the raw value when it does not map, so a
# family like 'enrollmentConfiguration' is a normal outcome rather than a
# problem - warning about it once per policy put a wall of yellow in the
# log of any tenant with Apple ADE policies and buried the real signal.
if ($isSecurityFamily) {
Write-Log "Could not translate Intent Template type $TemplateType" 2
}
else {
Write-LogDebug "No Intent category mapping for template family '$TemplateType'; documented as-is"
}
return $TemplateType
}
}
}
Invoke-InitializeIntentInput