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 +4 -4
- data/CHANGELOG.md +2 -0
- data/lib/helpers/sarif_helper.rb +28 -14
- data/lib/helpers/version_helper.rb +22 -0
- data/lib/still_active/sarif/rules.rb +66 -11
- data/lib/still_active/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e74ec3ec40686e3eab640b852270c36278b33c82b54fdc85fe7bbb43ce551455
|
|
4
|
+
data.tar.gz: 7304228ba4411b50474cba46dec421c3ce4ff9e53911ea256bef623874d0910c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
data/lib/helpers/sarif_helper.rb
CHANGED
|
@@ -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
|
-
|
|
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" =>
|
|
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
|
-
|
|
92
|
-
|
|
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
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
|
|
142
|
-
|
|
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
|
-
|
|
200
|
+
def find(id, flavour = :ruby)
|
|
201
|
+
all(flavour).find { |r| r[:id] == id }
|
|
147
202
|
end
|
|
148
203
|
end
|
|
149
204
|
end
|
data/lib/still_active/version.rb
CHANGED