Devolutions.CIEM.psm1
|
$script:ModuleRoot = $PSScriptRoot # --- Resolve data root (outside module version dir so data survives upgrades) --- if ($PSScriptRoot -match '(.*[/\\])Repository[/\\]Modules[/\\]') { $script:DataRoot = Join-Path $Matches[1] 'data' } else { $script:DataRoot = Join-Path $PSScriptRoot 'data' } if (-not (Test-Path $script:DataRoot)) { New-Item -Path $script:DataRoot -ItemType Directory -Force | Out-Null } # --- Bootstrap logger (used before Write-CIEMLog is dot-sourced) --- $script:_BootLogPath = Join-Path $script:DataRoot 'ciem.log' function _BootLog([string]$Msg, [string]$Sev = 'INFO') { $entry = "[$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss.fff')] [$Sev] [ModuleInit] $Msg" try { Add-Content -Path $script:_BootLogPath -Value $entry -Encoding UTF8 -ErrorAction SilentlyContinue } catch {} } _BootLog "Module loading from: $PSScriptRoot" # --- Sub-module directory roots (for runtime file discovery) --- $script:GraphRoot = Join-Path $PSScriptRoot 'modules/Devolutions.CIEM.Graph' $script:AzureRoot = Join-Path $PSScriptRoot 'modules/Azure/Infrastructure' $script:AzureDiscoveryRoot = Join-Path $PSScriptRoot 'modules/Azure/Discovery' $script:AWSRoot = Join-Path $PSScriptRoot 'modules/AWS/Infrastructure' $script:ChecksRoot = Join-Path $PSScriptRoot 'modules/Devolutions.CIEM.Checks' $script:PSURoot = Join-Path $PSScriptRoot 'modules/Devolutions.CIEM.PSU' # All sub-module roots in load order $subModuleRoots = @( $script:GraphRoot $script:AzureRoot $script:AzureDiscoveryRoot $script:AWSRoot $script:ChecksRoot $script:PSURoot ) # --- Import PSUSQLite (bundled dependency) --- _BootLog "Importing PSUSQLite..." Import-Module (Join-Path $PSScriptRoot 'modules/PSUSQLite/PSUSQLite.psd1') -Global _BootLog "PSUSQLite imported" # --- Load classes in dependency order --- # IMPORTANT: All dot-source calls MUST remain at the psm1 top level. # Wrapping dot-source in a helper function scopes class and function # definitions to that function, making them invisible to the module. # Base classes (must load first - other classes depend on these) _BootLog "Loading base classes..." foreach ($className in @('CIEMAuthenticationContext', 'CIEMProvider')) { $classPath = Join-Path $PSScriptRoot "Classes/$className.ps1" try { . $classPath } catch { _BootLog "FAILED to load class $className : $_" 'ERROR' } } # Checks classes (explicit order: base types before derived) _BootLog "Loading Checks classes..." foreach ($className in @('CIEMServiceCache', 'CIEMProviderService', 'CIEMCheck', 'CIEMScanResult')) { $classFile = Join-Path $script:ChecksRoot "Classes/$className.ps1" if (Test-Path $classFile) { try { . $classFile } catch { _BootLog "FAILED to load class $className : $_" 'ERROR' } } } # Unordered classes (Graph, Azure, Azure Discovery, AWS - no interdependencies) _BootLog "Loading provider classes..." foreach ($root in @($script:GraphRoot, $script:AzureRoot, $script:AzureDiscoveryRoot, $script:AWSRoot)) { foreach ($file in (Get-ChildItem (Join-Path $root 'Classes/*.ps1') -ErrorAction SilentlyContinue)) { try { . $file.FullName } catch { _BootLog "FAILED to load class $($file.Name) : $_" 'ERROR' } } } # --- Load private and public functions (base + all sub-modules) --- $_loadedCount = 0 $_failedCount = 0 foreach ($subdir in @('Private', 'Public')) { foreach ($file in (Get-ChildItem "$PSScriptRoot/$subdir/*.ps1" -ErrorAction SilentlyContinue)) { try { . $file.FullName; $_loadedCount++ } catch { _BootLog "FAILED to load $subdir/$($file.Name) : $_" 'ERROR'; $_failedCount++ } } foreach ($root in $subModuleRoots) { $rootName = Split-Path $root -Leaf foreach ($file in (Get-ChildItem (Join-Path $root "$subdir/*.ps1") -ErrorAction SilentlyContinue)) { try { . $file.FullName; $_loadedCount++ } catch { _BootLog "FAILED to load $rootName/$subdir/$($file.Name) : $_" 'ERROR'; $_failedCount++ } } } } # Switch to Write-CIEMLog now that it's available Write-CIEMLog -Message "Loaded $_loadedCount functions ($_failedCount failures)" -Component 'ModuleInit' # --- Load PSU page functions (must be exported for PSU's scriptblock resolution) --- foreach ($file in (Get-ChildItem "$script:PSURoot/Pages/*.ps1" -ErrorAction SilentlyContinue)) { try { . $file.FullName; $_loadedCount++ } catch { Write-CIEMLog "FAILED to load Page $($file.Name) : $_" -Severity ERROR -Component 'ModuleInit'; $_failedCount++ } } Write-CIEMLog -Message "PSU pages loaded (total: $_loadedCount functions, $_failedCount failures)" -Component 'ModuleInit' # --- Module-scoped state --- # Base $script:Config = $null $script:AuthContext = @{} $script:PSUEnvironment = $null $script:DatabasePath = $null # Azure $script:AzureAuthContext = $null # [CIEMAzureAuthContext] — set by Connect-CIEMAzure $script:AzureAuthProfilesCacheKey = 'CIEM:AuthProfiles:Azure' $script:AWSAuthProfileCacheKey = 'CIEM:AuthProfile:AWS' $script:CIEMConfigCacheKey = 'CIEM:Config' $script:ScanConfigCacheKey = 'CIEM:ScanConfig' # AWS $script:AWSAuthContext = $null # PSU $script:RelationshipColors = @{ 'CONTAINS' = '#1976d2' # ARM hierarchy containment (blue) 'member_of' = '#9c27b0' # Group membership (purple) 'owner_of' = '#f44336' # Ownership (red) 'has_role_member' = '#ff9800' # Role membership (orange) 'transitive_member_of' = '#4caf50' # Transitive membership (green) } # Risk policy constants $script:DormantPermissionThresholdDays = 90 $script:MediumEntitlementThreshold = 5 $script:PrivilegedRoleNames = @((Get-Content (Join-Path $script:AzureDiscoveryRoot 'Data/privileged_roles.json') -Raw | ConvertFrom-Json).name) $script:CIEMParallelThrottleLimitDiscovery = 5 $script:CIEMParallelThrottleLimitScan = 10 $script:CIEMSqlBatchSize = 500 $script:CIEMGraphBatchSize = 20 # --- Initialize database (base + provider schemas) --- Write-CIEMLog -Message "Initializing database..." -Component 'ModuleInit' try { New-CIEMDatabase Write-CIEMLog -Message "Database initialized at: $(Get-CIEMDatabasePath)" -Component 'ModuleInit' } catch { Write-CIEMLog -Message "Database initialization failed: $($_.Exception.Message)" -Severity ERROR -Component 'ModuleInit' } # Apply provider-specific schemas foreach ($schema in @( @{ Path = Join-Path $script:AzureRoot 'Data/azure_schema.sql'; Label = 'Azure' } @{ Path = Join-Path $script:AzureDiscoveryRoot 'Data/discovery_schema.sql'; Label = 'AzureDiscovery' } @{ Path = Join-Path $script:GraphRoot 'Data/graph_schema.sql'; Label = 'Graph' } )) { try { $dbPath = Get-CIEMDatabasePath if ($dbPath -and (Test-Path $schema.Path)) { $conn = Open-PSUSQLiteConnection -Database $dbPath try { $schemaSql = Get-Content -Path $schema.Path -Raw foreach ($statement in ($schemaSql -split ';\s*\n' | Where-Object { $_.Trim() })) { Invoke-PSUSQLiteQuery -Connection $conn -Query $statement.Trim() -AsNonQuery | Out-Null } } finally { $conn.Dispose() } } } catch { Write-CIEMLog -Message "$($schema.Label) schema failed: $($_.Exception.Message)" -Severity ERROR -Component 'ModuleInit' } } # --- Argument completers --- Register-CIEMArgumentCompleters # --- Export all public + page functions --- $exportDirs = @("$PSScriptRoot/Public") foreach ($root in $subModuleRoots) { $exportDirs += Join-Path $root 'Public' } $exportFunctions = @() foreach ($dir in $exportDirs) { $files = Get-ChildItem "$dir/*.ps1" -ErrorAction SilentlyContinue if ($files) { $exportFunctions += $files.BaseName } } # Page files may define multiple functions per file — extract all function names # (required because [scriptblock]::Create() in PSU tab/onClick only sees exported functions) foreach ($pageFile in (Get-ChildItem "$script:PSURoot/Pages/*.ps1" -ErrorAction SilentlyContinue)) { $content = Get-Content $pageFile.FullName -Raw $fnMatches = [regex]::Matches($content, '(?m)^function\s+([\w-]+)') foreach ($m in $fnMatches) { $exportFunctions += $m.Groups[1].Value } } Write-CIEMLog -Message "Exporting $($exportFunctions.Count) functions" -Component 'ModuleInit' Export-ModuleMember -Function $exportFunctions Write-CIEMLog -Message "Module load complete" -Component 'ModuleInit' |