mirror of
https://github.com/inspec/inspec
synced 2024-11-10 15:14:23 +00:00
Policy Statement Search capability for aws_iam_policy (#2918)
Signed-off-by: Clinton Wolfe <clintoncwolfe@gmail.com>
This commit is contained in:
parent
ceec3fc66c
commit
7130a77c06
5 changed files with 563 additions and 5 deletions
|
@ -5,9 +5,9 @@ platform: aws
|
||||||
|
|
||||||
# aws\_iam\_policy
|
# aws\_iam\_policy
|
||||||
|
|
||||||
Use the `aws_iam_policy` InSpec audit resource to test properties of a single managed AWS IAM Policy.
|
Use the `aws_iam_policy` InSpec audit resource to test properties of a single managed AWS IAM Policy. Use `aws_iam_policies` to audit IAM policies in bulk.
|
||||||
|
|
||||||
A policy is an entity in AWS that, when attached to an identity or resource, defines their permissions. AWS evaluates these policies when a principal, such as a user, makes a request. Permissions in the policies determine if the request is allowed or denied.
|
A policy defines the permissions of an identity or resource within AWS. AWS evaluates these policies when a principal, such as a user, makes a request. Policy permissions, also called "policy statements" in AWS, determine if a request is authorized -- and allow or deny it accordingly.
|
||||||
|
|
||||||
Each IAM Policy is uniquely identified by either its policy\_name or arn.
|
Each IAM Policy is uniquely identified by either its policy\_name or arn.
|
||||||
|
|
||||||
|
@ -50,11 +50,22 @@ The following examples show how to use this InSpec audit resource.
|
||||||
it { should be_attached }
|
it { should be_attached }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
### Examine the policy statements
|
||||||
|
|
||||||
|
describe aws_iam_policy('my-policy') do
|
||||||
|
# Verify that there is at least one statement allowing access to S3
|
||||||
|
it { should have_statement(Action: 's3:PutObject', Effect: 'allow') }
|
||||||
|
|
||||||
|
# have_statement does not expand wildcards. If you want to verify
|
||||||
|
# they are absent, an explicit check is required.
|
||||||
|
it { should_not have_statement(Action: 's3:*') }
|
||||||
|
end
|
||||||
|
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
## Properties
|
## Properties
|
||||||
|
|
||||||
* `arn`, `attachment_count`, `attached_groups`, `attached_roles`,`attached_users`, `default_version_id`
|
* `arn`, `attachment_count`, `attached_groups`, `attached_roles`,`attached_users`, `default_version_id`, `policy`, `statement_count`
|
||||||
|
|
||||||
## Property Examples
|
## Property Examples
|
||||||
|
|
||||||
|
@ -106,10 +117,38 @@ The 'default_version_id' value of the specified policy.
|
||||||
its('default_version_id') { should cmp "v1" }
|
its('default_version_id') { should cmp "v1" }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
### policy
|
||||||
|
|
||||||
|
This is a low-level, unsupported property.
|
||||||
|
|
||||||
|
Returns the default version of the policy document after decoding as a Ruby hash. This hash contains the policy statements and is useful for performing checks that cannot be expressed using higher-level matchers like `have_statement`.
|
||||||
|
|
||||||
|
For details regarding the contents of this structure, refer to the [AWS IAM Policy JSON Reference](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies.html). A set of examples is [also available](https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_examples.html).
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
# Fetch the policy structure as a Ruby object
|
||||||
|
policy_struct = aws_iam_policy('my-policy').policy
|
||||||
|
# Write a manually-constructed test to check that the policy
|
||||||
|
# has an IP constraint on the first statement
|
||||||
|
# ( Based on https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_examples_aws_deny-ip.html )
|
||||||
|
describe 'Check that we are restricting IP access' do
|
||||||
|
subject { policy_struct['Statement'].first['Condition'] }
|
||||||
|
it { should include 'NotIpAddress' }
|
||||||
|
end
|
||||||
|
|
||||||
|
### statement\_count
|
||||||
|
|
||||||
|
Returns the number of statements present in the `policy`.
|
||||||
|
|
||||||
|
# Make sure there are exactly two statements.
|
||||||
|
describe aws_iam_policy('my-policy') do
|
||||||
|
its('statement_count') { should cmp 2 }
|
||||||
|
end
|
||||||
|
|
||||||
## Matchers
|
## Matchers
|
||||||
|
|
||||||
This InSpec audit resource has the following special matchers. For a full list of available matchers, please visit our [matchers page](https://www.inspec.io/docs/reference/matchers/).
|
This InSpec audit resource has the following special matchers. For a full list of available matchers, please visit our [Universal Matchers page](https://www.inspec.io/docs/reference/matchers/).
|
||||||
|
|
||||||
### be\_attached
|
### be\_attached
|
||||||
|
|
||||||
|
@ -142,3 +181,59 @@ The test will pass if the identified policy attached the specified role.
|
||||||
describe aws_iam_policy('AWSSupportAccess') do
|
describe aws_iam_policy('AWSSupportAccess') do
|
||||||
it { should be_attached_to_role(ROLENAME) }
|
it { should be_attached_to_role(ROLENAME) }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
### have\_statement
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
* `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'.
|
||||||
|
* `Sid` - A user-provided string identifier for the statement.
|
||||||
|
* `Resource` - Expresses the operation's target. Acceptable values are ARNs, including the '*' wildcard. `Resource` may also use a list of ARN values.
|
||||||
|
|
||||||
|
Please note the following about the behavior of `have_statement`:
|
||||||
|
* `Action`, `Sid`, and `Resource` allow using a regular expression as the search critera instead of a string literal.
|
||||||
|
* it does not support wildcard expansion; to check for a wildcard value, check for it explicitly. For example, if the policy includes a statement with `"Action": "s3:*"` and the test checks for `Action: "s3:PutObject"`, the test _will not match_. You must write an additional test checking for the wildcard case.
|
||||||
|
* it supports searching list values. For example, if a statement contains a list of 3 resources, and a `have_statement` test specifes _one_ of those resources, it will match.
|
||||||
|
* `Action` and `Resource` allow using a list of string literals or regular expressions in a test, in which case _all_ must match on the _same_ statement for the test to match. Order is ignored.
|
||||||
|
* it does not support the `Principal` or `Conditional` key, or any of `NotAction`, `NotPrincipal`, or `NotResource`.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
# Verify there is no full-admin statement
|
||||||
|
describe aws_iam_policy('kryptonite') do
|
||||||
|
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
|
||||||
|
describe aws_iam_policy('bob-is-a-packrat') do
|
||||||
|
it { should have_statement(Effect: 'Allow',
|
||||||
|
# Using the AWS wildcard - this must match exactly
|
||||||
|
Resource: 'arn:aws:s3:::bobs-stuff*',
|
||||||
|
# Specify a list of actions - all must match, no others, order isn't important
|
||||||
|
Action: ['s3:PutObject', 's3:GetObject', 's3:DeleteObject'])}
|
||||||
|
|
||||||
|
# Bob would make new buckets constantly if we let him.
|
||||||
|
it { should_not have_statement(Effect: 'Allow', Action: 's3:CreateBucket')}
|
||||||
|
it { should_not have_statement(Effect: 'Allow', Action: 's3:*')}
|
||||||
|
it { should_not have_statement(Effect: 'Allow', Action: '*')}
|
||||||
|
|
||||||
|
# An alternative to checking for wildcards is to specify the
|
||||||
|
# statements you expect, then restrict statement count
|
||||||
|
its('statement_count') { should cmp 1 }
|
||||||
|
end
|
||||||
|
|
||||||
|
# Use regular expressions to examine the policy
|
||||||
|
describe aws_iam_policy('regex-demo') do
|
||||||
|
# Check to see if anything mentions RDS at all.
|
||||||
|
# This catches `rds:CreateDBinstance` and `rds:*`, but would not catch '*'.
|
||||||
|
it { should_not have_statement(Action: /^rds:.+$/)}
|
||||||
|
|
||||||
|
# This policy should refer to both sally and kim's s3 buckets.
|
||||||
|
# This will only match if there is a statement that refers to both resources.
|
||||||
|
it { should have_statement(Resource: [/arn:aws:s3.+:sally/, /arn:aws:s3.+:kim/]) }
|
||||||
|
# The following also matches on a statement mentioning only one of them
|
||||||
|
it { should have_statement(Resource: /arn:aws:s3.+:(sally|kim)/) }
|
||||||
|
end
|
||||||
|
|
|
@ -1,3 +1,7 @@
|
||||||
|
require 'json'
|
||||||
|
require 'set'
|
||||||
|
require 'uri'
|
||||||
|
|
||||||
class AwsIamPolicy < Inspec.resource(1)
|
class AwsIamPolicy < Inspec.resource(1)
|
||||||
name 'aws_iam_policy'
|
name 'aws_iam_policy'
|
||||||
desc 'Verifies settings for individual AWS IAM Policy'
|
desc 'Verifies settings for individual AWS IAM Policy'
|
||||||
|
@ -12,6 +16,21 @@ class AwsIamPolicy < Inspec.resource(1)
|
||||||
|
|
||||||
attr_reader :arn, :attachment_count, :default_version_id
|
attr_reader :arn, :attachment_count, :default_version_id
|
||||||
|
|
||||||
|
EXPECTED_CRITERIA = %w{
|
||||||
|
Action
|
||||||
|
Effect
|
||||||
|
Resource
|
||||||
|
Sid
|
||||||
|
}.freeze
|
||||||
|
|
||||||
|
UNIMPLEMENTED_CRITERIA = %w{
|
||||||
|
Conditional
|
||||||
|
NotAction
|
||||||
|
NotPrincipal
|
||||||
|
NotResource
|
||||||
|
Principal
|
||||||
|
}.freeze
|
||||||
|
|
||||||
def to_s
|
def to_s
|
||||||
"Policy #{@policy_name}"
|
"Policy #{@policy_name}"
|
||||||
end
|
end
|
||||||
|
@ -50,8 +69,133 @@ class AwsIamPolicy < Inspec.resource(1)
|
||||||
attached_roles.include?(role_name)
|
attached_roles.include?(role_name)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def policy
|
||||||
|
return nil unless exists?
|
||||||
|
return @policy if defined?(@policy)
|
||||||
|
|
||||||
|
catch_aws_errors do
|
||||||
|
backend = BackendFactory.create(inspec_runner)
|
||||||
|
gpv_response = backend.get_policy_version(policy_arn: arn, version_id: default_version_id)
|
||||||
|
@policy = JSON.parse(URI.decode_www_form_component(gpv_response.policy_version.document))
|
||||||
|
end
|
||||||
|
@policy
|
||||||
|
end
|
||||||
|
|
||||||
|
def statement_count
|
||||||
|
return nil unless exists?
|
||||||
|
policy['Statement'].count
|
||||||
|
end
|
||||||
|
|
||||||
|
def has_statement?(raw_criteria = {})
|
||||||
|
return nil unless exists?
|
||||||
|
criteria = has_statement__normalize_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|
|
||||||
|
true && \
|
||||||
|
has_statement__effect(statement, criteria) && \
|
||||||
|
has_statement__array_criterion(:action, statement, criteria) && \
|
||||||
|
has_statement__array_criterion(:resource, statement, criteria)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
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)
|
||||||
|
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."
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# If anything is left, it's spurious
|
||||||
|
unless raw_criteria.empty?
|
||||||
|
raise ArgumentError, "Unrecognized criteria #{raw_criteria.keys.join(', ')} to have_statement. Recognized criteria: #{EXPECTED_CRITERIA.join(', ')}"
|
||||||
|
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']}'"
|
||||||
|
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
|
||||||
|
policy['Statement'].map do |statement|
|
||||||
|
# Coerce some values into arrays
|
||||||
|
%w{Action Resource}.each do |field|
|
||||||
|
if statement.key?(field)
|
||||||
|
statement[field] = Array(statement[field])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Symbolize all keys
|
||||||
|
statement.keys.each do |field|
|
||||||
|
statement[field.downcase.to_sym] = statement.delete(field)
|
||||||
|
end
|
||||||
|
|
||||||
|
statement
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def has_statement__focus_on_sid(statements, criteria)
|
||||||
|
return statements unless criteria.key?(:sid)
|
||||||
|
sid_seek = criteria[:sid]
|
||||||
|
statements.select do |statement|
|
||||||
|
if sid_seek.is_a? Regexp
|
||||||
|
statement[:sid] =~ sid_seek
|
||||||
|
else
|
||||||
|
statement[:sid] == sid_seek
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def has_statement__effect(statement, criteria)
|
||||||
|
!criteria.key?(:effect) || criteria[:effect] == statement[:effect]
|
||||||
|
end
|
||||||
|
|
||||||
|
def has_statement__array_criterion(crit_name, statement, criteria)
|
||||||
|
return true unless criteria.key?(crit_name)
|
||||||
|
check = criteria[crit_name]
|
||||||
|
values = statement[crit_name] # This is an array due to normalize_statements
|
||||||
|
|
||||||
|
if check.is_a?(String)
|
||||||
|
# If check is a string, it only has to match one of the values
|
||||||
|
values.any? { |v| v == check }
|
||||||
|
elsif check.is_a?(Regexp)
|
||||||
|
# If check is a regex, it only has to match one of the values
|
||||||
|
values.any? { |v| v =~ check }
|
||||||
|
elsif check.is_a?(Array) && check.all? { |c| c.is_a? String }
|
||||||
|
# If check is an array of strings, perform setwise check
|
||||||
|
Set.new(values) == Set.new(check)
|
||||||
|
elsif check.is_a?(Array) && check.all? { |c| c.is_a? Regexp }
|
||||||
|
# If check is an array of regexes, all values must match all regexes
|
||||||
|
values.all? { |v| check.all? { |r| v =~ r } }
|
||||||
|
else
|
||||||
|
false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def validate_params(raw_params)
|
def validate_params(raw_params)
|
||||||
validated_params = check_resource_param_names(
|
validated_params = check_resource_param_names(
|
||||||
raw_params: raw_params,
|
raw_params: raw_params,
|
||||||
|
@ -113,6 +257,10 @@ class AwsIamPolicy < Inspec.resource(1)
|
||||||
BackendFactory.set_default_backend(self)
|
BackendFactory.set_default_backend(self)
|
||||||
self.aws_client_class = Aws::IAM::Client
|
self.aws_client_class = Aws::IAM::Client
|
||||||
|
|
||||||
|
def get_policy_version(criteria)
|
||||||
|
aws_service_client.get_policy_version(criteria)
|
||||||
|
end
|
||||||
|
|
||||||
def list_policies(criteria)
|
def list_policies(criteria)
|
||||||
aws_service_client.list_policies(criteria)
|
aws_service_client.list_policies(criteria)
|
||||||
end
|
end
|
||||||
|
|
|
@ -116,6 +116,99 @@ output "iam_group_administrators" {
|
||||||
value = "${aws_iam_group.administrators.name}"
|
value = "${aws_iam_group.administrators.name}"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#======================================================#
|
||||||
|
# IAM Policies
|
||||||
|
#======================================================#
|
||||||
|
|
||||||
|
# Test fixtures:
|
||||||
|
# Note: Principal is not allowed on an IAM Policy. (May be allowed on a role? certainly on s3 bucket?)
|
||||||
|
# alpha
|
||||||
|
# has 2 statements
|
||||||
|
# one is a wildcard on ec2
|
||||||
|
# both have IDs
|
||||||
|
# one is a resource full wildcard
|
||||||
|
# one is Allow, one is Deny
|
||||||
|
# scalar values throughout
|
||||||
|
# beta
|
||||||
|
# one statement
|
||||||
|
# list values for Resource and Action
|
||||||
|
# gamma
|
||||||
|
# allow all
|
||||||
|
|
||||||
|
resource "aws_iam_policy" "alpha" {
|
||||||
|
name = "${terraform.env}-alpha"
|
||||||
|
policy = <<EOF
|
||||||
|
{
|
||||||
|
"Version": "2012-10-17",
|
||||||
|
"Statement": [
|
||||||
|
{
|
||||||
|
"Sid": "alpha01",
|
||||||
|
"Action": "ec2:Describe*",
|
||||||
|
"Effect": "Allow",
|
||||||
|
"Resource": "*"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Sid": "alpha02",
|
||||||
|
"Action": "s3:GetObject",
|
||||||
|
"Effect": "Deny",
|
||||||
|
"Resource": "arn:aws:s3:::bobs-stuff"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
output "aws_iam_policy_alpha_name" {
|
||||||
|
value = "${aws_iam_policy.alpha.name}"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "aws_iam_policy" "beta" {
|
||||||
|
name = "${terraform.env}-beta"
|
||||||
|
policy = <<EOF
|
||||||
|
{
|
||||||
|
"Version": "2012-10-17",
|
||||||
|
"Statement": [
|
||||||
|
{
|
||||||
|
"Sid": "beta01",
|
||||||
|
"Action": [
|
||||||
|
"ec2:DescribeSubnets",
|
||||||
|
"ec2:DescribeSecurityGroups"
|
||||||
|
],
|
||||||
|
"Effect": "Deny",
|
||||||
|
"Resource": [
|
||||||
|
"arn:aws:ec2:::*",
|
||||||
|
"*"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
output "aws_iam_policy_beta_name" {
|
||||||
|
value = "${aws_iam_policy.beta.name}"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "aws_iam_policy" "gamma" {
|
||||||
|
name = "${terraform.env}-gamma"
|
||||||
|
policy = <<EOF
|
||||||
|
{
|
||||||
|
"Version": "2012-10-17",
|
||||||
|
"Statement": [
|
||||||
|
{
|
||||||
|
"Sid": "gamma01",
|
||||||
|
"Action": "*",
|
||||||
|
"Effect": "Allow",
|
||||||
|
"Resource": "*"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
output "aws_iam_policy_gamma_name" {
|
||||||
|
value = "${aws_iam_policy.gamma.name}"
|
||||||
|
}
|
||||||
#======================================================#
|
#======================================================#
|
||||||
# IAM Group Memberships
|
# IAM Group Memberships
|
||||||
#======================================================#
|
#======================================================#
|
||||||
|
|
|
@ -1,3 +1,15 @@
|
||||||
|
fixtures = {}
|
||||||
|
[
|
||||||
|
'aws_iam_policy_alpha_name',
|
||||||
|
'aws_iam_policy_beta_name',
|
||||||
|
].each do |fixture_name|
|
||||||
|
fixtures[fixture_name] = attribute(
|
||||||
|
fixture_name,
|
||||||
|
default: "default.#{fixture_name}",
|
||||||
|
description: 'See ../build/iam.tf',
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
control "aws_iam_policy recall" do
|
control "aws_iam_policy recall" do
|
||||||
describe aws_iam_policy("AWSSupportAccess") do
|
describe aws_iam_policy("AWSSupportAccess") do
|
||||||
it { should exist }
|
it { should exist }
|
||||||
|
@ -6,6 +18,10 @@ control "aws_iam_policy recall" do
|
||||||
describe aws_iam_policy(policy_name: "AWSSupportAccess") do
|
describe aws_iam_policy(policy_name: "AWSSupportAccess") do
|
||||||
it { should exist }
|
it { should exist }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe aws_iam_policy(fixtures['aws_iam_policy_alpha_name']) do
|
||||||
|
it { should exist }
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
control "aws_iam_policy properties" do
|
control "aws_iam_policy properties" do
|
||||||
|
@ -17,6 +33,11 @@ control "aws_iam_policy properties" do
|
||||||
its('attached_groups') { should be_empty }
|
its('attached_groups') { should be_empty }
|
||||||
its('attached_roles') { should be_empty }
|
its('attached_roles') { should be_empty }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe aws_iam_policy(fixtures['aws_iam_policy_alpha_name']) do
|
||||||
|
its('statement_count') { should cmp 2 }
|
||||||
|
its('policy') { should be_kind_of(Hash) }
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
control "aws_iam_policy matchers" do
|
control "aws_iam_policy matchers" do
|
||||||
|
@ -26,4 +47,36 @@ control "aws_iam_policy matchers" do
|
||||||
describe aws_iam_policy("AdministratorAccess") do
|
describe aws_iam_policy("AdministratorAccess") do
|
||||||
it { should be_attached_to_user("test-fixture-maker") }
|
it { should be_attached_to_user("test-fixture-maker") }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe aws_iam_policy(fixtures['aws_iam_policy_alpha_name']) do
|
||||||
|
it { should have_statement('Resource' => '*')}
|
||||||
|
it { should have_statement('Resource' => '*', 'Sid' => 'alpha01')}
|
||||||
|
it { should have_statement('Resource' => 'arn:aws:s3:::bobs-stuff', 'Sid' => 'alpha02')}
|
||||||
|
|
||||||
|
it { should have_statement('Effect' => 'Allow', 'Sid' => 'alpha01')}
|
||||||
|
it { should have_statement('Effect' => 'Deny', 'Sid' => 'alpha02')}
|
||||||
|
|
||||||
|
it { should have_statement('Action' => 'ec2:Describe*', 'Sid' => 'alpha01')}
|
||||||
|
it { should_not have_statement('Action' => 'ec2:Describe')}
|
||||||
|
it { should have_statement('Action' => /^ec2:Describe\*$/, 'Sid' => 'alpha01')}
|
||||||
|
it { should have_statement('Action' => /^ec2:.+$/, 'Sid' => 'alpha01')}
|
||||||
|
|
||||||
|
it { should have_statement('Action' => 'ec2:Describe*', 'Resource' => '*', 'Effect' => 'Allow') }
|
||||||
|
it { should_not have_statement('Action' => 'ec2:Describe*', 'Resource' => 'arn:aws:s3:::bobs-stuff') }
|
||||||
|
end
|
||||||
|
|
||||||
|
describe aws_iam_policy(fixtures['aws_iam_policy_beta_name']) do
|
||||||
|
it { should have_statement('Action' => 'ec2:DescribeSubnets')}
|
||||||
|
it { should have_statement('Action' => 'ec2:DescribeSecurityGroups')}
|
||||||
|
# Array indicates all must match
|
||||||
|
it { should_not have_statement('Action' => ['ec2:DescribeSecurityGroups'])}
|
||||||
|
it { should have_statement('Action' => ['ec2:DescribeSubnets', 'ec2:DescribeSecurityGroups'])}
|
||||||
|
it { should have_statement('Action' => ['ec2:DescribeSecurityGroups', 'ec2:DescribeSubnets'])}
|
||||||
|
|
||||||
|
it { should have_statement('Resource' => 'arn:aws:ec2:::*')}
|
||||||
|
it { should have_statement('Resource' => '*')}
|
||||||
|
it { should_not have_statement('Resource' => ['*'])}
|
||||||
|
it { should have_statement('Resource' => ['arn:aws:ec2:::*', '*'])}
|
||||||
|
it { should have_statement('Resource' => ['*', 'arn:aws:ec2:::*'])}
|
||||||
|
end
|
||||||
end
|
end
|
|
@ -90,6 +90,20 @@ class AwsIamPolicyPropertiesTest < Minitest::Test
|
||||||
assert_equal(['test-role'], AwsIamPolicy.new('test-policy-1').attached_roles)
|
assert_equal(['test-role'], AwsIamPolicy.new('test-policy-1').attached_roles)
|
||||||
assert_nil(AwsIamPolicy.new(policy_name: 'non-existant').attached_roles)
|
assert_nil(AwsIamPolicy.new(policy_name: 'non-existant').attached_roles)
|
||||||
end
|
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)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
|
@ -133,6 +147,113 @@ class AwsIamPolicyMatchersTest < Minitest::Test
|
||||||
def test_matcher_attached_to_role_negative
|
def test_matcher_attached_to_role_negative
|
||||||
refute AwsIamPolicy.new('test-policy-2').attached_to_role?('test-role')
|
refute AwsIamPolicy.new('test-policy-2').attached_to_role?('test-role')
|
||||||
end
|
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|
|
||||||
|
AwsIamPolicy.new('test-policy-1').has_statement?(criterion => test_value)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_have_statement_when_provided_unimplemented_criteria
|
||||||
|
[
|
||||||
|
'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
|
||||||
|
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'))
|
||||||
|
|
||||||
|
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) }
|
||||||
|
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'))
|
||||||
|
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/]))
|
||||||
|
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' => [/\*/]))
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
#=============================================================================#
|
#=============================================================================#
|
||||||
|
@ -158,7 +279,7 @@ module MAIPSB
|
||||||
OpenStruct.new({
|
OpenStruct.new({
|
||||||
policy_name: 'test-policy-2',
|
policy_name: 'test-policy-2',
|
||||||
arn: 'arn:aws:iam::aws:policy/test-policy-2',
|
arn: 'arn:aws:iam::aws:policy/test-policy-2',
|
||||||
default_version_id: 'v2',
|
default_version_id: 'v1',
|
||||||
attachment_count: 0,
|
attachment_count: 0,
|
||||||
is_attachable: false,
|
is_attachable: false,
|
||||||
}),
|
}),
|
||||||
|
@ -197,5 +318,53 @@ module MAIPSB
|
||||||
}
|
}
|
||||||
OpenStruct.new( policy[query[:policy_arn]] )
|
OpenStruct.new( policy[query[:policy_arn]] )
|
||||||
end
|
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'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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
|
||||||
end
|
end
|
Loading…
Reference in a new issue