still_active 3.0.0.rc4 → 3.0.0.rc5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +3 -0
- data/lib/helpers/activity_helper.rb +3 -3
- data/lib/helpers/alternatives_helper.rb +1 -1
- data/lib/helpers/ansi_helper.rb +1 -1
- data/lib/helpers/bot_context.rb +7 -7
- data/lib/helpers/bundler_helper.rb +1 -2
- data/lib/helpers/catalog_index.rb +1 -1
- data/lib/helpers/constraint_helper.rb +6 -6
- data/lib/helpers/cvss_helper.rb +2 -2
- data/lib/helpers/cyclonedx_helper.rb +14 -14
- data/lib/helpers/diff_markdown_helper.rb +4 -4
- data/lib/helpers/dotnet_helper.rb +4 -4
- data/lib/helpers/endoflife_helper.rb +3 -3
- data/lib/helpers/http_helper.rb +31 -19
- data/lib/helpers/lockfile_dependency_parser.rb +2 -2
- data/lib/helpers/markdown_escape.rb +1 -1
- data/lib/helpers/markdown_helper.rb +7 -7
- data/lib/helpers/ruby_advisory_db.rb +1 -1
- data/lib/helpers/ruby_helper.rb +3 -3
- data/lib/helpers/runtime_ceiling_helper.rb +2 -2
- data/lib/helpers/sarif_helper.rb +34 -34
- data/lib/helpers/status_helper.rb +1 -1
- data/lib/helpers/summary_helper.rb +1 -1
- data/lib/helpers/terminal_helper.rb +8 -8
- data/lib/helpers/vulnerability_helper.rb +1 -1
- data/lib/still_active/artifactory_client.rb +47 -18
- data/lib/still_active/cli.rb +38 -38
- data/lib/still_active/compact_index_client.rb +117 -0
- data/lib/still_active/config.rb +1 -1
- data/lib/still_active/config_file.rb +4 -4
- data/lib/still_active/deps_dev_client.rb +7 -7
- data/lib/still_active/diff.rb +12 -12
- data/lib/still_active/ecosystem_lens.rb +5 -5
- data/lib/still_active/ecosystems_client.rb +7 -7
- data/lib/still_active/forgejo_client.rb +3 -3
- data/lib/still_active/github_client.rb +6 -6
- data/lib/still_active/gitlab_client.rb +3 -3
- data/lib/still_active/options.rb +4 -4
- data/lib/still_active/osv_client.rb +8 -8
- data/lib/still_active/poison_security_correlator.rb +3 -3
- data/lib/still_active/pypi_client.rb +2 -2
- data/lib/still_active/repository.rb +4 -4
- data/lib/still_active/sarif/rules.rb +20 -20
- data/lib/still_active/sbom_reader.rb +8 -8
- data/lib/still_active/sbom_workflow.rb +7 -7
- data/lib/still_active/source_credentials.rb +39 -0
- data/lib/still_active/version.rb +1 -1
- data/lib/still_active/workflow.rb +57 -31
- data/still_active.gemspec +16 -20
- metadata +5 -115
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 23f5514e3d64b78d82b0812eece16e13325045841fdd0b253acff4f7c5e31dae
|
|
4
|
+
data.tar.gz: b9d8907b6699755744639cc00e31135cf45991416c222990dc79247ae4c2d88f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4d571b1a3788960da4f6ae9fb04466c00ab6c3c6dcd1bd2051b4d6244aaf8e14248aeb3acc1c4d2179db33d8d18f98e320182ab0cb15635290e1be795c5509f9
|
|
7
|
+
data.tar.gz: 7071c08ca0bb94ff85edee290d6d55d47247093863f5599a8c56c2899871ef80e424daac09142e2c9978e0efbd5020d493c88abadc28c5a3bd5b303ffcea6003
|
data/CHANGELOG.md
CHANGED
|
@@ -18,6 +18,7 @@ The new `--fail-if-poison[=TIER]` and `--fail-if-language-ceiling[=TIER]` gates
|
|
|
18
18
|
|
|
19
19
|
### Added
|
|
20
20
|
|
|
21
|
+
- **Direct private gem sources are now audited, not skipped.** A gem resolving from a private host still_active has no bespoke client for (a direct Contribsys, Gemstash, Gemfury, or a private mirror) previously reported no version data at all. still_active now reads such a source through the RubyGems compact index (`/info/<gem>`, the protocol every Bundler-compatible host serves, which is how `bundle install` resolves from it), so these gems get real version/latest/libyear signals for the first time, falling back to the same "unqueryable" notice only when a host genuinely doesn't serve it. Credentials come from Bundler's own host-keyed store (`bundle config` / `BUNDLE_<HOST>`), so a source URL from a lockfile only ever receives the credential the user configured for that exact host, and still_active's ambient `--artifactory-token` is never sent to a lockfile-derived host (it stays behind its explicit `--artifactory-host` allowlist). The #43 guard is unchanged: a private gem still never borrows a public name-collision's repository or maintenance data.
|
|
21
22
|
- **Poison-pill / compatibility-ceiling signal (SA008).** A dormant or archived package that caps one of its runtime dependencies below that dependency's current latest major holds the tree below a ceiling no upstream release will raise, and it grows more constraining over time as the capped dep ships new majors. still_active flags it with a receipt naming the capped dep, the cap, and how many majors behind it holds you, ranked by a severity tier (`note`/`warning`/`critical`, scaling with majors-behind) and enforceable in CI with `--fail-if-poison[=TIER]`. Rendered in terminal, markdown, and SARIF, and computed for `rubygems` (native) and the flat-resolution SBOM ecosystems. Scoped to flat resolution deliberately: npm nests versions and cargo coexists majors, so a below-latest cap there is subtree-local noise, not a tree-wide block, and is suppressed.
|
|
22
23
|
- **Security "below the fix", the strongest poison case, now cross-ecosystem.** When a dormant/archived package pins a dependency that is *itself* known-vulnerable in the same tree, below the version that patches a HIGH-or-critical advisory, you cannot patch the CVE without replacing the dead capper. still_active leads with these findings (red in terminal/markdown, escalated to `error` in SARIF) and names the advisory and its nearest fix. This case travels to **npm and cargo** at patch precision (their fixes are mostly same-major patch bumps, invisible to a major-level check), via a real node-semver/Cargo matcher and a tree-copy soundness guard that only fires when every resolved copy the constraint governs is vulnerable (a safe copy elsewhere, or a patched copy the cap can reach, clears it). The correlation is whole-tree from data already assembled, no extra fetches.
|
|
23
24
|
- **Language-runtime ceiling signal (SA009), poison's sibling.** A pinned dependency version can strand you on an end-of-life language runtime (its `required_ruby_version` / `requires_python` forbids a supported one). still_active flags it against the runtime's EOL schedule (from endoflife.date), enforceable with `--fail-if-language-ceiling[=TIER]`, and reconciled against poison so an "upgrade to lift the ceiling" can't contradict a cap that forbids that upgrade. Ruby on the native path; **Python** (`requires_python`) and **.NET/NuGet** on the `--sbom` path. .NET is shaped differently: a package declares a SET of target frameworks, and consuming one is a restore-time wall (NU1202), so the ceiling fires only when every concrete runtime target is end-of-life. A `netstandard` target is the escape hatch (consumable from a current .NET), so its presence never fires, keeping the signal off nearly every modern library; `.NET Framework` (4.x) and `.NET` (Core/5+) are checked against their separate EOL calendars.
|
|
@@ -33,6 +34,8 @@ The new `--fail-if-poison[=TIER]` and `--fail-if-language-ceiling[=TIER]` gates
|
|
|
33
34
|
|
|
34
35
|
### Fixed
|
|
35
36
|
|
|
37
|
+
- **A private-source gem is no longer linked to the public rubygems.org page.** `ruby_gems_url` was set to `rubygems.org/gems/<name>` for any gem with versions, including one resolving from a private source, where that URL is a public name collision (for `sidekiq-pro`, the `0.0.3` squat-warning decoy) rather than the gem the user installs. It is now set only for a gem that actually lives on public rubygems.org, the same #43 guard the repository-URL substitution already applies.
|
|
38
|
+
- **An Artifactory-sourced gem is no longer reported `(YANKED)` when its real host is a legacy private registry.** Artifactory's `/api/v1/versions/<gem>.json` merges metadata from a virtual repo's member remotes; when a member (a legacy host like Contribsys) can't answer it, the merge returns only whatever rubygems.org holds for the name, which for `sidekiq-pro` is a `0.0.3` squat-warning placeholder. still_active read that endpoint first, so the installed version was absent from the list and looked yanked. It now reads the RubyGems compact index (`/info/<gem>`, what Bundler resolves through) instead, listing exactly the versions the repo can resolve, with the versions API demoted to dating those versions and AQL kept only as a last resort for hosts serving no compact index (and no longer presenting its cache timestamp as a release date). A side benefit: the compact index's per-version Ruby requirement and dependency constraints let the runtime-ceiling and poison-pill checks work on Artifactory sources for the first time.
|
|
36
39
|
- **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
40
|
- **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).
|
|
38
41
|
- **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.
|
|
@@ -19,12 +19,12 @@ module StillActive
|
|
|
19
19
|
def last_activity(gem_data)
|
|
20
20
|
release = [
|
|
21
21
|
gem_data[:latest_version_release_date],
|
|
22
|
-
gem_data[:latest_pre_release_version_release_date]
|
|
22
|
+
gem_data[:latest_pre_release_version_release_date]
|
|
23
23
|
].filter_map { parse_time(_1) }.max
|
|
24
|
-
return {
|
|
24
|
+
return {date: release, kind: :release} if release
|
|
25
25
|
|
|
26
26
|
commit = parse_time(gem_data[:last_commit_date])
|
|
27
|
-
commit ? {
|
|
27
|
+
commit ? {date: commit, kind: :commit} : nil
|
|
28
28
|
end
|
|
29
29
|
|
|
30
30
|
# Coerce a Time or an iso8601-ish string (the SARIF path may supply either)
|
data/lib/helpers/ansi_helper.rb
CHANGED
data/lib/helpers/bot_context.rb
CHANGED
|
@@ -22,7 +22,7 @@ module StillActive
|
|
|
22
22
|
# commits ("Update README to mention SARIF"), so we anchor on the v-prefixed
|
|
23
23
|
# version. False negatives (a no-`v` Renovate config) are acceptable; false
|
|
24
24
|
# positives are not. The `v` is consumed, so the captured version excludes it.
|
|
25
|
-
RENOVATE_SUBJECT = /\A(?:(?:chore|fix|build)\(deps(?:-dev)?\):\s*)?update (?:dependency )?(\S+) to v(\d[\w
|
|
25
|
+
RENOVATE_SUBJECT = /\A(?:(?:chore|fix|build)\(deps(?:-dev)?\):\s*)?update (?:dependency )?(\S+) to v(\d[\w.-]*)/i
|
|
26
26
|
|
|
27
27
|
# Unanchored variants used only to EXTRACT the bump *after* a bot is already
|
|
28
28
|
# confirmed (via GITHUB_ACTOR / branch / the anchored subject above). Because
|
|
@@ -30,7 +30,7 @@ module StillActive
|
|
|
30
30
|
# prefix or scope Dependabot/Renovate is configured with and just find the
|
|
31
31
|
# "bump X from Y to Z" / "update X to vN" skeleton anywhere in the subject.
|
|
32
32
|
DEPENDABOT_BUMP = /bump (\S+) from (\S+) to (\S+)/i
|
|
33
|
-
RENOVATE_BUMP = /update (?:dependency )?(\S+) to v(\d[\w
|
|
33
|
+
RENOVATE_BUMP = /update (?:dependency )?(\S+) to v(\d[\w.-]*)/i
|
|
34
34
|
|
|
35
35
|
# Returns { bot: "dependabot" | "renovate", bumps: [{ gem:, from:, to: }] }
|
|
36
36
|
# or nil when no bot signal is present. `bumps` is parsed from the head
|
|
@@ -39,12 +39,12 @@ module StillActive
|
|
|
39
39
|
bot = detect_bot(env: env, head_subject: head_subject)
|
|
40
40
|
return if bot.nil?
|
|
41
41
|
|
|
42
|
-
{
|
|
42
|
+
{bot: bot, bumps: bumps_from(bot, head_subject)}
|
|
43
43
|
end
|
|
44
44
|
|
|
45
45
|
# A one-line, format-agnostic narrative for the detected context.
|
|
46
46
|
def summary(context)
|
|
47
|
-
label = context[:bot] == "renovate" ? "Renovate" : "Dependabot"
|
|
47
|
+
label = (context[:bot] == "renovate") ? "Renovate" : "Dependabot"
|
|
48
48
|
bumps = context[:bumps]
|
|
49
49
|
|
|
50
50
|
case bumps.length
|
|
@@ -98,9 +98,9 @@ module StillActive
|
|
|
98
98
|
return [] if subject.nil?
|
|
99
99
|
|
|
100
100
|
if bot == "dependabot" && (match = subject.match(DEPENDABOT_BUMP))
|
|
101
|
-
[{
|
|
101
|
+
[{gem: match[1], from: match[2], to: match[3]}]
|
|
102
102
|
elsif bot == "renovate" && (match = subject.match(RENOVATE_BUMP))
|
|
103
|
-
[{
|
|
103
|
+
[{gem: match[1], from: nil, to: match[2]}]
|
|
104
104
|
else
|
|
105
105
|
[]
|
|
106
106
|
end
|
|
@@ -108,7 +108,7 @@ module StillActive
|
|
|
108
108
|
|
|
109
109
|
def single_bump_summary(label, bump)
|
|
110
110
|
arrow = bump[:from] ? "#{bump[:from]} → #{bump[:to]}" : "→ #{bump[:to]}"
|
|
111
|
-
verb = label == "Renovate" ? "update" : "bump"
|
|
111
|
+
verb = (label == "Renovate") ? "update" : "bump"
|
|
112
112
|
"#{label} #{verb}: #{bump[:gem]} #{arrow}"
|
|
113
113
|
end
|
|
114
114
|
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "set"
|
|
4
3
|
require_relative "lockfile_dependency_parser"
|
|
5
4
|
|
|
6
5
|
module StillActive
|
|
@@ -41,7 +40,7 @@ module StillActive
|
|
|
41
40
|
source_type: spec.source_type || :unknown,
|
|
42
41
|
source_uri: spec.source_uri,
|
|
43
42
|
direct: is_direct,
|
|
44
|
-
dependency_path: is_direct ? nil : paths[spec.name]
|
|
43
|
+
dependency_path: is_direct ? nil : paths[spec.name]
|
|
45
44
|
}
|
|
46
45
|
end
|
|
47
46
|
end
|
|
@@ -45,7 +45,7 @@ module StillActive
|
|
|
45
45
|
|
|
46
46
|
# => { kind: :permissive|:ceiling|:exact_pin, majors_behind: Integer }
|
|
47
47
|
def analyze(requirement:, dep_latest:)
|
|
48
|
-
return {
|
|
48
|
+
return {kind: :permissive, majors_behind: 0} if requirement.to_s.length > MAX_REQUIREMENT_LENGTH
|
|
49
49
|
|
|
50
50
|
# `||` is an OR-range (npm): satisfied by ANY branch, so an unbounded branch
|
|
51
51
|
# lifts the cap entirely and the effective ceiling is the LOOSEST branch.
|
|
@@ -56,11 +56,11 @@ module StillActive
|
|
|
56
56
|
ceilings = branches.map { |clauses| branch_ceiling(clauses) }
|
|
57
57
|
latest_major = major(dep_latest)
|
|
58
58
|
|
|
59
|
-
return {
|
|
59
|
+
return {kind: :permissive, majors_behind: 0} if ceilings.any?(&:nil?)
|
|
60
60
|
|
|
61
61
|
behind = latest_major ? [latest_major - ceilings.max, 0].max : 0
|
|
62
|
-
kind = branches.all? { |clauses| all_exact?(clauses) } ? :exact_pin : :ceiling
|
|
63
|
-
{
|
|
62
|
+
kind = (branches.all? { |clauses| all_exact?(clauses) }) ? :exact_pin : :ceiling
|
|
63
|
+
{kind: kind, majors_behind: behind}
|
|
64
64
|
end
|
|
65
65
|
|
|
66
66
|
# Severity tiers for a compatibility ceiling, worst-last (mirrors
|
|
@@ -150,7 +150,7 @@ module StillActive
|
|
|
150
150
|
requirement: dep[:requirements],
|
|
151
151
|
dep_latest: dep_latest,
|
|
152
152
|
majors_behind: result[:majors_behind],
|
|
153
|
-
kind: result[:kind]
|
|
153
|
+
kind: result[:kind]
|
|
154
154
|
}
|
|
155
155
|
end
|
|
156
156
|
end
|
|
@@ -161,7 +161,7 @@ module StillActive
|
|
|
161
161
|
# by the terminal and markdown renderers so the selection can't drift.
|
|
162
162
|
def top_findings(findings, limit: 3)
|
|
163
163
|
ranked = findings.sort_by { |f| [-f[:majors_behind], f[:dependency].to_s] }
|
|
164
|
-
{
|
|
164
|
+
{shown: ranked.first(limit), total: findings.length}
|
|
165
165
|
end
|
|
166
166
|
|
|
167
167
|
private
|
data/lib/helpers/cvss_helper.rb
CHANGED
|
@@ -27,7 +27,7 @@ module StillActive
|
|
|
27
27
|
|
|
28
28
|
cvss = CvssSuite.new(vector.to_s)
|
|
29
29
|
cvss.valid? ? cvss.overall_score : nil
|
|
30
|
-
rescue
|
|
30
|
+
rescue => e
|
|
31
31
|
# We only reach here with cvss-suite loaded (available?), so a raise is the
|
|
32
32
|
# installed gem failing this call site -- an incompatible version, a renamed
|
|
33
33
|
# `overall_score` -- not a bad vector, which cvss-suite reports via valid?.
|
|
@@ -57,7 +57,7 @@ module StillActive
|
|
|
57
57
|
return if @warned
|
|
58
58
|
|
|
59
59
|
@warned = true
|
|
60
|
-
|
|
60
|
+
warn("warning: cvss-suite scoring failed: #{error.class} (#{error.message}); CVSS numbers unavailable this run, check your cvss-suite version")
|
|
61
61
|
end
|
|
62
62
|
end
|
|
63
63
|
end
|
|
@@ -34,9 +34,9 @@ module StillActive
|
|
|
34
34
|
"version" => 1,
|
|
35
35
|
"metadata" => {
|
|
36
36
|
"timestamp" => now.iso8601,
|
|
37
|
-
"tools" => [{
|
|
37
|
+
"tools" => [{"vendor" => "SeanLF", "name" => "still_active", "version" => tool_version}]
|
|
38
38
|
},
|
|
39
|
-
"components" => components
|
|
39
|
+
"components" => components
|
|
40
40
|
}
|
|
41
41
|
document["vulnerabilities"] = vulnerabilities unless vulnerabilities.empty?
|
|
42
42
|
JSON.pretty_generate(document)
|
|
@@ -52,13 +52,13 @@ module StillActive
|
|
|
52
52
|
|
|
53
53
|
def gem_component(name, data)
|
|
54
54
|
version = data[:version_used]
|
|
55
|
-
component = {
|
|
55
|
+
component = {"type" => "library", "name" => name}
|
|
56
56
|
component["version"] = version if version
|
|
57
57
|
component["bom-ref"] = bom_ref(name, data)
|
|
58
58
|
component["purl"] = purl(name, version, data[:source_type]) if version
|
|
59
59
|
component["licenses"] = licenses(data[:license]) if data[:license]
|
|
60
60
|
if data[:repository_url]
|
|
61
|
-
component["externalReferences"] = [{
|
|
61
|
+
component["externalReferences"] = [{"type" => "vcs", "url" => data[:repository_url]}]
|
|
62
62
|
end
|
|
63
63
|
properties = gem_properties(data)
|
|
64
64
|
component["properties"] = properties unless properties.empty?
|
|
@@ -78,7 +78,7 @@ module StillActive
|
|
|
78
78
|
# false-matching a public gem of the same name; git/rubygems-sourced gems
|
|
79
79
|
# get pkg:gem so a fork still matches the upstream gem's advisories.
|
|
80
80
|
def purl(name, version, source_type)
|
|
81
|
-
type = source_type == :path ? "generic" : "gem"
|
|
81
|
+
type = (source_type == :path) ? "generic" : "gem"
|
|
82
82
|
"pkg:#{type}/#{name}@#{version}"
|
|
83
83
|
end
|
|
84
84
|
|
|
@@ -86,7 +86,7 @@ module StillActive
|
|
|
86
86
|
# display; CycloneDX's license.id must be a single SPDX id, so split back
|
|
87
87
|
# into one entry per license rather than emitting an invalid joined id.
|
|
88
88
|
def licenses(license)
|
|
89
|
-
license.split(", ").map { |id| {
|
|
89
|
+
license.split(", ").map { |id| {"license" => {"id" => id}} }
|
|
90
90
|
end
|
|
91
91
|
|
|
92
92
|
def gem_properties(data)
|
|
@@ -95,8 +95,8 @@ module StillActive
|
|
|
95
95
|
"still_active:scorecard_score" => data[:scorecard_score]&.to_s,
|
|
96
96
|
"still_active:libyear" => data[:libyear]&.to_s,
|
|
97
97
|
"still_active:last_commit_date" => iso8601(data[:last_commit_date]),
|
|
98
|
-
"still_active:version_yanked" => boolean_property(data[:version_yanked])
|
|
99
|
-
}.filter_map { |name, value| {
|
|
98
|
+
"still_active:version_yanked" => boolean_property(data[:version_yanked])
|
|
99
|
+
}.filter_map { |name, value| {"name" => name, "value" => value} unless value.nil? }
|
|
100
100
|
end
|
|
101
101
|
|
|
102
102
|
# The Ruby interpreter. CycloneDX's "platform" type fits semantically, but
|
|
@@ -116,9 +116,9 @@ module StillActive
|
|
|
116
116
|
"purl" => "pkg:generic/ruby@#{version}",
|
|
117
117
|
"cpe" => "cpe:2.3:a:ruby-lang:ruby:#{version}:*:*:*:*:*:*:*",
|
|
118
118
|
"properties" => [
|
|
119
|
-
{
|
|
120
|
-
{
|
|
121
|
-
].reject { |p| p["value"].nil? }
|
|
119
|
+
{"name" => "still_active:eol", "value" => boolean_property(ruby_info[:eol])},
|
|
120
|
+
{"name" => "still_active:libyear", "value" => ruby_info[:libyear]&.to_s}
|
|
121
|
+
].reject { |p| p["value"].nil? }
|
|
122
122
|
}
|
|
123
123
|
end
|
|
124
124
|
|
|
@@ -133,9 +133,9 @@ module StillActive
|
|
|
133
133
|
entry = {
|
|
134
134
|
"bom-ref" => "#{advisory[:id]}:#{component_ref}",
|
|
135
135
|
"id" => advisory[:id],
|
|
136
|
-
"affects" => [{
|
|
136
|
+
"affects" => [{"ref" => component_ref}]
|
|
137
137
|
}
|
|
138
|
-
entry["source"] = {
|
|
138
|
+
entry["source"] = {"name" => advisory[:source]} if advisory[:source]
|
|
139
139
|
advisory_rating = rating(advisory)
|
|
140
140
|
entry["ratings"] = [advisory_rating] if advisory_rating
|
|
141
141
|
entry
|
|
@@ -148,7 +148,7 @@ module StillActive
|
|
|
148
148
|
score = VulnerabilityHelper.effective_score(advisory)
|
|
149
149
|
return if score.nil?
|
|
150
150
|
|
|
151
|
-
rating = {
|
|
151
|
+
rating = {"score" => score, "severity" => VulnerabilityHelper.highest_severity([advisory]) || "unknown", "method" => rating_method(advisory)}
|
|
152
152
|
vector = advisory[:cvss3_vector] || advisory[:cvss_vector]
|
|
153
153
|
rating["vector"] = vector if vector
|
|
154
154
|
rating
|
|
@@ -16,7 +16,7 @@ module StillActive
|
|
|
16
16
|
fresher: "fresher",
|
|
17
17
|
older_relative: "older relative to latest",
|
|
18
18
|
neutral: nil,
|
|
19
|
-
unknown: nil
|
|
19
|
+
unknown: nil
|
|
20
20
|
}.freeze
|
|
21
21
|
|
|
22
22
|
def render(diff, accepted: [])
|
|
@@ -31,7 +31,7 @@ module StillActive
|
|
|
31
31
|
removed_section(diff.removed),
|
|
32
32
|
bumps_section(diff.bumped),
|
|
33
33
|
signal_changes_section(diff.signal_changes),
|
|
34
|
-
ruby_section(diff.ruby)
|
|
34
|
+
ruby_section(diff.ruby)
|
|
35
35
|
].reject(&:empty?)
|
|
36
36
|
|
|
37
37
|
"#{sections.join("\n")}\n"
|
|
@@ -45,7 +45,7 @@ module StillActive
|
|
|
45
45
|
["added", diff.added.size],
|
|
46
46
|
["removed", diff.removed.size],
|
|
47
47
|
["bumped", diff.bumped.size],
|
|
48
|
-
["signal-changes", diff.signal_changes.size]
|
|
48
|
+
["signal-changes", diff.signal_changes.size]
|
|
49
49
|
]
|
|
50
50
|
parts << ["accepted", accepted.size] unless accepted.empty?
|
|
51
51
|
parts.map { |label, n| "#{n} #{label}" }.join(" · ")
|
|
@@ -124,7 +124,7 @@ module StillActive
|
|
|
124
124
|
data["scorecard_score"] && "OpenSSF #{data["scorecard_score"]}",
|
|
125
125
|
(data["vulnerability_count"].to_i.positive? ? "#{data["vulnerability_count"]} vulns" : nil),
|
|
126
126
|
(data["archived"] ? "archived" : nil),
|
|
127
|
-
data["libyear"] && "#{data["libyear"]}y behind"
|
|
127
|
+
data["libyear"] && "#{data["libyear"]}y behind"
|
|
128
128
|
].compact
|
|
129
129
|
"#{MarkdownEscape.code_span(added.name)} (#{MarkdownEscape.inline(bits.join(", "))})"
|
|
130
130
|
end
|
|
@@ -99,9 +99,9 @@ module StillActive
|
|
|
99
99
|
finding = {
|
|
100
100
|
requirement: runtimes.map { |r| r[:moniker] }.uniq.sort.join(", "),
|
|
101
101
|
eol_forced: true,
|
|
102
|
-
runtime: ceiling[:family] == :dotnetfx ? ".NET Framework" : ".NET",
|
|
102
|
+
runtime: (ceiling[:family] == :dotnetfx) ? ".NET Framework" : ".NET",
|
|
103
103
|
ceiling_version: ceiling[:version].to_s,
|
|
104
|
-
ceiling_eol_date: ceiling[:cycle][:eol_date]
|
|
104
|
+
ceiling_eol_date: ceiling[:cycle][:eol_date]
|
|
105
105
|
}
|
|
106
106
|
finding.merge(severity: ConstraintHelper.constraint_severity(finding))
|
|
107
107
|
end
|
|
@@ -122,14 +122,14 @@ module StillActive
|
|
|
122
122
|
def build(family, version)
|
|
123
123
|
return unless version && Gem::Version.correct?(version)
|
|
124
124
|
|
|
125
|
-
{
|
|
125
|
+
{family: family, version: Gem::Version.new(version)}
|
|
126
126
|
end
|
|
127
127
|
|
|
128
128
|
# The endoflife cycle for a classified target, or nil when the feed is down or
|
|
129
129
|
# the cycle isn't tracked (an ancient net20 predating the feed) -- unknown, not
|
|
130
130
|
# assumed EOL.
|
|
131
131
|
def cycle_for(classification, dotnet:, dotnetfx:)
|
|
132
|
-
window = classification[:family] == :dotnetfx ? dotnetfx : dotnet
|
|
132
|
+
window = (classification[:family] == :dotnetfx) ? dotnetfx : dotnet
|
|
133
133
|
return if window.nil?
|
|
134
134
|
|
|
135
135
|
# Match on the release segment, ignoring any prerelease suffix: endoflife
|
|
@@ -47,13 +47,13 @@ module StillActive
|
|
|
47
47
|
# nil (the note is then suppressed) rather than returning nil, which would
|
|
48
48
|
# silently disable criticals too.
|
|
49
49
|
latest = cycles.first["latest"]
|
|
50
|
-
latest_stable = latest && Gem::Version.correct?(latest) ? Gem::Version.new(latest) : nil
|
|
50
|
+
latest_stable = (latest && Gem::Version.correct?(latest)) ? Gem::Version.new(latest) : nil
|
|
51
51
|
|
|
52
52
|
{
|
|
53
53
|
oldest_supported: supported.map { |cycle| cycle[:version] }.min,
|
|
54
54
|
latest_stable: latest_stable,
|
|
55
55
|
latest_stable_fresh: !latest_stable.nil? && latest_stable_fresh?(cycles.first),
|
|
56
|
-
cycles: normalized
|
|
56
|
+
cycles: normalized
|
|
57
57
|
}
|
|
58
58
|
end
|
|
59
59
|
|
|
@@ -100,7 +100,7 @@ module StillActive
|
|
|
100
100
|
{
|
|
101
101
|
version: Gem::Version.new(version),
|
|
102
102
|
eol: eol_reached?(cycle["eol"]) == true,
|
|
103
|
-
eol_date: parse_eol(cycle["eol"])
|
|
103
|
+
eol_date: parse_eol(cycle["eol"])
|
|
104
104
|
}
|
|
105
105
|
end
|
|
106
106
|
|
data/lib/helpers/http_helper.rb
CHANGED
|
@@ -19,7 +19,7 @@ module StillActive
|
|
|
19
19
|
SocketError,
|
|
20
20
|
SystemCallError,
|
|
21
21
|
OpenSSL::SSL::SSLError,
|
|
22
|
-
EOFError
|
|
22
|
+
EOFError
|
|
23
23
|
].freeze
|
|
24
24
|
MAX_REDIRECTS = 3
|
|
25
25
|
# Ceiling on a single response body. These are metadata endpoints (version
|
|
@@ -29,6 +29,8 @@ module StillActive
|
|
|
29
29
|
# multi-GB body and OOM the process. 16 MiB leaves generous headroom for a
|
|
30
30
|
# gem with thousands of versions while bounding worst-case memory.
|
|
31
31
|
MAX_BODY_BYTES = 16 * 1024 * 1024
|
|
32
|
+
JSON_PARSER = ->(body) { JSON.parse(body) }
|
|
33
|
+
IDENTITY = ->(body) { body }
|
|
32
34
|
|
|
33
35
|
extend self
|
|
34
36
|
|
|
@@ -40,6 +42,16 @@ module StillActive
|
|
|
40
42
|
request_json(uri, headers) { |target| Net::HTTP::Get.new(target) }
|
|
41
43
|
end
|
|
42
44
|
|
|
45
|
+
# As get_json, but for endpoints that answer in plain text (the RubyGems
|
|
46
|
+
# compact index). Shares the redirect, auth-scoping and body-cap handling;
|
|
47
|
+
# only the parse step differs.
|
|
48
|
+
def get_text(base_uri, path, headers: {})
|
|
49
|
+
uri = base_uri.dup
|
|
50
|
+
uri.path = path
|
|
51
|
+
|
|
52
|
+
request_json(uri, headers, parse: IDENTITY) { |target| Net::HTTP::Get.new(target) }
|
|
53
|
+
end
|
|
54
|
+
|
|
43
55
|
def post_json(base_uri, path, body:, headers: {})
|
|
44
56
|
uri = base_uri.dup
|
|
45
57
|
uri.path = path
|
|
@@ -61,10 +73,10 @@ module StillActive
|
|
|
61
73
|
end
|
|
62
74
|
|
|
63
75
|
# Runs the request, following up to MAX_REDIRECTS trusted-host redirects,
|
|
64
|
-
# and returns the parsed
|
|
65
|
-
# returns the request object, so GET and
|
|
66
|
-
# logic.
|
|
67
|
-
def request_json(uri, headers)
|
|
76
|
+
# and returns the parsed body (or nil), where `parse` decides JSON vs text.
|
|
77
|
+
# The block is yielded each URI and returns the request object, so GET and
|
|
78
|
+
# POST share the redirect/auth/cap logic.
|
|
79
|
+
def request_json(uri, headers, parse: JSON_PARSER)
|
|
68
80
|
MAX_REDIRECTS.times do
|
|
69
81
|
http = Net::HTTP.new(uri.host, uri.port)
|
|
70
82
|
http.use_ssl = true
|
|
@@ -74,7 +86,7 @@ module StillActive
|
|
|
74
86
|
request = yield(uri)
|
|
75
87
|
headers.each { |key, value| request[key] = value }
|
|
76
88
|
|
|
77
|
-
outcome, payload = perform(http, request, uri)
|
|
89
|
+
outcome, payload = perform(http, request, uri, parse)
|
|
78
90
|
case outcome
|
|
79
91
|
when :done
|
|
80
92
|
return payload
|
|
@@ -83,23 +95,23 @@ module StillActive
|
|
|
83
95
|
when :redirect
|
|
84
96
|
location = payload["Location"]
|
|
85
97
|
if location.nil? || location.empty?
|
|
86
|
-
|
|
98
|
+
warn("warning: #{uri.host}#{uri.path} returned HTTP #{payload.code} with no Location header")
|
|
87
99
|
return
|
|
88
100
|
end
|
|
89
101
|
|
|
90
102
|
redirect_uri = uri + location
|
|
91
103
|
unless TRUSTED_HOSTS.include?(redirect_uri.host)
|
|
92
|
-
|
|
104
|
+
warn("warning: #{uri.host}#{uri.path} redirected to untrusted host #{redirect_uri.host}, skipping")
|
|
93
105
|
return
|
|
94
106
|
end
|
|
95
107
|
# We dial every request over TLS (use_ssl = true). A redirect that
|
|
96
108
|
# downgrades to http is either a misconfiguration or a downgrade
|
|
97
109
|
# attempt; refuse it rather than silently dialing http-over-TLS.
|
|
98
110
|
unless redirect_uri.scheme == "https"
|
|
99
|
-
|
|
111
|
+
warn("warning: #{uri.host}#{uri.path} redirected to non-https #{redirect_uri.scheme} target, skipping")
|
|
100
112
|
return
|
|
101
113
|
end
|
|
102
|
-
|
|
114
|
+
warn("warning: #{uri.host}#{uri.path} redirected to #{redirect_uri.host}#{redirect_uri.path} (stale metadata?)")
|
|
103
115
|
# Auth is scoped to an origin (scheme + host + port), not just a host:
|
|
104
116
|
# a different port is a different service and must not inherit the token.
|
|
105
117
|
headers = {} unless same_origin?(uri, redirect_uri)
|
|
@@ -107,16 +119,16 @@ module StillActive
|
|
|
107
119
|
end
|
|
108
120
|
end
|
|
109
121
|
|
|
110
|
-
|
|
122
|
+
warn("warning: #{uri.host}#{uri.path} too many redirects")
|
|
111
123
|
nil
|
|
112
124
|
rescue *TRANSPORT_ERRORS => e
|
|
113
|
-
|
|
125
|
+
warn("warning: #{uri.host}#{uri.path} failed: #{e.class} (#{e.message})")
|
|
114
126
|
nil
|
|
115
127
|
rescue JSON::ParserError => e
|
|
116
|
-
|
|
128
|
+
warn("warning: #{uri.host}#{uri.path} returned invalid JSON: #{e.message}")
|
|
117
129
|
nil
|
|
118
130
|
rescue URI::InvalidURIError => e
|
|
119
|
-
|
|
131
|
+
warn("warning: #{uri.host}#{uri.path} returned an invalid redirect Location: #{e.message}")
|
|
120
132
|
nil
|
|
121
133
|
end
|
|
122
134
|
|
|
@@ -124,23 +136,23 @@ module StillActive
|
|
|
124
136
|
# cap rather than buffered whole. Returns one of:
|
|
125
137
|
# [:redirect, response] a 3xx, for the caller to follow
|
|
126
138
|
# [:stop, nil] non-success (warns unless 404), or body over cap
|
|
127
|
-
# [:done, parsed] a 2xx with parsed JSON
|
|
139
|
+
# [:done, parsed] a 2xx with the parsed body (JSON or text)
|
|
128
140
|
# Redirect and non-success bodies are never read: returning from the block
|
|
129
141
|
# unwinds through Net::HTTP, which closes the connection without draining
|
|
130
142
|
# the body, so a huge error/redirect body can't OOM us either.
|
|
131
|
-
def perform(http, request, uri)
|
|
143
|
+
def perform(http, request, uri, parse)
|
|
132
144
|
http.request(request) do |response|
|
|
133
145
|
return [:redirect, response] if response.is_a?(Net::HTTPRedirection)
|
|
134
146
|
|
|
135
147
|
unless response.is_a?(Net::HTTPSuccess)
|
|
136
|
-
|
|
148
|
+
warn("warning: #{uri.host}#{uri.path} returned HTTP #{response.code}") unless response.is_a?(Net::HTTPNotFound)
|
|
137
149
|
return [:stop, nil]
|
|
138
150
|
end
|
|
139
151
|
|
|
140
152
|
body = read_capped_body(response, uri)
|
|
141
153
|
return [:stop, nil] if body.nil?
|
|
142
154
|
|
|
143
|
-
return [:done,
|
|
155
|
+
return [:done, parse.call(body)]
|
|
144
156
|
end
|
|
145
157
|
end
|
|
146
158
|
|
|
@@ -151,7 +163,7 @@ module StillActive
|
|
|
151
163
|
response.read_body do |chunk|
|
|
152
164
|
body << chunk
|
|
153
165
|
if body.bytesize > MAX_BODY_BYTES
|
|
154
|
-
|
|
166
|
+
warn("warning: #{uri.host}#{uri.path} response exceeded #{MAX_BODY_BYTES} bytes, skipping")
|
|
155
167
|
return nil
|
|
156
168
|
end
|
|
157
169
|
end
|
|
@@ -25,7 +25,7 @@ module StillActive
|
|
|
25
25
|
# Lockfile source blocks and the source_type each maps to. PLUGIN SOURCE is
|
|
26
26
|
# recognized as a block (so its lines are consumed as inert data, not
|
|
27
27
|
# mis-read as specs) but yields no auditable gems.
|
|
28
|
-
SOURCE_TYPES = {
|
|
28
|
+
SOURCE_TYPES = {"GEM" => :rubygems, "GIT" => :git, "PATH" => :path}.freeze
|
|
29
29
|
PLUGIN_SOURCE = "PLUGIN SOURCE"
|
|
30
30
|
|
|
31
31
|
# A section header sits at column 0; Bundler emits them in SCREAMING form.
|
|
@@ -93,7 +93,7 @@ module StillActive
|
|
|
93
93
|
end
|
|
94
94
|
end
|
|
95
95
|
|
|
96
|
-
{
|
|
96
|
+
{specs: specs, direct: direct, plugin_source?: plugin_source}
|
|
97
97
|
end
|
|
98
98
|
end
|
|
99
99
|
end
|
|
@@ -51,7 +51,7 @@ module StillActive
|
|
|
51
51
|
return "`#{content}`" unless content.include?("`")
|
|
52
52
|
|
|
53
53
|
fence = "`" * (content.scan(/`+/).map(&:length).max + 1)
|
|
54
|
-
pad = content.start_with?("`") || content.end_with?("`") ? " " : ""
|
|
54
|
+
pad = (content.start_with?("`") || content.end_with?("`")) ? " " : ""
|
|
55
55
|
"#{fence}#{pad}#{content}#{pad}#{fence}"
|
|
56
56
|
end
|
|
57
57
|
end
|
|
@@ -51,20 +51,20 @@ module StillActive
|
|
|
51
51
|
version_with_date(
|
|
52
52
|
text: data[:version_used],
|
|
53
53
|
url: version_url(ruby_gems_url, data[:version_used]),
|
|
54
|
-
date: data[:version_used_release_date]
|
|
54
|
+
date: data[:version_used_release_date]
|
|
55
55
|
)
|
|
56
56
|
end
|
|
57
57
|
|
|
58
58
|
formatted_latest_version = version_with_date(
|
|
59
59
|
text: data[:latest_version],
|
|
60
60
|
url: version_url(ruby_gems_url, data[:latest_version]),
|
|
61
|
-
date: data[:latest_version_release_date]
|
|
61
|
+
date: data[:latest_version_release_date]
|
|
62
62
|
)
|
|
63
63
|
|
|
64
64
|
formatted_latest_pre_release = version_with_date(
|
|
65
65
|
text: data[:latest_pre_release_version],
|
|
66
66
|
url: version_url(ruby_gems_url, data[:latest_pre_release_version]),
|
|
67
|
-
date: data[:latest_pre_release_version_release_date]
|
|
67
|
+
date: data[:latest_pre_release_version_release_date]
|
|
68
68
|
)
|
|
69
69
|
|
|
70
70
|
# Only link when there is a date: markdown_url with a nil date + a present
|
|
@@ -86,7 +86,7 @@ module StillActive
|
|
|
86
86
|
formatted_latest_pre_release || unsure,
|
|
87
87
|
formatted_last_commit || unsure,
|
|
88
88
|
format_libyear(data[:libyear]),
|
|
89
|
-
format_license(data[:license])
|
|
89
|
+
format_license(data[:license])
|
|
90
90
|
]
|
|
91
91
|
|
|
92
92
|
"| #{cells.join(" | ")} |"
|
|
@@ -136,7 +136,7 @@ module StillActive
|
|
|
136
136
|
poison_rank(data), # below-the-fix leads, then security-relevant, then the rest
|
|
137
137
|
-ConstraintHelper::SEVERITY.index(data[:poison_severity] || :note),
|
|
138
138
|
-Array(data[:constraints]).map { |c| c[:majors_behind].to_i }.max,
|
|
139
|
-
name.to_s
|
|
139
|
+
name.to_s
|
|
140
140
|
]
|
|
141
141
|
end
|
|
142
142
|
ranked.each do |name, data|
|
|
@@ -202,7 +202,7 @@ module StillActive
|
|
|
202
202
|
else
|
|
203
203
|
"requires #{runtime} #{req}, no #{runtime} #{ceiling[:latest_stable]} support yet"
|
|
204
204
|
end
|
|
205
|
-
fix = ceiling[:fixed_by_upgrade] && data[:latest_version] ? "; upgrade to #{data[:latest_version]} to lift it" : ""
|
|
205
|
+
fix = (ceiling[:fixed_by_upgrade] && data[:latest_version]) ? "; upgrade to #{data[:latest_version]} to lift it" : ""
|
|
206
206
|
"#{base}#{fix}"
|
|
207
207
|
end
|
|
208
208
|
|
|
@@ -226,7 +226,7 @@ module StillActive
|
|
|
226
226
|
i.zero? ? "#{dep} #{req} (#{behind} major#{"s" unless behind == 1} behind, latest #{poison_major(finding[:dep_latest])})" : "#{dep} #{req} (#{behind})"
|
|
227
227
|
end
|
|
228
228
|
joined = parts.join(", ")
|
|
229
|
-
top[:total] > 1 ? "#{joined} — #{top[:total]} total" : joined
|
|
229
|
+
(top[:total] > 1) ? "#{joined} — #{top[:total]} total" : joined
|
|
230
230
|
end
|
|
231
231
|
|
|
232
232
|
# "8.0.1" -> "8.x": the cap is a major-level gap.
|
|
@@ -72,7 +72,7 @@ module StillActive
|
|
|
72
72
|
# No safe version a consumer can upgrade TO: the correlation bundler-audit
|
|
73
73
|
# + `bundle outdated` can't produce alone. A factual read of the DB.
|
|
74
74
|
no_fix_available: no_forward_fix?(advisory),
|
|
75
|
-
source: "ruby-advisory-db"
|
|
75
|
+
source: "ruby-advisory-db"
|
|
76
76
|
}
|
|
77
77
|
end
|
|
78
78
|
|