still_active 3.0.0.rc1 → 3.0.0.rc3
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 +9 -3
- data/README.md +102 -402
- data/lib/helpers/dependency_helper.rb +20 -0
- data/lib/helpers/dotnet_helper.rb +142 -0
- data/lib/helpers/markdown_helper.rb +16 -7
- data/lib/helpers/sarif_helper.rb +48 -14
- data/lib/helpers/terminal_helper.rb +18 -4
- data/lib/helpers/version_helper.rb +40 -1
- data/lib/still_active/cli.rb +59 -15
- data/lib/still_active/deps_dev_client.rb +52 -2
- data/lib/still_active/ecosystem_lens.rb +47 -5
- data/lib/still_active/options.rb +1 -1
- data/lib/still_active/sbom_workflow.rb +22 -11
- data/lib/still_active/version.rb +1 -1
- data/lib/still_active/workflow.rb +4 -1
- metadata +3 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "../helpers/http_helper"
|
|
4
|
+
require_relative "../helpers/version_helper"
|
|
4
5
|
|
|
5
6
|
module StillActive
|
|
6
7
|
module DepsDevClient
|
|
@@ -61,12 +62,36 @@ module StillActive
|
|
|
61
62
|
versions = body["versions"]
|
|
62
63
|
return unless versions.is_a?(Array) && !versions.empty?
|
|
63
64
|
|
|
64
|
-
|
|
65
|
+
# deps.dev's `isDefault` is NOT reliably the latest release: cargo/wasi flags
|
|
66
|
+
# 0.7.0 (2019) while 0.14.7 (2025) ships, and pypi/httpx flags a 1.0.0.dev3
|
|
67
|
+
# prerelease over the 0.28.1 stable. Trusting it reads an active package as
|
|
68
|
+
# years-stale (a false SA002 "abandoned") and paints a downgrade as an
|
|
69
|
+
# upgrade. So rank by version and take the newest STABLE release; fall back to
|
|
70
|
+
# isDefault, then newest-by-date, only when no stable version parses (a
|
|
71
|
+
# genuinely prerelease-only package still reads active, not dormant).
|
|
72
|
+
entry = latest_stable_version(versions) ||
|
|
73
|
+
versions.find { |v| v.is_a?(Hash) && v["isDefault"] } ||
|
|
74
|
+
newest_version(versions)
|
|
65
75
|
return if entry.nil?
|
|
66
76
|
|
|
67
77
|
{ version: entry.dig("versionKey", "version"), published_at: entry["publishedAt"] }
|
|
68
78
|
end
|
|
69
79
|
|
|
80
|
+
# The newest non-prerelease version by version number (not publishedAt: a
|
|
81
|
+
# backported patch on an old line can post-date the latest major). nil when no
|
|
82
|
+
# version parses as a stable release.
|
|
83
|
+
def latest_stable_version(versions)
|
|
84
|
+
versions
|
|
85
|
+
.filter_map do |v|
|
|
86
|
+
next unless v.is_a?(Hash) && !v["isDeprecated"]
|
|
87
|
+
|
|
88
|
+
gem_version = VersionHelper.comparable(v.dig("versionKey", "version"))
|
|
89
|
+
[gem_version, v] if gem_version && !gem_version.prerelease?
|
|
90
|
+
end
|
|
91
|
+
.max_by(&:first)
|
|
92
|
+
&.last
|
|
93
|
+
end
|
|
94
|
+
|
|
70
95
|
# The package's most recent release date (ISO8601 string), or nil. This is
|
|
71
96
|
# the cross-ecosystem freshness signal, and deps.dev's publishedAt is more
|
|
72
97
|
# reliable than ecosyste.ms's latest_release_published_at, which can lag badly
|
|
@@ -96,6 +121,21 @@ module StillActive
|
|
|
96
121
|
}
|
|
97
122
|
end
|
|
98
123
|
|
|
124
|
+
# The NuGet target framework monikers a version declares (net6.0, net45,
|
|
125
|
+
# netstandard2.0, ...), or [] when unknown. On the NuGet-specific
|
|
126
|
+
# `:requirements` endpoint, not the version endpoint. The language-runtime
|
|
127
|
+
# ceiling (SA009) reads these to detect a package that targets only EOL .NET
|
|
128
|
+
# runtimes. Degrades to [] on 404/failure, never raising.
|
|
129
|
+
def target_frameworks(name:, version:)
|
|
130
|
+
return [] if name.nil? || version.nil?
|
|
131
|
+
|
|
132
|
+
path = "/v3alpha/systems/nuget/packages/#{encode(name)}/versions/#{encode(version)}:requirements"
|
|
133
|
+
body = HttpHelper.get_json(BASE_URI, path)
|
|
134
|
+
return [] if body.nil?
|
|
135
|
+
|
|
136
|
+
Array(body.dig("nuget", "targetFrameworks")).grep(String)
|
|
137
|
+
end
|
|
138
|
+
|
|
99
139
|
def advisory_detail(advisory_id:)
|
|
100
140
|
return if advisory_id.nil?
|
|
101
141
|
|
|
@@ -176,6 +216,14 @@ module StillActive
|
|
|
176
216
|
# Trims a repo URL's path to just the project. On GitLab the project path
|
|
177
217
|
# ends at the "/-/" separator (before tree/blob/etc) and may be nested;
|
|
178
218
|
# elsewhere it's owner/repo. Drops trailing slashes and a ".git" suffix.
|
|
219
|
+
# github.com top-level paths that are not user/org repositories; deps.dev
|
|
220
|
+
# occasionally returns one as SOURCE_REPO (a github.com/sponsors/<user> funding
|
|
221
|
+
# link). Parsing it as owner/repo yields a 404 and a blank repo cell, so treat a
|
|
222
|
+
# reserved first segment as "no repo" rather than a bogus lookup.
|
|
223
|
+
GITHUB_RESERVED_PATHS = [
|
|
224
|
+
"sponsors", "orgs", "marketplace", "apps", "topics", "settings", "notifications", "about", "pricing", "features", "security", "contact",
|
|
225
|
+
].freeze
|
|
226
|
+
|
|
179
227
|
def repo_path_segments(host, segments)
|
|
180
228
|
segments = segments.reject(&:empty?)
|
|
181
229
|
|
|
@@ -183,6 +231,8 @@ module StillActive
|
|
|
183
231
|
separator = segments.index("-")
|
|
184
232
|
segments = segments[0...separator] if separator
|
|
185
233
|
else
|
|
234
|
+
return [] if host.to_s.casecmp?("github.com") && GITHUB_RESERVED_PATHS.include?(segments.first&.downcase)
|
|
235
|
+
|
|
186
236
|
segments = segments.first(2)
|
|
187
237
|
end
|
|
188
238
|
|
|
@@ -196,7 +246,7 @@ module StillActive
|
|
|
196
246
|
# is chronological), and a dated release always wins over an undated one.
|
|
197
247
|
def newest_version(versions)
|
|
198
248
|
versions
|
|
199
|
-
.select { |v| v.is_a?(Hash) && v["publishedAt"] }
|
|
249
|
+
.select { |v| v.is_a?(Hash) && v["publishedAt"] && !v["isDeprecated"] }
|
|
200
250
|
.max_by { |v| v["publishedAt"].to_s }
|
|
201
251
|
end
|
|
202
252
|
|
|
@@ -8,6 +8,7 @@ require_relative "pypi_client"
|
|
|
8
8
|
require "time"
|
|
9
9
|
require_relative "../helpers/activity_helper"
|
|
10
10
|
require_relative "../helpers/constraint_helper"
|
|
11
|
+
require_relative "../helpers/dotnet_helper"
|
|
11
12
|
require_relative "../helpers/libyear_helper"
|
|
12
13
|
require_relative "../helpers/pep440_helper"
|
|
13
14
|
require_relative "../helpers/runtime_ceiling_helper"
|
|
@@ -52,7 +53,7 @@ module StillActive
|
|
|
52
53
|
# deps.dev; silence beats a false "holds your tree hostage".
|
|
53
54
|
FLAT_RESOLUTION_ECOSYSTEMS = [:rubygems, :pypi].freeze
|
|
54
55
|
|
|
55
|
-
def assess(ecosystem:, name:, version:, constraint_cache: {},
|
|
56
|
+
def assess(ecosystem:, name:, version:, constraint_cache: {}, runtime_ranges: {})
|
|
56
57
|
info = DepsDevClient.version_info(gem_name: name, version: version, system: ecosystem)
|
|
57
58
|
default = DepsDevClient.default_version_info(name: name, system: ecosystem)
|
|
58
59
|
# Retry the version lookup once when it came back empty but the package DID
|
|
@@ -87,6 +88,10 @@ module StillActive
|
|
|
87
88
|
name: name,
|
|
88
89
|
version_used: version,
|
|
89
90
|
version_used_release_date: used_release_date,
|
|
91
|
+
# The latest stable version string, so the shared formatters can render
|
|
92
|
+
# the "behind X"/up-to-date delta cross-ecosystem the same way the native
|
|
93
|
+
# path does. nil when deps.dev has no default version for the package.
|
|
94
|
+
latest_version: latest_version,
|
|
90
95
|
latest_version_release_date: latest_release_date,
|
|
91
96
|
# libyear parity with the native path: how far behind latest the locked
|
|
92
97
|
# version is, in release-years. Both dates come from deps.dev responses
|
|
@@ -109,7 +114,7 @@ module StillActive
|
|
|
109
114
|
}
|
|
110
115
|
gem_data[:version_unresolved] = true if version_unresolved
|
|
111
116
|
attach_constraints(gem_data, ecosystem: ecosystem, name: name, version: version, cache: constraint_cache)
|
|
112
|
-
attach_language_ceiling(gem_data, ecosystem: ecosystem, name: name, version: version, latest_version: default&.dig(:version),
|
|
117
|
+
attach_language_ceiling(gem_data, ecosystem: ecosystem, name: name, version: version, latest_version: default&.dig(:version), runtime_ranges: runtime_ranges)
|
|
113
118
|
gem_data
|
|
114
119
|
end
|
|
115
120
|
|
|
@@ -124,9 +129,17 @@ module StillActive
|
|
|
124
129
|
# pypi package; other ecosystems carry no ceiling here rather than a wrong one
|
|
125
130
|
# (cargo's rust_version is a soft MSRV hint, not an install wall). Best-effort:
|
|
126
131
|
# a nil range (endoflife feed down) or absent requires_python yields nothing.
|
|
127
|
-
def attach_language_ceiling(gem_data, ecosystem:, name:, version:, latest_version:,
|
|
132
|
+
def attach_language_ceiling(gem_data, ecosystem:, name:, version:, latest_version:, runtime_ranges:)
|
|
133
|
+
finding =
|
|
134
|
+
case ecosystem
|
|
135
|
+
when :pypi then python_ceiling(name: name, version: version, latest_version: latest_version, python_range: runtime_ranges[:python])
|
|
136
|
+
when :nuget then dotnet_ceiling(name: name, version: version, latest_version: latest_version, dotnet: runtime_ranges[:dotnet], dotnetfx: runtime_ranges[:dotnetfx])
|
|
137
|
+
end
|
|
138
|
+
gem_data[:language_ceiling] = finding if finding
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def python_ceiling(name:, version:, latest_version:, python_range:)
|
|
128
142
|
return if python_range.nil?
|
|
129
|
-
return unless ecosystem == :pypi
|
|
130
143
|
|
|
131
144
|
requirement = Pep440Helper.to_gem_requirement_string(PypiClient.requires_python(name: name, version: version))
|
|
132
145
|
finding = requirement && RuntimeCeilingHelper.analyze(requirement: requirement, support_window: python_range)
|
|
@@ -134,7 +147,36 @@ module StillActive
|
|
|
134
147
|
|
|
135
148
|
finding[:runtime] = "Python"
|
|
136
149
|
finding[:fixed_by_upgrade] = python_ceiling_lifted_by_upgrade?(name: name, version: version, latest_version: latest_version, python_range: python_range)
|
|
137
|
-
|
|
150
|
+
finding
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# The .NET ceiling: a NuGet package whose ONLY runtime targets are end-of-life
|
|
154
|
+
# frameworks caps you onto a dead runtime (a restore-time NU1202 wall). Unlike
|
|
155
|
+
# Python's requires_python range, .NET declares a SET of target frameworks;
|
|
156
|
+
# DotnetHelper carries the set logic and the netstandard escape-hatch rule.
|
|
157
|
+
def dotnet_ceiling(name:, version:, latest_version:, dotnet:, dotnetfx:)
|
|
158
|
+
return if dotnet.nil? && dotnetfx.nil?
|
|
159
|
+
|
|
160
|
+
frameworks = DepsDevClient.target_frameworks(name: name, version: version)
|
|
161
|
+
return if frameworks.empty?
|
|
162
|
+
|
|
163
|
+
finding = DotnetHelper.analyze(target_frameworks: frameworks, dotnet: dotnet, dotnetfx: dotnetfx)
|
|
164
|
+
return if finding.nil?
|
|
165
|
+
|
|
166
|
+
finding[:fixed_by_upgrade] = dotnet_ceiling_lifted_by_upgrade?(name: name, version: version, latest_version: latest_version, dotnet: dotnet, dotnetfx: dotnetfx)
|
|
167
|
+
finding
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Positive confirmation, like the Python check: only claim an upgrade lifts the
|
|
171
|
+
# ceiling when the latest version's targets ACTUALLY clear it (a fresh fetch),
|
|
172
|
+
# never inferring "safe to bump" from a failed lookup.
|
|
173
|
+
def dotnet_ceiling_lifted_by_upgrade?(name:, version:, latest_version:, dotnet:, dotnetfx:)
|
|
174
|
+
return false if latest_version.nil? || latest_version == version
|
|
175
|
+
|
|
176
|
+
latest_frameworks = DepsDevClient.target_frameworks(name: name, version: latest_version)
|
|
177
|
+
return false if latest_frameworks.empty?
|
|
178
|
+
|
|
179
|
+
DotnetHelper.analyze(target_frameworks: latest_frameworks, dotnet: dotnet, dotnetfx: dotnetfx).nil?
|
|
138
180
|
end
|
|
139
181
|
|
|
140
182
|
# Does upgrading the package to its latest release lift the cap? Requires
|
data/lib/still_active/options.rb
CHANGED
|
@@ -62,7 +62,7 @@ module StillActive
|
|
|
62
62
|
end
|
|
63
63
|
|
|
64
64
|
def add_sbom_option(opts)
|
|
65
|
-
opts.on("--sbom=PATH", String, "audit a CycloneDX SBOM cross-ecosystem (npm/pypi/cargo/go/maven/nuget) instead of a Gemfile;
|
|
65
|
+
opts.on("--sbom=PATH", String, "audit a CycloneDX SBOM cross-ecosystem (npm/pypi/cargo/go/maven/nuget) instead of a Gemfile; honours --sarif/--markdown/--terminal/--json") do |value|
|
|
66
66
|
options[:provided_sbom] = true
|
|
67
67
|
StillActive.config { |config| config.sbom_path = value }
|
|
68
68
|
end
|
|
@@ -4,6 +4,7 @@ require_relative "ecosystem_lens"
|
|
|
4
4
|
require_relative "ceiling_reconciler"
|
|
5
5
|
require_relative "poison_security_correlator"
|
|
6
6
|
require_relative "../helpers/python_helper"
|
|
7
|
+
require_relative "../helpers/dotnet_helper"
|
|
7
8
|
require "async"
|
|
8
9
|
require "async/barrier"
|
|
9
10
|
require "async/semaphore"
|
|
@@ -32,16 +33,15 @@ module StillActive
|
|
|
32
33
|
def call(sbom_result, &on_progress)
|
|
33
34
|
dependencies = sbom_result.dependencies
|
|
34
35
|
Async do
|
|
35
|
-
# The
|
|
36
|
-
#
|
|
37
|
-
# a feed failure degrades to "no ceiling checks"
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
end
|
|
36
|
+
# The language-runtime support windows, fetched once for the whole SBOM
|
|
37
|
+
# (pypi reads :python; nuget reads :dotnet/:dotnetfx). Guarded per feed like
|
|
38
|
+
# the native Ruby path: a feed failure degrades to "no ceiling checks" for
|
|
39
|
+
# that runtime, never aborts the audit.
|
|
40
|
+
runtime_ranges = {
|
|
41
|
+
python: safe_support_window("Python") { PythonHelper.supported_python_range },
|
|
42
|
+
dotnet: safe_support_window(".NET") { DotnetHelper.supported_dotnet_range },
|
|
43
|
+
dotnetfx: safe_support_window(".NET Framework") { DotnetHelper.supported_dotnetfx_range },
|
|
44
|
+
}
|
|
45
45
|
barrier = Async::Barrier.new
|
|
46
46
|
semaphore = Async::Semaphore.new(StillActive.config.parallelism, parent: barrier)
|
|
47
47
|
result = {}
|
|
@@ -61,7 +61,7 @@ module StillActive
|
|
|
61
61
|
# when the SBOM actually marked it (SbomReader leaves the key absent
|
|
62
62
|
# otherwise): absent stays "unknown", never a false that reads as test-only.
|
|
63
63
|
result["#{dep[:ecosystem]}/#{dep[:name]}@#{dep[:version]}"] =
|
|
64
|
-
EcosystemLens.assess(ecosystem: dep[:ecosystem], name: dep[:name], version: dep[:version], constraint_cache: constraint_cache,
|
|
64
|
+
EcosystemLens.assess(ecosystem: dep[:ecosystem], name: dep[:name], version: dep[:version], constraint_cache: constraint_cache, runtime_ranges: runtime_ranges)
|
|
65
65
|
.merge(dep.slice(:production))
|
|
66
66
|
rescue StandardError => e
|
|
67
67
|
# One dependency's failure must not abort the audit, but it must not
|
|
@@ -92,5 +92,16 @@ module StillActive
|
|
|
92
92
|
)
|
|
93
93
|
end.wait
|
|
94
94
|
end
|
|
95
|
+
|
|
96
|
+
private
|
|
97
|
+
|
|
98
|
+
# Fetch one runtime's support window, degrading a feed failure to nil (no
|
|
99
|
+
# ceiling checks for that runtime) with a warning, never aborting the audit.
|
|
100
|
+
def safe_support_window(label)
|
|
101
|
+
yield
|
|
102
|
+
rescue StandardError => e
|
|
103
|
+
$stderr.puts("warning: #{label} support window lookup failed: #{e.class} (#{e.message}); skipping language-ceiling checks")
|
|
104
|
+
nil
|
|
105
|
+
end
|
|
95
106
|
end
|
|
96
107
|
end
|
data/lib/still_active/version.rb
CHANGED
|
@@ -153,7 +153,10 @@ module StillActive
|
|
|
153
153
|
commit_date = signals[:last_commit_date]
|
|
154
154
|
archived = signals[:archived]
|
|
155
155
|
last_release = VersionHelper.find_version(versions: vs, pre_release: false)
|
|
156
|
-
last_pre_release = VersionHelper.
|
|
156
|
+
last_pre_release = VersionHelper.upcoming_pre_release(
|
|
157
|
+
pre_release: VersionHelper.find_version(versions: vs, pre_release: true),
|
|
158
|
+
release: last_release,
|
|
159
|
+
)
|
|
157
160
|
deps_dev = fetch_deps_dev_info(
|
|
158
161
|
gem_name: gem_name,
|
|
159
162
|
version: gem_version || VersionHelper.gem_version(version_hash: last_release),
|
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.0.0.
|
|
4
|
+
version: 3.0.0.rc3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Sean Floyd
|
|
@@ -254,7 +254,9 @@ files:
|
|
|
254
254
|
- lib/helpers/constraint_helper.rb
|
|
255
255
|
- lib/helpers/cvss_helper.rb
|
|
256
256
|
- lib/helpers/cyclonedx_helper.rb
|
|
257
|
+
- lib/helpers/dependency_helper.rb
|
|
257
258
|
- lib/helpers/diff_markdown_helper.rb
|
|
259
|
+
- lib/helpers/dotnet_helper.rb
|
|
258
260
|
- lib/helpers/emoji_helper.rb
|
|
259
261
|
- lib/helpers/endoflife_helper.rb
|
|
260
262
|
- lib/helpers/http_helper.rb
|