From 92706ace7c27ba4335ef35f36eee8a7868b8cb69 Mon Sep 17 00:00:00 2001 From: Mikael Souza Date: Wed, 26 Jun 2024 20:31:11 -0300 Subject: [PATCH] Add Support for Google Space Chat --- CFN_DEPLOY_AHA.yml | 38 +++++- handler.py | 45 +++++++ messagegenerator.py | 288 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 369 insertions(+), 2 deletions(-) diff --git a/CFN_DEPLOY_AHA.yml b/CFN_DEPLOY_AHA.yml index 8afd360..73fb47c 100644 --- a/CFN_DEPLOY_AHA.yml +++ b/CFN_DEPLOY_AHA.yml @@ -15,9 +15,10 @@ Metadata: - S3Key - Label: default: >- - Communication Channels - Slack/Microsoft Teams/Amazon Chime And/or + Communication Channels - Google Space/Slack/Microsoft Teams/Amazon Chime And/or EventBridge Parameters: + - GoogleSpaceWebhookURL - SlackWebhookURL - MicrosoftTeamsWebhookURL - AmazonChimeWebhookURL @@ -47,6 +48,8 @@ Metadata: default: Name of S3 Bucket S3Key: default: Name of .zip file in S3 Bucket + GoogleSpaceWebhookURL: + default: Google Space Webhook URL SlackWebhookURL: default: Slack Webhook URL MicrosoftTeamsWebhookURL: @@ -68,16 +71,18 @@ Metadata: AccountIDs: default: Exclude any account numbers? Conditions: + UsingGoogle: !Not [!Equals [!Ref GoogleSpaceWebhookURL, None]] UsingSlack: !Not [!Equals [!Ref SlackWebhookURL, None]] UsingTeams: !Not [!Equals [!Ref MicrosoftTeamsWebhookURL, None]] UsingChime: !Not [!Equals [!Ref AmazonChimeWebhookURL, None]] UsingEventBridge: !Not [!Equals [!Ref EventBusName, None]] - UsingSecrets: !Or [!Condition UsingSlack, !Condition UsingTeams, !Condition UsingChime, !Condition UsingEventBridge, !Condition UsingCrossAccountRole] + UsingSecrets: !Or [!Condition UsingGoogle, !Condition UsingSlack, !Condition UsingTeams, !Condition UsingChime, !Condition UsingEventBridge, !Condition UsingCrossAccountRole] UsingCrossAccountRole: !Not [!Equals [!Ref ManagementAccountRoleArn, None]] NotUsingMultiRegion: !Equals [!Ref SecondaryRegion, 'No'] UsingMultiRegion: !Not [!Equals [!Ref SecondaryRegion, 'No']] TestCondition: !Equals ['true', 'false'] UsingMultiRegionTeams: !And [!Condition UsingTeams, !Condition UsingMultiRegion] + UsingMultiRegionGoogle: !And [!Condition UsingGoogle, !Condition UsingMultiRegion] UsingMultiRegionSlack: !And [!Condition UsingSlack, !Condition UsingMultiRegion] UsingMultiRegionEventBridge: !And [!Condition UsingEventBridge, !Condition UsingMultiRegion] UsingMultiRegionChime: !And [!Condition UsingChime, !Condition UsingMultiRegion] @@ -150,6 +155,11 @@ Parameters: such as DataDog/NewRelic/PagerDuty. If you don't prefer to use EventBridge, leave the default (None). Type: String Default: None + GoogleSpaceWebhookURL: + Description: >- + Enter the Google Space Webhook URL. If you don't prefer to use Google Space Chat, leave the default (None). + Type: String + Default: None SlackWebhookURL: Description: >- Enter the Slack Webhook URL. If you don't prefer to use Slack, leave the default (None). @@ -452,6 +462,7 @@ Resources: - 'secretsmanager:GetSecretValue' Resource: - !If [UsingTeams, !Sub '${MicrosoftChannelSecret}', !Ref AWS::NoValue] + - !If [UsingGoogle, !Sub '${GoogleChannelSecret}', !Ref AWS::NoValue] - !If [UsingSlack, !Sub '${SlackChannelSecret}', !Ref AWS::NoValue] - !If [UsingEventBridge, !Sub '${EventBusNameSecret}', !Ref AWS::NoValue] - !If [UsingChime, !Sub '${ChimeChannelSecret}', !Ref AWS::NoValue] @@ -462,6 +473,12 @@ Resources: - 'arn:aws:secretsmanager:${SecondaryRegion}:${AWS::AccountId}:secret:${SecretNameWithSha}' - { SecretNameWithSha: !Select [1, !Split [':secret:', !Sub '${MicrosoftChannelSecret}' ]]} - !Ref AWS::NoValue + - !If + - UsingMultiRegionGoogle + - !Sub + - 'arn:aws:secretsmanager:${SecondaryRegion}:${AWS::AccountId}:secret:${SecretNameWithSha}' + - { SecretNameWithSha: !Select [1, !Split [':secret:', !Sub '${GoogleChannelSecret}' ]]} + - !Ref AWS::NoValue - !If - UsingMultiRegionSlack - !Sub @@ -576,6 +593,22 @@ Resources: Tags: - Key: HealthCheckMicrosoft Value: ChannelID + GoogleChannelSecret: + Type: 'AWS::SecretsManager::Secret' + Condition: UsingGoogle + Properties: + Name: GoogleChannelID + Description: Google Space Channel ID Secret + ReplicaRegions: + !If + - UsingMultiRegion + - [{ Region: !Sub '${SecondaryRegion}' }] + - !Ref "AWS::NoValue" + SecretString: + Ref: GoogleSpaceWebhookURL + Tags: + - Key: HealthCheckGoogle + Value: ChannelID SlackChannelSecret: Type: 'AWS::SecretsManager::Secret' Condition: UsingSlack @@ -657,6 +690,7 @@ Resources: Runtime: python3.11 Environment: Variables: + Google: !If [UsingGoogle, "True", !Ref 'AWS::NoValue'] Slack: !If [UsingSlack, "True", !Ref 'AWS::NoValue'] Teams: !If [UsingTeams, "True", !Ref 'AWS::NoValue'] Chime: !If [UsingChime, "True", !Ref 'AWS::NoValue'] diff --git a/handler.py b/handler.py index 2be0414..dc1e404 100644 --- a/handler.py +++ b/handler.py @@ -23,6 +23,8 @@ get_message_for_email, get_org_message_for_email, get_detail_for_eventbridge, + get_message_for_google_space, + get_org_message_for_google_space, ) print("boto3 version: ", boto3.__version__) @@ -59,6 +61,7 @@ def get_account_name(account_id): def send_alert(event_details, affected_accounts, affected_entities, event_type): + google_space_url = get_secrets()["google"] slack_url = get_secrets()["slack"] teams_url = get_secrets()["teams"] chime_url = get_secrets()["chime"] @@ -85,6 +88,16 @@ def send_alert(event_details, affected_accounts, affected_entities, event_type): except URLError as e: print("Server connection failed: ", e.reason) pass + if "chat.googleapis.com" in google_space_url: + try: + print("Sending the alert to Google Space") + send_to_google_space(get_message_for_google_space(event_details, event_type, affected_accounts, affected_entities), + google_space_url) + except HTTPError as e: + print("Got an error while sending message to Google Space: ", e.code, e.reason) + except URLError as e: + print("Server connection failed: ", e.reason) + pass if "hooks.slack.com/services" in slack_url: try: print("Sending the alert to Slack Webhook Channel") @@ -164,6 +177,7 @@ def send_alert(event_details, affected_accounts, affected_entities, event_type): def send_org_alert( event_details, affected_org_accounts, affected_org_entities, event_type ): + google_url = get_secrets()["google"] slack_url = get_secrets()["slack"] teams_url = get_secrets()["teams"] chime_url = get_secrets()["chime"] @@ -190,6 +204,20 @@ def send_org_alert( except URLError as e: print("Server connection failed: ", e.reason) pass + if "chat.googleapis.com" in google_url: + try: + print("Sending the alert to Google Space") + send_to_google_space( + get_org_message_for_google_space( + event_details, event_type, affected_org_accounts, resources + ), + google_url, + ) + except HTTPError as e: + print("Got an error while sending message to Google Space: ", e.code, e.reason) + except URLError as e: + print("Server connection failed: ", e.reason) + pass if "hooks.slack.com/services" in slack_url: try: print("Sending the alert to Slack Webhook Channel") @@ -266,6 +294,19 @@ def send_org_alert( pass +def send_to_google_space(message, webhookurl): + google_space_message = message + req = Request(webhookurl, data=json.dumps(google_space_message).encode("utf-8"), + headers={"Content-Type": "application/json; charset=UTF-8"}) + try: + response = urlopen(req) + response.read() + except HTTPError as e: + print("Request failed : ", e.code, e.reason) + except URLError as e: + print("Server connection failed: ", e.reason, e.reason) + + def send_to_slack(message, webhookurl): slack_message = message req = Request( @@ -690,6 +731,7 @@ def update_ddb( def get_secrets(): secret_teams_name = "MicrosoftChannelID" + secret_google_space_name = "GoogleChannelID" secret_slack_name = "SlackChannelID" secret_chime_name = "ChimeChannelID" region_name = os.environ["AWS_REGION"] @@ -704,6 +746,9 @@ def get_secrets(): secrets["teams"] = ( get_secret(secret_teams_name, client) if "Teams" in os.environ else "None" ) + secrets["google"] = ( + get_secret(secret_google_space_name, client) if "Google" in os.environ else "None" + ) secrets["slack"] = ( get_secret(secret_slack_name, client) if "Slack" in os.environ else "None" ) diff --git a/messagegenerator.py b/messagegenerator.py index 12c619b..a528bd2 100644 --- a/messagegenerator.py +++ b/messagegenerator.py @@ -8,6 +8,151 @@ import time +def get_message_for_google_space(event_details, event_type, affected_accounts, affected_entities): + message = "" + if len(affected_entities) >= 1: + affected_entities = "\n".join(affected_entities) + if affected_entities == "UNKNOWN": + affected_entities = "All resources\nin region" + else: + affected_entities = "All resources\nin region" + if len(affected_accounts) >= 1: + affected_accounts = "\n".join(affected_accounts) + else: + affected_accounts = "All accounts\nin region" + summary = "" + if event_type == "create": + title = "🚨 [NEW] AWS Health reported an issue with the " + event_details['successfulSet'][0]['event'][ + 'service'].upper() + " service in the " + event_details['successfulSet'][0]['event'][ + 'region'].upper() + " region." + message = { + "cards": [ + { + "header": { + "title": "AWS Health", + "subtitle": title, + "imageUrl": "https://i.imgur.com/tctwBdE.jpg" + }, + "sections": [ + { + "widgets": [ + { + "textParagraph": { + "text": f"Account(s): {affected_accounts}" + } + }, + { + "textParagraph": { + "text": f"Resource(s): {affected_entities}" + } + }, + { + "textParagraph": { + "text": f"Service: {event_details['successfulSet'][0]['event']['service']}" + } + }, + { + "textParagraph": { + "text": f"Region: {event_details['successfulSet'][0]['event']['region']}" + } + }, + { + "textParagraph": { + "text": f"Start Time (UTC): {cleanup_time(event_details['successfulSet'][0]['event']['startTime'])}" + } + }, + { + "textParagraph": { + "text": f"Status: {event_details['successfulSet'][0]['event']['statusCode']}" + } + }, + { + "textParagraph": { + "text": f"Event ARN: {event_details['successfulSet'][0]['event']['arn']}" + } + }, + { + "textParagraph": { + "text": f"Updates: {get_last_aws_update(event_details)}" + } + } + ] + } + ] + } + ] + } + + elif event_type == "resolve": + title = "✅ [RESOLVED] The AWS Health issue with the " + event_details['successfulSet'][0]['event'][ + 'service'].upper() + " service in the " + event_details['successfulSet'][0]['event'][ + 'region'].upper() + " region is now resolved." + message = { + "cards": [ + { + "header": { + "title": "AWS Health", + "subtitle": title, + "imageUrl": "https://i.imgur.com/tctwBdE.jpg" + }, + "sections": [ + { + "widgets": [ + { + "textParagraph": { + "text": f"Account(s): {affected_accounts}" + } + }, + { + "textParagraph": { + "text": f"Resource(s): {affected_entities}" + } + }, + { + "textParagraph": { + "text": f"Service: {event_details['successfulSet'][0]['event']['service']}" + } + }, + { + "textParagraph": { + "text": f"Region: {event_details['successfulSet'][0]['event']['region']}" + } + }, + { + "textParagraph": { + "text": f"Start Time (UTC): {cleanup_time(event_details['successfulSet'][0]['event']['startTime'])}" + } + }, + { + "textParagraph": { + "text": f"End Time (UTC): {cleanup_time(event_details['successfulSet'][0]['event']['endTime'])}" + } + }, + { + "textParagraph": { + "text": f"Status: {event_details['successfulSet'][0]['event']['statusCode']}" + } + }, + { + "textParagraph": { + "text": f"Event ARN: {event_details['successfulSet'][0]['event']['arn']}" + } + }, + { + "textParagraph": { + "text": f"Updates: {get_last_aws_update(event_details)}" + } + } + ] + } + ] + } + ] + } + print("Message sent to Google Space: ", message) + return message + + def get_message_for_slack(event_details, event_type, affected_accounts, affected_entities, slack_webhook): message = "" summary = "" @@ -139,6 +284,149 @@ def get_detail_for_eventbridge(event_details, affected_entities): return message + +def get_org_message_for_google_space(event_details, event_type, affected_org_accounts, affected_org_entities): + message = "" + if len(affected_org_entities) >= 1: + affected_org_entities = "\n".join(affected_org_entities) + else: + affected_org_entities = "All resources in region" + if len(affected_org_accounts) >= 1: + affected_org_accounts = "\n".join(affected_org_accounts) + else: + affected_org_accounts = "All accounts in region" + if event_type == "create": + title = "🚨 [NEW] AWS Health reported an issue with the " + event_details['successfulSet'][0]['event'][ + 'service'].upper() + " service in the " + event_details['successfulSet'][0]['event'][ + 'region'].upper() + " region." + message = { + "cards": [ + { + "header": { + "title": "AWS Health", + "subtitle": title, + "imageUrl": "https://i.imgur.com/tctwBdE.jpg" + }, + "sections": [ + { + "widgets": [ + { + "textParagraph": { + "text": f"Account(s): {affected_org_accounts}" + } + }, + { + "textParagraph": { + "text": f"Resource(s): {affected_org_entities}" + } + }, + { + "textParagraph": { + "text": f"Service: {event_details['successfulSet'][0]['event']['service']}" + } + }, + { + "textParagraph": { + "text": f"Region: {event_details['successfulSet'][0]['event']['region']}" + } + }, + { + "textParagraph": { + "text": f"Start Time (UTC): {cleanup_time(event_details['successfulSet'][0]['event']['startTime'])}" + } + }, + { + "textParagraph": { + "text": f"Status: {event_details['successfulSet'][0]['event']['statusCode']}" + } + }, + { + "textParagraph": { + "text": f"Event ARN: {event_details['successfulSet'][0]['event']['arn']}" + } + }, + { + "textParagraph": { + "text": f"Updates: {get_last_aws_update(event_details)}" + } + } + ] + } + ] + } + ] + } + + elif event_type == "resolve": + title = "✅ [RESOLVED] The AWS Health issue with the " + event_details['successfulSet'][0]['event'][ + 'service'].upper() + " service in the " + event_details['successfulSet'][0]['event'][ + 'region'].upper() + " region is now resolved." + message = { + "cards": [ + { + "header": { + "title": "AWS Health", + "subtitle": title, + "imageUrl": "https://i.imgur.com/tctwBdE.jpg" + }, + "sections": [ + { + "widgets": [ + { + "textParagraph": { + "text": f"Account(s): {affected_org_accounts}" + } + }, + { + "textParagraph": { + "text": f"Resource(s): {affected_org_entities}" + } + }, + { + "textParagraph": { + "text": f"Service: {event_details['successfulSet'][0]['event']['service']}" + } + }, + { + "textParagraph": { + "text": f"Region: {event_details['successfulSet'][0]['event']['region']}" + } + }, + { + "textParagraph": { + "text": f"Start Time (UTC): {cleanup_time(event_details['successfulSet'][0]['event']['startTime'])}" + } + }, + { + "textParagraph": { + "text": f"End Time (UTC): {cleanup_time(event_details['successfulSet'][0]['event']['endTime'])}" + } + }, + { + "textParagraph": { + "text": f"Status: {event_details['successfulSet'][0]['event']['statusCode']}" + } + }, + { + "textParagraph": { + "text": f"Event ARN: {event_details['successfulSet'][0]['event']['arn']}" + } + }, + { + "textParagraph": { + "text": f"Updates: {get_last_aws_update(event_details)}" + } + } + ] + } + ] + } + ] + } + return message + print("Message sent to Google Space: ", message) + + def get_org_message_for_slack(event_details, event_type, affected_org_accounts, affected_org_entities, slack_webhook): message = "" summary = ""