From 9d583c3dedcb5a8e91c083515790b42dffb8b40d Mon Sep 17 00:00:00 2001 From: Imani Manyara <19682005+imanimanyara@users.noreply.github.com> Date: Fri, 2 Oct 2026 06:39:03 -0400 Subject: [PATCH 1/3] Pin third-party release actions and limit token scope Code scanning flagged pypa/gh-action-pypi-publish and softprops/action-gh-release as unpinned, and ci.yml as granting the default token scope. Third-party actions are pinned to the commit SHA of their latest release (v1.14.2 and v3.0.3) per the org rule; ci.yml and release.yml now default to contents: read, with the publish job keeping its own id-token/contents write grant. --- .github/workflows/ci.yml | 3 +++ .github/workflows/release.yml | 7 +++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 49e2d61..a3ea9df 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,6 +5,9 @@ on: branches: [main] workflow_dispatch: +permissions: + contents: read + jobs: test: name: pytest + ruff + mypy diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c65db8c..529b9cb 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -5,6 +5,9 @@ on: tags: - "v*.*.*" +permissions: + contents: read + jobs: publish: name: build + publish wheel + bundle @@ -32,7 +35,7 @@ jobs: rm -rf dist/__pycache__ - name: Upload to PyPI (only whl + sdist) - uses: pypa/gh-action-pypi-publish@release/v1 + uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2 # At this point dist/ contains exactly *.whl + *.tar.gz. The # bundle + checksum file are written to bundle/ in the next # step so twine doesn't try to upload them. @@ -44,7 +47,7 @@ jobs: > bundle/SHA256SUMS - name: Attach bundle + checksums to GitHub Release - uses: softprops/action-gh-release@v3 + uses: softprops/action-gh-release@efb35369e0ad2afab669f228072c1b0d510eae64 # v3.0.3 with: files: | bundle/installer.py From f25396611843975ac7014f987ef1fc09af21883d Mon Sep 17 00:00:00 2001 From: Imani Manyara <19682005+imanimanyara@users.noreply.github.com> Date: Fri, 2 Oct 2026 06:39:03 -0400 Subject: [PATCH 2/3] Resolve code-scanning findings in launcher test and CLI The bootstrap launcher test matched URL hosts as substrings of the output, which CodeQL reports as incomplete URL sanitisation. It now extracts the printed URLs and compares parsed hostnames exactly. The intentional swallow of ConfigError when pre-loading the local registry gains a comment explaining why; behaviour is unchanged. --- src/get_installer/__main__.py | 4 ++++ tests/test_bootstrap_launchers.py | 17 +++++++++++++---- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/src/get_installer/__main__.py b/src/get_installer/__main__.py index f90db96..759ca63 100644 --- a/src/get_installer/__main__.py +++ b/src/get_installer/__main__.py @@ -194,6 +194,10 @@ def main(argv: list[str] | None = None) -> int: pre = Registry.load(fallback) allowed_origins = pre.access_control.allowed_origins except ConfigError: + # Deliberate: an unreadable local registry is treated + # like a missing one -- no allowed_origins pre-load. The + # URL fetch still runs; from_url falls back to this path + # only on fetch failure and raises if it is unusable too. pass registry = Registry.from_url( registry_arg, diff --git a/tests/test_bootstrap_launchers.py b/tests/test_bootstrap_launchers.py index ba6386a..5dbe7b0 100644 --- a/tests/test_bootstrap_launchers.py +++ b/tests/test_bootstrap_launchers.py @@ -14,12 +14,14 @@ import http.server import os +import re import shutil import socketserver import subprocess import sys import threading from pathlib import Path +from urllib.parse import urlsplit import pytest @@ -207,10 +209,17 @@ def test_install_sh_lists_install_urls_when_no_python(tmp_path: Path) -> None: ) assert r.returncode != 0 combined = (r.stdout + r.stderr).lower() - # All three escape hatches must be named with their URL host - assert "docs.astral.sh/uv" in combined - assert "pipx.pypa.io" in combined - assert "python.org" in combined + # All three escape hatches must be named with their URL host. Parse + # the printed URLs and compare hosts exactly, rather than matching a + # substring that could sit anywhere in a URL. + urls = [urlsplit(u) for u in re.findall(r"https?://\S+", combined)] + hosts = {u.hostname for u in urls} + assert any( + u.hostname == "docs.astral.sh" and u.path.startswith("/uv") + for u in urls + ) + assert "pipx.pypa.io" in hosts + assert "www.python.org" in hosts assert "--bootstrap-uv" in combined From 3d8d022b8ac0e1264208a466a45beb5825b3cb15 Mon Sep 17 00:00:00 2001 From: Imani Manyara <19682005+imanimanyara@users.noreply.github.com> Date: Fri, 2 Oct 2026 06:44:27 -0400 Subject: [PATCH 3/3] Compare launcher URL hosts by equality CodeQL still reads a membership test of a hostname literal as substring sanitisation, even against a set of parsed hosts. Equality on each parsed hostname states the intent unambiguously. --- tests/test_bootstrap_launchers.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/test_bootstrap_launchers.py b/tests/test_bootstrap_launchers.py index 5dbe7b0..bb6a6ad 100644 --- a/tests/test_bootstrap_launchers.py +++ b/tests/test_bootstrap_launchers.py @@ -213,13 +213,12 @@ def test_install_sh_lists_install_urls_when_no_python(tmp_path: Path) -> None: # the printed URLs and compare hosts exactly, rather than matching a # substring that could sit anywhere in a URL. urls = [urlsplit(u) for u in re.findall(r"https?://\S+", combined)] - hosts = {u.hostname for u in urls} assert any( u.hostname == "docs.astral.sh" and u.path.startswith("/uv") for u in urls ) - assert "pipx.pypa.io" in hosts - assert "www.python.org" in hosts + assert any(u.hostname == "pipx.pypa.io" for u in urls) + assert any(u.hostname == "www.python.org" for u in urls) assert "--bootstrap-uv" in combined