diff --git a/docs/resources/aws_iam_password_policy.md b/docs/resources/aws_iam_password_policy.md index 1ace36c46..b326591b5 100644 --- a/docs/resources/aws_iam_password_policy.md +++ b/docs/resources/aws_iam_password_policy.md @@ -11,17 +11,17 @@ Use the `aws_iam_password_policy` InSpec audit resource to test properties of th ## Syntax -An `aws_iam_password_policy` resource block takes no parameters, but uses several matchers. +An `aws_iam_password_policy` resource block takes no parameters. Several properties and matchers are available. describe aws_iam_password_policy do - its('requires_lowercase_characters?') { should be true } + it { should require_lowercase_characters } end
## Properties -* `allows_users_to_change_password?`, `expires_passwords`, `max_password_age`, `minimum_password_length`, `number_of_passwords_to_remember`, `prevents_password_reuse?`, `requires_lowercase_characters` , `requires_uppercase_characters?`, `requires_numbers?`, `requires_symbols?` +* `max_password_age_in_days`, `minimum_password_length`, `number_of_passwords_to_remember` ## Examples @@ -30,35 +30,35 @@ The following examples show how to use this InSpec audit resource. ### Test that the IAM Password Policy requires lowercase characters, uppercase characters, numbers, symbols, and a minimum length greater than eight describe aws_iam_password_policy do - its('requires_lowercase_characters?') { should be true } - its('requires_uppercase_characters?') { should be true } - its('requires_numbers?') { should be true } - its('requires_symbols?') { should be true } + it { should require_lowercase_characters } + it { should require_uppercase_characters } + it { should require_symbols } + it { should require_numbers } its('minimum_password_length') { should be > 8 } end ### Test that the IAM Password Policy allows users to change their password describe aws_iam_password_policy do - its('allows_user_to_change_password?') { should be true } + it { should allow_users_to_change_passwords } end ### Test that the IAM Password Policy expires passwords describe aws_iam_password_policy do - its('expires_passwords?') { should be true } + it { should expire_passwords } end ### Test that the IAM Password Policy has a max password age describe aws_iam_password_policy do - its('max_password_age') { should be > 90 * 86400 } + its('max_password_age_in_days') { should be 90 } end ### Test that the IAM Password Policy prevents password reuse describe aws_iam_password_policy do - its('prevents_password_reuse?') { should be true } + it { should prevent_password_reuse } end ### Test that the IAM Password Policy requires users to remember 3 previous passwords @@ -71,4 +71,4 @@ The following examples show how to use this InSpec audit resource. ## Matchers -For a full list of available matchers please visit our [matchers page](https://www.inspec.io/docs/reference/matchers/). +* `allows_users_to_change_passwords`, `expire_passwords`, `prevent_password_reuse`, `require_lowercase_characters` , `require_uppercase_characters`, `require_numbers`, `require_symbols` diff --git a/lib/resources/aws/aws_iam_password_policy.rb b/lib/resources/aws/aws_iam_password_policy.rb index 700819553..f781b6ae8 100644 --- a/lib/resources/aws/aws_iam_password_policy.rb +++ b/lib/resources/aws/aws_iam_password_policy.rb @@ -17,11 +17,20 @@ EOX # TODO: rewrite to avoid direct injection, match other resources, use AwsSingularResourceMixin def initialize(conn = nil) catch_aws_errors do - iam_resource = conn ? conn.iam_resource : inspec_runner.backend.aws_resource(Aws::IAM::Resource, {}) - @policy = iam_resource.account_password_policy + begin + if conn + # We're in a mocked unit test. + @policy = conn.iam_resource.account_password_policy + else + # Don't use the resource approach. It's a CRUD operation + # - if the policy does not exist, you get back a blank object to populate and save. + # Using the Client will throw an exception if no policy exists. + @policy = inspec_runner.backend.aws_client(Aws::IAM::Client).get_account_password_policy.password_policy + end + rescue Aws::IAM::Errors::NoSuchEntity + @policy = nil + end end - rescue Aws::IAM::Errors::NoSuchEntity - @policy = nil end # TODO: DRY up, see https://github.com/chef/inspec/issues/2633 @@ -49,54 +58,59 @@ EOX inspec if respond_to?(:inspec) end + def to_s + 'IAM Password-Policy' + end + def exists? !@policy.nil? end - def requires_lowercase_characters? - @policy.require_lowercase_characters - end - - def requires_uppercase_characters? - @policy.require_uppercase_characters - end + #-------------------------- Properties ----------------------------# def minimum_password_length @policy.minimum_password_length end - def requires_numbers? - @policy.require_numbers - end - - def requires_symbols? - @policy.require_symbols - end - - def allows_users_to_change_password? - @policy.allow_users_to_change_password - end - - def expires_passwords? - @policy.expire_passwords - end - - def max_password_age - raise 'this policy does not expire passwords' unless expires_passwords? + def max_password_age_in_days + raise 'this policy does not expire passwords' unless expire_passwords? @policy.max_password_age end - def prevents_password_reuse? - !@policy.password_reuse_prevention.nil? - end - def number_of_passwords_to_remember raise 'this policy does not prevent password reuse' \ - unless prevents_password_reuse? + unless prevent_password_reuse? @policy.password_reuse_prevention end - def to_s - 'IAM Password-Policy' + #-------------------------- Matchers ----------------------------# + [ + :require_lowercase_characters, + :require_uppercase_characters, + :require_symbols, + :require_numbers, + :expire_passwords, + ].each do |matcher_stem| + # Create our predicates (for example, 'require_symbols?') + stem_with_question_mark = (matcher_stem.to_s + '?').to_sym + define_method stem_with_question_mark do + @policy.send(matcher_stem) + end + # RSpec will expose that as (for example) `be_require_symbols`. + # To undo that, we have to make a matcher alias. + stem_with_be = ('be_' + matcher_stem.to_s).to_sym + RSpec::Matchers.alias_matcher matcher_stem, stem_with_be end + + # This one has an awkward name mapping + def allow_users_to_change_passwords? + @policy.allow_users_to_change_password + end + RSpec::Matchers.alias_matcher :allow_users_to_change_passwords, :be_allow_users_to_change_passwords + + # This one has custom logic and renaming + def prevent_password_reuse? + !@policy.password_reuse_prevention.nil? + end + RSpec::Matchers.alias_matcher :prevent_password_reuse, :be_prevent_password_reuse end diff --git a/test/aws/default/build/iam.tf b/test/aws/default/build/iam.tf index 8bf5c978f..5c34f6134 100644 --- a/test/aws/default/build/iam.tf +++ b/test/aws/default/build/iam.tf @@ -6,6 +6,21 @@ variable "login_profile_pgp_key" { default = "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" } +#======================================================# +# Accoount Password Policy +#======================================================# +# Only one of these is allowed +resource "aws_iam_account_password_policy" "fixture" { + minimum_password_length = 10 + require_lowercase_characters = true + require_numbers = true + require_uppercase_characters = true + require_symbols = true + allow_users_to_change_password = true + max_password_age = 365 + password_reuse_prevention = 7 +} + #======================================================# # IAM Users #======================================================# diff --git a/test/aws/default/verify/controls/aws_iam_password_policy.rb b/test/aws/default/verify/controls/aws_iam_password_policy.rb new file mode 100644 index 000000000..ef6fd46ab --- /dev/null +++ b/test/aws/default/verify/controls/aws_iam_password_policy.rb @@ -0,0 +1,33 @@ +# There are other tests in the "minimal" test account. + +#---------------------- Recall ------------------------# +# Password policy is a per-account singleton. If it's been configured, it exists. +control "aws_iam_password_policy existence" do + describe aws_iam_password_policy do + it { should exist } + end +end + +#------------- Properties -------------# + +control "aws_iam_password_policy properties" do + describe aws_iam_password_policy do + its('max_password_age_in_days') { should cmp 365 } + its('number_of_passwords_to_remember') { should cmp 7 } + end +end + +#------------- Matchers - Positive Case -------------# + +control "aws_iam_password_policy matchers" do + describe aws_iam_password_policy do + it { should require_lowercase_characters } + it { should require_uppercase_characters } + it { should require_numbers } + it { should require_symbols } + it { should allow_users_to_change_passwords } + it { should expire_passwords } + it { should prevent_password_reuse } + end +end + diff --git a/test/aws/minimal/verify/controls/aws_iam_password_policy.rb b/test/aws/minimal/verify/controls/aws_iam_password_policy.rb new file mode 100644 index 000000000..00f3d7bf5 --- /dev/null +++ b/test/aws/minimal/verify/controls/aws_iam_password_policy.rb @@ -0,0 +1,14 @@ + +#---------------------- Recall ------------------------# +# Password policy is a per-account singleton. If it's been configured, it exists. +control "aws_iam_password_policy properties" do + describe aws_iam_password_policy do + it { should_not exist } + end +end + +#------------- Properties - Negative Case -------------# +# No negative tests yet - we'd need a third account + +#------------- Matchers - Negative Case -------------# +# No negative tests yet - we'd need a third account \ No newline at end of file diff --git a/test/unit/resources/aws_iam_password_policy_test.rb b/test/unit/resources/aws_iam_password_policy_test.rb index a19908ee7..1dcf25245 100644 --- a/test/unit/resources/aws_iam_password_policy_test.rb +++ b/test/unit/resources/aws_iam_password_policy_test.rb @@ -30,7 +30,7 @@ class AwsIamPasswordPolicyTest < Minitest::Test @mock_resource.expect :account_password_policy, @mock_policy e = assert_raises Exception do - AwsIamPasswordPolicy.new(@mock_conn).max_password_age + AwsIamPasswordPolicy.new(@mock_conn).max_password_age_in_days end assert_equal e.message, 'this policy does not expire passwords' @@ -39,13 +39,13 @@ class AwsIamPasswordPolicyTest < Minitest::Test def test_prevents_password_reuse_returns_true_when_not_nil configure_policy_password_reuse_prevention(value: Object.new) - assert AwsIamPasswordPolicy.new(@mock_conn).prevents_password_reuse? + assert AwsIamPasswordPolicy.new(@mock_conn).prevent_password_reuse? end def test_prevents_password_reuse_returns_false_when_nil configure_policy_password_reuse_prevention(value: nil) - refute AwsIamPasswordPolicy.new(@mock_conn).prevents_password_reuse? + refute AwsIamPasswordPolicy.new(@mock_conn).prevent_password_reuse? end def test_number_of_passwords_to_remember_throws_when_nil