ci(security): drop the modules-check job, it re-derives ci.yml's answ… #1095
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: Security | |
| on: | |
| push: | |
| branches: [main] | |
| pull_request: | |
| branches: [main] | |
| schedule: | |
| # Weekly scan every Monday at 02:00 UTC | |
| - cron: "0 2 * * 1" | |
| # Supersede in-flight runs for the same PR; see .claude/CLAUDE.md "Superseding CI runs". | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }} | |
| cancel-in-progress: true | |
| permissions: | |
| contents: read | |
| jobs: | |
| # ── Static Application Security Testing (SAST) ─────────────────────────────── | |
| # Analyses source code for security vulnerabilities — injection flaws, insecure API usage, | |
| # hardcoded secrets — from the source rather than from a resolved dependency set. Two engines run | |
| # here: Semgrep below, and CodeQL after it. Semgrep uses per-language rule packs, passed via | |
| # SEMGREP_RULES; add one per adopted language. The deprecated semgrep/semgrep-action is | |
| # replaced by the upstream-recommended semgrep/semgrep container image running `semgrep ci`, so | |
| # the tool version is SHA-pinned like everything else (the action froze at v0.58.0 — see README's | |
| # "Dependency updates"). | |
| # TEND(lang-expand): add p/<language> to SEMGREP_RULES for each adopted language (semgrep.dev/r | |
| # lists available packs, e.g. p/java, p/typescript, p/rust). | |
| semgrep: | |
| name: Semgrep | |
| runs-on: ubuntu-latest | |
| container: | |
| image: semgrep/semgrep:1.174.0@sha256:f1f7b71861c7b28b6e0f661225a2c4f58a484f5d0f182465c6d6b3b22f972ade | |
| permissions: | |
| contents: read | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| - run: semgrep ci | |
| env: | |
| SEMGREP_RULES: p/golang p/python p/secrets | |
| # CodeQL, the second SAST engine and the one code scanning's own UI reads. Advanced setup on | |
| # purpose — see .claude/CLAUDE.md "CodeQL runs as advanced setup" for what GitHub's managed | |
| # default setup could not do, and for the repo setting that has to stay off for this to run. | |
| codeql: | |
| name: CodeQL Analysis (${{ matrix.language }}) | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| # Uploads the SARIF. The starter workflow's `actions: read` (private repos) and | |
| # `packages: read` (private CodeQL packs) buy nothing here; this repo is public and | |
| # queries come from the default suite. | |
| security-events: write | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| # TEND(lang-expand): one entry per adopted language. `none` extracts without building and | |
| # is what interpreted languages want; a compiled language has no such option and needs its | |
| # toolchain installed below, the way Go does. | |
| include: | |
| - language: actions | |
| build-mode: none | |
| - language: go | |
| build-mode: autobuild | |
| - language: python | |
| build-mode: none | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| # Ahead of init on purpose: the Go autobuilder runs under GOTOOLCHAIN=local, so whatever | |
| # is on PATH when extraction starts is the only toolchain it can use. | |
| # //meta/scripts:test_codeql_toolchain keeps this step, and its position, from being lost. | |
| - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 | |
| if: matrix.language == 'go' | |
| with: | |
| go-version-file: go.work | |
| # Globbed, not listed: this job has no module matrix to key off, and a hand-listed | |
| # module would go stale the day a second one lands. | |
| cache-dependency-path: | | |
| go.work.sum | |
| **/go.sum | |
| - uses: github/codeql-action/init@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4.37.8 | |
| with: | |
| languages: ${{ matrix.language }} | |
| # No separate autobuild step: that action documents itself as superseded by this input. | |
| build-mode: ${{ matrix.build-mode }} | |
| - uses: github/codeql-action/analyze@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4.37.8 | |
| with: | |
| # The string default setup uploaded under; it is what separates one language's | |
| # results from another's within a tool. | |
| category: "/language:${{ matrix.language }}" | |
| # Fan-in for the ruleset; the pattern's reasoning is at devcontainer.yml's `base-image-all`. | |
| # Strict on `skipped` unlike that one, since nothing gates this matrix. `if: always()` is | |
| # load-bearing: without it a failed matrix skips this job, and a skipped check counts as | |
| # passing. //meta/scripts:test_codeql_toolchain keeps both. | |
| codeql-all: | |
| name: CodeQL Analysis (all languages) | |
| runs-on: ubuntu-latest | |
| needs: [codeql] | |
| if: always() | |
| steps: | |
| - name: Verify all codeql jobs passed | |
| run: | | |
| if [ "${{ needs.codeql.result }}" != "success" ]; then | |
| echo "codeql result: ${{ needs.codeql.result }}" | |
| exit 1 | |
| fi | |
| echo "All codeql jobs passed" | |
| # ── Dependency CVE Scanning ─────────────────────────────────────────────────── | |
| # Per-language scanners that flag known-vulnerable third-party dependencies. The signal model | |
| # differs by language — govulncheck does call-graph reachability (only fires when our code | |
| # reaches the vulnerable symbol); pip-audit is manifest-based (fires on any flagged version in | |
| # the resolved set). Both run once per resolution unit: govulncheck per Go module, pip-audit | |
| # once over the workspace-wide uv resolution — but only because its export asks for the whole | |
| # workspace; see the flags on the export step, which are what make "every member" true. | |
| # TEND(lang-expand): add an equivalent per-language scanner as languages are adopted | |
| # (e.g. cargo-audit for Rust, trivy fs --scanners vuln for languages without a dedicated | |
| # reachability tool). | |
| # The matrix below is hand-listed; what holds it to the discovered set is ci.yml's | |
| # `modules-check`, not a gate here — see .claude/CLAUDE.md "Which workflow a check belongs in". | |
| govulncheck: | |
| name: govulncheck (${{ matrix.go_module }}) | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| go_module: | |
| - tools/network_infrastructure_maintenance | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| # The setup/install/run trio below is what golang/govulncheck-action wraps. We inline it: | |
| # the action is a thin composite, and its own pinned actions/checkout + actions/setup-go | |
| # trail current releases far enough to emit Node runtime deprecation warnings in every | |
| # consuming workflow. If this ever outgrows install-and-run, check whether that action has | |
| # started keeping its dependencies current and switch back. | |
| - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 | |
| with: | |
| # go.work pins the toolchain so a workspace-level `go X.Y.Z` directive doesn't trip | |
| # GOTOOLCHAIN=local. | |
| go-version-file: go.work | |
| cache-dependency-path: | | |
| go.work.sum | |
| ${{ matrix.go_module }}/go.sum | |
| - run: go install golang.org/x/vuln/cmd/govulncheck@latest | |
| - run: govulncheck -C ${{ matrix.go_module }} ./... | |
| govulncheck-all: | |
| name: govulncheck (all modules) | |
| runs-on: ubuntu-latest | |
| needs: [govulncheck] | |
| if: always() | |
| steps: | |
| - name: Verify all govulncheck jobs passed | |
| run: | | |
| if [ "${{ needs.govulncheck.result }}" != "success" ]; then | |
| echo "govulncheck result: ${{ needs.govulncheck.result }}" | |
| exit 1 | |
| fi | |
| echo "All govulncheck jobs passed" | |
| # Python sibling of govulncheck. pip-audit reads requirements.txt format; we feed it a | |
| # fresh `uv export` of uv.lock (the source of truth) into a tempfile rather than the | |
| # committed requirements_lock.txt, so this scan is decoupled from whether the derived | |
| # file is currently in sync — pre-commit and the Renovate auto-commit workflow each | |
| # police that on their own paths. | |
| # | |
| # What this job actually covers is decided by the export flags below, not by any check in | |
| # ci.yml — see the note on that step. The ci.yml-side clause | |
| # (`check_python_workspace_members`) is the weaker, second condition: a per-project | |
| # pyproject.toml matching no glob in `[tool.uv.workspace].members` never reaches uv.lock at | |
| # all, so no export flag could recover it. | |
| pip-audit: | |
| name: pip-audit | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 | |
| - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 | |
| with: | |
| # renovate: datasource=python-version depName=python | |
| python-version: "3.14" | |
| # `--all-packages` is load-bearing: without it `uv export` emits the *root* project's | |
| # dependencies only, so a workspace member that is registered, locked, and passing every | |
| # ci.yml check still has none of its dependencies scanned — a green job that looked at | |
| # nothing. Latent while `members` is empty, which is exactly how it would ship unnoticed. | |
| # `--no-emit-workspace` supersedes `--no-emit-project`: it drops every member rather than | |
| # just the root, so first-party packages stay out of the audit as `--all-packages` pulls | |
| # them in. //meta/scripts:test_pip_audit_coverage holds both flags here. | |
| # These deliberately differ from the `requirements_lock.txt` trio (pre-commit hook, ratify | |
| # script, freshness check), which must stay identical to *each other*; don't harmonise the | |
| # two sets — see TestUvExport in meta/scripts/test_check_modules.py. | |
| - name: Export uv.lock to requirements.txt | |
| run: | | |
| uv export --format requirements-txt --all-packages --no-emit-workspace \ | |
| --output-file "${RUNNER_TEMP}/requirements.txt" | |
| - name: pip-audit | |
| env: | |
| # renovate: datasource=pypi depName=pip-audit | |
| PIP_AUDIT_VERSION: "2.10.1" | |
| # `--require-hashes` makes pip-audit trust the file as the resolution (implies | |
| # `--no-deps`) AND fail if any line lacks a hash — a regression check against an | |
| # un-hashed export sneaking back in. `--strict` promotes any remaining warning | |
| # class to a failure. | |
| run: uvx "pip-audit@${PIP_AUDIT_VERSION}" --requirement "${RUNNER_TEMP}/requirements.txt" --require-hashes --strict | |
| # ── Supply Chain and Filesystem Scanning ───────────────────────────────────── | |
| # Scans the entire repository for secrets committed to source, dependency CVEs across all | |
| # ecosystems, and license compliance. Language-agnostic — Trivy covers new languages automatically | |
| # as they are added. | |
| trivy: | |
| name: Trivy | |
| runs-on: ubuntu-latest | |
| permissions: | |
| security-events: write | |
| contents: read | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| - uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0 | |
| with: | |
| scan-type: fs | |
| scan-ref: . | |
| format: sarif | |
| output: trivy.sarif | |
| - uses: github/codeql-action/upload-sarif@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4.37.8 | |
| if: always() | |
| with: | |
| sarif_file: trivy.sarif | |
| category: trivy |