Merge pull request #17 from aehrc/fix/guard-scaled-readonly-override #51
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Secret Scan | |
| # Blocks a push or PR that introduces a secret. This complements GitHub's own secret scanning | |
| # and push protection (both enabled on this repo): GitHub catches recognised provider tokens, | |
| # gitleaks catches the generic shapes — private keys, connection strings, high-entropy blobs. | |
| # | |
| # ⚠️ These jobs scan only the commits being ADDED, never full history, and that is deliberate. | |
| # `azure/install_cert_on_appgw/test-cert.key` is a real (but self-signed, February-2023-expired) | |
| # private key committed in 2022 and knowingly left unsuppressed in .gitleaksignore. A blocking | |
| # full-history scan would therefore fail on every run forever and teach everyone to ignore the | |
| # job. Scanning the incoming range keeps the gate meaningful: it fails only on something new. | |
| # | |
| # The scheduled job below does scan everything, non-blocking, to surface drift. | |
| on: | |
| push: | |
| branches: ['**'] | |
| pull_request: | |
| branches: [master] | |
| schedule: | |
| # Weekly, Mondays 02:00 UTC — informational full-history sweep. | |
| - cron: '0 2 * * 1' | |
| workflow_dispatch: | |
| permissions: | |
| contents: read | |
| env: | |
| # Pinned: a new gitleaks release can add rules, and an unpinned scanner turns an unrelated | |
| # push into a red build. Bump deliberately, re-triaging .gitleaksignore at the same time. | |
| GITLEAKS_VERSION: 8.30.0 | |
| GITLEAKS_SHA256: 79a3ab579b53f71efd634f3aaf7e04a0fa0cf206b7ed434638d1547a2470a66e | |
| jobs: | |
| scan-new-commits: | |
| name: Scan incoming commits | |
| if: github.event_name == 'push' || github.event_name == 'pull_request' | |
| runs-on: ubuntu-latest | |
| steps: | |
| # fetch-depth: 0 — a shallow clone has no merge base, so the commit range cannot be resolved. | |
| - uses: actions/checkout@v5 | |
| with: | |
| fetch-depth: 0 | |
| # The OSS CLI is installed directly rather than via gitleaks/gitleaks-action, which | |
| # requires a paid GITLEAKS_LICENSE for organisation-owned repositories. aehrc is an | |
| # organisation, so the action would fail here; the CLI itself has no such restriction. | |
| - name: Install gitleaks | |
| run: | | |
| set -euo pipefail | |
| url="https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" | |
| curl -sSfL "$url" -o gitleaks.tar.gz | |
| echo "${GITLEAKS_SHA256} gitleaks.tar.gz" | sha256sum -c - | |
| tar -xzf gitleaks.tar.gz gitleaks | |
| sudo install -m 0755 gitleaks /usr/local/bin/gitleaks | |
| gitleaks version | |
| - name: Resolve the commit range | |
| id: range | |
| env: | |
| EVENT: ${{ github.event_name }} | |
| BASE_SHA: ${{ github.event.pull_request.base.sha }} | |
| BEFORE: ${{ github.event.before }} | |
| SHA: ${{ github.sha }} | |
| run: | | |
| set -euo pipefail | |
| if [ "$EVENT" = "pull_request" ]; then | |
| # Compare against the merge base, not the base tip, or commits already on master | |
| # get rescanned and an unrelated finding fails someone else's PR. | |
| base="$(git merge-base "$BASE_SHA" "$SHA")" | |
| range="${base}..${SHA}" | |
| elif [ -n "${BEFORE:-}" ] && [ "$BEFORE" != "0000000000000000000000000000000000000000" ] \ | |
| && git cat-file -e "${BEFORE}^{commit}" 2>/dev/null; then | |
| range="${BEFORE}..${SHA}" | |
| elif git cat-file -e "origin/master^{commit}" 2>/dev/null \ | |
| && [ -n "$(git merge-base origin/master "$SHA" 2>/dev/null)" ]; then | |
| # `before` is all-zeros on a newly created branch and names a discarded commit after a | |
| # force-push; neither can be diffed. Diverging from master covers every commit the | |
| # branch actually adds, which a single-commit fallback would miss. | |
| base="$(git merge-base origin/master "$SHA")" | |
| echo "No usable 'before' ref; diffing against master's merge base instead." | |
| range="${base}..${SHA}" | |
| else | |
| # Last resort. `-1 <sha>` limits git log to that one commit — a bare "$SHA" would | |
| # mean every ancestor, i.e. the whole history, which is exactly what must not happen. | |
| echo "No merge base available; scanning this commit only." | |
| range="-1 ${SHA}" | |
| fi | |
| echo "range=$range" >> "$GITHUB_OUTPUT" | |
| echo "Scanning range: $range" | |
| git rev-list --count "$range" 2>/dev/null | sed 's/^/commits in range: /' || true | |
| - name: gitleaks | |
| run: | | |
| set -euo pipefail | |
| # --redact so a leaked value is never echoed into a public build log. | |
| gitleaks git . \ | |
| --no-banner \ | |
| --redact \ | |
| --report-format sarif \ | |
| --report-path gitleaks.sarif \ | |
| --log-opts "${{ steps.range.outputs.range }}" | |
| - name: Explain a failure | |
| if: failure() | |
| run: | | |
| { | |
| echo "### Secret scan failed" | |
| echo | |
| echo "gitleaks found a potential secret in the commits added by this push or PR." | |
| echo | |
| echo "1. **Treat the value as compromised and rotate it** — rewriting the commit does" | |
| echo " not help once it has been pushed to a public repository." | |
| echo "2. Remove it from the code and use a GitHub Actions secret, an ExternalSecret," | |
| echo " or the gitignored \`.env\` for local-only credentials." | |
| echo "3. If it is a **false positive**, add its fingerprint to \`.gitleaksignore\`" | |
| echo " with a comment explaining why. Reproduce locally with:" | |
| echo ' ```' | |
| echo " gitleaks git . --no-banner --redact --log-opts \"${{ steps.range.outputs.range }}\"" | |
| echo ' ```' | |
| echo | |
| echo "Note that fingerprints embed line numbers, so an existing entry can go stale" | |
| echo "when a file is edited. Re-triage rather than blindly re-adding." | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| - uses: actions/upload-artifact@v4 | |
| if: always() && hashFiles('gitleaks.sarif') != '' | |
| with: | |
| name: gitleaks-sarif | |
| path: gitleaks.sarif | |
| retention-days: 14 | |
| full-history: | |
| name: Full history sweep (informational) | |
| if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v5 | |
| with: | |
| fetch-depth: 0 | |
| - name: Install gitleaks | |
| run: | | |
| set -euo pipefail | |
| url="https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" | |
| curl -sSfL "$url" -o gitleaks.tar.gz | |
| echo "${GITLEAKS_SHA256} gitleaks.tar.gz" | sha256sum -c - | |
| tar -xzf gitleaks.tar.gz gitleaks | |
| sudo install -m 0755 gitleaks /usr/local/bin/gitleaks | |
| # Not a gate: this is EXPECTED to report the known expired test cert. It exists so that a | |
| # change in the count is visible. Compare against the documented baseline below. | |
| - name: Scan every branch and tag | |
| run: | | |
| set -uo pipefail | |
| git fetch --all --tags --quiet || true | |
| gitleaks git . --no-banner --redact \ | |
| --report-format json --report-path full.json --log-opts "--all" || true | |
| count="$(python3 -c 'import json;print(len(json.load(open("full.json"))))' 2>/dev/null || echo 0)" | |
| # Baseline triaged 2026-08-12: 1 finding — azure/install_cert_on_appgw/test-cert.key, | |
| # a self-signed private key expired Feb 2023, knowingly left unsuppressed. | |
| baseline=1 | |
| # Built with a one-liner rather than a heredoc: a heredoc terminator has to sit at | |
| # column 0, which would close this YAML block scalar and break the workflow file. | |
| python3 -c 'import json;d=json.load(open("full.json"));print("| Rule | File |\n|---|---|") or [print("| `%s` | `%s` |" % (f.get("RuleID","?"),f.get("File","?"))) for f in d] if d else None' > table.md 2>/dev/null || : > table.md | |
| { | |
| echo "### Full-history secret sweep" | |
| echo | |
| echo "| | |" | |
| echo "|---|---|" | |
| echo "| Findings | ${count} |" | |
| echo "| Triaged baseline | ${baseline} |" | |
| echo | |
| if [ "$count" -gt "$baseline" ]; then | |
| echo "⚠️ **Above baseline.** Something new is present in history, or a" | |
| echo ".gitleaksignore fingerprint went stale after a file edit. Investigate:" | |
| echo '```' | |
| echo 'gitleaks git . --no-banner --redact --log-opts "--all"' | |
| echo '```' | |
| elif [ "$count" -lt "$baseline" ]; then | |
| echo "✅ **Below baseline** — a known finding was resolved. Update \`baseline\`" | |
| echo "in this workflow and the note in \`.gitleaksignore\`." | |
| else | |
| echo "✅ Matches the triaged baseline; no drift." | |
| fi | |
| echo | |
| cat table.md | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| - uses: actions/upload-artifact@v4 | |
| if: always() && hashFiles('full.json') != '' | |
| with: | |
| name: gitleaks-full-history | |
| path: full.json | |
| retention-days: 30 |