Skip to content

Commit

Permalink
update codes
Browse files Browse the repository at this point in the history
  • Loading branch information
GavinZZ committed Jun 26, 2024
1 parent 4104a8e commit 8d38c20
Show file tree
Hide file tree
Showing 4 changed files with 94 additions and 7 deletions.
53 changes: 52 additions & 1 deletion packages/aws-cdk-lib/custom-resources/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ The return value from `onEvent` must be a JSON object with the following fields:
| -------------------- | ------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `PhysicalResourceId` | String | No | The allocated/assigned physical ID of the resource. If omitted for `Create` events, the event's `RequestId` will be used. For `Update`, the current physical ID will be used. If a different value is returned, CloudFormation will follow with a subsequent `Delete` for the previous ID (resource replacement). For `Delete`, it will always return the current physical resource ID, and if the user returns a different one, an error will occur. |
| `Data` | JSON | No | Resource attributes, which can later be retrieved through `Fn::GetAtt` on the custom resource object. |
| `NoEcho` | Boolean | No | Whether to mask the output of the custom resource when retrieved by using the `Fn::GetAtt` function. |
| `NoEcho` | Boolean | No | Whether to mask the output of the custom resource when retrieved by using the `Fn::GetAtt` function and to mask the `Data` values. |
| *any* | *any* | No | Any other field included in the response will be passed through to `isComplete`. This can sometimes be useful to pass state between the handlers. |

[Custom Resource Provider Request]: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/crpg-ref-requests.html#crpg-ref-request-fields
Expand Down Expand Up @@ -215,6 +215,57 @@ must return this name in `PhysicalResourceId` and make sure to handle
replacement properly. The `S3File` example demonstrates this
through the `objectKey` property.

### Masking the output of log statements

When using the Provider framework to create a custom resource, the request and response
objects are logged by the provider function.If secret values are returned in the custom
resource's Data object, it would be logged and exposed which possesses security threats.

To mask the output of log statements, you can utilize the `NoEcho` field in the custom
resource handler's response.

```ts
// Create custom resource handler entrypoint
const handler = new lambda.Function(this , 'my-handler', {
runtime: lambda.Runtime.NODEJS_20_X,
handler: 'index.handler',
code: lambda.Code.fromInline(`
exports.handler = async (event, context) => {
return {
PhysicalResourceId: '1234',
NoEcho: true,
Data: {
mySecret: 'secret-value',
hello: 'world',
ghToken: 'gho_xxxxxxx',
},
};
};`),
});

// Provision a custom resource provider framework
const provider = new Provider(this , 'my-provider', {
onEventHandler: handler,
});

new CustomResource(this , 'my-cr', {
serviceToken: provider.serviceToken,
});
```

When `NoEcho` field is set to `true` in the response of custom resource handler,
it will automatically mask all values in the `Data` object in the log statements.

```ts
PhysicalResourceId: '1234',
NoEcho: true,
Data: {
mySecret: '*****',
hello: '*****',
ghToken: '*****',
}
```

### When there are errors

As mentioned above, if any of the user handlers fail (i.e. throws an exception)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import * as url from 'url';
import { httpRequest } from './outbound';
import { log, withRetries } from './util';
import { OnEventResponse } from '../types';

export const CREATE_FAILED_PHYSICAL_ID_MARKER = 'AWSCDK::CustomResourceProviderFramework::CREATE_FAILED';
export const MISSING_PHYSICAL_ID_MARKER = 'AWSCDK::CustomResourceProviderFramework::MISSING_PHYSICAL_ID';
Expand Down Expand Up @@ -37,7 +38,11 @@ export async function submitResponse(status: 'SUCCESS' | 'FAILED', event: CloudF

const parsedUrl = url.parse(event.ResponseURL);
const loggingSafeUrl = `${parsedUrl.protocol}//${parsedUrl.hostname}/${parsedUrl.pathname}?***`;
log('submit response to cloudformation', loggingSafeUrl, json);
if (options.noEcho) {
log('submit redacted response to cloudformation', loggingSafeUrl, redactDataFromPayload(json));
} else {
log('submit response to cloudformation', loggingSafeUrl, json);
}

const retryOptions = {
attempts: 5,
Expand Down Expand Up @@ -101,4 +106,18 @@ export function safeHandler(block: (event: any) => Promise<void>) {
};
}

export function redactDataFromPayload(payload: OnEventResponse) {
// Create a deep copy of the payload object
const redactedPayload: OnEventResponse = JSON.parse(JSON.stringify(payload));

// Redact the data in the copied payload object
if (redactedPayload.Data) {
const keys = Object.keys(redactedPayload.Data);
for (const key of keys) {
redactedPayload.Data[key] = '*****';
}
}
return redactedPayload;
}

export class Retry extends Error { }
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,20 @@ async function onEvent(cfnRequest: AWSLambda.CloudFormationCustomResourceEvent)
cfnRequest.ResourceProperties = cfnRequest.ResourceProperties || { };

const onEventResult = await invokeUserFunction(consts.USER_ON_EVENT_FUNCTION_ARN_ENV, sanitizedRequest, cfnRequest.ResponseURL) as OnEventResponse;
log('onEvent returned:', onEventResult);
if (onEventResult.NoEcho) {
log('redacted onEvent returned:', cfnResponse.redactDataFromPayload(onEventResult));
} else {
log('onEvent returned:', onEventResult);
}

// merge the request and the result from onEvent to form the complete resource event
// this also performs validation.
const resourceEvent = createResponseEvent(cfnRequest, onEventResult);
log('event:', onEventResult);
if (onEventResult.NoEcho) {
log('readacted event:', cfnResponse.redactDataFromPayload(resourceEvent));
} else {
log('event:', resourceEvent);
}

// determine if this is an async provider based on whether we have an isComplete handler defined.
// if it is not defined, then we are basically ready to return a positive response.
Expand All @@ -62,10 +70,18 @@ async function onEvent(cfnRequest: AWSLambda.CloudFormationCustomResourceEvent)
// invoked a few times until `complete` is true or until it times out.
async function isComplete(event: AWSCDKAsyncCustomResource.IsCompleteRequest) {
const sanitizedRequest = { ...event, ResponseURL: '...' } as const;
log('isComplete', sanitizedRequest);
if (event.NoEcho) {
log('redacted isComplete request', cfnResponse.redactDataFromPayload(sanitizedRequest));
} else {
log('isComplete', sanitizedRequest);
}

const isCompleteResult = await invokeUserFunction(consts.USER_IS_COMPLETE_FUNCTION_ARN_ENV, sanitizedRequest, event.ResponseURL) as IsCompleteResponse;
log('user isComplete returned:', isCompleteResult);
if (event.NoEcho) {
log('redacted user isComplete returned:', cfnResponse.redactDataFromPayload(isCompleteResult));
} else {
log('user isComplete returned:', isCompleteResult);
}

// if we are not complete, return false, and don't send a response back.
if (!isCompleteResult.IsComplete) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,8 @@ interface OnEventResponse {
/**
* Whether to mask the output of the custom resource when retrieved
* by using the `Fn::GetAtt` function. If set to `true`, all returned
* values are masked with asterisks (*****).
* values are masked with asterisks (*****) and to mask the `Data` values
* with asterisks (*****) in the log statements.
*
* @default false
*/
Expand Down

0 comments on commit 8d38c20

Please sign in to comment.