inspec-core 5.22.50 → 6.8.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.
Files changed (79) hide show
  1. checksums.yaml +4 -4
  2. data/Chef-EULA +9 -0
  3. data/Gemfile +14 -4
  4. data/etc/features.sig +6 -0
  5. data/etc/features.yaml +97 -0
  6. data/inspec-core.gemspec +17 -7
  7. data/lib/inspec/backend.rb +2 -0
  8. data/lib/inspec/base_cli.rb +80 -4
  9. data/lib/inspec/cached_fetcher.rb +24 -3
  10. data/lib/inspec/cli.rb +293 -236
  11. data/lib/inspec/config.rb +24 -2
  12. data/lib/inspec/dependencies/cache.rb +33 -0
  13. data/lib/inspec/enhanced_outcomes.rb +1 -0
  14. data/lib/inspec/errors.rb +5 -0
  15. data/lib/inspec/exceptions.rb +2 -0
  16. data/lib/inspec/feature/config.rb +75 -0
  17. data/lib/inspec/feature/runner.rb +29 -0
  18. data/lib/inspec/feature.rb +42 -0
  19. data/lib/inspec/fetcher/git.rb +5 -0
  20. data/lib/inspec/fetcher/url.rb +24 -4
  21. data/lib/inspec/globals.rb +6 -0
  22. data/lib/inspec/iaf_file.rb +3 -2
  23. data/lib/inspec/input_registry.rb +5 -1
  24. data/lib/inspec/plugin/v1/plugin_types/fetcher.rb +7 -0
  25. data/lib/inspec/plugin/v2/plugin_types/streaming_reporter.rb +30 -2
  26. data/lib/inspec/profile.rb +44 -1
  27. data/lib/inspec/reporters.rb +67 -54
  28. data/lib/inspec/resources/nftables.rb +14 -1
  29. data/lib/inspec/resources/oracledb_session.rb +12 -3
  30. data/lib/inspec/resources/ssh_config.rb +100 -9
  31. data/lib/inspec/resources/ssh_key.rb +124 -0
  32. data/lib/inspec/resources/sshd_active_config.rb +2 -0
  33. data/lib/inspec/resources/sybase_session.rb +11 -2
  34. data/lib/inspec/resources.rb +1 -0
  35. data/lib/inspec/rule.rb +6 -6
  36. data/lib/inspec/run_data.rb +7 -5
  37. data/lib/inspec/runner.rb +43 -6
  38. data/lib/inspec/runner_rspec.rb +12 -9
  39. data/lib/inspec/secrets/yaml.rb +9 -3
  40. data/lib/inspec/shell.rb +10 -0
  41. data/lib/inspec/ui.rb +4 -0
  42. data/lib/inspec/utils/licensing_config.rb +9 -0
  43. data/lib/inspec/utils/telemetry/base.rb +149 -0
  44. data/lib/inspec/utils/telemetry/http.rb +40 -0
  45. data/lib/inspec/utils/telemetry/null.rb +11 -0
  46. data/lib/inspec/utils/telemetry/run_context_probe.rb +13 -1
  47. data/lib/inspec/utils/telemetry.rb +74 -3
  48. data/lib/inspec/version.rb +1 -1
  49. data/lib/inspec/waiver_file_reader.rb +68 -27
  50. data/lib/inspec.rb +2 -2
  51. data/lib/plugins/inspec-compliance/lib/inspec-compliance/cli.rb +189 -168
  52. data/lib/plugins/inspec-habitat/lib/inspec-habitat/cli.rb +10 -3
  53. data/lib/plugins/inspec-init/lib/inspec-init/cli.rb +1 -0
  54. data/lib/plugins/inspec-init/lib/inspec-init/cli_plugin.rb +23 -21
  55. data/lib/plugins/inspec-init/lib/inspec-init/cli_profile.rb +15 -13
  56. data/lib/plugins/inspec-init/lib/inspec-init/cli_resource.rb +15 -13
  57. data/lib/plugins/inspec-license/README.md +16 -0
  58. data/lib/plugins/inspec-license/inspec-license.gemspec +6 -0
  59. data/lib/plugins/inspec-license/lib/inspec-license/cli.rb +26 -0
  60. data/lib/plugins/inspec-license/lib/inspec-license.rb +14 -0
  61. data/lib/plugins/inspec-parallel/README.md +27 -0
  62. data/lib/plugins/inspec-parallel/inspec-parallel.gemspec +6 -0
  63. data/lib/plugins/inspec-parallel/lib/inspec-parallel/child_status_reporter.rb +61 -0
  64. data/lib/plugins/inspec-parallel/lib/inspec-parallel/cli.rb +39 -0
  65. data/lib/plugins/inspec-parallel/lib/inspec-parallel/command.rb +219 -0
  66. data/lib/plugins/inspec-parallel/lib/inspec-parallel/runner.rb +270 -0
  67. data/lib/plugins/inspec-parallel/lib/inspec-parallel/super_reporter/base.rb +24 -0
  68. data/lib/plugins/inspec-parallel/lib/inspec-parallel/super_reporter/silent.rb +7 -0
  69. data/lib/plugins/inspec-parallel/lib/inspec-parallel/super_reporter/status.rb +125 -0
  70. data/lib/plugins/inspec-parallel/lib/inspec-parallel/super_reporter/text.rb +23 -0
  71. data/lib/plugins/inspec-parallel/lib/inspec-parallel/validator.rb +170 -0
  72. data/lib/plugins/inspec-parallel/lib/inspec-parallel.rb +18 -0
  73. data/lib/plugins/inspec-sign/lib/inspec-sign/base.rb +20 -8
  74. data/lib/plugins/inspec-sign/lib/inspec-sign/cli.rb +11 -4
  75. data/lib/plugins/inspec-streaming-reporter-progress-bar/lib/inspec-streaming-reporter-progress-bar/streaming_reporter.rb +6 -13
  76. metadata +61 -19
  77. data/lib/inspec/utils/telemetry/collector.rb +0 -81
  78. data/lib/inspec/utils/telemetry/data_series.rb +0 -44
  79. data/lib/inspec/utils/telemetry/global_methods.rb +0 -22
@@ -0,0 +1,14 @@
1
+ module InspecPlugins
2
+ module License
3
+ class Plugin < ::Inspec.plugin(2)
4
+ plugin_name :"inspec-license"
5
+
6
+ if Inspec::Dist::EXEC_NAME == "inspec"
7
+ cli_command :license do
8
+ require_relative "inspec-license/cli"
9
+ InspecPlugins::License::CLI
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,27 @@
1
+ # Parallel Plugin
2
+
3
+ Plugin to handle parallel InSpec scan operations over multiple targets.
4
+
5
+ ## parallel cli_command
6
+
7
+ Implements the `inspec parallel exec` CLI command.
8
+
9
+ ## child-status Plugin
10
+
11
+ This reporter is an InSpec Streaming Reporter. It is used internally by inspec parallel to provide status updates on child processes.
12
+
13
+ ### What This Plugin Does
14
+
15
+ For each control executed, after it is complete, the plugin emits a line to STDOUT like:
16
+ ```
17
+ 12/P/24/Control Title Here
18
+ ```
19
+ When the run is complete, the single line 'EOF_MARKER' is emitted.
20
+
21
+ Where:
22
+
23
+ - 12 is the number of the control (12th seen out of all controls in all profiles)
24
+ - P indicates that it Passed (Also F = Failed, S = Skipped, E = Errored)
25
+ - 24 is the total number of controls in the run
26
+ - "Control Title Here" is the title (or if title is missing, id) of the last executed control
27
+
@@ -0,0 +1,6 @@
1
+ Gem::Specification.new do |spec|
2
+ spec.name = "inspec-parallel"
3
+ spec.summary = "Plugin to handle parallel InSpec scan operations over multiple targets"
4
+ spec.description = ""
5
+ spec.license = "Apache-2.0"
6
+ end
@@ -0,0 +1,61 @@
1
+ module InspecPlugins::Parallelism
2
+ class StreamingReporter < Inspec.plugin(2, :streaming_reporter)
3
+ # Registering these methods with RSpec::Core::Formatters class is mandatory
4
+ RSpec::Core::Formatters.register self, :example_passed, :example_failed, :example_pending, :close
5
+
6
+ def initialize(output)
7
+ @status_mapping = {}
8
+ @control_counter = 0
9
+ initialize_streaming_reporter
10
+ end
11
+
12
+ def example_passed(notification)
13
+ set_example(notification, "passed")
14
+ end
15
+
16
+ def example_failed(notification)
17
+ set_example(notification, "failed")
18
+ end
19
+
20
+ def example_pending(notification)
21
+ set_example(notification, "skipped")
22
+ end
23
+
24
+ def close(notification)
25
+ # HACK: if we've reached the end of the execution, send a special marker, to ease EOF detection on Windows
26
+ puts "EOF_MARKER"
27
+ end
28
+
29
+ private
30
+
31
+ def set_example(notification, status)
32
+ control_id = notification.example.metadata[:id]
33
+ title = notification.example.metadata[:title]
34
+ set_status_mapping(control_id, status)
35
+ output_status(control_id, title) if control_ended?(notification, control_id)
36
+ end
37
+
38
+ def output_status(control_id, title)
39
+ @control_counter += 1
40
+ stat = @status_mapping[control_id]
41
+ stat = if stat.include?("failed")
42
+ "F"
43
+ else
44
+ if stat.include?("skipped")
45
+ "S"
46
+ else
47
+ stat.include?("passed") ? "P" : "E"
48
+ end
49
+ end
50
+ display_name = title.gsub(/\n*\s+/, " ").to_s.force_encoding(Encoding::UTF_8) if title
51
+ display_name = control_id.to_s.lstrip.force_encoding(Encoding::UTF_8) unless title
52
+
53
+ puts "#{@control_counter}/#{stat}/#{controls_count}/#{display_name}"
54
+ end
55
+
56
+ def set_status_mapping(control_id, status)
57
+ @status_mapping[control_id] ||= []
58
+ @status_mapping[control_id].push(status)
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,39 @@
1
+ require_relative "command"
2
+ require "inspec/dist"
3
+ require "inspec/base_cli"
4
+ require "inspec/feature"
5
+
6
+ module InspecPlugins::Parallelism
7
+ class CLI < Inspec.plugin(2, :cli_command)
8
+ include Inspec::Dist
9
+
10
+ subcommand_desc "parallel SUBCOMMAND [options]", "Runs #{PRODUCT_NAME} operations parallely"
11
+
12
+ desc "exec", "Executes profile parallely"
13
+ option :option_file, aliases: :o, type: :string, required: true,
14
+ desc: "File that contains list of option strings"
15
+ option :dry_run, type: :boolean,
16
+ desc: "Print commands that will run"
17
+ option :verbose, type: :boolean,
18
+ desc: "Prints all thor options on dry run"
19
+ option :jobs, aliases: :j, type: :numeric,
20
+ desc: "Number of jobs to run parallely"
21
+ option :ui, type: :string, default: "status",
22
+ desc: "Which UI to use: status, text, silent"
23
+ option :bg, type: :boolean,
24
+ desc: "Runs parallel processes in background"
25
+ option :log_path, type: :string,
26
+ desc: "Path to the runner and error logs"
27
+ exec_options
28
+ def exec(default_profile = nil)
29
+ Inspec.with_feature("inspec-cli-parallel-exec") {
30
+ parallel_cmd = InspecPlugins::Parallelism::Command.new(options, default_profile)
31
+ if options[:dry_run]
32
+ parallel_cmd.dry_run
33
+ else
34
+ parallel_cmd.run
35
+ end
36
+ }
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,219 @@
1
+ require_relative "runner"
2
+ require_relative "validator"
3
+ require "erb" unless defined?(Erb)
4
+
5
+ module InspecPlugins
6
+ module Parallelism
7
+
8
+ class OptionFileNotReadable < RuntimeError
9
+ end
10
+
11
+ class Command
12
+ attr_accessor :cli_options_to_parallel_cmd, :default_profile, :sub_cmd, :invocations, :run_in_background
13
+
14
+ def initialize(cli_options_to_parallel_cmd, default_profile, sub_cmd = "exec")
15
+ @default_profile = default_profile
16
+ @cli_options_to_parallel_cmd = cli_options_to_parallel_cmd
17
+ @sub_cmd = sub_cmd
18
+ @logger = Inspec::Log
19
+ @invocations = read_options_file
20
+ @run_in_background = cli_options_to_parallel_cmd["bg"]
21
+ end
22
+
23
+ def run
24
+ validate_thor_options
25
+ validate_invocations!
26
+ runner = Runner.new(invocations, cli_options_to_parallel_cmd, sub_cmd)
27
+ catch_ctl_c_and_exit(runner) unless run_in_background
28
+ runner.run
29
+ end
30
+
31
+ def dry_run
32
+ validate_invocations!
33
+ dry_run_commands
34
+ end
35
+
36
+ private
37
+
38
+ def catch_ctl_c_and_exit(runner)
39
+ puts "Press CTL+C to stop\n"
40
+ trap("SIGINT") do
41
+ puts "\n"
42
+ puts "Shutting down jobs..."
43
+ if Inspec.locally_windows?
44
+ runner.kill_child_processes
45
+ sleep 1
46
+ puts "Renaming error log files..."
47
+ runner.rename_error_log_files
48
+ end
49
+ exit Inspec::UI::EXIT_TERMINATED_BY_CTL_C
50
+ end
51
+ end
52
+
53
+ def validate_thor_options
54
+ # only log path validation needed for now
55
+ validate_log_path
56
+ end
57
+
58
+ def validate_log_path
59
+ error, message = Validator.new(invocations, cli_options_to_parallel_cmd, sub_cmd).validate_log_path
60
+ if error
61
+ @logger.error message
62
+ Inspec::UI.new.exit(:usage_error)
63
+ end
64
+ end
65
+
66
+ def validate_invocations!
67
+ # Validation logic stays in Validator class...
68
+ Validator.new(invocations, cli_options_to_parallel_cmd, sub_cmd).validate
69
+ # UI logic stays in Command class.
70
+ valid = true
71
+ invocations.each do |invocation_data|
72
+ invocation_data[:validation_errors].each do |error_message|
73
+ valid = false
74
+ @logger.error "Line #{invocation_data[:line_no]}: " + error_message
75
+ end
76
+ end
77
+ unless valid
78
+ @logger.error "Please fix the options to proceed further."
79
+ Inspec::UI.new.exit(:usage_error)
80
+ end
81
+ end
82
+
83
+ def dry_run_commands
84
+ invocations.each do |invocation_data|
85
+ puts "inspec #{sub_cmd} #{invocation_data[:value]}"
86
+ end
87
+ end
88
+
89
+ ## Utility functions
90
+
91
+ def read_options_file
92
+ opts = []
93
+ begin
94
+ content = content_from_file(cli_options_to_parallel_cmd[:option_file])
95
+ rescue OptionFileNotReadable => e
96
+ @logger.error "Cannot read options file: #{e.message}"
97
+ Inspec::UI.new.exit(:usage_error)
98
+ end
99
+ content.each.with_index(1) do |str, index|
100
+ data_hash = { line_no: index }
101
+ str = ERB.new(str).result_with_hash(pid: "CHILD_PID").strip
102
+ str_has_comment = str.start_with?("#")
103
+ next if str.empty? || str_has_comment
104
+
105
+ default_options = fetch_default_options(str.split(" ")).lstrip
106
+ if str.start_with?("-")
107
+ data_hash[:value] = "#{default_profile} #{str} #{default_options}"
108
+ else
109
+ data_hash[:value] = "#{str} #{default_options}"
110
+ end
111
+ opts << data_hash
112
+ end
113
+ opts
114
+ end
115
+
116
+ def content_from_file(option_file)
117
+ if File.exist?(option_file)
118
+ unless [".sh", ".csh", ".ps1"].include? File.extname(option_file)
119
+ File.readlines(option_file)
120
+ else
121
+ if Inspec.locally_windows? && (File.extname(option_file) == ".ps1")
122
+ begin
123
+ output = `powershell -File "#{option_file}"`
124
+ output.split("\n")
125
+ rescue StandardError => e
126
+ raise OptionFileNotReadable.new("Error reading powershell file #{option_file}: #{e.message}")
127
+ end
128
+ elsif [".sh", ".csh"].include? File.extname(option_file)
129
+ begin
130
+ output = `bash "#{option_file}"`
131
+ output.split("\n")
132
+ rescue StandardError => e
133
+ raise OptionFileNotReadable.new("Error reading shell file #{option_file}: #{e.message}")
134
+ end
135
+ else
136
+ raise OptionFileNotReadable.new("Powershell not supported in your system.")
137
+ end
138
+ end
139
+ else
140
+ raise OptionFileNotReadable.new("Option file not found.")
141
+ end
142
+ end
143
+
144
+ # this must return empty string or default option string which are not part of option file
145
+ def fetch_default_options(option_line)
146
+ option_line = option_line.select { |word| word.start_with?("-") }
147
+
148
+ # remove prefixes from the options to compare with default options
149
+ option_line.map! do |option_key|
150
+ option_key.gsub(options_prefix(option_key), "").gsub("-", "_")
151
+ end
152
+
153
+ default_opts = ""
154
+ # iterate through the parallel cli default options and append the option and value which are not present in option file
155
+ parallel_cmd_default_cli_options.each do |cmd|
156
+ if cmd.is_a? String
157
+ append_default_value(default_opts, cmd) unless option_line.include?(cmd)
158
+ elsif cmd.is_a? Array
159
+ if !option_line.include?(cmd[0]) && !option_line.include?(cmd[1])
160
+ append_default_value(default_opts, cmd[0])
161
+ end
162
+ end
163
+ end
164
+ default_opts
165
+ end
166
+
167
+ # returns array of default options of the subcommand
168
+ def parallel_cmd_default_cli_options
169
+ sub_cmd_opts = Inspec::InspecCLI.commands[sub_cmd].options
170
+ parallel_cmd_default_opts = cli_options_to_parallel_cmd.keys & sub_cmd_opts.keys.map(&:to_s)
171
+ options_to_append = parallel_cmd_default_opts
172
+
173
+ if cli_options_to_parallel_cmd["dry_run"] && !cli_options_to_parallel_cmd["verbose"]
174
+ # to not show thor default options of inspec commands in dry run
175
+ sub_cmd_opts_with_defaults = fetch_sub_cmd_default_options(sub_cmd_opts)
176
+ options_to_append -= sub_cmd_opts_with_defaults
177
+ end
178
+
179
+ default_opts_to_append = []
180
+
181
+ # append the options and its aliases if available.
182
+ options_to_append.each do |option_name|
183
+ opt_alias = sub_cmd_opts[option_name.to_sym].aliases
184
+ if opt_alias.empty?
185
+ default_opts_to_append << option_name
186
+ else
187
+ default_opts_to_append << [option_name, opt_alias[0].to_s]
188
+ end
189
+ end
190
+ default_opts_to_append
191
+ end
192
+
193
+ def append_default_value(default_opts, command_name)
194
+ default_value = cli_options_to_parallel_cmd[command_name.to_sym]
195
+ default_value = default_value.join(" ") if default_value.is_a? Array
196
+ default_opts << " --#{command_name.gsub("_", "-")} #{default_value}"
197
+ end
198
+
199
+ def options_prefix(option_name)
200
+ if option_name.start_with?("--")
201
+ option_name.start_with?("--no-") ? "--no-" : "--"
202
+ else
203
+ "-"
204
+ end
205
+ end
206
+
207
+ def fetch_sub_cmd_default_options(sub_cmd_opts)
208
+ default_options_to_remove = []
209
+ sub_cmd_opts_with_defaults = sub_cmd_opts.select { |_, c| !c.default.nil? }.keys.map(&:to_s)
210
+ sub_cmd_opts_with_defaults.each do |default_opt_name|
211
+ if sub_cmd_opts[default_opt_name.to_sym].default == cli_options_to_parallel_cmd[default_opt_name]
212
+ default_options_to_remove << default_opt_name
213
+ end
214
+ end
215
+ default_options_to_remove
216
+ end
217
+ end
218
+ end
219
+ end
@@ -0,0 +1,270 @@
1
+ require "inspec/cli"
2
+ require "concurrent"
3
+ require_relative "super_reporter/base"
4
+
5
+ module InspecPlugins
6
+ module Parallelism
7
+ class Runner
8
+ attr_accessor :invocations, :sub_cmd, :total_jobs, :run_in_background, :log_path
9
+
10
+ def initialize(invocations, cli_options, sub_cmd = "exec")
11
+ @invocations = invocations
12
+ @sub_cmd = sub_cmd
13
+ @total_jobs = cli_options["jobs"] || Concurrent.physical_processor_count
14
+ @child_tracker = {}
15
+ @child_tracker_persisted = {}
16
+ @run_in_background = cli_options["bg"]
17
+ unless run_in_background
18
+ @ui = InspecPlugins::Parallelism::SuperReporter.make(cli_options["ui"], total_jobs, invocations)
19
+ end
20
+ @log_path = cli_options["log_path"]
21
+ end
22
+
23
+ def run
24
+ initiate_background_run if run_in_background # running a process as daemon changes parent process pid
25
+ original_stdout_stream = ChefLicensing::Config.output
26
+ until invocations.empty? && @child_tracker.empty?
27
+ # Changing output to STDERR to avoid the output interruption between runs
28
+ ChefLicensing::Config.output = STDERR
29
+ while should_start_more_jobs?
30
+ if Inspec.locally_windows?
31
+ spawn_another_process
32
+ else
33
+ fork_another_process
34
+ end
35
+ end
36
+
37
+ update_ui_poll_select
38
+ cleanup_child_processes
39
+ sleep 0.1
40
+ end
41
+ # Reset output to the original STDOUT stream as a safe measure.
42
+ ChefLicensing::Config.output = original_stdout_stream
43
+
44
+ # Requires renaming operations on windows only
45
+ # Do Rename and delete operations after all child processes have exited successfully
46
+ rename_error_log_files if Inspec.locally_windows?
47
+ cleanup_empty_error_log_files
48
+ cleanup_daemon_process if run_in_background
49
+ end
50
+
51
+ def initiate_background_run
52
+ if Inspec.locally_windows?
53
+ Inspec::UI.new.exit(:usage_error)
54
+ else
55
+ Process.daemon(true, true)
56
+ end
57
+ end
58
+
59
+ def cleanup_daemon_process
60
+ current_process_id = Process.pid
61
+ Process.kill(9, current_process_id)
62
+ # DO NOT TRY TO REFACTOR IT THIS WAY
63
+ # Calling Process.kill(9,Process.pid) kills the "stopper" process itself, rather than the one it's trying to stop.
64
+ end
65
+
66
+ def cleanup_empty_error_log_files
67
+ logs_dir_path = log_path || Dir.pwd
68
+ error_files = Dir.glob("#{logs_dir_path}/logs/*.err")
69
+ error_files.each do |error_file|
70
+ if File.exist?(error_file) && !File.size?(error_file)
71
+ File.delete(error_file)
72
+ end
73
+ end
74
+ end
75
+
76
+ def kill_child_processes
77
+ @child_tracker.each do |pid, info|
78
+ Process.kill("SIGKILL", pid)
79
+ rescue Exception => e
80
+ $stderr.puts "Error while shutting down process #{pid}: #{e.message}"
81
+ end
82
+ # Waiting for child processes to die after they have been killed
83
+ wait_for_child_processes_to_die
84
+ end
85
+
86
+ def wait_for_child_processes_to_die
87
+ until @child_tracker.empty?
88
+ begin
89
+ exited_pid = Process.waitpid(-1, Process::WNOHANG)
90
+ @child_tracker.delete exited_pid if exited_pid && exited_pid > 0
91
+ sleep 1
92
+ rescue Errno::ECHILD
93
+ Inspec::Log.info "Processes shutdown complete!"
94
+ rescue Exception => e
95
+ Inspec::Log.debug "Error while waiting for child processes to shutdown: #{e.message}"
96
+ end
97
+ end
98
+ end
99
+
100
+ def rename_error_log_files
101
+ @child_tracker_persisted.each do |pid, info|
102
+ rename_error_log(info[:error_log_file], pid)
103
+ end
104
+ end
105
+
106
+ def should_start_more_jobs?
107
+ @child_tracker.length < total_jobs && !invocations.empty?
108
+ end
109
+
110
+ def spawn_another_process
111
+ invocation = invocations.shift[:value]
112
+
113
+ child_reader, parent_writer = IO.pipe
114
+ begin
115
+ logs_dir_path = log_path || Dir.pwd
116
+ log_dir = File.join(logs_dir_path, "logs")
117
+ FileUtils.mkdir_p(log_dir)
118
+ error_log_file = File.open("#{log_dir}/#{Time.now.nsec}.err", "a+")
119
+ cmd = "#{$0} #{sub_cmd} #{invocation}"
120
+ log_msg = "#{Time.now.iso8601} Start Time: #{Time.now}\n#{Time.now.iso8601} Arguments: #{invocation}\n"
121
+ child_pid = Process.spawn(cmd, out: parent_writer, err: error_log_file.path)
122
+
123
+ # Logging
124
+ create_logs(child_pid, log_msg)
125
+ @child_tracker[child_pid] = { io: child_reader }
126
+
127
+ # This is used to rename error log files after all child processes are exited
128
+ @child_tracker_persisted[child_pid] = { error_log_file: error_log_file }
129
+ @ui.child_spawned(child_pid, invocation)
130
+
131
+ # Close the file to unlock the error log files opened by processes
132
+ error_log_file.close
133
+ rescue StandardError => e
134
+ $stderr.puts "#{Time.now.iso8601} Error Message: #{e.message}"
135
+ $stderr.puts "#{Time.now.iso8601} Error Backtrace: #{e.backtrace}"
136
+ end
137
+ end
138
+
139
+ def fork_another_process
140
+ invocation = invocations.shift[:value] # Be sure to do this shift() in parent process
141
+ # thing_that_reads_from_the_child, thing_that_writes_to_the_parent = IO.pipe
142
+ child_reader, parent_writer = IO.pipe
143
+ if (child_pid = Process.fork)
144
+ # In parent with newly forked child
145
+ parent_writer.close
146
+ @child_tracker[child_pid] = { io: child_reader }
147
+ @ui.child_forked(child_pid, invocation) unless run_in_background
148
+ else
149
+ # In child
150
+ child_reader.close
151
+ # replace stdout with writer
152
+ $stdout = parent_writer
153
+ create_logs(Process.pid, nil, $stderr)
154
+
155
+ begin
156
+ create_logs(
157
+ Process.pid,
158
+ "#{Time.now.iso8601} Start Time: #{Time.now}\n#{Time.now.iso8601} Arguments: #{invocation}\n"
159
+ )
160
+ runner_invocation(invocation)
161
+ rescue StandardError => e
162
+ $stderr.puts "#{Time.now.iso8601} Error Message: #{e.message}"
163
+ $stderr.puts "#{Time.now.iso8601} Error Backtrace: #{e.backtrace}"
164
+ end
165
+
166
+ # should be unreachable but child MUST exit
167
+ exit(42)
168
+ end
169
+ end
170
+
171
+ # Still in parent
172
+ # Loop over children and check for finished processes
173
+ def cleanup_child_processes
174
+ @child_tracker.each do |pid, info|
175
+ if Process.wait(pid, Process::WNOHANG)
176
+ # Expect to (probably) find EOF marker on the pipe, and close it if so
177
+ update_ui_poll_select(pid)
178
+
179
+ create_logs(pid, "#{Time.now.iso8601} Exit code: #{$?}\n")
180
+
181
+ # child exited - status in $?
182
+ @ui.child_exited(pid) unless run_in_background
183
+ @child_tracker.delete pid
184
+ end
185
+ end
186
+ end
187
+
188
+ def update_ui_poll_select(target_pid = nil)
189
+ # Focus on one pid's pipe if specified, otherwise poll all pipes
190
+ pipes_for_reading = target_pid ? [ @child_tracker[target_pid][:io] ] : @child_tracker.values.map { |i| i[:io] }
191
+ # Next line is due to a race between the close() and the wait()... shouldn't need it, but it fixes the race.
192
+ pipes_for_reading.reject!(&:closed?)
193
+ ready_pipes = IO.select(pipes_for_reading, [], [], 0.1)
194
+ return unless ready_pipes
195
+
196
+ ready_pipes[0].each do |pipe_ready_for_reading|
197
+ # If we weren't provided a PID, hackishly look up the pid from the matching IO.
198
+ pid = target_pid || @child_tracker.keys.detect { |p| @child_tracker[p][:io] == pipe_ready_for_reading }
199
+ begin
200
+ while (update_line = pipe_ready_for_reading.readline) && !pipe_ready_for_reading.closed?
201
+ if update_line =~ /EOF_MARKER/
202
+ pipe_ready_for_reading.close
203
+ break
204
+ elsif update_line =~ /WARN/ || update_line =~ /ERROR/ || update_line =~ /INFO/
205
+ create_logs(
206
+ pid,
207
+ "#{Time.now.iso8601} Extra log: #{update_line}\n"
208
+ )
209
+ break
210
+ end
211
+ update_ui_with_line(pid, update_line) unless run_in_background
212
+ # Only pull one line if we are doing normal updates; slurp the whole file
213
+ # if we are doing a final pull on a targeted PID
214
+ break unless target_pid
215
+ end
216
+ rescue EOFError
217
+ # On unix, readline throws an EOFError when we hit the end. On Windows, nothing apparently happens.
218
+ pipe_ready_for_reading.close
219
+ next
220
+ end
221
+ end
222
+ # TODO: loop over ready_pipes[2] and handle errors?
223
+ end
224
+
225
+ def update_ui_with_line(pid, update_line)
226
+ @ui.child_status_update_line(pid, update_line)
227
+ end
228
+
229
+ private
230
+
231
+ def runner_invocation(runner_option)
232
+ splitted_result = runner_option.split(" ")
233
+ profile_to_run = splitted_result[0]
234
+ splitted_result.delete_at(0)
235
+
236
+ # thor invocation
237
+ arguments = [sub_cmd, profile_to_run, splitted_result].flatten
238
+ Inspec::InspecCLI.start(arguments, enforce_license: true)
239
+ end
240
+
241
+ def create_logs(child_pid, run_log , stderr = nil)
242
+ logs_dir_path = log_path || Dir.pwd
243
+ log_dir = File.join(logs_dir_path, "logs")
244
+ FileUtils.mkdir_p(log_dir)
245
+
246
+ if stderr
247
+ log_file = File.join(log_dir, "#{child_pid}.err") unless File.exist?("#{child_pid}.err")
248
+ stderr.reopen(log_file, "a")
249
+ else
250
+ log_file = File.join(log_dir, "#{child_pid}.log") unless File.exist?("#{child_pid}.log")
251
+ File.write(log_file, run_log, mode: "a")
252
+ end
253
+ end
254
+
255
+ def rename_error_log(error_log_file, child_pid)
256
+ logs_dir_path = log_path || Dir.pwd
257
+ log_dir = File.join(logs_dir_path, "logs")
258
+ FileUtils.mkdir_p(log_dir)
259
+
260
+ if error_log_file.closed? && File.exist?(error_log_file.path)
261
+ begin
262
+ File.rename("#{error_log_file.path}", "#{log_dir}/#{child_pid}.err")
263
+ rescue
264
+ $stderr.puts "Cannot rename error log file #{error_log_file.path} for child pid #{child_pid}"
265
+ end
266
+ end
267
+ end
268
+ end
269
+ end
270
+ end
@@ -0,0 +1,24 @@
1
+ module InspecPlugins::Parallelism
2
+ class SuperReporter
3
+
4
+ def self.make(type, job_count, invocations)
5
+ Object.const_get("InspecPlugins::Parallelism::SuperReporter::" + (type[0].upcase + type[1..-1])).new(job_count, invocations)
6
+ end
7
+
8
+ class Base
9
+ def initialize(job_count, invocations); end
10
+
11
+ def child_spawned(pid, invocation); end
12
+
13
+ def child_forked(pid, invocation); end
14
+
15
+ def child_exited(pid); end
16
+
17
+ def child_status_update_line(pid, update_line); end
18
+ end
19
+
20
+ require_relative "text"
21
+ require_relative "status"
22
+ require_relative "silent"
23
+ end
24
+ end
@@ -0,0 +1,7 @@
1
+ module InspecPlugins::Parallelism
2
+ class SuperReporter
3
+ class Silent < InspecPlugins::Parallelism::SuperReporter::Base
4
+ # This is a silent super reporter with no reporting functionality.
5
+ end
6
+ end
7
+ end