From 39352c330f02f93b1ce0beb3dd21f674551c6e2a Mon Sep 17 00:00:00 2001 From: Raphael Bottino Date: Thu, 6 Apr 2023 13:43:51 -0500 Subject: [PATCH 01/25] First version of trend-cloudone-onboard. --- .gitignore | 8 + .gitmodules | 3 + .taskcat.yml | 22 +- VERSION | 2 +- submodules/cfn-abi-aws-cloudtrail | 1 + templates/sample-workload.template.yaml | 97 ------- templates/trend-cloudone-onboard/README.md | 83 ++++++ .../cloudone.template.yaml | 136 ++++++++++ .../cloudtrail.template.yaml | 145 +++++++++++ .../get-cloudone-region-and-account.yaml | 104 ++++++++ .../trend-cloudone-onboard/main.template.yaml | 132 ++++++++++ .../vision-one-enrollment.template.yaml | 121 +++++++++ .../workloadsecurity.template.yaml | 238 ++++++++++++++++++ 13 files changed, 980 insertions(+), 112 deletions(-) create mode 100644 .gitmodules create mode 160000 submodules/cfn-abi-aws-cloudtrail delete mode 100644 templates/sample-workload.template.yaml create mode 100644 templates/trend-cloudone-onboard/README.md create mode 100644 templates/trend-cloudone-onboard/cloudone.template.yaml create mode 100644 templates/trend-cloudone-onboard/cloudtrail.template.yaml create mode 100644 templates/trend-cloudone-onboard/get-cloudone-region-and-account.yaml create mode 100644 templates/trend-cloudone-onboard/main.template.yaml create mode 100644 templates/trend-cloudone-onboard/vision-one-enrollment.template.yaml create mode 100644 templates/trend-cloudone-onboard/workloadsecurity.template.yaml diff --git a/.gitignore b/.gitignore index 38dccc3..02e417d 100644 --- a/.gitignore +++ b/.gitignore @@ -160,3 +160,11 @@ cython_debug/ # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ + +# taskcat +taskcat_outputs/ +.taskcat/ +.taskcat.secrets.yml + +# General +.DS_Store \ No newline at end of file diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..4edf46f --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "submodules/cfn-abi-aws-cloudtrail"] + path = submodules/cfn-abi-aws-cloudtrail + url = https://github.com/aws-ia/cfn-abi-aws-cloudtrail.git diff --git a/.taskcat.yml b/.taskcat.yml index 1d06399..b7ff2b7 100644 --- a/.taskcat.yml +++ b/.taskcat.yml @@ -1,7 +1,8 @@ project: - name: update-me-to-project-repo-name - owner: quickstart@amazon.com + name: cfn-abi-trend-cloudone + owner: raphael_bottino@trendmicro.com #We need a DL for this. package_lambda: false + shorten_stack_name: true regions: - ap-northeast-1 - ap-northeast-2 @@ -16,18 +17,11 @@ project: tests: sample: parameters: - Param1: 'Inputs to Stack' # Examples: of other taskcat dynamic input parameters for more into see http://taskcat.io - # - # AvailabilityZones: $[taskcat_genaz_3] - # ByteValue: 1 - # PasswordA: $[taskcat_genpass_8A] - # PasswordB: $[taskcat_genpass_32S] - # RandomNumber: $[taskcat_random-numbers] - # RandomString: $[taskcat_random-string] - # StackName: TestStack - # UUID: $[taskcat_genuuid] - # + CloudOneApiKey: $[taskcat_secrets_get_CloudOneApiKey] + VisionOneServiceToken: $[taskcat_secrets_get_VisionOneServiceToken] + QSS3BucketName: $[taskcat_autobucket] + QSS3KeyPrefix: "cfn-abi-trend-cloudone/templates/trend-cloudone-onboard/" regions: - us-east-1 - template: templates/sample-workload.template.yaml + template: templates/trend-cloudone-onboard/main.template.yaml diff --git a/VERSION b/VERSION index ae39fab..45c7a58 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -v0.0.0 +v0.0.1 diff --git a/submodules/cfn-abi-aws-cloudtrail b/submodules/cfn-abi-aws-cloudtrail new file mode 160000 index 0000000..765e285 --- /dev/null +++ b/submodules/cfn-abi-aws-cloudtrail @@ -0,0 +1 @@ +Subproject commit 765e2853d92d4b15fddb212ea5416c6ac648562f diff --git a/templates/sample-workload.template.yaml b/templates/sample-workload.template.yaml deleted file mode 100644 index 989e996..0000000 --- a/templates/sample-workload.template.yaml +++ /dev/null @@ -1,97 +0,0 @@ ---- -AWSTemplateFormatVersion: '2010-09-09' -Description: Sample Stack -Parameters: - Param1: - Description: Param1 - Type: String -Resources: - LambdaExecutionRole: - Type: AWS::IAM::Role - Properties: - AssumeRolePolicyDocument: - Version: '2012-10-17' - Statement: - - Effect: Allow - Principal: - Service: - - lambda.amazonaws.com - Action: - - sts:AssumeRole - Path: "/" - Policies: - - PolicyName: lambda_policy - PolicyDocument: - Version: '2012-10-17' - Statement: - - Effect: Allow - Action: - - logs:CreateLogGroup - - logs:CreateLogStream - - logs:PutLogEvents - Resource: arn:aws:logs:*:*:* - - Effect: Allow - Action: - - cloudformation:DescribeStacks - Resource: "*" - GenID: - Type: AWS::Lambda::Function - Properties: - Code: - ZipFile: - Fn::Join: - - "\n" - - - import random - - import json - - import cfnresponse - - from cfnresponse import send, SUCCESS - - 'def handler(event, context):' - - " if event['RequestType'] == 'Delete':" - - " send(event, context, 'SUCCESS', {})" - - " return" - - " if event['RequestType'] == 'Create':" - - ' token= "%0x.%0x" % (random.SystemRandom().getrandbits(3*8), - random.SystemRandom().getrandbits(8*8))' - - " responseData = {}" - - " responseData['Data'] = token" - - " send(event, context, 'SUCCESS', responseData)" - - " return token" - Handler: index.handler - Runtime: python3.7 - Timeout: 5 - Role: - Fn::GetAtt: - - LambdaExecutionRole - - Arn - GetID: - Type: Custom::GenerateID - Version: '1.0' - Properties: - ServiceToken: - Fn::GetAtt: - - GenID - - Arn - ResponseURL: - Fn::Join: - - '' - - - http://ResponseURL - - Ref: AWS::StackId - - RequestId - StackId: - Ref: AWS::StackId - ResourceProperties: - RequestType: Create - RequestId: - Fn::Join: - - '' - - - Ref: AWS::StackId - - RequestId - LogicalResourceId: GenIDLogicalResourceId -Outputs: - ClusterID: - Value: - Fn::GetAtt: - - GetID - - Data - Param1Output: - Value: !Ref Param1 diff --git a/templates/trend-cloudone-onboard/README.md b/templates/trend-cloudone-onboard/README.md new file mode 100644 index 0000000..b2a538b --- /dev/null +++ b/templates/trend-cloudone-onboard/README.md @@ -0,0 +1,83 @@ +# Add AWS Account to Cloud One + +To fully integrate an AWS account in Cloud One, you must deploy resources in your AWS account and do manual steps in Trend Cloud One dashboard. This CloudFormation template automates all these steps on your behalf, including integrating it to Vision One. + +## What does it actually do? + +1. A Custom Resource gets the Cloud One Account ID and Cloud One Region. +2. A Custom Resource completes the integration between the Cloud One and Vision One accounts. +3. All the required IAM resources for Workload Security is created. +4. A Custom Resource completes the integration between the AWS and Workload Security accounts. +5. The default Cloud One CloudFormation stack is deployed. +6. A Custom Resource completes the integration between the AWS and Cloud One accounts. +7. A Custom Resource gets from Cloud One backend the Token for CloudTrail integration. +8. The default CloudTrail CloudFormation stack is deployed. + +## Requirements + +- Have an API Key for a [Cloud One](https://www.trendmicro.com/cloudone) account. Click [here](https://cloudone.trendmicro.com/docs/identity-and-account-management/c1-api-key/#new-api-key) for a guide on how to generate an API Key. +- An AWS Account with Admin permissions +- Generate a Vision One Enrollment Token. See step #1 in [this documentation](https://docs.trendmicro.com/en-us/enterprise/trend-micro-xdr-help/ConfiguringCloudOneWorkloadSecurity). + +## Limitations + +- Your Stack name must be up 8 characters long or shorter. I recommend `CloudOne`. +- You must deploy the stack to the following region based on your Cloud One account region: + +| Cloud One Region | AWS Region | +| ----------------- | --------------- | +| us-1 | us-east-1 | +| in-1 | ap-south-1 | +| gb-1 | eu-west-2 | +| au-1 | ap-southeast-2 | +| de-1 | eu-central-1 | +| jp-1 | ap-northeast-1 | +| sg-1 | ap-southeast-1 | +| ca-1 | ca-central-1 | + +## Parameters + +### Required + +- CloudOneApiKey + - Description: Cloud One API Key. See Requirements above for more details. +- VisionOneServiceToken + - Description: Vision One Service Token. See Requirements above for more details. +- CreateNewTrail: + - Description: Decides if a new Trail should be created. Defaults to False, so you must enter a S3 Bucket name in the ExistingCloudtrailBucketName parameter. In case you pick True, a new trail and bucket will be created for you. Setting this to True will incur in extra costs. +- ExistingCloudtrailBucketName: + - Description: Specify the name of an existing bucket that you want to use for forwarding to Trend Micro Cloud One. Only used if CreateNewTrail is set to False. + +### Shouldn't be Changed from Default + +These are going to be changed in case you decide to host the templates yourself. `QSS3BucketName` should be the bucket name that you host these templates from and `QSS3KeyPrefix` would be the key prefix/path of the root "folder" for these templates. Example: If the files are hosted in the bucket named `my-bucket` and inside the folder `trendmicro/onboarding`, `QSS3BucketName` value should be `my-bucket` and `QSS3KeyPrefix` value should be `trendmicro/onboarding`. + +- QSS3BucketName: + - Default: cloudone-community + - Description: S3 bucket name for the deployment assets. Deployment bucket name + can include numbers, lowercase letters, uppercase letters, and hyphens (-). +- QSS3KeyPrefix: + - Default: "" + - Description: S3 key prefix for the Deployment assets. Deployment key prefix can include numbers, lowercase letters uppercase letters, hyphens (-), dots(.) and forward slash (/). + +## Deployment + +### Via Dashboard + +[![Launch Stack](https://cdn.rawgit.com/buildkite/cloudformation-launch-stack-button-svg/master/launch-stack.svg)](https://console.aws.amazon.com/cloudformation/home#/stacks/new?stackName=CloudOne&templateURL=https://cloudone-community.s3.us-east-1.amazonaws.com/latest/Common/Cloud-Account/aws-cfn-cloud-account-connector/main.template.yaml) + +### Via CLI + +You can run the following: + +```bash +#!/bin/bash +export BUCKET="your-cloudtrail-bucket" +export APIKEY="your-cloudone-apikey" +export TOKEN="your-visionone-enrollment-token" +aws cloudformation create-stack --stack-name common-onboard-test --template-url https://cloudone-community.s3.us-east-1.amazonaws.com/latest/Common/Cloud-Account/aws-cfn-cloud-account-connector/main.template.yaml --capabilities CAPABILITY_NAMED_IAM CAPABILITY_AUTO_EXPAND --parameters ParameterKey=ExistingCloudtrailBucketName,ParameterValue=$BUCKET ParameterKey=CloudOneApiKey,ParameterValue=$APIKEY ParameterKey=VisionOneServiceToken,ParameterValue=$TOKEN ParameterKey=QSS3KeyPrefix,ParameterValue=$HASH/ +``` + +## Removal or Deployment Failure + +If one decides to remove this stack, or if it fails during deployment, all modifications made by it, including any kind of account integration, will be reverted back to its pre-deployment state. The only exception is in the Vision One side. The manually created token needs to be deleted manually as well, otherwise the Cloud One account will remain enrolled to the Vision One account. diff --git a/templates/trend-cloudone-onboard/cloudone.template.yaml b/templates/trend-cloudone-onboard/cloudone.template.yaml new file mode 100644 index 0000000..e828980 --- /dev/null +++ b/templates/trend-cloudone-onboard/cloudone.template.yaml @@ -0,0 +1,136 @@ +AWSTemplateFormatVersion: 2010-09-09 +Description: Trend Cloud One Workload Security Integration. + +Parameters: + CloudOneAccountID: + Description: Cloud One Account Id. You can learn more about it at https://cloudone.trendmicro.com/docs/identity-and-account-management/c1-api-key/ + Type: String + CloudOneApiKey: + Description: Cloud One API Key. You can learn more about it at https://cloudone.trendmicro.com/docs/identity-and-account-management/c1-api-key/ + Type: String + NoEcho: true + CloudOneRegion: + Description: Cloud One Region. More info at https://cloudone.trendmicro.com/docs/identity-and-account-management/c1-regions/ + Type: String + Default: us-1 + AllowedValues: + - us-1 + - trend-us-1 + - au-1 + - ie-1 + - sg-1 + - in-1 + - jp-1 + - ca-1 + - de-1 + + +Resources: + CloudOneIntegrationStack: + Type: AWS::CloudFormation::Stack + Properties: + Parameters: + CloudOneRegion: !Ref CloudOneRegion + CloudOneAccountID: !Ref CloudOneAccountID + CloudOneOIDCProviderURL: !Sub 'cloudaccounts.${CloudOneRegion}.cloudone.trendmicro.com' + TemplateURL: !Sub 'https://cloud-one-cloud-accounts-${AWS::Region}.s3.${AWS::Region}.amazonaws.com/templates/aws/cloud-account-management-role.template' + + AddAWSAccountToCloudOneFunction: + Type: AWS::Lambda::Function + Properties: + Runtime: python3.9 + Architectures: + - arm64 + Timeout: 60 + Handler: index.lambda_handler + Role: !GetAtt AddAWSAccountToCloudOneFunctionRole.Arn + Environment: + Variables: + CloudOneRoleArn: !GetAtt CloudOneIntegrationStack.Outputs.CloudOneRoleArn + CloudOneRegion: !Ref CloudOneRegion + CloudOneApiKey: !Ref CloudOneApiKey + Code: + ZipFile: + !Sub + |- + import json + import os + import urllib3 + import boto3 + import cfnresponse + + def lambda_handler(event, context): + status = cfnresponse.SUCCESS + response_data = {} + physicalResourceId = None + try: + + cloudOneRoleArn = os.environ['CloudOneRoleArn'] + cloudOneRegion = os.environ['CloudOneRegion'] + cloudOneApiKey = os.environ['CloudOneApiKey'] + + headers = { + 'api-version': 'v1', + 'Authorization': 'ApiKey '+cloudOneApiKey+'', + 'Content-Type': 'application/json' + } + + http = urllib3.PoolManager() + + + if event["RequestType"] == "Create" or event["RequestType"] == "Update": + + url = 'https://cloudaccounts.'+cloudOneRegion+'.cloudone.trendmicro.com/api/cloudaccounts/aws' + + payload = json.dumps({ + 'roleARN': cloudOneRoleArn + }) + encoded_payload = payload.encode("utf-8") + print(url) + response = http.request("POST", url=url, headers=headers, body=encoded_payload) + print(response) + response_json_data = json.loads(response.data.decode("utf-8")) + print(response_json_data) + physicalResourceId = response_json_data["id"] + response_data = {"ID": response_json_data["id"]} + + else: # if event["RequestType"] == "Delete": + id = event["PhysicalResourceId"] + + url = 'https://cloudaccounts.'+cloudOneRegion+'.cloudone.trendmicro.com/api/cloudaccounts/aws/' + id + + print(url) + response = http.request("DELETE", url=url, headers=headers) + print(response) + + except Exception as e: + print(e) + status = cfnresponse.FAILED + + cfnresponse.send(event, context, status, response_data, physicalResourceId) + + AddAWSAccountToCloudOne: + Type: AWS::CloudFormation::CustomResource + Properties: + ServiceToken: !GetAtt AddAWSAccountToCloudOneFunction.Arn + + AddAWSAccountToCloudOneFunctionRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Principal: + Service: + - lambda.amazonaws.com + Action: + - sts:AssumeRole + Path: "/" + ManagedPolicyArns: + - Fn::Join: + - "" + - - "arn:" + - Ref: AWS::Partition + - :iam::aws:policy/service-role/AWSLambdaBasicExecutionRole + diff --git a/templates/trend-cloudone-onboard/cloudtrail.template.yaml b/templates/trend-cloudone-onboard/cloudtrail.template.yaml new file mode 100644 index 0000000..ce25cde --- /dev/null +++ b/templates/trend-cloudone-onboard/cloudtrail.template.yaml @@ -0,0 +1,145 @@ +AWSTemplateFormatVersion: 2010-09-09 +Description: Trend Cloud One CloudTrail Onboarding. + +Parameters: + CloudOneApiKey: + Description: Cloud One API Key. You can learn more about it at https://cloudone.trendmicro.com/docs/identity-and-account-management/c1-api-key/ + Type: String + NoEcho: true + CloudOneRegion: + Description: Cloud One Region. More info at https://cloudone.trendmicro.com/docs/identity-and-account-management/c1-regions/ + Type: String + Default: us-1 + AllowedValues: + - us-1 + - trend-us-1 + - au-1 + - ie-1 + - sg-1 + - in-1 + - jp-1 + - ca-1 + +Resources: + GetCloudTrailStackParametersFunction: + Type: AWS::Lambda::Function + Properties: + Runtime: python3.9 + Architectures: + - arm64 + Timeout: 60 + Handler: index.lambda_handler + Role: !GetAtt GetCloudTrailStackParametersRole.Arn + Environment: + Variables: + CloudOneRegion: !Ref CloudOneRegion + CloudOneApiKey: !Ref CloudOneApiKey + AwsAccountId: !Ref "AWS::AccountId" + AwsRegion: !Ref "AWS::Region" + Code: + ZipFile: + !Sub + |- + import json + import os + import urllib3 + import cfnresponse + import re + import uuid + from urllib.parse import unquote + + def lambda_handler(event, context): + status = cfnresponse.SUCCESS + response_data = {} + physicalResourceId = None + try: + + if event["RequestType"] == "Create" or event["RequestType"] == "Update": + cloudOneRegion = os.environ['CloudOneRegion'] + cloudOneApiKey = os.environ['CloudOneApiKey'] + awsAccountId = os.environ['AwsAccountId'] + awsRegion = os.environ['AwsRegion'] + + url = 'https://cloudtrail.'+cloudOneRegion+'.cloudone.trendmicro.com/api/stacks' + + payload = json.dumps({ + 'providerAccountID': awsAccountId, + 'awsRegion': awsRegion + }) + headers = { + 'api-version': 'v1', + 'Authorization': 'ApiKey '+cloudOneApiKey+'', + 'Content-Type': 'application/json' + } + + http = urllib3.PoolManager() + encoded_payload = payload.encode("utf-8") + print(url) + response = http.request("POST", url=url, headers=headers, body=encoded_payload) + print(response) + response_json_data = json.loads(response.data.decode("utf-8")) + print(response_json_data) + encodedDeploymentUrl = response_json_data["deploymentURL"] + print(encodedDeploymentUrl) + templateUrl = re.search(r"templateUrl=([^&]+)", encodedDeploymentUrl).group(1) + s3BucketKey = re.search(r"param_S3BucketKey=([^&]+)", encodedDeploymentUrl).group(1) + s3BucketName = re.search(r"param_S3BucketName=([^&]+)", encodedDeploymentUrl).group(1) + serviceToken = re.search(r"param_ServiceToken=([^&]+)", encodedDeploymentUrl).group(1) + serviceUrl = re.search(r"param_ServiceURL=([^&]+)", encodedDeploymentUrl).group(1) + apiVersion = re.search(r"param_APIVersion=([^\n]+)", encodedDeploymentUrl).group(1) + + physicalResourceId = str(uuid.uuid4()) + response_data = { + "TemplateUrl": templateUrl, + "S3BucketKey": s3BucketKey, + "S3BucketName": s3BucketName, + "ServiceToken": serviceToken, + "ServiceUrl": unquote(serviceUrl), + "ApiVersion": apiVersion + } + + else: # if event["RequestType"] == "Delete": + physicalResourceId = event["PhysicalResourceId"] + + except Exception as e: + print(e) + status = cfnresponse.FAILED + + cfnresponse.send(event, context, status, response_data, physicalResourceId) + + GetCloudTrailStackParameters: + Type: AWS::CloudFormation::CustomResource + Properties: + ServiceToken: !GetAtt GetCloudTrailStackParametersFunction.Arn + + GetCloudTrailStackParametersRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Principal: + Service: + - lambda.amazonaws.com + Action: + - sts:AssumeRole + Path: "/" + ManagedPolicyArns: + - Fn::Join: + - "" + - - "arn:" + - Ref: AWS::Partition + - :iam::aws:policy/service-role/AWSLambdaBasicExecutionRole + +Outputs: + ServiceToken: + Value: !GetAtt GetCloudTrailStackParameters.ServiceToken + ServiceURL: + Value: !GetAtt GetCloudTrailStackParameters.ServiceUrl + S3BucketName: + Value: !GetAtt GetCloudTrailStackParameters.S3BucketName + APIVersion: + Value: !GetAtt GetCloudTrailStackParameters.ApiVersion + TemplateURL: + Value: !GetAtt GetCloudTrailStackParameters.TemplateUrl \ No newline at end of file diff --git a/templates/trend-cloudone-onboard/get-cloudone-region-and-account.yaml b/templates/trend-cloudone-onboard/get-cloudone-region-and-account.yaml new file mode 100644 index 0000000..49ab3e3 --- /dev/null +++ b/templates/trend-cloudone-onboard/get-cloudone-region-and-account.yaml @@ -0,0 +1,104 @@ +AWSTemplateFormatVersion: 2010-09-09 +Description: Get Trend Cloud One region and account id given an API Key. + +Parameters: + CloudOneApiKey: + Description: Cloud One API Key. You can learn more about it at https://cloudone.trendmicro.com/docs/identity-and-account-management/c1-api-key/ + Type: String + NoEcho: true + +Resources: + GetCloudOneRegionAndAccountFunction: + Type: AWS::Lambda::Function + Properties: + Runtime: python3.9 + Architectures: + - arm64 + Timeout: 60 + Handler: index.lambda_handler + Role: !GetAtt GetCloudOneRegionAndAccountRole.Arn + Environment: + Variables: + CloudOneApiKey: !Ref CloudOneApiKey + Code: + ZipFile: + !Sub + |- + import json + import os + import urllib3 + import boto3 + import cfnresponse + + def lambda_handler(event, context): + status = cfnresponse.SUCCESS + response_data = {} + physicalResourceId = None + try: + + if event["RequestType"] == "Create" or event["RequestType"] == "Update": + cloudOneApiKey = os.environ['CloudOneApiKey'] + apiKeyId = cloudOneApiKey.split(':')[0] + + url = 'https://accounts.cloudone.trendmicro.com/api/apikeys/' + apiKeyId + + headers = { + 'api-version': 'v1', + 'Authorization': 'ApiKey '+cloudOneApiKey+'', + 'Content-Type': 'application/json' + } + + http = urllib3.PoolManager() + print(url) + response = http.request("GET", url=url, headers=headers) + print(response) + response_json_data = json.loads(response.data.decode("utf-8")) + print(response_json_data) + urn = response_json_data["urn"] + region = urn.split(":")[3] + accountId = urn.split(":")[4] + physicalResourceId = response_json_data["urn"] + response_data = {"AccountId": accountId, "Region": region} + + else: # if event["RequestType"] == "Delete": + physicalResourceId = event["PhysicalResourceId"] + + except Exception as e: + print(e) + status = cfnresponse.FAILED + + cfnresponse.send(event, context, status, response_data, physicalResourceId) + + GetCloudOneRegionAndAccount: + Type: AWS::CloudFormation::CustomResource + Properties: + ServiceToken: !GetAtt GetCloudOneRegionAndAccountFunction.Arn + + GetCloudOneRegionAndAccountRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Principal: + Service: + - lambda.amazonaws.com + Action: + - sts:AssumeRole + Path: "/" + ManagedPolicyArns: + - Fn::Join: + - "" + - - "arn:" + - Ref: AWS::Partition + - :iam::aws:policy/service-role/AWSLambdaBasicExecutionRole + +Outputs: + CloudOneAccountId: + Description: Cloud One Account Id + Value: !GetAtt GetCloudOneRegionAndAccount.AccountId + + CloudOneRegion: + Description: Cloud One Account Region + Value: !GetAtt GetCloudOneRegionAndAccount.Region \ No newline at end of file diff --git a/templates/trend-cloudone-onboard/main.template.yaml b/templates/trend-cloudone-onboard/main.template.yaml new file mode 100644 index 0000000..79678a2 --- /dev/null +++ b/templates/trend-cloudone-onboard/main.template.yaml @@ -0,0 +1,132 @@ +AWSTemplateFormatVersion: 2010-09-09 +Description: Trend Cloud One All-In-One Onboard Stack + +Metadata: + AWS::CloudFormation::Interface: + ParameterGroups: + - Label: + default: 'Please adjust the fields below as required for deployment.' + Parameters: + - CloudOneApiKey + - VisionOneServiceToken + - CreateNewTrail + - ExistingCloudtrailBucketName + - Label: + default: 'Warning: Do not modify the fields below unless you know what you + are doing. Modifications may cause your deployment to fail.' + Parameters: + - QSS3BucketName + - QSS3KeyPrefix + ParameterLabels: + CloudOneApiKey: + default: CloudOneApiKey + VisionOneServiceToken: + default: VisionOneServiceToken + + +Parameters: + CloudOneApiKey: + Description: Cloud One API Key. You can learn more about it at https://cloudone.trendmicro.com/docs/identity-and-account-management/c1-api-key/ + Type: String + NoEcho: true + VisionOneServiceToken: + Description: Vision One Service Token. See step 1 at https://docs.trendmicro.com/en-us/enterprise/trend-micro-xdr-help/ConfiguringCloudOneWorkloadSecurity/ + Type: String + NoEcho: true + CreateNewTrail: + Description: Decides if a new Trail should be created. Defaults to False, + so you must enter a S3 Bucket name in the ExistingCloudtrailBucketName + parameter. In case you pick True, a new trail and bucket will be created + for you. Setting this to true will incur in extra costs. + Type: String + AllowedValues: + - "True" + - "False" + Default: "False" + ExistingCloudtrailBucketName: + Description: Specify the name of an existing bucket that you want to use for forwarding + to Trend Micro Cloud One. + Type: String + Default: EnterAValueHere + QSS3BucketName: + AllowedPattern: ^[0-9a-zA-Z]+([0-9a-zA-Z-]*[0-9a-zA-Z])*$ + ConstraintDescription: Deployment bucket name can include numbers, lowercase + letters, uppercase letters, and hyphens (-). It cannot start or end with a hyphen + (-). + Default: aws-abi-pilot + Description: S3 bucket name for the Deployment assets. Deployment bucket name + can include numbers, lowercase letters, uppercase letters, and hyphens (-). + It cannot start or end with a hyphen (-). + Type: String + QSS3KeyPrefix: + AllowedPattern: ^[0-9a-zA-Z-/.]*$ + ConstraintDescription: Deployment key prefix can include numbers, lowercase letters, + uppercase letters, hyphens (-), dots(.) and forward slash (/). + Default: "cfn-abi-aws-trend-cloudone/" + Description: S3 key prefix for the Deployment assets. Deployment key prefix + can include numbers, lowercase letters, uppercase letters, hyphens (-), dots(.) and + forward slash (/). + Type: String + +Conditions: + HasNoExistingCloudtrailBucketName: + !Equals ["True", !Ref CreateNewTrail] + +Resources: + GetCloudOneRegionAndAccountStack: + Type: AWS::CloudFormation::Stack + Properties: + Parameters: + CloudOneApiKey: !Ref CloudOneApiKey + TemplateURL: !Sub 'https://${QSS3BucketName}.s3.amazonaws.com/${QSS3KeyPrefix}get-cloudone-region-and-account.yaml' + + VisionOneEnrollmentStack: + Type: AWS::CloudFormation::Stack + Properties: + Parameters: + CloudOneApiKey: !Ref CloudOneApiKey + CloudOneRegion: !GetAtt GetCloudOneRegionAndAccountStack.Outputs.CloudOneRegion + VisionOneServiceToken: !Ref VisionOneServiceToken + TemplateURL: !Sub 'https://${QSS3BucketName}.s3.amazonaws.com/${QSS3KeyPrefix}vision-one-enrollment.template.yaml' + + WorkloadSecurityIntegrationStack: + Type: AWS::CloudFormation::Stack + Properties: + Parameters: + CloudOneApiKey: !Ref CloudOneApiKey + CloudOneRegion: !GetAtt GetCloudOneRegionAndAccountStack.Outputs.CloudOneRegion + TemplateURL: !Sub 'https://${QSS3BucketName}.s3.amazonaws.com/${QSS3KeyPrefix}workloadsecurity.template.yaml' + DependsOn: + - VisionOneEnrollmentStack + + CloudOneIntegrationStack: + Type: AWS::CloudFormation::Stack + Properties: + Parameters: + CloudOneRegion: !GetAtt GetCloudOneRegionAndAccountStack.Outputs.CloudOneRegion + CloudOneAccountID: !GetAtt GetCloudOneRegionAndAccountStack.Outputs.CloudOneAccountId + CloudOneApiKey: !Ref CloudOneApiKey + TemplateURL: !Sub 'https://${QSS3BucketName}.s3.amazonaws.com/${QSS3KeyPrefix}cloudone.template.yaml' + DependsOn: + - VisionOneEnrollmentStack + + CloudTrailGetInfoStack: + Type: AWS::CloudFormation::Stack + Properties: + Parameters: + CloudOneRegion: !GetAtt GetCloudOneRegionAndAccountStack.Outputs.CloudOneRegion + CloudOneApiKey: !Ref CloudOneApiKey + TemplateURL: !Sub 'https://${QSS3BucketName}.s3.amazonaws.com/${QSS3KeyPrefix}cloudtrail.template.yaml' + DependsOn: + - CloudOneIntegrationStack + + CloudTrail: + Type: AWS::CloudFormation::Stack + Properties: + Parameters: + ExistingCloudtrailBucketName: !If [HasNoExistingCloudtrailBucketName, "", !Ref ExistingCloudtrailBucketName] + ServiceToken: !GetAtt CloudTrailGetInfoStack.Outputs.ServiceToken + ServiceURL: !GetAtt CloudTrailGetInfoStack.Outputs.ServiceURL + S3BucketName: !GetAtt CloudTrailGetInfoStack.Outputs.S3BucketName + APIVersion: !GetAtt CloudTrailGetInfoStack.Outputs.APIVersion + TemplateURL: !GetAtt CloudTrailGetInfoStack.Outputs.TemplateURL diff --git a/templates/trend-cloudone-onboard/vision-one-enrollment.template.yaml b/templates/trend-cloudone-onboard/vision-one-enrollment.template.yaml new file mode 100644 index 0000000..c5b803d --- /dev/null +++ b/templates/trend-cloudone-onboard/vision-one-enrollment.template.yaml @@ -0,0 +1,121 @@ +AWSTemplateFormatVersion: 2010-09-09 +Description: Trend Cloud One Workload Security Integration. + +Parameters: + CloudOneApiKey: + Description: Cloud One API Key. You can learn more about it at https://cloudone.trendmicro.com/docs/identity-and-account-management/c1-api-key/ + Type: String + NoEcho: true + CloudOneRegion: + Description: Cloud One Region. More info at https://cloudone.trendmicro.com/docs/identity-and-account-management/c1-regions/ + Type: String + Default: us-1 + AllowedValues: + - us-1 + - trend-us-1 + - au-1 + - ie-1 + - sg-1 + - in-1 + - jp-1 + - ca-1 + - de-1 + VisionOneServiceToken: + Description: Vision One Service Token. See step 1 at https://docs.trendmicro.com/en-us/enterprise/trend-micro-xdr-help/ConfiguringCloudOneWorkloadSecurity/ + Type: String + + +Resources: + VisionOneEnrollmentFunction: + Type: AWS::Lambda::Function + Properties: + Runtime: python3.9 + Architectures: + - arm64 + Timeout: 60 + Handler: index.lambda_handler + Role: !GetAtt VisionOneEnrollmentFunctionRole.Arn + Environment: + Variables: + CloudOneRegion: !Ref CloudOneRegion + CloudOneApiKey: !Ref CloudOneApiKey + VisionOneServiceToken: !Ref VisionOneServiceToken + Code: + ZipFile: + !Sub + |- + import json + import os + import urllib3 + import boto3 + import cfnresponse + + def lambda_handler(event, context): + status = cfnresponse.SUCCESS + response_data = {} + physicalResourceId = None + try: + cloudOneRegion = os.environ['CloudOneRegion'] + cloudOneApiKey = os.environ['CloudOneApiKey'] + visionOneServiceToken = os.environ['VisionOneServiceToken'] + + headers = { + 'api-version': 'v1', + 'Authorization': 'ApiKey '+cloudOneApiKey+'', + 'Content-Type': 'application/json' + } + + http = urllib3.PoolManager() + + if event["RequestType"] == "Create" or event["RequestType"] == "Update": + + + url = 'https://visionone-connect.'+cloudOneRegion+'.cloudone.trendmicro.com/api/connectors' + + payload = json.dumps({ + 'enrollmentToken': visionOneServiceToken + }) + + encoded_payload = payload.encode("utf-8") + print(url) + response = http.request("POST", url=url, headers=headers, body=encoded_payload) + print(response) + response_json_data = json.loads(response.data.decode("utf-8")) + print(response_json_data) + physicalResourceId = response_json_data["registration"]["status"] + response_data = {"ID": response_json_data["registration"]["status"]} + + else: # if event["RequestType"] == "Delete": + physicalResourceId = event["PhysicalResourceId"] + + except Exception as e: + print(e) + status = cfnresponse.FAILED + + cfnresponse.send(event, context, status, response_data, physicalResourceId) + + VisionOneEnrollment: + Type: AWS::CloudFormation::CustomResource + Properties: + ServiceToken: !GetAtt VisionOneEnrollmentFunction.Arn + + VisionOneEnrollmentFunctionRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Principal: + Service: + - lambda.amazonaws.com + Action: + - sts:AssumeRole + Path: "/" + ManagedPolicyArns: + - Fn::Join: + - "" + - - "arn:" + - Ref: AWS::Partition + - :iam::aws:policy/service-role/AWSLambdaBasicExecutionRole + diff --git a/templates/trend-cloudone-onboard/workloadsecurity.template.yaml b/templates/trend-cloudone-onboard/workloadsecurity.template.yaml new file mode 100644 index 0000000..132a578 --- /dev/null +++ b/templates/trend-cloudone-onboard/workloadsecurity.template.yaml @@ -0,0 +1,238 @@ +AWSTemplateFormatVersion: 2010-09-09 +Description: Trend Cloud One Workload Security Integration. + +Parameters: + CloudOneApiKey: + Description: Cloud One API Key. You can learn more about it at https://cloudone.trendmicro.com/docs/identity-and-account-management/c1-api-key/ + Type: String + NoEcho: true + CloudOneRegion: + Description: Cloud One Region. More info at https://cloudone.trendmicro.com/docs/identity-and-account-management/c1-regions/ + Type: String + Default: us-1 + AllowedValues: + - us-1 + - trend-us-1 + - au-1 + - ie-1 + - sg-1 + - in-1 + - jp-1 + - ca-1 + - de-1 + + +Resources: +#Role for adding AWS Connector to C1WS + WorkloadSecurityRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Statement: + - Effect: Allow + Principal: + AWS: arn:aws:iam::147995105371:root + Action: + - 'sts:AssumeRole' + Condition: + StringEquals: + sts:ExternalId: !GetAtt GetExternalID.ExternalID + Path: / +#Policy for adding AWS Connector to C1WS + Policies: + - PolicyName: WorkloadSecurityRole + PolicyDocument: + Statement: + - Effect: Allow + Action: + - "ec2:DescribeImages" + - "ec2:DescribeInstances" + - "ec2:DescribeRegions" + - "ec2:DescribeSubnets" + - "ec2:DescribeTags" + - "ec2:DescribeVpcs" + - "ec2:DescribeAvailabilityZones" + - "ec2:DescribeSecurityGroups" + - "workspaces:DescribeWorkspaces" + - "workspaces:DescribeWorkspaceDirectories" + - "workspaces:DescribeWorkspaceBundles" + - "workspaces:DescribeTags" + - "iam:ListAccountAliases" + - "iam:GetRole" + - "iam:GetRolePolicy" + Resource: '*' + +#Lambda Execution Role + AWSConnectorLambdaFunctionRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Principal: + Service: + - lambda.amazonaws.com + Action: + - sts:AssumeRole + Path: "/" + ManagedPolicyArns: + - Fn::Join: + - "" + - - "arn:" + - Ref: AWS::Partition + - :iam::aws:policy/service-role/AWSLambdaBasicExecutionRole + +#Get External ID + GetExternalIDLambda: + Type: AWS::Lambda::Function + Properties: + Runtime: python3.9 + Architectures: + - arm64 + Timeout: 60 + Handler: index.lambda_handler + Role: !GetAtt AWSConnectorLambdaFunctionRole.Arn + Code: + ZipFile: + !Sub + |- + import json + import urllib3 + import boto3 + import cfnresponse + + def lambda_handler(event, context): + status = cfnresponse.SUCCESS + response_data = {} + physicalResourceId = None + try: + + if event["RequestType"] == "Create" or event["RequestType"] == "Update": + cloudOneApiKey = event['ResourceProperties']['CloudOneApiKey'] + cloudOneRegion = event['ResourceProperties']['CloudOneRegion'] + + url = 'https://workload.'+cloudOneRegion+'.cloudone.trendmicro.com/api/awsconnectorsettings' + + headers = { + 'api-version': 'v1', + 'Authorization': 'ApiKey '+cloudOneApiKey+'', + 'Content-Type': 'application/json' + } + + http = urllib3.PoolManager() + print (url) + response = http.request("GET", url=url, headers=headers) + response_json_data = json.loads(response.data.decode("utf-8")) + print(response_json_data["externalId"]) + physicalResourceId = response_json_data["externalId"] + response_data = {"ExternalID": response_json_data["externalId"]} + + else: # if event["RequestType"] == "Delete": + physicalResourceId = event["PhysicalResourceId"] + + except Exception as e: + print(e) + status = cfnresponse.FAILED + + cfnresponse.send(event, context, status, response_data, physicalResourceId) + +#Get External ID Custom Resource + GetExternalID: + Type: AWS::CloudFormation::CustomResource + Properties: + ServiceToken: !GetAtt GetExternalIDLambda.Arn + CloudOneApiKey: !Ref CloudOneApiKey + CloudOneRegion: !Ref CloudOneRegion + + +#Create AWS Connector + AWSConnectorCreateLambda: + Type: AWS::Lambda::Function + Properties: + Runtime: python3.9 + Architectures: + - arm64 + Timeout: 60 + Handler: index.lambda_handler + Role: !GetAtt AWSConnectorLambdaFunctionRole.Arn + Environment: + Variables: + awsaccountid: !Ref AWS::AccountId + externalid: !GetAtt GetExternalID.ExternalID + crossaccountrolearn: !GetAtt WorkloadSecurityRole.Arn + Code: + ZipFile: + !Sub + |- + import json + import urllib3 + import boto3 + import cfnresponse + import os + + def lambda_handler(event, context): + print(event) + + status = cfnresponse.SUCCESS + response_data = {} + physicalResourceId = None + + accountId = os.environ['awsaccountid'] + externalId = os.environ['externalid'] + crossAccountRoleArn = os.environ['crossaccountrolearn'] + cloudOneApiKey = event['ResourceProperties']['CloudOneApiKey'] + cloudOneRegion = event['ResourceProperties']['CloudOneRegion'] + + headers = { + 'api-version': 'v1', + 'Authorization': 'ApiKey '+cloudOneApiKey+'', + 'Content-Type': 'application/json' + } + + http = urllib3.PoolManager() + + try: + if event["RequestType"] == "Create" or event["RequestType"] == "Update": + + url = 'https://workload.'+cloudOneRegion+'.cloudone.trendmicro.com/api/awsconnectors' + + payload = json.dumps({ + "displayName": accountId, + "accountId": accountId, + "crossAccountRoleArn": crossAccountRoleArn + }) + + encoded_payload = payload.encode("utf-8") + response = http.request("POST", url=url, headers=headers, body=encoded_payload) + + response_json_data = json.loads(response.data.decode("utf-8")) + print(response_json_data) + physicalResourceId = str(response_json_data["ID"]) + response_data = {"ID": str(response_json_data["ID"])} + + else: # if event["RequestType"] == "Delete": + ID = event["PhysicalResourceId"] + + url = 'https://workload.'+cloudOneRegion+'.cloudone.trendmicro.com/api/awsconnectors/'+ID + response = http.request("DELETE", url=url, headers=headers) + print(response.data.decode("utf-8")) + + except Exception as e: + print(e) + status = cfnresponse.FAILED + + cfnresponse.send(event, context, status, response_data, physicalResourceId) + +#Create AWS Connector Custom Resource + AWSConnectorCreate: + Type: AWS::CloudFormation::CustomResource + Properties: + ServiceToken: !GetAtt AWSConnectorCreateLambda.Arn + CloudOneApiKey: !Ref CloudOneApiKey + CloudOneRegion: !Ref CloudOneRegion + DependsOn: GetExternalID + +Outputs: + CrossAccountRoleArn: + Value: !GetAtt WorkloadSecurityRole.Arn \ No newline at end of file From 9dae7b30b37d8eb733b3b60db3dcec058f5ddf5f Mon Sep 17 00:00:00 2001 From: Raphael Bottino Date: Tue, 11 Apr 2023 13:57:43 -0400 Subject: [PATCH 02/25] Updating to fix ash findings. --- .../trend-cloudone-onboard/cloudone.template.yaml | 9 ++++++++- .../trend-cloudone-onboard/cloudtrail.template.yaml | 7 +++++++ .../get-cloudone-region-and-account.yaml | 7 +++++++ templates/trend-cloudone-onboard/main.template.yaml | 10 ++++++++++ .../vision-one-enrollment.template.yaml | 7 +++++++ .../workloadsecurity.template.yaml | 10 ++++++++++ 6 files changed, 49 insertions(+), 1 deletion(-) diff --git a/templates/trend-cloudone-onboard/cloudone.template.yaml b/templates/trend-cloudone-onboard/cloudone.template.yaml index e828980..4985775 100644 --- a/templates/trend-cloudone-onboard/cloudone.template.yaml +++ b/templates/trend-cloudone-onboard/cloudone.template.yaml @@ -23,6 +23,10 @@ Parameters: - jp-1 - ca-1 - de-1 + DeadLetterQueue: + Description: Dead Letter Queue for Lambda Function + Type: String + Default: !Ref AWS::NoValue Resources: @@ -44,6 +48,9 @@ Resources: Timeout: 60 Handler: index.lambda_handler Role: !GetAtt AddAWSAccountToCloudOneFunctionRole.Arn + DeadLetterConfig: + TargetArn: !Ref DeadLetterQueue + ReservedConcurrentExecutions: 1 Environment: Variables: CloudOneRoleArn: !GetAtt CloudOneIntegrationStack.Outputs.CloudOneRoleArn @@ -133,4 +140,4 @@ Resources: - - "arn:" - Ref: AWS::Partition - :iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - + \ No newline at end of file diff --git a/templates/trend-cloudone-onboard/cloudtrail.template.yaml b/templates/trend-cloudone-onboard/cloudtrail.template.yaml index ce25cde..0d06451 100644 --- a/templates/trend-cloudone-onboard/cloudtrail.template.yaml +++ b/templates/trend-cloudone-onboard/cloudtrail.template.yaml @@ -19,6 +19,10 @@ Parameters: - in-1 - jp-1 - ca-1 + DeadLetterQueue: + Description: Dead Letter Queue for Lambda Function + Type: String + Default: !Ref AWS::NoValue Resources: GetCloudTrailStackParametersFunction: @@ -30,6 +34,9 @@ Resources: Timeout: 60 Handler: index.lambda_handler Role: !GetAtt GetCloudTrailStackParametersRole.Arn + DeadLetterConfig: + TargetArn: !Ref DeadLetterQueue + ReservedConcurrentExecutions: 1 Environment: Variables: CloudOneRegion: !Ref CloudOneRegion diff --git a/templates/trend-cloudone-onboard/get-cloudone-region-and-account.yaml b/templates/trend-cloudone-onboard/get-cloudone-region-and-account.yaml index 49ab3e3..5750d74 100644 --- a/templates/trend-cloudone-onboard/get-cloudone-region-and-account.yaml +++ b/templates/trend-cloudone-onboard/get-cloudone-region-and-account.yaml @@ -6,6 +6,10 @@ Parameters: Description: Cloud One API Key. You can learn more about it at https://cloudone.trendmicro.com/docs/identity-and-account-management/c1-api-key/ Type: String NoEcho: true + DeadLetterQueue: + Description: Dead Letter Queue for Lambda Function + Type: String + Default: !Ref AWS::NoValue Resources: GetCloudOneRegionAndAccountFunction: @@ -17,6 +21,9 @@ Resources: Timeout: 60 Handler: index.lambda_handler Role: !GetAtt GetCloudOneRegionAndAccountRole.Arn + DeadLetterConfig: + TargetArn: !Ref DeadLetterQueue + ReservedConcurrentExecutions: 1 Environment: Variables: CloudOneApiKey: !Ref CloudOneApiKey diff --git a/templates/trend-cloudone-onboard/main.template.yaml b/templates/trend-cloudone-onboard/main.template.yaml index 79678a2..21fc5ff 100644 --- a/templates/trend-cloudone-onboard/main.template.yaml +++ b/templates/trend-cloudone-onboard/main.template.yaml @@ -73,11 +73,17 @@ Conditions: !Equals ["True", !Ref CreateNewTrail] Resources: + DeadLetterQueue: + Type: AWS::SQS::Queue + Properties: + SqsManagedSseEnabled: true + GetCloudOneRegionAndAccountStack: Type: AWS::CloudFormation::Stack Properties: Parameters: CloudOneApiKey: !Ref CloudOneApiKey + DeadLetterQueue: !Ref DeadLetterQueue TemplateURL: !Sub 'https://${QSS3BucketName}.s3.amazonaws.com/${QSS3KeyPrefix}get-cloudone-region-and-account.yaml' VisionOneEnrollmentStack: @@ -87,6 +93,7 @@ Resources: CloudOneApiKey: !Ref CloudOneApiKey CloudOneRegion: !GetAtt GetCloudOneRegionAndAccountStack.Outputs.CloudOneRegion VisionOneServiceToken: !Ref VisionOneServiceToken + DeadLetterQueue: !Ref DeadLetterQueue TemplateURL: !Sub 'https://${QSS3BucketName}.s3.amazonaws.com/${QSS3KeyPrefix}vision-one-enrollment.template.yaml' WorkloadSecurityIntegrationStack: @@ -95,6 +102,7 @@ Resources: Parameters: CloudOneApiKey: !Ref CloudOneApiKey CloudOneRegion: !GetAtt GetCloudOneRegionAndAccountStack.Outputs.CloudOneRegion + DeadLetterQueue: !Ref DeadLetterQueue TemplateURL: !Sub 'https://${QSS3BucketName}.s3.amazonaws.com/${QSS3KeyPrefix}workloadsecurity.template.yaml' DependsOn: - VisionOneEnrollmentStack @@ -106,6 +114,7 @@ Resources: CloudOneRegion: !GetAtt GetCloudOneRegionAndAccountStack.Outputs.CloudOneRegion CloudOneAccountID: !GetAtt GetCloudOneRegionAndAccountStack.Outputs.CloudOneAccountId CloudOneApiKey: !Ref CloudOneApiKey + DeadLetterQueue: !Ref DeadLetterQueue TemplateURL: !Sub 'https://${QSS3BucketName}.s3.amazonaws.com/${QSS3KeyPrefix}cloudone.template.yaml' DependsOn: - VisionOneEnrollmentStack @@ -116,6 +125,7 @@ Resources: Parameters: CloudOneRegion: !GetAtt GetCloudOneRegionAndAccountStack.Outputs.CloudOneRegion CloudOneApiKey: !Ref CloudOneApiKey + DeadLetterQueue: !Ref DeadLetterQueue TemplateURL: !Sub 'https://${QSS3BucketName}.s3.amazonaws.com/${QSS3KeyPrefix}cloudtrail.template.yaml' DependsOn: - CloudOneIntegrationStack diff --git a/templates/trend-cloudone-onboard/vision-one-enrollment.template.yaml b/templates/trend-cloudone-onboard/vision-one-enrollment.template.yaml index c5b803d..c5a7829 100644 --- a/templates/trend-cloudone-onboard/vision-one-enrollment.template.yaml +++ b/templates/trend-cloudone-onboard/vision-one-enrollment.template.yaml @@ -23,6 +23,10 @@ Parameters: VisionOneServiceToken: Description: Vision One Service Token. See step 1 at https://docs.trendmicro.com/en-us/enterprise/trend-micro-xdr-help/ConfiguringCloudOneWorkloadSecurity/ Type: String + DeadLetterQueue: + Description: Dead Letter Queue for Lambda Function + Type: String + Default: !Ref AWS::NoValue Resources: @@ -35,6 +39,9 @@ Resources: Timeout: 60 Handler: index.lambda_handler Role: !GetAtt VisionOneEnrollmentFunctionRole.Arn + DeadLetterConfig: + TargetArn: !Ref DeadLetterQueue + ReservedConcurrentExecutions: 1 Environment: Variables: CloudOneRegion: !Ref CloudOneRegion diff --git a/templates/trend-cloudone-onboard/workloadsecurity.template.yaml b/templates/trend-cloudone-onboard/workloadsecurity.template.yaml index 132a578..0ce3268 100644 --- a/templates/trend-cloudone-onboard/workloadsecurity.template.yaml +++ b/templates/trend-cloudone-onboard/workloadsecurity.template.yaml @@ -20,6 +20,10 @@ Parameters: - jp-1 - ca-1 - de-1 + DeadLetterQueue: + Description: Dead Letter Queue for Lambda Function + Type: String + Default: !Ref AWS::NoValue Resources: @@ -93,6 +97,9 @@ Resources: Timeout: 60 Handler: index.lambda_handler Role: !GetAtt AWSConnectorLambdaFunctionRole.Arn + DeadLetterConfig: + TargetArn: !Ref DeadLetterQueue + ReservedConcurrentExecutions: 1 Code: ZipFile: !Sub @@ -156,6 +163,9 @@ Resources: Timeout: 60 Handler: index.lambda_handler Role: !GetAtt AWSConnectorLambdaFunctionRole.Arn + DeadLetterConfig: + TargetArn: !Ref DeadLetterQueue + ReservedConcurrentExecutions: 1 Environment: Variables: awsaccountid: !Ref AWS::AccountId From b80c083ef83a72abfa74b781ff829ad67361423f Mon Sep 17 00:00:00 2001 From: Raphael Bottino Date: Tue, 11 Apr 2023 14:12:47 -0400 Subject: [PATCH 03/25] Adding the rule evaluation exceptions. --- .gitignore | 5 ++- .../cloudone.template.yaml | 24 +++++++--- .../cloudtrail.template.yaml | 24 +++++++--- .../get-cloudone-region-and-account.yaml | 24 +++++++--- .../trend-cloudone-onboard/main.template.yaml | 10 ----- .../vision-one-enrollment.template.yaml | 27 +++++++---- .../workloadsecurity.template.yaml | 45 ++++++++++++++----- 7 files changed, 107 insertions(+), 52 deletions(-) diff --git a/.gitignore b/.gitignore index 02e417d..6ca2f95 100644 --- a/.gitignore +++ b/.gitignore @@ -167,4 +167,7 @@ taskcat_outputs/ .taskcat.secrets.yml # General -.DS_Store \ No newline at end of file +.DS_Store + +# Ash +aggregated_results.txt \ No newline at end of file diff --git a/templates/trend-cloudone-onboard/cloudone.template.yaml b/templates/trend-cloudone-onboard/cloudone.template.yaml index 4985775..10af221 100644 --- a/templates/trend-cloudone-onboard/cloudone.template.yaml +++ b/templates/trend-cloudone-onboard/cloudone.template.yaml @@ -23,11 +23,6 @@ Parameters: - jp-1 - ca-1 - de-1 - DeadLetterQueue: - Description: Dead Letter Queue for Lambda Function - Type: String - Default: !Ref AWS::NoValue - Resources: CloudOneIntegrationStack: @@ -40,6 +35,23 @@ Resources: TemplateURL: !Sub 'https://cloud-one-cloud-accounts-${AWS::Region}.s3.${AWS::Region}.amazonaws.com/templates/aws/cloud-account-management-role.template' AddAWSAccountToCloudOneFunction: + Metadata: + cfn_nag: + rules_to_suppress: + - id: W58 + reason: Lambda role provides access to CloudWatch Logs + - id: W89 + reason: Lambda does not need to communicate with VPC resources. + - id: W92 + reason: Lambda does not need reserved concurrent executions. + checkov: + skip: + - id: CKV_AWS_115 + comment: Lambda does not need reserved concurrent executions. + - id: CKV_AWS_116 + comment: DLQ not needed, as Lambda function only triggered by CloudFormation events. + - id: CKV_AWS_117 + comment: Lambda does not need to communicate with VPC resources. Type: AWS::Lambda::Function Properties: Runtime: python3.9 @@ -48,8 +60,6 @@ Resources: Timeout: 60 Handler: index.lambda_handler Role: !GetAtt AddAWSAccountToCloudOneFunctionRole.Arn - DeadLetterConfig: - TargetArn: !Ref DeadLetterQueue ReservedConcurrentExecutions: 1 Environment: Variables: diff --git a/templates/trend-cloudone-onboard/cloudtrail.template.yaml b/templates/trend-cloudone-onboard/cloudtrail.template.yaml index 0d06451..6828d7a 100644 --- a/templates/trend-cloudone-onboard/cloudtrail.template.yaml +++ b/templates/trend-cloudone-onboard/cloudtrail.template.yaml @@ -19,13 +19,26 @@ Parameters: - in-1 - jp-1 - ca-1 - DeadLetterQueue: - Description: Dead Letter Queue for Lambda Function - Type: String - Default: !Ref AWS::NoValue Resources: GetCloudTrailStackParametersFunction: + Metadata: + cfn_nag: + rules_to_suppress: + - id: W58 + reason: Lambda role provides access to CloudWatch Logs + - id: W89 + reason: Lambda does not need to communicate with VPC resources. + - id: W92 + reason: Lambda does not need reserved concurrent executions. + checkov: + skip: + - id: CKV_AWS_115 + comment: Lambda does not need reserved concurrent executions. + - id: CKV_AWS_116 + comment: DLQ not needed, as Lambda function only triggered by CloudFormation events. + - id: CKV_AWS_117 + comment: Lambda does not need to communicate with VPC resources. Type: AWS::Lambda::Function Properties: Runtime: python3.9 @@ -34,9 +47,6 @@ Resources: Timeout: 60 Handler: index.lambda_handler Role: !GetAtt GetCloudTrailStackParametersRole.Arn - DeadLetterConfig: - TargetArn: !Ref DeadLetterQueue - ReservedConcurrentExecutions: 1 Environment: Variables: CloudOneRegion: !Ref CloudOneRegion diff --git a/templates/trend-cloudone-onboard/get-cloudone-region-and-account.yaml b/templates/trend-cloudone-onboard/get-cloudone-region-and-account.yaml index 5750d74..86a05b4 100644 --- a/templates/trend-cloudone-onboard/get-cloudone-region-and-account.yaml +++ b/templates/trend-cloudone-onboard/get-cloudone-region-and-account.yaml @@ -6,13 +6,26 @@ Parameters: Description: Cloud One API Key. You can learn more about it at https://cloudone.trendmicro.com/docs/identity-and-account-management/c1-api-key/ Type: String NoEcho: true - DeadLetterQueue: - Description: Dead Letter Queue for Lambda Function - Type: String - Default: !Ref AWS::NoValue Resources: GetCloudOneRegionAndAccountFunction: + Metadata: + cfn_nag: + rules_to_suppress: + - id: W58 + reason: Lambda role provides access to CloudWatch Logs + - id: W89 + reason: Lambda does not need to communicate with VPC resources. + - id: W92 + reason: Lambda does not need reserved concurrent executions. + checkov: + skip: + - id: CKV_AWS_115 + comment: Lambda does not need reserved concurrent executions. + - id: CKV_AWS_116 + comment: DLQ not needed, as Lambda function only triggered by CloudFormation events. + - id: CKV_AWS_117 + comment: Lambda does not need to communicate with VPC resources. Type: AWS::Lambda::Function Properties: Runtime: python3.9 @@ -21,9 +34,6 @@ Resources: Timeout: 60 Handler: index.lambda_handler Role: !GetAtt GetCloudOneRegionAndAccountRole.Arn - DeadLetterConfig: - TargetArn: !Ref DeadLetterQueue - ReservedConcurrentExecutions: 1 Environment: Variables: CloudOneApiKey: !Ref CloudOneApiKey diff --git a/templates/trend-cloudone-onboard/main.template.yaml b/templates/trend-cloudone-onboard/main.template.yaml index 21fc5ff..79678a2 100644 --- a/templates/trend-cloudone-onboard/main.template.yaml +++ b/templates/trend-cloudone-onboard/main.template.yaml @@ -73,17 +73,11 @@ Conditions: !Equals ["True", !Ref CreateNewTrail] Resources: - DeadLetterQueue: - Type: AWS::SQS::Queue - Properties: - SqsManagedSseEnabled: true - GetCloudOneRegionAndAccountStack: Type: AWS::CloudFormation::Stack Properties: Parameters: CloudOneApiKey: !Ref CloudOneApiKey - DeadLetterQueue: !Ref DeadLetterQueue TemplateURL: !Sub 'https://${QSS3BucketName}.s3.amazonaws.com/${QSS3KeyPrefix}get-cloudone-region-and-account.yaml' VisionOneEnrollmentStack: @@ -93,7 +87,6 @@ Resources: CloudOneApiKey: !Ref CloudOneApiKey CloudOneRegion: !GetAtt GetCloudOneRegionAndAccountStack.Outputs.CloudOneRegion VisionOneServiceToken: !Ref VisionOneServiceToken - DeadLetterQueue: !Ref DeadLetterQueue TemplateURL: !Sub 'https://${QSS3BucketName}.s3.amazonaws.com/${QSS3KeyPrefix}vision-one-enrollment.template.yaml' WorkloadSecurityIntegrationStack: @@ -102,7 +95,6 @@ Resources: Parameters: CloudOneApiKey: !Ref CloudOneApiKey CloudOneRegion: !GetAtt GetCloudOneRegionAndAccountStack.Outputs.CloudOneRegion - DeadLetterQueue: !Ref DeadLetterQueue TemplateURL: !Sub 'https://${QSS3BucketName}.s3.amazonaws.com/${QSS3KeyPrefix}workloadsecurity.template.yaml' DependsOn: - VisionOneEnrollmentStack @@ -114,7 +106,6 @@ Resources: CloudOneRegion: !GetAtt GetCloudOneRegionAndAccountStack.Outputs.CloudOneRegion CloudOneAccountID: !GetAtt GetCloudOneRegionAndAccountStack.Outputs.CloudOneAccountId CloudOneApiKey: !Ref CloudOneApiKey - DeadLetterQueue: !Ref DeadLetterQueue TemplateURL: !Sub 'https://${QSS3BucketName}.s3.amazonaws.com/${QSS3KeyPrefix}cloudone.template.yaml' DependsOn: - VisionOneEnrollmentStack @@ -125,7 +116,6 @@ Resources: Parameters: CloudOneRegion: !GetAtt GetCloudOneRegionAndAccountStack.Outputs.CloudOneRegion CloudOneApiKey: !Ref CloudOneApiKey - DeadLetterQueue: !Ref DeadLetterQueue TemplateURL: !Sub 'https://${QSS3BucketName}.s3.amazonaws.com/${QSS3KeyPrefix}cloudtrail.template.yaml' DependsOn: - CloudOneIntegrationStack diff --git a/templates/trend-cloudone-onboard/vision-one-enrollment.template.yaml b/templates/trend-cloudone-onboard/vision-one-enrollment.template.yaml index c5a7829..f38449c 100644 --- a/templates/trend-cloudone-onboard/vision-one-enrollment.template.yaml +++ b/templates/trend-cloudone-onboard/vision-one-enrollment.template.yaml @@ -22,15 +22,27 @@ Parameters: - de-1 VisionOneServiceToken: Description: Vision One Service Token. See step 1 at https://docs.trendmicro.com/en-us/enterprise/trend-micro-xdr-help/ConfiguringCloudOneWorkloadSecurity/ - Type: String - DeadLetterQueue: - Description: Dead Letter Queue for Lambda Function - Type: String - Default: !Ref AWS::NoValue - + Type: String Resources: VisionOneEnrollmentFunction: + Metadata: + cfn_nag: + rules_to_suppress: + - id: W58 + reason: Lambda role provides access to CloudWatch Logs + - id: W89 + reason: Lambda does not need to communicate with VPC resources. + - id: W92 + reason: Lambda does not need reserved concurrent executions. + checkov: + skip: + - id: CKV_AWS_115 + comment: Lambda does not need reserved concurrent executions. + - id: CKV_AWS_116 + comment: DLQ not needed, as Lambda function only triggered by CloudFormation events. + - id: CKV_AWS_117 + comment: Lambda does not need to communicate with VPC resources. Type: AWS::Lambda::Function Properties: Runtime: python3.9 @@ -39,9 +51,6 @@ Resources: Timeout: 60 Handler: index.lambda_handler Role: !GetAtt VisionOneEnrollmentFunctionRole.Arn - DeadLetterConfig: - TargetArn: !Ref DeadLetterQueue - ReservedConcurrentExecutions: 1 Environment: Variables: CloudOneRegion: !Ref CloudOneRegion diff --git a/templates/trend-cloudone-onboard/workloadsecurity.template.yaml b/templates/trend-cloudone-onboard/workloadsecurity.template.yaml index 0ce3268..6901feb 100644 --- a/templates/trend-cloudone-onboard/workloadsecurity.template.yaml +++ b/templates/trend-cloudone-onboard/workloadsecurity.template.yaml @@ -20,11 +20,6 @@ Parameters: - jp-1 - ca-1 - de-1 - DeadLetterQueue: - Description: Dead Letter Queue for Lambda Function - Type: String - Default: !Ref AWS::NoValue - Resources: #Role for adding AWS Connector to C1WS @@ -89,6 +84,23 @@ Resources: #Get External ID GetExternalIDLambda: + Metadata: + cfn_nag: + rules_to_suppress: + - id: W58 + reason: Lambda role provides access to CloudWatch Logs + - id: W89 + reason: Lambda does not need to communicate with VPC resources. + - id: W92 + reason: Lambda does not need reserved concurrent executions. + checkov: + skip: + - id: CKV_AWS_115 + comment: Lambda does not need reserved concurrent executions. + - id: CKV_AWS_116 + comment: DLQ not needed, as Lambda function only triggered by CloudFormation events. + - id: CKV_AWS_117 + comment: Lambda does not need to communicate with VPC resources. Type: AWS::Lambda::Function Properties: Runtime: python3.9 @@ -97,9 +109,6 @@ Resources: Timeout: 60 Handler: index.lambda_handler Role: !GetAtt AWSConnectorLambdaFunctionRole.Arn - DeadLetterConfig: - TargetArn: !Ref DeadLetterQueue - ReservedConcurrentExecutions: 1 Code: ZipFile: !Sub @@ -155,6 +164,23 @@ Resources: #Create AWS Connector AWSConnectorCreateLambda: + Metadata: + cfn_nag: + rules_to_suppress: + - id: W58 + reason: Lambda role provides access to CloudWatch Logs + - id: W89 + reason: Lambda does not need to communicate with VPC resources. + - id: W92 + reason: Lambda does not need reserved concurrent executions. + checkov: + skip: + - id: CKV_AWS_115 + comment: Lambda does not need reserved concurrent executions. + - id: CKV_AWS_116 + comment: DLQ not needed, as Lambda function only triggered by CloudFormation events. + - id: CKV_AWS_117 + comment: Lambda does not need to communicate with VPC resources. Type: AWS::Lambda::Function Properties: Runtime: python3.9 @@ -163,9 +189,6 @@ Resources: Timeout: 60 Handler: index.lambda_handler Role: !GetAtt AWSConnectorLambdaFunctionRole.Arn - DeadLetterConfig: - TargetArn: !Ref DeadLetterQueue - ReservedConcurrentExecutions: 1 Environment: Variables: awsaccountid: !Ref AWS::AccountId From ff197ba9012b741e083c6880eee79d833dd123d0 Mon Sep 17 00:00:00 2001 From: Raphael Bottino Date: Tue, 11 Apr 2023 14:18:29 -0400 Subject: [PATCH 04/25] Adding the rule CKV_AWS_173 that I missed. --- templates/trend-cloudone-onboard/cloudone.template.yaml | 2 ++ templates/trend-cloudone-onboard/cloudtrail.template.yaml | 2 ++ .../trend-cloudone-onboard/get-cloudone-region-and-account.yaml | 2 ++ .../trend-cloudone-onboard/vision-one-enrollment.template.yaml | 2 ++ templates/trend-cloudone-onboard/workloadsecurity.template.yaml | 2 ++ 5 files changed, 10 insertions(+) diff --git a/templates/trend-cloudone-onboard/cloudone.template.yaml b/templates/trend-cloudone-onboard/cloudone.template.yaml index 10af221..52453c0 100644 --- a/templates/trend-cloudone-onboard/cloudone.template.yaml +++ b/templates/trend-cloudone-onboard/cloudone.template.yaml @@ -52,6 +52,8 @@ Resources: comment: DLQ not needed, as Lambda function only triggered by CloudFormation events. - id: CKV_AWS_117 comment: Lambda does not need to communicate with VPC resources. + - id: CKV_AWS_173 + comment: Environment variables are not sensitive. Type: AWS::Lambda::Function Properties: Runtime: python3.9 diff --git a/templates/trend-cloudone-onboard/cloudtrail.template.yaml b/templates/trend-cloudone-onboard/cloudtrail.template.yaml index 6828d7a..cf3383f 100644 --- a/templates/trend-cloudone-onboard/cloudtrail.template.yaml +++ b/templates/trend-cloudone-onboard/cloudtrail.template.yaml @@ -39,6 +39,8 @@ Resources: comment: DLQ not needed, as Lambda function only triggered by CloudFormation events. - id: CKV_AWS_117 comment: Lambda does not need to communicate with VPC resources. + - id: CKV_AWS_173 + comment: Environment variables are not sensitive. Type: AWS::Lambda::Function Properties: Runtime: python3.9 diff --git a/templates/trend-cloudone-onboard/get-cloudone-region-and-account.yaml b/templates/trend-cloudone-onboard/get-cloudone-region-and-account.yaml index 86a05b4..c06be56 100644 --- a/templates/trend-cloudone-onboard/get-cloudone-region-and-account.yaml +++ b/templates/trend-cloudone-onboard/get-cloudone-region-and-account.yaml @@ -26,6 +26,8 @@ Resources: comment: DLQ not needed, as Lambda function only triggered by CloudFormation events. - id: CKV_AWS_117 comment: Lambda does not need to communicate with VPC resources. + - id: CKV_AWS_173 + comment: Environment variables are not sensitive. Type: AWS::Lambda::Function Properties: Runtime: python3.9 diff --git a/templates/trend-cloudone-onboard/vision-one-enrollment.template.yaml b/templates/trend-cloudone-onboard/vision-one-enrollment.template.yaml index f38449c..e3defb1 100644 --- a/templates/trend-cloudone-onboard/vision-one-enrollment.template.yaml +++ b/templates/trend-cloudone-onboard/vision-one-enrollment.template.yaml @@ -43,6 +43,8 @@ Resources: comment: DLQ not needed, as Lambda function only triggered by CloudFormation events. - id: CKV_AWS_117 comment: Lambda does not need to communicate with VPC resources. + - id: CKV_AWS_173 + comment: Environment variables are not sensitive. Type: AWS::Lambda::Function Properties: Runtime: python3.9 diff --git a/templates/trend-cloudone-onboard/workloadsecurity.template.yaml b/templates/trend-cloudone-onboard/workloadsecurity.template.yaml index 6901feb..418f04a 100644 --- a/templates/trend-cloudone-onboard/workloadsecurity.template.yaml +++ b/templates/trend-cloudone-onboard/workloadsecurity.template.yaml @@ -181,6 +181,8 @@ Resources: comment: DLQ not needed, as Lambda function only triggered by CloudFormation events. - id: CKV_AWS_117 comment: Lambda does not need to communicate with VPC resources. + - id: CKV_AWS_173 + comment: Environment variables are not sensitive. Type: AWS::Lambda::Function Properties: Runtime: python3.9 From bf3b2080c0e008f0cf7a7830725cd4a4ae09e589 Mon Sep 17 00:00:00 2001 From: Raphael Bottino Date: Wed, 12 Apr 2023 12:39:50 -0500 Subject: [PATCH 05/25] Moving Lambda Functions code to .py Removing unused submodule --- .gitmodules | 3 - .../source/AWSConnectorCreateLambda/app.py | 64 ++++++ .../AddAWSAccountToCloudOneFunction/app.py | 61 +++++ .../app.py | 50 +++++ .../app.py | 73 ++++++ .../source/GetExternalIDLambda/app.py | 45 ++++ .../source/VisionOneEnrollmentFunction/app.py | 55 +++++ submodules/cfn-abi-aws-cloudtrail | 1 - templates/functions/source/_lambda_source | 3 - .../cloudone.template.yaml | 81 ++----- .../cloudtrail.template.yaml | 92 ++------ .../get-cloudone-region-and-account.yaml | 70 ++---- .../trend-cloudone-onboard/main.template.yaml | 208 +++++++++++++++++- .../vision-one-enrollment.template.yaml | 77 ++----- .../workloadsecurity.template.yaml | 132 +++-------- 15 files changed, 665 insertions(+), 350 deletions(-) create mode 100644 lambda_functions/source/AWSConnectorCreateLambda/app.py create mode 100644 lambda_functions/source/AddAWSAccountToCloudOneFunction/app.py create mode 100644 lambda_functions/source/GetCloudOneRegionAndAccountFunction/app.py create mode 100644 lambda_functions/source/GetCloudTrailStackParametersFunction/app.py create mode 100644 lambda_functions/source/GetExternalIDLambda/app.py create mode 100644 lambda_functions/source/VisionOneEnrollmentFunction/app.py delete mode 160000 submodules/cfn-abi-aws-cloudtrail delete mode 100644 templates/functions/source/_lambda_source diff --git a/.gitmodules b/.gitmodules index 4edf46f..e69de29 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +0,0 @@ -[submodule "submodules/cfn-abi-aws-cloudtrail"] - path = submodules/cfn-abi-aws-cloudtrail - url = https://github.com/aws-ia/cfn-abi-aws-cloudtrail.git diff --git a/lambda_functions/source/AWSConnectorCreateLambda/app.py b/lambda_functions/source/AWSConnectorCreateLambda/app.py new file mode 100644 index 0000000..3e5e40e --- /dev/null +++ b/lambda_functions/source/AWSConnectorCreateLambda/app.py @@ -0,0 +1,64 @@ +"""Custom Resource to add an AWS Account to Trend Cloud One Workload Security. + +Version: 1.0 + +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: MIT-0 +""" +import json +import urllib3 +import cfnresponse +import os + +def lambda_handler(event, context): + print(event) + + status = cfnresponse.SUCCESS + response_data = {} + physicalResourceId = None + + accountId = os.environ['awsaccountid'] + externalId = os.environ['externalid'] + crossAccountRoleArn = os.environ['crossaccountrolearn'] + cloudOneApiKey = event['ResourceProperties']['CloudOneApiKey'] + cloudOneRegion = event['ResourceProperties']['CloudOneRegion'] + + headers = { + 'api-version': 'v1', + 'Authorization': 'ApiKey '+cloudOneApiKey+'', + 'Content-Type': 'application/json' + } + + http = urllib3.PoolManager() + + try: + if event["RequestType"] == "Create" or event["RequestType"] == "Update": + + url = 'https://workload.'+cloudOneRegion+'.cloudone.trendmicro.com/api/awsconnectors' + + payload = json.dumps({ + "displayName": accountId, + "accountId": accountId, + "crossAccountRoleArn": crossAccountRoleArn + }) + + encoded_payload = payload.encode("utf-8") + response = http.request("POST", url=url, headers=headers, body=encoded_payload) + + response_json_data = json.loads(response.data.decode("utf-8")) + print(response_json_data) + physicalResourceId = str(response_json_data["ID"]) + response_data = {"ID": str(response_json_data["ID"])} + + else: # if event["RequestType"] == "Delete": + ID = event["PhysicalResourceId"] + + url = 'https://workload.'+cloudOneRegion+'.cloudone.trendmicro.com/api/awsconnectors/'+ID + response = http.request("DELETE", url=url, headers=headers) + print(response.data.decode("utf-8")) + + except Exception as exception: + print(exception) + status = cfnresponse.FAILED + + cfnresponse.send(event, context, status, response_data, physicalResourceId) \ No newline at end of file diff --git a/lambda_functions/source/AddAWSAccountToCloudOneFunction/app.py b/lambda_functions/source/AddAWSAccountToCloudOneFunction/app.py new file mode 100644 index 0000000..4a1a304 --- /dev/null +++ b/lambda_functions/source/AddAWSAccountToCloudOneFunction/app.py @@ -0,0 +1,61 @@ +"""Custom Resource to add an AWS Account to Trend Cloud One. + +Version: 1.0 + +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: MIT-0 +""" +import json +import os +import urllib3 +import cfnresponse + +def lambda_handler(event, context): + status = cfnresponse.SUCCESS + response_data = {} + physicalResourceId = None + try: + + cloudOneRoleArn = os.environ['CloudOneRoleArn'] + cloudOneRegion = os.environ['CloudOneRegion'] + cloudOneApiKey = os.environ['CloudOneApiKey'] + + headers = { + 'api-version': 'v1', + 'Authorization': 'ApiKey '+cloudOneApiKey+'', + 'Content-Type': 'application/json' + } + + http = urllib3.PoolManager() + + + if event["RequestType"] == "Create" or event["RequestType"] == "Update": + + url = 'https://cloudaccounts.'+cloudOneRegion+'.cloudone.trendmicro.com/api/cloudaccounts/aws' + + payload = json.dumps({ + 'roleARN': cloudOneRoleArn + }) + encoded_payload = payload.encode("utf-8") + print(url) + response = http.request("POST", url=url, headers=headers, body=encoded_payload) + print(response) + response_json_data = json.loads(response.data.decode("utf-8")) + print(response_json_data) + physicalResourceId = response_json_data["id"] + response_data = {"ID": response_json_data["id"]} + + else: # if event["RequestType"] == "Delete": + id = event["PhysicalResourceId"] + + url = 'https://cloudaccounts.'+cloudOneRegion+'.cloudone.trendmicro.com/api/cloudaccounts/aws/' + id + + print(url) + response = http.request("DELETE", url=url, headers=headers) + print(response) + + except Exception as exception: + print(exception) + status = cfnresponse.FAILED + + cfnresponse.send(event, context, status, response_data, physicalResourceId) \ No newline at end of file diff --git a/lambda_functions/source/GetCloudOneRegionAndAccountFunction/app.py b/lambda_functions/source/GetCloudOneRegionAndAccountFunction/app.py new file mode 100644 index 0000000..0cadaa1 --- /dev/null +++ b/lambda_functions/source/GetCloudOneRegionAndAccountFunction/app.py @@ -0,0 +1,50 @@ +"""Custom Resource to get the Trend Cloud One region and account id. + +Version: 1.0 + +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: MIT-0 +""" +import json +import os +import urllib3 +import cfnresponse + +def lambda_handler(event, context): + status = cfnresponse.SUCCESS + response_data = {} + physicalResourceId = None + try: + + if event["RequestType"] == "Create" or event["RequestType"] == "Update": + cloudOneApiKey = os.environ['CloudOneApiKey'] + apiKeyId = cloudOneApiKey.split(':')[0] + + url = 'https://accounts.cloudone.trendmicro.com/api/apikeys/' + apiKeyId + + headers = { + 'api-version': 'v1', + 'Authorization': 'ApiKey '+cloudOneApiKey+'', + 'Content-Type': 'application/json' + } + + http = urllib3.PoolManager() + print(url) + response = http.request("GET", url=url, headers=headers) + print(response) + response_json_data = json.loads(response.data.decode("utf-8")) + print(response_json_data) + urn = response_json_data["urn"] + region = urn.split(":")[3] + accountId = urn.split(":")[4] + physicalResourceId = response_json_data["urn"] + response_data = {"AccountId": accountId, "Region": region} + + else: # if event["RequestType"] == "Delete": + physicalResourceId = event["PhysicalResourceId"] + + except Exception as e: + print(e) + status = cfnresponse.FAILED + + cfnresponse.send(event, context, status, response_data, physicalResourceId) \ No newline at end of file diff --git a/lambda_functions/source/GetCloudTrailStackParametersFunction/app.py b/lambda_functions/source/GetCloudTrailStackParametersFunction/app.py new file mode 100644 index 0000000..e64ce4c --- /dev/null +++ b/lambda_functions/source/GetCloudTrailStackParametersFunction/app.py @@ -0,0 +1,73 @@ +"""Custom Resource to get the CloudTrail Stack Parameters from Trend Cloud One. + +Version: 1.0 + +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: MIT-0 +""" +import json +import os +import re +import uuid +from urllib.parse import unquote +import urllib3 +import cfnresponse + +def lambda_handler(event, context): + status = cfnresponse.SUCCESS + response_data = {} + physicalResourceId = None + try: + + if event["RequestType"] == "Create" or event["RequestType"] == "Update": + cloudOneRegion = os.environ['CloudOneRegion'] + cloudOneApiKey = os.environ['CloudOneApiKey'] + awsAccountId = os.environ['AwsAccountId'] + awsRegion = os.environ['AwsRegion'] + + url = 'https://cloudtrail.'+cloudOneRegion+'.cloudone.trendmicro.com/api/stacks' + + payload = json.dumps({ + 'providerAccountID': awsAccountId, + 'awsRegion': awsRegion + }) + headers = { + 'api-version': 'v1', + 'Authorization': 'ApiKey '+cloudOneApiKey+'', + 'Content-Type': 'application/json' + } + + http = urllib3.PoolManager() + encoded_payload = payload.encode("utf-8") + print(url) + response = http.request("POST", url=url, headers=headers, body=encoded_payload) + print(response) + response_json_data = json.loads(response.data.decode("utf-8")) + print(response_json_data) + encodedDeploymentUrl = response_json_data["deploymentURL"] + print(encodedDeploymentUrl) + templateUrl = re.search(r"templateUrl=([^&]+)", encodedDeploymentUrl).group(1) + s3BucketKey = re.search(r"param_S3BucketKey=([^&]+)", encodedDeploymentUrl).group(1) + s3BucketName = re.search(r"param_S3BucketName=([^&]+)", encodedDeploymentUrl).group(1) + serviceToken = re.search(r"param_ServiceToken=([^&]+)", encodedDeploymentUrl).group(1) + serviceUrl = re.search(r"param_ServiceURL=([^&]+)", encodedDeploymentUrl).group(1) + apiVersion = re.search(r"param_APIVersion=([^\n]+)", encodedDeploymentUrl).group(1) + + physicalResourceId = str(uuid.uuid4()) + response_data = { + "TemplateUrl": templateUrl, + "S3BucketKey": s3BucketKey, + "S3BucketName": s3BucketName, + "ServiceToken": serviceToken, + "ServiceUrl": unquote(serviceUrl), + "ApiVersion": apiVersion + } + + else: # if event["RequestType"] == "Delete": + physicalResourceId = event["PhysicalResourceId"] + + except Exception as exception: + print(exception) + status = cfnresponse.FAILED + + cfnresponse.send(event, context, status, response_data, physicalResourceId) \ No newline at end of file diff --git a/lambda_functions/source/GetExternalIDLambda/app.py b/lambda_functions/source/GetExternalIDLambda/app.py new file mode 100644 index 0000000..ad8fbf4 --- /dev/null +++ b/lambda_functions/source/GetExternalIDLambda/app.py @@ -0,0 +1,45 @@ +"""Custom Resource to get a Trend Cloud One Workload Security account's External Id. + +Version: 1.0 + +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: MIT-0 +""" +import json +import urllib3 +import cfnresponse + +def lambda_handler(event, context): + status = cfnresponse.SUCCESS + response_data = {} + physicalResourceId = None + try: + + if event["RequestType"] == "Create" or event["RequestType"] == "Update": + cloudOneApiKey = event['ResourceProperties']['CloudOneApiKey'] + cloudOneRegion = event['ResourceProperties']['CloudOneRegion'] + + url = 'https://workload.'+cloudOneRegion+'.cloudone.trendmicro.com/api/awsconnectorsettings' + + headers = { + 'api-version': 'v1', + 'Authorization': 'ApiKey '+cloudOneApiKey+'', + 'Content-Type': 'application/json' + } + + http = urllib3.PoolManager() + print (url) + response = http.request("GET", url=url, headers=headers) + response_json_data = json.loads(response.data.decode("utf-8")) + print(response_json_data["externalId"]) + physicalResourceId = response_json_data["externalId"] + response_data = {"ExternalID": response_json_data["externalId"]} + + else: # if event["RequestType"] == "Delete": + physicalResourceId = event["PhysicalResourceId"] + + except Exception as exception: + print(exception) + status = cfnresponse.FAILED + + cfnresponse.send(event, context, status, response_data, physicalResourceId) \ No newline at end of file diff --git a/lambda_functions/source/VisionOneEnrollmentFunction/app.py b/lambda_functions/source/VisionOneEnrollmentFunction/app.py new file mode 100644 index 0000000..9338450 --- /dev/null +++ b/lambda_functions/source/VisionOneEnrollmentFunction/app.py @@ -0,0 +1,55 @@ +"""Custom Resource to enroll a Trend Cloud One account to a Trend Vision One account. + +Version: 1.0 + +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: MIT-0 +""" +import json +import os +import urllib3 +import cfnresponse + +def lambda_handler(event, context): + status = cfnresponse.SUCCESS + response_data = {} + physicalResourceId = None + try: + cloudOneRegion = os.environ['CloudOneRegion'] + cloudOneApiKey = os.environ['CloudOneApiKey'] + visionOneServiceToken = os.environ['VisionOneServiceToken'] + + headers = { + 'api-version': 'v1', + 'Authorization': 'ApiKey '+cloudOneApiKey+'', + 'Content-Type': 'application/json' + } + + http = urllib3.PoolManager() + + if event["RequestType"] == "Create" or event["RequestType"] == "Update": + + + url = 'https://visionone-connect.'+cloudOneRegion+'.cloudone.trendmicro.com/api/connectors' + + payload = json.dumps({ + 'enrollmentToken': visionOneServiceToken + }) + + encoded_payload = payload.encode("utf-8") + print(url) + response = http.request("POST", url=url, headers=headers, body=encoded_payload) + print(response) + response_json_data = json.loads(response.data.decode("utf-8")) + print(response_json_data) + physicalResourceId = response_json_data["registration"]["status"] + response_data = {"ID": response_json_data["registration"]["status"]} + + else: # if event["RequestType"] == "Delete": + physicalResourceId = event["PhysicalResourceId"] + + except Exception as exception: + print(exception) + status = cfnresponse.FAILED + + cfnresponse.send(event, context, status, response_data, physicalResourceId) \ No newline at end of file diff --git a/submodules/cfn-abi-aws-cloudtrail b/submodules/cfn-abi-aws-cloudtrail deleted file mode 160000 index 765e285..0000000 --- a/submodules/cfn-abi-aws-cloudtrail +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 765e2853d92d4b15fddb212ea5416c6ac648562f diff --git a/templates/functions/source/_lambda_source b/templates/functions/source/_lambda_source deleted file mode 100644 index d36a1e1..0000000 --- a/templates/functions/source/_lambda_source +++ /dev/null @@ -1,3 +0,0 @@ -# -# Lambda source files go here -# diff --git a/templates/trend-cloudone-onboard/cloudone.template.yaml b/templates/trend-cloudone-onboard/cloudone.template.yaml index 52453c0..c3ce534 100644 --- a/templates/trend-cloudone-onboard/cloudone.template.yaml +++ b/templates/trend-cloudone-onboard/cloudone.template.yaml @@ -23,6 +23,25 @@ Parameters: - jp-1 - ca-1 - de-1 + TrendStagingS3Bucket: + AllowedPattern: ^[0-9a-zA-Z]+([0-9a-zA-Z-]*[0-9a-zA-Z])*$ + ConstraintDescription: Deployment bucket name can include numbers, lowercase + letters, uppercase letters, and hyphens (-). It cannot start or end with a hyphen + (-). + Default: aws-abi-pilot + Description: S3 bucket name for the Deployment assets. Deployment bucket name + can include numbers, lowercase letters, uppercase letters, and hyphens (-). + It cannot start or end with a hyphen (-). + Type: String + QSS3KeyPrefix: + AllowedPattern: ^[0-9a-zA-Z-/.]*$ + ConstraintDescription: Deployment key prefix can include numbers, lowercase letters, + uppercase letters, hyphens (-), dots(.) and forward slash (/). + Default: "cfn-abi-aws-trend-cloudone/" + Description: S3 key prefix for the Deployment assets. Deployment key prefix + can include numbers, lowercase letters, uppercase letters, hyphens (-), dots(.) and + forward slash (/). + Type: String Resources: CloudOneIntegrationStack: @@ -60,7 +79,7 @@ Resources: Architectures: - arm64 Timeout: 60 - Handler: index.lambda_handler + Handler: app.lambda_handler Role: !GetAtt AddAWSAccountToCloudOneFunctionRole.Arn ReservedConcurrentExecutions: 1 Environment: @@ -69,64 +88,8 @@ Resources: CloudOneRegion: !Ref CloudOneRegion CloudOneApiKey: !Ref CloudOneApiKey Code: - ZipFile: - !Sub - |- - import json - import os - import urllib3 - import boto3 - import cfnresponse - - def lambda_handler(event, context): - status = cfnresponse.SUCCESS - response_data = {} - physicalResourceId = None - try: - - cloudOneRoleArn = os.environ['CloudOneRoleArn'] - cloudOneRegion = os.environ['CloudOneRegion'] - cloudOneApiKey = os.environ['CloudOneApiKey'] - - headers = { - 'api-version': 'v1', - 'Authorization': 'ApiKey '+cloudOneApiKey+'', - 'Content-Type': 'application/json' - } - - http = urllib3.PoolManager() - - - if event["RequestType"] == "Create" or event["RequestType"] == "Update": - - url = 'https://cloudaccounts.'+cloudOneRegion+'.cloudone.trendmicro.com/api/cloudaccounts/aws' - - payload = json.dumps({ - 'roleARN': cloudOneRoleArn - }) - encoded_payload = payload.encode("utf-8") - print(url) - response = http.request("POST", url=url, headers=headers, body=encoded_payload) - print(response) - response_json_data = json.loads(response.data.decode("utf-8")) - print(response_json_data) - physicalResourceId = response_json_data["id"] - response_data = {"ID": response_json_data["id"]} - - else: # if event["RequestType"] == "Delete": - id = event["PhysicalResourceId"] - - url = 'https://cloudaccounts.'+cloudOneRegion+'.cloudone.trendmicro.com/api/cloudaccounts/aws/' + id - - print(url) - response = http.request("DELETE", url=url, headers=headers) - print(response) - - except Exception as e: - print(e) - status = cfnresponse.FAILED - - cfnresponse.send(event, context, status, response_data, physicalResourceId) + S3Bucket: !Ref 'TrendStagingS3Bucket' + S3Key: !Sub ${QSS3Prefix}/lambda_functions/packages/AddAWSAccountToCloudOneFunction/lambda.zip AddAWSAccountToCloudOne: Type: AWS::CloudFormation::CustomResource diff --git a/templates/trend-cloudone-onboard/cloudtrail.template.yaml b/templates/trend-cloudone-onboard/cloudtrail.template.yaml index cf3383f..3abc257 100644 --- a/templates/trend-cloudone-onboard/cloudtrail.template.yaml +++ b/templates/trend-cloudone-onboard/cloudtrail.template.yaml @@ -19,6 +19,25 @@ Parameters: - in-1 - jp-1 - ca-1 + TrendStagingS3Bucket: + AllowedPattern: ^[0-9a-zA-Z]+([0-9a-zA-Z-]*[0-9a-zA-Z])*$ + ConstraintDescription: Deployment bucket name can include numbers, lowercase + letters, uppercase letters, and hyphens (-). It cannot start or end with a hyphen + (-). + Default: aws-abi-pilot + Description: S3 bucket name for the Deployment assets. Deployment bucket name + can include numbers, lowercase letters, uppercase letters, and hyphens (-). + It cannot start or end with a hyphen (-). + Type: String + QSS3KeyPrefix: + AllowedPattern: ^[0-9a-zA-Z-/.]*$ + ConstraintDescription: Deployment key prefix can include numbers, lowercase letters, + uppercase letters, hyphens (-), dots(.) and forward slash (/). + Default: "cfn-abi-aws-trend-cloudone/" + Description: S3 key prefix for the Deployment assets. Deployment key prefix + can include numbers, lowercase letters, uppercase letters, hyphens (-), dots(.) and + forward slash (/). + Type: String Resources: GetCloudTrailStackParametersFunction: @@ -47,7 +66,7 @@ Resources: Architectures: - arm64 Timeout: 60 - Handler: index.lambda_handler + Handler: app.lambda_handler Role: !GetAtt GetCloudTrailStackParametersRole.Arn Environment: Variables: @@ -56,75 +75,8 @@ Resources: AwsAccountId: !Ref "AWS::AccountId" AwsRegion: !Ref "AWS::Region" Code: - ZipFile: - !Sub - |- - import json - import os - import urllib3 - import cfnresponse - import re - import uuid - from urllib.parse import unquote - - def lambda_handler(event, context): - status = cfnresponse.SUCCESS - response_data = {} - physicalResourceId = None - try: - - if event["RequestType"] == "Create" or event["RequestType"] == "Update": - cloudOneRegion = os.environ['CloudOneRegion'] - cloudOneApiKey = os.environ['CloudOneApiKey'] - awsAccountId = os.environ['AwsAccountId'] - awsRegion = os.environ['AwsRegion'] - - url = 'https://cloudtrail.'+cloudOneRegion+'.cloudone.trendmicro.com/api/stacks' - - payload = json.dumps({ - 'providerAccountID': awsAccountId, - 'awsRegion': awsRegion - }) - headers = { - 'api-version': 'v1', - 'Authorization': 'ApiKey '+cloudOneApiKey+'', - 'Content-Type': 'application/json' - } - - http = urllib3.PoolManager() - encoded_payload = payload.encode("utf-8") - print(url) - response = http.request("POST", url=url, headers=headers, body=encoded_payload) - print(response) - response_json_data = json.loads(response.data.decode("utf-8")) - print(response_json_data) - encodedDeploymentUrl = response_json_data["deploymentURL"] - print(encodedDeploymentUrl) - templateUrl = re.search(r"templateUrl=([^&]+)", encodedDeploymentUrl).group(1) - s3BucketKey = re.search(r"param_S3BucketKey=([^&]+)", encodedDeploymentUrl).group(1) - s3BucketName = re.search(r"param_S3BucketName=([^&]+)", encodedDeploymentUrl).group(1) - serviceToken = re.search(r"param_ServiceToken=([^&]+)", encodedDeploymentUrl).group(1) - serviceUrl = re.search(r"param_ServiceURL=([^&]+)", encodedDeploymentUrl).group(1) - apiVersion = re.search(r"param_APIVersion=([^\n]+)", encodedDeploymentUrl).group(1) - - physicalResourceId = str(uuid.uuid4()) - response_data = { - "TemplateUrl": templateUrl, - "S3BucketKey": s3BucketKey, - "S3BucketName": s3BucketName, - "ServiceToken": serviceToken, - "ServiceUrl": unquote(serviceUrl), - "ApiVersion": apiVersion - } - - else: # if event["RequestType"] == "Delete": - physicalResourceId = event["PhysicalResourceId"] - - except Exception as e: - print(e) - status = cfnresponse.FAILED - - cfnresponse.send(event, context, status, response_data, physicalResourceId) + S3Bucket: !Ref 'TrendStagingS3Bucket' + S3Key: !Sub ${QSS3Prefix}/lambda_functions/packages/GetCloudTrailStackParametersFunction/lambda.zip GetCloudTrailStackParameters: Type: AWS::CloudFormation::CustomResource diff --git a/templates/trend-cloudone-onboard/get-cloudone-region-and-account.yaml b/templates/trend-cloudone-onboard/get-cloudone-region-and-account.yaml index c06be56..e0d98b9 100644 --- a/templates/trend-cloudone-onboard/get-cloudone-region-and-account.yaml +++ b/templates/trend-cloudone-onboard/get-cloudone-region-and-account.yaml @@ -6,6 +6,25 @@ Parameters: Description: Cloud One API Key. You can learn more about it at https://cloudone.trendmicro.com/docs/identity-and-account-management/c1-api-key/ Type: String NoEcho: true + TrendStagingS3Bucket: + AllowedPattern: ^[0-9a-zA-Z]+([0-9a-zA-Z-]*[0-9a-zA-Z])*$ + ConstraintDescription: Deployment bucket name can include numbers, lowercase + letters, uppercase letters, and hyphens (-). It cannot start or end with a hyphen + (-). + Default: aws-abi-pilot + Description: S3 bucket name for the Deployment assets. Deployment bucket name + can include numbers, lowercase letters, uppercase letters, and hyphens (-). + It cannot start or end with a hyphen (-). + Type: String + QSS3KeyPrefix: + AllowedPattern: ^[0-9a-zA-Z-/.]*$ + ConstraintDescription: Deployment key prefix can include numbers, lowercase letters, + uppercase letters, hyphens (-), dots(.) and forward slash (/). + Default: "cfn-abi-aws-trend-cloudone/" + Description: S3 key prefix for the Deployment assets. Deployment key prefix + can include numbers, lowercase letters, uppercase letters, hyphens (-), dots(.) and + forward slash (/). + Type: String Resources: GetCloudOneRegionAndAccountFunction: @@ -34,59 +53,14 @@ Resources: Architectures: - arm64 Timeout: 60 - Handler: index.lambda_handler + Handler: app.lambda_handler Role: !GetAtt GetCloudOneRegionAndAccountRole.Arn Environment: Variables: CloudOneApiKey: !Ref CloudOneApiKey Code: - ZipFile: - !Sub - |- - import json - import os - import urllib3 - import boto3 - import cfnresponse - - def lambda_handler(event, context): - status = cfnresponse.SUCCESS - response_data = {} - physicalResourceId = None - try: - - if event["RequestType"] == "Create" or event["RequestType"] == "Update": - cloudOneApiKey = os.environ['CloudOneApiKey'] - apiKeyId = cloudOneApiKey.split(':')[0] - - url = 'https://accounts.cloudone.trendmicro.com/api/apikeys/' + apiKeyId - - headers = { - 'api-version': 'v1', - 'Authorization': 'ApiKey '+cloudOneApiKey+'', - 'Content-Type': 'application/json' - } - - http = urllib3.PoolManager() - print(url) - response = http.request("GET", url=url, headers=headers) - print(response) - response_json_data = json.loads(response.data.decode("utf-8")) - print(response_json_data) - urn = response_json_data["urn"] - region = urn.split(":")[3] - accountId = urn.split(":")[4] - physicalResourceId = response_json_data["urn"] - response_data = {"AccountId": accountId, "Region": region} - - else: # if event["RequestType"] == "Delete": - physicalResourceId = event["PhysicalResourceId"] - - except Exception as e: - print(e) - status = cfnresponse.FAILED - - cfnresponse.send(event, context, status, response_data, physicalResourceId) + S3Bucket: !Ref 'TrendStagingS3Bucket' + S3Key: !Sub ${QSS3Prefix}/lambda_functions/packages/GetCloudOneRegionAndAccountFunction/lambda.zip GetCloudOneRegionAndAccount: Type: AWS::CloudFormation::CustomResource diff --git a/templates/trend-cloudone-onboard/main.template.yaml b/templates/trend-cloudone-onboard/main.template.yaml index 79678a2..2b93eaa 100644 --- a/templates/trend-cloudone-onboard/main.template.yaml +++ b/templates/trend-cloudone-onboard/main.template.yaml @@ -11,6 +11,8 @@ Metadata: - VisionOneServiceToken - CreateNewTrail - ExistingCloudtrailBucketName + - TrendSolutionTagKey + - TrendSolutionName - Label: default: 'Warning: Do not modify the fields below unless you know what you are doing. Modifications may cause your deployment to fail.' @@ -48,6 +50,14 @@ Parameters: to Trend Micro Cloud One. Type: String Default: EnterAValueHere + TrendSolutionTagKey: + Description: Tag Key to be used for Trend Cloud One resources + Type: String + Default: Solution + TrendSolutionName: + Description: Tag Key to be used for Trend Cloud One resources + Type: String + Default: 'Trend Cloud One' QSS3BucketName: AllowedPattern: ^[0-9a-zA-Z]+([0-9a-zA-Z-]*[0-9a-zA-Z])*$ ConstraintDescription: Deployment bucket name can include numbers, lowercase @@ -74,6 +84,7 @@ Conditions: Resources: GetCloudOneRegionAndAccountStack: + DependsOn: rCopyZips Type: AWS::CloudFormation::Stack Properties: Parameters: @@ -81,44 +92,59 @@ Resources: TemplateURL: !Sub 'https://${QSS3BucketName}.s3.amazonaws.com/${QSS3KeyPrefix}get-cloudone-region-and-account.yaml' VisionOneEnrollmentStack: + DependsOn: rCopyZips Type: AWS::CloudFormation::Stack Properties: Parameters: CloudOneApiKey: !Ref CloudOneApiKey CloudOneRegion: !GetAtt GetCloudOneRegionAndAccountStack.Outputs.CloudOneRegion VisionOneServiceToken: !Ref VisionOneServiceToken + TrendStagingS3Bucket: !Ref TrendStagingS3Bucket + QSS3Prefix: !Ref QSS3KeyPrefix TemplateURL: !Sub 'https://${QSS3BucketName}.s3.amazonaws.com/${QSS3KeyPrefix}vision-one-enrollment.template.yaml' WorkloadSecurityIntegrationStack: + DependsOn: + - VisionOneEnrollmentStack + - rCopyZips Type: AWS::CloudFormation::Stack Properties: Parameters: CloudOneApiKey: !Ref CloudOneApiKey CloudOneRegion: !GetAtt GetCloudOneRegionAndAccountStack.Outputs.CloudOneRegion + TrendStagingS3Bucket: !Ref TrendStagingS3Bucket + QSS3Prefix: !Ref QSS3KeyPrefix TemplateURL: !Sub 'https://${QSS3BucketName}.s3.amazonaws.com/${QSS3KeyPrefix}workloadsecurity.template.yaml' - DependsOn: - - VisionOneEnrollmentStack + CloudOneIntegrationStack: + DependsOn: + - VisionOneEnrollmentStack + - rCopyZips Type: AWS::CloudFormation::Stack Properties: Parameters: CloudOneRegion: !GetAtt GetCloudOneRegionAndAccountStack.Outputs.CloudOneRegion CloudOneAccountID: !GetAtt GetCloudOneRegionAndAccountStack.Outputs.CloudOneAccountId CloudOneApiKey: !Ref CloudOneApiKey + TrendStagingS3Bucket: !Ref TrendStagingS3Bucket + QSS3Prefix: !Ref QSS3KeyPrefix TemplateURL: !Sub 'https://${QSS3BucketName}.s3.amazonaws.com/${QSS3KeyPrefix}cloudone.template.yaml' - DependsOn: - - VisionOneEnrollmentStack + CloudTrailGetInfoStack: + DependsOn: + - CloudOneIntegrationStack + - rCopyZips Type: AWS::CloudFormation::Stack Properties: Parameters: CloudOneRegion: !GetAtt GetCloudOneRegionAndAccountStack.Outputs.CloudOneRegion CloudOneApiKey: !Ref CloudOneApiKey + TrendStagingS3Bucket: !Ref TrendStagingS3Bucket + QSS3Prefix: !Ref QSS3KeyPrefix TemplateURL: !Sub 'https://${QSS3BucketName}.s3.amazonaws.com/${QSS3KeyPrefix}cloudtrail.template.yaml' - DependsOn: - - CloudOneIntegrationStack + CloudTrail: Type: AWS::CloudFormation::Stack @@ -130,3 +156,173 @@ Resources: S3BucketName: !GetAtt CloudTrailGetInfoStack.Outputs.S3BucketName APIVersion: !GetAtt CloudTrailGetInfoStack.Outputs.APIVersion TemplateURL: !GetAtt CloudTrailGetInfoStack.Outputs.TemplateURL + + # CopyZips + TrendStagingS3Bucket: + Type: AWS::S3::Bucket + DeletionPolicy: Retain + UpdateReplacePolicy: Retain + Metadata: + cfn_nag: + rules_to_suppress: + - id: W35 + reason: S3 access logs intentionally not enabled + checkov: + skip: + - id: CKV_AWS_18 + comment: S3 access logs intentionally not enabled + Properties: + BucketEncryption: + ServerSideEncryptionConfiguration: + - ServerSideEncryptionByDefault: + SSEAlgorithm: AES256 + OwnershipControls: + Rules: + - ObjectOwnership: BucketOwnerPreferred + PublicAccessBlockConfiguration: + BlockPublicAcls: True + BlockPublicPolicy: True + IgnorePublicAcls: True + RestrictPublicBuckets: True + Tags: + - Key: !Ref TrendSolutionTagKey + Value: !Ref TrendSolutionName + VersioningConfiguration: + Status: Enabled + + rCopyZips: + Type: Custom::CopyZips + Properties: + ServiceToken: !GetAtt 'rCopyZipsFunction.Arn' + DestBucket: !Ref 'TrendStagingS3Bucket' + SourceBucket: !Ref 'QSS3BucketName' + Prefix: !Ref 'QSS3KeyPrefix' + Objects: + - lambda_functions/packages/sra-cloudtrail-org/lambda.zip + - lambda_functions/packages/sra-cloudtrail-prerequisites/lambda.zip + - templates/sra-cloudtrail-org/sra-cloudtrail-org-bucket.yaml + - templates/sra-cloudtrail-org/sra-cloudtrail-org-kms.yaml + - templates/sra-cloudtrail-org/sra-cloudtrail-org.yaml + - templates/sra-cloudtrail-org/sra-cloudtrail-org-main-ssm.yaml + - templates/sra-cloudtrail-org/sra-cloudtrail-org-main.yaml + + rCopyZipsRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Principal: + Service: lambda.amazonaws.com + Action: sts:AssumeRole + ManagedPolicyArns: + - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole + Path: / + Policies: + - PolicyName: lambda-copier + PolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Action: + - s3:GetObject + - s3:GetObjectTagging + Resource: + - !Sub 'arn:${AWS::Partition}:s3:::${QSS3BucketName}/' + - !Sub 'arn:${AWS::Partition}:s3:::${QSS3BucketName}/${QSS3KeyPrefix}/*' + - Effect: Allow + Action: + - s3:PutObject + - s3:DeleteObject + - s3:PutObjectTagging + Resource: + - !Sub 'arn:${AWS::Partition}:s3:::${TrendStagingS3Bucket}/' + - !Sub 'arn:${AWS::Partition}:s3:::${TrendStagingS3Bucket}/${QSS3KeyPrefix}/*' + - Effect: Allow + Action: + - s3:ListAllMyBuckets + Resource: '*' + + rCopyZipsFunction: + Metadata: + cfn_nag: + rules_to_suppress: + - id: W58 + reason: Lambda role provides access to CloudWatch Logs + - id: W89 + reason: Lambda does not need to communicate with VPC resources. + - id: W92 + reason: Lambda does not need reserved concurrent executions. + checkov: + skip: + - id: CKV_AWS_115 + comment: Lambda does not need reserved concurrent executions. + - id: CKV_AWS_116 + comment: DLQ not needed, as Lambda function only triggered by CloudFormation events. + - id: CKV_AWS_117 + comment: Lambda does not need to communicate with VPC resources. + Type: AWS::Lambda::Function + Properties: + Description: Copies objects from a source S3 bucket to a destination + Handler: index.handler + Runtime: python3.9 + MemorySize: 128 + Role: !GetAtt 'rCopyZipsRole.Arn' + Timeout: 240 + Code: + ZipFile: | + import json + import logging + import threading + import boto3 + import cfnresponse + def copy_objects(source_bucket, dest_bucket, prefix, objects): + s3 = boto3.client('s3') + for o in objects: + key = prefix + o + copy_source = { + 'Bucket': source_bucket, + 'Key': key + } + print(('copy_source: %s' % copy_source)) + print(('dest_bucket = %s'%dest_bucket)) + print(('key = %s' %key)) + s3.copy_object(CopySource=copy_source, Bucket=dest_bucket, + Key=key) + def bucket_exists(): + s3 = boto3.client('s3') + buckets = s3.list_buckets() + for bucket in buckets['Buckets']: + return True + def delete_objects(bucket, prefix, objects): + s3 = boto3.client('s3') + if bucket_exists(): + objects = {'Objects': [{'Key': prefix + o} for o in objects]} + s3.delete_objects(Bucket=bucket, Delete=objects) + def timeout(event, context): + logging.error('Execution is about to time out, sending failure response to CloudFormation') + cfnresponse.send(event, context, cfnresponse.FAILED, {}, None) + def handler(event, context): + # make sure we send a failure to CloudFormation if the function + # is going to timeout + timer = threading.Timer((context.get_remaining_time_in_millis() + / 1000.00) - 0.5, timeout, args=[event, context]) + timer.start() + print(('Received event: %s' % json.dumps(event))) + status = cfnresponse.SUCCESS + try: + source_bucket = event['ResourceProperties']['SourceBucket'] + dest_bucket = event['ResourceProperties']['DestBucket'] + prefix = event['ResourceProperties']['Prefix'] + objects = event['ResourceProperties']['Objects'] + if event['RequestType'] == 'Delete': + delete_objects(dest_bucket, prefix, objects) + else: + copy_objects(source_bucket, dest_bucket, prefix, objects) + except Exception as e: + logging.error('Exception: %s' % e, exc_info=True) + status = cfnresponse.FAILED + finally: + timer.cancel() + cfnresponse.send(event, context, status, {}, None) \ No newline at end of file diff --git a/templates/trend-cloudone-onboard/vision-one-enrollment.template.yaml b/templates/trend-cloudone-onboard/vision-one-enrollment.template.yaml index e3defb1..fe39ce9 100644 --- a/templates/trend-cloudone-onboard/vision-one-enrollment.template.yaml +++ b/templates/trend-cloudone-onboard/vision-one-enrollment.template.yaml @@ -22,7 +22,26 @@ Parameters: - de-1 VisionOneServiceToken: Description: Vision One Service Token. See step 1 at https://docs.trendmicro.com/en-us/enterprise/trend-micro-xdr-help/ConfiguringCloudOneWorkloadSecurity/ - Type: String + Type: String + TrendStagingS3Bucket: + AllowedPattern: ^[0-9a-zA-Z]+([0-9a-zA-Z-]*[0-9a-zA-Z])*$ + ConstraintDescription: Deployment bucket name can include numbers, lowercase + letters, uppercase letters, and hyphens (-). It cannot start or end with a hyphen + (-). + Default: aws-abi-pilot + Description: S3 bucket name for the Deployment assets. Deployment bucket name + can include numbers, lowercase letters, uppercase letters, and hyphens (-). + It cannot start or end with a hyphen (-). + Type: String + QSS3KeyPrefix: + AllowedPattern: ^[0-9a-zA-Z-/.]*$ + ConstraintDescription: Deployment key prefix can include numbers, lowercase letters, + uppercase letters, hyphens (-), dots(.) and forward slash (/). + Default: "cfn-abi-aws-trend-cloudone/" + Description: S3 key prefix for the Deployment assets. Deployment key prefix + can include numbers, lowercase letters, uppercase letters, hyphens (-), dots(.) and + forward slash (/). + Type: String Resources: VisionOneEnrollmentFunction: @@ -51,7 +70,7 @@ Resources: Architectures: - arm64 Timeout: 60 - Handler: index.lambda_handler + Handler: app.lambda_handler Role: !GetAtt VisionOneEnrollmentFunctionRole.Arn Environment: Variables: @@ -59,58 +78,8 @@ Resources: CloudOneApiKey: !Ref CloudOneApiKey VisionOneServiceToken: !Ref VisionOneServiceToken Code: - ZipFile: - !Sub - |- - import json - import os - import urllib3 - import boto3 - import cfnresponse - - def lambda_handler(event, context): - status = cfnresponse.SUCCESS - response_data = {} - physicalResourceId = None - try: - cloudOneRegion = os.environ['CloudOneRegion'] - cloudOneApiKey = os.environ['CloudOneApiKey'] - visionOneServiceToken = os.environ['VisionOneServiceToken'] - - headers = { - 'api-version': 'v1', - 'Authorization': 'ApiKey '+cloudOneApiKey+'', - 'Content-Type': 'application/json' - } - - http = urllib3.PoolManager() - - if event["RequestType"] == "Create" or event["RequestType"] == "Update": - - - url = 'https://visionone-connect.'+cloudOneRegion+'.cloudone.trendmicro.com/api/connectors' - - payload = json.dumps({ - 'enrollmentToken': visionOneServiceToken - }) - - encoded_payload = payload.encode("utf-8") - print(url) - response = http.request("POST", url=url, headers=headers, body=encoded_payload) - print(response) - response_json_data = json.loads(response.data.decode("utf-8")) - print(response_json_data) - physicalResourceId = response_json_data["registration"]["status"] - response_data = {"ID": response_json_data["registration"]["status"]} - - else: # if event["RequestType"] == "Delete": - physicalResourceId = event["PhysicalResourceId"] - - except Exception as e: - print(e) - status = cfnresponse.FAILED - - cfnresponse.send(event, context, status, response_data, physicalResourceId) + S3Bucket: !Ref 'TrendStagingS3Bucket' + S3Key: !Sub ${QSS3Prefix}/lambda_functions/packages/VisionOneEnrollmentFunction/lambda.zip VisionOneEnrollment: Type: AWS::CloudFormation::CustomResource diff --git a/templates/trend-cloudone-onboard/workloadsecurity.template.yaml b/templates/trend-cloudone-onboard/workloadsecurity.template.yaml index 418f04a..6a47ceb 100644 --- a/templates/trend-cloudone-onboard/workloadsecurity.template.yaml +++ b/templates/trend-cloudone-onboard/workloadsecurity.template.yaml @@ -20,7 +20,26 @@ Parameters: - jp-1 - ca-1 - de-1 - + TrendStagingS3Bucket: + AllowedPattern: ^[0-9a-zA-Z]+([0-9a-zA-Z-]*[0-9a-zA-Z])*$ + ConstraintDescription: Deployment bucket name can include numbers, lowercase + letters, uppercase letters, and hyphens (-). It cannot start or end with a hyphen + (-). + Default: aws-abi-pilot + Description: S3 bucket name for the Deployment assets. Deployment bucket name + can include numbers, lowercase letters, uppercase letters, and hyphens (-). + It cannot start or end with a hyphen (-). + Type: String + QSS3KeyPrefix: + AllowedPattern: ^[0-9a-zA-Z-/.]*$ + ConstraintDescription: Deployment key prefix can include numbers, lowercase letters, + uppercase letters, hyphens (-), dots(.) and forward slash (/). + Default: "cfn-abi-aws-trend-cloudone/" + Description: S3 key prefix for the Deployment assets. Deployment key prefix + can include numbers, lowercase letters, uppercase letters, hyphens (-), dots(.) and + forward slash (/). + Type: String + Resources: #Role for adding AWS Connector to C1WS WorkloadSecurityRole: @@ -107,51 +126,11 @@ Resources: Architectures: - arm64 Timeout: 60 - Handler: index.lambda_handler + Handler: app.lambda_handler Role: !GetAtt AWSConnectorLambdaFunctionRole.Arn Code: - ZipFile: - !Sub - |- - import json - import urllib3 - import boto3 - import cfnresponse - - def lambda_handler(event, context): - status = cfnresponse.SUCCESS - response_data = {} - physicalResourceId = None - try: - - if event["RequestType"] == "Create" or event["RequestType"] == "Update": - cloudOneApiKey = event['ResourceProperties']['CloudOneApiKey'] - cloudOneRegion = event['ResourceProperties']['CloudOneRegion'] - - url = 'https://workload.'+cloudOneRegion+'.cloudone.trendmicro.com/api/awsconnectorsettings' - - headers = { - 'api-version': 'v1', - 'Authorization': 'ApiKey '+cloudOneApiKey+'', - 'Content-Type': 'application/json' - } - - http = urllib3.PoolManager() - print (url) - response = http.request("GET", url=url, headers=headers) - response_json_data = json.loads(response.data.decode("utf-8")) - print(response_json_data["externalId"]) - physicalResourceId = response_json_data["externalId"] - response_data = {"ExternalID": response_json_data["externalId"]} - - else: # if event["RequestType"] == "Delete": - physicalResourceId = event["PhysicalResourceId"] - - except Exception as e: - print(e) - status = cfnresponse.FAILED - - cfnresponse.send(event, context, status, response_data, physicalResourceId) + S3Bucket: !Ref 'TrendStagingS3Bucket' + S3Key: !Sub ${QSS3Prefix}/lambda_functions/packages/GetExternalIDLambda/lambda.zip #Get External ID Custom Resource GetExternalID: @@ -189,7 +168,7 @@ Resources: Architectures: - arm64 Timeout: 60 - Handler: index.lambda_handler + Handler: app.lambda_handler Role: !GetAtt AWSConnectorLambdaFunctionRole.Arn Environment: Variables: @@ -197,67 +176,8 @@ Resources: externalid: !GetAtt GetExternalID.ExternalID crossaccountrolearn: !GetAtt WorkloadSecurityRole.Arn Code: - ZipFile: - !Sub - |- - import json - import urllib3 - import boto3 - import cfnresponse - import os - - def lambda_handler(event, context): - print(event) - - status = cfnresponse.SUCCESS - response_data = {} - physicalResourceId = None - - accountId = os.environ['awsaccountid'] - externalId = os.environ['externalid'] - crossAccountRoleArn = os.environ['crossaccountrolearn'] - cloudOneApiKey = event['ResourceProperties']['CloudOneApiKey'] - cloudOneRegion = event['ResourceProperties']['CloudOneRegion'] - - headers = { - 'api-version': 'v1', - 'Authorization': 'ApiKey '+cloudOneApiKey+'', - 'Content-Type': 'application/json' - } - - http = urllib3.PoolManager() - - try: - if event["RequestType"] == "Create" or event["RequestType"] == "Update": - - url = 'https://workload.'+cloudOneRegion+'.cloudone.trendmicro.com/api/awsconnectors' - - payload = json.dumps({ - "displayName": accountId, - "accountId": accountId, - "crossAccountRoleArn": crossAccountRoleArn - }) - - encoded_payload = payload.encode("utf-8") - response = http.request("POST", url=url, headers=headers, body=encoded_payload) - - response_json_data = json.loads(response.data.decode("utf-8")) - print(response_json_data) - physicalResourceId = str(response_json_data["ID"]) - response_data = {"ID": str(response_json_data["ID"])} - - else: # if event["RequestType"] == "Delete": - ID = event["PhysicalResourceId"] - - url = 'https://workload.'+cloudOneRegion+'.cloudone.trendmicro.com/api/awsconnectors/'+ID - response = http.request("DELETE", url=url, headers=headers) - print(response.data.decode("utf-8")) - - except Exception as e: - print(e) - status = cfnresponse.FAILED - - cfnresponse.send(event, context, status, response_data, physicalResourceId) + S3Bucket: !Ref 'TrendStagingS3Bucket' + S3Key: !Sub ${QSS3Prefix}/lambda_functions/packages/GetExternalIDLambda/lambda.zip #Create AWS Connector Custom Resource AWSConnectorCreate: From 17da702fd0154228216fc0e2760a657edd3b24ee Mon Sep 17 00:00:00 2001 From: Raphael Bottino Date: Wed, 12 Apr 2023 13:12:06 -0500 Subject: [PATCH 06/25] Added all zips. Added a script to generate Zips from Source folder. --- .../source/AWSConnectorCreateLambda/lambda.zip | Bin 0 -> 989 bytes .../zip/AWSConnectorCreateLambda/lambda.zip | Bin 0 -> 989 bytes .../AddAWSAccountToCloudOneFunction/lambda.zip | Bin 0 -> 906 bytes .../lambda.zip | Bin 0 -> 857 bytes .../lambda.zip | Bin 0 -> 1127 bytes .../zip/GetExternalIDLambda/lambda.zip | Bin 0 -> 847 bytes .../zip/VisionOneEnrollmentFunction/lambda.zip | Bin 0 -> 910 bytes scripts/sample_userdata.sh | 1 - scripts/zip_lambda_source_code.sh | 14 ++++++++++++++ 9 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 lambda_functions/source/AWSConnectorCreateLambda/lambda.zip create mode 100644 lambda_functions/zip/AWSConnectorCreateLambda/lambda.zip create mode 100644 lambda_functions/zip/AddAWSAccountToCloudOneFunction/lambda.zip create mode 100644 lambda_functions/zip/GetCloudOneRegionAndAccountFunction/lambda.zip create mode 100644 lambda_functions/zip/GetCloudTrailStackParametersFunction/lambda.zip create mode 100644 lambda_functions/zip/GetExternalIDLambda/lambda.zip create mode 100644 lambda_functions/zip/VisionOneEnrollmentFunction/lambda.zip delete mode 100644 scripts/sample_userdata.sh create mode 100755 scripts/zip_lambda_source_code.sh diff --git a/lambda_functions/source/AWSConnectorCreateLambda/lambda.zip b/lambda_functions/source/AWSConnectorCreateLambda/lambda.zip new file mode 100644 index 0000000000000000000000000000000000000000..2507eafcb46eb1584887ee6e1d8d9a0a6f07379d GIT binary patch literal 989 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 ST|nv?8GL~BcVGr$U;qGAG=-%A literal 0 HcmV?d00001 diff --git a/lambda_functions/zip/GetCloudTrailStackParametersFunction/lambda.zip b/lambda_functions/zip/GetCloudTrailStackParametersFunction/lambda.zip new file mode 100644 index 0000000000000000000000000000000000000000..20b0f2c775aa75c042beb3e9a90a87aec6c34b58 GIT binary patch literal 1127 zcmWIWW@Zs#U|`^2DDv(JTPd-1`%z{F26Ju(1~vv6hQxvby@JZn5Kac>>-)`8ZUAv< z1vdjD%U4DQ2C%-RVY&IY%>@3fi+3zIprUMcdBJZ3i4W{&&oLxtX5Tnxpm3VU&FXfH z$|P0I?Rgjf+Xd-Q6S;iw0@nuDkMpYERp_f29=q%$J#n#WM&JsT34$5yY%^^aWU6(Q ztkpPu;namElDaDvSxN**MUcQ!^eUmpI>JIT&(cankO+j)i{v zIsAks@?V^z&cfL`<6=kNcAexa4Qd+ajJA|yyxqfC+AxJ>_0c!$FUop1WJ{b4e$bXH zxx%o@Jih-Pi^SuGr$HLc|E@NfCoh~mu_-M?M2FXFJNMMoH=Q1iBS%FkcHDbIZ& zt#(R-{Kb@}4_NSq&gDOqjB=li0np4~C8US#t>nirr| zn4q~LEpl_g9qt1kVxDff#1++By&=E(-gCK(S5nueO>g(zba7!;Wy&=;5I?V9%>?Mc^J&0tOA=(~NdZ@H?!vA)m0YXY3qWv!0^XQIFo$C5sFTGDcl2KTdHJwA>Zq2od z7k6gfxWA$E?UFkFi<3S2CyVd<;bSKDr_5YW@9eUGunCVZs_Q;-QE9z*D9-q@P}O2H z=9!(zd7De(w;kPii`g&w%qKqks-B-Gem&!PVzl|TPjYL<0GRu???fgYEj#s2bd7c=-;LT|7v{dPDtZ3dXjjqBeS2JT ztJvkH{+J@W`qHvjm;Q0jY%~1z^6KiNJVq9~%_=5`1nZnj*^LZffZ6HpcM!K-mGjORg6IB1Eg75Ks*4>@Cac5 literal 0 HcmV?d00001 diff --git a/lambda_functions/zip/GetExternalIDLambda/lambda.zip b/lambda_functions/zip/GetExternalIDLambda/lambda.zip new file mode 100644 index 0000000000000000000000000000000000000000..cdc4bbff2ad003a2f3cd5269715863ab2c323576 GIT binary patch literal 847 zcmWIWW@Zs#U|`^2xa`vtX0G^g$66)^hCnt31~vv6hQxvby@JZn5Kac>+(Tw5c|crR z!Og(P@|BT+0j#e#BryM$fk55)^%j-^6Qi0;{KR-Wxw>!D#=u*Y4jha%SuS6O})=X$P9JBb0!nx3y2gN%- z_`aOK^O58k1Kq6)WIvdME>*dI;K#fG?|IIXTIYQi-74ti_QA~N`<@6A z4~%AhKWBSvhPqknB^LXhNniI}snpB6f3^Pd(kmHUsg^eShgF}{6wm%R+jM$l->&B9 z)6x#BR36%vD)YZFIx{nh{`lUV5$}6ED&v-$o?P>O z^Q0Z$C*6~Man~yEh|o8~n9Z91v@Tw#30H_dkiejGE#vdym}CFCc4`^5S0dm_1Ib@^y;(Im!EStew{R9(j~UemKe$Xm6xR|)4sm& zy1Zfc)9Ai?-xh?~Gu8jT#_#8Nc+xemZgxM>HK(2iuVFtt^M09CS=FQ~#jQo{m4Q#f zB>Psszw*kw&t@OTy=TYW;xsMiXiSYSIvF0e?k?Mg3r<2MIvXg9(1 z$ClhO9}n0$&Ry#GHBWtK;m2!E<-adnJyX)x`!FW4ZRM#AhrOb{-P@e!J!fw4q3pJ+ z9G*toDhijaogTNi;hX-7-BYd#YfEl?5xOZo^x&NYmi&J=*C?O)xI{2||Dr`R=KP4b zs$Vfb$+2wTuIaC>^0a14Z#d4)muYJ?@6&aw{dPQ)yFX_1-~4a$UGjFazFT6zMwYWC zY`XKW{ACXCW@M6M#+BS9fZ2e70hq8EmNbG`u*A;_N&ILDKfs$6m`OnD85w+l^jl!I HVPF6Na07bG literal 0 HcmV?d00001 diff --git a/lambda_functions/zip/VisionOneEnrollmentFunction/lambda.zip b/lambda_functions/zip/VisionOneEnrollmentFunction/lambda.zip new file mode 100644 index 0000000000000000000000000000000000000000..4b780f761376366881d9a12b1762046f4a4a16dc GIT binary patch literal 910 zcmWIWW@Zs#U|`^2;PmMU(=)d5d&9)QV9UQ$wTkZyAWxonOCUPkV!u&+5HC6Du4RPQGwUlyR-vLav=37gl(` zIa3ypd9;51TsZv|Plr@Oy85$53VX2+zYr|!BJ>TA+X&;8)PQJ)anq#m|=QK495M4#?v@1N}F zob5_pM#%S0)(uFje%qCsVt4jOLbzXpyi$GnQ+D@)IkVS>{Fyq7Yu%=A`nOVJek>20 z_`1cQ`KcXqy31yrjdQHzov#<}544@5lJ1jcyt>LiEl6#9$c6OV59$_Qk@>+Unj8Pn z^3PdEX>o4*MHe|9dNj)Vy*c*PdG7BX%el$M{hd3GrzIADjPABWUi54;k19H#qpg<&wQ>J;( zyOninUE5vZi$*qIqUW#W7uc9`;M(yYS39S8@IOy8{?T?@&**6D%gCZzkE*7w5kD>w zsCw7dW_Pqs;e#78H{%2NAFQZif8n|5?+cyj>n4=8yxWm|Ja+GK*8X$-d22eFIhw-~ z=eJr~*<60LOXi;R+6|kW7xrqo$=RHgu# zOq^Se{X4u$OMG*=#5z5ZicUr27rQEU{_cO}_3WSbEC$t$yai7-e{T9`H}77I|DLoM zf5Y7EuimWV*V1>KeQLqtCI9RLycwC~m~rJ631BW_U;yR|h9!+47A)_uLh=q;z6tPV RWdo^V1VSGmt-}oB0RZv%i9G-S literal 0 HcmV?d00001 diff --git a/scripts/sample_userdata.sh b/scripts/sample_userdata.sh deleted file mode 100644 index a84a81f..0000000 --- a/scripts/sample_userdata.sh +++ /dev/null @@ -1 +0,0 @@ -#UserData and or scripts should be stored here, but only for source code revision purposes cf templatess should refer to prod s3bucket allways diff --git a/scripts/zip_lambda_source_code.sh b/scripts/zip_lambda_source_code.sh new file mode 100755 index 0000000..5c0a1f2 --- /dev/null +++ b/scripts/zip_lambda_source_code.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +# Recursively find all folders in the lambda_functions/source +# folder, zip them, and move them to a new folder under the +# lambda_functions/zip folder. +mkdir -p lambda_functions/zip +# find lambda_functions/source/ -type d -exec sh -c 'echo "$@"; zip -r "$@".zip "$@"; mkdir -p lambda_functions/zip/"$@"; mv lambda.zip lambda_functions/zip/"$@";' -- {} \; +for d in lambda_functions/source/* ; do + echo "$d" + zip -j -r "lambda".zip "$d" + NEW_FOLDER_NAME=$(basename "$d") + mkdir -p "lambda_functions/zip/$NEW_FOLDER_NAME" + mv lambda.zip "lambda_functions/zip/$NEW_FOLDER_NAME" +done \ No newline at end of file From 16d580cf317b28bc885d4dedbdcbecfc57397819 Mon Sep 17 00:00:00 2001 From: Raphael Bottino Date: Wed, 12 Apr 2023 13:23:46 -0500 Subject: [PATCH 07/25] Fixing Typos --- templates/trend-cloudone-onboard/cloudone.template.yaml | 2 +- templates/trend-cloudone-onboard/cloudtrail.template.yaml | 2 +- .../get-cloudone-region-and-account.yaml | 2 +- .../vision-one-enrollment.template.yaml | 2 +- .../trend-cloudone-onboard/workloadsecurity.template.yaml | 4 ++-- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/templates/trend-cloudone-onboard/cloudone.template.yaml b/templates/trend-cloudone-onboard/cloudone.template.yaml index c3ce534..7ea665a 100644 --- a/templates/trend-cloudone-onboard/cloudone.template.yaml +++ b/templates/trend-cloudone-onboard/cloudone.template.yaml @@ -89,7 +89,7 @@ Resources: CloudOneApiKey: !Ref CloudOneApiKey Code: S3Bucket: !Ref 'TrendStagingS3Bucket' - S3Key: !Sub ${QSS3Prefix}/lambda_functions/packages/AddAWSAccountToCloudOneFunction/lambda.zip + S3Key: !Sub ${QSS3KeyPrefix}/lambda_functions/packages/AddAWSAccountToCloudOneFunction/lambda.zip AddAWSAccountToCloudOne: Type: AWS::CloudFormation::CustomResource diff --git a/templates/trend-cloudone-onboard/cloudtrail.template.yaml b/templates/trend-cloudone-onboard/cloudtrail.template.yaml index 3abc257..155f701 100644 --- a/templates/trend-cloudone-onboard/cloudtrail.template.yaml +++ b/templates/trend-cloudone-onboard/cloudtrail.template.yaml @@ -76,7 +76,7 @@ Resources: AwsRegion: !Ref "AWS::Region" Code: S3Bucket: !Ref 'TrendStagingS3Bucket' - S3Key: !Sub ${QSS3Prefix}/lambda_functions/packages/GetCloudTrailStackParametersFunction/lambda.zip + S3Key: !Sub ${QSS3KeyPrefix}/lambda_functions/packages/GetCloudTrailStackParametersFunction/lambda.zip GetCloudTrailStackParameters: Type: AWS::CloudFormation::CustomResource diff --git a/templates/trend-cloudone-onboard/get-cloudone-region-and-account.yaml b/templates/trend-cloudone-onboard/get-cloudone-region-and-account.yaml index e0d98b9..0d1f03f 100644 --- a/templates/trend-cloudone-onboard/get-cloudone-region-and-account.yaml +++ b/templates/trend-cloudone-onboard/get-cloudone-region-and-account.yaml @@ -60,7 +60,7 @@ Resources: CloudOneApiKey: !Ref CloudOneApiKey Code: S3Bucket: !Ref 'TrendStagingS3Bucket' - S3Key: !Sub ${QSS3Prefix}/lambda_functions/packages/GetCloudOneRegionAndAccountFunction/lambda.zip + S3Key: !Sub ${QSS3KeyPrefix}/lambda_functions/packages/GetCloudOneRegionAndAccountFunction/lambda.zip GetCloudOneRegionAndAccount: Type: AWS::CloudFormation::CustomResource diff --git a/templates/trend-cloudone-onboard/vision-one-enrollment.template.yaml b/templates/trend-cloudone-onboard/vision-one-enrollment.template.yaml index fe39ce9..7eb08b9 100644 --- a/templates/trend-cloudone-onboard/vision-one-enrollment.template.yaml +++ b/templates/trend-cloudone-onboard/vision-one-enrollment.template.yaml @@ -79,7 +79,7 @@ Resources: VisionOneServiceToken: !Ref VisionOneServiceToken Code: S3Bucket: !Ref 'TrendStagingS3Bucket' - S3Key: !Sub ${QSS3Prefix}/lambda_functions/packages/VisionOneEnrollmentFunction/lambda.zip + S3Key: !Sub ${QSS3KeyPrefix}/lambda_functions/packages/VisionOneEnrollmentFunction/lambda.zip VisionOneEnrollment: Type: AWS::CloudFormation::CustomResource diff --git a/templates/trend-cloudone-onboard/workloadsecurity.template.yaml b/templates/trend-cloudone-onboard/workloadsecurity.template.yaml index 6a47ceb..34b64c1 100644 --- a/templates/trend-cloudone-onboard/workloadsecurity.template.yaml +++ b/templates/trend-cloudone-onboard/workloadsecurity.template.yaml @@ -130,7 +130,7 @@ Resources: Role: !GetAtt AWSConnectorLambdaFunctionRole.Arn Code: S3Bucket: !Ref 'TrendStagingS3Bucket' - S3Key: !Sub ${QSS3Prefix}/lambda_functions/packages/GetExternalIDLambda/lambda.zip + S3Key: !Sub ${QSS3KeyPrefix}/lambda_functions/packages/GetExternalIDLambda/lambda.zip #Get External ID Custom Resource GetExternalID: @@ -177,7 +177,7 @@ Resources: crossaccountrolearn: !GetAtt WorkloadSecurityRole.Arn Code: S3Bucket: !Ref 'TrendStagingS3Bucket' - S3Key: !Sub ${QSS3Prefix}/lambda_functions/packages/GetExternalIDLambda/lambda.zip + S3Key: !Sub ${QSS3KeyPrefix}/lambda_functions/packages/GetExternalIDLambda/lambda.zip #Create AWS Connector Custom Resource AWSConnectorCreate: From 8c6a8c80aa9bcc4e14c11da9e297d2b921af5cef Mon Sep 17 00:00:00 2001 From: Raphael Bottino Date: Wed, 12 Apr 2023 13:25:53 -0500 Subject: [PATCH 08/25] Removing the zips. --- .../zip/AWSConnectorCreateLambda/lambda.zip | Bin 989 -> 0 bytes .../AddAWSAccountToCloudOneFunction/lambda.zip | Bin 906 -> 0 bytes .../lambda.zip | Bin 857 -> 0 bytes .../lambda.zip | Bin 1127 -> 0 bytes .../zip/GetExternalIDLambda/lambda.zip | Bin 847 -> 0 bytes .../zip/VisionOneEnrollmentFunction/lambda.zip | Bin 910 -> 0 bytes 6 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 lambda_functions/zip/AWSConnectorCreateLambda/lambda.zip delete mode 100644 lambda_functions/zip/AddAWSAccountToCloudOneFunction/lambda.zip delete mode 100644 lambda_functions/zip/GetCloudOneRegionAndAccountFunction/lambda.zip delete mode 100644 lambda_functions/zip/GetCloudTrailStackParametersFunction/lambda.zip delete mode 100644 lambda_functions/zip/GetExternalIDLambda/lambda.zip delete mode 100644 lambda_functions/zip/VisionOneEnrollmentFunction/lambda.zip diff --git a/lambda_functions/zip/AWSConnectorCreateLambda/lambda.zip b/lambda_functions/zip/AWSConnectorCreateLambda/lambda.zip deleted file mode 100644 index 2507eafcb46eb1584887ee6e1d8d9a0a6f07379d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 989 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 ST|nv?8GL~BcVGr$U;qGAG=-%A diff --git a/lambda_functions/zip/GetCloudTrailStackParametersFunction/lambda.zip b/lambda_functions/zip/GetCloudTrailStackParametersFunction/lambda.zip deleted file mode 100644 index 20b0f2c775aa75c042beb3e9a90a87aec6c34b58..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1127 zcmWIWW@Zs#U|`^2DDv(JTPd-1`%z{F26Ju(1~vv6hQxvby@JZn5Kac>>-)`8ZUAv< z1vdjD%U4DQ2C%-RVY&IY%>@3fi+3zIprUMcdBJZ3i4W{&&oLxtX5Tnxpm3VU&FXfH z$|P0I?Rgjf+Xd-Q6S;iw0@nuDkMpYERp_f29=q%$J#n#WM&JsT34$5yY%^^aWU6(Q ztkpPu;namElDaDvSxN**MUcQ!^eUmpI>JIT&(cankO+j)i{v zIsAks@?V^z&cfL`<6=kNcAexa4Qd+ajJA|yyxqfC+AxJ>_0c!$FUop1WJ{b4e$bXH zxx%o@Jih-Pi^SuGr$HLc|E@NfCoh~mu_-M?M2FXFJNMMoH=Q1iBS%FkcHDbIZ& zt#(R-{Kb@}4_NSq&gDOqjB=li0np4~C8US#t>nirr| zn4q~LEpl_g9qt1kVxDff#1++By&=E(-gCK(S5nueO>g(zba7!;Wy&=;5I?V9%>?Mc^J&0tOA=(~NdZ@H?!vA)m0YXY3qWv!0^XQIFo$C5sFTGDcl2KTdHJwA>Zq2od z7k6gfxWA$E?UFkFi<3S2CyVd<;bSKDr_5YW@9eUGunCVZs_Q;-QE9z*D9-q@P}O2H z=9!(zd7De(w;kPii`g&w%qKqks-B-Gem&!PVzl|TPjYL<0GRu???fgYEj#s2bd7c=-;LT|7v{dPDtZ3dXjjqBeS2JT ztJvkH{+J@W`qHvjm;Q0jY%~1z^6KiNJVq9~%_=5`1nZnj*^LZffZ6HpcM!K-mGjORg6IB1Eg75Ks*4>@Cac5 diff --git a/lambda_functions/zip/GetExternalIDLambda/lambda.zip b/lambda_functions/zip/GetExternalIDLambda/lambda.zip deleted file mode 100644 index cdc4bbff2ad003a2f3cd5269715863ab2c323576..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 847 zcmWIWW@Zs#U|`^2xa`vtX0G^g$66)^hCnt31~vv6hQxvby@JZn5Kac>+(Tw5c|crR z!Og(P@|BT+0j#e#BryM$fk55)^%j-^6Qi0;{KR-Wxw>!D#=u*Y4jha%SuS6O})=X$P9JBb0!nx3y2gN%- z_`aOK^O58k1Kq6)WIvdME>*dI;K#fG?|IIXTIYQi-74ti_QA~N`<@6A z4~%AhKWBSvhPqknB^LXhNniI}snpB6f3^Pd(kmHUsg^eShgF}{6wm%R+jM$l->&B9 z)6x#BR36%vD)YZFIx{nh{`lUV5$}6ED&v-$o?P>O z^Q0Z$C*6~Man~yEh|o8~n9Z91v@Tw#30H_dkiejGE#vdym}CFCc4`^5S0dm_1Ib@^y;(Im!EStew{R9(j~UemKe$Xm6xR|)4sm& zy1Zfc)9Ai?-xh?~Gu8jT#_#8Nc+xemZgxM>HK(2iuVFtt^M09CS=FQ~#jQo{m4Q#f zB>Psszw*kw&t@OTy=TYW;xsMiXiSYSIvF0e?k?Mg3r<2MIvXg9(1 z$ClhO9}n0$&Ry#GHBWtK;m2!E<-adnJyX)x`!FW4ZRM#AhrOb{-P@e!J!fw4q3pJ+ z9G*toDhijaogTNi;hX-7-BYd#YfEl?5xOZo^x&NYmi&J=*C?O)xI{2||Dr`R=KP4b zs$Vfb$+2wTuIaC>^0a14Z#d4)muYJ?@6&aw{dPQ)yFX_1-~4a$UGjFazFT6zMwYWC zY`XKW{ACXCW@M6M#+BS9fZ2e70hq8EmNbG`u*A;_N&ILDKfs$6m`OnD85w+l^jl!I HVPF6Na07bG diff --git a/lambda_functions/zip/VisionOneEnrollmentFunction/lambda.zip b/lambda_functions/zip/VisionOneEnrollmentFunction/lambda.zip deleted file mode 100644 index 4b780f761376366881d9a12b1762046f4a4a16dc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 910 zcmWIWW@Zs#U|`^2;PmMU(=)d5d&9)QV9UQ$wTkZyAWxonOCUPkV!u&+5HC6Du4RPQGwUlyR-vLav=37gl(` zIa3ypd9;51TsZv|Plr@Oy85$53VX2+zYr|!BJ>TA+X&;8)PQJ)anq#m|=QK495M4#?v@1N}F zob5_pM#%S0)(uFje%qCsVt4jOLbzXpyi$GnQ+D@)IkVS>{Fyq7Yu%=A`nOVJek>20 z_`1cQ`KcXqy31yrjdQHzov#<}544@5lJ1jcyt>LiEl6#9$c6OV59$_Qk@>+Unj8Pn z^3PdEX>o4*MHe|9dNj)Vy*c*PdG7BX%el$M{hd3GrzIADjPABWUi54;k19H#qpg<&wQ>J;( zyOninUE5vZi$*qIqUW#W7uc9`;M(yYS39S8@IOy8{?T?@&**6D%gCZzkE*7w5kD>w zsCw7dW_Pqs;e#78H{%2NAFQZif8n|5?+cyj>n4=8yxWm|Ja+GK*8X$-d22eFIhw-~ z=eJr~*<60LOXi;R+6|kW7xrqo$=RHgu# zOq^Se{X4u$OMG*=#5z5ZicUr27rQEU{_cO}_3WSbEC$t$yai7-e{T9`H}77I|DLoM zf5Y7EuimWV*V1>KeQLqtCI9RLycwC~m~rJ631BW_U;yR|h9!+47A)_uLh=q;z6tPV RWdo^V1VSGmt-}oB0RZv%i9G-S From 4e2e2a2c411371ea4a18e0714317ea5f5a3121c7 Mon Sep 17 00:00:00 2001 From: Raphael Bottino Date: Thu, 13 Apr 2023 09:33:19 -0500 Subject: [PATCH 09/25] Adding cfnresponse.py to the lambdas. Updating taskcat config to build the zips. Adjust the prefix and change. TemplateURLs as a consequence. --- .gitignore | 2 + .taskcat.yml | 4 +- .../AWSConnectorCreateLambda/cfnresponse.py | 47 ++++++++++++++++++ .../AWSConnectorCreateLambda/lambda.zip | Bin 989 -> 0 bytes .../AWSConnectorCreateLambda/requirements.txt | 2 + .../cfnresponse.py | 47 ++++++++++++++++++ .../requirements.txt | 2 + .../cfnresponse.py | 47 ++++++++++++++++++ .../requirements.txt | 2 + .../cfnresponse.py | 47 ++++++++++++++++++ .../requirements.txt | 2 + .../source/GetExternalIDLambda/cfnresponse.py | 47 ++++++++++++++++++ .../GetExternalIDLambda/requirements.txt | 2 + .../cfnresponse.py | 47 ++++++++++++++++++ .../requirements.txt | 2 + .../trend-cloudone-onboard/main.template.yaml | 42 +++++++++------- 16 files changed, 323 insertions(+), 19 deletions(-) create mode 100644 lambda_functions/source/AWSConnectorCreateLambda/cfnresponse.py delete mode 100644 lambda_functions/source/AWSConnectorCreateLambda/lambda.zip create mode 100644 lambda_functions/source/AWSConnectorCreateLambda/requirements.txt create mode 100644 lambda_functions/source/AddAWSAccountToCloudOneFunction/cfnresponse.py create mode 100644 lambda_functions/source/AddAWSAccountToCloudOneFunction/requirements.txt create mode 100644 lambda_functions/source/GetCloudOneRegionAndAccountFunction/cfnresponse.py create mode 100644 lambda_functions/source/GetCloudOneRegionAndAccountFunction/requirements.txt create mode 100644 lambda_functions/source/GetCloudTrailStackParametersFunction/cfnresponse.py create mode 100644 lambda_functions/source/GetCloudTrailStackParametersFunction/requirements.txt create mode 100644 lambda_functions/source/GetExternalIDLambda/cfnresponse.py create mode 100644 lambda_functions/source/GetExternalIDLambda/requirements.txt create mode 100644 lambda_functions/source/VisionOneEnrollmentFunction/cfnresponse.py create mode 100644 lambda_functions/source/VisionOneEnrollmentFunction/requirements.txt diff --git a/.gitignore b/.gitignore index 6ca2f95..effeb29 100644 --- a/.gitignore +++ b/.gitignore @@ -165,6 +165,8 @@ cython_debug/ taskcat_outputs/ .taskcat/ .taskcat.secrets.yml +# since the zips are automatically generated, we don't want to check them in +lambda_functions/packages/ # General .DS_Store diff --git a/.taskcat.yml b/.taskcat.yml index b7ff2b7..fec6b8b 100644 --- a/.taskcat.yml +++ b/.taskcat.yml @@ -1,7 +1,7 @@ project: name: cfn-abi-trend-cloudone owner: raphael_bottino@trendmicro.com #We need a DL for this. - package_lambda: false + package_lambda: true shorten_stack_name: true regions: - ap-northeast-1 @@ -21,7 +21,7 @@ tests: CloudOneApiKey: $[taskcat_secrets_get_CloudOneApiKey] VisionOneServiceToken: $[taskcat_secrets_get_VisionOneServiceToken] QSS3BucketName: $[taskcat_autobucket] - QSS3KeyPrefix: "cfn-abi-trend-cloudone/templates/trend-cloudone-onboard/" + QSS3KeyPrefix: $[taskcat_project_name] regions: - us-east-1 template: templates/trend-cloudone-onboard/main.template.yaml diff --git a/lambda_functions/source/AWSConnectorCreateLambda/cfnresponse.py b/lambda_functions/source/AWSConnectorCreateLambda/cfnresponse.py new file mode 100644 index 0000000..2039acb --- /dev/null +++ b/lambda_functions/source/AWSConnectorCreateLambda/cfnresponse.py @@ -0,0 +1,47 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: MIT-0 + +from __future__ import print_function +import urllib3 +import json + +SUCCESS = "SUCCESS" +FAILED = "FAILED" + +http = urllib3.PoolManager() + + +def send(event, context, responseStatus, responseData, physicalResourceId=None, noEcho=False, reason=None): + responseUrl = event['ResponseURL'] + + print(responseUrl) + + responseBody = { + 'Status' : responseStatus, + 'Reason' : reason or "See the details in CloudWatch Log Stream: {}".format(context.log_stream_name), + 'PhysicalResourceId' : physicalResourceId or context.log_stream_name, + 'StackId' : event['StackId'], + 'RequestId' : event['RequestId'], + 'LogicalResourceId' : event['LogicalResourceId'], + 'NoEcho' : noEcho, + 'Data' : responseData + } + + json_responseBody = json.dumps(responseBody) + + print("Response body:") + print(json_responseBody) + + headers = { + 'content-type' : '', + 'content-length' : str(len(json_responseBody)) + } + + try: + response = http.request('PUT', responseUrl, headers=headers, body=json_responseBody) + print("Status code:", response.status) + + + except Exception as e: + + print("send(..) failed executing http.request(..):", e) \ No newline at end of file diff --git a/lambda_functions/source/AWSConnectorCreateLambda/lambda.zip b/lambda_functions/source/AWSConnectorCreateLambda/lambda.zip deleted file mode 100644 index 2507eafcb46eb1584887ee6e1d8d9a0a6f07379d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 989 zcmWIWW@Zs#U|`^2Sn1OfRv6`zY0b>Qu#tm-fsH|iA+ex9ub?tCgp+|;_K;bM9uSvS za5FHnd}U-{0PEWtdN==;fk6HF_{2(nMnR|5dwnKWI2=&fQm3%swMvVYxm}lea{4ys z>0y7~Z~n~48*pWMRFg{ie(U+~>&lNwd=JijBIWCFI^<|Wk4sCC3(Kp!{(<^^saidg z&xvq!{fb#8a+k~Dd(hv9lLAayEke(vEPVc#W$8>2!CgH1HtY2#`)pi3)p4(BAl@WrDmMhqD z-taMYEj9RX-b9)I=2Q(epM_b^n*R7pCk6HH%HP|5d3RmC#oT8NUOyZ<6~(^q(%rMx z=xoxV)h+!iuHN-vj9aJXdZ>8i6~zwT;Gi?LpVoMbF8-&~ev8AA^HBeWsY&fUQ?KmO zdFoR1LbEVolJY6uDXs#lTG8PJ(eL99)j6b{{p51yuhzeflQ=aFJN)k6d4K&v{urMJ zCnr5#tjD#bbmH#_#)Z>j^!k5pkGAuSs16P|*FRzZ&M(b-H%*^4HRMg}jG)+g2lB7> zU-@)D>TB%F*39#pSmp?N3gw-CT2mr(^pDIg-DV@Tje=84xQ_>Y%2qltIW=grgTz!< z>*qO<^JM%VHYmp0RN zrQV&7N!j|eedmEodM4}EUq54X;A{HvXIqj_rbRnA>ZIs3BG+S|nB+o{>H^o8wT3eSj z25-)(KYVspsHud`9Z{_i*M-s*HP&+<9uVD_UGI4GXWmbq3yYUTeowgEJfST3c<6E| z+sdUf5$0u!Ppx#~+G_B+P^|d`F+>#Ueo>R#j77C)Q9f7nmK!NbGDhrUY1fl7WEZZ|L_NR xGcw6B Date: Thu, 13 Apr 2023 16:33:01 -0500 Subject: [PATCH 10/25] Improve the character limit issue. Add comments with explanations on using external templates. --- .taskcat.yml | 3 +-- templates/trend-cloudone-onboard/cloudone.template.yaml | 2 ++ templates/trend-cloudone-onboard/main.template.yaml | 5 ++++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/.taskcat.yml b/.taskcat.yml index fec6b8b..d951331 100644 --- a/.taskcat.yml +++ b/.taskcat.yml @@ -15,9 +15,8 @@ project: - us-west-1 - us-west-2 tests: - sample: + t1: parameters: - # Examples: of other taskcat dynamic input parameters for more into see http://taskcat.io CloudOneApiKey: $[taskcat_secrets_get_CloudOneApiKey] VisionOneServiceToken: $[taskcat_secrets_get_VisionOneServiceToken] QSS3BucketName: $[taskcat_autobucket] diff --git a/templates/trend-cloudone-onboard/cloudone.template.yaml b/templates/trend-cloudone-onboard/cloudone.template.yaml index 7ea665a..c00ed80 100644 --- a/templates/trend-cloudone-onboard/cloudone.template.yaml +++ b/templates/trend-cloudone-onboard/cloudone.template.yaml @@ -51,6 +51,8 @@ Resources: CloudOneRegion: !Ref CloudOneRegion CloudOneAccountID: !Ref CloudOneAccountID CloudOneOIDCProviderURL: !Sub 'cloudaccounts.${CloudOneRegion}.cloudone.trendmicro.com' + # This CloudFormation template is not available in the public repository. Instead, it is available in a Product-maintaned S3 bucket. + # Soon we'll implement a mechanism to automatically download the template from the S3 bucket if a change is detected. TemplateURL: !Sub 'https://cloud-one-cloud-accounts-${AWS::Region}.s3.${AWS::Region}.amazonaws.com/templates/aws/cloud-account-management-role.template' AddAWSAccountToCloudOneFunction: diff --git a/templates/trend-cloudone-onboard/main.template.yaml b/templates/trend-cloudone-onboard/main.template.yaml index c599069..27aa7f8 100644 --- a/templates/trend-cloudone-onboard/main.template.yaml +++ b/templates/trend-cloudone-onboard/main.template.yaml @@ -151,10 +151,13 @@ Resources: CloudOneApiKey: !Ref CloudOneApiKey TrendStagingS3Bucket: !Ref TrendStagingS3Bucket QSS3KeyPrefix: !Ref QSS3KeyPrefix + # This CloudFormation template is not available in the public repository. Instead, it is dynamically generated by an API call to Cloud One + # backend in the CloudFormation Stack above (CloudOneIntegrationStack). This can't be local because of it. TemplateURL: !Sub 'https://${QSS3BucketName}.s3.${QSS3BucketRegion}.${AWS::URLSuffix}/${QSS3KeyPrefix}/templates/trend-cloudone-onboard/cloudtrail.template.yaml' - CloudTrail: + # CTrl is short for CloudTrail. This is name was truncated because one of the resources in this stack has a 64 character limit. + CTrl: Type: AWS::CloudFormation::Stack Properties: Parameters: From 4c6103c90bc08f59ce1174e03fc2f9557dc1a79f Mon Sep 17 00:00:00 2001 From: Raphael Bottino Date: Thu, 13 Apr 2023 21:16:21 -0500 Subject: [PATCH 11/25] Adding Custom Resource to grab the organizational trail. --- .../GetOrganizationalTrailBucketName/app.py | 48 +++++++++ .../cfnresponse.py | 47 +++++++++ .../requirements.txt | 2 + .../get-org-trail-bucket.template.yaml | 99 +++++++++++++++++++ .../trend-cloudone-onboard/main.template.yaml | 36 +++---- 5 files changed, 209 insertions(+), 23 deletions(-) create mode 100644 lambda_functions/source/GetOrganizationalTrailBucketName/app.py create mode 100644 lambda_functions/source/GetOrganizationalTrailBucketName/cfnresponse.py create mode 100644 lambda_functions/source/GetOrganizationalTrailBucketName/requirements.txt create mode 100644 templates/trend-cloudone-onboard/get-org-trail-bucket.template.yaml diff --git a/lambda_functions/source/GetOrganizationalTrailBucketName/app.py b/lambda_functions/source/GetOrganizationalTrailBucketName/app.py new file mode 100644 index 0000000..891fcb4 --- /dev/null +++ b/lambda_functions/source/GetOrganizationalTrailBucketName/app.py @@ -0,0 +1,48 @@ +"""Custom Resource to get the Organizational Trail's Bucket Name. + +Version: 1.0 + +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: MIT-0 +""" +import boto3 +import cfnresponse + +def lambda_handler(event, context): + status = cfnresponse.SUCCESS + response_data = {} + physical_resource_id = None + try: + + if event["RequestType"] == "Create" or event["RequestType"] == "Update": + + # List all CloudTrail trails and find the one that has IsOrganizationTrail set to True + cloud_trail_client = boto3.client('cloudtrail') + cloud_trail_trails = cloud_trail_client.describe_trails() + cloud_trail_trails = cloud_trail_trails["trailList"] + trail_name = "" + trail_bucket_name = "" + for trail in cloud_trail_trails: + if trail["IsOrganizationTrail"] is True: + trail_name = trail["Name"] + trail_bucket_name = trail["S3BucketName"] + break + + if not (trail_name or trail_bucket_name): + raise Exception("There is no organizational CloudTrail") + + physical_resource_id = trail_name + response_data = { + "TrailName": trail_name, + "BucketName": trail_bucket_name, + } + print(response_data) + + else: # if event["RequestType"] == "Delete": + physical_resource_id = event["PhysicalResourceId"] + + except Exception as exception: + print(exception) + status = cfnresponse.FAILED + + cfnresponse.send(event, context, status, response_data, physical_resource_id) diff --git a/lambda_functions/source/GetOrganizationalTrailBucketName/cfnresponse.py b/lambda_functions/source/GetOrganizationalTrailBucketName/cfnresponse.py new file mode 100644 index 0000000..2039acb --- /dev/null +++ b/lambda_functions/source/GetOrganizationalTrailBucketName/cfnresponse.py @@ -0,0 +1,47 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: MIT-0 + +from __future__ import print_function +import urllib3 +import json + +SUCCESS = "SUCCESS" +FAILED = "FAILED" + +http = urllib3.PoolManager() + + +def send(event, context, responseStatus, responseData, physicalResourceId=None, noEcho=False, reason=None): + responseUrl = event['ResponseURL'] + + print(responseUrl) + + responseBody = { + 'Status' : responseStatus, + 'Reason' : reason or "See the details in CloudWatch Log Stream: {}".format(context.log_stream_name), + 'PhysicalResourceId' : physicalResourceId or context.log_stream_name, + 'StackId' : event['StackId'], + 'RequestId' : event['RequestId'], + 'LogicalResourceId' : event['LogicalResourceId'], + 'NoEcho' : noEcho, + 'Data' : responseData + } + + json_responseBody = json.dumps(responseBody) + + print("Response body:") + print(json_responseBody) + + headers = { + 'content-type' : '', + 'content-length' : str(len(json_responseBody)) + } + + try: + response = http.request('PUT', responseUrl, headers=headers, body=json_responseBody) + print("Status code:", response.status) + + + except Exception as e: + + print("send(..) failed executing http.request(..):", e) \ No newline at end of file diff --git a/lambda_functions/source/GetOrganizationalTrailBucketName/requirements.txt b/lambda_functions/source/GetOrganizationalTrailBucketName/requirements.txt new file mode 100644 index 0000000..df1db04 --- /dev/null +++ b/lambda_functions/source/GetOrganizationalTrailBucketName/requirements.txt @@ -0,0 +1,2 @@ +# install the latest version +crhelper \ No newline at end of file diff --git a/templates/trend-cloudone-onboard/get-org-trail-bucket.template.yaml b/templates/trend-cloudone-onboard/get-org-trail-bucket.template.yaml new file mode 100644 index 0000000..5aae283 --- /dev/null +++ b/templates/trend-cloudone-onboard/get-org-trail-bucket.template.yaml @@ -0,0 +1,99 @@ +AWSTemplateFormatVersion: 2010-09-09 +Description: Get the AWS Organization CloudTrail Bucket + +Parameters: + TrendStagingS3Bucket: + AllowedPattern: ^[0-9a-zA-Z]+([0-9a-zA-Z-]*[0-9a-zA-Z])*$ + ConstraintDescription: Deployment bucket name can include numbers, lowercase + letters, uppercase letters, and hyphens (-). It cannot start or end with a hyphen + (-). + Default: aws-abi-pilot + Description: S3 bucket name for the Deployment assets. Deployment bucket name + can include numbers, lowercase letters, uppercase letters, and hyphens (-). + It cannot start or end with a hyphen (-). + Type: String + QSS3KeyPrefix: + AllowedPattern: ^[0-9a-zA-Z-/.]*$ + ConstraintDescription: Deployment key prefix can include numbers, lowercase letters, + uppercase letters, hyphens (-), dots(.) and forward slash (/). + Default: "cfn-abi-aws-trend-cloudone/" + Description: S3 key prefix for the Deployment assets. Deployment key prefix + can include numbers, lowercase letters, uppercase letters, hyphens (-), dots(.) and + forward slash (/). + Type: String + +Resources: + GetOrganizationalTrailBucketNameFunction: + Metadata: + cfn_nag: + rules_to_suppress: + - id: W58 + reason: Lambda role provides access to CloudWatch Logs + - id: W89 + reason: Lambda does not need to communicate with VPC resources. + - id: W92 + reason: Lambda does not need reserved concurrent executions. + checkov: + skip: + - id: CKV_AWS_115 + comment: Lambda does not need reserved concurrent executions. + - id: CKV_AWS_116 + comment: DLQ not needed, as Lambda function only triggered by CloudFormation events. + - id: CKV_AWS_117 + comment: Lambda does not need to communicate with VPC resources. + - id: CKV_AWS_173 + comment: Environment variables are not sensitive. + Type: AWS::Lambda::Function + Properties: + Runtime: python3.9 + Architectures: + - arm64 + Timeout: 60 + Handler: app.lambda_handler + Role: !GetAtt GetOrganizationalTrailBucketNameRole.Arn + Code: + S3Bucket: !Ref 'TrendStagingS3Bucket' + S3Key: !Sub ${QSS3KeyPrefix}/lambda_functions/packages/GetOrganizationalTrailBucketName/lambda.zip + + GetOrganizationalTrailBucketName: + Type: Custom::GetOrganizationalTrailBucketName + Properties: + ServiceToken: !GetAtt GetOrganizationalTrailBucketNameFunction.Arn + + GetOrganizationalTrailBucketNameRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Principal: + Service: + - lambda.amazonaws.com + Action: + - sts:AssumeRole + Path: "/" + ManagedPolicyArns: + - Fn::Join: + - "" + - - "arn:" + - Ref: AWS::Partition + - :iam::aws:policy/service-role/AWSLambdaBasicExecutionRole + Policies: + - PolicyName: GetOrganizationalTrailBucketNamePolicy + PolicyDocument: + Version: 2012-10-17 + Statement: + - Effect: Allow + Action: + - cloudtrail:DescribeTrails + Resource: '*' + +Outputs: + OrganizationalTrailBucketName: + Description: The name of the S3 bucket where CloudTrail logs are stored + Value: !GetAtt GetOrganizationalTrailBucketName.BucketName + + OrganizationalTrailName: + Description: The name of the CloudTrail trail + Value: !GetAtt GetOrganizationalTrailBucketName.TrailName \ No newline at end of file diff --git a/templates/trend-cloudone-onboard/main.template.yaml b/templates/trend-cloudone-onboard/main.template.yaml index 27aa7f8..b522462 100644 --- a/templates/trend-cloudone-onboard/main.template.yaml +++ b/templates/trend-cloudone-onboard/main.template.yaml @@ -9,8 +9,6 @@ Metadata: Parameters: - CloudOneApiKey - VisionOneServiceToken - - CreateNewTrail - - ExistingCloudtrailBucketName - TrendSolutionTagKey - TrendSolutionName - Label: @@ -35,21 +33,6 @@ Parameters: Description: Vision One Service Token. See step 1 at https://docs.trendmicro.com/en-us/enterprise/trend-micro-xdr-help/ConfiguringCloudOneWorkloadSecurity/ Type: String NoEcho: true - CreateNewTrail: - Description: Decides if a new Trail should be created. Defaults to False, - so you must enter a S3 Bucket name in the ExistingCloudtrailBucketName - parameter. In case you pick True, a new trail and bucket will be created - for you. Setting this to true will incur in extra costs. - Type: String - AllowedValues: - - "True" - - "False" - Default: "False" - ExistingCloudtrailBucketName: - Description: Specify the name of an existing bucket that you want to use for forwarding - to Trend Micro Cloud One. - Type: String - Default: EnterAValueHere TrendSolutionTagKey: Description: Tag Key to be used for Trend Cloud One resources Type: String @@ -84,10 +67,6 @@ Parameters: Description: Region name for the Deployment bucket. Type: String -Conditions: - HasNoExistingCloudtrailBucketName: - !Equals ["True", !Ref CreateNewTrail] - Resources: GetCloudOneRegionAndAccountStack: DependsOn: rCopyZips @@ -161,14 +140,24 @@ Resources: Type: AWS::CloudFormation::Stack Properties: Parameters: - ExistingCloudtrailBucketName: !If [HasNoExistingCloudtrailBucketName, "", !Ref ExistingCloudtrailBucketName] + ExistingCloudtrailBucketName: GetOrgTrailBucketStack.Outputs.BucketName ServiceToken: !GetAtt CloudTrailGetInfoStack.Outputs.ServiceToken ServiceURL: !GetAtt CloudTrailGetInfoStack.Outputs.ServiceURL S3BucketName: !GetAtt CloudTrailGetInfoStack.Outputs.S3BucketName APIVersion: !GetAtt CloudTrailGetInfoStack.Outputs.APIVersion TemplateURL: !GetAtt CloudTrailGetInfoStack.Outputs.TemplateURL - # CopyZips + # Resources for fetching the Organizational Trail's bucket + GetOrgTrailBucketStack: + DependsOn: rCopyZips + Type: AWS::CloudFormation::Stack + Properties: + Parameters: + TrendStagingS3Bucket: !Ref TrendStagingS3Bucket + QSS3KeyPrefix: !Ref QSS3KeyPrefix + TemplateURL: !Sub 'https://${QSS3BucketName}.s3.${QSS3BucketRegion}.${AWS::URLSuffix}/${QSS3KeyPrefix}/templates/trend-cloudone-onboard/get-org-trail-bucket.template.yaml' + + # CopyZips Resources TrendStagingS3Bucket: Type: AWS::S3::Bucket DeletionPolicy: Retain @@ -215,6 +204,7 @@ Resources: - lambda_functions/packages/GetCloudTrailStackParametersFunction/lambda.zip - lambda_functions/packages/GetExternalIDLambda/lambda.zip - lambda_functions/packages/VisionOneEnrollmentFunction/lambda.zip + - lambda_functions/packages/GetOrganizationalTrailBucketName/lambda.zip rCopyZipsRole: From 4068a8febd5d2ed57cbbbc1345ff92756037a86a Mon Sep 17 00:00:00 2001 From: Raphael Bottino Date: Fri, 14 Apr 2023 14:41:55 -0500 Subject: [PATCH 12/25] Adding a ExistingBucketName parameter. --- .../trend-cloudone-onboard/main.template.yaml | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/templates/trend-cloudone-onboard/main.template.yaml b/templates/trend-cloudone-onboard/main.template.yaml index b522462..b7b2dc6 100644 --- a/templates/trend-cloudone-onboard/main.template.yaml +++ b/templates/trend-cloudone-onboard/main.template.yaml @@ -41,6 +41,13 @@ Parameters: Description: Tag Key to be used for Trend Cloud One resources Type: String Default: 'Trend Cloud One' + ExistingCloudtrailBucketName: + Description: Optional. If left empty, the first existing Organizational Trail will be + selected and its bucket will be used as source for this integration. If you specify + a bucket that already has a Organizational Trail, that bucket will be used for this + integration. + Type: String + Default: "" QSS3BucketName: AllowedPattern: ^[0-9a-zA-Z]+([0-9a-zA-Z-]*[0-9a-zA-Z])*$ ConstraintDescription: Deployment bucket name can include numbers, lowercase @@ -67,6 +74,10 @@ Parameters: Description: Region name for the Deployment bucket. Type: String +Conditions: + FindCloudtrailBucketName: + !Equals [!Ref ExistingCloudtrailBucketName, ""] + Resources: GetCloudOneRegionAndAccountStack: DependsOn: rCopyZips @@ -140,7 +151,7 @@ Resources: Type: AWS::CloudFormation::Stack Properties: Parameters: - ExistingCloudtrailBucketName: GetOrgTrailBucketStack.Outputs.BucketName + ExistingCloudtrailBucketName: !If [FindCloudtrailBucketName, !GetAtt GetOrgTrailBucketStack.Outputs.OrganizationalTrailBucketName, !Ref ExistingCloudtrailBucketName] ServiceToken: !GetAtt CloudTrailGetInfoStack.Outputs.ServiceToken ServiceURL: !GetAtt CloudTrailGetInfoStack.Outputs.ServiceURL S3BucketName: !GetAtt CloudTrailGetInfoStack.Outputs.S3BucketName @@ -149,6 +160,7 @@ Resources: # Resources for fetching the Organizational Trail's bucket GetOrgTrailBucketStack: + Condition: FindCloudtrailBucketName DependsOn: rCopyZips Type: AWS::CloudFormation::Stack Properties: From 75e3a29c1dad85a190a96878cf500a3db1e679b3 Mon Sep 17 00:00:00 2001 From: Raphael Bottino Date: Fri, 14 Apr 2023 14:49:20 -0500 Subject: [PATCH 13/25] Adding ExistingCloudtrailBucketName to Parameters group. --- templates/trend-cloudone-onboard/main.template.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/templates/trend-cloudone-onboard/main.template.yaml b/templates/trend-cloudone-onboard/main.template.yaml index b7b2dc6..82ff571 100644 --- a/templates/trend-cloudone-onboard/main.template.yaml +++ b/templates/trend-cloudone-onboard/main.template.yaml @@ -11,6 +11,7 @@ Metadata: - VisionOneServiceToken - TrendSolutionTagKey - TrendSolutionName + - ExistingCloudtrailBucketName - Label: default: 'Warning: Do not modify the fields below unless you know what you are doing. Modifications may cause your deployment to fail.' From 8f106896edf9176ba7cfd5faee6f44b02914d765 Mon Sep 17 00:00:00 2001 From: Raphael Bottino Date: Tue, 18 Apr 2023 10:26:45 -0500 Subject: [PATCH 14/25] Reverting VERSION back to original value. --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 45c7a58..ae39fab 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -v0.0.1 +v0.0.0 From be7da119ca7ceffe0f53de41894c916585679a74 Mon Sep 17 00:00:00 2001 From: Raphael Bottino Date: Tue, 25 Apr 2023 10:52:20 -0500 Subject: [PATCH 15/25] Adding Vision One Enrollment Token generation/deletion. --- .../app.py | 77 ++++++++++++ .../cfnresponse.py | 47 ++++++++ .../requirements.txt | 2 + .../trend-cloudone-onboard/main.template.yaml | 34 ++++-- .../vision-one-generate-enrollment-token.yaml | 112 ++++++++++++++++++ 5 files changed, 265 insertions(+), 7 deletions(-) create mode 100644 lambda_functions/source/VisionOneGenerateEnrollmentTokenFunction/app.py create mode 100644 lambda_functions/source/VisionOneGenerateEnrollmentTokenFunction/cfnresponse.py create mode 100644 lambda_functions/source/VisionOneGenerateEnrollmentTokenFunction/requirements.txt create mode 100644 templates/trend-cloudone-onboard/vision-one-generate-enrollment-token.yaml diff --git a/lambda_functions/source/VisionOneGenerateEnrollmentTokenFunction/app.py b/lambda_functions/source/VisionOneGenerateEnrollmentTokenFunction/app.py new file mode 100644 index 0000000..997c73f --- /dev/null +++ b/lambda_functions/source/VisionOneGenerateEnrollmentTokenFunction/app.py @@ -0,0 +1,77 @@ +"""Custom Resource to generate and delete a Trend Vision One Enrollment Token. + +Version: 1.0 + +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: MIT-0 +""" +import json +import os +import urllib3 +import cfnresponse + +def define_endpoint(region): + if region == "Australia": + return "https://api.au.xdr.trendmicro.com" + elif region == "European Union": + return "https://api.eu.xdr.trendmicro.com" + elif region == "India": + return "https://api.in.xdr.trendmicro.com" + elif region == "Japan": + return "https://api.xdr.trendmicro.co.jp" + elif region == "Singapore": + return "https://api.sg.xdr.trendmicro.com" + elif region == "United States": + return "https://api.xdr.trendmicro.com" + raise Exception("Invalid region") + +def lambda_handler(event, context): + status = cfnresponse.SUCCESS + response_data = {} + physicalResourceId = None + try: + visionOneAuthenticationToken = os.environ['VisionOneAuthenticationToken'] + visionOneRegion = os.environ['VisionOneRegion'] + + headers = { + 'Authorization': 'Bearer ' + visionOneAuthenticationToken, + 'Content-Type': 'application/json', + 'Accept': 'application/json' + } + + http = urllib3.PoolManager() + url = define_endpoint(visionOneRegion) + + if event["RequestType"] == "Create" or event["RequestType"] == "Update": + + payload = json.dumps({ + 'productId': 'scc' + }) + url = url + "/v1.0/preview/uic/instances/enrollment" + encoded_payload = payload.encode("utf-8") + print(url) + response = http.request("POST", url=url, headers=headers, body=encoded_payload) + print(response) + response_json_data = json.loads(response.data.decode("utf-8")) + print(response_json_data) + physicalResourceId = str(response_json_data["connectorId"]) + response_data = { + "token": response_json_data["token"], + "connectorId": response_json_data["connectorId"], + "tokenExpireTime": response_json_data["tokenExpireTime"] + } + + else: # if event["RequestType"] == "Delete": + physicalResourceId = event["PhysicalResourceId"] + connectorId = physicalResourceId + url = url + "/v2.0/xdr/portal/connectors/onpremise/" + connectorId + response = http.request("DELETE", url=url, headers=headers) + print(response) + response_json_data = json.loads(response.data.decode("utf-8")) + print(response_json_data) + + except Exception as e: + print(e) + status = cfnresponse.FAILED + + cfnresponse.send(event, context, status, response_data, physicalResourceId) \ No newline at end of file diff --git a/lambda_functions/source/VisionOneGenerateEnrollmentTokenFunction/cfnresponse.py b/lambda_functions/source/VisionOneGenerateEnrollmentTokenFunction/cfnresponse.py new file mode 100644 index 0000000..2039acb --- /dev/null +++ b/lambda_functions/source/VisionOneGenerateEnrollmentTokenFunction/cfnresponse.py @@ -0,0 +1,47 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: MIT-0 + +from __future__ import print_function +import urllib3 +import json + +SUCCESS = "SUCCESS" +FAILED = "FAILED" + +http = urllib3.PoolManager() + + +def send(event, context, responseStatus, responseData, physicalResourceId=None, noEcho=False, reason=None): + responseUrl = event['ResponseURL'] + + print(responseUrl) + + responseBody = { + 'Status' : responseStatus, + 'Reason' : reason or "See the details in CloudWatch Log Stream: {}".format(context.log_stream_name), + 'PhysicalResourceId' : physicalResourceId or context.log_stream_name, + 'StackId' : event['StackId'], + 'RequestId' : event['RequestId'], + 'LogicalResourceId' : event['LogicalResourceId'], + 'NoEcho' : noEcho, + 'Data' : responseData + } + + json_responseBody = json.dumps(responseBody) + + print("Response body:") + print(json_responseBody) + + headers = { + 'content-type' : '', + 'content-length' : str(len(json_responseBody)) + } + + try: + response = http.request('PUT', responseUrl, headers=headers, body=json_responseBody) + print("Status code:", response.status) + + + except Exception as e: + + print("send(..) failed executing http.request(..):", e) \ No newline at end of file diff --git a/lambda_functions/source/VisionOneGenerateEnrollmentTokenFunction/requirements.txt b/lambda_functions/source/VisionOneGenerateEnrollmentTokenFunction/requirements.txt new file mode 100644 index 0000000..df1db04 --- /dev/null +++ b/lambda_functions/source/VisionOneGenerateEnrollmentTokenFunction/requirements.txt @@ -0,0 +1,2 @@ +# install the latest version +crhelper \ No newline at end of file diff --git a/templates/trend-cloudone-onboard/main.template.yaml b/templates/trend-cloudone-onboard/main.template.yaml index 82ff571..c902ed0 100644 --- a/templates/trend-cloudone-onboard/main.template.yaml +++ b/templates/trend-cloudone-onboard/main.template.yaml @@ -8,7 +8,8 @@ Metadata: default: 'Please adjust the fields below as required for deployment.' Parameters: - CloudOneApiKey - - VisionOneServiceToken + - VisionOneAuthenticationToken + - VisionOneRegion - TrendSolutionTagKey - TrendSolutionName - ExistingCloudtrailBucketName @@ -21,19 +22,27 @@ Metadata: ParameterLabels: CloudOneApiKey: default: CloudOneApiKey - VisionOneServiceToken: - default: VisionOneServiceToken - Parameters: CloudOneApiKey: Description: Cloud One API Key. You can learn more about it at https://cloudone.trendmicro.com/docs/identity-and-account-management/c1-api-key/ Type: String NoEcho: true - VisionOneServiceToken: - Description: Vision One Service Token. See step 1 at https://docs.trendmicro.com/en-us/enterprise/trend-micro-xdr-help/ConfiguringCloudOneWorkloadSecurity/ + VisionOneAuthenticationToken: + Description: Vision One Authentication Token. See https://docs.trendmicro.com/en-us/enterprise/trend-vision-one-olh/administrative-setti/user-accounts/obtaining-api-keys-f_001.aspx Type: String NoEcho: true + VisionOneRegion: + Description: Vision One Region. See https://automation.trendmicro.com/xdr/Guides/Regional-Domains + Type: String + Default: "United States" + AllowedValues: + - "Australia" + - "European Union" + - "India" + - "Japan" + - "Singapore" + - "United States" TrendSolutionTagKey: Description: Tag Key to be used for Trend Cloud One resources Type: String @@ -89,6 +98,17 @@ Resources: TrendStagingS3Bucket: !Ref TrendStagingS3Bucket QSS3KeyPrefix: !Ref QSS3KeyPrefix TemplateURL: !Sub 'https://${QSS3BucketName}.s3.${QSS3BucketRegion}.${AWS::URLSuffix}/${QSS3KeyPrefix}/templates/trend-cloudone-onboard/get-cloudone-region-and-account.yaml' + + GenerateVisionOneServiceTokenStack: + DependsOn: rCopyZips + Type: AWS::CloudFormation::Stack + Properties: + Parameters: + VisionOneAuthenticationToken: !Ref VisionOneAuthenticationToken + VisionOneRegion: !Ref VisionOneRegion + TrendStagingS3Bucket: !Ref TrendStagingS3Bucket + QSS3KeyPrefix: !Ref QSS3KeyPrefix + TemplateURL: !Sub 'https://${QSS3BucketName}.s3.${QSS3BucketRegion}.${AWS::URLSuffix}/${QSS3KeyPrefix}/templates/trend-cloudone-onboard/vision-one-generate-enrollment-token.yaml' VisionOneEnrollmentStack: DependsOn: rCopyZips @@ -97,7 +117,7 @@ Resources: Parameters: CloudOneApiKey: !Ref CloudOneApiKey CloudOneRegion: !GetAtt GetCloudOneRegionAndAccountStack.Outputs.CloudOneRegion - VisionOneServiceToken: !Ref VisionOneServiceToken + VisionOneServiceToken: !GetAtt GenerateVisionOneServiceTokenStack.Outputs.VisionOneEnrollmentToken TrendStagingS3Bucket: !Ref TrendStagingS3Bucket QSS3KeyPrefix: !Ref QSS3KeyPrefix TemplateURL: !Sub 'https://${QSS3BucketName}.s3.${QSS3BucketRegion}.${AWS::URLSuffix}/${QSS3KeyPrefix}/templates/trend-cloudone-onboard/vision-one-enrollment.template.yaml' diff --git a/templates/trend-cloudone-onboard/vision-one-generate-enrollment-token.yaml b/templates/trend-cloudone-onboard/vision-one-generate-enrollment-token.yaml new file mode 100644 index 0000000..8ee3cdb --- /dev/null +++ b/templates/trend-cloudone-onboard/vision-one-generate-enrollment-token.yaml @@ -0,0 +1,112 @@ +AWSTemplateFormatVersion: 2010-09-09 +Description: Trend Vision One Generate and Delete Enrollment Token. + +Parameters: + VisionOneAuthenticationToken: + Description: Vision One Authentication Token. See https://docs.trendmicro.com/en-us/enterprise/trend-vision-one-olh/administrative-setti/user-accounts/obtaining-api-keys-f_001.aspx + Type: String + VisionOneRegion: + Description: Vision One Region. See https://automation.trendmicro.com/xdr/Guides/Regional-Domains + Type: String + Default: "United States" + AllowedValues: + - "Australia" + - "European Union" + - "India" + - "Japan" + - "Singapore" + - "United States" + TrendStagingS3Bucket: + AllowedPattern: ^[0-9a-zA-Z]+([0-9a-zA-Z-]*[0-9a-zA-Z])*$ + ConstraintDescription: Deployment bucket name can include numbers, lowercase + letters, uppercase letters, and hyphens (-). It cannot start or end with a hyphen + (-). + Default: aws-abi-pilot + Description: S3 bucket name for the Deployment assets. Deployment bucket name + can include numbers, lowercase letters, uppercase letters, and hyphens (-). + It cannot start or end with a hyphen (-). + Type: String + QSS3KeyPrefix: + AllowedPattern: ^[0-9a-zA-Z-/.]*$ + ConstraintDescription: Deployment key prefix can include numbers, lowercase letters, + uppercase letters, hyphens (-), dots(.) and forward slash (/). + Default: "cfn-abi-aws-trend-cloudone/" + Description: S3 key prefix for the Deployment assets. Deployment key prefix + can include numbers, lowercase letters, uppercase letters, hyphens (-), dots(.) and + forward slash (/). + Type: String + +Resources: + VisionOneGenerateEnrollmentTokenFunction: + Metadata: + cfn_nag: + rules_to_suppress: + - id: W58 + reason: Lambda role provides access to CloudWatch Logs + - id: W89 + reason: Lambda does not need to communicate with VPC resources. + - id: W92 + reason: Lambda does not need reserved concurrent executions. + checkov: + skip: + - id: CKV_AWS_115 + comment: Lambda does not need reserved concurrent executions. + - id: CKV_AWS_116 + comment: DLQ not needed, as Lambda function only triggered by CloudFormation events. + - id: CKV_AWS_117 + comment: Lambda does not need to communicate with VPC resources. + - id: CKV_AWS_173 + comment: Environment variables are not sensitive. + Type: AWS::Lambda::Function + Properties: + Runtime: python3.9 + Architectures: + - arm64 + Timeout: 60 + Handler: app.lambda_handler + Role: !GetAtt VisionOneGenerateEnrollmentTokenFunctionRole.Arn + Environment: + Variables: + VisionOneAuthenticationToken: !Ref VisionOneAuthenticationToken + VisionOneRegion: !Ref VisionOneRegion + Code: + S3Bucket: !Ref 'TrendStagingS3Bucket' + S3Key: !Sub ${QSS3KeyPrefix}/lambda_functions/packages/VisionOneGenerateEnrollmentTokenFunction/lambda.zip + + VisionOneGenerateEnrollmentToken: + Type: AWS::CloudFormation::CustomResource + Properties: + ServiceToken: !GetAtt VisionOneGenerateEnrollmentTokenFunction.Arn + + VisionOneGenerateEnrollmentTokenFunctionRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Principal: + Service: + - lambda.amazonaws.com + Action: + - sts:AssumeRole + Path: "/" + ManagedPolicyArns: + - !Sub "arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + +Outputs: + VisionOneEnrollmentToken: + Description: Vision One Enrollment Token. + Value: !GetAtt VisionOneGenerateEnrollmentToken.token + Export: + Name: !Sub "${AWS::StackName}-VisionOneEnrollmentToken" + VisionOneEnrollmentConnectorId: + Description: Vision One Enrollment Connector Id. + Value: !GetAtt VisionOneGenerateEnrollmentToken.connectorId + Export: + Name: !Sub "${AWS::StackName}-VisionOneEnrollmentConnectorId" + VisionOneEnrollmentTokenExpireTime: + Description: Vision One Enrollment Token Expire Time. + Value: !GetAtt VisionOneGenerateEnrollmentToken.tokenExpireTime + Export: + Name: !Sub "${AWS::StackName}-VisionOneEnrollmentTokenExpireTime" From 5748c17bb8b3d4646ad8924a72b3b43962887485 Mon Sep 17 00:00:00 2001 From: Raphael Bottino Date: Thu, 27 Apr 2023 09:19:02 -0500 Subject: [PATCH 16/25] Updating .taskcat.yaml --- .taskcat.yml | 4 ++-- submodules/cfn-abi-aws-cloudtrail | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) create mode 160000 submodules/cfn-abi-aws-cloudtrail diff --git a/.taskcat.yml b/.taskcat.yml index d951331..4bf4f0a 100644 --- a/.taskcat.yml +++ b/.taskcat.yml @@ -17,8 +17,8 @@ project: tests: t1: parameters: - CloudOneApiKey: $[taskcat_secrets_get_CloudOneApiKey] - VisionOneServiceToken: $[taskcat_secrets_get_VisionOneServiceToken] + CloudOneApiKey: $[taskcat_ssm_/trend/cloudone_svc_apikey] + VisionOneAuthenticationToken: $[taskcat_ssm_/trend/visionone_authentication_token] QSS3BucketName: $[taskcat_autobucket] QSS3KeyPrefix: $[taskcat_project_name] regions: diff --git a/submodules/cfn-abi-aws-cloudtrail b/submodules/cfn-abi-aws-cloudtrail new file mode 160000 index 0000000..3fcc82b --- /dev/null +++ b/submodules/cfn-abi-aws-cloudtrail @@ -0,0 +1 @@ +Subproject commit 3fcc82b8145b6c70d2a700de57b317c9017b7725 From 22e0b61109bab804beeaa6b2c306a0d19d240cfa Mon Sep 17 00:00:00 2001 From: Raphael Bottino Date: Thu, 27 Apr 2023 13:31:59 -0500 Subject: [PATCH 17/25] Fixing typo --- templates/trend-cloudone-onboard/cloudone.template.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/trend-cloudone-onboard/cloudone.template.yaml b/templates/trend-cloudone-onboard/cloudone.template.yaml index c00ed80..bf00cae 100644 --- a/templates/trend-cloudone-onboard/cloudone.template.yaml +++ b/templates/trend-cloudone-onboard/cloudone.template.yaml @@ -51,7 +51,7 @@ Resources: CloudOneRegion: !Ref CloudOneRegion CloudOneAccountID: !Ref CloudOneAccountID CloudOneOIDCProviderURL: !Sub 'cloudaccounts.${CloudOneRegion}.cloudone.trendmicro.com' - # This CloudFormation template is not available in the public repository. Instead, it is available in a Product-maintaned S3 bucket. + # This CloudFormation template is not available in the public repository. Instead, it is available in a Product-maintained S3 bucket. # Soon we'll implement a mechanism to automatically download the template from the S3 bucket if a change is detected. TemplateURL: !Sub 'https://cloud-one-cloud-accounts-${AWS::Region}.s3.${AWS::Region}.amazonaws.com/templates/aws/cloud-account-management-role.template' From a56b15ed8ee7e22080b956c762117a579e3224f2 Mon Sep 17 00:00:00 2001 From: Raphael Bottino Date: Fri, 28 Apr 2023 13:30:30 -0500 Subject: [PATCH 18/25] Moving to use CloudTrail ABI Submodule. --- .gitmodules | 3 ++ submodules/cfn-abi-aws-cloudtrail | 2 +- .../trend-cloudone-onboard/main.template.yaml | 30 +++++++++---------- 3 files changed, 18 insertions(+), 17 deletions(-) diff --git a/.gitmodules b/.gitmodules index e69de29..4edf46f 100644 --- a/.gitmodules +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "submodules/cfn-abi-aws-cloudtrail"] + path = submodules/cfn-abi-aws-cloudtrail + url = https://github.com/aws-ia/cfn-abi-aws-cloudtrail.git diff --git a/submodules/cfn-abi-aws-cloudtrail b/submodules/cfn-abi-aws-cloudtrail index 3fcc82b..97f9a40 160000 --- a/submodules/cfn-abi-aws-cloudtrail +++ b/submodules/cfn-abi-aws-cloudtrail @@ -1 +1 @@ -Subproject commit 3fcc82b8145b6c70d2a700de57b317c9017b7725 +Subproject commit 97f9a407d2b3a7cf5d5ddf9d551d81e910f4c14d diff --git a/templates/trend-cloudone-onboard/main.template.yaml b/templates/trend-cloudone-onboard/main.template.yaml index c902ed0..0f725a9 100644 --- a/templates/trend-cloudone-onboard/main.template.yaml +++ b/templates/trend-cloudone-onboard/main.template.yaml @@ -12,7 +12,7 @@ Metadata: - VisionOneRegion - TrendSolutionTagKey - TrendSolutionName - - ExistingCloudtrailBucketName + - ExistingOrganizationalCloudtrailBucketName - Label: default: 'Warning: Do not modify the fields below unless you know what you are doing. Modifications may cause your deployment to fail.' @@ -51,11 +51,10 @@ Parameters: Description: Tag Key to be used for Trend Cloud One resources Type: String Default: 'Trend Cloud One' - ExistingCloudtrailBucketName: - Description: Optional. If left empty, the first existing Organizational Trail will be - selected and its bucket will be used as source for this integration. If you specify - a bucket that already has a Organizational Trail, that bucket will be used for this - integration. + ExistingOrganizationalCloudtrailBucketName: + Description: Optional. If left empty, a new Organizational Trail will be created and + used as source for this integration. If you specify a bucket that already has a + Organizational Trail, that bucket will be used for this integration. Type: String Default: "" QSS3BucketName: @@ -85,8 +84,8 @@ Parameters: Type: String Conditions: - FindCloudtrailBucketName: - !Equals [!Ref ExistingCloudtrailBucketName, ""] + CreateNewOrganizationalTrail: + !Equals [!Ref ExistingOrganizationalCloudtrailBucketName, ""] Resources: GetCloudOneRegionAndAccountStack: @@ -172,23 +171,21 @@ Resources: Type: AWS::CloudFormation::Stack Properties: Parameters: - ExistingCloudtrailBucketName: !If [FindCloudtrailBucketName, !GetAtt GetOrgTrailBucketStack.Outputs.OrganizationalTrailBucketName, !Ref ExistingCloudtrailBucketName] + ExistingCloudtrailBucketName: !If [CreateNewOrganizationalTrail, !GetAtt CloudTrailAbi.Outputs.oOrganizationCloudTrailS3BucketName, !Ref ExistingOrganizationalCloudtrailBucketName] ServiceToken: !GetAtt CloudTrailGetInfoStack.Outputs.ServiceToken ServiceURL: !GetAtt CloudTrailGetInfoStack.Outputs.ServiceURL S3BucketName: !GetAtt CloudTrailGetInfoStack.Outputs.S3BucketName APIVersion: !GetAtt CloudTrailGetInfoStack.Outputs.APIVersion TemplateURL: !GetAtt CloudTrailGetInfoStack.Outputs.TemplateURL - # Resources for fetching the Organizational Trail's bucket - GetOrgTrailBucketStack: - Condition: FindCloudtrailBucketName - DependsOn: rCopyZips + # Calling CloudTrail ABI to create a new organization trail + CloudTrailAbi: Type: AWS::CloudFormation::Stack Properties: Parameters: - TrendStagingS3Bucket: !Ref TrendStagingS3Bucket - QSS3KeyPrefix: !Ref QSS3KeyPrefix - TemplateURL: !Sub 'https://${QSS3BucketName}.s3.${QSS3BucketRegion}.${AWS::URLSuffix}/${QSS3KeyPrefix}/templates/trend-cloudone-onboard/get-org-trail-bucket.template.yaml' + pSRAS3BucketRegion: !Ref QSS3BucketRegion + pEnableDataEventsOnly: 'false' + TemplateURL: !Sub 'https://${QSS3BucketName}.s3.${QSS3BucketRegion}.${AWS::URLSuffix}/${QSS3KeyPrefix}/submodules/cfn-abi-aws-cloudtrail/templates/sra-cloudtrail-enable-in-org-ssm.yaml' # CopyZips Resources TrendStagingS3Bucket: @@ -237,6 +234,7 @@ Resources: - lambda_functions/packages/GetCloudTrailStackParametersFunction/lambda.zip - lambda_functions/packages/GetExternalIDLambda/lambda.zip - lambda_functions/packages/VisionOneEnrollmentFunction/lambda.zip + - lambda_functions/packages/VisionOneGenerateEnrollmentTokenFunction/lambda.zip - lambda_functions/packages/GetOrganizationalTrailBucketName/lambda.zip From 0c3ac1da04b5136952cfc80793151c14f52f4d89 Mon Sep 17 00:00:00 2001 From: Raphael Bottino Date: Fri, 28 Apr 2023 15:47:55 -0500 Subject: [PATCH 19/25] Removed submodule --- .gitmodules | 3 --- submodules/cfn-abi-aws-cloudtrail | 1 - 2 files changed, 4 deletions(-) delete mode 100644 .gitmodules delete mode 160000 submodules/cfn-abi-aws-cloudtrail diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 4edf46f..0000000 --- a/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "submodules/cfn-abi-aws-cloudtrail"] - path = submodules/cfn-abi-aws-cloudtrail - url = https://github.com/aws-ia/cfn-abi-aws-cloudtrail.git diff --git a/submodules/cfn-abi-aws-cloudtrail b/submodules/cfn-abi-aws-cloudtrail deleted file mode 160000 index 97f9a40..0000000 --- a/submodules/cfn-abi-aws-cloudtrail +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 97f9a407d2b3a7cf5d5ddf9d551d81e910f4c14d From b90828d3c776cc3e7ff231d37b2e592b274474a6 Mon Sep 17 00:00:00 2001 From: Raphael Bottino Date: Fri, 28 Apr 2023 15:59:35 -0500 Subject: [PATCH 20/25] Removing CloudTrail ABI and making having a CloudTrail mandatory. --- .../trend-cloudone-onboard/main.template.yaml | 24 ++++--------------- 1 file changed, 5 insertions(+), 19 deletions(-) diff --git a/templates/trend-cloudone-onboard/main.template.yaml b/templates/trend-cloudone-onboard/main.template.yaml index 0f725a9..bd622e8 100644 --- a/templates/trend-cloudone-onboard/main.template.yaml +++ b/templates/trend-cloudone-onboard/main.template.yaml @@ -52,11 +52,11 @@ Parameters: Type: String Default: 'Trend Cloud One' ExistingOrganizationalCloudtrailBucketName: - Description: Optional. If left empty, a new Organizational Trail will be created and - used as source for this integration. If you specify a bucket that already has a - Organizational Trail, that bucket will be used for this integration. + AllowedPattern: ^[0-9a-zA-Z]+([0-9a-zA-Z-]*[0-9a-zA-Z])*$ + Description: Bucket name of an existing Organizational CloudTrail. If you do not + have an existing Organizational CloudTrail, please create one before deploying. Type: String - Default: "" + MinLength: "3" QSS3BucketName: AllowedPattern: ^[0-9a-zA-Z]+([0-9a-zA-Z-]*[0-9a-zA-Z])*$ ConstraintDescription: Deployment bucket name can include numbers, lowercase @@ -83,10 +83,6 @@ Parameters: Description: Region name for the Deployment bucket. Type: String -Conditions: - CreateNewOrganizationalTrail: - !Equals [!Ref ExistingOrganizationalCloudtrailBucketName, ""] - Resources: GetCloudOneRegionAndAccountStack: DependsOn: rCopyZips @@ -134,7 +130,6 @@ Resources: QSS3KeyPrefix: !Ref QSS3KeyPrefix TemplateURL: !Sub 'https://${QSS3BucketName}.s3.${QSS3BucketRegion}.${AWS::URLSuffix}/${QSS3KeyPrefix}/templates/trend-cloudone-onboard/workloadsecurity.template.yaml' - CloudOneIntegrationStack: DependsOn: - VisionOneEnrollmentStack @@ -171,22 +166,13 @@ Resources: Type: AWS::CloudFormation::Stack Properties: Parameters: - ExistingCloudtrailBucketName: !If [CreateNewOrganizationalTrail, !GetAtt CloudTrailAbi.Outputs.oOrganizationCloudTrailS3BucketName, !Ref ExistingOrganizationalCloudtrailBucketName] + ExistingCloudtrailBucketName: ExistingOrganizationalCloudtrailBucketName ServiceToken: !GetAtt CloudTrailGetInfoStack.Outputs.ServiceToken ServiceURL: !GetAtt CloudTrailGetInfoStack.Outputs.ServiceURL S3BucketName: !GetAtt CloudTrailGetInfoStack.Outputs.S3BucketName APIVersion: !GetAtt CloudTrailGetInfoStack.Outputs.APIVersion TemplateURL: !GetAtt CloudTrailGetInfoStack.Outputs.TemplateURL - # Calling CloudTrail ABI to create a new organization trail - CloudTrailAbi: - Type: AWS::CloudFormation::Stack - Properties: - Parameters: - pSRAS3BucketRegion: !Ref QSS3BucketRegion - pEnableDataEventsOnly: 'false' - TemplateURL: !Sub 'https://${QSS3BucketName}.s3.${QSS3BucketRegion}.${AWS::URLSuffix}/${QSS3KeyPrefix}/submodules/cfn-abi-aws-cloudtrail/templates/sra-cloudtrail-enable-in-org-ssm.yaml' - # CopyZips Resources TrendStagingS3Bucket: Type: AWS::S3::Bucket From 6fb8907033d0215117ce7ff68cf73465e9d46afd Mon Sep 17 00:00:00 2001 From: Raphael Bottino Date: Fri, 28 Apr 2023 16:00:26 -0500 Subject: [PATCH 21/25] Adding the "!Ref" back. --- templates/trend-cloudone-onboard/main.template.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/trend-cloudone-onboard/main.template.yaml b/templates/trend-cloudone-onboard/main.template.yaml index bd622e8..77daf3d 100644 --- a/templates/trend-cloudone-onboard/main.template.yaml +++ b/templates/trend-cloudone-onboard/main.template.yaml @@ -166,7 +166,7 @@ Resources: Type: AWS::CloudFormation::Stack Properties: Parameters: - ExistingCloudtrailBucketName: ExistingOrganizationalCloudtrailBucketName + ExistingCloudtrailBucketName: !Ref ExistingOrganizationalCloudtrailBucketName ServiceToken: !GetAtt CloudTrailGetInfoStack.Outputs.ServiceToken ServiceURL: !GetAtt CloudTrailGetInfoStack.Outputs.ServiceURL S3BucketName: !GetAtt CloudTrailGetInfoStack.Outputs.S3BucketName From 52846acb3912c7dec9e020ff40e07703b86e9086 Mon Sep 17 00:00:00 2001 From: Raphael Bottino Date: Mon, 1 May 2023 13:15:38 -0500 Subject: [PATCH 22/25] Adding NoEcho to Vision One Auth token. --- .gitignore | 1 + lambda_functions/source/AWSConnectorCreateLambda/app.py | 2 -- .../trend-cloudone-onboard/vision-one-enrollment.template.yaml | 1 + 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 05faa34..bf140d7 100644 --- a/.gitignore +++ b/.gitignore @@ -167,6 +167,7 @@ cython_debug/ taskcat_outputs/ taskcat_outputs .taskcat +.taskcat.secrets.yml # ASH tool files aggregated_results.txt diff --git a/lambda_functions/source/AWSConnectorCreateLambda/app.py b/lambda_functions/source/AWSConnectorCreateLambda/app.py index 3e5e40e..0b68b0d 100644 --- a/lambda_functions/source/AWSConnectorCreateLambda/app.py +++ b/lambda_functions/source/AWSConnectorCreateLambda/app.py @@ -11,14 +11,12 @@ import os def lambda_handler(event, context): - print(event) status = cfnresponse.SUCCESS response_data = {} physicalResourceId = None accountId = os.environ['awsaccountid'] - externalId = os.environ['externalid'] crossAccountRoleArn = os.environ['crossaccountrolearn'] cloudOneApiKey = event['ResourceProperties']['CloudOneApiKey'] cloudOneRegion = event['ResourceProperties']['CloudOneRegion'] diff --git a/templates/trend-cloudone-onboard/vision-one-enrollment.template.yaml b/templates/trend-cloudone-onboard/vision-one-enrollment.template.yaml index 7eb08b9..dcb1829 100644 --- a/templates/trend-cloudone-onboard/vision-one-enrollment.template.yaml +++ b/templates/trend-cloudone-onboard/vision-one-enrollment.template.yaml @@ -23,6 +23,7 @@ Parameters: VisionOneServiceToken: Description: Vision One Service Token. See step 1 at https://docs.trendmicro.com/en-us/enterprise/trend-micro-xdr-help/ConfiguringCloudOneWorkloadSecurity/ Type: String + NoEcho: true TrendStagingS3Bucket: AllowedPattern: ^[0-9a-zA-Z]+([0-9a-zA-Z-]*[0-9a-zA-Z])*$ ConstraintDescription: Deployment bucket name can include numbers, lowercase From 92baf35b39cd1c45503ed853f368c0057fc30891 Mon Sep 17 00:00:00 2001 From: Raphael Bottino Date: Mon, 1 May 2023 13:38:24 -0500 Subject: [PATCH 23/25] Updating README.md --- images/cloudtrail-integration.png | Bin 0 -> 61596 bytes templates/trend-cloudone-onboard/README.md | 8 ++++++-- 2 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 images/cloudtrail-integration.png diff --git a/images/cloudtrail-integration.png b/images/cloudtrail-integration.png new file mode 100644 index 0000000000000000000000000000000000000000..c740c4ce7a77256554945394e0602d2b44315ec4 GIT binary patch literal 61596 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 R*FFXy@O1TaS?83{1OU8YzE1!E literal 0 HcmV?d00001 diff --git a/templates/trend-cloudone-onboard/README.md b/templates/trend-cloudone-onboard/README.md index b2a538b..5ccd8ac 100644 --- a/templates/trend-cloudone-onboard/README.md +++ b/templates/trend-cloudone-onboard/README.md @@ -1,6 +1,10 @@ -# Add AWS Account to Cloud One +# Add AWS Account to Cloud One and Enable CloudTrail Analyzes -To fully integrate an AWS account in Cloud One, you must deploy resources in your AWS account and do manual steps in Trend Cloud One dashboard. This CloudFormation template automates all these steps on your behalf, including integrating it to Vision One. +To fully integrate an AWS account in Cloud One, you must deploy resources in your AWS account and do manual steps in Trend Cloud One dashboard. This CloudFormation template automates all these steps on your behalf, including enabling the CloudTrail integration and connecting it to your Vision One tenant. Once it's working, you will start to receive alerts on any CloudTrail events that trigger a detection model in the Workbench App in Vision One - (XDR Threat Investigation > Workbench). This helps your organization to quickly detect suspicious malicious behavior on their AWS account and enable its Security and Cloud Operation teams to respond quickly to security events. + +## CloudTrail Integration Architecture + +![CloudTrail Integration Architecture](../../images/cloudtrail-integration.png) ## What does it actually do? From a086d28e35cd2bc3557f00e561f3d8c5b4f72cd2 Mon Sep 17 00:00:00 2001 From: Raphael Bottino Date: Tue, 2 May 2023 13:20:21 -0500 Subject: [PATCH 24/25] Fixing taskcat. Removing crhelper. Creating KMS and Secrets. Utilizing secrets instead of plaintext for confidential data. Minor fixes. Response to PR comments. --- .taskcat.yml | 1 + .../source/AWSConnectorCreateLambda/app.py | 4 +- .../AWSConnectorCreateLambda/requirements.txt | 1 - .../AddAWSAccountToCloudOneFunction/app.py | 4 +- .../requirements.txt | 1 - .../app.py | 4 +- .../requirements.txt | 1 - .../app.py | 4 +- .../requirements.txt | 1 - .../source/GetExternalIDLambda/app.py | 4 +- .../GetExternalIDLambda/requirements.txt | 1 - .../requirements.txt | 1 - .../source/VisionOneEnrollmentFunction/app.py | 4 +- .../requirements.txt | 1 - .../app.py | 4 +- .../requirements.txt | 1 - .../cloudone.template.yaml | 32 +++++++--- .../cloudtrail.template.yaml | 31 +++++++--- .../get-cloudone-region-and-account.yaml | 32 +++++++--- .../trend-cloudone-onboard/main.template.yaml | 58 ++++++++++++++++--- .../vision-one-enrollment.template.yaml | 32 +++++++--- .../vision-one-generate-enrollment-token.yaml | 25 +++++++- .../workloadsecurity.template.yaml | 35 +++++++---- 23 files changed, 211 insertions(+), 71 deletions(-) diff --git a/.taskcat.yml b/.taskcat.yml index 4bf4f0a..4f5c770 100644 --- a/.taskcat.yml +++ b/.taskcat.yml @@ -21,6 +21,7 @@ tests: VisionOneAuthenticationToken: $[taskcat_ssm_/trend/visionone_authentication_token] QSS3BucketName: $[taskcat_autobucket] QSS3KeyPrefix: $[taskcat_project_name] + ExistingOrganizationalCloudtrailBucketName: $[taskcat_ssm_/trend/existing_organizational_cloudtrail_bucket_name] regions: - us-east-1 template: templates/trend-cloudone-onboard/main.template.yaml diff --git a/lambda_functions/source/AWSConnectorCreateLambda/app.py b/lambda_functions/source/AWSConnectorCreateLambda/app.py index 0b68b0d..e97b5e0 100644 --- a/lambda_functions/source/AWSConnectorCreateLambda/app.py +++ b/lambda_functions/source/AWSConnectorCreateLambda/app.py @@ -9,6 +9,7 @@ import urllib3 import cfnresponse import os +import boto3 def lambda_handler(event, context): @@ -18,7 +19,8 @@ def lambda_handler(event, context): accountId = os.environ['awsaccountid'] crossAccountRoleArn = os.environ['crossaccountrolearn'] - cloudOneApiKey = event['ResourceProperties']['CloudOneApiKey'] + sm = boto3.client('secretsmanager') + cloudOneApiKey = sm.get_secret_value(SecretId=event['ResourceProperties']['CloudOneApiKeySecret'])['SecretString'] cloudOneRegion = event['ResourceProperties']['CloudOneRegion'] headers = { diff --git a/lambda_functions/source/AWSConnectorCreateLambda/requirements.txt b/lambda_functions/source/AWSConnectorCreateLambda/requirements.txt index df1db04..b6f7d1a 100644 --- a/lambda_functions/source/AWSConnectorCreateLambda/requirements.txt +++ b/lambda_functions/source/AWSConnectorCreateLambda/requirements.txt @@ -1,2 +1 @@ # install the latest version -crhelper \ No newline at end of file diff --git a/lambda_functions/source/AddAWSAccountToCloudOneFunction/app.py b/lambda_functions/source/AddAWSAccountToCloudOneFunction/app.py index 4a1a304..1907b96 100644 --- a/lambda_functions/source/AddAWSAccountToCloudOneFunction/app.py +++ b/lambda_functions/source/AddAWSAccountToCloudOneFunction/app.py @@ -9,6 +9,7 @@ import os import urllib3 import cfnresponse +import boto3 def lambda_handler(event, context): status = cfnresponse.SUCCESS @@ -18,7 +19,8 @@ def lambda_handler(event, context): cloudOneRoleArn = os.environ['CloudOneRoleArn'] cloudOneRegion = os.environ['CloudOneRegion'] - cloudOneApiKey = os.environ['CloudOneApiKey'] + sm = boto3.client('secretsmanager') + cloudOneApiKey = sm.get_secret_value(SecretId=os.environ['CloudOneApiKeySecret'])['SecretString'] headers = { 'api-version': 'v1', diff --git a/lambda_functions/source/AddAWSAccountToCloudOneFunction/requirements.txt b/lambda_functions/source/AddAWSAccountToCloudOneFunction/requirements.txt index df1db04..b6f7d1a 100644 --- a/lambda_functions/source/AddAWSAccountToCloudOneFunction/requirements.txt +++ b/lambda_functions/source/AddAWSAccountToCloudOneFunction/requirements.txt @@ -1,2 +1 @@ # install the latest version -crhelper \ No newline at end of file diff --git a/lambda_functions/source/GetCloudOneRegionAndAccountFunction/app.py b/lambda_functions/source/GetCloudOneRegionAndAccountFunction/app.py index 0cadaa1..223689f 100644 --- a/lambda_functions/source/GetCloudOneRegionAndAccountFunction/app.py +++ b/lambda_functions/source/GetCloudOneRegionAndAccountFunction/app.py @@ -9,6 +9,7 @@ import os import urllib3 import cfnresponse +import boto3 def lambda_handler(event, context): status = cfnresponse.SUCCESS @@ -17,7 +18,8 @@ def lambda_handler(event, context): try: if event["RequestType"] == "Create" or event["RequestType"] == "Update": - cloudOneApiKey = os.environ['CloudOneApiKey'] + sm = boto3.client('secretsmanager') + cloudOneApiKey = sm.get_secret_value(SecretId=os.environ['CloudOneApiKeySecret'])['SecretString'] apiKeyId = cloudOneApiKey.split(':')[0] url = 'https://accounts.cloudone.trendmicro.com/api/apikeys/' + apiKeyId diff --git a/lambda_functions/source/GetCloudOneRegionAndAccountFunction/requirements.txt b/lambda_functions/source/GetCloudOneRegionAndAccountFunction/requirements.txt index df1db04..b6f7d1a 100644 --- a/lambda_functions/source/GetCloudOneRegionAndAccountFunction/requirements.txt +++ b/lambda_functions/source/GetCloudOneRegionAndAccountFunction/requirements.txt @@ -1,2 +1 @@ # install the latest version -crhelper \ No newline at end of file diff --git a/lambda_functions/source/GetCloudTrailStackParametersFunction/app.py b/lambda_functions/source/GetCloudTrailStackParametersFunction/app.py index e64ce4c..84314ac 100644 --- a/lambda_functions/source/GetCloudTrailStackParametersFunction/app.py +++ b/lambda_functions/source/GetCloudTrailStackParametersFunction/app.py @@ -12,6 +12,7 @@ from urllib.parse import unquote import urllib3 import cfnresponse +import boto3 def lambda_handler(event, context): status = cfnresponse.SUCCESS @@ -21,7 +22,8 @@ def lambda_handler(event, context): if event["RequestType"] == "Create" or event["RequestType"] == "Update": cloudOneRegion = os.environ['CloudOneRegion'] - cloudOneApiKey = os.environ['CloudOneApiKey'] + sm = boto3.client('secretsmanager') + cloudOneApiKey = sm.get_secret_value(SecretId=os.environ['CloudOneApiKeySecret'])['SecretString'] awsAccountId = os.environ['AwsAccountId'] awsRegion = os.environ['AwsRegion'] diff --git a/lambda_functions/source/GetCloudTrailStackParametersFunction/requirements.txt b/lambda_functions/source/GetCloudTrailStackParametersFunction/requirements.txt index df1db04..b6f7d1a 100644 --- a/lambda_functions/source/GetCloudTrailStackParametersFunction/requirements.txt +++ b/lambda_functions/source/GetCloudTrailStackParametersFunction/requirements.txt @@ -1,2 +1 @@ # install the latest version -crhelper \ No newline at end of file diff --git a/lambda_functions/source/GetExternalIDLambda/app.py b/lambda_functions/source/GetExternalIDLambda/app.py index ad8fbf4..eea0a89 100644 --- a/lambda_functions/source/GetExternalIDLambda/app.py +++ b/lambda_functions/source/GetExternalIDLambda/app.py @@ -8,6 +8,7 @@ import json import urllib3 import cfnresponse +import boto3 def lambda_handler(event, context): status = cfnresponse.SUCCESS @@ -16,7 +17,8 @@ def lambda_handler(event, context): try: if event["RequestType"] == "Create" or event["RequestType"] == "Update": - cloudOneApiKey = event['ResourceProperties']['CloudOneApiKey'] + sm = boto3.client('secretsmanager') + cloudOneApiKey = sm.get_secret_value(SecretId=event['ResourceProperties']['CloudOneApiKeySecret'])['SecretString'] cloudOneRegion = event['ResourceProperties']['CloudOneRegion'] url = 'https://workload.'+cloudOneRegion+'.cloudone.trendmicro.com/api/awsconnectorsettings' diff --git a/lambda_functions/source/GetExternalIDLambda/requirements.txt b/lambda_functions/source/GetExternalIDLambda/requirements.txt index df1db04..b6f7d1a 100644 --- a/lambda_functions/source/GetExternalIDLambda/requirements.txt +++ b/lambda_functions/source/GetExternalIDLambda/requirements.txt @@ -1,2 +1 @@ # install the latest version -crhelper \ No newline at end of file diff --git a/lambda_functions/source/GetOrganizationalTrailBucketName/requirements.txt b/lambda_functions/source/GetOrganizationalTrailBucketName/requirements.txt index df1db04..b6f7d1a 100644 --- a/lambda_functions/source/GetOrganizationalTrailBucketName/requirements.txt +++ b/lambda_functions/source/GetOrganizationalTrailBucketName/requirements.txt @@ -1,2 +1 @@ # install the latest version -crhelper \ No newline at end of file diff --git a/lambda_functions/source/VisionOneEnrollmentFunction/app.py b/lambda_functions/source/VisionOneEnrollmentFunction/app.py index 9338450..c243c18 100644 --- a/lambda_functions/source/VisionOneEnrollmentFunction/app.py +++ b/lambda_functions/source/VisionOneEnrollmentFunction/app.py @@ -9,6 +9,7 @@ import os import urllib3 import cfnresponse +import boto3 def lambda_handler(event, context): status = cfnresponse.SUCCESS @@ -16,7 +17,8 @@ def lambda_handler(event, context): physicalResourceId = None try: cloudOneRegion = os.environ['CloudOneRegion'] - cloudOneApiKey = os.environ['CloudOneApiKey'] + sm = boto3.client('secretsmanager') + cloudOneApiKey = sm.get_secret_value(SecretId=os.environ['CloudOneApiKeySecret'])['SecretString'] visionOneServiceToken = os.environ['VisionOneServiceToken'] headers = { diff --git a/lambda_functions/source/VisionOneEnrollmentFunction/requirements.txt b/lambda_functions/source/VisionOneEnrollmentFunction/requirements.txt index df1db04..b6f7d1a 100644 --- a/lambda_functions/source/VisionOneEnrollmentFunction/requirements.txt +++ b/lambda_functions/source/VisionOneEnrollmentFunction/requirements.txt @@ -1,2 +1 @@ # install the latest version -crhelper \ No newline at end of file diff --git a/lambda_functions/source/VisionOneGenerateEnrollmentTokenFunction/app.py b/lambda_functions/source/VisionOneGenerateEnrollmentTokenFunction/app.py index 997c73f..f586d16 100644 --- a/lambda_functions/source/VisionOneGenerateEnrollmentTokenFunction/app.py +++ b/lambda_functions/source/VisionOneGenerateEnrollmentTokenFunction/app.py @@ -9,6 +9,7 @@ import os import urllib3 import cfnresponse +import boto3 def define_endpoint(region): if region == "Australia": @@ -30,7 +31,8 @@ def lambda_handler(event, context): response_data = {} physicalResourceId = None try: - visionOneAuthenticationToken = os.environ['VisionOneAuthenticationToken'] + sm = boto3.client('secretsmanager') + visionOneAuthenticationToken = sm.get_secret_value(SecretId=os.environ['VisionOneAuthenticationTokenSecret'])['SecretString'] visionOneRegion = os.environ['VisionOneRegion'] headers = { diff --git a/lambda_functions/source/VisionOneGenerateEnrollmentTokenFunction/requirements.txt b/lambda_functions/source/VisionOneGenerateEnrollmentTokenFunction/requirements.txt index df1db04..b6f7d1a 100644 --- a/lambda_functions/source/VisionOneGenerateEnrollmentTokenFunction/requirements.txt +++ b/lambda_functions/source/VisionOneGenerateEnrollmentTokenFunction/requirements.txt @@ -1,2 +1 @@ # install the latest version -crhelper \ No newline at end of file diff --git a/templates/trend-cloudone-onboard/cloudone.template.yaml b/templates/trend-cloudone-onboard/cloudone.template.yaml index bf00cae..dfd9159 100644 --- a/templates/trend-cloudone-onboard/cloudone.template.yaml +++ b/templates/trend-cloudone-onboard/cloudone.template.yaml @@ -5,8 +5,9 @@ Parameters: CloudOneAccountID: Description: Cloud One Account Id. You can learn more about it at https://cloudone.trendmicro.com/docs/identity-and-account-management/c1-api-key/ Type: String - CloudOneApiKey: - Description: Cloud One API Key. You can learn more about it at https://cloudone.trendmicro.com/docs/identity-and-account-management/c1-api-key/ + CloudOneApiKeySecret: + Description: Arn of Cloud One API Key Secret. You can learn more about it at + https://cloudone.trendmicro.com/docs/identity-and-account-management/c1-api-key/ Type: String NoEcho: true CloudOneRegion: @@ -23,6 +24,9 @@ Parameters: - jp-1 - ca-1 - de-1 + TrendKey: + Description: KMS Key Arn used to encrypt all Trend secrets. + Type: String TrendStagingS3Bucket: AllowedPattern: ^[0-9a-zA-Z]+([0-9a-zA-Z-]*[0-9a-zA-Z])*$ ConstraintDescription: Deployment bucket name can include numbers, lowercase @@ -88,7 +92,7 @@ Resources: Variables: CloudOneRoleArn: !GetAtt CloudOneIntegrationStack.Outputs.CloudOneRoleArn CloudOneRegion: !Ref CloudOneRegion - CloudOneApiKey: !Ref CloudOneApiKey + CloudOneApiKeySecret: !Ref CloudOneApiKeySecret Code: S3Bucket: !Ref 'TrendStagingS3Bucket' S3Key: !Sub ${QSS3KeyPrefix}/lambda_functions/packages/AddAWSAccountToCloudOneFunction/lambda.zip @@ -112,9 +116,19 @@ Resources: - sts:AssumeRole Path: "/" ManagedPolicyArns: - - Fn::Join: - - "" - - - "arn:" - - Ref: AWS::Partition - - :iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - \ No newline at end of file + - !Sub "arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + Policies: + - PolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Action: + - secretsmanager:GetSecretValue + Resource: + - !Sub "${CloudOneApiKeySecret}*" + - Effect: Allow + Action: + - kms:Decrypt + Resource: + - !Ref TrendKey + PolicyName: CloudOneApiKeySecretDecryptPolicy diff --git a/templates/trend-cloudone-onboard/cloudtrail.template.yaml b/templates/trend-cloudone-onboard/cloudtrail.template.yaml index 155f701..a04bbdc 100644 --- a/templates/trend-cloudone-onboard/cloudtrail.template.yaml +++ b/templates/trend-cloudone-onboard/cloudtrail.template.yaml @@ -2,8 +2,9 @@ AWSTemplateFormatVersion: 2010-09-09 Description: Trend Cloud One CloudTrail Onboarding. Parameters: - CloudOneApiKey: - Description: Cloud One API Key. You can learn more about it at https://cloudone.trendmicro.com/docs/identity-and-account-management/c1-api-key/ + CloudOneApiKeySecret: + Description: Arn of Cloud One API Key Secret. You can learn more about it at + https://cloudone.trendmicro.com/docs/identity-and-account-management/c1-api-key/ Type: String NoEcho: true CloudOneRegion: @@ -19,6 +20,9 @@ Parameters: - in-1 - jp-1 - ca-1 + TrendKey: + Description: KMS Key Arn used to encrypt all Trend secrets. + Type: String TrendStagingS3Bucket: AllowedPattern: ^[0-9a-zA-Z]+([0-9a-zA-Z-]*[0-9a-zA-Z])*$ ConstraintDescription: Deployment bucket name can include numbers, lowercase @@ -71,7 +75,7 @@ Resources: Environment: Variables: CloudOneRegion: !Ref CloudOneRegion - CloudOneApiKey: !Ref CloudOneApiKey + CloudOneApiKeySecret: !Ref CloudOneApiKeySecret AwsAccountId: !Ref "AWS::AccountId" AwsRegion: !Ref "AWS::Region" Code: @@ -97,11 +101,22 @@ Resources: - sts:AssumeRole Path: "/" ManagedPolicyArns: - - Fn::Join: - - "" - - - "arn:" - - Ref: AWS::Partition - - :iam::aws:policy/service-role/AWSLambdaBasicExecutionRole + - !Sub "arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + Policies: + - PolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Action: + - secretsmanager:GetSecretValue + Resource: + - !Sub "${CloudOneApiKeySecret}*" + - Effect: Allow + Action: + - kms:Decrypt + Resource: + - !Ref TrendKey + PolicyName: CloudOneApiKeySecretDecryptPolicy Outputs: ServiceToken: diff --git a/templates/trend-cloudone-onboard/get-cloudone-region-and-account.yaml b/templates/trend-cloudone-onboard/get-cloudone-region-and-account.yaml index 0d1f03f..59e5979 100644 --- a/templates/trend-cloudone-onboard/get-cloudone-region-and-account.yaml +++ b/templates/trend-cloudone-onboard/get-cloudone-region-and-account.yaml @@ -2,10 +2,14 @@ AWSTemplateFormatVersion: 2010-09-09 Description: Get Trend Cloud One region and account id given an API Key. Parameters: - CloudOneApiKey: - Description: Cloud One API Key. You can learn more about it at https://cloudone.trendmicro.com/docs/identity-and-account-management/c1-api-key/ + CloudOneApiKeySecret: + Description: Arn of the Cloud One API Key Secret. You can learn more about it + at https://cloudone.trendmicro.com/docs/identity-and-account-management/c1-api-key/ Type: String NoEcho: true + TrendKey: + Description: KMS Key Arn used to encrypt all Trend secrets. + Type: String TrendStagingS3Bucket: AllowedPattern: ^[0-9a-zA-Z]+([0-9a-zA-Z-]*[0-9a-zA-Z])*$ ConstraintDescription: Deployment bucket name can include numbers, lowercase @@ -57,7 +61,7 @@ Resources: Role: !GetAtt GetCloudOneRegionAndAccountRole.Arn Environment: Variables: - CloudOneApiKey: !Ref CloudOneApiKey + CloudOneApiKeySecret: !Ref CloudOneApiKeySecret Code: S3Bucket: !Ref 'TrendStagingS3Bucket' S3Key: !Sub ${QSS3KeyPrefix}/lambda_functions/packages/GetCloudOneRegionAndAccountFunction/lambda.zip @@ -81,11 +85,23 @@ Resources: - sts:AssumeRole Path: "/" ManagedPolicyArns: - - Fn::Join: - - "" - - - "arn:" - - Ref: AWS::Partition - - :iam::aws:policy/service-role/AWSLambdaBasicExecutionRole + - !Sub "arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + Policies: + - PolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Action: + - secretsmanager:GetSecretValue + Resource: + - !Sub "${CloudOneApiKeySecret}*" + - Effect: Allow + Action: + - kms:Decrypt + Resource: + - !Ref TrendKey + PolicyName: CloudOneApiKeySecretDecryptPolicy + Outputs: CloudOneAccountId: diff --git a/templates/trend-cloudone-onboard/main.template.yaml b/templates/trend-cloudone-onboard/main.template.yaml index 77daf3d..f025893 100644 --- a/templates/trend-cloudone-onboard/main.template.yaml +++ b/templates/trend-cloudone-onboard/main.template.yaml @@ -50,7 +50,7 @@ Parameters: TrendSolutionName: Description: Tag Key to be used for Trend Cloud One resources Type: String - Default: 'Trend Cloud One' + Default: 'TrendCloudOne' ExistingOrganizationalCloudtrailBucketName: AllowedPattern: ^[0-9a-zA-Z]+([0-9a-zA-Z-]*[0-9a-zA-Z])*$ Description: Bucket name of an existing Organizational CloudTrail. If you do not @@ -71,7 +71,7 @@ Parameters: AllowedPattern: ^[0-9a-zA-Z-/.]*$ ConstraintDescription: Deployment key prefix can include numbers, lowercase letters, uppercase letters, hyphens (-), dots(.) and forward slash (/). - Default: "cfn-abi-aws-trend-cloudone/" + Default: "cfn-abi-trend-cloudone/" Description: S3 key prefix for the Deployment assets. Deployment key prefix can include numbers, lowercase letters, uppercase letters, hyphens (-), dots(.) and forward slash (/). @@ -89,8 +89,9 @@ Resources: Type: AWS::CloudFormation::Stack Properties: Parameters: - CloudOneApiKey: !Ref CloudOneApiKey + CloudOneApiKeySecret: !Ref CloudOneApiKeySecret TrendStagingS3Bucket: !Ref TrendStagingS3Bucket + TrendKey: !GetAtt TrendKey.Arn QSS3KeyPrefix: !Ref QSS3KeyPrefix TemplateURL: !Sub 'https://${QSS3BucketName}.s3.${QSS3BucketRegion}.${AWS::URLSuffix}/${QSS3KeyPrefix}/templates/trend-cloudone-onboard/get-cloudone-region-and-account.yaml' @@ -99,8 +100,9 @@ Resources: Type: AWS::CloudFormation::Stack Properties: Parameters: - VisionOneAuthenticationToken: !Ref VisionOneAuthenticationToken + VisionOneAuthenticationTokenSecret: !Ref VisionOneAuthenticationTokenSecret VisionOneRegion: !Ref VisionOneRegion + TrendKey: !GetAtt TrendKey.Arn TrendStagingS3Bucket: !Ref TrendStagingS3Bucket QSS3KeyPrefix: !Ref QSS3KeyPrefix TemplateURL: !Sub 'https://${QSS3BucketName}.s3.${QSS3BucketRegion}.${AWS::URLSuffix}/${QSS3KeyPrefix}/templates/trend-cloudone-onboard/vision-one-generate-enrollment-token.yaml' @@ -110,9 +112,10 @@ Resources: Type: AWS::CloudFormation::Stack Properties: Parameters: - CloudOneApiKey: !Ref CloudOneApiKey + CloudOneApiKeySecret: !Ref CloudOneApiKeySecret CloudOneRegion: !GetAtt GetCloudOneRegionAndAccountStack.Outputs.CloudOneRegion VisionOneServiceToken: !GetAtt GenerateVisionOneServiceTokenStack.Outputs.VisionOneEnrollmentToken + TrendKey: !GetAtt TrendKey.Arn TrendStagingS3Bucket: !Ref TrendStagingS3Bucket QSS3KeyPrefix: !Ref QSS3KeyPrefix TemplateURL: !Sub 'https://${QSS3BucketName}.s3.${QSS3BucketRegion}.${AWS::URLSuffix}/${QSS3KeyPrefix}/templates/trend-cloudone-onboard/vision-one-enrollment.template.yaml' @@ -124,8 +127,9 @@ Resources: Type: AWS::CloudFormation::Stack Properties: Parameters: - CloudOneApiKey: !Ref CloudOneApiKey + CloudOneApiKeySecret: !Ref CloudOneApiKeySecret CloudOneRegion: !GetAtt GetCloudOneRegionAndAccountStack.Outputs.CloudOneRegion + TrendKey: !GetAtt TrendKey.Arn TrendStagingS3Bucket: !Ref TrendStagingS3Bucket QSS3KeyPrefix: !Ref QSS3KeyPrefix TemplateURL: !Sub 'https://${QSS3BucketName}.s3.${QSS3BucketRegion}.${AWS::URLSuffix}/${QSS3KeyPrefix}/templates/trend-cloudone-onboard/workloadsecurity.template.yaml' @@ -139,7 +143,8 @@ Resources: Parameters: CloudOneRegion: !GetAtt GetCloudOneRegionAndAccountStack.Outputs.CloudOneRegion CloudOneAccountID: !GetAtt GetCloudOneRegionAndAccountStack.Outputs.CloudOneAccountId - CloudOneApiKey: !Ref CloudOneApiKey + CloudOneApiKeySecret: !Ref CloudOneApiKeySecret + TrendKey: !GetAtt TrendKey.Arn TrendStagingS3Bucket: !Ref TrendStagingS3Bucket QSS3KeyPrefix: !Ref QSS3KeyPrefix TemplateURL: !Sub 'https://${QSS3BucketName}.s3.${QSS3BucketRegion}.${AWS::URLSuffix}/${QSS3KeyPrefix}/templates/trend-cloudone-onboard/cloudone.template.yaml' @@ -153,13 +158,13 @@ Resources: Properties: Parameters: CloudOneRegion: !GetAtt GetCloudOneRegionAndAccountStack.Outputs.CloudOneRegion - CloudOneApiKey: !Ref CloudOneApiKey + CloudOneApiKeySecret: !Ref CloudOneApiKeySecret + TrendKey: !GetAtt TrendKey.Arn TrendStagingS3Bucket: !Ref TrendStagingS3Bucket QSS3KeyPrefix: !Ref QSS3KeyPrefix # This CloudFormation template is not available in the public repository. Instead, it is dynamically generated by an API call to Cloud One # backend in the CloudFormation Stack above (CloudOneIntegrationStack). This can't be local because of it. TemplateURL: !Sub 'https://${QSS3BucketName}.s3.${QSS3BucketRegion}.${AWS::URLSuffix}/${QSS3KeyPrefix}/templates/trend-cloudone-onboard/cloudtrail.template.yaml' - # CTrl is short for CloudTrail. This is name was truncated because one of the resources in this stack has a 64 character limit. CTrl: @@ -173,6 +178,41 @@ Resources: APIVersion: !GetAtt CloudTrailGetInfoStack.Outputs.APIVersion TemplateURL: !GetAtt CloudTrailGetInfoStack.Outputs.TemplateURL + # Secrets Manager resources to hold the Cloud One API Key and the Vision One Enrollment Token + TrendKey: + Type: AWS::KMS::Key + Properties: + Description: Key used to encrypt/decrypt Trend Micro secrets. + Enabled: true + EnableKeyRotation: true + KeySpec: SYMMETRIC_DEFAULT + KeyPolicy: + Version: 2012-10-17 + Id: key-default-1 + Statement: + - Sid: Enable IAM User Permissions + Effect: Allow + Principal: + AWS: !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:root" + Action: 'kms:*' + Resource: '*' + + CloudOneApiKeySecret: + Type: AWS::SecretsManager::Secret + Properties: + Name: !Sub '${TrendSolutionName}-CloudOneApiKey-${AWS::StackName}' + Description: Trend Cloud One API Key + SecretString: !Ref CloudOneApiKey + KmsKeyId: !GetAtt TrendKey.Arn + + VisionOneAuthenticationTokenSecret: + Type: AWS::SecretsManager::Secret + Properties: + Name: !Sub '${TrendSolutionName}-VisionOneAuthToken-${AWS::StackName}' + Description: Trend Vision One Auth Key + SecretString: !Ref VisionOneAuthenticationToken + KmsKeyId: !GetAtt TrendKey.Arn + # CopyZips Resources TrendStagingS3Bucket: Type: AWS::S3::Bucket diff --git a/templates/trend-cloudone-onboard/vision-one-enrollment.template.yaml b/templates/trend-cloudone-onboard/vision-one-enrollment.template.yaml index dcb1829..ea1f668 100644 --- a/templates/trend-cloudone-onboard/vision-one-enrollment.template.yaml +++ b/templates/trend-cloudone-onboard/vision-one-enrollment.template.yaml @@ -2,8 +2,9 @@ AWSTemplateFormatVersion: 2010-09-09 Description: Trend Cloud One Workload Security Integration. Parameters: - CloudOneApiKey: - Description: Cloud One API Key. You can learn more about it at https://cloudone.trendmicro.com/docs/identity-and-account-management/c1-api-key/ + CloudOneApiKeySecret: + Description: Arn of Cloud One API Key Secret. You can learn more about it at + https://cloudone.trendmicro.com/docs/identity-and-account-management/c1-api-key/ Type: String NoEcho: true CloudOneRegion: @@ -20,6 +21,9 @@ Parameters: - jp-1 - ca-1 - de-1 + TrendKey: + Description: KMS Key Arn used to encrypt all Trend secrets. + Type: String VisionOneServiceToken: Description: Vision One Service Token. See step 1 at https://docs.trendmicro.com/en-us/enterprise/trend-micro-xdr-help/ConfiguringCloudOneWorkloadSecurity/ Type: String @@ -76,7 +80,7 @@ Resources: Environment: Variables: CloudOneRegion: !Ref CloudOneRegion - CloudOneApiKey: !Ref CloudOneApiKey + CloudOneApiKeySecret: !Ref CloudOneApiKeySecret VisionOneServiceToken: !Ref VisionOneServiceToken Code: S3Bucket: !Ref 'TrendStagingS3Bucket' @@ -101,9 +105,19 @@ Resources: - sts:AssumeRole Path: "/" ManagedPolicyArns: - - Fn::Join: - - "" - - - "arn:" - - Ref: AWS::Partition - - :iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - + - !Sub "arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + Policies: + - PolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Action: + - secretsmanager:GetSecretValue + Resource: + - !Sub "${CloudOneApiKeySecret}*" + - Effect: Allow + Action: + - kms:Decrypt + Resource: + - !Ref TrendKey + PolicyName: CloudOneApiKeySecretDecryptPolicy diff --git a/templates/trend-cloudone-onboard/vision-one-generate-enrollment-token.yaml b/templates/trend-cloudone-onboard/vision-one-generate-enrollment-token.yaml index 8ee3cdb..0bf3026 100644 --- a/templates/trend-cloudone-onboard/vision-one-generate-enrollment-token.yaml +++ b/templates/trend-cloudone-onboard/vision-one-generate-enrollment-token.yaml @@ -2,8 +2,9 @@ AWSTemplateFormatVersion: 2010-09-09 Description: Trend Vision One Generate and Delete Enrollment Token. Parameters: - VisionOneAuthenticationToken: - Description: Vision One Authentication Token. See https://docs.trendmicro.com/en-us/enterprise/trend-vision-one-olh/administrative-setti/user-accounts/obtaining-api-keys-f_001.aspx + VisionOneAuthenticationTokenSecret: + Description: Arn for Vision One Authentication Token Secret. See + https://docs.trendmicro.com/en-us/enterprise/trend-vision-one-olh/administrative-setti/user-accounts/obtaining-api-keys-f_001.aspx Type: String VisionOneRegion: Description: Vision One Region. See https://automation.trendmicro.com/xdr/Guides/Regional-Domains @@ -16,6 +17,9 @@ Parameters: - "Japan" - "Singapore" - "United States" + TrendKey: + Description: KMS Key Arn used to encrypt all Trend secrets. + Type: String TrendStagingS3Bucket: AllowedPattern: ^[0-9a-zA-Z]+([0-9a-zA-Z-]*[0-9a-zA-Z])*$ ConstraintDescription: Deployment bucket name can include numbers, lowercase @@ -67,7 +71,7 @@ Resources: Role: !GetAtt VisionOneGenerateEnrollmentTokenFunctionRole.Arn Environment: Variables: - VisionOneAuthenticationToken: !Ref VisionOneAuthenticationToken + VisionOneAuthenticationTokenSecret: !Ref VisionOneAuthenticationTokenSecret VisionOneRegion: !Ref VisionOneRegion Code: S3Bucket: !Ref 'TrendStagingS3Bucket' @@ -93,6 +97,21 @@ Resources: Path: "/" ManagedPolicyArns: - !Sub "arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + Policies: + - PolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Action: + - secretsmanager:GetSecretValue + Resource: + - !Sub "${VisionOneAuthenticationTokenSecret}*" + - Effect: Allow + Action: + - kms:Decrypt + Resource: + - !Ref TrendKey + PolicyName: VisionOneGenerateEnrollmentTokenFunctionRolePolicy Outputs: VisionOneEnrollmentToken: diff --git a/templates/trend-cloudone-onboard/workloadsecurity.template.yaml b/templates/trend-cloudone-onboard/workloadsecurity.template.yaml index 34b64c1..3a149d6 100644 --- a/templates/trend-cloudone-onboard/workloadsecurity.template.yaml +++ b/templates/trend-cloudone-onboard/workloadsecurity.template.yaml @@ -2,8 +2,9 @@ AWSTemplateFormatVersion: 2010-09-09 Description: Trend Cloud One Workload Security Integration. Parameters: - CloudOneApiKey: - Description: Cloud One API Key. You can learn more about it at https://cloudone.trendmicro.com/docs/identity-and-account-management/c1-api-key/ + CloudOneApiKeySecret: + Description: Arn of Cloud One API Key Secret. You can learn more about it at + https://cloudone.trendmicro.com/docs/identity-and-account-management/c1-api-key/ Type: String NoEcho: true CloudOneRegion: @@ -20,6 +21,9 @@ Parameters: - jp-1 - ca-1 - de-1 + TrendKey: + Description: KMS Key Arn used to encrypt all Trend secrets. + Type: String TrendStagingS3Bucket: AllowedPattern: ^[0-9a-zA-Z]+([0-9a-zA-Z-]*[0-9a-zA-Z])*$ ConstraintDescription: Deployment bucket name can include numbers, lowercase @@ -95,11 +99,22 @@ Resources: - sts:AssumeRole Path: "/" ManagedPolicyArns: - - Fn::Join: - - "" - - - "arn:" - - Ref: AWS::Partition - - :iam::aws:policy/service-role/AWSLambdaBasicExecutionRole + - !Sub "arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + Policies: + - PolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Action: + - secretsmanager:GetSecretValue + Resource: + - !Sub "${CloudOneApiKeySecret}*" + - Effect: Allow + Action: + - kms:Decrypt + Resource: + - !Ref TrendKey + PolicyName: CloudOneApiKeySecretDecryptPolicy #Get External ID GetExternalIDLambda: @@ -137,7 +152,7 @@ Resources: Type: AWS::CloudFormation::CustomResource Properties: ServiceToken: !GetAtt GetExternalIDLambda.Arn - CloudOneApiKey: !Ref CloudOneApiKey + CloudOneApiKeySecret: !Ref CloudOneApiKeySecret CloudOneRegion: !Ref CloudOneRegion @@ -177,14 +192,14 @@ Resources: crossaccountrolearn: !GetAtt WorkloadSecurityRole.Arn Code: S3Bucket: !Ref 'TrendStagingS3Bucket' - S3Key: !Sub ${QSS3KeyPrefix}/lambda_functions/packages/GetExternalIDLambda/lambda.zip + S3Key: !Sub ${QSS3KeyPrefix}/lambda_functions/packages/AWSConnectorCreateLambda/lambda.zip #Create AWS Connector Custom Resource AWSConnectorCreate: Type: AWS::CloudFormation::CustomResource Properties: ServiceToken: !GetAtt AWSConnectorCreateLambda.Arn - CloudOneApiKey: !Ref CloudOneApiKey + CloudOneApiKeySecret: !Ref CloudOneApiKeySecret CloudOneRegion: !Ref CloudOneRegion DependsOn: GetExternalID From 243603c1444d516b63b4cabd6dcd29004e7d7c23 Mon Sep 17 00:00:00 2001 From: Raphael Bottino Date: Tue, 2 May 2023 13:23:22 -0500 Subject: [PATCH 25/25] Parameterizing Trend's AWS account id. --- .../trend-cloudone-onboard/workloadsecurity.template.yaml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/templates/trend-cloudone-onboard/workloadsecurity.template.yaml b/templates/trend-cloudone-onboard/workloadsecurity.template.yaml index 3a149d6..f4745e9 100644 --- a/templates/trend-cloudone-onboard/workloadsecurity.template.yaml +++ b/templates/trend-cloudone-onboard/workloadsecurity.template.yaml @@ -24,6 +24,11 @@ Parameters: TrendKey: Description: KMS Key Arn used to encrypt all Trend secrets. Type: String + TrendWorkloadSecurityAWSAccountId: + Description: AWS Account ID for the AWS account where Trend Workload Security is hosted. + Most likely, you don't need to edit this parameter's default. + Type: String + Default: "147995105371" TrendStagingS3Bucket: AllowedPattern: ^[0-9a-zA-Z]+([0-9a-zA-Z-]*[0-9a-zA-Z])*$ ConstraintDescription: Deployment bucket name can include numbers, lowercase @@ -53,7 +58,7 @@ Resources: Statement: - Effect: Allow Principal: - AWS: arn:aws:iam::147995105371:root + AWS: !Sub "arn:${AWS::Partition}:iam::${TrendWorkloadSecurityAWSAccountId}:root" Action: - 'sts:AssumeRole' Condition: