AWS Lambda — Automate Analyzing your Permissions using IAM Access Advisor

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!

  1. 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:

Leave a Comment

Your email address will not be published. Required fields are marked *