modules/Devolutions.CIEM.Notifications/Public/Send-CIEMNotification.ps1

function Send-CIEMNotification {
    [CmdletBinding()]
    [OutputType([PSCustomObject])]
    param(
        [Parameter()]
        [int]$CurrentDiscoveryRunId,

        [Parameter(Mandatory)]
        [ValidateSet('Manual', 'ScheduledDiscovery')]
        [string]$InvocationSource,

        [Parameter()]
        [switch]$Test
    )

    $ErrorActionPreference = 'Stop'

    if (-not $Test -and -not $PSBoundParameters.ContainsKey('CurrentDiscoveryRunId')) {
        throw 'CurrentDiscoveryRunId is required unless -Test is specified.'
    }

    $enabledNotifications = @(Get-CIEMNotification | Where-Object { $_.Enabled -and $_.Type -eq 'ExposureChange' })
    if ($enabledNotifications.Count -eq 0) {
        return [PSCustomObject]@{
            SentCount   = 0
            FailedCount = 0
            SkippedCount = 0
        }
    }
    if ($enabledNotifications.Count -ne 1) {
        throw 'CIEM notifications V1 supports exactly one enabled ExposureChange notification.'
    }
    $notification = $enabledNotifications[0]

    $scopeAllowsSend = switch ($notification.AutoSendScope) {
        'AnyDiscovery' { $true }
        'ScheduledDiscovery' { $InvocationSource -eq 'ScheduledDiscovery' }
        'ManualOnly' { $InvocationSource -eq 'Manual' }
    }
    if (-not $scopeAllowsSend) {
        return [PSCustomObject]@{
            SentCount   = 0
            FailedCount = 0
            SkippedCount = 0
        }
    }

    $enabledChannels = @(Get-CIEMNotificationChannel | Where-Object { $_.Enabled -and $_.Type -eq 'Email' })
    if ($enabledChannels.Count -eq 0) {
        return [PSCustomObject]@{
            SentCount   = 0
            FailedCount = 0
            SkippedCount = 0
        }
    }
    if ($enabledChannels.Count -ne 1) {
        throw 'CIEM notifications V1 supports exactly one enabled Email notification channel.'
    }
    $channel = $enabledChannels[0]

    $profile = GetCIEMAssignedAuthenticationProfile -UsageType 'NotificationChannel' -UsageId 'email-default'

    $changes = if ($Test) {
        @([PSCustomObject]@{
            Id                    = 'test-notification'
            CurrentDiscoveryRunId = 0
            ChangeType            = 'NewRisk'
            Severity              = 'Critical'
            SeverityRank          = 1
            Title                 = 'Test exposure change notification'
            Evidence              = 'This is a test notification from the CIEM configuration page.'
            ImpactedIdentityName  = 'Test Identity'
            ImpactedResourceName  = 'Test Resource'
        })
    }
    else {
        @(Get-CIEMExposureChange -CurrentDiscoveryRunId $CurrentDiscoveryRunId)
    }

    $minimumSeverityRank = GetCIEMNotificationSeverityRank -Severity $notification.MinimumSeverity
    $matchingChanges = @($changes | Where-Object {
        $notification.ChangeTypes -contains $_.ChangeType -and [int]$_.SeverityRank -le $minimumSeverityRank
    })

    $sentCount = 0
    $failedCount = 0
    $recipientSummary = "To: $($channel.ToRecipients -join ', ')"
    if ($channel.CcRecipients.Count -gt 0) {
        $recipientSummary += "; Cc: $($channel.CcRecipients -join ', ')"
    }
    if ($channel.BccRecipients.Count -gt 0) {
        $recipientSummary += "; Bcc: $($channel.BccRecipients -join ', ')"
    }

    foreach ($change in $matchingChanges) {
        $templateValues = @{
            Severity              = $change.Severity
            Title                 = $change.Title
            Evidence              = $change.Evidence
            ChangeType            = $change.ChangeType
            Identity              = $change.ImpactedIdentityName
            Target                = $change.ImpactedResourceName
            CurrentDiscoveryRunId = $change.CurrentDiscoveryRunId
            SourceSignalId        = $change.Id
        }

        $subject = FormatCIEMNotificationTemplate -Template $notification.SubjectTemplate -Values $templateValues
        $textBody = FormatCIEMNotificationTemplate -Template $notification.TextBodyTemplate -Values $templateValues
        $htmlBody = FormatCIEMNotificationTemplate -Template $notification.HtmlBodyTemplate -Values $templateValues
        $attemptedAt = (Get-Date).ToString('o')

        try {
            $sendResult = SendCIEMEmailMessage -AuthenticationProfile $profile -Channel $channel -Subject $subject -TextBody $textBody -HtmlBody $htmlBody
            SaveCIEMNotificationHistory `
                -NotificationId $notification.Id `
                -ChannelId $channel.Id `
                -SourceSignalId ([string]$change.Id) `
                -SourceSignalType 'ExposureChange' `
                -Status 'Succeeded' `
                -AttemptedAt $attemptedAt `
                -MessageId $sendResult.MessageId `
                -RecipientSummary $recipientSummary
            $sentCount++
        }
        catch {
            $failedCount++
            SaveCIEMNotificationHistory `
                -NotificationId $notification.Id `
                -ChannelId $channel.Id `
                -SourceSignalId ([string]$change.Id) `
                -SourceSignalType 'ExposureChange' `
                -Status 'Failed' `
                -AttemptedAt $attemptedAt `
                -RecipientSummary $recipientSummary `
                -ErrorMessage $_.Exception.Message
        }
    }

    if ($failedCount -gt 0) {
        throw "notification send failed: $failedCount failure(s)"
    }

    [PSCustomObject]@{
        SentCount    = $sentCount
        FailedCount  = $failedCount
        SkippedCount = $changes.Count - $matchingChanges.Count
    }
}