package-audit 0.1.0 → 0.2.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 (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