@go-to-k/cdkd 0.288.4 → 0.288.5
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-TexsJufw.js → asg-provider-C_AHAmgD.js} +2 -2
- package/dist/{asg-provider-TexsJufw.js.map → asg-provider-C_AHAmgD.js.map} +1 -1
- package/dist/cli.js +2 -2
- package/dist/{deploy-engine-BiPxTgKT.js → deploy-engine-Dsf9bTqH.js} +574 -139
- package/dist/deploy-engine-Dsf9bTqH.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/{program-Ci4Y5UO9.js → program-CMhpftan.js} +470 -35
- package/dist/{program-Ci4Y5UO9.js.map → program-CMhpftan.js.map} +1 -1
- package/dist/{version-BmM4QJ9t.js → version-BFmq8S23.js} +2 -2
- package/dist/{version-BmM4QJ9t.js.map → version-BFmq8S23.js.map} +1 -1
- package/package.json +1 -1
- package/dist/deploy-engine-BiPxTgKT.js.map +0 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { _ as withStackName, d as applyDefaultNameForFallback, f as generateResourceName, h as looksLikeCdkdGeneratedName, m as getCurrentStackName, n as getLogger, p as generateResourceNameWithFallback, r as isStdoutReservedForPayload, s as getLiveRenderer } from "./logger-Dw-3y48G.js";
|
|
2
2
|
import { t as awsClientDefaults } from "./aws-client-defaults-D-iYiIJ6.js";
|
|
3
|
-
import { t as getCdkdVersion } from "./version-
|
|
3
|
+
import { t as getCdkdVersion } from "./version-BFmq8S23.js";
|
|
4
4
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
5
5
|
import { createHash, randomUUID } from "node:crypto";
|
|
6
6
|
import { CreateBucketCommand, DeleteObjectCommand, DeleteObjectsCommand, GetBucketLocationCommand, GetBucketReplicationCommand, GetObjectCommand, HeadBucketCommand, HeadObjectCommand, ListObjectVersionsCommand, ListObjectsV2Command, NoSuchKey, PutBucketEncryptionCommand, PutBucketPolicyCommand, PutObjectCommand, PutPublicAccessBlockCommand, S3Client, S3ServiceException } from "@aws-sdk/client-s3";
|
|
@@ -102,6 +102,7 @@ const IAM_PROPAGATION_ERROR_MESSAGE_PATTERNS = [
|
|
|
102
102
|
"required permissions for: ENHANCED_MONITORING",
|
|
103
103
|
"Caught ServiceAccessDeniedException",
|
|
104
104
|
"permissions required to assume the role",
|
|
105
|
+
"does not have a trust relationship allowing",
|
|
105
106
|
"authorized to assume the provided role",
|
|
106
107
|
"not authorized to access the Log Destination",
|
|
107
108
|
"Cannot access stream",
|
|
@@ -12257,13 +12258,45 @@ const TEMPLATE_SOURCED_RULES = {
|
|
|
12257
12258
|
trustAnyExpression: false,
|
|
12258
12259
|
sourceIsSameGeneration: false
|
|
12259
12260
|
};
|
|
12260
|
-
/**
|
|
12261
|
+
/**
|
|
12262
|
+
* An AWS readback projected from THIS record's own persisted STATE bag.
|
|
12263
|
+
*
|
|
12264
|
+
* Does NOT fail closed. This is the constant a caller reaches for when it knows
|
|
12265
|
+
* the SHAPE of its two bags and nothing about where the result lands —
|
|
12266
|
+
* `cdkd drift`'s two writers and `cdkd import`'s capture pass it, and so does
|
|
12267
|
+
* `DeployEngine.redactOperationsForJournal` for the journal's `previousState`
|
|
12268
|
+
* (issue [#2886](https://github.com/go-to-k/cdkd/issues/2886): a REPLAYED
|
|
12269
|
+
* baseline must not gain masks a rollback restore then persists). See
|
|
12270
|
+
* {@link STATE_SOURCED_BASELINE_RULES} for the one that does, and
|
|
12271
|
+
* `failClosedOnUncertifiedPositions` for why the difference is declared rather
|
|
12272
|
+
* than derived.
|
|
12273
|
+
*/
|
|
12261
12274
|
const STATE_SOURCED_READBACK_RULES = {
|
|
12262
12275
|
descendArrays: false,
|
|
12263
12276
|
trustAnyExpression: true,
|
|
12264
12277
|
sourceIsSameGeneration: true
|
|
12265
12278
|
};
|
|
12266
12279
|
/**
|
|
12280
|
+
* {@link STATE_SOURCED_READBACK_RULES} for a caller that KNOWS its bag becomes
|
|
12281
|
+
* a drift BASELINE — `observedProperties` and nothing else (issue
|
|
12282
|
+
* [#2852](https://github.com/go-to-k/cdkd/issues/2852)).
|
|
12283
|
+
*
|
|
12284
|
+
* Identical on the three shape flags, so every relaxation and refusal the
|
|
12285
|
+
* readback path already had applies unchanged; the only difference is that a
|
|
12286
|
+
* position this pass cannot certify is written as {@link SECRET_MASK} rather
|
|
12287
|
+
* than as the DECRYPTED readback. Passed by `cdkd state refresh-observed` and
|
|
12288
|
+
* derived by {@link scrubResourceRecord} for the observed bag — the deploy's
|
|
12289
|
+
* `drainObservedCaptures` baseline reaches it that way. `cdkd import`'s own
|
|
12290
|
+
* capture still passes the non-failing constant and keeps the residue; moving
|
|
12291
|
+
* it is a one-constant change in `src/cli/commands/import.ts`.
|
|
12292
|
+
*/
|
|
12293
|
+
const STATE_SOURCED_BASELINE_RULES = {
|
|
12294
|
+
descendArrays: false,
|
|
12295
|
+
trustAnyExpression: true,
|
|
12296
|
+
sourceIsSameGeneration: true,
|
|
12297
|
+
failClosedOnUncertifiedPositions: true
|
|
12298
|
+
};
|
|
12299
|
+
/**
|
|
12267
12300
|
* A STATE source that is no longer this bag's own generation — `cdkd scrub`'s
|
|
12268
12301
|
* `observedProperties` walk (issue #1917 review).
|
|
12269
12302
|
*
|
|
@@ -12409,7 +12442,7 @@ function isSecretExpressionByVerdictOrSpelling(expression) {
|
|
|
12409
12442
|
* so the position source is present with no map beside it.
|
|
12410
12443
|
*
|
|
12411
12444
|
* `cdkd state refresh-observed` and the deploy's `drainObservedCaptures` are
|
|
12412
|
-
* NOT affected: they take `
|
|
12445
|
+
* NOT affected: they take a `STATE_SOURCED_*` readback constant, which sets
|
|
12413
12446
|
* `sourceIsSameGeneration`, so {@link refuseUncertifiedReadbackPositions}
|
|
12414
12447
|
* restores the source even under the old strict class.
|
|
12415
12448
|
*
|
|
@@ -13324,7 +13357,7 @@ function redactByPath(bag, source, secrets, rules, secretExpressions, bagIsSameG
|
|
|
13324
13357
|
}
|
|
13325
13358
|
if (rules.descendArrays && bag.length === source.length) return bag.map((item, i) => redactByPath(item, source[i], secrets, rules, secretExpressions, bagIsSameGeneration));
|
|
13326
13359
|
}
|
|
13327
|
-
if (isPlainObject$2(bag) && isPlainObject$2(source)) {
|
|
13360
|
+
if (isPlainObject$2(bag) && hasPlainPrototype(bag) && isPlainObject$2(source)) {
|
|
13328
13361
|
const out = Object.create(null);
|
|
13329
13362
|
for (const [k, v] of Object.entries(bag)) out[k] = Object.hasOwn(source, k) ? redactByPath(v, source[k], secrets, rules, secretExpressions, bagIsSameGeneration) : redactSecretsForState(v, secrets);
|
|
13330
13363
|
return out;
|
|
@@ -13335,7 +13368,10 @@ function redactByPath(bag, source, secrets, rules, secretExpressions, bagIsSameG
|
|
|
13335
13368
|
* Is this rules constant one whose BAG is an AWS readback and whose SOURCE is a
|
|
13336
13369
|
* persisted STATE bag?
|
|
13337
13370
|
*
|
|
13338
|
-
* Today that is {@link STATE_SOURCED_READBACK_RULES}
|
|
13371
|
+
* Today that is {@link STATE_SOURCED_READBACK_RULES} and its fail-closed twin
|
|
13372
|
+
* {@link STATE_SOURCED_BASELINE_RULES}, which differ on nothing this predicate
|
|
13373
|
+
* reads (issue #2852 added a DESTINATION flag, not a shape one): the path where
|
|
13374
|
+
* the
|
|
13339
13375
|
* secrets map can be EMPTY by construction (nothing was resolved), so the value
|
|
13340
13376
|
* scan has no needles and POSITION is the only mechanism left. Derived from the
|
|
13341
13377
|
* flags rather than compared against the constant so a future one with the same
|
|
@@ -13831,6 +13867,23 @@ function unkeyedArrayPairsByAnchors(bag, source) {
|
|
|
13831
13867
|
*/
|
|
13832
13868
|
const POSITION_DECIDED = Symbol("position decided by a position pass");
|
|
13833
13869
|
/**
|
|
13870
|
+
* Marks a STRING leaf {@link refuseUncertifiedReadbackPositions} REFUSED — a
|
|
13871
|
+
* position whose source subtree proves a dynamic reference lives there while
|
|
13872
|
+
* the walk could not pair the two sides, so the readback value at it may be a
|
|
13873
|
+
* decrypted secret (issue
|
|
13874
|
+
* [#2852](https://github.com/go-to-k/cdkd/issues/2852)).
|
|
13875
|
+
*
|
|
13876
|
+
* A THIRD state, not a second spelling of {@link POSITION_DECIDED}, and the
|
|
13877
|
+
* difference is what keeps the fail-closed change from REGRESSING the derived
|
|
13878
|
+
* needles issue #2012 added. `POSITION_DECIDED` tells
|
|
13879
|
+
* {@link preferPositionDecisions} "this leaf is mine, the scan may not touch
|
|
13880
|
+
* it"; a refusal makes the opposite claim — the pass has NO answer here, only
|
|
13881
|
+
* the knowledge that the raw value is unsafe. So the scan still gets to win at
|
|
13882
|
+
* such a leaf (a derived needle NAMES the expression, which is strictly better
|
|
13883
|
+
* than a mask), and the mask stands only where nothing else spoke.
|
|
13884
|
+
*/
|
|
13885
|
+
const POSITION_UNCERTIFIED = Symbol("position refused by a position pass");
|
|
13886
|
+
/**
|
|
13834
13887
|
* Record one (plaintext -> expression) pair, or strike the plaintext out.
|
|
13835
13888
|
*
|
|
13836
13889
|
* Below {@link MIN_NEEDLE_LENGTH} nothing is recorded, and this floor DECIDES
|
|
@@ -14092,11 +14145,13 @@ function asIndex(marks, index) {
|
|
|
14092
14145
|
* newly extending #2427 to the EMPTY-map path, where the unchanged-resource
|
|
14093
14146
|
* `drainObservedCaptures` baseline lives and where `cdkd drift --revert` pushes
|
|
14094
14147
|
* the result to the live resource. With the guard a non-plain leaf falls
|
|
14095
|
-
* through to `refused`. That is the position passes' own answer
|
|
14096
|
-
* bag by identity
|
|
14097
|
-
*
|
|
14098
|
-
*
|
|
14099
|
-
*
|
|
14148
|
+
* through to `refused`. That is the position passes' own answer, which is the
|
|
14149
|
+
* bag by identity: their object arm carried no prototype guard of its own until
|
|
14150
|
+
* issue [#2869](https://github.com/go-to-k/cdkd/issues/2869), so a non-plain
|
|
14151
|
+
* leaf whose source subtree carries a reference WAS already flattened one
|
|
14152
|
+
* function earlier and this guard could only keep a `{}` intact. Both halves
|
|
14153
|
+
* are guarded now; the remaining copy of the defect is the VALUE scan's own
|
|
14154
|
+
* walk, which is issue #2427 and a different pass.
|
|
14100
14155
|
*
|
|
14101
14156
|
* The net effect is byte-identical to the FIRST ordering on every input where
|
|
14102
14157
|
* the un-certification did not fire — which is the whole point: it keeps that
|
|
@@ -14110,7 +14165,9 @@ function preferPositionDecisions(scanned, refused, bag, marks, inferred) {
|
|
|
14110
14165
|
}
|
|
14111
14166
|
if (Array.isArray(bag) && Array.isArray(refused) && Array.isArray(scanned) && refused.length === bag.length && scanned.length === bag.length) return refused.map((item, i) => preferPositionDecisions(scanned[i], item, bag[i], asIndex(marks, i), inferred));
|
|
14112
14167
|
if (typeof bag !== "string" || marks === POSITION_DECIDED) return refused;
|
|
14113
|
-
|
|
14168
|
+
const scanDecision = scanned === bag ? inferred.get(bag) ?? scanned : scanned;
|
|
14169
|
+
if (marks === POSITION_UNCERTIFIED && !(typeof scanDecision === "string" && isSingleDynamicReferenceToken(scanDecision))) return refused;
|
|
14170
|
+
return scanDecision;
|
|
14114
14171
|
}
|
|
14115
14172
|
/**
|
|
14116
14173
|
* DERIVED NEEDLES (issue [#2012](https://github.com/go-to-k/cdkd/issues/2012)):
|
|
@@ -14180,7 +14237,7 @@ function deriveReadbackNeedles(bag, source, secrets, rules) {
|
|
|
14180
14237
|
poisoned: /* @__PURE__ */ new Set(),
|
|
14181
14238
|
inferred: /* @__PURE__ */ new Set()
|
|
14182
14239
|
};
|
|
14183
|
-
refuseUncertifiedReadbackPositions(bag, source, secrets, collector);
|
|
14240
|
+
refuseUncertifiedReadbackPositions(bag, source, secrets, false, collector);
|
|
14184
14241
|
if (collector.needles.size === 0) return void 0;
|
|
14185
14242
|
const certain = /* @__PURE__ */ new Map();
|
|
14186
14243
|
const inferred = /* @__PURE__ */ new Map();
|
|
@@ -14191,6 +14248,149 @@ function deriveReadbackNeedles(bag, source, secrets, rules) {
|
|
|
14191
14248
|
};
|
|
14192
14249
|
}
|
|
14193
14250
|
/**
|
|
14251
|
+
* FAIL CLOSED over one readback subtree the position walk could not certify
|
|
14252
|
+
* (issue [#2852](https://github.com/go-to-k/cdkd/issues/2852)).
|
|
14253
|
+
*
|
|
14254
|
+
* Every STRING leaf the source cannot account for becomes
|
|
14255
|
+
* {@link SECRET_MASK} — at EVERY position it occupies, including a node the bag
|
|
14256
|
+
* reaches twice; see the memo below. So does a BINARY leaf, whose bytes are a
|
|
14257
|
+
* secret in the clear once `JSON.stringify` writes them. Everything else is
|
|
14258
|
+
* kept. Called from
|
|
14259
|
+
* {@link refuseUncertifiedReadbackPositions} — through
|
|
14260
|
+
* {@link refuseAgainstSource}, and DIRECTLY from its keyed-array arm, which
|
|
14261
|
+
* hoists the literal set and re-spells the `failClosed` test — and only where
|
|
14262
|
+
* that walk has already established
|
|
14263
|
+
* BOTH halves of the evidence: the SOURCE subtree at this position spells a
|
|
14264
|
+
* dynamic reference (so the template says a secret lives here), and the two
|
|
14265
|
+
* sides cannot be paired (so no position can say WHICH leaf holds its resolved
|
|
14266
|
+
* form). Before this, every such branch returned the bag — the decrypted
|
|
14267
|
+
* readback — verbatim.
|
|
14268
|
+
*
|
|
14269
|
+
* "The source cannot account for" is the whole claim, and it is deliberately
|
|
14270
|
+
* WEAKER than "no plaintext survives": a leaf the source spells verbatim is
|
|
14271
|
+
* kept, so a readback that echoes a template literal back keeps it. What the
|
|
14272
|
+
* pass guarantees is that no leaf survives on the strength of the walk having
|
|
14273
|
+
* given up.
|
|
14274
|
+
*
|
|
14275
|
+
* WHY A MASK RATHER THAN THE SOURCE. Substituting the source is what the
|
|
14276
|
+
* certified rows do, and it is exactly what the array arm's own comment (and
|
|
14277
|
+
* the issue #1915 fences) refuse here: with no pairing, writing the source
|
|
14278
|
+
* fabricates baseline content AWS never reported, which `cdkd drift --revert`
|
|
14279
|
+
* then pushes to the live resource. A mask fabricates no content — it keeps
|
|
14280
|
+
* the bag's SHAPE, adds no key, no element and no scalar-over-container — and
|
|
14281
|
+
* `SECRET_MASK` is already a first-class persisted state with its own
|
|
14282
|
+
* downstream guards (`drift.ts`'s `collectSecretMaskPaths` /
|
|
14283
|
+
* `preserveLiveValuesAtMaskedLeaves`, `runAccept`'s refusal,
|
|
14284
|
+
* `rollback-executor.ts`'s `refuseMaskedReplayBaseline`), because the
|
|
14285
|
+
* mask-only channel (issue #2274) already puts one there.
|
|
14286
|
+
*
|
|
14287
|
+
* WHY STRINGS ONLY. A recorded secret is a `string` by the type of
|
|
14288
|
+
* {@link RecordedSecretValues}, so a number, a boolean or `null` cannot BE a
|
|
14289
|
+
* resolved secret and masking one would only cost drift a comparison. A
|
|
14290
|
+
* NON-PLAIN object (a `Date` an AWS SDK readback carries, a `Buffer`) is
|
|
14291
|
+
* returned BY IDENTITY for the same reason plus a second one: rebuilding it
|
|
14292
|
+
* from its own enumerable keys yields `{}` — the corruption of issue
|
|
14293
|
+
* [#2869](https://github.com/go-to-k/cdkd/issues/2869).
|
|
14294
|
+
*
|
|
14295
|
+
* A leaf that IS a whole `{{resolve:...}}` token is kept: it is an expression
|
|
14296
|
+
* AWS echoed back unresolved, not plaintext, and replacing it with a mask would
|
|
14297
|
+
* DESTROY a value `cdkd drift` can re-resolve. WHOLE, not "contains one" — that
|
|
14298
|
+
* wider test spared `postgres://admin:<plaintext>@{{resolve:ssm-secure:/h}}`,
|
|
14299
|
+
* where the embedded token vouched for a leaf that was mostly the decrypted
|
|
14300
|
+
* secret. The residual is the issue #1917 shape — a plaintext that merely LOOKS
|
|
14301
|
+
* like a token — which every arm of this module already trusts.
|
|
14302
|
+
*
|
|
14303
|
+
* SO IS A LEAF THE SOURCE SUBTREE ITSELF SPELLS, and this is what keeps the
|
|
14304
|
+
* fail-closed change from emptying an ordinary drift baseline. `sourceLiterals`
|
|
14305
|
+
* is {@link wholeStringLeavesOf} over the SOURCE at the refused position — the
|
|
14306
|
+
* literal frame of an `Fn::Join`, the anchor values of an array AWS reordered,
|
|
14307
|
+
* every ordinary property beside the reference. A value the template SPELLS is
|
|
14308
|
+
* not the resolved form of a reference, so masking it buys nothing; and where
|
|
14309
|
+
* it coincides with one, that plaintext is already sitting in the record's own
|
|
14310
|
+
* `properties`, so the copy in the readback is not the disclosure. Scoped to
|
|
14311
|
+
* the SOURCE AT THE REFUSED POSITION rather than the whole record on purpose: a
|
|
14312
|
+
* coincidence three properties away is not evidence about this one. Read that
|
|
14313
|
+
* literally — when an ARRAY refuses element by element the refused position is
|
|
14314
|
+
* the array, so a SIBLING element's literal does spare a leaf. That is the
|
|
14315
|
+
* intended granularity (the elements are peers of one list AWS returned
|
|
14316
|
+
* together, and the pairing that failed is between the two LISTS), and it is
|
|
14317
|
+
* stated because "subtree" reads narrower than the code is.
|
|
14318
|
+
*
|
|
14319
|
+
* OVER-MASKING IS THE REMAINING COST AND IT IS THE INTENDED DIRECTION: a value
|
|
14320
|
+
* AWS NORMALISED (`us-east-1` returned as `US-EAST-1`) no longer matches the
|
|
14321
|
+
* source and is masked with the secret, because nothing distinguishes them once
|
|
14322
|
+
* the pairing is gone. That is phantom drift rather than a disclosure — the
|
|
14323
|
+
* same way this module chooses to be wrong at
|
|
14324
|
+
* {@link mixedLeafMayCarryPublicReference}.
|
|
14325
|
+
*
|
|
14326
|
+
* `mark` is {@link refuseUncertifiedReadbackPositions}'s MARK MODE, threaded
|
|
14327
|
+
* so the parallel tree keeps the same shape: {@link POSITION_UNCERTIFIED}
|
|
14328
|
+
* lands wherever the substituting pass puts a mask, and the bag's own value
|
|
14329
|
+
* everywhere else — which is that mode's contract.
|
|
14330
|
+
*/
|
|
14331
|
+
function refuseUncertifiedSubtree(value, sourceLiterals, mark, seen = /* @__PURE__ */ new Map()) {
|
|
14332
|
+
if (typeof value === "string") {
|
|
14333
|
+
if (isSingleDynamicReferenceToken(value) || sourceLiterals.has(value)) return value;
|
|
14334
|
+
if (value === "") return value;
|
|
14335
|
+
return mark ? POSITION_UNCERTIFIED : "***";
|
|
14336
|
+
}
|
|
14337
|
+
if (value === null || typeof value !== "object") return value;
|
|
14338
|
+
const memo = seen.get(value);
|
|
14339
|
+
if (memo !== void 0) return memo;
|
|
14340
|
+
if (Array.isArray(value)) {
|
|
14341
|
+
const out = [];
|
|
14342
|
+
seen.set(value, out);
|
|
14343
|
+
for (const item of value) out.push(refuseUncertifiedSubtree(item, sourceLiterals, mark, seen));
|
|
14344
|
+
return out;
|
|
14345
|
+
}
|
|
14346
|
+
if (isPlainObject$2(value) && hasPlainPrototype(value)) {
|
|
14347
|
+
const out = Object.create(null);
|
|
14348
|
+
seen.set(value, out);
|
|
14349
|
+
for (const [k, v] of Object.entries(value)) out[k] = refuseUncertifiedSubtree(v, sourceLiterals, mark, seen);
|
|
14350
|
+
return out;
|
|
14351
|
+
}
|
|
14352
|
+
if (ArrayBuffer.isView(value)) return mark ? POSITION_UNCERTIFIED : "***";
|
|
14353
|
+
seen.set(value, value);
|
|
14354
|
+
return value;
|
|
14355
|
+
}
|
|
14356
|
+
/**
|
|
14357
|
+
* {@link refuseUncertifiedSubtree} over a bag whose SOURCE is in hand, so the
|
|
14358
|
+
* literal set can never be built from anything but the source at the SAME
|
|
14359
|
+
* position.
|
|
14360
|
+
*
|
|
14361
|
+
* NOT the only spelling, and an earlier revision of this sentence said it was.
|
|
14362
|
+
* The keyed-array arm calls {@link refuseUncertifiedSubtree} DIRECTLY, because
|
|
14363
|
+
* it hoists the literal set out of its `bag.map` — so it also re-spells the
|
|
14364
|
+
* `failClosed` test this function owns. A future edit that drops that
|
|
14365
|
+
* re-spelling drops the DESTINATION check with it, which is why the two are
|
|
14366
|
+
* named here rather than left to be noticed.
|
|
14367
|
+
*/
|
|
14368
|
+
function refuseAgainstSource(bag, source, failClosed, mark) {
|
|
14369
|
+
if (failClosed !== true) return bag;
|
|
14370
|
+
return refuseUncertifiedSubtree(bag, wholeStringLeavesOf(source), mark);
|
|
14371
|
+
}
|
|
14372
|
+
/**
|
|
14373
|
+
* Does any element of `source` that the bag did NOT pair carry a dynamic
|
|
14374
|
+
* reference (issue [#2852](https://github.com/go-to-k/cdkd/issues/2852))?
|
|
14375
|
+
*
|
|
14376
|
+
* The evidence that licenses refusing the bag's own unpaired elements in the
|
|
14377
|
+
* IDENTITY-KEYED array arm. An identity key that does not round-trip
|
|
14378
|
+
* byte-identically — AWS case-normalises a `Name`, or expands one to an ARN —
|
|
14379
|
+
* drops its element to `partner === undefined`, and the element the source
|
|
14380
|
+
* spells as a reference is then left over with nothing pointing at it, so its
|
|
14381
|
+
* resolved plaintext is somewhere in the unpaired remainder.
|
|
14382
|
+
*
|
|
14383
|
+
* The converse is why this is a QUESTION rather than a blanket refusal: when
|
|
14384
|
+
* every reference-bearing source element DID find its partner, an extra bag
|
|
14385
|
+
* element is a peer AWS added (another `Environment` entry) and carries no
|
|
14386
|
+
* secret this source can account for. Refusing those would mask ordinary
|
|
14387
|
+
* readback content for no evidence, which is the same trade the object arm's
|
|
14388
|
+
* extra-KEY branch declines to make.
|
|
14389
|
+
*/
|
|
14390
|
+
function unpairedSourceCarriesReference(source, key, bagIdentities) {
|
|
14391
|
+
return source.some((item) => !bagIdentities.has(item[key]) && subtreeHasDynamicReference(item));
|
|
14392
|
+
}
|
|
14393
|
+
/**
|
|
14194
14394
|
* Refuse to persist a readback leaf the path pass could not CERTIFY, at any
|
|
14195
14395
|
* position the STATE source proves is secret-bearing (issue #1926 review).
|
|
14196
14396
|
*
|
|
@@ -14212,15 +14412,42 @@ function deriveReadbackNeedles(bag, source, secrets, rules) {
|
|
|
14212
14412
|
* ...the same MIXED leaf inside a PAIRED element LEAK take source
|
|
14213
14413
|
* `['--pw', '{{resolve:...}}']` (no identity key) LEAK take source*
|
|
14214
14414
|
* `[{Field, Val: '{{resolve:...}}'}]` (no `Name`) LEAK take source*
|
|
14215
|
-
* ...either of those, but REORDERED / normalised LEAK
|
|
14216
|
-
* an UNPAIRED element
|
|
14217
|
-
* an
|
|
14415
|
+
* ...either of those, but REORDERED / normalised LEAK MASK (#2852)
|
|
14416
|
+
* an UNPAIRED element, source reference left over LEAK needle | MASK
|
|
14417
|
+
* an UNPAIRED element, every source reference paired LEAK needle (#2012)
|
|
14418
|
+
* a RESHAPED container / added wrapper level LEAK MASK (#2852)
|
|
14419
|
+
* a source leaf promoted to a container LEAK MASK (#2852)
|
|
14420
|
+
* a RAW `Fn::Join` source vs a STRING readback LEAK MASK (#2846)
|
|
14421
|
+
* an observed KEY the source does not carry LEAK needle | LEAK
|
|
14422
|
+
* a `Date` under a reference-bearing source subtree `{}` kept (#2869)
|
|
14218
14423
|
* whole `{{resolve:...}}` token ok ok
|
|
14219
14424
|
* `Environment[]` keyed by `Name` (issue #1915) ok ok
|
|
14220
14425
|
* PUBLIC ssm MIXED leaf, POPULATED map ok ok
|
|
14221
14426
|
* PUBLIC ssm MIXED leaf, EMPTY map ok over-redacts
|
|
14222
14427
|
* ```
|
|
14223
14428
|
*
|
|
14429
|
+
* MASK rows are the FAIL-CLOSED change of issue
|
|
14430
|
+
* [#2852](https://github.com/go-to-k/cdkd/issues/2852). Every branch this walk
|
|
14431
|
+
* could not certify used to `return bag` — the decrypted readback, verbatim —
|
|
14432
|
+
* so "cannot pair" and "safe to persist" were the same answer. They are now
|
|
14433
|
+
* {@link refuseUncertifiedSubtree}, whose doc argues why a mask rather than the
|
|
14434
|
+
* source and why STRING leaves only. `needle | MASK` means the derived needles
|
|
14435
|
+
* of issue #2012 are consulted FIRST and the mask stands only where they had
|
|
14436
|
+
* nothing to say ({@link preferPositionDecisions}), so no row this table
|
|
14437
|
+
* previously closed by a needle is taken back.
|
|
14438
|
+
*
|
|
14439
|
+
* ONE row is deliberately still open: an observed KEY the source does not carry
|
|
14440
|
+
* keeps the plaintext when no needle names it. Refusing there needs evidence
|
|
14441
|
+
* that does not exist — the source has NO leaf at that position, so the walk
|
|
14442
|
+
* would be guessing — and the cost of guessing is not bounded: a write-only
|
|
14443
|
+
* credential AWS never echoes back (RDS `MasterUserPassword` and every
|
|
14444
|
+
* `getDriftUnknownPaths` sibling) leaves a reference-bearing source key
|
|
14445
|
+
* unpaired on EVERY readback, so keying the refusal on that would mask
|
|
14446
|
+
* `Runtime` / `FunctionArn` / `LastModified` for every secret-bearing resource
|
|
14447
|
+
* in the account. The extra-KEY asymmetry stated further down is the same
|
|
14448
|
+
* argument; issue [#2868](https://github.com/go-to-k/cdkd/issues/2868) owns the
|
|
14449
|
+
* shape where the plaintext has no counterpart in the source at all.
|
|
14450
|
+
*
|
|
14224
14451
|
* The last row is the price of the row above it and is tracked as issue
|
|
14225
14452
|
* [#2036](https://github.com/go-to-k/cdkd/issues/2036): with no map nothing was
|
|
14226
14453
|
* resolved, so nothing distinguishes a public parameter from a `SecureString`
|
|
@@ -14254,7 +14481,7 @@ function deriveReadbackNeedles(bag, source, secrets, rules) {
|
|
|
14254
14481
|
* {@link anchorsCorroboratePairing} answers only one of them and its own doc
|
|
14255
14482
|
* says nothing in it is sufficient alone.
|
|
14256
14483
|
*
|
|
14257
|
-
* The
|
|
14484
|
+
* The MASK rows are one root cause, not several: no needle and no
|
|
14258
14485
|
* position, so nothing distinguishes a resolved secret from an ordinary
|
|
14259
14486
|
* literal. They are NOT closed by taking the source subtree, which an earlier
|
|
14260
14487
|
* revision did and the issue #1915 fences correctly rejected — measured, it
|
|
@@ -14269,13 +14496,24 @@ function deriveReadbackNeedles(bag, source, secrets, rules) {
|
|
|
14269
14496
|
* The MIXED row is the shape this module itself calls DOMINANT for CDK — an
|
|
14270
14497
|
* `Fn::Join` around `secret.secretValueFromJson(...)`.
|
|
14271
14498
|
*
|
|
14272
|
-
* TAKE SOURCE rather than a {@link SECRET_MASK} on the rows it
|
|
14499
|
+
* TAKE SOURCE rather than a {@link SECRET_MASK} on the rows it CERTIFIES, for
|
|
14273
14500
|
* the same reason the whole-token arm does: a mask is not a value `cdkd drift`
|
|
14274
14501
|
* can re-resolve, so it would report a permanent phantom — and `cdkd drift
|
|
14275
14502
|
* --revert` pushes the BASELINE to AWS, so a masked baseline would write the
|
|
14276
14503
|
* literal `***` onto the live resource (the issue #1498 / #1501 class).
|
|
14277
14504
|
*
|
|
14278
|
-
*
|
|
14505
|
+
* That is an argument about a row where a SOURCE VALUE IS AVAILABLE, and it
|
|
14506
|
+
* decides nothing about a row where none is (issue #2852). There the choice is
|
|
14507
|
+
* not mask-versus-source but mask-versus-PLAINTEXT, and the two costs above are
|
|
14508
|
+
* both real: `runAccept` refuses a masked change and
|
|
14509
|
+
* `preserveLiveValuesAtMaskedLeaves` moves AWS's own value in before `--revert`
|
|
14510
|
+
* sends anything, so the mask degrades those two commands on that resource
|
|
14511
|
+
* rather than corrupting it — while the plaintext it replaces is the disclosure
|
|
14512
|
+
* of GHSA-p5qg-v9gv-hc7w sitting in `state.json`. Do not read the paragraph
|
|
14513
|
+
* above as a rule against the mask everywhere; it is a rule about the rows with
|
|
14514
|
+
* a certified source.
|
|
14515
|
+
*
|
|
14516
|
+
* The needle rows were the RESIDUAL and are closed by DERIVED NEEDLES (issue
|
|
14279
14517
|
* #2012) — see {@link deriveReadbackNeedles}. Neither has a position to argue
|
|
14280
14518
|
* from: an unpaired array element and an observed KEY the source does not carry
|
|
14281
14519
|
* are both positions with no source leaf to take. What they never lacked was a
|
|
@@ -14301,7 +14539,7 @@ function deriveReadbackNeedles(bag, source, secrets, rules) {
|
|
|
14301
14539
|
* drift, and a needle learned from a MIS-paired position is a false redaction
|
|
14302
14540
|
* everywhere it then matches.
|
|
14303
14541
|
*/
|
|
14304
|
-
function refuseUncertifiedReadbackPositions(bag, source, secrets, learn, mark) {
|
|
14542
|
+
function refuseUncertifiedReadbackPositions(bag, source, secrets, failClosed, learn, mark) {
|
|
14305
14543
|
if (isDynamicReferenceString(source) && typeof bag === "string") {
|
|
14306
14544
|
if (isSingleDynamicReferenceToken(source)) {
|
|
14307
14545
|
if (learn) learnWholeTokenNeedle(learn, bag, source);
|
|
@@ -14312,25 +14550,26 @@ function refuseUncertifiedReadbackPositions(bag, source, secrets, learn, mark) {
|
|
|
14312
14550
|
return mark ? POSITION_DECIDED : source;
|
|
14313
14551
|
}
|
|
14314
14552
|
if (!subtreeHasDynamicReference(source)) return bag;
|
|
14315
|
-
if (isPlainObject$2(bag) && isPlainObject$2(source)) {
|
|
14553
|
+
if (isPlainObject$2(bag) && hasPlainPrototype(bag) && isPlainObject$2(source)) {
|
|
14316
14554
|
const out = Object.create(null);
|
|
14317
|
-
for (const [k, v] of Object.entries(bag)) out[k] = Object.hasOwn(source, k) ? refuseUncertifiedReadbackPositions(v, source[k], secrets, learn, mark) : v;
|
|
14555
|
+
for (const [k, v] of Object.entries(bag)) out[k] = Object.hasOwn(source, k) ? refuseUncertifiedReadbackPositions(v, source[k], secrets, failClosed, learn, mark) : v;
|
|
14318
14556
|
return out;
|
|
14319
14557
|
}
|
|
14320
14558
|
if (Array.isArray(bag) && Array.isArray(source)) {
|
|
14321
14559
|
const key = identityKeyFor(bag, source);
|
|
14322
14560
|
if (key === void 0) {
|
|
14323
|
-
if (!unkeyedArrayPairsByAnchors(bag, source)) return bag;
|
|
14324
|
-
return bag.map((item, i) => refuseUncertifiedReadbackPositions(item, source[i], secrets, learn, mark));
|
|
14561
|
+
if (!unkeyedArrayPairsByAnchors(bag, source)) return refuseAgainstSource(bag, source, failClosed, mark);
|
|
14562
|
+
return bag.map((item, i) => refuseUncertifiedReadbackPositions(item, source[i], secrets, failClosed, learn, mark));
|
|
14325
14563
|
}
|
|
14326
14564
|
const sourceByIdentity = /* @__PURE__ */ new Map();
|
|
14327
14565
|
for (const item of source) sourceByIdentity.set(item[key], item);
|
|
14566
|
+
const orphanLiterals = unpairedSourceCarriesReference(source, key, new Set(bag.map((item) => item[key]))) && failClosed === true ? wholeStringLeavesOf(source) : void 0;
|
|
14328
14567
|
return bag.map((item) => {
|
|
14329
14568
|
const partner = sourceByIdentity.get(item[key]);
|
|
14330
|
-
return partner === void 0 ? item : refuseUncertifiedReadbackPositions(item, partner, secrets, learn, mark);
|
|
14569
|
+
return partner === void 0 ? orphanLiterals !== void 0 ? refuseUncertifiedSubtree(item, orphanLiterals, mark) : item : refuseUncertifiedReadbackPositions(item, partner, secrets, failClosed, learn, mark);
|
|
14331
14570
|
});
|
|
14332
14571
|
}
|
|
14333
|
-
return bag;
|
|
14572
|
+
return refuseAgainstSource(bag, source, failClosed, mark);
|
|
14334
14573
|
}
|
|
14335
14574
|
/**
|
|
14336
14575
|
* Deep-clone `bag`, replacing every occurrence of a recorded secret value with
|
|
@@ -14346,10 +14585,11 @@ function redactSecretsForState(bag, secrets, source, rules = TEMPLATE_DERIVED_RU
|
|
|
14346
14585
|
if (source !== void 0) {
|
|
14347
14586
|
const positioned = redactByPath(bag, source, secrets, rules, recordedExpressionsOf(secrets), isSameGenerationBag(bag));
|
|
14348
14587
|
if (!isReadbackProjectedFromState(rules)) return positioned;
|
|
14349
|
-
const
|
|
14588
|
+
const failClosed = rules.failClosedOnUncertifiedPositions === true;
|
|
14589
|
+
const refused = refuseUncertifiedReadbackPositions(positioned, source, secrets, failClosed);
|
|
14350
14590
|
const derived = deriveReadbackNeedles(bag, source, secrets, rules);
|
|
14351
14591
|
if (derived === void 0) return refused;
|
|
14352
|
-
const marks = refuseUncertifiedReadbackPositions(positioned, source, secrets, void 0, true);
|
|
14592
|
+
const marks = refuseUncertifiedReadbackPositions(positioned, source, secrets, failClosed, void 0, true);
|
|
14353
14593
|
return preferPositionDecisions(redactSecretsForState(bag, derived.certain), refused, bag, marks, derived.inferred);
|
|
14354
14594
|
}
|
|
14355
14595
|
const regex = buildNeedleRegex(substringNeedlesOf(secrets));
|
|
@@ -14483,7 +14723,7 @@ function scrubResourceRecord(record, secrets, sourceProperties, observedRules) {
|
|
|
14483
14723
|
const next = { ...record };
|
|
14484
14724
|
next.properties = redactSecretsForState(record.properties, secrets, sourceProperties);
|
|
14485
14725
|
if (record.attributes) next.attributes = redactSecretsForState(record.attributes, secrets);
|
|
14486
|
-
if (record.observedProperties) next.observedProperties = redactSecretsForState(record.observedProperties, secrets, sourceProperties ?? next.properties, observedRules ?? (sourceProperties
|
|
14726
|
+
if (record.observedProperties) next.observedProperties = redactSecretsForState(record.observedProperties, secrets, sourceProperties ?? next.properties, observedRules ?? (sourceProperties !== void 0 ? TEMPLATE_SOURCED_RULES : secrets.size === 0 ? STATE_SOURCED_BASELINE_RULES : STATE_SOURCED_READBACK_RULES));
|
|
14487
14727
|
return next;
|
|
14488
14728
|
}
|
|
14489
14729
|
/**
|
|
@@ -23680,7 +23920,7 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
23680
23920
|
const [rawLogicalId, rawAttributeName] = getAtt;
|
|
23681
23921
|
logicalId = rawLogicalId;
|
|
23682
23922
|
const resolvedAttributeName = await this.resolveValue(rawAttributeName, context);
|
|
23683
|
-
if (typeof resolvedAttributeName !== "string") throw new Error(`Fn::GetAtt attribute name for ${logicalId} must resolve to a string, got ${typeof resolvedAttributeName}: ${stringifyValue(resolvedAttributeName)}`);
|
|
23923
|
+
if (typeof resolvedAttributeName !== "string") throw new Error(`Fn::GetAtt attribute name for ${logicalId} must resolve to a string, got ${typeof resolvedAttributeName}: ${stringifyValue(this.maskValueLeaves(resolvedAttributeName, context))}`);
|
|
23684
23924
|
attributeName = resolvedAttributeName;
|
|
23685
23925
|
} else {
|
|
23686
23926
|
const split = splitGetAttStringForm(getAtt);
|
|
@@ -23693,13 +23933,13 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
23693
23933
|
if (!(resource.resourceType === "AWS::EC2::VPC" && attributeName === "Ipv6CidrBlocks") && resource.attributes !== void 0) {
|
|
23694
23934
|
const flatValue = Object.hasOwn(resource.attributes, attributeName) ? resource.attributes[attributeName] : void 0;
|
|
23695
23935
|
if (flatValue !== void 0) {
|
|
23696
|
-
this.rejectPlaceholderArnAttribute(resource, attributeName, flatValue, logicalId);
|
|
23936
|
+
this.rejectPlaceholderArnAttribute(resource, attributeName, flatValue, logicalId, context);
|
|
23697
23937
|
if (resource.resourceType === "AWS::Route53::HostedZone" && attributeName === "NameServers" && typeof flatValue === "string") {
|
|
23698
23938
|
const nameServers = flatValue === "" ? [] : flatValue.split(",");
|
|
23699
|
-
this.logger.debug(`Normalized legacy Fn::GetAtt attribute: ${logicalId}.${attributeName} -> ${stringifyAttributeForLog(attributeName, nameServers)}`);
|
|
23939
|
+
this.logger.debug(`Normalized legacy Fn::GetAtt attribute: ${logicalId}.${this.maskSecretsForLog(attributeName, context)} -> ${this.maskSecretsForLog(stringifyAttributeForLog(attributeName, nameServers), context)}`);
|
|
23700
23940
|
return this.noteAttributeSecrecy(logicalId, attributeName, nameServers, context);
|
|
23701
23941
|
}
|
|
23702
|
-
this.logger.debug(`Resolved Fn::GetAtt from attributes: ${logicalId}.${attributeName} -> ${stringifyAttributeForLog(attributeName, flatValue)}`);
|
|
23942
|
+
this.logger.debug(`Resolved Fn::GetAtt from attributes: ${logicalId}.${this.maskSecretsForLog(attributeName, context)} -> ${this.maskSecretsForLog(stringifyAttributeForLog(attributeName, flatValue), context)}`);
|
|
23703
23943
|
if (resource.resourceType === NESTED_STACK_RESOURCE_TYPE$1 && attributeName.startsWith(NESTED_STACK_OUTPUT_ATTRIBUTE_PREFIX) && carriesDynamicReference(flatValue)) return await this.reresolveCrossStackValue(flatValue, nestedStackChildRegionFromLocalArn(resource.physicalId), context, `nested stack ${logicalId} ${attributeName}`, crossStackSourceKey({ "Fn::GetAtt": getAtt }));
|
|
23704
23944
|
return this.noteAttributeSecrecy(logicalId, attributeName, flatValue, context);
|
|
23705
23945
|
}
|
|
@@ -23712,17 +23952,17 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
23712
23952
|
break;
|
|
23713
23953
|
}
|
|
23714
23954
|
if (cursor !== void 0) {
|
|
23715
|
-
this.logger.debug(`Resolved Fn::GetAtt from nested attributes: ${logicalId}.${attributeName} -> ${stringifyAttributeForLog(attributeName, cursor)}`);
|
|
23955
|
+
this.logger.debug(`Resolved Fn::GetAtt from nested attributes: ${logicalId}.${this.maskSecretsForLog(attributeName, context)} -> ${this.maskSecretsForLog(stringifyAttributeForLog(attributeName, cursor), context)}`);
|
|
23716
23956
|
return this.noteAttributeSecrecy(logicalId, attributeName, cursor, context);
|
|
23717
23957
|
}
|
|
23718
23958
|
}
|
|
23719
23959
|
}
|
|
23720
23960
|
if (resource.resourceType === NESTED_STACK_RESOURCE_TYPE$1 && attributeName.startsWith(NESTED_STACK_OUTPUT_ATTRIBUTE_PREFIX)) {
|
|
23721
23961
|
const declared = Object.keys(resource.attributes ?? {}).filter((k) => k.startsWith(NESTED_STACK_OUTPUT_ATTRIBUTE_PREFIX)).map((k) => k.slice(8)).sort();
|
|
23722
|
-
throw markNonRetryable(new IntrinsicResolutionRefusalError(`Cannot resolve Fn::GetAtt [${logicalId}, ${attributeName}]: the nested stack '${logicalId}' declares no output named '${attributeName.slice(8)}'. Its outputs are ${declared.length > 0 ? declared.join(", ") : "(none)"}. Check the output name in the nested stack's template, and deploy the child stack again if you have just added it.`));
|
|
23962
|
+
throw markNonRetryable(new IntrinsicResolutionRefusalError(`Cannot resolve Fn::GetAtt [${logicalId}, ${this.maskSecretsForLog(attributeName, context)}]: the nested stack '${logicalId}' declares no output named '${this.maskSecretsForLog(attributeName.slice(8), context)}'. Its outputs are ${declared.length > 0 ? declared.join(", ") : "(none)"}. Check the output name in the nested stack's template, and deploy the child stack again if you have just added it.`));
|
|
23723
23963
|
}
|
|
23724
23964
|
const value = await this.constructGuardedAttribute(resource, attributeName, context, logicalId);
|
|
23725
|
-
this.logger.debug(`Resolved Fn::GetAtt: ${logicalId}.${attributeName} -> ${stringifyAttributeForLog(attributeName, value)}`);
|
|
23965
|
+
this.logger.debug(`Resolved Fn::GetAtt: ${logicalId}.${this.maskSecretsForLog(attributeName, context)} -> ${this.maskSecretsForLog(stringifyAttributeForLog(attributeName, value), context)}`);
|
|
23726
23966
|
return value;
|
|
23727
23967
|
}
|
|
23728
23968
|
/**
|
|
@@ -23773,7 +24013,7 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
23773
24013
|
const declared = context.noEchoAttributeResources?.get(logicalId);
|
|
23774
24014
|
if ((declared === true || declared !== void 0 && declared.has(attributeName)) && context.recordedSecretValues) recordMaskOnlyValuesIn(value, context.recordedSecretValues);
|
|
23775
24015
|
if (context.redactedAttributeReads !== void 0 && carriesSecretMask(value)) {
|
|
23776
|
-
const read = `${logicalId}.${attributeName}`;
|
|
24016
|
+
const read = `${logicalId}.${this.maskSecretsForLog(attributeName, context)}`;
|
|
23777
24017
|
if (!context.redactedAttributeReads.includes(read)) context.redactedAttributeReads.push(read);
|
|
23778
24018
|
}
|
|
23779
24019
|
return value;
|
|
@@ -23807,10 +24047,10 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
23807
24047
|
* honest answer is to say so and name the remedy: the record heals on the
|
|
23808
24048
|
* resource's next in-place update (#1727).
|
|
23809
24049
|
*/
|
|
23810
|
-
rejectPlaceholderArnAttribute(resource, attributeName, value, logicalId) {
|
|
24050
|
+
rejectPlaceholderArnAttribute(resource, attributeName, value, logicalId, context) {
|
|
23811
24051
|
if (!REF_RETURNS_ARN_FROM_STATE.get(resource.resourceType)?.includes(attributeName)) return;
|
|
23812
24052
|
if (typeof value !== "string" || !isPlaceholderArn(value)) return;
|
|
23813
|
-
throw markNonRetryable(new IntrinsicResolutionRefusalError(`Cannot resolve Fn::GetAtt [${logicalId}, ${attributeName}] for ${resource.resourceType}: the recorded value "${value}" is a placeholder written by a cdkd version older than issue #1681 — its region and account fields are literal wildcards, so it is not a usable ARN. Deploy the stack again so the resource's next update heals the record (cdkd now records the real ARN), or re-import the resource.`));
|
|
24053
|
+
throw markNonRetryable(new IntrinsicResolutionRefusalError(`Cannot resolve Fn::GetAtt [${logicalId}, ${this.maskSecretsForLog(attributeName, context)}] for ${resource.resourceType}: the recorded value "${stringifyValue(this.maskValueLeaves(value, context))}" is a placeholder written by a cdkd version older than issue #1681 — its region and account fields are literal wildcards, so it is not a usable ARN. Deploy the stack again so the resource's next update heals the record (cdkd now records the real ARN), or re-import the resource.`));
|
|
23814
24054
|
}
|
|
23815
24055
|
/**
|
|
23816
24056
|
* Construct resource attribute value based on resource type, refusing to
|
|
@@ -23855,7 +24095,7 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
23855
24095
|
async constructGuardedAttribute(resource, attributeName, context, logicalId) {
|
|
23856
24096
|
const accountInfo = await getAccountInfo(this.resolverRegion);
|
|
23857
24097
|
const value = await this.constructAttribute(resource, attributeName, context, logicalId, accountInfo);
|
|
23858
|
-
if (accountInfo.fabricated && embedsAccountId(value, accountInfo.accountId)) throw new IntrinsicResolutionRefusalError(`Cannot resolve Fn::GetAtt [${logicalId}, ${attributeName}] for ${resource.resourceType}: STS did not report this deploy's account id, so cdkd would build the value from the placeholder account ${accountInfo.accountId} — structurally valid, naming a different account, and indistinguishable downstream from a real one. Fix the AWS credentials (or set AWS_ACCOUNT_ID to this deploy's account) and deploy again.`);
|
|
24098
|
+
if (accountInfo.fabricated && embedsAccountId(value, accountInfo.accountId)) throw new IntrinsicResolutionRefusalError(`Cannot resolve Fn::GetAtt [${logicalId}, ${this.maskSecretsForLog(attributeName, context)}] for ${resource.resourceType}: STS did not report this deploy's account id, so cdkd would build the value from the placeholder account ${accountInfo.accountId} — structurally valid, naming a different account, and indistinguishable downstream from a real one. Fix the AWS credentials (or set AWS_ACCOUNT_ID to this deploy's account) and deploy again.`);
|
|
23859
24099
|
return value;
|
|
23860
24100
|
}
|
|
23861
24101
|
/**
|
|
@@ -23868,14 +24108,14 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
23868
24108
|
* result. Keep this method's NAME — `scripts/gen-sdk-attr-coverage.ts` reads
|
|
23869
24109
|
* the resource types it references.
|
|
23870
24110
|
*/
|
|
23871
|
-
async constructAttribute(resource, attributeName,
|
|
24111
|
+
async constructAttribute(resource, attributeName, context, logicalId, accountInfo) {
|
|
23872
24112
|
const { resourceType, physicalId } = resource;
|
|
23873
24113
|
const { accountId, partition } = accountInfo;
|
|
23874
24114
|
const region = canonicalizeRegion(accountInfo.region);
|
|
23875
24115
|
if (resourceType === "AWS::DynamoDB::Table" || resourceType === "AWS::DynamoDB::GlobalTable") switch (attributeName) {
|
|
23876
24116
|
case "Arn": return `arn:${partition}:dynamodb:${region}:${accountId}:table/${physicalId}`;
|
|
23877
24117
|
case "StreamArn": return;
|
|
23878
|
-
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
|
|
24118
|
+
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
|
|
23879
24119
|
}
|
|
23880
24120
|
if (resourceType === "AWS::S3::Bucket") switch (attributeName) {
|
|
23881
24121
|
case "Arn": return s3BucketArn(physicalId, region);
|
|
@@ -23883,12 +24123,12 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
23883
24123
|
case "RegionalDomainName": return s3BucketRegionalDomainName(physicalId, region);
|
|
23884
24124
|
case "DualStackDomainName": return s3BucketDualStackDomainName(physicalId, region);
|
|
23885
24125
|
case "WebsiteURL": return s3BucketWebsiteUrl(physicalId, region);
|
|
23886
|
-
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
|
|
24126
|
+
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
|
|
23887
24127
|
}
|
|
23888
24128
|
if (resourceType === "AWS::IAM::Role") switch (attributeName) {
|
|
23889
24129
|
case "Arn": return `arn:${partition}:iam::${accountId}:role/${physicalId}`;
|
|
23890
24130
|
case "RoleId": return;
|
|
23891
|
-
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
|
|
24131
|
+
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
|
|
23892
24132
|
}
|
|
23893
24133
|
if (resourceType === "AWS::EC2::VPC") switch (attributeName) {
|
|
23894
24134
|
case "VpcId": return physicalId;
|
|
@@ -23900,7 +24140,7 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
23900
24140
|
const associations = (await ec2.send(new DescribeVpcsCommand({ VpcIds: [physicalId] }))).Vpcs?.[0]?.Ipv6CidrBlockAssociationSet || [];
|
|
23901
24141
|
const blocks = associations.filter((a) => a.Ipv6CidrBlockState?.State === "associated").map((a) => a.Ipv6CidrBlock);
|
|
23902
24142
|
if (blocks.length > 0) {
|
|
23903
|
-
this.logger.debug(`Resolved VPC Ipv6CidrBlocks for ${physicalId}: ${JSON.stringify(blocks)}`);
|
|
24143
|
+
this.logger.debug(`Resolved VPC Ipv6CidrBlocks for ${physicalId}: ${this.maskSecretsForLog(JSON.stringify(this.maskValueLeaves(blocks, context)), context)}`);
|
|
23904
24144
|
return blocks;
|
|
23905
24145
|
}
|
|
23906
24146
|
if (associations.filter((a) => a.Ipv6CidrBlockState?.State === "associating").length === 0) {
|
|
@@ -23917,38 +24157,38 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
23917
24157
|
return [];
|
|
23918
24158
|
}
|
|
23919
24159
|
case "DefaultSecurityGroup": return resource.attributes?.["DefaultSecurityGroup"] || physicalId;
|
|
23920
|
-
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
|
|
24160
|
+
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
|
|
23921
24161
|
}
|
|
23922
24162
|
if (resourceType === "AWS::IAM::Policy") switch (attributeName) {
|
|
23923
24163
|
case "Arn": return `arn:${partition}:iam::${accountId}:policy/${physicalId}`;
|
|
23924
24164
|
case "PolicyId": return;
|
|
23925
|
-
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
|
|
24165
|
+
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
|
|
23926
24166
|
}
|
|
23927
24167
|
if (resourceType === "AWS::IAM::User") switch (attributeName) {
|
|
23928
24168
|
case "Arn": return `arn:${partition}:iam::${accountId}:user/${physicalId}`;
|
|
23929
|
-
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
|
|
24169
|
+
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
|
|
23930
24170
|
}
|
|
23931
24171
|
if (resourceType === "AWS::IAM::Group") switch (attributeName) {
|
|
23932
24172
|
case "Arn": return `arn:${partition}:iam::${accountId}:group/${physicalId}`;
|
|
23933
|
-
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
|
|
24173
|
+
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
|
|
23934
24174
|
}
|
|
23935
24175
|
if (resourceType === "AWS::IAM::InstanceProfile") switch (attributeName) {
|
|
23936
24176
|
case "Arn": return `arn:${partition}:iam::${accountId}:instance-profile/${physicalId}`;
|
|
23937
|
-
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
|
|
24177
|
+
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
|
|
23938
24178
|
}
|
|
23939
24179
|
if (resourceType === "AWS::KMS::Key") switch (attributeName) {
|
|
23940
24180
|
case "Arn": return `arn:${partition}:kms:${region}:${accountId}:key/${physicalId}`;
|
|
23941
24181
|
case "KeyId": return physicalId;
|
|
23942
|
-
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
|
|
24182
|
+
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
|
|
23943
24183
|
}
|
|
23944
24184
|
if (resourceType === "AWS::Cognito::UserPool") switch (attributeName) {
|
|
23945
24185
|
case "Arn": return `arn:${partition}:cognito-idp:${region}:${accountId}:userpool/${physicalId}`;
|
|
23946
24186
|
case "UserPoolId": return physicalId;
|
|
23947
|
-
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
|
|
24187
|
+
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
|
|
23948
24188
|
}
|
|
23949
24189
|
if (resourceType === "AWS::Kinesis::Stream") switch (attributeName) {
|
|
23950
24190
|
case "Arn": return `arn:${partition}:kinesis:${region}:${accountId}:stream/${physicalId}`;
|
|
23951
|
-
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
|
|
24191
|
+
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
|
|
23952
24192
|
}
|
|
23953
24193
|
if (resourceType === "AWS::Events::Rule") switch (attributeName) {
|
|
23954
24194
|
case "Arn": {
|
|
@@ -23958,41 +24198,41 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
23958
24198
|
const busName = bus.startsWith("arn:") ? bus.split("/").pop() || "" : bus;
|
|
23959
24199
|
return busName ? `arn:${partition}:events:${region}:${accountId}:rule/${busName}/${physicalId}` : `arn:${partition}:events:${region}:${accountId}:rule/${physicalId}`;
|
|
23960
24200
|
}
|
|
23961
|
-
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
|
|
24201
|
+
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
|
|
23962
24202
|
}
|
|
23963
24203
|
if (resourceType === "AWS::Events::EventBus") switch (attributeName) {
|
|
23964
24204
|
case "Arn": return `arn:${partition}:events:${region}:${accountId}:event-bus/${physicalId}`;
|
|
23965
24205
|
case "Name": return physicalId;
|
|
23966
|
-
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
|
|
24206
|
+
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
|
|
23967
24207
|
}
|
|
23968
24208
|
if (resourceType === "AWS::EFS::FileSystem") switch (attributeName) {
|
|
23969
24209
|
case "Arn": return `arn:${partition}:elasticfilesystem:${region}:${accountId}:file-system/${physicalId}`;
|
|
23970
24210
|
case "FileSystemId": return physicalId;
|
|
23971
|
-
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
|
|
24211
|
+
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
|
|
23972
24212
|
}
|
|
23973
24213
|
if (resourceType === "AWS::KinesisFirehose::DeliveryStream") switch (attributeName) {
|
|
23974
24214
|
case "Arn": return `arn:${partition}:firehose:${region}:${accountId}:deliverystream/${physicalId}`;
|
|
23975
|
-
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
|
|
24215
|
+
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
|
|
23976
24216
|
}
|
|
23977
24217
|
if (resourceType === "AWS::CodeBuild::Project") switch (attributeName) {
|
|
23978
24218
|
case "Arn": return `arn:${partition}:codebuild:${region}:${accountId}:project/${physicalId}`;
|
|
23979
|
-
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
|
|
24219
|
+
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
|
|
23980
24220
|
}
|
|
23981
24221
|
if (resourceType === "AWS::CloudTrail::Trail") switch (attributeName) {
|
|
23982
24222
|
case "Arn":
|
|
23983
24223
|
if (physicalId.startsWith("arn:")) return physicalId;
|
|
23984
24224
|
return `arn:${partition}:cloudtrail:${region}:${accountId}:trail/${physicalId}`;
|
|
23985
|
-
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
|
|
24225
|
+
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
|
|
23986
24226
|
}
|
|
23987
24227
|
if (resourceType === "AWS::AppSync::GraphQLApi") switch (attributeName) {
|
|
23988
24228
|
case "Arn": return `arn:${partition}:appsync:${region}:${accountId}:apis/${physicalId}`;
|
|
23989
24229
|
case "ApiId": return physicalId;
|
|
23990
|
-
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
|
|
24230
|
+
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
|
|
23991
24231
|
}
|
|
23992
24232
|
if (resourceType === "AWS::ApiGatewayV2::Api") switch (attributeName) {
|
|
23993
24233
|
case "ExecuteApiArn": return `arn:${partition}:execute-api:${region}:${accountId}:${physicalId}`;
|
|
23994
24234
|
case "ApiId": return physicalId;
|
|
23995
|
-
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
|
|
24235
|
+
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
|
|
23996
24236
|
}
|
|
23997
24237
|
if (resourceType === "AWS::ServiceDiscovery::PrivateDnsNamespace" || resourceType === "AWS::ServiceDiscovery::HttpNamespace" || resourceType === "AWS::ServiceDiscovery::PublicDnsNamespace") switch (attributeName) {
|
|
23998
24238
|
case "Arn": return `arn:${partition}:servicediscovery:${region}:${accountId}:namespace/${physicalId}`;
|
|
@@ -24004,38 +24244,38 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
24004
24244
|
this.logger.warn(`Failed to fetch HostedZoneId for namespace ${physicalId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
24005
24245
|
return;
|
|
24006
24246
|
}
|
|
24007
|
-
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
|
|
24247
|
+
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
|
|
24008
24248
|
}
|
|
24009
24249
|
if (resourceType === "AWS::ServiceDiscovery::Service") switch (attributeName) {
|
|
24010
24250
|
case "Arn": return `arn:${partition}:servicediscovery:${region}:${accountId}:service/${physicalId}`;
|
|
24011
24251
|
case "Id": return physicalId;
|
|
24012
|
-
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
|
|
24252
|
+
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
|
|
24013
24253
|
}
|
|
24014
24254
|
if (resourceType === "AWS::CloudWatch::Alarm") switch (attributeName) {
|
|
24015
24255
|
case "Arn": return `arn:${partition}:cloudwatch:${region}:${accountId}:alarm:${physicalId}`;
|
|
24016
|
-
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
|
|
24256
|
+
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
|
|
24017
24257
|
}
|
|
24018
24258
|
if (resourceType === "AWS::CloudWatch::CompositeAlarm") switch (attributeName) {
|
|
24019
24259
|
case "Arn": return `arn:${partition}:cloudwatch:${region}:${accountId}:alarm:${physicalId}`;
|
|
24020
|
-
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
|
|
24260
|
+
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
|
|
24021
24261
|
}
|
|
24022
24262
|
if (resourceType === "AWS::RDS::DBInstance" || resourceType === "AWS::DocDB::DBInstance" || resourceType === "AWS::Neptune::DBInstance") switch (attributeName) {
|
|
24023
24263
|
case "DBInstanceArn":
|
|
24024
24264
|
case "Arn": return `arn:${partition}:rds:${region}:${accountId}:db:${physicalId}`;
|
|
24025
|
-
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
|
|
24265
|
+
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
|
|
24026
24266
|
}
|
|
24027
24267
|
if (resourceType === "AWS::RDS::DBCluster" || resourceType === "AWS::DocDB::DBCluster" || resourceType === "AWS::Neptune::DBCluster") switch (attributeName) {
|
|
24028
24268
|
case "DBClusterArn":
|
|
24029
24269
|
case "Arn": return `arn:${partition}:rds:${region}:${accountId}:cluster:${physicalId}`;
|
|
24030
|
-
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
|
|
24270
|
+
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
|
|
24031
24271
|
}
|
|
24032
24272
|
if (resourceType === "AWS::S3Express::DirectoryBucket") switch (attributeName) {
|
|
24033
24273
|
case "Arn": return `arn:${partition}:s3express:${region}:${accountId}:bucket/${physicalId}`;
|
|
24034
|
-
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
|
|
24274
|
+
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
|
|
24035
24275
|
}
|
|
24036
24276
|
if (resourceType === "AWS::Lambda::Function") switch (attributeName) {
|
|
24037
24277
|
case "Arn": return `arn:${partition}:lambda:${region}:${accountId}:function:${physicalId}`;
|
|
24038
|
-
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
|
|
24278
|
+
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
|
|
24039
24279
|
}
|
|
24040
24280
|
if (resourceType === "AWS::SQS::Queue") {
|
|
24041
24281
|
let queueName = physicalId;
|
|
@@ -24047,26 +24287,26 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
24047
24287
|
case "Arn": return `arn:${partition}:sqs:${region}:${accountId}:${queueName}`;
|
|
24048
24288
|
case "QueueUrl": return physicalId;
|
|
24049
24289
|
case "QueueName": return queueName;
|
|
24050
|
-
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
|
|
24290
|
+
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
|
|
24051
24291
|
}
|
|
24052
24292
|
}
|
|
24053
24293
|
if (resourceType === "AWS::SNS::Topic") switch (attributeName) {
|
|
24054
24294
|
case "TopicArn": return `arn:${partition}:sns:${region}:${accountId}:${physicalId}`;
|
|
24055
24295
|
case "TopicName": return physicalId;
|
|
24056
|
-
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
|
|
24296
|
+
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
|
|
24057
24297
|
}
|
|
24058
24298
|
if (resourceType === "AWS::Logs::LogGroup") switch (attributeName) {
|
|
24059
24299
|
case "Arn": return `arn:${partition}:logs:${region}:${accountId}:log-group:${physicalId}:*`;
|
|
24060
|
-
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
|
|
24300
|
+
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
|
|
24061
24301
|
}
|
|
24062
24302
|
if (resourceType === "AWS::ECR::Repository") switch (attributeName) {
|
|
24063
24303
|
case "Arn": return `arn:${partition}:ecr:${region}:${accountId}:repository/${physicalId}`;
|
|
24064
24304
|
case "RepositoryUri": return `${accountId}.dkr.ecr.${region}.${derivePartitionAndUrlSuffix(region).urlSuffix}/${physicalId}`;
|
|
24065
|
-
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
|
|
24305
|
+
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
|
|
24066
24306
|
}
|
|
24067
24307
|
if (resourceType === "AWS::ECS::Cluster") switch (attributeName) {
|
|
24068
24308
|
case "Arn": return `arn:${partition}:ecs:${region}:${accountId}:cluster/${physicalId}`;
|
|
24069
|
-
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
|
|
24309
|
+
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
|
|
24070
24310
|
}
|
|
24071
24311
|
if (resourceType === "AWS::ECS::Service") switch (attributeName) {
|
|
24072
24312
|
case "Name": {
|
|
@@ -24090,16 +24330,16 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
24090
24330
|
const serviceName = physicalId.substring(pipeIdx + 1);
|
|
24091
24331
|
return `${left.substring(0, clusterIdx)}:service/${clusterName}/${serviceName}`;
|
|
24092
24332
|
}
|
|
24093
|
-
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
|
|
24333
|
+
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
|
|
24094
24334
|
}
|
|
24095
24335
|
if (resourceType === "AWS::EC2::SecurityGroup") switch (attributeName) {
|
|
24096
24336
|
case "GroupId": return physicalId;
|
|
24097
24337
|
case "VpcId": return;
|
|
24098
|
-
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
|
|
24338
|
+
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
|
|
24099
24339
|
}
|
|
24100
24340
|
if (resourceType === "AWS::EC2::Subnet") switch (attributeName) {
|
|
24101
24341
|
case "SubnetId": return physicalId;
|
|
24102
|
-
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
|
|
24342
|
+
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
|
|
24103
24343
|
}
|
|
24104
24344
|
if (resourceType === "AWS::EC2::Instance") switch (attributeName) {
|
|
24105
24345
|
case "InstanceId": return physicalId;
|
|
@@ -24133,13 +24373,13 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
24133
24373
|
cachedEc2InstanceAttributes[cacheKey] = value;
|
|
24134
24374
|
return value;
|
|
24135
24375
|
}
|
|
24136
|
-
this.logger.warn(`DescribeInstances(${physicalId}) returned no ${attributeName}; returning physical ID`);
|
|
24376
|
+
this.logger.warn(`DescribeInstances(${physicalId}) returned no ${this.maskSecretsForLog(attributeName, context)}; returning physical ID`);
|
|
24137
24377
|
} catch (err) {
|
|
24138
|
-
this.logger.warn(`DescribeInstances(${physicalId}) failed for ${attributeName}: ${err instanceof Error ? err.message : String(err)}`);
|
|
24378
|
+
this.logger.warn(`DescribeInstances(${physicalId}) failed for ${this.maskSecretsForLog(attributeName, context)}: ${this.maskSecretsForLog(err instanceof Error ? err.message : String(err), context)}`);
|
|
24139
24379
|
}
|
|
24140
24380
|
return physicalId;
|
|
24141
24381
|
}
|
|
24142
|
-
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
|
|
24382
|
+
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
|
|
24143
24383
|
}
|
|
24144
24384
|
if (resourceType === "AWS::EC2::LaunchTemplate") {
|
|
24145
24385
|
if (attributeName === "LatestVersionNumber" || attributeName === "DefaultVersionNumber") {
|
|
@@ -24148,14 +24388,14 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
24148
24388
|
const value = attributeName === "LatestVersionNumber" ? lt?.LatestVersionNumber : lt?.DefaultVersionNumber;
|
|
24149
24389
|
if (value !== void 0 && value !== null) return String(value);
|
|
24150
24390
|
} catch (err) {
|
|
24151
|
-
this.logger.warn(`DescribeLaunchTemplates(${physicalId}) failed for ${attributeName}: ${err instanceof Error ? err.message : String(err)}`);
|
|
24391
|
+
this.logger.warn(`DescribeLaunchTemplates(${physicalId}) failed for ${this.maskSecretsForLog(attributeName, context)}: ${this.maskSecretsForLog(err instanceof Error ? err.message : String(err), context)}`);
|
|
24152
24392
|
}
|
|
24153
24393
|
return attributeName === "LatestVersionNumber" ? "$Latest" : "$Default";
|
|
24154
24394
|
}
|
|
24155
24395
|
if (attributeName === "LaunchTemplateId") return physicalId;
|
|
24156
|
-
return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
|
|
24396
|
+
return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
|
|
24157
24397
|
}
|
|
24158
|
-
return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
|
|
24398
|
+
return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
|
|
24159
24399
|
}
|
|
24160
24400
|
/**
|
|
24161
24401
|
* Shared unknown-attribute physicalId fallback (issues #1106 / #1111).
|
|
@@ -24184,13 +24424,16 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
24184
24424
|
* route through this helper — those are explicit `case`s in the
|
|
24185
24425
|
* per-type handlers.
|
|
24186
24426
|
*/
|
|
24187
|
-
guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId) {
|
|
24427
|
+
guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context) {
|
|
24188
24428
|
const expectsArnShape = attributeName.endsWith("Arn") && !physicalId.startsWith("arn:");
|
|
24189
24429
|
const expectsUrlShape = attributeName.endsWith("Url") && !/^https?:\/\//.test(physicalId);
|
|
24190
|
-
if (expectsArnShape || expectsUrlShape)
|
|
24191
|
-
|
|
24430
|
+
if (expectsArnShape || expectsUrlShape) {
|
|
24431
|
+
const expectedShape = expectsArnShape ? "an ARN (arn:...)" : "a URL (http(s)://...)";
|
|
24432
|
+
throw markNonRetryable(new IntrinsicResolutionRefusalError(`Cannot resolve Fn::GetAtt [${logicalId}, ${this.maskSecretsForLog(attributeName, context)}] for ${resourceType}: attributes are not enriched for this resource type, and the physical ID fallback "${this.maskSecretsForLog(physicalId, context)}" is not ${expectedShape}. CloudFormation would return a different value here, so falling back to the physical ID would silently produce a wrong value (e.g. in stack Outputs). Avoid this Fn::GetAtt, or file an issue at https://github.com/go-to-k/cdkd/issues so cdkd can enrich ${resourceType}.${this.maskSecretsForLog(attributeName, context)}.`));
|
|
24433
|
+
}
|
|
24434
|
+
if (this.strictGetAtt) throw markNonRetryable(new IntrinsicResolutionRefusalError(`Cannot resolve Fn::GetAtt [${logicalId}, ${this.maskSecretsForLog(attributeName, context)}] for ${resourceType}: attributes are not enriched for this resource type, and --strict-getatt rejects the physical ID fallback "${this.maskSecretsForLog(physicalId, context)}" (which may not be the value CloudFormation would return). Drop --strict-getatt to fall back with a warning, avoid this Fn::GetAtt, or file an issue at https://github.com/go-to-k/cdkd/issues so cdkd can enrich ${resourceType}.${this.maskSecretsForLog(attributeName, context)}.`));
|
|
24192
24435
|
this.physicalIdFallbackCount++;
|
|
24193
|
-
this.logger.warn(`Unknown attribute ${attributeName} for resource type ${resourceType}, returning physical ID`);
|
|
24436
|
+
this.logger.warn(`Unknown attribute ${this.maskSecretsForLog(attributeName, context)} for resource type ${resourceType}, returning physical ID`);
|
|
24194
24437
|
return physicalId;
|
|
24195
24438
|
}
|
|
24196
24439
|
/**
|
|
@@ -24391,13 +24634,13 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
24391
24634
|
} catch (getAttError) {
|
|
24392
24635
|
if (getAttError instanceof IntrinsicResolutionRefusalError) throw getAttError;
|
|
24393
24636
|
this.rethrowStructuralSubFailure(varNameStr, getAttError, context);
|
|
24394
|
-
this.logger.warn(this.subPlaceholderWarning(varNameStr, getAttError));
|
|
24637
|
+
this.logger.warn(this.maskSecretsForLog(this.subPlaceholderWarning(varNameStr, getAttError), context));
|
|
24395
24638
|
replacement = match[0];
|
|
24396
24639
|
}
|
|
24397
24640
|
else {
|
|
24398
24641
|
if (refError instanceof IntrinsicResolutionRefusalError) throw refError;
|
|
24399
24642
|
this.rethrowStructuralSubFailure(varNameStr, refError, context);
|
|
24400
|
-
this.logger.warn(this.subPlaceholderWarning(varNameStr, refError));
|
|
24643
|
+
this.logger.warn(this.maskSecretsForLog(this.subPlaceholderWarning(varNameStr, refError), context));
|
|
24401
24644
|
replacement = match[0];
|
|
24402
24645
|
}
|
|
24403
24646
|
}
|
|
@@ -24431,7 +24674,7 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
24431
24674
|
return `{{Fn::Select:${index}:OutOfBounds}}`;
|
|
24432
24675
|
}
|
|
24433
24676
|
const result = resolvedList[index];
|
|
24434
|
-
this.logger.debug(`Resolved Fn::Select: index ${index} -> ${
|
|
24677
|
+
this.logger.debug(`Resolved Fn::Select: index ${index} -> ${JSON.stringify(this.maskValueLeaves(result, context))}`);
|
|
24435
24678
|
return result;
|
|
24436
24679
|
}
|
|
24437
24680
|
/**
|
|
@@ -24575,7 +24818,7 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
24575
24818
|
throw markNonRetryable(new IntrinsicResolutionRefusalError(`Fn::Split: the value to split${sourceClause} must be a string, got ${resolvedValue === null ? "null" : typeof resolvedValue}. Fn::Split accepts only a string; check the value or the intrinsic that produced it.`));
|
|
24576
24819
|
}
|
|
24577
24820
|
const result = resolvedValue.split(delimiter);
|
|
24578
|
-
this.logger.debug(`Resolved Fn::Split: split by "${delimiter}" -> ${
|
|
24821
|
+
this.logger.debug(`Resolved Fn::Split: split by "${delimiter}" -> ${JSON.stringify(this.maskValueLeaves(result, context))}`);
|
|
24579
24822
|
return result;
|
|
24580
24823
|
}
|
|
24581
24824
|
/**
|
|
@@ -24606,7 +24849,7 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
24606
24849
|
const resolved1 = await this.resolveValue(value1, context);
|
|
24607
24850
|
const resolved2 = await this.resolveValue(value2, context);
|
|
24608
24851
|
const result = JSON.stringify(resolved1) === JSON.stringify(resolved2);
|
|
24609
|
-
this.logger.debug(`Resolved Fn::Equals: ${
|
|
24852
|
+
this.logger.debug(`Resolved Fn::Equals: ${JSON.stringify(this.maskValueLeaves(resolved1, context))} === ${JSON.stringify(this.maskValueLeaves(resolved2, context))} -> ${result}`);
|
|
24610
24853
|
return result;
|
|
24611
24854
|
}
|
|
24612
24855
|
/**
|
|
@@ -24754,7 +24997,10 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
24754
24997
|
if (context.recordedSecretValues) recordMaskOnlyValuesIn(recovered, context.recordedSecretValues);
|
|
24755
24998
|
return recovered;
|
|
24756
24999
|
}
|
|
24757
|
-
if (context.redactedAttributeReads !== void 0
|
|
25000
|
+
if (context.redactedAttributeReads !== void 0) {
|
|
25001
|
+
const loggedOrigin = this.maskSecretsForLog(origin, context);
|
|
25002
|
+
if (!context.redactedAttributeReads.includes(loggedOrigin)) context.redactedAttributeReads.push(loggedOrigin);
|
|
25003
|
+
}
|
|
24758
25004
|
}
|
|
24759
25005
|
if (!carriesDynamicReference(value)) return value;
|
|
24760
25006
|
const resolver = this.resolverForProducerRegion(producerRegion);
|
|
@@ -24870,12 +25116,12 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
24870
25116
|
try {
|
|
24871
25117
|
entry = await context.exportIndex.lookup(exportName);
|
|
24872
25118
|
} catch (err) {
|
|
24873
|
-
this.logger.warn(`Exports index lookup failed for '${loggedExportName}': ${err instanceof Error ? err.message : String(err)}; falling back to state.json scan`);
|
|
25119
|
+
this.logger.warn(`Exports index lookup failed for '${loggedExportName}': ${this.maskSecretsForLog(err instanceof Error ? err.message : String(err), context)}; falling back to state.json scan`);
|
|
24874
25120
|
entry = void 0;
|
|
24875
25121
|
}
|
|
24876
25122
|
if (entry && (!context.stackName || entry.producerStack !== context.stackName)) {
|
|
24877
25123
|
this.recordImport(context, exportName, entry.producerStack, entry.producerRegion);
|
|
24878
|
-
this.logger.info(`Resolved Fn::ImportValue: ${loggedExportName} (from index: ${entry.producerStack} / ${entry.producerRegion}; ${carriesDynamicReference(entry.value) ? "redacted dynamic reference" : "literal value"})`);
|
|
25124
|
+
this.logger.info(`Resolved Fn::ImportValue: ${loggedExportName} (from index: ${this.maskSecretsForLog(entry.producerStack, context)} / ${entry.producerRegion}; ${carriesDynamicReference(entry.value) ? "redacted dynamic reference" : "literal value"})`);
|
|
24879
25125
|
return await this.reresolveCrossStackValue(entry.value, entry.producerRegion, context, `Fn::ImportValue '${exportName}' (producer ${entry.producerStack} / ${entry.producerRegion})`, sourceKey, {
|
|
24880
25126
|
stackName: entry.producerStack,
|
|
24881
25127
|
region: entry.producerRegion,
|
|
@@ -24889,30 +25135,30 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
24889
25135
|
for (const ref of allStacks) {
|
|
24890
25136
|
const { stackName: refStack, region: refRegion } = ref;
|
|
24891
25137
|
if (context.stackName && refStack === context.stackName) {
|
|
24892
|
-
this.logger.debug(`Skipping current stack: ${refStack}`);
|
|
25138
|
+
this.logger.debug(`Skipping current stack: ${this.maskSecretsForLog(refStack, context)}`);
|
|
24893
25139
|
continue;
|
|
24894
25140
|
}
|
|
24895
25141
|
try {
|
|
24896
25142
|
const lookupRegion = refRegion ?? this.resolverRegion ?? "";
|
|
24897
25143
|
if (!lookupRegion) {
|
|
24898
|
-
this.logger.debug(`No region available for stack '${refStack}' — skipping (cdkd cannot read state without a region)`);
|
|
25144
|
+
this.logger.debug(`No region available for stack '${this.maskSecretsForLog(refStack, context)}' — skipping (cdkd cannot read state without a region)`);
|
|
24899
25145
|
continue;
|
|
24900
25146
|
}
|
|
24901
25147
|
const stateData = await context.stateBackend.getState(refStack, lookupRegion);
|
|
24902
25148
|
if (!stateData) {
|
|
24903
|
-
this.logger.debug(`No state found for stack: ${refStack} (${lookupRegion})`);
|
|
25149
|
+
this.logger.debug(`No state found for stack: ${this.maskSecretsForLog(refStack, context)} (${lookupRegion})`);
|
|
24904
25150
|
continue;
|
|
24905
25151
|
}
|
|
24906
25152
|
const { state } = stateData;
|
|
24907
25153
|
if (importableOutputKeys(state).includes(exportName)) {
|
|
24908
25154
|
const value = state.outputs[exportName];
|
|
24909
|
-
this.logger.info(`Resolved Fn::ImportValue: ${loggedExportName} (from stack: ${refStack} / ${lookupRegion}; ${carriesDynamicReference(value) ? "redacted dynamic reference" : "literal value"})`);
|
|
25155
|
+
this.logger.info(`Resolved Fn::ImportValue: ${loggedExportName} (from stack: ${this.maskSecretsForLog(refStack, context)} / ${lookupRegion}; ${carriesDynamicReference(value) ? "redacted dynamic reference" : "literal value"})`);
|
|
24910
25156
|
if (context.exportIndex) context.exportIndex.patchEntry(exportName, {
|
|
24911
25157
|
value,
|
|
24912
25158
|
producerStack: refStack,
|
|
24913
25159
|
producerRegion: lookupRegion
|
|
24914
25160
|
}).catch((err) => {
|
|
24915
|
-
this.logger.debug(`Failed to patch exports index for '${exportName}': ${err instanceof Error ? err.message : String(err)}`);
|
|
25161
|
+
this.logger.debug(`Failed to patch exports index for '${this.maskSecretsForLog(exportName, context)}': ${this.maskSecretsForLog(err instanceof Error ? err.message : String(err), context)}`);
|
|
24916
25162
|
});
|
|
24917
25163
|
this.recordImport(context, exportName, refStack, lookupRegion);
|
|
24918
25164
|
found = {
|
|
@@ -24923,7 +25169,7 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
24923
25169
|
break;
|
|
24924
25170
|
}
|
|
24925
25171
|
} catch (error) {
|
|
24926
|
-
this.logger.warn(`Failed to read state for stack ${refStack}: ${error instanceof Error ? error.message : String(error)}`);
|
|
25172
|
+
this.logger.warn(`Failed to read state for stack ${this.maskSecretsForLog(refStack, context)}: ${this.maskSecretsForLog(error instanceof Error ? error.message : String(error), context)}`);
|
|
24927
25173
|
continue;
|
|
24928
25174
|
}
|
|
24929
25175
|
}
|
|
@@ -24939,7 +25185,7 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
24939
25185
|
return cfnExport.value;
|
|
24940
25186
|
}
|
|
24941
25187
|
}
|
|
24942
|
-
throw new Error(`Fn::ImportValue: export '${
|
|
25188
|
+
throw new Error(`Fn::ImportValue: export '${loggedExportName}' not found in any stack. Searched ${allStacks.length} cdkd state record(s)${this.cfnFallback ? " and CloudFormation exports" : ""}. Make sure the exporting stack has been deployed and the Output has an Export.Name property.`);
|
|
24943
25189
|
}
|
|
24944
25190
|
/**
|
|
24945
25191
|
* CloudFormation `ListExports` fallback lookup for `Fn::ImportValue`
|
|
@@ -24970,7 +25216,7 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
24970
25216
|
};
|
|
24971
25217
|
return;
|
|
24972
25218
|
} catch (error) {
|
|
24973
|
-
this.logger.warn(`Fn::ImportValue: CloudFormation ListExports fallback failed for export '${this.maskSecretsForLog(exportName, context)}' (region ${this.resolverRegion}): ${error instanceof Error ? error.message : String(error)}. Grant cloudformation:ListExports to resolve exports from CloudFormation-managed stacks, or pass --no-cfn-fallback to disable the fallback.`);
|
|
25219
|
+
this.logger.warn(`Fn::ImportValue: CloudFormation ListExports fallback failed for export '${this.maskSecretsForLog(exportName, context)}' (region ${this.resolverRegion}): ${this.maskSecretsForLog(error instanceof Error ? error.message : String(error), context)}. Grant cloudformation:ListExports to resolve exports from CloudFormation-managed stacks, or pass --no-cfn-fallback to disable the fallback.`);
|
|
24974
25220
|
return;
|
|
24975
25221
|
}
|
|
24976
25222
|
}
|
|
@@ -25040,7 +25286,7 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
25040
25286
|
return await fetch;
|
|
25041
25287
|
} catch (error) {
|
|
25042
25288
|
const message = error instanceof Error ? error.message : String(error);
|
|
25043
|
-
this.logger.warn(`Fn::GetStackOutput: CloudFormation DescribeStacks fallback failed for stack '${this.maskSecretsForLog(stackName, context)}' (${region}): ${message}. Grant cloudformation:DescribeStacks to resolve outputs from CloudFormation-managed stacks, or pass --no-cfn-fallback to disable the fallback.`);
|
|
25289
|
+
this.logger.warn(`Fn::GetStackOutput: CloudFormation DescribeStacks fallback failed for stack '${this.maskSecretsForLog(stackName, context)}' (${this.maskSecretsForLog(region, context)}): ${this.maskSecretsForLog(message, context)}. Grant cloudformation:DescribeStacks to resolve outputs from CloudFormation-managed stacks, or pass --no-cfn-fallback to disable the fallback.`);
|
|
25044
25290
|
return;
|
|
25045
25291
|
}
|
|
25046
25292
|
}
|
|
@@ -25149,7 +25395,7 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
25149
25395
|
const resolvedRegion = await this.resolveValue(args["Region"], context);
|
|
25150
25396
|
if (typeof resolvedRegion !== "string" || resolvedRegion === "") throw new Error(`Fn::GetStackOutput: Region must resolve to a non-empty string, got ${typeof resolvedRegion}`);
|
|
25151
25397
|
const requestedRegion = canonicalizeRegion(resolvedRegion);
|
|
25152
|
-
if (!isClientSafeRegion(requestedRegion)) throw new Error(`Fn::GetStackOutput: '${
|
|
25398
|
+
if (!isClientSafeRegion(requestedRegion)) throw new Error(`Fn::GetStackOutput: '${this.maskThenStripThenMask(resolvedRegion, context).slice(0, 64)}' is not a valid AWS region name. The region selects both the AWS endpoint and the state-file key, so cdkd will not use it.`);
|
|
25153
25399
|
region = requestedRegion;
|
|
25154
25400
|
}
|
|
25155
25401
|
let roleArn;
|
|
@@ -25158,10 +25404,11 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
25158
25404
|
if (typeof raw !== "string" || raw === "") throw new Error(`Fn::GetStackOutput: RoleArn must be a literal string in the template (no Ref / Fn::GetAtt / Fn::Sub allowed for cross-account references). Got ${raw === null ? "null" : Array.isArray(raw) ? "array" : typeof raw}${typeof raw === "object" ? ` (intrinsic shape: ${JSON.stringify(raw).slice(0, 80)})` : ""}.`);
|
|
25159
25405
|
roleArn = raw;
|
|
25160
25406
|
}
|
|
25161
|
-
if (!roleArn && context.stackName && context.stackName === stackName && canonicalizeRegion(region) === canonicalizeRegion(this.resolverRegion)) throw new Error(`Fn::GetStackOutput: cannot reference own stack '${stackName}' in the same region '${region}'`);
|
|
25407
|
+
if (!roleArn && context.stackName && context.stackName === stackName && canonicalizeRegion(region) === canonicalizeRegion(this.resolverRegion)) throw new Error(`Fn::GetStackOutput: cannot reference own stack '${this.maskSecretsForLog(stackName, context)}' in the same region '${this.maskSecretsForLog(region, context)}'`);
|
|
25162
25408
|
const loggedStackName = this.maskSecretsForLog(stackName, context);
|
|
25163
25409
|
const loggedOutputName = this.maskSecretsForLog(outputName, context);
|
|
25164
|
-
this.
|
|
25410
|
+
const loggedRegion = this.maskSecretsForLog(region, context);
|
|
25411
|
+
this.logger.debug(`Resolving Fn::GetStackOutput: StackName=${loggedStackName}, Region=${loggedRegion}, OutputName=${loggedOutputName}${roleArn ? `, RoleArn=${roleArn}` : ""}`);
|
|
25165
25412
|
const stateData = roleArn ? await this.getCrossAccountStackState(roleArn, stackName, region, context) : await this.getSameAccountStackState(stackName, region, context);
|
|
25166
25413
|
if (!stateData) {
|
|
25167
25414
|
if (!roleArn && this.cfnFallback) {
|
|
@@ -25169,24 +25416,24 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
25169
25416
|
if (cfnOutputs) {
|
|
25170
25417
|
if (!Object.hasOwn(cfnOutputs, outputName)) {
|
|
25171
25418
|
const available = this.describeAvailableOutputs(Object.keys(cfnOutputs), context);
|
|
25172
|
-
throw new Error(`Fn::GetStackOutput: output '${
|
|
25419
|
+
throw new Error(`Fn::GetStackOutput: output '${loggedOutputName}' not found in CloudFormation stack '${loggedStackName}' (${loggedRegion}). Available outputs: ${available}`);
|
|
25173
25420
|
}
|
|
25174
25421
|
const value = cfnOutputs[outputName];
|
|
25175
|
-
this.logger.info(`Resolved Fn::GetStackOutput: StackName=${loggedStackName}, Region=${
|
|
25422
|
+
this.logger.info(`Resolved Fn::GetStackOutput: StackName=${loggedStackName}, Region=${loggedRegion}, OutputName=${loggedOutputName} (from CloudFormation stack outputs; weak reference — producer is not cdkd-managed)`);
|
|
25176
25423
|
return value;
|
|
25177
25424
|
}
|
|
25178
25425
|
}
|
|
25179
|
-
throw new Error(`Fn::GetStackOutput: stack '${
|
|
25426
|
+
throw new Error(`Fn::GetStackOutput: stack '${loggedStackName}' not found in region '${loggedRegion}'${roleArn ? ` (cross-account via ${roleArn})` : ""}. ${!roleArn && this.cfnFallback ? "Searched cdkd state and CloudFormation stacks. Make sure the producer stack has been deployed (via cdkd or CloudFormation)." : `Make sure the producer stack has been deployed via cdkd.`}`);
|
|
25180
25427
|
}
|
|
25181
25428
|
const outputs = stateData.state.outputs ?? {};
|
|
25182
25429
|
if (!Object.hasOwn(outputs, outputName)) {
|
|
25183
25430
|
const available = this.describeAvailableOutputs(Object.keys(outputs), context);
|
|
25184
|
-
throw new Error(`Fn::GetStackOutput: output '${
|
|
25431
|
+
throw new Error(`Fn::GetStackOutput: output '${loggedOutputName}' not found in stack '${loggedStackName}' (${loggedRegion}). Available outputs: ${available}`);
|
|
25185
25432
|
}
|
|
25186
25433
|
const value = outputs[outputName];
|
|
25187
|
-
this.logger.info(`Resolved Fn::GetStackOutput: StackName=${loggedStackName}, Region=${
|
|
25434
|
+
this.logger.info(`Resolved Fn::GetStackOutput: StackName=${loggedStackName}, Region=${loggedRegion}, OutputName=${loggedOutputName}${roleArn ? `, RoleArn=${roleArn}` : ""} (${carriesDynamicReference(value) ? "redacted dynamic reference" : "literal value"})`);
|
|
25188
25435
|
if (!roleArn) this.recordOutputRead(context, stackName, region, outputName);
|
|
25189
|
-
if (roleArn && !context.skipDynamicReferences && carriesDynamicReference(value)) throw markNonRetryable(new CrossAccountSecretRefusalError(`Fn::GetStackOutput: output '${
|
|
25436
|
+
if (roleArn && !context.skipDynamicReferences && carriesDynamicReference(value)) throw markNonRetryable(new CrossAccountSecretRefusalError(`Fn::GetStackOutput: output '${loggedOutputName}' of stack '${loggedStackName}' (${loggedRegion}) is a redacted dynamic reference, and this is a CROSS-ACCOUNT reference (RoleArn ${roleArn}). cdkd will not resolve a producer account's secret with the consumer's credentials — a same-named secret in the consumer account would answer instead. Export a non-secret value (e.g. the secret's ARN) and resolve it in the consumer stack, or reference the producer stack from within its own account.`));
|
|
25190
25437
|
return await this.reresolveCrossStackValue(value, region, context, `Fn::GetStackOutput '${outputName}' (producer ${stackName} / ${region})`, sourceKey, {
|
|
25191
25438
|
stackName,
|
|
25192
25439
|
region,
|
|
@@ -25304,19 +25551,19 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
25304
25551
|
}
|
|
25305
25552
|
if (!map) {
|
|
25306
25553
|
if (hasDefaultValue) return await resolveDefault();
|
|
25307
|
-
throw new Error(`Fn::FindInMap: mapping '${mapName}' not found in Mappings section`);
|
|
25554
|
+
throw new Error(`Fn::FindInMap: mapping '${this.maskSecretsForLog(mapName, context)}' not found in Mappings section`);
|
|
25308
25555
|
}
|
|
25309
25556
|
const topLevel = Object.hasOwn(map, topLevelKey) ? map[topLevelKey] : void 0;
|
|
25310
25557
|
if (!topLevel || typeof topLevel !== "object") {
|
|
25311
25558
|
if (hasDefaultValue) return await resolveDefault();
|
|
25312
|
-
throw new Error(`Fn::FindInMap: top-level key '${topLevelKey}' not found in mapping '${mapName}'`);
|
|
25559
|
+
throw new Error(`Fn::FindInMap: top-level key '${this.maskSecretsForLog(topLevelKey, context)}' not found in mapping '${this.maskSecretsForLog(mapName, context)}'`);
|
|
25313
25560
|
}
|
|
25314
25561
|
if (!Object.hasOwn(topLevel, secondLevelKey)) {
|
|
25315
25562
|
if (hasDefaultValue) return await resolveDefault();
|
|
25316
|
-
throw new Error(`Fn::FindInMap: second-level key '${secondLevelKey}' not found in mapping '${mapName}' -> '${topLevelKey}'`);
|
|
25563
|
+
throw new Error(`Fn::FindInMap: second-level key '${this.maskSecretsForLog(secondLevelKey, context)}' not found in mapping '${this.maskSecretsForLog(mapName, context)}' -> '${this.maskSecretsForLog(topLevelKey, context)}'`);
|
|
25317
25564
|
}
|
|
25318
25565
|
const result = topLevel[secondLevelKey];
|
|
25319
|
-
this.logger.debug(`Resolved Fn::FindInMap: ${mapName}.${topLevelKey}.${secondLevelKey} -> ${JSON.stringify(result)}`);
|
|
25566
|
+
this.logger.debug(`Resolved Fn::FindInMap: ${this.maskSecretsForLog(mapName, context)}.${this.maskSecretsForLog(topLevelKey, context)}.${this.maskSecretsForLog(secondLevelKey, context)} -> ${this.maskSecretsForLog(JSON.stringify(this.maskValueLeaves(result, context)), context)}`);
|
|
25320
25567
|
return result;
|
|
25321
25568
|
}
|
|
25322
25569
|
/**
|
|
@@ -25329,6 +25576,7 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
25329
25576
|
const resolvedValue = await this.resolveValue(value, context);
|
|
25330
25577
|
if (typeof resolvedValue !== "string") throw new Error(`Fn::Base64: value must resolve to a string, got ${typeof resolvedValue}`);
|
|
25331
25578
|
const result = Buffer.from(resolvedValue).toString("base64");
|
|
25579
|
+
if (context.recordedSecretValues && this.maskSecretsForLog(resolvedValue, context) !== resolvedValue) recordMaskOnlyValue(context.recordedSecretValues, result);
|
|
25332
25580
|
this.logger.debug(`Resolved Fn::Base64: ${this.maskSecretsForLog(resolvedValue, context)} -> ${this.maskSecretsForLog(result, context)}`);
|
|
25333
25581
|
return result;
|
|
25334
25582
|
}
|
|
@@ -25360,7 +25608,7 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
25360
25608
|
let clientRegion;
|
|
25361
25609
|
if (typeof resolvedValue === "string" && resolvedValue !== "") {
|
|
25362
25610
|
const requested = canonicalizeRegion(resolvedValue);
|
|
25363
|
-
if (!isClientSafeRegion(requested)) throw new Error(`Fn::GetAZs: '${
|
|
25611
|
+
if (!isClientSafeRegion(requested)) throw new Error(`Fn::GetAZs: '${this.maskThenStripThenMask(resolvedValue, context).slice(0, 64)}' is not a valid AWS region name. A region is substituted into the AWS service hostname, so cdkd will not build a client from it.`);
|
|
25364
25612
|
region = requested;
|
|
25365
25613
|
clientRegion = requested;
|
|
25366
25614
|
} else {
|
|
@@ -25369,7 +25617,7 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
25369
25617
|
}
|
|
25370
25618
|
const cached = cachedAvailabilityZones[region];
|
|
25371
25619
|
if (cached) {
|
|
25372
|
-
this.logger.debug(`Resolved Fn::GetAZs from cache: ${region} -> ${JSON.stringify(cached)}`);
|
|
25620
|
+
this.logger.debug(`Resolved Fn::GetAZs from cache: ${this.maskSecretsForLog(region, context)} -> ${JSON.stringify(this.maskValueLeaves(cached, context))}`);
|
|
25373
25621
|
return cached;
|
|
25374
25622
|
}
|
|
25375
25623
|
const ec2Client = this.clientsForRegion(clientRegion).ec2;
|
|
@@ -25383,11 +25631,11 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
25383
25631
|
Values: ["available"]
|
|
25384
25632
|
}] }))).AvailabilityZones || []).map((az) => az.ZoneName).filter((name) => name !== void 0).sort();
|
|
25385
25633
|
} catch (error) {
|
|
25386
|
-
throw new Error(`Fn::GetAZs: failed to describe availability zones for region '${region}': ${error instanceof Error ? error.message : String(error)}`);
|
|
25634
|
+
throw new Error(`Fn::GetAZs: failed to describe availability zones for region '${this.maskSecretsForLog(region, context)}': ${this.maskSecretsForLog(error instanceof Error ? error.message : String(error), context)}`);
|
|
25387
25635
|
}
|
|
25388
|
-
if (azNames.length === 0) throw new Error(`Fn::GetAZs: no availability zones returned for region '${region}'. Either the region is not enabled on this account (opt-in regions must be enabled before use), or the request was answered by a different region's endpoint.`);
|
|
25636
|
+
if (azNames.length === 0) throw new Error(`Fn::GetAZs: no availability zones returned for region '${this.maskSecretsForLog(region, context)}'. Either the region is not enabled on this account (opt-in regions must be enabled before use), or the request was answered by a different region's endpoint.`);
|
|
25389
25637
|
cachedAvailabilityZones[region] = azNames;
|
|
25390
|
-
this.logger.debug(`Resolved Fn::GetAZs: ${region} -> ${JSON.stringify(azNames)}`);
|
|
25638
|
+
this.logger.debug(`Resolved Fn::GetAZs: ${this.maskSecretsForLog(region, context)} -> ${JSON.stringify(this.maskValueLeaves(azNames, context))}`);
|
|
25391
25639
|
return azNames;
|
|
25392
25640
|
}
|
|
25393
25641
|
/**
|
|
@@ -25433,6 +25681,95 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
25433
25681
|
if (secrets && secrets.size > 0) masked = maskSecretsInText(masked, secrets);
|
|
25434
25682
|
return masked;
|
|
25435
25683
|
}
|
|
25684
|
+
/**
|
|
25685
|
+
* A copy of `value` with every string LEAF (and every object KEY) masked, for
|
|
25686
|
+
* a caller about to ENCODE it into a message (issue
|
|
25687
|
+
* [#2759](https://github.com/go-to-k/cdkd/issues/2759)).
|
|
25688
|
+
*
|
|
25689
|
+
* `stringifyValue` / `JSON.stringify` ESCAPE a leaf containing `"`, `\` or a
|
|
25690
|
+
* control character, and {@link maskSecretsInText} matches a needle
|
|
25691
|
+
* LITERALLY — so masking the ENCODED text misses exactly the plaintexts the
|
|
25692
|
+
* encoder rewrote (`pa"ss\word12` encodes to `["pa\"ss\\word12"]`, which no
|
|
25693
|
+
* needle matches). Masking each leaf first also buys the WHOLE-VALUE arm,
|
|
25694
|
+
* which has no {@link MIN_NEEDLE_LENGTH} floor, for a leaf that IS the
|
|
25695
|
+
* plaintext.
|
|
25696
|
+
*
|
|
25697
|
+
* Returns the STRUCTURE rather than a rendered string, deliberately: each
|
|
25698
|
+
* call site keeps its own encoder, so this changes which characters are
|
|
25699
|
+
* masked and nothing about how a value RENDERS. Encoding here instead
|
|
25700
|
+
* dropped `JSON.stringify`'s quotes around a bare string and made a `cdkd
|
|
25701
|
+
* scrub` log line unrecognisable to its own test.
|
|
25702
|
+
*
|
|
25703
|
+
* Object KEYS are masked too: a `Fn::Split` / `Fn::GetAtt` chain can put a
|
|
25704
|
+
* resolved value in key position, and an unmasked key discloses exactly as
|
|
25705
|
+
* much as an unmasked value.
|
|
25706
|
+
*
|
|
25707
|
+
* Cycle-safe by MEMOIZATION rather than a depth cap: a self-referential
|
|
25708
|
+
* structure terminates (the replacement is registered before its children are
|
|
25709
|
+
* walked, so the cycle closes on it) and a legal deep one is still walked to
|
|
25710
|
+
* the bottom. A repeated but NON-cyclic sub-object gets its real rendering
|
|
25711
|
+
* rather than a placeholder — see the note at the `Map`.
|
|
25712
|
+
*/
|
|
25713
|
+
maskValueLeaves(value, context) {
|
|
25714
|
+
const done = /* @__PURE__ */ new Map();
|
|
25715
|
+
const walk = (node) => {
|
|
25716
|
+
if (typeof node === "string") return this.maskSecretsForLog(node, context);
|
|
25717
|
+
if (node === null || typeof node !== "object") return node;
|
|
25718
|
+
const memo = done.get(node);
|
|
25719
|
+
if (memo !== void 0) return memo;
|
|
25720
|
+
if (Array.isArray(node)) {
|
|
25721
|
+
const out = [];
|
|
25722
|
+
done.set(node, out);
|
|
25723
|
+
for (const item of node) out.push(walk(item));
|
|
25724
|
+
return out;
|
|
25725
|
+
}
|
|
25726
|
+
const out = Object.create(null);
|
|
25727
|
+
done.set(node, out);
|
|
25728
|
+
for (const [key, child] of Object.entries(node)) out[this.maskSecretsForLog(key, context)] = walk(child);
|
|
25729
|
+
return out;
|
|
25730
|
+
};
|
|
25731
|
+
return walk(value);
|
|
25732
|
+
}
|
|
25733
|
+
/**
|
|
25734
|
+
* Mask `value`, STRIP its control characters, then mask again — the shape a
|
|
25735
|
+
* message that truncates its input needs (issue
|
|
25736
|
+
* [#2827](https://github.com/go-to-k/cdkd/issues/2827) review round 1).
|
|
25737
|
+
*
|
|
25738
|
+
* NEITHER SINGLE ORDER IS CORRECT, and both were measured. Masking AFTER
|
|
25739
|
+
* `stripControlChars` is what this fix was written to avoid: the strip
|
|
25740
|
+
* rewrites the text a literal needle has to match. But masking BEFORE it is
|
|
25741
|
+
* not safe either, because `stripControlChars` DELETES rather than replaces —
|
|
25742
|
+
* so a plaintext SPLIT by an invisible (`S3cret\u200ePassw0rd`) is missed by
|
|
25743
|
+
* the first mask and then RECONSTITUTED contiguous by the strip. That is the
|
|
25744
|
+
* go-to-k/cdkd#2874 class arriving through a different door.
|
|
25745
|
+
*
|
|
25746
|
+
* Masking in BOTH string spaces closes both: the first pass catches a needle
|
|
25747
|
+
* that occurs literally, the second catches one that only becomes contiguous
|
|
25748
|
+
* after stripping. `maskSecretsInText` is idempotent, so the overlap costs
|
|
25749
|
+
* nothing, and the caller truncates AFTERWARDS — never between the two.
|
|
25750
|
+
*
|
|
25751
|
+
* WHICH HALF IS FENCED, stated because a mutation probe made the difference
|
|
25752
|
+
* visible. The FIRST mask is demonstrated by a test: deleting it (masking
|
|
25753
|
+
* only after the strip) reds the split-needle case, because the recorded
|
|
25754
|
+
* needle is then the split form and the strip has destroyed it. The SECOND
|
|
25755
|
+
* mask is NOT reached by any test here and is defence in depth: it earns its
|
|
25756
|
+
* place only when the bag holds a needle that is the STRIPPED form of the
|
|
25757
|
+
* value in hand, and every route through THIS resolver records the value it
|
|
25758
|
+
* actually resolved — so the split copy is itself a needle and the first
|
|
25759
|
+
* mask already catches it. The shape was measured against a hand-built bag
|
|
25760
|
+
* during review, not produced by the resolver. Kept anyway: it is one
|
|
25761
|
+
* idempotent call, and the alternative is re-deciding per future caller
|
|
25762
|
+
* whether the bag and the value can disagree.
|
|
25763
|
+
*
|
|
25764
|
+
* THE BOUND, since this file's job is to state them: this covers a needle
|
|
25765
|
+
* split by a character `stripControlChars` removes. A needle split by
|
|
25766
|
+
* anything else, or one whose canonical form differs for another reason, is
|
|
25767
|
+
* `outputs-export-alias.ts`'s `canonicalForSecretScan` problem and is not
|
|
25768
|
+
* solved here.
|
|
25769
|
+
*/
|
|
25770
|
+
maskThenStripThenMask(value, context) {
|
|
25771
|
+
return this.maskSecretsForLog(stripControlChars(this.maskSecretsForLog(value, context)), context);
|
|
25772
|
+
}
|
|
25436
25773
|
async resolveDynamicReferences(value, context) {
|
|
25437
25774
|
const pattern = /\{\{resolve:([^}]+)\}\}/g;
|
|
25438
25775
|
let result = value;
|
|
@@ -25447,7 +25784,7 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
25447
25784
|
const isKnownSecret = service === "secretsmanager" || service === "ssm-secure" || recordedSecretExpressions.has(fullMatch);
|
|
25448
25785
|
if (isKnownSecret && context?.skipDynamicReferences) continue;
|
|
25449
25786
|
const regionVerdict = classifyReplaySecretRegion(fullMatch, this.explicitRegion ?? this.resolverRegion, context?.producerRegions);
|
|
25450
|
-
if (regionVerdict.kind === "ambiguous") throw markNonRetryable(new DynamicReferenceRegionAmbiguousError(`Refusing to resolve the secret reference ${fullMatch}: it names '${regionVerdict.secretName}' without a region, and this stack reads from ${regionVerdict.foreignProducerRegions.join(", ")} as well as its own region. cdkd cannot tell which one must answer, and resolving against the wrong one yields a different secret. Spell the reference as a full ARN to say which region owns it.`));
|
|
25787
|
+
if (regionVerdict.kind === "ambiguous") throw markNonRetryable(new DynamicReferenceRegionAmbiguousError(`Refusing to resolve the secret reference ${this.maskSecretsForLog(fullMatch, context)}: it names '${this.maskSecretsForLog(regionVerdict.secretName, context)}' without a region, and this stack reads from ${regionVerdict.foreignProducerRegions.join(", ")} as well as its own region. cdkd cannot tell which one must answer, and resolving against the wrong one yields a different secret. Spell the reference as a full ARN to say which region owns it.`));
|
|
25451
25788
|
if (regionVerdict.kind === "named-region") {
|
|
25452
25789
|
const foreign = await this.resolverForProducerRegion(regionVerdict.region).resolveDynamicReferences(fullMatch, withoutProducerRegions(context));
|
|
25453
25790
|
result = result.replace(fullMatch, () => foreign);
|
|
@@ -25492,7 +25829,7 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
25492
25829
|
resolved = param.value;
|
|
25493
25830
|
} else if (service === "ssm-secure") {
|
|
25494
25831
|
const param = await this.resolveSSMReference(parts, true, "ssm-secure", context);
|
|
25495
|
-
if (!param.secure) throw markNonRetryable(new IntrinsicResolutionRefusalError(`Refusing to resolve ${fullMatch}: the parameter is a ${param.type} parameter, and the ssm-secure spelling is defined for SecureString parameters only. Reference it as {{resolve:ssm:...}} if it is public configuration.`));
|
|
25832
|
+
if (!param.secure) throw markNonRetryable(new IntrinsicResolutionRefusalError(`Refusing to resolve ${this.maskSecretsForLog(fullMatch, context)}: the parameter is a ${param.type} parameter, and the ssm-secure spelling is defined for SecureString parameters only. Reference it as {{resolve:ssm:...}} if it is public configuration.`));
|
|
25496
25833
|
isSecret = true;
|
|
25497
25834
|
resolved = param.value;
|
|
25498
25835
|
} else {
|
|
@@ -25556,22 +25893,24 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
25556
25893
|
} else secretId = afterService;
|
|
25557
25894
|
if (!versionStage) versionStage = "AWSCURRENT";
|
|
25558
25895
|
if (!secretId) throw new Error("Dynamic reference: secretsmanager SECRET_ID is required");
|
|
25559
|
-
this.
|
|
25896
|
+
const loggedSecretId = this.maskSecretsForLog(secretId, context);
|
|
25897
|
+
const loggedJsonKey = this.maskSecretsForLog(jsonKey, context);
|
|
25898
|
+
this.logger.debug(`Resolving dynamic reference: secretsmanager:${loggedSecretId}:SecretString:${loggedJsonKey}:${this.maskSecretsForLog(versionStage, context)}:${this.maskSecretsForLog(versionId, context)}`);
|
|
25560
25899
|
const client = this.clientsForRegion(this.explicitRegion).secretsManager;
|
|
25561
25900
|
const command = new GetSecretValueCommand({
|
|
25562
25901
|
SecretId: secretId,
|
|
25563
25902
|
...versionStage && versionStage !== "" && { VersionStage: versionStage },
|
|
25564
25903
|
...versionId && versionId !== "" && { VersionId: versionId }
|
|
25565
25904
|
});
|
|
25566
|
-
const secretString = (await this.sendWithThrottleRetry(() => client.send(command),
|
|
25567
|
-
if (!secretString) throw new Error(`Dynamic reference: secret '${
|
|
25905
|
+
const secretString = (await this.sendWithThrottleRetry(() => client.send(command), `secretsmanager:${loggedSecretId}`)).SecretString;
|
|
25906
|
+
if (!secretString) throw new Error(`Dynamic reference: secret '${loggedSecretId}' does not contain a SecretString value`);
|
|
25568
25907
|
if (jsonKey) try {
|
|
25569
25908
|
const parsed = JSON.parse(secretString);
|
|
25570
25909
|
const keyValue = Object.hasOwn(parsed, jsonKey) ? parsed[jsonKey] : void 0;
|
|
25571
|
-
if (keyValue === void 0) throw new Error(`Dynamic reference: key '${
|
|
25910
|
+
if (keyValue === void 0) throw new Error(`Dynamic reference: key '${loggedJsonKey}' not found in secret '${loggedSecretId}'`);
|
|
25572
25911
|
return stringifyValue(keyValue);
|
|
25573
25912
|
} catch (error) {
|
|
25574
|
-
if (error instanceof SyntaxError) throw new Error(`Dynamic reference: secret '${
|
|
25913
|
+
if (error instanceof SyntaxError) throw new Error(`Dynamic reference: secret '${loggedSecretId}' is not valid JSON but JSON_KEY '${loggedJsonKey}' was specified`);
|
|
25575
25914
|
throw error;
|
|
25576
25915
|
}
|
|
25577
25916
|
return secretString;
|
|
@@ -25597,8 +25936,8 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
25597
25936
|
const ipBlock = await this.resolveValue(rawIpBlock, context);
|
|
25598
25937
|
const count = Number(await this.resolveValue(rawCount, context));
|
|
25599
25938
|
const cidrBits = Number(await this.resolveValue(rawCidrBits, context));
|
|
25600
|
-
if (!ipBlock || typeof ipBlock !== "string") throw new Error(`Fn::Cidr: ipBlock must be a string, got ${typeof ipBlock}: ${JSON.stringify(ipBlock)}`);
|
|
25601
|
-
this.logger.debug(`Resolving Fn::Cidr: ipBlock=${ipBlock}, count=${count}, cidrBits=${cidrBits}`);
|
|
25939
|
+
if (!ipBlock || typeof ipBlock !== "string") throw new Error(`Fn::Cidr: ipBlock must be a string, got ${typeof ipBlock}: ${JSON.stringify(this.maskValueLeaves(ipBlock, context))}`);
|
|
25940
|
+
this.logger.debug(`Resolving Fn::Cidr: ipBlock=${this.maskSecretsForLog(JSON.stringify(this.maskValueLeaves(ipBlock, context)), context)}, count=${count}, cidrBits=${cidrBits}`);
|
|
25602
25941
|
const isIpv6 = ipBlock.includes(":");
|
|
25603
25942
|
const results = [];
|
|
25604
25943
|
if (isIpv6) {
|
|
@@ -25630,7 +25969,7 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
25630
25969
|
results.push(`${a}.${b}.${c}.${d}/${subnetPrefix}`);
|
|
25631
25970
|
}
|
|
25632
25971
|
}
|
|
25633
|
-
this.logger.debug(`Fn::Cidr result: ${JSON.stringify(results)}`);
|
|
25972
|
+
this.logger.debug(`Fn::Cidr result: ${this.maskSecretsForLog(JSON.stringify(this.maskValueLeaves(results, context)), context)}`);
|
|
25634
25973
|
return results;
|
|
25635
25974
|
}
|
|
25636
25975
|
/** Expand IPv6 address to full 8-group form */
|
|
@@ -25720,21 +26059,22 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
25720
26059
|
async resolveSSMReference(parts, decrypt = true, service = "ssm", context) {
|
|
25721
26060
|
const parameterName = parts.slice(1).join(":");
|
|
25722
26061
|
if (!parameterName) throw new Error(`Dynamic reference: ${service} PARAMETER_NAME is required`);
|
|
25723
|
-
this.
|
|
26062
|
+
const loggedParameterName = this.maskSecretsForLog(parameterName, context);
|
|
26063
|
+
this.logger.debug(`Resolving dynamic reference: ${service}:${loggedParameterName}`);
|
|
25724
26064
|
const client = this.clientsForRegion(this.explicitRegion).ssm;
|
|
25725
26065
|
const command = new GetParameterCommand({
|
|
25726
26066
|
Name: parameterName,
|
|
25727
26067
|
WithDecryption: decrypt
|
|
25728
26068
|
});
|
|
25729
|
-
const response = await this.sendWithThrottleRetry(() => client.send(command),
|
|
26069
|
+
const response = await this.sendWithThrottleRetry(() => client.send(command), `${service}:${loggedParameterName}`);
|
|
25730
26070
|
const paramValue = response.Parameter?.Value;
|
|
25731
|
-
if (paramValue === void 0 || paramValue === null) throw new Error(`Dynamic reference: SSM parameter '${
|
|
26071
|
+
if (paramValue === void 0 || paramValue === null) throw new Error(`Dynamic reference: SSM parameter '${loggedParameterName}' not found or has no value`);
|
|
25732
26072
|
const paramType = response.Parameter?.Type;
|
|
25733
26073
|
const secure = paramType !== "String" && paramType !== "StringList";
|
|
25734
26074
|
if (secure && paramType !== "SecureString" && !this.warnedUnrecognizedSsmTypes.has(`${parameterName}\u0000${String(paramType)}`)) {
|
|
25735
26075
|
this.warnedUnrecognizedSsmTypes.add(`${parameterName}\u0000${String(paramType)}`);
|
|
25736
26076
|
const reported = paramType === void 0 ? "(absent)" : `'${String(paramType)}'`;
|
|
25737
|
-
this.logger.warn(
|
|
26077
|
+
this.logger.warn(`SSM parameter '${loggedParameterName}' reported an unrecognized Type ${reported} — treating its value as a secret, so cdkd will persist the {{resolve:${service}:...}} expression rather than the resolved value. Declare the parameter as String / StringList if it is public config.`);
|
|
25738
26078
|
}
|
|
25739
26079
|
return {
|
|
25740
26080
|
value: paramValue,
|
|
@@ -26943,7 +27283,7 @@ var CloudControlProvider = class {
|
|
|
26943
27283
|
const indeterminateGuard = await this.confirmDeleteTargetIdentity(logicalId, resourceType, physicalId, context);
|
|
26944
27284
|
if (context?.removeProtection === true && resourceType === "AWS::AutoScaling::AutoScalingGroup") {
|
|
26945
27285
|
this.logger.debug(`Delegating protected AutoScalingGroup ${logicalId} delete to the SDK ASGProvider (Cloud Control cannot force-delete a protected ASG)`);
|
|
26946
|
-
const { ASGProvider } = await import("./asg-provider-
|
|
27286
|
+
const { ASGProvider } = await import("./asg-provider-C_AHAmgD.js").then((n) => n.n);
|
|
26947
27287
|
const asgProvider = new ASGProvider();
|
|
26948
27288
|
return withIndeterminateGuard(await asgProvider.delete(logicalId, physicalId, resourceType, _properties, context), indeterminateGuard);
|
|
26949
27289
|
}
|
|
@@ -36438,7 +36778,7 @@ var DeployEngine = class {
|
|
|
36438
36778
|
const next = { ...op };
|
|
36439
36779
|
if (next.properties) next.properties = redactSecretsForState(next.properties, ownSecrets, templateProps);
|
|
36440
36780
|
if (next.attemptedProperties) next.attemptedProperties = redactSecretsForState(markSameGenerationBag({ ...next.attemptedProperties }), ownSecrets, templateProps);
|
|
36441
|
-
if (next.previousState) next.previousState = scrubResourceRecord(next.previousState, ownSecrets);
|
|
36781
|
+
if (next.previousState) next.previousState = scrubResourceRecord(next.previousState, ownSecrets, void 0, STATE_SOURCED_READBACK_RULES);
|
|
36442
36782
|
return next;
|
|
36443
36783
|
});
|
|
36444
36784
|
}
|
|
@@ -37366,6 +37706,8 @@ var DeployEngine = class {
|
|
|
37366
37706
|
renderer.removeTask(logicalId);
|
|
37367
37707
|
const message = error instanceof Error ? error.message : String(error);
|
|
37368
37708
|
this.logger.error(this.maskForResource(logicalId, `Failed to ${change.changeType.toLowerCase()} ${logicalId}: ${message}`));
|
|
37709
|
+
const orphanAdvice = this.orphanedNameCollisionAdvice(change.changeType, logicalId, error);
|
|
37710
|
+
if (orphanAdvice) this.logger.error(this.maskForResource(logicalId, orphanAdvice));
|
|
37369
37711
|
this.recordEvent({
|
|
37370
37712
|
eventType: "RESOURCE_FAILED",
|
|
37371
37713
|
stackName,
|
|
@@ -38058,6 +38400,99 @@ var DeployEngine = class {
|
|
|
38058
38400
|
};
|
|
38059
38401
|
}
|
|
38060
38402
|
/**
|
|
38403
|
+
* The plain-CREATE sibling of {@link replacementNameOrigin} (issue
|
|
38404
|
+
* [#2902](https://github.com/go-to-k/cdkd/issues/2902)).
|
|
38405
|
+
*
|
|
38406
|
+
* A CREATE that collides on a name cdkd DERIVED is very likely a resource
|
|
38407
|
+
* cdkd itself left behind: `DeletionPolicy: Retain` makes a rollback drop
|
|
38408
|
+
* the state record while leaving the resource in AWS (CloudFormation
|
|
38409
|
+
* semantics, and deliberate), and cdkd's generated names carry no random
|
|
38410
|
+
* component (`generateResourceName`) — so the next deploy asks AWS for
|
|
38411
|
+
* exactly the name the orphan still holds, fails, rolls back again, and
|
|
38412
|
+
* repeats forever. Before this, the user saw only the bare AWS sentence:
|
|
38413
|
+
* nothing named the collision's cause and nothing named a way out, so the
|
|
38414
|
+
* reported recovery was hand-deleting resources through the AWS API.
|
|
38415
|
+
*
|
|
38416
|
+
* CloudFormation never shows this because its generated names carry a
|
|
38417
|
+
* random suffix, so a retained orphan cannot collide with a later deploy.
|
|
38418
|
+
* The breakage is that combination — CFn's retain semantics with cdkd's
|
|
38419
|
+
* deterministic naming — rather than either half, which is why the fix here
|
|
38420
|
+
* is a diagnosis and a remedy rather than a behaviour change. Whether cdkd
|
|
38421
|
+
* should instead RE-ADOPT the retained resource is issue
|
|
38422
|
+
* [#2914](https://github.com/go-to-k/cdkd/issues/2914).
|
|
38423
|
+
*
|
|
38424
|
+
* Returns `undefined` — leaving the pre-existing wording untouched — for
|
|
38425
|
+
* every case it cannot vouch for:
|
|
38426
|
+
*
|
|
38427
|
+
* - not a CREATE. A replacement collision DOES arrive here — the
|
|
38428
|
+
* `NAMED_REPLACEMENT_COLLISION` throws happen inside `provisionResourceBody`,
|
|
38429
|
+
* which the caller invokes inside the same `try`, and this method's own
|
|
38430
|
+
* suite asserts their line was logged. What refuses them is the
|
|
38431
|
+
* `ProvisioningError` check below (they throw `CdkdError`), so deleting
|
|
38432
|
+
* EITHER guard alone leaves the suite green. That does not make this one
|
|
38433
|
+
* dead: a non-CREATE `ProvisioningError` whose message carries
|
|
38434
|
+
* `already exists` — an UPDATE-path sub-resource conflict — would reach
|
|
38435
|
+
* the advice without it, and the replacement message's remedy is to
|
|
38436
|
+
* RENAME, which does not recover an orphan. (An earlier revision of this
|
|
38437
|
+
* comment claimed the throws "never reach this catch at all". Three
|
|
38438
|
+
* reviewers disproved it independently.);
|
|
38439
|
+
* - not a name collision;
|
|
38440
|
+
* - no physical id on the error (a create that failed BEFORE the AWS call
|
|
38441
|
+
* never names one). At RUNTIME this is subsumed by the next guard --
|
|
38442
|
+
* `looksLikeCdkdGeneratedName` refuses a falsy id on its own first line,
|
|
38443
|
+
* measured: deleting this check ALONE leaves the suite green, deleting
|
|
38444
|
+
* both together reds it. It stays for the TYPE narrowing the message
|
|
38445
|
+
* interpolation needs, and so the refusal is readable here rather than
|
|
38446
|
+
* inferred from another module;
|
|
38447
|
+
* - a name cdkd did not derive — a user-supplied name may collide with a
|
|
38448
|
+
* resource of someone else's entirely, and telling that user to
|
|
38449
|
+
* `cdkd import` it would be advice to adopt what this stack does not own;
|
|
38450
|
+
* - a NESTED-STACK child. Its stack name is `<parent>~<logicalId>`, and CDK's
|
|
38451
|
+
* own stack-name rule bars `~`, so no Cloud Assembly stack can ever carry
|
|
38452
|
+
* it — `cdkd import` resolves its target from the assembly and walks
|
|
38453
|
+
* top-level stacks only, so the command would be unrunnable. That is the
|
|
38454
|
+
* same #2610 class this method's `canImport` check exists for, one level
|
|
38455
|
+
* down, so the child takes the delete-only arm.
|
|
38456
|
+
*
|
|
38457
|
+
* **A cdkd-DERIVED name is not proof the resource is THIS stack's**, which
|
|
38458
|
+
* the first revision of this advice assumed. Two ways it is not, both
|
|
38459
|
+
* reachable: a globally-namespaced type (`AWS::S3::Bucket` is the documented
|
|
38460
|
+
* exception — see `.claude/rules/provider-resource-identity.md`) can collide
|
|
38461
|
+
* with ANOTHER ACCOUNT's resource, and because the derivation is predictable
|
|
38462
|
+
* that name can be pre-registered by someone else; and the same stack name
|
|
38463
|
+
* deployed in two REGIONS derives the same name for a global type, so the
|
|
38464
|
+
* collision is with a live resource another state file already owns —
|
|
38465
|
+
* importing it would give two stacks one resource, and either `cdkd destroy`
|
|
38466
|
+
* would then delete it out from under the other. So the message names the
|
|
38467
|
+
* orphan as the LIKELY case rather than the certain one, and asks the reader
|
|
38468
|
+
* to confirm ownership before adopting.
|
|
38469
|
+
*/
|
|
38470
|
+
orphanedNameCollisionAdvice(changeType, logicalId, error) {
|
|
38471
|
+
if (changeType !== "CREATE") return void 0;
|
|
38472
|
+
if (!(error instanceof ProvisioningError)) return void 0;
|
|
38473
|
+
const physicalId = error.physicalId;
|
|
38474
|
+
if (!physicalId) return void 0;
|
|
38475
|
+
if (!isNameCollisionError(error.message)) return void 0;
|
|
38476
|
+
const stackName = getCurrentStackName();
|
|
38477
|
+
if (!looksLikeCdkdGeneratedName(physicalId, logicalId, stackName)) return void 0;
|
|
38478
|
+
if (!stackName) return void 0;
|
|
38479
|
+
const safeId = displaySafe(physicalId, { asciiOnly: true });
|
|
38480
|
+
const safeStack = displaySafe(stackName, { asciiOnly: true });
|
|
38481
|
+
const safeLogicalId = displaySafe(logicalId, { asciiOnly: true });
|
|
38482
|
+
const diagnosis = `${safeLogicalId}: the name AWS reports as taken (${safeId}) is one cdkd DERIVED from the logical id, and that derivation has no random component — so this is most likely a resource an earlier cdkd run left behind. A rollback leaves a resource carrying DeletionPolicy: Retain in AWS and drops it from state, as CloudFormation does; what differs is the name. CloudFormation would generate a fresh one for an unnamed resource and redeploy clean, whereas cdkd asks again for the name the orphan still holds — so re-running does not clear this.`;
|
|
38483
|
+
const deleteArm = `If it is not a resource you want to keep, delete ${safeId} in AWS — after confirming it holds nothing you need, since Retain is what kept it — and re-deploy.`;
|
|
38484
|
+
let canImport;
|
|
38485
|
+
try {
|
|
38486
|
+
canImport = typeof this.providerRegistry.getProvider(error.resourceType).import === "function";
|
|
38487
|
+
} catch {
|
|
38488
|
+
canImport = false;
|
|
38489
|
+
}
|
|
38490
|
+
const commandNamesTheRightResource = safeId === physicalId && safeStack === stackName && safeLogicalId === logicalId;
|
|
38491
|
+
const importableTarget = !stackName.includes("~");
|
|
38492
|
+
if (!canImport || !commandNamesTheRightResource || !importableTarget) return `${diagnosis} ${!canImport ? `cdkd cannot adopt ${displaySafe(error.resourceType, { asciiOnly: true })} back into state (its provider implements no import)` : !importableTarget ? `this is a nested-stack child, whose stack name cdkd import cannot resolve` : `cdkd cannot render an import command that provably names this resource`}, so the way forward is to delete it. ${deleteArm}`;
|
|
38493
|
+
return `${diagnosis} To recover, adopt it back into state instead of re-creating it: cdkd import ${shellQuote(safeStack)} --resource ${shellQuote(`${safeLogicalId}=${safeId}`)} (a selective import merges into existing state and needs no --force while the resource is absent from it). CONFIRM IT IS YOURS FIRST — a name cdkd derives is predictable, so for a globally-namespaced type it can belong to another account, and the same stack deployed in another region derives the same name. ${deleteArm}`;
|
|
38494
|
+
}
|
|
38495
|
+
/**
|
|
38061
38496
|
* Read `DeletionPolicy` / `UpdateReplacePolicy` from the synth template
|
|
38062
38497
|
* so they can be persisted in `ResourceState` (schema v5+). Always returns
|
|
38063
38498
|
* both keys (`undefined` when the template does not carry the attribute)
|
|
@@ -38422,5 +38857,5 @@ var DeployEngine = class {
|
|
|
38422
38857
|
};
|
|
38423
38858
|
|
|
38424
38859
|
//#endregion
|
|
38425
|
-
export { ProviderRegistry as $,
|
|
38426
|
-
//# sourceMappingURL=deploy-engine-
|
|
38860
|
+
export { ProviderRegistry as $, formatDockerLoginError as $n, PartialFailureError as $r, STATE_SOURCED_BASELINE_RULES as $t, renderStatefulReason as A, stringifyValue as An, PARTITION_TABLE as Ar, configStringRefusal as At, red as B, getBootstrapMarkerKey as Bn, setAwsClients as Br, s3BucketDomainName as Bt, refusesFinalSnapshot as C, CUSTOM_RESOURCE_RESPONSE_PREFIX as Cn, CFN_TEMPLATE_URL_LIMIT as Cr, refStateLookupFromResource as Ct, MULTI_REGION_RECREATE_BLOCKED_TYPES as D, importableOutputs as Dn, CUSTOM_RESOURCE_RESPONSE_OBJECT_DESCRIPTION as Dr, assertRegionMatch as Dt, extractDeploymentEventError as E, importableOutputKeys as En, uploadCfnTemplate as Er, resolveExplicitPhysicalId as Et, formatResourceLine as F, rewriteTemplateAssetReferences as Fn, clearBucketRegionCache as Fr, requireConfigString as Ft, isExportAliasCollision as G, validateContainerRepoName as Gn, DependencyError as Gr, DiffCalculator as Gt, collectDeclaredOutputNames as H, parseBootstrapMarker as Hn, CdkdError as Hr, s3BucketRegionalDomainName as Ht, bold as I, AssetModeResolver as In, resolveBucketRegion as Ir, classifyReplaySecretRegion as It, secretSafeKeyDisplay as J, buildDockerImage as Jn, IntrinsicResolutionRefusalError as Jr, findSilentDropProperties as Jt, secretBearing as K, buildDenyExternalAccessPolicy as Kn, DeployCancelledError as Kr, INTRINSIC_KEYS as Kt, cyan as L, BOOTSTRAP_MARKER_PREFIX as Ln, AwsClients as Lr, producerRegionsFromState as Lt, coerceWarmThroughput as M, buildAssetRedirectMap as Mn, derivePartitionAndUrlSuffix as Mr, replayWarn as Mt, isWarmThroughputDecrease as N, createAssetRedirectResolver as Nn, AssemblyReader as Nr, requireConfigArray as Nt, isStatefulRecreateTargetForReplace as O, shouldRetainResource as On, displaySafe as Or, coerceCfnBoolean as Ot, toFiniteNumber as P, loadPublishableAssetManifest as Pn, processStackMessages as Pr, requireConfigObject as Pt, clearOnUpdateRemoval as Q, dockerSpawnEnvWithSensitive as Qn, NestedStackChildDirectDestroyError as Qr, TemplateParser as Qt, gray as R, assertAssetBucketRegion as Rn, getAwsClients as Rr, withSharedDrainBudget as Rt, isFinalSnapshotError as S, shellQuote as Sn, CFN_TEMPLATE_BODY_LIMIT as Sr, parameterTypeMayLoseSecretIdentity as St, makeCanonicalizePropertiesFn as T, exportNamesCarriedFrom as Tn, findLargeInlineResources as Tr, normalizeAwsTagsToCfn as Tt, collectPublishedOutputNames as U, readBootstrapMarkerBody as Un, ConfigError as Ur, s3BucketWebsiteUrl as Ut, yellow as V, isCrossRegionRedirect as Vn, AssetError as Vr, s3BucketDualStackDomainName as Vt, exportAliasCollisionScrubWarning as W, validateAssetBucketName as Wn, CrossAccountSecretRefusalError as Wr, applyRoleArnIfSet as Wt, IAMRoleProvider as X, describeDockerExecFailure as Xn, LocalStartServiceError as Xr, withRetry as Xt, getCurrentResourceSecrets as Y, describeDockerCapturedOutput as Yn, LocalInvokeBuildError as Yr, describeTypeWithThrottleRetry as Yt, collectInlinePolicyNamesManagedBySiblings as Z, describeDockerFailure as Zn, LockError as Zr, DagBuilder as Zt, ATOMIC_FINAL_SNAPSHOT_TYPES as _, retryClassificationText as _i, rebuildClientForBucketRegion as _n, resolveStateBucketWithDefault as _r, carriesDynamicReference as _t, DeploymentEventsStore as a, StateError as ai, dynamicReferenceTokens as an, escapeRegExp$1 as ar, endCommandInterruptScope as at, ccRoutedFinalSnapshotError as b, buildLockContentionMessage as bn, stateBucketExistenceConfirmed as br, getAccountInfo as bt, replayFailedOperations as c, isCdkdError as ci, isSingleDynamicReferenceToken as cn, getDockerImageBySourceHash as cr, startInterruptWatch as ct, updatePartialReason as d, isMarkedNonRetryable as di, recordMaskOnlyValue as dn, getDefaultStateBucketName as dr, UNSPECIFIED_SKIP_REASON as dt, ProvisioningError as ei, STATE_SOURCED_CROSS_GENERATION_RULES as en, getDockerCmd as er, wouldReturnToSdkProvider as et, withResourceDeadline as f, isRetryableTransientError as fi, recoverMaskedOutput as fn, getLegacyStateBucketName as fr, deleteIndeterminateGuards as ft, bindingSkippedOutputs as g, markRedactedCause as gi, S3StateBackend as gn, resolveSkipPrefix as gr, IntrinsicFunctionResolver as gt, computeImplicitDeleteEdges as h, markNonRetryable as hi, LockManager as hn, resolveCaptureObservedState as hr, isTerminationProtectionPropagationError as ht, DeploymentEventsReader as i, StackTerminationProtectionError as ii, createSecretMasker as in, runDockerStreaming as ir, beginCommandInterruptScope as it, WARM_THROUGHPUT_MEMBERS as j, WorkGraph as jn, canonicalizeRegion as jr, readConfigString as jt, isStatefulRecreateTargetSync as k, AssetPublisher as kn, expectedOwnerParam as kr, configBooleanRefusal as kt, replayRollback as l, normalizeAwsError as li, maskSecretsInError as ln, Synthesizer as lr, CloudControlProvider as lt, IMPLICIT_DELETE_DEPENDENCIES as m, isTransientServerError as mi, scrubResourceRecord as mn, resolveAutoAssetStorage as mr, disableInstanceApiTermination as mt, DEFAULT_RESOURCE_WARN_AFTER_MS as n, ResourceUpdateNotSupportedError as ni, TEMPLATE_SOURCED_RULES as nn, redactDockerArgvValues as nr, maskDeep as nt, planFailedOps as o, SynthesisError as oi, errorCauseChain as on, stripControlChars as or, interruptWatchListenerCount as ot, maskingRetryLogger as p, isThrottlingError as pi, redactSecretsForState as pn, resolveApp as pr, deleteSkipReason as pt, secretBearingStateKeyWarning as q, describeAwsFailure as qn, DynamicReferenceRegionAmbiguousError as qr, findActionableSilentDrops as qt, DeployEngine as r, StackHasActiveImportsError as ri, carriesSecretMask as rn, runDockerForeground as rr, maskerOrIdentity as rt, planRollback as s, formatError as si, identityKeyFor as sn, AssetManifestLoader as sr, isInterruptedWaitError as st, DEFAULT_RESOURCE_TIMEOUT_MS as t, ResourceTimeoutError as ti, STATE_SOURCED_READBACK_RULES as tn, partitionSensitiveEnv as tr, createMaskedRetryLogger as tt, updatePartialMessage as u, withErrorHandling as ui, maskSecretsInText as un, synthesisStatusMessage as ur, slowCcOperationTimeoutMs as ut, PRE_DELETE_SNAPSHOT_TYPES as v, __exportAll as vi, UNRENDERABLE as vn, resolveStateBucketWithDefaultAndSource as vr, cfnRefValueFromPhysicalId as vt, unsupportedFinalSnapshotError as w, DEFAULT_STATE_PREFIX as wn, MIGRATE_TMP_PREFIX as wr, WAFv2WebACLProvider as wt, createPreDeleteFinalSnapshot as x, forceQuitRecoveryClause as xn, warnDeprecatedNoPrefixCliFlag as xr, isUnboundTemplateParameter as xt, buildFinalSnapshotIdentifier as y, buildForceUnlockCommand as yn, resolveUseCdkBootstrapAssets as yr, coerceParameterTypedValue as yt, green as z, ensureAssetStorage as zn, resetAwsClients as zr, s3BucketArn as zt };
|
|
38861
|
+
//# sourceMappingURL=deploy-engine-Dsf9bTqH.js.map
|