Skip to content

Commit b2378c5

Browse files
authored
v4.0: Convert to PowerShell module with new features (#120)
* Fix hardcoded Graph URLs, add error handling, and clean up docs (#115) - Replace 6 hardcoded graph.microsoft.com URLs with $script:GraphEndpoint in Get-AssignmentFailures to fix USGov/USGovDoD tenant support - Add try/catch error handling to Get-GroupMemberships matching the pattern from Get-TransitiveGroupMembership - Remove ghost .PARAMETER ShowAdminTemplates from comment-based help - Add missing v3.10.0 release notes entry Fixes #115 * Remove deprecated groupPolicyConfigurations and migrate deviceStatuses API (#117) - Remove Administrative Templates (groupPolicyConfigurations) from all 9 feature areas -- migrated policies already appear under Settings Catalog via configurationPolicies - Remove AdminTemplates from html-export.ps1 chart and categories - Migrate deviceConfigurations deviceStatuses endpoint (removed May 2024) to POST-based getConfigurationPolicyDevicesReport reporting API - Update totalCategories counter from 17 to 16 Fixes #117 * Add v4.0 features: group simulation, policy search, UI improvements - Add Option 12: Simulate Group Membership Impact (what-if add to group) - Add Option 14: Simulate Removing User from Group (what-if remove) - Add Option 15: Search Policy Assignments (reverse lookup by name) - Add terminal-width-aware separators via Get-Separator helper - Add UPN format validation before network calls - Normalize y/n prompts to accept Y/y/Yes/yes - Add Autopilot Deployment Profiles and ESP Profiles to simulation - Add ScopeTagFilter support to simulation options - Add dedicated -SimulateTargetGroup and -SimulateRemoveTargetGroup params - Remove deprecated groupPolicyConfigurations policy type - Fix hardcoded Graph URLs to use dynamic GraphEndpoint variable - Bump version to 4.0.0 * Convert to PowerShell module with setting search feature Convert the 11K-line monolith script into a proper PowerShell module publishable to PSGallery via Install-Module. Module structure: - 18 exported cmdlets (Public/) for all features - 30 private helper functions (Private/) - Pre-cached SettingDefinitions.json (17,785 definitions) - IntuneAssignmentChecker alias for interactive mode New features: - Search-IntuneSetting: search for specific settings across all Settings Catalog and Endpoint Security policies with configured values - Update-IntuneSettingDefinition: refresh the settings catalog from Graph - All options available as standalone cmdlets (e.g., Get-IntuneUserAssignment) - PSGallery version check (replaces GitHub self-update) Architecture: - Module manifest (.psd1) with proper metadata and dependencies - Module loader (.psm1) dot-sources Public/ and Private/ functions - Module-scoped variables for shared state (GraphEndpoint, etc.) - html-export.ps1 bundled locally (no more GitHub download) * Update CI workflows for PowerShell module - Add publish-module.yml: uses Publish-Module instead of Publish-Script, validates module loads before publishing - Update psscriptanalyzer.yml: scan Module/ directory recursively instead of single script file - Old publish-script.yml kept for reference (can be removed after v4.0 ships) * Improve setting search with abbreviation expansion and fuzzy matching - Add abbreviation map for common Intune terms (PSSO -> Platform SSO, WHFB -> Windows Hello for Business, LAPS, ASR, EDR, etc.) - Add normalized matching (strips spaces/separators for fuzzy ID matching) - Search now matches against both original keyword and expanded form - Default to showing only configured settings (-ShowAll for everything) - Widen policy name column to 80 chars to avoid truncation - Fix progress line overwrite artifact on shorter policy names * Fix app platform detection showing Multi-Platform for Windows apps Windows app types without the literal 'windows' substring in their @odata.type (win32LobApp, winGetApp, microsoftStoreForBusinessApp, officeSuiteApp) fell through to 'Multi-Platform' in the HTML report. Adds explicit mappings and returns 'Web App' for webApp. Closes #121 * Archive v3 script and align release notes for v4.0 module - Move legacy IntuneAssignmentChecker.ps1 and html-export.ps1 to old/v3/ to match existing old/v1, old/v2 pattern and prevent users from running the outdated monolith after switching to the module. - Delete publish-script.yml workflow (replaced by publish-module.yml; the old workflow would fail on release since Publish-Script cannot republish a path-moved script). - Correct release notes menu option numbers (13/14/15 were misnumbered as 14/15/16) and mention the app platform detection fix. * Rewrite README for v4.0 PowerShell module - Replace .\IntuneAssignmentChecker.ps1 CLI examples with cmdlet usage (Get-IntuneUserAssignment, Connect-IntuneAssignmentChecker, etc.). - Update parameter reference table to list the 18 exported cmdlets and their common parameters instead of old script switches. - Document the four new v4.0 menu options (12-15): group simulations, policy search, and setting search. - Fix System Options section: [T] Switch Tenant, [0] Exit, [98]/[99]. - Update Azure Automation runbook example to use Connect-* and New-* flow. - Add migration note for users upgrading from the v3.x Install-Script. - Remove the legacy 'Download and run the script' manual install path; replace with a local clone + Import-Module example. * Bump 'Last updated' banner date to 2026-04-14
1 parent 4283eae commit b2378c5

58 files changed

Lines changed: 15501 additions & 437 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.

.github/workflows/psscriptanalyzer.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,12 +43,12 @@ jobs:
4343
$infoCount = 0
4444
4545
Write-Host "============================================"
46-
Write-Host "Running PSScriptAnalyzer on IntuneAssignmentChecker.ps1"
46+
Write-Host "Running PSScriptAnalyzer on Module files"
4747
Write-Host "============================================"
4848
Write-Host ""
4949
50-
# Run the analysis
51-
$results = Invoke-ScriptAnalyzer -Path ./IntuneAssignmentChecker.ps1 -Settings ./.PSScriptAnalyzerSettings.psd1 -RecurseCustomRulePath
50+
# Run the analysis on the module directory (all .ps1 and .psm1 files)
51+
$results = Invoke-ScriptAnalyzer -Path ./Module/IntuneAssignmentChecker -Recurse -Settings ./.PSScriptAnalyzerSettings.psd1 -RecurseCustomRulePath
5252
5353
if ($results) {
5454
# Group by severity
@@ -151,7 +151,7 @@ jobs:
151151
$reportContent = @"
152152
# PSScriptAnalyzer Report
153153
154-
**Script:** IntuneAssignmentChecker.ps1
154+
**Module:** IntuneAssignmentChecker
155155
**Date:** $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')
156156
**Analyzer Version:** $(Get-Module -Name PSScriptAnalyzer -ListAvailable | Select-Object -ExpandProperty Version)
157157
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
name: Publish PowerShell Module
2+
3+
on:
4+
release:
5+
types: [published]
6+
workflow_dispatch:
7+
8+
jobs:
9+
test-and-publish:
10+
runs-on: ubuntu-latest
11+
steps:
12+
- name: Checkout repository
13+
uses: actions/checkout@v4
14+
15+
- name: Validate module loads
16+
shell: pwsh
17+
run: |
18+
Import-Module ./Module/IntuneAssignmentChecker -Force -ErrorAction Stop
19+
$commands = Get-Command -Module IntuneAssignmentChecker
20+
Write-Host "Module loaded successfully with $($commands.Count) exported commands"
21+
$commands | Sort-Object Name | ForEach-Object { Write-Host " - $($_.Name)" }
22+
23+
- name: Publish to PowerShell Gallery
24+
shell: pwsh
25+
env:
26+
PS_GALLERY_API_KEY: ${{ secrets.NUGET_KEY }}
27+
run: |
28+
Publish-Module -Path ./Module/IntuneAssignmentChecker -Repository PSGallery -NuGetApiKey $env:PS_GALLERY_API_KEY

.github/workflows/publish-script.yml

Lines changed: 0 additions & 19 deletions
This file was deleted.

Module/IntuneAssignmentChecker/Data/SettingDefinitions.json

Lines changed: 1 addition & 0 deletions
Large diffs are not rendered by default.
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
@{
2+
RootModule = 'IntuneAssignmentChecker.psm1'
3+
ModuleVersion = '4.0.0'
4+
GUID = 'c6e25ec6-5787-45ef-95af-8abeb8a17daf'
5+
Author = 'Ugur Koc'
6+
CompanyName = 'Community'
7+
Copyright = '(c) Ugur Koc. All rights reserved.'
8+
Description = 'Analyze and audit Microsoft Intune policy assignments. Check user, group, and device assignments, simulate group membership changes, search policies and settings, generate HTML reports, and more.'
9+
PowerShellVersion = '7.0'
10+
RequiredModules = @('Microsoft.Graph.Authentication')
11+
FunctionsToExport = @(
12+
'Invoke-IntuneAssignmentChecker'
13+
'Connect-IntuneAssignmentChecker'
14+
'Get-IntuneUserAssignment'
15+
'Get-IntuneGroupAssignment'
16+
'Get-IntuneDeviceAssignment'
17+
'Get-IntuneAllPolicies'
18+
'Get-IntuneAllUsersAssignment'
19+
'Get-IntuneAllDevicesAssignment'
20+
'New-IntuneHTMLReport'
21+
'Get-IntuneUnassignedPolicy'
22+
'Get-IntuneEmptyGroup'
23+
'Compare-IntuneGroupAssignment'
24+
'Get-IntuneFailedAssignment'
25+
'Test-IntuneGroupMembership'
26+
'Test-IntuneGroupRemoval'
27+
'Search-IntunePolicy'
28+
'Search-IntuneSetting'
29+
'Update-IntuneSettingDefinition'
30+
)
31+
CmdletsToExport = @()
32+
VariablesToExport = @()
33+
AliasesToExport = @('IntuneAssignmentChecker')
34+
FileList = @(
35+
'Data/SettingDefinitions.json'
36+
'html-export.ps1'
37+
)
38+
PrivateData = @{
39+
PSData = @{
40+
Tags = @('Intune', 'MEM', 'Endpoint', 'Assignment', 'Policy', 'Settings', 'Audit', 'Microsoft', 'Graph')
41+
LicenseUri = 'https://github.com/ugurkocde/IntuneAssignmentChecker/blob/main/LICENSE'
42+
ProjectUri = 'https://github.com/ugurkocde/IntuneAssignmentChecker'
43+
IconUri = ''
44+
ReleaseNotes = @'
45+
Version 4.0.0:
46+
- BREAKING: Converted from script to PowerShell module (use Install-Module instead of Install-Script)
47+
- Add Option 12: Simulate Group Membership Impact
48+
- Add Option 13: Simulate Removing User from Group
49+
- Add Option 14: Search Policy Assignments (reverse lookup)
50+
- Add Option 15: Search for Specific Settings (across Settings Catalog and Endpoint Security)
51+
- Add terminal-width-aware separators
52+
- Add UPN format validation before network calls
53+
- Normalize y/n prompts to accept Y/y/Yes/yes
54+
- Fix app platform detection showing Windows apps (win32LobApp, winGetApp, microsoftStoreForBusinessApp, officeSuiteApp) as Multi-Platform in HTML report
55+
- Remove deprecated groupPolicyConfigurations (Administrative Templates) policy type
56+
- Migrate deviceStatuses API endpoints
57+
- Fix hardcoded Graph URLs to use dynamic GraphEndpoint
58+
- All features available as individual cmdlets (e.g., Get-IntuneUserAssignment, Search-IntuneSetting)
59+
'@
60+
}
61+
}
62+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
#Requires -Version 7.0
2+
#Requires -Modules Microsoft.Graph.Authentication
3+
4+
# Module-scoped variables (set by Connect-IntuneAssignmentChecker)
5+
$script:GraphEndpoint = $null
6+
$script:GraphEnvironment = $null
7+
$script:CurrentTenantId = $null
8+
$script:CurrentTenantName = $null
9+
$script:CurrentUserUPN = $null
10+
$script:TemplateIdToFamilyCache = $null
11+
$script:ScopeTagLookup = $null
12+
$script:IntentTemplateSubtypeToFamily = @{
13+
'antivirus' = 'endpointSecurityAntivirus'
14+
'diskEncryption' = 'endpointSecurityDiskEncryption'
15+
'firewall' = 'endpointSecurityFirewall'
16+
'endpointDetectionAndResponse' = 'endpointSecurityEndpointDetectionAndResponse'
17+
'attackSurfaceReduction' = 'endpointSecurityAttackSurfaceReduction'
18+
'accountProtection' = 'endpointSecurityAccountProtection'
19+
}
20+
21+
# Dot-source all private functions
22+
$Private = @(Get-ChildItem -Path "$PSScriptRoot/Private/*.ps1" -ErrorAction SilentlyContinue)
23+
foreach ($file in $Private) {
24+
try { . $file.FullName }
25+
catch { Write-Error "Failed to load $($file.FullName): $_" }
26+
}
27+
28+
# Dot-source all public functions
29+
$Public = @(Get-ChildItem -Path "$PSScriptRoot/Public/*.ps1" -ErrorAction SilentlyContinue)
30+
foreach ($file in $Public) {
31+
try { . $file.FullName }
32+
catch { Write-Error "Failed to load $($file.FullName): $_" }
33+
}
34+
35+
# Create alias for interactive mode
36+
New-Alias -Name 'IntuneAssignmentChecker' -Value 'Invoke-IntuneAssignmentChecker' -Force
37+
38+
# Export public functions and alias
39+
Export-ModuleMember -Function $Public.BaseName -Alias 'IntuneAssignmentChecker'
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
function Add-AppExportData {
2+
param (
3+
[System.Collections.ArrayList]$ExportData,
4+
[string]$Category,
5+
[object[]]$Apps,
6+
[string]$AssignmentReason = "N/A"
7+
)
8+
9+
foreach ($app in $Apps) {
10+
$appName = if ($app.displayName) { $app.displayName } else { $app.name }
11+
$null = $ExportData.Add([PSCustomObject]@{
12+
Category = $Category
13+
Item = "$appName (ID: $($app.id))"
14+
ScopeTags = Get-ScopeTagNames -ScopeTagIds $app.roleScopeTagIds -ScopeTagLookup $script:ScopeTagLookup
15+
AssignmentReason = "$AssignmentReason - $($app.AssignmentIntent)"
16+
})
17+
}
18+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
function Add-ExportData {
2+
param (
3+
[System.Collections.ArrayList]$ExportData,
4+
[string]$Category,
5+
[object[]]$Items,
6+
[Parameter(Mandatory = $false)]
7+
[object]$AssignmentReason = "N/A"
8+
)
9+
10+
foreach ($item in $Items) {
11+
$itemName = if ($item.displayName) { $item.displayName } else { $item.name }
12+
13+
# Handle different types of assignment reason input
14+
$reason = if ($AssignmentReason -is [scriptblock]) {
15+
& $AssignmentReason $item
16+
}
17+
elseif ($item.AssignmentReason) {
18+
$item.AssignmentReason
19+
}
20+
elseif ($item.AssignmentSummary) {
21+
$item.AssignmentSummary
22+
}
23+
else {
24+
$AssignmentReason
25+
}
26+
27+
$null = $ExportData.Add([PSCustomObject]@{
28+
Category = $Category
29+
Item = "$itemName (ID: $($item.id))"
30+
ScopeTags = Get-ScopeTagNames -ScopeTagIds $item.roleScopeTagIds -ScopeTagLookup $script:ScopeTagLookup
31+
AssignmentReason = $reason
32+
})
33+
}
34+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
function Add-IntentTemplateFamilyInfo {
2+
param (
3+
[Parameter(Mandatory = $false)]
4+
$IntentPolicies
5+
)
6+
7+
if (-not $IntentPolicies) { return }
8+
9+
$lookup = Get-IntentTemplateFamilyLookup
10+
11+
foreach ($intent in $IntentPolicies) {
12+
if ($intent.templateId -and $lookup.ContainsKey($intent.templateId)) {
13+
if (-not $intent.templateReference) {
14+
$intent | Add-Member -NotePropertyName 'templateReference' -NotePropertyValue @{
15+
templateFamily = $lookup[$intent.templateId]
16+
}
17+
}
18+
}
19+
}
20+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
function Export-PolicyData {
2+
param (
3+
[Parameter(Mandatory = $true)]
4+
[System.Collections.ArrayList]$ExportData,
5+
[Parameter(Mandatory = $true)]
6+
[string]$FilePath
7+
)
8+
9+
$extension = [System.IO.Path]::GetExtension($FilePath).ToLower()
10+
11+
if ($extension -eq '.xlsx') {
12+
# Check if ImportExcel module is installed
13+
if (-not (Get-Module -ListAvailable -Name ImportExcel)) {
14+
Write-Host "The ImportExcel module is required for Excel export. Would you like to install it? (y/n)" -ForegroundColor Yellow
15+
$install = Read-Host
16+
if ($install -match '^[Yy]') {
17+
try {
18+
Install-Module -Name ImportExcel -Force -Scope CurrentUser
19+
Write-Host "ImportExcel module installed successfully." -ForegroundColor Green
20+
}
21+
catch {
22+
Write-Host "Failed to install ImportExcel module. Falling back to CSV export." -ForegroundColor Red
23+
$FilePath = [System.IO.Path]::ChangeExtension($FilePath, '.csv')
24+
$ExportData | Export-Csv -Path $FilePath -NoTypeInformation
25+
Write-Host "Results exported to $FilePath" -ForegroundColor Green
26+
return
27+
}
28+
}
29+
else {
30+
Write-Host "Falling back to CSV export." -ForegroundColor Yellow
31+
$FilePath = [System.IO.Path]::ChangeExtension($FilePath, '.csv')
32+
$ExportData | Export-Csv -Path $FilePath -NoTypeInformation
33+
Write-Host "Results exported to $FilePath" -ForegroundColor Green
34+
return
35+
}
36+
}
37+
38+
try {
39+
$ExportData | Export-Excel -Path $FilePath -AutoSize -AutoFilter -WorksheetName "Intune Assignments" -TableName "IntuneAssignments"
40+
Write-Host "Results exported to $FilePath" -ForegroundColor Green
41+
}
42+
catch {
43+
Write-Host "Failed to export to Excel. Falling back to CSV export." -ForegroundColor Red
44+
$FilePath = [System.IO.Path]::ChangeExtension($FilePath, '.csv')
45+
$ExportData | Export-Csv -Path $FilePath -NoTypeInformation
46+
Write-Host "Results exported to $FilePath" -ForegroundColor Green
47+
}
48+
}
49+
else {
50+
$ExportData | Export-Csv -Path $FilePath -NoTypeInformation
51+
Write-Host "Results exported to $FilePath" -ForegroundColor Green
52+
}
53+
}

0 commit comments

Comments
 (0)