git-pkgs 0.6.2 → 0.7.0
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/.gitattributes +28 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +15 -0
- data/Dockerfile +18 -0
- data/Formula/git-pkgs.rb +28 -0
- data/README.md +36 -4
- data/lib/git/pkgs/analyzer.rb +141 -9
- data/lib/git/pkgs/cli.rb +16 -6
- data/lib/git/pkgs/commands/blame.rb +0 -18
- data/lib/git/pkgs/commands/diff.rb +122 -5
- data/lib/git/pkgs/commands/diff_driver.rb +24 -4
- data/lib/git/pkgs/commands/init.rb +5 -0
- data/lib/git/pkgs/commands/list.rb +60 -15
- data/lib/git/pkgs/commands/show.rb +126 -3
- data/lib/git/pkgs/commands/stale.rb +6 -2
- data/lib/git/pkgs/commands/update.rb +3 -0
- data/lib/git/pkgs/commands/vulns/base.rb +354 -0
- data/lib/git/pkgs/commands/vulns/blame.rb +276 -0
- data/lib/git/pkgs/commands/vulns/diff.rb +172 -0
- data/lib/git/pkgs/commands/vulns/exposure.rb +418 -0
- data/lib/git/pkgs/commands/vulns/history.rb +345 -0
- data/lib/git/pkgs/commands/vulns/log.rb +218 -0
- data/lib/git/pkgs/commands/vulns/praise.rb +238 -0
- data/lib/git/pkgs/commands/vulns/scan.rb +231 -0
- data/lib/git/pkgs/commands/vulns/show.rb +216 -0
- data/lib/git/pkgs/commands/vulns/sync.rb +108 -0
- data/lib/git/pkgs/commands/vulns.rb +50 -0
- data/lib/git/pkgs/config.rb +8 -1
- data/lib/git/pkgs/database.rb +135 -5
- data/lib/git/pkgs/ecosystems.rb +83 -0
- data/lib/git/pkgs/models/package.rb +54 -0
- data/lib/git/pkgs/models/vulnerability.rb +300 -0
- data/lib/git/pkgs/models/vulnerability_package.rb +59 -0
- data/lib/git/pkgs/osv_client.rb +151 -0
- data/lib/git/pkgs/output.rb +22 -0
- data/lib/git/pkgs/version.rb +1 -1
- data/lib/git/pkgs.rb +6 -0
- metadata +66 -4
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "time"
|
|
4
|
+
|
|
5
|
+
module Git
|
|
6
|
+
module Pkgs
|
|
7
|
+
module Models
|
|
8
|
+
class Vulnerability < Sequel::Model
|
|
9
|
+
one_to_many :vulnerability_packages, key: :vulnerability_id
|
|
10
|
+
|
|
11
|
+
dataset_module do
|
|
12
|
+
def by_severity(severity)
|
|
13
|
+
where(severity: severity)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def critical
|
|
17
|
+
by_severity("critical")
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def high
|
|
21
|
+
by_severity("high")
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def medium
|
|
25
|
+
by_severity("medium")
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def low
|
|
29
|
+
by_severity("low")
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def not_withdrawn
|
|
33
|
+
where(withdrawn_at: nil)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def stale(max_age_seconds = 86400)
|
|
37
|
+
threshold = Time.now - max_age_seconds
|
|
38
|
+
where { fetched_at < threshold }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def fresh(max_age_seconds = 86400)
|
|
42
|
+
threshold = Time.now - max_age_seconds
|
|
43
|
+
where { fetched_at >= threshold }
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def severity_level
|
|
48
|
+
case severity&.downcase
|
|
49
|
+
when "critical" then 4
|
|
50
|
+
when "high" then 3
|
|
51
|
+
when "medium" then 2
|
|
52
|
+
when "low" then 1
|
|
53
|
+
else 0
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def severity_display
|
|
58
|
+
severity&.upcase || "UNKNOWN"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def withdrawn?
|
|
62
|
+
!withdrawn_at.nil?
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def aliases_list
|
|
66
|
+
return [] if aliases.nil? || aliases.empty?
|
|
67
|
+
|
|
68
|
+
aliases.split(",").map(&:strip)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Create or update from OSV API response data.
|
|
72
|
+
# Creates both the Vulnerability record and VulnerabilityPackage records
|
|
73
|
+
# for each affected package.
|
|
74
|
+
def self.from_osv(osv_data)
|
|
75
|
+
vuln_id = osv_data["id"]
|
|
76
|
+
severity_info = extract_severity(osv_data)
|
|
77
|
+
|
|
78
|
+
vuln = update_or_create(
|
|
79
|
+
{ id: vuln_id },
|
|
80
|
+
{
|
|
81
|
+
aliases: extract_aliases(osv_data),
|
|
82
|
+
severity: severity_info[:severity],
|
|
83
|
+
cvss_score: severity_info[:score],
|
|
84
|
+
cvss_vector: severity_info[:vector],
|
|
85
|
+
summary: osv_data["summary"],
|
|
86
|
+
details: osv_data["details"],
|
|
87
|
+
published_at: parse_timestamp(osv_data["published"]),
|
|
88
|
+
modified_at: parse_timestamp(osv_data["modified"]),
|
|
89
|
+
withdrawn_at: parse_timestamp(osv_data["withdrawn"]),
|
|
90
|
+
fetched_at: Time.now
|
|
91
|
+
}
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
# Create VulnerabilityPackage records for each affected package
|
|
95
|
+
(osv_data["affected"] || []).each do |affected|
|
|
96
|
+
pkg = affected["package"]
|
|
97
|
+
next unless pkg
|
|
98
|
+
|
|
99
|
+
ecosystem = pkg["ecosystem"]
|
|
100
|
+
name = pkg["name"]
|
|
101
|
+
|
|
102
|
+
affected_range = build_affected_range(affected)
|
|
103
|
+
fixed = extract_fixed_versions(affected)
|
|
104
|
+
|
|
105
|
+
VulnerabilityPackage.update_or_create(
|
|
106
|
+
{ vulnerability_id: vuln_id, ecosystem: ecosystem, package_name: name },
|
|
107
|
+
{
|
|
108
|
+
affected_versions: affected_range,
|
|
109
|
+
fixed_versions: fixed&.join(",")
|
|
110
|
+
}
|
|
111
|
+
)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
vuln
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def self.extract_aliases(osv_data)
|
|
118
|
+
aliases = osv_data["aliases"] || []
|
|
119
|
+
aliases.any? ? aliases.join(",") : nil
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def self.extract_severity(osv_data)
|
|
123
|
+
result = { severity: nil, score: nil, vector: nil }
|
|
124
|
+
|
|
125
|
+
if osv_data["severity"]&.any?
|
|
126
|
+
sev = osv_data["severity"].first
|
|
127
|
+
result[:vector] = sev["score"]
|
|
128
|
+
|
|
129
|
+
if sev["score"]&.include?("CVSS")
|
|
130
|
+
result[:score] = parse_cvss_score(sev["score"])
|
|
131
|
+
result[:severity] = score_to_severity(result[:score])
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Check root-level database_specific (GHSA format)
|
|
136
|
+
if osv_data["database_specific"]&.dig("severity")
|
|
137
|
+
result[:severity] ||= normalize_severity(osv_data["database_specific"]["severity"])
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Check affected entries for database_specific severity
|
|
141
|
+
osv_data["affected"]&.each do |affected|
|
|
142
|
+
db_specific = affected["database_specific"]
|
|
143
|
+
if db_specific && db_specific["severity"]
|
|
144
|
+
result[:severity] ||= normalize_severity(db_specific["severity"])
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
result
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def self.normalize_severity(severity)
|
|
152
|
+
return nil unless severity
|
|
153
|
+
|
|
154
|
+
case severity.downcase
|
|
155
|
+
when "critical" then "critical"
|
|
156
|
+
when "high" then "high"
|
|
157
|
+
when "moderate", "medium" then "medium"
|
|
158
|
+
when "low" then "low"
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def self.parse_cvss_score(vector)
|
|
163
|
+
return nil unless vector
|
|
164
|
+
|
|
165
|
+
if vector.match?(/\A\d+(\.\d+)?\z/)
|
|
166
|
+
return vector.to_f
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
return nil unless vector.include?("CVSS:")
|
|
170
|
+
|
|
171
|
+
metrics = parse_cvss_metrics(vector)
|
|
172
|
+
return nil if metrics.empty?
|
|
173
|
+
|
|
174
|
+
estimate_cvss_score(metrics)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def self.parse_cvss_metrics(vector)
|
|
178
|
+
metrics = {}
|
|
179
|
+
vector.split("/").each do |part|
|
|
180
|
+
key, value = part.split(":")
|
|
181
|
+
metrics[key] = value if key && value
|
|
182
|
+
end
|
|
183
|
+
metrics
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def self.estimate_cvss_score(metrics)
|
|
187
|
+
impact_values = { "N" => 0, "L" => 1, "H" => 2 }
|
|
188
|
+
c = impact_values[metrics["C"]] || 0
|
|
189
|
+
i = impact_values[metrics["I"]] || 0
|
|
190
|
+
a = impact_values[metrics["A"]] || 0
|
|
191
|
+
max_impact = [c, i, a].max
|
|
192
|
+
|
|
193
|
+
ac_easy = metrics["AC"] == "L"
|
|
194
|
+
av_network = metrics["AV"] == "N"
|
|
195
|
+
pr_none = metrics["PR"] == "N"
|
|
196
|
+
ui_none = metrics["UI"] == "N"
|
|
197
|
+
|
|
198
|
+
if max_impact == 2 && av_network && ac_easy && pr_none && ui_none
|
|
199
|
+
9.8
|
|
200
|
+
elsif max_impact == 2 && av_network && ac_easy
|
|
201
|
+
8.1
|
|
202
|
+
elsif max_impact == 2
|
|
203
|
+
7.0
|
|
204
|
+
elsif max_impact == 1 && av_network
|
|
205
|
+
5.3
|
|
206
|
+
elsif max_impact == 1
|
|
207
|
+
4.0
|
|
208
|
+
elsif max_impact == 0
|
|
209
|
+
0.0
|
|
210
|
+
else
|
|
211
|
+
5.0
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def self.score_to_severity(score)
|
|
216
|
+
return nil unless score
|
|
217
|
+
|
|
218
|
+
case score
|
|
219
|
+
when 9.0..10.0 then "critical"
|
|
220
|
+
when 7.0...9.0 then "high"
|
|
221
|
+
when 4.0...7.0 then "medium"
|
|
222
|
+
when 0.0...4.0 then "low"
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def self.build_affected_range(affected)
|
|
227
|
+
return nil unless affected
|
|
228
|
+
|
|
229
|
+
ranges = affected["ranges"] || []
|
|
230
|
+
versions = affected["versions"] || []
|
|
231
|
+
|
|
232
|
+
return versions.join(",") if versions.any? && ranges.empty?
|
|
233
|
+
|
|
234
|
+
range_parts = ranges.flat_map do |range|
|
|
235
|
+
events = range["events"] || []
|
|
236
|
+
build_range_from_events(events)
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
range_parts.compact.join(" || ")
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def self.build_range_from_events(events)
|
|
243
|
+
ranges = []
|
|
244
|
+
current_introduced = nil
|
|
245
|
+
|
|
246
|
+
events.each do |event|
|
|
247
|
+
if event["introduced"]
|
|
248
|
+
current_introduced = event["introduced"]
|
|
249
|
+
elsif event["fixed"] && current_introduced
|
|
250
|
+
if current_introduced == "0"
|
|
251
|
+
ranges << "<#{event["fixed"]}"
|
|
252
|
+
else
|
|
253
|
+
ranges << ">=#{current_introduced} <#{event["fixed"]}"
|
|
254
|
+
end
|
|
255
|
+
current_introduced = nil
|
|
256
|
+
elsif event["last_affected"] && current_introduced
|
|
257
|
+
if current_introduced == "0"
|
|
258
|
+
ranges << "<=#{event["last_affected"]}"
|
|
259
|
+
else
|
|
260
|
+
ranges << ">=#{current_introduced} <=#{event["last_affected"]}"
|
|
261
|
+
end
|
|
262
|
+
current_introduced = nil
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
if current_introduced
|
|
267
|
+
ranges << if current_introduced == "0"
|
|
268
|
+
">=0"
|
|
269
|
+
else
|
|
270
|
+
">=#{current_introduced}"
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
ranges
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def self.extract_fixed_versions(affected)
|
|
278
|
+
return nil unless affected
|
|
279
|
+
|
|
280
|
+
fixed = []
|
|
281
|
+
(affected["ranges"] || []).each do |range|
|
|
282
|
+
(range["events"] || []).each do |event|
|
|
283
|
+
fixed << event["fixed"] if event["fixed"]
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
fixed.uniq.empty? ? nil : fixed.uniq
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def self.parse_timestamp(str)
|
|
291
|
+
return nil unless str
|
|
292
|
+
|
|
293
|
+
Time.parse(str)
|
|
294
|
+
rescue ArgumentError
|
|
295
|
+
nil
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "vers"
|
|
4
|
+
|
|
5
|
+
module Git
|
|
6
|
+
module Pkgs
|
|
7
|
+
module Models
|
|
8
|
+
class VulnerabilityPackage < Sequel::Model
|
|
9
|
+
many_to_one :vulnerability, key: :vulnerability_id
|
|
10
|
+
|
|
11
|
+
dataset_module do
|
|
12
|
+
def for_package(ecosystem, name)
|
|
13
|
+
where(ecosystem: ecosystem, package_name: name)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def affects_version?(version)
|
|
18
|
+
return false if affected_versions.nil? || affected_versions.empty?
|
|
19
|
+
return false if version.nil? || version.empty?
|
|
20
|
+
|
|
21
|
+
# Convert OSV ecosystem to purl type for Vers
|
|
22
|
+
bib_ecosystem = Ecosystems.from_osv(ecosystem) || ecosystem.downcase
|
|
23
|
+
purl_type = Ecosystems.to_purl(bib_ecosystem) || bib_ecosystem
|
|
24
|
+
|
|
25
|
+
# Handle || separator (OR conditions between different ranges)
|
|
26
|
+
# Each part separated by || is an independent range (OR)
|
|
27
|
+
# Within each part, space-separated constraints are AND conditions
|
|
28
|
+
affected_versions.split(" || ").any? do |range_part|
|
|
29
|
+
range_matches?(version, range_part, purl_type)
|
|
30
|
+
end
|
|
31
|
+
rescue ArgumentError, Vers::Error
|
|
32
|
+
# If we can't parse the version or range, be conservative and assume affected
|
|
33
|
+
true
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def range_matches?(version, range_part, purl_type)
|
|
37
|
+
# Extract individual constraints (e.g., ">=7.1.0 <7.1.3.1" -> [">=7.1.0", "<7.1.3.1"])
|
|
38
|
+
constraints = range_part.scan(/[<>=!~^]+[^\s]+/)
|
|
39
|
+
return false if constraints.empty?
|
|
40
|
+
|
|
41
|
+
# All constraints must be satisfied (AND logic)
|
|
42
|
+
constraints.all? do |constraint|
|
|
43
|
+
Vers.satisfies?(version, constraint, purl_type)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def fixed_versions_list
|
|
48
|
+
return [] if fixed_versions.nil? || fixed_versions.empty?
|
|
49
|
+
|
|
50
|
+
fixed_versions.split(",").map(&:strip)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def purl
|
|
54
|
+
Models::Package.generate_purl(ecosystem, package_name)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "json"
|
|
5
|
+
require "uri"
|
|
6
|
+
|
|
7
|
+
module Git
|
|
8
|
+
module Pkgs
|
|
9
|
+
# Client for the OSV (Open Source Vulnerabilities) API.
|
|
10
|
+
# https://google.github.io/osv.dev/api/
|
|
11
|
+
class OsvClient
|
|
12
|
+
API_BASE = "https://api.osv.dev/v1"
|
|
13
|
+
BATCH_SIZE = 1000 # Max queries per batch request
|
|
14
|
+
|
|
15
|
+
class Error < StandardError; end
|
|
16
|
+
class ApiError < Error; end
|
|
17
|
+
|
|
18
|
+
def initialize
|
|
19
|
+
@http_clients = {}
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Query vulnerabilities for a single package version.
|
|
23
|
+
#
|
|
24
|
+
# @param ecosystem [String] OSV ecosystem name (e.g., "RubyGems")
|
|
25
|
+
# @param name [String] package name
|
|
26
|
+
# @param version [String] package version
|
|
27
|
+
# @return [Array<Hash>] array of vulnerability hashes
|
|
28
|
+
def query(ecosystem:, name:, version:)
|
|
29
|
+
payload = {
|
|
30
|
+
package: {
|
|
31
|
+
name: name,
|
|
32
|
+
ecosystem: ecosystem
|
|
33
|
+
},
|
|
34
|
+
version: version
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
response = post("/query", payload)
|
|
38
|
+
fetch_all_pages(response, payload)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Batch query vulnerabilities for multiple packages.
|
|
42
|
+
# More efficient than individual queries for large dependency sets.
|
|
43
|
+
#
|
|
44
|
+
# @param packages [Array<Hash>] array of {ecosystem:, name:, version:} hashes
|
|
45
|
+
# @return [Array<Array<Hash>>] array of vulnerability arrays, one per input package
|
|
46
|
+
def query_batch(packages)
|
|
47
|
+
return [] if packages.empty?
|
|
48
|
+
|
|
49
|
+
results = Array.new(packages.size) { [] }
|
|
50
|
+
|
|
51
|
+
packages.each_slice(BATCH_SIZE).with_index do |batch, batch_idx|
|
|
52
|
+
queries = batch.map do |pkg|
|
|
53
|
+
{
|
|
54
|
+
package: {
|
|
55
|
+
name: pkg[:name],
|
|
56
|
+
ecosystem: pkg[:ecosystem]
|
|
57
|
+
},
|
|
58
|
+
version: pkg[:version]
|
|
59
|
+
}
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
response = post("/querybatch", { queries: queries })
|
|
63
|
+
batch_results = response["results"] || []
|
|
64
|
+
|
|
65
|
+
batch_results.each_with_index do |result, idx|
|
|
66
|
+
global_idx = batch_idx * BATCH_SIZE + idx
|
|
67
|
+
results[global_idx] = result["vulns"] || []
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
results
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Fetch full details for a specific vulnerability by ID.
|
|
75
|
+
#
|
|
76
|
+
# @param vuln_id [String] vulnerability ID (e.g., "CVE-2024-1234", "GHSA-xxxx")
|
|
77
|
+
# @return [Hash] full vulnerability data
|
|
78
|
+
def get_vulnerability(vuln_id)
|
|
79
|
+
get("/vulns/#{URI.encode_uri_component(vuln_id)}")
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
private
|
|
83
|
+
|
|
84
|
+
def post(path, payload)
|
|
85
|
+
uri = URI("#{API_BASE}#{path}")
|
|
86
|
+
request = Net::HTTP::Post.new(uri)
|
|
87
|
+
request["Content-Type"] = "application/json"
|
|
88
|
+
request.body = JSON.generate(payload)
|
|
89
|
+
|
|
90
|
+
execute_request(uri, request)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def get(path)
|
|
94
|
+
uri = URI("#{API_BASE}#{path}")
|
|
95
|
+
request = Net::HTTP::Get.new(uri)
|
|
96
|
+
request["Content-Type"] = "application/json"
|
|
97
|
+
|
|
98
|
+
execute_request(uri, request)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def execute_request(uri, request)
|
|
102
|
+
http = http_client(uri)
|
|
103
|
+
response = http.request(request)
|
|
104
|
+
|
|
105
|
+
case response
|
|
106
|
+
when Net::HTTPSuccess
|
|
107
|
+
JSON.parse(response.body)
|
|
108
|
+
else
|
|
109
|
+
raise ApiError, "OSV API error: #{response.code} #{response.message}"
|
|
110
|
+
end
|
|
111
|
+
rescue JSON::ParserError => e
|
|
112
|
+
raise ApiError, "Invalid JSON response from OSV API: #{e.message}"
|
|
113
|
+
rescue Net::OpenTimeout, Net::ReadTimeout => e
|
|
114
|
+
raise ApiError, "OSV API timeout: #{e.message}"
|
|
115
|
+
rescue SocketError, Errno::ECONNREFUSED => e
|
|
116
|
+
raise ApiError, "OSV API connection error: #{e.message}"
|
|
117
|
+
rescue OpenSSL::SSL::SSLError => e
|
|
118
|
+
raise ApiError, "OSV API SSL error: #{e.message}"
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def http_client(uri)
|
|
122
|
+
key = "#{uri.host}:#{uri.port}"
|
|
123
|
+
@http_clients[key] ||= begin
|
|
124
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
125
|
+
http.use_ssl = uri.scheme == "https"
|
|
126
|
+
http.open_timeout = 10
|
|
127
|
+
http.read_timeout = 30
|
|
128
|
+
http
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
MAX_PAGES = 100
|
|
133
|
+
|
|
134
|
+
def fetch_all_pages(response, original_payload)
|
|
135
|
+
vulns = response["vulns"] || []
|
|
136
|
+
page_token = response["next_page_token"]
|
|
137
|
+
pages_fetched = 0
|
|
138
|
+
|
|
139
|
+
while page_token && pages_fetched < MAX_PAGES
|
|
140
|
+
payload = original_payload.merge(page_token: page_token)
|
|
141
|
+
response = post("/query", payload)
|
|
142
|
+
vulns.concat(response["vulns"] || [])
|
|
143
|
+
page_token = response["next_page_token"]
|
|
144
|
+
pages_fetched += 1
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
vulns
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
data/lib/git/pkgs/output.rb
CHANGED
|
@@ -52,6 +52,28 @@ module Git
|
|
|
52
52
|
|
|
53
53
|
error "Database not initialized. Run 'git pkgs init' first."
|
|
54
54
|
end
|
|
55
|
+
|
|
56
|
+
# Pick best author from commit, preferring humans over bots
|
|
57
|
+
def best_author(commit)
|
|
58
|
+
author_name = commit.respond_to?(:author_name) ? commit.author_name : commit[:author_name]
|
|
59
|
+
message = commit.respond_to?(:message) ? commit.message : commit[:message]
|
|
60
|
+
|
|
61
|
+
authors = [author_name] + parse_coauthors(message)
|
|
62
|
+
|
|
63
|
+
# Prefer human authors over bots
|
|
64
|
+
human = authors.find { |a| !bot_author?(a) }
|
|
65
|
+
human || authors.first
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def parse_coauthors(message)
|
|
69
|
+
return [] unless message
|
|
70
|
+
|
|
71
|
+
message.scan(/^Co-authored-by:([^<]+)<[^>]+>/i).flatten.map(&:strip)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def bot_author?(name)
|
|
75
|
+
name =~ /\[bot\]$|^dependabot|^renovate|^github-actions/i
|
|
76
|
+
end
|
|
55
77
|
end
|
|
56
78
|
end
|
|
57
79
|
end
|
data/lib/git/pkgs/version.rb
CHANGED
data/lib/git/pkgs.rb
CHANGED
|
@@ -8,6 +8,8 @@ require_relative "pkgs/cli"
|
|
|
8
8
|
require_relative "pkgs/database"
|
|
9
9
|
require_relative "pkgs/repository"
|
|
10
10
|
require_relative "pkgs/analyzer"
|
|
11
|
+
require_relative "pkgs/ecosystems"
|
|
12
|
+
require_relative "pkgs/osv_client"
|
|
11
13
|
|
|
12
14
|
require_relative "pkgs/models/branch"
|
|
13
15
|
require_relative "pkgs/models/branch_commit"
|
|
@@ -15,6 +17,9 @@ require_relative "pkgs/models/commit"
|
|
|
15
17
|
require_relative "pkgs/models/manifest"
|
|
16
18
|
require_relative "pkgs/models/dependency_change"
|
|
17
19
|
require_relative "pkgs/models/dependency_snapshot"
|
|
20
|
+
require_relative "pkgs/models/package"
|
|
21
|
+
require_relative "pkgs/models/vulnerability"
|
|
22
|
+
require_relative "pkgs/models/vulnerability_package"
|
|
18
23
|
|
|
19
24
|
require_relative "pkgs/commands/init"
|
|
20
25
|
require_relative "pkgs/commands/update"
|
|
@@ -37,6 +42,7 @@ require_relative "pkgs/commands/upgrade"
|
|
|
37
42
|
require_relative "pkgs/commands/schema"
|
|
38
43
|
require_relative "pkgs/commands/diff_driver"
|
|
39
44
|
require_relative "pkgs/commands/completions"
|
|
45
|
+
require_relative "pkgs/commands/vulns"
|
|
40
46
|
|
|
41
47
|
module Git
|
|
42
48
|
module Pkgs
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: git-pkgs
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.7.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Andrew Nesbitt
|
|
@@ -57,14 +57,56 @@ dependencies:
|
|
|
57
57
|
requirements:
|
|
58
58
|
- - "~>"
|
|
59
59
|
- !ruby/object:Gem::Version
|
|
60
|
-
version: '15.
|
|
60
|
+
version: '15.2'
|
|
61
61
|
type: :runtime
|
|
62
62
|
prerelease: false
|
|
63
63
|
version_requirements: !ruby/object:Gem::Requirement
|
|
64
64
|
requirements:
|
|
65
65
|
- - "~>"
|
|
66
66
|
- !ruby/object:Gem::Version
|
|
67
|
-
version: '15.
|
|
67
|
+
version: '15.2'
|
|
68
|
+
- !ruby/object:Gem::Dependency
|
|
69
|
+
name: vers
|
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - "~>"
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '1.0'
|
|
75
|
+
type: :runtime
|
|
76
|
+
prerelease: false
|
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - "~>"
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '1.0'
|
|
82
|
+
- !ruby/object:Gem::Dependency
|
|
83
|
+
name: purl
|
|
84
|
+
requirement: !ruby/object:Gem::Requirement
|
|
85
|
+
requirements:
|
|
86
|
+
- - "~>"
|
|
87
|
+
- !ruby/object:Gem::Version
|
|
88
|
+
version: '1.7'
|
|
89
|
+
type: :runtime
|
|
90
|
+
prerelease: false
|
|
91
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
92
|
+
requirements:
|
|
93
|
+
- - "~>"
|
|
94
|
+
- !ruby/object:Gem::Version
|
|
95
|
+
version: '1.7'
|
|
96
|
+
- !ruby/object:Gem::Dependency
|
|
97
|
+
name: sarif-ruby
|
|
98
|
+
requirement: !ruby/object:Gem::Requirement
|
|
99
|
+
requirements:
|
|
100
|
+
- - ">="
|
|
101
|
+
- !ruby/object:Gem::Version
|
|
102
|
+
version: '0'
|
|
103
|
+
type: :runtime
|
|
104
|
+
prerelease: false
|
|
105
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
106
|
+
requirements:
|
|
107
|
+
- - ">="
|
|
108
|
+
- !ruby/object:Gem::Version
|
|
109
|
+
version: '0'
|
|
68
110
|
description: A git subcommand for analyzing package/dependency usage in git repositories
|
|
69
111
|
over time
|
|
70
112
|
email:
|
|
@@ -74,7 +116,11 @@ executables:
|
|
|
74
116
|
extensions: []
|
|
75
117
|
extra_rdoc_files: []
|
|
76
118
|
files:
|
|
119
|
+
- ".gitattributes"
|
|
120
|
+
- ".ruby-version"
|
|
77
121
|
- CHANGELOG.md
|
|
122
|
+
- Dockerfile
|
|
123
|
+
- Formula/git-pkgs.rb
|
|
78
124
|
- LICENSE
|
|
79
125
|
- README.md
|
|
80
126
|
- exe/git-pkgs
|
|
@@ -101,16 +147,32 @@ files:
|
|
|
101
147
|
- lib/git/pkgs/commands/tree.rb
|
|
102
148
|
- lib/git/pkgs/commands/update.rb
|
|
103
149
|
- lib/git/pkgs/commands/upgrade.rb
|
|
150
|
+
- lib/git/pkgs/commands/vulns.rb
|
|
151
|
+
- lib/git/pkgs/commands/vulns/base.rb
|
|
152
|
+
- lib/git/pkgs/commands/vulns/blame.rb
|
|
153
|
+
- lib/git/pkgs/commands/vulns/diff.rb
|
|
154
|
+
- lib/git/pkgs/commands/vulns/exposure.rb
|
|
155
|
+
- lib/git/pkgs/commands/vulns/history.rb
|
|
156
|
+
- lib/git/pkgs/commands/vulns/log.rb
|
|
157
|
+
- lib/git/pkgs/commands/vulns/praise.rb
|
|
158
|
+
- lib/git/pkgs/commands/vulns/scan.rb
|
|
159
|
+
- lib/git/pkgs/commands/vulns/show.rb
|
|
160
|
+
- lib/git/pkgs/commands/vulns/sync.rb
|
|
104
161
|
- lib/git/pkgs/commands/where.rb
|
|
105
162
|
- lib/git/pkgs/commands/why.rb
|
|
106
163
|
- lib/git/pkgs/config.rb
|
|
107
164
|
- lib/git/pkgs/database.rb
|
|
165
|
+
- lib/git/pkgs/ecosystems.rb
|
|
108
166
|
- lib/git/pkgs/models/branch.rb
|
|
109
167
|
- lib/git/pkgs/models/branch_commit.rb
|
|
110
168
|
- lib/git/pkgs/models/commit.rb
|
|
111
169
|
- lib/git/pkgs/models/dependency_change.rb
|
|
112
170
|
- lib/git/pkgs/models/dependency_snapshot.rb
|
|
113
171
|
- lib/git/pkgs/models/manifest.rb
|
|
172
|
+
- lib/git/pkgs/models/package.rb
|
|
173
|
+
- lib/git/pkgs/models/vulnerability.rb
|
|
174
|
+
- lib/git/pkgs/models/vulnerability_package.rb
|
|
175
|
+
- lib/git/pkgs/osv_client.rb
|
|
114
176
|
- lib/git/pkgs/output.rb
|
|
115
177
|
- lib/git/pkgs/pager.rb
|
|
116
178
|
- lib/git/pkgs/repository.rb
|
|
@@ -137,7 +199,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
137
199
|
- !ruby/object:Gem::Version
|
|
138
200
|
version: '0'
|
|
139
201
|
requirements: []
|
|
140
|
-
rubygems_version: 4.0.
|
|
202
|
+
rubygems_version: 4.0.3
|
|
141
203
|
specification_version: 4
|
|
142
204
|
summary: Track package dependencies across git history
|
|
143
205
|
test_files: []
|