mirror of
https://github.com/inspec/inspec
synced 2024-11-10 07:04:15 +00:00
Accept symbols and downcased criteria in aws_iam_policy have_statement matcher (#3129)
Signed-off-by: Clinton Wolfe <clintoncwolfe@gmail.com>
This commit is contained in:
parent
03b6dd8324
commit
44c0fd2e4f
3 changed files with 111 additions and 82 deletions
|
@ -186,7 +186,7 @@ The test will pass if the identified policy attached the specified role.
|
|||
|
||||
Examines the list of statements contained in the policy and passes if at least one of the statements matches. This matcher does _not_ interpret the policy in a request authorization context, as AWS does when a request processed. Rather, `have_statement` examines the literal contents of the IAM policy, and reports on what is present (or absent, when used with `should_not`).
|
||||
|
||||
`have_statement` accepts the following criteria to search for matching statements. If any statement matches all the criteria, the test is successful.
|
||||
`have_statement` accepts the following criteria to search for matching statements. If any statement matches all the criteria, the test is successful. All criteria may be used as Titlecase (as in the AWS examples) or lowercase, string or symbol.
|
||||
|
||||
* `Action` - Expresses the requested operation. Acceptable literal values are any AWS operation name, including the '*' wildcard character. `Action` may also use a list of AWS operation names.
|
||||
* `Effect` - Expresses if the operation is permitted. Acceptable values are 'Deny' and 'Allow'.
|
||||
|
@ -204,7 +204,16 @@ Examples:
|
|||
|
||||
# Verify there is no full-admin statement
|
||||
describe aws_iam_policy('kryptonite') do
|
||||
it { should_not have_statement('Effect' => 'Allow', 'Resource' => '*', 'Action' => '*')}
|
||||
end
|
||||
|
||||
# Symbols and lowercase also allowed as criteria
|
||||
describe aws_iam_policy('kryptonite') do
|
||||
# All 4 the same
|
||||
it { should_not have_statement('Effect' => 'Allow', 'Resource' => '*', 'Action' => '*')}
|
||||
it { should_not have_statement('effect' => 'Allow', 'resource' => '*', 'action' => '*')}
|
||||
it { should_not have_statement(Effect: 'Allow', Resource: '*', Action: '*')}
|
||||
it { should_not have_statement(effect: 'Allow', resource: '*', action: '*')}
|
||||
end
|
||||
|
||||
# Verify bob is allowed to manage things on S3 buckets that start with bobs-stuff
|
||||
|
|
|
@ -16,6 +16,7 @@ class AwsIamPolicy < Inspec.resource(1)
|
|||
|
||||
attr_reader :arn, :attachment_count, :default_version_id
|
||||
|
||||
# Note that we also accept downcases and symbol versions of these
|
||||
EXPECTED_CRITERIA = %w{
|
||||
Action
|
||||
Effect
|
||||
|
@ -96,7 +97,7 @@ class AwsIamPolicy < Inspec.resource(1)
|
|||
def has_statement?(provided_criteria = {})
|
||||
return nil unless exists?
|
||||
raw_criteria = provided_criteria.dup # provided_criteria is used for output formatting - can't delete from it.
|
||||
criteria = has_statement__normalize_criteria(has_statement__validate_criteria(raw_criteria))
|
||||
criteria = has_statement__validate_criteria(raw_criteria)
|
||||
@normalized_statements ||= has_statement__normalize_statements
|
||||
statements = has_statement__focus_on_sid(@normalized_statements, criteria)
|
||||
statements.any? do |statement|
|
||||
|
@ -112,15 +113,30 @@ class AwsIamPolicy < Inspec.resource(1)
|
|||
def has_statement__validate_criteria(raw_criteria)
|
||||
recognized_criteria = {}
|
||||
EXPECTED_CRITERIA.each do |expected_criterion|
|
||||
if raw_criteria.key?(expected_criterion)
|
||||
recognized_criteria[expected_criterion] = raw_criteria.delete(expected_criterion)
|
||||
[
|
||||
expected_criterion,
|
||||
expected_criterion.downcase,
|
||||
expected_criterion.to_sym,
|
||||
expected_criterion.downcase.to_sym,
|
||||
].each do |variant|
|
||||
if raw_criteria.key?(variant)
|
||||
# Always store as downcased symbol
|
||||
recognized_criteria[expected_criterion.downcase.to_sym] = raw_criteria.delete(variant)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Special message for valid, but unimplemented statement attributes
|
||||
UNIMPLEMENTED_CRITERIA.each do |unimplemented_criterion|
|
||||
if raw_criteria.key?(unimplemented_criterion)
|
||||
raise ArgumentError, "Criterion '#{unimplemented_criterion}' is not supported for performing have_statement queries."
|
||||
[
|
||||
unimplemented_criterion,
|
||||
unimplemented_criterion.downcase,
|
||||
unimplemented_criterion.to_sym,
|
||||
unimplemented_criterion.downcase.to_sym,
|
||||
].each do |variant|
|
||||
if raw_criteria.key?(variant)
|
||||
raise ArgumentError, "Criterion '#{unimplemented_criterion}' is not supported for performing have_statement queries."
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -130,24 +146,15 @@ class AwsIamPolicy < Inspec.resource(1)
|
|||
end
|
||||
|
||||
# Effect has only 2 permitted values
|
||||
if recognized_criteria.key?('Effect')
|
||||
unless %w{Allow Deny}.include?(recognized_criteria['Effect'])
|
||||
raise ArgumentError, "Criterion 'Effect' for have_statement must be one of 'Allow' or 'Deny' - got '#{recognized_criteria['Effect']}'"
|
||||
if recognized_criteria.key?(:effect)
|
||||
unless %w{Allow Deny}.include?(recognized_criteria[:effect])
|
||||
raise ArgumentError, "Criterion 'Effect' for have_statement must be one of 'Allow' or 'Deny' - got '#{recognized_criteria[:effect]}'"
|
||||
end
|
||||
end
|
||||
|
||||
recognized_criteria
|
||||
end
|
||||
|
||||
def has_statement__normalize_criteria(criteria)
|
||||
# Transform keys into lowercase symbols
|
||||
criteria.keys.each do |provided_key|
|
||||
criteria[provided_key.downcase.to_sym] = criteria.delete(provided_key)
|
||||
end
|
||||
|
||||
criteria
|
||||
end
|
||||
|
||||
def has_statement__normalize_statements
|
||||
# Some single-statement policies place their statement
|
||||
# directly in policy['Statement'], rather than in an
|
||||
|
|
|
@ -164,7 +164,14 @@ class AwsIamPolicyMatchersTest < Minitest::Test
|
|||
'Resource' => 'dummy',
|
||||
'Sid' => 'dummy',
|
||||
}.each do |criterion, test_value|
|
||||
AwsIamPolicy.new('test-policy-1').has_statement?(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
|
||||
|
||||
|
@ -187,79 +194,85 @@ class AwsIamPolicyMatchersTest < Minitest::Test
|
|||
end
|
||||
|
||||
def test_have_statement_when_sid_is_provided
|
||||
assert(AwsIamPolicy.new('test-policy-1').has_statement?('Sid' => 'beta01'))
|
||||
assert(AwsIamPolicy.new('test-policy-2').has_statement?('Sid' => 'CloudWatchEventsFullAccess'))
|
||||
assert(AwsIamPolicy.new('test-policy-2').has_statement?('Sid' => 'IAMPassRoleForCloudWatchEvents'))
|
||||
refute(AwsIamPolicy.new('test-policy-2').has_statement?('Sid' => 'beta01'))
|
||||
['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?('Sid' => /eta/))
|
||||
assert(AwsIamPolicy.new('test-policy-2').has_statement?('Sid' => /CloudWatch/))
|
||||
refute(AwsIamPolicy.new('test-policy-2').has_statement?('Sid' => /eta/))
|
||||
end
|
||||
|
||||
def test_have_statement_when_provided_invalid_effect
|
||||
assert_raises(ArgumentError) { AwsIamPolicy.new('test-policy-1').has_statement?('Effect' => 'Disallow') }
|
||||
assert_raises(ArgumentError) { AwsIamPolicy.new('test-policy-1').has_statement?('Effect' => 'allow') }
|
||||
assert_raises(ArgumentError) { AwsIamPolicy.new('test-policy-1').has_statement?('Effect' => :Allow) }
|
||||
assert_raises(ArgumentError) { AwsIamPolicy.new('test-policy-1').has_statement?('Effect' => :allow) }
|
||||
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
|
||||
assert(AwsIamPolicy.new('test-policy-1').has_statement?('Effect' => 'Deny'))
|
||||
refute(AwsIamPolicy.new('test-policy-1').has_statement?('Effect' => 'Allow'))
|
||||
assert(AwsIamPolicy.new('test-policy-2').has_statement?('Effect' => 'Allow'))
|
||||
['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
|
||||
# Able to match a simple string action when multiple statements present
|
||||
assert(AwsIamPolicy.new('test-policy-2').has_statement?('Action' => 'iam:PassRole'))
|
||||
# Able to match a wildcard string action
|
||||
assert(AwsIamPolicy.new('test-policy-2').has_statement?('Action' => 'events:*'))
|
||||
# Do not match a wildcard when using strings
|
||||
refute(AwsIamPolicy.new('test-policy-2').has_statement?('Action' => 'events:EnableRule'))
|
||||
# Do match when using a regex
|
||||
assert(AwsIamPolicy.new('test-policy-2').has_statement?('Action' => /^events\:/))
|
||||
# Able to match one action when the statement has an array of actions
|
||||
assert(AwsIamPolicy.new('test-policy-1').has_statement?('Action' => '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?('Action' => ['ec2:DescribeSubnets']))
|
||||
# Do match if two actions specified when the statement has an array of actions
|
||||
assert(AwsIamPolicy.new('test-policy-1').has_statement?('Action' => ['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?('Action' => ['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?('Action' => /^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?('Action' => [/^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?('Action' => '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?('Action' => 'iam:*'))
|
||||
['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
|
||||
# Able to match a simple string resource when multiple statements present
|
||||
assert(AwsIamPolicy.new('test-policy-2').has_statement?('Resource' => 'arn:aws:iam::*:role/AWS_Events_Invoke_Targets'))
|
||||
# Able to match a wildcard string resource
|
||||
assert(AwsIamPolicy.new('test-policy-2').has_statement?('Resource' => '*'))
|
||||
# Do not match a wildcard when using strings
|
||||
refute(AwsIamPolicy.new('test-policy-2').has_statement?('Resource' => 'arn:aws:events:us-east-1:123456789012:rule/my-rule'))
|
||||
# Do match when using a regex
|
||||
assert(AwsIamPolicy.new('test-policy-2').has_statement?('Resource' => /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?('Resource' => '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?('Resource' => ['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?('Resource' => ['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?('Resource' => ['*', '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?('Resource' => /^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?('Resource' => [/\*/]))
|
||||
# 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?('Resource' => '*'))
|
||||
['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
|
||||
|
||||
|
|
Loading…
Reference in a new issue