Public/Invoke-FylgyrDriftScan.ps1

function Invoke-FylgyrDriftScan {
    [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,

        [string]$BaselinePath,

        [switch]$IgnoreConfig
    )

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

    $configContext = Get-FylgyrConfigSuppression -IgnoreConfig:$IgnoreConfig
    $configSuppressions = @($configContext.Rules)
    $configDiagnostics = @($configContext.Diagnostics)

    foreach ($configDiagnostic in $configDiagnostics) {
        $results.Add((Format-FylgyrResult `
            -CheckName 'ConfigSuppression' `
            -Status $configDiagnostic.Status `
            -Severity $configDiagnostic.Severity `
            -Resource $target `
            -Detail $configDiagnostic.Detail `
            -Remediation $configDiagnostic.Remediation `
            -Target $target `
            -Mode 'Drift'))
    }

    $auditEvents = @()
    try {
        $auditEvents = @(Get-OrgAuditLog -Owner $Owner -Token $Token -SinceHours $SinceHours)
    }
    catch {
        Write-Debug "Audit log cache warmup failed for '$target': $($_.Exception.Message)"
    }

    $driftChecks = @(
        @{ Name = 'Test-RecentCollaboratorChange'; Params = @{ Owner = $Owner; Repo = $Repo; Token = $Token; SinceHours = $SinceHours; BaselinePath = $BaselinePath } }
        @{ Name = 'Test-RecentProtectionChange'; Params = @{ Owner = $Owner; Repo = $Repo; Token = $Token; SinceHours = $SinceHours; BaselinePath = $BaselinePath; AuditEvents = $auditEvents } }
        @{ Name = 'Test-RecentForcePush'; Params = @{ Owner = $Owner; Repo = $Repo; Token = $Token; SinceHours = $SinceHours } }
        @{ Name = 'Test-RecentRunnerRegistration'; Params = @{ Owner = $Owner; Repo = $Repo; Token = $Token; SinceHours = $SinceHours; BaselinePath = $BaselinePath; AuditEvents = $auditEvents } }
        @{ Name = 'Test-RecentSecretChange'; Params = @{ Owner = $Owner; Repo = $Repo; Token = $Token; SinceHours = $SinceHours; AuditEvents = $auditEvents } }
        @{ Name = 'Test-RecentWorkflowAdd'; Params = @{ Owner = $Owner; Repo = $Repo; Token = $Token; BaselinePath = $BaselinePath } }
    )

    foreach ($entry in $driftChecks) {
        Write-Progress -Activity $target -Status "Running $($entry.Name)" -Id 4 -ParentId 1

        try {
            $checkResults = & $entry.Name @($entry.Params)
            foreach ($checkResult in $checkResults) {
                $checkResult.Target = $target
                if (-not $checkResult.PSObject.Properties['Mode']) {
                    $checkResult | Add-Member -NotePropertyName Mode -NotePropertyValue 'Drift'
                }
                $results.Add($checkResult)
            }
        }
        catch {
            $results.Add((Format-FylgyrResult `
                -CheckName ($entry.Name -replace '^Test-', '') `
                -Status 'Error' `
                -Severity 'High' `
                -Resource $target `
                -Detail "Drift check failed with error: $($_.Exception.Message)" `
                -Remediation 'Review the check failure and rerun drift mode.' `
                -Target $target `
                -Mode 'Drift'))
        }
    }

    Write-Progress -Activity $target -Id 4 -Completed
    return (Resolve-FylgyrSuppressionStatus -Results $results.ToArray() -Suppressions $configSuppressions)
}

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

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

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

        [string]$BaselinePath,

        [switch]$IgnoreConfig
    )

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

    $configContext = Get-FylgyrConfigSuppression -IgnoreConfig:$IgnoreConfig
    $configSuppressions = @($configContext.Rules)
    $configDiagnostics = @($configContext.Diagnostics)

    foreach ($configDiagnostic in $configDiagnostics) {
        $results.Add((Format-FylgyrResult `
            -CheckName 'ConfigSuppression' `
            -Status $configDiagnostic.Status `
            -Severity $configDiagnostic.Severity `
            -Resource $target `
            -Detail $configDiagnostic.Detail `
            -Remediation $configDiagnostic.Remediation `
            -Target $target `
            -Mode 'Drift'))
    }

    $auditEvents = @()
    try {
        $auditEvents = @(Get-OrgAuditLog -Owner $Owner -Token $Token -SinceHours $SinceHours)
    }
    catch {
        Write-Debug "Org audit log unavailable for '$target': $($_.Exception.Message)"
    }

    $orgDriftChecks = @(
        @{ Name = 'Test-RecentAppAuthorization'; Params = @{ Owner = $Owner; Token = $Token; SinceHours = $SinceHours; BaselinePath = $BaselinePath; AuditEvents = $auditEvents } }
        @{ Name = 'Test-RecentSecretChange'; Params = @{ Owner = $Owner; Token = $Token; SinceHours = $SinceHours; AuditEvents = $auditEvents } }
        @{ Name = 'Test-RecentTokenExposure'; Params = @{ Owner = $Owner; Token = $Token; SinceHours = $SinceHours; BaselinePath = $BaselinePath; AuditEvents = $auditEvents } }
    )

    foreach ($entry in $orgDriftChecks) {
        Write-Progress -Activity $target -Status "Running $($entry.Name)" -Id 5 -ParentId 1

        try {
            $checkResults = & $entry.Name @($entry.Params)
            foreach ($checkResult in $checkResults) {
                $checkResult.Target = $target
                if (-not $checkResult.PSObject.Properties['Mode']) {
                    $checkResult | Add-Member -NotePropertyName Mode -NotePropertyValue 'Drift'
                }
                $results.Add($checkResult)
            }
        }
        catch {
            $results.Add((Format-FylgyrResult `
                -CheckName ($entry.Name -replace '^Test-', '') `
                -Status 'Error' `
                -Severity 'High' `
                -Resource $target `
                -Detail "Org drift check failed with error: $($_.Exception.Message)" `
                -Remediation 'Review org drift check failure and rerun.' `
                -Target $target `
                -Mode 'Drift'))
        }
    }

    Write-Progress -Activity $target -Id 5 -Completed
    return (Resolve-FylgyrSuppressionStatus -Results $results.ToArray() -Suppressions $configSuppressions)
}