Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
0dee9b1
feat(api): add Intune reusable settings endpoints and tests
MWG-Logan Jan 4, 2026
8e69843
fix(api): boost BRR on ID filtering in ListIntuneReusableSettingTempl…
MWG-Logan Jan 12, 2026
f88cad0
fix(api): remove unnecessary initialization of Settings array
MWG-Logan Jan 12, 2026
bd391d6
fix: Hudu sync creating duplicate users and devices
redanthrax Jan 29, 2026
43db5a9
fix: GET to POST for domain analyser
kris6673 Jan 31, 2026
873f024
Merge remote-tracking branch 'upstream/dev' into dev
redanthrax Feb 3, 2026
0288392
Merge remote-tracking branch 'upstream/dev' into dev
redanthrax Feb 5, 2026
98b1c7a
fix(standards): Update Intune Deploy reusable settings to match Kelvi…
MWG-Logan Feb 9, 2026
869c747
Merge branch 'dev' into reusable-settings-standards
MWG-Logan Feb 9, 2026
4e8c7a9
Update Get-CIPPAlertMFAAdmins.ps1
Zacgoose Feb 10, 2026
7db9133
Use reporting DB for signin report insead of lighthouse making it pos…
Zacgoose Feb 10, 2026
603ffa5
Merge branch 'KelvinTegelaar:dev' into dev
StevenVBeek Feb 10, 2026
4fd9970
added new alerts
Feb 10, 2026
19f9d7f
Fix for Issue#5340
mpressley-np Feb 10, 2026
e6ba6fa
Persist only Enabled in FeatureFlags table
JohnDuprey Feb 11, 2026
fc23cd6
Include Selector2 1024-bit DKIM in rotation filter
JohnDuprey Feb 11, 2026
13a5b0e
Merge pull request #1804 from kris6673/dkim-selectors
KelvinTegelaar Feb 11, 2026
9694b0a
Merge pull request #1822 from mpressley-np/master-standard-oauthlowse…
KelvinTegelaar Feb 11, 2026
82d92f2
Merge pull request #1818 from Zacgoose/inactive-user-report-and-db-tw…
KelvinTegelaar Feb 11, 2026
9f1feb5
Merge pull request #1817 from Zacgoose/Exclude-JIT
KelvinTegelaar Feb 11, 2026
3c788f4
Merge pull request #1796 from redanthrax/dev
KelvinTegelaar Feb 11, 2026
24697fa
Merge pull request #1766 from BezaluLLC/reusable-settings-standards
KelvinTegelaar Feb 11, 2026
eda364e
Allow overwriting app templates; fix scope lookup
JohnDuprey Feb 11, 2026
e3f8284
add totals from db.
KelvinTegelaar Feb 12, 2026
df4ab75
Merge pull request #1820 from StevenVBeek/dev
KelvinTegelaar Feb 12, 2026
df83265
clean up AI fragments
KelvinTegelaar Feb 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions Config/standards.json
Original file line number Diff line number Diff line change
Expand Up @@ -4835,6 +4835,46 @@
}
]
},
{
"name": "standards.ReusableSettingsTemplate",
"cat": "Templates",
"label": "Reusable Settings Template",
"multiple": true,
"disabledFeatures": {
"report": false,
"warn": false,
"remediate": false
},
"impact": "Low Impact",
"impactColour": "info",
"addedDate": "2026-01-11",
"helpText": "Deploy and maintain Intune reusable settings templates that can be referenced by multiple policies.",
"executiveText": "Creates and keeps reusable Intune settings templates consistent so common firewall and configuration blocks can be reused across many policies.",
"addedComponent": [
{
"type": "autoComplete",
"multiple": true,
"creatable": false,
"required": true,
"name": "TemplateList",
"label": "Select Reusable Settings Template",
"api": {
"queryKey": "ListIntuneReusableSettingTemplates",
"url": "/api/ListIntuneReusableSettingTemplates",
"labelField": "displayName",
"valueField": "GUID",
"showRefresh": true,
"templateView": {
"title": "Reusable Settings",
"property": "RawJSON",
"type": "intune"
}
}
}
],
"powershellEquivalent": "",
"recommendedBy": []
},
{
"name": "standards.TransportRuleTemplate",
"label": "Transport Rule Template",
Expand Down
89 changes: 89 additions & 0 deletions Modules/CIPPCore/Public/Alerts/Get-CIPPAlertInactiveGuestUsers.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
function Get-CIPPAlertInactiveGuestUsers {
<#
.FUNCTIONALITY
Entrypoint
#>
[CmdletBinding()]
param (
[Parameter(Mandatory = $false)]
[Alias('input')]
$InputValue,
[Parameter(Mandatory = $false)]
[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
$TenantFilter
)

try {
try {
$inactiveDays = 90
$excludeDisabled = $false

$excludeDisabled = [bool]$InputValue.ExcludeDisabled
if ($null -ne $InputValue.DaysSinceLastLogin -and $InputValue.DaysSinceLastLogin -ne '') {
$parsedDays = 0
if ([int]::TryParse($InputValue.DaysSinceLastLogin.ToString(), [ref]$parsedDays) -and $parsedDays -gt 0) {
$inactiveDays = $parsedDays
}
}



$Lookup = (Get-Date).AddDays(-$inactiveDays).ToUniversalTime()
Write-Host "Checking for guest users inactive since $Lookup (excluding disabled: $excludeDisabled)"
# Build base filter - cannot filter assignedLicenses server-side
$BaseFilter = if ($excludeDisabled) { 'accountEnabled eq true' } else { '' }

$Uri = if ($BaseFilter) {
"https://graph.microsoft.com/beta/users?`$filter=$BaseFilter&`$select=id,UserPrincipalName,signInActivity,mail,userType,accountEnabled,assignedLicenses"
} else {
"https://graph.microsoft.com/beta/users?`$select=id,UserPrincipalName,signInActivity,mail,userType,accountEnabled,assignedLicenses"
}

$GraphRequest = New-GraphGetRequest -uri $Uri-tenantid $TenantFilter | Where-Object { $_.userType -eq 'Guest' }

$AlertData = foreach ($user in $GraphRequest) {
$lastInteractive = $user.signInActivity.lastSignInDateTime
$lastNonInteractive = $user.signInActivity.lastNonInteractiveSignInDateTime

# Find most recent sign-in
$lastSignIn = $null
if ($lastInteractive -and $lastNonInteractive) {
$lastSignIn = if ([DateTime]$lastInteractive -gt [DateTime]$lastNonInteractive) { $lastInteractive } else { $lastNonInteractive }
} elseif ($lastInteractive) {
$lastSignIn = $lastInteractive
} elseif ($lastNonInteractive) {
$lastSignIn = $lastNonInteractive
}

# Check if inactive
$isInactive = (-not $lastSignIn) -or ([DateTime]$lastSignIn -le $Lookup)
# Skip users who have never signed in by default (unless IncludeNeverSignedIn is specified)
if (-not $IncludeNeverSignedIn -and -not $lastSignIn) { continue }
# Only process inactive users
if ($isInactive) {

if (-not $lastSignIn) {
$Message = 'Guest user {0} has never signed in.' -f $user.UserPrincipalName
} else {
$daysSinceSignIn = [Math]::Round(((Get-Date) - [DateTime]$lastSignIn).TotalDays)
$Message = 'Guest user {0} has been inactive for {1} days. Last sign-in: {2}' -f $user.UserPrincipalName, $daysSinceSignIn, $lastSignIn
}


[PSCustomObject]@{
UserPrincipalName = $user.UserPrincipalName
Id = $user.id
lastSignIn = $lastSignIn
DaysSinceLastSignIn = if ($daysSinceSignIn) { $daysSinceSignIn } else { 'N/A' }
Message = $Message
Tenant = $TenantFilter
}
}
}

Write-AlertTrace -cmdletName $MyInvocation.MyCommand -tenantFilter $TenantFilter -data $AlertData
} catch {}
} catch {
Write-AlertMessage -tenant $($TenantFilter) -message "Failed to check inactive guest users for $($TenantFilter): $(Get-NormalizedError -message $_.Exception.message)"
}
}
85 changes: 85 additions & 0 deletions Modules/CIPPCore/Public/Alerts/Get-CIPPAlertInactiveUsers.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
function Get-CIPPAlertInactiveUsers {
<#
.FUNCTIONALITY
Entrypoint
#>
[CmdletBinding()]
param (
[Parameter(Mandatory = $false)]
[Alias('input')]
$InputValue,
[Parameter(Mandatory = $false)]
[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
$TenantFilter
)

try {
try {
$inactiveDays = 90
$excludeDisabled = $false

$excludeDisabled = [bool]$InputValue.ExcludeDisabled
if ($null -ne $InputValue.DaysSinceLastLogin -and $InputValue.DaysSinceLastLogin -ne '') {
$parsedDays = 0
if ([int]::TryParse($InputValue.DaysSinceLastLogin.ToString(), [ref]$parsedDays) -and $parsedDays -gt 0) {
$inactiveDays = $parsedDays
}
}

$Lookup = (Get-Date).AddDays(-$inactiveDays).ToUniversalTime()
Write-Host "Checking for users inactive since $Lookup (excluding disabled: $excludeDisabled)"
# Build base filter - cannot filter accountEnabled server-side
$BaseFilter = if ($excludeDisabled) { 'accountEnabled eq true' } else { '' }

$Uri = if ($BaseFilter) {
"https://graph.microsoft.com/beta/users?`$filter=$BaseFilter&`$select=id,UserPrincipalName,signInActivity,mail,userType,accountEnabled,assignedLicenses"
} else {
"https://graph.microsoft.com/beta/users?`$select=id,UserPrincipalName,signInActivity,mail,userType,accountEnabled,assignedLicenses"
}

$GraphRequest = New-GraphGetRequest -uri $Uri -tenantid $TenantFilter | Where-Object { $_.userType -eq 'Member' }

$AlertData = foreach ($user in $GraphRequest) {
$lastInteractive = $user.signInActivity.lastSignInDateTime
$lastNonInteractive = $user.signInActivity.lastNonInteractiveSignInDateTime

# Find most recent sign-in
$lastSignIn = $null
if ($lastInteractive -and $lastNonInteractive) {
$lastSignIn = if ([DateTime]$lastInteractive -gt [DateTime]$lastNonInteractive) { $lastInteractive } else { $lastNonInteractive }
} elseif ($lastInteractive) {
$lastSignIn = $lastInteractive
} elseif ($lastNonInteractive) {
$lastSignIn = $lastNonInteractive
}

# Check if inactive
$isInactive = (-not $lastSignIn) -or ([DateTime]$lastSignIn -le $Lookup)
# Skip users who have never signed in by default (unless IncludeNeverSignedIn is specified)
if (-not $IncludeNeverSignedIn -and -not $lastSignIn) { continue }
# Only process inactive users
if ($isInactive) {
if (-not $lastSignIn) {
$Message = 'User {0} has never signed in.' -f $user.UserPrincipalName
} else {
$daysSinceSignIn = [Math]::Round(((Get-Date) - [DateTime]$lastSignIn).TotalDays)
$Message = 'User {0} has been inactive for {1} days. Last sign-in: {2}' -f $user.UserPrincipalName, $daysSinceSignIn, $lastSignIn
}

[PSCustomObject]@{
UserPrincipalName = $user.UserPrincipalName
Id = $user.id
lastSignIn = $lastSignIn
DaysSinceLastSignIn = if ($daysSinceSignIn) { $daysSinceSignIn } else { 'N/A' }
Message = $Message
Tenant = $TenantFilter
}
}
}

Write-AlertTrace -cmdletName $MyInvocation.MyCommand -tenantFilter $TenantFilter -data $AlertData
} catch {}
} catch {
Write-AlertMessage -tenant $($TenantFilter) -message "Failed to check inactive users with licenses for $($TenantFilter): $(Get-NormalizedError -message $_.Exception.message)"
}
}
9 changes: 9 additions & 0 deletions Modules/CIPPCore/Public/Alerts/Get-CIPPAlertMFAAdmins.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,15 @@ function Get-CIPPAlertMFAAdmins {
if (!$DuoActive) {
$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 |
Where-Object { $_.userDisplayName -ne 'On-Premises Directory Synchronization Service Account' }

# Filter out JIT admins if any users were found
if ($Users) {
$Schema = Get-CIPPSchemaExtensions | Where-Object { $_.id -match '_cippUser' } | Select-Object -First 1
$JITAdmins = New-GraphGETRequest -uri "https://graph.microsoft.com/beta/users?`$select=id,$($Schema.id)&`$filter=$($Schema.id)/jitAdminEnabled eq true" -tenantid $TenantFilter -ComplexFilter
$JITAdminIds = $JITAdmins.id
$Users = $Users | Where-Object { $_.id -notin $JITAdminIds }
}

if ($Users.UserPrincipalName) {
$AlertData = foreach ($user in $Users) {
[PSCustomObject]@{
Expand Down
83 changes: 83 additions & 0 deletions Modules/CIPPCore/Public/Alerts/Get-CIPPAlertStaleEntraDevices.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
function Get-CIPPAlertStaleEntraDevices {
<#
.FUNCTIONALITY
Entrypoint
#>
[CmdletBinding()]
param (
[Parameter(Mandatory = $false)]
[Alias('input')]
$InputValue,
$TenantFilter
)

try {
try {
$inactiveDays = 90

$excludeDisabled = [bool]$InputValue.ExcludeDisabled
if ($null -ne $InputValue.DaysSinceLastActivity -and $InputValue.DaysSinceLastActivity -ne '') {
$parsedDays = 0
if ([int]::TryParse($InputValue.DaysSinceLastActivity.ToString(), [ref]$parsedDays) -and $parsedDays -gt 0) {
$inactiveDays = $parsedDays
}
}

$Lookup = (Get-Date).AddDays(-$inactiveDays).ToUniversalTime()
Write-Host "Checking for inactive Entra devices since $Lookup (excluding disabled: $excludeDisabled)"
# Build base filter - cannot filter accountEnabled server-side
$BaseFilter = if ($excludeDisabled) { 'accountEnabled eq true' } else { '' }

$Uri = if ($BaseFilter) {
"https://graph.microsoft.com/beta/devices?`$filter=$BaseFilter"
} else {
'https://graph.microsoft.com/beta/devices'
}

$GraphRequest = New-GraphGetRequest -uri $Uri -tenantid $TenantFilter

$AlertData = foreach ($device in $GraphRequest) {

$lastActivity = $device.approximateLastSignInDateTime

$isInactive = (-not $lastActivity) -or ([DateTime]$lastActivity -le $Lookup)
# Only process stale Entra devices
if ($isInactive) {

if (-not $lastActivity) {

$Message = 'Device {0} has never been active' -f $device.displayName
} else {
$daysSinceLastActivity = [Math]::Round(((Get-Date) - [DateTime]$lastActivity).TotalDays)
$Message = 'Device {0} has been inactive for {1} days. Last activity: {2}' -f $device.displayName, $daysSinceLastActivity, $lastActivity
}

if ($device.TrustType -eq 'Workplace') { $TrustType = 'Entra registered' }
elseif ($device.TrustType -eq 'AzureAd') { $TrustType = 'Entra joined' }
elseif ($device.TrustType -eq 'ServerAd') { $TrustType = 'Entra hybrid joined' }

[PSCustomObject]@{
DeviceName = if ($device.displayName) { $device.displayName } else { 'N/A' }
Id = if ($device.id) { $device.id } else { 'N/A' }
deviceOwnership = if ($device.deviceOwnership) { $device.deviceOwnership } else { 'N/A' }
operatingSystem = if ($device.operatingSystem) { $device.operatingSystem } else { 'N/A' }
enrollmentType = if ($device.enrollmentType) { $device.enrollmentType } else { 'N/A' }
Enabled = if ($device.accountEnabled) { $device.accountEnabled } else { 'N/A' }
Managed = if ($device.isManaged) { $device.isManaged } else { 'N/A' }
Complaint = if ($device.isCompliant) { $device.isCompliant } else { 'N/A' }
JoinType = $TrustType
lastActivity = if ($lastActivity) { $lastActivity } else { 'N/A' }
DaysSinceLastActivity = if ($daysSinceLastActivity) { $daysSinceLastActivity } else { 'N/A' }
RegisteredDateTime = if ($device.createdDateTime) { $device.createdDateTime } else { 'N/A' }
Message = $Message
Tenant = $TenantFilter
}
}
}

Write-AlertTrace -cmdletName $MyInvocation.MyCommand -tenantFilter $TenantFilter -data $AlertData
} catch {}
} catch {
Write-AlertMessage -tenant $($TenantFilter) -message "Failed to check inactive guest users for $($TenantFilter): $(Get-NormalizedError -message $_.Exception.message)"
}
}
8 changes: 4 additions & 4 deletions Modules/CIPPCore/Public/Compare-CIPPIntuneObject.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,7 @@ function Compare-CIPPIntuneObject {
foreach ($groupValue in $child.groupSettingCollectionValue) {
if ($groupValue.children) {
$nestedResults = Process-GroupSettingChildren -Children $groupValue.children -Source $Source -IntuneCollection $IntuneCollection
$results.AddRange($nestedResults)
foreach ($nr in $nestedResults) { $results.Add($nr) }
}
}
}
Expand Down Expand Up @@ -381,7 +381,7 @@ function Compare-CIPPIntuneObject {
# Also process any children within choice setting values
if ($child.choiceSettingValue?.children) {
$nestedResults = Process-GroupSettingChildren -Children $child.choiceSettingValue.children -Source $Source -IntuneCollection $IntuneCollection
$results.AddRange($nestedResults)
foreach ($nr in $nestedResults) { $results.Add($nr) }
}
}

Expand All @@ -399,7 +399,7 @@ function Compare-CIPPIntuneObject {
foreach ($groupValue in $settingInstance.groupSettingCollectionValue) {
if ($groupValue.children -is [System.Array]) {
$childResults = Process-GroupSettingChildren -Children $groupValue.children -Source 'Reference' -IntuneCollection $intuneCollection
$groupResults.AddRange($childResults)
foreach ($cr in $childResults) { $groupResults.Add($cr) }
}
}
# Return the results from the recursive processing
Expand Down Expand Up @@ -471,7 +471,7 @@ function Compare-CIPPIntuneObject {
foreach ($groupValue in $settingInstance.groupSettingCollectionValue) {
if ($groupValue.children -is [System.Array]) {
$childResults = Process-GroupSettingChildren -Children $groupValue.children -Source 'Difference' -IntuneCollection $intuneCollection
$groupResults.AddRange($childResults)
foreach ($cr in $childResults) { $groupResults.Add($cr) }
}
}
# Return the results from the recursive processing
Expand Down
Loading