package-audit 0.5.1 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
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.