package-audit 0.8.2 → 0.9.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7e0d76f96e0ad7b87f59b178565e72e9d52dddf9f7a579e016ff6779fac6d58a
4
- data.tar.gz: cb917258a2025355d73123cb76762254dd92bdac817c72375e4297fbe8cb6b8f
3
+ metadata.gz: 6a9f8b791f93ef77f2eecb40a1d0bacaebf40c94a06e938ef7528f26281ac30b
4
+ data.tar.gz: e9a24f0114518ba97deaa1687ca5c786fee0ba54dd868cbaec48c4d6d183be7b
5
5
  SHA512:
6
- metadata.gz: 280d5c3c95dd19ff526723fb7b9c5b387180c63a67d64eab8be9301512c9b3f5d4e252ba1e82317fb5b95e6f86380908344ea766de9ad4539fecd20afeadd3d7
7
- data.tar.gz: '09f40855f0197f00fa9fc05826da2ec42506fef23d387569dd108e4623488eb059982e47d7b0bb99f718de6e62140db3817b119b90aea473cc837a7ad2038b60'
6
+ metadata.gz: e106be4632777be3433e572bb60b34a9b8fa4f7e038f4be86b1d9744ffcaf2e709804bcd4ffffd2b001ee82887ea70cbb7234d2df37a4033224f155ac84b4b88
7
+ data.tar.gz: 400eb639d4f16d4671bc10b1e0b76ad7e49f7f3991239c8f3c5a7dbee5138fcc46d3ae819bcb06021d45a3d46e4ca2cff8e755760dda387976b2fd67d8c8aa03
data/README.md CHANGED
@@ -4,7 +4,7 @@
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
 
@@ -15,7 +15,7 @@ Supports **Ruby 2.6 → latest**, with the full Ruby matrix tested daily in CI.
15
15
  ## Supported Technologies
16
16
 
17
17
  * Ruby
18
- * Node (using Yarn)
18
+ * Node (using npm or Yarn)
19
19
 
20
20
  ## Report Example
21
21
 
@@ -75,6 +75,9 @@ Show only specific risk types:
75
75
  # Show only deprecated packages
76
76
  package-audit [DIR] --deprecated
77
77
 
78
+ # Show only outdated packages
79
+ package-audit [DIR] --outdated
80
+
78
81
  # Show only vulnerable packages
79
82
  package-audit [DIR] --vulnerable
80
83
 
@@ -93,24 +96,15 @@ package-audit [DIR] --skip-deprecated
93
96
  # Show everything except outdated packages
94
97
  package-audit [DIR] --skip-outdated
95
98
 
99
+ # Show everything except vulnerable packages
100
+ package-audit [DIR] --skip-vulnerable
101
+
96
102
  # Show only vulnerable packages (exclude deprecated and outdated)
97
103
  package-audit [DIR] --skip-deprecated --skip-outdated
98
104
  ```
99
105
 
100
106
  **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
107
 
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
108
  ### Scoping Options
115
109
 
116
110
  Filter by technology or group:
@@ -144,11 +138,14 @@ package-audit [DIR] --include-ignored
144
138
  ### Output Formats
145
139
 
146
140
  ```bash
147
- # CSV format
141
+ # CSV format (-f is an alias for --format)
148
142
  package-audit [DIR] --format csv
149
143
 
150
144
  # Markdown format
151
145
  package-audit [DIR] --format md
146
+
147
+ # CSV without the header row
148
+ package-audit [DIR] --format csv --exclude-headers
152
149
  ```
153
150
 
154
151
  ### Other Commands
@@ -157,7 +154,7 @@ package-audit [DIR] --format md
157
154
  # Show how risk is calculated
158
155
  package-audit risk
159
156
 
160
- # Show version
157
+ # Show version (-v and --version also work)
161
158
  package-audit version
162
159
  ```
163
160
 
@@ -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).
@@ -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
@@ -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|
@@ -136,16 +145,7 @@ module Package
136
145
  end
137
146
 
138
147
  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
148
+ SemverRange.new(vulnerable_versions).satisfied_by?(version)
149
149
  end
150
150
 
151
151
  def update_vulnerability_info(name, version, severity)
@@ -1,5 +1,5 @@
1
1
  module Package
2
2
  module Audit
3
- VERSION = '0.8.2'
3
+ VERSION = '0.9.0'
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.0
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
@@ -116,5 +119,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
116
119
  requirements: []
117
120
  rubygems_version: 4.0.10
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: []