packages/constructs/L2/custom-constructs/lib/custom.ts (259 lines of code) (raw):

/*! * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: Apache-2.0 */ import { MdaaConstructProps, MdaaNagSuppressions } from '@aws-mdaa/construct'; //NOSONAR import { MdaaLambdaFunction, MdaaLambdaRole } from '@aws-mdaa/lambda-constructs'; import { CustomResource, CustomResourceProps, Duration, Stack } from 'aws-cdk-lib'; import { Policy, PolicyDocument, PolicyStatement, Role } from 'aws-cdk-lib/aws-iam'; import { Code, ILayerVersion, Runtime } from 'aws-cdk-lib/aws-lambda'; import { Provider } from 'aws-cdk-lib/custom-resources'; import { Construct } from 'constructs'; import { ISecurityGroup, IVpc, SubnetSelection } from 'aws-cdk-lib/aws-ec2'; import { ConfigurationElement } from '@aws-mdaa/config'; import { NagPackSuppression } from 'cdk-nag'; // nosemgrep const _ = require('lodash'); export interface MdaaCustomResourceProps extends MdaaConstructProps { readonly resourceType: string; readonly code: Code; readonly runtime: Runtime; readonly handler: string; readonly handlerRolePolicyStatements: PolicyStatement[]; readonly handlerPolicySuppressions?: NagPackSuppression[]; readonly handlerProps: ConfigurationElement; readonly handlerLayers?: ILayerVersion[]; readonly pascalCaseProperties?: boolean; readonly handlerTimeout?: Duration; readonly vpc?: IVpc; readonly subnet?: SubnetSelection; readonly securityGroup?: ISecurityGroup; readonly environment?: { [key: string]: string; }; } export class MdaaCustomResource extends CustomResource { public handlerFunction: MdaaLambdaFunction; protected static handlerFunctionPlaceHolder: MdaaLambdaFunction; private static setProps(scope: Construct, props: MdaaCustomResourceProps) { const stack = Stack.of(scope); const handlerFunctionName = props.naming.resourceName(`${props.resourceType}-handler`, 64); const handlerRoleResourceId = `custom-${props.resourceType}-handler-role`; const existingHandlerRole = stack.node.tryFindChild(handlerRoleResourceId) as Role; const handlerRole = existingHandlerRole ? existingHandlerRole : new MdaaLambdaRole(stack, handlerRoleResourceId, { roleName: `${props.resourceType}-handler`, naming: props.naming, logGroupNames: [handlerFunctionName], createParams: false, createOutputs: false, }); const handlerPolicyResourceId = `custom-${props.resourceType}-handler-policy`; const existingPolicy = stack.node.tryFindChild(handlerPolicyResourceId) as Policy; const handlerPolicy = existingPolicy ? existingPolicy : new Policy(stack, handlerPolicyResourceId, { policyName: `${props.resourceType}-handler`, document: new PolicyDocument({ statements: props.handlerRolePolicyStatements }), }); if (existingPolicy) { handlerPolicy.addStatements(...props.handlerRolePolicyStatements); } else { handlerRole.attachInlinePolicy(handlerPolicy); MdaaNagSuppressions.addCodeResourceSuppressions( handlerPolicy, [ { id: 'NIST.800.53.R5-IAMNoInlinePolicy', reason: 'Function is for custom resource; inline policy use appropriate', }, { id: 'HIPAA.Security-IAMNoInlinePolicy', reason: 'Function is for custom resource; inline policy use appropriate', }, { id: 'PCI.DSS.321-IAMNoInlinePolicy', reason: 'Function is for custom resource; inline policy use appropriate', }, ...(props.handlerPolicySuppressions || []), ], true, ); } const handlerFunctionResourceId = `custom-${props.resourceType}-handler-function`; const existingHandlerFunction = stack.node.tryFindChild(handlerFunctionResourceId) as MdaaLambdaFunction; this.handlerFunctionPlaceHolder = existingHandlerFunction ? existingHandlerFunction : new MdaaLambdaFunction(stack, handlerFunctionResourceId, { naming: props.naming, runtime: props.runtime, code: props.code, handler: props.handler, role: handlerRole, functionName: `${props.resourceType}-handler`, layers: props.handlerLayers, timeout: props.handlerTimeout ? props.handlerTimeout : Duration.seconds(60), vpc: props.vpc, vpcSubnets: props.subnet, securityGroups: props.securityGroup ? [props.securityGroup] : undefined, environment: props.environment }); this.handlerFunctionPlaceHolder.node.addDependency(handlerPolicy); MdaaNagSuppressions.addCodeResourceSuppressions( this.handlerFunctionPlaceHolder, [ { id: 'NIST.800.53.R5-LambdaDLQ', reason: 'Function is for custom resource and error handling will be handled by CloudFormation.', }, { id: 'NIST.800.53.R5-LambdaInsideVPC', reason: 'Function is for custom resource.' }, { id: 'NIST.800.53.R5-LambdaConcurrency', reason: 'Function is for custom resource and will only execute during stack deployement. Reserved concurrency not appropriate.', }, { id: 'HIPAA.Security-LambdaDLQ', reason: 'Function is for custom resource and error handling will be handled by CloudFormation.', }, { id: 'PCI.DSS.321-LambdaDLQ', reason: 'Function is for custom resource and error handling will be handled by CloudFormation.', }, { id: 'HIPAA.Security-LambdaInsideVPC', reason: 'Function is for custom resource.' }, { id: 'PCI.DSS.321-LambdaInsideVPC', reason: 'Function is for custom resource.' }, { id: 'HIPAA.Security-LambdaConcurrency', reason: 'Function is for custom resource and will only execute during stack deployement. Reserved concurrency not appropriate.', }, { id: 'PCI.DSS.321-LambdaConcurrency', reason: 'Function is for custom resource and will only execute during stack deployement. Reserved concurrency not appropriate.', }, ], true, ); const providerFunctionName = props.naming.resourceName(`${props.resourceType}-provider`, 64); const providerRoleResourceId = `custom-${props.resourceType}-provider-role`; const existingProviderRole = stack.node.tryFindChild(providerRoleResourceId) as Role; const providerRole = existingProviderRole ? existingProviderRole : new MdaaLambdaRole(stack, providerRoleResourceId, { description: 'CR Role', roleName: `${props.resourceType}-provider`, naming: props.naming, logGroupNames: [providerFunctionName], createParams: false, createOutputs: false, }); MdaaNagSuppressions.addCodeResourceSuppressions( providerRole, [ { id: 'NIST.800.53.R5-IAMNoInlinePolicy', reason: 'Role is for Custom Resource Provider. Inline policy automatically added.', }, { id: 'HIPAA.Security-IAMNoInlinePolicy', reason: 'Role is for Custom Resource Provider. Inline policy automatically added.', }, { id: 'PCI.DSS.321-IAMNoInlinePolicy', reason: 'Role is for Custom Resource Provider. Inline policy automatically added.', }, ], true, ); const providerResourceId = `custom-${props.resourceType}-provider`; const existingProvider = stack.node.tryFindChild(providerResourceId) as Provider; const provider = existingProvider ? existingProvider : new Provider(stack, providerResourceId, { onEventHandler: this.handlerFunctionPlaceHolder, role: providerRole, providerFunctionName: providerFunctionName, }); MdaaNagSuppressions.addCodeResourceSuppressions( provider, [ { id: 'AwsSolutions-L1', reason: 'Lambda function Runtime set by CDK Provider Framework', }, { id: 'NIST.800.53.R5-LambdaDLQ', reason: 'Function is for custom resource and error handling will be handled by CloudFormation.', }, { id: 'NIST.800.53.R5-LambdaInsideVPC', reason: 'Function is for custom resource and will interact only with QuickSight APIs.', }, { id: 'NIST.800.53.R5-LambdaConcurrency', reason: 'Function is for custom resource and will only execute during stack deployement. Reserved concurrency not appropriate.', }, { id: 'HIPAA.Security-LambdaDLQ', reason: 'Function is for custom resource and error handling will be handled by CloudFormation.', }, { id: 'HIPAA.Security-LambdaInsideVPC', reason: 'Function is for custom resource and will interact only with QuickSight APIs.', }, { id: 'HIPAA.Security-LambdaConcurrency', reason: 'Function is for custom resource and will only execute during stack deployement. Reserved concurrency not appropriate.', }, { id: 'PCI.DSS.321-LambdaDLQ', reason: 'Function is for custom resource and error handling will be handled by CloudFormation.', }, { id: 'PCI.DSS.321-LambdaInsideVPC', reason: 'Function is for custom resource and will interact only with QuickSight APIs.', }, { id: 'PCI.DSS.321-LambdaConcurrency', reason: 'Function is for custom resource and will only execute during stack deployement. Reserved concurrency not appropriate.', }, ], true, ); const crProps: CustomResourceProps = { resourceType: `Custom::${props.resourceType}`, serviceToken: provider.serviceToken, properties: props.pascalCaseProperties ? (MdaaCustomResource.pascalCase(props.handlerProps) as ConfigurationElement) : props.handlerProps, }; return crProps; } constructor(scope: Construct, id: string, props: MdaaCustomResourceProps) { super(scope, id, MdaaCustomResource.setProps(scope, props)); this.handlerFunction = MdaaCustomResource.handlerFunctionPlaceHolder; } public static pascalCase<T>(props: T): unknown { return _.transform(props, MdaaCustomResource.transformUpperCaseObj, {}); } private static upcaseFirst(str: string): string { if (str === '') { return str; } return `${str[0].toLocaleUpperCase()}${str.slice(1)}`; } private static transformUpperCaseObj(result: ConfigurationElement, value: unknown, key: string) { const newKey = MdaaCustomResource.upcaseFirst(key); if (typeof value === 'string' || value instanceof String) result[newKey] = value; else if (value instanceof Array) result[newKey] = MdaaCustomResource.transformUpperCaseObjArray(value); else if (value instanceof Object) { result[newKey] = _.transform(value, MdaaCustomResource.transformUpperCaseObj, {}); } else result[newKey] = value; } private static transformUpperCaseObjArray(values: unknown[]): unknown { return values.map(value => { if (typeof value === 'string' || value instanceof String) return value; else if (value instanceof Array) return this.transformUpperCaseObjArray(value); else if (value instanceof Object) return _.transform(value, MdaaCustomResource.transformUpperCaseObj, {}); else return value; }); } }