diff --git a/AGENTS.md b/AGENTS.md index 7cf118a..33309ac 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -88,7 +88,7 @@ Not applicable: One badge criterion was considered during the 2026-07-30 Passing alignment and declined, so nobody re-opens it as an oversight. `documentation_interface` asks a project to provide reference documentation describing the external interface of what it produces. Upstream it is a MUST with `na_allowed` and a justification required, so a project may mark it not applicable. No rule was written for it, for three reasons. Any check this project could write reduces to a file that looks like API documentation existing, which is unobservable in the way `STANDARD.md` requires. R-CHG-02 already forces the public API to be declared, which is the half a reader needs to know what is covered. And no skill in the kit owns reference documentation, so a rule would have no `Fixed by:` line to name. -R-COM-08 is met by the Governance section of `CONTRIBUTING.md`: one maintainer decides and releases, only the maintainer has write access so every contribution arrives from a fork, and no succession is arranged. The rule is deliberately satisfiable by a solo project stating that it is one, so the section says that plainly rather than inventing a governance structure. It also records the one decision that is not a judgement call, that a rule changes when there is an upstream source for the change. R-COM-07 is met: the description, the homepage, and eight topics are set, all three readable with `gh repo view --json description,homepageUrl,repositoryTopics`. They are forge settings, so no file records them and no diff shows them changing. The social preview image is still unset, and R-COM-07 does not ask for one. +R-COM-08 is met by the Governance section of `CONTRIBUTING.md`: one maintainer decides and releases, only the maintainer has write access so every contribution arrives from a fork, and no succession is arranged. The rule is deliberately satisfiable by a solo project stating that it is one, so the section says that plainly rather than inventing a governance structure. It also records the one decision that is not a judgement call, that a rule changes when there is an upstream source for the change. R-COM-07 is met: the description, the homepage, and eight topics are set, all three readable with `gh repo view --json description,homepageUrl,repositoryTopics`. They are forge settings, so no file records them and no diff shows them changing. The social preview image is set, and R-COM-07 does not ask for one. R-CHG-03 was pending, until 0.2.0 was tagged: three plugin manifests and the newest `CHANGELOG.md` heading said 0.2.0 while the newest tag said `v0.1.0`. Because nothing had ever consumed 0.2.0, the fix folded the accumulated `Unreleased` section into it and dated it at the tag, rather than tagging an old commit and opening 0.3.0. Folding a release that never shipped means dropping every entry that describes a change to something introduced in the same release: the site landed in 0.2.0, so the entries tuning its page titles, sidebar, and search index described a state no user ever saw. @@ -96,11 +96,11 @@ The public API that R-CHG-02 requires is declared in the README under Versioning `CHANGELOG.md` pins Keep a Changelog 2.0.0, which was published on 2026-06-07 and is what `oss-changelog` prescribes. The file used to pin 1.1.0. -R-SEC-05 is met: `git cat-file -t v0.2.0` prints `tag`, and `git tag -v v0.2.0` reports a good SSH signature for the ED25519 key published at `gh api users/svyatov/ssh_signing_keys`. Verification needs `gpg.ssh.allowedSignersFile` pointed at an allowed-signers file naming that key with the tagger's email; without it `git tag -v` fails with a configuration error rather than a bad signature, which reads like an unsigned tag and is not one. The rule now carries both the fetch command and that distinction, so an audit on a fresh checkout can resolve it instead of reporting unknown. +R-SEC-05 is met: `git cat-file -t v0.13.0` prints `tag`, and `git tag -v v0.13.0` reports a good SSH signature for the ED25519 key published at `gh api users/svyatov/ssh_signing_keys`. Verification needs `gpg.ssh.allowedSignersFile` pointed at an allowed-signers file naming that key with the tagger's email; without it `git tag -v` fails with a configuration error rather than a bad signature, which reads like an unsigned tag and is not one. The rule now carries both the fetch command and that distinction, so an audit on a fresh checkout can resolve it instead of reporting unknown. -R-SEC-04 and R-SEC-09 were both pending on the repository becoming public. It is public now, and both are set. The default branch is guarded by a repository ruleset named `main`, scoped to `~DEFAULT_BRANCH` rather than the literal branch name so renaming the branch cannot unguard it. It requires a pull request, resolved review threads, squash as the only merge method, and the four checks CI reports, and it rejects force pushes, deletion, and non-linear history. It requires no approving review, because R-SEC-12 does not reach a repository with one principal and R-SEC-04 no longer asks for one. Expect `Branch-Protection` in an OpenSSF Scorecard result to read 3 of 10 for that reason: its tiers gate each other, review is tier 2, and status checks are tier 3. That is the intended state, not a regression. Read the ruleset with `gh api repos/svyatov/oss-kit/rulesets`, not with the classic `branches/main/protection` endpoint: this repository has no classic branch protection rule, so that endpoint answers `404 Branch not protected`, which reads like an unguarded branch and is not one. Code scanning runs through CodeQL default setup rather than a workflow file, scanning `actions`, `javascript`, `javascript-typescript`, and `typescript`, so nothing under `.github/workflows/` implements it and nothing there needs maintaining for it. +R-SEC-04 is met, and R-SEC-09 is open for fork pull requests. The default branch is guarded by a repository ruleset named `main`, scoped to `~DEFAULT_BRANCH` rather than the literal branch name so renaming the branch cannot unguard it. It requires a pull request, resolved review threads, squash as the only merge method, and the four checks CI reports, and it rejects force pushes, deletion, and non-linear history. It requires no approving review, because R-SEC-12 does not reach a repository with one principal and R-SEC-04 no longer asks for one. Expect `Branch-Protection` in an OpenSSF Scorecard result to read 3 of 10 for that reason: its tiers gate each other, review is tier 2, and status checks are tier 3. That is the intended state, not a regression. Read the ruleset with `gh api repos/svyatov/oss-kit/rulesets`, not with the classic `branches/main/protection` endpoint: this repository has no classic branch protection rule, so that endpoint answers `404 Branch not protected`, which reads like an unguarded branch and is not one. Code scanning runs through CodeQL default setup rather than a workflow file, scanning `actions`, `javascript`, `javascript-typescript`, and `typescript`, so nothing under `.github/workflows/` implements it. GitHub excludes fork pull requests from default setup, while this repository routes every external contribution through a fork. Default setup therefore does not meet R-SEC-09 on that path. -The ruleset also carries a `code_scanning` rule requiring CodeQL at `security_alerts_threshold: high_or_higher` and `alerts_threshold: errors`, and a `code_quality` rule at `severity: errors`. These block on what the analysis found, where the `CodeQL` entry in `required_status_checks` only blocks on whether it reported. The `code_quality` rule is the one to watch: GitHub Code Quality is a licensed product enabled per repository, it is absent from the rulesets REST reference, and it exposes no endpoint that reports whether it is on for this repository, so `gh api repos/svyatov/oss-kit/code-quality` answering `Not Found` says nothing either way. If a pull request ever sits unmergeable on a code quality result that never arrives, that rule is the first thing to check. +The ruleset also carries a `code_scanning` rule requiring CodeQL at `security_alerts_threshold: high_or_higher` and `alerts_threshold: errors`, and a `code_quality` rule at `severity: errors`. These block on what the analysis found, where the `CodeQL` entry in `required_status_checks` only blocks on whether it reported. The `code_quality` rule is a live gap. GitHub now documents `GET /repos/{owner}/{repo}/code-quality/setup`, and the read reports that Code Quality is unavailable for this repository. Remove the rule from `main` before relying on it as a merge gate. A ruleset has no "Do not allow bypassing the above settings" checkbox; the equivalent is its bypass list, and this one is empty. Nothing here exempts the owner from anything. It used to hold `RepositoryRole` `5`, Repository admin, at `pull_request` mode, which existed for one reason: the ruleset required an approving review, nobody can approve their own pull request, and without the exemption every change the sole maintainer opened was unmergeable. Dropping the review requirement removed the reason, so the entry went too, and the ruleset now binds the owner as tightly as anybody else. Confirm it from the create or read response, where `bypass_actors` reads `[]` and `current_user_can_bypass` reads `never`. Do not restore the entry to unblock a merge; a required approval nobody can give, undone by an exemption for the only person it named, is strictly weaker than not requiring it. @@ -110,7 +110,7 @@ R-SEC-13 is met by two more rulesets on `refs/tags/v*`, read back with `gh api r R-SEC-10 is met, read back rather than inferred from the write: `gh api repos/svyatov/oss-kit --jq .security_and_analysis` reports `secret_scanning` and `secret_scanning_push_protection` both `enabled`. The same response reports `secret_scanning_non_provider_patterns` and `secret_scanning_validity_checks` disabled, which is correct rather than a gap, because both belong to GitHub Secret Protection, a paid product the rule's `Check` places outside itself. Read that field with an admin token. For a caller without admin the key is absent from the response entirely, which reads as disabled and is not. -R-SEC-11 is met on coverage and on reporting, and open on blocking. `gh api repos/svyatov/oss-kit/vulnerability-alerts -i` answers 204 and `dependabot_security_updates` reads `enabled`, but the dependency graph does not parse `bun.lock`, so `gh api repos/svyatov/oss-kit/dependency-graph/sbom` lists 10 `pkg:npm` packages against the 492 that `site/bun.lock` resolves, alongside 8 `pkg:githubactions` and 1 `pkg:github`. The `osv-scanner` workflow covers that residual: its `pull-request` job scans what the change introduces, and its `scheduled` job scans the repository weekly and reports. +R-SEC-11 is met on coverage and on reporting, and open on blocking. `gh api repos/svyatov/oss-kit/vulnerability-alerts -i` answers 204 and `dependabot_security_updates` reads `enabled`, but the dependency graph does not parse `bun.lock`, so `gh api repos/svyatov/oss-kit/dependency-graph/sbom` lists 10 `pkg:npm` packages against the 469 that `site/bun.lock` resolves, alongside 8 `pkg:githubactions` and 1 `pkg:github`. The `osv-scanner` workflow covers that residual: its `pull-request` job scans what the change introduces, and its `scheduled` job scans the repository weekly and reports. What is open is the blocking half. The `main` ruleset requires four checks and `pull-request / osv-scan` is not among them, so the scan reports today and does not gate. It stays that way until a pull request from a fork runs it green, because a fork gets a read-only token while the called workflow declares `security-events: write` at its own top level. If GitHub compares the fork-capped grant rather than the declared one, that job fails at startup, produces no check run at all, and a required check that never reports blocks the merge with nothing to read. One green fork run settles it, and adding the `pull-request / osv-scan` context to the ruleset's `required_status_checks` closes the rule. Add it there and not to `code_scanning_tools`, which holds CodeQL and takes analysis tools rather than check contexts. diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ffc352..36920df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ The format is based on [Keep a Changelog 2.0.0](https://keepachangelog.com/en/2. ## [Unreleased] +### Fixed + +- `oss-harden` pins the `uses:` entries a shipped composite action carries, not the workflow lines alone. A repository whose `action.yml` names an unpinned action passed Step 3 with a mutable reference left in place. +- `oss-harden` says a Dependabot cooldown below three days shortens the delay. Step 5 said such a value replaced the platform default with something slower, which is the reverse of what it does. +- `oss-harden` sends a repository that receives fork pull requests to CodeQL advanced setup. GitHub excludes fork pull requests from default setup, so recommending it left R-SEC-09 open on the path every external contribution takes. + ## [0.13.0] - 2026-08-07 ### Changed diff --git a/skills/oss-harden/SKILL.md b/skills/oss-harden/SKILL.md index b6a1a66..d2efa58 100644 --- a/skills/oss-harden/SKILL.md +++ b/skills/oss-harden/SKILL.md @@ -50,6 +50,8 @@ With that settled, read what already exists before changing anything, using the Every workflow or pipeline file, so the `uses:` lines, `image:` and `include:` entries, and `permissions:` blocks that need fixing are known before any edit is proposed. +Every `action.yml` or `action.yaml` the repository ships, so composite action `uses:` entries are also known. + Every package manifest and lockfile the repository holds, including the ones under a documentation, example, or tooling directory and the ones nothing published depends on. The axis here is what is present, not what the project ships: a vulnerability arrives through any dependency the repository resolves, whether or not that dependency reaches a released artifact, and a repository that publishes nothing at all still resolves its own. Steps 5, 10, 11, and 12 all consume this list; establishing it once here is what stops each of them deriving its own. A repository with no registry dependencies has an empty list, which is an answer rather than a gap. Read the file for each ecosystem on that list before configuring anything for it. Each one carries the updater's ecosystem key and whether security updates exist for it, the lockfile and the command that installs from it frozen, the analyzer the language has, and what watches it for known vulnerabilities: @@ -74,9 +76,11 @@ The current branch or merge request protection settings for the default branch, Whether the newest release tag is signed, if a release has shipped yet; a repository with no tags yet has nothing to check here, so say that plainly instead of treating it as a gap. +Verified 2026-08-07 against [GitHub repository REST endpoints](https://docs.github.com/en/rest/repos/repos), [GitLab personal access tokens](https://docs.gitlab.com/user/profile/personal_access_tokens/), [GitLab job token scope API](https://docs.gitlab.com/api/project_job_token_scopes/), [GitLab project security settings API](https://docs.gitlab.com/api/project_security_settings/), and the current sources named in `references/ecosystems/`. + ### Step 3: Pin third-party references to immutable content -On GitHub, every external `uses:` line in every workflow should resolve to a full 40-character commit SHA, including GitHub-owned actions and reusable workflows outside the current repository. Preserve the selected version in a trailing comment so a human and the updater can still read it. On GitLab, there is no action-shaped `uses:` step; the same mutable-reference problem arrives through `image:`, `services:`, and external `include:` entries, so pin those instead, per R-SEC-06. Resolve the version the configuration already selects unless the user separately authorizes an upgrade. Check the newest compatible and newest major releases and report lag, but do not combine a security pin with a potentially breaking upgrade. The reference file for the detected forge gives the exact resolution process and the caveat that an annotated tag needs one extra step most naive lookups miss. +On GitHub, every external `uses:` line in every workflow and shipped composite action should resolve to a full 40-character commit SHA, including GitHub-owned actions and reusable workflows outside the current repository. Preserve the selected version in a trailing comment so a human and the updater can still read it. On GitLab, there is no action-shaped `uses:` step; the same mutable-reference problem arrives through `image:`, `services:`, and external `include:` entries, so pin those instead, per R-SEC-06. Resolve the version the configuration already selects unless the user separately authorizes an upgrade. Check the newest compatible and newest major releases and report lag, but do not combine a security pin with a potentially breaking upgrade. The reference file for the detected forge gives the exact resolution process and the caveat that an annotated tag needs one extra step most naive lookups miss. `scripts/resolve-pin.mjs` does that resolution. Pipe the workflow through it, or pass the references directly: @@ -99,10 +103,12 @@ On GitHub, every workflow needs a top-level `permissions:` block granting no mor Pinning freezes a reference until something unfreezes it; R-SEC-03 requires an updater covering both the project's application dependencies and its CI dependencies, on both forges. On GitHub, Dependabot's `github-actions` ecosystem is what proposes the SHA and comment update for every workflow this skill just pinned, so a `dependabot.yml` this skill writes or edits needs that ecosystem alongside one entry for every ecosystem the manifests read in Step 2 declare; a `dependabot.yml` that updates the package manager but leaves out `github-actions` re-freezes the actions the moment they are pinned. On GitLab, there is no equivalent of Dependabot shipped by the platform; the reference file names the documented alternative and what running it costs. Where a Renovate configuration already exists on either forge, extend its `packageRules` or presets rather than adding a competing Dependabot config; ask before replacing one updater with the other. -The same configuration answers R-SEC-14, which asks the updater to wait before proposing a version that was published minutes ago. Add a `cooldown` block with `default-days` to `dependabot.yml`, or `minimumReleaseAge` to a Renovate configuration, and set it in days rather than hours: the window this closes is the one between a compromised publish and somebody noticing, which the incidents it exists for have taken hours to close. The two updaters start from different places, so say which one the user is on. Renovate waits zero days until the key is set, and that repository has the window open today. Dependabot already delays a version update by three days on its own, so writing the number pins a value the platform chose and can change again, and gives the next reader a decision to audit. Write at least that three days there, because a smaller number replaces the default with something slower. Then check what the setting reaches: a Dependabot `include` or `exclude` list and a Renovate `packageRules` override both leave a configuration that reads as set and covers little. The reference files carry the resolution rules for each. Neither setting delays a fix for a known vulnerability, so say that when the user asks what it costs them. Where the repository has no updater at all, R-SEC-03 is the finding and this rule is not a second one. +The same configuration answers R-SEC-14, which asks the updater to wait before proposing a version that was published minutes ago. Add a `cooldown` block with `default-days` to `dependabot.yml`, or `minimumReleaseAge` to a Renovate configuration, and set it in days rather than hours: the window this closes is the one between a compromised publish and somebody noticing, which the incidents it exists for have taken hours to close. The two updaters start from different places, so say which one the user is on. Renovate waits zero days until the key is set, and that repository has the window open today. Dependabot already delays a version update by three days on its own, so writing the number pins a value the platform chose and can change again, and gives the next reader a decision to audit. Write at least that three days there, because a smaller number shortens the delay. Then check what the setting reaches: a Dependabot `include` or `exclude` list and a Renovate `packageRules` override both leave a configuration that reads as set and covers little. The reference files carry the resolution rules for each. Neither setting delays a fix for a known vulnerability, so say that when the user asks what it costs them. Where the repository has no updater at all, R-SEC-03 is the finding and this rule is not a second one. R-SEC-15 belongs to the same step because it is the other half of what an updater's pull request does to a runner. Read every CI step that installs registry dependencies, and check that it either declines dependency-supplied install code or that the repository commits the allowlist its package manager reads. The ecosystem reference gives the flag or the field for the manager in use, and names the ecosystems where no such control exists, which is most of them; where the rule falls outside, say so with the ecosystem named rather than reporting a gap the manager gives nobody a way to close. Where it does apply and the manager now blocks that code by default, write the flag or the allowlist anyway. A default that depends on which version of the manager the runner happens to ship is not evidence in a workflow file, which is the same reason R-SEC-08 asks for the frozen-install flag Yarn and pnpm already default to in CI. +Verified 2026-08-07 against [GitHub Actions metadata syntax](https://docs.github.com/en/actions/reference/workflows-and-actions/metadata-syntax), [GitHub secure use reference](https://docs.github.com/en/actions/reference/security/secure-use), [Dependabot options](https://docs.github.com/en/code-security/reference/supply-chain-security/dependabot-options-reference), [Dependabot cooldown guidance](https://docs.github.com/en/code-security/tutorials/secure-your-dependencies/optimizing-pr-creation-version-updates), [Renovate configuration options](https://docs.renovatebot.com/configuration-options/), and the current sources named in `references/ecosystems/`. The two GitHub Dependabot pages conflict on the unset cooldown default. + ### Step 6: Branch protection and required review R-SEC-04 requires the default branch to take changes only through a change request, require at least one CI status check to pass, and reject force pushes and deletion, on both forges. This is a forge setting, not a file, so give the user the resolved settings URL for the repository or project found in Step 1, wait for confirmation, then verify with the read command from Step 2. On GitHub, set this up as a repository ruleset; classic branch protection is the older form, still enforced where it already exists and worth reading, but not what to create today. Every control in this paragraph works without a second person, so set all of them whatever the repository's size. @@ -129,10 +135,14 @@ R-SEC-05 applies the same way on both forges, because the evidence comes from gi R-SEC-13 is the other half of the same surface, and unlike R-SEC-05 it is a forge setting rather than evidence git can produce: a signature says who cut the tag, and nothing in git stops the same person or anyone else with push access repointing it afterwards. Both forges express it as a separate object from the branch protection Step 6 set, so read it separately and do not report a guarded default branch as covering tags. On GitHub it is a ruleset targeting tags; on GitLab it is protected tags. Take the settings-block pattern from the Principles section for whichever is missing, and read the result back with the command in the matching reference file. Where releases are cut by a workflow rather than a person, the principal permitted to create a tag is that workflow's identity, so establish who or what publishes before naming anyone. A repository with no release tag yet still takes this control, because setting it before the first release is what makes the first release immutable. +Verified 2026-08-07 against [GitHub rules REST endpoints](https://docs.github.com/en/rest/repos/rules), [GitHub ruleset rules](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-rulesets/available-rules-for-rulesets), [Git tag](https://git-scm.com/docs/git-tag), [Git configuration](https://git-scm.com/docs/git-config), [GitLab merge request approvals](https://docs.gitlab.com/user/project/merge_requests/approvals/), and [GitLab protected tags](https://docs.gitlab.com/user/project/protected_tags/). + ### Step 9: Keep untrusted input out of privileged contexts R-SEC-07 applies to both forges through different mechanisms, so read the matching reference file rather than carrying the syntax across. On GitHub the risk is textual: a `${{ }}` expression is substituted into the script before any shell sees it, so `${{ github.event.issue.title }}` inside a `run:` block executes whatever its author wrote. Move user-controlled values into an `env:` block and quote the environment variable in the shell. Separately, a `pull_request_target` or `workflow_run` workflow runs with the base repository's privileges, so it must not execute or check out contributor-controlled code; where one does, that is the highest-severity finding this skill produces and it belongs at the top of the summary. On GitLab, quote every user-controlled predefined variable in `script:` and pass values as arguments rather than evaluating generated shell. Store secrets as masked and hidden variables, protect them when only protected refs need them, and keep variable-reference expansion disabled. +Verified 2026-08-07 against [GitHub script injections](https://docs.github.com/en/actions/concepts/security/script-injections), [GitHub `pull_request_target` security](https://docs.github.com/en/actions/reference/security/securely-using-pull_request_target), and [GitLab CI/CD variables](https://docs.gitlab.com/ci/variables/). + ### Step 10: Lockfile and frozen installs R-SEC-08 requires the package manager's lockfile to be committed and CI to install from it in a mode that fails rather than re-resolves. Working from the manifests Step 2 read, confirm each lockfile is committed rather than gitignored, and verify the current frozen-install command in that package manager's official documentation before changing CI. Examples include `npm ci`, `uv sync --locked`, `cargo build --locked`, `mix deps.get --check-locked`, Bundler with local `deployment` and `frozen` configuration, and `pip install --require-hashes` only when every requirement, including transitive dependencies, carries a hash. Where the project pins versions in a manifest but commits no lockfile, say that a version pin still trusts the registry to serve the same bytes while a recorded hash does not. A repository with no registry dependencies has nothing to lock; report that rather than a gap. @@ -149,7 +159,7 @@ Every reference file answers all three, under a heading naming what breaks after ### Step 11: Static analysis on pull requests -R-SEC-09 applies only where the repository holds source in a language a static analyzer supports, so establish that first and say plainly that the rule does not reach the repository when it does not, rather than reporting a violation. Where it does apply, check that an analysis workflow runs on pull requests to the default branch and that its result is a required check, since an analyzer whose failure does not block merge is advisory. On GitHub, prefer CodeQL default setup, which is a repository setting rather than a workflow file the project has to maintain, and give the resolved settings URL the same way Step 6 does; the reference file names the endpoint that reports whether it is already enabled. Where Step 6 set up a ruleset, add its code scanning rule too, which blocks a merge on what the analysis found rather than only on whether it reported, and so also catches the case where the analysis configuration is deleted and the required check simply stops appearing. +R-SEC-09 applies only where the repository holds source in a language a static analyzer supports, so establish that first and say plainly that the rule does not reach the repository when it does not, rather than reporting a violation. Where it does apply, check that an analysis workflow runs on pull requests to the default branch and that its result is a required check, since an analyzer whose failure does not block merge is advisory. On GitHub, prefer CodeQL default setup for repositories that do not receive fork pull requests. GitHub excludes fork pull requests from default setup, so use advanced setup when the rule must cover them. Give the resolved settings URL the same way Step 6 does; the reference file names the endpoint that reports whether it is already enabled. Where Step 6 set up a ruleset, add its code scanning rule too, which blocks a merge on what the analysis found rather than only on whether it reported, and so also catches the case where the analysis configuration is deleted and the required check simply stops appearing. ### Step 12: Enable the detection controls this repository can actually use @@ -163,6 +173,8 @@ A read can also fail to answer. On GitHub the `security_and_analysis` object is For R-SEC-11, switching the forge's alerting on does not finish the check. Compare the package set the forge reports watching against what the project actually resolves: a forge that cannot parse the project's lockfile still parses its manifests, so the security overview looks the same whether it covers ten packages or five hundred. Where the two sets differ, that residual is the finding, and the fix is a scanner that reads the lockfile the forge cannot. Scope what blocks a merge to what the change introduces, and let the repository-wide scan report instead. An advisory published overnight against a dependency with no fix available is not something a contributor can act on, and a check that fails for that reason teaches the maintainer to dismiss alerts. +Verified 2026-08-07 against the current package-manager sources named in `references/ecosystems/`, [GitHub code scanning setup types](https://docs.github.com/en/code-security/concepts/code-scanning/setup-types), [GitHub repository REST endpoints](https://docs.github.com/en/rest/repos/repos), [GitHub dependency graph ecosystems](https://docs.github.com/en/code-security/reference/supply-chain-security/dependency-graph-supported-package-ecosystems), [GitLab SAST](https://docs.gitlab.com/user/application_security/sast/), [GitLab secret detection](https://docs.gitlab.com/user/application_security/secret_detection/), and [GitLab dependency scanning](https://docs.gitlab.com/user/application_security/dependency_scanning/). + ### Step 13: Read OpenSSF Scorecard results Use Scorecard as supplementary evidence after the direct checks above, never as a substitute for them. A missing or stale result is normal and must not change a directly verified rule status. Do not offer to install or run another scanner unless the user asks for that expansion. @@ -178,3 +190,5 @@ Give every rule one of four statuses, and use no others: fixed, pending confirma Show what Steps 2 through 13 found and fixed, grouped by rule ID: which files this skill edited directly, which settings still need the user's confirmation with the resolved URL for each, and the supply-chain observation from Step 3 if one applies. Where a Scorecard result was read in Step 13, include its dated findings as supplementary evidence alongside this skill's direct findings. Do not mark a rule fixed until the file is written or the setting is confirmed and verified; a settings block the user has not yet confirmed stays listed as pending. Then close with what the maintainer could still turn on that this skill could not reach itself. Give each item what it does, why the skill could not set it, and what enabling it would take. Three kinds belong here: a control the forge exposes no API for, which only a click reaches; a control the forge's tier withholds, named with the tier it needs; and a control that strengthens the repository beyond what any rule in `STANDARD.md` requires. Keep this list separate from the rule findings and label it as optional. A maintainer reading an unmet rule and an available extra in one list cannot tell which of the two the standard actually asks for, and the one that reads as less urgent is the one that gets skipped. + +Verified 2026-08-07 against [OpenSSF Scorecard checks](https://github.com/ossf/scorecard/blob/main/docs/checks.md) and [GitHub Scorecard guidance](https://docs.github.com/en/code-security/security-overview/viewing-security-insights-for-your-organization#viewing-openssf-scorecard-metrics-for-an-organization).