Skip to content
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
71 changes: 51 additions & 20 deletions .github/workflows/reusable-secrets-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,33 @@
# license for organizations).
#
# No inputs required — always runs with default config
#
# Supply-chain hardening (GATOR-3555):
# - gitleaks is pinned to one version and its tarball is checked against a
# sha256 HARDCODED below (not the release's own checksums file, which an
# attacker who controls the release could replace too). A mismatch fails
# the job before anything is extracted.
# - actions/checkout is pinned to a commit SHA, and the job token is not
# persisted into .git/config.
# - The SARIF upload step was removed: no caller grants the job
# `security-events: write` (callers grant `contents: read`, and the org
# default token is read-only), so it failed on every run behind
# `continue-on-error`. Findings are reported in the job log (--verbose
# --redact) and the job fails on a hit, as before.
#
# Bumping gitleaks (manual; Dependabot does not track it):
# 1. Pick a stable release at least 7 days old:
# gh api repos/gitleaks/gitleaks/releases --jq '.[] | .tag_name+" "+.published_at'
# 2. Take the linux_x64 tarball's sha256 from TWO sources and check they match:
# gh api repos/gitleaks/gitleaks/releases/tags/vX.Y.Z \
# --jq '.assets[] | select(.name|endswith("_linux_x64.tar.gz")) | .digest'
# curl -fsSLO https://github.com/gitleaks/gitleaks/releases/download/vX.Y.Z/gitleaks_X.Y.Z_linux_x64.tar.gz
# sha256sum gitleaks_X.Y.Z_linux_x64.tar.gz # (macOS: shasum -a 256)
# 3. Update GITLEAKS_VERSION and GITLEAKS_SHA256 together, in a reviewed PR.
# actions/checkout is bumped by Dependabot (.github/dependabot.yml), which
# updates the SHA and its version comment.
#
# Regression test: self-test-secrets-check.yml (lint, fail-closed install, smoke).

name: Secrets Check

Expand All @@ -15,35 +42,39 @@ on:
jobs:
secrets-check:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0 # Full history for scanning
persist-credentials: false

- name: Install Gitleaks
shell: bash
env:
GH_TOKEN: ${{ github.token }}
GITLEAKS_VERSION: "8.30.1"
GITLEAKS_SHA256: "551f6fc83ea457d62a0d98237cbad105af8d557003051f41f3e7ca7b3f2470eb"
run: |
GITLEAKS_VERSION=$(curl -sH "Authorization: Bearer $GH_TOKEN" \
https://api.github.com/repos/gitleaks/gitleaks/releases/latest \
| grep '"tag_name"' | sed 's/.*"v\(.*\)".*/\1/')
if [ -z "$GITLEAKS_VERSION" ]; then
echo "::error::Failed to fetch latest gitleaks version (API rate limit?)"
exit 1
fi
curl -sSLo gitleaks.tar.gz "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz"
tar -xzf gitleaks.tar.gz gitleaks
chmod +x gitleaks
sudo mv gitleaks /usr/local/bin/
set -euo pipefail
tarball="gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz"
dldir="${RUNNER_TEMP}/gitleaks-download"
bindir="${RUNNER_TEMP}/gitleaks/bin"
mkdir -p "${dldir}"
cd "${dldir}"
curl -fsSL --proto '=https' --proto-redir '=https' --tlsv1.2 --retry 3 -o "${tarball}" \
"https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/${tarball}"
# The install dir is created only after the hash check passes.
echo "${GITLEAKS_SHA256} ${tarball}" | sha256sum --check --strict -
mkdir -p "${bindir}"
tar -xzf "${tarball}" -C "${bindir}" --no-same-owner gitleaks
chmod 0755 "${bindir}/gitleaks"
echo "${bindir}" >> "${GITHUB_PATH}"

- name: Run Gitleaks
shell: bash
run: |
set -euo pipefail
gitleaks version
gitleaks detect --source . --verbose --redact --report-format sarif --report-path gitleaks-report.sarif

- name: Upload SARIF report
if: always()
uses: github/codeql-action/upload-sarif@v4
with:
sarif_file: gitleaks-report.sarif
continue-on-error: true # Don't fail if Code Security is not enabled
170 changes: 170 additions & 0 deletions .github/workflows/self-test-secrets-check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
# Self-test for reusable-secrets-check.yml (GATOR-3555)
#
# A committed regression test for the supply-chain properties of the
# reusable secrets check:
# lint - no floating gitleaks lookup (releases/latest), every
# `uses:` pinned to a 40-hex commit SHA, checkout with
# persist-credentials: false, a 64-hex GITLEAKS_SHA256, and
# no token or secret reference.
# fail-closed - the Install step's own script, run with a wrong sha256,
# exits non-zero after the download and before anything is
# installed; run unmodified, it installs the pinned version.
# smoke - the reusable workflow at this commit scans this repo and
# passes.
#
# yq here is mikefarah yq v4, preinstalled on ubuntu-latest.

name: Self-test Secrets Check

on:
pull_request:
paths:
- .github/workflows/reusable-secrets-check.yml
- .github/workflows/self-test-secrets-check.yml
push:
branches:
- main
paths:
- .github/workflows/reusable-secrets-check.yml
- .github/workflows/self-test-secrets-check.yml

permissions:
contents: read

jobs:
lint:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Lint reusable-secrets-check.yml
shell: bash
env:
TARGET: .github/workflows/reusable-secrets-check.yml
run: |
set -euo pipefail
yq --version | grep -q 'mikefarah' || { echo "::error::mikefarah yq v4 is required"; exit 1; }
[ -f "${TARGET}" ] || { echo "::error::${TARGET} not found"; exit 1; }
yq -e '.jobs' "${TARGET}" > /dev/null

fail=0
violation() {
echo "::error file=${TARGET}::VIOLATION: $1"
fail=1
}

# 1. No floating version lookup.
while IFS= read -r line; do
violation "floating release lookup: ${line}"
done < <(grep -n 'releases/latest' "${TARGET}" || true)

# 2. Every uses: is pinned to a full commit SHA.
n_uses=0
while IFS= read -r ref; do
n_uses=$((n_uses + 1))
[[ "${ref}" =~ @[0-9a-f]{40}$ ]] || violation "uses: '${ref}' is not pinned to a 40-hex commit SHA"
done < <(yq '.. | select(tag == "!!map" and has("uses")) | .uses' "${TARGET}")
[ "${n_uses}" -gt 0 ] || violation "no uses: entries found"

# 3. Every checkout drops the token.
n_checkout=0
while IFS= read -r pc; do
n_checkout=$((n_checkout + 1))
[ "${pc}" = "false" ] || violation "actions/checkout without persist-credentials: false (got '${pc}')"
done < <(yq '.. | select(tag == "!!map" and has("uses")) | select(.uses | test("^actions/checkout@")) | .with["persist-credentials"]' "${TARGET}")
[ "${n_checkout}" -gt 0 ] || violation "no actions/checkout step found"

# 4. Exactly one hardcoded 64-hex GITLEAKS_SHA256.
n_sha=0
while IFS= read -r sha; do
n_sha=$((n_sha + 1))
[[ "${sha}" =~ ^[0-9a-f]{64}$ ]] || violation "GITLEAKS_SHA256 is not 64 lowercase hex: '${sha}'"
done < <(yq '.. | select(tag == "!!map" and has("GITLEAKS_SHA256")) | .GITLEAKS_SHA256' "${TARGET}")
[ "${n_sha}" -eq 1 ] || violation "expected exactly one GITLEAKS_SHA256, found ${n_sha}"

# 5. No token or secret reference.
while IFS= read -r line; do
violation "token or secret reference: ${line}"
done < <(grep -nE 'secrets\.|GH_TOKEN|github\.token' "${TARGET}" || true)

[ "${fail}" -eq 0 ] || exit 1
echo "lint: OK (${n_uses} uses, ${n_checkout} checkout, ${n_sha} GITLEAKS_SHA256)"

fail-closed:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Extract the Install Gitleaks script
shell: bash
env:
TARGET: .github/workflows/reusable-secrets-check.yml
run: |
set -euo pipefail
yq --version | grep -q 'mikefarah' || { echo "::error::mikefarah yq v4 is required"; exit 1; }
step='.jobs[].steps[] | select(.name == "Install Gitleaks")'
n=$(yq "[${step}] | length" "${TARGET}")
[ "${n}" = "1" ] || { echo "::error::expected one Install Gitleaks step, found ${n}"; exit 1; }
yq "${step} | .run" "${TARGET}" > "${RUNNER_TEMP}/install-gitleaks.sh"
version=$(yq "${step} | .env.GITLEAKS_VERSION" "${TARGET}")
sha=$(yq "${step} | .env.GITLEAKS_SHA256" "${TARGET}")
[[ "${version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || { echo "::error::bad GITLEAKS_VERSION '${version}'"; exit 1; }
[[ "${sha}" =~ ^[0-9a-f]{64}$ ]] || { echo "::error::bad GITLEAKS_SHA256"; exit 1; }
{
echo "PINNED_VERSION=${version}"
echo "PINNED_SHA256=${sha}"
} >> "${GITHUB_ENV}"
echo "extracted: $(wc -l < "${RUNNER_TEMP}/install-gitleaks.sh") lines, version ${version}"

- name: Wrong sha256 fails closed
shell: bash
run: |
set -euo pipefail
sandbox=$(mktemp -d "${RUNNER_TEMP}/neg.XXXXXX")
: > "${sandbox}/github_path"
zeros=$(printf '%064d' 0)
set +e
out=$(env RUNNER_TEMP="${sandbox}" GITHUB_PATH="${sandbox}/github_path" \
GITLEAKS_VERSION="${PINNED_VERSION}" GITLEAKS_SHA256="${zeros}" \
bash "${RUNNER_TEMP}/install-gitleaks.sh" 2>&1)
rc=$?
# errexit stays off through the checks so every one reports.
printf '%s\n' "${out}"
fail=0
check() {
if [ "$2" = "0" ]; then echo "ok: $1"; else echo "::error::FAILED: $1"; fail=1; fi
}
[ "${rc}" -ne 0 ]; check "exits non-zero (rc=${rc})" "$?"
[ -s "${sandbox}/gitleaks-download/gitleaks_${PINNED_VERSION}_linux_x64.tar.gz" ]
check "the tarball was downloaded, so the hash check is what failed" "$?"
grep -q ': FAILED' <<< "${out}"; check "sha256sum reported the mismatch" "$?"
[ ! -e "${sandbox}/gitleaks" ]; check "RUNNER_TEMP/gitleaks does not exist" "$?"
[ ! -s "${sandbox}/github_path" ]; check "nothing was added to GITHUB_PATH" "$?"
if command -v gitleaks; then on_path=1; else on_path=0; fi
check "gitleaks is not on PATH" "${on_path}"
exit "${fail}"

- name: Correct sha256 installs the pinned version
shell: bash
run: |
set -euo pipefail
sandbox=$(mktemp -d "${RUNNER_TEMP}/pos.XXXXXX")
: > "${sandbox}/github_path"
env RUNNER_TEMP="${sandbox}" GITHUB_PATH="${sandbox}/github_path" \
GITLEAKS_VERSION="${PINNED_VERSION}" GITLEAKS_SHA256="${PINNED_SHA256}" \
bash "${RUNNER_TEMP}/install-gitleaks.sh"
[ "$(cat "${sandbox}/github_path")" = "${sandbox}/gitleaks/bin" ] || { echo "::error::GITHUB_PATH not set to the install dir"; exit 1; }
got=$("${sandbox}/gitleaks/bin/gitleaks" version)
echo "gitleaks version: ${got}"
[ "${got}" = "${PINNED_VERSION}" ] || { echo "::error::expected ${PINNED_VERSION}, got ${got}"; exit 1; }

smoke:
uses: ./.github/workflows/reusable-secrets-check.yml
Loading