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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 40db656091a8559c095240ab373f96c905cc6b78898717c1932dff93428d8b6e
4
- data.tar.gz: a3220c0f098a071c3cd429914b40bda20826e3157035e778513410c01bd1a566
3
+ metadata.gz: 55535a9ef08ce99d8cdacbfe15e0ec11e770f4803e4ef212d9ff4565111c89a6
4
+ data.tar.gz: 3f1efc9489c0ac9139f94bb7ed66d8ccee255e9c61f79440c899c0dcc4cd5885
5
5
  SHA512:
6
- metadata.gz: 348811b4789ffb9dd446a15c6a73819adcf196737a3949689a18457f2b368daa7c2830f10063208e7073833c7b512feb1ddb36bf60e8376f2d84a2d40bd7d9e3
7
- data.tar.gz: b3a779536d324ea28160defac771a242020168f4668ac8e7540d89e6c5db7f833c0c9caa435b8bb50b2cb807f8e211e2257d33e64aa80c61affd271e1a3fd24e
6
+ metadata.gz: 9c7ee67ccfadae05b1ec3a7e53d77ed8d977ae43bd54bbc0b95dee86e33ddceb87592089abc7a1410dba71c00802cc6172c3849d23e789878a9e8e14a1de2d96
7
+ data.tar.gz: 4ff43ca9c9e7bc410b545cde33b77798bfe71d40ebf92874235a695fb1862b2fa826849ce62f67cab831dfa22b987506248754e0a03b3827612e17b12df3f9be
@@ -1,9 +1,11 @@
1
- require_relative './const'
1
+ require_relative './const/time'
2
2
  require_relative './version'
3
3
  require_relative './util/summary_printer'
4
4
  require_relative './ruby/bundler_specs'
5
- require_relative './dependency_printer'
5
+ require_relative './printer'
6
6
  require_relative './ruby/gem_collection'
7
+ require_relative './npm/node_collection'
8
+ require_relative './command_service'
7
9
 
8
10
  require 'json'
9
11
  require 'thor'
@@ -15,81 +17,44 @@ module Package
15
17
 
16
18
  map '--version' => :version
17
19
 
18
- desc 'report', 'Show a report of potentially deprecated, outdated or vulnerable gems'
20
+ desc 'report', 'Show a report of potentially deprecated, outdated or vulnerable packages'
19
21
  method_option :csv, type: :boolean, default: false, desc: 'Output using comma separated values (CSV)'
20
22
  method_option :'exclude-headers', type: :boolean, default: false, desc: 'Hide headers if when using CSV'
21
23
 
22
24
  def report
23
25
  within_rescue_block do
24
- gems = Ruby::GemCollection.all
25
- DependencyPrinter.new(gems, options).print
26
-
27
- if gems.any?
28
- Util::SummaryPrinter.total(gems.length) unless options[:csv]
29
- Util::SummaryPrinter.report unless options[:csv]
30
- exit 1
31
- else
32
- exit_with_success 'There are no deprecated, outdated or vulnerable gems!'
33
- end
26
+ exit CommandService.new(Dir.pwd, options).all
34
27
  end
35
28
  end
36
29
 
37
- desc 'deprecated', "Show gems with no updates by author for at least #{Const::YEARS_ELAPSED_TO_BE_OUTDATED} years"
30
+ desc 'deprecated',
31
+ "Show packages with no updates by author for at least #{Const::Time::YEARS_ELAPSED_TO_BE_OUTDATED} years"
38
32
  method_option :csv, type: :boolean, default: false, desc: 'Output using comma separated values (CSV)'
39
33
  method_option :'exclude-headers', type: :boolean, default: false, desc: 'Hide headers if when using CSV'
40
34
 
41
35
  def deprecated
42
36
  within_rescue_block do
43
- gems = Ruby::GemCollection.deprecated
44
- DependencyPrinter.new(gems, options).print(%i[name version latest_version latest_version_date groups])
45
-
46
- if gems.any?
47
- Util::SummaryPrinter.total(gems.length) unless options[:csv]
48
- Util::SummaryPrinter.deprecated unless options[:csv]
49
- exit 1
50
- else
51
- exit_with_success 'No potential deprecated have been found!'
52
- end
37
+ exit CommandService.new(Dir.pwd, options).deprecated
53
38
  end
54
39
  end
55
40
 
56
- desc 'outdated', 'Show gems, and optionally their dependencies, that are out of date'
57
- method_option :'include-implicit', type: :boolean, default: false,
58
- desc: 'Only both gems specified in Gemfile and their dependencies'
41
+ desc 'outdated', 'Show packages that are out of date'
59
42
  method_option :csv, type: :boolean, default: false, desc: 'Output using comma separated values (CSV)'
60
43
  method_option :'exclude-headers', type: :boolean, default: false, desc: 'Hide headers if when using CSV'
61
44
 
62
- def outdated # rubocop:disable Metrics/AbcSize
45
+ def outdated
63
46
  within_rescue_block do
64
- gems = Ruby::GemCollection.outdated(include_implicit: options[:'include-implicit'])
65
- DependencyPrinter.new(gems, options).print(%i[name version latest_version latest_version_date groups])
66
-
67
- if gems.any?
68
- Util::SummaryPrinter.total(gems.length) unless options[:csv]
69
- Util::SummaryPrinter.outdated unless options[:'include-implicit'] || options[:csv]
70
- exit 1
71
- else
72
- exit_with_success 'All gems are at latest versions!'
73
- end
47
+ exit CommandService.new(Dir.pwd, options).outdated
74
48
  end
75
49
  end
76
50
 
77
- desc 'vulnerable', 'Show gems and their dependencies that have security vulnerabilities'
51
+ desc 'vulnerable', 'Show packages and their dependencies that have security vulnerabilities'
78
52
  method_option :csv, type: :boolean, default: false, desc: 'Output using comma separated values (CSV)'
79
53
  method_option :'exclude-headers', type: :boolean, default: false, desc: 'Hide headers if when using CSV'
80
54
 
81
55
  def vulnerable
82
56
  within_rescue_block do
83
- gems = Ruby::GemCollection.vulnerable
84
- DependencyPrinter.new(gems, options).print(%i[name version latest_version groups vulnerabilities])
85
-
86
- if gems.any?
87
- Util::SummaryPrinter.total(gems.length) unless options[:csv]
88
- Util::SummaryPrinter.vulnerable unless options[:csv]
89
- exit 1
90
- else
91
- exit_with_success 'No vulnerabilities found!'
92
- end
57
+ exit CommandService.new(Dir.pwd, options).vulnerable
93
58
  end
94
59
  end
95
60
 
@@ -112,9 +77,6 @@ module Package
112
77
  private
113
78
 
114
79
  def within_rescue_block
115
- raise "Gemfile was not found in #{Dir.pwd}/Gemfile" unless File.exist?("#{Dir.pwd}/Gemfile")
116
- raise "Gemfile.lock was not found in #{Dir.pwd}/Gemfile.lock" unless File.exist?("#{Dir.pwd}/Gemfile.lock")
117
-
118
80
  yield
119
81
  rescue StandardError => e
120
82
  exit_with_error "#{e.class}: #{e.message}"
@@ -124,11 +86,6 @@ module Package
124
86
  puts Util::BashColor.red msg
125
87
  exit 1
126
88
  end
127
-
128
- def exit_with_success(msg)
129
- puts Util::BashColor.green msg
130
- exit 0
131
- end
132
89
  end
133
90
  end
134
91
  end
@@ -0,0 +1,187 @@
1
+ require_relative './const/cmd'
2
+ require_relative './const/file'
3
+
4
+ module Package
5
+ module Audit
6
+ class CommandService # rubocop:disable Metrics/ClassLength
7
+ RUBY_GEM = 'ruby gem'
8
+ NODE_MODULE = 'node module'
9
+
10
+ def initialize(dir, options)
11
+ @dir = dir
12
+ @options = options
13
+ end
14
+
15
+ def all # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/PerceivedComplexity
16
+ pkgs = []
17
+
18
+ if ruby?
19
+ gems = Ruby::GemCollection.all
20
+ pkgs += gems
21
+ Printer.new(gems, @options).print(Const::Fields::REPORT)
22
+
23
+ unless @options[:csv]
24
+ if gems.any?
25
+ Util::SummaryPrinter.statistics(RUBY_GEM, gems)
26
+ Util::SummaryPrinter.vulnerable(RUBY_GEM, Const::Cmd::BUNDLE_AUDIT)
27
+ else
28
+ print_success_message "There are no deprecated, outdated or vulnerable #{RUBY_GEM}s!"
29
+ end
30
+ end
31
+ end
32
+
33
+ if node?
34
+ npms = Npm::NodeCollection.new(@dir).all
35
+ pkgs += npms
36
+ Printer.new(npms, @options).print(Const::Fields::REPORT)
37
+
38
+ unless @options[:csv]
39
+ if npms.any?
40
+ Util::SummaryPrinter.statistics(NODE_MODULE, npms)
41
+ Util::SummaryPrinter.vulnerable(NODE_MODULE, Const::Cmd::YARN_AUDIT)
42
+ else
43
+ print_success_message "There are no deprecated, outdated or vulnerable #{NODE_MODULE}s!"
44
+ end
45
+ end
46
+ end
47
+
48
+ pkgs.any?
49
+ end
50
+
51
+ def vulnerable # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/PerceivedComplexity
52
+ pkgs = []
53
+
54
+ if ruby?
55
+ gems = Ruby::GemCollection.vulnerable
56
+ pkgs += gems
57
+ Printer.new(gems, @options).print(Const::Fields::VULNERABLE)
58
+
59
+ unless @options[:csv]
60
+ if gems.any?
61
+ Util::SummaryPrinter.total(RUBY_GEM, gems)
62
+ Util::SummaryPrinter.vulnerable(RUBY_GEM, Const::Cmd::BUNDLE_AUDIT)
63
+ else
64
+ print_success_message "There are no #{RUBY_GEM} vulnerabilities!"
65
+ end
66
+ end
67
+ end
68
+
69
+ if node?
70
+ npms = Npm::NodeCollection.new(@dir).vulnerable
71
+ pkgs += npms
72
+ Printer.new(npms, @options).print(Const::Fields::VULNERABLE)
73
+
74
+ unless @options[:csv]
75
+ if npms.any?
76
+ Util::SummaryPrinter.total(NODE_MODULE, npms)
77
+ Util::SummaryPrinter.vulnerable(NODE_MODULE, Const::Cmd::YARN_AUDIT)
78
+ else
79
+ print_success_message "There are no #{NODE_MODULE} vulnerabilities!"
80
+ end
81
+ end
82
+ end
83
+
84
+ pkgs.any?
85
+ end
86
+
87
+ def outdated # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/PerceivedComplexity
88
+ pkgs = []
89
+
90
+ if ruby?
91
+ gems = Ruby::GemCollection.outdated
92
+ pkgs += gems
93
+ Printer.new(gems, @options).print(Const::Fields::OUTDATED)
94
+
95
+ unless @options[:csv]
96
+ if gems.any?
97
+ Util::SummaryPrinter.total(RUBY_GEM, gems)
98
+ else
99
+ print_success_message "There are no outdated #{RUBY_GEM}s!"
100
+ end
101
+ end
102
+ end
103
+
104
+ if node?
105
+ npms = Npm::NodeCollection.new(@dir).outdated
106
+ pkgs += npms
107
+ Printer.new(npms, @options).print(Const::Fields::OUTDATED)
108
+
109
+ unless @options[:csv]
110
+ if npms.any?
111
+ Util::SummaryPrinter.total(NODE_MODULE, npms)
112
+ else
113
+ print_success_message "There are no outdated #{NODE_MODULE}s!"
114
+ end
115
+ end
116
+ end
117
+
118
+ pkgs.any?
119
+ end
120
+
121
+ def deprecated # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/PerceivedComplexity
122
+ pkgs = []
123
+
124
+ if ruby?
125
+ gems = Ruby::GemCollection.deprecated
126
+ pkgs += gems
127
+ Printer.new(gems, @options).print(Const::Fields::OUTDATED)
128
+
129
+ unless @options[:csv]
130
+ if gems.any?
131
+ Util::SummaryPrinter.total(RUBY_GEM, gems)
132
+ Util::SummaryPrinter.deprecated
133
+ else
134
+ print_success_message "There are no potentially deprecated #{RUBY_GEM}s!"
135
+ end
136
+ end
137
+ end
138
+
139
+ if node?
140
+ npms = Npm::NodeCollection.new(@dir).deprecated
141
+ pkgs += npms
142
+ Printer.new(npms, @options).print(Const::Fields::OUTDATED)
143
+
144
+ unless @options[:csv]
145
+ if npms.any?
146
+ Util::SummaryPrinter.total(NODE_MODULE, npms)
147
+ Util::SummaryPrinter.deprecated
148
+ else
149
+ print_success_message "There are no potentially deprecated #{NODE_MODULE}s!"
150
+ end
151
+ end
152
+ end
153
+
154
+ pkgs.any?
155
+ end
156
+
157
+ private
158
+
159
+ def ruby?
160
+ gemfile_present = File.exist?("#{@dir}/#{Const::File::GEMFILE}")
161
+ gemfile_lock_present = File.exist?("#{@dir}/#{Const::File::GEMFILE_LOCK}")
162
+
163
+ if gemfile_present && gemfile_lock_present
164
+ true
165
+ elsif gemfile_present
166
+ raise "#{Const::File::GEMFILE_LOCK} was not found in #{@dir}/"
167
+ end
168
+ end
169
+
170
+ def node?
171
+ package_json_present = File.exist?("#{@dir}/#{Const::File::PACKAGE_JSON}")
172
+ package_lock_json_present = File.exist?("#{@dir}/#{Const::File::PACKAGE_LOCK_JSON}")
173
+ yarn_lock_present = File.exist?("#{@dir}/#{Const::File::YARN_LOCK}")
174
+
175
+ if package_json_present && (package_lock_json_present || yarn_lock_present)
176
+ true
177
+ elsif package_json_present
178
+ raise "#{Const::File::PACKAGE_LOCK_JSON} or #{Const::File::YARN_LOCK} was not found in #{@dir}/"
179
+ end
180
+ end
181
+
182
+ def print_success_message(msg)
183
+ puts Util::BashColor.green msg
184
+ end
185
+ end
186
+ end
187
+ end
@@ -0,0 +1,16 @@
1
+ module Package
2
+ module Audit
3
+ module Const
4
+ module Cmd
5
+ BUNDLE_AUDIT = 'bundle exec bundle-audit check --update'
6
+ BUNDLE_AUDIT_JSON = 'bundle exec bundle-audit check --update --quiet --format json'
7
+
8
+ NPM_AUDIT = 'npm audit'
9
+ NPM_AUDIT_JSON = 'npm audit --json'
10
+
11
+ YARN_AUDIT = 'yarn audit'
12
+ YARN_AUDIT_JSON = 'yarn audit --json'
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,36 @@
1
+ module Package
2
+ module Audit
3
+ module Const
4
+ module Fields
5
+ ALL = %i[
6
+ name
7
+ version
8
+ version_date
9
+ latest_version
10
+ latest_version_date
11
+ groups
12
+ vulnerabilities
13
+ risk_type
14
+ risk_explanation
15
+ ]
16
+
17
+ REPORT = %i[name version latest_version latest_version_date groups vulnerabilities risk_type risk_explanation]
18
+ VULNERABLE = %i[name version latest_version groups vulnerabilities]
19
+ OUTDATED = %i[name version latest_version latest_version_date groups]
20
+
21
+ # the names of these fields must match the instance variables in the Dependency class
22
+ HEADERS = {
23
+ name: 'Package',
24
+ version: 'Version',
25
+ version_date: 'Date',
26
+ latest_version: 'Latest',
27
+ latest_version_date: 'Latest Date',
28
+ groups: 'Groups',
29
+ vulnerabilities: 'Vulnerabilities',
30
+ risk_type: 'Risk',
31
+ risk_explanation: 'Risk Explanation'
32
+ }
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,13 @@
1
+ module Package
2
+ module Audit
3
+ module Const
4
+ module File
5
+ GEMFILE = 'Gemfile'
6
+ GEMFILE_LOCK = 'Gemfile.lock'
7
+ PACKAGE_JSON = 'package.json'
8
+ PACKAGE_LOCK_JSON = 'package-lock.json'
9
+ YARN_LOCK = 'yarn.lock'
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,11 @@
1
+ module Package
2
+ module Audit
3
+ module Const
4
+ module Time
5
+ SECONDS_PER_YEAR = 31_556_952 # length of a gregorian year (365.2425 days)
6
+ YEARS_ELAPSED_TO_BE_OUTDATED = 2
7
+ SECONDS_ELAPSED_TO_BE_OUTDATED = SECONDS_PER_YEAR * YEARS_ELAPSED_TO_BE_OUTDATED
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,26 @@
1
+ module Package
2
+ module Audit
3
+ class DuplicatePackageMerger
4
+ def initialize(pkgs)
5
+ @pkgs = pkgs.sort_by(&:full_name)
6
+ end
7
+
8
+ def run # rubocop:disable Metrics/AbcSize
9
+ pkgs = @pkgs.take(1)
10
+
11
+ @pkgs[1..]&.each do |curr|
12
+ prev = pkgs[-1]
13
+ if curr.full_name == prev.full_name
14
+ prev.update(groups: prev.groups | curr.groups,
15
+ vulnerabilities: prev.vulnerabilities + curr.vulnerabilities,
16
+ risks: prev.risks + curr.risks)
17
+ else
18
+ pkgs << curr
19
+ end
20
+ end
21
+
22
+ pkgs
23
+ end
24
+ end
25
+ end
26
+ end
@@ -1,5 +1,3 @@
1
- require_relative '../const'
2
-
3
1
  module Package
4
2
  module Audit
5
3
  module Enum
@@ -1,10 +1,10 @@
1
- require_relative '../const'
1
+ require_relative '../const/time'
2
2
 
3
3
  module Package
4
4
  module Audit
5
5
  module Enum
6
6
  module RiskExplanation
7
- POTENTIAL_DEPRECATION = "no updates by author in over #{Const::YEARS_ELAPSED_TO_BE_OUTDATED} years"
7
+ POTENTIAL_DEPRECATION = "no updates by author in over #{Const::Time::YEARS_ELAPSED_TO_BE_OUTDATED} years"
8
8
  OUTDATED_BY_MAJOR_VERSION = 'behind by a major version'
9
9
  OUTDATED = 'not at latest version'
10
10
  VULNERABILITY = 'security vulnerability'
@@ -5,6 +5,7 @@ module Package
5
5
  NONE = 'none'
6
6
  LOW = 'low'
7
7
  MEDIUM = 'medium'
8
+ MODERATE = 'moderate'
8
9
  HIGH = 'high'
9
10
  CRITICAL = 'critical'
10
11
  UNKNOWN = 'unknown'
@@ -12,16 +12,17 @@ module Package
12
12
  end
13
13
 
14
14
  def format # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
15
+ version_parts = @curr.split('.').map(&:to_i)
16
+ latest_version_parts = @target.split('.').map(&:to_i)
15
17
  curr_tokens = @curr.split('.')
16
- target_tokens = @target.split('.')
17
18
 
18
- if curr_tokens[0] && curr_tokens[0] < target_tokens[0]
19
+ if (version_parts.first || 0) < (latest_version_parts.first || 0)
19
20
  Util::BashColor.orange(@curr)
20
- elsif curr_tokens[1] && curr_tokens[1] < target_tokens[1]
21
+ elsif version_parts[1] && latest_version_parts[1] && version_parts[1] < latest_version_parts[1]
21
22
  "#{curr_tokens[0]}.#{Util::BashColor.yellow(curr_tokens[1..]&.join('.'))}"
22
- elsif curr_tokens[2] && curr_tokens[2] < target_tokens[2]
23
+ elsif version_parts[2] && latest_version_parts[2] && version_parts[2] < latest_version_parts[2]
23
24
  "#{curr_tokens[0..1]&.join('.')}.#{Util::BashColor.green(curr_tokens[2..]&.join('.'))}"
24
- elsif curr_tokens[3] && curr_tokens[3] < target_tokens[3]
25
+ elsif version_parts[3] && latest_version_parts[3] && version_parts[3] < latest_version_parts[3]
25
26
  "#{curr_tokens[0..2]&.join('.')}.#{Util::BashColor.green(curr_tokens[3])}"
26
27
  else
27
28
  @curr
@@ -1,5 +1,5 @@
1
- require_relative '../const'
2
1
  require_relative './base'
2
+ require_relative '../const/time'
3
3
  require_relative '../util/bash_color'
4
4
 
5
5
  require 'time'
@@ -16,7 +16,7 @@ module Package
16
16
  def format
17
17
  seconds_since_date = (Time.now - Time.parse(@date)).to_i
18
18
 
19
- if seconds_since_date >= Const::SECONDS_ELAPSED_TO_BE_OUTDATED
19
+ if seconds_since_date >= Const::Time::SECONDS_ELAPSED_TO_BE_OUTDATED
20
20
  Util::BashColor.yellow(@date)
21
21
  else
22
22
  @date
@@ -16,7 +16,7 @@ module Package
16
16
  case vulnerability
17
17
  when Enum::VulnerabilityType::UNKNOWN, Enum::VulnerabilityType::CRITICAL, Enum::VulnerabilityType::HIGH
18
18
  Util::BashColor.red(vulnerability)
19
- when Enum::VulnerabilityType::MEDIUM
19
+ when Enum::VulnerabilityType::MEDIUM, Enum::VulnerabilityType::MODERATE
20
20
  Util::BashColor.orange(vulnerability)
21
21
  when Enum::VulnerabilityType::LOW
22
22
  Util::BashColor.yellow(vulnerability)
@@ -0,0 +1,64 @@
1
+ require_relative './yarn_lock_parser'
2
+ require_relative './npm_meta_data'
3
+ require_relative './vulnerability_finder'
4
+ require_relative '../duplicate_package_merger'
5
+
6
+ module Package
7
+ module Audit
8
+ module Npm
9
+ class NodeCollection
10
+ PACKAGE_JSON = 'package.json'
11
+ PACKAGE_LOCK = 'package-lock.json'
12
+ YARN_LOCK = 'yarn.lock'
13
+
14
+ def initialize(dir)
15
+ @dir = dir
16
+ end
17
+
18
+ def all
19
+ implicit_pkgs = fetch_from_lock_file
20
+ vulnerable_pkgs = VulnerabilityFinder.new(implicit_pkgs).run
21
+ pkgs = NpmMetaData.new(vulnerable_pkgs + implicit_pkgs).fetch.filter(&:risk?)
22
+ DuplicatePackageMerger.new(pkgs).run
23
+ end
24
+
25
+ def deprecated
26
+ implicit_pkgs = fetch_from_lock_file
27
+ pkgs = NpmMetaData.new(implicit_pkgs).fetch.filter(&:deprecated?)
28
+ DuplicatePackageMerger.new(pkgs).run
29
+ end
30
+
31
+ def outdated
32
+ implicit_pkgs = fetch_from_lock_file
33
+ pkgs = NpmMetaData.new(implicit_pkgs).fetch.filter(&:outdated?)
34
+ DuplicatePackageMerger.new(pkgs).run
35
+ end
36
+
37
+ def vulnerable
38
+ implicit_pkgs = fetch_from_lock_file
39
+ vulnerable_pkgs = VulnerabilityFinder.new(implicit_pkgs).run
40
+ pkgs = NpmMetaData.new(vulnerable_pkgs).fetch
41
+ DuplicatePackageMerger.new(pkgs).run
42
+ end
43
+
44
+ private
45
+
46
+ def fetch_from_package_json
47
+ package_json = JSON.parse(File.read("#{@dir}/#{PACKAGE_JSON}"), symbolize_names: true)
48
+ default_deps = package_json[:dependencies] || {}
49
+ dev_deps = package_json[:devDependencies] || {}
50
+ [default_deps, dev_deps]
51
+ end
52
+
53
+ def fetch_from_lock_file
54
+ default_deps, dev_deps = fetch_from_package_json
55
+ if File.exist?("#{@dir}/#{YARN_LOCK}")
56
+ YarnLockParser.new("#{@dir}/#{YARN_LOCK}").fetch(default_deps || {}, dev_deps || {})
57
+ else
58
+ []
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,41 @@
1
+ require 'json'
2
+ require 'net/http'
3
+
4
+ module Package
5
+ module Audit
6
+ module Npm
7
+ class NpmMetaData
8
+ REGISTRY_URL = 'https://registry.npmjs.org'
9
+
10
+ def initialize(packages)
11
+ @packages = packages
12
+ end
13
+
14
+ def fetch
15
+ threads = @packages.map do |package|
16
+ Thread.new do
17
+ response = Net::HTTP.get_response(URI.parse("#{REGISTRY_URL}/#{package.name}"))
18
+ raise response.error unless response.is_a?(Net::HTTPSuccess)
19
+
20
+ json_package = JSON.parse(response.body, symbolize_names: true)
21
+ update_meta_data(package, json_package)
22
+ end
23
+ end
24
+ threads.each(&:join)
25
+ @packages
26
+ end
27
+
28
+ private
29
+
30
+ def update_meta_data(package, json_data)
31
+ latest_version = json_data[:'dist-tags'][:latest]
32
+ version_date = json_data[:time][package.version.to_sym]
33
+ latest_version_date = json_data[:time][latest_version.to_sym]
34
+ package.update version_date: Time.parse(version_date).strftime('%Y-%m-%d'),
35
+ latest_version: latest_version,
36
+ latest_version_date: Time.parse(latest_version_date).strftime('%Y-%m-%d')
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,43 @@
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(pkgs)
11
+ @pkg_hash = pkgs.to_h { |pkg| [pkg.name, pkg] }
12
+ @vuln_hash = {}
13
+ end
14
+
15
+ def run
16
+ json_string_lines = `#{Const::Cmd::YARN_AUDIT_JSON}`
17
+ array = json_string_lines.scan(AUDIT_ADVISORY_REGEX)
18
+
19
+ vulnerability_json_array = JSON.parse("[#{array.join(',')}]", symbolize_names: true)
20
+ vulnerability_json_array.each do |vulnerability_json|
21
+ update_meta_data(vulnerability_json)
22
+ end
23
+ @vuln_hash.values
24
+ end
25
+
26
+ private
27
+
28
+ def update_meta_data(json) # rubocop:disable Metrics/AbcSize
29
+ parent_name = json[:data][:resolution][:path].split('>').first
30
+ advisory = json[:data][:advisory]
31
+ name = advisory[:module_name]
32
+ version = advisory[:findings][0][:version]
33
+ full_name = "#{name}@#{version}"
34
+ vulnerability = advisory[:severity] || Enum::VulnerabilityType::UNKNOWN
35
+
36
+ @vuln_hash[full_name] = Package.new(name, version) unless @vuln_hash.key? full_name
37
+ @vuln_hash[full_name].update vulnerabilities: @vuln_hash[full_name].vulnerabilities + [vulnerability]
38
+ @vuln_hash[full_name].update groups: @pkg_hash[parent_name].groups
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end