git-pkgs 0.6.1 → 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 +23 -0
- data/Dockerfile +18 -0
- data/Formula/git-pkgs.rb +28 -0
- data/README.md +69 -5
- data/lib/git/pkgs/analyzer.rb +140 -8
- data/lib/git/pkgs/cli.rb +16 -6
- data/lib/git/pkgs/commands/blame.rb +0 -18
- data/lib/git/pkgs/commands/diff.rb +181 -7
- data/lib/git/pkgs/commands/diff_driver.rb +25 -5
- data/lib/git/pkgs/commands/init.rb +5 -0
- data/lib/git/pkgs/commands/list.rb +68 -15
- data/lib/git/pkgs/commands/show.rb +126 -3
- data/lib/git/pkgs/commands/stale.rb +38 -4
- data/lib/git/pkgs/commands/tree.rb +44 -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/commands/why.rb +40 -1
- data/lib/git/pkgs/config.rb +10 -2
- 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 +77 -0
- metadata +66 -4
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Git
|
|
4
|
+
module Pkgs
|
|
5
|
+
module Commands
|
|
6
|
+
module Vulns
|
|
7
|
+
class Blame
|
|
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 blame [ref] [options]"
|
|
20
|
+
opts.separator ""
|
|
21
|
+
opts.separator "Show who introduced each vulnerability."
|
|
22
|
+
opts.separator ""
|
|
23
|
+
opts.separator "Arguments:"
|
|
24
|
+
opts.separator " ref Git ref to analyze (default: HEAD)"
|
|
25
|
+
opts.separator ""
|
|
26
|
+
opts.separator "Options:"
|
|
27
|
+
|
|
28
|
+
opts.on("-e", "--ecosystem=NAME", "Filter by ecosystem") do |v|
|
|
29
|
+
options[:ecosystem] = v
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
opts.on("-s", "--severity=LEVEL", "Minimum severity (critical, high, medium, low)") do |v|
|
|
33
|
+
options[:severity] = v
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
opts.on("-r", "--ref=REF", "Git ref to analyze (default: HEAD)") do |v|
|
|
37
|
+
options[:ref] = v
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
opts.on("-b", "--branch=NAME", "Branch context for finding commits") do |v|
|
|
41
|
+
options[:branch] = v
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
opts.on("-f", "--format=FORMAT", "Output format (text, json)") do |v|
|
|
45
|
+
options[:format] = v
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
opts.on("--all-time", "Show blame for all historical vulnerabilities") do
|
|
49
|
+
options[:all_time] = true
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
opts.on("-h", "--help", "Show this help") do
|
|
53
|
+
puts opts
|
|
54
|
+
exit
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
parser.parse!(@args)
|
|
59
|
+
options[:ref] ||= @args.shift unless @args.empty?
|
|
60
|
+
options
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def run
|
|
64
|
+
repo = Repository.new
|
|
65
|
+
|
|
66
|
+
unless Database.exists?(repo.git_dir)
|
|
67
|
+
error "No database found. Run 'git pkgs init' first. Blame requires commit history."
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
Database.connect(repo.git_dir)
|
|
71
|
+
|
|
72
|
+
if @options[:all_time]
|
|
73
|
+
run_all_time(repo)
|
|
74
|
+
else
|
|
75
|
+
run_at_ref(repo)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def run_at_ref(repo)
|
|
80
|
+
ref = @options[:ref] || "HEAD"
|
|
81
|
+
commit_sha = repo.rev_parse(ref)
|
|
82
|
+
target_commit = Models::Commit.first(sha: commit_sha)
|
|
83
|
+
|
|
84
|
+
unless target_commit
|
|
85
|
+
error "Commit #{commit_sha[0, 7]} not in database. Run 'git pkgs update' first."
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
deps = compute_dependencies_at_commit(target_commit, repo)
|
|
89
|
+
|
|
90
|
+
if deps.empty?
|
|
91
|
+
empty_result "No dependencies found"
|
|
92
|
+
return
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
supported_deps = deps.select { |d| Ecosystems.supported?(d[:ecosystem]) }
|
|
96
|
+
vulns = scan_for_vulnerabilities(supported_deps)
|
|
97
|
+
|
|
98
|
+
if @options[:severity]
|
|
99
|
+
min_level = SEVERITY_ORDER[@options[:severity].downcase] || 4
|
|
100
|
+
vulns = vulns.select { |v| (SEVERITY_ORDER[v[:severity]&.downcase] || 4) <= min_level }
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
if vulns.empty?
|
|
104
|
+
puts "No known vulnerabilities found"
|
|
105
|
+
return
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
blame_results = vulns.map do |vuln|
|
|
109
|
+
introducing = find_introducing_commit(
|
|
110
|
+
vuln[:ecosystem],
|
|
111
|
+
vuln[:package_name],
|
|
112
|
+
vuln[:id],
|
|
113
|
+
target_commit
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
vuln.merge(introducing_commit: introducing)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
output_results(blame_results)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def run_all_time(repo)
|
|
123
|
+
branch_name = @options[:branch] || repo.default_branch
|
|
124
|
+
branch = Models::Branch.first(name: branch_name)
|
|
125
|
+
|
|
126
|
+
unless branch&.last_analyzed_sha
|
|
127
|
+
error "No analysis found for branch '#{branch_name}'. Run 'git pkgs init' first."
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Get all unique packages from dependency changes
|
|
131
|
+
packages = Models::DependencyChange
|
|
132
|
+
.select(:ecosystem, :name)
|
|
133
|
+
.select_group(:ecosystem, :name)
|
|
134
|
+
.all
|
|
135
|
+
|
|
136
|
+
blame_results = []
|
|
137
|
+
|
|
138
|
+
packages.each do |pkg|
|
|
139
|
+
next unless Ecosystems.supported?(pkg.ecosystem)
|
|
140
|
+
|
|
141
|
+
osv_ecosystem = Ecosystems.to_osv(pkg.ecosystem)
|
|
142
|
+
next unless osv_ecosystem
|
|
143
|
+
|
|
144
|
+
vuln_pkgs = Models::VulnerabilityPackage
|
|
145
|
+
.for_package(osv_ecosystem, pkg.name)
|
|
146
|
+
.eager(:vulnerability)
|
|
147
|
+
.all
|
|
148
|
+
|
|
149
|
+
vuln_pkgs.each do |vp|
|
|
150
|
+
next if vp.vulnerability&.withdrawn?
|
|
151
|
+
|
|
152
|
+
introducing = find_historical_introducing_commit(pkg.ecosystem, pkg.name, vp)
|
|
153
|
+
next unless introducing
|
|
154
|
+
|
|
155
|
+
severity = vp.vulnerability&.severity
|
|
156
|
+
|
|
157
|
+
if @options[:severity]
|
|
158
|
+
min_level = SEVERITY_ORDER[@options[:severity].downcase] || 4
|
|
159
|
+
next unless (SEVERITY_ORDER[severity&.downcase] || 4) <= min_level
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
blame_results << {
|
|
163
|
+
id: vp.vulnerability_id,
|
|
164
|
+
severity: severity,
|
|
165
|
+
package_name: pkg.name,
|
|
166
|
+
package_version: introducing[:version],
|
|
167
|
+
summary: vp.vulnerability&.summary,
|
|
168
|
+
introducing_commit: introducing[:commit_info],
|
|
169
|
+
status: introducing[:status]
|
|
170
|
+
}
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
if blame_results.empty?
|
|
175
|
+
puts "No historical vulnerabilities found"
|
|
176
|
+
return
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
output_results(blame_results)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def find_historical_introducing_commit(ecosystem, package_name, vuln_pkg)
|
|
183
|
+
window = find_vulnerability_window(ecosystem, package_name, vuln_pkg)
|
|
184
|
+
return nil unless window
|
|
185
|
+
|
|
186
|
+
{
|
|
187
|
+
commit_info: format_commit_info(window[:introducing].commit),
|
|
188
|
+
version: window[:introducing].requirement,
|
|
189
|
+
status: window[:status]
|
|
190
|
+
}
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def output_results(blame_results)
|
|
194
|
+
blame_results.sort_by! do |v|
|
|
195
|
+
[SEVERITY_ORDER[v[:severity]&.downcase] || 4, v[:package_name]]
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
if @options[:format] == "json"
|
|
199
|
+
require "json"
|
|
200
|
+
puts JSON.pretty_generate(blame_results)
|
|
201
|
+
else
|
|
202
|
+
output_blame_text(blame_results)
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def find_introducing_commit(ecosystem, package_name, vuln_id, up_to_commit)
|
|
207
|
+
osv_ecosystem = Ecosystems.to_osv(ecosystem)
|
|
208
|
+
vuln_pkg = Models::VulnerabilityPackage.first(
|
|
209
|
+
vulnerability_id: vuln_id,
|
|
210
|
+
ecosystem: osv_ecosystem,
|
|
211
|
+
package_name: package_name
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
return nil unless vuln_pkg
|
|
215
|
+
|
|
216
|
+
changes = Models::DependencyChange
|
|
217
|
+
.join(:commits, id: :commit_id)
|
|
218
|
+
.where(ecosystem: ecosystem, name: package_name)
|
|
219
|
+
.where(change_type: %w[added modified])
|
|
220
|
+
.where { Sequel[:commits][:committed_at] <= up_to_commit.committed_at }
|
|
221
|
+
.order(Sequel.desc(Sequel[:commits][:committed_at]))
|
|
222
|
+
.eager(:commit)
|
|
223
|
+
.all
|
|
224
|
+
|
|
225
|
+
changes.each do |change|
|
|
226
|
+
next unless vuln_pkg.affects_version?(change.requirement)
|
|
227
|
+
return format_commit_info(change.commit)
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
first_add = Models::DependencyChange
|
|
231
|
+
.join(:commits, id: :commit_id)
|
|
232
|
+
.where(ecosystem: ecosystem, name: package_name)
|
|
233
|
+
.where(change_type: "added")
|
|
234
|
+
.order(Sequel[:commits][:committed_at])
|
|
235
|
+
.eager(:commit)
|
|
236
|
+
.first
|
|
237
|
+
|
|
238
|
+
return format_commit_info(first_add.commit) if first_add && vuln_pkg.affects_version?(first_add.requirement)
|
|
239
|
+
|
|
240
|
+
nil
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def output_blame_text(results)
|
|
244
|
+
has_status = results.any? { |r| r[:status] }
|
|
245
|
+
max_severity = results.map { |v| (v[:severity] || "").length }.max || 8
|
|
246
|
+
max_id = results.map { |v| v[:id].length }.max || 15
|
|
247
|
+
max_pkg = results.map { |v| "#{v[:package_name]} #{v[:package_version]}".length }.max || 20
|
|
248
|
+
|
|
249
|
+
results.each do |result|
|
|
250
|
+
severity = (result[:severity] || "unknown").upcase.ljust(max_severity)
|
|
251
|
+
id = result[:id].ljust(max_id)
|
|
252
|
+
pkg = "#{result[:package_name]} #{result[:package_version]}".ljust(max_pkg)
|
|
253
|
+
|
|
254
|
+
intro = result[:introducing_commit]
|
|
255
|
+
commit_info = if intro
|
|
256
|
+
"#{intro[:sha]} #{intro[:date]} #{intro[:author]} \"#{intro[:message]}\""
|
|
257
|
+
else
|
|
258
|
+
"(unknown origin)"
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
status_str = has_status ? " [#{result[:status]}]" : ""
|
|
262
|
+
line = "#{severity} #{id} #{pkg} #{commit_info}#{status_str}"
|
|
263
|
+
colored_line = case result[:severity]&.downcase
|
|
264
|
+
when "critical", "high" then Color.red(line)
|
|
265
|
+
when "medium" then Color.yellow(line)
|
|
266
|
+
when "low" then Color.cyan(line)
|
|
267
|
+
else line
|
|
268
|
+
end
|
|
269
|
+
puts colored_line
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
end
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Git
|
|
4
|
+
module Pkgs
|
|
5
|
+
module Commands
|
|
6
|
+
module Vulns
|
|
7
|
+
class Diff
|
|
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 diff [ref1] [ref2] [options]"
|
|
20
|
+
opts.separator ""
|
|
21
|
+
opts.separator "Compare vulnerability state between two commits."
|
|
22
|
+
opts.separator ""
|
|
23
|
+
opts.separator "Arguments:"
|
|
24
|
+
opts.separator " ref1 First git ref (default: HEAD~1)"
|
|
25
|
+
opts.separator " ref2 Second git ref (default: HEAD)"
|
|
26
|
+
opts.separator ""
|
|
27
|
+
opts.separator "Examples:"
|
|
28
|
+
opts.separator " git pkgs vulns diff main feature-branch"
|
|
29
|
+
opts.separator " git pkgs vulns diff v1.0.0 v2.0.0"
|
|
30
|
+
opts.separator " git pkgs vulns diff HEAD~10"
|
|
31
|
+
opts.separator ""
|
|
32
|
+
opts.separator "Options:"
|
|
33
|
+
|
|
34
|
+
opts.on("-e", "--ecosystem=NAME", "Filter by ecosystem") do |v|
|
|
35
|
+
options[:ecosystem] = v
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
opts.on("-s", "--severity=LEVEL", "Minimum severity (critical, high, medium, low)") do |v|
|
|
39
|
+
options[:severity] = v
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
opts.on("-b", "--branch=NAME", "Branch context for finding commits") do |v|
|
|
43
|
+
options[:branch] = v
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
opts.on("-f", "--format=FORMAT", "Output format (text, json)") do |v|
|
|
47
|
+
options[:format] = v
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
opts.on("-h", "--help", "Show this help") do
|
|
51
|
+
puts opts
|
|
52
|
+
exit
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
parser.parse!(@args)
|
|
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. Diff requires commit history."
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
Database.connect(repo.git_dir)
|
|
68
|
+
|
|
69
|
+
ref1, ref2 = parse_diff_refs(repo)
|
|
70
|
+
commit1_sha = repo.rev_parse(ref1)
|
|
71
|
+
commit2_sha = repo.rev_parse(ref2)
|
|
72
|
+
|
|
73
|
+
commit1 = Models::Commit.first(sha: commit1_sha)
|
|
74
|
+
commit2 = Models::Commit.first(sha: commit2_sha)
|
|
75
|
+
|
|
76
|
+
error "Commit #{commit1_sha[0, 7]} not in database. Run 'git pkgs update' first." unless commit1
|
|
77
|
+
error "Commit #{commit2_sha[0, 7]} not in database. Run 'git pkgs update' first." unless commit2
|
|
78
|
+
|
|
79
|
+
deps1 = compute_dependencies_at_commit(commit1, repo)
|
|
80
|
+
deps2 = compute_dependencies_at_commit(commit2, repo)
|
|
81
|
+
|
|
82
|
+
supported_deps1 = deps1.select { |d| Ecosystems.supported?(d[:ecosystem]) }
|
|
83
|
+
supported_deps2 = deps2.select { |d| Ecosystems.supported?(d[:ecosystem]) }
|
|
84
|
+
|
|
85
|
+
vulns1 = scan_for_vulnerabilities(supported_deps1)
|
|
86
|
+
vulns2 = scan_for_vulnerabilities(supported_deps2)
|
|
87
|
+
|
|
88
|
+
if @options[:severity]
|
|
89
|
+
min_level = SEVERITY_ORDER[@options[:severity].downcase] || 4
|
|
90
|
+
vulns1 = vulns1.select { |v| (SEVERITY_ORDER[v[:severity]&.downcase] || 4) <= min_level }
|
|
91
|
+
vulns2 = vulns2.select { |v| (SEVERITY_ORDER[v[:severity]&.downcase] || 4) <= min_level }
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
vulns1_ids = vulns1.map { |v| v[:id] }.to_set
|
|
95
|
+
vulns2_ids = vulns2.map { |v| v[:id] }.to_set
|
|
96
|
+
|
|
97
|
+
added = vulns2.reject { |v| vulns1_ids.include?(v[:id]) }
|
|
98
|
+
removed = vulns1.reject { |v| vulns2_ids.include?(v[:id]) }
|
|
99
|
+
|
|
100
|
+
if added.empty? && removed.empty?
|
|
101
|
+
puts "No vulnerability changes between #{ref1} and #{ref2}"
|
|
102
|
+
return
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
if @options[:format] == "json"
|
|
106
|
+
require "json"
|
|
107
|
+
puts JSON.pretty_generate({
|
|
108
|
+
from: ref1,
|
|
109
|
+
to: ref2,
|
|
110
|
+
added: added,
|
|
111
|
+
removed: removed
|
|
112
|
+
})
|
|
113
|
+
else
|
|
114
|
+
output_diff_text(added, removed, ref1, ref2)
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def parse_diff_refs(repo)
|
|
119
|
+
args = @args.dup
|
|
120
|
+
ref1 = args.shift
|
|
121
|
+
ref2 = args.shift
|
|
122
|
+
|
|
123
|
+
if ref1.nil?
|
|
124
|
+
ref1 = "HEAD~1"
|
|
125
|
+
ref2 = "HEAD"
|
|
126
|
+
elsif ref2.nil?
|
|
127
|
+
ref2 = ref1
|
|
128
|
+
ref1 = "HEAD"
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
if ref1.include?("...")
|
|
132
|
+
parts = ref1.split("...")
|
|
133
|
+
ref1 = parts[0]
|
|
134
|
+
ref2 = parts[1]
|
|
135
|
+
elsif ref1.include?("..")
|
|
136
|
+
parts = ref1.split("..")
|
|
137
|
+
ref1 = parts[0]
|
|
138
|
+
ref2 = parts[1]
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
[ref1, ref2]
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def output_diff_text(added, removed, ref1, ref2)
|
|
145
|
+
all_vulns = added.map { |v| v.merge(diff_type: :added) } +
|
|
146
|
+
removed.map { |v| v.merge(diff_type: :removed) }
|
|
147
|
+
|
|
148
|
+
all_vulns.sort_by! do |v|
|
|
149
|
+
[SEVERITY_ORDER[v[:severity]&.downcase] || 4, v[:package_name]]
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
max_severity = all_vulns.map { |v| (v[:severity] || "").length }.max || 8
|
|
153
|
+
max_id = all_vulns.map { |v| v[:id].length }.max || 15
|
|
154
|
+
max_pkg = all_vulns.map { |v| "#{v[:package_name]} #{v[:package_version]}".length }.max || 20
|
|
155
|
+
|
|
156
|
+
all_vulns.each do |vuln|
|
|
157
|
+
prefix = vuln[:diff_type] == :added ? "+" : "-"
|
|
158
|
+
severity = (vuln[:severity] || "unknown").upcase.ljust(max_severity)
|
|
159
|
+
id = vuln[:id].ljust(max_id)
|
|
160
|
+
pkg = "#{vuln[:package_name]} #{vuln[:package_version]}".ljust(max_pkg)
|
|
161
|
+
note = vuln[:diff_type] == :added ? "(introduced in #{ref2})" : "(fixed in #{ref2})"
|
|
162
|
+
|
|
163
|
+
color = vuln[:diff_type] == :added ? :red : :green
|
|
164
|
+
line = "#{prefix}#{severity} #{id} #{pkg} #{note}"
|
|
165
|
+
puts Color.send(color, line)
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|