package-audit 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/lib/package/audit/cli.rb +14 -57
  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/version.rb +6 -5
  13. data/lib/package/audit/formatter/version_date.rb +2 -2
  14. data/lib/package/audit/formatter/vulnerability.rb +1 -1
  15. data/lib/package/audit/npm/node_collection.rb +64 -0
  16. data/lib/package/audit/npm/npm_meta_data.rb +41 -0
  17. data/lib/package/audit/npm/vulnerability_finder.rb +43 -0
  18. data/lib/package/audit/npm/yarn_lock_parser.rb +42 -0
  19. data/lib/package/audit/{dependency.rb → package.rb} +38 -4
  20. data/lib/package/audit/{dependency_printer.rb → printer.rb} +29 -47
  21. data/lib/package/audit/risk_calculator.rb +49 -34
  22. data/lib/package/audit/ruby/bundler_specs.rb +1 -1
  23. data/lib/package/audit/ruby/gem_collection.rb +14 -18
  24. data/lib/package/audit/ruby/gem_meta_data.rb +11 -9
  25. data/lib/package/audit/ruby/vulnerability_finder.rb +22 -12
  26. data/lib/package/audit/util/summary_printer.rb +26 -19
  27. data/lib/package/audit/version.rb +1 -1
  28. data/sig/package/audit/command_service.rbs +29 -0
  29. data/sig/package/audit/const/cmd.rbs +14 -0
  30. data/sig/package/audit/const/fields.rbs +13 -0
  31. data/sig/package/audit/const/file.rbs +13 -0
  32. data/sig/package/audit/const/time.rbs +11 -0
  33. data/sig/package/audit/duplicate_package_merger.rbs +11 -0
  34. data/sig/package/audit/enum/vulnerability_type.rbs +1 -0
  35. data/sig/package/audit/npm/node_collection.rbs +29 -0
  36. data/sig/package/audit/npm/npm_meta_data.rbs +19 -0
  37. data/sig/package/audit/npm/vulnerability_finder.rbs +20 -0
  38. data/sig/package/audit/npm/yarn_lock_parser.rbs +19 -0
  39. data/sig/package/audit/{dependency.rbs → package.rbs} +14 -4
  40. data/sig/package/audit/printer.rbs +24 -0
  41. data/sig/package/audit/risk_calculator.rbs +6 -6
  42. data/sig/package/audit/ruby/gem_collection.rbs +4 -4
  43. data/sig/package/audit/ruby/gem_meta_data.rbs +7 -8
  44. data/sig/package/audit/ruby/vulnerability_finder.rbs +7 -1
  45. data/sig/package/audit/util/summary_printer.rbs +3 -5
  46. metadata +27 -9
  47. data/lib/package/audit/const.rb +0 -5
  48. data/sig/const.rbs +0 -5
  49. data/sig/package/audit/dependency_printer.rbs +0 -24
@@ -0,0 +1,42 @@
1
+ module Package
2
+ module Audit
3
+ module Npm
4
+ class YarnLockParser
5
+ def initialize(yarn_lock_path)
6
+ @yarn_lock_path = File.read(yarn_lock_path)
7
+ end
8
+
9
+ def fetch(default_deps, dev_deps)
10
+ pkgs = []
11
+ default_deps.merge(dev_deps).each do |dep_name, expected_version|
12
+ pkg_block = fetch_package_block(dep_name, expected_version)
13
+ version = fetch_package_version(dep_name, pkg_block)
14
+ pks = Package.new(dep_name.to_s, version)
15
+ pks.update groups: dev_deps.key?(dep_name) ? %i[development] : %i[default development]
16
+ pkgs << pks
17
+ end
18
+ pkgs
19
+ end
20
+
21
+ private
22
+
23
+ def fetch_package_block(dep_name, expected_version)
24
+ regex = /#{Regexp.escape(dep_name)}@#{Regexp.escape(expected_version)}.*?:.*?(\n\n|\z)/m
25
+ blocks = @yarn_lock_path.match(regex)
26
+ if blocks.nil? || blocks[0].nil?
27
+ raise NoMatchingPatternError, "Unable to find #{dep_name} in #{@yarn_lock_path}"
28
+ end
29
+
30
+ blocks[0] || ''
31
+ end
32
+
33
+ def fetch_package_version(dep_name, pkg_block)
34
+ version = pkg_block.match(/version "(.*?)"/)&.[](1)
35
+ raise NoMatchingPatternError, "Unable to find #{dep_name} version in #{@yarn_lock_path}" if version.nil?
36
+
37
+ version || '0.0.0.0'
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -6,15 +6,21 @@ require_relative './enum/risk_explanation'
6
6
 
7
7
  module Package
8
8
  module Audit
9
- class Dependency
9
+ class Package
10
10
  attr_reader :name, :version
11
11
  attr_accessor :groups, :version_date, :latest_version, :latest_version_date, :vulnerabilities
12
12
 
13
- def initialize(name, version)
13
+ def initialize(name, version, **attr)
14
14
  @name = name.to_s
15
15
  @version = version.to_s
16
16
  @groups = []
17
17
  @vulnerabilities = []
18
+ @risks = []
19
+ update(**attr)
20
+ end
21
+
22
+ def full_name
23
+ "#{name}@#{version}"
18
24
  end
19
25
 
20
26
  def update(**attr)
@@ -22,11 +28,15 @@ module Package
22
28
  end
23
29
 
24
30
  def risk
25
- @risk ||= RiskCalculator.new(self).find || Risk.new(Enum::RiskType::NONE)
31
+ risks.max || Risk.new(Enum::RiskType::NONE)
32
+ end
33
+
34
+ def risks
35
+ RiskCalculator.new(self).find
26
36
  end
27
37
 
28
38
  def risk?
29
- risk.type != Enum::RiskType::NONE
39
+ risks.any?
30
40
  end
31
41
 
32
42
  def group_list
@@ -45,6 +55,30 @@ module Package
45
55
  risk.explanation
46
56
  end
47
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
+
48
82
  def to_csv(fields)
49
83
  fields.map { |field| send(field) }.join(',')
50
84
  end
@@ -1,3 +1,4 @@
1
+ require_relative './const/fields'
1
2
  require_relative './formatter/risk'
2
3
  require_relative './formatter/version'
3
4
  require_relative './formatter/version_date'
@@ -5,61 +6,44 @@ 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,4 +1,4 @@
1
- require_relative '../dependency'
1
+ require_relative '../package'
2
2
  require_relative './gem_meta_data'
3
3
  require_relative './vulnerability_finder'
4
4
 
@@ -1,5 +1,6 @@
1
1
  require_relative './bundler_specs'
2
2
  require_relative './../enum/risk_type'
3
+ require_relative '../duplicate_package_merger'
3
4
 
4
5
  module Package
5
6
  module Audit
@@ -7,35 +8,30 @@ module Package
7
8
  class GemCollection
8
9
  def self.all
9
10
  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)
11
+ pkgs = specs.map { |spec| Package.new(spec.name, spec.version) }
12
+ vulnerable_pkgs = VulnerabilityFinder.new.run
13
+ pkgs = GemMetaData.new(pkgs + vulnerable_pkgs).fetch.filter(&:risk?)
14
+ DuplicatePackageMerger.new(pkgs).run
13
15
  end
14
16
 
15
17
  def self.deprecated
16
18
  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)
19
+ pkgs = specs.map { |spec| Package.new(spec.name, spec.version) }
20
+ pkgs = GemMetaData.new(pkgs).fetch.filter(&:deprecated?)
21
+ DuplicatePackageMerger.new(pkgs).run
22
22
  end
23
23
 
24
24
  def self.outdated(include_implicit: false)
25
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)
26
+ pkgs = specs.map { |spec| Package.new(spec.name, spec.version) }
27
+ pkgs = GemMetaData.new(pkgs).fetch.filter(&:outdated?)
28
+ DuplicatePackageMerger.new(pkgs).run
31
29
  end
32
30
 
33
31
  def self.vulnerable
34
- dependencies = VulnerabilityFinder.run
35
-
36
- GemMetaData.new(dependencies).fetch.filter do |dep|
37
- dep.risk.explanation == Enum::RiskExplanation::VULNERABILITY
38
- end.sort_by(&:name).uniq(&:name)
32
+ pkgs = VulnerabilityFinder.new.run
33
+ pkgs = GemMetaData.new(pkgs).fetch.filter(&:vulnerable?)
34
+ DuplicatePackageMerger.new(pkgs).run
39
35
  end
40
36
  end
41
37
  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,32 @@
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
9
+ @vuln_hash = {}
10
+ end
11
+
12
+ def run
13
+ json_result = `#{Const::Cmd::BUNDLE_AUDIT_JSON}`
14
+ vulnerability_json_array = JSON.parse(json_result, symbolize_names: true)[:results]
15
+ vulnerability_json_array.each do |vulnerability_json|
16
+ update_meta_data(vulnerability_json)
18
17
  end
19
- gem_hash.values
18
+ @vuln_hash.values
19
+ end
20
+
21
+ private
22
+
23
+ def update_meta_data(json)
24
+ name = json[:gem][:name]
25
+ version = json[:gem][:version]
26
+ full_name = "#{name}@#{version}"
27
+ vulnerability = json[:advisory][:criticality] || Enum::VulnerabilityType::UNKNOWN
28
+ @vuln_hash[full_name] = Package.new(name, version) unless @vuln_hash.key? full_name
29
+ @vuln_hash[full_name].update vulnerabilities: @vuln_hash[full_name].vulnerabilities + [vulnerability]
20
30
  end
21
31
  end
22
32
  end
@@ -1,4 +1,4 @@
1
- require_relative '../const'
1
+ require_relative '../const/time'
2
2
  require_relative './bash_color'
3
3
 
4
4
  module Package
@@ -12,44 +12,51 @@ module Package
12
12
  end
13
13
 
14
14
  def self.deprecated
15
- puts Util::BashColor.blue("\nAlthough gems listed above have no recent updates, they may not be deprecated.")
16
- puts Util::BashColor.blue("Please contact the gem author for more information about its status.\n")
15
+ puts Util::BashColor.blue('Although the packages above have no recent updates, they may not be deprecated.')
16
+ puts Util::BashColor.blue("Please contact the package author for more information about its status.\n")
17
17
  end
18
18
 
19
- def self.outdated
20
- printf("\n%<info>s\n%<cmd>s\n\n",
21
- info: Util::BashColor.blue('To show both Gemfile gems and their dependencies run:'),
22
- cmd: Util::BashColor.magenta(' > bundle exec package-audit outdated --include-implicit'))
19
+ def self.vulnerable(package_type, cmd)
20
+ printf("%<info>s\n%<cmd>s\n\n",
21
+ info: Util::BashColor.blue("To get more information about the #{package_type} vulnerabilities run:"),
22
+ cmd: Util::BashColor.magenta(" > #{cmd}"))
23
23
  end
24
24
 
25
- def self.vulnerable
26
- printf("\n%<info>s\n%<cmd>s\n\n",
27
- info: Util::BashColor.blue('To get more information about the vulnerabilities run:'),
28
- cmd: Util::BashColor.magenta(' > bundle exec bundle-audit check --update'))
25
+ def self.total(package_type, pkgs)
26
+ puts Util::BashColor.cyan("Found a total of #{pkgs.length} #{package_type}s.\n")
29
27
  end
30
28
 
31
- def self.total(num)
32
- puts Util::BashColor.cyan("\nFound a total of #{num} gems.")
29
+ def self.statistics(package_type, pkgs)
30
+ outdated = pkgs.count(&:outdated?)
31
+ deprecated = pkgs.count(&:deprecated?)
32
+ vulnerable = pkgs.count(&:vulnerable?)
33
+
34
+ vulnerabilities = pkgs.sum { |pkg| pkg.vulnerabilities.length }
35
+
36
+ puts Util::BashColor.cyan("Found a total of #{pkgs.length} #{package_type}s.\n" \
37
+ "#{vulnerable} vulnerable (#{vulnerabilities} vulnerabilities), " \
38
+ "#{outdated} outdated, #{deprecated} deprecated.\n")
33
39
  end
34
40
 
35
41
  def self.risk # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
36
- puts Util::BashColor.blue('1. Check if the dependency has a security vulnerability.')
42
+ puts Util::BashColor.blue('1. Check if the package has a security vulnerability.')
37
43
  puts ' If yes, the following vulnerability -> risk mapping is used:'
38
44
  puts " - #{Util::BashColor.red('unknown')} vulnerability\t-> #{Util::BashColor.red('high')} risk"
39
45
  puts " - #{Util::BashColor.red('critical')} vulnerability\t-> #{Util::BashColor.red('high')} risk"
40
46
  puts " - #{Util::BashColor.red('high')} vulnerability\t-> #{Util::BashColor.red('high')} risk"
41
47
  puts " - #{Util::BashColor.orange('medium')} vulnerability\t-> #{Util::BashColor.orange('medium')} risk"
48
+ puts " - #{Util::BashColor.orange('moderate')} vulnerability\t-> #{Util::BashColor.orange('medium')} risk" # rubocop:disable Layout/LineLength
42
49
  puts " - #{Util::BashColor.yellow('low')} vulnerability\t-> #{Util::BashColor.yellow('low')} risk"
43
50
 
44
51
  puts
45
52
 
46
- puts Util::BashColor.blue('2. Check the dependency for potential deprecation.')
47
- puts " If no new releases by author for at least #{Const::YEARS_ELAPSED_TO_BE_OUTDATED} years:"
53
+ puts Util::BashColor.blue('2. Check the package for potential deprecation.')
54
+ puts " If no new releases by author for at least #{Const::Time::YEARS_ELAPSED_TO_BE_OUTDATED} years:"
48
55
  puts " - assign the risk to\t-> #{Util::BashColor.orange('medium')} risk"
49
56
 
50
57
  puts
51
58
 
52
- puts Util::BashColor.blue('3. Check if a newer version of the dependency is available.')
59
+ puts Util::BashColor.blue('3. Check if a newer version of the package is available.')
53
60
 
54
61
  puts ' If yes, assign risk as follows:'
55
62
  puts " - #{Util::BashColor.orange('major version')} mismatch\t-> #{Util::BashColor.orange('medium')} risk" # rubocop:disable Layout/LineLength
@@ -65,8 +72,8 @@ module Package
65
72
 
66
73
  puts
67
74
 
68
- puts Util::BashColor.blue('5. Check whether the dependency is used in production or not.')
69
- puts ' If a dependency is limited to a non-production environment:'
75
+ puts Util::BashColor.blue('5. Check whether the package is used in production or not.')
76
+ puts ' If a package is limited to a non-production environment:'
70
77
  puts " - cap risk severity to\t -> #{Util::BashColor.orange('medium')} risk"
71
78
  end
72
79
  end
@@ -1,5 +1,5 @@
1
1
  module Package
2
2
  module Audit
3
- VERSION = '0.1.0'
3
+ VERSION = '0.2.0'
4
4
  end
5
5
  end
@@ -0,0 +1,29 @@
1
+ module Package
2
+ module Audit
3
+ class CommandService
4
+ NODE_MODULE: String
5
+ RUBY_GEM: String
6
+
7
+ @dir: String
8
+ @options: Hash[Symbol, untyped]
9
+
10
+ def initialize: (String, Hash[Symbol, untyped]) -> void
11
+
12
+ def all: -> bool
13
+
14
+ def deprecated: -> bool
15
+
16
+ def outdated: -> bool
17
+
18
+ def vulnerable: -> bool
19
+
20
+ private
21
+
22
+ def node?: -> bool?
23
+
24
+ def print_success_message: (String) -> void
25
+
26
+ def ruby?: -> bool?
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,14 @@
1
+ module Package
2
+ module Audit
3
+ module Const
4
+ module Cmd
5
+ BUNDLE_AUDIT: String
6
+ BUNDLE_AUDIT_JSON: String
7
+ NPM_AUDIT: String
8
+ NPM_AUDIT_JSON: String
9
+ YARN_AUDIT: String
10
+ YARN_AUDIT_JSON: String
11
+ end
12
+ end
13
+ end
14
+ end