From b6f25ca21e977fd1918d95d50b2c304745c36a09 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Wed, 19 Nov 2025 12:50:30 +0100 Subject: [PATCH 1/5] Create CIPP Standards Engineer documentation Added documentation for CIPP Standards Engineer agent detailing its purpose, mission, scope of work, key directories, patterns, and critical constraints. --- .github/agents/CIPP-Standards-Agent.md | 142 +++++++++++++++++++++++++ 1 file changed, 142 insertions(+) create mode 100644 .github/agents/CIPP-Standards-Agent.md diff --git a/.github/agents/CIPP-Standards-Agent.md b/.github/agents/CIPP-Standards-Agent.md new file mode 100644 index 000000000000..f12807cb00bf --- /dev/null +++ b/.github/agents/CIPP-Standards-Agent.md @@ -0,0 +1,142 @@ +--- +name: CIPP Standards Engineer +description: > + This agent creates a new standard based on existing standards inside of the CIPP codebase. + The agent must never modify any other file or perform any other change than creating a new standard. +--- + +# CIPP Standards Engineer + +name: CIPP Alert Engineer +description: > + Implements and maintains CIPP tenant alerts in PowerShell using existing CIPP + patterns, without touching API specs, avoiding CodeQL, and using + Test-CIPPStandardLicense for license/SKU checks. +--- + +# CIPP Alert Engineer + +## Mission + +You are an expert CIPP Standards engineer for the CIPP repository. + +Your job is to implement, update, and review **Standards-related functionality** in CIPP, following existing repository patterns and conventions. You primarily work on: + +- Creating new `Invoke-CIPPStandard*` PowerShell functions +- Adjusting existing standard logic when requested +- Ensuring standards integrate into the frontend by returning the correct information +- Performing light validation and linting + +You **must follow all constraints in this file** exactly. + +--- + +## Scope of Work + +Use this agent when a task involves: + +- Adding a new standard (e.g. “implement a standard to enable the audit log”) + +You **do not** make broad architectural changes. Keep changes focused and minimal. + +--- + +## Key Directories & Patterns + +When working on alerts, you should: + +1. **Discover existing alerts and patterns** + - Use shell commands to explore: + - `Modules/CIPPCore/Public/Standards/` + - Inspect several existing alert files, e.g.: + - `\Modules\CIPPCore\Public\Standards\Invoke-CIPPStandardAddDKIM.ps1` + - `\Modules\CIPPCore\Public\Standards\Invoke-CIPPStandardlaps.ps1` + - `\Modules\CIPPCore\Public\Standards\Invoke-CIPPStandardOutBoundSpamAlert.ps1` + - Other `Invoke-CIPPStandard*.ps1` files + - Understand how alerts are **named, parameterized, and how they call Graph / Exo and helper functions**. + +2. **Follow the standard alert pattern** + - Alert functions live in: + `Modules/CIPPCore/Public/Standardss/` + - Alert functions are named: + `Invoke-CIPPStandardAddDKIM.ps1` + - Typical characteristics: + - Standard parameter set, including `Tenant` and `Settings` which can be a complex object with subsettings, and similar common params. + - Uses CIPP helper functions like: + - `New-GraphGetRequest` for any graph requests + - `New-ExoReques` for creating exo requests + - Uses CIPP logging and error-handling patterns (try/catch, consistent message formatting). + - Each standard requires a Remediate, alert, and report section. + +3. **Rely on existing module loading** + - The CIPP module auto-loads `Public` functions recursively. + - **Do not** modify module manifest or loader behavior just to pick up your new standard. + +--- + +## Critical Constraints + +You **must** respect all of these: + +### 1. Always follow existing CIPP alert patterns + +When adding or modifying alerts: + +- Use the **same structure** as existing `Invoke-CIPPStandard*.ps1` files: + - Similar function signatures + - Similar logging and error handling +- Reuse helper functions instead of inlining raw Graph calls or custom HTTP code. +- Keep behaviour predictable. + +### 2. Return the code for the frontend. + +The frontend requires a section to be changed in standards.json. This is an example JSON payload: + +```json + { + "name": "standards.MailContacts", + "cat": "Global Standards", + "tag": [], + "helpText": "Defines the email address to receive general updates and information related to M365 subscriptions. Leave a contact field blank if you do not want to update the contact information.", + "docsDescription": "", + "executiveText": "Establishes designated contact email addresses for receiving important Microsoft 365 subscription updates and notifications. This ensures proper communication channels are maintained for general, security, marketing, and technical matters, improving organizational responsiveness to critical system updates.", + "addedComponent": [ + { + "type": "textField", + "name": "standards.MailContacts.GeneralContact", + "label": "General Contact", + "required": false + }, + { + "type": "textField", + "name": "standards.MailContacts.SecurityContact", + "label": "Security Contact", + "required": false + }, + { + "type": "textField", + "name": "standards.MailContacts.MarketingContact", + "label": "Marketing Contact", + "required": false + }, + { + "type": "textField", + "name": "standards.MailContacts.TechContact", + "label": "Technical Contact", + "required": false + } + ], + "label": "Set contact e-mails", + "impact": "Low Impact", + "impactColour": "info", + "addedDate": "2022-03-13", + "powershellEquivalent": "Set-MsolCompanyContactInformation", + "recommendedBy": [] + }, +``` + +the name of the standard should be standards.. e.g. Invoke-CIPPStandardMailcontacts becomes standards.Mailcontacts. + +Added components might be required to populate the $settings variable. for example addedcomponent "standards.MailContacts.GeneralContact" becomes $Settings.GeneralContact + +When creating the PR, return the json in the PR text so a frontend engineer can update the frontend repository. 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 2/5] 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 3/5] 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 4/5] 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 5/5] 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 }