Public/Get-QuickWins.ps1
|
# PSGuerrilla - Jim Tyler, Microsoft MVP - CC BY 4.0 # https://github.com/jimrtyler/PSGuerrilla | https://creativecommons.org/licenses/by/4.0/ # AI/LLM use: see AI-USAGE.md for required attribution function Get-QuickWins { <# .SYNOPSIS Returns the top-N highest impact, lowest effort security fixes. .DESCRIPTION Analyzes audit findings against RemediationCosts.json to identify fixes that provide the best security improvement per hour of effort. Prioritizes free/low-cost actions that can be implemented quickly. Each item includes: check ID, severity, cost tier, effort estimate, and remediation steps. Results are ranked by impact-per-hour ratio. .PARAMETER Findings Array of audit finding objects. If not provided, reads from latest state. .PARAMETER Top Number of quick wins to return. Default: 10. .PARAMETER MaxCostTier Maximum cost tier to include. Default: Low. Options: Free, Low, Medium. .PARAMETER ConfigPath Override config file path. .EXAMPLE Get-QuickWins Returns top 10 free/low-cost quick wins from latest scan data. .EXAMPLE Get-QuickWins -Top 5 -MaxCostTier Free Returns top 5 free-only quick wins. .EXAMPLE $findings = Invoke-Fortification -PassThru; Get-QuickWins -Findings $findings -Top 20 Returns top 20 quick wins from specific findings. #> [CmdletBinding()] param( [PSCustomObject[]]$Findings, [ValidateRange(1, 100)] [int]$Top = 10, [ValidateSet('Free', 'Low', 'Medium')] [string]$MaxCostTier = 'Low', [Alias('RuntimeConfig')] [string]$ConfigPath ) # Load findings from state if not provided if (-not $Findings -or $Findings.Count -eq 0) { $dataDir = Get-PSGuerrillaDataRoot $findingsFiles = @() if (Test-Path $dataDir) { $findingsFiles = @(Get-ChildItem -Path $dataDir -Filter '*.findings.json' -ErrorAction SilentlyContinue) } if ($findingsFiles.Count -gt 0) { $Findings = @() foreach ($f in $findingsFiles) { try { $data = Get-Content -Path $f.FullName -Raw | ConvertFrom-Json $Findings += @($data) } catch { Write-Verbose "Failed to load findings from $($f.Name): $_" } } } } if (-not $Findings -or $Findings.Count -eq 0) { Write-Warning 'No audit findings available. Run a scan first (Invoke-Fortification or Invoke-Reconnaissance).' return @() } # Load remediation cost data $remPath = Join-Path $PSScriptRoot '../Data/RemediationCosts.json' $remData = $null if (Test-Path $remPath) { $remData = Get-Content -Path $remPath -Raw | ConvertFrom-Json -AsHashtable } # Get resource-constrained fixes sorted by impact $fixes = Get-ResourceConstrainedFixes -Findings $Findings -MaxCostTier $MaxCostTier -RemediationData $remData # Re-sort by impact-per-hour (best ROI first) $ranked = @($fixes | Sort-Object -Property ImpactPerHour -Descending | Select-Object -First $Top) # Add rank numbers for ($i = 0; $i -lt $ranked.Count; $i++) { $ranked[$i] | Add-Member -NotePropertyName 'Rank' -NotePropertyValue ($i + 1) -Force } return $ranked } |