Skip to content

Commit d4f09cb

Browse files
Merge pull request #84 from KelvinTegelaar/dev
[pull] dev from KelvinTegelaar:dev
2 parents d5b3ab0 + df83265 commit d4f09cb

40 files changed

Lines changed: 2103 additions & 137 deletions

File tree

Config/standards.json

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4835,6 +4835,46 @@
48354835
}
48364836
]
48374837
},
4838+
{
4839+
"name": "standards.ReusableSettingsTemplate",
4840+
"cat": "Templates",
4841+
"label": "Reusable Settings Template",
4842+
"multiple": true,
4843+
"disabledFeatures": {
4844+
"report": false,
4845+
"warn": false,
4846+
"remediate": false
4847+
},
4848+
"impact": "Low Impact",
4849+
"impactColour": "info",
4850+
"addedDate": "2026-01-11",
4851+
"helpText": "Deploy and maintain Intune reusable settings templates that can be referenced by multiple policies.",
4852+
"executiveText": "Creates and keeps reusable Intune settings templates consistent so common firewall and configuration blocks can be reused across many policies.",
4853+
"addedComponent": [
4854+
{
4855+
"type": "autoComplete",
4856+
"multiple": true,
4857+
"creatable": false,
4858+
"required": true,
4859+
"name": "TemplateList",
4860+
"label": "Select Reusable Settings Template",
4861+
"api": {
4862+
"queryKey": "ListIntuneReusableSettingTemplates",
4863+
"url": "/api/ListIntuneReusableSettingTemplates",
4864+
"labelField": "displayName",
4865+
"valueField": "GUID",
4866+
"showRefresh": true,
4867+
"templateView": {
4868+
"title": "Reusable Settings",
4869+
"property": "RawJSON",
4870+
"type": "intune"
4871+
}
4872+
}
4873+
}
4874+
],
4875+
"powershellEquivalent": "",
4876+
"recommendedBy": []
4877+
},
48384878
{
48394879
"name": "standards.TransportRuleTemplate",
48404880
"label": "Transport Rule Template",
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
function Get-CIPPAlertInactiveGuestUsers {
2+
<#
3+
.FUNCTIONALITY
4+
Entrypoint
5+
#>
6+
[CmdletBinding()]
7+
param (
8+
[Parameter(Mandatory = $false)]
9+
[Alias('input')]
10+
$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
13+
$TenantFilter
14+
)
15+
16+
try {
17+
try {
18+
$inactiveDays = 90
19+
$excludeDisabled = $false
20+
21+
$excludeDisabled = [bool]$InputValue.ExcludeDisabled
22+
if ($null -ne $InputValue.DaysSinceLastLogin -and $InputValue.DaysSinceLastLogin -ne '') {
23+
$parsedDays = 0
24+
if ([int]::TryParse($InputValue.DaysSinceLastLogin.ToString(), [ref]$parsedDays) -and $parsedDays -gt 0) {
25+
$inactiveDays = $parsedDays
26+
}
27+
}
28+
29+
30+
31+
$Lookup = (Get-Date).AddDays(-$inactiveDays).ToUniversalTime()
32+
Write-Host "Checking for guest users inactive since $Lookup (excluding disabled: $excludeDisabled)"
33+
# Build base filter - cannot filter assignedLicenses server-side
34+
$BaseFilter = if ($excludeDisabled) { 'accountEnabled eq true' } else { '' }
35+
36+
$Uri = if ($BaseFilter) {
37+
"https://graph.microsoft.com/beta/users?`$filter=$BaseFilter&`$select=id,UserPrincipalName,signInActivity,mail,userType,accountEnabled,assignedLicenses"
38+
} else {
39+
"https://graph.microsoft.com/beta/users?`$select=id,UserPrincipalName,signInActivity,mail,userType,accountEnabled,assignedLicenses"
40+
}
41+
42+
$GraphRequest = New-GraphGetRequest -uri $Uri-tenantid $TenantFilter | Where-Object { $_.userType -eq 'Guest' }
43+
44+
$AlertData = foreach ($user in $GraphRequest) {
45+
$lastInteractive = $user.signInActivity.lastSignInDateTime
46+
$lastNonInteractive = $user.signInActivity.lastNonInteractiveSignInDateTime
47+
48+
# Find most recent sign-in
49+
$lastSignIn = $null
50+
if ($lastInteractive -and $lastNonInteractive) {
51+
$lastSignIn = if ([DateTime]$lastInteractive -gt [DateTime]$lastNonInteractive) { $lastInteractive } else { $lastNonInteractive }
52+
} elseif ($lastInteractive) {
53+
$lastSignIn = $lastInteractive
54+
} elseif ($lastNonInteractive) {
55+
$lastSignIn = $lastNonInteractive
56+
}
57+
58+
# Check if inactive
59+
$isInactive = (-not $lastSignIn) -or ([DateTime]$lastSignIn -le $Lookup)
60+
# Skip users who have never signed in by default (unless IncludeNeverSignedIn is specified)
61+
if (-not $IncludeNeverSignedIn -and -not $lastSignIn) { continue }
62+
# Only process inactive users
63+
if ($isInactive) {
64+
65+
if (-not $lastSignIn) {
66+
$Message = 'Guest user {0} has never signed in.' -f $user.UserPrincipalName
67+
} else {
68+
$daysSinceSignIn = [Math]::Round(((Get-Date) - [DateTime]$lastSignIn).TotalDays)
69+
$Message = 'Guest user {0} has been inactive for {1} days. Last sign-in: {2}' -f $user.UserPrincipalName, $daysSinceSignIn, $lastSignIn
70+
}
71+
72+
73+
[PSCustomObject]@{
74+
UserPrincipalName = $user.UserPrincipalName
75+
Id = $user.id
76+
lastSignIn = $lastSignIn
77+
DaysSinceLastSignIn = if ($daysSinceSignIn) { $daysSinceSignIn } else { 'N/A' }
78+
Message = $Message
79+
Tenant = $TenantFilter
80+
}
81+
}
82+
}
83+
84+
Write-AlertTrace -cmdletName $MyInvocation.MyCommand -tenantFilter $TenantFilter -data $AlertData
85+
} catch {}
86+
} catch {
87+
Write-AlertMessage -tenant $($TenantFilter) -message "Failed to check inactive guest users for $($TenantFilter): $(Get-NormalizedError -message $_.Exception.message)"
88+
}
89+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
function Get-CIPPAlertInactiveUsers {
2+
<#
3+
.FUNCTIONALITY
4+
Entrypoint
5+
#>
6+
[CmdletBinding()]
7+
param (
8+
[Parameter(Mandatory = $false)]
9+
[Alias('input')]
10+
$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
13+
$TenantFilter
14+
)
15+
16+
try {
17+
try {
18+
$inactiveDays = 90
19+
$excludeDisabled = $false
20+
21+
$excludeDisabled = [bool]$InputValue.ExcludeDisabled
22+
if ($null -ne $InputValue.DaysSinceLastLogin -and $InputValue.DaysSinceLastLogin -ne '') {
23+
$parsedDays = 0
24+
if ([int]::TryParse($InputValue.DaysSinceLastLogin.ToString(), [ref]$parsedDays) -and $parsedDays -gt 0) {
25+
$inactiveDays = $parsedDays
26+
}
27+
}
28+
29+
$Lookup = (Get-Date).AddDays(-$inactiveDays).ToUniversalTime()
30+
Write-Host "Checking for users inactive since $Lookup (excluding disabled: $excludeDisabled)"
31+
# Build base filter - cannot filter accountEnabled server-side
32+
$BaseFilter = if ($excludeDisabled) { 'accountEnabled eq true' } else { '' }
33+
34+
$Uri = if ($BaseFilter) {
35+
"https://graph.microsoft.com/beta/users?`$filter=$BaseFilter&`$select=id,UserPrincipalName,signInActivity,mail,userType,accountEnabled,assignedLicenses"
36+
} else {
37+
"https://graph.microsoft.com/beta/users?`$select=id,UserPrincipalName,signInActivity,mail,userType,accountEnabled,assignedLicenses"
38+
}
39+
40+
$GraphRequest = New-GraphGetRequest -uri $Uri -tenantid $TenantFilter | Where-Object { $_.userType -eq 'Member' }
41+
42+
$AlertData = foreach ($user in $GraphRequest) {
43+
$lastInteractive = $user.signInActivity.lastSignInDateTime
44+
$lastNonInteractive = $user.signInActivity.lastNonInteractiveSignInDateTime
45+
46+
# Find most recent sign-in
47+
$lastSignIn = $null
48+
if ($lastInteractive -and $lastNonInteractive) {
49+
$lastSignIn = if ([DateTime]$lastInteractive -gt [DateTime]$lastNonInteractive) { $lastInteractive } else { $lastNonInteractive }
50+
} elseif ($lastInteractive) {
51+
$lastSignIn = $lastInteractive
52+
} elseif ($lastNonInteractive) {
53+
$lastSignIn = $lastNonInteractive
54+
}
55+
56+
# Check if inactive
57+
$isInactive = (-not $lastSignIn) -or ([DateTime]$lastSignIn -le $Lookup)
58+
# Skip users who have never signed in by default (unless IncludeNeverSignedIn is specified)
59+
if (-not $IncludeNeverSignedIn -and -not $lastSignIn) { continue }
60+
# Only process inactive users
61+
if ($isInactive) {
62+
if (-not $lastSignIn) {
63+
$Message = 'User {0} has never signed in.' -f $user.UserPrincipalName
64+
} else {
65+
$daysSinceSignIn = [Math]::Round(((Get-Date) - [DateTime]$lastSignIn).TotalDays)
66+
$Message = 'User {0} has been inactive for {1} days. Last sign-in: {2}' -f $user.UserPrincipalName, $daysSinceSignIn, $lastSignIn
67+
}
68+
69+
[PSCustomObject]@{
70+
UserPrincipalName = $user.UserPrincipalName
71+
Id = $user.id
72+
lastSignIn = $lastSignIn
73+
DaysSinceLastSignIn = if ($daysSinceSignIn) { $daysSinceSignIn } else { 'N/A' }
74+
Message = $Message
75+
Tenant = $TenantFilter
76+
}
77+
}
78+
}
79+
80+
Write-AlertTrace -cmdletName $MyInvocation.MyCommand -tenantFilter $TenantFilter -data $AlertData
81+
} catch {}
82+
} catch {
83+
Write-AlertMessage -tenant $($TenantFilter) -message "Failed to check inactive users with licenses for $($TenantFilter): $(Get-NormalizedError -message $_.Exception.message)"
84+
}
85+
}

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,15 @@ function Get-CIPPAlertMFAAdmins {
2020
if (!$DuoActive) {
2121
$Users = New-GraphGETRequest -uri "https://graph.microsoft.com/beta/reports/authenticationMethods/userRegistrationDetails?`$top=999&filter=IsAdmin eq true and isMfaRegistered eq false and userType eq 'member'&`$select=id,userDisplayName,userPrincipalName,lastUpdatedDateTime,isMfaRegistered,IsAdmin" -tenantid $($TenantFilter) -AsApp $true |
2222
Where-Object { $_.userDisplayName -ne 'On-Premises Directory Synchronization Service Account' }
23+
24+
# Filter out JIT admins if any users were found
25+
if ($Users) {
26+
$Schema = Get-CIPPSchemaExtensions | Where-Object { $_.id -match '_cippUser' } | Select-Object -First 1
27+
$JITAdmins = New-GraphGETRequest -uri "https://graph.microsoft.com/beta/users?`$select=id,$($Schema.id)&`$filter=$($Schema.id)/jitAdminEnabled eq true" -tenantid $TenantFilter -ComplexFilter
28+
$JITAdminIds = $JITAdmins.id
29+
$Users = $Users | Where-Object { $_.id -notin $JITAdminIds }
30+
}
31+
2332
if ($Users.UserPrincipalName) {
2433
$AlertData = foreach ($user in $Users) {
2534
[PSCustomObject]@{
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
function Get-CIPPAlertStaleEntraDevices {
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+
try {
16+
$inactiveDays = 90
17+
18+
$excludeDisabled = [bool]$InputValue.ExcludeDisabled
19+
if ($null -ne $InputValue.DaysSinceLastActivity -and $InputValue.DaysSinceLastActivity -ne '') {
20+
$parsedDays = 0
21+
if ([int]::TryParse($InputValue.DaysSinceLastActivity.ToString(), [ref]$parsedDays) -and $parsedDays -gt 0) {
22+
$inactiveDays = $parsedDays
23+
}
24+
}
25+
26+
$Lookup = (Get-Date).AddDays(-$inactiveDays).ToUniversalTime()
27+
Write-Host "Checking for inactive Entra devices since $Lookup (excluding disabled: $excludeDisabled)"
28+
# Build base filter - cannot filter accountEnabled server-side
29+
$BaseFilter = if ($excludeDisabled) { 'accountEnabled eq true' } else { '' }
30+
31+
$Uri = if ($BaseFilter) {
32+
"https://graph.microsoft.com/beta/devices?`$filter=$BaseFilter"
33+
} else {
34+
'https://graph.microsoft.com/beta/devices'
35+
}
36+
37+
$GraphRequest = New-GraphGetRequest -uri $Uri -tenantid $TenantFilter
38+
39+
$AlertData = foreach ($device in $GraphRequest) {
40+
41+
$lastActivity = $device.approximateLastSignInDateTime
42+
43+
$isInactive = (-not $lastActivity) -or ([DateTime]$lastActivity -le $Lookup)
44+
# Only process stale Entra devices
45+
if ($isInactive) {
46+
47+
if (-not $lastActivity) {
48+
49+
$Message = 'Device {0} has never been active' -f $device.displayName
50+
} else {
51+
$daysSinceLastActivity = [Math]::Round(((Get-Date) - [DateTime]$lastActivity).TotalDays)
52+
$Message = 'Device {0} has been inactive for {1} days. Last activity: {2}' -f $device.displayName, $daysSinceLastActivity, $lastActivity
53+
}
54+
55+
if ($device.TrustType -eq 'Workplace') { $TrustType = 'Entra registered' }
56+
elseif ($device.TrustType -eq 'AzureAd') { $TrustType = 'Entra joined' }
57+
elseif ($device.TrustType -eq 'ServerAd') { $TrustType = 'Entra hybrid joined' }
58+
59+
[PSCustomObject]@{
60+
DeviceName = if ($device.displayName) { $device.displayName } else { 'N/A' }
61+
Id = if ($device.id) { $device.id } else { 'N/A' }
62+
deviceOwnership = if ($device.deviceOwnership) { $device.deviceOwnership } else { 'N/A' }
63+
operatingSystem = if ($device.operatingSystem) { $device.operatingSystem } else { 'N/A' }
64+
enrollmentType = if ($device.enrollmentType) { $device.enrollmentType } else { 'N/A' }
65+
Enabled = if ($device.accountEnabled) { $device.accountEnabled } else { 'N/A' }
66+
Managed = if ($device.isManaged) { $device.isManaged } else { 'N/A' }
67+
Complaint = if ($device.isCompliant) { $device.isCompliant } else { 'N/A' }
68+
JoinType = $TrustType
69+
lastActivity = if ($lastActivity) { $lastActivity } else { 'N/A' }
70+
DaysSinceLastActivity = if ($daysSinceLastActivity) { $daysSinceLastActivity } else { 'N/A' }
71+
RegisteredDateTime = if ($device.createdDateTime) { $device.createdDateTime } else { 'N/A' }
72+
Message = $Message
73+
Tenant = $TenantFilter
74+
}
75+
}
76+
}
77+
78+
Write-AlertTrace -cmdletName $MyInvocation.MyCommand -tenantFilter $TenantFilter -data $AlertData
79+
} catch {}
80+
} catch {
81+
Write-AlertMessage -tenant $($TenantFilter) -message "Failed to check inactive guest users for $($TenantFilter): $(Get-NormalizedError -message $_.Exception.message)"
82+
}
83+
}

Modules/CIPPCore/Public/Compare-CIPPIntuneObject.ps1

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -295,7 +295,7 @@ function Compare-CIPPIntuneObject {
295295
foreach ($groupValue in $child.groupSettingCollectionValue) {
296296
if ($groupValue.children) {
297297
$nestedResults = Process-GroupSettingChildren -Children $groupValue.children -Source $Source -IntuneCollection $IntuneCollection
298-
$results.AddRange($nestedResults)
298+
foreach ($nr in $nestedResults) { $results.Add($nr) }
299299
}
300300
}
301301
}
@@ -381,7 +381,7 @@ function Compare-CIPPIntuneObject {
381381
# Also process any children within choice setting values
382382
if ($child.choiceSettingValue?.children) {
383383
$nestedResults = Process-GroupSettingChildren -Children $child.choiceSettingValue.children -Source $Source -IntuneCollection $IntuneCollection
384-
$results.AddRange($nestedResults)
384+
foreach ($nr in $nestedResults) { $results.Add($nr) }
385385
}
386386
}
387387

@@ -399,7 +399,7 @@ function Compare-CIPPIntuneObject {
399399
foreach ($groupValue in $settingInstance.groupSettingCollectionValue) {
400400
if ($groupValue.children -is [System.Array]) {
401401
$childResults = Process-GroupSettingChildren -Children $groupValue.children -Source 'Reference' -IntuneCollection $intuneCollection
402-
$groupResults.AddRange($childResults)
402+
foreach ($cr in $childResults) { $groupResults.Add($cr) }
403403
}
404404
}
405405
# Return the results from the recursive processing
@@ -471,7 +471,7 @@ function Compare-CIPPIntuneObject {
471471
foreach ($groupValue in $settingInstance.groupSettingCollectionValue) {
472472
if ($groupValue.children -is [System.Array]) {
473473
$childResults = Process-GroupSettingChildren -Children $groupValue.children -Source 'Difference' -IntuneCollection $intuneCollection
474-
$groupResults.AddRange($childResults)
474+
foreach ($cr in $childResults) { $groupResults.Add($cr) }
475475
}
476476
}
477477
# Return the results from the recursive processing

0 commit comments

Comments
 (0)