| name | python-supply-chain |
| description | Hardens a Python repository's supply chain — Dependabot update automation, pip-audit vulnerability scanning, secret scanning and push protection, CodeQL, OpenSSF Scorecard, SBOMs, CODEOWNERS. Use for 'set up dependabot', 'audit dependencies', 'enable secret scanning', 'harden this repo'. Not for CI quality gates, workflow hardening, or PyPI publishing. |
| license | MIT |
python-supply-chain
Installs layered supply-chain controls into a Python package repository on GitHub:
dependency-update automation with a freshness delay, vulnerability and secret
scanning, code scanning, ownership rules for the paths that define automation, and
SBOM/provenance artifacts. The design premise, taken from the 2025–2026 incident wave
(the Shai-Hulud npm worm, the malicious axios release spread by dependency bots,
mutable-tag action compromises), is that the attack surface is now your automation
and the first hours after a release — so no single control below is sufficient, and
bot output is treated with the same suspicion as human PRs.
When NOT to use
- CI quality-gate workflows, branch rulesets, action SHA pinning, zizmor — the
python-ci skill, if installed, owns those. This skill only adds scanning and
update workflows, and notes where a control depends on a ruleset.
- PyPI trusted publishing (OIDC) and the tag-triggered publish workflow — the
python-release skill. This skill stops at repo-side SBOM/provenance; the publish
step that uploads to PyPI belongs there.
- Pre-commit hook wiring (including a local gitleaks hook) — the
python-precommit skill. Local hooks are advisory; this skill installs the
enforcing server/CI layer.
- Packaging and lockfile setup (
uv init, pyproject.toml) — python-packaging.
- Agent hooks / AGENTS.md rules — agent-guardrails. This skill only protects
agent-config paths via CODEOWNERS.
- Active incident response beyond credential rotation basics — escalate to a
human; do not attempt autonomous cleanup of a live compromise.
Workflow
Work through the steps in order; each is independently valuable, so stop where the
user's scope ends. Steps that change GitHub settings need gh authenticated with
admin rights on the repo — otherwise print the commands for the user to run.
1. Audit the current posture
Run the bundled read-only checker from the repo root before adding anything:
python3 "${CLAUDE_SKILL_DIR}/scripts/check_supply_chain.py" --root .
python3 "${CLAUDE_SKILL_DIR}/scripts/check_supply_chain.py" --root . --github
(Invoke it from this skill's folder with an absolute path if cwd differs.) It prints
one PASS|WARN|FAIL|NOTE check: detail line per control and exits non-zero on FAIL.
Fix FAILs in the steps below; treat WARNs as the backlog. Never duplicate a control
that already exists — tune it instead.
2. Baseline: a committed lockfile
Every control below assumes the dependency set is pinned and committed: uv.lock
(preferred), poetry.lock/pdm.lock, or hash-pinned requirements
(--generate-hashes with pip-tools). If there is no lockfile, that is a
python-packaging problem — flag it and stop; auditing an unpinned dependency set
audits a guess.
3. Dependency-update automation (Dependabot)
Write .github/dependabot.yml:
version: 2
updates:
- package-ecosystem: "uv"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 10
cooldown:
default-days: 7
groups:
python-minor-patch:
update-types: ["minor", "patch"]
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
cooldown:
default-days: 7
Key decisions, with detail in references/dependabot.md:
- Cooldown is the supply-chain control, not the schedule: it delays adoption of
freshly published versions past the window in which malicious releases are usually
caught, and it is auto-bypassed for security-advisory updates — the reason to
prefer it over a package-manager-level freshness pin alone.
- Grouping minor+patch is the accepted fix for PR-noise complaints. Keep majors
ungrouped so breaking changes get individual review.
- Never blanket auto-merge bot PRs. In the April 2026 malicious-
axios incident
bots opened 154 of 214 update PRs and ~60% of bot PRs merged without human review.
Require CI plus a human review on Dependabot PRs like any other PR.
- Renovate is the alternative when the user needs config-as-code flexibility;
its cooldown equivalent is
minimumReleaseAge. Trade-offs and its pinDigest
footgun are in the reference. Pick one bot — running both doubles the noise.
- Dependabot's pip/uv cooldown paths are younger than its npm/actions ones; after
enabling, confirm update PRs actually arrive on the next scheduled run.
4. Vulnerability audit of the lockfile (pip-audit)
Audit the project's pinned set, exported from the lockfile:
uv export --format requirements-txt --no-emit-project -o /tmp/audit-req.txt
uvx --from pip-audit==2.7.3 pip-audit -r /tmp/audit-req.txt --no-deps
(==2.7.3 is an example pin — check PyPI for the current release and pin that.
Unpinned uvx executes whatever was published minutes ago: the exact attack this
skill exists to prevent.) Plain-pip fallback, run inside the project environment:
python -m pip install pip-audit==2.7.3 && pip-audit.
- Do NOT run bare
uvx pip-audit with no -r: it audits pip-audit's own ephemeral
environment, not your project, and reports a clean bill for the wrong thing.
--no-deps skips re-resolution and is correct because the export is fully pinned.
- Report findings with the fix version per package; do not blind-upgrade everything,
and avoid
pip-audit --fix in automation (it mutates the environment, not the
lockfile).
- Alternative:
osv-scanner (Go binary) reads uv.lock directly.
- Recurrence: Dependabot security updates cover the alerting side; add a scheduled
scan workflow only if the user wants audit output in CI (workflow shape in
references/scanning-and-provenance.md).
5. Resolution freshness window (uv projects)
For uv projects, add a resolver-level buffer against just-published packages:
[tool.uv]
exclude-newer = "2026-06-27T00:00:00Z"
Recent uv releases also accept a relative window such as exclude-newer = "7 days";
run uv lock after setting it — an unsupported form fails there, not silently.
Two gotchas: a fixed timestamp freezes resolution in the past until someone bumps it
(add it to the release checklist), and unlike Dependabot's cooldown it delays
security fixes too — which is why you run both: exclude-newer guards uv lock
runs on laptops and CI, cooldown guards the bot, security advisories bypass the bot's
cooldown. Reference: https://docs.astral.sh/uv/reference/settings/#exclude-newer.
6. Secret scanning, push protection, and a CI backstop
GitHub-side scanning is free and on by default for public repos; private repos need
GitHub Secret Protection licensing. Enable/verify both toggles:
gh api -X PATCH "repos/{owner}/{repo}" --input - <<'EOF'
{ "security_and_analysis": {
"secret_scanning": { "status": "enabled" },
"secret_scanning_push_protection": { "status": "enabled" } } }
EOF
Push protection blocks a detected secret before it enters history — strictly
stronger than any after-the-fact scan — but GitHub documents that it only blocks a
subset of high-confidence patterns. Add a CI gitleaks scan as the backstop that also
catches --no-verify bypasses of local hooks (workflow YAML, plus the
gitleaks-action org-license gotcha, in the reference). If a secret is ever found:
rotate the credential first; history rewriting does not un-leak anything already
cloned, forked, or cached.
7. Code scanning (CodeQL)
Default setup is enough for pure-Python repos (no build step needed):
gh api -X PATCH "repos/{owner}/{repo}/code-scanning/default-setup" -f state=configured
Free for public repos; private repos need GitHub Code Security licensing. Use the
advanced workflow (in the reference) only when the user needs query-pack control or
language: actions coverage of workflow files. Making code-scanning results block
merges is a ruleset — hand that to python-ci.
8. CODEOWNERS for the paths that define automation
# .github/CODEOWNERS — LAST matching rule wins (not first)
* @OWNER/maintainers
/.github/ @OWNER/platform-security
/.github/CODEOWNERS @OWNER/platform-security
/AGENTS.md @OWNER/platform-security
/CLAUDE.md @OWNER/platform-security
/.claude/ @OWNER/platform-security
Rationale: whoever can edit workflows, dependabot.yml, or agent instruction files
can bypass every other control, so those paths need a stricter owner than the code.
Two silent failures: CODEOWNERS enforces nothing until "require review from code
owners" is enabled in branch protection/rulesets (python-ci's territory — say so in
the handoff), and an owner without write access is ignored without error — check the
file's rendered view on GitHub for syntax/owner errors.
9. Posture dashboard (OpenSSF Scorecard, optional)
Add the ossf/scorecard-action weekly workflow (YAML in the reference) to get a
0–10 score across ~a dozen heuristics with SARIF results in the Security tab. Treat
it as a triage dashboard, not a gate: its own maintainers call the checks heuristic,
and chasing the number (e.g. pinning SHAs no one audited) is documented security
theater. Public repos can check their existing score at
https://scorecard.dev/viewer/?uri=github.com/OWNER/REPO before adding anything.
10. SBOM and release provenance
Two SBOM paths — pick per audience:
gh api "repos/{owner}/{repo}/dependency-graph/sbom" --jq .sbom > sbom.spdx.json
uv export --format requirements-txt --no-emit-project -o /tmp/sbom-req.txt
uvx --from cyclonedx-bom==4.4.3 cyclonedx-py requirements /tmp/sbom-req.txt -o sbom.cdx.json
For releases, attest the built artifacts with actions/attest-build-provenance
(and actions/attest-sbom for the SBOM) inside the release workflow — snippet and
required permissions in the reference. The publish workflow itself, and PyPI-side
attestations via trusted publishing, belong to the python-release skill. Tell the
user the honest limit: an attestation proves where and how an artifact was built,
not that it is safe — real campaigns have shipped validly-signed malicious builds —
and it only matters if consumers verify (gh attestation verify dist/pkg.whl --repo OWNER/REPO).
11. Re-audit and hand off
Re-run step 1; the script should report no FAILs. List remaining WARN/NOTE items
with a one-line reason each (deliberate skip vs. needs licensing vs. sibling-skill
territory).
Output spec — what done looks like
.github/dependabot.yml with a pip or uv ecosystem entry, weekly schedule,
cooldown, grouped minor/patch updates, and a github-actions entry.
- Committed lockfile; a documented, pinned pip-audit invocation (or scheduled scan).
- Secret scanning + push protection enabled (or exact commands handed to the user
when permissions/licensing block it), plus a CI gitleaks backstop.
- CodeQL enabled (default setup or committed workflow).
- CODEOWNERS covering
/.github/ and agent-config paths, with the
branch-protection dependency stated in the handoff.
- Optional per scope: Scorecard workflow, SBOM artifact, attestation step wired into
the release workflow (or delegated to python-release).
scripts/check_supply_chain.py --root . exits 0.
Failure modes and gotchas
uvx pip-audit without -r audits the wrong environment (the tool's own
ephemeral venv) and happily reports zero findings.
- Dependabot cooldown can wedge frequently-released dependencies: the filter has
evaluated only the latest release, so a dep that ships weekly may never propose an
update (dependabot-core #14234/#14579; partially fixed — verify current behavior).
Symptom: a dependency silently months stale despite Dependabot running green.
- Cooldown at the package-manager level blocks security fixes; Dependabot's
cooldown does not (advisory updates bypass it). Don't replace one with the other.
- Cooldown is not proven for transitive dependencies (confirmed gap for npm in
dependabot-core #14683; unverified for pip/uv) — the lockfile audit in step 4 is
the control that actually covers transitives.
- Bot-authored PRs carry unearned trust: attackers mimic Dependabot branding,
and auto-merge-on-green shipped malware in the axios incident. Review like any PR.
- Dependabot does not alert on SHA-pinned actions (only semver tags) — a known
gap between this skill's alerting and python-ci's pinning policy; the dependabot
github-actions entry still proposes pin bumps, so keep it.
- gitleaks-action requires a license key for organization repos
(
GITLEAKS_LICENSE secret; free for personal accounts) — a silent-failure setup
step people miss.
- CODEOWNERS is last-match-wins — a trailing
* rule overrides every protection
above it. Put the catch-all first, specific security paths after.
- Push protection is a subset filter, not a guarantee — keep the CI backstop.
- Scorecard is gameable and heuristic — never wire it as a merge gate; its
public weekly scan omits several checks (CI-Tests, Contributors,
Dependency-Update-Tool) for API-cost reasons, so scores differ from local runs.
- Installing a Python package executes code — at install time (build backends,
setup.py) and import time. Never install a package to inspect it; read it on
https://inspector.pypi.io first. Watch for typosquats (request vs requests)
and for hallucinated package names in LLM suggestions (slopsquatting) — verify
the exact name, repo link, and release history on pypi.org before uv add.
- Free-tier boundary: dependency graph, Dependabot alerts and security updates
are free everywhere; secret scanning and CodeQL are free for public repos only —
on private repos, hand the user the licensing decision instead of a broken toggle.
Files
- references/dependabot.md — full annotated
dependabot.yml, cooldown semantics and bug detail, Renovate comparison,
noise-tuning, auto-merge policy evidence.
- references/scanning-and-provenance.md —
gitleaks/TruffleHog CI workflows, advanced CodeQL workflow, Scorecard workflow,
SBOM/attestation snippets with permissions, verification commands.
- scripts/check_supply_chain.py — read-only posture
audit;
--github adds GitHub-settings checks via gh; exits non-zero on FAIL.