From cea4affc3c2f30c26c6896aa816810042d79d3a8 Mon Sep 17 00:00:00 2001 From: felickz <1760475+felickz@users.noreply.github.com> Date: Fri, 5 Jul 2024 15:38:11 -0400 Subject: [PATCH 1/4] Clarify in summary this is only alerts that have a CVE --- action.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/action.ps1 b/action.ps1 index 53869fb..3c7bf98 100644 --- a/action.ps1 +++ b/action.ps1 @@ -143,7 +143,7 @@ $epssMatch = $Dependabot_Alerts_CVEs | ForEach-Object { $epssHash[$_] } | Where- $isFail = $epssMatch.Count -gt 0 #Summary -$summary = "[$OrganizationName/$RepositoryName] - $($Dependabot_Alerts_CVEs.Count) Dependabot Alerts total.`n" +$summary = "[$OrganizationName/$RepositoryName] - $($Dependabot_Alerts_CVEs.Count) Dependabot Alerts total that reference a CVE.`n" $summary += $isFail ? "$($epssMatch.Count) CVEs found in Dependabot alerts that exceed the EPSS '$EPSS_Threshold' threshold :`n $( $epssMatch | ForEach-Object { "$($_.cve) - $($_.epss) EPSS ($($_.percentile) percentile) `n" })" : "No CVEs found in Dependabot alerts that exceed the EPSS '$EPSS_Threshold' threshold." if ($isFail) { From a517efab9de28799e4acec2e2c69c0d7f4aa7119 Mon Sep 17 00:00:00 2001 From: felickz <1760475+felickz@users.noreply.github.com> Date: Fri, 5 Jul 2024 19:17:25 -0400 Subject: [PATCH 2/4] Output markdown report --- action.ps1 | 176 ++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 140 insertions(+), 36 deletions(-) diff --git a/action.ps1 b/action.ps1 index 3c7bf98..7ba1c75 100644 --- a/action.ps1 +++ b/action.ps1 @@ -5,14 +5,16 @@ Action to detect if any open Dependabot alerts exceed a specified EPSS (Ecosyste Requirements: - GITHUB_TOKEN env variable with repo scope or security_events scope. For public repositories, you may instead use the public_repo scope. .EXAMPLE -# PS>gh auth token # <-- Easy to grab a local auth token to test with from here! -# PS>Write-Host "initializing local run! Ensure you provide a valid GITHUB_TOKEN otherwise you will get a 401!!! " -# $VerbosePreference = 'SilentlyContinue' -# $env:GITHUB_TOKEN = gh auth token -# $env:GITHUB_REPOSITORY = 'vulna-felickz/python-dependabot-no-cve' -# CLEAR GLOBAL VARIABLES! -# Remove-Variable * -ErrorAction SilentlyContinue; -# PS> action.ps1 +PS>gh auth token # <-- Easy to grab a local auth token to test with from here! +PS>Write-Host "initializing local run! Ensure you provide a valid GITHUB_TOKEN otherwise you will get a 401!!! " +$VerbosePreference = 'SilentlyContinue' +$env:GITHUB_TOKEN = gh auth token +$env:GITHUB_REPOSITORY = 'vulna-felickz/python-dependabot-no-cve' +$env:GITHUB_REPOSITORY = 'vulna-felickz/log4shell-vulnerable-app' +$env:GITHUB_STEP_SUMMARY = $(New-Item -Name /_temp/_runner_file_commands/step_summary_a01d8a3b-1412-4059-9cf1-f7c4b54cff76 -ItemType File -Force).FullName +CLEAR GLOBAL VARIABLES! +Remove-Variable * -ErrorAction SilentlyContinue; +PS> action.ps1 .PARAMETER GitHubToken The GitHub PAT that is used to authenticate to GitHub GH CLI (uses the envioronment value GH_TOKEN). @@ -37,6 +39,41 @@ param( [string]$EPSS_Threshold = "0.6" ) +function Convert-ToOrdinalPercentile { + param ( + [decimal]$decimal + ) + + $percentile = [math]::Floor($decimal * 100) + $suffix = 'th' + + switch ($percentile % 100) { + { $_ -in 11..13 } { $suffix = 'th' } + 1 { $suffix = 'st' } + 2 { $suffix = 'nd' } + 3 { $suffix = 'rd' } + } + + return "$percentile$suffix" +} + +#⚪🟡🟠🔴 +#low, medium, high, critical +function Convert-SeverityToEmoji { + param ( + [string]$severity + ) + + switch ($severity) { + "low" { return "⚪" } + "medium" { return "🟡" } + "high" { return "🟠" } + "critical" { return "🔴" } + default { return "⁉️" } + } + +} + function Decompress-GZip($infile, $outfile) { $inStream = New-Object System.IO.FileStream $inFile, ([IO.FileMode]::Open), ([IO.FileAccess]::Read), ([IO.FileShare]::Read) $gzipStream = New-Object System.IO.Compression.GzipStream $inStream, ([IO.Compression.CompressionMode]::Decompress) @@ -104,47 +141,114 @@ $RepositoryName = $actionRepo.Repo #https://docs.github.com/en/rest/dependabot/alerts?apiVersion=2022-11-28#list-dependabot-alerts-for-a-repository $perPage = 100 $Dependabot_Alerts = Invoke-GHRestMethod -Method GET -Uri "https://api.github.com/repos/$OrganizationName/$RepositoryName/dependabot/alerts?state=open&per_page=$perPage" -ExtendedResult $true -$Dependabot_Alerts_CVEs = $Dependabot_Alerts.result | Where-Object { $_.security_advisory.cve_id -ne $null } | ForEach-Object { $_.security_advisory.cve_id } +$Dependabot_Alerts_CVEs = $Dependabot_Alerts.result #Get next page of dependabot alerts if there is one while ($null -ne $Dependabot_Alerts.nextLink) { $Dependabot_Alerts = Invoke-GHRestMethod -Method GET -Uri $Dependabot_Alerts.nextLink -ExtendedResult $true - $Dependabot_Alerts_CVEs += $Dependabot_Alerts.result | Where-Object { $_.security_advisory.cve_id -ne $null } | ForEach-Object { $_.security_advisory.cve_id } + $Dependabot_Alerts_CVEs += $Dependabot_Alerts.result } -Write-ActionInfo "$OrganizationName/$RepositoryName Dependabot CVEs Count: $($Dependabot_Alerts_CVEs.Count)" -Write-ActionDebug "$OrganizationName/$RepositoryName Dependabot CVEs: $Dependabot_Alerts_CVEs" - -# Check if $Dependabot_Alerts_CVEs is null or empty and exit successfully -if ($null -eq $Dependabot_Alerts_CVEs -or $Dependabot_Alerts_CVEs.Count -eq 0) { - Write-ActionInfo "No Dependabot CVEs found. Exiting script successfully." - exit 0 -} +$DependabotAlertCount = $Dependabot_Alerts_CVEs.Count +$DependabotAlertNullCveCount = $($Dependabot_Alerts_CVEs | Where-Object { $_.security_advisory.cve_id -eq $null } ).Count +Write-ActionInfo "$OrganizationName/$RepositoryName Dependabot Alert Count: $DependabotAlertCount ($DependabotAlertNullCveCount with no CVE)" +Write-ActionDebug "$OrganizationName/$RepositoryName Dependabot CVEs: $($Dependabot_Alerts_CVEs|ForEach-Object { $_.security_advisory.cve_id })" -#Grab the EPSS data(https://www.first.org/epss/data_stats) from csv https://epss.cyentia.com/epss_scores-2024-03-02.csv" -#TODO - Use First API ? https://www.first.org/epss/api -$date = (Get-Date).ToUniversalTime().ToString("yyyy-MM-dd") -$csv = "epss_scores-$date.csv" -try { - Invoke-WebRequest -Uri "https://epss.cyentia.com/$csv.gz" -OutFile "$csv.gz" +# If no Dependabot alerts with CVEs found, no need to check EPSS +if ($null -eq $Dependabot_Alerts_CVEs -or $Dependabot_Alerts_CVEs.Count -eq 0 -or $DependabotAlertNullCveCount -eq $DependabotAlertCount) { + Write-ActionInfo "No Dependabot Alerts with CVEs found." + $epssMatch = @() } -catch { - # Incase the date math is delayed and the file is not available yet (TODO cache the last known good file and use that if the current date is not available yet) - $date = (Get-Date).ToUniversalTime().AddDays(-1).ToString("yyyy-MM-dd") +else { + #Grab the EPSS data(https://www.first.org/epss/data_stats) from csv https://epss.cyentia.com/epss_scores-2024-03-02.csv.gz" + #TODO - Use First API ? https://www.first.org/epss/api + $date = (Get-Date).ToUniversalTime().ToString("yyyy-MM-dd") $csv = "epss_scores-$date.csv" - Invoke-WebRequest -Uri "https://epss.cyentia.com/$csv.gz" -OutFile "$csv.gz" + try { + Invoke-WebRequest -Uri "https://epss.cyentia.com/$csv.gz" -OutFile "$csv.gz" + } + catch { + # Incase the date math is delayed and the file is not available yet (TODO cache the last known good file and use that if the current date is not available yet) + $date = (Get-Date).ToUniversalTime().AddDays(-1).ToString("yyyy-MM-dd") + $csv = "epss_scores-$date.csv" + Invoke-WebRequest -Uri "https://epss.cyentia.com/$csv.gz" -OutFile "$csv.gz" + } + Decompress-GZip "$csv.gz" $csv + $epss = Import-Csv -Path $csv + $epssHash = @{} + $epss | ForEach-Object { $epssHash[$_.cve] = $_ } + + + $Dependabot_Alerts_CVEs | ForEach-Object { + $epssInfo = $epssHash[$_.security_advisory.cve_id] + $scoring = New-Object PSObject -Property @{ + cve = $epssInfo.cve + epss = $epssInfo.epss + percentile = $epssInfo.percentile + exceedsThreshold = ($epssInfo -and [decimal]$epssInfo.epss -ge [decimal]$EPSS_Threshold) ? $true : $false + } + $_ | Add-Member -MemberType NoteProperty -Name "scoring" -Value $scoring + } + + + #Check if any Dependabot alerts have an EPSS score equal/above the threshold + #$epssMatch = $Dependabot_Alerts_CVEs | ForEach-Object { $epssHash[$_] } | Where-Object { [decimal]$_.epss -ge [decimal]$EPSS_Threshold } } -Decompress-GZip "$csv.gz" $csv -$epss = Import-Csv -Path $csv -$epssHash = @{} -$epss | ForEach-Object { $epssHash[$_.cve] = $_ } -#Check if any Dependabot alerts have an EPSS score equal/above the threshold -$epssMatch = $Dependabot_Alerts_CVEs | ForEach-Object { $epssHash[$_] } | Where-Object { [decimal]$_.epss -ge [decimal]$EPSS_Threshold } -$isFail = $epssMatch.Count -gt 0 +#set failure if an of the Dependabot_Alerts_CVEs have an EPSS score equal/above the threshold +$Failures = $Dependabot_Alerts_CVEs | Where-Object { $_.Scoring.exceedsThreshold } +$isFail = $Failures.Count -gt 0 #Summary $summary = "[$OrganizationName/$RepositoryName] - $($Dependabot_Alerts_CVEs.Count) Dependabot Alerts total that reference a CVE.`n" -$summary += $isFail ? "$($epssMatch.Count) CVEs found in Dependabot alerts that exceed the EPSS '$EPSS_Threshold' threshold :`n $( $epssMatch | ForEach-Object { "$($_.cve) - $($_.epss) EPSS ($($_.percentile) percentile) `n" })" : "No CVEs found in Dependabot alerts that exceed the EPSS '$EPSS_Threshold' threshold." +$summary += $isFail ? "Found $($Failures.Count) CVEs in Dependabot alerts that exceed the EPSS '$EPSS_Threshold' threshold :`n $( $Failures | ForEach-Object { "$($_.scoring.cve) - $($_.scoring.epss) EPSS ($($_.scoring.percentile) percentile) `n" })" : "No CVEs found in Dependabot alerts that exceed the EPSS '$EPSS_Threshold' threshold." + +#Actions Markdown Summary - https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#adding-a-job-summary +#flashy! - https://github.blog/2022-05-09-supercharging-github-actions-with-job-summaries/ +$markdownSummary = "# $($epssMatch.Count -gt 0 ? '🚨' : '👍') Dependabot EPSS[^1] 🤖 Report ($((Get-Date).ToString("yyyy-MM-dd"))) `n" + +if ($isFail) { + + $markdownSummary += @" +| Status 🚦 | CVE 🐛 | EPSS(Percentile) 🚨 | Dependabot 🤖 | Advisory 🔒 | CVSS 🔢 | Created 📅 | Package 📦 | Manifest 📝 | Scope 🖥️ | Fix ❓ | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | `n +"@ + + + #Loop through all $epssMatch and add to markdownSummary + $markdownSummaryTableRows = $Failures | ForEach-Object { + $cve = $_.scoring.cve + $epss = [math]::Round([decimal]$_.scoring.epss * 100).ToString() + '%' + $percentile = Convert-ToOrdinalPercentile -decimal $_.scoring.percentile + $ghsa = $_.security_advisory.ghsa_id + $created = $_.created_at.ToString("yyyy-MM-dd") + $alertNumber = $_.number + $alertUrl = $_.html_url + $cvssScore = $_.security_advisory.cvss.score + $cvssVector = $_.security_advisory.cvss.vector_string + $package = $_.dependency.package.name + $ecosystem = $_.dependency.package.ecosystem + $manifest = $_.dependency.manifest_path + $advisory = $_.security_advisory.summary + $severity = $_.security_advisory.severity + $color = Convert-SeverityToEmoji -severity $_.security_advisory.severity + $scope = $_.dependency.scope + $fixAvailable = $_.security_vulnerability.first_patched_version -and $_.security_vulnerability.first_patched_version.identifier -ne $null ? "[✅](# `"$($_.security_vulnerability.first_patched_version.identifier)`")" : "❌" + "[🔴](## `"Error`") | [$cve](https://nvd.nist.gov/vuln/detail/$cve) | $epss ($percentile) | [#$alertNumber]($alertUrl) [🤖](## `"$advisory`") | [$ghsa](https://github.com/advisories/$ghsa) | [$color](## `"$severity`")[$cvssScore](https://www.first.org/cvss/calculator/3.1#$cvssVector) | $created | $package($ecosystem) | [📝](## `"$manifest`") | $scope | $fixAvailable `n" + } + $markdownSummary += $markdownSummaryTableRows +} +else { + $markdownSummary += $summary +} + +$markdownSummary += "[^1]: The Exploit Prediction Scoring System (EPSS) is a data-driven effort for estimating the likelihood (probability) that a software vulnerability will be exploited in the wild. EPSS is a percentile score that ranges from 0 to 1, with higher scores indicating a higher likelihood of exploitation. For more information, see [FIRST.org](https://www.first.org/epss).`n" + + +#Output Step Summary - To the GITHUB_STEP_SUMMARY environment file. GITHUB_STEP_SUMMARY is unique for each step in a job +$markdownSummary > $env:GITHUB_STEP_SUMMARY +#Get-Item -Path $env:GITHUB_STEP_SUMMARY | Show-Markdown +Write-ActionDebug "Markdown Summary from env var GITHUB_STEP_SUMMARY: '$env:GITHUB_STEP_SUMMARY' " +Write-ActionDebug $(Get-Content $env:GITHUB_STEP_SUMMARY) if ($isFail) { Set-ActionFailed -Message $summary From f616d5f94b09b199e86df5969d374cb6651c3b1b Mon Sep 17 00:00:00 2001 From: Chad Bentz <1760475+felickz@users.noreply.github.com> Date: Fri, 5 Jul 2024 19:27:52 -0400 Subject: [PATCH 3/4] Add screenshot --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index c321b0d..3c2dd0c 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,11 @@ Action to detect if any open Dependabot alert CVEs exceed an EPSS threshold and ![image](https://github.com/user-attachments/assets/267c2084-5769-4a82-92ae-2bad09701202) +Includes an Actions workflow summary: + +![image](https://github.com/user-attachments/assets/dc53adad-5aed-4493-acf2-5ea544f30916) + + ## Usage ```yml From 31158fb167f9e9bf02d711e6068b78112e1b91ea Mon Sep 17 00:00:00 2001 From: felickz <1760475+felickz@users.noreply.github.com> Date: Fri, 5 Jul 2024 19:28:49 -0400 Subject: [PATCH 4/4] Allow line break on ecosystem to enhance wordwrap on long string --- action.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/action.ps1 b/action.ps1 index 7ba1c75..a75c224 100644 --- a/action.ps1 +++ b/action.ps1 @@ -233,7 +233,7 @@ if ($isFail) { $color = Convert-SeverityToEmoji -severity $_.security_advisory.severity $scope = $_.dependency.scope $fixAvailable = $_.security_vulnerability.first_patched_version -and $_.security_vulnerability.first_patched_version.identifier -ne $null ? "[✅](# `"$($_.security_vulnerability.first_patched_version.identifier)`")" : "❌" - "[🔴](## `"Error`") | [$cve](https://nvd.nist.gov/vuln/detail/$cve) | $epss ($percentile) | [#$alertNumber]($alertUrl) [🤖](## `"$advisory`") | [$ghsa](https://github.com/advisories/$ghsa) | [$color](## `"$severity`")[$cvssScore](https://www.first.org/cvss/calculator/3.1#$cvssVector) | $created | $package($ecosystem) | [📝](## `"$manifest`") | $scope | $fixAvailable `n" + "[🔴](## `"Error`") | [$cve](https://nvd.nist.gov/vuln/detail/$cve) | $epss ($percentile) | [#$alertNumber]($alertUrl) [🤖](## `"$advisory`") | [$ghsa](https://github.com/advisories/$ghsa) | [$color](## `"$severity`")[$cvssScore](https://www.first.org/cvss/calculator/3.1#$cvssVector) | $created | $package ($ecosystem) | [📝](## `"$manifest`") | $scope | $fixAvailable `n" } $markdownSummary += $markdownSummaryTableRows }