Skip to content

Commit bbf203d

Browse files
Merge pull request KelvinTegelaar#1711 from KelvinTegelaar/copilot/add-secure-score-automation
Add SecureScoreRemediation standard for bulk Secure Score control updates
2 parents b6f25ca + 0763560 commit bbf203d

File tree

1 file changed

+194
-0
lines changed

1 file changed

+194
-0
lines changed
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
function Invoke-CIPPStandardSecureScoreRemediation {
2+
<#
3+
.FUNCTIONALITY
4+
Internal
5+
.COMPONENT
6+
(APIName) SecureScoreRemediation
7+
.SYNOPSIS
8+
(Label) Update Secure Score Control Profiles
9+
.DESCRIPTION
10+
(Helptext) Allows bulk updating of Secure Score control profiles across tenants. Select controls and assign them to different states: Default, Ignored, Third-Party, or Reviewed.
11+
(DocsDescription) Enables automated or template-based updates to Microsoft Secure Score recommendations. This is particularly useful for MSPs managing multiple tenants, allowing you to mark controls as "Third-party" (e.g., when using Mimecast, IronScales, or other third-party security tools) or set them to other states in bulk. This ensures Secure Scores accurately reflect each tenant's true security posture without repetitive manual updates.
12+
.NOTES
13+
CAT
14+
Global Standards
15+
TAG
16+
"lowimpact"
17+
EXECUTIVETEXT
18+
Automates the management of Secure Score control profiles by allowing bulk updates across tenants. This ensures accurate representation of security posture when using third-party security tools or when certain controls need to be marked as resolved or ignored, significantly reducing manual administrative overhead for MSPs managing multiple clients.
19+
ADDEDCOMPONENT
20+
{"type":"autoComplete","multiple":true,"creatable":true,"name":"standards.SecureScoreRemediation.Default","label":"Controls to set to Default"}
21+
{"type":"autoComplete","multiple":true,"creatable":true,"name":"standards.SecureScoreRemediation.Ignored","label":"Controls to set to Ignored"}
22+
{"type":"autoComplete","multiple":true,"creatable":true,"name":"standards.SecureScoreRemediation.ThirdParty","label":"Controls to set to Third-Party"}
23+
{"type":"autoComplete","multiple":true,"creatable":true,"name":"standards.SecureScoreRemediation.Reviewed","label":"Controls to set to Reviewed"}
24+
IMPACT
25+
Low Impact
26+
ADDEDDATE
27+
2025-11-19
28+
POWERSHELLEQUIVALENT
29+
New-GraphPostRequest to /beta/security/secureScoreControlProfiles/{id}
30+
RECOMMENDEDBY
31+
UPDATECOMMENTBLOCK
32+
Run the Tools\Update-StandardsComments.ps1 script to update this comment block
33+
.LINK
34+
https://docs.cipp.app/user-documentation/tenant/standards/list-standards
35+
#>
36+
37+
param($Tenant, $Settings)
38+
39+
40+
# Get current secure score controls
41+
try {
42+
$CurrentControls = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/security/secureScoreControlProfiles' -tenantid $Tenant
43+
} catch {
44+
$ErrorMessage = Get-NormalizedError -Message $_.Exception.Message
45+
Write-LogMessage -API 'Standards' -tenant $Tenant -message "Could not retrieve Secure Score controls for $Tenant. Error: $ErrorMessage" -sev Error
46+
return
47+
}
48+
49+
# Build list of controls with their desired states
50+
$ControlsToUpdate = [System.Collections.Generic.List[object]]::new()
51+
52+
# Process Default controls
53+
$DefaultControls = $Settings.Default.value ?? $Settings.Default
54+
if ($DefaultControls) {
55+
foreach ($ControlName in $DefaultControls) {
56+
$ControlsToUpdate.Add(@{
57+
ControlName = $ControlName
58+
State = 'default'
59+
Reason = 'Default'
60+
})
61+
}
62+
}
63+
64+
# Process Ignored controls
65+
$IgnoredControls = $Settings.Ignored.value ?? $Settings.Ignored
66+
if ($IgnoredControls) {
67+
foreach ($ControlName in $IgnoredControls) {
68+
$ControlsToUpdate.Add(@{
69+
ControlName = $ControlName
70+
State = 'ignored'
71+
Reason = 'Ignored'
72+
})
73+
}
74+
}
75+
76+
# Process ThirdParty controls
77+
$ThirdPartyControls = $Settings.ThirdParty.value ?? $Settings.ThirdParty
78+
if ($ThirdPartyControls) {
79+
foreach ($ControlName in $ThirdPartyControls) {
80+
$ControlsToUpdate.Add(@{
81+
ControlName = $ControlName
82+
State = 'thirdParty'
83+
Reason = 'ThirdParty'
84+
})
85+
}
86+
}
87+
88+
# Process Reviewed controls
89+
$ReviewedControls = $Settings.Reviewed.value ?? $Settings.Reviewed
90+
if ($ReviewedControls) {
91+
foreach ($ControlName in $ReviewedControls) {
92+
$ControlsToUpdate.Add(@{
93+
ControlName = $ControlName
94+
State = 'reviewed'
95+
Reason = 'Reviewed'
96+
})
97+
}
98+
}
99+
100+
if ($Settings.remediate -eq $true) {
101+
Write-Host 'Processing Secure Score control updates'
102+
103+
foreach ($Control in $ControlsToUpdate) {
104+
# Skip if this is a Defender control (starts with scid_)
105+
if ($Control.ControlName -match '^scid_') {
106+
Write-LogMessage -API 'Standards' -tenant $tenant -message "Skipping Defender control $($Control.ControlName) - cannot be updated via this API" -sev Info
107+
continue
108+
}
109+
110+
# Build the request body
111+
$Body = @{
112+
state = $Control.State
113+
comment = $Control.Reason
114+
}
115+
116+
try {
117+
$CurrentControl = $CurrentControls | Where-Object { $_.id -eq $Control.ControlName }
118+
119+
# Check if already in desired state
120+
if ($CurrentControl.state -eq $Control.State) {
121+
Write-LogMessage -API 'Standards' -tenant $tenant -message "Control $($Control.ControlName) is already in state $($Control.State)" -sev Info
122+
} else {
123+
# Update the control
124+
$null = New-GraphPostRequest -uri "https://graph.microsoft.com/beta/security/secureScoreControlProfiles/$($Control.ControlName)" -tenantid $Tenant -type PATCH -Body (ConvertTo-Json -InputObject $Body -Compress)
125+
Write-LogMessage -API 'Standards' -tenant $tenant -message "Successfully set control $($Control.ControlName) to $($Control.State)" -sev Info
126+
}
127+
} catch {
128+
$ErrorMessage = Get-NormalizedError -Message $_.Exception.Message
129+
Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to set control $($Control.ControlName) to $($Control.State). Error: $ErrorMessage" -sev Error
130+
}
131+
}
132+
}
133+
134+
if ($Settings.alert -eq $true) {
135+
$AlertMessages = [System.Collections.Generic.List[string]]::new()
136+
137+
foreach ($Control in $ControlsToUpdate) {
138+
if ($Control.ControlName -match '^scid_') {
139+
continue
140+
}
141+
142+
$CurrentControl = $CurrentControls | Where-Object { $_.id -eq $Control.ControlName }
143+
144+
if ($CurrentControl) {
145+
if ($CurrentControl.state -eq $Control.State) {
146+
Write-LogMessage -API 'Standards' -tenant $tenant -message "Control $($Control.ControlName) is in expected state: $($Control.State)" -sev Info
147+
} else {
148+
$AlertMessage = "Control $($Control.ControlName) is in state $($CurrentControl.state), expected $($Control.State)"
149+
$AlertMessages.Add($AlertMessage)
150+
Write-LogMessage -API 'Standards' -tenant $tenant -message $AlertMessage -sev Alert
151+
}
152+
} else {
153+
$AlertMessage = "Control $($Control.ControlName) not found in tenant"
154+
$AlertMessages.Add($AlertMessage)
155+
Write-LogMessage -API 'Standards' -tenant $tenant -message $AlertMessage -sev Warning
156+
}
157+
}
158+
159+
if ($AlertMessages.Count -gt 0) {
160+
Write-StandardsAlert -message "Secure Score controls not in expected state" -object @{Issues = $AlertMessages.ToArray()} -tenant $Tenant -standardName 'SecureScoreRemediation' -standardId $Settings.standardId
161+
}
162+
}
163+
164+
if ($Settings.report -eq $true) {
165+
$ReportData = [System.Collections.Generic.List[object]]::new()
166+
167+
foreach ($Control in $ControlsToUpdate) {
168+
if ($Control.ControlName -match '^scid_') {
169+
continue
170+
}
171+
172+
$CurrentControl = $CurrentControls | Where-Object { $_.id -eq $Control.ControlName }
173+
174+
if ($CurrentControl) {
175+
$ReportData.Add(@{
176+
ControlName = $Control.ControlName
177+
CurrentState = $CurrentControl.state
178+
DesiredState = $Control.State
179+
InCompliance = ($CurrentControl.state -eq $Control.State)
180+
})
181+
} else {
182+
$ReportData.Add(@{
183+
ControlName = $Control.ControlName
184+
CurrentState = 'Not Found'
185+
DesiredState = $Control.State
186+
InCompliance = $false
187+
})
188+
}
189+
}
190+
191+
Set-CIPPStandardsCompareField -FieldName 'standards.SecureScoreRemediation' -FieldValue $ReportData.ToArray() -Tenant $tenant
192+
Add-CIPPBPAField -FieldName 'SecureScoreRemediation' -FieldValue $ReportData.ToArray() -StoreAs json -Tenant $tenant
193+
}
194+
}

0 commit comments

Comments
 (0)