Skip to content

IAM_ROLE_NOT_USED #5

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 125 additions & 0 deletions python/IAM_ROLE_NOT_USED/IAM_ROLE_NOT_USED.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
# Copyright 2017-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"). You may
# not use this file except in compliance with the License. A copy of the License is located at
#
# http://aws.amazon.com/apache2.0/
#
# or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for
# the specific language governing permissions and limitations under the License.

'''
#####################################
## Gherkin ##
#####################################
Rule Name:
IAM_ROLE_NOT_USED

Description:
Check that an AWS IAM Role is being used in the last X days, default value is 90 days

Rationale:
Ensure that no AWS IAM Role is unused and make an action if unused (e.g. delete the user).

Indicative Severity:
Low

Trigger:
Periodic

Reports on:
AWS::IAM::Role

Rule Parameters:
DaysBeforeUnused
(Optional) Number of days when AWS IAM Roles are considered unused (default 90 days). If the value is 0, IAM Roles must be used at least once every 24 hours.

Scenarios:
Scenario: 1
Given: Rule parameter Days is not a positive integer
Then: Return Error
Scenario: 2
Given: No AMI Role is unused from last DaysBeforeUnused days
Then: Return COMPLIANT
Scenario: 3
Given: One or more AMI Role is unused from last DaysBeforeUnused days
Then: Return NON_COMPLIANT
'''


import json

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove extra breaks

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

from datetime import datetime, timezone

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove extra breaks

from rdklib import Evaluator, Evaluation, ConfigRule, ComplianceType, InvalidParametersError

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove extra breaks


CURRENT_TIME = datetime.now(timezone.utc)
RESOURCE_TYPE = 'AWS::IAM::Role'
PAGE_SIZE = 20
DEFAULT_DAYS = 90

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove extra breaks


class IAM_ROLE_NOT_USED(ConfigRule):

def evaluate_periodic(self, event, client_factory, valid_rule_parameters):
evaluations = []
iam_client = client_factory.build_client(service='iam')
config_client = client_factory.build_client(service='config')

for username in describe_roles(config_client):
user_data = iam_client.get_role(RoleName=username)
last_used = user_data['Role']['RoleLastUsed']
if last_used:
diff = (CURRENT_TIME - last_used['LastUsedDate']).days
else:
created_on = user_data['Role']['CreateDate']
diff = (CURRENT_TIME - created_on).days
if diff <= valid_rule_parameters['DaysBeforeUnused']:
evaluations.append(Evaluation(ComplianceType.COMPLIANT, username, RESOURCE_TYPE))
else:
evaluations.append(Evaluation(ComplianceType.NON_COMPLIANT,
username, RESOURCE_TYPE,
annotation="Ensure that no AWS IAM Role is unused and make an action if unused (e.g. delete the user)."))
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Annotation should be focusing on explaning the situation, to help on resolution.
"This AWS IAM Role has not been used within the last {} day(s)".format(_rule_param_)

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed the annotation message.

return evaluations

def evaluate_parameters(self, rule_parameters):
if 'DaysBeforeUnused' not in rule_parameters:
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, you stated it is optional. You should not make it mandatory.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Modified according to the comment.

raise InvalidParametersError('The Config Rule must have the parameter "DaysBeforeUnused"')

if not rule_parameters.get('DaysBeforeUnused'):
rule_parameters['DaysBeforeUnused'] = DEFAULT_DAYS

# The int() function will raise an error if the string configured can't be converted to an integer
try:
rule_parameters['DaysBeforeUnused'] = int(rule_parameters['DaysBeforeUnused'])
except ValueError:
raise InvalidParametersError('The parameter "DaysBeforeUnused" must be a integer')

if rule_parameters['DaysBeforeUnused'] < 0:
raise InvalidParametersError('The parameter "DaysBeforeUnused" must be greater than or equal to 0')
return rule_parameters


def describe_roles(config_client):
sql = "select * where resourceType = 'AWS::IAM::Role'"
next_token = True
response = config_client.select_resource_config(Expression=sql, Limit=PAGE_SIZE)
while next_token:
for result in response['Results']:
yield json.loads(result)['resourceName']
if 'NextToken' in response:
next_token = response['NextToken']
response = config_client.select_resource_config(Expression=sql, NextToken=next_token, Limit=PAGE_SIZE)
else:
next_token = False


################################
# DO NOT MODIFY ANYTHING BELOW #
################################
def lambda_handler(event, context):
my_rule = IAM_ROLE_NOT_USED()
evaluator = Evaluator(my_rule)
return evaluator.handle(event, context)
82 changes: 82 additions & 0 deletions python/IAM_ROLE_NOT_USED/IAM_ROLE_NOT_USED_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# Copyright 2017-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"). You may
# not use this file except in compliance with the License. A copy of the License is located at
#
# http://aws.amazon.com/apache2.0/
#
# or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for
# the specific language governing permissions and limitations under the License.

import unittest

from datetime import datetime, timezone, timedelta

from mock import patch, MagicMock
from rdklib import Evaluation, ComplianceType, InvalidParametersError
import rdklibtest

##############
# Parameters #
##############

# Define the default resource to report to Config Rules
RESOURCE_TYPE = 'AWS::IAM::Role'

#############
# Main Code #
#############

MODULE = __import__('IAM_ROLE_NOT_USED')
RULE = MODULE.IAM_ROLE_NOT_USED()

CLIENT_FACTORY = MagicMock()

#example for mocking S3 API calls
IAM_CLIENT_MOCK = MagicMock()
CONFIG_CLIENT = MagicMock()


def mock_get_client(service, *args, **kwargs):
if service == 'iam':
return IAM_CLIENT_MOCK
if service == 'config':
return CONFIG_CLIENT
raise Exception("Attempting to create an unknown client")


@patch.object(CLIENT_FACTORY, 'build_client', MagicMock(side_effect=mock_get_client))
class ComplianceTest(unittest.TestCase):

def test_compliance(self):
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

need to follow the naming convention.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

rule_parameters = {"DaysBeforeUnused": "90"}
rule_parameters = RULE.evaluate_parameters(rule_parameters)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

test evaluate parameter separately.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added another testcase for evaluating rule parameters

input_event = rdklibtest.create_test_scheduled_event(rule_parameters_json=rule_parameters)
CONFIG_CLIENT.select_resource_config.return_value = {"Results":['{"resourceName":"config-rule"}']}
IAM_CLIENT_MOCK.get_role.return_value = {"Role":{"RoleLastUsed": {"LastUsedDate": datetime.now(timezone.utc)}}}
response = RULE.evaluate_periodic(input_event, CLIENT_FACTORY, rule_parameters)
resp_expected = [Evaluation(ComplianceType.COMPLIANT, 'config-rule', RESOURCE_TYPE)]
rdklibtest.assert_successful_evaluation(self, response, resp_expected, 1)

def test_non_compliance(self):
rule_parameters = {"DaysBeforeUnused":"90"}
rule_parameters = RULE.evaluate_parameters(rule_parameters)
input_event = rdklibtest.create_test_scheduled_event(rule_parameters_json=rule_parameters)
CONFIG_CLIENT.select_resource_config.return_value = {"Results":['{"resourceName":"AWS-CodePipeline-Service"}']}
IAM_CLIENT_MOCK.get_role.return_value = {"Role":{"RoleLastUsed": {"LastUsedDate": datetime.now(timezone.utc) - timedelta(days=100)}}}
response = RULE.evaluate_periodic(input_event, CLIENT_FACTORY, rule_parameters)
resp_expected = [Evaluation(ComplianceType.NON_COMPLIANT, 'AWS-CodePipeline-Service', RESOURCE_TYPE)]
rdklibtest.assert_successful_evaluation(self, response, resp_expected, 1)

def test_invalid_params_strings(self):
rule_parameters = {"DaysBeforeUnused": "sdfsdf"}
with self.assertRaises(InvalidParametersError) as context:
RULE.evaluate_parameters(rule_parameters)
self.assertIn('The parameter "DaysBeforeUnused" must be a integer', str(context.exception))

def test_invalid_params_negative(self):
rule_parameters = {"DaysBeforeUnused": "-10"}
with self.assertRaises(InvalidParametersError) as context:
RULE.evaluate_parameters(rule_parameters)
self.assertIn('The parameter "DaysBeforeUnused" must be greater than or equal to 0', str(context.exception))
12 changes: 12 additions & 0 deletions python/IAM_ROLE_NOT_USED/parameters.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"Version": "1.0",
"Parameters": {
"RuleName": "IAM_ROLE_NOT_USED",
"SourceRuntime": "python3.6-lib",
"CodeKey": "IAM_ROLE_NOT_USED.zip",
"InputParameters": "{\"DaysBeforeUnused\": 90}",
"OptionalParameters": "{}",
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Put as empty here

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I meant:
"OptionalParameters": "{\"DaysBeforeUnused\": \"\"}",

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

"SourcePeriodic": "One_Hour"
},
"Tags": "[]"
}