mirror of
https://github.com/inspec/inspec
synced 2024-12-24 03:53:15 +00:00
e054be7d0d
Signed-off-by: Clinton Wolfe <clintoncwolfe@gmail.com>
477 lines
18 KiB
Ruby
477 lines
18 KiB
Ruby
|
|
require 'helper'
|
|
require 'stringio'
|
|
|
|
require 'inspec/config'
|
|
|
|
describe 'Inspec::Config' do
|
|
|
|
# ========================================================================== #
|
|
# Constructor
|
|
# ========================================================================== #
|
|
describe 'the constructor' do
|
|
describe 'when no args are provided' do
|
|
it 'should initialize properly' do
|
|
cfg = Inspec::Config.new
|
|
cfg.must_respond_to :final_options
|
|
end
|
|
end
|
|
|
|
describe 'when CLI args are provided' do
|
|
it 'should initialize properly' do
|
|
cfg = Inspec::Config.new({color: true, log_level: 'warn'})
|
|
cfg.must_respond_to :final_options
|
|
end
|
|
end
|
|
|
|
# TODO: add test for reading from default config path
|
|
|
|
end
|
|
|
|
# ========================================================================== #
|
|
# File Validation
|
|
# ========================================================================== #
|
|
describe 'when validating a file' do
|
|
let(:cfg) { Inspec::Config.new({}, cfg_io) }
|
|
let(:cfg_io) { StringIO.new(ConfigTestHelper.fixture(fixture_name)) }
|
|
let(:seen_fields) { cfg.final_options.keys.sort }
|
|
|
|
describe 'when the file is a legacy file' do
|
|
let(:fixture_name) { 'legacy' }
|
|
it 'should read the file successfully' do
|
|
expected = ['color', 'reporter', 'target_id', 'type'].sort
|
|
seen_fields.must_equal expected
|
|
end
|
|
end
|
|
|
|
describe 'when the file is a valid v1.1 file' do
|
|
let(:fixture_name) { 'basic' }
|
|
it 'should read the file successfully' do
|
|
expected = ['create_lockfile', 'reporter', 'type'].sort
|
|
seen_fields.must_equal expected
|
|
end
|
|
end
|
|
|
|
describe 'when the file is minimal' do
|
|
let(:fixture_name) { 'minimal' }
|
|
it 'should read the file successfully' do
|
|
expected = ['reporter', 'type'].sort
|
|
seen_fields.must_equal expected
|
|
end
|
|
end
|
|
|
|
describe 'when the file has malformed json' do
|
|
let(:fixture_name) { 'malformed_json' }
|
|
it 'should throw an exception' do
|
|
ex = proc { cfg }.must_raise(Inspec::ConfigError::MalformedJson)
|
|
# Failed to load JSON configuration: 765: unexpected token at '{ "hot_garbage": "a", "version": "1.1",
|
|
# '
|
|
# Config was: "{ \"hot_garbage\": \"a\", \"version\": \"1.1\", \n"
|
|
ex.message.must_include 'Failed to load JSON config' # The message
|
|
ex.message.must_include 'unexpected token' # The specific parser error
|
|
ex.message.must_include 'hot_garbage' # A sample of the unacceptable contents
|
|
end
|
|
end
|
|
|
|
describe 'when the file has a bad file version' do
|
|
let(:fixture_name) { 'bad_version' }
|
|
it 'should throw an exception' do
|
|
ex = proc { cfg }.must_raise(Inspec::ConfigError::Invalid)
|
|
ex.message.must_include 'Unsupported config file version'
|
|
ex.message.must_include '99.99'
|
|
ex.message.must_include '1.1'
|
|
end
|
|
end
|
|
|
|
describe 'when a 1.1 file has an invalid top-level entry' do
|
|
let(:fixture_name) { 'bad_top_level' }
|
|
it 'should throw an exception' do
|
|
ex = proc { cfg }.must_raise(Inspec::ConfigError::Invalid)
|
|
ex.message.must_include 'Unrecognized top-level'
|
|
ex.message.must_include 'unsupported_field'
|
|
ex.message.must_include 'compliance'
|
|
end
|
|
end
|
|
end
|
|
|
|
# ========================================================================== #
|
|
# Defaults
|
|
# ========================================================================== #
|
|
describe 'reading defaults' do
|
|
let(:cfg) { Inspec::Config.new({}, nil, command) }
|
|
let(:final_options) { cfg.final_options }
|
|
let(:seen_fields) { cfg.final_options.keys.sort }
|
|
|
|
describe 'when the exec command is used' do
|
|
let(:command) { :exec }
|
|
it 'should have the correct defaults' do
|
|
expected = ['color', 'create_lockfile', 'backend_cache', 'reporter', 'show_progress', 'type'].sort
|
|
seen_fields.must_equal expected
|
|
final_options['reporter'].must_be_kind_of Hash
|
|
final_options['reporter'].count.must_equal 1
|
|
final_options['reporter'].keys.must_include 'cli'
|
|
final_options['show_progress'].must_equal false
|
|
final_options['color'].must_equal true
|
|
final_options['create_lockfile'].must_equal true
|
|
final_options['backend_cache'].must_equal true
|
|
end
|
|
end
|
|
|
|
describe 'when the shell command is used' do
|
|
let(:command) { :shell }
|
|
it 'should have the correct defaults' do
|
|
expected = ['reporter', 'type'].sort
|
|
seen_fields.must_equal expected
|
|
final_options['reporter'].must_be_kind_of Hash
|
|
final_options['reporter'].count.must_equal 1
|
|
final_options['reporter'].keys.must_include 'cli'
|
|
end
|
|
end
|
|
end
|
|
|
|
# ========================================================================== #
|
|
# Reading CLI Options
|
|
# ========================================================================== #
|
|
# The config facility supports passing in CLI options in the constructor, so
|
|
# that it can handle merging internally. That is tested here.
|
|
#
|
|
# This is different than storing options
|
|
# in the config file with the same name as the CLI options, which is
|
|
# tested under 'CLI Options Stored in File'
|
|
describe 'reading CLI options' do
|
|
let(:cfg) { Inspec::Config.new(cli_opts) }
|
|
let(:final_options) { cfg.final_options }
|
|
let(:seen_fields) { cfg.final_options.keys.sort }
|
|
|
|
describe 'when the CLI opts are present' do
|
|
let(:cli_opts) do
|
|
{
|
|
color: true,
|
|
'string_key' => 'string_value',
|
|
array_value: [1,2,3],
|
|
}
|
|
end
|
|
|
|
it 'should transparently round-trip the options' do
|
|
expected = ['color', 'array_value', 'reporter', 'string_key', 'type'].sort
|
|
seen_fields.must_equal expected
|
|
final_options[:color].must_equal true
|
|
final_options['color'].must_equal true
|
|
final_options['string_key'].must_equal 'string_value'
|
|
final_options[:string_key].must_equal 'string_value'
|
|
final_options['array_value'].must_equal [1,2,3]
|
|
final_options[:array_value].must_equal [1,2,3]
|
|
end
|
|
end
|
|
end
|
|
|
|
# ========================================================================== #
|
|
# CLI Options Stored in File
|
|
# ========================================================================== #
|
|
describe 'reading CLI options stored in the config file' do
|
|
let(:cfg) { Inspec::Config.new({}, cfg_io) }
|
|
let(:final_options) { cfg.final_options }
|
|
let(:cfg_io) { StringIO.new(ConfigTestHelper.fixture(fixture_name)) }
|
|
let(:seen_fields) { cfg.final_options.keys.sort }
|
|
|
|
# These two test cases have the same options but in different file versions.
|
|
describe 'when the CLI opts are present in a 1.1 file' do
|
|
let(:fixture_name) { :like_legacy }
|
|
it 'should read the options' do
|
|
expected = ['color', 'reporter', 'target_id', 'type'].sort
|
|
seen_fields.must_equal expected
|
|
final_options['color'].must_equal "true" # Dubious - should this be String or TrueClass?
|
|
final_options['target_id'].must_equal 'mynode'
|
|
end
|
|
end
|
|
|
|
describe 'when the CLI opts are present in a legacy file' do
|
|
let(:fixture_name) { :legacy }
|
|
it 'should read the options' do
|
|
expected = ['color', 'reporter', 'target_id', 'type'].sort
|
|
seen_fields.must_equal expected
|
|
final_options['color'].must_equal "true" # Dubious - should this be String or TrueClass?
|
|
final_options['target_id'].must_equal 'mynode'
|
|
end
|
|
end
|
|
end
|
|
|
|
# ========================================================================== #
|
|
# Parsing and Validating Reporters
|
|
# ========================================================================== #
|
|
|
|
# TODO: this should be moved into plugins for the reporters
|
|
describe 'when parsing reporters' do
|
|
let(:cfg) { Inspec::Config.new(cli_opts) }
|
|
let(:seen_reporters) { cfg['reporter'] }
|
|
|
|
describe 'when paring CLI reporter' do
|
|
let(:cli_opts) { { 'reporter' => ['cli'] } }
|
|
it 'parse cli reporters' do
|
|
expected_value = { 'cli' => { 'stdout' => true }}
|
|
seen_reporters.must_equal expected_value
|
|
end
|
|
end
|
|
|
|
describe 'when paring CLI reporter' do
|
|
let(:cli_opts) { { 'reporter' => ['cli'], 'target_id' => '1d3e399f-4d71-4863-ac54-84d437fbc444' } }
|
|
it 'parses cli report and attaches target_id' do
|
|
expected_value = {"cli"=>{"stdout"=>true, "target_id"=>"1d3e399f-4d71-4863-ac54-84d437fbc444"}}
|
|
seen_reporters.must_equal expected_value
|
|
end
|
|
end
|
|
end
|
|
|
|
describe 'when validating reporters' do
|
|
# validate_reporters is private, so we use .send
|
|
let(:cfg) { Inspec::Config.new }
|
|
it 'valid reporter' do
|
|
reporters = { 'json' => { 'stdout' => true } }
|
|
cfg.send(:validate_reporters!, reporters)
|
|
end
|
|
|
|
it 'invalid reporter type' do
|
|
reporters = ['json', 'magenta']
|
|
proc { cfg.send(:validate_reporters!, reporters) }.must_raise NotImplementedError
|
|
end
|
|
|
|
it 'two reporters outputting to stdout' do
|
|
stdout = { 'stdout' => true }
|
|
reporters = { 'json' => stdout, 'cli' => stdout }
|
|
proc { cfg.send(:validate_reporters!, reporters) }.must_raise ArgumentError
|
|
end
|
|
end
|
|
|
|
# ========================================================================== #
|
|
# Miscellaneous Option Finalization
|
|
# ========================================================================== #
|
|
|
|
describe 'option finalization' do
|
|
it 'raises if `--password/--sudo-password` are used without value' do
|
|
# When you invoke `inspec shell --password` (with no value for password,
|
|
# though it is setup to expect a string) Thor will set the key with value -1
|
|
ex = proc { Inspec::Config.new({'sudo_password' => -1}) }.must_raise(ArgumentError)
|
|
ex.message.must_match(/Please provide a value for --sudo-password/)
|
|
end
|
|
|
|
it 'assumes `--sudo` if `--sudo-password` is used without it' do
|
|
cfg = Inspec::Config.new('sudo_password' => 'somepass')
|
|
cfg.key?('sudo').must_equal true
|
|
end
|
|
|
|
it 'calls `Compliance::API.login` if `opts[:compliance] is passed`' do
|
|
InspecPlugins::Compliance::API.expects(:login)
|
|
cfg_io = StringIO.new(ConfigTestHelper.fixture('with_compliance'))
|
|
Inspec::Config.new({ backend: 'mock' }, cfg_io)
|
|
end
|
|
end
|
|
# ========================================================================== #
|
|
# Fetching Credentials
|
|
# ========================================================================== #
|
|
describe 'when fetching creds' do
|
|
let(:cfg) { Inspec::Config.new(cli_opts, cfg_io) }
|
|
let(:cfg_io) { StringIO.new(ConfigTestHelper.fixture(file_fixture_name)) }
|
|
let(:seen_fields) { creds.keys.sort }
|
|
let(:creds) { cfg.unpack_train_credentials }
|
|
|
|
describe 'when generic creds are present on the cli' do
|
|
let(:cfg_io) { nil }
|
|
let(:cli_opts) { { sudo: true, 'shell_command': 'ksh' } }
|
|
it 'should pass the credentials as-is' do
|
|
expected = [:backend, :sudo, :shell_command].sort
|
|
seen_fields.must_equal expected
|
|
creds[:sudo].must_equal true
|
|
creds[:shell_command].must_equal 'ksh'
|
|
creds[:backend].must_equal 'local' # Checking for default
|
|
end
|
|
end
|
|
|
|
describe 'when creds are specified on the CLI with a backend and transport prefixes' do
|
|
let(:cfg_io) { nil }
|
|
let(:cli_opts) { { backend: 'ssh', ssh_host: 'example.com', ssh_key_files: 'mykey' } }
|
|
it 'should read the backend and strip prefixes' do
|
|
expected = [:backend, :host, :key_files].sort
|
|
seen_fields.must_equal expected
|
|
creds[:backend].must_equal 'ssh'
|
|
creds[:host].must_equal 'example.com'
|
|
creds[:key_files].must_equal 'mykey'
|
|
end
|
|
end
|
|
|
|
describe 'when creds are specified with a non-credset target_uri' do
|
|
let(:cfg_io) { nil }
|
|
let(:cli_opts) { { target: 'ssh://bob@somehost' } }
|
|
it 'should unpack the options using the URI parser' do
|
|
expected = [:backend, :host, :user].sort
|
|
seen_fields.must_equal expected
|
|
creds[:backend].must_equal 'ssh'
|
|
creds[:host].must_equal 'somehost'
|
|
creds[:user].must_equal 'bob'
|
|
end
|
|
end
|
|
|
|
describe 'when backcompat creds are specified on the CLI without a transport prefix' do
|
|
let(:cfg_io) { nil }
|
|
let(:cli_opts) { { target: 'ssh://some.host', user: 'bob' } }
|
|
it 'should assign the options correctly' do
|
|
expected = [:backend, :host, :user].sort
|
|
seen_fields.must_equal expected
|
|
creds[:backend].must_equal 'ssh'
|
|
creds[:host].must_equal 'some.host'
|
|
creds[:user].must_equal 'bob'
|
|
end
|
|
end
|
|
end
|
|
|
|
# ========================================================================== #
|
|
# Merging Options
|
|
# ========================================================================== #
|
|
describe 'when merging options' do
|
|
let(:cfg) { Inspec::Config.new(cli_opts, cfg_io, command) }
|
|
let(:cfg_io) { StringIO.new(ConfigTestHelper.fixture(file_fixture_name)) }
|
|
let(:seen_fields) { cfg.final_options.keys.sort }
|
|
let(:command) { nil }
|
|
|
|
describe 'when there is both a default and a config file setting' do
|
|
let(:file_fixture_name) { :override_check }
|
|
let(:cli_opts) { {} }
|
|
it 'the config file setting should prevail' do
|
|
Inspec::Config::Defaults.stubs(:default_for_command).returns('target_id'=> 'value_from_default')
|
|
expected = ['reporter', 'target_id', 'type'].sort
|
|
seen_fields.must_equal expected
|
|
cfg.final_options['target_id'].must_equal 'value_from_config_file'
|
|
cfg.final_options[:target_id].must_equal 'value_from_config_file'
|
|
end
|
|
end
|
|
|
|
describe 'when there is both a default and a CLI option' do
|
|
let(:cli_opts) { { target_id: 'value_from_cli_opts' } }
|
|
let(:cfg_io) { nil }
|
|
it 'the CLI option should prevail' do
|
|
Inspec::Config::Defaults.stubs(:default_for_command).returns('target_id'=> 'value_from_default')
|
|
expected = ['reporter', 'target_id', 'type'].sort
|
|
seen_fields.must_equal expected
|
|
cfg.final_options['target_id'].must_equal 'value_from_cli_opts'
|
|
cfg.final_options[:target_id].must_equal 'value_from_cli_opts'
|
|
end
|
|
end
|
|
|
|
describe 'when there is both a config file setting and a CLI option' do
|
|
let(:file_fixture_name) { :override_check }
|
|
let(:cli_opts) { { target_id: 'value_from_cli_opts' } }
|
|
it 'the CLI option should prevail' do
|
|
expected = ['reporter', 'target_id', 'type'].sort
|
|
seen_fields.must_equal expected
|
|
cfg.final_options['target_id'].must_equal 'value_from_cli_opts'
|
|
cfg.final_options[:target_id].must_equal 'value_from_cli_opts'
|
|
end
|
|
end
|
|
|
|
describe 'specifically check default vs config file override for "reporter" setting' do
|
|
let(:cli_opts) { {} }
|
|
let(:command) { :shell } # shell default is [ :cli ]
|
|
let(:file_fixture_name) { :override_check } # This fixture sets the cfg file contents to request a json reporter
|
|
it 'the config file setting should prevail' do
|
|
expected = ['reporter', 'target_id', 'type'].sort
|
|
seen_fields.must_equal expected
|
|
cfg.final_options['reporter'].must_be_kind_of Hash
|
|
cfg.final_options['reporter'].keys.must_equal ['json']
|
|
cfg.final_options['reporter']['json']['path'].must_equal 'path/from/config/file'
|
|
cfg.final_options[:reporter].must_be_kind_of Hash
|
|
cfg.final_options[:reporter].keys.must_equal ['json']
|
|
cfg.final_options[:reporter]['json']['path'].must_equal 'path/from/config/file'
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
|
|
# ========================================================================== #
|
|
# Test Fixtures
|
|
# ========================================================================== #
|
|
|
|
module ConfigTestHelper
|
|
def fixture(fixture_name)
|
|
case fixture_name.to_sym
|
|
when :legacy
|
|
# TODO - this is dubious, but based on https://www.inspec.io/docs/reference/reporters/#automate-reporter
|
|
# Things that have 'compliance' as a toplevel have also been seen
|
|
<<~EOJ1
|
|
{
|
|
"color": "true",
|
|
"target_id": "mynode",
|
|
"reporter": {
|
|
"automate" : {
|
|
"url" : "https://YOUR_A2_URL/data-collector/v0/",
|
|
"token" : "YOUR_A2_ADMIN_TOKEN"
|
|
}
|
|
}
|
|
}
|
|
EOJ1
|
|
when :basic
|
|
<<~EOJ2
|
|
{
|
|
"version": "1.1",
|
|
"cli_options": {
|
|
"create_lockfile": "false"
|
|
},
|
|
"reporter": {
|
|
"automate" : {
|
|
"url": "http://some.where",
|
|
"token" : "YOUR_A2_ADMIN_TOKEN"
|
|
}
|
|
}
|
|
}
|
|
EOJ2
|
|
when :like_legacy
|
|
<<~EOJ3
|
|
{
|
|
"version": "1.1",
|
|
"cli_options": {
|
|
"color": "true",
|
|
"target_id": "mynode"
|
|
},
|
|
"reporter": {
|
|
"automate" : {
|
|
"url" : "https://YOUR_A2_URL/data-collector/v0/",
|
|
"token" : "YOUR_A2_ADMIN_TOKEN"
|
|
}
|
|
}
|
|
}
|
|
EOJ3
|
|
when :override_check
|
|
<<~EOJ4
|
|
{
|
|
"version": "1.1",
|
|
"cli_options": {
|
|
"target_id": "value_from_config_file"
|
|
},
|
|
"reporter": {
|
|
"json": {
|
|
"path": "path/from/config/file"
|
|
}
|
|
}
|
|
}
|
|
EOJ4
|
|
when :minimal
|
|
'{ "version": "1.1" }'
|
|
when :bad_version
|
|
'{ "version": "99.99" }'
|
|
when :bad_top_level
|
|
'{ "version": "1.1", "unsupported_field": "some_value" }'
|
|
when :malformed_json
|
|
'{ "hot_garbage": "a", "version": "1.1", '
|
|
when :with_compliance
|
|
# TODO - this is dubious, need to verify
|
|
<<~EOJ5
|
|
{
|
|
"compliance": {
|
|
"server":"https://some.host",
|
|
"user":"someuser"
|
|
}
|
|
}
|
|
EOJ5
|
|
end
|
|
end
|
|
module_function :fixture
|
|
end
|