From a65875ba4612966cd7d8d72dc11b8fd925a93abc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 19 Nov 2025 11:55:02 +0000 Subject: [PATCH 1/4] Initial plan From 74b49ca7849a52fbbeca02c8cf1215f5a1bab2e3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 19 Nov 2025 11:59:02 +0000 Subject: [PATCH 2/4] Add Invoke-CIPPStandardSecureScoreRemediation.ps1 Co-authored-by: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> --- ...oke-CIPPStandardSecureScoreRemediation.ps1 | 176 ++++++++++++++++++ 1 file changed, 176 insertions(+) create mode 100644 Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSecureScoreRemediation.ps1 diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSecureScoreRemediation.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSecureScoreRemediation.ps1 new file mode 100644 index 000000000000..74c3c262ba7e --- /dev/null +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSecureScoreRemediation.ps1 @@ -0,0 +1,176 @@ +function Invoke-CIPPStandardSecureScoreRemediation { + <# + .FUNCTIONALITY + Internal + .COMPONENT + (APIName) SecureScoreRemediation + .SYNOPSIS + (Label) Update Secure Score Control Profiles + .DESCRIPTION + (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. + (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. + .NOTES + CAT + Global Standards + TAG + "lowimpact" + EXECUTIVETEXT + 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. + ADDEDCOMPONENT + {"type":"input","name":"standards.SecureScoreRemediation.Controls","label":"Control Updates (JSON array)","placeholder":"[{\"ControlName\":\"example\",\"State\":\"thirdPartyMitigation\",\"Reason\":\"Using third-party tool\"}]"} + IMPACT + Low Impact + ADDEDDATE + 2025-11-19 + POWERSHELLEQUIVALENT + New-GraphPostRequest to /beta/security/secureScoreControlProfiles/{id} + RECOMMENDEDBY + UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block + .LINK + https://docs.cipp.app/user-documentation/tenant/standards/list-standards + #> + + param($Tenant, $Settings) + + # Validate that Controls array exists and is not empty + if (-not $Settings.Controls -or $Settings.Controls.Count -eq 0) { + Write-LogMessage -API 'Standards' -tenant $tenant -message 'No controls specified for Secure Score remediation. Skipping.' -sev Info + return + } + + # Process controls from settings + # Settings.Controls should be an array of objects with ControlName, State, Reason, and optionally VendorInformation + $Controls = $Settings.Controls + if ($Controls -is [string]) { + try { + $Controls = $Controls | ConvertFrom-Json + } catch { + $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message + Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to parse Controls JSON: $ErrorMessage" -sev Error + return + } + } + + # Get current secure score controls + try { + $CurrentControls = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/security/secureScoreControlProfiles' -tenantid $Tenant + } catch { + $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Could not retrieve Secure Score controls for $Tenant. Error: $ErrorMessage" -sev Error + return + } + + if ($Settings.remediate -eq $true) { + Write-Host 'Processing Secure Score control updates' + + foreach ($Control in $Controls) { + # Skip if this is a Defender control (starts with scid_) + if ($Control.ControlName -match '^scid_') { + Write-LogMessage -API 'Standards' -tenant $tenant -message "Skipping Defender control $($Control.ControlName) - cannot be updated via this API" -sev Info + continue + } + + # Validate required fields + if (-not $Control.ControlName -or -not $Control.State) { + Write-LogMessage -API 'Standards' -tenant $tenant -message "Skipping control update - ControlName and State are required" -sev Warning + continue + } + + # Build the request body + $Body = @{ + state = $Control.State + } + + if ($Control.Reason) { + $Body.comment = $Control.Reason + } + + if ($Control.VendorInformation) { + $Body.vendorInformation = $Control.VendorInformation + } + + try { + $CurrentControl = $CurrentControls | Where-Object { $_.id -eq $Control.ControlName } + + if (-not $CurrentControl) { + Write-LogMessage -API 'Standards' -tenant $tenant -message "Control $($Control.ControlName) not found in tenant" -sev Warning + continue + } + + # Check if already in desired state + if ($CurrentControl.state -eq $Control.State) { + Write-LogMessage -API 'Standards' -tenant $tenant -message "Control $($Control.ControlName) is already in state $($Control.State)" -sev Info + } else { + # Update the control + $null = New-GraphPostRequest -uri "https://graph.microsoft.com/beta/security/secureScoreControlProfiles/$($Control.ControlName)" -tenantid $Tenant -type PATCH -Body (ConvertTo-Json -InputObject $Body -Compress) + Write-LogMessage -API 'Standards' -tenant $tenant -message "Successfully set control $($Control.ControlName) to $($Control.State)" -sev Info + } + } catch { + $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message + Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to set control $($Control.ControlName) to $($Control.State). Error: $ErrorMessage" -sev Error + } + } + } + + if ($Settings.alert -eq $true) { + $AlertMessages = @() + + foreach ($Control in $Controls) { + if ($Control.ControlName -match '^scid_') { + continue + } + + $CurrentControl = $CurrentControls | Where-Object { $_.id -eq $Control.ControlName } + + if ($CurrentControl) { + if ($CurrentControl.state -eq $Control.State) { + Write-LogMessage -API 'Standards' -tenant $tenant -message "Control $($Control.ControlName) is in expected state: $($Control.State)" -sev Info + } else { + $AlertMessage = "Control $($Control.ControlName) is in state $($CurrentControl.state), expected $($Control.State)" + $AlertMessages += $AlertMessage + Write-LogMessage -API 'Standards' -tenant $tenant -message $AlertMessage -sev Alert + } + } else { + $AlertMessage = "Control $($Control.ControlName) not found in tenant" + $AlertMessages += $AlertMessage + Write-LogMessage -API 'Standards' -tenant $tenant -message $AlertMessage -sev Warning + } + } + + if ($AlertMessages.Count -gt 0) { + Write-StandardsAlert -message "Secure Score controls not in expected state" -object @{Issues = $AlertMessages} -tenant $Tenant -standardName 'SecureScoreRemediation' -standardId $Settings.standardId + } + } + + if ($Settings.report -eq $true) { + $ReportData = @() + + foreach ($Control in $Controls) { + if ($Control.ControlName -match '^scid_') { + continue + } + + $CurrentControl = $CurrentControls | Where-Object { $_.id -eq $Control.ControlName } + + if ($CurrentControl) { + $ReportData += @{ + ControlName = $Control.ControlName + CurrentState = $CurrentControl.state + DesiredState = $Control.State + InCompliance = ($CurrentControl.state -eq $Control.State) + } + } else { + $ReportData += @{ + ControlName = $Control.ControlName + CurrentState = 'Not Found' + DesiredState = $Control.State + InCompliance = $false + } + } + } + + Set-CIPPStandardsCompareField -FieldName 'standards.SecureScoreRemediation' -FieldValue $ReportData -Tenant $tenant + Add-CIPPBPAField -FieldName 'SecureScoreRemediation' -FieldValue $ReportData -StoreAs json -Tenant $tenant + } +} From 9914b85fdeed812e16d24d40cec9fa3d7a3dfa23 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 19 Nov 2025 12:04:50 +0000 Subject: [PATCH 3/4] Fix SecureScoreRemediation to follow CIPP patterns - remove +=, simplify validation Co-authored-by: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> --- ...oke-CIPPStandardSecureScoreRemediation.ps1 | 58 +++++-------------- 1 file changed, 14 insertions(+), 44 deletions(-) diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSecureScoreRemediation.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSecureScoreRemediation.ps1 index 74c3c262ba7e..5cae64a2f7c4 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSecureScoreRemediation.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSecureScoreRemediation.ps1 @@ -33,25 +33,6 @@ function Invoke-CIPPStandardSecureScoreRemediation { param($Tenant, $Settings) - # Validate that Controls array exists and is not empty - if (-not $Settings.Controls -or $Settings.Controls.Count -eq 0) { - Write-LogMessage -API 'Standards' -tenant $tenant -message 'No controls specified for Secure Score remediation. Skipping.' -sev Info - return - } - - # Process controls from settings - # Settings.Controls should be an array of objects with ControlName, State, Reason, and optionally VendorInformation - $Controls = $Settings.Controls - if ($Controls -is [string]) { - try { - $Controls = $Controls | ConvertFrom-Json - } catch { - $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message - Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to parse Controls JSON: $ErrorMessage" -sev Error - return - } - } - # Get current secure score controls try { $CurrentControls = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/security/secureScoreControlProfiles' -tenantid $Tenant @@ -64,19 +45,13 @@ function Invoke-CIPPStandardSecureScoreRemediation { if ($Settings.remediate -eq $true) { Write-Host 'Processing Secure Score control updates' - foreach ($Control in $Controls) { + foreach ($Control in $Settings.Controls) { # Skip if this is a Defender control (starts with scid_) if ($Control.ControlName -match '^scid_') { Write-LogMessage -API 'Standards' -tenant $tenant -message "Skipping Defender control $($Control.ControlName) - cannot be updated via this API" -sev Info continue } - # Validate required fields - if (-not $Control.ControlName -or -not $Control.State) { - Write-LogMessage -API 'Standards' -tenant $tenant -message "Skipping control update - ControlName and State are required" -sev Warning - continue - } - # Build the request body $Body = @{ state = $Control.State @@ -92,11 +67,6 @@ function Invoke-CIPPStandardSecureScoreRemediation { try { $CurrentControl = $CurrentControls | Where-Object { $_.id -eq $Control.ControlName } - - if (-not $CurrentControl) { - Write-LogMessage -API 'Standards' -tenant $tenant -message "Control $($Control.ControlName) not found in tenant" -sev Warning - continue - } # Check if already in desired state if ($CurrentControl.state -eq $Control.State) { @@ -114,9 +84,9 @@ function Invoke-CIPPStandardSecureScoreRemediation { } if ($Settings.alert -eq $true) { - $AlertMessages = @() + $AlertMessages = [System.Collections.Generic.List[string]]::new() - foreach ($Control in $Controls) { + foreach ($Control in $Settings.Controls) { if ($Control.ControlName -match '^scid_') { continue } @@ -128,25 +98,25 @@ function Invoke-CIPPStandardSecureScoreRemediation { Write-LogMessage -API 'Standards' -tenant $tenant -message "Control $($Control.ControlName) is in expected state: $($Control.State)" -sev Info } else { $AlertMessage = "Control $($Control.ControlName) is in state $($CurrentControl.state), expected $($Control.State)" - $AlertMessages += $AlertMessage + $AlertMessages.Add($AlertMessage) Write-LogMessage -API 'Standards' -tenant $tenant -message $AlertMessage -sev Alert } } else { $AlertMessage = "Control $($Control.ControlName) not found in tenant" - $AlertMessages += $AlertMessage + $AlertMessages.Add($AlertMessage) Write-LogMessage -API 'Standards' -tenant $tenant -message $AlertMessage -sev Warning } } if ($AlertMessages.Count -gt 0) { - Write-StandardsAlert -message "Secure Score controls not in expected state" -object @{Issues = $AlertMessages} -tenant $Tenant -standardName 'SecureScoreRemediation' -standardId $Settings.standardId + Write-StandardsAlert -message "Secure Score controls not in expected state" -object @{Issues = $AlertMessages.ToArray()} -tenant $Tenant -standardName 'SecureScoreRemediation' -standardId $Settings.standardId } } if ($Settings.report -eq $true) { - $ReportData = @() + $ReportData = [System.Collections.Generic.List[object]]::new() - foreach ($Control in $Controls) { + foreach ($Control in $Settings.Controls) { if ($Control.ControlName -match '^scid_') { continue } @@ -154,23 +124,23 @@ function Invoke-CIPPStandardSecureScoreRemediation { $CurrentControl = $CurrentControls | Where-Object { $_.id -eq $Control.ControlName } if ($CurrentControl) { - $ReportData += @{ + $ReportData.Add(@{ ControlName = $Control.ControlName CurrentState = $CurrentControl.state DesiredState = $Control.State InCompliance = ($CurrentControl.state -eq $Control.State) - } + }) } else { - $ReportData += @{ + $ReportData.Add(@{ ControlName = $Control.ControlName CurrentState = 'Not Found' DesiredState = $Control.State InCompliance = $false - } + }) } } - Set-CIPPStandardsCompareField -FieldName 'standards.SecureScoreRemediation' -FieldValue $ReportData -Tenant $tenant - Add-CIPPBPAField -FieldName 'SecureScoreRemediation' -FieldValue $ReportData -StoreAs json -Tenant $tenant + Set-CIPPStandardsCompareField -FieldName 'standards.SecureScoreRemediation' -FieldValue $ReportData.ToArray() -Tenant $tenant + Add-CIPPBPAField -FieldName 'SecureScoreRemediation' -FieldValue $ReportData.ToArray() -StoreAs json -Tenant $tenant } } From 0763560be652f9d731a5fecf7910f1e69fd1cc47 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 19 Nov 2025 12:18:33 +0000 Subject: [PATCH 4/4] Refactor SecureScoreRemediation to use 4 separate autocomplete fields per feedback Co-authored-by: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> --- ...oke-CIPPStandardSecureScoreRemediation.ps1 | 76 +++++++++++++++---- 1 file changed, 62 insertions(+), 14 deletions(-) diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSecureScoreRemediation.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSecureScoreRemediation.ps1 index 5cae64a2f7c4..133b2123f019 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSecureScoreRemediation.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSecureScoreRemediation.ps1 @@ -7,8 +7,8 @@ function Invoke-CIPPStandardSecureScoreRemediation { .SYNOPSIS (Label) Update Secure Score Control Profiles .DESCRIPTION - (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. - (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. + (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. + (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. .NOTES CAT Global Standards @@ -17,7 +17,10 @@ function Invoke-CIPPStandardSecureScoreRemediation { EXECUTIVETEXT 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. ADDEDCOMPONENT - {"type":"input","name":"standards.SecureScoreRemediation.Controls","label":"Control Updates (JSON array)","placeholder":"[{\"ControlName\":\"example\",\"State\":\"thirdPartyMitigation\",\"Reason\":\"Using third-party tool\"}]"} + {"type":"autoComplete","multiple":true,"creatable":true,"name":"standards.SecureScoreRemediation.Default","label":"Controls to set to Default"} + {"type":"autoComplete","multiple":true,"creatable":true,"name":"standards.SecureScoreRemediation.Ignored","label":"Controls to set to Ignored"} + {"type":"autoComplete","multiple":true,"creatable":true,"name":"standards.SecureScoreRemediation.ThirdParty","label":"Controls to set to Third-Party"} + {"type":"autoComplete","multiple":true,"creatable":true,"name":"standards.SecureScoreRemediation.Reviewed","label":"Controls to set to Reviewed"} IMPACT Low Impact ADDEDDATE @@ -32,6 +35,7 @@ function Invoke-CIPPStandardSecureScoreRemediation { #> param($Tenant, $Settings) + # Get current secure score controls try { @@ -42,10 +46,61 @@ function Invoke-CIPPStandardSecureScoreRemediation { return } + # Build list of controls with their desired states + $ControlsToUpdate = [System.Collections.Generic.List[object]]::new() + + # Process Default controls + $DefaultControls = $Settings.Default.value ?? $Settings.Default + if ($DefaultControls) { + foreach ($ControlName in $DefaultControls) { + $ControlsToUpdate.Add(@{ + ControlName = $ControlName + State = 'default' + Reason = 'Default' + }) + } + } + + # Process Ignored controls + $IgnoredControls = $Settings.Ignored.value ?? $Settings.Ignored + if ($IgnoredControls) { + foreach ($ControlName in $IgnoredControls) { + $ControlsToUpdate.Add(@{ + ControlName = $ControlName + State = 'ignored' + Reason = 'Ignored' + }) + } + } + + # Process ThirdParty controls + $ThirdPartyControls = $Settings.ThirdParty.value ?? $Settings.ThirdParty + if ($ThirdPartyControls) { + foreach ($ControlName in $ThirdPartyControls) { + $ControlsToUpdate.Add(@{ + ControlName = $ControlName + State = 'thirdParty' + Reason = 'ThirdParty' + }) + } + } + + # Process Reviewed controls + $ReviewedControls = $Settings.Reviewed.value ?? $Settings.Reviewed + if ($ReviewedControls) { + foreach ($ControlName in $ReviewedControls) { + $ControlsToUpdate.Add(@{ + ControlName = $ControlName + State = 'reviewed' + Reason = 'Reviewed' + }) + } + } + if ($Settings.remediate -eq $true) { Write-Host 'Processing Secure Score control updates' - foreach ($Control in $Settings.Controls) { + foreach ($Control in $ControlsToUpdate) { # Skip if this is a Defender control (starts with scid_) if ($Control.ControlName -match '^scid_') { Write-LogMessage -API 'Standards' -tenant $tenant -message "Skipping Defender control $($Control.ControlName) - cannot be updated via this API" -sev Info @@ -55,14 +110,7 @@ function Invoke-CIPPStandardSecureScoreRemediation { # Build the request body $Body = @{ state = $Control.State - } - - if ($Control.Reason) { - $Body.comment = $Control.Reason - } - - if ($Control.VendorInformation) { - $Body.vendorInformation = $Control.VendorInformation + comment = $Control.Reason } try { @@ -86,7 +134,7 @@ function Invoke-CIPPStandardSecureScoreRemediation { if ($Settings.alert -eq $true) { $AlertMessages = [System.Collections.Generic.List[string]]::new() - foreach ($Control in $Settings.Controls) { + foreach ($Control in $ControlsToUpdate) { if ($Control.ControlName -match '^scid_') { continue } @@ -116,7 +164,7 @@ function Invoke-CIPPStandardSecureScoreRemediation { if ($Settings.report -eq $true) { $ReportData = [System.Collections.Generic.List[object]]::new() - foreach ($Control in $Settings.Controls) { + foreach ($Control in $ControlsToUpdate) { if ($Control.ControlName -match '^scid_') { continue }