@go-to-k/cdkd 0.283.18 → 0.283.20
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/README.md +5 -0
- package/dist/{asg-provider-D8Ln3-ij.js → asg-provider-k-TEX_Ej.js} +2 -2
- package/dist/{asg-provider-D8Ln3-ij.js.map → asg-provider-k-TEX_Ej.js.map} +1 -1
- package/dist/cli.js +358 -16
- package/dist/cli.js.map +1 -1
- package/dist/{deploy-engine-B-0iA5XV.js → deploy-engine-ISOKkuun.js} +331 -39
- package/dist/deploy-engine-ISOKkuun.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/dist/deploy-engine-B-0iA5XV.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { $ as coerceCfnBoolean, $t as
|
|
2
|
+
import { $ as coerceCfnBoolean, $t as resolveCaptureObservedState, A as cyan, An as LocalStartServiceError, At as rewriteTemplateAssetReferences, B as findSilentDropProperties, Bt as buildDockerImage, C as makeCanonicalizePropertiesFn, Ct as shouldRetainResource, D as renderStatefulReason, Dn as DeployCancelledError, Dt as buildAssetRedirectMap, E as isStatefulRecreateTargetSync, Et as WorkGraph, F as IAMRoleProvider, Fn as ProvisioningError, Ft as getBootstrapMarkerKey, G as IntrinsicFunctionResolver, Gn as withErrorHandling, Gt as AssetManifestLoader, H as slowCcOperationTimeoutMs, Ht as getDockerCmd, I as collectInlinePolicyNamesManagedBySiblings, In as ResourceTimeoutError, It as parseBootstrapMarker, J as refStateLookupFromResource, Jn as isThrottlingError, Jt as synthesisStatusMessage, K as cfnRefValueFromPhysicalId, Kn as isMarkedNonRetryable, Kt as getDockerImageBySourceHash, L as clearOnUpdateRemoval, Ln as ResourceUpdateNotSupportedError, Lt as validateAssetBucketName, M as green, Mn as MissingCdkCliError, Mt as AssetModeResolver, N as red, Nn as NestedStackChildDirectDestroyError, Nt as BOOTSTRAP_MARKER_PREFIX, O as formatResourceLine, On as LocalInvokeBuildError$1, Ot as createAssetRedirectResolver, P as yellow, Pn as PartialFailureError, Pt as ensureAssetStorage, Q as assertRegionMatch, Qt as resolveAutoAssetStorage, R as ProviderRegistry, Rn as StackHasActiveImportsError, Rt as validateContainerRepoName, S as unsupportedFinalSnapshotError, Sn as setAwsClients, St as rebuildClientForBucketRegion, T as MULTI_REGION_RECREATE_BLOCKED_TYPES, Tt as stringifyValue, U as disableInstanceApiTermination, Ut as runDockerForeground, V as CloudControlProvider, Vt as formatDockerLoginError, W as isTerminationProtectionPropagationError, Wn as normalizeAwsError, Wt as runDockerStreaming, X as normalizeAwsTagsToCfn, Xt as getLegacyStateBucketName, Y as WAFv2WebACLProvider, Yn as markNonRetryable, Yt as getDefaultStateBucketName, Z as resolveExplicitPhysicalId, Zt as resolveApp, _ as buildFinalSnapshotIdentifier, _t as withRetry, a as DeploymentEventsStore, an as warnDeprecatedNoPrefixCliFlag, at as requireConfigObject, b as isFinalSnapshotError, bn as getAwsClients, bt as LockManager, c as replayFailedOperations, cn as MIGRATE_TMP_PREFIX, ct as scrubResourceRecord, d as deleteSkipReason, dn as expectedOwnerParam, dt as s3BucketDualStackDomainName, en as resolveSkipPrefix, et as configBooleanRefusal, f as withResourceDeadline, fn as PARTITION_TABLE, ft as s3BucketRegionalDomainName, g as PRE_DELETE_SNAPSHOT_TYPES, gn as processStackMessages, gt as describeTypeWithThrottleRetry, h as ATOMIC_FINAL_SNAPSHOT_TYPES, hn as AssemblyReader, ht as DiffCalculator, i as DeploymentEventsReader, in as stateBucketExistenceConfirmed, it as requireConfigArray, j as gray, jt as escapeRegExp, k as bold, kn as LocalMigrateError, kt as loadPublishableAssetManifest, l as replayRollback, ln as findLargeInlineResources, lt as s3BucketArn, m as computeImplicitDeleteEdges, mn as derivePartitionAndUrlSuffix, mt as applyRoleArnIfSet, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nn as resolveStateBucketWithDefaultAndSource, nt as readConfigString, o as planFailedOps, on as CFN_TEMPLATE_BODY_LIMIT, ot as requireConfigString, p as IMPLICIT_DELETE_DEPENDENCIES, pn as canonicalizeRegion, pt as s3BucketWebsiteUrl, q as getAccountInfo, qn as isRetryableTransientError, qt as Synthesizer, r as DeployEngine, rn as resolveUseCdkBootstrapAssets, rt as replayWarn, s as planRollback, sn as CFN_TEMPLATE_URL_LIMIT, st as redactSecretsForState, t as DEFAULT_RESOURCE_TIMEOUT_MS, tn as resolveStateBucketWithDefault, tt as configStringRefusal, u as UNSPECIFIED_SKIP_REASON, un as uploadCfnTemplate, ut as s3BucketDomainName, v as ccRoutedFinalSnapshotError, vn as resolveBucketRegion, vt as DagBuilder, w as extractDeploymentEventError, wn as CdkdError, wt as AssetPublisher, x as refusesFinalSnapshot, xn as resetAwsClients, xt as S3StateBackend, y as createPreDeleteFinalSnapshot, yn as AwsClients, yt as TemplateParser, z as findActionableSilentDrops, zn as StackTerminationProtectionError, zt as buildDenyExternalAccessPolicy } from "./deploy-engine-ISOKkuun.js";
|
|
3
3
|
import { a as getLiveRenderer, c as PATTERN_B_RESOURCE_TYPES, d as generateResourceNameWithFallback, 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-k-TEX_Ej.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";
|
|
@@ -38111,6 +38111,58 @@ var Route53Provider = class {
|
|
|
38111
38111
|
};
|
|
38112
38112
|
}
|
|
38113
38113
|
/**
|
|
38114
|
+
* The attribute set for a hosted zone adopted through the NAME-lookup
|
|
38115
|
+
* branch (issue #1875). `null` means DECLINE the row.
|
|
38116
|
+
*
|
|
38117
|
+
* Unlike the `knownPhysicalId` branch — whose verification call already
|
|
38118
|
+
* carries the delegation set — this branch resolves the zone through
|
|
38119
|
+
* `ListHostedZonesByName`, which does NOT return one, so the attributes
|
|
38120
|
+
* cost one extra `GetHostedZone`.
|
|
38121
|
+
*
|
|
38122
|
+
* **A vanished zone DECLINES, it does not degrade.** `NoSuchHostedZone`
|
|
38123
|
+
* between the list and this read is exactly the condition the
|
|
38124
|
+
* `knownPhysicalId` branch answers with `null` / `skipped-not-found`, and
|
|
38125
|
+
* adopting a zone that demonstrably no longer exists is worse than
|
|
38126
|
+
* declining it — the next deploy would compare against a record for
|
|
38127
|
+
* something AWS does not have. The two branches must agree; everything
|
|
38128
|
+
* else degrades.
|
|
38129
|
+
*
|
|
38130
|
+
* **The degraded answer is the EMPTY map, and that is load-bearing rather
|
|
38131
|
+
* than tidy.** `import.ts`'s attribute carry-over is gated on the returned
|
|
38132
|
+
* map being NON-empty (`row.attributes && Object.keys(...).length > 0 ?
|
|
38133
|
+
* row.attributes : undefined`, then `?? priorAttributes ?? {}`) — a gate
|
|
38134
|
+
* that exists precisely because almost every provider spells
|
|
38135
|
+
* `attributes: {}` explicitly. So a partial `{ Id }` is NON-empty, takes
|
|
38136
|
+
* the row branch, and OVERWRITES a previously-recorded good map: a zone
|
|
38137
|
+
* imported successfully once and re-imported while `route53:GetHostedZone`
|
|
38138
|
+
* is denied would LOSE its stored `NameServers`, leaving state strictly
|
|
38139
|
+
* worse than before this fix. `{}` falls through and preserves it. Do NOT
|
|
38140
|
+
* "improve" this into keeping what it can — that is the one change that
|
|
38141
|
+
* makes the failure path destructive.
|
|
38142
|
+
*
|
|
38143
|
+
* NEVER throws otherwise, matching the convention the neighbouring
|
|
38144
|
+
* adoption paths already use (`AppSyncProvider.childImportAttributes`,
|
|
38145
|
+
* issue #1728, and `importRecordSet`'s canonicalization, which adopts
|
|
38146
|
+
* VERBATIM rather than declining): an import must not hard-fail because
|
|
38147
|
+
* one optional attribute could not be read. `import.ts` only aborts when
|
|
38148
|
+
* ZERO resources import, so under `--migrate-from-cloudformation` a throw
|
|
38149
|
+
* here would cost the row at exactly the moment the CloudFormation stack
|
|
38150
|
+
* is being retired — the resource would end up in NEITHER CloudFormation
|
|
38151
|
+
* nor cdkd state.
|
|
38152
|
+
*/
|
|
38153
|
+
async hostedZoneImportAttributes(zoneId, logicalId) {
|
|
38154
|
+
try {
|
|
38155
|
+
return {
|
|
38156
|
+
Id: zoneId,
|
|
38157
|
+
NameServers: (await this.getClient().send(new GetHostedZoneCommand({ Id: zoneId }))).DelegationSet?.NameServers ?? []
|
|
38158
|
+
};
|
|
38159
|
+
} catch (error) {
|
|
38160
|
+
if (error instanceof Error && error.name === "NoSuchHostedZone") return null;
|
|
38161
|
+
this.logger.warn(`Imported hosted zone ${zoneId} but could not read its NameServers: ${error instanceof Error ? error.message : String(error)}. The zone is adopted without them (any attributes already in state for this zone are kept). Re-run \`cdkd import --resource ${logicalId}=${zoneId} --force\` once the permission is granted, or make a template change that forces an UPDATE — a plain \`cdkd deploy\` does NOT heal the record, because an unchanged zone is NO_CHANGE and never calls update().`);
|
|
38162
|
+
return {};
|
|
38163
|
+
}
|
|
38164
|
+
}
|
|
38165
|
+
/**
|
|
38114
38166
|
* Adopt an existing hosted zone (issue #1702).
|
|
38115
38167
|
*
|
|
38116
38168
|
* An explicit `--resource` id is ground truth and is only VERIFIED. With no
|
|
@@ -38132,13 +38184,44 @@ var Route53Provider = class {
|
|
|
38132
38184
|
* (`skipped-not-found`, the next deploy creates the zone) while an
|
|
38133
38185
|
* ambiguous name or a failed lookup throws, which `importOne` records as
|
|
38134
38186
|
* `failed` for this row alone.
|
|
38187
|
+
*
|
|
38188
|
+
* BOTH branches record the same `{ Id, NameServers }` attribute set a
|
|
38189
|
+
* `cdkd deploy` CREATE records (issue #1875). Recording `attributes: {}`
|
|
38190
|
+
* left an adopted zone with no `NameServers` in state, and nothing else
|
|
38191
|
+
* rescues it: `IntrinsicFunctionResolver.resolveGetAtt`'s flat-attribute
|
|
38192
|
+
* branch misses (so PR #1868's legacy comma-string normalization, which
|
|
38193
|
+
* lives in that branch, never runs either), `constructAttribute` has no
|
|
38194
|
+
* `AWS::Route53::HostedZone` case, and resolution falls through to
|
|
38195
|
+
* `guardedPhysicalIdFallback`, which hands back the zone id STRING — so a
|
|
38196
|
+
* downstream `Fn::Join` threw `second argument must be a list` and the
|
|
38197
|
+
* Output was silently dropped. `provider.getAttribute` does return the list
|
|
38198
|
+
* but is only reached from the orphan-rewriter path, never from ordinary
|
|
38199
|
+
* template resolution.
|
|
38200
|
+
*
|
|
38201
|
+
* The delegation set is normalized `?? []` — a LIST, never a comma string —
|
|
38202
|
+
* exactly as `createHostedZone` / `updateHostedZone` / `getHostedZoneAttribute`
|
|
38203
|
+
* do, so an imported zone and a deployed one resolve IDENTICALLY. A
|
|
38204
|
+
* divergence here would be a phantom-drift hazard as well as a resolution
|
|
38205
|
+
* one.
|
|
38206
|
+
*
|
|
38207
|
+
* **A plain `cdkd deploy` does NOT rewrite these attributes**, so do not
|
|
38208
|
+
* offer one as the remedy when a read fails. `deploy-engine.ts` `continue`s
|
|
38209
|
+
* on `NO_CHANGE`, so an unchanged zone never reaches `update()` and its
|
|
38210
|
+
* `attributes` are never rewritten; `kickOffAutoRefreshObservedProperties`
|
|
38211
|
+
* refreshes `observedProperties`, a different field. What DOES rewrite the
|
|
38212
|
+
* record is re-running the import for that row
|
|
38213
|
+
* (`cdkd import --resource <logicalId>=<zoneId> --force`, which re-enters
|
|
38214
|
+
* this method) or a template change that forces a real UPDATE.
|
|
38135
38215
|
*/
|
|
38136
38216
|
async importHostedZone(input) {
|
|
38137
38217
|
if (input.knownPhysicalId) try {
|
|
38138
|
-
await this.getClient().send(new GetHostedZoneCommand({ Id: input.knownPhysicalId }));
|
|
38218
|
+
const response = await this.getClient().send(new GetHostedZoneCommand({ Id: input.knownPhysicalId }));
|
|
38139
38219
|
return {
|
|
38140
38220
|
physicalId: input.knownPhysicalId,
|
|
38141
|
-
attributes: {
|
|
38221
|
+
attributes: {
|
|
38222
|
+
Id: input.knownPhysicalId.replace("/hostedzone/", ""),
|
|
38223
|
+
NameServers: response.DelegationSet?.NameServers ?? []
|
|
38224
|
+
}
|
|
38142
38225
|
};
|
|
38143
38226
|
} catch (err) {
|
|
38144
38227
|
if (err instanceof Error && err.name === "NoSuchHostedZone") return null;
|
|
@@ -38147,16 +38230,19 @@ var Route53Provider = class {
|
|
|
38147
38230
|
const zoneName = input.properties["Name"];
|
|
38148
38231
|
if (typeof zoneName !== "string" || !zoneName) return null;
|
|
38149
38232
|
try {
|
|
38233
|
+
const zoneId = await this.resolveHostedZoneId({ HostedZoneName: zoneName }, input.logicalId, input.resourceType, void 0, {
|
|
38234
|
+
requireUnambiguousZoneName: true,
|
|
38235
|
+
requirePrivateZone: templateZoneVisibility(input.properties),
|
|
38236
|
+
subject: {
|
|
38237
|
+
noun: "hosted zone",
|
|
38238
|
+
remedy: `pass --resource ${input.logicalId}=<hostedZoneId> to adopt it explicitly`
|
|
38239
|
+
}
|
|
38240
|
+
});
|
|
38241
|
+
const attributes = await this.hostedZoneImportAttributes(zoneId, input.logicalId);
|
|
38242
|
+
if (attributes === null) return null;
|
|
38150
38243
|
return {
|
|
38151
|
-
physicalId:
|
|
38152
|
-
|
|
38153
|
-
requirePrivateZone: templateZoneVisibility(input.properties),
|
|
38154
|
-
subject: {
|
|
38155
|
-
noun: "hosted zone",
|
|
38156
|
-
remedy: `pass --resource ${input.logicalId}=<hostedZoneId> to adopt it explicitly`
|
|
38157
|
-
}
|
|
38158
|
-
}),
|
|
38159
|
-
attributes: {}
|
|
38244
|
+
physicalId: zoneId,
|
|
38245
|
+
attributes
|
|
38160
38246
|
};
|
|
38161
38247
|
} catch (error) {
|
|
38162
38248
|
if (error instanceof HostedZoneNameNotFoundError) return null;
|
|
@@ -57660,7 +57746,8 @@ async function computeStackDiff(currentState, template, region, stackName, state
|
|
|
57660
57746
|
stackName,
|
|
57661
57747
|
bestEffort: true,
|
|
57662
57748
|
...mergedParameters && { parameters: mergedParameters },
|
|
57663
|
-
...conditions && { conditions }
|
|
57749
|
+
...conditions && { conditions },
|
|
57750
|
+
skipDynamicReferences: true
|
|
57664
57751
|
});
|
|
57665
57752
|
return diffCalculator.calculateDiff(currentState, effectiveTemplate, resolveFn, canonicalizeProperties);
|
|
57666
57753
|
}
|
|
@@ -58177,6 +58264,254 @@ function createDiffCommand() {
|
|
|
58177
58264
|
return cmd;
|
|
58178
58265
|
}
|
|
58179
58266
|
|
|
58267
|
+
//#endregion
|
|
58268
|
+
//#region src/cli/commands/scrub.ts
|
|
58269
|
+
/**
|
|
58270
|
+
* Signals `cdkd scrub` found stacks whose state still holds plaintext secrets.
|
|
58271
|
+
* Only thrown under `--dry-run --fail`; carries no message (the plan was
|
|
58272
|
+
* already printed) and maps to a non-zero exit so CI can gate on it.
|
|
58273
|
+
*/
|
|
58274
|
+
var ScrubNeededError = class ScrubNeededError extends CdkdError {
|
|
58275
|
+
silent = true;
|
|
58276
|
+
constructor() {
|
|
58277
|
+
super("scrub needed", "SCRUB_NEEDED");
|
|
58278
|
+
this.name = "ScrubNeededError";
|
|
58279
|
+
Object.setPrototypeOf(this, ScrubNeededError.prototype);
|
|
58280
|
+
}
|
|
58281
|
+
};
|
|
58282
|
+
/**
|
|
58283
|
+
* `cdkd scrub` — rewrite persisted state so any resolved secret dynamic
|
|
58284
|
+
* reference (`{{resolve:secretsmanager:...}}`) is stored as its UNRESOLVED
|
|
58285
|
+
* expression rather than the plaintext value (GHSA fix).
|
|
58286
|
+
*
|
|
58287
|
+
* A normal `cdkd deploy` already scrubs state as a side effect (the deploy
|
|
58288
|
+
* engine redacts every persisted bag), so this command is for cleaning up
|
|
58289
|
+
* existing state WITHOUT a redeploy — e.g. after upgrading cdkd on a stack you
|
|
58290
|
+
* do not want to re-provision right now.
|
|
58291
|
+
*
|
|
58292
|
+
* It needs the CDK app (`--app`) because a state file records the RESOLVED
|
|
58293
|
+
* plaintext with no marker of which values are secrets: only the template
|
|
58294
|
+
* carries the `{{resolve:...}}` expressions. So scrub synthesizes the template,
|
|
58295
|
+
* re-resolves each resource's properties to learn the resolved secret VALUES
|
|
58296
|
+
* (recorded, never printed or re-persisted), and replaces those values in the
|
|
58297
|
+
* state record's `properties` / `attributes` / `observedProperties` with the
|
|
58298
|
+
* expression. No AWS resource is created, updated, or deleted; only state.json
|
|
58299
|
+
* is rewritten. This is why it is a top-level command and not `cdkd state
|
|
58300
|
+
* scrub` — the `cdkd state ...` family operates on the state bucket alone and
|
|
58301
|
+
* deliberately needs no CDK code.
|
|
58302
|
+
*
|
|
58303
|
+
* IMPORTANT: scrubbing does not un-expose an already-leaked secret. A value
|
|
58304
|
+
* that was stored in plaintext should be treated as compromised and ROTATED in
|
|
58305
|
+
* Secrets Manager; scrub only stops it from being re-read out of state going
|
|
58306
|
+
* forward.
|
|
58307
|
+
*
|
|
58308
|
+
* ORDERING: scrub matches the CURRENT resolved secret value against what state
|
|
58309
|
+
* holds, so run it BEFORE rotating. Once the secret is rotated, the value in
|
|
58310
|
+
* state no longer matches the current one and scrub cannot find it (it reports
|
|
58311
|
+
* "nothing to scrub"). A rotated-away stale value in state is invalidated by
|
|
58312
|
+
* the rotation, but to remove it, redeploy the stack (which rewrites the record
|
|
58313
|
+
* with the expression).
|
|
58314
|
+
*/
|
|
58315
|
+
async function scrubCommand(stacks, options) {
|
|
58316
|
+
const logger = getLogger();
|
|
58317
|
+
if (options.verbose) logger.setLevel("debug");
|
|
58318
|
+
warnIfDeprecatedRegion(options);
|
|
58319
|
+
await applyRoleArnIfSet({
|
|
58320
|
+
roleArn: options.roleArn,
|
|
58321
|
+
region: options.region
|
|
58322
|
+
});
|
|
58323
|
+
const app = resolveApp(options.app);
|
|
58324
|
+
if (!app) throw new Error("CDK app is required (scrub needs the template to identify secret references). Pass --app, set CDKD_APP, or add \"app\" to cdk.json.");
|
|
58325
|
+
const region = options.region || process.env["AWS_REGION"] || "us-east-1";
|
|
58326
|
+
const stateBucket = await resolveStateBucketWithDefault(options.stateBucket, region);
|
|
58327
|
+
setAwsClients(new AwsClients({
|
|
58328
|
+
...options.region && { region: options.region },
|
|
58329
|
+
...options.profile && { profile: options.profile }
|
|
58330
|
+
}));
|
|
58331
|
+
logger.info(synthesisStatusMessage(app, "Synthesizing CDK app..."));
|
|
58332
|
+
const synthesizer = new Synthesizer();
|
|
58333
|
+
const context = parseContextOptions(options.context);
|
|
58334
|
+
const synthOptions = {
|
|
58335
|
+
app,
|
|
58336
|
+
output: options.output,
|
|
58337
|
+
...options.region && { region: options.region },
|
|
58338
|
+
...options.profile && { profile: options.profile },
|
|
58339
|
+
...Object.keys(context).length > 0 && { context },
|
|
58340
|
+
stateBucket,
|
|
58341
|
+
deferMacroExpansion: true
|
|
58342
|
+
};
|
|
58343
|
+
const allStacks = (await synthesizer.synthesize(synthOptions)).stacks;
|
|
58344
|
+
const stackPatterns = stacks.length > 0 ? stacks : options.stack ? [options.stack] : [];
|
|
58345
|
+
let targetStacks;
|
|
58346
|
+
if (options.all) targetStacks = allStacks;
|
|
58347
|
+
else if (stackPatterns.length > 0) targetStacks = matchStacks(allStacks, stackPatterns);
|
|
58348
|
+
else if (allStacks.length === 1) targetStacks = allStacks;
|
|
58349
|
+
else throw new Error(`Multiple stacks found: ${allStacks.map(describeStack).join(", ")}. Specify stack name(s) or use --all`);
|
|
58350
|
+
if (targetStacks.length === 0) throw new Error("No stacks matched.");
|
|
58351
|
+
await synthesizer.expandMacrosForStacks(targetStacks, synthOptions);
|
|
58352
|
+
const stateConfig = {
|
|
58353
|
+
bucket: stateBucket,
|
|
58354
|
+
prefix: options.statePrefix
|
|
58355
|
+
};
|
|
58356
|
+
const stateS3 = new AwsClients({
|
|
58357
|
+
region,
|
|
58358
|
+
...options.profile && { profile: options.profile }
|
|
58359
|
+
});
|
|
58360
|
+
const stateBackend = new S3StateBackend(stateS3.s3, stateConfig, {
|
|
58361
|
+
region,
|
|
58362
|
+
...options.profile && { profile: options.profile }
|
|
58363
|
+
});
|
|
58364
|
+
const lockManager = new LockManager(stateS3.s3, stateConfig);
|
|
58365
|
+
let totalStacksScrubbed = 0;
|
|
58366
|
+
for (const stack of targetStacks) {
|
|
58367
|
+
const scrubbed = await scrubStack(stack, stack.region || region, stateBackend, lockManager, {
|
|
58368
|
+
dryRun: options.dryRun ?? false,
|
|
58369
|
+
roleArn: options.roleArn,
|
|
58370
|
+
logger
|
|
58371
|
+
});
|
|
58372
|
+
if (scrubbed.recordsChanged > 0) {
|
|
58373
|
+
totalStacksScrubbed++;
|
|
58374
|
+
logger.info(`${options.dryRun ? "Would scrub" : "Scrubbed"} ${scrubbed.recordsChanged} resource record(s) in ${stack.stackName}`);
|
|
58375
|
+
} else logger.info(`No plaintext secrets found in ${stack.stackName}`);
|
|
58376
|
+
}
|
|
58377
|
+
if (totalStacksScrubbed === 0) {
|
|
58378
|
+
logger.info("\nNo plaintext secrets found in any target stack state. Nothing to scrub.");
|
|
58379
|
+
return;
|
|
58380
|
+
}
|
|
58381
|
+
if (options.dryRun) {
|
|
58382
|
+
logger.info(`\nPlan: ${totalStacksScrubbed} stack(s) hold plaintext secrets and would be scrubbed (--dry-run, no state written). ROTATE any exposed secret in Secrets Manager.`);
|
|
58383
|
+
if (options.fail) throw new ScrubNeededError();
|
|
58384
|
+
return;
|
|
58385
|
+
}
|
|
58386
|
+
logger.info(`\nDone: scrubbed ${totalStacksScrubbed} stack(s). The plaintext is no longer stored, but a value that was ever persisted should be treated as compromised — ROTATE it in Secrets Manager (scrub matches the current value, so scrub BEFORE rotating).`);
|
|
58387
|
+
}
|
|
58388
|
+
/**
|
|
58389
|
+
* Scrub one stack's state. Re-resolves the template's per-resource properties to
|
|
58390
|
+
* learn the resolved secret VALUES, then replaces those values in the state
|
|
58391
|
+
* record with their `{{resolve:...}}` expressions. Returns counts; performs no
|
|
58392
|
+
* AWS mutation. Acquires the stack lock for the read-modify-write unless
|
|
58393
|
+
* `dryRun`.
|
|
58394
|
+
*/
|
|
58395
|
+
async function scrubStack(stack, region, stateBackend, lockManager, opts) {
|
|
58396
|
+
const { logger } = opts;
|
|
58397
|
+
const acquired = !opts.dryRun;
|
|
58398
|
+
if (acquired) await lockManager.acquireLockWithRetry(stack.stackName, region, void 0, "scrub");
|
|
58399
|
+
try {
|
|
58400
|
+
const loaded = await stateBackend.getState(stack.stackName, region);
|
|
58401
|
+
if (!loaded) {
|
|
58402
|
+
logger.debug(`No state for ${stack.stackName} (${region}) — skipping`);
|
|
58403
|
+
return {
|
|
58404
|
+
recordsChanged: 0,
|
|
58405
|
+
secretsFound: 0
|
|
58406
|
+
};
|
|
58407
|
+
}
|
|
58408
|
+
const state = loaded.state;
|
|
58409
|
+
const perResourceSecrets = /* @__PURE__ */ new Map();
|
|
58410
|
+
const outputSecrets = /* @__PURE__ */ new Map();
|
|
58411
|
+
const resolver = new IntrinsicFunctionResolver(region);
|
|
58412
|
+
let parameters = {};
|
|
58413
|
+
let conditions = {};
|
|
58414
|
+
try {
|
|
58415
|
+
parameters = await resolver.resolveParameters(stack.template);
|
|
58416
|
+
} catch (err) {
|
|
58417
|
+
logger.debug(`Parameter resolution skipped for ${stack.stackName}: ${err instanceof Error ? err.message : String(err)}`);
|
|
58418
|
+
}
|
|
58419
|
+
try {
|
|
58420
|
+
conditions = await resolver.evaluateConditions({
|
|
58421
|
+
template: stack.template,
|
|
58422
|
+
resources: state.resources,
|
|
58423
|
+
...Object.keys(parameters).length > 0 && { parameters },
|
|
58424
|
+
bestEffort: true
|
|
58425
|
+
});
|
|
58426
|
+
} catch (err) {
|
|
58427
|
+
logger.debug(`Condition evaluation skipped for ${stack.stackName}: ${err instanceof Error ? err.message : String(err)}`);
|
|
58428
|
+
}
|
|
58429
|
+
const templateResources = stack.template.Resources ?? {};
|
|
58430
|
+
for (const logicalId of Object.keys(state.resources)) {
|
|
58431
|
+
const templateResource = templateResources[logicalId];
|
|
58432
|
+
if (!templateResource?.Properties) continue;
|
|
58433
|
+
const recordedSecretValues = /* @__PURE__ */ new Map();
|
|
58434
|
+
try {
|
|
58435
|
+
await resolver.resolve(templateResource.Properties, {
|
|
58436
|
+
template: stack.template,
|
|
58437
|
+
resources: state.resources,
|
|
58438
|
+
...Object.keys(parameters).length > 0 && { parameters },
|
|
58439
|
+
...Object.keys(conditions).length > 0 && { conditions },
|
|
58440
|
+
stackName: stack.stackName,
|
|
58441
|
+
recordedSecretValues,
|
|
58442
|
+
bestEffort: true
|
|
58443
|
+
});
|
|
58444
|
+
} catch (err) {
|
|
58445
|
+
logger.debug(`Resolution of ${logicalId} during scrub was partial: ${err instanceof Error ? err.message : String(err)}`);
|
|
58446
|
+
}
|
|
58447
|
+
if (recordedSecretValues.size > 0) perResourceSecrets.set(logicalId, recordedSecretValues);
|
|
58448
|
+
}
|
|
58449
|
+
const templateOutputs = stack.template.Outputs ?? {};
|
|
58450
|
+
for (const [name, output] of Object.entries(templateOutputs)) {
|
|
58451
|
+
const value = output.Value;
|
|
58452
|
+
if (value === void 0) continue;
|
|
58453
|
+
try {
|
|
58454
|
+
await resolver.resolve(value, {
|
|
58455
|
+
template: stack.template,
|
|
58456
|
+
resources: state.resources,
|
|
58457
|
+
...Object.keys(parameters).length > 0 && { parameters },
|
|
58458
|
+
...Object.keys(conditions).length > 0 && { conditions },
|
|
58459
|
+
stackName: stack.stackName,
|
|
58460
|
+
recordedSecretValues: outputSecrets,
|
|
58461
|
+
bestEffort: true
|
|
58462
|
+
});
|
|
58463
|
+
} catch (err) {
|
|
58464
|
+
logger.debug(`Resolution of output ${name} during scrub was partial: ${err instanceof Error ? err.message : String(err)}`);
|
|
58465
|
+
}
|
|
58466
|
+
}
|
|
58467
|
+
const totalSecrets = outputSecrets.size + [...perResourceSecrets.values()].reduce((n, m) => n + m.size, 0);
|
|
58468
|
+
if (totalSecrets === 0) return {
|
|
58469
|
+
recordsChanged: 0,
|
|
58470
|
+
secretsFound: 0
|
|
58471
|
+
};
|
|
58472
|
+
let recordsChanged = 0;
|
|
58473
|
+
const newResources = {};
|
|
58474
|
+
for (const [logicalId, record] of Object.entries(state.resources)) {
|
|
58475
|
+
const secrets = perResourceSecrets.get(logicalId);
|
|
58476
|
+
const scrubbed = secrets ? scrubResourceRecord(record, secrets) : record;
|
|
58477
|
+
if (JSON.stringify(scrubbed) !== JSON.stringify(record)) recordsChanged++;
|
|
58478
|
+
newResources[logicalId] = scrubbed;
|
|
58479
|
+
}
|
|
58480
|
+
const newOutputs = outputSecrets.size > 0 ? redactSecretsForState(state.outputs, outputSecrets) : state.outputs;
|
|
58481
|
+
if (JSON.stringify(newOutputs) !== JSON.stringify(state.outputs)) recordsChanged++;
|
|
58482
|
+
if (recordsChanged > 0 && !opts.dryRun) {
|
|
58483
|
+
const nextState = {
|
|
58484
|
+
...state,
|
|
58485
|
+
resources: newResources,
|
|
58486
|
+
outputs: newOutputs,
|
|
58487
|
+
lastModified: Date.now()
|
|
58488
|
+
};
|
|
58489
|
+
await stateBackend.saveState(stack.stackName, region, nextState, { expectedEtag: loaded.etag });
|
|
58490
|
+
}
|
|
58491
|
+
return {
|
|
58492
|
+
recordsChanged,
|
|
58493
|
+
secretsFound: totalSecrets
|
|
58494
|
+
};
|
|
58495
|
+
} finally {
|
|
58496
|
+
if (acquired) await lockManager.releaseLock(stack.stackName, region).catch((err) => {
|
|
58497
|
+
logger.warn(`Failed to release lock for ${stack.stackName}: ${err instanceof Error ? err.message : String(err)}`);
|
|
58498
|
+
});
|
|
58499
|
+
}
|
|
58500
|
+
}
|
|
58501
|
+
function createScrubCommand() {
|
|
58502
|
+
const cmd = new Command("scrub").description("Rewrite persisted state so resolved secret dynamic references are stored as their {{resolve:...}} expression, not the plaintext value (no deploy).").argument("[stacks...]", "Stack name(s) to scrub (physical name or display path)").option("--all", "Scrub every stack in the synthesized app", false).option("--dry-run", "Report what would be scrubbed without writing state").option("--fail", "With --dry-run, exit non-zero if any plaintext secret is found (CI gate)");
|
|
58503
|
+
[
|
|
58504
|
+
...commonOptions,
|
|
58505
|
+
...appOptions,
|
|
58506
|
+
...stateOptions,
|
|
58507
|
+
...stackOptions,
|
|
58508
|
+
...contextOptions
|
|
58509
|
+
].forEach((opt) => cmd.addOption(opt));
|
|
58510
|
+
cmd.addOption(deprecatedRegionOption);
|
|
58511
|
+
cmd.action(withErrorHandling(scrubCommand));
|
|
58512
|
+
return cmd;
|
|
58513
|
+
}
|
|
58514
|
+
|
|
58180
58515
|
//#endregion
|
|
58181
58516
|
//#region src/analyzer/drift-normalize.ts
|
|
58182
58517
|
/**
|
|
@@ -58538,6 +58873,7 @@ function isIgnoredPath(path, ignorePaths) {
|
|
|
58538
58873
|
*/
|
|
58539
58874
|
function diffAt(path, stateValue, awsValue, out, ignorePaths, unionWalkObjects) {
|
|
58540
58875
|
if (deepEqual(stateValue, awsValue)) return;
|
|
58876
|
+
if (typeof stateValue === "string" && stateValue.includes("{{resolve:")) return;
|
|
58541
58877
|
if (isPlainObject(stateValue) && isPlainObject(awsValue) && !Array.isArray(stateValue) && !Array.isArray(awsValue)) {
|
|
58542
58878
|
const keys = unionWalkObjects ? /* @__PURE__ */ new Set([...Object.keys(stateValue), ...Object.keys(awsValue)]) : Object.keys(stateValue);
|
|
58543
58879
|
for (const key of keys) {
|
|
@@ -68569,7 +68905,12 @@ async function resolveImportedProperties(stackState, template, region, stateBack
|
|
|
68569
68905
|
stackName: stackState.stackName
|
|
68570
68906
|
};
|
|
68571
68907
|
for (const [logicalId, resource] of entries) try {
|
|
68572
|
-
|
|
68908
|
+
const recordedSecretValues = /* @__PURE__ */ new Map();
|
|
68909
|
+
const resolved = await resolver.resolve(resource.properties ?? {}, {
|
|
68910
|
+
...baseContext,
|
|
68911
|
+
recordedSecretValues
|
|
68912
|
+
});
|
|
68913
|
+
resource.properties = recordedSecretValues.size > 0 ? redactSecretsForState(resolved, recordedSecretValues) : resolved;
|
|
68573
68914
|
} catch (err) {
|
|
68574
68915
|
logger.warn(`Failed to resolve intrinsics in Properties for imported resource '${logicalId}' (${resource.resourceType}): ${err instanceof Error ? err.message : String(err)}. State will be written with the raw intrinsic shape, which may cause 'cdkd destroy' to fail on this resource — re-import once every referenced sibling is in state, or remove this resource via 'cdkd state orphan'.`);
|
|
68575
68916
|
}
|
|
@@ -78728,7 +79069,7 @@ function createMigrateCommand() {
|
|
|
78728
79069
|
*/
|
|
78729
79070
|
function buildProgram() {
|
|
78730
79071
|
const program = new Command();
|
|
78731
|
-
program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.283.
|
|
79072
|
+
program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.283.20");
|
|
78732
79073
|
program.hook("preAction", (_thisCommand, actionCommand) => {
|
|
78733
79074
|
const { profile } = actionCommand.optsWithGlobals();
|
|
78734
79075
|
if (profile !== void 0) process.env["AWS_PROFILE"] = profile;
|
|
@@ -78741,6 +79082,7 @@ function buildProgram() {
|
|
|
78741
79082
|
program.addCommand(createDriftCommand());
|
|
78742
79083
|
program.addCommand(createDestroyCommand());
|
|
78743
79084
|
program.addCommand(createRollbackCommand());
|
|
79085
|
+
program.addCommand(createScrubCommand());
|
|
78744
79086
|
program.addCommand(createEventsCommand());
|
|
78745
79087
|
program.addCommand(createGcCommand());
|
|
78746
79088
|
program.addCommand(createOrphanCommand());
|