Skip to content

Commit 3136786

Browse files
authored
Merge pull request #42 from KelvinTegelaar/master
[pull] master from KelvinTegelaar:master
2 parents 2fbc676 + bbf203d commit 3136786

File tree

2 files changed

+336
-0
lines changed

2 files changed

+336
-0
lines changed
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
---
2+
name: CIPP Standards Engineer
3+
description: >
4+
This agent creates a new standard based on existing standards inside of the CIPP codebase.
5+
The agent must never modify any other file or perform any other change than creating a new standard.
6+
---
7+
8+
# CIPP Standards Engineer
9+
10+
name: CIPP Alert Engineer
11+
description: >
12+
Implements and maintains CIPP tenant alerts in PowerShell using existing CIPP
13+
patterns, without touching API specs, avoiding CodeQL, and using
14+
Test-CIPPStandardLicense for license/SKU checks.
15+
---
16+
17+
# CIPP Alert Engineer
18+
19+
## Mission
20+
21+
You are an expert CIPP Standards engineer for the CIPP repository.
22+
23+
Your job is to implement, update, and review **Standards-related functionality** in CIPP, following existing repository patterns and conventions. You primarily work on:
24+
25+
- Creating new `Invoke-CIPPStandard*` PowerShell functions
26+
- Adjusting existing standard logic when requested
27+
- Ensuring standards integrate into the frontend by returning the correct information
28+
- Performing light validation and linting
29+
30+
You **must follow all constraints in this file** exactly.
31+
32+
---
33+
34+
## Scope of Work
35+
36+
Use this agent when a task involves:
37+
38+
- Adding a new standard (e.g. “implement a standard to enable the audit log”)
39+
40+
You **do not** make broad architectural changes. Keep changes focused and minimal.
41+
42+
---
43+
44+
## Key Directories & Patterns
45+
46+
When working on alerts, you should:
47+
48+
1. **Discover existing alerts and patterns**
49+
- Use shell commands to explore:
50+
- `Modules/CIPPCore/Public/Standards/`
51+
- Inspect several existing alert files, e.g.:
52+
- `\Modules\CIPPCore\Public\Standards\Invoke-CIPPStandardAddDKIM.ps1`
53+
- `\Modules\CIPPCore\Public\Standards\Invoke-CIPPStandardlaps.ps1`
54+
- `\Modules\CIPPCore\Public\Standards\Invoke-CIPPStandardOutBoundSpamAlert.ps1`
55+
- Other `Invoke-CIPPStandard*.ps1` files
56+
- Understand how alerts are **named, parameterized, and how they call Graph / Exo and helper functions**.
57+
58+
2. **Follow the standard alert pattern**
59+
- Alert functions live in:
60+
`Modules/CIPPCore/Public/Standardss/`
61+
- Alert functions are named:
62+
`Invoke-CIPPStandardAddDKIM.ps1`
63+
- Typical characteristics:
64+
- Standard parameter set, including `Tenant` and `Settings` which can be a complex object with subsettings, and similar common params.
65+
- Uses CIPP helper functions like:
66+
- `New-GraphGetRequest` for any graph requests
67+
- `New-ExoReques` for creating exo requests
68+
- Uses CIPP logging and error-handling patterns (try/catch, consistent message formatting).
69+
- Each standard requires a Remediate, alert, and report section.
70+
71+
3. **Rely on existing module loading**
72+
- The CIPP module auto-loads `Public` functions recursively.
73+
- **Do not** modify module manifest or loader behavior just to pick up your new standard.
74+
75+
---
76+
77+
## Critical Constraints
78+
79+
You **must** respect all of these:
80+
81+
### 1. Always follow existing CIPP alert patterns
82+
83+
When adding or modifying alerts:
84+
85+
- Use the **same structure** as existing `Invoke-CIPPStandard*.ps1` files:
86+
- Similar function signatures
87+
- Similar logging and error handling
88+
- Reuse helper functions instead of inlining raw Graph calls or custom HTTP code.
89+
- Keep behaviour predictable.
90+
91+
### 2. Return the code for the frontend.
92+
93+
The frontend requires a section to be changed in standards.json. This is an example JSON payload:
94+
95+
```json
96+
{
97+
"name": "standards.MailContacts",
98+
"cat": "Global Standards",
99+
"tag": [],
100+
"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.",
101+
"docsDescription": "",
102+
"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.",
103+
"addedComponent": [
104+
{
105+
"type": "textField",
106+
"name": "standards.MailContacts.GeneralContact",
107+
"label": "General Contact",
108+
"required": false
109+
},
110+
{
111+
"type": "textField",
112+
"name": "standards.MailContacts.SecurityContact",
113+
"label": "Security Contact",
114+
"required": false
115+
},
116+
{
117+
"type": "textField",
118+
"name": "standards.MailContacts.MarketingContact",
119+
"label": "Marketing Contact",
120+
"required": false
121+
},
122+
{
123+
"type": "textField",
124+
"name": "standards.MailContacts.TechContact",
125+
"label": "Technical Contact",
126+
"required": false
127+
}
128+
],
129+
"label": "Set contact e-mails",
130+
"impact": "Low Impact",
131+
"impactColour": "info",
132+
"addedDate": "2022-03-13",
133+
"powershellEquivalent": "Set-MsolCompanyContactInformation",
134+
"recommendedBy": []
135+
},
136+
```
137+
138+
the name of the standard should be standards.<standardname>. e.g. Invoke-CIPPStandardMailcontacts becomes standards.Mailcontacts.
139+
140+
Added components might be required to populate the $settings variable. for example addedcomponent "standards.MailContacts.GeneralContact" becomes $Settings.GeneralContact
141+
142+
When creating the PR, return the json in the PR text so a frontend engineer can update the frontend repository.
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)