diff --git a/package.json b/package.json index 73af824089036..96281e2f0dd36 100644 --- a/package.json +++ b/package.json @@ -109,6 +109,8 @@ "@aws-cdk/core/minimatch/**", "@aws-cdk/cx-api/semver", "@aws-cdk/cx-api/semver/**", + "@aws-cdk/pipelines/aws-sdk", + "@aws-cdk/pipelines/aws-sdk/**", "@aws-cdk/yaml-cfn/yaml", "@aws-cdk/yaml-cfn/yaml/**", "aws-cdk-lib/@balena/dockerignore", diff --git a/packages/@aws-cdk/aws-apigatewayv2/README.md b/packages/@aws-cdk/aws-apigatewayv2/README.md index 0d4d7849fd8df..944e328fc8532 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/README.md +++ b/packages/@aws-cdk/aws-apigatewayv2/README.md @@ -311,6 +311,15 @@ new WebSocketStage(stack, 'mystage', { }); ``` +To retrieve a websocket URL and a callback URL: + +```ts +const webSocketURL = webSocketStage.url; +// wss://${this.api.apiId}.execute-api.${s.region}.${s.urlSuffix}/${urlPath} +const callbackURL = webSocketURL.callbackUrl; +// https://${this.api.apiId}.execute-api.${s.region}.${s.urlSuffix}/${urlPath} +``` + To add any other route: ```ts diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/stage.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/stage.ts index a50353a79ca2d..f6bc91909dcba 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/stage.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/stage.ts @@ -13,6 +13,14 @@ export interface IWebSocketStage extends IStage { * The API this stage is associated to. */ readonly api: IWebSocketApi; + + /** + * The callback URL to this stage. + * You can use the callback URL to send messages to the client from the backend system. + * https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-basic-concept.html + * https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-how-to-call-websocket-api-connections.html + */ + readonly callbackUrl: string; } /** @@ -57,6 +65,10 @@ export class WebSocketStage extends StageBase implements IWebSocketStage { get url(): string { throw new Error('url is not available for imported stages.'); } + + get callbackUrl(): string { + throw new Error('callback url is not available for imported stages.'); + } } return new Import(scope, id); } @@ -86,11 +98,20 @@ export class WebSocketStage extends StageBase implements IWebSocketStage { } /** - * The URL to this stage. + * The websocket URL to this stage. */ public get url(): string { const s = Stack.of(this); const urlPath = this.stageName; return `wss://${this.api.apiId}.execute-api.${s.region}.${s.urlSuffix}/${urlPath}`; } + + /** + * The callback URL to this stage. + */ + public get callbackUrl(): string { + const s = Stack.of(this); + const urlPath = this.stageName; + return `https://${this.api.apiId}.execute-api.${s.region}.${s.urlSuffix}/${urlPath}`; + } } diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/websocket/stage.test.ts b/packages/@aws-cdk/aws-apigatewayv2/test/websocket/stage.test.ts index 4ff13cd6bb8d0..bec3e34e5d4fb 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/test/websocket/stage.test.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/test/websocket/stage.test.ts @@ -40,5 +40,23 @@ describe('WebSocketStage', () => { // THEN expect(imported.stageName).toEqual(stage.stageName); + expect(() => imported.url).toThrow(); + expect(() => imported.callbackUrl).toThrow(); + }); + + test('callback URL', () => { + // GIVEN + const stack = new Stack(); + const api = new WebSocketApi(stack, 'Api'); + + // WHEN + const defaultStage = new WebSocketStage(stack, 'Stage', { + webSocketApi: api, + stageName: 'dev', + }); + + // THEN + expect(defaultStage.callbackUrl.endsWith('/dev')).toBe(true); + expect(defaultStage.callbackUrl.startsWith('https://')).toBe(true); }); }); diff --git a/packages/@aws-cdk/pipelines/ORIGINAL_API.md b/packages/@aws-cdk/pipelines/ORIGINAL_API.md index 3f1bd5920bcd2..d46acb44af989 100644 --- a/packages/@aws-cdk/pipelines/ORIGINAL_API.md +++ b/packages/@aws-cdk/pipelines/ORIGINAL_API.md @@ -495,4 +495,60 @@ const validationAction = new ShellScriptAction({ // 'test.js' was produced from 'test/test.ts' during the synth step commands: ['node ./test.js'], }); -``` \ No newline at end of file +``` + +### Confirm permissions broadening + +To keep tabs on the security impact of changes going out through your pipeline, +you can insert a security check before any stage deployment. This security check +will check if the upcoming deployment would add any new IAM permissions or +security group rules, and if so pause the pipeline and require you to confirm +the changes. + +The security check will appear as two distinct actions in your pipeline: first +a CodeBuild project that runs `cdk diff` on the stage that's about to be deployed, +followed by a Manual Approval action that pauses the pipeline. If it so happens +that there no new IAM permissions or security group rules will be added by the deployment, +the manual approval step is automatically satisfied. The pipeline will look like this: + +```txt +Pipeline +├── ... +├── MyApplicationStage +│   ├── MyApplicationSecurityCheck // Security Diff Action +│   ├── MyApplicationManualApproval // Manual Approval Action +│   ├── Stack.Prepare +│   └── Stack.Deploy +└── ... +``` + +You can enable the security check by passing `confirmBroadeningPermissions` to +`addApplicationStage`: + +```ts +const stage = pipeline.addApplicationStage(new MyApplication(this, 'PreProd'), { + confirmBroadeningPermissions: true, +}); +``` + +To get notified when there is a change that needs your manual approval, +create an SNS Topic, subscribe your own email address, and pass it in via +`securityNotificationTopic`: + +```ts +import * as sns from '@aws-cdk/aws-sns'; +import * as subscriptions from '@aws-cdk/aws-sns-subscriptions'; +import * as pipelines from '@aws-cdk/pipelines'; + +const topic = new sns.Topic(this, 'SecurityChangesTopic'); +topic.addSubscription(new subscriptions.EmailSubscription('test@email.com')); + +const pipeline = new CdkPipeline(app, 'Pipeline', { /* ... */ }); +const stage = pipeline.addApplicationStage(new MyApplication(this, 'PreProd'), { + confirmBroadeningPermissions: true, + securityNotificationTopic: topic, +}); +``` + +**Note**: Manual Approvals notifications only apply when an application has security +check enabled. \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/README.md b/packages/@aws-cdk/pipelines/README.md index 76a8c3a84f8fb..711a73f7e64e1 100644 --- a/packages/@aws-cdk/pipelines/README.md +++ b/packages/@aws-cdk/pipelines/README.md @@ -840,7 +840,7 @@ and orphan the old bucket. You should manually delete the orphaned bucket after you are sure you have redeployed all CDK applications and there are no more references to the old asset bucket. -## Security Tips +## Security Considerations It's important to stay safe while employing Continuous Delivery. The CDK Pipelines library comes with secure defaults to the best of our ability, but by its @@ -862,6 +862,68 @@ We therefore expect you to mind the following: changes can be deployed through git. Avoid the chances of credentials leaking by not having them in the first place! +### Confirm permissions broadening + +To keep tabs on the security impact of changes going out through your pipeline, +you can insert a security check before any stage deployment. This security check +will check if the upcoming deployment would add any new IAM permissions or +security group rules, and if so pause the pipeline and require you to confirm +the changes. + +The security check will appear as two distinct actions in your pipeline: first +a CodeBuild project that runs `cdk diff` on the stage that's about to be deployed, +followed by a Manual Approval action that pauses the pipeline. If it so happens +that there no new IAM permissions or security group rules will be added by the deployment, +the manual approval step is automatically satisfied. The pipeline will look like this: + +```txt +Pipeline +├── ... +├── MyApplicationStage +│   ├── MyApplicationSecurityCheck // Security Diff Action +│   ├── MyApplicationManualApproval // Manual Approval Action +│   ├── Stack.Prepare +│   └── Stack.Deploy +└── ... +``` + +You can insert the security check by using a `ConfirmPermissionsBroadening` step: + +```ts +const stage = new MyApplicationStage(this, 'MyApplication'); +pipeline.addStage(stage, { + pre: [ + new ConfirmPermissionsBroadening('Check', { stage }), + ], +}); +``` + +To get notified when there is a change that needs your manual approval, +create an SNS Topic, subscribe your own email address, and pass it in as +as the `notificationTopic` property: + +```ts +import * as sns from '@aws-cdk/aws-sns'; +import * as subscriptions from '@aws-cdk/aws-sns-subscriptions'; +import * as pipelines from '@aws-cdk/pipelines'; + +const topic = new sns.Topic(this, 'SecurityChangesTopic'); +topic.addSubscription(new subscriptions.EmailSubscription('test@email.com')); + +const stage = new MyApplicationStage(this, 'MyApplication'); +pipeline.addStage(stage, { + pre: [ + new ConfirmPermissionsBroadening('Check', { + stage, + notificationTopic: topic, + }), + ], +}); +``` + +**Note**: Manual Approvals notifications only apply when an application has security +check enabled. + ## Troubleshooting Here are some common errors you may encounter while using this library. diff --git a/packages/@aws-cdk/pipelines/lib/codepipeline/codepipeline-action-factory.ts b/packages/@aws-cdk/pipelines/lib/codepipeline/codepipeline-action-factory.ts index 89d419b56223d..62c9fa86d025b 100644 --- a/packages/@aws-cdk/pipelines/lib/codepipeline/codepipeline-action-factory.ts +++ b/packages/@aws-cdk/pipelines/lib/codepipeline/codepipeline-action-factory.ts @@ -1,9 +1,8 @@ import * as cb from '@aws-cdk/aws-codebuild'; import * as cp from '@aws-cdk/aws-codepipeline'; import { Construct } from 'constructs'; -import { PipelineBase } from '../main'; import { ArtifactMap } from './artifact-map'; -import { CodeBuildOptions } from './codepipeline'; +import { CodeBuildOptions, CodePipeline } from './codepipeline'; /** * Options for the `CodePipelineActionFactory.produce()` method. @@ -43,7 +42,7 @@ export interface ProduceActionOptions { /** * The pipeline the action is being generated for */ - readonly pipeline: PipelineBase; + readonly pipeline: CodePipeline; /** * If this action factory creates a CodeBuild step, default options to inherit diff --git a/packages/@aws-cdk/pipelines/lib/codepipeline/confirm-permissions-broadening.ts b/packages/@aws-cdk/pipelines/lib/codepipeline/confirm-permissions-broadening.ts new file mode 100644 index 0000000000000..95b66267be25c --- /dev/null +++ b/packages/@aws-cdk/pipelines/lib/codepipeline/confirm-permissions-broadening.ts @@ -0,0 +1,88 @@ +import { IStage } from '@aws-cdk/aws-codepipeline'; +import * as cpa from '@aws-cdk/aws-codepipeline-actions'; +import * as sns from '@aws-cdk/aws-sns'; +import { Stage } from '@aws-cdk/core'; +import { Node } from 'constructs'; +import { Step } from '../blueprint'; +import { ApplicationSecurityCheck } from '../private/application-security-check'; +import { CodePipeline } from './codepipeline'; +import { CodePipelineActionFactoryResult, ICodePipelineActionFactory, ProduceActionOptions } from './codepipeline-action-factory'; + +/** + * Properties for a `PermissionsBroadeningCheck` + */ +export interface PermissionsBroadeningCheckProps { + /** + * The CDK Stage object to check the stacks of + * + * This should be the same Stage object you are passing to `addStage()`. + */ + readonly stage: Stage; + + /** + * Topic to send notifications when a human needs to give manual confirmation + * + * @default - no notification + */ + readonly notificationTopic?: sns.ITopic +} + +/** + * Pause the pipeline if a deployment would add IAM permissions or Security Group rules + * + * This step is only supported in CodePipeline pipelines. + */ +export class ConfirmPermissionsBroadening extends Step implements ICodePipelineActionFactory { + constructor(id: string, private readonly props: PermissionsBroadeningCheckProps) { + super(id); + } + + public produceAction(stage: IStage, options: ProduceActionOptions): CodePipelineActionFactoryResult { + const sec = this.getOrCreateSecCheck(options.pipeline); + this.props.notificationTopic?.grantPublish(sec.cdkDiffProject); + + const variablesNamespace = Node.of(this.props.stage).addr; + + const approveActionName = `${options.actionName}.Confirm`; + stage.addAction(new cpa.CodeBuildAction({ + runOrder: options.runOrder, + actionName: `${options.actionName}.Check`, + input: options.artifacts.toCodePipeline(options.pipeline.cloudAssemblyFileSet), + project: sec.cdkDiffProject, + variablesNamespace, + environmentVariables: { + STAGE_PATH: { value: Node.of(this.props.stage).path }, + STAGE_NAME: { value: stage.stageName }, + ACTION_NAME: { value: approveActionName }, + ...this.props.notificationTopic ? { + NOTIFICATION_ARN: { value: this.props.notificationTopic.topicArn }, + NOTIFICATION_SUBJECT: { value: `Confirm permission broadening in ${this.props.stage.stageName}` }, + } : {}, + }, + })); + + stage.addAction(new cpa.ManualApprovalAction({ + actionName: approveActionName, + runOrder: options.runOrder + 1, + additionalInformation: `#{${variablesNamespace}.MESSAGE}`, + externalEntityLink: `#{${variablesNamespace}.LINK}`, + })); + + return { runOrdersConsumed: 2 }; + } + + private getOrCreateSecCheck(pipeline: CodePipeline): ApplicationSecurityCheck { + const id = 'PipelinesSecurityCheck'; + const existing = Node.of(pipeline).tryFindChild(id); + if (existing) { + if (!(existing instanceof ApplicationSecurityCheck)) { + throw new Error(`Expected '${Node.of(existing).path}' to be 'ApplicationSecurityCheck' but was '${existing}'`); + } + return existing; + } + + return new ApplicationSecurityCheck(pipeline, id, { + codePipeline: pipeline.pipeline, + }); + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/lib/codepipeline/index.ts b/packages/@aws-cdk/pipelines/lib/codepipeline/index.ts index 00e10509bb0df..4b2a86d61fc4d 100644 --- a/packages/@aws-cdk/pipelines/lib/codepipeline/index.ts +++ b/packages/@aws-cdk/pipelines/lib/codepipeline/index.ts @@ -1,5 +1,6 @@ export * from './artifact-map'; export * from './codebuild-step'; +export * from './confirm-permissions-broadening'; export * from './codepipeline'; export * from './codepipeline-action-factory'; export * from './codepipeline-source'; \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/lib/legacy/pipeline.ts b/packages/@aws-cdk/pipelines/lib/legacy/pipeline.ts index 95a828b981ea1..ec09a80dccb0e 100644 --- a/packages/@aws-cdk/pipelines/lib/legacy/pipeline.ts +++ b/packages/@aws-cdk/pipelines/lib/legacy/pipeline.ts @@ -6,14 +6,15 @@ import { Annotations, App, Aws, CfnOutput, Fn, Lazy, PhysicalName, Stack, Stage import { Construct } from 'constructs'; import { AssetType } from '../blueprint/asset-type'; import { dockerCredentialsInstallCommands, DockerCredential, DockerCredentialUsage } from '../docker-credentials'; +import { ApplicationSecurityCheck } from '../private/application-security-check'; import { appOf, assemblyBuilderOf } from '../private/construct-internals'; import { DeployCdkStackAction, PublishAssetsAction, UpdatePipelineAction } from './actions'; -import { AddStageOptions, AssetPublishingCommand, CdkStage, StackOutput } from './stage'; +import { AddStageOptions, AssetPublishingCommand, BaseStageOptions, CdkStage, StackOutput } from './stage'; +import { SimpleSynthAction } from './synths'; // v2 - keep this import as a separate section to reduce merge conflict when forward merging with the v2 branch. // eslint-disable-next-line import { Construct as CoreConstruct } from '@aws-cdk/core'; -import { SimpleSynthAction } from './synths'; const CODE_BUILD_LENGTH_LIMIT = 100; /** @@ -184,6 +185,7 @@ export class CdkPipeline extends CoreConstruct { private readonly _outputArtifacts: Record = {}; private readonly _cloudAssemblyArtifact: codepipeline.Artifact; private readonly _dockerCredentials: DockerCredential[]; + private _applicationSecurityCheck?: ApplicationSecurityCheck; constructor(scope: Construct, id: string, props: CdkPipelineProps) { super(scope, id); @@ -288,6 +290,22 @@ export class CdkPipeline extends CoreConstruct { return this._pipeline.stage(stageName); } + /** + * Get a cached version of an Application Security Check, which consists of: + * - CodeBuild Project to check for security changes in a stage + * - Lambda Function that approves the manual approval if no security changes are detected + * + * @internal + */ + public _getApplicationSecurityCheck(): ApplicationSecurityCheck { + if (!this._applicationSecurityCheck) { + this._applicationSecurityCheck = new ApplicationSecurityCheck(this, 'PipelineApplicationSecurityCheck', { + codePipeline: this._pipeline, + }); + } + return this._applicationSecurityCheck; + } + /** * Add pipeline stage that will deploy the given application stage * @@ -300,7 +318,7 @@ export class CdkPipeline extends CoreConstruct { * publishing stage. */ public addApplicationStage(appStage: Stage, options: AddStageOptions = {}): CdkStage { - const stage = this.addStage(appStage.stageName); + const stage = this.addStage(appStage.stageName, options); stage.addApplication(appStage, options); return stage; } @@ -312,7 +330,7 @@ export class CdkPipeline extends CoreConstruct { * application, but you can use this method if you want to add other kinds of * Actions to a pipeline. */ - public addStage(stageName: string) { + public addStage(stageName: string, options?: BaseStageOptions) { const pipelineStage = this._pipeline.addStage({ stageName, }); @@ -325,6 +343,7 @@ export class CdkPipeline extends CoreConstruct { publishAsset: this._assets.addPublishAssetAction.bind(this._assets), stackOutputArtifact: (artifactId) => this._outputArtifacts[artifactId], }, + ...options, }); this._stages.push(stage); return stage; diff --git a/packages/@aws-cdk/pipelines/lib/legacy/stage.ts b/packages/@aws-cdk/pipelines/lib/legacy/stage.ts index 55c847d984a58..ee4b860848503 100644 --- a/packages/@aws-cdk/pipelines/lib/legacy/stage.ts +++ b/packages/@aws-cdk/pipelines/lib/legacy/stage.ts @@ -1,12 +1,17 @@ +import * as codebuild from '@aws-cdk/aws-codebuild'; import * as codepipeline from '@aws-cdk/aws-codepipeline'; import * as cpactions from '@aws-cdk/aws-codepipeline-actions'; +import { CodeBuildAction } from '@aws-cdk/aws-codepipeline-actions'; +import * as sns from '@aws-cdk/aws-sns'; import { Stage, Aspects } from '@aws-cdk/core'; import * as cxapi from '@aws-cdk/cx-api'; import { Construct } from 'constructs'; import { AssetType } from '../blueprint/asset-type'; +import { ApplicationSecurityCheck } from '../private/application-security-check'; import { AssetManifestReader, DockerImageManifestEntry, FileManifestEntry } from '../private/asset-manifest'; import { topologicalSort } from '../private/toposort'; import { DeployCdkStackAction } from './actions'; +import { CdkPipeline } from './pipeline'; // v2 - keep this import as a separate section to reduce merge conflict when forward merging with the v2 branch. // eslint-disable-next-line @@ -35,8 +40,30 @@ export interface CdkStageProps { * Features the Stage needs from its environment */ readonly host: IStageHost; + + /** + * Run a security check before every application prepare/deploy actions. + * + * Note: Stage level security check can be overriden per application as follows: + * `stage.addApplication(app, { confirmBroadeningPermissions: false })` + * + * @default false + */ + readonly confirmBroadeningPermissions?: boolean; + + /** + * Optional SNS topic to send notifications to when any security check registers + * changes within a application. + * + * Note: The Stage Notification Topic can be overriden per application as follows: + * `stage.addApplication(app, { securityNotificationTopic: newTopic })` + * + * @default undefined no stage level notification topic + */ + readonly securityNotificationTopic?: sns.ITopic; } + /** * Stage in a CdkPipeline * @@ -51,15 +78,25 @@ export class CdkStage extends CoreConstruct { private readonly stacksToDeploy = new Array(); private readonly stageName: string; private readonly host: IStageHost; + private readonly confirmBroadeningPermissions: boolean; + private readonly pipeline?: CdkPipeline; + private readonly securityNotificationTopic?: sns.ITopic; + private _applicationSecurityCheck?: ApplicationSecurityCheck; private _prepared = false; constructor(scope: Construct, id: string, props: CdkStageProps) { super(scope, id); + if (scope instanceof CdkPipeline) { + this.pipeline = scope; + } + this.stageName = props.stageName; this.pipelineStage = props.pipelineStage; this.cloudAssemblyArtifact = props.cloudAssemblyArtifact; this.host = props.host; + this.confirmBroadeningPermissions = props.confirmBroadeningPermissions ?? false; + this.securityNotificationTopic = props.securityNotificationTopic; Aspects.of(this).add({ visit: () => this.prepareStage() }); } @@ -79,6 +116,10 @@ export class CdkStage extends CoreConstruct { const asm = appStage.synth({ validateOnSynthesis: true }); const extraRunOrderSpace = options.extraRunOrderSpace ?? 0; + if (options.confirmBroadeningPermissions ?? this.confirmBroadeningPermissions) { + this.addSecurityCheck(appStage, options); + } + if (asm.stacks.length === 0) { // If we don't check here, a more puzzling "stage contains no actions" // error will be thrown come deployment time. @@ -108,6 +149,30 @@ export class CdkStage extends CoreConstruct { } } + /** + * Get a cached version of an ApplicationSecurityCheck, which consists of: + * - CodeBuild Project to check for security changes in a stage + * - Lambda Function that approves the manual approval if no security changes are detected + * + * The ApplicationSecurityCheck is cached from the pipeline **if** this stage is scoped + * to a CDK Pipeline. If this stage **is not** scoped to a pipeline, create an ApplicationSecurityCheck + * scoped to the stage itself. + * + * @internal + */ + private getApplicationSecurityCheck(): ApplicationSecurityCheck { + if (this._applicationSecurityCheck) { + return this._applicationSecurityCheck; + } + + this._applicationSecurityCheck = this.pipeline + ? this.pipeline._getApplicationSecurityCheck() + : new ApplicationSecurityCheck(this, 'StageApplicationSecurityCheck', { + codePipeline: this.pipelineStage.pipeline as codepipeline.Pipeline, + }); + return this._applicationSecurityCheck; + } + /** * Add a deployment action based on a stack artifact */ @@ -225,6 +290,61 @@ export class CdkStage extends CoreConstruct { return stripPrefix(s, `${this.stageName}-`); } + /** + * Add a security check before the prepare/deploy actions of an CDK stage. + * The security check consists of two actions: + * - CodeBuild Action to check for security changes in a stage + * - Manual Approval Action that is auto approved via a Lambda if no security changes detected + */ + private addSecurityCheck(appStage: Stage, options?: BaseStageOptions) { + const { cdkDiffProject } = this.getApplicationSecurityCheck(); + const notificationTopic: sns.ITopic | undefined = options?.securityNotificationTopic ?? this.securityNotificationTopic; + notificationTopic?.grantPublish(cdkDiffProject); + + const appStageName = appStage.stageName; + const approveActionName = `${appStageName}ManualApproval`; + const diffAction = new CodeBuildAction({ + runOrder: this.nextSequentialRunOrder(), + actionName: `${appStageName}SecurityCheck`, + input: this.cloudAssemblyArtifact, + project: cdkDiffProject, + variablesNamespace: `${appStageName}SecurityCheck`, + environmentVariables: { + STAGE_PATH: { + value: this.pipelineStage.pipeline.stack.stackName, + type: codebuild.BuildEnvironmentVariableType.PLAINTEXT, + }, + STAGE_NAME: { + value: this.stageName, + type: codebuild.BuildEnvironmentVariableType.PLAINTEXT, + }, + ACTION_NAME: { + value: approveActionName, + type: codebuild.BuildEnvironmentVariableType.PLAINTEXT, + }, + ...notificationTopic ? { + NOTIFICATION_ARN: { + value: notificationTopic.topicArn, + type: codebuild.BuildEnvironmentVariableType.PLAINTEXT, + }, + NOTIFICATION_SUBJECT: { + value: `Confirm permission broadening in ${appStageName}`, + type: codebuild.BuildEnvironmentVariableType.PLAINTEXT, + }, + } : {}, + }, + }); + + const approve = new cpactions.ManualApprovalAction({ + actionName: approveActionName, + runOrder: this.nextSequentialRunOrder(), + additionalInformation: `#{${appStageName}SecurityCheck.MESSAGE}`, + externalEntityLink: `#{${appStageName}SecurityCheck.LINK}`, + }); + + this.addActions(diffAction, approve); + } + /** * Make sure all assets depended on by this stack are published in this pipeline * @@ -370,10 +490,39 @@ export interface AssetPublishingCommand { readonly assetPublishingRoleArn: string; } +/** + * Base options for a pipelines stage + */ +export interface BaseStageOptions { + /** + * Runs a `cdk diff --security-only --fail` to pause the pipeline if there + * are any security changes. + * + * If the stage is configured with `confirmBroadeningPermissions` enabled, you can use this + * property to override the stage configuration. For example, Pipeline Stage + * "Prod" has confirmBroadeningPermissions enabled, with applications "A", "B", "C". All three + * applications will run a security check, but if we want to disable the one for "C", + * we run `stage.addApplication(C, { confirmBroadeningPermissions: false })` to override the pipeline + * stage behavior. + * + * Adds 1 to the run order space. + * + * @default false + */ + readonly confirmBroadeningPermissions?: boolean; + /** + * Optional SNS topic to send notifications to when the security check registers + * changes within the application. + * + * @default undefined no notification topic for security check manual approval action + */ + readonly securityNotificationTopic?: sns.ITopic; +} + /** * Options for adding an application stage to a pipeline */ -export interface AddStageOptions { +export interface AddStageOptions extends BaseStageOptions { /** * Add manual approvals before executing change sets * diff --git a/packages/@aws-cdk/pipelines/lib/main/pipeline-base.ts b/packages/@aws-cdk/pipelines/lib/main/pipeline-base.ts index 563697746a8cf..6ff5a1be60853 100644 --- a/packages/@aws-cdk/pipelines/lib/main/pipeline-base.ts +++ b/packages/@aws-cdk/pipelines/lib/main/pipeline-base.ts @@ -1,6 +1,6 @@ import { Aspects, Stage } from '@aws-cdk/core'; import { Construct } from 'constructs'; -import { AddStageOpts as StageOptions, WaveOptions, Wave, IFileSetProducer, ShellStep } from '../blueprint'; +import { AddStageOpts as StageOptions, WaveOptions, Wave, IFileSetProducer, ShellStep, FileSet } from '../blueprint'; // v2 - keep this import as a separate section to reduce merge conflict when forward merging with the v2 branch. // eslint-disable-next-line @@ -46,6 +46,13 @@ export abstract class PipelineBase extends CoreConstruct { */ public readonly waves: Wave[]; + /** + * The FileSet tha contains the cloud assembly + * + * This is the primary output of the synth step. + */ + public readonly cloudAssemblyFileSet: FileSet; + private built = false; constructor(scope: Construct, id: string, props: PipelineBaseProps) { @@ -55,13 +62,14 @@ export abstract class PipelineBase extends CoreConstruct { props.synth.primaryOutputDirectory('cdk.out'); } - this.synth = props.synth; - this.waves = []; - if (!props.synth.primaryOutput) { throw new Error(`synthStep ${props.synth} must produce a primary output, but is not producing anything. Configure the Step differently or use a different Step type.`); } + this.synth = props.synth; + this.waves = []; + this.cloudAssemblyFileSet = props.synth.primaryOutput; + Aspects.of(this).add({ visit: () => this.buildJustInTime() }); } diff --git a/packages/@aws-cdk/pipelines/lib/private/application-security-check.ts b/packages/@aws-cdk/pipelines/lib/private/application-security-check.ts new file mode 100644 index 0000000000000..152404db70a30 --- /dev/null +++ b/packages/@aws-cdk/pipelines/lib/private/application-security-check.ts @@ -0,0 +1,183 @@ +import * as path from 'path'; +import * as codebuild from '@aws-cdk/aws-codebuild'; +import * as cp from '@aws-cdk/aws-codepipeline'; +import * as iam from '@aws-cdk/aws-iam'; +import * as lambda from '@aws-cdk/aws-lambda'; +import { Duration, Tags } from '@aws-cdk/core'; +import { Construct } from 'constructs'; + +// keep this import separate from other imports to reduce chance for merge conflicts with v2-main +// eslint-disable-next-line no-duplicate-imports, import/order +import { Construct as CoreConstruct } from '@aws-cdk/core'; + +/** + * Properteis for an ApplicationSecurityCheck + */ +export interface ApplicationSecurityCheckProps { + /** + * The pipeline that will be automatically approved + * + * Will have a tag added to it. + */ + readonly codePipeline: cp.Pipeline; +} + +/** + * A construct containing both the Lambda and CodeBuild Project + * needed to conduct a security check on any given application stage. + * + * The Lambda acts as an auto approving mechanism that should only be + * triggered when the CodeBuild Project registers no security changes. + * + * The CodeBuild Project runs a security diff on the application stage, + * and exports the link to the console of the project. + */ +export class ApplicationSecurityCheck extends CoreConstruct { + /** + * A lambda function that approves a Manual Approval Action, given + * the following payload: + * + * { + * "PipelineName": [CodePipelineName], + * "StageName": [CodePipelineStageName], + * "ActionName": [ManualApprovalActionName] + * } + */ + public readonly preApproveLambda: lambda.Function; + /** + * A CodeBuild Project that runs a security diff on the application stage. + * + * - If the diff registers no security changes, CodeBuild will invoke the + * pre-approval lambda and approve the ManualApprovalAction. + * - If changes are detected, CodeBuild will exit into a ManualApprovalAction + */ + public readonly cdkDiffProject: codebuild.Project; + + constructor(scope: Construct, id: string, props: ApplicationSecurityCheckProps) { + super(scope, id); + + Tags.of(props.codePipeline).add('SECURITY_CHECK', 'ALLOW_APPROVE', { + includeResourceTypes: ['AWS::CodePipeline::Pipeline'], + }); + + this.preApproveLambda = new lambda.Function(this, 'CDKPipelinesAutoApprove', { + handler: 'index.handler', + runtime: lambda.Runtime.NODEJS_14_X, + code: lambda.Code.fromAsset(path.resolve(__dirname, 'approve-lambda')), + timeout: Duration.minutes(5), + }); + + this.preApproveLambda.addToRolePolicy(new iam.PolicyStatement({ + actions: ['codepipeline:GetPipelineState', 'codepipeline:PutApprovalResult'], + conditions: { + StringEquals: { + 'aws:ResourceTag/SECURITY_CHECK': 'ALLOW_APPROVE', + }, + }, + resources: ['*'], + })); + + const invokeLambda = + 'aws lambda invoke' + + ` --function-name ${this.preApproveLambda.functionName}` + + ' --invocation-type Event' + + ' --payload "$payload"' + + ' lambda.out'; + + const message = [ + 'An upcoming change would broaden security changes in $PIPELINE_NAME.', + 'Review and approve the changes in CodePipeline to proceed with the deployment.', + '', + 'Review the changes in CodeBuild:', + '', + '$LINK', + '', + 'Approve the changes in CodePipeline (stage $STAGE_NAME, action $ACTION_NAME):', + '', + '$PIPELINE_LINK', + ]; + const publishNotification = + 'aws sns publish' + + ' --topic-arn $NOTIFICATION_ARN' + + ' --subject "$NOTIFICATION_SUBJECT"' + + ` --message "${message.join('\n')}"`; + + this.cdkDiffProject = new codebuild.Project(this, 'CDKSecurityCheck', { + buildSpec: codebuild.BuildSpec.fromObject({ + version: 0.2, + phases: { + build: { + commands: [ + 'npm install -g aws-cdk', + // $CODEBUILD_INITIATOR will always be Code Pipeline and in the form of: + // "codepipeline/example-pipeline-name-Xxx" + 'export PIPELINE_NAME="$(node -pe \'`${process.env.CODEBUILD_INITIATOR}`.split("/")[1]\')"', + 'payload="$(node -pe \'JSON.stringify({ "PipelineName": process.env.PIPELINE_NAME, "StageName": process.env.STAGE_NAME, "ActionName": process.env.ACTION_NAME })\' )"', + // ARN: "arn:aws:codebuild:$region:$account_id:build/$project_name:$project_execution_id$" + 'ARN=$CODEBUILD_BUILD_ARN', + 'REGION="$(node -pe \'`${process.env.ARN}`.split(":")[3]\')"', + 'ACCOUNT_ID="$(node -pe \'`${process.env.ARN}`.split(":")[4]\')"', + 'PROJECT_NAME="$(node -pe \'`${process.env.ARN}`.split(":")[5].split("/")[1]\')"', + 'PROJECT_ID="$(node -pe \'`${process.env.ARN}`.split(":")[6]\')"', + // Manual Approval adds 'http/https' to the resolved link + 'export LINK="https://$REGION.console.aws.amazon.com/codesuite/codebuild/$ACCOUNT_ID/projects/$PROJECT_NAME/build/$PROJECT_NAME:$PROJECT_ID/?region=$REGION"', + 'export PIPELINE_LINK="https://$REGION.console.aws.amazon.com/codesuite/codepipeline/pipelines/$PIPELINE_NAME/view?region=$REGION"', + // Run invoke only if cdk diff passes (returns exit code 0) + // 0 -> true, 1 -> false + ifElse({ + condition: 'cdk diff -a . --security-only --fail $STAGE_PATH/\\*', + thenStatements: [ + invokeLambda, + 'export MESSAGE="No security-impacting changes detected."', + ], + elseStatements: [ + `[ -z "\${NOTIFICATION_ARN}" ] || ${publishNotification}`, + 'export MESSAGE="Deployment would make security-impacting changes. Click the link below to inspect them, then click Approve if all changes are expected."', + ], + }), + ], + }, + }, + env: { + 'exported-variables': [ + 'LINK', + 'MESSAGE', + ], + }, + }), + }); + + // this is needed to check the status the stacks when doing `cdk diff` + this.cdkDiffProject.addToRolePolicy(new iam.PolicyStatement({ + actions: ['sts:AssumeRole'], + resources: ['*'], + conditions: { + 'ForAnyValue:StringEquals': { + 'iam:ResourceTag/aws-cdk:bootstrap-role': ['deploy'], + }, + }, + })); + + this.preApproveLambda.grantInvoke(this.cdkDiffProject); + } +} + +interface ifElseOptions { + readonly condition: string, + readonly thenStatements: string[], + readonly elseStatements?: string[] +} + +const ifElse = ({ condition, thenStatements, elseStatements }: ifElseOptions): string => { + let statement = thenStatements.reduce((acc, ifTrue) => { + return `${acc} ${ifTrue};`; + }, `if ${condition}; then`); + + if (elseStatements) { + statement = elseStatements.reduce((acc, ifFalse) => { + return `${acc} ${ifFalse};`; + }, `${statement} else`); + } + + return `${statement} fi`; +}; \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/lib/private/approve-lambda/index.ts b/packages/@aws-cdk/pipelines/lib/private/approve-lambda/index.ts new file mode 100644 index 0000000000000..0eadb9d9871e3 --- /dev/null +++ b/packages/@aws-cdk/pipelines/lib/private/approve-lambda/index.ts @@ -0,0 +1,48 @@ +// eslint-disable-next-line import/no-extraneous-dependencies +import * as AWS from 'aws-sdk'; + +const client = new AWS.CodePipeline({ apiVersion: '2015-07-09' }); +const TIMEOUT_IN_MINUTES = 5; + +const sleep = (seconds: number) => { + return new Promise(resolve => setTimeout(resolve, seconds * 1000)); +}; + +export async function handler(event: any, _context: any) { + const { + PipelineName: pipelineName, + StageName: stageName, + ActionName: actionName, + } = event; + + function parseState(response: any): string | undefined { + const stages = response.stageStates; + const validStages = stages?.filter((s: any) => s.stageName === stageName); + const manualApproval = validStages.length && + validStages[0].actionStates.filter((state: any) => state.actionName === actionName); + const latest = manualApproval && manualApproval.length && + manualApproval[0].latestExecution; + + return latest ? latest.token : undefined; + } + + const deadline = Date.now() + TIMEOUT_IN_MINUTES * 60000; + while (Date.now() < deadline) { + const response = await client.getPipelineState({ name: pipelineName }).promise(); + const token = parseState(response); + if (token) { + await client.putApprovalResult({ + pipelineName, + actionName, + stageName, + result: { + summary: 'No security changes detected. Automatically approved by Lambda.', + status: 'Approved', + }, + token, + }).promise(); + return; + } + await sleep(5); + } +} diff --git a/packages/@aws-cdk/pipelines/package.json b/packages/@aws-cdk/pipelines/package.json index 1db513581a96e..45252fe965ac7 100644 --- a/packages/@aws-cdk/pipelines/package.json +++ b/packages/@aws-cdk/pipelines/package.json @@ -32,52 +32,56 @@ "organization": true }, "devDependencies": { - "@types/jest": "^26.0.24", + "@aws-cdk/assert-internal": "0.0.0", + "@aws-cdk/aws-apigateway": "0.0.0", + "@aws-cdk/aws-ecr-assets": "0.0.0", + "@aws-cdk/aws-sqs": "0.0.0", + "@aws-cdk/aws-sns-subscriptions": "0.0.0", + "@types/jest": "^26.0.23", + "aws-sdk": "^2.848.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", - "pkglint": "0.0.0", - "@aws-cdk/aws-s3": "0.0.0", - "@aws-cdk/aws-sqs": "0.0.0", - "@aws-cdk/aws-ecr": "0.0.0", - "@aws-cdk/aws-ecr-assets": "0.0.0", - "@aws-cdk/assert-internal": "0.0.0" + "pkglint": "0.0.0" }, "peerDependencies": { - "constructs": "^3.3.69", - "@aws-cdk/core": "0.0.0", - "@aws-cdk/aws-codecommit": "0.0.0", "@aws-cdk/aws-codebuild": "0.0.0", + "@aws-cdk/aws-codecommit": "0.0.0", "@aws-cdk/aws-codepipeline": "0.0.0", "@aws-cdk/aws-codepipeline-actions": "0.0.0", + "@aws-cdk/aws-ec2": "0.0.0", "@aws-cdk/aws-ecr": "0.0.0", "@aws-cdk/aws-events": "0.0.0", "@aws-cdk/aws-iam": "0.0.0", - "@aws-cdk/aws-ec2": "0.0.0", - "@aws-cdk/aws-secretsmanager": "0.0.0", - "@aws-cdk/cloud-assembly-schema": "0.0.0", + "@aws-cdk/aws-lambda": "0.0.0", "@aws-cdk/aws-s3": "0.0.0", "@aws-cdk/aws-s3-assets": "0.0.0", - "@aws-cdk/cx-api": "0.0.0" + "@aws-cdk/aws-secretsmanager": "0.0.0", + "@aws-cdk/aws-sns": "0.0.0", + "@aws-cdk/cloud-assembly-schema": "0.0.0", + "@aws-cdk/core": "0.0.0", + "@aws-cdk/cx-api": "0.0.0", + "constructs": "^3.3.69" }, "dependencies": { - "constructs": "^3.3.69", - "@aws-cdk/core": "0.0.0", - "@aws-cdk/aws-codecommit": "0.0.0", "@aws-cdk/aws-codebuild": "0.0.0", + "@aws-cdk/aws-codecommit": "0.0.0", "@aws-cdk/aws-codepipeline": "0.0.0", "@aws-cdk/aws-codepipeline-actions": "0.0.0", - "@aws-cdk/cloud-assembly-schema": "0.0.0", + "@aws-cdk/aws-ec2": "0.0.0", "@aws-cdk/aws-ecr": "0.0.0", "@aws-cdk/aws-events": "0.0.0", "@aws-cdk/aws-iam": "0.0.0", - "@aws-cdk/aws-ec2": "0.0.0", - "@aws-cdk/aws-secretsmanager": "0.0.0", + "@aws-cdk/aws-lambda": "0.0.0", "@aws-cdk/aws-s3": "0.0.0", "@aws-cdk/aws-s3-assets": "0.0.0", - "@aws-cdk/cx-api": "0.0.0" + "@aws-cdk/aws-secretsmanager": "0.0.0", + "@aws-cdk/aws-sns": "0.0.0", + "@aws-cdk/cloud-assembly-schema": "0.0.0", + "@aws-cdk/core": "0.0.0", + "@aws-cdk/cx-api": "0.0.0", + "constructs": "^3.3.69" }, - "bundledDependencies": [], "keywords": [ "aws", "cdk", diff --git a/packages/@aws-cdk/pipelines/test/compliance/security-check.test.ts b/packages/@aws-cdk/pipelines/test/compliance/security-check.test.ts new file mode 100644 index 0000000000000..e698f355b8609 --- /dev/null +++ b/packages/@aws-cdk/pipelines/test/compliance/security-check.test.ts @@ -0,0 +1,362 @@ +import { arrayWith, objectLike, stringLike } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; +import { Topic } from '@aws-cdk/aws-sns'; +import { Stack } from '@aws-cdk/core'; +import * as cdkp from '../../lib'; +import { LegacyTestGitHubNpmPipeline, ModernTestGitHubNpmPipeline, OneStackApp, PIPELINE_ENV, TestApp } from '../testhelpers'; +import { behavior } from '../testhelpers/compliance'; + +let app: TestApp; +let pipelineStack: Stack; + +beforeEach(() => { + app = new TestApp(); + pipelineStack = new Stack(app, 'PipelineSecurityStack', { env: PIPELINE_ENV }); +}); + +afterEach(() => { + app.cleanup(); +}); + +behavior('security check option generates lambda/codebuild at pipeline scope', (suite) => { + suite.legacy(() => { + const pipeline = new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk'); + pipeline.addApplicationStage(new OneStackApp(app, 'App'), { confirmBroadeningPermissions: true }); + + THEN_codePipelineExpectation(); + }); + + suite.modern(() => { + const pipeline = new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk'); + const stage = new OneStackApp(app, 'App'); + pipeline.addStage(stage, { + pre: [ + new cdkp.ConfirmPermissionsBroadening('Check', { + stage, + }), + ], + }); + + THEN_codePipelineExpectation(); + }); + + function THEN_codePipelineExpectation() { + expect(pipelineStack).toCountResources('AWS::Lambda::Function', 1); + expect(pipelineStack).toHaveResourceLike('AWS::Lambda::Function', { + Role: { + 'Fn::GetAtt': [ + stringLike('CdkPipeline*SecurityCheckCDKPipelinesAutoApproveServiceRole*'), + 'Arn', + ], + }, + }); + // 1 for github build, 1 for synth stage, and 1 for the application security check + expect(pipelineStack).toCountResources('AWS::CodeBuild::Project', 3); + } +}); + +behavior('pipeline created with auto approve tags and lambda/codebuild w/ valid permissions', (suite) => { + suite.legacy(() => { + const pipeline = new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk'); + pipeline.addApplicationStage(new OneStackApp(app, 'App'), { confirmBroadeningPermissions: true }); + + THEN_codePipelineExpectation(); + }); + + suite.modern(() => { + const pipeline = new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk'); + const stage = new OneStackApp(app, 'App'); + pipeline.addStage(stage, { + pre: [ + new cdkp.ConfirmPermissionsBroadening('Check', { + stage, + }), + ], + }); + + THEN_codePipelineExpectation(); + }); + + function THEN_codePipelineExpectation() { + // CodePipeline must be tagged as SECURITY_CHECK=ALLOW_APPROVE + expect(pipelineStack).toHaveResource('AWS::CodePipeline::Pipeline', { + Tags: [ + { + Key: 'SECURITY_CHECK', + Value: 'ALLOW_APPROVE', + }, + ], + }); + // Lambda Function only has access to pipelines tagged SECURITY_CHECK=ALLOW_APPROVE + expect(pipelineStack).toHaveResourceLike('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: ['codepipeline:GetPipelineState', 'codepipeline:PutApprovalResult'], + Condition: { + StringEquals: { 'aws:ResourceTag/SECURITY_CHECK': 'ALLOW_APPROVE' }, + }, + Effect: 'Allow', + Resource: '*', + }, + ], + }, + }); + // CodeBuild must have access to the stacks and invoking the lambda function + expect(pipelineStack).toHaveResourceLike('AWS::IAM::Policy', { + PolicyDocument: { + Statement: arrayWith( + { + Action: 'sts:AssumeRole', + Condition: { + 'ForAnyValue:StringEquals': { + 'iam:ResourceTag/aws-cdk:bootstrap-role': [ + 'deploy', + ], + }, + }, + Effect: 'Allow', + Resource: '*', + }, + { + Action: 'lambda:InvokeFunction', + Effect: 'Allow', + Resource: { + 'Fn::GetAtt': [ + stringLike('*AutoApprove*'), + 'Arn', + ], + }, + }, + ), + }, + }); + } +}); + +behavior('confirmBroadeningPermissions option at addApplicationStage runs security check on all apps unless overriden', (suite) => { + suite.legacy(() => { + const pipeline = new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk'); + const securityStage = pipeline.addApplicationStage(new OneStackApp(app, 'StageSecurityCheckStack'), { confirmBroadeningPermissions: true }); + securityStage.addApplication(new OneStackApp(app, 'AnotherStack')); + securityStage.addApplication(new OneStackApp(app, 'SkipCheckStack'), { confirmBroadeningPermissions: false }); + + THEN_codePipelineExpectation(); + }); + + // For the modern API, there is no inheritance + suite.doesNotApply.modern(); + + function THEN_codePipelineExpectation() { + expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Stages: [ + { + Actions: [{ Name: 'GitHub', RunOrder: 1 }], + Name: 'Source', + }, + { + Actions: [{ Name: 'Synth', RunOrder: 1 }], + Name: 'Build', + }, + { + Actions: [{ Name: 'SelfMutate', RunOrder: 1 }], + Name: 'UpdatePipeline', + }, + { + Actions: [ + { Name: 'StageSecurityCheckStackSecurityCheck', RunOrder: 1 }, + { Name: 'StageSecurityCheckStackManualApproval', RunOrder: 2 }, + { Name: 'AnotherStackSecurityCheck', RunOrder: 5 }, + { Name: 'AnotherStackManualApproval', RunOrder: 6 }, + { Name: 'Stack.Prepare', RunOrder: 3 }, + { Name: 'Stack.Deploy', RunOrder: 4 }, + { Name: 'AnotherStack-Stack.Prepare', RunOrder: 7 }, + { Name: 'AnotherStack-Stack.Deploy', RunOrder: 8 }, + { Name: 'SkipCheckStack-Stack.Prepare', RunOrder: 9 }, + { Name: 'SkipCheckStack-Stack.Deploy', RunOrder: 10 }, + ], + Name: 'StageSecurityCheckStack', + }, + ], + }); + } +}); + +behavior('confirmBroadeningPermissions option at addApplication runs security check only on selected application', (suite) => { + suite.legacy(() => { + const pipeline = new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk'); + const noSecurityStage = pipeline.addApplicationStage(new OneStackApp(app, 'NoSecurityCheckStack')); + noSecurityStage.addApplication(new OneStackApp(app, 'EnableCheckStack'), { confirmBroadeningPermissions: true }); + + THEN_codePipelineExpectation(); + }); + + // For the modern API, there is no inheritance + suite.doesNotApply.modern(); + + function THEN_codePipelineExpectation() { + expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Stages: [ + { + Actions: [{ Name: 'GitHub', RunOrder: 1 }], + Name: 'Source', + }, + { + Actions: [{ Name: 'Synth', RunOrder: 1 }], + Name: 'Build', + }, + { + Actions: [{ Name: 'SelfMutate', RunOrder: 1 }], + Name: 'UpdatePipeline', + }, + { + Actions: [ + { Name: 'EnableCheckStackSecurityCheck', RunOrder: 3 }, + { Name: 'EnableCheckStackManualApproval', RunOrder: 4 }, + { Name: 'Stack.Prepare', RunOrder: 1 }, + { Name: 'Stack.Deploy', RunOrder: 2 }, + { Name: 'EnableCheckStack-Stack.Prepare', RunOrder: 5 }, + { Name: 'EnableCheckStack-Stack.Deploy', RunOrder: 6 }, + ], + Name: 'NoSecurityCheckStack', + }, + ], + }); + } +}); + +behavior('confirmBroadeningPermissions and notification topic options generates the right resources', (suite) => { + suite.legacy(() => { + const pipeline = new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk'); + const topic = new Topic(pipelineStack, 'NotificationTopic'); + pipeline.addApplicationStage(new OneStackApp(app, 'MyStack'), { + confirmBroadeningPermissions: true, + securityNotificationTopic: topic, + }); + + THEN_codePipelineExpectation(); + }); + + suite.modern(() => { + const pipeline = new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk'); + const topic = new Topic(pipelineStack, 'NotificationTopic'); + const stage = new OneStackApp(app, 'MyStack'); + pipeline.addStage(stage, { + pre: [ + new cdkp.ConfirmPermissionsBroadening('Approve', { + stage, + notificationTopic: topic, + }), + ], + }); + + THEN_codePipelineExpectation(); + }); + + function THEN_codePipelineExpectation() { + expect(pipelineStack).toCountResources('AWS::SNS::Topic', 1); + expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Stages: arrayWith( + { + Name: 'MyStack', + Actions: [ + objectLike({ + Configuration: { + ProjectName: { Ref: stringLike('*SecurityCheck*') }, + EnvironmentVariables: { + 'Fn::Join': ['', [ + stringLike('*'), + { Ref: 'NotificationTopicEB7A0DF1' }, + stringLike('*'), + ]], + }, + }, + Name: stringLike('*Check'), + Namespace: stringLike('*'), + RunOrder: 1, + }), + objectLike({ + Configuration: { + CustomData: stringLike('#{*.MESSAGE}'), + ExternalEntityLink: stringLike('#{*.LINK}'), + }, + Name: stringLike('*Approv*'), + RunOrder: 2, + }), + objectLike({ Name: 'Stack.Prepare', RunOrder: 3 }), + objectLike({ Name: 'Stack.Deploy', RunOrder: 4 }), + ], + }, + ), + }); + } +}); + +behavior('Stages declared outside the pipeline create their own ApplicationSecurityCheck', (suite) => { + suite.legacy(() => { + const pipeline = new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk'); + const pipelineStage = pipeline.codePipeline.addStage({ + stageName: 'UnattachedStage', + }); + + const unattachedStage = new cdkp.CdkStage(pipelineStack, 'UnattachedStage', { + stageName: 'UnattachedStage', + pipelineStage, + cloudAssemblyArtifact: pipeline.cloudAssemblyArtifact, + host: { + publishAsset: () => undefined, + stackOutputArtifact: () => undefined, + }, + }); + + unattachedStage.addApplication(new OneStackApp(app, 'UnattachedStage'), { + confirmBroadeningPermissions: true, + }); + + THEN_codePipelineExpectation(); + }); + + // Not a valid use of the modern API + suite.doesNotApply.modern(); + + function THEN_codePipelineExpectation() { + expect(pipelineStack).toCountResources('AWS::Lambda::Function', 1); + // 1 for github build, 1 for synth stage, and 1 for the application security check + expect(pipelineStack).toCountResources('AWS::CodeBuild::Project', 3); + expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Tags: [ + { + Key: 'SECURITY_CHECK', + Value: 'ALLOW_APPROVE', + }, + ], + Stages: [ + { Name: 'Source' }, + { Name: 'Build' }, + { Name: 'UpdatePipeline' }, + { + Actions: [ + { + Configuration: { + ProjectName: { Ref: 'UnattachedStageStageApplicationSecurityCheckCDKSecurityCheckADCE795B' }, + }, + Name: 'UnattachedStageSecurityCheck', + RunOrder: 1, + }, + { + Configuration: { + CustomData: '#{UnattachedStageSecurityCheck.MESSAGE}', + ExternalEntityLink: '#{UnattachedStageSecurityCheck.LINK}', + }, + Name: 'UnattachedStageManualApproval', + RunOrder: 2, + }, + { Name: 'Stack.Prepare', RunOrder: 3 }, + { Name: 'Stack.Deploy', RunOrder: 4 }, + ], + Name: 'UnattachedStage', + }, + ], + }); + } +}); \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/test/integ.pipeline-security.expected.json b/packages/@aws-cdk/pipelines/test/integ.pipeline-security.expected.json new file mode 100644 index 0000000000000..3495d780cecdd --- /dev/null +++ b/packages/@aws-cdk/pipelines/test/integ.pipeline-security.expected.json @@ -0,0 +1,2399 @@ +{ + "Resources": { + "TestPipelineArtifactsBucketEncryptionKey13258842": { + "Type": "AWS::KMS::Key", + "Properties": { + "KeyPolicy": { + "Statement": [ + { + "Action": "kms:*", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::12345678:root" + ] + ] + } + }, + "Resource": "*" + }, + { + "Action": [ + "kms:Decrypt", + "kms:DescribeKey" + ], + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::12345678:role/cdk-hnb659fds-deploy-role-12345678-test-region" + ] + ] + } + }, + "Resource": "*" + } + ], + "Version": "2012-10-17" + } + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "TestPipelineArtifactsBucketEncryptionKeyAliasE8D86DD3": { + "Type": "AWS::KMS::Alias", + "Properties": { + "AliasName": "alias/codepipeline-pipelinesecuritystacktestpipelinef7060861", + "TargetKeyId": { + "Fn::GetAtt": [ + "TestPipelineArtifactsBucketEncryptionKey13258842", + "Arn" + ] + } + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "TestPipelineArtifactsBucket026AF2F9": { + "Type": "AWS::S3::Bucket", + "Properties": { + "BucketEncryption": { + "ServerSideEncryptionConfiguration": [ + { + "ServerSideEncryptionByDefault": { + "KMSMasterKeyID": { + "Fn::GetAtt": [ + "TestPipelineArtifactsBucketEncryptionKey13258842", + "Arn" + ] + }, + "SSEAlgorithm": "aws:kms" + } + } + ] + }, + "PublicAccessBlockConfiguration": { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true + } + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "TestPipelineArtifactsBucketPolicyDF75C611": { + "Type": "AWS::S3::BucketPolicy", + "Properties": { + "Bucket": { + "Ref": "TestPipelineArtifactsBucket026AF2F9" + }, + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*" + ], + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::12345678:role/cdk-hnb659fds-deploy-role-12345678-test-region" + ] + ] + } + }, + "Resource": [ + { + "Fn::GetAtt": [ + "TestPipelineArtifactsBucket026AF2F9", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "TestPipelineArtifactsBucket026AF2F9", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + } + } + }, + "TestPipelineRole63C35BBD": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "codepipeline.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "TestPipelineRoleDefaultPolicyFA69BF2D": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*", + "s3:DeleteObject*", + "s3:PutObject", + "s3:Abort*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "TestPipelineArtifactsBucket026AF2F9", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "TestPipelineArtifactsBucket026AF2F9", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + }, + { + "Action": [ + "kms:Decrypt", + "kms:DescribeKey", + "kms:Encrypt", + "kms:ReEncrypt*", + "kms:GenerateDataKey*" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "TestPipelineArtifactsBucketEncryptionKey13258842", + "Arn" + ] + } + }, + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "TestPipelineBuildSynthCodePipelineActionRoleF7BF5926", + "Arn" + ] + } + }, + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "TestPipelineUnattachedStageSingleStageSecurityCheckCodePipelineActionRoleFF6E43E2", + "Arn" + ] + } + }, + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "TestPipelineUnattachedStageSingleStageManualApprovalCodePipelineActionRoleF7A614C8", + "Arn" + ] + } + }, + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "TestPipelinePreProductionPreProductionSecurityCheckCodePipelineActionRole4E54C194", + "Arn" + ] + } + }, + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "TestPipelinePreProductionPreProductionManualApprovalCodePipelineActionRole81B9C4F9", + "Arn" + ] + } + }, + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "TestPipelinePreProductionSafeProductionSecurityCheckCodePipelineActionRole399C68A6", + "Arn" + ] + } + }, + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "TestPipelinePreProductionSafeProductionManualApprovalCodePipelineActionRole4F30C0D9", + "Arn" + ] + } + }, + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "TestPipelineNoSecurityCheckEnableSecurityCheckSecurityCheckCodePipelineActionRole8D10AA6D", + "Arn" + ] + } + }, + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "TestPipelineNoSecurityCheckEnableSecurityCheckManualApprovalCodePipelineActionRole27FC4015", + "Arn" + ] + } + }, + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::12345678:role/cdk-hnb659fds-deploy-role-12345678-test-region" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "TestPipelineRoleDefaultPolicyFA69BF2D", + "Roles": [ + { + "Ref": "TestPipelineRole63C35BBD" + } + ] + } + }, + "TestPipeline34ACDBF9": { + "Type": "AWS::CodePipeline::Pipeline", + "Properties": { + "RoleArn": { + "Fn::GetAtt": [ + "TestPipelineRole63C35BBD", + "Arn" + ] + }, + "Stages": [ + { + "Actions": [ + { + "ActionTypeId": { + "Category": "Source", + "Owner": "ThirdParty", + "Provider": "GitHub", + "Version": "1" + }, + "Configuration": { + "Owner": "OWNER", + "Repo": "REPO", + "Branch": "master", + "OAuthToken": "not-a-secret", + "PollForSourceChanges": true + }, + "Name": "GitHub", + "OutputArtifacts": [ + { + "Name": "Artifact_Source_GitHub" + } + ], + "RunOrder": 1 + } + ], + "Name": "Source" + }, + { + "Actions": [ + { + "ActionTypeId": { + "Category": "Build", + "Owner": "AWS", + "Provider": "CodeBuild", + "Version": "1" + }, + "Configuration": { + "ProjectName": { + "Ref": "TestPipelineBuildSynthCdkBuildProject755D4B01" + }, + "EnvironmentVariables": "[{\"name\":\"_PROJECT_CONFIG_HASH\",\"type\":\"PLAINTEXT\",\"value\":\"fade37e243023bb2c0d6730c10a2c61567fbe168675a7c5e26a8810aadc7e513\"}]" + }, + "InputArtifacts": [ + { + "Name": "Artifact_Source_GitHub" + } + ], + "Name": "Synth", + "OutputArtifacts": [ + { + "Name": "CloudAsm" + } + ], + "RoleArn": { + "Fn::GetAtt": [ + "TestPipelineBuildSynthCodePipelineActionRoleF7BF5926", + "Arn" + ] + }, + "RunOrder": 1 + } + ], + "Name": "Build" + }, + { + "Actions": [ + { + "ActionTypeId": { + "Category": "Build", + "Owner": "AWS", + "Provider": "CodeBuild", + "Version": "1" + }, + "Configuration": { + "ProjectName": { + "Ref": "UnattachedStageStageApplicationSecurityCheckCDKSecurityCheckADCE795B" + }, + "EnvironmentVariables": { + "Fn::Join": [ + "", + [ + "[{\"name\":\"STAGE_PATH\",\"type\":\"PLAINTEXT\",\"value\":\"PipelineSecurityStack\"},{\"name\":\"STAGE_NAME\",\"type\":\"PLAINTEXT\",\"value\":\"UnattachedStage\"},{\"name\":\"ACTION_NAME\",\"type\":\"PLAINTEXT\",\"value\":\"SingleStageManualApproval\"},{\"name\":\"NOTIFICATION_ARN\",\"type\":\"PLAINTEXT\",\"value\":\"", + { + "Ref": "SecurityChangesTopic9762A9B3" + }, + "\"},{\"name\":\"NOTIFICATION_SUBJECT\",\"type\":\"PLAINTEXT\",\"value\":\"Confirm permission broadening in SingleStage\"}]" + ] + ] + } + }, + "InputArtifacts": [ + { + "Name": "CloudAsm" + } + ], + "Name": "SingleStageSecurityCheck", + "Namespace": "SingleStageSecurityCheck", + "RoleArn": { + "Fn::GetAtt": [ + "TestPipelineUnattachedStageSingleStageSecurityCheckCodePipelineActionRoleFF6E43E2", + "Arn" + ] + }, + "RunOrder": 1 + }, + { + "ActionTypeId": { + "Category": "Approval", + "Owner": "AWS", + "Provider": "Manual", + "Version": "1" + }, + "Configuration": { + "CustomData": "#{SingleStageSecurityCheck.MESSAGE}", + "ExternalEntityLink": "#{SingleStageSecurityCheck.LINK}" + }, + "Name": "SingleStageManualApproval", + "RoleArn": { + "Fn::GetAtt": [ + "TestPipelineUnattachedStageSingleStageManualApprovalCodePipelineActionRoleF7A614C8", + "Arn" + ] + }, + "RunOrder": 2 + }, + { + "ActionTypeId": { + "Category": "Deploy", + "Owner": "AWS", + "Provider": "CloudFormation", + "Version": "1" + }, + "Configuration": { + "StackName": "SingleStage-MyStack", + "Capabilities": "CAPABILITY_NAMED_IAM,CAPABILITY_AUTO_EXPAND", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::12345678:role/cdk-hnb659fds-cfn-exec-role-12345678-test-region" + ] + ] + }, + "ActionMode": "CHANGE_SET_REPLACE", + "ChangeSetName": "PipelineChange", + "TemplatePath": "CloudAsm::assembly-PipelineSecurityStack-SingleStage/PipelineSecurityStackSingleStageMyStack29962269.template.json" + }, + "InputArtifacts": [ + { + "Name": "CloudAsm" + } + ], + "Name": "SingleStage-MyStack.Prepare", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::12345678:role/cdk-hnb659fds-deploy-role-12345678-test-region" + ] + ] + }, + "RunOrder": 3 + }, + { + "ActionTypeId": { + "Category": "Deploy", + "Owner": "AWS", + "Provider": "CloudFormation", + "Version": "1" + }, + "Configuration": { + "StackName": "SingleStage-MyStack", + "ActionMode": "CHANGE_SET_EXECUTE", + "ChangeSetName": "PipelineChange" + }, + "Name": "SingleStage-MyStack.Deploy", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::12345678:role/cdk-hnb659fds-deploy-role-12345678-test-region" + ] + ] + }, + "RunOrder": 4 + } + ], + "Name": "UnattachedStage" + }, + { + "Actions": [ + { + "ActionTypeId": { + "Category": "Build", + "Owner": "AWS", + "Provider": "CodeBuild", + "Version": "1" + }, + "Configuration": { + "ProjectName": { + "Ref": "TestPipelinePipelineApplicationSecurityCheckCDKSecurityCheckBEE4547C" + }, + "EnvironmentVariables": { + "Fn::Join": [ + "", + [ + "[{\"name\":\"STAGE_PATH\",\"type\":\"PLAINTEXT\",\"value\":\"PipelineSecurityStack\"},{\"name\":\"STAGE_NAME\",\"type\":\"PLAINTEXT\",\"value\":\"PreProduction\"},{\"name\":\"ACTION_NAME\",\"type\":\"PLAINTEXT\",\"value\":\"PreProductionManualApproval\"},{\"name\":\"NOTIFICATION_ARN\",\"type\":\"PLAINTEXT\",\"value\":\"", + { + "Ref": "SecurityChangesTopic9762A9B3" + }, + "\"},{\"name\":\"NOTIFICATION_SUBJECT\",\"type\":\"PLAINTEXT\",\"value\":\"Confirm permission broadening in PreProduction\"}]" + ] + ] + } + }, + "InputArtifacts": [ + { + "Name": "CloudAsm" + } + ], + "Name": "PreProductionSecurityCheck", + "Namespace": "PreProductionSecurityCheck", + "RoleArn": { + "Fn::GetAtt": [ + "TestPipelinePreProductionPreProductionSecurityCheckCodePipelineActionRole4E54C194", + "Arn" + ] + }, + "RunOrder": 1 + }, + { + "ActionTypeId": { + "Category": "Approval", + "Owner": "AWS", + "Provider": "Manual", + "Version": "1" + }, + "Configuration": { + "CustomData": "#{PreProductionSecurityCheck.MESSAGE}", + "ExternalEntityLink": "#{PreProductionSecurityCheck.LINK}" + }, + "Name": "PreProductionManualApproval", + "RoleArn": { + "Fn::GetAtt": [ + "TestPipelinePreProductionPreProductionManualApprovalCodePipelineActionRole81B9C4F9", + "Arn" + ] + }, + "RunOrder": 2 + }, + { + "ActionTypeId": { + "Category": "Build", + "Owner": "AWS", + "Provider": "CodeBuild", + "Version": "1" + }, + "Configuration": { + "ProjectName": { + "Ref": "TestPipelinePipelineApplicationSecurityCheckCDKSecurityCheckBEE4547C" + }, + "EnvironmentVariables": { + "Fn::Join": [ + "", + [ + "[{\"name\":\"STAGE_PATH\",\"type\":\"PLAINTEXT\",\"value\":\"PipelineSecurityStack\"},{\"name\":\"STAGE_NAME\",\"type\":\"PLAINTEXT\",\"value\":\"PreProduction\"},{\"name\":\"ACTION_NAME\",\"type\":\"PLAINTEXT\",\"value\":\"SafeProductionManualApproval\"},{\"name\":\"NOTIFICATION_ARN\",\"type\":\"PLAINTEXT\",\"value\":\"", + { + "Ref": "SecurityChangesTopic9762A9B3" + }, + "\"},{\"name\":\"NOTIFICATION_SUBJECT\",\"type\":\"PLAINTEXT\",\"value\":\"Confirm permission broadening in SafeProduction\"}]" + ] + ] + } + }, + "InputArtifacts": [ + { + "Name": "CloudAsm" + } + ], + "Name": "SafeProductionSecurityCheck", + "Namespace": "SafeProductionSecurityCheck", + "RoleArn": { + "Fn::GetAtt": [ + "TestPipelinePreProductionSafeProductionSecurityCheckCodePipelineActionRole399C68A6", + "Arn" + ] + }, + "RunOrder": 5 + }, + { + "ActionTypeId": { + "Category": "Approval", + "Owner": "AWS", + "Provider": "Manual", + "Version": "1" + }, + "Configuration": { + "CustomData": "#{SafeProductionSecurityCheck.MESSAGE}", + "ExternalEntityLink": "#{SafeProductionSecurityCheck.LINK}" + }, + "Name": "SafeProductionManualApproval", + "RoleArn": { + "Fn::GetAtt": [ + "TestPipelinePreProductionSafeProductionManualApprovalCodePipelineActionRole4F30C0D9", + "Arn" + ] + }, + "RunOrder": 6 + }, + { + "ActionTypeId": { + "Category": "Deploy", + "Owner": "AWS", + "Provider": "CloudFormation", + "Version": "1" + }, + "Configuration": { + "StackName": "PreProduction-MyStack", + "Capabilities": "CAPABILITY_NAMED_IAM,CAPABILITY_AUTO_EXPAND", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::12345678:role/cdk-hnb659fds-cfn-exec-role-12345678-test-region" + ] + ] + }, + "ActionMode": "CHANGE_SET_REPLACE", + "ChangeSetName": "PipelineChange", + "TemplatePath": "CloudAsm::assembly-PipelineSecurityStack-PreProduction/PipelineSecurityStackPreProductionMyStackDCCBB4EA.template.json" + }, + "InputArtifacts": [ + { + "Name": "CloudAsm" + } + ], + "Name": "MyStack.Prepare", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::12345678:role/cdk-hnb659fds-deploy-role-12345678-test-region" + ] + ] + }, + "RunOrder": 3 + }, + { + "ActionTypeId": { + "Category": "Deploy", + "Owner": "AWS", + "Provider": "CloudFormation", + "Version": "1" + }, + "Configuration": { + "StackName": "PreProduction-MyStack", + "ActionMode": "CHANGE_SET_EXECUTE", + "ChangeSetName": "PipelineChange" + }, + "Name": "MyStack.Deploy", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::12345678:role/cdk-hnb659fds-deploy-role-12345678-test-region" + ] + ] + }, + "RunOrder": 4 + }, + { + "ActionTypeId": { + "Category": "Deploy", + "Owner": "AWS", + "Provider": "CloudFormation", + "Version": "1" + }, + "Configuration": { + "StackName": "SafeProduction-MySafeStack", + "Capabilities": "CAPABILITY_NAMED_IAM,CAPABILITY_AUTO_EXPAND", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::12345678:role/cdk-hnb659fds-cfn-exec-role-12345678-test-region" + ] + ] + }, + "ActionMode": "CHANGE_SET_REPLACE", + "ChangeSetName": "PipelineChange", + "TemplatePath": "CloudAsm::assembly-PipelineSecurityStack-SafeProduction/PipelineSecurityStackSafeProductionMySafeStackC0D87904.template.json" + }, + "InputArtifacts": [ + { + "Name": "CloudAsm" + } + ], + "Name": "SafeProduction-MySafeStack.Prepare", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::12345678:role/cdk-hnb659fds-deploy-role-12345678-test-region" + ] + ] + }, + "RunOrder": 7 + }, + { + "ActionTypeId": { + "Category": "Deploy", + "Owner": "AWS", + "Provider": "CloudFormation", + "Version": "1" + }, + "Configuration": { + "StackName": "SafeProduction-MySafeStack", + "ActionMode": "CHANGE_SET_EXECUTE", + "ChangeSetName": "PipelineChange" + }, + "Name": "SafeProduction-MySafeStack.Deploy", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::12345678:role/cdk-hnb659fds-deploy-role-12345678-test-region" + ] + ] + }, + "RunOrder": 8 + }, + { + "ActionTypeId": { + "Category": "Deploy", + "Owner": "AWS", + "Provider": "CloudFormation", + "Version": "1" + }, + "Configuration": { + "StackName": "DisableSecurityCheck-MySafeStack", + "Capabilities": "CAPABILITY_NAMED_IAM,CAPABILITY_AUTO_EXPAND", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::12345678:role/cdk-hnb659fds-cfn-exec-role-12345678-test-region" + ] + ] + }, + "ActionMode": "CHANGE_SET_REPLACE", + "ChangeSetName": "PipelineChange", + "TemplatePath": "CloudAsm::assembly-PipelineSecurityStack-DisableSecurityCheck/PipelineSecurityStackDisableSecurityCheckMySafeStack7A4F8E95.template.json" + }, + "InputArtifacts": [ + { + "Name": "CloudAsm" + } + ], + "Name": "DisableSecurityCheck-MySafeStack.Prepare", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::12345678:role/cdk-hnb659fds-deploy-role-12345678-test-region" + ] + ] + }, + "RunOrder": 9 + }, + { + "ActionTypeId": { + "Category": "Deploy", + "Owner": "AWS", + "Provider": "CloudFormation", + "Version": "1" + }, + "Configuration": { + "StackName": "DisableSecurityCheck-MySafeStack", + "ActionMode": "CHANGE_SET_EXECUTE", + "ChangeSetName": "PipelineChange" + }, + "Name": "DisableSecurityCheck-MySafeStack.Deploy", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::12345678:role/cdk-hnb659fds-deploy-role-12345678-test-region" + ] + ] + }, + "RunOrder": 10 + } + ], + "Name": "PreProduction" + }, + { + "Actions": [ + { + "ActionTypeId": { + "Category": "Build", + "Owner": "AWS", + "Provider": "CodeBuild", + "Version": "1" + }, + "Configuration": { + "ProjectName": { + "Ref": "TestPipelinePipelineApplicationSecurityCheckCDKSecurityCheckBEE4547C" + }, + "EnvironmentVariables": "[{\"name\":\"STAGE_PATH\",\"type\":\"PLAINTEXT\",\"value\":\"PipelineSecurityStack\"},{\"name\":\"STAGE_NAME\",\"type\":\"PLAINTEXT\",\"value\":\"NoSecurityCheck\"},{\"name\":\"ACTION_NAME\",\"type\":\"PLAINTEXT\",\"value\":\"EnableSecurityCheckManualApproval\"}]" + }, + "InputArtifacts": [ + { + "Name": "CloudAsm" + } + ], + "Name": "EnableSecurityCheckSecurityCheck", + "Namespace": "EnableSecurityCheckSecurityCheck", + "RoleArn": { + "Fn::GetAtt": [ + "TestPipelineNoSecurityCheckEnableSecurityCheckSecurityCheckCodePipelineActionRole8D10AA6D", + "Arn" + ] + }, + "RunOrder": 3 + }, + { + "ActionTypeId": { + "Category": "Approval", + "Owner": "AWS", + "Provider": "Manual", + "Version": "1" + }, + "Configuration": { + "CustomData": "#{EnableSecurityCheckSecurityCheck.MESSAGE}", + "ExternalEntityLink": "#{EnableSecurityCheckSecurityCheck.LINK}" + }, + "Name": "EnableSecurityCheckManualApproval", + "RoleArn": { + "Fn::GetAtt": [ + "TestPipelineNoSecurityCheckEnableSecurityCheckManualApprovalCodePipelineActionRole27FC4015", + "Arn" + ] + }, + "RunOrder": 4 + }, + { + "ActionTypeId": { + "Category": "Deploy", + "Owner": "AWS", + "Provider": "CloudFormation", + "Version": "1" + }, + "Configuration": { + "StackName": "NoSecurityCheck-MyStack", + "Capabilities": "CAPABILITY_NAMED_IAM,CAPABILITY_AUTO_EXPAND", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::12345678:role/cdk-hnb659fds-cfn-exec-role-12345678-test-region" + ] + ] + }, + "ActionMode": "CHANGE_SET_REPLACE", + "ChangeSetName": "PipelineChange", + "TemplatePath": "CloudAsm::assembly-PipelineSecurityStack-NoSecurityCheck/PipelineSecurityStackNoSecurityCheckMyStack3484019E.template.json" + }, + "InputArtifacts": [ + { + "Name": "CloudAsm" + } + ], + "Name": "MyStack.Prepare", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::12345678:role/cdk-hnb659fds-deploy-role-12345678-test-region" + ] + ] + }, + "RunOrder": 1 + }, + { + "ActionTypeId": { + "Category": "Deploy", + "Owner": "AWS", + "Provider": "CloudFormation", + "Version": "1" + }, + "Configuration": { + "StackName": "NoSecurityCheck-MyStack", + "ActionMode": "CHANGE_SET_EXECUTE", + "ChangeSetName": "PipelineChange" + }, + "Name": "MyStack.Deploy", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::12345678:role/cdk-hnb659fds-deploy-role-12345678-test-region" + ] + ] + }, + "RunOrder": 2 + }, + { + "ActionTypeId": { + "Category": "Deploy", + "Owner": "AWS", + "Provider": "CloudFormation", + "Version": "1" + }, + "Configuration": { + "StackName": "EnableSecurityCheck-MyStack", + "Capabilities": "CAPABILITY_NAMED_IAM,CAPABILITY_AUTO_EXPAND", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::12345678:role/cdk-hnb659fds-cfn-exec-role-12345678-test-region" + ] + ] + }, + "ActionMode": "CHANGE_SET_REPLACE", + "ChangeSetName": "PipelineChange", + "TemplatePath": "CloudAsm::assembly-PipelineSecurityStack-EnableSecurityCheck/PipelineSecurityStackEnableSecurityCheckMyStack0B9FE272.template.json" + }, + "InputArtifacts": [ + { + "Name": "CloudAsm" + } + ], + "Name": "EnableSecurityCheck-MyStack.Prepare", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::12345678:role/cdk-hnb659fds-deploy-role-12345678-test-region" + ] + ] + }, + "RunOrder": 5 + }, + { + "ActionTypeId": { + "Category": "Deploy", + "Owner": "AWS", + "Provider": "CloudFormation", + "Version": "1" + }, + "Configuration": { + "StackName": "EnableSecurityCheck-MyStack", + "ActionMode": "CHANGE_SET_EXECUTE", + "ChangeSetName": "PipelineChange" + }, + "Name": "EnableSecurityCheck-MyStack.Deploy", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::12345678:role/cdk-hnb659fds-deploy-role-12345678-test-region" + ] + ] + }, + "RunOrder": 6 + } + ], + "Name": "NoSecurityCheck" + } + ], + "ArtifactStore": { + "EncryptionKey": { + "Id": { + "Fn::GetAtt": [ + "TestPipelineArtifactsBucketEncryptionKey13258842", + "Arn" + ] + }, + "Type": "KMS" + }, + "Location": { + "Ref": "TestPipelineArtifactsBucket026AF2F9" + }, + "Type": "S3" + }, + "Name": "TestPipeline", + "RestartExecutionOnUpdate": true, + "Tags": [ + { + "Key": "SECURITY_CHECK", + "Value": "ALLOW_APPROVE" + } + ] + }, + "DependsOn": [ + "TestPipelineRoleDefaultPolicyFA69BF2D", + "TestPipelineRole63C35BBD" + ] + }, + "TestPipelineBuildSynthCodePipelineActionRoleF7BF5926": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::12345678:root" + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "TestPipelineBuildSynthCodePipelineActionRoleDefaultPolicy65DF5C76": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "codebuild:BatchGetBuilds", + "codebuild:StartBuild", + "codebuild:StopBuild" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "TestPipelineBuildSynthCdkBuildProject755D4B01", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "TestPipelineBuildSynthCodePipelineActionRoleDefaultPolicy65DF5C76", + "Roles": [ + { + "Ref": "TestPipelineBuildSynthCodePipelineActionRoleF7BF5926" + } + ] + } + }, + "TestPipelineBuildSynthCdkBuildProjectRole4C6E5729": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "codebuild.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "TestPipelineBuildSynthCdkBuildProjectRoleDefaultPolicy73DC4481": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:test-region:12345678:log-group:/aws/codebuild/", + { + "Ref": "TestPipelineBuildSynthCdkBuildProject755D4B01" + } + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:test-region:12345678:log-group:/aws/codebuild/", + { + "Ref": "TestPipelineBuildSynthCdkBuildProject755D4B01" + }, + ":*" + ] + ] + } + ] + }, + { + "Action": [ + "codebuild:CreateReportGroup", + "codebuild:CreateReport", + "codebuild:UpdateReport", + "codebuild:BatchPutTestCases", + "codebuild:BatchPutCodeCoverages" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":codebuild:test-region:12345678:report-group/", + { + "Ref": "TestPipelineBuildSynthCdkBuildProject755D4B01" + }, + "-*" + ] + ] + } + }, + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*", + "s3:DeleteObject*", + "s3:PutObject", + "s3:Abort*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "TestPipelineArtifactsBucket026AF2F9", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "TestPipelineArtifactsBucket026AF2F9", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + }, + { + "Action": [ + "kms:Decrypt", + "kms:DescribeKey", + "kms:Encrypt", + "kms:ReEncrypt*", + "kms:GenerateDataKey*" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "TestPipelineArtifactsBucketEncryptionKey13258842", + "Arn" + ] + } + }, + { + "Action": [ + "kms:Decrypt", + "kms:Encrypt", + "kms:ReEncrypt*", + "kms:GenerateDataKey*" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "TestPipelineArtifactsBucketEncryptionKey13258842", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "TestPipelineBuildSynthCdkBuildProjectRoleDefaultPolicy73DC4481", + "Roles": [ + { + "Ref": "TestPipelineBuildSynthCdkBuildProjectRole4C6E5729" + } + ] + } + }, + "TestPipelineBuildSynthCdkBuildProject755D4B01": { + "Type": "AWS::CodeBuild::Project", + "Properties": { + "Artifacts": { + "Type": "CODEPIPELINE" + }, + "Environment": { + "ComputeType": "BUILD_GENERAL1_SMALL", + "EnvironmentVariables": [ + { + "Name": "NPM_CONFIG_UNSAFE_PERM", + "Type": "PLAINTEXT", + "Value": "true" + } + ], + "Image": "aws/codebuild/standard:5.0", + "ImagePullCredentialsType": "CODEBUILD", + "PrivilegedMode": false, + "Type": "LINUX_CONTAINER" + }, + "ServiceRole": { + "Fn::GetAtt": [ + "TestPipelineBuildSynthCdkBuildProjectRole4C6E5729", + "Arn" + ] + }, + "Source": { + "BuildSpec": "{\n \"version\": \"0.2\",\n \"phases\": {\n \"pre_build\": {\n \"commands\": [\n \"yarn install --frozen-lockfile\"\n ]\n },\n \"build\": {\n \"commands\": [\n \"yarn build\",\n \"npx cdk synth\"\n ]\n }\n },\n \"artifacts\": {\n \"base-directory\": \"cdk.out\",\n \"files\": \"**/*\"\n }\n}", + "Type": "CODEPIPELINE" + }, + "EncryptionKey": { + "Fn::GetAtt": [ + "TestPipelineArtifactsBucketEncryptionKey13258842", + "Arn" + ] + } + } + }, + "TestPipelineUnattachedStageSingleStageSecurityCheckCodePipelineActionRoleFF6E43E2": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::12345678:root" + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "TestPipelineUnattachedStageSingleStageSecurityCheckCodePipelineActionRoleDefaultPolicyFC737D71": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "codebuild:BatchGetBuilds", + "codebuild:StartBuild", + "codebuild:StopBuild" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "UnattachedStageStageApplicationSecurityCheckCDKSecurityCheckADCE795B", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "TestPipelineUnattachedStageSingleStageSecurityCheckCodePipelineActionRoleDefaultPolicyFC737D71", + "Roles": [ + { + "Ref": "TestPipelineUnattachedStageSingleStageSecurityCheckCodePipelineActionRoleFF6E43E2" + } + ] + } + }, + "TestPipelineUnattachedStageSingleStageManualApprovalCodePipelineActionRoleF7A614C8": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::12345678:root" + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "TestPipelinePreProductionPreProductionSecurityCheckCodePipelineActionRole4E54C194": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::12345678:root" + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "TestPipelinePreProductionPreProductionSecurityCheckCodePipelineActionRoleDefaultPolicy10D0864F": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "codebuild:BatchGetBuilds", + "codebuild:StartBuild", + "codebuild:StopBuild" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "TestPipelinePipelineApplicationSecurityCheckCDKSecurityCheckBEE4547C", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "TestPipelinePreProductionPreProductionSecurityCheckCodePipelineActionRoleDefaultPolicy10D0864F", + "Roles": [ + { + "Ref": "TestPipelinePreProductionPreProductionSecurityCheckCodePipelineActionRole4E54C194" + } + ] + } + }, + "TestPipelinePreProductionPreProductionManualApprovalCodePipelineActionRole81B9C4F9": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::12345678:root" + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "TestPipelinePreProductionSafeProductionSecurityCheckCodePipelineActionRole399C68A6": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::12345678:root" + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "TestPipelinePreProductionSafeProductionSecurityCheckCodePipelineActionRoleDefaultPolicyB836B566": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "codebuild:BatchGetBuilds", + "codebuild:StartBuild", + "codebuild:StopBuild" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "TestPipelinePipelineApplicationSecurityCheckCDKSecurityCheckBEE4547C", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "TestPipelinePreProductionSafeProductionSecurityCheckCodePipelineActionRoleDefaultPolicyB836B566", + "Roles": [ + { + "Ref": "TestPipelinePreProductionSafeProductionSecurityCheckCodePipelineActionRole399C68A6" + } + ] + } + }, + "TestPipelinePreProductionSafeProductionManualApprovalCodePipelineActionRole4F30C0D9": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::12345678:root" + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "TestPipelineNoSecurityCheckEnableSecurityCheckSecurityCheckCodePipelineActionRole8D10AA6D": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::12345678:root" + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "TestPipelineNoSecurityCheckEnableSecurityCheckSecurityCheckCodePipelineActionRoleDefaultPolicyE83A2CA1": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "codebuild:BatchGetBuilds", + "codebuild:StartBuild", + "codebuild:StopBuild" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "TestPipelinePipelineApplicationSecurityCheckCDKSecurityCheckBEE4547C", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "TestPipelineNoSecurityCheckEnableSecurityCheckSecurityCheckCodePipelineActionRoleDefaultPolicyE83A2CA1", + "Roles": [ + { + "Ref": "TestPipelineNoSecurityCheckEnableSecurityCheckSecurityCheckCodePipelineActionRole8D10AA6D" + } + ] + } + }, + "TestPipelineNoSecurityCheckEnableSecurityCheckManualApprovalCodePipelineActionRole27FC4015": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::12345678:root" + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "TestPipelinePipelineApplicationSecurityCheckCDKPipelinesAutoApproveServiceRole7594919D": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + } + }, + "TestPipelinePipelineApplicationSecurityCheckCDKPipelinesAutoApproveServiceRoleDefaultPolicyE47AE90F": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "codepipeline:GetPipelineState", + "codepipeline:PutApprovalResult" + ], + "Condition": { + "StringEquals": { + "aws:ResourceTag/SECURITY_CHECK": "ALLOW_APPROVE" + } + }, + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "TestPipelinePipelineApplicationSecurityCheckCDKPipelinesAutoApproveServiceRoleDefaultPolicyE47AE90F", + "Roles": [ + { + "Ref": "TestPipelinePipelineApplicationSecurityCheckCDKPipelinesAutoApproveServiceRole7594919D" + } + ] + } + }, + "TestPipelinePipelineApplicationSecurityCheckCDKPipelinesAutoApprove1EE0AA81": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "cdk-hnb659fds-assets-12345678-test-region", + "S3Key": "6c6c8f170c2cc5c6e35d90fe172fbc17cae75777b84707d58332dee79f444404.zip" + }, + "Role": { + "Fn::GetAtt": [ + "TestPipelinePipelineApplicationSecurityCheckCDKPipelinesAutoApproveServiceRole7594919D", + "Arn" + ] + }, + "Handler": "index.handler", + "Runtime": "nodejs14.x", + "Timeout": 300 + }, + "DependsOn": [ + "TestPipelinePipelineApplicationSecurityCheckCDKPipelinesAutoApproveServiceRoleDefaultPolicyE47AE90F", + "TestPipelinePipelineApplicationSecurityCheckCDKPipelinesAutoApproveServiceRole7594919D" + ] + }, + "TestPipelinePipelineApplicationSecurityCheckCDKSecurityCheckRoleA54CF050": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "codebuild.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "TestPipelinePipelineApplicationSecurityCheckCDKSecurityCheckRoleDefaultPolicyF2137052": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:test-region:12345678:log-group:/aws/codebuild/", + { + "Ref": "TestPipelinePipelineApplicationSecurityCheckCDKSecurityCheckBEE4547C" + } + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:test-region:12345678:log-group:/aws/codebuild/", + { + "Ref": "TestPipelinePipelineApplicationSecurityCheckCDKSecurityCheckBEE4547C" + }, + ":*" + ] + ] + } + ] + }, + { + "Action": [ + "codebuild:CreateReportGroup", + "codebuild:CreateReport", + "codebuild:UpdateReport", + "codebuild:BatchPutTestCases", + "codebuild:BatchPutCodeCoverages" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":codebuild:test-region:12345678:report-group/", + { + "Ref": "TestPipelinePipelineApplicationSecurityCheckCDKSecurityCheckBEE4547C" + }, + "-*" + ] + ] + } + }, + { + "Action": "sts:AssumeRole", + "Condition": { + "ForAnyValue:StringEquals": { + "iam:ResourceTag/aws-cdk:bootstrap-role": [ + "deploy" + ] + } + }, + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": "lambda:InvokeFunction", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "TestPipelinePipelineApplicationSecurityCheckCDKPipelinesAutoApprove1EE0AA81", + "Arn" + ] + } + }, + { + "Action": "sns:Publish", + "Effect": "Allow", + "Resource": { + "Ref": "SecurityChangesTopic9762A9B3" + } + }, + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "TestPipelineArtifactsBucket026AF2F9", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "TestPipelineArtifactsBucket026AF2F9", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + }, + { + "Action": [ + "kms:Decrypt", + "kms:DescribeKey" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "TestPipelineArtifactsBucketEncryptionKey13258842", + "Arn" + ] + } + }, + { + "Action": [ + "kms:Decrypt", + "kms:Encrypt", + "kms:ReEncrypt*", + "kms:GenerateDataKey*" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "TestPipelineArtifactsBucketEncryptionKey13258842", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "TestPipelinePipelineApplicationSecurityCheckCDKSecurityCheckRoleDefaultPolicyF2137052", + "Roles": [ + { + "Ref": "TestPipelinePipelineApplicationSecurityCheckCDKSecurityCheckRoleA54CF050" + } + ] + } + }, + "TestPipelinePipelineApplicationSecurityCheckCDKSecurityCheckBEE4547C": { + "Type": "AWS::CodeBuild::Project", + "Properties": { + "Artifacts": { + "Type": "NO_ARTIFACTS" + }, + "Environment": { + "ComputeType": "BUILD_GENERAL1_SMALL", + "Image": "aws/codebuild/standard:1.0", + "ImagePullCredentialsType": "CODEBUILD", + "PrivilegedMode": false, + "Type": "LINUX_CONTAINER" + }, + "ServiceRole": { + "Fn::GetAtt": [ + "TestPipelinePipelineApplicationSecurityCheckCDKSecurityCheckRoleA54CF050", + "Arn" + ] + }, + "Source": { + "BuildSpec": { + "Fn::Join": [ + "", + [ + "{\n \"version\": 0.2,\n \"phases\": {\n \"build\": {\n \"commands\": [\n \"npm install -g aws-cdk\",\n \"export PIPELINE_NAME=\\\"$(node -pe '`${process.env.CODEBUILD_INITIATOR}`.split(\\\"/\\\")[1]')\\\"\",\n \"payload=\\\"$(node -pe 'JSON.stringify({ \\\"PipelineName\\\": process.env.PIPELINE_NAME, \\\"StageName\\\": process.env.STAGE_NAME, \\\"ActionName\\\": process.env.ACTION_NAME })' )\\\"\",\n \"ARN=$CODEBUILD_BUILD_ARN\",\n \"REGION=\\\"$(node -pe '`${process.env.ARN}`.split(\\\":\\\")[3]')\\\"\",\n \"ACCOUNT_ID=\\\"$(node -pe '`${process.env.ARN}`.split(\\\":\\\")[4]')\\\"\",\n \"PROJECT_NAME=\\\"$(node -pe '`${process.env.ARN}`.split(\\\":\\\")[5].split(\\\"/\\\")[1]')\\\"\",\n \"PROJECT_ID=\\\"$(node -pe '`${process.env.ARN}`.split(\\\":\\\")[6]')\\\"\",\n \"export LINK=\\\"https://$REGION.console.aws.amazon.com/codesuite/codebuild/$ACCOUNT_ID/projects/$PROJECT_NAME/build/$PROJECT_NAME:$PROJECT_ID/?region=$REGION\\\"\",\n \"export PIPELINE_LINK=\\\"https://$REGION.console.aws.amazon.com/codesuite/codepipeline/pipelines/$PIPELINE_NAME/view?region=$REGION\\\"\",\n \"if cdk diff -a . --security-only --fail $STAGE_PATH/\\\\*; then aws lambda invoke --function-name ", + { + "Ref": "TestPipelinePipelineApplicationSecurityCheckCDKPipelinesAutoApprove1EE0AA81" + }, + " --invocation-type Event --payload \\\"$payload\\\" lambda.out; export MESSAGE=\\\"No security-impacting changes detected.\\\"; else [ -z \\\"${NOTIFICATION_ARN}\\\" ] || aws sns publish --topic-arn $NOTIFICATION_ARN --subject \\\"$NOTIFICATION_SUBJECT\\\" --message \\\"An upcoming change would broaden security changes in $PIPELINE_NAME.\\nReview and approve the changes in CodePipeline to proceed with the deployment.\\n\\nReview the changes in CodeBuild:\\n\\n$LINK\\n\\nApprove the changes in CodePipeline (stage $STAGE_NAME, action $ACTION_NAME):\\n\\n$PIPELINE_LINK\\\"; export MESSAGE=\\\"Deployment would make security-impacting changes. Click the link below to inspect them, then click Approve if all changes are expected.\\\"; fi\"\n ]\n }\n },\n \"env\": {\n \"exported-variables\": [\n \"LINK\",\n \"MESSAGE\"\n ]\n }\n}" + ] + ] + }, + "Type": "NO_SOURCE" + }, + "EncryptionKey": { + "Fn::GetAtt": [ + "TestPipelineArtifactsBucketEncryptionKey13258842", + "Arn" + ] + } + } + }, + "UnattachedStageStageApplicationSecurityCheckCDKPipelinesAutoApproveServiceRole1358574A": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + } + }, + "UnattachedStageStageApplicationSecurityCheckCDKPipelinesAutoApproveServiceRoleDefaultPolicy5AF69BD3": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "codepipeline:GetPipelineState", + "codepipeline:PutApprovalResult" + ], + "Condition": { + "StringEquals": { + "aws:ResourceTag/SECURITY_CHECK": "ALLOW_APPROVE" + } + }, + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "UnattachedStageStageApplicationSecurityCheckCDKPipelinesAutoApproveServiceRoleDefaultPolicy5AF69BD3", + "Roles": [ + { + "Ref": "UnattachedStageStageApplicationSecurityCheckCDKPipelinesAutoApproveServiceRole1358574A" + } + ] + } + }, + "UnattachedStageStageApplicationSecurityCheckCDKPipelinesAutoApprove249F82F9": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "cdk-hnb659fds-assets-12345678-test-region", + "S3Key": "6c6c8f170c2cc5c6e35d90fe172fbc17cae75777b84707d58332dee79f444404.zip" + }, + "Role": { + "Fn::GetAtt": [ + "UnattachedStageStageApplicationSecurityCheckCDKPipelinesAutoApproveServiceRole1358574A", + "Arn" + ] + }, + "Handler": "index.handler", + "Runtime": "nodejs14.x", + "Timeout": 300 + }, + "DependsOn": [ + "UnattachedStageStageApplicationSecurityCheckCDKPipelinesAutoApproveServiceRoleDefaultPolicy5AF69BD3", + "UnattachedStageStageApplicationSecurityCheckCDKPipelinesAutoApproveServiceRole1358574A" + ] + }, + "UnattachedStageStageApplicationSecurityCheckCDKSecurityCheckRoleD3505CF0": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "codebuild.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "UnattachedStageStageApplicationSecurityCheckCDKSecurityCheckRoleDefaultPolicy6F6EA2A6": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:test-region:12345678:log-group:/aws/codebuild/", + { + "Ref": "UnattachedStageStageApplicationSecurityCheckCDKSecurityCheckADCE795B" + } + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:test-region:12345678:log-group:/aws/codebuild/", + { + "Ref": "UnattachedStageStageApplicationSecurityCheckCDKSecurityCheckADCE795B" + }, + ":*" + ] + ] + } + ] + }, + { + "Action": [ + "codebuild:CreateReportGroup", + "codebuild:CreateReport", + "codebuild:UpdateReport", + "codebuild:BatchPutTestCases", + "codebuild:BatchPutCodeCoverages" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":codebuild:test-region:12345678:report-group/", + { + "Ref": "UnattachedStageStageApplicationSecurityCheckCDKSecurityCheckADCE795B" + }, + "-*" + ] + ] + } + }, + { + "Action": "sts:AssumeRole", + "Condition": { + "ForAnyValue:StringEquals": { + "iam:ResourceTag/aws-cdk:bootstrap-role": [ + "deploy" + ] + } + }, + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": "lambda:InvokeFunction", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "UnattachedStageStageApplicationSecurityCheckCDKPipelinesAutoApprove249F82F9", + "Arn" + ] + } + }, + { + "Action": "sns:Publish", + "Effect": "Allow", + "Resource": { + "Ref": "SecurityChangesTopic9762A9B3" + } + }, + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "TestPipelineArtifactsBucket026AF2F9", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "TestPipelineArtifactsBucket026AF2F9", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + }, + { + "Action": [ + "kms:Decrypt", + "kms:DescribeKey" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "TestPipelineArtifactsBucketEncryptionKey13258842", + "Arn" + ] + } + }, + { + "Action": [ + "kms:Decrypt", + "kms:Encrypt", + "kms:ReEncrypt*", + "kms:GenerateDataKey*" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "TestPipelineArtifactsBucketEncryptionKey13258842", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "UnattachedStageStageApplicationSecurityCheckCDKSecurityCheckRoleDefaultPolicy6F6EA2A6", + "Roles": [ + { + "Ref": "UnattachedStageStageApplicationSecurityCheckCDKSecurityCheckRoleD3505CF0" + } + ] + } + }, + "UnattachedStageStageApplicationSecurityCheckCDKSecurityCheckADCE795B": { + "Type": "AWS::CodeBuild::Project", + "Properties": { + "Artifacts": { + "Type": "NO_ARTIFACTS" + }, + "Environment": { + "ComputeType": "BUILD_GENERAL1_SMALL", + "Image": "aws/codebuild/standard:1.0", + "ImagePullCredentialsType": "CODEBUILD", + "PrivilegedMode": false, + "Type": "LINUX_CONTAINER" + }, + "ServiceRole": { + "Fn::GetAtt": [ + "UnattachedStageStageApplicationSecurityCheckCDKSecurityCheckRoleD3505CF0", + "Arn" + ] + }, + "Source": { + "BuildSpec": { + "Fn::Join": [ + "", + [ + "{\n \"version\": 0.2,\n \"phases\": {\n \"build\": {\n \"commands\": [\n \"npm install -g aws-cdk\",\n \"export PIPELINE_NAME=\\\"$(node -pe '`${process.env.CODEBUILD_INITIATOR}`.split(\\\"/\\\")[1]')\\\"\",\n \"payload=\\\"$(node -pe 'JSON.stringify({ \\\"PipelineName\\\": process.env.PIPELINE_NAME, \\\"StageName\\\": process.env.STAGE_NAME, \\\"ActionName\\\": process.env.ACTION_NAME })' )\\\"\",\n \"ARN=$CODEBUILD_BUILD_ARN\",\n \"REGION=\\\"$(node -pe '`${process.env.ARN}`.split(\\\":\\\")[3]')\\\"\",\n \"ACCOUNT_ID=\\\"$(node -pe '`${process.env.ARN}`.split(\\\":\\\")[4]')\\\"\",\n \"PROJECT_NAME=\\\"$(node -pe '`${process.env.ARN}`.split(\\\":\\\")[5].split(\\\"/\\\")[1]')\\\"\",\n \"PROJECT_ID=\\\"$(node -pe '`${process.env.ARN}`.split(\\\":\\\")[6]')\\\"\",\n \"export LINK=\\\"https://$REGION.console.aws.amazon.com/codesuite/codebuild/$ACCOUNT_ID/projects/$PROJECT_NAME/build/$PROJECT_NAME:$PROJECT_ID/?region=$REGION\\\"\",\n \"export PIPELINE_LINK=\\\"https://$REGION.console.aws.amazon.com/codesuite/codepipeline/pipelines/$PIPELINE_NAME/view?region=$REGION\\\"\",\n \"if cdk diff -a . --security-only --fail $STAGE_PATH/\\\\*; then aws lambda invoke --function-name ", + { + "Ref": "UnattachedStageStageApplicationSecurityCheckCDKPipelinesAutoApprove249F82F9" + }, + " --invocation-type Event --payload \\\"$payload\\\" lambda.out; export MESSAGE=\\\"No security-impacting changes detected.\\\"; else [ -z \\\"${NOTIFICATION_ARN}\\\" ] || aws sns publish --topic-arn $NOTIFICATION_ARN --subject \\\"$NOTIFICATION_SUBJECT\\\" --message \\\"An upcoming change would broaden security changes in $PIPELINE_NAME.\\nReview and approve the changes in CodePipeline to proceed with the deployment.\\n\\nReview the changes in CodeBuild:\\n\\n$LINK\\n\\nApprove the changes in CodePipeline (stage $STAGE_NAME, action $ACTION_NAME):\\n\\n$PIPELINE_LINK\\\"; export MESSAGE=\\\"Deployment would make security-impacting changes. Click the link below to inspect them, then click Approve if all changes are expected.\\\"; fi\"\n ]\n }\n },\n \"env\": {\n \"exported-variables\": [\n \"LINK\",\n \"MESSAGE\"\n ]\n }\n}" + ] + ] + }, + "Type": "NO_SOURCE" + }, + "EncryptionKey": { + "Fn::GetAtt": [ + "TestPipelineArtifactsBucketEncryptionKey13258842", + "Arn" + ] + } + } + }, + "SecurityChangesTopic9762A9B3": { + "Type": "AWS::SNS::Topic" + }, + "SecurityChangesTopictestemailcom7C32D452": { + "Type": "AWS::SNS::Subscription", + "Properties": { + "Protocol": "email", + "TopicArn": { + "Ref": "SecurityChangesTopic9762A9B3" + }, + "Endpoint": "test@email.com" + } + } + }, + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store." + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/test/integ.pipeline-security.ts b/packages/@aws-cdk/pipelines/test/integ.pipeline-security.ts new file mode 100644 index 0000000000000..8b43db6087a1c --- /dev/null +++ b/packages/@aws-cdk/pipelines/test/integ.pipeline-security.ts @@ -0,0 +1,110 @@ +/// !cdk-integ PipelineSecurityStack +import * as codepipeline from '@aws-cdk/aws-codepipeline'; +import * as codepipeline_actions from '@aws-cdk/aws-codepipeline-actions'; +import * as iam from '@aws-cdk/aws-iam'; +import * as sns from '@aws-cdk/aws-sns'; +import * as subscriptions from '@aws-cdk/aws-sns-subscriptions'; +import { App, SecretValue, Stack, StackProps, Stage, StageProps } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import * as cdkp from '../lib'; + +class MyStage extends Stage { + constructor(scope: Construct, id: string, props?: StageProps) { + super(scope, id, props); + const stack = new Stack(this, 'MyStack', { + env: props?.env, + }); + const topic = new sns.Topic(stack, 'Topic'); + topic.grantPublish(new iam.AccountPrincipal(stack.account)); + } +} + +class MySafeStage extends Stage { + constructor(scope: Construct, id: string, props?: StageProps) { + super(scope, id, props); + const stack = new Stack(this, 'MySafeStack', { + env: props?.env, + }); + new sns.Topic(stack, 'MySafeTopic'); + } +} + +export class TestCdkStack extends Stack { + constructor(scope: Construct, id: string, props?: StackProps) { + super(scope, id, props); + + // The code that defines your stack goes here + const sourceArtifact = new codepipeline.Artifact(); + const cloudAssemblyArtifact = new codepipeline.Artifact('CloudAsm'); + + const pipeline = new cdkp.CdkPipeline(this, 'TestPipeline', { + selfMutating: false, + pipelineName: 'TestPipeline', + cloudAssemblyArtifact, + sourceAction: new codepipeline_actions.GitHubSourceAction({ + actionName: 'GitHub', + output: sourceArtifact, + oauthToken: SecretValue.plainText('not-a-secret'), + owner: 'OWNER', + repo: 'REPO', + trigger: codepipeline_actions.GitHubTrigger.POLL, + }), + synthAction: cdkp.SimpleSynthAction.standardYarnSynth({ + sourceArtifact, + cloudAssemblyArtifact, + buildCommand: 'yarn build', + }), + }); + + const pipelineStage = pipeline.codePipeline.addStage({ + stageName: 'UnattachedStage', + }); + + const unattachedStage = new cdkp.CdkStage(this, 'UnattachedStage', { + stageName: 'UnattachedStage', + pipelineStage, + cloudAssemblyArtifact, + host: { + publishAsset: () => undefined, + stackOutputArtifact: () => undefined, + }, + }); + + const topic = new sns.Topic(this, 'SecurityChangesTopic'); + topic.addSubscription(new subscriptions.EmailSubscription('test@email.com')); + + unattachedStage.addApplication(new MyStage(this, 'SingleStage', { + env: { account: this.account, region: this.region }, + }), { confirmBroadeningPermissions: true, securityNotificationTopic: topic }); + + const stage1 = pipeline.addApplicationStage(new MyStage(this, 'PreProduction', { + env: { account: this.account, region: this.region }, + }), { confirmBroadeningPermissions: true, securityNotificationTopic: topic }); + + stage1.addApplication(new MySafeStage(this, 'SafeProduction', { + env: { account: this.account, region: this.region }, + })); + + stage1.addApplication(new MySafeStage(this, 'DisableSecurityCheck', { + env: { account: this.account, region: this.region }, + }), { confirmBroadeningPermissions: false }); + + const stage2 = pipeline.addApplicationStage(new MyStage(this, 'NoSecurityCheck', { + env: { account: this.account, region: this.region }, + })); + + stage2.addApplication(new MyStage(this, 'EnableSecurityCheck', { + env: { account: this.account, region: this.region }, + }), { confirmBroadeningPermissions: true }); + } +} + +const app = new App({ + context: { + '@aws-cdk/core:newStyleStackSynthesis': 'true', + }, +}); +new TestCdkStack(app, 'PipelineSecurityStack', { + env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION }, +}); +app.synth(); diff --git a/packages/aws-cdk/lib/api/cxapp/cloud-assembly.ts b/packages/aws-cdk/lib/api/cxapp/cloud-assembly.ts index 690d626f8e8dd..f42b0ee640fc2 100644 --- a/packages/aws-cdk/lib/api/cxapp/cloud-assembly.ts +++ b/packages/aws-cdk/lib/api/cxapp/cloud-assembly.ts @@ -156,7 +156,7 @@ export class CloudAssembly { return new StackCollection(this, topLevelStacks); } else { throw new Error('Since this app includes more than a single stack, specify which stacks to use (wildcards are supported) or specify `--all`\n' + - `Stacks: ${stacks.map(x => x.id).join(' ')}`); + `Stacks: ${stacks.map(x => x.hierarchicalId).join(' · ')}`); } default: throw new Error(`invalid default behavior: ${defaultSelection}`); diff --git a/packages/aws-cdk/test/api/cloud-assembly.test.ts b/packages/aws-cdk/test/api/cloud-assembly.test.ts index 54d3a18f682ef..c17f108142a1e 100644 --- a/packages/aws-cdk/test/api/cloud-assembly.test.ts +++ b/packages/aws-cdk/test/api/cloud-assembly.test.ts @@ -86,6 +86,15 @@ test('select behavior: single', async () => { .rejects.toThrow('Since this app includes more than a single stack, specify which stacks to use (wildcards are supported) or specify `--all`'); }); +test('stack list error contains node paths', async () => { + // GIVEN + const cxasm = await testCloudAssembly(); + + // WHEN + await expect(cxasm.selectStacks({ patterns: [] }, { defaultBehavior: DefaultSelection.OnlySingle })) + .rejects.toThrow('withouterrorsNODEPATH'); +}); + test('select behavior: repeat', async () => { // GIVEN const cxasm = await testCloudAssembly(); @@ -147,6 +156,7 @@ async function testCloudAssembly({ env }: { env?: string, versionReporting?: boo const cloudExec = new MockCloudExecutable({ stacks: [{ stackName: 'withouterrors', + displayName: 'withouterrorsNODEPATH', env, template: { resource: 'noerrorresource' }, },