Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
8 changes: 4 additions & 4 deletions .github/workflows/psscriptanalyzer.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,12 @@ jobs:
$infoCount = 0

Write-Host "============================================"
Write-Host "Running PSScriptAnalyzer on IntuneAssignmentChecker.ps1"
Write-Host "Running PSScriptAnalyzer on Module files"
Write-Host "============================================"
Write-Host ""

# Run the analysis
$results = Invoke-ScriptAnalyzer -Path ./IntuneAssignmentChecker.ps1 -Settings ./.PSScriptAnalyzerSettings.psd1 -RecurseCustomRulePath
# Run the analysis on the module directory (all .ps1 and .psm1 files)
$results = Invoke-ScriptAnalyzer -Path ./Module/IntuneAssignmentChecker -Recurse -Settings ./.PSScriptAnalyzerSettings.psd1 -RecurseCustomRulePath

if ($results) {
# Group by severity
Expand Down Expand Up @@ -151,7 +151,7 @@ jobs:
$reportContent = @"
# PSScriptAnalyzer Report

**Script:** IntuneAssignmentChecker.ps1
**Module:** IntuneAssignmentChecker
**Date:** $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')
**Analyzer Version:** $(Get-Module -Name PSScriptAnalyzer -ListAvailable | Select-Object -ExpandProperty Version)

Expand Down
28 changes: 28 additions & 0 deletions .github/workflows/publish-module.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
name: Publish PowerShell Module

on:
release:
types: [published]
workflow_dispatch:

jobs:
test-and-publish:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Validate module loads
shell: pwsh
run: |
Import-Module ./Module/IntuneAssignmentChecker -Force -ErrorAction Stop
$commands = Get-Command -Module IntuneAssignmentChecker
Write-Host "Module loaded successfully with $($commands.Count) exported commands"
$commands | Sort-Object Name | ForEach-Object { Write-Host " - $($_.Name)" }

- name: Publish to PowerShell Gallery
shell: pwsh
env:
PS_GALLERY_API_KEY: ${{ secrets.NUGET_KEY }}
run: |
Publish-Module -Path ./Module/IntuneAssignmentChecker -Repository PSGallery -NuGetApiKey $env:PS_GALLERY_API_KEY
19 changes: 0 additions & 19 deletions .github/workflows/publish-script.yml

This file was deleted.

Large diffs are not rendered by default.

62 changes: 62 additions & 0 deletions Module/IntuneAssignmentChecker/IntuneAssignmentChecker.psd1
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
@{
RootModule = 'IntuneAssignmentChecker.psm1'
ModuleVersion = '4.0.0'
GUID = 'c6e25ec6-5787-45ef-95af-8abeb8a17daf'
Author = 'Ugur Koc'
CompanyName = 'Community'
Copyright = '(c) Ugur Koc. All rights reserved.'
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.'
PowerShellVersion = '7.0'
RequiredModules = @('Microsoft.Graph.Authentication')
FunctionsToExport = @(
'Invoke-IntuneAssignmentChecker'
'Connect-IntuneAssignmentChecker'
'Get-IntuneUserAssignment'
'Get-IntuneGroupAssignment'
'Get-IntuneDeviceAssignment'
'Get-IntuneAllPolicies'
'Get-IntuneAllUsersAssignment'
'Get-IntuneAllDevicesAssignment'
'New-IntuneHTMLReport'
'Get-IntuneUnassignedPolicy'
'Get-IntuneEmptyGroup'
'Compare-IntuneGroupAssignment'
'Get-IntuneFailedAssignment'
'Test-IntuneGroupMembership'
'Test-IntuneGroupRemoval'
'Search-IntunePolicy'
'Search-IntuneSetting'
'Update-IntuneSettingDefinition'
)
CmdletsToExport = @()
VariablesToExport = @()
AliasesToExport = @('IntuneAssignmentChecker')
FileList = @(
'Data/SettingDefinitions.json'
'html-export.ps1'
)
PrivateData = @{
PSData = @{
Tags = @('Intune', 'MEM', 'Endpoint', 'Assignment', 'Policy', 'Settings', 'Audit', 'Microsoft', 'Graph')
LicenseUri = 'https://github.com/ugurkocde/IntuneAssignmentChecker/blob/main/LICENSE'
ProjectUri = 'https://github.com/ugurkocde/IntuneAssignmentChecker'
IconUri = ''
ReleaseNotes = @'
Version 4.0.0:
- BREAKING: Converted from script to PowerShell module (use Install-Module instead of Install-Script)
- Add Option 12: Simulate Group Membership Impact
- Add Option 13: Simulate Removing User from Group
- Add Option 14: Search Policy Assignments (reverse lookup)
- Add Option 15: Search for Specific Settings (across Settings Catalog and Endpoint Security)
- Add terminal-width-aware separators
- Add UPN format validation before network calls
- Normalize y/n prompts to accept Y/y/Yes/yes
- Fix app platform detection showing Windows apps (win32LobApp, winGetApp, microsoftStoreForBusinessApp, officeSuiteApp) as Multi-Platform in HTML report
- Remove deprecated groupPolicyConfigurations (Administrative Templates) policy type
- Migrate deviceStatuses API endpoints
- Fix hardcoded Graph URLs to use dynamic GraphEndpoint
- All features available as individual cmdlets (e.g., Get-IntuneUserAssignment, Search-IntuneSetting)
'@
}
}
}
39 changes: 39 additions & 0 deletions Module/IntuneAssignmentChecker/IntuneAssignmentChecker.psm1
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
#Requires -Version 7.0
#Requires -Modules Microsoft.Graph.Authentication

# Module-scoped variables (set by Connect-IntuneAssignmentChecker)
$script:GraphEndpoint = $null
$script:GraphEnvironment = $null
$script:CurrentTenantId = $null
$script:CurrentTenantName = $null
$script:CurrentUserUPN = $null
$script:TemplateIdToFamilyCache = $null
$script:ScopeTagLookup = $null
$script:IntentTemplateSubtypeToFamily = @{
'antivirus' = 'endpointSecurityAntivirus'
'diskEncryption' = 'endpointSecurityDiskEncryption'
'firewall' = 'endpointSecurityFirewall'
'endpointDetectionAndResponse' = 'endpointSecurityEndpointDetectionAndResponse'
'attackSurfaceReduction' = 'endpointSecurityAttackSurfaceReduction'
'accountProtection' = 'endpointSecurityAccountProtection'
}

# Dot-source all private functions
$Private = @(Get-ChildItem -Path "$PSScriptRoot/Private/*.ps1" -ErrorAction SilentlyContinue)
foreach ($file in $Private) {
try { . $file.FullName }
catch { Write-Error "Failed to load $($file.FullName): $_" }
}

# Dot-source all public functions
$Public = @(Get-ChildItem -Path "$PSScriptRoot/Public/*.ps1" -ErrorAction SilentlyContinue)
foreach ($file in $Public) {
try { . $file.FullName }
catch { Write-Error "Failed to load $($file.FullName): $_" }
}

# Create alias for interactive mode
New-Alias -Name 'IntuneAssignmentChecker' -Value 'Invoke-IntuneAssignmentChecker' -Force

# Export public functions and alias
Export-ModuleMember -Function $Public.BaseName -Alias 'IntuneAssignmentChecker'
18 changes: 18 additions & 0 deletions Module/IntuneAssignmentChecker/Private/Add-AppExportData.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
function Add-AppExportData {
param (
[System.Collections.ArrayList]$ExportData,
[string]$Category,
[object[]]$Apps,
[string]$AssignmentReason = "N/A"
)

foreach ($app in $Apps) {
$appName = if ($app.displayName) { $app.displayName } else { $app.name }
$null = $ExportData.Add([PSCustomObject]@{
Category = $Category
Item = "$appName (ID: $($app.id))"
ScopeTags = Get-ScopeTagNames -ScopeTagIds $app.roleScopeTagIds -ScopeTagLookup $script:ScopeTagLookup
AssignmentReason = "$AssignmentReason - $($app.AssignmentIntent)"
})
}
}
34 changes: 34 additions & 0 deletions Module/IntuneAssignmentChecker/Private/Add-ExportData.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
function Add-ExportData {
param (
[System.Collections.ArrayList]$ExportData,
[string]$Category,
[object[]]$Items,
[Parameter(Mandatory = $false)]
[object]$AssignmentReason = "N/A"
)

foreach ($item in $Items) {
$itemName = if ($item.displayName) { $item.displayName } else { $item.name }

# Handle different types of assignment reason input
$reason = if ($AssignmentReason -is [scriptblock]) {
& $AssignmentReason $item
}
elseif ($item.AssignmentReason) {
$item.AssignmentReason
}
elseif ($item.AssignmentSummary) {
$item.AssignmentSummary
}
else {
$AssignmentReason
}

$null = $ExportData.Add([PSCustomObject]@{
Category = $Category
Item = "$itemName (ID: $($item.id))"
ScopeTags = Get-ScopeTagNames -ScopeTagIds $item.roleScopeTagIds -ScopeTagLookup $script:ScopeTagLookup
AssignmentReason = $reason
})
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
function Add-IntentTemplateFamilyInfo {
param (
[Parameter(Mandatory = $false)]
$IntentPolicies
)

if (-not $IntentPolicies) { return }

$lookup = Get-IntentTemplateFamilyLookup

foreach ($intent in $IntentPolicies) {
if ($intent.templateId -and $lookup.ContainsKey($intent.templateId)) {
if (-not $intent.templateReference) {
$intent | Add-Member -NotePropertyName 'templateReference' -NotePropertyValue @{
templateFamily = $lookup[$intent.templateId]
}
}
}
}
}
53 changes: 53 additions & 0 deletions Module/IntuneAssignmentChecker/Private/Export-PolicyData.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
function Export-PolicyData {
param (
[Parameter(Mandatory = $true)]
[System.Collections.ArrayList]$ExportData,
[Parameter(Mandatory = $true)]
[string]$FilePath
)

$extension = [System.IO.Path]::GetExtension($FilePath).ToLower()

if ($extension -eq '.xlsx') {
# Check if ImportExcel module is installed
if (-not (Get-Module -ListAvailable -Name ImportExcel)) {
Write-Host "The ImportExcel module is required for Excel export. Would you like to install it? (y/n)" -ForegroundColor Yellow
$install = Read-Host
if ($install -match '^[Yy]') {
try {
Install-Module -Name ImportExcel -Force -Scope CurrentUser
Write-Host "ImportExcel module installed successfully." -ForegroundColor Green
}
catch {
Write-Host "Failed to install ImportExcel module. Falling back to CSV export." -ForegroundColor Red
$FilePath = [System.IO.Path]::ChangeExtension($FilePath, '.csv')
$ExportData | Export-Csv -Path $FilePath -NoTypeInformation
Write-Host "Results exported to $FilePath" -ForegroundColor Green
return
}
}
else {
Write-Host "Falling back to CSV export." -ForegroundColor Yellow
$FilePath = [System.IO.Path]::ChangeExtension($FilePath, '.csv')
$ExportData | Export-Csv -Path $FilePath -NoTypeInformation
Write-Host "Results exported to $FilePath" -ForegroundColor Green
return
}
}

try {
$ExportData | Export-Excel -Path $FilePath -AutoSize -AutoFilter -WorksheetName "Intune Assignments" -TableName "IntuneAssignments"
Write-Host "Results exported to $FilePath" -ForegroundColor Green
}
catch {
Write-Host "Failed to export to Excel. Falling back to CSV export." -ForegroundColor Red
$FilePath = [System.IO.Path]::ChangeExtension($FilePath, '.csv')
$ExportData | Export-Csv -Path $FilePath -NoTypeInformation
Write-Host "Results exported to $FilePath" -ForegroundColor Green
}
}
else {
$ExportData | Export-Csv -Path $FilePath -NoTypeInformation
Write-Host "Results exported to $FilePath" -ForegroundColor Green
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
function Export-ResultsIfRequested {
param (
[System.Collections.ArrayList]$ExportData,
[string]$DefaultFileName,
[switch]$ForceExport,
[string]$CustomExportPath
)

if ($ForceExport -or $ExportToCSV) {
$exportPath = if ($CustomExportPath) {
$CustomExportPath
}
else {
Show-SaveFileDialog -DefaultFileName $DefaultFileName
}

if ($exportPath) {
Export-PolicyData -ExportData $ExportData -FilePath $exportPath
}
}
elseif (-not $parameterMode) {
$export = Read-Host "`nWould you like to export the results to CSV? (y/n)"
if ($export -match '^[Yy]') {
$exportPath = Show-SaveFileDialog -DefaultFileName $DefaultFileName
if ($exportPath) {
Export-PolicyData -ExportData $ExportData -FilePath $exportPath
}
}
}
}
12 changes: 12 additions & 0 deletions Module/IntuneAssignmentChecker/Private/Filter-ByScopeTag.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
function Filter-ByScopeTag {
param (
[object[]]$Items,
[string]$FilterTag,
[hashtable]$ScopeTagLookup
)
if ([string]::IsNullOrWhiteSpace($FilterTag)) { return $Items }
return @($Items | Where-Object {
$names = Get-ScopeTagNames -ScopeTagIds $_.roleScopeTagIds -ScopeTagLookup $ScopeTagLookup
($names -split ', ') -contains $FilterTag
})
}
Loading
Loading