Public/Drift/Test-RecentWorkflowAdd.ps1
|
function Test-RecentWorkflowAdd { [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, [string]$BaselinePath ) $target = "$Owner/$Repo" $results = [System.Collections.Generic.List[PSCustomObject]]::new() try { $workflowFiles = @(Get-WorkflowFile -Owner $Owner -Repo $Repo -Token $Token) } catch { $results.Add((Format-FylgyrResult ` -CheckName 'RecentWorkflowAdd' ` -Status 'Error' ` -Severity 'Medium' ` -Resource $target ` -Detail "Failed to collect workflow file inventory: $($_.Exception.Message)" ` -Remediation 'Verify repository access and rerun.' ` -Target $target ` -Mode 'Drift')) return $results.ToArray() } $currentPaths = @($workflowFiles | ForEach-Object { [string]$_.Path } | Sort-Object -Unique) $currentSnapshot = [PSCustomObject]@{ Paths = $currentPaths StateSnapshot = $currentPaths } if (-not $BaselinePath) { $results.Add((Format-FylgyrResult ` -CheckName 'RecentWorkflowAdd' ` -Status 'Info' ` -Severity 'Info' ` -Resource $target ` -Detail 'Baseline not provided. Captured current workflow inventory for next drift run.' ` -Remediation 'Provide -BaselinePath from a previous scan to detect newly added workflow files.' ` -Target $target ` -Evidence @{ Source = 'baseline-diff' To = $currentSnapshot Fidelity = 'No previous snapshot available; this run establishes baseline state.' StateSnapshot = $currentPaths } ` -Mode 'Drift')) return $results.ToArray() } try { $comparison = Compare-FylgyrBaseline -BaselinePath $BaselinePath -CheckName 'RecentWorkflowAdd' -Resource $target -CurrentSnapshot $currentSnapshot } catch { $results.Add((Format-FylgyrResult ` -CheckName 'RecentWorkflowAdd' ` -Status 'Error' ` -Severity 'Medium' ` -Resource $target ` -Detail "Failed to compare workflow inventory with baseline: $($_.Exception.Message)" ` -Remediation 'Provide a valid JSON baseline generated by a previous Invoke-Fylgyr scan.' ` -Target $target ` -Mode 'Drift')) return $results.ToArray() } $previousPaths = @() if ($comparison.HasBaseline -and $comparison.BaselineSnapshot) { if ($comparison.BaselineSnapshot.PSObject.Properties['Paths']) { $previousPaths = @($comparison.BaselineSnapshot.Paths) } elseif ($comparison.BaselineSnapshot -is [System.Array]) { $previousPaths = @($comparison.BaselineSnapshot) } } $previousSet = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) foreach ($path in $previousPaths) { if (-not [string]::IsNullOrWhiteSpace($path)) { $previousSet.Add([string]$path) | Out-Null } } $newPaths = [System.Collections.Generic.List[string]]::new() foreach ($path in $currentPaths) { if (-not $previousSet.Contains($path)) { $newPaths.Add($path) } } if ($newPaths.Count -eq 0) { $results.Add((Format-FylgyrResult ` -CheckName 'RecentWorkflowAdd' ` -Status 'Pass' ` -Severity 'Info' ` -Resource $target ` -Detail 'No new workflow files detected compared to baseline.' ` -Remediation 'No action needed.' ` -Target $target ` -Evidence @{ Source = 'baseline-diff' From = $comparison.BaselineSnapshot To = $currentSnapshot StateSnapshot = $currentPaths } ` -Mode 'Drift')) return $results.ToArray() } foreach ($newPath in $newPaths) { $file = $workflowFiles | Where-Object { $_.Path -eq $newPath } | Select-Object -First 1 $content = if ($file) { [string]$file.Content } else { '' } $isHighRisk = $content -match '(?m)pull_request_target' -or $content -match '(?im)runs-on:\s*\[?\s*self-hosted' $severity = if ($isHighRisk) { 'High' } else { 'Medium' } $detailSuffix = if ($isHighRisk) { ' The workflow includes high-risk execution primitives (self-hosted or pull_request_target).' } else { '' } $results.Add((Format-FylgyrResult ` -CheckName 'RecentWorkflowAdd' ` -Status 'Drift' ` -Severity $severity ` -Resource $target ` -Detail "New workflow file detected: '$newPath'.$detailSuffix" ` -Remediation 'Review workflow provenance, pin all actions to SHAs, and validate trigger and runner trust boundaries before merge.' ` -AttackMapping @('tj-actions-shai-hulud', 'shai-hulud-runner-backdoor') ` -Target $target ` -Evidence @{ Source = 'baseline-diff' From = $comparison.BaselineSnapshot To = $currentSnapshot NewPath = $newPath Fidelity = 'Baseline diff has no actor attribution; rely on audit logs for identity context.' StateSnapshot = $currentPaths } ` -Mode 'Drift')) } return $results.ToArray() } |