still_active 3.1.0 → 3.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.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +27 -0
  3. data/README.md +3 -1
  4. data/lib/still_active/artifactory_client.rb +2 -20
  5. data/lib/still_active/assessment.rb +85 -0
  6. data/lib/still_active/cli.rb +84 -23
  7. data/lib/still_active/config.rb +2 -0
  8. data/lib/still_active/deps_dev_client.rb +161 -15
  9. data/lib/still_active/diff.rb +38 -0
  10. data/lib/still_active/ecosystem_lens.rb +80 -44
  11. data/lib/still_active/ecosystems_client.rb +10 -7
  12. data/lib/still_active/errors.rb +10 -0
  13. data/lib/still_active/forgejo_client.rb +5 -1
  14. data/lib/still_active/github_client.rb +28 -5
  15. data/lib/still_active/gitlab_client.rb +5 -1
  16. data/lib/still_active/go_toolchain.rb +75 -0
  17. data/lib/still_active/helpers/alternatives_helper.rb +2 -2
  18. data/lib/still_active/helpers/cyclonedx_helper.rb +7 -0
  19. data/lib/still_active/helpers/diff_markdown_helper.rb +5 -0
  20. data/lib/still_active/helpers/emoji_helper.rb +2 -1
  21. data/lib/still_active/helpers/endoflife_helper.rb +3 -2
  22. data/lib/still_active/helpers/http_cache.rb +109 -0
  23. data/lib/still_active/helpers/http_helper.rb +148 -28
  24. data/lib/still_active/helpers/markdown_helper.rb +1 -1
  25. data/lib/still_active/helpers/repository_check.rb +33 -0
  26. data/lib/still_active/helpers/status_helper.rb +11 -0
  27. data/lib/still_active/helpers/summary_helper.rb +6 -1
  28. data/lib/still_active/helpers/terminal_helper.rb +9 -1
  29. data/lib/still_active/options.rb +7 -0
  30. data/lib/still_active/osv_client.rb +39 -2
  31. data/lib/still_active/repository_signals.rb +113 -0
  32. data/lib/still_active/rubygems_client.rb +34 -0
  33. data/lib/still_active/sarif/rules.rb +1 -1
  34. data/lib/still_active/sbom_workflow.rb +16 -2
  35. data/lib/still_active/source_health.rb +88 -0
  36. data/lib/still_active/version.rb +1 -1
  37. data/lib/still_active/workflow.rb +63 -52
  38. data/lib/still_active.rb +1 -0
  39. data/still_active.gemspec +0 -1
  40. metadata +9 -16
@@ -4,6 +4,9 @@ require_relative "artifactory_client"
4
4
  require_relative "compact_index_client"
5
5
  require_relative "source_credentials"
6
6
  require_relative "ceiling_reconciler"
7
+ require_relative "errors"
8
+ require_relative "assessment"
9
+ require_relative "repository_signals"
7
10
  require_relative "deps_dev_client"
8
11
  require_relative "osv_client"
9
12
  require_relative "poison_security_correlator"
@@ -19,6 +22,7 @@ require_relative "helpers/constraint_helper"
19
22
  require_relative "helpers/http_helper"
20
23
  require_relative "helpers/libyear_helper"
21
24
  require_relative "helpers/ruby_advisory_db"
25
+ require_relative "source_health"
22
26
  require_relative "helpers/ruby_helper"
23
27
  require_relative "helpers/runtime_ceiling_helper"
24
28
  require_relative "helpers/version_helper"
@@ -27,13 +31,16 @@ require "async"
27
31
  require "async/barrier"
28
32
  require "async/semaphore"
29
33
  require "cgi"
30
- require "gems"
34
+ require_relative "rubygems_client"
31
35
 
32
36
  module StillActive
33
37
  module Workflow
34
38
  extend self
35
39
 
36
- def call(&on_progress)
40
+ # `health` is the run's SourceHealth report: when it distrusts deps.dev for
41
+ # rubygems, deps.dev's advisories count as unchecked unless ruby-advisory-db
42
+ # answered for them.
43
+ def call(health: SourceHealth::Report.healthy, &on_progress)
37
44
  task = Async do
38
45
  # Load the optional ruby-advisory-db once, before the fan-out, so the
39
46
  # read-only Database is shared across fibers rather than reloaded per gem.
@@ -53,11 +60,14 @@ module StillActive
53
60
  nil
54
61
  end
55
62
  # Resolve the GitHub token once here, single-fibered, before the fan-out:
56
- # provider_for reads it per gem across fibers and gh_cli_token shells out,
63
+ # RepositorySignals reads it per gem across fibers and gh_cli_token shells out,
57
64
  # so resolving eagerly keeps that off the concurrent path and guarantees
58
65
  # every fiber sees one consistent value (token -> live GitHub, incl. private
59
66
  # repos; only a genuinely absent token falls back to ecosyste.ms).
60
67
  StillActive.config.github_oauth_token
68
+ # Every pinned gem's deps.dev version record in one batch request, rather
69
+ # than one GET each. The same names the per-gem lookups already send.
70
+ DepsDevClient.prefetch_versions(StillActive.config.gems.filter_map { [:rubygems, _1[:name], _1[:version]] if _1[:version] })
61
71
  barrier = Async::Barrier.new
62
72
  semaphore = Async::Semaphore.new(StillActive.config.parallelism, parent: barrier)
63
73
  result_object = {}
@@ -83,18 +93,20 @@ module StillActive
83
93
  constraint_cache: constraint_cache,
84
94
  ruby_range: ruby_range
85
95
  )
86
- rescue Octokit::TooManyRequests
87
- $stderr.print("\r\e[K") if on_progress
88
- warn("rate limited checking #{gem[:name]}: set GITHUB_TOKEN to increase your limit")
89
96
  rescue => e
90
97
  $stderr.print("\r\e[K") if on_progress
91
98
  warn("error occurred for #{gem[:name]}: #{e.class}\n\t#{e.message}")
92
99
  ensure
100
+ # An assessment that raised part-way is left without its answers;
101
+ # Assessment.from reads those as unchecked and failed.
93
102
  completed += 1
94
103
  on_progress&.call(completed, total)
95
104
  end
96
105
  end
97
106
  barrier.wait
107
+ unless health.deps_dev_trusted?(:rubygems) || advisory_db
108
+ result_object.each_value { |data| data[:vulnerabilities_checked] = false }
109
+ end
98
110
  # Whole-tree correlation, once every gem's signals are in: a ceiling's
99
111
  # "upgrade to lift it" must not contradict a poison finding that caps the
100
112
  # same gem below that upgrade.
@@ -105,9 +117,13 @@ module StillActive
105
117
  # Gems are inserted as their async tasks finish, so the natural order is
106
118
  # nondeterministic completion order. Sort by name once here so every
107
119
  # consumer (JSON, SARIF, the baseline diff) gets a stable, diffable order.
108
- result_object.sort_by { |name, _| name }.to_h
120
+ # Every pass over the hashes is done; from here on consumers read the
121
+ # finished Assessment.
122
+ result_object.sort_by { |name, _| name }.to_h.transform_values { Assessment.from(_1) }
109
123
  end
110
124
  task.wait
125
+ ensure
126
+ DepsDevClient.clear_prefetch
111
127
  end
112
128
 
113
129
  def ruby_freshness
@@ -150,7 +166,8 @@ module StillActive
150
166
  signals = repo_signals(
151
167
  source: repo_info[:source],
152
168
  repository_owner: repo_info[:owner],
153
- repository_name: repo_info[:name]
169
+ repository_name: repo_info[:name],
170
+ public: public_source?(source_uri)
154
171
  )
155
172
  commit_date = signals[:last_commit_date]
156
173
  archived = signals[:archived]
@@ -174,6 +191,7 @@ module StillActive
174
191
  repository_url: repo_info[:url],
175
192
  last_commit_date: commit_date,
176
193
  archived: archived,
194
+ **repository_availability(signals),
177
195
  **deps_dev
178
196
  })
179
197
 
@@ -286,7 +304,8 @@ module StillActive
286
304
  def gem_info_non_rubygems(gem_name:, gem_version:, result_object:, source_uri: nil, advisory_db: nil)
287
305
  repo_info = repository_info_for_non_rubygems(gem_name: gem_name, source_uri: source_uri)
288
306
  source, owner, name = repo_info.values_at(:source, :owner, :name)
289
- deps_dev = gem_version ? fetch_deps_dev_info(gem_name: gem_name, version: gem_version, advisory_db: advisory_db) : {}
307
+ # With no version there is nothing to look advisories up by.
308
+ deps_dev = gem_version ? fetch_deps_dev_info(gem_name: gem_name, version: gem_version, advisory_db: advisory_db) : {vulnerabilities_checked: false}
290
309
 
291
310
  # Fall back to repo-derived project_id for scorecard when deps.dev doesn't
292
311
  # have the version. Use ||= so a maintained score already found via the
@@ -303,6 +322,7 @@ module StillActive
303
322
  repository_url: repo_info[:url],
304
323
  last_commit_date: signals[:last_commit_date],
305
324
  archived: signals[:archived],
325
+ **repository_availability(signals),
306
326
  **deps_dev
307
327
  })
308
328
  end
@@ -371,7 +391,16 @@ module StillActive
371
391
  end
372
392
 
373
393
  def fetch_deps_dev_info(gem_name:, version:, advisory_db: nil)
374
- info = DepsDevClient.version_info(gem_name: gem_name, version: version)
394
+ # deps.dev failing to answer leaves the advisories unchecked, unless
395
+ # ruby-advisory-db, the Ruby authority, is loaded and answers for them.
396
+ # No version (the latest-version lookup failed too) means nothing was asked.
397
+ checked = !version.nil?
398
+ info = begin
399
+ DepsDevClient.version_info(gem_name: gem_name, version: version)
400
+ rescue HttpHelper::Unavailable
401
+ checked = !advisory_db.nil?
402
+ nil
403
+ end
375
404
  scorecard = DepsDevClient.project_scorecard(project_id: info&.dig(:project_id))
376
405
  advisory_keys = info&.dig(:advisory_keys) || []
377
406
  # The keys ARE the evidence the version is vulnerable; the detail fetch only
@@ -397,6 +426,7 @@ module StillActive
397
426
  deprecated: info&.dig(:deprecated) == true,
398
427
  deprecation_reason: info&.dig(:deprecation_reason),
399
428
  vulnerability_count: vulnerabilities.length,
429
+ vulnerabilities_checked: checked,
400
430
  vulnerabilities: vulnerabilities
401
431
  }
402
432
  end
@@ -409,20 +439,8 @@ module StillActive
409
439
  elsif unqueryable_private_source?(source_uri)
410
440
  private_source_versions(gem_name: gem_name, source_uri: source_uri)
411
441
  else
412
- Gems.versions(gem_name)
442
+ RubygemsClient.versions(gem_name)
413
443
  end
414
- rescue Gems::NotFound
415
- []
416
- # Gems::GemError is the `gems` library's catch-all for any non-success,
417
- # non-404 response -- crucially a 429 rate-limit or a 5xx. Left unrescued it
418
- # escapes to the per-gem rescue in #call and strips the gem of ALL its signals
419
- # (not just versions), blaming a generic "error occurred". The poison-pill
420
- # enrichment adds extra version lookups that make a 429 likelier, so a
421
- # best-effort feature must not be able to degrade an unrelated gem's core data:
422
- # degrade to "no versions known" here, exactly like Gems::NotFound.
423
- rescue Gems::GemError, *HttpHelper::TRANSPORT_ERRORS => e
424
- warn("warning: rubygems.org versions lookup failed for #{gem_name}: #{e.class} (#{e.message})")
425
- []
426
444
  end
427
445
 
428
446
  def github_packages_uri?(uri)
@@ -433,7 +451,7 @@ module StillActive
433
451
 
434
452
  # A rubygems-type source that isn't public rubygems.org and that we have no
435
453
  # client for (Gemfury, Gemstash, geminabox, a private mirror). We must NOT
436
- # fall through to Gems.versions, which always hits public rubygems.org: that
454
+ # fall through to RubygemsClient.versions, which always hits public rubygems.org: that
437
455
  # would silently report a public name-collision's data, or blanks, as if it
438
456
  # were the private gem's. github_packages/artifactory are handled above, so
439
457
  # anything left with a non-rubygems.org host is unqueryable. Refs #43.
@@ -501,7 +519,8 @@ module StillActive
501
519
  repo.merge(project_id: deps_dev_project_id(repo))
502
520
  end
503
521
 
504
- # deps.dev scorecards index github.com and gitlab.com only. A Forgejo/Codeberg
522
+ # Of the forges Repository recognizes, deps.dev indexes github.com and
523
+ # gitlab.com (DepsDevClient::PROJECT_HOSTS). A Forgejo/Codeberg
505
524
  # repo has no deps.dev project, so leave its project_id nil rather than minting
506
525
  # a bogus github.com/owner/name that would fetch the wrong (or no) scorecard.
507
526
  DEPS_DEV_HOST_BY_SOURCE = {github: "github.com", gitlab: "gitlab.com"}.freeze
@@ -522,7 +541,7 @@ module StillActive
522
541
  end
523
542
 
524
543
  # Locally-installed gem metadata and the gem's own version payload are
525
- # source-accurate. This public rubygems.org Gems.info lookup is the last
544
+ # source-accurate. This public rubygems.org info lookup is the last
526
545
  # resort, and is skipped for an unqueryable private source: otherwise a
527
546
  # public name-collision's repo/archived/last-commit data would stand in for
528
547
  # the private gem, the same substitution #43 prevents for versions.
@@ -549,46 +568,38 @@ module StillActive
549
568
  end
550
569
 
551
570
  def rubygems_gem_repository_url(gem_name:)
552
- info = Gems.info(gem_name)
571
+ info = RubygemsClient.info(gem_name)
553
572
  return [] if info.nil?
554
573
 
555
574
  [
556
575
  info["homepage_uri"],
557
576
  info["source_code_uri"]
558
577
  ].compact.uniq
559
- rescue Gems::NotFound
560
- []
561
578
  end
562
579
 
563
- # The repo-signal provider for a source, or nil for an unhandled host. Every
564
- # provider answers archived/last_commit_date; richer signals (e.g.
565
- # commits_since_release) are duck-typed and dispatched by respond_to?, so a
566
- # provider opts into them by defining the method, with no base class and no
567
- # assumption that every source supports every signal.
568
- def provider_for(source)
569
- case source
570
- # Without a GitHub token, the live API caps at 60 req/hr -- unusable past a
571
- # handful of gems. Fall back to ecosyste.ms (5000 anonymous) so a large
572
- # Gemfile still resolves. With a token, the live API stays primary (freshest,
573
- # and it carries commits_since_release, which ecosyste.ms doesn't).
574
- when :github then StillActive.config.github_oauth_token ? GithubClient : EcosystemsClient
575
- when :gitlab then GitlabClient
576
- when :forgejo then ForgejoClient
577
- end
580
+ # archived + last-activity date, and which service answered, from
581
+ # RepositorySignals. `public:` says the gem came from a public registry, so
582
+ # its repository's name may go to ecosyste.ms.
583
+ def repo_signals(source:, repository_owner:, repository_name:, public: false)
584
+ host = Repository::SOURCE_BY_HOST.key(source)
585
+ RepositorySignals.for(host: host, owner: repository_owner, name: repository_name, public: public)
586
+ end
587
+
588
+ # rubygems.org, as opposed to a private registry the lockfile names.
589
+ def public_source?(source_uri)
590
+ !(github_packages_uri?(source_uri) || ArtifactoryClient.artifactory_uri?(source_uri) || unqueryable_private_source?(source_uri))
578
591
  end
579
592
 
580
- # One provider call yields both archived and the last-activity date (the
581
- # repo object carries both), so a gem's repo signals cost a single request
582
- # instead of two. Returns {} for an unhandled host.
583
- def repo_signals(source:, repository_owner:, repository_name:)
584
- provider_for(source)&.repo_signals(owner: repository_owner, name: repository_name) || {}
593
+ def repository_availability(signals)
594
+ {repository_check: signals[:check], **(signals[:source] ? {repository_source: signals[:source]} : {})}
585
595
  end
586
596
 
597
+ # GitHub's compare endpoint, and only with a token: it's a per-gem call, and
598
+ # 60 anonymous requests an hour would starve the repository lookups.
587
599
  def unreleased_commits(source:, repository_owner:, repository_name:, version:)
588
- provider = provider_for(source)
589
- return unless provider.respond_to?(:commits_since_release)
600
+ return unless source == :github && StillActive.config.github_oauth_token
590
601
 
591
- provider.commits_since_release(owner: repository_owner, name: repository_name, version: version)
602
+ GithubClient.commits_since_release(owner: repository_owner, name: repository_name, version: version)
592
603
  end
593
604
  end
594
605
  end
data/lib/still_active.rb CHANGED
@@ -4,6 +4,7 @@ require_relative "still_active/version"
4
4
  require_relative "still_active/errors"
5
5
  require_relative "still_active/config"
6
6
  require_relative "still_active/sbom_reader"
7
+ require_relative "still_active/source_health"
7
8
  require_relative "still_active/ecosystem_lens"
8
9
  require_relative "still_active/sbom_workflow"
9
10
  require_relative "still_active/cli"
data/still_active.gemspec CHANGED
@@ -62,7 +62,6 @@ Gem::Specification.new do |spec|
62
62
  # CvssHelper reads.
63
63
  spec.add_runtime_dependency("cvss-suite", ">= 5.0")
64
64
  spec.add_runtime_dependency("faraday-retry")
65
- spec.add_runtime_dependency("gems")
66
65
  spec.add_runtime_dependency("octokit")
67
66
  # Spec-compliant package-URL parsing for SBOM ingestion (decodes npm scopes,
68
67
  # maven group:artifact, qualifiers). 0.1 is the verified floor; the official
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: still_active
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.1.0
4
+ version: 3.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sean Floyd
@@ -65,20 +65,6 @@ dependencies:
65
65
  - - ">="
66
66
  - !ruby/object:Gem::Version
67
67
  version: '0'
68
- - !ruby/object:Gem::Dependency
69
- name: gems
70
- requirement: !ruby/object:Gem::Requirement
71
- requirements:
72
- - - ">="
73
- - !ruby/object:Gem::Version
74
- version: '0'
75
- type: :runtime
76
- prerelease: false
77
- version_requirements: !ruby/object:Gem::Requirement
78
- requirements:
79
- - - ">="
80
- - !ruby/object:Gem::Version
81
- version: '0'
82
68
  - !ruby/object:Gem::Dependency
83
69
  name: octokit
84
70
  requirement: !ruby/object:Gem::Requirement
@@ -149,6 +135,7 @@ files:
149
135
  - bin/still_active
150
136
  - lib/still_active.rb
151
137
  - lib/still_active/artifactory_client.rb
138
+ - lib/still_active/assessment.rb
152
139
  - lib/still_active/ceiling_reconciler.rb
153
140
  - lib/still_active/cli.rb
154
141
  - lib/still_active/compact_index_client.rb
@@ -163,6 +150,7 @@ files:
163
150
  - lib/still_active/forgejo_client.rb
164
151
  - lib/still_active/github_client.rb
165
152
  - lib/still_active/gitlab_client.rb
153
+ - lib/still_active/go_toolchain.rb
166
154
  - lib/still_active/helpers/activity_helper.rb
167
155
  - lib/still_active/helpers/alternatives_helper.rb
168
156
  - lib/still_active/helpers/ansi_helper.rb
@@ -177,6 +165,7 @@ files:
177
165
  - lib/still_active/helpers/dotnet_helper.rb
178
166
  - lib/still_active/helpers/emoji_helper.rb
179
167
  - lib/still_active/helpers/endoflife_helper.rb
168
+ - lib/still_active/helpers/http_cache.rb
180
169
  - lib/still_active/helpers/http_helper.rb
181
170
  - lib/still_active/helpers/libyear_helper.rb
182
171
  - lib/still_active/helpers/lockfile_dependency_parser.rb
@@ -185,6 +174,7 @@ files:
185
174
  - lib/still_active/helpers/markdown_helper.rb
186
175
  - lib/still_active/helpers/pep440_helper.rb
187
176
  - lib/still_active/helpers/python_helper.rb
177
+ - lib/still_active/helpers/repository_check.rb
188
178
  - lib/still_active/helpers/ruby_advisory_db.rb
189
179
  - lib/still_active/helpers/ruby_helper.rb
190
180
  - lib/still_active/helpers/runtime_ceiling_helper.rb
@@ -201,10 +191,13 @@ files:
201
191
  - lib/still_active/poison_security_correlator.rb
202
192
  - lib/still_active/pypi_client.rb
203
193
  - lib/still_active/repository.rb
194
+ - lib/still_active/repository_signals.rb
195
+ - lib/still_active/rubygems_client.rb
204
196
  - lib/still_active/sarif/rules.rb
205
197
  - lib/still_active/sbom_reader.rb
206
198
  - lib/still_active/sbom_workflow.rb
207
199
  - lib/still_active/source_credentials.rb
200
+ - lib/still_active/source_health.rb
208
201
  - lib/still_active/suppressions.rb
209
202
  - lib/still_active/version.rb
210
203
  - lib/still_active/workflow.rb
@@ -233,7 +226,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
233
226
  - !ruby/object:Gem::Version
234
227
  version: 3.2.3
235
228
  requirements: []
236
- rubygems_version: 4.0.16
229
+ rubygems_version: 4.0.20
237
230
  specification_version: 4
238
231
  summary: Audit your dependencies for maintenance health, abandonment, and below-the-fix
239
232
  vulnerabilities. Ruby gems natively; npm, PyPI, Cargo, Go, Maven, and NuGet via