-
Notifications
You must be signed in to change notification settings - Fork 24
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
chore(ci): add workflow for scanning unicorn core for CVEs (#1274)
## Description This adds a new task file for scanning for CVEs as well as a workflow to run a nightly scan against the latest unicorn flavor core release. The tasks are built to be dynamic to allow for scanning other packages/versions/flavors as needed and adjust the reporting threshold. The workflow will specifically scan the latest release of core (unicorn flavor) and create a github issue with all High/Critical CVEs listed for triaging/fixing. ## Related Issue Fixes #1273 Closes #1162 - this will replace the need to trigger security-hub's scanning. ## Type of change - [ ] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [x] Other (security config, docs update, etc) ## Steps to Validate This workflow was partially tested on my test repo here (run against registry1 flavor due to permissions for private packages): - CI Run (note artifact for the scan): https://github.com/mjnagel/test/actions/runs/13297486419 - Open Dashboard Issue (note that this has been edited by second run): mjnagel/test#106 ## Checklist before merging - [x] Test, docs, adr added or updated as needed - [x] [Contributor Guide](https://github.com/defenseunicorns/uds-template-capability/blob/main/CONTRIBUTING.md) followed
- Loading branch information
Showing
6 changed files
with
313 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
# Copyright 2025 Defense Unicorns | ||
# SPDX-License-Identifier: AGPL-3.0-or-later OR LicenseRef-Defense-Unicorns-Commercial | ||
|
||
# This workflow runs a nightly CVE scan against the unicorn flavor of UDS Core and aggregates results in a GitHub issue | ||
name: CVE Scan | ||
|
||
on: | ||
schedule: | ||
# Nightly at 12am MT / 7am UTC | ||
- cron: "0 7 * * *" | ||
pull_request: | ||
paths: | ||
- .github/workflows/cve-scan.yaml | ||
- tasks/scan.yaml | ||
- tasks/grype-markdown.tmpl | ||
workflow_call: | ||
inputs: | ||
release: | ||
type: boolean | ||
description: "Whether this is a release or not (will update issue)" | ||
default: false | ||
|
||
permissions: | ||
id-token: write | ||
contents: read | ||
issues: write # Needed to create/update issues | ||
packages: read # Allows reading the unicorn GHCR packages | ||
|
||
jobs: | ||
scan-unicorn-package: | ||
runs-on: ubuntu-latest | ||
|
||
steps: | ||
- name: Checkout repository | ||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 | ||
|
||
- name: Environment setup | ||
uses: ./.github/actions/setup | ||
with: | ||
registry1Username: ${{ secrets.IRON_BANK_ROBOT_USERNAME }} | ||
registry1Password: ${{ secrets.IRON_BANK_ROBOT_PASSWORD }} | ||
ghToken: ${{ secrets.GITHUB_TOKEN }} | ||
chainguardIdentity: ${{ secrets.CHAINGUARD_IDENTITY }} | ||
|
||
- name: Install Grype | ||
run: curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b /usr/local/bin | ||
|
||
- name: Scan latest unicorn package | ||
# This task uses the defaults of latest version, unicorn flavor, core package, and high severity | ||
run: uds run -f tasks/scan.yaml | ||
|
||
# Only upload artifacts for PR runs | ||
- name: Upload CVE report to GitHub artifacts | ||
if: ${{ github.event_name == 'pull_request' }} | ||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 | ||
with: | ||
name: cve-scan-report | ||
path: cve/scans/core-vulnerability-report.md | ||
|
||
# Create or update GitHub issue for scheduled runs | ||
- name: Create/Update CVE Scan Issue | ||
if: ${{ github.event_name == 'schedule' || inputs.release }} | ||
env: | ||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
REPO: ${{ github.repository }} | ||
REPORT_FILE: "cve/scans/core-vulnerability-report.md" | ||
run: | | ||
ISSUE_TITLE="CVE Dashboard" | ||
ISSUE_AUTHOR="github-actions[bot]" | ||
# Read the CVE report content *without* JSON escaping issues | ||
CVE_REPORT=$(cat "$REPORT_FILE") | ||
# Search for an existing issue by title & author | ||
ISSUE_NUMBER=$(gh issue list --repo "$REPO" --search "$ISSUE_TITLE in:title author:$ISSUE_AUTHOR" --state open --json number --jq '.[0].number') | ||
if [[ -n "$ISSUE_NUMBER" ]]; then | ||
echo "Updating existing issue #$ISSUE_NUMBER..." | ||
gh issue edit "$ISSUE_NUMBER" --repo "$REPO" --title "$ISSUE_TITLE" --body "$CVE_REPORT" | ||
else | ||
echo "Creating a new issue..." | ||
gh issue create --repo "$REPO" --title "$ISSUE_TITLE" --body "$CVE_REPORT" --label "security" | ||
fi |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,6 +5,8 @@ build/** | |
*.tar.zst | ||
zarf-sbom | ||
zarf-sbom/** | ||
cve | ||
cve/** | ||
tmp/ | ||
env.ts | ||
**/node_modules | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
# Tasks for UDS Core | ||
|
||
This directory contains a number of [UDS task files](https://uds.defenseunicorns.com/reference/cli/uds-runner/) that are used both for CI and local dev to support testing and publishing workflows. | ||
|
||
## `create.yaml` | ||
|
||
Create tasks are used to create the core packages and bundles. See all available tasks and descriptions with `uds run -f tasks/create.yaml --list`. | ||
|
||
## `deploy.yaml` | ||
|
||
Deploy tasks are user to deploy the core packages and bundles. See all available tasks and descriptions with `uds run -f tasks/deploy.yaml --list`. | ||
|
||
## `iac.yaml` | ||
|
||
IAC tasks are primarily used for nightly/weekly CI deployments to cloud provider infrastructure. See all available tasks and descriptions with `uds run -f tasks/iac.yaml --list`. | ||
|
||
## `lint.yaml` | ||
|
||
Linting tasks provide basic lint checks and fixes for spelling, formatting, and licensing headers. Some of these tasks are also used for pre-commit checks. See all available tasks and descriptions with `uds run -f tasks/lint.yaml --list`. | ||
|
||
## `publish.yaml` | ||
|
||
Publish tasks are used in CI to publish the core packages to the OCI registry. See all available tasks and descriptions with `uds run -f tasks/publish.yaml --list`. | ||
|
||
## `scan.yaml` | ||
|
||
Scan tasks will run CVE scanning against a given package and provide a markdown report of all CVEs. The default task will run this end to end and accepts variables `VERSION`, `FLAVOR`, `PACKAGE`, and `MIN_SEVERITY`. For example, the following task will scan the `core-base:0.34.0-upstream` package and report out any CVEs that are Medium severity or worse: | ||
|
||
```console | ||
uds run -f tasks/scan.yaml --set PACKAGE=core-base --set MIN_SEVERITY=Medium --set VERSION=0.34.0 --set FLAVOR=upstream | ||
``` | ||
|
||
All pulled information and produced artifacts will be stored under the `cve` directory locally, with the aggregated report for this example at `cve/scans/core-base-vulnerability-report.md`. Note that by default this will pull the package for your local system architecture although it can be overridden using the `ZARF_ARCHITECTURE` env if necessary (for example, registry1 flavor packages are only published for amd64). | ||
|
||
See all available tasks and descriptions with `uds run -f tasks/scan.yaml --list`. | ||
|
||
## `setup.yaml` | ||
|
||
Setup tasks provide developers and CI with basic Kubernetes clusters using [`uds-k3d`](https://github.com/defenseunicorns/uds-k3d) as well as the zarf init package. See all available tasks and descriptions with `uds run -f tasks/setup.yaml --list`. | ||
|
||
## `test.yaml` | ||
|
||
Test tasks run validations and end-to-end testing as well as some full create/deploy/test tasks. See all available tasks and descriptions with `uds run -f tasks/test.yaml --list`. | ||
|
||
## `utils.yaml` | ||
|
||
Utility tasks provide various utilities to support publishing and testing primarily. See all available tasks and descriptions with `uds run -f tasks/utils.yaml --list`. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
{{- $severity_order := dict "Negligible" 0 "Unknown" 1 "Low" 2 "Medium" 3 "High" 4 "Critical" 5 -}} | ||
{{- $min_severity := "High" -}} {{- /* This will be replaced dynamically */ -}} | ||
{{- $min_severity_rank := index $severity_order $min_severity -}} | ||
| Name | Installed | Fixed-In | Type | Vulnerability | Severity | | ||
|------|-----------|----------|------|--------------|----------| | ||
{{- range .Matches }} | ||
{{- $cve_severity := index $severity_order .Vulnerability.Severity }} | ||
{{- if ge $cve_severity $min_severity_rank }} | ||
| {{ .Artifact.Name }} | {{ .Artifact.Version }} | {{ if .Vulnerability.Fix.Versions }}{{ .Vulnerability.Fix.Versions | join ", " }}{{ else }}(not fixed){{ end }} | {{ .Artifact.Type }} | {{ .Vulnerability.ID }} | {{ .Vulnerability.Severity }} | | ||
{{- end }} | ||
{{- end }} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,158 @@ | ||
# Copyright 2025 Defense Unicorns | ||
# SPDX-License-Identifier: AGPL-3.0-or-later OR LicenseRef-Defense-Unicorns-Commercial | ||
|
||
includes: | ||
- utils: utils.yaml | ||
|
||
variables: | ||
- name: VERSION | ||
description: "The version of the packages to scan, or 'latest' for the latest version" | ||
default: "latest" | ||
- name: FLAVOR | ||
description: "The flavor of the package to scan" | ||
default: unicorn | ||
- name: PACKAGE | ||
description: "The name of the package to scan from the registry/repository" | ||
default: core | ||
- name: MIN_SEVERITY | ||
description: "The minimum severity level for CVEs to report" | ||
default: "High" | ||
|
||
tasks: | ||
- name: default | ||
actions: | ||
- task: pull-sbom | ||
- task: scan-sbom | ||
- task: aggregate-results | ||
|
||
- name: pull-sbom | ||
description: "Pull the SBOMs for the specified package" | ||
actions: | ||
- task: utils:determine-repo | ||
- description: "Append flavor to version" | ||
if: ${{ ne .variables.VERSION "latest" }} | ||
cmd: echo ${VERSION}-${FLAVOR} | ||
setVariables: | ||
- name: VERSION | ||
- description: "Get latest tag version from OCI" | ||
if: ${{ eq .variables.VERSION "latest" }} | ||
cmd: uds zarf tools registry ls ${TARGET_REPO}/${PACKAGE} | grep ${FLAVOR} | sort -V | tail -1 | ||
setVariables: | ||
- name: VERSION | ||
- description: "Pull the SBOMs from the package" | ||
cmd: | | ||
rm -rf cve/sboms/${PACKAGE} | ||
mkdir -p cve/sboms | ||
uds zarf package inspect sbom oci://${TARGET_REPO}/${PACKAGE}:${VERSION} --output cve/sboms | ||
# Note: This task assumes local SBOMs already available for the specified package | ||
- name: scan-sbom | ||
description: "Scan the local SBOMs for vulnerabilities" | ||
actions: | ||
- description: "Scan the SBOMs for vulnerabilities" | ||
shell: | ||
darwin: bash | ||
linux: bash | ||
cmd: | | ||
rm -rf cve/scans/${PACKAGE} | ||
mkdir -p cve/scans/${PACKAGE} | ||
# Generate a temporary Grype template with the severity injected | ||
sed "s/{{- \$min_severity := .* -}}/{{- \$min_severity := \"$MIN_SEVERITY\" -}}/" tasks/grype-markdown.tmpl > tasks/grype-markdown-severity.tmpl | ||
for image in cve/sboms/${PACKAGE}/*.json; do | ||
imagename=$(basename "$image" .json) | ||
grype "sbom:$image" -o template -t tasks/grype-markdown-severity.tmpl > cve/scans/${PACKAGE}/$imagename.md | ||
done | ||
rm -rf tasks/grype-markdown-severity.tmpl | ||
# Note: This task assumes proper variables passed in for PACKAGE, VERSION, and MIN_SEVERITY to generate an accurate report | ||
- name: aggregate-results | ||
description: "Aggregate the scan results into a markdown report" | ||
actions: | ||
- description: "Aggregate the scan results" | ||
shell: | ||
darwin: bash | ||
linux: bash | ||
cmd: | | ||
output_file="cve/scans/${PACKAGE}-vulnerability-report.md" | ||
echo "## Vulnerability Report for ${PACKAGE} ${VERSION}" > "$output_file" | ||
echo "" >> "$output_file" | ||
# Get timestamp in UTC (ISO format) | ||
TIMESTAMP=$(date -u +"%Y-%m-%d %H:%M:%S UTC") | ||
echo "This report includes vulnerabilities detected by Grype with a minimum severity of **${MIN_SEVERITY}**." >> "$output_file" | ||
echo "" >> "$output_file" | ||
echo "**Last scanned at:** ${TIMESTAMP}" >> "$output_file" | ||
echo "" >> "$output_file" | ||
if [[ "$MIN_SEVERITY" == "Critical" ]]; then | ||
echo "**Total Critical CVE counts:**" >> "$output_file" | ||
echo "- Critical: {{TOTAL_CRITICAL}}" >> "$output_file" | ||
else | ||
echo "**Total Critical/High CVE counts:**" >> "$output_file" | ||
echo "- Critical: {{TOTAL_CRITICAL}}" >> "$output_file" | ||
echo "- High: {{TOTAL_HIGH}}" >> "$output_file" | ||
fi | ||
echo "" >> "$output_file" | ||
echo "### Detailed Image Vulnerabilities" >> "$output_file" | ||
echo "" >> "$output_file" | ||
any_vulnerabilities_found=false | ||
for scan in cve/scans/${PACKAGE}/*.md; do | ||
[ -e "$scan" ] || continue # Skip if no matching files | ||
# Extract image name and format properly | ||
image_name=$(basename "$scan" .md) | ||
image_name=${image_name//_/\/} # Replace all `_` with `/` | ||
image_name=${image_name/%\//:} # Replace the LAST `/` with `:` | ||
# Check if the scan file contains any vulnerabilities (ignoring headers) | ||
vuln_count=$(tail -n +3 "$scan") | ||
if [[ ! "$vuln_count" ]]; then | ||
continue | ||
fi | ||
any_vulnerabilities_found=true | ||
# Append the results to the report | ||
echo "<details><summary><code>$image_name</code></summary>" >> "$output_file" | ||
echo "" >> "$output_file" | ||
cat "$scan" >> "$output_file" | ||
echo "" >> "$output_file" | ||
echo "</details>" >> "$output_file" | ||
echo "" >> "$output_file" | ||
done | ||
# If no vulnerabilities were found in any image, indicate that in the report | ||
if [[ "$any_vulnerabilities_found" == "false" ]]; then | ||
echo "No vulnerabilities of severity **${MIN_SEVERITY}** or greater found." >> "$output_file" | ||
fi | ||
# Extract Critical CVE counts | ||
critical_cve=$(grep -hE '\|.*\|.*\|.*\|.*\|.*\| Critical \|' "$output_file" | cut -d'|' -f6) | ||
if [[ -n "$critical_cve" ]]; then | ||
total_critical_cve=$(echo "$critical_cve" | wc -l | tr -d ' ') | ||
unique_critical_cve=$(echo "$critical_cve" | sort -u | wc -l | tr -d ' ') | ||
else | ||
total_critical_cve=0 | ||
unique_critical_cve=0 | ||
fi | ||
# Extract High CVE counts | ||
high_cve=$(grep -hE '\|.*\|.*\|.*\|.*\|.*\| High \|' "$output_file" | cut -d'|' -f6) | ||
if [[ -n "$high_cve" ]]; then | ||
total_high_cve=$(echo "$high_cve" | wc -l | tr -d ' ') | ||
unique_high_cve=$(echo "$high_cve" | sort -u | wc -l | tr -d ' ') | ||
else | ||
total_high_cve=0 | ||
unique_high_cve=0 | ||
fi | ||
# Replace placeholders with actual totals | ||
sed -i "s/{{TOTAL_CRITICAL}}/${total_critical_cve} (${unique_critical_cve} unique)/g" "$output_file" | ||
sed -i "s/{{TOTAL_HIGH}}/${total_high_cve} (${unique_high_cve} unique)/g" "$output_file" |