apps/DevolutionsCIEM/app.ps1
|
#Requires -Version 7.0 <# .SYNOPSIS Devolutions CIEM - Cloud Infrastructure Entitlement Management Dashboard .DESCRIPTION PowerShell Universal v5 App for CIEM security findings visualization. This PoC demonstrates: - Dashboard with summary cards - Data grid for findings display - Charts for severity visualization - Navigation with multiple pages .NOTES Version: 0.1.0 Author: Adam Bertram Company: Devolutions Inc. #> # Helper function to find config.json path # Works in both local development ($PSScriptRoot) and PSU deployment (module path) function Get-CIEMConfigPath { # Try 1: If Devolutions.CIEM module is loaded, use its path $module = Get-Module -Name 'Devolutions.CIEM' -ErrorAction SilentlyContinue if ($module) { $configPath = Join-Path $module.ModuleBase 'config.json' if (Test-Path $configPath) { return $configPath } } # Try 2: Local development - script is in apps/DevolutionsCIEM/app.ps1 if ($PSScriptRoot) { $configPath = Join-Path $PSScriptRoot '..' '..' 'config.json' $configPath = [System.IO.Path]::GetFullPath($configPath) if (Test-Path $configPath) { return $configPath } } # Try 3: PSU default module locations $psuModulePaths = @( '/home/data/.powershell/Modules/Devolutions.CIEM/config.json' # Azure App Service '/root/.local/share/powershell/Modules/Devolutions.CIEM/config.json' # Linux (Join-Path $env:USERPROFILE 'Documents\PowerShell\Modules\Devolutions.CIEM\config.json') # Windows ) foreach ($path in $psuModulePaths) { if ($path -and (Test-Path $path)) { return $path } } # Return null if not found return $null } # Sample findings data for PoC demonstration # In production, this would come from Invoke-CIEMScan $script: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' ) # Theme configuration - Devolutions brand colors $Theme = @{ palette = @{ primary = @{ main = '#1976d2' } secondary = @{ main = '#dc004e' } } } # ============================================================================ # Page: Dashboard (Home) # ============================================================================ $DashboardPage = New-UDPage -Name 'Dashboard' -Url '/ciem' -Content { # Get findings data $Findings = $script:SampleFindings $FailedFindings = $Findings | Where-Object { $_.Status -eq 'FAIL' } $PassedFindings = $Findings | Where-Object { $_.Status -eq 'PASS' } # Severity counts $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 # Header 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' } # Summary Cards Row New-UDGrid -Container -Content { # Total Findings Card 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' } } # Failed Findings Card 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' } } # Passed Findings Card 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' } } # Critical Issues Card 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' } } } # Charts Row New-UDGrid -Container -Content { # Severity Distribution Pie Chart 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' } } } } # Service Distribution Bar Chart 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' } } } } } # Recent Critical Findings 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 (Detailed View) # ============================================================================ $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 = $script: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 # In production: Invoke-UDRedirect "https://devolutions.net/pam" } } } } -Navigation $Navigation -NavigationLayout permanent # ============================================================================ # Page: Scan Configuration # ============================================================================ $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' } } # Action buttons 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' # In production, this would call: # $results = Invoke-CIEMScan -Provider $Provider -SubscriptionId $subscriptionId 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' } } } } # Scan History (placeholder) 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: 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.1.0-alpha' } @{ Property = 'PSU App Version'; Value = '0.1.0' } @{ Property = 'PowerShell Universal'; Value = '5.4.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 # ============================================================================ # Page: Configuration # ============================================================================ $ConfigPage = New-UDPage -Name 'Configuration' -Url '/ciem/config' -Content { # Load current configuration $ConfigPath = Get-CIEMConfigPath $CurrentConfig = if ($ConfigPath -and (Test-Path $ConfigPath)) { Get-Content $ConfigPath -Raw | ConvertFrom-Json } else { # Defaults if config doesn't exist [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 { # Azure Authentication Card 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' } } # Service Principal fields - conditionally visible 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' } } } } # Scan Settings Card 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 } } } # PAM Integration Card 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' } } } # Output Settings Card 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 } } } } # Action Buttons New-UDElement -Tag 'div' -Content { New-UDStack -Direction 'row' -Spacing 2 -Content { New-UDButton -Text 'Save Configuration' -Variant 'contained' -Color 'primary' -OnClick { try { # Collect all form values $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 # Parse subscription filter $subscriptionFilter = if ([string]::IsNullOrWhiteSpace($subscriptionFilterRaw)) { @() } else { $subscriptionFilterRaw -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ } } # Build settings hashtable $settings = @{ 'azure.authentication.method' = $authMethod 'azure.subscriptionFilter' = $subscriptionFilter 'scan.throttleLimit' = $throttleLimit 'scan.timeoutSeconds' = $timeoutSeconds 'scan.continueOnError' = $continueOnError 'output.verboseLogging' = $verboseLogging 'pam.remediationUrl' = $pamUrl } # Add service principal settings if applicable if ($authMethod -eq 'ServicePrincipal') { $spTenantId = (Get-UDElement -Id 'spTenantId').value $spClientId = (Get-UDElement -Id 'spClientId').value $spClientSecret = (Get-UDElement -Id 'spClientSecret').value $settings['azure.authentication.servicePrincipal.tenantId'] = $spTenantId $settings['azure.authentication.servicePrincipal.clientId'] = $spClientId if (-not [string]::IsNullOrEmpty($spClientSecret)) { $settings['azure.authentication.servicePrincipal.clientSecret'] = $spClientSecret } } # Read existing config $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 # Helper function to set nested values 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 } # Apply settings foreach ($key in $settings.Keys) { Set-NestedValue -Hashtable $config -Path $key -Value $settings[$key] } # Save config $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 form elements to default values 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' } # Refresh service principal fields visibility 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 # ============================================================================ # Create the App # ============================================================================ New-UDApp -Title 'Devolutions CIEM' -Pages @( $DashboardPage $FindingsPage $ScanPage $ConfigPage $AboutPage ) -DefaultTheme 'Light' |