Private/Send-ApprovalRequest.ps1
|
function Send-ApprovalRequest { <# .SYNOPSIS Sends an approval request via Console, Email, Teams, or Slack. .DESCRIPTION Dispatches an approval request for a runbook step through the configured communication channel. Console mode uses interactive Read-Host. Email uses Send-MailMessage. Teams and Slack post to incoming webhooks with rich formatting. Returns an approval response object. #> [CmdletBinding()] param( [Parameter(Mandatory)] [ValidateSet('Console', 'Email', 'Teams', 'Slack')] [string]$Method, [Parameter()] [string]$Contact, [Parameter(Mandatory)] [string]$RunbookName, [Parameter(Mandatory)] [string]$StepDescription, [Parameter()] [object]$BlastRadius, [Parameter()] [string]$ComputerName, [Parameter()] [string]$SmtpServer, [Parameter()] [string]$EmailFrom, [Parameter()] [PSCredential]$SmtpCredential, [Parameter()] [switch]$UseSsl ) $requestId = [guid]::NewGuid().ToString('N').Substring(0, 8) $timestamp = Get-Date $blastLevel = if ($BlastRadius) { $BlastRadius.Level } else { 'Unknown' } $blastReasons = if ($BlastRadius -and $BlastRadius.Reasons) { $BlastRadius.Reasons -join '; ' } else { 'Not assessed' } $approvalResult = [PSCustomObject]@{ RequestId = $requestId Method = $Method RunbookName = $RunbookName StepDescription = $StepDescription ComputerName = $ComputerName BlastLevel = $blastLevel Approved = $false Approver = $null ResponseTime = $null RequestedAt = $timestamp.ToString('o') RespondedAt = $null Notes = $null } switch ($Method) { 'Console' { Write-Host '' Write-Host '========================================' -ForegroundColor Yellow Write-Host ' APPROVAL REQUIRED' -ForegroundColor Yellow Write-Host '========================================' -ForegroundColor Yellow Write-Host '' Write-Host " Runbook: $RunbookName" -ForegroundColor Cyan Write-Host " Step: $StepDescription" -ForegroundColor Cyan Write-Host " Target: $ComputerName" -ForegroundColor Cyan Write-Host " Blast Radius: $blastLevel" -ForegroundColor $(if ($blastLevel -eq 'Critical') { 'Red' } elseif ($blastLevel -eq 'High') { 'DarkYellow' } else { 'Green' }) Write-Host " Reasons: $blastReasons" -ForegroundColor Gray Write-Host '' $response = Read-Host 'Approve this action? (YES/NO)' $respondedAt = Get-Date $approvalResult.Approved = ($response -eq 'YES') $approvalResult.Approver = $env:USERNAME $approvalResult.ResponseTime = ($respondedAt - $timestamp).TotalSeconds $approvalResult.RespondedAt = $respondedAt.ToString('o') $approvalResult.Notes = if ($response -eq 'YES') { 'Approved via console' } else { "Denied via console (response: $response)" } } 'Email' { if (-not $Contact) { throw 'Contact (email address) is required for Email approval method.' } if (-not $SmtpServer) { throw 'SmtpServer is required for Email approval method.' } if (-not $EmailFrom) { $EmailFrom = "runbook-engine@$($env:USERDNSDOMAIN)" } $subject = "APPROVAL REQUIRED: $RunbookName - $StepDescription [$requestId]" $body = @" Runbook Approval Request ======================== Runbook: $RunbookName Step: $StepDescription Target: $ComputerName Blast Radius: $blastLevel Details: $blastReasons Request ID: $requestId Requested At: $($timestamp.ToString('yyyy-MM-dd HH:mm:ss')) To approve, reply to this email with "YES" in the body. To deny, reply with "NO". --- Infra-RunbookEngine - Automated Runbook Execution "@ $mailParams = @{ From = $EmailFrom To = $Contact Subject = $subject Body = $body SmtpServer = $SmtpServer } if ($SmtpCredential) { $mailParams['Credential'] = $SmtpCredential } if ($UseSsl) { $mailParams['UseSsl'] = $true } try { Send-MailMessage @mailParams -ErrorAction Stop $approvalResult.Notes = "Approval request sent via email to $Contact. Awaiting response." Write-Host "Approval request sent to $Contact. Waiting for response..." -ForegroundColor Yellow # Poll for response (in a real implementation, this would check a mailbox or webhook) Write-Host "Email approval is asynchronous. Proceeding with approval pending." -ForegroundColor Yellow $approvalResult.Approved = $false $approvalResult.Notes = "Email sent to $Contact. Manual follow-up required." } catch { Write-Warning "Failed to send approval email: $_" $approvalResult.Notes = "Failed to send email: $_" } } 'Teams' { if (-not $Contact) { throw 'Contact (Teams webhook URL) is required for Teams approval method.' } $colorMap = @{ 'Low' = '00FF00' 'Medium' = 'FFA500' 'High' = 'FF8C00' 'Critical' = 'FF0000' 'Unknown' = '808080' } $cardBody = @{ '@type' = 'MessageCard' '@context' = 'http://schema.org/extensions' themeColor = $colorMap[$blastLevel] summary = "Approval Required: $RunbookName" sections = @( @{ activityTitle = "Runbook Approval Required" activitySubtitle = "Request ID: $requestId" facts = @( @{ name = 'Runbook'; value = $RunbookName } @{ name = 'Step'; value = $StepDescription } @{ name = 'Target'; value = $ComputerName } @{ name = 'Blast Radius'; value = $blastLevel } @{ name = 'Details'; value = $blastReasons } @{ name = 'Requested At'; value = $timestamp.ToString('yyyy-MM-dd HH:mm:ss') } ) markdown = $true } ) } | ConvertTo-Json -Depth 10 try { Invoke-RestMethod -Uri $Contact -Method Post -Body $cardBody -ContentType 'application/json' -ErrorAction Stop $approvalResult.Notes = "Approval card posted to Teams webhook." Write-Host "Approval request posted to Teams. Awaiting response..." -ForegroundColor Yellow } catch { Write-Warning "Failed to post Teams approval request: $_" $approvalResult.Notes = "Failed to post to Teams: $_" } } 'Slack' { if (-not $Contact) { throw 'Contact (Slack webhook URL) is required for Slack approval method.' } $emoji = switch ($blastLevel) { 'Low' { ':white_check_mark:' } 'Medium' { ':warning:' } 'High' { ':bangbang:' } 'Critical' { ':rotating_light:' } default { ':question:' } } $slackBody = @{ blocks = @( @{ type = 'header' text = @{ type = 'plain_text'; text = "$emoji Runbook Approval Required"; emoji = $true } } @{ type = 'section' fields = @( @{ type = 'mrkdwn'; text = "*Runbook:*`n$RunbookName" } @{ type = 'mrkdwn'; text = "*Step:*`n$StepDescription" } @{ type = 'mrkdwn'; text = "*Target:*`n$ComputerName" } @{ type = 'mrkdwn'; text = "*Blast Radius:*`n$blastLevel" } ) } @{ type = 'section' text = @{ type = 'mrkdwn'; text = "*Details:*`n$blastReasons" } } @{ type = 'context' elements = @( @{ type = 'mrkdwn'; text = "Request ID: ``$requestId`` | $($timestamp.ToString('yyyy-MM-dd HH:mm:ss'))" } ) } ) } | ConvertTo-Json -Depth 10 try { Invoke-RestMethod -Uri $Contact -Method Post -Body $slackBody -ContentType 'application/json' -ErrorAction Stop $approvalResult.Notes = "Approval message posted to Slack webhook." Write-Host "Approval request posted to Slack. Awaiting response..." -ForegroundColor Yellow } catch { Write-Warning "Failed to post Slack approval request: $_" $approvalResult.Notes = "Failed to post to Slack: $_" } } } return $approvalResult } |