still_active 1.0.1 → 1.2.0
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 +69 -0
- data/README.md +92 -46
- data/lib/helpers/activity_helper.rb +3 -1
- data/lib/helpers/bundler_helper.rb +30 -1
- data/lib/helpers/emoji_helper.rb +1 -1
- data/lib/helpers/http_helper.rb +38 -10
- data/lib/helpers/libyear_helper.rb +20 -0
- data/lib/helpers/markdown_helper.rb +51 -10
- data/lib/helpers/ruby_helper.rb +83 -0
- data/lib/helpers/terminal_helper.rb +57 -7
- data/lib/helpers/version_helper.rb +1 -1
- data/lib/helpers/vulnerability_helper.rb +36 -0
- data/lib/still_active/cli.rb +41 -12
- data/lib/still_active/config.rb +6 -0
- data/lib/still_active/core_ext.rb +1 -1
- data/lib/still_active/deps_dev_client.rb +18 -0
- data/lib/still_active/gitlab_client.rb +31 -10
- data/lib/still_active/options.rb +22 -1
- data/lib/still_active/repository.rb +4 -1
- data/lib/still_active/version.rb +1 -1
- data/lib/still_active/workflow.rb +143 -13
- data/lib/still_active.rb +4 -0
- data/still_active.gemspec +8 -4
- metadata +10 -23
- data/.github/dependabot.yml +0 -21
- data/.github/workflows/codeql-analysis.yml +0 -39
- data/.github/workflows/publish.yml +0 -19
- data/.github/workflows/rspec.yml +0 -26
- data/.github/workflows/rubocop-analysis.yml +0 -38
- data/.gitignore +0 -18
- data/.rspec +0 -4
- data/.rubocop.yml +0 -23
- data/Gemfile +0 -12
- data/Gemfile.lock +0 -239
- data/Rakefile +0 -12
- data/bin/console +0 -11
- data/bin/setup +0 -8
- data/fixtures/debug_versions.json +0 -38
- data/fixtures/still_active_version.json +0 -9
- data/fixtures/vcr_cassettes/deps_dev_project.yml +0 -46
- data/fixtures/vcr_cassettes/deps_dev_version.yml +0 -56
- data/fixtures/vcr_cassettes/gems.yml +0 -3762
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8c861ae8a727347f9b276576f3da48d806fbc25ab05020d849e5e92c8de49732
|
|
4
|
+
data.tar.gz: bc3e5d7429e17adc0b6b0e955f50a6ddb9e7f157f1c656bf8c9044113cd5a97e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e922835a769aeb9817dd27e99bf194c39e21e43794ee66d6d7d1db4c2c9ccaf11cf40d75ac251302f014f3df539998ab4e285f446fb68b6aaa080259421cc1fb
|
|
7
|
+
data.tar.gz: 932b7e3dbd72cd02492070eced16a77e1a8a1649bb86132f2a7f827339d3f42138633139cbfd5134e31b1c4a92d18b28758afafc74674402f72a9ed2d7512399
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,74 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.2.0] - 2026-02-20
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- `--fail-if-vulnerable[=SEVERITY]` flag: exit 1 if any gem has known vulnerabilities, optionally filtered by severity (low/medium/high/critical)
|
|
8
|
+
- `--fail-if-outdated=LIBYEARS` flag: exit 1 if any gem exceeds the given libyear threshold
|
|
9
|
+
- Coloured OpenSSF column in terminal output: green for strong practices (7.0+), yellow for notably weak (below 4.0)
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- Removed composite health score (0-100) and Health column from terminal, markdown, and JSON output; individual columns (vulns, OpenSSF, activity, version) communicate these signals without collapsing them into one number
|
|
14
|
+
- Replaced `--fail-below-score` with `--fail-if-vulnerable` and `--fail-if-outdated` for targeted CI gating
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- Repository URLs with `.git` suffix (e.g. `socketry/async.git`) caused 404s against GitHub/GitLab APIs
|
|
19
|
+
- GitLab 301 redirects for renamed projects silently failed; now follows up to 3 redirects with trusted host check
|
|
20
|
+
- Network errors (`ECONNRESET`, timeouts, etc.) during RubyGems version lookup or HTTP API calls dropped the entire gem from results instead of warning
|
|
21
|
+
- GitHub Packages URI check used substring match, allowing crafted URLs to bypass host validation; now parses URI and compares host exactly
|
|
22
|
+
- Tri-state `archived?` predicate renamed to `archived` to honestly reflect `true`/`false`/`nil` return contract
|
|
23
|
+
- Rubocop offences from code scanning (WordArray, IfInsideElse, MultilineHash, frozen_string_literal)
|
|
24
|
+
|
|
25
|
+
## [1.1.0] - 2026-02-20
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
|
|
29
|
+
- `--ignore=GEM,GEM2,...` flag to exclude gems from pass/fail checks while keeping them in output
|
|
30
|
+
- `--fail-below-score=SCORE` flag for health-based CI gating (exit 1 if any gem scores below threshold)
|
|
31
|
+
- Yanked version detection: flags pinned versions that have been pulled from RubyGems
|
|
32
|
+
- Archived repo detection via GitHub and GitLab APIs, treated as critical for exit checks
|
|
33
|
+
- Libyear metric: years between installed and latest release per gem, total in summary
|
|
34
|
+
- Advisory enrichment: CVSS scores, titles, and IDs from deps.dev per vulnerability
|
|
35
|
+
- Composite health score (0-100) combining version freshness, activity, OpenSSF Scorecard, and vulnerabilities
|
|
36
|
+
- Health column in terminal and markdown output, system average in terminal summary
|
|
37
|
+
- Ruby version freshness: reports current Ruby version, EOL status, and libyear behind latest via endoflife.date API
|
|
38
|
+
- Source detection: identifies gem source type (rubygems, git, path) from Bundler lockfile
|
|
39
|
+
- Non-rubygems gem handling: git/path-sourced gems show gracefully with source indicator instead of failing silently
|
|
40
|
+
- GitHub Packages registry support: fetches versions from `rubygems.pkg.github.com` using existing `--github-oauth-token` (requires `read:packages` scope)
|
|
41
|
+
- CVSS v2 fallback: older advisories without v3 scores now show severity using v2 scores from deps.dev
|
|
42
|
+
|
|
43
|
+
### Changed
|
|
44
|
+
|
|
45
|
+
- Vulnerability column shows count with highest severity label (e.g. "3 (critical)")
|
|
46
|
+
- Markdown vulnerability column shows advisory IDs
|
|
47
|
+
- Markdown table adds libyear and health columns
|
|
48
|
+
- Terminal summary includes libyear total and health average
|
|
49
|
+
- JSON output wrapped in `{ "gems": ..., "ruby": ... }` structure
|
|
50
|
+
- Version string validation guards against malformed versions from git-sourced gems
|
|
51
|
+
- Progress counter on stderr during gem checking so large Gemfiles don't appear frozen
|
|
52
|
+
- Actionable rate limit message when GitHub API quota is exhausted
|
|
53
|
+
- `--fail-below-score` now validates range (0-100) at parse time
|
|
54
|
+
- `--gems` option stores structured data from the start instead of mutating mid-run
|
|
55
|
+
- API failures (timeouts, HTTP errors, malformed responses) now warn on stderr instead of degrading silently
|
|
56
|
+
- Vulnerability count based on successfully fetched advisories so count and severity always agree
|
|
57
|
+
|
|
58
|
+
### Fixed
|
|
59
|
+
|
|
60
|
+
- Vulnerability counts now checked against installed version, not latest (was masking CVEs in older pinned versions)
|
|
61
|
+
- `GitlabClient.archived?` returned `false` on API failure instead of `nil`, incorrectly asserting repos were not archived
|
|
62
|
+
- `repo_archived?` rescued all `StandardError`, masking bugs; now catches only `Octokit::Error` and `Faraday::Error`
|
|
63
|
+
- `last_commit_date` had no error handling; any failure dropped the entire gem from results
|
|
64
|
+
- Malformed date strings from GitHub/GitLab APIs no longer raise unhandled `ArgumentError`
|
|
65
|
+
|
|
66
|
+
## [1.0.2] - 2026-02-19
|
|
67
|
+
|
|
68
|
+
### Changed
|
|
69
|
+
|
|
70
|
+
- Reduce gem package from 2.4MB to essentials only (lib/, bin/still_active, LICENSE, README, CHANGELOG, gemspec)
|
|
71
|
+
|
|
3
72
|
## [1.0.1] - 2026-02-19
|
|
4
73
|
|
|
5
74
|
### Changed
|
data/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
**How do you know if your Ruby dependencies are still maintained?**
|
|
4
4
|
|
|
5
|
-
`bundle outdated` tells you version drift. `bundler-audit` catches known CVEs. Neither tells you whether anyone is still working on the thing. `still_active` checks maintenance activity, version freshness, security scores, and
|
|
5
|
+
`bundle outdated` tells you version drift. `bundler-audit` catches known CVEs. Neither tells you whether anyone is still working on the thing. `still_active` checks maintenance activity, version freshness, security scores, vulnerabilities, libyear drift, and archived repos for every gem in your Gemfile.
|
|
6
6
|
|
|
7
7
|
[](https://badge.fury.io/rb/still_active)
|
|
8
8
|

|
|
@@ -10,16 +10,18 @@
|
|
|
10
10
|

|
|
11
11
|
|
|
12
12
|
```
|
|
13
|
-
Name
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
13
|
+
Name Version Activity OpenSSF Vulns
|
|
14
|
+
───────────────────────────────────────────────────────────────────
|
|
15
|
+
async 2.36.0 (latest) ok 7.1/10 0
|
|
16
|
+
backbone-rails 1.2.3 (latest) archived 3.6/10 0
|
|
17
|
+
bootstrap-slider-rails 9.8.0 (latest) critical - 0
|
|
18
|
+
gitlab-markup 2.0.0 (latest) ok - 0
|
|
19
|
+
local_gem 0.1.0 (path) - - 0
|
|
20
|
+
nested_form 0.3.2 (git) archived 3.3/10 0
|
|
21
|
+
remotipart 1.4.4 (git) critical 3.1/10 0
|
|
22
|
+
|
|
23
|
+
7 gems: 4 up to date, 0 outdated · 2 active, 2 stale, 2 archived · 0 vulnerabilities
|
|
24
|
+
Ruby 4.0.1 (latest)
|
|
23
25
|
```
|
|
24
26
|
|
|
25
27
|
## Why `still_active`?
|
|
@@ -29,11 +31,16 @@ Most dependency tools answer one question. `still_active` answers all of them at
|
|
|
29
31
|
| | `bundle outdated` | `bundler-audit` | `libyear-bundler` | **`still_active`** |
|
|
30
32
|
| ---------------------------- | ----------------- | --------------- | ----------------- | ---------------------------- |
|
|
31
33
|
| Outdated versions | Yes | - | Yes | **Yes** |
|
|
32
|
-
| Known vulnerabilities (CVEs) | - | Yes | - | **Yes**
|
|
34
|
+
| Known vulnerabilities (CVEs) | - | Yes | - | **Yes** (with severity) |
|
|
33
35
|
| OpenSSF Scorecard | - | - | - | **Yes** |
|
|
34
36
|
| Last commit activity | - | - | - | **Yes** |
|
|
37
|
+
| Libyear drift | - | - | Yes | **Yes** |
|
|
38
|
+
| Archived repo detection | - | - | - | **Yes** |
|
|
39
|
+
| Yanked version detection | - | - | - | **Yes** |
|
|
40
|
+
| Ruby version freshness | - | - | - | **Yes** (EOL + libyear) |
|
|
41
|
+
| Git/path/GH Packages sources | - | - | - | **Yes** |
|
|
35
42
|
| GitLab support | - | - | - | **Yes** |
|
|
36
|
-
| CI quality gates | - | Exit code | - | **Yes**
|
|
43
|
+
| CI quality gates | - | Exit code | - | **Yes** (5 modes) |
|
|
37
44
|
| Multiple output formats | - | - | - | **Terminal, JSON, Markdown** |
|
|
38
45
|
| Single command | Yes | Yes | Yes | **Yes** |
|
|
39
46
|
|
|
@@ -54,8 +61,11 @@ still_active
|
|
|
54
61
|
# check specific gems
|
|
55
62
|
still_active --gems=rails,nokogiri,sidekiq
|
|
56
63
|
|
|
57
|
-
# CI pipeline: fail if any gem is critically stale
|
|
58
|
-
still_active --fail-if-critical
|
|
64
|
+
# CI pipeline: fail if any gem is critically stale or has vulnerabilities
|
|
65
|
+
still_active --fail-if-critical --fail-if-vulnerable
|
|
66
|
+
|
|
67
|
+
# ignore specific gems in CI checks
|
|
68
|
+
still_active --fail-if-warning --ignore=legacy_gem,internal_gem
|
|
59
69
|
|
|
60
70
|
# markdown table for pull requests or documentation
|
|
61
71
|
still_active --markdown
|
|
@@ -86,6 +96,10 @@ Usage: still_active [options]
|
|
|
86
96
|
--warning-range-end=YEARS maximum years since last activity that triggers a warning (beyond this is critical)
|
|
87
97
|
--fail-if-critical Exit 1 if any gem has critical activity warning
|
|
88
98
|
--fail-if-warning Exit 1 if any gem has warning or critical activity warning
|
|
99
|
+
--fail-if-vulnerable[=SEVERITY]
|
|
100
|
+
Exit 1 if any gem has vulnerabilities (optionally at or above SEVERITY)
|
|
101
|
+
--fail-if-outdated=LIBYEARS Exit 1 if any gem exceeds LIBYEARS behind latest
|
|
102
|
+
--ignore=GEM,GEM2,... Exclude gems from pass/fail checks (still shown in output)
|
|
89
103
|
--critical-warning-emoji=EMOJI
|
|
90
104
|
--futurist-emoji=EMOJI
|
|
91
105
|
--success-emoji=EMOJI
|
|
@@ -102,32 +116,44 @@ Usage: still_active [options]
|
|
|
102
116
|
**JSON** (default when piped) -- structured data for automation:
|
|
103
117
|
|
|
104
118
|
```bash
|
|
105
|
-
still_active --json --
|
|
119
|
+
still_active --json --gemfile=spec/still_active/edge_case_gemfile/Gemfile
|
|
106
120
|
```
|
|
107
121
|
|
|
108
122
|
```json
|
|
109
123
|
{
|
|
110
|
-
"
|
|
111
|
-
"
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
124
|
+
"gems": {
|
|
125
|
+
"async": {
|
|
126
|
+
"source_type": "rubygems",
|
|
127
|
+
"version_used": "2.36.0",
|
|
128
|
+
"latest_version": "2.36.0",
|
|
129
|
+
"repository_url": "https://github.com/socketry/async",
|
|
130
|
+
"last_commit_date": "2026-01-22 04:09:48 UTC",
|
|
131
|
+
"archived": false,
|
|
132
|
+
"scorecard_score": 7.1,
|
|
133
|
+
"vulnerability_count": 0,
|
|
134
|
+
"libyear": 0.0
|
|
135
|
+
},
|
|
136
|
+
"nested_form": {
|
|
137
|
+
"source_type": "git",
|
|
138
|
+
"version_used": "0.3.2",
|
|
139
|
+
"repository_url": "https://github.com/ryanb/nested_form",
|
|
140
|
+
"last_commit_date": "2021-12-11 21:47:02 UTC",
|
|
141
|
+
"archived": true,
|
|
142
|
+
"scorecard_score": 3.3,
|
|
143
|
+
"vulnerability_count": 0
|
|
144
|
+
},
|
|
145
|
+
"local_gem": {
|
|
146
|
+
"source_type": "path",
|
|
147
|
+
"version_used": "0.1.0",
|
|
148
|
+
"scorecard_score": null,
|
|
149
|
+
"vulnerability_count": 0
|
|
150
|
+
}
|
|
120
151
|
},
|
|
121
|
-
"
|
|
122
|
-
"
|
|
123
|
-
"
|
|
124
|
-
"
|
|
125
|
-
"
|
|
126
|
-
"repository_url": "https://github.com/sparklemotion/nokogiri",
|
|
127
|
-
"last_commit_date": "2026-02-17 19:13:22 UTC",
|
|
128
|
-
"scorecard_score": 6.5,
|
|
129
|
-
"vulnerability_count": 0,
|
|
130
|
-
"ruby_gems_url": "https://rubygems.org/gems/nokogiri"
|
|
152
|
+
"ruby": {
|
|
153
|
+
"version": "4.0.1",
|
|
154
|
+
"eol": false,
|
|
155
|
+
"latest_version": "4.0.1",
|
|
156
|
+
"libyear": 0.0
|
|
131
157
|
}
|
|
132
158
|
}
|
|
133
159
|
```
|
|
@@ -138,18 +164,37 @@ still_active --json --gems=rails,nokogiri
|
|
|
138
164
|
still_active --markdown
|
|
139
165
|
```
|
|
140
166
|
|
|
141
|
-
| activity | up to date? | OpenSSF | vulns | name
|
|
142
|
-
| -------- | ----------- | ------- | ----- |
|
|
143
|
-
|
|
|
144
|
-
|
|
|
145
|
-
|
|
|
167
|
+
| activity | up to date? | OpenSSF | vulns | name | version used | latest version | latest pre-release | last commit | libyear |
|
|
168
|
+
| -------- | ----------- | ------- | ----- | ------------------------------------------------------------ | -------------------------------------------------------------------------- | -------------------------------------------------------------------------- | ------------------ | ----------------------------------------------------- | ------- |
|
|
169
|
+
| | ✅ | 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 |
|
|
170
|
+
| 🚩 | ✅ | 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 |
|
|
171
|
+
| ❓ | ❓ | ❓ | ✅ | local_gem | 0.1.0 (path) | ❓ | ❓ | ❓ | - |
|
|
172
|
+
| 🚩 | ❓ | 3.3/10 | ✅ | [nested_form](https://github.com/ryanb/nested_form) | 0.3.2 (git) | ❓ | ❓ | [2021/12](https://github.com/ryanb/nested_form) | - |
|
|
173
|
+
|
|
174
|
+
**Ruby 4.0.1** (latest) ✅
|
|
146
175
|
|
|
147
176
|
### CI quality gating
|
|
148
177
|
|
|
149
|
-
Use
|
|
178
|
+
Use exit-code flags to fail CI pipelines based on dependency status:
|
|
150
179
|
|
|
151
180
|
```bash
|
|
152
|
-
|
|
181
|
+
# fail on critically stale or archived gems
|
|
182
|
+
still_active --fail-if-critical --json
|
|
183
|
+
|
|
184
|
+
# fail on any stale, critical, or archived gem
|
|
185
|
+
still_active --fail-if-warning --json
|
|
186
|
+
|
|
187
|
+
# fail if any gem has known vulnerabilities
|
|
188
|
+
still_active --fail-if-vulnerable --json
|
|
189
|
+
|
|
190
|
+
# fail only on high/critical severity vulnerabilities
|
|
191
|
+
still_active --fail-if-vulnerable=high --json
|
|
192
|
+
|
|
193
|
+
# fail if any gem is more than 3 libyears behind
|
|
194
|
+
still_active --fail-if-outdated=3 --json
|
|
195
|
+
|
|
196
|
+
# combine flags and exclude known exceptions
|
|
197
|
+
still_active --fail-if-warning --fail-if-vulnerable --ignore=legacy_gem --json
|
|
153
198
|
```
|
|
154
199
|
|
|
155
200
|
### Activity thresholds
|
|
@@ -162,9 +207,10 @@ Activity is determined by the most recent signal across last commit date, latest
|
|
|
162
207
|
|
|
163
208
|
### Data sources
|
|
164
209
|
|
|
165
|
-
- **Versions and release dates** from [RubyGems.org](https://rubygems.org)
|
|
166
|
-
- **Last commit date** from the [GitHub](https://docs.github.com/en/rest) or [GitLab](https://docs.gitlab.com/ee/api/) API
|
|
167
|
-
- **OpenSSF Scorecard** and **
|
|
210
|
+
- **Versions and release dates** from [RubyGems.org](https://rubygems.org) or [GitHub Packages](https://docs.github.com/en/packages)
|
|
211
|
+
- **Last commit date and archived status** from the [GitHub](https://docs.github.com/en/rest) or [GitLab](https://docs.gitlab.com/ee/api/) API
|
|
212
|
+
- **OpenSSF Scorecard**, **vulnerability counts**, and **CVSS severity** from Google's [deps.dev](https://deps.dev) API
|
|
213
|
+
- **Ruby version freshness** from [endoflife.date](https://endoflife.date)
|
|
168
214
|
|
|
169
215
|
### Configuration defaults
|
|
170
216
|
|
|
@@ -8,8 +8,10 @@ module StillActive
|
|
|
8
8
|
|
|
9
9
|
using StillActive::CoreExt
|
|
10
10
|
|
|
11
|
-
# Returns :ok, :stale, :critical, or :unknown
|
|
11
|
+
# Returns :archived, :ok, :stale, :critical, or :unknown
|
|
12
12
|
def activity_level(gem_data)
|
|
13
|
+
return :archived if gem_data[:archived]
|
|
14
|
+
|
|
13
15
|
most_recent = [
|
|
14
16
|
gem_data[:last_commit_date],
|
|
15
17
|
gem_data[:latest_version_release_date],
|
|
@@ -12,7 +12,36 @@ module StillActive
|
|
|
12
12
|
.locked_gems
|
|
13
13
|
.specs
|
|
14
14
|
.select { |spec| gemfile_gems.include?(spec.name) }
|
|
15
|
-
.
|
|
15
|
+
.map do |spec|
|
|
16
|
+
{
|
|
17
|
+
name: spec.name,
|
|
18
|
+
version: spec.version.version,
|
|
19
|
+
source_type: detect_source_type(spec),
|
|
20
|
+
source_uri: detect_source_uri(spec),
|
|
21
|
+
}
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def detect_source_type(spec)
|
|
28
|
+
case spec.source
|
|
29
|
+
when ::Bundler::Source::Rubygems then :rubygems
|
|
30
|
+
when ::Bundler::Source::Git then :git
|
|
31
|
+
when ::Bundler::Source::Path then :path
|
|
32
|
+
else :unknown
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def detect_source_uri(spec)
|
|
37
|
+
case spec.source
|
|
38
|
+
when ::Bundler::Source::Rubygems
|
|
39
|
+
spec.source.remotes&.first&.to_s
|
|
40
|
+
when ::Bundler::Source::Git
|
|
41
|
+
spec.source.uri
|
|
42
|
+
when ::Bundler::Source::Path
|
|
43
|
+
spec.source.path&.to_s
|
|
44
|
+
end
|
|
16
45
|
end
|
|
17
46
|
end
|
|
18
47
|
end
|
data/lib/helpers/emoji_helper.rb
CHANGED
|
@@ -10,7 +10,7 @@ module StillActive
|
|
|
10
10
|
case ActivityHelper.activity_level(result_hash)
|
|
11
11
|
when :ok then ""
|
|
12
12
|
when :stale then StillActive.config.warning_emoji
|
|
13
|
-
when :critical then StillActive.config.critical_warning_emoji
|
|
13
|
+
when :archived, :critical then StillActive.config.critical_warning_emoji
|
|
14
14
|
when :unknown then StillActive.config.unsure_emoji
|
|
15
15
|
end
|
|
16
16
|
end
|
data/lib/helpers/http_helper.rb
CHANGED
|
@@ -5,6 +5,9 @@ require "json"
|
|
|
5
5
|
|
|
6
6
|
module StillActive
|
|
7
7
|
module HttpHelper
|
|
8
|
+
TRUSTED_HOSTS = ["github.com", "gitlab.com", "api.deps.dev", "endoflife.date", "rubygems.pkg.github.com"].freeze
|
|
9
|
+
MAX_REDIRECTS = 3
|
|
10
|
+
|
|
8
11
|
extend self
|
|
9
12
|
|
|
10
13
|
def get_json(base_uri, path, headers: {}, params: {})
|
|
@@ -12,19 +15,44 @@ module StillActive
|
|
|
12
15
|
uri.path = path
|
|
13
16
|
uri.query = URI.encode_www_form(params) unless params.empty?
|
|
14
17
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
18
|
+
MAX_REDIRECTS.times do
|
|
19
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
20
|
+
http.use_ssl = true
|
|
21
|
+
http.open_timeout = 10
|
|
22
|
+
http.read_timeout = 10
|
|
23
|
+
|
|
24
|
+
request = Net::HTTP::Get.new(uri)
|
|
25
|
+
headers.each { |key, value| request[key] = value }
|
|
26
|
+
|
|
27
|
+
response = http.request(request)
|
|
19
28
|
|
|
20
|
-
|
|
21
|
-
|
|
29
|
+
if response.is_a?(Net::HTTPRedirection)
|
|
30
|
+
redirect_uri = uri + response["Location"]
|
|
31
|
+
unless TRUSTED_HOSTS.include?(redirect_uri.host)
|
|
32
|
+
$stderr.puts("warning: #{uri.host}#{uri.path} redirected to untrusted host #{redirect_uri.host}, skipping")
|
|
33
|
+
return
|
|
34
|
+
end
|
|
35
|
+
$stderr.puts("warning: #{uri.host}#{uri.path} redirected to #{redirect_uri.host}#{redirect_uri.path} (stale metadata?)")
|
|
36
|
+
headers = {} if redirect_uri.host != uri.host
|
|
37
|
+
uri = redirect_uri
|
|
38
|
+
next
|
|
39
|
+
end
|
|
22
40
|
|
|
23
|
-
|
|
24
|
-
|
|
41
|
+
unless response.is_a?(Net::HTTPSuccess)
|
|
42
|
+
$stderr.puts("warning: #{uri.host}#{uri.path} returned HTTP #{response.code}") unless response.is_a?(Net::HTTPNotFound)
|
|
43
|
+
return
|
|
44
|
+
end
|
|
25
45
|
|
|
26
|
-
|
|
27
|
-
|
|
46
|
+
return JSON.parse(response.body)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
$stderr.puts("warning: #{uri.host}#{uri.path} too many redirects")
|
|
50
|
+
nil
|
|
51
|
+
rescue Net::OpenTimeout, Net::ReadTimeout, SocketError, Errno::ECONNREFUSED, Errno::ECONNRESET => e
|
|
52
|
+
$stderr.puts("warning: #{uri.host}#{uri.path} failed: #{e.class} (#{e.message})")
|
|
53
|
+
nil
|
|
54
|
+
rescue JSON::ParserError => e
|
|
55
|
+
$stderr.puts("warning: #{uri.host}#{uri.path} returned invalid JSON: #{e.message}")
|
|
28
56
|
nil
|
|
29
57
|
end
|
|
30
58
|
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../still_active/core_ext"
|
|
4
|
+
|
|
5
|
+
module StillActive
|
|
6
|
+
module LibyearHelper
|
|
7
|
+
extend self
|
|
8
|
+
|
|
9
|
+
def gem_libyear(version_used_release_date:, latest_version_release_date:)
|
|
10
|
+
return if version_used_release_date.nil? || latest_version_release_date.nil?
|
|
11
|
+
|
|
12
|
+
diff = latest_version_release_date - version_used_release_date
|
|
13
|
+
[diff / CoreExt::SECONDS_PER_YEAR, 0.0].max.round(1)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def total_libyear(result)
|
|
17
|
+
result.each_value.sum { |d| d[:libyear] || 0.0 }
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -1,12 +1,33 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "vulnerability_helper"
|
|
4
|
+
|
|
3
5
|
module StillActive
|
|
4
6
|
module MarkdownHelper
|
|
5
7
|
extend self
|
|
6
8
|
|
|
9
|
+
def ruby_line(ruby_info)
|
|
10
|
+
version = ruby_info[:version]
|
|
11
|
+
latest = ruby_info[:latest_version]
|
|
12
|
+
libyear = ruby_info[:libyear]
|
|
13
|
+
eol = ruby_info[:eol]
|
|
14
|
+
eol_date = ruby_info[:eol_date]
|
|
15
|
+
|
|
16
|
+
return "**Ruby #{version}** (latest) #{StillActive.config.success_emoji}" if version == latest
|
|
17
|
+
|
|
18
|
+
libyear_part = libyear ? "#{libyear} libyears behind #{latest}" : "behind #{latest}"
|
|
19
|
+
|
|
20
|
+
if eol
|
|
21
|
+
eol_part = eol_date ? "EOL #{eol_date.strftime("%Y-%m-%d")}" : "EOL"
|
|
22
|
+
"**Ruby #{version}** (#{eol_part}, #{libyear_part}) #{StillActive.config.critical_warning_emoji}"
|
|
23
|
+
else
|
|
24
|
+
"**Ruby #{version}** (#{libyear_part}) #{StillActive.config.warning_emoji}"
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
7
28
|
def markdown_table_header_line
|
|
8
|
-
"| activity | up to date? | OpenSSF | vulns | name | version used | latest version | latest pre-release | last commit |\n" \
|
|
9
|
-
"| -------- | ----------- | ------- | ----- | ---- | ------------ | -------------- | ------------------ | ----------- |"
|
|
29
|
+
"| activity | up to date? | OpenSSF | vulns | name | version used | latest version | latest pre-release | last commit | libyear |\n" \
|
|
30
|
+
"| -------- | ----------- | ------- | ----- | ---- | ------------ | -------------- | ------------------ | ----------- | ------- |"
|
|
10
31
|
end
|
|
11
32
|
|
|
12
33
|
def markdown_table_body_line(gem_name:, data:)
|
|
@@ -18,11 +39,17 @@ module StillActive
|
|
|
18
39
|
|
|
19
40
|
formatted_name = markdown_url(text: gem_name, url: repository_url)
|
|
20
41
|
|
|
21
|
-
formatted_version_used =
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
42
|
+
formatted_version_used = if [:git, :path].include?(data[:source_type])
|
|
43
|
+
data[:version_used] ? "#{data[:version_used]} (#{data[:source_type]})" : "(#{data[:source_type]})"
|
|
44
|
+
elsif data[:version_yanked]
|
|
45
|
+
"#{data[:version_used]} (YANKED #{StillActive.config.critical_warning_emoji})"
|
|
46
|
+
else
|
|
47
|
+
version_with_date(
|
|
48
|
+
text: data[:version_used],
|
|
49
|
+
url: version_url(ruby_gems_url, data[:version_used]),
|
|
50
|
+
date: data[:version_used_release_date],
|
|
51
|
+
)
|
|
52
|
+
end
|
|
26
53
|
|
|
27
54
|
formatted_latest_version = version_with_date(
|
|
28
55
|
text: data[:latest_version],
|
|
@@ -44,12 +71,13 @@ module StillActive
|
|
|
44
71
|
inactive_repository_emoji || unsure,
|
|
45
72
|
using_latest_version_emoji || unsure,
|
|
46
73
|
format_scorecard(data[:scorecard_score]),
|
|
47
|
-
format_vulns(data
|
|
74
|
+
format_vulns(data),
|
|
48
75
|
formatted_name,
|
|
49
76
|
formatted_version_used || unsure,
|
|
50
77
|
formatted_latest_version || unsure,
|
|
51
78
|
formatted_latest_pre_release || unsure,
|
|
52
79
|
formatted_last_commit || unsure,
|
|
80
|
+
format_libyear(data[:libyear]),
|
|
53
81
|
]
|
|
54
82
|
|
|
55
83
|
"| #{cells.join(" | ")} |"
|
|
@@ -79,11 +107,24 @@ module StillActive
|
|
|
79
107
|
"#{score}/10"
|
|
80
108
|
end
|
|
81
109
|
|
|
82
|
-
def
|
|
110
|
+
def format_libyear(value)
|
|
111
|
+
return "-" if value.nil?
|
|
112
|
+
|
|
113
|
+
"#{value}y"
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def format_vulns(data)
|
|
117
|
+
count = data[:vulnerability_count]
|
|
83
118
|
return StillActive.config.unsure_emoji if count.nil?
|
|
84
119
|
return StillActive.config.success_emoji if count.zero?
|
|
85
120
|
|
|
86
|
-
|
|
121
|
+
vulnerabilities = data[:vulnerabilities] || []
|
|
122
|
+
severity = VulnerabilityHelper.highest_severity(vulnerabilities)
|
|
123
|
+
ids = vulnerabilities.flat_map { |v| [v[:id], *v[:aliases]] }.compact.uniq.first(3)
|
|
124
|
+
|
|
125
|
+
parts = [severity ? "#{count} (#{severity})" : count.to_s]
|
|
126
|
+
parts << ids.join(", ") unless ids.empty?
|
|
127
|
+
parts.join(" ")
|
|
87
128
|
end
|
|
88
129
|
|
|
89
130
|
def markdown_url(text:, url:)
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "time"
|
|
4
|
+
require_relative "http_helper"
|
|
5
|
+
require_relative "libyear_helper"
|
|
6
|
+
|
|
7
|
+
module StillActive
|
|
8
|
+
module RubyHelper
|
|
9
|
+
extend self
|
|
10
|
+
|
|
11
|
+
ENDOFLIFE_URI = URI("https://endoflife.date/")
|
|
12
|
+
|
|
13
|
+
def ruby_freshness
|
|
14
|
+
return unless standard_ruby?
|
|
15
|
+
|
|
16
|
+
cycles = fetch_cycles
|
|
17
|
+
return if cycles.nil?
|
|
18
|
+
|
|
19
|
+
current = current_ruby_version
|
|
20
|
+
current_cycle = find_cycle(cycles, current)
|
|
21
|
+
latest_cycle = cycles.first
|
|
22
|
+
|
|
23
|
+
return if latest_cycle.nil?
|
|
24
|
+
|
|
25
|
+
latest_version = latest_cycle["latest"]
|
|
26
|
+
latest_release_date = parse_date(latest_cycle["releaseDate"])
|
|
27
|
+
current_release_date = parse_date(current_cycle&.dig("releaseDate"))
|
|
28
|
+
eol_value = current_cycle&.dig("eol")
|
|
29
|
+
|
|
30
|
+
{
|
|
31
|
+
version: current,
|
|
32
|
+
release_date: current_release_date,
|
|
33
|
+
eol_date: parse_eol(eol_value),
|
|
34
|
+
eol: eol_reached?(eol_value),
|
|
35
|
+
latest_version: latest_version,
|
|
36
|
+
latest_release_date: latest_release_date,
|
|
37
|
+
libyear: LibyearHelper.gem_libyear(
|
|
38
|
+
version_used_release_date: current_release_date,
|
|
39
|
+
latest_version_release_date: latest_release_date,
|
|
40
|
+
),
|
|
41
|
+
}
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def standard_ruby?
|
|
47
|
+
RUBY_ENGINE == "ruby"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def current_ruby_version
|
|
51
|
+
RUBY_VERSION
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def fetch_cycles
|
|
55
|
+
HttpHelper.get_json(ENDOFLIFE_URI, "/api/ruby.json")
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def find_cycle(cycles, version)
|
|
59
|
+
major_minor = version.split(".")[0..1].join(".")
|
|
60
|
+
cycles.find { |c| c["cycle"] == major_minor }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def parse_date(date_string)
|
|
64
|
+
return if date_string.nil?
|
|
65
|
+
|
|
66
|
+
Time.parse(date_string)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def parse_eol(value)
|
|
70
|
+
case value
|
|
71
|
+
when String then parse_date(value)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def eol_reached?(value)
|
|
76
|
+
case value
|
|
77
|
+
when true then true
|
|
78
|
+
when false then false
|
|
79
|
+
when String then Time.parse(value) <= Time.now
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|