require "functional/helper" require "tempfile" # For tests related to reading inputs from plugins, see plugins_test.rb describe "inputs" do include FunctionalHelper let(:inputs_profiles_path) { File.join(profile_path, "inputs") } let(:external_attributes_file_path) { "#{inputs_profiles_path}/hashmap/files/inputs.yml" } parallelize_me! # This tests being able to load complex structures from # cli option-specified files. %w{ flat nested }.each do |input_file| it "runs OK on #{input_file} inputs" do cmd = "exec " cmd += File.join(inputs_profiles_path, "basic") cmd += " --no-create-lockfile" cmd += " --input-file " + File.join(inputs_profiles_path, "basic", "files", "#{input_file}.yaml") cmd += " --controls " + input_file result = run_inspec_process(cmd) _(result.stderr).must_equal "" assert_exit_code 0, result end end describe "when asking for usage help" do it "includes the new --input-file option" do result = run_inspec_process("exec help", lock: true) # --no-create-lockfile option breaks usage help lines = result.stdout.split("\n") line = lines.detect { |l| l.include? "--input-file" } _(line).wont_be_nil end it "includes the legacy --attrs option" do result = run_inspec_process("exec help", lock: true) lines = result.stdout.split("\n") line = lines.detect { |l| l.include? "--attrs" } _(line).wont_be_nil end it "includes the --input option" do result = run_inspec_process("exec help", lock: true) # --no-create-lockfile option breaks usage help assert_match(/--input\s/, result.stdout) # Careful not to match --input-file end end describe "when using a cli-specified file" do let(:result) do cmd = "exec " cmd += File.join(inputs_profiles_path, "basic") + " " cmd += flag + " " + File.join(inputs_profiles_path, "basic", "files", "flat.yaml") cmd += " --controls flat" run_inspec_process(cmd) end describe "when the --input-file flag is used" do let(:flag) { "--input-file" } it "works" do assert_exit_code 0, result end end describe "when the --attrs flag is used" do let(:flag) { "--attrs" } it "works" do assert_exit_code 0, result end end end describe "when being passed inputs via the Runner API" do let(:run_result) { run_runner_api_process(runner_options) } let(:common_options) do { profile: "#{inputs_profiles_path}/via-runner", reporter: ["json"], } end # options: # profile: path to profile to run # All other opts passed to InSpec::Runner.new(...) # then add.target is called def run_runner_api_process(options) # Remove profile from options. All other are passed to Runner. profile = options.delete(:profile) # Make a tmpfile Tempfile.open do |script| # Clear and concat - can't just assign, it's readonly script.puts <<~EOSCRIPT # Ruby load path $LOAD_PATH.clear $LOAD_PATH.concat(#{$LOAD_PATH}) # require inspec require "inspec" require "inspec/runner" # inject pretty-printed runner opts runner_args = #{options.inspect} # Profile to run: profile_location = "#{profile}" # Run Execution runner = Inspec::Runner.new(runner_args) runner.add_target profile_location runner.run EOSCRIPT script.flush run_cmd("ruby #{script.path}") end end describe "when using the current :inputs key" do let(:runner_options) { common_options.merge({ inputs: { test_input_01: "value_from_api" } }) } it "finds the values and does not issue any warnings" do output = run_result.stdout refute_includes output, "DEPRECATION" structured_output = JSON.parse(output) assert_equal "passed", structured_output["profiles"][0]["controls"][0]["results"][0]["status"] end end describe "when using the current :inputs key with both string and symbol key in hashes" do let(:runner_options) { common_options.merge({ inputs: { test_input_01: "value_from_api", test_input_hash_string: { "string_key": "string_value" }, test_input_hash_symbol: { symbol_key: :symbol_value } } }) } it "finds the values and runs successfully" do output = run_result.stdout structured_output = JSON.parse(output) assert_equal "passed", structured_output["profiles"][0]["controls"][0]["results"][0]["status"] assert_equal "passed", structured_output["profiles"][0]["controls"][0]["results"][1]["status"] assert_equal "passed", structured_output["profiles"][0]["controls"][0]["results"][2]["status"] end end describe "when using the legacy :attributes key" do let(:runner_options) { common_options.merge({ attributes: { test_input_01: "value_from_api" } }) } it "finds the values but issues a DEPRECATION warning" do run = run_result output = run.stdout assert_empty run.stderr assert_includes output, "DEPRECATION" structured_output = JSON.parse(output.lines.reject { |l| l.include? "DEPRECATION" }.join("\n") ) assert_equal "passed", structured_output["profiles"][0]["controls"][0]["results"][0]["status"] end end end describe "when using the --input inline raw input flag CLI option" do let(:result) { run_inspec_process("exec #{inputs_profiles_path}/cli #{input_opt} #{control_opt}", json: true) } let(:control_opt) { "" } describe "when the --input is used once with one value" do let(:input_opt) { "--input test_input_01=value_from_cli_01" } let(:control_opt) { "--controls test_control_01" } it("correctly reads the input") { assert_json_controls_passing(result) } end describe "when the --input is used once with two values" do let(:input_opt) { "--input test_input_01=value_from_cli_01 test_input_02=value_from_cli_02" } let(:control_opt) { "--controls test_control_01 test_control_02" } it("correctly reads both inputs") { assert_json_controls_passing(result) } end describe "when the --input is used once with two values and a comma" do let(:input_opt) { "--input test_input_01=value_from_cli_01, test_input_02=value_from_cli_02" } let(:control_opt) { "--controls test_control_01 test_control_02" } it("correctly reads both inputs ignoring the comma") { assert_json_controls_passing(result) } end # See https://github.com/inspec/inspec/issues/4977 describe "when the --input is used with a numeric value" do describe "when the --input is used with an integer value" do let(:input_opt) { "--input test_input_03=11" } let(:control_opt) { "--controls test_control_numeric_implicit" } it("correctly reads the input as numeric") { assert_json_controls_passing(result) } end describe "when the --input is used with a integer value and is declared as a numeric type in metadata" do let(:input_opt) { "--input test_input_04=11" } let(:control_opt) { "--controls test_control_numeric_type" } it("correctly reads the input as numeric") { assert_json_controls_passing(result) } end describe "when the --input is used with a float value" do let(:input_opt) { "--input test_input_05=-11.0" } let(:control_opt) { "--controls test_control_numeric_float" } it("correctly reads the input as numeric") { assert_json_controls_passing(result) } end end # See https://github.com/inspec/inspec/issues/4799 describe "when the --input is used with a boolean value" do describe "when the --input is passed true" do let(:input_opt) { "--input test_input_13=true" } let(:control_opt) { "--controls test_control_bool_true" } it("correctly reads the input as TrueClass") { assert_json_controls_passing(result) } end describe "when the --input is passed false" do let(:input_opt) { "--input test_input_14=false" } let(:control_opt) { "--controls test_control_bool_false" } it("correctly reads the input as FalseClass") { assert_json_controls_passing(result) } end describe "when the --input is passed TRUE" do let(:input_opt) { "--input test_input_15=TRUE" } let(:control_opt) { "--controls test_control_bool_true_caps" } it("correctly reads the input as a TrueClass even when capitalized") { assert_json_controls_passing(result) } end end describe "when the --input is a complex structure" do # Garbage describe "when the --input is malformed YAML " do let(:input_opt) { "--input test_input_08='[a, b, }]'" } it "runs with failed tests and provides a YAML warning message" do output = result.stderr assert_includes output, "WARN" assert_includes output, "treated as YAML" assert_includes output, "test_input_08" assert_exit_code(100, result) end end # YAML describe "when the --input is a YAML array" do let(:input_opt) { "--input test_input_06='[a,b,c]'" } let(:control_opt) { "--controls test_control_yaml_array" } it("correctly reads the input as a yaml array") { assert_json_controls_passing(result) } end describe "when the --input is a YAML hash" do # Note: this produces a String-keyed Hash let(:input_opt) { "--input test_input_07='{a: apples, b: bananas, c: cantelopes}'" } let(:control_opt) { "--controls test_control_yaml_hash" } it("correctly reads the input as a yaml hash") { assert_json_controls_passing(result) } end describe "when the --input is a YAML deep structure" do # Note: this produces a String-keyed Hash let(:input_opt) { "--input test_input_09='{a: apples, g: [grape01, grape02] }'" } let(:control_opt) { "--controls test_control_yaml_deep" } it("correctly reads the input as a yaml struct") { assert_json_controls_passing(result) } end # JSON mode # These three were tested manually on 2020-05-05 on win2016 and passed # Under CI however, we have multiple layers of quoting and dequoting # and it breaks badly. https://github.com/inspec/inspec/issues/5015 unless windows? describe "when the --input is a JSON array" do let(:input_opt) { %q{--input test_input_10='["a","b","c"]'} } let(:control_opt) { "--controls test_control_json_array" } it("correctly reads the input as a json array") { assert_empty result.stderr assert_json_controls_passing(result) } end describe "when the --input is a JSON hash" do # Note: this produces a String-keyed Hash let(:input_opt) { %q{--input test_input_11='{"a": "apples", "b": "bananas", "c": "cantelopes"}'} } let(:control_opt) { "--controls test_control_json_hash" } it("correctly reads the input as a json hash") { assert_json_controls_passing(result) } end describe "when the --input is a JSON deep structure" do # Note: this produces a String-keyed Hash let(:input_opt) { %q{--input test_input_12='{"a": "apples", "g": ["grape01", "grape02"] }'} } let(:control_opt) { "--controls test_control_json_deep" } it("correctly reads the input as a json struct") { assert_json_controls_passing(result) } end end end describe "when the --input is used twice with one value each" do let(:input_opt) { "--input test_input_01=value_from_cli_01 --input test_input_02=value_from_cli_02" } let(:control_opt) { "--controls test_control_02" } # Expected, though unfortunate, behavior is to only notice the second input it("correctly reads the second input") { assert_json_controls_passing(result) } end describe "when the --input is used with no equal sign" do let(:input_opt) { "--input value_from_cli_01" } it "does not run and provides an error message" do output = result.stdout assert_includes "ERROR", output assert_includes "An '=' is required", output assert_includes "input_name_1=input_value_1", output assert_equal 1, result.exit_status end end describe "when the --input is used with a .yaml extension" do let(:input_opt) { "--input myfile.yaml" } it "does not run and provides an error message" do output = result.stdout assert_includes "ERROR", output assert_includes "individual input values", output assert_includes "Use --input-file", output assert_equal 1, result.exit_status end end end describe "when accessing inputs in a variety of scopes using the DSL" do it "is able to read the inputs using the input keyword" do cmd = "exec #{inputs_profiles_path}/scoping" result = run_inspec_process(cmd, json: true) assert_json_controls_passing(result) end it "is able to read the inputs using the legacy attribute keyword" do cmd = "exec #{inputs_profiles_path}/legacy-attributes-dsl" result = run_inspec_process(cmd, json: true) assert_json_controls_passing(result) end it "is able to read the inputs in describe.one blocks" do cmd = "exec #{inputs_profiles_path}/describe-one" result = run_inspec_process(cmd, json: true) assert_json_controls_passing(result) end end describe "run profile with metadata inputs" do it "works when using the new 'inputs' key" do cmd = "exec #{inputs_profiles_path}/metadata-basic" result = run_inspec_process(cmd, json: true) assert_json_controls_passing(result) _(result.stderr).must_be_empty end it "works when using the legacy 'attributes' key" do cmd = "exec #{inputs_profiles_path}/metadata-legacy" result = run_inspec_process(cmd, json: true) assert_json_controls_passing(result) # Will eventually issue deprecation warning end it "does not error when inputs are empty" do cmd = "exec " cmd += File.join(inputs_profiles_path, "metadata-empty") result = run_inspec_process(cmd, json: true) _(result.stderr).must_include "WARN: Inputs must be defined as an Array in metadata files. Skipping definition from profile-with-empty-attributes." assert_exit_code 0, result end it "errors with invalid input types" do cmd = "exec " cmd += File.join(inputs_profiles_path, "metadata-invalid") result = run_inspec_process(cmd, json: true) _(result.stderr).must_equal "Type 'Color' is not a valid input type.\n" assert_exit_code 1, result end it "errors with required input not defined" do cmd = "exec " cmd += File.join(inputs_profiles_path, "metadata-required") result = run_inspec_process(cmd, json: true) _(result.stderr).must_include "Input 'a_required_input' is required and does not have a value.\n" assert_exit_code 1, result end describe "when profile inheritance is used" do it "should correctly assign input values using namespacing" do cmd = "exec " + File.join(inputs_profiles_path, "inheritance", "wrapper") result = run_inspec_process(cmd, json: true) assert_json_controls_passing(result) end end end describe "when using a profile with undeclared (valueless) inputs" do it "should warn about them and not abort the run" do cmd = "exec #{inputs_profiles_path}/undeclared" result = run_inspec_process(cmd, json: true) _(result.stderr).must_include "WARN: Input 'undeclared_01'" _(result.stderr).must_include "does not have a value" assert_json_controls_passing(result) end end # Addresses https://github.com/inspec/inspec/issues/4769 describe "when using a profile with required inputs" do describe "when the values are not provided" do it "should emit an error and exit code 1" do cmd = "exec #{inputs_profiles_path}/required" result = run_inspec_process(cmd, json: true) _(result.stderr).must_include "Input 'required_01'" _(result.stderr).must_include "does not have a value" assert_exit_code 1, result end end describe "when the values are provided by an input file" do it "should not warn and run normally" do cmd = "exec #{inputs_profiles_path}/required --input-file #{inputs_profiles_path}/required/files/inputs.yaml" result = run_inspec_process(cmd, json: true) _(result.stderr).must_be_empty assert_json_controls_passing(result) end end describe "when the values are provided by a CLI flag" do it "should not warn and run normally" do cmd = "exec #{inputs_profiles_path}/required --input required_01=anything" result = run_inspec_process(cmd, json: true) _(result.stderr).must_be_empty assert_json_controls_passing(result) end end end describe "when a profile is used with sensitive inputs" do it "should access the values but hide them in the reporter data" do result = run_inspec_process("exec #{inputs_profiles_path}/metadata-sensitive", json: true) _(result.stderr).must_be_empty assert_json_controls_passing(result) # Inspect the JSON result to find the inputs hash and verify they are redacted as needed inputs = @json["profiles"][0]["attributes"] # TODO rename to inputs, break automate _(inputs[1]["options"]["value"]).wont_include "secret" _(inputs[1]["options"]["value"]).must_include "***" _(inputs[2]["options"]["value"]).wont_include "***" # Explicit sensitive = false end end describe "when a profile is executed with inputs through external file, metadata file and profile DSL" do it "should access the values successfully from all input ways" do result = run_inspec_process("exec #{inputs_profiles_path}/hashmap --input-file #{external_attributes_file_path}", json: true) _(result.stderr).must_be_empty assert_json_controls_passing(result) end end describe "when a profile is used with input options" do it "should be a success for valid values when pattern flag is passed through metadata file" do result = run_inspec_process("exec #{inputs_profiles_path}/metadata-pattern", json: true) _(result.stderr).must_be_empty assert_json_controls_passing(result) end it "should be a success for valid values when required, type and pattern flag is passed through dsl" do result = run_inspec_process("exec #{inputs_profiles_path}/dsl --controls pattern_flag_success_check required_flag_success_check type_flag_success_check", json: true) _(result.stderr).must_be_empty assert_json_controls_passing(result) end it "should be a failure for invalid value when required flag is passed through dsl" do result = run_inspec_process("exec #{inputs_profiles_path}/dsl --controls required_flag_failure_check", json: true) _(result.stderr).must_include "Input 'input_value_04' is required and does not have a value.\n" assert_exit_code 1, result end it "should be a failure for invalid value when type flag is passed through dsl" do result = run_inspec_process("exec #{inputs_profiles_path}/dsl --controls type_flag_failure_check", json: true) _(result.stderr).must_be_empty output = JSON.parse(result[0]) assert_equal "failed", output["profiles"][0]["controls"][0]["results"][0]["status"] assert_exit_code(100, result) end it "should be a failure for invalid value when pattern flag is passed through dsl" do result = run_inspec_process("exec #{inputs_profiles_path}/dsl --controls pattern_flag_failure_check", json: true) _(result.stderr).must_be_empty output = JSON.parse(result[0]) assert_equal "failed", output["profiles"][0]["controls"][0]["results"][0]["status"] assert_exit_code(100, result) end end end