Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore(ci): add workflow for scanning unicorn core for CVEs #1274

Merged
merged 7 commits into from
Feb 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"
Loading