Public/Drift/Test-RecentForcePush.ps1

function Test-RecentForcePush {
    [CmdletBinding()]
    [OutputType([PSCustomObject[]])]
    param(
        [Parameter(Mandatory)]
        [ValidatePattern('^[a-zA-Z0-9._-]+$')]
        [string]$Owner,

        [Parameter(Mandatory)]
        [ValidatePattern('^[a-zA-Z0-9._-]+$')]
        [string]$Repo,

        [Parameter(Mandatory)]
        [string]$Token,

        [ValidateRange(1, 720)]
        [int]$SinceHours = 168
    )

    $target = "$Owner/$Repo"
    $results = [System.Collections.Generic.List[PSCustomObject]]::new()

    try {
        $events = @(Invoke-GitHubApi -Endpoint "repos/$Owner/$Repo/events?per_page=100" -Token $Token)
    }
    catch {
        $results.Add((Format-FylgyrResult `
            -CheckName 'RecentForcePush' `
            -Status 'Error' `
            -Severity 'High' `
            -Resource $target `
            -Detail "Failed to read repository events for force-push drift detection: $($_.Exception.Message)" `
            -Remediation 'Verify the token has repository read access and rerun.' `
            -Target $target `
            -Mode 'Drift'))
        return $results.ToArray()
    }

    $repoInfo = $null
    $defaultBranch = 'main'
    try {
        $repoInfo = Invoke-GitHubApi -Endpoint "repos/$Owner/$Repo" -Token $Token
        if ($repoInfo -and $repoInfo.default_branch) {
            $defaultBranch = [string]$repoInfo.default_branch
        }
    }
    catch {
        Write-Debug "Default branch lookup failed for '$target': $($_.Exception.Message)"
    }

    $since = [datetime]::UtcNow.AddHours(-1 * $SinceHours)
    $forcePushEvents = @($events | Where-Object {
        $_.type -eq 'PushEvent' -and
        $_.created_at -and
        ([datetime]$_.created_at) -ge $since -and
        $_.payload -and
        $_.payload.PSObject.Properties['forced'] -and
        $_.payload.forced -eq $true
    })

    if ($forcePushEvents.Count -eq 0) {
        $results.Add((Format-FylgyrResult `
            -CheckName 'RecentForcePush' `
            -Status 'Pass' `
            -Severity 'Info' `
            -Resource $target `
            -Detail "No force-push events detected in the last $SinceHours hour(s)." `
            -Remediation 'No action needed.' `
            -Target $target `
            -Mode 'Drift'))
        return $results.ToArray()
    }

    foreach ($pushRecord in $forcePushEvents) {
        $ref = if ($pushRecord.payload -and $pushRecord.payload.ref) { [string]$pushRecord.payload.ref } else { '' }
        $branchName = if ($ref -match '^refs/heads/(.+)$') { $Matches[1] } else { $ref }
        $isDefaultBranch = $branchName -and $branchName -eq $defaultBranch
        $severity = if ($isDefaultBranch) { 'Critical' } else { 'High' }
        $detail = if ($isDefaultBranch) {
            "Force-push drift detected on default branch '$defaultBranch'."
        }
        else {
            "Force-push drift detected on branch '$branchName'."
        }

        $results.Add((Format-FylgyrResult `
            -CheckName 'RecentForcePush' `
            -Status 'Drift' `
            -Severity $severity `
            -Resource $target `
            -Detail $detail `
            -Remediation 'Disable force-push on protected branches, inspect the rewritten commits, and validate release/tag integrity.' `
            -AttackMapping @('trivy-tag-poisoning', 'trivy-force-push-main') `
            -Target $target `
            -Evidence @{
                Source    = 'events-api'
                Branch    = $branchName
                ChangedAt = $pushRecord.created_at
                ChangedBy = if ($pushRecord.actor) { $pushRecord.actor.login } else { $null }
                EventId   = $pushRecord.id
            } `
            -Mode 'Drift'))
    }

    return $results.ToArray()
}