still_active 3.0.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +34 -0
- data/README.md +3 -1
- data/lib/still_active/artifactory_client.rb +2 -20
- data/lib/still_active/assessment.rb +85 -0
- data/lib/still_active/cli.rb +84 -23
- data/lib/still_active/config.rb +2 -0
- data/lib/still_active/deps_dev_client.rb +161 -15
- data/lib/still_active/diff.rb +38 -0
- data/lib/still_active/ecosystem_lens.rb +80 -44
- data/lib/still_active/ecosystems_client.rb +10 -7
- data/lib/still_active/errors.rb +10 -0
- data/lib/still_active/forgejo_client.rb +5 -1
- data/lib/still_active/github_client.rb +28 -5
- data/lib/still_active/gitlab_client.rb +5 -1
- data/lib/still_active/go_toolchain.rb +75 -0
- data/lib/still_active/helpers/alternatives_helper.rb +2 -2
- data/lib/still_active/helpers/cvss_helper.rb +14 -48
- data/lib/still_active/helpers/cyclonedx_helper.rb +7 -0
- data/lib/still_active/helpers/diff_markdown_helper.rb +5 -0
- data/lib/still_active/helpers/emoji_helper.rb +2 -1
- data/lib/still_active/helpers/endoflife_helper.rb +3 -2
- data/lib/still_active/helpers/http_cache.rb +109 -0
- data/lib/still_active/helpers/http_helper.rb +148 -28
- data/lib/still_active/helpers/markdown_helper.rb +1 -1
- data/lib/still_active/helpers/repository_check.rb +33 -0
- data/lib/still_active/helpers/status_helper.rb +11 -0
- data/lib/still_active/helpers/summary_helper.rb +6 -1
- data/lib/still_active/helpers/terminal_helper.rb +9 -1
- data/lib/still_active/helpers/vulnerability_helper.rb +4 -5
- data/lib/still_active/options.rb +7 -0
- data/lib/still_active/osv_client.rb +39 -2
- data/lib/still_active/repository_signals.rb +113 -0
- data/lib/still_active/rubygems_client.rb +34 -0
- data/lib/still_active/sarif/rules.rb +1 -1
- data/lib/still_active/sbom_workflow.rb +16 -2
- data/lib/still_active/source_health.rb +88 -0
- data/lib/still_active/version.rb +1 -1
- data/lib/still_active/workflow.rb +63 -52
- data/lib/still_active.rb +1 -0
- data/still_active.gemspec +6 -7
- metadata +13 -6
|
@@ -180,7 +180,7 @@ module StillActive
|
|
|
180
180
|
tags: ["security", "supply-chain", "maintenance", "external/cwe/cwe-1104"],
|
|
181
181
|
neutral: {
|
|
182
182
|
short: "Package is deprecated by its maintainer",
|
|
183
|
-
full: "The maintainer has marked this package deprecated in its registry. Unlike every other maintenance signal here, this is not inferred from dates or repository state: the person who publishes it has said to stop using it, and the deprecation message often names the replacement. A deprecated package can still look healthy by release recency, so this fires independently of the activity signals.",
|
|
183
|
+
full: "The maintainer has marked this package deprecated in its registry. Unlike every other maintenance signal here, this is not inferred from dates or repository state: the person who publishes it has said to stop using it, and the deprecation message often names the replacement. A deprecated package can still look healthy by release recency, so this fires independently of the activity signals. A language runtime counts too: when the Go team ends a Go release line, the toolchain on it is reported here.",
|
|
184
184
|
help_text: "Read the deprecation message, which usually names the successor, and migrate to it. There will be no further fixes, including security patches."
|
|
185
185
|
}
|
|
186
186
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "ecosystem_lens"
|
|
4
|
+
require_relative "assessment"
|
|
5
|
+
require_relative "source_health"
|
|
4
6
|
require_relative "ceiling_reconciler"
|
|
5
7
|
require_relative "poison_security_correlator"
|
|
6
8
|
require_relative "helpers/python_helper"
|
|
@@ -30,7 +32,9 @@ module StillActive
|
|
|
30
32
|
# rate-limited run read "all clear" while silently skipping deps.
|
|
31
33
|
Outcome = Data.define(:assessed, :failures)
|
|
32
34
|
|
|
33
|
-
|
|
35
|
+
# `health` is the run's SourceHealth report: in an ecosystem it distrusts,
|
|
36
|
+
# deps.dev's advisories count as unchecked. The Go toolchain's come from OSV.
|
|
37
|
+
def call(sbom_result, health: SourceHealth::Report.healthy, &on_progress)
|
|
34
38
|
dependencies = sbom_result.dependencies
|
|
35
39
|
Async do
|
|
36
40
|
# The language-runtime support windows, fetched once for the whole SBOM
|
|
@@ -42,6 +46,9 @@ module StillActive
|
|
|
42
46
|
dotnet: safe_support_window(".NET") { DotnetHelper.supported_dotnet_range },
|
|
43
47
|
dotnetfx: safe_support_window(".NET Framework") { DotnetHelper.supported_dotnetfx_range }
|
|
44
48
|
}
|
|
49
|
+
# Every version record in one batch request, rather than one GET each; the
|
|
50
|
+
# Go toolchain's come from OSV and endoflife.date instead.
|
|
51
|
+
DepsDevClient.prefetch_versions(dependencies.filter_map { [_1[:ecosystem], _1[:name], _1[:version]] unless GoToolchain.toolchain?(_1[:ecosystem], _1[:name]) })
|
|
45
52
|
barrier = Async::Barrier.new
|
|
46
53
|
semaphore = Async::Semaphore.new(StillActive.config.parallelism, parent: barrier)
|
|
47
54
|
result = {}
|
|
@@ -78,6 +85,11 @@ module StillActive
|
|
|
78
85
|
end
|
|
79
86
|
end
|
|
80
87
|
barrier.wait
|
|
88
|
+
result.each_value do |data|
|
|
89
|
+
next if health.deps_dev_trusted?(data[:ecosystem]) || GoToolchain.toolchain?(data[:ecosystem], data[:name])
|
|
90
|
+
|
|
91
|
+
data[:vulnerabilities_checked] = false
|
|
92
|
+
end
|
|
81
93
|
# Whole-tree correlation once every package's signals are in: a Python
|
|
82
94
|
# ceiling's "upgrade to lift it" must not contradict a poison finding that
|
|
83
95
|
# caps the same package below that upgrade (same guarantee as the native path).
|
|
@@ -87,10 +99,12 @@ module StillActive
|
|
|
87
99
|
PoisonSecurityCorrelator.correlate(result)
|
|
88
100
|
# Stable, diffable order regardless of async completion order.
|
|
89
101
|
Outcome.new(
|
|
90
|
-
assessed: result.sort_by { |key, _| key }.to_h,
|
|
102
|
+
assessed: result.sort_by { |key, _| key }.to_h.transform_values { Assessment.from(_1) },
|
|
91
103
|
failures: failures.sort_by { |failure| "#{failure[:ecosystem]}/#{failure[:name]}@#{failure[:version]}" }
|
|
92
104
|
)
|
|
93
105
|
end.wait
|
|
106
|
+
ensure
|
|
107
|
+
DepsDevClient.clear_prefetch
|
|
94
108
|
end
|
|
95
109
|
|
|
96
110
|
private
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "time"
|
|
4
|
+
require_relative "deps_dev_client"
|
|
5
|
+
|
|
6
|
+
module StillActive
|
|
7
|
+
# Whether deps.dev is answering in the shape we read and is current, checked once
|
|
8
|
+
# per run before the fan-out. deps.dev is an alpha API and it is the advisory
|
|
9
|
+
# source for everything but ruby-advisory-db and the Go toolchain, so when it
|
|
10
|
+
# isn't healthy its advisories count as unchecked rather than clean.
|
|
11
|
+
#
|
|
12
|
+
# Two checks. The advisory canary asks for a package with permanent advisories
|
|
13
|
+
# and expects them back (a renamed field reads as zero everywhere). The
|
|
14
|
+
# freshness canary asks, per ecosystem in the audit, for packages that publish
|
|
15
|
+
# often and expects one of them to have published in the last 30 days: a stalled index
|
|
16
|
+
# serves stale versions and misses new advisories without any error. Neither
|
|
17
|
+
# catches one stale package in a live index (deps.dev's Go stdlib was that).
|
|
18
|
+
module SourceHealth
|
|
19
|
+
extend self
|
|
20
|
+
|
|
21
|
+
# Packages that publish often; an ecosystem is fresh if ANY of its set has a
|
|
22
|
+
# version published in the window. Chosen by replaying two years of deps.dev
|
|
23
|
+
# publish dates (2026-09-24): the longest stretch with no version from the
|
|
24
|
+
# set is 10 days (nuget), 8.7 (npm), 7 (cargo, rubygems), 6.7 (pypi, maven),
|
|
25
|
+
# 6 (go), so 30 days is a 3x margin. Single canaries weren't enough:
|
|
26
|
+
# aws-sdk-core went 34 days and golang.org/x/net 35, which would have failed
|
|
27
|
+
# a healthy index closed. Recheck with each set's publishedAt history.
|
|
28
|
+
FRESHNESS_CANARIES = {
|
|
29
|
+
rubygems: ["aws-partitions", "aws-sdk-ec2"],
|
|
30
|
+
npm: ["next", "@types/node"],
|
|
31
|
+
pypi: ["boto3", "botocore"],
|
|
32
|
+
cargo: ["aws-sdk-ec2", "oxc_parser", "swc_core"],
|
|
33
|
+
go: ["cloud.google.com/go/storage", "github.com/aws/aws-sdk-go-v2/service/s3"],
|
|
34
|
+
maven: ["software.amazon.awssdk:s3", "software.amazon.awssdk:ec2"],
|
|
35
|
+
nuget: ["AWSSDK.S3", "AWSSDK.Core"]
|
|
36
|
+
}.freeze
|
|
37
|
+
FRESH_WITHIN_SECONDS = 30 * 24 * 60 * 60
|
|
38
|
+
|
|
39
|
+
Report = Data.define(:advisory_schema_ok, :stale_ecosystems) do
|
|
40
|
+
def self.healthy = new(advisory_schema_ok: true, stale_ecosystems: [])
|
|
41
|
+
|
|
42
|
+
# Whether deps.dev's advisories for this ecosystem count as an answer.
|
|
43
|
+
def deps_dev_trusted?(ecosystem)
|
|
44
|
+
advisory_schema_ok && !stale_ecosystems.include?(ecosystem)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def degraded? = !advisory_schema_ok || !stale_ecosystems.empty?
|
|
48
|
+
|
|
49
|
+
def as_json
|
|
50
|
+
{
|
|
51
|
+
status: degraded? ? "degraded" : "ok",
|
|
52
|
+
deps_dev_advisories: advisory_schema_ok ? "ok" : "failed",
|
|
53
|
+
deps_dev_stale_ecosystems: stale_ecosystems.map(&:to_s)
|
|
54
|
+
}
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def warnings
|
|
58
|
+
lines = []
|
|
59
|
+
lines << "deps.dev's advisory check failed (its `advisoryKeys` field may have changed, or the API is unreachable); its advisories count as unchecked unless another source answers for them (ruby-advisory-db, OSV for the Go toolchain)" unless advisory_schema_ok
|
|
60
|
+
stale_ecosystems.each do |ecosystem|
|
|
61
|
+
lines << "deps.dev's #{ecosystem} index has nothing published by #{FRESHNESS_CANARIES[ecosystem].join(", ")} in 30 days (or couldn't be read), so it looks stale; its #{ecosystem} advisories count as unchecked unless another source answers for them, and its latest versions may be behind"
|
|
62
|
+
end
|
|
63
|
+
lines
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def check(ecosystems:, now: Time.now)
|
|
68
|
+
Report.new(
|
|
69
|
+
advisory_schema_ok: DepsDevClient.advisory_schema_ok?,
|
|
70
|
+
stale_ecosystems: ecosystems.uniq.select { FRESHNESS_CANARIES.key?(_1) && !fresh?(_1, now) }
|
|
71
|
+
)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
private
|
|
75
|
+
|
|
76
|
+
# Fresh when any canary has a version published in the window. One that can't
|
|
77
|
+
# be read (after the client's retry) just doesn't count; if none can, the check
|
|
78
|
+
# can't confirm freshness and reads stale.
|
|
79
|
+
def fresh?(ecosystem, now)
|
|
80
|
+
FRESHNESS_CANARIES[ecosystem].any? do |name|
|
|
81
|
+
date = DepsDevClient.newest_publish_date(name: name, system: ecosystem)
|
|
82
|
+
!date.nil? && now - Time.parse(date) < FRESH_WITHIN_SECONDS
|
|
83
|
+
rescue HttpHelper::Unavailable, ArgumentError, TypeError
|
|
84
|
+
false
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
data/lib/still_active/version.rb
CHANGED
|
@@ -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
|
-
|
|
34
|
+
require_relative "rubygems_client"
|
|
31
35
|
|
|
32
36
|
module StillActive
|
|
33
37
|
module Workflow
|
|
34
38
|
extend self
|
|
35
39
|
|
|
36
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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
|
-
#
|
|
564
|
-
#
|
|
565
|
-
#
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
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
|
-
|
|
581
|
-
|
|
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
|
-
|
|
589
|
-
return unless provider.respond_to?(:commits_since_release)
|
|
600
|
+
return unless source == :github && StillActive.config.github_oauth_token
|
|
590
601
|
|
|
591
|
-
|
|
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
|
@@ -55,14 +55,13 @@ Gem::Specification.new do |spec|
|
|
|
55
55
|
# carry an artificial floor.
|
|
56
56
|
spec.add_runtime_dependency("async", ">= 2.2")
|
|
57
57
|
spec.add_runtime_dependency("bundler", ">= 2.0")
|
|
58
|
-
#
|
|
59
|
-
#
|
|
60
|
-
#
|
|
61
|
-
#
|
|
62
|
-
#
|
|
63
|
-
|
|
58
|
+
# CVSS base score from an OSV vector, for the CVSS-4-only advisory deps.dev can't
|
|
59
|
+
# score. 5.0 is the floor because it is the first release without the poison-pill
|
|
60
|
+
# constraints (a Ruby 2.6 floor, a bigdecimal cap, an exact bundler pin) that kept
|
|
61
|
+
# it optional through 3.0; 5.0 also adds base_score on CVSS 4.0, which is the API
|
|
62
|
+
# CvssHelper reads.
|
|
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.
|
|
4
|
+
version: 3.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Sean Floyd
|
|
@@ -38,21 +38,21 @@ dependencies:
|
|
|
38
38
|
- !ruby/object:Gem::Version
|
|
39
39
|
version: '2.0'
|
|
40
40
|
- !ruby/object:Gem::Dependency
|
|
41
|
-
name:
|
|
41
|
+
name: cvss-suite
|
|
42
42
|
requirement: !ruby/object:Gem::Requirement
|
|
43
43
|
requirements:
|
|
44
44
|
- - ">="
|
|
45
45
|
- !ruby/object:Gem::Version
|
|
46
|
-
version: '0'
|
|
46
|
+
version: '5.0'
|
|
47
47
|
type: :runtime
|
|
48
48
|
prerelease: false
|
|
49
49
|
version_requirements: !ruby/object:Gem::Requirement
|
|
50
50
|
requirements:
|
|
51
51
|
- - ">="
|
|
52
52
|
- !ruby/object:Gem::Version
|
|
53
|
-
version: '0'
|
|
53
|
+
version: '5.0'
|
|
54
54
|
- !ruby/object:Gem::Dependency
|
|
55
|
-
name:
|
|
55
|
+
name: faraday-retry
|
|
56
56
|
requirement: !ruby/object:Gem::Requirement
|
|
57
57
|
requirements:
|
|
58
58
|
- - ">="
|
|
@@ -135,6 +135,7 @@ files:
|
|
|
135
135
|
- bin/still_active
|
|
136
136
|
- lib/still_active.rb
|
|
137
137
|
- lib/still_active/artifactory_client.rb
|
|
138
|
+
- lib/still_active/assessment.rb
|
|
138
139
|
- lib/still_active/ceiling_reconciler.rb
|
|
139
140
|
- lib/still_active/cli.rb
|
|
140
141
|
- lib/still_active/compact_index_client.rb
|
|
@@ -149,6 +150,7 @@ files:
|
|
|
149
150
|
- lib/still_active/forgejo_client.rb
|
|
150
151
|
- lib/still_active/github_client.rb
|
|
151
152
|
- lib/still_active/gitlab_client.rb
|
|
153
|
+
- lib/still_active/go_toolchain.rb
|
|
152
154
|
- lib/still_active/helpers/activity_helper.rb
|
|
153
155
|
- lib/still_active/helpers/alternatives_helper.rb
|
|
154
156
|
- lib/still_active/helpers/ansi_helper.rb
|
|
@@ -163,6 +165,7 @@ files:
|
|
|
163
165
|
- lib/still_active/helpers/dotnet_helper.rb
|
|
164
166
|
- lib/still_active/helpers/emoji_helper.rb
|
|
165
167
|
- lib/still_active/helpers/endoflife_helper.rb
|
|
168
|
+
- lib/still_active/helpers/http_cache.rb
|
|
166
169
|
- lib/still_active/helpers/http_helper.rb
|
|
167
170
|
- lib/still_active/helpers/libyear_helper.rb
|
|
168
171
|
- lib/still_active/helpers/lockfile_dependency_parser.rb
|
|
@@ -171,6 +174,7 @@ files:
|
|
|
171
174
|
- lib/still_active/helpers/markdown_helper.rb
|
|
172
175
|
- lib/still_active/helpers/pep440_helper.rb
|
|
173
176
|
- lib/still_active/helpers/python_helper.rb
|
|
177
|
+
- lib/still_active/helpers/repository_check.rb
|
|
174
178
|
- lib/still_active/helpers/ruby_advisory_db.rb
|
|
175
179
|
- lib/still_active/helpers/ruby_helper.rb
|
|
176
180
|
- lib/still_active/helpers/runtime_ceiling_helper.rb
|
|
@@ -187,10 +191,13 @@ files:
|
|
|
187
191
|
- lib/still_active/poison_security_correlator.rb
|
|
188
192
|
- lib/still_active/pypi_client.rb
|
|
189
193
|
- lib/still_active/repository.rb
|
|
194
|
+
- lib/still_active/repository_signals.rb
|
|
195
|
+
- lib/still_active/rubygems_client.rb
|
|
190
196
|
- lib/still_active/sarif/rules.rb
|
|
191
197
|
- lib/still_active/sbom_reader.rb
|
|
192
198
|
- lib/still_active/sbom_workflow.rb
|
|
193
199
|
- lib/still_active/source_credentials.rb
|
|
200
|
+
- lib/still_active/source_health.rb
|
|
194
201
|
- lib/still_active/suppressions.rb
|
|
195
202
|
- lib/still_active/version.rb
|
|
196
203
|
- lib/still_active/workflow.rb
|
|
@@ -219,7 +226,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
219
226
|
- !ruby/object:Gem::Version
|
|
220
227
|
version: 3.2.3
|
|
221
228
|
requirements: []
|
|
222
|
-
rubygems_version: 4.0.
|
|
229
|
+
rubygems_version: 4.0.20
|
|
223
230
|
specification_version: 4
|
|
224
231
|
summary: Audit your dependencies for maintenance health, abandonment, and below-the-fix
|
|
225
232
|
vulnerabilities. Ruby gems natively; npm, PyPI, Cargo, Go, Maven, and NuGet via
|