Private/Write-RotationRecord.ps1

function Write-RotationRecord {
    <#
    .SYNOPSIS
        Writes one structured rotation record to a Log Analytics custom table.

    .DESCRIPTION
        Uses the Logs Ingestion API (data collection endpoint plus data collection
        rule), not the HTTP Data Collector API. The latter retires on 14 September
        2026 and would be dead on arrival.

        The record never contains credential material. Secret versions are recorded
        as identifiers so an auditor can correlate a rotation with the Key Vault
        audit log without either system holding a value.

        Failure to write a record is logged but does not fail the rotation. The
        credential change already happened; losing the telemetry is the lesser
        problem, and the job output still carries the same information.
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(Mandatory)][pscustomobject]$Record,

        [Parameter(Mandatory)][string]$DataCollectionEndpoint,
        [Parameter(Mandatory)][string]$DataCollectionRuleId,
        [string]$StreamName = 'Custom-CredentialRotation_CL'
    )

    if (-not $PSCmdlet.ShouldProcess($StreamName, 'Write rotation record')) { return }

    try {
        $token = (Get-AzAccessToken -ResourceUrl 'https://monitor.azure.com' -ErrorAction Stop).Token
        $uri = '{0}/dataCollectionRules/{1}/streams/{2}?api-version=2023-01-01' -f
            $DataCollectionEndpoint.TrimEnd('/'), $DataCollectionRuleId, $StreamName

        $body = ConvertTo-Json -InputObject @($Record) -Depth 5 -Compress

        $null = Invoke-RestMethod -Uri $uri -Method Post -Body $body `
            -ContentType 'application/json' `
            -Headers @{ Authorization = "Bearer $token" } `
            -ErrorAction Stop

        Write-RotationLog -Message "Rotation record written for $($Record.SecretName)" -Level Info -Scope 'audit'
    }
    catch {
        Write-RotationLog -Message "Could not write rotation record for $($Record.SecretName): $($_.Exception.Message)" -Level Warning -Scope 'audit'
    }
}

function New-RotationRecord {
    <#
    .SYNOPSIS
        Builds the record shape expected by the CredentialRotation_CL custom table.

    .DESCRIPTION
        TimeGenerated is set explicitly so the record carries the time the rotation
        completed rather than the time the batch happened to flush.
    #>

    [CmdletBinding()]
    [OutputType([pscustomobject])]
    param(
        [Parameter(Mandatory)][string]$SecretName,
        [Parameter(Mandatory)][string]$VMName,
        [Parameter(Mandatory)][string]$ResourceGroupName,
        [Parameter(Mandatory)][string]$SubscriptionId,
        [Parameter(Mandatory)][ValidateSet('Windows', 'Linux')][string]$OSType,
        [Parameter(Mandatory)][ValidateSet('Password', 'SSHKey')][string]$CredentialType,
        [Parameter(Mandatory)][ValidateSet('Expiry', 'Access', 'Missing', 'Manual', 'ResumePending')][string]$TriggerReason,
        [Parameter(Mandatory)][ValidateSet('Rotated', 'Skipped', 'Failed', 'WhatIf')][string]$Result,

        [datetime]$StartedAt = (Get-Date).ToUniversalTime(),
        [string]$TriggeredBy,
        [string]$PreviousSecretVersion,
        [string]$NewSecretVersion,
        [string]$Detail
    )

    $now = (Get-Date).ToUniversalTime()

    return [pscustomobject]@{
        TimeGenerated         = $now.ToString('o')
        SecretName            = $SecretName
        VMName                = $VMName
        ResourceGroupName     = $ResourceGroupName
        SubscriptionId        = $SubscriptionId
        OSType                = $OSType
        CredentialType        = $CredentialType
        TriggerReason         = $TriggerReason
        TriggeredBy           = $TriggeredBy
        Result                = $Result
        StartedAt             = $StartedAt.ToString('o')
        DurationMs            = [int]($now - $StartedAt).TotalMilliseconds
        PreviousSecretVersion = $PreviousSecretVersion
        NewSecretVersion      = $NewSecretVersion
        Detail                = $Detail
    }
}