Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
612f2f8
Update Get-CIPPStandards.ps1
chris-dewey-1991 Jun 20, 2026
5549d62
Table cleanup fixes
Zacgoose Jun 23, 2026
eac806c
IP lookup and Audit Log Processing improvements
Zacgoose Jun 24, 2026
f690b6d
More reliable RG name resolution
Zacgoose Jun 24, 2026
739d378
90 Day cache cleanup
Zacgoose Jun 24, 2026
b560d15
Move APISpec to config folder
Zacgoose Jun 24, 2026
850f6a2
New Licence Updates
Zacgoose Jun 24, 2026
4d112f5
User Offboarding task validation
Zacgoose Jun 24, 2026
05f5f1f
alert fixes for custom tests
Zacgoose Jun 24, 2026
0c3d4ba
Custom Test Alerting overhaul
Zacgoose Jun 24, 2026
a8143b4
feat(defender): add MTD role, realign iOS sync
kris6673 Jun 25, 2026
63e0b12
Feat/Fix: Add Mobile Threat Defense role and realign iOS sync (#2108)
Zacgoose Jun 25, 2026
7dea7f4
Requirements clarification
Zacgoose Jun 25, 2026
ac8f243
Language and Region fixes
Zacgoose Jun 25, 2026
6c62d78
Blank value fixes
Zacgoose Jun 25, 2026
dc8b3af
Update Set-CIPPStandardsCompareField.ps1
Zacgoose Jun 25, 2026
e21df9c
Update New-ExoRequest.ps1
Zacgoose Jun 25, 2026
158e973
Update Set-CIPPSAMAdminRoles.ps1
Zacgoose Jun 25, 2026
e6c0c93
Update Set-CIPPSAMAdminRoles.ps1
Zacgoose Jun 25, 2026
67856c4
Update New-ExoRequest.ps1
Zacgoose Jun 25, 2026
fe87252
Revert "Update New-ExoRequest.ps1"
Zacgoose Jun 26, 2026
ad7e409
Revert "Update Set-CIPPSAMAdminRoles.ps1"
Zacgoose Jun 26, 2026
b6b918e
permission repair improvements
Zacgoose Jun 26, 2026
e987ea4
Move NinjaOne Sync from BPA to Reporting DB
Zacgoose Jun 26, 2026
04b3e12
Disable App insights page when not needed
Zacgoose Jun 26, 2026
9deb458
Update Get-NinjaOneFieldMapping.ps1
Zacgoose Jun 26, 2026
428ebd3
Update Set-CIPPDBCacheRiskDetections.ps1
Zacgoose Jun 26, 2026
45b96d6
Fix contact template deployment to align with frontend changes
Zacgoose Jun 26, 2026
d2e88c5
Start job earlier
Zacgoose Jun 26, 2026
3204da3
Update Get-CIPPStandards.ps1 to fix Custom Quarantine Policy not repo…
Zacgoose Jun 26, 2026
6192717
Fixes for spam filter policy when using custom names
Zacgoose Jun 26, 2026
a9b3e40
Purview DLP Policy and Standard implementation and fixes
Zacgoose Jun 26, 2026
40ca72e
chore: update version to 10.5.5
JohnDuprey Jun 26, 2026
a511657
Dev to hotfix (#2113)
JohnDuprey Jun 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Config/CIPPTimers.json
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,7 @@
"Id": "5e8a9b4c-2d6f-4a3e-b7c1-9d0e5f3a8b2c",
"Command": "Start-IntuneReportExportOrchestrator",
"Description": "Submit Intune report-export jobs ahead of nightly DB cache run",
"Cron": "0 0 2 * * *",
"Cron": "0 0 1 * * *",
"Priority": 22,
"RunOnProcessor": true,
"TZOffset": true,
Expand Down
123 changes: 123 additions & 0 deletions Config/ConversionTable.csv

Large diffs are not rendered by default.

15 changes: 15 additions & 0 deletions Config/FeatureFlags.json
Original file line number Diff line number Diff line change
Expand Up @@ -81,5 +81,20 @@
],
"Pages": [],
"Hidden": false
},
{
"Id": "AppInsights",
"Name": "App Insights",
"Description": "App Insights page not used in NG",
"Enabled": true,
"AllowUserToggle": false,
"Timers": [],
"Endpoints": [
"ExecAppInsightsQuery"
],
"Pages": [
"/cipp/advanced/diagnostics"
],
"Hidden": true
}
]
File renamed without changes.
38 changes: 23 additions & 15 deletions Config/standards.json
Original file line number Diff line number Diff line change
Expand Up @@ -378,8 +378,8 @@
"cat": "Global Standards",
"tag": ["CIS M365 7.0.0 (1.3.6)", "CustomerLockBoxEnabled"],
"appliesToTest": ["CIS_1_3_6"],
"helpText": "**Requires Entra ID P2.** Enables Customer Lockbox that offers an approval process for Microsoft support to access organization data",
"docsDescription": "**Requires Entra ID P2.** Customer Lockbox ensures that Microsoft can't access your content to do service operations without your explicit approval. Customer Lockbox ensures only authorized requests allow access to your organizations data.",
"helpText": "**Requires CustomerLockbox (E5, E7, A5, Purview Addon for BP, EDU or FL)** Enables Customer Lockbox that offers an approval process for Microsoft support to access organization data",
"docsDescription": "**Requires CustomerLockbox (E5, E7, A5, Purview Addon for BP, EDU or FL)** Customer Lockbox ensures that Microsoft can't access your content to do service operations without your explicit approval. Customer Lockbox ensures only authorized requests allow access to your organizations data.",
"executiveText": "Requires explicit organizational approval before Microsoft support staff can access company data for service operations. This provides an additional layer of data protection and ensures the organization maintains control over who can access sensitive business information, even during technical support scenarios.",
"addedComponent": [],
"label": "Enable Customer Lockbox",
Expand Down Expand Up @@ -4157,12 +4157,10 @@
"defaultValue": false
},
{
"type": "autoComplete",
"multiple": true,
"creatable": true,
"type": "LanguageCodeMultiSelect",
"required": false,
"name": "standards.SpamFilterPolicy.LanguageBlockList",
"label": "Languages to block (uppercase ISO 639-1 two-letter)",
"label": "Languages to block (ISO 639-1 two-letter)",
"condition": {
"field": "standards.SpamFilterPolicy.EnableLanguageBlockList",
"compareType": "is",
Expand All @@ -4176,12 +4174,10 @@
"defaultValue": false
},
{
"type": "autoComplete",
"multiple": true,
"creatable": true,
"type": "CountryCodeMultiSelect",
"required": false,
"name": "standards.SpamFilterPolicy.RegionBlockList",
"label": "Regions to block (uppercase ISO 3166-1 two-letter)",
"label": "Regions to block (ISO 3166-1 two-letter)",
"condition": {
"field": "standards.SpamFilterPolicy.EnableRegionBlockList",
"compareType": "is",
Expand Down Expand Up @@ -5865,7 +5861,7 @@
{
"type": "switch",
"name": "standards.TeamsFederationConfiguration.AllowTeamsConsumer",
"label": "Allow users to communicate with other organizations"
"label": "Allow users to communicate with consumer Teams accounts"
},
{
"type": "autoComplete",
Expand Down Expand Up @@ -7255,6 +7251,12 @@
"label": "Block Android if partner data unavailable",
"defaultValue": false
},
{
"type": "switch",
"name": "standards.DefenderCompliancePolicy.grantMobileThreatDefensePartnerRole",
"label": "Grant MTD role to MDE on enrolled Android COBO/COPE devices",
"defaultValue": false
},
{
"type": "switch",
"name": "standards.DefenderCompliancePolicy.ConnectIos",
Expand All @@ -7264,13 +7266,19 @@
{
"type": "switch",
"name": "standards.DefenderCompliancePolicy.ConnectIosCompliance",
"label": "Connect iOS 13.0+ (App-based MAM)",
"label": "Connect iOS/iPadOS devices for app protection policy evaluation (MAM)",
"defaultValue": false
},
{
"type": "switch",
"name": "standards.DefenderCompliancePolicy.appSync",
"label": "Enable App Sync for iOS",
"label": "Enable App Sync (sending application inventory) for iOS/iPadOS devices",
"defaultValue": false
},
{
"type": "switch",
"name": "standards.DefenderCompliancePolicy.allowPartnerToCollectIosPersonalApplicationMetadata",
"label": "Send full application inventory data on personally-owned iOS/iPadOS devices",
"defaultValue": false
},
{
Expand All @@ -7282,13 +7290,13 @@
{
"type": "switch",
"name": "standards.DefenderCompliancePolicy.allowPartnerToCollectIosCertificateMetadata",
"label": "Collect certificate metadata from iOS",
"label": "Enable Certificate Sync for iOS/iPadOS devices",
"defaultValue": false
},
{
"type": "switch",
"name": "standards.DefenderCompliancePolicy.allowPartnerToCollectIosPersonalCertificateMetadata",
"label": "Collect personal certificate metadata from iOS",
"label": "Send full certificate inventory data on personally-owned iOS/iPadOS devices",
"defaultValue": false
},
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ function Push-TableCleanupTask {
if ($Table) {
Write-Information "Deleting table $($Table.Context.TableName)"
try {
Remove-AzDataTable -Context $Table.Context -Force
Remove-AzDataTable -Context $Table.Context
} catch {
#Write-LogMessage -API 'TableCleanup' -message "Failed to delete table $($Table.Context.TableName)" -sev Error -LogData (Get-CippException -Exception $_)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,21 @@ function New-CIPPAuditLogSearchResultsCache {
Add-CIPPAzDataTableEntity @CacheWebhooksTable -Entity $cacheEntity -Force
}
Write-Information "Successfully cached search ID: $($SearchId) for tenant: $TenantFilter"

try {
$PrefetchIPs = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
foreach ($sr in $searchResults) {
$cip = $sr.auditData.clientip
if (![string]::IsNullOrWhiteSpace($cip)) { $null = $PrefetchIPs.Add(([string]$cip).Trim()) }
}
if ($PrefetchIPs.Count -gt 0) {
$null = Get-CIPPGeoIPLocationBatch -IPs @($PrefetchIPs)
Write-Information "Geo prefetch: warmed cache for $($PrefetchIPs.Count) distinct IP(s) (search $SearchId)"
}
} catch {
Write-Information "Geo prefetch during ingestion failed for search ${SearchId}: $($_.Exception.Message)"
}

try {
$FailedDownloadsTable = Get-CippTable -TableName 'FailedAuditLogDownloads'
$failedEntities = Get-CIPPAzDataTableEntity @FailedDownloadsTable -Filter "PartitionKey eq '$TenantFilter' and SearchId eq '$SearchId'"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
function Get-CIPPManagedIdentityResourceId {
<#
.SYNOPSIS
Get the Azure resource ID that the Function App's managed identity belongs to.
.DESCRIPTION
Reads the 'xms_mirid' claim from a managed identity access token. For a system-assigned
identity (which CIPP uses), this claim is the ARM resource ID of the host resource itself
- i.e. the Function App site, including its resource group:

/subscriptions/{sub}/resourcegroups/{rg}/providers/Microsoft.Web/sites/{site}

This is the most reliable in-process source for the site's resource group because it is
present in every managed identity token, requires no extra ARM/Graph call, and - unlike
parsing WEBSITE_OWNER_NAME - always names the site's RG rather than the App Service Plan's
webspace RG.

Note: for a user-assigned identity, xms_mirid points at the userAssignedIdentities resource
instead, which may live in a different RG. Callers that need the site's RG should validate
the returned ID against the expected site (see Get-CIPPFunctionAppResourceGroup).
.PARAMETER ResourceUrl
The Azure resource URL to request the token for. Defaults to Azure Resource Manager.
.EXAMPLE
Get-CIPPManagedIdentityResourceId
Returns e.g. /subscriptions/.../resourcegroups/CIPP-myinstance/providers/Microsoft.Web/sites/cippabcde
#>
[CmdletBinding()]
param(
[Parameter(Mandatory = $false)]
[string]$ResourceUrl = 'https://management.azure.com/'
)

$Token = Get-CIPPAzIdentityToken -ResourceUrl $ResourceUrl
if (-not $Token) {
throw 'Could not acquire a managed identity token to read the xms_mirid claim.'
}

# JWT payload is the second dot-delimited segment, base64url-encoded.
$Parts = $Token.Split('.')
if ($Parts.Count -lt 2) {
throw 'Managed identity token is not a well-formed JWT.'
}

$Payload = $Parts[1].Replace('-', '+').Replace('_', '/')
switch ($Payload.Length % 4) {
2 { $Payload += '==' }
3 { $Payload += '=' }
}

$Claims = [System.Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($Payload)) | ConvertFrom-Json
return $Claims.xms_mirid
}
165 changes: 165 additions & 0 deletions Modules/CIPPCore/Public/Compare-CIPPDlpCompliancePolicy.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
function ConvertTo-CIPPComparableString {
<#
.SYNOPSIS
Produce an order-independent canonical string for a value, for equality comparison.
.DESCRIPTION
Recursively serializes scalars, dictionaries/objects (keys sorted), and arrays (elements sorted)
into a deterministic string. Two values are equal iff their canonical strings match - independent
of property order or array order, which is the right semantics for DLP locations and the set of
sensitive information types (order is not meaningful for matching).
.FUNCTIONALITY
Internal
#>
[CmdletBinding()]
param($Value)

if ($null -eq $Value) { return 'null' }
if ($Value -is [string]) { return '"' + $Value + '"' }
if ($Value -is [bool] -or $Value -is [int] -or $Value -is [long] -or $Value -is [double] -or $Value -is [decimal]) {
return [string]$Value
}
if ($Value -is [System.Collections.IDictionary]) {
$parts = foreach ($k in (@($Value.Keys) | Sort-Object)) { '"' + $k + '":' + (ConvertTo-CIPPComparableString -Value $Value[$k]) }
return '{' + ($parts -join ',') + '}'
}
if ($Value -is [System.Management.Automation.PSCustomObject]) {
$parts = foreach ($p in ($Value.PSObject.Properties | Sort-Object Name)) { '"' + $p.Name + '":' + (ConvertTo-CIPPComparableString -Value $p.Value) }
return '{' + ($parts -join ',') + '}'
}
if ($Value -is [System.Collections.IEnumerable]) {
$items = @(foreach ($item in $Value) { ConvertTo-CIPPComparableString -Value $item }) | Sort-Object
return '[' + ($items -join ',') + ']'
}
return '"' + ([string]$Value) + '"'
}

function ConvertTo-CIPPDlpComparable {
<#
.SYNOPSIS
Normalize a DLP policy source (template or live policy) + its rules into a comparable param map.
.DESCRIPTION
Runs the source through the exact same normalization the deploy path uses - allowlist filtering,
location normalization, sensitive-information-type conversion (which also strips output-only
rulePackId), and IncidentReportContent string->array - so a template and the live policy it was
deployed from collapse to identical structures when nothing has actually drifted.
.PARAMETER PolicySource
The policy-level object (a stored template, or a Get-DlpCompliancePolicy result).
.PARAMETER RuleSource
The rule collection (template RuleParams, or Get-DlpComplianceRule results).
.OUTPUTS
PSCustomObject with Policy (hashtable of normalized policy params) and Rules (ordered map of
rule name -> hashtable of normalized rule params).
.FUNCTIONALITY
Internal
#>
[CmdletBinding()]
param($PolicySource, $RuleSource)

$Fields = Get-CIPPDlpComplianceFieldList

$Policy = Format-CIPPCompliancePolicyParams -Source $PolicySource -AllowedFields $Fields.Policy -LocationFields $Fields.Location
$Policy.Remove('Name') | Out-Null # identity, not a comparable setting
# Mirror deploy: an invalid/transient Mode (e.g. PendingDeletion) is never deployed, so it must not
# register as drift either.
if ($Policy.ContainsKey('Mode') -and $Policy['Mode'] -notin $Fields.ValidPolicyModes) {
$Policy.Remove('Mode') | Out-Null
}

$Rules = [ordered]@{}
foreach ($Rule in @($RuleSource) | Where-Object { $_ }) {
$RuleParams = Format-CIPPCompliancePolicyParams -Source $Rule -AllowedFields $Fields.Rule
$RuleName = [string]$RuleParams['Name']
$RuleParams.Remove('Policy') | Out-Null
$RuleParams.Remove('Name') | Out-Null
foreach ($SitField in @('ContentContainsSensitiveInformation', 'ExceptIfContentContainsSensitiveInformation')) {
if ($RuleParams.ContainsKey($SitField)) {
$RuleParams[$SitField] = @(ConvertTo-CIPPSensitiveInformationType -SensitiveInformation $RuleParams[$SitField])
}
}
if ($RuleParams.ContainsKey('IncidentReportContent') -and $RuleParams['IncidentReportContent'] -is [string]) {
$RuleParams['IncidentReportContent'] = @($RuleParams['IncidentReportContent'] -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ })
}
if (-not [string]::IsNullOrWhiteSpace($RuleName)) { $Rules[$RuleName] = $RuleParams }
}

return [pscustomobject]@{ Policy = $Policy; Rules = $Rules }
}

function Compare-CIPPDlpCompliancePolicy {
<#
.SYNOPSIS
Compare a stored DLP template against the live policy + rules in a tenant and report drift.
.DESCRIPTION
Normalizes both sides through ConvertTo-CIPPDlpComparable and diffs them field by field
(policy-level and per-rule, matched by rule name). Returns the overall state and the specific
differing fields with their expected (template) and current (tenant) values, so callers can
decide whether to remediate and can surface exactly what differs.
.PARAMETER TenantFilter
Target tenant.
.PARAMETER Template
The stored template object (already ConvertFrom-Json'd).
.OUTPUTS
PSCustomObject: Name, State ('Missing' | 'PendingDeletion' | 'InSync' | 'Drift'), and Differences
(array of { Scope, Field, Expected, Current }).
.FUNCTIONALITY
Internal
#>
[CmdletBinding()]
param(
[Parameter(Mandatory)] [string] $TenantFilter,
[Parameter(Mandatory)] $Template
)

$PolicyName = $Template.Name ?? $Template.name

$LivePolicy = try {
New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-DlpCompliancePolicy' -Compliance |
Where-Object { $_.Name -eq $PolicyName } | Select-Object -First 1
} catch { $null }

if (-not $LivePolicy) {
return [pscustomobject]@{ Name = $PolicyName; State = 'Missing'; Differences = @() }
}
if ($LivePolicy.Mode -eq 'PendingDeletion') {
return [pscustomobject]@{ Name = $PolicyName; State = 'PendingDeletion'; Differences = @() }
}

$LiveRules = try {
@(New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-DlpComplianceRule' -Compliance |
Where-Object { $_.ParentPolicyName -eq $PolicyName })
} catch { @() }

$Want = ConvertTo-CIPPDlpComparable -PolicySource $Template -RuleSource $Template.RuleParams
$Have = ConvertTo-CIPPDlpComparable -PolicySource $LivePolicy -RuleSource $LiveRules

$Differences = [System.Collections.Generic.List[object]]::new()

# Policy-level diff
foreach ($Key in (@($Want.Policy.Keys) + @($Have.Policy.Keys) | Select-Object -Unique)) {
$Expected = if ($Want.Policy.ContainsKey($Key)) { $Want.Policy[$Key] } else { $null }
$Current = if ($Have.Policy.ContainsKey($Key)) { $Have.Policy[$Key] } else { $null }
if ((ConvertTo-CIPPComparableString -Value $Expected) -ne (ConvertTo-CIPPComparableString -Value $Current)) {
$Differences.Add([pscustomobject]@{ Scope = 'Policy'; Field = $Key; Expected = $Expected; Current = $Current })
}
}

# Rule-level diff (only rules the template defines; matched by name)
foreach ($RuleName in @($Want.Rules.Keys)) {
if (@($Have.Rules.Keys) -notcontains $RuleName) {
$Differences.Add([pscustomobject]@{ Scope = "Rule '$RuleName'"; Field = '(entire rule)'; Expected = 'present'; Current = 'missing' })
continue
}
$WantRule = $Want.Rules[$RuleName]
$HaveRule = $Have.Rules[$RuleName]
foreach ($Key in (@($WantRule.Keys) + @($HaveRule.Keys) | Select-Object -Unique)) {
$Expected = if ($WantRule.ContainsKey($Key)) { $WantRule[$Key] } else { $null }
$Current = if ($HaveRule.ContainsKey($Key)) { $HaveRule[$Key] } else { $null }
if ((ConvertTo-CIPPComparableString -Value $Expected) -ne (ConvertTo-CIPPComparableString -Value $Current)) {
$Differences.Add([pscustomobject]@{ Scope = "Rule '$RuleName'"; Field = $Key; Expected = $Expected; Current = $Current })
}
}
}

$State = if ($Differences.Count -eq 0) { 'InSync' } else { 'Drift' }
return [pscustomobject]@{ Name = $PolicyName; State = $State; Differences = @($Differences) }
}
Loading