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