Skip to content

Commit 578f421

Browse files
Merge pull request #1522 from KelvinTegelaar/dev
Dev to release
2 parents 2e6997f + a06b461 commit 578f421

File tree

220 files changed

+8716
-1712
lines changed

Some content is hidden

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

220 files changed

+8716
-1712
lines changed

CIPP-Permissions.json

Lines changed: 789 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 = "{ `"[email protected]`": $(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 = "{ `"[email protected]`": $(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+
}

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

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,16 @@ function Get-CIPPAlertHuntressRogueApps {
1313
Param (
1414
[Parameter(Mandatory = $false)]
1515
[Alias('input')]
16-
$InputValue,
17-
$TenantFilter,
18-
[Parameter(Mandatory = $false)]
19-
[bool]$IgnoreDisabledApps = $false
16+
[bool]$InputValue = $false,
17+
$TenantFilter
2018
)
2119

2220
try {
2321
$RogueApps = Invoke-RestMethod -Uri 'https://raw.githubusercontent.com/huntresslabs/rogueapps/main/public/rogueapps.json'
2422
$RogueAppFilter = $RogueApps.appId -join "','"
2523
$ServicePrincipals = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/servicePrincipals?`$filter=appId in ('$RogueAppFilter')" -tenantid $TenantFilter
2624
# If IgnoreDisabledApps is true, filter out disabled service principals
27-
if ($IgnoreDisabledApps) {
25+
if ($InputValue) {
2826
$ServicePrincipals = $ServicePrincipals | Where-Object { $_.accountEnabled -eq $true }
2927
}
3028

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

Lines changed: 42 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,28 +8,60 @@ function Get-CIPPAlertInactiveLicensedUsers {
88
[Parameter(Mandatory = $false)]
99
[Alias('input')]
1010
$InputValue,
11+
[Parameter(Mandatory = $false)]
12+
[switch]$IncludeNeverSignedIn, # Include users who have never signed in (default is to skip them), future use would allow this to be set in an alert configuration
1113
$TenantFilter
1214
)
1315

1416
try {
1517
try {
18+
$Lookup = (Get-Date).AddDays(-90).ToUniversalTime()
19+
20+
# Build base filter - cannot filter assignedLicenses server-side
21+
$BaseFilter = if ($InputValue -eq $true) { "accountEnabled eq true" } else { "" }
1622

17-
$Lookup = (Get-Date).AddDays(-90).ToUniversalTime().ToString('o')
18-
$GraphRequest = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/users?`$filter=(signInActivity/lastNonInteractiveSignInDateTime le $Lookup)&`$select=id,UserPrincipalName,signInActivity,mail,userType,accountEnabled,assignedLicenses" -scope 'https://graph.microsoft.com/.default' -tenantid $TenantFilter |
19-
Where-Object { $null -ne $_.assignedLicenses.skuId }
23+
$Uri = if ($BaseFilter) {
24+
"https://graph.microsoft.com/beta/users?`$filter=$BaseFilter&`$select=id,UserPrincipalName,signInActivity,mail,userType,accountEnabled,assignedLicenses"
25+
} else {
26+
"https://graph.microsoft.com/beta/users?`$select=id,UserPrincipalName,signInActivity,mail,userType,accountEnabled,assignedLicenses"
27+
}
28+
29+
$GraphRequest = New-GraphGetRequest -uri $Uri -scope 'https://graph.microsoft.com/.default' -tenantid $TenantFilter |
30+
Where-Object { $null -ne $_.assignedLicenses -and $_.assignedLicenses.Count -gt 0 }
2031

21-
# true = only active users
22-
if ($InputValue -eq $true) { $GraphRequest = $GraphRequest | Where-Object { $_.accountEnabled -eq $true } }
2332
$AlertData = foreach ($user in $GraphRequest) {
24-
$Message = 'User {0} has been inactive for 90 days, but still has a license assigned.' -f $user.UserPrincipalName
25-
$user | Select-Object -Property UserPrincipalName, signInActivity, @{Name = 'Message'; Expression = { $Message } }
33+
$lastInteractive = $user.signInActivity.lastSignInDateTime
34+
$lastNonInteractive = $user.signInActivity.lastNonInteractiveSignInDateTime
2635

27-
}
28-
Write-AlertTrace -cmdletName $MyInvocation.MyCommand -tenantFilter $TenantFilter -data $AlertData
36+
# Find most recent sign-in
37+
$lastSignIn = $null
38+
if ($lastInteractive -and $lastNonInteractive) {
39+
$lastSignIn = if ([DateTime]$lastInteractive -gt [DateTime]$lastNonInteractive) { $lastInteractive } else { $lastNonInteractive }
40+
} elseif ($lastInteractive) {
41+
$lastSignIn = $lastInteractive
42+
} elseif ($lastNonInteractive) {
43+
$lastSignIn = $lastNonInteractive
44+
}
2945

30-
} catch {}
46+
# Check if inactive
47+
$isInactive = (-not $lastSignIn) -or ([DateTime]$lastSignIn -le $Lookup)
48+
# Skip users who have never signed in by default (unless IncludeNeverSignedIn is specified)
49+
if (-not $IncludeNeverSignedIn -and -not $lastSignIn) { continue }
50+
# Only process inactive users
51+
if ($isInactive) {
52+
if (-not $lastSignIn) {
53+
$Message = 'User {0} has never signed in but still has a license assigned.' -f $user.UserPrincipalName
54+
} else {
55+
$daysSinceSignIn = [Math]::Round(((Get-Date) - [DateTime]$lastSignIn).TotalDays)
56+
$Message = 'User {0} has been inactive for {1} days but still has a license assigned. Last sign-in: {2}' -f $user.UserPrincipalName, $daysSinceSignIn, $lastSignIn
57+
}
3158

59+
$user | Select-Object -Property UserPrincipalName, signInActivity, @{Name = 'Message'; Expression = { $Message } }
60+
}
61+
}
3262

63+
Write-AlertTrace -cmdletName $MyInvocation.MyCommand -tenantFilter $TenantFilter -data $AlertData
64+
} catch {}
3365
} catch {
3466
Write-AlertMessage -tenant $($TenantFilter) -message "Failed to check inactive users with licenses for $($TenantFilter): $(Get-NormalizedError -message $_.Exception.message)"
3567
}
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+
}

0 commit comments

Comments
 (0)