Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,37 @@ You should not fork this repository and expect to reproduce the same in your AWS

See [CONTRIBUTING](CONTRIBUTING.md#security-issue-notifications) for more information.

## Envoirnment Variables

To set up the required environment variables, you can either define them in your .env file, set them up in the environment variables of your CLI instance, or define them in code using the builder. At the very minimum, you need to define an account number for the RES account and a profile.

Here are all the environment variables you can set up in the .env file:
```
GIT_REPOSITORY=git-repo-name
CDK_QUALIFIER=valid-qualifier
AWS_REGION=valid-region
ACCOUNT_RES=valid-account-id
RES_ACCOUNT_AWS_PROFILE=valid-profile
ACCOUNT_DEV=valid-account-id
DEV_ACCOUNT_AWS_PROFILE=valid-profile
ACCOUNT_INT=valid-account-id (optional)
INT_ACCOUNT_AWS_PROFILE=valid-profile (optional)
PROXY_SECRET_ARN=valid-proxy-arn (optional)
```

Alternatively, it is recommended to set these variables up using your pipeline builder like so:

```
PipelineBlueprint.builder()
.defineStages([{ stage: 'RES', account: '12345678', region: 'us-east-1' }])
.repositoryProvider(new BasicRepositoryProvider({ name: 'repo-name', repositoryType: 'COMMIT', branch: 'main' }))
.applicationName('appname')
.proxy({ proxySecretArn: 'validarn', proxyTestUrl: 'url' });
```
This is not exhaustive and is just an example of a few. It is recommended that the documentation of the builder should be referred to when defining these custom parameters.

Note also that the hierarchy of privilege here is the CLI environment variables, the .env variables, and then finally the variables defined in code as above. So the CLI environment variables take the most precedence, and if not available, it will fall down the list.

## License

This project is licensed under the Apache-2.0 License.
Expand Down
209 changes: 209 additions & 0 deletions packages/@cdklabs/cdk-cicd-wrapper-cli/src/cmds/AnalyzerCommand.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

/* eslint-disable no-console */
import * as yargs from 'yargs';
import { CodePipeline, GetPipelineCommand } from '@aws-sdk/client-codepipeline';
import { CodeBuild, BatchGetProjectsCommand } from '@aws-sdk/client-codebuild';
import { KMS, DescribeKeyCommand } from '@aws-sdk/client-kms';
import { fromNodeProviderChain } from '@aws-sdk/credential-providers';

/**
* Command class that checks CodePipeline and CodeBuild configurations.
*/
class Command implements yargs.CommandModule {
/**
* The command name.
*/
command = 'analyze';

/**
* A description of the command.
*/
describe = 'analyzes the environment and pipeline configuration to spot potential common issues.';

/**
* Builds the command arguments.
* @param args The argument parser.
* @returns The argument parser with defined options.
*/
builder(args: yargs.Argv) {
return args
.option('profile', {
type: 'string',
requiresArg: true,
demandOption: 'AWS profile is required',
description: 'AWS profile',
})
.option('region', {
type: 'string',
requiresArg: true,
demandOption: 'AWS region is required',
description: 'AWS region',
})
.option('cdk-qualifier', {
type: 'string',
requiresArg: true,
demandOption: 'CDK Qualifier Required',
description: 'The qualifier used for the CDK project',
});
}

/**
* Handles the command execution.
* @param args The parsed command arguments.
*/
async handler(args: yargs.Arguments) {
const region = args.region as string;
const profile = args.profile as string;
const cdkQualifier = args['cdk-qualifier'] as string;

const sdkConfig = {
region,
credentials: fromNodeProviderChain({ profile }),
};

const pipelineClient = new CodePipeline(sdkConfig);
const codebuildClient = new CodeBuild(sdkConfig);
const kmsClient = new KMS(sdkConfig);

console.log('analyzing for common issues...')
try {
const pipelines = await pipelineClient.listPipelines({});
for (const pipeline of pipelines.pipelines || []) {
if (pipeline.name?.includes(cdkQualifier)) {
console.log('retrieving pipeline details...')
const pipelineDetails = await pipelineClient.send(
new GetPipelineCommand({ name: pipeline.name })
);

if (!pipelineDetails.pipeline?.stages) {
console.error(`Your pipeline has no stages!`);
} else {

//Pipeline sanity check against .env
console.log('preforming pipeline sanity check against env vars...')
if (process.env.ACCOUNT_DEV && process.env.ACCOUNT_DEV !== '-') {
const hasDevStage = pipelineDetails.pipeline?.stages.some(stage => stage.name === 'DEV');
if (!hasDevStage) {
console.warn(`NOTE : pipeline failed sanity check, your .env / env vars specify a DEV stage but your pipeline does not have one`);
}
}

if (process.env.ACCOUNT_INT && process.env.ACCOUNT_INT !== '-') {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what if we have another stage defined which you dont check here, what happens next?

const hasIntStage = pipelineDetails.pipeline?.stages.some(stage => stage.name === 'INT');
if (!hasIntStage) {
console.warn(`NOTE : pipeline failed sanity check, your .env / env vars specify an INT stage but your pipeline does not have one`);
}
}



console.log('checking codebuilds...')
for (const stage of pipelineDetails.pipeline?.stages) {
for (const action of stage.actions || []) {
// Codebuild Checks
if (action.actionTypeId?.provider === 'CodeBuild' && action.configuration?.ProjectName) {
const codebuildProjectName = action.configuration?.ProjectName;
// Check the env vars of the synth action
if (action.name === 'Synth') {
await checkEnvVarsCodeBuildProject(codebuildClient, codebuildProjectName);
}

// Check the CodeBuild projects to see if they are using a valid KMS Key
await checkKMSCodeBuildProject(codebuildClient, kmsClient, codebuildProjectName);
}
}
}
}
}
}
} catch (err) {
console.error(err);
}
}
}

/**
* Validates if a string is a valid ARN.
* @param arn The ARN string.
* @returns True if valid, otherwise false.
*/
function isValidArn(arn: string): boolean {
const arnRegex = /^arn:aws[a-zA-Z-]*:[a-zA-Z0-9-]+:[a-zA-Z0-9-]*:[0-9]*:[^/].*$/;
return arnRegex.test(arn);
}

/**
* Checks the CodeBuild project for environment variables.
* @param codebuildClient The CodeBuild client.
* @param projectName The name of the CodeBuild project.
*/
async function checkEnvVarsCodeBuildProject(codebuildClient: CodeBuild, projectName: string) {
try {
const projectDetails = await codebuildClient.send(
new BatchGetProjectsCommand({ names: [projectName] })
);

const project = projectDetails.projects?.[0];
if (project) {
const envVars = project.environment?.environmentVariables || [];
console.log('build stage environment variables:', envVars);

const proxySecretArn = envVars.find(env => env.name === 'PROXY_SECRET_ARN');
const codestarConnectionArn = envVars.find(env => env.name === 'CODESTAR_CONNECTION_ARN');

console.log('proxySecretArn:', proxySecretArn);
console.log('codestarConnectionArn:', codestarConnectionArn);

if (!proxySecretArn || proxySecretArn.value === '' || proxySecretArn.value === '-') {
console.warn(`NOTE: you do not have a PROXY_SECRET_ARN configured in the synth stage of your pipeline. if your organization uses a network proxy, your CodeBuild may fail.`);
} else if (proxySecretArn.value && !isValidArn(proxySecretArn.value)) {
console.error(`ERROR : Invalid ARN format for PROXY_SECRET_ARN in CodeBuild project ${projectName}.`);
}

if (!codestarConnectionArn || codestarConnectionArn.value === '' || codestarConnectionArn.value === '-') {
console.warn(`NOTE: your CODESTAR_CONNECTION_ARN is empty in the Synth stage of your pipeline. If you are not using CodeCommit as your repository, you need to define a valid CodeStar connection ARN.`);
} else if (codestarConnectionArn.value && !isValidArn(codestarConnectionArn.value)) {
console.error(`ERROR : invalid arn format for CODESTAR_CONNECTION_ARN in codebuild project ${projectName}.`);
}
}
} catch (err) {
console.error(`ERROR : failed to retrieve details for codebuild project ${projectName}:`, err);
}
}

/**
* Checks the CodeBuild project for KMS key validity.
* @param codebuildClient The CodeBuild client.
* @param kmsClient The KMS client.
* @param projectName The name of the CodeBuild project.
*/
async function checkKMSCodeBuildProject(codebuildClient: CodeBuild, kmsClient: KMS, projectName: string) {
try {
const projectDetails = await codebuildClient.send(
new BatchGetProjectsCommand({ names: [projectName] })
);

const project = projectDetails.projects?.[0];
if (project) {
const encryptionKey = project.encryptionKey;

if (encryptionKey) {
const kmsKeyDetails = await kmsClient.send(
new DescribeKeyCommand({ KeyId: encryptionKey })
);

if (kmsKeyDetails.KeyMetadata?.KeyState === 'Disabled') {
console.error(`ERROR : KMS key ${encryptionKey} for project ${projectName} is disabled.`);
}
} else {
console.warn(`NOTE : no KMS key configured for project ${projectName}.`);
}
}
} catch (err) {
console.error(`ERROR : failed to retrieve details for codebuild project ${projectName}:`, err);
}
}

export default new Command();
2 changes: 2 additions & 0 deletions packages/@cdklabs/cdk-cicd-wrapper-cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import * as yargs from 'yargs';
import checkDependencies from './cmds/CheckDependenciesCommand';
import complianceBucket from './cmds/ComplianceBucketCommand';
import analyzer from './cmds/AnalyzerCommand';
import configure from './cmds/ConfigureCommand';
import license from './cmds/LicenseCommand';
import security from './cmds/SecurityCommand';
Expand All @@ -23,6 +24,7 @@ async function main() {
ya.command(license);
ya.command(complianceBucket);
ya.command(security);
ya.command(analyzer);
ya.command(checkDependencies);

// Enable command recommendations and strict command handling
Expand Down
59 changes: 59 additions & 0 deletions packages/@cdklabs/cdk-cicd-wrapper/src/stacks/PipelineBlueprint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,61 @@ const defaultConfigs = {
},
};

/**
* Validates the environment variables and default configurations.
* Throws an error if any validation fails.
*/
function validateConfig(props: IPipelineBlueprintProps, stageDefinitions: AllStage<Partial<IStageDefinition>>, region: string) {
const envVars = [
{ name: 'AWS_REGION', pattern: /^[a-z]{2}-[a-z]+-\d{1}$/, errorMessage: 'Invalid AWS region format' },
{ name: 'ACCOUNT_RES', pattern: /^\d{12}$/, errorMessage: 'Invalid RES account number format' },
{ name: 'RES_ACCOUNT_AWS_PROFILE', pattern: /^[a-zA-Z0-9-_]+$/, errorMessage: 'Invalid AWS profile name format' },
{ name: 'ACCOUNT_DEV', pattern: /^\d{12}$/, errorMessage: 'Invalid DEV account number format' },
{ name: 'DEV_ACCOUNT_AWS_PROFILE', pattern: /^[a-zA-Z0-9-_]+$/, errorMessage: 'Invalid AWS profile name format' },
{ name: 'ACCOUNT_INT', pattern: /^\d{12}$/, errorMessage: 'Invalid INT account number format' },
{ name: 'INT_ACCOUNT_AWS_PROFILE', pattern: /^[a-zA-Z0-9-_]+$/, errorMessage: 'Invalid AWS profile name format' },

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what about other stages?

{ name: 'PROXY_SECRET_ARN', pattern: /^arn:aws:[a-z-]+:[a-z0-9-]+:\d{12}:secret\/[a-zA-Z0-9-_]+$/, errorMessage: 'Invalid ARN format' },
];

envVars.forEach((envVar) => {
const value = process.env[envVar.name];
if (value && !envVar.pattern.test(value)) {
throw new Error(`Environment variable ${envVar.name} is invalid: ${envVar.errorMessage}`);
}
});

if (!props.applicationName) {
throw new Error('Application name is required');
}

if (!props.applicationQualifier) {
throw new Error('Application qualifier is required');
} else if (!/^[a-zA-Z0-9-_]+$/.test(props.applicationQualifier)) {
throw new Error('Invalid application qualifier format');
}

if (!region) {
throw new Error('AWS region is required');
} else if (!/^[a-z]{2}-[a-z]+-\d{1}$/.test(region)) {
throw new Error('Invalid AWS region format');
}

if (!stageDefinitions.RES) {
throw new Error('At least RES stage is required');
}

Object.entries(stageDefinitions).forEach(([stage, providedDefinition]) => {
if (!providedDefinition.account) {
throw new Error(`Stage ${stage} does not have an associated account.`);
} else if (!/^\d{12}$/.test(providedDefinition.account)) {
throw new Error(`Invalid account number format for stage ${stage}`);
}
if (providedDefinition.region && !/^[a-z]{2}-[a-z]+-\d{1}$/.test(providedDefinition.region)) {
throw new Error(`Invalid AWS region format for stage ${stage}`);
}
});
}

/**
* Class for building a Pipeline Blueprint.
*/
Expand Down Expand Up @@ -360,6 +415,10 @@ export class PipelineBlueprintBuilder {
* @returns The created stack.
*/
public synth(app: cdk.App) {

// Validate the entire configuration
validateConfig(this.props, this.stageDefinitions, this._region!);

this.props.deploymentDefinition = this.generateDeploymentDefinitions();
this.props.plugins = { ...this.props.plugins, ...this._plugins };

Expand Down