Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,24 @@ The format is based on [Keep a Changelog 2.0.0](https://keepachangelog.com/en/2.

## [Unreleased]

### Added

- `oss-harden` counts a repository's workflow files as source in an analyzed language. R-SEC-09 now reaches a repository whose application language no analyzer supports. Step 11 read as CodeQL over application code alone, and reported such a repository outside the rule while nothing examined its workflows.
- The GitHub reference names CodeQL's `actions` language and zizmor as the analyzers for those files. It maps zizmor's audits onto the five rules the kit otherwise scores by reading the files: R-SEC-01, R-SEC-02, R-SEC-07, R-SEC-14, and R-PUB-02.
- The RubyGems reference covers Bundler's own `cooldown` setting under R-SEC-14. It gives the three precedence layers and the `Gemfile` form. Two conditions leave a configured cooldown covering less than it reads. A version whose gem server emits no `created_at` stays resolvable. From Bundler 4.0.15, a version already in `Gemfile.lock` is exempt on every resolution path.
- The RubyGems reference states the install-time code execution that R-SEC-15 places outside itself, and gives the command that counts the installed gems holding it.

### Changed

- The documented install is `npx skills add svyatov/oss-kit`. The `--skill '*'` flag existed to preselect nine loose rows in the picker, and 0.14.0 replaced those rows with one select-all row, so the flag now only repeats what a single keystroke does.
- `oss-skill` says a repository documenting a whole-collection install groups its skills through a plugin manifest rather than through `--skill '*'`. The reference recommended the flag, which was the only answer before the CLI read a manifest for groupings.
- `oss-harden` prices GitHub Code Quality at its general availability rate, says the product reaches organization-owned repositories on GitHub Team or GitHub Enterprise Cloud only, and gives the setup command that decides whether the `code_quality` ruleset rule can be added at all. The paragraph was written during the public preview, when the cost was deferred and the availability limit was unstated.

### Fixed

- The RubyGems reference no longer says `bundle config set --local frozen true` writes the setting where a reviewer and CI can read it. `bundle gem` generates a `.gitignore` ignoring `/.bundle/`, so on a gem skeleton nothing commits that file. The reference now says to read the ignore file first, and names `BUNDLE_FROZEN` as the setting that reaches CI.
- The GitHub reference's contents list links to its dependency updates heading again. That heading gained R-SEC-14 and the anchor was left behind.

## [0.14.0] - 2026-08-11

### Changed
Expand Down
4 changes: 2 additions & 2 deletions skills/oss-harden/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,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 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.
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. Count the workflow files when you establish that set. A CI configuration is itself source in an analyzed language, so a repository whose application code no analyzer supports is still inside this rule once it has workflows, and reporting it outside on the strength of its application language is a false clean over the surface an attacker reaches first. The reference files name what analyzes those files on each forge. 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

Expand All @@ -173,7 +173,7 @@ 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/).
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/), and the workflow-file paragraph on 2026-08-11 against [CodeQL Actions built-in queries](https://docs.github.com/en/code-security/code-scanning/managing-your-code-scanning-configuration/actions-built-in-queries).

### Step 13: Read OpenSSF Scorecard results

Expand Down
36 changes: 35 additions & 1 deletion skills/oss-harden/references/ecosystems/rubygems.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ On GitLab the Renovate manager is `bundler`.

## Lockfile and frozen install (R-SEC-08)

Bundler writes `Gemfile.lock` without being asked, and the frozen mode is configuration rather than a command flag. `bundle config set --local frozen true` sets it in `.bundle/config` inside the repository, where the workflow file and a reviewer can both see it. Bundler documents `frozen` as "Disallow any automatic changes to `Gemfile.lock`. Bundler commands will be blocked unless the lockfile can be installed exactly as written", and names the usual trigger, a `Gemfile` edited by hand without regenerating the lockfile.
Bundler writes `Gemfile.lock` without being asked, and the frozen mode is configuration rather than a command flag. `bundle config set --local frozen true` writes it to `.bundle/config` in the working tree, which is not the same as writing it to the repository: `bundle gem` generates a `.gitignore` whose first line is `/.bundle/`, so on a gem skeleton the setting is invisible to a reviewer and absent from every checkout CI makes. Check the ignore file before reporting the control as set. Where it is ignored, either track the config file deliberately or set `BUNDLE_FROZEN=true` in the workflow, which reaches CI without depending on what got committed. Bundler documents `frozen` as "Disallow any automatic changes to `Gemfile.lock`. Bundler commands will be blocked unless the lockfile can be installed exactly as written", and names the usual trigger, a `Gemfile` edited by hand without regenerating the lockfile.

`deployment` is documented as equivalent to setting `frozen` to `true` and `path` to `vendor/bundle`, so it is the stricter of the two only in where the gems land. Where a project already sets `deployment`, that is the frozen half satisfied; do not add both and report them as two controls.

Expand Down Expand Up @@ -41,3 +41,37 @@ Advisories come from the GitHub Advisory Database, which names this ecosystem Ru
`osv-scanner` reads `Gemfile.lock` and `gems.locked`.

Verified 2026-07-31 against [Dependabot supported ecosystems and repositories](https://docs.github.com/en/code-security/reference/supply-chain-security/supported-ecosystems-and-repositories), [Dependency graph supported package ecosystems](https://docs.github.com/en/code-security/reference/supply-chain-security/dependency-graph-supported-package-ecosystems), [GitHub Advisory Database](https://docs.github.com/en/code-security/concepts/vulnerability-reporting-and-management/github-advisory-database), [bundle config](https://github.com/rubygems/rubygems/blob/master/lib/bundler/man/bundle-config.1.ronn), [CodeQL supported languages and frameworks](https://codeql.github.com/docs/codeql-overview/supported-languages-and-frameworks/), [GitLab SAST](https://docs.gitlab.com/user/application_security/sast/), [Renovate managers](https://docs.renovatebot.com/modules/manager/), and [osv-scanner supported languages and lockfiles](https://google.github.io/osv-scanner/supported-languages-and-lockfiles/).

## Update cooldown (R-SEC-14)

The rule is scored against the updater, so `cooldown` in `dependabot.yml` or `minimumReleaseAge` in a Renovate configuration is what closes it, and `references/github.md` and `references/gitlab.md` carry those. Bundler has carried its own since 4.0.13, released 2026-06-03, where the changelog lists it under Security. It is worth setting alongside the updater because it covers an install the updater never touched, such as a contributor adding a gem by hand.

The unit is whole days and the default is unset, meaning no cooldown at all. Bundler resolves the effective value from three layers, highest precedence first: `--cooldown N` on `install`, `update`, `add`, and `outdated`; then `bundle config set cooldown N` or `BUNDLE_COOLDOWN=N`; then the per-source `cooldown:` keyword in the `Gemfile`.

```ruby
source "https://rubygems.org", cooldown: 3
```

Prefer the `Gemfile` form to `bundle config set cooldown`, for the reason the frozen section above gives: on a `bundle gem` skeleton `/.bundle/` is ignored, so the setting looks applied locally and reaches nobody else, while the `Gemfile` is already tracked. Note the trade that comes with it. The two higher layers apply uniformly to every source, including one that declared its own value, so a private registry is exempted permanently by declaring `source "https://internal", cooldown: 0` and not by any command line. `--cooldown 0` is the escape hatch for a single run, which the project wants the day a real security fix ships.

Two things make a configured cooldown cover less than it reads. Bundler filters on a per-version `created_at` timestamp in the v2 compact index, and a version whose server does not emit one is treated as outside the window and stays resolvable, which reaches older gem servers, rubygems.org entries predating its v2 cutover, and private registries still serving v1. Confirm the registry emits `created_at` in its `/info/<gem>` responses before reporting the control as covering the project's gems. Separately, from 4.0.15 a version already in `Gemfile.lock` is exempt on every resolution path, so the setting governs what enters the lockfile and never what is already pinned there. That is the same leak `references/ecosystems/npm.md` records for bun, and it is the reason cooldown is not a substitute for watching the lockfile under R-SEC-11.

Confirm it took effect rather than trusting the syntax. Bundler names the setting when it holds a version back, reporting the count of versions excluded and the `--cooldown 0` bypass.

Verified 2026-08-11 against `bundle install` and `bundle config` from Bundler 4.0.17 and the [Bundler changelog](https://github.com/rubygems/rubygems/blob/master/bundler/CHANGELOG.md) entries for 4.0.13 through 4.0.15.

## Install-time code execution (R-SEC-15)

R-SEC-15 places this ecosystem outside itself, and that stands: RubyGems documents no way to decline a dependency's install-time code, so there is nothing for the rule to check and no finding to report. What the rule's silence should not do is leave the reader believing the exposure is absent, because it is larger here than in the ecosystems the rule does reach.

A gem whose gemspec sets `spec.extensions` runs `extconf.rb`, `Rakefile`, or `mkrf_conf.rb` during `bundle install`, before anything is required. There is no `--ignore-scripts`, no allowlist, and no per-gem opt-out. The BufferZoneCorp campaign disclosed on 2026-05-01 used exactly this path: seven gems published under names resembling Rails and ActiveSupport utilities carried credential harvesting in `extconf.rb`, reading SSH keys, AWS credentials, `.npmrc`, `.netrc`, and RubyGems credentials from developer machines and CI runners.

So report the surface rather than a violation, and give the user its size:

```bash
ruby -e 'Gem::Specification.each { |s| puts s.name if s.extensions.any? }'
```

The controls that do apply are the ones the other sections carry. A committed lockfile and frozen install stop an unreviewed version arriving on its own, the cooldown above keeps a newly published one out of resolution long enough for a yank to land, and the vulnerability watch under R-SEC-11 is what eventually names it. Say which of the three the repository has, because for this ecosystem they are the whole answer.

Verified 2026-08-11 against [Socket's BufferZoneCorp research](https://socket.dev/blog/malicious-ruby-gems-and-go-modules-steal-secrets-poison-ci) and the R-SEC-15 `Check` line in `skills/oss-audit/STANDARD.md`.
20 changes: 19 additions & 1 deletion skills/oss-harden/references/github.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Concrete commands and settings for the decisions `SKILL.md` makes, on GitHub Act
- [Read the current state (Step 2)](#read-the-current-state-step-2)
- [Pin external actions and reusable workflows to a commit SHA (R-SEC-01)](#pin-external-actions-and-reusable-workflows-to-a-commit-sha-r-sec-01)
- [Set least-privilege permissions (R-SEC-02)](#set-least-privilege-permissions-r-sec-02)
- [Automated dependency updates (R-SEC-03)](#automated-dependency-updates-r-sec-03)
- [Automated dependency updates (R-SEC-03, R-SEC-14)](#automated-dependency-updates-r-sec-03-r-sec-14)
- [Branch protection and rulesets (R-SEC-04, R-SEC-12)](#branch-protection-and-rulesets-r-sec-04-r-sec-12)
- [Deriving the required checks](#deriving-the-required-checks)
- [Updating an existing ruleset](#updating-an-existing-ruleset)
Expand All @@ -22,6 +22,7 @@ Concrete commands and settings for the decisions `SKILL.md` makes, on GitHub Act
- [Tag rulesets (R-SEC-13)](#tag-rulesets-r-sec-13)
- [Untrusted input (R-SEC-07)](#untrusted-input-r-sec-07)
- [Static analysis (R-SEC-09)](#static-analysis-r-sec-09)
- [The workflow files are one of the languages](#the-workflow-files-are-one-of-the-languages)
- [Detection controls (R-SEC-10, R-SEC-11)](#detection-controls-r-sec-10-r-sec-11)
- [The write is not the evidence](#the-write-is-not-the-evidence)
- [Absent is not disabled](#absent-is-not-disabled)
Expand Down Expand Up @@ -401,6 +402,23 @@ Quoting the expression directly inside `run:` is not sufficient because expressi

For a public repository in a CodeQL-supported language, prefer CodeQL default setup and confirm its pull request analysis appears as a required status check. Private repositories require GitHub Code Security on an eligible GitHub Team or Enterprise plan. If CodeQL does not support the language, use the project's established analyzer and require its pull request result rather than adding a no-op CodeQL configuration.

### The workflow files are one of the languages

`actions` is a CodeQL language, generally available since April 2025, so the repository's own workflows are inside R-SEC-09 whatever the application is written in. Default setup enables it once workflow files are on the default branch; advanced setup analyzes them only where the matrix names `actions`. Its queries cover script injection, unvalidated dangerous inputs, and missing permissions, with data flow tracked between steps rather than by pattern match, so read the matrix before reporting a repository with workflows as analyzed.

CodeQL is the deeper half of this surface and not the wider one. zizmor audits the same files for the classes those queries largely leave, and its audits map onto five rules this kit already owns: `unpinned-uses`, `stale-action-refs`, and `impostor-commit` onto R-SEC-01, `excessive-permissions` onto R-SEC-02, `template-injection`, `github-env`, and `dangerous-triggers` onto R-SEC-07, `dependabot-cooldown` onto R-SEC-14, and `use-trusted-publishing` onto R-PUB-02. Those rules are otherwise scored by reading the files, so a project that runs it converts five judgements into a check. Recommend it as a second analyzer beside CodeQL rather than a replacement for it.

```yaml
- name: Run zizmor
uses: zizmorcore/zizmor-action@3dc1ecc9bcb9e94e9b2c709687979e1298497054 # v0.6.2
```

Two things about that step are worth saying to the user rather than leaving in the defaults. It uploads SARIF to code scanning unless `advanced-security` is set to `false`, so the job needs `security-events: write` and its findings arrive where the `code_scanning` rule below can block on them. And its `version` input defaults to `latest`, so pinning the action by SHA pins the wrapper while the analyzer it downloads still floats; set `version` to close the gap R-SEC-01 exists for.

zizmor reads GitHub Actions, Dependabot, and pre-commit configurations. It does not read `.gitlab-ci.yml`, so this is a GitHub-side recommendation with no GitLab equivalent, and `references/gitlab.md` answers that forge with GitLab SAST instead.

Verified 2026-08-11 against [CodeQL Actions built-in queries](https://docs.github.com/en/code-security/code-scanning/managing-your-code-scanning-configuration/actions-built-in-queries), [the Actions workflow analysis GA announcement](https://github.blog/changelog/2025-04-22-github-actions-workflow-security-analysis-with-codeql-is-now-generally-available/), [zizmor's audit reference](https://docs.zizmor.sh/audits/), and [zizmor-action](https://github.com/zizmorcore/zizmor-action).

Where the branch is guarded by a ruleset, the `code_scanning` rule is the stronger way to make the analysis binding, and it is worth having alongside the status check rather than instead of it:

```json
Expand Down
Loading