Skip to content

Detect incomplete GutPacer household deletion (#84) #121

Detect incomplete GutPacer household deletion (#84)

Detect incomplete GutPacer household deletion (#84) #121

name: Security baseline
on:
pull_request:
push:
branches: [main]
schedule:
# Monday 03:31 JST. The non-round minute avoids peak scheduler load.
- cron: '31 18 * * 0'
workflow_dispatch:
permissions:
contents: read
concurrency:
group: security-baseline-${{ github.ref }}
cancel-in-progress: true
jobs:
secret-scan:
name: Full-history secret scan
runs-on: ubuntu-latest
steps:
- name: Check out full history
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
fetch-depth: 0
- name: Check public repository boundary
run: python3 scripts/check_public_repo.py --tracked
# It only ran in pre-commit, so a clone without the hook installed, or a
# change made through the web UI, went straight through (noticed
# 2026-09-03). Run it over every tracked text file. The checker itself is
# excluded via .langcheckignore, because it holds the characters it looks
# for in its own regexes.
- name: Check language contamination
run: |
git ls-files '*.md' '*.mjs' '*.js' '*.ts' '*.html' '*.py' \
| xargs python3 scripts/check_language_contamination.py
# gitleaks-action is not used here: it decides the scan range itself.
#
# Measured 2026-09-17: this job was named "Full-history secret scan" and
# checked out with fetch-depth: 0, but the action adds
# --log-opts=--no-merges --first-parent <sha>^..<sha>, so on pull_request
# and push it scanned a single commit (149-5,675 bytes).
#
# Correction (same day): an earlier version of this comment claimed the full
# history was never scanned and that past secrets could never be found. That
# was wrong. The action only narrows the range on push and pull_request; on
# schedule it passes no --log-opts and scans the whole history (measured:
# 183 commits / 1.63 MB). The real problems are that the job name did not
# match what it did per PR, and that a change merged in a PR was not looked
# at from a full-history angle until that week's cron run.
#
# gitleaks is called directly so the range is explicit. The version is
# pinned and the download is verified with SHA-256.
- name: Install gitleaks (pinned + checksum verified)
env:
GITLEAKS_VERSION: 8.30.1
GITLEAKS_SHA256: 551f6fc83ea457d62a0d98237cbad105af8d557003051f41f3e7ca7b3f2470eb
run: |
set -euo pipefail
curl -sSfL -o /tmp/gitleaks.tar.gz \
"https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz"
echo "${GITLEAKS_SHA256} /tmp/gitleaks.tar.gz" | sha256sum -c -
tar -xzf /tmp/gitleaks.tar.gz -C /tmp gitleaks
/tmp/gitleaks version
- name: Scan the whole history
run: |
set -euo pipefail
# No --log-opts: gitleaks walks the whole history by default.
/tmp/gitleaks git . --redact -v --exit-code 1 \
--report-format sarif --report-path gitleaks.sarif 2>&1 | tee gitleaks.log
# A green check is not evidence that anything was looked at. Read the number
# of commits gitleaks reported and fail if it does not match the repository's
# history. Measured range across 10 VEAI repos (2026-09-17): 82-181% of HEAD.
# The broken state was 0.45% (1 of 222), so a 50% floor catches it.
- name: Verify the scan actually covered the history
if: always()
run: |
set -euo pipefail
expected=$(git rev-list --count HEAD)
scanned=$(grep -oE '[0-9]+ commits scanned' gitleaks.log | grep -oE '^[0-9]+' | head -1 || echo 0)
floor=$(( expected / 2 ))
[ "$floor" -lt 2 ] && floor=2
echo "history=${expected} scanned=${scanned} floor=${floor}"
if [ "$scanned" -lt "$floor" ]; then
echo "::error::Did not scan the whole history (${scanned} of ${expected} commits). The job is not doing what its name says."
exit 1
fi
echo "Scanned the whole history (${scanned} commits)."
dependency-audit:
name: Node.js dependency audit
runs-on: ubuntu-latest
steps:
- name: Check out repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- name: Set up Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
with:
node-version: 24
cache: npm
- name: Audit dependencies
run: npm audit --audit-level=high