package-audit 0.5.1 → 0.6.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: 3987dbcffb0bef510d5897ad47ec79aa5ba65572c62d1a78003496a44264ca7e
4
- data.tar.gz: f2608608cee05dde5a409e9dc6a4c885b208ee7d52cc7d6e745e0d3ccf37d7b7
3
+ metadata.gz: f9f4f23e752d513ff8a35a325296edf1d2d0a99b200d3e64ae92ce00ce145ac1
4
+ data.tar.gz: 043a77af2e0a46688ba0964e6d595bfe3a50fecc59f73f4957c24ca0ad4c9cbe
5
5
  SHA512:
6
- metadata.gz: baa304f965258c639f7e4bee858da18ddd74bfb83926a66d95ce43367ac5bfcf363e4847dd56c98ed649cda9d7143cbb44bb2015d1c7dc263b73f8942538011e
7
- data.tar.gz: a5adfb16e863dacea34dc1d1ca8e4962e76ab984f766d86073f2a5bcfceea9923d2c048ae11487e8a0644d9420025a6dba61f57dede9b6faaf74cd82954f5f52
6
+ metadata.gz: 3ebd17673929a378540502f8757397bd37c1a86121fa99f691fb94b537b53f9eec5f7698be6076af0cc0aa7c163458eede4875316e6755a1a05e4a7b8e33e83b
7
+ data.tar.gz: 190255842a47fbf044d75703cf8263b177a91ed5a282c7d610cb99f27ad3fe194170b73ba09575afb6fc181b26f2d797fa11f4e9f481cb8f1619af581bd61f86
@@ -1,7 +1,9 @@
1
1
  require_relative 'const/file'
2
2
  require_relative 'const/time'
3
+ require_relative 'enum/format'
3
4
  require_relative 'enum/option'
4
5
  require_relative 'services/command_parser'
6
+ require_relative 'util//risk_legend'
5
7
  require_relative 'version'
6
8
 
7
9
  require 'json'
@@ -10,7 +12,7 @@ require 'thor'
10
12
  module Package
11
13
  module Audit
12
14
  class CLI < Thor
13
- default_task :report
15
+ default_task :default
14
16
 
15
17
  class_option Enum::Option::CONFIG,
16
18
  aliases: '-c', banner: 'FILE',
@@ -24,18 +26,18 @@ module Package
24
26
  class_option Enum::Option::INCLUDE_IGNORED,
25
27
  type: :boolean, default: false,
26
28
  desc: 'Include packages ignored by a configuration file'
27
- class_option Enum::Option::CSV,
28
- type: :boolean, default: false,
29
- desc: 'Output reports using comma separated values (CSV)'
29
+ class_option Enum::Option::FORMAT,
30
+ aliases: '-f', banner: Enum::Format.all.join('|'), type: :string,
31
+ desc: 'Output reports using a different format (e.g. CSV or Markdown)'
30
32
  class_option Enum::Option::CSV_EXCLUDE_HEADERS,
31
33
  type: :boolean, default: false,
32
- desc: "Hide headers when using the --#{Enum::Option::CSV} option"
34
+ desc: "Hide headers when using the #{Enum::Format::CSV} format"
33
35
 
34
36
  map '-v' => :version
35
37
  map '--version' => :version
36
38
 
37
- desc 'report [DIR]', 'Show a report of potentially deprecated, outdated or vulnerable packages'
38
- def report(dir = Dir.pwd)
39
+ desc '[DIR]', 'Show a report of potentially deprecated, outdated or vulnerable packages'
40
+ def default(dir = Dir.pwd)
39
41
  within_rescue_block { exit CommandParser.new(dir, options, Enum::Report::ALL).run }
40
42
  end
41
43
 
@@ -57,7 +59,7 @@ module Package
57
59
 
58
60
  desc 'risk', 'Print information on how risk is calculated'
59
61
  def risk
60
- Util::SummaryPrinter.risk
62
+ Util::RiskLegend.print
61
63
  end
62
64
 
63
65
  desc 'version', 'Print the currently installed version of the package-audit gem'
@@ -70,7 +72,7 @@ module Package
70
72
  end
71
73
 
72
74
  def method_missing(command, *args)
73
- invoke :report, [command], args
75
+ invoke :default, [command], args
74
76
  end
75
77
 
76
78
  def respond_to_missing?
@@ -3,13 +3,13 @@ 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'
6
+ BUNDLE_AUDIT_JSON = 'bundle-audit check --update --quiet --format json "%s" 2>/dev/null'
7
7
 
8
8
  NPM_AUDIT = 'npm audit'
9
9
  NPM_AUDIT_JSON = 'npm audit --json'
10
10
 
11
11
  YARN_AUDIT = 'yarn audit'
12
- YARN_AUDIT_JSON = 'yarn audit --json --cwd %s'
12
+ YARN_AUDIT_JSON = 'yarn audit --json --cwd "%s"'
13
13
  end
14
14
  end
15
15
  end
@@ -0,0 +1,14 @@
1
+ module Package
2
+ module Audit
3
+ module Enum
4
+ module Format
5
+ CSV = 'csv'
6
+ MARKDOWN = 'md'
7
+
8
+ def self.all
9
+ constants.map { |key| const_get(key) }.sort
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -3,7 +3,7 @@ module Package
3
3
  module Enum
4
4
  module Option
5
5
  CONFIG = 'config'
6
- CSV = 'csv'
6
+ FORMAT = 'format'
7
7
  CSV_EXCLUDE_HEADERS = 'exclude-headers'
8
8
  GROUP = 'group'
9
9
  INCLUDE_IGNORED = 'include-ignored'
@@ -6,7 +6,7 @@ module Package
6
6
  RUBY = 'ruby'
7
7
 
8
8
  def self.all
9
- constants.map { |key| Enum::Technology.const_get(key) }.sort
9
+ constants.map { |key| const_get(key) }.sort
10
10
  end
11
11
  end
12
12
  end
@@ -25,7 +25,7 @@ module Package
25
25
  end
26
26
 
27
27
  def update(**attr)
28
- attr.each { |key, value| instance_variable_set("@#{key}", value) }
28
+ attr.each { |key, value| instance_variable_set(:"@#{key}", value) }
29
29
  end
30
30
 
31
31
  def risk
@@ -41,7 +41,7 @@ module Package
41
41
  end
42
42
 
43
43
  def group_list
44
- @groups.join('|')
44
+ @groups.join(' ')
45
45
  end
46
46
 
47
47
  def vulnerabilities_grouped
@@ -18,6 +18,7 @@ module Package
18
18
  def self.gemfile(dir)
19
19
  current_dependencies = Bundler.with_unbundled_env do
20
20
  ENV['BUNDLE_GEMFILE'] = "#{dir}/Gemfile"
21
+ Bundler.ui.level = 'error'
21
22
  Bundler.reset!
22
23
  Bundler.ui.silence do
23
24
  Bundler.load.dependencies.to_h { |dep| [dep.name, dep] }
@@ -18,13 +18,28 @@ module Package
18
18
  @dir = dir
19
19
  @options = options
20
20
  @report = report
21
- @config = parse_config_file
21
+ @config = parse_config_file!
22
22
  @groups = @options[Enum::Option::GROUP]
23
- @technologies = parse_technologies
23
+ @technologies = parse_technologies!
24
+ validate_format!
24
25
  @spinner = Util::Spinner.new('Evaluating packages and their dependencies...')
25
26
  end
26
27
 
27
- def run # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
28
+ def run
29
+ if File.file? @dir.to_s
30
+ raise "\"#{@dir}\" is a file instead of directory"
31
+ elsif !File.directory? @dir.to_s
32
+ raise "\"#{@dir}\" is not a valid directory"
33
+ elsif @technologies.empty?
34
+ raise 'No supported technologies found in this directory'
35
+ else
36
+ process_technologies
37
+ end
38
+ end
39
+
40
+ private
41
+
42
+ def process_technologies # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
28
43
  mutex = Mutex.new
29
44
  cumulative_pkgs = []
30
45
  thread_index = 0
@@ -34,7 +49,7 @@ module Package
34
49
  Thread.new do
35
50
  all_pkgs, ignored_pkgs = PackageFinder.new(@config, @dir, @report, @groups).run(technology)
36
51
  ignored_pkgs = [] if @options[Enum::Option::INCLUDE_IGNORED]
37
- cumulative_pkgs += all_pkgs || []
52
+ cumulative_pkgs += (all_pkgs || []) - (ignored_pkgs || [])
38
53
  sleep 0.1 while technology_index != thread_index # print each technology in order
39
54
  mutex.synchronize do
40
55
  @spinner.stop
@@ -55,17 +70,15 @@ module Package
55
70
  @spinner.stop
56
71
  end
57
72
 
58
- private
59
-
60
73
  def print_results(technology, pkgs, ignored_pkgs)
61
74
  PackagePrinter.new(@options, pkgs).print(Const::Fields::DEFAULT)
62
- print_summary(technology, pkgs, ignored_pkgs) unless @options[Enum::Option::CSV]
63
- print_disclaimer(technology) unless @options[Enum::Option::CSV] || pkgs.empty?
75
+ print_summary(technology, pkgs, ignored_pkgs) unless @options[Enum::Option::FORMAT] == Enum::Format::CSV
76
+ print_disclaimer(technology) unless @options[Enum::Option::FORMAT] == Enum::Format::CSV || pkgs.empty?
64
77
  end
65
78
 
66
79
  def print_summary(technology, pkgs, ignored_pkgs)
67
80
  if @report == Enum::Report::ALL
68
- Util::SummaryPrinter.statistics(technology, @report, pkgs, ignored_pkgs)
81
+ Util::SummaryPrinter.statistics(@options[Enum::Option::FORMAT], technology, @report, pkgs, ignored_pkgs)
69
82
  else
70
83
  Util::SummaryPrinter.total(technology, @report, pkgs, ignored_pkgs)
71
84
  end
@@ -91,7 +104,7 @@ module Package
91
104
  end
92
105
  end
93
106
 
94
- def parse_config_file
107
+ def parse_config_file!
95
108
  if @options[Enum::Option::CONFIG].nil?
96
109
  YAML.load_file("#{@dir}/#{Const::File::CONFIG}") if File.exist? "#{@dir}/#{Const::File::CONFIG}"
97
110
  elsif File.exist? @options[Enum::Option::CONFIG]
@@ -101,7 +114,13 @@ module Package
101
114
  end
102
115
  end
103
116
 
104
- def parse_technologies
117
+ def validate_format!
118
+ format = @options[Enum::Option::FORMAT]
119
+ raise ArgumentError, "Invalid format: #{format}, should be one of [#{Enum::Format.all.join('|')}]" unless
120
+ @options[Enum::Option::FORMAT].nil? || Enum::Format.all.include?(format)
121
+ end
122
+
123
+ def parse_technologies!
105
124
  technology_validator = Technology::Validator.new(@dir)
106
125
  @options[Enum::Option::TECHNOLOGY]&.each { |technology| technology_validator.validate! technology }
107
126
  @options[Enum::Option::TECHNOLOGY] || Technology::Detector.new(@dir).detect
@@ -9,7 +9,8 @@ require 'yaml'
9
9
  module Package
10
10
  module Audit
11
11
  class PackageFilter
12
- def initialize(config)
12
+ def initialize(report, config)
13
+ @report = report
13
14
  @config = config
14
15
  end
15
16
 
@@ -30,9 +31,28 @@ module Package
30
31
  end
31
32
 
32
33
  def ignore_package?(pkg, yaml)
33
- (!pkg.deprecated? || yaml&.dig(Const::YAML::DEPRECATED) == false) &&
34
- (!pkg.outdated? || yaml&.dig(Const::YAML::OUTDATED) == false) &&
35
- (!pkg.vulnerable? || yaml&.dig(Const::YAML::VULNERABLE) == false)
34
+ case @report
35
+ when Enum::Report::DEPRECATED
36
+ ignore_deprecated?(pkg, yaml)
37
+ when Enum::Report::OUTDATED
38
+ ignore_outdated?(pkg, yaml)
39
+ when Enum::Report::VULNERABLE
40
+ ignore_vulnerable?(pkg, yaml)
41
+ else
42
+ ignore_deprecated?(pkg, yaml) && ignore_outdated?(pkg, yaml) && ignore_vulnerable?(pkg, yaml)
43
+ end
44
+ end
45
+
46
+ def ignore_deprecated?(pkg, yaml)
47
+ !pkg.deprecated? || yaml&.dig(Const::YAML::DEPRECATED) == false
48
+ end
49
+
50
+ def ignore_outdated?(pkg, yaml)
51
+ !pkg.outdated? || yaml&.dig(Const::YAML::OUTDATED) == false
52
+ end
53
+
54
+ def ignore_vulnerable?(pkg, yaml)
55
+ !pkg.vulnerable? || yaml&.dig(Const::YAML::VULNERABLE) == false
36
56
  end
37
57
  end
38
58
  end
@@ -48,7 +48,7 @@ module Package
48
48
  end
49
49
 
50
50
  def filter_pkgs_based_on_config(pkgs)
51
- package_filter = PackageFilter.new(@config)
51
+ package_filter = PackageFilter.new(@report, @config)
52
52
  ignored_pkgs = []
53
53
 
54
54
  pkgs.each do |pkg|
@@ -21,8 +21,11 @@ module Package
21
21
  check_fields(fields)
22
22
  return if @pkgs.empty?
23
23
 
24
- if @options[Enum::Option::CSV]
24
+ case @options[Enum::Option::FORMAT]
25
+ when Enum::Format::CSV
25
26
  csv(fields, exclude_headers: @options[Enum::Option::CSV_EXCLUDE_HEADERS])
27
+ when Enum::Format::MARKDOWN
28
+ markdown(fields)
26
29
  else
27
30
  pretty(fields)
28
31
  end
@@ -39,72 +42,78 @@ module Package
39
42
  "Available fields names are: #{Const::Fields::DEFAULT}."
40
43
  end
41
44
 
42
- def pretty(fields = Const::Fields::DEFAULT) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
43
- # find the maximum length of each field across all the packages so we know how many
44
- # characters of horizontal space to allocate for each field when printing
45
- fields.each do |key|
46
- instance_variable_set "@max_#{key}", Const::Fields::HEADERS[key].length
47
- @pkgs.each do |gem|
48
- curr_field_length = case key
49
- when :vulnerabilities
50
- gem.vulnerabilities_grouped.length
51
- when :groups
52
- gem.group_list.length
53
- else
54
- gem.send(key)&.gsub(BASH_FORMATTING_REGEX, '')&.length || 0
55
- end
56
- max_field_length = instance_variable_get "@max_#{key}"
57
- instance_variable_set "@max_#{key}", [curr_field_length, max_field_length].max
58
- end
59
- end
60
-
61
- line_length = fields.sum { |key| instance_variable_get "@max_#{key}" } +
62
- (COLUMN_GAP * (fields.length - 1))
45
+ def pretty(fields = Const::Fields::DEFAULT) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
46
+ max_widths = get_field_max_widths(fields)
47
+ header = fields.map.with_index do |field, index|
48
+ Const::Fields::HEADERS[field].gsub(BASH_FORMATTING_REGEX, '').ljust(max_widths[index])
49
+ end.join(' ' * COLUMN_GAP)
50
+ separator = max_widths.map { |width| '=' * width }.join('=' * COLUMN_GAP)
63
51
 
64
- puts '=' * line_length
65
- puts fields.map { |key|
66
- Const::Fields::HEADERS[key].gsub(BASH_FORMATTING_REGEX, '').ljust(instance_variable_get("@max_#{key}"))
67
- }.join(' ' * COLUMN_GAP)
68
- puts '=' * line_length
52
+ puts separator
53
+ puts header
54
+ puts separator
69
55
 
70
56
  @pkgs.each do |pkg|
71
- puts fields.map { |key|
72
- val = pkg.send(key) || ''
73
- val = case key
74
- when :groups
75
- pkg.group_list
76
- when :risk_type
77
- Formatter::Risk.new(pkg.risk_type).format
78
- when :version
79
- Formatter::Version.new(pkg.version, pkg.latest_version).format
80
- when :vulnerabilities
81
- Formatter::Vulnerability.new(pkg.vulnerabilities).format
82
- when :latest_version_date
83
- Formatter::VersionDate.new(pkg.latest_version_date).format
84
- else
85
- val
86
- end
87
-
57
+ puts fields.map.with_index { |key, index|
58
+ val = get_field_value(pkg, key)
88
59
  formatting_length = val.length - val.gsub(BASH_FORMATTING_REGEX, '').length
89
- val.ljust(instance_variable_get("@max_#{key}") + formatting_length)
60
+ val.ljust(max_widths[index] + formatting_length)
90
61
  }.join(' ' * COLUMN_GAP)
91
62
  end
92
63
  end
93
64
 
94
- def csv(fields, exclude_headers: false)
95
- value_fields = fields.map do |field|
96
- case field
97
- when :groups
98
- :group_list
99
- when :vulnerabilities
100
- :vulnerabilities_grouped
101
- else
102
- field
65
+ def csv(fields = Const::Fields::DEFAULT, exclude_headers: false)
66
+ puts fields.join(',') unless exclude_headers
67
+ @pkgs.map do |pkg|
68
+ puts fields.map { |field| get_field_value(pkg, field) }.join(',').gsub(BASH_FORMATTING_REGEX, '')
69
+ end
70
+ end
71
+
72
+ def markdown(fields = Const::Fields::DEFAULT) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
73
+ max_widths = get_field_max_widths(fields)
74
+ header = fields.map.with_index do |field, index|
75
+ Const::Fields::HEADERS[field].gsub(BASH_FORMATTING_REGEX, '').ljust(max_widths[index])
76
+ end.join(' | ')
77
+ separator = max_widths.map { |width| ":#{'-' * width}" }.join('-|')
78
+
79
+ puts "| #{header} |"
80
+ puts "|#{separator}-|"
81
+
82
+ @pkgs.each do |pkg|
83
+ row = fields.map.with_index do |key, index|
84
+ val = get_field_value(pkg, key)
85
+ formatting_length = val.length - val.gsub(BASH_FORMATTING_REGEX, '').length
86
+ val.ljust(max_widths[index] + formatting_length)
103
87
  end
88
+ puts "| #{row.join(' | ')} |"
104
89
  end
90
+ end
105
91
 
106
- puts fields.join(',') unless exclude_headers
107
- @pkgs.map { |gem| puts gem.to_csv(value_fields) }
92
+ def get_field_max_widths(fields)
93
+ # Calculate the maximum width for each column, including header titles and content
94
+ fields.map do |field|
95
+ [@pkgs.map do |pkg|
96
+ value = get_field_value(pkg, field).to_s.gsub(BASH_FORMATTING_REGEX, '').length
97
+ value
98
+ end.max, Const::Fields::HEADERS[field].gsub(BASH_FORMATTING_REGEX, '').length].max
99
+ end
100
+ end
101
+
102
+ def get_field_value(pkg, field) # rubocop:disable Metrics/MethodLength
103
+ case field
104
+ when :groups
105
+ pkg.group_list
106
+ when :risk_type
107
+ Formatter::Risk.new(pkg.risk_type).format
108
+ when :version
109
+ Formatter::Version.new(pkg.version, pkg.latest_version).format
110
+ when :vulnerabilities
111
+ Formatter::Vulnerability.new(pkg.vulnerabilities).format
112
+ when :latest_version_date
113
+ Formatter::VersionDate.new(pkg.latest_version_date).format
114
+ else
115
+ pkg.send(field) || ''
116
+ end
108
117
  end
109
118
  end
110
119
  end
@@ -28,27 +28,20 @@ module Package
28
28
  package_lock_json_present = File.exist?("#{@dir}/#{Const::File::PACKAGE_LOCK_JSON}")
29
29
  yarn_lock_present = File.exist?("#{@dir}/#{Const::File::YARN_LOCK}")
30
30
 
31
- unless package_json_present
32
- puts Util::BashColor.red("\"#{Const::File::PACKAGE_JSON}\" was not found in #{@dir}")
33
- end
34
- unless package_lock_json_present || yarn_lock_present
35
- puts Util::BashColor.red("\"#{Const::File::PACKAGE_LOCK_JSON}\" or \"#{Const::File::YARN_LOCK}\" " \
36
- "was not found in #{@dir}")
37
- end
31
+ raise "\"#{Const::File::PACKAGE_JSON}\" was not found in #{@dir}" unless package_json_present
32
+
33
+ return if package_lock_json_present || yarn_lock_present
38
34
 
39
- exit 1 unless package_json_present && (package_lock_json_present || yarn_lock_present)
35
+ raise "\"#{Const::File::PACKAGE_LOCK_JSON}\" or \"#{Const::File::YARN_LOCK}\" " \
36
+ "was not found in #{@dir}"
40
37
  end
41
38
 
42
39
  def validate_ruby!
43
40
  gemfile_present = File.exist?("#{@dir}/#{Const::File::GEMFILE}")
44
41
  gemfile_lock_present = File.exist?("#{@dir}/#{Const::File::GEMFILE_LOCK}")
45
42
 
46
- puts Util::BashColor.red("\"#{Const::File::GEMFILE}\" was not found in #{@dir}") unless gemfile_present
47
- unless gemfile_lock_present
48
- puts Util::BashColor.red("\"#{Const::File::GEMFILE_LOCK}\" was not found in #{@dir}")
49
- end
50
-
51
- exit 1 unless gemfile_present && gemfile_lock_present
43
+ raise "\"#{Const::File::GEMFILE}\" was not found in #{@dir}" unless gemfile_present
44
+ raise "\"#{Const::File::GEMFILE_LOCK}\" was not found in #{@dir}" unless gemfile_lock_present
52
45
  end
53
46
  end
54
47
  end
@@ -0,0 +1,49 @@
1
+ require_relative '../const/time'
2
+ require_relative 'bash_color'
3
+
4
+ module Package
5
+ module Audit
6
+ module Util
7
+ module RiskLegend
8
+ def self.print # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
9
+ puts Util::BashColor.blue('1. Check if the package has a security vulnerability.')
10
+ puts ' If yes, the following vulnerability -> risk mapping is used:'
11
+ puts " - #{Util::BashColor.red('unknown')} vulnerability\t-> #{Util::BashColor.red('high')} risk"
12
+ puts " - #{Util::BashColor.red('critical')} vulnerability\t-> #{Util::BashColor.red('high')} risk"
13
+ puts " - #{Util::BashColor.red('high')} vulnerability\t-> #{Util::BashColor.red('high')} risk"
14
+ puts " - #{Util::BashColor.orange('medium')} vulnerability\t-> #{Util::BashColor.orange('medium')} risk"
15
+ puts " - #{Util::BashColor.orange('moderate')} vulnerability\t-> #{Util::BashColor.orange('medium')} risk" # rubocop:disable Layout/LineLength
16
+ puts " - #{Util::BashColor.yellow('low')} vulnerability\t-> #{Util::BashColor.yellow('low')} risk"
17
+
18
+ puts
19
+
20
+ puts Util::BashColor.blue('2. Check the package for potential deprecation.')
21
+ puts " If no new releases by author for at least #{Const::Time::YEARS_ELAPSED_TO_BE_OUTDATED} years:"
22
+ puts " - assign the risk to\t-> #{Util::BashColor.orange('medium')} risk"
23
+
24
+ puts
25
+
26
+ puts Util::BashColor.blue('3. Check if a newer version of the package is available.')
27
+
28
+ puts ' If yes, assign risk as follows:'
29
+ puts " - #{Util::BashColor.orange('major version')} mismatch\t-> #{Util::BashColor.orange('medium')} risk" # rubocop:disable Layout/LineLength
30
+ puts " - #{Util::BashColor.yellow('minor version')} mismatch\t-> #{Util::BashColor.yellow('low')} risk"
31
+ puts " - #{Util::BashColor.green('patch version')} mismatch\t-> #{Util::BashColor.yellow('low')} risk"
32
+ puts " - #{Util::BashColor.green('build version')} mismatch\t-> #{Util::BashColor.yellow('low')} risk"
33
+
34
+ puts
35
+
36
+ puts Util::BashColor.blue('4. Take the highest risk from the first 3 steps.')
37
+ puts ' If two risks match in severity, use the following precedence:'
38
+ puts " - #{Util::BashColor.red('vulnerability')} > #{Util::BashColor.orange('deprecation')} > #{Util::BashColor.yellow('outdatedness')}" # rubocop:disable Layout/LineLength
39
+
40
+ puts
41
+
42
+ puts Util::BashColor.blue('5. Check whether the package is used in production or not.')
43
+ puts ' If a package is limited to a non-production group:'
44
+ puts " - cap risk severity to\t -> #{Util::BashColor.yellow('low')} risk"
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -33,59 +33,71 @@ module Package
33
33
  end
34
34
  end
35
35
 
36
- def self.statistics(technology, report, pkgs, ignored_pkgs)
37
- outdated = pkgs.count(&:outdated?)
38
- deprecated = pkgs.count(&:deprecated?)
39
- vulnerable = pkgs.count(&:vulnerable?)
36
+ def self.statistics(format, technology, report, pkgs, ignored_pkgs)
37
+ stats = calculate_statistics(pkgs, ignored_pkgs)
38
+ display_results(format, technology, report, pkgs, ignored_pkgs, stats)
39
+ end
40
+
41
+ private_class_method def self.calculate_statistics(pkgs, ignored_pkgs)
42
+ stats = {
43
+ outdated: count_status(pkgs, :outdated?),
44
+ deprecated: count_status(pkgs, :deprecated?),
45
+ vulnerable: count_status(pkgs, :vulnerable?),
46
+ outdated_ignored: count_status(ignored_pkgs, :outdated?),
47
+ deprecated_ignored: count_status(ignored_pkgs, :deprecated?),
48
+ vulnerable_ignored: count_status(ignored_pkgs, :vulnerable?)
49
+ }
50
+
51
+ stats[:vulnerabilities] = pkgs.sum { |pkg| pkg.vulnerabilities.length }
52
+ stats
53
+ end
40
54
 
41
- vulnerabilities = pkgs.sum { |pkg| pkg.vulnerabilities.length }
55
+ private_class_method def self.count_status(pkgs, status)
56
+ pkgs.count(&status)
57
+ end
42
58
 
59
+ private_class_method def self.display_results(format, technology, report, pkgs, ignored_pkgs, stats) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/ParameterLists
43
60
  if pkgs.any?
44
- puts Util::BashColor.cyan("#{vulnerable} vulnerable (#{vulnerabilities} vulnerabilities), " \
45
- "#{outdated} outdated, #{deprecated} deprecated.")
61
+ print status_message(stats)
62
+ print Util::BashColor.cyan(' \\') if format == Enum::Format::MARKDOWN
63
+ puts
46
64
  total(technology, report, pkgs, ignored_pkgs)
65
+ elsif ignored_pkgs.any?
66
+ print status_message(stats)
67
+ print Util::BashColor.cyan(' \\') if format == Enum::Format::MARKDOWN
68
+ puts
69
+ puts Util::BashColor.green("There are no deprecated, outdated or vulnerable #{technology} " \
70
+ "packages (#{ignored_pkgs.length} ignored)!\n")
47
71
  else
48
72
  puts Util::BashColor.green("There are no deprecated, outdated or vulnerable #{technology} packages!\n")
49
73
  end
50
74
  end
51
75
 
52
- def self.risk # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
53
- puts Util::BashColor.blue('1. Check if the package has a security vulnerability.')
54
- puts ' If yes, the following vulnerability -> risk mapping is used:'
55
- puts " - #{Util::BashColor.red('unknown')} vulnerability\t-> #{Util::BashColor.red('high')} risk"
56
- puts " - #{Util::BashColor.red('critical')} vulnerability\t-> #{Util::BashColor.red('high')} risk"
57
- puts " - #{Util::BashColor.red('high')} vulnerability\t-> #{Util::BashColor.red('high')} risk"
58
- puts " - #{Util::BashColor.orange('medium')} vulnerability\t-> #{Util::BashColor.orange('medium')} risk"
59
- puts " - #{Util::BashColor.orange('moderate')} vulnerability\t-> #{Util::BashColor.orange('medium')} risk" # rubocop:disable Layout/LineLength
60
- puts " - #{Util::BashColor.yellow('low')} vulnerability\t-> #{Util::BashColor.yellow('low')} risk"
61
-
62
- puts
63
-
64
- puts Util::BashColor.blue('2. Check the package for potential deprecation.')
65
- puts " If no new releases by author for at least #{Const::Time::YEARS_ELAPSED_TO_BE_OUTDATED} years:"
66
- puts " - assign the risk to\t-> #{Util::BashColor.orange('medium')} risk"
76
+ private_class_method def self.status_message(stats)
77
+ outdated_str = "#{stats[:outdated]} outdated" + outdated_details(stats)
78
+ deprecated_str = "#{stats[:deprecated]} deprecated" + deprecated_details(stats)
79
+ vulnerable_str = "#{stats[:vulnerable]} vulnerable" + vulnerability_details(stats)
67
80
 
68
- puts
69
-
70
- puts Util::BashColor.blue('3. Check if a newer version of the package is available.')
71
-
72
- puts ' If yes, assign risk as follows:'
73
- puts " - #{Util::BashColor.orange('major version')} mismatch\t-> #{Util::BashColor.orange('medium')} risk" # rubocop:disable Layout/LineLength
74
- puts " - #{Util::BashColor.yellow('minor version')} mismatch\t-> #{Util::BashColor.yellow('low')} risk"
75
- puts " - #{Util::BashColor.green('patch version')} mismatch\t-> #{Util::BashColor.yellow('low')} risk"
76
- puts " - #{Util::BashColor.green('build version')} mismatch\t-> #{Util::BashColor.yellow('low')} risk"
77
-
78
- puts
81
+ Util::BashColor.cyan("#{vulnerable_str}, #{outdated_str}, #{deprecated_str}.")
82
+ end
79
83
 
80
- puts Util::BashColor.blue('4. Take the highest risk from the first 3 steps.')
81
- puts ' If two risks match in severity, use the following precedence:'
82
- puts " - #{Util::BashColor.red('vulnerability')} > #{Util::BashColor.orange('deprecation')} > #{Util::BashColor.yellow('outdatedness')}" # rubocop:disable Layout/LineLength
84
+ private_class_method def self.deprecated_details(stats)
85
+ details = []
86
+ details << "#{stats[:deprecated_ignored]} ignored" if stats[:deprecated_ignored].positive?
87
+ details.any? ? " (#{details.join(', ')})" : ''
88
+ end
83
89
 
84
- puts
90
+ private_class_method def self.outdated_details(stats)
91
+ details = []
92
+ details << "#{stats[:outdated_ignored]} ignored" if stats[:outdated_ignored].positive?
93
+ details.any? ? " (#{details.join(', ')})" : ''
94
+ end
85
95
 
86
- puts Util::BashColor.blue('5. Check whether the package is used in production or not.')
87
- puts ' If a package is limited to a non-production group:'
88
- puts " - cap risk severity to\t -> #{Util::BashColor.yellow('low')} risk"
96
+ private_class_method def self.vulnerability_details(stats)
97
+ details = []
98
+ details << "#{stats[:vulnerabilities]} vulnerabilities" if stats[:vulnerabilities].positive?
99
+ details << "#{stats[:vulnerable_ignored]} ignored" if stats[:vulnerable_ignored].positive?
100
+ details.any? ? " (#{details.join(', ')})" : ''
89
101
  end
90
102
  end
91
103
  end
@@ -1,5 +1,5 @@
1
1
  module Package
2
2
  module Audit
3
- VERSION = '0.5.1'
3
+ VERSION = '0.6.0'
4
4
  end
5
5
  end
@@ -3,6 +3,8 @@ module Package
3
3
  class CLI
4
4
  def self.exit_on_failure?: -> bool
5
5
 
6
+ def default: (String) -> void
7
+
6
8
  def deprecated: (String) -> void
7
9
 
8
10
  def outdated: (String) -> void
@@ -0,0 +1,12 @@
1
+ module Package
2
+ module Audit
3
+ module Enum
4
+ module Format
5
+ CSV: String
6
+ MARKDOWN: String
7
+
8
+ def self.all: -> Array[String]
9
+ end
10
+ end
11
+ end
12
+ end
@@ -3,7 +3,7 @@ module Package
3
3
  module Enum
4
4
  module Option
5
5
  CONFIG: String
6
- CSV: String
6
+ FORMAT: String
7
7
  CSV_EXCLUDE_HEADERS: String
8
8
  GROUP: String
9
9
  INCLUDE_IGNORED: String
@@ -6,7 +6,7 @@ module Package
6
6
 
7
7
  def initialize: (String) -> void
8
8
 
9
- def format: -> void
9
+ def format: -> String
10
10
  end
11
11
  end
12
12
  end
@@ -17,15 +17,21 @@ module Package
17
17
 
18
18
  def learn_more_command: (String) -> String?
19
19
 
20
- def parse_config_file: -> Hash[String, untyped]?
20
+ def parse_config_file!: -> Hash[String, untyped]?
21
21
 
22
22
  def parse_technologies: -> Array[String]
23
23
 
24
+ def parse_technologies!: -> Array[String]
25
+
24
26
  def print_disclaimer: (String) -> void
25
27
 
26
28
  def print_results: (String, Array[Package], Array[Package]) -> void
27
29
 
28
30
  def print_summary: (String, Array[Package], Array[Package]) -> void
31
+
32
+ def process_technologies: -> int
33
+
34
+ def validate_format!: -> void
29
35
  end
30
36
  end
31
37
  end
@@ -2,15 +2,22 @@ module Package
2
2
  module Audit
3
3
  class PackageFilter
4
4
  @config: Hash[String, untyped]?
5
+ @report: Symbol
5
6
 
6
- def initialize: (Hash[String, untyped]?) -> void
7
+ def initialize: (Symbol, Hash[String, untyped]?) -> void
7
8
 
8
9
  def ignored?: (Package) -> bool
9
10
 
10
11
  private
11
12
 
13
+ def ignore_deprecated?: (Package, Hash[String, untyped]?) -> bool
14
+
15
+ def ignore_outdated?: (Package, Hash[String, untyped]?) -> bool
16
+
12
17
  def ignore_package?: (Package, Hash[String, untyped]?) -> bool
13
18
 
19
+ def ignore_vulnerable?: (Package, Hash[String, untyped]?) -> bool
20
+
14
21
  def pkg_version_in_config?: (Package, Hash[String, untyped]?) -> bool
15
22
 
16
23
  def pkg_yaml_from_config: (Package) -> Hash[String, untyped]?
@@ -18,7 +18,13 @@ module Package
18
18
 
19
19
  def csv: (Array[Symbol], ?exclude_headers: bool) -> void
20
20
 
21
- def pretty: (?Array[Symbol]) -> void
21
+ def get_field_max_widths: (Array[Symbol]) -> Array[Integer]
22
+
23
+ def get_field_value: (Package, Symbol) -> String
24
+
25
+ def markdown: (Array[Symbol]) -> void
26
+
27
+ def pretty: (Array[Symbol]) -> void
22
28
  end
23
29
  end
24
30
  end
@@ -0,0 +1,9 @@
1
+ module Package
2
+ module Audit
3
+ module Util
4
+ module RiskLegend
5
+ def self.print: -> void
6
+ end
7
+ end
8
+ end
9
+ end
@@ -4,15 +4,29 @@ module Package
4
4
  module SummaryPrinter
5
5
  def self.all: -> void
6
6
 
7
+ def self.calculate_statistics: (Array[Package], Array[Package]) -> Hash[Symbol, Integer]
8
+
9
+ def self.count_status: (Array[Package], Symbol) -> Integer
10
+
7
11
  def self.deprecated: -> void
8
12
 
9
- def self.risk: -> void
13
+ def self.display_results: (String, String, Symbol, Array[Package], Array[Package], Hash[Symbol, Integer]) -> void
10
14
 
11
- def self.statistics: (String, Symbol, Array[Package], Array[Package]) -> void
15
+ def self.statistics: (String, String, Symbol, Array[Package], Array[Package]) -> void
16
+
17
+ def self.status_message: (Hash[Symbol, Integer]) -> String
12
18
 
13
19
  def self.total: (String, Symbol, Array[Package], Array[Package]) -> void
14
20
 
15
21
  def self.vulnerable: (String, String?) -> void
22
+
23
+ def self.deprecated_details: (Hash[Symbol, Integer]) -> String
24
+
25
+ def self.outdated_details: (Hash[Symbol, Integer]) -> String
26
+
27
+ def self.vulnerability_details: (Hash[Symbol, Integer]) -> String
28
+
29
+ def calculate_statistics: (Array[Package], Array[Package]) -> Hash[Symbol, Integer]
16
30
  end
17
31
  end
18
32
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: package-audit
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.1
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tactica Communications Inc.
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-11-22 00:00:00.000000000 Z
11
+ date: 2024-02-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler-audit
@@ -54,6 +54,7 @@ files:
54
54
  - lib/package/audit/const/file.rb
55
55
  - lib/package/audit/const/time.rb
56
56
  - lib/package/audit/const/yaml.rb
57
+ - lib/package/audit/enum/format.rb
57
58
  - lib/package/audit/enum/group.rb
58
59
  - lib/package/audit/enum/option.rb
59
60
  - lib/package/audit/enum/report.rb
@@ -85,6 +86,7 @@ files:
85
86
  - lib/package/audit/technology/detector.rb
86
87
  - lib/package/audit/technology/validator.rb
87
88
  - lib/package/audit/util/bash_color.rb
89
+ - lib/package/audit/util/risk_legend.rb
88
90
  - lib/package/audit/util/spinner.rb
89
91
  - lib/package/audit/util/summary_printer.rb
90
92
  - lib/package/audit/version.rb
@@ -94,6 +96,7 @@ files:
94
96
  - sig/package/audit/const/file.rbs
95
97
  - sig/package/audit/const/time.rbs
96
98
  - sig/package/audit/const/yaml.rbs
99
+ - sig/package/audit/enum/format.rbs
97
100
  - sig/package/audit/enum/group.rbs
98
101
  - sig/package/audit/enum/option.rbs
99
102
  - sig/package/audit/enum/report.rbs
@@ -125,6 +128,7 @@ files:
125
128
  - sig/package/audit/technology/detector.rbs
126
129
  - sig/package/audit/technology/validator.rbs
127
130
  - sig/package/audit/util/bash_color.rbs
131
+ - sig/package/audit/util/risk_legend.rbs
128
132
  - sig/package/audit/util/spinner.rbs
129
133
  - sig/package/audit/util/summary_printer.rbs
130
134
  - sig/package/audit/version.rbs
@@ -143,14 +147,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
143
147
  requirements:
144
148
  - - ">="
145
149
  - !ruby/object:Gem::Version
146
- version: 2.7.0
150
+ version: 2.6.0
147
151
  required_rubygems_version: !ruby/object:Gem::Requirement
148
152
  requirements:
149
153
  - - ">="
150
154
  - !ruby/object:Gem::Version
151
155
  version: '0'
152
156
  requirements: []
153
- rubygems_version: 3.4.21
157
+ rubygems_version: 3.4.22
154
158
  signing_key:
155
159
  specification_version: 4
156
160
  summary: A helper tool to find outdated, deprecated and vulnerable dependencies.