Skip to content
Draft
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
3 changes: 3 additions & 0 deletions .github/secscan-exclusions/linux-arm.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# CVEs excluded from the secscan gate for this artifact, one per line.
# Every entry needs a justification comment and a link to its draft GHSA.
# High/critical exclusions need GRC/SecOps approval recorded in the GHSA (SEC0025).
3 changes: 3 additions & 0 deletions .github/secscan-exclusions/linux-arm64.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# CVEs excluded from the secscan gate for this artifact, one per line.
# Every entry needs a justification comment and a link to its draft GHSA.
# High/critical exclusions need GRC/SecOps approval recorded in the GHSA (SEC0025).
3 changes: 3 additions & 0 deletions .github/secscan-exclusions/linux-ppc64le.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# CVEs excluded from the secscan gate for this artifact, one per line.
# Every entry needs a justification comment and a link to its draft GHSA.
# High/critical exclusions need GRC/SecOps approval recorded in the GHSA (SEC0025).
3 changes: 3 additions & 0 deletions .github/secscan-exclusions/linux-s390x.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# CVEs excluded from the secscan gate for this artifact, one per line.
# Every entry needs a justification comment and a link to its draft GHSA.
# High/critical exclusions need GRC/SecOps approval recorded in the GHSA (SEC0025).
3 changes: 3 additions & 0 deletions .github/secscan-exclusions/linux-x64.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# CVEs excluded from the secscan gate for this artifact, one per line.
# Every entry needs a justification comment and a link to its draft GHSA.
# High/critical exclusions need GRC/SecOps approval recorded in the GHSA (SEC0025).
8 changes: 8 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,14 @@ jobs:
path: |
_package

secscan:
needs: build
permissions:
contents: read
uses: ./.github/workflows/secscan.yaml # zizmor: ignore[self-repository]
with:
source: artifacts

release:
needs: build
runs-on: ubuntu-latest
Expand Down
137 changes: 137 additions & 0 deletions .github/workflows/secscan.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
# Scans the Linux runner release tarballs with Canonical secscan to meet SEC0025.
# Called by release.yml on the freshly built packages (report-only; it does not block the release), and run
# weekly against the latest published release.
name: Secscan

on:
schedule:
- cron: "29 4 * * 1"
workflow_dispatch:
workflow_call:
inputs:
source:
description: "'artifacts' scans this run's runner-packages-* artifacts; otherwise the latest release."
type: string
default: release
pull_request:
paths:
- .github/workflows/secscan.yaml
- .github/secscan-exclusions/**

permissions: {}

env:
SSDLC_CYCLE: "26.10"

jobs:
secscan:
name: Secscan ${{ matrix.runtime }}
runs-on: [self-hosted-linux-amd64-noble-private-endpoint-medium]
permissions:
contents: read
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
runtime: [linux-x64, linux-arm64, linux-ppc64le, linux-s390x, linux-arm]
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Install secscan client
env:
RUNTIME: ${{ matrix.runtime }}
run: |
# A concurrent job on the same host may win the install race.
snap list canonical-secscan-client >/dev/null 2>&1 ||
sudo snap install canonical-secscan-client ||
snap list canonical-secscan-client >/dev/null 2>&1
sudo snap connect canonical-secscan-client:home system:home
# Per-job dir so concurrent jobs on one host cannot collide on tokens or cleanup.
secscan_dir="$HOME/snap/canonical-secscan-client/common/$GITHUB_RUN_ID-$GITHUB_RUN_ATTEMPT-$RUNTIME"
mkdir -p "$secscan_dir"
echo "SECSCAN_DIR=$secscan_dir" >> "$GITHUB_ENV"
echo "NO_PROXY=${NO_PROXY:+$NO_PROXY,}.internal" >> "$GITHUB_ENV"
echo "no_proxy=${no_proxy:+$no_proxy,}.internal" >> "$GITHUB_ENV"
- name: Download built package
if: inputs.source == 'artifacts'
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: runner-packages-${{ matrix.runtime }}
path: packages
- name: Download latest release package
if: inputs.source != 'artifacts'
env:
RUNTIME: ${{ matrix.runtime }}
GH_TOKEN: ${{ github.token }}
run: |
mkdir -p packages
url="$(curl -fsSL -H "Authorization: Bearer $GH_TOKEN" "https://api.github.com/repos/$GITHUB_REPOSITORY/releases/latest" \
| python3 -c 'import json,sys,os; r=os.environ["RUNTIME"]; print(next(a["browser_download_url"] for a in json.load(sys.stdin)["assets"] if a["name"].startswith(f"actions-runner-{r}-") and a["name"].endswith(".tar.gz")))')"
curl -fsSL -H "Authorization: Bearer $GH_TOKEN" -o "packages/${url##*/}" "$url"
- name: Stage package
id: stage
env:
RUNTIME: ${{ matrix.runtime }}
run: |
shopt -s nullglob
files=(packages/actions-runner-"$RUNTIME"-*.tar.gz)
[[ ${#files[@]} -eq 1 ]] || { echo "::error::expected one $RUNTIME tarball, found ${#files[@]}"; exit 1; }
name="${files[0]##*/}"
version="${name#actions-runner-"$RUNTIME"-}"; version="${version%.tar.gz}"
cp "${files[0]}" "$SECSCAN_DIR/$name"
echo "target=$SECSCAN_DIR/$name" >> "$GITHUB_OUTPUT"
echo "version=$version" >> "$GITHUB_OUTPUT"
- name: Scan
env:
ARTIFACT: ${{ matrix.runtime }}
TARGET: ${{ steps.stage.outputs.target }}
SSDLC_VERSION: ${{ steps.stage.outputs.version }}
SSDLC_CHANNEL: stable
run: |
select_exclusions() { # $1 artifact name, $2 staging dir; prints staged path or nothing
local src=".github/secscan-exclusions/$1.txt"
if [[ -f "$src" ]] && grep -qvE '^[[:space:]]*(#|$)' "$src"; then
cp "$src" "$2/exclusions-$1.txt"; echo "$2/exclusions-$1.txt"
fi
}
classify() { # $1 secscan-client --batch exit code; echoes annotation, returns it
case "$1" in
0) echo "No CVEs found." ;;
101) echo "::error::Excluded CVEs are no longer reported; remove them from .github/secscan-exclusions/." ;;
1[0-9][0-9]) echo "::error::CVEs found; the secscan-report artifact's $ARTIFACT.txt lists them; triage per SECURITY.md." ;;
*) echo "::error::secscan-client failed (exit $1); re-run the job." ;;
esac
return "$1"
}
mkdir -p reports
exclusions=()
excl_file="$(select_exclusions "$ARTIFACT" "$SECSCAN_DIR")"
[[ -n "$excl_file" ]] && exclusions=(--exclusions-filename "$excl_file")
set +e
secscan-client --batch submit \
--scanner trivy --type package --format archive \
--ssdlc-product-name github-runner \
--ssdlc-product-version "$SSDLC_VERSION" \
--ssdlc-product-channel "$SSDLC_CHANNEL" \
--ssdlc-cycle "$SSDLC_CYCLE" \
--token "$SECSCAN_DIR/$ARTIFACT.token" \
"${exclusions[@]}" \
--wait-and-print "$TARGET" | tee "reports/$ARTIFACT.txt"
rc=${PIPESTATUS[0]}
if (( rc == 0 || rc >= 100 )); then
secscan-client --batch report --token "$SECSCAN_DIR/$ARTIFACT.token" \
> "reports/$ARTIFACT.report.html" 2> "reports/$ARTIFACT.report.err" || true
fi
set -e
classify "$rc"
- name: Upload report
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: secscan-report-${{ matrix.runtime }}
path: reports/
if-no-files-found: warn
- name: Clean up secscan staging
if: always()
run: rm -rf "${SECSCAN_DIR:?}"
20 changes: 20 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,23 @@ assigned and coordinating the release of the fix.
The [Ubuntu Security disclosure and embargo policy](https://ubuntu.com/security/disclosure-policy)
contains more information about what you can expect when you contact us, and what we
expect from you.

## Vulnerability scanning

The `Secscan` workflow scans the Linux runner tarballs with Canonical's secscan service
(Trivy). `Runner CD` runs it on the freshly built packages alongside the release; a
failing scan marks the run as failed but does not block the release. It also runs weekly
against the latest release and on demand.

Each `secscan-report-<runtime>` artifact contains `<runtime>.txt`, the authoritative
verdict plus the CVE IDs secscan reported, and `<runtime>.report.html`, Trivy's detailed
report, which may not list every CVE secscan reports for archive scans. The HTML report is
best-effort: it is absent when the scan fails before completing.

When it fails, maintainers download the `secscan-report-*` artifacts and either fix the
issue (usually a .NET or bundled Node.js update) or open a draft
[GitHub security advisory](../../security/advisories/new) recording the CVE, its severity
and the remediation plan. A CVE accepted as a false positive or acceptable risk is added
to `.github/secscan-exclusions/<runtime>.txt` with a justification comment and a link to
its advisory. High or critical exclusions need security team approval. The SSDLC cycle is
set by `SSDLC_CYCLE` in the workflow and must be bumped each cycle.
Loading