still_active 3.0.0.rc3 → 3.0.0.rc4

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 48cf9cbac0c507c9de0eae6930aac65686f32893d1e330c19d0392ccc5a3a824
4
- data.tar.gz: 0a1370fc970a0b03e7a5663a4c320322f4ac7788d161d144405cb8c703b005b5
3
+ metadata.gz: e74ec3ec40686e3eab640b852270c36278b33c82b54fdc85fe7bbb43ce551455
4
+ data.tar.gz: 7304228ba4411b50474cba46dec421c3ce4ff9e53911ea256bef623874d0910c
5
5
  SHA512:
6
- metadata.gz: 85e0df44ac6e16fe28023c5dedf5e7a08466c045f5e36558b91d42e71949d159df10262a894139916fc6dc5c744d5c15ec159630297a734104c17e1e60b2826e
7
- data.tar.gz: 4da6ad45492f57b76a1ec2f3e0f486be5aa7765cd2a4b956de0cf02c66af61c2d21ae0ff278aaf77a33a0e38b18a7d72a1a61bcd9b91d46259fac4d3a266b57b
6
+ metadata.gz: 6879718004e7d7a8765c7666f3d68686323873825ddb26240f929ee4b89a3337b0f7880373a34e669a72e545422dbf7a227a8142c9fede87f186e7f40fccfc3a
7
+ data.tar.gz: 2ed322c87a224e7e0bf2ea644dca8cdb341349b6985c251a47cf4abc62fcb9cef24d3dd6642d78789190aa7d64e1401814d4c1fa2fac7647746d4fef49657794
data/CHANGELOG.md CHANGED
@@ -33,6 +33,8 @@ The new `--fail-if-poison[=TIER]` and `--fail-if-language-ceiling[=TIER]` gates
33
33
 
34
34
  ### Fixed
35
35
 
36
+ - **SARIF `tool.driver` version fields are populated and spec-correct.** `version` was null, so a consumer reading `tool.driver.version` (rather than `semanticVersion`) saw nothing, and `semanticVersion` carried the RubyGems version verbatim (`3.0.0.rc4`), which is not valid SemVer 2.0.0 for a prerelease. Now `version` carries the free-form gem version and `semanticVersion` the SemVer form (`3.0.0-rc4`); final releases like `3.0.0` were already valid.
37
+ - **Cross-ecosystem SARIF no longer reads as a Ruby audit.** The `--sbom` path emitted the native Gemfile audit's Ruby-worded SARIF rule catalog, so a Go/npm/pypi repo's GitHub Code Scanning alerts were titled "Gem source repository is archived", advised `bundle update <gem>`, and referenced `Gemfile.lock`/RubyGems in the rule detail. The catalog now has an ecosystem-neutral flavour for SBOM runs (titles and guidance say "package"/"dependency"), and the native-only SA006 (Ruby runtime EOL), which the cross-ecosystem path can never emit, is dropped from the SBOM catalog rather than advertised in a non-Ruby repo. Per-result finding messages were already neutral; this fixes the rule metadata GitHub renders as the alert title and detail. The native Gemfile SARIF is unchanged (still `gem`-worded, SA006 present).
36
38
  - **A stale pre-release no longer corrupts the up-to-date signal.** A pre-release older than the latest stable (an `8.1.0.rc1` still listed after `8.1.2` shipped, or a decade-old `rc` on a gem long past it) was treated as a live upgrade target: `up_to_date` compares the version in hand against it, so any current version read as up to date and a gem that was actually behind got the "futurist" marker, while the pre-release column filled with superseded rcs. The latest pre-release is now kept only when strictly newer than the latest stable (or when there is no stable release at all, where it is the only signal), so the column, the `up_to_date` field, and the emoji agree. Genuine upcoming pre-releases still surface. On a real bundle this was mislabelling roughly twenty gems.
37
39
  - **`--baseline` now honours a committed `.still_active.yml`.** The PR-diff gate was the only gate that ignored the suppression list: a maintenance regression the audit and SARIF gates already accept still tripped `--baseline` the moment the dependency was added, with no way to accept it short of merging it to the baseline first. An accepted regression is now moved out of the CI-failable set and shown under an "Accepted (suppressed via `.still_active.yml`)" section with its reason, so the acceptance stays visible rather than silently vanishing. Only maintenance kinds a bare gem+signal entry can cover are accepted (archived/staleness map to `activity`/`libyear`); a newly introduced vulnerability still fails, since suppressing one needs an explicit advisory id the diff regression doesn't carry.
38
40
  - **The fail-open on the vulnerability gate is closed on both paths.** `--fail-if-vulnerable=<severity>` no longer passes a confirmed advisory that carries no CVSS score (an unscored advisory read as "below threshold" and cleared the gate, worst for a freshly disclosed CVE); it now fails closed with a per-gem stderr note (see Upgrading). And the native path no longer drops a confirmed advisory when its deps.dev detail fetch fails (429/timeout/5xx); it previously `filter_map`'d the failure away and zeroed `vulnerability_count`, it now keeps a minimal advisory so the finding survives and the fail-closed logic applies.
@@ -8,6 +8,7 @@ require_relative "lockfile_indexer"
8
8
  require_relative "activity_helper"
9
9
  require_relative "dependency_helper"
10
10
  require_relative "constraint_helper"
11
+ require_relative "version_helper"
11
12
 
12
13
  module StillActive
13
14
  # Renders a still_active workflow result as a SARIF 2.1.0 document.
@@ -42,7 +43,7 @@ module StillActive
42
43
  lockfile_uri: lockfile_uri,
43
44
  )
44
45
 
45
- JSON.pretty_generate(document(results: results, tool_version: tool_version))
46
+ JSON.pretty_generate(document(results: results, tool_version: tool_version, flavour: :ruby))
46
47
  end
47
48
 
48
49
  # The SBOM path emits the same rule set and document, but there is no lockfile
@@ -50,6 +51,9 @@ module StillActive
50
51
  # ruby_info is nil (Ruby EOL / SA006 is a native-only signal). Findings are
51
52
  # named ecosystem/name (each gem_data carries :ecosystem and :name), so a
52
53
  # cross-ecosystem alert reads "npm/left-pad ..." rather than a bare gem name.
54
+ # A polyglot SBOM has no single ecosystem, so the rule catalog uses the
55
+ # ecosystem-neutral wording (`flavour: :neutral`) rather than the native "gem"
56
+ # idiom, so a Go/npm/pypi repo's Code Scanning UI doesn't read as a Ruby audit.
53
57
  #
54
58
  # result: SbomWorkflow assessed hash ("ecosystem/name@version" => gem_data)
55
59
  # source_uri: the SBOM file to point findings at (basename)
@@ -63,12 +67,19 @@ module StillActive
63
67
  lockfile_uri: source_uri,
64
68
  )
65
69
 
66
- JSON.pretty_generate(document(results: results, tool_version: tool_version))
70
+ JSON.pretty_generate(document(results: results, tool_version: tool_version, flavour: :neutral))
67
71
  end
68
72
 
69
73
  private
70
74
 
71
- def document(results:, tool_version:)
75
+ def document(results:, tool_version:, flavour:)
76
+ # The catalog is the single source of truth for BOTH the driver rule list and
77
+ # each result's ruleIndex (its position in that list). The SBOM (:neutral)
78
+ # catalog drops the native-only SA006, so a result's ruleIndex must be looked
79
+ # up against the same-flavour catalog, not the ruby default, or SA007/8/9 would
80
+ # index the wrong driver rule. Stamping it here keeps the two in lockstep.
81
+ catalog = Sarif::Rules.all(flavour)
82
+ index_by_id = catalog.each_with_index.to_h { |rule, i| [rule[:id], i] }
72
83
  {
73
84
  "$schema" => SARIF_SCHEMA,
74
85
  "version" => "2.1.0",
@@ -76,20 +87,29 @@ module StillActive
76
87
  "tool" => {
77
88
  "driver" => {
78
89
  "name" => TOOL_NAME,
79
- "semanticVersion" => tool_version,
90
+ # `version` is the free-form native version string (the RubyGems
91
+ # version verbatim); `semanticVersion` MUST be SemVer 2.0.0, so a
92
+ # prerelease like "3.0.0.rc4" is rendered as "3.0.0-rc4". Both derive
93
+ # from StillActive::VERSION. GitHub Code Scanning only needs `name`,
94
+ # but some SARIF consumers read either version field.
95
+ "version" => tool_version,
96
+ "semanticVersion" => VersionHelper.to_semver(tool_version),
80
97
  "informationUri" => TOOL_URI,
81
- "rules" => sarif_rules,
98
+ "rules" => catalog.map { |r| sarif_rule(r) },
82
99
  },
83
100
  },
84
101
  "originalUriBaseIds" => { "%SRCROOT%" => { "uri" => "file:///" } },
85
- "results" => results,
102
+ "results" => results.map { |res| stamp_rule_index(res, index_by_id) },
86
103
  "columnKind" => "utf16CodeUnits",
87
104
  }],
88
105
  }
89
106
  end
90
107
 
91
- def sarif_rules
92
- Sarif::Rules.all.map { |r| sarif_rule(r) }
108
+ # SARIF ruleIndex is optional; omit it rather than emit nil when the rule isn't
109
+ # in this run's catalog (can't happen today, but never point at a bogus index).
110
+ def stamp_rule_index(result, index_by_id)
111
+ index = index_by_id[result["ruleId"]]
112
+ index ? result.merge("ruleIndex" => index) : result
93
113
  end
94
114
 
95
115
  def sarif_rule(r)
@@ -333,7 +353,6 @@ module StillActive
333
353
  latest_part = ruby_info[:latest_version] ? " Latest is #{ruby_info[:latest_version]}." : ""
334
354
  base = {
335
355
  "ruleId" => "SA006",
336
- "ruleIndex" => rule_index("SA006"),
337
356
  "level" => "error",
338
357
  "message" => { "text" => "Ruby #{version} has reached end-of-life#{eol_part}.#{latest_part}" },
339
358
  "locations" => [{
@@ -350,7 +369,6 @@ module StillActive
350
369
  level ||= Sarif::Rules.find(rule_id)[:level]
351
370
  base = {
352
371
  "ruleId" => rule_id,
353
- "ruleIndex" => rule_index(rule_id),
354
372
  "level" => level,
355
373
  "message" => { "text" => message },
356
374
  "locations" => [location],
@@ -379,10 +397,6 @@ module StillActive
379
397
  Digest::SHA256.hexdigest(["v1", rule_id, gem_name, advisory_id].compact.join("|"))[0, 16]
380
398
  end
381
399
 
382
- def rule_index(rule_id)
383
- Sarif::Rules.all.index { |r| r[:id] == rule_id }
384
- end
385
-
386
400
  def format_date(value)
387
401
  t = ActivityHelper.parse_time(value)
388
402
  t ? t.utc.strftime("%Y-%m-%d") : value.to_s
@@ -60,6 +60,28 @@ module StillActive
60
60
  to_gem_version(version_string)
61
61
  end
62
62
 
63
+ # Renders a MAJOR.MINOR.PATCH[.prerelease] RubyGems version as SemVer 2.0.0,
64
+ # for fields that require it (SARIF's tool.driver.semanticVersion). A gem
65
+ # prerelease joins its prerelease identifiers to the release with a ".", SemVer
66
+ # with a "-": "3.0.0.rc4" -> "3.0.0-rc4", "1.2.3.pre.5" -> "1.2.3-pre.5". A
67
+ # plain release ("3.0.0") is already SemVer and passes through, and a short
68
+ # numeric release is padded to MAJOR.MINOR.PATCH. A string that doesn't start
69
+ # with a numeric segment is returned unchanged rather than coerced into a bogus
70
+ # version. Scoped to the tool's own version scheme: a 4+-segment numeric
71
+ # release (e.g. a gem like "6.1.7.6") is NOT converted to valid SemVer.
72
+ def to_semver(version_string)
73
+ return version_string if version_string.nil? || version_string.empty?
74
+
75
+ parts = version_string.split(".")
76
+ release = parts.take_while { |part| part.match?(/\A\d+\z/) }
77
+ return version_string if release.empty?
78
+
79
+ prerelease = parts.drop(release.length)
80
+ release << "0" while release.length < 3
81
+ base = release.join(".")
82
+ prerelease.empty? ? base : "#{base}-#{prerelease.join(".")}"
83
+ end
84
+
63
85
  def gem_version(version_hash:)
64
86
  version_hash&.dig("number")
65
87
  end
@@ -38,6 +38,15 @@ module StillActive
38
38
  format("%.1f", score)
39
39
  end
40
40
 
41
+ # Each rule's canonical wording is Ruby-flavoured (the native Gemfile path is
42
+ # the tool's origin). A rule that can fire on the cross-ecosystem SBOM path
43
+ # also carries `neutral:` overrides for the strings that name "gem"/Ruby, so a
44
+ # polyglot SBOM run (Go/npm/pypi/...) emits an ecosystem-agnostic catalog. The
45
+ # catalog is emitted once per run, so wording is chosen per-run (native vs
46
+ # SBOM), not per-result. SA006 (Ruby EOL) is genuinely Ruby-only
47
+ # (`native_only: true`): the SBOM path can never emit it, so the neutral
48
+ # catalog drops it entirely rather than advertise a "Ruby runtime EOL" rule in,
49
+ # say, a Go repo's Code Scanning tab.
41
50
  RAW_RULES = [
42
51
  {
43
52
  id: "SA001",
@@ -48,6 +57,11 @@ module StillActive
48
57
  level: "error",
49
58
  security_severity: "7.5",
50
59
  tags: ["security", "supply-chain", "external/cwe/cwe-1357"],
60
+ neutral: {
61
+ short: "Package's source repository is archived",
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
+ },
51
65
  },
52
66
  {
53
67
  id: "SA002",
@@ -58,6 +72,11 @@ module StillActive
58
72
  level: "warning",
59
73
  security_severity: nil,
60
74
  tags: ["maintenance"],
75
+ neutral: {
76
+ short: "Package has had no release for over 3 years",
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
+ },
61
80
  },
62
81
  {
63
82
  id: "SA003",
@@ -68,6 +87,9 @@ module StillActive
68
87
  level: "error",
69
88
  security_severity: "7.0", # default; per-result override from CVSS
70
89
  tags: ["security", "vulnerability", "external/cwe/cwe-1104"],
90
+ neutral: {
91
+ short: "Package has known vulnerabilities (via deps.dev / OSV)",
92
+ },
71
93
  },
72
94
  {
73
95
  id: "SA004",
@@ -78,6 +100,10 @@ module StillActive
78
100
  level: "warning",
79
101
  security_severity: nil,
80
102
  tags: ["maintenance", "libyear"],
103
+ neutral: {
104
+ short: "Package is significantly behind the latest release",
105
+ help_text: "Schedule an upgrade to the latest release.",
106
+ },
81
107
  },
82
108
  {
83
109
  id: "SA005",
@@ -88,6 +114,9 @@ module StillActive
88
114
  level: "note",
89
115
  security_severity: nil,
90
116
  tags: ["supply-chain", "openssf"],
117
+ neutral: {
118
+ short: "Package's OpenSSF Scorecard is low",
119
+ },
91
120
  },
92
121
  {
93
122
  id: "SA006",
@@ -98,6 +127,7 @@ module StillActive
98
127
  level: "error",
99
128
  security_severity: "8.5",
100
129
  tags: ["security", "runtime", "external/cwe/cwe-1104"],
130
+ native_only: true,
101
131
  },
102
132
  {
103
133
  id: "SA007",
@@ -108,6 +138,11 @@ module StillActive
108
138
  level: "error",
109
139
  security_severity: "8.0",
110
140
  tags: ["security", "supply-chain", "external/cwe/cwe-1104"],
141
+ neutral: {
142
+ short: "Resolved package version has been yanked from its registry",
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
+ },
111
146
  },
112
147
  {
113
148
  id: "SA008",
@@ -118,12 +153,17 @@ module StillActive
118
153
  level: "warning",
119
154
  security_severity: nil, # maintenance/resolvability, not a CVE (as SA002/SA004/SA005)
120
155
  tags: ["maintenance", "supply-chain", "external/cwe/cwe-1104"],
156
+ neutral: {
157
+ short: "Dormant package caps a dependency below its latest major",
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
+ },
121
161
  },
122
162
  {
123
163
  id: "SA009",
124
164
  name: "RuntimeCeiling",
125
165
  short: "Resolved package version caps its language runtime",
126
- full: "A resolved package version's declared runtime constraint (Ruby `ruby_version`, Python `requires_python`) caps the language runtime you can run: either below every still-supported release (stranding you on an end-of-life runtime with no security patches) or below the latest stable (a compatibility ceiling to plan around). The default level is note; an EOL-forcing cap is raised to error per result.",
166
+ full: "A resolved package version's declared runtime constraint (Ruby `ruby_version`, Python `requires_python`, .NET target framework) caps the language runtime you can run: either below every still-supported release (stranding you on an end-of-life runtime with no security patches) or below the latest stable (a compatibility ceiling to plan around). The default level is note; an EOL-forcing cap is raised to error per result.",
127
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.",
128
168
  level: "note",
129
169
  security_severity: nil, # maintenance/compatibility, not a CVE (as SA002/SA004/SA005/SA008)
@@ -131,19 +171,34 @@ module StillActive
131
171
  },
132
172
  ].freeze
133
173
 
134
- CATALOG = RAW_RULES.map do |r|
135
- r.merge(
136
- tags: r[:tags].dup.freeze,
137
- help_markdown: "#{r[:help_text]}\n\nSee [#{r[:id]} docs](#{help_uri(r[:id])}) for full guidance.",
138
- ).freeze
139
- end.freeze
174
+ # Builds a frozen catalog for one wording flavour. `:neutral` applies each
175
+ # rule's `neutral:` overrides (ecosystem-agnostic wording for the SBOM path);
176
+ # any other flavour keeps the canonical Ruby wording. help_markdown is derived
177
+ # from the resolved help_text so it tracks the flavour. The neutral (SBOM)
178
+ # catalog omits `native_only:` rules, which the cross-ecosystem path can never
179
+ # emit (SA006, Ruby runtime EOL).
180
+ def build_catalog(flavour)
181
+ neutral = flavour == :neutral
182
+ rules = neutral ? RAW_RULES.reject { |r| r[:native_only] } : RAW_RULES
183
+ rules.map do |r|
184
+ resolved = neutral && r[:neutral] ? r.merge(r[:neutral]) : r
185
+ resolved.except(:neutral, :native_only).merge(
186
+ tags: r[:tags].dup.freeze,
187
+ help_markdown: "#{resolved[:help_text]}\n\nSee [#{r[:id]} docs](#{help_uri(r[:id])}) for full guidance.",
188
+ ).freeze
189
+ end.freeze
190
+ end
191
+
192
+ CATALOG = build_catalog(:ruby)
193
+ NEUTRAL_CATALOG = build_catalog(:neutral)
140
194
 
141
- def all
142
- CATALOG
195
+ # flavour: :ruby (default, native Gemfile path) or :neutral (SBOM path).
196
+ def all(flavour = :ruby)
197
+ flavour == :neutral ? NEUTRAL_CATALOG : CATALOG
143
198
  end
144
199
 
145
- def find(id)
146
- CATALOG.find { |r| r[:id] == id }
200
+ def find(id, flavour = :ruby)
201
+ all(flavour).find { |r| r[:id] == id }
147
202
  end
148
203
  end
149
204
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module StillActive
4
- VERSION = "3.0.0.rc3"
4
+ VERSION = "3.0.0.rc4"
5
5
  end
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.rc3
4
+ version: 3.0.0.rc4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sean Floyd