{
    "AWSTemplateFormatVersion": "2010-09-09",
    "Description": "This Cloudformation Template deploys the AWS resouces needed to ingest alerts to Incident Detection and Response from 3rd Party APMs which do not have direct integration with Amazon EventBridge. The AWS resources deployed contains below. - Secrets Manager - Authorizer Lambda function, execution role and permission - Transform Lambda function, execution role and permission - Custom EventBridge - API Gateway and Authorizer",
    "Parameters": {
        "APMNameParameter": {
            "Type": "String",
            "Default": "Dynatrace",
            "Description": "Enter the name of the Application Performance Monitoring (APM) tool without any spaces. e.g. Dynatrace, SumoLogic, Prometheus.",
            "AllowedPattern": "^[a-zA-Z0-9]*$",
            "ConstraintDescription": "The pattern allows only lowercase and uppercase alphabetical characters and numerals (^[a-zA-Z0-9]*$)."
        }
    },
    "Resources": {
        "SecretCreation": {
            "Type": "AWS::SecretsManager::Secret",
            "Properties": {
                "Name": {
                    "Fn::Sub": "${APMNameParameter}MySecretTokenName"
                },
                "GenerateSecretString": {
                    "SecretStringTemplate": "{\"username\": \"test-user\"}",
                    "GenerateStringKey": "APMSecureToken",
                    "PasswordLength": 18,
                    "ExcludePunctuation": true
                }
            }
        },
        "CustomEventBus": {
            "Type": "AWS::Events::EventBus",
            "Properties": {
                "Name": {
                    "Fn::Sub": "${APMNameParameter}-AWSIncidentDetectionResponse-EventBus"
                }
            }
        },
        "TransformLambdaExecutionRole": {
            "Type": "AWS::IAM::Role",
            "Properties": {
                "AssumeRolePolicyDocument": {
                    "Version": "2012-10-17",
                    "Statement": [
                        {
                            "Effect": "Allow",
                            "Principal": {
                                "Service": [
                                    "lambda.amazonaws.com"
                                ]
                            },
                            "Action": [
                                "sts:AssumeRole"
                            ]
                        }
                    ]
                },
                "Description": "IAM Role for Transform Lambda function to create logs and put events for custom Event Bus",
                "Policies": [
                    {
                        "PolicyName": "IDR-TransformLambdaExecutionRolePolicy",
                        "PolicyDocument": {
                            "Version": "2012-10-17",
                            "Statement": [
                                {
                                    "Effect": "Allow",
                                    "Action": "logs:CreateLogGroup",
                                    "Resource": {
                                        "Fn::Sub": "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:*"
                                    }
                                },
                                {
                                    "Effect": "Allow",
                                    "Action": [
                                        "logs:CreateLogStream",
                                        "logs:PutLogEvents"
                                    ],
                                    "Resource": {
                                        "Fn::Sub": "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/${APMNameParameter}-AWSIncidentDetectionResponse-Lambda-Transform:*"
                                    }
                                },
                                {
                                    "Effect": "Allow",
                                    "Action": "events:PutEvents",
                                    "Resource": {
                                        "Fn::GetAtt": "CustomEventBus.Arn"
                                    }
                                }
                            ]
                        }
                    }
                ],
                "RoleName": {
                    "Fn::Sub": "IDR-TransformLambdaExecutionRole-${AWS::Region}"
                }
            },
            "DependsOn": [
                "CustomEventBus"
            ]
        },
        "TransformLambdaFunction": {
            "Type": "AWS::Lambda::Function",
            "Properties": {
                "FunctionName": {
                    "Fn::Sub": "${APMNameParameter}-AWSIncidentDetectionResponse-Lambda-Transform"
                },
                "Description": "This function adds the necessary key:value pairs to the APM payload so that events can be ingested by AWS Incident Detection and Response.",
                "Runtime": "python3.10",
                "Timeout": 8,
                "Role": {
                    "Fn::GetAtt": "TransformLambdaExecutionRole.Arn"
                },
                "Environment": {
                    "Variables": {
                        "EnvEventBusName": {
                            "Ref": "CustomEventBus"
                        }
                    }
                },
                "Handler": "index.lambda_handler",
                "Code": {
                    "ZipFile": "import logging\nimport json\nimport boto3\nimport os\nfrom uuid import uuid4\nimport functools\n\n\nlogger = logging.getLogger()\nlogger.setLevel(logging.INFO)\n\nclient = boto3.client('events')\n\nDETAIL_TYPE = \"ams.monitoring/generic-apm\" # Do not modify.\nDETAIL_SOURCE = \"GenericAPMEvent\" # Do not modify. \nEVENTBUS_NAME_ENVIRONMENT_VALUE = \"EnvEventBusName\" # Do not modify\n\nclass UnexpectedError(Exception):\n    pass\n\nclass EnvironmentKeyError(Exception):\n    pass\n\nclass EnvironmentError(Exception):\n    pass\n\nclass EventBusPutError(Exception):\n    pass\n\nclass ResponseError(Exception):\n    pass\n\ndef catch_exceptions(func):\n    @functools.wraps(func)\n    def wrapper(self, *args, **kwargs):\n        if self.response is not None and self.response != 200: \n            return\n        try:\n            return func(self, *args, **kwargs)\n        except ValueError as e:\n            self.response = 400\n            self.error_message = (f\"{self.session_id}: ValueError in {func.__name__}: {e}\")\n            logger.error(self.error_message, exc_info=True)\n        except KeyError as e:\n            self.response = 400\n            print(self.response)\n            self.error_message = (f\"{self.session_id}: KeyError in {func.__name__}: {e}\")\n            logger.error(self.error_message, exc_info=True)\n        except EnvironmentKeyError as e:\n            self.response = 500\n            self.error_message = (f\"{self.session_id}: EnvironmentKeyError in {func.__name__}: Unknown Environment Key{e}\")\n            logger.error(self.error_message, exc_info=True)\n        except EnvironmentError as e:\n            self.response = 500\n            self.error_message = (f\"{self.session_id}: EnvironmentError in {func.__name__}: Unknown Environment Error{e}\")\n            logger.error(self.error_message, exc_info=True)\n        except EventBusPutError as e:\n            self.response = 500\n            self.error_message = (f\"{self.session_id}: EventBusPutError in {func.__name__}: Unable to put event to eventbus: {e}\")\n            logger.error(self.error_message, exc_info=True)\n        except ResponseError as e:\n            self.response = 500\n            self.error_message = (f\"{self.session_id}: ResponseError in {func.__name__}: Unable to set response: {e}\")\n            logger.error(self.error_message, exc_info=True)\n        except Exception as e:\n            self.response = 500\n            self.error_message = (f\"{self.session_id}: Unexpected error in {func.__name__}: {e}\")\n            logger.error(self.error_message, exc_info=True)\n    return wrapper\n\nclass Transformations:\n    def __init__(self, event):\n        self._session_id = str(uuid4())\n        self._response = None\n        self.error_message = None\n        self.event = event\n\n        self.event_bus_name = self._get_eventbus_name()\n        self.raw_json = self._add_idr_key_value()\n        \n    @property\n    def session_id(self):\n        return self._session_id\n\n    @property\n    def response(self):\n        return self._response\n\n    @response.setter\n    @catch_exceptions\n    def response(self, value):\n        try:\n            self._response = int(value)\n        except Exception as e:\n            raise ResponseError(e)\n\n    @catch_exceptions\n    def _get_eventbus_name(self):\n        event_bus_name = None\n        try:\n            event_bus_name = os.environ[EVENTBUS_NAME_ENVIRONMENT_VALUE]\n        except KeyError as e:\n            raise EnvironmentKeyError(e)\n        except Exception as e:\n            raise EnvironmentError(e)\n        return event_bus_name\n\n    @catch_exceptions\n    def _add_idr_key_value(self):\n        logger.info (f\"{self.session_id}: Adding incident-detection-response-identifier to event\")\n        raw_json = None\n        try:\n            raw_json = json.loads(self.event[\"body\"])\n        except:\n            raw_json = self.event\n        # Replace the dictionary path, raw_json[\"detail\"][\"ProblemTitle\"], with the path to your alert name per your APM Payload.\n        raw_json[\"detail\"][\"incident-detection-response-identifier\"] = raw_json[\"detail\"][\"ProblemTitle\"]\n        logger.info(f\"{self.session_id}: Successfully added incident-detection-response-identifier to event: {raw_json}\")\n        return raw_json\n\n    @catch_exceptions\n    def put_event(self):\n        if self.response is not None and self.response != 200:\n            return\n        print(f\"{self.session_id}: Putting event to eventbus using {self.raw_json}. Current status code: {self.response}\")\n        response = client.put_events(\n            Entries=[{\n                'Detail': json.dumps(self.raw_json[\"detail\"], indent=2),\n                'DetailType': DETAIL_TYPE , # Do not modify.\n                'Source': DETAIL_SOURCE, # Do not modify.\n                'EventBusName': self.event_bus_name # Do not modify.\n            }]\n        )\n        if response[\"ResponseMetadata\"][\"HTTPStatusCode\"] != 200:\n            logger.error(f\"{self.session_id}: EventBus Put API Response: {response}\")\n            raise EventBusPutError(response)\n        else: \n            logger.info(f\"{self.session_id}: EventBus Put API Response: {response}\")\n        self.response = response[\"ResponseMetadata\"][\"HTTPStatusCode\"]\n\ndef create_api_gateway_response(transform_class_object):\n    status_code = int(transform_class_object.response)\n    if 200 <= status_code < 300:\n        response_status = 200\n        body = \"Success\"\n    elif 400 <= status_code < 500:\n        response_status = 400\n        body = \"Bad Request\"\n    else:\n        response_status = 500\n        body = \"Internal Server Error\"\n\n    if response_status != 200:\n        logger.error(f\"Exit Error: {transform_class_object.error_message}\")\n    else:\n        logger.info(f\"{transform_class_object.session_id}: Successfully sent event to eventbus {transform_class_object.event_bus_name}\\nincident-detection-response-identifier:{transform_class_object.raw_json['detail']['incident-detection-response-identifier']}\\nDetailType:{DETAIL_TYPE}\\nSource:{DETAIL_SOURCE}\")\n\n    response = {\n        \"statusCode\": response_status,\n        \"body\": json.dumps(body) if isinstance(body, dict) else str(body),\n        \"headers\": {\n            \"Content-Type\": \"application/json\",\n        }\n    }\n\n    return response\n\ndef lambda_handler(event, context):\n    logger.info(event)\n    transformation = Transformations(event)\n    if transformation.response is not None and transformation.response != 200:\n        return create_api_gateway_response(transformation)\n    transformation.put_event()\n    return create_api_gateway_response(transformation)"
                }
            },
            "DependsOn": [
                "TransformLambdaExecutionRole"
            ]
        },
        "AuthorizerLambdaExecutionRole": {
            "Type": "AWS::IAM::Role",
            "Properties": {
                "AssumeRolePolicyDocument": {
                    "Version": "2012-10-17",
                    "Statement": [
                        {
                            "Effect": "Allow",
                            "Principal": {
                                "Service": [
                                    "lambda.amazonaws.com"
                                ]
                            },
                            "Action": [
                                "sts:AssumeRole"
                            ]
                        }
                    ]
                },
                "Description": "IAM Role for Transform Lambda function to create logs and put events for customer Event Bus",
                "Policies": [
                    {
                        "PolicyName": "IDR-AuthorizerLambdaExecutionRolePolicy",
                        "PolicyDocument": {
                            "Version": "2012-10-17",
                            "Statement": [
                                {
                                    "Effect": "Allow",
                                    "Action": "logs:CreateLogGroup",
                                    "Resource": {
                                        "Fn::Sub": "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:*"
                                    }
                                },
                                {
                                    "Effect": "Allow",
                                    "Action": [
                                        "logs:CreateLogStream",
                                        "logs:PutLogEvents"
                                    ],
                                    "Resource": {
                                        "Fn::Sub": "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/${APMNameParameter}-AWSIncidentDetectionResponse-Lambda-Authorizer:*"
                                    }
                                },
                                {
                                    "Effect": "Allow",
                                    "Action": "secretsmanager:GetSecretValue",
                                    "Resource": {
                                        "Fn::Sub": "${SecretCreation}"
                                    }
                                }
                            ]
                        }
                    }
                ],
                "RoleName": {
                    "Fn::Sub": "IDR-AuthorizerLambdaExecutionRole-${AWS::Region}"
                }
            },
            "DependsOn": [
                "SecretCreation"
            ]
        },
        "AuthorizerLambdaFunction": {
            "Type": "AWS::Lambda::Function",
            "Properties": {
                "FunctionName": {
                    "Fn::Sub": "${APMNameParameter}-AWSIncidentDetectionResponse-Lambda-Authorizer"
                },
                "Handler": "index.handler",
                "Role": {
                    "Fn::GetAtt": "AuthorizerLambdaExecutionRole.Arn"
                },
                "Environment": {
                    "Variables": {
                        "EnvSecretName": {
                            "Ref": "SecretCreation"
                        }
                    }
                },
                "Runtime": "nodejs20.x",
                "Code": {
                    "ZipFile": "// This is an example Authentication Lambda that is configured for use with the Dynatrace APM, using a header named authorizationToken.\n// Please edit this code accordingly to meet your internal security posture and/or APM requirements.\nconst {\nSecretsManagerClient,\nGetSecretValueCommand\n    } = require(\"@aws-sdk/client-secrets-manager\");\n\nexports.handler = async(event) => {          \n\nconst secret_name = process.env.EnvSecretName;\n    const client = new SecretsManagerClient({\n        region: process.env.AWS_REGION\n        });\n    \n    let response;\n    \n    try {\n        response = await client.send(\n        new GetSecretValueCommand({\n            SecretId: secret_name,\n            VersionStage: \"AWSCURRENT\", // VersionStage defaults to AWSCURRENT if unspecified\n        })\n        );\n    } catch (error) {\n    throw error;\n    }\n\n    const secret = response.SecretString;\n    \n    const token = event['authorizationToken']\n    const resource = event[\"methodArn\"]\n\n    let permission = \"Deny\";\n    let authResult;\n    \n    if(token === JSON.parse(secret).APMSecureToken) {\n        permission = \"Allow\"\n        authResult = \"======= Authorization Success =======\";\n    } else {\n    authResult = \"======= Authorization Failure =======\";\n    }\n    console.log(authResult)\n    \n    const authResponse = { \n        \"principalId\": \"idrAuth\", \n        \"policyDocument\": \n            { \n                \"Version\": \"2012-10-17\", \n                \"Statement\": \n                        [\n                            {\n                                \"Action\": \"execute-api:Invoke\", \n                                \"Resource\": [resource], \n                                \"Effect\": permission\n                            }\n                        ]\n            }\n\n    }\n    return authResponse;\n};"
                }
            },
            "DependsOn": [
                "AuthorizerLambdaExecutionRole",
                "SecretCreation"
            ]
        },
        "RestAPIforAPM": {
            "Type": "AWS::ApiGateway::RestApi",
            "Properties": {
                "Name": {
                    "Fn::Sub": "${APMNameParameter}-AWSIncidentDetectionResponse-APIGW"
                },
                "EndpointConfiguration": {
                    "Types": [
                        "REGIONAL"
                    ]
                }
            }
        },
        "APIGWResourcesforAPM": {
            "Type": "AWS::ApiGateway::Resource",
            "Properties": {
                "ParentId": {
                    "Fn::GetAtt": "RestAPIforAPM.RootResourceId"
                },
                "PathPart": "APIGWResourcesforAPM",
                "RestApiId": {
                    "Ref": "RestAPIforAPM"
                }
            }
        },
        "APIGWAuthorizerforAPM": {
            "Type": "AWS::ApiGateway::Authorizer",
            "Properties": {
                "Name": {
                    "Fn::Sub": "${APMNameParameter}-APIGW-Authorizer"
                },
                "Type": "TOKEN",
                "IdentitySource": "method.request.header.authorizationToken",
                "AuthorizerUri": {
                    "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${AuthorizerLambdaFunction.Arn}/invocations"
                },
                "RestApiId": {
                    "Ref": "RestAPIforAPM"
                }
            }
        },
        "APMAPIGwMethod": {
            "Type": "AWS::ApiGateway::Method",
            "Properties": {
                "RestApiId": {
                    "Ref": "RestAPIforAPM"
                },
                "ResourceId": {
                    "Ref": "APIGWResourcesforAPM"
                },
                "HttpMethod": "POST",
                "MethodResponses": [
                    {
                        "StatusCode": 200
                    }
                ],
                "AuthorizationType": "CUSTOM",
                "AuthorizerId": {
                    "Ref": "APIGWAuthorizerforAPM"
                },
                "Integration": {
                    "Type": "AWS",
                    "IntegrationResponses": [
                        {
                            "StatusCode": 200
                        }
                    ],
                    "IntegrationHttpMethod": "POST",
                    "Uri": {
                        "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${TransformLambdaFunction.Arn}/invocations"
                    }
                }
            },
            "DependsOn": [
                "APIGWAuthorizerforAPM"
            ]
        },
        "APIGWCloudWatchRole": {
            "Type": "AWS::IAM::Role",
            "Properties": {
                "AssumeRolePolicyDocument": {
                    "Version": "2012-10-17",
                    "Statement": [
                        {
                            "Effect": "Allow",
                            "Principal": {
                                "Service": [
                                    "apigateway.amazonaws.com"
                                ]
                            },
                            "Action": "sts:AssumeRole"
                        }
                    ]
                },
                "Path": "/",
                "ManagedPolicyArns": [
                    "arn:aws:iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs"
                ]
            }
        },
        "Account": {
            "Type": "AWS::ApiGateway::Account",
            "Properties": {
                "CloudWatchRoleArn": {
                    "Fn::GetAtt": "APIGWCloudWatchRole.Arn"
                }
            }
        },
        "APIGWDeployment": {
            "DependsOn": [
                "APMAPIGwMethod",
                "APIGWCloudWatchRole"
            ],
            "Type": "AWS::ApiGateway::Deployment",
            "Properties": {
                "RestApiId": {
                    "Ref": "RestAPIforAPM"
                },
                "Description": "My deployment to prod",
                "StageName": {
                    "Fn::Sub": "${APMNameParameter}-Stage-Prod"
                },
                "StageDescription": {
                    "AccessLogSetting": {
                        "DestinationArn": {
                            "Fn::GetAtt": "MyLogGroup.Arn"
                        },
                        "Format": "$context.extendedRequestId $context.identity.sourceIp $context.identity.caller $context.identity.user [$context.requestTime] \"$context.httpMethod $context.resourcePath $context.protocol\" $context.status $context.responseLength $context.requestId"
                    }
                }
            }
        },
        "MyLogGroup": {
            "Type": "AWS::Logs::LogGroup",
            "Properties": {
                "LogGroupName": {
                    "Fn::Join": [
                        "-",
                        [
                            {
                                "Ref": "RestAPIforAPM"
                            },
                            "apigw-access-logs"
                        ]
                    ]
                }
            }
        },
        "APIGWUsagePlan": {
            "Type": "AWS::ApiGateway::UsagePlan",
            "Properties": {
                "ApiStages": [
                    {
                        "ApiId": {
                            "Ref": "RestAPIforAPM"
                        },
                        "Stage": {
                            "Fn::Sub": "${APMNameParameter}-Stage-Prod"
                        }
                    }
                ],
                "Description": "API Gateway throttling and quota limits on individual client API keys",
                "Quota": {
                    "Limit": 2000,
                    "Period": "MONTH"
                },
                "Throttle": {
                    "BurstLimit": 50,
                    "RateLimit": 20
                },
                "UsagePlanName": "APIGW_Throttling_Plan"
            },
            "DependsOn": [
                "APIGWDeployment"
            ]
        },
        "ConfigTransformLambdaPermissionforAPIGW": {
            "Type": "AWS::Lambda::Permission",
            "DependsOn": [
                "RestAPIforAPM",
                "TransformLambdaFunction"
            ],
            "Properties": {
                "Action": "lambda:InvokeFunction",
                "FunctionName": {
                    "Ref": "TransformLambdaFunction"
                },
                "Principal": "apigateway.amazonaws.com"
            }
        },
        "ConfigAuthorizerLambdaPermissionforAPIGW": {
            "Type": "AWS::Lambda::Permission",
            "DependsOn": [
                "RestAPIforAPM",
                "AuthorizerLambdaFunction"
            ],
            "Properties": {
                "Action": "lambda:InvokeFunction",
                "FunctionName": {
                    "Ref": "AuthorizerLambdaFunction"
                },
                "Principal": "apigateway.amazonaws.com"
            }
        }
    }
}