Skip to content

Commit 93507d9

Browse files
authored
Merge pull request #1034 from KelvinTegelaar/dev
[pull] dev from KelvinTegelaar:dev
2 parents 061fd64 + e4bd4be commit 93507d9

9 files changed

Lines changed: 255 additions & 62 deletions

File tree

Config/FeatureFlags.json

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,31 @@
4444
],
4545
"Hidden": true
4646
},
47+
{
48+
"Id": "CopilotAI",
49+
"Name": "Copilot & AI",
50+
"Description": "Under Development: Microsoft 365 Copilot and AI management pages including settings, usage reports, Agent365 packages, and Shadow AI analysis.",
51+
"Enabled": false,
52+
"AllowUserToggle": false,
53+
"Timers": [],
54+
"Endpoints": [
55+
"ListCopilotSettings",
56+
"ExecCopilotSettings",
57+
"ListCopilotUsage",
58+
"ListAgent365Packages",
59+
"ListAgent365PackageDetail",
60+
"ListShadowAI"
61+
],
62+
"Pages": [
63+
"/copilot/settings",
64+
"/copilot/shadow-ai",
65+
"/copilot/agent365/packages",
66+
"/copilot/reports/copilot-adoption",
67+
"/copilot/reports/copilot-usage",
68+
"/copilot/reports/copilot-trend"
69+
],
70+
"Hidden": true
71+
},
4772
{
4873
"Id": "MCPServer",
4974
"Name": "MCP Server",
@@ -57,4 +82,4 @@
5782
"Pages": [],
5883
"Hidden": false
5984
}
60-
]
85+
]
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
function ConvertTo-CIPPSensitivityLabelParams {
2+
<#
3+
.SYNOPSIS
4+
Normalize a sensitivity label template/object into the flat parameter shape that New-Label/Set-Label expect.
5+
.DESCRIPTION
6+
Get-Label (the read shape) does not expose flat Encryption*/Apply* properties. Instead it encodes
7+
encryption, content marking and watermarking inside the 'LabelActions' array, e.g.
8+
9+
{ "Type":"encrypt", "SubType":null, "Settings":[ {"Key":"protectiontype","Value":"userdefined"}, ... ] }
10+
{ "Type":"applycontentmarking", "SubType":"footer", "Settings":[ {"Key":"text","Value":"..."}, ... ] }
11+
12+
New-Label/Set-Label (the write shape) instead take flat 'Apply*'/'Encryption*' parameters. This
13+
function bridges the two: when a label object carries 'LabelActions' it expands those actions into
14+
the flat parameters and drops the read-only 'LabelActions'/'Settings'/'LocaleSettings'/'Conditions'
15+
arrays (which are not valid input in their read form). A flat object (manual JSON authored against
16+
the deploy schema) has no 'LabelActions' and passes through unchanged.
17+
18+
Deploy-time validation/allowlisting still happens in Set-CIPPSensitivityLabel via
19+
Get-CIPPSensitivityLabelField; this function only reshapes.
20+
.PARAMETER Label
21+
The label template/object to normalize (a Get-Label object, a stored template, or flat manual JSON).
22+
.FUNCTIONALITY
23+
Internal
24+
#>
25+
[CmdletBinding()]
26+
param(
27+
[Parameter(Mandatory)] $Label
28+
)
29+
30+
# A captured Get-Label object always has a LabelActions property (even if empty); flat manual JSON does not.
31+
$HasActions = [bool]$Label.PSObject.Properties['LabelActions']
32+
# Read-shape arrays that are not valid New-/Set-Label input - dropped when reshaping a captured label.
33+
$ReadShapeArrays = @('LabelActions', 'Settings', 'LocaleSettings', 'Conditions')
34+
35+
$Flat = [ordered]@{}
36+
foreach ($Prop in $Label.PSObject.Properties) {
37+
if ($HasActions -and $Prop.Name -in $ReadShapeArrays) { continue }
38+
$Flat[$Prop.Name] = $Prop.Value
39+
}
40+
41+
if (-not $HasActions) {
42+
return [pscustomobject]$Flat
43+
}
44+
45+
foreach ($Raw in @($Label.LabelActions)) {
46+
if ($null -eq $Raw) { continue }
47+
$Action = if ($Raw -is [string]) { $Raw | ConvertFrom-Json } else { $Raw }
48+
49+
$Set = @{}
50+
foreach ($KV in $Action.Settings) { $Set[$KV.Key] = $KV.Value }
51+
$Enabled = ($Set['disabled'] -ne 'true')
52+
53+
switch ($Action.Type) {
54+
'encrypt' {
55+
$Flat['EncryptionEnabled'] = $Enabled
56+
if (-not $Enabled) { break }
57+
58+
$ProtectionType = "$($Set['protectiontype'])".ToLower()
59+
if ($ProtectionType -eq 'template') {
60+
$Flat['EncryptionProtectionType'] = 'Template'
61+
if ($Set['templateid']) { $Flat['EncryptionTemplateId'] = $Set['templateid'] }
62+
if ($Set.ContainsKey('contentexpiredondateindaysornever')) { $Flat['EncryptionContentExpiredOnDateInDaysOrNever'] = $Set['contentexpiredondateindaysornever'] }
63+
if ($Set.ContainsKey('offlineaccessdays')) { $Flat['EncryptionOfflineAccessDays'] = [int]$Set['offlineaccessdays'] }
64+
} else {
65+
$Flat['EncryptionProtectionType'] = 'UserDefined'
66+
if ($Set.ContainsKey('donotforward')) { $Flat['EncryptionDoNotForward'] = ($Set['donotforward'] -eq 'true') }
67+
if ($Set.ContainsKey('encryptonly')) { $Flat['EncryptionEncryptOnly'] = ($Set['encryptonly'] -eq 'true') }
68+
if ($Set.ContainsKey('promptuser')) { $Flat['EncryptionPromptUser'] = ($Set['promptuser'] -eq 'true') }
69+
}
70+
}
71+
'applycontentmarking' {
72+
$Prefix = switch ("$($Action.SubType)".ToLower()) {
73+
'header' { 'ApplyContentMarkingHeader' }
74+
'footer' { 'ApplyContentMarkingFooter' }
75+
'watermark' { 'ApplyWaterMarking' }
76+
default { $null }
77+
}
78+
if (-not $Prefix) { break }
79+
80+
$Flat["${Prefix}Enabled"] = $Enabled
81+
if ($Set['text']) { $Flat["${Prefix}Text"] = $Set['text'] }
82+
if ($Set['fontcolor']) { $Flat["${Prefix}FontColor"] = $Set['fontcolor'] }
83+
if ($Set['fontname']) { $Flat["${Prefix}FontName"] = $Set['fontname'] }
84+
if ($Set.ContainsKey('fontsize') -and "$($Set['fontsize'])".Trim()) { $Flat["${Prefix}FontSize"] = [int]$Set['fontsize'] }
85+
if ($Prefix -eq 'ApplyWaterMarking') {
86+
if ($Set['layout']) { $Flat['ApplyWaterMarkingLayout'] = $Set['layout'] }
87+
} else {
88+
if ($Set['alignment']) { $Flat["${Prefix}Alignment"] = $Set['alignment'] }
89+
if ($Action.SubType -eq 'footer' -and $Set.ContainsKey('margin') -and "$($Set['margin'])".Trim()) { $Flat["${Prefix}Margin"] = [int]$Set['margin'] }
90+
}
91+
}
92+
'applywatermarking' {
93+
$Flat['ApplyWaterMarkingEnabled'] = $Enabled
94+
if ($Set['text']) { $Flat['ApplyWaterMarkingText'] = $Set['text'] }
95+
if ($Set['fontcolor']) { $Flat['ApplyWaterMarkingFontColor'] = $Set['fontcolor'] }
96+
if ($Set['fontname']) { $Flat['ApplyWaterMarkingFontName'] = $Set['fontname'] }
97+
if ($Set.ContainsKey('fontsize') -and "$($Set['fontsize'])".Trim()) { $Flat['ApplyWaterMarkingFontSize'] = [int]$Set['fontsize'] }
98+
if ($Set['layout']) { $Flat['ApplyWaterMarkingLayout'] = $Set['layout'] }
99+
}
100+
'protectgroup' {
101+
$Flat['SiteAndGroupProtectionEnabled'] = $Enabled
102+
if ($Set['privacy']) { $Flat['SiteAndGroupProtectionPrivacy'] = $Set['privacy'] }
103+
if ($Set.ContainsKey('allowaccesstoguestusers')) { $Flat['SiteAndGroupProtectionAllowAccessToGuestUsers'] = ($Set['allowaccesstoguestusers'] -eq 'true') }
104+
if ($Set.ContainsKey('allowemailfromguestusers')) { $Flat['SiteAndGroupProtectionAllowEmailFromGuestUsers'] = ($Set['allowemailfromguestusers'] -eq 'true') }
105+
}
106+
}
107+
}
108+
109+
return [pscustomobject]$Flat
110+
}

Modules/CIPPCore/Public/Get-CIPPDrift.ps1

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -409,12 +409,22 @@ function Get-CIPPDrift {
409409
# Persist newly detected deviations to the tenantDrift table so the summary page can count them
410410
$NewDriftEntities = [System.Collections.Generic.List[object]]::new()
411411
foreach ($Deviation in $AllDeviations) {
412-
if (-not $ExistingDriftStates.ContainsKey($Deviation.standardName)) {
413-
$RowKey = $Deviation.standardName -replace '\.', '_'
412+
# Diagnostic: standardName must be a scalar string. Azure Tables cannot store a PSObject,
413+
# so a non-string here is what causes "Unsupported property types found: StandardName".
414+
# Log the offending value (with tenant) so the producing standard can be identified.
415+
if ($Deviation.standardName -isnot [string]) {
416+
Write-Warning "Drift deviation for tenant '$TenantFilter' has a non-string standardName (type $($Deviation.standardName.GetType().FullName)): $(ConvertTo-Json -InputObject $Deviation.standardName -Depth 5 -Compress -ErrorAction SilentlyContinue)"
417+
}
418+
# Coerce to string so the table write never fails on this property. RowKey already
419+
# coerces via -replace; this makes the stored StandardName column match.
420+
$StandardNameValue = [string]$Deviation.standardName
421+
if ([string]::IsNullOrWhiteSpace($StandardNameValue)) { continue }
422+
if (-not $ExistingDriftStates.ContainsKey($StandardNameValue)) {
423+
$RowKey = $StandardNameValue -replace '\.', '_'
414424
$NewDriftEntities.Add(@{
415425
PartitionKey = $TenantFilter
416426
RowKey = $RowKey
417-
StandardName = $Deviation.standardName
427+
StandardName = $StandardNameValue
418428
Status = 'New'
419429
LastModified = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ')
420430
})
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
function Get-CIPPSensitivityLabelField {
2+
<#
3+
.SYNOPSIS
4+
Returns the valid New-Label / Set-Label parameter names CIPP supports for sensitivity label deployment.
5+
.DESCRIPTION
6+
Single source of truth for the sensitivity label field allowlist, shared by Set-CIPPSensitivityLabel
7+
(deploy) and Invoke-AddSensitivityLabelTemplate (capture keep-list) so the two cannot drift.
8+
9+
Names match the Microsoft Purview New-Label/Set-Label cmdlet parameters exactly. Note the content
10+
marking and watermark parameters are all 'Apply'-prefixed (ApplyContentMarkingHeaderText,
11+
ApplyWaterMarkingText, ...) - the bare 'ContentMarking*' names do not exist and cause an
12+
AmbiguousParameterSetException.
13+
14+
'Priority' is included here but is only valid on Set-Label, not New-Label - Set-CIPPSensitivityLabel
15+
applies it via a dedicated Set-Label call. 'Disabled' is intentionally absent because it is not a
16+
valid parameter on either cmdlet.
17+
.FUNCTIONALITY
18+
Internal
19+
#>
20+
[CmdletBinding()]
21+
param()
22+
23+
return @(
24+
# Core
25+
'Name', 'DisplayName', 'Comment', 'Tooltip', 'ParentId', 'ContentType', 'Priority',
26+
'Conditions', 'LocaleSettings', 'Settings', 'AdvancedSettings',
27+
28+
# Encryption
29+
'EncryptionEnabled', 'EncryptionProtectionType',
30+
'EncryptionTemplateId', 'EncryptionLinkedTemplateId', 'EncryptionAipTemplateScopes',
31+
'EncryptionRightsDefinitions', 'EncryptionContentExpiredOnDateInDaysOrNever',
32+
'EncryptionDoNotForward', 'EncryptionEncryptOnly', 'EncryptionPromptUser',
33+
'EncryptionOfflineAccessDays',
34+
35+
# Content marking - header
36+
'ApplyContentMarkingHeaderEnabled', 'ApplyContentMarkingHeaderText',
37+
'ApplyContentMarkingHeaderFontSize', 'ApplyContentMarkingHeaderFontColor',
38+
'ApplyContentMarkingHeaderFontName', 'ApplyContentMarkingHeaderAlignment',
39+
'ApplyContentMarkingHeaderMargin',
40+
41+
# Content marking - footer
42+
'ApplyContentMarkingFooterEnabled', 'ApplyContentMarkingFooterText',
43+
'ApplyContentMarkingFooterFontSize', 'ApplyContentMarkingFooterFontColor',
44+
'ApplyContentMarkingFooterFontName', 'ApplyContentMarkingFooterAlignment',
45+
'ApplyContentMarkingFooterMargin',
46+
47+
# Watermark
48+
'ApplyWaterMarkingEnabled', 'ApplyWaterMarkingText',
49+
'ApplyWaterMarkingFontSize', 'ApplyWaterMarkingFontColor',
50+
'ApplyWaterMarkingFontName', 'ApplyWaterMarkingLayout',
51+
52+
# Site & group protection
53+
'SiteAndGroupProtectionEnabled', 'SiteAndGroupProtectionPrivacy',
54+
'SiteAndGroupProtectionLevel',
55+
'SiteAndGroupProtectionAllowAccessToGuestUsers',
56+
'SiteAndGroupProtectionAllowEmailFromGuestUsers',
57+
'SiteAndGroupProtectionAllowFullAccess',
58+
'SiteAndGroupProtectionAllowLimitedAccess',
59+
'SiteAndGroupProtectionBlockAccess'
60+
)
61+
}

Modules/CIPPCore/Public/Set-CIPPSensitivityLabel.ps1

Lines changed: 24 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -16,29 +16,8 @@ function Set-CIPPSensitivityLabel {
1616
$Headers
1717
)
1818

19-
$LabelAllowedFields = @(
20-
'Name', 'DisplayName', 'Comment', 'Tooltip', 'ParentId',
21-
'Disabled', 'ContentType', 'Priority',
22-
'EncryptionEnabled', 'EncryptionProtectionType', 'EncryptionRightsDefinitions',
23-
'EncryptionContentExpiredOnDateInDaysOrNever', 'EncryptionDoNotForward',
24-
'EncryptionEncryptOnly', 'EncryptionOfflineAccessDays',
25-
'EncryptionPromptUser', 'EncryptionAESKeySize',
26-
'ContentMarkingHeaderEnabled', 'ContentMarkingHeaderText',
27-
'ContentMarkingHeaderFontSize', 'ContentMarkingHeaderFontColor', 'ContentMarkingHeaderAlignment',
28-
'ContentMarkingFooterEnabled', 'ContentMarkingFooterText',
29-
'ContentMarkingFooterFontSize', 'ContentMarkingFooterFontColor', 'ContentMarkingFooterAlignment',
30-
'ContentMarkingFooterMargin',
31-
'ContentMarkingWatermarkEnabled', 'ContentMarkingWatermarkText',
32-
'ContentMarkingWatermarkFontSize', 'ContentMarkingWatermarkFontColor', 'ContentMarkingWatermarkLayout',
33-
'ApplyContentMarkingHeaderEnabled', 'ApplyContentMarkingFooterEnabled', 'ApplyWaterMarkingEnabled',
34-
'SiteAndGroupProtectionEnabled', 'SiteAndGroupProtectionPrivacy',
35-
'SiteAndGroupProtectionAllowAccessToGuestUsers',
36-
'SiteAndGroupProtectionAllowEmailFromGuestUsers',
37-
'SiteAndGroupProtectionAllowFullAccess',
38-
'SiteAndGroupProtectionAllowLimitedAccess',
39-
'SiteAndGroupProtectionBlockAccess',
40-
'Conditions', 'AdvancedSettings', 'Settings', 'LocaleSettings'
41-
)
19+
# Valid New-Label/Set-Label parameter names (single source of truth, shared with the template endpoint).
20+
$LabelAllowedFields = Get-CIPPSensitivityLabelField
4221
$PolicyAllowedFields = @(
4322
'Name', 'Comment', 'Labels', 'AdvancedSettings', 'Settings',
4423
'ExchangeLocation', 'ExchangeLocationException',
@@ -50,10 +29,20 @@ function Set-CIPPSensitivityLabel {
5029
$PolicyLocationFields = $PolicyAllowedFields | Where-Object { $_ -like '*Location*' }
5130
$LabelPolicyAddPrefixed = @('Labels') + $PolicyLocationFields
5231

53-
$LabelParams = Format-CIPPCompliancePolicyParams -Source $Template -AllowedFields $LabelAllowedFields
32+
# Normalize the read shape (Get-Label LabelActions) into the flat New-/Set-Label parameter shape.
33+
# Flat manual JSON authored against the deploy schema passes through unchanged.
34+
$NormalizedLabel = ConvertTo-CIPPSensitivityLabelParams -Label $Template
35+
$LabelParams = Format-CIPPCompliancePolicyParams -Source $NormalizedLabel -AllowedFields $LabelAllowedFields
5436
$PolicySource = $Template.PolicyParams
5537
$LabelName = $LabelParams.Name
5638

39+
# Priority is valid on Set-Label but not New-Label, so it is applied via a dedicated Set-Label call below.
40+
$LabelPriority = $null
41+
if ($LabelParams.ContainsKey('Priority')) {
42+
$LabelPriority = $LabelParams['Priority']
43+
$LabelParams.Remove('Priority')
44+
}
45+
5746
try {
5847
$ExistingLabels = try { New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-Label' -Compliance | Select-Object Name, DisplayName } catch { @() }
5948
$ExistingLabelPolicies = try { New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-LabelPolicy' -Compliance | Select-Object Name } catch { @() }
@@ -69,6 +58,17 @@ function Set-CIPPSensitivityLabel {
6958
$LabelAction = "Created sensitivity label '$LabelName' in $TenantFilter."
7059
}
7160

61+
# Priority is Set-Label only (not a New-Label parameter) and is tenant-relative: a value valid in the
62+
# source tenant can be out of range in the target. Apply it best-effort so an invalid priority never
63+
# masks an otherwise successful label deployment.
64+
if ($null -ne $LabelPriority) {
65+
try {
66+
$null = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Set-Label' -cmdParams @{ Identity = $LabelName; Priority = $LabelPriority } -Compliance -useSystemMailbox $true
67+
} catch {
68+
Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Deployed sensitivity label '$LabelName' but could not set priority $LabelPriority in $($TenantFilter): $($_.Exception.Message)" -sev Warning
69+
}
70+
}
71+
7272
if ($PolicySource) {
7373
$PolicyHash = Format-CIPPCompliancePolicyParams -Source $PolicySource -AllowedFields $PolicyAllowedFields
7474
if (-not $PolicyHash.ContainsKey('Labels') -or -not $PolicyHash['Labels']) {

0 commit comments

Comments
 (0)