| name | supply-chain-security |
| description | Secure an open-source project against supply-chain attacks and handle vulnerability reports. Use when writing a SECURITY.md, setting up private vulnerability reporting, responding to a reported CVE, hardening GitHub Actions permissions, pinning dependencies, adding SBOM or build provenance, signing releases, or improving an OpenSSF Scorecard. Also use when evaluating whether a dependency or a maintainer handoff is trustworthy, and when reviewing a PR that touches CI, build scripts, or install hooks. |
Supply Chain Security
Your project's security posture affects everyone downstream. A popular package is a
high-value target precisely because compromising it is cheaper than compromising its
users individually.
The realistic threat model
Ranked by observed frequency in actual incidents, not by drama:
- Compromised maintainer account — phishing, credential reuse, no 2FA. The
single most common root cause of package compromise.
- Malicious dependency — typosquat, or a legitimate package taken over via a
maintainer handoff to a stranger who asked nicely.
- Compromised CI — a workflow with excessive permissions, an unpinned third-party
action,
pull_request_target executing fork code.
- Malicious contribution — a subtle backdoor in a PR from a patient contributor
who spent months building trust.
- Build-time injection —
postinstall scripts, setup.py executing at install,
compromised build servers.
Note what is not at the top: vulnerabilities in your own code. Those matter, but the
supply chain is where the leverage is.
Baseline hardening
Do all of these. Together they take an afternoon and remove most of the realistic risk.
Accounts
- Hardware 2FA (WebAuthn) on GitHub and every package registry, for every maintainer.
SMS 2FA is not sufficient against the phishing that actually happens.
- Registry publishing via OIDC/trusted publishing instead of long-lived tokens.
- Audit org membership and repo access quarterly. Remove inactive maintainers'
publish rights, kindly and explicitly — this is not a demotion, it is hygiene.
Repository
- Branch protection on
main, including for admins.
- Signed commits/tags for releases (
git config commit.gpgsign true; Sigstore
gitsign avoids GPG key management entirely).
- Enable private vulnerability reporting: Settings → Security → "Private
vulnerability reporting". This gives researchers a channel that is not a public issue.
- Enable Dependabot alerts, secret scanning, and push protection.
CI (see also ci-pipelines)
permissions:
contents: read
Dependencies (see dependency-hygiene)
- Commit the lockfile. Install with
npm ci / pip install -r requirements.txt --require-hashes / cargo --locked.
- Disable install scripts where feasible (
npm ci --ignore-scripts).
- Review any new dependency's install hooks before adding it.
SECURITY.md
# Security Policy
## Supported Versions
| Version | Supported |
|---------|-----------|
| 4.x | Yes |
| 3.x | Security fixes until 2026-12-31 |
| < 3.0 | No |
## Reporting a Vulnerability
Report privately via [GitHub Security Advisories](../../security/advisories/new)
or email security@example.org.
Please do not open a public issue for security problems.
- Acknowledgement within **48 hours**
- Assessment within **7 days**
- Fix or mitigation plan within **30 days** for high severity
- We credit reporters in the advisory unless you prefer otherwise
## Scope
In scope: the published package, the CLI, the default configuration.
Out of scope: issues requiring an already-compromised host, denial of service
via deliberately malformed input to a debug-only tool, dependencies (report upstream).
Only promise timelines you will meet. A missed 48-hour promise is what turns a
cooperative researcher into a public disclosure.
Handling a report
- Acknowledge fast, even with no assessment yet.
- Reproduce, and determine severity honestly. CVSS is a rough tool; the questions
that matter are: what can an attacker do, what access do they need, and how many
users are affected in a default configuration?
- Open a private GitHub Security Advisory. It gives you a private fork to develop
the fix, and a CVE request button.
- Fix in the private fork. Do not push the fix to a public branch before the
advisory — the commit is the disclosure, and exploit development is faster than
user upgrades.
- Coordinate disclosure. Standard is 90 days, or on fix release, whichever is
first. Negotiate with the reporter; most are reasonable when you communicate.
- Release the fix to every supported branch, patch releases only, no other changes
in them.
- Publish the advisory with affected versions, patched versions, workaround for
users who cannot upgrade, and reporter credit.
- Notify downstream — GitHub advisories propagate to Dependabot automatically.
Also post where your users are.
If a reporter is hostile or demands a bounty you have not offered, stay factual and
keep to the process. If they publish early, do not escalate publicly; ship the fix.
Provenance and signing
For anything widely installed, make the build verifiable:
npm publish --provenance
cosign sign-blob --yes dist/tool.tar.gz
gh attestation verify dist/tool.tar.gz --repo owner/repo
Publish alongside a release: SHA256SUMS, signatures, and an SBOM
(syft . -o spdx-json, or cargo sbom). Also document the verification command in
your install docs — an unverifiable signature nobody knows how to check provides no
security, only the appearance of it.
OpenSSF Scorecard
- uses: ossf/scorecard-action@v2
with: { results_file: results.sarif, results_format: sarif, publish_results: true }
Treat the score as a checklist, not a target. The checks that actually reduce risk:
Branch-Protection, Token-Permissions, Pinned-Dependencies, Dangerous-Workflow,
Signed-Releases, Code-Review. The ones that measure process theater more than
safety: CII-Best-Practices, Contributors, Packaging. Do not contort the project
to raise a number.
Reviewing PRs for supply-chain risk
Escalate scrutiny sharply when a PR touches:
.github/workflows/** — especially permissions, new actions, or new triggers
package.json scripts, setup.py, build.rs, Makefile install targets
- Any new dependency, especially one that is new, low-download, or recently transferred
- Base64 blobs, minified vendored code, unexplained binaries, or generated files
committed without their generator
- Network calls in build or test code
.npmrc, .pypirc, credentials handling, anything reading environment variables
Specific tells worth knowing: obfuscated payloads assembled from string fragments,
code that behaves differently under CI environment variables, a dependency version
bump paired with an unrelated build script change, and a first-time contributor whose
PR touches only CI configuration.
Ask about anything you do not understand. "I don't follow what this line does — can
you explain?" is a complete and sufficient review comment, and a legitimate
contributor will answer it happily.
Maintainer handoff
The scenario behind several major incidents: a burned-out maintainer hands publishing
rights to a helpful stranger.
If you are handing off: verify identity beyond a GitHub account, transfer gradually
(commit rights → review rights → publish rights), keep 2FA requirements in place,
and announce the change publicly. If you are stepping away entirely and nobody has
earned that trust, archive the project and say so in the README. An archived
project is safe; an abandoned one with live publishing rights is a liability.
If you are asked to take over a project: expect and welcome scrutiny.
Anti-patterns
- No 2FA on the publishing account. Root cause of the majority of package
compromises.
- Unpinned third-party actions.
pull_request_target with fork checkout.
- Long-lived registry tokens when OIDC is available.
- Fixing a vulnerability in a public commit before the advisory.
- No SECURITY.md, forcing researchers to open a public issue.
- Promising a 24-hour response you cannot honor.
- Treating "it's just a dev dependency" as safe. Dev dependencies run on developer
machines with SSH keys and cloud credentials. That is the target.
- Ignoring a report because the reporter was rude.
- Accepting a maintainer offer from a stranger because you are tired.