
In March 2024, XZ Utils, a compression utility present on nearly every Linux server, was compromised by a backdoor hidden over two years. The attacker had earned the maintainer's trust, obtained commit rights, then injected malicious code allowing remote code execution through SSH. That attack captures the software supply chain dilemma perfectly: how do you trust dependencies maintained by strangers?
Supply chain attacks are exploding. According to Sonatype, they rose by 245%
in three years. SolarWinds, Event-Stream, Polyfill.io, tj-actions: every
incident shows that the popularity of a project does not guarantee its
security. An npm package with millions of downloads can be taken over by an
attacker. A GitHub action with thousands of stars can exfiltrate your secrets.
The fundamental problem: we trust blindly. We install hundreds of
dependencies without checking who maintains them, how they are built, whether
the releases are signed. We use GitHub actions with mutable tags (@v1) that
can change at any moment. We load scripts from third-party CDNs with no control.
OpenSSF Scorecard gives an objective answer to that question of trust. This tool, built by the Open Source Security Foundation, analyses a GitHub repository automatically and gives it a score from 0 to 10 based on concrete security criteria: branch protection, code reviews, signatures, known vulnerabilities, dangerous patterns in the workflows, and more.
What you will learn
- Understand what Scorecard evaluates and how to read the results
- Install and use the CLI against a public repository
- Wire Scorecard into a CI/CD pipeline, on GitHub and on GitLab
- Read the 18 checks that run by default, their weight, and the remediation of each
- Raise your score through a progressive action plan
What is OpenSSF Scorecard?
Scorecard is an automated tool measuring the security posture of an open source project. It runs a series of checks (heuristic controls) and produces a score for each of them.
The idea is simple: rather than trusting a dependency blindly, you objectively verify whether the project follows security good practices.
Why it is useful
A Scorecard score does not replace a code review: it answers a different question, and a far quicker one to ask. Does the project behind this dependency work cleanly? Four uses follow from that, from the most common to the most automated.
- Assessing a dependency before adoption: a score below 5 should raise a flag
- Auditing your own projects: identifying the weaknesses to fix
- Proving your security maturity: showing a score badge in your README
- Automating the monitoring: wiring Scorecard into the CI/CD
What Scorecard evaluates
Scorecard analyses varied aspects of a project's security:
| Category | Example checks |
|---|---|
| Code protection | Branch Protection, Code Review, Signed Releases |
| Build quality | Pinned Dependencies, CI Tests, Fuzzing |
| Maintenance | Active maintainers, response to vulnerabilities |
| Operational security | Dangerous Workflows, Token Permissions |
Every check returns a score from 0 to 10 with a reason explaining the result.
Installation
Scorecard can be installed in several ways depending on the environment.
# Get the latest available versionVERSION=$(curl -s https://api.github.com/repos/ossf/scorecard/releases/latest | grep tag_name | cut -d '"' -f 4)
# Download the releasewget "https://github.com/ossf/scorecard/releases/download/${VERSION}/scorecard_${VERSION#v}_linux_amd64.tar.gz"
# Extract and installtar xzf scorecard_*_linux_amd64.tar.gzsudo install scorecard /usr/local/bin/scorecard
# Check the installationscorecard version# Installation through Homebrewbrew install scorecard
# Check the installationscorecard version# Use the official image, pinned by digestdocker run -it \ gcr.io/openssf/scorecard:v5.5.0@sha256:b9a535801cb5fc8e7b2bea4cf45dd2db9a342cab8cc32bb5be2ac1355a95356f \ --repo=github.com/org/projectBasic usage
Configuring GitHub authentication
A GitHub token is required
Without a GitHub token, Scorecard hits the API limits very quickly (60 requests per hour). In practice, a token is indispensable to analyse a project.
Token type and permissions:
| Token type | Required scope | Limitation |
|---|---|---|
| Classic token | public_repo (or repo for private repositories) | Full access to every check |
| Fine-grained token | Contents: read, Metadata: read | Cannot read the classic branch protection rules |
Recommendation: prefer a ruleset and the problem disappears
The token constraint only applies to classic branch protection. Scorecard
then has to read settings that are not public, which requires a classic token
with the public_repo scope, or a fine-grained token carrying
Administration: Read. Without either, Branch-Protection returns -1.
With a ruleset, it needs none of those rights: rulesets are public, and
the GITHUB_TOKEN handed to every workflow by default is enough. That is
written in Scorecard's own code, and it is what its error message suggests.
Moving from classic protection to a ruleset therefore removes a secret to
create, store and rotate.
See the official documentation.
# Create a classic token at https://github.com/settings/tokens# Minimal scope: public_repo (or repo for private repositories)
# Export the tokenexport GITHUB_AUTH_TOKEN=ghp_xxxxxxxxxxxxAnalysing a GitHub project
Once the token is configured, the basic command analyses a repository:
# Analyse a public projectscorecard --repo=github.com/kubernetes/kubernetesReading the output
Scorecard prints a summary per check. Here is a real example on the Kubernetes project:
Aggregate score: 6.8 / 10
Check scores:|---------|------------------------|-----------------------------------------------------------------------|| SCORE | NAME | REASON ||---------|------------------------|-----------------------------------------------------------------------|| 10 / 10 | Binary-Artifacts | no binaries found in the repo || 10 / 10 | CI-Tests | 13 out of 13 merged PRs checked by a CI test || 5 / 10 | CII-Best-Practices | badge detected: Passing || 8 / 10 | Code-Review | Found 13/16 approved changesets || 10 / 10 | Contributors | project has 37 contributing companies or organizations || 0 / 10 | Dependency-Update-Tool | no update tool detected || 10 / 10 | Fuzzing | project is fuzzed || 10 / 10 | License | license file detected || 10 / 10 | Maintained | 30 commit(s) and 4 issue activity found in the last 90 days || 0 / 10 | Pinned-Dependencies | dependency not pinned by hash detected || 0 / 10 | SAST | SAST tool is not run on all commits || 10 / 10 | Security-Policy | security policy file detected || 8 / 10 | Vulnerabilities | 2 existing vulnerabilities detected ||---------|------------------------|-----------------------------------------------------------------------|Checks showing "?"
Some checks can show ? instead of a score. That means the check could not run
(insufficient data, token permissions, or a feature the project does not use).
Output formats
Scorecard supports several formats for integration with other tools:
# JSON format for automated processingscorecard --repo=github.com/org/project --format=json > scorecard.json
# SARIF format for GitHub Securityscorecard --repo=github.com/org/project --format=sarif > scorecard.sarifThe checks in detail
Scorecard carries about twenty checks, 18 of which run by default. Each check evaluates one security aspect and gives a score from 0 to 10. Here is the full list with the evaluation criteria and the actions that raise each score.
The overall score is a weighted average
The score you read is not the plain average of the checks: each one carries a weight from 1 to 10. Without that table you cannot tell where to start, and fixing a check worth 1 costs the same reading time as one worth 10 for a tenth of the effect.
| Weight | Checks |
|---|---|
| 10 | Binary-Artifacts, Branch-Protection, Code-Review, Dangerous-Workflow, Maintained, Signed-Releases, Token-Permissions, Vulnerabilities |
| 7.5 | Dependency-Update-Tool, Packaging, Pinned-Dependencies, SAST |
| 5 | CI-Tests, Contributors, Fuzzing, License |
| 1 | CII-Best-Practices, Security-Policy, Webhooks |
Two weights come as a surprise, and they change the order of priorities. Security-Policy weighs 1, although it naturally sits next to License which weighs 5. And Signed-Releases weighs 10, as much as Branch-Protection: signing your releases counts as much as protecting your main branch.
A check that was not evaluated is not a zero
The API returns -1 when a check simply could not be evaluated, for lack
of a release to inspect for Signed-Releases, or for lack of the rights needed to
read a classic branch protection. That -1 is excluded from the denominator:
it does not penalise the overall score.
The consequence is counter-intuitive and worth remembering: making a setting readable but wrong scores worse than leaving it unreadable. As long as a setting is not exposed, it stays out of the calculation. Once exposed and badly set, it enters the denominator and can break a tier. A half-configured ruleset can therefore score worse than no ruleset at all.
On ossf/scorecard, sigstore/cosign and kubernetes/kubernetes,
Branch-Protection does return -1 with the message "some github tokens can't
read classic branch protection rules". Those three projects are not poorly
protected: they are simply not measurable with a public token.
Critical checks (high risk)
Binary-Artifacts
Risk: unauditable code in the repository.
Checks the absence of executable binary files (.exe, .class, .pyc,
minified JS, and so on) in the source code. Binaries cannot be reviewed and may
carry malicious or obsolete code.
Score 10/10: no binary found in the repository.
Remediation: remove the binaries and generate them from the source code at build time.
Branch-Protection
Risk: malicious code injected with no control.
Checks that the main branches (main, master) are protected through the
GitHub protection rules.
The tiered scale:
| Points | Requirements |
|---|---|
| 3/10 | Force push and branch deletion forbidden |
| 6/10 | + Pull request mandatory, at least 1 reviewer required, branch up to date before merge, last push approved |
| 8/10 | + At least 1 required CI status check |
| 9/10 | + At least 2 reviewers, review by code owners with a CODEOWNERS file present |
| 10/10 | + Approvals dismissed after a new commit, admins included |
Those tiers are sequential, and that is the subtlety costing the most time. Scorecard builds the score tier by tier and stops at the first incomplete tier: everything earned above it counts for nothing at all. Reading the table as a partial sum, "I ticked two rows out of five, so I have part of the points", leads to a wrong conclusion.
Measured on a repository with a rich ruleset: leaving
require_last_push_approval at false is enough to block the 6/10 tier. The
score drops to 5/10, and two far more demanding settings already enabled,
code owner review and dismissal of stale approvals, earn nothing as long as
that single boolean is missing.
Remediation: configure a ruleset under Settings > Rules > Rulesets, rather than the classic rules under Settings > Branches. The reason is given further down, in the section about the authentication token: a ruleset is readable without administration rights.
Code-Review
Risk: vulnerabilities or malicious code going undetected.
Measures whether changes pass through human review before integration. It analyses the last 30 commits to check they were approved.
The scale:
- -7 points if a human change is not reviewed
- -3 additional points if several changes are not reviewed
- -3 points if bot changes are not reviewed
AI reviews
Reviews by bots (including AI/ML ones) do not count as code review, because they do not guarantee that a second person understood the change.
Remediation: make reviews mandatory through the branch protection rules.
Dangerous-Workflow
Risk: repository compromise (critical).
Detects dangerous patterns in GitHub Actions workflows:
- Untrusted checkout:
pull_request_targetorworkflow_runwith an explicit checkout of the pull request, which lets the pull request author run arbitrary code with the repository secrets - Script injection: unsanitised GitHub context variables in inline scripts
(for example
${{ github.event.issue.title }})
Score 10/10: no dangerous pattern detected.
Remediation: read the GitHub Security Lab guide and the Securing pull_request_target guide.
Signed-Releases
Risk: installing malicious releases.
Checks that releases are signed cryptographically. It analyses the last 5 releases for the presence of signature files.
Extensions looked for: .minisig, .asc (PGP), .sig, .sign,
.sigstore, .sigstore.json, .intoto.jsonl (SLSA).
The scale:
- 8/10: a signature present for every release
- 10/10: a SLSA attestation (
.intoto.jsonl) present
Remediation: sign your releases and publish provenance attestations; see GitHub Actions attestations.
Token-Permissions
Risk: tokens holding excessive permissions.
Checks that GitHub Actions workflows follow the principle of least privilege for
the GITHUB_TOKEN.
The optimal score: permissions set to read-only at the global level, and write permissions declared only on the jobs that need them.
# ✅ Good practicepermissions: contents: read # Read-only by default
jobs: deploy: permissions: contents: write # Write for this job onlyRemediation: use StepSecurity to work out the minimal permissions needed, and see GITHUB_TOKEN permissions.
Vulnerabilities
Risk: known, exploitable vulnerabilities.
Checks through OSV (Open Source Vulnerabilities) whether the project or its dependencies carry open, unfixed vulnerabilities.
The score: depends on the number and the severity of the vulnerabilities detected.
Remediation: fix the vulnerabilities in the code or update the dependencies to non-vulnerable versions.
Build quality checks
CI-Tests
Risk: bugs and vulnerabilities going undetected.
Checks whether the project runs automated tests before merging pull requests. It analyses the last 30 commits for the presence of CI checks.
Systems detected: GitHub Actions, Travis CI, CircleCI, Jenkins, AppVeyor, BuildKite, Woodpecker, and any system whose name contains "test" or "e2e".
Score 10/10: every recent commit went through CI tests.
Remediation: add tests with GitHub Actions or another CI system.
Dependency-Update-Tool
Risk: outdated dependencies carrying known vulnerabilities.
Checks whether the project uses an automatic dependency update tool.
Tools detected:
- Dependabot
- Renovate
- PyUp (Python)
Score 10/10: a configuration file for one of those tools is detected.
Remediation: enable Dependabot in the GitHub settings or configure Renovate at the root of the project.
Fuzzing
Risk: undiscovered vulnerabilities in the code.
Checks whether the project uses fuzzing (testing with random data) to find bugs.
Methods detected:
- Inclusion in OSS-Fuzz
- ClusterFuzzLite in the repository
- Native Go fuzz tests (
func FuzzXxx) - Property-based testing libraries (QuickCheck, fast-check, FsCheck, and so on)
Remediation: add the project to OSS-Fuzz or use the language's native fuzzing.
Pinned-Dependencies
Risk: compromised dependencies (a supply chain attack).
Checks that dependencies are pinned to hashes rather than to mutable versions or tags.
What is analysed:
- GitHub actions (they must use a SHA, not
@v1) - Docker images (they must use a digest)
- Dependencies in shell scripts and Dockerfiles
# ❌ A mutable tag: it can change without notice- uses: actions/checkout@v4
# ✅ A pinned SHA: immutable, with the version in a comment- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1Remediation: use StepSecurity
to pin the actions automatically, and Renovate with pinDigests: true for the
Docker images. The procedure is detailed in
Pinning actions by SHA.
SAST
Risk: bugs and vulnerabilities in the source code.
Checks whether the project runs static application security testing (SAST) on pull requests.
Tools detected:
- CodeQL (github-code-scanning)
- SonarCloud
- The
github/codeql-actionaction in the workflows
Remediation: enable CodeQL under Settings > Security > Code scanning or
add the github/codeql-action action to a workflow.
Governance checks
CII-Best-Practices
Risk: security good practices not followed.
Checks whether the project earned an OpenSSF Best Practices badge.
The scale:
- 2/10: badge in progress
- 5/10: "Passing" badge
- 7/10: "Silver" badge
- 10/10: "Gold" badge
Remediation: register on bestpractices.dev and fill in the questionnaire. That questionnaire holds 67 criteria at the passing level: the shortcuts are detailed in Earning the OpenSSF Best Practices badge without losing a day.
Contributors
Risk: a project depending on a single organisation.
Checks whether the project has contributors from several different organisations (based on the "Company" field of the GitHub profiles).
Score 10/10: at least 3 different organisations among the last 30 commits, with at least 5 commits each.
Remediation: encourage contributors to fill in their organisation in their GitHub profile.
License
Risk: an obstacle to security auditing, and a legal risk.
Checks for the presence of a licence in the repository.
The scale:
- 6/10: a
LICENSE,COPYINGorCOPYRIGHTfile detected - 9/10: + the file at the root of the project
- 10/10: + a licence recognised by the FSF or OSI (an SPDX identifier)
Remediation: add a LICENSE file at the root with an
SPDX licence.
Maintained
Risk: unfixed vulnerabilities in an abandoned project.
Evaluates the recent activity of the project over the last 90 days.
Score 10/10: at least 1 commit per week over the last 90 days.
Partial score: issue activity from collaborators or maintainers.
Score 0: an archived project.
Careful
An inactive project can be the target of a maintainer takeover, an attacker regaining control to inject malicious code (see XZ Utils).
Security-Policy
Risk: insecure reporting of vulnerabilities.
Checks for the presence of a SECURITY.md file explaining how to report
vulnerabilities responsibly.
The scale:
- 6/10: a contact email or URL present
- 9/10: + explanatory text beyond the bare links
- 10/10: + disclosure timelines mentioned (for example "90 days")
Remediation: create a SECURITY.md file at the root with the reporting
instructions. On GitHub, use Settings > Security > Security advisories.
Other checks
Packaging
Checks whether the project publishes packages through GitHub Packages or publication actions towards npm, PyPI and others.
SBOM (experimental)
Checks for the presence of an SBOM in the source code or in the release artefacts.
This check does not run by default: it appears neither in the CLI output nor
in the securityscorecards.dev API response. A reader looking for their SBOM
score will not find it, and that is not a configuration mistake.
Webhooks (experimental)
Checks that the configured webhooks use a secret to authenticate the requests.
This one does not run by default either, and its weight is 1 when it does. These are the two checks explaining the gap between the twenty described here and the 18 checks the API returns.
CI/CD integration
GitHub Actions
The native integration runs Scorecard automatically and publishes the results in the GitHub Security tab:
name: Scorecard Analysis
on: push: branches: [ "main" ] schedule: # Weekly analysis on Sunday at midnight - cron: '0 0 * * 0'
permissions: # Needed to publish the results security-events: write id-token: write
jobs: analysis: name: Scorecard analysis runs-on: ubuntu-24.04
steps: - name: Checkout code uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false
- name: Run analysis uses: ossf/scorecard-action@2d1146689b8cda280b9bc96326124645441f03bc # v2.4.4 with: results_file: results.sarif results_format: sarif publish_results: true
- name: Upload to code-scanning uses: github/codeql-action/upload-sarif@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4.37.3 with: sarif_file: results.sarifPublishing the results
The publish_results: true option shows the score on
securityscorecards.dev and gives you a badge
for the README.
GitLab CI
On GitLab, you use the CLI directly:
scorecard: stage: security image: gcr.io/openssf/scorecard:v5.5.0@sha256:b9a535801cb5fc8e7b2bea4cf45dd2db9a342cab8cc32bb5be2ac1355a95356f variables: GITHUB_AUTH_TOKEN: $GITLAB_TOKEN script: - scorecard --repo=gitlab.com/$CI_PROJECT_PATH --format=json > scorecard.json artifacts: reports: codequality: scorecard.json paths: - scorecard.json rules: - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCHShowing the score badge
Once the results are published, you can add a badge to the README to show the hardening effort:
The badge updates automatically after each analysis.
Raising your score
Start by reading your report, not this list. The optimal order depends on where you start from, and several of these steps may earn nothing at all because the check already sits at its maximum. Measured on five real repositories, steps 2, 3 and 5 were already at 10/10 while the overall score was stuck at 6.9. So cross your report with the weight table given above: a check worth 10 stuck at 3 matters ten times more than a check worth 1 stuck at 0.
Here is the action plan, in generally decreasing order of gain:
-
Enable branch protection
In the GitHub settings, configure:
- Mandatory review by at least 1 person
- Required CI statuses before merge
- Force push forbidden
-
Pin the dependencies
Replace every mutable tag with a SHA in the GitHub Actions workflows and the Dockerfiles.
-
Sign the releases
Sign the artefacts and publish provenance attestations.
-
Enable Dependabot or Renovate
Configure automatic dependency updates to receive the security fixes.
-
Audit the workflows
Check there is no dangerous pattern and minimise the token permissions.
Earning the OpenSSF Best Practices badge without losing a day
CII-Best-Practices is the one check that no repository setting can satisfy.
It reads a record hosted on bestpractices.dev, and that record is filled in by
hand: 67 criteria at the passing level, 43 of them mandatory, as
counted in the project's criteria/criteria.yml. This is the check whose
remediation fits in one sentence and takes half a day. Two official mechanisms
cut that cost, and neither appears on the service's landing page. The BadgeApp
documentation is nonetheless explicit about the order of preference:
We do support POST for changing project data, but we generally recommend using automation proposals URLs or the
.bestpractices.jsonfile instead.
Pre-filling with .bestpractices.json
Placed at the repository root, or as .project.d/bestpractices.json, this
file is read by the service to pre-fill the answers. Each criterion takes
two keys, <criterion>_status and <criterion>_justification. The status
accepts ?, N/A, Unmet or Met, and a ? means "I do not know": it is
ignored entirely, so a partial file breaks nothing and can be completed
over time.
{ "release_notes_status": "Met", "release_notes_justification": "Every version publishes its notes on the repository Releases page", "floss_license_osi_status": "Unmet", "floss_license_osi_justification": "The content is under CC BY-SA 4.0, a free licence but not OSI-approved", "warnings_status": "?"}Pre-filling through a proposal URL
An edit URL carries the answers in its parameters. The service shows them highlighted in the form, a human reviews, then saves:
https://www.bestpractices.dev/en/projects/<ID>/passing/edit?release_notes_status=Met&release_notes_justification=...Three rules decide the outcome, and none of them is obvious:
| Rule | What happens otherwise |
|---|---|
| The record must already exist | An edit URL aimed at an unregistered repository answers 404. Looking a project up by URL creates nothing. |
| Criterion names use underscores, never dots or brackets | The key is dropped silently, with no error message at all. |
Without the overrides parameter, only the still unknown fields are filled | On a field that already holds a value, the proposal flags a disagreement and changes nothing. |
Everything travels in the query string, whose length the server bounds: keep the proposal URL for a handful of criteria, and use the file for the full questionnaire.
Two traps in their own documentation
Each of these costs half an hour to whoever meets it alone. From docs/api.md,
the project links to a file called basepractices-json.md that does not
exist: the real name is bestpractices-json.md. And the sample file
docs/self.json still carries description_sufficient and
contribution_criteria, two identifiers removed from the criteria set.
Since an outdated key is ignored without warning, a file copied from that
sample half works, and nothing says so.
What caps the badge, and what does not
Two facts save a long search for a mistake that never happened. The
release_notes criterion is mandatory: without a published release, the
passing badge stays out of reach whatever the other answers say. On the other
hand floss_license_osi, which a Creative Commons licence cannot satisfy
since it is not OSI-approved, is only suggested: answering Unmet with a
justification is enough, and the badge remains reachable. That nuance matters
for a content repository or a lab catalogue. One last thing to plan for, the
questionnaire and its justifications are written in English, and other
people will read them.
Three checks out of reach for a lone maintainer
A well-kept personal project structurally tops out around 7/10, and knowing why saves a long search for a mistake you did not make. Three checks, all of high weight, depend on the size of the team rather than on the maintainer's rigour.
| Check | Why it resists |
|---|---|
| Code-Review | It counts approved changesets. Overriding your own protection as an administrator creates no approval, and nobody else reviews. |
| Maintained | It penalises any repository younger than 90 days, whatever its activity. Time is the only remedy. |
| Contributors | It counts distinct organisations among the contributors, not people. A single-author repository cannot climb. |
Those three are absent from the plan above, and deliberately so: no setting unlocks them.
Scorecard in a DevSecOps strategy
Scorecard fits naturally into a wider supply chain security approach. It checks the development practices upstream, while the scanners detect the vulnerabilities in the artefacts produced. The combination gives you defence in depth: a repository can hold no known CVE and still be one stolen maintainer account away from compromise, which is exactly what Scorecard measures and a scanner does not.
Key points
- Scorecard gives an objective answer to the question of trusting an open source project, in seconds.
- Analyse your critical dependencies before adoption: a score below 5 should raise a flag.
- Wire Scorecard into your CI/CD to watch your own projects over time, with a weekly schedule.
- The three checks that move the needle fastest are Branch-Protection, Pinned-Dependencies and Token-Permissions.
- Show the badge to make the security effort visible, and combine Scorecard with artefact scanners for defence in depth.
- A good Scorecard score does not guarantee the absence of vulnerabilities, but it significantly cuts the risk of a maintainer takeover and of supply chain attacks like the XZ Utils one.
Next steps
- Pinning actions by SHA: the fix for the Pinned-Dependencies check, the most frequently failed one.
- GITHUB_TOKEN permissions: what the Token-Permissions check expects, job by job.
- Security checklist: the recap that turns the score into concrete actions on your workflows.
Resources
- OpenSSF Scorecard, the official documentation
- GitHub ossf/scorecard
- The Scorecard GitHub Action
- Scorecard Monitor, multi-project tracking
- Open Source Malware, a community database of malicious packages (npm, PyPI, NuGet)