Package Manager Hardening Skill
You are auditing and hardening a software repository's dependency management against supply chain attack risk. Your job is to detect gaps, explain why each matters, and — with the user's consent — fix them.
Step 1: Run the audit script
Run audit.py from the repo root to collect all findings as structured JSON:
python {SKILL_DIR}/audit.py --path .
Where {SKILL_DIR} is the directory containing this SKILL.md file (e.g. skills/harden-packages).
The script performs all file-based checks — ecosystem detection, lockfile presence, version constraint analysis, CI config scanning, Dependabot and Harden-Runner inspection — and emits a single JSON object. No manual file reading is needed for the audit phase.
If the script fails or is unavailable, fall back to reading files directly using the checklist in the Reference: Manual Audit Checklist section below.
Companion tool: verify_hash.py
Any time hardening work requires writing a cryptographic hash, digest, or commit
SHA into a file — pinning a GitHub Action, a container image, a Gradle wrapper, a
lockfile, a Terraform provider — use verify_hash.py to resolve it from the
authoritative upstream registry. Never invent, guess, autocomplete, or
extrapolate a hash.
python {SKILL_DIR}/verify_hash.py --help # list subcommands
# Examples (each prints just the hash on stdout; add --json for metadata)
python {SKILL_DIR}/verify_hash.py gh-action actions/checkout v4
python {SKILL_DIR}/verify_hash.py oci node:20-alpine
python {SKILL_DIR}/verify_hash.py pypi requests 2.32.3 --sdist
python {SKILL_DIR}/verify_hash.py npm left-pad 1.3.0
python {SKILL_DIR}/verify_hash.py crate serde 1.0.210
python {SKILL_DIR}/verify_hash.py gem rails 7.2.1
python {SKILL_DIR}/verify_hash.py packagist symfony/console 7.1.5
python {SKILL_DIR}/verify_hash.py gradle-dist 8.10
python {SKILL_DIR}/verify_hash.py maven org.apache.commons:commons-lang3:3.17.0
python {SKILL_DIR}/verify_hash.py tf-provider hashicorp/aws 5.84.0
python {SKILL_DIR}/verify_hash.py go-module github.com/stretchr/testify v1.9.0
python {SKILL_DIR}/verify_hash.py git-ref https://gitlab.com/foo/bar.git v1.0.0
Exit codes: 0 success, 1 upstream lookup failed, 2 usage error, 3 required
external tool missing (only for oci and git-ref, which shell out to
crane/skopeo/docker or git). If a lookup fails or the right tool isn't
available, stop and ask the user — do not write a placeholder.
The per-ecosystem AGENTS files in agents/ reference this helper as the preferred
verification path and document a manual fallback for each ecosystem.
Step 2: Interpret findings
Read the JSON output. The top-level keys are:
ecosystems_detected— list of ecosystems found (nodejs, python, go, rust, php, ruby, dotnet, terraform, maven, gradle)- One key per ecosystem (e.g.
nodejs,python) with nested findings dependabot— per-ecosystem Dependabot configuration statusharden_runner— per-workflow Harden-Runner status
Each check has a status field: "pass", "warn", "fail", or "missing".
Interpretation notes:
nodejs.exact_pins.unpinned— list ofdependencies/devDependenciesusing^,~, or ranges. Any entry here is a real finding.nodejs.minimum_release_age/python.uv_config.exclude_newer/rust.cooldown— resolver-level cooldown facts (no standalone status). The cooldown verdict is the cross-cuttingcooldownsection below;python.uv_config.statusnow reflects hash verification (integrity) only.cooldown.status/cooldown.ecosystems.<eco>— the release-age cooldown verdict. It is satisfied by either a resolver-level cooldown (uvexclude-newer, npm/pnpm/bun/yarn age gates,cargo-cooldown) or a Dependabot cooldown — not both.warnmeans both are configured: the resolver-level one has no security-update exception and blocks Dependabot's automated security updates (see the finding'snote).failmeans neither is configured. Recommend Dependabot (security fixes keep flowing). Driven by the--cooldown-strategyflag (autodefault /dependabot/resolver).rust.exact_pins.loose— Cargo entries without=prefix. Flag each one.terraform.exact_pins.loose— provider version constraints that aren't= X.Y.Z. Flag each one and note this requires human approval to change.dependabot.ecosystems.<eco>.status: warn— ecosystem is in dependabot.yml but has nocooldown:block.dependabot.ecosystems.github-actions— onlydefault-daysis reliable here;semver-*-dayskeys often don't trigger because action tags aren't parsed as SemVer. Don't flag a missingsemver-*-daysforgithub-actionsas a gap.dependabot.ecosystems.<eco>.status: missing— ecosystem not in dependabot.yml at all.harden_runner.workflows.<name>.egress_policy: audit— runner is present but in audit mode, not block. Flag as ⚠️.harden_runner.workflows.<name>.harden_runner_present: false— missing entirely. Flag as ❌.terraform.known_bug— always flag: Dependabot cooldown for terraform providers has a known bug; exact pinning is the primary control.maven.exact_pins.loose— pom.xml<version>entries that are ranges ([1.2,2.0)),LATEST,RELEASE, orSNAPSHOT. Every entry is a real finding. Property placeholders (${...}) are not flagged.maven.enforcer_plugin.status: fail—maven-enforcer-pluginis missing or has fewer than two of the canonical hardening rules (banDynamicVersions,requirePluginVersions,requireReleaseDeps,dependencyConvergence). Maven has no native lockfile; the enforcer plugin is the substitute.maven.strict_checksums.status: fail— neither<checksumPolicy>fail</checksumPolicy>nor--strict-checksums/-Con the CI invocation. Maven defaults to warn, not fail, on checksum mismatch.maven.sca_scanner.status: fail— no OWASP Dependency-Check, CycloneDX, or OSS Index plugin configured.gradle.exact_pins.loose—build.gradle(.kts)dependency strings with+,latest.release, ranges, orSNAPSHOT.gradle.dependency_locking.status: fail—dependencyLocking { lockAllConfigurations() }missing, not inLockMode.STRICT, or nogradle.lockfilecommitted.gradle.dependency_verification.status: fail—gradle/verification-metadata.xmlmissing orverify-metadatanot set to true. This is the strongest artefact-integrity control on the JVM; recommend it whenever it's absent.gradle.wrapper.status: fail—gradle-wrapper.propertiesmissingdistributionSha256Sum=line. The Gradle Wrapper download is otherwise unverified.gradle.repository_control— flag any of:FAIL_ON_PROJECT_REPOSnot set,mavenLocal()present (allows developer-local artefacts to substitute for Central),jcenter()present (unmaintained since 2024).gradle.reject_dynamic.status: fail— neitherfailOnNonReproducibleResolution()norfailOnDynamicVersions()configured.gradle.ci.writes_locks_in_ci: true— CI is running--write-locksor--write-verification-metadata. These are local-only operations; running them in CI defeats the controls. Always flag.dependabot.ecosystems.gradle.gradle_wrapper_ecosystem: missing— sub-finding: the Gradle Wrapper has its own Dependabot ecosystem (gradle-wrapper) separate fromgradle, and is missing.dotnet.lockfile.status: fail—packages.lock.jsonis missing. NuGet does not generate this by default;RestorePackagesWithLockFilemust be enabled.dotnet.lock_file_opt_in.status: fail—<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>is not present in any project file orDirectory.Build.props. Without it,--locked-modehas nothing to enforce.dotnet.exact_pins.loose—<PackageReference>or<PackageVersion>entries using*,*-*, or range notation ([1.0,),[1.0, 2.0)). Flag each one.dotnet.central_package_management.status: fail—Directory.Packages.propswithManagePackageVersionsCentrallyis absent. Not strictly required but strongly recommended for multi-project solutions to prevent version drift.dotnet.source_mapping.status: fail—nuget.configis absent or lacks<packageSourceMapping>. Without source mapping, any package name can be served from any configured source, enabling dependency confusion attacks.dotnet.ci.locked_mode: false— CI is not runningdotnet restore --locked-mode. Without it the lockfile is not enforced and the build can silently re-resolve to a different version set.dotnet.ci.vulnerability_check: false—dotnet list package --vulnerableis not in CI. Adddotnet list package --vulnerable --include-transitiveas a build-fail gate.
Step 3: Report findings
Present a structured audit report grouped by ecosystem. Use ✅ / ⚠️ / ❌ markers. After the per-ecosystem checklists, give a short prioritised summary: what poses the most supply chain risk right now, and what's quick to fix vs requires more thought.
Only report on items that are actually wrong or missing — don't enumerate every passing check.
Example report structure:
## Hardening Audit: [repo name]
### Stack detected
- Node.js (pnpm)
- Python (uv)
- CI: 2 workflow files
### Node.js (pnpm)
✅ pnpm-lock.yaml committed
✅ minimumReleaseAge: "7 days" set
❌ trustPolicy not set — a compromised version that drops provenance won't be blocked
⚠️ onlyBuiltDependencies not configured — all installed packages can run postinstall scripts
### Python (uv)
✅ uv.lock committed (hash verification is automatic with `uv sync --frozen`)
✅ exclude-newer = "7 days"
❌ [tool.uv.pip] require-hashes not set — ad-hoc `uv pip install` calls won't enforce hashes
### Dependabot
✅ npm cooldown configured (7/30/7/3 days)
❌ pip ecosystem missing from dependabot.yml
### GitHub Actions
⚠️ ci.yml: harden-runner present but egress-policy is "audit", not "block"
❌ release.yml: harden-runner not present
❌ No zizmor static analysis configured
⚠️ actions/checkout in ci.yml does not set persist-credentials: false (zizmor: artipacked × 3)
⚠️ ci.yml uses `runs-on: ubuntu-latest` (4 jobs) — should pin to `ubuntu-24.04`
❌ No `actions/dependency-review-action` job (PRs can introduce vulnerable deps unchecked)
❌ No OpenSSF Scorecard workflow *(opt-in — requires fine-grained PAT + branch protection setup)*
❌ No `SECURITY.md` and private vulnerability reporting is disabled
⚪ No OpenSSF Best Practices Passing badge *(opt-in — manual self-assessment at bestpractices.dev)*
⚪ No OpenSSF OSPS Baseline badge *(opt-in — manual self-assessment at bestpractices.dev)*
### Priority fixes (default, hands-off — will be applied if you agree)
1. Add harden-runner to release.yml — release workflows are high-value targets
2. Add a zizmor job to CI; pin the version (`uvx zizmor==X.Y.Z`) and resolve current findings
3. Add `actions/dependency-review-action` as a PR-only required check (fail-on-severity: high, license denylist)
4. Add `SECURITY.md` linking to private vulnerability reporting; enable PVR in repo Settings
5. Pin runner images: `ubuntu-latest` → `ubuntu-24.04` across all jobs
6. Set egress-policy: block in ci.yml once allowlist is confirmed
7. Add require-hashes = true under [tool.uv.pip] (defensive, for ad-hoc `uv pip install`)
8. Add pip entry to dependabot.yml
9. Set trustPolicy: no-downgrade in pnpm-workspace.yaml
### Opt-in (will *not* be applied unless you ask — see Step 5)
- Add `ossf/scorecard-action` workflow + README badge (needs `SCORECARD_TOKEN` PAT, branch protection config, repo-admin actions). Say *"set up Scorecard"* or *"add OpenSSF badges"* to apply.
- Register for the OpenSSF Best Practices Passing badge at <https://www.bestpractices.dev/> and add to README (manual self-assessment).
- Complete the OpenSSF OSPS Baseline self-assessment at <https://www.bestpractices.dev/> and add the Baseline badge to README (manual self-assessment).
Step 4: Offer to fix
After the report, ask: "Would you like me to apply the fixes? I can handle all of them, or you can tell me which ones to skip."
Default profile: what gets auto-applied vs deferred
The default fix flow is deliberately hands-off for the user — only changes that the agent can complete without a PAT, an external account, repo-admin actions, or out-of-band human input are applied. Everything else is reported as a finding but deferred to Step 5 so the user opts in explicitly when they have the time and credentials to follow through.
Apply by default (hands-off):
- Lockfile / package-manager config (
pnpm-workspace.yaml,.npmrc,.yarnrc.yml,bunfig.toml,pyproject.toml [tool.uv],.cargo/config.toml, etc.) .github/dependabot.ymlecosystem entries and cooldown blocks.github/workflows/*.yml: harden-runner additions, runner image pinning, action SHA pinning, zizmor job, dependency-review-action job, CodeQL workflow, fuzz workflowSECURITY.mdcontent (the file itself; the PVR repo-setting flip is a separate gh api call that the agent runs only with user consent — see below)- Tightening obvious non-breaking config (e.g.
--ignore-scriptsonnpm ci,BUNDLE_FROZEN=truein CI)
Defer to Step 5 (opt-in only — do not apply autonomously):
.github/workflows/scorecard.yml(Scorecard workflow). Requires a fine-grained PATSCORECARD_TOKEN, branch protection configured beforehand, andallow_auto_mergeenabled at the repo level. Adding the workflow without these produces a permanently-red badge.- Any addition of OpenSSF badges to
README.md(Scorecard, Best Practices Passing, OSPS Baseline). The Best Practices and Baseline badges depend on external self-assessment at https://www.bestpractices.dev/ that the agent cannot perform. - Branch-protection mutations on the default branch (
gh api -X PUT repos/<owner>/<repo>/branches/<branch>/protection). Admin-level repo change; can lock out the user if misconfigured. - Repo-setting flips that require admin (
allow_auto_merge,dependabot_security_updates,private-vulnerability-reporting, secret creation). Surface them as findings and asghcommands the user can run, but do not execute autonomously unless the user explicitly says so. - Creating, rotating, or installing any token / secret (
gh secret set SCORECARD_TOKEN, PAT generation instructions).
Flag for human review (apply only with explicit per-item approval):
- Tightening version constraints (
^/~/>=→ exact pins) in any production manifest. See per-ecosystem rules below. - Repository-source changes in Gradle (
mavenLocal()/jcenter()removal,RepositoriesMode.FAIL_ON_PROJECT_REPOS).
If the user later says "apply everything including badges", "set up Scorecard", "add OpenSSF badges", "do the badging", or any equivalent, jump to Step 5.
Default fix order
If the user agrees (fully or partially), apply the fixes in this order — lower risk / non-breaking changes first:
pnpm-workspace.yaml/.npmrc/.yarnrc.yml/bunfig.toml— add trust policy / build-script config. Cooldown is a choice, not an add-on: only add a resolver-level cooldown (minimumReleaseAge/minimum-release-age/npmMinimalAgeGate) if there is no Dependabot cooldown for the ecosystem. If Dependabot cooldown is present, do not add a resolver-level one — it would block Dependabot security autoupdates (see thecooldowninterpretation note).pyproject.toml— add[tool.uv.pip] require-hashes = true / verify-hashes = true(hash verification; automatic foruv sync --frozen, these flags only protect ad-hocuv pip install). Add[tool.uv] exclude-neweronly if the repo is not relying on a Dependabotuv/pipcooldown — prefer the Dependabot cooldown so security fixes are not blocked..cargo/config.toml— add a[cooldown]block only if there is no Dependabotcargocooldown (same resolver-vs-Dependabot trade-off).composer.json— addroave/security-advisoriesdev dependency; tighten^/~pins to exact (flag for human review)- Ruby CI — add
BUNDLE_FROZEN=trueandbundle audit checkto CI workflow 5a. .NET — add<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>toDirectory.Build.props; rundotnet restorelocally and commitpackages.lock.json; add--locked-modeanddotnet list package --vulnerable --include-transitiveto CI; add<packageSourceMapping>tonuget.config - Terraform
*.tf— tighten~>/>=constraints to=exact pins (do not apply autonomously — present proposed changes and require explicit human approval; changing constraints can causeterraform initto fail) - Maven
pom.xml— addmaven-enforcer-pluginwithbanDynamicVersions/requirePluginVersions/requireReleaseDeps/dependencyConvergence; set<checksumPolicy>fail</checksumPolicy>(do not autonomously change<version>pins — flag for human review, since tightening a transitive can break downstream consumers) - Gradle
build.gradle(.kts)/settings.gradle(.kts)— adddependencyLocking { lockAllConfigurations(); lockMode.set(LockMode.STRICT) },failOnNonReproducibleResolution(), andRepositoriesMode.FAIL_ON_PROJECT_REPOS; removemavenLocal()andjcenter()(flag — repository changes can break local development). Generatinggradle.lockfileandgradle/verification-metadata.xmlrequires running./gradlew --write-locksand./gradlew --write-verification-metadata sha256,pgp helplocally — do not run these in CI, and do not run them autonomously; instruct the user to run them and commit the result. .github/dependabot.yml— add missing ecosystem entries with cooldown blocks.github/workflows/*.yml— add or update harden-runner steps
For each file you modify, show a clear before/after diff and explain what changed and why.
Config templates to apply
pnpm-workspace.yaml additions:
minimumReleaseAge: "7 days"
trustPolicy: no-downgrade
npm .npmrc additions:
save-exact=true
minimum-release-age=10080
audit=true
fund=false
Yarn .yarnrc.yml additions:
defaultSemverRangePrefix: ""
npmMinimalAgeGate: 604800
Bun bunfig.toml additions:
[install]
exact = true
minimumReleaseAge = "7d"
lifecycleScripts = false
uv pyproject.toml additions:
[tool.uv]
# Rolling 7-day cooldown — re-evaluated on every `uv lock`.
# Supported since uv 0.9.17 (Dec 2025).
exclude-newer = "7 days"
[tool.uv.pip]
# Defensive: enforce hashes for ad-hoc `uv pip install` calls.
# `uv sync --frozen` already enforces lockfile hashes automatically;
# these flags only affect the pip-compat surface.
require-hashes = true
verify-hashes = true
Note on dev tooling: declare ruff/pytest/etc. as a PEP 735 dependency group, not as inline pip install lines in CI — Dependabot (package-ecosystem: "uv") cannot update what isn't in a manifest:
[dependency-groups]
dev = [
"ruff==0.11.2",
"pytest==8.3.5",
]
Then install in CI with uv sync --frozen --group dev and run via uv run <tool>.
Cargo .cargo/config.toml additions:
[cooldown]
days = 7
Dependabot cooldown block (per ecosystem):
cooldown:
default-days: 7
semver-major-days: 30
semver-minor-days: 7
semver-patch-days: 3
Dependabot cooldown for github-actions (special case):
GitHub Actions tags (v4, v4.1.2, plus the SHA-pinned # v4.1.2 comment workflow) are not always parsed as SemVer by Dependabot, so the semver-*-days keys do not reliably apply. Use only default-days for the github-actions ecosystem so the cooldown always takes effect:
# Note: GitHub Actions tags (v4, v4.1.2) aren't always parsed as SemVer
# by Dependabot, so we rely on default-days which always applies.
cooldown:
default-days: 7
Composer CI install:
export COMPOSER_NO_INTERACTION=1
composer install --no-scripts --no-plugins --prefer-dist
composer audit --locked
roave/security-advisories (add as dev dependency):
composer require --dev roave/security-advisories:dev-latest
Dependabot composer ecosystem entry:
- package-ecosystem: "composer"
directory: "/"
schedule:
interval: "daily"
cooldown:
default-days: 7
semver-major-days: 30
semver-minor-days: 7
semver-patch-days: 3
Harden-Runner endpoints for Composer:
packagist.org:443
repo.packagist.org:443
Ruby CI install:
export BUNDLE_FROZEN=true
bundle install --jobs 4 --retry 3
bundle exec bundle-audit check --update
.NET CI install and audit:
dotnet restore --locked-mode
dotnet build --no-restore --configuration Release
dotnet test --no-restore --configuration Release
dotnet list package --vulnerable --include-transitive
.NET Directory.Build.props (lockfile opt-in):
<Project>
<PropertyGroup>
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
</PropertyGroup>
</Project>
.NET nuget.config (package source mapping):
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<clear />
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
</packageSources>
<packageSourceMapping>
<packageSource key="nuget.org">
<package pattern="*" />
</packageSource>
</packageSourceMapping>
</configuration>
Dependabot nuget ecosystem entry:
- package-ecosystem: "nuget"
directory: "/"
schedule:
interval: "daily"
cooldown:
default-days: 7
semver-major-days: 30
semver-minor-days: 7
semver-patch-days: 3
Harden-Runner endpoints for NuGet:
api.nuget.org:443
globalcdn.nuget.org:443
Add dotnetcli.azureedge.net:443 and builds.dotnet.microsoft.com:443 when using
actions/setup-dotnet. Add your private feed hostname for Azure Artifacts, GitHub Packages,
or Artifactory.
Dependabot bundler ecosystem entry:
- package-ecosystem: "bundler"
directory: "/"
schedule:
interval: "daily"
cooldown:
default-days: 7
semver-major-days: 30
semver-minor-days: 7
semver-patch-days: 3
Harden-Runner endpoints for Bundler:
rubygems.org:443
api.rubygems.org:443
index.rubygems.org:443
Maven enforcer plugin (add to <build><plugins> in pom.xml):
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-enforcer-plugin</artifactId>
<version>3.5.0</version>
<executions>
<execution>
<id>enforce</id>
<goals><goal>enforce</goal></goals>
<configuration>
<rules>
<banDynamicVersions/>
<requirePluginVersions>
<banLatest>true</banLatest>
<banRelease>true</banRelease>
<banSnapshots>true</banSnapshots>
</requirePluginVersions>
<requireReleaseDeps/>
<dependencyConvergence/>
</rules>
</configuration>
</execution>
</executions>
</plugin>
Maven strict checksums (settings.xml repository entry):
<releases>
<enabled>true</enabled>
<checksumPolicy>fail</checksumPolicy>
</releases>
Maven CI invocation:
mvn --batch-mode --strict-checksums --fail-fast verify
mvn --batch-mode org.owasp:dependency-check-maven:check
Dependabot maven ecosystem entry:
- package-ecosystem: "maven"
directory: "/"
schedule:
interval: "daily"
cooldown:
default-days: 7
semver-major-days: 30
semver-minor-days: 7
semver-patch-days: 3
Gradle hardening block (root build.gradle.kts):
allprojects {
configurations.all {
resolutionStrategy {
failOnNonReproducibleResolution()
}
}
dependencyLocking {
lockAllConfigurations()
lockMode.set(LockMode.STRICT)
}
}
Gradle repository control (settings.gradle.kts):
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
mavenCentral()
gradlePluginPortal()
}
}
Gradle generate lockfile and verification metadata (run locally, commit the result — never in CI):
./gradlew dependencies --write-locks
./gradlew --write-verification-metadata sha256,pgp --export-keys help
Gradle CI invocation:
./gradlew --no-daemon --console=plain --stacktrace build
./gradlew --no-daemon dependencyCheckAnalyze
Dependabot gradle + gradle-wrapper ecosystem entries:
- package-ecosystem: "gradle"
directory: "/"
schedule:
interval: "daily"
cooldown:
default-days: 7
semver-major-days: 30
semver-minor-days: 7
semver-patch-days: 3
- package-ecosystem: "gradle-wrapper"
directory: "/"
schedule:
interval: "weekly"
Harden-Runner endpoints for Maven and Gradle:
repo.maven.apache.org:443
repo1.maven.org:443
plugins.gradle.org:443
services.gradle.org:443
downloads.gradle.org:443
Add nvd.nist.gov:443 services.nvd.nist.gov:443 if OWASP Dependency-Check runs in CI. Add dl.google.com:443 for Android/Kotlin projects. Add private registry hostnames (Nexus, Artifactory, GitHub Packages) if used.
Terraform required_providers exact pinning:
terraform {
required_version = "= 1.9.8"
required_providers {
aws = {
source = "hashicorp/aws"
version = "= 5.84.0" # replace with current exact version
}
}
}
Terraform CI init with lockfile enforcement:
terraform init -input=false -lockfile=readonly
Multi-platform provider lock (run locally, commit result):
terraform providers lock \
-platform=linux/amd64 \
-platform=linux/arm64 \
-platform=darwin/arm64 \
-platform=darwin/amd64
Dependabot terraform ecosystem entry:
- package-ecosystem: "terraform"
directory: "/"
schedule:
interval: "daily"
cooldown:
default-days: 7
semver-major-days: 30
semver-minor-days: 7
semver-patch-days: 3
Harden-Runner step (start in audit mode; switch to block after confirming allowlist):
- uses: step-security/harden-runner@6c3c2f2c1c457b00c10c4848d6f5491db3b629df # v2
with:
egress-policy: audit
disable-sudo: true
allowed-endpoints: >
api.github.com:443
github.com:443
objects.githubusercontent.com:443
Append the ecosystem-specific endpoints:
- Node.js:
registry.npmjs.org:443(addnpm.pkg.github.com:443if GitHub Packages used) - Python:
pypi.org:443 files.pythonhosted.org:443(addastral.sh:443 release-assets.githubusercontent.com:443 raw.githubusercontent.com:443ifastral-sh/setup-uvis used — it downloads the uv binary from GitHub releases (which redirect torelease-assets.githubusercontent.com, not the oldergithub-production-release-asset-2e65be.s3.amazonaws.com), and on v8+ also fetches its version manifest fromraw.githubusercontent.com/astral-sh/versions. v6.x baked the manifest into the action and did not needraw.githubusercontent.com.) - Go:
proxy.golang.org:443 sum.golang.org:443 storage.googleapis.com:443 - Rust:
crates.io:443 index.crates.io:443 static.crates.io:443 - PHP:
packagist.org:443 repo.packagist.org:443 - Ruby:
rubygems.org:443 api.rubygems.org:443 index.rubygems.org:443(addraw.githubusercontent.com:443ifbundle audit updateruns in CI) - .NET / NuGet:
api.nuget.org:443 globalcdn.nuget.org:443(adddotnetcli.azureedge.net:443 builds.dotnet.microsoft.com:443ifactions/setup-dotnetis used; add private feed hostname for Azure Artifacts / GitHub Packages) - Maven / Gradle:
repo.maven.apache.org:443 repo1.maven.org:443 plugins.gradle.org:443 services.gradle.org:443 downloads.gradle.org:443(addnvd.nist.gov:443 services.nvd.nist.gov:443if OWASP Dependency-Check runs;dl.google.com:443for Android/Kotlin) - Terraform:
registry.terraform.io:443 releases.hashicorp.com:443 checkpoint-api.hashicorp.com:443 - OpenTofu:
registry.opentofu.org:443(provider binary CDN endpoints vary by provider — discover viaauditmode first)
Important notes
- Never downgrade security. If a stricter setting is already in place (e.g., a longer cooldown than the recommended 7 days), leave it as-is and note it as ✅.
- Don't touch version numbers in package manifests unless the user explicitly asks. Pinning existing unpinned ranges is a breaking change that deserves separate review.
- Harden-Runner egress-policy: always start new additions in
auditmode, notblock. Switching toblockrequires the user to first review the audit logs and build a confirmed allowlist. - Flag items requiring human judgment rather than silently skipping them — for example, if
onlyBuiltDependenciesis missing, list the packages that currently run build scripts and ask the user to confirm which should be allowed before writing the config. - Never guess package names. When any fix involves adding a dependency (e.g.
roave/security-advisories,bundler-audit, an SCA plugin), verify the exact name against the registry in the current session before writing it — typosquatting and slopsquatting (names language models tend to invent) are actively exploited. The per-ecosystem AGENTS files each carry a mandatory "Never Guess" identity-verification section; apply the same rule during skill-driven fixes. go installpinning: never use@latest,@master, or@mainin anygo installinvocation — in Makefiles, scripts, or CI workflows. Always use an explicit@vX.Y.Z. The go sum database provides hash verification but the version itself is still mutable without an explicit pin.- Go stdlib CVEs: standard library vulnerabilities cannot be fixed by bumping a
requireentry — they require upgrading thegodirective ingo.modto the patched Go release. When govulncheck reports stdlib findings, check the "Fixed in" version and update thegodirective accordingly. - Go
godirective format: use the full patch version (go 1.25.9), not the bare minor (go 1.25) or base patch (go 1.25.0).go mod tidypreserves fully-specified patch versions but normalisesgo 1.25→go 1.25.0. govulncheck reads thegodirective as the stdlib CVE baseline —go 1.25.0will surface all CVEs fixed in 1.25.1 through the latest patch. Do not split a patch-versionedgodirective intogo 1.25+toolchain go1.25.9on the assumption that the toolchain directive covers CVE exposure — it does not; only thegodirective does. - Reusable workflow pinning: job-level
uses:entries that delegate to a reusable workflow (e.g., SLSA generators, shared org workflows) are mutable refs and must be SHA-pinned, just like step-level actions. They cannot receive harden-runner steps, so flag them separately. - zizmor in CI: if the user wants to harden a GitHub Actions setup, recommend adding a
zizmorjob alongside Harden-Runner. They are complementary — Harden-Runner is runtime egress control, zizmor is static analysis of the workflow file itself. Pin the zizmor version (uvx zizmor==X.Y.Z) and run at--min-severity=medium(default persona) or--persona=auditor --min-severity=low(stricter). - dependency-review + SECURITY.md are zero-infra, hands-off GitHub-native controls: include both in the default fix flow. They cost nothing, require no external accounts or tokens, and produce a vulnerability-disclosure path (
SECURITY.md+ PVR). - OpenSSF Scorecard is not in the default fix flow even though the workflow itself is short. It only produces useful signal once a
SCORECARD_TOKENPAT is created, branch protection is configured, andallow_auto_mergeis enabled at the repo level — all admin-level human actions. Surface it as a finding in the report and apply it only via Step 5 when the user explicitly opts in. - Branch protection: every new required-check job (zizmor, dependency-review, Scorecard) must be added to the repo's required status checks for the default branch, otherwise it can be merged around. Use
gh api -X PUT repos/<owner>/<repo>/branches/<branch>/protectionand updaterequired_status_checks.contexts. This is an admin-level repo mutation — surface the exactghcommand in the report but only execute it autonomously inside Step 5, after the user has explicitly opted in.
Step 5 (opt-in): OpenSSF badging and Scorecard setup
Do not enter this step automatically. Only run it when the user explicitly opts in with a phrase like:
- "set up Scorecard" / "add Scorecard"
- "add OpenSSF badges" / "do the badging" / "set up the badges"
- "apply everything including badges"
- "finish the OpenSSF setup"
The reason this step is gated: every item below either requires a token the user has to generate, an external account at https://www.bestpractices.dev/, or an admin-level repo mutation that can lock the user out if misconfigured. An AI agent on its own cannot complete any of these safely.
5a. OpenSSF Scorecard workflow
Before creating the workflow, walk the user through the prerequisites in this order — do not add scorecard.yml until they're done, because a workflow added prematurely will produce a permanently-red Scorecard badge that's worse than no badge:
- Confirm GitHub plan. Scorecard works on any public repo for free; private repos need GHAS. Stop here if it's a private repo without GHAS and the user doesn't want to pay.
- Create the
SCORECARD_TOKENPAT. Walk the user through: https://github.com/settings/personal-access-tokens/new. The token must be a fine-grained PAT with onlyAdministration: Read-onlyon the target repo. No other scopes. Recommend a 1-year expiry with a calendar reminder to rotate. Have the user paste the token value intogh secret set SCORECARD_TOKEN --repo <owner>/<repo>themselves — do not ask them to share it in chat. - Enable required repo settings (each is a
gh apicall — show the command and ask for explicit confirmation before running):allow_auto_merge=truesogh pr merge --autoworks once branch protection is ondependabot_security_updatesenabled (required fordependency-review-actionto function)- Private vulnerability reporting enabled
- Configure branch protection on the default branch. For solo / no-reviewer projects use the config block documented in the OpenSSF Scorecard section of the manual checklist (count=0,
enforce_admins: true,require_code_owner_reviews: false,require_last_push_approval: false). For multi-maintainer repos, proposerequired_approving_review_count: 1,require_code_owner_reviews: true,require_last_push_approval: trueand ask the user to confirm. - Only then add
.github/workflows/scorecard.ymlwith: weekly schedule + push to default branch +workflow_dispatch, SARIF upload viagithub/codeql-action/upload-sarif,publish_results: true,repo_token: ${{ secrets.SCORECARD_TOKEN }}, harden-runner inauditmode, action pins to SHA (resolve withverify_hash.py gh-action ...). - Wait for the first scheduled run (or trigger via
gh workflow run scorecard.yml) before adding the Scorecard badge to the README. A badge pointing at an empty result reads as a failure. - Add the Scorecard badge to the top of the README only after the first successful run completes.
5b. OpenSSF Best Practices Passing badge
Fully manual — the agent's job is to walk the user through it, not to fake it:
- Register the project at https://www.bestpractices.dev/en/projects/new.
- Complete the Passing self-assessment (~100 questions across the criteria categories). The agent can read each criterion aloud and propose evidence URLs (links to the repo, workflows,
SECURITY.md, etc.) but must not submit answers — the user is attesting personally. - Once 100% Passing is awarded, add the Passing badge to the README.
5c. OpenSSF OSPS Baseline badge
Same structure as 5b but a different framework:
- From the same project page at https://www.bestpractices.dev/, start the OSPS Baseline self-assessment (smaller — ~25 controls across AC / BR / DO / GV / LE / QA / VM).
- Walk the user through each control. The Baseline self-assessment is independent of the Passing badge — a project can hold either, both, or neither.
- Once the assessment is submitted, add the Baseline badge to the README. The Baseline badge URL links to the same
bestpractices.devproject record as the Passing badge.
5d. Anything else the user adds to the opt-in queue
If the user says "also do X" during Step 5, where X is normally a default-flow item that was already applied, just confirm it's done. If X is a new admin-level mutation not covered above, surface the exact command and ask for confirmation before executing.
Reference: Manual Audit Checklist
Use this section only if audit.py cannot be run. It replicates what the script checks.
Node.js (npm / pnpm / yarn / bun)
- Is
package-lock.json/pnpm-lock.yaml/yarn.lock/bun.lockpresent and not gitignored? - npm/yarn: does CI run
lockfile-lint(pinned version) validating--allowed-hosts,--validate-https, and--validate-integrity? Lockfileresolved-URL tampering passes hash verification andnpm ci— a lint check is the dedicated control. - Do
dependenciesanddevDependenciesuse exact versions (no^or~)? - npm ≥ 11.10: is
minimum-release-ageset in.npmrc? - pnpm ≥ 10.16: is
minimumReleaseAgeset inpnpm-workspace.yaml? IstrustPolicy: no-downgradeset? - Yarn Berry ≥ 4.10: is
npmMinimalAgeGateset in.yarnrc.yml? - Bun ≥ 1.3: is
minimumReleaseAgeset inbunfig.toml? - pnpm: is
onlyBuiltDependencies(allowlist) orignoredBuiltDependenciesconfigured? - Does CI use
npm ci/pnpm install --frozen-lockfile/yarn install --immutable/bun install --frozen-lockfile? - npm: does CI run
npm audit signaturesafter install? It verifies registry signatures and provenance attestations for the full tree — catching tampered artifacts that lockfile hash verification cannot (the lockfile hash is recorded from the artifact, so a substituted artifact carries a matching hash).
Python (pip / uv)
- uv: is
uv.lockpresent and not gitignored? Isexclude-newerset in[tool.uv](rolling duration like"7 days"preferred over a frozen timestamp)? Arerequire-hashesandverify-hashesset in[tool.uv.pip](note: this is[tool.uv.pip], not[tool.uv]— uv rejects them at the top level)? Hash verification againstuv.lockis automatic viauv sync --frozenregardless. Are dev tools (ruff/pytest/etc.) declared in[dependency-groups]so Dependabot can update them, rather than inlinepip install x==yin CI? - pip: is there a
requirements.lockwith hashes (frompip-compile --generate-hashes)? - Are all package specs using
==(not>=,~=, or unpinned)? - uv: does CI use
uv sync --frozen? pip:pip install --require-hashes?
Go
- Are
go.modandgo.sumboth present and not gitignored? - Are all
requireentries using explicit tagged versions (not@latest,@master,@main)? - Is
GONOSUMDBscoped only to private modules (not*)? - Does CI run
go mod verifyandgovulncheck ./...? - Does CI run
go mod verifybeforego build? A tampered or stale module graph must be caught before the compiler touches any dependency. - Does the
go mod tidydiff check usegit diff --exit-code -- go.mod go.sum(with the--pathspec separator)? Does it also run beforego build? - Is
GOTOOLCHAIN=localset in CI? Without it, the go binary can silently auto-fetch a different toolchain at runtime even afteractions/setup-gohas installed the intended version. - Does the
godirective ingo.modinclude the full patch version (e.g.,go 1.25.9notgo 1.25orgo 1.25.0)? govulncheck reads thegodirective — notgo env GOVERSION— as the stdlib baseline for CVE analysis. A barego 1.25.0causes govulncheck to report all CVEs fixed in later patch releases as active, even if the installed toolchain is already patched. - Are Makefile or script
go installinvocations pinned to explicit versions (not@latest,@master,@main)?go installwith@latestis the same supply-chain risk as an unpinned GitHub Action — the only difference is that the go sum database verifies the hash, but the version itself is still mutable. - Does CI install tools via
make <target>that the job doesn't actually use? If so, extract only the needed commands rather than running the full target (e.g., don't run adepstarget that installs a linter when the job never callsmake lint).
Rust / Cargo
- Is
Cargo.lockpresent and not gitignored? - Do
Cargo.tomldependencies use=exact version syntax? - Is
cargo-cooldownconfigured in.cargo/config.tomlwith[cooldown] days = 7? - Does CI use
--lockedoncargo build/cargo testand runcargo audit --deny warnings?
PHP / Composer
- Is
composer.lockpresent and not gitignored? - Do all
requireentries use exact version strings (no^,~,>=,*)? - Is Composer 2.7 or later in use?
- Does CI run
composer audit --locked? Isroave/security-advisories:dev-latesta dev dependency? - Does CI use
COMPOSER_NO_INTERACTION=1and--no-scripts --no-plugins --prefer-dist?
.NET / NuGet
- Is
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>set inDirectory.Build.propsor each.csproj? (Not the default — must be opted in.) - Is
packages.lock.jsonpresent next to each.csprojand committed? (Generated bydotnet restoreonce the opt-in is set.) - Does CI use
dotnet restore --locked-mode? (Fail if packages.lock.json would need to change.) - Are version constraints free of floating (
*,*-*) and range syntax ([1.0,),[1.0, 2.0))? Bare versions like13.0.3are acceptable when paired with a lockfile;[13.0.3]bracket notation is the strictly exact form. - Is
Directory.Packages.propswithManagePackageVersionsCentrallypresent for multi-project solutions? - Is
nuget.configpresent with<packageSourceMapping>configured? (Prevents dependency confusion attacks.) - Does CI run
dotnet list package --vulnerable --include-transitive? - Is
global.jsonpresent with the .NET SDK version pinned ("rollForward": "disable")?
Ruby / Bundler
- Is
Gemfile.lockpresent and not gitignored? - Do all
gementries use exact versions (no~>,>=)? - Is
BUNDLE_FROZEN=trueset in CI? - Does CI run
bundle audit check? - Is a
rubydirective inGemfileor a.ruby-versionfile committed?
JVM (Maven / Gradle — Java / Kotlin)
Maven (pom.xml present):
- Do all
<version>entries use exact, immutable values? Flag any range ([1.2,2.0),(1.2,)),LATEST,RELEASE, orSNAPSHOT. - Is every plugin pinned in
<build><pluginManagement>with an explicit<version>? - Is
maven-enforcer-pluginconfigured with at least two of:banDynamicVersions,requirePluginVersions,requireReleaseDeps,dependencyConvergence? - Is the checksum policy
fail— either via<checksumPolicy>fail</checksumPolicy>in a repository definition or via--strict-checksums/-Con every CI Maven invocation? - Is at least one SCA scanner (
dependency-check-maven,cyclonedx-maven-plugin, orossindex-maven-plugin) wired into the build? - Does CI pass
--batch-modeand--fail-fast? - Is
.mvn/extensions.xmlreviewed? Extensions load before any POM is parsed and run JVM code on the build host.
Gradle (build.gradle or build.gradle.kts present):
- Are all dependency coordinates exact? Flag any
+,latest.release, range ([1.0,2.0)), orSNAPSHOT. - Is
dependencyLocking { lockAllConfigurations(); lockMode.set(LockMode.STRICT) }configured at the root and isgradle.lockfilecommitted for every project module? - Is
gradle/verification-metadata.xmlcommitted, with<verify-metadata>true</verify-metadata>and ideally<verify-signatures>true</verify-signatures>? - Does
gradle/wrapper/gradle-wrapper.propertiesinclude adistributionSha256Sum=line? - In
settings.gradle(.kts), isrepositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)set? - Is
mavenLocal()orjcenter()referenced anywhere? Both are red flags. - Is
failOnNonReproducibleResolution()(orfailOnDynamicVersions()+failOnChangingVersions()) configured on every configuration? - Does CI invoke
./gradlew(not a systemgradle) and pass--no-daemon? - Does CI ever run
--write-locksor--write-verification-metadata? If so, that defeats the controls and must be removed. - Is at least one SCA plugin (
org.owasp.dependencycheck,org.cyclonedx.bom, or Snyk) configured?
Dependabot ecosystems for JVM projects:
- Maven:
package-ecosystem: "maven"with cooldown. - Gradle:
package-ecosystem: "gradle"with cooldown, pluspackage-ecosystem: "gradle-wrapper"(a separate ecosystem that updates the Wrapper version itself).
Terraform / OpenTofu
- Is
.terraform.lock.hclpresent in each root module, not gitignored, with multi-platformh1:hashes? - Do all
required_providersblocks use= X.Y.Zexact constraints? - Does CI use
terraform init -lockfile=readonly? - OpenTofu: is state encryption configured via
encryption {}blocks?
Dependabot
- Is
.github/dependabot.ymlpresent? - For each detected ecosystem, is there an
updatesentry with acooldownblock? - Terraform: note known cooldown bug (issue #13715) — exact pinning is the primary control.
Harden-Runner
- Is
step-security/harden-runner(SHA-pinned, e.g.@6c3c2f2c1c457b00c10c4848d6f5491db3b629df # v2) the first step in every job that installs dependencies? Flag any@v2/@v3/@mainreferences — actions must be SHA-pinned to a 40-char commit, with a# vX.Y.Ztrailing comment. - Is
egress-policyset toblock(notaudit)? Isdisable-sudo: trueset? - Are job-level
uses:entries (reusable workflows) SHA-pinned? These are mutable refs just like step-leveluses:but cannot receive harden-runner steps — the entire job is delegated to the reusable workflow. Flag unpinned reusable workflow refs separately from the harden-runner audit.
Workflow static analysis (zizmor)
zizmor is the canonical static analyser for GitHub Actions workflows. It catches issues the file-based checks above can't reach — template injection, missing persist-credentials: false, missing workflow concurrency:, dangerous triggers (pull_request_target with checkout-of-head), secrets exposed to forks, mutable reusable-workflow refs, etc.
- Is there a
zizmorjob in CI? Flag as ❌ if missing. - Is the zizmor version pinned (e.g.
uvx zizmor==1.24.1)? Flag bareuvx zizmoras ⚠️. - Is
--min-severityset to at leastmedium(default) orlow(preferred, with--persona=auditor)? - When recommending or adding zizmor, run it locally first:
uvx zizmor --persona=auditor .. Report each finding and offer to fix; do not auto-suppress findings. - Common findings to know:
artipacked(medium):actions/checkoutmissingpersist-credentials: false. Fix on every checkout unless the job pushes to git.template-injection(high/medium):${{ ... }}interpolated into arun:block. Fix by passing the value throughenv:and quoting"$VAR"in the shell.concurrency-limits(low): no workflow-levelconcurrency:block. Add one keyed on${{ github.workflow }}-${{ github.ref }}withcancel-in-progress: truefor CI;falsefor release/deploy workflows.dangerous-triggers(high):pull_request_targetcombined with a checkout ofgithub.event.pull_request.head.ref. Requires immediate human review.unpinned-uses(high): action referenced by tag (@v4) instead of SHA. Already covered by the harden-runner / SHA-pinning checks above, but zizmor will surface it again.excessive-permissions(medium): workflow or job missingpermissions:declaration, or granting more thancontents: readwithout justification.undocumented-permissions(auditor low):permissions:block missing inline comments justifying each grant. Fix with trailing# why this is neededcomments on each line.
Runner image pinning
- Are all
runs-on:values pinned to a specific image (ubuntu-24.04,ubuntu-22.04,windows-2022,macos-14)? Flag everyubuntu-latest/windows-latest/macos-latestas ⚠️. GitHub rolls these forward and has historically changed pre-installed tools mid-lifecycle. - Note that Dependabot does not propose runner-image bumps. Recommend adding a calendar reminder or tracking via a long-lived issue when a new LTS is GA.
Dependency review on PRs
-
Is there an
actions/dependency-review-actionjob, gated onif: github.event_name == 'pull_request'? Flag absence as ❌. -
Is
fail-on-severityat leasthigh?moderateorlowis preferred for security-sensitive repos. -
Is there a
deny-licenseslist appropriate to the project's own license? At minimum, MIT/Apache projects should deny GPL/AGPL. -
Is
comment-summary-in-pr: on-failureset so contributors see why the check failed? -
Is the job a required status check on
main? If not, the gate can be merged around. -
Gotcha: the action requires Dependabot security updates to be enabled on the repo (not just the always-on dependency graph). Without it, the job fails with
Dependency review is not supported on this repository. Please ensure that Dependency graph is enabled. Enable it with:gh api -X PATCH repos/<owner>/<repo> \ -f 'security_and_analysis[dependabot_security_updates][status]=enabled'Verify with
gh api repos/<owner>/<repo> --jq '.security_and_analysis.dependabot_security_updates.status'— should returnenabled. This is free for public repos and included with GitHub Advanced Security for private repos. Add this to the fix plan whenever you recommenddependency-review-action, and re-run any failed job after flipping the setting.
OpenSSF Scorecard
-
Is there a
.github/workflows/scorecard.ymlrunningossf/scorecard-actionon a weekly schedule + push tomain? Flag absence as ❌. -
Are SARIF results uploaded via
github/codeql-action/upload-sarifso they appear in the Security tab? -
Is
publish_results: trueset so the README badge resolves? -
Is the workflow's
egress-policyset toaudit(notblock)? Scorecard legitimately contacts deps.dev, OSV, npm, PyPI, etc. and cannot run under a fixed allowlist. Flagblockhere as ⚠️. -
Is the Scorecard badge in the README so the score is publicly visible?
-
Is
repo_token: ${{ secrets.SCORECARD_TOKEN }}set on the action step? Without it, theBranch-Protectioncheck fails the entire run withsome github tokens can't read classic branch protection rules. The token must be a fine-grained PAT with onlyAdministration: Read-onlyon the target repo (no other scopes). Flag absence as ❌ if branch protection is configured; ⚠️ otherwise. -
Is
workflow_dispatch:set on theon:triggers? Without itgh workflow run scorecard.ymlerrors with HTTP 422. Recommend adding it to any scheduled workflow so manual re-runs work without pushing empty commits. (Re-running a historical run via the UI uses that commit's workflow YAML and ignores the currentmainversion — so old failures stay failed forever; that's expected, not a bug.) -
Is branch protection itself configured to score well on Scorecard's
Branch-Protectioncheck? On solo / no-reviewer projects there's a non-obvious config that ticks five Scorecard warnings without blocking self-merge:// gh api -X PUT repos/<owner>/<repo>/branches/main/protection --input - { "required_status_checks": { "strict": true, "contexts": ["..."] }, "enforce_admins": true, // protect against your own mistakes too "required_pull_request_reviews": { "dismiss_stale_reviews": true, // no-op until reviews required; tick the box "require_code_owner_reviews": false, // MUST be false on solo — GitHub forbids self-approval "required_approving_review_count": 0, // require PR flow but not an approver "require_last_push_approval": false // MUST be false on solo — even with count=0 this requires an approval after each push and blocks self-merge }, "restrictions": null, "required_linear_history": true, "allow_force_pushes": false, "allow_deletions": false, "required_conversation_resolution": true }The trick is
required_approving_review_count: 0. It satisfies Scorecard's "PRs are required" check and letsdismiss_stale_reviewstick its box, while keeping self-merge withgh pr merge --auto --squashworking on a solo repo. Do not setrequire_code_owner_reviews: trueorrequire_last_push_approval: trueon a solo project — both will block every merge (codeowners: GitHub forbids approving your own PR; last-push-approval: even at count=0, GitHub still requires an approval after the most recent push, which the pusher can't provide). The remaining Scorecard warnings (codeowners review not required,last push approval is disabled) are known-acceptable trade-offs; document them inSECURITY.mdalongside theCode-ReviewandMaintainedtrade-offs. They auto-flip totruethe moment a second maintainer is added. -
Also: enable
allow_auto_mergeat the repo level (gh api -X PATCH repos/<owner>/<repo> -f allow_auto_merge=true) sogh pr merge --auto --squash --delete-branchworks — without it you getGraphQL: Auto merge is not allowed for this repository. -
Note that a dropping Scorecard score is a regression to triage — not just a green/red flag at a point in time.
-
Common Scorecard
Pinned-Dependenciesfinding: inlinenpm install pkg@verandpip install pkg==x.y.zin workflow steps are version-pinned but not hash-verified, and Scorecard scores them ~9/10. The fix is to commit a lockfile and switch tonpm ci --ignore-scripts/uv sync --frozen --group dev. Recommend this whenever you see inline tool installs in a workflow.
CI tool installs (npm / pip)
- For every
run: npm install ...orrun: pip install ...in.github/workflows/, is there a corresponding committed lockfile?- npm:
package.json+package-lock.json, installed withnpm ci --ignore-scripts. Flag inlinenpm install pkg@veras ⚠️ and barenpx --yesas ❌. - pip: tools declared in
pyproject.toml [dependency-groups]and locked inuv.lock, installed withuv sync --frozen --group dev. Flag inlinepip install pkg==x.y.zas ⚠️ and barepip install pkgas ❌.
- npm:
- Is
npm ciused (notnpm install)?npm installcan mutate the lockfile silently in CI. Flag barenpm installin CI as ❌. - Is
--ignore-scriptsset on the npm install? Without it, a malicious transitive dep can execute arbitrary code viapreinstall/postinstallhooks during install. Flag missing--ignore-scriptsas ⚠️. - Is the corresponding ecosystem (
npm,uv,pip) present in.github/dependabot.ymlwith a cooldown? Otherwise the lockfile rots. - Are auto-generated lockfiles (
package-lock.json,uv.lock) annotated inREUSE.toml? They cannot carry inline SPDX headers (would be stripped on regeneration). - When adding npm tooling to a repo for the first time, the full sequence is:
package.json(private: true, tool indevDependenciesat exact version)npm install --package-lock-only --ignore-scriptsto generatepackage-lock.json- Add
npmtodependabot.yml - Add both files to
REUSE.toml - Switch the workflow step to
npm ci --ignore-scripts
Static application security testing (CodeQL)
- Is there a
.github/workflows/codeql.ymlrunninggithub/codeql-action? Flag absence as ❌. - Does it trigger on
push,pull_requestto the default branch, and aschedule(weekly)? Missingscheduleis ⚠️ — catches drift after dependency bumps. - Does the language matrix include
actionsplus the project's primary language(s)? Missingactionsis ⚠️ — it complements zizmor by catching workflow vulnerabilities. - Is
queries: security-extendedset? Bare default orsecurity-and-qualityis ⚠️ for security-focused repos (the latter dilutes signal with code-quality findings). - Is
egress-policy: auditset on Harden-Runner? CodeQL legitimately fetches query packs and submits results;blockis impractical and will break the workflow. Flagblockhere as ⚠️. - Is
permissions: security-events: writegranted? Without it the SARIF upload silently no-ops. - Note: CodeQL is free for public repos and included with GHAS for private. Mention this when recommending it for private repos.
Fuzzing
- Is there a fuzzing integration appropriate to the language? Flag absence as ❌ (Scorecard scores 0 for
Fuzzingwithout one). Important: Scorecard's recognition is narrow and language-specific; do not assume a property-based test suite counts.- Python:
import atherissomewhere in the repo. Hypothesis is not recognised by Scorecard for Python (unlike for Erlang / Haskell / Elixir / Gleam, where property-based testing is recognised). Add an Atheris harness alongside the Hypothesis suite — they catch different bug classes (Hypothesis = blind property; Atheris = coverage-guided libFuzzer). Pin Atheris in a hash-verifiedfuzz/requirements.txt(it can't go in the main dev group because wheels only ship for Linux x86_64 + Python ≤ 3.11). - Go:
func FuzzXxx(f *testing.F)targets, run as part of CI. - Rust:
cargo-fuzztargets infuzz/. - C/C++: ClusterFuzzLite (
.clusterfuzzlite/Dockerfile) or OSS-Fuzz (project listed in https://github.com/google/oss-fuzz/tree/master/projects). - Cross-language alternative:
.clusterfuzzlite/Dockerfileis recognised regardless of language and gives you actual continuous fuzzing infrastructure. Heavier setup but more value.
- Python:
- For parser-style code (anything consuming external file content, network input, user-controlled strings), the contract under test should be "parser must not crash on arbitrary input." Confirm tests cover the actual parsing surface, not trivial functions.
- Recommend running fuzz harnesses on a schedule (weekly is fine) rather than per-PR — coverage-guided fuzzing wants minutes-to-hours of runtime per harness, which is too slow for the critical PR path.
- Reference: https://github.com/ossf/scorecard/blob/main/docs/checks.md#fuzzing.
Interpreting Scorecard findings
When reviewing a repo's Scorecard score (Security tab → Code scanning → ScorecardID alerts), some findings are deliberately not actionable for legitimate reasons. Recognise and document these rather than chasing a green-only score:
Maintained< 90 days: a time-based finding for new repos. Auto-resolves; document and dismiss aswon't fixif the repo is genuinely active. No code change.Code-Review0/N approved changesets: Scorecard wants every PR approved by a different person. Conflicts with deliberate single-maintainer policies. If the user explicitly wants to merge their own PRs, document the trade-off inSECURITY.mdand dismiss withwon't fix+ comment.CII-Best-Practices0: requires self-certification at https://www.bestpractices.dev/. Cannot be automated; document as a roadmap item with the registration URL. Once awarded, add the badge to README.Pinned-Dependencies9 ("npmCommand not pinned by hash"): see CI tool installs section above; fix by switching tonpm ci --ignore-scriptsfrom a committed lockfile.Token-Permissions< 10: a workflow grants more thancontents: readwithout job-level overrides. Fix at the workflow level.Dangerous-Workflowfinding:pull_request_targetwith checkout-of-head, or expression injection. Treat as urgent — these are RCE-equivalent.
When dismissing a Scorecard alert, always set dismissed_reason: won't fix (the API only accepts false positive, won't fix, used in tests) and include a dismissed_comment linking to the documented justification in SECURITY.md. The comment must be ≤ 280 chars.
SECURITY.md and private vulnerability reporting
- Is there a
SECURITY.mdat the repo root? Flag absence as ❌. - Does it link to
https://github.com/<owner>/<repo>/security/advisories/new? - Does it include explicit "do not file public issues" wording?
- Does it state response-time expectations (acknowledgement + patch SLOs)?
- Is GitHub's private vulnerability reporting enabled? Check via
gh api repos/<owner>/<repo>/private-vulnerability-reporting --jq .enabled. Flagfalseas ❌. Enable withgh api -X PUT repos/<owner>/<repo>/private-vulnerability-reporting.
Scan to join WeChat group