diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 426dd6be..1c6c3a75 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -200,7 +200,7 @@ jobs: run: uv export --frozen --no-dev --no-emit-project --format requirements.txt --output-file .ci-audit-requirements.txt - name: Audit dependencies - run: uv run pip-audit -r .ci-audit-requirements.txt + run: uv run python scripts/run_dependency_audit.py -r .ci-audit-requirements.txt validate-fingerprints: runs-on: ubuntu-latest diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 69e166a0..62cd3f85 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -1,6 +1,8 @@ name: CodeQL on: + push: + branches: ["main"] pull_request: branches: ["main"] schedule: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1fd11abd..e72d23d2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -156,7 +156,7 @@ jobs: run: uv export --frozen --no-dev --no-emit-project --format requirements.txt --output-file .ci-audit-requirements.txt - name: Audit dependencies - run: uv run pip-audit -r .ci-audit-requirements.txt + run: uv run python scripts/run_dependency_audit.py -r .ci-audit-requirements.txt build: # Pure build job. The supply-chain isolation contract requires @@ -403,9 +403,9 @@ jobs: mkdir -p sbom # ``--no-deps`` tells pip-audit not to install the locked # requirements again; it just reads the requirements text - # to produce the SBOM. Exit 1 means findings and is accepted here - # because the enforcing audit runs in ``test``. Any higher status, - # missing/corrupt JSON, or missing project root fails publication. + # to produce the SBOM. Every nonzero status blocks publication, + # including a finding first disclosed after the enforcing audit ran. + # Missing/corrupt JSON or a missing project root also fails closed. set +e uv tool run pip-audit -r .ci-audit-requirements.txt \ --format=cyclonedx-json \ @@ -413,7 +413,7 @@ jobs: --no-deps audit_status=$? set -e - if [ "$audit_status" -gt 1 ]; then + if [ "$audit_status" -ne 0 ]; then exit "$audit_status" fi test -s "sbom/recon-tool-${VERSION}.cdx.json" diff --git a/CHANGELOG.md b/CHANGELOG.md index e882e374..ffd98021 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,34 @@ operator, corporate group, ownership, or control. ## [Unreleased] +## [2.6.6] - 2026-07-18 + +### Tool Surface Changes + +Tool surface changes: no CLI command or flag changes. + +### Changed + +- CodeQL now analyzes every push to `main` in addition to pull requests, + scheduled scans, and manual runs, keeping exact default-branch evidence + current without a release-time dispatch. +- Roadmap, OpenSSF, supply-chain, security, and release guidance now separates + shipped behavior, durable policy, and volatile external evidence without + pinning current trust claims to an obsolete release or sample size. + +### Fixed + +- Enforcing CI and release dependency audits now retry exactly once only for a + strict allowlist of recognized transport failures. Vulnerability findings, + malformed input, unknown failures, and exhausted retries remain nonzero and + keep publication blocked. +- The dependency-audit runner now resolves the installed auditor under Python + isolated mode, preventing checkout files from shadowing the blocking audit + module. The later isolated SBOM job now blocks on every nonzero audit status, + including a finding first disclosed after the enforcing audit ran. +- The latest security-audit resolution now records v2.6.4 as its actual closure + release instead of retaining an Unreleased label. + ## [2.6.5] - 2026-07-18 ### Tool Surface Changes diff --git a/ROADMAP.md b/ROADMAP.md index 25bf93f8..b314a974 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -4,8 +4,9 @@ The canonical roadmap lives in [docs/roadmap.md](docs/roadmap.md). Current status: recon v2.6.5 has a stable, production-ready baseline. The CLI, JSON schema, local stdio MCP server, bounded collectors, generated-artifact -guards, validation gates, and release path are shipped. Unreleased work improves -first-run help and error recovery, removes rejected MCP values from logs, +guards, validation gates, and release path are shipped. Recent shipped work +improves first-run help and error recovery, removes rejected MCP values from +logs, aligns public issue intake with the no-target-data policy, corrects the package surface description, requires verification dates on new fingerprint detections, groups batch help by task, redacts unexpected batch exception @@ -13,16 +14,16 @@ details, reports cache deletion failures, exposes payload-free result and CT cache metadata, hardens diagnostic rendering, discloses doctor connectivity, keeps narrow help complete, adds all-source failure recovery, executes the sealed wheel before publication, and treats closed output pipes as normal -control flow. Current polish also makes catalog search bounded and +control flow. Shipped polish also makes catalog search bounded and field-associated, preserves every same-slug fingerprint record in `show`, aligns CLI and MCP category matching, keeps signal summary projections consistent, emits plain redirected gate logs, and makes partial-release reruns -stop on named preconditions before mutation. The current maintenance pass also +stop on named preconditions before mutation. Shipped maintenance also bounds default cache payload inspection with exact completeness counts, exposes and clears interrupted-write residue, separates fingerprint corpus errors from misses under bounded input, rejects empty catalog filters, preserves ranked signal hierarchy, and keeps welcome descriptions associated at 80 columns. -The current MCP diagnostics pass makes static registry failures exit non-zero, +The shipped MCP diagnostics pass makes static registry failures exit non-zero, exercises every canonical local JSON resource through live stdio, preserves the failed protocol phase, and documents the three-part server and client check. The active product work is evidence-semantic integrity and a @@ -37,7 +38,7 @@ follow-up work. The first bounded internal claim contract now evaluates the exac DMARC `p=reject` observation and drives the opt-in cohort schema 2.2 DMARC denominator while schema 2.1 remains the default. -Unreleased maintenance also refuses HTTP destinations whose hostname or DNS +Shipped maintenance also refuses HTTP destinations whose hostname or DNS result cannot be validated, confines default MCP logging to the running server, bounds fatal MCP stderr, keeps doctor checks useful across ordinary HTTP request failures, labels CT cache overview freshness, and completes narrow root @@ -51,18 +52,21 @@ validates the completed SBOM, exported provenance bundle, exact signer workflow, source tag and commit digest, and cross-channel digests. Reviewed installer helpers bind installation to their release version and preserve one existing package-manager owner; consumer verification now has -complete fail-closed POSIX and PowerShell paths. Current release hardening also +complete fail-closed POSIX and PowerShell paths. Shipped release hardening also reuses one bounded exact PyPI metadata validator, includes the completed SBOM in GitHub provenance, and rejects unsafe existing-release state before recovery can replace assets. The operator recovery block itself now uses strict Bash mode, named failures, and a visible pre-mutation success checkpoint. One -digest-bound v2.6.3 historical exception preserves the -published distribution-only bundle while still requiring SBOM validation; -future releases fail if SBOM provenance is absent. + digest-bound v2.6.3 historical exception preserves the + published distribution-only bundle while still requiring SBOM validation; + future releases fail if SBOM provenance is absent. Enforcing dependency + audits resolve the installed auditor under Python isolated mode, and the + later SBOM job treats every nonzero audit status as fatal. -Pull requests targeting `main` now run CodeQL before merge while scheduled and -manual default-branch scans remain available. Remote readiness keeps the other -required code-owned Scorecard controls at `10` and uses the observed SAST floor +Pull requests targeting `main` run CodeQL before merge, every push to `main` +updates exact default-branch analysis, and scheduled and manual scans remain +available. Remote readiness keeps the other required code-owned Scorecard +controls at `10` and uses the observed SAST floor of `7` while the public sample still contains pre-policy pull requests. The SAST requirement returns to `10` only after the public API reports supported SAST checks for every sampled merged pull request; historical checks are not diff --git a/docs/assurance-case.md b/docs/assurance-case.md index 7a9801b3..0cdf9b2f 100644 --- a/docs/assurance-case.md +++ b/docs/assurance-case.md @@ -116,7 +116,7 @@ matched environment. Full details and the bounded recipe are in | A remote readiness report cannot combine current-main CI with an older or moved published package: the remote and local current project-version tag plus `HEAD` must resolve to the same full commit | `release_readiness::_check_release_tag_binding`; `test_release_readiness::test_remote_release_tag_binding_rejects_mixed_head_and_release_state`; `test_release_readiness::test_remote_release_tag_binding_rejects_moved_public_tag` | | Local release transaction starts from clean current `main`, synchronizes exact-version installer helpers with other owned surfaces, stages only those surfaces, creates one reviewed tag, and restores files, index, commit, and owned tag when the mutation transaction fails | `test_release_script::test_release_surface_generation_updates_installers_and_artifacts`, `test_release_script::test_release_transaction_owns_both_installer_helpers`, `test_release_script::test_release_push_command_names_only_the_reviewed_tag`, `test_release_script::test_release_rollback_restores_files_index_commit_and_owned_tag`, `test_install_scripts::test_installers_bind_the_reviewed_release_version_and_owner` | | A release tag must match the project version and dated nonempty changelog section, identify the workflow SHA, and be contained in freshly fetched current `main` before the test gate can run | `test_validate_release_tag::test_matching_tag_with_notes_and_main_ancestry_passes`, `test_validate_release_tag::test_mismatched_tag_fails_before_git`, `test_validate_release_tag::test_non_main_tag_fails`, `test_release_workflow::test_release_preflight_blocks_mismatched_or_non_main_tags` | -| CycloneDX output contains the exact versioned project root and complete dependency edge; publication and remote verification fail rather than repairing malformed or non-UTF-8 completed evidence | `test_finalize_sbom::test_finalize_sbom_adds_project_root_and_dependency_edge`, `test_finalize_sbom::test_completed_sbom_validation_fails_without_repair`, `test_finalize_sbom::test_completed_sbom_validation_rejects_non_utf8_bytes`, `test_release_workflow::test_pypi_publication_waits_for_valid_sbom`, `test_release_workflow_contract::TestGithubReleaseAttachesBothArtifacts` | +| CycloneDX generation rejects every nonzero audit status; output contains the exact versioned project root and complete dependency edge; publication and remote verification fail rather than repairing malformed or non-UTF-8 completed evidence | `test_run_dependency_audit::test_enforcing_workflows_use_bounded_audit_runner`, `test_release_workflow_contract::TestSbomJobIsIsolated::test_sbom_job_validates_complete_project_bom`, `test_finalize_sbom::test_finalize_sbom_adds_project_root_and_dependency_edge`, `test_finalize_sbom::test_completed_sbom_validation_fails_without_repair`, `test_finalize_sbom::test_completed_sbom_validation_rejects_non_utf8_bytes`, `test_release_workflow::test_pypi_publication_waits_for_valid_sbom` | | GitHub verification for releases produced by the current workflow binds the wheel, sdist, and completed SBOM to the exported bundle, exact release workflow, source tag, commit digest, and hosted-runner boundary. The exact v2.6.3 historical exception requires wheel and sdist provenance plus completed SBOM structure validation; PyPI uses pinned PEP 740 verification and OIDC Trusted Publishing | `test_release_readiness::test_github_attestations_verify_wheel_sdist_and_sbom`; `test_release_readiness::test_github_attestations_preserve_exact_v263_legacy_subject_boundary`; `release.yml` (`attest`, `publish-pypi` jobs); `test_release_workflow_contract::TestAttestationJob` | | Existing GitHub Release recovery permits asset replacement only for the exact published tag and title with mutable, expected-only inventory, complete or partial; draft, prerelease, immutable, duplicate, unexpected, and malformed state fails before `--clobber`, and the remote tag must still resolve to the workflow SHA | `check_release_recovery::validate_release_recovery`; `test_release_recovery::test_unsafe_existing_release_state_is_rejected`; `test_release_workflow_contract::TestGithubReleaseAttachesBothArtifacts` | diff --git a/docs/openssf-posture.md b/docs/openssf-posture.md index b458c593..6dfb4f57 100644 --- a/docs/openssf-posture.md +++ b/docs/openssf-posture.md @@ -7,8 +7,8 @@ while the Best Practices Badge is an account-backed project questionnaire. ## Current Snapshot - Date checked: 2026-07-18. -- Live API state at check time: public API rechecked for the exact `HEAD` commit - that published v2.6.4 after CI and publication completed. +- Live API state at check time: public API rechecked for the exact current + release `HEAD` after CI and publication completed. - Scorecard version: `v5.3.0`. - Score: `8.2`. - Public source: `https://api.securityscorecards.dev/projects/github.com/blisspixel/recon`. @@ -24,22 +24,23 @@ The measured non-SAST code-owned controls are green: dangerous workflow patterns, dependency update automation, token permissions, pinned dependencies, binary artifacts, security policy, known vulnerabilities, packaging, fuzzing, signed releases, and license all score `10`. CI-Tests also scores `10` because -all 17 sampled merged pull requests ran CI. SAST scores `7`: CodeQL is detected, -but the same 17 pull-request heads predate PR-scoped CodeQL and have no supported -SAST check. A scheduled or manual scan of `main` does not rewrite that history. -New pull requests targeting `main` now run CodeQL while the weekly and manual -main analysis remains available. The SAST gate can return to `10` only after the +every sampled merged pull request ran CI. SAST scores `7`: CodeQL is detected, +but the sample still includes merged pull-request heads from before PR-scoped +CodeQL was required. Current pull requests provide supported SAST evidence; a +default-branch scan does not rewrite older pull-request history. Pull requests +targeting `main` and pushes to `main` run CodeQL, while weekly and manual main +analysis remains available. The SAST gate can return to `10` only after the public API reports successful supported SAST checks for every merged pull -request in its sampled window. The exact overall score remains a dated external -snapshot; the enforced `8.0` overall and `7` SAST floors are the current -regression policy. +request in its sampled window. The exact sample size and overall score remain +dated external state; the enforced `8.0` overall and `7` SAST floors are the +current regression policy. The remaining low or unknown checks are process-bound: | Check | Current reason | Real next step | |---|---|---| | `Branch-Protection` | `main` is protected, but administrator bypass remains and PRs are not mandatory. | Keep the current required-check ruleset for clean-main work. If the project moves to a multi-maintainer flow, require PRs, remove administrator bypass, and require CODEOWNERS review. | -| `Code-Review` | No reviewed pull-request history exists for Scorecard to evaluate. | Use normal reviewed PRs for non-urgent work when there is another qualified reviewer. Do not manufacture review history. | +| `Code-Review` | No human-approved changesets appear in Scorecard's sampled window; automated review comments are not approvals. | Use normal reviewed PRs for non-urgent work when there is another qualified reviewer. Do not manufacture review history. | | `CII-Best-Practices` | No OpenSSF Best Practices Badge project is linked. | Use [openssf-badge-readiness.md](openssf-badge-readiness.md) to answer the questionnaire from committed evidence; link a badge only after the real project page exists and the answers match the repository. | | `Contributors` | Scorecard sees no contributor diversity across organizations. | Accept this for a single-maintainer project. Do not add artificial contributors. | diff --git a/docs/release-process.md b/docs/release-process.md index 602d6020..703bcbc2 100644 --- a/docs/release-process.md +++ b/docs/release-process.md @@ -209,7 +209,10 @@ Triggered by any tag matching `v*` pushed to the repo. The workflow: 2. **test**: after preflight, runs the exact stable and candidate MCP SDK matrix, then delegates to the complete `scripts/check.py` gate with added-line text hygiene covering the full previous-release-to-tag range. A separate - hash-pinned runtime requirements export is audited with `pip-audit`. + hash-pinned runtime requirements export is audited with `pip-audit`. The + audit runner resolves the installed auditor under Python isolated mode and + retries once only for recognized transport failures; findings, unknown + failures, and a second transport failure remain fatal. 3. **build**: after `test`, exact uv 0.11.17 uses the hash-locked build graph to produce one sdist, constructs one wheel from that exact sdist, and immediately seals both under `dist/`. Main CI runs the same sequence twice, @@ -221,9 +224,9 @@ Triggered by any tag matching `v*` pushed to the repo. The workflow: `python -m recon_tool --version` from the wheel in isolated environments. 5. **sbom**: after `test`, generates the CycloneDX release SBOM independently of the package build path, adds the project root and dependency edge, and - validates the resulting document. Findings may coexist with this artifact - because the enforcing dependency audit already ran in `test`; SBOM tool or - validation failure is fatal. + validates the resulting document. Every nonzero audit status is fatal here, + including a finding first disclosed after `test`; SBOM tool or validation + failure is also fatal. 6. **attest**: after `build` and `sbom`, records one GitHub provenance attestation whose subject set contains the wheel, sdist, and completed SBOM. 7. **export-attestations**: after `build`, `sbom`, and `attest`, exports the diff --git a/docs/roadmap.md b/docs/roadmap.md index ff1b8274..3bec3e02 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -51,13 +51,13 @@ current debt without turning every refinement into feature work. | Debt class | Current state | Next boundary | |---|---|---| | Feature work | Governed by the dependency order below, not by the polish loop | Do not add commands, schemas, hosted surfaces, or inference modes without their existing evidence gate | -| UX flow | Root help, no-argument onboarding, malformed-input recovery, all-source failure recovery, low-confidence next steps, batch outcome guidance, cross-platform release verification, target-free catalog discovery, and explicit bounded-versus-complete cache inspection are implemented (Unreleased) | Specify batch all-error exit semantics before considering an opt-in strict mode | -| Visual polish | Lookup and batch help use task panels; linear help and adaptive welcome rows keep commands complete; fingerprint previews, ranked signal results, and narrow cache rows keep hierarchy and field association without changing structured order (Unreleased) | Preserve complete option visibility and exact technical-token copyability before changing presentation metadata | -| Observability | MCP rejection logs and unexpected batch details stay bounded; live MCP diagnostics retain completed rows and name the failed protocol phase; cache overview names exact inspected, uninspected, failed, and temporary-artifact state; corpus tests separate collection errors from negative observations; captured gate logs are plain; remote readiness and release recovery name exact evidence and preconditions (Unreleased) | Define a versioned doctor or cache record only after a machine consumer and compatibility envelope are named | -| Reliability | Static and live MCP diagnostics require canonical tools and resources, with live JSON resource reads; typed batch errors, bounded workers, bounded corpus and default cache inspection, residue-aware cache clearing, closed-pipe handling, explicit degradation, exact exit codes, complete catalog inspection, sealed artifact reconstruction and parity, and command-status-aware release recovery are implemented and listed in Unreleased | Do not change the current batch or corpus-test exit contract without a compatibility decision covering mixed and all-error streams | -| Security | Unresolved HTTP destinations fail closed; rejected values and unexpected details stay out of default output; persisted inputs, corpus files, local catalog text, and default cache payload work are bounded; empty catalog filters cannot bypass compact defaults; release verification binds artifacts and recovery commands to exact status, workflow, tag, signer, and commit evidence; pull requests targeting `main` run CodeQL while weekly and manual main scans remain (Unreleased) | Ratchet the remote SAST requirement from its evidence-backed transition floor of 7 to 10 only after the public Scorecard window reports successful supported SAST checks for every sampled merged pull request; do not backfill history for the metric | -| Accessibility | `--plain` is shipped; help uses complete linear output when needed; welcome alignment is content-aware at ordinary widths; catalog and cache rows preserve labels when narrow; color is never the only status channel (Unreleased) | Keep both paths complete; do not replace the parser or hide specialist controls | -| Documentation accuracy | README, MCP quick starts, stability, operational, security, generated CLI, release, catalog, cache, corpus, agent, schema, example, snapshot, and test surfaces use reserved target identities or disclosure-safe aggregates; the release gate rejects the retired target vocabulary (Unreleased) | Keep eventual machine diagnostics separate from human output until versioned contracts are justified; retain manual review for previously unseen organization-shaped prose and preserve provider definitions | +| UX flow | Root help, no-argument onboarding, malformed-input recovery, all-source failure recovery, low-confidence next steps, batch outcome guidance, cross-platform release verification, target-free catalog discovery, and explicit bounded-versus-complete cache inspection are implemented | Specify batch all-error exit semantics before considering an opt-in strict mode | +| Visual polish | Lookup and batch help use task panels; linear help and adaptive welcome rows keep commands complete; fingerprint previews, ranked signal results, and narrow cache rows keep hierarchy and field association without changing structured order | Preserve complete option visibility and exact technical-token copyability before changing presentation metadata | +| Observability | MCP rejection logs and unexpected batch details stay bounded; live MCP diagnostics retain completed rows and name the failed protocol phase; cache overview names exact inspected, uninspected, failed, and temporary-artifact state; corpus tests separate collection errors from negative observations; captured gate logs are plain; remote readiness and release recovery name exact evidence and preconditions | Define a versioned doctor or cache record only after a machine consumer and compatibility envelope are named | +| Reliability | Static and live MCP diagnostics require canonical tools and resources, with live JSON resource reads; typed batch errors, bounded workers, bounded corpus and default cache inspection, residue-aware cache clearing, closed-pipe handling, explicit degradation, exact exit codes, complete catalog inspection, sealed artifact reconstruction and parity, command-status-aware release recovery, fail-closed bounded dependency-audit retry, and all-nonzero SBOM audit gating are implemented | Do not change the current batch or corpus-test exit contract without a compatibility decision covering mixed and all-error streams | +| Security | Unresolved HTTP destinations fail closed; rejected values and unexpected details stay out of default output; persisted inputs, corpus files, local catalog text, and default cache payload work are bounded; empty catalog filters cannot bypass compact defaults; release verification binds artifacts and recovery commands to exact status, workflow, tag, signer, and commit evidence; the installed dependency auditor resolves under Python isolated mode; pull requests and pushes to `main` run CodeQL while weekly and manual main scans remain | Ratchet the remote SAST requirement from its evidence-backed transition floor of 7 to 10 only after the public Scorecard window reports successful supported SAST checks for every sampled merged pull request; do not backfill history for the metric | +| Accessibility | `--plain` is shipped; help uses complete linear output when needed; welcome alignment is content-aware at ordinary widths; catalog and cache rows preserve labels when narrow; color is never the only status channel | Keep both paths complete; do not replace the parser or hide specialist controls | +| Documentation accuracy | README, MCP quick starts, stability, operational, security, generated CLI, release, catalog, cache, corpus, agent, schema, example, snapshot, and test surfaces use reserved target identities or disclosure-safe aggregates; the release gate rejects the retired target vocabulary | Keep eventual machine diagnostics separate from human output until versioned contracts are justified; retain manual review for previously unseen organization-shaped prose and preserve provider definitions | ## Priority Order diff --git a/docs/security-audit-resolutions.md b/docs/security-audit-resolutions.md index 540facf7..c665cc98 100644 --- a/docs/security-audit-resolutions.md +++ b/docs/security-audit-resolutions.md @@ -18,13 +18,38 @@ notes, see [`docs/security.md`](security.md). --- -## Closed: unresolved HTTP destinations and MCP diagnostic side effects (Unreleased) +## Closed in v2.6.6: dependency-audit execution identity and advisory timing + +| Field | Value | +|---|---| +| **Severity (as audited)** | Low; release-control integrity requires a reviewed checkout change | +| **Source** | Cycle 15 security diff review (2026-07) | +| **Fully closed** | **v2.6.6** | +| **Pinned by** | `tests/test_run_dependency_audit.py`, `tests/test_release_workflow_contract.py` | + +The enforcing audit runner previously invoked `python -m pip_audit` through the +ordinary current-directory module search. A reviewed checkout containing a +root-level `pip_audit.py` or `pip_audit/` package could therefore replace the +installed auditor and return a false success. The runner now uses Python +isolated mode, preserving the installed environment while excluding checkout +module shadowing. A real subprocess regression creates a colliding module and +requires the installed `pip-audit` version output. + +The later isolated SBOM job also previously accepted audit status 1 because the +earlier release test job had already audited the same lock. Advisory state can +change between jobs, so the SBOM job now treats every nonzero audit status as +fatal. Artifact isolation, exact output validation, and publication dependencies +remain unchanged. + +--- + +## Closed in v2.6.4: unresolved HTTP destinations and MCP diagnostic side effects | Field | Value | |---|---| | **Severity (as audited)** | High for destination validation; medium for process and terminal diagnostic integrity | | **Source** | Cycle 5 bounded maintenance review (2026-07) | -| **Fully closed** | **Unreleased** | +| **Fully closed** | **v2.6.4** | | **Pinned by** | `tests/test_http.py`, `tests/test_http_advanced.py`, `tests/test_doctor_mcp.py`, `tests/test_server.py` | The shared HTTP preflight previously returned "not private" when hostname @@ -728,9 +753,12 @@ the fix. No ignore needed; the audit passes on the upgraded version. **pyjwt, PYSEC-2025-183 / CVE-2025-45768 - fixed.** The historical 2.12.1 exception was removed on 2026-07-10 after the lockfile moved to PyJWT 2.13.0 and an unignored audit of the frozen runtime dependency set -reported no known vulnerabilities. CI and the release workflow now run the -same unqualified `pip-audit` gate. The earlier reachability analysis remains -part of the audit history, but no exception is active. +reported no known vulnerabilities. CI and the release workflow run the +installed `pip-audit` module under Python isolated mode through a fail-closed +runner that retries once only for recognized transport failures. Findings, +unknown failures, and a second transport failure remain nonzero. The earlier +reachability analysis remains part of the audit history, but no exception is +active. --- diff --git a/docs/security.md b/docs/security.md index 979f7c98..25f408a3 100644 --- a/docs/security.md +++ b/docs/security.md @@ -258,8 +258,11 @@ recon does **not** defend against: - **DNS cache poisoning at the OS level.** If the user's resolver is compromised, the entire threat model is compromised regardless of what recon does. - **Supply-chain compromise of transitive dependencies.** Locked runtime dependencies are checked by blocking `pip-audit` gates in CI and release - workflows. This detects known advisories; it does not prevent a compromise or - inspect dependencies in-process. + workflows. Their runner resolves the installed auditor under Python isolated + mode, retries once only for recognized transport failures, and never converts + a finding or exhausted retry into success. The later isolated SBOM job also + blocks on every nonzero audit status. This detects known advisories; it does + not prevent a compromise or inspect dependencies in-process. - **Side-channel timing attacks against the user's identity.** Every query recon makes is visible to the intermediary services (OIDC endpoints, CT providers, the user's DNS resolver). See [`legal.md`](legal.md#what-sees-your-queries) for the exposure inventory. - **Logging / telemetry exfiltration.** recon emits no telemetry. All output goes to the user's terminal or files they own. - **Malicious plugin systems.** recon has no plugin system and executes no user code. diff --git a/docs/supply-chain.md b/docs/supply-chain.md index 03ba1524..43298545 100644 --- a/docs/supply-chain.md +++ b/docs/supply-chain.md @@ -24,7 +24,14 @@ produces and publishes: | **Constrained deterministic-build check** | `SOURCE_DATE_EPOCH`, uv 0.11.17, and the exact hash-locked backend graph are fixed; CI builds the sdist and reconstructs its wheel twice in one Ubuntu job, then compares both hashes | See the bounded recipe and limitations below | | **Sealed distribution gate** | A separate read-only release job requires exactly one tag-matching wheel and sdist, then executes both `recon --version` and `python -m recon_tool --version` from the wheel before either publication channel can run | Inspect the `package-smoke` job in the tagged release workflow run | | **Published-channel parity gate** | After PyPI publication, a separate read-only job requires the exact wheel and sdist and compares their SHA-256 digests with the sealed build pair. GitHub Release publication cannot create or replace assets until parity passes | Inspect the `verify-pypi-parity` job or run `scripts/check_release_channel_parity.py` from the exact source tag | -| **CycloneDX SBOM** | Generated from a runtime-requirements export of `uv.lock` (`pip-audit --format=cyclonedx-json`), completed with the `recon-tool` root component and dependency edge, validated as nonempty JSON, and attached to the GitHub Release. The isolated SBOM job may emit an artifact when findings exist because the separate enforcing audit already blocks the release test job. Any SBOM tool, output, or validation failure blocks both PyPI and GitHub publication | Download `recon-tool-.cdx.json` from the release assets and inspect `metadata.component` and the root dependency entry | +| **CycloneDX SBOM** | Generated from a runtime-requirements export of `uv.lock` (`pip-audit --format=cyclonedx-json`), completed with the `recon-tool` root component and dependency edge, validated as nonempty JSON, and attached to the GitHub Release. Any finding, SBOM tool failure, malformed output, or validation failure in this later isolated job blocks both PyPI and GitHub publication | Download `recon-tool-.cdx.json` from the release assets and inspect `metadata.component` and the root dependency entry | + +The enforcing CI and release dependency audits resolve the installed auditor +under Python isolated mode and retry exactly once only when their output matches +a narrow set of recognized transport failures. A vulnerability summary, +malformed input, unknown failure, or second transport failure remains nonzero. +The later isolated SBOM job also fails on every nonzero audit status, including a +finding first disclosed after the enforcing release audit ran. ## Consumer verification quick path @@ -499,9 +506,10 @@ recovery boundary are documented in [release-process.md](release-process.md). The repository also runs supply-chain posture checks outside the release flow: - OpenSSF Scorecard publishes the public posture badge and SARIF results. -- CodeQL runs on pull requests targeting `main`, on a weekly schedule, and on - demand. The pull-request event supplies pre-merge analysis; the schedule keeps - a full default-branch scan independent of change traffic. +- CodeQL runs on pull requests targeting `main`, on pushes to `main`, on a + weekly schedule, and on demand. Pull requests supply pre-merge analysis; + pushes keep exact default-branch commits current; the schedule provides an + independent full default-branch scan. - ClusterFuzzLite runs a PR-scoped Atheris fuzzer over recon's local parser, normalization, cache deserialization, and formatter-serialization boundaries. The workflow is read-only, SHA-pinned, and bounded to 180 seconds of fuzzing @@ -554,16 +562,16 @@ The repository also runs supply-chain posture checks outside the release flow: - `.github/CODEOWNERS` routes all repository paths to the maintainer account so external pull requests have a clear review owner. -The 2026-07-18 Scorecard recheck for the exact v2.6.4 `HEAD` commit reports +The 2026-07-18 Scorecard recheck for the exact current-release `HEAD` reports score `8.2`. The non-SAST measured code-owned controls are at `10`; SAST is `7` -because all 17 sampled merged pull requests predate PR-scoped CodeQL. Remote -release readiness requires an overall score of at least `8.0`, keeps the other -required code-owned controls at `10`, and enforces the current SAST floor of -`7`. New pull requests targeting `main` run CodeQL, and the weekly and manual -default-branch scans remain. The SAST requirement can return to `10` only after -the public API reports successful supported SAST checks for every merged pull -request in its sampled window. The dated `8.2` value is a snapshot, not a -permanent promise. +because the sampled window still includes merged pull-request heads from before +PR-scoped CodeQL was required. Remote release readiness requires an overall +score of at least `8.0`, keeps the other required code-owned controls at `10`, +and enforces the current SAST floor of `7`. Current pull requests and pushes to +`main` run CodeQL, and weekly and manual default-branch scans remain. The SAST +requirement can return to `10` only after the public API reports successful +supported SAST checks for every merged pull request in its sampled window. The +sample size and dated `8.2` value are snapshots, not permanent promises. The June 28 review found one code-owned gap and several repository-process gaps. The code-owned gap was an unpinned installer download-and-run path; the installer now refuses to execute diff --git a/docs/traceability-matrix.md b/docs/traceability-matrix.md index e24404b6..e0c61dc0 100644 --- a/docs/traceability-matrix.md +++ b/docs/traceability-matrix.md @@ -88,7 +88,7 @@ Promise 6; listed here for completeness of the requirement-to-gate map. | The sealed release contains exactly one tag-matching wheel and sdist, and the wheel executes through both installed entry points before PyPI or GitHub publication | `release.yml` package-smoke job; `test_release_workflow_contract::TestPackageSmokeJob` | | PyPI must expose exactly the sealed wheel and sdist through bounded, exact metadata with trusted file URLs and matching SHA-256 digests before GitHub assets can be created or replaced, and remote readiness reuses the same metadata validator and repeats the comparison | `scripts/check_release_channel_parity.py`; `test_release_channel_parity::test_public_metadata_validator_returns_only_the_exact_safe_pair`; `test_release_channel_parity::test_exact_pair_returns_shared_digests`; `test_release_workflow_contract::TestPublishedChannelParity`; `test_release_readiness::test_release_channel_parity_reports_exact_digest_evidence` | | Remote readiness requires the remote and local current project-version tag plus `HEAD` to identify the same full commit before combining CI, Scorecard, and publication evidence | `release_readiness::_check_release_tag_binding`; `test_release_readiness::test_remote_release_tag_binding_rejects_mixed_head_and_release_state`; `test_release_readiness::test_remote_release_tag_binding_rejects_moved_public_tag` | -| CycloneDX output has the exact project root and complete dependency edge; malformed or non-UTF-8 completed evidence is rejected without repair, and SBOM failure blocks publication | `scripts/finalize_sbom.py`; `test_finalize_sbom::test_finalize_sbom_adds_project_root_and_dependency_edge`, `test_finalize_sbom::test_completed_sbom_validation_fails_without_repair`, `test_finalize_sbom::test_completed_sbom_validation_rejects_non_utf8_bytes`, `test_release_workflow::test_pypi_publication_waits_for_valid_sbom`, `test_release_workflow_contract::TestGithubReleaseAttachesBothArtifacts` | +| CycloneDX generation rejects every nonzero audit status; output has the exact project root and complete dependency edge; malformed or non-UTF-8 completed evidence is rejected without repair; and any SBOM failure blocks publication | `scripts/finalize_sbom.py`; `test_release_workflow_contract::TestSbomJobIsIsolated::test_sbom_job_validates_complete_project_bom`, `test_finalize_sbom::test_finalize_sbom_adds_project_root_and_dependency_edge`, `test_finalize_sbom::test_completed_sbom_validation_fails_without_repair`, `test_finalize_sbom::test_completed_sbom_validation_rejects_non_utf8_bytes`, `test_release_workflow::test_pypi_publication_waits_for_valid_sbom` | | Signed, traceable releases use PyPI Trusted Publishing and pinned PEP 740 verification plus GitHub verification bound to the downloaded bundle, exact release workflow, source tag, commit digest, and hosted-runner boundary. The current workflow covers the wheel, sdist, and completed SBOM; the exact v2.6.3 historical exception covers its wheel and sdist while completed SBOM structure remains mandatory | `release.yml` (attestations and trusted publishing); `test_release_readiness::test_github_attestations_verify_wheel_sdist_and_sbom`; `test_release_readiness::test_github_attestations_preserve_exact_v263_legacy_subject_boundary`; `test_release_workflow_contract::TestAttestationJob` | | Existing GitHub Release recovery validates exact identity, mutable publication state, expected-only inventory, complete or partial, and a remote tag still bound to the workflow SHA before any asset replacement | `scripts/check_release_recovery.py`; `test_release_recovery::test_unsafe_existing_release_state_is_rejected`; `test_release_workflow_contract::TestGithubReleaseAttachesBothArtifacts` | diff --git a/scripts/run_dependency_audit.py b/scripts/run_dependency_audit.py new file mode 100644 index 00000000..a3a83b11 --- /dev/null +++ b/scripts/run_dependency_audit.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python3 +"""Run pip-audit with one fail-closed retry for known transport faults.""" + +from __future__ import annotations + +import re +import subprocess +import sys +import time +from collections.abc import Callable, Sequence + +_MAX_ATTEMPTS = 2 +_RETRY_DELAY_SECONDS = 3.0 +_VULNERABILITY_SUMMARY = re.compile(r"\bFound \d+ known vulnerabilit(?:y|ies)\b") +_TRANSPORT_FAILURE_MARKERS = ( + "requests.exceptions.ConnectionError", + "requests.exceptions.ConnectTimeout", + "requests.exceptions.ReadTimeout", + "urllib3.exceptions.ProtocolError", + "ConnectionAbortedError", + "ConnectionResetError", + "Could not connect to PyPI's vulnerability feed", + "PyPI is not redirecting properly", +) + +AuditResult = subprocess.CompletedProcess[str] +AuditRunner = Callable[[Sequence[str]], AuditResult] +Sleeper = Callable[[float], None] + + +def _run_once(arguments: Sequence[str]) -> AuditResult: + return subprocess.run( # noqa: S603 - fixed interpreter/module, argv list, no shell. + [sys.executable, "-I", "-m", "pip_audit", *arguments], + check=False, + capture_output=True, + text=True, + encoding="utf-8", + errors="replace", + ) + + +def _replay(result: AuditResult) -> None: + if result.stdout: + sys.stdout.write(result.stdout) + sys.stdout.flush() + if result.stderr: + sys.stderr.write(result.stderr) + sys.stderr.flush() + + +def _is_retryable_transport_failure(result: AuditResult) -> bool: + if result.returncode == 0: + return False + output = f"{result.stdout}\n{result.stderr}" + if _VULNERABILITY_SUMMARY.search(output): + return False + return any(marker in output for marker in _TRANSPORT_FAILURE_MARKERS) + + +def run( + arguments: Sequence[str], + *, + runner: AuditRunner = _run_once, + sleeper: Sleeper = time.sleep, +) -> int: + for attempt in range(1, _MAX_ATTEMPTS + 1): + result = runner(arguments) + _replay(result) + retryable = _is_retryable_transport_failure(result) + if result.returncode == 0: + return 0 + if retryable and attempt < _MAX_ATTEMPTS: + print( + "dependency audit: recognized transport failure; retrying once in " + f"{_RETRY_DELAY_SECONDS:g} seconds", + file=sys.stderr, + flush=True, + ) + sleeper(_RETRY_DELAY_SECONDS) + continue + if retryable: + print( + "dependency audit: transport failure persisted after one retry; audit remains failed", + file=sys.stderr, + flush=True, + ) + return result.returncode + raise AssertionError("bounded dependency-audit attempts were exhausted") + + +def main(argv: Sequence[str] | None = None) -> int: + return run(sys.argv[1:] if argv is None else argv) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/test_documentation_integrity.py b/tests/test_documentation_integrity.py index d9fbe92a..c220888e 100644 --- a/tests/test_documentation_integrity.py +++ b/tests/test_documentation_integrity.py @@ -139,6 +139,10 @@ def test_security_and_maintainer_docs_name_current_runtime_surfaces() -> None: ): assert receipt in resolutions + assert "Closed in v2.6.4: unresolved HTTP destinations and MCP diagnostic side effects" in resolutions + assert "| **Fully closed** | **v2.6.4** |" in resolutions + assert "| **Fully closed** | **Unreleased** |" not in resolutions + def test_traceability_records_current_mutation_gate() -> None: traceability = _read("docs/traceability-matrix.md") diff --git a/tests/test_release_workflow.py b/tests/test_release_workflow.py index bd72cc5e..e00294e9 100644 --- a/tests/test_release_workflow.py +++ b/tests/test_release_workflow.py @@ -85,7 +85,8 @@ def test_release_test_job_runs_complete_quality_gate() -> None: assert "--text-range" in step_text assert "git describe --tags" in step_text assert "scripts/check_mcp_compatibility.py" in step_text - assert "pip-audit" in step_text + assert "scripts/run_dependency_audit.py" in step_text + assert "uv run pip-audit" not in step_text def test_pypi_publication_waits_for_valid_sbom() -> None: diff --git a/tests/test_release_workflow_contract.py b/tests/test_release_workflow_contract.py index 06321037..c8c45388 100644 --- a/tests/test_release_workflow_contract.py +++ b/tests/test_release_workflow_contract.py @@ -283,6 +283,8 @@ def test_sbom_job_validates_complete_project_bom(self, workflow): assert "scripts/finalize_sbom.py" in text assert "test -s" in text assert "audit_status" in text + assert 'if [ "$audit_status" -ne 0 ]; then' in text + assert '"$audit_status" -gt 1' not in text assert "|| true" not in text def test_sbom_job_seals_only_the_exact_expected_regular_file(self, workflow): diff --git a/tests/test_run_dependency_audit.py b/tests/test_run_dependency_audit.py new file mode 100644 index 00000000..68388b62 --- /dev/null +++ b/tests/test_run_dependency_audit.py @@ -0,0 +1,169 @@ +"""Fail-closed retry semantics for the dependency-audit runner.""" + +from __future__ import annotations + +import subprocess +from collections.abc import Sequence +from pathlib import Path + +import pytest +import yaml + +from scripts import run_dependency_audit + +ROOT = Path(__file__).resolve().parents[1] + + +def _result(returncode: int, *, stdout: str = "", stderr: str = "") -> subprocess.CompletedProcess[str]: + return subprocess.CompletedProcess(["pip-audit"], returncode, stdout, stderr) + + +class _Results: + def __init__(self, *results: subprocess.CompletedProcess[str]) -> None: + self._results = list(results) + self.calls: list[tuple[str, ...]] = [] + + def __call__(self, arguments: Sequence[str]) -> subprocess.CompletedProcess[str]: + self.calls.append(tuple(arguments)) + return self._results.pop(0) + + +def test_success_returns_without_retry(capsys: pytest.CaptureFixture[str]) -> None: + runner = _Results(_result(0, stdout="No known vulnerabilities found\n")) + sleeps: list[float] = [] + + assert run_dependency_audit.run(["-r", "locked.txt"], runner=runner, sleeper=sleeps.append) == 0 + assert runner.calls == [("-r", "locked.txt")] + assert sleeps == [] + assert capsys.readouterr().out == "No known vulnerabilities found\n" + + +def test_vulnerability_result_is_never_retried(capsys: pytest.CaptureFixture[str]) -> None: + runner = _Results( + _result( + 1, + stdout="Found 1 known vulnerability in 1 package\n", + stderr="requests.exceptions.ConnectionError appears in unrelated diagnostic context\n", + ) + ) + sleeps: list[float] = [] + + assert run_dependency_audit.run(["-r", "locked.txt"], runner=runner, sleeper=sleeps.append) == 1 + assert len(runner.calls) == 1 + assert sleeps == [] + assert "retrying" not in capsys.readouterr().err + + +def test_recognized_transport_failure_retries_once(capsys: pytest.CaptureFixture[str]) -> None: + runner = _Results( + _result(1, stderr="requests.exceptions.ConnectionError: connection reset\n"), + _result(0, stdout="No known vulnerabilities found\n"), + ) + sleeps: list[float] = [] + + assert run_dependency_audit.run(["-r", "locked.txt"], runner=runner, sleeper=sleeps.append) == 0 + assert runner.calls == [("-r", "locked.txt"), ("-r", "locked.txt")] + assert sleeps == [3.0] + captured = capsys.readouterr() + assert "recognized transport failure; retrying once in 3 seconds" in captured.err + assert "No known vulnerabilities found" in captured.out + + +def test_transport_failure_exhaustion_preserves_failure(capsys: pytest.CaptureFixture[str]) -> None: + runner = _Results( + _result(1, stderr="urllib3.exceptions.ProtocolError: reset\n"), + _result(2, stderr="ConnectionResetError: reset again\n"), + ) + sleeps: list[float] = [] + + assert run_dependency_audit.run([], runner=runner, sleeper=sleeps.append) == 2 + assert len(runner.calls) == 2 + assert sleeps == [3.0] + assert "transport failure persisted after one retry; audit remains failed" in capsys.readouterr().err + + +@pytest.mark.parametrize( + ("final_result", "expected_status"), + [ + (_result(1, stdout="Found 1 known vulnerability in 1 package\n"), 1), + (_result(2, stderr="pip-audit: error: malformed response\n"), 2), + ], +) +def test_transport_retry_preserves_later_non_transport_failure( + final_result: subprocess.CompletedProcess[str], + expected_status: int, +) -> None: + runner = _Results( + _result(1, stderr="ConnectionResetError: initial reset\n"), + final_result, + ) + sleeps: list[float] = [] + + assert run_dependency_audit.run([], runner=runner, sleeper=sleeps.append) == expected_status + assert len(runner.calls) == 2 + assert sleeps == [3.0] + + +def test_unrecognized_failure_is_not_retried(capsys: pytest.CaptureFixture[str]) -> None: + runner = _Results(_result(2, stderr="pip-audit: error: malformed requirements file\n")) + sleeps: list[float] = [] + + assert run_dependency_audit.run([], runner=runner, sleeper=sleeps.append) == 2 + assert len(runner.calls) == 1 + assert sleeps == [] + assert "retrying" not in capsys.readouterr().err + + +@pytest.mark.parametrize("collision_shape", ["module", "package"]) +def test_pip_audit_module_resolution_ignores_checkout_shadow( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, + collision_shape: str, +) -> None: + marker = "SHADOWED_PIP_AUDIT_MODULE" + if collision_shape == "module": + (tmp_path / "pip_audit.py").write_text(f'print("{marker}")\n', encoding="utf-8") + else: + package = tmp_path / "pip_audit" + package.mkdir() + (package / "__init__.py").write_text("", encoding="utf-8") + (package / "__main__.py").write_text(f'print("{marker}")\n', encoding="utf-8") + monkeypatch.chdir(tmp_path) + + result = run_dependency_audit._run_once(["--version"]) + + output = f"{result.stdout}\n{result.stderr}" + assert result.returncode == 0 + assert marker not in output + assert "pip-audit" in output + + +def test_enforcing_workflows_use_bounded_audit_runner() -> None: + expected = "uv run python scripts/run_dependency_audit.py -r .ci-audit-requirements.txt" + for relative, job_name in ( + (".github/workflows/ci.yml", "audit"), + (".github/workflows/release.yml", "test"), + ): + workflow = yaml.safe_load((ROOT / relative).read_text(encoding="utf-8")) + steps = workflow["jobs"][job_name]["steps"] + audit_step = next(step for step in steps if step.get("name") == "Audit dependencies") + assert audit_step["run"] == expected + assert "continue-on-error" not in audit_step + assert "uv run pip-audit" not in "\n".join(str(step.get("run", "")) for step in steps) + + +def test_dependency_audit_guidance_matches_fail_closed_workflows() -> None: + guidance = { + relative: (ROOT / relative).read_text(encoding="utf-8") + for relative in ( + "docs/release-process.md", + "docs/security.md", + "docs/supply-chain.md", + ) + } + + normalized = {relative: " ".join(text.split()) for relative, text in guidance.items()} + assert all("Python isolated mode" in text for text in normalized.values()) + assert all("every nonzero audit status" in text.casefold() for text in normalized.values()) + assert "Findings may coexist with this artifact" not in guidance["docs/release-process.md"] + assert "may emit an artifact when findings exist" not in guidance["docs/supply-chain.md"] diff --git a/tests/test_scorecard_posture.py b/tests/test_scorecard_posture.py index 35cd3043..d3b1b327 100644 --- a/tests/test_scorecard_posture.py +++ b/tests/test_scorecard_posture.py @@ -167,7 +167,7 @@ def test_release_workflow_exports_scorecard_recognized_provenance() -> None: assert "provenance/*" in release_text -def test_codeql_workflow_is_pr_scoped_scheduled_and_least_privilege() -> None: +def test_codeql_workflow_covers_main_prs_pushes_and_schedule_with_least_privilege() -> None: workflow = _load_yaml(".github/workflows/codeql.yml") triggers = _workflow_on(workflow) job = workflow["jobs"]["analyze"] @@ -176,7 +176,7 @@ def test_codeql_workflow_is_pr_scoped_scheduled_and_least_privilege() -> None: assert "schedule" in triggers assert "workflow_dispatch" in triggers assert triggers["pull_request"] == {"branches": ["main"]} - assert "push" not in triggers + assert triggers["push"] == {"branches": ["main"]} assert workflow["permissions"] == _READ_ONLY_PERMISSIONS assert job["permissions"] == _ALLOWED_ELEVATED_JOB_PERMISSIONS[".github/workflows/codeql.yml"]["analyze"] assert "github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e" in step_text @@ -330,14 +330,15 @@ def test_openssf_posture_docs_track_real_scorecard_limits() -> None: for required in ( "2026-07-18", "Score: `8.2`", - "public API rechecked for the exact `HEAD` commit that published v2.6.4", + "public API rechecked for the exact current release `HEAD`", "live API URL", "Remote release readiness queries that API for `HEAD`", "overall score of at least `8.0`", "requires the documented SAST floor of `7`", - "all 17 sampled merged pull requests ran CI", - "same 17 pull-request heads predate PR-scoped CodeQL", - "New pull requests targeting `main` now run CodeQL", + "every sampled merged pull request ran CI", + "sample still includes merged pull-request heads from before PR-scoped CodeQL was required", + "Current pull requests provide supported SAST evidence", + "pushes to `main` run CodeQL", "public API reports successful supported SAST checks for every merged pull request", "OpenSSF Best Practices Badge is claimed", "openssf-badge-readiness.md", @@ -348,23 +349,29 @@ def test_openssf_posture_docs_track_real_scorecard_limits() -> None: "CII-Best-Practices", "Contributors", "Do not manufacture review history", + "automated review comments are not approvals", "bestpractices.dev", "github.com/ossf/scorecard", "about-code-owners", ): assert required in text + assert "v2.6.4" not in text + assert "17 sampled" not in text + def test_supply_chain_docs_name_current_scorecard_recheck() -> None: text = " ".join((_ROOT / "docs" / "supply-chain.md").read_text(encoding="utf-8").split()) for required in ( - "2026-07-18 Scorecard recheck for the exact v2.6.4 `HEAD` commit reports score `8.2`", + "2026-07-18 Scorecard recheck for the exact current-release `HEAD` reports score `8.2`", "non-SAST measured code-owned controls are at `10`; SAST is `7`", - "all 17 sampled merged pull requests predate PR-scoped CodeQL", + "sampled window still includes merged pull-request heads from before PR-scoped CodeQL was required", "overall score of at least `8.0`", "enforces the current SAST floor of `7`", "public API reports successful supported SAST checks for every merged pull request", + "Current pull requests and pushes to `main` run CodeQL", + "retry exactly once only when their output matches a narrow set of recognized transport failures", "requires the CI matrix, gitleaks, and CodeQL checks", "June 28 review found one code-owned gap", "remaining Scorecard limits are intentional or process-bound", @@ -377,6 +384,9 @@ def test_supply_chain_docs_name_current_scorecard_recheck() -> None: ): assert required in text + assert "exact v2.6.4 `HEAD`" not in text + assert "17 sampled" not in text + def test_openssf_docs_do_not_turn_live_api_state_into_commit_promise() -> None: openssf = (_ROOT / "docs" / "openssf-posture.md").read_text(encoding="utf-8")