M365IdentityPosture.psm1
|
#Requires -Version 7.0 #Requires -PSEdition Core <# .SYNOPSIS M365 Identity & Security Posture Assessment Module .DESCRIPTION Comprehensive identity and access security reporting framework for Microsoft cloud services. Provides assessment tools for Authentication Context, Access Package documentation with interactive visualization, Role Assignments, Conditional Access policies, and related security configurations across Microsoft 365, Azure AD/Entra ID, and hybrid scenarios. .NOTES Module Name: M365IdentityPosture Author: Sebastian Flæng Markdanner Website: https://chanceofsecurity.com GitHub: https://github.com/Noble-Effeciency13/M365IdentityPosture Version: 1.1.0 #> # Module configuration $script:ModuleRoot = $PSScriptRoot $script:ModuleName = 'M365IdentityPosture' $script:ModuleVersion = '1.1.0' $script:ToolVersion = $script:ModuleVersion # Initialize module-scoped variables $script:graphConnected = $false $script:CurrentTenantId = $null $script:TenantShortName = $null $script:LogPath = Join-Path -Path ([System.IO.Path]::GetTempPath()) -ChildPath "$($script:ModuleName)_$(Get-Date -Format 'yyyyMMdd_HHmmss').log" # Authentication data containers (module-scoped) $script:PurviewAuthenticationData = $null $script:SharePointAuthenticationData = $null $script:AzureAuthenticationData = $null $script:AllSensitivityLabels = $null # Role name cache for performance optimization $script:__AuthContext_RoleNameCache = @{} # Load core internal functions early (logging + banner) so they are available during module initialization. try { . (Join-Path -Path $PSScriptRoot -ChildPath 'Private\Utilities\Write-ModuleLog.ps1') . (Join-Path -Path $PSScriptRoot -ChildPath 'Private\Utilities\Show-ModuleBanner.ps1') } catch { throw "Failed to load core module utilities (Write-ModuleLog/Show-ModuleBanner): $($_.Exception.Message)" } # Initialize module logging Write-ModuleLog -Message "Module initialization started: $script:ModuleName v$script:ModuleVersion" -Level Info -NoConsole Write-ModuleLog -Message "PowerShell Version: $($PSVersionTable.PSVersion)" -Level Info -NoConsole Write-ModuleLog -Message "Operating System: $($PSVersionTable.OS)" -Level Info -NoConsole # Get function definition files Write-ModuleLog -Message 'Loading module functions...' -Level Verbose -NoConsole # Recursively discover ALL private function files (any subfolder under Private) $privateRoot = Join-Path $PSScriptRoot 'Private' $Private = @() if (Test-Path $privateRoot) { $Private = Get-ChildItem -Path $privateRoot -Filter '*.ps1' -Recurse -File -ErrorAction SilentlyContinue | Sort-Object FullName # These are loaded explicitly above (needed early in module initialization). $Private = @( $Private | Where-Object { $_.FullName -notlike '*\Private\Utilities\Write-ModuleLog.ps1' -and $_.FullName -notlike '*\Private\Utilities\Show-ModuleBanner.ps1' } ) # Group for logging by relative folder $grouped = $Private | Group-Object { Split-Path $_.FullName -Parent } foreach ($g in $grouped) { $relative = ($g.Name -replace [Regex]::Escape($privateRoot), 'Private') Write-ModuleLog -Message ('Found {0} functions in {1}' -f $g.Count, $relative) -Level Verbose -NoConsole } Write-ModuleLog -Message ('Total private function files discovered recursively: {0}' -f ($Private.Count)) -Level Verbose -NoConsole } # Load public functions AFTER private functions $Public = @(Get-ChildItem -Path "$PSScriptRoot\Public\*.ps1" -ErrorAction SilentlyContinue) Write-ModuleLog -Message "Found $($Public.Count) public functions" -Level Verbose -NoConsole Write-ModuleLog -Message "Total functions to load: $($Public.Count + $Private.Count)" -Level Verbose -NoConsole # Dot source the function files - PRIVATE FIRST, then PUBLIC $functionLoadErrors = @() # Load private functions foreach ($import in $Private) { try { Write-ModuleLog -Message "Importing private function: $($import.Name)" -Level Verbose -NoConsole . $import.FullName } catch { $errorMsg = "Failed to import private function $($import.Name): $_" Write-ModuleLog -Message $errorMsg -Level Error $functionLoadErrors += $errorMsg } } # Load public functions (they can now access private functions) foreach ($import in $Public) { try { Write-ModuleLog -Message "Importing public function: $($import.Name)" -Level Verbose -NoConsole . $import.FullName } catch { $errorMsg = "Failed to import public function $($import.Name): $_" Write-ModuleLog -Message $errorMsg -Level Error $functionLoadErrors += $errorMsg } } if ($functionLoadErrors.Count -gt 0) { throw "Module initialization failed. $($functionLoadErrors.Count) functions failed to load. Check the log at: $script:LogPath" } # Export only the public functions if ($Public.Count -gt 0) { Export-ModuleMember -Function $Public.BaseName Write-ModuleLog -Message "Exported $($Public.Count) public functions: $($Public.BaseName -join ', ')" -Level Verbose -NoConsole } # Also explicitly export the Write-ModuleLog function for use in public functions Export-ModuleMember -Function 'Write-ModuleLog' # Module initialization complete Write-ModuleLog -Message 'Module initialization completed successfully' -Level Success -NoConsole # Display module information when loaded interactively if ($Host.Name -eq 'ConsoleHost' -and -not $env:M365IdentityPosture_QUIET) { # Check if we're being called from within our own module functions $callStack = Get-PSCallStack $isInternalCall = $callStack | Where-Object { $_.Command -like 'Invoke-AuthContext*' -or $_.InvocationInfo.MyCommand.Module.Name -eq $script:ModuleName } # Only show banner and version check on initial import, not during function execution if (-not $isInternalCall) { # Check for module updates (silent if PSGallery unavailable) Test-ModuleVersion # Display module banner Show-ModuleBanner -MinWidth 67 } } # Module cleanup on removal $OnRemoveScript = { Write-ModuleLog -Message 'Module removal initiated' -Level Info -NoConsole # Disconnect from services if connected if ($script:graphConnected) { try { Write-ModuleLog -Message 'Disconnecting from Microsoft Graph' -Level Info -NoConsole Disconnect-MgGraph -ErrorAction SilentlyContinue | Out-Null } catch { Write-Verbose "Failed to disconnect from Microsoft Graph: $_" } } # Clean up SharePoint connection if (Get-Module Microsoft.Online.SharePoint.PowerShell) { try { Write-ModuleLog -Message 'Disconnecting from SharePoint Online' -Level Info -NoConsole Disconnect-SPOService -ErrorAction SilentlyContinue | Out-Null } catch { Write-Verbose "Failed to disconnect from SharePoint Online: $_" } } # Clean up Exchange Online connection if (Get-PSSession | Where-Object { $_.ConfigurationName -eq 'Microsoft.Exchange' }) { try { Write-ModuleLog -Message 'Disconnecting from Exchange Online' -Level Info -NoConsole Disconnect-ExchangeOnline -Confirm:$false -ErrorAction SilentlyContinue | Out-Null } catch { Write-Verbose "Failed to disconnect from Exchange Online: $_" } } # Clean up Azure connection $azContext = Get-AzContext -ErrorAction SilentlyContinue if ($azContext) { try { Write-ModuleLog -Message 'Disconnecting from Azure' -Level Info -NoConsole Disconnect-AzAccount -ErrorAction SilentlyContinue | Out-Null } catch { Write-Verbose "Failed to disconnect from Azure: $_" } } Write-ModuleLog -Message 'Module removal completed' -Level Info -NoConsole Write-Verbose "$script:ModuleName module removed and connections cleaned up" } $ExecutionContext.SessionState.Module.OnRemove += $OnRemoveScript # Module is ready Write-ModuleLog -Message 'Module ready for use' -Level Success -NoConsole Write-Verbose "Module $script:ModuleName v$script:ModuleVersion loaded successfully" # Test that critical functions are available $criticalFunctions = @('Invoke-AuthContextInventoryCore', 'Invoke-Preflight', 'Invoke-GraphPhase') $missingFunctions = @() foreach ($func in $criticalFunctions) { if (-not (Get-Command $func -ErrorAction SilentlyContinue)) { $missingFunctions += $func Write-ModuleLog -Message "Critical function missing: $func" -Level Error -NoConsole } } if ($missingFunctions.Count -gt 0) { Write-Warning "Module loaded with missing critical functions: $($missingFunctions -join ', ')" Write-Warning 'Some features may not work correctly.' } |