diff --git a/ops_bot/aws.py b/ops_bot/aws.py index ad150ab..37376e0 100644 --- a/ops_bot/aws.py +++ b/ops_bot/aws.py @@ -4,7 +4,7 @@ from typing import Any, List, Tuple from fastapi import Request -from ops_bot.common import COLOR_ALARM, COLOR_OK, COLOR_UNKNOWN +from ops_bot.common import COLOR_ALARM, COLOR_OK, COLOR_UNKNOWN, COLOR_WARNING from ops_bot.config import RoutingKey @@ -28,7 +28,12 @@ def handle_notification(payload: Any) -> List[Tuple[str, str]]: def handle_json_notification(payload: Any, body: Any) -> List[Tuple[str, str]]: if "AlarmName" not in body: + payload_str = payload.get("Message") msg = "Received unknown json payload type over AWS SNS" + msg += f"""\n
+```json +{payload_str} +```""" logging.info(msg) logging.info(payload.get("Message")) return [(msg, msg)] @@ -56,6 +61,89 @@ def handle_json_notification(payload: Any, body: Any) -> List[Tuple[str, str]]: return [(plain, formatted)] +def handle_cloudtrail_signin(payload: Any) -> List[Tuple[str, str]]: + region = payload["region"] + event_type = payload["detail"]["eventType"] + event_time = payload["detail"]["eventTime"] + + account_id = None + if "accountId" in payload["detail"]["userIdentity"]: + account_id = payload["detail"]["userIdentity"]["accountId"] + else: + account_id = payload["detail"]["recipientAccountId"] + + user_type = payload["detail"]["userIdentity"]["type"] + if user_type == "IAMUser": + user = payload["detail"]["userIdentity"]["userName"] + elif user_type == "Root": + user = "Root User" + elif user_type == "AssumedRole": + user = payload["detail"]["userIdentity"]["principalId"] + else: + user = "Unknown user" + + mfa_used = "unknown" + if ( + "additionalEventData" in payload["detail"] + and "MFAUsed" in payload["detail"]["additionalEventData"] + ): + mfa_used = payload["detail"]["additionalEventData"]["MFAUsed"] + + was_failure = False + if ( + "responseElements" in payload["detail"] + and "ConsoleLogin" in payload["detail"]["responseElements"] + ): + was_failure = payload["detail"]["responseElements"]["ConsoleLogin"] == "Failure" + + error_message = None + if "errorMessage" in payload["detail"]: + error_message = payload["detail"]["errorMessage"] + + if was_failure: + title = f"Failed AWS Console Sign attempt by user `{user}`." + color = COLOR_WARNING + else: + title = f"AWS Console Sign detected by user `{user}`." + color = COLOR_ALARM + + formatted = [ + x + for x in [ + f"**🚨 ALERT[{event_type}]** : {title}", + f"- **Region**: {region}", + f"- **MFA Used**: {mfa_used}", + (f"- **Error Message**: {error_message}" if error_message else None), + f"- **Event Time**: {event_time}", + f"- **Account ID**: {account_id}", + ] + if x is not None + ] + + plain = title + + return [(plain, "
".join(formatted))] + + +def handle_cloudtrail_generic(payload: Any) -> List[Tuple[str, str]]: + region = payload["region"] + event_type = payload["detail"]["eventType"] + event_time = payload["detail"]["eventTime"] + account_id = payload["detail"]["recipientAccountId"] + detail = payload["detail-type"] + + plain = f"{detail}" + + formatted = [ + f"**⚠️CLOUDTRAIL EVENT[{event_type}]**: {detail}", + f"**Region**: {region}", + f"**Event Time**: {event_time}", + f"**Account ID**: {account_id}", + ] + + return [(plain, "
".join(formatted))] + + async def parse_sns_event( route: RoutingKey, payload: Any, @@ -71,4 +159,11 @@ async def parse_sns_event( return handle_json_notification(payload, body) except Exception: return handle_notification(payload) + elif "source" in payload: + source = payload["source"] + if source == "aws.signin": + return handle_cloudtrail_signin(payload) + else: + return handle_cloudtrail_generic(payload) + raise Exception("Unnown SNS payload type") diff --git a/tests/test_ops_bot.py b/tests/test_ops_bot.py index 2825fce..4cd1c58 100644 --- a/tests/test_ops_bot.py +++ b/tests/test_ops_bot.py @@ -45,6 +45,106 @@ sns_notification = """{ "UnsubscribeURL" : "https://sns.us-west-2.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:us-west-2:123456789012:MyTopic:c9135db0-26c4-47ec-8998-413945fb5a96" }""" +sns_signin = """ +{ + "version": "0", + "id": "000000-e441-44ce-22c1-00000000", + "detail-type": "AWS Console Sign In via CloudTrail", + "source": "aws.signin", + "account": "1234567890", + "time": "2025-01-31T14:03:15Z", + "region": "eu-north-1", + "resources": [], + "detail": { + "eventVersion": "1.09", + "userIdentity": { + "type": "IAMUser", + "principalId": "ABCDEFGHIJKLMNOPQRSTU", + "arn": "arn:aws:iam::1234567890:user/user@example.com", + "accountId": "1234567890", + "userName": "user@example.com" + }, + "eventTime": "2025-01-31T14:03:15Z", + "eventSource": "signin.amazonaws.com", + "eventName": "ConsoleLogin", + "awsRegion": "eu-north-1", + "sourceIPAddress": "193.0.0.0.1", + "userAgent": "Mozilla/5.0 (X11; Linux x86_64; rv:134.0) Gecko/20100101 Firefox/134.0", + "requestParameters": null, + "responseElements": { + "ConsoleLogin": "Success" + }, + "additionalEventData": { + "LoginTo": "https://console.aws.amazon.com/console/home", + "MobileVersion": "No", + "MFAIdentifier": "arn:aws:iam::1234567890:u2f/user/user@example.com/user-omg-my-yubikey", + "MFAUsed": "Yes" + }, + "eventID": "000000-1539-4d7f-b6cc-000000000", + "readOnly": false, + "eventType": "AwsConsoleSignIn", + "managementEvent": true, + "recipientAccountId": "1234567890", + "eventCategory": "Management", + "tlsDetails": { + "tlsVersion": "TLSv1.3", + "cipherSuite": "TLS_AES_128_GCM_SHA256", + "clientProvidedHostHeader": "eu-north-1.signin.aws.amazon.com" + } + } +} +""" + +sns_signin_failure = """ +{ + "version": "0", + "id": "0000-a6cf-b920-6e14-000000", + "detail-type": "AWS Console Sign In via CloudTrail", + "source": "aws.signin", + "account": "1234567890", + "time": "2025-01-31T14:01:49Z", + "region": "eu-north-1", + "resources": [], + "detail": { + "eventVersion": "1.09", + "userIdentity": { + "type": "IAMUser", + "principalId": "AIDARWPFIVFS76W7ZBVBO", + "accountId": "1234567890", + "accessKeyId": "", + "userName": "user@example.com" + }, + "eventTime": "2025-01-31T14:01:49Z", + "eventSource": "signin.amazonaws.com", + "eventName": "ConsoleLogin", + "awsRegion": "eu-north-1", + "sourceIPAddress": "193.0.0.0.1", + "userAgent": "Mozilla/5.0 (X11; Linux x86_64; rv:134.0) Gecko/20100101 Firefox/134.0", + "errorMessage": "Failed authentication", + "requestParameters": null, + "responseElements": { + "ConsoleLogin": "Failure" + }, + "additionalEventData": { + "LoginTo": "https://console.aws.amazon.com/console/home?", + "MobileVersion": "No", + "MFAUsed": "Yes" + }, + "eventID": "00000-572b-4006-8d9f-00000", + "readOnly": false, + "eventType": "AwsConsoleSignIn", + "managementEvent": true, + "recipientAccountId": "1234567890", + "eventCategory": "Management", + "tlsDetails": { + "tlsVersion": "TLSv1.3", + "cipherSuite": "TLS_AES_128_GCM_SHA256", + "clientProvidedHostHeader": "eu-north-1.signin.aws.amazon.com" + } + } +} +""" + async def test_aws_sns_notification() -> None: r = await aws.parse_sns_event(None, json.loads(sns_notification), None) @@ -67,3 +167,17 @@ async def test_aws_sns_unsubscribe() -> None: print(r) expected = "You have chosen to deactivate subscription arn:aws:sns:us-west-2:123456789012:MyTopic:2bcfbf39-05c3-41de-beaa-fcfcc21c8f55.\nTo cancel this operation and restore the subscription, visit the SubscribeURL included in this message.\n\nhttps://sns.us-west-2.amazonaws.com/?Action=ConfirmSubscription&TopicArn=arn:aws:sns:us-west-2:123456789012:MyTopic&Token=2336412f37fb6..." assert r[0] == (expected, expected) + + +async def test_aws_sns_signin() -> None: + r = await aws.parse_sns_event(None, json.loads(sns_signin), None) + print(r) + expected = "**🚨 ALERT[AwsConsoleSignIn]** : AWS Console Sign detected by user `user@example.com`.
- **Region**: eu-north-1
- **MFA Used**: Yes
- **Event Time**: 2025-01-31T14:03:15Z
- **Account ID**: 1234567890" + assert r[0][1] == expected + + +async def test_aws_sns_signin_failure() -> None: + r = await aws.parse_sns_event(None, json.loads(sns_signin_failure), None) + print(r) + expected = "**🚨 ALERT[AwsConsoleSignIn]** : Failed AWS Console Sign attempt by user `user@example.com`.
- **Region**: eu-north-1
- **MFA Used**: Yes
- **Error Message**: Failed authentication
- **Event Time**: 2025-01-31T14:01:49Z
- **Account ID**: 1234567890" + assert r[0][1] == expected