require "helper" require "inspec/resource" require "resources/aws/aws_iam_policy" require "resource_support/aws" require "resources/aws/aws_iam_policy" # MAIPSB = MockAwsIamPolicySingularBackend # Abbreviation not used outside this file #=============================================================================# # Constructor Tests #=============================================================================# class AwsIamPolicyConstructorTest < Minitest::Test def setup AwsIamPolicy::BackendFactory.select(MAIPSB::Empty) end def test_rejects_empty_params assert_raises(ArgumentError) { AwsIamPolicy.new } end def test_accepts_policy_name_as_scalar AwsIamPolicy.new("test-policy-1") end def test_accepts_policy_name_as_hash AwsIamPolicy.new(policy_name: "test-policy-1") end def test_rejects_unrecognized_params assert_raises(ArgumentError) { AwsIamPolicy.new(shoe_size: 9) } end end #=============================================================================# # Search / Recall #=============================================================================# class AwsIamPolicyRecallTest < Minitest::Test def setup AwsIamPolicy::BackendFactory.select(MAIPSB::Basic) end def test_search_hit_via_scalar_works assert AwsIamPolicy.new("test-policy-1").exists? end def test_search_hit_via_hash_works assert AwsIamPolicy.new(policy_name: "test-policy-1").exists? end def test_search_miss_is_not_an_exception refute AwsIamPolicy.new(policy_name: "non-existant").exists? end end #=============================================================================# # Properties #=============================================================================# class AwsIamPolicyPropertiesTest < Minitest::Test def setup AwsIamPolicy::BackendFactory.select(MAIPSB::Basic) end def test_property_arn assert_equal("arn:aws:iam::aws:policy/test-policy-1", AwsIamPolicy.new("test-policy-1").arn) assert_nil(AwsIamPolicy.new(policy_name: "non-existant").arn) end def test_property_default_version_id assert_equal("v1", AwsIamPolicy.new("test-policy-1").default_version_id) assert_nil(AwsIamPolicy.new(policy_name: "non-existant").default_version_id) end def test_property_attachment_count assert_equal(3, AwsIamPolicy.new("test-policy-1").attachment_count) assert_nil(AwsIamPolicy.new(policy_name: "non-existant").attachment_count) end def test_property_attached_users assert_equal(["test-user"], AwsIamPolicy.new("test-policy-1").attached_users) assert_nil(AwsIamPolicy.new(policy_name: "non-existant").attached_users) end def test_property_attached_groups assert_equal(["test-group"], AwsIamPolicy.new("test-policy-1").attached_groups) assert_nil(AwsIamPolicy.new(policy_name: "non-existant").attached_groups) end def test_property_attached_roles assert_equal(["test-role"], AwsIamPolicy.new("test-policy-1").attached_roles) assert_nil(AwsIamPolicy.new(policy_name: "non-existant").attached_roles) end def test_property_policy policy = AwsIamPolicy.new("test-policy-1").policy assert_kind_of(Hash, policy) assert(policy.key?("Statement"), "test-policy-1 should have a Statement key when unpacked") assert_equal(1, policy["Statement"].count, "test-policy-1 should have 1 statements when unpacked") assert_nil(AwsIamPolicy.new("non-existant").policy) end def test_property_statement_count assert_nil(AwsIamPolicy.new("non-existant").statement_count) assert_equal(1, AwsIamPolicy.new("test-policy-1").statement_count) assert_equal(2, AwsIamPolicy.new("test-policy-2").statement_count) assert_equal(1, AwsIamPolicy.new("test-policy-3").statement_count) end end #=============================================================================# # Matchers #=============================================================================# class AwsIamPolicyMatchersTest < Minitest::Test def setup AwsIamPolicy::BackendFactory.select(MAIPSB::Basic) end def test_matcher_attached_positive assert AwsIamPolicy.new("test-policy-1").attached? end def test_matcher_attached_negative refute AwsIamPolicy.new("test-policy-2").attached? end def test_matcher_attached_to_user_positive assert AwsIamPolicy.new("test-policy-1").attached_to_user?("test-user") end def test_matcher_attached_to_user_negative refute AwsIamPolicy.new("test-policy-2").attached_to_user?("test-user") end def test_matcher_attached_to_group_positive assert AwsIamPolicy.new("test-policy-1").attached_to_group?("test-group") end def test_matcher_attached_to_group_negative refute AwsIamPolicy.new("test-policy-2").attached_to_group?("test-group") end def test_matcher_attached_to_role_positive assert AwsIamPolicy.new("test-policy-1").attached_to_role?("test-role") end def test_matcher_attached_to_role_negative refute AwsIamPolicy.new("test-policy-2").attached_to_role?("test-role") end def test_have_statement_when_policy_does_not_exist assert_nil AwsIamPolicy.new("nonesuch").has_statement?("Effect" => "foo") end def test_have_statement_when_provided_no_criteria AwsIamPolicy.new("test-policy-1").has_statement? end def test_have_statement_when_provided_acceptable_criteria { "Action" => "dummy", "Effect" => "Deny", # This has restictions on the value provided "Resource" => "dummy", "Sid" => "dummy", }.each do |criterion, test_value| [ criterion, criterion.downcase, criterion.to_sym, criterion.downcase.to_sym ].each do |variant| AwsIamPolicy.new("test-policy-1").has_statement?(variant => test_value) end end end def test_have_statement_when_provided_unimplemented_criteria %w{ Conditional NotAction NotPrincipal NotResource Principal }.each do |criterion| ex = assert_raises(ArgumentError) { AwsIamPolicy.new("test-policy-1").has_statement?(criterion => "dummy") } assert_match(/not supported/, ex.message) end end def test_have_statement_when_provided_unrecognized_criteria ex = assert_raises(ArgumentError) { AwsIamPolicy.new("test-policy-1").has_statement?("foo" => "dummy") } assert_match(/Unrecognized/, ex.message) end def test_have_statement_when_sid_is_provided ["Sid", "sid", :Sid, :sid].each do |variant| assert(AwsIamPolicy.new("test-policy-1").has_statement?(variant => "beta01")) assert(AwsIamPolicy.new("test-policy-2").has_statement?(variant => "CloudWatchEventsFullAccess")) assert(AwsIamPolicy.new("test-policy-2").has_statement?(variant => "IAMPassRoleForCloudWatchEvents")) refute(AwsIamPolicy.new("test-policy-2").has_statement?(variant => "beta01")) assert(AwsIamPolicy.new("test-policy-1").has_statement?(variant => /eta/)) assert(AwsIamPolicy.new("test-policy-2").has_statement?(variant => /CloudWatch/)) refute(AwsIamPolicy.new("test-policy-2").has_statement?(variant => /eta/)) end end def test_have_statement_when_effect_is_provided ["Effect", "effect", :Effect, :effect].each do |variant| assert(AwsIamPolicy.new("test-policy-1").has_statement?(variant => "Deny")) refute(AwsIamPolicy.new("test-policy-1").has_statement?(variant => "Allow")) assert(AwsIamPolicy.new("test-policy-2").has_statement?(variant => "Allow")) assert_raises(ArgumentError) { AwsIamPolicy.new("test-policy-1").has_statement?(variant => "Disallow") } assert_raises(ArgumentError) { AwsIamPolicy.new("test-policy-1").has_statement?(variant => "allow") } assert_raises(ArgumentError) { AwsIamPolicy.new("test-policy-1").has_statement?(variant => :Allow) } assert_raises(ArgumentError) { AwsIamPolicy.new("test-policy-1").has_statement?(variant => :allow) } end end def test_have_statement_when_action_is_provided ["Action", "action", :Action, :action].each do |variant| # Able to match a simple string action when multiple statements present assert(AwsIamPolicy.new("test-policy-2").has_statement?(variant => "iam:PassRole")) # Able to match a wildcard string action assert(AwsIamPolicy.new("test-policy-2").has_statement?(variant => "events:*")) # Do not match a wildcard when using strings refute(AwsIamPolicy.new("test-policy-2").has_statement?(variant => "events:EnableRule")) # Do match when using a regex assert(AwsIamPolicy.new("test-policy-2").has_statement?(variant => /^events\:/)) # Able to match one action when the statement has an array of actions assert(AwsIamPolicy.new("test-policy-1").has_statement?(variant => "ec2:DescribeSubnets")) # Do not match if only one action specified as an array when the statement has an array of actions refute(AwsIamPolicy.new("test-policy-1").has_statement?(variant => ["ec2:DescribeSubnets"])) # Do match if two actions specified when the statement has an array of actions assert(AwsIamPolicy.new("test-policy-1").has_statement?(variant => ["ec2:DescribeSubnets", "ec2:DescribeSecurityGroups"])) # Do match setwise if two actions specified when the statement has an array of actions assert(AwsIamPolicy.new("test-policy-1").has_statement?(variant => ["ec2:DescribeSecurityGroups", "ec2:DescribeSubnets"])) # Do match if only one regex action specified when the statement has an array of actions assert(AwsIamPolicy.new("test-policy-1").has_statement?(variant => /^ec2\:Describe/)) # Do match if one regex action specified in an array when the statement has an array of actions assert(AwsIamPolicy.new("test-policy-1").has_statement?(variant => [/^ec2\:Describe/])) # Able to match a degenerate policy doc in which there is exactly one statement as a hash. assert(AwsIamPolicy.new("test-policy-3").has_statement?(variant => "acm:GetCertificate")) # Don't explode, and also don't match, if a policy has a statement without an Action refute(AwsIamPolicy.new("test-policy-4").has_statement?(variant => "iam:*")) end end def test_have_statement_when_resource_is_provided ["Resource", "resource", :Resource, :resource].each do |variant| # Able to match a simple string resource when multiple statements present assert(AwsIamPolicy.new("test-policy-2").has_statement?(variant => "arn:aws:iam::*:role/AWS_Events_Invoke_Targets")) # Able to match a wildcard string resource assert(AwsIamPolicy.new("test-policy-2").has_statement?(variant => "*")) # Do not match a wildcard when using strings refute(AwsIamPolicy.new("test-policy-2").has_statement?(variant => "arn:aws:events:us-east-1:123456789012:rule/my-rule")) # Do match when using a regex assert(AwsIamPolicy.new("test-policy-2").has_statement?(variant => /AWS_Events_Invoke_Targets$/)) # Able to match one resource when the statement has an array of resources assert(AwsIamPolicy.new("test-policy-1").has_statement?(variant => "arn:aws:ec2:::*")) # Do not match if only one resource specified as an array when the statement has an array of resources refute(AwsIamPolicy.new("test-policy-1").has_statement?(variant => ["arn:aws:ec2:::*"])) # Do match if two resources specified when the statement has an array of resources assert(AwsIamPolicy.new("test-policy-1").has_statement?(variant => ["arn:aws:ec2:::*", "*"])) # Do match setwise if two resources specified when the statement has an array of resources assert(AwsIamPolicy.new("test-policy-1").has_statement?(variant => ["*", "arn:aws:ec2:::*"])) # Do match if only one regex resource specified when the statement has an array of resources assert(AwsIamPolicy.new("test-policy-1").has_statement?(variant => /^arn\:aws\:ec2/)) # Do match if one regex resource specified in an array when the statement has an array of resources assert(AwsIamPolicy.new("test-policy-1").has_statement?(variant => [/\*/])) # Able to match a degenerate policy doc in which there is exactly one statement as a hash. assert(AwsIamPolicy.new("test-policy-3").has_statement?(variant => "*")) end end end #=============================================================================# # Test Fixtures #=============================================================================# module MAIPSB class Empty < AwsBackendBase def list_policies(query) OpenStruct.new(policies: []) end end class Basic < AwsBackendBase def list_policies(query) fixtures = [ OpenStruct.new({ policy_name: "test-policy-1", arn: "arn:aws:iam::aws:policy/test-policy-1", default_version_id: "v1", attachment_count: 3, is_attachable: true, }), OpenStruct.new({ policy_name: "test-policy-2", arn: "arn:aws:iam::aws:policy/test-policy-2", default_version_id: "v1", attachment_count: 0, is_attachable: false, }), OpenStruct.new({ policy_name: "test-policy-3", arn: "arn:aws:iam::aws:policy/test-policy-3", default_version_id: "v1", attachment_count: 0, is_attachable: true, }), OpenStruct.new({ policy_name: "test-policy-4", arn: "arn:aws:iam::aws:policy/test-policy-4", default_version_id: "v1", attachment_count: 0, is_attachable: false, }), ] OpenStruct.new({ policies: fixtures }) end def list_entities_for_policy(query) policy = {} policy["arn:aws:iam::aws:policy/test-policy-1"] = { policy_groups: [ OpenStruct.new({ group_name: "test-group", group_id: "AIDAIJ3FUBXLZ4VXV34LE", }), ], policy_users: [ OpenStruct.new({ user_name: "test-user", user_id: "AIDAIJ3FUBXLZ4VXV34LE", }), ], policy_roles: [ OpenStruct.new({ role_name: "test-role", role_id: "AIDAIJ3FUBXLZ4VXV34LE", }), ], } policy["arn:aws:iam::aws:policy/test-policy-2"] = { policy_groups: [], policy_users: [], policy_roles: [], } OpenStruct.new( policy[query[:policy_arn]] ) end def get_policy_version(query) fixtures = { "arn:aws:iam::aws:policy/test-policy-1" => { "v1" => OpenStruct.new( # This is the integration test fixture "beta" # { # "Version"=>"2012-10-17", # "Statement"=> [ # { # "Sid"=>"beta01", # "Action"=>["ec2:DescribeSubnets", "ec2:DescribeSecurityGroups"], # "Effect"=>"Deny", # "Resource"=>["arn:aws:ec2:::*", "*"] # } # ] # } document: "%7B%0A%20%20%22Version%22%3A%20%222012-10-17%22%2C%0A%20%20%22Statement%22%3A%20%5B%0A%20%20%20%20%7B%0A%20%20%20%20%20%20%22Sid%22%3A%20%22beta01%22%2C%0A%20%20%20%20%20%20%22Action%22%3A%20%5B%0A%20%20%20%20%20%20%20%20%22ec2%3ADescribeSubnets%22%2C%0A%20%20%20%20%20%20%20%20%22ec2%3ADescribeSecurityGroups%22%0A%20%20%20%20%20%20%5D%2C%0A%20%20%20%20%20%20%22Effect%22%3A%20%22Deny%22%2C%0A%20%20%20%20%20%20%22Resource%22%3A%20%5B%0A%20%20%20%20%20%20%20%20%22arn%3Aaws%3Aec2%3A%3A%3A%2A%22%2C%0A%20%20%20%20%20%20%20%20%22%2A%22%0A%20%20%20%20%20%20%5D%0A%20%20%20%20%7D%0A%20%20%5D%0A%7D%0A" ), }, "arn:aws:iam::aws:policy/test-policy-2" => { "v1" => OpenStruct.new( # This is AWS-managed CloudWatchEventsFullAccess # { # "Version"=>"2012-10-17", # "Statement"=> [ # { # "Sid"=>"CloudWatchEventsFullAccess", # "Effect"=>"Allow", # "Action"=>"events:*", # "Resource"=>"*" # }, # { # "Sid"=>"IAMPassRoleForCloudWatchEvents", # "Effect"=>"Allow", # "Action"=>"iam:PassRole", # "Resource"=>"arn:aws:iam::*:role/AWS_Events_Invoke_Targets" # } # ] # } document: "%7B%0A%20%20%20%20%22Version%22%3A%20%222012-10-17%22%2C%0A%20%20%20%20%22Statement%22%3A%20%5B%0A%20%20%20%20%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%22Sid%22%3A%20%22CloudWatchEventsFullAccess%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%22Effect%22%3A%20%22Allow%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%22Action%22%3A%20%22events%3A%2A%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%22Resource%22%3A%20%22%2A%22%0A%20%20%20%20%20%20%20%20%7D%2C%0A%20%20%20%20%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%22Sid%22%3A%20%22IAMPassRoleForCloudWatchEvents%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%22Effect%22%3A%20%22Allow%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%22Action%22%3A%20%22iam%3APassRole%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%22Resource%22%3A%20%22arn%3Aaws%3Aiam%3A%3A%2A%3Arole%2FAWS_Events_Invoke_Targets%22%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%5D%0A%7D" ), }, "arn:aws:iam::aws:policy/test-policy-3" => { "v1" => OpenStruct.new( # This is AWS-managed AWSCertificateManagerReadOnly # { # "Version": "2012-10-17", # "Statement": { # "Effect": "Allow", # "Action": [ # "acm:DescribeCertificate", # "acm:ListCertificates", # "acm:GetCertificate", # "acm:ListTagsForCertificate" # ], # "Resource": "*" # } # } document: "%7B%0A%20%20%20%20%22Version%22%3A%20%222012-10-17%22%2C%0A%20%20%20%20%22Statement%22%3A%20%7B%0A%20%20%20%20%20%20%20%20%22Effect%22%3A%20%22Allow%22%2C%0A%20%20%20%20%20%20%20%20%22Action%22%3A%20%5B%0A%20%20%20%20%20%20%20%20%20%20%20%20%22acm%3ADescribeCertificate%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%22acm%3AListCertificates%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%22acm%3AGetCertificate%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%22acm%3AListTagsForCertificate%22%0A%20%20%20%20%20%20%20%20%5D%2C%0A%20%20%20%20%20%20%20%20%22Resource%22%3A%20%22%2A%22%0A%20%20%20%20%7D%0A%7D" ), }, "arn:aws:iam::aws:policy/test-policy-4" => { "v1" => OpenStruct.new( # This is arn:aws:iam::aws:policy/PowerUserAccess # { # "Version": "2012-10-17", # "Statement": [ # { # "Effect": "Allow", # "NotAction": [ # "iam:*", # "organizations:*" # ], # "Resource": "*" # }, # { # "Effect": "Allow", # "Action": [ # "iam:CreateServiceLinkedRole", # "iam:DeleteServiceLinkedRole", # "iam:ListRoles", # "organizations:DescribeOrganization" # ], # "Resource": "*" # } # ] # } document: "%7B%0A%20%20%22Version%22%3A%20%222012-10-17%22%2C%0A%20%20%22Statement%22%3A%20%5B%0A%20%20%20%20%7B%0A%20%20%20%20%20%20%22Effect%22%3A%20%22Allow%22%2C%0A%20%20%20%20%20%20%22NotAction%22%3A%20%5B%22iam%3A%2A%22%2C%20%22organizations%3A%2A%22%5D%2C%0A%20%20%20%20%20%20%22Resource%22%3A%20%22%2A%22%0A%20%20%20%20%7D%2C%7B%0A%20%20%20%20%20%20%22Effect%22%3A%20%22Allow%22%2C%0A%20%20%20%20%20%20%22Action%22%3A%20%5B%0A%20%20%20%20%20%20%20%20%20%20%22iam%3ACreateServiceLinkedRole%22%2C%0A%20%20%20%20%20%20%20%20%20%20%22iam%3ADeleteServiceLinkedRole%22%2C%0A%20%20%20%20%20%20%20%20%20%20%22iam%3AListRoles%22%2C%0A%20%20%20%20%20%20%20%20%20%20%22organizations%3ADescribeOrganization%22%0A%20%20%20%20%20%20%5D%2C%0A%20%20%20%20%20%20%22Resource%22%3A%20%22%2A%22%0A%20%20%20%20%7D%0A%20%20%5D%0A%7D" ), }, } pv = fixtures.dig(query[:policy_arn], query[:version_id]) return OpenStruct.new(policy_version: pv) if pv raise Aws::IAM::Errors::NoSuchEntity.new(nil, nil) end end end