diff --git a/CHANGELOG.d/20260930-semgrep-maturin-asset-urlopen.md b/CHANGELOG.d/20260930-semgrep-maturin-asset-urlopen.md index 5b64363842..9a950691b9 100644 --- a/CHANGELOG.d/20260930-semgrep-maturin-asset-urlopen.md +++ b/CHANGELOG.d/20260930-semgrep-maturin-asset-urlopen.md @@ -2,6 +2,8 @@ - `scripts/ci/verify_release_maturin_tool_assets.py` fetches from a fixed `https://github.com/PyO3/maturin/releases/download/v1.15.0/` origin, and `verify_assets` admits only - five literal asset names, but `p/default`'s `dynamic-urllib-use-detected` flagged the call on - main and failed Semgrep on every PR. The call now carries the repository's standard reasoned - `nosemgrep`/`nosec B310` suppression. + five literal asset names. The downloader now uses a standard-library opener + that admits one credential-free HTTPS redirect only from the exact GitHub + release path to `release-assets.githubusercontent.com`, bounds the response, + and closes it on every path. It uses neither `urlopen` nor + `HTTPSConnection`, and carries no `nosemgrep` or `nosec` suppression. diff --git a/CHANGELOG.d/20261001-noema-document-reader-transitive-cves.md b/CHANGELOG.d/20261001-noema-document-reader-transitive-cves.md new file mode 100644 index 0000000000..4a19c4bb5e --- /dev/null +++ b/CHANGELOG.d/20261001-noema-document-reader-transitive-cves.md @@ -0,0 +1,8 @@ +### Noema document reader transitive security updates + +- Updated the locked `fast-uri` dependency from 3.1.7 to 3.1.8 and + `ip-address` from 10.7.0 to 10.7.1. These are the first releases outside + the affected ranges for GHSA-hrr3-gc8f-f4qj, GHSA-j6r3-76f7-8jcv, and + GHSA-h3mg-xc3c-68pw. The direct dependency ranges are unchanged. +- Added a deterministic regression contract that rejects reintroduction of + vulnerable hoisted or nested copies of either transitive package. diff --git a/CHANGELOG.d/20261001-strix-litellm-credential-exfiltration.md b/CHANGELOG.d/20261001-strix-litellm-credential-exfiltration.md new file mode 100644 index 0000000000..e4d748764c --- /dev/null +++ b/CHANGELOG.d/20261001-strix-litellm-credential-exfiltration.md @@ -0,0 +1,6 @@ +### Shared Strix LiteLLM security floor + +- Pinned LiteLLM 1.94.3 in the Strix source input and regenerated the complete + hash lock after exact-head `pip-audit` found CVE-2026-84377 in 1.94.1. +- Added a source/lock parity regression contract and preserved the stacked Noema + document-reader transitive security repair without copying its implementation. diff --git a/CHANGELOG.md b/CHANGELOG.md index d90fa0c899..f2d7903f11 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,33 @@ +### Shared Strix lock advances beyond the PyJWT recursion DoS + +- Advance the explicit Strix source pin and generated hash lock from PyJWT + `2.14.0` to `2.15.0`, closing GHSA-42vr-xj54-vc7v / CVE-2026-101918. Exact Security + Scan run `36741151937` found the advisory in dependent PR #2540; the + canonical owner repair stays in #2531. A source/lock contract, deterministic + lock regeneration, and pip-audit evidence keep the dependent branch free of a + leaf workaround and prevent a return to `2.14.0`. Exact-head hosted security + Checks, independent approval, ordinary protected integration, and immutable + consumer-pin advancement remain required before release admission. + +### Shared urllib3 locks close proxy and streaming CVEs + +- Pin urllib3 2.8.0 as an explicit source input in both the pip-audit and + Strix security-tooling closures, regenerate their hash locks without unrelated + version movement, and add a four-file parity contract. This closes + CVE-2026-97687 and CVE-2026-97689 found by exact-head Python Security while + preserving hash checking and the existing Strix cryptography override. + +### Shared security fixtures use patched PyJWT and PyO3 releases + +- The Strix hash lock now takes PyJWT `2.14.0` as an explicit source input, + closing CVE-2026-102274 without hiding the dependency in the cryptography-only + override file. The offline Rust coverage fixture advances from PyO3 `0.22.6` + to `0.29.2`, beyond the `0.29.0` fixes for GHSA-36hh-v3qg-5jq4 and + GHSA-chgr-c6px-7xpp. Source/lock parity tests prevent either generated lock + from silently returning to the vulnerable versions. Protected integration, + immutable consumer-pin advancement, and fresh exact-head hosted security + Checks remain required before release admission. + ### Intel macOS native archives are bound to x86_64 bytes - The release prescreener now requires every native member in an Intel macOS diff --git a/docs/doctoring/shared-security-baseline-pyjwt-pyo3-20260930.md b/docs/doctoring/shared-security-baseline-pyjwt-pyo3-20260930.md new file mode 100644 index 0000000000..d0f1cbbb2b --- /dev/null +++ b/docs/doctoring/shared-security-baseline-pyjwt-pyo3-20260930.md @@ -0,0 +1,157 @@ +# Shared PyJWT and PyO3 security baseline repair + +## Status and decision + +**Proposed; release HOLD.** The central `.github` repository owns both affected +dependency surfaces, so the repair belongs on a protected-main foundation PR. +It must not be copied into PR #1026, which changes neither lock. The owner repair +must pass exact-head review and hosted security Checks, merge ordinarily, and +then reach #1026 through a non-force merge from protected `main`. + +## Exact incident evidence + +PR [#1026](https://github.com/ContextualWisdomLab/.github/pull/1026) was observed +at exact head `6f645a73502e159d5a229805afa34868ad9bb851` against protected +`main@37b10243cec3d160ecc9c1be75c71428b160a703`. + +- [Security Scan run 36495499815](https://github.com/ContextualWisdomLab/.github/actions/runs/36495499815), + job `109380628690`, reported PyO3 `0.22.6` in + `tests/fixtures/coverage-cargo/Cargo.lock`: GHSA-36hh-v3qg-5jq4 (High, 8.0) + and GHSA-chgr-c6px-7xpp (Medium, 5.5). Both advisories fix the defect in + PyO3 `0.29.0`; this repair selects and locally verifies `0.29.2`. +- [Python Security run 36495499871](https://github.com/ContextualWisdomLab/.github/actions/runs/36495499871), + job `109380725819`, reported PyJWT `2.13.0` in + `requirements-strix-ci-hashes.txt` as affected by CVE-2026-102274. PyJWT + `2.14.0` is the fixed release. +- The exact #1026 diff changes neither vulnerable file. The same lock bytes + were present on protected `main`, establishing a shared baseline defect rather + than a PR-specific regression. + +## Root cause and operational scenarios + +PyJWT was only a transitive MCP dependency, so the generated Strix lock could +select a newly vulnerable release without an explicit reviewed source pin. A +malformed RSA JWK can raise a plain `ValueError` and abort processing of the +whole JWK Set. An operator can therefore lose otherwise valid signing keys and +fail authentication or review-agent startup because one untrusted key is bad. + +The offline Rust coverage fixture intentionally pins exact crate releases, but +its PyO3 pin was not advanced when the two 2026 advisories were published. One +defect permits an out-of-bounds read from iterator methods; the other omits a +required `Sync` bound and permits a data race. Even though this is a test +fixture, the central scanner correctly treats its lock as executable supply +chain material. + +## RED to GREEN contract + +RED commit `cd84d887` introduced two fail-closed contracts: + +1. `requirements-strix-ci.txt` must explicitly select PyJWT `2.14.0`, and the + generated hash lock must contain the same version. +2. The Rust coverage fixture manifest and lock must both select PyO3 `0.29.2`. + +The implementation adds the direct PyJWT input, regenerates the Python 3.13 +manylinux hash lock with the repository command, advances the exact PyO3 +manifest pin, and regenerates the Cargo lock with Rust `1.97.1`. The +cryptography override remains single-purpose; it does not become a general +security-version overlay. + +## Ownership, release, and failure recovery + +The fixed-source release workflows consume `requirements-strix-ci-hashes.txt` +from immutable central revisions. This PR repairs the canonical owner bytes but +does not rewrite those workflows to an open branch. After ordinary protected +merge, a separate consumer change must advance their exact source commit and +rerun API/schema, security, SBOM, and provenance evidence. If any exact-head +scanner, build, or independent review fails, the PR remains HOLD and the root +cause is repaired here; no bypass or mutable source reference is permitted. + +## Local verification on the repaired tree + +- Focused dependency, fixed-source, Maturin asset, and Rust toolchain contracts: + 48 passed, 1 skipped. +- Repository regression suite: 5,160 passed, 11 skipped, 40 subtests passed. +- Rust `1.97.1` `cargo check --locked`: passed for the coverage fixture. +- Python lock regeneration with `uv 0.12.18`, seeded with the existing reviewed + output, was byte-identical. Input SHA-256 values were `c3812261…` for + `requirements-strix-ci.txt` and `3b745514…` for the override; the output was + `8f8318d4…`. A hash-enforced installation loaded PyJWT `2.14.0`. A fresh + unseeded solve is intentionally not claimed to be byte-identical because it + may select newer allowed transitive releases. +- `pip-audit`: no known vulnerabilities in the Strix lock. OSV's direct + `pyo3@0.29.2` query returned no vulnerability records. + +No production Python module changes in this repair. The repository-wide +coverage and docstring commands expose separate protected-main baseline debt: +coverage is 99% (178 statements missing) and `interrogate scripts/ci` is 97% +(43 docstrings missing). Those failures are not waived or called green here; +they require their own bounded owner repair before the repository can claim the +100% global gates. + +## References + +GitHub. (2026, June 12). *Out-of-bounds read in PyO3 iterator methods* +(GHSA-36hh-v3qg-5jq4). GitHub Advisory Database. +https://github.com/advisories/GHSA-36hh-v3qg-5jq4 + +GitHub. (2026, June 12). *PyO3 missing Sync bound can lead to a data race* +(GHSA-chgr-c6px-7xpp). GitHub Advisory Database. +https://github.com/advisories/GHSA-chgr-c6px-7xpp + +Open Source Vulnerabilities. (2026). *CVE-2026-102274: PyJWT RSA JWK Set +availability failure*. +https://osv.dev/vulnerability/CVE-2026-102274 + +## 2026-10-01 urllib3 audit follow-up + +Python Security run `36733279716`, job `109949358063`, found two newly +published vulnerabilities in urllib3 2.7.0: CVE-2026-97687 permits target TLS +policy to weaken or replace HTTPS proxy TLS policy, and CVE-2026-97689 permits +an unbounded chunk-size line to consume memory in streaming clients. Both are +fixed in urllib3 2.8.0. The same vulnerable transitive pin appeared in the +pip-audit and Strix hash locks, so this remains one central security-owner +repair rather than two consumer workarounds. + +The repair adds urllib3 2.8.0 to both source inputs and regenerates both locks +with their recorded uv commands. A contract requires exactly one 2.8.0 row in +each source input and generated lock. Comparison against exact predecessor +`d1aa3659fca527a6c7330151f3ab4df3d7578391` shows no unrelated package-version +movement. Repeated compilation produced identical SHA-256 digests, and +pip-audit 2.10.1 reported no known vulnerabilities for either generated lock. +These local results are not merge authority: exact-head hosted security Checks, +terminal authenticated CodeQL evidence, independent approval, and ordinary +protected merge remain required. + +## 2026-10-01 PyJWT recursion denial-of-service follow-up + +Security Scan run [36741151937](https://github.com/ContextualWisdomLab/.github/actions/runs/36741151937) +found GHSA-42vr-xj54-vc7v / +CVE-2026-101918 in PyJWT 2.14.0. Dependency Review job `109975641239` and OSV +job `109975641271` both rejected that shared Strix lock. An attacker-controlled, +deeply nested unsigned JWT payload can exhaust Python recursion during unverified +payload parsing in `PyJWKClient.get_signing_key_from_jwt`, before key lookup, +and raise an uncaught request-level exception; +the available evidence does not establish a process crash or authentication +bypass. PyJWT 2.15.0 contains the upstream fix. + +The canonical-owner repair advances the explicit source pin and generated lock +to 2.15.0. Lock regeneration changes only the PyJWT version and its wheel/sdist +hashes; urllib3 2.8.0, PyO3 0.29.2, and the single-purpose cryptography override +remain unchanged. The existing parity test was first changed to require 2.15.0 +and failed against the 2.14.0 source and lock before implementation. Hosted +exact-head Security, CodeQL, independent approval, ordinary protected merge, +and immutable consumer-pin advancement remain release gates. + +Local verification used the hosted-workflow Python 3.12 line. The focused +source/lock contract passed 5 tests and the warnings-as-errors repository suite +passed 5,167 tests, 6 skips, and 40 subtests. Repeating the recorded `uv 0.12.18` +compile command was byte-identical at lock SHA-256 `76443a3300d0…`; a +hash-enforced, no-dependency installation loaded PyJWT 2.15.0 and urllib3 2.8.0. +`pip-audit 2.10.1` reported no known vulnerabilities. The repository's existing +97% docstring baseline remains a separate HOLD and is not represented as green. +An independent review found no remaining Critical, Important, or Minor finding +after correcting the advisory's attack-vector wording. + +GitHub. (2026). *PyJWT has a denial of service vulnerability via maliciously +crafted JWT token with deeply nested payload* (GHSA-42vr-xj54-vc7v). +https://github.com/jpadilla/pyjwt/security/advisories/GHSA-42vr-xj54-vc7v diff --git a/docs/doctoring/shared-security-litellm-credential-exfiltration-20261001.md b/docs/doctoring/shared-security-litellm-credential-exfiltration-20261001.md new file mode 100644 index 0000000000..fc573fc8e1 --- /dev/null +++ b/docs/doctoring/shared-security-litellm-credential-exfiltration-20261001.md @@ -0,0 +1,53 @@ +# Shared Strix LiteLLM credential-exfiltration RCA + +## Incident binding + +On 2026-10-01, `.github` PR #2531 exact head +`516471fbe7d4e93a50c7bbba20402447f06f8d8b` failed Python Security run +`36799069276`, job `110169140365`. The unmodified +`requirements-strix-ci-hashes.txt` selected LiteLLM 1.94.1, and `pip-audit` +reported CVE-2026-84377 / GHSA-3cv6-jpf6-8222. The advisory describes an +authenticated request-body routing override that can redirect an upstream call, +exfiltrate configured provider credentials, and reach internal services. + +This is a canonical control-plane dependency defect, not a product-PR finding +and not an audit-service transient. The same exact head also failed Trivy on the +Noema document reader's `fast-uri` 3.1.7 and `ip-address` 10.7.0. Stacked PR +#2545 already owns and proves the minimal Node transitive repair, so its ordinary +commit is preserved in the repaired #2531 ancestry instead of being copied or +reimplemented. + +## Test-first repair + +The RED contract +`test_strix_litellm_security_pin_is_an_explicit_lock_input` first failed because +the source input did not own a LiteLLM pin. The minimal repair: + +1. selects `litellm==1.94.3` in `requirements-strix-ci.txt`, the first patched + release in the retained 1.94 line; +2. regenerates `requirements-strix-ci-hashes.txt` with the repository's declared + `uv pip compile --generate-hashes` command; +3. requires exact source/lock parity so a future resolver run cannot silently + restore an affected release; and +4. preserves #2545's `fast-uri==3.1.8` and `ip-address==10.7.1` source overrides, + lock, and nested-copy regression contract as unchanged ancestry. + +No scanner finding is ignored or suppressed. `pip-audit` over the repaired +hash lock reports no known vulnerabilities. At repaired exact head +`fe879f7b7f48f729f757e03851bf61149470ccb5`, Python Security run `36800615364`, +Security Scan run `36800615435`, SAST run `36800615456`, and runtime-quality run +`36800615444` are terminal GREEN. CodeQL run `36800615319` remains fail-closed: +both language jobs recorded `verdict=pending` while the exact-head dispatch job +succeeded. A qualifying independent approval and ordinary protected merge are +still required; local or partial hosted evidence is not merge authorization. + +## References + +BerriAI. (2026, August 26). *Authenticated SSRF and provider-credential +exfiltration via unvalidated request-body routing parameters* +(GHSA-3cv6-jpf6-8222) [Security advisory]. GitHub. +https://github.com/BerriAI/litellm/security/advisories/GHSA-3cv6-jpf6-8222 + +National Institute of Standards and Technology. (2026). *CVE-2026-84377*. +National Vulnerability Database. +https://nvd.nist.gov/vuln/detail/CVE-2026-84377 diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 6e5f1c549a..80426b2cfa 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -1,5 +1,29 @@ # Product and Technical Gap Baseline +## 2026-10-01 bounded Maturin release downloader SAST closure + +| Gap | Exact evidence | Action | Status | +|---|---|---|---| +| The canonical Maturin asset verifier replaced a suppressed dynamic `urlopen` call with direct `HTTPSConnection`, but Semgrep's low-confidence certificate-validation audit still rejects every use of that low-level API | `.github#2531@e33d97d022e1c1a26b35d51f9fa4b695fe969547`; SAST run `36802256836`; job `110178968784`; rule `python.lang.security.audit.httpsconnection-detected.httpsconnection-detected` | At source-repair head `eede925e71e0f1526560a305f5219a13c6631227`, replace the low-level connection with a standard-library opener whose redirect handler admits one credential-free HTTPS hop only from the fixed GitHub release path to the exact release-assets host; preserve bounded reads and terminal response closure; prohibit scanner suppressions | **Proposed / RED scanner-contract reproduction and 31 tests plus 4 subtests GREEN locally; source-repair exact-head Security, Python Security, SAST, and runtime-quality GREEN; terminal CodeQL and independent approval required** | + +## 2026-10-01 shared Strix LiteLLM credential-exfiltration closure + +| Gap | Exact evidence | Action | Status | +|---|---|---|---| +| The shared Strix hash lock selected LiteLLM 1.94.1, which is affected by CVE-2026-84377 / GHSA-3cv6-jpf6-8222 and can expose configured provider credentials through authenticated routing overrides | Failing predecessor `.github#2531@516471fbe7d4e93a50c7bbba20402447f06f8d8b`, Python Security run `36799069276`, job `110169140365`; repaired exact head `fe879f7b7f48f729f757e03851bf61149470ccb5`, Python Security run `36800615364`, Security Scan run `36800615435`, SAST run `36800615456`, and runtime-quality run `36800615444` | Preserve stacked #2545's Noema document-reader transitive repair, add a direct `litellm==1.94.3` source floor, regenerate the complete hash lock, bind source and lock with a RED-to-GREEN regression, and require authenticated CodeQL verdict evidence plus independent approval | **Proposed / dependency, security, SAST, and runtime-quality Checks GREEN; CodeQL run `36800615319` fail-closed pending authenticated verdicts; independent approval required** | + +## 2026-10-01 PyJWT recursion denial-of-service closure + +| Gap | Exact evidence | Action | Status | +|---|---|---|---| +| The shared Strix source and hash lock retained PyJWT 2.14.0 after GHSA-42vr-xj54-vc7v / CVE-2026-101918 disclosed an unauthenticated recursion DoS | Security Scan run `36741151937`; dependency-review job `109975641239`; OSV job `109975641271`; dependent `.github#2540@612d8e77cf13eba84782a22587a72d3ffb4b6c6e`; canonical owner PR #2531 predecessor `dde3ea7876ceb1569db717975cc74f44cc8d18f9` | In canonical owner PR #2531, advance source and lock to 2.15.0 without unrelated package movement, preserve exact source/lock parity, and merge-forward dependent branches only after owner acceptance | **Proposed / exact-head Checks and independent approval required** | + +## 2026-10-01 shared urllib3 security closure + +| Gap | Exact evidence | Action | Status | +|---|---|---|---| +| pip-audit and Strix locks retained urllib3 2.7.0 after CVE-2026-97687 and CVE-2026-97689 were published | Python Security run `36733279716`, job `109949358063`; exact predecessor `d1aa3659fca527a6c7330151f3ab4df3d7578391` | In canonical owner PR #2531, pin urllib3 2.8.0 in both source inputs, regenerate both hash locks without unrelated version movement, and bind all four files with one contract | **Proposed / exact-head Checks and independent approval required** | + 작성 기준일: **2026-08-26 10:35 KST** 대상: **ContextualWisdomLab/.github** 중앙 거버넌스·자동화 레포지터리와 이를 소비하는 naruon 생태계 현재 보호된 `main`: `826b92394c63deb6981c3a8d16a724d71f85a0d7` @@ -7,6 +31,16 @@ 이 문서는 제품·기술·운영 Gap을 현재 문서와 현재 GitHub 상태에 묶어 두는 기준선이다. 새 작업은 먼저 이 문서의 Gap ID를 PR 설명과 테스트 증거에 연결하고, PR의 정확한 exact HEAD·Checks·리뷰를 다시 수집한 뒤 구현한다. 표의 상태는 작성 시점의 관측값이므로, 병합 판단에는 재사용하지 않는다. 이 인벤토리는 스냅샷이며 merge authorization이 아니다. +### 2026-09-30 공유 보안 기준 exact-head delta + +이 delta는 아래 2026-08-26 인벤토리를 덮어쓰지 않는다. 2026-09-30 재수집한 +보호 `main`은 `37b10243cec3d160ecc9c1be75c71428b160a703`이고, live API의 첫 +페이지에는 열린 PR 50개가 있었다. 페이지 전체를 조직의 총 PR 수로 추론하지 않는다. + +| Gap ID | 상태 | exact-head evidence | causal owner / next gate | +|---|---|---|---| +| CONTROL-SHARED-SECURITY-LOCK-01 | **Source repair in progress — release HOLD** | `.github#1026@6f645a73502e159d5a229805afa34868ad9bb851`의 Security Scan run `36495499815`는 공통 Rust fixture의 PyO3 `0.22.6`에서 GHSA-36hh-v3qg-5jq4와 GHSA-chgr-c6px-7xpp를 검출했고, Python Security run `36495499871`은 공통 Strix hash lock의 PyJWT `2.13.0`에서 CVE-2026-102274를 검출했다. 두 파일은 #1026 변경 범위 밖이며 보호 `main`에도 동일하게 남아 있었다. RED commit `cd84d887`는 PyO3 `0.29.2`와 PyJWT `2.14.0` source/lock parity를 요구한다. | 중앙 `.github`가 공통 fixture와 Strix lock을 소유한다. [RCA와 검증 계약](doctoring/shared-security-baseline-pyjwt-pyo3-20260930.md)에 따라 owner PR의 exact-head Checks와 독립 승인, ordinary protected merge, immutable consumer source pin 갱신, 그리고 #1026의 비강제 main merge-forward가 순서대로 필요하다. 어떤 실패도 #1026 전용 패치나 bypass로 처리하지 않는다. | + ### 2026-09-19 exact-head incident delta | Gap ID | 상태 | exact-head evidence | causal owner / next gate | diff --git a/requirements-pip-audit-ci-hashes.txt b/requirements-pip-audit-ci-hashes.txt index 7a41a3d2d8..9a331a0d62 100644 --- a/requirements-pip-audit-ci-hashes.txt +++ b/requirements-pip-audit-ci-hashes.txt @@ -391,7 +391,9 @@ 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 +urllib3==2.8.0 \ + --hash=sha256:0cf3cae568d36aa9576b28dfb35f11328f1cb974ca7647d9475ebb86c75ac6e3 \ + --hash=sha256:63bf2ead4c879426ebf22ef2a781eeb4aa3b4ae798a0435506f8687fd5bb9b63 + # via + # -r requirements-pip-audit-ci.txt + # requests diff --git a/requirements-pip-audit-ci.txt b/requirements-pip-audit-ci.txt index 684087ba51..bec9276178 100644 --- a/requirements-pip-audit-ci.txt +++ b/requirements-pip-audit-ci.txt @@ -1 +1,2 @@ pip-audit==2.10.1 +urllib3==2.8.0 diff --git a/requirements-strix-ci-hashes.txt b/requirements-strix-ci-hashes.txt index eb83beda17..7270540502 100644 --- a/requirements-strix-ci-hashes.txt +++ b/requirements-strix-ci-hashes.txt @@ -144,6 +144,7 @@ anyio==4.14.2 \ --hash=sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494 \ --hash=sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f # via + # -r requirements-strix-ci.txt # google-genai # gql # httpx @@ -1119,24 +1120,45 @@ jsonschema-specifications==2025.9.1 \ --hash=sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe \ --hash=sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d # via jsonschema -litellm==1.94.1 \ - --hash=sha256:001be1cde7950f2ae484e450ab2f8e93ab8791e5e8d4da560d21f2fb456b0b47 \ - --hash=sha256:07c1771315d7d26e242ef90b9336bcbc49a52158ff72ee640b4f8160cc963147 \ - --hash=sha256:156c62022320bccab7c3507b6b13400b26e55b74c799e5a4a2d5bf904a77368f \ - --hash=sha256:1b0bc4a2373e54f2bd4c13f8ef9fda3839bfb2e1173fb4bcea3150b07d4c59bc \ - --hash=sha256:2103e9b155d6545b48936d2ac2e614661613adb9e3d081c58c7303ca5dd6c656 \ - --hash=sha256:44e55a55270dee8bb85e063940c368d32040e6db66765c55db4b884fc002d4ef \ - --hash=sha256:66bc95498af3ab687ce7570704cb274bcf1d78049afa87a9f5f64db45b72847d \ - --hash=sha256:a6f5274876f20dd5c9e53ba3da502e94f5b3c681c5027a0398231d0caae4aacd \ - --hash=sha256:af37356cf5b325a2887c40ff772b39e1e0865b988297c544b29123ffb13fd1b9 \ - --hash=sha256:b0145d6b9fb718d12b7242ce5c975123f4dbfecd7b8ed1eb6a6939b0e506c946 \ - --hash=sha256:c5c9247d9fea8fe7cda851f7b15db560ee547a8325a0af86048967edf3ccfa15 \ - --hash=sha256:cfef0468bda9c1ba8f554bebc2966f08436f1ead98017e7ed2d7663ece77f1c2 \ - --hash=sha256:d14e5812b5f36af2ab45461ee0c925251bc07daf65c33b8f2ce3fd3ec1235eae \ - --hash=sha256:e9b6d92e305d96bdadb8a5ccd343b1ac188de142fbd6c91f72c75416b8c25c48 \ - --hash=sha256:e9effe4c1e9206740b4bb4c98142ea1f71bae57e49df007cd25ef24b0ce4563f \ - --hash=sha256:ffa9a6cd9b6205d60b02ffc0b7f077a03693d835b06d2a34bfeaabb4f073c08a +litellm==1.94.3 \ + --hash=sha256:0ddb60a439cc361e69bb4f550a78d1656c8edad6753f8fece14f60bbd0fcd054 \ + --hash=sha256:1017f29a6f72a16ef1b026208b3e52c7197b91ae848b396c817bfb051f86c300 \ + --hash=sha256:136960922878cbc66065346583a60fd0d35f3b1dbe853d636d10da39f049399f \ + --hash=sha256:14f06f1486aa4eef0684a2088539548e3647495fa119edc2b8145c5cfee3ca24 \ + --hash=sha256:1594d231e005f6a99e319310f2e3c0d33f26d8c746e90ec42fedc2b413f16573 \ + --hash=sha256:1ec0ff6e81acbfa85b4af90f9460c46a7872a28d53aaeee956ad7d289f47edc4 \ + --hash=sha256:2098c818ce5d8139eb46a851f26d1ac5caac1502e19d5223af4930b31682c461 \ + --hash=sha256:45b9f56f02a5417b249ecaf6f01f2531e744b17b701aaea441741ed9431ba35e \ + --hash=sha256:4a21e3f5cc72b18bd424009e5ad9addcee5ebdd726c7b26cadb4c87f865f5f1b \ + --hash=sha256:56bb6b303e2e341eb55570fd0cfa5734e5d89e8a1a5ee0096e7bb0bd9f27f9e2 \ + --hash=sha256:57a82dbbf1528aa530f021f1e6fe7528331c7d5f95fa755a17f984623eaf29ae \ + --hash=sha256:584b705ece98e7b7a2fc3cf4b720711d0b786e4d131de5c4469c34cabc3f55e0 \ + --hash=sha256:5b7ef13f56e167c76ca5dfc2e6bba8e52faf6371a540c00585ee7cff81af4ba0 \ + --hash=sha256:68b5b8c4ec8d51e4a2d02cf5405b6dd85be06b8ccf0205b7b0b30c1d3c02aebe \ + --hash=sha256:6ae267172e1c615346523faf6de6cac2f21937bda2e15e0af8c5d3291476a4e5 \ + --hash=sha256:87a343c86304daa6be5f96493f1eb0796d57142886f21f8d0815e1712d2bfc18 \ + --hash=sha256:93e13c1919f8eaeb3bee9a9160c6339d1fc3913254981a1e152023c5815667cc \ + --hash=sha256:9d0fabd73ca571771773a892de1c57e208349c99a3bbc51cd049a326f7a6f6e2 \ + --hash=sha256:9e339bea866f313487b1d7e150374360700f7ed80cac41722f7ddb072b9b685e \ + --hash=sha256:9f0529505cd5cceed220f89b311f4403fa076beae18069e9df31c4e59b226871 \ + --hash=sha256:a18386278e02a2b7f8d73bf836f1c7945133becca7894ee0641dbacc6473322d \ + --hash=sha256:ac40df3e6f412a2e87f61553640922d63c8114e433c85992461317b5f67ce78f \ + --hash=sha256:b6984b3b8032ab6807aa7c56423e961a5c4220d6b93a348dbf37ec1162b68e2d \ + --hash=sha256:b6ba10b699165a9a46811f2d3abe92967dc1c2d16f972d1057dae5c5c9cd460b \ + --hash=sha256:c0eee30a6d7c38fd4702339a5a53648f140ed8e5307baa321215d0d10dd9e535 \ + --hash=sha256:c526656e85ee1be21038c75774c094b97e7439e649dc13f61cb434700199990a \ + --hash=sha256:ccf58062430fdfd53d1ebb02d55d2316ad1068005dbb4186a522039077489f9b \ + --hash=sha256:d2027a4e492fd85702ffdbcc43be770a450c521b8c8c0c995e6f7d2fb6df1d49 \ + --hash=sha256:d8eedd2dc2e504ea8452392c4c66c102ed7ccd204307d72420fd073552b59ea9 \ + --hash=sha256:de2b3cc1a5cb5b9c7e393b55a0da58cdbe46f0b373b215d4422e30b2d5dec29b \ + --hash=sha256:deac3f3efdb5373729f37f612d2469696bbc928baef5f7c80ae7a2b0940b7939 \ + --hash=sha256:e05cabfc94633e47b725bed0851ecf18809117d23e8d6ef9abaa6059c2712261 \ + --hash=sha256:e4255bc09a27d99e8c1526baf76ff307e16d3543f51fda9c87c51a71c99c8ff7 \ + --hash=sha256:f8913c9f10ef3fb1baf4ab16fc77e110f92c1c0a9a16fa0f86d1b90b417e063f \ + --hash=sha256:f8c9a92d6165570065349b8d324e7b58800cb993c39b85a5f3c7458b5d926f3d \ + --hash=sha256:fafb59618a85f3c96d38a123ac53187c75e722a6b4e34d305063661262f62ea8 # via + # -r requirements-strix-ci.txt # openai-agents # strix-agent markdown-it-py==4.2.0 \ @@ -1811,10 +1833,12 @@ pygments==2.20.0 \ --hash=sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f \ --hash=sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176 # via rich -pyjwt==2.13.0 \ - --hash=sha256:41571c89ca91598c79e8ef18a2d07367d4810fbbd6f637794879baf1b7703423 \ - --hash=sha256:66adcc2aff09b3f1bbd95fc1e1577df8ac8723c978552fd43304c8a290ac5728 - # via mcp +pyjwt==2.15.0 \ + --hash=sha256:7a3742debf6b879e912dbb9819ceec1594be812452b78c5f2e2dfc56564954f8 \ + --hash=sha256:b11c5f9791d7bf51c2b39a81ed669f6b2dbbd669df2942f6c60167e9e3d1abe4 + # via + # -r requirements-strix-ci.txt + # mcp pyopenssl==26.4.0 \ --hash=sha256:28dfcce0162b9211413e26dfbfdf1d24317fbeba18fc93c12400a1856b2a0bc7 \ --hash=sha256:f0eb0cb2d581d3ad2b9c489468485e7f2ab6727d08401bcf9d824c3caddf3c1c @@ -2346,10 +2370,11 @@ typing-inspection==0.4.4 \ # mcp # pydantic # pydantic-settings -urllib3==2.7.0 \ - --hash=sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c \ - --hash=sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897 +urllib3==2.8.0 \ + --hash=sha256:0cf3cae568d36aa9576b28dfb35f11328f1cb974ca7647d9475ebb86c75ac6e3 \ + --hash=sha256:63bf2ead4c879426ebf22ef2a781eeb4aa3b4ae798a0435506f8687fd5bb9b63 # via + # -r requirements-strix-ci.txt # docker # requests uvicorn==0.49.0 \ diff --git a/requirements-strix-ci.txt b/requirements-strix-ci.txt index 50e8a05f9b..3a31a6c2c1 100644 --- a/requirements-strix-ci.txt +++ b/requirements-strix-ci.txt @@ -1,6 +1,9 @@ strix-agent==1.5.3 +litellm==1.94.3 anyio==4.14.2 +pyjwt==2.15.0 openai[httpx2]==2.54.0 +urllib3==2.8.0 aiohttp==3.14.3 google-cloud-aiplatform==1.133.0 protobuf<8.0.0 diff --git a/scripts/ci/noema-document-reader/package-lock.json b/scripts/ci/noema-document-reader/package-lock.json index 1026fd79a3..fe692cdf0e 100644 --- a/scripts/ci/noema-document-reader/package-lock.json +++ b/scripts/ci/noema-document-reader/package-lock.json @@ -458,9 +458,9 @@ "license": "MIT" }, "node_modules/fast-uri": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.7.tgz", - "integrity": "sha512-dOvZVzjdZdz7phd9v6jCbwxrBW3fK6n8Rc0CtdmM4bumzMnxywBYhuph6J819RRw/ku+rLbelwfMunktuzVVHg==", + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz", + "integrity": "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==", "funding": [ { "type": "github", @@ -669,9 +669,9 @@ "license": "ISC" }, "node_modules/ip-address": { - "version": "10.7.0", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.0.tgz", - "integrity": "sha512-BGFsyJd5mpXp3rK6jIdADLNgpJUK1jnjzvYF8lK+VyDab9JAmqN0YOKDdP17HlgKb2+ehPgDc8EtnRLbGCAMhA==", + "version": "10.7.1", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.1.tgz", + "integrity": "sha512-4OUAqU9Z1i3vCnS05hzGiFnEMDpQ+62pAD/MVQOp83fYyNC8GleCqaS0QikQBmcWCrKFiUs/B8ztRRiYOAXuCA==", "license": "MIT", "engines": { "node": ">= 12" diff --git a/scripts/ci/noema-document-reader/package.json b/scripts/ci/noema-document-reader/package.json index aa4fc0d3ff..2408b5854b 100644 --- a/scripts/ci/noema-document-reader/package.json +++ b/scripts/ci/noema-document-reader/package.json @@ -5,5 +5,9 @@ "dependencies": { "@rhwp/core": "0.7.7", "hwp-mcp": "0.3.0" + }, + "overrides": { + "fast-uri": "3.1.8", + "ip-address": "10.7.1" } } diff --git a/scripts/ci/verify_release_maturin_tool_assets.py b/scripts/ci/verify_release_maturin_tool_assets.py index 5554f15bd4..b91e1b7469 100644 --- a/scripts/ci/verify_release_maturin_tool_assets.py +++ b/scripts/ci/verify_release_maturin_tool_assets.py @@ -9,9 +9,11 @@ import json import sys import tarfile +import urllib.error +import urllib.request import zipfile from pathlib import Path -from urllib.request import Request, urlopen +from urllib.parse import urlsplit try: from scripts.ci.release_dependency_gate import classify_platform_link @@ -29,15 +31,88 @@ } MAX_ASSET_BYTES = 16 * 1024 * 1024 MAX_BINARY_BYTES = 32 * 1024 * 1024 +ASSET_FILENAMES = frozenset({ + "maturin-aarch64-unknown-linux-musl.tar.gz", + "maturin-x86_64-unknown-linux-musl.tar.gz", + "maturin-aarch64-apple-darwin.tar.gz", + "maturin-x86_64-apple-darwin.tar.gz", + "maturin-x86_64-pc-windows-msvc.zip", +}) +GITHUB_RELEASE_HOST = "github.com" +GITHUB_RELEASE_CDN_HOST = "release-assets.githubusercontent.com" +GITHUB_RELEASE_PATH = "/PyO3/maturin/releases/download/v1.15.0/" + + +class _ExactReleaseRedirect(urllib.request.HTTPRedirectHandler): + """Admit one credential-free redirect to the exact GitHub release CDN.""" + + def http_error_302(self, request, response, code, message, headers): + """Validate and follow one exact release redirect, closing its response.""" + try: + location = headers.get("Location", "") + try: + redirect = urlsplit(location) + redirect_port = redirect.port + except ValueError as error: + raise ValueError("maturin release redirect is not trusted") from error + source = urlsplit(request.full_url) + if ( + getattr(request, "_cwl_release_redirected", False) + or source.scheme != "https" + or source.hostname != GITHUB_RELEASE_HOST + or redirect.scheme != "https" + or redirect.hostname != GITHUB_RELEASE_CDN_HOST + or redirect_port not in {None, 443} + or redirect.username is not None + or redirect.password is not None + or not redirect.path.startswith("/") + or redirect.fragment + ): + raise ValueError("maturin release redirect is not trusted") + redirected = urllib.request.Request( + location, + headers={"User-Agent": "cwl-release-gate"}, + method="GET", + ) + redirected._cwl_release_redirected = True + finally: + response.close() + return self.parent.open(redirected, timeout=request.timeout) + + http_error_301 = http_error_302 + http_error_303 = http_error_302 + http_error_307 = http_error_302 + http_error_308 = http_error_302 def _download(filename: str) -> bytes: - url = f"https://github.com/PyO3/maturin/releases/download/v1.15.0/{filename}" - # Fixed https origin and tag; verify_assets admits only five literal asset names. - with urlopen( # nosemgrep: python.lang.security.audit.dynamic-urllib-use-detected.dynamic-urllib-use-detected # nosec B310 - Request(url, headers={"User-Agent": "cwl-release-gate"}), timeout=60 - ) as response: + """Download one admitted release asset through the fixed GitHub CDN hop.""" + if filename not in ASSET_FILENAMES: + raise ValueError("maturin asset name is unexpected") + request = urllib.request.Request( + f"https://{GITHUB_RELEASE_HOST}{GITHUB_RELEASE_PATH}{filename}", + headers={"User-Agent": "cwl-release-gate"}, + method="GET", + ) + opener = urllib.request.build_opener( + urllib.request.ProxyHandler({}), _ExactReleaseRedirect() + ) + response = None + try: + response = opener.open(request, timeout=60) + if response.status != 200: + raise ValueError(f"maturin release download returned HTTP {response.status}") raw = response.read(MAX_ASSET_BYTES + 1) + except urllib.error.HTTPError as error: + try: + raise ValueError( + f"maturin release download returned HTTP {error.code}" + ) from error + finally: + error.close() + finally: + if response is not None: + response.close() if len(raw) > MAX_ASSET_BYTES: raise ValueError("maturin release asset exceeds inspection limit") return raw @@ -71,13 +146,7 @@ def verify_assets(evidence: dict, reader: str, fetch=_download) -> None: raise ValueError("maturin asset evidence is incomplete") for key, asset in sorted(evidence["assets"].items()): filename = asset["asset_filename"] - if filename not in { - "maturin-aarch64-unknown-linux-musl.tar.gz", - "maturin-x86_64-unknown-linux-musl.tar.gz", - "maturin-aarch64-apple-darwin.tar.gz", - "maturin-x86_64-apple-darwin.tar.gz", - "maturin-x86_64-pc-windows-msvc.zip", - }: + if filename not in ASSET_FILENAMES: raise ValueError(f"{key}: maturin asset name is unexpected") raw = fetch(filename) if len(raw) > MAX_ASSET_BYTES or hashlib.sha256(raw).hexdigest() != asset["asset_sha256"]: diff --git a/tests/fixtures/coverage-cargo/Cargo.lock b/tests/fixtures/coverage-cargo/Cargo.lock index 9a54521941..20666db700 100644 --- a/tests/fixtures/coverage-cargo/Cargo.lock +++ b/tests/fixtures/coverage-cargo/Cargo.lock @@ -2,18 +2,6 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "autocfg" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" - -[[package]] -name = "cfg-if" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" - [[package]] name = "coverage-cargo-fixtures" version = "0.0.0" @@ -29,15 +17,6 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" -[[package]] -name = "indoc" -version = "2.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" -dependencies = [ - "rustversion", -] - [[package]] name = "itoa" version = "1.0.15" @@ -50,15 +29,6 @@ version = "0.2.189" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" -[[package]] -name = "memoffset" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" -dependencies = [ - "autocfg", -] - [[package]] name = "once_cell" version = "1.21.4" @@ -82,37 +52,32 @@ dependencies = [ [[package]] name = "pyo3" -version = "0.22.6" +version = "0.29.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f402062616ab18202ae8319da13fa4279883a2b8a9d9f83f20dbade813ce1884" +checksum = "4688ddedf473e32662b9b067670129a8afb8c18e351482c70d62ba4a88171e8b" dependencies = [ - "cfg-if", - "indoc", "libc", - "memoffset", "once_cell", "portable-atomic", "pyo3-build-config", "pyo3-ffi", "pyo3-macros", - "unindent", ] [[package]] name = "pyo3-build-config" -version = "0.22.6" +version = "0.29.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b14b5775b5ff446dd1056212d778012cbe8a0fbffd368029fd9e25b514479c38" +checksum = "f41027e41b4bd03f6e60f9f417fe24a6341a6bb744edd62b6f709f2a52ea30e9" dependencies = [ - "once_cell", "target-lexicon", ] [[package]] name = "pyo3-ffi" -version = "0.22.6" +version = "0.29.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ab5bcf04a2cdcbb50c7d6105de943f543f9ed92af55818fd17b660390fc8636" +checksum = "e591a95526fead067432c3b3a33fc74770b87b1e04e73671090d9c2055a2b327" dependencies = [ "libc", "pyo3-build-config", @@ -120,9 +85,9 @@ dependencies = [ [[package]] name = "pyo3-macros" -version = "0.22.6" +version = "0.29.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fd24d897903a9e6d80b968368a34e1525aeb719d568dba8b3d4bfa5dc67d453" +checksum = "73225868fc1cd84eef2c3c230ddb91273bf1de46aeb8a4248da76d32a0924a1c" dependencies = [ "proc-macro2", "pyo3-macros-backend", @@ -132,13 +97,12 @@ dependencies = [ [[package]] name = "pyo3-macros-backend" -version = "0.22.6" +version = "0.29.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36c011a03ba1e50152b4b394b479826cad97e7a21eb52df179cd91ac411cbfbe" +checksum = "571575aa3749fa6216757dd47d2a3e7ef360f329a40f0666a9fbd14889024952" dependencies = [ "heck", "proc-macro2", - "pyo3-build-config", "quote", "syn", ] @@ -152,12 +116,6 @@ dependencies = [ "proc-macro2", ] -[[package]] -name = "rustversion" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" - [[package]] name = "ryu" version = "1.0.20" @@ -177,18 +135,12 @@ dependencies = [ [[package]] name = "target-lexicon" -version = "0.12.16" +version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" +checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" [[package]] name = "unicode-ident" version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954" - -[[package]] -name = "unindent" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3" diff --git a/tests/fixtures/coverage-cargo/Cargo.toml b/tests/fixtures/coverage-cargo/Cargo.toml index dbe13ab8ab..67af5592ca 100644 --- a/tests/fixtures/coverage-cargo/Cargo.toml +++ b/tests/fixtures/coverage-cargo/Cargo.toml @@ -6,4 +6,4 @@ edition = "2021" [dependencies] itoa = "=1.0.15" ryu = "=1.0.20" -pyo3 = { version = "=0.22.6", features = ["extension-module", "abi3-py310"] } +pyo3 = { version = "=0.29.2", features = ["extension-module", "abi3-py310"] } diff --git a/tests/test_noema_document_reader_dependency_security.py b/tests/test_noema_document_reader_dependency_security.py new file mode 100644 index 0000000000..e7511447eb --- /dev/null +++ b/tests/test_noema_document_reader_dependency_security.py @@ -0,0 +1,62 @@ +"""Keep the Noema document reader above known vulnerable transitive releases.""" + +from __future__ import annotations + +import json +from pathlib import Path + +LOCK_FILE = ( + Path(__file__).resolve().parents[1] + / "scripts" + / "ci" + / "noema-document-reader" + / "package-lock.json" +) +PACKAGE_FILE = LOCK_FILE.with_name("package.json") + + +def _locked_versions(lock_data: dict[str, object], package_name: str) -> list[tuple[int, ...]]: + """Return every hoisted or nested locked release for one package.""" + package_records = lock_data["packages"] + assert isinstance(package_records, dict) + path_suffix = f"node_modules/{package_name}" + release_versions: list[tuple[int, ...]] = [] + for package_path, package_data in package_records.items(): + if package_path != path_suffix and not package_path.endswith(f"/{path_suffix}"): + continue + assert isinstance(package_data, dict) + release_versions.append(tuple(int(part) for part in package_data["version"].split("."))) + return release_versions + + +def test_document_reader_transitives_include_security_fixes() -> None: + """Reject releases affected by the September 2026 URI and IP advisories.""" + lock_data = json.loads(LOCK_FILE.read_text(encoding="utf-8")) + for package_name, minimum_version in { + "fast-uri": (3, 1, 8), + "ip-address": (10, 7, 1), + }.items(): + locked_versions = _locked_versions(lock_data, package_name) + assert locked_versions + assert all(version >= minimum_version for version in locked_versions) + + +def test_nested_vulnerable_transitive_is_detected() -> None: + """Do not let a patched hoisted package hide a vulnerable nested copy.""" + lock_data = { + "packages": { + "node_modules/ip-address": {"version": "10.7.1"}, + "node_modules/parent/node_modules/ip-address": {"version": "10.7.0"}, + } + } + assert _locked_versions(lock_data, "ip-address") == [(10, 7, 1), (10, 7, 0)] + + +def test_document_reader_source_owns_transitive_security_fixes() -> None: + """Require source overrides so lock regeneration preserves the repair.""" + package_data = json.loads(PACKAGE_FILE.read_text(encoding="utf-8")) + + assert package_data["overrides"] == { + "fast-uri": "3.1.8", + "ip-address": "10.7.1", + } diff --git a/tests/test_rust_coverage_fixture_dependencies.py b/tests/test_rust_coverage_fixture_dependencies.py new file mode 100644 index 0000000000..9e6caa5d22 --- /dev/null +++ b/tests/test_rust_coverage_fixture_dependencies.py @@ -0,0 +1,33 @@ +"""Security contracts for the offline Rust coverage dependency fixture.""" + +from pathlib import Path + +try: + import tomllib +except ModuleNotFoundError: # pragma: no cover - Python 3.10 compatibility + import tomli as tomllib + + +REPOSITORY_ROOT = Path(__file__).resolve().parents[1] +FIXTURE_ROOT = REPOSITORY_ROOT / "tests" / "fixtures" / "coverage-cargo" + + +def test_coverage_fixture_uses_patched_pyo3_release() -> None: + """Keep the fixture manifest and lock on the reviewed PyO3 0.29.2 release.""" + manifest = tomllib.loads( + (FIXTURE_ROOT / "Cargo.toml").read_text(encoding="utf-8") + ) + cargo_lock = tomllib.loads( + (FIXTURE_ROOT / "Cargo.lock").read_text(encoding="utf-8") + ) + pyo3_packages = [ + package_entry + for package_entry in cargo_lock["package"] + if package_entry["name"] == "pyo3" + ] + + assert manifest["dependencies"]["pyo3"]["version"] == "=0.29.2" + assert {package_entry["version"] for package_entry in pyo3_packages} == { + "0.29.2" + } + assert len(pyo3_packages) == 1 diff --git a/tests/test_strix_runtime_dependencies.py b/tests/test_strix_runtime_dependencies.py index 20444b0abd..6f664daa20 100644 --- a/tests/test_strix_runtime_dependencies.py +++ b/tests/test_strix_runtime_dependencies.py @@ -3,6 +3,29 @@ REPOSITORY_ROOT = Path(__file__).resolve().parents[1] +def _locked_requirement_versions(requirements_text: str, package_name: str) -> list[str]: + """Return every exact version row for one normalized package name.""" + package_versions = [] + for requirement_line in requirements_text.splitlines(): + requirement_name, separator, version_and_hash_marker = ( + requirement_line.strip().partition("==") + ) + requirement_name = requirement_name.split("[", 1)[0].strip() + if separator and requirement_name.casefold() == package_name.casefold(): + package_versions.append(version_and_hash_marker.split()[0]) + return package_versions + + +def test_locked_requirement_versions_normalizes_extras() -> None: + """Treat extras as the same distribution when detecting duplicate pins.""" + requirements = "pyjwt==2.14.0\npyjwt[crypto]==2.13.0\n" + + assert _locked_requirement_versions(requirements, "pyjwt") == [ + "2.14.0", + "2.13.0", + ] + + def test_strix_installs_openai_httpx2_runtime() -> None: requirements = (REPOSITORY_ROOT / "requirements-strix-ci.txt").read_text( encoding="utf-8" @@ -27,3 +50,47 @@ def test_strix_anyio_security_pin_is_an_explicit_lock_input() -> None: assert "anyio==4.14.2" in requirements.splitlines() assert "anyio==4.14.2 \\" in requirements_lock.splitlines() + + +def test_strix_pyjwt_security_pin_is_an_explicit_lock_input() -> None: + """Keep PyJWT above the GHSA-42vr-xj54-vc7v parser DoS fix.""" + requirements = (REPOSITORY_ROOT / "requirements-strix-ci.txt").read_text( + encoding="utf-8" + ) + requirements_lock = ( + REPOSITORY_ROOT / "requirements-strix-ci-hashes.txt" + ).read_text(encoding="utf-8") + + assert _locked_requirement_versions(requirements, "pyjwt") == ["2.15.0"] + assert _locked_requirement_versions(requirements_lock, "pyjwt") == ["2.15.0"] + + +def test_strix_litellm_security_pin_is_an_explicit_lock_input() -> None: + """Keep LiteLLM outside the CVE-2026-84377 credential leak range.""" + requirements = (REPOSITORY_ROOT / "requirements-strix-ci.txt").read_text( + encoding="utf-8" + ) + requirements_lock = ( + REPOSITORY_ROOT / "requirements-strix-ci-hashes.txt" + ).read_text(encoding="utf-8") + + assert _locked_requirement_versions(requirements, "litellm") == ["1.94.3"] + assert _locked_requirement_versions(requirements_lock, "litellm") == ["1.94.3"] + + +def test_shared_urllib3_security_pin_is_an_explicit_lock_input() -> None: + """Keep both audited dependency closures above the urllib3 CVE fixes.""" + requirement_paths = ( + "requirements-pip-audit-ci.txt", + "requirements-pip-audit-ci-hashes.txt", + "requirements-strix-ci.txt", + "requirements-strix-ci-hashes.txt", + ) + + for requirement_path in requirement_paths: + requirements_text = (REPOSITORY_ROOT / requirement_path).read_text( + encoding="utf-8" + ) + assert _locked_requirement_versions(requirements_text, "urllib3") == [ + "2.8.0" + ] diff --git a/tests/test_verify_release_maturin_tool_assets.py b/tests/test_verify_release_maturin_tool_assets.py index 07244a678b..e9c80e91f0 100644 --- a/tests/test_verify_release_maturin_tool_assets.py +++ b/tests/test_verify_release_maturin_tool_assets.py @@ -6,9 +6,9 @@ import runpy import sys import tarfile +import urllib.request import zipfile from pathlib import Path -from urllib import request import pytest @@ -145,20 +145,129 @@ def read(self, member): def test_maturin_download_is_bounded(monkeypatch): class Response: - def __enter__(self): - return self - - def __exit__(self, *args): - return False + status = 200 def read(self, limit): assert limit == 4 return b"four" + def close(self): + pass + + class Opener: + def open(self, request, timeout): + assert request.method == "GET" + assert request.full_url.endswith( + "/maturin-x86_64-pc-windows-msvc.zip" + ) + assert request.headers == {"User-agent": "cwl-release-gate"} + assert timeout == 60 + return Response() + monkeypatch.setattr(verifier, "MAX_ASSET_BYTES", 3) - monkeypatch.setattr(verifier, "urlopen", lambda req, timeout: Response()) + def build_opener(proxy_handler, redirect_handler): + assert isinstance(proxy_handler, urllib.request.ProxyHandler) + assert proxy_handler.proxies == {} + assert isinstance(redirect_handler, verifier._ExactReleaseRedirect) + return Opener() + + monkeypatch.setattr(verifier.urllib.request, "build_opener", build_opener) with pytest.raises(ValueError, match="asset exceeds"): - verifier._download("maturin.zip") + verifier._download("maturin-x86_64-pc-windows-msvc.zip") + + +def test_maturin_download_rejects_unlisted_name_before_network(monkeypatch): + """Caller-controlled paths and URLs never reach the network transport.""" + monkeypatch.setattr( + verifier.urllib.request, + "build_opener", + lambda *_args, **_kwargs: pytest.fail("network opened for unlisted asset"), + ) + for filename in ("foreign.zip", "../maturin.zip", "https://example.test/x", "x?y"): + with pytest.raises(ValueError, match="asset name is unexpected"): + verifier._download(filename) + + +@pytest.mark.parametrize( + "location", + [ + "http://release-assets.githubusercontent.com/asset", + "https://example.test/asset", + "https://127.0.0.1/asset", + "https://release-assets.githubusercontent.com:invalid/asset", + "https://[bad/asset", + "file:///tmp/asset", + ], +) +def test_maturin_download_rejects_unsafe_redirect(monkeypatch, location): + """Only the credential-free GitHub release CDN redirect is admissible.""" + closed = [] + + class Response: + def close(self): + closed.append("response") + + handler = verifier._ExactReleaseRedirect() + handler.add_parent( + type("Parent", (), {"open": lambda *_args, **_kwargs: pytest.fail("redirect opened")})() + ) + request = urllib.request.Request( + "https://github.com/PyO3/maturin/releases/download/v1.15.0/asset" + ) + request.timeout = 60 + with pytest.raises(ValueError, match="redirect is not trusted"): + handler.http_error_302( + request, Response(), 302, "Found", {"Location": location} + ) + assert closed == ["response"] + + +def test_maturin_download_follows_one_exact_release_cdn_redirect(monkeypatch): + """The normal GitHub release redirect stays HTTPS and drops all authority.""" + closed = [] + sentinel = object() + + class Response: + def close(self): + closed.append("response") + + captured = {} + + class Parent: + def open(self, request, timeout): + captured.update(request=request, timeout=timeout) + return sentinel + + handler = verifier._ExactReleaseRedirect() + handler.add_parent(Parent()) + request = urllib.request.Request( + "https://github.com/PyO3/maturin/releases/download/v1.15.0/asset" + ) + request.timeout = 60 + location = ( + "https://release-assets.githubusercontent.com/" + "github-production-release-asset/123/asset?sig=abc" + ) + assert ( + handler.http_error_302( + request, Response(), 302, "Found", {"Location": location} + ) + is sentinel + ) + assert closed == ["response"] + assert captured["timeout"] == 60 + assert captured["request"].full_url == location + assert captured["request"].headers == {"User-agent": "cwl-release-gate"} + assert captured["request"]._cwl_release_redirected is True + + +def test_maturin_downloader_has_no_scanner_suppressions(): + """The downloader must remove the general URL sink, not hide findings.""" + source = Path(verifier.__file__).read_text(encoding="utf-8") + assert "nosemgrep" not in source + assert "nosec" not in source + assert "urlopen" not in source + assert "HTTPSConnection" not in source def test_maturin_main_reads_an_explicit_asset_root(tmp_path, monkeypatch): @@ -194,27 +303,30 @@ def links(binary, target, reader, *, allow_subset): return assets[binary.decode()]["native_links"] class Response: + status = 200 + def __init__(self, raw): self.raw = raw - def __enter__(self): - return self - - def __exit__(self, *args): - return False - def read(self, limit): assert limit == verifier.MAX_ASSET_BYTES + 1 return self.raw - def urlopen(req, timeout): - assert req.headers["User-agent"] == "cwl-release-gate" - assert timeout == 60 - return Response(archives[req.full_url.rsplit("/", 1)[-1]]) + def close(self): + pass + + class Opener: + def open(self, request, timeout): + assert request.method == "GET" + assert request.headers == {"User-agent": "cwl-release-gate"} + assert timeout == 60 + return Response(archives[request.full_url.rsplit("/", 1)[-1]]) monkeypatch.setattr(Path, "read_text", read_text) monkeypatch.setattr(scanner, "_reader", lambda: {"path": "/reader"}) monkeypatch.setattr(scanner, "_links", links) - monkeypatch.setattr(request, "urlopen", urlopen) + monkeypatch.setattr( + urllib.request, "build_opener", lambda _proxy, _redirect: Opener() + ) monkeypatch.setattr(sys, "argv", ["verify"]) runpy.run_path(verifier.__file__, run_name="__main__")