package-audit 0.8.1 → 0.8.2

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: d613e9a49c48ae22ef6e156b7f6a6ba1f2ebe8172e30589cc2eabbb5530bfdc1
4
- data.tar.gz: aad9e813b4e6d2b73f15b19626fbc70631312d8965823a27d44cceef4c5115aa
3
+ metadata.gz: 7e0d76f96e0ad7b87f59b178565e72e9d52dddf9f7a579e016ff6779fac6d58a
4
+ data.tar.gz: cb917258a2025355d73123cb76762254dd92bdac817c72375e4297fbe8cb6b8f
5
5
  SHA512:
6
- metadata.gz: e6b3406928eb792637f7fbf5de5745a83ae1ce7181febc24d95882c6e38281002bcb953b3f727e18cf983606285f4df428aa36c203ed874a9d919c47c53dad9f
7
- data.tar.gz: c6abc41931c2c26b91443474262353b35a0c64b0d2372258fec255f4f9569c127c3e48a172a219ec1fbd5a71fb7d4e3f15a6bbd6e2f717bb8b65ae0da74ec31f
6
+ metadata.gz: 280d5c3c95dd19ff526723fb7b9c5b387180c63a67d64eab8be9301512c9b3f5d4e252ba1e82317fb5b95e6f86380908344ea766de9ad4539fecd20afeadd3d7
7
+ data.tar.gz: '09f40855f0197f00fa9fc05826da2ec42506fef23d387569dd108e4623488eb059982e47d7b0bb99f718de6e62140db3817b119b90aea473cc837a7ad2038b60'
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2023 Vadim Kononov
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,218 @@
1
+ # Package::Audit
2
+
3
+ [![Gem Version](https://img.shields.io/gem/v/package-audit.svg?label=Gem&logo=rubygems&logoColor=white)](https://rubygems.org/gems/package-audit)
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
+ [![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
+ [![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)
8
+
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
+
11
+ Supports **Ruby 2.6 → latest**, with the full Ruby matrix tested daily in CI.
12
+
13
+ [!["Buy Me A Coffee"](https://www.buymeacoffee.com/assets/img/custom_images/yellow_img.png)](https://www.buymeacoffee.com/vkononov)
14
+
15
+ ## Supported Technologies
16
+
17
+ * Ruby
18
+ * Node (using Yarn)
19
+
20
+ ## Report Example
21
+
22
+ ![Example Output](docs/example.png)
23
+
24
+ ### Understanding the Flags Column
25
+
26
+ The Flags column shows which risk types apply to each package:
27
+
28
+ - `⦗V··⦘` - Vulnerable (has security vulnerabilities)
29
+ - `⦗·O·⦘` - Outdated (newer version available)
30
+ - `⦗··D⦘` - Deprecated (no updates in 2+ years)
31
+ - `⦗VO·⦘` - Both vulnerable and outdated
32
+ - `⦗VOD⦘` - All three risk types apply
33
+
34
+ The footer uses the same notation (⦗V⦘ulnerable, ⦗O⦘utdated, ⦗D⦘eprecated) as a legend.
35
+
36
+ ## Continuous Integration
37
+
38
+ This gem provides a return code of `0` to indicate success and `1` to indicate failure. It is specifically designed for seamless integration into continuous integration pipelines.
39
+
40
+ ## Installation
41
+
42
+ Add this line to your application's Gemfile:
43
+
44
+ ```ruby
45
+ gem 'package-audit', require: false
46
+ ```
47
+
48
+ And then execute:
49
+
50
+ ```bash
51
+ bundle
52
+ ```
53
+
54
+ Or install it yourself as:
55
+
56
+ ```bash
57
+ gem install package-audit
58
+ ```
59
+
60
+ ## Usage
61
+
62
+ ### Basic Report
63
+
64
+ Run a full audit (`DIR` defaults to current directory):
65
+
66
+ ```bash
67
+ package-audit [DIR]
68
+ ```
69
+
70
+ ### Filtering by Risk Type
71
+
72
+ Show only specific risk types:
73
+
74
+ ```bash
75
+ # Show only deprecated packages
76
+ package-audit [DIR] --deprecated
77
+
78
+ # Show only vulnerable packages
79
+ package-audit [DIR] --vulnerable
80
+
81
+ # Show packages that are deprecated and/or vulnerable
82
+ package-audit [DIR] --deprecated --vulnerable
83
+ ```
84
+
85
+ ### Excluding Risk Types
86
+
87
+ Exclude specific risk types from the report:
88
+
89
+ ```bash
90
+ # Show everything except deprecated packages
91
+ package-audit [DIR] --skip-deprecated
92
+
93
+ # Show everything except outdated packages
94
+ package-audit [DIR] --skip-outdated
95
+
96
+ # Show only vulnerable packages (exclude deprecated and outdated)
97
+ package-audit [DIR] --skip-deprecated --skip-outdated
98
+ ```
99
+
100
+ **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
+
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
+ ### Scoping Options
115
+
116
+ Filter by technology or group:
117
+
118
+ ```bash
119
+ # Specific technologies
120
+ package-audit [DIR] -t node -t ruby
121
+ package-audit [DIR] --technology node --technology ruby
122
+
123
+ # Specific groups
124
+ package-audit [DIR] -g staging -g production
125
+ package-audit [DIR] --group staging --group production
126
+ ```
127
+
128
+ **Note:** `Package::Audit` will attempt to automatically detect the technologies used by the project.
129
+
130
+ ### Configuration
131
+
132
+ Use a custom configuration file (see [Configuration File](#configuration-file) for details):
133
+
134
+ ```bash
135
+ package-audit [DIR] --config .package-audit.yml
136
+ ```
137
+
138
+ Include packages normally ignored by configuration:
139
+
140
+ ```bash
141
+ package-audit [DIR] --include-ignored
142
+ ```
143
+
144
+ ### Output Formats
145
+
146
+ ```bash
147
+ # CSV format
148
+ package-audit [DIR] --format csv
149
+
150
+ # Markdown format
151
+ package-audit [DIR] --format md
152
+ ```
153
+
154
+ ### Other Commands
155
+
156
+ ```bash
157
+ # Show how risk is calculated
158
+ package-audit risk
159
+
160
+ # Show version
161
+ package-audit version
162
+ ```
163
+
164
+ ### Getting Help
165
+
166
+ ```bash
167
+ package-audit help
168
+ package-audit help [COMMAND]
169
+ ```
170
+
171
+ ## Configuration File
172
+
173
+ 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
+
175
+ #### Below is an example of a configuration file:
176
+
177
+ ```YAML
178
+ technology:
179
+ node:
180
+ nth-check:
181
+ version: 1.0.2
182
+ vulnerable: false
183
+ ruby:
184
+ devise-async:
185
+ version: 1.0.0
186
+ deprecated: false
187
+ puma:
188
+ version: 6.3.0
189
+ deprecated: false
190
+ selenium-webdriver:
191
+ version: 4.1.0
192
+ outdated: false
193
+ ```
194
+
195
+ #### This configuration file allows you to specify the following exclusions:
196
+
197
+
198
+ * Ignore all security vulnerabilities associated with `nth-check@1.0.2`.
199
+ * Suppress messages regarding potential deprecations for `device-async@1.0.0` and `puma@6.3.0`.
200
+ * Disable warnings about newer available versions of `selenium-webdriver@4.1.0`
201
+
202
+ **Note:** If the installed package version differs from the expected package version specified in the configuration file, the exclusion settings will not apply to that particular package.
203
+
204
+ **Note:** If a package is reported for multiple reasons (e.g. vulnerable and outdated), it will still be reported unless the exclusion criteria match every reason for being on the report.
205
+
206
+ > By design, wildcard (`*`) version exclusions are not supported to prevent developers from inadvertently overlooking crucial messages when packages are updated.
207
+
208
+ ## Contributing
209
+
210
+ 1. Fork it
211
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
212
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
213
+ 4. Push to the branch (`git push origin my-new-feature`)
214
+ 5. Create new Pull Request
215
+
216
+ ## License
217
+
218
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
data/docs/example.png ADDED
Binary file
@@ -6,10 +6,6 @@ module Package
6
6
  BUNDLE_AUDIT_JSON = 'bundle-audit check --update --quiet --format json "%s" 2>/dev/null'
7
7
 
8
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 --cwd "%s"'
13
9
  end
14
10
  end
15
11
  end
@@ -1,13 +1,18 @@
1
- require 'open3'
1
+ require 'json'
2
+ require 'net/http'
3
+ require 'openssl'
2
4
 
3
- require_relative '../const/cmd'
5
+ require_relative '../const/file'
4
6
  require_relative '../enum/vulnerability_type'
5
7
 
6
8
  module Package
7
9
  module Audit
8
10
  module Npm
9
- class VulnerabilityFinder
10
- AUDIT_ADVISORY_REGEX = /^{"type":"auditAdvisory".*$/
11
+ class VulnerabilityFinder # rubocop:disable Metrics/ClassLength
12
+ BULK_ADVISORY_URL = 'https://registry.npmjs.org/-/npm/v1/security/advisories/bulk'
13
+ BATCH_SIZE = 100
14
+ TIMEOUT = 30
15
+ VERSION_LINE = /^\s+"?version"?\s+"([^"]+)"/
11
16
 
12
17
  def initialize(dir, pkgs)
13
18
  @dir = dir
@@ -16,48 +21,142 @@ module Package
16
21
  end
17
22
 
18
23
  def run
19
- # Suppress Node.js url.parse deprecation warnings from yarn audit command
20
- command = format(Const::Cmd::YARN_AUDIT_JSON, @dir)
21
- env_vars = { 'NODE_NO_WARNINGS' => '1' }
24
+ versions_by_name = collect_versions_by_name
25
+ return [] if versions_by_name.empty?
22
26
 
23
- json_string_lines, = Open3.capture3(env_vars, command)
24
- array = json_string_lines.scan(AUDIT_ADVISORY_REGEX)
25
-
26
- vulnerability_json_array = JSON.parse("[#{array.join(',')}]", symbolize_names: true)
27
- vulnerability_json_array.each do |vulnerability_json|
28
- update_meta_data(vulnerability_json)
27
+ versions_by_name.each_slice(BATCH_SIZE) do |batch|
28
+ batch_versions = batch.to_h
29
+ fetch_advisories(batch_versions).each do |name, advisories|
30
+ package_name = name.to_s
31
+ apply_advisories(package_name, Array(advisories), batch_versions[package_name] || [])
32
+ end
29
33
  end
34
+
30
35
  @vuln_hash.values
31
36
  end
32
37
 
33
38
  private
34
39
 
35
- def update_meta_data(json)
36
- resolution_path = json.dig(:data, :resolution, :path)
37
- return unless resolution_path # Skip if no resolution path (private package)
40
+ def collect_versions_by_name
41
+ versions_by_name = {}
42
+
43
+ (lockfile_packages + @pkg_hash.values.map { |pkg| [pkg.name, pkg.version] }).each do |name, version|
44
+ next if version.to_s.empty? || version == 'unknown'
45
+
46
+ versions = versions_by_name[name] ||= []
47
+ versions << version unless versions.include?(version)
48
+ end
49
+
50
+ versions_by_name
51
+ end
52
+
53
+ def lockfile_packages
54
+ path = "#{@dir}/#{Const::File::YARN_LOCK}"
55
+ return [] unless File.exist?(path)
56
+
57
+ packages = []
58
+ current_names = []
59
+ File.foreach(path) do |line|
60
+ current_names, packages = process_lockfile_line(line, current_names, packages)
61
+ end
62
+ packages
63
+ end
64
+
65
+ def process_lockfile_line(line, current_names, packages)
66
+ if lockfile_key_line?(line)
67
+ [package_names_from_lock_key(line.strip.chomp(':')), packages]
68
+ elsif (match = line.match(VERSION_LINE))
69
+ match_version = match[1]
70
+ current_names.each { |name| packages << [name, match_version] }
71
+ [[], packages]
72
+ else
73
+ [current_names, packages]
74
+ end
75
+ end
38
76
 
39
- parent_name = resolution_path.split('>').first
40
- advisory = json[:data][:advisory]
41
- package_info = extract_package_info(advisory)
42
- update_vulnerability_info(package_info, parent_name, advisory[:severity])
77
+ def lockfile_key_line?(line)
78
+ line.match?(/^\S/) && line.include?('@') && line.strip.end_with?(':')
43
79
  end
44
80
 
45
- def extract_package_info(advisory)
46
- {
47
- name: advisory[:module_name],
48
- version: advisory[:findings][0][:version]
49
- }
81
+ def package_names_from_lock_key(key)
82
+ key.split(',').map do |entry|
83
+ package_name_from_spec(entry.strip.gsub(/\A["']|["']\z/, ''))
84
+ end.uniq
85
+ end
86
+
87
+ def package_name_from_spec(spec)
88
+ if spec.start_with?('@')
89
+ "@#{spec.split('@', 3)[1]}"
90
+ else
91
+ spec.split('@', 2).first
92
+ end
93
+ end
94
+
95
+ def fetch_advisories(versions_by_name)
96
+ uri = URI(BULK_ADVISORY_URL)
97
+ response = make_request(uri, versions_by_name.to_json)
98
+ unless response.is_a?(Net::HTTPSuccess)
99
+ warn "Warning: Unable to fetch npm advisories (#{response.code} #{response.message})."
100
+ return {}
101
+ end
102
+
103
+ JSON.parse(response.body)
104
+ rescue StandardError => e
105
+ warn "Warning: Error while fetching npm advisories: #{e.message}"
106
+ {}
107
+ end
108
+
109
+ def make_request(uri, body)
110
+ request = Net::HTTP::Post.new(uri)
111
+ request['Content-Type'] = 'application/json'
112
+ request['Accept'] = 'application/json'
113
+ request.body = body
114
+ create_http_client(uri).request(request)
115
+ end
116
+
117
+ def create_http_client(uri)
118
+ Net::HTTP.new(uri.host, uri.port).tap do |http|
119
+ http.use_ssl = true
120
+ http.verify_mode = OpenSSL::SSL::VERIFY_PEER
121
+ http.cert_store = OpenSSL::X509::Store.new
122
+ http.cert_store.set_default_paths
123
+ http.read_timeout = TIMEOUT
124
+ http.open_timeout = TIMEOUT
125
+ end
126
+ end
127
+
128
+ def apply_advisories(name, advisories, versions)
129
+ advisories.each do |advisory|
130
+ Array(versions).each do |version|
131
+ next unless version_matches?(version, advisory['vulnerable_versions'])
132
+
133
+ update_vulnerability_info(name, version, advisory['severity'])
134
+ end
135
+ end
136
+ end
137
+
138
+ 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
50
149
  end
51
150
 
52
- def update_vulnerability_info(package_info, parent_name, severity)
53
- name = package_info[:name]
54
- version = package_info[:version]
151
+ def update_vulnerability_info(name, version, severity)
55
152
  full_name = "#{name}@#{version}"
56
153
  vulnerability = severity || Enum::VulnerabilityType::UNKNOWN
57
154
 
58
155
  @vuln_hash[full_name] ||= Package.new(name, version, 'node')
59
- @vuln_hash[full_name].update vulnerabilities: @vuln_hash[full_name].vulnerabilities + [vulnerability]
60
- @vuln_hash[full_name].update groups: @pkg_hash[parent_name]&.groups&.map(&:to_s) || []
156
+ @vuln_hash[full_name].update(
157
+ vulnerabilities: @vuln_hash[full_name].vulnerabilities + [vulnerability],
158
+ groups: @pkg_hash[name]&.groups&.map(&:to_s) || []
159
+ )
61
160
  end
62
161
  end
63
162
  end
@@ -226,8 +226,8 @@ module Package
226
226
 
227
227
  def parse_version_dates(response_body)
228
228
  versions = JSON.parse(response_body)
229
- versions.each_with_object({}) do |version_info, dates|
230
- dates[version_info['number']] = version_info['created_at']
229
+ versions.to_h do |version_info|
230
+ [version_info['number'], version_info['created_at']]
231
231
  end
232
232
  end
233
233
 
@@ -140,7 +140,7 @@ module Package
140
140
  when Enum::Technology::RUBY
141
141
  Const::Cmd::BUNDLE_AUDIT
142
142
  when Enum::Technology::NODE
143
- Const::Cmd::YARN_AUDIT
143
+ Const::Cmd::NPM_AUDIT
144
144
  else
145
145
  raise ArgumentError, "Unexpected technology \"#{technology}\" found in #{__method__}"
146
146
  end
@@ -1,5 +1,5 @@
1
1
  module Package
2
2
  module Audit
3
- VERSION = '0.8.1'
3
+ VERSION = '0.8.2'
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.1
4
+ version: 0.8.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vadim Kononov
@@ -46,6 +46,9 @@ executables:
46
46
  extensions: []
47
47
  extra_rdoc_files: []
48
48
  files:
49
+ - LICENSE.txt
50
+ - README.md
51
+ - docs/example.png
49
52
  - exe/package-audit
50
53
  - lib/package/audit/cli.rb
51
54
  - lib/package/audit/const/cmd.rb
@@ -111,7 +114,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
111
114
  - !ruby/object:Gem::Version
112
115
  version: '0'
113
116
  requirements: []
114
- rubygems_version: 3.7.2
117
+ rubygems_version: 4.0.10
115
118
  specification_version: 4
116
119
  summary: A helper tool to find outdated, deprecated and vulnerable dependencies.
117
120
  test_files: []