Skip to content

v2.1.13: auditable assurance docs + closed proving-test gaps #172

v2.1.13: auditable assurance docs + closed proving-test gaps

v2.1.13: auditable assurance docs + closed proving-test gaps #172

Workflow file for this run

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/*