still_active 3.0.0.rc4 → 3.0.0.rc5
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 +3 -0
- data/lib/helpers/activity_helper.rb +3 -3
- data/lib/helpers/alternatives_helper.rb +1 -1
- data/lib/helpers/ansi_helper.rb +1 -1
- data/lib/helpers/bot_context.rb +7 -7
- data/lib/helpers/bundler_helper.rb +1 -2
- data/lib/helpers/catalog_index.rb +1 -1
- data/lib/helpers/constraint_helper.rb +6 -6
- data/lib/helpers/cvss_helper.rb +2 -2
- data/lib/helpers/cyclonedx_helper.rb +14 -14
- data/lib/helpers/diff_markdown_helper.rb +4 -4
- data/lib/helpers/dotnet_helper.rb +4 -4
- data/lib/helpers/endoflife_helper.rb +3 -3
- data/lib/helpers/http_helper.rb +31 -19
- data/lib/helpers/lockfile_dependency_parser.rb +2 -2
- data/lib/helpers/markdown_escape.rb +1 -1
- data/lib/helpers/markdown_helper.rb +7 -7
- data/lib/helpers/ruby_advisory_db.rb +1 -1
- data/lib/helpers/ruby_helper.rb +3 -3
- data/lib/helpers/runtime_ceiling_helper.rb +2 -2
- data/lib/helpers/sarif_helper.rb +34 -34
- data/lib/helpers/status_helper.rb +1 -1
- data/lib/helpers/summary_helper.rb +1 -1
- data/lib/helpers/terminal_helper.rb +8 -8
- data/lib/helpers/vulnerability_helper.rb +1 -1
- data/lib/still_active/artifactory_client.rb +47 -18
- data/lib/still_active/cli.rb +38 -38
- data/lib/still_active/compact_index_client.rb +117 -0
- data/lib/still_active/config.rb +1 -1
- data/lib/still_active/config_file.rb +4 -4
- data/lib/still_active/deps_dev_client.rb +7 -7
- data/lib/still_active/diff.rb +12 -12
- data/lib/still_active/ecosystem_lens.rb +5 -5
- data/lib/still_active/ecosystems_client.rb +7 -7
- data/lib/still_active/forgejo_client.rb +3 -3
- data/lib/still_active/github_client.rb +6 -6
- data/lib/still_active/gitlab_client.rb +3 -3
- data/lib/still_active/options.rb +4 -4
- data/lib/still_active/osv_client.rb +8 -8
- data/lib/still_active/poison_security_correlator.rb +3 -3
- data/lib/still_active/pypi_client.rb +2 -2
- data/lib/still_active/repository.rb +4 -4
- data/lib/still_active/sarif/rules.rb +20 -20
- data/lib/still_active/sbom_reader.rb +8 -8
- data/lib/still_active/sbom_workflow.rb +7 -7
- data/lib/still_active/source_credentials.rb +39 -0
- data/lib/still_active/version.rb +1 -1
- data/lib/still_active/workflow.rb +57 -31
- data/still_active.gemspec +16 -20
- metadata +5 -115
data/lib/helpers/ruby_helper.rb
CHANGED
|
@@ -38,8 +38,8 @@ module StillActive
|
|
|
38
38
|
latest_release_date: latest_release_date,
|
|
39
39
|
libyear: LibyearHelper.gem_libyear(
|
|
40
40
|
version_used_release_date: current_release_date,
|
|
41
|
-
latest_version_release_date: latest_release_date
|
|
42
|
-
)
|
|
41
|
+
latest_version_release_date: latest_release_date
|
|
42
|
+
)
|
|
43
43
|
}
|
|
44
44
|
end
|
|
45
45
|
|
|
@@ -54,7 +54,7 @@ module StillActive
|
|
|
54
54
|
private
|
|
55
55
|
|
|
56
56
|
def current_ruby_version
|
|
57
|
-
lockfile_ruby_version || (RUBY_ENGINE == "ruby" ? RUBY_VERSION : nil)
|
|
57
|
+
lockfile_ruby_version || ((RUBY_ENGINE == "ruby") ? RUBY_VERSION : nil)
|
|
58
58
|
end
|
|
59
59
|
|
|
60
60
|
def lockfile_ruby_version
|
|
@@ -82,7 +82,7 @@ module StillActive
|
|
|
82
82
|
ceiling_version: ceiling[:version].to_s,
|
|
83
83
|
ceiling_eol_date: ceiling[:eol_date],
|
|
84
84
|
oldest_supported: support_window[:oldest_supported].to_s,
|
|
85
|
-
latest_stable: support_window[:latest_stable].to_s
|
|
85
|
+
latest_stable: support_window[:latest_stable].to_s
|
|
86
86
|
}
|
|
87
87
|
finding.merge(severity: ConstraintHelper.constraint_severity(finding))
|
|
88
88
|
end
|
|
@@ -92,7 +92,7 @@ module StillActive
|
|
|
92
92
|
requirement: requirement,
|
|
93
93
|
eol_forced: false,
|
|
94
94
|
oldest_supported: support_window[:oldest_supported].to_s,
|
|
95
|
-
latest_stable: support_window[:latest_stable].to_s
|
|
95
|
+
latest_stable: support_window[:latest_stable].to_s
|
|
96
96
|
}
|
|
97
97
|
finding.merge(severity: ConstraintHelper.constraint_severity(finding))
|
|
98
98
|
end
|
data/lib/helpers/sarif_helper.rb
CHANGED
|
@@ -40,7 +40,7 @@ module StillActive
|
|
|
40
40
|
ruby_info: ruby_info,
|
|
41
41
|
line_index: line_index,
|
|
42
42
|
ruby_line: ruby_line,
|
|
43
|
-
lockfile_uri: lockfile_uri
|
|
43
|
+
lockfile_uri: lockfile_uri
|
|
44
44
|
)
|
|
45
45
|
|
|
46
46
|
JSON.pretty_generate(document(results: results, tool_version: tool_version, flavour: :ruby))
|
|
@@ -64,7 +64,7 @@ module StillActive
|
|
|
64
64
|
ruby_info: nil,
|
|
65
65
|
line_index: {},
|
|
66
66
|
ruby_line: nil,
|
|
67
|
-
lockfile_uri: source_uri
|
|
67
|
+
lockfile_uri: source_uri
|
|
68
68
|
)
|
|
69
69
|
|
|
70
70
|
JSON.pretty_generate(document(results: results, tool_version: tool_version, flavour: :neutral))
|
|
@@ -95,13 +95,13 @@ module StillActive
|
|
|
95
95
|
"version" => tool_version,
|
|
96
96
|
"semanticVersion" => VersionHelper.to_semver(tool_version),
|
|
97
97
|
"informationUri" => TOOL_URI,
|
|
98
|
-
"rules" => catalog.map { |r| sarif_rule(r) }
|
|
99
|
-
}
|
|
98
|
+
"rules" => catalog.map { |r| sarif_rule(r) }
|
|
99
|
+
}
|
|
100
100
|
},
|
|
101
|
-
"originalUriBaseIds" => {
|
|
101
|
+
"originalUriBaseIds" => {"%SRCROOT%" => {"uri" => "file:///"}},
|
|
102
102
|
"results" => results.map { |res| stamp_rule_index(res, index_by_id) },
|
|
103
|
-
"columnKind" => "utf16CodeUnits"
|
|
104
|
-
}]
|
|
103
|
+
"columnKind" => "utf16CodeUnits"
|
|
104
|
+
}]
|
|
105
105
|
}
|
|
106
106
|
end
|
|
107
107
|
|
|
@@ -113,17 +113,17 @@ module StillActive
|
|
|
113
113
|
end
|
|
114
114
|
|
|
115
115
|
def sarif_rule(r)
|
|
116
|
-
properties = {
|
|
116
|
+
properties = {"tags" => r[:tags], "precision" => "high"}
|
|
117
117
|
properties["security-severity"] = r[:security_severity] if r[:security_severity]
|
|
118
118
|
{
|
|
119
119
|
"id" => r[:id],
|
|
120
120
|
"name" => r[:name],
|
|
121
|
-
"shortDescription" => {
|
|
122
|
-
"fullDescription" => {
|
|
123
|
-
"help" => {
|
|
121
|
+
"shortDescription" => {"text" => r[:short]},
|
|
122
|
+
"fullDescription" => {"text" => r[:full]},
|
|
123
|
+
"help" => {"text" => r[:help_text], "markdown" => r[:help_markdown]},
|
|
124
124
|
"helpUri" => Sarif::Rules.help_uri(r[:id]),
|
|
125
|
-
"defaultConfiguration" => {
|
|
126
|
-
"properties" => properties
|
|
125
|
+
"defaultConfiguration" => {"level" => r[:level]},
|
|
126
|
+
"properties" => properties
|
|
127
127
|
}
|
|
128
128
|
end
|
|
129
129
|
|
|
@@ -157,16 +157,16 @@ module StillActive
|
|
|
157
157
|
if ActivityHelper.activity_level(data) == :critical
|
|
158
158
|
activity = ActivityHelper.last_activity(data)
|
|
159
159
|
years = ((Time.now - activity[:date]) / SECONDS_PER_YEAR).round(1)
|
|
160
|
-
noun = activity[:kind] == :release ? "no release" : "no commits"
|
|
160
|
+
noun = (activity[:kind] == :release) ? "no release" : "no commits"
|
|
161
161
|
out << mark_suppressed(
|
|
162
162
|
result(
|
|
163
163
|
"SA002",
|
|
164
164
|
name,
|
|
165
165
|
"#{display} #{version}: #{noun} in #{years} years (last #{activity[:date].utc.strftime("%Y-%m-%d")})#{alternatives_suffix(data)}#{transitive_suffix(data)}.",
|
|
166
|
-
location
|
|
166
|
+
location
|
|
167
167
|
),
|
|
168
168
|
display,
|
|
169
|
-
:activity
|
|
169
|
+
:activity
|
|
170
170
|
)
|
|
171
171
|
end
|
|
172
172
|
end
|
|
@@ -220,7 +220,7 @@ module StillActive
|
|
|
220
220
|
end
|
|
221
221
|
|
|
222
222
|
def language_ceiling_level(data)
|
|
223
|
-
{
|
|
223
|
+
{critical: "error", warning: "warning", note: "note"}.fetch(data[:language_ceiling][:severity], "note")
|
|
224
224
|
end
|
|
225
225
|
|
|
226
226
|
def language_ceiling_message(display, version, data)
|
|
@@ -234,7 +234,7 @@ module StillActive
|
|
|
234
234
|
else
|
|
235
235
|
"no #{runtime} #{ceiling[:latest_stable]} support yet"
|
|
236
236
|
end
|
|
237
|
-
fix = ceiling[:fixed_by_upgrade] && data[:latest_version] ? "; upgrade to #{data[:latest_version]} to lift it" : ""
|
|
237
|
+
fix = (ceiling[:fixed_by_upgrade] && data[:latest_version]) ? "; upgrade to #{data[:latest_version]} to lift it" : ""
|
|
238
238
|
"#{display} #{version}: requires #{runtime} #{ceiling[:requirement]}, #{body}#{fix}#{transitive_suffix(data)}."
|
|
239
239
|
end
|
|
240
240
|
|
|
@@ -250,7 +250,7 @@ module StillActive
|
|
|
250
250
|
# security finding, not the maintenance-tier signal poison usually is.
|
|
251
251
|
return "error" if data[:poison_security_relevant]
|
|
252
252
|
|
|
253
|
-
{
|
|
253
|
+
{critical: "error", warning: "warning", note: "note"}.fetch(data[:poison_severity], "warning")
|
|
254
254
|
end
|
|
255
255
|
|
|
256
256
|
def poison_message(display, version, data)
|
|
@@ -290,14 +290,14 @@ module StillActive
|
|
|
290
290
|
def mark_suppressed(result_hash, gem_name, signal, advisory: nil, aliases: [])
|
|
291
291
|
config = StillActive.config
|
|
292
292
|
if config.ignored_gems.include?(gem_name)
|
|
293
|
-
result_hash["suppressions"] = [{
|
|
293
|
+
result_hash["suppressions"] = [{"kind" => "external", "justification" => "ignored via --ignore"}]
|
|
294
294
|
return result_hash
|
|
295
295
|
end
|
|
296
296
|
|
|
297
297
|
entry = config.suppressions.match(gem: gem_name, signal: signal, advisory: advisory, aliases: aliases)
|
|
298
298
|
return result_hash unless entry
|
|
299
299
|
|
|
300
|
-
suppression = {
|
|
300
|
+
suppression = {"kind" => "external"}
|
|
301
301
|
suppression["justification"] = entry.reason if entry.reason
|
|
302
302
|
result_hash["suppressions"] = [suppression]
|
|
303
303
|
result_hash
|
|
@@ -331,9 +331,9 @@ module StillActive
|
|
|
331
331
|
"#{display} #{version}: #{advisory_id}#{title}#{alias_suffix}#{no_fix}#{transitive_suffix(data)}.",
|
|
332
332
|
location,
|
|
333
333
|
level: level,
|
|
334
|
-
fp_extra: advisory_id
|
|
334
|
+
fp_extra: advisory_id
|
|
335
335
|
)
|
|
336
|
-
base["properties"] = {
|
|
336
|
+
base["properties"] = {"security-severity" => severity} if severity
|
|
337
337
|
mark_suppressed(base, display, :vulnerability, advisory: vuln[:id], aliases: Array(vuln[:aliases]))
|
|
338
338
|
end
|
|
339
339
|
|
|
@@ -354,13 +354,13 @@ module StillActive
|
|
|
354
354
|
base = {
|
|
355
355
|
"ruleId" => "SA006",
|
|
356
356
|
"level" => "error",
|
|
357
|
-
"message" => {
|
|
357
|
+
"message" => {"text" => "Ruby #{version} has reached end-of-life#{eol_part}.#{latest_part}"},
|
|
358
358
|
"locations" => [{
|
|
359
359
|
"physicalLocation" => {
|
|
360
|
-
"artifactLocation" => {
|
|
361
|
-
"region" => {
|
|
362
|
-
}
|
|
363
|
-
}]
|
|
360
|
+
"artifactLocation" => {"uri" => lockfile_uri, "uriBaseId" => "%SRCROOT%"},
|
|
361
|
+
"region" => {"startLine" => ruby_line}
|
|
362
|
+
}
|
|
363
|
+
}]
|
|
364
364
|
}
|
|
365
365
|
apply_fingerprint(base, fingerprint("SA006", "ruby"))
|
|
366
366
|
end
|
|
@@ -370,8 +370,8 @@ module StillActive
|
|
|
370
370
|
base = {
|
|
371
371
|
"ruleId" => rule_id,
|
|
372
372
|
"level" => level,
|
|
373
|
-
"message" => {
|
|
374
|
-
"locations" => [location]
|
|
373
|
+
"message" => {"text" => message},
|
|
374
|
+
"locations" => [location]
|
|
375
375
|
}
|
|
376
376
|
apply_fingerprint(base, fingerprint(rule_id, gem_name, fp_extra))
|
|
377
377
|
end
|
|
@@ -379,7 +379,7 @@ module StillActive
|
|
|
379
379
|
def apply_fingerprint(result, fp)
|
|
380
380
|
result["partialFingerprints"] = {
|
|
381
381
|
"primaryLocationLineHash" => fp,
|
|
382
|
-
"stillActiveFinding/v1" => fp
|
|
382
|
+
"stillActiveFinding/v1" => fp
|
|
383
383
|
}
|
|
384
384
|
result
|
|
385
385
|
end
|
|
@@ -387,9 +387,9 @@ module StillActive
|
|
|
387
387
|
def location_for(gem_name, line_index, lockfile_uri)
|
|
388
388
|
{
|
|
389
389
|
"physicalLocation" => {
|
|
390
|
-
"artifactLocation" => {
|
|
391
|
-
"region" => {
|
|
392
|
-
}
|
|
390
|
+
"artifactLocation" => {"uri" => lockfile_uri, "uriBaseId" => "%SRCROOT%"},
|
|
391
|
+
"region" => {"startLine" => line_index[gem_name] || 1}
|
|
392
|
+
}
|
|
393
393
|
}
|
|
394
394
|
end
|
|
395
395
|
|
|
@@ -45,7 +45,7 @@ module StillActive
|
|
|
45
45
|
# archived != EOL: a repo archived while the gem still publishes recent
|
|
46
46
|
# releases (development moved to a monorepo) isn't dead -- let the
|
|
47
47
|
# releases speak, but keep :stale so the archived repo stays a yellow flag.
|
|
48
|
-
ActivityHelper.release_recency_level(gem_data) == :ok ? :stale : :archived
|
|
48
|
+
(ActivityHelper.release_recency_level(gem_data) == :ok) ? :stale : :archived
|
|
49
49
|
when :critical then :legacy # long-dormant but clean: feature-complete, not a fire
|
|
50
50
|
else level # :stale / :ok / :unknown
|
|
51
51
|
end
|
|
@@ -43,7 +43,7 @@ module StillActive
|
|
|
43
43
|
vulnerabilities: vulnerabilities,
|
|
44
44
|
# The single worst per-gem verdict (plus EOL Ruby), so a consumer reads
|
|
45
45
|
# one project-level posture without scanning every gem's status.
|
|
46
|
-
status: StatusHelper.project_status(result, ruby_info: ruby_info)
|
|
46
|
+
status: StatusHelper.project_status(result, ruby_info: ruby_info)
|
|
47
47
|
}
|
|
48
48
|
summary[:ruby_eol] = ruby_info[:eol] == true if ruby_info
|
|
49
49
|
summary
|
|
@@ -39,7 +39,7 @@ module StillActive
|
|
|
39
39
|
# "gems" for a native Ruby audit -- calling npm/cargo/go packages "gems" is a
|
|
40
40
|
# Ruby-ism that reads wrong cross-ecosystem.
|
|
41
41
|
def dependency_noun(result)
|
|
42
|
-
result.each_value.any? { |data| data[:ecosystem] } ? "dependencies" : "gems"
|
|
42
|
+
(result.each_value.any? { |data| data[:ecosystem] }) ? "dependencies" : "gems"
|
|
43
43
|
end
|
|
44
44
|
|
|
45
45
|
def build_row(name, data)
|
|
@@ -49,7 +49,7 @@ module StillActive
|
|
|
49
49
|
format_activity(data),
|
|
50
50
|
format_scorecard(data[:scorecard_score]),
|
|
51
51
|
format_vulns(data),
|
|
52
|
-
format_license(data[:license])
|
|
52
|
+
format_license(data[:license])
|
|
53
53
|
]
|
|
54
54
|
end
|
|
55
55
|
|
|
@@ -153,9 +153,9 @@ module StillActive
|
|
|
153
153
|
# gems point at the parent (#60), direct ones offer alternatives.
|
|
154
154
|
def sub_lines(data)
|
|
155
155
|
lines = if data[:poison]
|
|
156
|
-
[poison_line(data), (data[:direct] == false ? nil : alternatives_line(data))]
|
|
156
|
+
[poison_line(data), ((data[:direct] == false) ? nil : alternatives_line(data))]
|
|
157
157
|
else
|
|
158
|
-
[data[:direct] == false ? dependency_path_line(data) : alternatives_line(data)]
|
|
158
|
+
[(data[:direct] == false) ? dependency_path_line(data) : alternatives_line(data)]
|
|
159
159
|
end
|
|
160
160
|
# A language-runtime ceiling is orthogonal to poison/alternatives (a gem can
|
|
161
161
|
# be maintained yet still cap your Ruby), so it always gets its own line.
|
|
@@ -206,7 +206,7 @@ module StillActive
|
|
|
206
206
|
return if constraints.nil? || constraints.empty?
|
|
207
207
|
|
|
208
208
|
path = data[:dependency_path]
|
|
209
|
-
via = data[:direct] == false && path && path.length >= 2 ? " (via #{path.first})" : ""
|
|
209
|
+
via = (data[:direct] == false && path && path.length >= 2) ? " (via #{path.first})" : ""
|
|
210
210
|
# Colour carries the tier: red = act-now (3+ majors behind), yellow = plan,
|
|
211
211
|
# dim = a minor/FYI cap (1 behind). A security-relevant cap (it pins a
|
|
212
212
|
# vulnerable dependency below the fix) is always red -- that's the finding to
|
|
@@ -234,7 +234,7 @@ module StillActive
|
|
|
234
234
|
end
|
|
235
235
|
|
|
236
236
|
def poison_colour(severity)
|
|
237
|
-
{
|
|
237
|
+
{critical: :red, warning: :yellow, note: :dim}.fetch(severity, :yellow)
|
|
238
238
|
end
|
|
239
239
|
|
|
240
240
|
# A worst-first "N label (X critical, Y note)" summary fragment, coloured by
|
|
@@ -339,11 +339,11 @@ module StillActive
|
|
|
339
339
|
parts << activity
|
|
340
340
|
parts << "#{summary[:vulnerabilities]} vulnerabilities"
|
|
341
341
|
poison_tiers = result.each_value.select { |data| data[:poison] }.map { |data| data[:poison_severity] }
|
|
342
|
-
poison_part = tier_summary_part(poison_tiers, "#{poison_tiers.size} poison-#{poison_tiers.size == 1 ? "pill" : "pills"}")
|
|
342
|
+
poison_part = tier_summary_part(poison_tiers, "#{poison_tiers.size} poison-#{(poison_tiers.size == 1) ? "pill" : "pills"}")
|
|
343
343
|
parts << poison_part if poison_part
|
|
344
344
|
ceilings = result.each_value.filter_map { |data| data[:language_ceiling] }
|
|
345
345
|
runtimes = ceilings.map { |ceiling| ceiling[:runtime] }.uniq
|
|
346
|
-
runtime_label = runtimes.size == 1 ? runtimes.first : "language"
|
|
346
|
+
runtime_label = (runtimes.size == 1) ? runtimes.first : "language"
|
|
347
347
|
ceiling_part = tier_summary_part(ceilings.map { |ceiling| ceiling[:severity] }, "#{ceilings.size} #{runtime_label} ceiling#{"s" unless ceilings.size == 1}")
|
|
348
348
|
parts << ceiling_part if ceiling_part
|
|
349
349
|
total_libyear = LibyearHelper.total_libyear(result)
|
|
@@ -10,7 +10,7 @@ module StillActive
|
|
|
10
10
|
# a CVSS-4-only advisory returns cvss3Score 0 (unscored); OSV's
|
|
11
11
|
# `database_specific.severity` reads correctly for it and rescues a real HIGH from
|
|
12
12
|
# fail-closed noise. GitHub uses MODERATE where we use medium.
|
|
13
|
-
OSV_LABELS = {
|
|
13
|
+
OSV_LABELS = {"critical" => "critical", "high" => "high", "moderate" => "medium", "medium" => "medium", "low" => "low"}.freeze
|
|
14
14
|
|
|
15
15
|
def highest_severity(vulnerabilities)
|
|
16
16
|
return if vulnerabilities.nil? || vulnerabilities.empty?
|
|
@@ -5,6 +5,8 @@ require "cgi"
|
|
|
5
5
|
require "json"
|
|
6
6
|
require "uri"
|
|
7
7
|
require_relative "../helpers/http_helper"
|
|
8
|
+
require_relative "compact_index_client"
|
|
9
|
+
require_relative "source_credentials"
|
|
8
10
|
|
|
9
11
|
module StillActive
|
|
10
12
|
module ArtifactoryClient
|
|
@@ -18,8 +20,18 @@ module StillActive
|
|
|
18
20
|
false
|
|
19
21
|
end
|
|
20
22
|
|
|
23
|
+
# Version sources in precedence order:
|
|
24
|
+
# 1. the compact index, the only endpoint that lists what the repo can
|
|
25
|
+
# actually resolve (#142)
|
|
26
|
+
# 2. the versions API, for hosts that serve no compact index
|
|
27
|
+
# 3. AQL, a cache inventory, when neither answers
|
|
28
|
+
# The compact index carries no timestamps, so a compact-index list is dated
|
|
29
|
+
# from the versions API.
|
|
21
30
|
def versions(gem_name:, source_uri:)
|
|
22
31
|
headers = auth_headers(gem_name: gem_name, source_uri: source_uri)
|
|
32
|
+
compact = CompactIndexClient.versions(gem_name: gem_name, source_uri: source_uri, headers: headers)
|
|
33
|
+
return dated(compact, gem_name: gem_name, source_uri: source_uri, headers: headers) unless compact.empty?
|
|
34
|
+
|
|
23
35
|
vs = RubygemsClient.versions(gem_name: gem_name, source_uri: source_uri, headers: headers)
|
|
24
36
|
return vs unless vs.empty?
|
|
25
37
|
|
|
@@ -30,14 +42,36 @@ module StillActive
|
|
|
30
42
|
|
|
31
43
|
private
|
|
32
44
|
|
|
45
|
+
# Matched on the exact version number, so the versions API can only annotate a
|
|
46
|
+
# version the compact index already listed. That keeps a name collision from
|
|
47
|
+
# contributing anything: the placeholder gem's versions simply don't match, and
|
|
48
|
+
# the real ones stay undated rather than borrowing a stranger's release date.
|
|
49
|
+
def dated(versions, gem_name:, source_uri:, headers:)
|
|
50
|
+
return versions if versions.all? { |version| version["created_at"] }
|
|
51
|
+
|
|
52
|
+
published = RubygemsClient.versions(gem_name: gem_name, source_uri: source_uri, headers: headers)
|
|
53
|
+
return versions unless published.is_a?(Array)
|
|
54
|
+
|
|
55
|
+
by_number = published.grep(Hash).to_h { |version| [version["number"], version] }
|
|
56
|
+
# compact first: an absent compact-index field must not blank out the value
|
|
57
|
+
# the versions API supplied for the same version.
|
|
58
|
+
versions.map { |version| (by_number[version["number"]] || {}).merge(version.compact) }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Bundler's own per-host credential (bundle config / BUNDLE_<HOST>) wins, exactly
|
|
62
|
+
# as `bundle install` would use it. Only when the host has none do we consider
|
|
63
|
+
# still_active's ambient --artifactory-token, and only for the one host the user
|
|
64
|
+
# explicitly allowlisted: the token is not host-keyed, so sending it to a
|
|
65
|
+
# lockfile-derived host would leak it. See SourceCredentials for the host-keyed
|
|
66
|
+
# store this shares with the generic private-source path.
|
|
33
67
|
def credentials(gem_name:, source_uri:)
|
|
34
|
-
|
|
35
|
-
bundler = Bundler.settings[source_uri] || Bundler.settings[host]
|
|
68
|
+
bundler = Bundler.settings.credentials_for(URI(source_uri))
|
|
36
69
|
return bundler if bundler && !bundler.empty?
|
|
37
70
|
|
|
38
71
|
global = StillActive.config.artifactory_token
|
|
39
72
|
return unless global
|
|
40
73
|
|
|
74
|
+
host = URI(source_uri).host
|
|
41
75
|
configured_host = StillActive.config.artifactory_host
|
|
42
76
|
unless configured_host && host&.casecmp?(configured_host)
|
|
43
77
|
warn_unauthorized_host(gem_name: gem_name, host: host)
|
|
@@ -48,23 +82,15 @@ module StillActive
|
|
|
48
82
|
end
|
|
49
83
|
|
|
50
84
|
def warn_unauthorized_host(gem_name:, host:)
|
|
51
|
-
|
|
85
|
+
warn(
|
|
52
86
|
"warning: an Artifactory token is set but #{host} (source for #{gem_name}) is not an authorized host, " \
|
|
53
87
|
"so the token will not be sent. " \
|
|
54
|
-
"To allow it, set --artifactory-host=#{host} or STILL_ACTIVE_ARTIFACTORY_HOST=#{host}"
|
|
88
|
+
"To allow it, set --artifactory-host=#{host} or STILL_ACTIVE_ARTIFACTORY_HOST=#{host}"
|
|
55
89
|
)
|
|
56
90
|
end
|
|
57
91
|
|
|
58
92
|
def auth_headers(gem_name:, source_uri:)
|
|
59
|
-
|
|
60
|
-
return {} unless creds
|
|
61
|
-
|
|
62
|
-
if creds.include?(":")
|
|
63
|
-
user, pass = creds.split(":", 2).map { |part| CGI.unescape(part) }
|
|
64
|
-
{ "Authorization" => "Basic #{["#{user}:#{pass}"].pack("m0")}" }
|
|
65
|
-
else
|
|
66
|
-
{ "Authorization" => "Bearer #{creds}" }
|
|
67
|
-
end
|
|
93
|
+
SourceCredentials.auth_header(credentials(gem_name: gem_name, source_uri: source_uri))
|
|
68
94
|
end
|
|
69
95
|
|
|
70
96
|
# Artifactory's Rubygems-compatible API
|
|
@@ -99,8 +125,8 @@ module StillActive
|
|
|
99
125
|
base = URI(artifactory_base)
|
|
100
126
|
path = "#{base.path}#{AQL_PATH}"
|
|
101
127
|
query = {
|
|
102
|
-
"name" => {
|
|
103
|
-
"repo" => repo_key
|
|
128
|
+
"name" => {"$match" => "#{gem_name}-*.gem"},
|
|
129
|
+
"repo" => repo_key
|
|
104
130
|
}
|
|
105
131
|
body = %(items.find(#{JSON.generate(query)}).include("repo", "path", "name", "created"))
|
|
106
132
|
response = HttpHelper.post_json(base, path, body: body, headers: headers.merge("Content-Type" => "text/plain"))
|
|
@@ -115,7 +141,7 @@ module StillActive
|
|
|
115
141
|
def parse_source_url(source_uri)
|
|
116
142
|
match = source_uri.match(SOURCE_URL_PATTERN)
|
|
117
143
|
unless match
|
|
118
|
-
|
|
144
|
+
warn("warning: unrecognized Artifactory source URL for AQL fallback: #{source_uri}")
|
|
119
145
|
return [nil, nil]
|
|
120
146
|
end
|
|
121
147
|
|
|
@@ -128,10 +154,13 @@ module StillActive
|
|
|
128
154
|
version = extract_version(item["name"], gem_name)
|
|
129
155
|
next if version.nil?
|
|
130
156
|
|
|
157
|
+
# No "created_at": the AQL `created` field is when this Artifactory
|
|
158
|
+
# cached the artifact, not when the version was published. The two
|
|
159
|
+
# agree to within a day for a gem someone pulled promptly and diverge
|
|
160
|
+
# by years for one pulled late, so it cannot back a staleness signal.
|
|
131
161
|
{
|
|
132
162
|
"number" => version,
|
|
133
|
-
"
|
|
134
|
-
"prerelease" => Gem::Version.new(version).prerelease?,
|
|
163
|
+
"prerelease" => Gem::Version.new(version).prerelease?
|
|
135
164
|
}
|
|
136
165
|
end
|
|
137
166
|
.uniq { |h| h["number"] }
|