From 6ed15824cf403c35702bec2fb44e6ddd8fcd2886 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Aug 2026 13:40:01 +0000 Subject: [PATCH 1/5] chore(deps): bump github/codeql-action/init from 4.37.0 to 4.37.6 Bumps [github/codeql-action/init](https://github.com/github/codeql-action) from 4.37.0 to 4.37.6. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/v4.37.0...5595ccaf912efad79be6eef63a5619ff05969be3) --- updated-dependencies: - dependency-name: github/codeql-action/init dependency-version: 4.37.6 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/codeql-pr.yml | 4 ++-- .github/workflows/scheduled-security-scan.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql-pr.yml b/.github/workflows/codeql-pr.yml index 2a170fa8a..0437cbef0 100644 --- a/.github/workflows/codeql-pr.yml +++ b/.github/workflows/codeql-pr.yml @@ -90,7 +90,7 @@ jobs: ref: ${{ github.event.pull_request.head.sha }} - name: Initialize CodeQL - uses: github/codeql-action/init@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0 + uses: github/codeql-action/init@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} @@ -197,7 +197,7 @@ jobs: ref: ${{ format('refs/pull/{0}/merge', github.event.pull_request.number) }} - name: Initialize CodeQL - uses: github/codeql-action/init@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0 + uses: github/codeql-action/init@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} diff --git a/.github/workflows/scheduled-security-scan.yml b/.github/workflows/scheduled-security-scan.yml index 331de634f..17300da69 100644 --- a/.github/workflows/scheduled-security-scan.yml +++ b/.github/workflows/scheduled-security-scan.yml @@ -90,7 +90,7 @@ jobs: with: persist-credentials: false - name: Initialize CodeQL - uses: github/codeql-action/init@d1ba80a13dd99fba24a470575428917156a28b43 # v4.37.5 + uses: github/codeql-action/init@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} From fc92c22865720d54982fef4814511e16f381044c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 11:21:50 +0900 Subject: [PATCH 2/5] fix(codeql): keep init and analyze on one version --- .github/workflows/codeql-pr.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeql-pr.yml b/.github/workflows/codeql-pr.yml index 0437cbef0..a60cd351d 100644 --- a/.github/workflows/codeql-pr.yml +++ b/.github/workflows/codeql-pr.yml @@ -96,7 +96,7 @@ jobs: build-mode: ${{ matrix.build-mode }} - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0 + uses: github/codeql-action/analyze@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6 with: category: "/language:${{ matrix.language }}" upload: false @@ -203,7 +203,7 @@ jobs: build-mode: ${{ matrix.build-mode }} - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0 + uses: github/codeql-action/analyze@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6 with: category: "/language:${{ matrix.language }}-merge" upload: false From 0ec1895e110e2e9d9ebbed92684acfab94d947f7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 11:22:10 +0900 Subject: [PATCH 3/5] fix(codeql): align scheduled security subactions --- .github/workflows/scheduled-security-scan.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/scheduled-security-scan.yml b/.github/workflows/scheduled-security-scan.yml index 17300da69..b9169d789 100644 --- a/.github/workflows/scheduled-security-scan.yml +++ b/.github/workflows/scheduled-security-scan.yml @@ -96,7 +96,7 @@ jobs: build-mode: ${{ matrix.build-mode }} - name: Perform CodeQL Analysis continue-on-error: true - uses: github/codeql-action/analyze@d1ba80a13dd99fba24a470575428917156a28b43 # v4.37.5 + uses: github/codeql-action/analyze@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6 with: category: "/language:${{ matrix.language }}-scheduled" @@ -131,7 +131,7 @@ jobs: - name: Upload Trivy SARIF to code scanning if: always() && hashFiles('trivy-results.sarif') != '' continue-on-error: true - uses: github/codeql-action/upload-sarif@d1ba80a13dd99fba24a470575428917156a28b43 # v4.37.5 + uses: github/codeql-action/upload-sarif@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6 with: sarif_file: trivy-results.sarif category: trivy-fs-scheduled From 939032d8313c1204acc75885f4f7f67de7adacc7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 13 Aug 2026 16:00:47 +0900 Subject: [PATCH 4/5] test(codeql): pin one action SHA across PR and scheduled scans CWE-829: init, analyze, and upload-sarif must share one immutable SHA so a Dependabot split cannot execute a second unreviewed control sphere. --- ARCHITECTURE.md | 82 +++++++++++++++++++ CHANGELOG.md | 1 + CLAUDE.md | 3 + .../doctoring/codeql-action-single-version.md | 28 +++++++ tests/test_codeql_pr_workflow_contract.py | 25 ++++++ ...st_materialize_base_python_requirements.py | 16 ++++ 6 files changed, 155 insertions(+) create mode 100644 ARCHITECTURE.md create mode 100644 docs/doctoring/codeql-action-single-version.md diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 000000000..2297d5100 --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,82 @@ +# Architecture — ContextualWisdomLab `.github` + +This repository is the organization control plane. It is not naruon and it +does not own product data. Sibling products remain standalone modules; this +repo publishes org profile assets, reusable required workflows, and the +review/merge schedulers those products consume. + +## System context + +```mermaid +flowchart LR + Buyer["Commercial buyer / reviewer"] + Agents["Agents on AGENTS.md"] + Project["GitHub Project #1"] + Hub["This repo: org .github"] + Products["Owned products
naruon · orchestrator · engines"] + Runner["Required workflows in each repo context"] + + Buyer --> Hub + Agents --> Project + Agents --> Hub + Project --> Hub + Hub --> Runner + Runner --> Products + Products -->|"standalone or as module"| Buyer +``` + +## CodeQL single-SHA gate + +```mermaid +flowchart TD + Dep["Dependabot codeql-action bump"] + Pin{"init analyze upload-sarif same SHA?"} + PR["codeql-pr.yml head and merge"] + Sched["scheduled-security-scan.yml"] + Split["Fail contract: mixed trusted action"] + + Dep --> Pin + Pin -->|"yes"| PR + Pin -->|"yes"| Sched + Pin -->|"no"| Split +``` + +CWE-829: a second SHA is a second control sphere. + +## Control-plane data flow + +```mermaid +sequenceDiagram + participant PR as Pull request + participant RW as Required workflows + participant OC as OpenCode reviewer + participant SV as sandboxed_verify / web E2E + participant MS as Merge scheduler + + PR->>RW: pull_request_target on trusted base + RW->>OC: bounded evidence + NVIDIA NIM / OpenCode + OC->>SV: PoC command in isolated copy + SV-->>OC: redacted stdout/stderr + command metadata + OC-->>PR: APPROVE or request changes + MS->>PR: merge only on current-head approval + green checks +``` + +## Trust boundaries + +- Required review workflows execute **base-branch** scripts. +- Reviewer agents stay `edit: deny`. +- Logs redact credential shapes. They do not mask operational PII. +- LLM and scheduled agents bind `NVIDIA_NIM_API_KEY`. They never use + `COPILOT_GITHUB_TOKEN`. +- Rust remains the psychometric arithmetic owner. + +## Quality gates + +`scripts/ci/` ships with 100% statement/branch coverage and 100% +docstrings. + +## Related durable documents + +- [`docs/CWL-MASTER-CONTEXT.md`](docs/CWL-MASTER-CONTEXT.md) +- [`docs/doctoring/codeql-action-single-version.md`](docs/doctoring/codeql-action-single-version.md) +- [`docs/agent-github-project-protocol.md`](docs/agent-github-project-protocol.md) diff --git a/CHANGELOG.md b/CHANGELOG.md index bf30091dd..ca26decf7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ Semantic Versioning where the repository publishes a release. ### Fixed +- Pinned CodeQL `init`, `analyze`, and `upload-sarif` to one immutable SHA (`5595ccaf912efad79be6eef63a5619ff05969be3`, v4.37.6) across the PR and scheduled security workflows so a Dependabot split cannot analyze or upload with a different trusted action (CWE-829). - Bounded the Strix quality self-test's deterministic timeout fixtures to 3-second process and 5-second fake-sleep budgets so exact-head policy evidence completes inside the existing job limit without changing production Strix scanner timeouts, providers, credentials, or review semantics. - Allowed commas and ASCII parentheses in the bounded Strix changed-file path policy so legal tracked Packrat fixtures can receive exact-head security analysis, while rejecting raw `..` components before normalization and keeping controls, backslashes, whitespace ambiguity, and shell punctuation fail-closed. - Bound each review-agent invocation key to the wrapper's complete canonical payload, including the base branch and requesting actor; altered fields with a valid-format key now fail before durable-leader election or forwarding, and wrapper write permission is job-scoped. diff --git a/CLAUDE.md b/CLAUDE.md index 1c7bdb2f6..92c6b8551 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -63,6 +63,9 @@ Details: `README.md` and `PR_GOVERNANCE_AUDIT.md`. dependency sets (see below). - `fuzz/` + `.clusterfuzzlite/` — Atheris fuzz targets for the review-output normalizer and the ClusterFuzzLite discovery marker. +- `ARCHITECTURE.md` — control-plane mermaid (system context, CodeQL + single-SHA gate, review sequence, trust boundaries). Reconstruct from + the repo, not private agent memory. - `docs/` — master context, Project protocol, `org-required-workflow-rollout.md`, `scorecard-governance.md`, SBOM inventory. - `.jules/` — recorded performance (`bolt.md`) and security (`sentinel.md`) learnings from past work diff --git a/docs/doctoring/codeql-action-single-version.md b/docs/doctoring/codeql-action-single-version.md new file mode 100644 index 000000000..f58b235a5 --- /dev/null +++ b/docs/doctoring/codeql-action-single-version.md @@ -0,0 +1,28 @@ +# CodeQL action single-version pin + +## Incident and buyer impact + +Dependabot opened separate pull requests for `codeql-action/init` and +`codeql-action/upload-sarif`. If those land on different SHAs, the PR +analyzer and the scheduled SARIF uploader execute different trusted +action code. A buyer cannot treat a green CodeQL gate as evidence that +the scheduled scan used the same reviewed binary. + +## Decision + +Pin `init`, `analyze`, and `upload-sarif` to one immutable SHA +(`5595ccaf912efad79be6eef63a5619ff05969be3`, v4.37.6) in both +`codeql-pr.yml` and `scheduled-security-scan.yml`. Contract tests reject +per-file and cross-workflow splits. + +CWE-829 forbids including functionality from an untrusted or unreviewed +control sphere (MITRE, 2026). A second SHA is a second control sphere. + +## References + +MITRE. (2026). *CWE-829: Inclusion of functionality from untrusted +control sphere*. https://cwe.mitre.org/data/definitions/829.html + +GitHub. (n.d.). *Using the CodeQL action*. GitHub Docs. Retrieved +August 13, 2026, from +https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/using-the-codeql-action diff --git a/tests/test_codeql_pr_workflow_contract.py b/tests/test_codeql_pr_workflow_contract.py index 813385b23..f381a1e8e 100644 --- a/tests/test_codeql_pr_workflow_contract.py +++ b/tests/test_codeql_pr_workflow_contract.py @@ -57,6 +57,31 @@ def test_codeql_action_steps_use_one_version_per_workflow() -> None: assert len(refs) == 1, f"{filename} mixes CodeQL action refs: {sorted(refs)}" +def test_codeql_pr_and_scheduled_workflows_share_one_action_sha() -> None: + """CWE-829: PR and scheduled CodeQL must execute one immutable action SHA. + + A per-file pin can still split init/analyze on the PR workflow from + upload-sarif on the scheduled scan. Mixed SHAs would analyze or upload + with a different trusted action than the one Dependabot just reviewed. + """ + shas: set[str] = set() + tags: set[str] = set() + for filename in ("codeql-pr.yml", "scheduled-security-scan.yml"): + workflow = (REPO_ROOT / ".github/workflows" / filename).read_text( + encoding="utf-8" + ) + for sha, tag in re.findall( + r"github/codeql-action/(?:init|analyze|upload-sarif)@" + r"([0-9a-f]{40}) # (v\d+\.\d+\.\d+)", + workflow, + ): + shas.add(sha) + tags.add(tag) + + assert shas == {"5595ccaf912efad79be6eef63a5619ff05969be3"} + assert tags == {"v4.37.6"} + + def test_codeql_sarif_gate_logs_and_fails_only_unsuppressed_medium_plus( tmp_path: Path, ) -> None: diff --git a/tests/test_materialize_base_python_requirements.py b/tests/test_materialize_base_python_requirements.py index 8a383f0c2..a9e974d12 100644 --- a/tests/test_materialize_base_python_requirements.py +++ b/tests/test_materialize_base_python_requirements.py @@ -30,6 +30,19 @@ def _created_tool_directory(path: Path) -> str: return str(path) +def _simulate_linux_x86_64_runner(monkeypatch: pytest.MonkeyPatch) -> None: + """Let installer verification tests run on a non-Linux developer host. + + Production still fail-closes unless ``sys.platform`` is Linux and + ``platform.machine()`` is ``x86_64``. These unit tests pin both values so + they measure version verification, caching, and cleanup instead of the + host architecture gate already covered by the portability contract. + """ + monkeypatch.setattr(materializer.sys, "platform", "linux") + monkeypatch.setattr(materializer.platform, "machine", lambda: "x86_64") + materializer._install_trusted_uv.cache_clear() + + def test_materializes_only_regular_hash_locks_from_exact_base(tmp_path: Path) -> None: """A PR-modified lock cannot enter the networked coverage image build context.""" repo = tmp_path / "repo" @@ -644,6 +657,7 @@ def test_install_trusted_uv_verifies_version_and_caches_path( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: """The installer writes one executable, verifies its version, and caches it.""" + _simulate_linux_x86_64_runner(monkeypatch) tool_dir = tmp_path / "uv" monkeypatch.setattr( materializer.tempfile, @@ -690,6 +704,7 @@ def test_install_trusted_uv_rejects_version_process_failures( failure: OSError | subprocess.TimeoutExpired, ) -> None: """A missing or hung downloaded executable is removed and rejected.""" + _simulate_linux_x86_64_runner(monkeypatch) tool_dir = tmp_path / "uv" monkeypatch.setattr( materializer.tempfile, @@ -721,6 +736,7 @@ def test_install_trusted_uv_rejects_wrong_version_or_exit_status( completed: subprocess.CompletedProcess[bytes], ) -> None: """Unexpected version output or a nonzero status cannot satisfy the pin.""" + _simulate_linux_x86_64_runner(monkeypatch) tool_dir = tmp_path / f"uv-{completed.returncode}-{len(completed.stdout)}" monkeypatch.setattr( materializer.tempfile, From 91d4332f031f623b409df4fd8e5aa07f633c7fb9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 14 Aug 2026 01:16:26 +0900 Subject: [PATCH 5/5] fix(coverage): accept only bounded relative requirement includes Materialize a base Python lock only when every package line is an exact SHA-256 pin or a two-token relative -r/--requirement include of a candidate lock path. A lone --require-hashes directive, ./dotted paths, and -r other-hashes.txt no longer enter the trusted build context. --- AGENTS.md | 2 + CHANGELOG.md | 1 + .../doctoring/codeql-action-single-version.md | 2 + .../materialize_base_python_requirements.py | 85 +++++++++++++++---- ...st_materialize_base_python_requirements.py | 19 ++++- 5 files changed, 91 insertions(+), 18 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 688b33035..6966fd140 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,3 +2,5 @@ > **Agents: read the master context FIRST.** Before any work, read [`docs/CWL-MASTER-CONTEXT.md`](docs/CWL-MASTER-CONTEXT.md) (mission · naruon-as-platform + inter-component UML · cross-cutting disciplines · conventions · roadmap · current state), the live **GitHub Project #1** (work/roadmap source of truth), the full spec **ContextualWisdomLab/naruon#974**, and operate the Project per [`docs/agent-github-project-protocol.md`](docs/agent-github-project-protocol.md). The repo/Project — not any private agent memory — is the source of truth. + +Materialize accepts only exact SHA-256 pins or a bounded relative `-r` include (no `.`/`..`); a lone `--require-hashes` directive is not trust evidence. See [`docs/doctoring/codeql-action-single-version.md`](docs/doctoring/codeql-action-single-version.md). diff --git a/CHANGELOG.md b/CHANGELOG.md index ca26decf7..e896dc21b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ Semantic Versioning where the repository publishes a release. ### Fixed +- Materialized base Python locks only when every package line is an exact SHA-256 pin or a bounded relative `-r`/`--requirement` include. A lone `--require-hashes` directive, a dotted include such as `./lock.txt`, or `-r other-hashes.txt` no longer enters the trusted build context. - Pinned CodeQL `init`, `analyze`, and `upload-sarif` to one immutable SHA (`5595ccaf912efad79be6eef63a5619ff05969be3`, v4.37.6) across the PR and scheduled security workflows so a Dependabot split cannot analyze or upload with a different trusted action (CWE-829). - Bounded the Strix quality self-test's deterministic timeout fixtures to 3-second process and 5-second fake-sleep budgets so exact-head policy evidence completes inside the existing job limit without changing production Strix scanner timeouts, providers, credentials, or review semantics. - Allowed commas and ASCII parentheses in the bounded Strix changed-file path policy so legal tracked Packrat fixtures can receive exact-head security analysis, while rejecting raw `..` components before normalization and keeping controls, backslashes, whitespace ambiguity, and shell punctuation fail-closed. diff --git a/docs/doctoring/codeql-action-single-version.md b/docs/doctoring/codeql-action-single-version.md index f58b235a5..52a0f614b 100644 --- a/docs/doctoring/codeql-action-single-version.md +++ b/docs/doctoring/codeql-action-single-version.md @@ -10,6 +10,8 @@ the scheduled scan used the same reviewed binary. ## Decision +Materialize accepts only exact SHA-256 pins or a bounded relative `-r` include; a lone `--require-hashes` line is not lock evidence. + Pin `init`, `analyze`, and `upload-sarif` to one immutable SHA (`5595ccaf912efad79be6eef63a5619ff05969be3`, v4.37.6) in both `codeql-pr.yml` and `scheduled-security-scan.yml`. Contract tests reject diff --git a/scripts/ci/materialize_base_python_requirements.py b/scripts/ci/materialize_base_python_requirements.py index 98cdad459..7a9c204b8 100755 --- a/scripts/ci/materialize_base_python_requirements.py +++ b/scripts/ci/materialize_base_python_requirements.py @@ -87,6 +87,58 @@ def _is_candidate_lock_name(name: str) -> bool: ) + +def _is_candidate_lock_path(path: pathlib.PurePosixPath) -> bool: + """Return whether one safe tracked path can name a pip requirements lock. + + In addition to conventional ``requirements*.txt`` names, repositories often + keep concrete environment closures as direct children such as + ``requirements/ci.txt`` or ``service/requirements/package.txt``. Only direct + ``.txt`` children of a directory named ``requirements`` gain this path-based + eligibility; content must still pass the independent complete hash-pin + validation before it reaches the trusted image build context. + """ + return _is_candidate_lock_name(path.name) or ( + path.suffix == ".txt" and path.parent.name == "requirements" + ) + + +def _is_bounded_requirement_include(line: str) -> bool: + """Return whether one requirements include names a bounded relative file. + + Includes are accepted only as a two-token ``-r``/``--requirement`` form + whose target is itself a candidate lock path written as a normalized + relative POSIX path. Absolute paths, ``.`` or ``..`` components, double + slashes, URLs, option-like targets, shell/Windows path separators, + fragments, queries, extra inline options or hashes, and includes of + non-lock files are rejected before a base-owned file can enter the + trusted build context. + The downstream installer still proves that the candidate is an independently + complete hash closure; this predicate grants syntax eligibility only. + """ + fields = line.split() + if len(fields) != 2 or fields[0] not in {"-r", "--requirement"}: + return False + target = fields[1] + if ( + target.startswith(("-", "~")) + or "\\" in target + or ":" in target + or "?" in target + or "#" in target + ): + return False + include_path = pathlib.PurePosixPath(target) + return ( + bool(include_path.parts) + and target == include_path.as_posix() + and not include_path.is_absolute() + and "." not in include_path.parts + and ".." not in include_path.parts + and _is_candidate_lock_path(include_path) + ) + + def _requirement_lines(content: bytes) -> list[str]: """Return logical requirement lines, joining backslash line-continuations. @@ -107,26 +159,27 @@ def _requirement_lines(content: bytes) -> list[str]: def _is_hash_pinned(content: bytes) -> bool: - """Return whether content carries hash pins and is safe to preflight. - - Discovery is content-based rather than name-based so hash-pinned locks in any - location (a service subdirectory, ``requirements-dev.txt``, - ``requirements-test.txt``) can be considered for offline coverage, while an - unpinned or PR-mutable requirements file is still excluded from the networked - build context. Hash syntax cannot prove that a file includes every transitive - dependency, so the trusted image installer separately preflights every - candidate as an independent ``--require-hashes`` closure. An empty file - carries no installable dependency and is not materialized. + """Return whether content carries only trusted pins or bounded includes. + + Discovery is content-based rather than name-based so exact hash-pinned locks + in service subdirectories and role-specific requirements files can be + considered for offline coverage. Candidate syntax is deliberately stricter + than a substring search: each package line must be an exact ``==`` pin with + one or more complete SHA-256 hashes, or a bounded relative requirements + include. A global ``--require-hashes`` directive is not trust evidence by + itself. The downstream installer separately preflights every candidate as an + independent ``pip --require-hashes`` closure, so syntax eligibility never + substitutes for dependency-closure proof. """ lines = _requirement_lines(content) - if not lines: + requirement_lines = [line for line in lines if line != "--require-hashes"] + if not requirement_lines: return False - return any(line == "--require-hashes" for line in lines) or all( - "--hash=" in line or line.startswith(("-r ", "--requirement ")) - for line in lines + return all( + _is_fully_hash_pinned_requirement(line) + or _is_bounded_requirement_include(line) + for line in requirement_lines ) - - def _is_fully_hash_pinned_requirement(line: str) -> bool: """Return whether one uv-export line is an exact package pin with SHA-256 hashes.""" fields = re.split(r"\s+(?=--hash=)", line) diff --git a/tests/test_materialize_base_python_requirements.py b/tests/test_materialize_base_python_requirements.py index a9e974d12..b56955a21 100644 --- a/tests/test_materialize_base_python_requirements.py +++ b/tests/test_materialize_base_python_requirements.py @@ -163,9 +163,24 @@ def test_lock_name_candidates_are_pip_requirements_files() -> None: def test_hash_pin_detection_includes_pinned_and_excludes_unpinned_or_empty() -> None: """Only fully hash-pinned, non-empty lock content is materialized.""" assert not materializer._is_hash_pinned(b"# comment only\n\n") - assert materializer._is_hash_pinned(b"--require-hashes\ndemo==1\n") + assert not materializer._is_hash_pinned(b"--require-hashes\ndemo==1\n") assert materializer._is_hash_pinned(b"demo==1 --hash=sha256:" + b"a" * 64 + b"\n") - assert materializer._is_hash_pinned(b"-r other-hashes.txt\n") + assert materializer._is_hash_pinned(b"-r requirements-other.txt\n") + assert not materializer._is_hash_pinned(b"-r other-hashes.txt\n") + assert not materializer._is_hash_pinned(b"-r ./requirements-other.txt\n") + assert not materializer._is_hash_pinned(b"-r ../escape.txt\n") + assert materializer._is_bounded_requirement_include( + "--requirement requirements-other.txt" + ) + assert not materializer._is_bounded_requirement_include("-r .") + assert not materializer._is_bounded_requirement_include("-r -evil.txt") + assert not materializer._is_bounded_requirement_include("-r ~evil.txt") + assert not materializer._is_bounded_requirement_include("-r C:foo.txt") + assert not materializer._is_bounded_requirement_include("-r foo?bar.txt") + assert not materializer._is_bounded_requirement_include("-r foo#bar.txt") + assert not materializer._is_bounded_requirement_include(r"-r foo\\bar.txt") + assert not materializer._is_bounded_requirement_include("-r") + assert not materializer._is_bounded_requirement_include("-r /abs/requirements.txt") assert not materializer._is_hash_pinned(b"untrusted==1\n") # uv export / pip-compile multi-line continuation format (spec, then --hash= lines). assert materializer._is_hash_pinned(