Skip to content

Commit 603a42e

Browse files
committed
ci: harden workflows with least-privilege permissions, CodeQL, and actionlint
Tighten the CI/CD supply chain to close OpenSSF Scorecard gaps: - Set a least-privilege top-level `permissions: contents: read` on the backport-fixes, ci, cut-release, dependency-review, publish-pypi, and release workflows; jobs re-grant only the write scopes they need. - Add a CodeQL workflow running the security-extended query suite on the Python sources (SAST) for pushes, pull requests, and a weekly schedule. The analyze job is gated on public-repo visibility, since SARIF upload to code scanning requires public repos. - Add an actionlint workflow that lints `.github/workflows/**` at PR time. The actionlint installer is fetched by commit SHA and sha256-verified before it runs, and shellcheck is wired in explicitly. - Replace the fragile `grep`-based artifact version check in publish-pypi with a `compgen -G` glob match so version verification no longer depends on regex-escaping the version string.
1 parent 45dc5f0 commit 603a42e

8 files changed

Lines changed: 146 additions & 1 deletion

File tree

.github/workflows/backport-fixes.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ concurrency:
3131
group: backport-${{ inputs.fromBranch }}-to-${{ inputs.toBranch }}
3232
cancel-in-progress: false
3333

34+
# Least-privilege default; the job re-grants the write scopes it needs.
35+
permissions:
36+
contents: read
37+
3438
jobs:
3539
backport:
3640
runs-on: ubuntu-latest

.github/workflows/ci.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ on:
66
branches:
77
- main
88

9+
# Least-privilege default; this workflow only reads the repo.
10+
permissions:
11+
contents: read
12+
913
jobs:
1014
quality:
1115
runs-on: ubuntu-latest

.github/workflows/codeql.yml

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
name: CodeQL
2+
3+
# Static application security testing (SAST) for the Python sources across
4+
# all workspace packages. Runs CodeQL's security-extended query suite on
5+
# pull requests, pushes to main, and a weekly schedule, uploading results
6+
# to the GitHub Security tab.
7+
#
8+
# Closes the OpenSSF Scorecard "SAST" gap: CodeQL statically analyzes the
9+
# first-party source, complementing the dependency-vulnerability scanning
10+
# (pip-audit) that runs separately.
11+
12+
on:
13+
push:
14+
branches:
15+
- main
16+
pull_request:
17+
branches:
18+
- main
19+
schedule:
20+
# Wednesdays 06:00 UTC
21+
- cron: "0 6 * * 3"
22+
workflow_dispatch:
23+
24+
permissions:
25+
contents: read
26+
27+
jobs:
28+
analyze:
29+
name: Analyze (python)
30+
# Code scanning (SARIF upload) requires GitHub Code Security, which is
31+
# only available on public repos. Gate on visibility so the job only
32+
# runs when the repo is public.
33+
if: ${{ github.event.repository.visibility == 'public' }}
34+
runs-on: ubuntu-latest
35+
permissions:
36+
# Required for CodeQL to upload its SARIF results to code scanning.
37+
security-events: write
38+
contents: read
39+
actions: read
40+
41+
steps:
42+
- name: Checkout
43+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
44+
45+
# build-mode: none — CodeQL analyzes Python sources directly, no
46+
# compilation step required.
47+
- name: Initialize CodeQL
48+
uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2
49+
with:
50+
languages: python
51+
build-mode: none
52+
queries: security-extended
53+
54+
- name: Perform CodeQL analysis
55+
uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2
56+
with:
57+
category: "/language:python"

.github/workflows/cut-release.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,10 @@ concurrency:
4141
group: cut-release
4242
cancel-in-progress: false
4343

44+
# Least-privilege default; the job re-grants the write scope it needs.
45+
permissions:
46+
contents: read
47+
4448
jobs:
4549
cut:
4650
runs-on: ubuntu-latest

.github/workflows/dependency-review.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ name: Dependency Review
33
on:
44
pull_request:
55

6+
# Least-privilege default; the job re-grants what it needs.
7+
permissions:
8+
contents: read
9+
610
jobs:
711
dependency-review:
812
runs-on: ubuntu-latest

.github/workflows/publish-pypi.yml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ concurrency:
1818
group: publish-pypi-${{ github.ref }}
1919
cancel-in-progress: false
2020

21+
# Least-privilege default; the job re-grants the scopes it needs (OIDC).
22+
permissions:
23+
contents: read
24+
2125
jobs:
2226
publish:
2327
runs-on: ubuntu-latest
@@ -78,7 +82,7 @@ jobs:
7882
run: |
7983
v="${{ steps.version.outputs.version }}"
8084
for dir in dist authplane-mcp/dist authplane-fastmcp/dist; do
81-
if ! ls "$dir" | grep -q "${v//./\\.}"; then
85+
if ! compgen -G "$dir/*$v*" > /dev/null; then
8286
echo "::error::Built artifacts in ${dir} do not contain version ${v}. hatch-vcs likely resolved a different version. Refusing to publish."
8387
ls "$dir"
8488
exit 1

.github/workflows/release.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@ concurrency:
3838
group: release-${{ github.ref }}
3939
cancel-in-progress: false
4040

41+
# Least-privilege default; the job re-grants the write scope it needs.
42+
permissions:
43+
contents: read
44+
4145
jobs:
4246
release:
4347
runs-on: ubuntu-latest
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
name: Lint workflows
2+
3+
# Catches workflow YAML / shell-in-`run:` regressions at PR time so a
4+
# typo can't reach a release tag and surface only when a publish run
5+
# fails. Scoped to changes under `.github/workflows/**` to keep CI
6+
# overhead off unrelated PRs.
7+
8+
on:
9+
pull_request:
10+
paths:
11+
- ".github/workflows/**"
12+
push:
13+
branches:
14+
- main
15+
paths:
16+
- ".github/workflows/**"
17+
18+
permissions:
19+
contents: read
20+
21+
jobs:
22+
actionlint:
23+
runs-on: ubuntu-latest
24+
steps:
25+
- name: Checkout
26+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
27+
28+
# Pulls the matching actionlint binary release from GitHub Releases
29+
# via the upstream download script. The script is fetched by commit
30+
# SHA (not a mutable tag) and sha256-verified before it runs — this
31+
# closes Scorecard's "downloadThenRun not pinned by hash" gap. The
32+
# script then checksum-verifies the actionlint binary it pulls from
33+
# the matching release.
34+
#
35+
# To bump: change ACTIONLINT_VERSION, set ACTIONLINT_SCRIPT_SHA to the
36+
# commit the new tag points at (`gh api repos/rhysd/actionlint/commits/vX.Y.Z -q .sha`),
37+
# and update ACTIONLINT_SCRIPT_SHA256 to that file's sha256.
38+
#
39+
# Install dir is passed explicitly as the script's second positional
40+
# arg so the workflow doesn't couple to the script's internal default
41+
# of $PWD (which happens to be $GITHUB_WORKSPACE after checkout —
42+
# a coincidence, not a contract).
43+
- name: Install actionlint
44+
env:
45+
ACTIONLINT_VERSION: "1.7.7"
46+
ACTIONLINT_SCRIPT_SHA: "03d0035246f3e81f36aed592ffb4bebf33a03106"
47+
ACTIONLINT_SCRIPT_SHA256: "221d1d16c03e4e4fcd867de34104e8d479bdce20ccdfa553b9a5c0dc29bf6af2"
48+
ACTIONLINT_INSTALL_DIR: ${{ runner.temp }}/actionlint
49+
run: |
50+
mkdir -p "${ACTIONLINT_INSTALL_DIR}"
51+
script="${ACTIONLINT_INSTALL_DIR}/download-actionlint.bash"
52+
curl -fsSL -o "${script}" \
53+
"https://raw.githubusercontent.com/rhysd/actionlint/${ACTIONLINT_SCRIPT_SHA}/scripts/download-actionlint.bash"
54+
echo "${ACTIONLINT_SCRIPT_SHA256} ${script}" | sha256sum -c -
55+
bash "${script}" "${ACTIONLINT_VERSION}" "${ACTIONLINT_INSTALL_DIR}"
56+
echo "${ACTIONLINT_INSTALL_DIR}" >> "${GITHUB_PATH}"
57+
"${ACTIONLINT_INSTALL_DIR}/actionlint" -version
58+
59+
# `-shellcheck=shellcheck` makes the shellcheck dependency explicit
60+
# rather than relying on actionlint's implicit lookup against the
61+
# runner image's $PATH; if the Ubuntu image ever drops shellcheck the
62+
# job fails loudly instead of silently degrading.
63+
- name: Run actionlint
64+
run: actionlint -color -shellcheck=shellcheck

0 commit comments

Comments
 (0)