From 34da1b2a2b26c3420e1765f7d064d290154c6be4 Mon Sep 17 00:00:00 2001 From: Micah Nagel Date: Wed, 12 Feb 2025 17:00:34 -0700 Subject: [PATCH 1/5] chore(ci): add workflow for scanning unicorn core for CVEs --- .github/workflows/cve-scan.yaml | 72 +++++++++++++++ .gitignore | 2 + tasks/grype-markdown.tmpl | 11 +++ tasks/scan.yaml | 156 ++++++++++++++++++++++++++++++++ 4 files changed, 241 insertions(+) create mode 100644 .github/workflows/cve-scan.yaml create mode 100644 tasks/grype-markdown.tmpl create mode 100644 tasks/scan.yaml diff --git a/.github/workflows/cve-scan.yaml b/.github/workflows/cve-scan.yaml new file mode 100644 index 000000000..9187aed14 --- /dev/null +++ b/.github/workflows/cve-scan.yaml @@ -0,0 +1,72 @@ +# 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 + +permissions: + id-token: write + contents: read + issues: write # Needed to create/update issues + +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: 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' }} + 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 diff --git a/.gitignore b/.gitignore index 060031358..03fae75bd 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,8 @@ build/** *.tar.zst zarf-sbom zarf-sbom/** +cve +cve/** tmp/ env.ts **/node_modules diff --git a/tasks/grype-markdown.tmpl b/tasks/grype-markdown.tmpl new file mode 100644 index 000000000..ca5956249 --- /dev/null +++ b/tasks/grype-markdown.tmpl @@ -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 }}(won't fix){{ end }} | {{ .Artifact.Type }} | {{ .Vulnerability.ID }} | {{ .Vulnerability.Severity }} | + {{- end }} +{{- end }} diff --git a/tasks/scan.yaml b/tasks/scan.yaml new file mode 100644 index 000000000..1591a2e90 --- /dev/null +++ b/tasks/scan.yaml @@ -0,0 +1,156 @@ +# 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" + + 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 "### $image_name" >> "$output_file" + echo "" >> "$output_file" + cat "$scan" >> "$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 found 🎉" >> "$output_file" + echo "" >> "$output_file" + echo "No vulnerabilities of severity **${MIN_SEVERITY}** or greater were found in ${PACKAGE} ${VERSION}." >> "$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" From fd5025a727c7f0fc99d9314ae9c39bc3ba3c57c2 Mon Sep 17 00:00:00 2001 From: Micah Nagel Date: Wed, 12 Feb 2025 17:06:53 -0700 Subject: [PATCH 2/5] chore: add grype install --- .github/workflows/cve-scan.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/cve-scan.yaml b/.github/workflows/cve-scan.yaml index 9187aed14..68e43af64 100644 --- a/.github/workflows/cve-scan.yaml +++ b/.github/workflows/cve-scan.yaml @@ -34,6 +34,9 @@ jobs: 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 From 796d170f7484ad5a297af1da198cb423c50d8b94 Mon Sep 17 00:00:00 2001 From: Micah Nagel Date: Wed, 12 Feb 2025 17:31:28 -0700 Subject: [PATCH 3/5] fix: permissions for workflow --- .github/workflows/cve-scan.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/cve-scan.yaml b/.github/workflows/cve-scan.yaml index 68e43af64..32359422d 100644 --- a/.github/workflows/cve-scan.yaml +++ b/.github/workflows/cve-scan.yaml @@ -17,6 +17,7 @@ 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: From fc60e777a7a04b90c72127dea5b63e30581afbf2 Mon Sep 17 00:00:00 2001 From: Micah Nagel Date: Wed, 12 Feb 2025 20:29:16 -0700 Subject: [PATCH 4/5] fix: grype-markdown.tmpl --- tasks/grype-markdown.tmpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tasks/grype-markdown.tmpl b/tasks/grype-markdown.tmpl index ca5956249..a31abac9f 100644 --- a/tasks/grype-markdown.tmpl +++ b/tasks/grype-markdown.tmpl @@ -6,6 +6,6 @@ {{- 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 }}(won't fix){{ end }} | {{ .Artifact.Type }} | {{ .Vulnerability.ID }} | {{ .Vulnerability.Severity }} | +| {{ .Artifact.Name }} | {{ .Artifact.Version }} | {{ if .Vulnerability.Fix.Versions }}{{ .Vulnerability.Fix.Versions | join ", " }}{{ else }}(not fixed){{ end }} | {{ .Artifact.Type }} | {{ .Vulnerability.ID }} | {{ .Vulnerability.Severity }} | {{- end }} {{- end }} From d1de9c84af04afa5908fbbfa31a5b331cbbb8b84 Mon Sep 17 00:00:00 2001 From: Micah Nagel Date: Thu, 13 Feb 2025 10:45:03 -0700 Subject: [PATCH 5/5] chore: add doc, cleanup format, run on release --- .github/workflows/cve-scan.yaml | 9 ++++- .github/workflows/tag-and-release.yaml | 12 +++++++ tasks/README.md | 47 ++++++++++++++++++++++++++ tasks/scan.yaml | 10 +++--- 4 files changed, 73 insertions(+), 5 deletions(-) create mode 100644 tasks/README.md diff --git a/.github/workflows/cve-scan.yaml b/.github/workflows/cve-scan.yaml index 32359422d..cdaeb02be 100644 --- a/.github/workflows/cve-scan.yaml +++ b/.github/workflows/cve-scan.yaml @@ -12,6 +12,13 @@ on: 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 @@ -52,7 +59,7 @@ jobs: # Create or update GitHub issue for scheduled runs - name: Create/Update CVE Scan Issue - if: ${{ github.event_name == 'schedule' }} + if: ${{ github.event_name == 'schedule' || inputs.release }} env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} REPO: ${{ github.repository }} diff --git a/.github/workflows/tag-and-release.yaml b/.github/workflows/tag-and-release.yaml index 85d4bbb99..eb1acb47c 100644 --- a/.github/workflows/tag-and-release.yaml +++ b/.github/workflows/tag-and-release.yaml @@ -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: diff --git a/tasks/README.md b/tasks/README.md new file mode 100644 index 000000000..8d7ad1766 --- /dev/null +++ b/tasks/README.md @@ -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`. diff --git a/tasks/scan.yaml b/tasks/scan.yaml index 1591a2e90..c60c9581e 100644 --- a/tasks/scan.yaml +++ b/tasks/scan.yaml @@ -97,6 +97,8 @@ tasks: echo "- High: {{TOTAL_HIGH}}" >> "$output_file" fi echo "" >> "$output_file" + echo "### Detailed Image Vulnerabilities" >> "$output_file" + echo "" >> "$output_file" any_vulnerabilities_found=false @@ -118,17 +120,17 @@ tasks: any_vulnerabilities_found=true # Append the results to the report - echo "### $image_name" >> "$output_file" + echo "
$image_name" >> "$output_file" echo "" >> "$output_file" cat "$scan" >> "$output_file" echo "" >> "$output_file" + echo "
" >> "$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 found 🎉" >> "$output_file" - echo "" >> "$output_file" - echo "No vulnerabilities of severity **${MIN_SEVERITY}** or greater were found in ${PACKAGE} ${VERSION}." >> "$output_file" + echo "No vulnerabilities of severity **${MIN_SEVERITY}** or greater found." >> "$output_file" fi # Extract Critical CVE counts