inspec-core 5.22.72 → 6.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Chef-EULA +9 -0
- data/Gemfile +24 -38
- data/etc/features.sig +6 -0
- data/etc/features.yaml +94 -0
- data/inspec-core.gemspec +16 -15
- data/lib/inspec/backend.rb +2 -0
- data/lib/inspec/base_cli.rb +80 -4
- data/lib/inspec/cached_fetcher.rb +24 -3
- data/lib/inspec/cli.rb +292 -235
- data/lib/inspec/config.rb +24 -11
- data/lib/inspec/dependencies/cache.rb +33 -0
- data/lib/inspec/dependencies/dependency_set.rb +2 -2
- data/lib/inspec/dsl.rb +1 -1
- data/lib/inspec/enhanced_outcomes.rb +1 -0
- data/lib/inspec/errors.rb +5 -0
- data/lib/inspec/exceptions.rb +2 -0
- data/lib/inspec/feature/config.rb +75 -0
- data/lib/inspec/feature/runner.rb +26 -0
- data/lib/inspec/feature.rb +34 -0
- data/lib/inspec/fetcher/git.rb +5 -0
- data/lib/inspec/fetcher/url.rb +7 -29
- data/lib/inspec/globals.rb +6 -0
- data/lib/inspec/input_registry.rb +1 -5
- data/lib/inspec/plugin/v1/plugin_types/fetcher.rb +7 -0
- data/lib/inspec/plugin/v2/plugin_types/streaming_reporter.rb +30 -2
- data/lib/inspec/profile.rb +46 -3
- data/lib/inspec/reporters/cli.rb +1 -1
- data/lib/inspec/reporters.rb +67 -54
- data/lib/inspec/resources/groups.rb +0 -52
- data/lib/inspec/resources/nftables.rb +1 -14
- data/lib/inspec/resources/oracledb_session.rb +3 -9
- data/lib/inspec/resources/postgres_session.rb +5 -9
- data/lib/inspec/resources/sybase_session.rb +2 -11
- data/lib/inspec/resources/virtualization.rb +1 -1
- data/lib/inspec/rule.rb +9 -14
- data/lib/inspec/run_data.rb +7 -5
- data/lib/inspec/runner.rb +35 -6
- data/lib/inspec/runner_rspec.rb +12 -9
- data/lib/inspec/secrets/yaml.rb +9 -3
- data/lib/inspec/shell.rb +10 -0
- data/lib/inspec/ui.rb +4 -0
- data/lib/inspec/utils/licensing_config.rb +9 -0
- data/lib/inspec/utils/profile_ast_helpers.rb +2 -1
- data/lib/inspec/utils/waivers/csv_file_reader.rb +1 -1
- data/lib/inspec/utils/waivers/excel_file_reader.rb +1 -1
- data/lib/inspec/version.rb +1 -1
- data/lib/inspec/waiver_file_reader.rb +68 -27
- data/lib/inspec.rb +2 -1
- data/lib/matchers/matchers.rb +3 -3
- data/lib/plugins/inspec-compliance/README.md +1 -11
- data/lib/plugins/inspec-compliance/lib/inspec-compliance/cli.rb +189 -170
- data/lib/plugins/inspec-habitat/lib/inspec-habitat/cli.rb +10 -3
- data/lib/plugins/inspec-init/lib/inspec-init/cli.rb +1 -0
- data/lib/plugins/inspec-init/lib/inspec-init/cli_plugin.rb +23 -21
- data/lib/plugins/inspec-init/lib/inspec-init/cli_profile.rb +15 -13
- data/lib/plugins/inspec-init/lib/inspec-init/cli_resource.rb +15 -13
- data/lib/plugins/inspec-license/README.md +16 -0
- data/lib/plugins/inspec-license/inspec-license.gemspec +6 -0
- data/lib/plugins/inspec-license/lib/inspec-license/cli.rb +26 -0
- data/lib/plugins/inspec-license/lib/inspec-license.rb +14 -0
- data/lib/plugins/inspec-parallel/README.md +27 -0
- data/lib/plugins/inspec-parallel/inspec-parallel.gemspec +6 -0
- data/lib/plugins/inspec-parallel/lib/inspec-parallel/child_status_reporter.rb +61 -0
- data/lib/plugins/inspec-parallel/lib/inspec-parallel/cli.rb +39 -0
- data/lib/plugins/inspec-parallel/lib/inspec-parallel/command.rb +219 -0
- data/lib/plugins/inspec-parallel/lib/inspec-parallel/runner.rb +265 -0
- data/lib/plugins/inspec-parallel/lib/inspec-parallel/super_reporter/base.rb +24 -0
- data/lib/plugins/inspec-parallel/lib/inspec-parallel/super_reporter/silent.rb +7 -0
- data/lib/plugins/inspec-parallel/lib/inspec-parallel/super_reporter/status.rb +124 -0
- data/lib/plugins/inspec-parallel/lib/inspec-parallel/super_reporter/text.rb +23 -0
- data/lib/plugins/inspec-parallel/lib/inspec-parallel/validator.rb +170 -0
- data/lib/plugins/inspec-parallel/lib/inspec-parallel.rb +18 -0
- data/lib/plugins/inspec-sign/lib/inspec-sign/base.rb +10 -11
- data/lib/plugins/inspec-sign/lib/inspec-sign/cli.rb +11 -4
- data/lib/plugins/inspec-streaming-reporter-progress-bar/lib/inspec-streaming-reporter-progress-bar/streaming_reporter.rb +6 -13
- data/lib/source_readers/inspec.rb +1 -1
- metadata +45 -25
@@ -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,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
|
-
#
|
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
|
-
|
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
|
-
|
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 =
|
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|
|