From 9f95a4e06e5d0e318e527ddc17b1b2e40fc781e4 Mon Sep 17 00:00:00 2001 From: Jeremy Frasier Date: Thu, 29 Aug 2024 10:18:01 -0400 Subject: [PATCH 01/28] First cut at creating this Lambda function --- README.md | 45 +++++++++---- requirements-dev.txt | 4 ++ src/lambda_handler.py | 150 ++++++++++++++++++++++++++++-------------- 3 files changed, 139 insertions(+), 60 deletions(-) diff --git a/README.md b/README.md index 3f52419..bd1a7f0 100644 --- a/README.md +++ b/README.md @@ -2,13 +2,9 @@ [![GitHub Build Status](https://github.com/cisagov/disable-inactive-iam-users-lambda/workflows/build/badge.svg)](https://github.com/cisagov/disable-inactive-iam-users-lambda/actions) -This is a generic skeleton project that can be used to quickly get a -new [cisagov](https://github.com/cisagov) GitHub -[AWS Lambda](https://aws.amazon.com/lambda/) project using the Python runtimes -started. This skeleton project contains [licensing information](LICENSE), as -well as [pre-commit hooks](https://pre-commit.com) and -[GitHub Actions](https://github.com/features/actions) configurations -appropriate for the major languages that we use. +This repository contains the code for an AWS Lambda function that +disables access for users who have not used said access sufficiently +recently. ## Building the base Lambda image ## @@ -57,6 +53,13 @@ Once you are finished you can stop the detached container with the following com docker compose down ``` +To customize the name of the deployment file, you can override the +`BUILD_FILE_NAME` environment variable. For example: + +```console +BUILD_FILE_NAME="disable_inactive_iam_users_lambda.zip" docker compose up build_deployment_package +``` + ## How to update Python dependencies ## The Python dependencies are maintained using a [Pipenv](https://github.com/pypa/pipenv) @@ -72,12 +75,30 @@ cd src/py3.9 pipenv lock ``` -## New Repositories from a Skeleton ## +## Lambda inputs ## + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| expiration_days | A strictly positive integer denoting the number of days after which an IAM user's access is considered inactive if unused. | `number` | n/a | yes | + +## Example Lambda input ## + +The following is an example of the JSON input event that is expected by the +Lambda: + +```json +{ + "expiration_days": 45 +} +``` + +## Deploying the Lambda ## -Please see our [Project Setup guide](https://github.com/cisagov/development-guide/tree/develop/project_setup) -for step-by-step instructions on how to start a new repository from -a skeleton. This will save you time and effort when configuring a -new repository! +The easiest way to deploy the Lambda and related resources is to use +the +[cisagov/disable-inactive-iam-users-terraform](https://github.com/cisagov/disable-inactive-iam-users-terraform) +repository. Refer to the documentation in that project for more +information. ## Contributing ## diff --git a/requirements-dev.txt b/requirements-dev.txt index bdc1615..e17dfb6 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,4 +1,8 @@ --requirement requirements-test.txt +# boto3 is not strictly required, but it can be useful for some local +# development testing activities, so we include it here as a +# convenience. +boto3 ipython pipenv semver diff --git a/src/lambda_handler.py b/src/lambda_handler.py index 74ca692..6c69272 100644 --- a/src/lambda_handler.py +++ b/src/lambda_handler.py @@ -1,23 +1,27 @@ -"""Simple AWS Lambda handler to verify functionality.""" +"""AWS Lambda handler to disable inactive IAM users.""" # Standard Python Libraries -from datetime import datetime, timezone +from datetime import datetime, timedelta, timezone import logging import os -from typing import Any, Optional, Union +from typing import Any, Dict, List, NamedTuple, Optional, Union # Third-Party Libraries -import cowsay -import cowsay.characters - -# cisagov Libraries -from example import example_div +import boto3 default_log_level = "INFO" logger = logging.getLogger() logger.setLevel(default_log_level) +class EventValidation(NamedTuple): + """Named tuple to hold event validation information.""" + + errors: List[str] + event: Dict[str, Any] + valid: bool + + def failed_task(result: dict[str, Any], error_msg: str) -> None: """Update a given result because of a failure during processing.""" result["success"] = False @@ -36,50 +40,100 @@ def task_default(event): return result -def task_cowsay(event) -> dict[str, Union[Optional[str], bool]]: - """Generate an output message using the provided information.""" - result: dict[str, Union[Optional[str], bool]] = {"message": None, "success": True} - - character: str = event.get("character", "tux") - if character not in cowsay.characters.CHARS.keys(): - error_msg = 'Character "%s" is not valid.' - logging.error(error_msg, character) - failed_task(result, error_msg % character) - else: - contents: str = event.get("contents", "Hello from AWS Lambda!") - logger.info( - 'Creating output using "%s" with contents "%s"', character, contents - ) - result["message"] = cowsay.get_output_string(character, contents) - - return result - - -def task_divide(event) -> dict[str, Union[Optional[float], bool]]: - """Divide one number by another and provide the result.""" - result: dict[str, Union[Optional[float], bool]] = {"result": None, "success": True} - numerator: str = event.get("numerator", None) - denominator: str = event.get("denominator", None) +def validate_event_data(event: Dict[str, Any]) -> EventValidation: + """Validate the event data and return a tuple containing the validated event, a boolean result (True if valid, False if invalid), and a list of error message strings.""" + result = True + errors = [] - if denominator is None or numerator is None: - error_msg = "Request must include both a numerator and a denominator." - logging.error(error_msg) - failed_task(result, error_msg) + # Check that expiration_days can be interpreted as a strictly positive + # integer. + if "expiration_days" not in event: + errors.append('Missing required key "expiration_days" in event.') + elif not event["expiration_days"]: + errors.append('"account_ids" must be non-null.') else: try: - variable_error_msg = "numerator: %s, denominator: %s" - result["result"] = example_div(int(numerator), int(denominator)) + tmp = int(event["expiration_days"]) + if tmp < 0: + errors.append('"account_ids" must be a strictly positive integer.') except ValueError: - error_msg = "The provided values must be integers." - logging.error(error_msg) - logging.error(variable_error_msg, numerator, denominator) - failed_task(result, error_msg) - except ZeroDivisionError: - error_msg = "The denominator cannot be zero." - logging.error(error_msg) - logging.error(variable_error_msg, numerator, denominator) - failed_task(result, error_msg) - + errors.append('"account_ids" must be an integer.') + + if errors: + result = False + + return EventValidation(errors, event, result) + + +def task_disable(event): + """Iterate over users and disable any inactive access.""" + result: Dict[str, Union[Optional[str], bool]] = {"message": None, "success": True} + + # Validate all event data before going any further + event_validation: EventValidation = validate_event_data(event) + if not event_validation.valid: + for e in event_validation.errors: + logging.error(e) + failed_task(result, " ".join(event_validation.errors)) + return result + validated_event = event_validation.event + + # The number of days after which unused access is considered inactive. + expiration_days: int = int(validated_event["expiration_days"]) + + # Create an IAM client + iam: boto3.client = boto3.client("iam") + + # Create a paginator for users + user_paginator = iam.get_paginator("list_users") + + # Create an iterator from the paginator + user_iterator = user_paginator.paginate() + + # Capture the current time and date + now = datetime.now() + too_old = timedelta(days=expiration_days) + + # Iterate over the users + for user in user_iterator: + user_name = user["UserName"] + password_last_used = user["PasswordLastUsed"] + + logging.debug("Examining user %s's console access", user_name) + + if now - password_last_used > too_old: + logging.info( + "Disabling user %s's console access due to inactivity", user_name + ) + # Disable the user's console access + # iam.delete_login_profile(UserName=user_name) + + # Create a paginator for access keys + access_key_paginator = iam.get_paginator("list_access_keys") + + # Create an iterator from the paginator + access_key_iterator = access_key_paginator.paginate(UserName=user_name) + + logging.debug("Examining user %s's access keys", user_name) + # Iterate over the access keys + for access_key in access_key_iterator: + access_key_id = access_key["AccessKeyId"] + access_key_last_used = iam.get_access_key_last_used( + AccessKeyId=access_key_id + ) + + logging.debug("Examining user %s's access key %s", user_name, access_key_id) + + if now - access_key_last_used > too_old: + logging.info( + "Disabling user %s's access key %s due to inactivity", + user_name, + access_key_id, + ) + # Make the access key inactive + # iam.update_access_key(AccessKeyId=access_key_id, Status="Inactive", UserName=user_name) + result["message"] = "Successfully disabled inactive IAM users." + logging.info(result["message"]) return result From cfab69044afc9b86112bd42c0b3d988c3a47b815 Mon Sep 17 00:00:00 2001 From: Jeremy Frasier Date: Fri, 30 Aug 2024 10:11:40 -0400 Subject: [PATCH 02/28] Ensure that expiration_days is nonzero --- src/lambda_handler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lambda_handler.py b/src/lambda_handler.py index 6c69272..21808bb 100644 --- a/src/lambda_handler.py +++ b/src/lambda_handler.py @@ -54,7 +54,7 @@ def validate_event_data(event: Dict[str, Any]) -> EventValidation: else: try: tmp = int(event["expiration_days"]) - if tmp < 0: + if tmp <= 0: errors.append('"account_ids" must be a strictly positive integer.') except ValueError: errors.append('"account_ids" must be an integer.') From 24997b95ab84999cdc8bbb3644a9dd44bc07d409 Mon Sep 17 00:00:00 2001 From: Jeremy Frasier Date: Fri, 30 Aug 2024 10:45:22 -0400 Subject: [PATCH 03/28] Correct the way I was using boto3 paginators --- src/lambda_handler.py | 79 +++++++++++++++++++++---------------------- 1 file changed, 39 insertions(+), 40 deletions(-) diff --git a/src/lambda_handler.py b/src/lambda_handler.py index 21808bb..fc2608b 100644 --- a/src/lambda_handler.py +++ b/src/lambda_handler.py @@ -84,55 +84,54 @@ def task_disable(event): # Create an IAM client iam: boto3.client = boto3.client("iam") - # Create a paginator for users - user_paginator = iam.get_paginator("list_users") - - # Create an iterator from the paginator - user_iterator = user_paginator.paginate() - # Capture the current time and date now = datetime.now() too_old = timedelta(days=expiration_days) - # Iterate over the users - for user in user_iterator: - user_name = user["UserName"] - password_last_used = user["PasswordLastUsed"] - - logging.debug("Examining user %s's console access", user_name) - - if now - password_last_used > too_old: - logging.info( - "Disabling user %s's console access due to inactivity", user_name - ) - # Disable the user's console access - # iam.delete_login_profile(UserName=user_name) - - # Create a paginator for access keys - access_key_paginator = iam.get_paginator("list_access_keys") - - # Create an iterator from the paginator - access_key_iterator = access_key_paginator.paginate(UserName=user_name) + # Create a paginator for users + user_paginator = iam.get_paginator("list_users") - logging.debug("Examining user %s's access keys", user_name) - # Iterate over the access keys - for access_key in access_key_iterator: - access_key_id = access_key["AccessKeyId"] - access_key_last_used = iam.get_access_key_last_used( - AccessKeyId=access_key_id - ) + # Iterate over the users + for page in user_paginator.paginate(): + for user in page["Users"]: + user_name = user["UserName"] + password_last_used = user["PasswordLastUsed"] - logging.debug("Examining user %s's access key %s", user_name, access_key_id) + logging.debug("Examining user %s's console access", user_name) - if now - access_key_last_used > too_old: + if now - password_last_used > too_old: logging.info( - "Disabling user %s's access key %s due to inactivity", - user_name, - access_key_id, + "Disabling user %s's console access due to inactivity", user_name ) - # Make the access key inactive - # iam.update_access_key(AccessKeyId=access_key_id, Status="Inactive", UserName=user_name) - result["message"] = "Successfully disabled inactive IAM users." + # Disable the user's console access + # iam.delete_login_profile(UserName=user_name) + + # Create a paginator for the current user's access keys + access_key_paginator = iam.get_paginator("list_access_keys") + + logging.debug("Examining user %s's access keys", user_name) + # Iterate over the access keys + for page in access_key_paginator.paginate(UserName=user_name): + for access_key in page["AccessKeyMetadata"]: + access_key_id = access_key["AccessKeyId"] + access_key_last_used = iam.get_access_key_last_used( + AccessKeyId=access_key_id + ) + + logging.debug( + "Examining user %s's access key %s", user_name, access_key_id + ) + + if now - access_key_last_used > too_old: + logging.info( + "Disabling user %s's access key %s due to inactivity", + user_name, + access_key_id, + ) + # Make the access key inactive + # iam.update_access_key(AccessKeyId=access_key_id, Status="Inactive", UserName=user_name) + + result["message"] = "Successfully disabled access for inactive IAM users." logging.info(result["message"]) return result From 025efbad4887be2f5e45bc549533af3ace6cdc3b Mon Sep 17 00:00:00 2001 From: Jeremy Frasier Date: Fri, 30 Aug 2024 10:52:07 -0400 Subject: [PATCH 04/28] Ignore access keys that are already inactive --- src/lambda_handler.py | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/src/lambda_handler.py b/src/lambda_handler.py index fc2608b..f88f644 100644 --- a/src/lambda_handler.py +++ b/src/lambda_handler.py @@ -118,18 +118,23 @@ def task_disable(event): AccessKeyId=access_key_id ) - logging.debug( - "Examining user %s's access key %s", user_name, access_key_id - ) - - if now - access_key_last_used > too_old: - logging.info( - "Disabling user %s's access key %s due to inactivity", + # We can safely ignore any access keys that are already + # inactive + if access_key["Status"] == "Active": + logging.debug( + "Examining user %s's active access key %s", user_name, access_key_id, ) - # Make the access key inactive - # iam.update_access_key(AccessKeyId=access_key_id, Status="Inactive", UserName=user_name) + + if now - access_key_last_used > too_old: + logging.info( + "Disabling user %s's access key %s due to inactivity", + user_name, + access_key_id, + ) + # Make the access key inactive + # iam.update_access_key(AccessKeyId=access_key_id, Status="Inactive", UserName=user_name) result["message"] = "Successfully disabled access for inactive IAM users." logging.info(result["message"]) From 47253b9d788f18c416c75539a7a70a5436adabe8 Mon Sep 17 00:00:00 2001 From: Jeremy Frasier Date: Fri, 30 Aug 2024 11:14:08 -0400 Subject: [PATCH 05/28] Handle the case where a user's console access was created recently If a user's console access was created too recently then we cannot yet determine inactivity, so we should not disable it. --- src/lambda_handler.py | 39 +++++++++++++++++++++++++++++++-------- 1 file changed, 31 insertions(+), 8 deletions(-) diff --git a/src/lambda_handler.py b/src/lambda_handler.py index f88f644..65e125b 100644 --- a/src/lambda_handler.py +++ b/src/lambda_handler.py @@ -95,16 +95,39 @@ def task_disable(event): for page in user_paginator.paginate(): for user in page["Users"]: user_name = user["UserName"] + # This value may be None if the user has never logged in. password_last_used = user["PasswordLastUsed"] - logging.debug("Examining user %s's console access", user_name) - - if now - password_last_used > too_old: - logging.info( - "Disabling user %s's console access due to inactivity", user_name - ) - # Disable the user's console access - # iam.delete_login_profile(UserName=user_name) + login_profile = None + try: + login_profile = iam.get_login_profile(UserName=user_name)[ + "LoginProfile" + ] + except boto3.IAM.Client.exceptions.NoSuchEntityException: + logging.debug("User %s does not have console access.", user_name) + + if login_profile: + logging.debug("Examining user %s's console access", user_name) + login_profile_create = login_profile["CreateDate"] + + # This if skips any users that were created recently but have + # not yet logged in. We don't want to disable their access + # yet. + if now - login_profile_create > too_old: + # Disable console access for any users who have never + # logged in or have not logged in sufficiently recently. + if password_last_used is None or now - password_last_used > too_old: + logging.info( + "Disabling user %s's console access due to inactivity", + user_name, + ) + # Disable the user's console access + # iam.delete_login_profile(UserName=user_name) + else: + logging.debug( + "User %s's console access created too recently for inactivity to be determined.", + user_name, + ) # Create a paginator for the current user's access keys access_key_paginator = iam.get_paginator("list_access_keys") From 0834ed5731ae50f643794f6d4f2ce1cb9fc6389c Mon Sep 17 00:00:00 2001 From: Jeremy Frasier Date: Fri, 30 Aug 2024 11:15:23 -0400 Subject: [PATCH 06/28] Handle the case where a user's access key was created recently If a user's access key was created too recently then we cannot yet determine inactivity, so we should not disable it. --- src/lambda_handler.py | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/src/lambda_handler.py b/src/lambda_handler.py index 65e125b..c3bddc0 100644 --- a/src/lambda_handler.py +++ b/src/lambda_handler.py @@ -137,6 +137,9 @@ def task_disable(event): for page in access_key_paginator.paginate(UserName=user_name): for access_key in page["AccessKeyMetadata"]: access_key_id = access_key["AccessKeyId"] + access_key_create = access_key["CreateDate"] + # This value may be None if the user has never used this + # access key. access_key_last_used = iam.get_access_key_last_used( AccessKeyId=access_key_id ) @@ -150,14 +153,29 @@ def task_disable(event): access_key_id, ) - if now - access_key_last_used > too_old: - logging.info( - "Disabling user %s's access key %s due to inactivity", + # This if skips any access keys that were created + # recently but have not yet been used. We don't want + # to disable such keys yet. + if now - access_key_create > too_old: + # Disable access keys that have never been used or + # that have not been used sufficiently recently. + if ( + access_key_last_used is None + or now - access_key_last_used > too_old + ): + logging.info( + "Disabling user %s's access key %s due to inactivity", + user_name, + access_key_id, + ) + # Make the access key inactive + # iam.update_access_key(AccessKeyId=access_key_id, Status="Inactive", UserName=user_name) + else: + logging.debug( + "User %s's access key %s created too recently for inactivity to be determined.", user_name, access_key_id, ) - # Make the access key inactive - # iam.update_access_key(AccessKeyId=access_key_id, Status="Inactive", UserName=user_name) result["message"] = "Successfully disabled access for inactive IAM users." logging.info(result["message"]) From f9d6beac17b467111d4bca3408bb47035ae42f49 Mon Sep 17 00:00:00 2001 From: Jeremy Frasier Date: Fri, 30 Aug 2024 11:33:44 -0400 Subject: [PATCH 07/28] Update Pipfiles (and lock files) for this project --- src/py3.7/Pipfile | 3 +- src/py3.7/Pipfile.lock | 66 +++++++++++++++++++++++++----------------- src/py3.8/Pipfile | 3 +- src/py3.8/Pipfile.lock | 65 ++++++++++++++++++++++++++--------------- src/py3.9/Pipfile | 3 +- src/py3.9/Pipfile.lock | 65 ++++++++++++++++++++++++++--------------- 6 files changed, 125 insertions(+), 80 deletions(-) diff --git a/src/py3.7/Pipfile b/src/py3.7/Pipfile index e49bd0f..00ffed6 100644 --- a/src/py3.7/Pipfile +++ b/src/py3.7/Pipfile @@ -7,5 +7,4 @@ name = "pypi" python_version = "3.7" [packages] -cowsay = "*" -example = {file = "https://github.com/cisagov/skeleton-python-library/archive/v0.1.0.tar.gz"} +boto3 = "*" diff --git a/src/py3.7/Pipfile.lock b/src/py3.7/Pipfile.lock index e14bffd..7b48f1d 100644 --- a/src/py3.7/Pipfile.lock +++ b/src/py3.7/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "fb8e36b2dfbe5f058679f36466257570fc6c43f47b6d060d907073b831843d83" + "sha256": "49efc3368e25cc541579c91c92b08134ed8a23fc9d5c328ef8207674428382f5" }, "pipfile-spec": 6, "requires": { @@ -16,48 +16,62 @@ ] }, "default": { - "contextlib2": { + "boto3": { "hashes": [ - "sha256:3fbdb64466afd23abaf6c977627b75b6139a5a3e8ce38405c5b413aed7a0471f", - "sha256:ab1e2bfe1d01d968e1b7e8d9023bc51ef3509bba217bb730cee3827e1ee82869" + "sha256:07e0f335d801765999da67325455ea8219c1a6d7f06bdaad0975ee505276bcbe", + "sha256:1ee9c52d83e8f4902300e985d62688cf31ca8fc47a80534b4295350ebc418e46" ], - "markers": "python_version >= '3.6'", - "version": "==21.6.0" + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==1.35.9" }, - "cowsay": { + "botocore": { "hashes": [ - "sha256:c00e02444f5bc7332826686bd44d963caabbaba9a804a63153822edce62bbbf3" + "sha256:92962460e4f35d139a23bca28149722030143257ee2916de442243c2464a7434", + "sha256:9e44572fd2401b89dd58bf8b71ac2c36d5b0437f8cbf40de83302c499965fb54" ], - "index": "pypi", - "version": "==5.0" + "markers": "python_version >= '3.8'", + "version": "==1.35.9" }, - "docopt": { + "jmespath": { "hashes": [ - "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491" + "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980", + "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe" ], - "version": "==0.6.2" + "markers": "python_version >= '3.7'", + "version": "==1.0.1" }, - "example": { - "file": "https://github.com/cisagov/skeleton-python-library/archive/v0.1.0.tar.gz", + "python-dateutil": { "hashes": [ - "sha256:d4ae2105b555cb386daf39e06b05594596e881e67faffc46c69d9e7ce56c8c4c" + "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", + "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427" ], - "version": "==0.1.0" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.9.0.post0" }, - "schema": { + "s3transfer": { "hashes": [ - "sha256:f06717112c61895cabc4707752b88716e8420a8819d71404501e114f91043197", - "sha256:f3ffdeeada09ec34bf40d7d79996d9f7175db93b7a5065de0faa7f41083c1e6c" + "sha256:0711534e9356d3cc692fdde846b4a1e4b0cb6519971860796e6bc4c7aea00ef6", + "sha256:eca1c20de70a39daee580aef4986996620f365c4e0fda6a86100231d62f1bf69" ], - "version": "==0.7.5" + "markers": "python_version >= '3.8'", + "version": "==0.10.2" }, - "setuptools": { + "six": { "hashes": [ - "sha256:57f6f22bde4e042978bcd50176fdb381d7c21a9efa4041202288d3737a0c6a54", - "sha256:a7620757bf984b58deaf32fc8a4577a9bbc0850cf92c20e1ce41c38c19e5fb75" + "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", + "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" ], - "markers": "python_version >= '3.7'", - "version": "==65.6.3" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.16.0" + }, + "urllib3": { + "hashes": [ + "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472", + "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168" + ], + "markers": "python_version >= '3.10'", + "version": "==2.2.2" } }, "develop": {} diff --git a/src/py3.8/Pipfile b/src/py3.8/Pipfile index b568dea..c330683 100644 --- a/src/py3.8/Pipfile +++ b/src/py3.8/Pipfile @@ -7,5 +7,4 @@ name = "pypi" python_version = "3.8" [packages] -cowsay = "*" -example = {file = "https://github.com/cisagov/skeleton-python-library/archive/v0.1.0.tar.gz"} +boto3 = "*" diff --git a/src/py3.8/Pipfile.lock b/src/py3.8/Pipfile.lock index 1d146e2..b8c972c 100644 --- a/src/py3.8/Pipfile.lock +++ b/src/py3.8/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "e7d647bd6df129d143384648900fa34961e527257ff08634f04b9badb5fd87f4" + "sha256": "0ba145c19353da73840755ed85984b6653241c800c6ad2c772805a6089dfb424" }, "pipfile-spec": 6, "requires": { @@ -16,45 +16,62 @@ ] }, "default": { - "contextlib2": { + "boto3": { "hashes": [ - "sha256:3fbdb64466afd23abaf6c977627b75b6139a5a3e8ce38405c5b413aed7a0471f", - "sha256:ab1e2bfe1d01d968e1b7e8d9023bc51ef3509bba217bb730cee3827e1ee82869" + "sha256:07e0f335d801765999da67325455ea8219c1a6d7f06bdaad0975ee505276bcbe", + "sha256:1ee9c52d83e8f4902300e985d62688cf31ca8fc47a80534b4295350ebc418e46" ], - "markers": "python_version >= '3.6'", - "version": "==21.6.0" + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==1.35.9" }, - "cowsay": { + "botocore": { "hashes": [ - "sha256:274b1e6fc1b966d53976333eb90ac94cb07a450a700b455af9fbdf882244b30a" + "sha256:92962460e4f35d139a23bca28149722030143257ee2916de442243c2464a7434", + "sha256:9e44572fd2401b89dd58bf8b71ac2c36d5b0437f8cbf40de83302c499965fb54" ], - "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==6.1" + "version": "==1.35.9" }, - "docopt": { + "jmespath": { "hashes": [ - "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491" + "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980", + "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe" ], - "version": "==0.6.2" + "markers": "python_version >= '3.7'", + "version": "==1.0.1" }, - "example": { - "file": "https://github.com/cisagov/skeleton-python-library/archive/v0.1.0.tar.gz" - }, - "schema": { + "python-dateutil": { "hashes": [ - "sha256:f06717112c61895cabc4707752b88716e8420a8819d71404501e114f91043197", - "sha256:f3ffdeeada09ec34bf40d7d79996d9f7175db93b7a5065de0faa7f41083c1e6c" + "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", + "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427" ], - "version": "==0.7.5" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.9.0.post0" }, - "setuptools": { + "s3transfer": { "hashes": [ - "sha256:4ac1475276d2f1c48684874089fefcd83bd7162ddaafb81fac866ba0db282a87", - "sha256:b454a35605876da60632df1a60f736524eb73cc47bbc9f3f1ef1b644de74fd2a" + "sha256:0711534e9356d3cc692fdde846b4a1e4b0cb6519971860796e6bc4c7aea00ef6", + "sha256:eca1c20de70a39daee580aef4986996620f365c4e0fda6a86100231d62f1bf69" ], "markers": "python_version >= '3.8'", - "version": "==68.2.2" + "version": "==0.10.2" + }, + "six": { + "hashes": [ + "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", + "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.16.0" + }, + "urllib3": { + "hashes": [ + "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472", + "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168" + ], + "markers": "python_version >= '3.10'", + "version": "==2.2.2" } }, "develop": {} diff --git a/src/py3.9/Pipfile b/src/py3.9/Pipfile index 0e08b63..354d8ce 100644 --- a/src/py3.9/Pipfile +++ b/src/py3.9/Pipfile @@ -7,5 +7,4 @@ name = "pypi" python_version = "3.9" [packages] -cowsay = "*" -example = {file = "https://github.com/cisagov/skeleton-python-library/archive/v0.1.0.tar.gz"} +boto3 = "*" diff --git a/src/py3.9/Pipfile.lock b/src/py3.9/Pipfile.lock index 7a12085..e04aefe 100644 --- a/src/py3.9/Pipfile.lock +++ b/src/py3.9/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "0917b51635aa96ace981678b22ea817619fd4113a78afe4c2935281268631b7e" + "sha256": "35b63359d999438d3e907ecb6ace25d1308a41b2a143969981cb78334ef592ec" }, "pipfile-spec": 6, "requires": { @@ -16,45 +16,62 @@ ] }, "default": { - "contextlib2": { + "boto3": { "hashes": [ - "sha256:3fbdb64466afd23abaf6c977627b75b6139a5a3e8ce38405c5b413aed7a0471f", - "sha256:ab1e2bfe1d01d968e1b7e8d9023bc51ef3509bba217bb730cee3827e1ee82869" + "sha256:07e0f335d801765999da67325455ea8219c1a6d7f06bdaad0975ee505276bcbe", + "sha256:1ee9c52d83e8f4902300e985d62688cf31ca8fc47a80534b4295350ebc418e46" ], - "markers": "python_version >= '3.6'", - "version": "==21.6.0" + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==1.35.9" }, - "cowsay": { + "botocore": { "hashes": [ - "sha256:274b1e6fc1b966d53976333eb90ac94cb07a450a700b455af9fbdf882244b30a" + "sha256:92962460e4f35d139a23bca28149722030143257ee2916de442243c2464a7434", + "sha256:9e44572fd2401b89dd58bf8b71ac2c36d5b0437f8cbf40de83302c499965fb54" ], - "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==6.1" + "version": "==1.35.9" }, - "docopt": { + "jmespath": { "hashes": [ - "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491" + "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980", + "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe" ], - "version": "==0.6.2" + "markers": "python_version >= '3.7'", + "version": "==1.0.1" }, - "example": { - "file": "https://github.com/cisagov/skeleton-python-library/archive/v0.1.0.tar.gz" - }, - "schema": { + "python-dateutil": { "hashes": [ - "sha256:f06717112c61895cabc4707752b88716e8420a8819d71404501e114f91043197", - "sha256:f3ffdeeada09ec34bf40d7d79996d9f7175db93b7a5065de0faa7f41083c1e6c" + "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", + "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427" ], - "version": "==0.7.5" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.9.0.post0" }, - "setuptools": { + "s3transfer": { "hashes": [ - "sha256:4ac1475276d2f1c48684874089fefcd83bd7162ddaafb81fac866ba0db282a87", - "sha256:b454a35605876da60632df1a60f736524eb73cc47bbc9f3f1ef1b644de74fd2a" + "sha256:0711534e9356d3cc692fdde846b4a1e4b0cb6519971860796e6bc4c7aea00ef6", + "sha256:eca1c20de70a39daee580aef4986996620f365c4e0fda6a86100231d62f1bf69" ], "markers": "python_version >= '3.8'", - "version": "==68.2.2" + "version": "==0.10.2" + }, + "six": { + "hashes": [ + "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", + "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.16.0" + }, + "urllib3": { + "hashes": [ + "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472", + "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168" + ], + "markers": "python_version >= '3.10'", + "version": "==2.2.2" } }, "develop": {} From 8142ee4f862e7ad40abd555c09d233ac0cfaad39 Mon Sep 17 00:00:00 2001 From: Jeremy Frasier Date: Fri, 30 Aug 2024 11:34:21 -0400 Subject: [PATCH 08/28] Update Docker composition to match the needs of this project I also removed the run_lambda_locally section as it will not be used with this project. --- docker-compose.yml | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 6e46434..5cea940 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,7 +6,7 @@ services: build: . # This uses the value of the LAMBDA_TAG environment variable from # the invoking environment but falls back to a default value. - image: cisagov/example_lambda:${LAMBDA_TAG:-latest} + image: cisagov/disable_inactive_iam_users_lambda:${LAMBDA_TAG:-latest} entrypoint: /opt/build_artifact.sh environment: # This uses the value of the BUILD_FILE_NAME environment variable @@ -15,10 +15,3 @@ services: volumes: - ./src/build_artifact.sh:/opt/build_artifact.sh - .:/var/task/output - run_lambda_locally: - build: . - # This uses the value of the LAMBDA_TAG environment variable from - # the invoking environment but falls back to a default value. - image: cisagov/example_lambda:${LAMBDA_TAG:-latest} - ports: - - "9000:8080" From f470e94429ffd296075c54bda5c0000429a46ecc Mon Sep 17 00:00:00 2001 From: Jeremy Frasier Date: Fri, 30 Aug 2024 11:52:58 -0400 Subject: [PATCH 09/28] Correctly use boto3 IAM client exception class --- src/lambda_handler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lambda_handler.py b/src/lambda_handler.py index c3bddc0..8d00ab6 100644 --- a/src/lambda_handler.py +++ b/src/lambda_handler.py @@ -103,7 +103,7 @@ def task_disable(event): login_profile = iam.get_login_profile(UserName=user_name)[ "LoginProfile" ] - except boto3.IAM.Client.exceptions.NoSuchEntityException: + except iam.exceptions.NoSuchEntityException: logging.debug("User %s does not have console access.", user_name) if login_profile: From 8c9da654a839256473fc77a37fbe7adfed5c5c49 Mon Sep 17 00:00:00 2001 From: Jeremy Frasier Date: Fri, 30 Aug 2024 12:18:28 -0400 Subject: [PATCH 10/28] Make now a timezone-aware datetime object Otherwise we cannot perform arithmetic such as now - login_profile_create, since the latter is timezone-aware. --- src/lambda_handler.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/lambda_handler.py b/src/lambda_handler.py index 8d00ab6..6a4a446 100644 --- a/src/lambda_handler.py +++ b/src/lambda_handler.py @@ -84,8 +84,12 @@ def task_disable(event): # Create an IAM client iam: boto3.client = boto3.client("iam") - # Capture the current time and date - now = datetime.now() + # Capture the current time and date. + # + # Note that Python requires a little trickery to create a datetime object + # with the current time _and_ the timezone information set to the local + # system timezone. + now = datetime.now(timezone.utc).astimezone() too_old = timedelta(days=expiration_days) # Create a paginator for users From a3b54ecb5f6b78f2a8b8c244f5a7e6c5dc383b15 Mon Sep 17 00:00:00 2001 From: Jeremy Frasier Date: Fri, 30 Aug 2024 12:21:49 -0400 Subject: [PATCH 11/28] Remove boto3 from the Pipfiles This package is already available to Lambda functions. --- src/py3.7/Pipfile | 1 - src/py3.7/Pipfile.lock | 62 ++---------------------------------------- src/py3.8/Pipfile | 1 - src/py3.8/Pipfile.lock | 62 ++---------------------------------------- src/py3.9/Pipfile | 1 - src/py3.9/Pipfile.lock | 62 ++---------------------------------------- 6 files changed, 6 insertions(+), 183 deletions(-) diff --git a/src/py3.7/Pipfile b/src/py3.7/Pipfile index 00ffed6..569b1f2 100644 --- a/src/py3.7/Pipfile +++ b/src/py3.7/Pipfile @@ -7,4 +7,3 @@ name = "pypi" python_version = "3.7" [packages] -boto3 = "*" diff --git a/src/py3.7/Pipfile.lock b/src/py3.7/Pipfile.lock index 7b48f1d..30e2e81 100644 --- a/src/py3.7/Pipfile.lock +++ b/src/py3.7/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "49efc3368e25cc541579c91c92b08134ed8a23fc9d5c328ef8207674428382f5" + "sha256": "7e7ef69da7248742e869378f8421880cf8f0017f96d94d086813baa518a65489" }, "pipfile-spec": 6, "requires": { @@ -15,64 +15,6 @@ } ] }, - "default": { - "boto3": { - "hashes": [ - "sha256:07e0f335d801765999da67325455ea8219c1a6d7f06bdaad0975ee505276bcbe", - "sha256:1ee9c52d83e8f4902300e985d62688cf31ca8fc47a80534b4295350ebc418e46" - ], - "index": "pypi", - "markers": "python_version >= '3.8'", - "version": "==1.35.9" - }, - "botocore": { - "hashes": [ - "sha256:92962460e4f35d139a23bca28149722030143257ee2916de442243c2464a7434", - "sha256:9e44572fd2401b89dd58bf8b71ac2c36d5b0437f8cbf40de83302c499965fb54" - ], - "markers": "python_version >= '3.8'", - "version": "==1.35.9" - }, - "jmespath": { - "hashes": [ - "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980", - "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe" - ], - "markers": "python_version >= '3.7'", - "version": "==1.0.1" - }, - "python-dateutil": { - "hashes": [ - "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", - "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.9.0.post0" - }, - "s3transfer": { - "hashes": [ - "sha256:0711534e9356d3cc692fdde846b4a1e4b0cb6519971860796e6bc4c7aea00ef6", - "sha256:eca1c20de70a39daee580aef4986996620f365c4e0fda6a86100231d62f1bf69" - ], - "markers": "python_version >= '3.8'", - "version": "==0.10.2" - }, - "six": { - "hashes": [ - "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", - "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.16.0" - }, - "urllib3": { - "hashes": [ - "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472", - "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168" - ], - "markers": "python_version >= '3.10'", - "version": "==2.2.2" - } - }, + "default": {}, "develop": {} } diff --git a/src/py3.8/Pipfile b/src/py3.8/Pipfile index c330683..4cf15c8 100644 --- a/src/py3.8/Pipfile +++ b/src/py3.8/Pipfile @@ -7,4 +7,3 @@ name = "pypi" python_version = "3.8" [packages] -boto3 = "*" diff --git a/src/py3.8/Pipfile.lock b/src/py3.8/Pipfile.lock index b8c972c..dc24d3e 100644 --- a/src/py3.8/Pipfile.lock +++ b/src/py3.8/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "0ba145c19353da73840755ed85984b6653241c800c6ad2c772805a6089dfb424" + "sha256": "7f7606f08e0544d8d012ef4d097dabdd6df6843a28793eb6551245d4b2db4242" }, "pipfile-spec": 6, "requires": { @@ -15,64 +15,6 @@ } ] }, - "default": { - "boto3": { - "hashes": [ - "sha256:07e0f335d801765999da67325455ea8219c1a6d7f06bdaad0975ee505276bcbe", - "sha256:1ee9c52d83e8f4902300e985d62688cf31ca8fc47a80534b4295350ebc418e46" - ], - "index": "pypi", - "markers": "python_version >= '3.8'", - "version": "==1.35.9" - }, - "botocore": { - "hashes": [ - "sha256:92962460e4f35d139a23bca28149722030143257ee2916de442243c2464a7434", - "sha256:9e44572fd2401b89dd58bf8b71ac2c36d5b0437f8cbf40de83302c499965fb54" - ], - "markers": "python_version >= '3.8'", - "version": "==1.35.9" - }, - "jmespath": { - "hashes": [ - "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980", - "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe" - ], - "markers": "python_version >= '3.7'", - "version": "==1.0.1" - }, - "python-dateutil": { - "hashes": [ - "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", - "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.9.0.post0" - }, - "s3transfer": { - "hashes": [ - "sha256:0711534e9356d3cc692fdde846b4a1e4b0cb6519971860796e6bc4c7aea00ef6", - "sha256:eca1c20de70a39daee580aef4986996620f365c4e0fda6a86100231d62f1bf69" - ], - "markers": "python_version >= '3.8'", - "version": "==0.10.2" - }, - "six": { - "hashes": [ - "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", - "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.16.0" - }, - "urllib3": { - "hashes": [ - "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472", - "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168" - ], - "markers": "python_version >= '3.10'", - "version": "==2.2.2" - } - }, + "default": {}, "develop": {} } diff --git a/src/py3.9/Pipfile b/src/py3.9/Pipfile index 354d8ce..3d4aa4b 100644 --- a/src/py3.9/Pipfile +++ b/src/py3.9/Pipfile @@ -7,4 +7,3 @@ name = "pypi" python_version = "3.9" [packages] -boto3 = "*" diff --git a/src/py3.9/Pipfile.lock b/src/py3.9/Pipfile.lock index e04aefe..f93f953 100644 --- a/src/py3.9/Pipfile.lock +++ b/src/py3.9/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "35b63359d999438d3e907ecb6ace25d1308a41b2a143969981cb78334ef592ec" + "sha256": "a36a5392bb1e8bbc06bfaa0761e52593cf2d83b486696bf54667ba8da616c839" }, "pipfile-spec": 6, "requires": { @@ -15,64 +15,6 @@ } ] }, - "default": { - "boto3": { - "hashes": [ - "sha256:07e0f335d801765999da67325455ea8219c1a6d7f06bdaad0975ee505276bcbe", - "sha256:1ee9c52d83e8f4902300e985d62688cf31ca8fc47a80534b4295350ebc418e46" - ], - "index": "pypi", - "markers": "python_version >= '3.8'", - "version": "==1.35.9" - }, - "botocore": { - "hashes": [ - "sha256:92962460e4f35d139a23bca28149722030143257ee2916de442243c2464a7434", - "sha256:9e44572fd2401b89dd58bf8b71ac2c36d5b0437f8cbf40de83302c499965fb54" - ], - "markers": "python_version >= '3.8'", - "version": "==1.35.9" - }, - "jmespath": { - "hashes": [ - "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980", - "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe" - ], - "markers": "python_version >= '3.7'", - "version": "==1.0.1" - }, - "python-dateutil": { - "hashes": [ - "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", - "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.9.0.post0" - }, - "s3transfer": { - "hashes": [ - "sha256:0711534e9356d3cc692fdde846b4a1e4b0cb6519971860796e6bc4c7aea00ef6", - "sha256:eca1c20de70a39daee580aef4986996620f365c4e0fda6a86100231d62f1bf69" - ], - "markers": "python_version >= '3.8'", - "version": "==0.10.2" - }, - "six": { - "hashes": [ - "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", - "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.16.0" - }, - "urllib3": { - "hashes": [ - "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472", - "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168" - ], - "markers": "python_version >= '3.10'", - "version": "==2.2.2" - } - }, + "default": {}, "develop": {} } From 1236fea4d10203ccc2331a96f1ee8c6c6cd5c19b Mon Sep 17 00:00:00 2001 From: Jeremy Frasier Date: Fri, 30 Aug 2024 12:28:28 -0400 Subject: [PATCH 12/28] Correctly extract the last used date from the get_access_key_last_used response --- src/lambda_handler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lambda_handler.py b/src/lambda_handler.py index 6a4a446..07fd840 100644 --- a/src/lambda_handler.py +++ b/src/lambda_handler.py @@ -146,7 +146,7 @@ def task_disable(event): # access key. access_key_last_used = iam.get_access_key_last_used( AccessKeyId=access_key_id - ) + )["AccessKeyLastUsed"]["LastUsedDate"] # We can safely ignore any access keys that are already # inactive From deaedec0c6683e1f0a14659817fa02729e0b7a7c Mon Sep 17 00:00:00 2001 From: Jeremy Frasier Date: Fri, 30 Aug 2024 12:33:35 -0400 Subject: [PATCH 13/28] Correctly handle the case where the user has never used his or her console access In such a case the "PasswordLastUsed" key does not exist for the user. --- src/lambda_handler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lambda_handler.py b/src/lambda_handler.py index 07fd840..fa7dcd4 100644 --- a/src/lambda_handler.py +++ b/src/lambda_handler.py @@ -100,7 +100,7 @@ def task_disable(event): for user in page["Users"]: user_name = user["UserName"] # This value may be None if the user has never logged in. - password_last_used = user["PasswordLastUsed"] + password_last_used = user.get("PasswordLastUsed", None) login_profile = None try: From d17992fa1ed85d7dbe9a133f5b66c0e53344051f Mon Sep 17 00:00:00 2001 From: Jeremy Frasier Date: Fri, 30 Aug 2024 12:42:17 -0400 Subject: [PATCH 14/28] Add a debug message for the case where an access key is already inactive --- src/lambda_handler.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/lambda_handler.py b/src/lambda_handler.py index fa7dcd4..9fb7182 100644 --- a/src/lambda_handler.py +++ b/src/lambda_handler.py @@ -180,6 +180,12 @@ def task_disable(event): user_name, access_key_id, ) + else: + logging.debug( + "User %s's access key %s is already inactive.", + user_name, + access_key_id, + ) result["message"] = "Successfully disabled access for inactive IAM users." logging.info(result["message"]) From bba9e3bf8a6412b4daba706ddd6af975ba7afb38 Mon Sep 17 00:00:00 2001 From: Jeremy Frasier Date: Fri, 30 Aug 2024 13:27:05 -0400 Subject: [PATCH 15/28] Word-wrap paragraph to get rid of linting error --- CONTRIBUTING.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0ab2ea8..a78ef60 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -25,12 +25,12 @@ one. ## Pull requests ## If you choose to [submit a pull -request](https://github.com/cisagov/disable-inactive-iam-users-lambda/pulls), you will -notice that our continuous integration (CI) system runs a fairly -extensive set of linters and syntax checkers. Your pull request may -fail these checks, and that's OK. If you want you can stop there and -wait for us to make the necessary corrections to ensure your code -passes the CI checks. +request](https://github.com/cisagov/disable-inactive-iam-users-lambda/pulls), +you will notice that our continuous integration (CI) system runs a +fairly extensive set of linters and syntax checkers. Your pull +request may fail these checks, and that's OK. If you want you can +stop there and wait for us to make the necessary corrections to ensure +your code passes the CI checks. If you want to make the changes yourself, or if you want to become a regular contributor, then you will want to set up From aabbede72c93da51e94c2290139088c086716947 Mon Sep 17 00:00:00 2001 From: Jeremy Frasier Date: Fri, 30 Aug 2024 13:35:50 -0400 Subject: [PATCH 16/28] Correct reference to repo that does not exist --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index bd1a7f0..e24bae7 100644 --- a/README.md +++ b/README.md @@ -96,7 +96,7 @@ Lambda: The easiest way to deploy the Lambda and related resources is to use the -[cisagov/disable-inactive-iam-users-terraform](https://github.com/cisagov/disable-inactive-iam-users-terraform) +[cisagov/disable-inactive-iam-users-tf-module](https://github.com/cisagov/disable-inactive-iam-users-tf-module) repository. Refer to the documentation in that project for more information. From 1a2521a65a6f9db9318806272468f369ede872de Mon Sep 17 00:00:00 2001 From: Jeremy Frasier Date: Fri, 30 Aug 2024 14:04:53 -0400 Subject: [PATCH 17/28] Uncomment code that actually does the disabling --- src/lambda_handler.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/lambda_handler.py b/src/lambda_handler.py index 9fb7182..09c88da 100644 --- a/src/lambda_handler.py +++ b/src/lambda_handler.py @@ -126,7 +126,7 @@ def task_disable(event): user_name, ) # Disable the user's console access - # iam.delete_login_profile(UserName=user_name) + iam.delete_login_profile(UserName=user_name) else: logging.debug( "User %s's console access created too recently for inactivity to be determined.", @@ -173,7 +173,11 @@ def task_disable(event): access_key_id, ) # Make the access key inactive - # iam.update_access_key(AccessKeyId=access_key_id, Status="Inactive", UserName=user_name) + iam.update_access_key( + AccessKeyId=access_key_id, + Status="Inactive", + UserName=user_name, + ) else: logging.debug( "User %s's access key %s created too recently for inactivity to be determined.", From 96a0f518fa4df8936c127b51ee44bf5334b15f3a Mon Sep 17 00:00:00 2001 From: Jeremy Frasier Date: Fri, 30 Aug 2024 15:13:46 -0400 Subject: [PATCH 18/28] Bump version from 0.0.2 to 1.0.0 --- src/version.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/version.txt b/src/version.txt index 3b93d0b..5becc17 100644 --- a/src/version.txt +++ b/src/version.txt @@ -1 +1 @@ -__version__ = "0.0.2" +__version__ = "1.0.0" From e3ecd04e7f68497fb42d129012ee4b76d6ef91ba Mon Sep 17 00:00:00 2001 From: Jeremy Frasier Date: Fri, 30 Aug 2024 15:13:52 -0400 Subject: [PATCH 19/28] Bump version from 1.0.0 to 1.0.0-rc.1 --- src/version.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/version.txt b/src/version.txt index 5becc17..ee6dec6 100644 --- a/src/version.txt +++ b/src/version.txt @@ -1 +1 @@ -__version__ = "1.0.0" +__version__ = "1.0.0-rc.1" From b6b80d6b3e18aafba3c407da9bc3db85c2c240cb Mon Sep 17 00:00:00 2001 From: Jeremy Frasier Date: Fri, 30 Aug 2024 15:16:56 -0400 Subject: [PATCH 20/28] Set author label in Dockerfile to the VM dev distro --- Dockerfile | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/Dockerfile b/Dockerfile index dc501b9..0d7eda7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -31,13 +31,7 @@ FROM amazon/aws-lambda-python:$PY_VERSION as build-stage # For a list of pre-defined annotation keys and value types see: # https://github.com/opencontainers/image-spec/blob/master/annotations.md ### -# github@cisa.dhs.gov is a very generic email distribution, and it is -# unlikely that anyone on that distribution is familiar with the -# particulars of your repository. It is therefore *strongly* -# suggested that you use an email address here that is specific to the -# person or group that maintains this repository; for example: -# LABEL org.opencontainers.image.authors="vm-fusion-dev-group@trio.dhs.gov" -LABEL org.opencontainers.image.authors="github@cisa.dhs.gov" +LABEL org.opencontainers.image.authors="vm-dev@gwe.cisa.dhs.gov" LABEL org.opencontainers.image.vendor="Cybersecurity and Infrastructure Security Agency" # Declare it a third time so it's brought into this scope. From 78a4470c6442006784eb5139de117ebca2171014 Mon Sep 17 00:00:00 2001 From: Shane Frasier Date: Fri, 30 Aug 2024 16:05:43 -0400 Subject: [PATCH 21/28] Fix copy-and-paste errors Co-authored-by: dav3r --- src/lambda_handler.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/lambda_handler.py b/src/lambda_handler.py index 09c88da..9728999 100644 --- a/src/lambda_handler.py +++ b/src/lambda_handler.py @@ -50,14 +50,14 @@ def validate_event_data(event: Dict[str, Any]) -> EventValidation: if "expiration_days" not in event: errors.append('Missing required key "expiration_days" in event.') elif not event["expiration_days"]: - errors.append('"account_ids" must be non-null.') + errors.append('"expiration_days" must be non-null.') else: try: tmp = int(event["expiration_days"]) if tmp <= 0: - errors.append('"account_ids" must be a strictly positive integer.') + errors.append('"expiration_days" must be a strictly positive integer.') except ValueError: - errors.append('"account_ids" must be an integer.') + errors.append('"expiration_days" must be an integer.') if errors: result = False From 469a51f7b2a31a15266cd6e5e9583145d4370559 Mon Sep 17 00:00:00 2001 From: Jeremy Frasier Date: Mon, 2 Sep 2024 13:25:28 -0400 Subject: [PATCH 22/28] Uncomment Dependabot ignore lines Also correct repo reference in comment. Co-authored-by: Nick <50747025+mcdonnnj@users.noreply.github.com> --- .github/dependabot.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index bc9df56..2031641 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -19,9 +19,9 @@ updates: - dependency-name: hashicorp/setup-terraform - dependency-name: mxschmitt/action-tmate - dependency-name: step-security/harden-runner - # # Managed by cisagov/disable-inactive-iam-users-lambda - # - dependency-name: actions/upload-artifact - # - dependency-name: github/codeql-action + # Managed by cisagov/skeleton-aws-lambda-python + - dependency-name: actions/upload-artifact + - dependency-name: github/codeql-action package-ecosystem: github-actions schedule: interval: weekly From 1e4f91803d1552cade7a5a0d18c65aca2f6a3f3d Mon Sep 17 00:00:00 2001 From: Jeremy Frasier Date: Tue, 3 Sep 2024 09:08:22 -0400 Subject: [PATCH 23/28] Simplify timestamp logic --- src/lambda_handler.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/lambda_handler.py b/src/lambda_handler.py index 9728999..8787709 100644 --- a/src/lambda_handler.py +++ b/src/lambda_handler.py @@ -84,13 +84,13 @@ def task_disable(event): # Create an IAM client iam: boto3.client = boto3.client("iam") - # Capture the current time and date. + # Determine the cutoff. Any access not used after this datetime will be + # disabled. # # Note that Python requires a little trickery to create a datetime object # with the current time _and_ the timezone information set to the local # system timezone. - now = datetime.now(timezone.utc).astimezone() - too_old = timedelta(days=expiration_days) + cutoff = datetime.now(timezone.utc).astimezone() - timedelta(days=expiration_days) # Create a paginator for users user_paginator = iam.get_paginator("list_users") @@ -117,10 +117,10 @@ def task_disable(event): # This if skips any users that were created recently but have # not yet logged in. We don't want to disable their access # yet. - if now - login_profile_create > too_old: + if login_profile_create < cutoff: # Disable console access for any users who have never # logged in or have not logged in sufficiently recently. - if password_last_used is None or now - password_last_used > too_old: + if password_last_used is None or password_last_used < cutoff: logging.info( "Disabling user %s's console access due to inactivity", user_name, @@ -160,12 +160,12 @@ def task_disable(event): # This if skips any access keys that were created # recently but have not yet been used. We don't want # to disable such keys yet. - if now - access_key_create > too_old: + if access_key_create < cutoff: # Disable access keys that have never been used or # that have not been used sufficiently recently. if ( access_key_last_used is None - or now - access_key_last_used > too_old + or access_key_last_used < cutoff ): logging.info( "Disabling user %s's access key %s due to inactivity", From df9e9e2da3c1a666a0b8b0d122ce2ab50c65cf26 Mon Sep 17 00:00:00 2001 From: Jeremy Frasier Date: Tue, 3 Sep 2024 09:52:23 -0400 Subject: [PATCH 24/28] Add pydoc for task_disable() parameters --- src/lambda_handler.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/lambda_handler.py b/src/lambda_handler.py index 8787709..cd539eb 100644 --- a/src/lambda_handler.py +++ b/src/lambda_handler.py @@ -66,7 +66,12 @@ def validate_event_data(event: Dict[str, Any]) -> EventValidation: def task_disable(event): - """Iterate over users and disable any inactive access.""" + """Iterate over users and disable any inactive access. + + :param event: The event dict that contains the parameters sent when + the function is invoked. + :return: The result of the action. + """ result: Dict[str, Union[Optional[str], bool]] = {"message": None, "success": True} # Validate all event data before going any further From 15da8c670ba095db39f885c71306dd3b98b0ccac Mon Sep 17 00:00:00 2001 From: Jeremy Frasier Date: Wed, 4 Sep 2024 09:07:39 -0400 Subject: [PATCH 25/28] Bump version from 1.0.0-rc.1 to 1.0.0-rc.2 --- src/version.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/version.txt b/src/version.txt index ee6dec6..1acb7e3 100644 --- a/src/version.txt +++ b/src/version.txt @@ -1 +1 @@ -__version__ = "1.0.0-rc.1" +__version__ = "1.0.0-rc.2" From 311f8b8d2ea7997042970e4b1d025173f49ac28a Mon Sep 17 00:00:00 2001 From: Jeremy Frasier Date: Wed, 4 Sep 2024 14:06:17 -0400 Subject: [PATCH 26/28] Add support for Python 3.10 and remove support for older versions of Python This is the latest Python runtime we can currently support, given our AWS Terraform provider constraints. Co-authored-by: Dave Redmin --- Dockerfile | 6 +++--- src/{py3.7 => py3.10}/Pipfile | 2 +- src/{py3.9 => py3.10}/Pipfile.lock | 4 ++-- src/py3.7/Pipfile.lock | 20 -------------------- src/py3.8/Pipfile | 9 --------- src/py3.8/Pipfile.lock | 20 -------------------- src/py3.9/Pipfile | 9 --------- 7 files changed, 6 insertions(+), 64 deletions(-) rename src/{py3.7 => py3.10}/Pipfile (80%) rename src/{py3.9 => py3.10}/Pipfile.lock (69%) delete mode 100644 src/py3.7/Pipfile.lock delete mode 100644 src/py3.8/Pipfile delete mode 100644 src/py3.8/Pipfile.lock delete mode 100644 src/py3.9/Pipfile diff --git a/Dockerfile b/Dockerfile index 0d7eda7..14fd7cc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,9 +1,9 @@ -ARG PY_VERSION=3.9 +ARG PY_VERSION=3.10 FROM amazon/aws-lambda-python:$PY_VERSION as install-stage # Declare it a second time so it's brought into this scope. -ARG PY_VERSION=3.9 +ARG PY_VERSION=3.10 # Install the Python packages necessary to install the Lambda dependencies. RUN python3 -m pip install --no-cache-dir \ @@ -35,7 +35,7 @@ LABEL org.opencontainers.image.authors="vm-dev@gwe.cisa.dhs.gov" LABEL org.opencontainers.image.vendor="Cybersecurity and Infrastructure Security Agency" # Declare it a third time so it's brought into this scope. -ARG PY_VERSION=3.9 +ARG PY_VERSION=3.10 # This must be present in the image to generate a deployment artifact. ENV BUILD_PY_VERSION=$PY_VERSION diff --git a/src/py3.7/Pipfile b/src/py3.10/Pipfile similarity index 80% rename from src/py3.7/Pipfile rename to src/py3.10/Pipfile index 569b1f2..cf9d16a 100644 --- a/src/py3.7/Pipfile +++ b/src/py3.10/Pipfile @@ -4,6 +4,6 @@ verify_ssl = true name = "pypi" [requires] -python_version = "3.7" +python_version = "3.10" [packages] diff --git a/src/py3.9/Pipfile.lock b/src/py3.10/Pipfile.lock similarity index 69% rename from src/py3.9/Pipfile.lock rename to src/py3.10/Pipfile.lock index f93f953..7c5f2dd 100644 --- a/src/py3.9/Pipfile.lock +++ b/src/py3.10/Pipfile.lock @@ -1,11 +1,11 @@ { "_meta": { "hash": { - "sha256": "a36a5392bb1e8bbc06bfaa0761e52593cf2d83b486696bf54667ba8da616c839" + "sha256": "fedbd2ab7afd84cf16f128af0619749267b62277b4cb6989ef16d4bef6e4eef2" }, "pipfile-spec": 6, "requires": { - "python_version": "3.9" + "python_version": "3.10" }, "sources": [ { diff --git a/src/py3.7/Pipfile.lock b/src/py3.7/Pipfile.lock deleted file mode 100644 index 30e2e81..0000000 --- a/src/py3.7/Pipfile.lock +++ /dev/null @@ -1,20 +0,0 @@ -{ - "_meta": { - "hash": { - "sha256": "7e7ef69da7248742e869378f8421880cf8f0017f96d94d086813baa518a65489" - }, - "pipfile-spec": 6, - "requires": { - "python_version": "3.7" - }, - "sources": [ - { - "name": "pypi", - "url": "https://pypi.org/simple", - "verify_ssl": true - } - ] - }, - "default": {}, - "develop": {} -} diff --git a/src/py3.8/Pipfile b/src/py3.8/Pipfile deleted file mode 100644 index 4cf15c8..0000000 --- a/src/py3.8/Pipfile +++ /dev/null @@ -1,9 +0,0 @@ -[[source]] -url = "https://pypi.org/simple" -verify_ssl = true -name = "pypi" - -[requires] -python_version = "3.8" - -[packages] diff --git a/src/py3.8/Pipfile.lock b/src/py3.8/Pipfile.lock deleted file mode 100644 index dc24d3e..0000000 --- a/src/py3.8/Pipfile.lock +++ /dev/null @@ -1,20 +0,0 @@ -{ - "_meta": { - "hash": { - "sha256": "7f7606f08e0544d8d012ef4d097dabdd6df6843a28793eb6551245d4b2db4242" - }, - "pipfile-spec": 6, - "requires": { - "python_version": "3.8" - }, - "sources": [ - { - "name": "pypi", - "url": "https://pypi.org/simple", - "verify_ssl": true - } - ] - }, - "default": {}, - "develop": {} -} diff --git a/src/py3.9/Pipfile b/src/py3.9/Pipfile deleted file mode 100644 index 3d4aa4b..0000000 --- a/src/py3.9/Pipfile +++ /dev/null @@ -1,9 +0,0 @@ -[[source]] -url = "https://pypi.org/simple" -verify_ssl = true -name = "pypi" - -[requires] -python_version = "3.9" - -[packages] From 033615575f325d3d0f46c84539dbed09f1bea4fa Mon Sep 17 00:00:00 2001 From: Jeremy Frasier Date: Wed, 4 Sep 2024 14:55:30 -0400 Subject: [PATCH 27/28] Update workflow to only support Python 3.10 Lambda runtime --- .github/workflows/build.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3f15c98..e3dd05b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -219,9 +219,7 @@ jobs: matrix: # Python runtime versions supported by AWS python-version: - - "3.7" - - "3.8" - - "3.9" + - "3.10" steps: - id: harden-runner name: Harden the runner From 28f2b82849bfebce58ad5dcfc5adb6e560bd3eae Mon Sep 17 00:00:00 2001 From: Jeremy Frasier Date: Thu, 5 Sep 2024 13:28:11 -0400 Subject: [PATCH 28/28] Finalize version from 1.0.0-rc.2 to 1.0.0 --- src/version.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/version.txt b/src/version.txt index 1acb7e3..5becc17 100644 --- a/src/version.txt +++ b/src/version.txt @@ -1 +1 @@ -__version__ = "1.0.0-rc.2" +__version__ = "1.0.0"