myprecious 0.0.5 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/bin/myprecious +5 -1
- data/lib/myprecious.rb +619 -95
- data/lib/myprecious/cves.rb +239 -0
- data/lib/myprecious/data_caches.rb +71 -0
- data/lib/myprecious/python_packages.rb +1190 -0
- data/lib/myprecious/ruby_gems.rb +291 -0
- metadata +141 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: d1d5adc30e0ccc2fb1c7a183402c7ac0015a9fbdde70e18267ad61d21bf4d790
|
4
|
+
data.tar.gz: '07394d3a1ff9a238220b9ae4d2be7328a9878ee08c47ec7179e9564af53e9e60'
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6f152193febe7482d2e9aeb1f3435b56c84f127a897cd0e3c3f8e35c5027a93d955bcb04b09c1e16f6d3a691bb92e85ff15047e31ba0b956daa5664ff6430287
|
7
|
+
data.tar.gz: ba2ea748b6a1c5f566f28375776639cb774449c885b11db306d02f709b7d6c86d7727acad771fd9eedaab610fa5be93914aa30b30607ada1d66691a9362c1f20
|
data/bin/myprecious
CHANGED
data/lib/myprecious.rb
CHANGED
@@ -1,111 +1,635 @@
|
|
1
|
-
require 'gems'
|
2
1
|
require 'date'
|
2
|
+
require 'git'
|
3
|
+
require 'ostruct'
|
4
|
+
require 'pathname'
|
5
|
+
require 'rake/toolkit_program'
|
6
|
+
require 'uri'
|
3
7
|
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
8
|
+
# Declare the module here so it doesn't cause problems for files in the
|
9
|
+
# "myprecious" directory (or _does_ cause problems if they try to declare
|
10
|
+
# it a class)
|
11
|
+
module MyPrecious
|
12
|
+
DATA_DIR = Pathname('~/.local/lib/myprecious').expand_path
|
13
|
+
ONE_DAY = 60 * 60 * 24
|
14
|
+
end
|
15
|
+
require 'myprecious/data_caches'
|
16
|
+
require 'myprecious/cves'
|
17
|
+
|
18
|
+
module MyPrecious
|
19
|
+
extend Rake::DSL
|
20
|
+
|
21
|
+
Program = Rake::ToolkitProgram
|
22
|
+
Program.title = "myprecious Dependecy Reporting Tool"
|
23
|
+
|
24
|
+
def self.tracing_errors?
|
25
|
+
!ENV['TRACE_ERRORS'].to_s.empty?
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.common_program_args(parser, args)
|
29
|
+
parser.on(
|
30
|
+
'-o', '--out FILE',
|
31
|
+
"Output file to generate",
|
32
|
+
) {|fpath| args.output_file = Pathname(fpath)}
|
33
|
+
|
34
|
+
args.target = Pathname.pwd
|
35
|
+
parser.on(
|
36
|
+
'-C', '--dir PATH',
|
37
|
+
"Path to inspect",
|
38
|
+
) do |fpath|
|
39
|
+
fpath = Pathname(fpath)
|
40
|
+
parser.invalid_args!("#{fpath} does not exist.") unless fpath.exist?
|
41
|
+
args.target = fpath
|
42
|
+
CVEs.config_dir = fpath
|
43
|
+
end
|
44
|
+
|
45
|
+
parser.on(
|
46
|
+
'--[no-]cache',
|
47
|
+
"Control caching of dependency information"
|
48
|
+
) {|v| MyPrecious.caching_disabled = v}
|
49
|
+
end
|
50
|
+
|
51
|
+
# Declare the tasks exposed as subcommands in this block
|
52
|
+
Program.command_tasks do
|
53
|
+
desc "Generate report on Ruby gems"
|
54
|
+
task('ruby-gems').parse_args(into: OpenStruct.new) do |parser, args|
|
55
|
+
parser.expect_positional_cardinality(0)
|
56
|
+
common_program_args(parser, args)
|
57
|
+
end
|
58
|
+
task 'ruby-gems' do
|
59
|
+
require 'myprecious/ruby_gems'
|
60
|
+
args = Program.args
|
61
|
+
out_fpath = args.output_file || Reporting.default_output_fpath(args.target)
|
62
|
+
|
63
|
+
col_order = Reporting.read_column_order_from(out_fpath)
|
64
|
+
|
65
|
+
# Get all gems used via RubyGemInfo.each_gem_used, accumulating version requirements
|
66
|
+
gems = RubyGemInfo.accum_gem_lock_info(args.target)
|
67
|
+
|
68
|
+
out_fpath.open('w') do |outf|
|
69
|
+
# Header
|
70
|
+
outf.puts "Last updated: #{Time.now.rfc2822}; Use for directional purposes only, this data is not real time and might be slightly inaccurate"
|
71
|
+
outf.puts
|
72
|
+
|
73
|
+
Reporting.header_lines(
|
74
|
+
col_order,
|
75
|
+
RubyGemInfo.method(:col_title)
|
76
|
+
).each {|l| outf.puts l}
|
77
|
+
|
78
|
+
# Iterate all gems in name order, pulling column values from the RubyGemInfo objects
|
79
|
+
gems.keys.sort_by {|n| n.downcase}.map {|name| gems[name]}.each do |gem|
|
80
|
+
Reporting.on_dependency(gem.name) do
|
81
|
+
outf.puts col_order.markdown_columns(MarkdownAdapter.new(gem))
|
37
82
|
end
|
38
|
-
elsif line_number > 1
|
39
|
-
gem_name_index = word_array[gem_name_pos].strip
|
40
|
-
#extract just the name of the gem from the first column
|
41
|
-
#since that column contains a markdown-formatted hyperlink
|
42
|
-
gem_name_index = gem_name_index[/\[(.*?)\]/,1]
|
43
|
-
gem_lines[gem_name_index] = line_number
|
44
83
|
end
|
84
|
+
end
|
85
|
+
|
86
|
+
end
|
87
|
+
|
88
|
+
desc "Generate report on Python packages"
|
89
|
+
task('python-packages').parse_args(into: OpenStruct.new) do |parser, args|
|
90
|
+
parser.expect_positional_cardinality(0)
|
91
|
+
common_program_args(parser, args)
|
92
|
+
|
93
|
+
parser.on(
|
94
|
+
'-r', '--requirements-file FILE',
|
95
|
+
"requirements.txt-style file to read"
|
96
|
+
) do |file|
|
97
|
+
if args.requirements_file
|
98
|
+
invalid_args!("Only one requirements file may be specified.")
|
99
|
+
end
|
100
|
+
args.requirements_file = file
|
101
|
+
end
|
102
|
+
end
|
103
|
+
task 'python-packages' do
|
104
|
+
require 'myprecious/python_packages'
|
105
|
+
args = Program.args
|
106
|
+
out_fpath = args.output_file || Reporting.default_output_fpath(args.target)
|
107
|
+
|
108
|
+
col_order = Reporting.read_column_order_from(out_fpath)
|
109
|
+
|
110
|
+
req_file = args.requirements_file
|
111
|
+
req_file ||= PyPackageInfo.guess_req_file(args.target)
|
112
|
+
req_file = args.target.join(req_file) unless req_file.nil?
|
113
|
+
if req_file.nil? || !req_file.exist?
|
114
|
+
invalid_args!("Unable to guess requirement file name; specify with '-r FILE' option.")
|
115
|
+
end
|
116
|
+
pkgs = PyPackageInfo::Reader.new(req_file).each_installed_package
|
117
|
+
|
118
|
+
out_fpath.open('w') do |outf|
|
119
|
+
# Header
|
120
|
+
outf.puts "Last updated: #{Time.now.rfc2822}; Use for directional purposes only, this data is not real time and might be slightly inaccurate"
|
121
|
+
outf.puts
|
122
|
+
|
123
|
+
Reporting.header_lines(
|
124
|
+
col_order,
|
125
|
+
PyPackageInfo.method(:col_title)
|
126
|
+
).each {|l| outf.puts l}
|
45
127
|
|
128
|
+
pkgs = pkgs.to_a
|
129
|
+
pkgs.sort_by! {|pkg| pkg.name.downcase}
|
130
|
+
pkgs.each do |pkg|
|
131
|
+
Reporting.on_dependency(pkg.name) do
|
132
|
+
outf.puts col_order.markdown_columns(MarkdownAdapter.new(pkg))
|
133
|
+
end
|
134
|
+
end
|
46
135
|
end
|
47
|
-
#puts gem_lines
|
48
|
-
else
|
49
|
-
File.open('myprecious.md', 'w') { |write_file|
|
50
|
-
write_file.puts "Gem | Our Version | Latest Version | Date available | Age (in days) | License Type | Change Log"
|
51
|
-
write_file.puts "--- | --- | --- | --- | --- | --- | ---"
|
52
|
-
}
|
53
136
|
end
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
137
|
+
|
138
|
+
desc "Clear all myprecious data caches"
|
139
|
+
task('clear-caches').parse_args(into: OpenStruct.new) do |parser, args|
|
140
|
+
args.prompt = true
|
141
|
+
parser.on('--[no-]confirm', "Control confirmation prompt") {|v| args.prompt = v}
|
142
|
+
|
143
|
+
# This option exists to force users to specify the full "--no-confirm"
|
144
|
+
parser.on('--no-confir', "Warn about incomplete --no-confirm") do
|
145
|
+
parser.invalid_args!("Incomplete --no-confirm flag")
|
146
|
+
end
|
147
|
+
end
|
148
|
+
task 'clear-caches' do
|
149
|
+
# Load all "myprecious/*.rb" files so we know where all the caches are
|
150
|
+
Dir[File.expand_path('myprecious/*.rb', __dir__)].each {|f| require f}
|
151
|
+
|
152
|
+
next if Program.args.prompt && !yes_no('Delete all cached data (y/n)?')
|
153
|
+
MyPrecious.data_caches.each do |cache|
|
62
154
|
begin
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
155
|
+
rm_r cache
|
156
|
+
rescue Errno::ENOENT
|
157
|
+
# No problem, we wanted to delete this anyway
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
##
|
164
|
+
# Prompt user for a yes/no answer
|
165
|
+
#
|
166
|
+
# It doesn't matter if they've redirected STDIN/STDOUT -- this grabs the TTY
|
167
|
+
# directly.
|
168
|
+
#
|
169
|
+
def self.yes_no(prompt)
|
170
|
+
Pathname('/dev/tty').open('r+') do |term|
|
171
|
+
loop do
|
172
|
+
term.write("#{prompt} ")
|
173
|
+
case term.gets[0..-2]
|
174
|
+
when 'y', 'Y', 'yes', 'Yes', 'YES'
|
175
|
+
return true
|
176
|
+
when 'n', 'N', 'no', 'No', 'NO'
|
177
|
+
return false
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
##
|
184
|
+
# Tool for getting information about the Git repository associated with a
|
185
|
+
# directory
|
186
|
+
#
|
187
|
+
class GitInfoExtractor
|
188
|
+
URL_PATTERN = /\/([^\/]+)\.git$/
|
189
|
+
|
190
|
+
def initialize(dir)
|
191
|
+
super()
|
192
|
+
@dir = dir
|
193
|
+
end
|
194
|
+
attr_reader :dir
|
195
|
+
|
196
|
+
def git_info
|
197
|
+
@git_info ||= Git.open(self.dir)
|
198
|
+
end
|
199
|
+
|
200
|
+
def origin_remote
|
201
|
+
git_info.remotes.find {|r| r.name == 'origin'}
|
202
|
+
end
|
203
|
+
|
204
|
+
def repo_name
|
205
|
+
@repo_name ||= (URL_PATTERN =~ origin_remote.url) && $1
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
##
|
210
|
+
# Common behavior in dependency report generation
|
211
|
+
#
|
212
|
+
# The methods here are not specific to any language or dependency management
|
213
|
+
# framework. They do work with ColumnOrder and an expected set of attributes
|
214
|
+
# on the dependency information objects.
|
215
|
+
#
|
216
|
+
module Reporting
|
217
|
+
##
|
218
|
+
# Compute the default output filepath for a directory
|
219
|
+
#
|
220
|
+
def default_output_fpath(dir)
|
221
|
+
dir / (GitInfoExtractor.new(dir).repo_name + "-dependency-tracking.md")
|
222
|
+
end
|
223
|
+
module_function :default_output_fpath
|
224
|
+
|
225
|
+
##
|
226
|
+
# Read the column order from the file at the given path
|
227
|
+
#
|
228
|
+
# If +fpath+ indicates a file that does not exist, return the default
|
229
|
+
# ColumnOrder.
|
230
|
+
#
|
231
|
+
def read_column_order_from(fpath)
|
232
|
+
result = ColumnOrder.new
|
233
|
+
begin
|
234
|
+
prev_line = nil
|
235
|
+
fpath.open {|inf| inf.each_line do |line|
|
236
|
+
if prev_line && /^-+(?:\|-+)*$/ =~ line.gsub(' ', '')
|
237
|
+
result.read_order_from_headers(prev_line)
|
238
|
+
break
|
239
|
+
end
|
240
|
+
prev_line = line
|
241
|
+
end}
|
242
|
+
rescue Errno::ENOENT
|
243
|
+
# No problem
|
244
|
+
end
|
245
|
+
return result
|
246
|
+
end
|
247
|
+
module_function :read_column_order_from
|
248
|
+
|
249
|
+
##
|
250
|
+
# Generate header lines for the output Markdown table
|
251
|
+
#
|
252
|
+
# Returns an Array of strings, currently two lines giving header text and
|
253
|
+
# divider row.
|
254
|
+
#
|
255
|
+
def header_lines(order, titlizer)
|
256
|
+
col_titles = order.map {|c| titlizer.call(c)}
|
257
|
+
|
258
|
+
# Check that all attributes in #order round-trip through the title name
|
259
|
+
order.zip(col_titles) do |attr, title|
|
260
|
+
unless title.kind_of?(Symbol) || ColumnOrder.col_from_text_name(title) == attr
|
261
|
+
raise "'#{attr}' does not round-trip (rendered as #{result.inspect})"
|
262
|
+
end
|
263
|
+
end
|
264
|
+
|
265
|
+
return [
|
266
|
+
col_titles.join(" | "),
|
267
|
+
(["---"] * col_titles.length).join(" | "),
|
268
|
+
]
|
269
|
+
end
|
270
|
+
module_function :header_lines
|
271
|
+
# TODO: Mark dependencies with colors a la https://stackoverflow.com/a/41247934
|
272
|
+
|
273
|
+
##
|
274
|
+
# Converts an attribute name to the column title for generic attributes
|
275
|
+
#
|
276
|
+
# Dependency info classes (like RubyGemInfo) can delegate common column
|
277
|
+
# title generation to this function.
|
278
|
+
#
|
279
|
+
def common_col_title(attr)
|
280
|
+
case attr
|
281
|
+
when :current_version then 'Our Version'
|
282
|
+
when :age then 'Age (days)'
|
283
|
+
when :latest_version then 'Latest Version'
|
284
|
+
when :latest_released then 'Date Available'
|
285
|
+
when :recommended_version then 'Recommended Version'
|
286
|
+
when :license then 'License Type'
|
287
|
+
when :changelog then 'Change Log'
|
288
|
+
when :obsolescence then 'How Bad'
|
289
|
+
when :cves then 'CVEs'
|
290
|
+
else
|
291
|
+
warn("'#{attr}' column does not have a mapped name")
|
292
|
+
attr
|
293
|
+
end
|
294
|
+
end
|
295
|
+
module_function :common_col_title
|
296
|
+
|
297
|
+
##
|
298
|
+
# Determine obsolescence level from days
|
299
|
+
#
|
300
|
+
# Returns one of +nil+, +:mild+, +:moderate+, or +:severe+.
|
301
|
+
#
|
302
|
+
# +at_least_moderate:+ allows putting a floor of +:moderate+ obsolescence
|
303
|
+
# on the result.
|
304
|
+
#
|
305
|
+
def obsolescence_by_age(days, at_least_moderate: false)
|
306
|
+
return case
|
307
|
+
when days < 270
|
308
|
+
at_least_moderate ? :moderate : nil
|
309
|
+
when days < 500
|
310
|
+
at_least_moderate ? :moderate : :mild
|
311
|
+
when days < 730
|
312
|
+
:moderate
|
313
|
+
else
|
314
|
+
:severe
|
315
|
+
end
|
316
|
+
end
|
317
|
+
module_function :obsolescence_by_age
|
318
|
+
|
319
|
+
##
|
320
|
+
# Wrap output from processing of an individual dependency with intro and
|
321
|
+
# outro messages
|
322
|
+
#
|
323
|
+
def on_dependency(name)
|
324
|
+
progress_out = $stdout
|
325
|
+
progress_out.puts("--- Reporting on #{name}...")
|
326
|
+
yield
|
327
|
+
progress_out.puts(" (done)")
|
328
|
+
end
|
329
|
+
module_function :on_dependency
|
330
|
+
end
|
331
|
+
|
332
|
+
##
|
333
|
+
# Order of columns in a Markdown table
|
334
|
+
#
|
335
|
+
# Contains the default column ordering when constructed. Columns are
|
336
|
+
# identified by the Symbol commonly used as an attribute on a dependency
|
337
|
+
# info object (e.g. RubyGemInfo instance). Objects of this class behave
|
338
|
+
# to some extent like frozen Array instances.
|
339
|
+
#
|
340
|
+
class ColumnOrder
|
341
|
+
DEFAULT = %i[name current_version age latest_version latest_released recommended_version cves license changelog].freeze
|
342
|
+
COLUMN_FROM_TEXT_NAME = {
|
343
|
+
'gem' => :name,
|
344
|
+
'package' => :name,
|
345
|
+
'module' => :name,
|
346
|
+
'our version' => :current_version,
|
347
|
+
'how bad' => :obsolescence,
|
348
|
+
'latest version' => :latest_version,
|
349
|
+
'date available' => :latest_released,
|
350
|
+
'age (days)' => :age,
|
351
|
+
'license type' => :license,
|
352
|
+
/change ?log/ => :changelog,
|
353
|
+
'recommended version' => :recommended_version,
|
354
|
+
'cves' => :cves,
|
355
|
+
}
|
356
|
+
|
357
|
+
def initialize
|
358
|
+
super
|
359
|
+
@order = DEFAULT
|
360
|
+
end
|
361
|
+
|
362
|
+
##
|
363
|
+
# Get the +n+-th column attribute Symbol
|
364
|
+
#
|
365
|
+
def [](n)
|
366
|
+
@order[n]
|
367
|
+
end
|
368
|
+
|
369
|
+
def length
|
370
|
+
@order.length
|
371
|
+
end
|
372
|
+
|
373
|
+
def each(&blk)
|
374
|
+
@order.each(&blk)
|
375
|
+
end
|
376
|
+
include Enumerable
|
377
|
+
|
378
|
+
##
|
379
|
+
# Update the column order to match those in the given line
|
380
|
+
#
|
381
|
+
# Columns not included in the line are appended in the order they
|
382
|
+
# appear in the default order.
|
383
|
+
#
|
384
|
+
def read_order_from_headers(headers_line)
|
385
|
+
headers = headers_line.split('|').map {|h| h.strip.squeeze(' ')}
|
386
|
+
@order = headers.map {|h| self.class.col_from_text_name(h)}.compact
|
387
|
+
|
388
|
+
# Add in any missing columns at the end
|
389
|
+
@order.concat(DEFAULT - @order)
|
390
|
+
|
391
|
+
return @order.dup
|
392
|
+
end
|
393
|
+
|
394
|
+
##
|
395
|
+
# Render a line to include in a Markdown table for the given dependency
|
396
|
+
#
|
397
|
+
# The dependency must know how to respond to (Ruby) messages (i.e.
|
398
|
+
# have attributes) for all columns currently included in the order as
|
399
|
+
# represented by this instance.
|
400
|
+
#
|
401
|
+
def markdown_columns(dependency)
|
402
|
+
map {|attr| dependency.send(attr)}.join(" | ")
|
403
|
+
end
|
404
|
+
|
405
|
+
##
|
406
|
+
# Given a text name, derive the equivalent column attribute
|
407
|
+
#
|
408
|
+
def self.col_from_text_name(n)
|
409
|
+
n = n.downcase
|
410
|
+
entry = COLUMN_FROM_TEXT_NAME.find {|k, v| k === n}
|
411
|
+
return entry && entry[1]
|
412
|
+
end
|
413
|
+
end
|
414
|
+
|
415
|
+
##
|
416
|
+
# Extension of String that can accomodate some additional commentary
|
417
|
+
#
|
418
|
+
# The +update_info+ attribute is used to pass information about changes
|
419
|
+
# to licensing between the current and recommended version of a dependency,
|
420
|
+
# and may be +nil+.
|
421
|
+
#
|
422
|
+
class LicenseDescription < String
|
423
|
+
attr_accessor :update_info
|
424
|
+
end
|
425
|
+
|
426
|
+
##
|
427
|
+
# Dependency info wrapper to generate nice Markdown columns
|
428
|
+
#
|
429
|
+
# This wrapper takes basic data from the underlying dependency info object
|
430
|
+
# and returns enhanced Markdown for selected columns (e.g. +name+).
|
431
|
+
#
|
432
|
+
class MarkdownAdapter
|
433
|
+
QS_VALUE_UNSAFE = /#{URI::UNSAFE}|[&=]/
|
434
|
+
|
435
|
+
def initialize(dep)
|
436
|
+
super()
|
437
|
+
@dependency = dep
|
438
|
+
end
|
439
|
+
attr_reader :dependency
|
440
|
+
|
441
|
+
def self.embellish(attr_name, &blk)
|
442
|
+
define_method(attr_name) do
|
443
|
+
value = begin
|
444
|
+
dependency.send(attr_name)
|
445
|
+
rescue NoMethodError
|
446
|
+
raise
|
447
|
+
rescue StandardError
|
448
|
+
return "(error)"
|
449
|
+
end
|
450
|
+
|
451
|
+
begin
|
452
|
+
instance_exec(value, &blk)
|
453
|
+
rescue StandardError => ex
|
454
|
+
err_key = [ex.backtrace[0], ex.to_s]
|
455
|
+
unless (@errors ||= Set.new).include?(err_key)
|
456
|
+
@errors << err_key
|
457
|
+
if MyPrecious.tracing_errors?
|
458
|
+
$stderr.puts("Traceback (most recent call last):")
|
459
|
+
ex.backtrace[1..-1].reverse.each_with_index do |loc, i|
|
460
|
+
$stderr.puts("#{(i + 1).to_s.rjust(8)}: #{loc}")
|
100
461
|
end
|
462
|
+
$stderr.puts("#{ex.backtrace[0]}: #{ex} (#{ex.class} while computing #{attr_name})")
|
463
|
+
else
|
464
|
+
$stderr.puts("#{ex} (while computing #{attr_name})")
|
101
465
|
end
|
102
466
|
end
|
103
|
-
|
104
|
-
|
467
|
+
value
|
468
|
+
end
|
469
|
+
end
|
470
|
+
end
|
471
|
+
|
472
|
+
##
|
473
|
+
# Generate Markdown linking the +name+ to the homepage for the dependency
|
474
|
+
#
|
475
|
+
embellish(:name) do |base_val|
|
476
|
+
cswatch = begin
|
477
|
+
color_swatch + ' '
|
478
|
+
rescue StandardError
|
479
|
+
''
|
480
|
+
end
|
481
|
+
"#{cswatch}[#{base_val}](#{dependency.homepage_uri})"
|
482
|
+
rescue Gems::NotFound
|
483
|
+
base_val
|
484
|
+
end
|
485
|
+
|
486
|
+
##
|
487
|
+
# Decorate the latest version as a link to the release history
|
488
|
+
#
|
489
|
+
embellish(:latest_version) do |base_val|
|
490
|
+
release_history_url = begin
|
491
|
+
dependency.release_history_url
|
492
|
+
rescue StandardError
|
493
|
+
nil
|
494
|
+
end
|
495
|
+
|
496
|
+
if release_history_url
|
497
|
+
"[#{base_val}](#{release_history_url})"
|
498
|
+
else
|
499
|
+
base_val
|
500
|
+
end
|
501
|
+
end
|
502
|
+
|
503
|
+
##
|
504
|
+
# Include information about temporal difference between current and
|
505
|
+
# recommended versions
|
506
|
+
#
|
507
|
+
embellish(:recommended_version) do |recced_ver|
|
508
|
+
if recced_ver.kind_of?(String)
|
509
|
+
recced_ver = Gem::Version.new(recced_ver)
|
510
|
+
end
|
511
|
+
next recced_ver if dependency.current_version.nil? || dependency.current_version >= recced_ver
|
512
|
+
|
513
|
+
span_comment = begin
|
514
|
+
if days_newer = dependency.days_between_current_and_recommended
|
515
|
+
" -- #{days_newer} days newer"
|
516
|
+
else
|
517
|
+
""
|
518
|
+
end
|
519
|
+
end
|
520
|
+
|
521
|
+
cve_url = URI('https://nvd.nist.gov/products/cpe/search/results')
|
522
|
+
cve_url.query = URI.encode_www_form(
|
523
|
+
keyword: "cpe:2.3:a:*:#{dependency.name.downcase}:#{recced_ver}",
|
524
|
+
)
|
525
|
+
|
526
|
+
"**#{recced_ver}**#{span_comment} ([current CVEs](#{cve_url}))"
|
527
|
+
end
|
528
|
+
|
529
|
+
##
|
530
|
+
# Include update info in the license column
|
531
|
+
#
|
532
|
+
embellish(:license) do |base_val|
|
533
|
+
begin
|
534
|
+
update_info = base_val.update_info
|
535
|
+
rescue NoMethodError
|
536
|
+
base_val
|
537
|
+
else
|
538
|
+
update_info.to_s.empty? ? base_val : "#{base_val}<br/>(#{update_info})"
|
539
|
+
end
|
540
|
+
end
|
541
|
+
|
542
|
+
##
|
543
|
+
# Render short links for http: or https: changelog URLs
|
544
|
+
#
|
545
|
+
embellish(:changelog) do |base_val|
|
546
|
+
begin
|
547
|
+
uri = URI.parse(base_val)
|
548
|
+
if ['http', 'https'].include?(uri.scheme)
|
549
|
+
next "[on #{uri.hostname}](#{base_val})"
|
105
550
|
end
|
551
|
+
rescue StandardError
|
106
552
|
end
|
553
|
+
|
554
|
+
base_val
|
555
|
+
end
|
556
|
+
|
557
|
+
##
|
558
|
+
# Render links to NIST's NVD
|
559
|
+
#
|
560
|
+
NVD_CVE_URL_TEMPLATE = "https://nvd.nist.gov/vuln/detail/%s"
|
561
|
+
embellish(:cves) do |base_val|
|
562
|
+
base_val.map do |cve|
|
563
|
+
link_text_parts = [cve]
|
564
|
+
addnl_info_parts = []
|
565
|
+
begin
|
566
|
+
addnl_info_parts << cve.vendors.to_a.join(',') unless cve.vendors.empty?
|
567
|
+
rescue StandardError
|
568
|
+
end
|
569
|
+
begin
|
570
|
+
addnl_info_parts << cve.score.to_s if cve.score
|
571
|
+
rescue StandardError
|
572
|
+
end
|
573
|
+
unless addnl_info_parts.empty?
|
574
|
+
link_text_parts << "(#{addnl_info_parts.join(' - ')})"
|
575
|
+
end
|
576
|
+
"[#{link_text_parts.join(' ')}](#{NVD_CVE_URL_TEMPLATE % cve})"
|
577
|
+
end.join(', ')
|
578
|
+
end
|
579
|
+
|
580
|
+
def obsolescence
|
581
|
+
color_swatch
|
582
|
+
rescue StandardError
|
583
|
+
''
|
584
|
+
end
|
585
|
+
|
586
|
+
##
|
587
|
+
# Get a CSS-style hex color code corresponding to the obsolescence of the dependency
|
588
|
+
#
|
589
|
+
def color
|
590
|
+
red = "fb0e0e"
|
591
|
+
|
592
|
+
if (dependency.cves.map(&:score).compact.max || 0) >= 7
|
593
|
+
return red
|
594
|
+
end
|
595
|
+
|
596
|
+
case dependency.obsolescence
|
597
|
+
when :mild then "dde418"
|
598
|
+
when :moderate then "f9b733"
|
599
|
+
when :severe then red
|
600
|
+
else "4dda1b"
|
601
|
+
end
|
602
|
+
end
|
603
|
+
|
604
|
+
##
|
605
|
+
# Markdown for an obsolescence color swatch
|
606
|
+
#
|
607
|
+
# Sourced from: https://stackoverflow.com/a/41247934
|
608
|
+
#
|
609
|
+
def color_swatch
|
610
|
+
"![##{color}](https://placehold.it/15/#{color}/000000?text=+)"
|
611
|
+
end
|
612
|
+
|
613
|
+
##
|
614
|
+
# Delegate other attribute queries to the base dependency object
|
615
|
+
#
|
616
|
+
# Errors are caught and rendered as "(error)"
|
617
|
+
#
|
618
|
+
def method_missing(meth, *args, &blk)
|
619
|
+
dependency.send(meth, *args, &blk)
|
620
|
+
rescue NoMethodError
|
621
|
+
raise
|
622
|
+
rescue StandardError
|
623
|
+
"(error)"
|
624
|
+
end
|
625
|
+
end
|
626
|
+
|
627
|
+
module URIModuleMethods
|
628
|
+
def try_parse(s)
|
629
|
+
parse(s)
|
630
|
+
rescue URI::InvalidURIError
|
631
|
+
nil
|
107
632
|
end
|
108
|
-
File.open('myprecious.md', 'w') { |f| f.write(final_write.join) }
|
109
|
-
file.close
|
110
633
|
end
|
634
|
+
URI.extend(URIModuleMethods)
|
111
635
|
end
|