Skip to content

Commit

Permalink
chore(ci): add workflow for scanning unicorn core for CVEs (#1274)
Browse files Browse the repository at this point in the history
## 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
mjnagel authored Feb 13, 2025
1 parent 02db070 commit d7226be
Show file tree
Hide file tree
Showing 6 changed files with 313 additions and 0 deletions.
83 changes: 83 additions & 0 deletions .github/workflows/cve-scan.yaml
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
12 changes: 12 additions & 0 deletions .github/workflows/tag-and-release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,18 @@ jobs:
snapshot: false
secrets: inherit

scan-release:
needs: publish-uds-core-release
permissions:
contents: read
packages: read
id-token: write
issues: write
uses: ./.github/workflows/cve-scan.yaml
with:
release: true
secrets: inherit

checkpoint-uds-core-release:
needs: publish-uds-core-release
permissions:
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ build/**
*.tar.zst
zarf-sbom
zarf-sbom/**
cve
cve/**
tmp/
env.ts
**/node_modules
Expand Down
47 changes: 47 additions & 0 deletions tasks/README.md
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`.
11 changes: 11 additions & 0 deletions tasks/grype-markdown.tmpl
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 }}
158 changes: 158 additions & 0 deletions tasks/scan.yaml
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"

0 comments on commit d7226be

Please sign in to comment.