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
@@ -32,20 +32,20 @@ module StillActive
32
32
  # Apply the committed .still_active.yml first so CLI flags (parsed next)
33
33
  # win over it: CLI flag > env var > config file > default.
34
34
  config_data = ConfigFile.load
35
- ConfigFile.apply(StillActive.config, config_data).each { |warning| $stderr.puts("warning: #{warning}") }
35
+ ConfigFile.apply(StillActive.config, config_data).each { |warning| warn("warning: #{warning}") }
36
36
  options = begin
37
37
  Options.new.parse!(args)
38
38
  rescue ArgumentError, OptionParser::ParseError => e
39
39
  # Bad CLI input (conflicting flags, a missing file, an unknown flag, a bad
40
40
  # enum value) is a user error: print it and exit 2, matching the SBOM/baseline
41
41
  # paths, rather than letting the raise bubble into a Ruby backtrace.
42
- $stderr.puts("error: #{e.message}")
42
+ warn("error: #{e.message}")
43
43
  exit(2)
44
44
  end
45
45
  # After CLI flags resolve, nudge (don't auto-inherit) an un-imported
46
46
  # bundler-audit ignore list when the vulnerability gate is on.
47
47
  hint = ConfigFile.import_hint(config_data)
48
- $stderr.puts("hint: #{hint}") if hint
48
+ warn("hint: #{hint}") if hint
49
49
  # An SBOM audit is cross-ecosystem: it runs the deps.dev/ecosyste.ms lens
50
50
  # over the SBOM's packages, not Bundler over a lockfile. Dispatch before the
51
51
  # Bundler resolution below so a Gemfile is never required (or read).
@@ -55,7 +55,7 @@ module StillActive
55
55
  begin
56
56
  StillActive.config.gems = BundlerHelper.gemfile_dependencies
57
57
  rescue MissingLockfileError => e
58
- $stderr.puts("error: #{e.message}")
58
+ warn("error: #{e.message}")
59
59
  exit(2)
60
60
  end
61
61
  end
@@ -85,7 +85,7 @@ module StillActive
85
85
  output = {
86
86
  "$schema": SCHEMA_URL,
87
87
  schema_version: 1,
88
- tool: { name: "still_active", version: StillActive::VERSION },
88
+ tool: {name: "still_active", version: StillActive::VERSION},
89
89
  generated_at: Time.now.utc.iso8601,
90
90
  # A one-object digest of the audit's posture, so a machine/LLM
91
91
  # consumer reads the headline counts without iterating every gem.
@@ -95,9 +95,9 @@ module StillActive
95
95
  gems: result.transform_values do |data|
96
96
  data.merge(
97
97
  activity_level: ActivityHelper.activity_level(data),
98
- status: StatusHelper.gem_status(data),
98
+ status: StatusHelper.gem_status(data)
99
99
  )
100
- end,
100
+ end
101
101
  }
102
102
  output[:ruby] = ruby_info if ruby_info
103
103
  output[:pr_context] = pr_context if pr_context
@@ -127,7 +127,7 @@ module StillActive
127
127
  # field rename would silently zero every vuln count. Canary the schema once
128
128
  # and warn loudly so a degraded run never reads as an authoritative all-clear.
129
129
  unless DepsDevClient.advisory_schema_ok?
130
- $stderr.puts("warning: deps.dev vulnerability schema check failed (its `advisoryKeys` field may have changed, or the API is unreachable); vulnerability counts may be understated -- a clean result is NOT authoritative")
130
+ warn("warning: deps.dev vulnerability schema check failed (its `advisoryKeys` field may have changed, or the API is unreachable); vulnerability counts may be understated -- a clean result is NOT authoritative")
131
131
  end
132
132
  sbom = SbomReader.parse(path)
133
133
  outcome = if $stderr.tty?
@@ -174,7 +174,7 @@ module StillActive
174
174
  end
175
175
 
176
176
  def unsupported_sbom_format!(flag)
177
- $stderr.puts("error: #{flag} output is not supported in --sbom mode (it needs the native Ruby audit's lockfile/snapshot, which a cross-ecosystem SBOM can't supply); use --sarif, --markdown, --terminal, or --json")
177
+ warn("error: #{flag} output is not supported in --sbom mode (it needs the native Ruby audit's lockfile/snapshot, which a cross-ecosystem SBOM can't supply); use --sarif, --markdown, --terminal, or --json")
178
178
  exit(2)
179
179
  end
180
180
 
@@ -182,7 +182,7 @@ module StillActive
182
182
  sarif_json = SarifHelper.render_sbom(
183
183
  result: result,
184
184
  source_uri: File.basename(sbom_path),
185
- tool_version: StillActive::VERSION,
185
+ tool_version: StillActive::VERSION
186
186
  )
187
187
 
188
188
  write_output(sarif_path, sarif_json)
@@ -190,7 +190,7 @@ module StillActive
190
190
 
191
191
  # "-" means stdout (the SARIF/CycloneDX convention); any other PATH is a file.
192
192
  def write_output(path, content)
193
- path == "-" ? puts(content) : File.write(path, content)
193
+ (path == "-") ? puts(content) : File.write(path, content)
194
194
  end
195
195
 
196
196
  # SbomReader never raises (a malformed file degrades to an empty result), which
@@ -204,15 +204,15 @@ module StillActive
204
204
  # Matches SbomReader's own parse contract (a `components` array is what it
205
205
  # reads). A metadata-only CycloneDX has nothing to audit, so the message
206
206
  # names the missing array rather than over-claiming "not CycloneDX".
207
- $stderr.puts("error: #{path} has no CycloneDX `components` array to audit")
207
+ warn("error: #{path} has no CycloneDX `components` array to audit")
208
208
  exit(2)
209
209
  rescue JSON::ParserError
210
- $stderr.puts("error: #{path} is not valid JSON")
210
+ warn("error: #{path} is not valid JSON")
211
211
  exit(2)
212
212
  rescue SystemCallError => e
213
213
  # Options checked the path exists, not that it's readable (a permission
214
214
  # denial, or a directory). Exit cleanly instead of crashing with a trace.
215
- $stderr.puts("error: cannot read SBOM file #{path}: #{e.message}")
215
+ warn("error: cannot read SBOM file #{path}: #{e.message}")
216
216
  exit(2)
217
217
  end
218
218
 
@@ -222,16 +222,16 @@ module StillActive
222
222
  def emit_sbom_json(result, unassessable)
223
223
  output = {
224
224
  schema_version: 1,
225
- tool: { name: "still_active", version: StillActive::VERSION },
225
+ tool: {name: "still_active", version: StillActive::VERSION},
226
226
  generated_at: Time.now.utc.iso8601,
227
227
  summary: sbom_summary(result, unassessable),
228
228
  dependencies: result.transform_values do |data|
229
229
  data.merge(
230
230
  activity_level: ActivityHelper.activity_level(data),
231
- status: StatusHelper.gem_status(data),
231
+ status: StatusHelper.gem_status(data)
232
232
  )
233
233
  end,
234
- unassessable:,
234
+ unassessable:
235
235
  }
236
236
  puts iso8601_times(output).to_json
237
237
  end
@@ -244,7 +244,7 @@ module StillActive
244
244
  # The single worst per-dependency verdict, so a consumer reads one
245
245
  # project-level posture without scanning every dependency.
246
246
  status: StatusHelper.project_status(result),
247
- status_counts: statuses.tally,
247
+ status_counts: statuses.tally
248
248
  }
249
249
  end
250
250
 
@@ -253,8 +253,8 @@ module StillActive
253
253
  def warn_unassessable(unassessable)
254
254
  return if unassessable.empty?
255
255
 
256
- noun = unassessable.size == 1 ? "dependency" : "dependencies"
257
- $stderr.puts("warning: #{unassessable.size} #{noun} could not be assessed " \
256
+ noun = (unassessable.size == 1) ? "dependency" : "dependencies"
257
+ warn("warning: #{unassessable.size} #{noun} could not be assessed " \
258
258
  "(a private/alternative registry, unsupported ecosystem, missing version, no package URL, or a lookup failure); " \
259
259
  "see \"unassessable\" in the JSON output")
260
260
  end
@@ -278,7 +278,7 @@ module StillActive
278
278
  def warn_stale_suppressions
279
279
  present = StillActive.config.gems.map { |gem| gem[:name] }
280
280
  StillActive.config.suppressions.stale_gem_warnings(present).each do |warning|
281
- $stderr.puts("warning: #{warning}")
281
+ warn("warning: #{warning}")
282
282
  end
283
283
  end
284
284
 
@@ -288,10 +288,10 @@ module StillActive
288
288
  def warn_output_flag_conflicts(options)
289
289
  modes = active_output_modes
290
290
  if modes.size > 1
291
- $stderr.puts("warning: multiple output modes set (#{modes.join(", ")}); using #{modes.first}, ignoring #{modes.drop(1).join(", ")}")
291
+ warn("warning: multiple output modes set (#{modes.join(", ")}); using #{modes.first}, ignoring #{modes.drop(1).join(", ")}")
292
292
  end
293
293
  if options[:provided_cyclonedx_version] && StillActive.config.cyclonedx_path.nil?
294
- $stderr.puts("warning: --cyclonedx-version has no effect without --cyclonedx")
294
+ warn("warning: --cyclonedx-version has no effect without --cyclonedx")
295
295
  end
296
296
  end
297
297
 
@@ -301,14 +301,14 @@ module StillActive
301
301
  [
302
302
  ("--baseline" if config.baseline_path),
303
303
  ("--sarif" if config.sarif_path),
304
- ("--cyclonedx" if config.cyclonedx_path),
304
+ ("--cyclonedx" if config.cyclonedx_path)
305
305
  ].compact
306
306
  end
307
307
 
308
308
  def emit_sarif(result, ruby_info, sarif_path)
309
309
  lockfile = resolve_lockfile_path(StillActive.config.gemfile_path)
310
310
  unless File.exist?(lockfile)
311
- $stderr.puts("error: --sarif requires a lockfile at #{lockfile}")
311
+ warn("error: --sarif requires a lockfile at #{lockfile}")
312
312
  exit(2)
313
313
  end
314
314
 
@@ -316,7 +316,7 @@ module StillActive
316
316
  result: result,
317
317
  ruby_info: ruby_info,
318
318
  lockfile_path: lockfile,
319
- tool_version: StillActive::VERSION,
319
+ tool_version: StillActive::VERSION
320
320
  )
321
321
 
322
322
  write_output(sarif_path, sarif_json)
@@ -327,7 +327,7 @@ module StillActive
327
327
  result: result,
328
328
  ruby_info: ruby_info,
329
329
  tool_version: StillActive::VERSION,
330
- spec_version: StillActive.config.cyclonedx_version,
330
+ spec_version: StillActive.config.cyclonedx_version
331
331
  )
332
332
 
333
333
  write_output(cyclonedx_path, sbom)
@@ -349,13 +349,13 @@ module StillActive
349
349
  puts DiffMarkdownHelper.render(diff, accepted: accepted)
350
350
  exit(1) if diff.regressions.any?
351
351
  rescue JSON::ParserError => e
352
- $stderr.puts("error: --baseline file is not valid JSON: #{e.message}")
352
+ warn("error: --baseline file is not valid JSON: #{e.message}")
353
353
  exit(2)
354
354
  rescue Diff::UnsupportedSchemaError => e
355
- $stderr.puts("error: #{e.message}")
355
+ warn("error: #{e.message}")
356
356
  exit(2)
357
357
  rescue Errno::ENOENT, Errno::EACCES, Errno::EISDIR => e
358
- $stderr.puts("error: cannot read baseline file: #{e.message}")
358
+ warn("error: cannot read baseline file: #{e.message}")
359
359
  exit(2)
360
360
  end
361
361
 
@@ -383,9 +383,9 @@ module StillActive
383
383
  def current_snapshot(result, ruby_info)
384
384
  snapshot = {
385
385
  "schema_version" => 1,
386
- "tool" => { "name" => "still_active", "version" => StillActive::VERSION },
386
+ "tool" => {"name" => "still_active", "version" => StillActive::VERSION},
387
387
  "generated_at" => Time.now.utc.iso8601,
388
- "gems" => JSON.parse(result.to_json),
388
+ "gems" => JSON.parse(result.to_json)
389
389
  }
390
390
  snapshot["ruby"] = JSON.parse(ruby_info.to_json) if ruby_info
391
391
  snapshot
@@ -406,11 +406,11 @@ module StillActive
406
406
  gem_data[:last_activity_warning_emoji] = EmojiHelper.inactive_gem_emoji(gem_data)
407
407
  gem_data[:up_to_date_emoji] = EmojiHelper.using_latest_emoji(
408
408
  using_last_release: VersionHelper.up_to_date(
409
- version_used: gem_data[:version_used], latest_version: gem_data[:latest_version],
409
+ version_used: gem_data[:version_used], latest_version: gem_data[:latest_version]
410
410
  ),
411
411
  using_last_pre_release: VersionHelper.up_to_date(
412
- version_used: gem_data[:version_used], latest_pre_release_version: gem_data[:latest_pre_release_version],
413
- ),
412
+ version_used: gem_data[:version_used], latest_pre_release_version: gem_data[:latest_pre_release_version]
413
+ )
414
414
  )
415
415
 
416
416
  puts MarkdownHelper.markdown_table_body_line(gem_name: name, data: gem_data)
@@ -460,7 +460,7 @@ module StillActive
460
460
 
461
461
  ids = unknown.filter_map { |vuln| vuln[:id] }.join(", ")
462
462
  labelled = ids.empty? ? "" : " (#{ids})"
463
- $stderr.puts("warning: #{gem} has an advisory of unknown severity#{labelled}; failing --fail-if-vulnerable=#{threshold} because it can't be ruled out below the threshold (review, then fix or suppress it in .still_active.yml)")
463
+ warn("warning: #{gem} has an advisory of unknown severity#{labelled}; failing --fail-if-vulnerable=#{threshold} because it can't be ruled out below the threshold (review, then fix or suppress it in .still_active.yml)")
464
464
  end
465
465
  end
466
466
 
@@ -485,7 +485,7 @@ module StillActive
485
485
 
486
486
  # Bare --fail-if-poison fails at :warning and above (a 1-behind :note is FYI,
487
487
  # not a build breaker); --fail-if-poison=TIER sets an explicit threshold.
488
- threshold = config.fail_if_poison == true ? :warning : config.fail_if_poison
488
+ threshold = (config.fail_if_poison == true) ? :warning : config.fail_if_poison
489
489
  ConstraintHelper.severity_at_or_above?(data[:poison_severity], threshold)
490
490
  end
491
491
 
@@ -498,7 +498,7 @@ module StillActive
498
498
  # bare flag defaults to :critical (the genuine blocker: no supported runtime
499
499
  # is reachable). A latest-not-yet :note is an FYI/contribution lead that gates
500
500
  # only when the user explicitly opts in with =note.
501
- threshold = config.fail_if_language_ceiling == true ? :critical : config.fail_if_language_ceiling
501
+ threshold = (config.fail_if_language_ceiling == true) ? :critical : config.fail_if_language_ceiling
502
502
  ConstraintHelper.severity_at_or_above?(data.dig(:language_ceiling, :severity), threshold)
503
503
  end
504
504
 
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "cgi"
4
+ require "rubygems/resolver"
5
+ require "uri"
6
+ require_relative "../helpers/http_helper"
7
+
8
+ module StillActive
9
+ # The RubyGems compact index (`/info/<gem>`), which is what Bundler itself
10
+ # resolves through. It is not built by merging upstream metadata, so it lists
11
+ # the versions actually resolvable from the source even when a member remote (a
12
+ # legacy private gem host) can't answer the metadata endpoint Artifactory
13
+ # prefers (#142).
14
+ #
15
+ # The protocol is generic: any host that serves it (rubygems.org, Artifactory, a
16
+ # direct Contribsys/Gemstash/geminabox source) works here. The caller supplies
17
+ # the source URI and any auth headers, so this stays free of host-specific
18
+ # credential handling.
19
+ module CompactIndexClient
20
+ extend self
21
+
22
+ def versions(gem_name:, source_uri:, headers: {})
23
+ base = URI(source_uri.chomp("/"))
24
+ path = "#{base.path}/info/#{CGI.escape(gem_name)}"
25
+ body = HttpHelper.get_text(base, path, headers: headers)
26
+ return [] if body.nil? || body.empty?
27
+
28
+ result = parse(body)
29
+ # The parser never raises: a non-index 200 (an auth wall or HTML error page
30
+ # served with status 200) yields no versions instead of an error. Since the
31
+ # compact index is the primary source, a silent empty result would look
32
+ # identical to a clean parse and hide a broken source, so say so and let the
33
+ # caller fall through. A header-only index (every version yanked) is valid
34
+ # and empty, so the "---" marker, not mere emptiness, tells them apart.
35
+ if result.empty? && !compact_index?(body)
36
+ warn("warning: #{base.host} answered /info/#{gem_name} with a 200 that is not a RubyGems " \
37
+ "compact index (an auth wall or error page?); ignoring it and trying other sources")
38
+ end
39
+ result
40
+ end
41
+
42
+ private
43
+
44
+ def compact_index?(body)
45
+ body.split("\n").include?("---")
46
+ end
47
+
48
+ def parse(body)
49
+ entries(body)
50
+ .filter_map { |line| version_hash(line) }
51
+ .group_by { |hash| hash["number"] }
52
+ .map { |_number, rows| platform_independent(rows).except("platform") }
53
+ .sort_by { |hash| Gem::Version.new(hash["number"]) }
54
+ .reverse
55
+ end
56
+
57
+ # A version gets one row per built platform, variants first and the
58
+ # platform-independent build last. That last row is the one describing the
59
+ # gem rather than one prebuilt binary: a native gem's variants ship fewer
60
+ # dependencies, since they don't need the toolchain the source build does.
61
+ def platform_independent(rows)
62
+ rows.find { |row| row["platform"].nil? } || rows.first
63
+ end
64
+
65
+ # Everything after the `---` header. Artifactory is known to emit blank
66
+ # lines inside its index files (Bundler's own compact index parser carries
67
+ # the same workaround), and a blank line would parse as a nil version.
68
+ def entries(body)
69
+ lines = body.split("\n")
70
+ header = lines.index("---")
71
+ lines = lines[(header + 1)..] if header
72
+ lines.reject { |line| line.strip.empty? }
73
+ end
74
+
75
+ def version_hash(line)
76
+ number, platform = gem_parser.parse(line)
77
+ return if number.nil? || !Gem::Version.correct?(number)
78
+
79
+ meta = metadata(line)
80
+ {
81
+ "number" => number,
82
+ "platform" => platform,
83
+ "prerelease" => Gem::Version.new(number).prerelease?,
84
+ # Omitted entirely when a version declares no Ruby requirement, where
85
+ # the versions API sends ">= 0". Both mean unconstrained, and the
86
+ # ceiling check reads a missing requirement as "no ceiling".
87
+ "ruby_version" => meta["ruby"],
88
+ "checksum" => meta["checksum"],
89
+ # rubygems.org emits this; Artifactory's generated index does not, which
90
+ # is why a compact-index list still gets dated from the versions API.
91
+ "created_at" => meta["created_at"]
92
+ }
93
+ end
94
+
95
+ # The requirements section (everything after "|") is comma-separated
96
+ # `key:value` pairs, with `&` joining multiple constraints on one key. We
97
+ # parse it directly rather than through GemParser: GemParser's colon split
98
+ # changed across RubyGems versions and shreds an ISO-8601 `created_at`, whose
99
+ # value contains colons. Splitting on the FIRST colon keeps the timestamp
100
+ # whole on every version.
101
+ def metadata(line)
102
+ _deps, requirements = line.split("|", 2)
103
+ return {} unless requirements
104
+
105
+ requirements.split(",").each_with_object({}) do |pair, meta|
106
+ key, value = pair.split(":", 2)
107
+ meta[key] = value.split("&").join(", ") if value
108
+ end
109
+ end
110
+
111
+ # Used only for the leading `version[-platform]` token, which is a stable
112
+ # split across RubyGems versions. Ships with RubyGems since 3.2.3.
113
+ def gem_parser
114
+ @gem_parser ||= Gem::Resolver::APISet::GemParser.new
115
+ end
116
+ end
117
+ end
@@ -174,7 +174,7 @@ module StillActive
174
174
  end
175
175
 
176
176
  def presence(value)
177
- value && !value.empty? ? value : nil
177
+ (value && !value.empty?) ? value : nil
178
178
  end
179
179
  end
180
180
  end
@@ -26,7 +26,7 @@ module StillActive
26
26
  return {} if data.nil?
27
27
 
28
28
  unless data.is_a?(Hash)
29
- $stderr.puts("warning: #{FILENAME} must be a mapping of settings; ignoring it")
29
+ warn("warning: #{FILENAME} must be a mapping of settings; ignoring it")
30
30
  return {}
31
31
  end
32
32
 
@@ -34,7 +34,7 @@ module StillActive
34
34
  rescue Psych::Exception => e
35
35
  # Covers a syntax error and a disallowed tag (e.g. !ruby/object); either
36
36
  # way the committed file must never take the audit down with it.
37
- $stderr.puts("warning: #{FILENAME} could not be loaded (#{e.message}); ignoring it")
37
+ warn("warning: #{FILENAME} could not be loaded (#{e.message}); ignoring it")
38
38
  {}
39
39
  end
40
40
 
@@ -89,7 +89,7 @@ module StillActive
89
89
  count = bundler_audit_ignore_count(path)
90
90
  return unless count.positive?
91
91
 
92
- noun = count == 1 ? "advisory" : "advisories"
92
+ noun = (count == 1) ? "advisory" : "advisories"
93
93
  "#{BUNDLER_AUDIT_FILE} lists #{count} accepted #{noun}; add `import: [#{BUNDLER_AUDIT_FILE}]` to #{FILENAME} to honour them in still_active's --fail-if-vulnerable gate too"
94
94
  end
95
95
 
@@ -197,7 +197,7 @@ module StillActive
197
197
  next []
198
198
  end
199
199
 
200
- Array(imported["ignore"]).map { |advisory| { "advisory" => advisory, "reason" => "imported from #{rel}" } }
200
+ Array(imported["ignore"]).map { |advisory| {"advisory" => advisory, "reason" => "imported from #{rel}"} }
201
201
  rescue Psych::Exception => e
202
202
  warnings << "#{FILENAME}: import target #{rel} could not be loaded (#{e.message}), skipping it"
203
203
  []
@@ -15,7 +15,7 @@ module StillActive
15
15
  # `[]` -- so a rename or drop of that field would silently turn every count to
16
16
  # 0 and read a known-vulnerable package as clean (exit 0). django 3.0.0 carries
17
17
  # 30+ permanent advisories; if the canary returns none, the schema drifted.
18
- ADVISORY_CANARY = { system: "pypi", name: "django", version: "3.0.0" }.freeze
18
+ ADVISORY_CANARY = {system: "pypi", name: "django", version: "3.0.0"}.freeze
19
19
 
20
20
  # Is deps.dev still returning advisories in the shape we parse? False when the
21
21
  # canary comes back empty (schema drift) or unreachable (can't confirm). The
@@ -42,7 +42,7 @@ module StillActive
42
42
  # The locked version's release date -- the cross-ecosystem libyear input
43
43
  # (paired with the package's latest-release date). Already in this response,
44
44
  # so no extra fetch; nil when the feed omits it.
45
- published_at: body["publishedAt"],
45
+ published_at: body["publishedAt"]
46
46
  }
47
47
  end
48
48
 
@@ -74,7 +74,7 @@ module StillActive
74
74
  newest_version(versions)
75
75
  return if entry.nil?
76
76
 
77
- { version: entry.dig("versionKey", "version"), published_at: entry["publishedAt"] }
77
+ {version: entry.dig("versionKey", "version"), published_at: entry["publishedAt"]}
78
78
  end
79
79
 
80
80
  # The newest non-prerelease version by version number (not publishedAt: a
@@ -117,7 +117,7 @@ module StillActive
117
117
  # activity directly -- still_active's core question -- so we surface it
118
118
  # alongside the aggregate. nil when the check is absent (never 0, which
119
119
  # would read as "unmaintained" rather than "not measured").
120
- maintained: maintained_check_score(scorecard),
120
+ maintained: maintained_check_score(scorecard)
121
121
  }
122
122
  end
123
123
 
@@ -153,7 +153,7 @@ module StillActive
153
153
  cvss3_score: body["cvss3Score"],
154
154
  cvss3_vector: body["cvss3Vector"],
155
155
  cvss2_score: body["cvss2Score"],
156
- source: "deps.dev",
156
+ source: "deps.dev"
157
157
  }
158
158
  end
159
159
 
@@ -177,7 +177,7 @@ module StillActive
177
177
  return if @alias_drift_warned
178
178
 
179
179
  @alias_drift_warned = true
180
- $stderr.puts("warning: deps.dev returned a non-string advisory alias (#{raw.class}); the ALPHA API schema may have drifted")
180
+ warn("warning: deps.dev returned a non-string advisory alias (#{raw.class}); the ALPHA API schema may have drifted")
181
181
  end
182
182
 
183
183
  # The OpenSSF "Maintained" check score (0-10), or nil when it's absent. A
@@ -221,7 +221,7 @@ module StillActive
221
221
  # link). Parsing it as owner/repo yields a 404 and a blank repo cell, so treat a
222
222
  # reserved first segment as "no repo" rather than a bogus lookup.
223
223
  GITHUB_RESERVED_PATHS = [
224
- "sponsors", "orgs", "marketplace", "apps", "topics", "settings", "notifications", "about", "pricing", "features", "security", "contact",
224
+ "sponsors", "orgs", "marketplace", "apps", "topics", "settings", "notifications", "about", "pricing", "features", "security", "contact"
225
225
  ].freeze
226
226
 
227
227
  def repo_path_segments(host, segments)
@@ -25,7 +25,7 @@ module StillActive
25
25
  new_gem_archived: :activity,
26
26
  archived: :activity,
27
27
  new_gem_stale: :libyear,
28
- libyear_worsened: :libyear,
28
+ libyear_worsened: :libyear
29
29
  }.freeze
30
30
 
31
31
  class UnsupportedSchemaError < StandardError; end
@@ -70,7 +70,7 @@ module StillActive
70
70
  after_version: after["version_used"],
71
71
  kind: classify_bump(before, after),
72
72
  before: before,
73
- after: after,
73
+ after: after
74
74
  )
75
75
  end
76
76
  changes = collect_signal_changes(before, after)
@@ -82,7 +82,7 @@ module StillActive
82
82
  added: added,
83
83
  bumped: bumped,
84
84
  signal_changes: signal_changes,
85
- ruby_delta: ruby,
85
+ ruby_delta: ruby
86
86
  )
87
87
 
88
88
  Result.new(
@@ -91,7 +91,7 @@ module StillActive
91
91
  bumped: bumped,
92
92
  signal_changes: signal_changes,
93
93
  regressions: regressions,
94
- ruby: ruby,
94
+ ruby: ruby
95
95
  )
96
96
  end
97
97
 
@@ -181,7 +181,7 @@ module StillActive
181
181
  changes = []
182
182
 
183
183
  if !before["archived"] && after["archived"]
184
- changes << { kind: :archived, from: false, to: true }
184
+ changes << {kind: :archived, from: false, to: true}
185
185
  end
186
186
 
187
187
  opened = vuln_count(after) - vuln_count(before)
@@ -191,7 +191,7 @@ module StillActive
191
191
  # "new" in this list even though it's a re-keying of the same issue.
192
192
  # The vulnerability_count gate above keeps that to detail-string noise.
193
193
  new_ids = advisory_ids(after) - advisory_ids(before)
194
- changes << { kind: :new_vulnerability, from: vuln_count(before), to: vuln_count(after), ids: new_ids.first(3) }
194
+ changes << {kind: :new_vulnerability, from: vuln_count(before), to: vuln_count(after), ids: new_ids.first(3)}
195
195
  end
196
196
 
197
197
  if before["scorecard_score"] && after["scorecard_score"]
@@ -200,7 +200,7 @@ module StillActive
200
200
  # -> 7.0 dip is noise within the safe zone). Only drops below 7.0 cross.
201
201
  crossed = before["scorecard_score"] >= SCORECARD_GOOD_THRESHOLD && after["scorecard_score"] < SCORECARD_GOOD_THRESHOLD
202
202
  if drop >= SCORECARD_DROP_THRESHOLD || crossed
203
- changes << { kind: :scorecard_dropped, from: before["scorecard_score"], to: after["scorecard_score"], crossed_good: crossed }
203
+ changes << {kind: :scorecard_dropped, from: before["scorecard_score"], to: after["scorecard_score"], crossed_good: crossed}
204
204
  end
205
205
  end
206
206
 
@@ -211,12 +211,12 @@ module StillActive
211
211
  # technically grows (because upstream is releasing faster).
212
212
  delta = after["libyear"] - before["libyear"]
213
213
  if delta > LIBYEAR_DELTA_THRESHOLD
214
- changes << { kind: :libyear_worsened, from: before["libyear"], to: after["libyear"], delta: delta.round(2) }
214
+ changes << {kind: :libyear_worsened, from: before["libyear"], to: after["libyear"], delta: delta.round(2)}
215
215
  end
216
216
  end
217
217
 
218
218
  if !before["version_yanked"] && after["version_yanked"]
219
- changes << { kind: :version_yanked }
219
+ changes << {kind: :version_yanked}
220
220
  end
221
221
 
222
222
  changes
@@ -241,7 +241,7 @@ module StillActive
241
241
  regs << Regression.new(
242
242
  kind: :bump_introduced_vulns,
243
243
  gem: b.name,
244
- detail: "#{b.before_version} -> #{b.after_version}",
244
+ detail: "#{b.before_version} -> #{b.after_version}"
245
245
  )
246
246
  end
247
247
  end
@@ -263,7 +263,7 @@ module StillActive
263
263
  regs << Regression.new(
264
264
  kind: :libyear_worsened,
265
265
  gem: sc.name,
266
- detail: "libyear #{ch[:from]} -> #{ch[:to]} (+#{ch[:delta]}y; same pinned version)",
266
+ detail: "libyear #{ch[:from]} -> #{ch[:to]} (+#{ch[:delta]}y; same pinned version)"
267
267
  )
268
268
  end
269
269
  end
@@ -287,7 +287,7 @@ module StillActive
287
287
  to: after["version"],
288
288
  newly_eol: !before["eol"] && !!after["eol"],
289
289
  libyear_before: before["libyear"],
290
- libyear_after: after["libyear"],
290
+ libyear_after: after["libyear"]
291
291
  }
292
292
  end
293
293
 
@@ -39,7 +39,7 @@ module StillActive
39
39
  rubygems: "rubygems.org",
40
40
  npm: "npmjs.org",
41
41
  pypi: "pypi.org",
42
- cargo: "crates.io",
42
+ cargo: "crates.io"
43
43
  }.freeze
44
44
 
45
45
  # A transitive cap only holds the WHOLE tree hostage where the ecosystem
@@ -98,7 +98,7 @@ module StillActive
98
98
  # already fetched above (no extra call); nil when either date is missing.
99
99
  libyear: LibyearHelper.gem_libyear(
100
100
  version_used_release_date: parse_time(used_release_date),
101
- latest_version_release_date: parse_time(latest_release_date),
101
+ latest_version_release_date: parse_time(latest_release_date)
102
102
  ),
103
103
  # Whether the pinned version is at or ahead of deps.dev's latest stable, so a
104
104
  # prerelease or an ahead-of-stable pin reads current (true), not "behind" --
@@ -110,7 +110,7 @@ module StillActive
110
110
  scorecard_score: scorecard&.dig(:score),
111
111
  scorecard_maintained: scorecard&.dig(:maintained),
112
112
  vulnerability_count: vulnerabilities.length,
113
- vulnerabilities: vulnerabilities,
113
+ vulnerabilities: vulnerabilities
114
114
  }
115
115
  gem_data[:version_unresolved] = true if version_unresolved
116
116
  attach_constraints(gem_data, ecosystem: ecosystem, name: name, version: version, cache: constraint_cache)
@@ -239,7 +239,7 @@ module StillActive
239
239
  candidates = declared.filter_map do |dep|
240
240
  next if dep[:package_name].to_s.empty? || dep[:requirements].to_s.empty?
241
241
 
242
- { dependency: dep[:package_name], requirement: dep[:requirements] }
242
+ {dependency: dep[:package_name], requirement: dep[:requirements]}
243
243
  end
244
244
  gem_data[:capped_deps] = candidates unless candidates.empty?
245
245
  end
@@ -262,7 +262,7 @@ module StillActive
262
262
  # fetch only enriches CVSS/title. A failed enrichment must not drop the
263
263
  # count to zero and read a known-vulnerable dep as clean, so a key whose
264
264
  # detail can't be loaded still contributes a minimal advisory.
265
- deps_dev = advisory_keys.map { DepsDevClient.advisory_detail(advisory_id: _1) || { id: _1, source: "deps.dev" } }
265
+ deps_dev = advisory_keys.map { DepsDevClient.advisory_detail(advisory_id: _1) || {id: _1, source: "deps.dev"} }
266
266
  # No ruby-advisory-db here: that database is Ruby-only and the native
267
267
  # Bundler path already merges it. Cross-ecosystem advisories come from
268
268
  # deps.dev/OSV, which covers every system the lens serves.