package-audit 0.8.2 → 0.9.1

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7e0d76f96e0ad7b87f59b178565e72e9d52dddf9f7a579e016ff6779fac6d58a
4
- data.tar.gz: cb917258a2025355d73123cb76762254dd92bdac817c72375e4297fbe8cb6b8f
3
+ metadata.gz: f5f9a3b6c50fcbebe1832c0559eddcd5a4e99c94779d1a251ad00c540df6c6b6
4
+ data.tar.gz: 84c02793f8d4b6a07099179fa5deee0ee473541668eb4b9212ea62b0e61d37d5
5
5
  SHA512:
6
- metadata.gz: 280d5c3c95dd19ff526723fb7b9c5b387180c63a67d64eab8be9301512c9b3f5d4e252ba1e82317fb5b95e6f86380908344ea766de9ad4539fecd20afeadd3d7
7
- data.tar.gz: '09f40855f0197f00fa9fc05826da2ec42506fef23d387569dd108e4623488eb059982e47d7b0bb99f718de6e62140db3817b119b90aea473cc837a7ad2038b60'
6
+ metadata.gz: 197beea22db4bdc21106d235e6bf0d360d61c396498d516a1fbd8aa8cca2342348aa4bcdacbd08376199ad0ff0a0555f0e145a9d2d19d4a6e1e4f29a684c6a4f
7
+ data.tar.gz: 8540a1858d4ad3a094f20d77c174dafcbffe1b3af1fc1a9ead5806443efb314a7bbbcb77d7fb3ccc764aed0f921de9e71b588ea82283b06a4cf4bb1dfb3fc414
data/README.md CHANGED
@@ -4,18 +4,16 @@
4
4
  [![Ruby](https://img.shields.io/badge/Ruby-2.6%20to%204.0-CC342D?logo=ruby&logoColor=white)](https://github.com/vkononov/package-audit/blob/main/.github/workflows/test.yml)
5
5
  [![Test Matrix](https://img.shields.io/github/actions/workflow/status/vkononov/package-audit/test.yml?branch=main&label=Test%20Matrix&logo=github)](https://github.com/vkononov/package-audit/actions/workflows/test.yml)
6
6
  [![Lint](https://img.shields.io/github/actions/workflow/status/vkononov/package-audit/lint.yml?branch=main&label=Lint&logo=github)](https://github.com/vkononov/package-audit/actions/workflows/lint.yml)
7
- [![License](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
7
+ [![License](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/license/MIT)
8
8
 
9
9
  A useful tool for patch management and prioritization, `package-audit` produces a list of dependencies that are outdated, deprecated or have security vulnerabilities.
10
10
 
11
11
  Supports **Ruby 2.6 → latest**, with the full Ruby matrix tested daily in CI.
12
12
 
13
- [!["Buy Me A Coffee"](https://www.buymeacoffee.com/assets/img/custom_images/yellow_img.png)](https://www.buymeacoffee.com/vkononov)
14
-
15
13
  ## Supported Technologies
16
14
 
17
15
  * Ruby
18
- * Node (using Yarn)
16
+ * Node (using npm or Yarn)
19
17
 
20
18
  ## Report Example
21
19
 
@@ -75,6 +73,9 @@ Show only specific risk types:
75
73
  # Show only deprecated packages
76
74
  package-audit [DIR] --deprecated
77
75
 
76
+ # Show only outdated packages
77
+ package-audit [DIR] --outdated
78
+
78
79
  # Show only vulnerable packages
79
80
  package-audit [DIR] --vulnerable
80
81
 
@@ -93,24 +94,15 @@ package-audit [DIR] --skip-deprecated
93
94
  # Show everything except outdated packages
94
95
  package-audit [DIR] --skip-outdated
95
96
 
97
+ # Show everything except vulnerable packages
98
+ package-audit [DIR] --skip-vulnerable
99
+
96
100
  # Show only vulnerable packages (exclude deprecated and outdated)
97
101
  package-audit [DIR] --skip-deprecated --skip-outdated
98
102
  ```
99
103
 
100
104
  **Important:** Packages with multiple risk types are handled intelligently. For example, a package that is both outdated and vulnerable will still appear when using `--skip-outdated` because it has a vulnerability.
101
105
 
102
- ### Understanding the Flags Column
103
-
104
- The Flags column in the output shows which risk types apply to each package:
105
-
106
- - `⦗V··⦘` - Vulnerable (has security vulnerabilities)
107
- - `⦗·O·⦘` - Outdated (newer version available)
108
- - `⦗··D⦘` - Deprecated (no updates in 2+ years)
109
- - `⦗VO·⦘` - Both vulnerable and outdated
110
- - `⦗VOD⦘` - All three risk types apply
111
-
112
- The footer uses the same notation `⦗V⦘ulnerable, ⦗O⦘utdated, ⦗D⦘eprecated` as a legend.
113
-
114
106
  ### Scoping Options
115
107
 
116
108
  Filter by technology or group:
@@ -144,11 +136,14 @@ package-audit [DIR] --include-ignored
144
136
  ### Output Formats
145
137
 
146
138
  ```bash
147
- # CSV format
139
+ # CSV format (-f is an alias for --format)
148
140
  package-audit [DIR] --format csv
149
141
 
150
142
  # Markdown format
151
143
  package-audit [DIR] --format md
144
+
145
+ # CSV without the header row
146
+ package-audit [DIR] --format csv --exclude-headers
152
147
  ```
153
148
 
154
149
  ### Other Commands
@@ -157,7 +152,7 @@ package-audit [DIR] --format md
157
152
  # Show how risk is calculated
158
153
  package-audit risk
159
154
 
160
- # Show version
155
+ # Show version (-v and --version also work)
161
156
  package-audit version
162
157
  ```
163
158
 
@@ -172,6 +167,8 @@ package-audit help [COMMAND]
172
167
 
173
168
  The `package-audit` gem automatically searches for `.package-audit.yml` in the current directory or in the specified `DIR` if available. However, you have the option to override the default configuration file location by using the `--config` (or `-c`) flag.
174
169
 
170
+ When `--technology` is specified, stale exclusions are cleaned up only for the selected technologies. Configuration entries for other technologies are preserved.
171
+
175
172
  #### Below is an example of a configuration file:
176
173
 
177
174
  ```YAML
@@ -215,4 +212,4 @@ technology:
215
212
 
216
213
  ## License
217
214
 
218
- The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
215
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/license/MIT).
@@ -3,7 +3,7 @@ module Package
3
3
  module Const
4
4
  module Cmd
5
5
  BUNDLE_AUDIT = 'bundle-audit check --update'
6
- BUNDLE_AUDIT_JSON = 'bundle-audit check --update --quiet --format json "%s" 2>/dev/null'
6
+ BUNDLE_AUDIT_JSON = %w[bundle-audit check --update --quiet --format json].freeze
7
7
 
8
8
  NPM_AUDIT = 'npm audit'
9
9
  end
@@ -1,6 +1,7 @@
1
1
  require_relative '../const/file'
2
2
  require_relative '../services/duplicate_package_merger'
3
3
  require_relative 'npm_meta_data'
4
+ require_relative 'package_lock_parser'
4
5
  require_relative 'vulnerability_finder'
5
6
  require_relative 'yarn_lock_parser'
6
7
 
@@ -73,22 +74,27 @@ module Package
73
74
  dependencies.reject { |_name, version| local_dependency?(version) }
74
75
  end
75
76
 
77
+ # Packages that live in the project itself (paths, workspaces, portals)
78
+ # are not published to the registry and cannot be audited against it.
76
79
  def local_dependency?(version)
77
- # Check for local file paths
78
- version.to_s.start_with?('file:', 'link:', './', '../') ||
79
- version.to_s.include?('file:') ||
80
- # Check for git repositories with local paths
81
- (version.to_s.start_with?('git+') && version.to_s.include?('file:'))
80
+ version.to_s.start_with?('file:', 'link:', 'workspace:', 'portal:', './', '../') ||
81
+ version.to_s.include?('file:')
82
82
  end
83
83
 
84
84
  def fetch_from_lock_file
85
85
  default_deps, dev_deps, resolutions = fetch_from_package_json
86
- if File.exist?("#{@dir}/#{Const::File::YARN_LOCK}")
87
- YarnLockParser.new("#{@dir}/#{Const::File::YARN_LOCK}").fetch(default_deps || {}, dev_deps || {},
88
- resolutions || {})
89
- else
90
- []
91
- end
86
+ parser = lock_file_parser
87
+ return [] if parser.nil?
88
+
89
+ parser.fetch(default_deps || {}, dev_deps || {}, resolutions || {})
90
+ end
91
+
92
+ def lock_file_parser
93
+ yarn_lock = "#{@dir}/#{Const::File::YARN_LOCK}"
94
+ return YarnLockParser.new(yarn_lock) if File.exist?(yarn_lock)
95
+
96
+ package_lock = "#{@dir}/#{Const::File::PACKAGE_LOCK_JSON}"
97
+ PackageLockParser.new(package_lock) if File.exist?(package_lock)
92
98
  end
93
99
  end
94
100
  end
@@ -0,0 +1,80 @@
1
+ require 'json'
2
+
3
+ require_relative '../enum/group'
4
+ require_relative '../models/package'
5
+
6
+ # Ruby 2.7 added this. A missing lockfile entry raises it, same as the yarn parser.
7
+ Object.const_set(:NoMatchingPatternError, Class.new(StandardError)) unless defined?(NoMatchingPatternError)
8
+
9
+ module Package
10
+ module Audit
11
+ module Npm
12
+ # Reads package-lock.json (lockfileVersion 1, 2 and 3) and resolves the
13
+ # installed version of each direct dependency.
14
+ class PackageLockParser
15
+ NODE_MODULES = 'node_modules/'
16
+
17
+ def initialize(package_lock_path)
18
+ @package_lock_path = package_lock_path
19
+ @lock = JSON.parse(File.read(package_lock_path))
20
+ end
21
+
22
+ def fetch(default_deps, dev_deps, _resolutions = {})
23
+ default_deps.merge(dev_deps).map do |dep_name, _expected_version|
24
+ version = direct_version(dep_name)
25
+ raise NoMatchingPatternError, "Unable to find \"#{dep_name}\" in #{@package_lock_path}" if version.nil?
26
+
27
+ pkg = Package.new(dep_name.to_s, version, 'node')
28
+ pkg.update groups: package_groups(dep_name, dev_deps)
29
+ pkg
30
+ end
31
+ end
32
+
33
+ # Every installed package, including transitive ones, as [name, version] pairs.
34
+ # Entries outside node_modules are workspace sources, not registry packages.
35
+ def all_packages
36
+ if @lock.key?('packages')
37
+ @lock['packages'].map do |path, entry|
38
+ next if entry['version'].nil? || !path.include?(NODE_MODULES)
39
+
40
+ [name_from_path(path), entry['version']]
41
+ end.compact
42
+ else
43
+ walk_v1(@lock['dependencies'] || {})
44
+ end
45
+ end
46
+
47
+ private
48
+
49
+ def direct_version(dep_name)
50
+ if @lock.key?('packages')
51
+ @lock.dig('packages', "#{NODE_MODULES}#{dep_name}", 'version')
52
+ else
53
+ @lock.dig('dependencies', dep_name, 'version')
54
+ end
55
+ end
56
+
57
+ def package_groups(dep_name, dev_deps)
58
+ if dev_deps.key?(dep_name)
59
+ [Enum::Group::DEV]
60
+ else
61
+ [Enum::Group::DEFAULT, Enum::Group::DEV]
62
+ end
63
+ end
64
+
65
+ # "node_modules/a/node_modules/@scope/b" => "@scope/b"
66
+ def name_from_path(path)
67
+ path.split(NODE_MODULES).last
68
+ end
69
+
70
+ def walk_v1(dependencies, packages = [])
71
+ dependencies.each do |name, entry|
72
+ packages << [name, entry['version']] unless entry['version'].nil?
73
+ walk_v1(entry['dependencies'] || {}, packages)
74
+ end
75
+ packages
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,138 @@
1
+ module Package
2
+ module Audit
3
+ module Npm
4
+ # Evaluates npm semver ranges (as used in registry advisories) against an
5
+ # installed version. Supports comparator sets joined by "||", hyphen
6
+ # ranges, caret and tilde ranges, and x-ranges. A range that cannot be
7
+ # understood is treated as matching, so an advisory is never silently
8
+ # dropped because of unusual syntax.
9
+ class SemverRange
10
+ WILDCARDS = %w[* x X].freeze
11
+
12
+ def initialize(range)
13
+ @range = range.to_s.strip
14
+ end
15
+
16
+ def satisfied_by?(version)
17
+ return true if @range.empty?
18
+
19
+ installed = parse_version(version)
20
+ return false if installed.nil?
21
+
22
+ @range.split('||').any? { |set| comparator_set_matches?(set.strip, installed) }
23
+ rescue ArgumentError
24
+ true
25
+ end
26
+
27
+ private
28
+
29
+ def comparator_set_matches?(set, installed)
30
+ return true if set.empty? || WILDCARDS.include?(set)
31
+
32
+ Gem::Requirement.new(*requirements_for(set)).satisfied_by?(installed)
33
+ end
34
+
35
+ def requirements_for(set)
36
+ return hyphen_requirements(*set.split(/\s+-\s+/, 2)) if set.match?(/\s-\s/)
37
+
38
+ set.split(/\s+/).flat_map { |comparator| comparator_requirements(comparator) }
39
+ end
40
+
41
+ def comparator_requirements(comparator)
42
+ case comparator
43
+ when /\A\^(.+)\z/ then caret_requirements(Regexp.last_match(1))
44
+ when /\A~>?(.+)\z/ then tilde_requirements(Regexp.last_match(1))
45
+ when /\A(>=|<=|>|<|=)\s*(.+)\z/ then operator_requirements(Regexp.last_match(1), Regexp.last_match(2))
46
+ else x_range_requirements(comparator)
47
+ end
48
+ end
49
+
50
+ def operator_requirements(operator, partial)
51
+ parts = version_parts(partial)
52
+ return [] if parts.empty?
53
+ return ["#{operator} #{normalize(partial)}"] if parts.length == 3 || partial.include?('-')
54
+
55
+ partial_operator_requirements(operator, fill(parts), bump(parts))
56
+ end
57
+
58
+ # ">=1.2" means >=1.2.0, ">1.2" means >=1.3.0, "<=1.2" means <1.3.0, "=1.2" means 1.2.x
59
+ def partial_operator_requirements(operator, lower, upper)
60
+ case operator
61
+ when '>' then [">= #{upper}"]
62
+ when '<=' then ["< #{upper}"]
63
+ when '=' then [">= #{lower}", "< #{upper}"]
64
+ else ["#{operator} #{lower}"]
65
+ end
66
+ end
67
+
68
+ def hyphen_requirements(low, high)
69
+ requirements = []
70
+ requirements << ">= #{normalize(low)}" unless version_parts(low).empty?
71
+ high_parts = version_parts(high)
72
+ return requirements if high_parts.empty?
73
+
74
+ requirements << (high_parts.length == 3 ? "<= #{normalize(high)}" : "< #{bump(high_parts)}")
75
+ end
76
+
77
+ def caret_requirements(partial)
78
+ parts = version_parts(partial)
79
+ return [] if parts.empty?
80
+
81
+ # Bump the leftmost non-zero part, or the last given part when all are zero.
82
+ index = parts.index(&:positive?) || (parts.length - 1)
83
+ [">= #{normalize(partial)}", "< #{bump(parts.first(index + 1))}"]
84
+ end
85
+
86
+ def tilde_requirements(partial)
87
+ parts = version_parts(partial)
88
+ return [] if parts.empty?
89
+
90
+ upper = parts.length == 1 ? "#{parts[0] + 1}.0.0" : "#{parts[0]}.#{parts[1] + 1}.0"
91
+ [">= #{normalize(partial)}", "< #{upper}"]
92
+ end
93
+
94
+ def x_range_requirements(partial)
95
+ parts = version_parts(partial)
96
+ return [] if parts.empty?
97
+ return ["= #{normalize(partial)}"] if parts.length == 3 && !partial.match?(/[xX*]/)
98
+
99
+ [">= #{fill(parts)}", "< #{bump(parts)}"]
100
+ end
101
+
102
+ # Numeric leading parts of a version, stopping at the first wildcard.
103
+ # "1.2.x" => [1, 2], "1.2.3-beta" => [1, 2, 3], "x" => []
104
+ def version_parts(partial)
105
+ partial.to_s.sub(/\A[v=]+/, '').sub(/[-+].*\z/, '').split('.').take_while do |part|
106
+ part.match?(/\A\d+\z/)
107
+ end.map(&:to_i)
108
+ end
109
+
110
+ def fill(parts)
111
+ (parts + [0, 0, 0]).first(3).join('.')
112
+ end
113
+
114
+ def bump(parts)
115
+ bumped = parts.dup
116
+ bumped[-1] += 1
117
+ fill(bumped)
118
+ end
119
+
120
+ # Gem::Version understands "1.2.3-beta.1" (as a prerelease) but not build
121
+ # metadata, a leading "v", or missing patch numbers.
122
+ def normalize(version)
123
+ cleaned = version.to_s.sub(/\A[v=]+/, '').sub(/\+.*\z/, '')
124
+ numeric, prerelease = cleaned.split('-', 2)
125
+ numeric = numeric.to_s
126
+ numeric = fill(numeric.split('.').map(&:to_i)) if numeric.match?(/\A\d+(\.\d+){0,2}\z/)
127
+ prerelease ? "#{numeric}-#{prerelease}" : numeric
128
+ end
129
+
130
+ def parse_version(version)
131
+ Gem::Version.new(normalize(version))
132
+ rescue ArgumentError
133
+ nil
134
+ end
135
+ end
136
+ end
137
+ end
138
+ end
@@ -4,6 +4,8 @@ require 'openssl'
4
4
 
5
5
  require_relative '../const/file'
6
6
  require_relative '../enum/vulnerability_type'
7
+ require_relative 'package_lock_parser'
8
+ require_relative 'semver_range'
7
9
 
8
10
  module Package
9
11
  module Audit
@@ -12,7 +14,7 @@ module Package
12
14
  BULK_ADVISORY_URL = 'https://registry.npmjs.org/-/npm/v1/security/advisories/bulk'
13
15
  BATCH_SIZE = 100
14
16
  TIMEOUT = 30
15
- VERSION_LINE = /^\s+"?version"?\s+"([^"]+)"/
17
+ VERSION_LINE = /^\s+"?version"?(?::\s*|\s+)["']?([^"'\s]+)["']?\s*$/
16
18
 
17
19
  def initialize(dir, pkgs)
18
20
  @dir = dir
@@ -51,9 +53,16 @@ module Package
51
53
  end
52
54
 
53
55
  def lockfile_packages
54
- path = "#{@dir}/#{Const::File::YARN_LOCK}"
55
- return [] unless File.exist?(path)
56
+ yarn_lock = "#{@dir}/#{Const::File::YARN_LOCK}"
57
+ return yarn_lockfile_packages(yarn_lock) if File.exist?(yarn_lock)
56
58
 
59
+ package_lock = "#{@dir}/#{Const::File::PACKAGE_LOCK_JSON}"
60
+ return PackageLockParser.new(package_lock).all_packages if File.exist?(package_lock)
61
+
62
+ []
63
+ end
64
+
65
+ def yarn_lockfile_packages(path)
57
66
  packages = []
58
67
  current_names = []
59
68
  File.foreach(path) do |line|
@@ -96,14 +105,13 @@ module Package
96
105
  uri = URI(BULK_ADVISORY_URL)
97
106
  response = make_request(uri, versions_by_name.to_json)
98
107
  unless response.is_a?(Net::HTTPSuccess)
99
- warn "Warning: Unable to fetch npm advisories (#{response.code} #{response.message})."
100
- return {}
108
+ raise "Unable to fetch npm advisories (#{response.code} #{response.message})"
101
109
  end
102
110
 
103
- JSON.parse(response.body)
104
- rescue StandardError => e
105
- warn "Warning: Error while fetching npm advisories: #{e.message}"
106
- {}
111
+ advisories = JSON.parse(response.body)
112
+ raise 'Invalid npm advisory response' unless advisories.is_a?(Hash)
113
+
114
+ advisories
107
115
  end
108
116
 
109
117
  def make_request(uri, body)
@@ -136,16 +144,7 @@ module Package
136
144
  end
137
145
 
138
146
  def version_matches?(version, vulnerable_versions)
139
- return true if vulnerable_versions.nil? || vulnerable_versions.empty?
140
-
141
- vulnerable_versions.split(/\s*\|\|\s*/).any? do |range|
142
- requirements = range.split(/\s+/).reject(&:empty?)
143
- next true if requirements.empty?
144
-
145
- Gem::Requirement.new(*requirements).satisfied_by?(Gem::Version.new(version))
146
- rescue ArgumentError
147
- true
148
- end
147
+ SemverRange.new(vulnerable_versions).satisfied_by?(version)
149
148
  end
150
149
 
151
150
  def update_vulnerability_info(name, version, severity)
@@ -17,7 +17,7 @@ module Package
17
17
  def initialize(dir, pkgs)
18
18
  @dir = dir
19
19
  @pkgs = pkgs
20
- @gem_hash = {}
20
+ @gem_hash = @pkgs.to_h { |pkg| [pkg.name, pkg] }
21
21
  end
22
22
 
23
23
  def fetch
@@ -11,7 +11,9 @@ module Package
11
11
  end
12
12
 
13
13
  def run
14
- json_result = `#{format(Const::Cmd::BUNDLE_AUDIT_JSON, @dir)}`
14
+ json_result = IO.popen(
15
+ [*Const::Cmd::BUNDLE_AUDIT_JSON, @dir.to_s], err: File::NULL, &:read
16
+ )
15
17
  vulnerability_json_array = JSON.parse(json_result, symbolize_names: true)[:results]
16
18
  vulnerability_json_array.each do |vulnerability_json|
17
19
  update_meta_data(vulnerability_json)
@@ -62,7 +62,10 @@ module Package
62
62
  cleaned
63
63
  end
64
64
 
65
- def clean_packages_for_technology(technology, packages)
65
+ def clean_packages_for_technology(technology, packages) # rubocop:disable Metrics/MethodLength
66
+ technologies = @options[Enum::Option::TECHNOLOGY]
67
+ return packages if technologies && !technologies.include?(technology)
68
+
66
69
  return {} unless packages.is_a?(Hash)
67
70
 
68
71
  current_packages = current_packages_for_technology(technology)
@@ -1,5 +1,5 @@
1
1
  module Package
2
2
  module Audit
3
- VERSION = '0.8.2'
3
+ VERSION = '0.9.1'
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: package-audit
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.2
4
+ version: 0.9.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vadim Kononov
@@ -37,8 +37,9 @@ dependencies:
37
37
  - - "~>"
38
38
  - !ruby/object:Gem::Version
39
39
  version: '1.2'
40
- description: A useful tool for patch management and prioritization, package-audit
41
- produces a list of dependencies that are outdated, deprecated or have security vulnerabilities.
40
+ description: Audits project dependencies across supported package managers and lists
41
+ those that are outdated, deprecated or have known security vulnerabilities, for
42
+ patch prioritization.
42
43
  email:
43
44
  - vadim@konoson.com
44
45
  executables:
@@ -73,6 +74,8 @@ files:
73
74
  - lib/package/audit/models/risk.rb
74
75
  - lib/package/audit/npm/node_collection.rb
75
76
  - lib/package/audit/npm/npm_meta_data.rb
77
+ - lib/package/audit/npm/package_lock_parser.rb
78
+ - lib/package/audit/npm/semver_range.rb
76
79
  - lib/package/audit/npm/vulnerability_finder.rb
77
80
  - lib/package/audit/npm/yarn_lock_parser.rb
78
81
  - lib/package/audit/ruby/bundler_specs.rb
@@ -114,7 +117,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
114
117
  - !ruby/object:Gem::Version
115
118
  version: '0'
116
119
  requirements: []
117
- rubygems_version: 4.0.10
120
+ rubygems_version: 4.0.20
118
121
  specification_version: 4
119
- summary: A helper tool to find outdated, deprecated and vulnerable dependencies.
122
+ summary: CLI that reports outdated, deprecated and vulnerable dependencies in a project
120
123
  test_files: []