Skip to content

RDS_LOGGING_ENABLED and IAM_ROLE_NOT_USED #8

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 6 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
112 changes: 112 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,112 @@
# 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 = 100
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 role_name in describe_roles(config_client):
role_data = iam_client.get_role(RoleName=role_name)
role = role_data.get('Role')
last_used = role.get('RoleLastUsed')
if last_used:
diff = (CURRENT_TIME - last_used.get('LastUsedDate')).days
else:
created_on = role.get('CreateDate')
diff = (CURRENT_TIME - created_on).days
days_before_unused = valid_rule_parameters.get('DaysBeforeUnused')
if diff <= days_before_unused:
evaluations.append(Evaluation(ComplianceType.COMPLIANT, role_name, RESOURCE_TYPE))
else:
evaluations.append(Evaluation(ComplianceType.NON_COMPLIANT, role_name, RESOURCE_TYPE,
annotation="This AWS IAM Role has not been used within the last {} day(s)".format(days_before_unused)))
return evaluations

def evaluate_parameters(self, rule_parameters):

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

def lambda_handler(event, context):
my_rule = IAM_ROLE_NOT_USED()
evaluator = Evaluator(my_rule)
return evaluator.handle(event, context)
67 changes: 67 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,67 @@
# 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

RESOURCE_TYPE = 'AWS::IAM::Role'

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

CLIENT_FACTORY = MagicMock()
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_scenario1_rulesparameterisnotinteger_returnserror(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_scenario1_rulesparameterisnotpositiveinteger_returnserror(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))

def test_scenario2_noiamroleisunusedwithindaysbeforeunusedrulesparameter_returnscompliant(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)

def test_scenario3_oneormoreiamrolesunused_returnsnoncompliant(self):
rule_parameters = {"DaysBeforeUnused": "80"}
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,
annotation='This AWS IAM Role has not been used within the last 80 day(s)')]
rdklibtest.assert_successful_evaluation(self, response, resp_expected)
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": "{}",
"OptionalParameters": "{\"DaysBeforeUnused\": \"\"}",
"SourcePeriodic": "One_Hour"
},
"Tags": "[]"
}
94 changes: 94 additions & 0 deletions python/RDS_LOGGING_ENABLED/RDS_LOGGING_ENABLED.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# 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:
RDS_LOGGING_ENABLED

Description:
Check that respective logs of Amazon RDS are enabled.
The result is NON_COMPLIANT if any log types are not enabled.
Oracle: (Alert, Audit, Trace, Listener)
PostgreSQL: (Postgresql, Upgrade)
MySQL: (Audit, Error, General, SlowQuery)
MariaDB: (Audit, Error, General, SlowQuery)
SQL Server: (Error, Agent)
Aurora: (Audit, Error, General, SlowQuery)
Aurora-MySQL: (Audit, Error, General, SlowQuery)
Aurora-PostgreSQL: (Postgresql, Upgrade)

Indicative Severity:
Medium

Trigger:
Configuration change on AWS::RDS::DBInstance

Reports on:
AWS::RDS::DBInstance

Rule Parameters:
None

Scenarios:
Scenario: 1
Given: 'enabledCloudwatchLogsExports' in configuration item of Amazon RDS instance has all log types enabled
Then: Return COMPLIANT

Scenario: 2
Given: 'enabledCloudwatchLogsExports' in configuration item of Amazon RDS instance has one or more log types not enabled
Then: Return NON_COMPLIANT

Scenario: 3
Given: If engine specified is not in the above mentioned Amazon RDS engines
Then: Return NOT_APPLICABLE

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

class RDS_LOGGING_ENABLED(ConfigRule):

engine_logs = {
'postgres': ["postgresql", "upgrade"],
'mariadb': ["audit", "error", "general", "slowquery"],
'mysql': ["audit", "error", "general", "slowquery"],
'oracle-ee': ["trace", "audit", "alert", "listener"],
'oracle-se': ["trace", "audit", "alert", "listener"],
'oracle-se1': ["trace", "audit", "alert", "listener"],
'oracle-se2': ["trace", "audit", "alert", "listener"],
'sqlserver-ee': ["error", "agent"],
'sqlserver-ex': ["error", "agent"],
'sqlserver-se': ["error", "agent"],
'sqlserver-web': ["error", "agent"],
'aurora': ["audit", "error", "general", "slowquery"],
'aurora-mysql': ["audit", "error", "general", "slowquery"],
'aurora-postgresql': ["postgresql", "upgrade"]
}

def evaluate_change(self, event, client_factory, configuration_item, valid_rule_parameters):
configuration = configuration_item.get('configuration')
engine = configuration.get('engine')
logs_list = self.engine_logs.get(engine)
if logs_list:
logs_enabled_list = configuration.get('enabledCloudwatchLogsExports', [])
logs_not_enabled_list = list(set(logs_list) - set(logs_enabled_list))
if logs_not_enabled_list:
return [Evaluation(ComplianceType.NON_COMPLIANT,
annotation='One or more logs are not enabled')]
return [Evaluation(ComplianceType.COMPLIANT)]
return [Evaluation(ComplianceType.NOT_APPLICABLE, annotation="Engine is not defined")]

def lambda_handler(event, context):
my_rule = RDS_LOGGING_ENABLED()
evaluator = Evaluator(my_rule)
Copy link

Choose a reason for hiding this comment

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

please upgrade rdk lib and use the latest template

return evaluator.handle(event, context)
79 changes: 79 additions & 0 deletions python/RDS_LOGGING_ENABLED/RDS_LOGGING_ENABLED_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# 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
import rdklibtest
from mock import patch, MagicMock
from rdklib import Evaluation, ComplianceType

RESOURCE_TYPE = 'AWS::RDS::DBInstance'

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

CLIENT_FACTORY = MagicMock()
RDS_CLIENT_MOCK = MagicMock()

def mock_get_client(client_name, *args, **kwargs):
if client_name == 'rds':
return RDS_CLIENT_MOCK
raise Exception("Attempting to create an unknown client")

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

all_logs_enabled_compliant = {
"configuration": {
"enabledCloudwatchLogsExports": ["postgresql", "upgrade"],
"engine": "postgres"
}
}
no_logs_enabled_non_compliant = {
"configuration": {
"enabledCloudwatchLogsExports": [],
"engine": "postgres"
}
}
one_log_enabled_non_compliant = {
"configuration": {
"enabledCloudwatchLogsExports": ["error"],
"engine": "aurora"
}
}
engine_not_applicable = {
"configuration": {
"enabledCloudwatchLogsExports": ["error"],
"engine": "redshift"
}
}

def test_scenario1_evaluatechange_alllogsenabledonrds_returnscompliant(self):
response = RULE.evaluate_change(None, CLIENT_FACTORY, self.all_logs_enabled_compliant, None)
resp_expected = [Evaluation(ComplianceType.COMPLIANT)]
rdklibtest.assert_successful_evaluation(self, response, resp_expected)

def test_scenario2_evaluatechange_nologsenabledonrds_returnsnoncompliant(self):
response = RULE.evaluate_change(None, CLIENT_FACTORY, self.no_logs_enabled_non_compliant, None)
resp_expected = [Evaluation(ComplianceType.NON_COMPLIANT,
annotation="One or more logs are not enabled")]
rdklibtest.assert_successful_evaluation(self, response, resp_expected)

def test_scenario2_evaluatechange_onetypeoflogisenabledonrds_returnsnoncompliant(self):
response = RULE.evaluate_change(None, CLIENT_FACTORY, self.one_log_enabled_non_compliant, None)
resp_expected = [Evaluation(ComplianceType.NON_COMPLIANT,
annotation="One or more logs are not enabled")]
rdklibtest.assert_successful_evaluation(self, response, resp_expected)

def test_scenario3_evaluatechange_specifiedenginenotpresent_returnsnotapplicable(self):
response = RULE.evaluate_change(None, CLIENT_FACTORY, self.engine_not_applicable, None)
resp_expected = [Evaluation(ComplianceType.NOT_APPLICABLE,
annotation="Engine is not defined")]
rdklibtest.assert_successful_evaluation(self, response, resp_expected)
12 changes: 12 additions & 0 deletions python/RDS_LOGGING_ENABLED/parameters.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"Version": "1.0",
"Parameters": {
"RuleName": "RDS_LOGGING_ENABLED",
"SourceRuntime": "python3.6-lib",
"CodeKey": "RDS_LOGGING_ENABLED.zip",
"InputParameters": "{}",
"OptionalParameters": "{}",
"SourceEvents": "AWS::RDS::DBInstance"
},
"Tags": "[]"
}