still_active 2.0.0 → 3.0.0.rc2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +42 -0
- data/README.md +104 -357
- data/lib/helpers/activity_helper.rb +8 -0
- data/lib/helpers/constraint_helper.rb +208 -0
- data/lib/helpers/cvss_helper.rb +63 -0
- data/lib/helpers/cyclonedx_helper.rb +20 -4
- data/lib/helpers/diff_markdown_helper.rb +21 -5
- data/lib/helpers/endoflife_helper.rb +114 -0
- data/lib/helpers/http_helper.rb +17 -2
- data/lib/helpers/markdown_helper.rb +118 -1
- data/lib/helpers/pep440_helper.rb +100 -0
- data/lib/helpers/python_helper.rb +19 -0
- data/lib/helpers/ruby_advisory_db.rb +23 -0
- data/lib/helpers/ruby_helper.rb +13 -24
- data/lib/helpers/runtime_ceiling_helper.rb +121 -0
- data/lib/helpers/sarif_helper.rb +105 -3
- data/lib/helpers/semver_satisfaction.rb +68 -0
- data/lib/helpers/status_helper.rb +68 -0
- data/lib/helpers/summary_helper.rb +4 -0
- data/lib/helpers/terminal_helper.rb +144 -6
- data/lib/helpers/version_helper.rb +27 -0
- data/lib/helpers/vulnerability_helper.rb +73 -7
- data/lib/still_active/ceiling_reconciler.rb +43 -0
- data/lib/still_active/cli.rb +212 -9
- data/lib/still_active/config.rb +28 -1
- data/lib/still_active/config_file.rb +27 -0
- data/lib/still_active/deps_dev_client.rb +115 -5
- data/lib/still_active/diff.rb +22 -0
- data/lib/still_active/ecosystem_lens.rb +284 -0
- data/lib/still_active/ecosystems_client.rb +123 -0
- data/lib/still_active/options.rb +44 -1
- data/lib/still_active/osv_client.rb +147 -0
- data/lib/still_active/poison_security_correlator.rb +232 -0
- data/lib/still_active/pypi_client.rb +56 -0
- data/lib/still_active/sarif/rules.rb +33 -9
- data/lib/still_active/sbom_reader.rb +191 -0
- data/lib/still_active/sbom_workflow.rb +96 -0
- data/lib/still_active/suppressions.rb +6 -5
- data/lib/still_active/version.rb +1 -1
- data/lib/still_active/workflow.rb +205 -9
- data/lib/still_active.rb +3 -0
- data/still_active.gemspec +28 -5
- metadata +61 -11
data/README.md
CHANGED
|
@@ -1,51 +1,45 @@
|
|
|
1
1
|
# `still_active`
|
|
2
2
|
|
|
3
|
-
**How do you know
|
|
3
|
+
**How do you know the dependencies you ship are still maintained?**
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
[Install](#installation) · [Quick start](#quick-start) · [GitHub Action & CI](#github-action--ci) · [Cross-ecosystem](#cross-ecosystem-audit) · [Configuration](#configuration) · [Rules](docs/rules.md)
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Your package manager tells you what's outdated and what has a known CVE. Neither tells you whether anyone's still working on the thing. An abandoned dependency is a quiet liability: when it finally breaks or a vulnerability lands, there's no release coming and no one to ping, and you find out at the worst time.
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
[](https://github.com/marketplace/actions/still_active)
|
|
11
|
-

|
|
12
|
-

|
|
13
|
-

|
|
9
|
+
`still_active` flags that risk before it bites, across your whole dependency graph: archived repos, no release in years, low OpenSSF scores, unfixable vulnerabilities, and the poison-pill case where a dormant package holds one of your deps below its own security patch. Native on **Ruby** gems; **npm, PyPI, Cargo, Go, Maven, and NuGet** via a CycloneDX SBOM.
|
|
14
10
|
|
|
15
11
|
```
|
|
16
|
-
Name
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
7 gems: 4 up to date, 0 outdated · 2 active, 2 stale, 2 archived · 0 vulnerabilities
|
|
27
|
-
Ruby 4.0.1 (latest)
|
|
12
|
+
Name Version Activity OpenSSF Vulns License
|
|
13
|
+
────────────────────────────────────────────────────────────────────
|
|
14
|
+
backbone-rails 1.2.3 (latest) archived 2.6/10 0 -
|
|
15
|
+
nokogiri 1.19.4 (latest) ok 5.9/10 0 MIT
|
|
16
|
+
paperclip 6.1.0 (latest) archived 2/10 0 MIT
|
|
17
|
+
↳ poison: caps terrapin ~> 0.6.0 (1 major behind, latest 1.x)
|
|
18
|
+
rake 13.4.2 (latest) ok 5.5/10 0 MIT
|
|
19
|
+
|
|
20
|
+
4 gems: 4 up to date · 2 active, 2 archived · 0 vulnerabilities · 1 poison-pill
|
|
21
|
+
Ruby 4.0.5 (latest)
|
|
28
22
|
```
|
|
29
23
|
|
|
30
24
|
## Why `still_active`?
|
|
31
25
|
|
|
32
|
-
|
|
26
|
+
No package ecosystem's standard tooling answers "is anyone still maintaining this?" `npm audit`, `cargo audit`, `pip-audit`, and `bundler-audit` find known CVEs; the `outdated` commands find version drift. None tell you a dependency's upstream is archived, hasn't shipped in three years, or is holding one of your deps below its security fix. That maintenance gap is what `still_active` fills. It's **complementary to**, not a replacement for, the tools you already run.
|
|
27
|
+
|
|
28
|
+
On Ruby, where it runs natively:
|
|
33
29
|
|
|
34
30
|
| | `bundle outdated` | `bundler-audit` | `libyear-bundler` | **`still_active`** |
|
|
35
31
|
| ---------------------------- | ----------------- | ---------------------- | ----------------- | ------------------------ |
|
|
36
32
|
| Outdated versions | Yes | - | Yes | Yes |
|
|
37
|
-
| Known vulnerabilities (CVEs) | - | Yes (ruby-advisory-db) | - | Yes (deps.dev + ruby-advisory-db) |
|
|
33
|
+
| Known vulnerabilities (CVEs) | - | Yes (ruby-advisory-db) | - | Yes (deps.dev + OSV + ruby-advisory-db) |
|
|
38
34
|
| Libyear drift | - | - | Yes | Yes |
|
|
39
|
-
| **Last
|
|
35
|
+
| **Last-commit activity** | - | - | - | **Yes** |
|
|
40
36
|
| **Archived repo detection** | - | - | - | **Yes** |
|
|
41
37
|
| **OpenSSF Scorecard** | - | - | - | **Yes** |
|
|
42
|
-
| **
|
|
43
|
-
| **
|
|
44
|
-
|
|
|
45
|
-
| CI quality gates | - | Exit code | - | Yes (4 flags) |
|
|
46
|
-
| Output formats | Text | Text | Text | Terminal, JSON, Markdown, SARIF, CycloneDX |
|
|
38
|
+
| **Poison-pill / below-the-fix** | - | - | - | **Yes** |
|
|
39
|
+
| **Cross-ecosystem** (npm/PyPI/Cargo/Go/Maven/NuGet) | - | - | - | **Yes** (`--sbom`) |
|
|
40
|
+
| CI quality gates | - | Exit code | - | Yes (6 gate flags) |
|
|
47
41
|
|
|
48
|
-
|
|
42
|
+
With `bundler-audit` installed alongside, `still_active` merges its `ruby-advisory-db` advisories with its own deps.dev + OSV sources, so running both no longer means reconciling two vuln counts by hand.
|
|
49
43
|
|
|
50
44
|
## Installation
|
|
51
45
|
|
|
@@ -53,177 +47,29 @@ The bolded rows are the gap `still_active` fills: nobody else answers "is the ma
|
|
|
53
47
|
gem install still_active
|
|
54
48
|
```
|
|
55
49
|
|
|
56
|
-
|
|
50
|
+
Requires an actively-maintained Ruby (the gemspec floor tracks Ruby's [EOL schedule](https://endoflife.date/ruby)). You don't have to run it *on* the Ruby you audit: it reports on the version pinned in `Gemfile.lock`, so run it from any current Ruby and it still flags an EOL target.
|
|
57
51
|
|
|
58
|
-
## Quick
|
|
52
|
+
## Quick start
|
|
59
53
|
|
|
60
54
|
```bash
|
|
61
55
|
# audit your Gemfile (auto-detects output format)
|
|
62
56
|
still_active
|
|
63
57
|
|
|
64
|
-
#
|
|
65
|
-
still_active --
|
|
58
|
+
# cross-ecosystem: audit any CycloneDX SBOM (npm, PyPI, Cargo, Go, Maven, NuGet)
|
|
59
|
+
syft dir:. -o cyclonedx-json > sbom.json && still_active --sbom=sbom.json
|
|
66
60
|
|
|
67
|
-
# CI
|
|
61
|
+
# fail CI if any gem is critically stale or vulnerable
|
|
68
62
|
still_active --fail-if-critical --fail-if-vulnerable
|
|
69
63
|
|
|
70
|
-
#
|
|
71
|
-
still_active --fail-if-warning --ignore=legacy_gem,internal_gem
|
|
72
|
-
|
|
73
|
-
# markdown table for pull requests or documentation
|
|
64
|
+
# markdown table for a pull request
|
|
74
65
|
still_active --markdown
|
|
75
66
|
```
|
|
76
67
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
### Authentication
|
|
80
|
-
|
|
81
|
-
`still_active` discovers a GitHub token in this order:
|
|
82
|
-
|
|
83
|
-
1. `--github-oauth-token=TOKEN` CLI flag
|
|
84
|
-
2. `GITHUB_TOKEN` environment variable (CI convention)
|
|
85
|
-
3. `GH_TOKEN` environment variable (`gh` CLI convention)
|
|
86
|
-
4. `gh auth token` (if `gh` is installed and authenticated)
|
|
87
|
-
|
|
88
|
-
Without a token, GitHub API calls are unauthenticated and rate-limited to 60 requests/hour — you will hit the limit on anything beyond a handful of gems. With a token the limit is 5000 requests/hour. When a rate-limit response comes back with a reset that's near (within 60 seconds, typically a secondary/burst limit that the concurrent fan-out can trip even with a token), still_active waits it out and retries rather than dropping that gem's signals; a far-off reset (you've exhausted the hourly limit) isn't waited out, so add a token or run less often.
|
|
89
|
-
|
|
90
|
-
GitLab cascade mirrors GitHub: `--gitlab-token` → `GITLAB_TOKEN` → `glab auth status --show-token`. Optional for public repos, required for private ones.
|
|
91
|
-
|
|
92
|
-
Forgejo/Codeberg (the rare gem whose canonical `source_code_uri` is `codeberg.org`) is read anonymously by default; set `STILL_ACTIVE_FORGEJO_TOKEN` (or `CODEBERG_TOKEN`) only to raise the rate limit or reach a private repo. There is no CLI flag, since Codeberg has no ubiquitous CLI to borrow a token from the way `gh`/`glab` do.
|
|
93
|
-
|
|
94
|
-
Artifactory auth looks for Bundler configuration's credentials first so that private registries work without extra configuration if they are already configured in Bundler. To set those credentials in Bundler, run `bundle config set credentials.my-org.jfrog.io user:pass`. If no credentials are set there, still_active falls back using a token provided via `--artifactory-token` or `STILL_ACTIVE_ARTIFACTORY_TOKEN`. Authentication expects a `user:password` format for Basic auth, otherwise it will be treated as a bare token for Bearer auth. Valid authentication is required for private JFrog gem registries (`*.jfrog.io`).
|
|
95
|
-
|
|
96
|
-
When providing the artifactory token via flag or env, you must also set `--artifactory-host` or `STILL_ACTIVE_ARTIFACTORY_HOST` to the expected registry hostname (e.g. `my-org.jfrog.io`). still_active only sends the credentials to that host, ensuring that a lockfile containing other Artifactory hosts will not leak the token (a security risk). Providing a token/host in this manner will work only for a single host. To support multiple Artifactory hosts, use Bundler's configuration for per-host credentials.
|
|
97
|
-
|
|
98
|
-
### CLI options
|
|
99
|
-
|
|
100
|
-
```text
|
|
101
|
-
Usage: still_active [options]
|
|
102
|
-
|
|
103
|
-
all flags are optional
|
|
104
|
-
|
|
105
|
-
--gemfile=GEMFILE path to gemfile
|
|
106
|
-
--gems=GEM,GEM2,... Gem(s)
|
|
107
|
-
--terminal Coloured terminal output (default in TTY)
|
|
108
|
-
--markdown Markdown table output
|
|
109
|
-
--json JSON output (default when piped)
|
|
110
|
-
--alternatives Suggest maintained alternatives (Ruby Toolbox leads) for archived/critical gems
|
|
111
|
-
--unreleased-commits Count commits on the default branch since the latest release (GitHub only; opt-in)
|
|
112
|
-
--direct-only Audit only direct (declared) deps, not the full transitive graph
|
|
113
|
-
--sarif[=PATH] SARIF 2.1.0 output for GitHub Code Scanning
|
|
114
|
-
--cyclonedx[=PATH] CycloneDX SBOM output (stdout, or a file path)
|
|
115
|
-
--cyclonedx-version=VERSION CycloneDX spec version: 1.6 (default) or 1.7
|
|
116
|
-
--baseline=PATH Compare current state to baseline JSON; emit markdown deltas
|
|
117
|
-
--github-oauth-token=TOKEN GitHub OAuth token to make API calls
|
|
118
|
-
--gitlab-token=TOKEN GitLab personal access token for API calls
|
|
119
|
-
--artifactory-token=TOKEN Artifactory token for private gem registry API calls
|
|
120
|
-
--artifactory-host=HOST Artifactory host allowed to receive the global token (e.g. my-org.jfrog.io)
|
|
121
|
-
--simultaneous-requests=QTY Number of simultaneous requests made
|
|
122
|
-
--safe-range-end=YEARS maximum years since last release considered safe, no warning (default 1.5)
|
|
123
|
-
--warning-range-end=YEARS maximum years since last release that triggers a warning, beyond this is critical (default 3)
|
|
124
|
-
--fail-if-critical Exit 1 if any gem has critical activity warning
|
|
125
|
-
--fail-if-warning Exit 1 if any gem has warning or critical activity warning
|
|
126
|
-
--fail-if-vulnerable[=SEVERITY]
|
|
127
|
-
Exit 1 if any gem has vulnerabilities (optionally at or above SEVERITY)
|
|
128
|
-
--fail-if-outdated=LIBYEARS Exit 1 if any gem exceeds LIBYEARS behind latest
|
|
129
|
-
--ignore=GEM,GEM2,... Exclude gems from pass/fail checks (still shown in output)
|
|
130
|
-
--critical-warning-emoji=EMOJI
|
|
131
|
-
--futurist-emoji=EMOJI
|
|
132
|
-
--success-emoji=EMOJI
|
|
133
|
-
--unsure-emoji=EMOJI
|
|
134
|
-
--warning-emoji=EMOJI
|
|
135
|
-
-h, --help Show this message
|
|
136
|
-
-v, --version Show version
|
|
137
|
-
```
|
|
138
|
-
|
|
139
|
-
### Output formats
|
|
140
|
-
|
|
141
|
-
**Terminal** (default on TTY) -- coloured table with summary line. Shown above.
|
|
142
|
-
|
|
143
|
-
**JSON** (default when piped) -- structured data for automation:
|
|
144
|
-
|
|
145
|
-
```bash
|
|
146
|
-
still_active --json --gemfile=spec/still_active/edge_case_gemfile/Gemfile
|
|
147
|
-
```
|
|
148
|
-
|
|
149
|
-
```json
|
|
150
|
-
{
|
|
151
|
-
"gems": {
|
|
152
|
-
"async": {
|
|
153
|
-
"source_type": "rubygems",
|
|
154
|
-
"version_used": "2.36.0",
|
|
155
|
-
"latest_version": "2.36.0",
|
|
156
|
-
"repository_url": "https://github.com/socketry/async",
|
|
157
|
-
"last_commit_date": "2026-01-22 04:09:48 UTC",
|
|
158
|
-
"archived": false,
|
|
159
|
-
"scorecard_score": 7.1,
|
|
160
|
-
"vulnerability_count": 0,
|
|
161
|
-
"license": "MIT",
|
|
162
|
-
"libyear": 0.0
|
|
163
|
-
},
|
|
164
|
-
"nested_form": {
|
|
165
|
-
"source_type": "git",
|
|
166
|
-
"version_used": "0.3.2",
|
|
167
|
-
"repository_url": "https://github.com/ryanb/nested_form",
|
|
168
|
-
"last_commit_date": "2021-12-11 21:47:02 UTC",
|
|
169
|
-
"archived": true,
|
|
170
|
-
"scorecard_score": 3.3,
|
|
171
|
-
"vulnerability_count": 0
|
|
172
|
-
},
|
|
173
|
-
"local_gem": {
|
|
174
|
-
"source_type": "path",
|
|
175
|
-
"version_used": "0.1.0",
|
|
176
|
-
"scorecard_score": null,
|
|
177
|
-
"vulnerability_count": 0
|
|
178
|
-
}
|
|
179
|
-
},
|
|
180
|
-
"ruby": {
|
|
181
|
-
"version": "4.0.1",
|
|
182
|
-
"eol": false,
|
|
183
|
-
"latest_version": "4.0.1",
|
|
184
|
-
"libyear": 0.0
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
```
|
|
68
|
+
Full flags: [`docs/cli.md`](docs/cli.md). Tokens for private registries and self-hosted forges: [`docs/authentication.md`](docs/authentication.md).
|
|
188
69
|
|
|
189
|
-
|
|
70
|
+
## GitHub Action & CI
|
|
190
71
|
|
|
191
|
-
|
|
192
|
-
still_active --markdown
|
|
193
|
-
```
|
|
194
|
-
|
|
195
|
-
| activity | up to date? | OpenSSF | vulns | name | version used | latest version | latest pre-release | last commit | libyear | license |
|
|
196
|
-
| -------- | ----------- | ------- | ----- | ------------------------------------------------------------ | -------------------------------------------------------------------------- | -------------------------------------------------------------------------- | ------------------ | ----------------------------------------------------- | ------- | ------- |
|
|
197
|
-
| | ✅ | 7.1/10 | ✅ | [async](https://github.com/socketry/async) | [2.36.0](https://rubygems.org/gems/async/versions/2.36.0) (2026/01) | [2.36.0](https://rubygems.org/gems/async/versions/2.36.0) (2026/01) | ❓ | [2026/01](https://github.com/socketry/async) | 0.0y | MIT |
|
|
198
|
-
| 🚩 | ✅ | 3.6/10 | ✅ | [backbone-rails](https://github.com/aflatter/backbone-rails) | [1.2.3](https://rubygems.org/gems/backbone-rails/versions/1.2.3) (2016/02) | [1.2.3](https://rubygems.org/gems/backbone-rails/versions/1.2.3) (2016/02) | ❓ | [2016/02](https://github.com/aflatter/backbone-rails) | 0.0y | MIT |
|
|
199
|
-
| ❓ | ❓ | ❓ | ✅ | local_gem | 0.1.0 (path) | ❓ | ❓ | ❓ | - | - |
|
|
200
|
-
| 🚩 | ❓ | 3.3/10 | ✅ | [nested_form](https://github.com/ryanb/nested_form) | 0.3.2 (git) | ❓ | ❓ | [2021/12](https://github.com/ryanb/nested_form) | - | MIT |
|
|
201
|
-
|
|
202
|
-
**Ruby 4.0.1** (latest) ✅
|
|
203
|
-
|
|
204
|
-
**CycloneDX** -- a standards-track SBOM so your dependency graph and still_active's signals flow into Trivy, Dependency-Track, or Snyk:
|
|
205
|
-
|
|
206
|
-
```bash
|
|
207
|
-
still_active --cyclonedx # CycloneDX 1.6 to stdout
|
|
208
|
-
still_active --cyclonedx=sbom.json # write to a file
|
|
209
|
-
still_active --cyclonedx --cyclonedx-version=1.7
|
|
210
|
-
```
|
|
211
|
-
|
|
212
|
-
Emits **1.6 by default** — the version mainstream consumers ingest today (`cyclonedx-core-java`/Dependency-Track and `cyclonedx-go`/Trivy both cap at 1.6 as of 2026); `--cyclonedx-version=1.7` opts into the latest. Gem name/version/`purl`/licenses and vulnerabilities map to native CycloneDX fields; maintenance signals with no native home (archived, OpenSSF score, libyear, last commit, yanked) ride in `still_active:`-namespaced `properties`. The `serialNumber` is content-derived, so two SBOMs of the same lockfile differ only by their generation timestamp.
|
|
213
|
-
|
|
214
|
-
### SARIF output (GitHub Code Scanning)
|
|
215
|
-
|
|
216
|
-
Emit findings as SARIF 2.1.0 — they show up in the GitHub Security tab and as inline annotations on `Gemfile.lock` in pull requests.
|
|
217
|
-
|
|
218
|
-
> **See it live:** this repo audits itself on every push. Browse the live findings in the [Code Scanning Security tab](https://github.com/SeanLF/still_active/security/code-scanning?query=tool%3Astill_active+is%3Aopen) — currently 2× `SA005` (low OpenSSF Scorecard).
|
|
219
|
-
|
|
220
|
-
```bash
|
|
221
|
-
still_active --sarif # writes still_active.sarif.json
|
|
222
|
-
still_active --sarif=path/to/out.sarif.json
|
|
223
|
-
still_active --sarif=- # stdout
|
|
224
|
-
```
|
|
225
|
-
|
|
226
|
-
**Easy mode** — use the [`still_active-action`](https://github.com/SeanLF/still_active-action) wrapper:
|
|
72
|
+
The [`still_active-action`](https://github.com/SeanLF/still_active-action) uploads findings to your **GitHub Security tab** as SARIF, with inline PR annotations on `Gemfile.lock`:
|
|
227
73
|
|
|
228
74
|
```yaml
|
|
229
75
|
permissions:
|
|
@@ -246,213 +92,114 @@ jobs:
|
|
|
246
92
|
with: { sarif_file: still_active.sarif.json }
|
|
247
93
|
```
|
|
248
94
|
|
|
249
|
-
|
|
95
|
+
Gate the build on what you care about (all off by default, so nothing breaks until you opt in), and use `--baseline` for PR review.
|
|
250
96
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
env:
|
|
254
|
-
GITHUB_TOKEN: ${{ github.token }}
|
|
255
|
-
- uses: github/codeql-action/upload-sarif@v3
|
|
256
|
-
if: always()
|
|
257
|
-
with: { sarif_file: still_active.sarif.json }
|
|
258
|
-
```
|
|
259
|
-
|
|
260
|
-
Rule reference (SA001–SA007) and how to suppress: see [`docs/rules.md`](docs/rules.md).
|
|
261
|
-
|
|
262
|
-
### Baseline diff (PR review)
|
|
263
|
-
|
|
264
|
-
`--baseline=FILE` compares the current run against a previously captured JSON snapshot and emits a markdown delta report. Designed for the PR question reviewers actually ask: **what got worse?**
|
|
97
|
+
<details>
|
|
98
|
+
<summary>CI gate flags and PR-review diff</summary>
|
|
265
99
|
|
|
266
100
|
```bash
|
|
267
|
-
#
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
In CI, capture a baseline on main and compare on PR branches. Exits 1 if any regression is detected (new vulns, newly-archived deps, scorecard drops crossing 7.0, libyear growth on unchanged versions, Ruby newly EOL, etc.).
|
|
101
|
+
still_active --fail-if-critical # upstream critically stale or archived
|
|
102
|
+
still_active --fail-if-vulnerable # any known vulnerability (or =low|medium|high|critical)
|
|
103
|
+
still_active --fail-if-outdated=3 # more than 3 libyears behind latest
|
|
104
|
+
still_active --fail-if-poison # a dormant package caps a dep below its latest major
|
|
105
|
+
still_active --fail-if-language-ceiling # a pin strands you on an EOL language runtime
|
|
273
106
|
|
|
274
|
-
|
|
107
|
+
# PR review: report only what got worse since a saved snapshot, exit 1 on any regression
|
|
108
|
+
still_active --json > /tmp/main.json && still_active --baseline=/tmp/main.json
|
|
109
|
+
```
|
|
110
|
+
</details>
|
|
275
111
|
|
|
276
|
-
|
|
112
|
+
Rule reference (SA001-SA009), suppression, and composing with `dependency-review-action`: [`docs/rules.md`](docs/rules.md), [`docs/ci.md`](docs/ci.md). This repo audits itself every push, so you can browse live findings in its [Code Scanning tab](https://github.com/SeanLF/still_active/security/code-scanning?query=tool%3Astill_active+is%3Aopen).
|
|
277
113
|
|
|
278
|
-
|
|
114
|
+
## Cross-ecosystem audit
|
|
279
115
|
|
|
280
|
-
|
|
116
|
+
Point `--sbom` at a CycloneDX SBOM (from [Syft](https://github.com/anchore/syft), Trivy, or any producer) and `still_active` assesses `npm`, `pypi`, `cargo`, `go`, `maven`, and `nuget` packages the same way it does gems, via [deps.dev](https://deps.dev) and [ecosyste.ms](https://ecosyste.ms). The SBOM is treated as **untrusted input** (only ecosystem/name/version are read, repositories resolve from deps.dev, and anything unassessable is surfaced rather than faked as `ok`). Most signals apply everywhere; a few are deliberately scoped.
|
|
281
117
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
| Platform | GitHub.com / GHES only | Any CI |
|
|
285
|
-
| Languages | Multi (GitHub dep graph) | Ruby |
|
|
286
|
-
| Vulnerabilities | GHSA | deps.dev + ruby-advisory-db (merged) |
|
|
287
|
-
| Licenses | Yes (allow/deny gating) | Surfaced (no gating) |
|
|
288
|
-
| OpenSSF Scorecard | Yes (display) | Yes (display + threshold) |
|
|
289
|
-
| **Last-commit activity** | - | **Yes** |
|
|
290
|
-
| **Archived repo detection** | - | **Yes** |
|
|
291
|
-
| **Libyear drift** | - | **Yes** |
|
|
292
|
-
| **Ruby EOL detection** | - | **Yes** |
|
|
293
|
-
| **Yanked version detection** | - | **Yes** |
|
|
294
|
-
| Diff vs base | Native (GitHub API) | `--baseline=FILE` |
|
|
295
|
-
| Output | Inline PR annotations | Terminal / Markdown / JSON / SARIF / CycloneDX |
|
|
118
|
+
<details>
|
|
119
|
+
<summary>Which signal covers which ecosystem</summary>
|
|
296
120
|
|
|
297
|
-
|
|
121
|
+
| Signal (rule) | Ruby (native) | `--sbom` (cross-ecosystem) |
|
|
122
|
+
| ------------- | ------------- | -------------------------- |
|
|
123
|
+
| Archived (SA001), no recent release (SA002), low OpenSSF (SA005), libyear (SA004) | Yes | Yes (all six ecosystems) |
|
|
124
|
+
| Vulnerabilities (SA003) | deps.dev + OSV + ruby-advisory-db | deps.dev + OSV |
|
|
125
|
+
| ↳ "below the fix" (a dead package pins a vulnerable dep) | Yes | npm, Cargo, PyPI |
|
|
126
|
+
| Poison-pill (SA008) | rubygems | PyPI (flat resolution only) |
|
|
127
|
+
| Language-runtime ceiling (SA009) | Ruby | Python |
|
|
128
|
+
| Ruby EOL (SA006), yanked (SA007) | Yes | n/a |
|
|
298
129
|
|
|
299
|
-
|
|
300
|
-
|
|
130
|
+
Full rule detail in [`docs/rules.md`](docs/rules.md).
|
|
131
|
+
</details>
|
|
301
132
|
|
|
302
|
-
|
|
303
|
-
dependency-review:
|
|
304
|
-
runs-on: ubuntu-latest
|
|
305
|
-
steps:
|
|
306
|
-
- uses: actions/checkout@v4
|
|
307
|
-
- uses: actions/dependency-review-action@v4
|
|
308
|
-
with:
|
|
309
|
-
fail-on-severity: high
|
|
310
|
-
show-openssf-scorecard: true
|
|
133
|
+
The play is **maintenance**, not CVE scanning, so compose Trivy/Grype for full vulnerability coverage.
|
|
311
134
|
|
|
312
|
-
|
|
313
|
-
runs-on: ubuntu-latest
|
|
314
|
-
steps:
|
|
315
|
-
- uses: actions/checkout@v4
|
|
316
|
-
- uses: ruby/setup-ruby@v1
|
|
317
|
-
with: { ruby-version: ".ruby-version", bundler-cache: true }
|
|
318
|
-
- uses: SeanLF/still_active-action@v0
|
|
319
|
-
with:
|
|
320
|
-
fail-if-critical: true
|
|
321
|
-
```
|
|
135
|
+
## Output formats
|
|
322
136
|
|
|
323
|
-
|
|
137
|
+
Auto-detected: a coloured terminal table on a TTY (above), JSON when piped. Or ask explicitly.
|
|
324
138
|
|
|
325
|
-
|
|
139
|
+
<details>
|
|
140
|
+
<summary><strong>JSON</strong> (<code>--json</code>): a versioned, contract-tested schema for automation</summary>
|
|
326
141
|
|
|
327
|
-
```
|
|
328
|
-
|
|
329
|
-
|
|
142
|
+
```json
|
|
143
|
+
{
|
|
144
|
+
"gems": {
|
|
145
|
+
"nested_form": {
|
|
146
|
+
"source_type": "git",
|
|
147
|
+
"version_used": "0.3.2",
|
|
148
|
+
"repository_url": "https://github.com/ryanb/nested_form",
|
|
149
|
+
"archived": true,
|
|
150
|
+
"scorecard_score": 3.3,
|
|
151
|
+
"vulnerability_count": 0,
|
|
152
|
+
"status": "archived"
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
"ruby": { "version": "4.0.5", "eol": false, "latest_version": "4.0.5" }
|
|
156
|
+
}
|
|
157
|
+
```
|
|
330
158
|
|
|
331
|
-
|
|
332
|
-
|
|
159
|
+
Fields are documented in [`docs/schema.md`](docs/schema.md).
|
|
160
|
+
</details>
|
|
333
161
|
|
|
334
|
-
|
|
335
|
-
|
|
162
|
+
<details>
|
|
163
|
+
<summary><strong>Markdown</strong> (<code>--markdown</code>): a table for pull requests, docs, or wikis</summary>
|
|
336
164
|
|
|
337
|
-
|
|
338
|
-
|
|
165
|
+
| activity | up to date? | OpenSSF | vulns | name | version | last commit |
|
|
166
|
+
| -------- | ----------- | ------- | ----- | ---- | ------- | ----------- |
|
|
167
|
+
| | ✅ | 5.9/10 | ✅ | [nokogiri](https://github.com/sparklemotion/nokogiri) | 1.19.4 | 2026/06 |
|
|
168
|
+
| 🚩 | ✅ | 2/10 | ✅ | [paperclip](https://github.com/thoughtbot/paperclip) | 6.1.0 | 2021/03 |
|
|
169
|
+
</details>
|
|
339
170
|
|
|
340
|
-
#
|
|
341
|
-
still_active --fail-if-outdated=3 --json
|
|
171
|
+
**SARIF** feeds GitHub Code Scanning (see [GitHub Action & CI](#github-action--ci)). **CycloneDX** (`--cyclonedx`) emits a standards-track SBOM so your graph and `still_active`'s signals flow into Trivy, Dependency-Track, or Snyk.
|
|
342
172
|
|
|
343
|
-
|
|
344
|
-
still_active --fail-if-warning --fail-if-vulnerable --ignore=legacy_gem --json
|
|
345
|
-
```
|
|
173
|
+
## Configuration
|
|
346
174
|
|
|
347
|
-
|
|
175
|
+
A committed `.still_active.yml` keeps policy in version control and replaces the blunt `--ignore=GEM` (which mutes *every* gate for a gem) with granular, expiring suppression. A vulnerability suppression must name an advisory id (so a new CVE is never pre-silenced), an `expires:` date makes accepted risk re-surface instead of rotting, and a suppressed finding still shows in output (and as a dismissed SARIF entry).
|
|
348
176
|
|
|
349
|
-
|
|
177
|
+
<details>
|
|
178
|
+
<summary><code>.still_active.yml</code> example</summary>
|
|
350
179
|
|
|
351
180
|
```yaml
|
|
352
|
-
#
|
|
353
|
-
|
|
354
|
-
fail_if_vulnerable: high # true, or a minimum severity: low|medium|high|critical
|
|
355
|
-
fail_if_outdated: 3 # libyears
|
|
356
|
-
unreleased_commits: true
|
|
357
|
-
output: json # terminal | markdown | json
|
|
358
|
-
direct_only: true # audit only declared deps, not the full transitive graph (--direct-only)
|
|
359
|
-
|
|
360
|
-
# Pull bundler-audit's accepted-advisory list instead of maintaining two files
|
|
361
|
-
import:
|
|
362
|
-
- .bundler-audit.yml
|
|
181
|
+
fail_if_vulnerable: high # true, or a minimum severity: low|medium|high|critical
|
|
182
|
+
fail_if_poison: warning # true (=warning), or a tier: note|warning|critical
|
|
363
183
|
|
|
364
184
|
ignore:
|
|
365
|
-
#
|
|
185
|
+
# accept ONE advisory by id; a different or new CVE on nokogiri still fails
|
|
366
186
|
- advisory: CVE-2024-1234
|
|
367
187
|
gem: nokogiri
|
|
368
188
|
reason: "no fix released; not reachable from our code path"
|
|
369
|
-
expires: 2026-09-01
|
|
370
|
-
|
|
371
|
-
# Accept staleness on a vendored gem, but still fail if it gets a CVE
|
|
372
|
-
- gem: legacy_thing
|
|
373
|
-
signal: activity # activity | vulnerability | libyear
|
|
374
|
-
reason: "vendored, intentionally frozen"
|
|
375
|
-
|
|
376
|
-
# A bare gem name keeps the old whole-gem behaviour (mutes every signal)
|
|
377
|
-
- some_internal_gem
|
|
378
|
-
```
|
|
379
|
-
|
|
380
|
-
Design:
|
|
381
|
-
|
|
382
|
-
- **Granularity.** Key by `advisory:` (one CVE) and/or `signal:` (`activity` / `vulnerability` / `libyear`), not the whole gem. A vulnerability suppression **must** name an advisory id, so a newly disclosed CVE on the same gem is never pre-silenced. `--ignore=GEM` (and a bare gem-name entry) still mute everything, by design.
|
|
383
|
-
- **Precedence.** CLI flag > env var > config file > default. A CLI flag always wins; `--ignore` unions with the file's suppressions rather than replacing them. Secrets (tokens) and invocation-specific paths (`--gemfile`, `--gems`, `--baseline`, output paths) are intentionally **not** read from the file, so a committed config never carries a credential.
|
|
384
|
-
- **Expiry.** An `expires:` date makes accepted risk visible: once it passes, the entry stops applying and the finding fails the gate again (Trivy-style), so a suppression can't rot silently. `reason:` is optional but recommended. A suppression naming a gem that isn't in your dependency graph (a typo, or a gem you've since removed) is also surfaced as a warning, so dead entries don't accumulate.
|
|
385
|
-
- **bundler-audit, suggested not absorbed.** still_active never silently inherits another tool's ignore list: auto-importing `.bundler-audit.yml` would suppress vulnerabilities you only accepted in bundler-audit's context, with no reason or expiry here. Instead, when `--fail-if-vulnerable` is on and an un-imported `.bundler-audit.yml` is present, it prints a one-line hint suggesting the `import:` line, leaving the opt-in to you.
|
|
386
|
-
- **Output.** Suppression changes the **exit code** and marks the finding in **SARIF** as a native `suppressions[]` entry (with your `reason` as the justification, so GitHub Code Scanning renders it dismissed rather than open). The finding still appears in JSON/terminal/markdown output; suppression accepts a risk, it doesn't hide that the risk exists.
|
|
387
|
-
|
|
388
|
-
### Activity thresholds
|
|
389
|
-
|
|
390
|
-
Activity is driven by release recency (the latest stable or pre-release date), since a release is what you can actually `bundle update` to. A recent commit does not offset a stale release: the last commit date is shown as context and only stands in when a gem has no releases at all (e.g. git-sourced). Thresholds are calibrated against real RubyGems cadence, where healthy mature gems often go a year or more between releases:
|
|
391
|
-
|
|
392
|
-
- **ok**: last release within 18 months (configurable with `--safe-range-end`)
|
|
393
|
-
- **stale**: last release between 18 months and 3 years ago
|
|
394
|
-
- **critical**: last release over 3 years ago (configurable with `--warning-range-end`)
|
|
395
|
-
|
|
396
|
-
### Alternative gem leads (opt-in)
|
|
397
|
-
|
|
398
|
-
When a gem is flagged archived or critical, `--alternatives` surfaces up to three maintained gems from the same [Ruby Toolbox](https://www.ruby-toolbox.com) category, ranked by total downloads:
|
|
399
|
-
|
|
400
|
-
```bash
|
|
401
|
-
still_active --gems=paperclip --alternatives
|
|
189
|
+
expires: 2026-09-01 # re-surfaces as a normal failure after this date
|
|
402
190
|
```
|
|
191
|
+
</details>
|
|
403
192
|
|
|
404
|
-
|
|
405
|
-
↳ leads (Ruby Toolbox): shrine · carrierwave · kt-paperclip (verify fit)
|
|
406
|
-
```
|
|
407
|
-
|
|
408
|
-
These are **leads, not recommendations**: same-category does not mean drop-in replacement, so verify fit before switching. Ruby has no authoritative "use instead" metadata (unlike npm `deprecate`, Go's `// Deprecated:`, or NuGet's alternate-package field), so this is a best-effort heuristic. It is silent when the catalog has no entry for the gem, and the feature never blocks or fails a run. Leads appear in terminal, markdown, JSON, and SARIF output. When the flag is off, terminal output shows a one-line hint on flagged gems that the option exists (other formats stay silent).
|
|
409
|
-
|
|
410
|
-
### Transitive dependencies
|
|
193
|
+
Full semantics, thresholds, and transitive behaviour: [`docs/configuration.md`](docs/configuration.md).
|
|
411
194
|
|
|
412
|
-
|
|
195
|
+
## Data sources
|
|
413
196
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
`--alternatives` stays **direct-only** by design — "replace gem X with Y" is incoherent for a gem you never selected. Pass `--direct-only` to audit just your declared dependencies (the pre-1.7 behaviour), which is also much cheaper in API calls.
|
|
417
|
-
|
|
418
|
-
> **Run on a schedule, not on every commit.** Auditing the full graph means a repo/release/advisory lookup for *every* resolved gem (hundreds, for a real app), and the GitHub signals are rate-limited (60 req/hour unauthenticated, 5000 with a token). Maintenance status changes over days and weeks, not per-commit, so a nightly or weekly job (or `--direct-only` in PR gates) gets you the signal without burning your API budget for nothing. This is why advisory tools like Brakeman ship their check database; still_active's signals are inherently live (a release date or an archived flag can't be vendored), so the answer is cadence, not bundling.
|
|
419
|
-
|
|
420
|
-
### Unreleased commits (opt-in)
|
|
421
|
-
|
|
422
|
-
`--unreleased-commits` adds an `unreleased_commits` count to the JSON output: commits on the default branch since the latest release's tag. It catches the case the release-recency signal can't, a gem with a *recent* release but a pile of merged-but-unreleased fixes sitting on top, or conversely one that looks stale but is genuinely *done* (no unreleased work). It is the one maintenance signal no Ruby tool surfaces today; only GitHub's own UI shows it.
|
|
423
|
-
|
|
424
|
-
It is **opt-in and GitHub-only**: enabling it adds one extra API call per GitHub-hosted gem (the git tag is resolved from the RubyGems version by trying `v1.2.3` then `1.2.3` as the compare base), so mind your rate limit on a large lockfile. Non-GitHub sources report `null` (GitLab has no equivalent scalar; the signal is duck-typed, so a provider either implements it or doesn't). The count is **informational and never gates a run**. Read it as a lead, not a verdict: it is inflated for monorepos (the count spans the whole repository, e.g. `bundler` living in `rubygems/rubygems`) and for release-branch projects (the default branch is the next-version trunk, so `rails` reads ~2000 commits ahead of its latest stable tag).
|
|
425
|
-
|
|
426
|
-
### Data sources
|
|
427
|
-
|
|
428
|
-
- **Versions, release dates, and licenses** from [RubyGems.org](https://rubygems.org), [GitHub Packages](https://docs.github.com/en/packages), or [JFrog Artifactory](https://jfrog.com/artifactory/) gem registries
|
|
429
|
-
- **Last commit date and archived status** from the [GitHub](https://docs.github.com/en/rest), [GitLab](https://docs.gitlab.com/ee/api/), or [Forgejo/Gitea](https://forgejo.org/docs/latest/user/api-usage/) (Codeberg) API
|
|
430
|
-
- **OpenSSF Scorecard**, **vulnerability counts**, and **CVSS severity** from Google's [deps.dev](https://deps.dev) API
|
|
431
|
-
- **Additional advisories** from [ruby-advisory-db](https://github.com/rubysec/ruby-advisory-db), merged in when `bundler-audit` is installed alongside (run `bundle audit update` to keep its checkout current)
|
|
432
|
-
- **Ruby version freshness** from [endoflife.date](https://endoflife.date)
|
|
433
|
-
- **Alternative gem leads** (with `--alternatives`) from the [rubytoolbox/catalog](https://github.com/rubytoolbox/catalog) category data
|
|
434
|
-
|
|
435
|
-
### Configuration defaults
|
|
436
|
-
|
|
437
|
-
| Option | Default | Description |
|
|
438
|
-
| ----------------------- | ----------- | ---------------------------------------------------------------- |
|
|
439
|
-
| `output_format` | auto-detect | Coloured terminal on TTY, JSON when piped |
|
|
440
|
-
| `safe_range_end` | 1.5 years | Last release within this range is "ok" |
|
|
441
|
-
| `warning_range_end` | 3 years | Last release within this range is "stale"; beyond is "critical" |
|
|
442
|
-
| `simultaneous_requests` | 10 | Concurrent API requests |
|
|
197
|
+
Release dates and licenses from [RubyGems](https://rubygems.org) / [GitHub Packages](https://docs.github.com/en/packages) / [Artifactory](https://jfrog.com/artifactory/); repo activity from the [GitHub](https://docs.github.com/en/rest) / [GitLab](https://docs.gitlab.com/ee/api/) / [Codeberg](https://forgejo.org/docs/latest/user/api-usage/) API, or [ecosyste.ms](https://ecosyste.ms) tokenless ([CC-BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/)); OpenSSF Scorecard and CVSS from [deps.dev](https://deps.dev); advisory severities and fixed-version ranges from [OSV](https://osv.dev) (CVSS-4 scoring needs the optional [`cvss-suite`](https://rubygems.org/gems/cvss-suite)); extra advisories from [ruby-advisory-db](https://github.com/rubysec/ruby-advisory-db); runtime EOL from [endoflife.date](https://endoflife.date).
|
|
443
198
|
|
|
444
199
|
## Development
|
|
445
200
|
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
A pre-push hook runs `rake` automatically before each `git push`, so cross-file rubocop rules don't escape to CI. Skip with `git push --no-verify` if you really need to.
|
|
449
|
-
|
|
450
|
-
To install this gem onto your local machine, run `bundle exec rake install`. New versions are published automatically to [rubygems.org](https://rubygems.org) when a GitHub Release is created (via trusted publishing).
|
|
451
|
-
|
|
452
|
-
## Contributing
|
|
453
|
-
|
|
454
|
-
Bug reports and pull requests are welcome.
|
|
201
|
+
`bin/setup` installs dependencies and wires git hooks; `rake` runs the full lint + test suite. A pre-push hook runs `rake` automatically. Releases publish to [rubygems.org](https://rubygems.org) automatically on a GitHub Release (trusted publishing). See [`CONTRIBUTING.md`](CONTRIBUTING.md).
|
|
455
202
|
|
|
456
203
|
## License
|
|
457
204
|
|
|
458
|
-
|
|
205
|
+
Open source under the [MIT License](https://opensource.org/licenses/MIT).
|
|
@@ -42,6 +42,14 @@ module StillActive
|
|
|
42
42
|
def activity_level(gem_data)
|
|
43
43
|
return :archived if gem_data[:archived]
|
|
44
44
|
|
|
45
|
+
release_recency_level(gem_data)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Recency verdict from release/commit dates alone, ignoring the archived
|
|
49
|
+
# flag: :ok, :stale, :critical, or :unknown. Lets the lifecycle status tell
|
|
50
|
+
# an archived-but-still-publishing gem (development moved to a monorepo) from
|
|
51
|
+
# a genuinely dormant one.
|
|
52
|
+
def release_recency_level(gem_data)
|
|
45
53
|
activity = last_activity(gem_data)
|
|
46
54
|
return :unknown if activity.nil?
|
|
47
55
|
|