diff --git a/cartography/data/jobs/cleanup/aws_import_account_access_key_cleanup.json b/cartography/data/jobs/cleanup/aws_import_account_access_key_cleanup.json index 01ad012fb..83b53839e 100644 --- a/cartography/data/jobs/cleanup/aws_import_account_access_key_cleanup.json +++ b/cartography/data/jobs/cleanup/aws_import_account_access_key_cleanup.json @@ -1,8 +1,17 @@ { - "statements": [{ - "query": "MATCH (n:AccountAccessKey)<-[:AWS_ACCESS_KEY]-(:AWSUser)<-[:RESOURCE]-(:AWSAccount{id: $AWS_ID}) WHERE n.lastupdated <> $UPDATE_TAG WITH n LIMIT $LIMIT_SIZE DETACH DELETE (n)", - "iterative": true, - "iterationsize": 100 - }], + "statements": [ + { + "query": "MATCH (n:AccountAccessKey)<-[:AWS_ACCESS_KEY]-(:AWSUser)<-[:RESOURCE]-(:AWSAccount{id: $AWS_ID}) WHERE n.lastupdated <> $UPDATE_TAG WITH n LIMIT $LIMIT_SIZE DETACH DELETE (n)", + "iterative": true, + "iterationsize": 100, + "__comment__": "cleanup access keys that are attached to users" + }, + { + "query": "MATCH (n:AccountAccessKey) WHERE NOT (n)<-[:AWS_ACCESS_KEY]-(:AWSUser) AND n.lastupdated <> $UPDATE_TAG WITH n LIMIT $LIMIT_SIZE DETACH DELETE (n)", + "iterative": true, + "iterationsize": 100, + "__comment__": "cleanup access keys that no longer attached to users, such as when a user no longer exists" + } + ], "name": "cleanup AccountAccessKey" } diff --git a/cartography/intel/aws/iam.py b/cartography/intel/aws/iam.py index 78158bc07..382e0801c 100644 --- a/cartography/intel/aws/iam.py +++ b/cartography/intel/aws/iam.py @@ -227,6 +227,16 @@ def get_account_access_key_data(boto3_session: boto3.session.Session, username: logger.warning( f"Could not get access key for user {username} due to NoSuchEntityException; skipping.", ) + for access_key in access_keys['AccessKeyMetadata']: + access_key_id = access_key['AccessKeyId'] + last_used_info = client.get_access_key_last_used( + AccessKeyId=access_key_id, + )['AccessKeyLastUsed'] + # only LastUsedDate may be null + # https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/iam/client/get_access_key_last_used.html + access_key['LastUsedDate'] = last_used_info.get('LastUsedDate') + access_key['LastUsedService'] = last_used_info['ServiceName'] + access_key['LastUsedRegion'] = last_used_info['Region'] return access_keys @@ -490,7 +500,11 @@ def load_user_access_keys(neo4j_session: neo4j.Session, user_access_keys: Dict, WITH user MERGE (key:AccountAccessKey{accesskeyid: $AccessKeyId}) ON CREATE SET key.firstseen = timestamp(), key.createdate = $CreateDate - SET key.status = $Status, key.lastupdated = $aws_update_tag + SET key.status = $Status, + key.lastupdated = $aws_update_tag, + key.lastuseddate = $LastUsedDate, + key.lastusedservice = $LastUsedService, + key.lastusedregion = $LastUsedRegion WITH user,key MERGE (user)-[r:AWS_ACCESS_KEY]->(key) ON CREATE SET r.firstseen = timestamp() @@ -506,6 +520,9 @@ def load_user_access_keys(neo4j_session: neo4j.Session, user_access_keys: Dict, AccessKeyId=key['AccessKeyId'], CreateDate=str(key['CreateDate']), Status=key['Status'], + LastUsedDate=key['LastUsedDate'], + LastUsedService=key['LastUsedService'], + LastUsedRegion=key['LastUsedRegion'], aws_update_tag=aws_update_tag, ) diff --git a/docs/root/modules/aws/schema.md b/docs/root/modules/aws/schema.md index 3c0c4baae..81565b029 100644 --- a/docs/root/modules/aws/schema.md +++ b/docs/root/modules/aws/schema.md @@ -691,6 +691,9 @@ Representation of an AWS [Access Key](https://docs.aws.amazon.com/IAM/latest/API | lastupdated | Timestamp of the last time the node was updated | createdate | Date when access key was created | | status | Active: valid for API calls. Inactive: not valid for API calls| +| lastuseddate | Date when the key was last used | +| lastusedservice | The service that was last used with the access key | +| lastusedregion | The region where the access key was last used | | **accesskeyid** | The ID for this access key| #### Relationships