As soon as I read about AWS IAM access advisor APIs, I knew this is something useful. Last week, we came across a use case where we wanted to get notified for all the IAM Roles with services, not accessed for more than 90 days.
⚡TL;DR
- AWS Lambda — list IAM Roles with services, not accessed from more than 90 days.
- AWS SES — get notified about all IAM Roles with the list of services not accessed for the last 90 days.
🔥But, how does It work?
We’ll go through creating an AWS Lambda to automate this task using access advisor APIs provided by Python Boto3. We’ll be using following access advisor APIs:
- generate_service_last_accessed_details — generates the service last accessed data for an IAM resource (user, role, group, or policy). You need to call this API first to start a job that generates the service last accessed data for the IAM resource. This API returns a JobId that you will use for the other APIs, such as get_service_last_accessed_details, to determine the status of the job completion.
- get_service_last_accessed_details — use this to retrieve the service last accessed data for an IAM resource based on the JobID you pass in.
Let’s get started with coding!
- Create Lambda, select Python 3.x run time and Attach IAM Role
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"ses:SendEmail",
"logs:CreateLogStream",
"ses:SendRawEmail",
"iam:GenerateServiceLastAccessedDetails",
"iam:ListRoles",
"iam:GetServiceLastAccessedDetails",
"logs:CreateLogGroup",
"logs:PutLogEvents"
],
"Resource": "*"
}
]
}
2. Get IAM Client to get service last accessed details:
def get_iam_client():
"""
Get identity and access management client
"""
return boto3.client('iam')
3. Get SES Client for sending email to notify:
def get_ses_client():
"""
Get simple email service client
"""
return boto3.client('ses')
4. Overall Lambda Function will look like this:
import boto3
from botocore.exceptions import ClientError
from datetime import datetime, timezone
import traceback
from time import sleep
RECIPIENTS = ["[email protected]"]
SENDER = "John Doe <[email protected]>"
LAST_ACCESS_THRESHOLD = 90
def get_iam_client():
"""
Get identity and access management client
"""
return boto3.client(
'iam'
)
def get_ses_client():
"""
Get simple email service client
"""
return boto3.client(
'ses'
)
def get_older_iam_roles(client):
"""
Get dictionary of iam roles with servics older than 90 days
:param client: iam client to list roles
"""
Services_Access = {}
# listing arns
list_roles_arn = client.list_roles()
if not list_roles_arn:
raise Exception("No roles found in account")
# iterating over arns
for single_arn in list_roles_arn['Roles']:
TODAYS_DAY = datetime.now(timezone.utc)
ARNS = single_arn['Arn']
jobid_response = client.generate_service_last_accessed_details(
Arn=ARNS
)
role_jobid = jobid_response['JobId']
service_response = client.get_service_last_accessed_details(
JobId=role_jobid
)
# checking if job is completed else wait and retry until job is completed
while service_response['JobStatus'] != 'COMPLETED':
# print(f'pending : {service_response}')
service_response = client.get_service_last_accessed_details(
JobId=role_jobid
)
sleep(1)
# getting last access services
last_accessed_services = service_response['ServicesLastAccessed']
# iterating over servies to get the services with last access date greater than 90
for last_accessed in last_accessed_services:
try:
# print(last_accessed)
role_lastaccess_day = last_accessed['LastAuthenticated']
# difference between today and last access date
days_difference = TODAYS_DAY - role_lastaccess_day
# checking if difference is greater than 90
check_difference = days_difference.days > LAST_ACCESS_THRESHOLD
if check_difference:
Services_Access[ARNS] = {}
Services_Access[ARNS][last_accessed['ServiceName']] = days_difference.days
except Exception as e:
continue
# returning dictionary containing iam roles for services with no access > 90 days
return Services_Access
def send_email_iam_services(client, old_iams, sender, receipients):
"""
Send email for all the iam roles with services access more than 90 days
:param client: simple emailing service client
:param old_iams: iam role arns dictionary
:param sender: email sender
:param receipients: list of receipients e.g. ['[email protected]', '[email protected]'] etc.
"""
CHARSET = "UTF-8"
SUBJECT = "List of IAM Roles"
TABLE_START="""<table style="width:100%">
<tr>
<th>Role ARN</th>
<th>Service</th>
<th>Days</th>
</tr> """
TABLE_BODY = """ """
TABLE_END = """</table>"""
for role, services in old_iams.items():
TABLE_BODY += "<tr>"
for service in services.items():
TABLE_BODY += "<td>" + str(role) + "</td>" +"<td>" + str(service[0]) + "</td>" + "<td>" + str(service[1]) + "</td>"
TABLE_BODY += "</tr>"
TOTAL_TABLE = TABLE_START + TABLE_BODY + TABLE_END
# Try to send the email.
try:
#Provide the contents of the email.
response = client.send_email(
Destination={
'ToAddresses': receipients,
},
Message={
'Body': {
'Html': {
'Charset': CHARSET,
'Data': TOTAL_TABLE,
}
},
'Subject': {
'Charset': CHARSET,
'Data': SUBJECT,
},
},
Source=sender,
# If you are not using a configuration set, comment or delete the
# following line
)
# Display an error if something goes wrong.
except ClientError as e:
print(e.response['Error']['Message'])
raise Exception(e.response['Error']['Message'])
else:
print("Email sent for older IAM Roles! Message ID:"),
print(response['MessageId'])
def lambda_handler(event, context):
try:
# getting iam client to use
iam_client = get_iam_client()
# getting ses client to use
ses_client = get_ses_client()
# getting dictionary of all iam roles with last access > 90
old_iams = get_older_iam_roles(iam_client)
# sending email for iam roles we got in last step
send_email_iam_services(ses_client, old_iams, SENDER, RECIPIENTS)
except Exception as e:
traceback.print_exc()
raise Exception(str(e))
You’ll receive a clean email for all IAM roles like this:

Couple of things to note:
- You may need to upload the deployment package manually with the latest boto3 version instead of using AWS imported boto3 (more info: https://aws.amazon.com/premiumsupport/knowledge-center/build-python-lambda-deployment-package/)
- Recipients should be verified SES emails if we are using SES sandbox environment.
- Lambda timeout should be in minutes at least as some times it takes time for AWS to process jobs.
- Tested on Python version 3.x