package-audit 0.1.0 → 0.3.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/lib/package/audit/cli.rb +23 -66
- data/lib/package/audit/command_service.rb +187 -0
- data/lib/package/audit/const/cmd.rb +16 -0
- data/lib/package/audit/const/fields.rb +36 -0
- data/lib/package/audit/const/file.rb +13 -0
- data/lib/package/audit/const/time.rb +11 -0
- data/lib/package/audit/duplicate_package_merger.rb +26 -0
- data/lib/package/audit/enum/environment.rb +0 -2
- data/lib/package/audit/enum/risk_explanation.rb +2 -2
- data/lib/package/audit/enum/vulnerability_type.rb +1 -0
- data/lib/package/audit/formatter/risk.rb +1 -1
- data/lib/package/audit/formatter/version.rb +7 -6
- data/lib/package/audit/formatter/version_date.rb +3 -3
- data/lib/package/audit/formatter/vulnerability.rb +2 -2
- data/lib/package/audit/npm/node_collection.rb +64 -0
- data/lib/package/audit/npm/npm_meta_data.rb +41 -0
- data/lib/package/audit/npm/vulnerability_finder.rb +44 -0
- data/lib/package/audit/npm/yarn_lock_parser.rb +46 -0
- data/lib/package/audit/package.rb +91 -0
- data/lib/package/audit/{dependency_printer.rb → printer.rb} +33 -51
- data/lib/package/audit/risk_calculator.rb +49 -34
- data/lib/package/audit/ruby/bundler_specs.rb +16 -9
- data/lib/package/audit/ruby/gem_collection.rb +26 -26
- data/lib/package/audit/ruby/gem_meta_data.rb +11 -9
- data/lib/package/audit/ruby/vulnerability_finder.rb +23 -12
- data/lib/package/audit/util/summary_printer.rb +28 -21
- data/lib/package/audit/version.rb +1 -1
- data/sig/package/audit/command_service.rbs +29 -0
- data/sig/package/audit/const/cmd.rbs +14 -0
- data/sig/package/audit/const/fields.rbs +13 -0
- data/sig/package/audit/const/file.rbs +13 -0
- data/sig/package/audit/const/time.rbs +11 -0
- data/sig/package/audit/duplicate_package_merger.rbs +11 -0
- data/sig/package/audit/enum/vulnerability_type.rbs +1 -0
- data/sig/package/audit/npm/node_collection.rbs +29 -0
- data/sig/package/audit/npm/npm_meta_data.rbs +19 -0
- data/sig/package/audit/npm/vulnerability_finder.rbs +21 -0
- data/sig/package/audit/npm/yarn_lock_parser.rbs +20 -0
- data/sig/package/audit/{dependency.rbs → package.rbs} +14 -4
- data/sig/package/audit/printer.rbs +24 -0
- data/sig/package/audit/risk_calculator.rbs +6 -6
- data/sig/package/audit/ruby/bundler_specs.rbs +2 -2
- data/sig/package/audit/ruby/gem_collection.rbs +8 -4
- data/sig/package/audit/ruby/gem_meta_data.rbs +7 -8
- data/sig/package/audit/ruby/vulnerability_finder.rbs +10 -1
- data/sig/package/audit/util/summary_printer.rbs +3 -5
- metadata +27 -9
- data/lib/package/audit/const.rb +0 -5
- data/lib/package/audit/dependency.rb +0 -57
- data/sig/const.rbs +0 -5
- data/sig/package/audit/dependency_printer.rbs +0 -24
@@ -0,0 +1,44 @@
|
|
1
|
+
require_relative '../const/cmd'
|
2
|
+
require_relative '../enum/vulnerability_type'
|
3
|
+
|
4
|
+
module Package
|
5
|
+
module Audit
|
6
|
+
module Npm
|
7
|
+
class VulnerabilityFinder
|
8
|
+
AUDIT_ADVISORY_REGEX = /^{"type":"auditAdvisory".*$/
|
9
|
+
|
10
|
+
def initialize(dir, pkgs)
|
11
|
+
@dir = dir
|
12
|
+
@pkg_hash = pkgs.to_h { |pkg| [pkg.name, pkg] }
|
13
|
+
@vuln_hash = {}
|
14
|
+
end
|
15
|
+
|
16
|
+
def run
|
17
|
+
json_string_lines = `#{format(Const::Cmd::YARN_AUDIT_JSON, @dir)}`
|
18
|
+
array = json_string_lines.scan(AUDIT_ADVISORY_REGEX)
|
19
|
+
|
20
|
+
vulnerability_json_array = JSON.parse("[#{array.join(',')}]", symbolize_names: true)
|
21
|
+
vulnerability_json_array.each do |vulnerability_json|
|
22
|
+
update_meta_data(vulnerability_json)
|
23
|
+
end
|
24
|
+
@vuln_hash.values
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def update_meta_data(json) # rubocop:disable Metrics/AbcSize
|
30
|
+
parent_name = json[:data][:resolution][:path].split('>').first
|
31
|
+
advisory = json[:data][:advisory]
|
32
|
+
name = advisory[:module_name]
|
33
|
+
version = advisory[:findings][0][:version]
|
34
|
+
full_name = "#{name}@#{version}"
|
35
|
+
vulnerability = advisory[:severity] || Enum::VulnerabilityType::UNKNOWN
|
36
|
+
|
37
|
+
@vuln_hash[full_name] = Package.new(name, version) unless @vuln_hash.key? full_name
|
38
|
+
@vuln_hash[full_name].update vulnerabilities: @vuln_hash[full_name].vulnerabilities + [vulnerability]
|
39
|
+
@vuln_hash[full_name].update groups: @pkg_hash[parent_name].groups
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module Package
|
2
|
+
module Audit
|
3
|
+
module Npm
|
4
|
+
class YarnLockParser
|
5
|
+
def initialize(yarn_lock_path)
|
6
|
+
@yarn_lock_file = File.read(yarn_lock_path)
|
7
|
+
@yarn_lock_path = yarn_lock_path
|
8
|
+
end
|
9
|
+
|
10
|
+
def fetch(default_deps, dev_deps)
|
11
|
+
pkgs = []
|
12
|
+
default_deps.merge(dev_deps).each do |dep_name, expected_version|
|
13
|
+
pkg_block = fetch_package_block(dep_name, expected_version)
|
14
|
+
version = fetch_package_version(dep_name, pkg_block)
|
15
|
+
pks = Package.new(dep_name.to_s, version)
|
16
|
+
pks.update groups: dev_deps.key?(dep_name) ? %i[development] : %i[default development]
|
17
|
+
pkgs << pks
|
18
|
+
end
|
19
|
+
pkgs
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def fetch_package_block(dep_name, expected_version)
|
25
|
+
regex = /#{Regexp.escape(dep_name)}@#{Regexp.escape(expected_version)}.*?:.*?(\n\n|\z)/m
|
26
|
+
blocks = @yarn_lock_file.match(regex)
|
27
|
+
if blocks.nil? || blocks[0].nil?
|
28
|
+
raise NoMatchingPatternError, "Unable to find \"#{dep_name}\" in #{@yarn_lock_path}"
|
29
|
+
end
|
30
|
+
|
31
|
+
blocks[0] || ''
|
32
|
+
end
|
33
|
+
|
34
|
+
def fetch_package_version(dep_name, pkg_block)
|
35
|
+
version = pkg_block.match(/version"?\s*"(.*?)"/)&.captures&.[](0)
|
36
|
+
if version.nil?
|
37
|
+
raise NoMatchingPatternError,
|
38
|
+
"Unable to find the version of \"#{dep_name}\" in #{@yarn_lock_path}"
|
39
|
+
end
|
40
|
+
|
41
|
+
version || '0.0.0.0'
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
require_relative 'risk'
|
2
|
+
require_relative 'risk_calculator'
|
3
|
+
require_relative 'enum/environment'
|
4
|
+
require_relative 'enum/risk_type'
|
5
|
+
require_relative 'enum/risk_explanation'
|
6
|
+
|
7
|
+
module Package
|
8
|
+
module Audit
|
9
|
+
class Package
|
10
|
+
attr_reader :name, :version
|
11
|
+
attr_accessor :groups, :version_date, :latest_version, :latest_version_date, :vulnerabilities
|
12
|
+
|
13
|
+
def initialize(name, version, **attr)
|
14
|
+
@name = name.to_s
|
15
|
+
@version = version.to_s
|
16
|
+
@groups = []
|
17
|
+
@vulnerabilities = []
|
18
|
+
@risks = []
|
19
|
+
update(**attr)
|
20
|
+
end
|
21
|
+
|
22
|
+
def full_name
|
23
|
+
"#{name}@#{version}"
|
24
|
+
end
|
25
|
+
|
26
|
+
def update(**attr)
|
27
|
+
attr.each { |key, value| instance_variable_set("@#{key}", value) }
|
28
|
+
end
|
29
|
+
|
30
|
+
def risk
|
31
|
+
risks.max || Risk.new(Enum::RiskType::NONE)
|
32
|
+
end
|
33
|
+
|
34
|
+
def risks
|
35
|
+
RiskCalculator.new(self).find
|
36
|
+
end
|
37
|
+
|
38
|
+
def risk?
|
39
|
+
risks.any?
|
40
|
+
end
|
41
|
+
|
42
|
+
def group_list
|
43
|
+
@groups.join('|')
|
44
|
+
end
|
45
|
+
|
46
|
+
def vulnerabilities_grouped
|
47
|
+
@vulnerabilities.group_by(&:itself).map { |k, v| "#{k}(#{v.length})" }.join('|')
|
48
|
+
end
|
49
|
+
|
50
|
+
def risk_type
|
51
|
+
risk.type
|
52
|
+
end
|
53
|
+
|
54
|
+
def risk_explanation
|
55
|
+
risk.explanation
|
56
|
+
end
|
57
|
+
|
58
|
+
def deprecated?
|
59
|
+
risks.each do |risk|
|
60
|
+
return true if risk.explanation == Enum::RiskExplanation::POTENTIAL_DEPRECATION
|
61
|
+
end
|
62
|
+
false
|
63
|
+
end
|
64
|
+
|
65
|
+
def outdated?
|
66
|
+
risks.each do |risk|
|
67
|
+
return true if [
|
68
|
+
Enum::RiskExplanation::OUTDATED,
|
69
|
+
Enum::RiskExplanation::OUTDATED_BY_MAJOR_VERSION
|
70
|
+
].include?(risk.explanation || '')
|
71
|
+
end
|
72
|
+
false
|
73
|
+
end
|
74
|
+
|
75
|
+
def vulnerable?
|
76
|
+
risks.each do |risk|
|
77
|
+
return true if risk.explanation == Enum::RiskExplanation::VULNERABILITY
|
78
|
+
end
|
79
|
+
false
|
80
|
+
end
|
81
|
+
|
82
|
+
def to_csv(fields)
|
83
|
+
fields.map { |field| send(field) }.join(',')
|
84
|
+
end
|
85
|
+
|
86
|
+
def to_s
|
87
|
+
"#{@name} #{@version} - [#{@groups.sort.join(', ')}]"
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -1,65 +1,49 @@
|
|
1
|
-
require_relative '
|
2
|
-
require_relative '
|
3
|
-
require_relative '
|
4
|
-
require_relative '
|
1
|
+
require_relative 'const/fields'
|
2
|
+
require_relative 'formatter/risk'
|
3
|
+
require_relative 'formatter/version'
|
4
|
+
require_relative 'formatter/version_date'
|
5
|
+
require_relative 'formatter/vulnerability'
|
5
6
|
|
6
7
|
module Package
|
7
8
|
module Audit
|
8
|
-
class
|
9
|
+
class Printer
|
9
10
|
BASH_FORMATTING_REGEX = /\e\[\d+(?:;\d+)*m/
|
10
11
|
|
11
12
|
COLUMN_GAP = 2
|
12
13
|
|
13
|
-
|
14
|
-
|
15
|
-
name
|
16
|
-
version
|
17
|
-
latest_version
|
18
|
-
latest_version_date
|
19
|
-
groups
|
20
|
-
vulnerabilities
|
21
|
-
risk_type
|
22
|
-
risk_explanation
|
23
|
-
]
|
24
|
-
|
25
|
-
HEADERS = {
|
26
|
-
name: 'Package',
|
27
|
-
version: 'Version',
|
28
|
-
latest_version: 'Latest',
|
29
|
-
latest_version_date: 'Latest Date',
|
30
|
-
groups: 'Groups',
|
31
|
-
vulnerabilities: 'Vulnerabilities',
|
32
|
-
risk_type: 'Risk',
|
33
|
-
risk_explanation: 'Risk Explanation'
|
34
|
-
}
|
35
|
-
|
36
|
-
def initialize(dependencies, options)
|
37
|
-
@dependencies = dependencies
|
14
|
+
def initialize(pkgs, options)
|
15
|
+
@pkgs = pkgs
|
38
16
|
@options = options
|
39
17
|
end
|
40
18
|
|
41
|
-
def print(fields
|
42
|
-
|
43
|
-
|
44
|
-
end
|
19
|
+
def print(fields)
|
20
|
+
check_fields(fields)
|
21
|
+
return if @pkgs.empty?
|
45
22
|
|
46
23
|
if @options[:csv]
|
47
24
|
csv(fields, exclude_headers: @options[:'exclude-headers'])
|
48
25
|
else
|
49
26
|
pretty(fields)
|
50
27
|
end
|
28
|
+
puts
|
51
29
|
end
|
52
30
|
|
53
31
|
private
|
54
32
|
|
55
|
-
def
|
56
|
-
return
|
33
|
+
def check_fields(fields)
|
34
|
+
return unless (fields - Const::Fields::ALL).any?
|
57
35
|
|
58
|
-
|
36
|
+
raise ArgumentError,
|
37
|
+
"#{fields - Const::Fields::ALL} are not valid field names. " \
|
38
|
+
"Available fields names are: #{Const::Fields::ALL}."
|
39
|
+
end
|
40
|
+
|
41
|
+
def pretty(fields = Const::Fields::REPORT) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
42
|
+
# find the maximum length of each field across all the packages so we know how many
|
59
43
|
# characters of horizontal space to allocate for each field when printing
|
60
44
|
fields.each do |key|
|
61
|
-
instance_variable_set "@max_#{key}", HEADERS[key].length
|
62
|
-
@
|
45
|
+
instance_variable_set "@max_#{key}", Const::Fields::HEADERS[key].length
|
46
|
+
@pkgs.each do |gem|
|
63
47
|
curr_field_length = case key
|
64
48
|
when :vulnerabilities
|
65
49
|
gem.vulnerabilities_grouped.length
|
@@ -78,24 +62,24 @@ module Package
|
|
78
62
|
|
79
63
|
puts '=' * line_length
|
80
64
|
puts fields.map { |key|
|
81
|
-
HEADERS[key].gsub(BASH_FORMATTING_REGEX, '').ljust(instance_variable_get("@max_#{key}"))
|
65
|
+
Const::Fields::HEADERS[key].gsub(BASH_FORMATTING_REGEX, '').ljust(instance_variable_get("@max_#{key}"))
|
82
66
|
}.join(' ' * COLUMN_GAP)
|
83
67
|
puts '=' * line_length
|
84
68
|
|
85
|
-
@
|
69
|
+
@pkgs.each do |pkg|
|
86
70
|
puts fields.map { |key|
|
87
|
-
val =
|
71
|
+
val = pkg.send(key) || ''
|
88
72
|
val = case key
|
89
73
|
when :groups
|
90
|
-
|
74
|
+
pkg.group_list
|
91
75
|
when :risk_type
|
92
|
-
Formatter::Risk.new(
|
76
|
+
Formatter::Risk.new(pkg.risk_type).format
|
93
77
|
when :version
|
94
|
-
Formatter::Version.new(
|
78
|
+
Formatter::Version.new(pkg.version, pkg.latest_version).format
|
95
79
|
when :vulnerabilities
|
96
|
-
Formatter::Vulnerability.new(
|
80
|
+
Formatter::Vulnerability.new(pkg.vulnerabilities).format
|
97
81
|
when :latest_version_date
|
98
|
-
Formatter::VersionDate.new(
|
82
|
+
Formatter::VersionDate.new(pkg.latest_version_date).format
|
99
83
|
else
|
100
84
|
val
|
101
85
|
end
|
@@ -106,9 +90,7 @@ module Package
|
|
106
90
|
end
|
107
91
|
end
|
108
92
|
|
109
|
-
def csv(fields, exclude_headers: false)
|
110
|
-
return if @dependencies.empty?
|
111
|
-
|
93
|
+
def csv(fields, exclude_headers: false)
|
112
94
|
value_fields = fields.map do |field|
|
113
95
|
case field
|
114
96
|
when :groups
|
@@ -121,7 +103,7 @@ module Package
|
|
121
103
|
end
|
122
104
|
|
123
105
|
puts fields.join(',') unless exclude_headers
|
124
|
-
@
|
106
|
+
@pkgs.map { |gem| puts gem.to_csv(value_fields) }
|
125
107
|
end
|
126
108
|
end
|
127
109
|
end
|
@@ -1,64 +1,79 @@
|
|
1
|
-
require_relative '
|
1
|
+
require_relative 'const/time'
|
2
2
|
|
3
3
|
module Package
|
4
4
|
module Audit
|
5
5
|
class RiskCalculator
|
6
|
-
def initialize(
|
7
|
-
@
|
6
|
+
def initialize(pkg)
|
7
|
+
@pkg = pkg
|
8
8
|
end
|
9
9
|
|
10
10
|
def find
|
11
|
-
|
12
|
-
deprecation_risk = assess_vulnerability_risk
|
13
|
-
version_risk = assess_version_risk
|
11
|
+
risks = assess_vulnerability_risks + assess_deprecation_risks + assess_version_risks
|
14
12
|
|
15
|
-
|
16
|
-
|
17
|
-
|
13
|
+
unless production_dependency?
|
14
|
+
risks.each_with_index do |risk, index|
|
15
|
+
risks[index] =
|
16
|
+
[risk, Risk.new(Enum::RiskType::MEDIUM, risk.explanation)].min || Risk.new(Enum::RiskType::NONE)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
risks
|
18
20
|
end
|
19
21
|
|
20
22
|
private
|
21
23
|
|
22
|
-
def
|
23
|
-
|
24
|
+
def assess_vulnerability_risks # rubocop:disable Metrics/MethodLength
|
25
|
+
risks = []
|
26
|
+
|
27
|
+
if (@pkg.vulnerabilities & [
|
24
28
|
Enum::VulnerabilityType::UNKNOWN,
|
25
29
|
Enum::VulnerabilityType::CRITICAL,
|
26
30
|
Enum::VulnerabilityType::HIGH
|
27
31
|
]).any?
|
28
|
-
Risk.new(Enum::RiskType::HIGH, Enum::RiskExplanation::VULNERABILITY)
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
Risk.new(Enum::RiskType::
|
32
|
+
risks << Risk.new(Enum::RiskType::HIGH, Enum::RiskExplanation::VULNERABILITY)
|
33
|
+
end
|
34
|
+
if (@pkg.vulnerabilities & [
|
35
|
+
Enum::VulnerabilityType::MEDIUM,
|
36
|
+
Enum::VulnerabilityType::MODERATE
|
37
|
+
]).any?
|
38
|
+
risks << Risk.new(Enum::RiskType::MEDIUM, Enum::RiskExplanation::VULNERABILITY)
|
39
|
+
end
|
40
|
+
if @pkg.vulnerabilities.include? Enum::VulnerabilityType::LOW
|
41
|
+
risks << Risk.new(Enum::RiskType::LOW, Enum::RiskExplanation::VULNERABILITY)
|
35
42
|
end
|
43
|
+
risks
|
36
44
|
end
|
37
45
|
|
38
|
-
def
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
46
|
+
def assess_version_risks # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
47
|
+
risks = []
|
48
|
+
|
49
|
+
return risks if @pkg.latest_version.nil?
|
50
|
+
|
51
|
+
version_parts = @pkg.version.split('.').map(&:to_i)
|
52
|
+
latest_version_parts = @pkg.latest_version.split('.').map(&:to_i)
|
53
|
+
|
54
|
+
if (version_parts.first || 0) < (latest_version_parts.first || 0)
|
55
|
+
risks << Risk.new(Enum::RiskType::MEDIUM, Enum::RiskExplanation::OUTDATED_BY_MAJOR_VERSION)
|
56
|
+
end
|
57
|
+
if (version_parts.first || 0) == (latest_version_parts.first || 0) &&
|
58
|
+
(version_parts[1..] <=> latest_version_parts[1..]) == -1
|
59
|
+
risks << Risk.new(Enum::RiskType::LOW, Enum::RiskExplanation::OUTDATED)
|
45
60
|
end
|
61
|
+
risks
|
46
62
|
end
|
47
63
|
|
48
|
-
def
|
49
|
-
|
64
|
+
def assess_deprecation_risks
|
65
|
+
risks = []
|
66
|
+
seconds_since_date = (Time.now - Time.parse(@pkg.latest_version_date)).to_i
|
50
67
|
|
51
|
-
if
|
52
|
-
|
53
|
-
Risk.new(Enum::RiskType::MEDIUM, Enum::RiskExplanation::POTENTIAL_DEPRECATION)
|
54
|
-
else
|
55
|
-
Risk.new(Enum::RiskType::NONE)
|
68
|
+
if seconds_since_date >= Const::Time::SECONDS_ELAPSED_TO_BE_OUTDATED
|
69
|
+
risks << Risk.new(Enum::RiskType::MEDIUM, Enum::RiskExplanation::POTENTIAL_DEPRECATION)
|
56
70
|
end
|
71
|
+
risks
|
57
72
|
end
|
58
73
|
|
59
74
|
def production_dependency?
|
60
|
-
@
|
61
|
-
|
75
|
+
@pkg.groups.none? || (@pkg.groups & [Enum::Environment::DEFAULT,
|
76
|
+
Enum::Environment::PRODUCTION]).any?
|
62
77
|
end
|
63
78
|
end
|
64
79
|
end
|
@@ -1,6 +1,6 @@
|
|
1
|
-
require_relative '../
|
2
|
-
require_relative '
|
3
|
-
require_relative '
|
1
|
+
require_relative '../package'
|
2
|
+
require_relative 'gem_meta_data'
|
3
|
+
require_relative 'vulnerability_finder'
|
4
4
|
|
5
5
|
require 'bundler'
|
6
6
|
|
@@ -8,16 +8,23 @@ module Package
|
|
8
8
|
module Audit
|
9
9
|
module Ruby
|
10
10
|
class BundlerSpecs
|
11
|
-
def self.all
|
12
|
-
Bundler.
|
11
|
+
def self.all(dir)
|
12
|
+
Bundler.with_unbundled_env do
|
13
|
+
ENV['BUNDLE_GEMFILE'] = "#{dir}/Gemfile"
|
14
|
+
Bundler.ui.silence { Bundler.definition.resolve }
|
15
|
+
end
|
13
16
|
end
|
14
17
|
|
15
|
-
def self.gemfile
|
16
|
-
current_dependencies = Bundler.
|
17
|
-
|
18
|
+
def self.gemfile(dir)
|
19
|
+
current_dependencies = Bundler.with_unbundled_env do
|
20
|
+
ENV['BUNDLE_GEMFILE'] = "#{dir}/Gemfile"
|
21
|
+
Bundler.reset!
|
22
|
+
Bundler.ui.silence do
|
23
|
+
Bundler.load.dependencies.to_h { |dep| [dep.name, dep] }
|
24
|
+
end
|
18
25
|
end
|
19
26
|
|
20
|
-
gemfile_specs, = all.partition do |spec|
|
27
|
+
gemfile_specs, = all(dir).partition do |spec|
|
21
28
|
current_dependencies.key? spec.name
|
22
29
|
end
|
23
30
|
gemfile_specs
|
@@ -1,41 +1,41 @@
|
|
1
|
-
require_relative '
|
2
|
-
require_relative '
|
1
|
+
require_relative 'bundler_specs'
|
2
|
+
require_relative '../enum/risk_type'
|
3
|
+
require_relative '../duplicate_package_merger'
|
3
4
|
|
4
5
|
module Package
|
5
6
|
module Audit
|
6
7
|
module Ruby
|
7
8
|
class GemCollection
|
8
|
-
def
|
9
|
-
|
10
|
-
dependencies = specs.map { |spec| Dependency.new(spec.name, spec.version) }
|
11
|
-
vulnerable_deps = VulnerabilityFinder.run
|
12
|
-
GemMetaData.new(dependencies + vulnerable_deps).fetch.filter(&:risk?).sort_by(&:name).uniq(&:name)
|
9
|
+
def initialize(dir)
|
10
|
+
@dir = dir
|
13
11
|
end
|
14
12
|
|
15
|
-
def
|
16
|
-
specs = BundlerSpecs.gemfile
|
17
|
-
|
18
|
-
|
19
|
-
GemMetaData.new(
|
20
|
-
|
21
|
-
end.sort_by(&:name).uniq(&:name)
|
13
|
+
def all
|
14
|
+
specs = BundlerSpecs.gemfile(@dir)
|
15
|
+
pkgs = specs.map { |spec| Package.new(spec.name, spec.version) }
|
16
|
+
vulnerable_pkgs = VulnerabilityFinder.new(@dir).run
|
17
|
+
pkgs = GemMetaData.new(pkgs + vulnerable_pkgs).fetch.filter(&:risk?)
|
18
|
+
DuplicatePackageMerger.new(pkgs).run
|
22
19
|
end
|
23
20
|
|
24
|
-
def
|
25
|
-
specs =
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
dep.version < dep.latest_version
|
30
|
-
end.sort_by(&:name).uniq(&:name)
|
21
|
+
def deprecated
|
22
|
+
specs = BundlerSpecs.gemfile(@dir)
|
23
|
+
pkgs = specs.map { |spec| Package.new(spec.name, spec.version) }
|
24
|
+
pkgs = GemMetaData.new(pkgs).fetch.filter(&:deprecated?)
|
25
|
+
DuplicatePackageMerger.new(pkgs).run
|
31
26
|
end
|
32
27
|
|
33
|
-
def
|
34
|
-
|
28
|
+
def outdated(include_implicit: false)
|
29
|
+
specs = include_implicit ? BundlerSpecs.all(@dir) : BundlerSpecs.gemfile(@dir)
|
30
|
+
pkgs = specs.map { |spec| Package.new(spec.name, spec.version) }
|
31
|
+
pkgs = GemMetaData.new(pkgs).fetch.filter(&:outdated?)
|
32
|
+
DuplicatePackageMerger.new(pkgs).run
|
33
|
+
end
|
35
34
|
|
36
|
-
|
37
|
-
|
38
|
-
|
35
|
+
def vulnerable
|
36
|
+
pkgs = VulnerabilityFinder.new(@dir).run
|
37
|
+
pkgs = GemMetaData.new(pkgs).fetch.filter(&:vulnerable?)
|
38
|
+
DuplicatePackageMerger.new(pkgs).run
|
39
39
|
end
|
40
40
|
end
|
41
41
|
end
|
@@ -1,11 +1,11 @@
|
|
1
|
-
require_relative '../
|
1
|
+
require_relative '../package'
|
2
2
|
|
3
3
|
module Package
|
4
4
|
module Audit
|
5
5
|
module Ruby
|
6
6
|
class GemMetaData
|
7
|
-
def initialize(
|
8
|
-
@
|
7
|
+
def initialize(pkgs)
|
8
|
+
@pkgs = pkgs
|
9
9
|
@gem_hash = {}
|
10
10
|
end
|
11
11
|
|
@@ -20,23 +20,25 @@ module Package
|
|
20
20
|
def find_rubygems_metadata # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
21
21
|
fetcher = Gem::SpecFetcher.fetcher
|
22
22
|
|
23
|
-
@
|
24
|
-
gem_dependency = Gem::Dependency.new
|
23
|
+
@pkgs.each do |pkg|
|
24
|
+
gem_dependency = Gem::Dependency.new pkg.name, ">= #{pkg.version}"
|
25
25
|
local_version_date = Time.new(0)
|
26
26
|
latest_version_date = Time.new(0)
|
27
|
-
local_version = Gem::Version.new(
|
28
|
-
latest_version = Gem::Version.new('0.0.0')
|
27
|
+
local_version = Gem::Version.new(pkg.version)
|
28
|
+
latest_version = Gem::Version.new('0.0.0.0')
|
29
29
|
|
30
30
|
remote_dependencies, = fetcher.spec_for_dependency gem_dependency
|
31
31
|
|
32
|
+
next unless remote_dependencies.any?
|
33
|
+
|
32
34
|
remote_dependencies.each do |remote_spec, _|
|
33
35
|
latest_version = remote_spec.version if latest_version < remote_spec.version
|
34
36
|
latest_version_date = remote_spec.date if latest_version_date < remote_spec.date
|
35
37
|
local_version_date = remote_spec.date if local_version == remote_spec.version
|
36
38
|
end
|
37
39
|
|
38
|
-
@gem_hash[
|
39
|
-
|
40
|
+
@gem_hash[pkg.name] = pkg
|
41
|
+
pkg.update latest_version: latest_version.to_s,
|
40
42
|
version_date: local_version_date.strftime('%Y-%m-%d'),
|
41
43
|
latest_version_date: latest_version_date.strftime('%Y-%m-%d')
|
42
44
|
end
|
@@ -1,22 +1,33 @@
|
|
1
|
+
require_relative '../const/cmd'
|
1
2
|
require_relative '../enum/vulnerability_type'
|
2
3
|
|
3
4
|
module Package
|
4
5
|
module Audit
|
5
6
|
module Ruby
|
6
7
|
class VulnerabilityFinder
|
7
|
-
def
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
gem_hash[gem_name].update vulnerabilities: gem_hash[gem_name].vulnerabilities + [vulnerability]
|
8
|
+
def initialize(dir)
|
9
|
+
@dir = dir
|
10
|
+
@vuln_hash = {}
|
11
|
+
end
|
12
|
+
|
13
|
+
def run
|
14
|
+
json_result = `#{format(Const::Cmd::BUNDLE_AUDIT_JSON, @dir)}`
|
15
|
+
vulnerability_json_array = JSON.parse(json_result, symbolize_names: true)[:results]
|
16
|
+
vulnerability_json_array.each do |vulnerability_json|
|
17
|
+
update_meta_data(vulnerability_json)
|
18
18
|
end
|
19
|
-
|
19
|
+
@vuln_hash.values
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def update_meta_data(json)
|
25
|
+
name = json[:gem][:name]
|
26
|
+
version = json[:gem][:version]
|
27
|
+
full_name = "#{name}@#{version}"
|
28
|
+
vulnerability = json[:advisory][:criticality] || Enum::VulnerabilityType::UNKNOWN
|
29
|
+
@vuln_hash[full_name] = Package.new(name, version) unless @vuln_hash.key? full_name
|
30
|
+
@vuln_hash[full_name].update vulnerabilities: @vuln_hash[full_name].vulnerabilities + [vulnerability]
|
20
31
|
end
|
21
32
|
end
|
22
33
|
end
|