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,345 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Git
|
|
4
|
+
module Pkgs
|
|
5
|
+
module Commands
|
|
6
|
+
module Vulns
|
|
7
|
+
class History
|
|
8
|
+
include Base
|
|
9
|
+
|
|
10
|
+
def initialize(args)
|
|
11
|
+
@args = args.dup
|
|
12
|
+
@options = parse_options
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def parse_options
|
|
16
|
+
options = {}
|
|
17
|
+
|
|
18
|
+
parser = OptionParser.new do |opts|
|
|
19
|
+
opts.banner = "Usage: git pkgs vulns history <package|cve> [options]"
|
|
20
|
+
opts.separator ""
|
|
21
|
+
opts.separator "Show vulnerability timeline for a specific package or CVE."
|
|
22
|
+
opts.separator ""
|
|
23
|
+
opts.separator "Arguments:"
|
|
24
|
+
opts.separator " package|cve Package name or CVE/GHSA ID"
|
|
25
|
+
opts.separator ""
|
|
26
|
+
opts.separator "Examples:"
|
|
27
|
+
opts.separator " git pkgs vulns history lodash"
|
|
28
|
+
opts.separator " git pkgs vulns history CVE-2024-1234"
|
|
29
|
+
opts.separator " git pkgs vulns history GHSA-xxxx-yyyy"
|
|
30
|
+
opts.separator ""
|
|
31
|
+
opts.separator "Options:"
|
|
32
|
+
|
|
33
|
+
opts.on("-e", "--ecosystem=NAME", "Filter by ecosystem") do |v|
|
|
34
|
+
options[:ecosystem] = v
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
opts.on("--since=DATE", "Show events after date") do |v|
|
|
38
|
+
options[:since] = v
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
opts.on("--until=DATE", "Show events before date") do |v|
|
|
42
|
+
options[:until] = v
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
opts.on("-f", "--format=FORMAT", "Output format (text, json)") do |v|
|
|
46
|
+
options[:format] = v
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
opts.on("-h", "--help", "Show this help") do
|
|
50
|
+
puts opts
|
|
51
|
+
exit
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
parser.parse!(@args)
|
|
56
|
+
options[:target] = @args.shift
|
|
57
|
+
options
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def run
|
|
61
|
+
repo = Repository.new
|
|
62
|
+
|
|
63
|
+
unless Database.exists?(repo.git_dir)
|
|
64
|
+
error "No database found. Run 'git pkgs init' first. History requires commit history."
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
Database.connect(repo.git_dir)
|
|
68
|
+
|
|
69
|
+
target = @options[:target]
|
|
70
|
+
error "Usage: git pkgs vulns history <package|cve>" unless target
|
|
71
|
+
|
|
72
|
+
if target.match?(/^(CVE-|GHSA-)/i)
|
|
73
|
+
run_cve_history(target.upcase, repo)
|
|
74
|
+
else
|
|
75
|
+
run_package_history(target, repo)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def run_cve_history(cve_id, repo)
|
|
80
|
+
ensure_vulns_synced
|
|
81
|
+
|
|
82
|
+
vuln = Models::Vulnerability.first(id: cve_id)
|
|
83
|
+
unless vuln
|
|
84
|
+
error "Vulnerability #{cve_id} not found. Run 'git pkgs vulns sync' first."
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
vuln_pkgs = Models::VulnerabilityPackage.where(vulnerability_id: cve_id).all
|
|
88
|
+
|
|
89
|
+
if vuln_pkgs.empty?
|
|
90
|
+
puts "No affected packages found for #{cve_id}"
|
|
91
|
+
return
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
timeline = []
|
|
95
|
+
|
|
96
|
+
if vuln.published_at
|
|
97
|
+
timeline << {
|
|
98
|
+
date: vuln.published_at,
|
|
99
|
+
event_type: :cve_published,
|
|
100
|
+
description: "#{cve_id} published",
|
|
101
|
+
severity: vuln.severity
|
|
102
|
+
}
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
vuln_pkgs.each do |vp|
|
|
106
|
+
ecosystem = Ecosystems.from_osv(vp.ecosystem) || vp.ecosystem.downcase
|
|
107
|
+
changes = Models::DependencyChange
|
|
108
|
+
.join(:commits, id: :commit_id)
|
|
109
|
+
.where(ecosystem: ecosystem, name: vp.package_name)
|
|
110
|
+
.order(Sequel[:commits][:committed_at])
|
|
111
|
+
.eager(:commit)
|
|
112
|
+
.all
|
|
113
|
+
|
|
114
|
+
changes.each do |change|
|
|
115
|
+
current_affected = change.requirement && vp.affects_version?(change.requirement)
|
|
116
|
+
previous_affected = change.previous_requirement && vp.affects_version?(change.previous_requirement)
|
|
117
|
+
|
|
118
|
+
event = nil
|
|
119
|
+
case change.change_type
|
|
120
|
+
when "added"
|
|
121
|
+
if current_affected
|
|
122
|
+
event = {
|
|
123
|
+
date: change.commit.committed_at,
|
|
124
|
+
event_type: :vulnerable_added,
|
|
125
|
+
description: "#{vp.package_name} #{change.requirement} added (vulnerable)",
|
|
126
|
+
commit: format_commit_info(change.commit)
|
|
127
|
+
}
|
|
128
|
+
end
|
|
129
|
+
when "modified"
|
|
130
|
+
if current_affected && !previous_affected
|
|
131
|
+
event = {
|
|
132
|
+
date: change.commit.committed_at,
|
|
133
|
+
event_type: :became_vulnerable,
|
|
134
|
+
description: "#{vp.package_name} updated to #{change.requirement} (vulnerable)",
|
|
135
|
+
commit: format_commit_info(change.commit)
|
|
136
|
+
}
|
|
137
|
+
elsif !current_affected && previous_affected
|
|
138
|
+
event = {
|
|
139
|
+
date: change.commit.committed_at,
|
|
140
|
+
event_type: :fixed,
|
|
141
|
+
description: "#{vp.package_name} updated to #{change.requirement} (fixed)",
|
|
142
|
+
commit: format_commit_info(change.commit)
|
|
143
|
+
}
|
|
144
|
+
end
|
|
145
|
+
when "removed"
|
|
146
|
+
if previous_affected
|
|
147
|
+
event = {
|
|
148
|
+
date: change.commit.committed_at,
|
|
149
|
+
event_type: :removed,
|
|
150
|
+
description: "#{vp.package_name} removed",
|
|
151
|
+
commit: format_commit_info(change.commit)
|
|
152
|
+
}
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
timeline << event if event
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
timeline = filter_timeline_by_date(timeline)
|
|
161
|
+
timeline.sort_by! { |e| e[:date] }
|
|
162
|
+
|
|
163
|
+
if timeline.empty?
|
|
164
|
+
puts "No history found for #{cve_id}"
|
|
165
|
+
return
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
if @options[:format] == "json"
|
|
169
|
+
require "json"
|
|
170
|
+
puts JSON.pretty_generate({
|
|
171
|
+
cve: cve_id,
|
|
172
|
+
severity: vuln.severity,
|
|
173
|
+
summary: vuln.summary,
|
|
174
|
+
published_at: vuln.published_at&.strftime("%Y-%m-%d"),
|
|
175
|
+
timeline: timeline.map { |e| e.merge(date: e[:date].strftime("%Y-%m-%d")) }
|
|
176
|
+
})
|
|
177
|
+
else
|
|
178
|
+
output_cve_timeline(cve_id, vuln, timeline)
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def run_package_history(package_name, repo)
|
|
183
|
+
ensure_vulns_synced
|
|
184
|
+
|
|
185
|
+
ecosystem = @options[:ecosystem]
|
|
186
|
+
|
|
187
|
+
changes_query = Models::DependencyChange
|
|
188
|
+
.join(:commits, id: :commit_id)
|
|
189
|
+
.where(Sequel.ilike(:name, package_name))
|
|
190
|
+
.order(Sequel[:commits][:committed_at])
|
|
191
|
+
.eager(:commit, :manifest)
|
|
192
|
+
|
|
193
|
+
changes_query = changes_query.where(ecosystem: ecosystem) if ecosystem
|
|
194
|
+
|
|
195
|
+
changes = changes_query.all
|
|
196
|
+
|
|
197
|
+
if changes.empty?
|
|
198
|
+
puts "No history found for package '#{package_name}'"
|
|
199
|
+
return
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
osv_ecosystem = ecosystem ? Ecosystems.to_osv(ecosystem) : nil
|
|
203
|
+
vuln_query = Models::VulnerabilityPackage.where(Sequel.ilike(:package_name, package_name))
|
|
204
|
+
vuln_query = vuln_query.where(ecosystem: osv_ecosystem) if osv_ecosystem
|
|
205
|
+
|
|
206
|
+
vuln_pkgs = vuln_query.eager(:vulnerability).all
|
|
207
|
+
|
|
208
|
+
timeline = []
|
|
209
|
+
|
|
210
|
+
changes.each do |change|
|
|
211
|
+
affected_vulns = vuln_pkgs.select do |vp|
|
|
212
|
+
change.requirement && vp.affects_version?(change.requirement)
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
active_vulns = affected_vulns.reject { |vp| vp.vulnerability&.withdrawn? }
|
|
216
|
+
withdrawn_vulns = affected_vulns.select { |vp| vp.vulnerability&.withdrawn? }
|
|
217
|
+
|
|
218
|
+
vuln_parts = []
|
|
219
|
+
vuln_parts << "vulnerable to #{active_vulns.map(&:vulnerability_id).join(", ")}" if active_vulns.any?
|
|
220
|
+
vuln_parts << "#{withdrawn_vulns.map(&:vulnerability_id).join(", ")} withdrawn" if withdrawn_vulns.any?
|
|
221
|
+
vuln_info = vuln_parts.any? ? "(#{vuln_parts.join("; ")})" : ""
|
|
222
|
+
|
|
223
|
+
event = {
|
|
224
|
+
date: change.commit.committed_at,
|
|
225
|
+
event_type: change.change_type.to_sym,
|
|
226
|
+
description: "#{change.change_type.capitalize} #{package_name} #{change.requirement} #{vuln_info}".strip,
|
|
227
|
+
version: change.requirement,
|
|
228
|
+
commit: format_commit_info(change.commit),
|
|
229
|
+
affected_vulns: active_vulns.map(&:vulnerability_id),
|
|
230
|
+
withdrawn_vulns: withdrawn_vulns.map(&:vulnerability_id)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
timeline << event
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
vuln_pkgs.each do |vp|
|
|
237
|
+
vuln = vp.vulnerability
|
|
238
|
+
next unless vuln&.published_at
|
|
239
|
+
|
|
240
|
+
withdrawn_note = vuln.withdrawn? ? " [withdrawn]" : ""
|
|
241
|
+
timeline << {
|
|
242
|
+
date: vuln.published_at,
|
|
243
|
+
event_type: :cve_published,
|
|
244
|
+
description: "#{vp.vulnerability_id} published (#{vuln.severity || "unknown"} severity)#{withdrawn_note}"
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if vuln.withdrawn? && vuln.withdrawn_at
|
|
248
|
+
timeline << {
|
|
249
|
+
date: vuln.withdrawn_at,
|
|
250
|
+
event_type: :cve_withdrawn,
|
|
251
|
+
description: "#{vp.vulnerability_id} withdrawn"
|
|
252
|
+
}
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
timeline = filter_timeline_by_date(timeline)
|
|
257
|
+
timeline.sort_by! { |e| e[:date] }
|
|
258
|
+
|
|
259
|
+
if timeline.empty?
|
|
260
|
+
puts "No history found for package '#{package_name}'"
|
|
261
|
+
return
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
if @options[:format] == "json"
|
|
265
|
+
require "json"
|
|
266
|
+
puts JSON.pretty_generate({
|
|
267
|
+
package: package_name,
|
|
268
|
+
timeline: timeline.map { |e| e.merge(date: e[:date].strftime("%Y-%m-%d")) }
|
|
269
|
+
})
|
|
270
|
+
else
|
|
271
|
+
output_package_timeline(package_name, timeline)
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def filter_timeline_by_date(timeline)
|
|
276
|
+
if @options[:since]
|
|
277
|
+
since_time = parse_date(@options[:since])
|
|
278
|
+
timeline = timeline.select { |e| e[:date] >= since_time } if since_time
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
if @options[:until]
|
|
282
|
+
until_time = parse_date(@options[:until])
|
|
283
|
+
timeline = timeline.select { |e| e[:date] <= until_time } if until_time
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
timeline
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def output_cve_timeline(cve_id, vuln, timeline)
|
|
290
|
+
puts "#{cve_id} (#{vuln.severity || "unknown"} severity)"
|
|
291
|
+
puts vuln.summary if vuln.summary
|
|
292
|
+
puts ""
|
|
293
|
+
|
|
294
|
+
timeline.each do |event|
|
|
295
|
+
date = event[:date].strftime("%Y-%m-%d")
|
|
296
|
+
desc = event[:description]
|
|
297
|
+
|
|
298
|
+
line = if event[:commit]
|
|
299
|
+
"#{date} #{desc} #{event[:commit][:sha]} #{event[:commit][:author]}"
|
|
300
|
+
else
|
|
301
|
+
"#{date} #{desc}"
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
colored_line = case event[:event_type]
|
|
305
|
+
when :cve_published then Color.yellow(line)
|
|
306
|
+
when :vulnerable_added, :became_vulnerable then Color.red(line)
|
|
307
|
+
when :fixed, :removed then Color.green(line)
|
|
308
|
+
else line
|
|
309
|
+
end
|
|
310
|
+
puts colored_line
|
|
311
|
+
end
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
def output_package_timeline(package_name, timeline)
|
|
315
|
+
puts "History for #{package_name}"
|
|
316
|
+
puts ""
|
|
317
|
+
|
|
318
|
+
timeline.each do |event|
|
|
319
|
+
date = event[:date].strftime("%Y-%m-%d")
|
|
320
|
+
desc = event[:description]
|
|
321
|
+
|
|
322
|
+
line = if event[:commit]
|
|
323
|
+
"#{date} #{desc} #{event[:commit][:sha]} #{event[:commit][:author]}"
|
|
324
|
+
else
|
|
325
|
+
"#{date} #{desc}"
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
colored_line = case event[:event_type]
|
|
329
|
+
when :cve_published then Color.yellow(line)
|
|
330
|
+
when :cve_withdrawn then Color.cyan(line)
|
|
331
|
+
when :added
|
|
332
|
+
event[:affected_vulns]&.any? ? Color.red(line) : line
|
|
333
|
+
when :modified
|
|
334
|
+
event[:affected_vulns]&.any? ? Color.red(line) : Color.green(line)
|
|
335
|
+
when :removed then Color.cyan(line)
|
|
336
|
+
else line
|
|
337
|
+
end
|
|
338
|
+
puts colored_line
|
|
339
|
+
end
|
|
340
|
+
end
|
|
341
|
+
end
|
|
342
|
+
end
|
|
343
|
+
end
|
|
344
|
+
end
|
|
345
|
+
end
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Git
|
|
4
|
+
module Pkgs
|
|
5
|
+
module Commands
|
|
6
|
+
module Vulns
|
|
7
|
+
class Log
|
|
8
|
+
include Base
|
|
9
|
+
|
|
10
|
+
def initialize(args)
|
|
11
|
+
@args = args.dup
|
|
12
|
+
@options = parse_options
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def parse_options
|
|
16
|
+
options = {}
|
|
17
|
+
|
|
18
|
+
parser = OptionParser.new do |opts|
|
|
19
|
+
opts.banner = "Usage: git pkgs vulns log [options]"
|
|
20
|
+
opts.separator ""
|
|
21
|
+
opts.separator "Show commits that introduced or fixed vulnerabilities."
|
|
22
|
+
opts.separator ""
|
|
23
|
+
opts.separator "Options:"
|
|
24
|
+
|
|
25
|
+
opts.on("-e", "--ecosystem=NAME", "Filter by ecosystem") do |v|
|
|
26
|
+
options[:ecosystem] = v
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
opts.on("-s", "--severity=LEVEL", "Minimum severity (critical, high, medium, low)") do |v|
|
|
30
|
+
options[:severity] = v
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
opts.on("-b", "--branch=NAME", "Branch to analyze") do |v|
|
|
34
|
+
options[:branch] = v
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
opts.on("--since=DATE", "Show commits after date") do |v|
|
|
38
|
+
options[:since] = v
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
opts.on("--until=DATE", "Show commits before date") do |v|
|
|
42
|
+
options[:until] = v
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
opts.on("--author=NAME", "Filter by author") do |v|
|
|
46
|
+
options[:author] = v
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
opts.on("--introduced", "Show only commits that introduced vulnerabilities") do
|
|
50
|
+
options[:introduced] = true
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
opts.on("--fixed", "Show only commits that fixed vulnerabilities") do
|
|
54
|
+
options[:fixed] = true
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
opts.on("-f", "--format=FORMAT", "Output format (text, json)") do |v|
|
|
58
|
+
options[:format] = v
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
opts.on("-h", "--help", "Show this help") do
|
|
62
|
+
puts opts
|
|
63
|
+
exit
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
parser.parse!(@args)
|
|
68
|
+
options
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def run
|
|
72
|
+
repo = Repository.new
|
|
73
|
+
|
|
74
|
+
unless Database.exists?(repo.git_dir)
|
|
75
|
+
error "No database found. Run 'git pkgs init' first. Log requires commit history."
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
Database.connect(repo.git_dir)
|
|
79
|
+
|
|
80
|
+
commits_with_vulns = find_commits_with_vuln_changes(repo)
|
|
81
|
+
|
|
82
|
+
if commits_with_vulns.empty?
|
|
83
|
+
puts "No commits with vulnerability changes found"
|
|
84
|
+
return
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
if @options[:format] == "json"
|
|
88
|
+
require "json"
|
|
89
|
+
puts JSON.pretty_generate(commits_with_vulns)
|
|
90
|
+
else
|
|
91
|
+
output_vuln_log(commits_with_vulns)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def find_commits_with_vuln_changes(repo)
|
|
96
|
+
branch_name = @options[:branch] || repo.default_branch
|
|
97
|
+
branch = Models::Branch.first(name: branch_name)
|
|
98
|
+
return [] unless branch
|
|
99
|
+
|
|
100
|
+
commits_query = Models::Commit
|
|
101
|
+
.join(:branch_commits, commit_id: :id)
|
|
102
|
+
.where(Sequel[:branch_commits][:branch_id] => branch.id)
|
|
103
|
+
.where(has_dependency_changes: true)
|
|
104
|
+
.order(Sequel.desc(Sequel[:commits][:committed_at]))
|
|
105
|
+
|
|
106
|
+
if @options[:since]
|
|
107
|
+
since_time = parse_date(@options[:since])
|
|
108
|
+
commits_query = commits_query.where { Sequel[:commits][:committed_at] >= since_time }
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
if @options[:until]
|
|
112
|
+
until_time = parse_date(@options[:until])
|
|
113
|
+
commits_query = commits_query.where { Sequel[:commits][:committed_at] <= until_time }
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
if @options[:author]
|
|
117
|
+
commits_query = commits_query.where(Sequel.ilike(:author_name, "%#{@options[:author]}%"))
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
commits = commits_query.all
|
|
121
|
+
results = []
|
|
122
|
+
|
|
123
|
+
ensure_vulns_synced
|
|
124
|
+
|
|
125
|
+
commits.each do |commit|
|
|
126
|
+
changes = commit.dependency_changes.to_a
|
|
127
|
+
vuln_changes = []
|
|
128
|
+
|
|
129
|
+
changes.each do |change|
|
|
130
|
+
next unless Ecosystems.supported?(change.ecosystem)
|
|
131
|
+
|
|
132
|
+
osv_ecosystem = Ecosystems.to_osv(change.ecosystem)
|
|
133
|
+
next unless osv_ecosystem
|
|
134
|
+
|
|
135
|
+
vuln_pkgs = Models::VulnerabilityPackage
|
|
136
|
+
.where(ecosystem: osv_ecosystem, package_name: change.name)
|
|
137
|
+
.eager(:vulnerability)
|
|
138
|
+
.all
|
|
139
|
+
|
|
140
|
+
vuln_pkgs.each do |vp|
|
|
141
|
+
next if vp.vulnerability&.withdrawn?
|
|
142
|
+
|
|
143
|
+
current_affected = change.requirement && vp.affects_version?(change.requirement)
|
|
144
|
+
previous_affected = change.previous_requirement && vp.affects_version?(change.previous_requirement)
|
|
145
|
+
|
|
146
|
+
case change.change_type
|
|
147
|
+
when "added"
|
|
148
|
+
if current_affected
|
|
149
|
+
vuln_changes << { type: :introduced, vuln_id: vp.vulnerability_id, severity: vp.vulnerability&.severity }
|
|
150
|
+
end
|
|
151
|
+
when "modified"
|
|
152
|
+
if current_affected && !previous_affected
|
|
153
|
+
vuln_changes << { type: :introduced, vuln_id: vp.vulnerability_id, severity: vp.vulnerability&.severity }
|
|
154
|
+
elsif !current_affected && previous_affected
|
|
155
|
+
vuln_changes << { type: :fixed, vuln_id: vp.vulnerability_id, severity: vp.vulnerability&.severity }
|
|
156
|
+
end
|
|
157
|
+
when "removed"
|
|
158
|
+
if previous_affected
|
|
159
|
+
vuln_changes << { type: :fixed, vuln_id: vp.vulnerability_id, severity: vp.vulnerability&.severity }
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
next if vuln_changes.empty?
|
|
166
|
+
|
|
167
|
+
if @options[:introduced]
|
|
168
|
+
vuln_changes = vuln_changes.select { |vc| vc[:type] == :introduced }
|
|
169
|
+
elsif @options[:fixed]
|
|
170
|
+
vuln_changes = vuln_changes.select { |vc| vc[:type] == :fixed }
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
next if vuln_changes.empty?
|
|
174
|
+
|
|
175
|
+
results << {
|
|
176
|
+
sha: commit.sha[0, 7],
|
|
177
|
+
full_sha: commit.sha,
|
|
178
|
+
date: commit.committed_at&.strftime("%Y-%m-%d"),
|
|
179
|
+
author: commit.author_name,
|
|
180
|
+
message: commit.message&.lines&.first&.strip&.slice(0, 40),
|
|
181
|
+
vuln_changes: vuln_changes
|
|
182
|
+
}
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
results
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def output_vuln_log(results)
|
|
189
|
+
results.each do |result|
|
|
190
|
+
sha = result[:sha]
|
|
191
|
+
date = result[:date]
|
|
192
|
+
author = result[:author]
|
|
193
|
+
message = result[:message]
|
|
194
|
+
|
|
195
|
+
vuln_summary = result[:vuln_changes].map do |vc|
|
|
196
|
+
prefix = vc[:type] == :introduced ? "+" : "-"
|
|
197
|
+
"#{prefix}#{vc[:vuln_id]}"
|
|
198
|
+
end.join(" ")
|
|
199
|
+
|
|
200
|
+
introduced_count = result[:vuln_changes].count { |vc| vc[:type] == :introduced }
|
|
201
|
+
fixed_count = result[:vuln_changes].count { |vc| vc[:type] == :fixed }
|
|
202
|
+
|
|
203
|
+
line = "#{sha} #{date} #{author.to_s.ljust(15)[0, 15]} \"#{message}\" #{vuln_summary}"
|
|
204
|
+
colored_line = if introduced_count > fixed_count
|
|
205
|
+
Color.red(line)
|
|
206
|
+
elsif fixed_count > introduced_count
|
|
207
|
+
Color.green(line)
|
|
208
|
+
else
|
|
209
|
+
line
|
|
210
|
+
end
|
|
211
|
+
puts colored_line
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
end
|