Skip to content

Commit 2be9954

Browse files
authored
security: hash-pin publish.yml TestPyPI installs (#299)
* security: hash-pin publish.yml TestPyPI installs, add smithery.yaml Closes Scorecard Pinned-Dependencies alerts #49 (real-PyPI runtime deps, now pinned via uv export's own hash resolution) and #50 (the mcpg install from TestPyPI, now pinned to the sha256 of the wheel this same workflow run built - TestPyPI serves that identical file back, so this also catches upload-path tampering, not just dependency confusion). Verified locally: a fresh venv installs the full hashed dependency set cleanly via pip's hash-checking mode. Also adds smithery.yaml (verification hash for the Smithery registry listing), previously untracked. Entire-Checkpoint: 30082aaf6e70 * security: harden hash-pin script per review, add explicit CodeQL workflow Sourcery review on #299 flagged two real gaps in the TestPyPI hash-pin step: the wheel glob could silently match >1 file, and the extracted hash wasn't validated before being written into the pin file. Both now fail loudly instead of proceeding on bad input. Also closes Scorecard SAST alert #54: CodeQL was running via GitHub's "default setup", which Scorecard's static SAST check can't see (it only recognizes an explicit codeql-action workflow). Switched to an explicit .github/workflows/codeql.yml (python + actions, matching this repo's actual content) and disabled default setup via the API to avoid the two configurations conflicting. Entire-Checkpoint: 8dc67c1349b0
1 parent 03c7f07 commit 2be9954

3 files changed

Lines changed: 85 additions & 5 deletions

File tree

.github/workflows/codeql.yml

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
name: CodeQL
2+
3+
# Explicit workflow (replaces GitHub's "default setup") for two reasons:
4+
# 1. Scorecard's SAST check only recognizes a workflow file running
5+
# github/codeql-action — "default setup" scans just as thoroughly but
6+
# is invisible to that static check (scorecard alert #54).
7+
# 2. Running on `pull_request` (not just a schedule) gates every PR, not
8+
# just periodic scans of main.
9+
10+
on:
11+
push:
12+
branches: ["main"]
13+
pull_request:
14+
branches: ["main"]
15+
schedule:
16+
- cron: "23 8 * * 3"
17+
18+
permissions:
19+
contents: read
20+
21+
jobs:
22+
analyze:
23+
name: Analyze (${{ matrix.language }})
24+
runs-on: ubuntu-latest
25+
permissions:
26+
# Required to upload SARIF results.
27+
security-events: write
28+
# Required to fetch the source.
29+
contents: read
30+
# Required for the "actions" language (workflow analysis).
31+
actions: read
32+
33+
strategy:
34+
fail-fast: false
35+
matrix:
36+
# Matches this repo's actual content — pure Python plus the
37+
# workflow files themselves. (Default setup had also flagged
38+
# c-cpp from autodetection; there's no C/C++ source in the repo,
39+
# only vendored build artifacts under the gitignored .venv/.)
40+
language: ["python", "actions"]
41+
42+
steps:
43+
- name: Checkout repository
44+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
45+
46+
- name: Initialize CodeQL
47+
uses: github/codeql-action/init@e60ea984bd3baa95954f2856bcf24f9eaba46637 # v3
48+
with:
49+
languages: ${{ matrix.language }}
50+
build-mode: none
51+
52+
- name: Perform CodeQL analysis
53+
uses: github/codeql-action/analyze@e60ea984bd3baa95954f2856bcf24f9eaba46637 # v3
54+
with:
55+
category: "/language:${{ matrix.language }}"

.github/workflows/publish.yml

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -79,14 +79,21 @@ jobs:
7979
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7
8080
with:
8181
python-version: "3.14"
82+
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
83+
with:
84+
name: dist
85+
path: dist/
8286
- name: Install runtime deps from real PyPI (anti-confusion)
8387
# TestPyPI is a public sandbox; combining its index with
8488
# ``--extra-index-url=pypi`` lets an attacker shadow our
8589
# deps with a higher-numbered fake (dependency confusion).
8690
# Step 1 pins real PyPI only, dep list canonically derived
87-
# from pyproject.toml.
91+
# from pyproject.toml. Hashes come from uv's own lockfile
92+
# resolution, so pip refuses to install anything whose
93+
# artifact doesn't match what we resolved (belt-and-braces
94+
# on top of the index pinning above).
8895
run: |
89-
uv export --no-dev --no-emit-project --no-hashes \
96+
uv export --no-dev --no-emit-project \
9097
--format requirements-txt > /tmp/mcpg-deps.txt
9198
python -m pip install -r /tmp/mcpg-deps.txt
9299
- name: Wait for TestPyPI to index the upload
@@ -110,16 +117,32 @@ jobs:
110117
done
111118
echo "::error::TestPyPI simple index never listed mcpg==${VER}"
112119
exit 1
113-
- name: Install mcpg from TestPyPI (no deps)
120+
- name: Install mcpg from TestPyPI (no deps, hash-pinned)
114121
# A short retry on the install itself too: even after the simple
115122
# index origin has the version, a CDN edge cache in front of it can
116-
# lag a few more seconds.
123+
# lag a few more seconds. The hash comes from the wheel this same
124+
# workflow run built and uploaded a few steps ago (downloaded
125+
# above) — TestPyPI serves that identical file back byte-for-byte,
126+
# so pinning to it also catches any upload-path tampering, not
127+
# just dependency confusion.
117128
run: |
118129
VER="${GITHUB_REF_NAME#v}"
130+
mapfile -t WHEELS < <(ls dist/mcpg-"${VER}"-*.whl)
131+
if [ "${#WHEELS[@]}" -ne 1 ]; then
132+
echo "::error::Expected exactly one wheel for mcpg==${VER} in dist/, found ${#WHEELS[@]}: ${WHEELS[*]}"
133+
exit 1
134+
fi
135+
WHEEL="${WHEELS[0]}"
136+
HASH="$(python -m pip hash "$WHEEL" | sed -n 's/^--hash=sha256://p')"
137+
if ! [[ "$HASH" =~ ^[0-9a-f]{64}$ ]]; then
138+
echo "::error::pip hash output for $WHEEL did not look like a sha256 hex digest: '${HASH}'"
139+
exit 1
140+
fi
141+
echo "mcpg==${VER} --hash=sha256:${HASH}" > /tmp/mcpg-pin.txt
119142
for i in 1 2 3; do
120143
if python -m pip install --no-deps \
121144
--index-url https://test.pypi.org/simple/ \
122-
"mcpg==${VER}"; then
145+
-r /tmp/mcpg-pin.txt; then
123146
exit 0
124147
fi
125148
echo "pip install attempt $i/3 failed; retrying in 10s..."

smithery.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
version: 1
2+
verification: "21ea4bfd62e446e674040d0da57bdd3c1cd1dba72dcbe832bb90a55b47d6e11c"

0 commit comments

Comments
 (0)