|
| 1 | +using namespace System.Net |
| 2 | + |
| 3 | +function Invoke-ListTenantAlignment { |
| 4 | + <# |
| 5 | + .FUNCTIONALITY |
| 6 | + Entrypoint |
| 7 | + .ROLE |
| 8 | + Tenant.Standards.Read |
| 9 | + #> |
| 10 | + [CmdletBinding()] |
| 11 | + param($Request, $TriggerMetadata) |
| 12 | + |
| 13 | + $APIName = $Request.Params.CIPPEndpoint |
| 14 | + $Headers = $Request.Headers |
| 15 | + |
| 16 | + # Get all standard templates |
| 17 | + $TemplateTable = Get-CippTable -tablename 'templates' |
| 18 | + $TemplateFilter = "PartitionKey eq 'StandardsTemplateV2'" |
| 19 | + $Templates = (Get-CIPPAzDataTableEntity @TemplateTable -Filter $TemplateFilter) | ForEach-Object { |
| 20 | + $JSON = $_.JSON -replace '"Action":', '"action":' |
| 21 | + try { |
| 22 | + $RowKey = $_.RowKey |
| 23 | + $Data = $JSON | ConvertFrom-Json -Depth 100 -ErrorAction SilentlyContinue |
| 24 | + } catch { |
| 25 | + Write-Host "$($RowKey) standard could not be loaded: $($_.Exception.Message)" |
| 26 | + return |
| 27 | + } |
| 28 | + if ($Data) { |
| 29 | + $Data | Add-Member -NotePropertyName 'GUID' -NotePropertyValue $_.GUID -Force |
| 30 | + $Data |
| 31 | + } |
| 32 | + } |
| 33 | + |
| 34 | + # Get standards comparison data using the same pattern as ListStandardsCompare |
| 35 | + $StandardsTable = Get-CIPPTable -TableName 'CippStandardsReports' |
| 36 | + $Standards = Get-CIPPAzDataTableEntity @StandardsTable |
| 37 | + |
| 38 | + # Build tenant standards data structure like in ListStandardsCompare |
| 39 | + $TenantStandards = @{} |
| 40 | + foreach ($Standard in $Standards) { |
| 41 | + $FieldName = $Standard.RowKey |
| 42 | + $FieldValue = $Standard.Value |
| 43 | + $Tenant = $Standard.PartitionKey |
| 44 | + |
| 45 | + # Process field value like in ListStandardsCompare |
| 46 | + if ($FieldValue -is [System.Boolean]) { |
| 47 | + $FieldValue = [bool]$FieldValue |
| 48 | + } elseif ($FieldValue -like '*{*') { |
| 49 | + $FieldValue = ConvertFrom-Json -InputObject $FieldValue -ErrorAction SilentlyContinue |
| 50 | + } else { |
| 51 | + $FieldValue = [string]$FieldValue |
| 52 | + } |
| 53 | + |
| 54 | + if (-not $TenantStandards.ContainsKey($Tenant)) { |
| 55 | + $TenantStandards[$Tenant] = @{} |
| 56 | + } |
| 57 | + $TenantStandards[$Tenant][$FieldName] = @{ |
| 58 | + Value = $FieldValue |
| 59 | + LastRefresh = $Standard.TimeStamp.ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ') |
| 60 | + } |
| 61 | + } |
| 62 | + |
| 63 | + $Results = [System.Collections.Generic.List[object]]::new() |
| 64 | + |
| 65 | + # Process each template against all tenants |
| 66 | + foreach ($Template in $Templates) { |
| 67 | + $TemplateStandards = $Template.standards |
| 68 | + if (-not $TemplateStandards) { |
| 69 | + continue |
| 70 | + } |
| 71 | + |
| 72 | + # Check if template has tenant assignments (scope) |
| 73 | + $TemplateAssignedTenants = @() |
| 74 | + $AppliestoAllTenants = $false |
| 75 | + |
| 76 | + if ($Template.tenantFilter -and $Template.tenantFilter.Count -gt 0) { |
| 77 | + # Extract tenant values from the tenantFilter array |
| 78 | + $TenantValues = $Template.tenantFilter | ForEach-Object { $_.value } |
| 79 | + |
| 80 | + if ($TenantValues -contains 'AllTenants') { |
| 81 | + $AppliestoAllTenants = $true |
| 82 | + Write-Host "Template '$($Template.templateName)' applies to all tenants (AllTenants)" |
| 83 | + } else { |
| 84 | + $TemplateAssignedTenants = $TenantValues |
| 85 | + Write-Host "Template '$($Template.templateName)' is assigned to specific tenants: $($TemplateAssignedTenants -join ', ')" |
| 86 | + } |
| 87 | + } else { |
| 88 | + $AppliestoAllTenants = $true |
| 89 | + Write-Host "Template '$($Template.templateName)' applies to all tenants (no tenantFilter)" |
| 90 | + } |
| 91 | + |
| 92 | + $AllStandards = [System.Collections.Generic.List[string]]::new() |
| 93 | + $ReportingEnabledStandards = [System.Collections.Generic.List[string]]::new() |
| 94 | + $ReportingDisabledStandards = [System.Collections.Generic.List[string]]::new() |
| 95 | + |
| 96 | + foreach ($StandardKey in $TemplateStandards.PSObject.Properties.Name) { |
| 97 | + $StandardConfig = $TemplateStandards.$StandardKey |
| 98 | + $StandardId = "standards.$StandardKey" |
| 99 | + |
| 100 | + |
| 101 | + $Actions = @() |
| 102 | + if ($StandardConfig.action) { |
| 103 | + $Actions = $StandardConfig.action |
| 104 | + } elseif ($StandardConfig.Action) { |
| 105 | + $Actions = $StandardConfig.Action |
| 106 | + } elseif ($StandardConfig.PSObject.Properties['action']) { |
| 107 | + $Actions = $StandardConfig.PSObject.Properties['action'].Value |
| 108 | + } |
| 109 | + |
| 110 | + $ReportingEnabled = $false |
| 111 | + if ($Actions -and $Actions.Count -gt 0) { |
| 112 | + $ReportingEnabled = ($Actions | Where-Object { $_.value -and ($_.value.ToLower() -eq 'report' -or $_.value.ToLower() -eq 'remediate') }).Count -gt 0 |
| 113 | + } |
| 114 | + |
| 115 | + $AllStandards.Add($StandardId) |
| 116 | + |
| 117 | + if ($ReportingEnabled) { |
| 118 | + $ReportingEnabledStandards.Add($StandardId) |
| 119 | + } else { |
| 120 | + $ReportingDisabledStandards.Add($StandardId) |
| 121 | + } |
| 122 | + |
| 123 | + if ($StandardKey -eq 'IntuneTemplate' -and $StandardConfig -is [array]) { |
| 124 | + $AllStandards.Remove($StandardId) |
| 125 | + if ($ReportingEnabled) { |
| 126 | + $ReportingEnabledStandards.Remove($StandardId) |
| 127 | + } else { |
| 128 | + $ReportingDisabledStandards.Remove($StandardId) |
| 129 | + } |
| 130 | + |
| 131 | + foreach ($IntuneTemplate in $StandardConfig) { |
| 132 | + if ($IntuneTemplate.TemplateList.value) { |
| 133 | + $IntuneStandardId = "standards.IntuneTemplate.$($IntuneTemplate.TemplateList.value)" |
| 134 | + |
| 135 | + $IntuneActions = if ($IntuneTemplate.action) { $IntuneTemplate.action } else { @() } |
| 136 | + $IntuneReportingEnabled = ($IntuneActions | Where-Object { $_.value -and ($_.value.ToLower() -eq 'report' -or $_.value.ToLower() -eq 'remediate') }).Count -gt 0 |
| 137 | + |
| 138 | + $AllStandards.Add($IntuneStandardId) |
| 139 | + |
| 140 | + if ($IntuneReportingEnabled) { |
| 141 | + $ReportingEnabledStandards.Add($IntuneStandardId) |
| 142 | + } else { |
| 143 | + $ReportingDisabledStandards.Add($IntuneStandardId) |
| 144 | + } |
| 145 | + } |
| 146 | + } |
| 147 | + } |
| 148 | + } |
| 149 | + |
| 150 | + foreach ($TenantName in $TenantStandards.Keys) { |
| 151 | + if (-not $AppliestoAllTenants -and $TenantName -notin $TemplateAssignedTenants) { |
| 152 | + Write-Host "Skipping tenant '$TenantName' for template '$($Template.templateName)' - not in assigned tenant list" |
| 153 | + continue |
| 154 | + } |
| 155 | + $AllCount = $AllStandards.Count |
| 156 | + |
| 157 | + $CompliantStandards = 0 |
| 158 | + $NonCompliantStandards = 0 |
| 159 | + $ReportingDisabledStandardsCount = 0 |
| 160 | + $LatestDataCollection = $null |
| 161 | + $ComparisonTable = @() |
| 162 | + |
| 163 | + foreach ($StandardKey in $AllStandards) { |
| 164 | + $IsReportingDisabled = $ReportingDisabledStandards -contains $StandardKey |
| 165 | + |
| 166 | + if ($TenantStandards[$TenantName].ContainsKey($StandardKey)) { |
| 167 | + $StandardObject = $TenantStandards[$TenantName][$StandardKey] |
| 168 | + $Value = $StandardObject.Value |
| 169 | + |
| 170 | + if ($StandardObject.LastRefresh) { |
| 171 | + $RefreshTime = [DateTime]::Parse($StandardObject.LastRefresh) |
| 172 | + if (-not $LatestDataCollection -or $RefreshTime -gt $LatestDataCollection) { |
| 173 | + $LatestDataCollection = $RefreshTime |
| 174 | + } |
| 175 | + } |
| 176 | + |
| 177 | + $IsCompliant = ($Value -eq $true) |
| 178 | + |
| 179 | + if ($IsReportingDisabled) { |
| 180 | + $ReportingDisabledStandardsCount++ |
| 181 | + $ComplianceStatus = 'Reporting Disabled' |
| 182 | + } elseif ($IsCompliant) { |
| 183 | + $CompliantStandards++ |
| 184 | + $ComplianceStatus = 'Compliant' |
| 185 | + } else { |
| 186 | + $NonCompliantStandards++ |
| 187 | + $ComplianceStatus = 'Non-Compliant' |
| 188 | + } |
| 189 | + |
| 190 | + $ComparisonTable += [PSCustomObject]@{ |
| 191 | + StandardName = $StandardKey |
| 192 | + Compliant = $IsCompliant |
| 193 | + StandardValue = ($Value | ConvertTo-Json -Compress) |
| 194 | + ComplianceStatus = $ComplianceStatus |
| 195 | + ReportingDisabled = $IsReportingDisabled |
| 196 | + } |
| 197 | + } else { |
| 198 | + if ($IsReportingDisabled) { |
| 199 | + $ReportingDisabledStandardsCount++ |
| 200 | + $ComplianceStatus = 'Reporting Disabled' |
| 201 | + } else { |
| 202 | + $NonCompliantStandards++ |
| 203 | + $ComplianceStatus = 'Non-Compliant' |
| 204 | + } |
| 205 | + |
| 206 | + $ComparisonTable += [PSCustomObject]@{ |
| 207 | + StandardName = $StandardKey |
| 208 | + Compliant = $false |
| 209 | + StandardValue = 'NOT FOUND' |
| 210 | + ComplianceStatus = $ComplianceStatus |
| 211 | + ReportingDisabled = $IsReportingDisabled |
| 212 | + } |
| 213 | + } |
| 214 | + } |
| 215 | + |
| 216 | + |
| 217 | + $AlignmentPercentage = if (($AllCount - $ReportingDisabledStandardsCount) -gt 0) { |
| 218 | + [Math]::Round(($CompliantStandards / ($AllCount - $ReportingDisabledStandardsCount)) * 100) |
| 219 | + } else { |
| 220 | + 0 |
| 221 | + } |
| 222 | + |
| 223 | + $TenantOnlyStandards = $TenantStandards[$TenantName].Keys | Where-Object { $_ -notin $AllStandards } |
| 224 | + |
| 225 | + $Result = [PSCustomObject]@{ |
| 226 | + tenantFilter = $TenantName |
| 227 | + standardName = $Template.templateName |
| 228 | + standardId = $Template.GUID |
| 229 | + alignmentScore = $AlignmentPercentage |
| 230 | + latestDataCollection = if ($LatestDataCollection) { $LatestDataCollection } else { $null } |
| 231 | + } |
| 232 | + |
| 233 | + $Results.Add($Result) |
| 234 | + } |
| 235 | + } |
| 236 | + |
| 237 | + Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ |
| 238 | + StatusCode = [HttpStatusCode]::OK |
| 239 | + Body = @($Results) |
| 240 | + }) |
| 241 | +} |
0 commit comments