From e5ddd7d6e095ef148b1b7a3afc8a36af19bd343d Mon Sep 17 00:00:00 2001 From: Naveen Kumar Date: Wed, 4 Mar 2020 16:01:20 +0530 Subject: [PATCH 1/3] IAM_ROLE_NOT_USED Initial Push for IAM_ROLE_NOT_USED --- python/IAM_ROLE_NOT_USED/IAM_ROLE_NOT_USED.py | 126 ++++++++++++++++++ .../IAM_ROLE_NOT_USED_test.py | 82 ++++++++++++ python/IAM_ROLE_NOT_USED/parameters.json | 12 ++ 3 files changed, 220 insertions(+) create mode 100644 python/IAM_ROLE_NOT_USED/IAM_ROLE_NOT_USED.py create mode 100644 python/IAM_ROLE_NOT_USED/IAM_ROLE_NOT_USED_test.py create mode 100644 python/IAM_ROLE_NOT_USED/parameters.json 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..39e876465 --- /dev/null +++ b/python/IAM_ROLE_NOT_USED/IAM_ROLE_NOT_USED.py @@ -0,0 +1,126 @@ +# 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 + +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 will check for 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 + + +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'] + #compliance_type = ComplianceType.NOT_APPLICABLE + 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): + valid_rule_parameters = rule_parameters + if 'DaysBeforeUnused' not in rule_parameters: + raise ValueError('The Config Rule must have the parameter "DaysBeforeUnused"') + + if not rule_parameters['DaysBeforeUnused']: + rule_parameters['DaysBeforeUnused'] = 90 + + # 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 valid_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 From bf39ad71034dbafe2c4269ded6c7db35dbe0edb0 Mon Sep 17 00:00:00 2001 From: Naveen Kumar Date: Wed, 4 Mar 2020 17:51:23 +0530 Subject: [PATCH 2/3] Removed comments --- python/IAM_ROLE_NOT_USED/IAM_ROLE_NOT_USED.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/python/IAM_ROLE_NOT_USED/IAM_ROLE_NOT_USED.py b/python/IAM_ROLE_NOT_USED/IAM_ROLE_NOT_USED.py index 39e876465..31a2ac58f 100644 --- a/python/IAM_ROLE_NOT_USED/IAM_ROLE_NOT_USED.py +++ b/python/IAM_ROLE_NOT_USED/IAM_ROLE_NOT_USED.py @@ -17,7 +17,7 @@ IAM_ROLE_NOT_USED Description: - Check that an AWS IAM Role is being used in the last X days + 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). @@ -58,6 +58,7 @@ CURRENT_TIME = datetime.now(timezone.utc) RESOURCE_TYPE = 'AWS::IAM::Role' PAGE_SIZE = 20 +DEFAULT_DAYS = 90 class IAM_ROLE_NOT_USED(ConfigRule): @@ -70,7 +71,6 @@ def evaluate_periodic(self, event, client_factory, valid_rule_parameters): for username in describe_roles(config_client): user_data = iam_client.get_role(RoleName=username) last_used = user_data['Role']['RoleLastUsed'] - #compliance_type = ComplianceType.NOT_APPLICABLE if last_used: diff = (CURRENT_TIME - last_used['LastUsedDate']).days else: @@ -85,12 +85,11 @@ def evaluate_periodic(self, event, client_factory, valid_rule_parameters): return evaluations def evaluate_parameters(self, rule_parameters): - valid_rule_parameters = rule_parameters if 'DaysBeforeUnused' not in rule_parameters: - raise ValueError('The Config Rule must have the parameter "DaysBeforeUnused"') + raise InvalidParametersError('The Config Rule must have the parameter "DaysBeforeUnused"') - if not rule_parameters['DaysBeforeUnused']: - rule_parameters['DaysBeforeUnused'] = 90 + 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: @@ -100,7 +99,7 @@ def evaluate_parameters(self, rule_parameters): if rule_parameters['DaysBeforeUnused'] < 0: raise InvalidParametersError('The parameter "DaysBeforeUnused" must be greater than or equal to 0') - return valid_rule_parameters + return rule_parameters def describe_roles(config_client): From e51e88acc16ed0c1f58dc4e648765cca0734f9f4 Mon Sep 17 00:00:00 2001 From: jongogogo Date: Tue, 17 Mar 2020 09:29:58 +0800 Subject: [PATCH 3/3] Update IAM_ROLE_NOT_USED.py --- python/IAM_ROLE_NOT_USED/IAM_ROLE_NOT_USED.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/IAM_ROLE_NOT_USED/IAM_ROLE_NOT_USED.py b/python/IAM_ROLE_NOT_USED/IAM_ROLE_NOT_USED.py index 31a2ac58f..e1cdb35fb 100644 --- a/python/IAM_ROLE_NOT_USED/IAM_ROLE_NOT_USED.py +++ b/python/IAM_ROLE_NOT_USED/IAM_ROLE_NOT_USED.py @@ -33,7 +33,7 @@ Rule Parameters: DaysBeforeUnused - (Optional) Number of days when AWS IAM Roles are considered unused (default 90 days). If the value is 0 will check for 24 hours + (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