diff --git a/app/server/event_handlers/aws.py b/app/server/event_handlers/aws.py index 30e49cb4..45064cd4 100644 --- a/app/server/event_handlers/aws.py +++ b/app/server/event_handlers/aws.py @@ -2,6 +2,7 @@ import re import urllib.parse from commands.utils import log_ops_message +from integrations import google_drive, opsgenie def parse(payload, client): @@ -19,6 +20,8 @@ def parse(payload, client): blocks = format_auto_mitigation(payload) elif isinstance(msg, str) and "IAM User" in msg: blocks = format_new_iam_user(payload) + elif isinstance(msg, str) and "API Key with value token=" in msg: + blocks = format_api_key_detected(payload, client) else: blocks = [] log_ops_message( @@ -38,6 +41,39 @@ def nested_get(dictionary, keys): return dictionary +def alert_on_call(product, client, api_key, github_repo): + # get the list of folders + folders = google_drive.list_folders() + # get the folder id for the Product + for folder in folders: + if folder["name"] == product: + folder = folder["id"] + break + # Get folder metadata + folder_metadata = google_drive.list_metadata(folder).get("appProperties", {}) + oncall = [] + message = "" + private_message = "" + + # Get OpsGenie users on call and construct string + if "genie_schedule" in folder_metadata: + for email in opsgenie.get_on_call_users(folder_metadata["genie_schedule"]): + r = client.users_lookupByEmail(email=email) + if r.get("ok"): + oncall.append(r["user"]) + message = f"{product} on-call staff " + for user in oncall: + # send a private message to the people on call. + message += f"<@{user['id']}> " + private_message = f"Hello {user['profile']['first_name']}!\nA Notify API key has been leaked and needs to be revoked. 🙀 \nThe key is *{api_key}* and the file is {github_repo}. You can see the message in #internal-sre-alerts to start an incident." + # send the private message + client.chat_postMessage( + channel=user["id"], text=private_message, as_user=True + ) + message += "have been notified." + return message + + def format_abuse_notification(payload, msg): regex = r"arn:aws:sns:\w.*:(\d.*):\w.*" account = re.search(regex, payload.TopicArn).groups()[0] @@ -198,3 +234,43 @@ def format_cloudwatch_alarm(msg): {"type": "section", "text": {"type": "mrkdwn", "text": link}}, ] return blocks + + +# If the message contains an api key it will be parsed by the format_api_key_detected function. + + +def format_api_key_detected(payload, client): + msg = payload.Message + regex = r"API Key with value token='(\w.+)' has been detected in url='(\w.+)'" + # extract the api key and the github repo from the message + api_key = re.search(regex, msg).groups()[0] + github_repo = re.search(regex, msg).groups()[1] + + # send a private message with the api-key and github repo to the people on call. + on_call_message = alert_on_call("Notify", client, api_key, github_repo) + + # Format the message displayed in Slack + return [ + {"type": "section", "text": {"type": "mrkdwn", "text": " "}}, + { + "type": "header", + "text": { + "type": "plain_text", + "text": "🙀 Notify API Key has been compromised!🔑", + }, + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": f"Notify API Key *{api_key}* has been committed in github file {github_repo}. The key needs to be revoked!", + }, + }, + { + "type": "section", + "text": { + "type": "plain_text", + "text": f"{on_call_message}", + }, + }, + ] diff --git a/app/tests/server/event_handlers/test_aws_handler.py b/app/tests/server/event_handlers/test_aws_handler.py index 84974c69..e6f6133e 100644 --- a/app/tests/server/event_handlers/test_aws_handler.py +++ b/app/tests/server/event_handlers/test_aws_handler.py @@ -178,6 +178,81 @@ def test_format_new_iam_user_extracts_the_user_and_inserts_it_into_blocks(): assert "test_user@cds-snc.ca" in response[2]["text"]["text"] +@patch("server.event_handlers.aws.format_api_key_detected") +def test_parse_returns_blocks_if_api_key_detected(format_api_key_detected_mock): + # Test that the parse function returns the blocks returned by format_api_key_detected + client = MagicMock() + format_api_key_detected_mock.return_value = ["foo", "bar"] + payload = mock_api_key_detected() + response = aws.parse(payload, client) + assert response == ["foo", "bar"] + format_api_key_detected_mock.assert_called_once_with(payload, client) + + +@patch("server.event_handlers.aws.alert_on_call") +def test_format_api_key_detected_extracts_the_api_key_and_inserts_it_into_blocks( + alert_on_call_mock, +): + # Test that the format_api_key_detected function extracts the api key properly + client = MagicMock() + payload = mock_api_key_detected() + response = aws.format_api_key_detected(payload, client) + assert "gcntfy-api-key-blah" in response[2]["text"]["text"] + + +@patch("server.event_handlers.aws.alert_on_call") +def test_format_api_key_detected_extracts_the_url_and_inserts_it_into_blocks( + alert_on_call_mock, +): + # Test that the format_api_key_detected function extracts the url properly + client = MagicMock() + payload = mock_api_key_detected() + response = aws.format_api_key_detected(payload, client) + assert "https://github.com/blah" in response[2]["text"]["text"] + + +@patch("server.event_handlers.aws.alert_on_call") +def test_format_api_key_detected_extracts_the_on_call_message_and_inserts_it_into_blocks( + alert_on_call_mock, +): + # Test that the format_api_key_detected function extracts the on call message properly + client = MagicMock() + alert_on_call_mock.return_value = "test message" + payload = mock_api_key_detected() + response = aws.format_api_key_detected(payload, client) + assert "test message" in response[3]["text"]["text"] + + +@patch("integrations.google_drive.get_google_service") +@patch("commands.incident.google_drive.list_folders") +@patch("commands.incident.google_drive.list_metadata") +@patch("integrations.opsgenie.get_on_call_users") +def test_alert_on_call_returns_message( + get_on_call_users_mock, + list_metadata_mock, + google_list_folders_mock, + get_google_service_mock, +): + # Test that the alert_on_call function returns the proper message + client = MagicMock() + product = "test" + api_key = "test_api_key" + github_repo = "test_repo" + google_list_folders_mock.return_value = [ + { + "name": "Notify", + "id": 12345, + "appProperties": {"genie_schedule": "test_schedule"}, + } + ] + list_metadata_mock.return_value = { + "name": "Notify", + "appProperties": {"genie_schedule": "test_schedule"}, + } + response = aws.alert_on_call(product, client, api_key, github_repo) + assert "test on-call staff have been notified" in response + + def mock_abuse_alert(): return MagicMock( Type="Notification", @@ -253,3 +328,19 @@ def mock_new_iam_user(): SigningCertURL="https://sns.ca-central-1.amazonaws.com/SimpleNotificationService-56e67fcb41f6fec09b0196692625d385.pem", UnsubscribeURL="https://sns.ca-central-1.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:ca-central-1:412578375350:test-sre-bot:4636a013-5224-4207-91b2-d6d7c7ab7ea7", ) + + +# Mock the message returned from AWS when a API key has been compromised +def mock_api_key_detected(): + return MagicMock( + Type="Notification", + MessageId="1e5f5647g-adb5-5d6f-ab5e-c2e508881361", + TopicArn="arn:aws:sns:ca-central-1:412578375350:test-sre-bot", + Subject="API Key detected", + Message="API Key with value token='gcntfy-api-key-blah' has been detected in url='https://github.com/blah'! This key needs to be revoked asap.", + Timestamp="2023-09-25T20:50:37.868Z", + SignatureVersion="1", + Signature="EXAMPLEO0OA1HN4MIHrtym3N6SWqvotsY4EcG+Ty/wrfZcxpQ3mximWM7ZfoYlzZ8NBh4s1XTPuqbl5efK64TEuPgNWBMKsm5Gc2d8H6hoDpLqAOELGl2/xlvWf2CovLH/KPj8xrSwAgOS9jL4r/EEMdXYb705YMMBudu78gooatU9EpVl+1I2MCP2AW0ZJWrcSwYMqxo9yo7H6coyBRlmTxP97PlELXoqXLfufsfFBjZ0eFycndG5A0YHeue82uLF5fIHGpcTjqNzLF0PXuJoS9xVkGx3X7p+dzmRE4rp/swGyKCqbXvgldPRycuj7GSk3r8HLSfzjqHyThnDqMECA==", + SigningCertURL="https://sns.ca-central-1.amazonaws.com/SimpleNotificationService-56e67fcb41f6fec09b0196692625d385.pem", + UnsubscribeURL="https://sns.ca-central-1.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:ca-central-1:412578375350:test-sre-bot:4636a013-5224-4207-91b2-d6d7c7ab7ea7", + )