Skip to content

Commit 3743a8d

Browse files
Merge pull request #51 from KelvinTegelaar/dev
[pull] dev from KelvinTegelaar:dev
2 parents df469f1 + c72aef9 commit 3743a8d

183 files changed

Lines changed: 6467 additions & 1292 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CIPP-Permissions.json

Lines changed: 804 additions & 0 deletions
Large diffs are not rendered by default.

CIPPTimers.json

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -80,14 +80,6 @@
8080
"RunOnProcessor": true,
8181
"PreferredProcessor": "standards"
8282
},
83-
{
84-
"Id": "5113c66d-c040-42df-9565-39dff90ddd55",
85-
"Command": "Start-CIPPGraphSubscriptionCleanupTimer",
86-
"Description": "Orchestrator to cleanup old Graph subscriptions",
87-
"Cron": "0 0 0 * * *",
88-
"Priority": 5,
89-
"RunOnProcessor": true
90-
},
9183
{
9284
"Id": "97145a1d-28f0-4bb2-b929-5a43517d23cc",
9385
"Command": "Start-SchedulerOrchestrator",

Config/standards.json

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1722,6 +1722,35 @@
17221722
"powershellEquivalent": "New-ProtectionAlert and Set-ProtectionAlert",
17231723
"recommendedBy": []
17241724
},
1725+
{
1726+
"name": "standards.SafeLinksTemplatePolicy",
1727+
"label": "SafeLinks Policy Template",
1728+
"cat": "Templates",
1729+
"multiple": false,
1730+
"disabledFeatures": {
1731+
"report": false,
1732+
"warn": false,
1733+
"remediate": false
1734+
},
1735+
"impact": "Medium Impact",
1736+
"addedDate": "2025-04-29",
1737+
"helpText": "Deploy and manage SafeLinks policy templates to protect against malicious URLs in emails and Office documents.",
1738+
"addedComponent": [
1739+
{
1740+
"type": "autoComplete",
1741+
"multiple": true,
1742+
"creatable": false,
1743+
"name": "standards.SafeLinksTemplatePolicy.TemplateIds",
1744+
"label": "Select SafeLinks Policy Templates",
1745+
"api": {
1746+
"url": "/api/ListSafeLinksPolicyTemplates",
1747+
"labelField": "TemplateName",
1748+
"valueField": "GUID",
1749+
"queryKey": "ListSafeLinksPolicyTemplates"
1750+
}
1751+
}
1752+
]
1753+
},
17251754
{
17261755
"name": "standards.SafeLinksPolicy",
17271756
"cat": "Defender Standards",

Modules/CIPPCore/Public/Add-CIPPGroupMember.ps1

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,21 +7,22 @@ function Add-CIPPGroupMember(
77
[string]$APIName = 'Add Group Member'
88
) {
99
try {
10-
if ($member -like '*#EXT#*') { $member = [System.Web.HttpUtility]::UrlEncode($member) }
11-
$MemberIDs = 'https://graph.microsoft.com/v1.0/directoryObjects/' + (New-GraphGetRequest -uri "https://graph.microsoft.com/beta/users/$($member)" -tenantid $TenantFilter).id
12-
$addmemberbody = "{ `"members@odata.bind`": $(ConvertTo-Json @($MemberIDs)) }"
10+
if ($Member -like '*#EXT#*') { $Member = [System.Web.HttpUtility]::UrlEncode($Member) }
11+
$MemberIDs = 'https://graph.microsoft.com/v1.0/directoryObjects/' + (New-GraphGetRequest -uri "https://graph.microsoft.com/beta/users/$($Member)" -tenantid $TenantFilter).id
12+
$AddMemberBody = "{ `"members@odata.bind`": $(ConvertTo-Json @($MemberIDs)) }"
1313
if ($GroupType -eq 'Distribution list' -or $GroupType -eq 'Mail-Enabled Security') {
14-
$Params = @{ Identity = $GroupId; Member = $member; BypassSecurityGroupManagerCheck = $true }
15-
$null = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Add-DistributionGroupMember' -cmdParams $params -UseSystemMailbox $true
14+
$Params = @{ Identity = $GroupId; Member = $Member; BypassSecurityGroupManagerCheck = $true }
15+
$null = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Add-DistributionGroupMember' -cmdParams $Params -UseSystemMailbox $true
1616
} else {
17-
$null = New-GraphPostRequest -uri "https://graph.microsoft.com/beta/groups/$($GroupId)" -tenantid $TenantFilter -type patch -body $addmemberbody -Verbose
17+
$null = New-GraphPostRequest -uri "https://graph.microsoft.com/beta/groups/$($GroupId)" -tenantid $TenantFilter -type patch -body $AddMemberBody -Verbose
1818
}
19-
$Message = "Successfully added user $($Member) to $($GroupId)."
20-
Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $Message -Sev 'Info'
21-
return $message
19+
$Results = "Successfully added user $($Member) to $($GroupId)."
20+
Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $Results -Sev 'Info'
21+
return $Results
2222
} catch {
23-
$message = "Failed to add user $($Member) to $($GroupId) - $($_.Exception.Message)"
24-
Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $message -Sev 'error' -LogData (Get-CippException -Exception $_)
25-
return $message
23+
$ErrorMessage = Get-CippException -Exception $_
24+
$Results = "Failed to add user $($Member) to $($GroupId) - $($ErrorMessage.NormalizedError)"
25+
Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $Results -Sev 'error' -LogData $ErrorMessage
26+
throw $Results
2627
}
2728
}

Modules/CIPPCore/Public/Alerts/Get-CIPPAlertAppSecretExpiry.ps1

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,22 @@ function Get-CIPPAlertAppSecretExpiry {
1818
return
1919
}
2020

21-
$AlertData = foreach ($App in $applist) {
21+
$AlertData = [System.Collections.Generic.List[PSCustomObject]]::new()
22+
23+
foreach ($App in $applist) {
2224
Write-Host "checking $($App.displayName)"
2325
if ($App.passwordCredentials) {
2426
foreach ($Credential in $App.passwordCredentials) {
2527
if ($Credential.endDateTime -lt (Get-Date).AddDays(30) -and $Credential.endDateTime -gt (Get-Date).AddDays(-7)) {
2628
Write-Host ("Application '{0}' has secrets expiring on {1}" -f $App.displayName, $Credential.endDateTime)
27-
@{ DisplayName = $App.displayName; Expires = $Credential.endDateTime }
29+
30+
$Message = [PSCustomObject]@{
31+
AppName = $App.displayName
32+
AppId = $App.appId
33+
Expires = $Credential.endDateTime
34+
Tenant = $TenantFilter
35+
}
36+
$AlertData.Add($Message)
2837
}
2938
}
3039
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
function Get-CIPPAlertGlobalAdminNoAltEmail {
2+
<#
3+
.FUNCTIONALITY
4+
Entrypoint
5+
#>
6+
[CmdletBinding()]
7+
Param (
8+
[Parameter(Mandatory = $false)]
9+
[Alias('input')]
10+
$InputValue,
11+
$TenantFilter
12+
)
13+
try {
14+
# Get all Global Admin accounts using the role template ID
15+
$globalAdmins = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/directoryRoles/roleTemplateId=62e90394-69f5-4237-9190-012177145e10/members?`$select=id,displayName,userPrincipalName,otherMails" -tenantid $($TenantFilter) -AsApp $true | Where-Object {
16+
$_.userDisplayName -ne 'On-Premises Directory Synchronization Service Account' -and $_.'@odata.type' -eq '#microsoft.graph.user'
17+
}
18+
19+
# Filter for Global Admins without alternate email addresses
20+
$adminsWithoutAltEmail = $globalAdmins | Where-Object {
21+
$null -eq $_.otherMails -or $_.otherMails.Count -eq 0
22+
}
23+
24+
if ($adminsWithoutAltEmail.Count -gt 0) {
25+
$AlertData = "The following Global Admin accounts do not have an alternate email address set: $($adminsWithoutAltEmail.userPrincipalName -join ', ')"
26+
Write-AlertTrace -cmdletName $MyInvocation.MyCommand -tenantFilter $TenantFilter -data $AlertData
27+
}
28+
} catch {
29+
Write-LogMessage -message "Failed to check alternate email status for Global Admins: $($_.exception.message)" -API 'Global Admin Alt Email Alerts' -tenant $TenantFilter -sev Error
30+
}
31+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
function Get-CIPPAlertLowDomainScore {
2+
<#
3+
.FUNCTIONALITY
4+
Entrypoint
5+
#>
6+
[CmdletBinding()]
7+
Param (
8+
[Parameter(Mandatory)]
9+
$TenantFilter,
10+
[Alias('input')]
11+
[ValidateRange(0, 100)]
12+
[int]$InputValue = 70
13+
)
14+
15+
$DomainData = Get-CIPPDomainAnalyser -TenantFilter $TenantFilter
16+
$LowScoreDomains = $DomainData | Where-Object {
17+
$_.ScorePercentage -lt $InputValue -and $_.ScorePercentage -ne ''
18+
} | ForEach-Object {
19+
"$($_.Domain): Domain security score is $($_.ScorePercentage)%, which is below the threshold of $InputValue%. Issues: $($_.ScoreExplanation)"
20+
}
21+
22+
if ($LowScoreDomains) {
23+
Write-AlertTrace -cmdletName $MyInvocation.MyCommand -tenantFilter $TenantFilter -data $LowScoreDomains
24+
}
25+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
function Get-CIPPAlertNewRiskyUsers {
2+
<#
3+
.FUNCTIONALITY
4+
Entrypoint
5+
#>
6+
[CmdletBinding()]
7+
Param (
8+
[Parameter(Mandatory = $false)]
9+
[Alias('input')]
10+
$TenantFilter
11+
)
12+
$Deltatable = Get-CIPPTable -Table DeltaCompare
13+
try {
14+
# Check if tenant has P2 capabilities
15+
$Capabilities = Get-CIPPTenantCapabilities -TenantFilter $TenantFilter
16+
if (-not $Capabilities.AADPremiumService) {
17+
Write-AlertMessage -tenant $($TenantFilter) -message 'Tenant does not have Azure AD Premium P2 licensing required for risky users detection'
18+
return
19+
}
20+
21+
$Filter = "PartitionKey eq 'RiskyUsersDelta' and RowKey eq '{0}'" -f $TenantFilter
22+
$RiskyUsersDelta = (Get-CIPPAzDataTableEntity @Deltatable -Filter $Filter).delta | ConvertFrom-Json -ErrorAction SilentlyContinue
23+
24+
# Get current risky users with more detailed information
25+
$NewDelta = (New-GraphGetRequest -uri 'https://graph.microsoft.com/v1.0/identityProtection/riskyUsers' -tenantid $TenantFilter) | Select-Object userPrincipalName, riskLevel, riskState, riskDetail, riskLastUpdatedDateTime, isProcessing, history
26+
27+
$NewDeltatoSave = $NewDelta | ConvertTo-Json -Depth 10 -Compress -ErrorAction SilentlyContinue | Out-String
28+
$DeltaEntity = @{
29+
PartitionKey = 'RiskyUsersDelta'
30+
RowKey = [string]$TenantFilter
31+
delta = "$NewDeltatoSave"
32+
}
33+
Add-CIPPAzDataTableEntity @DeltaTable -Entity $DeltaEntity -Force
34+
35+
if ($RiskyUsersDelta) {
36+
$AlertData = $NewDelta | Where-Object {
37+
$_.userPrincipalName -notin $RiskyUsersDelta.userPrincipalName
38+
} | ForEach-Object {
39+
$riskHistory = if ($_.history) {
40+
$latestHistory = $_.history | Sort-Object -Property riskLastUpdatedDateTime -Descending | Select-Object -First 1
41+
"Previous Risk Level: $($latestHistory.riskLevel), Last Updated: $($latestHistory.riskLastUpdatedDateTime)"
42+
}
43+
else {
44+
'No previous risk history'
45+
}
46+
47+
# Map risk level to severity
48+
$severity = switch ($_.riskLevel) {
49+
'high' { 'Critical' }
50+
'medium' { 'Warning' }
51+
'low' { 'Info' }
52+
default { 'Info' }
53+
}
54+
55+
@{
56+
Message = "New risky user detected: $($_.userPrincipalName)"
57+
Details = @{
58+
RiskLevel = $_.riskLevel
59+
RiskState = $_.riskState
60+
RiskDetail = $_.riskDetail
61+
LastUpdated = $_.riskLastUpdatedDateTime
62+
IsProcessing = $_.isProcessing
63+
RiskHistory = $riskHistory
64+
Severity = $severity
65+
}
66+
}
67+
}
68+
69+
if ($AlertData) {
70+
Write-AlertTrace -cmdletName $MyInvocation.MyCommand -tenantFilter $TenantFilter -data $AlertData
71+
}
72+
}
73+
}
74+
catch {
75+
Write-AlertMessage -tenant $($TenantFilter) -message "Could not get risky users for $($TenantFilter): $(Get-NormalizedError -message $_.Exception.message)"
76+
}
77+
}

Modules/CIPPCore/Public/Alerts/Get-CIPPAlertSharepointQuota.ps1

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
21
function Get-CIPPAlertSharepointQuota {
32
<#
43
.FUNCTIONALITY
@@ -12,10 +11,11 @@ function Get-CIPPAlertSharepointQuota {
1211
$TenantFilter
1312
)
1413
Try {
15-
$tenantName = (New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/sites/root' -asApp $true -tenantid $TenantFilter).id.Split('.')[0]
16-
$sharepointToken = (Get-GraphToken -scope "https://$($tenantName)-admin.sharepoint.com/.default" -tenantid $TenantFilter)
17-
$sharepointToken.Add('accept', 'application/json')
18-
$sharepointQuota = (Invoke-RestMethod -Method 'GET' -Headers $sharepointToken -Uri "https://$($tenantName)-admin.sharepoint.com/_api/StorageQuotas()?api-version=1.3.2" -ErrorAction Stop).value
14+
$SharePointInfo = Get-SharePointAdminLink -Public $false -tenantFilter $TenantFilter
15+
$extraHeaders = @{
16+
'Accept' = 'application/json'
17+
}
18+
$sharepointQuota = (New-GraphGetRequest -extraHeaders $extraHeaders -scope "$($SharePointInfo.AdminUrl)/.default" -tenantid $TenantFilter -uri "$($SharePointInfo.AdminUrl)/_api/StorageQuotas()?api-version=1.3.2")
1919
} catch {
2020
return
2121
}
@@ -31,4 +31,4 @@ function Get-CIPPAlertSharepointQuota {
3131
Write-AlertTrace -cmdletName $MyInvocation.MyCommand -tenantFilter $TenantFilter -data $AlertData
3232
}
3333
}
34-
}
34+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
function Get-CIPPAlertVulnerabilities {
2+
<#
3+
.FUNCTIONALITY
4+
Entrypoint
5+
#>
6+
[CmdletBinding()]
7+
Param (
8+
[Parameter(Mandatory = $false)]
9+
[Alias('input')]
10+
$InputValue,
11+
$TenantFilter
12+
)
13+
14+
try {
15+
$VulnerabilityRequest = New-GraphGetRequest -tenantid $TenantFilter -uri "https://api.securitycenter.microsoft.com/api/machines/SoftwareVulnerabilitiesByMachine?`$top=999&`$filter=cveId ne null" -scope 'https://api.securitycenter.microsoft.com/.default'
16+
17+
if ($VulnerabilityRequest) {
18+
$AlertData = [System.Collections.Generic.List[PSCustomObject]]::new()
19+
20+
# Group by CVE ID and create objects for each vulnerability
21+
$VulnerabilityGroups = $VulnerabilityRequest | Where-Object { $_.cveId } | Group-Object cveId
22+
23+
foreach ($Group in $VulnerabilityGroups) {
24+
$FirstVuln = $Group.Group | Sort-Object firstSeenTimestamp | Select-Object -First 1
25+
$HoursOld = [math]::Round(((Get-Date) - [datetime]$FirstVuln.firstSeenTimestamp).TotalHours)
26+
27+
# Skip if vulnerability is not old enough
28+
if ($HoursOld -lt [int]$InputValue) {
29+
continue
30+
}
31+
32+
$DaysOld = [math]::Round(((Get-Date) - [datetime]$FirstVuln.firstSeenTimestamp).TotalDays)
33+
$AffectedDevices = ($Group.Group | Select-Object -ExpandProperty deviceName -Unique) -join ', '
34+
35+
$VulnerabilityAlert = [PSCustomObject]@{
36+
CVE = $Group.Name
37+
Severity = $FirstVuln.vulnerabilitySeverityLevel
38+
FirstSeenTimestamp = $FirstVuln.firstSeenTimestamp
39+
LastSeenTimestamp = $FirstVuln.lastSeenTimestamp
40+
DaysOld = $DaysOld
41+
HoursOld = $HoursOld
42+
AffectedDeviceCount = $Group.Count
43+
AffectedDevices = $AffectedDevices
44+
SoftwareName = $FirstVuln.softwareName
45+
SoftwareVendor = $FirstVuln.softwareVendor
46+
SoftwareVersion = $FirstVuln.softwareVersion
47+
CVSSScore = $FirstVuln.cvssScore
48+
ExploitabilityLevel = $FirstVuln.exploitabilityLevel
49+
RecommendedUpdate = $FirstVuln.recommendedSecurityUpdate
50+
RecommendedUpdateId = $FirstVuln.recommendedSecurityUpdateId
51+
RecommendedUpdateUrl = $FirstVuln.recommendedSecurityUpdateUrl
52+
Tenant = $TenantFilter
53+
}
54+
$AlertData.Add($VulnerabilityAlert)
55+
}
56+
57+
# Only send alert if we have vulnerabilities that meet the criteria
58+
if ($AlertData.Count -gt 0) {
59+
Write-AlertTrace -cmdletName $MyInvocation.MyCommand -tenantFilter $TenantFilter -data $AlertData
60+
}
61+
}
62+
} catch {
63+
Write-LogMessage -message "Failed to check vulnerabilities: $($_.exception.message)" -API 'Vulnerability Alerts' -tenant $TenantFilter -sev Error
64+
}
65+
}

0 commit comments

Comments
 (0)