PR dependency integrity
Issue #334 adds three complementary checks to pull requests:
- CI orchestration and tests use
uv sync --locked --no-install-project --extra dev --extra ci. A staleuv.lockfails instead of being rewritten. The scripts-only project itself is not installed, avoiding an independently resolved setuptools build environment. Matrix-specific requirements are checked against installed versions instead of installed again. - The full-lock audit queries PyPI advisories for every registry package/version
in
uv.lock, including alternative Python/platform resolutions. All known vulnerabilities are blocking regardless of severity. Auditing uses exact pins and disables pip resolution; packages are not installed by the scanner. - GitHub Dependency Review blocks newly introduced vulnerabilities at severity
lowor higher, across all dependency scopes. It uses read-only permissions and does not post PR comments. It is independent of the full-lock audit.
The standalone market-calendar and nightly packaged-dependency smoke tests still
install their own requirements.txt into fresh environments. They intentionally
test the published standalone dependency ranges, not the root project’s locked
environment. Their install results are compatibility evidence, not lockfile
reproducibility evidence. Release checksums and SBOMs remain outside this change.
Audit and exceptions
Run from the repository root with the locked Python 3.11 environment:
uv sync --locked --no-install-project --extra dev --extra ci --python 3.11
uv run --no-sync python scripts/check_supply_chain.py check
uv run --no-sync python scripts/check_supply_chain.py audit --report supply-chain-audit.json
check is offline. audit calls the advisory service but needs no credentials.
The audit covers each marker alternative by splitting packages into batches
with at most one version per normalized name. It verifies the exact report
inventory; skipped packages, missing reports and scanner/service errors fail.
The JSON report is uploaded by CI even when the audit fails.
config/security-exceptions.json currently contains no exceptions. Issue #387 retired the seven
Python 3.9 lock exceptions by moving the shared root environment to Python 3.10 and regenerating
the lock with patched dependency versions. Standalone skill probes may still run on Python 3.9, but
they do not consume uv.lock or install the root project.
A vulnerability exception must identify an exact package, version and advisory, plus its owner, reason and expiry. Expiry is evaluated against the UTC date; an exception expires at the start of its expiry date. Malformed, duplicate, unknown or expired entries fail the offline check. An advisory alias is accepted only for the same exact package and version.
Approaching-expiry diagnostics
Both check and audit evaluate all exception entries against one captured UTC
date. Starting 14 days before expiry, each affected entry is printed to
stderr with its package/version, advisory, owner, UTC expiry date and remaining
days. The inclusive thresholds are:
| Days remaining | Status | Policy behavior |
|---|---|---|
| More than 14 | active |
No expiry warning |
| 8 through 14 | warning |
Warn; exception remains valid |
| 1 through 7 | urgent |
Warn; exception remains valid |
| 0 or fewer | expired |
Fail with exit 1; do not run the advisory scanner |
For an expiry date of October 6, warning begins September 22, urgency begins September 29, and the exception becomes invalid at October 6 00:00 UTC. Expiry status describes the exception deadline, not vulnerability severity or whether a dependency is safe.
When audit runs, its JSON report includes exception_expiry with
schema_version: 1, evaluated_on (UTC date), the most urgent overall status,
and an exceptions array. Each row contains package, version, advisory,
owner, expires_on, days_remaining and status. Rows sort by expiry date,
normalized package name, exact version and advisory; separate advisories for the
same package/version remain separate rows. An empty policy produces an empty
array and active status. This block is present on successful scans, blocked
vulnerability results and scanner errors. Inspect errors and the command exit
code for the actual audit result, and inspect blocked when that field is
present. Policy-validation and scanner failures can omit blocked; its absence
means vulnerability evaluation did not complete, not that no vulnerabilities
were blocked. Consumers must not assume every report contains blocked or
inventory.
A direct audit invocation with an expired policy writes a failure report with
exception_expiry and errors, exits 1, and performs no scanner calls. Invalid
policy structure also produces an error report, without partial expiry entries.
The output replaces any previous report at --report; a report-write failure
is reported on stderr and returns exit 1.
Policy structure and duplicate identities are validated before expiration. If a policy is both malformed or duplicated and expired, the structural/duplicate error is reported first. The rejection behavior and exit code remain unchanged; callers should not depend on which diagnostic appears first for a policy with multiple defects.
In CI, check runs before audit. Approaching-expiry reports are included in
the existing audit artifact when the audit step runs. On or after expiry,
check fails first and the audit step is skipped: the CI log contains the
expiry diagnostics, but no new audit JSON artifact is guaranteed in that case.
These are per-invocation diagnostics; they do not add a scheduled notification.
The approaching-expiry reporting was introduced before the #387 remediation and remains active for any future exact exception. The remediation did not extend a deadline or weaken the policy.
These exceptions can document existing debt for the full-lock audit. They never
populate GitHub’s global allow-ghsas setting: a PR introducing a vulnerability
remains blocked by Dependency Review even if a full-lock exception exists.
Prefer upgrading a vulnerable dependency over adding an exception. Changes to
exception policy are ordinary reviewed source changes, not automatic approvals.
No new license-denial policy is imposed here. Dependency Review’s license check is disabled, and license exceptions are unsupported. A passing result therefore does not assert license compliance.
Actions and updates
Remote Actions and reusable workflows use full commit SHAs, with version
comments for readability. The offline checker also inspects local action
definitions. Docker actions must use a SHA-256 digest. Workflow tokens default
to contents: read; this change does not alter branch-protection settings.
Weekly Dependabot updates cover the root uv project, GitHub Actions and
docs/Gemfile (Bundler). Dependency updates are proposed as PRs, never auto-merged.
The setup-uv Action and the uv executable version are both fixed explicitly.