Public/Get-InforcerAlignmentDetails.ps1
|
<# .SYNOPSIS Retrieves alignment scores or alignment details from the Inforcer API. .DESCRIPTION Without -BaselineId: retrieves alignment score summaries (table or raw). With -BaselineId: retrieves detailed alignment data including metrics, per-policy matches, deviations, diffs, variables, and tags. When -TenantId is also specified, queries a single tenant. When only -BaselineId is specified, queries the first member tenant (baseline policies are identical across members). -BaselineId accepts a GUID or a friendly baseline name (resolved via the baselines API). .PARAMETER Format Table (default) or Raw. .PARAMETER TenantId Optional. Filter to this tenant. When used with -BaselineId, queries only this tenant. .PARAMETER BaselineId Optional. Baseline GUID or friendly name. When provided, retrieves detailed alignment data. Without -TenantId, loops through all member tenants of the baseline. .PARAMETER Tag Optional. When Format is Table (without -BaselineId), filter to tenants with tag containing this value (case-insensitive). .PARAMETER OutputType PowerShellObject (default) or JsonObject. JSON uses Depth 100. .EXAMPLE Get-InforcerAlignmentDetails .EXAMPLE Get-InforcerAlignmentDetails -Format Raw -OutputType JsonObject .EXAMPLE Get-InforcerAlignmentDetails -TenantId 482 -Tag Production .EXAMPLE Get-InforcerAlignmentDetails -TenantId 139 -BaselineId "Provision M365" Retrieves detailed alignment data using the baseline friendly name. .EXAMPLE Get-InforcerAlignmentDetails -TenantId 139 -BaselineId "91e0b0f7-69f1-453f-8d73-5a6f726b5b21" -Format Raw Retrieves raw alignment detail response by baseline GUID. .EXAMPLE Get-InforcerAlignmentDetails -BaselineId "Inforcer Blueprint Baseline - Tier 2 - Enhanced" Retrieves alignment details for all member tenants of the baseline. .OUTPUTS PSObject or String .LINK https://github.com/royklo/InforcerCommunity/blob/main/docs/CMDLET-REFERENCE.md#get-inforceralignmentdetails .LINK Connect-Inforcer #> function Get-InforcerAlignmentDetails { [CmdletBinding()] [OutputType([PSObject], [string])] param( [Parameter(Mandatory = $false)] [ValidateSet('Table', 'Raw')] [string]$Format = 'Table', [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true)] [Alias('ClientTenantId')] [object]$TenantId, [Parameter(Mandatory = $false)] [string]$BaselineId, [Parameter(Mandatory = $false)] [string]$Tag, [Parameter(Mandatory = $false)] [ValidateSet('PowerShellObject', 'JsonObject')] [string]$OutputType = 'PowerShellObject' ) if (-not (Test-InforcerSession)) { Write-Error -Message 'Not connected yet. Please run Connect-Inforcer first.' -ErrorId 'NotConnected' -Category ConnectionError return } # --- Alignment Details mode: -BaselineId provided --- if (-not [string]::IsNullOrWhiteSpace($BaselineId)) { # Resolve baseline (fetch baselines once — reused for member lookup when no TenantId) $allBaselines = $null try { $guidTest = [guid]::Empty if (-not [guid]::TryParse($BaselineId.Trim(), [ref]$guidTest)) { # Name lookup — fetch baselines so we can reuse them for members $allBaselines = @(Invoke-InforcerApiRequest -Endpoint '/beta/baselines' -Method GET -OutputType PowerShellObject) $baselineGuid = Resolve-InforcerBaselineId -BaselineId $BaselineId -BaselineData $allBaselines } else { $baselineGuid = $BaselineId.Trim() } } catch { Write-Error -Message $_.Exception.Message -ErrorId 'InvalidBaselineId' -Category InvalidArgument return } # Build list of tenants to query $tenantsToQuery = [System.Collections.Generic.List[object]]::new() if ($null -ne $TenantId) { # Single tenant mode — resolve ID and friendly name try { $clientTenantId = Resolve-InforcerTenantId -TenantId $TenantId } catch { Write-Error -Message $_.Exception.Message -ErrorId 'InvalidTenantId' -Category InvalidArgument return } $tenantFriendlyName = "Tenant $clientTenantId" $tenantsForName = @(Invoke-InforcerApiRequest -Endpoint '/beta/tenants' -Method GET -OutputType PowerShellObject) foreach ($t in $tenantsForName) { if ($t -is [PSObject]) { $cidProp = $t.PSObject.Properties['clientTenantId'] if ($cidProp -and [int]$cidProp.Value -eq $clientTenantId) { $fnProp = $t.PSObject.Properties['tenantFriendlyName'] if ($fnProp -and $fnProp.Value) { $tenantFriendlyName = $fnProp.Value.ToString() } break } } } [void]$tenantsToQuery.Add(@{ Id = $clientTenantId; Name = $tenantFriendlyName }) } else { # All members mode — find baseline members if ($null -eq $allBaselines) { $allBaselines = @(Invoke-InforcerApiRequest -Endpoint '/beta/baselines' -Method GET -OutputType PowerShellObject) } $baselineObj = $null foreach ($bl in $allBaselines) { if (-not ($bl -is [PSObject])) { continue } $idProp = $bl.PSObject.Properties['id'] if ($idProp -and $null -ne $idProp.Value -and $idProp.Value.ToString() -eq $baselineGuid) { $baselineObj = $bl break } } if ($null -eq $baselineObj) { Write-Error -Message "Baseline not found: $baselineGuid" -ErrorId 'BaselineNotFound' -Category ObjectNotFound return } $membersProp = $baselineObj.PSObject.Properties['members'] if (-not $membersProp -or $null -eq $membersProp.Value -or @($membersProp.Value).Count -eq 0) { Write-Warning 'No member tenants found for this baseline.' return } # Pick first member only — baseline policies are the same regardless of member foreach ($member in @($membersProp.Value)) { if (-not ($member -is [PSObject])) { continue } $mIdProp = $member.PSObject.Properties['clientTenantId'] if (-not $mIdProp -or $null -eq $mIdProp.Value) { continue } $mId = [int]$mIdProp.Value $mNameProp = $member.PSObject.Properties['tenantFriendlyName'] $mName = if ($mNameProp -and $null -ne $mNameProp.Value) { $mNameProp.Value.ToString() } else { "Tenant $mId" } [void]$tenantsToQuery.Add(@{ Id = $mId; Name = $mName }) break } if ($tenantsToQuery.Count -eq 0) { Write-Warning 'No member tenants found for this baseline.' return } Write-Verbose "Using member tenant $($tenantsToQuery[0].Name) ($($tenantsToQuery[0].Id)) to retrieve baseline policies." } # Status map for policy arrays $arrayStatusMap = @{ 'matchedPolicies' = 'Aligned' 'matchedWithAcceptedDeviations' = 'Accepted Deviation' 'deviatedUnaccepted' = 'Unaccepted Deviation' 'missingFromSubjectUnaccepted' = 'Recommended From Baseline' 'additionalInSubjectUnaccepted' = 'Existing Customer Policy' } # Query each tenant foreach ($tenantEntry in $tenantsToQuery) { $tId = $tenantEntry.Id $tName = $tenantEntry.Name Write-Verbose "Retrieving alignment details for tenant $tId ($tName), baseline $baselineGuid..." $detailEndpoint = "/beta/tenants/$tId/alignmentDetails?customBaselineId=$baselineGuid" $response = Invoke-InforcerApiRequest -Endpoint $detailEndpoint -Method GET -OutputType $OutputType -ErrorAction SilentlyContinue -ErrorVariable apiErr if ($null -eq $response) { $errMsg = if ($apiErr -and $apiErr.Count -gt 0) { $apiErr[0].Exception.Message } else { $null } if ($errMsg -match 'permission|forbidden|access') { Write-Warning "No access to alignment details for tenant '$tName' ($tId). You may not have permission to view this baseline's data." } else { Write-Warning "No alignment data returned for tenant '$tName' ($tId)." } continue } if ($OutputType -eq 'JsonObject') { $response continue } if ($Format -eq 'Raw') { if ($response -is [PSObject]) { $null = Add-InforcerPropertyAliases -InputObject $response -ObjectType AlignmentDetail } $response continue } # Format Table: flatten into metrics summary + per-policy rows $alignment = $null $metrics = $null $completedAt = $null if ($response -is [PSObject]) { $alignmentProp = $response.PSObject.Properties['alignment'] if ($alignmentProp -and $null -ne $alignmentProp.Value) { $alignment = $alignmentProp.Value } $metricsProp = $response.PSObject.Properties['metrics'] if ($metricsProp -and $null -ne $metricsProp.Value) { $metrics = $metricsProp.Value } $completedAtProp = $response.PSObject.Properties['completedAt'] if ($completedAtProp -and $null -ne $completedAtProp.Value) { $completedAt = $completedAtProp.Value } } if ($null -eq $alignment) { Write-Warning "No alignment data returned for tenant $tName ($tId)." continue } $alignmentScoreVal = $alignment.PSObject.Properties['alignmentScore'].Value $alignmentScoreFormatted = if ($null -ne $alignmentScoreVal) { [Math]::Round([double]$alignmentScoreVal, 2) } else { $null } $totalPolicies = if ($metrics) { $metrics.PSObject.Properties['totalPolicies'].Value } else { $null } $alignedCount = if ($metrics) { $metrics.PSObject.Properties['matchedPolicies'].Value } else { $null } $acceptedDeviationCount = if ($metrics) { $metrics.PSObject.Properties['matchedWithAcceptedDeviations'].Value } else { $null } $unacceptedDeviationCount = if ($metrics) { $metrics.PSObject.Properties['deviatedPolicies'].Value } else { $null } $recommendedCount = if ($metrics) { $metrics.PSObject.Properties['recommendedPoliciesFromBaseline'].Value } else { $null } $existingCustomerCount = if ($metrics) { $metrics.PSObject.Properties['customerOnlyPolicies'].Value } else { $null } # Display metrics summary via host (not pipeline) so it doesn't break | Format-Table Write-Host "" Write-Host " Tenant: $tName ($tId)" -ForegroundColor Cyan Write-Host " Alignment Score: $alignmentScoreFormatted | Total: $totalPolicies | Aligned: $alignedCount | Accepted Deviation: $acceptedDeviationCount | Unaccepted Deviation: $unacceptedDeviationCount | Recommended: $recommendedCount | Customer Only: $existingCustomerCount" Write-Host " Completed: $completedAt" Write-Host "" # Per-policy rows foreach ($arrayName in $arrayStatusMap.Keys) { $policyArrayProp = $alignment.PSObject.Properties[$arrayName] if (-not $policyArrayProp -or $null -eq $policyArrayProp.Value) { continue } $status = $arrayStatusMap[$arrayName] foreach ($policy in @($policyArrayProp.Value)) { if (-not ($policy -is [PSObject])) { continue } $tagNames = [System.Collections.Generic.List[string]]::new() $policyTagsProp = $policy.PSObject.Properties['policyTags'] if ($policyTagsProp -and $null -ne $policyTagsProp.Value) { foreach ($t in @($policyTagsProp.Value)) { if ($t -is [PSObject] -and $t.PSObject.Properties['name']) { [void]$tagNames.Add($t.PSObject.Properties['name'].Value) } } } $row = [PSCustomObject]@{ PolicyName = $policy.PSObject.Properties['policyName'].Value AlignmentStatus = $status Product = $policy.PSObject.Properties['product'].Value PrimaryGroup = $policy.PSObject.Properties['primaryGroup'].Value SecondaryGroup = $policy.PSObject.Properties['secondaryGroup'].Value InforcerPolicyTypeName = $policy.PSObject.Properties['inforcerPolicyTypeName'].Value Tags = ($tagNames -join ', ') } $row.PSObject.TypeNames.Insert(0, 'InforcerCommunity.AlignmentDetailPolicy') $row } } } return } function FormatAlignmentScore($scoreVal) { if ($null -eq $scoreVal) { return $null } $scoreStr = $scoreVal.ToString().Replace(',', '.') $scoreNumeric = 0.0 if (-not [double]::TryParse($scoreStr, [System.Globalization.NumberStyles]::Float, [System.Globalization.CultureInfo]::InvariantCulture, [ref]$scoreNumeric)) { return $null } $rounded = [Math]::Round($scoreNumeric, 1) if ($rounded -eq [Math]::Floor($rounded)) { return [int][Math]::Floor($rounded).ToString() } return $rounded.ToString('F1', [System.Globalization.CultureInfo]::InvariantCulture).Replace('.', ',') } if ($Format -eq 'Raw') { Write-Verbose 'Retrieving alignment scores (raw)...' $response = Invoke-InforcerApiRequest -Endpoint '/beta/alignmentScores' -Method GET -OutputType $OutputType if ($null -eq $response) { return } if ($null -ne $TenantId) { try { $clientTenantId = Resolve-InforcerTenantId -TenantId $TenantId } catch { Write-Error -Message $_.Exception.Message -ErrorId 'InvalidTenantId' -Category InvalidArgument return } Write-Verbose "Filtering alignment scores to tenant ID $clientTenantId..." $predicate = { param($p) $tidProp = $p.PSObject.Properties['tenantId'] if (-not $tidProp) { $tidProp = $p.PSObject.Properties['clientTenantId'] } if ($tidProp -and [int]$tidProp.Value -eq $clientTenantId) { return $true } $summariesProp = $p.PSObject.Properties['alignmentSummaries'] if ($summariesProp -and $summariesProp.Value -is [object[]]) { foreach ($s in $summariesProp.Value) { if ($s -is [PSObject]) { $aidProp = $s.PSObject.Properties['alignedBaselineTenantId'] if ($aidProp -and [int]$aidProp.Value -eq $clientTenantId) { return $true } } } } $false } if ($OutputType -eq 'JsonObject') { Filter-InforcerResponse -InputObject $response -FilterScript $predicate -OutputType JsonObject } else { $filtered = Filter-InforcerResponse -InputObject $response -FilterScript $predicate -OutputType PowerShellObject foreach ($item in (ConvertTo-InforcerArray $filtered)) { if ($item -is [PSObject]) { $null = Add-InforcerPropertyAliases -InputObject $item -ObjectType AlignmentScore $item.PSObject.TypeNames.Insert(0, 'InforcerCommunity.AlignmentScoreRaw') } } $filtered } return } if ($OutputType -eq 'JsonObject') { return $response } foreach ($item in (ConvertTo-InforcerArray $response)) { if ($item -is [PSObject]) { $null = Add-InforcerPropertyAliases -InputObject $item -ObjectType AlignmentScore $item.PSObject.TypeNames.Insert(0, 'InforcerCommunity.AlignmentScoreRaw') } } $response return } # Format Table: use /beta/alignmentScores for fresh data, optionally join tenant names from /beta/tenants Write-Verbose 'Retrieving alignment scores for table...' $alignmentResponse = Invoke-InforcerApiRequest -Endpoint '/beta/alignmentScores' -Method GET -OutputType PowerShellObject if ($null -eq $alignmentResponse) { return } $allAlignmentData = ConvertTo-InforcerArray $alignmentResponse # Detect flat format before deciding whether we need /beta/tenants $firstItem = $null if ($allAlignmentData.Count -gt 0) { $firstItem = $allAlignmentData[0] } $flatFormat = ($firstItem -is [PSObject]) -and $firstItem.PSObject.Properties['tenantId'] -and $firstItem.PSObject.Properties['score'] -and -not $firstItem.PSObject.Properties['alignmentSummaries'] # Only fetch tenants when needed: nested format, or -TenantId, or -Tag (for baseline-owner expansion or tag filtering) $needTenants = (-not $flatFormat) -or ($null -ne $TenantId) -or -not [string]::IsNullOrWhiteSpace($Tag) $allTenants = @() $tenantLookup = @{} if ($needTenants) { Write-Verbose 'Retrieving tenant information for alignment table...' $tenantResponse = Invoke-InforcerApiRequest -Endpoint '/beta/tenants' -Method GET -OutputType PowerShellObject if ($null -eq $tenantResponse) { return } $allTenants = ConvertTo-InforcerArray $tenantResponse foreach ($t in $allTenants) { if ($t -is [PSObject]) { $null = Add-InforcerPropertyAliases -InputObject $t -ObjectType Tenant $idProp = $t.PSObject.Properties['clientTenantId'] if ($null -ne $idProp -and $null -ne $idProp.Value) { $id = [int]$idProp.Value $tenantLookup[$id] = $t } } } } # Filter tenants by TenantId and Tag (same as before) $tenants = @($allTenants | Where-Object { $_ -is [PSObject] }) if ($null -ne $TenantId) { try { $clientTenantId = Resolve-InforcerTenantId -TenantId $TenantId -TenantData $allTenants } catch { Write-Error -Message $_.Exception.Message -ErrorId 'InvalidTenantId' -Category InvalidArgument return } Write-Verbose "Filtering to tenant ID: $clientTenantId" $tenants = @($tenants | Where-Object { $idProp = $_.PSObject.Properties['clientTenantId'] $idProp -and [int]$idProp.Value -eq $clientTenantId }) # If no tenant with this ID exists in the system, return nothing. if ($tenants.Count -eq 0) { Write-Verbose "No tenant found with ID $clientTenantId. Returning no results." return } } if (-not [string]::IsNullOrWhiteSpace($Tag)) { Write-Verbose "Filtering to tenants with tag containing: $Tag" $tenants = @($tenants | Where-Object { $tagsProp = $_.PSObject.Properties['tags'] if (-not $tagsProp -or $null -eq $tagsProp.Value) { return $false } $val = $tagsProp.Value if ($val -is [object[]]) { foreach ($x in $val) { if ($x -and $x.ToString().IndexOf($Tag, [System.StringComparison]::OrdinalIgnoreCase) -ge 0) { return $true } } } else { if ($val.ToString().IndexOf($Tag, [System.StringComparison]::OrdinalIgnoreCase) -ge 0) { return $true } } $false }) } Write-Verbose "Building alignment table from alignment scores ($($allAlignmentData.Count) source(s))..." # Helper: add to $targetIds any tenant that is aligned to a baseline owned by an ID in $baselineOwnerIds function Add-ChildTenantIdsFromAlignments { param([System.Collections.Hashtable]$targetIds, [array]$allTenants, [array]$baselineOwnerIds) foreach ($t in $allTenants) { if (-not ($t -is [PSObject])) { continue } $sumProp = $t.PSObject.Properties['alignmentSummaries'] if (-not $sumProp -or $null -eq $sumProp.Value) { continue } $sums = $sumProp.Value if ($sums -isnot [object[]]) { $sums = @($sums) } foreach ($s in $sums) { if (-not ($s -is [PSObject])) { continue } $abtProp = $s.PSObject.Properties['alignedBaselineTenantId'] if ($abtProp -and $null -ne $abtProp.Value) { $abt = 0 if ([int]::TryParse($abtProp.Value.ToString(), [ref]$abt) -and ($baselineOwnerIds -contains $abt)) { $childId = 0 $childProp = $t.PSObject.Properties['clientTenantId'] if ($childProp -and [int]::TryParse($childProp.Value.ToString(), [ref]$childId)) { $targetIds[$childId] = $true } } } } } } # Only filter by tenant when user explicitly passed -TenantId or -Tag; otherwise show all alignment rows $tenantIds = @{} if ($null -ne $TenantId -or -not [string]::IsNullOrWhiteSpace($Tag)) { foreach ($t in $tenants) { $idProp = $t.PSObject.Properties['clientTenantId'] if ($idProp -and $null -ne $idProp.Value) { $tid = 0 if ([int]::TryParse($idProp.Value.ToString(), [ref]$tid)) { $tenantIds[$tid] = $true } } } # If the requested tenant is a baseline owner, also include tenants aligned TO it if ($null -ne $TenantId -and $tenantIds.Count -gt 0) { $baselineOwnerIds = @($tenantIds.Keys) Add-ChildTenantIdsFromAlignments -targetIds $tenantIds -allTenants $allTenants -baselineOwnerIds $baselineOwnerIds } } # API can return flat array (tenantId, score, baselineGroupName...) or nested (clientTenantId, alignmentSummaries) $alignmentTable = [System.Collections.ArrayList]::new() if ($flatFormat) { foreach ($item in $allAlignmentData) { if (-not ($item -is [PSObject])) { continue } $tidProp = $item.PSObject.Properties['tenantId'] if (-not $tidProp) { $tidProp = $item.PSObject.Properties['clientTenantId'] } $targetTenantClientTenantId = 0 if ($tidProp -and $null -ne $tidProp.Value) { if (-not [int]::TryParse($tidProp.Value.ToString(), [ref]$targetTenantClientTenantId)) { continue } } if ($tenantIds.Count -gt 0 -and -not $tenantIds.ContainsKey($targetTenantClientTenantId)) { continue } $scoreVal = $item.PSObject.Properties['score'].Value $alignmentScoreFormatted = FormatAlignmentScore $scoreVal $row = [PSCustomObject]@{ TargetTenantFriendlyName = ($item.PSObject.Properties['tenantFriendlyName'].Value -as [string]) TargetTenantClientTenantId = $targetTenantClientTenantId AlignmentScore = $alignmentScoreFormatted BaselineName = $item.PSObject.Properties['baselineGroupName'].Value BaselineId = $item.PSObject.Properties['baselineGroupId'].Value LastComparisonDateTime = $item.PSObject.Properties['lastComparisonDateTime'].Value } $row.PSObject.TypeNames.Insert(0, 'InforcerCommunity.AlignmentScore') [void]$alignmentTable.Add($row) } } else { foreach ($tenant in $allAlignmentData) { if (-not ($tenant -is [PSObject])) { continue } $cidProp = $tenant.PSObject.Properties['clientTenantId'] $targetTenantClientTenantId = 0 if ($null -ne $cidProp -and $null -ne $cidProp.Value) { if (-not [int]::TryParse($cidProp.Value.ToString(), [ref]$targetTenantClientTenantId)) { continue } } if ($tenantIds.Count -gt 0 -and -not $tenantIds.ContainsKey($targetTenantClientTenantId)) { continue } $summariesProp = $tenant.PSObject.Properties['alignmentSummaries'] if (-not $summariesProp -or $null -eq $summariesProp.Value) { continue } $summaries = ConvertTo-InforcerArray $summariesProp.Value if ($summaries.Count -eq 0) { continue } $targetTenantFriendlyName = $tenant.PSObject.Properties['tenantFriendlyName'].Value -as [string] if (-not $targetTenantFriendlyName) { $targetTenantFriendlyName = '' } foreach ($alignment in $summaries) { if (-not ($alignment -is [PSObject])) { continue } $scoreProp = $alignment.PSObject.Properties['alignmentScore'] $alignmentScoreFormatted = FormatAlignmentScore $(if ($scoreProp -and $null -ne $scoreProp.Value) { $scoreProp.Value } else { $null }) $row = [PSCustomObject]@{ TargetTenantFriendlyName = $targetTenantFriendlyName TargetTenantClientTenantId = $targetTenantClientTenantId AlignmentScore = $alignmentScoreFormatted BaselineName = $alignment.PSObject.Properties['alignedBaselineName'].Value BaselineId = $alignment.PSObject.Properties['alignedBaselineId'].Value LastComparisonDateTime = $alignment.PSObject.Properties['lastComparisonDateTime'].Value AlignedThreshold = $alignment.PSObject.Properties['alignedThreshold'].Value SemiAlignedThreshold = $alignment.PSObject.Properties['semiAlignedThreshold'].Value } $row.PSObject.TypeNames.Insert(0, 'InforcerCommunity.AlignmentScore') [void]$alignmentTable.Add($row) } } } Write-Verbose "Generated alignment table with $($alignmentTable.Count) row(s)." if ($OutputType -eq 'JsonObject') { $json = $alignmentTable | ConvertTo-Json -Depth 100 Write-Output $json return } $alignmentTable } |