Skip to content

Commit a511657

Browse files
authored
Dev to hotfix (KelvinTegelaar#2113)
2 parents e8cd121 + 40ca72e commit a511657

59 files changed

Lines changed: 1856 additions & 552 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Config/CIPPTimers.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,7 @@
241241
"Id": "5e8a9b4c-2d6f-4a3e-b7c1-9d0e5f3a8b2c",
242242
"Command": "Start-IntuneReportExportOrchestrator",
243243
"Description": "Submit Intune report-export jobs ahead of nightly DB cache run",
244-
"Cron": "0 0 2 * * *",
244+
"Cron": "0 0 1 * * *",
245245
"Priority": 22,
246246
"RunOnProcessor": true,
247247
"TZOffset": true,

Config/ConversionTable.csv

Lines changed: 123 additions & 0 deletions
Large diffs are not rendered by default.

Config/FeatureFlags.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,5 +81,20 @@
8181
],
8282
"Pages": [],
8383
"Hidden": false
84+
},
85+
{
86+
"Id": "AppInsights",
87+
"Name": "App Insights",
88+
"Description": "App Insights page not used in NG",
89+
"Enabled": true,
90+
"AllowUserToggle": false,
91+
"Timers": [],
92+
"Endpoints": [
93+
"ExecAppInsightsQuery"
94+
],
95+
"Pages": [
96+
"/cipp/advanced/diagnostics"
97+
],
98+
"Hidden": true
8499
}
85100
]
File renamed without changes.

Config/standards.json

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -378,8 +378,8 @@
378378
"cat": "Global Standards",
379379
"tag": ["CIS M365 7.0.0 (1.3.6)", "CustomerLockBoxEnabled"],
380380
"appliesToTest": ["CIS_1_3_6"],
381-
"helpText": "**Requires Entra ID P2.** Enables Customer Lockbox that offers an approval process for Microsoft support to access organization data",
382-
"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.",
381+
"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",
382+
"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.",
383383
"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.",
384384
"addedComponent": [],
385385
"label": "Enable Customer Lockbox",
@@ -4157,12 +4157,10 @@
41574157
"defaultValue": false
41584158
},
41594159
{
4160-
"type": "autoComplete",
4161-
"multiple": true,
4162-
"creatable": true,
4160+
"type": "LanguageCodeMultiSelect",
41634161
"required": false,
41644162
"name": "standards.SpamFilterPolicy.LanguageBlockList",
4165-
"label": "Languages to block (uppercase ISO 639-1 two-letter)",
4163+
"label": "Languages to block (ISO 639-1 two-letter)",
41664164
"condition": {
41674165
"field": "standards.SpamFilterPolicy.EnableLanguageBlockList",
41684166
"compareType": "is",
@@ -4176,12 +4174,10 @@
41764174
"defaultValue": false
41774175
},
41784176
{
4179-
"type": "autoComplete",
4180-
"multiple": true,
4181-
"creatable": true,
4177+
"type": "CountryCodeMultiSelect",
41824178
"required": false,
41834179
"name": "standards.SpamFilterPolicy.RegionBlockList",
4184-
"label": "Regions to block (uppercase ISO 3166-1 two-letter)",
4180+
"label": "Regions to block (ISO 3166-1 two-letter)",
41854181
"condition": {
41864182
"field": "standards.SpamFilterPolicy.EnableRegionBlockList",
41874183
"compareType": "is",
@@ -5865,7 +5861,7 @@
58655861
{
58665862
"type": "switch",
58675863
"name": "standards.TeamsFederationConfiguration.AllowTeamsConsumer",
5868-
"label": "Allow users to communicate with other organizations"
5864+
"label": "Allow users to communicate with consumer Teams accounts"
58695865
},
58705866
{
58715867
"type": "autoComplete",
@@ -7255,6 +7251,12 @@
72557251
"label": "Block Android if partner data unavailable",
72567252
"defaultValue": false
72577253
},
7254+
{
7255+
"type": "switch",
7256+
"name": "standards.DefenderCompliancePolicy.grantMobileThreatDefensePartnerRole",
7257+
"label": "Grant MTD role to MDE on enrolled Android COBO/COPE devices",
7258+
"defaultValue": false
7259+
},
72587260
{
72597261
"type": "switch",
72607262
"name": "standards.DefenderCompliancePolicy.ConnectIos",
@@ -7264,13 +7266,19 @@
72647266
{
72657267
"type": "switch",
72667268
"name": "standards.DefenderCompliancePolicy.ConnectIosCompliance",
7267-
"label": "Connect iOS 13.0+ (App-based MAM)",
7269+
"label": "Connect iOS/iPadOS devices for app protection policy evaluation (MAM)",
72687270
"defaultValue": false
72697271
},
72707272
{
72717273
"type": "switch",
72727274
"name": "standards.DefenderCompliancePolicy.appSync",
7273-
"label": "Enable App Sync for iOS",
7275+
"label": "Enable App Sync (sending application inventory) for iOS/iPadOS devices",
7276+
"defaultValue": false
7277+
},
7278+
{
7279+
"type": "switch",
7280+
"name": "standards.DefenderCompliancePolicy.allowPartnerToCollectIosPersonalApplicationMetadata",
7281+
"label": "Send full application inventory data on personally-owned iOS/iPadOS devices",
72747282
"defaultValue": false
72757283
},
72767284
{
@@ -7282,13 +7290,13 @@
72827290
{
72837291
"type": "switch",
72847292
"name": "standards.DefenderCompliancePolicy.allowPartnerToCollectIosCertificateMetadata",
7285-
"label": "Collect certificate metadata from iOS",
7293+
"label": "Enable Certificate Sync for iOS/iPadOS devices",
72867294
"defaultValue": false
72877295
},
72887296
{
72897297
"type": "switch",
72907298
"name": "standards.DefenderCompliancePolicy.allowPartnerToCollectIosPersonalCertificateMetadata",
7291-
"label": "Collect personal certificate metadata from iOS",
7299+
"label": "Send full certificate inventory data on personally-owned iOS/iPadOS devices",
72927300
"defaultValue": false
72937301
},
72947302
{

Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Maintenance/Push-TableCleanupTask.ps1

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ function Push-TableCleanupTask {
1616
if ($Table) {
1717
Write-Information "Deleting table $($Table.Context.TableName)"
1818
try {
19-
Remove-AzDataTable -Context $Table.Context -Force
19+
Remove-AzDataTable -Context $Table.Context
2020
} catch {
2121
#Write-LogMessage -API 'TableCleanup' -message "Failed to delete table $($Table.Context.TableName)" -sev Error -LogData (Get-CippException -Exception $_)
2222
}

Modules/CIPPCore/Public/AuditLogs/New-CIPPAuditLogSearchResultsCache.ps1

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,21 @@ function New-CIPPAuditLogSearchResultsCache {
7272
Add-CIPPAzDataTableEntity @CacheWebhooksTable -Entity $cacheEntity -Force
7373
}
7474
Write-Information "Successfully cached search ID: $($SearchId) for tenant: $TenantFilter"
75+
76+
try {
77+
$PrefetchIPs = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
78+
foreach ($sr in $searchResults) {
79+
$cip = $sr.auditData.clientip
80+
if (![string]::IsNullOrWhiteSpace($cip)) { $null = $PrefetchIPs.Add(([string]$cip).Trim()) }
81+
}
82+
if ($PrefetchIPs.Count -gt 0) {
83+
$null = Get-CIPPGeoIPLocationBatch -IPs @($PrefetchIPs)
84+
Write-Information "Geo prefetch: warmed cache for $($PrefetchIPs.Count) distinct IP(s) (search $SearchId)"
85+
}
86+
} catch {
87+
Write-Information "Geo prefetch during ingestion failed for search ${SearchId}: $($_.Exception.Message)"
88+
}
89+
7590
try {
7691
$FailedDownloadsTable = Get-CippTable -TableName 'FailedAuditLogDownloads'
7792
$failedEntities = Get-CIPPAzDataTableEntity @FailedDownloadsTable -Filter "PartitionKey eq '$TenantFilter' and SearchId eq '$SearchId'"
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
function Get-CIPPManagedIdentityResourceId {
2+
<#
3+
.SYNOPSIS
4+
Get the Azure resource ID that the Function App's managed identity belongs to.
5+
.DESCRIPTION
6+
Reads the 'xms_mirid' claim from a managed identity access token. For a system-assigned
7+
identity (which CIPP uses), this claim is the ARM resource ID of the host resource itself
8+
- i.e. the Function App site, including its resource group:
9+
10+
/subscriptions/{sub}/resourcegroups/{rg}/providers/Microsoft.Web/sites/{site}
11+
12+
This is the most reliable in-process source for the site's resource group because it is
13+
present in every managed identity token, requires no extra ARM/Graph call, and - unlike
14+
parsing WEBSITE_OWNER_NAME - always names the site's RG rather than the App Service Plan's
15+
webspace RG.
16+
17+
Note: for a user-assigned identity, xms_mirid points at the userAssignedIdentities resource
18+
instead, which may live in a different RG. Callers that need the site's RG should validate
19+
the returned ID against the expected site (see Get-CIPPFunctionAppResourceGroup).
20+
.PARAMETER ResourceUrl
21+
The Azure resource URL to request the token for. Defaults to Azure Resource Manager.
22+
.EXAMPLE
23+
Get-CIPPManagedIdentityResourceId
24+
Returns e.g. /subscriptions/.../resourcegroups/CIPP-myinstance/providers/Microsoft.Web/sites/cippabcde
25+
#>
26+
[CmdletBinding()]
27+
param(
28+
[Parameter(Mandatory = $false)]
29+
[string]$ResourceUrl = 'https://management.azure.com/'
30+
)
31+
32+
$Token = Get-CIPPAzIdentityToken -ResourceUrl $ResourceUrl
33+
if (-not $Token) {
34+
throw 'Could not acquire a managed identity token to read the xms_mirid claim.'
35+
}
36+
37+
# JWT payload is the second dot-delimited segment, base64url-encoded.
38+
$Parts = $Token.Split('.')
39+
if ($Parts.Count -lt 2) {
40+
throw 'Managed identity token is not a well-formed JWT.'
41+
}
42+
43+
$Payload = $Parts[1].Replace('-', '+').Replace('_', '/')
44+
switch ($Payload.Length % 4) {
45+
2 { $Payload += '==' }
46+
3 { $Payload += '=' }
47+
}
48+
49+
$Claims = [System.Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($Payload)) | ConvertFrom-Json
50+
return $Claims.xms_mirid
51+
}
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
function ConvertTo-CIPPComparableString {
2+
<#
3+
.SYNOPSIS
4+
Produce an order-independent canonical string for a value, for equality comparison.
5+
.DESCRIPTION
6+
Recursively serializes scalars, dictionaries/objects (keys sorted), and arrays (elements sorted)
7+
into a deterministic string. Two values are equal iff their canonical strings match - independent
8+
of property order or array order, which is the right semantics for DLP locations and the set of
9+
sensitive information types (order is not meaningful for matching).
10+
.FUNCTIONALITY
11+
Internal
12+
#>
13+
[CmdletBinding()]
14+
param($Value)
15+
16+
if ($null -eq $Value) { return 'null' }
17+
if ($Value -is [string]) { return '"' + $Value + '"' }
18+
if ($Value -is [bool] -or $Value -is [int] -or $Value -is [long] -or $Value -is [double] -or $Value -is [decimal]) {
19+
return [string]$Value
20+
}
21+
if ($Value -is [System.Collections.IDictionary]) {
22+
$parts = foreach ($k in (@($Value.Keys) | Sort-Object)) { '"' + $k + '":' + (ConvertTo-CIPPComparableString -Value $Value[$k]) }
23+
return '{' + ($parts -join ',') + '}'
24+
}
25+
if ($Value -is [System.Management.Automation.PSCustomObject]) {
26+
$parts = foreach ($p in ($Value.PSObject.Properties | Sort-Object Name)) { '"' + $p.Name + '":' + (ConvertTo-CIPPComparableString -Value $p.Value) }
27+
return '{' + ($parts -join ',') + '}'
28+
}
29+
if ($Value -is [System.Collections.IEnumerable]) {
30+
$items = @(foreach ($item in $Value) { ConvertTo-CIPPComparableString -Value $item }) | Sort-Object
31+
return '[' + ($items -join ',') + ']'
32+
}
33+
return '"' + ([string]$Value) + '"'
34+
}
35+
36+
function ConvertTo-CIPPDlpComparable {
37+
<#
38+
.SYNOPSIS
39+
Normalize a DLP policy source (template or live policy) + its rules into a comparable param map.
40+
.DESCRIPTION
41+
Runs the source through the exact same normalization the deploy path uses - allowlist filtering,
42+
location normalization, sensitive-information-type conversion (which also strips output-only
43+
rulePackId), and IncidentReportContent string->array - so a template and the live policy it was
44+
deployed from collapse to identical structures when nothing has actually drifted.
45+
.PARAMETER PolicySource
46+
The policy-level object (a stored template, or a Get-DlpCompliancePolicy result).
47+
.PARAMETER RuleSource
48+
The rule collection (template RuleParams, or Get-DlpComplianceRule results).
49+
.OUTPUTS
50+
PSCustomObject with Policy (hashtable of normalized policy params) and Rules (ordered map of
51+
rule name -> hashtable of normalized rule params).
52+
.FUNCTIONALITY
53+
Internal
54+
#>
55+
[CmdletBinding()]
56+
param($PolicySource, $RuleSource)
57+
58+
$Fields = Get-CIPPDlpComplianceFieldList
59+
60+
$Policy = Format-CIPPCompliancePolicyParams -Source $PolicySource -AllowedFields $Fields.Policy -LocationFields $Fields.Location
61+
$Policy.Remove('Name') | Out-Null # identity, not a comparable setting
62+
# Mirror deploy: an invalid/transient Mode (e.g. PendingDeletion) is never deployed, so it must not
63+
# register as drift either.
64+
if ($Policy.ContainsKey('Mode') -and $Policy['Mode'] -notin $Fields.ValidPolicyModes) {
65+
$Policy.Remove('Mode') | Out-Null
66+
}
67+
68+
$Rules = [ordered]@{}
69+
foreach ($Rule in @($RuleSource) | Where-Object { $_ }) {
70+
$RuleParams = Format-CIPPCompliancePolicyParams -Source $Rule -AllowedFields $Fields.Rule
71+
$RuleName = [string]$RuleParams['Name']
72+
$RuleParams.Remove('Policy') | Out-Null
73+
$RuleParams.Remove('Name') | Out-Null
74+
foreach ($SitField in @('ContentContainsSensitiveInformation', 'ExceptIfContentContainsSensitiveInformation')) {
75+
if ($RuleParams.ContainsKey($SitField)) {
76+
$RuleParams[$SitField] = @(ConvertTo-CIPPSensitiveInformationType -SensitiveInformation $RuleParams[$SitField])
77+
}
78+
}
79+
if ($RuleParams.ContainsKey('IncidentReportContent') -and $RuleParams['IncidentReportContent'] -is [string]) {
80+
$RuleParams['IncidentReportContent'] = @($RuleParams['IncidentReportContent'] -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ })
81+
}
82+
if (-not [string]::IsNullOrWhiteSpace($RuleName)) { $Rules[$RuleName] = $RuleParams }
83+
}
84+
85+
return [pscustomobject]@{ Policy = $Policy; Rules = $Rules }
86+
}
87+
88+
function Compare-CIPPDlpCompliancePolicy {
89+
<#
90+
.SYNOPSIS
91+
Compare a stored DLP template against the live policy + rules in a tenant and report drift.
92+
.DESCRIPTION
93+
Normalizes both sides through ConvertTo-CIPPDlpComparable and diffs them field by field
94+
(policy-level and per-rule, matched by rule name). Returns the overall state and the specific
95+
differing fields with their expected (template) and current (tenant) values, so callers can
96+
decide whether to remediate and can surface exactly what differs.
97+
.PARAMETER TenantFilter
98+
Target tenant.
99+
.PARAMETER Template
100+
The stored template object (already ConvertFrom-Json'd).
101+
.OUTPUTS
102+
PSCustomObject: Name, State ('Missing' | 'PendingDeletion' | 'InSync' | 'Drift'), and Differences
103+
(array of { Scope, Field, Expected, Current }).
104+
.FUNCTIONALITY
105+
Internal
106+
#>
107+
[CmdletBinding()]
108+
param(
109+
[Parameter(Mandatory)] [string] $TenantFilter,
110+
[Parameter(Mandatory)] $Template
111+
)
112+
113+
$PolicyName = $Template.Name ?? $Template.name
114+
115+
$LivePolicy = try {
116+
New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-DlpCompliancePolicy' -Compliance |
117+
Where-Object { $_.Name -eq $PolicyName } | Select-Object -First 1
118+
} catch { $null }
119+
120+
if (-not $LivePolicy) {
121+
return [pscustomobject]@{ Name = $PolicyName; State = 'Missing'; Differences = @() }
122+
}
123+
if ($LivePolicy.Mode -eq 'PendingDeletion') {
124+
return [pscustomobject]@{ Name = $PolicyName; State = 'PendingDeletion'; Differences = @() }
125+
}
126+
127+
$LiveRules = try {
128+
@(New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-DlpComplianceRule' -Compliance |
129+
Where-Object { $_.ParentPolicyName -eq $PolicyName })
130+
} catch { @() }
131+
132+
$Want = ConvertTo-CIPPDlpComparable -PolicySource $Template -RuleSource $Template.RuleParams
133+
$Have = ConvertTo-CIPPDlpComparable -PolicySource $LivePolicy -RuleSource $LiveRules
134+
135+
$Differences = [System.Collections.Generic.List[object]]::new()
136+
137+
# Policy-level diff
138+
foreach ($Key in (@($Want.Policy.Keys) + @($Have.Policy.Keys) | Select-Object -Unique)) {
139+
$Expected = if ($Want.Policy.ContainsKey($Key)) { $Want.Policy[$Key] } else { $null }
140+
$Current = if ($Have.Policy.ContainsKey($Key)) { $Have.Policy[$Key] } else { $null }
141+
if ((ConvertTo-CIPPComparableString -Value $Expected) -ne (ConvertTo-CIPPComparableString -Value $Current)) {
142+
$Differences.Add([pscustomobject]@{ Scope = 'Policy'; Field = $Key; Expected = $Expected; Current = $Current })
143+
}
144+
}
145+
146+
# Rule-level diff (only rules the template defines; matched by name)
147+
foreach ($RuleName in @($Want.Rules.Keys)) {
148+
if (@($Have.Rules.Keys) -notcontains $RuleName) {
149+
$Differences.Add([pscustomobject]@{ Scope = "Rule '$RuleName'"; Field = '(entire rule)'; Expected = 'present'; Current = 'missing' })
150+
continue
151+
}
152+
$WantRule = $Want.Rules[$RuleName]
153+
$HaveRule = $Have.Rules[$RuleName]
154+
foreach ($Key in (@($WantRule.Keys) + @($HaveRule.Keys) | Select-Object -Unique)) {
155+
$Expected = if ($WantRule.ContainsKey($Key)) { $WantRule[$Key] } else { $null }
156+
$Current = if ($HaveRule.ContainsKey($Key)) { $HaveRule[$Key] } else { $null }
157+
if ((ConvertTo-CIPPComparableString -Value $Expected) -ne (ConvertTo-CIPPComparableString -Value $Current)) {
158+
$Differences.Add([pscustomobject]@{ Scope = "Rule '$RuleName'"; Field = $Key; Expected = $Expected; Current = $Current })
159+
}
160+
}
161+
}
162+
163+
$State = if ($Differences.Count -eq 0) { 'InSync' } else { 'Drift' }
164+
return [pscustomobject]@{ Name = $PolicyName; State = $State; Differences = @($Differences) }
165+
}

0 commit comments

Comments
 (0)