Parameters: CloudtraillakeEventDataStoreArn: Type: String AllowedPattern: ^arn:aws:cloudtrail:.* Description: The ARN of the CloudTrail Lake Event Data Store. Permission will be given to the Lambda function to query this event data store. NotifyEmailAddress: Type: String AllowedPattern: .+@.+ Description: "The email address which will recieve notifications from SNS for any service limit quota limits that are requested. Note: you will need to check your inbox after deploying the CDK and confirm the SNS subscription" Resources: CloudtraillakeQueryHandlerServiceRole5D9D9813: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Statement: - Action: sts:AssumeRole Effect: Allow Principal: Service: lambda.amazonaws.com Version: "2012-10-17" ManagedPolicyArns: - Fn::Join: - "" - - "arn:" - Ref: AWS::Partition - :iam::aws:policy/service-role/AWSLambdaBasicExecutionRole CloudtraillakeQueryHandlerServiceRoleDefaultPolicy3FB4268D: Type: AWS::IAM::Policy Properties: PolicyDocument: Statement: - Action: - cloudtrail:getQueryResults - cloudtrail:startQuery Effect: Allow Resource: Ref: CloudtraillakeEventDataStoreArn Version: "2012-10-17" PolicyName: CloudtraillakeQueryHandlerServiceRoleDefaultPolicy3FB4268D Roles: - Ref: CloudtraillakeQueryHandlerServiceRole5D9D9813 CloudtraillakeQuery: Type: AWS::Lambda::Function Properties: Code: ZipFile: "import json\nimport boto3\nimport time\nimport re\nimport datetime\nimport os\nclient = boto3.client('cloudtrail')\nRequiredParameters = ['EventDataStore', 'QueryStatement']\nMaxQueryResults = 100\n\ndef lambda_handler(event, context):\n \n # check the input parameters from the function invocation\n for requiredParam in RequiredParameters:\n if requiredParam not in event:\n return {\n 'statusCode': 400,\n 'body': json.dumps({\n \"MissingParameter\": requiredParam\n })\n }\n EventDataStore = event['EventDataStore']\n QueryStatement = event['QueryStatement']\n \n # obtain the event data store associated with this Lambda function if not provided\n if EventDataStore == '' or EventDataStore == \"FROM_ENV\":\n EventDataStore = os.environ['EVENT_DATA_STORE']\n \n # If a full Arn was passed, we only need the event data store ID\n matchEDS = re.search(\"^arn:.*eventdatastore\\/(.*)\", EventDataStore)\n if matchEDS:\n EventDataStore = matchEDS.group(1)\n \n # insert the EventDataStore into the QueryFormatParams if used m{EventDataStore} in place of hard coding it into the QueryStatement\n # if eventDataStore is derived from the environment, then it will be formatted in to the SQL\n if not 'QueryFormatParams' in event:\n event['QueryFormatParams'] = {}\n if not 'EventDataStore' in event['QueryFormatParams']:\n event['QueryFormatParams']['EventDataStore'] = EventDataStore\n \n # further manipulate the QueryStatement if the caller passed in parameters to format into the query \n # note the format is {m[VariableName]}\n # for example:\n # \"QueryStatement\": \"SELECT eventID, eventName, eventSource, eventTime FROM {m[EventDataStore]} WHERE ...\n # \"QueryFormatParams\" : {\n # \"EventDataStore\": \"996f9246-56ad-49eb-bdd2-7276a6d17884\",\n # \"invalidParams\": \"will not be inserted\"\n #}\n if 'QueryFormatParams' in event:\n QueryStatement = QueryStatement.format(m=event['QueryFormatParams'])\n \n # start the query\n response = client.start_query(\n QueryStatement=QueryStatement\n )\n QueryId = response['QueryId']\n \n # begin getting query results\n QueryResultRows = []\n NextToken = None\n \n while 1:\n\n # get the batch of query results\n args = {}\n args['EventDataStore'] = EventDataStore\n args['QueryId'] = QueryId\n args['MaxQueryResults'] = MaxQueryResults\n if NextToken is not None:\n args['NextToken'] = NextToken\n response = client.get_query_results(**args)\n \n # handle if query is not yet finished\n if response['QueryStatus'] != 'FINISHED':\n time.sleep(1)\n continue\n \n # save the results and continue getting results if any\n if 'QueryResultRows' in response:\n QueryResultRows.extend(response['QueryResultRows'])\n if 'NextToken' in response:\n NextToken=response['NextToken']\n else:\n break\n\n print(\"CloudTrail Lake Query:\", { \"TotalResults\": len(QueryResultRows), \"QueryStatement\": QueryStatement, \"EventDataStore\": EventDataStore } )\n\n return {\n 'statusCode': 200,\n 'body': QueryResultRows\n }\n\n" Role: Fn::GetAtt: - CloudtraillakeQueryHandlerServiceRole5D9D9813 - Arn Environment: Variables: EVENT_DATA_STORE: Ref: CloudtraillakeEventDataStoreArn Handler: index.lambda_handler Runtime: python3.9 Timeout: 300 DependsOn: - CloudtraillakeQueryHandlerServiceRoleDefaultPolicy3FB4268D - CloudtraillakeQueryHandlerServiceRole5D9D9813 ServiceLimitCheckerKey1E901EA9: Type: AWS::KMS::Key Properties: KeyPolicy: Statement: - Action: kms:* Effect: Allow Principal: AWS: Fn::Join: - "" - - "arn:" - Ref: AWS::Partition - ":iam::" - Ref: AWS::AccountId - :root Resource: "*" Version: "2012-10-17" UpdateReplacePolicy: Retain DeletionPolicy: Retain ServiceLimitChecker3F9FF6D3: Type: AWS::SNS::Topic Properties: KmsMasterKeyId: Fn::GetAtt: - ServiceLimitCheckerKey1E901EA9 - Arn ServiceLimitCheckerTokenSubscription1819F69A8: Type: AWS::SNS::Subscription Properties: Protocol: email TopicArn: Ref: ServiceLimitChecker3F9FF6D3 Endpoint: Ref: NotifyEmailAddress Role1ABCC5F0: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Statement: - Action: sts:AssumeRole Effect: Allow Principal: Service: Fn::Join: - "" - - states. - Ref: AWS::Region - .amazonaws.com Version: "2012-10-17" Description: Role for CloudtraillakeOrchectrator state machine to interface with other AWS resources RoleDefaultPolicy5FFB7DAB: Type: AWS::IAM::Policy Properties: PolicyDocument: Statement: - Action: lambda:InvokeFunction Effect: Allow Resource: - Fn::GetAtt: - CloudtraillakeQuery - Arn - Fn::Join: - "" - - Fn::GetAtt: - CloudtraillakeQuery - Arn - :* - Action: sns:Publish Effect: Allow Resource: Ref: ServiceLimitChecker3F9FF6D3 - Action: - kms:Decrypt - kms:Encrypt - kms:GenerateDataKey* - kms:ReEncrypt* Effect: Allow Resource: Fn::GetAtt: - ServiceLimitCheckerKey1E901EA9 - Arn Version: "2012-10-17" PolicyName: RoleDefaultPolicy5FFB7DAB Roles: - Ref: Role1ABCC5F0 CloudtraillakeOrchestrator0B66B823: Type: AWS::StepFunctions::StateMachine Properties: RoleArn: Fn::GetAtt: - Role1ABCC5F0 - Arn DefinitionString: Fn::Join: - "" - - '{"Comment":"A sample state machine that queries CloudTrail Lake using the CloudtraillakeQuery Lambda function to demonstrate its capabilities.","StartAt":"CloudtraillakeQuery_RequestServiceQuotaIncrease","States":{"CloudtraillakeQuery_RequestServiceQuotaIncrease":{"Type":"Task","Resource":"arn:aws:states:::lambda:invoke","Parameters":{"FunctionName":"' - Fn::GetAtt: - CloudtraillakeQuery - Arn - "\",\"Payload\":{\"EventDataStore\":\"FROM_ENV\",\"QueryStatement\":\"SELECT json_extract_scalar(element_at(responseElements, 'requestedQuota'), '$.id') as requestId, awsRegion, recipientAccountId FROM {m[EventDataStore]} WHERE eventSource='servicequotas.amazonaws.com' and eventname = 'RequestServiceQuotaIncrease'\"}},\"Retry\":[{\"ErrorEquals\":[\"Lambda.ServiceException\",\"Lambda.AWSLambdaException\",\"Lambda.SdkClientException\"],\"IntervalSeconds\":2,\"MaxAttempts\":6,\"BackoffRate\":2}],\"Next\":\"Each_RequestServiceQuotaIncrease\"},\"Each_RequestServiceQuotaIncrease\":{\"Type\":\"Map\",\"End\":true,\"Iterator\":{\"StartAt\":\"CloudtraillakeQuery_UpdateServiceQuotaIncreaseRequestStatus\",\"States\":{\"CloudtraillakeQuery_UpdateServiceQuotaIncreaseRequestStatus\":{\"Type\":\"Task\",\"Resource\":\"arn:aws:states:::lambda:invoke\",\"Parameters\":{\"FunctionName\":\"" - Fn::GetAtt: - CloudtraillakeQuery - Arn - "\",\"Payload\":{\"EventDataStore\":\"FROM_ENV\",\"QueryStatement\":\"SELECT recipientAccountId, awsRegion, serviceEventDetails FROM {m[EventDataStore]} WHERE eventSource='servicequotas.amazonaws.com' and eventname = 'UpdateServiceQuotaIncreaseRequestStatus' and element_at(serviceEventDetails, 'requestId') = '{m[RequestId]}'\",\"QueryFormatParams\":{\"RequestId.$\":\"$[0].requestId\"}}},\"Retry\":[{\"ErrorEquals\":[\"Lambda.ServiceException\",\"Lambda.AWSLambdaException\",\"Lambda.SdkClientException\"],\"IntervalSeconds\":2,\"MaxAttempts\":6,\"BackoffRate\":2}],\"Next\":\"Send_Report\"},\"Send_Report\":{\"Type\":\"Task\",\"Resource\":\"arn:aws:states:::sns:publish\",\"Parameters\":{\"Message\":{\"ServiceLimitIncreaseStatus.$\":\"$.Payload.body[0]\"},\"TopicArn\":\"" - Ref: ServiceLimitChecker3F9FF6D3 - '"},"End":true}}},"ItemsPath":"$.Payload.body"}}}' DependsOn: - RoleDefaultPolicy5FFB7DAB - Role1ABCC5F0