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.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +3 -0
  3. data/lib/helpers/activity_helper.rb +3 -3
  4. data/lib/helpers/alternatives_helper.rb +1 -1
  5. data/lib/helpers/ansi_helper.rb +1 -1
  6. data/lib/helpers/bot_context.rb +7 -7
  7. data/lib/helpers/bundler_helper.rb +1 -2
  8. data/lib/helpers/catalog_index.rb +1 -1
  9. data/lib/helpers/constraint_helper.rb +6 -6
  10. data/lib/helpers/cvss_helper.rb +2 -2
  11. data/lib/helpers/cyclonedx_helper.rb +14 -14
  12. data/lib/helpers/diff_markdown_helper.rb +4 -4
  13. data/lib/helpers/dotnet_helper.rb +4 -4
  14. data/lib/helpers/endoflife_helper.rb +3 -3
  15. data/lib/helpers/http_helper.rb +31 -19
  16. data/lib/helpers/lockfile_dependency_parser.rb +2 -2
  17. data/lib/helpers/markdown_escape.rb +1 -1
  18. data/lib/helpers/markdown_helper.rb +7 -7
  19. data/lib/helpers/ruby_advisory_db.rb +1 -1
  20. data/lib/helpers/ruby_helper.rb +3 -3
  21. data/lib/helpers/runtime_ceiling_helper.rb +2 -2
  22. data/lib/helpers/sarif_helper.rb +34 -34
  23. data/lib/helpers/status_helper.rb +1 -1
  24. data/lib/helpers/summary_helper.rb +1 -1
  25. data/lib/helpers/terminal_helper.rb +8 -8
  26. data/lib/helpers/vulnerability_helper.rb +1 -1
  27. data/lib/still_active/artifactory_client.rb +47 -18
  28. data/lib/still_active/cli.rb +38 -38
  29. data/lib/still_active/compact_index_client.rb +117 -0
  30. data/lib/still_active/config.rb +1 -1
  31. data/lib/still_active/config_file.rb +4 -4
  32. data/lib/still_active/deps_dev_client.rb +7 -7
  33. data/lib/still_active/diff.rb +12 -12
  34. data/lib/still_active/ecosystem_lens.rb +5 -5
  35. data/lib/still_active/ecosystems_client.rb +7 -7
  36. data/lib/still_active/forgejo_client.rb +3 -3
  37. data/lib/still_active/github_client.rb +6 -6
  38. data/lib/still_active/gitlab_client.rb +3 -3
  39. data/lib/still_active/options.rb +4 -4
  40. data/lib/still_active/osv_client.rb +8 -8
  41. data/lib/still_active/poison_security_correlator.rb +3 -3
  42. data/lib/still_active/pypi_client.rb +2 -2
  43. data/lib/still_active/repository.rb +4 -4
  44. data/lib/still_active/sarif/rules.rb +20 -20
  45. data/lib/still_active/sbom_reader.rb +8 -8
  46. data/lib/still_active/sbom_workflow.rb +7 -7
  47. data/lib/still_active/source_credentials.rb +39 -0
  48. data/lib/still_active/version.rb +1 -1
  49. data/lib/still_active/workflow.rb +57 -31
  50. data/still_active.gemspec +16 -20
  51. metadata +5 -115
@@ -26,7 +26,7 @@ module StillActive
26
26
  # carries per-version declared dependency constraints (the poison-pill input).
27
27
  PACKAGES_BASE_URI = URI("https://packages.ecosyste.ms/")
28
28
  # ecosyste.ms asks consumers to identify themselves for its "polite pool".
29
- USER_AGENT = "still_active/#{StillActive::VERSION} (+https://github.com/SeanLF/still_active)"
29
+ USER_AGENT = "still_active/#{StillActive::VERSION} (+https://github.com/SeanLF/still_active)".freeze
30
30
 
31
31
  # archived + last-commit date from a single repository call. ecosyste.ms's
32
32
  # pushed_at mirrors GitHub's, so this returns the same shape as GithubClient.
@@ -35,13 +35,13 @@ module StillActive
35
35
  return {} if owner.nil? || name.nil?
36
36
 
37
37
  path = "/api/v1/hosts/GitHub/repositories/#{encode_repo(owner, name)}"
38
- body = HttpHelper.get_json(BASE_URI, path, headers: { "User-Agent" => USER_AGENT }, params: politeness_params)
38
+ body = HttpHelper.get_json(BASE_URI, path, headers: {"User-Agent" => USER_AGENT}, params: politeness_params)
39
39
  # A non-Hash 200 body (error envelope rendered as an array, schema drift)
40
40
  # would otherwise raise on indexing and vanish the gem from the audit via
41
41
  # the workflow's rescue; degrade to "no signal" like any other read failure.
42
42
  return {} unless body.is_a?(Hash)
43
43
 
44
- signals = { last_commit_date: parse_time(body["pushed_at"], owner, name) }
44
+ signals = {last_commit_date: parse_time(body["pushed_at"], owner, name)}
45
45
  # Only assert archived when the field is actually present. A missing field
46
46
  # must read as unknown, not be invented as false -- otherwise a partial
47
47
  # crawl could silently mask the most actionable verdict (gem is archived).
@@ -73,7 +73,7 @@ module StillActive
73
73
  return [] if name.nil? || version.nil?
74
74
 
75
75
  path = "/api/v1/registries/#{encode(registry)}/packages/#{encode(name)}/versions/#{encode(version)}"
76
- body = HttpHelper.get_json(PACKAGES_BASE_URI, path, headers: { "User-Agent" => USER_AGENT }, params: politeness_params)
76
+ body = HttpHelper.get_json(PACKAGES_BASE_URI, path, headers: {"User-Agent" => USER_AGENT}, params: politeness_params)
77
77
  return [] unless body.is_a?(Hash)
78
78
 
79
79
  dependencies = body["dependencies"]
@@ -86,7 +86,7 @@ module StillActive
86
86
  requirements = dep["requirements"]
87
87
  next if package_name.nil? || requirements.nil?
88
88
 
89
- { package_name: package_name, requirements: requirements }
89
+ {package_name: package_name, requirements: requirements}
90
90
  end
91
91
  end
92
92
 
@@ -98,7 +98,7 @@ module StillActive
98
98
  # lockfile and we don't attribute every user's traffic to one address.
99
99
  def politeness_params
100
100
  email = StillActive.config.ecosystems_email
101
- email ? { mailto: email } : {}
101
+ email ? {mailto: email} : {}
102
102
  end
103
103
 
104
104
  # nil or a non-string (numeric/array from an off-spec payload) -> no date,
@@ -108,7 +108,7 @@ module StillActive
108
108
 
109
109
  Time.parse(value)
110
110
  rescue ArgumentError
111
- $stderr.puts("warning: could not parse repo date for #{owner}/#{name}: #{value.inspect}")
111
+ warn("warning: could not parse repo date for #{owner}/#{name}: #{value.inspect}")
112
112
  nil
113
113
  end
114
114
 
@@ -24,7 +24,7 @@ module StillActive
24
24
  body = HttpHelper.get_json(base_uri(host), "/api/v1/repos/#{owner}/#{name}", headers: auth_headers)
25
25
  return {} if body.nil?
26
26
 
27
- { archived: body["archived"] == true, last_commit_date: parse_time(body["updated_at"], owner, name) }
27
+ {archived: body["archived"] == true, last_commit_date: parse_time(body["updated_at"], owner, name)}
28
28
  end
29
29
 
30
30
  private
@@ -34,7 +34,7 @@ module StillActive
34
34
 
35
35
  Time.parse(value)
36
36
  rescue ArgumentError
37
- $stderr.puts("warning: could not parse repo date for #{owner}/#{name}: #{value.inspect}")
37
+ warn("warning: could not parse repo date for #{owner}/#{name}: #{value.inspect}")
38
38
  nil
39
39
  end
40
40
 
@@ -44,7 +44,7 @@ module StillActive
44
44
 
45
45
  def auth_headers
46
46
  token = StillActive.config.forgejo_token
47
- token ? { "Authorization" => "token #{token}" } : {}
47
+ token ? {"Authorization" => "token #{token}"} : {}
48
48
  end
49
49
  end
50
50
  end
@@ -29,9 +29,9 @@ module StillActive
29
29
  end
30
30
  return {} unless repo
31
31
 
32
- { archived: repo.archived, last_commit_date: as_time(repo.pushed_at, owner, name) }
32
+ {archived: repo.archived, last_commit_date: as_time(repo.pushed_at, owner, name)}
33
33
  rescue Octokit::Error, Faraday::Error => e
34
- $stderr.puts("warning: repo signals failed for #{owner}/#{name}: #{e.class}")
34
+ warn("warning: repo signals failed for #{owner}/#{name}: #{e.class}")
35
35
  {}
36
36
  end
37
37
 
@@ -54,7 +54,7 @@ module StillActive
54
54
  end
55
55
  nil
56
56
  rescue Octokit::Error, Faraday::Error => e
57
- $stderr.puts("warning: unreleased-commits check failed for #{owner}/#{name}: #{e.class}")
57
+ warn("warning: unreleased-commits check failed for #{owner}/#{name}: #{e.class}")
58
58
  nil
59
59
  end
60
60
 
@@ -75,12 +75,12 @@ module StillActive
75
75
  # Hourly-limit exhaustion (or a far reset): not worth auto-waiting.
76
76
  # Surface the one actionable hint rather than a generic class name,
77
77
  # then return nil so this signal is simply absent for the gem.
78
- $stderr.puts("rate limited on #{label}; set GITHUB_TOKEN to raise your limit, or run less often")
78
+ warn("rate limited on #{label}; set GITHUB_TOKEN to raise your limit, or run less often")
79
79
  return
80
80
  end
81
81
 
82
82
  retried = true
83
- $stderr.puts("rate limited on #{label}; waiting #{wait}s for reset")
83
+ warn("rate limited on #{label}; waiting #{wait}s for reset")
84
84
  sleep(wait)
85
85
  retry
86
86
  end
@@ -119,7 +119,7 @@ module StillActive
119
119
 
120
120
  Time.parse(value)
121
121
  rescue ArgumentError
122
- $stderr.puts("warning: could not parse repo date for #{owner}/#{name}: #{value.inspect}")
122
+ warn("warning: could not parse repo date for #{owner}/#{name}: #{value.inspect}")
123
123
  nil
124
124
  end
125
125
  end
@@ -20,7 +20,7 @@ module StillActive
20
20
  body = HttpHelper.get_json(BASE_URI, path, headers: auth_headers)
21
21
  return {} if body.nil?
22
22
 
23
- { archived: body["archived"] == true, last_commit_date: parse_time(body["last_activity_at"], owner, name) }
23
+ {archived: body["archived"] == true, last_commit_date: parse_time(body["last_activity_at"], owner, name)}
24
24
  end
25
25
 
26
26
  private
@@ -30,13 +30,13 @@ module StillActive
30
30
 
31
31
  Time.parse(value)
32
32
  rescue ArgumentError
33
- $stderr.puts("warning: could not parse repo date for #{owner}/#{name}: #{value.inspect}")
33
+ warn("warning: could not parse repo date for #{owner}/#{name}: #{value.inspect}")
34
34
  nil
35
35
  end
36
36
 
37
37
  def auth_headers
38
38
  token = StillActive.config.gitlab_token
39
- token ? { "PRIVATE-TOKEN" => token } : {}
39
+ token ? {"PRIVATE-TOKEN" => token} : {}
40
40
  end
41
41
 
42
42
  def encode_project(owner, name)
@@ -57,7 +57,7 @@ module StillActive
57
57
  opts.on("--gems=GEM,GEM2,...", Array, "Gem(s)") do |value|
58
58
  options[:provided_gems] = true
59
59
  # Explicitly named gems are direct by definition (the user chose them).
60
- StillActive.config { |config| config.gems = value.map { |g| { name: g, direct: true } } }
60
+ StillActive.config { |config| config.gems = value.map { |g| {name: g, direct: true} } }
61
61
  end
62
62
  end
63
63
 
@@ -130,14 +130,14 @@ module StillActive
130
130
  opts.on(
131
131
  "--safe-range-end=YEARS",
132
132
  Float,
133
- "maximum years since last release considered safe, no warning (default 1.5; fractional allowed)",
133
+ "maximum years since last release considered safe, no warning (default 1.5; fractional allowed)"
134
134
  ) do |value|
135
135
  StillActive.config { |config| config.no_warning_range_end = value }
136
136
  end
137
137
  opts.on(
138
138
  "--warning-range-end=YEARS",
139
139
  Float,
140
- "maximum years since last release that triggers a warning, beyond this is critical (default 3)",
140
+ "maximum years since last release that triggers a warning, beyond this is critical (default 3)"
141
141
  ) do |value|
142
142
  StillActive.config { |config| config.warning_range_end = value }
143
143
  end
@@ -181,7 +181,7 @@ module StillActive
181
181
  # Ceiling findings are only ever :critical or :note, so =warning can't
182
182
  # match anything the bare (=critical) default doesn't already catch.
183
183
  if value == "warning"
184
- $stderr.puts("warning: --fail-if-language-ceiling=warning has no effect (runtime ceilings are only critical or note); behaves as =critical")
184
+ warn("warning: --fail-if-language-ceiling=warning has no effect (runtime ceilings are only critical or note); behaves as =critical")
185
185
  end
186
186
  StillActive.config { |config| config.fail_if_language_ceiling = value.to_sym }
187
187
  else
@@ -34,15 +34,15 @@ module StillActive
34
34
  cargo: "crates.io",
35
35
  maven: "Maven",
36
36
  go: "Go",
37
- nuget: "NuGet",
37
+ nuget: "NuGet"
38
38
  }.freeze
39
39
 
40
40
  # Prefer the newest CVSS version a record carries (v4 is the whole point: it's the
41
41
  # one deps.dev can't score). `severity[].score` is the vector STRING, oddly named.
42
- CVSS_PRIORITY = { "CVSS_V4" => 3, "CVSS_V3" => 2, "CVSS_V2" => 1 }.freeze
42
+ CVSS_PRIORITY = {"CVSS_V4" => 3, "CVSS_V3" => 2, "CVSS_V2" => 1}.freeze
43
43
  # A v2 vector has no `CVSS:X.Y` prefix, so the version can't be read from the
44
44
  # string; fall back to the entry type so the CycloneDX rating method labels it v2.
45
- TYPE_VERSIONS = { "CVSS_V4" => "4.0", "CVSS_V3" => "3.1", "CVSS_V2" => "2.0" }.freeze
45
+ TYPE_VERSIONS = {"CVSS_V4" => "4.0", "CVSS_V3" => "3.1", "CVSS_V2" => "2.0"}.freeze
46
46
 
47
47
  # Enrich each advisory in place with the OSV severity label and the fixed
48
48
  # versions for the audited package. A missing/failed lookup is a no-op on that
@@ -57,12 +57,12 @@ module StillActive
57
57
  advisory[:cvss_version] = record[:cvss_version]
58
58
  advisory[:cvss_vector] = record[:cvss_vector]
59
59
  advisory[:fixed_versions] = fixed_versions(record, ecosystem: ecosystem, name: name)
60
- rescue StandardError => e
60
+ rescue => e
61
61
  # Enrichment is additive and best-effort. An unexpected OSV shape must never
62
62
  # raise out through the workflow's per-gem rescue, which would DROP the whole
63
63
  # gem and read a known-vulnerable dependency as clean. Leave the advisory
64
64
  # exactly as deps.dev produced it.
65
- $stderr.puts("warning: OSV enrichment for #{advisory[:id]} failed: #{e.class} (#{e.message}); leaving advisory unchanged")
65
+ warn("warning: OSV enrichment for #{advisory[:id]} failed: #{e.class} (#{e.message}); leaving advisory unchanged")
66
66
  end
67
67
  end
68
68
 
@@ -83,7 +83,7 @@ module StillActive
83
83
  cvss_score: cvss[:score],
84
84
  cvss_version: cvss[:version],
85
85
  cvss_vector: cvss[:vector],
86
- affected: Array(body["affected"]).filter_map { |entry| parse_affected(entry) },
86
+ affected: Array(body["affected"]).filter_map { |entry| parse_affected(entry) }
87
87
  }
88
88
  end
89
89
 
@@ -100,7 +100,7 @@ module StillActive
100
100
  return {} if entry.nil?
101
101
 
102
102
  vector = entry["score"]
103
- { score: CvssHelper.score(vector), version: cvss_version(vector) || TYPE_VERSIONS[entry["type"]], vector: vector }
103
+ {score: CvssHelper.score(vector), version: cvss_version(vector) || TYPE_VERSIONS[entry["type"]], vector: vector}
104
104
  end
105
105
 
106
106
  # The X.Y version from a "CVSS:X.Y/..." vector prefix, or nil.
@@ -137,7 +137,7 @@ module StillActive
137
137
 
138
138
  Array(range["events"]).filter_map { |event| event["fixed"] if event.is_a?(Hash) }
139
139
  end
140
- { ecosystem: package["ecosystem"], name: package["name"], fixed: fixed }
140
+ {ecosystem: package["ecosystem"], name: package["name"], fixed: fixed}
141
141
  end
142
142
 
143
143
  def encode(value)
@@ -73,7 +73,7 @@ module StillActive
73
73
  next unless data[:vulnerability_count].to_i.positive?
74
74
 
75
75
  name = data[:name] || key
76
- map["#{data[:ecosystem]}/#{name}"] = { vulns: hashes(data[:vulnerabilities]), used_version: data[:version_used] }
76
+ map["#{data[:ecosystem]}/#{name}"] = {vulns: hashes(data[:vulnerabilities]), used_version: data[:version_used]}
77
77
  end
78
78
  end
79
79
 
@@ -85,7 +85,7 @@ module StillActive
85
85
  def copy_index(result_object)
86
86
  result_object.each_with_object({}) do |(key, data), map|
87
87
  name = data[:name] || key
88
- (map["#{data[:ecosystem]}/#{name}"] ||= []) << { version: data[:version_used], vulns: hashes(data[:vulnerabilities]) }
88
+ (map["#{data[:ecosystem]}/#{name}"] ||= []) << {version: data[:version_used], vulns: hashes(data[:vulnerabilities])}
89
89
  end
90
90
  end
91
91
 
@@ -148,7 +148,7 @@ module StillActive
148
148
  capped_dep_vulnerable: true,
149
149
  capped_below_fix: true,
150
150
  below_fix_advisory: receipt[:id],
151
- below_fix_fixed_in: nearest_fix(receipt, oldest),
151
+ below_fix_fixed_in: nearest_fix(receipt, oldest)
152
152
  }
153
153
  end
154
154
 
@@ -20,7 +20,7 @@ module StillActive
20
20
 
21
21
  BASE_URI = URI("https://pypi.org/")
22
22
  # Polite identification, matching EcosystemsClient's convention.
23
- USER_AGENT = "still_active/#{StillActive::VERSION} (+https://github.com/SeanLF/still_active)"
23
+ USER_AGENT = "still_active/#{StillActive::VERSION} (+https://github.com/SeanLF/still_active)".freeze
24
24
 
25
25
  # The PEP 440 `requires_python` specifier a release declares (e.g.
26
26
  # ">=3.8,<3.13"), or nil when the version is unreadable or declares none.
@@ -30,7 +30,7 @@ module StillActive
30
30
  return if name.nil? || version.nil?
31
31
 
32
32
  path = "/pypi/#{encode(name)}/#{encode(version)}/json"
33
- body = HttpHelper.get_json(BASE_URI, path, headers: { "User-Agent" => USER_AGENT })
33
+ body = HttpHelper.get_json(BASE_URI, path, headers: {"User-Agent" => USER_AGENT})
34
34
  return unless body.is_a?(Hash)
35
35
 
36
36
  # Guard the nested read: a non-Hash `info` (schema drift) would make
@@ -8,9 +8,9 @@ module StillActive
8
8
  SOURCE_BY_HOST = {
9
9
  "github.com" => :github,
10
10
  "gitlab.com" => :gitlab,
11
- "codeberg.org" => :forgejo,
11
+ "codeberg.org" => :forgejo
12
12
  }.freeze
13
- REPO_REGEX = %r{(?<url>https?://(?:www\.)?(?<host>github\.com|gitlab\.com|codeberg\.org)/(?<owner>[\w.\-]+)/(?<name>[\w.\-]+))}i
13
+ REPO_REGEX = %r{(?<url>https?://(?:www\.)?(?<host>github\.com|gitlab\.com|codeberg\.org)/(?<owner>[\w.-]+)/(?<name>[\w.-]+))}i
14
14
 
15
15
  extend self
16
16
 
@@ -22,12 +22,12 @@ module StillActive
22
22
 
23
23
  def url_with_owner_and_name(url:)
24
24
  match = url&.match(REPO_REGEX)
25
- return { source: :unhandled, owner: nil, name: nil } unless match
25
+ return {source: :unhandled, owner: nil, name: nil} unless match
26
26
 
27
27
  clean_url = match[:url].delete_suffix(".git")
28
28
  name = match[:name].delete_suffix(".git")
29
29
 
30
- { url: clean_url, source: SOURCE_BY_HOST.fetch(match[:host].downcase), owner: match[:owner], name: name }
30
+ {url: clean_url, source: SOURCE_BY_HOST.fetch(match[:host].downcase), owner: match[:owner], name: name}
31
31
  end
32
32
  end
33
33
  end
@@ -60,8 +60,8 @@ module StillActive
60
60
  neutral: {
61
61
  short: "Package's source repository is archived",
62
62
  full: "The package's upstream repository has been marked archived. No further fixes, including security patches, should be expected.",
63
- help_text: "Look for a maintained fork or alternative. If you must keep the package, vendor or fork it so you can apply patches yourself.",
64
- },
63
+ help_text: "Look for a maintained fork or alternative. If you must keep the package, vendor or fork it so you can apply patches yourself."
64
+ }
65
65
  },
66
66
  {
67
67
  id: "SA002",
@@ -75,8 +75,8 @@ module StillActive
75
75
  neutral: {
76
76
  short: "Package has had no release for over 3 years",
77
77
  full: "The package's latest release is over 3 years old. Not formally archived, but a strong abandonment signal: a consumer cannot pull fixes that were never released. For a package with no releases at all (e.g. git-sourced), the last commit date is used instead.",
78
- help_text: "Verify the package still works on your supported runtime and consider a maintained alternative.",
79
- },
78
+ help_text: "Verify the package still works on your supported runtime and consider a maintained alternative."
79
+ }
80
80
  },
81
81
  {
82
82
  id: "SA003",
@@ -88,8 +88,8 @@ module StillActive
88
88
  security_severity: "7.0", # default; per-result override from CVSS
89
89
  tags: ["security", "vulnerability", "external/cwe/cwe-1104"],
90
90
  neutral: {
91
- short: "Package has known vulnerabilities (via deps.dev / OSV)",
92
- },
91
+ short: "Package has known vulnerabilities (via deps.dev / OSV)"
92
+ }
93
93
  },
94
94
  {
95
95
  id: "SA004",
@@ -102,8 +102,8 @@ module StillActive
102
102
  tags: ["maintenance", "libyear"],
103
103
  neutral: {
104
104
  short: "Package is significantly behind the latest release",
105
- help_text: "Schedule an upgrade to the latest release.",
106
- },
105
+ help_text: "Schedule an upgrade to the latest release."
106
+ }
107
107
  },
108
108
  {
109
109
  id: "SA005",
@@ -115,8 +115,8 @@ module StillActive
115
115
  security_severity: nil,
116
116
  tags: ["supply-chain", "openssf"],
117
117
  neutral: {
118
- short: "Package's OpenSSF Scorecard is low",
119
- },
118
+ short: "Package's OpenSSF Scorecard is low"
119
+ }
120
120
  },
121
121
  {
122
122
  id: "SA006",
@@ -127,7 +127,7 @@ module StillActive
127
127
  level: "error",
128
128
  security_severity: "8.5",
129
129
  tags: ["security", "runtime", "external/cwe/cwe-1104"],
130
- native_only: true,
130
+ native_only: true
131
131
  },
132
132
  {
133
133
  id: "SA007",
@@ -141,8 +141,8 @@ module StillActive
141
141
  neutral: {
142
142
  short: "Resolved package version has been yanked from its registry",
143
143
  full: "The resolved version has been yanked by the package owner, typically for a serious bug or vulnerability.",
144
- help_text: "Update to a non-yanked version immediately.",
145
- },
144
+ help_text: "Update to a non-yanked version immediately."
145
+ }
146
146
  },
147
147
  {
148
148
  id: "SA008",
@@ -156,8 +156,8 @@ module StillActive
156
156
  neutral: {
157
157
  short: "Dormant package caps a dependency below its latest major",
158
158
  full: "A dormant (abandoned or archived) package declares a runtime constraint that caps one of its dependencies below that dependency's current latest major. Because nobody is shipping the package, the cap will never lift, and it grows more constraining as the capped dependency releases new majors: the tree is held below a ceiling no upstream release will raise.",
159
- help_text: "Replace or fork the dormant package, or vendor a version that relaxes the constraint. For a transitive pill, target the direct dependency that pulls it in.",
160
- },
159
+ help_text: "Replace or fork the dormant package, or vendor a version that relaxes the constraint. For a transitive pill, target the direct dependency that pulls it in."
160
+ }
161
161
  },
162
162
  {
163
163
  id: "SA009",
@@ -167,8 +167,8 @@ module StillActive
167
167
  help_text: "If a newer release of the package lifts the cap, upgrade it. Otherwise replace or fork the package, or contribute support for the newer runtime upstream.",
168
168
  level: "note",
169
169
  security_severity: nil, # maintenance/compatibility, not a CVE (as SA002/SA004/SA005/SA008)
170
- tags: ["maintenance", "runtime", "external/cwe/cwe-1104"],
171
- },
170
+ tags: ["maintenance", "runtime", "external/cwe/cwe-1104"]
171
+ }
172
172
  ].freeze
173
173
 
174
174
  # Builds a frozen catalog for one wording flavour. `:neutral` applies each
@@ -181,10 +181,10 @@ module StillActive
181
181
  neutral = flavour == :neutral
182
182
  rules = neutral ? RAW_RULES.reject { |r| r[:native_only] } : RAW_RULES
183
183
  rules.map do |r|
184
- resolved = neutral && r[:neutral] ? r.merge(r[:neutral]) : r
184
+ resolved = (neutral && r[:neutral]) ? r.merge(r[:neutral]) : r
185
185
  resolved.except(:neutral, :native_only).merge(
186
186
  tags: r[:tags].dup.freeze,
187
- help_markdown: "#{resolved[:help_text]}\n\nSee [#{r[:id]} docs](#{help_uri(r[:id])}) for full guidance.",
187
+ help_markdown: "#{resolved[:help_text]}\n\nSee [#{r[:id]} docs](#{help_uri(r[:id])}) for full guidance."
188
188
  ).freeze
189
189
  end.freeze
190
190
  end
@@ -194,7 +194,7 @@ module StillActive
194
194
 
195
195
  # flavour: :ruby (default, native Gemfile path) or :neutral (SBOM path).
196
196
  def all(flavour = :ruby)
197
- flavour == :neutral ? NEUTRAL_CATALOG : CATALOG
197
+ (flavour == :neutral) ? NEUTRAL_CATALOG : CATALOG
198
198
  end
199
199
 
200
200
  def find(id, flavour = :ruby)
@@ -32,7 +32,7 @@ module StillActive
32
32
  "cargo" => :cargo,
33
33
  "maven" => :maven,
34
34
  "golang" => :go,
35
- "nuget" => :nuget,
35
+ "nuget" => :nuget
36
36
  }.freeze
37
37
 
38
38
  # PURL types that are not package dependencies: CI actions and opaque
@@ -100,7 +100,7 @@ module StillActive
100
100
 
101
101
  name = component["name"]
102
102
  purl = component["purl"]
103
- return [:unassessable, { ecosystem: nil, name: name, reason: :no_purl }] unless purl.is_a?(String) && !purl.empty?
103
+ return [:unassessable, {ecosystem: nil, name: name, reason: :no_purl}] unless purl.is_a?(String) && !purl.empty?
104
104
 
105
105
  classify_purl(purl, name)
106
106
  end
@@ -121,7 +121,7 @@ module StillActive
121
121
  "repo1.maven.org",
122
122
  "repo.maven.apache.org",
123
123
  "api.nuget.org",
124
- "proxy.golang.org",
124
+ "proxy.golang.org"
125
125
  ].freeze
126
126
 
127
127
  def classify_purl(purl, name)
@@ -130,7 +130,7 @@ module StillActive
130
130
  ecosystem = ECOSYSTEMS[type]
131
131
 
132
132
  if ecosystem
133
- return [:unassessable, { ecosystem: type, name: name, reason: :no_version }] if parsed.version.to_s.empty?
133
+ return [:unassessable, {ecosystem: type, name: name, reason: :no_version}] if parsed.version.to_s.empty?
134
134
 
135
135
  full_name = build_name(ecosystem, parsed.namespace, parsed.name)
136
136
  repository_url = parsed.qualifiers&.dig("repository_url")
@@ -143,14 +143,14 @@ module StillActive
143
143
  # from a public one, so it is still assessed by name. Best-effort, not a
144
144
  # guarantee that every private package is caught.
145
145
  if repository_url && !public_registry?(repository_url)
146
- return [:unassessable, { ecosystem: ecosystem, name: full_name, version: parsed.version, reason: :private_registry, repository_url: repository_url }]
146
+ return [:unassessable, {ecosystem: ecosystem, name: full_name, version: parsed.version, reason: :private_registry, repository_url: repository_url}]
147
147
  end
148
148
 
149
- [:dependency, { ecosystem: ecosystem, name: full_name, version: parsed.version }]
149
+ [:dependency, {ecosystem: ecosystem, name: full_name, version: parsed.version}]
150
150
  elsif NOISE_TYPES.include?(type)
151
151
  nil # CI actions / opaque binaries: not a package dependency
152
152
  else
153
- [:unassessable, { ecosystem: type, name: name, reason: :unsupported_ecosystem }]
153
+ [:unassessable, {ecosystem: type, name: name, reason: :unsupported_ecosystem}]
154
154
  end
155
155
  rescue ArgumentError
156
156
  # InvalidPackageURL is a subclass of ArgumentError, but PackageURL.parse also
@@ -158,7 +158,7 @@ module StillActive
158
158
  # or bad percent-encoding, both common in real Syft/Trivy output. Catch both so
159
159
  # one malformed component degrades to unassessable, never a backtrace that drops
160
160
  # every other dependency's verdict (SbomReader's documented "never raises").
161
- [:unassessable, { ecosystem: nil, name: name, reason: :malformed_purl }]
161
+ [:unassessable, {ecosystem: nil, name: name, reason: :malformed_purl}]
162
162
  end
163
163
 
164
164
  # Whether a repository_url points at a known public registry. We only read the
@@ -40,7 +40,7 @@ module StillActive
40
40
  runtime_ranges = {
41
41
  python: safe_support_window("Python") { PythonHelper.supported_python_range },
42
42
  dotnet: safe_support_window(".NET") { DotnetHelper.supported_dotnet_range },
43
- dotnetfx: safe_support_window(".NET Framework") { DotnetHelper.supported_dotnetfx_range },
43
+ dotnetfx: safe_support_window(".NET Framework") { DotnetHelper.supported_dotnetfx_range }
44
44
  }
45
45
  barrier = Async::Barrier.new
46
46
  semaphore = Async::Semaphore.new(StillActive.config.parallelism, parent: barrier)
@@ -63,15 +63,15 @@ module StillActive
63
63
  result["#{dep[:ecosystem]}/#{dep[:name]}@#{dep[:version]}"] =
64
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
- rescue StandardError => e
66
+ rescue => e
67
67
  # One dependency's failure must not abort the audit, but it must not
68
68
  # disappear either: record it as an unassessable entry (same shape as
69
69
  # the reader's, with a distinct reason) so it's counted and reported.
70
70
  # production rides along (when known, via slice) so a failed prod dep
71
71
  # stays distinguishable from a failed dev one.
72
- failures << { ecosystem: dep[:ecosystem], name: dep[:name], version: dep[:version], reason: :assessment_error, error: "#{e.class}: #{e.message}" }
72
+ failures << {ecosystem: dep[:ecosystem], name: dep[:name], version: dep[:version], reason: :assessment_error, error: "#{e.class}: #{e.message}"}
73
73
  .merge(dep.slice(:production))
74
- $stderr.puts("error assessing #{dep[:ecosystem]}/#{dep[:name]}@#{dep[:version]}: #{e.class}\n\t#{e.message}")
74
+ warn("error assessing #{dep[:ecosystem]}/#{dep[:name]}@#{dep[:version]}: #{e.class}\n\t#{e.message}")
75
75
  ensure
76
76
  completed += 1
77
77
  on_progress&.call(completed, total)
@@ -88,7 +88,7 @@ module StillActive
88
88
  # Stable, diffable order regardless of async completion order.
89
89
  Outcome.new(
90
90
  assessed: result.sort_by { |key, _| key }.to_h,
91
- failures: failures.sort_by { |failure| "#{failure[:ecosystem]}/#{failure[:name]}@#{failure[:version]}" },
91
+ failures: failures.sort_by { |failure| "#{failure[:ecosystem]}/#{failure[:name]}@#{failure[:version]}" }
92
92
  )
93
93
  end.wait
94
94
  end
@@ -99,8 +99,8 @@ module StillActive
99
99
  # ceiling checks for that runtime) with a warning, never aborting the audit.
100
100
  def safe_support_window(label)
101
101
  yield
102
- rescue StandardError => e
103
- $stderr.puts("warning: #{label} support window lookup failed: #{e.class} (#{e.message}); skipping language-ceiling checks")
102
+ rescue => e
103
+ warn("warning: #{label} support window lookup failed: #{e.class} (#{e.message}); skipping language-ceiling checks")
104
104
  nil
105
105
  end
106
106
  end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler"
4
+ require "cgi"
5
+ require "uri"
6
+
7
+ module StillActive
8
+ # Auth headers for a gem source, resolved the way Bundler itself does: from its
9
+ # per-host credential store (`bundle config` / the `BUNDLE_<HOST>` env var).
10
+ #
11
+ # The store is HOST-KEYED (`credentials_for` is `self[uri.to_s] || self[uri.host]`),
12
+ # which is the whole security property when a source URI comes from a lockfile: a
13
+ # host the user never configured resolves to no credential, so a malicious lockfile
14
+ # naming `attacker.example.com` cannot make still_active send another host's
15
+ # credential there. There is deliberately NO ambient/global fallback here; the only
16
+ # ambient token still_active holds (`--artifactory-token`) is guarded by an explicit
17
+ # host allowlist inside ArtifactoryClient, which is the sole caller that adds it.
18
+ module SourceCredentials
19
+ extend self
20
+
21
+ def headers_for(source_uri)
22
+ auth_header(Bundler.settings.credentials_for(URI(source_uri)))
23
+ end
24
+
25
+ # Turns a Bundler credential string into an Authorization header: `user:pass`
26
+ # becomes Basic (each half percent-decoded, since Bundler stores them encoded),
27
+ # a bare token becomes Bearer. Empty/nil yields no header.
28
+ def auth_header(credentials)
29
+ return {} if credentials.nil? || credentials.empty?
30
+
31
+ if credentials.include?(":")
32
+ user, pass = credentials.split(":", 2).map { |part| CGI.unescape(part) }
33
+ {"Authorization" => "Basic #{["#{user}:#{pass}"].pack("m0")}"}
34
+ else
35
+ {"Authorization" => "Bearer #{credentials}"}
36
+ end
37
+ end
38
+ end
39
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module StillActive
4
- VERSION = "3.0.0.rc4"
4
+ VERSION = "3.0.0.rc5"
5
5
  end