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.
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