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