packages/apps/core/app/lib/app.ts (406 lines of code) (raw):

/*! * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: Apache-2.0 */ import { ConfigConfigPathValueTransformer, ConfigurationElement, MdaaConfigTransformer, MdaaCustomAspect, TagElement, } from '@aws-mdaa/config'; import { MdaaL3ConstructProps } from '@aws-mdaa/l3-construct'; import { MdaaLambdaFunction, MdaaLambdaFunctionProps, MdaaLambdaRole } from '@aws-mdaa/lambda-constructs'; import { IMdaaResourceNaming, MdaaDefaultResourceNaming } from '@aws-mdaa/naming'; import { App, AppProps, Aspects, CfnMacro, Stack, Tags } from 'aws-cdk-lib'; import { Code, Runtime } from 'aws-cdk-lib/aws-lambda'; import { StringParameter } from 'aws-cdk-lib/aws-ssm'; import { CfnLaunchRoleConstraint, CfnLaunchRoleConstraintProps, CloudFormationProduct, CloudFormationProductProps, CloudFormationTemplate, Portfolio, } from 'aws-cdk-lib/aws-servicecatalog'; import { AwsSolutionsChecks, HIPAASecurityChecks, NIST80053R5Checks, PCIDSS321Checks } from 'cdk-nag'; import * as fs from 'fs'; import * as path from 'path'; import * as yaml from 'yaml'; import { MdaaAppConfigParser, MdaaAppConfigParserProps, MdaaBaseConfigContents } from './app_config'; import * as configSchema from './config-schema.json'; import { MdaaProductStack, MdaaProductStackProps, MdaaStack } from './stack'; import { MdaaNagSuppressions } from '@aws-mdaa/construct'; //NOSONAR // nosemgrep import assert = require('assert'); const pjson = require('../package.json'); export interface MdaaAppProps extends AppProps { readonly appConfigRaw?: ConfigurationElement; readonly useBootstrap?: boolean; } export interface MdaaPackageNameVersion { readonly name: string; readonly version: string; } /** * Base class for MDAA CDK Apps. Provides consistent app behaviours in * configuration parsing, stack generation, resource naming, * and CDK Nag compliance configurations. * Reads all required inputs as CDK Context. */ export abstract class MdaaCdkApp extends App { private readonly naming: IMdaaResourceNaming; protected readonly moduleName: string; private readonly appConfigRaw: ConfigurationElement; private readonly tags: { [name: string]: string }; private readonly org: string; private readonly env: string; private readonly domain: string; private readonly solutionId: string; private readonly solutionName: string; private readonly solutionVersion: string; protected readonly deployRegion?: string; protected readonly deployAccount?: string; private readonly useBootstrap: boolean; private readonly stack: MdaaStack; private readonly baseConfigParser: MdaaAppConfigParser<MdaaBaseConfigContents>; private readonly additionalAccounts?: string[]; private readonly additionalAccountStacks: { [AccountRecovery: string]: Stack }; /** * Constructor does most of the app initialization, reading inputs from CDK context, parsing App config files, configuring resource naming, and configuring CDK Nag. * @param props - CDK AppProps (default empty). Not typically required if running using the CDK cli, but useful for direct instantiation. * @param packageNameVersion */ constructor(props: MdaaAppProps, packageNameVersion?: MdaaPackageNameVersion) { super(props); this.node.setContext('aws-cdk:enableDiffNoFail', true); this.node.setContext('@aws-cdk/core:enablePartitionLiterals', true); assert(this.node.tryGetContext('org'), "Organization must be specified in context as 'org'"); assert(this.node.tryGetContext('env'), "Environment must be specified in context as 'env'"); assert(this.node.tryGetContext('domain'), "Domain must be specified in context as 'domain'"); assert(this.node.tryGetContext('module_name'), "Module Name must be specified in context as 'module_name'"); this.org = this.node.tryGetContext('org').toLowerCase(); this.env = this.node.tryGetContext('env').toLowerCase(); this.domain = this.node.tryGetContext('domain').toLowerCase(); this.moduleName = this.node.tryGetContext('module_name').toLowerCase(); // Solution Details this.solutionId = pjson.solution_id; this.solutionName = pjson.solution_name; this.solutionVersion = pjson.version; const packageName = packageNameVersion?.name.replace('@aws-mdaa/', '') ?? 'unknown'; const packageVersion = packageNameVersion?.version ?? 'unknown'; console.log(`Running MDAA Module ${packageName} Version: ${packageVersion}`); if (this.node.tryGetContext('@aws-mdaa/legacyCaefTags')) { this.tags = { caef_org: this.org, caef_env: this.env, caef_domain: this.domain, caef_cdk_app: packageName, caef_module_name: this.moduleName, }; } else { this.tags = { mdaa_org: this.org, mdaa_env: this.env, mdaa_domain: this.domain, mdaa_cdk_app: packageName, mdaa_module_name: this.moduleName, }; } if (props.useBootstrap != undefined) { this.useBootstrap = props.useBootstrap; } else { this.useBootstrap = this.node.tryGetContext('use_bootstrap') == undefined ? true : /true/i.test(this.node.tryGetContext('use_bootstrap')); } const namingModule: string = this.node.tryGetContext('naming_module'); const namingClass: string = this.node.tryGetContext('naming_class'); this.naming = this.configNamingModule(namingModule, namingClass); const logSuppressions: boolean = this.node.tryGetContext('log_suppressions') == undefined ? false : /true/i.test(this.node.tryGetContext('log_suppressions')); Aspects.of(this).add(new AwsSolutionsChecks({ verbose: true, logIgnores: logSuppressions })); Aspects.of(this).add(new NIST80053R5Checks({ verbose: true, logIgnores: logSuppressions })); Aspects.of(this).add(new HIPAASecurityChecks({ verbose: true, logIgnores: logSuppressions })); Aspects.of(this).add(new PCIDSS321Checks({ verbose: true, logIgnores: logSuppressions })); this.applyCustomAspects(); this.appConfigRaw = { ...this.loadConfigFromFiles(this.node.tryGetContext('module_configs')?.split(',') || []), ...this.loadAppConfigDataFromContext(), ...props.appConfigRaw, }; this.tags = { ...this.loadTagConfigFromFiles(), ...this.loadTagConfigDataFromContext(), ...this.tags }; this.deployAccount = process.env.CDK_DEPLOY_ACCOUNT || process.env.CDK_DEFAULT_ACCOUNT; this.deployRegion = process.env.CI_SUPPLIED_TARGET_REGION || process.env.CDK_DEFAULT_REGION; this.additionalAccounts = this.node.tryGetContext('additional_accounts')?.split(','); this.stack = this.createEmptyStack(); this.additionalAccountStacks = Object.fromEntries( this.additionalAccounts?.map(account => { const stackName = this.naming.stackName(account); const stackProps = { naming: this.naming, env: { region: this.stack.region, account: account, }, }; const additionalAccountStack = new Stack(this, stackName, stackProps); additionalAccountStack.addDependency(this.stack); return [account, additionalAccountStack]; }) || [], ); this.baseConfigParser = new MdaaAppConfigParser<MdaaBaseConfigContents>( this.stack, this.getConfigParserProps(), configSchema, undefined, true, ); } protected static parsePackageJson(pjsonPath: string): MdaaPackageNameVersion { // nosemgrep const pjson = require(pjsonPath); return { name: pjson.name, version: pjson.version.replace(/\.\d*$/, '.x'), }; } private loadTagConfigDataFromContext(): { [key: string]: string } { const tagConfigDataFromContextString: string = this.node.tryGetContext('tag_config_data'); if (tagConfigDataFromContextString) { return JSON.parse(tagConfigDataFromContextString); } return {}; } private loadAppConfigDataFromContext(): ConfigurationElement { const appConfigDataFromContextString: string = this.node.tryGetContext('module_config_data'); if (appConfigDataFromContextString) { return JSON.parse(appConfigDataFromContextString); } return {}; } private loadTagConfigFromFiles(): TagElement { const tagConfigRaw = this.loadConfigFromFiles(this.node.tryGetContext('tag_configs')?.split(',') || []); return (tagConfigRaw['tags'] as TagElement) || {}; } private loadConfigFromFiles(fileList: string[]): ConfigurationElement { // nosemgrep const _ = require('lodash'); function customizer(objValue: unknown, srcValue: unknown): unknown[] | undefined { if (_.isArray(objValue)) { return (objValue as unknown[]).concat(srcValue); } return undefined; } let configRaw: ConfigurationElement = {}; fileList.forEach((fileName: string) => { console.log(`Reading config from ${fileName}`); // nosemgrep const parsedYaml = yaml.parse(fs.readFileSync(fileName.trim(), 'utf8')); //Resolve relative paths in parsedYaml const baseDir = path.dirname(fileName.trim()); const pathResolvedYaml = new MdaaConfigTransformer(new ConfigConfigPathValueTransformer(baseDir)).transformConfig( parsedYaml, ); configRaw = _.mergeWith(configRaw, pathResolvedYaml, customizer); }); return configRaw; } private configNamingModule(namingModule: string, namingClass: string): IMdaaResourceNaming { if (namingModule) { // nosemgrep const naming_module_path = namingModule.startsWith('./') ? path.resolve(namingModule) : namingModule; // nosemgrep const customNamingModule = require(naming_module_path); return new customNamingModule[namingClass]({ cdkNode: this.node, org: this.org, env: this.env, domain: this.domain, moduleName: this.moduleName, }); } else { return new MdaaDefaultResourceNaming({ cdkNode: this.node, org: this.org, env: this.env, domain: this.domain, moduleName: this.moduleName, }); } } private applyCustomAspects() { const customAspectsContextString: string = this.node.tryGetContext('custom_aspects'); if (customAspectsContextString) { const customAspects: MdaaCustomAspect[] = JSON.parse(customAspectsContextString); customAspects.forEach(customAspect => this.applyCustomAspect(customAspect)); } } private applyCustomAspect(customAspect: MdaaCustomAspect) { // nosemgrep const customAspectModulePath = customAspect.aspect_module.startsWith('./') ? path.resolve(customAspect.aspect_module) : customAspect.aspect_module; console.log(`Applying custom aspect: ${customAspect.aspect_module}:${customAspect.aspect_class}`); // nosemgrep const customAspectModule = require(customAspectModulePath); const aspect = new customAspectModule[customAspect.aspect_class](customAspect.aspect_props); Aspects.of(this).add(aspect); } public generateStack(): Stack { if (this.baseConfigParser.serviceCatalogConfig) { const productStack = this.createEmptyProductStack(this.stack); this.subGenerateResources(productStack, this.createL3ConstructProps(productStack), this.getConfigParserProps()); const productProps: CloudFormationProductProps = { productName: this.baseConfigParser.serviceCatalogConfig.name, owner: this.baseConfigParser.serviceCatalogConfig.owner, productVersions: [ { productVersionName: 'v1', cloudFormationTemplate: CloudFormationTemplate.fromProductStack(productStack), }, ], }; const product = new CloudFormationProduct(this.stack, 'Product', productProps); const portfolio = Portfolio.fromPortfolioArn( this.stack, 'portfolio', this.baseConfigParser.serviceCatalogConfig.portfolio_arn, ); if (this.baseConfigParser.serviceCatalogConfig.launch_role_name) { const launchRoleConstraintProps: CfnLaunchRoleConstraintProps = { portfolioId: portfolio.portfolioId, productId: product.productId, localRoleName: this.baseConfigParser.serviceCatalogConfig.launch_role_name, }; new CfnLaunchRoleConstraint(this.stack, 'launch-role-constraint', launchRoleConstraintProps); } portfolio.addProduct(product); } else { this.subGenerateResources(this.stack, this.createL3ConstructProps(this.stack), this.getConfigParserProps()); } this.addTagsAndSuppressions(); return this.stack; } /** * Implemented in derived MDAA App classes in order to generate CDK scopes. */ protected abstract subGenerateResources( stack: Stack, l3ConstructProps: MdaaL3ConstructProps, parserProps: MdaaAppConfigParserProps, ): void; private createEmptyStack(): MdaaStack { const stackName = this.naming.stackName(); const stackDescription = `(${this.solutionId}-${this.moduleName}) ${this.solutionName}. Version ${this.solutionVersion}`; const stackProps = { naming: this.naming, description: stackDescription, useBootstrap: this.useBootstrap, env: { region: this.deployRegion, account: this.deployAccount, }, }; const stack = new MdaaStack(this, stackName, stackProps); new StringParameter(stack, 'StackDescriptionParameter', { parameterName: this.naming.ssmPath('aws-solution'), stringValue: stackDescription, description: 'Stack description parameter to update on version changes', }); return stack; } private createEmptyProductStack(stack: MdaaStack): MdaaProductStack { const productStackProps: MdaaProductStackProps = { naming: this.naming, useBootstrap: this.useBootstrap, moduleName: this.moduleName, }; const productStack = new MdaaProductStack(stack, `${stack.stackName}-product`, productStackProps); const provisioningMacroFunctionRole = new MdaaLambdaRole(stack, 'provisioning-macro-function-role', { description: 'Provisioning Macro Role', roleName: 'prov-macro', naming: this.naming, logGroupNames: [this.naming.resourceName('provisioningMacro')], createParams: false, createOutputs: false, }); const provisioningMacroFunctionProps: MdaaLambdaFunctionProps = { runtime: Runtime.PYTHON_3_13, code: Code.fromAsset(`${__dirname}/../src/python/provisioning_macro`), handler: 'provisioning_macro.lambda_handler', functionName: 'provisioningMacro', role: provisioningMacroFunctionRole, naming: this.naming, environment: { LOG_LEVEL: 'INFO', }, }; const provisioningMacroFunction = new MdaaLambdaFunction( stack, 'provisioning-macro-function', provisioningMacroFunctionProps, ); MdaaNagSuppressions.addCodeResourceSuppressions( provisioningMacroFunction, [ { id: 'NIST.800.53.R5-LambdaDLQ', reason: 'Function is for Cfn Macro and error handling will be handled by CloudFormation.', }, { id: 'NIST.800.53.R5-LambdaInsideVPC', reason: 'Function is for Cfn Macro and will interact only with CloudFormation.', }, { id: 'NIST.800.53.R5-LambdaConcurrency', reason: 'Function is for Cfn Macro and will only execute during stack deployement. Reserved concurrency not appropriate.', }, { id: 'HIPAA.Security-LambdaDLQ', reason: 'Function is for Cfn Macro and error handling will be handled by CloudFormation.', }, { id: 'PCI.DSS.321-LambdaDLQ', reason: 'Function is for Cfn Macro and error handling will be handled by CloudFormation.', }, { id: 'HIPAA.Security-LambdaInsideVPC', reason: 'Function is for Cfn Macro and will interact only with CloudFormation.', }, { id: 'PCI.DSS.321-LambdaInsideVPC', reason: 'Function is for Cfn Macro and will interact only with CloudFormation.', }, { id: 'HIPAA.Security-LambdaConcurrency', reason: 'Function is for Cfn Macro and will only execute during stack deployement. Reserved concurrency not appropriate.', }, { id: 'PCI.DSS.321-LambdaConcurrency', reason: 'Function is for Cfn Macro and will only execute during stack deployement. Reserved concurrency not appropriate.', }, ], true, ); const provisioningMacro = new CfnMacro(stack, 'provisioning-macro', { name: this.naming.resourceName('provisioning-macro'), functionName: provisioningMacroFunction.functionArn, }); productStack.templateOptions.transforms = [provisioningMacro.name]; return productStack; } private addTagsAndSuppressions() { const allStacks = [this.stack, ...Object.entries(this.additionalAccountStacks).map(x => x[1])]; allStacks.forEach(stack => { this.baseConfigParser.nagSuppressions?.by_path?.forEach(suppression => { try { MdaaNagSuppressions.addConfigResourceSuppressionsByPath(stack, suppression.path, suppression.suppressions); } catch (error) { console.log(`Error adding suppression for path ${suppression.path} to stack ${stack.stackName}`); } }); // Apply our tags for (const tagKey in this.tags) { if (tagKey in this.tags) { Tags.of(stack).add(tagKey, this.tags[tagKey]); } } }); } private createL3ConstructProps(stack: MdaaStack): MdaaL3ConstructProps { return { naming: this.naming, roleHelper: stack.roleHelper, crossAccountStacks: this.additionalAccountStacks, tags: this.tags, }; } /** * * @returns Astandard set of MDAA Stack Props for use in Mdaa App Configs */ private getConfigParserProps(): MdaaAppConfigParserProps { return { org: this.org, domain: this.domain, environment: this.env, module_name: this.moduleName, rawConfig: this.appConfigRaw, naming: this.naming, }; } }