From 319661c32c13402294a98e5ef53dd8ae8c6d4c9a Mon Sep 17 00:00:00 2001 From: wshallwshall Date: Wed, 29 Jul 2026 20:36:21 -0500 Subject: [PATCH 1/6] build(deps): declare the CI toolchain as PEP 735 dependency groups + two hashed exports A version pin does NOT satisfy Scorecard's PinnedDependenciesID. This repo's own alert data proves it: bandit==1.9.4 (#74) and zizmor==1.5.2 (96) are exactly pinned and still flagged, while the two --require-hashes installs are flagged in neither the open nor the dismissed set. ADR 0034 section 3 named routing CI tooling through a pyproject dependency group as the convergence target and deferred it. This is the declaration half. Two groups, split on THE MERGE PATH: ci-scanners bandit / pip-audit / zizmor -> the BLOCKING security gates ci-quality diff-cover / mutmut / pytest-cov / pytest-timeout -> ADVISORY measurement Splitting them keeps mutmut -- a mutation engine that rewrites and executes source -- out of every required gate's install closure. Exported hash-pinned to ci/locks/ci-scanners.lock (33 reqs, 163 hashes) and ci/locks/ci-quality.lock (27 reqs, 164 hashes). Each pin's rationale sits beside the pin rather than in a workflow comment, because a comment in security.yml pushes every dismissed alert anchored below it onto a new line number (ADR 0034's convergence rule). NOT extras (an extra is published wheel metadata) and NOT in [tool.uv] default-groups (a default group would land in the release SBOM, the container image locks, and in what pip-audit audits AS RUNTIME). Verified: with these groups non-default all four pre-existing DEP-1 artifacts re-export byte-identically -- `git diff --exit-code` exit 0. semgrep is deliberately EXCLUDED: it requires opentelemetry-sdk>=1.37,<1.38 while the project's [otel] extra resolves 1.44, so a plain group silently downgrades the shipped otel runtime in all four artifacts. The only fix ([tool.uv] conflicts) would declare a product extra and a CI scanner permanently mutually exclusive. Recorded as a reasoned residual with the exact recipe in the ADR amendment. Verified: uv lock --check exit 0; both locks install with `pip install --require-hashes` into a fresh venv at exit 0, zero source builds, all six tools runnable at the pinned versions; the group locks and constraints.lock agree on all 20 shared packages (pytest and pytest-timeout among them), so a hashed tool install cannot re-point what the coverage run executes under. .gitattributes also gains ci/locks/*.lock and -- a latent bug found while measuring -- constraints.lock, which had `text: unspecified` and so checked out CRLF under core.autocrlf=true, exactly the drift that stanza exists to prevent. --- .gitattributes | 7 + ci/locks/ci-quality.lock | 233 +++++++++++++++ ci/locks/ci-scanners.lock | 234 +++++++++++++++ pyproject.toml | 76 +++++ uv.lock | 588 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 1138 insertions(+) create mode 100644 ci/locks/ci-quality.lock create mode 100644 ci/locks/ci-scanners.lock diff --git a/.gitattributes b/.gitattributes index c50bb1f3..ea16e11b 100644 --- a/.gitattributes +++ b/.gitattributes @@ -3,6 +3,13 @@ uv.lock text eol=lf requirements.lock text eol=lf docker/locks/*.lock text eol=lf +# constraints.lock was MISSING here (`git check-attr text -- constraints.lock` -> unspecified), so under +# core.autocrlf=true it checks out CRLF — precisely the drift this stanza exists to prevent. Harmless so +# far only because git's clean filter normalizes before `git diff`, which is also why export sync must be +# verified with `git diff` and never a raw `diff` (the latter reports a phantom whole-file difference). +constraints.lock text eol=lf +# The PEP 735 CI-toolchain exports (ADR 0034 §3) — same DEP-1 diff gate, same LF requirement. +ci/locks/*.lock text eol=lf .mefor-hooks/** text eol=lf # Shell scripts are executed on the Linux CI runners; force LF so a contributor's # core.autocrlf=true can never commit CRLF (which breaks `#!/usr/bin/env bash\r`). diff --git a/ci/locks/ci-quality.lock b/ci/locks/ci-quality.lock new file mode 100644 index 00000000..45e1ed83 --- /dev/null +++ b/ci/locks/ci-quality.lock @@ -0,0 +1,233 @@ +# This file was autogenerated by uv via the following command: +# uv export --only-group ci-quality --format requirements.txt -o ci/locks/ci-quality.lock +chardet==7.4.3 \ + --hash=sha256:0ac3bf11c645734a1701a3804e43eabd98851838192267d08c353a834ab79fea \ + --hash=sha256:1173b74051570cf08099d7429d92e4882d375ad4217f92a6e5240ccfb26f231e \ + --hash=sha256:27cc23da03630cdecc9aa81a895aa86629c211f995cd57651f0fbc280717bf93 \ + --hash=sha256:457f619882ba66327d4d8d14c6c342269bdb1e4e1c38e8117df941d14d351b04 \ + --hash=sha256:6e3bd9f936e04bae89c254262af08d9e5b98f805175ba1e29d454e6cba3107b7 \ + --hash=sha256:9f3504c139a2ad544077dd2d9e412cd08b01786843d76997cd43bb6de311723c \ + --hash=sha256:acc46d1b8b7d5783216afe15db56d1c179b9a40e5a1558bc13164c4fd20674c4 \ + --hash=sha256:b95c934b9ad59e2ba8abb9be49df70d3ad1b0d95d864b9fdb7588d4fa8bd921c \ + --hash=sha256:c77867f0c1cb8bd819502249fcdc500364aedb07881e11b743726fa2148e7b6e \ + --hash=sha256:cc1d4eb92a4ec1c2df3b490836ffa46922e599d34ce0bb75cf41fd2bf6303d56 \ + --hash=sha256:cf1efeaf65a6ef2f5b9cc3a1df6f08ba2831b369ccaa4c7018eaf90aa757bb11 \ + --hash=sha256:d892d3dcd652fdef53e3d6327d39b17c0df40a899dfc919abaeb64c974497531 + # via diff-cover +click==8.4.1 \ + --hash=sha256:482be17c6991b8c19c5429a1e995d9b0efdbb63172824c41f99965dc0ade8ec2 \ + --hash=sha256:918b5633eddf6b41c32d4f454bf0de810065c74e3f7dbf8ee5452f8be88d3e96 + # via mutmut +colorama==0.4.6 ; sys_platform == 'win32' \ + --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \ + --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6 + # via + # click + # pytest +coverage==7.15.2 \ + --hash=sha256:075560438765b7a2ef43bf7aa7758661b53d889df47f062a31bda6c1ade553a2 \ + --hash=sha256:0e55510bc98ae943cece9e667a6c0fe94c6a92913720dea34243657a17993d0c \ + --hash=sha256:1473b3ba8e7ee0f076117b1a72c23f579a2b9e2bb742f48a8d86ea27ca93f91a \ + --hash=sha256:17c432b5f73ad52ef46fb06019f6fa7c66ce381961cf0f7dfd1d3a4bd3a98145 \ + --hash=sha256:25fd15dd40a0a2c51a500d664ca29053c09c3259d998407bf982b6e114696138 \ + --hash=sha256:2fbeeeecea279727f8ac16c8e1133ddfeee793e985c86ae343d6a5ce744eef8c \ + --hash=sha256:2ff08701be2d1556fc78b326c80a3e8042da09352ecb3819105f8e386c8a3071 \ + --hash=sha256:38c9518b7103826c403a461544e3c2e77151e8676d06eaed85911a97e962584a \ + --hash=sha256:3df60dc267f0a2ca23cb7a9ab1109c62b9335ffbf519fcfe167157c28c09b81d \ + --hash=sha256:40f633c5c5fc783732f6312280122e859538fa24461235597c13d803ea9a108a \ + --hash=sha256:6aa28cfb6488e5453b5b762d65f73aa586380f6693a04d58078ce228a29b06c0 \ + --hash=sha256:6fc448c377d6eeb00a47c673494bd9bae29280ca53987e1869e67ebedfe20658 \ + --hash=sha256:7466cc7ab6dc0db871d264bf99e8779f0917ee63d40730af0552f71535a6e072 \ + --hash=sha256:77f091ea3a9cc611cd29f433565476bc1936c084ac8eee00ea0e7e70c27e4199 \ + --hash=sha256:77f0ef5011df53a4bd1b35211ab122287f8d9b8d7aa1c4553e5c2deb24b1d446 \ + --hash=sha256:7c63387e21ab21f512c69c9756a8c7dadd322c7275edb064064433c9a09c3743 \ + --hash=sha256:835ec4e20b45f0a7f63ed78f94065aca00de033403df8377bfe8b9c6abc0a7be \ + --hash=sha256:913b6c56e110da40e035bbd168353bf7aaa2544a5eaccea5d98a4629aac156c7 \ + --hash=sha256:9854ca62c152874b2060772503535be2e8f53f70b8aaa7686b094888d872f984 \ + --hash=sha256:9911f31aad8906abe337c271343485cf20df5e70df5d2f57f9f136e7b55f26bc \ + --hash=sha256:a164b50081fc7357331c4024ef4d17b78ba325f8380d05f5a69599a7e05257ee \ + --hash=sha256:aaccad4129d735a8a4d526f26929894c9a4e8ef7034566f210b176749d6906e3 \ + --hash=sha256:b9a6367e4aff723e8ee8190836836124284e8fcd4265e307c844010cfa074f3f \ + --hash=sha256:bcc0aae933921d03096f53b0b03eeb702129fd406dee59f08d2efacc68681fa5 \ + --hash=sha256:bfd341ccf78128e72c094bc70cc25b3ef309c33c7c2c66ba3ed4309549e02de1 \ + --hash=sha256:cb0fddaa6884be6aae36ced9544b5e90f7d5f03845a2853bf47a14953a4e8688 \ + --hash=sha256:dee88b1ed88587abd8c0269a1fc1f4cc77f7750d1dfde2869e2a123af420e67d \ + --hash=sha256:e370c12133095ff18432de8c044962be85a5a96d90c6fcbce8e17e76236d2328 \ + --hash=sha256:e38def96ad59853824c97953fdcd2c320a84ba3ce99b417db78af8bb6c3db635 \ + --hash=sha256:eb6bcae8d1a9d305351ecb108232441d11c5cfe9de840a04388ba5d2db8d735c \ + --hash=sha256:f653e5d7248c1191ec988a85c72edeab46c3ff44f90639a4ed4874ec0be90243 \ + --hash=sha256:fe41909c9515c3bfdb5f02c4d1f857dba322d9a9a1178069b91eea77889df63a + # via + # mutmut + # pytest-cov +diff-cover==10.4.1 \ + --hash=sha256:0ec566955c9ee7da2f6cc48fa16fac7f97ad1fc4e50a887ffb9cfe5eb1e831df \ + --hash=sha256:dc8f2654c485ec4f16e679b5af6e205783cde71185d4ceb8157662dca2d531e9 +iniconfig==2.3.0 \ + --hash=sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730 \ + --hash=sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12 + # via pytest +jinja2==3.1.6 \ + --hash=sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d \ + --hash=sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67 + # via diff-cover +libcst==1.9.0 \ + --hash=sha256:087b58a9afe076bb08e2d726478e1f16cb928d67ffa9092817e033c335de522a \ + --hash=sha256:0c7b548512db25af9c2997a95fa731bd6b6928ecbad6c0915d7482d8bb42d34f \ + --hash=sha256:107593af46945593e7825821793393262bc4fa1d3ea24c3ed487b61269bbbdf8 \ + --hash=sha256:32395244edfe6538e0ea2bf82051d60103d3f54861274805c4fb3745efb70a85 \ + --hash=sha256:444e84c76cd035cd2fe136838c1a524d34b08216521f6f5093df8a6f6cfa5799 \ + --hash=sha256:45808c03528b3ad40b14095348a918e08d98c47b4a125d631cb78e817c0a5b16 \ + --hash=sha256:496c24e0d3240bc7da45dae543aff3f5b6509978c39262d6b84ed2fb999dded2 \ + --hash=sha256:497d5329345f1f5df84e41b0bbd00204b64a2fd30dfe3cfaeaebca633a31e877 \ + --hash=sha256:50ab94bb2524b419056d4003032b8c67102ac800f8d85b3c4260a01746d94dbb \ + --hash=sha256:568288cdbfe3b4ca3ae4852cb0a439ff053dd54c841bc4995bf2b71238b5de40 \ + --hash=sha256:a2faaf92500d0226358125630f5aab4758e8aad3f2d70a10892ec3c700781a54 \ + --hash=sha256:bb5d0946f2b4c6711b5d69fe4f833b364f9e7a1a2b08f88b98619dc18975099a \ + --hash=sha256:ea490fa8540503db5f321f0268becab46eb50f710e8cec8041e241fd66f6874f \ + --hash=sha256:eccf4c57d273cdd3fe1c67b72cf9bb1bbd4547aa011824e96ccf5b7136057aa4 \ + --hash=sha256:f6248cb07444ab9a6733a855737a9febed8b9adca51347019348b09a3ac7dfe9 + # via mutmut +linkify-it-py==2.1.0 \ + --hash=sha256:0d252c1594ecba2ecedc444053db5d3a9b7ec1b0dd929c8f1d74dce89f86c05e \ + --hash=sha256:43360231720999c10e9328dc3691160e27a718e280673d444c38d7d3aaa3b98b + # via markdown-it-py +markdown-it-py==4.2.0 \ + --hash=sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49 \ + --hash=sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a + # via + # mdit-py-plugins + # rich + # textual +markupsafe==3.0.3 \ + --hash=sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf \ + --hash=sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175 \ + --hash=sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab \ + --hash=sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634 \ + --hash=sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe \ + --hash=sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa \ + --hash=sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97 \ + --hash=sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9 \ + --hash=sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc \ + --hash=sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4 \ + --hash=sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698 \ + --hash=sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9 \ + --hash=sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d \ + --hash=sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581 \ + --hash=sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026 \ + --hash=sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5 \ + --hash=sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d \ + --hash=sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe \ + --hash=sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda \ + --hash=sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e \ + --hash=sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737 \ + --hash=sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523 \ + --hash=sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50 + # via jinja2 +mdit-py-plugins==0.6.1 \ + --hash=sha256:214c82fb2ac524472ab6a5bcab1de80f73b50443e187f401bfd77efbc7c6481d \ + --hash=sha256:a2bca0f039f39dbd35fb74ae1b5f998608c437463371f0ff7f49a19a17a114d0 + # via textual +mdurl==0.1.2 \ + --hash=sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 \ + --hash=sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba + # via markdown-it-py +mutmut==3.6.0 \ + --hash=sha256:a9f5b8dcf6cbf9496769d7cf8bdbba37a0ec709ad98f88d103238b62f10bdf37 \ + --hash=sha256:bcbd3e4d0d2d4edf3dfb42955417279a8866a3dbbcb87d619f2f3fd0ac7fafda +packaging==26.2 \ + --hash=sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e \ + --hash=sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661 + # via pytest +platformdirs==4.11.0 \ + --hash=sha256:0555d18370482847566ffabcaa53ad7c6c1c29f195989ae1ed634a05f76ea1e0 \ + --hash=sha256:360ccded2b7fce0af0ff80cc8f5942a1c5d99b0e856033acb030bfc634709e74 + # via textual +pluggy==1.6.0 \ + --hash=sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3 \ + --hash=sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746 + # via + # diff-cover + # pytest + # pytest-cov +pygments==2.20.0 \ + --hash=sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f \ + --hash=sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176 + # via + # diff-cover + # pytest + # rich + # textual +pytest==9.1.1 \ + --hash=sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313 \ + --hash=sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c + # via + # mutmut + # pytest-cov + # pytest-timeout +pytest-cov==7.1.0 \ + --hash=sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2 \ + --hash=sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678 +pytest-timeout==2.4.0 \ + --hash=sha256:7e68e90b01f9eff71332b25001f85c75495fc4e3a836701876183c4bcfd0540a \ + --hash=sha256:c42667e5cdadb151aeb5b26d114aff6bdf5a907f176a007a30b940d3d865b5c2 +pyyaml==6.0.3 \ + --hash=sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c \ + --hash=sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3 \ + --hash=sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65 \ + --hash=sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310 \ + --hash=sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac \ + --hash=sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9 \ + --hash=sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7 \ + --hash=sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35 \ + --hash=sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065 \ + --hash=sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c \ + --hash=sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764 \ + --hash=sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac \ + --hash=sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3 \ + --hash=sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702 \ + --hash=sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788 \ + --hash=sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba \ + --hash=sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5 \ + --hash=sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f \ + --hash=sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b + # via libcst +rich==15.0.0 \ + --hash=sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb \ + --hash=sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36 + # via textual +setproctitle==1.3.7 \ + --hash=sha256:02432f26f5d1329ab22279ff863c83589894977063f59e6c4b4845804a08f8c2 \ + --hash=sha256:1403d2abfd32790b6369916e2313dffbe87d6b11dca5bbd898981bcde48e7a2b \ + --hash=sha256:14c7eba8d90c93b0e79c01f0bd92a37b61983c27d6d7d5a3b5defd599113d60e \ + --hash=sha256:1fae595d032b30dab4d659bece20debd202229fce12b55abab978b7f30783d73 \ + --hash=sha256:45e3ef48350abb49cf937d0a8ba15e42cee1e5ae13ca41a77c66d1abc27a5070 \ + --hash=sha256:46178672599b940368d769474fe13ecef1b587d58bb438ea72b9987f74c56ea5 \ + --hash=sha256:52b054a61c99d1b72fba58b7f5486e04b20fefc6961cd76722b424c187f362ed \ + --hash=sha256:5818e4080ac04da1851b3ec71e8a0f64e3748bf9849045180566d8b736702416 \ + --hash=sha256:585edf25e54e21a94ccb0fe81ad32b9196b69ebc4fc25f81da81fb8a50cca9e4 \ + --hash=sha256:6134c63853d87a4897ba7d5cc0e16abfa687f6c66fc09f262bb70d67718f2309 \ + --hash=sha256:6fc87caf9e323ac426910306c3e5d3205cd9f8dcac06d233fcafe9337f0928a3 \ + --hash=sha256:7f9e9e3ff135cbcc3edd2f4cf29b139f4aca040d931573102742db70ff428c17 \ + --hash=sha256:80c36c6a87ff72eabf621d0c79b66f3bdd0ecc79e873c1e9f0651ee8bf215c63 \ + --hash=sha256:96c38cdeef9036eb2724c2210e8d0b93224e709af68c435d46a4733a3675fee1 \ + --hash=sha256:9e64e98077fb30b6cf98073d6c439cd91deb8ebbf8fc62d9dbf52bd38b0c6ac0 \ + --hash=sha256:b53602371a52b91c80aaf578b5ada29d311d12b8a69c0c17fbc35b76a1fd4f2e \ + --hash=sha256:b91387cc0f02a00ac95dcd93f066242d3cca10ff9e6153de7ee07069c6f0f7c8 \ + --hash=sha256:bc2bc917691c1537d5b9bca1468437176809c7e11e5694ca79a9ca12345dcb9e \ + --hash=sha256:cbc388e3d86da1f766d8fc2e12682e446064c01cea9f88a88647cfe7c011de6a \ + --hash=sha256:e7c5bfe4228ea22373e3025965d1a4116097e555ee3436044f5c954a5e63ac45 \ + --hash=sha256:fcb966a6c57cf07cc9448321a08f3be6b11b7635be502669bc1d8745115d7e7f + # via mutmut +textual==8.2.8 \ + --hash=sha256:267375fd402dc8d981457212efa71f0e3365fd17bba144ba9bb3ed7563cb374a \ + --hash=sha256:3f106a9fbc73e39dd266c9712432087de78a6d644084c7c241d6a25c3169115b + # via mutmut +typing-extensions==4.15.0 \ + --hash=sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466 \ + --hash=sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548 + # via textual +uc-micro-py==2.0.0 \ + --hash=sha256:3603a3859af53e5a39bc7677713c78ea6589ff188d70f4fee165db88e22b242c \ + --hash=sha256:c53691e495c8db60e16ffc4861a35469b0ba0821fe409a8a7a0a71864d33a811 + # via linkify-it-py diff --git a/ci/locks/ci-scanners.lock b/ci/locks/ci-scanners.lock new file mode 100644 index 00000000..471ed1fb --- /dev/null +++ b/ci/locks/ci-scanners.lock @@ -0,0 +1,234 @@ +# This file was autogenerated by uv via the following command: +# uv export --only-group ci-scanners --format requirements.txt -o ci/locks/ci-scanners.lock +bandit==1.9.4 \ + --hash=sha256:b589e5de2afe70bd4d53fa0c1da6199f4085af666fde00e8a034f152a52cd628 \ + --hash=sha256:f89ffa663767f5a0585ea075f01020207e966a9c0f2b9ef56a57c7963a3f6f8e +boolean-py==5.0 \ + --hash=sha256:60cbc4bad079753721d32649545505362c754e121570ada4658b852a3a318d95 \ + --hash=sha256:ef28a70bd43115208441b53a045d1549e2f0ec6e3d08a9d142cbc41c1938e8d9 + # via license-expression +cachecontrol==0.14.4 \ + --hash=sha256:b7ac014ff72ee199b5f8af1de29d60239954f223e948196fa3d84adaffc71d2b \ + --hash=sha256:e6220afafa4c22a47dd0badb319f84475d79108100d04e26e8542ef7d3ab05a1 + # via pip-audit +certifi==2026.6.17 \ + --hash=sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432 \ + --hash=sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db + # via requests +charset-normalizer==3.4.7 \ + --hash=sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c \ + --hash=sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0 \ + --hash=sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a \ + --hash=sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab \ + --hash=sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18 \ + --hash=sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44 \ + --hash=sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d \ + --hash=sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b \ + --hash=sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10 \ + --hash=sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246 \ + --hash=sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e \ + --hash=sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41 \ + --hash=sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960 \ + --hash=sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e \ + --hash=sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72 \ + --hash=sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb \ + --hash=sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e \ + --hash=sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f \ + --hash=sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1 \ + --hash=sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356 \ + --hash=sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4 \ + --hash=sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5 \ + --hash=sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e \ + --hash=sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0 \ + --hash=sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d \ + --hash=sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0 \ + --hash=sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae \ + --hash=sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe \ + --hash=sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3 \ + --hash=sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44 \ + --hash=sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46 \ + --hash=sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b \ + --hash=sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24 \ + --hash=sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79 + # via requests +colorama==0.4.6 ; sys_platform == 'win32' \ + --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \ + --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6 + # via bandit +cyclonedx-python-lib==11.11.0 \ + --hash=sha256:3049fc83e06a059b5c5907a527625a8ed5073caab10607ed4c9e5503b590fd44 \ + --hash=sha256:4b3194db72b613717f2912447e67ab618c75ff7dcac6c4af3c0e9e1ac617c102 + # via pip-audit +defusedxml==0.7.1 \ + --hash=sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69 \ + --hash=sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61 + # via py-serializable +filelock==3.32.2 \ + --hash=sha256:87dd94cf281e586d135fa51132b8e3d9a598b316e90377a288663c9321036c82 \ + --hash=sha256:c33351e1f49cae33414acbc6d56784e6ecee82514ec90795da1161fc4836b5b8 + # via cachecontrol +idna==3.18 \ + --hash=sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2 \ + --hash=sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848 + # via requests +license-expression==30.4.4 \ + --hash=sha256:421788fdcadb41f049d2dc934ce666626265aeccefddd25e162a26f23bcbf8a4 \ + --hash=sha256:73448f0aacd8d0808895bdc4b2c8e01a8d67646e4188f887375398c761f340fd + # via cyclonedx-python-lib +markdown-it-py==4.2.0 \ + --hash=sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49 \ + --hash=sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a + # via rich +mdurl==0.1.2 \ + --hash=sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 \ + --hash=sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba + # via markdown-it-py +msgpack==1.2.1 \ + --hash=sha256:04c721c2c7448767e9e3f2520a475663d8ee0f09c31890f6d2bd70fd636a9647 \ + --hash=sha256:05f340e47e7e47d2da8db9b53e1bb1d294369e9ef45a747441309f6650b8351d \ + --hash=sha256:0adcf06ffde0777c0e1a9b771a2b1c4226ba1bbf748c8efcc02fcdeca3299107 \ + --hash=sha256:0e2bf9280bceb5efca998435904b5d3e9fdbcc11d90dc9df30aec7973252b720 \ + --hash=sha256:196300e7e5d6e74d50f1607ab9c06c4a1484c383cd22defd727902591f7e8dde \ + --hash=sha256:20466cca18c49c7292a8984bc15d65857b171e7264bdcb5f96baf8be238791fc \ + --hash=sha256:575957e79cd51903a4e8495a242442949641e08f1efd5197b43bebd3ea7682b4 \ + --hash=sha256:5ad5467fc3f68b5468e06c5f788d712e9f8ffc8b0cd1bcb160c105c1ee92dae7 \ + --hash=sha256:5bb9c386f0a329c035ddbab4b72d1028bf9627add8dda41070288563d57ed1b1 \ + --hash=sha256:5f6277e5f783c36786a145e0247fc189a03f35f84b251646e53592d2bc12b355 \ + --hash=sha256:74847557e28ce71bd3c438a447ca90e4b507e997ddbdef8a12a7b283b86c156b \ + --hash=sha256:810b916696c86ef0deb3b74588480224df4c1b071136c34183e4a2a4284d7ac7 \ + --hash=sha256:8c2ed1e48cc0f460bf3c7780e7137ff21a4e18433451916f2442c1b21036cd7d \ + --hash=sha256:8d00f177ca88a77c1cf848d204a38f249751650b601cb6532acc68805d8a8273 \ + --hash=sha256:98b58bdb89c46190e4609bb36abe17c6d4105ad13f9c5f8f6f64d320f8ced3fb \ + --hash=sha256:aa6c4be5d1c02a42b066ca6ddb71adf36432868fdcdb6ee87e634e86e0674190 \ + --hash=sha256:b50b727bd652bdc37d950336c848ef20ec54a4cafc38dce19b1cd86ad625d0f7 \ + --hash=sha256:c1c79a604a2969a868a78b6ebd27a887e00c624f14f66b3038e0590cb23332d1 \ + --hash=sha256:ca0dacff965c47afdc3749a8469d7302a8f801d6a28758d55120d75e66ce6889 \ + --hash=sha256:dd3bfe82d53edfe4b7fc9a7ec9761e23a7a5b1dac22264505af428253c29ed24 \ + --hash=sha256:ec0e675d59150a6269ddc9139087c722292664a37d071a849c05c473350f1f2d \ + --hash=sha256:f12038a35fabd52e56a3547bab42401af49a45caa6dd00b34c44de235bc93ee2 \ + --hash=sha256:f9389552ecf4784886345ead0647e4edc96bee37cbab05b75540f542f766c48c + # via cachecontrol +packageurl-python==0.17.6 \ + --hash=sha256:1252ce3a102372ca6f86eb968e16f9014c4ba511c5c37d95a7f023e2ca6e5c25 \ + --hash=sha256:31a85c2717bc41dd818f3c62908685ff9eebcb68588213745b14a6ee9e7df7c9 + # via cyclonedx-python-lib +packaging==26.2 \ + --hash=sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e \ + --hash=sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661 + # via + # pip-audit + # pip-requirements-parser +pip==26.2 \ + --hash=sha256:2d8542afcc84cdd8e846c2b36b2861fad1da376dd98f8e7113e9108a3c331690 \ + --hash=sha256:931c303696af6fa3417112103b1cad26890e5a07eccb5b99783700e33f2b8aad + # via pip-api +pip-api==0.0.34 \ + --hash=sha256:8b2d7d7c37f2447373aa2cf8b1f60a2f2b27a84e1e9e0294a3f6ef10eb3ba6bb \ + --hash=sha256:9b75e958f14c5a2614bae415f2adf7eeb54d50a2cfbe7e24fd4826471bac3625 + # via pip-audit +pip-audit==2.10.1 \ + --hash=sha256:1eb4565d19ebe5d48996f4b770b4d2b32887e12cb12cfa637f1a064011b55ffc \ + --hash=sha256:99ef3f600a317c1945f1e89e227ef26e1c2d618429b8bd3fa6f4f7c440c4611a +pip-requirements-parser==32.0.1 \ + --hash=sha256:4659bc2a667783e7a15d190f6fccf8b2486685b6dba4c19c3876314769c57526 \ + --hash=sha256:b4fa3a7a0be38243123cf9d1f3518da10c51bdb165a2b2985566247f9155a7d3 + # via pip-audit +platformdirs==4.11.0 \ + --hash=sha256:0555d18370482847566ffabcaa53ad7c6c1c29f195989ae1ed634a05f76ea1e0 \ + --hash=sha256:360ccded2b7fce0af0ff80cc8f5942a1c5d99b0e856033acb030bfc634709e74 + # via pip-audit +py-serializable==2.1.0 \ + --hash=sha256:9d5db56154a867a9b897c0163b33a793c804c80cee984116d02d49e4578fc103 \ + --hash=sha256:b56d5d686b5a03ba4f4db5e769dc32336e142fc3bd4d68a8c25579ebb0a67304 + # via cyclonedx-python-lib +pygments==2.20.0 \ + --hash=sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f \ + --hash=sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176 + # via rich +pyparsing==3.3.2 \ + --hash=sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d \ + --hash=sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc + # via pip-requirements-parser +pyyaml==6.0.3 \ + --hash=sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c \ + --hash=sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3 \ + --hash=sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65 \ + --hash=sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310 \ + --hash=sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac \ + --hash=sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9 \ + --hash=sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7 \ + --hash=sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35 \ + --hash=sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065 \ + --hash=sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c \ + --hash=sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764 \ + --hash=sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac \ + --hash=sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3 \ + --hash=sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702 \ + --hash=sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788 \ + --hash=sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba \ + --hash=sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5 \ + --hash=sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f \ + --hash=sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b + # via bandit +requests==2.34.2 \ + --hash=sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0 \ + --hash=sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed + # via + # cachecontrol + # pip-audit +rich==15.0.0 \ + --hash=sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb \ + --hash=sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36 + # via + # bandit + # pip-audit +sortedcontainers==2.4.0 \ + --hash=sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88 \ + --hash=sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0 + # via cyclonedx-python-lib +stevedore==5.9.0 \ + --hash=sha256:abbd0af7a38a8bbb1d6adea2e35b17609cf004eaac323e88a8d8963640dd2b3c \ + --hash=sha256:e520945d4c257700eddc1eb1d79df04b2ea578eef185e0e3fa5b442fc848d3f7 + # via bandit +tomli==2.4.1 \ + --hash=sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853 \ + --hash=sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe \ + --hash=sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d \ + --hash=sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26 \ + --hash=sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6 \ + --hash=sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f \ + --hash=sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662 \ + --hash=sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c \ + --hash=sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41 \ + --hash=sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f \ + --hash=sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15 \ + --hash=sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7 \ + --hash=sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c \ + --hash=sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac \ + --hash=sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8 \ + --hash=sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232 \ + --hash=sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4 \ + --hash=sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396 \ + --hash=sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba \ + --hash=sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf + # via pip-audit +tomli-w==1.2.0 \ + --hash=sha256:188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90 \ + --hash=sha256:2dd14fac5a47c27be9cd4c976af5a12d87fb1f0b4512f81d69cce3b35ae25021 + # via pip-audit +urllib3==2.7.0 \ + --hash=sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c \ + --hash=sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897 + # via requests +zizmor==1.5.2 \ + --hash=sha256:03b27c46d87e96a0acdf78190fbb674c174f67ded396c74dadd64d7c4ecba680 \ + --hash=sha256:0c5f656106c430748858ae459c41c9eed09a1e01e5f42015d80c8fe34740d173 \ + --hash=sha256:506b355c693d40df01bdf26fd1dfcb41801af7b8c51767d4159efdc2655965c4 \ + --hash=sha256:5e18b06af4208624ef707e7859b07539870ca26b74a89537e1a6bfba2e44fc9a \ + --hash=sha256:848f04c0d84b085dfb79c66951404372ebf9b3dd0c73076f1baac83db29e74e4 \ + --hash=sha256:86395dd985ed6bf9acffea8a900b30d8d4dd4c9e20421f16f2db7ad92299e24e \ + --hash=sha256:8c65b4dbc9690d3f0f5d9756ae0b5ca8d3dfb4f806d82a5195836a3b613ae996 \ + --hash=sha256:9d40780b19da7901423de80ccce083a8c0d2114e0cf6432aa20d8e60d15e97f6 \ + --hash=sha256:a40feabc2c043aca60f9edbd35a676d97d684af5b06ed61a7752869c79b3be30 \ + --hash=sha256:dcef697a88983e7ce6948df1603e2325e2cf5c523828af94615e6c432fa8d98a \ + --hash=sha256:df1326fe9f9ddcbe9d862a97571c848276882d0ceeee39e18fcfc9ec5c66cd7b diff --git a/pyproject.toml b/pyproject.toml index 4046bf26..d37f34f2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -208,6 +208,82 @@ messagefoundry-tray = "messagefoundry.tray.__main__:main" # and broke `build --wheel`.) tests/test_password_*.py load the corpus via importlib.resources, so a # build-config change that dropped it would fail the suite — that's the guard, not a force-include. +# --- CI TOOLCHAIN — PEP 735 dependency groups (ADR 0034 §3) ---------------------------------------- +# +# WHY THEY EXIST: a version pin does NOT satisfy Scorecard's PinnedDependenciesID. Proven by this +# repo's own alert data — `bandit==1.9.4` (alert #74) and `zizmor==1.5.2` (alert 96) are EXACTLY pinned +# and still flagged, while the two `--require-hashes` installs are flagged in neither the open nor the +# dismissed set. Declaring the tools here routes them through `uv.lock`, out to `ci/locks/*.lock` via +# `uv export` WITH HASHES, and into CI as `pip install --require-hashes -r ci/locks/.lock`. The +# same machinery (DEP-1 diff gate + the Dependabot resync) that keeps the other four exports fresh +# keeps these fresh, which is what stops a hash-pinned toolchain rotting into a pinned-but-unpatched +# one — the failure ADR 0034 §3 names as "worse posture than floating". +# +# NOT EXTRAS, deliberately: an extra is published wheel metadata, so `[ci-scanners]` would become a +# real install target for every downstream consumer of the wheel. A dependency group never ships. +# +# NOT IN `[tool.uv] default-groups`, deliberately: a default group lands in all four committed DEP-1 +# artifacts — i.e. in the release SBOM, the container image locks, and in what `pip-audit` audits AS +# RUNTIME. Measured with these groups non-default: all four re-export byte-identically (DIFFS=0). +# +# THE SPLIT IS THE MERGE PATH. `ci-scanners` is what the BLOCKING security gates install for +# themselves (security.yml's pip-audit + bandit jobs, zizmor.yml); `ci-quality` is ADVISORY +# measurement (quality-advisory.yml's coverage + mutation jobs). Keeping them apart keeps `mutmut` — a +# mutation engine that rewrites and executes source — out of every required gate's install closure. +# +# NOT HERE: `semgrep`. It hard-conflicts with the `[otel]` extra (semgrep 1.172.0 requires +# `opentelemetry-sdk>=1.37,<1.38` while `[otel]` resolves 1.44), so a plain group silently DOWNGRADES +# the shipped otel runtime in all four DEP-1 artifacts — measured, bisected to semgrep alone. The only +# fix is `[tool.uv] conflicts = [[{ extra = "otel" }, { group = "..." }]]`, which declares a PRODUCT +# extra and a CI scanner permanently mutually exclusive. Excluded by decision, recorded as a reasoned +# residual in ADR 0034 §3 with the exact recipe so a future owner can flip it in one commit. +# +# Each pin's rationale lives HERE rather than in the workflow: the workflow comments would otherwise +# push every dismissed Scorecard alert anchored below them onto a new line number (ADR 0034's +# convergence rule). +[dependency-groups] +# The scanners the BLOCKING security gates install for themselves. Exact `==` throughout — for each of +# these the VERSION IS THE CONTRACT of a gate that can red a PR: +# bandit an unpinned 1.9.x upgrade silently changed `# nosec` parsing (1.9.x wants space-separated +# test IDs) and broke a green branch; this is the findings baseline of a blocking gate. +# pip-audit `==` makes the audit reproducible — an unpinned auditor can change its advisory-database +# handling between two runs of the same commit. +# zizmor zizmor.yml's gate asserts a CLEAN baseline, so a newly-added rule reds a green PR for a +# reason unrelated to its diff. Verify the tag at +# https://github.com/zizmorcore/zizmor/releases if an install 404s. +# NB: this group also hash-pins `pip` itself (it arrives as a pip-audit → pip-api dependency), which is +# what let security.yml drop two `pip install --upgrade pip` bootstraps outright rather than pin them. +ci-scanners = [ + "bandit==1.9.4", + "pip-audit==2.10.1", + "zizmor==1.5.2", +] +# The ADVISORY measurement tools (quality-advisory.yml). Exact where something PARSES the tool's +# output, a floor where nothing does: +# diff-cover `==`: the inline-annotation surface depends on this version's +# `--format github-annotations:` and on adjacent-line coalescing inside +# GitHubAnnotationsReportGenerator. +# mutmut `==`: the workflow SHELL-PARSES mutmut's human-readable output (the `N/M` progress +# line, `🎉 N`, `: survived`, `: no tests`) and reconciles two independent derivations +# of the killed count. A reword produces a GREEN receipt off a wrong number — exactly +# the failure class that workflow's liveness job exists for. Must stay 3.x: 2.5.1 +# crashes on Python 3.14 in its pony-ORM cache before generating a single mutant. +# pytest-cov floor only — nothing parses it; it just has to emit a `coverage.xml` diff-cover +# reads. Pinned at the current major so a re-lock cannot regress to 6.x, and left a +# floor so Dependabot can move it without a pyproject edit. +# pytest-timeout DELIBERATELY the identical spec to `[project.optional-dependencies].dev`. mutmut 3 +# always passes `--timeout`/`--timeout-method` to pytest and dies inside +# BadTestExecutionCommandsException without the plugin, so naming it here makes this +# lock self-sufficient instead of depending on the editable `[dev]` install having +# supplied it. Same spec in both places ⇒ uv resolves ONE version and they cannot +# disagree. +ci-quality = [ + "diff-cover==10.4.1", + "mutmut==3.6.0", + "pytest-cov>=7.0", + "pytest-timeout>=2.3", +] + [tool.pytest.ini_options] asyncio_mode = "auto" # Run the whole suite on ONE shared asyncio event loop (tests AND fixtures). pytest-asyncio's default diff --git a/uv.lock b/uv.lock index 0518dbe1..8949b977 100644 --- a/uv.lock +++ b/uv.lock @@ -174,6 +174,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3c/d7/8fb3044eaef08a310acfe23dae9a8e2e07d305edc29a53497e52bc76eca7/asyncpg-0.31.0-cp314-cp314t-win_amd64.whl", hash = "sha256:bd4107bb7cdd0e9e65fae66a62afd3a249663b844fa34d479f6d5b3bef9c04c3", size = 706062, upload-time = "2025-11-24T23:26:44.086Z" }, ] +[[package]] +name = "bandit" +version = "1.9.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "pyyaml" }, + { name = "rich" }, + { name = "stevedore" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/aa/c3/0cb80dfe0f3076e5da7e4c5ad8e57bac6ac357ff4a6406205501cade4965/bandit-1.9.4.tar.gz", hash = "sha256:b589e5de2afe70bd4d53fa0c1da6199f4085af666fde00e8a034f152a52cd628", size = 4242677, upload-time = "2026-02-25T06:44:15.503Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/a4/a26d5b25671d27e03afb5401a0be5899d94ff8fab6a698b1ac5be3ec29ef/bandit-1.9.4-py3-none-any.whl", hash = "sha256:f89ffa663767f5a0585ea075f01020207e966a9c0f2b9ef56a57c7963a3f6f8e", size = 134741, upload-time = "2026-02-25T06:44:13.694Z" }, +] + [[package]] name = "bcrypt" version = "5.0.0" @@ -225,6 +240,33 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/27/44/d2ef5e87509158ad2187f4dd0852df80695bb1ee0cfe0a684727b01a69e0/bcrypt-5.0.0-cp39-abi3-win_arm64.whl", hash = "sha256:f2347d3534e76bf50bca5500989d6c1d05ed64b440408057a37673282c654927", size = 144953, upload-time = "2025-09-25T19:50:37.32Z" }, ] +[[package]] +name = "boolean-py" +version = "5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c4/cf/85379f13b76f3a69bca86b60237978af17d6aa0bc5998978c3b8cf05abb2/boolean_py-5.0.tar.gz", hash = "sha256:60cbc4bad079753721d32649545505362c754e121570ada4658b852a3a318d95", size = 37047, upload-time = "2025-04-03T10:39:49.734Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/ca/78d423b324b8d77900030fa59c4aa9054261ef0925631cd2501dd015b7b7/boolean_py-5.0-py3-none-any.whl", hash = "sha256:ef28a70bd43115208441b53a045d1549e2f0ec6e3d08a9d142cbc41c1938e8d9", size = 26577, upload-time = "2025-04-03T10:39:48.449Z" }, +] + +[[package]] +name = "cachecontrol" +version = "0.14.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "msgpack" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2d/f6/c972b32d80760fb79d6b9eeb0b3010a46b89c0b23cf6329417ff7886cd22/cachecontrol-0.14.4.tar.gz", hash = "sha256:e6220afafa4c22a47dd0badb319f84475d79108100d04e26e8542ef7d3ab05a1", size = 16150, upload-time = "2025-11-14T04:32:13.138Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/79/c45f2d53efe6ada1110cf6f9fca095e4ff47a0454444aefdde6ac4789179/cachecontrol-0.14.4-py3-none-any.whl", hash = "sha256:b7ac014ff72ee199b5f8af1de29d60239954f223e948196fa3d84adaffc71d2b", size = 22247, upload-time = "2025-11-14T04:32:11.733Z" }, +] + +[package.optional-dependencies] +filecache = [ + { name = "filelock" }, +] + [[package]] name = "cbor2" version = "6.1.2" @@ -291,6 +333,25 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, ] +[[package]] +name = "chardet" +version = "7.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/19/b6/9df434a8eeba2e6628c465a1dfa31034228ef79b26f76f46278f4ef7e49d/chardet-7.4.3.tar.gz", hash = "sha256:cc1d4eb92a4ec1c2df3b490836ffa46922e599d34ce0bb75cf41fd2bf6303d56", size = 784800, upload-time = "2026-04-13T21:33:39.803Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/70/a8/bf0811d859e13801279a2ae64f37a408027b282f2047bc0001c75dd356ad/chardet-7.4.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d892d3dcd652fdef53e3d6327d39b17c0df40a899dfc919abaeb64c974497531", size = 872887, upload-time = "2026-04-13T21:33:23.328Z" }, + { url = "https://files.pythonhosted.org/packages/51/ac/b9d68ebddfe1b02c77af5bf81120e12b036b4432dc6af7a303d90e2bc38b/chardet-7.4.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:acc46d1b8b7d5783216afe15db56d1c179b9a40e5a1558bc13164c4fd20674c4", size = 853964, upload-time = "2026-04-13T21:33:24.724Z" }, + { url = "https://files.pythonhosted.org/packages/2a/81/17fa103ea9caf5d325a5e4051ab2ba65996fd66baa60b81ee41af1f54e10/chardet-7.4.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ac3bf11c645734a1701a3804e43eabd98851838192267d08c353a834ab79fea", size = 876006, upload-time = "2026-04-13T21:33:26.098Z" }, + { url = "https://files.pythonhosted.org/packages/c2/20/193faab46a68ea550587331a698c3dca8099f8901d10937c4443135c7ed9/chardet-7.4.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6e3bd9f936e04bae89c254262af08d9e5b98f805175ba1e29d454e6cba3107b7", size = 887680, upload-time = "2026-04-13T21:33:27.49Z" }, + { url = "https://files.pythonhosted.org/packages/40/c6/94a3c673327392652ee8bdea9a45bc8a5f5365197a7387d68f0eed007115/chardet-7.4.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:27cc23da03630cdecc9aa81a895aa86629c211f995cd57651f0fbc280717bf93", size = 879865, upload-time = "2026-04-13T21:33:29.052Z" }, + { url = "https://files.pythonhosted.org/packages/b1/2c/cad8b5e3623a987f3c930b68e2bdd06cfc388cd91cd42ed05f1227701b73/chardet-7.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:b95c934b9ad59e2ba8abb9be49df70d3ad1b0d95d864b9fdb7588d4fa8bd921c", size = 939594, upload-time = "2026-04-13T21:33:31.391Z" }, + { url = "https://files.pythonhosted.org/packages/33/e0/d06e42fd6f02a58e5e227e5106587751cb38adcff0aaf949add744b78b6e/chardet-7.4.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c77867f0c1cb8bd819502249fcdc500364aedb07881e11b743726fa2148e7b6e", size = 889714, upload-time = "2026-04-13T21:33:32.772Z" }, + { url = "https://files.pythonhosted.org/packages/d4/ed/40d091954d48abea037baae6be8fb79905e5f78d34d12ea955132c7d8011/chardet-7.4.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:cf1efeaf65a6ef2f5b9cc3a1df6f08ba2831b369ccaa4c7018eaf90aa757bb11", size = 872319, upload-time = "2026-04-13T21:33:34.427Z" }, + { url = "https://files.pythonhosted.org/packages/bb/77/82a46821dbfbdfe062710d2bf2ede13426304e3567a23c57d919c0c31630/chardet-7.4.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9f3504c139a2ad544077dd2d9e412cd08b01786843d76997cd43bb6de311723c", size = 892021, upload-time = "2026-04-13T21:33:35.766Z" }, + { url = "https://files.pythonhosted.org/packages/49/57/42d30c562bda5b4a839766c1aad8d5856b798ad2a1c3247b72a679afec94/chardet-7.4.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457f619882ba66327d4d8d14c6c342269bdb1e4e1c38e8117df941d14d351b04", size = 902509, upload-time = "2026-04-13T21:33:37.096Z" }, + { url = "https://files.pythonhosted.org/packages/8c/6c/0a40afdb50a0fe041ab95553b835a8160b6cf0e81edf2ae2fe9f5224cbf9/chardet-7.4.3-py3-none-any.whl", hash = "sha256:1173b74051570cf08099d7429d92e4882d375ad4217f92a6e5240ccfb26f231e", size = 626562, upload-time = "2026-04-13T21:33:38.559Z" }, +] + [[package]] name = "charset-normalizer" version = "3.4.7" @@ -353,6 +414,45 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] +[[package]] +name = "coverage" +version = "7.15.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/76/d0/55fe630f4cf94e3fcba868240fad8c8cdd1f764e2a932f8926347e6ec4cd/coverage-7.15.2.tar.gz", hash = "sha256:3df60dc267f0a2ca23cb7a9ab1109c62b9335ffbf519fcfe167157c28c09b81d", size = 927741, upload-time = "2026-07-15T18:56:19.558Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/5f/aed265fd7a3551a394f36dfe41868aee709b7f95db4052205b4ad1563ac3/coverage-7.15.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:40f633c5c5fc783732f6312280122e859538fa24461235597c13d803ea9a108a", size = 221650, upload-time = "2026-07-15T18:55:14.527Z" }, + { url = "https://files.pythonhosted.org/packages/6b/2c/222ba12a545189017120f8eddfc1a0bd4616b47d5d4a8d99421edb2fe4c6/coverage-7.15.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:075560438765b7a2ef43bf7aa7758661b53d889df47f062a31bda6c1ade553a2", size = 221988, upload-time = "2026-07-15T18:55:16.674Z" }, + { url = "https://files.pythonhosted.org/packages/aa/38/304b5877ab46e6c290b4292cfcf3fe28245f0e5597cad7f6acc91fc7e0a4/coverage-7.15.2-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:25fd15dd40a0a2c51a500d664ca29053c09c3259d998407bf982b6e114696138", size = 253029, upload-time = "2026-07-15T18:55:18.856Z" }, + { url = "https://files.pythonhosted.org/packages/6c/58/821b533b8db9e44cf1d8a97bd525149ced40dde1d0093da02cb78e715244/coverage-7.15.2-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b9a6367e4aff723e8ee8190836836124284e8fcd4265e307c844010cfa074f3f", size = 255536, upload-time = "2026-07-15T18:55:21.027Z" }, + { url = "https://files.pythonhosted.org/packages/f1/f2/7aa06604c389d32ea7f0a6a988359a7eafc3cd3f8e7bc2e88cd2fdf0b877/coverage-7.15.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9854ca62c152874b2060772503535be2e8f53f70b8aaa7686b094888d872f984", size = 256881, upload-time = "2026-07-15T18:55:23.125Z" }, + { url = "https://files.pythonhosted.org/packages/a2/4f/1ef342339c7916d0096bc5888cc0f653882cc7bc8f897d5cb89143287c9b/coverage-7.15.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:913b6c56e110da40e035bbd168353bf7aaa2544a5eaccea5d98a4629aac156c7", size = 259196, upload-time = "2026-07-15T18:55:25.099Z" }, + { url = "https://files.pythonhosted.org/packages/fe/f4/7ed055d7a9c5ec13b161773a115a5ccc6b0081d568c31fad830806306cc7/coverage-7.15.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:aaccad4129d735a8a4d526f26929894c9a4e8ef7034566f210b176749d6906e3", size = 253036, upload-time = "2026-07-15T18:55:27.018Z" }, + { url = "https://files.pythonhosted.org/packages/14/79/ea82cca18c242a3a38b6c017da39726aa62dcb64aa635abf79b92009975c/coverage-7.15.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a164b50081fc7357331c4024ef4d17b78ba325f8380d05f5a69599a7e05257ee", size = 254887, upload-time = "2026-07-15T18:55:29.084Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ba/a136db3c0d9562b00e10b72540dbf3a33cd3bc5b95060c9308e247494623/coverage-7.15.2-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:bfd341ccf78128e72c094bc70cc25b3ef309c33c7c2c66ba3ed4309549e02de1", size = 252852, upload-time = "2026-07-15T18:55:31.184Z" }, + { url = "https://files.pythonhosted.org/packages/17/17/ea334246b16b7d059953fad6fdefa11e33c68efbd3fe37b1098120a1fac2/coverage-7.15.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:1473b3ba8e7ee0f076117b1a72c23f579a2b9e2bb742f48a8d86ea27ca93f91a", size = 257128, upload-time = "2026-07-15T18:55:33.163Z" }, + { url = "https://files.pythonhosted.org/packages/ed/c3/074fb66d46d607855f710876b117cbda562c5ab08363528e78820449f937/coverage-7.15.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:17c432b5f73ad52ef46fb06019f6fa7c66ce381961cf0f7dfd1d3a4bd3a98145", size = 252668, upload-time = "2026-07-15T18:55:35.063Z" }, + { url = "https://files.pythonhosted.org/packages/e1/c1/f620850ada9b36435921c9a3a8057013422b1d964eb4bf37fe138724d192/coverage-7.15.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:77f0ef5011df53a4bd1b35211ab122287f8d9b8d7aa1c4553e5c2deb24b1d446", size = 254325, upload-time = "2026-07-15T18:55:37.125Z" }, + { url = "https://files.pythonhosted.org/packages/cc/31/a729ca3689404493af82ef8e6ff70bd88bdda8da89aeef6ca9b387aeb2b4/coverage-7.15.2-cp314-cp314-win32.whl", hash = "sha256:f653e5d7248c1191ec988a85c72edeab46c3ff44f90639a4ed4874ec0be90243", size = 223844, upload-time = "2026-07-15T18:55:39.078Z" }, + { url = "https://files.pythonhosted.org/packages/c6/83/5d809dc808fb1698c671f3e372259bb9158e64b7ea526fc6ab7de64de9fe/coverage-7.15.2-cp314-cp314-win_amd64.whl", hash = "sha256:9911f31aad8906abe337c271343485cf20df5e70df5d2f57f9f136e7b55f26bc", size = 224331, upload-time = "2026-07-15T18:55:41.346Z" }, + { url = "https://files.pythonhosted.org/packages/16/4e/35e488548e952795829e129995c4174df33bf432b591d1aa42c8d9e4e7ad/coverage-7.15.2-cp314-cp314-win_arm64.whl", hash = "sha256:e38def96ad59853824c97953fdcd2c320a84ba3ce99b417db78af8bb6c3db635", size = 223760, upload-time = "2026-07-15T18:55:43.518Z" }, + { url = "https://files.pythonhosted.org/packages/ed/49/dd2c86cd6374038f6e415fb5bfb86db5218553209c081384a020369dee79/coverage-7.15.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:835ec4e20b45f0a7f63ed78f94065aca00de033403df8377bfe8b9c6abc0a7be", size = 222384, upload-time = "2026-07-15T18:55:45.569Z" }, + { url = "https://files.pythonhosted.org/packages/d3/74/173ff17a1c0808e5a438f549f6f145d5ac7528f2791310b63523e3200ac7/coverage-7.15.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7466cc7ab6dc0db871d264bf99e8779f0917ee63d40730af0552f71535a6e072", size = 222647, upload-time = "2026-07-15T18:55:47.544Z" }, + { url = "https://files.pythonhosted.org/packages/84/f8/b8cba872162356fb44ac79c10309d987206a4461e32072fc29228dad7331/coverage-7.15.2-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e370c12133095ff18432de8c044962be85a5a96d90c6fcbce8e17e76236d2328", size = 264013, upload-time = "2026-07-15T18:55:49.768Z" }, + { url = "https://files.pythonhosted.org/packages/ee/67/a807a7586d0b8cae485308ddd55756f0806c92f8e0b411bacbf23c48edf3/coverage-7.15.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fe41909c9515c3bfdb5f02c4d1f857dba322d9a9a1178069b91eea77889df63a", size = 266135, upload-time = "2026-07-15T18:55:51.941Z" }, + { url = "https://files.pythonhosted.org/packages/ce/67/cd78771dc985f7e4ebdcc82b1a96d9a932af9e806f01f2f91a89f4c72e80/coverage-7.15.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6aa28cfb6488e5453b5b762d65f73aa586380f6693a04d58078ce228a29b06c0", size = 268555, upload-time = "2026-07-15T18:55:54.065Z" }, + { url = "https://files.pythonhosted.org/packages/18/3e/10134cf81275188c58568f324fc74aedff32c63ca4d5bbc513a91944a6f0/coverage-7.15.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bcc0aae933921d03096f53b0b03eeb702129fd406dee59f08d2efacc68681fa5", size = 269674, upload-time = "2026-07-15T18:55:56.066Z" }, + { url = "https://files.pythonhosted.org/packages/75/4a/771b77de446cba985dc414bbc5844bd21604da05dbc044286df8318a48a7/coverage-7.15.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7c63387e21ab21f512c69c9756a8c7dadd322c7275edb064064433c9a09c3743", size = 263101, upload-time = "2026-07-15T18:55:58.107Z" }, + { url = "https://files.pythonhosted.org/packages/5f/b5/70a7011da15f4071943361183aefa27847f3e3aec4fd335f1cb3d3a622b1/coverage-7.15.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0e55510bc98ae943cece9e667a6c0fe94c6a92913720dea34243657a17993d0c", size = 266007, upload-time = "2026-07-15T18:56:00.468Z" }, + { url = "https://files.pythonhosted.org/packages/b4/0d/f9547e804ce7ad49646ffeffac26699510efbe6c0f751b66fdc960c4e825/coverage-7.15.2-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:2ff08701be2d1556fc78b326c80a3e8042da09352ecb3819105f8e386c8a3071", size = 263611, upload-time = "2026-07-15T18:56:02.615Z" }, + { url = "https://files.pythonhosted.org/packages/ac/59/f576a396659c0efd351f5c1544f67c3560e89c7761cabf7f65e412beeda5/coverage-7.15.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:38c9518b7103826c403a461544e3c2e77151e8676d06eaed85911a97e962584a", size = 267344, upload-time = "2026-07-15T18:56:04.622Z" }, + { url = "https://files.pythonhosted.org/packages/7c/5d/c2e4fce3579c0cb635024293f1a32bbe26df101b3e3a69f22243d1352b6c/coverage-7.15.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:dee88b1ed88587abd8c0269a1fc1f4cc77f7750d1dfde2869e2a123af420e67d", size = 262456, upload-time = "2026-07-15T18:56:06.641Z" }, + { url = "https://files.pythonhosted.org/packages/bb/dd/956287d69436b66094bc4b57ac2da71e43bfd2a5524e958900b9f582fcf8/coverage-7.15.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2fbeeeecea279727f8ac16c8e1133ddfeee793e985c86ae343d6a5ce744eef8c", size = 264771, upload-time = "2026-07-15T18:56:08.795Z" }, + { url = "https://files.pythonhosted.org/packages/2c/5a/6f979530c2734c575de77cf58f5f28d51f7123a94b5030fd9156fe5f363c/coverage-7.15.2-cp314-cp314t-win32.whl", hash = "sha256:cb0fddaa6884be6aae36ced9544b5e90f7d5f03845a2853bf47a14953a4e8688", size = 224151, upload-time = "2026-07-15T18:56:10.856Z" }, + { url = "https://files.pythonhosted.org/packages/54/7e/27f6b2a74d484742f4017553e710b01e396b23d809df3e95ca0bb9a2824b/coverage-7.15.2-cp314-cp314t-win_amd64.whl", hash = "sha256:77f091ea3a9cc611cd29f433565476bc1936c084ac8eee00ea0e7e70c27e4199", size = 224981, upload-time = "2026-07-15T18:56:12.928Z" }, + { url = "https://files.pythonhosted.org/packages/b1/48/284863423aa474240f6842bd00d680da22f4e6ea2e466618ef7c9c9e69a9/coverage-7.15.2-cp314-cp314t-win_arm64.whl", hash = "sha256:6fc448c377d6eeb00a47c673494bd9bae29280ca53987e1869e67ebedfe20658", size = 224294, upload-time = "2026-07-15T18:56:15.156Z" }, + { url = "https://files.pythonhosted.org/packages/ec/82/32e3bd191d498e64f6f911ad55d14006a0861e54869d2d32452326399e65/coverage-7.15.2-py3-none-any.whl", hash = "sha256:eb6bcae8d1a9d305351ecb108232441d11c5cfe9de840a04388ba5d2db8d735c", size = 213375, upload-time = "2026-07-15T18:56:17.305Z" }, +] + [[package]] name = "cryptography" version = "49.0.0" @@ -403,6 +503,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c2/e6/f60198ea8d9dfa15fff9ed4ca02ce362f6eadd9ba757dcc50634c4257b63/cryptography-49.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:026ac7423e6fa66872d3bf889be5974507da3944f866f704fa200eadacd00001", size = 3785547, upload-time = "2026-06-12T20:02:26.847Z" }, ] +[[package]] +name = "cyclonedx-python-lib" +version = "11.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "license-expression" }, + { name = "packageurl-python" }, + { name = "py-serializable" }, + { name = "sortedcontainers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/75/c9/5d0ccdd19bc7d8ab803b90695c1706aa2ea8529685d18e682dc2524d2630/cyclonedx_python_lib-11.11.0.tar.gz", hash = "sha256:4b3194db72b613717f2912447e67ab618c75ff7dcac6c4af3c0e9e1ac617c102", size = 1442983, upload-time = "2026-06-17T11:57:49.055Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/f3/56ccb2884aaa3db5622368e5191a3384b15f35392aa93df8b2f508c660d2/cyclonedx_python_lib-11.11.0-py3-none-any.whl", hash = "sha256:3049fc83e06a059b5c5907a527625a8ed5073caab10607ed4c9e5503b590fd44", size = 528689, upload-time = "2026-06-17T11:57:47.358Z" }, +] + [[package]] name = "defusedxml" version = "0.7.1" @@ -412,6 +527,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604, upload-time = "2021-03-08T10:59:24.45Z" }, ] +[[package]] +name = "diff-cover" +version = "10.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "chardet" }, + { name = "jinja2" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/85/f9/9f49b333bd03e9bbc1bcc3cde14b4927239195f374cb88e8f4aee57550be/diff_cover-10.4.1.tar.gz", hash = "sha256:0ec566955c9ee7da2f6cc48fa16fac7f97ad1fc4e50a887ffb9cfe5eb1e831df", size = 108279, upload-time = "2026-07-24T03:58:08.376Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f5/71/bce893908031195b86a8222ac46ebf689c69b0ca10670c1893c56eb87d77/diff_cover-10.4.1-py3-none-any.whl", hash = "sha256:dc8f2654c485ec4f16e679b5af6e205783cde71185d4ceb8157662dca2d531e9", size = 59875, upload-time = "2026-07-24T03:58:07.163Z" }, +] + [[package]] name = "elementpath" version = "5.1.3" @@ -474,6 +604,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1e/30/7ac78056464c6fd3062701af46574a779cbb378785d58920e8d8141792f7/fhirpathpy-2.2.3-py3-none-any.whl", hash = "sha256:da223da6fcf5efe4db6c1a19994ecebcf61cabb795103902f105485ad7675a10", size = 232575, upload-time = "2026-06-29T16:56:03.932Z" }, ] +[[package]] +name = "filelock" +version = "3.32.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/57/3ba6e6cb097f85b855b00163d169f35365f44277df044dcf96d55b8f62a3/filelock-3.32.2.tar.gz", hash = "sha256:c33351e1f49cae33414acbc6d56784e6ecee82514ec90795da1161fc4836b5b8", size = 217172, upload-time = "2026-07-29T22:46:04.895Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/e8/72f8cef9fdfeffe06213fe8508039396ee48daa0e3259457ed766173bfd6/filelock-3.32.2-py3-none-any.whl", hash = "sha256:87dd94cf281e586d135fa51132b8e3d9a598b316e90377a288663c9321036c82", size = 98830, upload-time = "2026-07-29T22:46:03.52Z" }, +] + [[package]] name = "googleapis-common-protos" version = "1.75.0" @@ -623,6 +762,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5a/de/bbc12563bbf979618d17625a4e753ff7a078523e28d870d3626daa97261a/invoke-3.0.3-py3-none-any.whl", hash = "sha256:f11327165e5cbb89b2ad1d88d3292b5113332c43b8553b494da435d6ec6f5053", size = 160958, upload-time = "2026-04-07T15:17:46.875Z" }, ] +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + [[package]] name = "ldap3" version = "2.9.1" @@ -635,6 +786,31 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/4e/f6/71d6ec9f18da0b2201287ce9db6afb1a1f637dedb3f0703409558981c723/ldap3-2.9.1-py2.py3-none-any.whl", hash = "sha256:5869596fc4948797020d3f03b7939da938778a0f9e2009f7a072ccf92b8e8d70", size = 432192, upload-time = "2021-07-18T06:34:12.905Z" }, ] +[[package]] +name = "libcst" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/02/c0/098e5c91ff1537f00c85a6438b6cb1863d17144680cc91f47c87f104a200/libcst-1.9.0.tar.gz", hash = "sha256:087b58a9afe076bb08e2d726478e1f16cb928d67ffa9092817e033c335de522a", size = 914739, upload-time = "2026-07-29T21:28:43.153Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/65/2a/4943c71d90975bc59034a057dea346b365c276f308c1d31f5cf2bd85492d/libcst-1.9.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:eccf4c57d273cdd3fe1c67b72cf9bb1bbd4547aa011824e96ccf5b7136057aa4", size = 2044529, upload-time = "2026-07-29T19:25:11.04Z" }, + { url = "https://files.pythonhosted.org/packages/be/ed/1168b98c2a0f338be3a44753647baab051feaf6167cc887bf4447f8fd920/libcst-1.9.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:32395244edfe6538e0ea2bf82051d60103d3f54861274805c4fb3745efb70a85", size = 2203519, upload-time = "2026-07-29T19:25:12.543Z" }, + { url = "https://files.pythonhosted.org/packages/25/7d/2eaa697a80f899bcf2245680bbfcc1e478eb3b3328c21d4b2880bdf00dac/libcst-1.9.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:444e84c76cd035cd2fe136838c1a524d34b08216521f6f5093df8a6f6cfa5799", size = 2255879, upload-time = "2026-07-29T19:25:14.137Z" }, + { url = "https://files.pythonhosted.org/packages/90/03/793b9fd96dd52d202f5f2b88d7e54f05ebed767d1bb3b32395a1817bf6ab/libcst-1.9.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:bb5d0946f2b4c6711b5d69fe4f833b364f9e7a1a2b08f88b98619dc18975099a", size = 2269807, upload-time = "2026-07-29T19:25:15.647Z" }, + { url = "https://files.pythonhosted.org/packages/a3/f4/1bc7aaea03971c45e8a885fed9fc1c73176dbbd00b0296a3cde9529bafd6/libcst-1.9.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:45808c03528b3ad40b14095348a918e08d98c47b4a125d631cb78e817c0a5b16", size = 2378171, upload-time = "2026-07-29T19:25:17.289Z" }, + { url = "https://files.pythonhosted.org/packages/8e/f7/bc49e367d2bc8817213594dd52cf6b2da2dbbda90b5382d60653999274ac/libcst-1.9.0-cp314-cp314-win_amd64.whl", hash = "sha256:568288cdbfe3b4ca3ae4852cb0a439ff053dd54c841bc4995bf2b71238b5de40", size = 2177847, upload-time = "2026-07-29T19:25:19.35Z" }, + { url = "https://files.pythonhosted.org/packages/87/80/4d81577a22e6d535d1a3409f3a1c6903e09036f0e17fc47f92169dbc3501/libcst-1.9.0-cp314-cp314-win_arm64.whl", hash = "sha256:107593af46945593e7825821793393262bc4fa1d3ea24c3ed487b61269bbbdf8", size = 2058134, upload-time = "2026-07-29T19:25:20.769Z" }, + { url = "https://files.pythonhosted.org/packages/d1/7f/c3f3a0e7a1a2adaa76e815e82a7814d6bfe7d49432276bba652248c68d0b/libcst-1.9.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f6248cb07444ab9a6733a855737a9febed8b9adca51347019348b09a3ac7dfe9", size = 2036102, upload-time = "2026-07-29T19:25:22.212Z" }, + { url = "https://files.pythonhosted.org/packages/4e/af/2f5543255b2c7d749b966adeccc6bfb1652cf8b425afb913d0f3f025a865/libcst-1.9.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:496c24e0d3240bc7da45dae543aff3f5b6509978c39262d6b84ed2fb999dded2", size = 2194806, upload-time = "2026-07-29T19:25:24.017Z" }, + { url = "https://files.pythonhosted.org/packages/cb/5b/03f4cddce426d005e208b39ea7b2d456e667cdee0f1891360f0fc8430f20/libcst-1.9.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:ea490fa8540503db5f321f0268becab46eb50f710e8cec8041e241fd66f6874f", size = 2246762, upload-time = "2026-07-29T19:25:25.365Z" }, + { url = "https://files.pythonhosted.org/packages/d8/31/9d5fe1e43dc3dbcc74f70f3e0e73fcdd8d84effc1059d8b45974f0d0d2eb/libcst-1.9.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:50ab94bb2524b419056d4003032b8c67102ac800f8d85b3c4260a01746d94dbb", size = 2259633, upload-time = "2026-07-29T19:25:26.928Z" }, + { url = "https://files.pythonhosted.org/packages/a8/2b/6752b28d88c3a19b3bc0b9e1838443b6760d5c862b4f4b37955402659e24/libcst-1.9.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a2faaf92500d0226358125630f5aab4758e8aad3f2d70a10892ec3c700781a54", size = 2368043, upload-time = "2026-07-29T19:25:28.344Z" }, + { url = "https://files.pythonhosted.org/packages/e2/94/775825b2637f8ab05694b6a4b3802ae6783b4e799f9b58d2400c7e2d4369/libcst-1.9.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0c7b548512db25af9c2997a95fa731bd6b6928ecbad6c0915d7482d8bb42d34f", size = 2176432, upload-time = "2026-07-29T19:25:29.921Z" }, + { url = "https://files.pythonhosted.org/packages/fb/3d/88ad67427c6fd9db929e087912b0a540e5140e5cb77e7ca4170edaac8531/libcst-1.9.0-cp314-cp314t-win_arm64.whl", hash = "sha256:497d5329345f1f5df84e41b0bbd00204b64a2fd30dfe3cfaeaebca633a31e877", size = 2052931, upload-time = "2026-07-29T19:25:31.397Z" }, +] + [[package]] name = "librt" version = "0.13.0" @@ -670,6 +846,30 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5f/5d/3dcec2884ba1b0806d1408612555c38dd5d68e90156b59f75f6e36435c3a/librt-0.13.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2f281549a4c52ac7bb97997f14353f8bd0e53a34ca0dad1c905cfd0b4a58ae99", size = 110771, upload-time = "2026-07-08T12:26:12.303Z" }, ] +[[package]] +name = "license-expression" +version = "30.4.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "boolean-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/40/71/d89bb0e71b1415453980fd32315f2a037aad9f7f70f695c7cec7035feb13/license_expression-30.4.4.tar.gz", hash = "sha256:73448f0aacd8d0808895bdc4b2c8e01a8d67646e4188f887375398c761f340fd", size = 186402, upload-time = "2025-07-22T11:13:32.17Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/40/791891d4c0c4dab4c5e187c17261cedc26285fd41541577f900470a45a4d/license_expression-30.4.4-py3-none-any.whl", hash = "sha256:421788fdcadb41f049d2dc934ce666626265aeccefddd25e162a26f23bcbf8a4", size = 120615, upload-time = "2025-07-22T11:13:31.217Z" }, +] + +[[package]] +name = "linkify-it-py" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "uc-micro-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2e/c9/06ea13676ef354f0af6169587ae292d3e2406e212876a413bf9eece4eb23/linkify_it_py-2.1.0.tar.gz", hash = "sha256:43360231720999c10e9328dc3691160e27a718e280673d444c38d7d3aaa3b98b", size = 29158, upload-time = "2026-03-01T07:48:47.683Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b4/de/88b3be5c31b22333b3ca2f6ff1de4e863d8fe45aaea7485f591970ec1d3e/linkify_it_py-2.1.0-py3-none-any.whl", hash = "sha256:0d252c1594ecba2ecedc444053db5d3a9b7ec1b0dd929c8f1d74dce89f86c05e", size = 19878, upload-time = "2026-03-01T07:48:46.098Z" }, +] + [[package]] name = "lxml" version = "6.1.1" @@ -714,6 +914,74 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7f/2c/0f1e93c636720e8a3eb59af2bfda99d98b55891e1c53bc30c2e0e865f01b/lxml-6.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:58bb955caba94e467d2a96da17660d2d704e0675894cba21ab8a775b8621fd1c", size = 3817223, upload-time = "2026-05-19T19:22:56.823Z" }, ] +[[package]] +name = "markdown-it-py" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" }, +] + +[package.optional-dependencies] +linkify = [ + { name = "linkify-it-py" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "mdit-py-plugins" +version = "0.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/59/fc/f8d0863f8862f25602c0404d75568e89fb6b4109804645e5cdfb1be5cf56/mdit_py_plugins-0.6.1.tar.gz", hash = "sha256:a2bca0f039f39dbd35fb74ae1b5f998608c437463371f0ff7f49a19a17a114d0", size = 56114, upload-time = "2026-05-13T09:03:38.91Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/69/6da5581c6a7fede7dc261bf4e67d6adca4196f176b43288b55b3db395b6e/mdit_py_plugins-0.6.1-py3-none-any.whl", hash = "sha256:214c82fb2ac524472ab6a5bcab1de80f73b50443e187f401bfd77efbc7c6481d", size = 66663, upload-time = "2026-05-13T09:03:37.76Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + [[package]] name = "messagefoundry" source = { editable = "." } @@ -788,6 +1056,19 @@ xml = [ { name = "xmlschema" }, ] +[package.dev-dependencies] +ci-quality = [ + { name = "diff-cover" }, + { name = "mutmut" }, + { name = "pytest-cov" }, + { name = "pytest-timeout" }, +] +ci-scanners = [ + { name = "bandit" }, + { name = "pip-audit" }, + { name = "zizmor" }, +] + [package.metadata] requires-dist = [ { name = "aioodbc", marker = "extra == 'sqlserver'", specifier = ">=0.5" }, @@ -835,6 +1116,66 @@ requires-dist = [ ] provides-extras = ["dev", "dicom", "fhir", "harness", "otel", "postgres", "sftp", "sqlserver", "vault", "webauthn", "x12", "xml"] +[package.metadata.requires-dev] +ci-quality = [ + { name = "diff-cover", specifier = "==10.4.1" }, + { name = "mutmut", specifier = "==3.6.0" }, + { name = "pytest-cov", specifier = ">=7.0" }, + { name = "pytest-timeout", specifier = ">=2.3" }, +] +ci-scanners = [ + { name = "bandit", specifier = "==1.9.4" }, + { name = "pip-audit", specifier = "==2.10.1" }, + { name = "zizmor", specifier = "==1.5.2" }, +] + +[[package]] +name = "msgpack" +version = "1.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/31/f9/c0a1c127f9049db9155afc316952ea571720dd01833ff5e4d7e8e6352dbb/msgpack-1.2.1.tar.gz", hash = "sha256:04c721c2c7448767e9e3f2520a475663d8ee0f09c31890f6d2bd70fd636a9647", size = 183960, upload-time = "2026-06-18T16:13:52.594Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/58/cce442852c6b9e1639c7c8ac8fd9143121cb32dab0f308df4d1426a8eb9c/msgpack-1.2.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:05f340e47e7e47d2da8db9b53e1bb1d294369e9ef45a747441309f6650b8351d", size = 83610, upload-time = "2026-06-18T16:13:25.724Z" }, + { url = "https://files.pythonhosted.org/packages/60/5c/15b4c7a0182f75ffa90751958ba36a9c01cafee367d49a3edc10ed140b01/msgpack-1.2.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:810b916696c86ef0deb3b74588480224df4c1b071136c34183e4a2a4284d7ac7", size = 83138, upload-time = "2026-06-18T16:13:26.781Z" }, + { url = "https://files.pythonhosted.org/packages/b8/a6/99e58722feaffc5f2fbcc0c8c0d1451ab9f84097f7af87291b46af2390f4/msgpack-1.2.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ca0dacff965c47afdc3749a8469d7302a8f801d6a28758d55120d75e66ce6889", size = 406090, upload-time = "2026-06-18T16:13:28.072Z" }, + { url = "https://files.pythonhosted.org/packages/19/03/8c63e8cf52958534ef688625965ab04c269a6cadd8caef16758b380a821a/msgpack-1.2.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e2bf9280bceb5efca998435904b5d3e9fdbcc11d90dc9df30aec7973252b720", size = 412106, upload-time = "2026-06-18T16:13:29.427Z" }, + { url = "https://files.pythonhosted.org/packages/63/d2/155d9e71b40e41fd934bc0c48b9b2770f22263e1ac20aad8e29fdca7be3f/msgpack-1.2.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:aa6c4be5d1c02a42b066ca6ddb71adf36432868fdcdb6ee87e634e86e0674190", size = 374851, upload-time = "2026-06-18T16:13:30.631Z" }, + { url = "https://files.pythonhosted.org/packages/98/48/deaf2326262a8d5ea3295ce9649912ecd3f551ba7ec8e33c665d2ba583f3/msgpack-1.2.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec0e675d59150a6269ddc9139087c722292664a37d071a849c05c473350f1f2d", size = 396168, upload-time = "2026-06-18T16:13:31.977Z" }, + { url = "https://files.pythonhosted.org/packages/10/2a/b4410f906c2ec0008f1608d3ab5143afc3ad3f4e6da0fed3ea2231d0bef4/msgpack-1.2.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:dd3bfe82d53edfe4b7fc9a7ec9761e23a7a5b1dac22264505af428253c29ed24", size = 371959, upload-time = "2026-06-18T16:13:33.282Z" }, + { url = "https://files.pythonhosted.org/packages/59/86/1edc67270099a528fa2093ea60fe191233cd238e4bd30cfacf7db79fc959/msgpack-1.2.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5ad5467fc3f68b5468e06c5f788d712e9f8ffc8b0cd1bcb160c105c1ee92dae7", size = 408457, upload-time = "2026-06-18T16:13:34.567Z" }, + { url = "https://files.pythonhosted.org/packages/82/90/8b630fef07d8c5ab457b71ff2c217910c83d333c7a68472c186e87cc504a/msgpack-1.2.1-cp314-cp314-win32.whl", hash = "sha256:98b58bdb89c46190e4609bb36abe17c6d4105ad13f9c5f8f6f64d320f8ced3fb", size = 65942, upload-time = "2026-06-18T16:13:36.056Z" }, + { url = "https://files.pythonhosted.org/packages/16/f1/467b81e98b24dd3885d7b1857728797b4ffc76a7a7483af4fb321a07de3c/msgpack-1.2.1-cp314-cp314-win_amd64.whl", hash = "sha256:74847557e28ce71bd3c438a447ca90e4b507e997ddbdef8a12a7b283b86c156b", size = 72627, upload-time = "2026-06-18T16:13:37.079Z" }, + { url = "https://files.pythonhosted.org/packages/a7/1d/5d8c4c89985feb6acefb82a09e501c60392261856d2408d20bfe4f0360b1/msgpack-1.2.1-cp314-cp314-win_arm64.whl", hash = "sha256:b50b727bd652bdc37d950336c848ef20ec54a4cafc38dce19b1cd86ad625d0f7", size = 66908, upload-time = "2026-06-18T16:13:38.23Z" }, + { url = "https://files.pythonhosted.org/packages/1b/02/ad2afb678b4de94496cd432b581759b756a92c1192d8c767edd6b132efdc/msgpack-1.2.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:8d00f177ca88a77c1cf848d204a38f249751650b601cb6532acc68805d8a8273", size = 86000, upload-time = "2026-06-18T16:13:39.44Z" }, + { url = "https://files.pythonhosted.org/packages/54/74/0b797484013128837f3b1cbb6cea019277c4de4e377dc512b4d9a0f92940/msgpack-1.2.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5bb9c386f0a329c035ddbab4b72d1028bf9627add8dda41070288563d57ed1b1", size = 86544, upload-time = "2026-06-18T16:13:40.447Z" }, + { url = "https://files.pythonhosted.org/packages/a9/b4/b774d7eb95561739907fec675582f83203cf41c597a418c2589b4bfb8e9d/msgpack-1.2.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:20466cca18c49c7292a8984bc15d65857b171e7264bdcb5f96baf8be238791fc", size = 427661, upload-time = "2026-06-18T16:13:41.574Z" }, + { url = "https://files.pythonhosted.org/packages/b2/f9/3243191dc9937e00756c8bc1b0272fed8f23758e43df2a3b46f533e5090f/msgpack-1.2.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:196300e7e5d6e74d50f1607ab9c06c4a1484c383cd22defd727902591f7e8dde", size = 426375, upload-time = "2026-06-18T16:13:42.936Z" }, + { url = "https://files.pythonhosted.org/packages/23/c7/1693111db9944ba4ad4b67a1e788400d78a0b6af7a6523dc7e4e58f8274b/msgpack-1.2.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:575957e79cd51903a4e8495a242442949641e08f1efd5197b43bebd3ea7682b4", size = 380495, upload-time = "2026-06-18T16:13:44.306Z" }, + { url = "https://files.pythonhosted.org/packages/3e/2b/92f86956a0c13e8662f7e2ad630c4eb4db07497b967589bd5245e018b2c1/msgpack-1.2.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8c2ed1e48cc0f460bf3c7780e7137ff21a4e18433451916f2442c1b21036cd7d", size = 410897, upload-time = "2026-06-18T16:13:45.629Z" }, + { url = "https://files.pythonhosted.org/packages/da/ea/1479f72d200313a76fc2f823a79d1e07ed052ab7b8a0280640aa7b95de42/msgpack-1.2.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:5f6277e5f783c36786a145e0247fc189a03f35f84b251646e53592d2bc12b355", size = 378519, upload-time = "2026-06-18T16:13:46.998Z" }, + { url = "https://files.pythonhosted.org/packages/f5/4d/fa006060ffa1011d32bfae826fe766fe73e02982183601633b7121058ab3/msgpack-1.2.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f9389552ecf4784886345ead0647e4edc96bee37cbab05b75540f542f766c48c", size = 419815, upload-time = "2026-06-18T16:13:48.205Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/aab6c946570496b78e67804721f3d5e2d62a93081b9b37df77764ef56347/msgpack-1.2.1-cp314-cp314t-win32.whl", hash = "sha256:c1c79a604a2969a868a78b6ebd27a887e00c624f14f66b3038e0590cb23332d1", size = 70914, upload-time = "2026-06-18T16:13:49.385Z" }, + { url = "https://files.pythonhosted.org/packages/13/0a/e608956488a2af014cfe6e3d665e090b8ee42aa14b07f8f95b8880d66b09/msgpack-1.2.1-cp314-cp314t-win_amd64.whl", hash = "sha256:f12038a35fabd52e56a3547bab42401af49a45caa6dd00b34c44de235bc93ee2", size = 77999, upload-time = "2026-06-18T16:13:50.467Z" }, + { url = "https://files.pythonhosted.org/packages/d2/8a/27e2e57055176e366a46b85d02d68e7a5bcfbdd8474c9706375d965f24d3/msgpack-1.2.1-cp314-cp314t-win_arm64.whl", hash = "sha256:0adcf06ffde0777c0e1a9b771a2b1c4226ba1bbf748c8efcc02fcdeca3299107", size = 71160, upload-time = "2026-06-18T16:13:51.498Z" }, +] + +[[package]] +name = "mutmut" +version = "3.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "coverage" }, + { name = "libcst" }, + { name = "pytest" }, + { name = "setproctitle" }, + { name = "textual" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/af/b0/ebcae42b90b07756b7aa10c4176835f436332e6c1cb28bc35bae83462382/mutmut-3.6.0.tar.gz", hash = "sha256:bcbd3e4d0d2d4edf3dfb42955417279a8866a3dbbcb87d619f2f3fd0ac7fafda", size = 51538, upload-time = "2026-06-06T07:44:51.798Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/5a/a0caa3f9db407b5d12c311bd4c87aa67fdd6e3f329377149e303108a1c51/mutmut-3.6.0-py3-none-any.whl", hash = "sha256:a9f5b8dcf6cbf9496769d7cf8bdbba37a0ec709ad98f88d103238b62f10bdf37", size = 47770, upload-time = "2026-06-06T07:44:50.038Z" }, +] + [[package]] name = "mypy" version = "2.3.0" @@ -987,6 +1328,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a6/0e/49df70d9b81fb5cbae4bbf2a49d865b09bcbcbc4eb53f5851b1027738d78/opentelemetry_semantic_conventions-0.65b0-py3-none-any.whl", hash = "sha256:1cacde7b0ad306f84c5ef08c3dbe1bbaf20165bba6f8bff43b670e555a086bcb", size = 204645, upload-time = "2026-07-16T15:25:30.688Z" }, ] +[[package]] +name = "packageurl-python" +version = "0.17.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f5/d6/3b5a4e3cfaef7a53869a26ceb034d1ff5e5c27c814ce77260a96d50ab7bb/packageurl_python-0.17.6.tar.gz", hash = "sha256:1252ce3a102372ca6f86eb968e16f9014c4ba511c5c37d95a7f023e2ca6e5c25", size = 50618, upload-time = "2025-11-24T15:20:17.998Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/2f/c7277b7615a93f51b5fbc1eacfc1b75e8103370e786fd8ce2abf6e5c04ab/packageurl_python-0.17.6-py3-none-any.whl", hash = "sha256:31a85c2717bc41dd818f3c62908685ff9eebcb68588213745b14a6ee9e7df7c9", size = 36776, upload-time = "2025-11-24T15:20:16.962Z" }, +] + [[package]] name = "packaging" version = "26.2" @@ -1020,6 +1370,70 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" }, ] +[[package]] +name = "pip" +version = "26.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/db/96/e6f8e9d9d7b9cc4457092712a7e919c3186aa2c2fa9ffed2c5d29cc947e8/pip-26.2.tar.gz", hash = "sha256:2d8542afcc84cdd8e846c2b36b2861fad1da376dd98f8e7113e9108a3c331690", size = 1848845, upload-time = "2026-07-29T21:57:56.407Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/36/a3aed958d60531cb442b7ab4596cda7b3621cfb916f8ae1d6769795c7dc1/pip-26.2-py3-none-any.whl", hash = "sha256:931c303696af6fa3417112103b1cad26890e5a07eccb5b99783700e33f2b8aad", size = 1816475, upload-time = "2026-07-29T21:57:54.763Z" }, +] + +[[package]] +name = "pip-api" +version = "0.0.34" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pip" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/f1/ee85f8c7e82bccf90a3c7aad22863cc6e20057860a1361083cd2adacb92e/pip_api-0.0.34.tar.gz", hash = "sha256:9b75e958f14c5a2614bae415f2adf7eeb54d50a2cfbe7e24fd4826471bac3625", size = 123017, upload-time = "2024-07-09T20:32:30.641Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/f7/ebf5003e1065fd00b4cbef53bf0a65c3d3e1b599b676d5383ccb7a8b88ba/pip_api-0.0.34-py3-none-any.whl", hash = "sha256:8b2d7d7c37f2447373aa2cf8b1f60a2f2b27a84e1e9e0294a3f6ef10eb3ba6bb", size = 120369, upload-time = "2024-07-09T20:32:29.099Z" }, +] + +[[package]] +name = "pip-audit" +version = "2.10.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cachecontrol", extra = ["filecache"] }, + { name = "cyclonedx-python-lib" }, + { name = "packaging" }, + { name = "pip-api" }, + { name = "pip-requirements-parser" }, + { name = "platformdirs" }, + { name = "requests" }, + { name = "rich" }, + { name = "tomli" }, + { name = "tomli-w" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/a4/f21d5f0a0edabcbce31560b73c7c5a6f72ae87af4236fd1069c8f59a353d/pip_audit-2.10.1.tar.gz", hash = "sha256:1eb4565d19ebe5d48996f4b770b4d2b32887e12cb12cfa637f1a064011b55ffc", size = 54275, upload-time = "2026-06-10T22:17:01.744Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/a7/b0c504148114047bd1bc9d97447453c6850ca176bb2f3c0038835994e8b7/pip_audit-2.10.1-py3-none-any.whl", hash = "sha256:99ef3f600a317c1945f1e89e227ef26e1c2d618429b8bd3fa6f4f7c440c4611a", size = 62023, upload-time = "2026-06-10T22:17:00.309Z" }, +] + +[[package]] +name = "pip-requirements-parser" +version = "32.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, + { name = "pyparsing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/2a/63b574101850e7f7b306ddbdb02cb294380d37948140eecd468fae392b54/pip-requirements-parser-32.0.1.tar.gz", hash = "sha256:b4fa3a7a0be38243123cf9d1f3518da10c51bdb165a2b2985566247f9155a7d3", size = 209359, upload-time = "2022-12-21T15:25:22.732Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/d0/d04f1d1e064ac901439699ee097f58688caadea42498ec9c4b4ad2ef84ab/pip_requirements_parser-32.0.1-py3-none-any.whl", hash = "sha256:4659bc2a667783e7a15d190f6fccf8b2486685b6dba4c19c3876314769c57526", size = 35648, upload-time = "2022-12-21T15:25:21.046Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/78/9b/560e4be8e26f6fd133a03630a8df0c663b9e8d61b4ade152b72005aec83b/platformdirs-4.11.0.tar.gz", hash = "sha256:0555d18370482847566ffabcaa53ad7c6c1c29f195989ae1ed634a05f76ea1e0", size = 31953, upload-time = "2026-07-21T13:09:36.565Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7d/68/d8d58938dfb1370b266a1a729e6d77a985be23689a0496498ee17b2cbf90/platformdirs-4.11.0-py3-none-any.whl", hash = "sha256:360ccded2b7fce0af0ff80cc8f5942a1c5d99b0e856033acb030bfc634709e74", size = 23247, upload-time = "2026-07-21T13:09:35.422Z" }, +] + [[package]] name = "pluggy" version = "1.6.0" @@ -1075,6 +1489,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617, upload-time = "2026-01-28T18:15:36.514Z" }, ] +[[package]] +name = "py-serializable" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "defusedxml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/73/21/d250cfca8ff30c2e5a7447bc13861541126ce9bd4426cd5d0c9f08b5547d/py_serializable-2.1.0.tar.gz", hash = "sha256:9d5db56154a867a9b897c0163b33a793c804c80cee984116d02d49e4578fc103", size = 52368, upload-time = "2025-07-21T09:56:48.07Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/bf/7595e817906a29453ba4d99394e781b6fabe55d21f3c15d240f85dd06bb1/py_serializable-2.1.0-py3-none-any.whl", hash = "sha256:b56d5d686b5a03ba4f4db5e769dc32336e142fc3bd4d68a8c25579ebb0a67304", size = 23045, upload-time = "2025-07-21T09:56:46.848Z" }, +] + [[package]] name = "pyasn1" version = "0.6.4" @@ -1264,6 +1690,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/54/18/1dd71c9b43192ab83f1d531ad6002dc81108ac36c475f79fb7a295abe2f4/pyopenssl-26.3.0-py3-none-any.whl", hash = "sha256:46367f8f66b92271e6d218da9c87607e1ef5a0bc5c8dea5bb3db82f395c385a3", size = 56008, upload-time = "2026-06-12T20:28:05.999Z" }, ] +[[package]] +name = "pyparsing" +version = "3.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/91/9c6ee907786a473bf81c5f53cf703ba0957b23ab84c264080fb5a450416f/pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc", size = 6851574, upload-time = "2026-01-21T03:57:59.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" }, +] + [[package]] name = "pyside6" version = "6.11.1" @@ -1353,6 +1788,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/03/e2/08a497ef684b88559c9cc5f4ad53a37e7b99e727094a86d6ea32536d5d3c/pytest_asyncio-1.4.0-py3-none-any.whl", hash = "sha256:933ca923a23075a87fb7070c0ec272a6848489824d887c85c812670932835aa1", size = 16930, upload-time = "2026-05-26T09:56:02.576Z" }, ] +[[package]] +name = "pytest-cov" +version = "7.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage" }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2", size = 55592, upload-time = "2026-03-21T20:11:16.284Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" }, +] + [[package]] name = "pytest-rerunfailures" version = "16.4" @@ -1452,6 +1901,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, ] +[[package]] +name = "rich" +version = "15.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, +] + [[package]] name = "ruff" version = "0.15.22" @@ -1477,6 +1939,34 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/57/c9/e69b1ff4c8b69093ef08b8919ab767af0569666865b39c30a8795d88d3c6/ruff-0.15.22-py3-none-win_arm64.whl", hash = "sha256:e1168075b72158510839f250027659cdd78476f40507dd517892304c41318661", size = 11298172, upload-time = "2026-07-16T15:14:10.51Z" }, ] +[[package]] +name = "setproctitle" +version = "1.3.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8d/48/49393a96a2eef1ab418b17475fb92b8fcfad83d099e678751b05472e69de/setproctitle-1.3.7.tar.gz", hash = "sha256:bc2bc917691c1537d5b9bca1468437176809c7e11e5694ca79a9ca12345dcb9e", size = 27002, upload-time = "2025-09-05T12:51:25.278Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/89/c7/43ac3a98414f91d1b86a276bc2f799ad0b4b010e08497a95750d5bc42803/setproctitle-1.3.7-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:80c36c6a87ff72eabf621d0c79b66f3bdd0ecc79e873c1e9f0651ee8bf215c63", size = 18052, upload-time = "2025-09-05T12:50:17.928Z" }, + { url = "https://files.pythonhosted.org/packages/cd/2c/dc258600a25e1a1f04948073826bebc55e18dbd99dc65a576277a82146fa/setproctitle-1.3.7-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b53602371a52b91c80aaf578b5ada29d311d12b8a69c0c17fbc35b76a1fd4f2e", size = 13071, upload-time = "2025-09-05T12:50:19.061Z" }, + { url = "https://files.pythonhosted.org/packages/ab/26/8e3bb082992f19823d831f3d62a89409deb6092e72fc6940962983ffc94f/setproctitle-1.3.7-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fcb966a6c57cf07cc9448321a08f3be6b11b7635be502669bc1d8745115d7e7f", size = 33180, upload-time = "2025-09-05T12:50:20.395Z" }, + { url = "https://files.pythonhosted.org/packages/f1/af/ae692a20276d1159dd0cf77b0bcf92cbb954b965655eb4a69672099bb214/setproctitle-1.3.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:46178672599b940368d769474fe13ecef1b587d58bb438ea72b9987f74c56ea5", size = 34043, upload-time = "2025-09-05T12:50:22.454Z" }, + { url = "https://files.pythonhosted.org/packages/34/b2/6a092076324dd4dac1a6d38482bedebbff5cf34ef29f58585ec76e47bc9d/setproctitle-1.3.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7f9e9e3ff135cbcc3edd2f4cf29b139f4aca040d931573102742db70ff428c17", size = 35892, upload-time = "2025-09-05T12:50:23.937Z" }, + { url = "https://files.pythonhosted.org/packages/1c/1a/8836b9f28cee32859ac36c3df85aa03e1ff4598d23ea17ca2e96b5845a8f/setproctitle-1.3.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:14c7eba8d90c93b0e79c01f0bd92a37b61983c27d6d7d5a3b5defd599113d60e", size = 32898, upload-time = "2025-09-05T12:50:25.617Z" }, + { url = "https://files.pythonhosted.org/packages/ef/22/8fabdc24baf42defb599714799d8445fe3ae987ec425a26ec8e80ea38f8e/setproctitle-1.3.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:9e64e98077fb30b6cf98073d6c439cd91deb8ebbf8fc62d9dbf52bd38b0c6ac0", size = 34308, upload-time = "2025-09-05T12:50:26.827Z" }, + { url = "https://files.pythonhosted.org/packages/15/1b/b9bee9de6c8cdcb3b3a6cb0b3e773afdb86bbbc1665a3bfa424a4294fda2/setproctitle-1.3.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b91387cc0f02a00ac95dcd93f066242d3cca10ff9e6153de7ee07069c6f0f7c8", size = 32536, upload-time = "2025-09-05T12:50:28.5Z" }, + { url = "https://files.pythonhosted.org/packages/37/0c/75e5f2685a5e3eda0b39a8b158d6d8895d6daf3ba86dec9e3ba021510272/setproctitle-1.3.7-cp314-cp314-win32.whl", hash = "sha256:52b054a61c99d1b72fba58b7f5486e04b20fefc6961cd76722b424c187f362ed", size = 12731, upload-time = "2025-09-05T12:50:43.955Z" }, + { url = "https://files.pythonhosted.org/packages/d2/ae/acddbce90d1361e1786e1fb421bc25baeb0c22ef244ee5d0176511769ec8/setproctitle-1.3.7-cp314-cp314-win_amd64.whl", hash = "sha256:5818e4080ac04da1851b3ec71e8a0f64e3748bf9849045180566d8b736702416", size = 13464, upload-time = "2025-09-05T12:50:45.057Z" }, + { url = "https://files.pythonhosted.org/packages/01/6d/20886c8ff2e6d85e3cabadab6aab9bb90acaf1a5cfcb04d633f8d61b2626/setproctitle-1.3.7-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:6fc87caf9e323ac426910306c3e5d3205cd9f8dcac06d233fcafe9337f0928a3", size = 18062, upload-time = "2025-09-05T12:50:29.78Z" }, + { url = "https://files.pythonhosted.org/packages/9a/60/26dfc5f198715f1343b95c2f7a1c16ae9ffa45bd89ffd45a60ed258d24ea/setproctitle-1.3.7-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6134c63853d87a4897ba7d5cc0e16abfa687f6c66fc09f262bb70d67718f2309", size = 13075, upload-time = "2025-09-05T12:50:31.604Z" }, + { url = "https://files.pythonhosted.org/packages/21/9c/980b01f50d51345dd513047e3ba9e96468134b9181319093e61db1c47188/setproctitle-1.3.7-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1403d2abfd32790b6369916e2313dffbe87d6b11dca5bbd898981bcde48e7a2b", size = 34744, upload-time = "2025-09-05T12:50:32.777Z" }, + { url = "https://files.pythonhosted.org/packages/86/b4/82cd0c86e6d1c4538e1a7eb908c7517721513b801dff4ba3f98ef816a240/setproctitle-1.3.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e7c5bfe4228ea22373e3025965d1a4116097e555ee3436044f5c954a5e63ac45", size = 35589, upload-time = "2025-09-05T12:50:34.13Z" }, + { url = "https://files.pythonhosted.org/packages/8a/4f/9f6b2a7417fd45673037554021c888b31247f7594ff4bd2239918c5cd6d0/setproctitle-1.3.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:585edf25e54e21a94ccb0fe81ad32b9196b69ebc4fc25f81da81fb8a50cca9e4", size = 37698, upload-time = "2025-09-05T12:50:35.524Z" }, + { url = "https://files.pythonhosted.org/packages/20/92/927b7d4744aac214d149c892cb5fa6dc6f49cfa040cb2b0a844acd63dcaf/setproctitle-1.3.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:96c38cdeef9036eb2724c2210e8d0b93224e709af68c435d46a4733a3675fee1", size = 34201, upload-time = "2025-09-05T12:50:36.697Z" }, + { url = "https://files.pythonhosted.org/packages/0a/0c/fd4901db5ba4b9d9013e62f61d9c18d52290497f956745cd3e91b0d80f90/setproctitle-1.3.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:45e3ef48350abb49cf937d0a8ba15e42cee1e5ae13ca41a77c66d1abc27a5070", size = 35801, upload-time = "2025-09-05T12:50:38.314Z" }, + { url = "https://files.pythonhosted.org/packages/e7/e3/54b496ac724e60e61cc3447f02690105901ca6d90da0377dffe49ff99fc7/setproctitle-1.3.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1fae595d032b30dab4d659bece20debd202229fce12b55abab978b7f30783d73", size = 33958, upload-time = "2025-09-05T12:50:39.841Z" }, + { url = "https://files.pythonhosted.org/packages/ea/a8/c84bb045ebf8c6fdc7f7532319e86f8380d14bbd3084e6348df56bdfe6fd/setproctitle-1.3.7-cp314-cp314t-win32.whl", hash = "sha256:02432f26f5d1329ab22279ff863c83589894977063f59e6c4b4845804a08f8c2", size = 12745, upload-time = "2025-09-05T12:50:41.377Z" }, + { url = "https://files.pythonhosted.org/packages/08/b6/3a5a4f9952972791a9114ac01dfc123f0df79903577a3e0a7a404a695586/setproctitle-1.3.7-cp314-cp314t-win_amd64.whl", hash = "sha256:cbc388e3d86da1f766d8fc2e12682e446064c01cea9f88a88647cfe7c011de6a", size = 13469, upload-time = "2025-09-05T12:50:42.67Z" }, +] + [[package]] name = "shiboken6" version = "6.11.1" @@ -1512,6 +2002,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, ] +[[package]] +name = "sortedcontainers" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594, upload-time = "2021-05-16T22:03:42.897Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" }, +] + [[package]] name = "sspilib" version = "0.5.0" @@ -1538,6 +2037,68 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ec/bb/2799cc2ede3ed41131f8975621e7213dfc7ef4acbbaadfa440f32500c370/starlette-1.3.1-py3-none-any.whl", hash = "sha256:c7372aae11c3c3f26a42df7bd626cec2f47d03483d261d369516a615a53714c6", size = 73632, upload-time = "2026-06-12T09:23:10.017Z" }, ] +[[package]] +name = "stevedore" +version = "5.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/dd/04d56c2a5232358df41f3d0f0e31833d378b6c8ed7803a6b1b7867b0eba6/stevedore-5.9.0.tar.gz", hash = "sha256:abbd0af7a38a8bbb1d6adea2e35b17609cf004eaac323e88a8d8963640dd2b3c", size = 514850, upload-time = "2026-07-02T11:38:08.509Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/8d/008761f6e1000600e5303db30d05724bdcf3d2d186cbb59fac79b52e39ed/stevedore-5.9.0-py3-none-any.whl", hash = "sha256:e520945d4c257700eddc1eb1d79df04b2ea578eef185e0e3fa5b442fc848d3f7", size = 54463, upload-time = "2026-07-02T11:38:07.43Z" }, +] + +[[package]] +name = "textual" +version = "8.2.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py", extra = ["linkify"] }, + { name = "mdit-py-plugins" }, + { name = "platformdirs" }, + { name = "pygments" }, + { name = "rich" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/00/21/39a76b01bd5eea82a04baaca7580e105d8c59450df03998345bb2cfb307b/textual-8.2.8.tar.gz", hash = "sha256:3f106a9fbc73e39dd266c9712432087de78a6d644084c7c241d6a25c3169115b", size = 1860502, upload-time = "2026-06-30T06:51:24.495Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/be/35261223d9416a0751cdff1c7b4a6f881387218a12d439fe22fefebc8c04/textual-8.2.8-py3-none-any.whl", hash = "sha256:267375fd402dc8d981457212efa71f0e3365fd17bba144ba9bb3ed7563cb374a", size = 731418, upload-time = "2026-06-30T06:51:26.364Z" }, +] + +[[package]] +name = "tomli" +version = "2.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" }, + { url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" }, + { url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" }, + { url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" }, + { url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" }, + { url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" }, + { url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" }, + { url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" }, + { url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" }, + { url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" }, + { url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" }, + { url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" }, + { url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" }, + { url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" }, + { url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" }, + { url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" }, + { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" }, +] + +[[package]] +name = "tomli-w" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/19/75/241269d1da26b624c0d5e110e8149093c759b7a286138f4efd61a60e75fe/tomli_w-1.2.0.tar.gz", hash = "sha256:2dd14fac5a47c27be9cd4c976af5a12d87fb1f0b4512f81d69cce3b35ae25021", size = 7184, upload-time = "2025-01-15T12:07:24.262Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/18/c86eb8e0202e32dd3df50d43d7ff9854f8e0603945ff398974c1d91ac1ef/tomli_w-1.2.0-py3-none-any.whl", hash = "sha256:188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90", size = 6675, upload-time = "2025-01-15T12:07:22.074Z" }, +] + [[package]] name = "tomlkit" version = "0.15.1" @@ -1586,6 +2147,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e5/6d/b53b99a9f2766d095985947a5782f1702cabb129a34f7a802d7197af832f/tzdata-2026.3-py2.py3-none-any.whl", hash = "sha256:dc096730c87af6cab1b171c9d532be840741ff5d459015e7f6947bd7d7e54931", size = 348168, upload-time = "2026-07-10T08:50:36.46Z" }, ] +[[package]] +name = "uc-micro-py" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/78/67/9a363818028526e2d4579334460df777115bdec1bb77c08f9db88f6389f2/uc_micro_py-2.0.0.tar.gz", hash = "sha256:c53691e495c8db60e16ffc4861a35469b0ba0821fe409a8a7a0a71864d33a811", size = 6611, upload-time = "2026-03-01T06:31:27.526Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/73/d21edf5b204d1467e06500080a50f79d49ef2b997c79123a536d4a17d97c/uc_micro_py-2.0.0-py3-none-any.whl", hash = "sha256:3603a3859af53e5a39bc7677713c78ea6589ff188d70f4fee165db88e22b242c", size = 6383, upload-time = "2026-03-01T06:31:26.257Z" }, +] + [[package]] name = "urllib3" version = "2.7.0" @@ -1740,3 +2310,21 @@ sdist = { url = "https://files.pythonhosted.org/packages/55/f0/2431c2c8f77326757 wheels = [ { url = "https://files.pythonhosted.org/packages/5c/fb/8be39dc47b0ed9bbc80f384906eed87fa974c9b377a7a430118c3b405550/xmlschema-4.3.2-py3-none-any.whl", hash = "sha256:cf5c970a30f6ebcb3da35260e694704cc0b6794d8ace46125bb2a95e38ed9307", size = 504613, upload-time = "2026-06-30T06:11:47.462Z" }, ] + +[[package]] +name = "zizmor" +version = "1.5.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c5/48/a692769e2bbb62635d55849175849bb9fec2240d0a16e16bd28d8cfe314f/zizmor-1.5.2.tar.gz", hash = "sha256:848f04c0d84b085dfb79c66951404372ebf9b3dd0c73076f1baac83db29e74e4", size = 296921, upload-time = "2025-03-23T15:01:14.821Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/59/31/f717638faf223776b239040574f67bfdeb2ef3279673c08a099a525c9f4f/zizmor-1.5.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:03b27c46d87e96a0acdf78190fbb674c174f67ded396c74dadd64d7c4ecba680", size = 4687915, upload-time = "2025-03-23T15:01:06.138Z" }, + { url = "https://files.pythonhosted.org/packages/1f/0a/0a401630e9f98b8660c20a75cfb4995245c738da90d9bc1c3cd709da8a8a/zizmor-1.5.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:a40feabc2c043aca60f9edbd35a676d97d684af5b06ed61a7752869c79b3be30", size = 4443376, upload-time = "2025-03-23T15:01:04.384Z" }, + { url = "https://files.pythonhosted.org/packages/ce/cf/91527ae1e53e3be260545630e740bd34f5dfa566b23360dea5b07d15e4e5/zizmor-1.5.2-py3-none-manylinux_2_24_aarch64.whl", hash = "sha256:9d40780b19da7901423de80ccce083a8c0d2114e0cf6432aa20d8e60d15e97f6", size = 4601870, upload-time = "2025-03-23T15:00:57.752Z" }, + { url = "https://files.pythonhosted.org/packages/b2/04/4cdec1dff48ef4bb733344568d9d6a8a82cc655581787866cf38d999001a/zizmor-1.5.2-py3-none-manylinux_2_28_armv7l.whl", hash = "sha256:df1326fe9f9ddcbe9d862a97571c848276882d0ceeee39e18fcfc9ec5c66cd7b", size = 4513121, upload-time = "2025-03-23T15:01:00.093Z" }, + { url = "https://files.pythonhosted.org/packages/dc/27/7fbb2e2ad2d33de12b15014b762d39b1529b94bcbfebf55b735e67e00bd4/zizmor-1.5.2-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:0c5f656106c430748858ae459c41c9eed09a1e01e5f42015d80c8fe34740d173", size = 4840803, upload-time = "2025-03-23T15:01:02.222Z" }, + { url = "https://files.pythonhosted.org/packages/4d/80/6061f5d37cadd185c3e921bc20b8fcf34b262cad009911c50010b7e8d3d5/zizmor-1.5.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5e18b06af4208624ef707e7859b07539870ca26b74a89537e1a6bfba2e44fc9a", size = 4590530, upload-time = "2025-03-23T15:01:08.435Z" }, + { url = "https://files.pythonhosted.org/packages/35/61/777a1ac136d8f256d94f2f269baeb11a6874a9241953fd6694fd4eb5751c/zizmor-1.5.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:506b355c693d40df01bdf26fd1dfcb41801af7b8c51767d4159efdc2655965c4", size = 4525617, upload-time = "2025-03-23T15:01:10.738Z" }, + { url = "https://files.pythonhosted.org/packages/09/46/8ba5dc116afd6105774ba527d66e7ecf635b2fdf56daddd18ef2933201e6/zizmor-1.5.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:8c65b4dbc9690d3f0f5d9756ae0b5ca8d3dfb4f806d82a5195836a3b613ae996", size = 4914859, upload-time = "2025-03-23T15:01:12.911Z" }, + { url = "https://files.pythonhosted.org/packages/df/0e/9fd2a2e3bbc904878c74a4270aef61be8564f88d68a3dc6ebdc3e0ea2b81/zizmor-1.5.2-py3-none-win32.whl", hash = "sha256:dcef697a88983e7ce6948df1603e2325e2cf5c523828af94615e6c432fa8d98a", size = 3939633, upload-time = "2025-03-23T15:01:18.196Z" }, + { url = "https://files.pythonhosted.org/packages/94/60/837501b8fc475086f5f26c72fa77513cedb5f761744bca809fdc2ee68e6a/zizmor-1.5.2-py3-none-win_amd64.whl", hash = "sha256:86395dd985ed6bf9acffea8a900b30d8d4dd4c9e20421f16f2db7ad92299e24e", size = 4435978, upload-time = "2025-03-23T15:01:16.284Z" }, +] From 78f3cf166df5730ee47acff0a91a25ff6cff1f82 Mon Sep 17 00:00:00 2001 From: wshallwshall Date: Wed, 29 Jul 2026 20:36:51 -0500 Subject: [PATCH 2/6] fix(security): the anti-slopsquat gate could not see [dependency-groups] declared_distributions() read [project].dependencies + [project.optional-dependencies] only. The six CI-toolchain names just declared in [dependency-groups] would therefore have entered the tree UNVETTED, and so would every future group entry -- the gate would have gone silently blind on a whole table while still reporting a clean sweep. This is not a bookkeeping fix. Those names are resolved into uv.lock and installed into the runner that executes the BLOCKING security gates, which makes a squatted scanner name a SHARPER target than a runtime dependency, not a softer one: it runs with the job's token in hand before any of these controls report. PEP 735 lets a group entry be a {include-group = "other"} table rather than a requirement string. Those are skipped -- they name another group, not a distribution, and handing the dict to requirement_name() would raise and take the whole gate down. The included group's own members are still swept, because every group is iterated regardless of who includes it. Three new tests prove the table is PARSED and separately that it is ENFORCED (a hallucinated name declared in a group must fail vetting end-to-end), plus a liveness test asserting the examined count actually MOVED -- as a set delta, not a magic total, so an ordinary dependency tomorrow does not red it but the table falling out of the parser does. Measured: examined 41 -> 47 distributions, added exactly {bandit, pip-audit, zizmor, diff-cover, mutmut, pytest-cov}. pytest-timeout adds nothing because it is deliberately declared in BOTH the dev extra and ci-quality with the identical spec, which is what stops uv resolving two versions of it. Verified against live PyPI: all 47 exist, publish, and are established (exit 0). --- scripts/security/new_dependency_check.py | 24 +++++- tests/test_new_dependency_check.py | 100 +++++++++++++++++++++++ 2 files changed, 122 insertions(+), 2 deletions(-) diff --git a/scripts/security/new_dependency_check.py b/scripts/security/new_dependency_check.py index f5971269..8b05afbe 100644 --- a/scripts/security/new_dependency_check.py +++ b/scripts/security/new_dependency_check.py @@ -19,6 +19,12 @@ human remembering". CLAUDE.md section 5 states the rule in prose, and prose is what a session rationalizes past at 2am. +WHAT IT SWEEPS: ``[project].dependencies``, every ``[project.optional-dependencies]`` extra, and every +PEP 735 ``[dependency-groups]`` group. The last was added 2026-07-29 with the hash-pinned CI toolchain +(ADR 0034 section 3): those names are not shipped in the wheel, but they are resolved into ``uv.lock`` +and installed into the runner that executes the BLOCKING security gates, which makes a squatted scanner +name a sharper target than a runtime dependency rather than a softer one. + WHAT IT CHECKS, per declared distribution: * EXISTS -- PyPI serves a project page for the name at all. * HAS RELEASES -- at least one release with files. A registered-but-empty placeholder is the shape @@ -114,14 +120,28 @@ def requirement_name(spec: str) -> str | None: def declared_distributions(pyproject_text: str) -> dict[str, str]: """Every distribution this project declares -> the spec it was declared with. - Covers ``[project].dependencies`` AND every ``[project.optional-dependencies]`` extra, because an - extra is exactly where a niche, plausible-sounding, hallucination-prone name lands. + Covers THREE tables, because a name this gate does not read is a name that enters the tree unvetted: + + * ``[project].dependencies`` -- the runtime closure. + * every ``[project.optional-dependencies]`` extra -- exactly where a niche, plausible-sounding, + hallucination-prone name lands. + * every PEP 735 ``[dependency-groups]`` group -- the CI toolchain (ADR 0034 section 3). These are + not published wheel metadata, but they ARE resolved into ``uv.lock``, hash-exported to + ``ci/locks/*.lock``, and installed into the runner that executes the blocking security gates. A + slopsquatted scanner name is if anything a SHARPER target than a runtime dependency: it runs with + the job's token before any of these controls report. """ data = tomllib.loads(pyproject_text) project = data.get("project") or {} specs: list[str] = list(project.get("dependencies") or []) for extra_specs in (project.get("optional-dependencies") or {}).values(): specs.extend(extra_specs or []) + for group_specs in (data.get("dependency-groups") or {}).values(): + # A PEP 735 entry is EITHER a requirement string OR a `{include-group = "other"}` table. The + # table names another group in this same file, not a distribution, so it has nothing to vet -- + # and passing the dict to requirement_name() would raise. Include-groups are covered anyway: + # every group is iterated here regardless of who includes it. + specs.extend(spec for spec in (group_specs or []) if isinstance(spec, str)) found: dict[str, str] = {} for spec in specs: diff --git a/tests/test_new_dependency_check.py b/tests/test_new_dependency_check.py index 117718a3..9d408ebc 100644 --- a/tests/test_new_dependency_check.py +++ b/tests/test_new_dependency_check.py @@ -90,6 +90,73 @@ def test_declared_distributions_covers_extras_not_just_core() -> None: assert set(found) == {"fastapi", "hl7", "pynetdicom", "pydicom", "hvac"} +def test_declared_distributions_covers_pep735_dependency_groups() -> None: + """The CI toolchain (ADR 0034 §3) is declared in `[dependency-groups]`, not in an extra. + + Those names are resolved into `uv.lock` and installed into the runner that executes the BLOCKING + security gates, so a squatted scanner name lands with the job's token in hand. A gate that cannot + see the table cannot vet it, and would report a clean sweep while ignoring it entirely. + """ + found = declared_distributions( + """ + [project] + dependencies = ["fastapi>=0.100"] + + [dependency-groups] + ci-scanners = ["bandit==1.9.4", "zizmor==1.5.2"] + ci-quality = ["mutmut==3.6.0", "pytest-cov>=7.0"] + """ + ) + assert set(found) == {"fastapi", "bandit", "zizmor", "mutmut", "pytest-cov"} + + +def test_an_include_group_table_is_skipped_not_crashed_on() -> None: + """PEP 735 lets a group entry be `{include-group = "other"}` instead of a requirement string. + + That table names another GROUP, not a distribution, so there is nothing to vet — and handing the + dict to `requirement_name()` would raise, taking the whole gate down with it. The included group's + own members are still swept, because every group is iterated regardless of who includes it. + """ + found = declared_distributions( + """ + [project] + dependencies = [] + + [dependency-groups] + base = ["bandit==1.9.4"] + everything = [{ include-group = "base" }, "zizmor==1.5.2"] + """ + ) + assert set(found) == {"bandit", "zizmor"} + + +def test_a_bogus_name_declared_in_a_dependency_group_is_rejected() -> None: + """End-to-end: a hallucinated name in a GROUP must fail vetting exactly like one in an extra. + + Drives the real `declared_distributions` -> `vet` path rather than asserting the parser alone, so + "the table is parsed" and "the table is enforced" are two different claims and both are proven. + """ + declared = declared_distributions( + """ + [project] + dependencies = ["fastapi>=0.100"] + + [dependency-groups] + ci-scanners = ["bandit==1.9.4", "hl7-sast-scanner==2.0"] + """ + ) + assert "hl7-sast-scanner" in declared, "the group name never reached the vetting stage" + findings, examined = vet( + declared, + lambda name: None if name == "hl7-sast-scanner" else _fetch_all_good(name), + now=_NOW, + ) + assert examined == 3 + assert [(f.distribution, f.problem) for f in findings] == [ + ("hl7-sast-scanner", "does not exist on PyPI") + ] + + # --- the vetting rules: each failure mode proven, not assumed ---------------------------------- @@ -305,6 +372,39 @@ def test_the_real_pyproject_declares_a_plausible_number_of_distributions() -> No # Spot-check one core dep and one extra, so a table being dropped entirely is caught by name. assert "fastapi" in declared assert "pynetdicom" in declared, "the [dicom] extra is not being read" + assert "bandit" in declared, "the [dependency-groups] CI toolchain is not being read" + + +def test_the_ci_toolchain_groups_actually_raise_the_examined_count() -> None: + """The count must MOVE, not merely be plausible — a parser that silently reads nothing new passes + every floor above. + + Measured: 41 distributions before `[dependency-groups]` was swept, 47 after. The six added names + are the CI toolchain (`bandit`, `pip-audit`, `zizmor`, `diff-cover`, `mutmut`, `pytest-cov`); + `pytest-timeout` is declared in BOTH the `dev` extra and `ci-quality` and so adds nothing, which is + itself deliberate — the identical spec in both places is what stops uv resolving two versions. + + Asserted as a SET DELTA rather than a magic total, so adding an ordinary dependency tomorrow does + not red this, but the group table falling out of the parser does. + """ + from scripts.security.new_dependency_check import declared_distributions as parse + + text = (_ROOT / "pyproject.toml").read_text(encoding="utf-8") + with_groups = set(parse(text)) + + # The same file with [dependency-groups] renamed to an inert table = the pre-change behaviour. + without = set(parse(text.replace("\n[dependency-groups]\n", "\n[inert-not-a-real-table]\n", 1))) + + added = with_groups - without + print(f"examined: {len(without)} -> {len(with_groups)} (+{len(added)}): {sorted(added)}") + assert added == {"bandit", "pip-audit", "zizmor", "diff-cover", "mutmut", "pytest-cov"}, ( + f"the [dependency-groups] sweep added {sorted(added)}; expected the six CI toolchain names. " + "If a tool was deliberately added or removed, re-point this set in the same commit." + ) + assert "pytest-timeout" in without, ( + "pytest-timeout must remain declared in the [dev] extra too — the ci-quality group deliberately " + "repeats its exact spec so uv cannot resolve two different versions" + ) def test_every_finding_message_names_the_remedy() -> None: From 553018a02dd9f0e9239497f23b179bb97c573489 Mon Sep 17 00:00:00 2001 From: wshallwshall Date: Wed, 29 Jul 2026 20:37:21 -0500 Subject: [PATCH 3/6] ci(security): install the CI toolchain hash-pinned, and teach the guards that see it Consumes the two PEP 735 exports. Five install sites become `pip install --require-hashes -r ci/locks/.lock`: security.yml pip-audit step (was pip install --upgrade pip "pip-audit==2.10.1") security.yml bandit step (was pip install --upgrade pip "bandit==1.9.4") zizmor.yml zizmor install (was pip install --upgrade pip "zizmor==1.5.2") quality-advisory.yml coverage (was uv pip install pytest-cov "diff-cover==10.4.1") quality-advisory.yml mutation (was uv pip install "mutmut==3.6.0" pytest-timeout) Two unpinned `--upgrade pip` bootstraps DISAPPEAR rather than get pinned: ci-scanners.lock hash-pins pip==26.2 itself (a pip-audit -> pip-api dependency), so a hash-verified pip now arrives in the same command that used to fetch an unverified one. `python -m pip`, not `uv pip install --system`, in quality-advisory.yml -- deliberately. --require-hashes is pip's contract, and Scorecard's pinned-dependency parser keys on pip/pip3/`python -m pip`, so `uv pip` would be invisible to the very check this exists to satisfy. pip-audit now also audits both toolchain locks. This is load-bearing, not tidiness: hash-pinning makes a toolchain STICKY, so without it a CVE in a pinned scanner is invisible to every gate -- the "pinned, stale, unpatched is worse than floating" failure ADR 0034 section 3 names. It is also the mitigation for an UNVERIFIABLE unknown: whether Dependabot's uv ecosystem enumerates [dependency-groups] at all cannot be tested before the next weekly run, and this converts "silently stale" into a red gate within ~24h via the daily cron. Six-place lockstep per new lock (export + git diff in the gate; export + git diff --quiet + git add in dependabot-lock-resync.yml), enforced by test_dep1_lock_resync_lockstep.py. The resync's header count goes FOUR -> SIX. Guards taught, in the same commit their own failure messages demand: * bandit and pip-audit dropped from RELEASE_PINNED_TOOLS (their pins moved into the lock). uv and semgrep STAY -- uv cannot be hash-locked by uv (circular: it produces the locks), semgrep is excluded by decision. So the table stays non-vacuous. * test_diff_cover_is_pinned_exactly / test_mutmut_is_pinned_to_3x_with_pytest_timeout re-pointed at the group + the lock, keeping their BEHAVIOURAL assertions (mutmut must be 3.x; pytest-timeout must be present) asserted against what is actually installed. Five NEW tests, because `-r ` is INVISIBLE to _install_targets (it skips -r and its argument), so these lines passed every existing scan by not being looked at: require-hashes present; the lock fully ==-pinned AND hashed PER REQUIREMENT; the lock is in the DEP-1 export set (the anti-rot guard); every moved tool still declared in a group; no moved tool also installed inline. That per-requirement hash check earned itself: the first version asserted `"--hash=sha256:" in text`, which stayed GREEN under an injected regression while its six sibling guards all went red -- a lock with 33 pinned packages and one hash would have passed. Evidence (each guard was made to fail on purpose first): 8 injected regressions, 8 RED as designed, clean revert. 128 workflow shell blocks bash -n clean. Parsed-YAML diff vs origin/main: triggers, top-level and per-job permissions, per-job if/continue-on-error and step counts ALL IDENTICAL in all four workflows -- only run: bodies changed. DEP-1's exact six-artifact `git diff --exit-code` exits 0. Full suite 9464 passed. NOT TOUCHED, deliberately: release.yml (zero edits -- no PR CI leg executes it, so a break first surfaces at a tag; its sigstore==4.4.0 cooldown decision stays intact) and the uv bootstrap (circular by construction). Both recorded as residuals in the ADR amendment. --- .github/dependabot.yml | 7 +- .github/workflows/dependabot-lock-resync.yml | 23 +- .github/workflows/quality-advisory.yml | 36 +-- .github/workflows/security.yml | 30 ++- .github/workflows/zizmor.yml | 7 +- tests/test_ci_venv_pinning.py | 221 ++++++++++++++++++- tests/test_quality_advisory_invariants.py | 77 ++++++- 7 files changed, 359 insertions(+), 42 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 2449496e..fd436e2b 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -6,9 +6,14 @@ updates: # pyproject.toml + uv.lock and REGENERATES uv.lock IN its PRs (version updates GA 2025-03, # security updates GA 2025-12) — the old "pip" ecosystem updated requirements/pyproject but NOT # uv.lock. It still does not re-derive the EXPORTED locks (requirements.lock + docker/locks/* + - # constraints.lock), which the DEP-1 gate in security.yml byte-diffs; + # constraints.lock + ci/locks/* — SIX artifacts), which the DEP-1 gate in security.yml byte-diffs; # .github/workflows/dependabot-lock-resync.yml re-exports those on the Dependabot branch so the # gate stays green. + # + # UNVERIFIED, and it cannot be verified before the next weekly run: whether this ecosystem enumerates + # PEP 735 `[dependency-groups]` (the hash-pinned CI toolchain, ADR 0034 §3) at all. If it does not, + # those pins go stale silently — which is why security.yml's pip-audit step audits ci/locks/*.lock + # directly, turning "silently stale" into a red gate within ~24h via the daily cron. - package-ecosystem: "uv" directory: "/" schedule: diff --git a/.github/workflows/dependabot-lock-resync.yml b/.github/workflows/dependabot-lock-resync.yml index eeb188c9..6ce3d315 100644 --- a/.github/workflows/dependabot-lock-resync.yml +++ b/.github/workflows/dependabot-lock-resync.yml @@ -1,17 +1,22 @@ name: Dependabot lock resync -# Re-exports the FOUR committed "uv export" artifacts (requirements.lock + +# Re-exports the SIX committed "uv export" artifacts (requirements.lock + # docker/locks/requirements-core.lock + docker/locks/requirements-sqlserver.lock + the HASHLESS -# constraints.lock) on a Dependabot PR that touched uv.lock / pyproject.toml, and commits them back -# to the PR branch so the DEP-1 drift gate (security.yml -> pip-audit job, step "Check the lockfile -# is in sync with pyproject (DEP-1)") goes green WITHOUT a human re-export. +# constraints.lock + the two PEP 735 CI-toolchain locks ci/locks/ci-scanners.lock and +# ci/locks/ci-quality.lock) on a Dependabot PR that touched uv.lock / pyproject.toml, and commits them +# back to the PR branch so the DEP-1 drift gate (security.yml -> pip-audit job, step "Check the +# lockfile is in sync with pyproject (DEP-1)") goes green WITHOUT a human re-export. # # WHY: the native "uv" Dependabot ecosystem regenerates uv.lock + pyproject.toml in its PR, but NOT -# the exported lock artifacts. The DEP-1 gate re-runs "uv lock --check" + the four "uv export"s and +# the exported lock artifacts. The DEP-1 gate re-runs "uv lock --check" + all six "uv export"s and # "git diff --exit-code"s the result, so a Dependabot uv PR would otherwise leave the exports stale # and red the gate. This workflow runs the IDENTICAL commands and pushes the refreshed exports onto # the Dependabot branch. # +# The two ci/locks/*.lock artifacts are what makes the CI toolchain hash-pinned rather than merely +# version-pinned (ADR 0034 §3). They are ALSO what keeps it fresh: a hash-pinned toolchain outside this +# resync machinery would rot into a pinned, stale, unpatched toolchain — worse posture than floating. +# # KEEP THIS LIST IN LOCKSTEP WITH security.yml's DEP-1 step. A file the gate diffs but this job does # not re-export is un-fixable by the bot: the resync pushes an incomplete set, DEP-1 re-exports the # full set, finds the missing one stale, and the PR stays red with no human-free path to green. @@ -128,6 +133,10 @@ jobs: # The hashless constraint export every CI install pins against (`--constraint constraints.lock`, # ci.yml). Same lock, --no-hashes; omitted here it would stay stale and red DEP-1 forever. uv export --all-extras --no-emit-project --no-hashes --format requirements.txt -o constraints.lock + # The CI TOOLCHAIN locks (PEP 735 dependency groups, ADR 0034 §3). Same lockstep rule as the + # four above: the gate diffs them, so the bot must re-export them or the PR has no path green. + uv export --only-group ci-scanners --format requirements.txt -o ci/locks/ci-scanners.lock + uv export --only-group ci-quality --format requirements.txt -o ci/locks/ci-quality.lock - name: Commit and push the resynced locks if: steps.creds.outputs.present == 'true' @@ -139,11 +148,11 @@ jobs: set -euo pipefail git config user.name 'dependabot[bot]' git config user.email '49699333+dependabot[bot]@users.noreply.github.com' - if git diff --quiet -- requirements.lock docker/locks/requirements-core.lock docker/locks/requirements-sqlserver.lock constraints.lock; then + if git diff --quiet -- requirements.lock docker/locks/requirements-core.lock docker/locks/requirements-sqlserver.lock constraints.lock ci/locks/ci-scanners.lock ci/locks/ci-quality.lock; then echo 'Exported lock files already in sync; nothing to push.' exit 0 fi - git add requirements.lock docker/locks/requirements-core.lock docker/locks/requirements-sqlserver.lock constraints.lock + git add requirements.lock docker/locks/requirements-core.lock docker/locks/requirements-sqlserver.lock constraints.lock ci/locks/ci-scanners.lock ci/locks/ci-quality.lock git commit -m 'chore(deps): resync exported lock files (DEP-1)' # Push with the persisted App-token credential (NOT GITHUB_TOKEN). An App-token push emits a # synchronize event so the required checks (DEP-1, ci.yml) re-run on the new commit; a diff --git a/.github/workflows/quality-advisory.yml b/.github/workflows/quality-advisory.yml index dedbf02c..72234624 100644 --- a/.github/workflows/quality-advisory.yml +++ b/.github/workflows/quality-advisory.yml @@ -298,12 +298,20 @@ jobs: - name: Install project + coverage tools run: | uv pip install --system --constraint constraints.lock -e ".[dev,harness,fhir,dicom,x12,xml,webauthn]" -e packaging/messagefoundry-webconsole - # NEITHER package is in constraints.lock, so the --constraint above is a no-op for them and both - # previously floated to whatever PyPI served that day. diff-cover is now pinned exactly: the - # inline-annotation surface depends on `--format github-annotations:` and on adjacent-line - # coalescing in GitHubAnnotationsReportGenerator. Deliberately NOT in pyproject.toml -- these are - # CI-only tools, and adding them would trip the DEP-1 lock-sync gate and force four re-exports. - uv pip install --system --constraint constraints.lock pytest-cov "diff-cover==10.4.1" + # HASH-PINNED from the CI toolchain lock (ADR 0034 §3). These tools now DO live in + # pyproject.toml ([dependency-groups].ci-quality, where each pin's rationale sits) and DEP-1 + # re-exports this lock by design -- superseding the note that used to sit here claiming they + # were deliberately kept out of pyproject to avoid tripping that gate. + # + # `python -m pip`, not `uv pip install --system`, for two reasons: `--require-hashes` is pip's + # contract (and every other hashed install in this repo uses pip), and Scorecard's + # pinned-dependency parser keys on pip/pip3/`python -m pip` -- so `uv pip` would be invisible + # to the very check this change exists to satisfy. Same setup-python interpreter either way. + # + # Safe against the editable install above: the group lock and constraints.lock agree on all 10 + # shared packages (pytest and pytest-timeout among them), so this cannot re-point what the + # coverage run executes under. + python -m pip install --require-hashes -r ci/locks/ci-quality.lock - name: Tests under coverage (advisory - never fails) continue-on-error: true env: @@ -428,13 +436,15 @@ jobs: - name: Install project + mutmut run: | uv pip install --system --constraint constraints.lock -e ".[dev,harness,fhir,dicom,x12,xml,webauthn]" -e packaging/messagefoundry-webconsole - # mutmut 3, pinned. `mutmut<3` resolved to 2.5.1, which CRASHES on Python 3.14 in its pony-ORM - # cache (`cannot pickle 'itertools.count'`) before generating a single mutant -- verified from - # run 30248096425. pytest-timeout is REQUIRED: mutmut 3 always passes `--timeout`/ - # `--timeout-method` to pytest, and without the plugin every test invocation dies with an - # unrecognised-argument error that surfaces only as BadTestExecutionCommandsException. - # Neither package is in constraints.lock, so the --constraint above does not pin them. - uv pip install --system "mutmut==3.6.0" pytest-timeout + # mutmut 3 + pytest-timeout, HASH-PINNED from the CI toolchain lock (ADR 0034 §3). Both live in + # pyproject.toml's [dependency-groups].ci-quality, which records why each is load-bearing: + # `mutmut<3` resolved to 2.5.1, which CRASHES on Python 3.14 in its pony-ORM cache + # (`cannot pickle 'itertools.count'`) before generating a single mutant -- verified from run + # 30248096425; and pytest-timeout is REQUIRED because mutmut 3 always passes `--timeout`/ + # `--timeout-method` to pytest, so without the plugin every invocation dies with an + # unrecognised-argument error surfacing only as BadTestExecutionCommandsException. + # `python -m pip` because --require-hashes is pip's contract and Scorecard only parses pip. + python -m pip install --require-hashes -r ci/locks/ci-quality.lock - name: Mutation-test a bounded scope (advisory - never fails) continue-on-error: true env: diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index b80be034..08e87a43 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -78,7 +78,12 @@ jobs: # same lock, so a fresh `uv pip install -e ".[extras]"` cannot drift to a newer upstream # release. Re-export it here so it can never fall out of step with uv.lock. uv export --all-extras --no-emit-project --no-hashes --format requirements.txt -o constraints.lock - git diff --exit-code -- requirements.lock docker/locks/requirements-core.lock docker/locks/requirements-sqlserver.lock constraints.lock + # The CI TOOLCHAIN locks (ADR 0034 §3): PEP 735 `[dependency-groups]`, so the scanners and the + # quality tools flow through uv.lock and are consumed HASH-PINNED. Non-default groups, so they + # stay out of the four exports above (and out of the SBOM / image locks / audited runtime). + uv export --only-group ci-scanners --format requirements.txt -o ci/locks/ci-scanners.lock + uv export --only-group ci-quality --format requirements.txt -o ci/locks/ci-quality.lock + git diff --exit-code -- requirements.lock docker/locks/requirements-core.lock docker/locks/requirements-sqlserver.lock constraints.lock ci/locks/ci-scanners.lock ci/locks/ci-quality.lock - name: Install from the hashed lockfile (DEP-1) run: | # --require-hashes enforces a hash for every requirement (the lockfile carries them): a @@ -89,13 +94,19 @@ jobs: /tmp/lockcheck/bin/pip install --require-hashes -r requirements.lock - name: Audit the locked dependencies (DEP-1) run: | - # PINNED. This job exists to prove nothing unpinned enters the tree, and it was installing - # its own auditor with a bare `pip install pip-audit` — dependency intake that none of the - # lockfiles it checks actually covers. `==` also makes the audit reproducible: an unpinned - # auditor can change its advisory database handling between two runs of the same commit. - python -m pip install --upgrade pip "pip-audit==2.10.1" + # HASH-PINNED from the lock, not merely `==`-pinned: a version pin does not satisfy Scorecard + # PinnedDependenciesID, and the transitive closure floated regardless. Rationale for each pin + # lives in pyproject.toml's [dependency-groups]. The `--upgrade pip` bootstrap is GONE rather + # than pinned — this lock hash-pins `pip` itself (a pip-audit → pip-api dependency). + python -m pip install --require-hashes -r ci/locks/ci-scanners.lock # Audit the committed, pinned set — reproducible (vs auditing a fresh latest-resolve). pip-audit -r requirements.lock --desc + # Audit the TOOLCHAIN locks too. NOT bookkeeping: hash-pinning makes the toolchain STICKY, so + # without this a CVE in a pinned scanner is invisible to every gate — the "pinned, stale, + # unpatched is worse than floating" failure ADR 0034 §3 names. `--ignore-vuln ` is the + # escape hatch for a triaged advisory, exactly as for the lockfile audit above. + pip-audit -r ci/locks/ci-scanners.lock --desc + pip-audit -r ci/locks/ci-quality.lock --desc # ANTI-SLOPSQUAT. Deliberately a STEP in this already-REQUIRED job rather than a new context: it # blocks today with no branch-protection change, the same reasoning as the ledger-gate backstop # in ci.yml. (A hard-failing job that is NOT a required context does not stop auto-merge — it @@ -300,9 +311,10 @@ jobs: python-version: "3.14" - name: Scan source for insecure patterns run: | - # Pin bandit: an unpinned upgrade once silently changed `# nosec` parsing (1.9.x requires - # space-separated test IDs) and broke a green branch. Bump deliberately, not implicitly. - python -m pip install --upgrade pip "bandit==1.9.4" + # HASH-PINNED from the lock (ADR 0034 §3); bandit's version is the findings baseline of a + # blocking gate — the `# nosec`-parsing incident behind that pin is recorded beside the pin + # itself, in pyproject.toml's [dependency-groups].ci-scanners. Bump there, then re-export. + python -m pip install --require-hashes -r ci/locks/ci-scanners.lock # Skips are reviewed-safe project idioms. The subprocess sites (B603/B607) are annotated # per-line with `# nosec` at the call instead of a wholesale skip, so a NEW unreviewed # subprocess call is still flagged (review low-27): diff --git a/.github/workflows/zizmor.yml b/.github/workflows/zizmor.yml index 807a6e0c..99a00a02 100644 --- a/.github/workflows/zizmor.yml +++ b/.github/workflows/zizmor.yml @@ -36,10 +36,11 @@ jobs: - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14" - - name: Install zizmor (pinned) + - name: Install zizmor (hash-pinned from the CI toolchain lock) run: | - # Pin deliberately; verify the tag at https://github.com/zizmorcore/zizmor/releases if it 404s. - python -m pip install --upgrade pip "zizmor==1.5.2" + # HASH-PINNED from the lock (ADR 0034 §3). The version lives in pyproject.toml's + # [dependency-groups].ci-scanners with its rationale; bump it there, then re-export. + python -m pip install --require-hashes -r ci/locks/ci-scanners.lock # actionlint FIRST: it catches the class zizmor structurally cannot. An invalid `${{ }}` anywhere # in a `run:` body -- comments included -- aborts workflow COMPILATION, so no jobs are created and # every required context silently never appears (the PR looks stuck, not red). zizmor parses the diff --git a/tests/test_ci_venv_pinning.py b/tests/test_ci_venv_pinning.py index a2921331..70a95337 100644 --- a/tests/test_ci_venv_pinning.py +++ b/tests/test_ci_venv_pinning.py @@ -54,11 +54,37 @@ The gitleaks/trivy downloads are the non-pip half of the same intake, covered by `test_release_asset_downloads_in_blocking_jobs_are_checksum_verified` at the end of this module. + +**Amendment, 2026-07-29 — the CI toolchain moved into a PEP 735 dependency group.** + +A version pin does not satisfy Scorecard's `PinnedDependenciesID`; this repo's own alert data proves it +(`bandit==1.9.4` is alert #74 and `zizmor==1.5.2` is alert 96, both exactly pinned and both flagged, +while the two `--require-hashes` installs are flagged in neither the open nor the dismissed set). So +`bandit`, `pip-audit`, `zizmor`, `mutmut`, `diff-cover` and `pytest-cov` now live in +`pyproject.toml`'s `[dependency-groups]`, flow into `uv.lock`, and are `uv export`ed to +`ci/locks/ci-scanners.lock` / `ci/locks/ci-quality.lock` WITH hashes. The workflows install them with +`pip install --require-hashes -r `. `bandit` and `pip-audit` were consequently dropped from +`RELEASE_PINNED_TOOLS` — deliberately, in this same commit, exactly as that check's own failure +message instructs. + +**The blindness that creates, and what closes it.** `_install_targets()` skips `-r` *and its +argument*, so `pip install --require-hashes -r ci/locks/ci-scanners.lock` names ZERO targets: it +passes every scan in the first half of this module by being invisible to it, not by being verified. +`test_lock_installed_toolchain_*` and `test_*_moved_tool*` below are what actually look at those +lines. In particular `test_lock_installed_toolchain_locks_are_in_the_dep1_set` is the guard against +ADR 0034 §3's stated failure mode — a hand-maintained lock outside the DEP-1 export/resync machinery +rotting into a pinned, stale, *unpatched* toolchain, which is worse posture than floating. + +`semgrep` is deliberately NOT in a group and stays a version-pinned inline install: it hard-conflicts +with the project's own `[otel]` extra, and the only resolution (`[tool.uv] conflicts`) would declare a +shipped product extra and a CI scanner permanently mutually exclusive. Recorded as a reasoned residual +in ADR 0034 §3, which is why it is still in `RELEASE_PINNED_TOOLS`. """ from __future__ import annotations import re +import tomllib from pathlib import Path import pytest @@ -172,10 +198,14 @@ def test_scratch_venvs_do_not_hide_an_unpinned_pip_fetch(workflow: str) -> None: # The scanners the BLOCKING jobs install for themselves. These run on every `pull_request` and # three of them back required contexts, so they are held to the release rule despite not being on # the release path — see the 2026-07-29 correction in the module docstring. - ("security.yml", "pip-audit"), + # + # `uv` is the resolver that produces every lock this repo commits, so it cannot be hash-locked BY + # that lock (circular by construction) and stays an inline `==` pin. `semgrep` stays inline by + # decision (the `[otel]` conflict — see the amendment). `bandit` and `pip-audit` were HERE until + # 2026-07-29 and are now installed from `ci/locks/ci-scanners.lock` instead; they are checked by + # `LOCK_INSTALLED_TOOLCHAINS` + `MOVED_TO_A_GROUP` below, not by this table. ("security.yml", "uv"), ("security.yml", "semgrep"), - ("security.yml", "bandit"), ) #: `security.yml`'s OWN unpinned installs, registered rather than pinned. Only the `pip` bootstraps @@ -188,8 +218,30 @@ def test_scratch_venvs_do_not_hide_an_unpinned_pip_fetch(workflow: str) -> None: #: `uv` and `pip-audit` were HERE until 2026-07-29 and are now pinned instead; the reasoning that put #: them here — that this file's jobs are schedule/dispatch-only — was factually wrong (it triggers on #: `pull_request` and `pip-audit` is a required context). See the module docstring. +#: +#: Only ONE `--upgrade pip` bootstrap is left in the file (the `uv` install in the DEP-1 step). The +#: other two went away with their steps rather than being pinned: the `bandit` and `pip-audit` installs +#: are now `pip install --require-hashes -r ci/locks/ci-scanners.lock`, and that lock hash-pins `pip` +#: itself — so a hash-verified pip arrives in the same command that used to fetch an unverified one. SECURITY_YML_ACCEPTED_UNPINNED = frozenset({"pip"}) +#: ``(workflow, lock)`` pairs where a CI toolchain is installed from a hash-pinned `uv export`. +#: +#: These lines are INVISIBLE to every scan in the first half of this module: `_install_targets` skips +#: `-r` and its argument, so `pip install --require-hashes -r ` names zero packages and passes by +#: not being looked at. The five tests below are the ones that look. +LOCK_INSTALLED_TOOLCHAINS = ( + ("security.yml", "ci/locks/ci-scanners.lock"), + ("zizmor.yml", "ci/locks/ci-scanners.lock"), + ("quality-advisory.yml", "ci/locks/ci-quality.lock"), +) + +#: Tools that MOVED from an inline `pip install ==` into a PEP 735 dependency group. Two +#: directions need guarding, and neither is visible to anything else: the declaration silently +#: disappearing from `pyproject.toml` while the lock still carries the name as somebody else's +#: transitive, and a future edit re-adding an inline `pip install bandit` beside the lock install. +MOVED_TO_A_GROUP = ("bandit", "pip-audit", "zizmor", "mutmut", "diff-cover", "pytest-cov") + def _install_targets(line: str) -> list[str]: """The package tokens a ``pip install`` line names — flags, and the arguments of flags that take @@ -319,6 +371,171 @@ def test_security_yml_unpinned_installs_are_registered() -> None: ) +# --- the CI toolchain installed from a hash-pinned dependency-group lock --------------------------- + + +def _dist_name(spec: str) -> str: + """The distribution name from a PEP 508 requirement string (`"mutmut==3.6.0"` -> `"mutmut"`).""" + match = re.match(r"^[A-Za-z0-9][A-Za-z0-9._-]*", spec.strip()) + return match.group(0) if match else "" + + +def _names_target(line: str, package: str) -> bool: + """Does this `pip install` line name ``package`` as a target (bare, or with a version spec)?""" + return any( + target == package or target.startswith(tuple(f"{package}{op}" for op in _SPEC_OPS)) + for target in _install_targets(line) + ) + + +def _locked_requirements(lock: Path) -> list[tuple[str, int]]: + """Each requirement in a `uv export`, paired with how many `--hash=sha256:` lines it OWNS. + + Structural, per requirement, rather than a whole-file substring search. A `"--hash=sha256:" in + text` check passes on a lock carrying 33 pinned packages and ONE hash — which is precisely the + unverified-install shape it is supposed to be catching. (Learned the hard way: that weaker form + stayed green under an injected regression while the six sibling guards all went red.) + + `uv export` emits `name==version \\` followed by indented `--hash=` continuations, then an optional + `# via ...` comment. + """ + out: list[tuple[str, int]] = [] + for raw in lock.read_text(encoding="utf-8").splitlines(): + line = raw.strip() + if not line or line.startswith("#"): + continue + if line.startswith("--hash="): + if out: # a hash before any requirement would be malformed; ignore rather than crash + name, count = out[-1] + out[-1] = (name, count + 1) + elif not line.startswith("-"): + out.append((line.removesuffix("\\").strip(), 0)) + return out + + +@pytest.mark.parametrize(("workflow", "lock"), LOCK_INSTALLED_TOOLCHAINS) +def test_lock_installed_toolchain_install_is_hash_verified(workflow: str, lock: str) -> None: + """Installing FROM a hashed lock buys nothing unless `--require-hashes` is actually passed. + + Without the flag pip treats the file as an ordinary requirements list, ignores every `--hash=` + line, and is free to resolve a different artifact — a silently unpinned install that still reads + like a pinned one at a glance. + """ + lines = [ + ln for ln in _code_lines(_WORKFLOWS / workflow) if _PIP_INSTALL.search(ln) and lock in ln + ] + # Non-vacuity: a renamed lock or a restructured step must fail loudly, not pass by matching nothing. + assert lines, ( + f"{workflow} no longer installs from {lock} — if the toolchain moved, re-point " + f"LOCK_INSTALLED_TOOLCHAINS in the same commit rather than letting this pass vacuously" + ) + for ln in lines: + assert "--require-hashes" in ln, ( + f"{workflow} installs {lock} WITHOUT --require-hashes: {ln!r}. The hashes in the lock are " + f"inert unless the flag is passed, so this is an unpinned install wearing a lock's clothes." + ) + + +@pytest.mark.parametrize(("workflow", "lock"), LOCK_INSTALLED_TOOLCHAINS) +def test_lock_installed_toolchain_lock_is_pinned_and_hashed(workflow: str, lock: str) -> None: + """The lock itself must be fully `==`-pinned AND carry hashes. + + Blocks substituting a `--no-hashes` export (the shape `constraints.lock` legitimately has, because + pip rejects hashes in a `--constraint` file): that would keep the filename, keep the + `--require-hashes` flag, and fail closed at install time instead of silently — but the failure + would first appear in CI rather than here. + """ + path = _REPO / lock + assert path.is_file(), f"{workflow} installs {lock}, which is not committed" + reqs = _locked_requirements(path) + total_hashes = sum(n for _, n in reqs) + print(f"[ci-venv-pinning] {lock}: {len(reqs)} requirements, {total_hashes} hashes") + assert reqs, ( + f"{lock} declares no requirements — an empty lock installs nothing and proves nothing" + ) + unpinned = [name for name, _ in reqs if "==" not in name] + assert not unpinned, f"{lock} has requirement lines without an `==` pin: {unpinned}" + # EVERY requirement, not "the file contains a hash somewhere" — see _locked_requirements. + unhashed = [name for name, count in reqs if count == 0] + assert not unhashed, ( + f"{lock} pins these with `==` but carries NO `--hash=` line for them: {unhashed}. Under " + f"`--require-hashes` pip would reject the file outright, so this is a CI break rather than a " + f"silent hole — but a lock exported with `--no-hashes` defeats the whole point of routing the " + f"toolchain through uv.lock (ADR 0034 §3), and it should fail here rather than in the runner." + ) + + +@pytest.mark.parametrize("lock", sorted({lock for _, lock in LOCK_INSTALLED_TOOLCHAINS})) +def test_lock_installed_toolchain_locks_are_in_the_dep1_set(lock: str) -> None: + """Each toolchain lock must be REGENERATED and diff-gated by DEP-1, not hand-maintained. + + This is the guard against ADR 0034 §3's stated failure mode: "a hand-maintained fifth lock outside + that machinery would rot into a pinned, STALE, UNPATCHED toolchain — worse posture than floating". + A hash-pinned toolchain is only an improvement while something keeps it fresh. + + Only the gate half is asserted here; `tests/test_dep1_lock_resync_lockstep.py` then forces the + Dependabot resync to export, short-circuit on, and stage the identical set. + """ + body = (_WORKFLOWS / "security.yml").read_text(encoding="utf-8") + assert re.search(rf"^\s*uv export\s+.*-o {re.escape(lock)}\s*$", body, re.MULTILINE), ( + f"security.yml's DEP-1 step does not `uv export` {lock}, so nothing regenerates it and the " + f"drift gate never compares it. A stale hash-pinned toolchain is worse than a floating one." + ) + + +@pytest.mark.parametrize("package", MOVED_TO_A_GROUP) +def test_moved_tools_are_declared_in_a_dependency_group(package: str) -> None: + """Each moved tool is still DECLARED in `[dependency-groups]`, not merely present in the lock. + + The direction this catches: the declaration is deleted while the exported lock still carries the + name as some other tool's transitive. The install keeps working, the version stops being ours to + choose, and every other check here still passes. + """ + groups = tomllib.loads((_REPO / "pyproject.toml").read_text(encoding="utf-8")).get( + "dependency-groups" + ) + assert groups, ( + "pyproject.toml declares no [dependency-groups] — the CI toolchain lost its source" + ) + declared = { + _dist_name(spec) + for specs in groups.values() + for spec in specs or [] + # PEP 735 lets an entry be a `{include-group = "..."}` table rather than a requirement string. + if isinstance(spec, str) + } + print(f"[ci-venv-pinning] [dependency-groups] declares: {sorted(declared)}") + assert package in declared, ( + f"{package!r} is installed from a ci/locks lock but is no longer declared in any " + f"[dependency-groups] entry ({sorted(declared)}). It may still resolve as a transitive of " + f"another tool — in which case its version is no longer a decision this repo makes." + ) + + +def test_no_moved_tool_is_reinstalled_inline() -> None: + """No moved tool may ALSO be installed by an inline `pip install `. + + The regression: someone adds `pip install bandit` beside the lock install (to debug, or because a + lock refresh was inconvenient). Both run, the last one wins, and the version that actually executes + is the unpinned one — while every hash-pinning check above still passes, because they only ever + look at the lock line. + """ + offenders = [ + f"{workflow}: {package} named inline in {ln!r}" + for workflow in sorted({wf for wf, _ in LOCK_INSTALLED_TOOLCHAINS}) + for ln in _code_lines(_WORKFLOWS / workflow) + if _PIP_INSTALL.search(ln) + for package in MOVED_TO_A_GROUP + if _names_target(ln, package) + ] + assert not offenders, ( + "a tool routed through a hash-pinned dependency group is ALSO installed inline:\n " + + "\n ".join(offenders) + + "\nRemove the inline install; bump the pin in pyproject.toml's [dependency-groups] and " + "re-export instead." + ) + + # --- the non-pip half of the same intake: fetched release assets ---------------------------------- diff --git a/tests/test_quality_advisory_invariants.py b/tests/test_quality_advisory_invariants.py index 7573fba3..ef8c9eb4 100644 --- a/tests/test_quality_advisory_invariants.py +++ b/tests/test_quality_advisory_invariants.py @@ -17,12 +17,41 @@ """ import re +import tomllib from pathlib import Path import pytest import yaml -_WORKFLOW = Path(__file__).resolve().parents[1] / ".github" / "workflows" / "quality-advisory.yml" +_REPO = Path(__file__).resolve().parents[1] +_WORKFLOW = _REPO / ".github" / "workflows" / "quality-advisory.yml" +#: The hash-pinned `uv export` of `[dependency-groups].ci-quality` that this workflow installs from. +_CI_QUALITY_LOCK = _REPO / "ci" / "locks" / "ci-quality.lock" + + +def _group_pin(package: str) -> str: + """The PEP 508 spec ``package`` is declared with in ``[dependency-groups]``. + + The advisory tools' pins moved out of this workflow and into `pyproject.toml` so they could flow + into `uv.lock` and be exported WITH HASHES (ADR 0034 §3 — a version pin does not satisfy Scorecard's + PinnedDependenciesID). Fails loudly rather than returning a default: "the declaration is gone" and + "the declaration is unpinned" must not produce the same result. + """ + groups = tomllib.loads((_REPO / "pyproject.toml").read_text(encoding="utf-8")).get( + "dependency-groups" + ) + assert groups, "pyproject.toml declares no [dependency-groups]" + for specs in groups.values(): + for spec in specs or []: + # A PEP 735 entry may be a `{include-group = "..."}` table rather than a requirement string. + if isinstance(spec, str) and re.match( + rf"^{re.escape(package)}(?![A-Za-z0-9._-])", spec + ): + return spec + raise AssertionError( + f"{package!r} is not declared in any [dependency-groups] entry: {groups!r}" + ) + # Steps that actually run a quality tool. Setup steps (checkout, setup-python, apt-get, the tool # installs) are deliberately NOT required to be continue-on-error: masking an infrastructure failure @@ -158,9 +187,24 @@ def test_jscpd_stays_on_4x(raw: str) -> None: assert re.search(r"jscpd@4\.\d+\.\d+", raw), "jscpd must stay pinned to a 4.x release" -def test_diff_cover_is_pinned_exactly(raw: str) -> None: - """The annotation surface depends on this version's `--format github-annotations:`.""" - assert re.search(r'"diff-cover==\d+\.\d+\.\d+"', raw), "diff-cover must be pinned with ==" +def test_diff_cover_is_pinned_exactly() -> None: + """The annotation surface depends on this version's `--format github-annotations:`. + + Re-pointed 2026-07-29: the pin moved OUT of this workflow. `diff-cover` is now declared in + `pyproject.toml`'s `[dependency-groups].ci-quality` and installed from the hash-pinned + `ci/locks/ci-quality.lock`, because a version pin alone does not satisfy Scorecard's + `PinnedDependenciesID` (ADR 0034 §3). So assert the pin where it now lives — in BOTH places, since + an exact spec in pyproject that the exported lock disagrees with would mean the lock is stale. + """ + pin = _group_pin("diff-cover") + assert re.fullmatch(r"diff-cover==\d+\.\d+\.\d+", pin), ( + f"diff-cover must be pinned exactly in [dependency-groups], got {pin!r}" + ) + version = pin.split("==", 1)[1] + lock = _CI_QUALITY_LOCK.read_text(encoding="utf-8") + assert re.search(rf"^diff-cover=={re.escape(version)}\b", lock, re.MULTILINE), ( + f"ci/locks/ci-quality.lock does not pin diff-cover=={version} — re-run the DEP-1 exports" + ) def test_the_ruff_version_is_derived_from_the_lock_not_hardcoded(workflow: dict, code: str) -> None: @@ -265,10 +309,29 @@ def test_step_summary_writes_are_size_guarded(code: str) -> None: def test_mutmut_is_pinned_to_3x_with_pytest_timeout(code: str) -> None: """mutmut 2.5.1 crashes on Python 3.14 before generating a mutant, and `|| true` made that look green for months. pytest-timeout is not optional: mutmut 3 always passes `--timeout` to pytest, - and without the plugin every invocation dies inside BadTestExecutionCommandsException.""" - assert re.search(r'"mutmut==3\.\d+\.\d+"', code), "mutmut must be pinned to an exact 3.x" + and without the plugin every invocation dies inside BadTestExecutionCommandsException. + + Re-pointed 2026-07-29 to `[dependency-groups].ci-quality` + `ci/locks/ci-quality.lock` (ADR 0034 + §3). The two BEHAVIOURAL assertions are kept and still asserted against the LOCK, which is what the + job actually installs: `mutmut` must be a 3.x, and `pytest-timeout` must be present. Those are + properties of the installed environment, not of a pin's spelling. + """ + pin = _group_pin("mutmut") + assert re.fullmatch(r"mutmut==3\.\d+\.\d+", pin), ( + f"mutmut must be pinned to an exact 3.x in [dependency-groups], got {pin!r}" + ) assert "mutmut<3" not in code, "mutmut 2.x does not run on Python 3.14" - assert "pytest-timeout" in code, "mutmut 3 requires pytest-timeout" + lock = _CI_QUALITY_LOCK.read_text(encoding="utf-8") + assert re.search(r"^mutmut==3\.", lock, re.MULTILINE), ( + "ci/locks/ci-quality.lock does not resolve mutmut to a 3.x — re-run the DEP-1 exports" + ) + assert re.search(r"^pytest-timeout==", lock, re.MULTILINE), ( + "ci/locks/ci-quality.lock carries no pytest-timeout; mutmut 3 always passes `--timeout` to " + "pytest, so every invocation would die inside BadTestExecutionCommandsException" + ) + assert "ci/locks/ci-quality.lock" in code, ( + "the mutation job must install from the hash-pinned toolchain lock" + ) def test_mutmut_copies_the_package_not_just_the_mutated_file(code: str) -> None: From 7a340c78f1fa39442f9c8afbb5cbc91ed8a4729f Mon Sep 17 00:00:00 2001 From: wshallwshall Date: Wed, 29 Jul 2026 20:37:45 -0500 Subject: [PATCH 4/6] docs(adr): amend 0034 -- section 3's convergence target is built; name every residual Amends ADR 0034 (no new number minted; docs/BACKLOG.md untouched). Supersedes section 3's "Deferred, recorded here as the convergence target", the "Option B ... remains the only thing that closes the alert" sentence in Recommended hardening, and the "four committed lock artifacts" count -- there are now six. Records what a dismissal needs to stay dismissed honestly: the reason, visible. WHAT CLOSES -- and the uncomfortable part first: this does NOT reduce the open count. The three lines converted (bandit #74, zizmor 96, pip-audit) are all in the DISMISSED set, so the real effect is that three dismissals whose recorded reason ("CI installs editably, which cannot use --require-hashes") is now FALSE for those lines become re-triageable AS FIXED. Under this ADR's own Decision -- a dismissal with a false reason is worse than an open finding -- that is the point, not a consolation. The 2 genuinely OPEN PinnedDependenciesID alerts are the SBOM scratch-venv cyclonedx-bom pair, and closing them requires moving both release.yml and security.yml in lockstep (the byte-identity test forces it). Recorded as the next increment, not claimed. RESIDUALS, each with its reason rather than implied: * sigstore==4.4.0 -- NOT a gap but an owner decision this change must not invert. The lock resolves 4.5.0; the pin is 4.4.0 because dependabot.yml sets cooldown.default-days: 5. Routing it through the lock would pin the SIGNING toolchain fresher than the repo's own policy allows, at the highest-privilege point in the build. Left exactly as it is. * the uv bootstrap -- permanently circular: you cannot hash-lock uv with uv. Includes the cheap out-of-band fix that would remove it (setup-uv is already SHA-pinned in 9 places). * pipx install ruff -- outside the guard's _PIP_INSTALL regex AND outside Scorecard's, so there is no alert to close; pipx has no --require-hashes. * semgrep -- excluded by decision, with the exact [tool.uv] conflicts recipe written down so a future owner can flip it in one commit instead of re-deriving the analysis. * the editable installs -- structurally unhashable; section 3's original rationale stands. Also recorded rather than glossed: * The convergence-rule cost. security.yml gained ~9 lines near the top, so every dismissed alert anchored below re-fires as a new number. NO claim of line-neutrality is made. * The one evidence gap and its bound: dependabot-lock-resync.yml skips on a human PR, so its new export lines are text-verified only until the next Dependabot uv PR -- bounded by the flag-identity test plus the fact that the GATE does execute on this PR. * Dependabot's PEP 735 visibility is UNVERIFIED and unverifiable pre-merge. New AC-8 with a link to the tests that verify it, so adr-analyze resolves it. --- ...is-triage-policy-accepted-risk-register.md | 129 ++++++++++++++++++ 1 file changed, 129 insertions(+) diff --git a/docs/adr/0034-static-analysis-triage-policy-accepted-risk-register.md b/docs/adr/0034-static-analysis-triage-policy-accepted-risk-register.md index c376bcbd..19422891 100644 --- a/docs/adr/0034-static-analysis-triage-policy-accepted-risk-register.md +++ b/docs/adr/0034-static-analysis-triage-policy-accepted-risk-register.md @@ -70,6 +70,8 @@ Scorecard runs on the same mirror and surfaced **48 findings**. These are **repo → `tests/test_multipart.py::test_oversized_part_header_is_refused_not_parsed` - **AC-7** — WHEN the multipart parser scans a `Content-Disposition` line, THE SYSTEM SHALL do so in time linear in the line's length. → `tests/test_multipart.py::test_hostile_disposition_header_parses_in_linear_time` +- **AC-8** — WHERE a CI job installs a third-party quality or security tool that this repo routes through a PEP 735 dependency group, THE SYSTEM SHALL install it from a hash-pinned `uv export` with `--require-hashes`, and THAT lock SHALL be regenerated and diff-gated by the DEP-1 step rather than hand-maintained. + → `tests/test_ci_venv_pinning.py` (`test_lock_installed_toolchain_*`, `test_moved_tools_are_declared_in_a_dependency_group`, `test_no_moved_tool_is_reinstalled_inline`) · `tests/test_dep1_lock_resync_lockstep.py` ## Options considered @@ -253,3 +255,130 @@ same way; suppressing this rule for a scanner is the industry-standard dispositi **Convergence note (per the rule above):** the anchor is `tls_probe.py:146`, inside `_offer_context`. That module is new and small, so expect this to re-fire as a fresh alert number the first time anything is inserted above line 146. Re-dismiss with this rationale rather than re-triaging from scratch. + +## Amendment — 2026-07-29: §3's convergence target is BUILT — the CI toolchain is hash-pinned + +§3 above ("A version pin does not satisfy this check") named the fix and deferred it: *"The correct fix +is to route CI tooling through a `pyproject` dependency group so it flows into `uv.lock` and the +exports."* That is now built. **This section supersedes §3's "Deferred, recorded here as the convergence +target" and the "Option B … remains the only thing that closes the alert" sentence in *Recommended +hardening*, and it retires the "four committed lock artifacts" count — there are now six.** + +### What is now genuinely hash-pinned + +`pyproject.toml` gained a PEP 735 `[dependency-groups]` table with two groups, exported to two new +committed artifacts and consumed with `--require-hashes`: + +| Group | Lock | Installed by | Contents | +|---|---|---|---| +| `ci-scanners` | `ci/locks/ci-scanners.lock` (33 reqs, 163 hashes) | `security.yml` pip-audit + bandit jobs, `zizmor.yml` | `bandit==1.9.4`, `pip-audit==2.10.1`, `zizmor==1.5.2` | +| `ci-quality` | `ci/locks/ci-quality.lock` (27 reqs, 164 hashes) | `quality-advisory.yml` coverage + mutation jobs | `diff-cover==10.4.1`, `mutmut==3.6.0`, `pytest-cov>=7.0`, `pytest-timeout>=2.3` | + +Four design decisions worth recording, because each is a place a later change could silently undo the +posture: + +1. **The split is the merge path.** `ci-scanners` is what the *blocking* gates install for themselves; + `ci-quality` is *advisory* measurement. This keeps `mutmut` — a mutation engine that rewrites and + executes source — out of every required gate's dependency closure. +2. **Not extras, and not `[tool.uv] default-groups`.** An extra is published wheel metadata, so + `pip install messagefoundry[ci-scanners]` would become a real install target; a dependency group + never ships. A *default* group would land in all four pre-existing DEP-1 artifacts — i.e. in the + release SBOM, the container image locks, and in what `pip-audit` audits **as runtime**. Verified: + with these groups non-default, all four re-export byte-identically (`git diff --exit-code` → 0). +3. **The locks are inside the DEP-1 machinery, not beside it.** §3's own objection to a fifth lock was + that a hand-maintained one "would rot into a pinned, **stale, unpatched** toolchain — worse posture + than floating". So both are `uv export`ed by `security.yml`'s DEP-1 step, diff-gated there, and + re-exported + staged by `dependabot-lock-resync.yml`. `tests/test_dep1_lock_resync_lockstep.py` + enforces the six-place lockstep; `test_lock_installed_toolchain_locks_are_in_the_dep1_set` enforces + that a lock the workflows install is one the gate regenerates. +4. **`pip-audit` now audits the toolchain locks too** (`pip-audit -r ci/locks/*.lock --desc`). This is + the load-bearing half of decision 3: hash-pinning makes a toolchain *sticky*, so without this a CVE + in a pinned scanner would be invisible to every gate. `--ignore-vuln ` is the escape hatch, as + for the runtime lock. Consequence accepted deliberately: a CVE in `mutmut`'s or `diff-cover`'s + closure will red a **required** gate over an advisory tool. + +Two unpinned `pip install --upgrade pip` bootstraps disappeared rather than being pinned: the +`ci-scanners` lock hash-pins `pip==26.2` itself (it arrives as a `pip-audit` → `pip-api` dependency), so +a hash-verified pip now lands in the same command that previously fetched an unverified one. + +### Which findings this closes + +Three Scorecard-visible `pip install` lines became `--require-hashes` installs: + +| Line | Alert | Note | +|---|---|---| +| `security.yml` bandit step | **#74** | the exact proof-case §3 cites | +| `zizmor.yml` zizmor install | **96** | §3's second proof-case | +| `security.yml` pip-audit step | whichever anchors that line | previously `pip-audit==2.10.1`, unhashed | + +**Stated honestly: this does not reduce the OPEN count.** #74 and 96 are in the **dismissed +(`won't fix`)** set, so the effect is that three dismissals whose recorded reason ("CI installs +editably, which cannot use `--require-hashes`") is now *false for those lines* become re-triageable **as +fixed**. Under this ADR's own Decision — a dismissal with a false reason is worse than an open finding — +that is the point, not a consolation. + +The **2 genuinely open** `PinnedDependenciesID` alerts are untouched: they are the medium pair on the +**SBOM scratch venv** (`python -m pip install "pip==26.1.2" "cyclonedx-bom~=7.3.1"`, in `release.yml` +and `security.yml`). Closing them needs a third group *and* moving **both** halves in lockstep, because +`test_sbom_install_is_byte_identical_in_release_and_security` requires the two commands to stay +identical. `release.yml` was deliberately scoped out of this change (see the residuals), so this is +recorded as the next increment rather than done. + +### Residuals — dismissals that stay dismissed, and why + +A `won't fix` makes an item invisible, so each remaining one is named with its reason rather than +implied: + +| Residual | Why it is not fixed here | +|---|---| +| **`release.yml`'s `sigstore==4.4.0`** | **Not a gap — an owner decision this change must not invert.** The lock resolves `sigstore` to **4.5.0**; ADR 0034 pins 4.4.0 because `.github/dependabot.yml` sets `cooldown.default-days: 5` and 4.5.0 was <48 h old. Routing `sigstore` through the lock would pin the *signing* toolchain fresher than the repo's own update policy allows — inverting that policy at the highest-privilege point in the build (the job holding `contents`/`id-token`/`attestations: write`). Left exactly as it is. Re-evaluate when 4.5.0 ages out. | +| **The `uv` bootstrap** (`security.yml`, `python -m pip install --upgrade pip "uv==0.12.0"`) | **Permanently circular: you cannot hash-lock `uv` with `uv`.** That install produces every lock this repo commits. `uv` stays an inline `==` pin, and `pip` remains the sole registered entry in `SECURITY_YML_ACCEPTED_UNPINNED`. *Cheap out-of-band fix that removes it entirely:* `astral-sh/setup-uv@c771a70e…` is already SHA-pinned and used in 9 places (`ci.yml` ×6, `quality-advisory.yml` ×2, the resync ×1); swapping it in deletes the install. Separate change. | +| **`quality-advisory.yml`'s `pipx install ruff`** | **Outside the guard's regex and outside Scorecard's.** `test_ci_venv_pinning.py`'s `_PIP_INSTALL` matches `pip`/`pip3`/`python -m pip` only, so the unpinned fallback branch is invisible to every existing guard — and because it is not a `pip install`, **no alert exists to close**. `pipx` has no `--require-hashes`, so fixing it means changing the install mechanism, not the pin. Recorded, not done. | +| **`semgrep`** | **Excluded by decision.** `semgrep==1.172.0` requires `opentelemetry-sdk>=1.37,<1.38` while the project's `[otel]` extra resolves 1.44. In a plain group the universal resolve silently **downgrades the shipped otel runtime** in all four pre-existing DEP-1 artifacts — measured and bisected to semgrep alone (the other tools give DIFFS=0). The only fix is `[tool.uv] conflicts = [[{ extra = "otel" }, { group = "semgrep-tools" }]]`, which declares a **product extra** and a **CI scanner** permanently mutually exclusive (`uv sync --all-extras --all-groups` would stop working) and still forces a `click 8.4.1 → 8.4.2` re-resolve across all four artifacts. Pinning a *scanner*'s supply chain is not worth a lasting constraint on a shipped surface. The recipe is written down here so a future owner can flip it in one commit rather than re-deriving the analysis. | +| **The 5 editable `pip install -e ".[…]"` sites** + 7 `uv pip install --system -e` sites | Structurally unhashable; §3's original rationale is correct for these and stands. | +| Docker digest-pinning (7), `TokenPermissions` (6), `BranchProtection`/`CodeReview`/`Maintained` (3), `Fuzzing`, `CIIBestPractices` | Different check classes; unchanged by this work. | + +### Two notes on evidence + +**Pre-merge coverage was the reason for the scope choice.** All three targeted workflows run on +`pull_request` — `security.yml`'s pip-audit and bandit jobs are *required contexts*, `zizmor.yml`'s +`paths: .github/**` filter matches this change, and `quality-advisory.yml`'s install steps are not +`continue-on-error`, so a bad hashed install reds the job visibly. That is the opposite of the +"What no test can see" problem the 2026-07-28 round had to accept, and it is why `release.yml` was left +alone: nothing in PR CI executes it, so a break there first surfaces at a tag. + +**The one gap, and its bound.** `dependabot-lock-resync.yml` triggers on `pull_request` for +`pyproject.toml`/`uv.lock`, but its `if:` requires `pull_request.user.login == 'dependabot[bot]'`, so it +**skips** on a human PR and there is no `workflow_dispatch` route. Its two new export lines are +text-verified only and first execute on the next Dependabot uv PR. The bound is the same one +`test_sbom_install_is_byte_identical_in_release_and_security` already relies on: +`test_export_flags_are_identical_per_lock_file` forces the resync's flag strings byte-identical to the +gate's, **and the gate does execute on this PR** — a dry-run is evidence for as long as the two are the +same command. + +**Unverified, and it cannot be verified before merge:** whether Dependabot's `uv` ecosystem enumerates +`[dependency-groups]` at all. First observation is the next weekly uv PR. If it does not, routine +staleness returns — which is precisely why the `pip-audit` addition (decision 4) is load-bearing: it +converts "silently stale" into "loudly red within ~24 h on anything security-relevant" via the daily +cron. Confirm after the next Dependabot run. + +### Convergence-rule consequence, stated rather than glossed + +`security.yml` gained ~9 lines near the top of the file, so per the line-drift rule above **every +dismissed alert anchored below them re-fires as a new alert number**. That is unavoidable when adding +exports to an early step. The mitigation applied: workflow comments were kept short and each pin's +rationale moved to `pyproject.toml` beside the pin itself (which is also where a future bumper will +look). **No claim of line-neutrality is made here** — expect to re-dismiss, and re-dismiss with the +rationale in this section rather than re-triaging from scratch. + +### Adjacent fix folded in + +`.gitattributes` was missing **`constraints.lock`** (`git check-attr text -- constraints.lock` → +`unspecified`), so under `core.autocrlf=true` it checks out CRLF — exactly the drift that stanza exists +to prevent. Harmless so far only because git's clean filter normalizes before `git diff`, which is also +why export sync must be verified with `git diff` and never a raw `diff`. Fixed alongside the new +`ci/locks/*.lock text eol=lf` entry. Closes no alert. + +**Still open, not done:** `quality-advisory.yml`'s `pipx install ruff` fallback installs *unpinned* ruff +instead of failing closed, and `constraints.lock` is `sed`-scraped for a `ruff==` pin that — unlike +`packaging==` — has **no PR-time canary test**. Both are recorded here; neither closes a Scorecard alert. From f1c2e72f5f64889602c3a8a037559f09cdf5b8ee Mon Sep 17 00:00:00 2001 From: wshallwshall Date: Wed, 29 Jul 2026 21:34:24 -0500 Subject: [PATCH 5/6] test(pins): close seven guard holes the toolchain move opened The adversarial pass found no wrong byte in any lock -- `uv lock --check` exit 0, all six exports byte-identical, the uv.lock diff 588 insertions / 0 deletions. Every finding was a rule the prose asserted and nothing checked. Each hole below was reproduced by injecting the regression and confirming the suite stayed GREEN, then closed and confirmed RED. 1. THE `==` PIN WAS ENFORCED BY NOTHING. Moving bandit/pip-audit out of RELEASE_PINNED_TOOLS deleted the only check that rejected a floor (_PIN_OPS excludes `>=` by design). Neither replacement can see one: asserting the NAME is declared does not, and asserting the EXPORTED lock is `==`-pinned cannot, because `uv export` writes `bandit==1.9.4` from `bandit>=1.9.4` just as readily. Proven: all three scanner specs rewritten to `>=` re-locked, re-exported the two toolchain locks BYTE-IDENTICALLY, and passed every guard in this repo. EXACT_GROUP_PINS + FLOOR_BY_DESIGN now assert the shape at the declaration, where the decision lives. 2. THE EXPORT SELECTOR WAS UNPINNED. `uv export .* -o ` accepts `--group` for `--only-group`, and `--group` is additive: it grew ci-scanners.lock from 33 to 69 requirements, pulling fastapi/uvicorn/hl7/httpx/aiosqlite into what the BLOCKING bandit and zizmor jobs install -- fully pinned, fully hashed, byte-identical under DEP-1, nothing red. The command is now matched exactly, with the group required to match the lock's stem, and test_each_group_pin_reaches_its_own_lock ties the two together from the other direction. 3. FIVE INSTALL SITES, THREE TABLE ROWS, CHECKED WITH ">= 1". Four of the five were individually deletable at zero test cost. The coverage job's failure is the dangerous one because it is silent: `pytest -q --cov` dies on `unrecognized arguments`, `|| true` swallows it, and diff-coverage reports "skipped" and exits 0. The counts are now exact, and the two quality-advisory assertions resolve under `jobs.coverage` / `jobs.mutation` instead of searching the whole file. 4. THE BOOTSTRAP INVENTORY SAID ONE; THE FILE HAS TWO (the uv install and the semgrep step). SECURITY_YML_ACCEPTED_UNPINNED registers the NAME `pip`, so it cannot tell two accepted bootstraps from twenty -- now an exact count. 5. THE INLINE-REINSTALL SCAN MISSED `pipx install` (in-repo idiom already: ruff, twice) and swept only three workflows, so `pip install bandit` in ci.yml was invisible. Now both spellings across all 16 workflows. 6. `_locked_requirements` SILENTLY SKIPPED any non-`--hash` directive -- an `--index-url` redirect or an `-e .` line was neither counted nor rejected. Raises now. 7. NOTHING GUARDED THE LOAD-BEARING NON-DEFAULT-GROUP INVARIANT, and `--no-dev` is not the protection it looks like: it disables only the group literally NAMED `dev`. Measured -- `default-groups = ["ci-scanners"]` puts the scanners into requirements.lock (98 -> 121) AND into docker/locks/requirements-core.lock (41 -> 69), the SBOM input, straight past its `--no-dev`; the two `--all-extras` exports pass neither `--no-dev` nor `--no-default-groups`, so they are unguarded by construction. Under that leak 109 guards passed and only the new one red. 12 injected regressions, 12 red, clean revert. Full suite 9476 passed / 817 skipped / 0 failed; ruff + ruff format clean; the DEP-1 six-artifact `git diff --exit-code` exit 0. --- tests/test_ci_venv_pinning.py | 396 ++++++++++++++++++++-- tests/test_quality_advisory_invariants.py | 47 ++- 2 files changed, 411 insertions(+), 32 deletions(-) diff --git a/tests/test_ci_venv_pinning.py b/tests/test_ci_venv_pinning.py index 70a95337..34675f86 100644 --- a/tests/test_ci_venv_pinning.py +++ b/tests/test_ci_venv_pinning.py @@ -75,6 +75,19 @@ ADR 0034 §3's stated failure mode — a hand-maintained lock outside the DEP-1 export/resync machinery rotting into a pinned, stale, *unpatched* toolchain, which is worse posture than floating. +**What moving those two rows out of `RELEASE_PINNED_TOOLS` cost, and what pays it back.** That table was +the only check that rejected a *floor* (`_PIN_OPS` deliberately excludes `>=`), so its removal left the +`==` requirement on `bandit`/`pip-audit`/`zizmor` enforced by nothing: asserting the name is declared +does not see a floor, and asserting the *exported lock* is `==`-pinned cannot see one either, because +`uv export` writes `bandit==1.9.4` from `bandit>=1.9.4` just as readily. `EXACT_GROUP_PINS` / +`FLOOR_BY_DESIGN` restore it at the declaration, where the decision now lives. Three further holes the +2026-07-29 adversarial pass found and this module now closes, each verified by injecting the regression: +the install-site COUNT (five sites collapsed onto three table rows, so four were individually +deletable), the export SELECTOR (`--group` for `--only-group` pulls the whole project runtime into a +blocking scanner's install closure while staying pinned, hashed and byte-identical under DEP-1), and +group names LEAKING into the four runtime exports (`--no-dev` only filters the group literally named +`dev`, so it is not the protection it looks like). + `semgrep` is deliberately NOT in a group and stays a version-pinned inline install: it hard-conflicts with the project's own `[otel]` extra, and the only resolution (`[tool.uv] conflicts`) would declare a shipped product extra and a CI scanner permanently mutually exclusive. Recorded as a reasoned residual @@ -219,21 +232,40 @@ def test_scratch_venvs_do_not_hide_an_unpinned_pip_fetch(workflow: str) -> None: #: them here — that this file's jobs are schedule/dispatch-only — was factually wrong (it triggers on #: `pull_request` and `pip-audit` is a required context). See the module docstring. #: -#: Only ONE `--upgrade pip` bootstrap is left in the file (the `uv` install in the DEP-1 step). The +#: TWO `--upgrade pip` bootstraps are left in the file, not one — counted, because an inventory that +#: undercounts is how a residual becomes invisible: the `uv` install in the DEP-1 step, and the +#: `semgrep` install (`python -m pip install --upgrade pip "semgrep==1.172.0"`, whose *semgrep* pin is +#: registered above in `RELEASE_PINNED_TOOLS` while its unpinned `pip` half is registered HERE). The #: other two went away with their steps rather than being pinned: the `bandit` and `pip-audit` installs #: are now `pip install --require-hashes -r ci/locks/ci-scanners.lock`, and that lock hash-pins `pip` #: itself — so a hash-verified pip arrives in the same command that used to fetch an unverified one. SECURITY_YML_ACCEPTED_UNPINNED = frozenset({"pip"}) -#: ``(workflow, lock)`` pairs where a CI toolchain is installed from a hash-pinned `uv export`. +#: How many `--upgrade pip` bootstraps `security.yml` is KNOWN to still carry. An exact count, not a +#: floor: `SECURITY_YML_ACCEPTED_UNPINNED` registers the *name* `pip`, so it cannot distinguish two +#: accepted bootstraps from twenty. Both survivors are named in the docstring above and in ADR 0034 §3's +#: residuals table; a third must be a decision, and lowering this number is the receipt for removing one. +SECURITY_YML_PIP_BOOTSTRAPS = 2 + +#: ``(workflow, lock, install sites)`` triples where a CI toolchain is installed from a hash-pinned +#: `uv export`. #: #: These lines are INVISIBLE to every scan in the first half of this module: `_install_targets` skips #: `-r` and its argument, so `pip install --require-hashes -r ` names zero packages and passes by -#: not being looked at. The five tests below are the ones that look. +#: not being looked at. The tests below are the ones that look. +#: +#: THE COUNT IS LOAD-BEARING, not decoration. Five install sites collapse onto three (workflow, lock) +#: pairs — `security.yml` installs `ci-scanners.lock` twice (the pip-audit step and the bandit step) and +#: `quality-advisory.yml` installs `ci-quality.lock` twice (the coverage job and the mutation job). An +#: `assert lines` non-vacuity check is satisfied by ONE surviving line, so deleting either of a pair +#: leaves its job installing nothing while every check here stays green. Measured: with only "≥1", four +#: of the five sites were individually deletable at zero test cost — and the coverage job's failure mode +#: is silent (`pytest -q --cov` dies on `unrecognized arguments`, `|| true` swallows it, and the +#: diff-coverage step reports "skipped" and exits 0). LOCK_INSTALLED_TOOLCHAINS = ( - ("security.yml", "ci/locks/ci-scanners.lock"), - ("zizmor.yml", "ci/locks/ci-scanners.lock"), - ("quality-advisory.yml", "ci/locks/ci-quality.lock"), + ("security.yml", "ci/locks/ci-scanners.lock", 2), + ("zizmor.yml", "ci/locks/ci-scanners.lock", 1), + ("quality-advisory.yml", "ci/locks/ci-quality.lock", 2), ) #: Tools that MOVED from an inline `pip install ==` into a PEP 735 dependency group. Two @@ -242,6 +274,28 @@ def test_scratch_venvs_do_not_hide_an_unpinned_pip_fetch(workflow: str) -> None: #: transitive, and a future edit re-adding an inline `pip install bandit` beside the lock install. MOVED_TO_A_GROUP = ("bandit", "pip-audit", "zizmor", "mutmut", "diff-cover", "pytest-cov") +#: Moved tools whose `[dependency-groups]` spec must be an EXACT `==` pin, and why the exactness is the +#: contract rather than a preference. This exists because moving `bandit` and `pip-audit` out of +#: `RELEASE_PINNED_TOOLS` deleted the ONLY check that rejected a floor: `test_moved_tools_are_declared_*` +#: asserts the NAME is declared, and `test_lock_installed_toolchain_lock_is_pinned_and_hashed` inspects +#: the EXPORTED lock — which `uv export` writes fully `==`-pinned from a `>=` spec just as readily, so it +#: is structurally blind to the difference. Verified: rewriting all three scanner specs to `>=` re-locked, +#: re-exported byte-identically, and passed every guard in this repo. +#: +#: The failure that buys: a floor means Dependabot's weekly `uv` PR moves a blocking gate's version +#: inside `uv.lock` with NO `pyproject.toml` diff to review, `dependabot-lock-resync.yml` re-exports and +#: stages the new lock automatically, and a required gate's findings baseline changes GREEN. +EXACT_GROUP_PINS = ("bandit", "pip-audit", "zizmor", "diff-cover", "mutmut") + +#: The counterpart: moved tools deliberately declared as a FLOOR. Enumerated so "floor by design" and +#: "floor nobody noticed" cannot look the same. +#: +#: `pytest-cov` — nothing parses its output; it only has to emit a `coverage.xml` that `diff-cover` +#: reads, so it is floored at the current major and left free for Dependabot to move. +#: `pytest-timeout` — deliberately the IDENTICAL spec to `[project.optional-dependencies].dev`, so the +#: hashed toolchain install cannot re-point the plugin the coverage run already executes under. +FLOOR_BY_DESIGN = ("pytest-cov", "pytest-timeout") + def _install_targets(line: str) -> list[str]: """The package tokens a ``pip install`` line names — flags, and the arguments of flags that take @@ -254,9 +308,16 @@ def _install_targets(line: str) -> list[str]: match = _PIP_INSTALL.search(line) if match is None: # pragma: no cover - callers filter on the same regex return [] + return _tokens_after(line[match.end() :]) + + +def _tokens_after(rest: str) -> list[str]: + """The install targets in the tail of an install command — flags, and the arguments of flags that + take one, removed. Shared by the ``pip`` and ``pipx`` scans so the two cannot disagree about what + counts as a target.""" targets: list[str] = [] skip_next = False - for tok in line[match.end() :].split(): + for tok in rest.split(): if skip_next: skip_next = False continue @@ -380,6 +441,53 @@ def _dist_name(spec: str) -> str: return match.group(0) if match else "" +def _dependency_groups() -> dict[str, list[str]]: + """``[dependency-groups]`` -> group name -> its requirement STRINGS. + + PEP 735 lets an entry be a ``{include-group = "other"}`` table rather than a requirement string; + those are dropped here (the included group is iterated on its own anyway). + """ + groups = tomllib.loads((_REPO / "pyproject.toml").read_text(encoding="utf-8")).get( + "dependency-groups" + ) + assert groups, ( + "pyproject.toml declares no [dependency-groups] — the CI toolchain lost its source" + ) + return { + name: [spec for spec in (specs or []) if isinstance(spec, str)] + for name, specs in groups.items() + } + + +def _group_spec(package: str) -> str: + """The PEP 508 spec ``package`` is declared with in ``[dependency-groups]``. + + Raises rather than returning a default: "the declaration is gone" and "the declaration is unpinned" + must not produce the same result. + """ + for specs in _dependency_groups().values(): + for spec in specs: + if _dist_name(spec) == package: + return spec.strip() + raise AssertionError(f"{package!r} is not declared in any [dependency-groups] entry") + + +def _project_declared_names() -> set[str]: + """Every distribution declared in ``[project]`` — runtime deps plus every extra. + + A name declared BOTH in `[project]` and in a dependency group (today: `pytest-timeout`, held + deliberately identical to the `dev` extra) legitimately appears in the runtime exports, so the leak + check below must not treat it as a leak. + """ + project = tomllib.loads((_REPO / "pyproject.toml").read_text(encoding="utf-8")).get( + "project", {} + ) + specs: list[str] = list(project.get("dependencies") or []) + for extra_specs in (project.get("optional-dependencies") or {}).values(): + specs.extend(extra_specs or []) + return {_dist_name(spec) for spec in specs if _dist_name(spec)} + + def _names_target(line: str, package: str) -> bool: """Does this `pip install` line name ``package`` as a target (bare, or with a version spec)?""" return any( @@ -398,6 +506,11 @@ def _locked_requirements(lock: Path) -> list[tuple[str, int]]: `uv export` emits `name==version \\` followed by indented `--hash=` continuations, then an optional `# via ...` comment. + + Any OTHER leading-dash directive is a hard failure rather than a skip. `-e .`, `--index-url`, + `--extra-index-url` and `--find-links` all change what pip actually resolves, and a directive that is + neither counted as a requirement nor rejected is invisible to the very guard whose point is not being + fooled by a lock's shape. """ out: list[tuple[str, int]] = [] for raw in lock.read_text(encoding="utf-8").splitlines(): @@ -408,26 +521,56 @@ def _locked_requirements(lock: Path) -> list[tuple[str, int]]: if out: # a hash before any requirement would be malformed; ignore rather than crash name, count = out[-1] out[-1] = (name, count + 1) - elif not line.startswith("-"): + elif line.startswith("-"): + raise AssertionError( + f"{lock.name} carries the directive {line!r}, which is neither a requirement nor a " + f"`--hash=` line. An index redirect or an editable/project line changes what pip " + f"resolves and would otherwise pass through this parser uncounted — decide about it " + f"here rather than letting it be silently ignored." + ) + else: out.append((line.removesuffix("\\").strip(), 0)) return out -@pytest.mark.parametrize(("workflow", "lock"), LOCK_INSTALLED_TOOLCHAINS) -def test_lock_installed_toolchain_install_is_hash_verified(workflow: str, lock: str) -> None: +def _exported_names(export: Path) -> list[str]: + """The `name==version` requirement lines of ANY `uv export`, hashed or not. + + Deliberately separate from `_locked_requirements`: that one is strict about directives because it + guards a `--require-hashes` install, whereas `constraints.lock` is legitimately `--no-hashes` and is + only read here for WHICH names it contains. + """ + return [ + line.removesuffix("\\").strip() + for raw in export.read_text(encoding="utf-8").splitlines() + if (line := raw.strip()) and not line.startswith(("#", "-")) + ] + + +@pytest.mark.parametrize(("workflow", "lock", "sites"), LOCK_INSTALLED_TOOLCHAINS) +def test_lock_installed_toolchain_install_is_hash_verified( + workflow: str, lock: str, sites: int +) -> None: """Installing FROM a hashed lock buys nothing unless `--require-hashes` is actually passed. Without the flag pip treats the file as an ordinary requirements list, ignores every `--hash=` line, and is free to resolve a different artifact — a silently unpinned install that still reads like a pinned one at a glance. + + The site COUNT is asserted exactly, not as "at least one": see LOCK_INSTALLED_TOOLCHAINS. A `>= 1` + check is satisfied by one survivor of a pair, so it cannot see a job losing its only install. """ lines = [ ln for ln in _code_lines(_WORKFLOWS / workflow) if _PIP_INSTALL.search(ln) and lock in ln ] - # Non-vacuity: a renamed lock or a restructured step must fail loudly, not pass by matching nothing. - assert lines, ( - f"{workflow} no longer installs from {lock} — if the toolchain moved, re-point " - f"LOCK_INSTALLED_TOOLCHAINS in the same commit rather than letting this pass vacuously" + print(f"[ci-venv-pinning] {workflow}: {len(lines)} install site(s) for {lock}") + # Non-vacuity AND completeness: a renamed lock, a restructured step, or a DELETED sibling install + # must fail loudly rather than pass by matching only what is left. + assert len(lines) == sites, ( + f"{workflow} installs {lock} at {len(lines)} site(s), expected {sites}. If a job legitimately " + f"gained or lost its toolchain install, update the count in LOCK_INSTALLED_TOOLCHAINS in the " + f"same commit — a job silently left with no install still passes every other check here, and " + f"in the coverage job it fails INVISIBLY (`|| true` swallows the missing --cov plugin)." ) for ln in lines: assert "--require-hashes" in ln, ( @@ -436,8 +579,10 @@ def test_lock_installed_toolchain_install_is_hash_verified(workflow: str, lock: ) -@pytest.mark.parametrize(("workflow", "lock"), LOCK_INSTALLED_TOOLCHAINS) -def test_lock_installed_toolchain_lock_is_pinned_and_hashed(workflow: str, lock: str) -> None: +@pytest.mark.parametrize(("workflow", "lock", "sites"), LOCK_INSTALLED_TOOLCHAINS) +def test_lock_installed_toolchain_lock_is_pinned_and_hashed( + workflow: str, lock: str, sites: int +) -> None: """The lock itself must be fully `==`-pinned AND carry hashes. Blocks substituting a `--no-hashes` export (the shape `constraints.lock` legitimately has, because @@ -449,7 +594,10 @@ def test_lock_installed_toolchain_lock_is_pinned_and_hashed(workflow: str, lock: assert path.is_file(), f"{workflow} installs {lock}, which is not committed" reqs = _locked_requirements(path) total_hashes = sum(n for _, n in reqs) - print(f"[ci-venv-pinning] {lock}: {len(reqs)} requirements, {total_hashes} hashes") + print( + f"[ci-venv-pinning] {lock}: {len(reqs)} requirements, {total_hashes} hashes " + f"({sites} install site(s) in {workflow})" + ) assert reqs, ( f"{lock} declares no requirements — an empty lock installs nothing and proves nothing" ) @@ -465,7 +613,7 @@ def test_lock_installed_toolchain_lock_is_pinned_and_hashed(workflow: str, lock: ) -@pytest.mark.parametrize("lock", sorted({lock for _, lock in LOCK_INSTALLED_TOOLCHAINS})) +@pytest.mark.parametrize("lock", sorted({lock for _, lock, _ in LOCK_INSTALLED_TOOLCHAINS})) def test_lock_installed_toolchain_locks_are_in_the_dep1_set(lock: str) -> None: """Each toolchain lock must be REGENERATED and diff-gated by DEP-1, not hand-maintained. @@ -475,11 +623,154 @@ def test_lock_installed_toolchain_locks_are_in_the_dep1_set(lock: str) -> None: Only the gate half is asserted here; `tests/test_dep1_lock_resync_lockstep.py` then forces the Dependabot resync to export, short-circuit on, and stage the identical set. + + THE SELECTOR IS PART OF THE CONTRACT, not just the `-o` path. A `.*` between `uv export` and `-o` + accepts `--group` in place of `--only-group`, and `--group` is additive: it emits the PROJECT's whole + closure plus the group. Measured — `uv export --group ci-scanners -o ci/locks/ci-scanners.lock` grows + the lock from 33 to 69 requirements, pulling `fastapi`, `uvicorn`, `hl7`, `httpx` and `aiosqlite` + into what the BLOCKING bandit and zizmor jobs install — and it is fully `==`-pinned and fully hashed, + so it passed every other guard here, passed the resync lockstep, and re-exported byte-identically + under DEP-1. Nothing went red. The group name is also required to MATCH the lock's stem: without it, + `--only-group ci-quality -o ci/locks/ci-scanners.lock` is equally green. """ + group = Path(lock).stem body = (_WORKFLOWS / "security.yml").read_text(encoding="utf-8") - assert re.search(rf"^\s*uv export\s+.*-o {re.escape(lock)}\s*$", body, re.MULTILINE), ( - f"security.yml's DEP-1 step does not `uv export` {lock}, so nothing regenerates it and the " - f"drift gate never compares it. A stale hash-pinned toolchain is worse than a floating one." + pattern = rf"^\s*uv export --only-group {re.escape(group)} --format requirements\.txt -o {re.escape(lock)}\s*$" + assert re.search(pattern, body, re.MULTILINE), ( + f"security.yml's DEP-1 step does not export {lock} with exactly " + f"`uv export --only-group {group} --format requirements.txt -o {lock}`. Either nothing " + f"regenerates it (so the drift gate never compares it, and a stale hash-pinned toolchain is " + f"worse than a floating one), or the SELECTOR changed: `--group` is additive and would pull the " + f"whole project runtime into a scanner's install closure while staying pinned, hashed and " + f"byte-identical under DEP-1." + ) + + +@pytest.mark.parametrize("lock", sorted({lock for _, lock, _ in LOCK_INSTALLED_TOOLCHAINS})) +def test_each_group_pin_reaches_its_own_lock(lock: str) -> None: + """Every spec declared in group `` must appear `==`-pinned in `ci/locks/.lock`. + + The second half of the selector guard, from the other direction: this is what catches a lock exported + from the WRONG group (the filename says `ci-scanners`, the export said `--only-group ci-quality`) and + a pin bumped in `pyproject.toml` without a re-export — which would leave the declared version and the + installed version disagreeing, with `pyproject.toml` reading as the source of truth it no longer is. + """ + group = Path(lock).stem + specs = _dependency_groups().get(group) + assert specs, f"[dependency-groups] has no `{group}` entry, but {lock} is exported from it" + locked = {name.split("==", 1)[0] for name, _ in _locked_requirements(_REPO / lock)} + text = (_REPO / lock).read_text(encoding="utf-8") + print(f"[ci-venv-pinning] {group}: {len(specs)} declared spec(s) checked against {lock}") + for spec in specs: + name = _dist_name(spec) + assert name in locked, ( + f"{lock} does not contain {name!r}, which `[dependency-groups].{group}` declares. The lock " + f"was exported from a different group, or is stale — re-run the DEP-1 exports." + ) + if "==" in spec: + version = spec.split("==", 1)[1].strip().strip("\"'") + assert re.search(rf"^{re.escape(name)}=={re.escape(version)}\b", text, re.MULTILINE), ( + f"`[dependency-groups].{group}` declares {spec!r} but {lock} pins a different version. " + f"A pin bumped without a re-export means pyproject.toml no longer describes what CI " + f"installs — re-run the DEP-1 exports in the same commit." + ) + + +@pytest.mark.parametrize("package", EXACT_GROUP_PINS) +def test_moved_tool_pins_are_exact_not_floors(package: str) -> None: + """A moved tool's `[dependency-groups]` spec must be `==`, not a floor. + + NOTHING ELSE CHECKS THIS. Moving `bandit`/`pip-audit` out of `RELEASE_PINNED_TOOLS` removed the only + guard that rejected a floor (`_PIN_OPS` excludes `>=` by design), and the two replacements are blind + to it: one asserts the NAME is declared, the other inspects the EXPORTED lock — and `uv export` + writes `bandit==1.9.4` from `bandit>=1.9.4` just as readily. Verified end to end: rewriting all three + scanner specs to `>=` re-locked, re-exported the two toolchain locks BYTE-IDENTICALLY, and passed + every guard in this repo. + + Why exactness rather than freshness: each of these versions is the CONTRACT of a gate that can red a + PR — bandit's `# nosec` parsing is a blocking gate's findings baseline, zizmor's gate asserts a clean + baseline so a new rule reds an unrelated PR, and the mutation job SHELL-PARSES mutmut's human-readable + output. A floor lets Dependabot's weekly `uv` PR move any of them with no `pyproject.toml` diff to + review, since the resync re-exports the lock automatically. + """ + spec = _group_spec(package) + assert re.fullmatch(rf"{re.escape(package)}==\d+(\.\d+)*", spec), ( + f"`[dependency-groups]` declares {spec!r}; {package} must be an EXACT `==` pin. `>=` is a FLOOR: " + f"it re-resolves inside uv.lock on any re-lock, and the exported lock looks identically pinned " + f"either way. If a floor is genuinely wanted, move {package!r} to FLOOR_BY_DESIGN with the " + f"reason — do not relax this pattern." + ) + + +@pytest.mark.parametrize("package", FLOOR_BY_DESIGN) +def test_floored_group_pins_stay_declared(package: str) -> None: + """The counterpart to EXACT_GROUP_PINS: a floor is a decision, so it must be an enumerated one. + + Asserts only that the entry still EXISTS and is not accidentally exact-pinned without the reason + moving with it. The point is that reading these two tables together tells you every group spec's + intended shape — so "floor by design" can never be mistaken for "floor nobody noticed". + """ + spec = _group_spec(package) + print(f"[ci-venv-pinning] floor by design: {spec}") + assert re.fullmatch(rf"{re.escape(package)}(>=|~=)\d+(\.\d+)*", spec), ( + f"`{package}` is listed in FLOOR_BY_DESIGN but declared as {spec!r}. If it was deliberately " + f"tightened to an exact pin, move it to EXACT_GROUP_PINS with its reason in the same commit." + ) + + +def test_dependency_groups_do_not_leak_into_the_runtime_exports() -> None: + """No CI-toolchain name may appear in the four PRE-EXISTING DEP-1 artifacts. + + This is the load-bearing invariant `pyproject.toml` asserts in prose and nothing enforced: the groups + must stay NON-DEFAULT, because a default group lands in `requirements.lock`, in the container image + locks that feed BOTH SBOM builds, and in what `pip-audit` audits AS RUNTIME. + + `--no-dev` is NOT the protection it looks like. `uv export --help`: it disables "the development + dependency group" — the group literally named `dev` — so it does not filter any other default group. + Measured: adding `default-groups = ["ci-scanners"]` puts `bandit`, `pip-audit` and `zizmor` into + `requirements.lock` (98 -> 121 requirements) AND into `docker/locks/requirements-core.lock` + (41 -> 69), the SBOM input, past its `--no-dev`. The two `--all-extras` exports pass neither + `--no-dev` nor `--no-default-groups`, so they are unguarded by construction — including against the + most natural mistake available, a group named `dev` mirroring the existing `dev` EXTRA, which is uv's + implicit default group. In every one of those cases the whole suite still passes and DEP-1 merely + demands a re-export, then blesses the leak. + + A name declared in BOTH `[project]` and a group is exempt: `pytest-timeout` is deliberately held to + the identical spec as the `dev` extra, so its presence in the runtime exports is correct. + """ + runtime_exports = ( + "requirements.lock", + "constraints.lock", + "docker/locks/requirements-core.lock", + "docker/locks/requirements-sqlserver.lock", + ) + groups = _dependency_groups() + project_names = _project_declared_names() + toolchain = { + _dist_name(spec) + for specs in groups.values() + for spec in specs + if _dist_name(spec) and _dist_name(spec) not in project_names + } + assert toolchain, "no CI-toolchain-only names to check — this guard would pass vacuously" + print( + f"[ci-venv-pinning] checked {len(runtime_exports)} runtime exports for " + f"{len(toolchain)} toolchain-only name(s): {sorted(toolchain)}" + ) + + leaks: list[str] = [] + for export in runtime_exports: + path = _REPO / export + assert path.is_file(), f"{export} is missing — the DEP-1 artifact set changed" + names = {name.split("==", 1)[0] for name in _exported_names(path)} + print(f"[ci-venv-pinning] {export}: {len(names)} requirements") + leaks += [f"{export}: {name}" for name in sorted(toolchain & names)] + assert not leaks, ( + "a [dependency-groups] name reached a RUNTIME export:\n " + + "\n ".join(leaks) + + "\nThe CI toolchain must stay in NON-DEFAULT groups. Check for a `[tool.uv] default-groups` " + "entry, or a group named `dev` (uv's implicit default group). A leaked name is in the release " + "SBOM, the container image locks, and what pip-audit reports as this project's runtime." ) @@ -512,27 +803,78 @@ def test_moved_tools_are_declared_in_a_dependency_group(package: str) -> None: ) +#: `pipx install ` — the OTHER installer in this repo's workflows, and one `_PIP_INSTALL` cannot +#: see (it anchors on `pip`/`pip3`, so `pipx` never matches). Not hypothetical: `quality-advisory.yml` +#: already installs `ruff` with `pipx` twice, so it is in-repo idiom rather than a shape nobody would +#: reach for. `pipx` has no `--require-hashes`, so a moved tool arriving this way is unpinnable. +_PIPX_INSTALL = re.compile(r"\bpipx\s+(?:-\S+\s+)*install\b") + + +def _pipx_targets(line: str) -> list[str]: + """The package tokens a ``pipx install`` line names — same token walk as `_install_targets`.""" + match = _PIPX_INSTALL.search(line) + if match is None: + return [] + return _tokens_after(line[match.end() :]) + + def test_no_moved_tool_is_reinstalled_inline() -> None: - """No moved tool may ALSO be installed by an inline `pip install `. + """No moved tool may ALSO be installed by an inline `pip install ` or `pipx install `. The regression: someone adds `pip install bandit` beside the lock install (to debug, or because a lock refresh was inconvenient). Both run, the last one wins, and the version that actually executes is the unpinned one — while every hash-pinning check above still passes, because they only ever look at the lock line. + + TWO blind spots were closed here rather than left implied. (1) The scan covered only the three + workflows in `LOCK_INSTALLED_TOOLCHAINS`, so `pip install bandit` added to `ci.yml` or `codeql.yml` + was invisible — it now sweeps EVERY workflow. (2) `_PIP_INSTALL` does not match `pipx install`, which + this repo already uses, so that spelling defeated the guard entirely. """ + workflows = sorted(_WORKFLOWS.glob("*.yml")) + assert workflows, f"no workflows found under {_WORKFLOWS} — this guard would pass vacuously" + print( + f"[ci-venv-pinning] swept {len(workflows)} workflow(s) for inline installs of " + f"{len(MOVED_TO_A_GROUP)} moved tool(s)" + ) offenders = [ - f"{workflow}: {package} named inline in {ln!r}" - for workflow in sorted({wf for wf, _ in LOCK_INSTALLED_TOOLCHAINS}) - for ln in _code_lines(_WORKFLOWS / workflow) - if _PIP_INSTALL.search(ln) + f"{wf.name}: {package} named inline in {ln!r}" + for wf in workflows + for ln in _code_lines(wf) + if _PIP_INSTALL.search(ln) or _PIPX_INSTALL.search(ln) for package in MOVED_TO_A_GROUP if _names_target(ln, package) + or any( + target == package or target.startswith(tuple(f"{package}{op}" for op in _SPEC_OPS)) + for target in _pipx_targets(ln) + ) ] assert not offenders, ( "a tool routed through a hash-pinned dependency group is ALSO installed inline:\n " + "\n ".join(offenders) + "\nRemove the inline install; bump the pin in pyproject.toml's [dependency-groups] and " - "re-export instead." + "re-export instead. (`pipx` has no --require-hashes at all, so that spelling cannot be pinned.)" + ) + + +def test_security_yml_pip_bootstrap_count_is_exact() -> None: + """`security.yml` carries exactly `SECURITY_YML_PIP_BOOTSTRAPS` unpinned `--upgrade pip` fetches. + + `SECURITY_YML_ACCEPTED_UNPINNED` registers the NAME `pip`, so it cannot tell two accepted bootstraps + from twenty — and the inventory in this module claimed ONE while the file carried TWO (the `uv` install + in the DEP-1 step and the `semgrep` install). ADR 0034 §3 is the register a future session reads before + re-dismissing a Scorecard alert, so an inventory that undercounts is how a real finding becomes + invisible. Counted here so the number cannot drift out of the prose again. + """ + lines = [ln for ln in _code_lines(_WORKFLOWS / "security.yml") if "--upgrade pip" in ln] + print(f"[ci-venv-pinning] security.yml --upgrade pip bootstraps: {len(lines)}") + for ln in lines: + print(f"[ci-venv-pinning] {ln}") + assert len(lines) == SECURITY_YML_PIP_BOOTSTRAPS, ( + f"security.yml has {len(lines)} `--upgrade pip` bootstrap(s), expected " + f"{SECURITY_YML_PIP_BOOTSTRAPS}: {lines}. Each surviving one is a Scorecard " + f"PinnedDependenciesID finding that ADR 0034 §3 must name as a residual. Removing one is good " + f"news — lower the constant. ADDING one needs a reason in that residuals table." ) diff --git a/tests/test_quality_advisory_invariants.py b/tests/test_quality_advisory_invariants.py index ef8c9eb4..11789f44 100644 --- a/tests/test_quality_advisory_invariants.py +++ b/tests/test_quality_advisory_invariants.py @@ -27,6 +27,8 @@ _WORKFLOW = _REPO / ".github" / "workflows" / "quality-advisory.yml" #: The hash-pinned `uv export` of `[dependency-groups].ci-quality` that this workflow installs from. _CI_QUALITY_LOCK = _REPO / "ci" / "locks" / "ci-quality.lock" +#: The path as the workflow spells it. +_CI_QUALITY_LOCK_REF = "ci/locks/ci-quality.lock" def _group_pin(package: str) -> str: @@ -108,6 +110,36 @@ def _steps(workflow: dict) -> list[tuple[str, dict]]: return [(name, step) for name, job in workflow["jobs"].items() for step in job["steps"]] +def _assert_job_installs_the_toolchain_lock(workflow: dict, job: str) -> None: + """The NAMED job must install the hash-pinned toolchain lock — resolved per job, not file-wide. + + A `"ci/locks/ci-quality.lock" in ` assertion is satisfied by EITHER job's install + line, so deleting the coverage job's while leaving the mutation job's (or vice versa) passes. Both + were individually deletable at zero test cost before this. The coverage job's failure mode is the + dangerous one because it is SILENT: without the install, `pytest -q --cov` dies on + `unrecognized arguments: --cov`, `|| true` swallows the exit code, no `coverage.xml` is written, and + the diff-coverage step reports "skipped" and exits 0 — a green advisory receipt measuring nothing. + """ + assert job in workflow["jobs"], ( + f"quality-advisory.yml has no `{job}` job — re-point this guard rather than letting it pass" + ) + runs = [ + step.get("run") or "" + for step in workflow["jobs"][job]["steps"] + if _CI_QUALITY_LOCK_REF in (step.get("run") or "") + ] + print(f"[quality-advisory] job {job!r}: {len(runs)} step(s) install {_CI_QUALITY_LOCK_REF}") + assert len(runs) == 1, ( + f"the `{job}` job has {len(runs)} step(s) installing {_CI_QUALITY_LOCK_REF}, expected exactly 1. " + f"Its tools are pinned by that lock alone; with no install the job runs against whatever the " + f"editable install happened to pull in." + ) + assert "--require-hashes" in runs[0], ( + f"the `{job}` job installs {_CI_QUALITY_LOCK_REF} without --require-hashes — the hashes in the " + f"lock are inert unless the flag is passed." + ) + + def _analysis_steps(workflow: dict) -> list[tuple[str, dict]]: return [ (job, step) @@ -187,7 +219,7 @@ def test_jscpd_stays_on_4x(raw: str) -> None: assert re.search(r"jscpd@4\.\d+\.\d+", raw), "jscpd must stay pinned to a 4.x release" -def test_diff_cover_is_pinned_exactly() -> None: +def test_diff_cover_is_pinned_exactly(workflow: dict) -> None: """The annotation surface depends on this version's `--format github-annotations:`. Re-pointed 2026-07-29: the pin moved OUT of this workflow. `diff-cover` is now declared in @@ -195,7 +227,12 @@ def test_diff_cover_is_pinned_exactly() -> None: `ci/locks/ci-quality.lock`, because a version pin alone does not satisfy Scorecard's `PinnedDependenciesID` (ADR 0034 §3). So assert the pin where it now lives — in BOTH places, since an exact spec in pyproject that the exported lock disagrees with would mean the lock is stale. + + Also asserts the COVERAGE job specifically installs that lock. Dropping the `raw` parameter left + nothing per-job checking the coverage side at all: `_group_pin` reads `pyproject.toml`, and the + lock-install assertion in `test_mutmut_*` is workflow-wide. """ + _assert_job_installs_the_toolchain_lock(workflow, "coverage") pin = _group_pin("diff-cover") assert re.fullmatch(r"diff-cover==\d+\.\d+\.\d+", pin), ( f"diff-cover must be pinned exactly in [dependency-groups], got {pin!r}" @@ -306,7 +343,7 @@ def test_step_summary_writes_are_size_guarded(code: str) -> None: ) -def test_mutmut_is_pinned_to_3x_with_pytest_timeout(code: str) -> None: +def test_mutmut_is_pinned_to_3x_with_pytest_timeout(code: str, workflow: dict) -> None: """mutmut 2.5.1 crashes on Python 3.14 before generating a mutant, and `|| true` made that look green for months. pytest-timeout is not optional: mutmut 3 always passes `--timeout` to pytest, and without the plugin every invocation dies inside BadTestExecutionCommandsException. @@ -329,9 +366,9 @@ def test_mutmut_is_pinned_to_3x_with_pytest_timeout(code: str) -> None: "ci/locks/ci-quality.lock carries no pytest-timeout; mutmut 3 always passes `--timeout` to " "pytest, so every invocation would die inside BadTestExecutionCommandsException" ) - assert "ci/locks/ci-quality.lock" in code, ( - "the mutation job must install from the hash-pinned toolchain lock" - ) + # Resolved under `jobs.mutation`, not searched file-wide: the coverage job's install line would + # otherwise satisfy this assertion for the mutation job. + _assert_job_installs_the_toolchain_lock(workflow, "mutation") def test_mutmut_copies_the_package_not_just_the_mutated_file(code: str) -> None: From 6990783ae55796ad06d6bce241b2da13f743a87c Mon Sep 17 00:00:00 2001 From: wshallwshall Date: Wed, 29 Jul 2026 21:35:14 -0500 Subject: [PATCH 6/6] docs(pins): correct four claims this change shipped that are not true ADR 0034's own Decision is that a dismissal carrying a false reason is worse than an open finding. These four were recorded reasons, so they get held to that. security.yml, the pip-audit step: "the `--upgrade pip` bootstrap is GONE rather than pinned" is TRUE of the bandit job and zizmor.yml, and FALSE of the job it was written in. security.yml:66 still runs `pip install --upgrade pip "uv=="` in the same interpreter two steps earlier, and THAT unverified pip does the load-bearing work: six `uv export`s plus the `git diff --exit-code` gate. The hashed install then DOWNGRADES pip to 26.2 after the fact. Scoped to say so; the register already conceded one bootstrap survives, so the file contradicted itself. quality-advisory.yml, both install steps: the recorded rationale for `uv pip` -> `python -m pip` was unsound in both halves. `uv pip install --require-hashes` exists (env UV_REQUIRE_HASHES) and setup-uv is already in both jobs, so it is not "pip's contract". And the Scorecard half argues the wrong direction -- a line its parser cannot read generates NO finding, so converting one can only ADD parseable surface, not close an alert. The change is kept (one spelling for every hashed install in the repo, one thing for the guards to match) with that as the stated reason. ADR 0034, the sigstore residual: "The lock resolves `sigstore` to 4.5.0" is false as written -- `sigstore` appears NOWHERE in uv.lock or in any of the six exports (0 hits); 4.5.0 was measured in a scratch resolve. Restated counterfactually, because a reader who checks the claim, finds nothing, and concludes the row is stale would re-open the cooldown decision the row exists to preserve. docs/Code_Quality_Standards.md:312 was the one copy of "they needed NO requirements.lock change -- DEP-1 unaffected" that escaped the sweep (quality-advisory.yml's copy was rewritten). Both halves are now false. Dated correction rather than a rewrite: it sits inside a dated rollout record. Also in ADR 0034: - The line-drift figure was a guess ("~9") and understated the re-anchor budget by two thirds. Measured per workflow instead: security.yml 464 -> 490 (+26), quality-advisory 619 -> 637 (+18), the resync +9, zizmor +1, each with its first changed line. - A second residual row for the unpinned `pip` in the semgrep step. The semgrep row explained only the [otel] conflict; the same line is also an unpinned pip fetch, and an inventory that undercounts is how a real finding goes invisible. Mitigating: semgrep is not a required context. - The pip-audit consequence, with the measurement that frames it, because this one is the owner's call. It is NOT a new posture -- requirements.lock is `--all-extras`, so ruff/mypy/pytest already sit in the blocking audit's input -- but it IS a wider blast radius: 40 distributions new to the required gate (23 via ci-scanners, 17 via ci-quality). Measured 0 advisories across all 60 name==version pairs. The one-line retreat, if wanted, is to move the ci-quality audit to its own step and keep the scanners' blocking. - A table of the seven guard holes the adversarial pass found, what passed green with each regression in place, and what now enforces it. --- .github/workflows/quality-advisory.yml | 18 +++-- .github/workflows/security.yml | 22 +++++- docs/Code_Quality_Standards.md | 2 + ...is-triage-policy-accepted-risk-register.md | 73 ++++++++++++++++--- 4 files changed, 95 insertions(+), 20 deletions(-) diff --git a/.github/workflows/quality-advisory.yml b/.github/workflows/quality-advisory.yml index 72234624..cd96c88b 100644 --- a/.github/workflows/quality-advisory.yml +++ b/.github/workflows/quality-advisory.yml @@ -303,10 +303,16 @@ jobs: # re-exports this lock by design -- superseding the note that used to sit here claiming they # were deliberately kept out of pyproject to avoid tripping that gate. # - # `python -m pip`, not `uv pip install --system`, for two reasons: `--require-hashes` is pip's - # contract (and every other hashed install in this repo uses pip), and Scorecard's - # pinned-dependency parser keys on pip/pip3/`python -m pip` -- so `uv pip` would be invisible - # to the very check this change exists to satisfy. Same setup-python interpreter either way. + # `python -m pip`, not `uv pip install --system`, for ONE honest reason: every other hashed + # install in this repo is a pip install, and keeping one spelling means one thing for the + # guards in tests/test_ci_venv_pinning.py to match. Same setup-python interpreter either way. + # + # NOT because uv cannot: `uv pip install --require-hashes` exists (env UV_REQUIRE_HASHES), and + # setup-uv is already in this job -- the earlier claim that "--require-hashes is pip's + # contract" was simply wrong. NOR for Scorecard's benefit: its parser keys on + # pip/pip3/`python -m pip`, so a `uv pip` line generates NO finding at all, and converting one + # can only ADD parseable surface -- it cannot close an alert. This line is posture-neutral to + # Scorecard and is here for uniformity. # # Safe against the editable install above: the group lock and constraints.lock agree on all 10 # shared packages (pytest and pytest-timeout among them), so this cannot re-point what the @@ -443,7 +449,9 @@ jobs: # 30248096425; and pytest-timeout is REQUIRED because mutmut 3 always passes `--timeout`/ # `--timeout-method` to pytest, so without the plugin every invocation dies with an # unrecognised-argument error surfacing only as BadTestExecutionCommandsException. - # `python -m pip` because --require-hashes is pip's contract and Scorecard only parses pip. + # `python -m pip` for UNIFORMITY with every other hashed install in this repo, not because uv + # cannot do it (`uv pip install --require-hashes` exists) and not for Scorecard (a `uv pip` + # line generates no finding at all) -- see the coverage job's install step for the full note. python -m pip install --require-hashes -r ci/locks/ci-quality.lock - name: Mutation-test a bounded scope (advisory - never fails) continue-on-error: true diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index 08e87a43..8a60ae51 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -96,15 +96,29 @@ jobs: run: | # HASH-PINNED from the lock, not merely `==`-pinned: a version pin does not satisfy Scorecard # PinnedDependenciesID, and the transitive closure floated regardless. Rationale for each pin - # lives in pyproject.toml's [dependency-groups]. The `--upgrade pip` bootstrap is GONE rather - # than pinned — this lock hash-pins `pip` itself (a pip-audit → pip-api dependency). + # lives in pyproject.toml's [dependency-groups]. THIS step's `--upgrade pip` bootstrap is gone + # rather than pinned — the lock hash-pins `pip` itself (a pip-audit → pip-api dependency). + # Scoped deliberately, because the unqualified claim was FALSE for this job: the DEP-1 step + # above still runs `pip install --upgrade pip "uv=="` in this same interpreter two steps + # earlier, and that unverified pip does the load-bearing work (the six exports + the diff + # gate); the line below then DOWNGRADES pip to the locked version after the fact. So this + # job's posture improves for the AUDITOR, not for the exporter. The `uv` bootstrap is circular + # by construction (ADR 0034 section 3's residuals) and is why `pip` stays registered in + # SECURITY_YML_ACCEPTED_UNPINNED. The claim holds unqualified in the bandit job and zizmor.yml. python -m pip install --require-hashes -r ci/locks/ci-scanners.lock # Audit the committed, pinned set — reproducible (vs auditing a fresh latest-resolve). pip-audit -r requirements.lock --desc # Audit the TOOLCHAIN locks too. NOT bookkeeping: hash-pinning makes the toolchain STICKY, so # without this a CVE in a pinned scanner is invisible to every gate — the "pinned, stale, - # unpatched is worse than floating" failure ADR 0034 §3 names. `--ignore-vuln ` is the - # escape hatch for a triaged advisory, exactly as for the lockfile audit above. + # unpatched is worse than floating" failure ADR 0034 section 3 names. `--ignore-vuln ` is + # the escape hatch for a triaged advisory, exactly as for the lockfile audit above. + # + # CONSEQUENCE, stated rather than discovered later: this is a REQUIRED context, so a CVE in + # mutmut's or diff-cover's closure reds the merge gate over an ADVISORY tool. That is not a new + # posture — requirements.lock is exported `--all-extras`, so ruff, mypy and pytest already do + # exactly this — but it does add 40 distributions to the blocking set (23 from ci-scanners, 17 + # from ci-quality; measured). Zero advisories at the time of writing. If the owner would rather + # the advisory half not block, move the ci-quality line alone to its own step. pip-audit -r ci/locks/ci-scanners.lock --desc pip-audit -r ci/locks/ci-quality.lock --desc # ANTI-SLOPSQUAT. Deliberately a STEP in this already-REQUIRED job rather than a new context: it diff --git a/docs/Code_Quality_Standards.md b/docs/Code_Quality_Standards.md index adfa56bd..9e4d936c 100644 --- a/docs/Code_Quality_Standards.md +++ b/docs/Code_Quality_Standards.md @@ -311,6 +311,8 @@ Ordered by anti-slop leverage, not effort (build placement per §5). **✅ = shi **Rollout record (measured 2026-07-13 — how the \#1047 sweep was executed):** `B,C4,SIM,UP,I` = **853 violations** (238 `B008` FastAPI false positives to exclude; 111 `I001` repo-wide import reorder); `C901` = **85 hits**. Safe rollout: (a) exclude framework-idiom rules (`B008` on `api/`); (b) **grandfather** the existing backlog so the *required* gate stays green (per-file-ignores / ratchet — new code only); (c) run the repo-wide import sort as a **dedicated pass when parallel worktrees are quiescent** — a 100+-file sweep conflicts with in-flight sessions; (d) keep `C901` **advisory**. (The built coverage/mutation gates install their tools CI-side via an ephemeral `uv pip install`, so they needed **no** `requirements.lock` change — DEP-1 unaffected.) +> **Correction, 2026-07-29 (ADR 0034 §3).** Both halves of that last parenthesis are now false, and this is the copy that escaped the sweep — `quality-advisory.yml`'s own version of the claim was rewritten in the same change. `pytest-cov`, `diff-cover`, `mutmut` and `pytest-timeout` moved into `pyproject.toml`'s PEP 735 `[dependency-groups].ci-quality` and are installed from a hash-pinned `uv export` (`python -m pip install --require-hashes -r ci/locks/ci-quality.lock`), because a *version* pin does not satisfy Scorecard's `PinnedDependenciesID`. So `pyproject.toml` **did** change, and the lock lives **inside** the DEP-1 export/diff machinery rather than beside it (a hash-pinned toolchain outside it rots into a pinned, stale, *unpatched* one — worse posture than floating). `requirements.lock` itself is genuinely unaffected: the groups are deliberately **non-default**, so they stay out of all four pre-existing exports — verified by `git diff --exit-code`, and now guarded by `test_dependency_groups_do_not_leak_into_the_runtime_exports`. + ### A.4 Documented caveat — solo-maintainer review Row 5's "human review" is **self-review** (the [SDS §A.6](Secure_Development_Standards.md#a6-documented-deviations) / [AI companion Appendix A.6](Secure_AI_Development_Standards.md#a6-documented-deviations) single-maintainer deviation). The Stanford overconfidence finding (§3) bites hardest exactly when the author reviews their own AI-authored code — which is the strongest argument for the mutation gate (Built this cycle — \#1040), since it is the one control that *adversarially* checks whether the tests assert anything, independent of the author's confidence. diff --git a/docs/adr/0034-static-analysis-triage-policy-accepted-risk-register.md b/docs/adr/0034-static-analysis-triage-policy-accepted-risk-register.md index 19422891..c96d4533 100644 --- a/docs/adr/0034-static-analysis-triage-policy-accepted-risk-register.md +++ b/docs/adr/0034-static-analysis-triage-policy-accepted-risk-register.md @@ -294,12 +294,28 @@ posture: 4. **`pip-audit` now audits the toolchain locks too** (`pip-audit -r ci/locks/*.lock --desc`). This is the load-bearing half of decision 3: hash-pinning makes a toolchain *sticky*, so without this a CVE in a pinned scanner would be invisible to every gate. `--ignore-vuln ` is the escape hatch, as - for the runtime lock. Consequence accepted deliberately: a CVE in `mutmut`'s or `diff-cover`'s - closure will red a **required** gate over an advisory tool. + for the runtime lock. + + **Consequence, and the measurement that frames it — this one is the owner's to confirm.** `pip-audit` + is a *required* context, so a CVE in `mutmut`'s or `diff-cover`'s closure will red the merge gate over + an **advisory** tool. Two facts bound how novel that is. It is **not a new posture**: + `requirements.lock` is exported `--all-extras`, so `ruff`, `mypy`, `pytest` and `pytest-timeout` + already sit in the blocking audit's input — the repo has always blocked merges on CVEs in dev + tooling. But it **is** a wider blast radius: **40** distributions are new to the blocking set (23 via + `ci-scanners` — `bandit`, `zizmor`, `pip-audit`, `cachecontrol`, `rich`, `msgpack`, … — and 17 via + `ci-quality` — `mutmut`, `diff-cover`, `pytest-cov`, `jinja2`, `markupsafe`, `coverage`, `libcst`, + `textual`, …). Measured at the time of writing: **0 advisories** across all 60 `name==version` pairs + (`pip-audit` on both locks, exit 0; independently cross-checked against OSV `querybatch`). If the + advisory half should not block, the one-line change is to move `pip-audit -r ci/locks/ci-quality.lock` + into its own step — keeping the *scanners*' audit blocking, which is the same "the split is the merge + path" line decision 1 already draws. Two unpinned `pip install --upgrade pip` bootstraps disappeared rather than being pinned: the `ci-scanners` lock hash-pins `pip==26.2` itself (it arrives as a `pip-audit` → `pip-api` dependency), so -a hash-verified pip now lands in the same command that previously fetched an unverified one. +a hash-verified pip now lands in the same command that previously fetched an unverified one. **Two +survive** in `security.yml` — the `uv` bootstrap and the `semgrep` step, both named in the residuals +below and counted by `test_security_yml_pip_bootstrap_count_is_exact` so the number cannot drift out of +this prose again. ### Which findings this closes @@ -331,8 +347,9 @@ implied: | Residual | Why it is not fixed here | |---|---| -| **`release.yml`'s `sigstore==4.4.0`** | **Not a gap — an owner decision this change must not invert.** The lock resolves `sigstore` to **4.5.0**; ADR 0034 pins 4.4.0 because `.github/dependabot.yml` sets `cooldown.default-days: 5` and 4.5.0 was <48 h old. Routing `sigstore` through the lock would pin the *signing* toolchain fresher than the repo's own update policy allows — inverting that policy at the highest-privilege point in the build (the job holding `contents`/`id-token`/`attestations: write`). Left exactly as it is. Re-evaluate when 4.5.0 ages out. | -| **The `uv` bootstrap** (`security.yml`, `python -m pip install --upgrade pip "uv==0.12.0"`) | **Permanently circular: you cannot hash-lock `uv` with `uv`.** That install produces every lock this repo commits. `uv` stays an inline `==` pin, and `pip` remains the sole registered entry in `SECURITY_YML_ACCEPTED_UNPINNED`. *Cheap out-of-band fix that removes it entirely:* `astral-sh/setup-uv@c771a70e…` is already SHA-pinned and used in 9 places (`ci.yml` ×6, `quality-advisory.yml` ×2, the resync ×1); swapping it in deletes the install. Separate change. | +| **`release.yml`'s `sigstore==4.4.0`** | **Not a gap — an owner decision this change must not invert.** Routing `sigstore` through the lock **would** resolve it to **4.5.0** — stated counterfactually because that is what it is: `sigstore` is deliberately **absent** from `uv.lock` and from all six exports (0 hits), and the 4.5.0 figure was measured in a scratch resolve. A reader who greps `uv.lock`, finds nothing, and concludes this row is stale would re-open the very decision it exists to preserve. ADR 0034 pins 4.4.0 because `.github/dependabot.yml` sets `cooldown.default-days: 5` and 4.5.0 was <48 h old. Routing `sigstore` through the lock would pin the *signing* toolchain fresher than the repo's own update policy allows — inverting that policy at the highest-privilege point in the build (the job holding `contents`/`id-token`/`attestations: write`). Left exactly as it is. Re-evaluate when 4.5.0 ages out. | +| **The `uv` bootstrap** (`security.yml`, `python -m pip install --upgrade pip "uv==0.12.0"`) | **Permanently circular: you cannot hash-lock `uv` with `uv`.** That install produces every lock this repo commits. `uv` stays an inline `==` pin, and `pip` remains the sole registered *name* in `SECURITY_YML_ACCEPTED_UNPINNED`. Note it is also the pip that runs the **six exports and the diff gate** — the `--require-hashes` install two steps later *downgrades* pip to the locked version afterwards, so the DEP-1 step's own posture is unchanged by this work. *Cheap out-of-band fix that removes it entirely:* `astral-sh/setup-uv@c771a70e…` is already SHA-pinned and used in 9 places (`ci.yml` ×6, `quality-advisory.yml` ×2, the resync ×1); swapping it in deletes the install. Separate change. | +| **`security.yml`'s unpinned `pip` in the `semgrep` step** — `python -m pip install --upgrade pip "semgrep==1.172.0"` | **The SECOND surviving bootstrap, named because an undercounted inventory is how a real finding goes invisible.** The semgrep row below explains only the `[otel]` conflict that keeps *semgrep* inline; this row records that the same line is also an **unpinned `pip` fetch**. So two `--upgrade pip` bootstraps remain in the file, not one — now asserted as an exact count by `test_security_yml_pip_bootstrap_count_is_exact`, since `SECURITY_YML_ACCEPTED_UNPINNED` registers the *name* `pip` and cannot tell two accepted bootstraps from twenty. Mitigating: `semgrep` is **not** a required context (`tests/test_required_contexts.py`), so this one does not sit on the merge path. It disappears whenever the semgrep row's `[tool.uv] conflicts` recipe is taken. | | **`quality-advisory.yml`'s `pipx install ruff`** | **Outside the guard's regex and outside Scorecard's.** `test_ci_venv_pinning.py`'s `_PIP_INSTALL` matches `pip`/`pip3`/`python -m pip` only, so the unpinned fallback branch is invisible to every existing guard — and because it is not a `pip install`, **no alert exists to close**. `pipx` has no `--require-hashes`, so fixing it means changing the install mechanism, not the pin. Recorded, not done. | | **`semgrep`** | **Excluded by decision.** `semgrep==1.172.0` requires `opentelemetry-sdk>=1.37,<1.38` while the project's `[otel]` extra resolves 1.44. In a plain group the universal resolve silently **downgrades the shipped otel runtime** in all four pre-existing DEP-1 artifacts — measured and bisected to semgrep alone (the other tools give DIFFS=0). The only fix is `[tool.uv] conflicts = [[{ extra = "otel" }, { group = "semgrep-tools" }]]`, which declares a **product extra** and a **CI scanner** permanently mutually exclusive (`uv sync --all-extras --all-groups` would stop working) and still forces a `click 8.4.1 → 8.4.2` re-resolve across all four artifacts. Pinning a *scanner*'s supply chain is not worth a lasting constraint on a shipped surface. The recipe is written down here so a future owner can flip it in one commit rather than re-deriving the analysis. | | **The 5 editable `pip install -e ".[…]"` sites** + 7 `uv pip install --system -e` sites | Structurally unhashable; §3's original rationale is correct for these and stands. | @@ -364,12 +381,46 @@ cron. Confirm after the next Dependabot run. ### Convergence-rule consequence, stated rather than glossed -`security.yml` gained ~9 lines near the top of the file, so per the line-drift rule above **every -dismissed alert anchored below them re-fires as a new alert number**. That is unavoidable when adding -exports to an early step. The mitigation applied: workflow comments were kept short and each pin's -rationale moved to `pyproject.toml` beside the pin itself (which is also where a future bumper will -look). **No claim of line-neutrality is made here** — expect to re-dismiss, and re-dismiss with the -rationale in this section rather than re-triaging from scratch. +**Measured, not estimated** (an earlier draft of this section guessed "~9", which would have understated +the re-anchor budget by two thirds — a wrong number here costs a re-triage, so count it): + +| Workflow | `origin/main` | this change | drift | first changed line | +|---|---|---|---|---| +| `security.yml` | 464 | 490 | **+26** | ~81 (the DEP-1 export step) | +| `quality-advisory.yml` | 619 | 637 | **+18** | ~301 (the coverage install step) | +| `dependabot-lock-resync.yml` | 141 | 150 | **+9** | ~1 (the header) | +| `zizmor.yml` | 89 | 90 | **+1** | ~39 (the install step) | + +Per the line-drift rule above, **every dismissed alert anchored below the first changed line re-fires as +a new alert number** — up to 26 lines of drift in `security.yml`. That is unavoidable when adding exports +to an early step. The mitigation applied: each *pin's* rationale moved to `pyproject.toml` beside the pin +itself (which is also where a future bumper will look), so the workflow comments carry only what is +specific to the call site. **No claim of line-neutrality is made here** — expect to re-dismiss, and +re-dismiss with the rationale in this section rather than re-triaging from scratch. Re-measure this table +if the change is rebased; do not carry the numbers forward on faith. + +### What the adversarial pass found in the guards, and what now enforces it + +Three reviews attacked this change before it landed. None found a wrong byte in a lock — `uv lock --check` +exit 0, all six exports byte-identical, the runtime closure a `588 insertions / 0 deletions` diff (a +version or edge change is impossible without a deletion). Every finding was a **guard gap**: a rule the +prose asserted and nothing checked. Each was reproduced by injecting the regression and confirming the +suite stayed **green**, then closed and confirmed **red**: + +| Hole | What passed green with the regression in place | Now enforced by | +|---|---|---| +| Moving `bandit`/`pip-audit` out of `RELEASE_PINNED_TOOLS` deleted the only check that rejected a **floor** | all three scanner specs rewritten to `>=` → re-locked, **re-exported byte-identically**, whole suite green. `uv export` writes `bandit==1.9.4` from `bandit>=1.9.4`, so lock-side checks are structurally blind | `EXACT_GROUP_PINS` / `FLOOR_BY_DESIGN` + `test_moved_tool_pins_are_exact_not_floors` — asserted at the **declaration**, where the decision lives | +| The export **selector** was unpinned (`uv export .* -o `) | `--group` for `--only-group` grew `ci-scanners.lock` from **33 → 69** requirements, pulling `fastapi`/`uvicorn`/`hl7`/`httpx`/`aiosqlite` into the **blocking** bandit and zizmor jobs — fully pinned, fully hashed, byte-identical under DEP-1, every guard green | `test_lock_installed_toolchain_locks_are_in_the_dep1_set` now requires the exact `--only-group ` command, and `test_each_group_pin_reaches_its_own_lock` ties group → lock from the other direction | +| "The toolchain never enters the runtime/SBOM locks" was **prose only** | `default-groups = ["ci-scanners"]` puts the scanners into `requirements.lock` (98 → 121) **and** into `docker/locks/requirements-core.lock` (41 → 69), the SBOM input, straight past its `--no-dev` — because `--no-dev` disables only the group literally named `dev` | `test_dependency_groups_do_not_leak_into_the_runtime_exports` | +| 5 install sites collapsed onto 3 table rows checked with "≥1" | 4 of the 5 individually deletable at **zero** test cost; the coverage job's failure is silent (`pytest -q --cov` dies on `unrecognized arguments`, `\|\| true` swallows it, diff-coverage reports "skipped" and exits 0) | exact site counts in `LOCK_INSTALLED_TOOLCHAINS`, plus per-**job** resolution in `test_quality_advisory_invariants.py` | +| The bootstrap inventory said **one**, the file had **two** | — | `test_security_yml_pip_bootstrap_count_is_exact`, and the semgrep residual row above | +| The inline-reinstall scan missed `pipx install` and swept only 3 workflows | `pipx install bandit`, or a `pip install bandit` in `ci.yml` | `_PIPX_INSTALL` + a sweep of all 16 workflows | +| `_locked_requirements` silently skipped any non-`--hash` directive | an `--index-url` redirect or an `-e .` line, uncounted | that branch now raises | + +The pattern is the one this ADR's own Decision names: **a claim recorded without a check is a dismissal +with a reason that can quietly become false.** Two false claims were also corrected in place rather than +left standing — the `sigstore` row's tense (see the residuals) and a `security.yml` comment asserting a +bootstrap was gone from a job that still runs one. ### Adjacent fix folded in