v2.1.13: auditable assurance docs + closed proving-test gaps #172
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Release | |
| on: | |
| push: | |
| tags: | |
| - "v*" | |
| # Workflow-level default: read-only. Each job explicitly opts into any | |
| # elevated scope it actually needs. The prior ``id-token: write`` here | |
| # granted OIDC-token minting to every job — including ``test`` and | |
| # ``build`` which install and execute dependency code. A compromised | |
| # dependency could mint a token and publish to PyPI under our trusted- | |
| # publisher identity. Scoping to ``publish-pypi`` / ``github-release`` | |
| # respects least-privilege. | |
| # | |
| # Supply-chain isolation contract (v1.9.3.3+): | |
| # | |
| # * ``test`` job installs dev deps and runs the full gate | |
| # (pytest, ruff, pip-audit). Dev deps execute here. No dist/ | |
| # exists, so a compromised dev dep cannot tamper with release | |
| # artifacts from this job. | |
| # * ``build`` job is PURE: ``uv build`` runs against the locked | |
| # runtime dependency set (``--no-dev``, no dev group); ``dist/`` is | |
| # created and immediately uploaded as an artifact. No additional | |
| # code executes between build and upload. ``actions/upload-artifact`` | |
| # seals the bytes into GitHub-managed storage at upload time. | |
| # * ``sbom`` job runs on a separate runner. It installs pip-audit | |
| # in its own workspace and generates the CycloneDX SBOM from the | |
| # locked-requirements text. dist/ does not exist in this runner, | |
| # so even a compromised pip-audit cannot reach release artifacts. | |
| # * ``publish-pypi`` and ``github-release`` download the sealed | |
| # artifacts from GitHub storage. They never re-build, never | |
| # re-install dev deps, never execute additional dependency code. | |
| # | |
| # The threat the contract addresses: if a transitive dev dependency | |
| # of pip-audit (or pip-audit itself) is compromised at the locked | |
| # version, executing it cannot modify dist/ because dist/ is not | |
| # on the same runner. This closes the v1.9.3.1 audit finding | |
| # "Dev deps can tamper release artifacts" (HIGH). | |
| permissions: | |
| contents: read | |
| # `.python-version` pins the local / dev toolchain to 3.14, and uv reads it | |
| # unless UV_PYTHON is set. Every job here installs Python 3.11 (the supported | |
| # floor) via setup-python, so pin uv to the same interpreter workflow-wide; | |
| # otherwise uv sync / export / pip-audit would resolve and audit against 3.14, | |
| # not the released runtime floor. | |
| env: | |
| UV_PYTHON: "3.11" | |
| jobs: | |
| test: | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Install uv | |
| uses: astral-sh/setup-uv@v8.1.0 | |
| - name: Set up Python | |
| uses: actions/setup-python@v6 | |
| with: | |
| python-version: "3.11" | |
| - name: Install dependencies | |
| run: uv sync | |
| - name: Run tests | |
| run: uv run pytest tests/ -q --cov=recon_tool --cov-branch --cov-fail-under=82 | |
| - name: Lint | |
| run: uv run ruff check recon_tool/ | |
| - name: Validate built-in fingerprints | |
| # Mirrors ci.yml so a YAML schema or duplicate-slug regression | |
| # cannot reach PyPI just because release.yml's test job missed it. | |
| # This check pools slugs across files and rejects same-slug entries | |
| # whose display names disagree. | |
| run: uv run python scripts/validate_fingerprint.py recon_tool/data/fingerprints/ --quiet | |
| - name: Enforce fingerprint metadata coverage (v1.9.7+) | |
| # Mirrors ci.yml. Presence gate: every detection in every | |
| # category must carry a non-empty ``description``. Prevents | |
| # a catalog regression from reaching PyPI just because the | |
| # tests passed. | |
| run: uv run python scripts/check_metadata_coverage.py | |
| - name: Export runtime requirements | |
| # Hash-pinned (no --no-hashes): the exported requirements carry | |
| # per-package sha256 hashes, so the audit reads a hash-pinned | |
| # surface. pip-audit handles hashed requirements. | |
| run: uv export --frozen --no-dev --no-emit-project --format requirements.txt --output-file .ci-audit-requirements.txt | |
| - name: Audit dependencies | |
| # Mirrors ci.yml. PYSEC-2025-183 (CVE-2025-45768) is a disputed, | |
| # no-fix advisory in pyjwt, pulled transitively by mcp[crypto] for | |
| # MCP's HTTP/OAuth transport, which recon's stdio-only MCP server | |
| # never invokes. This ignore is kept in lockstep with ci.yml so a | |
| # tagged release is not blocked by an advisory CI already accepts. | |
| # See docs/security-audit-resolutions.md; drop when a fixed pyjwt ships. | |
| run: uv run pip-audit -r .ci-audit-requirements.txt --ignore-vuln PYSEC-2025-183 | |
| build: | |
| # Pure build job. The supply-chain isolation contract requires | |
| # that NO additional dependency code executes in this workspace | |
| # between ``uv build`` and ``actions/upload-artifact``. Do not | |
| # add ``uv sync`` here. Do not run pip-audit, SBOM | |
| # generation, ruff, pyright, pytest, or any other tool that | |
| # imports project dependencies in this job. Those belong in | |
| # other jobs that do not touch dist/. | |
| needs: test | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Install uv | |
| uses: astral-sh/setup-uv@v8.1.0 | |
| - name: Set up Python | |
| uses: actions/setup-python@v6 | |
| with: | |
| python-version: "3.11" | |
| - name: Sync runtime deps (no dev group) | |
| # ``uv build`` resolves the build backend (hatchling) from | |
| # pyproject; this sync ensures we have a clean uv cache state | |
| # without installing the optional dev set. Dev tooling | |
| # (pip-audit, pytest, ruff, pyright) is deliberately absent | |
| # from this workspace. | |
| run: uv sync --no-dev | |
| - name: Pin reproducible build timestamp | |
| # Reproducible builds: pin file mtimes in the wheel/sdist to the tagged | |
| # commit's committer timestamp so the same source yields byte-identical | |
| # artifacts a consumer can rebuild and verify (the property is gated on | |
| # every change by ci.yml's reproducible-build job). hatchling and uv | |
| # build honor SOURCE_DATE_EPOCH. checkout is shallow, but the tagged | |
| # commit is present, so ``git log -1`` reads its timestamp. This is a git | |
| # read, not project dependency code, so the supply-chain isolation | |
| # contract (no dependency code between build and seal) is unaffected. | |
| run: echo "SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct)" >> "$GITHUB_ENV" | |
| - name: Build package | |
| run: uv build | |
| # IMMEDIATE upload — nothing executes between build and seal. | |
| # The artifact bytes are stored in GitHub-managed artifact | |
| # storage at this point; downstream jobs read from there, not | |
| # from this runner's filesystem. | |
| - name: Upload build artifacts | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: dist | |
| path: dist/ | |
| attest: | |
| # SLSA build-provenance attestation. Kept in its own job, like | |
| # publish-pypi and github-release, so the supply-chain isolation | |
| # contract holds: this job downloads the sealed artifacts and runs | |
| # NO project dependency code, so the id-token it is granted cannot be | |
| # minted by a compromised dependency. It signs a provenance | |
| # attestation (GitHub-native, OIDC) linking the wheel and sdist to | |
| # this workflow run; verify later with `gh attestation verify`. | |
| # Reproducible builds (SOURCE_DATE_EPOCH, gated in ci.yml) and | |
| # sigstore-signed PyPI attestations (PEP 740, in the publish-pypi job) | |
| # now ship alongside this; only the full SLSA L3 generator workflow | |
| # stays deferred as disproportionate (see docs/supply-chain.md). | |
| needs: build | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| id-token: write | |
| attestations: write | |
| steps: | |
| - name: Download build artifacts | |
| uses: actions/download-artifact@v8 | |
| with: | |
| name: dist | |
| path: dist/ | |
| - name: Attest build provenance | |
| uses: actions/attest-build-provenance@v4 | |
| with: | |
| subject-path: "dist/*" | |
| sbom: | |
| # Separate runner. dist/ does not exist in this workspace. A | |
| # compromised pip-audit (or any transitive) cannot tamper with | |
| # the release artifacts because the artifacts are sealed in | |
| # GitHub storage and not present on this filesystem. | |
| needs: test | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Install uv | |
| uses: astral-sh/setup-uv@v8.1.0 | |
| - name: Set up Python | |
| uses: actions/setup-python@v6 | |
| with: | |
| python-version: "3.11" | |
| - name: Export runtime requirements | |
| # Re-export so the SBOM job is self-contained — it does not | |
| # consume any artifact from build/test. Same lock, same | |
| # resolved set, derivable independently. | |
| run: uv export --frozen --no-dev --no-emit-project --no-hashes --format requirements.txt --output-file .ci-audit-requirements.txt | |
| - name: Install pip-audit (isolated) | |
| # pipx-style isolation: pip-audit lives only in a uv-managed | |
| # virtualenv on this throwaway runner. It never sees dist/ | |
| # because dist/ is on the ``build`` job's filesystem (sealed | |
| # into GitHub storage at upload time and not retrieved here). | |
| run: uv tool install pip-audit | |
| - name: Generate CycloneDX SBOM | |
| run: | | |
| set -euo pipefail | |
| VERSION="${GITHUB_REF_NAME#v}" | |
| 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. The "|| true" catches the audit's | |
| # vulnerability-finding exit code; the SBOM file is written | |
| # regardless. The audit *gate* runs in the ``test`` job. | |
| uv tool run pip-audit -r .ci-audit-requirements.txt \ | |
| --format=cyclonedx-json \ | |
| --output "sbom/recon-tool-${VERSION}.cdx.json" \ | |
| --no-deps || true | |
| test -f "sbom/recon-tool-${VERSION}.cdx.json" | |
| ls -la sbom/ | |
| - name: Upload SBOM artifact | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: sbom | |
| path: sbom/ | |
| publish-pypi: | |
| needs: build | |
| runs-on: ubuntu-latest | |
| environment: pypi | |
| permissions: | |
| id-token: write | |
| steps: | |
| - name: Download build artifacts | |
| uses: actions/download-artifact@v8 | |
| with: | |
| name: dist | |
| path: dist/ | |
| - name: Publish to PyPI | |
| uses: pypa/gh-action-pypi-publish@release/v1 | |
| with: | |
| skip-existing: true | |
| # PEP 740: generate sigstore-signed digital attestations for the wheel | |
| # and sdist and upload them to PyPI, so installers and auditors can | |
| # verify the artifacts' provenance directly from the index. Uses the | |
| # OIDC id-token granted to this job; complements the GitHub-native | |
| # build-provenance attestation produced by the ``attest`` job. | |
| attestations: true | |
| github-release: | |
| needs: [build, sbom] | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Extract changelog | |
| id: changelog | |
| run: | | |
| VERSION="${GITHUB_REF_NAME#v}" | |
| echo "version=$VERSION" >> "$GITHUB_OUTPUT" | |
| awk "/^## \[${VERSION}\]/{found=1; next} /^## \[/{if(found) exit} found{print}" CHANGELOG.md > release_notes.md | |
| cat release_notes.md | |
| - name: Download build artifacts | |
| uses: actions/download-artifact@v8 | |
| with: | |
| name: dist | |
| path: dist/ | |
| - name: Download SBOM artifact | |
| uses: actions/download-artifact@v8 | |
| with: | |
| name: sbom | |
| path: sbom/ | |
| - name: Create GitHub Release | |
| uses: softprops/action-gh-release@v3 | |
| with: | |
| name: "v${{ steps.changelog.outputs.version }}" | |
| body_path: release_notes.md | |
| draft: false | |
| prerelease: false | |
| # Attach wheels, sdist, and the CycloneDX SBOM. Consumers | |
| # pull the SBOM by stable name (recon-tool-<version>.cdx.json) | |
| # for downstream supply-chain audits. | |
| files: | | |
| dist/* | |
| sbom/* |