Skip to content

Commit

Permalink
Merge pull request #7 from advanced-security/workflow-summary
Browse files Browse the repository at this point in the history
Enhancement - Add workflow summary
  • Loading branch information
felickz authored Jul 5, 2024
2 parents 76e7d60 + c56a338 commit 1819f18
Show file tree
Hide file tree
Showing 2 changed files with 146 additions and 37 deletions.
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
178 changes: 141 additions & 37 deletions action.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand All @@ -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)
Expand Down Expand Up @@ -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.`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 = "[$OrganizationName/$RepositoryName] - $($Dependabot_Alerts_CVEs.Count) Dependabot Alerts total that reference a CVE.`n"
$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
Expand Down

0 comments on commit 1819f18

Please sign in to comment.