diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index ed497e40..b75aace9 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -17,6 +17,7 @@ "source=${env:HOME}${env:USERPROFILE}/.gitconfig,target=/home/vscode/.gitconfig,type=bind" ], "features": { + "ghcr.io/devcontainers/features/github-cli:1": {}, "ghcr.io/devcontainers/features/docker-outside-of-docker:1": { "version": "latest", "moby": "true", diff --git a/.github/scripts/check-sbom-issues-against-ignores.sh b/.github/scripts/check-sbom-issues-against-ignores.sh deleted file mode 100755 index 8db75ab2..00000000 --- a/.github/scripts/check-sbom-issues-against-ignores.sh +++ /dev/null @@ -1,46 +0,0 @@ -#!/bin/bash - -# Usage: ./check-sbom-issues-against-ignores.sh -IGNORED_ISSUES_FILE="$1" -SCAN_RESULTS_FILE="$2" - -# Check if files exist -if [[ ! -f "$IGNORED_ISSUES_FILE" || ! -f "$SCAN_RESULTS_FILE" ]]; then - echo "Error: One or both of the required files do not exist." - exit 1 -fi - -# Read ignored issues into an array -mapfile -t IGNORED_ISSUES < <(jq -r '.[]' "$IGNORED_ISSUES_FILE") - -# Read scan results and check for critical vulnerabilities -CRITICAL_FOUND=false - -# Loop through vulnerabilities in the scan results -while IFS= read -r MATCH; do - VULN_ID=$(echo "$MATCH" | jq -r '.vulnerability.id') - - # Check if the vulnerability ID is in the ignored list - FOUND=false - for IGNORED in "${IGNORED_ISSUES[@]}"; do - if [[ "$IGNORED" == "$VULN_ID" ]]; then - FOUND=true - echo "Warning: Ignored vulnerability found: $VULN_ID" - break - fi - done - - # If the vulnerability is not found in the ignored list, mark critical as found - if [[ "$FOUND" == false ]]; then - echo "Error: Critical vulnerability found that is not in the ignore list: $VULN_ID" - CRITICAL_FOUND=true - fi -done < <(jq -c '.matches[] | select(.vulnerability.severity == "Critical")' "$SCAN_RESULTS_FILE") - -# Exit with error if critical vulnerability is found -if [[ "$CRITICAL_FOUND" == true ]]; then - exit 1 -fi - -echo "No unignored critical vulnerabilities found." -exit 0 diff --git a/cdk.json b/cdk.json index 8fd3c35c..11a3fdbe 100644 --- a/cdk.json +++ b/cdk.json @@ -71,6 +71,34 @@ "@aws-cdk/aws-ecs:removeDefaultDeploymentAlarm": true, "@aws-cdk/custom-resources:logApiResponseDataPropertyTrueDefault": false, "@aws-cdk/aws-s3:keepNotificationInImportedBucket": false, - "cdk-migrate": true + "@aws-cdk/aws-appsync:appSyncGraphQLAPIScopeLambdaPermission": true, + "@aws-cdk/aws-dynamodb:resourcePolicyPerReplica": true, + "@aws-cdk/aws-dynamodb:retainTableReplica": true, + "@aws-cdk/aws-ec2:bastionHostUseAmazonLinux2023ByDefault": true, + "@aws-cdk/aws-ec2:ec2SumTImeoutEnabled": true, + "@aws-cdk/aws-ec2:requirePrivateSubnetsForEgressOnlyInternetGateway": true, + "@aws-cdk/aws-ecs-patterns:secGroupsDisablesImplicitOpenListener": true, + "@aws-cdk/aws-ecs-patterns:uniqueTargetGroupId": true, + "@aws-cdk/aws-ecs:reduceEc2FargateCloudWatchPermissions": true, + "@aws-cdk/aws-elasticloadbalancingV2:albDualstackWithoutPublicIpv4SecurityGroupRulesDefault": true, + "@aws-cdk/aws-elasticloadbalancingv2:networkLoadBalancerWithSecurityGroupByDefault": true, + "@aws-cdk/aws-events:requireEventBusPolicySid": true, + "@aws-cdk/aws-iam:oidcRejectUnauthorizedConnections": true, + "@aws-cdk/aws-kms:applyImportedAliasPermissionsToPrincipal": true, + "@aws-cdk/aws-lambda-nodejs:sdkV3ExcludeSmithyPackages": true, + "@aws-cdk/aws-lambda:useCdkManagedLogGroup": true, + "@aws-cdk/aws-rds:setCorrectValueForDatabaseInstanceReadReplicaInstanceResourceId": true, + "@aws-cdk/aws-route53-patterns:useDistribution": true, + "@aws-cdk/aws-route53-targets:userPoolDomainNameMethodWithoutCustomResource": true, + "@aws-cdk/aws-s3:publicAccessBlockedByDefault": true, + "@aws-cdk/aws-s3:setUniqueReplicationRoleName": true, + "@aws-cdk/aws-signer:signingProfileNamePassedToCfn": true, + "@aws-cdk/aws-stepfunctions-tasks:fixRunEcsTaskPolicy": true, + "@aws-cdk/aws-stepfunctions:useDistributedMapResultWriterV2": true, + "@aws-cdk/core:aspectPrioritiesMutating": true, + "@aws-cdk/core:cfnIncludeRejectComplexResourceUpdateCreatePolicyIntrinsics": true, + "@aws-cdk/core:enableAdditionalMetadataCollection": true, + "@aws-cdk/core:explicitStackTags": true, + "@aws-cdk/s3-notifications:addS3TrustKeyPolicyForSnsSubscriptions": true } } diff --git a/packages/cdk/bin/utils/appUtils.ts b/packages/cdk/bin/utils/appUtils.ts index a96c1961..d81ce64d 100644 --- a/packages/cdk/bin/utils/appUtils.ts +++ b/packages/cdk/bin/utils/appUtils.ts @@ -45,7 +45,7 @@ const findResourcesByType = (construct: IConstruct, type: string): Array, rules: Array): void => { +export const addSuppressions = (resources: Array, rules: Array): void => { resources.forEach(resource => { if (!resource.cfnOptions.metadata) { resource.cfnOptions.metadata = {} diff --git a/packages/cdk/constructs/DelayResource.ts b/packages/cdk/constructs/DelayResource.ts index 2975b60c..547ae4ef 100644 --- a/packages/cdk/constructs/DelayResource.ts +++ b/packages/cdk/constructs/DelayResource.ts @@ -1,8 +1,22 @@ import {Construct} from "constructs" -import {Duration, CustomResource} from "aws-cdk-lib" +import { + Duration, + CustomResource, + Fn, + RemovalPolicy +} from "aws-cdk-lib" import {Function, Runtime, Code} from "aws-cdk-lib/aws-lambda" -import {Role, ServicePrincipal, ManagedPolicy} from "aws-cdk-lib/aws-iam" +import { + Role, + ServicePrincipal, + ManagedPolicy, + PolicyStatement +} from "aws-cdk-lib/aws-iam" import {Provider} from "aws-cdk-lib/custom-resources" +import {Key} from "aws-cdk-lib/aws-kms" +import {CfnLogGroup, LogGroup} from "aws-cdk-lib/aws-logs" +import {NagSuppressions} from "cdk-nag" +import {addSuppressions} from "../bin/utils/appUtils" export interface DelayResourceProps { /** @@ -14,6 +28,11 @@ export interface DelayResourceProps { * Optional description for the delay resource */ readonly description?: string + + /** + * Name for the delay resource + */ + readonly name: string } /** @@ -22,29 +41,72 @@ export interface DelayResourceProps { */ export class DelayResource extends Construct { public readonly customResource: CustomResource - public readonly delaySeconds: number - constructor(scope: Construct, id: string, props: DelayResourceProps = {}) { + constructor(scope: Construct, id: string, props: DelayResourceProps) { super(scope, id) + const { + delaySeconds = 30, + description = `Delay resource for ${delaySeconds} seconds`, + name + } = props + const cloudWatchLogsKmsKey = Key.fromKeyArn( + scope, `${name}delayResourceCloudWatchLogsKmsKey`, Fn.importValue("account-resources:CloudwatchLogsKmsKeyArn")) + + const cloudwatchEncryptionKMSPolicy = ManagedPolicy.fromManagedPolicyArn( + scope, `${name}delayResourceCloudwatchEncryptionKMSPolicyArn`, + Fn.importValue("account-resources:CloudwatchEncryptionKMSPolicyArn")) + + const logGroup = new LogGroup(scope, `${name}LambdaLogGroup`, { + encryptionKey: cloudWatchLogsKmsKey, + logGroupName: `/aws/lambda/${name}`, + retention: 30, + removalPolicy: RemovalPolicy.DESTROY + }) + + const cfnlogGroup = logGroup.node.defaultChild as CfnLogGroup + addSuppressions([cfnlogGroup], ["CW_LOGGROUP_RETENTION_PERIOD_CHECK"]) + const putLogsManagedPolicy = new ManagedPolicy(scope, `${name}LambdaPutLogsManagedPolicy`, { + description: `write to ${name} logs`, + statements: [ + new PolicyStatement({ + actions: [ + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + resources: [ + logGroup.logGroupArn, + `${logGroup.logGroupArn}:log-stream:*` + ] + }) + ] + }) - this.delaySeconds = props.delaySeconds || 30 + NagSuppressions.addResourceSuppressions(putLogsManagedPolicy, [ + { + id: "AwsSolutions-IAM5", + // eslint-disable-next-line max-len + reason: "Suppress error for not having wildcards in permissions. This is a fine as we need to have permissions on all log streams under path" + } + ]) // create IAM role for the delay Lambda function - const lambdaExecutionRole = new Role(this, "LambdaExecutionRole", { + const lambdaExecutionRole = new Role(this, `${name}LambdaExecutionRole`, { assumedBy: new ServicePrincipal("lambda.amazonaws.com"), description: "Execution role for delay custom resource Lambda function", managedPolicies: [ - ManagedPolicy.fromAwsManagedPolicyName("service-role/AWSLambdaBasicExecutionRole") + putLogsManagedPolicy, + cloudwatchEncryptionKMSPolicy ] }) // create the delay Lambda function with inline Python code - const delayFunction = new Function(this, "DelayFunction", { + const delayFunction = new Function(this, `${name}DelayFunction`, { runtime: Runtime.PYTHON_3_12, + logGroup: logGroup, handler: "index.handler", role: lambdaExecutionRole, timeout: Duration.minutes(15), // max Lambda timeout to handle long delays - description: props.description || `Delay resource for ${this.delaySeconds} seconds`, + description: description, code: Code.fromInline(` from time import sleep import json @@ -78,17 +140,52 @@ def handler(event, context): `) }) + const frameworkInvokeDelayFunctionPolicy = new ManagedPolicy(this, + `${name}DelayProviderInvokeDelayFunctionPolicy`, { + description: `Allow provider framework to invoke ${name} delay function`, + statements: [ + new PolicyStatement({ + actions: [ + "lambda:InvokeFunction", + "lambda:GetFunction" + ], + resources: [ + delayFunction.functionArn + ] + }) + ] + }) + + const frameworkOnEventRole = new Role(this, `${name}DelayProviderFrameworkOnEventRole`, { + assumedBy: new ServicePrincipal("lambda.amazonaws.com"), + description: "Execution role for delay provider framework onEvent Lambda function", + managedPolicies: [ + putLogsManagedPolicy, + cloudwatchEncryptionKMSPolicy, + frameworkInvokeDelayFunctionPolicy + ] + }) + + const frameworkOnEventRoleRef = Role.fromRoleArn( + this, + `${name}DelayProviderFrameworkOnEventRoleRef`, + frameworkOnEventRole.roleArn, + {mutable: false} + ) + // create the custom resource provider - const provider = new Provider(this, "DelayProvider", { - onEventHandler: delayFunction + const provider = new Provider(this, `${name}DelayProvider`, { + onEventHandler: delayFunction, + logGroup: logGroup, + frameworkOnEventRole: frameworkOnEventRoleRef }) // create the custom resource that triggers the delay - this.customResource = new CustomResource(this, "DelayCustomResource", { + this.customResource = new CustomResource(this, `${name}DelayCustomResource`, { serviceToken: provider.serviceToken, properties: { - WaitSeconds: this.delaySeconds, - Description: props.description || `Delay for ${this.delaySeconds} seconds`, + WaitSeconds: delaySeconds, + Description: description, // timestamp to ensure updates trigger when properties change Timestamp: Date.now() } diff --git a/packages/cdk/constructs/SecretWithParameter.ts b/packages/cdk/constructs/SecretWithParameter.ts index 23c14171..8d3570ce 100644 --- a/packages/cdk/constructs/SecretWithParameter.ts +++ b/packages/cdk/constructs/SecretWithParameter.ts @@ -2,12 +2,14 @@ import {Construct} from "constructs" import {SecretValue} from "aws-cdk-lib" import {StringParameter, ParameterTier} from "aws-cdk-lib/aws-ssm" import {Secret} from "aws-cdk-lib/aws-secretsmanager" +import {IKey} from "aws-cdk-lib/aws-kms" export interface SecretWithParameterProps { readonly secretName: string readonly parameterName: string readonly description: string readonly secretValue: string + readonly kmsKey: IKey } export class SecretWithParameter extends Construct { @@ -21,7 +23,8 @@ export class SecretWithParameter extends Construct { const secret = new Secret(this, "Secret", { secretName: props.secretName, description: props.description, - secretStringValue: SecretValue.unsafePlainText(props.secretValue) + secretStringValue: SecretValue.unsafePlainText(props.secretValue), + encryptionKey: props.kmsKey }) // Create SSM parameter that references the secret diff --git a/packages/cdk/nagSuppressions.ts b/packages/cdk/nagSuppressions.ts index 92153616..5c39c152 100644 --- a/packages/cdk/nagSuppressions.ts +++ b/packages/cdk/nagSuppressions.ts @@ -180,84 +180,10 @@ export const nagSuppressions = (stack: Stack, account: string) => { ) }) - // Suppress DelayResource IAM and runtime issues - safeAddNagSuppression( - stack, - "/EpsAssistMeStack/VectorIndex/PolicySyncWait/LambdaExecutionRole/Resource", - [ - { - id: "AwsSolutions-IAM4", - reason: "DelayResource Lambda uses AWS managed policy for basic Lambda execution role.", - appliesTo: ["Policy::arn::iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"] - } - ] - ) - - safeAddNagSuppression( - stack, - "/EpsAssistMeStack/VectorIndex/IndexReadyWait/LambdaExecutionRole/Resource", - [ - { - id: "AwsSolutions-IAM4", - reason: "DelayResource Lambda uses AWS managed policy for basic Lambda execution role.", - appliesTo: ["Policy::arn::iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"] - } - ] - ) - - // Suppress DelayProvider framework ServiceRole issues - safeAddNagSuppression( - stack, - "/EpsAssistMeStack/VectorIndex/PolicySyncWait/DelayProvider/framework-onEvent/ServiceRole/Resource", - [ - { - id: "AwsSolutions-IAM4", - reason: "Auto-generated CDK Provider role uses AWS managed policy for Lambda execution.", - appliesTo: ["Policy::arn::iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"] - } - ] - ) - - safeAddNagSuppression( - stack, - "/EpsAssistMeStack/VectorIndex/PolicySyncWait/DelayProvider/framework-onEvent/ServiceRole/DefaultPolicy/Resource", - [ - { - id: "AwsSolutions-IAM5", - reason: "Auto-generated CDK Provider role requires wildcard permissions for Lambda invocation.", - appliesTo: ["Resource:::*"] - } - ] - ) - - safeAddNagSuppression( - stack, - "/EpsAssistMeStack/VectorIndex/IndexReadyWait/DelayProvider/framework-onEvent/ServiceRole/Resource", - [ - { - id: "AwsSolutions-IAM4", - reason: "Auto-generated CDK Provider role uses AWS managed policy for Lambda execution.", - appliesTo: ["Policy::arn::iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"] - } - ] - ) - - safeAddNagSuppression( - stack, - "/EpsAssistMeStack/VectorIndex/IndexReadyWait/DelayProvider/framework-onEvent/ServiceRole/DefaultPolicy/Resource", - [ - { - id: "AwsSolutions-IAM5", - reason: "Auto-generated CDK Provider role requires wildcard permissions for Lambda invocation.", - appliesTo: ["Resource:::*"] - } - ] - ) - // Suppress DelayFunction runtime version warnings safeAddNagSuppression( stack, - "/EpsAssistMeStack/VectorIndex/PolicySyncWait/DelayFunction/Resource", + `/EpsAssistMeStack/VectorIndex/IndexReadyWait/${stack.stackName}-index-ready-waitDelayFunction/Resource`, [ { id: "AwsSolutions-L1", @@ -268,7 +194,7 @@ export const nagSuppressions = (stack: Stack, account: string) => { safeAddNagSuppression( stack, - "/EpsAssistMeStack/VectorIndex/IndexReadyWait/DelayFunction/Resource", + `/EpsAssistMeStack/VectorIndex/PolicySyncWait/${stack.stackName}-policy-sync-waitDelayFunction/Resource`, [ { id: "AwsSolutions-L1", @@ -296,7 +222,7 @@ export const nagSuppressions = (stack: Stack, account: string) => { // see https://github.com/aws/aws-cdk/issues/36269 for issue raised with CDK team safeAddNagSuppression( stack, - "/EpsAssistMeStack/VectorIndex/PolicySyncWait/DelayProvider/framework-onEvent/Resource", + `/EpsAssistMeStack/VectorIndex/PolicySyncWait/${stack.stackName}-policy-sync-waitDelayProvider/framework-onEvent/Resource`, [ { id: "AwsSolutions-L1", @@ -307,7 +233,7 @@ export const nagSuppressions = (stack: Stack, account: string) => { safeAddNagSuppression( stack, - "/EpsAssistMeStack/VectorIndex/IndexReadyWait/DelayProvider/framework-onEvent/Resource", + `/EpsAssistMeStack/VectorIndex/IndexReadyWait/${stack.stackName}-index-ready-waitDelayProvider/framework-onEvent/Resource`, [ { id: "AwsSolutions-L1", @@ -328,22 +254,6 @@ export const nagSuppressions = (stack: Stack, account: string) => { } ] ) - // Suppress BedrockLogging KMS wildcard permissions - safeAddNagSuppression( - stack, - "/EpsAssistMeStack/BedrockLogging/BedrockLoggingRole/DefaultPolicy/Resource", - [ - { - id: "AwsSolutions-IAM5", - reason: "KMS wildcard permissions (GenerateDataKey*, ReEncrypt*) are required for CloudWatch Logs encryption operations.", - appliesTo: [ - "Action::kms:GenerateDataKey*", - "Action::kms:ReEncrypt*", - "Resource:::*" - ] - } - ] - ) // Suppress BedrockLogging Lambda wildcard permissions for Bedrock API safeAddNagSuppression( @@ -369,21 +279,6 @@ export const nagSuppressions = (stack: Stack, account: string) => { ] ) - // Suppress BedrockLogging Provider framework role using AWS managed policy - safeAddNagSuppression( - stack, - "/EpsAssistMeStack/BedrockLogging/LoggingConfigProvider/framework-onEvent/ServiceRole/Resource", - [ - { - id: "AwsSolutions-IAM4", - reason: "Auto-generated CDK Provider role uses AWS managed policy for Lambda execution.", - appliesTo: [ - "Policy::arn::iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" - ] - } - ] - ) - safeAddNagSuppression( stack, "/EpsAssistMeStack/Custom::CDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C/ServiceRole/DefaultPolicy/Resource", @@ -394,17 +289,6 @@ export const nagSuppressions = (stack: Stack, account: string) => { } ] ) - // Suppress BedrockLogging Provider framework wildcard permissions - safeAddNagSuppression( - stack, - "/EpsAssistMeStack/BedrockLogging/LoggingConfigProvider/framework-onEvent/ServiceRole/DefaultPolicy/Resource", - [ - { - id: "AwsSolutions-IAM5", - reason: "Auto-generated CDK Provider role requires wildcard permissions for Lambda invocation." - } - ] - ) safeAddNagSuppression( stack, diff --git a/packages/cdk/resources/BedrockLoggingConfiguration.ts b/packages/cdk/resources/BedrockLoggingConfiguration.ts index 7a4ce96f..e5d0362f 100644 --- a/packages/cdk/resources/BedrockLoggingConfiguration.ts +++ b/packages/cdk/resources/BedrockLoggingConfiguration.ts @@ -1,5 +1,5 @@ import {Construct} from "constructs" -import {CustomResource, RemovalPolicy} from "aws-cdk-lib" +import {CustomResource, Fn, RemovalPolicy} from "aws-cdk-lib" import { Role, ServicePrincipal, @@ -10,6 +10,7 @@ import {LogGroup, RetentionDays} from "aws-cdk-lib/aws-logs" import {Provider} from "aws-cdk-lib/custom-resources" import {Key} from "aws-cdk-lib/aws-kms" import {LambdaFunction} from "../constructs/LambdaFunction" +import {NagSuppressions} from "cdk-nag" export interface BedrockLoggingConfigurationProps { readonly stackName: string @@ -26,61 +27,49 @@ export class BedrockLoggingConfiguration extends Construct { constructor(scope: Construct, id: string, props: BedrockLoggingConfigurationProps) { super(scope, id) - // Create KMS key for encryption - const kmsKey = new Key(this, "BedrockLogsKmsKey", { - description: `KMS key for ${props.stackName} Bedrock logs encryption`, - enableKeyRotation: true, - removalPolicy: RemovalPolicy.DESTROY - }) + const cloudWatchLogsKmsKey = Key.fromKeyArn( + scope, "cloudWatchLogsKmsKey", Fn.importValue("account-resources:CloudwatchLogsKmsKeyArn")) + + const cloudwatchEncryptionKMSPolicy = ManagedPolicy.fromManagedPolicyArn( + scope, "cloudwatchEncryptionKMSPolicyArn", Fn.importValue("account-resources:CloudwatchEncryptionKMSPolicyArn")) // Create CloudWatch Log Group for model invocations const modelInvocationLogGroup = new LogGroup(this, "ModelInvocationLogGroup", { logGroupName: `/aws/bedrock/${props.stackName}/model-invocations`, retention: props.logRetentionInDays as RetentionDays, - encryptionKey: kmsKey, + encryptionKey: cloudWatchLogsKmsKey, removalPolicy: RemovalPolicy.DESTROY }) - // Grant CloudWatch Logs service permission to use KMS key - kmsKey.addToResourcePolicy(new PolicyStatement({ - sid: "Allow CloudWatch Logs", - principals: [new ServicePrincipal(`logs.${props.region}.amazonaws.com`)], - actions: [ - "kms:Encrypt", - "kms:Decrypt", - "kms:ReEncrypt", - "kms:GenerateDataKey", - "kms:DescribeKey" - ], - resources: ["*"], - conditions: { - ArnLike: { - "kms:EncryptionContext:aws:logs:arn": - `arn:aws:logs:${props.region}:${props.account}:log-group:/aws/bedrock/${props.stackName}/*` - } + // Create IAM role for Bedrock to write logs + const putLogsManagedPolicy = new ManagedPolicy(scope, "LambdaPutLogsManagedPolicy", { + description: `write to ${modelInvocationLogGroup.logGroupName} logs`, + statements: [ + new PolicyStatement({ + actions: [ + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + resources: [`${modelInvocationLogGroup.logGroupArn}:*` ] + }) + ] + }) + NagSuppressions.addResourceSuppressions(putLogsManagedPolicy, [ + { + id: "AwsSolutions-IAM5", + // eslint-disable-next-line max-len + reason: "Suppress error for not having wildcards in permissions. This is a fine as we need to have permissions on all log streams under path" } - })) + ]) - // Create IAM role for Bedrock to write logs const bedrockLoggingRole = new Role(this, "BedrockLoggingRole", { assumedBy: new ServicePrincipal("bedrock.amazonaws.com"), - description: "Role for Bedrock to write model invocation logs" - }) - - // Grant permissions to write to CloudWatch Logs - bedrockLoggingRole.addToPolicy(new PolicyStatement({ - actions: [ - "logs:CreateLogStream", - "logs:PutLogEvents", - "logs:DescribeLogStreams" - ], - resources: [ - `${modelInvocationLogGroup.logGroupArn}:*` + description: "Role for Bedrock to write model invocation logs", + managedPolicies: [ + putLogsManagedPolicy, + cloudwatchEncryptionKMSPolicy ] - })) - - // Grant KMS permissions - kmsKey.grantEncryptDecrypt(bedrockLoggingRole) + }) // Create managed policy for Bedrock logging configuration const bedrockLoggingConfigPolicy = new ManagedPolicy(this, "BedrockLoggingConfigPolicy", { @@ -103,7 +92,6 @@ export class BedrockLoggingConfiguration extends Construct { // Create Lambda function for custom resource const loggingConfigFunction = new LambdaFunction(this, "LoggingConfigFunction", { - stackName: props.stackName, functionName: `${props.stackName}-BedrockLoggingConfig`, packageBasePath: "packages/bedrockLoggingConfigFunction", handler: "app.handler.handler", @@ -115,12 +103,47 @@ export class BedrockLoggingConfiguration extends Construct { ENABLE_LOGGING: props.enableLogging !== undefined ? props.enableLogging.toString() : "true", CLOUDWATCH_LOG_GROUP_NAME: modelInvocationLogGroup.logGroupName, CLOUDWATCH_ROLE_ARN: bedrockLoggingRole.roleArn - } + }, + stackName: props.stackName + }) + + const frameworkInvokeBedrockLoggingPolicy = new ManagedPolicy(this, + `${props.stackName}BedrockLoggingInvokeDelayFunctionPolicy`, { + description: `Allow provider framework to invoke ${props.stackName} bedrock logging function`, + statements: [ + new PolicyStatement({ + actions: [ + "lambda:InvokeFunction", + "lambda:GetFunction" + ], + resources: [ + loggingConfigFunction.function.functionArn + ] + }) + ] + }) + + const frameworkOnEventRole = new Role(this, `${props.stackName}BedrockLoggingProviderFrameworkOnEventRole`, { + assumedBy: new ServicePrincipal("lambda.amazonaws.com"), + description: "Execution role for bedrock logging provider framework onEvent Lambda function", + managedPolicies: [ + putLogsManagedPolicy, + cloudwatchEncryptionKMSPolicy, + frameworkInvokeBedrockLoggingPolicy + ] }) + const frameworkOnEventRoleRef = Role.fromRoleArn( + this, + `${props.stackName}OnEventRoleRef`, + frameworkOnEventRole.roleArn, + {mutable: false} + ) // Create custom resource provider const provider = new Provider(this, "LoggingConfigProvider", { - onEventHandler: loggingConfigFunction.function + onEventHandler: loggingConfigFunction.function, + logGroup: modelInvocationLogGroup, + frameworkOnEventRole: frameworkOnEventRoleRef }) // Create custom resource diff --git a/packages/cdk/resources/Secrets.ts b/packages/cdk/resources/Secrets.ts index b8b2b1a0..5593ed53 100644 --- a/packages/cdk/resources/Secrets.ts +++ b/packages/cdk/resources/Secrets.ts @@ -2,6 +2,8 @@ import {Construct} from "constructs" import {StringParameter} from "aws-cdk-lib/aws-ssm" import {Secret} from "aws-cdk-lib/aws-secretsmanager" import {SecretWithParameter} from "../constructs/SecretWithParameter" +import {Key} from "aws-cdk-lib/aws-kms" +import {RemovalPolicy} from "aws-cdk-lib" export interface SecretsProps { readonly stackName: string @@ -14,16 +16,25 @@ export class Secrets extends Construct { public readonly slackBotSigningSecret: Secret public readonly slackBotTokenParameter: StringParameter public readonly slackSigningSecretParameter: StringParameter + public readonly secretKmsKey: Key constructor(scope: Construct, id: string, props: SecretsProps) { super(scope, id) + const kmsKey = new Key(this, "BucketKey", { + enableKeyRotation: true, + description: `KMS key for ${props.stackName} secret encryption`, + removalPolicy: RemovalPolicy.DESTROY + }) + kmsKey.addAlias(`alias/${props.stackName}-secret-key`) + // Create Slack bot OAuth token secret and parameter const slackBotToken = new SecretWithParameter(this, "SlackBotToken", { secretName: `/${props.stackName}/bot-token`, parameterName: `/${props.stackName}/bot-token/parameter`, description: "Slack Bot OAuth Token for EPS Assist", - secretValue: JSON.stringify({token: props.slackBotToken}) + secretValue: JSON.stringify({token: props.slackBotToken}), + kmsKey: kmsKey }) // Create Slack signing secret for request verification @@ -31,7 +42,8 @@ export class Secrets extends Construct { secretName: `/${props.stackName}/signing-secret`, parameterName: `/${props.stackName}/signing-secret/parameter`, description: "Slack Signing Secret", - secretValue: JSON.stringify({secret: props.slackSigningSecret}) + secretValue: JSON.stringify({secret: props.slackSigningSecret}), + kmsKey: kmsKey }) // Export secrets and parameters for use by other constructs @@ -39,5 +51,6 @@ export class Secrets extends Construct { this.slackBotSigningSecret = slackBotSigning.secret this.slackBotTokenParameter = slackBotToken.parameter this.slackSigningSecretParameter = slackBotSigning.parameter + this.secretKmsKey = kmsKey } } diff --git a/packages/cdk/resources/VectorIndex.ts b/packages/cdk/resources/VectorIndex.ts index 9d843f28..66ef0a8b 100644 --- a/packages/cdk/resources/VectorIndex.ts +++ b/packages/cdk/resources/VectorIndex.ts @@ -55,7 +55,7 @@ export class VectorIndex extends Construct { } } - const cfnIndex = new CfnIndex(this, "MyCfnIndex", { + const cfnIndex = new CfnIndex(this, "OpenSearchIndex", { collectionEndpoint: props.collection.collectionEndpoint, indexName: this.indexName, mappings: indexMapping, @@ -71,7 +71,8 @@ export class VectorIndex extends Construct { // to ensure data access policies are synced before index creation const policySyncWait = new DelayResource(this, "PolicySyncWait", { delaySeconds: 60, - description: "Wait for OpenSearch data access policies to sync" + description: "Wait for OpenSearch data access policies to sync", + name: `${props.stackName}-policy-sync-wait` }) policySyncWait.customResource.node.addDependency(props.collection.dataAccessPolicy) @@ -85,7 +86,8 @@ export class VectorIndex extends Construct { // to ensure index is actually available for Bedrock const indexReadyWait = new DelayResource(this, "IndexReadyWait", { delaySeconds: 60, - description: "Wait for OpenSearch index to be fully available" + description: "Wait for OpenSearch index to be fully available", + name: `${props.stackName}-index-ready-wait` }) indexReadyWait.customResource.node.addDependency(cfnIndex) diff --git a/packages/cdk/resources/VectorKnowledgeBaseResources.ts b/packages/cdk/resources/VectorKnowledgeBaseResources.ts index 4470224a..0dc07561 100644 --- a/packages/cdk/resources/VectorKnowledgeBaseResources.ts +++ b/packages/cdk/resources/VectorKnowledgeBaseResources.ts @@ -20,6 +20,7 @@ import { CfnLogGroup } from "aws-cdk-lib/aws-logs" import {Key} from "aws-cdk-lib/aws-kms" +import {addSuppressions} from "../bin/utils/appUtils" // Amazon Titan embedding model for vector generation const EMBEDDING_MODEL = "amazon.titan-embed-text-v2:0" @@ -176,13 +177,9 @@ export class VectorKnowledgeBaseResources extends Construct { // Suppress CFN guard rules for log group const cfnlogGroup = kbLogGroup.node.defaultChild as CfnLogGroup - cfnlogGroup.cfnOptions.metadata = { - guard: { - SuppressedRules: [ - "CW_LOGGROUP_RETENTION_PERIOD_CHECK" - ] - } - } + addSuppressions([cfnlogGroup], [ + "CW_LOGGROUP_RETENTION_PERIOD_CHECK" + ]) // Create delivery source for the Knowledge Base const kbDeliverySource = new CfnDeliverySource(this, "KBDeliverySource", { diff --git a/packages/cdk/tsconfig.json b/packages/cdk/tsconfig.json index 36de595f..358b7ad8 100644 --- a/packages/cdk/tsconfig.json +++ b/packages/cdk/tsconfig.json @@ -26,6 +26,6 @@ ] }, "references": [], - "include": ["resources/**/*", "constructs/**/*", "stacks/**/*", "tests/**/*", "nagSuppressions.ts"], + "include": ["resources/**/*", "constructs/**/*", "stacks/**/*", "tests/**/*", "nagSuppressions.ts", "bin/**/*.ts"], "exclude": ["node_modules", "cdk.out"] } diff --git a/packages/slackBotFunction/app/services/bedrock.py b/packages/slackBotFunction/app/services/bedrock.py index 44d02019..8bb06f0b 100644 --- a/packages/slackBotFunction/app/services/bedrock.py +++ b/packages/slackBotFunction/app/services/bedrock.py @@ -8,7 +8,6 @@ from app.core.config import get_retrieve_generate_config, get_logger from app.services.prompt_loader import load_prompt - logger = get_logger() diff --git a/packages/slackBotFunction/app/services/slack.py b/packages/slackBotFunction/app/services/slack.py index 66fe280e..dcf09c06 100644 --- a/packages/slackBotFunction/app/services/slack.py +++ b/packages/slackBotFunction/app/services/slack.py @@ -3,7 +3,6 @@ from slack_sdk import WebClient from app.core.config import bot_messages, get_logger - logger = get_logger() diff --git a/packages/slackBotFunction/app/slack/slack_events.py b/packages/slackBotFunction/app/slack/slack_events.py index 726968cc..a66c2ad1 100644 --- a/packages/slackBotFunction/app/slack/slack_events.py +++ b/packages/slackBotFunction/app/slack/slack_events.py @@ -40,7 +40,6 @@ from app.services.ai_processor import process_ai_query - logger = get_logger() processing_error_message = "Error processing message" diff --git a/packages/slackBotFunction/tests/conftest.py b/packages/slackBotFunction/tests/conftest.py index 3b62aecc..72788718 100644 --- a/packages/slackBotFunction/tests/conftest.py +++ b/packages/slackBotFunction/tests/conftest.py @@ -4,7 +4,6 @@ from unittest.mock import MagicMock, Mock, patch import os - TEST_BOT_TOKEN = "test-bot-token" TEST_SIGNING_SECRET = "test-signing-secret" diff --git a/scripts/run_regression_tests.py b/scripts/run_regression_tests.py index 772c6a5f..be906eb5 100644 --- a/scripts/run_regression_tests.py +++ b/scripts/run_regression_tests.py @@ -4,6 +4,7 @@ Script to generate user defined unique ID which can be used to check the status of the regression test run to be reported to the CI. """ + import argparse from datetime import datetime, timedelta, timezone import random