|
| 1 | +function Invoke-CIPPStandardAppManagementPolicy { |
| 2 | + <# |
| 3 | + .FUNCTIONALITY |
| 4 | + Internal |
| 5 | + .COMPONENT |
| 6 | + (APIName) AppManagementPolicy |
| 7 | + .SYNOPSIS |
| 8 | + (Label) Set Default App Management Policy |
| 9 | + .DESCRIPTION |
| 10 | + (Helptext) Configures the default app management policy to control application and service principal credential restrictions such as password and key credential lifetimes. |
| 11 | + (DocsDescription) Configures the default app management policy to control application and service principal credential restrictions. This includes password addition restrictions, custom password addition, symmetric key addition, and credential lifetime limits for both applications and service principals. |
| 12 | + .NOTES |
| 13 | + CAT |
| 14 | + Entra (AAD) Standards |
| 15 | + TAG |
| 16 | + EXECUTIVETEXT |
| 17 | + Enforces credential restrictions on application registrations and service principals to limit how secrets and certificates are created and how long they remain valid. This reduces the risk of long-lived or unmanaged credentials being used to access your tenant. |
| 18 | + ADDEDCOMPONENT |
| 19 | + {"type":"autoComplete","multiple":false,"creatable":false,"label":"Password Addition","name":"standards.AppManagementPolicy.passwordCredentialsPasswordAddition","options":[{"label":"Enabled","value":"enabled"},{"label":"Disabled","value":"disabled"}]} |
| 20 | + {"type":"autoComplete","multiple":false,"creatable":false,"label":"Custom Password","name":"standards.AppManagementPolicy.passwordCredentialsCustomPasswordAddition","options":[{"label":"Enabled","value":"enabled"},{"label":"Disabled","value":"disabled"}]} |
| 21 | + {"type":"number","label":"Password Credentials Max Lifetime (Days)","name":"standards.AppManagementPolicy.passwordCredentialsMaxLifetime"} |
| 22 | + {"type":"number","label":"Key Credentials Max Lifetime (Days)","name":"standards.AppManagementPolicy.keyCredentialsMaxLifetime"} |
| 23 | + IMPACT |
| 24 | + Medium Impact |
| 25 | + ADDEDDATE |
| 26 | + 2026-03-13 |
| 27 | + POWERSHELLEQUIVALENT |
| 28 | + Graph API |
| 29 | + RECOMMENDEDBY |
| 30 | + UPDATECOMMENTBLOCK |
| 31 | + Run the Tools\Update-StandardsComments.ps1 script to update this comment block |
| 32 | + .LINK |
| 33 | + https://docs.cipp.app/user-documentation/tenant/standards/list-standards |
| 34 | + #> |
| 35 | + |
| 36 | + param($Tenant, $Settings) |
| 37 | + |
| 38 | + # Get current app management policy |
| 39 | + try { |
| 40 | + $CurrentPolicy = New-GraphGetRequest -Uri 'https://graph.microsoft.com/v1.0/policies/defaultAppManagementPolicy' -tenantid $Tenant -AsApp $true |
| 41 | + } catch { |
| 42 | + $ErrorMessage = Get-CippException -Exception $_ |
| 43 | + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to get App Management Policy. Error: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage |
| 44 | + return |
| 45 | + } |
| 46 | + |
| 47 | + # Unwrap autoComplete values - frontend sends {label, value} objects, extract the string |
| 48 | + $passwordAdditionState = [string]$Settings.passwordCredentialsPasswordAddition.value |
| 49 | + $customPasswordState = [string]$Settings.passwordCredentialsCustomPasswordAddition.value |
| 50 | + $passwordMaxLifetimeDays = $Settings.passwordCredentialsMaxLifetime |
| 51 | + $keyMaxLifetimeDays = $Settings.keyCredentialsMaxLifetime |
| 52 | + |
| 53 | + # Convert user-entered days to ISO 8601 duration format (P<n>D) |
| 54 | + $passwordMaxLifetimeISO = if (-not [string]::IsNullOrWhiteSpace($passwordMaxLifetimeDays)) { "P${passwordMaxLifetimeDays}D" } else { $null } |
| 55 | + $keyMaxLifetimeISO = if (-not [string]::IsNullOrWhiteSpace($keyMaxLifetimeDays)) { "P${keyMaxLifetimeDays}D" } else { $null } |
| 56 | + |
| 57 | + # Build desired password credential restrictions |
| 58 | + $desiredPasswordCredentials = [System.Collections.Generic.List[object]]::new() |
| 59 | + |
| 60 | + # Password addition + symmetric key addition (mirrors password addition) |
| 61 | + if (-not [string]::IsNullOrWhiteSpace($passwordAdditionState)) { |
| 62 | + $desiredPasswordCredentials.Add([ordered]@{ |
| 63 | + restrictionType = 'passwordAddition' |
| 64 | + state = $passwordAdditionState |
| 65 | + maxLifetime = $null |
| 66 | + restrictForAppsCreatedAfterDateTime = '0001-01-01T00:00:00Z' |
| 67 | + }) |
| 68 | + $desiredPasswordCredentials.Add([ordered]@{ |
| 69 | + restrictionType = 'symmetricKeyAddition' |
| 70 | + state = $passwordAdditionState |
| 71 | + maxLifetime = $null |
| 72 | + restrictForAppsCreatedAfterDateTime = '0001-01-01T00:00:00Z' |
| 73 | + }) |
| 74 | + } |
| 75 | + |
| 76 | + # Custom password |
| 77 | + if (-not [string]::IsNullOrWhiteSpace($customPasswordState)) { |
| 78 | + $desiredPasswordCredentials.Add([ordered]@{ |
| 79 | + restrictionType = 'customPasswordAddition' |
| 80 | + state = $customPasswordState |
| 81 | + maxLifetime = $null |
| 82 | + restrictForAppsCreatedAfterDateTime = '0001-01-01T00:00:00Z' |
| 83 | + }) |
| 84 | + } |
| 85 | + |
| 86 | + # Password credential max lifetime |
| 87 | + if ($passwordMaxLifetimeISO) { |
| 88 | + $desiredPasswordCredentials.Add([ordered]@{ |
| 89 | + restrictionType = 'passwordLifetime' |
| 90 | + state = 'enabled' |
| 91 | + maxLifetime = $passwordMaxLifetimeISO |
| 92 | + restrictForAppsCreatedAfterDateTime = '0001-01-01T00:00:00Z' |
| 93 | + }) |
| 94 | + } |
| 95 | + |
| 96 | + # Symmetric key credential max lifetime |
| 97 | + if ($keyMaxLifetimeISO) { |
| 98 | + $desiredPasswordCredentials.Add([ordered]@{ |
| 99 | + restrictionType = 'symmetricKeyLifetime' |
| 100 | + state = 'enabled' |
| 101 | + maxLifetime = $keyMaxLifetimeISO |
| 102 | + restrictForAppsCreatedAfterDateTime = '0001-01-01T00:00:00Z' |
| 103 | + }) |
| 104 | + } |
| 105 | + |
| 106 | + # Key credentials (asymmetric key lifetime) |
| 107 | + $desiredKeyCredentials = @( |
| 108 | + if ($keyMaxLifetimeISO) { |
| 109 | + [ordered]@{ |
| 110 | + restrictionType = 'asymmetricKeyLifetime' |
| 111 | + state = 'enabled' |
| 112 | + maxLifetime = $keyMaxLifetimeISO |
| 113 | + restrictForAppsCreatedAfterDateTime = '0001-01-01T00:00:00Z' |
| 114 | + } |
| 115 | + } |
| 116 | + ) |
| 117 | + |
| 118 | + if ($desiredPasswordCredentials.Count -eq 0 -and $desiredKeyCredentials.Count -eq 0) { |
| 119 | + Write-LogMessage -API 'Standards' -Tenant $Tenant -Message 'AppManagementPolicy: No valid restriction settings were configured.' -Sev Info |
| 120 | + return |
| 121 | + } |
| 122 | + |
| 123 | + # Sort desired restrictions by restrictionType so order matches Graph API responses |
| 124 | + # Use script block because items are hashtables, not PSCustomObjects |
| 125 | + $sortedDesiredPasswordCredentials = @($desiredPasswordCredentials | Sort-Object { $_.restrictionType }) |
| 126 | + $sortedDesiredKeyCredentials = @($desiredKeyCredentials | Sort-Object { $_.restrictionType }) |
| 127 | + |
| 128 | + $desiredState = [PSCustomObject]@{ |
| 129 | + isEnabled = $true |
| 130 | + applicationRestrictions = [PSCustomObject]@{ |
| 131 | + passwordCredentials = $sortedDesiredPasswordCredentials |
| 132 | + keyCredentials = $sortedDesiredKeyCredentials |
| 133 | + } |
| 134 | + servicePrincipalRestrictions = [PSCustomObject]@{ |
| 135 | + passwordCredentials = $sortedDesiredPasswordCredentials |
| 136 | + keyCredentials = $sortedDesiredKeyCredentials |
| 137 | + } |
| 138 | + } |
| 139 | + |
| 140 | + # Sort current policy arrays the same way for consistent comparison |
| 141 | + $CurrentValue = [PSCustomObject]@{ |
| 142 | + isEnabled = [bool]$CurrentPolicy.isEnabled |
| 143 | + applicationRestrictions = [PSCustomObject]@{ |
| 144 | + passwordCredentials = @($CurrentPolicy.applicationRestrictions.passwordCredentials | Sort-Object -Property restrictionType) |
| 145 | + keyCredentials = @($CurrentPolicy.applicationRestrictions.keyCredentials | Sort-Object -Property restrictionType) |
| 146 | + } |
| 147 | + servicePrincipalRestrictions = [PSCustomObject]@{ |
| 148 | + passwordCredentials = @($CurrentPolicy.servicePrincipalRestrictions.passwordCredentials | Sort-Object -Property restrictionType) |
| 149 | + keyCredentials = @($CurrentPolicy.servicePrincipalRestrictions.keyCredentials | Sort-Object -Property restrictionType) |
| 150 | + } |
| 151 | + } |
| 152 | + |
| 153 | + $CurrentJson = $CurrentValue | ConvertTo-Json -Depth 10 -Compress |
| 154 | + $ExpectedJson = $desiredState | ConvertTo-Json -Depth 10 -Compress |
| 155 | + $StateIsCorrect = $CurrentJson -eq $ExpectedJson |
| 156 | + |
| 157 | + if ($Settings.remediate -eq $true) { |
| 158 | + if ($StateIsCorrect -eq $true) { |
| 159 | + Write-LogMessage -API 'Standards' -Tenant $Tenant -Message 'App Management Policy is already in the desired state.' -Sev Info |
| 160 | + } else { |
| 161 | + try { |
| 162 | + $GraphRequest = @{ |
| 163 | + tenantID = $Tenant |
| 164 | + uri = 'https://graph.microsoft.com/v1.0/policies/defaultAppManagementPolicy' |
| 165 | + AsApp = $true |
| 166 | + Type = 'PATCH' |
| 167 | + ContentType = 'application/json; charset=utf-8' |
| 168 | + Body = $desiredState | ConvertTo-Json -Depth 20 -Compress |
| 169 | + } |
| 170 | + |
| 171 | + $null = New-GraphPostRequest @GraphRequest |
| 172 | + Write-LogMessage -API 'Standards' -Tenant $Tenant -Message 'Updated default app management policy.' -Sev Info |
| 173 | + } catch { |
| 174 | + $ErrorMessage = Get-CippException -Exception $_ |
| 175 | + Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Failed to update default app management policy. Error: $($ErrorMessage.NormalizedError)" -Sev Error -LogData $ErrorMessage |
| 176 | + } |
| 177 | + } |
| 178 | + } |
| 179 | + |
| 180 | + if ($Settings.alert -eq $true) { |
| 181 | + if ($StateIsCorrect -eq $true) { |
| 182 | + Write-LogMessage -API 'Standards' -Tenant $Tenant -Message 'App Management Policy is configured correctly.' -Sev Info |
| 183 | + } else { |
| 184 | + Write-StandardsAlert -message 'App Management Policy is not configured correctly.' -object $CurrentValue -tenant $Tenant -standardName 'AppManagementPolicy' -standardId $Settings.standardId |
| 185 | + Write-LogMessage -API 'Standards' -Tenant $Tenant -Message 'App Management Policy is not configured correctly.' -Sev Info |
| 186 | + } |
| 187 | + } |
| 188 | + |
| 189 | + if ($Settings.report -eq $true) { |
| 190 | + Set-CIPPStandardsCompareField -FieldName 'standards.AppManagementPolicy' -CurrentValue $CurrentValue -ExpectedValue $desiredState -TenantFilter $Tenant |
| 191 | + Add-CIPPBPAField -FieldName 'AppManagementPolicy' -FieldValue $StateIsCorrect -StoreAs bool -Tenant $Tenant |
| 192 | + } |
| 193 | +} |
0 commit comments