diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3f91e9f..b996751 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -93,7 +93,7 @@ jobs: with: python-version: "3.14" - name: Install the linter - run: pip install -r requirements-lint.txt + run: pip install --require-hashes -r requirements-lint.txt # The gate. `--select` on the command line replaces the families named in # pyproject.toml and nothing else: the per-file ignores and the target # version still come from there, so the two runs below cannot drift apart @@ -121,7 +121,7 @@ jobs: with: python-version: "3.14" - name: Install the type checker - run: pip install -r requirements-lint.txt + run: pip install --require-hashes -r requirements-lint.txt # No project dependencies installed on purpose: `pydivert` is Windows-only # and `psutil` is not needed to READ this package, and both are ignored by # name in [tool.mypy]. Verified in that exact shape (Linux, neither @@ -145,7 +145,7 @@ jobs: with: python-version: "3.14" - name: Install the scanner - run: pip install -r requirements-lint.txt + run: pip install -r requirements-scan.txt # No --error and no --severity here: the scan reports, and tools/semgrep_gate.py # decides. `--severity` knows INFO/WARNING/ERROR only, while registry rules also # carry HIGH and CRITICAL - measured with a probe rule, `--severity ERROR` @@ -181,7 +181,7 @@ jobs: with: python-version: "3.14" - name: Install the auditor - run: pip install -r requirements-lint.txt + run: pip install -r requirements-scan.txt # The pinned set, installed exactly as the build installs it, into its own # environment - then audited BY PATH. Auditing the requirement files instead # silently skips `packaging` and `setuptools` (measured), and a scanner that @@ -249,7 +249,6 @@ jobs: python-version: "3.14" - name: Install dependencies run: | - python -m pip install --upgrade pip pip install --require-hashes -r requirements.txt pip install -r requirements-dev.txt # A pull request pays for what it touched; the weekly run pays for @@ -320,13 +319,20 @@ jobs: python-version: ${{ matrix.python-version }} cache: pip - name: Install dependencies + # 馃敶 No `pip install --upgrade pip`, here or anywhere else in this repository. + # That line fetched an UNPINNED pip from the index and then used it to check + # the hashes on the next line - the one unverified link in a hash-checked + # chain, and the last place anybody would look. The pip that checks them is + # the one `setup-python` shipped with the interpreter, and that action is + # pinned by SHA. Guarded by tests/test_version_and_release.py. run: | - python -m pip install --upgrade pip pip install --require-hashes -r requirements.txt pip install -r requirements-dev.txt - # diff-cover only: the rest of that file is for the analysis jobs, and - # installing it whole here would drag semgrep onto both matrix legs. - pip install "$(grep -E '^diff-cover==' requirements-lint.txt)" + # The whole lint file rather than diff-cover picked out of it by grep. A + # hash cannot travel through `$(grep ...)`: it hands pip a bare pin, and + # pip stays out of hash-checking mode. semgrep and pip-audit moved to + # requirements-scan.txt, so this no longer drags an engine onto both legs. + pip install --require-hashes -r requirements-lint.txt # ONE run of the whole suite, under coverage. testpaths=["tests"] (pyproject) # means this already includes the suites that used to own separate steps: @@ -504,7 +510,6 @@ jobs: cache: pip - name: Install dependencies run: | - python -m pip install --upgrade pip # One hash-checked resolution over both files: the runtime pins and # the freezer's whole closure. Nothing here may resolve to bytes # that are not written down. diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 095dc30..96e1bc8 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -101,7 +101,6 @@ jobs: - name: Install the test dependencies run: | - python -m pip install --upgrade pip pip install --require-hashes -r requirements.txt pip install -r requirements-dev.txt diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3713ed7..53ae2af 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -8,30 +8,38 @@ on: tags: - "v*" -# The default token only reads the repo; creating a Release and uploading assets -# needs write. Scoped to this workflow, not the whole repository. +# Read-only at the top, and the job below raises exactly what it needs. Same rule as +# ci.yml, pages.yml and scorecard.yml already follow; this file was the last one +# breaking it, and OpenSSF Scorecard named it (Token-Permissions, 0/10, 2026-08-19). +# +# 馃敶 Not cosmetic while there is only one job. A permission written here is granted +# to every job ADDED to this file later - a job whose author has no reason to scroll +# back up, in the one workflow that can publish an asset under this project's name. permissions: - contents: write - # For the SBOM attestation. `id-token` mints the short-lived OIDC token that - # signs it, `attestations` writes the result to the repository's attestation - # store. Both are required by actions/attest and neither grants anything else; - # a release that skipped them would still publish an SBOM, just an unsigned one - # that anybody could swap. - # - # NOT here on purpose: `artifact-metadata: write`, which that action's README - # lists as a third permission. It is needed to create the artifact STORAGE - # RECORD, and `create-storage-record` requires `push-to-registry`, which - # defaults to false and is not used here (read in the action's own action.yml, - # 2026-08-12). Granting a permission we do not need would contradict the line - # above it. - id-token: write - attestations: write + contents: read jobs: release: name: Build and publish the Windows executable runs-on: windows-latest timeout-minutes: 30 + permissions: + # Creating a Release and uploading its assets. The default token only reads. + contents: write + # For the SBOM attestation. `id-token` mints the short-lived OIDC token that + # signs it, `attestations` writes the result to the repository's attestation + # store. Both are required by actions/attest and neither grants anything else; + # a release that skipped them would still publish an SBOM, just an unsigned one + # that anybody could swap. + # + # NOT here on purpose: `artifact-metadata: write`, which that action's README + # lists as a third permission. It is needed to create the artifact STORAGE + # RECORD, and `create-storage-record` requires `push-to-registry`, which + # defaults to false and is not used here (read in the action's own action.yml, + # 2026-08-12). Granting a permission we do not need would contradict the line + # above it. + id-token: write + attestations: write steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -45,7 +53,6 @@ jobs: - name: Install dependencies run: | - python -m pip install --upgrade pip # One hash-checked resolution over both files - the same command the # build job in ci.yml runs, so a release cannot be built from a # different set of bytes than the one CI proved. diff --git a/CHANGELOG.md b/CHANGELOG.md index bffcc86..ce786c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,14 @@ The format follows [Keep a Changelog](https://keepachangelog.com/); versions fol Accents are optional: `opoznienie` finds `Op贸藕nienie`. A setting that lives in the Settings window is named rather than missed. +- **You can now check where a download came from, not just that it is unchanged.** Every + release archive carries a signed build attestation, so one command answers "was this really + built from that source, by that workflow": + `gh attestation verify BeanNetworkTester-v0.5.0-windows-x64.zip -R donislawdev/BeanNetworkTester`. + A checksum proves the file matches the release page. This proves the release page itself came + out of this repository's own workflow, from a specific commit. The same command also verifies + the SBOM shipped beside the archive. + ### Changed - **Traffic in Connections is shown in the unit that fits the number.** A big flow used to read diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fae2578..23fbcb1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -9,7 +9,8 @@ testable on any OS. ```bash pip install --require-hashes -r requirements.txt # runtime, pinned to exact artefacts pip install -r requirements-dev.txt -pip install -r requirements-lint.txt # ruff and mypy (semgrep on Linux and macOS) +pip install --require-hashes -r requirements-lint.txt # ruff, mypy and diff-cover +pip install -r requirements-scan.txt # semgrep (Linux and macOS) and pip-audit python -m pytest tests # full suite - no Windows, no driver, no admin rights python smoke_gui.py # GUI smoke with a fake tkinter ruff check # F and B fail a pull request, S and ASYNC are a report diff --git a/README.md b/README.md index 4c40714..2b2844c 100644 --- a/README.md +++ b/README.md @@ -1178,9 +1178,11 @@ Every job in that workflow, and what a red one means: than run it. **ruff** fails a pull request on a dead-code or bug-shape finding (`F` and `B`) and reports the security family (`S`, `ASYNC`) as annotations that never block. **mypy** type-checks the package. **semgrep** scans with its default registry ruleset and a finding at ERROR, HIGH or -CRITICAL fails the run, while everything below that is printed in full. The three tool versions -are pinned in `requirements-lint.txt`, so a new release of a linter cannot redden a pull request -that changed nothing. +CRITICAL fails the run, while everything below that is printed in full. All three tool versions are +pinned, so a new release of a linter cannot redden a pull request that changed nothing: ruff and +mypy in `requirements-lint.txt`, which CI installs by exact file hashes, and semgrep in +`requirements-scan.txt`, pinned to a version only. Semgrep is the one place where a pin cannot +promise a stable answer anyway, because it downloads its rules when it runs. One step is worth knowing about because no unit test can do its job: a **GUI render check on real Tk** under a virtual screen, at the minimum supported 1366x768, **in every language**. It builds diff --git a/README.pl.md b/README.pl.md index b6b92b5..41758c1 100644 --- a/README.pl.md +++ b/README.pl.md @@ -1034,8 +1034,10 @@ uruchamiaj膮. **ruff** wywraca pull requesta na martwym kodzie i na kszta艂tach a rodzin臋 bezpiecze艅stwa (`S`, `ASYNC`) tylko wypisuje w diffie i nigdy nie blokuje. **mypy** sprawdza typy w pakiecie. **semgrep** skanuje domy艣lnym zestawem regu艂 z rejestru, przy czym znalezisko na poziomie ERROR, HIGH albo CRITICAL wywraca przebieg, a wszystko ni偶ej l膮duje w logu. -Wersje tych trzech narz臋dzi s膮 przypi臋te w `requirements-lint.txt`, wi臋c nowe wydanie lintera nie -zaczerwieni pull requesta, w kt贸rym nic si臋 nie zmieni艂o. +Wersje wszystkich trzech narz臋dzi s膮 przypi臋te, wi臋c nowe wydanie lintera nie zaczerwieni pull +requesta, w kt贸rym nic si臋 nie zmieni艂o: ruff i mypy w `requirements-lint.txt`, kt贸ry CI instaluje +po dok艂adnych skr贸tach plik贸w, a semgrep w `requirements-scan.txt`, przypi臋ty tylko wersj膮. Akurat +przy semgrepie przypi臋cie i tak nie obiecuje sta艂ej odpowiedzi, bo regu艂y pobiera przy starcie. Jeden krok wart jest osobnego zdania, bo 偶aden test jednostkowy go nie zast膮pi: **render GUI na prawdziwym Tk** pod wirtualnym ekranem, w minimalnej wspieranej rozdzielczo艣ci 1366x768 i **w diff --git a/requirements-lint.txt b/requirements-lint.txt index a26a915..1e6516d 100644 --- a/requirements-lint.txt +++ b/requirements-lint.txt @@ -1,4 +1,5 @@ -# Static analysis, CI only. Nothing here is imported by the program. +# Static analysis and coverage tooling, CI only. Nothing here is imported by the +# program, and nothing here reaches a user. # # 馃敶 PINNED, and for a different reason than requirements.txt is pinned. There the # pin says which library a release shipped; here it says which RULES a pull request @@ -8,47 +9,473 @@ # after the same commit built a working exe on CI and a crashing one on a developer # machine. # +# 馃敶 HASHED as well since 2026-08-19, which is why this file holds the whole +# CLOSURE and not just the three tools. pip turns hash-checking on for the entire +# install as soon as one requirement carries a hash, so every package it resolves +# has to be written down here. Install it as +# +# pip install --require-hashes -r requirements-lint.txt +# +# and regenerate the hash blocks with `python tools/pin_hashes.py requirements-lint.txt`. +# +# 馃敶 What is NOT here, and why: semgrep and pip-audit moved to requirements-scan.txt. +# Their closures are tens of packages that move on their own schedule, and freezing +# them by hash buys nothing this pays for - both are scanners that run in a job with +# a read-only token and produce a report, neither can put a byte into a release. The +# split is the honest half of the trade, not an oversight: see that file's own note. +# # Raise these by hand, deliberately, and read the release notes when you do: a new # ruff minor may add rules to a family we select, and that is a decision about what # blocks a pull request, not a routine bump. -ruff==0.16.3 +ruff==0.16.3 \ + --hash=sha256:09571e6d1288ed9be475207a3ac04ada404f1cd898104be0f6ab8d7df438575b \ + --hash=sha256:0c5710e247a58a4521e66e124ba9a74655b414f61ba3a2e9e3811e11098f48f7 \ + --hash=sha256:294b95c4ae0cda9388525c2047778aa758d6b8d4bb876fd4e9eaa3ebc92343eb \ + --hash=sha256:2c18c5a101eb540010638cc1ff3c84944d3adb3df62b8d98ca8f22ba484d3413 \ + --hash=sha256:388cdf2166642bd9b13d52b5932d3170f34f8abed7e8d9a855f1d84b83645a0a \ + --hash=sha256:3d0c7c40c87c2a820509c31ba007968da6e1306468c067b2d82fbfdbcd0e8474 \ + --hash=sha256:8457c44f15033c85ddbb77b15d451df9e24e4bd03b628396dd3610cedc3b8f82 \ + --hash=sha256:9e0b1da805eb043654645d74d5de1e5ce2edc686e40790d2b86f56d71cc06a84 \ + --hash=sha256:9f738c0fdfa8eed0b2ce7fb27ee7258208a92a68d7949e62aa15164bc7b389da \ + --hash=sha256:a2d85c02f9b8e165d85e6779184d38c4132de12603dab59c51c28e22584f9e4d \ + --hash=sha256:a37bdea0bbe21780f590bf437d6412c8c4e1b6cd010f91a65c2c40c5e5f5f870 \ + --hash=sha256:b8ca152da82c1acc1fa8d5874b15951935f0eef46f10e6954c83859011b6178a \ + --hash=sha256:c5536e3acfbf9563085aa2be7b13c629c3077e902afc5b941ac44024dbb9f506 \ + --hash=sha256:e2ed719e14aa64d895c2ee922594a90a43c861a93f0575a95ff8c47cdbd13eb9 \ + --hash=sha256:e76d33a347661a84b5be6d043d0347fdc745dfdcf825a8f4fed64b5e26eebdf2 \ + --hash=sha256:e80a7d69ca2a6d1c4d352ec91458cdca6e56c83cdbcabd93e4abe1e53591d948 \ + --hash=sha256:fb785f0be25abe69d320415cd4f833b59e17ba7613d9ba6a958023b6bceb0a50 \ + --hash=sha256:fe155130631a2471fd2e14a7a664a4dfbd7194b8229c3d7b2a40b21178639081 # Same pin, same reason: a mypy release can start reporting a class of error the # previous one did not, and that is a decision about what blocks a pull request. # Verified 2026-08-19 on Linux WITHOUT pydivert or psutil installed - the shape a # runner has - because both are ignored by name in [tool.mypy] and a missing # module is a different code path from an untyped one. -mypy==2.3.1 - -# Linux and macOS only, deliberately: the Windows wheel installs (57 MB) and then -# `semgrep-core` fails to run a scan at all on this machine - an OCaml backtrace -# on a three-line local rule, measured 2026-08-19. Local scans go through WSL, -# CI runs on ubuntu. The marker keeps `pip install -r` honest on Windows instead -# of downloading an engine that cannot start. -# -# 馃敶 The pin fixes the ENGINE, not the rules: `--config p/default` fetches those -# from the registry at scan time, so a new or re-graded rule can turn a pull -# request red with no commit behind it. That is the trade for not vendoring them, -# and vendoring is not open to us - the Semgrep Rules License permits use for our -# own purposes and forbids redistributing the rules, which is what putting them in -# a public repository would be. -semgrep==1.173.0; sys_platform != "win32" - -# The weekly audit of what we PIN. Not the same question as dependency-review, -# which looks at what a pull request ADDS: this one asks whether an advisory has -# been published since, against versions that have not moved. -# -# 馃敶 Run against an INSTALLED environment (`--path`), never against the -# requirement files. Measured 2026-08-19: `pip-audit -r requirements.txt -r -# requirements-build.txt --require-hashes` audits 7 of the 9 pinned packages and -# says "No known vulnerabilities found" - it silently skips `packaging` and -# `setuptools`. The same set installed into a venv and audited by path comes back -# with 10 (those two, plus pip itself). A scanner that quietly covers less than it -# was asked to is worse than none, because its clean report gets quoted. -pip-audit==2.10.1 +mypy==2.3.1 \ + --hash=sha256:114dff494000f18bd10d5d95d84b8567b26da60279ecbe838131841df20e635d \ + --hash=sha256:18162b128c3f9c703cd35f5537446900b0d21a2549aa7a95d21380d2ef643fb0 \ + --hash=sha256:192abaedf75da1bc0b1cef104927e70ec49c1ef0031cc4825c7ee10a438ed24d \ + --hash=sha256:1c80fbc405ed8020f5ff3802dc18cf060197bcdd3fbdd6a26ef2fd34dfdd5226 \ + --hash=sha256:2166b29228835e1f88ff411e96639e6ca3c7fdde84b62ec211f70f86b4051167 \ + --hash=sha256:2329c0501293d4e1f33bc15d04d6304d65a1cdda967ee93a05c1e681a3923133 \ + --hash=sha256:2a0ba2e57847849fb0d1fcdabb32786d223095ed8bc121dfe322bcdb3d9c46bc \ + --hash=sha256:30c0477d4aab7b7f39c8397dc877f2c96b9fe5588ec379f372c56eb63d599f63 \ + --hash=sha256:375d7013876a8233b2d05be185bfa09f689696cd999ce8b1cfe6acac5c80e8a3 \ + --hash=sha256:3c80cd23d85368bdd9f37d5231dfd97d35bcbf5bf41af96ef3a9b078ad1957f9 \ + --hash=sha256:3f7e865dd51f235f60a2dbcd8728a1c095f5ca28f095d48a725b84cd935735c4 \ + --hash=sha256:47a8a7a0a7f6f6e63995c0ac36fa0c07b127413fdc81f0439b7f3dccafd33561 \ + --hash=sha256:47c1b1207258513a9d93495f69c8be9de73916186f0e52703e8c461b7a623419 \ + --hash=sha256:4956f34d145e145562a0a0bf367f642bbc85c04ec2baf47ae015947c3169a85d \ + --hash=sha256:4c5095a327483591c94e0c8d3ef9e50d4ab1369b541eae007c1f23bc2a41f6bb \ + --hash=sha256:5159ae60f5dbc3a498af5ba8365505808ac8031bc63f9e00304ad545d40bdd9b \ + --hash=sha256:52eaf3a155f35cf80b40220288c861eb45f14a2340c1f6cbfbdb0feff32879d1 \ + --hash=sha256:57a936373fc690c43a8cd7e7e12a35148e4ec5aa7698ad7fc0a9f918bdc5be41 \ + --hash=sha256:586b3612214cceabb3c0f588c97e7d1e535393f06a60e912e994f6b3ace97523 \ + --hash=sha256:667196b352f4cf304ded4c10f90cfc179263a1acfb3cdcfa984bdfd340d498bc \ + --hash=sha256:6941ab3619377bc3f32ca02876b07d27f216f5201604b664d3937ea0fdd23bb4 \ + --hash=sha256:6ed5c7e3419083268e5c9258bd1c1ef91af44a9e89374dbcaf37b775716e72eb \ + --hash=sha256:6f041a6de52c9217ca125e78ba0a335cb7fd98a1c0580978e49ab2b126f70b57 \ + --hash=sha256:71af9c8a894e862b58e92abb08e53b05a384a1e5e5d6dc7cda59126211a53d82 \ + --hash=sha256:77ad9529e67dca28e511f5cd5671436584ce91f6d3bac159a353158187b986ac \ + --hash=sha256:83d36c2924df7426333abe7faf4724a7e1aab0d9fd41625e81b4683034b80c13 \ + --hash=sha256:84081f538ce27375045c02e3d7f81bd11d853400621ae245d87ce7b6c420ec74 \ + --hash=sha256:851833db876e7b650f93719c74b7879a08e338979c96054fdfc3bfd90a486355 \ + --hash=sha256:858fc57d3d91fa728e33e7ad71def60fc6272694607b306cd3292db53ae39080 \ + --hash=sha256:8ad80807dc3ab8ea978b1b2b6e4a657194ace1d4ef03e0e731aff1abd517da29 \ + --hash=sha256:8e036f06b41630f4c8a1d48f9ac6aa26acc65f8be089973f5519da643318f03f \ + --hash=sha256:94f04929f1c44c35fb0061e912087edaf504acede963a4a7d00680bd089d8531 \ + --hash=sha256:9b4eacbee8a69836c06eff6d0dd4e134a07c2b047755b30c08625fe214f322c6 \ + --hash=sha256:a32bbbb940af990d3be0b8af321c7b6815bb1b3b48142fe7459b9cc5f58959ff \ + --hash=sha256:b091a455111214cb5c9d54a57b9618e9a49f9fe2a42e4e1ac86e9d104ed96ce8 \ + --hash=sha256:b9c53e395c12cad2c6d4b67d5da7c6057638a132d85c08b73646b18f802a0045 \ + --hash=sha256:bb26deed807bdb0457cf3e3f1cd7c4a1cf9d66864eaf1b4a61e06805d4c6b1f9 \ + --hash=sha256:bbfe022634a2a195406bd469e888d2eaf193b02ba7e607391cd7640374aaae3b \ + --hash=sha256:bf678dffd16efcda2c15cbd30e9ecc0081388e29ea23687a88e686ed92638dc3 \ + --hash=sha256:c8637731bb5eee3671eb2c3200827aa3564ed8a9309ecee4d1afe77e6d031bdb \ + --hash=sha256:cfb12e360242d23d91f5e978d94f58ea66acf5804c4fb6f2f794a20d4cb1b595 \ + --hash=sha256:d00d769056bde2f4e69c175071eba45cfb44fa1ed92bdfbfe64a93e0543b0cf0 \ + --hash=sha256:df12e20c9efd614738c71b390007ecd0181125afc4ccafca04d78a1d2eed2c01 \ + --hash=sha256:e099200a1b1b1223a4951f0a90cbff1b8c91b250ba599dab1f7217a628144d90 \ + --hash=sha256:e598c8c66401d26b150872154a286e6d484cf2789c3bb28a7556806298423021 \ + --hash=sha256:e5f1c50bb05b64e2026b52867e8d21106f01313c744a2c4ecc34c90d12e8d6e2 \ + --hash=sha256:e9144ac16fde007096f9563eb2041b4433c2d705c4218edeb79e7e9d01035ee6 \ + --hash=sha256:eda22fd4efa9dcd39331d1dede9b5b8b8a7fd69af07592e778433da98610d29e \ + --hash=sha256:ef0c6335cda9d807f8193d8ff6204a72bc909fa9882aacbca14f43cdb7188306 \ + --hash=sha256:f12fdb70459d0060dea40b29e52163a961b156106d68d57882a6a9f648983a53 \ + --hash=sha256:f5d716048611e85ca9eefb2e1baa5d73ede389b5820ded260ea27c757d667af8 \ + --hash=sha256:ff715e45b2231a8e85de1d163d1b42791e4d7aab8f5145f85fee1b710b735aff # Coverage of the CHANGE, on top of the whole-repository gate in pyproject. The # average moves so slowly at this size that a pull request can add a hundred # untested lines without pushing it below the threshold - which is precisely the # code most worth a test, because it is the code nobody has run twice yet. -diff-cover==10.5.1 +# +# Installed by the `tests` job as part of this whole file rather than picked out of +# it by name. The old shape was `pip install "$(grep -E '^diff-cover==' ...)"`, which +# a hash cannot travel through: grep hands pip a bare `name==version` and pip stays +# out of hash-checking mode. Two extra wheels on each matrix leg is the price, and +# the pip cache pays most of it. +diff-cover==10.5.1 \ + --hash=sha256:67ce07494e605b5d7d7b04aeec2cf524950ed0c12f026dd176883cf33c74fba0 \ + --hash=sha256:f1c9417c62111e40a81c482c692c5cdd131ff93317baa993044cb291912ebba3 + +# The closure underneath those three, resolved with pip for Python 3.14 and +# measured identical on manylinux2014_x86_64 and win_amd64 (2026-08-19), which is +# why no line here needs an environment marker. These are not choices - they are +# what the three tools above pull in, written down so hash-checking has an answer +# for every package it meets. Regenerate the whole block after any bump above: +# +# pip install --dry-run --report - --only-binary=:all: --python-version 3.14 \ +# --platform manylinux2014_x86_64 --target /tmp/x -r requirements-lint.txt +ast_serialize==0.8.0 \ + --hash=sha256:0485a25ef519c62e749ee3c1ad8070e591b380d67226349eb5a70b228dc1ac4a \ + --hash=sha256:057769b5921336eb2d9124f2a731b42ed05ffdac559b840dbdf6f3937cf153dc \ + --hash=sha256:1f9caa63fad8241257ae401b5ff0a64026c6adb36b8e86cbe8782d9ea505daf6 \ + --hash=sha256:252f883290d1cdb728eb7fe1d9a7221b88af5a329aae0bc91ddee4dafb820331 \ + --hash=sha256:2880350b13d3eae69a0d70bc1fb6c9bfaca4dbd0e20ba8cd1aa483080b56ff06 \ + --hash=sha256:293cc1c5bfa741f8e3fbe8175b9c07beee487c9a6fdbb25a5acad9f1df2d30a9 \ + --hash=sha256:2d39a56282cfcc0d8eeea37267c754be59c98d48505c23b1dae5c6011f3813dd \ + --hash=sha256:2efa40b068197d5efb62655b43baadb842ed71c4958cccd3e8b86a35726f0119 \ + --hash=sha256:31883542dd6c94d178f5db3d32fbd69c5eb88b3a7c018e7ac8cc0c45195ddbed \ + --hash=sha256:3926fa117b5e65019853a2969966d11c7175af377a3425991f3fe73784412405 \ + --hash=sha256:39e92ff8e8cb45947fe9007174b2950e1fb098e6abd00266a13cd3bcf6675068 \ + --hash=sha256:3a8660fe66667b76a6e9dccd1d33e66b229fde3b308db991c041609226c005b6 \ + --hash=sha256:3ccebbed24f1281062d5852353c72c47502955926cfcb8345ffb3a44d87ff3d3 \ + --hash=sha256:3d822605fa7bb326ef868d25fafced7fc660fa46d9b90c02ea86d5e2f5d325f7 \ + --hash=sha256:40a57b73731be45da4fa41430c4d5dc94a24b3a4faba7b9e069978c0402064ea \ + --hash=sha256:43dd6d596879bb1cb8a12cc9dae7bb10090a39a35883026c24f82488a195619a \ + --hash=sha256:485f1113af805e9e170b95ef993ca3fbd4f89c04bab25c58b4fc632d854801ab \ + --hash=sha256:4c38b915511e32bc718c49dbce98ff9af36bac0ad6a604f58000cd5e3aecdba7 \ + --hash=sha256:4ca7e6fd1ad845d1cc649dc2ecd499db2f8f46af5bf8da7b70dd858774cc038b \ + --hash=sha256:5075b9da3ef807eda752502446dfecea3b381c4900b7e27a5d5f4f899eb39951 \ + --hash=sha256:54f95b486018d262bcb387a9afd96f0da74508b442762b80c769454a6fbb3ee3 \ + --hash=sha256:6102f2f985c2e542be85cd857678ec9356fefa792b93cadfadd31139f5696f27 \ + --hash=sha256:6479d9722a4cd21b578f5478074c41e6169f04811996ec881655560f703a5bba \ + --hash=sha256:6c37c43e4004dfb42d321ddedc569dc17ff4259296f3af577c9ea46a809bc010 \ + --hash=sha256:6f18048fe9f6dd266bd577cdec48bdcecb74faaa01fe941324435483b013ed2a \ + --hash=sha256:77308ae6c5cf5264cc0f01a7c556ec77a9e68eb1f61b093534d698139fdc3b14 \ + --hash=sha256:861794565b06337005c1447ef23103a3d5a627d08bdc827870d00d0b28ef5f51 \ + --hash=sha256:86b8a1e6d90467345356098b040150e82fbc26d24a7a202224b13dc1f6264ca0 \ + --hash=sha256:8c9d537f59e936392cfd3597789d1390304dd659efc3c486ce7f40fb6b8a9f53 \ + --hash=sha256:8d53a23f27e1ed3a36b2d26fd2a1a6228c8e85a1ed62ff7cdb44bd610769f20a \ + --hash=sha256:9118ad3e369727060b2696fc4078f250ecffca4248ba87f537f55cea9f9dce06 \ + --hash=sha256:96abc072ad29db8d02194afd47d68987322622787daceae82398d7b69f3ba2e6 \ + --hash=sha256:9830ff7e764f74d9eefb01170c61a9f0fd2c027dac5fcb72e064decd57d56371 \ + --hash=sha256:9a2ef9cf12f2de4f1028c42c1dd7d775255e0fb3e5bb48896c97e35ef52366fe \ + --hash=sha256:9d187197d234aa45d6cfa2b096be5f666e8cc2e7eb3722d0ab8926293cf5720c \ + --hash=sha256:9da7330f3e235bf7da89b8d39205c6350fc0c08a85379743f2df9fff87d6d980 \ + --hash=sha256:a02cbed7d8bfdcdee88edaac12bd50d53d9953aaa2e1852ef078625be5f1c0b5 \ + --hash=sha256:a63bed264e818cd83eec11feed0f50aa162542b91132ef58afebc857182763a5 \ + --hash=sha256:ab0f9a59f7d63d0d441b56b9a818b273705264352d5115cfee12e940e816d958 \ + --hash=sha256:ac4f0a83c55a9b782f79ad55a5247b7db123c1db405959791c2ef886e9710c9f \ + --hash=sha256:b2a5978662fd4db463dfb4b974d2b10ac6430b98f5333aabc7051909df3561d0 \ + --hash=sha256:bd84d60bca7079e741be4ac5dbe237751a59d7f6f9f0126b11880d63822cbe16 \ + --hash=sha256:c85d8d18db5b2dfcb3b7e38a4d600ca35504c0ed8a6f75cd1c811e4ffe248a15 \ + --hash=sha256:d8b3c8eee4c1baef9d4e84d2a59a805501617127be42615cb48970b15b0892b6 \ + --hash=sha256:db1b957291bca08c7e72f43a12357b2948e20775d970e3fc3dac0aa3160ab725 \ + --hash=sha256:ddd3b61f45c132da66c5476b281891e08c1fd87fbdabe8a6973e1622efc85f06 \ + --hash=sha256:e0910c3442a75216dde0f102d854ba2aaa71d2482e0ee213630b9bf29584fba3 \ + --hash=sha256:e1bd223df0f6c96b396975fa604cb33bce53d9b4a0185490be4c4a289f7c9c87 \ + --hash=sha256:e7266307e5fba39836edb79def8608887af48820508bff3c5f2941e1e04d1534 \ + --hash=sha256:e94f9121d13fa36cbf21314783c77d05ae3a0868decd18cf5233fdcc6de49ac8 \ + --hash=sha256:f0190a33d7f97c65e9069f7a7f40499eea6b5cbe260c558378109caf20ce934b \ + --hash=sha256:f3186969ee66a9863b00acc6523ace44c56974eecb348a7ea4b228d9f0b80e19 \ + --hash=sha256:f359df4bd921918af8bebd142a376c77511d7151cc8ba852760b587b5a4a54f3 \ + --hash=sha256:f7cc5f10386994c0f4844f1e6d6a97127e9b478660eb6dec2b257644f0acab64 \ + --hash=sha256:fa70ed4dea0bb18b30a1789c77baa701d0ef30c474f2ccabdea61e25623a8827 \ + --hash=sha256:fdc0d5b18ff8fb364e87923e47c0a91d0d69dbcaeaa274591f7fd26892cc3a3a \ + --hash=sha256:ffa5e7cb08f96fed9121f77b224151e41caf88feab9d652bb46c78202b6fbeda +chardet==7.6.0 \ + --hash=sha256:089e3bb81a0a07e94f15461ded9f9ee66d349615b1a9fd557d4de1003e2fc12e \ + --hash=sha256:0ad9bc6dab4f338673353fa3f0dc96122f559aaf746087408106e2fcbf132fe8 \ + --hash=sha256:0bdb6f03107b7ace3f44e0edd91aa24456ee558787df265cc19daf45785b31c7 \ + --hash=sha256:0c44a32da32cc8b23d6b20d98ace15ec7600950e4955d1bf5ab1f849b0187fdb \ + --hash=sha256:0f304de7041afaec0195ad6464937cd112392002e9d72ed15d55f20a9abd3a13 \ + --hash=sha256:167d7ba3ee08b654e36d7b43ebd9a36606c9a12e2fabdb361757a095ca3b7e3d \ + --hash=sha256:19fea52164e6e00f2a21ed418f42e4b0162a09199274c86d07ad3efd661317c4 \ + --hash=sha256:249993b88ac7a58cad2781acea8f379152a28a719c9b401d614898c63a8c83da \ + --hash=sha256:271ab71ec1be61dbbce0436de0848895c03eae051c379e937a39573d9ce403d9 \ + --hash=sha256:284136186ff90735f901ed0a1c6d41e7af67c666841cc0eceb58482a21b7056c \ + --hash=sha256:2b5d31f9b7f793e15e81cca877e7ccd72bffffa2a3443a9d47be9dfee84fad69 \ + --hash=sha256:2cf0adaca8b1c4bacfade9d0a1e4f8f70b1bb122833d6f07ab90e3adc84eb13a \ + --hash=sha256:360260d074d8712ac1e9048fcafb0fdde246f9d0b12555748ad0017c5ecee43d \ + --hash=sha256:406936df1328a3284fef366eaa2bfd1cccd0ef1b10cb99781dd5b022ea644b84 \ + --hash=sha256:4076d795897ce45239825956a1334e134322ecc4bfe84dbb12acd5390de0fbc1 \ + --hash=sha256:43ea433e43a23c55e8e17f3fad1e07f5cfe5450c73124b95b0d849c21ad379ee \ + --hash=sha256:459e2b1c98f9a86a4698112aa42dffa802bbbff883c1ff144071f87224125862 \ + --hash=sha256:4b81d3f7d7914442d5f7d515b8c6d79cee6b794bc208971fb6902f176671166a \ + --hash=sha256:55a4c31adc7c7e83ad412f2f66b6b7358d0d4fe67505e7f58e18f68f75d341bb \ + --hash=sha256:57e6846cc13ce1ff59979f4ec9da770c57e12aa99046073f632de5a51d9a6f20 \ + --hash=sha256:5e9b31b9ae93872d66439b046a1e08c2ea99791f3c254dce1e2633e395c5587c \ + --hash=sha256:61238d5945b36af9a2ad13494f8969b7deb3c3b4abe223e54670c064e73f5328 \ + --hash=sha256:6424512f576fa7e88b7431d38a42d57552c8f717465a975fc42e497cd280d833 \ + --hash=sha256:75d6c3a4d2046d49e83d2d2206eb073a1f390743e856d90c1bbc19949b26acf4 \ + --hash=sha256:7b586cab9e9072dddd89bc2bd27ee72808d0c84ec73695fe6ec0f3c46b057c65 \ + --hash=sha256:7bbc8a9652c7f859c593847f220c1d264f25749369abb1a267b404ee8cceb209 \ + --hash=sha256:83512a475a2f3886166aa0bca1bbb39343a4eb3186dd5532127d6f2591d09118 \ + --hash=sha256:8900f6c7cf6b015b17a51767cc6144689059ba1cdceaa383d29eb037ac28579e \ + --hash=sha256:93d9df6089ded42ed1fe9f57e272c0b74bd0464d45c0c7d50f09f26f31105c3c \ + --hash=sha256:a12023d48d0e207791c01161d03cb3c0d85c6a15f345eb9d3d56063a63d1e40f \ + --hash=sha256:a4f0a368ad04d5def08bdfaa17c7e15e71552f93923dc2aa9b2f7d9dee02fbb6 \ + --hash=sha256:aa03322e07ac08d520ec50bb50c73143d0892d1adc067d4c5e58f4ef4b2363a8 \ + --hash=sha256:b3b4c96c4df93899b3c8b9e8159e06b1f55c66d7ca384d91481108e251a06eb0 \ + --hash=sha256:b73f277c1ac09c4f8076c4214b816c7aa78a0a2f0cb7156742f4303f856bedc3 \ + --hash=sha256:c54b6a8d3b219560fa5cf4c28df932c37471afe047afdc152067104e741f38c1 \ + --hash=sha256:c6061adf247ab5dda173b67010e13904c6071717660c7c8077fb50aca362b264 \ + --hash=sha256:cbaca8f563a9de07ab1a53157dba93802e54c26afe3339892afcc7c59ea4ef1b \ + --hash=sha256:cedbc584789eb2edfde20fd03669972a833ce6019e60014ae613f9bfc440e8e3 \ + --hash=sha256:cf6d08c2373b7772a558d141f9e8cee53fe1d222341bac612e4d558b04995f73 \ + --hash=sha256:d5dc835e40e0e09c2c3eab43731a8b5127834f42786dda09ba2f4b699ccd527a \ + --hash=sha256:d6030886e7da2740bf299b6a8cc75b4dcc2c90db0ca8fe0a6e4fd0bfd071dabd \ + --hash=sha256:da86fc1b40ff5996fbb5e4c2d2dca770eac2c893cef157dacc050b8b4d929846 \ + --hash=sha256:dde4080fb6bb8db96e8c44893771bcc0d235f4c22cdddb194a765a65e3a72ba7 \ + --hash=sha256:f14f46ef1977e41ce1f4814ca6984cea7f8b6baf8cbc6626ef7bf3d13cf7ea13 \ + --hash=sha256:f2ec3c78cc6b54bf8e091ec4ee885473078b5d7ef18ab1b01c86ae1e98bf88f7 \ + --hash=sha256:fc1e1571321baf8927582fe34363ad7f02279f11c8c2839c14b4c76894148db6 +Jinja2==3.1.6 \ + --hash=sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d \ + --hash=sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67 +librt==0.15.0 \ + --hash=sha256:04d5387b908676c0b8d5d2f5fb58373b4ea382d81f7a6f0fab8ea2a462bb4738 \ + --hash=sha256:077471b3182db4e17c36ae91555f36a4d2c00080b267f749bcad34a478a9a302 \ + --hash=sha256:0a15cb554761247d84a3ec0cbdf4078d70725384f0e4662c0fa3b26266eb60ad \ + --hash=sha256:0da0d94cb802f32a0524653e7201f2cef72d5f700a5407678f5290483d4fcd08 \ + --hash=sha256:0e2d0c0acf5b0ada7d045912b7cf787c21315c95b38b1fa939ef72d45d366b3d \ + --hash=sha256:0f0ee3644d951f31055ad07d77d92520e84505dd7a432cc4cd501dd70ee06785 \ + --hash=sha256:1172c6ad2a88b646e7fe3b480e3fac4ab4418b3443fd8a4061fdd531e0622fc7 \ + --hash=sha256:1256589e0b0adb31751d685a68bce29d73407ddf4ef05d4188f49d5dcf9566d9 \ + --hash=sha256:1a1a8cd430c7dd0c083f455cb1b328d7fc682b05c31b940906f7845bdff80881 \ + --hash=sha256:1cd3b721f24c206398b9e26da3c3a9c011e6e89d06f318ba8ebefc30f1003890 \ + --hash=sha256:1e47b8ba865d7ede071a91a7163073bbaeb72541f1ef8a07d512c45c7b5007f2 \ + --hash=sha256:1f4ef2e71db33df4309167ed7f1520c4fae5e611226e159fa9cf33f93e6ddb3d \ + --hash=sha256:2067ff438048cead9d223ca5675bae2a25e520a7c3e6c1498bf9c6892d22caab \ + --hash=sha256:22d6263b9d39d7bbb286fa791945646e3218f1be2d693e36fb630f1d0e59cd13 \ + --hash=sha256:234d8d394721fa0d786af15ebf1f3fb7f3ed82fd1cd0cde45c2f247b5d4281d2 \ + --hash=sha256:256237037a3ab001ae8d9803b2d43562a4c3aa38739843694349e4d5ebb0fd56 \ + --hash=sha256:291bf73caf78b9e88d6fae9bfd693207ff7d832e2fdbe2cf8e746bc13f5f892b \ + --hash=sha256:29c4cab9df457b19672c39be7f384ebb2bc925c4e2684b8780c222b43eb36389 \ + --hash=sha256:2cfd1a81a648806e6a7717be4cc4d1bb392fa229752bf8444ba365e381e984d6 \ + --hash=sha256:2fde98cf1fc4bac144ce23c2c4c017b924ba714509ea9334977b0b27050c837d \ + --hash=sha256:32896a0af72508ea979e0acb4e4c04cbeeae04938167950d535c83c45597167d \ + --hash=sha256:355e3a4c725225a14262004fc1872a552b9d3634b4f791a0dfc80804aafbfd55 \ + --hash=sha256:3722a099730704c9a3d70c879fc0f51daec25fe5f1555672d97bc595abeafb95 \ + --hash=sha256:38c0c7d4b6fc06c3324b3f9162c8391bfc4fd9dde53afe1033ce7edb48d5a714 \ + --hash=sha256:39ffd14646190c454f0d86e0d256b33f00a87a26ab410e619773b841d0e41416 \ + --hash=sha256:3de789c82752730f94782a5ee518baf9c05edf85733aeaf73bb6e518755cdf54 \ + --hash=sha256:3e79f05e4a08b4d880342673312bbc895b56df7765605796f15902eb5367d3ae \ + --hash=sha256:3ff5893a2c23d886aa9ce786de5ac6ddc74aeeaf90743682b74d920e117d2e28 \ + --hash=sha256:411ca4d1b905b860ceba7570dd6717a71dedaddcc4b0f77ece710aa41ee11f8d \ + --hash=sha256:4388184646efe2054911c5b00a1077d6d1ee86a95b7e8ba96dc7850a809f3f40 \ + --hash=sha256:4a6369168d371207339b1e50d4532b06a7121586141f82599505a3f315751d47 \ + --hash=sha256:4bbcc257e3babea20a91715c361b24554ec4e8f51aa578568afc230799fe1a19 \ + --hash=sha256:4e66cbe84437497d951b799d3e1551291b6fb3d643820a7014b3655d57a59162 \ + --hash=sha256:4e6ee93fc3cf848dcbf0cce2eca73d8e7dcd0cc2b6df3a529d57750b30a4c55c \ + --hash=sha256:4eafbaff06b9563f8b1c850621ce51605de05208e09d4d71ce490bc972b7b9e8 \ + --hash=sha256:52e8db01f603f5da0ca30987479acff98769382efc8e142fa3962395dcf3ffdb \ + --hash=sha256:5500eeae393a184d14e1f35645962c27129d20c81afa4069e6ef826ebc2b3aaa \ + --hash=sha256:55456ea87d8df21808446d03817be2f65e20391c1c615d9187440dff28cd08dc \ + --hash=sha256:5563302a8359bc2295bb7084d1a8ed1519df96afb30eb2aa4e0bff7b54228988 \ + --hash=sha256:567b1c430f8bd560e689421468278ac5941bab4a05303b5d95b6ae10db03f451 \ + --hash=sha256:57f5eeb6ad4c180de583b1038e61fe5fbd9796bb69a8a1c1a0c7ddbec4c8c60f \ + --hash=sha256:59fe030d8ae4a57e3fb7756bf35a858de74e04066fc8555c53d0af979132af81 \ + --hash=sha256:5a6526a2a956bbb1e4ae3568c82e650fc99119c66bb011ea60715744955a2b4d \ + --hash=sha256:5a86a5a08c2235316bdb359d5dbb6ce0abfca7fac06363103e2c5af571d92f95 \ + --hash=sha256:5d2a91724463bfed4f573cd7a9fdc856d2e230d0c0e5a61416a93481dccd8605 \ + --hash=sha256:64b0c8c35aa4c4ed79896359f3e0b285cbe4e610042106500da4811c322cc108 \ + --hash=sha256:68242379c9b65a582b6e97318a1e9fbd6d445e58954f2d437991c4804ab11578 \ + --hash=sha256:6912fa5e635d74529ac7cdb1bdf6ca3af4453da8d1edbe0110ee1cb4ad407ebf \ + --hash=sha256:6c013cd3a1721e69e14380ada97eaa4b7b0cdf1c6b96fa765d4ea47c875088db \ + --hash=sha256:6c0eb900c0e91f4aebe680845242e614f1864edfd44106380d0752ac29522bf8 \ + --hash=sha256:6c6624fe268625869485553dd7cc1daf30d22558215bb2a4ff16f67a9801a31a \ + --hash=sha256:6d15a29033c57490cfe2069097c6fc4049e4e65ffbb749be7dc453b7c4c68965 \ + --hash=sha256:6d28a05796b99f749bf8794f17ba9ba1612d0076b802e9cfc62c554634e9ce3b \ + --hash=sha256:6d5225ef8801e4ea5e482fa9b5dfb891dd9ef6f6d870f1f25d449ca2c70ac218 \ + --hash=sha256:6da110e5f314c19ab8478464d02ae18808ae73d522c15260fa4918acdcd64da9 \ + --hash=sha256:6ecfc32dfb46fb7b565bcd6abf9412acf978775a998273d22888a6d7953730dd \ + --hash=sha256:713bd7df21170b982e729e46870f31d6b437bd1a9b4648cffb529bd3c2ec5c4b \ + --hash=sha256:71599e011ac880e8e45d46047d714871894c7d4ab6f25626f8d4f89da21f368d \ + --hash=sha256:7220697efaa6e5348fc3d18ee7f8563d4bfecd9872b37ffb915bfc1d08840622 \ + --hash=sha256:73b30cfa976659b3917c8f6153bdb0591c6a9ec6583599fd24a689b690622022 \ + --hash=sha256:779a6e7c894737e5983e7790a9c78c4000c30e23c9aada08081bdbea53b0fa60 \ + --hash=sha256:80811e1c42386ea95c6fb30571d3250ad43d7863f883f787f70517f441150e59 \ + --hash=sha256:814ff83a25b5fce8b9c80c4dd803153fb5c5599fc74db9e022466938368957ef \ + --hash=sha256:81a398f45b45a59200e13cd5ad1ae1d3f44334de98b148331afe2cdfee701c52 \ + --hash=sha256:823b92cf3c18ecd08afc70c42473888b41b6e8ef5046f3b82c05c154a2fa3d22 \ + --hash=sha256:82909c8f7eb9952656b65d3147afde4cf8e6d5a991eebc86418b5e65843b0ab8 \ + --hash=sha256:83380ffde38062a2e9bb55d83e74474f6614665528b98a6928720fc006dfffbb \ + --hash=sha256:8443e38dcfcfdbcf5add5118c623efd788d65ac2e25756d6251a54a06a4d0aca \ + --hash=sha256:84d244b00604d17df3fc7736c327892d6bba66181254aa4087be807b6c342bdc \ + --hash=sha256:856f743ae607f2c1380eccb566c0038a9fb3eabf0fc2be2704d76d9f73557239 \ + --hash=sha256:85ea21ec6730194d67156b0e0b5430ccb1d61f8b8b907e39b37f9812b74a13f0 \ + --hash=sha256:86a21a7bd3fe3a419512ef424cc1c020f6771d0b29cfddff36d1635a855e63f0 \ + --hash=sha256:88c2a17815c266e6d8180204ff62cb739ab869ada4a746d4c505331526ac58f1 \ + --hash=sha256:89cc46cfd15022e35084355478c9ac809d90b1152222706ac9a7655ec21df6fa \ + --hash=sha256:8ae493ed5f659a7761c43d42f183db514536073ded9bcf671d2d1df47e29a07e \ + --hash=sha256:8b2fdd7ead3c995c37940a790690660d0ca006c302db26cc51933f6766866fc3 \ + --hash=sha256:8b62076030baa2d8b1501a46bf0e19c27a489aa90671c55665bff7887f7660b0 \ + --hash=sha256:8e11699ed745931c395acd3621b07062e0f840efa6935aad87a64ed0995f0915 \ + --hash=sha256:92bfed8deec93df30286b9fe9e3b1dd17329cc076a192b4ee5ec223841d54953 \ + --hash=sha256:96bb17dbe8bab3c0954fbebfc69ed395599de75b6bbc35e3270a878e15d4dd65 \ + --hash=sha256:97335f59082f9fe2ce6c2a9cc6433a0114bbb6cd4d5c09dd76c95c68b9f9a8b0 \ + --hash=sha256:a417149c0cba4d50b61e992e5a15e69eaf96746609b461cc4ed168aeef6b79dd \ + --hash=sha256:a5207ec414d1c4a2a7231b2086970dc036f94293cdf338190984958a013a42f1 \ + --hash=sha256:a54cf9e0ef47b96af580849db5471142200568ce1e02cbf416addab551369570 \ + --hash=sha256:a56a1d4f859a82ca5b99fc4b82c9b027b15e3c455c5cd99e7d0719f27bb20b6c \ + --hash=sha256:a5fa8f1f916988d0bf1afea005bda37f56ac41a18016e813ccf0097a8d460ca4 \ + --hash=sha256:a6cd22c9da0d866558e46a041f1cc0c2bbb26b61b137b2347fa834c332e1d101 \ + --hash=sha256:aa1f1995789dca3698bc550aaceb09a51bd5df0a057ff84ff15296cd1975b801 \ + --hash=sha256:b0411b4066db926b80258c60dcb0e6db4c9cee312eab45b7e8866b17ddf9ada1 \ + --hash=sha256:b230acc1c3bfe2d6f2627ba2b95dc92e58aa494600e9722d0e6ccbc931e59702 \ + --hash=sha256:b30e600e8f337b9bd7f39b86d9fdfedc73cc46e3d0f745931a23a234220bb7e2 \ + --hash=sha256:b845b8d48088fad0cadc84be4b8fda63203be7e9237b71015b3925443c1f35ab \ + --hash=sha256:b87d67e33afaf265262f2a66db578284b88ee2e6fcd224579cb5c15518677ad8 \ + --hash=sha256:bac89069bc496ebdf4f79ebb57bbd10d0b214c8454225deb672d91002bd17e18 \ + --hash=sha256:bc25fb356d0c7810bb49ff3df908ad1fda6995d660ab099ded69244ed7ab6053 \ + --hash=sha256:bccbd8e5b0bffb7106cf18eb1baa3d7194b1cebb3b4b1cdbd4bdb19382a6ee6c \ + --hash=sha256:c16d15ee371643ab48dc8248a3e680ebbeca573a13af2c3dd0c985b142d77162 \ + --hash=sha256:c434e072557ade9cbc642d052c89d031efe47d5c9614523619d0d74a02378e81 \ + --hash=sha256:c47318cd3a61401452de11282242937e3e057c4fd3dbaf601e269d0928a06c0a \ + --hash=sha256:c70bc1b602cf59917e8f0c7a2cbc8bcc6fbc14d5486136b00707a79619121d63 \ + --hash=sha256:c7eec6a42018bc1d45763b1c162d3d2bf7c3b9a1b0ed30d3e91dcba390efefcc \ + --hash=sha256:c802434092b769b1d613ed2e13fac15fbfce1934a74bd10283b03c0fae231cd1 \ + --hash=sha256:cc30523e3f1a23fb7511cc659834a0d01a1042bb9de359bc1c131cc4ec6c9656 \ + --hash=sha256:d00d20d1818e82a07a0ee0aa89a98b17ed7916b92441090b683719cb20a59b6d \ + --hash=sha256:d2813ba2503764f0450680c533d13df7cff9b49df1411062eded5f67db4195b9 \ + --hash=sha256:d2c05c729b589e734c09578bf5964be48a911765484840d017bbc84f49d4c4ad \ + --hash=sha256:d4c7bacb70930f3d0a56f4ecf1be474a1f0d941b01dd73b756f3c256d42cb879 \ + --hash=sha256:d5f51401d102c885b9ca509e62c79b1dbff286e1b9b047fde6f763780789356d \ + --hash=sha256:d8363d7accb0286ac3a0e633f396e93800dafb8150494505daf9515bbda591f3 \ + --hash=sha256:d8bc24219b24c0af375718942ab75e3544b2763085f40f965be4326734ae8328 \ + --hash=sha256:d8edcf6f550e918dca779c069b9e156385c60b406f99fc7641f32c52f7193659 \ + --hash=sha256:da7a94d6a3411f579d72aa3e3bc5fbca7ed4549f3dbd7e5de3aa567333374285 \ + --hash=sha256:db13ca398005abcbe538deda87b686d9bd08b7001cf40c4c06b444960ae10a26 \ + --hash=sha256:dbab647e88d90b3167b91efe7091e248653688ed4337e4f90907a722c7361bb9 \ + --hash=sha256:dbd605739f228912dc49027cb764456b9757750bdc2b6b7773164db7096c6fd1 \ + --hash=sha256:e0b5deec9a8664eb722c797241970fd4aa1894d25fda36a1ddac0f7407606bd6 \ + --hash=sha256:e0d00c708fb2f5822b152429b1ac80a58dbbbc3f6c232c4d13a3f7fcf2ea5b4c \ + --hash=sha256:e1a49adf16a7c9d9646816c2946135527197b6fcf4347c7b8b761cf1bfbf4489 \ + --hash=sha256:e3b461183c5fa7681b48560f91515f53a953122fb30c71e07abc67d7ddf58c38 \ + --hash=sha256:e4c911f15a1652ca94ae9f1abd92e74cbb1b3597d2d92fdd556202f94e8cd455 \ + --hash=sha256:e56b6a368529bed262da40ce13f8fef590db0479819cca84f16a1f01ac356d0b \ + --hash=sha256:e87bc679f86a99aa3b26e3c78eeb821a247c9a28eae48eaafcc32c3bf4c3bb9e \ + --hash=sha256:e8c9a650a188e38bac005048cbe6342e81407782944d01934540ab75e417df21 \ + --hash=sha256:eab9208b00ca55bf75983ec99f7bf13acc746a36102e98953addaad7f7ea1e1b \ + --hash=sha256:ec3ba415afaf951f6951b1dd16d3c8e4f540065fc382d7e70b823a79567ca374 \ + --hash=sha256:ec4b19788f835711a2072f9dbe6b03b3bf32ed1f0fb30cf399bdd59d9f0c33fa \ + --hash=sha256:f395a4a9a03ac062dbe9a9f82e0c720502e590a38feee6a757bc82e9c63afbd8 \ + --hash=sha256:f42b74a53e5f26a0ba0007411a7455b66c67ce4022a39cc1f56fc4efd65bcbab \ + --hash=sha256:f54598964d357b1c5ab77cf5d92f21e598fe0e23cdbe9618480807f81b4eba15 \ + --hash=sha256:f56b397858a23dacf35ede366ed2212fdc03a6a57a1ad36468ad6e9dc5fac091 \ + --hash=sha256:f5de7feedc56337a088eb15cd9fafa9938367362221d8cc62c642b7f94821993 \ + --hash=sha256:f75720477ee05d509a310e856cacc8d909adc182f7b91193c207bcc26d7ee6db \ + --hash=sha256:f779070399f991400fc451719e0ea388eb7de313388bada2c127a35de05f798a \ + --hash=sha256:f9ca190fe9edc0eb08eec558a509a16d28d91c35667b8f043cba40ed5e77a959 \ + --hash=sha256:fa60887537e1d0cd2d9982269d33a709bf54b195cd2b9364fc0a758022af5bd9 \ + --hash=sha256:fc1ed11c4ad0b91af24def2050f2840ea4567828e3dd058fbe608d982f6e5465 \ + --hash=sha256:febb1ce6cac545a54e6b769982824e955a700fdd9fbf3a08a3d82c990968b57d +MarkupSafe==3.0.3 \ + --hash=sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f \ + --hash=sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a \ + --hash=sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf \ + --hash=sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19 \ + --hash=sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf \ + --hash=sha256:0f4b68347f8c5eab4a13419215bdfd7f8c9b19f2b25520968adfad23eb0ce60c \ + --hash=sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175 \ + --hash=sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219 \ + --hash=sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb \ + --hash=sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6 \ + --hash=sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab \ + --hash=sha256:15d939a21d546304880945ca1ecb8a039db6b4dc49b2c5a400387cdae6a62e26 \ + --hash=sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1 \ + --hash=sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce \ + --hash=sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218 \ + --hash=sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634 \ + --hash=sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695 \ + --hash=sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad \ + --hash=sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73 \ + --hash=sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c \ + --hash=sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe \ + --hash=sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa \ + --hash=sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559 \ + --hash=sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa \ + --hash=sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37 \ + --hash=sha256:3537e01efc9d4dccdf77221fb1cb3b8e1a38d5428920e0657ce299b20324d758 \ + --hash=sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f \ + --hash=sha256:38664109c14ffc9e7437e86b4dceb442b0096dfe3541d7864d9cbe1da4cf36c8 \ + --hash=sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d \ + --hash=sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c \ + --hash=sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97 \ + --hash=sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a \ + --hash=sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19 \ + --hash=sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9 \ + --hash=sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9 \ + --hash=sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc \ + --hash=sha256:591ae9f2a647529ca990bc681daebdd52c8791ff06c2bfa05b65163e28102ef2 \ + --hash=sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4 \ + --hash=sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354 \ + --hash=sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50 \ + --hash=sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698 \ + --hash=sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9 \ + --hash=sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b \ + --hash=sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc \ + --hash=sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115 \ + --hash=sha256:7c3fb7d25180895632e5d3148dbdc29ea38ccb7fd210aa27acbd1201a1902c6e \ + --hash=sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485 \ + --hash=sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f \ + --hash=sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12 \ + --hash=sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025 \ + --hash=sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009 \ + --hash=sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d \ + --hash=sha256:949b8d66bc381ee8b007cd945914c721d9aba8e27f71959d750a46f7c282b20b \ + --hash=sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a \ + --hash=sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5 \ + --hash=sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f \ + --hash=sha256:a320721ab5a1aba0a233739394eb907f8c8da5c98c9181d1161e77a0c8e36f2d \ + --hash=sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1 \ + --hash=sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287 \ + --hash=sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6 \ + --hash=sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f \ + --hash=sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581 \ + --hash=sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed \ + --hash=sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b \ + --hash=sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c \ + --hash=sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026 \ + --hash=sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8 \ + --hash=sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676 \ + --hash=sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6 \ + --hash=sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e \ + --hash=sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d \ + --hash=sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d \ + --hash=sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01 \ + --hash=sha256:df2449253ef108a379b8b5d6b43f4b1a8e81a061d6537becd5582fba5f9196d7 \ + --hash=sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419 \ + --hash=sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795 \ + --hash=sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1 \ + --hash=sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5 \ + --hash=sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d \ + --hash=sha256:e8fc20152abba6b83724d7ff268c249fa196d8259ff481f3b1476383f8f24e42 \ + --hash=sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe \ + --hash=sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda \ + --hash=sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e \ + --hash=sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737 \ + --hash=sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523 \ + --hash=sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591 \ + --hash=sha256:f71a396b3bf33ecaa1626c255855702aca4d3d9fea5e051b41ac59a9c1c41edc \ + --hash=sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a \ + --hash=sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50 +mypy_extensions==1.1.0 \ + --hash=sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505 \ + --hash=sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558 +pathspec==1.1.1 \ + --hash=sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a \ + --hash=sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189 +pluggy==1.6.0 \ + --hash=sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3 \ + --hash=sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746 +Pygments==2.21.0 \ + --hash=sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9 \ + --hash=sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c +typing_extensions==4.16.0 \ + --hash=sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8 \ + --hash=sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5 diff --git a/requirements-scan.txt b/requirements-scan.txt new file mode 100644 index 0000000..ca73e6b --- /dev/null +++ b/requirements-scan.txt @@ -0,0 +1,49 @@ +# The two scanners, CI only. Split out of requirements-lint.txt on 2026-08-19. +# +# 馃敶 Pinned by VERSION and deliberately not by hash, which is the one place in this +# repository where that sentence appears. The reasoning, so nobody has to redo it: +# +# * hash-checking is all-or-nothing for a whole `pip install`, so hashing these +# means writing down their entire closure - tens of packages between them, each +# with its own release cadence, and Dependabot has open bugs on rewriting +# `--hash` blocks. That is a standing maintenance cost on every bump; +# * what it would buy is smaller than it looks. Both run in a job whose token is +# `contents: read`, both produce a report, and neither can put a byte into a +# release: requirements.txt and requirements-build.txt are hash-checked, and +# those are the only two files the build and release workflows install from; +# * semgrep in particular cannot be frozen in the sense that matters anyway. The +# pin fixes the ENGINE, and `--config p/default` fetches the RULES from the +# registry at scan time. +# +# The cost is named rather than hidden: OpenSSF Scorecard counts the semgrep job's +# install as an unpinned dependency, and it is right to. This is the answer we chose +# over a lock file nobody would keep current. +# +# Install it as `pip install -r requirements-scan.txt`. + +# Linux and macOS only, deliberately: the Windows wheel installs (57 MB) and then +# `semgrep-core` fails to run a scan at all on this machine - an OCaml backtrace +# on a three-line local rule, measured 2026-08-19. Local scans go through WSL, +# CI runs on ubuntu. The marker keeps `pip install -r` honest on Windows instead +# of downloading an engine that cannot start. +# +# 馃敶 The pin fixes the ENGINE, not the rules: `--config p/default` fetches those +# from the registry at scan time, so a new or re-graded rule can turn a pull +# request red with no commit behind it. That is the trade for not vendoring them, +# and vendoring is not open to us - the Semgrep Rules License permits use for our +# own purposes and forbids redistributing the rules, which is what putting them in +# a public repository would be. +semgrep==1.173.0; sys_platform != "win32" + +# The weekly audit of what we PIN. Not the same question as dependency-review, +# which looks at what a pull request ADDS: this one asks whether an advisory has +# been published since, against versions that have not moved. +# +# 馃敶 Run against an INSTALLED environment (`--path`), never against the +# requirement files. Measured 2026-08-19: `pip-audit -r requirements.txt -r +# requirements-build.txt --require-hashes` audits 7 of the 9 pinned packages and +# says "No known vulnerabilities found" - it silently skips `packaging` and +# `setuptools`. The same set installed into a venv and audited by path comes back +# with 10 (those two, plus pip itself). A scanner that quietly covers less than it +# was asked to is worse than none, because its clean report gets quoted. +pip-audit==2.10.1 diff --git a/tests/test_mutation_registry.py b/tests/test_mutation_registry.py index 0876484..2a965ca 100644 --- a/tests/test_mutation_registry.py +++ b/tests/test_mutation_registry.py @@ -991,6 +991,50 @@ "new": "pyinstaller>=", "test": "test_both_workflows_install_the_same_pinned_builder", }, + { + # The one unverified link in a hash-checked chain: an unpinned pip, fetched + # from the index a line before it is asked to verify our hashes. Additive on + # purpose - it puts the line back without taking anything away, so exactly + # one test answers. + "label": "supply chain: a workflow upgrades pip from the index again", + "file": ".github/workflows/ci.yml", + "old": " pip install --require-hashes -r requirements-lint.txt", + "new": " python -m pip install --upgrade pip\n" + " pip install --require-hashes -r requirements-lint.txt", + "test": "test_no_workflow_bootstraps_pip_from_the_index", + }, + { + # The flag, not the file. Without --require-hashes the hashes still get + # checked today and stop being checked the day a line loses its block - + # a downgrade with no error anywhere. + "label": "supply chain: an analysis job stops asking pip to check hashes", + "file": ".github/workflows/ci.yml", + "old": " - name: Install the linter\n" + " run: pip install --require-hashes -r requirements-lint.txt", + "new": " - name: Install the linter\n" + " run: pip install -r requirements-lint.txt", + "test": "test_every_install_of_a_hashed_file_asks_pip_to_check_the_hashes", + }, + { + # Version-agnostic like the pyinstaller entry above, and for the same + # reason: spelling the number here would make the entry go stale on the + # next bump. A closure line that stops being a pin breaks hash-checking + # for the whole install, not just for itself. + "label": "supply chain: a line in the lint closure loosens into a range", + "file": "requirements-lint.txt", + "old": "pluggy==", + "new": "pluggy>=", + "test": "test_the_analysis_tools_carry_their_artefact_hashes_too", + }, + { + # The permission that outlives the job it was written for: back at the top + # of release.yml, where every job added later inherits it. + "label": "supply chain: release.yml grants write at the file level again", + "file": ".github/workflows/release.yml", + "old": "permissions:\n contents: read", + "new": "permissions:\n contents: write", + "test": "test_the_release_workflow_grants_write_on_the_job_not_the_whole_file", + }, { # The correction that stops a rounded figure being printed in a unit # that cannot hold it: 1023.7 B rounds to 1024 B, and the byte band diff --git a/tests/test_version_and_release.py b/tests/test_version_and_release.py index f36edb7..d3c01f3 100644 --- a/tests/test_version_and_release.py +++ b/tests/test_version_and_release.py @@ -526,28 +526,14 @@ def test_the_release_attests_exactly_the_archive_it_publishes(): check(f"{action} is still in the release workflow", action in text) -def test_every_pinned_runtime_requirement_carries_its_artefact_hashes(): - """A version pins a NUMBER. Hashes pin the BYTES. +def _check_every_requirement_carries_hashes(filename): + """Shared by the two hash-checked files: every line pinned, every pin hashed. - `pydivert==3.1.3` says which release to fetch, and says nothing about what - comes back: an index or a publishing account that has been taken over can - serve different bytes under the same version, and this particular wheel - carries the WinDivert kernel driver that gets installed on a user's machine. - With hashes present pip refuses anything that does not match. - - Measured while writing this (2026-08-19), because the failure mode is not the - obvious one: corrupting the hash of ONE artefact does not fail the install - - pip falls back to another artefact of the same version, which is why every - artefact PyPI published for that version is listed. Corrupting them all is - what produces "THESE PACKAGES DO NOT MATCH THE HASHES" and exit 1. - - This runs offline, so it checks the SHAPE rather than the values: the file - that ships cannot quietly lose its hashes. Regenerate with - `python tools/pin_hashes.py requirements.txt`. + Offline on purpose - it reads the SHAPE, not the values, so it says the same + thing on a runner with no network as it does here. """ import re - path = os.path.join(ROOT, "requirements.txt") - with open(path, encoding="utf-8") as handle: + with open(os.path.join(ROOT, filename), encoding="utf-8") as handle: lines = handle.read().splitlines() pinned = {} @@ -557,27 +543,96 @@ def test_every_pinned_runtime_requirement_carries_its_artefact_hashes(): if not stripped or stripped.startswith("#"): continue if stripped.startswith("--hash="): - check("a hash line follows a requirement", current is not None, f"({stripped[:40]})") + check(f"{filename}: a hash line follows a requirement", + current is not None, f"({stripped[:40]})") digest = stripped[len("--hash="):].rstrip(" \\") - check(f"{current}: sha256 in the shape pip reads", + check(f"{filename}: {current}: sha256 in the shape pip reads", re.fullmatch(r"sha256:[0-9a-f]{64}", digest) is not None, f"({digest[:24]})") pinned[current].append(digest) continue match = re.match(r"^([A-Za-z0-9._-]+)==", stripped) - check(f"every requirement is pinned with == ({stripped[:40]})", match is not None) + check(f"{filename}: every requirement is pinned with == ({stripped[:40]})", + match is not None) current = match.group(1) if match else None pinned[current] = [] - check("the file still names its requirements", len(pinned) >= 2, f"({sorted(pinned)})") + check(f"{filename} still names its requirements", len(pinned) >= 2, f"({sorted(pinned)})") bare = [name for name, hashes in pinned.items() if not hashes] - check("every pinned requirement carries at least one hash", not bare, f"({bare})") + check(f"{filename}: every pinned requirement carries at least one hash", + not bare, f"({bare})") # More than one, or pip's fallback to another artefact of the same version # would be an unhashed path back in through the front door. thin = [name for name, hashes in pinned.items() if len(hashes) < 2] - check("each names every artefact, not just the one this machine picks", + check(f"{filename}: each names every artefact, not just the one this machine picks", not thin, f"({thin})") +def test_every_pinned_runtime_requirement_carries_its_artefact_hashes(): + """A version pins a NUMBER. Hashes pin the BYTES. + + `pydivert==3.1.3` says which release to fetch, and says nothing about what + comes back: an index or a publishing account that has been taken over can + serve different bytes under the same version, and this particular wheel + carries the WinDivert kernel driver that gets installed on a user's machine. + With hashes present pip refuses anything that does not match. + + Measured while writing this (2026-08-19), because the failure mode is not the + obvious one: corrupting the hash of ONE artefact does not fail the install - + pip falls back to another artefact of the same version, which is why every + artefact PyPI published for that version is listed. Corrupting them all is + what produces "THESE PACKAGES DO NOT MATCH THE HASHES" and exit 1. + + This runs offline, so it checks the SHAPE rather than the values: the file + that ships cannot quietly lose its hashes. Regenerate with + `python tools/pin_hashes.py requirements.txt`. + """ + _check_every_requirement_carries_hashes("requirements.txt") + + +def test_the_analysis_tools_carry_their_artefact_hashes_too(): + """Same shape, second file - and here the closure is the point. + + `requirements-lint.txt` used to pin three tools by version alone. Since + 2026-08-19 it is hash-checked, which is all-or-nothing in pip: the moment one + requirement carries a hash, EVERY package the install resolves needs one. So + the file holds the whole closure (13 packages, resolved for Python 3.14 and + measured identical on manylinux2014_x86_64 and win_amd64), not just the three + names a person chose. + + That is what this guards. Losing a transitive line does not loosen a pin - + it breaks the install outright, on every job at once, which is a confusing + failure to meet for the first time on a runner. Regenerate with + `python tools/pin_hashes.py requirements-lint.txt`. + + semgrep and pip-audit are deliberately NOT here: they live in + requirements-scan.txt, pinned by version and not by hash, for the reasons + written in that file. See test_the_scanners_are_pinned_by_version_at_least. + """ + _check_every_requirement_carries_hashes("requirements-lint.txt") + + +def test_the_scanners_are_pinned_by_version_at_least(): + """The file we chose NOT to hash still has to pin. + + requirements-scan.txt is the one place here that answers "pinned by version, + not by bytes", and the reason is written there: the two scanners' closures are + tens of packages, and neither can put a byte into a release. That is a decision + about hashes - it is not permission to let the versions float, which would put + the semgrep engine and the pip-audit database back on "green yesterday, red + today with no commit behind it". + """ + import re + with open(os.path.join(ROOT, "requirements-scan.txt"), encoding="utf-8") as handle: + lines = [ln.strip() for ln in handle.read().splitlines() + if ln.strip() and not ln.strip().startswith("#")] + check("requirements-scan.txt still names its scanners", len(lines) >= 2, f"({lines})") + loose = [ln for ln in lines if not re.match(r"^[A-Za-z0-9._-]+==\d", ln)] + check("every scanner is pinned with ==", not loose, f"({loose})") + for tool in ("semgrep", "pip-audit"): + check(f"{tool} is still pinned in requirements-scan.txt", + any(ln.startswith(tool + "==") for ln in lines), f"({lines})") + + def test_the_dev_requirements_do_not_pull_in_the_hashed_file(): """The two cannot share one `pip install`, and the reason is pip's, not ours. @@ -593,3 +648,104 @@ def test_the_dev_requirements_do_not_pull_in_the_hashed_file(): check("requirements-dev.txt does not include requirements.txt", not any(ln.startswith("-r requirements.txt") for ln in lines), f"({lines[:3]})") check("it still lists the test tooling", any("pytest" in ln for ln in lines), f"({lines})") + + +def _workflow_paths(): + import glob + return sorted(glob.glob(os.path.join(ROOT, ".github", "workflows", "*.yml"))) + + +def test_no_workflow_bootstraps_pip_from_the_index(): + """`pip install --upgrade pip` was the one unverified link in a hash-checked chain. + + Every install in these workflows that matters is `--require-hashes`, and the + program doing the checking was itself fetched from the index, unpinned, one line + earlier. A poisoned pip is then the thing verifying our hashes, and it is the + last place anybody would look. Removed in five places on 2026-08-19; the pip that + checks them is the one `setup-python` shipped with the interpreter, and that + action is pinned by SHA. + + 馃敶 This guard exists because the SCANNER cannot see most of them. OpenSSF + Scorecard skips steps that run in a Windows shell (`checks/raw/ + shell_download_validate.go`: "Skip unsupported shells. We don't support Windows + shells"), and three of the five were in `windows-latest` jobs - so it reported + two and stayed quiet about the rest. Fixing only what a scanner names is how a + repository ends up green and unchanged. + """ + offenders = [] + for path in _workflow_paths(): + with open(path, encoding="utf-8") as handle: + for number, line in enumerate(handle.read().splitlines(), 1): + code = line.split("#", 1)[0] + if "pip install" in code and "--upgrade" in code and "pip" in code.split("--upgrade")[1]: + offenders.append(f"{os.path.basename(path)}:{number}") + check("no workflow upgrades pip from the index before checking hashes", + not offenders, f"({offenders})") + + +def test_every_install_of_a_hashed_file_asks_pip_to_check_the_hashes(): + """The flag is not decoration: without it the hashes are advisory. + + pip does turn hash-checking on by itself when a requirement carries a hash, so + dropping `--require-hashes` would still verify what is written down - and would + silently stop being an error the day a line loses its hash block. The flag makes + that an install failure instead of a quiet downgrade, which is the same reason + it is what OpenSSF Scorecard looks for (`isUnpinnedPipInstall`, read 2026-08-19: + the flag is the ONLY thing that makes a pip command count as pinned). + """ + import re + hashed = ("requirements.txt", "requirements-build.txt", "requirements-lint.txt") + seen, bare = 0, [] + for path in _workflow_paths(): + with open(path, encoding="utf-8") as handle: + for number, line in enumerate(handle.read().splitlines(), 1): + code = line.split("#", 1)[0] + if not re.search(r"\bpip install\b", code): + continue + names = [f for f in hashed if f"-r {f}" in code] + if not names: + continue + seen += 1 + if "--require-hashes" not in code: + bare.append(f"{os.path.basename(path)}:{number} {names}") + check("the hashed files are still installed by these workflows", seen >= 5, f"({seen})") + check("every install of a hashed file passes --require-hashes", not bare, f"({bare})") + + +def test_the_release_workflow_grants_write_on_the_job_not_the_whole_file(): + """A permission belongs to the job that uses it, never to the file. + + release.yml is the only workflow that can publish an asset under this project's + name, and it held `contents: write` at the top - inherited by every job added to + that file later, by an author with no reason to scroll up. Named by OpenSSF + Scorecard (Token-Permissions, 0/10) and moved onto the single `release` job on + 2026-08-19. ci.yml, pages.yml and scorecard.yml already worked this way. + + Read as text: PyYAML is deliberately not a test dependency (same choice as + test_site.py and the CI-jobs guard in test_readme_guards.py). + """ + path = os.path.join(ROOT, ".github", "workflows", "release.yml") + with open(path, encoding="utf-8") as handle: + lines = handle.read().splitlines() + + top, inside = [], False + for line in lines: + if line.startswith("permissions:"): + inside = True + continue + if inside: + if line.startswith(" "): + top.append(line.strip()) + continue + if not line.strip(): + continue + break + check("release.yml still declares top-level permissions", bool(top), f"({top})") + granted = [entry for entry in top + if not entry.startswith("#") and entry.endswith(": write")] + check("release.yml grants nothing writable at the top level", not granted, f"({granted})") + + body = "\n".join(lines) + for scope in ("contents: write", "id-token: write", "attestations: write"): + check(f"the release job still asks for {scope}", + f" {scope}" in body, "(a six-space indent is the job's own block)")