@go-to-k/cdkd 0.284.32 → 0.284.34
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{asg-provider-BVzQ7mQX.js → asg-provider-DZ_xdKkZ.js} +2 -2
- package/dist/{asg-provider-BVzQ7mQX.js.map → asg-provider-DZ_xdKkZ.js.map} +1 -1
- package/dist/cli.js +323 -26
- package/dist/cli.js.map +1 -1
- package/dist/{deploy-engine-DsUQ4GxO.js → deploy-engine-BxP9k1OS.js} +310 -24
- package/dist/deploy-engine-BxP9k1OS.js.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/dist/deploy-engine-DsUQ4GxO.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { $ as endCommandInterruptScope, $t as buildAssetRedirectMap, A as MULTI_REGION_RECREATE_BLOCKED_TYPES, An as resolveStateBucketWithDefaultAndSource, At as maskSecretsInError, B as collectDeclaredOutputNames, Bn as PARTITION_TABLE, Bt as DiffCalculator, C as ccRoutedFinalSnapshotError, Cn as getDefaultStateBucketName, Cr as isThrottlingError, Ct as STATE_SOURCED_CROSS_GENERATION_RULES, D as unsupportedFinalSnapshotError, Dn as resolveCaptureObservedState, Dt as dynamicReferenceTokens, E as refusesFinalSnapshot, En as resolveAutoAssetStorage, Et as createSecretMasker, F as cyan, Fn as CFN_TEMPLATE_URL_LIMIT, Ft as s3BucketDomainName, G as stateKeySecretExposure, Gt as TemplateParser, H as exportAliasCollisionScrubWarning, Hn as derivePartitionAndUrlSuffix, Ht as describeTypeWithThrottleRetry, I as gray, In as MIGRATE_TMP_PREFIX, It as s3BucketDualStackDomainName, J as clearOnUpdateRemoval, Jn as getAwsClients, Jt as rebuildClientForBucketRegion, K as IAMRoleProvider, Kn as resolveBucketRegion, Kt as LockManager, L as green, Ln as findLargeInlineResources, Lt as s3BucketRegionalDomainName, M as renderStatefulReason, Mn as stateBucketExistenceConfirmed, Mt as redactSecretsForState, N as formatResourceLine, Nn as warnDeprecatedNoPrefixCliFlag, Nt as scrubResourceRecord, O as makeCanonicalizePropertiesFn, On as resolveSkipPrefix, Ot as errorCauseChain, P as bold, Pn as CFN_TEMPLATE_BODY_LIMIT, Pt as s3BucketArn, Q as beginCommandInterruptScope, Qn as CdkdError, Qt as WorkGraph, R as red, Rn as uploadCfnTemplate, Rt as s3BucketWebsiteUrl, S as buildFinalSnapshotIdentifier, Sn as synthesisStatusMessage, Sr as isRetryableTransientError, St as requireConfigString, T as isFinalSnapshotError, Tn as resolveApp, Tt as TEMPLATE_SOURCED_RULES, U as isExportAliasCollision, Un as AssemblyReader, Ut as withRetry, V as collectPublishedOutputNames, Vn as canonicalizeRegion, Vt as INTRINSIC_KEYS, W as secretBearingStateKeyWarning, Wn as processStackMessages, Wt as DagBuilder, X as findActionableSilentDrops, Xn as setAwsClients, Xt as AssetPublisher, Y as ProviderRegistry, Yn as resetAwsClients, Yt as shouldRetainResource, Z as findSilentDropProperties, Zt as stringifyValue, _ as maskingRetryLogger, _n as runDockerForeground, _t as configStringRefusal, a as DeploymentEventsStore, an as AssetModeResolver, ar as LocalStartServiceError, at as isTerminationProtectionPropagationError, b as ATOMIC_FINAL_SNAPSHOT_TYPES, bn as getDockerImageBySourceHash, br as withErrorHandling, bt as requireConfigArray, c as planRollback, cn as getBootstrapMarkerKey, cr as NestedStackChildDirectDestroyError, ct as cfnRefValueFromPhysicalId, d as replayRollback, dn as validateAssetBucketName, dr as ResourceTimeoutError, dt as WAFv2WebACLProvider, en as createAssetRedirectResolver, er as CrossAccountSecretRefusalError, et as isInterruptedWaitError, f as updatePartialMessage, fn as validateContainerRepoName, fr as ResourceUpdateNotSupportedError, ft as normalizeAwsTagsToCfn, g as withResourceDeadline, gn as getDockerCmd, gt as configBooleanRefusal, h as deleteSkipReason, hn as formatDockerLoginError, ht as coerceCfnBoolean, i as DeploymentEventsReader, in as stripControlChars, ir as LocalMigrateError, it as disableInstanceApiTermination, j as isStatefulRecreateTargetSync, jn as resolveUseCdkBootstrapAssets, jt as maskSecretsInText, k as extractDeploymentEventError, kn as resolveStateBucketWithDefault, kt as isSingleDynamicReferenceToken, l as producerRegionsFromState, ln as parseBootstrapMarker, lr as PartialFailureError, lt as getAccountInfo, m as UNSPECIFIED_SKIP_REASON, mn as buildDockerImage, mr as StackTerminationProtectionError, mt as assertRegionMatch, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nn as rewriteTemplateAssetReferences, nr as DeployCancelledError, nt as CloudControlProvider, o as classifyReplaySecretRegion, on as BOOTSTRAP_MARKER_PREFIX, ot as IntrinsicFunctionResolver, p as updatePartialReason, pn as buildDenyExternalAccessPolicy, pr as StackHasActiveImportsError, pt as resolveExplicitPhysicalId, q as collectInlinePolicyNamesManagedBySiblings, qn as AwsClients, qt as S3StateBackend, r as DeployEngine, rn as escapeRegExp, rr as LocalInvokeBuildError$1, rt as slowCcOperationTimeoutMs, s as planFailedOps, sn as ensureAssetStorage, sr as MissingCdkCliError, st as carriesDynamicReference, t as DEFAULT_RESOURCE_TIMEOUT_MS, tn as loadPublishableAssetManifest, tt as startInterruptWatch, u as replayFailedOperations, un as readBootstrapMarkerBody, ur as ProvisioningError, ut as refStateLookupFromResource, v as IMPLICIT_DELETE_DEPENDENCIES, vn as runDockerStreaming, vt as readConfigString, w as createPreDeleteFinalSnapshot, wn as getLegacyStateBucketName, wr as markNonRetryable, wt as STATE_SOURCED_READBACK_RULES, x as PRE_DELETE_SNAPSHOT_TYPES, xn as Synthesizer, xr as isMarkedNonRetryable, xt as requireConfigObject, y as computeImplicitDeleteEdges, yn as AssetManifestLoader, yr as normalizeAwsError, yt as replayWarn, z as yellow, zn as expectedOwnerParam, zt as applyRoleArnIfSet } from "./deploy-engine-
|
|
2
|
+
import { $ as endCommandInterruptScope, $t as buildAssetRedirectMap, A as MULTI_REGION_RECREATE_BLOCKED_TYPES, An as resolveStateBucketWithDefaultAndSource, At as maskSecretsInError, B as collectDeclaredOutputNames, Bn as PARTITION_TABLE, Bt as DiffCalculator, C as ccRoutedFinalSnapshotError, Cn as getDefaultStateBucketName, Cr as isThrottlingError, Ct as STATE_SOURCED_CROSS_GENERATION_RULES, D as unsupportedFinalSnapshotError, Dn as resolveCaptureObservedState, Dt as dynamicReferenceTokens, E as refusesFinalSnapshot, En as resolveAutoAssetStorage, Et as createSecretMasker, F as cyan, Fn as CFN_TEMPLATE_URL_LIMIT, Ft as s3BucketDomainName, G as stateKeySecretExposure, Gt as TemplateParser, H as exportAliasCollisionScrubWarning, Hn as derivePartitionAndUrlSuffix, Ht as describeTypeWithThrottleRetry, I as gray, In as MIGRATE_TMP_PREFIX, It as s3BucketDualStackDomainName, J as clearOnUpdateRemoval, Jn as getAwsClients, Jt as rebuildClientForBucketRegion, K as IAMRoleProvider, Kn as resolveBucketRegion, Kt as LockManager, L as green, Ln as findLargeInlineResources, Lt as s3BucketRegionalDomainName, M as renderStatefulReason, Mn as stateBucketExistenceConfirmed, Mt as redactSecretsForState, N as formatResourceLine, Nn as warnDeprecatedNoPrefixCliFlag, Nt as scrubResourceRecord, O as makeCanonicalizePropertiesFn, On as resolveSkipPrefix, Ot as errorCauseChain, P as bold, Pn as CFN_TEMPLATE_BODY_LIMIT, Pt as s3BucketArn, Q as beginCommandInterruptScope, Qn as CdkdError, Qt as WorkGraph, R as red, Rn as uploadCfnTemplate, Rt as s3BucketWebsiteUrl, S as buildFinalSnapshotIdentifier, Sn as synthesisStatusMessage, Sr as isRetryableTransientError, St as requireConfigString, T as isFinalSnapshotError, Tn as resolveApp, Tt as TEMPLATE_SOURCED_RULES, U as isExportAliasCollision, Un as AssemblyReader, Ut as withRetry, V as collectPublishedOutputNames, Vn as canonicalizeRegion, Vt as INTRINSIC_KEYS, W as secretBearingStateKeyWarning, Wn as processStackMessages, Wt as DagBuilder, X as findActionableSilentDrops, Xn as setAwsClients, Xt as AssetPublisher, Y as ProviderRegistry, Yn as resetAwsClients, Yt as shouldRetainResource, Z as findSilentDropProperties, Zt as stringifyValue, _ as maskingRetryLogger, _n as runDockerForeground, _t as configStringRefusal, a as DeploymentEventsStore, an as AssetModeResolver, ar as LocalStartServiceError, at as isTerminationProtectionPropagationError, b as ATOMIC_FINAL_SNAPSHOT_TYPES, bn as getDockerImageBySourceHash, br as withErrorHandling, bt as requireConfigArray, c as planRollback, cn as getBootstrapMarkerKey, cr as NestedStackChildDirectDestroyError, ct as cfnRefValueFromPhysicalId, d as replayRollback, dn as validateAssetBucketName, dr as ResourceTimeoutError, dt as WAFv2WebACLProvider, en as createAssetRedirectResolver, er as CrossAccountSecretRefusalError, et as isInterruptedWaitError, f as updatePartialMessage, fn as validateContainerRepoName, fr as ResourceUpdateNotSupportedError, ft as normalizeAwsTagsToCfn, g as withResourceDeadline, gn as getDockerCmd, gt as configBooleanRefusal, h as deleteSkipReason, hn as formatDockerLoginError, ht as coerceCfnBoolean, i as DeploymentEventsReader, in as stripControlChars, ir as LocalMigrateError, it as disableInstanceApiTermination, j as isStatefulRecreateTargetSync, jn as resolveUseCdkBootstrapAssets, jt as maskSecretsInText, k as extractDeploymentEventError, kn as resolveStateBucketWithDefault, kt as isSingleDynamicReferenceToken, l as producerRegionsFromState, ln as parseBootstrapMarker, lr as PartialFailureError, lt as getAccountInfo, m as UNSPECIFIED_SKIP_REASON, mn as buildDockerImage, mr as StackTerminationProtectionError, mt as assertRegionMatch, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nn as rewriteTemplateAssetReferences, nr as DeployCancelledError, nt as CloudControlProvider, o as classifyReplaySecretRegion, on as BOOTSTRAP_MARKER_PREFIX, ot as IntrinsicFunctionResolver, p as updatePartialReason, pn as buildDenyExternalAccessPolicy, pr as StackHasActiveImportsError, pt as resolveExplicitPhysicalId, q as collectInlinePolicyNamesManagedBySiblings, qn as AwsClients, qt as S3StateBackend, r as DeployEngine, rn as escapeRegExp, rr as LocalInvokeBuildError$1, rt as slowCcOperationTimeoutMs, s as planFailedOps, sn as ensureAssetStorage, sr as MissingCdkCliError, st as carriesDynamicReference, t as DEFAULT_RESOURCE_TIMEOUT_MS, tn as loadPublishableAssetManifest, tt as startInterruptWatch, u as replayFailedOperations, un as readBootstrapMarkerBody, ur as ProvisioningError, ut as refStateLookupFromResource, v as IMPLICIT_DELETE_DEPENDENCIES, vn as runDockerStreaming, vt as readConfigString, w as createPreDeleteFinalSnapshot, wn as getLegacyStateBucketName, wr as markNonRetryable, wt as STATE_SOURCED_READBACK_RULES, x as PRE_DELETE_SNAPSHOT_TYPES, xn as Synthesizer, xr as isMarkedNonRetryable, xt as requireConfigObject, y as computeImplicitDeleteEdges, yn as AssetManifestLoader, yr as normalizeAwsError, yt as replayWarn, z as yellow, zn as expectedOwnerParam, zt as applyRoleArnIfSet } from "./deploy-engine-BxP9k1OS.js";
|
|
3
3
|
import { a as getLiveRenderer, c as PATTERN_B_RESOURCE_TYPES, d as generateResourceNameWithFallback, f as getCurrentStackName, h as withStackName, i as runStackBuffered, m as withSkipPrefix, n as getLogger, o as PATTERN_B_NAME_OPTIONS, s as PATTERN_B_NAME_PROPERTIES, u as generateResourceName } from "./logger-zRrlbaQt.js";
|
|
4
|
-
import { t as ASGProvider } from "./asg-provider-
|
|
4
|
+
import { t as ASGProvider } from "./asg-provider-DZ_xdKkZ.js";
|
|
5
5
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
6
6
|
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
7
7
|
import { CopyObjectCommand, CreateBucketCommand, DeleteBucketAnalyticsConfigurationCommand, DeleteBucketCommand, DeleteBucketCorsCommand, DeleteBucketEncryptionCommand, DeleteBucketIntelligentTieringConfigurationCommand, DeleteBucketInventoryConfigurationCommand, DeleteBucketLifecycleCommand, DeleteBucketMetricsConfigurationCommand, DeleteBucketOwnershipControlsCommand, DeleteBucketPolicyCommand, DeleteBucketReplicationCommand, DeleteBucketTaggingCommand, DeleteBucketWebsiteCommand, DeleteObjectsCommand, GetBucketAccelerateConfigurationCommand, GetBucketCorsCommand, GetBucketEncryptionCommand, GetBucketLifecycleConfigurationCommand, GetBucketLocationCommand, GetBucketLoggingCommand, GetBucketNotificationConfigurationCommand, GetBucketOwnershipControlsCommand, GetBucketPolicyCommand, GetBucketReplicationCommand, GetBucketTaggingCommand, GetBucketVersioningCommand, GetBucketWebsiteCommand, GetObjectCommand, GetObjectLockConfigurationCommand, GetPublicAccessBlockCommand, HeadBucketCommand, ListBucketAnalyticsConfigurationsCommand, ListBucketIntelligentTieringConfigurationsCommand, ListBucketInventoryConfigurationsCommand, ListBucketMetricsConfigurationsCommand, ListObjectVersionsCommand, ListObjectsV2Command, NoSuchBucket, PutBucketAccelerateConfigurationCommand, PutBucketAnalyticsConfigurationCommand, PutBucketCorsCommand, PutBucketEncryptionCommand, PutBucketIntelligentTieringConfigurationCommand, PutBucketInventoryConfigurationCommand, PutBucketLifecycleConfigurationCommand, PutBucketLoggingCommand, PutBucketMetricsConfigurationCommand, PutBucketNotificationConfigurationCommand, PutBucketOwnershipControlsCommand, PutBucketPolicyCommand, PutBucketReplicationCommand, PutBucketTaggingCommand, PutBucketVersioningCommand, PutBucketWebsiteCommand, PutObjectCommand, PutObjectLockConfigurationCommand, PutPublicAccessBlockCommand, S3Client, S3ServiceException } from "@aws-sdk/client-s3";
|
|
@@ -63254,6 +63254,71 @@ function unresolvableCrossStackReadError(origin, stackName, intrinsic, path, cau
|
|
|
63254
63254
|
return new ScrubRefusalError(`Scrub of ${stackName} could not resolve the ${intrinsic} in ${origin}${path ? ` at ${path}` : ""}: ${maskSecretsInText(cause instanceof Error ? cause.message : String(cause), secrets)}. Refusing rather than continuing: the value that reference carries may be a secret, and scrub would then have no plaintext to look for — reporting the stack clean over state that still holds it. Deploy the producer stack (or correct the reference) and re-run 'cdkd scrub'.`, "SCRUB_CROSS_STACK_READ_UNRESOLVED");
|
|
63255
63255
|
}
|
|
63256
63256
|
/**
|
|
63257
|
+
* The two sentences the plaintext-producer refusal is built from: what the
|
|
63258
|
+
* producer's TEMPLATE is being claimed to say, and the remedy that clears it
|
|
63259
|
+
* (issues [#2133](https://github.com/go-to-k/cdkd/issues/2133) review and
|
|
63260
|
+
* [#2146](https://github.com/go-to-k/cdkd/issues/2146) review).
|
|
63261
|
+
*
|
|
63262
|
+
* SEPARATE FROM THE ERROR BUILDER so both can be tested over verdict shapes the
|
|
63263
|
+
* walk does not produce today. The one below — `chained` with an EMPTY `via` —
|
|
63264
|
+
* is exactly such a shape: unreachable, because `chained` only ever returns
|
|
63265
|
+
* from a non-root frame whose `via` therefore has at least one entry, and yet
|
|
63266
|
+
* the arm that used to catch it was the WIDENED wording, which asserts "this run
|
|
63267
|
+
* could not match '<key>' to a declared output" — false of every `chained`
|
|
63268
|
+
* verdict, since a chained one matched the key and left through its value.
|
|
63269
|
+
*
|
|
63270
|
+
* WHICH SIGNAL KEYS WHICH ARM, since this file has now been round-tripped on it
|
|
63271
|
+
* once. The CLAIM keys on `kind`: only `widened` may say "could not say which",
|
|
63272
|
+
* and no amount of chain changes that. WHAT the claim names keys on the chain:
|
|
63273
|
+
* a `widened` verdict CAN have crossed a re-export, and saying "publishes at
|
|
63274
|
+
* least one output from a `{{resolve:...}}` expression" when the walk has just
|
|
63275
|
+
* established that none of its outputs does is the same over-claim in the other
|
|
63276
|
+
* direction.
|
|
63277
|
+
*
|
|
63278
|
+
* `via` IS DE-DUPLICATED, and may name the producer itself (issue #2146 review).
|
|
63279
|
+
* A chain that returns to the direct producer under a DIFFERENT output key —
|
|
63280
|
+
* `A -> B -> A`, or a self-import `A -> A` — is a real walk result, since the
|
|
63281
|
+
* visited set is keyed by `(stack, key)` rather than by stack. Left as-is it
|
|
63282
|
+
* rendered "producer 'A' ... RE-EXPORTING a value that 'A' declares ... (through
|
|
63283
|
+
* 'A')" and a remedy running `cdkd scrub A` two or three times, which reads as a
|
|
63284
|
+
* bug in the tool rather than as advice. The claim de-duplicates
|
|
63285
|
+
* ORDER-PRESERVINGLY keeping the FIRST occurrence (so the stack that declares
|
|
63286
|
+
* the expression stays last, where `chainRoot` reads it), and the remedy keeps
|
|
63287
|
+
* the LAST (so the DIRECT producer — the one actually holding the plaintext —
|
|
63288
|
+
* stays at the end of the order, where the user must scrub it).
|
|
63289
|
+
*
|
|
63290
|
+
* Naming the producer inside its own chain is left standing rather than
|
|
63291
|
+
* filtered: for `A -> B -> A` it is A that declares the expression, under an
|
|
63292
|
+
* output the consumer's key did not match, so dropping it would move the claim
|
|
63293
|
+
* onto B, which declares nothing.
|
|
63294
|
+
*/
|
|
63295
|
+
function scrubRefusalWording(verdict, loggedExportKey, loggedProducerStack, loggedVia) {
|
|
63296
|
+
const chain = dedupePreservingOrder(loggedVia, "first");
|
|
63297
|
+
const chainRoot = chain[chain.length - 1];
|
|
63298
|
+
const through = chain.length > 1 ? ` (through ${chain.slice(0, -1).map((s) => `'${s}'`).join(", ")})` : "";
|
|
63299
|
+
const templateClaim = verdict.kind === "declared" ? `declares '${loggedExportKey}' from a {{resolve:...}} expression` : verdict.kind === "chained" ? chainRoot !== void 0 ? `publishes '${loggedExportKey}' by RE-EXPORTING a value that '${chainRoot}' declares from a {{resolve:...}} expression${through}` : `publishes '${loggedExportKey}' by RE-EXPORTING a value another stack of this app declares from a {{resolve:...}} expression` : chainRoot !== void 0 ? `publishes at least one output that RE-EXPORTS a value '${chainRoot}' declares from a {{resolve:...}} expression${through} (this run could not match '${loggedExportKey}' to a declared output, so it cannot say which)` : `publishes at least one output from a {{resolve:...}} expression (this run could not match '${loggedExportKey}' to a declared output, so it cannot say which)`;
|
|
63300
|
+
const scrubOrder = dedupePreservingOrder([...chain].reverse().concat(loggedProducerStack), "last");
|
|
63301
|
+
return {
|
|
63302
|
+
templateClaim,
|
|
63303
|
+
remedy: scrubOrder.length > 1 ? `Scrub the producers first, from the head of the chain (${scrubOrder.map((s) => `'cdkd scrub ${s}'`).join(", then ")})` : `Scrub the producer first ('cdkd scrub ${loggedProducerStack}')`
|
|
63304
|
+
};
|
|
63305
|
+
}
|
|
63306
|
+
/**
|
|
63307
|
+
* `values` with repeats removed, keeping either the FIRST or the LAST occurrence
|
|
63308
|
+
* of each and preserving the order of the ones kept.
|
|
63309
|
+
*/
|
|
63310
|
+
function dedupePreservingOrder(values, keep) {
|
|
63311
|
+
const seen = /* @__PURE__ */ new Set();
|
|
63312
|
+
const out = [];
|
|
63313
|
+
const ordered = keep === "last" ? [...values].reverse() : values;
|
|
63314
|
+
for (const value of ordered) {
|
|
63315
|
+
if (seen.has(value)) continue;
|
|
63316
|
+
seen.add(value);
|
|
63317
|
+
out.push(value);
|
|
63318
|
+
}
|
|
63319
|
+
return keep === "last" ? out.reverse() : out;
|
|
63320
|
+
}
|
|
63321
|
+
/**
|
|
63257
63322
|
* The refusal a cross-stack read raises when it SUCCEEDED but the PRODUCER's
|
|
63258
63323
|
* own state record still stores the PLAINTEXT (issue
|
|
63259
63324
|
* [#2133](https://github.com/go-to-k/cdkd/issues/2133) review).
|
|
@@ -63278,9 +63343,14 @@ function unresolvableCrossStackReadError(origin, stackName, intrinsic, path, cau
|
|
|
63278
63343
|
function plaintextProducerCrossStackReadError(origin, stackName, intrinsic, path, producerStack, exportKey, verdict, secrets) {
|
|
63279
63344
|
const loggedExportKey = maskSecretsInText(exportKey, secrets);
|
|
63280
63345
|
const loggedProducerStack = maskSecretsInText(producerStack, secrets);
|
|
63281
|
-
const
|
|
63282
|
-
return new ScrubRefusalError(`Scrub of ${stackName} resolved the ${intrinsic} in ${origin}${path ? ` at ${path}` : ""} to a PLAINTEXT value: the producer stack '${loggedProducerStack}' ${templateClaim}, but its own state still stores the resolved plaintext rather than that expression. scrub has no expression to write in this stack's place, so it cannot redact the imported secret and must not report this stack clean.
|
|
63346
|
+
const { templateClaim, remedy } = scrubRefusalWording(verdict, loggedExportKey, loggedProducerStack, verdict.via.map((s) => maskSecretsInText(s, secrets)));
|
|
63347
|
+
return new ScrubRefusalError(`Scrub of ${stackName} resolved the ${intrinsic} in ${origin}${path ? ` at ${path}` : ""} to a PLAINTEXT value: the producer stack '${loggedProducerStack}' ${templateClaim}, but its own state still stores the resolved plaintext rather than that expression. scrub has no expression to write in this stack's place, so it cannot redact the imported secret and must not report this stack clean. ${remedy}, then re-run 'cdkd scrub ${stackName}' — the read will then return the expression and this stack is scrubbed normally.`, "SCRUB_CROSS_STACK_PRODUCER_PLAINTEXT");
|
|
63283
63348
|
}
|
|
63349
|
+
/** The `no` verdict, shared so the walk allocates nothing for the common case. */
|
|
63350
|
+
const NO_SECRET_EXPRESSION = {
|
|
63351
|
+
kind: "no",
|
|
63352
|
+
via: []
|
|
63353
|
+
};
|
|
63284
63354
|
/**
|
|
63285
63355
|
* Can the producer's STORED value be a legacy plaintext secret at all (issue
|
|
63286
63356
|
* #2133 review)?
|
|
@@ -63332,22 +63402,239 @@ function storedValueCarriesNoPlaintext(stored) {
|
|
|
63332
63402
|
* asserts what the producer declares and only the `declared` verdict has
|
|
63333
63403
|
* checked that for THIS key.
|
|
63334
63404
|
*
|
|
63335
|
-
*
|
|
63336
|
-
*
|
|
63337
|
-
*
|
|
63338
|
-
*
|
|
63339
|
-
*
|
|
63340
|
-
|
|
63341
|
-
|
|
63342
|
-
|
|
63343
|
-
|
|
63344
|
-
|
|
63345
|
-
|
|
63346
|
-
|
|
63405
|
+
* RE-EXPORT CHAINS ARE FOLLOWED (issue
|
|
63406
|
+
* [#2146](https://github.com/go-to-k/cdkd/issues/2146)). A MIDDLE stack whose
|
|
63407
|
+
* output is `{"Fn::ImportValue": "<upstream export>"}` declares no literal
|
|
63408
|
+
* `{{resolve:` of its own, so the single-template test returned `no` for it and
|
|
63409
|
+
* the consumer at the END of the chain was reported CLEAN over its surviving
|
|
63410
|
+
* plaintext — the #2133 silent success, reached through a re-export instead of
|
|
63411
|
+
* a direct import. (`cdkd scrub --all` masks it: `orderScrubTargets` scrubs
|
|
63412
|
+
* producers first, so the middle stack stores the expression by the time the
|
|
63413
|
+
* consumer is reached. `cdkd scrub <one stack>` does not.) This walk therefore
|
|
63414
|
+
* follows a matched output's `Fn::ImportValue` (through the export-name index)
|
|
63415
|
+
* and its `Fn::GetStackOutput` (through the literal `StackName` / `OutputName`)
|
|
63416
|
+
* into the next producer's template, and keeps going until it finds an
|
|
63417
|
+
* expression or runs out of reachable outputs.
|
|
63418
|
+
*
|
|
63419
|
+
* TERMINATION IS A VISITED SET over `(stack, key)` pairs, NOT a hop cap. Each
|
|
63420
|
+
* pair expands deterministically, so a second visit can never yield evidence
|
|
63421
|
+
* the first did not, and the pair space is finite (stacks x outputs) — so the
|
|
63422
|
+
* walk halts on a CYCLE (two stacks re-exporting each other; no deploy can
|
|
63423
|
+
* produce one, a template can express one) while losing no reach. A depth cap
|
|
63424
|
+
* was rejected because it is lossy in the one direction that matters: a chain
|
|
63425
|
+
* longer than the cap would return `no`, which is exactly the silent success
|
|
63426
|
+
* this walk exists to kill.
|
|
63427
|
+
*
|
|
63428
|
+
* WIDENING IS ROOT-ONLY, and that means WHERE THE FALLBACK FIRES, not what a
|
|
63429
|
+
* widened subject may expand into. The all-outputs fallback fires for the DIRECT
|
|
63430
|
+
* producer alone, where the key came from a read and may match nothing because
|
|
63431
|
+
* the producer's `Export.Name` is an intrinsic scrub cannot reproduce. One hop
|
|
63432
|
+
* up the key is a LITERAL export / output name read out of a template, so a miss
|
|
63433
|
+
* there means that template genuinely does not declare it, and widening THERE
|
|
63434
|
+
* would refuse the consumer over an unrelated secret two stacks away that no
|
|
63435
|
+
* scrub could ever clear.
|
|
63436
|
+
*
|
|
63437
|
+
* A widened root's subjects DO still expand into hops, deliberately, and the
|
|
63438
|
+
* trade is stated rather than hidden (issue #2146 review — an earlier revision
|
|
63439
|
+
* of this comment read as if they did not, which contradicted the code). Not
|
|
63440
|
+
* expanding them would make an intrinsic-`Export.Name` producer that re-exports
|
|
63441
|
+
* a secret undetectable — the exact silent success this walk exists to kill,
|
|
63442
|
+
* reintroduced for the one population the widening exists for. Expanding them
|
|
63443
|
+
* extends #2133's accepted over-approximation ("it can refuse a stack that
|
|
63444
|
+
* imports a non-secret export from a producer that has some other secret
|
|
63445
|
+
* output") by one hop, in the direction that repo rule calls safe; and the
|
|
63446
|
+
* refusal it produces now names the head of the chain and the order to scrub it
|
|
63447
|
+
* in, so it is at least as actionable as the root widening it extends.
|
|
63448
|
+
*
|
|
63449
|
+
* Residuals, stated rather than hidden. Each keeps the pre-#2133 outcome for
|
|
63450
|
+
* that one reference — a possible false CLEAN, never a refusal:
|
|
63451
|
+
* - a producer that publishes a secret with NO literal `{{resolve:` and no
|
|
63452
|
+
* followable re-export: one reading it through `Ref` to a parameter, or
|
|
63453
|
+
* re-exporting an export whose producer is outside this app (whose template
|
|
63454
|
+
* scrub never sees), and a producer outside this app at all;
|
|
63455
|
+
* - a NON-LITERAL `Fn::ImportValue` argument (an `Fn::Sub`-assembled export
|
|
63456
|
+
* name) and a NON-LITERAL or foreign `Fn::GetStackOutput` `StackName` /
|
|
63457
|
+
* `OutputName`: the hop cannot be resolved statically, and guessing one would
|
|
63458
|
+
* refuse over a reference the walk never established;
|
|
63459
|
+
* - a `Fn::GetStackOutput` naming a stack of this app in ANOTHER REGION:
|
|
63460
|
+
* {@link buildExportOwnerIndex} and `producerTemplates` are keyed by stack
|
|
63461
|
+
* NAME alone, so the walk reads that name's template regardless of `Region`;
|
|
63462
|
+
* - a secret reachable only through the TRUE branch of an `Fn::If` in a
|
|
63463
|
+
* producer's output: see {@link collectReExportHops}, which takes the FALSE
|
|
63464
|
+
* branch for a condition it cannot evaluate.
|
|
63465
|
+
*/
|
|
63466
|
+
function producerPublishesSecretExpression(templates, exportOwners, producerStack, key) {
|
|
63467
|
+
const seen = /* @__PURE__ */ new Set([`${producerStack}\u0000${key}`]);
|
|
63468
|
+
const queue = [{
|
|
63469
|
+
stack: producerStack,
|
|
63470
|
+
key,
|
|
63471
|
+
via: []
|
|
63472
|
+
}];
|
|
63473
|
+
let widened = false;
|
|
63474
|
+
while (queue.length > 0) {
|
|
63475
|
+
const frame = queue.shift();
|
|
63476
|
+
if (!frame) break;
|
|
63477
|
+
const template = templates.get(frame.stack);
|
|
63478
|
+
if (!template) continue;
|
|
63479
|
+
const outputs = template.Outputs ?? {};
|
|
63480
|
+
const matched = [];
|
|
63481
|
+
for (const [name, output] of Object.entries(outputs)) {
|
|
63482
|
+
const exportName = output?.Export?.Name;
|
|
63483
|
+
if (name === frame.key || typeof exportName === "string" && exportName === frame.key) matched.push(output?.Value);
|
|
63484
|
+
}
|
|
63485
|
+
const isRoot = frame.via.length === 0;
|
|
63486
|
+
let subjects;
|
|
63487
|
+
if (matched.length > 0) subjects = matched;
|
|
63488
|
+
else if (isRoot) {
|
|
63489
|
+
widened = true;
|
|
63490
|
+
subjects = Object.values(outputs).map((o) => o?.Value);
|
|
63491
|
+
} else continue;
|
|
63492
|
+
for (const subject of subjects) {
|
|
63493
|
+
if (JSON.stringify(subject ?? null).includes("{{resolve:")) {
|
|
63494
|
+
if (widened) return {
|
|
63495
|
+
kind: "widened",
|
|
63496
|
+
via: frame.via
|
|
63497
|
+
};
|
|
63498
|
+
return isRoot ? {
|
|
63499
|
+
kind: "declared",
|
|
63500
|
+
via: []
|
|
63501
|
+
} : {
|
|
63502
|
+
kind: "chained",
|
|
63503
|
+
via: frame.via
|
|
63504
|
+
};
|
|
63505
|
+
}
|
|
63506
|
+
for (const hop of collectReExportHops(subject, templates, exportOwners)) {
|
|
63507
|
+
const id = `${hop.stack}\u0000${hop.key}`;
|
|
63508
|
+
if (seen.has(id)) continue;
|
|
63509
|
+
seen.add(id);
|
|
63510
|
+
queue.push({
|
|
63511
|
+
stack: hop.stack,
|
|
63512
|
+
key: hop.key,
|
|
63513
|
+
via: [...frame.via, hop.stack]
|
|
63514
|
+
});
|
|
63515
|
+
}
|
|
63516
|
+
}
|
|
63347
63517
|
}
|
|
63348
|
-
|
|
63349
|
-
|
|
63350
|
-
|
|
63518
|
+
return NO_SECRET_EXPRESSION;
|
|
63519
|
+
}
|
|
63520
|
+
/**
|
|
63521
|
+
* The re-export hops one output VALUE carries (issue #2146).
|
|
63522
|
+
*
|
|
63523
|
+
* Only STATICALLY resolvable references produce a hop, mirroring
|
|
63524
|
+
* {@link inferCrossStackStackDeps}: an `Fn::ImportValue` of a literal export
|
|
63525
|
+
* name (looked up in `exportOwners`) and an `Fn::GetStackOutput` whose
|
|
63526
|
+
* `StackName` and `OutputName` are both literal strings and whose stack is one
|
|
63527
|
+
* of this app's. An assembled name resolves to a hop the walk cannot follow,
|
|
63528
|
+
* which keeps the pre-#2146 outcome for that value rather than guessing.
|
|
63529
|
+
*
|
|
63530
|
+
* EVERY owner of a duplicated export name is returned, not the first (issue
|
|
63531
|
+
* #2146 review). See {@link buildExportOwnerIndex}: picking one silently
|
|
63532
|
+
* decides the verdict on insertion order, and the visited set makes following
|
|
63533
|
+
* all of them cheap and terminating.
|
|
63534
|
+
*
|
|
63535
|
+
* The WHOLE node is walked, not just its top level, because a re-export is
|
|
63536
|
+
* routinely wrapped — `Fn::Join`ing an imported password into a URL is the
|
|
63537
|
+
* shape the live fixture uses.
|
|
63538
|
+
*
|
|
63539
|
+
* `Fn::If` IS NOT WALKED ON BOTH ARMS, and this is load-bearing rather than
|
|
63540
|
+
* tidy (issue #2146 review). `makeCrossStackPrePass` deliberately mirrors
|
|
63541
|
+
* `resolveIf` — selected branch only — so that a conditional import of a
|
|
63542
|
+
* not-yet-deployed producer cannot refuse a whole stack, unbypassably, over a
|
|
63543
|
+
* reference the deploy never read. A hop walk that descended into both arms
|
|
63544
|
+
* broke that mirror and produced something worse: a producer whose output is
|
|
63545
|
+
* `{"Fn::If": ["IsProd", {"Fn::ImportValue": "ProdSecret"}, {"Fn::ImportValue":
|
|
63546
|
+
* "DevPlain"}]}`, deployed in dev with a benign stored value, yielded a
|
|
63547
|
+
* `chained` verdict and a refusal NO `cdkd scrub` can clear — nothing can turn
|
|
63548
|
+
* that producer's non-secret value into an expression, and there is no bypass
|
|
63549
|
+
* flag. On main the same input answered `no`, so it would have been a
|
|
63550
|
+
* regression this change introduced.
|
|
63551
|
+
*
|
|
63552
|
+
* WHICH branch: the FALSE one, unconditionally. `resolveIf` selects FALSE for a
|
|
63553
|
+
* condition it cannot evaluate, and every condition here belongs to ANOTHER
|
|
63554
|
+
* stack — scrub evaluates conditions for the stack it is scrubbing, from that
|
|
63555
|
+
* template's parameter DEFAULTS, and has neither the parameters nor the
|
|
63556
|
+
* evaluation for a producer's. A MALFORMED `Fn::If` (args not a 3-tuple) yields
|
|
63557
|
+
* NO hop at all: which branch is live is unknowable, and the pre-pass's answer
|
|
63558
|
+
* there (walk it with refusals DISARMED) has no counterpart in a function whose
|
|
63559
|
+
* only output is "refuse or not".
|
|
63560
|
+
*
|
|
63561
|
+
* Residual, and it is the direction this file accepts elsewhere: a secret
|
|
63562
|
+
* reachable only through the TRUE branch is not detected, so that reference
|
|
63563
|
+
* keeps its pre-#2133 outcome. The asymmetry with the `{{resolve:` scan in
|
|
63564
|
+
* {@link producerPublishesSecretExpression} — which `JSON.stringify`s the whole
|
|
63565
|
+
* subject and therefore sees BOTH arms — is #2133's behaviour, unchanged here
|
|
63566
|
+
* on purpose: narrowing it would change which stacks that older refusal fires
|
|
63567
|
+
* on, which is not this change's subject.
|
|
63568
|
+
*/
|
|
63569
|
+
function collectReExportHops(value, templates, exportOwners) {
|
|
63570
|
+
const hops = [];
|
|
63571
|
+
const walk = (node) => {
|
|
63572
|
+
if (Array.isArray(node)) {
|
|
63573
|
+
for (const item of node) walk(item);
|
|
63574
|
+
return;
|
|
63575
|
+
}
|
|
63576
|
+
if (node === null || typeof node !== "object") return;
|
|
63577
|
+
const obj = node;
|
|
63578
|
+
if ("Fn::If" in obj) {
|
|
63579
|
+
const args = obj["Fn::If"];
|
|
63580
|
+
if (Array.isArray(args) && args.length === 3) walk(args[2]);
|
|
63581
|
+
return;
|
|
63582
|
+
}
|
|
63583
|
+
const imported = obj["Fn::ImportValue"];
|
|
63584
|
+
if (typeof imported === "string") for (const owner of exportOwners.get(imported) ?? []) hops.push({
|
|
63585
|
+
stack: owner,
|
|
63586
|
+
key: imported
|
|
63587
|
+
});
|
|
63588
|
+
const read = obj["Fn::GetStackOutput"];
|
|
63589
|
+
if (read !== null && typeof read === "object" && !Array.isArray(read)) {
|
|
63590
|
+
const args = read;
|
|
63591
|
+
const stack = args["StackName"];
|
|
63592
|
+
const outputName = args["OutputName"];
|
|
63593
|
+
if (typeof stack === "string" && typeof outputName === "string" && templates.has(stack)) hops.push({
|
|
63594
|
+
stack,
|
|
63595
|
+
key: outputName
|
|
63596
|
+
});
|
|
63597
|
+
}
|
|
63598
|
+
for (const child of Object.values(obj)) walk(child);
|
|
63599
|
+
};
|
|
63600
|
+
walk(value);
|
|
63601
|
+
return hops;
|
|
63602
|
+
}
|
|
63603
|
+
/**
|
|
63604
|
+
* `Export.Name` -> EVERY app stack that declares it (issue #2146), for literal
|
|
63605
|
+
* export names only. Built ONCE per stack scrub rather than per reference,
|
|
63606
|
+
* since {@link producerPublishesSecretExpression} needs it on every cross-stack
|
|
63607
|
+
* read the pre-pass performs.
|
|
63608
|
+
*
|
|
63609
|
+
* ALL OWNERS, not the first (issue #2146 review). `inferCrossStackStackDeps`
|
|
63610
|
+
* keeps one because it is choosing a deploy EDGE and either choice orders the
|
|
63611
|
+
* graph; this index decides whether a value is SECRET-BEARING, where following
|
|
63612
|
+
* the wrong twin returns `no` and reports a stack clean over surviving
|
|
63613
|
+
* plaintext. Two stacks declaring one export name is an AWS-side error, but
|
|
63614
|
+
* scrub reads TEMPLATES — an app that has never been deployed, or one deployed
|
|
63615
|
+
* to two regions, presents it routinely — so there IS a better answer than the
|
|
63616
|
+
* first writer: take the over-approximation and follow both. The visited set
|
|
63617
|
+
* bounds the cost and guarantees termination either way.
|
|
63618
|
+
*
|
|
63619
|
+
* Keyed by stack NAME with no region, which is the residual
|
|
63620
|
+
* {@link producerPublishesSecretExpression} lists: an `Fn::GetStackOutput`
|
|
63621
|
+
* naming this app's stack in another region reads the template of the name,
|
|
63622
|
+
* since `producerTemplates` has no region either.
|
|
63623
|
+
*/
|
|
63624
|
+
function buildExportOwnerIndex(templates) {
|
|
63625
|
+
const owners = /* @__PURE__ */ new Map();
|
|
63626
|
+
for (const [stackName, template] of templates) {
|
|
63627
|
+
const outputs = template.Outputs ?? {};
|
|
63628
|
+
for (const output of Object.values(outputs)) {
|
|
63629
|
+
const name = output?.Export?.Name;
|
|
63630
|
+
if (typeof name !== "string" || name === "") continue;
|
|
63631
|
+
const existing = owners.get(name);
|
|
63632
|
+
if (existing) {
|
|
63633
|
+
if (!existing.includes(stackName)) existing.push(stackName);
|
|
63634
|
+
} else owners.set(name, [stackName]);
|
|
63635
|
+
}
|
|
63636
|
+
}
|
|
63637
|
+
return owners;
|
|
63351
63638
|
}
|
|
63352
63639
|
/**
|
|
63353
63640
|
* Build the CROSS-STACK pre-pass for ONE stack's scrub (issue
|
|
@@ -63433,6 +63720,17 @@ function producerPublishesSecretExpression(template, key) {
|
|
|
63433
63720
|
*/
|
|
63434
63721
|
function makeCrossStackPrePass(deps) {
|
|
63435
63722
|
const { stackName, resolver, producerTemplates, findings, logger } = deps;
|
|
63723
|
+
const exportOwners = buildExportOwnerIndex(producerTemplates);
|
|
63724
|
+
const verdicts = /* @__PURE__ */ new Map();
|
|
63725
|
+
const secretExpressionVerdict = (stack, key) => {
|
|
63726
|
+
const id = `${stack}\u0000${key}`;
|
|
63727
|
+
let verdict = verdicts.get(id);
|
|
63728
|
+
if (!verdict) {
|
|
63729
|
+
verdict = producerPublishesSecretExpression(producerTemplates, exportOwners, stack, key);
|
|
63730
|
+
verdicts.set(id, verdict);
|
|
63731
|
+
}
|
|
63732
|
+
return verdict;
|
|
63733
|
+
};
|
|
63436
63734
|
/**
|
|
63437
63735
|
* Is this a refusal NO RE-RUN can clear (issue #2133 review)?
|
|
63438
63736
|
*
|
|
@@ -63494,8 +63792,8 @@ function makeCrossStackPrePass(deps) {
|
|
|
63494
63792
|
* wrong-account lookup that refusal exists to prevent.
|
|
63495
63793
|
*
|
|
63496
63794
|
* The OUTCOME is the same today either way — a cross-account producer is
|
|
63497
|
-
* never in `appStacks`, so the
|
|
63498
|
-
* regardless — but a future change that widened `producerTemplates` would
|
|
63795
|
+
* never in `appStacks`, so the `producerTemplates.has(...)` return below
|
|
63796
|
+
* fires first regardless — but a future change that widened `producerTemplates` would
|
|
63499
63797
|
* be relying on this sentence, so it must not claim the second arm has no
|
|
63500
63798
|
* expression behind it.
|
|
63501
63799
|
*
|
|
@@ -63569,10 +63867,9 @@ function makeCrossStackPrePass(deps) {
|
|
|
63569
63867
|
}
|
|
63570
63868
|
const producer = recordedProducer(key, probe);
|
|
63571
63869
|
if (!producer) return;
|
|
63572
|
-
|
|
63573
|
-
|
|
63574
|
-
|
|
63575
|
-
if (verdict === "no") return;
|
|
63870
|
+
if (!producerTemplates.has(producer.stack)) return;
|
|
63871
|
+
const verdict = secretExpressionVerdict(producer.stack, producer.key);
|
|
63872
|
+
if (verdict.kind === "no") return;
|
|
63576
63873
|
const stored = await storedProducerValue(producer, context.stateBackend);
|
|
63577
63874
|
if (!stored) return;
|
|
63578
63875
|
if (carriesDynamicReference(stored.stored)) return;
|
|
@@ -85753,7 +86050,7 @@ function createMigrateCommand() {
|
|
|
85753
86050
|
*/
|
|
85754
86051
|
function buildProgram() {
|
|
85755
86052
|
const program = new Command();
|
|
85756
|
-
program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.284.
|
|
86053
|
+
program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.284.34");
|
|
85757
86054
|
program.hook("preAction", (_thisCommand, actionCommand) => {
|
|
85758
86055
|
const { profile } = actionCommand.optsWithGlobals();
|
|
85759
86056
|
if (profile !== void 0) process.env["AWS_PROFILE"] = profile;
|