Providers/AD/Private/Set-ADTestPasswordSecret.ps1
|
function Set-ADTestPasswordSecret { <# .SYNOPSIS Stores AD test environment passwords in a SecretStore vault .DESCRIPTION Takes password data and stores it securely in a specified SecretStore vault. Each password is stored as a SecureString with associated metadata. .PARAMETER PasswordData Array of objects containing password information to store. Each object should have: ServiceAccountName, Password, CreatedDate .PARAMETER VaultName Name of the secret vault to store passwords in. Defaults to "ADTestEnvironment" .PARAMETER CorrelationId Correlation ID for tracking related operations .PARAMETER OverwriteExisting Overwrite existing secrets with the same name .PARAMETER SecretNamePrefix Prefix for secret names. Defaults to service account name .EXAMPLE $passwordData = @( @{ ServiceAccountName = 'svc-app1'; Password = 'SecurePass123!'; CreatedDate = Get-Date } ) Set-ADTestPasswordSecret -PasswordData $passwordData Stores passwords in the default ADTestEnvironment vault .EXAMPLE Set-ADTestPasswordSecret -PasswordData $passwords -VaultName "ProdVault" -OverwriteExisting Stores passwords in a custom vault, overwriting any existing secrets .OUTPUTS PSCustomObject containing storage operation results .NOTES Author: Jeffrey Stuhr Version: 1.0.0 Last Updated: 2025-08-05 Secret Naming Convention: - Format: {ServiceAccountName}-{Timestamp} - Timestamp: yyyyMMdd-HHmmss - Ensures unique secret names and version tracking #> [CmdletBinding(SupportsShouldProcess = $true)] [OutputType([PSCustomObject])] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '', Justification = 'Declared so -WhatIf and -Confirm bind and forward. Set-TestVaultSecret calls ShouldProcess once per secret, which is the granularity worth confirming; prompting here as well would ask twice for one write.')] param( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [array]$PasswordData, [Parameter()] [ValidateNotNullOrEmpty()] [string]$VaultName = "ADTestEnvironment", [Parameter()] [System.Guid]$CorrelationId = [System.Guid]::NewGuid(), [Parameter()] [switch]$OverwriteExisting, [Parameter()] [string]$SecretNamePrefix ) begin { Write-Verbose "Starting Set-ADTestPasswordSecret - CorrelationId: $CorrelationId" $result = @{ CorrelationId = $CorrelationId VaultName = $VaultName TotalPasswords = $PasswordData.Count StoredSecrets = @() SkippedSecrets = @() FailedSecrets = @() Timestamp = Get-Date -Format "yyyyMMdd-HHmmss" Errors = @() Warnings = @() } # Ensure SecretManagement module is available try { Import-Module Microsoft.PowerShell.SecretManagement -Force -ErrorAction Stop Write-Verbose "SecretManagement module imported successfully" } catch { throw "SecretManagement module not available. Run Test-ADTestSecretStorePrerequisite first. Error: $($_.Exception.Message)" } # Verify vault exists $vault = Get-SecretVault -Name $VaultName -ErrorAction SilentlyContinue if (-not $vault) { throw "SecretStore vault '$VaultName' not found. Create it first using New-ADTestSecretVault." } Write-Verbose "Using vault: $VaultName" } process { try { Write-Verbose "Processing $($PasswordData.Count) password entries" foreach ($entry in $PasswordData) { $secretResult = @{ ServiceAccount = $null SecretName = $null Success = $false Error = $null Skipped = $false SkipReason = $null } try { # Validate required properties if (-not $entry.ServiceAccountName) { $secretResult.SkipReason = "Missing ServiceAccountName" $secretResult.Skipped = $true $result.SkippedSecrets += [PSCustomObject]$secretResult $result.Warnings += "Skipping entry with missing ServiceAccountName" Write-Warning "Skipping entry with missing ServiceAccountName" continue } if (-not $entry.Password) { $secretResult.ServiceAccount = $entry.ServiceAccountName $secretResult.SkipReason = "Missing Password" $secretResult.Skipped = $true $result.SkippedSecrets += [PSCustomObject]$secretResult $result.Warnings += "Skipping entry with missing Password for account: $($entry.ServiceAccountName)" Write-Warning "Skipping entry with missing Password for account: $($entry.ServiceAccountName)" continue } $secretResult.ServiceAccount = $entry.ServiceAccountName # Create secret name. # # Always namespaced, because SecretStore vault registrations are aliases # onto ONE per-user store rather than isolated containers - a secret # written to one vault is readable from every other. Verified: a probe # written to a vault named EntraTestEnvironment read back unchanged from a # vault named OktaTestEnvironment. So a bare "<account>-<timestamp>" sits # in the same namespace as every other module's secrets, and the vault name # protects nothing. if ($SecretNamePrefix) { $secretName = "$SecretNamePrefix-$($entry.ServiceAccountName)-$($result.Timestamp)" } else { $secretName = "ADTestEnvironment-$($entry.ServiceAccountName)-$($result.Timestamp)" } $secretResult.SecretName = $secretName # Check if secret already exists $existingSecret = Get-SecretInfo -Name $secretName -Vault $VaultName -ErrorAction SilentlyContinue if ($existingSecret -and -not $OverwriteExisting) { $secretResult.SkipReason = "Secret already exists (use -OverwriteExisting to replace)" $secretResult.Skipped = $true $result.SkippedSecrets += [PSCustomObject]$secretResult $result.Warnings += "Secret '$secretName' already exists - skipping (use -OverwriteExisting to replace)" Write-Warning "Secret '$secretName' already exists - skipping" continue } # Create metadata object $metadata = @{ ServiceAccount = $entry.ServiceAccountName CreatedDate = if ($entry.CreatedDate) { $entry.CreatedDate.ToString('yyyy-MM-dd HH:mm:ss') } else { (Get-Date).ToString('yyyy-MM-dd HH:mm:ss') } StoredDate = (Get-Date).ToString('yyyy-MM-dd HH:mm:ss') CorrelationId = $CorrelationId.ToString() Source = "ADTestEnvironment" } # Add optional metadata if ($entry.Description) { $metadata.Description = $entry.Description } if ($entry.Department) { $metadata.Department = $entry.Department } if ($entry.ExpirationDate) { $metadata.ExpirationDate = $entry.ExpirationDate.ToString('yyyy-MM-dd HH:mm:ss') } # Convert password to SecureString without binding the plain value to a # cmdlet parameter, where a transcript would capture it. $securePassword = ConvertTo-TestSecureString -PlainText $entry.Password # Store the secret Write-Verbose "Storing secret: $secretName" Set-Secret -Name $secretName -Secret $securePassword -Vault $VaultName -Metadata $metadata -ErrorAction Stop $secretResult.Success = $true $result.StoredSecrets += [PSCustomObject]@{ SecretName = $secretName ServiceAccount = $entry.ServiceAccountName StoredDate = Get-Date Metadata = $metadata } Write-Verbose "Successfully stored secret for: $($entry.ServiceAccountName)" } catch { $errorMsg = "Failed to store secret for '$($entry.ServiceAccountName)': $($_.Exception.Message)" $secretResult.Error = $errorMsg $result.FailedSecrets += [PSCustomObject]$secretResult $result.Errors += $errorMsg Write-Error $errorMsg continue } } # Summary $storedCount = $result.StoredSecrets.Count $skippedCount = $result.SkippedSecrets.Count $failedCount = $result.FailedSecrets.Count Write-Verbose "Password storage complete - Stored: $storedCount, Skipped: $skippedCount, Failed: $failedCount" if ($storedCount -eq 0 -and $failedCount -gt 0) { throw "No passwords were successfully stored. See errors for details." } } catch { $errorMsg = "Failed to store AD test passwords: $($_.Exception.Message)" $result.Errors += $errorMsg Write-Error $errorMsg -ErrorAction Stop } } end { Write-Verbose "Completed Set-ADTestPasswordSecret - CorrelationId: $CorrelationId" return [PSCustomObject]$result } } |