package-audit 0.8.0 → 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: 7305fff5b318e2831908713c3d49b984787b99baaca769115339518297dee6e4
4
- data.tar.gz: 6db0ad1ecece32ddc3b93ea89fc9366b1fba59861e925d9e6955708173f3f32d
3
+ metadata.gz: 7e0d76f96e0ad7b87f59b178565e72e9d52dddf9f7a579e016ff6779fac6d58a
4
+ data.tar.gz: cb917258a2025355d73123cb76762254dd92bdac817c72375e4297fbe8cb6b8f
5
5
  SHA512:
6
- metadata.gz: 32b8c325d2f938ead2ab5ab8ab190ac94f4591667cbf0eaf884258b64e8ac06dc9a367ca7ecefab5a8688e1713378543ae5834ce6a8dbdab6874fdccded4b528
7
- data.tar.gz: ca7e0687952bd9bcb7708b159e2551ad6cfa84bec8636eb05e954cb5763bb714d407ef190651ef764756f2a333c83e33b694a52e07dbff633019ba2f1d5a5563
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
@@ -35,13 +35,15 @@ module Package
35
35
 
36
36
  def deprecated
37
37
  implicit_pkgs = fetch_from_lock_file
38
- pkgs = NpmMetaData.new(implicit_pkgs).fetch.filter(&:deprecated?)
38
+ vulnerable_pkgs = VulnerabilityFinder.new(@dir, implicit_pkgs).run
39
+ pkgs = NpmMetaData.new(vulnerable_pkgs + implicit_pkgs).fetch.filter(&:deprecated?)
39
40
  DuplicatePackageMerger.new(pkgs).run
40
41
  end
41
42
 
42
43
  def outdated
43
44
  implicit_pkgs = fetch_from_lock_file
44
- pkgs = NpmMetaData.new(implicit_pkgs).fetch.filter(&:outdated?)
45
+ vulnerable_pkgs = VulnerabilityFinder.new(@dir, implicit_pkgs).run
46
+ pkgs = NpmMetaData.new(vulnerable_pkgs + implicit_pkgs).fetch.filter(&:outdated?)
45
47
  DuplicatePackageMerger.new(pkgs).run
46
48
  end
47
49
 
@@ -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
@@ -38,14 +38,16 @@ module Package
38
38
  def deprecated
39
39
  specs = BundlerSpecs.gemfile(@dir)
40
40
  pkgs = specs.map { |spec| Package.new(spec.name, spec.version, Enum::Technology::RUBY) }
41
- pkgs = GemMetaData.new(@dir, pkgs).fetch.filter(&:deprecated?)
41
+ vulnerable_pkgs = VulnerabilityFinder.new(@dir).run
42
+ pkgs = GemMetaData.new(@dir, pkgs + vulnerable_pkgs).fetch.filter(&:deprecated?)
42
43
  DuplicatePackageMerger.new(pkgs).run
43
44
  end
44
45
 
45
46
  def outdated(include_implicit: false)
46
47
  specs = include_implicit ? BundlerSpecs.all(@dir) : BundlerSpecs.gemfile(@dir)
47
48
  pkgs = specs.map { |spec| Package.new(spec.name, spec.version, Enum::Technology::RUBY) }
48
- pkgs = GemMetaData.new(@dir, pkgs).fetch.filter(&:outdated?)
49
+ vulnerable_pkgs = VulnerabilityFinder.new(@dir).run
50
+ pkgs = GemMetaData.new(@dir, pkgs + vulnerable_pkgs).fetch.filter(&:outdated?)
49
51
  DuplicatePackageMerger.new(pkgs).run
50
52
  end
51
53
 
@@ -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
 
@@ -47,6 +47,7 @@ module Package
47
47
  thread_index = 0
48
48
 
49
49
  @spinner.start
50
+ @any_section_printed = false
50
51
  threads = @technologies.map.with_index do |technology, technology_index|
51
52
  Thread.new do
52
53
  all_pkgs, ignored_pkgs = PackageFinder.new(@config, @dir, @report, @groups).run(technology)
@@ -56,7 +57,14 @@ module Package
56
57
  cumulative_pkgs += active_pkgs
57
58
  mutex.synchronize { all_packages_for_config += all_pkgs || [] }
58
59
 
59
- sleep 0.1 while technology_index != thread_index # print each technology in order
60
+ # Wait for our turn to print (synchronized read of thread_index)
61
+ loop do
62
+ ready = mutex.synchronize { technology_index == thread_index }
63
+ break if ready
64
+
65
+ sleep 0.05
66
+ end
67
+
60
68
  mutex.synchronize do
61
69
  @spinner.stop
62
70
  print_results(technology, active_pkgs, ignored_pkgs || [])
@@ -82,17 +90,39 @@ module Package
82
90
  @spinner.stop
83
91
  end
84
92
 
85
- def print_results(technology, pkgs, ignored_pkgs)
93
+ def print_results(technology, pkgs, ignored_pkgs) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
94
+ format = @options[Enum::Option::FORMAT]
95
+ is_pretty = format.nil?
96
+ is_csv = format == Enum::Format::CSV
97
+ has_packages = pkgs.any?
98
+
99
+ # Pretty format: blank line before each section
100
+ # CSV/Markdown: blank line between sections (only if previous section had output)
101
+ puts if is_pretty || (@any_section_printed && has_packages)
102
+
86
103
  PackagePrinter.new(@options, pkgs).print(Const::Fields::DEFAULT)
87
- print_summary(technology, pkgs, ignored_pkgs) unless @options[Enum::Option::FORMAT] == Enum::Format::CSV
88
- print_disclaimer(technology) unless @options[Enum::Option::FORMAT] || pkgs.empty?
104
+
105
+ # Markdown: blank line between table and summary (when there's a table)
106
+ puts if !is_pretty && !is_csv && has_packages
107
+
108
+ print_summary(technology, pkgs, ignored_pkgs) unless is_csv
109
+
110
+ # Pretty format: blank line before disclaimer
111
+ puts if is_pretty && has_packages
112
+
113
+ print_disclaimer(technology) unless format || pkgs.empty?
114
+
115
+ # Track that this section produced output (for CSV/Markdown separator logic)
116
+ # Pretty and Markdown always print summary, CSV only prints if there are packages
117
+ @any_section_printed = true if has_packages || !is_csv
89
118
  end
90
119
 
91
120
  def print_summary(technology, pkgs, ignored_pkgs)
121
+ format = @options[Enum::Option::FORMAT]
92
122
  if @report == Enum::Report::ALL
93
- Util::SummaryPrinter.statistics(@options[Enum::Option::FORMAT], technology, @report, pkgs, ignored_pkgs)
123
+ Util::SummaryPrinter.statistics(format, technology, @report, pkgs, ignored_pkgs)
94
124
  else
95
- Util::SummaryPrinter.total(technology, @report, pkgs, ignored_pkgs)
125
+ Util::SummaryPrinter.total(format, technology, @report, pkgs, ignored_pkgs)
96
126
  end
97
127
  end
98
128
 
@@ -110,7 +140,7 @@ module Package
110
140
  when Enum::Technology::RUBY
111
141
  Const::Cmd::BUNDLE_AUDIT
112
142
  when Enum::Technology::NODE
113
- Const::Cmd::YARN_AUDIT
143
+ Const::Cmd::NPM_AUDIT
114
144
  else
115
145
  raise ArgumentError, "Unexpected technology \"#{technology}\" found in #{__method__}"
116
146
  end
@@ -24,10 +24,8 @@ module Package
24
24
  case @options[Enum::Option::FORMAT]
25
25
  when Enum::Format::CSV
26
26
  csv(fields, exclude_headers: @options[Enum::Option::CSV_EXCLUDE_HEADERS])
27
- puts
28
27
  when Enum::Format::MARKDOWN
29
28
  markdown(fields)
30
- puts
31
29
  else
32
30
  pretty(fields)
33
31
  end
@@ -51,7 +49,6 @@ module Package
51
49
  separator_plain = max_widths.map { |width| '─' * width }.join('─' * COLUMN_GAP)
52
50
  separator = Util::BashColor.light_green(separator_plain)
53
51
 
54
- puts ' '
55
52
  puts " #{separator}"
56
53
  puts " #{header}"
57
54
  puts " #{separator}"
@@ -15,7 +15,6 @@ module Package
15
15
  puts " #{Util::BashColor.blue('Although the packages above have no recent updates, ' \
16
16
  'they may not be deprecated.')}"
17
17
  puts " #{Util::BashColor.blue('Please contact the package author for more information about its status.')}"
18
- puts
19
18
  end
20
19
 
21
20
  def self.vulnerable(technology, cmd)
@@ -24,16 +23,22 @@ module Package
24
23
  cmd: Util::BashColor.magenta(" > #{cmd}"))
25
24
  end
26
25
 
27
- def self.total(technology, report, pkgs, ignored_pkgs)
26
+ def self.total(format, technology, report, pkgs, ignored_pkgs) # rubocop:disable Metrics/MethodLength
27
+ prefix = format.nil? ? ' ' : ''
28
28
  if ignored_pkgs.any?
29
- puts " #{Util::BashColor.cyan("Found a total of #{pkgs.length} #{technology.capitalize} packages " \
30
- "(#{ignored_pkgs.length} ignored).")}"
29
+ puts "#{prefix}#{Util::BashColor.cyan(
30
+ "Found a total of #{pkgs.length} #{technology.capitalize} packages " \
31
+ "(#{ignored_pkgs.length} ignored)."
32
+ )}"
31
33
  elsif pkgs.any?
32
- puts " #{Util::BashColor.cyan("Found a total of #{pkgs.length} #{technology.capitalize} packages.")}"
34
+ puts "#{prefix}#{Util::BashColor.cyan(
35
+ "Found a total of #{pkgs.length} #{technology.capitalize} packages."
36
+ )}"
33
37
  else
34
- puts " #{Util::BashColor.green("There are no #{report} #{technology.capitalize} packages!")}"
38
+ puts "#{prefix}#{Util::BashColor.green(
39
+ "There are no #{report} #{technology.capitalize} packages!"
40
+ )}"
35
41
  end
36
- puts
37
42
  end
38
43
 
39
44
  def self.statistics(format, technology, report, pkgs, ignored_pkgs)
@@ -60,22 +65,23 @@ module Package
60
65
  end
61
66
 
62
67
  private_class_method def self.display_results(format, technology, report, pkgs, ignored_pkgs, stats) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/ParameterLists
68
+ prefix = format.nil? ? ' ' : ''
63
69
  if pkgs.any?
64
- print " #{status_message(stats)}"
70
+ print "#{prefix}#{status_message(stats)}"
65
71
  print Util::BashColor.cyan(' \\') if format == Enum::Format::MARKDOWN
66
72
  puts
67
- total(technology, report, pkgs, ignored_pkgs)
73
+ total(format, technology, report, pkgs, ignored_pkgs)
68
74
  elsif ignored_pkgs.any?
69
- print " #{status_message(stats)}"
75
+ print "#{prefix}#{status_message(stats)}"
70
76
  print Util::BashColor.cyan(' \\') if format == Enum::Format::MARKDOWN
71
77
  puts
72
- puts " #{Util::BashColor.green('There are no deprecated, outdated or vulnerable ' \
73
- "#{technology.capitalize} packages (#{ignored_pkgs.length} ignored)!")}"
74
- puts
78
+ puts "#{prefix}#{Util::BashColor.green(
79
+ 'There are no deprecated, outdated or vulnerable ' \
80
+ "#{technology.capitalize} packages (#{ignored_pkgs.length} ignored)!"
81
+ )}"
75
82
  else
76
- puts " #{Util::BashColor.green('There are no deprecated, outdated or vulnerable ' \
77
- "#{technology.capitalize} packages!")}"
78
- puts
83
+ puts "#{prefix}#{Util::BashColor.green('There are no deprecated, outdated or vulnerable ' \
84
+ "#{technology.capitalize} packages!")}"
79
85
  end
80
86
  end
81
87
 
@@ -1,5 +1,5 @@
1
1
  module Package
2
2
  module Audit
3
- VERSION = '0.8.0'
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.0
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: []