diff --git a/CHANGELOG.md b/CHANGELOG.md index d90fa0c899..0a112f74bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ +### 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..c6c3ff67b6 --- /dev/null +++ b/docs/doctoring/shared-security-baseline-pyjwt-pyo3-20260930.md @@ -0,0 +1,103 @@ +# 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 diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 6e5f1c549a..0c477915d8 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -7,6 +7,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-strix-ci-hashes.txt b/requirements-strix-ci-hashes.txt index eb83beda17..969c12b602 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 @@ -1811,10 +1812,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.14.0 \ + --hash=sha256:77283c83fb56ecf566a886c757a714bc83668e38156de2cce8263302f42e0b86 \ + --hash=sha256:ad0cef71c756a56e74863c2919cf0985f72decbcfcb550ee2f422e7c62b5eedc + # via + # -r requirements-strix-ci.txt + # mcp pyopenssl==26.4.0 \ --hash=sha256:28dfcce0162b9211413e26dfbfdf1d24317fbeba18fc93c12400a1856b2a0bc7 \ --hash=sha256:f0eb0cb2d581d3ad2b9c489468485e7f2ab6727d08401bcf9d824c3caddf3c1c diff --git a/requirements-strix-ci.txt b/requirements-strix-ci.txt index 50e8a05f9b..d4dd336d19 100644 --- a/requirements-strix-ci.txt +++ b/requirements-strix-ci.txt @@ -1,5 +1,6 @@ strix-agent==1.5.3 anyio==4.14.2 +pyjwt==2.14.0 openai[httpx2]==2.54.0 aiohttp==3.14.3 google-cloud-aiplatform==1.133.0 diff --git a/scripts/ci/strix_quick_gate.sh b/scripts/ci/strix_quick_gate.sh index cb97fd8eb8..5dfbc7a948 100755 --- a/scripts/ci/strix_quick_gate.sh +++ b/scripts/ci/strix_quick_gate.sh @@ -2710,6 +2710,10 @@ run_strix_once() { local resolved_target_path local timeout_seconds="$STRIX_PROCESS_TIMEOUT_SECONDS" local total_budget_limited_timeout=0 + local report_scope_instruction="" + if is_pull_request_event; then + report_scope_instruction="For this pull request scan, name at least one repository-relative changed file that you actually inspected in the final report. State what you checked. Do not copy finish_scan parameter descriptions into the report. If no changed file was inspected, say so and do not claim a completed source review. Report vulnerabilities normally." + fi if [ "$RUN_START_EPOCH" -le 0 ]; then RUN_START_EPOCH="$(date +%s)" fi @@ -2771,6 +2775,7 @@ run_strix_once() { STRIX_CHILD_EXECUTABLE_ROOT="$STRIX_EXECUTABLE_ROOT" \ STRIX_CHILD_EXECUTABLE_SHA256="$STRIX_EXECUTABLE_SHA256" \ STRIX_CHILD_REQUIRE_EXECUTABLE_INTEGRITY="${IS_PR_EVIDENCE_RUN:-false}" \ + STRIX_CHILD_REPORT_SCOPE_INSTRUCTION="$report_scope_instruction" \ python3 - "$timeout_seconds" "$resolved_target_path" "$SCAN_MODE" "$STRIX_LOG" "$STRIX_SCAN_WORKING_DIR" <<'PY' import hashlib import hmac @@ -2956,6 +2961,10 @@ REPORT_SCOPE_INSTRUCTION = ( "In the final penetration test report, list every source file you reviewed " "by its path relative to the scan target, including files with no findings." ) +# PR scans prepend their source-specific evidence request so the scanner +# receives one combined instruction rather than two competing flags. +if os.environ.get("STRIX_CHILD_REPORT_SCOPE_INSTRUCTION"): + REPORT_SCOPE_INSTRUCTION = os.environ["STRIX_CHILD_REPORT_SCOPE_INSTRUCTION"] + " " + REPORT_SCOPE_INSTRUCTION command = [resolved_strix_bin, "-n", "-t", str(target_cwd), "--scan-mode", scan_mode, "--instruction", REPORT_SCOPE_INSTRUCTION] try: diff --git a/scripts/ci/strix_report_scope.py b/scripts/ci/strix_report_scope.py index cc374c5852..78471b2ccd 100644 --- a/scripts/ci/strix_report_scope.py +++ b/scripts/ci/strix_report_scope.py @@ -22,6 +22,14 @@ def _names_scoped_ancestor(report: str, changed_paths: list[str]) -> bool: return False +FINISH_TOOL_PLACEHOLDERS = { + "executive_summary": "Business-level summary for leadership.", + "methodology": "Frameworks, scope, and approach.", + "technical_analysis": "Consolidated findings + systemic themes.", + "recommendations": "Prioritized, actionable remediation.", +} + + def validate(output: Path, changed_paths: list[str]) -> None: if not output.is_dir() or output.is_symlink(): raise ValueError("scan output directory is missing") @@ -41,6 +49,12 @@ def validate(output: Path, changed_paths: list[str]) -> None: raise ValueError("scan results are not an object") if metadata.get("status") != "completed" or results.get("scan_completed") is not True or results.get("success") is not True: raise ValueError("scan report is incomplete") + for field, placeholder in FINISH_TOOL_PLACEHOLDERS.items(): + value = results.get(field) + if not isinstance(value, str) or not value.strip(): + raise ValueError("scan report is incomplete") + if value.strip() == placeholder: + raise ValueError("scan report contains a finish-tool placeholder") report = report_path.read_text(encoding="utf-8") if not any(path in report for path in changed_paths) and not _names_scoped_ancestor(report, changed_paths): raise ValueError("scan report does not identify a changed source file") diff --git a/scripts/ci/test_strix_quick_gate.sh b/scripts/ci/test_strix_quick_gate.sh index e53741bd51..13a9711a87 100755 --- a/scripts/ci/test_strix_quick_gate.sh +++ b/scripts/ci/test_strix_quick_gate.sh @@ -7119,6 +7119,14 @@ while [ "$#" -gt 0 ]; do done scoped_file="$target_path/${FAKE_STRIX_EXPECTED_CHANGED_FILE:?}" +if [[ " $* " != *" --instruction For this pull request scan, name at least one repository-relative changed file that you actually inspected"* ]]; then + echo "Error: PR scan did not request source-specific report evidence" >&2 + exit 69 +fi +if [[ " $* " != *"Do not copy finish_scan parameter descriptions into the report"* ]]; then + echo "Error: PR scan did not reject finish-tool example prose" >&2 + exit 70 +fi if [ ! -f "$scoped_file" ]; then echo "Error: PR head scoped file missing ($scoped_file)" >&2 exit 61 @@ -7160,7 +7168,7 @@ else fi echo "scan ok with PR head content" mkdir -p strix_runs/current -printf '%s\n' '{"status":"completed","scan_results":{"scan_completed":true,"success":true}}' >strix_runs/current/run.json +printf '%s\n' '{"status":"completed","scan_results":{"scan_completed":true,"success":true,"executive_summary":"No issues in the changed file.","methodology":"Reviewed the changed source file.","technical_analysis":"No untrusted input reaches the change.","recommendations":"No remediation required."}}' >strix_runs/current/run.json printf 'Assessed %s\n' "$FAKE_STRIX_EXPECTED_CHANGED_FILE" >strix_runs/current/penetration_test_report.md EOF chmod +x "$fake_strix" @@ -7778,7 +7786,7 @@ if [ -f "$target_path/contextual_orchestrator/__main__.py" ]; then fi mkdir -p strix_runs/current -printf '%s\n' '{"status":"completed","scan_results":{"scan_completed":true,"success":true}}' >strix_runs/current/run.json +printf '%s\n' '{"status":"completed","scan_results":{"scan_completed":true,"success":true,"executive_summary":"No issues in the changed file.","methodology":"Reviewed the changed source file.","technical_analysis":"No untrusted input reaches the change.","recommendations":"No remediation required."}}' >strix_runs/current/run.json printf '%s\n' 'Assessed backend/api/auth.py' >strix_runs/current/penetration_test_report.md if [ "$matched_backend_context" -eq 1 ]; then exit 0 @@ -8113,7 +8121,7 @@ run_pull_request_target_shallow_head_merge_base_fallback_case() { set -euo pipefail echo "scan ok" mkdir -p strix_runs/current -printf '%s\n' '{"status":"completed","scan_results":{"scan_completed":true,"success":true}}' >strix_runs/current/run.json +printf '%s\n' '{"status":"completed","scan_results":{"scan_completed":true,"success":true,"executive_summary":"No issues in the changed file.","methodology":"Reviewed the changed source file.","technical_analysis":"No untrusted input reaches the change.","recommendations":"No remediation required."}}' >strix_runs/current/run.json printf '%s\n' 'Assessed 한글 경로/app.py' >strix_runs/current/penetration_test_report.md exit 0 EOF 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_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_report_scope.py b/tests/test_strix_report_scope.py index 284b34e8d5..2c6c0711e8 100644 --- a/tests/test_strix_report_scope.py +++ b/tests/test_strix_report_scope.py @@ -12,7 +12,19 @@ def test_report_scope_rejects_unrelated_success_and_accepts_scoped_success(tmp_p run = tmp_path / "current-scan" run.mkdir() (run / "run.json").write_text( - json.dumps({"status": "completed", "scan_results": {"scan_completed": True, "success": True}}), + json.dumps( + { + "status": "completed", + "scan_results": { + "scan_completed": True, + "success": True, + "executive_summary": "No issues found in the changed file.", + "methodology": "Reviewed the changed source file.", + "technical_analysis": "The changed function preserves authorization checks.", + "recommendations": "Retain the existing checks.", + }, + } + ), encoding="utf-8", ) report = run / "penetration_test_report.md" @@ -23,6 +35,17 @@ def test_report_scope_rejects_unrelated_success_and_accepts_scoped_success(tmp_p report.write_text("Assessed python/fast_mlsirm/report.py; no vulnerabilities found.\n", encoding="utf-8") assert subprocess.run(command, capture_output=True).returncode == 0 assert subprocess.run(command[:-1], capture_output=True).returncode == 1 + metadata = json.loads((run / "run.json").read_text(encoding="utf-8")) + metadata["scan_results"]["executive_summary"] = "Business-level summary for leadership." + (run / "run.json").write_text(json.dumps(metadata), encoding="utf-8") + result = subprocess.run(command, capture_output=True, text=True, check=False) + assert result.returncode == 1 + assert "placeholder" in result.stderr + del metadata["scan_results"]["executive_summary"] + (run / "run.json").write_text(json.dumps(metadata), encoding="utf-8") + result = subprocess.run(command, capture_output=True, text=True, check=False) + assert result.returncode == 1 + assert "incomplete" in result.stderr # Scope section of the 0-finding fast-mlsirm#2246 report (run 36580588738, @@ -42,7 +65,19 @@ def _completed_run(tmp_path: Path, report: str) -> None: run = tmp_path / "current-scan" run.mkdir() (run / "run.json").write_text( - json.dumps({"status": "completed", "scan_results": {"scan_completed": True, "success": True}}), + json.dumps( + { + "status": "completed", + "scan_results": { + "scan_completed": True, + "success": True, + "executive_summary": "No issues found in the audited PR scope.", + "methodology": "Reviewed the PR-scope source directories.", + "technical_analysis": "Changed numerical code has no untrusted input path.", + "recommendations": "No remediation required.", + }, + } + ), encoding="utf-8", ) (run / "penetration_test_report.md").write_text(report, encoding="utf-8") diff --git a/tests/test_strix_runtime_dependencies.py b/tests/test_strix_runtime_dependencies.py index 20444b0abd..e7bc3462a5 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,16 @@ 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 the patched PyJWT version reproducible from the source input.""" + 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.14.0"] + assert _locked_requirement_versions(requirements_lock, "pyjwt") == ["2.14.0"]