constructor()

in source/solution_deploy/lib/solution_deploy-stack.ts [50:770]


  constructor(scope: cdk.App, id: string, props: SHARRStackProps) {
    super(scope, id, props);
    const stack = cdk.Stack.of(this);
    const RESOURCE_PREFIX = props.solutionId.replace(/^DEV-/,''); // prefix on every resource name

    //-------------------------------------------------------------------------
    // Solutions Bucket - Source Code
    //
    const SolutionsBucket = s3.Bucket.fromBucketAttributes(this, 'SolutionsBucket', {
        bucketName: props.solutionDistBucket + '-' + this.region
    });

    //=========================================================================
    // MAPPINGS
    //=========================================================================
    new cdk.CfnMapping(this, 'SourceCode', {
        mapping: { "General": { 
            "S3Bucket": props.solutionDistBucket,
            "KeyPrefix": props.solutionTMN + '/' + props.solutionVersion
        } }
    })

    //-------------------------------------------------------------------------
    // KMS Key for solution encryption
    //

    // Key Policy
    const kmsKeyPolicy:PolicyDocument = new PolicyDocument()
    
    const kmsServicePolicy = new PolicyStatement({
        principals: [
            new ServicePrincipal('sns.amazonaws.com'),
            new ServicePrincipal(`logs.${this.urlSuffix}`)
        ],
        actions: [
            "kms:Encrypt*",
            "kms:Decrypt*",
            "kms:ReEncrypt*",
            "kms:GenerateDataKey*",
            "kms:Describe*"
        ],
        resources: [
            '*'
        ],
        conditions: {
            ArnEquals: {
                "kms:EncryptionContext:aws:logs:arn": this.formatArn({
                    service: 'logs',
                    resource: 'log-group:SO0111-SHARR-*' 
                })
            }
        }
    })
    kmsKeyPolicy.addStatements(kmsServicePolicy)

    const kmsRootPolicy = new PolicyStatement({
        principals: [
            new AccountRootPrincipal()
        ],
        actions: [
            'kms:*'
        ],
        resources: [
            '*'
        ]
    })
    kmsKeyPolicy.addStatements(kmsRootPolicy)

    const kmsKey = new kms.Key(this, 'SHARR-key', {
        enableKeyRotation: true,
        alias: `${RESOURCE_PREFIX}-SHARR-Key`,
        trustAccountIdentities: true,
        policy: kmsKeyPolicy
    });

    const kmsKeyParm = new StringParameter(this, 'SHARR_Key', {
        description: 'KMS Customer Managed Key that SHARR will use to encrypt data',
        parameterName: `/Solutions/${RESOURCE_PREFIX}/CMK_ARN`,
        stringValue: kmsKey.keyArn
    });

    //-------------------------------------------------------------------------
    // SNS Topic for notification fanout on Playbook completion
    //
    const snsTopic = new sns.Topic(this, 'SHARR-Topic', {
        displayName: 'SHARR Playbook Topic (' + RESOURCE_PREFIX + ')',
        topicName: RESOURCE_PREFIX + '-SHARR_Topic',
        masterKey: kmsKey
    });

    new StringParameter(this, 'SHARR_SNS_Topic', {
        description: 'SNS Topic ARN where SHARR will send status messages. This\
        topic can be useful for driving additional actions, such as email notifications,\
        trouble ticket updates.',
        parameterName: '/Solutions/' + RESOURCE_PREFIX + '/SNS_Topic_ARN',
        stringValue: snsTopic.topicArn
    });

    const mapping = new cdk.CfnMapping(this, 'mappings');
    mapping.setValue("sendAnonymousMetrics", "data", this.SEND_ANONYMOUS_DATA)

	new StringParameter(this, 'SHARR_SendAnonymousMetrics', {
		description: 'Flag to enable or disable sending anonymous metrics.',
		parameterName: '/Solutions/' + RESOURCE_PREFIX + '/sendAnonymousMetrics',
		stringValue: mapping.findInMap("sendAnonymousMetrics", "data")
	});

    new StringParameter(this, 'SHARR_version', {
        description: 'Solution version for metrics.',
        parameterName: '/Solutions/' + RESOURCE_PREFIX + '/version',
        stringValue: props.solutionVersion
    });

    /**
     * @description Lambda Layer for common solution functions
     * @type {lambda.LayerVersion}
     */
    const sharrLambdaLayer = new lambda.LayerVersion(this, 'SharrLambdaLayer', {
        compatibleRuntimes: [
            lambda.Runtime.PYTHON_3_6,
            lambda.Runtime.PYTHON_3_7,
            lambda.Runtime.PYTHON_3_8
        ],
        description: 'SO0111 SHARR Common functions used by the solution',
        license: "https://www.apache.org/licenses/LICENSE-2.0",
        code: lambda.Code.fromBucket(
            SolutionsBucket,
            props.solutionTMN + '/' + props.solutionVersion + '/lambda/layer.zip'
        ), 
    });

    /**
     * @description Policy for role used by common Orchestrator Lambdas
     * @type {Policy}
     */
    const orchestratorPolicy = new Policy(this, 'orchestratorPolicy', {
        policyName: RESOURCE_PREFIX + '-SHARR_Orchestrator',
        statements: [
            new PolicyStatement({
                actions: [
                    'logs:CreateLogGroup',
                    'logs:CreateLogStream',
                    'logs:PutLogEvents'
                ],
                resources: ['*']
            }),
            new PolicyStatement({
                actions: [
                    'ssm:GetParameter',
                    'ssm:GetParameters',
                    'ssm:PutParameter'
                ],
                resources: [`arn:${this.partition}:ssm:*:${this.account}:parameter/Solutions/SO0111/*`]
            }),
            new PolicyStatement({
                 actions: [
                    'sts:AssumeRole'
                ],
                resources: [
                    `arn:${this.partition}:iam::*:role/${RESOURCE_PREFIX}-SHARR-Orchestrator-Member`,
                    'arn:' + this.partition + ':iam::*:role/' + RESOURCE_PREFIX +
                        '-Remediate-*', 
                ]
            })
        ]
    })

    {
        let childToMod = orchestratorPolicy.node.findChild('Resource') as CfnPolicy;
        childToMod.cfnOptions.metadata = {
            cfn_nag: {
                rules_to_suppress: [{
                    id: 'W12',
                    reason: 'Resource * is required for read-only policies used by orchestrator Lambda functions.'
                }]
            }
        }
    }

    /**
     * @description Role used by common Orchestrator Lambdas
     * @type {Role}
     */

    const orchestratorRole = new Role(this, 'orchestratorRole', {
        assumedBy: new ServicePrincipal('lambda.amazonaws.com'),
        description: 'Lambda role to allow cross account read-only SHARR orchestrator functions',
        roleName: `${RESOURCE_PREFIX}-SHARR-Orchestrator-Admin`
    });

    orchestratorRole.attachInlinePolicy(orchestratorPolicy);

    {
        let childToMod = orchestratorRole.node.findChild('Resource') as CfnRole;
        childToMod.cfnOptions.metadata = {
            cfn_nag: {
                rules_to_suppress: [{
                    id: 'W28',
                    reason: 'Static names chosen intentionally to provide easy integration with playbook orchestrator step functions.'
                }]
            }
        }
    }

    /**
     * @description checkSSMDocState - get the status of an ssm document
     * @type {lambda.Function}
     */
    const checkSSMDocState = new lambda.Function(this, 'checkSSMDocState', {
        functionName: RESOURCE_PREFIX + '-SHARR-checkSSMDocState',
        handler: 'check_ssm_doc_state.lambda_handler',
        runtime: props.runtimePython,
        description: 'Checks the status of an SSM Automation Document in the target account',
        code: lambda.Code.fromBucket(
            SolutionsBucket,
            props.solutionTMN + '/' + props.solutionVersion + '/lambda/check_ssm_doc_state.py.zip'
        ),
        environment: {
            log_level: 'info',
            AWS_PARTITION: this.partition,
            SOLUTION_ID: props.solutionId,
            SOLUTION_VERSION: props.solutionVersion
        },
        memorySize: 256,
        timeout: cdk.Duration.seconds(600),
        role: orchestratorRole,
        layers: [sharrLambdaLayer]
    });

    {
        const childToMod = checkSSMDocState.node.findChild('Resource') as lambda.CfnFunction;

        childToMod.cfnOptions.metadata = {
            cfn_nag: {
                rules_to_suppress: [
                {
                    id: 'W58',
                    reason: 'False positive. Access is provided via a policy'
                },
                {
                    id: 'W89',
                    reason: 'There is no need to run this lambda in a VPC'
                },
                {
                    id: 'W92',
                    reason: 'There is no need for Reserved Concurrency'
                }
                ]
            }
        };
    }

    /**
     * @description getApprovalRequirement - determine whether manual approval is required
     * @type {lambda.Function}
     */
         const getApprovalRequirement = new lambda.Function(this, 'getApprovalRequirement', {
            functionName: RESOURCE_PREFIX + '-SHARR-getApprovalRequirement',
            handler: 'get_approval_requirement.lambda_handler',
            runtime: props.runtimePython,
            description: 'Determines if a manual approval is required for remediation',
            code: lambda.Code.fromBucket(
                SolutionsBucket,
                props.solutionTMN + '/' + props.solutionVersion + '/lambda/get_approval_requirement.py.zip'
            ),
            environment: {
                log_level: 'info',
                AWS_PARTITION: this.partition,
                SOLUTION_ID: props.solutionId,
                SOLUTION_VERSION: props.solutionVersion,
                WORKFLOW_RUNBOOK: ''
            },
            memorySize: 256,
            timeout: cdk.Duration.seconds(600),
            role: orchestratorRole,
            layers: [sharrLambdaLayer]
        });
    
        {
            const childToMod = getApprovalRequirement.node.findChild('Resource') as lambda.CfnFunction;
    
            childToMod.cfnOptions.metadata = {
                cfn_nag: {
                    rules_to_suppress: [{
                        id: 'W58',
                        reason: 'False positive. Access is provided via a policy'
                    },{
                        id: 'W89',
                        reason: 'There is no need to run this lambda in a VPC'
                    },
                    {
                        id: 'W92',
                        reason: 'There is no need for Reserved Concurrency'
                    }]
                }
            };
        }
    

    /**
     * @description execAutomation - initiate an SSM automation document in a target account
     * @type {lambda.Function}
     */
    const execAutomation = new lambda.Function(this, 'execAutomation', {
        functionName: RESOURCE_PREFIX + '-SHARR-execAutomation',
        handler: 'exec_ssm_doc.lambda_handler',
        runtime: props.runtimePython,
        description: 'Executes an SSM Automation Document in a target account',
        code: lambda.Code.fromBucket(
            SolutionsBucket,
            props.solutionTMN + '/' + props.solutionVersion + '/lambda/exec_ssm_doc.py.zip'
        ),
        environment: {
            log_level: 'info',
            AWS_PARTITION: this.partition,
            SOLUTION_ID: props.solutionId,
            SOLUTION_VERSION: props.solutionVersion
        },
        memorySize: 256,
        timeout: cdk.Duration.seconds(600),
        role: orchestratorRole,
        layers: [sharrLambdaLayer]
    });

    {
        const childToMod = execAutomation.node.findChild('Resource') as lambda.CfnFunction;

        childToMod.cfnOptions.metadata = {
            cfn_nag: {
                rules_to_suppress: [{
                    id: 'W58',
                    reason: 'False positive. Access is provided via a policy'
                },{
                    id: 'W89',
                    reason: 'There is no need to run this lambda in a VPC'
                },
                {
                    id: 'W92',
                    reason: 'There is no need for Reserved Concurrency'
                }]
            }
        };
    }

    /**
     * @description monitorSSMExecState - get the status of an ssm execution
     * @type {lambda.Function}
     */
    const monitorSSMExecState = new lambda.Function(this, 'monitorSSMExecState', {
        functionName: RESOURCE_PREFIX + '-SHARR-monitorSSMExecState',
        handler: 'check_ssm_execution.lambda_handler',
        runtime: props.runtimePython,
        description: 'Checks the status of an SSM automation document execution',
        code: lambda.Code.fromBucket(
            SolutionsBucket,
            props.solutionTMN + '/' + props.solutionVersion + '/lambda/check_ssm_execution.py.zip'
        ),
        environment: {
            log_level: 'info',
            AWS_PARTITION: this.partition,
            SOLUTION_ID: props.solutionId,
            SOLUTION_VERSION: props.solutionVersion
        },
        memorySize: 256,
        timeout: cdk.Duration.seconds(600),
        role: orchestratorRole,
        layers: [sharrLambdaLayer]
    });

    {
        const childToMod = monitorSSMExecState.node.findChild('Resource') as lambda.CfnFunction;

        childToMod.cfnOptions.metadata = {
            cfn_nag: {
                rules_to_suppress: [{
                    id: 'W58',
                    reason: 'False positive. Access is provided via a policy'
                },{
                    id: 'W89',
                    reason: 'There is no need to run this lambda in a VPC'
                },
                {
                    id: 'W92',
                    reason: 'There is no need for Reserved Concurrency'
                }]
            }
        };
    }

    /**
     * @description Policy for role used by common Orchestrator notification lambda
     * @type {Policy}
     */
    const notifyPolicy = new Policy(this, 'notifyPolicy', {
        policyName: RESOURCE_PREFIX + '-SHARR_Orchestrator_Notifier',
        statements: [
            new PolicyStatement({
                actions: [
                    'logs:CreateLogGroup',
                    'logs:CreateLogStream',
                    'logs:PutLogEvents'
                ],
                resources: ['*']
            }),
            new PolicyStatement({
                actions: [
                    'securityhub:BatchUpdateFindings'
                ],
                resources: ['*']
            }),
            new PolicyStatement({
                actions: [
                    'ssm:GetParameter',
                    'ssm:PutParameter'
                ],
                resources: [`arn:${this.partition}:ssm:${this.region}:${this.account}:parameter/Solutions/SO0111/*`]
            }),
            new PolicyStatement({
                actions: [
                    'kms:Encrypt',
                    'kms:Decrypt',
                    'kms:GenerateDataKey',
                ],
                resources: [kmsKey.keyArn]
            }),
            new PolicyStatement({
                actions: [
                    'sns:Publish'
                ],
                resources: [ 
                    `arn:${this.partition}:sns:${this.region}:${this.account}:${RESOURCE_PREFIX}-SHARR_Topic`
                ]
            })
        ]
    })

    {
        let childToMod = notifyPolicy.node.findChild('Resource') as CfnPolicy;
        childToMod.cfnOptions.metadata = {
            cfn_nag: {
                rules_to_suppress: [{
                    id: 'W12',
                    reason: 'Resource * is required for CloudWatch Logs and Security Hub policies used by core solution Lambda function for notifications.'
                },{
                    id: 'W58',
                    reason: 'False positive. Access is provided via a policy'
                }]
            }
        }
    }

    notifyPolicy.attachToRole(orchestratorRole) // Any Orchestrator Lambda can send to sns
    
    /**
     * @description Role used by common Orchestrator Lambdas
     * @type {Role}
     */

    const notifyRole = new Role(this, 'notifyRole', {
        assumedBy: new ServicePrincipal('lambda.amazonaws.com'),
        description: 'Lambda role to perform notification and logging from orchestrator step function'
    });

    notifyRole.attachInlinePolicy(notifyPolicy);

    {
        let childToMod = notifyRole.node.findChild('Resource') as CfnRole;
        childToMod.cfnOptions.metadata = {
            cfn_nag: {
                rules_to_suppress: [{
                    id: 'W28',
                    reason: 'Static names chosen intentionally to provide easy integration with playbook orchestrator step functions.'
                }]
            }
        }
    }

    /**
     * @description sendNotifications - send notifications and log messages from Orchestrator step function
     * @type {lambda.Function}
     */
    const sendNotifications = new lambda.Function(this, 'sendNotifications', {
        functionName: RESOURCE_PREFIX + '-SHARR-sendNotifications',
        handler: 'send_notifications.lambda_handler',
        runtime: props.runtimePython,
        description: 'Sends notifications and log messages',
        code: lambda.Code.fromBucket(
            SolutionsBucket,
            props.solutionTMN + '/' + props.solutionVersion + '/lambda/send_notifications.py.zip'
        ),
        environment: {
            log_level: 'info',
            AWS_PARTITION: this.partition,
            SOLUTION_ID: props.solutionId,
            SOLUTION_VERSION: props.solutionVersion
        },
        memorySize: 256,
        timeout: cdk.Duration.seconds(600),
        role: notifyRole,
        layers: [sharrLambdaLayer]
    });

    {
        const childToMod = sendNotifications.node.findChild('Resource') as lambda.CfnFunction;

        childToMod.cfnOptions.metadata = {
            cfn_nag: {
                rules_to_suppress: [{
                    id: 'W58',
                    reason: 'False positive. Access is provided via a policy'
                },{
                    id: 'W89',
                    reason: 'There is no need to run this lambda in a VPC'
                },
                {
                    id: 'W92',
                    reason: 'There is no need for Reserved Concurrency due to low request rate'
                }]
            }
        };
    }

    //-------------------------------------------------------------------------
    // Custom Lambda Policy
    //
    const createCustomActionPolicy = new Policy(this, 'createCustomActionPolicy', {
        policyName: RESOURCE_PREFIX + '-SHARR_Custom_Action',
        statements: [
            new PolicyStatement({
                actions: [
                    'cloudwatch:PutMetricData'
                ],
                resources: ['*']
            }),
            new PolicyStatement({
                actions: [
                    'logs:CreateLogGroup',
                    'logs:CreateLogStream',
                    'logs:PutLogEvents'
                ],
                resources: ['*']
            }),
            new PolicyStatement({
                actions: [
                    'securityhub:CreateActionTarget',
                    'securityhub:DeleteActionTarget'
                ],
                resources: ['*']
            }),
            new PolicyStatement({
                actions: [
                    'ssm:GetParameter',
                    'ssm:GetParameters',
                    'ssm:PutParameter'
                ],
                resources: [`arn:${this.partition}:ssm:*:${this.account}:parameter/Solutions/SO0111/*`]
            }),
        ]
    })

    const createCAPolicyResource = createCustomActionPolicy.node.findChild('Resource') as CfnPolicy;

    createCAPolicyResource.cfnOptions.metadata = {
        cfn_nag: {
            rules_to_suppress: [{
                id: 'W12',
                reason: 'Resource * is required for CloudWatch Logs policies used on Lambda functions.'
            }]
        }
    };

    //-------------------------------------------------------------------------
    // Custom Lambda Role
    //
    const createCustomActionRole = new Role(this, 'createCustomActionRole', {
        assumedBy: new ServicePrincipal('lambda.amazonaws.com'),
        description: 'Lambda role to allow creation of Security Hub Custom Actions'
    });

    createCustomActionRole.attachInlinePolicy(createCustomActionPolicy);

    const createCARoleResource = createCustomActionRole.node.findChild('Resource') as CfnRole;

    createCARoleResource.cfnOptions.metadata = {
        cfn_nag: {
            rules_to_suppress: [{
                id: 'W28',
                reason: 'Static names chosen intentionally to provide easy integration with playbook templates'
            }]
        }
    };

    //-------------------------------------------------------------------------
    // Custom Lambda - Create Custom Action
    //
    const createCustomAction = new lambda.Function(this, 'CreateCustomAction', {
        functionName: RESOURCE_PREFIX + '-SHARR-CustomAction',
        handler: 'createCustomAction.lambda_handler',
        runtime: props.runtimePython,
        description: 'Custom resource to create an action target in Security Hub',
        code: lambda.Code.fromBucket(
            SolutionsBucket,
            props.solutionTMN + '/' + props.solutionVersion + '/lambda/createCustomAction.py.zip'
        ),
        environment: {
            log_level: 'info',
            AWS_PARTITION: this.partition,
            sendAnonymousMetrics: mapping.findInMap("sendAnonymousMetrics", "data"),
            SOLUTION_ID: props.solutionId,
            SOLUTION_VERSION: props.solutionVersion
        },
        memorySize: 256,
        timeout: cdk.Duration.seconds(600),
        role: createCustomActionRole,
        layers: [sharrLambdaLayer]
    });

    const createCAFuncResource = createCustomAction.node.findChild('Resource') as lambda.CfnFunction;

    createCAFuncResource.cfnOptions.metadata = {
        cfn_nag: {
            rules_to_suppress: [
            {
                id: 'W58',
                reason: 'False positive. the lambda role allows write to CW Logs'
            },
            {
                id: 'W89',
                reason: 'There is no need to run this lambda in a VPC'
            },
            {
                id: 'W92',
                reason: 'There is no need for Reserved Concurrency due to low request rate'
            }]
        }
    };

    const orchestrator = new OrchestratorConstruct(this, "orchestrator", {
        roleArn: orchestratorRole.roleArn,
        ssmDocStateLambda: checkSSMDocState.functionArn,
        ssmExecDocLambda: execAutomation.functionArn,
        ssmExecMonitorLambda: monitorSSMExecState.functionArn,
        notifyLambda: sendNotifications.functionArn,
        getApprovalRequirementLambda: getApprovalRequirement.functionArn,
        solutionId: RESOURCE_PREFIX,
        solutionName: props.solutionName,
        solutionVersion: props.solutionVersion,
        orchLogGroup: props.orchLogGroup,
        kmsKeyParm: kmsKeyParm
    })

    let orchStateMachine = orchestrator.node.findChild('StateMachine') as StateMachine
    let stateMachineConstruct = orchStateMachine.node.defaultChild as CfnStateMachine
    let orchArnParm = orchestrator.node.findChild('SHARR_Orchestrator_Arn') as StringParameter
    let orchestratorArn = orchArnParm.node.defaultChild as CfnParameter

    //---------------------------------------------------------------------
    // OneTrigger - Remediate with SHARR custom action
    //
    new OneTrigger(this, 'RemediateWithSharr', {
        targetArn: orchStateMachine.stateMachineArn,
        serviceToken: createCustomAction.functionArn,
        prereq: [
            createCAFuncResource,
            createCAPolicyResource
        ]
    })

    //-------------------------------------------------------------------------
    // Loop through all of the Playbooks and create an option to load each
    //
    const PB_DIR = `${__dirname}/../../playbooks`
    var ignore = ['.DS_Store', 'core', 'python_lib', 'python_tests', '.pytest_cache', 'NEWPLAYBOOK', '.coverage'];
    let illegalChars = /[\._]/g;

    var standardLogicalNames: string[] = []

    fs.readdir(PB_DIR, (err, items) => {
        items.forEach(file => {
            if (!ignore.includes(file)) {
                var template_file = `${file}Stack.template`

                //---------------------------------------------------------------------
                // Playbook Admin Template Nested Stack
                //
                let parmname = file.replace(illegalChars, '')
                let adminStackOption = new cdk.CfnParameter(this, `LoadAdminStack${parmname}`, {
                    type: "String",
                    description: `Load CloudWatch Event Rules for ${file}?`,
                    default: "yes",
                    allowedValues: ["yes", "no"],
                })
                adminStackOption.overrideLogicalId(`Load${parmname}AdminStack`)
                standardLogicalNames.push(`Load${parmname}AdminStack`)

                let adminStack = new cdk.CfnStack(this, `PlaybookAdminStack${file}`, {
                    templateUrl: "https://" + cdk.Fn.findInMap("SourceCode", "General", "S3Bucket") +
                    "-reference.s3.amazonaws.com/" + cdk.Fn.findInMap("SourceCode", "General", "KeyPrefix") +
                    "/playbooks/" + template_file
                })
                adminStack.addDependsOn(stateMachineConstruct)
                adminStack.addDependsOn(orchestratorArn)

                adminStack.cfnOptions.condition = new cdk.CfnCondition(this, `load${file}Cond`, {
                    expression: 
                        cdk.Fn.conditionEquals(adminStackOption, "yes")
                });
            }
        });
    })
    stack.templateOptions.metadata = {
        "AWS::CloudFormation::Interface": {
            ParameterGroups: [
                {
                    Label: {default: "Security Standard Playbooks"},
                    Parameters: standardLogicalNames
                }
            ]
        },
    };
  }