diff --git a/.projenrc.ts b/.projenrc.ts index 249e1039d..f319322ba 100644 --- a/.projenrc.ts +++ b/.projenrc.ts @@ -899,6 +899,7 @@ const toolkitLib = configureProject( sdkDep('@aws-sdk/client-cloudformation'), sdkDep('@aws-sdk/client-cloudwatch-logs'), sdkDep('@aws-sdk/client-cloudcontrol'), + sdkDep('@aws-sdk/client-cloudtrail'), sdkDep('@aws-sdk/client-codebuild'), sdkDep('@aws-sdk/client-ec2'), sdkDep('@aws-sdk/client-ecr'), diff --git a/packages/@aws-cdk/integ-runner/THIRD_PARTY_LICENSES b/packages/@aws-cdk/integ-runner/THIRD_PARTY_LICENSES index 04e32fc6b..7020a3edf 100644 --- a/packages/@aws-cdk/integ-runner/THIRD_PARTY_LICENSES +++ b/packages/@aws-cdk/integ-runner/THIRD_PARTY_LICENSES @@ -1028,6 +1028,212 @@ The @aws-cdk/integ-runner package includes the following third-party software/li limitations under the License. +---------------- + +** @aws-sdk/client-cloudtrail@3.1075.0 - https://www.npmjs.com/package/@aws-sdk/client-cloudtrail/v/3.1075.0 | Apache-2.0 + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2018-2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + ---------------- ** @aws-sdk/client-cloudwatch-logs@3.1078.0 - https://www.npmjs.com/package/@aws-sdk/client-cloudwatch-logs/v/3.1078.0 | Apache-2.0 @@ -4120,7 +4326,7 @@ The @aws-cdk/integ-runner package includes the following third-party software/li ---------------- -** @aws-sdk/core@3.974.26 - https://www.npmjs.com/package/@aws-sdk/core/v/3.974.26 | Apache-2.0 +** @aws-sdk/core@3.974.28 - https://www.npmjs.com/package/@aws-sdk/core/v/3.974.28 | Apache-2.0 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ @@ -4531,7 +4737,7 @@ Apache License ---------------- -** @aws-sdk/credential-provider-env@3.972.52 - https://www.npmjs.com/package/@aws-sdk/credential-provider-env/v/3.972.52 | Apache-2.0 +** @aws-sdk/credential-provider-env@3.972.54 - https://www.npmjs.com/package/@aws-sdk/credential-provider-env/v/3.972.54 | Apache-2.0 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ @@ -4736,11 +4942,11 @@ Apache License ---------------- -** @aws-sdk/credential-provider-http@3.972.54 - https://www.npmjs.com/package/@aws-sdk/credential-provider-http/v/3.972.54 | Apache-2.0 +** @aws-sdk/credential-provider-http@3.972.56 - https://www.npmjs.com/package/@aws-sdk/credential-provider-http/v/3.972.56 | Apache-2.0 ---------------- -** @aws-sdk/credential-provider-ini@3.972.59 - https://www.npmjs.com/package/@aws-sdk/credential-provider-ini/v/3.972.59 | Apache-2.0 +** @aws-sdk/credential-provider-ini@3.972.61 - https://www.npmjs.com/package/@aws-sdk/credential-provider-ini/v/3.972.61 | Apache-2.0 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ @@ -4945,11 +5151,11 @@ Apache License ---------------- -** @aws-sdk/credential-provider-login@3.972.58 - https://www.npmjs.com/package/@aws-sdk/credential-provider-login/v/3.972.58 | Apache-2.0 +** @aws-sdk/credential-provider-login@3.972.60 - https://www.npmjs.com/package/@aws-sdk/credential-provider-login/v/3.972.60 | Apache-2.0 ---------------- -** @aws-sdk/credential-provider-node@3.972.61 - https://www.npmjs.com/package/@aws-sdk/credential-provider-node/v/3.972.61 | Apache-2.0 +** @aws-sdk/credential-provider-node@3.972.63 - https://www.npmjs.com/package/@aws-sdk/credential-provider-node/v/3.972.63 | Apache-2.0 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ @@ -5154,7 +5360,7 @@ Apache License ---------------- -** @aws-sdk/credential-provider-process@3.972.52 - https://www.npmjs.com/package/@aws-sdk/credential-provider-process/v/3.972.52 | Apache-2.0 +** @aws-sdk/credential-provider-process@3.972.54 - https://www.npmjs.com/package/@aws-sdk/credential-provider-process/v/3.972.54 | Apache-2.0 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ @@ -5359,7 +5565,7 @@ Apache License ---------------- -** @aws-sdk/credential-provider-sso@3.972.58 - https://www.npmjs.com/package/@aws-sdk/credential-provider-sso/v/3.972.58 | Apache-2.0 +** @aws-sdk/credential-provider-sso@3.972.60 - https://www.npmjs.com/package/@aws-sdk/credential-provider-sso/v/3.972.60 | Apache-2.0 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ @@ -5564,7 +5770,7 @@ Apache License ---------------- -** @aws-sdk/credential-provider-web-identity@3.972.58 - https://www.npmjs.com/package/@aws-sdk/credential-provider-web-identity/v/3.972.58 | Apache-2.0 +** @aws-sdk/credential-provider-web-identity@3.972.60 - https://www.npmjs.com/package/@aws-sdk/credential-provider-web-identity/v/3.972.60 | Apache-2.0 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ @@ -7001,7 +7207,7 @@ Apache License ---------------- -** @aws-sdk/nested-clients@3.997.26 - https://www.npmjs.com/package/@aws-sdk/nested-clients/v/3.997.26 | Apache-2.0 +** @aws-sdk/nested-clients@3.997.28 - https://www.npmjs.com/package/@aws-sdk/nested-clients/v/3.997.28 | Apache-2.0 ---------------- @@ -7211,7 +7417,7 @@ Apache License ---------------- -** @aws-sdk/token-providers@3.1078.0 - https://www.npmjs.com/package/@aws-sdk/token-providers/v/3.1078.0 | Apache-2.0 +** @aws-sdk/token-providers@3.1080.0 - https://www.npmjs.com/package/@aws-sdk/token-providers/v/3.1080.0 | Apache-2.0 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ @@ -7621,7 +7827,7 @@ Apache License ---------------- -** @aws/lambda-invoke-store@0.2.4 - https://www.npmjs.com/package/@aws/lambda-invoke-store/v/0.2.4 | Apache-2.0 +** @aws/lambda-invoke-store@0.3.0 - https://www.npmjs.com/package/@aws/lambda-invoke-store/v/0.3.0 | Apache-2.0 Apache License Version 2.0, January 2004 @@ -8084,7 +8290,7 @@ Apache License ---------------- -** @smithy/core@3.29.0 - https://www.npmjs.com/package/@smithy/core/v/3.29.0 | Apache-2.0 +** @smithy/core@3.29.1 - https://www.npmjs.com/package/@smithy/core/v/3.29.1 | Apache-2.0 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ @@ -8905,7 +9111,7 @@ Apache License ---------------- -** @smithy/node-http-handler@4.9.2 - https://www.npmjs.com/package/@smithy/node-http-handler/v/4.9.2 | Apache-2.0 +** @smithy/node-http-handler@4.9.3 - https://www.npmjs.com/package/@smithy/node-http-handler/v/4.9.3 | Apache-2.0 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ diff --git a/packages/@aws-cdk/toolkit-lib/.projen/deps.json b/packages/@aws-cdk/toolkit-lib/.projen/deps.json index 7942149f6..b34300fbe 100644 --- a/packages/@aws-cdk/toolkit-lib/.projen/deps.json +++ b/packages/@aws-cdk/toolkit-lib/.projen/deps.json @@ -229,6 +229,11 @@ "version": "^3", "type": "runtime" }, + { + "name": "@aws-sdk/client-cloudtrail", + "version": "^3", + "type": "runtime" + }, { "name": "@aws-sdk/client-cloudwatch-logs", "version": "^3", diff --git a/packages/@aws-cdk/toolkit-lib/lib/api/aws-auth/sdk.ts b/packages/@aws-cdk/toolkit-lib/lib/api/aws-auth/sdk.ts index d83a2ce34..1c1619ea5 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/api/aws-auth/sdk.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/api/aws-auth/sdk.ts @@ -172,6 +172,14 @@ import { ListChangeSetsCommand, } from '@aws-sdk/client-cloudformation'; import type { OperationEvent } from '@aws-sdk/client-cloudformation/dist-types/models/models_0'; +import { + CloudTrailClient, + LookupEventsCommand, +} from '@aws-sdk/client-cloudtrail'; +import type { + LookupEventsCommandInput, + LookupEventsCommandOutput, +} from '@aws-sdk/client-cloudtrail'; import type { FilterLogEventsCommandInput, FilterLogEventsCommandOutput, @@ -535,6 +543,10 @@ export interface ICloudWatchLogsClient { filterLogEvents(input: FilterLogEventsCommandInput): Promise; } +export interface ICloudTrailClient { + lookupEvents(input: LookupEventsCommandInput): Promise; +} + export interface ICodeBuildClient { updateProject(input: UpdateProjectCommandInput): Promise; } @@ -904,6 +916,14 @@ export class SDK { }; } + public cloudTrail(): ICloudTrailClient { + const client = new CloudTrailClient(this.config); + return { + lookupEvents: (input: LookupEventsCommandInput): Promise => + client.send(new LookupEventsCommand(input)), + }; + } + public codeBuild(): ICodeBuildClient { const client = new CodeBuildClient(this.config); return { diff --git a/packages/@aws-cdk/toolkit-lib/lib/api/aws-auth/util.ts b/packages/@aws-cdk/toolkit-lib/lib/api/aws-auth/util.ts index 3e0a4403e..804df1d0c 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/api/aws-auth/util.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/api/aws-auth/util.ts @@ -1,5 +1,17 @@ import * as fs from 'fs-extra'; +/** + * Whether an error from an AWS SDK call is an authorization failure. + * + * Services are inconsistent in how they phrase it: most raise `AccessDeniedException` + * (e.g. CloudTrail, SSM), some `AccessDenied` (e.g. S3), and some surface the code on + * `Code` rather than `name`. Match all of them. + */ +export function isAccessDeniedError(e: any): boolean { + const name = e?.name ?? e?.Code ?? ''; + return name === 'AccessDenied' || name === 'AccessDeniedException'; +} + /** * Read a file if it exists, or return undefined * diff --git a/packages/@aws-cdk/toolkit-lib/lib/api/diagnosing/cloudtrail-investigation.ts b/packages/@aws-cdk/toolkit-lib/lib/api/diagnosing/cloudtrail-investigation.ts new file mode 100644 index 000000000..e45941e2c --- /dev/null +++ b/packages/@aws-cdk/toolkit-lib/lib/api/diagnosing/cloudtrail-investigation.ts @@ -0,0 +1,596 @@ +import { + assumedRoleSessionName, + functionNameFromArnOrName, + identityMatches, + nameSegment, + serviceTokenReferencedLogicalId, +} from './resource-identifiers'; +import { getStackTemplate } from './stack-template'; +import type { AdditionalDiagnosticContext } from '../../actions/diagnose'; +import type { ICloudFormationClient, ICloudTrailClient, SDK } from '../aws-auth/sdk'; +import { isAccessDeniedError } from '../aws-auth/util'; +import type { IoHelper } from '../io/private/io-helper'; +import type { ResourceError } from '../stack-events/resource-errors'; + +/** + * Stack-level CloudTrail investigation. + * + * CloudTrail records management (control-plane) events — roughly, low-traffic configuration + * APIs — including calls that fail. Those failures often carry the root cause of a deployment + * failure that never reaches CloudFormation: a custom resource's handler being denied an API + * call, or a service acting on a resource's behalf with that resource's role (e.g. Lambda + * creating ENIs for a VPC-attached function). Data-plane calls (e.g. `s3:PutObject`) are not + * recorded, so failures on those remain invisible to this investigation. + * + * The `LookupEvents` API accepts at most one server-side filter, and service-on-your-behalf + * calls use unpredictable assumed-role session names, so there is no per-resource lookup key + * we could compute. Instead we do one unfiltered sweep over the failure window and correlate + * client-side: an event belongs to the stack when its *calling principal* carries a known + * stack identity (a physical ID, an IAM role referenced by resource properties, or a + * custom-resource backing function) as a whole ARN segment. Matching only the caller — never + * the resources a call touched — keeps unrelated principals' activity against stack resources + * out; a missed correlation degrades to "no events found", not a wrong attribution. + */ + +/** + * How far before the first failure event to scan. + * + * The fatal API call always precedes the failure event CloudFormation records, sometimes by + * minutes (e.g. a provider-framework handler that polls before giving up). + */ +const SWEEP_WINDOW_BEFORE_MS = 5 * 60 * 1000; + +/** + * How far after the last failure event to scan. + * + * Small on purpose: rollback begins right after the last failure event, and teardown + * produces large numbers of irrelevant errored events (e.g. polling of already-deleted + * resources) that would drown out the root cause. + */ +const SWEEP_WINDOW_AFTER_MS = 2 * 60 * 1000; + +/** + * Maximum pages of `LookupEvents` results (50 events each) per sweep. + * + * A quiet account produces ~2-3 pages for a five-minute window; busy accounts can exceed + * this cap, in which case the output discloses that the scan was truncated. + */ +const MAX_SWEEP_PAGES = 10; + +/** + * Maximum distinct error events to surface. + */ +const MAX_EVENTS_SHOWN = 5; + +/** + * Minimum length for an identity key. Matching is by whole ARN segment (never bare + * substring), so this only guards against degenerate keys — empty or so short and generic + * that an exact segment collision with an unrelated principal is conceivable. + */ +const MIN_IDENTITY_KEY_LENGTH = 4; + +/** + * If the failure is more recent than this, events may not have been delivered yet + * (CloudTrail delivery averages ~5 minutes, up to 15), so more may appear on a later run. + */ +const DELIVERY_LATENCY_MS = 15 * 60 * 1000; + +/** + * An identity that ties CloudTrail events back to a stack resource. + */ +interface StackIdentity { + /** + * The segment value to look for in the event's calling-principal ARNs. + */ + readonly key: string; + + /** + * The stack the identified resource belongs to. + */ + readonly stackArn: string; + + /** + * Logical ID of the stack resource this identity belongs to. + */ + readonly logicalId: string; + + /** + * Human-readable description of the resource, used to attribute events whose resource is + * not itself among the reported problems. + */ + readonly description: string; +} + +/** + * A CloudTrail error event correlated to a stack resource. + */ +export interface CorrelatedCloudTrailError { + /** + * The stack of the resource the event was attributed to. + */ + readonly stackArn: string; + + /** + * Logical ID of the stack resource the event was attributed to. + */ + readonly logicalId: string; + + /** + * Human-readable description of the attributed resource. + */ + readonly description: string; + + /** + * The formatted, user-facing description of the event. + */ + readonly message: string; + + /** + * Time the API call was made (not when the event was delivered). + */ + readonly eventTime?: Date; +} + +/** + * Result of a CloudTrail investigation for a stack. + */ +export interface CloudTrailInvestigation { + /** + * Correlated error events, deduplicated, earliest-first, capped at {@link MAX_EVENTS_SHOWN}. + */ + readonly errors: CorrelatedCloudTrailError[]; + + /** + * Extra notes about the scan itself (truncation, overflow, delivery-latency hint). + */ + readonly notes: string[]; +} + +/** + * Investigate a failed stack via CloudTrail: sweep the failure window for errored + * control-plane calls made by the stack's own principals, and return them attributed to the + * resource they belong to. + * + * Returns `undefined` when the investigation could not run (no failure timestamps to bound + * the window, or the lookup itself failed). Lookup failures are reported through the IoHost + * as warnings — a broken lookup is a tooling problem, not a diagnosis result. + */ +export async function investigateStackViaCloudTrail( + sdk: SDK, + errors: readonly ResourceError[], + ioHelper: IoHelper, +): Promise { + const debug = (msg: string) => ioHelper.defaults.debug(msg); + + // Without failure timestamps (change-set / early-validation errors) we cannot bound the + // sweep; an unbounded window would surface unrelated account activity. + const timestamps = errors.map((e) => e.timestamp?.valueOf()).filter((t): t is number => t !== undefined); + if (timestamps.length === 0) { + await debug('CloudTrail investigation: no failure timestamps to bound the window'); + return undefined; + } + + const windowStart = new Date(Math.min(...timestamps) - SWEEP_WINDOW_BEFORE_MS); + // All failure events precede the rollback that follows them, so ending the window shortly + // after the last failure event naturally excludes rollback/teardown noise. + const windowEnd = new Date(Math.max(...timestamps) + SWEEP_WINDOW_AFTER_MS); + + const cfn = sdk.cloudFormation(); + const identities: StackIdentity[] = []; + for (const stackArn of new Set(errors.map((e) => e.stackArn).filter((s) => !!s))) { + identities.push(...await buildStackIdentitySet(cfn, stackArn, debug)); + } + if (identities.length === 0) { + await debug('CloudTrail investigation: no stack identities to correlate against'); + return undefined; + } + + const sweep = await sweepCloudTrail(sdk.cloudTrail(), windowStart, windowEnd, ioHelper); + if (!sweep) { + return undefined; + } + + const correlated = correlateEvents(sweep.events, identities); + const { shown, omitted } = presentEvents(correlated); + + const notes: string[] = []; + if (sweep.truncated) { + notes.push(`(only the most recent ${sweep.eventsChecked} CloudTrail events in the window were checked)`); + } + if (omitted > 0) { + notes.push(`(${omitted} more distinct CloudTrail error event(s) not shown)`); + } + // Delivery latency applies whether or not something was already found: the fatal event may + // still be in flight while an earlier, benign error has already been delivered. + if (Date.now() - Math.max(...timestamps) < DELIVERY_LATENCY_MS) { + notes.push( + 'CloudTrail delivery can lag up to 15 minutes behind the failure; ' + + 're-run `cdk diagnose` in a few minutes to check for additional control-plane errors (e.g. AccessDenied).', + ); + } + + return { errors: shown, notes }; +} + +/** + * A CloudTrail context attributed to a specific stack resource. + */ +export interface AttributedCloudTrailContext { + /** + * The stack of the resource the context belongs to. + */ + readonly stackArn: string; + + /** + * Logical ID of the resource the context belongs to. + */ + readonly logicalId: string; + + /** + * Human-readable description of the resource, for labeling the context when it cannot be + * attached to that resource's own problem entry. + */ + readonly description: string; + + /** + * The diagnostic context to attach. + */ + readonly context: AdditionalDiagnosticContext; +} + +/** + * Group a CloudTrail investigation's findings into per-resource diagnostic contexts. + * + * Grouping is per (stack, logical ID) — logical IDs alone are not unique across nested + * stacks. The caller attaches each context to the matching problem, or presents it with its + * `description` label when the resource is not itself among the problems. + */ +export function attributedCloudTrailContexts(investigation: CloudTrailInvestigation): AttributedCloudTrailContext[] { + const byResource = new Map(); + for (const e of investigation.errors) { + const key = `${e.stackArn}|${e.logicalId}`; + const existing = byResource.get(key); + if (existing) { + existing.messages.push(e.message); + } else { + byResource.set(key, { stackArn: e.stackArn, logicalId: e.logicalId, description: e.description, messages: [e.message] }); + } + } + + return [...byResource.values()].map((r) => ({ + stackArn: r.stackArn, + logicalId: r.logicalId, + description: r.description, + context: { source: 'CloudTrail Errors', messages: r.messages }, + })); +} + +/** + * Build the set of identities that tie CloudTrail events back to this stack: + * + * - physical IDs of all stack resources (a Lambda's role-session name is its function name; + * role physical IDs appear in assumed-role session ARNs); + * - IAM roles referenced by resource properties named `Role`/`RoleArn`/`ServiceToken` at any + * depth in the template (services acting on a resource's behalf assume these; the role + * *name* — not ARN — is embedded in the resulting session ARN, so bare names are indexed + * too). + * + * Best-effort: if the resource listing fails, template-derived identities (e.g. a literal + * ServiceToken ARN) are still collected. + */ +async function buildStackIdentitySet( + cfn: ICloudFormationClient, + stackArn: string, + debug: (msg: string) => Promise, +): Promise { + const identities: StackIdentity[] = []; + const seen = new Set(); + const add = (key: string | undefined, logicalId: string, description: string) => { + if (key && key.length >= MIN_IDENTITY_KEY_LENGTH && !seen.has(key)) { + seen.add(key); + identities.push({ key, stackArn, logicalId, description }); + } + }; + + const physicalIdByLogicalId = new Map(); + try { + const resp = await cfn.describeStackResources({ StackName: stackArn }); + for (const r of resp.StackResources ?? []) { + if (r.LogicalResourceId && r.PhysicalResourceId) { + physicalIdByLogicalId.set(r.LogicalResourceId, r.PhysicalResourceId); + add(r.PhysicalResourceId, r.LogicalResourceId, `${r.LogicalResourceId} (${r.ResourceType})`); + // ARN-shaped physical IDs also get their trailing name segment indexed: session ARNs + // embed names, not ARNs. + add(nameSegment(r.PhysicalResourceId), r.LogicalResourceId, `${r.LogicalResourceId} (${r.ResourceType})`); + } + } + } catch (e: any) { + // Not fatal: template-derived identities below don't need the physical IDs (literal + // ARNs/names resolve on their own; only Ref/GetAtt references do). + await debug(`CloudTrail investigation: failed to list stack resources: ${e.message}`); + } + + const template = await getStackTemplate(cfn, stackArn, debug); + for (const [logicalId, resource] of Object.entries(template?.Resources ?? {})) { + for (const { propName, value } of collectPrincipalProperties(resource?.Properties)) { + const resolved = resolveToIdentity(value, physicalIdByLogicalId); + if (!resolved) { + continue; + } + const description = `${logicalId} (via ${propName})`; + if (propName === 'ServiceToken') { + // The backing function's role-session name is the function name. + add(functionNameFromArnOrName(resolved), logicalId, description); + } else { + add(resolved, logicalId, description); + add(nameSegment(resolved), logicalId, description); + } + } + } + + return identities; +} + +/** + * Collect properties that reference a calling principal — `Role`/`RoleArn` (any depth, since + * many resources nest role references, e.g. Firehose destination configurations or + * EventBridge targets) and the top-level `ServiceToken` of custom resources. + */ +function collectPrincipalProperties(props: any, depth = 0): Array<{ propName: string; value: any }> { + if (!props || typeof props !== 'object' || depth > 10) { + return []; + } + const ret: Array<{ propName: string; value: any }> = []; + for (const [key, value] of Object.entries(props)) { + if (/Role(Arn)?$/i.test(key) || key === 'ServiceToken') { + ret.push({ propName: key, value }); + } + ret.push(...collectPrincipalProperties(value, depth + 1)); + } + return ret; +} + +/** + * Resolve a template property value to a matchable identity string: literals pass through, + * `Ref`/`Fn::GetAtt` resolve via the resource's physical ID, and an `Fn::Sub` role ARN + * yields its role name when that segment is literal. Unresolvable expressions (e.g. + * `Fn::ImportValue`) return `undefined` — a missed identity degrades to "no events found". + */ +function resolveToIdentity(value: any, physicalIdByLogicalId: Map): string | undefined { + if (typeof value === 'string') { + return value; + } + const referenced = serviceTokenReferencedLogicalId(value); + if (referenced) { + return physicalIdByLogicalId.get(referenced); + } + const sub = value?.['Fn::Sub']; + if (typeof sub === 'string') { + if (!sub.includes('${')) { + return sub; + } + // Common shape: 'arn:aws:iam::${AWS::AccountId}:role/' — the name segment + // is all we need for matching, and it is often free of substitutions. + const name = nameSegment(sub); + if (name && !name.includes('${')) { + return name; + } + } + return undefined; +} + +interface ParsedCloudTrailEvent { + readonly eventTime?: Date; + readonly eventSource?: string; + readonly eventName?: string; + readonly errorCode?: string; + readonly errorMessage?: string; + + /** + * ARNs identifying the CALLING principal. Deliberately excludes `resources[]` (what the + * call touched): matching on touched resources would attribute unrelated principals' + * errored calls against stack resources to the stack. + */ + readonly callerIdentityStrings: string[]; +} + +interface SweepResult { + /** + * The errored events found in the window. + */ + readonly events: ParsedCloudTrailEvent[]; + + /** + * Total events examined (errored or not) — the number the truncation note discloses. + */ + readonly eventsChecked: number; + + readonly truncated: boolean; +} + +/** + * One unfiltered `LookupEvents` sweep over the window, filtered to errored events. + * + * Failures of the lookup itself are not diagnosis results: an inaccessible CloudTrail is a + * tooling problem, so it is reported as an IoHost warning (with the missing-permission hint + * when applicable) and the investigation returns `undefined`. + */ +async function sweepCloudTrail( + cloudTrail: ICloudTrailClient, + windowStart: Date, + windowEnd: Date, + ioHelper: IoHelper, +): Promise { + const events: ParsedCloudTrailEvent[] = []; + let eventsChecked = 0; + let truncated = false; + try { + let nextToken: string | undefined; + let pages = 0; + do { + const resp = await cloudTrail.lookupEvents({ + StartTime: windowStart, + EndTime: windowEnd, + MaxResults: 50, + NextToken: nextToken, + }); + pages++; + for (const e of resp.Events ?? []) { + eventsChecked++; + const parsed = parseCloudTrailEvent(e.CloudTrailEvent); + if (parsed?.errorCode) { + events.push(parsed); + } + } + nextToken = resp.NextToken; + if (nextToken && pages >= MAX_SWEEP_PAGES) { + truncated = true; + break; + } + } while (nextToken); + } catch (e: any) { + if (isAccessDeniedError(e)) { + await ioHelper.defaults.warn( + 'Could not query CloudTrail for control-plane errors: the lookup role is missing the ' + + '`cloudtrail:LookupEvents` permission. Grant it to surface errors (e.g. AccessDenied) ' + + 'that resource logs may not show.', + ); + } else { + await ioHelper.defaults.debug(`CloudTrail investigation: lookup failed: ${e.message}`); + } + return undefined; + } + return { events, eventsChecked, truncated }; +} + +/** + * Attribute errored events to stack resources. + * + * An event is correlated when a stack identity appears as a whole segment of one of the + * event's calling-principal ARNs. Errored calls made by the deploying principal itself carry + * no stack identity, which is correct: their errors are already in the resource's status + * reason. + * + * A single event can match several identities — an assumed-role session ARN + * (`arn:...:assumed-role//`) contains both the role and, for + * Lambda-made calls, the function name as the session name. Attribution must not depend on + * identity-set insertion order (which follows the unordered `DescribeStackResources` + * response), so the most specific match wins: the session-name segment identifies the actual + * caller (the function), the role only who it borrowed permissions from; ties fall back to + * the longest key. + */ +function correlateEvents( + events: ParsedCloudTrailEvent[], + identities: StackIdentity[], +): Array<{ event: ParsedCloudTrailEvent; identity: StackIdentity }> { + const ret: Array<{ event: ParsedCloudTrailEvent; identity: StackIdentity }> = []; + for (const event of events) { + const identity = mostSpecificMatch(event, identities); + if (identity) { + ret.push({ event, identity }); + } + } + return ret; +} + +/** + * The best-matching identity for an event: session-name matches beat other matches, longer + * keys beat shorter ones (a longer segment is a more specific name). + */ +function mostSpecificMatch(event: ParsedCloudTrailEvent, identities: StackIdentity[]): StackIdentity | undefined { + const sessionNames = new Set( + event.callerIdentityStrings + .map(assumedRoleSessionName) + .filter((s): s is string => !!s), + ); + + let best: StackIdentity | undefined; + let bestRank = -1; + for (const identity of identities) { + if (!event.callerIdentityStrings.some((s) => identityMatches(s, identity.key))) { + continue; + } + // Two-tier rank: session-name matches sort above everything else; key length breaks ties. + const rank = (sessionNames.has(identity.key) ? 1_000_000 : 0) + identity.key.length; + if (rank > bestRank) { + best = identity; + bestRank = rank; + } + } + return best; +} + +/** + * Deduplicate and format correlated events for presentation. + * + * Retries and polling produce runs of identical errors; collapse by + * `(eventSource, eventName, errorCode)` keeping the earliest occurrence (the call closest to + * the root cause), present earliest-first, and cap the list. The number of distinct events + * cut by the cap is returned for the caller to disclose. + */ +function presentEvents( + correlated: Array<{ event: ParsedCloudTrailEvent; identity: StackIdentity }>, +): { shown: CorrelatedCloudTrailError[]; omitted: number } { + const byKey = new Map(); + for (const c of correlated) { + const key = `${c.event.eventSource}|${c.event.eventName}|${c.event.errorCode}`; + const existing = byKey.get(key); + if (!existing) { + byKey.set(key, { ...c, count: 1 }); + } else { + existing.count++; + if (c.event.eventTime && existing.event.eventTime && c.event.eventTime < existing.event.eventTime) { + // Keep the earlier event AND its own attribution — the first-seen occurrence may + // have been made by a different resource's principal. + existing.event = c.event; + existing.identity = c.identity; + } + } + } + + const distinct = [...byKey.values()].sort( + (a, b) => (a.event.eventTime?.valueOf() ?? 0) - (b.event.eventTime?.valueOf() ?? 0), + ); + + const shown = distinct.slice(0, MAX_EVENTS_SHOWN).map((c) => ({ + stackArn: c.identity.stackArn, + logicalId: c.identity.logicalId, + description: c.identity.description, + eventTime: c.event.eventTime, + message: formatEvent(c.event, c.count), + })); + + return { shown, omitted: distinct.length - shown.length }; +} + +function formatEvent(event: ParsedCloudTrailEvent, count: number): string { + const occurrences = count > 1 ? ` (${count} occurrences, earliest shown)` : ''; + const message = event.errorMessage ? ` — ${event.errorMessage}` : ''; + return `${event.errorCode} on ${event.eventSource ?? '?'}:${event.eventName ?? '?'}${message}${occurrences}`; +} + +/** Parse the `CloudTrailEvent` JSON blob into the fields we care about. */ +function parseCloudTrailEvent(json: string | undefined): ParsedCloudTrailEvent | undefined { + if (!json) { + return undefined; + } + try { + const obj = JSON.parse(json); + const callerIdentityStrings = [ + obj.userIdentity?.arn, + obj.userIdentity?.sessionContext?.sessionIssuer?.arn, + obj.userIdentity?.inScopeOf?.credentialsIssuedTo, + ].filter((s: any): s is string => typeof s === 'string'); + return { + eventTime: typeof obj.eventTime === 'string' ? new Date(obj.eventTime) : undefined, + eventSource: typeof obj.eventSource === 'string' ? obj.eventSource : undefined, + eventName: typeof obj.eventName === 'string' ? obj.eventName : undefined, + errorCode: typeof obj.errorCode === 'string' ? obj.errorCode : undefined, + errorMessage: typeof obj.errorMessage === 'string' ? obj.errorMessage : undefined, + callerIdentityStrings, + }; + } catch { + return undefined; + } +} + diff --git a/packages/@aws-cdk/toolkit-lib/lib/api/diagnosing/investigate-custom-resource.ts b/packages/@aws-cdk/toolkit-lib/lib/api/diagnosing/investigate-custom-resource.ts index c47a4ffd4..2f768e398 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/api/diagnosing/investigate-custom-resource.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/api/diagnosing/investigate-custom-resource.ts @@ -5,8 +5,8 @@ import { extractLogStreamName, logStreamNameFromPhysicalId, } from './resource-identifiers'; +import { getStackTemplate } from './stack-template'; import type { AdditionalDiagnosticContext } from '../../actions/diagnose'; -import { deserializeStructure } from '../../util'; import type { ICloudFormationClient, ICloudWatchLogsClient, ILambdaClient, SDK } from '../aws-auth/sdk'; import type { ResourceError } from '../stack-events/resource-errors'; @@ -88,27 +88,6 @@ export async function investigateCustomResource( return fetchCustomResourceLogs(cwl, lambda, functionName, templateLogGroup, streamName, err.timestamp, region, debug); } -/** - * Fetch and parse the stack's (original) template. Returns `undefined` if it can't be read. - */ -async function getStackTemplate( - cfn: ICloudFormationClient, - stackName: string, - debug: (msg: string) => Promise, -): Promise { - try { - const resp = await cfn.getTemplate({ StackName: stackName, TemplateStage: 'Original' }); - if (!resp.TemplateBody) { - await debug('Custom resource investigation: empty template body'); - return undefined; - } - return deserializeStructure(resp.TemplateBody); - } catch (e: any) { - await debug(`Custom resource investigation: failed to read template: ${e.message}`); - return undefined; - } -} - /** * Resolve the backing Lambda's configured log group from the template. * diff --git a/packages/@aws-cdk/toolkit-lib/lib/api/diagnosing/investigate-ecs-service.ts b/packages/@aws-cdk/toolkit-lib/lib/api/diagnosing/investigate-ecs-service.ts index 5d8c34a7c..7c5a4df7c 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/api/diagnosing/investigate-ecs-service.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/api/diagnosing/investigate-ecs-service.ts @@ -9,6 +9,9 @@ const FALLBACK_LOG_WINDOW_MS = 30 * 60 * 1000; /** * Options that influence how a resource is investigated. + * + * This is shared by all resource-type investigations, so it only carries options that + * apply across them. */ export interface InvestigateOptions { /** diff --git a/packages/@aws-cdk/toolkit-lib/lib/api/diagnosing/resource-identifiers.ts b/packages/@aws-cdk/toolkit-lib/lib/api/diagnosing/resource-identifiers.ts index 6566c7c8f..c590f28a2 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/api/diagnosing/resource-identifiers.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/api/diagnosing/resource-identifiers.ts @@ -87,3 +87,54 @@ export function logStreamNameFromPhysicalId(physicalId: string | undefined): str export function ecsStoppedTasksConsoleUrl(region: string, cluster: string, serviceName: string): string { return `https://${region}.console.aws.amazon.com/ecs/v2/clusters/${cluster}/services/${serviceName}/tasks?status=STOPPED®ion=${region}`; } + +/** + * The trailing name segment of an ARN-shaped identifier (e.g. the role name out of a role + * ARN), or `undefined` when the value has no path to strip. + */ +export function nameSegment(value: string): string | undefined { + if (!value.includes('/')) { + return undefined; + } + const segment = value.split('/').pop(); + return segment && segment !== value ? segment : undefined; +} + +/** + * The session-name segment of an assumed-role ARN + * (`arn:...:sts:::assumed-role//`), or `undefined` for any + * other shape. + * + * The session name identifies the actual caller: a Lambda execution role's session name is + * the function name, while services assuming a role on a resource's behalf generate one + * (e.g. `awslambda_110_...`). + */ +export function assumedRoleSessionName(arn: string): string | undefined { + return arn.match(/:assumed-role\/[^/]+\/(.+)$/)?.[1]; +} + +/** + * Whether an identity key occurs in an ARN as a whole segment. + * + * Segment-anchored on purpose: a bare substring test would let a key that is a prefix of + * another resource's name (e.g. `my-svc-1` inside `my-svc-10`) claim that resource's events. + * Single-segment keys (names, physical IDs) must equal a full `:`/`/`-delimited segment; + * multi-segment keys (ARNs, path-shaped IDs) must be bounded by delimiters or string ends. + */ +export function identityMatches(haystack: string, key: string): boolean { + if (haystack === key) { + return true; + } + if (!key.includes(':') && !key.includes('/')) { + return haystack.split(/[:/]/).includes(key); + } + const isBoundary = (c: string | undefined) => c === undefined || c === ':' || c === '/'; + let idx = haystack.indexOf(key); + while (idx !== -1) { + if (isBoundary(haystack[idx - 1]) && isBoundary(haystack[idx + key.length])) { + return true; + } + idx = haystack.indexOf(key, idx + 1); + } + return false; +} diff --git a/packages/@aws-cdk/toolkit-lib/lib/api/diagnosing/stack-diagnoser.ts b/packages/@aws-cdk/toolkit-lib/lib/api/diagnosing/stack-diagnoser.ts index a7e1a13df..1cc8887c6 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/api/diagnosing/stack-diagnoser.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/api/diagnosing/stack-diagnoser.ts @@ -2,8 +2,9 @@ import type { ChangeSetSummary, Stack } from '@aws-sdk/client-cloudformation'; import { ChangeSetStatus, ChangeType } from '@aws-sdk/client-cloudformation'; import type { ChangeSetResourceError } from './changeset-error-fetcher'; import { ChangeSetResourceErrorFetcher } from './changeset-error-fetcher'; +import { attributedCloudTrailContexts, investigateStackViaCloudTrail } from './cloudtrail-investigation'; import { investigateResource } from './resource-investigation'; -import type { StackDiagnosis, StackProblemSource, TracedResourceError } from '../../actions/diagnose'; +import type { AdditionalDiagnosticContext, StackDiagnosis, StackProblemSource, TracedResourceError } from '../../actions/diagnose'; import type { ICloudFormationClient, SDK } from '../aws-auth/sdk'; import type { EnvironmentResources } from '../environment'; import type { IoHelper } from '../io/private/io-helper'; @@ -149,10 +150,70 @@ export class CloudFormationStackDiagnoser { stackStatus: stack.StackStatus ?? '', statusReason: stack.StackStatusReason ?? '', }, - problems: await this.enhanceErrors(errors.all), + problems: await this.addCloudTrailContext(await this.enhanceErrors(errors.all), errors.all), }; } + /** + * Run the stack-level CloudTrail investigation and attach its findings to the problems. + * + * One sweep per diagnosis (cdk diagnose or cdk deploy): the sweep covers the whole failure + * window and correlates events to resources afterwards. A finding whose resource is itself + * among the problems (matched by stack AND logical ID — logical IDs repeat across nested + * stacks) is attached to that problem. Findings for other resources, and notes about the + * scan itself, are labeled with the resource they belong to and reported at the stack + * level. When run right after a deployment, the events may not be delivered yet, in which + * case the output notifies the user. + */ + private async addCloudTrailContext(problems: TracedResourceError[], errs: readonly ResourceError[]): Promise { + const sdk = await this.additionalExplorationSdk(); + if (!sdk || problems.length === 0) { + return problems; + } + + let investigation; + try { + investigation = await investigateStackViaCloudTrail(sdk, errs, this.props.ioHelper); + } catch (e: any) { + await this.props.ioHelper.defaults.debug(`CloudTrail investigation failed: ${e.message}`); + return problems; + } + if (!investigation) { + return problems; + } + + // Contexts whose resource is a reported problem attach to it; the rest are labeled with + // their resource so they don't read as belonging to whatever problem carries them. + const attachable = new Map(); + const stackLevel: AdditionalDiagnosticContext[] = []; + for (const attributed of attributedCloudTrailContexts(investigation)) { + const problem = problems.find((p) => p.logicalId === attributed.logicalId && p.stackArn === attributed.stackArn); + if (problem) { + attachable.set(problem, [...(attachable.get(problem) ?? []), attributed.context]); + } else { + stackLevel.push({ + ...attributed.context, + messages: attributed.context.messages.map((m) => `${attributed.description}: ${m}`), + }); + } + } + if (investigation.notes.length > 0) { + stackLevel.push({ source: 'CloudTrail', messages: investigation.notes }); + } + + return problems.map((p, i) => { + const additional = [ + ...(attachable.get(p) ?? []), + // Stack-level findings have no problem of their own to live under; report them once, + // on the first problem, labeled with the resource they belong to. + ...(i === 0 ? stackLevel : []), + ]; + return additional.length > 0 + ? { ...p, additionalContext: [...(p.additionalContext ?? []), ...additional] } + : p; + }); + } + /** * Diagnose a deployment failure via stack events * @@ -161,7 +222,7 @@ export class CloudFormationStackDiagnoser { private async _diagnoseViaStackEvents(stack: Stack): Promise { const poller = new StackEventPoller(this.cfn, { stackArn: stack.StackId!, - initialPollRange: PollRange.mostRecentOperation(), + initialPollRange: PollRange.mostRecentDeploymentAttempt(), }); // We don't need the resulting events of polling. Polling will automatically update the error collection, diff --git a/packages/@aws-cdk/toolkit-lib/lib/api/diagnosing/stack-template.ts b/packages/@aws-cdk/toolkit-lib/lib/api/diagnosing/stack-template.ts new file mode 100644 index 000000000..7b042cb2b --- /dev/null +++ b/packages/@aws-cdk/toolkit-lib/lib/api/diagnosing/stack-template.ts @@ -0,0 +1,25 @@ +import { deserializeStructure } from '../../util'; +import type { ICloudFormationClient } from '../aws-auth/sdk'; + +/** + * Fetch and parse a stack's (original) template. Returns `undefined` if it can't be read — + * investigations are best-effort, so a failed read is logged at debug and diagnosis + * continues without the template. + */ +export async function getStackTemplate( + cfn: ICloudFormationClient, + stackName: string, + debug: (msg: string) => Promise, +): Promise { + try { + const resp = await cfn.getTemplate({ StackName: stackName, TemplateStage: 'Original' }); + if (!resp.TemplateBody) { + await debug(`Stack template for ${stackName}: empty template body`); + return undefined; + } + return deserializeStructure(resp.TemplateBody); + } catch (e: any) { + await debug(`Stack template for ${stackName}: failed to read: ${e.message}`); + return undefined; + } +} diff --git a/packages/@aws-cdk/toolkit-lib/lib/api/environment/environment-resources.ts b/packages/@aws-cdk/toolkit-lib/lib/api/environment/environment-resources.ts index 3b4d09e9c..117c395ee 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/api/environment/environment-resources.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/api/environment/environment-resources.ts @@ -2,6 +2,7 @@ import type { Environment } from '@aws-cdk/cloud-assembly-api'; import { ToolkitError } from '../../toolkit/toolkit-error'; import { formatErrorMessage } from '../../util'; import type { SDK } from '../aws-auth/private'; +import { isAccessDeniedError } from '../aws-auth/util'; import type { IoHelper } from '../io/private'; import { Notices } from '../notices'; import { type EcrRepositoryInfo, ToolkitInfo } from '../toolkit-info'; @@ -85,7 +86,7 @@ export class EnvironmentResources { doValidate(await this.versionFromSsmParameter(ssmParameterName), this.environment); return; } catch (e: any) { - if (e.name !== 'AccessDeniedException') { + if (!isAccessDeniedError(e)) { throw e; } diff --git a/packages/@aws-cdk/toolkit-lib/lib/api/stack-events/stack-activity-monitor.ts b/packages/@aws-cdk/toolkit-lib/lib/api/stack-events/stack-activity-monitor.ts index d68bab61e..c0a0fa286 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/api/stack-events/stack-activity-monitor.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/api/stack-events/stack-activity-monitor.ts @@ -6,6 +6,7 @@ import { StackProgressMonitor } from './stack-progress-monitor'; import type { StackActivity } from '../../payloads/stack-activity'; import { stackNameFromArn } from '../../util/cloudformation'; import type { ICloudFormationClient } from '../aws-auth/private'; +import { isAccessDeniedError } from '../aws-auth/util'; import type { EnvironmentResources } from '../environment'; import { IO, type IoHelper } from '../io/private'; import { resourceMetadata } from '../resource-metadata/resource-metadata'; @@ -286,7 +287,7 @@ export class StackActivityMonitor { const errorMessage = e instanceof Error ? e.message : String(e); const isPermissionsError = - e.name === 'AccessDeniedException' || + isAccessDeniedError(e) || (typeof errorMessage === 'string' && errorMessage.toLowerCase().includes('not authorized to perform: cloudformation:gethookresult')); if (isPermissionsError && this.envResources) { diff --git a/packages/@aws-cdk/toolkit-lib/lib/api/stack-events/stack-event-poller.ts b/packages/@aws-cdk/toolkit-lib/lib/api/stack-events/stack-event-poller.ts index 7ff0ce11c..bd48b633e 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/api/stack-events/stack-event-poller.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/api/stack-events/stack-event-poller.ts @@ -83,6 +83,47 @@ export abstract class PollRange { }; } + /** + * Include the most recent deployment attempt, spanning a rollback and the operation that + * triggered it. + * + * A failed deployment that rolls back is recorded under two CloudFormation operation IDs: + * the failed create/update and the rollback that follows. Scanning newest-first, the + * rollback is seen first — but its events are mostly successful deletes; the actual resource + * failures (e.g. `CREATE_FAILED`) belong to the *preceding* operation. So when the newest + * operation is a rollback (any of its events carry a ROLLBACK status), we continue into the + * one operation that precedes it. + * + * When the newest operation is NOT a rollback (e.g. a `--no-rollback` failure), it *is* the + * whole deployment attempt, and anything older belongs to a previous, unrelated deployment — + * so we stop at the first operation boundary, exactly like {@link mostRecentOperation}. + */ + public static mostRecentDeploymentAttempt(): IPollRange { + // Track distinct operation IDs seen so far, treating a missing OperationId as its own + // value (the empty string) so events without one still count toward the boundary — + // otherwise an event stream lacking OperationId would never stop and we'd scan the whole + // stack history. + const operationIds = new Set(); + let newestOperationIsRollback = false; + return { + shouldStop(event) { + const id = event.event.OperationId ?? ''; + if (!operationIds.has(id)) { + // An operation boundary (scanning newest-first, so this is an OLDER operation). + // Cross it only once, and only when the newest operation was a rollback. + if (operationIds.size >= 2 || (operationIds.size === 1 && !newestOperationIsRollback)) { + return 'stop-exclude'; + } + operationIds.add(id); + } + if (operationIds.size === 1 && (event.event.ResourceStatus ?? '').includes('ROLLBACK')) { + newestOperationIsRollback = true; + } + return 'continue'; + }, + }; + } + /** * A poll range decider that always returns 'continue', consuming the entire CFN event stream. */ diff --git a/packages/@aws-cdk/toolkit-lib/package.json b/packages/@aws-cdk/toolkit-lib/package.json index b303e51a3..5e0f0119b 100644 --- a/packages/@aws-cdk/toolkit-lib/package.json +++ b/packages/@aws-cdk/toolkit-lib/package.json @@ -93,6 +93,7 @@ "@aws-sdk/client-bedrock-agentcore-control": "^3", "@aws-sdk/client-cloudcontrol": "^3", "@aws-sdk/client-cloudformation": "^3", + "@aws-sdk/client-cloudtrail": "^3", "@aws-sdk/client-cloudwatch-logs": "^3", "@aws-sdk/client-codebuild": "^3", "@aws-sdk/client-ec2": "^3", diff --git a/packages/@aws-cdk/toolkit-lib/test/_helpers/mock-sdk.ts b/packages/@aws-cdk/toolkit-lib/test/_helpers/mock-sdk.ts index 8b85cc42c..ba23b146d 100644 --- a/packages/@aws-cdk/toolkit-lib/test/_helpers/mock-sdk.ts +++ b/packages/@aws-cdk/toolkit-lib/test/_helpers/mock-sdk.ts @@ -7,6 +7,7 @@ import { BedrockAgentCoreControlClient } from '@aws-sdk/client-bedrock-agentcore import { CloudControlClient } from '@aws-sdk/client-cloudcontrol'; import type { Stack } from '@aws-sdk/client-cloudformation'; import { CloudFormationClient, StackStatus } from '@aws-sdk/client-cloudformation'; +import { CloudTrailClient } from '@aws-sdk/client-cloudtrail'; import { CloudWatchLogsClient } from '@aws-sdk/client-cloudwatch-logs'; import { CodeBuildClient } from '@aws-sdk/client-codebuild'; import { EC2Client } from '@aws-sdk/client-ec2'; @@ -42,6 +43,7 @@ export const awsMock = { bedrockAgentCoreControl: mockClient(BedrockAgentCoreControlClient), cloudControl: mockClient(CloudControlClient), cloudFormation: mockClient(CloudFormationClient), + cloudTrail: mockClient(CloudTrailClient), cloudWatch: mockClient(CloudWatchLogsClient), codeBuild: mockClient(CodeBuildClient), ec2: mockClient(EC2Client), @@ -64,6 +66,7 @@ export const mockAppSyncClient = awsMock.appSync; export const mockBedrockAgentCoreControlClient = awsMock.bedrockAgentCoreControl; export const mockCloudControlClient = awsMock.cloudControl; export const mockCloudFormationClient = awsMock.cloudFormation; +export const mockCloudTrailClient = awsMock.cloudTrail; export const mockCloudWatchClient = awsMock.cloudWatch; export const mockCodeBuildClient = awsMock.codeBuild; export const mockEC2Client = awsMock.ec2; diff --git a/packages/@aws-cdk/toolkit-lib/test/api/diagnosing/cloudtrail-investigation.test.ts b/packages/@aws-cdk/toolkit-lib/test/api/diagnosing/cloudtrail-investigation.test.ts new file mode 100644 index 000000000..4b96ed2a6 --- /dev/null +++ b/packages/@aws-cdk/toolkit-lib/test/api/diagnosing/cloudtrail-investigation.test.ts @@ -0,0 +1,537 @@ +import type { StackResource } from '@aws-sdk/client-cloudformation'; +import { DescribeStackResourcesCommand, GetTemplateCommand, ResourceStatus } from '@aws-sdk/client-cloudformation'; +import { LookupEventsCommand } from '@aws-sdk/client-cloudtrail'; +import { investigateStackViaCloudTrail } from '../../../lib/api/diagnosing/cloudtrail-investigation'; +import type { ResourceError } from '../../../lib/api/stack-events/resource-errors'; +import { + mockCloudFormationClient, + mockCloudTrailClient, + MockSdk, + restoreSdkMocksToDefault, +} from '../../_helpers/mock-sdk'; +import { TestIoHost } from '../../_helpers/test-io-host'; + +// Fixtures modeled on real events captured while validating the design against two failed +// stacks: (A) a custom resource whose handler was denied s3:CreateBucket, and (B) a +// VPC-attached Lambda whose execution role could not call ec2:CreateNetworkInterface (the +// denied call is made by the Lambda *service* under an unpredictable session name). + +const ACCOUNT = '123456789012'; +const STACK_ARN = `arn:aws:cloudformation:us-east-1:${ACCOUNT}:stack/MyStack/abc`; +const FAILURE_TIME = new Date('2026-06-19T12:00:00.000Z'); + +const CR_FUNCTION_NAME = 'MyStack-CrHandler8F2F7DBF-mMR596IDq0hl'; +const CR_ROLE_NAME = 'MyStack-CrHandlerServiceRole2D0F6-hA5P1ui2vtp5'; +const VPC_ROLE_NAME = 'MyStack-VpcFnRole-MD1RNDeRclyR'; + +let sdk: MockSdk; +let ioHost: TestIoHost; + +beforeEach(() => { + restoreSdkMocksToDefault(); + jest.useFakeTimers().setSystemTime(FAILURE_TIME.valueOf() + 60 * 60 * 1000); // an hour after the failure + sdk = new MockSdk(); + ioHost = new TestIoHost('debug'); + + mockCloudFormationClient.on(DescribeStackResourcesCommand).resolves({ + StackResources: [ + resource('CrHandler', 'AWS::Lambda::Function', CR_FUNCTION_NAME), + resource('CrHandlerRole', 'AWS::IAM::Role', CR_ROLE_NAME), + resource('VpcFn', 'AWS::Lambda::Function', 'MyStack-VpcFn-iB66SjyhkS37'), + resource('VpcFnRole', 'AWS::IAM::Role', VPC_ROLE_NAME), + resource('MyCustomResource', 'Custom::MyThing', '2026/06/19/[$LATEST]abc123'), + ], + }); + mockCloudFormationClient.on(GetTemplateCommand).resolves({ + TemplateBody: JSON.stringify({ + Resources: { + CrHandler: { Type: 'AWS::Lambda::Function', Properties: { Role: { 'Fn::GetAtt': ['CrHandlerRole', 'Arn'] } } }, + VpcFn: { Type: 'AWS::Lambda::Function', Properties: { Role: { 'Fn::GetAtt': ['VpcFnRole', 'Arn'] } } }, + MyCustomResource: { + Type: 'Custom::MyThing', + Properties: { ServiceToken: { 'Fn::GetAtt': ['CrHandler', 'Arn'] } }, + }, + }, + }), + }); +}); + +afterEach(() => { + jest.useRealTimers(); +}); + +function resource(logicalId: string, type: string, physicalId: string): StackResource { + return { + LogicalResourceId: logicalId, + ResourceType: type, + PhysicalResourceId: physicalId, + Timestamp: FAILURE_TIME, + ResourceStatus: ResourceStatus.CREATE_FAILED, + }; +} + +function resourceError(overrides: Partial = {}): ResourceError { + return { + stackArn: STACK_ARN, + parentStackLogicalIds: [], + logicalId: 'MyCustomResource', + resourceType: 'Custom::MyThing', + message: 'Received response status [FAILED] from custom resource', + timestamp: FAILURE_TIME, + ...overrides, + }; +} + +/** A CloudTrail event as made by the custom resource's handler (Lambda role session). */ +function handlerEvent(fields: Record = {}) { + return { + CloudTrailEvent: JSON.stringify({ + eventTime: '2026-06-19T11:59:00Z', + eventSource: 's3.amazonaws.com', + eventName: 'CreateBucket', + errorCode: 'AccessDenied', + errorMessage: `User: arn:aws:sts::${ACCOUNT}:assumed-role/${CR_ROLE_NAME}/${CR_FUNCTION_NAME} is not authorized to perform: s3:CreateBucket`, + userIdentity: { + arn: `arn:aws:sts::${ACCOUNT}:assumed-role/${CR_ROLE_NAME}/${CR_FUNCTION_NAME}`, + sessionContext: { sessionIssuer: { arn: `arn:aws:iam::${ACCOUNT}:role/${CR_ROLE_NAME}` } }, + inScopeOf: { credentialsIssuedTo: `arn:aws:lambda:us-east-1:${ACCOUNT}:function:${CR_FUNCTION_NAME}` }, + }, + ...fields, + }), + }; +} + +/** The service-on-your-behalf shape: Lambda service assuming the role with a generated session name. */ +function serviceOnBehalfEvent(fields: Record = {}) { + return { + CloudTrailEvent: JSON.stringify({ + eventTime: '2026-06-19T11:58:00Z', + eventSource: 'ec2.amazonaws.com', + eventName: 'CreateNetworkInterface', + errorCode: 'Client.UnauthorizedOperation', + errorMessage: + `User: arn:aws:sts::${ACCOUNT}:assumed-role/${VPC_ROLE_NAME}/awslambda_320_20260619115800000 ` + + 'is not authorized to perform: ec2:CreateNetworkInterface', + userIdentity: { + arn: `arn:aws:sts::${ACCOUNT}:assumed-role/${VPC_ROLE_NAME}/awslambda_320_20260619115800000`, + sessionContext: { sessionIssuer: { arn: `arn:aws:iam::${ACCOUNT}:role/${VPC_ROLE_NAME}` } }, + }, + ...fields, + }), + }; +} + +/** An errored event from an unrelated principal in the same account (concurrent deployment noise). */ +function unrelatedEvent() { + return { + CloudTrailEvent: JSON.stringify({ + eventTime: '2026-06-19T11:59:30Z', + eventSource: 'lambda.amazonaws.com', + eventName: 'GetFunction20150331v2', + errorCode: 'ResourceNotFoundException', + errorMessage: 'Function not found: arn:aws:lambda:us-east-1:123456789012:function:other-stack-OtherFn-zzz', + userIdentity: { + arn: `arn:aws:sts::${ACCOUNT}:assumed-role/other-stack-OtherRole-yyy/session`, + sessionContext: { sessionIssuer: { arn: `arn:aws:iam::${ACCOUNT}:role/other-stack-OtherRole-yyy` } }, + }, + }), + }; +} + +describe('correlation', () => { + test('attributes a handler AccessDenied to the custom resource via the role-session ARN', async () => { + mockCloudTrailClient.on(LookupEventsCommand).resolves({ Events: [handlerEvent(), unrelatedEvent()] }); + + const result = await investigateStackViaCloudTrail(sdk, [resourceError()], ioHost.asHelper('diagnose')); + + expect(result!.errors).toHaveLength(1); + expect(result!.errors[0].message).toMatch(/AccessDenied on s3\.amazonaws\.com:CreateBucket/); + expect(result!.errors[0].message).toMatch(/not authorized to perform: s3:CreateBucket/); + }); + + test('attributes a service-on-your-behalf denial via the role name in the session ARN', async () => { + // The ENI case: session name is service-generated, so only the bare role name embedded in + // the session ARN can tie the event to the stack. + mockCloudTrailClient.on(LookupEventsCommand).resolves({ Events: [serviceOnBehalfEvent()] }); + + const result = await investigateStackViaCloudTrail( + sdk, + [resourceError({ logicalId: 'VpcFn', resourceType: 'AWS::Lambda::Function' })], + ioHost.asHelper('diagnose'), + ); + + expect(result!.errors).toHaveLength(1); + expect(result!.errors[0].logicalId).toEqual('VpcFnRole'); + expect(result!.errors[0].message).toMatch(/Client\.UnauthorizedOperation on ec2\.amazonaws\.com:CreateNetworkInterface/); + }); + + test('excludes errored events from unrelated principals in the same account', async () => { + mockCloudTrailClient.on(LookupEventsCommand).resolves({ Events: [unrelatedEvent(), unrelatedEvent()] }); + + const result = await investigateStackViaCloudTrail(sdk, [resourceError()], ioHost.asHelper('diagnose')); + + expect(result!.errors).toHaveLength(0); + }); + + test('excludes errored calls made by the deploying principal (already in StatusReason)', async () => { + // CloudFormation's own denied calls carry no stack identity in userIdentity (and often an + // empty resources list); their message is already surfaced via the resource StatusReason. + mockCloudTrailClient.on(LookupEventsCommand).resolves({ + Events: [{ + CloudTrailEvent: JSON.stringify({ + eventTime: '2026-06-19T11:59:00Z', + eventSource: 'sns.amazonaws.com', + eventName: 'GetTopicAttributes', + errorCode: 'AccessDenied', + userIdentity: { + arn: `arn:aws:sts::${ACCOUNT}:assumed-role/deploy-role/AWSCloudFormation`, + sessionContext: { sessionIssuer: { arn: `arn:aws:iam::${ACCOUNT}:role/deploy-role` } }, + invokedBy: 'cloudformation.amazonaws.com', + }, + }), + }], + }); + + const result = await investigateStackViaCloudTrail(sdk, [resourceError()], ioHost.asHelper('diagnose')); + + expect(result!.errors).toHaveLength(0); + }); + + test('does not correlate via resources[].ARN — only the calling principal counts', async () => { + // An unrelated principal's denied call AGAINST a stack resource is not the stack's + // failure. Matching on touched resources would attribute foreign activity to the stack. + mockCloudTrailClient.on(LookupEventsCommand).resolves({ + Events: [{ + CloudTrailEvent: JSON.stringify({ + eventTime: '2026-06-19T11:59:00Z', + eventSource: 's3.amazonaws.com', + eventName: 'PutBucketPolicy', + errorCode: 'AccessDenied', + userIdentity: { + arn: `arn:aws:sts::${ACCOUNT}:assumed-role/some-unrelated-ci-role/ci-session`, + sessionContext: { sessionIssuer: { arn: `arn:aws:iam::${ACCOUNT}:role/some-unrelated-ci-role` } }, + }, + resources: [{ ARN: `arn:aws:lambda:us-east-1:${ACCOUNT}:function:${CR_FUNCTION_NAME}` }], + }), + }], + }); + + const result = await investigateStackViaCloudTrail(sdk, [resourceError()], ioHost.asHelper('diagnose')); + + expect(result!.errors).toHaveLength(0); + }); + + test('attributes to the session-name identity (the caller) when multiple identities match', async () => { + // A handler's session ARN contains both the role name and the function name (as the + // session name). The role is listed BEFORE the function in DescribeStackResources + // (see beforeEach fixture ordering is not guaranteed either way), so first-match would + // be order-dependent; the session-name segment identifies the actual caller and must + // win regardless of insertion order. + mockCloudFormationClient.on(DescribeStackResourcesCommand).resolves({ + StackResources: [ + resource('CrHandlerRole', 'AWS::IAM::Role', CR_ROLE_NAME), + resource('CrHandler', 'AWS::Lambda::Function', CR_FUNCTION_NAME), + ], + }); + mockCloudTrailClient.on(LookupEventsCommand).resolves({ Events: [handlerEvent()] }); + + const result = await investigateStackViaCloudTrail(sdk, [resourceError()], ioHost.asHelper('diagnose')); + + expect(result!.errors).toHaveLength(1); + expect(result!.errors[0].logicalId).toEqual('CrHandler'); + }); + + test('does not match an identity that is a prefix of another principal name', async () => { + // 'MyStack-VpcFn-iB66SjyhkS37' must not claim events from a hypothetical + // 'MyStack-VpcFn-iB66SjyhkS37x' principal: matching is by whole ARN segment. + mockCloudTrailClient.on(LookupEventsCommand).resolves({ + Events: [{ + CloudTrailEvent: JSON.stringify({ + eventTime: '2026-06-19T11:59:00Z', + eventSource: 's3.amazonaws.com', + eventName: 'CreateBucket', + errorCode: 'AccessDenied', + userIdentity: { + arn: `arn:aws:sts::${ACCOUNT}:assumed-role/other-role/MyStack-VpcFn-iB66SjyhkS37x`, + sessionContext: { sessionIssuer: { arn: `arn:aws:iam::${ACCOUNT}:role/other-role` } }, + }, + }), + }], + }); + + const result = await investigateStackViaCloudTrail(sdk, [resourceError()], ioHost.asHelper('diagnose')); + + expect(result!.errors).toHaveLength(0); + }); + + test('correlates roles referenced by nested template properties', async () => { + // Role references are often nested (Firehose destination configs, EventBridge targets); + // the identity scan must find them at any depth. The role here is NOT a stack resource + // (imported by ARN), so only the template scan can index it. + mockCloudFormationClient.on(GetTemplateCommand).resolves({ + TemplateBody: JSON.stringify({ + Resources: { + DeliveryStream: { + Type: 'AWS::KinesisFirehose::DeliveryStream', + Properties: { + S3DestinationConfiguration: { + BucketARN: 'arn:aws:s3:::some-bucket', + RoleARN: `arn:aws:iam::${ACCOUNT}:role/imported-firehose-role`, + }, + }, + }, + }, + }), + }); + mockCloudTrailClient.on(LookupEventsCommand).resolves({ + Events: [{ + CloudTrailEvent: JSON.stringify({ + eventTime: '2026-06-19T11:59:00Z', + eventSource: 's3.amazonaws.com', + eventName: 'PutBucketPolicy', + errorCode: 'AccessDenied', + userIdentity: { + arn: `arn:aws:sts::${ACCOUNT}:assumed-role/imported-firehose-role/firehose-session`, + sessionContext: { sessionIssuer: { arn: `arn:aws:iam::${ACCOUNT}:role/imported-firehose-role` } }, + }, + }), + }], + }); + + const result = await investigateStackViaCloudTrail(sdk, [resourceError()], ioHost.asHelper('diagnose')); + + expect(result!.errors).toHaveLength(1); + expect(result!.errors[0].logicalId).toEqual('DeliveryStream'); + }); + + test('correlates a role expressed as Fn::Sub with a literal name segment', async () => { + mockCloudFormationClient.on(GetTemplateCommand).resolves({ + TemplateBody: JSON.stringify({ + Resources: { + Project: { + Type: 'AWS::CodeBuild::Project', + Properties: { ServiceRole: { 'Fn::Sub': 'arn:aws:iam::${AWS::AccountId}:role/my-codebuild-role' } }, + }, + }, + }), + }); + mockCloudTrailClient.on(LookupEventsCommand).resolves({ + Events: [{ + CloudTrailEvent: JSON.stringify({ + eventTime: '2026-06-19T11:59:00Z', + eventSource: 'ec2.amazonaws.com', + eventName: 'CreateNetworkInterface', + errorCode: 'Client.UnauthorizedOperation', + userIdentity: { + arn: `arn:aws:sts::${ACCOUNT}:assumed-role/my-codebuild-role/codebuild-session`, + sessionContext: { sessionIssuer: { arn: `arn:aws:iam::${ACCOUNT}:role/my-codebuild-role` } }, + }, + }), + }], + }); + + const result = await investigateStackViaCloudTrail(sdk, [resourceError()], ioHost.asHelper('diagnose')); + + expect(result!.errors).toHaveLength(1); + expect(result!.errors[0].logicalId).toEqual('Project'); + }); + + test('still collects template-derived identities when DescribeStackResources fails', async () => { + // A denied/throttled resource listing must not abort the investigation: a literal + // ServiceToken ARN in the template is resolvable without physical IDs. + const denied = new Error('not authorized'); + denied.name = 'AccessDeniedException'; + mockCloudFormationClient.on(DescribeStackResourcesCommand).rejects(denied); + mockCloudFormationClient.on(GetTemplateCommand).resolves({ + TemplateBody: JSON.stringify({ + Resources: { + MyCustomResource: { + Type: 'Custom::MyThing', + Properties: { ServiceToken: `arn:aws:lambda:us-east-1:${ACCOUNT}:function:${CR_FUNCTION_NAME}` }, + }, + }, + }), + }); + mockCloudTrailClient.on(LookupEventsCommand).resolves({ Events: [handlerEvent()] }); + + const result = await investigateStackViaCloudTrail(sdk, [resourceError()], ioHost.asHelper('diagnose')); + + expect(result!.errors).toHaveLength(1); + expect(result!.errors[0].logicalId).toEqual('MyCustomResource'); + }); +}); + +describe('presentation', () => { + test('collapses repeated identical errors and leads with the earliest distinct event', async () => { + // Regression from validation: rollback/retry polling produced 67 identical + // ResourceNotFoundException events *after* the single root-cause denial. Chronological + // "last 5" would show only the noise; dedup + earliest-first must surface the root cause. + const noise = Array.from({ length: 67 }, (_, i) => handlerEvent({ + eventTime: `2026-06-19T11:59:${String(10 + Math.floor(i / 10))}Z`, + eventSource: 'lambda.amazonaws.com', + eventName: 'GetFunctionConfiguration20150331v2', + errorCode: 'ResourceNotFoundException', + errorMessage: 'Function not found', + })); + mockCloudTrailClient.on(LookupEventsCommand).resolves({ + Events: [...noise, serviceOnBehalfEvent({ eventTime: '2026-06-19T11:58:00Z' })], + }); + + const result = await investigateStackViaCloudTrail( + sdk, + [resourceError({ logicalId: 'VpcFn' })], + ioHost.asHelper('diagnose'), + ); + + expect(result!.errors).toHaveLength(2); + expect(result!.errors[0].message).toMatch(/CreateNetworkInterface/); + expect(result!.errors[1].message).toMatch(/ResourceNotFoundException/); + expect(result!.errors[1].message).toMatch(/67 occurrences/); + }); + + test('caps distinct events at 5 and discloses the cut as a note', async () => { + const events = Array.from({ length: 7 }, (_, i) => handlerEvent({ + eventName: `DistinctCall${i}`, + eventTime: `2026-06-19T11:59:0${i}Z`, + })); + mockCloudTrailClient.on(LookupEventsCommand).resolves({ Events: events }); + + const result = await investigateStackViaCloudTrail(sdk, [resourceError()], ioHost.asHelper('diagnose')); + + expect(result!.errors).toHaveLength(5); + expect(result!.errors[0].message).toMatch(/DistinctCall0/); + // The overflow disclosure is scan metadata, not an error attributed to a resource. + expect(result!.notes.join('\n')).toMatch(/2 more distinct CloudTrail error event\(s\) not shown/); + }); + + test('keeps the earlier event\'s own attribution when dedup replaces a later duplicate', async () => { + // Two events with the same (eventSource, eventName, errorCode) from DIFFERENT resources: + // LookupEvents returns newest-first, so the later (12:01) event is seen first. The dedup + // must keep the earlier (11:58) event AND its resource, not mix event A with identity B. + mockCloudTrailClient.on(LookupEventsCommand).resolves({ + Events: [ + handlerEvent({ + eventTime: '2026-06-19T12:01:00Z', + eventSource: 'iam.amazonaws.com', + eventName: 'PassRole', + errorCode: 'AccessDenied', + errorMessage: 'handler denied', + }), + serviceOnBehalfEvent({ + eventTime: '2026-06-19T11:58:00Z', + eventSource: 'iam.amazonaws.com', + eventName: 'PassRole', + errorCode: 'AccessDenied', + errorMessage: 'vpc role denied', + }), + ], + }); + + const result = await investigateStackViaCloudTrail(sdk, [resourceError()], ioHost.asHelper('diagnose')); + + expect(result!.errors).toHaveLength(1); + // The earlier event came from the VPC role's principal, so the attribution follows it. + expect(result!.errors[0].logicalId).toEqual('VpcFnRole'); + expect(result!.errors[0].message).toMatch(/vpc role denied/); + expect(result!.errors[0].message).toMatch(/2 occurrences/); + }); +}); + +describe('sweep mechanics', () => { + test('sweeps without a LookupAttribute, bounded to the failure window', async () => { + mockCloudTrailClient.on(LookupEventsCommand).resolves({ Events: [] }); + + await investigateStackViaCloudTrail(sdk, [resourceError()], ioHost.asHelper('diagnose')); + + expect(mockCloudTrailClient).toHaveReceivedCommandWith(LookupEventsCommand, { + StartTime: new Date(FAILURE_TIME.valueOf() - 5 * 60 * 1000), + EndTime: new Date(FAILURE_TIME.valueOf() + 2 * 60 * 1000), + MaxResults: 50, + }); + const input = mockCloudTrailClient.commandCalls(LookupEventsCommand)[0].args[0].input as any; + expect(input.LookupAttributes).toBeUndefined(); + }); + + test('pages through results and discloses truncation with the number of events checked', async () => { + // Two events per page (one errored, one clean): the disclosure must count events + // CHECKED (20), not errored events kept (10). + mockCloudTrailClient.on(LookupEventsCommand).resolves({ + Events: [ + handlerEvent(), + { CloudTrailEvent: JSON.stringify({ eventTime: '2026-06-19T11:59:00Z', eventSource: 's3.amazonaws.com', eventName: 'CreateBucket' }) }, + ], + NextToken: 'more', + }); + + const result = await investigateStackViaCloudTrail(sdk, [resourceError()], ioHost.asHelper('diagnose')); + + expect(mockCloudTrailClient).toHaveReceivedCommandTimes(LookupEventsCommand, 10); + expect(result!.notes.join('\n')).toMatch(/only the most recent 20 CloudTrail events in the window were checked/); + }); + + test('skips the sweep when no failure has a timestamp to bound the window', async () => { + const result = await investigateStackViaCloudTrail( + sdk, + [resourceError({ timestamp: undefined })], + ioHost.asHelper('diagnose'), + ); + + expect(mockCloudTrailClient).not.toHaveReceivedCommand(LookupEventsCommand); + expect(result).toBeUndefined(); + }); + + test('hints at delivery latency when the failure is recent', async () => { + jest.setSystemTime(FAILURE_TIME.valueOf() + 3 * 60 * 1000); // 3 minutes after failure + mockCloudTrailClient.on(LookupEventsCommand).resolves({ Events: [] }); + + const result = await investigateStackViaCloudTrail(sdk, [resourceError()], ioHost.asHelper('diagnose')); + + expect(result!.errors).toHaveLength(0); + expect(result!.notes.join('\n')).toMatch(/can lag up to 15 minutes/); + expect(result!.notes.join('\n')).toMatch(/cdk diagnose/); + }); + + test('hints at delivery latency even when some events already correlated', async () => { + // An early benign error being delivered does not mean the fatal one has been: the hint + // must not be suppressed by partial delivery. + jest.setSystemTime(FAILURE_TIME.valueOf() + 3 * 60 * 1000); + mockCloudTrailClient.on(LookupEventsCommand).resolves({ Events: [handlerEvent()] }); + + const result = await investigateStackViaCloudTrail(sdk, [resourceError()], ioHost.asHelper('diagnose')); + + expect(result!.errors).toHaveLength(1); + expect(result!.notes.join('\n')).toMatch(/can lag up to 15 minutes/); + }); + + test('stays quiet when nothing correlates and the failure is old', async () => { + mockCloudTrailClient.on(LookupEventsCommand).resolves({ Events: [] }); + + const result = await investigateStackViaCloudTrail(sdk, [resourceError()], ioHost.asHelper('diagnose')); + + expect(result!.errors).toHaveLength(0); + expect(result!.notes).toHaveLength(0); + }); +}); + +describe('lookup failures are tooling warnings, not diagnosis', () => { + test('a denied LookupEvents emits an actionable warning and returns no investigation', async () => { + const denied = new Error('User is not authorized to perform: cloudtrail:LookupEvents'); + denied.name = 'AccessDeniedException'; + mockCloudTrailClient.on(LookupEventsCommand).rejects(denied); + + const result = await investigateStackViaCloudTrail(sdk, [resourceError()], ioHost.asHelper('diagnose')); + + expect(result).toBeUndefined(); + ioHost.expectMessage({ level: 'warn', containing: 'cloudtrail:LookupEvents' }); + }); + + test('other lookup failures degrade silently to debug', async () => { + mockCloudTrailClient.on(LookupEventsCommand).rejects(new Error('ThrottlingException: rate exceeded')); + + const result = await investigateStackViaCloudTrail(sdk, [resourceError()], ioHost.asHelper('diagnose')); + + expect(result).toBeUndefined(); + expect(ioHost.messages.filter((m) => m.level === 'warn')).toHaveLength(0); + ioHost.expectMessage({ level: 'debug', containing: 'lookup failed' }); + }); +}); diff --git a/packages/@aws-cdk/toolkit-lib/test/api/diagnosing/resource-investigation.test.ts b/packages/@aws-cdk/toolkit-lib/test/api/diagnosing/resource-investigation.test.ts index 512ef7ed5..13df98c1c 100644 --- a/packages/@aws-cdk/toolkit-lib/test/api/diagnosing/resource-investigation.test.ts +++ b/packages/@aws-cdk/toolkit-lib/test/api/diagnosing/resource-investigation.test.ts @@ -9,8 +9,11 @@ import { import { GetFunctionConfigurationCommand } from '@aws-sdk/client-lambda'; import { parseLambdaLogEvents } from '../../../lib/api/diagnosing/format-utils'; import { + assumedRoleSessionName, extractLogStreamName, functionNameFromArnOrName, + identityMatches, + nameSegment, parseEcsServiceIdentifier, serviceTokenReferencedLogicalId, } from '../../../lib/api/diagnosing/resource-identifiers'; @@ -454,6 +457,87 @@ describe('extractLogStreamName', () => { }); }); +describe('identityMatches', () => { + const SESSION_ARN = 'arn:aws:sts::123456789012:assumed-role/MyStack-CrHandlerRole-abc/MyStack-CrHandler-def'; + + test('matches a single-segment key as a whole segment', () => { + expect(identityMatches(SESSION_ARN, 'MyStack-CrHandler-def')).toBe(true); + expect(identityMatches(SESSION_ARN, 'MyStack-CrHandlerRole-abc')).toBe(true); + }); + + test('does not match a key that is a prefix of a segment', () => { + // The core guarantee: 'my-svc-1' must not claim 'my-svc-10'. + expect(identityMatches('arn:aws:sts::123456789012:assumed-role/role/my-svc-10', 'my-svc-1')).toBe(false); + expect(identityMatches(SESSION_ARN, 'MyStack-CrHandler')).toBe(false); + }); + + test('does not match a key that is a suffix of a segment', () => { + expect(identityMatches('arn:aws:sts::123456789012:assumed-role/role/prod-my-svc', 'my-svc')).toBe(false); + }); + + test('matches whole strings exactly', () => { + expect(identityMatches('my-function-name', 'my-function-name')).toBe(true); + }); + + test('matches a segment at the start and end of the haystack', () => { + expect(identityMatches('my-svc/suffix', 'my-svc')).toBe(true); + expect(identityMatches('prefix/my-svc', 'my-svc')).toBe(true); + }); + + test('matches a multi-segment key when delimiter-bounded', () => { + const roleArn = 'arn:aws:iam::123456789012:role/service-role/my-role'; + expect(identityMatches(roleArn, 'role/service-role/my-role')).toBe(true); + expect(identityMatches(roleArn, 'service-role/my-rol')).toBe(false); + }); + + test('matches a multi-segment key at a later occurrence when the first is unbounded', () => { + // First occurrence of 'b/c' inside 'ab/c' is not segment-bounded; the second is. + expect(identityMatches('x/ab/c/y/b/c', 'b/c')).toBe(true); + }); + + test('does not match an empty or absent key occurrence', () => { + expect(identityMatches('a/b/c', 'd')).toBe(false); + }); +}); + +describe('nameSegment', () => { + test('extracts the trailing segment of a role ARN', () => { + expect(nameSegment('arn:aws:iam::123456789012:role/my-role')).toEqual('my-role'); + }); + + test('extracts the trailing segment of a path-shaped ID', () => { + expect(nameSegment('cluster-name/service-name')).toEqual('service-name'); + }); + + test('returns undefined for a value without a path', () => { + expect(nameSegment('my-function-name')).toBeUndefined(); + }); + + test('returns undefined for a trailing slash', () => { + expect(nameSegment('some/path/')).toBeUndefined(); + }); +}); + +describe('assumedRoleSessionName', () => { + test('extracts the session name from an assumed-role ARN', () => { + expect(assumedRoleSessionName('arn:aws:sts::123456789012:assumed-role/my-role/my-session')) + .toEqual('my-session'); + }); + + test('keeps session names that contain slashes intact', () => { + expect(assumedRoleSessionName('arn:aws:sts::123456789012:assumed-role/my-role/a/b')) + .toEqual('a/b'); + }); + + test('returns undefined for a plain role ARN', () => { + expect(assumedRoleSessionName('arn:aws:iam::123456789012:role/my-role')).toBeUndefined(); + }); + + test('returns undefined for a non-ARN string', () => { + expect(assumedRoleSessionName('my-function-name')).toBeUndefined(); + }); +}); + describe('parseLambdaLogEvents', () => { const msgs = (events: Array<{ message?: string }>) => parseLambdaLogEvents(events); diff --git a/packages/@aws-cdk/toolkit-lib/test/api/diagnosing/stack-diagnoser.test.ts b/packages/@aws-cdk/toolkit-lib/test/api/diagnosing/stack-diagnoser.test.ts index 2eb11f11e..a9d8e596b 100644 --- a/packages/@aws-cdk/toolkit-lib/test/api/diagnosing/stack-diagnoser.test.ts +++ b/packages/@aws-cdk/toolkit-lib/test/api/diagnosing/stack-diagnoser.test.ts @@ -1,13 +1,16 @@ import { ChangeSetStatus, + DescribeStackResourcesCommand, + ResourceStatus, } from '@aws-sdk/client-cloudformation'; +import { LookupEventsCommand } from '@aws-sdk/client-cloudtrail'; import type { StackDiagnosis } from '../../../lib/actions/diagnose'; import { CloudFormationStackDiagnoser } from '../../../lib/api/diagnosing/stack-diagnoser'; import type { ISourceTracer } from '../../../lib/api/source-tracing/private/source-tracing'; import type { SourceTrace } from '../../../lib/api/source-tracing/types'; import { ResourceErrors } from '../../../lib/api/stack-events/resource-errors'; import { FakeCloudFormation } from '../../_helpers/fake-aws/fake-cloudformation'; -import { mockCloudFormationClient, MockSdk, restoreSdkMocksToDefault } from '../../_helpers/mock-sdk'; +import { mockCloudFormationClient, mockCloudTrailClient, MockSdk, restoreSdkMocksToDefault } from '../../_helpers/mock-sdk'; import { TestIoHost } from '../../_helpers/test-io-host'; let sdk: MockSdk; @@ -118,6 +121,66 @@ describe('CloudFormationStackDiagnoser', () => { }); }); + test('finds the failure on a rolled-back stack, across the rollback and create operations', async () => { + // A rolled-back deployment spans two CloudFormation operations: the failed create + // (operation A) and the rollback that follows (operation B). describeStackEvents returns + // newest-first, so the rollback's (successful) DELETE events come first and the actual + // CREATE_FAILED belongs to the older operation A. The poll range must include both. + fakeCfn.createStackSync({ StackName: 'MyStack', StackStatus: 'ROLLBACK_COMPLETE' }); + const stack = fakeCfn.accessStack('MyStack'); + const opCreate = 'op-create-aaaa'; + const opRollback = 'op-rollback-bbbb'; + + // Pushed oldest-first; unshift makes the final array newest-first. + // Operation A (create): the real failure. + stack.events.unshift({ + StackId: stack.id, + StackName: 'MyStack', + EventId: 'evt-create-failed', + LogicalResourceId: 'MyResource', + PhysicalResourceId: 'phys-1', + ResourceType: 'Custom::MyThing', + ResourceStatus: 'CREATE_FAILED', + ResourceStatusReason: 'Received response status [FAILED] from custom resource', + OperationId: opCreate, + Timestamp: new Date('2026-06-25T18:32:12Z'), + }); + // Operation B (rollback): only successful deletes — no failure signal here. + stack.events.unshift({ + StackId: stack.id, + StackName: 'MyStack', + EventId: 'evt-delete', + LogicalResourceId: 'MyResource', + PhysicalResourceId: 'phys-1', + ResourceType: 'Custom::MyThing', + ResourceStatus: 'DELETE_COMPLETE', + OperationId: opRollback, + Timestamp: new Date('2026-06-25T18:32:16Z'), + }); + stack.events.unshift({ + StackId: stack.id, + StackName: 'MyStack', + EventId: 'evt-rollback-complete', + LogicalResourceId: 'MyStack', + PhysicalResourceId: stack.id, + ResourceType: 'AWS::CloudFormation::Stack', + ResourceStatus: 'ROLLBACK_COMPLETE', + OperationId: opRollback, + Timestamp: new Date('2026-06-25T18:32:31Z'), + }); + + const result = await makeDiagnoser().diagnoseFromFresh('MyStack'); + + expect(result).toMatchObject({ + type: 'problem', + detectedBy: { type: 'deployment' }, + problems: [expect.objectContaining({ + logicalId: 'MyResource', + message: 'Received response status [FAILED] from custom resource', + })], + }); + }); + test('diagnoses change set creation failure', async () => { fakeCfn.createStackSync({ StackName: 'MyStack', StackStatus: 'CREATE_COMPLETE' }); fakeCfn.createChangeSetSync({ @@ -470,4 +533,88 @@ describe('CloudFormationStackDiagnoser', () => { }); }); }); + + describe('CloudTrail investigation wiring', () => { + const STACK_ARN = 'arn:aws:cloudformation:us-east-1:123456789012:stack/MyStack/abc'; + const FAILURE_TIME = new Date('2026-06-19T12:00:00.000Z'); + + function makeErrors(): ResourceErrors { + const errors = new ResourceErrors(); + errors.update({ + event: { + EventId: 'evt-1', + StackId: STACK_ARN, + StackName: 'MyStack', + LogicalResourceId: 'CrHandler', + PhysicalResourceId: 'MyStack-CrHandler-abc123def456', + ResourceType: 'AWS::Lambda::Function', + ResourceStatus: 'CREATE_FAILED', + ResourceStatusReason: 'Handler error', + Timestamp: FAILURE_TIME, + }, + parentStackLogicalIds: [], + isRootStackEvent: false, + }); + return errors; + } + + function makeExploringDiagnoser() { + return new CloudFormationStackDiagnoser({ + sdk, + sourceTracer: fakeTracer, + ioHelper: ioHost.asHelper('diagnose'), + topLevelStackHierarchicalId: 'MyStack', + additionalExplorationSdkProvider: async () => sdk, + }); + } + + test('attaches correlated CloudTrail errors to the failed resource on any path', async () => { + // No cloudTrailEnabled gating anymore: the investigation runs for deploy-originating + // error collections too (a recent failure just yields fewer delivered events). + fakeCfn.createStackSync({ StackName: 'MyStack', StackStatus: 'UPDATE_FAILED', StackId: STACK_ARN }); + mockCloudFormationClient.on(DescribeStackResourcesCommand).resolves({ + StackResources: [{ + LogicalResourceId: 'CrHandler', + ResourceType: 'AWS::Lambda::Function', + PhysicalResourceId: 'MyStack-CrHandler-abc123def456', + ResourceStatus: ResourceStatus.CREATE_FAILED, + Timestamp: FAILURE_TIME, + }], + }); + mockCloudTrailClient.on(LookupEventsCommand).resolves({ + Events: [{ + CloudTrailEvent: JSON.stringify({ + eventTime: '2026-06-19T11:59:00Z', + eventSource: 's3.amazonaws.com', + eventName: 'CreateBucket', + errorCode: 'AccessDenied', + errorMessage: 'not authorized to perform: s3:CreateBucket', + userIdentity: { arn: 'arn:aws:sts::123456789012:assumed-role/some-role/MyStack-CrHandler-abc123def456' }, + }), + }], + }); + + const result = await makeExploringDiagnoser().diagnoseFromErrorCollection(makeErrors(), { + StackName: 'MyStack', + StackStatus: 'UPDATE_FAILED', + CreationTime: new Date(), + }); + + assertProblem(result); + const context = result.problems[0].additionalContext?.find((c) => c.source === 'CloudTrail Errors'); + expect(context).toBeDefined(); + expect(context!.messages[0]).toMatch(/AccessDenied on s3\.amazonaws\.com:CreateBucket/); + }); + + test('does not run the investigation without an exploration SDK', async () => { + const result = await makeDiagnoser().diagnoseFromErrorCollection(makeErrors(), { + StackName: 'MyStack', + StackStatus: 'UPDATE_FAILED', + CreationTime: new Date(), + }); + + assertProblem(result); + expect(mockCloudTrailClient).not.toHaveReceivedCommand(LookupEventsCommand); + }); + }); }); diff --git a/packages/@aws-cdk/toolkit-lib/test/api/stack-events/stack-event-poller.test.ts b/packages/@aws-cdk/toolkit-lib/test/api/stack-events/stack-event-poller.test.ts index a45500904..b4bc85004 100644 --- a/packages/@aws-cdk/toolkit-lib/test/api/stack-events/stack-event-poller.test.ts +++ b/packages/@aws-cdk/toolkit-lib/test/api/stack-events/stack-event-poller.test.ts @@ -214,3 +214,68 @@ describe('poll', () => { expect(poller.deleteFailures).toEqual([]); }); }); + +describe('PollRange.mostRecentDeploymentAttempt', () => { + // The decider only looks at event.OperationId and ResourceStatus; isRootStackEvent is + // irrelevant here. + const ev = (operationId: string | undefined, status?: string) => ({ + event: { OperationId: operationId, ResourceStatus: status } as StackEvent, + parentStackLogicalIds: [], + isRootStackEvent: false, + }); + + const decisions = ( + range: ReturnType, + ops: Array<[string | undefined, string?]>, + ) => ops.map(([op, status]) => range.shouldStop(ev(op, status))); + + test('spans a rollback and the create it rolled back, then stops at a third operation', () => { + const range = PollRange.mostRecentDeploymentAttempt(); + // Newest-first: rollback op B (3 events), then create op A (2 events), then an older op C. + expect(decisions(range, [ + ['B', 'ROLLBACK_COMPLETE'], ['B', 'DELETE_COMPLETE'], ['B', 'ROLLBACK_IN_PROGRESS'], + ['A', 'CREATE_FAILED'], ['A', 'CREATE_IN_PROGRESS'], + ['C', 'UPDATE_COMPLETE'], + ])).toEqual([ + 'continue', 'continue', 'continue', // op B (the rollback) + 'continue', 'continue', // op A (the create that triggered it) + 'stop-exclude', // op C — third distinct operation + ]); + }); + + test('stops at the first operation boundary when the newest operation is not a rollback', () => { + // Regression: a `--no-rollback` failure is a single operation with no rollback following + // it. Continuing into the next (older) operation would report a previous, unrelated + // deployment attempt's failures as part of this one. + const range = PollRange.mostRecentDeploymentAttempt(); + expect(decisions(range, [ + ['Y', 'UPDATE_FAILED'], ['Y', 'UPDATE_IN_PROGRESS'], + ['X', 'UPDATE_FAILED'], + ])).toEqual([ + 'continue', 'continue', // op Y (the --no-rollback failure) + 'stop-exclude', // op X — a previous deployment attempt, not part of this one + ]); + }); + + test('stops at an operation boundary even when events have no OperationId', () => { + // Regression: undefined OperationId must still count toward the boundary, otherwise the + // poller scans the entire stack history. + const range = PollRange.mostRecentDeploymentAttempt(); + expect(decisions(range, [ + [undefined, 'ROLLBACK_COMPLETE'], [undefined, 'DELETE_COMPLETE'], + ['A', 'CREATE_FAILED'], + ['B', 'UPDATE_COMPLETE'], + ])).toEqual([ + 'continue', 'continue', // the (single) undefined operation, a rollback + 'continue', // op A — the operation the rollback rolled back + 'stop-exclude', // op B — third distinct + ]); + }); + + test('never stops within a single operation', () => { + const range = PollRange.mostRecentDeploymentAttempt(); + expect(decisions(range, [ + ['A', 'ROLLBACK_COMPLETE'], ['A', 'CREATE_FAILED'], ['A', 'CREATE_IN_PROGRESS'], ['A', 'CREATE_IN_PROGRESS'], + ])).toEqual(['continue', 'continue', 'continue', 'continue']); + }); +}); diff --git a/packages/aws-cdk/THIRD_PARTY_LICENSES b/packages/aws-cdk/THIRD_PARTY_LICENSES index be879de73..4d0dbfcee 100644 --- a/packages/aws-cdk/THIRD_PARTY_LICENSES +++ b/packages/aws-cdk/THIRD_PARTY_LICENSES @@ -1028,6 +1028,212 @@ The aws-cdk package includes the following third-party software/licensing: limitations under the License. +---------------- + +** @aws-sdk/client-cloudtrail@3.1075.0 - https://www.npmjs.com/package/@aws-sdk/client-cloudtrail/v/3.1075.0 | Apache-2.0 + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2018-2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + ---------------- ** @aws-sdk/client-cloudwatch-logs@3.1078.0 - https://www.npmjs.com/package/@aws-sdk/client-cloudwatch-logs/v/3.1078.0 | Apache-2.0 @@ -4120,7 +4326,7 @@ The aws-cdk package includes the following third-party software/licensing: ---------------- -** @aws-sdk/core@3.974.26 - https://www.npmjs.com/package/@aws-sdk/core/v/3.974.26 | Apache-2.0 +** @aws-sdk/core@3.974.28 - https://www.npmjs.com/package/@aws-sdk/core/v/3.974.28 | Apache-2.0 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ @@ -4531,7 +4737,7 @@ Apache License ---------------- -** @aws-sdk/credential-provider-env@3.972.52 - https://www.npmjs.com/package/@aws-sdk/credential-provider-env/v/3.972.52 | Apache-2.0 +** @aws-sdk/credential-provider-env@3.972.54 - https://www.npmjs.com/package/@aws-sdk/credential-provider-env/v/3.972.54 | Apache-2.0 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ @@ -4736,11 +4942,11 @@ Apache License ---------------- -** @aws-sdk/credential-provider-http@3.972.54 - https://www.npmjs.com/package/@aws-sdk/credential-provider-http/v/3.972.54 | Apache-2.0 +** @aws-sdk/credential-provider-http@3.972.56 - https://www.npmjs.com/package/@aws-sdk/credential-provider-http/v/3.972.56 | Apache-2.0 ---------------- -** @aws-sdk/credential-provider-ini@3.972.59 - https://www.npmjs.com/package/@aws-sdk/credential-provider-ini/v/3.972.59 | Apache-2.0 +** @aws-sdk/credential-provider-ini@3.972.61 - https://www.npmjs.com/package/@aws-sdk/credential-provider-ini/v/3.972.61 | Apache-2.0 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ @@ -4945,11 +5151,11 @@ Apache License ---------------- -** @aws-sdk/credential-provider-login@3.972.58 - https://www.npmjs.com/package/@aws-sdk/credential-provider-login/v/3.972.58 | Apache-2.0 +** @aws-sdk/credential-provider-login@3.972.60 - https://www.npmjs.com/package/@aws-sdk/credential-provider-login/v/3.972.60 | Apache-2.0 ---------------- -** @aws-sdk/credential-provider-node@3.972.61 - https://www.npmjs.com/package/@aws-sdk/credential-provider-node/v/3.972.61 | Apache-2.0 +** @aws-sdk/credential-provider-node@3.972.63 - https://www.npmjs.com/package/@aws-sdk/credential-provider-node/v/3.972.63 | Apache-2.0 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ @@ -5154,7 +5360,7 @@ Apache License ---------------- -** @aws-sdk/credential-provider-process@3.972.52 - https://www.npmjs.com/package/@aws-sdk/credential-provider-process/v/3.972.52 | Apache-2.0 +** @aws-sdk/credential-provider-process@3.972.54 - https://www.npmjs.com/package/@aws-sdk/credential-provider-process/v/3.972.54 | Apache-2.0 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ @@ -5359,7 +5565,7 @@ Apache License ---------------- -** @aws-sdk/credential-provider-sso@3.972.58 - https://www.npmjs.com/package/@aws-sdk/credential-provider-sso/v/3.972.58 | Apache-2.0 +** @aws-sdk/credential-provider-sso@3.972.60 - https://www.npmjs.com/package/@aws-sdk/credential-provider-sso/v/3.972.60 | Apache-2.0 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ @@ -5564,7 +5770,7 @@ Apache License ---------------- -** @aws-sdk/credential-provider-web-identity@3.972.58 - https://www.npmjs.com/package/@aws-sdk/credential-provider-web-identity/v/3.972.58 | Apache-2.0 +** @aws-sdk/credential-provider-web-identity@3.972.60 - https://www.npmjs.com/package/@aws-sdk/credential-provider-web-identity/v/3.972.60 | Apache-2.0 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ @@ -7001,7 +7207,7 @@ Apache License ---------------- -** @aws-sdk/nested-clients@3.997.26 - https://www.npmjs.com/package/@aws-sdk/nested-clients/v/3.997.26 | Apache-2.0 +** @aws-sdk/nested-clients@3.997.28 - https://www.npmjs.com/package/@aws-sdk/nested-clients/v/3.997.28 | Apache-2.0 ---------------- @@ -7211,7 +7417,7 @@ Apache License ---------------- -** @aws-sdk/token-providers@3.1078.0 - https://www.npmjs.com/package/@aws-sdk/token-providers/v/3.1078.0 | Apache-2.0 +** @aws-sdk/token-providers@3.1080.0 - https://www.npmjs.com/package/@aws-sdk/token-providers/v/3.1080.0 | Apache-2.0 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ @@ -7621,7 +7827,7 @@ Apache License ---------------- -** @aws/lambda-invoke-store@0.2.4 - https://www.npmjs.com/package/@aws/lambda-invoke-store/v/0.2.4 | Apache-2.0 +** @aws/lambda-invoke-store@0.3.0 - https://www.npmjs.com/package/@aws/lambda-invoke-store/v/0.3.0 | Apache-2.0 Apache License Version 2.0, January 2004 @@ -8155,7 +8361,7 @@ Apache License ---------------- -** @smithy/core@3.29.0 - https://www.npmjs.com/package/@smithy/core/v/3.29.0 | Apache-2.0 +** @smithy/core@3.29.1 - https://www.npmjs.com/package/@smithy/core/v/3.29.1 | Apache-2.0 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ @@ -8976,7 +9182,7 @@ Apache License ---------------- -** @smithy/node-http-handler@4.9.2 - https://www.npmjs.com/package/@smithy/node-http-handler/v/4.9.2 | Apache-2.0 +** @smithy/node-http-handler@4.9.3 - https://www.npmjs.com/package/@smithy/node-http-handler/v/4.9.3 | Apache-2.0 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ diff --git a/packages/cdk-assets/THIRD_PARTY_LICENSES b/packages/cdk-assets/THIRD_PARTY_LICENSES index f18c54429..7857d0241 100644 --- a/packages/cdk-assets/THIRD_PARTY_LICENSES +++ b/packages/cdk-assets/THIRD_PARTY_LICENSES @@ -1030,7 +1030,7 @@ The cdk-assets package includes the following third-party software/licensing: ---------------- -** @aws-sdk/core@3.974.26 - https://www.npmjs.com/package/@aws-sdk/core/v/3.974.26 | Apache-2.0 +** @aws-sdk/core@3.974.28 - https://www.npmjs.com/package/@aws-sdk/core/v/3.974.28 | Apache-2.0 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ @@ -1441,7 +1441,7 @@ Apache License ---------------- -** @aws-sdk/credential-provider-env@3.972.52 - https://www.npmjs.com/package/@aws-sdk/credential-provider-env/v/3.972.52 | Apache-2.0 +** @aws-sdk/credential-provider-env@3.972.54 - https://www.npmjs.com/package/@aws-sdk/credential-provider-env/v/3.972.54 | Apache-2.0 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ @@ -1646,11 +1646,11 @@ Apache License ---------------- -** @aws-sdk/credential-provider-http@3.972.54 - https://www.npmjs.com/package/@aws-sdk/credential-provider-http/v/3.972.54 | Apache-2.0 +** @aws-sdk/credential-provider-http@3.972.56 - https://www.npmjs.com/package/@aws-sdk/credential-provider-http/v/3.972.56 | Apache-2.0 ---------------- -** @aws-sdk/credential-provider-ini@3.972.59 - https://www.npmjs.com/package/@aws-sdk/credential-provider-ini/v/3.972.59 | Apache-2.0 +** @aws-sdk/credential-provider-ini@3.972.61 - https://www.npmjs.com/package/@aws-sdk/credential-provider-ini/v/3.972.61 | Apache-2.0 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ @@ -1855,11 +1855,11 @@ Apache License ---------------- -** @aws-sdk/credential-provider-login@3.972.58 - https://www.npmjs.com/package/@aws-sdk/credential-provider-login/v/3.972.58 | Apache-2.0 +** @aws-sdk/credential-provider-login@3.972.60 - https://www.npmjs.com/package/@aws-sdk/credential-provider-login/v/3.972.60 | Apache-2.0 ---------------- -** @aws-sdk/credential-provider-node@3.972.61 - https://www.npmjs.com/package/@aws-sdk/credential-provider-node/v/3.972.61 | Apache-2.0 +** @aws-sdk/credential-provider-node@3.972.63 - https://www.npmjs.com/package/@aws-sdk/credential-provider-node/v/3.972.63 | Apache-2.0 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ @@ -2064,7 +2064,7 @@ Apache License ---------------- -** @aws-sdk/credential-provider-process@3.972.52 - https://www.npmjs.com/package/@aws-sdk/credential-provider-process/v/3.972.52 | Apache-2.0 +** @aws-sdk/credential-provider-process@3.972.54 - https://www.npmjs.com/package/@aws-sdk/credential-provider-process/v/3.972.54 | Apache-2.0 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ @@ -2269,7 +2269,7 @@ Apache License ---------------- -** @aws-sdk/credential-provider-sso@3.972.58 - https://www.npmjs.com/package/@aws-sdk/credential-provider-sso/v/3.972.58 | Apache-2.0 +** @aws-sdk/credential-provider-sso@3.972.60 - https://www.npmjs.com/package/@aws-sdk/credential-provider-sso/v/3.972.60 | Apache-2.0 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ @@ -2474,7 +2474,7 @@ Apache License ---------------- -** @aws-sdk/credential-provider-web-identity@3.972.58 - https://www.npmjs.com/package/@aws-sdk/credential-provider-web-identity/v/3.972.58 | Apache-2.0 +** @aws-sdk/credential-provider-web-identity@3.972.60 - https://www.npmjs.com/package/@aws-sdk/credential-provider-web-identity/v/3.972.60 | Apache-2.0 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ @@ -3295,7 +3295,7 @@ Apache License ---------------- -** @aws-sdk/nested-clients@3.997.26 - https://www.npmjs.com/package/@aws-sdk/nested-clients/v/3.997.26 | Apache-2.0 +** @aws-sdk/nested-clients@3.997.28 - https://www.npmjs.com/package/@aws-sdk/nested-clients/v/3.997.28 | Apache-2.0 ---------------- @@ -3505,7 +3505,7 @@ Apache License ---------------- -** @aws-sdk/token-providers@3.1078.0 - https://www.npmjs.com/package/@aws-sdk/token-providers/v/3.1078.0 | Apache-2.0 +** @aws-sdk/token-providers@3.1080.0 - https://www.npmjs.com/package/@aws-sdk/token-providers/v/3.1080.0 | Apache-2.0 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ @@ -3915,7 +3915,7 @@ Apache License ---------------- -** @aws/lambda-invoke-store@0.2.4 - https://www.npmjs.com/package/@aws/lambda-invoke-store/v/0.2.4 | Apache-2.0 +** @aws/lambda-invoke-store@0.3.0 - https://www.npmjs.com/package/@aws/lambda-invoke-store/v/0.3.0 | Apache-2.0 Apache License Version 2.0, January 2004 @@ -4378,7 +4378,7 @@ Apache License ---------------- -** @smithy/core@3.29.0 - https://www.npmjs.com/package/@smithy/core/v/3.29.0 | Apache-2.0 +** @smithy/core@3.29.1 - https://www.npmjs.com/package/@smithy/core/v/3.29.1 | Apache-2.0 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ @@ -4994,7 +4994,7 @@ Apache License ---------------- -** @smithy/node-http-handler@4.9.2 - https://www.npmjs.com/package/@smithy/node-http-handler/v/4.9.2 | Apache-2.0 +** @smithy/node-http-handler@4.9.3 - https://www.npmjs.com/package/@smithy/node-http-handler/v/4.9.3 | Apache-2.0 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ diff --git a/yarn.lock b/yarn.lock index d4d54ecb7..96c8f302a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -501,6 +501,7 @@ __metadata: "@aws-sdk/client-bedrock-agentcore-control": "npm:^3" "@aws-sdk/client-cloudcontrol": "npm:^3" "@aws-sdk/client-cloudformation": "npm:^3" + "@aws-sdk/client-cloudtrail": "npm:^3" "@aws-sdk/client-cloudwatch-logs": "npm:^3" "@aws-sdk/client-codebuild": "npm:^3" "@aws-sdk/client-ec2": "npm:^3" @@ -651,6 +652,52 @@ __metadata: languageName: unknown linkType: soft +"@aws-crypto/sha256-browser@npm:5.2.0": + version: 5.2.0 + resolution: "@aws-crypto/sha256-browser@npm:5.2.0" + dependencies: + "@aws-crypto/sha256-js": "npm:^5.2.0" + "@aws-crypto/supports-web-crypto": "npm:^5.2.0" + "@aws-crypto/util": "npm:^5.2.0" + "@aws-sdk/types": "npm:^3.222.0" + "@aws-sdk/util-locate-window": "npm:^3.0.0" + "@smithy/util-utf8": "npm:^2.0.0" + tslib: "npm:^2.6.2" + checksum: 10c0/05f6d256794df800fe9aef5f52f2ac7415f7f3117d461f85a6aecaa4e29e91527b6fd503681a17136fa89e9dd3d916e9c7e4cfb5eba222875cb6c077bdc1d00d + languageName: node + linkType: hard + +"@aws-crypto/sha256-js@npm:5.2.0, @aws-crypto/sha256-js@npm:^5.2.0": + version: 5.2.0 + resolution: "@aws-crypto/sha256-js@npm:5.2.0" + dependencies: + "@aws-crypto/util": "npm:^5.2.0" + "@aws-sdk/types": "npm:^3.222.0" + tslib: "npm:^2.6.2" + checksum: 10c0/6c48701f8336341bb104dfde3d0050c89c288051f6b5e9bdfeb8091cf3ffc86efcd5c9e6ff2a4a134406b019c07aca9db608128f8d9267c952578a3108db9fd1 + languageName: node + linkType: hard + +"@aws-crypto/supports-web-crypto@npm:^5.2.0": + version: 5.2.0 + resolution: "@aws-crypto/supports-web-crypto@npm:5.2.0" + dependencies: + tslib: "npm:^2.6.2" + checksum: 10c0/4d2118e29d68ca3f5947f1e37ce1fbb3239a0c569cc938cdc8ab8390d595609b5caf51a07c9e0535105b17bf5c52ea256fed705a07e9681118120ab64ee73af2 + languageName: node + linkType: hard + +"@aws-crypto/util@npm:^5.2.0": + version: 5.2.0 + resolution: "@aws-crypto/util@npm:5.2.0" + dependencies: + "@aws-sdk/types": "npm:^3.222.0" + "@smithy/util-utf8": "npm:^2.0.0" + tslib: "npm:^2.6.2" + checksum: 10c0/0362d4c197b1fd64b423966945130207d1fe23e1bb2878a18e361f7743c8d339dad3f8729895a29aa34fff6a86c65f281cf5167c4bf253f21627ae80b6dd2951 + languageName: node + linkType: hard + "@aws-sdk/checksums@npm:^3.1000.11": version: 3.1000.11 resolution: "@aws-sdk/checksums@npm:3.1000.11" @@ -728,6 +775,24 @@ __metadata: languageName: node linkType: hard +"@aws-sdk/client-cloudtrail@npm:^3": + version: 3.1075.0 + resolution: "@aws-sdk/client-cloudtrail@npm:3.1075.0" + dependencies: + "@aws-crypto/sha256-browser": "npm:5.2.0" + "@aws-crypto/sha256-js": "npm:5.2.0" + "@aws-sdk/core": "npm:^3.974.23" + "@aws-sdk/credential-provider-node": "npm:^3.972.58" + "@aws-sdk/types": "npm:^3.973.13" + "@smithy/core": "npm:^3.24.6" + "@smithy/fetch-http-handler": "npm:^5.4.6" + "@smithy/node-http-handler": "npm:^4.7.6" + "@smithy/types": "npm:^4.14.3" + tslib: "npm:^2.6.2" + checksum: 10c0/a7a8118e743280ffdfbd6bd016d60055819a2589a682ef5879fa3b2708e457e8b71b91dfb215df25dd9f03e5eda0d5f2e492dbbf11df36d8ec7bfedf95c4bba3 + languageName: node + linkType: hard + "@aws-sdk/client-cloudwatch-logs@npm:^3": version: 3.1078.0 resolution: "@aws-sdk/client-cloudwatch-logs@npm:3.1078.0" @@ -1071,19 +1136,19 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/core@npm:^3.974.26": - version: 3.974.26 - resolution: "@aws-sdk/core@npm:3.974.26" +"@aws-sdk/core@npm:^3.974.23, @aws-sdk/core@npm:^3.974.26, @aws-sdk/core@npm:^3.974.28": + version: 3.974.28 + resolution: "@aws-sdk/core@npm:3.974.28" dependencies: "@aws-sdk/types": "npm:^3.973.15" "@aws-sdk/xml-builder": "npm:^3.972.33" - "@aws/lambda-invoke-store": "npm:^0.2.2" + "@aws/lambda-invoke-store": "npm:^0.3.0" "@smithy/core": "npm:^3.29.0" "@smithy/signature-v4": "npm:^5.6.1" "@smithy/types": "npm:^4.15.1" bowser: "npm:^2.11.0" tslib: "npm:^2.6.2" - checksum: 10c0/3e2f10c874b87cac48ccba38b4ec4bb7e2d1869f21a526044f4d32ed793eaab7dd4ed49f7ad02eb8a37a9042e2df24b08f8b6af645207522920416a10df62a2a + checksum: 10c0/0b4e3606effc65e6440294338433990346b96a31f04e568842fc9a9cd62e5497ddb3be7a2fc63ad4537ecae6083a0dd5ec5b0052639c2120a9333e43f99139e5 languageName: node linkType: hard @@ -1100,127 +1165,127 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/credential-provider-env@npm:^3.972.52": - version: 3.972.52 - resolution: "@aws-sdk/credential-provider-env@npm:3.972.52" +"@aws-sdk/credential-provider-env@npm:^3.972.52, @aws-sdk/credential-provider-env@npm:^3.972.54": + version: 3.972.54 + resolution: "@aws-sdk/credential-provider-env@npm:3.972.54" dependencies: - "@aws-sdk/core": "npm:^3.974.26" + "@aws-sdk/core": "npm:^3.974.28" "@aws-sdk/types": "npm:^3.973.15" "@smithy/core": "npm:^3.29.0" "@smithy/types": "npm:^4.15.1" tslib: "npm:^2.6.2" - checksum: 10c0/1626bd328408a7c511386715f3a7071243787605a6f846a1299d8bc72db7cd70181bd4623d38035b3e84986c88e2ceef0f870e511725d4effea33f3050d8fa64 + checksum: 10c0/86cd1be75066e3cf53346a67e1eacbc66e7773cf893ba15e1dde1733b7ed294f14fc621e5d80b27d725abab3f816c683cca14363b5ffbfba5e30f659792b1190 languageName: node linkType: hard -"@aws-sdk/credential-provider-http@npm:^3.972.54": - version: 3.972.54 - resolution: "@aws-sdk/credential-provider-http@npm:3.972.54" +"@aws-sdk/credential-provider-http@npm:^3.972.54, @aws-sdk/credential-provider-http@npm:^3.972.56": + version: 3.972.56 + resolution: "@aws-sdk/credential-provider-http@npm:3.972.56" dependencies: - "@aws-sdk/core": "npm:^3.974.26" + "@aws-sdk/core": "npm:^3.974.28" "@aws-sdk/types": "npm:^3.973.15" "@smithy/core": "npm:^3.29.0" "@smithy/fetch-http-handler": "npm:^5.6.2" "@smithy/node-http-handler": "npm:^4.9.2" "@smithy/types": "npm:^4.15.1" tslib: "npm:^2.6.2" - checksum: 10c0/600fbf04df77194b9fccffb61245b3f9f29218a61ebc5b0745d6cc01322697739cf298eafd91760a28780c11b6018347b566f8a045e602cd5a8f999e93dc37fa + checksum: 10c0/4232594560d49f3dde23eb0444ee55b28733376c54077757dcaf638ae05a2aec37b352d3561e9f765287715cb0de189f0cb3e0254667e5c11f160677f15caed5 languageName: node linkType: hard -"@aws-sdk/credential-provider-ini@npm:^3.972.59": - version: 3.972.59 - resolution: "@aws-sdk/credential-provider-ini@npm:3.972.59" - dependencies: - "@aws-sdk/core": "npm:^3.974.26" - "@aws-sdk/credential-provider-env": "npm:^3.972.52" - "@aws-sdk/credential-provider-http": "npm:^3.972.54" - "@aws-sdk/credential-provider-login": "npm:^3.972.58" - "@aws-sdk/credential-provider-process": "npm:^3.972.52" - "@aws-sdk/credential-provider-sso": "npm:^3.972.58" - "@aws-sdk/credential-provider-web-identity": "npm:^3.972.58" - "@aws-sdk/nested-clients": "npm:^3.997.26" +"@aws-sdk/credential-provider-ini@npm:^3.972.59, @aws-sdk/credential-provider-ini@npm:^3.972.61": + version: 3.972.61 + resolution: "@aws-sdk/credential-provider-ini@npm:3.972.61" + dependencies: + "@aws-sdk/core": "npm:^3.974.28" + "@aws-sdk/credential-provider-env": "npm:^3.972.54" + "@aws-sdk/credential-provider-http": "npm:^3.972.56" + "@aws-sdk/credential-provider-login": "npm:^3.972.60" + "@aws-sdk/credential-provider-process": "npm:^3.972.54" + "@aws-sdk/credential-provider-sso": "npm:^3.972.60" + "@aws-sdk/credential-provider-web-identity": "npm:^3.972.60" + "@aws-sdk/nested-clients": "npm:^3.997.28" "@aws-sdk/types": "npm:^3.973.15" "@smithy/core": "npm:^3.29.0" "@smithy/credential-provider-imds": "npm:^4.4.5" "@smithy/types": "npm:^4.15.1" tslib: "npm:^2.6.2" - checksum: 10c0/a3875f6b37942bfbac9dac1e57d7b9a5d642dfcbc8333014a66895377e47a21e2238709d97a9da0eb7bfdaf37307479a08ff30a0451a2ad7f984c8c89255e67e + checksum: 10c0/7d50fd8853dd0540bc3941b1792a557a0544a27f7586fa5a23360ac21f4d8678d6f502e89638c88bb558c4c8dc16cd7ba6b01327e1f12b1e45b9f0350722edea languageName: node linkType: hard -"@aws-sdk/credential-provider-login@npm:^3.972.58": - version: 3.972.58 - resolution: "@aws-sdk/credential-provider-login@npm:3.972.58" +"@aws-sdk/credential-provider-login@npm:^3.972.58, @aws-sdk/credential-provider-login@npm:^3.972.60": + version: 3.972.60 + resolution: "@aws-sdk/credential-provider-login@npm:3.972.60" dependencies: - "@aws-sdk/core": "npm:^3.974.26" - "@aws-sdk/nested-clients": "npm:^3.997.26" + "@aws-sdk/core": "npm:^3.974.28" + "@aws-sdk/nested-clients": "npm:^3.997.28" "@aws-sdk/types": "npm:^3.973.15" "@smithy/core": "npm:^3.29.0" "@smithy/types": "npm:^4.15.1" tslib: "npm:^2.6.2" - checksum: 10c0/9c3da8d17733920d765322d8fcea3d8b597989e35918662b6245482f65f33f900c7b549dece1da55171249b36664b3a7748b4d1d7512d243f22a0ac30ff566ba + checksum: 10c0/3eea91906876a987cb66a7e88f5f9e1581a7903cc18c7270463f7e5fd104c738995bf632bca98501784b8f54df1cc5060e9730199f8b46c7fad0a81a363f34d6 languageName: node linkType: hard -"@aws-sdk/credential-provider-node@npm:^3.972.61": - version: 3.972.61 - resolution: "@aws-sdk/credential-provider-node@npm:3.972.61" +"@aws-sdk/credential-provider-node@npm:^3.972.58, @aws-sdk/credential-provider-node@npm:^3.972.61": + version: 3.972.63 + resolution: "@aws-sdk/credential-provider-node@npm:3.972.63" dependencies: - "@aws-sdk/credential-provider-env": "npm:^3.972.52" - "@aws-sdk/credential-provider-http": "npm:^3.972.54" - "@aws-sdk/credential-provider-ini": "npm:^3.972.59" - "@aws-sdk/credential-provider-process": "npm:^3.972.52" - "@aws-sdk/credential-provider-sso": "npm:^3.972.58" - "@aws-sdk/credential-provider-web-identity": "npm:^3.972.58" + "@aws-sdk/credential-provider-env": "npm:^3.972.54" + "@aws-sdk/credential-provider-http": "npm:^3.972.56" + "@aws-sdk/credential-provider-ini": "npm:^3.972.61" + "@aws-sdk/credential-provider-process": "npm:^3.972.54" + "@aws-sdk/credential-provider-sso": "npm:^3.972.60" + "@aws-sdk/credential-provider-web-identity": "npm:^3.972.60" "@aws-sdk/types": "npm:^3.973.15" "@smithy/core": "npm:^3.29.0" "@smithy/credential-provider-imds": "npm:^4.4.5" "@smithy/types": "npm:^4.15.1" tslib: "npm:^2.6.2" - checksum: 10c0/eb86129c9b1f3fc96b504fbcf9d9405f3f7ec24cb10a59dd1a28b5fa1ecef908e8e3aa9df0f1939950ccc9669dbf8664d30f091cf4c7b79423dab9188a1d5367 + checksum: 10c0/bc0e50044196acd702450b1275c102416fec8be9a93ba559335779483a716a4fb6c9a32d7ce271d167546f14ec50e094e24db3eb4e2ee9a33b1e75f53ebe31ab languageName: node linkType: hard -"@aws-sdk/credential-provider-process@npm:^3.972.52": - version: 3.972.52 - resolution: "@aws-sdk/credential-provider-process@npm:3.972.52" +"@aws-sdk/credential-provider-process@npm:^3.972.52, @aws-sdk/credential-provider-process@npm:^3.972.54": + version: 3.972.54 + resolution: "@aws-sdk/credential-provider-process@npm:3.972.54" dependencies: - "@aws-sdk/core": "npm:^3.974.26" + "@aws-sdk/core": "npm:^3.974.28" "@aws-sdk/types": "npm:^3.973.15" "@smithy/core": "npm:^3.29.0" "@smithy/types": "npm:^4.15.1" tslib: "npm:^2.6.2" - checksum: 10c0/65b5772ec0e6ea1dd9be3819278aa10361abb3e79df4022316f60aff2ed2492db5b1ecc8ebc08727beede323b6b605b8a61e60b70a6c43b360370f09352b32b3 + checksum: 10c0/ba4510fc6c2741544ccd8b7485cea06fefe9d3695cbb5a153d51b86685b61afadf8acc79f4731352e30d3ca02638838d1e8799bac5be33ad94de1ead290ac497 languageName: node linkType: hard -"@aws-sdk/credential-provider-sso@npm:^3.972.58": - version: 3.972.58 - resolution: "@aws-sdk/credential-provider-sso@npm:3.972.58" +"@aws-sdk/credential-provider-sso@npm:^3.972.58, @aws-sdk/credential-provider-sso@npm:^3.972.60": + version: 3.972.60 + resolution: "@aws-sdk/credential-provider-sso@npm:3.972.60" dependencies: - "@aws-sdk/core": "npm:^3.974.26" - "@aws-sdk/nested-clients": "npm:^3.997.26" - "@aws-sdk/token-providers": "npm:3.1078.0" + "@aws-sdk/core": "npm:^3.974.28" + "@aws-sdk/nested-clients": "npm:^3.997.28" + "@aws-sdk/token-providers": "npm:3.1080.0" "@aws-sdk/types": "npm:^3.973.15" "@smithy/core": "npm:^3.29.0" "@smithy/types": "npm:^4.15.1" tslib: "npm:^2.6.2" - checksum: 10c0/d12aa015435d99e08d7959c46ea3624141178e833d135b52ead89691526b3103e6b0e578bf62be85bb6a57c9f6018e77c420f64236379604f20c91042b71967c + checksum: 10c0/0577f4fbdc25616a96831b0d47f7fa9734fad1cecc792586263ab8f5e360a720ec9e5ff19064b7f1368b625cea83b632abdfad499050a3dab60aa7005f629e6d languageName: node linkType: hard -"@aws-sdk/credential-provider-web-identity@npm:^3.972.58": - version: 3.972.58 - resolution: "@aws-sdk/credential-provider-web-identity@npm:3.972.58" +"@aws-sdk/credential-provider-web-identity@npm:^3.972.58, @aws-sdk/credential-provider-web-identity@npm:^3.972.60": + version: 3.972.60 + resolution: "@aws-sdk/credential-provider-web-identity@npm:3.972.60" dependencies: - "@aws-sdk/core": "npm:^3.974.26" - "@aws-sdk/nested-clients": "npm:^3.997.26" + "@aws-sdk/core": "npm:^3.974.28" + "@aws-sdk/nested-clients": "npm:^3.997.28" "@aws-sdk/types": "npm:^3.973.15" "@smithy/core": "npm:^3.29.0" "@smithy/types": "npm:^4.15.1" tslib: "npm:^2.6.2" - checksum: 10c0/3c4e185fac1ff6cc81494c4f0b8c9344c99935499c2f53c63e69276ff039ca4d336b0afae83223acb48230adc5f322d8b5807be13981f1a651a89965c0e100fb + checksum: 10c0/3efe2e94f1fab86b049b50f05543dc3d6621321843a046395a29c6c7720da9b1109f0e6a0fa5b18426d2e351ae135bc73439815b06ff03b7f427eacc47c55ccc languageName: node linkType: hard @@ -1352,11 +1417,11 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/nested-clients@npm:^3.997.26": - version: 3.997.26 - resolution: "@aws-sdk/nested-clients@npm:3.997.26" +"@aws-sdk/nested-clients@npm:^3.997.26, @aws-sdk/nested-clients@npm:^3.997.28": + version: 3.997.28 + resolution: "@aws-sdk/nested-clients@npm:3.997.28" dependencies: - "@aws-sdk/core": "npm:^3.974.26" + "@aws-sdk/core": "npm:^3.974.28" "@aws-sdk/signature-v4-multi-region": "npm:^3.996.38" "@aws-sdk/types": "npm:^3.973.15" "@smithy/core": "npm:^3.29.0" @@ -1364,7 +1429,7 @@ __metadata: "@smithy/node-http-handler": "npm:^4.9.2" "@smithy/types": "npm:^4.15.1" tslib: "npm:^2.6.2" - checksum: 10c0/38c23c3f98926e4491aae973df851d503ca16bbd97b613d84aedbb391e6835e0ddba22c60a1b96047a1a3dda3ecd2175500d701076b5bd7446bb9a44a3a9989a + checksum: 10c0/5cab1b83f2d91c11080dde33080eda7433fb9cd60b4a4f154f2073098a785ed00995087e675708edfe4d3f1a3590c6ad9cd72d441ef18a7261d62b10029afda6 languageName: node linkType: hard @@ -1380,21 +1445,21 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/token-providers@npm:3.1078.0": - version: 3.1078.0 - resolution: "@aws-sdk/token-providers@npm:3.1078.0" +"@aws-sdk/token-providers@npm:3.1080.0": + version: 3.1080.0 + resolution: "@aws-sdk/token-providers@npm:3.1080.0" dependencies: - "@aws-sdk/core": "npm:^3.974.26" - "@aws-sdk/nested-clients": "npm:^3.997.26" + "@aws-sdk/core": "npm:^3.974.28" + "@aws-sdk/nested-clients": "npm:^3.997.28" "@aws-sdk/types": "npm:^3.973.15" "@smithy/core": "npm:^3.29.0" "@smithy/types": "npm:^4.15.1" tslib: "npm:^2.6.2" - checksum: 10c0/90ab6dd1460e0c0083305146d830d76faead11ab170a9d8d274c0157a323b45e0a45959b8f22cf6bf3ca0230eb3f731e0eab13fba755b20377495adee28fff38 + checksum: 10c0/0b8765b90b6cd59cf35d099561d07f0c79885a2a5b8d1ca73ac93e9f26eb0d25a12266e864c5528186da534c0755c53085ea12a577cedfc8d4b83b8dc0947880 languageName: node linkType: hard -"@aws-sdk/types@npm:^3.973.15": +"@aws-sdk/types@npm:^3.222.0, @aws-sdk/types@npm:^3.973.13, @aws-sdk/types@npm:^3.973.15": version: 3.973.15 resolution: "@aws-sdk/types@npm:3.973.15" dependencies: @@ -1404,6 +1469,15 @@ __metadata: languageName: node linkType: hard +"@aws-sdk/util-locate-window@npm:^3.0.0": + version: 3.965.8 + resolution: "@aws-sdk/util-locate-window@npm:3.965.8" + dependencies: + tslib: "npm:^2.6.2" + checksum: 10c0/fc3fd154cf560d815a4e9a58aadd1aed860a9dd8cc82ce9824faf9255c688626339077940d35ae91798b753dd7ad4e735c08d4f015b573e681130712a8b52865 + languageName: node + linkType: hard + "@aws-sdk/xml-builder@npm:^3.972.33": version: 3.972.33 resolution: "@aws-sdk/xml-builder@npm:3.972.33" @@ -1414,10 +1488,10 @@ __metadata: languageName: node linkType: hard -"@aws/lambda-invoke-store@npm:^0.2.2": - version: 0.2.4 - resolution: "@aws/lambda-invoke-store@npm:0.2.4" - checksum: 10c0/29d874d7c1a2d971e0c02980594204f89cda718f215f2fc52b6c56eacbdad1fa5f6ce1b358e5811f5cd35d04c76299a67a8aff95318446af2bdfb4910f213e13 +"@aws/lambda-invoke-store@npm:^0.3.0": + version: 0.3.0 + resolution: "@aws/lambda-invoke-store@npm:0.3.0" + checksum: 10c0/b4a2e6b3b5397bc606053e64270d26dc5c886336f88a98cad587b1592eec17058f8fb172f1827a9f0e591f3595cf8f01575c8c9b36cde38c06456f8a65204046 languageName: node linkType: hard @@ -3555,13 +3629,13 @@ __metadata: languageName: node linkType: hard -"@smithy/core@npm:^3.29.0": - version: 3.29.0 - resolution: "@smithy/core@npm:3.29.0" +"@smithy/core@npm:^3.24.6, @smithy/core@npm:^3.29.0, @smithy/core@npm:^3.29.1": + version: 3.29.1 + resolution: "@smithy/core@npm:3.29.1" dependencies: "@smithy/types": "npm:^4.15.1" tslib: "npm:^2.6.2" - checksum: 10c0/4d1dfa9935bd7aecd53c70f8dcf340736cbd9e5893da65d4bf8ece038d62bdbc09ce9e8beef4bd35b808470d5408eaefd4cccba7dbffdb945526679846325081 + checksum: 10c0/5e536cf179d4e66742884ec07301c9efdc3d6edd9161841c498096ce15cba1592653c639bdcc12d000462f844c10d7c41739312be5d78246c8c130f4407a6174 languageName: node linkType: hard @@ -3576,14 +3650,23 @@ __metadata: languageName: node linkType: hard -"@smithy/fetch-http-handler@npm:^5.6.2": - version: 5.6.2 - resolution: "@smithy/fetch-http-handler@npm:5.6.2" +"@smithy/fetch-http-handler@npm:^5.4.6, @smithy/fetch-http-handler@npm:^5.6.2": + version: 5.6.3 + resolution: "@smithy/fetch-http-handler@npm:5.6.3" dependencies: - "@smithy/core": "npm:^3.29.0" + "@smithy/core": "npm:^3.29.1" "@smithy/types": "npm:^4.15.1" tslib: "npm:^2.6.2" - checksum: 10c0/adcc1bab05e2aad9d2e28adc359821dfdae1b76987cd0434b1003214b2e9453bbab666b989d9c070e184b1e2431ba1ba125af61fcdc92961f507b12170364d2d + checksum: 10c0/13f046a0edf6f642581e730537d98636dbbda3acda3c70334d5878818d1f2c98213a82330b4cf396fbc558fbebf3e4c1ab6fe932a85ffdbb3678b7d0d8e4d38f + languageName: node + linkType: hard + +"@smithy/is-array-buffer@npm:^2.2.0": + version: 2.2.0 + resolution: "@smithy/is-array-buffer@npm:2.2.0" + dependencies: + tslib: "npm:^2.6.2" + checksum: 10c0/2f2523cd8cc4538131e408eb31664983fecb0c8724956788b015aaf3ab85a0c976b50f4f09b176f1ed7bbe79f3edf80743be7a80a11f22cd9ce1285d77161aaf languageName: node linkType: hard @@ -3607,14 +3690,14 @@ __metadata: languageName: node linkType: hard -"@smithy/node-http-handler@npm:^4.9.2": - version: 4.9.2 - resolution: "@smithy/node-http-handler@npm:4.9.2" +"@smithy/node-http-handler@npm:^4.7.6, @smithy/node-http-handler@npm:^4.9.2": + version: 4.9.3 + resolution: "@smithy/node-http-handler@npm:4.9.3" dependencies: - "@smithy/core": "npm:^3.29.0" + "@smithy/core": "npm:^3.29.1" "@smithy/types": "npm:^4.15.1" tslib: "npm:^2.6.2" - checksum: 10c0/b619c3f79a762538a6ebf34a84abb60ac77ccbdf577449daf7358b2bb04ce791ca90a1ceeffbfde66a87165cceeabe1c92cec11b1a1eecaef2f8277480cb63d0 + checksum: 10c0/f584aa4ca9b2197b26b0295ffed4af9b00a69bdef99fc6c9918eafb1b20d7217ea354832b1f8d51500b1483033af31918863bd2ea294fbbff2ff975822ec33f6 languageName: node linkType: hard @@ -3660,7 +3743,7 @@ __metadata: languageName: node linkType: hard -"@smithy/types@npm:^4, @smithy/types@npm:^4.15.1": +"@smithy/types@npm:^4, @smithy/types@npm:^4.14.3, @smithy/types@npm:^4.15.1": version: 4.15.1 resolution: "@smithy/types@npm:4.15.1" dependencies: @@ -3669,6 +3752,16 @@ __metadata: languageName: node linkType: hard +"@smithy/util-buffer-from@npm:^2.2.0": + version: 2.2.0 + resolution: "@smithy/util-buffer-from@npm:2.2.0" + dependencies: + "@smithy/is-array-buffer": "npm:^2.2.0" + tslib: "npm:^2.6.2" + checksum: 10c0/223d6a508b52ff236eea01cddc062b7652d859dd01d457a4e50365af3de1e24a05f756e19433f6ccf1538544076b4215469e21a4ea83dc1d58d829725b0dbc5a + languageName: node + linkType: hard + "@smithy/util-retry@npm:^4": version: 4.5.5 resolution: "@smithy/util-retry@npm:4.5.5" @@ -3689,6 +3782,16 @@ __metadata: languageName: node linkType: hard +"@smithy/util-utf8@npm:^2.0.0": + version: 2.3.0 + resolution: "@smithy/util-utf8@npm:2.3.0" + dependencies: + "@smithy/util-buffer-from": "npm:^2.2.0" + tslib: "npm:^2.6.2" + checksum: 10c0/e18840c58cc507ca57fdd624302aefd13337ee982754c9aa688463ffcae598c08461e8620e9852a424d662ffa948fc64919e852508028d09e89ced459bd506ab + languageName: node + linkType: hard + "@smithy/util-waiter@npm:^4": version: 4.5.5 resolution: "@smithy/util-waiter@npm:4.5.5"