Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/alert notify leaked key #299

Merged
merged 3 commits into from
Oct 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions app/server/event_handlers/aws.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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(
Expand All @@ -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", {})
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah this is interesting - I didn't know you could do this for Google Drive

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]
Expand Down Expand Up @@ -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}",
},
},
]
91 changes: 91 additions & 0 deletions app/tests/server/event_handlers/test_aws_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,81 @@ def test_format_new_iam_user_extracts_the_user_and_inserts_it_into_blocks():
assert "[email protected]" 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",
Expand Down Expand Up @@ -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",
)
Loading