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
}