package-audit 0.1.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/lib/package/audit/cli.rb +23 -66
  3. data/lib/package/audit/command_service.rb +187 -0
  4. data/lib/package/audit/const/cmd.rb +16 -0
  5. data/lib/package/audit/const/fields.rb +36 -0
  6. data/lib/package/audit/const/file.rb +13 -0
  7. data/lib/package/audit/const/time.rb +11 -0
  8. data/lib/package/audit/duplicate_package_merger.rb +26 -0
  9. data/lib/package/audit/enum/environment.rb +0 -2
  10. data/lib/package/audit/enum/risk_explanation.rb +2 -2
  11. data/lib/package/audit/enum/vulnerability_type.rb +1 -0
  12. data/lib/package/audit/formatter/risk.rb +1 -1
  13. data/lib/package/audit/formatter/version.rb +7 -6
  14. data/lib/package/audit/formatter/version_date.rb +3 -3
  15. data/lib/package/audit/formatter/vulnerability.rb +2 -2
  16. data/lib/package/audit/npm/node_collection.rb +64 -0
  17. data/lib/package/audit/npm/npm_meta_data.rb +41 -0
  18. data/lib/package/audit/npm/vulnerability_finder.rb +44 -0
  19. data/lib/package/audit/npm/yarn_lock_parser.rb +46 -0
  20. data/lib/package/audit/package.rb +91 -0
  21. data/lib/package/audit/{dependency_printer.rb → printer.rb} +33 -51
  22. data/lib/package/audit/risk_calculator.rb +49 -34
  23. data/lib/package/audit/ruby/bundler_specs.rb +16 -9
  24. data/lib/package/audit/ruby/gem_collection.rb +26 -26
  25. data/lib/package/audit/ruby/gem_meta_data.rb +11 -9
  26. data/lib/package/audit/ruby/vulnerability_finder.rb +23 -12
  27. data/lib/package/audit/util/summary_printer.rb +28 -21
  28. data/lib/package/audit/version.rb +1 -1
  29. data/sig/package/audit/command_service.rbs +29 -0
  30. data/sig/package/audit/const/cmd.rbs +14 -0
  31. data/sig/package/audit/const/fields.rbs +13 -0
  32. data/sig/package/audit/const/file.rbs +13 -0
  33. data/sig/package/audit/const/time.rbs +11 -0
  34. data/sig/package/audit/duplicate_package_merger.rbs +11 -0
  35. data/sig/package/audit/enum/vulnerability_type.rbs +1 -0
  36. data/sig/package/audit/npm/node_collection.rbs +29 -0
  37. data/sig/package/audit/npm/npm_meta_data.rbs +19 -0
  38. data/sig/package/audit/npm/vulnerability_finder.rbs +21 -0
  39. data/sig/package/audit/npm/yarn_lock_parser.rbs +20 -0
  40. data/sig/package/audit/{dependency.rbs → package.rbs} +14 -4
  41. data/sig/package/audit/printer.rbs +24 -0
  42. data/sig/package/audit/risk_calculator.rbs +6 -6
  43. data/sig/package/audit/ruby/bundler_specs.rbs +2 -2
  44. data/sig/package/audit/ruby/gem_collection.rbs +8 -4
  45. data/sig/package/audit/ruby/gem_meta_data.rbs +7 -8
  46. data/sig/package/audit/ruby/vulnerability_finder.rbs +10 -1
  47. data/sig/package/audit/util/summary_printer.rbs +3 -5
  48. metadata +27 -9
  49. data/lib/package/audit/const.rb +0 -5
  50. data/lib/package/audit/dependency.rb +0 -57
  51. data/sig/const.rbs +0 -5
  52. 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 './formatter/risk'
2
- require_relative './formatter/version'
3
- require_relative './formatter/version_date'
4
- require_relative './formatter/vulnerability'
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 DependencyPrinter
9
+ class Printer
9
10
  BASH_FORMATTING_REGEX = /\e\[\d+(?:;\d+)*m/
10
11
 
11
12
  COLUMN_GAP = 2
12
13
 
13
- # the names of these fields must match the instance variables in the Dependency class
14
- FIELDS = %i[
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 = FIELDS)
42
- if (fields - FIELDS).any?
43
- raise ArgumentError, "#{fields - FIELDS} are not valid field names. Available fields names are: #{FIELDS}."
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 pretty(fields) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
56
- return if @dependencies.empty?
33
+ def check_fields(fields)
34
+ return unless (fields - Const::Fields::ALL).any?
57
35
 
58
- # find the maximum length of each field across all the dependencies so we know how many
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
- @dependencies.each do |gem|
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
- @dependencies.each do |dep|
69
+ @pkgs.each do |pkg|
86
70
  puts fields.map { |key|
87
- val = dep.send(key) || ''
71
+ val = pkg.send(key) || ''
88
72
  val = case key
89
73
  when :groups
90
- dep.group_list
74
+ pkg.group_list
91
75
  when :risk_type
92
- Formatter::Risk.new(dep.risk_type).format
76
+ Formatter::Risk.new(pkg.risk_type).format
93
77
  when :version
94
- Formatter::Version.new(dep.version, dep.latest_version).format
78
+ Formatter::Version.new(pkg.version, pkg.latest_version).format
95
79
  when :vulnerabilities
96
- Formatter::Vulnerability.new(dep.vulnerabilities).format
80
+ Formatter::Vulnerability.new(pkg.vulnerabilities).format
97
81
  when :latest_version_date
98
- Formatter::VersionDate.new(dep.latest_version_date).format
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) # rubocop:disable Metrics/MethodLength
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
- @dependencies.map { |gem| puts gem.to_csv(value_fields) }
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 './const'
1
+ require_relative 'const/time'
2
2
 
3
3
  module Package
4
4
  module Audit
5
5
  class RiskCalculator
6
- def initialize(dependency)
7
- @dependency = dependency
6
+ def initialize(pkg)
7
+ @pkg = pkg
8
8
  end
9
9
 
10
10
  def find
11
- vulnerability_risk = assess_vulnerability_risk
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
- risk = [vulnerability_risk, deprecation_risk, version_risk].max
16
- risk = [risk, Risk.new(Enum::RiskType::MEDIUM, risk.explanation)].min unless risk.nil? || production_dependency?
17
- risk
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 assess_vulnerability_risk # rubocop:disable Metrics/MethodLength
23
- if (@dependency.vulnerabilities & [
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
- elsif @dependency.vulnerabilities.include? Enum::VulnerabilityType::MEDIUM
30
- Risk.new(Enum::RiskType::MEDIUM, Enum::RiskExplanation::VULNERABILITY)
31
- elsif @dependency.vulnerabilities.include? Enum::VulnerabilityType::LOW
32
- Risk.new(Enum::RiskType::LOW, Enum::RiskExplanation::VULNERABILITY)
33
- else
34
- Risk.new(Enum::RiskType::NONE)
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 assess_version_risk
39
- if (@dependency.version.split('.').first || '') < (@dependency.latest_version.split('.').first || '')
40
- Risk.new(Enum::RiskType::MEDIUM, Enum::RiskExplanation::OUTDATED_BY_MAJOR_VERSION)
41
- elsif @dependency.version < @dependency.latest_version
42
- Risk.new(Enum::RiskType::LOW, Enum::RiskExplanation::OUTDATED)
43
- else
44
- Risk.new(Enum::RiskType::NONE)
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 assess_deprecation_risk
49
- seconds_since_date = (Time.now - Time.parse(@dependency.latest_version_date)).to_i
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 @dependency.version == @dependency.latest_version &&
52
- seconds_since_date >= Const::SECONDS_ELAPSED_TO_BE_OUTDATED
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
- @dependency.groups.none? || (@dependency.groups & [Enum::Environment::DEFAULT,
61
- Enum::Environment::PRODUCTION]).any?
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 '../dependency'
2
- require_relative './gem_meta_data'
3
- require_relative './vulnerability_finder'
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.ui.silence { Bundler.definition.resolve }
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.ui.silence do
17
- Bundler.load.dependencies.to_h { |dep| [dep.name, dep] }
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 './bundler_specs'
2
- require_relative './../enum/risk_type'
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 self.all
9
- specs = BundlerSpecs.gemfile
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 self.deprecated
16
- specs = BundlerSpecs.gemfile
17
- dependencies = specs.map { |spec| Dependency.new(spec.name, spec.version) }
18
-
19
- GemMetaData.new(dependencies).fetch.filter do |dep|
20
- dep.risk.explanation == Enum::RiskExplanation::POTENTIAL_DEPRECATION
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 self.outdated(include_implicit: false)
25
- specs = include_implicit ? BundlerSpecs.all : BundlerSpecs.gemfile
26
- dependencies = specs.map { |spec| Dependency.new(spec.name, spec.version) }
27
-
28
- GemMetaData.new(dependencies).fetch.filter do |dep|
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 self.vulnerable
34
- dependencies = VulnerabilityFinder.run
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
- GemMetaData.new(dependencies).fetch.filter do |dep|
37
- dep.risk.explanation == Enum::RiskExplanation::VULNERABILITY
38
- end.sort_by(&:name).uniq(&:name)
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 '../dependency'
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(dependencies)
8
- @dependencies = dependencies
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
- @dependencies.each do |dep|
24
- gem_dependency = Gem::Dependency.new dep.name, ">= #{dep.version}"
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(dep.version)
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[dep.name] = dep
39
- dep.update latest_version: latest_version.to_s,
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 self.run # rubocop:disable Metrics/AbcSize
8
- gem_hash = {}
9
- json_str = `bundle exec bundle-audit check --update --quiet --format json`
10
- json_results = JSON.parse(json_str, symbolize_names: true)[:results]
11
- json_results.each do |result|
12
- gem_name = result[:gem][:name]
13
- vulnerability = result[:advisory][:criticality] || Enum::VulnerabilityType::UNKNOWN
14
- unless gem_hash.key? gem_name
15
- gem_hash[gem_name] = Dependency.new result[:gem][:name], result[:gem][:version]
16
- end
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
- gem_hash.values
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