JyskIT.Automation.psm1
#Region Module Variables $script:ModuleRoot = $PSScriptRoot $script:Config = $null $script:CurrentGraphToken = $null $script:PartnerCredentials = $null # Token cache configuration $script:TokenCache = @{} $script:TokenCacheConfig = @{ MaxSize = 100 # Maximum number of tokens to cache RefreshBuffer = 5 # Minutes before expiry to trigger refresh # PersistencePath should be in a writable location even if the module is installed in Program Files, such as local app data PersistencePath = [System.IO.Path]::Combine([System.Environment]::GetFolderPath([System.Environment+SpecialFolder]::LocalApplicationData), 'JyskIT.Automation\tokencache.xml') } . (Join-Path $PSScriptRoot 'Config\Baseline\PolicyTypeSettings.ps1') # Module default configuration $script:DefaultConfig = @{ CuranetBaseURL = 'https://reseller.curanet.dk' SpamfilterAPIBaseURL = 'https://spamfilter.io/api' KeyVaultName = 'KV-M365-SAM' SubscriptionName = 'internal-002-mpn' PartnerTenantId = 'b6a41db1-6b1a-4833-9b69-f8e363090e45' GDAP = Get-Content (Join-Path $PSScriptRoot 'Config\GDAP\default.json') | ConvertFrom-Json -AsHashtable PolicyTypeSettings = $PolicyTypeSettings } #Region Custom Exceptions class TokenOperationException : Exception { [string]$Operation [string]$DetailedMessage TokenOperationException([string]$operation, [string]$message, [Exception]$innerException) : base($message, $innerException) { $this.Operation = $operation $this.DetailedMessage = $message } } #Region Helper Functions $script:LogConfig = @{ LogPath = [System.IO.Path]::Combine([System.Environment]::GetFolderPath([System.Environment+SpecialFolder]::LocalApplicationData), 'JyskIT.Automation\logs', "JyskIT.Automation-$(Get-Date -Format 'dd-MM-yyyy').log") MaxLogAgeDays = 30 LogLevel = 'Verbose' # Default log level: Error, Warning, Info, Debug, Verbose WriteToFile = $true WriteToHost = $true } function Write-ModuleLog { [CmdletBinding()] param( [Parameter(Mandatory)] [string]$Message, [Parameter()] [ValidateSet('Error', 'Warning', 'Info', 'Debug', 'Verbose')] [string]$Level = 'Info', [Parameter()] [System.Management.Automation.ErrorRecord]$ErrorRecord, [Parameter()] [string]$Component = 'General', [Parameter()] [string]$FunctionName = $MyInvocation.MyCommand.Name, [Parameter()] [switch]$NoOutput, [Parameter()] [switch]$ThrowError, [Parameter()] [string]$ErrorOperation, [Parameter()] [string]$ErrorMessage ) # Define log level weights for filtering $levelWeights = @{ Error = 1 Warning = 2 Info = 3 Debug = 4 Verbose = 5 } # Check if we should process this log level if ($levelWeights[$Level] -gt $levelWeights[$script:LogConfig.LogLevel]) { return } try { $timestamp = Get-Date -Format 'dd-MM-yyyy HH:mm:ss' $callStack = Get-PSCallStack | Select-Object -Skip 1 -First 1 $caller = if ($callStack) { $callStack.Command } else { 'Unknown' } # Build the log entry $logEntry = [PSCustomObject]@{ Timestamp = $timestamp Level = $Level.ToUpper() Component = $Component Function = $FunctionName Caller = $caller Message = $Message ErrorDetails = if ($ErrorRecord) { @{ Exception = $ErrorRecord.Exception.Message Category = $ErrorRecord.CategoryInfo.Category TargetObject = $ErrorRecord.TargetObject ScriptStackTrace = $ErrorRecord.ScriptStackTrace PositionMessage = $ErrorRecord.InvocationInfo.PositionMessage } | ConvertTo-Json -Compress } else { $null } } # Format the log message for output $consoleMessage = "$($logEntry.Timestamp) [$($logEntry.Level)] [$($logEntry.Component)] $($logEntry.Message)" if ($ErrorRecord) { $consoleMessage += "`nError: $($ErrorRecord.Exception.Message)" $consoleMessage += "`nStack: $($ErrorRecord.ScriptStackTrace)" } # Write to console if enabled if ($script:LogConfig.WriteToHost -and -not $NoOutput) { switch ($Level) { 'Error' { Write-Error $consoleMessage } 'Warning' { Write-Warning $consoleMessage } 'Info' { Write-Information $consoleMessage -InformationAction Continue } 'Debug' { Write-Debug $consoleMessage } 'Verbose' { Write-Verbose $consoleMessage } } } # Write to file if enabled if ($script:LogConfig.WriteToFile) { # Ensure log directory exists $logDir = Split-Path $script:LogConfig.LogPath -Parent if (-not (Test-Path $logDir)) { New-Item -Path $logDir -ItemType Directory -Force | Out-Null } # Convert log entry to JSON and append to file $logEntry | ConvertTo-Json -Compress | Add-Content -Path $script:LogConfig.LogPath -Encoding UTF8 } # Throw error if requested if ($ThrowError) { if ($ErrorRecord) { throw [TokenOperationException]::new( $ErrorOperation, $ErrorMessage ?? $Message, $ErrorRecord.Exception ) } else { throw [TokenOperationException]::new( $ErrorOperation, $ErrorMessage ?? $Message, $null ) } } } catch { # Fallback error handling - write directly to console $fallbackMessage = "Failed to write to log: $($_.Exception.Message)" Write-Error $fallbackMessage if ($ThrowError) { throw } } } function Initialize-ModuleLogging { [CmdletBinding()] param() try { # Create logs directory if it doesn't exist $logDir = Split-Path $script:LogConfig.LogPath -Parent if (-not (Test-Path $logDir)) { New-Item -Path $logDir -ItemType Directory -Force | Out-Null } # Clean up old log files Get-ChildItem -Path $logDir -Filter "JyskIT.Automation-*.log" | Where-Object { $_.LastWriteTime -lt (Get-Date).AddDays(-$script:LogConfig.MaxLogAgeDays) } | ForEach-Object { Write-ModuleLog -Message "Removing old log file: $($_.Name)" -Level Debug -Component 'LogMaintenance' Remove-Item $_.FullName -Force } # Initialize new log file with header $moduleInfo = Get-Module JyskIT.Automation @" # JyskIT.Automation Module Log # Module Version: $($moduleInfo.Version) # Log Started: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') # PowerShell Version: $($PSVersionTable.PSVersion) # OS: $([System.Environment]::OSVersion.VersionString) "@ | Out-File -FilePath $script:LogConfig.LogPath -Encoding UTF8 Write-ModuleLog -Message "Module logging initialized" -Level Verbose -Component 'LogInitialization' } catch { Write-Error "Failed to initialize logging: $_" } } # Helper function to get log entries function Get-ModuleLog { [CmdletBinding()] param( [Parameter()] [DateTime]$StartTime, [Parameter()] [DateTime]$EndTime, [Parameter()] [ValidateSet('Error', 'Warning', 'Info', 'Debug', 'Verbose')] [string[]]$Level, [Parameter()] [string]$Component, [Parameter()] [switch]$Raw ) try { $logs = Get-Content -Path $script:LogConfig.LogPath | Where-Object { $_ -match '^{' } | ForEach-Object { $_ | ConvertFrom-Json } # Apply filters if ($StartTime) { $logs = $logs | Where-Object { [DateTime]$_.Timestamp -ge $StartTime } } if ($EndTime) { $logs = $logs | Where-Object { [DateTime]$_.Timestamp -le $EndTime } } if ($Level) { $logs = $logs | Where-Object { $_.Level -in $Level } } if ($Component) { $logs = $logs | Where-Object { $_.Component -eq $Component } } if ($Raw) { return $logs } else { return $logs | Format-Table -Property Timestamp, Level, Component, Message -AutoSize } } catch { Write-Error "Failed to retrieve logs: $_" } } function Initialize-ModuleDependencies { [CmdletBinding()] param() $requiredModules = @( @{Name = 'Microsoft.Graph'; MinimumVersion = '2.24.0'}, @{Name = 'Az.Accounts'; MinimumVersion = '3.0.5'}, @{Name = 'Az.KeyVault'; MinimumVersion = '6.2.0'}, @{Name = 'ExchangeOnlineManagement'; MinimumVersion = '3.6.0'}, @{Name = 'Microsoft.PowerShell.ConsoleGuiTools'; MinimumVersion = '0.7.7'} ) foreach ($module in $requiredModules) { $installed = Get-Module -ListAvailable -Name $module.Name | Where-Object { $_.Version -ge [version]$module.MinimumVersion } if (-not $installed) { try { Write-ModuleLog -Message "Installing $($module.Name) module (minimum version $($module.MinimumVersion))..." -Level Info -Component 'ModuleInitialization' Write-ModuleLog -Message "This might take a while, please be patient" -Level Info -Component 'ModuleInitialization' Install-Module -Name $module.Name -MinimumVersion $module.MinimumVersion -Force -AllowClobber -Scope CurrentUser Write-ModuleLog -Message "$($module.Name) module installed successfully" -Level Info -Component 'ModuleInitialization' } catch { Write-ModuleLog -Message "Failed to install $($module.Name) module: $_" -Level Error -Component 'ModuleInitialization' throw } } } } #Region Module Initialization try { Initialize-ModuleLogging } catch { Write-Error "Failed to initialize module logging: $_" throw } # Initialize required modules try { Initialize-ModuleDependencies } catch { Write-Error "Failed to initialize module dependencies: $_" throw } # Create necessary directories $directories = @( (Join-Path $PSScriptRoot 'cache'), (Join-Path $PSScriptRoot 'logs') ) foreach ($dir in $directories) { if (!(Test-Path $dir)) { New-Item -Path $dir -ItemType Directory -Force | Out-Null } } # Load config if exists $configPath = Join-Path $script:ModuleRoot 'Config\config.json' if (Test-Path $configPath) { try { $script:Config = Get-Content $configPath | ConvertFrom-Json -AsHashtable Write-Verbose "Loaded configuration from $configPath" } catch { Write-Warning "Failed to load configuration from $configPath, using defaults" $script:Config = $script:DefaultConfig } } else { $script:Config = $script:DefaultConfig Write-Verbose "No configuration file found, using defaults" } # Import functions $Public = @(Get-ChildItem -Path $PSScriptRoot\Public\*.ps1 -Recurse -ErrorAction SilentlyContinue) $Private = @(Get-ChildItem -Path $PSScriptRoot\Private\*.ps1 -Recurse -ErrorAction SilentlyContinue) foreach ($import in @($Private + $Public)) { try { Write-Verbose "Importing $($import.FullName)" . $import.FullName } catch { Write-Error -Message "Failed to import function $($import.FullName): $_" } } # Initialize token cache try { Write-Verbose "Initializing token cache" Initialize-TokenCache } catch { Write-Warning "Failed to initialize token cache: $_" } # Clean up on module removal $MyInvocation.MyCommand.ScriptBlock.Module.OnRemove = { Write-Verbose "Cleaning up module resources..." try { Disconnect-MgGraph -ErrorAction SilentlyContinue Save-TokenCache } catch { Write-Warning "Cleanup error: $_" } } # Export public functions Export-ModuleMember -Function $Public.BaseName |