diff --git a/python/IAM_ROLE_NOT_USED/IAM_ROLE_NOT_USED.py b/python/IAM_ROLE_NOT_USED/IAM_ROLE_NOT_USED.py new file mode 100644 index 000000000..e1cdb35fb --- /dev/null +++ b/python/IAM_ROLE_NOT_USED/IAM_ROLE_NOT_USED.py @@ -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 + +from datetime import datetime, timezone + +from rdklib import Evaluator, Evaluation, ConfigRule, ComplianceType, InvalidParametersError + + +CURRENT_TIME = datetime.now(timezone.utc) +RESOURCE_TYPE = 'AWS::IAM::Role' +PAGE_SIZE = 20 +DEFAULT_DAYS = 90 + + +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).")) + return evaluations + + def evaluate_parameters(self, rule_parameters): + if 'DaysBeforeUnused' not in rule_parameters: + 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) diff --git a/python/IAM_ROLE_NOT_USED/IAM_ROLE_NOT_USED_test.py b/python/IAM_ROLE_NOT_USED/IAM_ROLE_NOT_USED_test.py new file mode 100644 index 000000000..2bd79fa09 --- /dev/null +++ b/python/IAM_ROLE_NOT_USED/IAM_ROLE_NOT_USED_test.py @@ -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): + 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":"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)) diff --git a/python/IAM_ROLE_NOT_USED/parameters.json b/python/IAM_ROLE_NOT_USED/parameters.json new file mode 100644 index 000000000..d16aeba70 --- /dev/null +++ b/python/IAM_ROLE_NOT_USED/parameters.json @@ -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": "{}", + "SourcePeriodic": "One_Hour" + }, + "Tags": "[]" +} \ No newline at end of file