package-audit 0.5.1 → 0.6.1
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 +4 -4
- data/lib/package/audit/cli.rb +11 -9
- data/lib/package/audit/const/cmd.rb +2 -2
- data/lib/package/audit/enum/format.rb +14 -0
- data/lib/package/audit/enum/option.rb +1 -1
- data/lib/package/audit/enum/technology.rb +1 -1
- data/lib/package/audit/models/package.rb +2 -2
- data/lib/package/audit/npm/node_collection.rb +17 -0
- data/lib/package/audit/npm/npm_meta_data.rb +24 -2
- data/lib/package/audit/npm/vulnerability_finder.rb +7 -1
- data/lib/package/audit/ruby/bundler_specs.rb +16 -1
- data/lib/package/audit/services/command_parser.rb +56 -15
- data/lib/package/audit/services/config_cleaner.rb +221 -0
- data/lib/package/audit/services/package_filter.rb +24 -4
- data/lib/package/audit/services/package_finder.rb +1 -1
- data/lib/package/audit/services/package_printer.rb +65 -56
- data/lib/package/audit/technology/validator.rb +7 -14
- data/lib/package/audit/util/risk_legend.rb +49 -0
- data/lib/package/audit/util/spinner.rb +1 -1
- data/lib/package/audit/util/summary_printer.rb +58 -45
- data/lib/package/audit/version.rb +1 -1
- metadata +12 -52
- data/sig/package/audit/cli.rbs +0 -33
- data/sig/package/audit/const/cmd.rbs +0 -14
- data/sig/package/audit/const/fields.rbs +0 -11
- data/sig/package/audit/const/file.rbs +0 -14
- data/sig/package/audit/const/time.rbs +0 -11
- data/sig/package/audit/const/yaml.rbs +0 -13
- data/sig/package/audit/enum/group.rbs +0 -15
- data/sig/package/audit/enum/option.rbs +0 -14
- data/sig/package/audit/enum/report.rbs +0 -12
- data/sig/package/audit/enum/risk_explanation.rbs +0 -12
- data/sig/package/audit/enum/risk_type.rbs +0 -12
- data/sig/package/audit/enum/technology.rbs +0 -12
- data/sig/package/audit/enum/vulnerability_type.rbs +0 -15
- data/sig/package/audit/formatter/base.rbs +0 -9
- data/sig/package/audit/formatter/risk_printer.rbs +0 -13
- data/sig/package/audit/formatter/version_date.rbs +0 -13
- data/sig/package/audit/formatter/version_printer.rbs +0 -14
- data/sig/package/audit/formatter/vulnerability.rbs +0 -13
- data/sig/package/audit/models/package.rbs +0 -47
- data/sig/package/audit/models/risk.rbs +0 -12
- data/sig/package/audit/npm/node_collection.rbs +0 -28
- data/sig/package/audit/npm/npm_meta_data.rbs +0 -19
- data/sig/package/audit/npm/vulnerability_finder.rbs +0 -21
- data/sig/package/audit/npm/yarn_lock_parser.rbs +0 -22
- data/sig/package/audit/ruby/bundler_specs.rbs +0 -11
- data/sig/package/audit/ruby/gem_collection.rbs +0 -22
- data/sig/package/audit/ruby/gem_meta_data.rbs +0 -23
- data/sig/package/audit/ruby/vulnerability_finder.rbs +0 -18
- data/sig/package/audit/services/command_parser.rbs +0 -31
- data/sig/package/audit/services/duplicate_package_merger.rbs +0 -11
- data/sig/package/audit/services/package_filter.rbs +0 -19
- data/sig/package/audit/services/package_finder.rbs +0 -26
- data/sig/package/audit/services/package_printer.rbs +0 -24
- data/sig/package/audit/services/risk_calculator.rbs +0 -21
- data/sig/package/audit/technology/detector.rbs +0 -19
- data/sig/package/audit/technology/validator.rbs +0 -19
- data/sig/package/audit/util/bash_color.rbs +0 -21
- data/sig/package/audit/util/spinner.rbs +0 -24
- data/sig/package/audit/util/summary_printer.rbs +0 -19
- data/sig/package/audit/version.rbs +0 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c4f817af73ba4616da54a934f0f6c4120d921e6026226c20a02c592c6b55e064
|
4
|
+
data.tar.gz: 818282a8f489ba8afd7b9675b78db96c3e2b9b18731415085ff44c25eaf07a77
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 46267de0e991aaf0e1fdeb4f8c778cc2a59abf49f7d7a0849ae9731c9fb633ae8c958ac1865bf17aa98d50cd359e8e30d16975031f4dfd4fc1a7c2aacf6868fc
|
7
|
+
data.tar.gz: 65c6acb5e9e224f6736781f1db26eee0ec7772f220c87b8e8da1f2cecc872cfb667f78336314ad4b670000f849c60051087cdc9cda1b9e69ad11d896b717c471
|
data/lib/package/audit/cli.rb
CHANGED
@@ -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 :
|
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::
|
28
|
-
|
29
|
-
desc: 'Output reports using
|
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
|
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 '
|
38
|
-
def
|
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::
|
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 :
|
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
|
@@ -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
|
@@ -58,9 +58,26 @@ module Package
|
|
58
58
|
package_json = JSON.parse(File.read("#{@dir}/#{Const::File::PACKAGE_JSON}"), symbolize_names: true)
|
59
59
|
default_deps = package_json[:dependencies] || {}
|
60
60
|
dev_deps = package_json[:devDependencies] || {}
|
61
|
+
|
62
|
+
# Filter out local dependencies before processing
|
63
|
+
default_deps = filter_local_dependencies(default_deps)
|
64
|
+
dev_deps = filter_local_dependencies(dev_deps)
|
65
|
+
|
61
66
|
[default_deps, dev_deps]
|
62
67
|
end
|
63
68
|
|
69
|
+
def filter_local_dependencies(dependencies)
|
70
|
+
dependencies.reject { |_name, version| local_dependency?(version) }
|
71
|
+
end
|
72
|
+
|
73
|
+
def local_dependency?(version)
|
74
|
+
# Check for local file paths
|
75
|
+
version.to_s.start_with?('file:', 'link:', './', '../') ||
|
76
|
+
version.to_s.include?('file:') ||
|
77
|
+
# Check for git repositories with local paths
|
78
|
+
(version.to_s.start_with?('git+') && version.to_s.include?('file:'))
|
79
|
+
end
|
80
|
+
|
64
81
|
def fetch_from_lock_file
|
65
82
|
default_deps, dev_deps = fetch_from_package_json
|
66
83
|
if File.exist?("#{@dir}/#{Const::File::YARN_LOCK}")
|
@@ -1,5 +1,6 @@
|
|
1
1
|
require 'json'
|
2
2
|
require 'net/http'
|
3
|
+
require 'socket'
|
3
4
|
|
4
5
|
module Package
|
5
6
|
module Audit
|
@@ -11,7 +12,7 @@ module Package
|
|
11
12
|
@packages = packages
|
12
13
|
end
|
13
14
|
|
14
|
-
def fetch # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
15
|
+
def fetch # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
|
15
16
|
threads = @packages.map do |package|
|
16
17
|
Thread.new do
|
17
18
|
response = Net::HTTP.get_response(URI.parse("#{REGISTRY_URL}/#{package.name}"))
|
@@ -20,14 +21,35 @@ module Package
|
|
20
21
|
|
21
22
|
json_package = JSON.parse(response.body, symbolize_names: true)
|
22
23
|
update_meta_data(package, json_package)
|
24
|
+
rescue Net::TimeoutError, Net::OpenTimeout, Net::ReadTimeout => e
|
25
|
+
warn "Warning: Network timeout while fetching metadata for #{package.name}: #{e.message}"
|
26
|
+
Thread.current[:exception] = e
|
27
|
+
rescue SocketError, Errno::ECONNREFUSED, Errno::EHOSTUNREACH => e
|
28
|
+
warn "Warning: Network error while fetching metadata for #{package.name}: #{e.message}"
|
29
|
+
Thread.current[:exception] = e
|
23
30
|
rescue StandardError => e
|
24
31
|
Thread.current[:exception] = e
|
25
32
|
end
|
26
33
|
end
|
34
|
+
|
35
|
+
network_errors = []
|
27
36
|
threads.each do |thread|
|
28
37
|
thread.join
|
29
|
-
|
38
|
+
next unless thread[:exception]
|
39
|
+
|
40
|
+
case thread[:exception]
|
41
|
+
when Net::TimeoutError, Net::OpenTimeout, Net::ReadTimeout, SocketError, Errno::ECONNREFUSED, Errno::EHOSTUNREACH # rubocop:disable Layout/LineLength
|
42
|
+
network_errors << thread[:exception]
|
43
|
+
else
|
44
|
+
raise thread[:exception]
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
unless network_errors.empty?
|
49
|
+
warn "Warning: #{network_errors.size} network error(s) occurred while fetching package metadata."
|
50
|
+
warn 'Some packages may not show complete version information.'
|
30
51
|
end
|
52
|
+
|
31
53
|
@packages
|
32
54
|
end
|
33
55
|
|
@@ -1,3 +1,5 @@
|
|
1
|
+
require 'open3'
|
2
|
+
|
1
3
|
require_relative '../const/cmd'
|
2
4
|
require_relative '../enum/vulnerability_type'
|
3
5
|
|
@@ -14,7 +16,11 @@ module Package
|
|
14
16
|
end
|
15
17
|
|
16
18
|
def run
|
17
|
-
|
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' }
|
22
|
+
|
23
|
+
json_string_lines, = Open3.capture3(env_vars, command)
|
18
24
|
array = json_string_lines.scan(AUDIT_ADVISORY_REGEX)
|
19
25
|
|
20
26
|
vulnerability_json_array = JSON.parse("[#{array.join(',')}]", symbolize_names: true)
|
@@ -9,15 +9,17 @@ module Package
|
|
9
9
|
module Ruby
|
10
10
|
class BundlerSpecs
|
11
11
|
def self.all(dir)
|
12
|
-
Bundler.with_unbundled_env do
|
12
|
+
specs = Bundler.with_unbundled_env do
|
13
13
|
ENV['BUNDLE_GEMFILE'] = "#{dir}/Gemfile"
|
14
14
|
Bundler.ui.silence { Bundler.definition.resolve }
|
15
15
|
end
|
16
|
+
filter_local_dependencies(specs)
|
16
17
|
end
|
17
18
|
|
18
19
|
def self.gemfile(dir)
|
19
20
|
current_dependencies = Bundler.with_unbundled_env do
|
20
21
|
ENV['BUNDLE_GEMFILE'] = "#{dir}/Gemfile"
|
22
|
+
Bundler.ui.level = 'error'
|
21
23
|
Bundler.reset!
|
22
24
|
Bundler.ui.silence do
|
23
25
|
Bundler.load.dependencies.to_h { |dep| [dep.name, dep] }
|
@@ -29,6 +31,19 @@ module Package
|
|
29
31
|
end
|
30
32
|
gemfile_specs
|
31
33
|
end
|
34
|
+
|
35
|
+
def self.filter_local_dependencies(specs)
|
36
|
+
specs.reject { |spec| local_dependency?(spec) }
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.local_dependency?(spec)
|
40
|
+
# Check if the gem has a local source (path or git with local path)
|
41
|
+
source = spec.source
|
42
|
+
return true if source.is_a?(Bundler::Source::Path)
|
43
|
+
return true if source.is_a?(Bundler::Source::Git) && source.uri.start_with?('file:', './', '../')
|
44
|
+
|
45
|
+
false
|
46
|
+
end
|
32
47
|
end
|
33
48
|
end
|
34
49
|
end
|
@@ -6,6 +6,7 @@ require_relative '../technology/detector'
|
|
6
6
|
require_relative '../technology/validator'
|
7
7
|
require_relative '../util/spinner'
|
8
8
|
require_relative '../util/summary_printer'
|
9
|
+
require_relative 'config_cleaner'
|
9
10
|
require_relative 'package_finder'
|
10
11
|
require_relative 'package_printer'
|
11
12
|
|
@@ -13,20 +14,36 @@ require 'yaml'
|
|
13
14
|
|
14
15
|
module Package
|
15
16
|
module Audit
|
16
|
-
class CommandParser
|
17
|
+
class CommandParser # rubocop:disable Metrics/ClassLength
|
17
18
|
def initialize(dir, options, report)
|
18
19
|
@dir = dir
|
19
20
|
@options = options
|
20
21
|
@report = report
|
21
|
-
@config = parse_config_file
|
22
|
+
@config = parse_config_file!
|
22
23
|
@groups = @options[Enum::Option::GROUP]
|
23
|
-
@technologies = parse_technologies
|
24
|
-
|
24
|
+
@technologies = parse_technologies!
|
25
|
+
validate_format!
|
26
|
+
@spinner = Util::Spinner.new("Evaluating packages and their dependencies for #{human_readable_technologies}...")
|
25
27
|
end
|
26
28
|
|
27
|
-
def run
|
29
|
+
def run
|
30
|
+
if File.file? @dir.to_s
|
31
|
+
raise "\"#{@dir}\" is a file instead of directory"
|
32
|
+
elsif !File.directory? @dir.to_s
|
33
|
+
raise "\"#{@dir}\" is not a valid directory"
|
34
|
+
elsif @technologies.empty?
|
35
|
+
raise 'No supported technologies found in this directory'
|
36
|
+
else
|
37
|
+
process_technologies
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def process_technologies # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
28
44
|
mutex = Mutex.new
|
29
45
|
cumulative_pkgs = []
|
46
|
+
all_packages_for_config = []
|
30
47
|
thread_index = 0
|
31
48
|
|
32
49
|
@spinner.start
|
@@ -34,11 +51,14 @@ module Package
|
|
34
51
|
Thread.new do
|
35
52
|
all_pkgs, ignored_pkgs = PackageFinder.new(@config, @dir, @report, @groups).run(technology)
|
36
53
|
ignored_pkgs = [] if @options[Enum::Option::INCLUDE_IGNORED]
|
37
|
-
|
54
|
+
active_pkgs = (all_pkgs || []) - (ignored_pkgs || [])
|
55
|
+
cumulative_pkgs += active_pkgs
|
56
|
+
mutex.synchronize { all_packages_for_config += all_pkgs || [] }
|
57
|
+
|
38
58
|
sleep 0.1 while technology_index != thread_index # print each technology in order
|
39
59
|
mutex.synchronize do
|
40
60
|
@spinner.stop
|
41
|
-
print_results(technology,
|
61
|
+
print_results(technology, active_pkgs, ignored_pkgs || [])
|
42
62
|
thread_index += 1
|
43
63
|
@spinner.start
|
44
64
|
end
|
@@ -50,22 +70,24 @@ module Package
|
|
50
70
|
thread.join
|
51
71
|
raise thread[:exception] if thread[:exception]
|
52
72
|
end
|
73
|
+
|
74
|
+
@spinner.stop # Stop spinner before cleaning config to ensure clean output
|
75
|
+
clean_config(all_packages_for_config)
|
76
|
+
|
53
77
|
cumulative_pkgs.any? ? 1 : 0
|
54
78
|
ensure
|
55
79
|
@spinner.stop
|
56
80
|
end
|
57
81
|
|
58
|
-
private
|
59
|
-
|
60
82
|
def print_results(technology, pkgs, ignored_pkgs)
|
61
83
|
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::
|
84
|
+
print_summary(technology, pkgs, ignored_pkgs) unless @options[Enum::Option::FORMAT] == Enum::Format::CSV
|
85
|
+
print_disclaimer(technology) unless @options[Enum::Option::FORMAT] || pkgs.empty?
|
64
86
|
end
|
65
87
|
|
66
88
|
def print_summary(technology, pkgs, ignored_pkgs)
|
67
89
|
if @report == Enum::Report::ALL
|
68
|
-
Util::SummaryPrinter.statistics(technology, @report, pkgs, ignored_pkgs)
|
90
|
+
Util::SummaryPrinter.statistics(@options[Enum::Option::FORMAT], technology, @report, pkgs, ignored_pkgs)
|
69
91
|
else
|
70
92
|
Util::SummaryPrinter.total(technology, @report, pkgs, ignored_pkgs)
|
71
93
|
end
|
@@ -91,7 +113,7 @@ module Package
|
|
91
113
|
end
|
92
114
|
end
|
93
115
|
|
94
|
-
def parse_config_file
|
116
|
+
def parse_config_file!
|
95
117
|
if @options[Enum::Option::CONFIG].nil?
|
96
118
|
YAML.load_file("#{@dir}/#{Const::File::CONFIG}") if File.exist? "#{@dir}/#{Const::File::CONFIG}"
|
97
119
|
elsif File.exist? @options[Enum::Option::CONFIG]
|
@@ -101,10 +123,29 @@ module Package
|
|
101
123
|
end
|
102
124
|
end
|
103
125
|
|
104
|
-
def
|
126
|
+
def validate_format!
|
127
|
+
format = @options[Enum::Option::FORMAT]
|
128
|
+
raise ArgumentError, "Invalid format: #{format}, should be one of [#{Enum::Format.all.join('|')}]" unless
|
129
|
+
@options[Enum::Option::FORMAT].nil? || Enum::Format.all.include?(format)
|
130
|
+
end
|
131
|
+
|
132
|
+
def parse_technologies!
|
105
133
|
technology_validator = Technology::Validator.new(@dir)
|
106
134
|
@options[Enum::Option::TECHNOLOGY]&.each { |technology| technology_validator.validate! technology }
|
107
|
-
@options[Enum::Option::TECHNOLOGY] || Technology::Detector.new(@dir).detect
|
135
|
+
(@options[Enum::Option::TECHNOLOGY] || Technology::Detector.new(@dir).detect).sort
|
136
|
+
end
|
137
|
+
|
138
|
+
def clean_config(all_packages)
|
139
|
+
ConfigCleaner.new(@dir, @config, all_packages, @options).run
|
140
|
+
end
|
141
|
+
|
142
|
+
def human_readable_technologies
|
143
|
+
array = @technologies.map(&:capitalize)
|
144
|
+
return '' if array.nil?
|
145
|
+
return array.join if array.size <= 1
|
146
|
+
return array.join(' and ') if array.size == 2
|
147
|
+
|
148
|
+
"#{array[0..-2].join(', ')}, and #{array.last}"
|
108
149
|
end
|
109
150
|
end
|
110
151
|
end
|
@@ -0,0 +1,221 @@
|
|
1
|
+
require_relative '../const/file'
|
2
|
+
require_relative '../const/yaml'
|
3
|
+
require_relative '../enum/option'
|
4
|
+
require_relative '../enum/technology'
|
5
|
+
|
6
|
+
require 'yaml'
|
7
|
+
require 'json'
|
8
|
+
|
9
|
+
module Package
|
10
|
+
module Audit
|
11
|
+
class ConfigCleaner # rubocop:disable Metrics/ClassLength
|
12
|
+
def initialize(dir, config, all_packages, options)
|
13
|
+
@dir = dir
|
14
|
+
@config = config
|
15
|
+
@all_packages = all_packages
|
16
|
+
@options = options
|
17
|
+
@config_file_path = determine_config_file_path
|
18
|
+
@removed_packages = []
|
19
|
+
end
|
20
|
+
|
21
|
+
def run
|
22
|
+
return unless @config && File.exist?(@config_file_path)
|
23
|
+
|
24
|
+
cleaned_config = clean_config
|
25
|
+
|
26
|
+
return unless config_changed?(cleaned_config)
|
27
|
+
|
28
|
+
write_config_file(cleaned_config)
|
29
|
+
print_summary unless @options[Enum::Option::FORMAT]
|
30
|
+
end
|
31
|
+
|
32
|
+
attr_reader :removed_packages
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def determine_config_file_path
|
37
|
+
if @options[Enum::Option::CONFIG].nil?
|
38
|
+
"#{@dir}/#{Const::File::CONFIG}"
|
39
|
+
else
|
40
|
+
@options[Enum::Option::CONFIG]
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def clean_config
|
45
|
+
return {} unless @config
|
46
|
+
|
47
|
+
cleaned = {}
|
48
|
+
|
49
|
+
if @config[Const::YAML::TECHNOLOGY]
|
50
|
+
cleaned[Const::YAML::TECHNOLOGY] = {}
|
51
|
+
|
52
|
+
# Sort technologies alphabetically to ensure consistent ordering
|
53
|
+
@config[Const::YAML::TECHNOLOGY].sort.each do |technology, packages|
|
54
|
+
cleaned_packages = clean_packages_for_technology(technology, packages)
|
55
|
+
cleaned[Const::YAML::TECHNOLOGY][technology] = cleaned_packages unless cleaned_packages.empty?
|
56
|
+
end
|
57
|
+
|
58
|
+
# Remove the technology key if no technologies have any packages
|
59
|
+
cleaned.delete(Const::YAML::TECHNOLOGY) if cleaned[Const::YAML::TECHNOLOGY].empty?
|
60
|
+
end
|
61
|
+
|
62
|
+
cleaned
|
63
|
+
end
|
64
|
+
|
65
|
+
def clean_packages_for_technology(technology, packages)
|
66
|
+
return {} unless packages.is_a?(Hash)
|
67
|
+
|
68
|
+
current_packages = current_packages_for_technology(technology)
|
69
|
+
cleaned_packages = {}
|
70
|
+
|
71
|
+
packages.each do |package_name, package_config|
|
72
|
+
next unless package_config.is_a?(Hash)
|
73
|
+
|
74
|
+
if should_keep_package?(package_name, package_config, current_packages)
|
75
|
+
cleaned_packages[package_name] = sort_package_config(package_config)
|
76
|
+
else
|
77
|
+
track_removed_package(technology, package_name, package_config)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
# Sort package names alphabetically
|
82
|
+
cleaned_packages.sort.to_h
|
83
|
+
end
|
84
|
+
|
85
|
+
def current_packages_for_technology(technology)
|
86
|
+
@all_packages.select { |pkg| pkg.technology == technology }
|
87
|
+
.to_h { |pkg| [pkg.name, pkg.version] }
|
88
|
+
end
|
89
|
+
|
90
|
+
def should_keep_package?(package_name, package_config, current_packages)
|
91
|
+
config_version = package_config[Const::YAML::VERSION]
|
92
|
+
current_version = current_packages[package_name]
|
93
|
+
|
94
|
+
# Keep the package if it exists and the version matches
|
95
|
+
current_version && config_version == current_version
|
96
|
+
end
|
97
|
+
|
98
|
+
def sort_package_config(package_config)
|
99
|
+
sorted_config = {}
|
100
|
+
|
101
|
+
# Add version first if it exists
|
102
|
+
if package_config[Const::YAML::VERSION]
|
103
|
+
sorted_config[Const::YAML::VERSION] =
|
104
|
+
package_config[Const::YAML::VERSION]
|
105
|
+
end
|
106
|
+
|
107
|
+
# Add other keys in alphabetical order
|
108
|
+
other_keys = (package_config.keys - [Const::YAML::VERSION]).sort
|
109
|
+
other_keys.each do |key|
|
110
|
+
sorted_config[key] = package_config[key]
|
111
|
+
end
|
112
|
+
|
113
|
+
sorted_config
|
114
|
+
end
|
115
|
+
|
116
|
+
def track_removed_package(technology, package_name, package_config)
|
117
|
+
@removed_packages << {
|
118
|
+
technology: technology,
|
119
|
+
name: package_name,
|
120
|
+
version: package_config[Const::YAML::VERSION],
|
121
|
+
reason: determine_removal_reason(package_name, package_config)
|
122
|
+
}
|
123
|
+
end
|
124
|
+
|
125
|
+
def determine_removal_reason(package_name, package_config)
|
126
|
+
technology = find_technology_for_package(package_name)
|
127
|
+
return 'unknown reason' unless technology
|
128
|
+
|
129
|
+
current_packages = current_packages_for_technology(technology)
|
130
|
+
config_version = package_config[Const::YAML::VERSION]
|
131
|
+
current_version = current_packages[package_name]
|
132
|
+
|
133
|
+
determine_reason_based_on_versions(package_name, technology, config_version, current_version)
|
134
|
+
end
|
135
|
+
|
136
|
+
def find_technology_for_package(package_name)
|
137
|
+
@config[Const::YAML::TECHNOLOGY].each do |tech, packages|
|
138
|
+
return tech if packages.key?(package_name)
|
139
|
+
end
|
140
|
+
nil
|
141
|
+
end
|
142
|
+
|
143
|
+
def determine_reason_based_on_versions(package_name, technology, config_version, current_version)
|
144
|
+
if current_version.nil?
|
145
|
+
determine_reason_for_missing_package(package_name, technology)
|
146
|
+
elsif config_version != current_version
|
147
|
+
"version changed from #{config_version} to #{current_version}"
|
148
|
+
else
|
149
|
+
'unknown reason'
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
def determine_reason_for_missing_package(package_name, technology)
|
154
|
+
if package_exists_in_project_files?(package_name, technology)
|
155
|
+
'package version has changed'
|
156
|
+
else
|
157
|
+
'package no longer exists'
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
def package_exists_in_project_files?(package_name, technology)
|
162
|
+
case technology
|
163
|
+
when Enum::Technology::RUBY
|
164
|
+
package_exists_in_gemfile?(package_name)
|
165
|
+
when Enum::Technology::NODE
|
166
|
+
package_exists_in_package_json?(package_name)
|
167
|
+
else
|
168
|
+
false
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
def package_exists_in_gemfile?(package_name)
|
173
|
+
gemfile_path = "#{@dir}/#{Const::File::GEMFILE}"
|
174
|
+
return false unless File.exist?(gemfile_path)
|
175
|
+
|
176
|
+
gemfile_content = File.read(gemfile_path)
|
177
|
+
# Check for gem declarations with single or double quotes
|
178
|
+
gemfile_content.match?(/^\s*gem\s+['"]#{Regexp.escape(package_name)}['"]/)
|
179
|
+
end
|
180
|
+
|
181
|
+
def package_exists_in_package_json?(package_name)
|
182
|
+
package_json_path = "#{@dir}/#{Const::File::PACKAGE_JSON}"
|
183
|
+
return false unless File.exist?(package_json_path)
|
184
|
+
|
185
|
+
begin
|
186
|
+
package_json = JSON.parse(File.read(package_json_path))
|
187
|
+
dependencies = package_json['dependencies'] || {}
|
188
|
+
dev_dependencies = package_json['devDependencies'] || {}
|
189
|
+
|
190
|
+
dependencies.key?(package_name) || dev_dependencies.key?(package_name)
|
191
|
+
rescue JSON::ParserError
|
192
|
+
false
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
def config_changed?(cleaned_config)
|
197
|
+
# Compare YAML representations to detect key reordering
|
198
|
+
cleaned_config.to_yaml != @config.to_yaml
|
199
|
+
end
|
200
|
+
|
201
|
+
def write_config_file(cleaned_config)
|
202
|
+
File.write(@config_file_path, cleaned_config.to_yaml)
|
203
|
+
end
|
204
|
+
|
205
|
+
def print_summary
|
206
|
+
return if @removed_packages.empty?
|
207
|
+
|
208
|
+
puts
|
209
|
+
puts "Cleaned up #{@removed_packages.count} package(s) from #{File.basename(@config_file_path)}:"
|
210
|
+
|
211
|
+
# Sort by technology then by name for consistent output
|
212
|
+
@removed_packages.sort_by { |pkg| [pkg[:technology], pkg[:name]] }.each do |removed_package|
|
213
|
+
package_info = "#{removed_package[:name]}@#{removed_package[:version]}"
|
214
|
+
tech_info = "(#{removed_package[:technology]})"
|
215
|
+
reason = removed_package[:reason]
|
216
|
+
puts " - #{package_info} #{tech_info}: #{reason}"
|
217
|
+
end
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|
221
|
+
end
|
@@ -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
|
-
|
34
|
-
|
35
|
-
(
|
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
|