Devolutions.CIEM.psm1
|
#Requires -Version 5.1 Set-StrictMode -Version Latest # Module root path for use by all functions $script:ModuleRoot = $PSScriptRoot # ============================================================================ # Auto-install Az.Accounts if not present # ============================================================================ # Az.Accounts is required for Azure authentication and API calls (Invoke-AzRestMethod). # Auto-installing here ensures the module works when installed from PSU Gallery, # which does not auto-install RequiredModules dependencies. if (-not (Get-Module -ListAvailable -Name 'Az.Accounts')) { Write-Verbose 'Devolutions.CIEM: Az.Accounts module not found. Installing...' try { Install-Module -Name 'Az.Accounts' -Force -AllowClobber -Repository PSGallery -ErrorAction Stop Write-Verbose 'Devolutions.CIEM: Az.Accounts installed successfully.' } catch { throw "Az.Accounts is required but could not be installed: $($_.Exception.Message). Install manually: Install-Module Az.Accounts" } } # Load configuration into script-scoped variable $script:configFilePath = Join-Path -Path $PSScriptRoot -ChildPath 'config.json' if (Test-Path $script:configFilePath) { try { $script:Config = Get-Content $script:configFilePath -Raw | ConvertFrom-Json } catch { Write-Warning "Failed to load config.json: $_" $script:Config = $null } } # Apply default configuration if not loaded if (-not $script:Config) { $script:Config = [PSCustomObject]@{ azure = [PSCustomObject]@{ authentication = [PSCustomObject]@{ method = 'CurrentContext' } subscriptionFilter = @() endpoints = [PSCustomObject]@{ graphApi = 'https://graph.microsoft.com/v1.0' armApi = 'https://management.azure.com' } } scan = [PSCustomObject]@{ throttleLimit = 10 timeoutSeconds = 300 continueOnError = $true } pam = [PSCustomObject]@{ remediationUrl = 'https://devolutions.net/pam' } } } # Initialize script-scoped service variables (populated during scan) $script:EntraService = @{} $script:IAMService = @{} $script:KeyVaultService = @{} $script:StorageService = @{} # Get public, private, and check function definition files $Public = @(Get-ChildItem -Path $PSScriptRoot\Public\*.ps1 -ErrorAction SilentlyContinue) $Private = @(Get-ChildItem -Path $PSScriptRoot\Private\*.ps1 -ErrorAction SilentlyContinue) $Checks = @(Get-ChildItem -Path $PSScriptRoot\Checks\*\*.ps1 -ErrorAction SilentlyContinue) # Dot source the files foreach ($import in @($Private + $Checks + $Public)) { try { Write-Verbose "Importing $($import.FullName)" . $import.FullName } catch { Write-Error "Failed to import function $($import.FullName): $_" } } # Export public functions foreach ($file in $Public) { Export-ModuleMember -Function $file.BaseName } # ============================================================================ # PSU App Function - Returns the CIEM dashboard for PowerShell Universal # ============================================================================ function New-DevolutionsCIEMApp { <# .SYNOPSIS Creates the Devolutions CIEM PowerShell Universal App. .DESCRIPTION Returns a PSU dashboard for Cloud Infrastructure Entitlement Management. This function is called by PSU when the app is loaded via the -Module/-Command pattern. .EXAMPLE New-DevolutionsCIEMApp .NOTES This function is exported for PSU to invoke via New-PSUApp -Module -Command. #> [CmdletBinding()] param() # Helper function to find config.json path function Get-CIEMConfigPath { $module = Get-Module -Name 'Devolutions.CIEM' -ErrorAction SilentlyContinue if ($module) { $configPath = Join-Path $module.ModuleBase 'config.json' if (Test-Path $configPath) { return $configPath } } if ($script:ModuleRoot) { $configPath = Join-Path $script:ModuleRoot 'config.json' if (Test-Path $configPath) { return $configPath } } return $null } # Sample findings data for PoC demonstration $SampleFindings = @( [PSCustomObject]@{ Id = 'ENTRA-001'; CheckId = 'entra_id_mfa_not_enabled_for_users'; Title = 'MFA Not Enabled for Users' Severity = 'CRITICAL'; Status = 'FAIL'; Provider = 'Azure'; Service = 'Entra ID' ResourceId = 'user@contoso.com'; ResourceName = 'John Doe' Description = 'Multi-factor authentication is not enabled for this user account' Remediation = 'Enable MFA for all user accounts through Azure AD > Security > MFA' ComplianceFramework = 'CIS Azure 1.4' } [PSCustomObject]@{ Id = 'ENTRA-002'; CheckId = 'entra_id_no_conditional_access_policy'; Title = 'No Conditional Access Policies' Severity = 'HIGH'; Status = 'FAIL'; Provider = 'Azure'; Service = 'Entra ID' ResourceId = 'tenant-12345'; ResourceName = 'Contoso Tenant' Description = 'No conditional access policies are configured for the tenant' Remediation = 'Configure conditional access policies in Azure AD' ComplianceFramework = 'CIS Azure 1.4' } [PSCustomObject]@{ Id = 'IAM-001'; CheckId = 'iam_custom_role_excessive_permissions'; Title = 'Custom Role with Excessive Permissions' Severity = 'HIGH'; Status = 'FAIL'; Provider = 'Azure'; Service = 'IAM' ResourceId = '/subscriptions/sub-123/providers/Microsoft.Authorization/roleDefinitions/role-456' ResourceName = 'Custom Admin Role' Description = 'Custom role has wildcard (*) permissions' Remediation = 'Review and restrict permissions to least privilege' ComplianceFramework = 'CIS Azure 1.4' } [PSCustomObject]@{ Id = 'KV-001'; CheckId = 'keyvault_secrets_expiration_not_set'; Title = 'KeyVault Secrets Without Expiration' Severity = 'MEDIUM'; Status = 'FAIL'; Provider = 'Azure'; Service = 'KeyVault' ResourceId = '/subscriptions/sub-123/resourceGroups/rg-prod/providers/Microsoft.KeyVault/vaults/kv-prod' ResourceName = 'kv-prod' Description = 'Secrets in this KeyVault do not have expiration dates set' Remediation = 'Set expiration dates on all secrets' ComplianceFramework = 'CIS Azure 1.4' } [PSCustomObject]@{ Id = 'STORAGE-001'; CheckId = 'storage_blob_public_access_enabled'; Title = 'Blob Public Access Enabled' Severity = 'CRITICAL'; Status = 'FAIL'; Provider = 'Azure'; Service = 'Storage' ResourceId = '/subscriptions/sub-123/resourceGroups/rg-prod/providers/Microsoft.Storage/storageAccounts/stprod123' ResourceName = 'stprod123' Description = 'Storage account allows public blob access' Remediation = 'Disable public blob access on the storage account' ComplianceFramework = 'CIS Azure 1.4' } [PSCustomObject]@{ Id = 'ENTRA-003'; CheckId = 'entra_id_security_defaults_disabled'; Title = 'Security Defaults Disabled' Severity = 'MEDIUM'; Status = 'FAIL'; Provider = 'Azure'; Service = 'Entra ID' ResourceId = 'tenant-12345'; ResourceName = 'Contoso Tenant' Description = 'Security defaults are disabled and no conditional access policies exist' Remediation = 'Enable security defaults or configure conditional access' ComplianceFramework = 'CIS Azure 1.4' } [PSCustomObject]@{ Id = 'KV-002'; CheckId = 'keyvault_rbac_enabled'; Title = 'KeyVault RBAC Enabled' Severity = 'INFO'; Status = 'PASS'; Provider = 'Azure'; Service = 'KeyVault' ResourceId = '/subscriptions/sub-123/resourceGroups/rg-prod/providers/Microsoft.KeyVault/vaults/kv-secure' ResourceName = 'kv-secure' Description = 'RBAC is properly enabled for KeyVault access' Remediation = 'N/A - Check passed' ComplianceFramework = 'CIS Azure 1.4' } [PSCustomObject]@{ Id = 'STORAGE-002'; CheckId = 'storage_encryption_at_rest'; Title = 'Encryption at Rest Enabled' Severity = 'INFO'; Status = 'PASS'; Provider = 'Azure'; Service = 'Storage' ResourceId = '/subscriptions/sub-123/resourceGroups/rg-prod/providers/Microsoft.Storage/storageAccounts/stprod456' ResourceName = 'stprod456' Description = 'Storage account has encryption at rest enabled' Remediation = 'N/A - Check passed' ComplianceFramework = 'CIS Azure 1.4' } ) # Define navigation $Navigation = @( New-UDListItem -Label 'Dashboard' -Icon (New-UDIcon -Icon 'Home') -Href '/ciem' New-UDListItem -Label 'Findings' -Icon (New-UDIcon -Icon 'ExclamationTriangle') -Href '/ciem/findings' New-UDListItem -Label 'Scan' -Icon (New-UDIcon -Icon 'Play') -Href '/ciem/scan' New-UDListItem -Label 'Configuration' -Icon (New-UDIcon -Icon 'Cog') -Href '/ciem/config' New-UDListItem -Label 'About' -Icon (New-UDIcon -Icon 'InfoCircle') -Href '/ciem/about' ) # Page: Dashboard $DashboardPage = New-UDPage -Name 'Dashboard' -Url '/ciem' -Content { $Findings = $SampleFindings $FailedFindings = $Findings | Where-Object { $_.Status -eq 'FAIL' } $PassedFindings = $Findings | Where-Object { $_.Status -eq 'PASS' } $CriticalCount = ($FailedFindings | Where-Object { $_.Severity -eq 'CRITICAL' }).Count $HighCount = ($FailedFindings | Where-Object { $_.Severity -eq 'HIGH' }).Count $MediumCount = ($FailedFindings | Where-Object { $_.Severity -eq 'MEDIUM' }).Count $LowCount = ($FailedFindings | Where-Object { $_.Severity -eq 'LOW' }).Count New-UDTypography -Text 'Devolutions CIEM Dashboard' -Variant 'h4' -Style @{ marginBottom = '20px'; marginTop = '10px' } New-UDTypography -Text 'Cloud Infrastructure Entitlement Management - Security Findings Overview' -Variant 'subtitle1' -Style @{ marginBottom = '30px'; color = '#666' } New-UDGrid -Container -Content { New-UDGrid -Item -ExtraSmallSize 12 -SmallSize 6 -MediumSize 3 -Content { New-UDCard -Title 'Total Findings' -Content { New-UDTypography -Text $Findings.Count -Variant 'h3' -Style @{ color = '#1976d2'; textAlign = 'center' } } -Style @{ textAlign = 'center' } } New-UDGrid -Item -ExtraSmallSize 12 -SmallSize 6 -MediumSize 3 -Content { New-UDCard -Title 'Failed Checks' -Content { New-UDTypography -Text $FailedFindings.Count -Variant 'h3' -Style @{ color = '#f44336'; textAlign = 'center' } } -Style @{ textAlign = 'center' } } New-UDGrid -Item -ExtraSmallSize 12 -SmallSize 6 -MediumSize 3 -Content { New-UDCard -Title 'Passed Checks' -Content { New-UDTypography -Text $PassedFindings.Count -Variant 'h3' -Style @{ color = '#4caf50'; textAlign = 'center' } } -Style @{ textAlign = 'center' } } New-UDGrid -Item -ExtraSmallSize 12 -SmallSize 6 -MediumSize 3 -Content { New-UDCard -Title 'Critical Issues' -Content { New-UDTypography -Text $CriticalCount -Variant 'h3' -Style @{ color = '#9c27b0'; textAlign = 'center' } } -Style @{ textAlign = 'center' } } } New-UDGrid -Container -Content { New-UDGrid -Item -ExtraSmallSize 12 -MediumSize 6 -Content { New-UDCard -Title 'Findings by Severity' -Content { $SeverityData = @( @{ Name = 'Critical'; Count = $CriticalCount; color = '#9c27b0' } @{ Name = 'High'; Count = $HighCount; color = '#f44336' } @{ Name = 'Medium'; Count = $MediumCount; color = '#ff9800' } @{ Name = 'Low'; Count = $LowCount; color = '#2196f3' } ) | Where-Object { $_.Count -gt 0 } if ($SeverityData.Count -gt 0) { New-UDChartJS -Type 'doughnut' -Data $SeverityData -DataProperty Count -LabelProperty Name -BackgroundColor @('#9c27b0', '#f44336', '#ff9800', '#2196f3') } else { New-UDTypography -Text 'No failed findings' -Style @{ textAlign = 'center'; padding = '40px' } } } } New-UDGrid -Item -ExtraSmallSize 12 -MediumSize 6 -Content { New-UDCard -Title 'Findings by Service' -Content { $ServiceData = $FailedFindings | Group-Object -Property Service | ForEach-Object { @{ Name = $_.Name; Count = $_.Count } } if ($ServiceData.Count -gt 0) { New-UDChartJS -Type 'bar' -Data $ServiceData -DataProperty Count -LabelProperty Name -BackgroundColor '#1976d2' } else { New-UDTypography -Text 'No failed findings' -Style @{ textAlign = 'center'; padding = '40px' } } } } } New-UDCard -Title 'Recent Critical & High Findings' -Style @{ marginTop = '20px' } -Content { $CriticalHighFindings = $FailedFindings | Where-Object { $_.Severity -in @('CRITICAL', 'HIGH') } | Select-Object -First 5 if ($CriticalHighFindings.Count -gt 0) { New-UDTable -Data $CriticalHighFindings -Columns @( New-UDTableColumn -Property 'Id' -Title 'ID' New-UDTableColumn -Property 'Title' -Title 'Finding' New-UDTableColumn -Property 'Severity' -Title 'Severity' -Render { $color = switch ($EventData.Severity) { 'CRITICAL' { '#9c27b0' } 'HIGH' { '#f44336' } default { '#666' } } New-UDChip -Label $EventData.Severity -Style @{ backgroundColor = $color; color = 'white' } } New-UDTableColumn -Property 'Service' -Title 'Service' New-UDTableColumn -Property 'ResourceName' -Title 'Resource' ) } else { New-UDTypography -Text 'No critical or high severity findings!' -Style @{ padding = '20px'; color = '#4caf50' } } } } -Navigation $Navigation -NavigationLayout permanent # Page: Findings $FindingsPage = New-UDPage -Name 'Findings' -Url '/ciem/findings' -Content { New-UDTypography -Text 'Security Findings' -Variant 'h4' -Style @{ marginBottom = '20px'; marginTop = '10px' } New-UDTypography -Text 'Detailed view of all CIEM security findings' -Variant 'subtitle1' -Style @{ marginBottom = '30px'; color = '#666' } New-UDDataGrid -LoadRows { $Data = $SampleFindings | ForEach-Object { @{ id = $_.Id; checkId = $_.CheckId; title = $_.Title; severity = $_.Severity; status = $_.Status provider = $_.Provider; service = $_.Service; resourceName = $_.ResourceName description = $_.Description; remediation = $_.Remediation } } $Data | Out-UDDataGridData -Context $EventData -TotalRows $Data.Count } -Columns @( New-UDDataGridColumn -Field 'id' -HeaderName 'ID' -Width 120 New-UDDataGridColumn -Field 'title' -HeaderName 'Finding' -Flex 1 New-UDDataGridColumn -Field 'severity' -HeaderName 'Severity' -Width 120 -Render { $color = switch ($EventData.severity) { 'CRITICAL' { '#9c27b0' } 'HIGH' { '#f44336' } 'MEDIUM' { '#ff9800' } 'LOW' { '#2196f3' } 'INFO' { '#4caf50' } default { '#666' } } New-UDChip -Label $EventData.severity -Style @{ backgroundColor = $color; color = 'white' } } New-UDDataGridColumn -Field 'status' -HeaderName 'Status' -Width 100 -Render { $color = if ($EventData.status -eq 'FAIL') { '#f44336' } else { '#4caf50' } New-UDChip -Label $EventData.status -Style @{ backgroundColor = $color; color = 'white' } } New-UDDataGridColumn -Field 'service' -HeaderName 'Service' -Width 120 New-UDDataGridColumn -Field 'resourceName' -HeaderName 'Resource' -Width 150 ) -AutoHeight $true -Pagination -ShowQuickFilter -LoadDetailContent { New-UDCard -Content { New-UDTypography -Text "Description" -Variant 'h6' New-UDTypography -Text $EventData.row.description -Style @{ marginBottom = '15px' } New-UDTypography -Text "Remediation" -Variant 'h6' New-UDTypography -Text $EventData.row.remediation -Style @{ marginBottom = '15px' } New-UDButton -Text 'View in Devolutions PAM' -Variant 'contained' -OnClick { Show-UDToast -Message 'Redirecting to Devolutions PAM for remediation...' -Duration 3000 } } } } -Navigation $Navigation -NavigationLayout permanent # Page: Scan $ScanPage = New-UDPage -Name 'Scan' -Url '/ciem/scan' -Content { New-UDTypography -Text 'Run CIEM Scan' -Variant 'h4' -Style @{ marginBottom = '20px'; marginTop = '10px' } New-UDTypography -Text 'Configure and execute a CIEM security scan against your cloud environment' -Variant 'subtitle1' -Style @{ marginBottom = '30px'; color = '#666' } New-UDCard -Title 'Scan Configuration' -Content { New-UDAlert -Severity 'info' -Text 'This is a PoC demonstration. In production, this would connect to actual Azure subscriptions using the Devolutions.CIEM module.' New-UDElement -Tag 'div' -Content { New-UDSelect -Id 'provider' -Label 'Cloud Provider' -Option { New-UDSelectOption -Name 'Azure' -Value 'azure' New-UDSelectOption -Name 'AWS' -Value 'aws' } -DefaultValue 'azure' -FullWidth } -Attributes @{ style = @{ marginBottom = '16px'; marginTop = '16px' } } New-UDElement -Tag 'div' -Content { New-UDTextbox -Id 'subscriptionId' -Label 'Subscription ID (Optional)' -Placeholder 'Leave empty to scan all accessible subscriptions' -FullWidth } -Attributes @{ style = @{ marginBottom = '16px' } } New-UDElement -Tag 'div' -Content { New-UDCheckbox -Id 'includePassedChecks' -Label 'Include Passed Checks in Results' -Checked $true } -Attributes @{ style = @{ marginBottom = '16px' } } New-UDElement -Tag 'div' -Content { New-UDSelect -Id 'outputFormat' -Label 'Output Format' -Option { New-UDSelectOption -Name 'Dashboard (Default)' -Value 'dashboard' New-UDSelectOption -Name 'JSON Export' -Value 'json' New-UDSelectOption -Name 'CSV Export' -Value 'csv' } -DefaultValue 'dashboard' -FullWidth } -Attributes @{ style = @{ marginBottom = '24px' } } New-UDStack -Direction 'row' -Spacing 2 -Content { New-UDButton -Text 'Start Scan' -Variant 'contained' -Color 'primary' -OnClick { $Provider = (Get-UDElement -Id 'provider').value Show-UDToast -Message "Scan initiated for $Provider provider..." -Duration 5000 Show-UDToast -Message "PoC Mode: Displaying sample findings data" -Duration 5000 -BackgroundColor '#ff9800' Start-Sleep -Seconds 2 Invoke-UDRedirect '/ciem/findings' } New-UDButton -Text 'Test Authentication' -Variant 'outlined' -Color 'primary' -OnClick { $Provider = (Get-UDElement -Id 'provider').value Show-UDToast -Message "Testing $Provider authentication..." -Duration 3000 try { switch ($Provider) { 'azure' { $context = Get-AzContext -ErrorAction Stop if ($context) { $message = "Azure authentication successful!`nAccount: $($context.Account.Id)`nSubscription: $($context.Subscription.Name)`nTenant: $($context.Tenant.Id)" Show-UDToast -Message $message -Duration 8000 -BackgroundColor '#4caf50' } else { Show-UDToast -Message "Azure: Not authenticated. Run Connect-AzAccount first." -Duration 5000 -BackgroundColor '#f44336' } } 'aws' { $identity = Get-STSCallerIdentity -ErrorAction Stop if ($identity) { $message = "AWS authentication successful!`nAccount: $($identity.Account)`nARN: $($identity.Arn)`nUserId: $($identity.UserId)" Show-UDToast -Message $message -Duration 8000 -BackgroundColor '#4caf50' } else { Show-UDToast -Message "AWS: Not authenticated. Configure AWS credentials first." -Duration 5000 -BackgroundColor '#f44336' } } default { Show-UDToast -Message "Provider '$Provider' is not yet supported for authentication testing." -Duration 5000 -BackgroundColor '#ff9800' } } } catch { $errorMsg = $_.Exception.Message Show-UDToast -Message "Authentication failed for $Provider`: $errorMsg" -Duration 8000 -BackgroundColor '#f44336' } } } } New-UDCard -Title 'Recent Scan History' -Style @{ marginTop = '20px' } -Content { $ScanHistory = @( @{ Id = 1; Date = (Get-Date).AddHours(-2).ToString('yyyy-MM-dd HH:mm'); Provider = 'Azure'; Findings = 6; Duration = '45s' } @{ Id = 2; Date = (Get-Date).AddDays(-1).ToString('yyyy-MM-dd HH:mm'); Provider = 'Azure'; Findings = 8; Duration = '52s' } @{ Id = 3; Date = (Get-Date).AddDays(-3).ToString('yyyy-MM-dd HH:mm'); Provider = 'Azure'; Findings = 12; Duration = '1m 10s' } ) New-UDTable -Data $ScanHistory -Columns @( New-UDTableColumn -Property 'Date' -Title 'Scan Date' New-UDTableColumn -Property 'Provider' -Title 'Provider' New-UDTableColumn -Property 'Findings' -Title 'Failed Findings' New-UDTableColumn -Property 'Duration' -Title 'Duration' ) } } -Navigation $Navigation -NavigationLayout permanent # Page: Configuration $ConfigPage = New-UDPage -Name 'Configuration' -Url '/ciem/config' -Content { $ConfigPath = Get-CIEMConfigPath $CurrentConfig = if ($ConfigPath -and (Test-Path $ConfigPath)) { Get-Content $ConfigPath -Raw | ConvertFrom-Json } else { [PSCustomObject]@{ azure = @{ authentication = @{ method = 'CurrentContext'; servicePrincipal = @{ tenantId = $null; clientId = $null; clientSecret = $null } }; subscriptionFilter = @() } scan = @{ throttleLimit = 10; timeoutSeconds = 300; continueOnError = $true } output = @{ verboseLogging = $false } pam = @{ remediationUrl = 'https://devolutions.net/pam' } } } New-UDTypography -Text 'Configuration' -Variant 'h4' -Style @{ marginBottom = '20px'; marginTop = '10px' } New-UDTypography -Text 'Configure CIEM scan settings, authentication, and integrations' -Variant 'subtitle1' -Style @{ marginBottom = '30px'; color = '#666' } New-UDGrid -Container -Spacing 3 -Content { New-UDGrid -Item -ExtraSmallSize 12 -MediumSize 6 -Content { New-UDCard -Title 'Azure Authentication' -Content { New-UDElement -Tag 'div' -Content { New-UDSelect -Id 'authMethod' -Label 'Authentication Method' -Option { New-UDSelectOption -Name 'Current Context (Az PowerShell)' -Value 'CurrentContext' New-UDSelectOption -Name 'Service Principal' -Value 'ServicePrincipal' New-UDSelectOption -Name 'Managed Identity' -Value 'ManagedIdentity' New-UDSelectOption -Name 'Interactive Browser' -Value 'Interactive' } -DefaultValue $CurrentConfig.azure.authentication.method -FullWidth -OnChange { Sync-UDElement -Id 'spFieldsContainer' } } -Attributes @{ style = @{ marginBottom = '16px' } } New-UDDynamic -Id 'spFieldsContainer' -Content { $selectedMethod = (Get-UDElement -Id 'authMethod').value if ($selectedMethod -eq 'ServicePrincipal') { New-UDElement -Tag 'div' -Content { New-UDTextbox -Id 'spTenantId' -Label 'Tenant ID' -Value $CurrentConfig.azure.authentication.servicePrincipal.tenantId -FullWidth -Placeholder 'Enter Azure AD Tenant ID' } -Attributes @{ style = @{ marginBottom = '16px' } } New-UDElement -Tag 'div' -Content { New-UDTextbox -Id 'spClientId' -Label 'Client ID (App ID)' -Value $CurrentConfig.azure.authentication.servicePrincipal.clientId -FullWidth -Placeholder 'Enter Service Principal Client ID' } -Attributes @{ style = @{ marginBottom = '16px' } } New-UDElement -Tag 'div' -Content { New-UDTextbox -Id 'spClientSecret' -Label 'Client Secret' -Type 'password' -FullWidth -Placeholder 'Enter Service Principal Secret' } -Attributes @{ style = @{ marginBottom = '16px' } } } } New-UDElement -Tag 'div' -Content { $filterValue = if ($CurrentConfig.azure.subscriptionFilter -is [array]) { $CurrentConfig.azure.subscriptionFilter -join ', ' } else { '' } New-UDTextbox -Id 'subscriptionFilter' -Label 'Subscription Filter' -Value $filterValue -FullWidth -Placeholder 'Comma-separated subscription IDs (leave empty for all)' } -Attributes @{ style = @{ marginTop = '16px' } } } } New-UDGrid -Item -ExtraSmallSize 12 -MediumSize 6 -Content { New-UDCard -Title 'Scan Settings' -Content { New-UDElement -Tag 'div' -Content { New-UDTextbox -Id 'throttleLimit' -Label 'Throttle Limit' -Value $CurrentConfig.scan.throttleLimit -FullWidth -Placeholder '1-100' -Type 'number' } -Attributes @{ style = @{ marginBottom = '16px' } } New-UDElement -Tag 'div' -Content { New-UDTextbox -Id 'timeoutSeconds' -Label 'Timeout (Seconds)' -Value $CurrentConfig.scan.timeoutSeconds -FullWidth -Placeholder 'Scan timeout in seconds' -Type 'number' } -Attributes @{ style = @{ marginBottom = '16px' } } New-UDElement -Tag 'div' -Content { New-UDCheckbox -Id 'continueOnError' -Label 'Continue on Error' -Checked $CurrentConfig.scan.continueOnError } } } New-UDGrid -Item -ExtraSmallSize 12 -MediumSize 6 -Content { New-UDCard -Title 'PAM Integration' -Content { New-UDElement -Tag 'div' -Content { New-UDTextbox -Id 'pamUrl' -Label 'Remediation URL' -Value $CurrentConfig.pam.remediationUrl -FullWidth -Placeholder 'URL for Devolutions PAM integration' } } } New-UDGrid -Item -ExtraSmallSize 12 -MediumSize 6 -Content { New-UDCard -Title 'Output Settings' -Content { New-UDElement -Tag 'div' -Content { New-UDCheckbox -Id 'verboseLogging' -Label 'Verbose Logging' -Checked $CurrentConfig.output.verboseLogging } } } } New-UDElement -Tag 'div' -Content { New-UDStack -Direction 'row' -Spacing 2 -Content { New-UDButton -Text 'Save Configuration' -Variant 'contained' -Color 'primary' -OnClick { try { $authMethod = (Get-UDElement -Id 'authMethod').value $subscriptionFilterRaw = (Get-UDElement -Id 'subscriptionFilter').value $throttleLimit = [int](Get-UDElement -Id 'throttleLimit').value $timeoutSeconds = [int](Get-UDElement -Id 'timeoutSeconds').value $continueOnError = (Get-UDElement -Id 'continueOnError').checked $verboseLogging = (Get-UDElement -Id 'verboseLogging').checked $pamUrl = (Get-UDElement -Id 'pamUrl').value $subscriptionFilter = if ([string]::IsNullOrWhiteSpace($subscriptionFilterRaw)) { @() } else { $subscriptionFilterRaw -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ } } $settings = @{ 'azure.authentication.method' = $authMethod 'azure.subscriptionFilter' = $subscriptionFilter 'scan.throttleLimit' = $throttleLimit 'scan.timeoutSeconds' = $timeoutSeconds 'scan.continueOnError' = $continueOnError 'output.verboseLogging' = $verboseLogging 'pam.remediationUrl' = $pamUrl } if ($authMethod -eq 'ServicePrincipal') { $settings['azure.authentication.servicePrincipal.tenantId'] = (Get-UDElement -Id 'spTenantId').value $settings['azure.authentication.servicePrincipal.clientId'] = (Get-UDElement -Id 'spClientId').value $spClientSecret = (Get-UDElement -Id 'spClientSecret').value if (-not [string]::IsNullOrEmpty($spClientSecret)) { $settings['azure.authentication.servicePrincipal.clientSecret'] = $spClientSecret } } $ConfigPath = Get-CIEMConfigPath if (-not $ConfigPath) { throw 'Configuration file not found. Ensure the Devolutions.CIEM module is installed.' } $config = Get-Content $ConfigPath -Raw | ConvertFrom-Json -AsHashtable function Set-NestedValue { param([hashtable]$Hashtable, [string]$Path, $Value) $parts = $Path -split '\.'; $current = $Hashtable for ($i = 0; $i -lt $parts.Count - 1; $i++) { $part = $parts[$i]; if (-not $current.ContainsKey($part)) { $current[$part] = @{} }; $current = $current[$part] } $current[$parts[-1]] = $Value } foreach ($key in $settings.Keys) { Set-NestedValue -Hashtable $config -Path $key -Value $settings[$key] } $jsonContent = $config | ConvertTo-Json -Depth 10 Set-Content -Path $ConfigPath -Value $jsonContent -Encoding UTF8 Show-UDToast -Message 'Configuration saved successfully!' -Duration 5000 -BackgroundColor '#4caf50' } catch { Show-UDToast -Message "Failed to save configuration: $($_.Exception.Message)" -Duration 8000 -BackgroundColor '#f44336' } } New-UDButton -Text 'Reset to Defaults' -Variant 'outlined' -Color 'secondary' -OnClick { try { Set-UDElement -Id 'authMethod' -Properties @{ value = 'CurrentContext' } Set-UDElement -Id 'subscriptionFilter' -Properties @{ value = '' } Set-UDElement -Id 'throttleLimit' -Properties @{ value = '10' } Set-UDElement -Id 'timeoutSeconds' -Properties @{ value = '300' } Set-UDElement -Id 'continueOnError' -Properties @{ checked = $true } Set-UDElement -Id 'verboseLogging' -Properties @{ checked = $false } Set-UDElement -Id 'pamUrl' -Properties @{ value = 'https://devolutions.net/pam' } Sync-UDElement -Id 'spFieldsContainer' Show-UDToast -Message 'Form reset to default values. Click Save to apply.' -Duration 5000 -BackgroundColor '#ff9800' } catch { Show-UDToast -Message "Failed to reset: $($_.Exception.Message)" -Duration 8000 -BackgroundColor '#f44336' } } } } -Attributes @{ style = @{ marginTop = '24px' } } } -Navigation $Navigation -NavigationLayout permanent # Page: About $AboutPage = New-UDPage -Name 'About' -Url '/ciem/about' -Content { New-UDTypography -Text 'About Devolutions CIEM' -Variant 'h4' -Style @{ marginBottom = '20px'; marginTop = '10px' } New-UDCard -Title 'Cloud Infrastructure Entitlement Management' -Content { New-UDTypography -Text 'Devolutions CIEM is a security scanning solution that helps identify identity and access management issues across your cloud infrastructure.' -Variant 'body1' -Style @{ marginBottom = '20px' } New-UDTypography -Text 'Key Features:' -Variant 'h6' -Style @{ marginTop = '20px' } New-UDList -Content { New-UDListItem -Label '46 Azure identity-focused security checks' New-UDListItem -Label 'Entra ID (Azure AD) security validation' New-UDListItem -Label 'IAM/RBAC permissions analysis' New-UDListItem -Label 'KeyVault access and configuration checks' New-UDListItem -Label 'Storage account security validation' New-UDListItem -Label 'Integration with Devolutions PAM for remediation' } New-UDTypography -Text 'Version Information:' -Variant 'h6' -Style @{ marginTop = '20px' } New-UDTable -Data @( @{ Property = 'Module Version'; Value = '0.2.0' } @{ Property = 'PSU App Version'; Value = '0.2.0' } @{ Property = 'PowerShell Universal'; Value = '5.4+' } @{ Property = 'Author'; Value = 'Adam Bertram' } @{ Property = 'Company'; Value = 'Devolutions Inc.' } ) -Columns @( New-UDTableColumn -Property 'Property' -Title 'Property' New-UDTableColumn -Property 'Value' -Title 'Value' ) -Dense New-UDTypography -Text 'Learn More:' -Variant 'h6' -Style @{ marginTop = '20px' } New-UDButton -Text 'Devolutions PAM' -Href 'https://devolutions.net/pam' -Variant 'outlined' -Style @{ marginRight = '10px' } New-UDButton -Text 'Documentation' -Href 'https://docs.devolutions.net' -Variant 'outlined' } } -Navigation $Navigation -NavigationLayout permanent # Return the App New-UDApp -Title 'Devolutions CIEM' -Pages @( $DashboardPage $FindingsPage $ScanPage $ConfigPage $AboutPage ) -DefaultTheme 'Light' } Export-ModuleMember -Function New-DevolutionsCIEMApp |