inspec-core 5.22.65 → 6.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (77) hide show
  1. checksums.yaml +4 -4
  2. data/Chef-EULA +9 -0
  3. data/Gemfile +24 -32
  4. data/etc/features.sig +6 -0
  5. data/etc/features.yaml +94 -0
  6. data/inspec-core.gemspec +15 -14
  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 +292 -235
  11. data/lib/inspec/config.rb +24 -11
  12. data/lib/inspec/dependencies/cache.rb +33 -0
  13. data/lib/inspec/dependencies/dependency_set.rb +2 -2
  14. data/lib/inspec/dsl.rb +1 -1
  15. data/lib/inspec/enhanced_outcomes.rb +1 -0
  16. data/lib/inspec/errors.rb +5 -0
  17. data/lib/inspec/exceptions.rb +2 -0
  18. data/lib/inspec/feature/config.rb +75 -0
  19. data/lib/inspec/feature/runner.rb +26 -0
  20. data/lib/inspec/feature.rb +34 -0
  21. data/lib/inspec/fetcher/git.rb +5 -0
  22. data/lib/inspec/fetcher/url.rb +7 -29
  23. data/lib/inspec/globals.rb +6 -0
  24. data/lib/inspec/input_registry.rb +1 -5
  25. data/lib/inspec/plugin/v1/plugin_types/fetcher.rb +7 -0
  26. data/lib/inspec/plugin/v2/plugin_types/streaming_reporter.rb +30 -2
  27. data/lib/inspec/profile.rb +46 -3
  28. data/lib/inspec/reporters/cli.rb +1 -1
  29. data/lib/inspec/reporters.rb +67 -54
  30. data/lib/inspec/resources/nftables.rb +1 -14
  31. data/lib/inspec/resources/oracledb_session.rb +3 -9
  32. data/lib/inspec/resources/postgres_session.rb +1 -1
  33. data/lib/inspec/resources/sybase_session.rb +2 -11
  34. data/lib/inspec/resources/virtualization.rb +1 -1
  35. data/lib/inspec/rule.rb +9 -14
  36. data/lib/inspec/run_data.rb +7 -5
  37. data/lib/inspec/runner.rb +35 -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/profile_ast_helpers.rb +2 -1
  44. data/lib/inspec/utils/waivers/csv_file_reader.rb +1 -1
  45. data/lib/inspec/utils/waivers/excel_file_reader.rb +1 -1
  46. data/lib/inspec/version.rb +1 -1
  47. data/lib/inspec/waiver_file_reader.rb +68 -27
  48. data/lib/inspec.rb +2 -1
  49. data/lib/matchers/matchers.rb +3 -3
  50. data/lib/plugins/inspec-compliance/README.md +1 -11
  51. data/lib/plugins/inspec-compliance/lib/inspec-compliance/cli.rb +189 -170
  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 +265 -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 +124 -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 +10 -11
  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. data/lib/source_readers/inspec.rb +1 -1
  77. metadata +45 -19
@@ -0,0 +1,265 @@
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
+ until invocations.empty? && @child_tracker.empty?
26
+ while should_start_more_jobs?
27
+ if Inspec.locally_windows?
28
+ spawn_another_process
29
+ else
30
+ fork_another_process
31
+ end
32
+ end
33
+
34
+ update_ui_poll_select
35
+ cleanup_child_processes
36
+ sleep 0.1
37
+ end
38
+
39
+ # Requires renaming operations on windows only
40
+ # Do Rename and delete operations after all child processes have exited successfully
41
+ rename_error_log_files if Inspec.locally_windows?
42
+ cleanup_empty_error_log_files
43
+ cleanup_daemon_process if run_in_background
44
+ end
45
+
46
+ def initiate_background_run
47
+ if Inspec.locally_windows?
48
+ Inspec::UI.new.exit(:usage_error)
49
+ else
50
+ Process.daemon(true, true)
51
+ end
52
+ end
53
+
54
+ def cleanup_daemon_process
55
+ current_process_id = Process.pid
56
+ Process.kill(9, current_process_id)
57
+ # DO NOT TRY TO REFACTOR IT THIS WAY
58
+ # Calling Process.kill(9,Process.pid) kills the "stopper" process itself, rather than the one it's trying to stop.
59
+ end
60
+
61
+ def cleanup_empty_error_log_files
62
+ logs_dir_path = log_path || Dir.pwd
63
+ error_files = Dir.glob("#{logs_dir_path}/logs/*.err")
64
+ error_files.each do |error_file|
65
+ if File.exist?(error_file) && !File.size?(error_file)
66
+ File.delete(error_file)
67
+ end
68
+ end
69
+ end
70
+
71
+ def kill_child_processes
72
+ @child_tracker.each do |pid, info|
73
+ Process.kill("SIGKILL", pid)
74
+ rescue Exception => e
75
+ $stderr.puts "Error while shutting down process #{pid}: #{e.message}"
76
+ end
77
+ # Waiting for child processes to die after they have been killed
78
+ wait_for_child_processes_to_die
79
+ end
80
+
81
+ def wait_for_child_processes_to_die
82
+ until @child_tracker.empty?
83
+ begin
84
+ exited_pid = Process.waitpid(-1, Process::WNOHANG)
85
+ @child_tracker.delete exited_pid if exited_pid && exited_pid > 0
86
+ sleep 1
87
+ rescue Errno::ECHILD
88
+ Inspec::Log.info "Processes shutdown complete!"
89
+ rescue Exception => e
90
+ Inspec::Log.debug "Error while waiting for child processes to shutdown: #{e.message}"
91
+ end
92
+ end
93
+ end
94
+
95
+ def rename_error_log_files
96
+ @child_tracker_persisted.each do |pid, info|
97
+ rename_error_log(info[:error_log_file], pid)
98
+ end
99
+ end
100
+
101
+ def should_start_more_jobs?
102
+ @child_tracker.length < total_jobs && !invocations.empty?
103
+ end
104
+
105
+ def spawn_another_process
106
+ invocation = invocations.shift[:value]
107
+
108
+ child_reader, parent_writer = IO.pipe
109
+ begin
110
+ logs_dir_path = log_path || Dir.pwd
111
+ log_dir = File.join(logs_dir_path, "logs")
112
+ FileUtils.mkdir_p(log_dir)
113
+ error_log_file = File.open("#{log_dir}/#{Time.now.nsec}.err", "a+")
114
+ cmd = "#{$0} #{sub_cmd} #{invocation}"
115
+ log_msg = "#{Time.now.iso8601} Start Time: #{Time.now}\n#{Time.now.iso8601} Arguments: #{invocation}\n"
116
+ child_pid = Process.spawn(cmd, out: parent_writer, err: error_log_file.path)
117
+
118
+ # Logging
119
+ create_logs(child_pid, log_msg)
120
+ @child_tracker[child_pid] = { io: child_reader }
121
+
122
+ # This is used to rename error log files after all child processes are exited
123
+ @child_tracker_persisted[child_pid] = { error_log_file: error_log_file }
124
+ @ui.child_spawned(child_pid, invocation)
125
+
126
+ # Close the file to unlock the error log files opened by processes
127
+ error_log_file.close
128
+ rescue StandardError => e
129
+ $stderr.puts "#{Time.now.iso8601} Error Message: #{e.message}"
130
+ $stderr.puts "#{Time.now.iso8601} Error Backtrace: #{e.backtrace}"
131
+ end
132
+ end
133
+
134
+ def fork_another_process
135
+ invocation = invocations.shift[:value] # Be sure to do this shift() in parent process
136
+ # thing_that_reads_from_the_child, thing_that_writes_to_the_parent = IO.pipe
137
+ child_reader, parent_writer = IO.pipe
138
+ if (child_pid = Process.fork)
139
+ # In parent with newly forked child
140
+ parent_writer.close
141
+ @child_tracker[child_pid] = { io: child_reader }
142
+ @ui.child_forked(child_pid, invocation) unless run_in_background
143
+ else
144
+ # In child
145
+ child_reader.close
146
+ # replace stdout with writer
147
+ $stdout = parent_writer
148
+ create_logs(Process.pid, nil, $stderr)
149
+
150
+ begin
151
+ create_logs(
152
+ Process.pid,
153
+ "#{Time.now.iso8601} Start Time: #{Time.now}\n#{Time.now.iso8601} Arguments: #{invocation}\n"
154
+ )
155
+ runner_invocation(invocation)
156
+ rescue StandardError => e
157
+ $stderr.puts "#{Time.now.iso8601} Error Message: #{e.message}"
158
+ $stderr.puts "#{Time.now.iso8601} Error Backtrace: #{e.backtrace}"
159
+ end
160
+
161
+ # should be unreachable but child MUST exit
162
+ exit(42)
163
+ end
164
+ end
165
+
166
+ # Still in parent
167
+ # Loop over children and check for finished processes
168
+ def cleanup_child_processes
169
+ @child_tracker.each do |pid, info|
170
+ if Process.wait(pid, Process::WNOHANG)
171
+ # Expect to (probably) find EOF marker on the pipe, and close it if so
172
+ update_ui_poll_select(pid)
173
+
174
+ create_logs(pid, "#{Time.now.iso8601} Exit code: #{$?}\n")
175
+
176
+ # child exited - status in $?
177
+ @ui.child_exited(pid) unless run_in_background
178
+ @child_tracker.delete pid
179
+ end
180
+ end
181
+ end
182
+
183
+ def update_ui_poll_select(target_pid = nil)
184
+ # Focus on one pid's pipe if specified, otherwise poll all pipes
185
+ pipes_for_reading = target_pid ? [ @child_tracker[target_pid][:io] ] : @child_tracker.values.map { |i| i[:io] }
186
+ # Next line is due to a race between the close() and the wait()... shouldn't need it, but it fixes the race.
187
+ pipes_for_reading.reject!(&:closed?)
188
+ ready_pipes = IO.select(pipes_for_reading, [], [], 0.1)
189
+ return unless ready_pipes
190
+
191
+ ready_pipes[0].each do |pipe_ready_for_reading|
192
+ # If we weren't provided a PID, hackishly look up the pid from the matching IO.
193
+ pid = target_pid || @child_tracker.keys.detect { |p| @child_tracker[p][:io] == pipe_ready_for_reading }
194
+ begin
195
+ while (update_line = pipe_ready_for_reading.readline) && !pipe_ready_for_reading.closed?
196
+ if update_line =~ /EOF_MARKER/
197
+ pipe_ready_for_reading.close
198
+ break
199
+ elsif update_line =~ /WARN/ || update_line =~ /ERROR/ || update_line =~ /INFO/
200
+ create_logs(
201
+ pid,
202
+ "#{Time.now.iso8601} Extra log: #{update_line}\n"
203
+ )
204
+ break
205
+ end
206
+ update_ui_with_line(pid, update_line) unless run_in_background
207
+ # Only pull one line if we are doing normal updates; slurp the whole file
208
+ # if we are doing a final pull on a targeted PID
209
+ break unless target_pid
210
+ end
211
+ rescue EOFError
212
+ # On unix, readline throws an EOFError when we hit the end. On Windows, nothing apparently happens.
213
+ pipe_ready_for_reading.close
214
+ next
215
+ end
216
+ end
217
+ # TODO: loop over ready_pipes[2] and handle errors?
218
+ end
219
+
220
+ def update_ui_with_line(pid, update_line)
221
+ @ui.child_status_update_line(pid, update_line)
222
+ end
223
+
224
+ private
225
+
226
+ def runner_invocation(runner_option)
227
+ splitted_result = runner_option.split(" ")
228
+ profile_to_run = splitted_result[0]
229
+ splitted_result.delete_at(0)
230
+
231
+ # thor invocation
232
+ arguments = [sub_cmd, profile_to_run, splitted_result].flatten
233
+ Inspec::InspecCLI.start(arguments, enforce_license: true)
234
+ end
235
+
236
+ def create_logs(child_pid, run_log , stderr = nil)
237
+ logs_dir_path = log_path || Dir.pwd
238
+ log_dir = File.join(logs_dir_path, "logs")
239
+ FileUtils.mkdir_p(log_dir)
240
+
241
+ if stderr
242
+ log_file = File.join(log_dir, "#{child_pid}.err") unless File.exist?("#{child_pid}.err")
243
+ stderr.reopen(log_file, "a")
244
+ else
245
+ log_file = File.join(log_dir, "#{child_pid}.log") unless File.exist?("#{child_pid}.log")
246
+ File.write(log_file, run_log, mode: "a")
247
+ end
248
+ end
249
+
250
+ def rename_error_log(error_log_file, child_pid)
251
+ logs_dir_path = log_path || Dir.pwd
252
+ log_dir = File.join(logs_dir_path, "logs")
253
+ FileUtils.mkdir_p(log_dir)
254
+
255
+ if error_log_file.closed? && File.exist?(error_log_file.path)
256
+ begin
257
+ File.rename("#{error_log_file.path}", "#{log_dir}/#{child_pid}.err")
258
+ rescue
259
+ $stderr.puts "Cannot rename error log file #{error_log_file.path} for child pid #{child_pid}"
260
+ end
261
+ end
262
+ end
263
+ end
264
+ end
265
+ 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
@@ -0,0 +1,124 @@
1
+ require "highline"
2
+
3
+ module InspecPlugins::Parallelism
4
+ class SuperReporter
5
+ class Status < InspecPlugins::Parallelism::SuperReporter::Base
6
+
7
+ attr_reader :status_by_pid, :slots
8
+
9
+ def initialize(job_count, invocations)
10
+ @status_by_pid = {}
11
+ @slots = Array.new(job_count)
12
+ paint_header(job_count, invocations)
13
+ paint
14
+ end
15
+
16
+ # --------
17
+ # SuperReporter API
18
+ # --------
19
+ def child_spawned(pid, invocation)
20
+ new_child("spawned", pid, invocation)
21
+ end
22
+
23
+ def child_forked(pid, invocation)
24
+ new_child("forked", pid, invocation)
25
+ end
26
+
27
+ def child_exited(pid)
28
+ slots[status_by_pid[pid][:slot]] = "exited"
29
+
30
+ status_by_pid[pid][:pct] = 100.0
31
+ status_by_pid[pid][:slot] = nil
32
+ status_by_pid[pid][:exit] = $?
33
+
34
+ # TODO: consider holding slot in 100 status for UI grace
35
+
36
+ paint
37
+ end
38
+
39
+ def child_status_update_line(pid, update_line)
40
+ control_serial, status, control_count, title = update_line.split("/")
41
+ percent = 100.0 * control_serial.to_i / control_count.to_i.to_f
42
+
43
+ status_by_pid[pid][:pct] = percent
44
+ status_by_pid[pid][:last_control] = title
45
+ status_by_pid[pid][:last_status] = status
46
+
47
+ paint
48
+ end
49
+
50
+ # --------
51
+ # Utilities
52
+ # --------
53
+ private
54
+
55
+ def new_child(how, pid, invocation)
56
+ # Update status by PID with new info
57
+ status_by_pid[pid] = {
58
+ pct: 0.0,
59
+ inv: invocation,
60
+ how: how,
61
+ }
62
+
63
+ # Assign first empty slot
64
+ slots.each_index do |idx|
65
+ next unless slots[idx].nil? || slots[idx] == "exited"
66
+
67
+ slots[idx] = pid
68
+ status_by_pid[pid][:slot] = idx
69
+ break
70
+ end
71
+
72
+ # TODO: consider printing log message
73
+ paint
74
+ end
75
+
76
+ def terminal_width
77
+ return @terminal_width if @terminal_width
78
+
79
+ @highline ||= HighLine.new
80
+ width = @highline.output_cols.to_i
81
+ width = 80 if width < 1
82
+ @terminal_width = width
83
+ end
84
+
85
+ def paint
86
+ # Determine the width of a slot
87
+ slot_width = terminal_width / slots.length
88
+ line = ""
89
+ # Loop over slots
90
+ slots.each_index do |idx|
91
+ if slots[idx].nil?
92
+ # line += "idle".center(slot_width)
93
+ # Need to improve UI
94
+ elsif slots[idx] == "exited"
95
+ line += "Done".center(slot_width)
96
+ else
97
+ pid = slots[idx]
98
+ with_pid = format("%s: %0.1f%%", pid, status_by_pid[pid][:pct])
99
+ if with_pid.length <= slot_width - 2
100
+ line += with_pid.center(slot_width)
101
+ else
102
+ line += format("%0.1f%%", status_by_pid[pid][:pct]).center(slot_width)
103
+ end
104
+ end
105
+ end
106
+
107
+ print "\r" + (" " * terminal_width) + "\r"
108
+ print line
109
+ end
110
+
111
+ def paint_header(jobs, invocations)
112
+ puts "InSpec Parallel".center(terminal_width)
113
+ puts "Running #{invocations.length} invocations in #{jobs} slots".center(terminal_width)
114
+ puts "-" * terminal_width
115
+ slot_width = terminal_width / slots.length
116
+ slots.each_index do |idx|
117
+ print "Slot #{idx + 1}".center(slot_width)
118
+ end
119
+ puts
120
+ puts "-" * terminal_width
121
+ end
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,23 @@
1
+ module InspecPlugins::Parallelism
2
+ class SuperReporter
3
+ class Text < InspecPlugins::Parallelism::SuperReporter::Base
4
+ def child_spawned(pid, _inv)
5
+ puts "[#{Time.now.iso8601}] Spawned child PID #{pid}"
6
+ end
7
+
8
+ def child_forked(pid, _inv)
9
+ puts "[#{Time.now.iso8601}] Forked child PID #{pid}"
10
+ end
11
+
12
+ def child_exited(pid)
13
+ puts "[#{Time.now.iso8601}] Exited child PID #{pid} status #{$?}"
14
+ end
15
+
16
+ def child_status_update_line(pid, update_line)
17
+ control_serial, _status, control_count, _title = update_line.split("/")
18
+ percent = 100.0 * control_serial.to_i / control_count.to_i.to_f
19
+ puts "[#{Time.now.iso8601}] #{pid} " + format("%.1f%%", percent)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,170 @@
1
+ require "inspec/cli"
2
+ module InspecPlugins
3
+ module Parallelism
4
+ class Validator
5
+
6
+ # TODO: make this list dynamic so plugins can self-declare
7
+ PARALLEL_SAFE_REPORTERS = [
8
+ "automate", # Performs HTTP transactions, silent on STDOUT
9
+ "child-status", # Writes dedicated protocol to STDOUT, expected by parent
10
+ ].freeze
11
+
12
+ attr_accessor :invocations, :sub_cmd, :thor_options_for_sub_cmd, :aliases_mapping, :cli_options, :config_content, :stdin_config
13
+
14
+ def initialize(invocations, cli_options, sub_cmd = "exec")
15
+ @invocations = invocations
16
+ @sub_cmd = sub_cmd
17
+ @thor_options_for_sub_cmd = Inspec::InspecCLI.commands[sub_cmd].options
18
+ @aliases_mapping = create_aliases_mapping
19
+ @cli_options = cli_options
20
+ @config_content = nil
21
+ @stdin_config = nil
22
+ end
23
+
24
+ def validate
25
+ invocations.each do |invocation_data|
26
+ invocation_data[:validation_errors] = []
27
+
28
+ convert_cli_to_thor_options(invocation_data)
29
+ check_for_spurious_options(invocation_data)
30
+ check_for_required_fields(invocation_data)
31
+ check_for_reporter_options(invocation_data)
32
+
33
+ end
34
+ end
35
+
36
+ def validate_log_path
37
+ return [] unless cli_options["log_path"]
38
+
39
+ if File.directory?(cli_options["log_path"])
40
+ []
41
+ else
42
+ [true, "Log path #{cli_options["log_path"]} is not accessible"]
43
+ end
44
+ end
45
+
46
+ private
47
+
48
+ def create_aliases_mapping
49
+ alias_mapping = {}
50
+ thor_options_for_sub_cmd.each do |_, sub_cmd_option|
51
+ aliases = sub_cmd_option.aliases
52
+ unless aliases.empty?
53
+ alias_mapping[aliases[0]] = sub_cmd_option.name
54
+ end
55
+ end
56
+ alias_mapping
57
+ end
58
+
59
+ def check_for_spurious_options(invocation_data)
60
+ # LIMITATION: Assume the first arg is the profile name, and there is exactly one of them.
61
+ invalid_options = invocation_data[:thor_args][1..-1]
62
+ invocation_data[:validation_errors].push "No such option: #{invalid_options}" unless invalid_options.empty?
63
+ end
64
+
65
+ def check_for_required_fields(invocation_data)
66
+ required_fields = thor_options_for_sub_cmd.collect { |_, thor_option| thor_option.name if thor_option.required }.compact
67
+ option_keys = invocation_data[:thor_opts].keys
68
+ invocation_data[:thor_opts].keys.map { |key| option_keys.push(aliases_mapping[key.to_sym]) if aliases_mapping[key.to_sym] }
69
+ if !required_fields.empty? && (option_keys & required_fields).empty?
70
+ invocation_data[:validation_errors].push "No value provided for required options: #{required_fields}"
71
+ end
72
+ end
73
+
74
+ def check_for_reporter_options(invocation_data)
75
+ # if no reporter option, that's an error
76
+ unless invocation_data[:thor_opts].include?("reporter")
77
+ # Check for config reporter validation only if --reporter option is missing from options file
78
+ return if check_reporter_options_in_config(invocation_data)
79
+
80
+ invocation_data[:validation_errors] << "A --reporter option must be specified for each invocation in the options file"
81
+ return
82
+ end
83
+
84
+ have_child_status_reporter = false
85
+
86
+ # Reporter option is formatted as an array
87
+ invocation_data[:thor_opts]["reporter"].each do |reporter_spec|
88
+ reporter_name, file_output = reporter_spec.split(":")
89
+
90
+ have_child_status_reporter = true if reporter_name == "child-status"
91
+
92
+ # if there is a reporter option, each entry must either write to a file or
93
+ # else be the special child-status reporter or the automate reporter
94
+ next if PARALLEL_SAFE_REPORTERS.include?(reporter_name)
95
+
96
+ unless file_output
97
+ invocation_data[:validation_errors] << "The #{reporter_name} reporter requires being directed to a file, like #{reporter_name}:filename.out"
98
+ end
99
+ end
100
+
101
+ # if there is no child-status reporter, add one to the raw value and the parsed array
102
+ unless have_child_status_reporter
103
+ # Eww
104
+ invocation_data[:thor_opts]["reporter"] << "child-status"
105
+ invocation_data[:value].gsub!("--reporter ", "--reporter child-status ")
106
+ end
107
+ end
108
+
109
+ def check_reporter_options_in_config(invocation_data)
110
+ config_opts = invocation_data[:thor_opts]["config"] || invocation_data[:thor_opts]["json_config"]
111
+ cfg_io = check_for_piped_config_from_stdin(config_opts)
112
+
113
+ if cfg_io == STDIN
114
+ # Scenario of using config from STDIN
115
+ @config_content ||= cfg_io.read
116
+ else
117
+ if config_opts.nil?
118
+ # Scenario of using default config.json file when path not provided
119
+ default_path = File.join(Inspec.config_dir, "config.json")
120
+ config_opts = default_path
121
+ return unless File.exist?(config_opts)
122
+ elsif !File.exist?(config_opts)
123
+ invocation_data[:validation_errors] << "Could not read configuration file at #{config_opts}"
124
+ return
125
+ end
126
+ @config_content = File.open(config_opts).read
127
+ end
128
+
129
+ reporter_config = JSON.parse(config_content)["reporter"] unless config_content.nil? || config_content.empty?
130
+ unless reporter_config
131
+ invocation_data[:validation_errors] << "Config should have reporter option specified for each invocation which is not using --reporter option in options file"
132
+ end
133
+ @config_content
134
+ end
135
+
136
+ def check_for_piped_config_from_stdin(config_opts)
137
+ return nil unless config_opts
138
+ return nil unless config_opts == "-"
139
+
140
+ @stdin_config ||= STDIN
141
+ end
142
+
143
+ ## Utility functions
144
+
145
+ # Parse the invocation string using Thor into Thor options
146
+ # This approach was reverse engineered from studying
147
+ # https://github.com/rails/thor/blob/ab3b5be455791f4efb79f0efb4f88cc6b59c8ccf/lib/thor/base.rb#L53
148
+
149
+ def convert_cli_to_thor_options(invocation_data)
150
+ invocation_words = invocation_data[:value].split(" ")
151
+
152
+ # LIMITATION: this approach is limited to having exactly one profile in the invocation
153
+ args = [invocation_words.shift] # That is, the profile path
154
+
155
+ # Here we're piggybacking on on a hook used by the start() method, and provides the
156
+ # specifics for the subcommand
157
+ config = { command_options: thor_options_for_sub_cmd }
158
+
159
+ # This performs the parse
160
+ thor = Inspec::InspecCLI.new(args, invocation_words, config)
161
+
162
+ # A hash (with indifferent access) of option names to option config data
163
+ invocation_data[:thor_opts] = thor.options
164
+
165
+ # A list of everything else it could not parse, including the profile
166
+ invocation_data[:thor_args] = thor.args
167
+ end
168
+ end
169
+ end
170
+ end
@@ -0,0 +1,18 @@
1
+ module InspecPlugins
2
+ module Parallelism
3
+ class Plugin < ::Inspec.plugin(2)
4
+ plugin_name :"inspec-parallel"
5
+
6
+ cli_command :parallel do
7
+ require_relative "inspec-parallel/cli"
8
+ InspecPlugins::Parallelism::CLI
9
+ end
10
+
11
+ streaming_reporter :"child-status" do
12
+ require_relative "inspec-parallel/child_status_reporter"
13
+ InspecPlugins::Parallelism::StreamingReporter
14
+ end
15
+
16
+ end
17
+ end
18
+ end
@@ -36,15 +36,11 @@ module InspecPlugins
36
36
  FileUtils.mkdir_p(path)
37
37
 
38
38
  puts "Generating signing key in #{path}/#{options["keyname"]}.pem.key"
39
- # https://github.com/inspec/inspec/security/code-scanning/1
40
- # https://github.com/inspec/inspec/security/code-scanning/2
41
- # The following line was flagged by GitHub code scanning as a security vulnerability.
42
- # Update the code to eliminate the vulnerability.
43
- File.open("#{path}/#{options["keyname"]}.pem.key", "w") do |io|
39
+ open "#{path}/#{options["keyname"]}.pem.key", "w" do |io|
44
40
  io.write key.to_pem
45
41
  end
46
42
  puts "Generating validation key in #{path}/#{options["keyname"]}.pem.pub"
47
- File.open("#{path}/#{options["keyname"]}.pem.pub", "w") do |io|
43
+ open "#{path}/#{options["keyname"]}.pem.pub", "w" do |io|
48
44
  io.write key.public_key.to_pem
49
45
  end
50
46
  end
@@ -71,8 +67,7 @@ module InspecPlugins
71
67
  # Generating tar.gz file using archive method of Inspec Cli
72
68
  Inspec::InspecCLI.new.archive(profile_path, "error")
73
69
  tarfile = "#{filename}.tar.gz"
74
- # Update IO.binread with File.binread because of https://github.com/inspec/inspec/security/code-scanning/3
75
- tar_content = File.binread(tarfile)
70
+ tar_content = IO.binread(tarfile)
76
71
  FileUtils.rm(tarfile)
77
72
 
78
73
  # Generate the signature
@@ -97,12 +92,16 @@ module InspecPlugins
97
92
  Inspec::UI.new.exit(:usage_error)
98
93
  end
99
94
 
100
- def self.profile_verify(signed_profile_path)
95
+ def self.profile_verify(signed_profile_path, silent = false)
101
96
  file_to_verify = signed_profile_path
102
- puts "Verifying #{file_to_verify}"
97
+ puts "Verifying #{file_to_verify}" unless silent
103
98
 
104
99
  iaf_file = Inspec::IafFile.new(file_to_verify)
105
100
  if iaf_file.valid?
101
+ # Signed profile verification is called from runner and not from CLI
102
+ # Do not exit and do not print logs
103
+ return if silent
104
+
106
105
  puts "Detected format version '#{iaf_file.version}'"
107
106
  puts "Attempting to verify using key '#{iaf_file.key_name}'"
108
107
  puts "Profile is valid."
@@ -157,7 +156,7 @@ module InspecPlugins
157
156
  ui.exit(:usage_error)
158
157
  end
159
158
 
160
- lines = File.readlines(p)
159
+ lines = IO.readlines(p)
161
160
  lines << "\nprofile_content_id: #{profile_content_id}\n"
162
161
 
163
162
  File.open("#{p}", "w" ) do |f|