Skip to content

Commit 74b49ca

Browse files
Add Invoke-CIPPStandardSecureScoreRemediation.ps1
Co-authored-by: KelvinTegelaar <[email protected]>
1 parent a65875b commit 74b49ca

File tree

1 file changed

+176
-0
lines changed

1 file changed

+176
-0
lines changed
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
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. Configure controls as resolved, ignored, or third-party mitigated to accurately reflect your security posture.
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 mitigation" (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":"input","name":"standards.SecureScoreRemediation.Controls","label":"Control Updates (JSON array)","placeholder":"[{\"ControlName\":\"example\",\"State\":\"thirdPartyMitigation\",\"Reason\":\"Using third-party tool\"}]"}
21+
IMPACT
22+
Low Impact
23+
ADDEDDATE
24+
2025-11-19
25+
POWERSHELLEQUIVALENT
26+
New-GraphPostRequest to /beta/security/secureScoreControlProfiles/{id}
27+
RECOMMENDEDBY
28+
UPDATECOMMENTBLOCK
29+
Run the Tools\Update-StandardsComments.ps1 script to update this comment block
30+
.LINK
31+
https://docs.cipp.app/user-documentation/tenant/standards/list-standards
32+
#>
33+
34+
param($Tenant, $Settings)
35+
36+
# Validate that Controls array exists and is not empty
37+
if (-not $Settings.Controls -or $Settings.Controls.Count -eq 0) {
38+
Write-LogMessage -API 'Standards' -tenant $tenant -message 'No controls specified for Secure Score remediation. Skipping.' -sev Info
39+
return
40+
}
41+
42+
# Process controls from settings
43+
# Settings.Controls should be an array of objects with ControlName, State, Reason, and optionally VendorInformation
44+
$Controls = $Settings.Controls
45+
if ($Controls -is [string]) {
46+
try {
47+
$Controls = $Controls | ConvertFrom-Json
48+
} catch {
49+
$ErrorMessage = Get-NormalizedError -Message $_.Exception.Message
50+
Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to parse Controls JSON: $ErrorMessage" -sev Error
51+
return
52+
}
53+
}
54+
55+
# Get current secure score controls
56+
try {
57+
$CurrentControls = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/security/secureScoreControlProfiles' -tenantid $Tenant
58+
} catch {
59+
$ErrorMessage = Get-NormalizedError -Message $_.Exception.Message
60+
Write-LogMessage -API 'Standards' -tenant $Tenant -message "Could not retrieve Secure Score controls for $Tenant. Error: $ErrorMessage" -sev Error
61+
return
62+
}
63+
64+
if ($Settings.remediate -eq $true) {
65+
Write-Host 'Processing Secure Score control updates'
66+
67+
foreach ($Control in $Controls) {
68+
# Skip if this is a Defender control (starts with scid_)
69+
if ($Control.ControlName -match '^scid_') {
70+
Write-LogMessage -API 'Standards' -tenant $tenant -message "Skipping Defender control $($Control.ControlName) - cannot be updated via this API" -sev Info
71+
continue
72+
}
73+
74+
# Validate required fields
75+
if (-not $Control.ControlName -or -not $Control.State) {
76+
Write-LogMessage -API 'Standards' -tenant $tenant -message "Skipping control update - ControlName and State are required" -sev Warning
77+
continue
78+
}
79+
80+
# Build the request body
81+
$Body = @{
82+
state = $Control.State
83+
}
84+
85+
if ($Control.Reason) {
86+
$Body.comment = $Control.Reason
87+
}
88+
89+
if ($Control.VendorInformation) {
90+
$Body.vendorInformation = $Control.VendorInformation
91+
}
92+
93+
try {
94+
$CurrentControl = $CurrentControls | Where-Object { $_.id -eq $Control.ControlName }
95+
96+
if (-not $CurrentControl) {
97+
Write-LogMessage -API 'Standards' -tenant $tenant -message "Control $($Control.ControlName) not found in tenant" -sev Warning
98+
continue
99+
}
100+
101+
# Check if already in desired state
102+
if ($CurrentControl.state -eq $Control.State) {
103+
Write-LogMessage -API 'Standards' -tenant $tenant -message "Control $($Control.ControlName) is already in state $($Control.State)" -sev Info
104+
} else {
105+
# Update the control
106+
$null = New-GraphPostRequest -uri "https://graph.microsoft.com/beta/security/secureScoreControlProfiles/$($Control.ControlName)" -tenantid $Tenant -type PATCH -Body (ConvertTo-Json -InputObject $Body -Compress)
107+
Write-LogMessage -API 'Standards' -tenant $tenant -message "Successfully set control $($Control.ControlName) to $($Control.State)" -sev Info
108+
}
109+
} catch {
110+
$ErrorMessage = Get-NormalizedError -Message $_.Exception.Message
111+
Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to set control $($Control.ControlName) to $($Control.State). Error: $ErrorMessage" -sev Error
112+
}
113+
}
114+
}
115+
116+
if ($Settings.alert -eq $true) {
117+
$AlertMessages = @()
118+
119+
foreach ($Control in $Controls) {
120+
if ($Control.ControlName -match '^scid_') {
121+
continue
122+
}
123+
124+
$CurrentControl = $CurrentControls | Where-Object { $_.id -eq $Control.ControlName }
125+
126+
if ($CurrentControl) {
127+
if ($CurrentControl.state -eq $Control.State) {
128+
Write-LogMessage -API 'Standards' -tenant $tenant -message "Control $($Control.ControlName) is in expected state: $($Control.State)" -sev Info
129+
} else {
130+
$AlertMessage = "Control $($Control.ControlName) is in state $($CurrentControl.state), expected $($Control.State)"
131+
$AlertMessages += $AlertMessage
132+
Write-LogMessage -API 'Standards' -tenant $tenant -message $AlertMessage -sev Alert
133+
}
134+
} else {
135+
$AlertMessage = "Control $($Control.ControlName) not found in tenant"
136+
$AlertMessages += $AlertMessage
137+
Write-LogMessage -API 'Standards' -tenant $tenant -message $AlertMessage -sev Warning
138+
}
139+
}
140+
141+
if ($AlertMessages.Count -gt 0) {
142+
Write-StandardsAlert -message "Secure Score controls not in expected state" -object @{Issues = $AlertMessages} -tenant $Tenant -standardName 'SecureScoreRemediation' -standardId $Settings.standardId
143+
}
144+
}
145+
146+
if ($Settings.report -eq $true) {
147+
$ReportData = @()
148+
149+
foreach ($Control in $Controls) {
150+
if ($Control.ControlName -match '^scid_') {
151+
continue
152+
}
153+
154+
$CurrentControl = $CurrentControls | Where-Object { $_.id -eq $Control.ControlName }
155+
156+
if ($CurrentControl) {
157+
$ReportData += @{
158+
ControlName = $Control.ControlName
159+
CurrentState = $CurrentControl.state
160+
DesiredState = $Control.State
161+
InCompliance = ($CurrentControl.state -eq $Control.State)
162+
}
163+
} else {
164+
$ReportData += @{
165+
ControlName = $Control.ControlName
166+
CurrentState = 'Not Found'
167+
DesiredState = $Control.State
168+
InCompliance = $false
169+
}
170+
}
171+
}
172+
173+
Set-CIPPStandardsCompareField -FieldName 'standards.SecureScoreRemediation' -FieldValue $ReportData -Tenant $tenant
174+
Add-CIPPBPAField -FieldName 'SecureScoreRemediation' -FieldValue $ReportData -StoreAs json -Tenant $tenant
175+
}
176+
}

0 commit comments

Comments
 (0)