@go-to-k/cdkd 0.288.4 → 0.288.6
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-CC6BMuNK.js} +2 -2
- package/dist/{asg-provider-TexsJufw.js.map → asg-provider-CC6BMuNK.js.map} +1 -1
- package/dist/cli.js +2 -2
- package/dist/{deploy-engine-BiPxTgKT.js → deploy-engine-umIP3xus.js} +1338 -165
- package/dist/deploy-engine-umIP3xus.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/{program-Ci4Y5UO9.js → program-CxMjCbiG.js} +589 -57
- package/dist/{program-Ci4Y5UO9.js.map → program-CxMjCbiG.js.map} +1 -1
- package/dist/{version-BmM4QJ9t.js → version-4xTzOaA1.js} +2 -2
- package/dist/{version-BmM4QJ9t.js.map → version-4xTzOaA1.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-4xTzOaA1.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,54 @@ 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 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
|
+
* by `cdkd import`'s observed capture (issue
|
|
12289
|
+
* [#2885](https://github.com/go-to-k/cdkd/issues/2885), which moved it off the
|
|
12290
|
+
* non-failing constant), and derived by {@link scrubResourceRecord} for the
|
|
12291
|
+
* observed bag — the deploy's `drainObservedCaptures` baseline reaches it that
|
|
12292
|
+
* way.
|
|
12293
|
+
*
|
|
12294
|
+
* WHAT SELECTS IT IS THE DESTINATION, which is why it cannot be derived from
|
|
12295
|
+
* the two bags: `cdkd drift --accept` walks with the same shape flags and then
|
|
12296
|
+
* writes its result into `properties` for a record with no
|
|
12297
|
+
* `observedProperties`, where a mask is a REGRESSION rather than a refusal
|
|
12298
|
+
* (`cdkd export` blocks such a record and the rollback replay refuses the
|
|
12299
|
+
* operation). A caller declares this constant when it knows its bag becomes a
|
|
12300
|
+
* drift baseline and nothing else.
|
|
12301
|
+
*/
|
|
12302
|
+
const STATE_SOURCED_BASELINE_RULES = {
|
|
12303
|
+
descendArrays: false,
|
|
12304
|
+
trustAnyExpression: true,
|
|
12305
|
+
sourceIsSameGeneration: true,
|
|
12306
|
+
failClosedOnUncertifiedPositions: true
|
|
12307
|
+
};
|
|
12308
|
+
/**
|
|
12267
12309
|
* A STATE source that is no longer this bag's own generation — `cdkd scrub`'s
|
|
12268
12310
|
* `observedProperties` walk (issue #1917 review).
|
|
12269
12311
|
*
|
|
@@ -12409,7 +12451,7 @@ function isSecretExpressionByVerdictOrSpelling(expression) {
|
|
|
12409
12451
|
* so the position source is present with no map beside it.
|
|
12410
12452
|
*
|
|
12411
12453
|
* `cdkd state refresh-observed` and the deploy's `drainObservedCaptures` are
|
|
12412
|
-
* NOT affected: they take `
|
|
12454
|
+
* NOT affected: they take a `STATE_SOURCED_*` readback constant, which sets
|
|
12413
12455
|
* `sourceIsSameGeneration`, so {@link refuseUncertifiedReadbackPositions}
|
|
12414
12456
|
* restores the source even under the old strict class.
|
|
12415
12457
|
*
|
|
@@ -13324,7 +13366,7 @@ function redactByPath(bag, source, secrets, rules, secretExpressions, bagIsSameG
|
|
|
13324
13366
|
}
|
|
13325
13367
|
if (rules.descendArrays && bag.length === source.length) return bag.map((item, i) => redactByPath(item, source[i], secrets, rules, secretExpressions, bagIsSameGeneration));
|
|
13326
13368
|
}
|
|
13327
|
-
if (isPlainObject$2(bag) && isPlainObject$2(source)) {
|
|
13369
|
+
if (isPlainObject$2(bag) && hasPlainPrototype(bag) && isPlainObject$2(source)) {
|
|
13328
13370
|
const out = Object.create(null);
|
|
13329
13371
|
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
13372
|
return out;
|
|
@@ -13335,7 +13377,10 @@ function redactByPath(bag, source, secrets, rules, secretExpressions, bagIsSameG
|
|
|
13335
13377
|
* Is this rules constant one whose BAG is an AWS readback and whose SOURCE is a
|
|
13336
13378
|
* persisted STATE bag?
|
|
13337
13379
|
*
|
|
13338
|
-
* Today that is {@link STATE_SOURCED_READBACK_RULES}
|
|
13380
|
+
* Today that is {@link STATE_SOURCED_READBACK_RULES} and its fail-closed twin
|
|
13381
|
+
* {@link STATE_SOURCED_BASELINE_RULES}, which differ on nothing this predicate
|
|
13382
|
+
* reads (issue #2852 added a DESTINATION flag, not a shape one): the path where
|
|
13383
|
+
* the
|
|
13339
13384
|
* secrets map can be EMPTY by construction (nothing was resolved), so the value
|
|
13340
13385
|
* scan has no needles and POSITION is the only mechanism left. Derived from the
|
|
13341
13386
|
* flags rather than compared against the constant so a future one with the same
|
|
@@ -13831,6 +13876,23 @@ function unkeyedArrayPairsByAnchors(bag, source) {
|
|
|
13831
13876
|
*/
|
|
13832
13877
|
const POSITION_DECIDED = Symbol("position decided by a position pass");
|
|
13833
13878
|
/**
|
|
13879
|
+
* Marks a STRING leaf {@link refuseUncertifiedReadbackPositions} REFUSED — a
|
|
13880
|
+
* position whose source subtree proves a dynamic reference lives there while
|
|
13881
|
+
* the walk could not pair the two sides, so the readback value at it may be a
|
|
13882
|
+
* decrypted secret (issue
|
|
13883
|
+
* [#2852](https://github.com/go-to-k/cdkd/issues/2852)).
|
|
13884
|
+
*
|
|
13885
|
+
* A THIRD state, not a second spelling of {@link POSITION_DECIDED}, and the
|
|
13886
|
+
* difference is what keeps the fail-closed change from REGRESSING the derived
|
|
13887
|
+
* needles issue #2012 added. `POSITION_DECIDED` tells
|
|
13888
|
+
* {@link preferPositionDecisions} "this leaf is mine, the scan may not touch
|
|
13889
|
+
* it"; a refusal makes the opposite claim — the pass has NO answer here, only
|
|
13890
|
+
* the knowledge that the raw value is unsafe. So the scan still gets to win at
|
|
13891
|
+
* such a leaf (a derived needle NAMES the expression, which is strictly better
|
|
13892
|
+
* than a mask), and the mask stands only where nothing else spoke.
|
|
13893
|
+
*/
|
|
13894
|
+
const POSITION_UNCERTIFIED = Symbol("position refused by a position pass");
|
|
13895
|
+
/**
|
|
13834
13896
|
* Record one (plaintext -> expression) pair, or strike the plaintext out.
|
|
13835
13897
|
*
|
|
13836
13898
|
* Below {@link MIN_NEEDLE_LENGTH} nothing is recorded, and this floor DECIDES
|
|
@@ -14092,11 +14154,13 @@ function asIndex(marks, index) {
|
|
|
14092
14154
|
* newly extending #2427 to the EMPTY-map path, where the unchanged-resource
|
|
14093
14155
|
* `drainObservedCaptures` baseline lives and where `cdkd drift --revert` pushes
|
|
14094
14156
|
* 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
|
-
*
|
|
14157
|
+
* through to `refused`. That is the position passes' own answer, which is the
|
|
14158
|
+
* bag by identity: their object arm carried no prototype guard of its own until
|
|
14159
|
+
* issue [#2869](https://github.com/go-to-k/cdkd/issues/2869), so a non-plain
|
|
14160
|
+
* leaf whose source subtree carries a reference WAS already flattened one
|
|
14161
|
+
* function earlier and this guard could only keep a `{}` intact. Both halves
|
|
14162
|
+
* are guarded now; the remaining copy of the defect is the VALUE scan's own
|
|
14163
|
+
* walk, which is issue #2427 and a different pass.
|
|
14100
14164
|
*
|
|
14101
14165
|
* The net effect is byte-identical to the FIRST ordering on every input where
|
|
14102
14166
|
* the un-certification did not fire — which is the whole point: it keeps that
|
|
@@ -14110,7 +14174,9 @@ function preferPositionDecisions(scanned, refused, bag, marks, inferred) {
|
|
|
14110
14174
|
}
|
|
14111
14175
|
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
14176
|
if (typeof bag !== "string" || marks === POSITION_DECIDED) return refused;
|
|
14113
|
-
|
|
14177
|
+
const scanDecision = scanned === bag ? inferred.get(bag) ?? scanned : scanned;
|
|
14178
|
+
if (marks === POSITION_UNCERTIFIED && !(typeof scanDecision === "string" && isSingleDynamicReferenceToken(scanDecision))) return refused;
|
|
14179
|
+
return scanDecision;
|
|
14114
14180
|
}
|
|
14115
14181
|
/**
|
|
14116
14182
|
* DERIVED NEEDLES (issue [#2012](https://github.com/go-to-k/cdkd/issues/2012)):
|
|
@@ -14180,7 +14246,7 @@ function deriveReadbackNeedles(bag, source, secrets, rules) {
|
|
|
14180
14246
|
poisoned: /* @__PURE__ */ new Set(),
|
|
14181
14247
|
inferred: /* @__PURE__ */ new Set()
|
|
14182
14248
|
};
|
|
14183
|
-
refuseUncertifiedReadbackPositions(bag, source, secrets, collector);
|
|
14249
|
+
refuseUncertifiedReadbackPositions(bag, source, secrets, false, collector);
|
|
14184
14250
|
if (collector.needles.size === 0) return void 0;
|
|
14185
14251
|
const certain = /* @__PURE__ */ new Map();
|
|
14186
14252
|
const inferred = /* @__PURE__ */ new Map();
|
|
@@ -14191,6 +14257,149 @@ function deriveReadbackNeedles(bag, source, secrets, rules) {
|
|
|
14191
14257
|
};
|
|
14192
14258
|
}
|
|
14193
14259
|
/**
|
|
14260
|
+
* FAIL CLOSED over one readback subtree the position walk could not certify
|
|
14261
|
+
* (issue [#2852](https://github.com/go-to-k/cdkd/issues/2852)).
|
|
14262
|
+
*
|
|
14263
|
+
* Every STRING leaf the source cannot account for becomes
|
|
14264
|
+
* {@link SECRET_MASK} — at EVERY position it occupies, including a node the bag
|
|
14265
|
+
* reaches twice; see the memo below. So does a BINARY leaf, whose bytes are a
|
|
14266
|
+
* secret in the clear once `JSON.stringify` writes them. Everything else is
|
|
14267
|
+
* kept. Called from
|
|
14268
|
+
* {@link refuseUncertifiedReadbackPositions} — through
|
|
14269
|
+
* {@link refuseAgainstSource}, and DIRECTLY from its keyed-array arm, which
|
|
14270
|
+
* hoists the literal set and re-spells the `failClosed` test — and only where
|
|
14271
|
+
* that walk has already established
|
|
14272
|
+
* BOTH halves of the evidence: the SOURCE subtree at this position spells a
|
|
14273
|
+
* dynamic reference (so the template says a secret lives here), and the two
|
|
14274
|
+
* sides cannot be paired (so no position can say WHICH leaf holds its resolved
|
|
14275
|
+
* form). Before this, every such branch returned the bag — the decrypted
|
|
14276
|
+
* readback — verbatim.
|
|
14277
|
+
*
|
|
14278
|
+
* "The source cannot account for" is the whole claim, and it is deliberately
|
|
14279
|
+
* WEAKER than "no plaintext survives": a leaf the source spells verbatim is
|
|
14280
|
+
* kept, so a readback that echoes a template literal back keeps it. What the
|
|
14281
|
+
* pass guarantees is that no leaf survives on the strength of the walk having
|
|
14282
|
+
* given up.
|
|
14283
|
+
*
|
|
14284
|
+
* WHY A MASK RATHER THAN THE SOURCE. Substituting the source is what the
|
|
14285
|
+
* certified rows do, and it is exactly what the array arm's own comment (and
|
|
14286
|
+
* the issue #1915 fences) refuse here: with no pairing, writing the source
|
|
14287
|
+
* fabricates baseline content AWS never reported, which `cdkd drift --revert`
|
|
14288
|
+
* then pushes to the live resource. A mask fabricates no content — it keeps
|
|
14289
|
+
* the bag's SHAPE, adds no key, no element and no scalar-over-container — and
|
|
14290
|
+
* `SECRET_MASK` is already a first-class persisted state with its own
|
|
14291
|
+
* downstream guards (`drift.ts`'s `collectSecretMaskPaths` /
|
|
14292
|
+
* `preserveLiveValuesAtMaskedLeaves`, `runAccept`'s refusal,
|
|
14293
|
+
* `rollback-executor.ts`'s `refuseMaskedReplayBaseline`), because the
|
|
14294
|
+
* mask-only channel (issue #2274) already puts one there.
|
|
14295
|
+
*
|
|
14296
|
+
* WHY STRINGS ONLY. A recorded secret is a `string` by the type of
|
|
14297
|
+
* {@link RecordedSecretValues}, so a number, a boolean or `null` cannot BE a
|
|
14298
|
+
* resolved secret and masking one would only cost drift a comparison. A
|
|
14299
|
+
* NON-PLAIN object (a `Date` an AWS SDK readback carries, a `Buffer`) is
|
|
14300
|
+
* returned BY IDENTITY for the same reason plus a second one: rebuilding it
|
|
14301
|
+
* from its own enumerable keys yields `{}` — the corruption of issue
|
|
14302
|
+
* [#2869](https://github.com/go-to-k/cdkd/issues/2869).
|
|
14303
|
+
*
|
|
14304
|
+
* A leaf that IS a whole `{{resolve:...}}` token is kept: it is an expression
|
|
14305
|
+
* AWS echoed back unresolved, not plaintext, and replacing it with a mask would
|
|
14306
|
+
* DESTROY a value `cdkd drift` can re-resolve. WHOLE, not "contains one" — that
|
|
14307
|
+
* wider test spared `postgres://admin:<plaintext>@{{resolve:ssm-secure:/h}}`,
|
|
14308
|
+
* where the embedded token vouched for a leaf that was mostly the decrypted
|
|
14309
|
+
* secret. The residual is the issue #1917 shape — a plaintext that merely LOOKS
|
|
14310
|
+
* like a token — which every arm of this module already trusts.
|
|
14311
|
+
*
|
|
14312
|
+
* SO IS A LEAF THE SOURCE SUBTREE ITSELF SPELLS, and this is what keeps the
|
|
14313
|
+
* fail-closed change from emptying an ordinary drift baseline. `sourceLiterals`
|
|
14314
|
+
* is {@link wholeStringLeavesOf} over the SOURCE at the refused position — the
|
|
14315
|
+
* literal frame of an `Fn::Join`, the anchor values of an array AWS reordered,
|
|
14316
|
+
* every ordinary property beside the reference. A value the template SPELLS is
|
|
14317
|
+
* not the resolved form of a reference, so masking it buys nothing; and where
|
|
14318
|
+
* it coincides with one, that plaintext is already sitting in the record's own
|
|
14319
|
+
* `properties`, so the copy in the readback is not the disclosure. Scoped to
|
|
14320
|
+
* the SOURCE AT THE REFUSED POSITION rather than the whole record on purpose: a
|
|
14321
|
+
* coincidence three properties away is not evidence about this one. Read that
|
|
14322
|
+
* literally — when an ARRAY refuses element by element the refused position is
|
|
14323
|
+
* the array, so a SIBLING element's literal does spare a leaf. That is the
|
|
14324
|
+
* intended granularity (the elements are peers of one list AWS returned
|
|
14325
|
+
* together, and the pairing that failed is between the two LISTS), and it is
|
|
14326
|
+
* stated because "subtree" reads narrower than the code is.
|
|
14327
|
+
*
|
|
14328
|
+
* OVER-MASKING IS THE REMAINING COST AND IT IS THE INTENDED DIRECTION: a value
|
|
14329
|
+
* AWS NORMALISED (`us-east-1` returned as `US-EAST-1`) no longer matches the
|
|
14330
|
+
* source and is masked with the secret, because nothing distinguishes them once
|
|
14331
|
+
* the pairing is gone. That is phantom drift rather than a disclosure — the
|
|
14332
|
+
* same way this module chooses to be wrong at
|
|
14333
|
+
* {@link mixedLeafMayCarryPublicReference}.
|
|
14334
|
+
*
|
|
14335
|
+
* `mark` is {@link refuseUncertifiedReadbackPositions}'s MARK MODE, threaded
|
|
14336
|
+
* so the parallel tree keeps the same shape: {@link POSITION_UNCERTIFIED}
|
|
14337
|
+
* lands wherever the substituting pass puts a mask, and the bag's own value
|
|
14338
|
+
* everywhere else — which is that mode's contract.
|
|
14339
|
+
*/
|
|
14340
|
+
function refuseUncertifiedSubtree(value, sourceLiterals, mark, seen = /* @__PURE__ */ new Map()) {
|
|
14341
|
+
if (typeof value === "string") {
|
|
14342
|
+
if (isSingleDynamicReferenceToken(value) || sourceLiterals.has(value)) return value;
|
|
14343
|
+
if (value === "") return value;
|
|
14344
|
+
return mark ? POSITION_UNCERTIFIED : "***";
|
|
14345
|
+
}
|
|
14346
|
+
if (value === null || typeof value !== "object") return value;
|
|
14347
|
+
const memo = seen.get(value);
|
|
14348
|
+
if (memo !== void 0) return memo;
|
|
14349
|
+
if (Array.isArray(value)) {
|
|
14350
|
+
const out = [];
|
|
14351
|
+
seen.set(value, out);
|
|
14352
|
+
for (const item of value) out.push(refuseUncertifiedSubtree(item, sourceLiterals, mark, seen));
|
|
14353
|
+
return out;
|
|
14354
|
+
}
|
|
14355
|
+
if (isPlainObject$2(value) && hasPlainPrototype(value)) {
|
|
14356
|
+
const out = Object.create(null);
|
|
14357
|
+
seen.set(value, out);
|
|
14358
|
+
for (const [k, v] of Object.entries(value)) out[k] = refuseUncertifiedSubtree(v, sourceLiterals, mark, seen);
|
|
14359
|
+
return out;
|
|
14360
|
+
}
|
|
14361
|
+
if (ArrayBuffer.isView(value)) return mark ? POSITION_UNCERTIFIED : "***";
|
|
14362
|
+
seen.set(value, value);
|
|
14363
|
+
return value;
|
|
14364
|
+
}
|
|
14365
|
+
/**
|
|
14366
|
+
* {@link refuseUncertifiedSubtree} over a bag whose SOURCE is in hand, so the
|
|
14367
|
+
* literal set can never be built from anything but the source at the SAME
|
|
14368
|
+
* position.
|
|
14369
|
+
*
|
|
14370
|
+
* NOT the only spelling, and an earlier revision of this sentence said it was.
|
|
14371
|
+
* The keyed-array arm calls {@link refuseUncertifiedSubtree} DIRECTLY, because
|
|
14372
|
+
* it hoists the literal set out of its `bag.map` — so it also re-spells the
|
|
14373
|
+
* `failClosed` test this function owns. A future edit that drops that
|
|
14374
|
+
* re-spelling drops the DESTINATION check with it, which is why the two are
|
|
14375
|
+
* named here rather than left to be noticed.
|
|
14376
|
+
*/
|
|
14377
|
+
function refuseAgainstSource(bag, source, failClosed, mark) {
|
|
14378
|
+
if (failClosed !== true) return bag;
|
|
14379
|
+
return refuseUncertifiedSubtree(bag, wholeStringLeavesOf(source), mark);
|
|
14380
|
+
}
|
|
14381
|
+
/**
|
|
14382
|
+
* Does any element of `source` that the bag did NOT pair carry a dynamic
|
|
14383
|
+
* reference (issue [#2852](https://github.com/go-to-k/cdkd/issues/2852))?
|
|
14384
|
+
*
|
|
14385
|
+
* The evidence that licenses refusing the bag's own unpaired elements in the
|
|
14386
|
+
* IDENTITY-KEYED array arm. An identity key that does not round-trip
|
|
14387
|
+
* byte-identically — AWS case-normalises a `Name`, or expands one to an ARN —
|
|
14388
|
+
* drops its element to `partner === undefined`, and the element the source
|
|
14389
|
+
* spells as a reference is then left over with nothing pointing at it, so its
|
|
14390
|
+
* resolved plaintext is somewhere in the unpaired remainder.
|
|
14391
|
+
*
|
|
14392
|
+
* The converse is why this is a QUESTION rather than a blanket refusal: when
|
|
14393
|
+
* every reference-bearing source element DID find its partner, an extra bag
|
|
14394
|
+
* element is a peer AWS added (another `Environment` entry) and carries no
|
|
14395
|
+
* secret this source can account for. Refusing those would mask ordinary
|
|
14396
|
+
* readback content for no evidence, which is the same trade the object arm's
|
|
14397
|
+
* extra-KEY branch declines to make.
|
|
14398
|
+
*/
|
|
14399
|
+
function unpairedSourceCarriesReference(source, key, bagIdentities) {
|
|
14400
|
+
return source.some((item) => !bagIdentities.has(item[key]) && subtreeHasDynamicReference(item));
|
|
14401
|
+
}
|
|
14402
|
+
/**
|
|
14194
14403
|
* Refuse to persist a readback leaf the path pass could not CERTIFY, at any
|
|
14195
14404
|
* position the STATE source proves is secret-bearing (issue #1926 review).
|
|
14196
14405
|
*
|
|
@@ -14212,15 +14421,42 @@ function deriveReadbackNeedles(bag, source, secrets, rules) {
|
|
|
14212
14421
|
* ...the same MIXED leaf inside a PAIRED element LEAK take source
|
|
14213
14422
|
* `['--pw', '{{resolve:...}}']` (no identity key) LEAK take source*
|
|
14214
14423
|
* `[{Field, Val: '{{resolve:...}}'}]` (no `Name`) LEAK take source*
|
|
14215
|
-
* ...either of those, but REORDERED / normalised LEAK
|
|
14216
|
-
* an UNPAIRED element
|
|
14217
|
-
* an
|
|
14424
|
+
* ...either of those, but REORDERED / normalised LEAK MASK (#2852)
|
|
14425
|
+
* an UNPAIRED element, source reference left over LEAK needle | MASK
|
|
14426
|
+
* an UNPAIRED element, every source reference paired LEAK needle (#2012)
|
|
14427
|
+
* a RESHAPED container / added wrapper level LEAK MASK (#2852)
|
|
14428
|
+
* a source leaf promoted to a container LEAK MASK (#2852)
|
|
14429
|
+
* a RAW `Fn::Join` source vs a STRING readback LEAK MASK (#2846)
|
|
14430
|
+
* an observed KEY the source does not carry LEAK needle | LEAK
|
|
14431
|
+
* a `Date` under a reference-bearing source subtree `{}` kept (#2869)
|
|
14218
14432
|
* whole `{{resolve:...}}` token ok ok
|
|
14219
14433
|
* `Environment[]` keyed by `Name` (issue #1915) ok ok
|
|
14220
14434
|
* PUBLIC ssm MIXED leaf, POPULATED map ok ok
|
|
14221
14435
|
* PUBLIC ssm MIXED leaf, EMPTY map ok over-redacts
|
|
14222
14436
|
* ```
|
|
14223
14437
|
*
|
|
14438
|
+
* MASK rows are the FAIL-CLOSED change of issue
|
|
14439
|
+
* [#2852](https://github.com/go-to-k/cdkd/issues/2852). Every branch this walk
|
|
14440
|
+
* could not certify used to `return bag` — the decrypted readback, verbatim —
|
|
14441
|
+
* so "cannot pair" and "safe to persist" were the same answer. They are now
|
|
14442
|
+
* {@link refuseUncertifiedSubtree}, whose doc argues why a mask rather than the
|
|
14443
|
+
* source and why STRING leaves only. `needle | MASK` means the derived needles
|
|
14444
|
+
* of issue #2012 are consulted FIRST and the mask stands only where they had
|
|
14445
|
+
* nothing to say ({@link preferPositionDecisions}), so no row this table
|
|
14446
|
+
* previously closed by a needle is taken back.
|
|
14447
|
+
*
|
|
14448
|
+
* ONE row is deliberately still open: an observed KEY the source does not carry
|
|
14449
|
+
* keeps the plaintext when no needle names it. Refusing there needs evidence
|
|
14450
|
+
* that does not exist — the source has NO leaf at that position, so the walk
|
|
14451
|
+
* would be guessing — and the cost of guessing is not bounded: a write-only
|
|
14452
|
+
* credential AWS never echoes back (RDS `MasterUserPassword` and every
|
|
14453
|
+
* `getDriftUnknownPaths` sibling) leaves a reference-bearing source key
|
|
14454
|
+
* unpaired on EVERY readback, so keying the refusal on that would mask
|
|
14455
|
+
* `Runtime` / `FunctionArn` / `LastModified` for every secret-bearing resource
|
|
14456
|
+
* in the account. The extra-KEY asymmetry stated further down is the same
|
|
14457
|
+
* argument; issue [#2868](https://github.com/go-to-k/cdkd/issues/2868) owns the
|
|
14458
|
+
* shape where the plaintext has no counterpart in the source at all.
|
|
14459
|
+
*
|
|
14224
14460
|
* The last row is the price of the row above it and is tracked as issue
|
|
14225
14461
|
* [#2036](https://github.com/go-to-k/cdkd/issues/2036): with no map nothing was
|
|
14226
14462
|
* resolved, so nothing distinguishes a public parameter from a `SecureString`
|
|
@@ -14254,7 +14490,7 @@ function deriveReadbackNeedles(bag, source, secrets, rules) {
|
|
|
14254
14490
|
* {@link anchorsCorroboratePairing} answers only one of them and its own doc
|
|
14255
14491
|
* says nothing in it is sufficient alone.
|
|
14256
14492
|
*
|
|
14257
|
-
* The
|
|
14493
|
+
* The MASK rows are one root cause, not several: no needle and no
|
|
14258
14494
|
* position, so nothing distinguishes a resolved secret from an ordinary
|
|
14259
14495
|
* literal. They are NOT closed by taking the source subtree, which an earlier
|
|
14260
14496
|
* revision did and the issue #1915 fences correctly rejected — measured, it
|
|
@@ -14269,13 +14505,24 @@ function deriveReadbackNeedles(bag, source, secrets, rules) {
|
|
|
14269
14505
|
* The MIXED row is the shape this module itself calls DOMINANT for CDK — an
|
|
14270
14506
|
* `Fn::Join` around `secret.secretValueFromJson(...)`.
|
|
14271
14507
|
*
|
|
14272
|
-
* TAKE SOURCE rather than a {@link SECRET_MASK} on the rows it
|
|
14508
|
+
* TAKE SOURCE rather than a {@link SECRET_MASK} on the rows it CERTIFIES, for
|
|
14273
14509
|
* the same reason the whole-token arm does: a mask is not a value `cdkd drift`
|
|
14274
14510
|
* can re-resolve, so it would report a permanent phantom — and `cdkd drift
|
|
14275
14511
|
* --revert` pushes the BASELINE to AWS, so a masked baseline would write the
|
|
14276
14512
|
* literal `***` onto the live resource (the issue #1498 / #1501 class).
|
|
14277
14513
|
*
|
|
14278
|
-
*
|
|
14514
|
+
* That is an argument about a row where a SOURCE VALUE IS AVAILABLE, and it
|
|
14515
|
+
* decides nothing about a row where none is (issue #2852). There the choice is
|
|
14516
|
+
* not mask-versus-source but mask-versus-PLAINTEXT, and the two costs above are
|
|
14517
|
+
* both real: `runAccept` refuses a masked change and
|
|
14518
|
+
* `preserveLiveValuesAtMaskedLeaves` moves AWS's own value in before `--revert`
|
|
14519
|
+
* sends anything, so the mask degrades those two commands on that resource
|
|
14520
|
+
* rather than corrupting it — while the plaintext it replaces is the disclosure
|
|
14521
|
+
* of GHSA-p5qg-v9gv-hc7w sitting in `state.json`. Do not read the paragraph
|
|
14522
|
+
* above as a rule against the mask everywhere; it is a rule about the rows with
|
|
14523
|
+
* a certified source.
|
|
14524
|
+
*
|
|
14525
|
+
* The needle rows were the RESIDUAL and are closed by DERIVED NEEDLES (issue
|
|
14279
14526
|
* #2012) — see {@link deriveReadbackNeedles}. Neither has a position to argue
|
|
14280
14527
|
* from: an unpaired array element and an observed KEY the source does not carry
|
|
14281
14528
|
* are both positions with no source leaf to take. What they never lacked was a
|
|
@@ -14301,7 +14548,7 @@ function deriveReadbackNeedles(bag, source, secrets, rules) {
|
|
|
14301
14548
|
* drift, and a needle learned from a MIS-paired position is a false redaction
|
|
14302
14549
|
* everywhere it then matches.
|
|
14303
14550
|
*/
|
|
14304
|
-
function refuseUncertifiedReadbackPositions(bag, source, secrets, learn, mark) {
|
|
14551
|
+
function refuseUncertifiedReadbackPositions(bag, source, secrets, failClosed, learn, mark) {
|
|
14305
14552
|
if (isDynamicReferenceString(source) && typeof bag === "string") {
|
|
14306
14553
|
if (isSingleDynamicReferenceToken(source)) {
|
|
14307
14554
|
if (learn) learnWholeTokenNeedle(learn, bag, source);
|
|
@@ -14312,25 +14559,26 @@ function refuseUncertifiedReadbackPositions(bag, source, secrets, learn, mark) {
|
|
|
14312
14559
|
return mark ? POSITION_DECIDED : source;
|
|
14313
14560
|
}
|
|
14314
14561
|
if (!subtreeHasDynamicReference(source)) return bag;
|
|
14315
|
-
if (isPlainObject$2(bag) && isPlainObject$2(source)) {
|
|
14562
|
+
if (isPlainObject$2(bag) && hasPlainPrototype(bag) && isPlainObject$2(source)) {
|
|
14316
14563
|
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;
|
|
14564
|
+
for (const [k, v] of Object.entries(bag)) out[k] = Object.hasOwn(source, k) ? refuseUncertifiedReadbackPositions(v, source[k], secrets, failClosed, learn, mark) : v;
|
|
14318
14565
|
return out;
|
|
14319
14566
|
}
|
|
14320
14567
|
if (Array.isArray(bag) && Array.isArray(source)) {
|
|
14321
14568
|
const key = identityKeyFor(bag, source);
|
|
14322
14569
|
if (key === void 0) {
|
|
14323
|
-
if (!unkeyedArrayPairsByAnchors(bag, source)) return bag;
|
|
14324
|
-
return bag.map((item, i) => refuseUncertifiedReadbackPositions(item, source[i], secrets, learn, mark));
|
|
14570
|
+
if (!unkeyedArrayPairsByAnchors(bag, source)) return refuseAgainstSource(bag, source, failClosed, mark);
|
|
14571
|
+
return bag.map((item, i) => refuseUncertifiedReadbackPositions(item, source[i], secrets, failClosed, learn, mark));
|
|
14325
14572
|
}
|
|
14326
14573
|
const sourceByIdentity = /* @__PURE__ */ new Map();
|
|
14327
14574
|
for (const item of source) sourceByIdentity.set(item[key], item);
|
|
14575
|
+
const orphanLiterals = unpairedSourceCarriesReference(source, key, new Set(bag.map((item) => item[key]))) && failClosed === true ? wholeStringLeavesOf(source) : void 0;
|
|
14328
14576
|
return bag.map((item) => {
|
|
14329
14577
|
const partner = sourceByIdentity.get(item[key]);
|
|
14330
|
-
return partner === void 0 ? item : refuseUncertifiedReadbackPositions(item, partner, secrets, learn, mark);
|
|
14578
|
+
return partner === void 0 ? orphanLiterals !== void 0 ? refuseUncertifiedSubtree(item, orphanLiterals, mark) : item : refuseUncertifiedReadbackPositions(item, partner, secrets, failClosed, learn, mark);
|
|
14331
14579
|
});
|
|
14332
14580
|
}
|
|
14333
|
-
return bag;
|
|
14581
|
+
return refuseAgainstSource(bag, source, failClosed, mark);
|
|
14334
14582
|
}
|
|
14335
14583
|
/**
|
|
14336
14584
|
* Deep-clone `bag`, replacing every occurrence of a recorded secret value with
|
|
@@ -14346,10 +14594,11 @@ function redactSecretsForState(bag, secrets, source, rules = TEMPLATE_DERIVED_RU
|
|
|
14346
14594
|
if (source !== void 0) {
|
|
14347
14595
|
const positioned = redactByPath(bag, source, secrets, rules, recordedExpressionsOf(secrets), isSameGenerationBag(bag));
|
|
14348
14596
|
if (!isReadbackProjectedFromState(rules)) return positioned;
|
|
14349
|
-
const
|
|
14597
|
+
const failClosed = rules.failClosedOnUncertifiedPositions === true;
|
|
14598
|
+
const refused = refuseUncertifiedReadbackPositions(positioned, source, secrets, failClosed);
|
|
14350
14599
|
const derived = deriveReadbackNeedles(bag, source, secrets, rules);
|
|
14351
14600
|
if (derived === void 0) return refused;
|
|
14352
|
-
const marks = refuseUncertifiedReadbackPositions(positioned, source, secrets, void 0, true);
|
|
14601
|
+
const marks = refuseUncertifiedReadbackPositions(positioned, source, secrets, failClosed, void 0, true);
|
|
14353
14602
|
return preferPositionDecisions(redactSecretsForState(bag, derived.certain), refused, bag, marks, derived.inferred);
|
|
14354
14603
|
}
|
|
14355
14604
|
const regex = buildNeedleRegex(substringNeedlesOf(secrets));
|
|
@@ -14483,7 +14732,7 @@ function scrubResourceRecord(record, secrets, sourceProperties, observedRules) {
|
|
|
14483
14732
|
const next = { ...record };
|
|
14484
14733
|
next.properties = redactSecretsForState(record.properties, secrets, sourceProperties);
|
|
14485
14734
|
if (record.attributes) next.attributes = redactSecretsForState(record.attributes, secrets);
|
|
14486
|
-
if (record.observedProperties) next.observedProperties = redactSecretsForState(record.observedProperties, secrets, sourceProperties ?? next.properties, observedRules ?? (sourceProperties
|
|
14735
|
+
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
14736
|
return next;
|
|
14488
14737
|
}
|
|
14489
14738
|
/**
|
|
@@ -16349,7 +16598,7 @@ async function fetchCreateOnlyPropertyPaths(resourceType) {
|
|
|
16349
16598
|
if (Array.isArray(createOnly)) for (const path of createOnly) {
|
|
16350
16599
|
if (typeof path !== "string") continue;
|
|
16351
16600
|
if (!path.startsWith("/properties/")) continue;
|
|
16352
|
-
const segments = path.slice(12).split("/").map(unescapeJsonPointerSegment$
|
|
16601
|
+
const segments = path.slice(12).split("/").map(unescapeJsonPointerSegment$2).filter((segment) => segment.length > 0);
|
|
16353
16602
|
if (segments.length > 0) result.push(segments);
|
|
16354
16603
|
}
|
|
16355
16604
|
}
|
|
@@ -16359,7 +16608,7 @@ async function fetchCreateOnlyPropertyPaths(resourceType) {
|
|
|
16359
16608
|
/**
|
|
16360
16609
|
* Unescape an RFC 6901 JSON Pointer segment (`~1` -> `/`, `~0` -> `~`).
|
|
16361
16610
|
*/
|
|
16362
|
-
function unescapeJsonPointerSegment$
|
|
16611
|
+
function unescapeJsonPointerSegment$2(segment) {
|
|
16363
16612
|
return segment.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
16364
16613
|
}
|
|
16365
16614
|
|
|
@@ -21827,16 +22076,106 @@ const REF_RETURNS_ARN_FROM_STATE = /* @__PURE__ */ new Map([
|
|
|
21827
22076
|
* Only non-empty string values qualify — an intrinsic-shaped or empty value is
|
|
21828
22077
|
* skipped so the caller falls back to the raw physical id rather than emitting
|
|
21829
22078
|
* a broken `[object Object]` / `''`.
|
|
21830
|
-
|
|
21831
|
-
|
|
22079
|
+
*
|
|
22080
|
+
* A leaf carrying {@link SECRET_MASK} is handled specially, and that handling
|
|
22081
|
+
* is a SECURITY property rather than a shape one (issue
|
|
22082
|
+
* [#2847](https://github.com/go-to-k/cdkd/issues/2847)). Both bags this reads
|
|
22083
|
+
* can hold the mask: `attributes` because `CloudControlProvider.import` masks
|
|
22084
|
+
* every model key it cannot certify is a read-only attribute, and `properties`
|
|
22085
|
+
* because the mask-only channel (issue #2274) writes it there too.
|
|
22086
|
+
* `SECRET_MASK` is a non-empty string, so the lookup HITS and
|
|
22087
|
+
* `cfnRefValueFromPhysicalId` returns `'***'` as the resource's `Ref` value —
|
|
22088
|
+
* which `resolveRefValue` hands back verbatim and a green deploy substitutes
|
|
22089
|
+
* into the consumer's property and sends to AWS. That is the #1498 / #1501
|
|
22090
|
+
* corrupted-write class, reached through the ONE attribute reader that is not
|
|
22091
|
+
* an `Fn::GetAtt`.
|
|
22092
|
+
*
|
|
22093
|
+
* ## The skip is OPT-IN, and that is the load-bearing design decision
|
|
22094
|
+
*
|
|
22095
|
+
* `onMaskedValue` is not a notification bolted onto a global behaviour change;
|
|
22096
|
+
* it is the SWITCH. With no callback this function returns the mask exactly as
|
|
22097
|
+
* it did before issue #2847 — same value, same callers, nothing to audit. Only
|
|
22098
|
+
* a caller that passes one gets the skip, and by passing one it declares it
|
|
22099
|
+
* will ACT on the report.
|
|
22100
|
+
*
|
|
22101
|
+
* IT WAS UNCONDITIONAL FOR THREE REVIEW ROUNDS, and each round found a fresh
|
|
22102
|
+
* caller broken by it, because skipping a mask is only an improvement for a
|
|
22103
|
+
* caller that has somewhere to put the refusal. For everyone else it REMOVES a
|
|
22104
|
+
* guarded sentinel and substitutes an unguarded wrong value: the fall-through
|
|
22105
|
+
* emits the raw physical id — for `AWS::S3Tables::Table` an ARN ending in a
|
|
22106
|
+
* UUID rather than the table name CFn `Ref` returns — which
|
|
22107
|
+
* `refuseMaskedReplayBaseline`, `cdkd export`'s blocker, `cdkd drift`'s mask
|
|
22108
|
+
* handling and the deploy-time refusal all pass, where every one of them
|
|
22109
|
+
* REJECTS `'***'` loudly. The rounds found it in `cdkd orphan`, then in
|
|
22110
|
+
* `resolveOutputs`, then in `cdkd import`; the pattern was the design, not the
|
|
22111
|
+
* call sites, so the design changed rather than the sites.
|
|
22112
|
+
*
|
|
22113
|
+
* A caller therefore chooses between exactly two things, and doing NOTHING is
|
|
22114
|
+
* the safe default rather than a hole:
|
|
22115
|
+
*
|
|
22116
|
+
* 1. **Pass no callback** — pre-#2847 behaviour, the mask travels, downstream
|
|
22117
|
+
* readers catch it. Every caller that has not opted in is in this bucket by
|
|
22118
|
+
* construction, so there is no per-caller audit to keep current.
|
|
22119
|
+
* 2. **Pass one and act on it** — skip the mask and refuse. `resolveRefValue`
|
|
22120
|
+
* → `noteRefStateMask` → `ResolverContext.redactedAttributeReads`, read by
|
|
22121
|
+
* `DeployEngine.refuseRedactedAttributeReads` on the CREATE / UPDATE arms
|
|
22122
|
+
* and by `resolveOutputs`' own per-output check;
|
|
22123
|
+
* `src/analyzer/orphan-rewriter.ts` has no resolver context and reports the
|
|
22124
|
+
* site as `unresolvable` instead (under `--force` it warns and substitutes
|
|
22125
|
+
* `SECRET_MASK`, i.e. it opts back into bucket 1's VALUE deliberately).
|
|
22126
|
+
*
|
|
22127
|
+
* BUCKET 1 IS NOT "the harmless callers", and one of them was mis-described as
|
|
22128
|
+
* display-only before the opt-in existed: `cdkd export`'s
|
|
22129
|
+
* `resolveChildImportParameters` builds a bagless context whose result becomes
|
|
22130
|
+
* a `Parameter[]` on `CreateChangeSet --change-set-type IMPORT` — a re-apply to
|
|
22131
|
+
* a live system. It is safe for bucket 1's ORDINARY reason rather than a
|
|
22132
|
+
* special one: bagless, so it ships `'***'`, which CloudFormation rejects
|
|
22133
|
+
* loudly, exactly as it did before issue #2847. Whether it should REFUSE
|
|
22134
|
+
* instead is a separate choice nobody has made. That is the shape of the
|
|
22135
|
+
* argument every bucket-1 caller gets: not "this value goes nowhere" but "the
|
|
22136
|
+
* mask still reaches a reader that recognises it".
|
|
22137
|
+
*
|
|
22138
|
+
* The resolver passes a callback from ONE site — `resolveRefValue` — and it
|
|
22139
|
+
* passes one only when `context.redactedAttributeReads` EXISTS. Testing the
|
|
22140
|
+
* bag at the call site rather than inside `noteRefStateMask` is the whole
|
|
22141
|
+
* point: the note returning early still leaves the SKIP done, so a bagless
|
|
22142
|
+
* context took the fall-through with nowhere to record the refusal. That is
|
|
22143
|
+
* how `cdkd import` came to PERSIST a raw physical id into
|
|
22144
|
+
* `resource.properties` — from where `cdkd export` writes it into the imported
|
|
22145
|
+
* template and `cdkd drift --revert` sends it to AWS. `cdkd diff` and
|
|
22146
|
+
* `cdkd scrub` are bagless too and simply resolve as they did before.
|
|
22147
|
+
*
|
|
22148
|
+
* `onMaskedValue` fires only when the WHOLE lookup came up empty, not at the
|
|
22149
|
+
* masked leaf. The scan spans two bags and several alias keys, so a masked
|
|
22150
|
+
* `properties.TableName` beside a live `attributes.TableName` is an ordinary,
|
|
22151
|
+
* fully resolvable record — `cdkd import`'s own shape — and notifying there
|
|
22152
|
+
* would fail a deploy that has the value it needs. The distinction is "cdkd
|
|
22153
|
+
* could not answer, and the reason was a redaction", which is the only case
|
|
22154
|
+
* that must refuse. An ABSENT key keeps degrading to the physical id exactly as
|
|
22155
|
+
* before: the recovery branches document that graceful fall-through for a
|
|
22156
|
+
* pre-#1045 / pre-#1681 record, and nothing about that case changed.
|
|
22157
|
+
*/
|
|
22158
|
+
function refStateLookupFromResource(resource, onMaskedValue) {
|
|
21832
22159
|
return (keys) => {
|
|
22160
|
+
let masked;
|
|
21833
22161
|
for (const source of [resource.properties, resource.attributes]) {
|
|
21834
22162
|
if (!source) continue;
|
|
21835
22163
|
for (const key of keys) {
|
|
21836
22164
|
const value = source[key];
|
|
21837
|
-
if (typeof value === "string" && value.length > 0)
|
|
22165
|
+
if (typeof value === "string" && value.length > 0) {
|
|
22166
|
+
if (carriesSecretMask(value)) {
|
|
22167
|
+
if (onMaskedValue === void 0) return value;
|
|
22168
|
+
masked ??= {
|
|
22169
|
+
key,
|
|
22170
|
+
notify: onMaskedValue
|
|
22171
|
+
};
|
|
22172
|
+
continue;
|
|
22173
|
+
}
|
|
22174
|
+
return value;
|
|
22175
|
+
}
|
|
21838
22176
|
}
|
|
21839
22177
|
}
|
|
22178
|
+
if (masked !== void 0) masked.notify(masked.key);
|
|
21840
22179
|
};
|
|
21841
22180
|
}
|
|
21842
22181
|
/**
|
|
@@ -23585,7 +23924,7 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
23585
23924
|
async resolveRef(logicalId, context) {
|
|
23586
23925
|
const resource = Object.hasOwn(context.resources, logicalId) ? context.resources[logicalId] : void 0;
|
|
23587
23926
|
if (resource) {
|
|
23588
|
-
const refValue = this.resolveRefValue(resource);
|
|
23927
|
+
const refValue = this.resolveRefValue(logicalId, resource, context);
|
|
23589
23928
|
this.logger.debug(`Resolved Ref to resource: ${logicalId} -> ${refValue}`);
|
|
23590
23929
|
return refValue;
|
|
23591
23930
|
}
|
|
@@ -23667,8 +24006,77 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
23667
24006
|
* children, whose `Ref` is an ARN recovered from the provider-recorded ARN
|
|
23668
24007
|
* attribute through the same `stateLookup` seam.
|
|
23669
24008
|
*/
|
|
23670
|
-
resolveRefValue(resource) {
|
|
23671
|
-
|
|
24009
|
+
resolveRefValue(logicalId, resource, context) {
|
|
24010
|
+
const canRefuse = context.redactedAttributeReads !== void 0;
|
|
24011
|
+
return cfnRefValueFromPhysicalId(resource.resourceType, resource.physicalId, canRefuse ? refStateLookupFromResource(resource, (key) => this.noteRefStateMask(logicalId, key, context)) : refStateLookupFromResource(resource));
|
|
24012
|
+
}
|
|
24013
|
+
/**
|
|
24014
|
+
* The `Ref` twin of {@link noteAttributeSecrecy} (issue
|
|
24015
|
+
* [#2847](https://github.com/go-to-k/cdkd/issues/2847) review).
|
|
24016
|
+
*
|
|
24017
|
+
* `noteAttributeSecrecy`'s own contract is "every branch serving a value out
|
|
24018
|
+
* of a PERSISTED `attributes` bag must call this", and `Ref` was the branch
|
|
24019
|
+
* that did not: {@link refStateLookupFromResource} reads `properties` then
|
|
24020
|
+
* `attributes` to recover a `Ref` value the physical id cannot yield, and a
|
|
24021
|
+
* masked leaf there travelled all the way to AWS with `redactedAttributeReads`
|
|
24022
|
+
* left empty.
|
|
24023
|
+
*
|
|
24024
|
+
* It is a SEPARATE method rather than a call into `noteAttributeSecrecy` for
|
|
24025
|
+
* two reasons, and both are about what the entry has to SAY. The refusal
|
|
24026
|
+
* joins these entries into a user-facing sentence whose `Fn::GetAtt` arm ends
|
|
24027
|
+
* "stop reading it" — advice that is wrong here, because this read is CDKD's
|
|
24028
|
+
* own: CloudFormation defines these types' `Ref` as a state key rather than
|
|
24029
|
+
* the physical id, so no template edit stops it. And `noteAttributeSecrecy`'s
|
|
24030
|
+
* other half — recording a `NoEcho`-declared value as a mask-only needle — has
|
|
24031
|
+
* nothing to do at this site: the value has ALREADY been masked in state, so
|
|
24032
|
+
* there is no plaintext to register.
|
|
24033
|
+
*
|
|
24034
|
+
* The SPELLING (`Ref <LogicalId> (state key <Key>)`) is what
|
|
24035
|
+
* `DeployEngine.maskedRecordRemedyFor` partitions on, so it is load-bearing
|
|
24036
|
+
* rather than cosmetic — that helper reads the logical id out of it to emit
|
|
24037
|
+
* the re-import command, and pins the shape in a test.
|
|
24038
|
+
*
|
|
24039
|
+
* `key` is never masked before interpolation because it is not template text:
|
|
24040
|
+
* it comes from the fixed key lists `cfnRefValueFromPhysicalId` passes
|
|
24041
|
+
* (`TableName` / `Name` / `SelectionId` / `RepositoryId` / the AppSync ARN
|
|
24042
|
+
* attributes), all cdkd literals.
|
|
24043
|
+
*/
|
|
24044
|
+
noteRefStateMask(logicalId, key, context) {
|
|
24045
|
+
this.pushRedactedAttributeRead(context, {
|
|
24046
|
+
kind: "ref-state-key",
|
|
24047
|
+
logicalId,
|
|
24048
|
+
key,
|
|
24049
|
+
display: `Ref ${logicalId} (state key ${key})`
|
|
24050
|
+
});
|
|
24051
|
+
}
|
|
24052
|
+
/**
|
|
24053
|
+
* The ONE writer of {@link ResolverContext.redactedAttributeReads}, so the
|
|
24054
|
+
* bag's absent-context check and its de-duplication rule live in one place
|
|
24055
|
+
* rather than being re-spelled at each of the three pushing branches.
|
|
24056
|
+
*
|
|
24057
|
+
* DE-DUPES ON THE WHOLE TUPLE, not on `display` alone, and that is the last
|
|
24058
|
+
* decision this file moved out of string space (issue #2847 round-5 review).
|
|
24059
|
+
* An earlier revision compared renderings, which is where two of this PR's
|
|
24060
|
+
* blockers came from: a decision keyed on a joined human-readable string.
|
|
24061
|
+
* Here it gated entry EXISTENCE while `DeployEngine`'s Outputs guard filters
|
|
24062
|
+
* the surviving entries by `kind`, so an `attribute` entry whose `display`
|
|
24063
|
+
* collided with a later `ref-state-key` one suppressed the refusal outright.
|
|
24064
|
+
* Reachable only through an adversarial logical id (`Ref Foo (state key Table`
|
|
24065
|
+
* with an attribute named `Name)`) — `main`'s string bag is equally
|
|
24066
|
+
* contrived, which is why review called it a cleanup rather than a defect —
|
|
24067
|
+
* but the class is the point, not this instance.
|
|
24068
|
+
*
|
|
24069
|
+
* `key` carries its weight here: for the `attribute` kind it is the
|
|
24070
|
+
* ALREADY-MASKED attribute name, so two names differing only ABOVE the mask
|
|
24071
|
+
* still render and compare identically and still collapse, exactly as they
|
|
24072
|
+
* did when the bag held strings. Nothing else reads it, and comparing it is
|
|
24073
|
+
* what keeps it from being a field written by two producers and read by none.
|
|
24074
|
+
*/
|
|
24075
|
+
pushRedactedAttributeRead(context, read) {
|
|
24076
|
+
const bag = context.redactedAttributeReads;
|
|
24077
|
+
if (bag === void 0) return;
|
|
24078
|
+
if (bag.some((entry) => entry.kind === read.kind && entry.logicalId === read.logicalId && entry.key === read.key && entry.display === read.display)) return;
|
|
24079
|
+
bag.push(read);
|
|
23672
24080
|
}
|
|
23673
24081
|
/**
|
|
23674
24082
|
* Resolve Fn::GetAtt intrinsic function
|
|
@@ -23680,7 +24088,7 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
23680
24088
|
const [rawLogicalId, rawAttributeName] = getAtt;
|
|
23681
24089
|
logicalId = rawLogicalId;
|
|
23682
24090
|
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)}`);
|
|
24091
|
+
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
24092
|
attributeName = resolvedAttributeName;
|
|
23685
24093
|
} else {
|
|
23686
24094
|
const split = splitGetAttStringForm(getAtt);
|
|
@@ -23693,13 +24101,13 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
23693
24101
|
if (!(resource.resourceType === "AWS::EC2::VPC" && attributeName === "Ipv6CidrBlocks") && resource.attributes !== void 0) {
|
|
23694
24102
|
const flatValue = Object.hasOwn(resource.attributes, attributeName) ? resource.attributes[attributeName] : void 0;
|
|
23695
24103
|
if (flatValue !== void 0) {
|
|
23696
|
-
this.rejectPlaceholderArnAttribute(resource, attributeName, flatValue, logicalId);
|
|
24104
|
+
this.rejectPlaceholderArnAttribute(resource, attributeName, flatValue, logicalId, context);
|
|
23697
24105
|
if (resource.resourceType === "AWS::Route53::HostedZone" && attributeName === "NameServers" && typeof flatValue === "string") {
|
|
23698
24106
|
const nameServers = flatValue === "" ? [] : flatValue.split(",");
|
|
23699
|
-
this.logger.debug(`Normalized legacy Fn::GetAtt attribute: ${logicalId}.${attributeName} -> ${stringifyAttributeForLog(attributeName, nameServers)}`);
|
|
24107
|
+
this.logger.debug(`Normalized legacy Fn::GetAtt attribute: ${logicalId}.${this.maskSecretsForLog(attributeName, context)} -> ${this.maskSecretsForLog(stringifyAttributeForLog(attributeName, nameServers), context)}`);
|
|
23700
24108
|
return this.noteAttributeSecrecy(logicalId, attributeName, nameServers, context);
|
|
23701
24109
|
}
|
|
23702
|
-
this.logger.debug(`Resolved Fn::GetAtt from attributes: ${logicalId}.${attributeName} -> ${stringifyAttributeForLog(attributeName, flatValue)}`);
|
|
24110
|
+
this.logger.debug(`Resolved Fn::GetAtt from attributes: ${logicalId}.${this.maskSecretsForLog(attributeName, context)} -> ${this.maskSecretsForLog(stringifyAttributeForLog(attributeName, flatValue), context)}`);
|
|
23703
24111
|
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
24112
|
return this.noteAttributeSecrecy(logicalId, attributeName, flatValue, context);
|
|
23705
24113
|
}
|
|
@@ -23712,17 +24120,17 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
23712
24120
|
break;
|
|
23713
24121
|
}
|
|
23714
24122
|
if (cursor !== void 0) {
|
|
23715
|
-
this.logger.debug(`Resolved Fn::GetAtt from nested attributes: ${logicalId}.${attributeName} -> ${stringifyAttributeForLog(attributeName, cursor)}`);
|
|
24123
|
+
this.logger.debug(`Resolved Fn::GetAtt from nested attributes: ${logicalId}.${this.maskSecretsForLog(attributeName, context)} -> ${this.maskSecretsForLog(stringifyAttributeForLog(attributeName, cursor), context)}`);
|
|
23716
24124
|
return this.noteAttributeSecrecy(logicalId, attributeName, cursor, context);
|
|
23717
24125
|
}
|
|
23718
24126
|
}
|
|
23719
24127
|
}
|
|
23720
24128
|
if (resource.resourceType === NESTED_STACK_RESOURCE_TYPE$1 && attributeName.startsWith(NESTED_STACK_OUTPUT_ATTRIBUTE_PREFIX)) {
|
|
23721
24129
|
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.`));
|
|
24130
|
+
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
24131
|
}
|
|
23724
24132
|
const value = await this.constructGuardedAttribute(resource, attributeName, context, logicalId);
|
|
23725
|
-
this.logger.debug(`Resolved Fn::GetAtt: ${logicalId}.${attributeName} -> ${stringifyAttributeForLog(attributeName, value)}`);
|
|
24133
|
+
this.logger.debug(`Resolved Fn::GetAtt: ${logicalId}.${this.maskSecretsForLog(attributeName, context)} -> ${this.maskSecretsForLog(stringifyAttributeForLog(attributeName, value), context)}`);
|
|
23726
24134
|
return value;
|
|
23727
24135
|
}
|
|
23728
24136
|
/**
|
|
@@ -23773,8 +24181,13 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
23773
24181
|
const declared = context.noEchoAttributeResources?.get(logicalId);
|
|
23774
24182
|
if ((declared === true || declared !== void 0 && declared.has(attributeName)) && context.recordedSecretValues) recordMaskOnlyValuesIn(value, context.recordedSecretValues);
|
|
23775
24183
|
if (context.redactedAttributeReads !== void 0 && carriesSecretMask(value)) {
|
|
23776
|
-
const
|
|
23777
|
-
|
|
24184
|
+
const maskedAttributeName = this.maskSecretsForLog(attributeName, context);
|
|
24185
|
+
this.pushRedactedAttributeRead(context, {
|
|
24186
|
+
kind: "attribute",
|
|
24187
|
+
logicalId,
|
|
24188
|
+
key: maskedAttributeName,
|
|
24189
|
+
display: `${logicalId}.${maskedAttributeName}`
|
|
24190
|
+
});
|
|
23778
24191
|
}
|
|
23779
24192
|
return value;
|
|
23780
24193
|
}
|
|
@@ -23807,10 +24220,10 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
23807
24220
|
* honest answer is to say so and name the remedy: the record heals on the
|
|
23808
24221
|
* resource's next in-place update (#1727).
|
|
23809
24222
|
*/
|
|
23810
|
-
rejectPlaceholderArnAttribute(resource, attributeName, value, logicalId) {
|
|
24223
|
+
rejectPlaceholderArnAttribute(resource, attributeName, value, logicalId, context) {
|
|
23811
24224
|
if (!REF_RETURNS_ARN_FROM_STATE.get(resource.resourceType)?.includes(attributeName)) return;
|
|
23812
24225
|
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.`));
|
|
24226
|
+
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
24227
|
}
|
|
23815
24228
|
/**
|
|
23816
24229
|
* Construct resource attribute value based on resource type, refusing to
|
|
@@ -23855,7 +24268,7 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
23855
24268
|
async constructGuardedAttribute(resource, attributeName, context, logicalId) {
|
|
23856
24269
|
const accountInfo = await getAccountInfo(this.resolverRegion);
|
|
23857
24270
|
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.`);
|
|
24271
|
+
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
24272
|
return value;
|
|
23860
24273
|
}
|
|
23861
24274
|
/**
|
|
@@ -23868,14 +24281,14 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
23868
24281
|
* result. Keep this method's NAME — `scripts/gen-sdk-attr-coverage.ts` reads
|
|
23869
24282
|
* the resource types it references.
|
|
23870
24283
|
*/
|
|
23871
|
-
async constructAttribute(resource, attributeName,
|
|
24284
|
+
async constructAttribute(resource, attributeName, context, logicalId, accountInfo) {
|
|
23872
24285
|
const { resourceType, physicalId } = resource;
|
|
23873
24286
|
const { accountId, partition } = accountInfo;
|
|
23874
24287
|
const region = canonicalizeRegion(accountInfo.region);
|
|
23875
24288
|
if (resourceType === "AWS::DynamoDB::Table" || resourceType === "AWS::DynamoDB::GlobalTable") switch (attributeName) {
|
|
23876
24289
|
case "Arn": return `arn:${partition}:dynamodb:${region}:${accountId}:table/${physicalId}`;
|
|
23877
24290
|
case "StreamArn": return;
|
|
23878
|
-
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
|
|
24291
|
+
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
|
|
23879
24292
|
}
|
|
23880
24293
|
if (resourceType === "AWS::S3::Bucket") switch (attributeName) {
|
|
23881
24294
|
case "Arn": return s3BucketArn(physicalId, region);
|
|
@@ -23883,12 +24296,12 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
23883
24296
|
case "RegionalDomainName": return s3BucketRegionalDomainName(physicalId, region);
|
|
23884
24297
|
case "DualStackDomainName": return s3BucketDualStackDomainName(physicalId, region);
|
|
23885
24298
|
case "WebsiteURL": return s3BucketWebsiteUrl(physicalId, region);
|
|
23886
|
-
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
|
|
24299
|
+
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
|
|
23887
24300
|
}
|
|
23888
24301
|
if (resourceType === "AWS::IAM::Role") switch (attributeName) {
|
|
23889
24302
|
case "Arn": return `arn:${partition}:iam::${accountId}:role/${physicalId}`;
|
|
23890
24303
|
case "RoleId": return;
|
|
23891
|
-
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
|
|
24304
|
+
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
|
|
23892
24305
|
}
|
|
23893
24306
|
if (resourceType === "AWS::EC2::VPC") switch (attributeName) {
|
|
23894
24307
|
case "VpcId": return physicalId;
|
|
@@ -23900,7 +24313,7 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
23900
24313
|
const associations = (await ec2.send(new DescribeVpcsCommand({ VpcIds: [physicalId] }))).Vpcs?.[0]?.Ipv6CidrBlockAssociationSet || [];
|
|
23901
24314
|
const blocks = associations.filter((a) => a.Ipv6CidrBlockState?.State === "associated").map((a) => a.Ipv6CidrBlock);
|
|
23902
24315
|
if (blocks.length > 0) {
|
|
23903
|
-
this.logger.debug(`Resolved VPC Ipv6CidrBlocks for ${physicalId}: ${JSON.stringify(blocks)}`);
|
|
24316
|
+
this.logger.debug(`Resolved VPC Ipv6CidrBlocks for ${physicalId}: ${this.maskSecretsForLog(JSON.stringify(this.maskValueLeaves(blocks, context)), context)}`);
|
|
23904
24317
|
return blocks;
|
|
23905
24318
|
}
|
|
23906
24319
|
if (associations.filter((a) => a.Ipv6CidrBlockState?.State === "associating").length === 0) {
|
|
@@ -23917,38 +24330,38 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
23917
24330
|
return [];
|
|
23918
24331
|
}
|
|
23919
24332
|
case "DefaultSecurityGroup": return resource.attributes?.["DefaultSecurityGroup"] || physicalId;
|
|
23920
|
-
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
|
|
24333
|
+
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
|
|
23921
24334
|
}
|
|
23922
24335
|
if (resourceType === "AWS::IAM::Policy") switch (attributeName) {
|
|
23923
24336
|
case "Arn": return `arn:${partition}:iam::${accountId}:policy/${physicalId}`;
|
|
23924
24337
|
case "PolicyId": return;
|
|
23925
|
-
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
|
|
24338
|
+
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
|
|
23926
24339
|
}
|
|
23927
24340
|
if (resourceType === "AWS::IAM::User") switch (attributeName) {
|
|
23928
24341
|
case "Arn": return `arn:${partition}:iam::${accountId}:user/${physicalId}`;
|
|
23929
|
-
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
|
|
24342
|
+
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
|
|
23930
24343
|
}
|
|
23931
24344
|
if (resourceType === "AWS::IAM::Group") switch (attributeName) {
|
|
23932
24345
|
case "Arn": return `arn:${partition}:iam::${accountId}:group/${physicalId}`;
|
|
23933
|
-
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
|
|
24346
|
+
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
|
|
23934
24347
|
}
|
|
23935
24348
|
if (resourceType === "AWS::IAM::InstanceProfile") switch (attributeName) {
|
|
23936
24349
|
case "Arn": return `arn:${partition}:iam::${accountId}:instance-profile/${physicalId}`;
|
|
23937
|
-
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
|
|
24350
|
+
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
|
|
23938
24351
|
}
|
|
23939
24352
|
if (resourceType === "AWS::KMS::Key") switch (attributeName) {
|
|
23940
24353
|
case "Arn": return `arn:${partition}:kms:${region}:${accountId}:key/${physicalId}`;
|
|
23941
24354
|
case "KeyId": return physicalId;
|
|
23942
|
-
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
|
|
24355
|
+
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
|
|
23943
24356
|
}
|
|
23944
24357
|
if (resourceType === "AWS::Cognito::UserPool") switch (attributeName) {
|
|
23945
24358
|
case "Arn": return `arn:${partition}:cognito-idp:${region}:${accountId}:userpool/${physicalId}`;
|
|
23946
24359
|
case "UserPoolId": return physicalId;
|
|
23947
|
-
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
|
|
24360
|
+
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
|
|
23948
24361
|
}
|
|
23949
24362
|
if (resourceType === "AWS::Kinesis::Stream") switch (attributeName) {
|
|
23950
24363
|
case "Arn": return `arn:${partition}:kinesis:${region}:${accountId}:stream/${physicalId}`;
|
|
23951
|
-
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
|
|
24364
|
+
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
|
|
23952
24365
|
}
|
|
23953
24366
|
if (resourceType === "AWS::Events::Rule") switch (attributeName) {
|
|
23954
24367
|
case "Arn": {
|
|
@@ -23958,41 +24371,41 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
23958
24371
|
const busName = bus.startsWith("arn:") ? bus.split("/").pop() || "" : bus;
|
|
23959
24372
|
return busName ? `arn:${partition}:events:${region}:${accountId}:rule/${busName}/${physicalId}` : `arn:${partition}:events:${region}:${accountId}:rule/${physicalId}`;
|
|
23960
24373
|
}
|
|
23961
|
-
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
|
|
24374
|
+
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
|
|
23962
24375
|
}
|
|
23963
24376
|
if (resourceType === "AWS::Events::EventBus") switch (attributeName) {
|
|
23964
24377
|
case "Arn": return `arn:${partition}:events:${region}:${accountId}:event-bus/${physicalId}`;
|
|
23965
24378
|
case "Name": return physicalId;
|
|
23966
|
-
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
|
|
24379
|
+
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
|
|
23967
24380
|
}
|
|
23968
24381
|
if (resourceType === "AWS::EFS::FileSystem") switch (attributeName) {
|
|
23969
24382
|
case "Arn": return `arn:${partition}:elasticfilesystem:${region}:${accountId}:file-system/${physicalId}`;
|
|
23970
24383
|
case "FileSystemId": return physicalId;
|
|
23971
|
-
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
|
|
24384
|
+
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
|
|
23972
24385
|
}
|
|
23973
24386
|
if (resourceType === "AWS::KinesisFirehose::DeliveryStream") switch (attributeName) {
|
|
23974
24387
|
case "Arn": return `arn:${partition}:firehose:${region}:${accountId}:deliverystream/${physicalId}`;
|
|
23975
|
-
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
|
|
24388
|
+
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
|
|
23976
24389
|
}
|
|
23977
24390
|
if (resourceType === "AWS::CodeBuild::Project") switch (attributeName) {
|
|
23978
24391
|
case "Arn": return `arn:${partition}:codebuild:${region}:${accountId}:project/${physicalId}`;
|
|
23979
|
-
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
|
|
24392
|
+
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
|
|
23980
24393
|
}
|
|
23981
24394
|
if (resourceType === "AWS::CloudTrail::Trail") switch (attributeName) {
|
|
23982
24395
|
case "Arn":
|
|
23983
24396
|
if (physicalId.startsWith("arn:")) return physicalId;
|
|
23984
24397
|
return `arn:${partition}:cloudtrail:${region}:${accountId}:trail/${physicalId}`;
|
|
23985
|
-
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
|
|
24398
|
+
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
|
|
23986
24399
|
}
|
|
23987
24400
|
if (resourceType === "AWS::AppSync::GraphQLApi") switch (attributeName) {
|
|
23988
24401
|
case "Arn": return `arn:${partition}:appsync:${region}:${accountId}:apis/${physicalId}`;
|
|
23989
24402
|
case "ApiId": return physicalId;
|
|
23990
|
-
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
|
|
24403
|
+
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
|
|
23991
24404
|
}
|
|
23992
24405
|
if (resourceType === "AWS::ApiGatewayV2::Api") switch (attributeName) {
|
|
23993
24406
|
case "ExecuteApiArn": return `arn:${partition}:execute-api:${region}:${accountId}:${physicalId}`;
|
|
23994
24407
|
case "ApiId": return physicalId;
|
|
23995
|
-
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
|
|
24408
|
+
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
|
|
23996
24409
|
}
|
|
23997
24410
|
if (resourceType === "AWS::ServiceDiscovery::PrivateDnsNamespace" || resourceType === "AWS::ServiceDiscovery::HttpNamespace" || resourceType === "AWS::ServiceDiscovery::PublicDnsNamespace") switch (attributeName) {
|
|
23998
24411
|
case "Arn": return `arn:${partition}:servicediscovery:${region}:${accountId}:namespace/${physicalId}`;
|
|
@@ -24004,38 +24417,38 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
24004
24417
|
this.logger.warn(`Failed to fetch HostedZoneId for namespace ${physicalId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
24005
24418
|
return;
|
|
24006
24419
|
}
|
|
24007
|
-
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
|
|
24420
|
+
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
|
|
24008
24421
|
}
|
|
24009
24422
|
if (resourceType === "AWS::ServiceDiscovery::Service") switch (attributeName) {
|
|
24010
24423
|
case "Arn": return `arn:${partition}:servicediscovery:${region}:${accountId}:service/${physicalId}`;
|
|
24011
24424
|
case "Id": return physicalId;
|
|
24012
|
-
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
|
|
24425
|
+
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
|
|
24013
24426
|
}
|
|
24014
24427
|
if (resourceType === "AWS::CloudWatch::Alarm") switch (attributeName) {
|
|
24015
24428
|
case "Arn": return `arn:${partition}:cloudwatch:${region}:${accountId}:alarm:${physicalId}`;
|
|
24016
|
-
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
|
|
24429
|
+
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
|
|
24017
24430
|
}
|
|
24018
24431
|
if (resourceType === "AWS::CloudWatch::CompositeAlarm") switch (attributeName) {
|
|
24019
24432
|
case "Arn": return `arn:${partition}:cloudwatch:${region}:${accountId}:alarm:${physicalId}`;
|
|
24020
|
-
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
|
|
24433
|
+
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
|
|
24021
24434
|
}
|
|
24022
24435
|
if (resourceType === "AWS::RDS::DBInstance" || resourceType === "AWS::DocDB::DBInstance" || resourceType === "AWS::Neptune::DBInstance") switch (attributeName) {
|
|
24023
24436
|
case "DBInstanceArn":
|
|
24024
24437
|
case "Arn": return `arn:${partition}:rds:${region}:${accountId}:db:${physicalId}`;
|
|
24025
|
-
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
|
|
24438
|
+
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
|
|
24026
24439
|
}
|
|
24027
24440
|
if (resourceType === "AWS::RDS::DBCluster" || resourceType === "AWS::DocDB::DBCluster" || resourceType === "AWS::Neptune::DBCluster") switch (attributeName) {
|
|
24028
24441
|
case "DBClusterArn":
|
|
24029
24442
|
case "Arn": return `arn:${partition}:rds:${region}:${accountId}:cluster:${physicalId}`;
|
|
24030
|
-
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
|
|
24443
|
+
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
|
|
24031
24444
|
}
|
|
24032
24445
|
if (resourceType === "AWS::S3Express::DirectoryBucket") switch (attributeName) {
|
|
24033
24446
|
case "Arn": return `arn:${partition}:s3express:${region}:${accountId}:bucket/${physicalId}`;
|
|
24034
|
-
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
|
|
24447
|
+
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
|
|
24035
24448
|
}
|
|
24036
24449
|
if (resourceType === "AWS::Lambda::Function") switch (attributeName) {
|
|
24037
24450
|
case "Arn": return `arn:${partition}:lambda:${region}:${accountId}:function:${physicalId}`;
|
|
24038
|
-
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
|
|
24451
|
+
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
|
|
24039
24452
|
}
|
|
24040
24453
|
if (resourceType === "AWS::SQS::Queue") {
|
|
24041
24454
|
let queueName = physicalId;
|
|
@@ -24047,26 +24460,26 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
24047
24460
|
case "Arn": return `arn:${partition}:sqs:${region}:${accountId}:${queueName}`;
|
|
24048
24461
|
case "QueueUrl": return physicalId;
|
|
24049
24462
|
case "QueueName": return queueName;
|
|
24050
|
-
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
|
|
24463
|
+
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
|
|
24051
24464
|
}
|
|
24052
24465
|
}
|
|
24053
24466
|
if (resourceType === "AWS::SNS::Topic") switch (attributeName) {
|
|
24054
24467
|
case "TopicArn": return `arn:${partition}:sns:${region}:${accountId}:${physicalId}`;
|
|
24055
24468
|
case "TopicName": return physicalId;
|
|
24056
|
-
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
|
|
24469
|
+
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
|
|
24057
24470
|
}
|
|
24058
24471
|
if (resourceType === "AWS::Logs::LogGroup") switch (attributeName) {
|
|
24059
24472
|
case "Arn": return `arn:${partition}:logs:${region}:${accountId}:log-group:${physicalId}:*`;
|
|
24060
|
-
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
|
|
24473
|
+
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
|
|
24061
24474
|
}
|
|
24062
24475
|
if (resourceType === "AWS::ECR::Repository") switch (attributeName) {
|
|
24063
24476
|
case "Arn": return `arn:${partition}:ecr:${region}:${accountId}:repository/${physicalId}`;
|
|
24064
24477
|
case "RepositoryUri": return `${accountId}.dkr.ecr.${region}.${derivePartitionAndUrlSuffix(region).urlSuffix}/${physicalId}`;
|
|
24065
|
-
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
|
|
24478
|
+
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
|
|
24066
24479
|
}
|
|
24067
24480
|
if (resourceType === "AWS::ECS::Cluster") switch (attributeName) {
|
|
24068
24481
|
case "Arn": return `arn:${partition}:ecs:${region}:${accountId}:cluster/${physicalId}`;
|
|
24069
|
-
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
|
|
24482
|
+
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
|
|
24070
24483
|
}
|
|
24071
24484
|
if (resourceType === "AWS::ECS::Service") switch (attributeName) {
|
|
24072
24485
|
case "Name": {
|
|
@@ -24090,16 +24503,16 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
24090
24503
|
const serviceName = physicalId.substring(pipeIdx + 1);
|
|
24091
24504
|
return `${left.substring(0, clusterIdx)}:service/${clusterName}/${serviceName}`;
|
|
24092
24505
|
}
|
|
24093
|
-
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
|
|
24506
|
+
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
|
|
24094
24507
|
}
|
|
24095
24508
|
if (resourceType === "AWS::EC2::SecurityGroup") switch (attributeName) {
|
|
24096
24509
|
case "GroupId": return physicalId;
|
|
24097
24510
|
case "VpcId": return;
|
|
24098
|
-
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
|
|
24511
|
+
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
|
|
24099
24512
|
}
|
|
24100
24513
|
if (resourceType === "AWS::EC2::Subnet") switch (attributeName) {
|
|
24101
24514
|
case "SubnetId": return physicalId;
|
|
24102
|
-
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
|
|
24515
|
+
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
|
|
24103
24516
|
}
|
|
24104
24517
|
if (resourceType === "AWS::EC2::Instance") switch (attributeName) {
|
|
24105
24518
|
case "InstanceId": return physicalId;
|
|
@@ -24133,13 +24546,13 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
24133
24546
|
cachedEc2InstanceAttributes[cacheKey] = value;
|
|
24134
24547
|
return value;
|
|
24135
24548
|
}
|
|
24136
|
-
this.logger.warn(`DescribeInstances(${physicalId}) returned no ${attributeName}; returning physical ID`);
|
|
24549
|
+
this.logger.warn(`DescribeInstances(${physicalId}) returned no ${this.maskSecretsForLog(attributeName, context)}; returning physical ID`);
|
|
24137
24550
|
} catch (err) {
|
|
24138
|
-
this.logger.warn(`DescribeInstances(${physicalId}) failed for ${attributeName}: ${err instanceof Error ? err.message : String(err)}`);
|
|
24551
|
+
this.logger.warn(`DescribeInstances(${physicalId}) failed for ${this.maskSecretsForLog(attributeName, context)}: ${this.maskSecretsForLog(err instanceof Error ? err.message : String(err), context)}`);
|
|
24139
24552
|
}
|
|
24140
24553
|
return physicalId;
|
|
24141
24554
|
}
|
|
24142
|
-
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
|
|
24555
|
+
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
|
|
24143
24556
|
}
|
|
24144
24557
|
if (resourceType === "AWS::EC2::LaunchTemplate") {
|
|
24145
24558
|
if (attributeName === "LatestVersionNumber" || attributeName === "DefaultVersionNumber") {
|
|
@@ -24148,14 +24561,14 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
24148
24561
|
const value = attributeName === "LatestVersionNumber" ? lt?.LatestVersionNumber : lt?.DefaultVersionNumber;
|
|
24149
24562
|
if (value !== void 0 && value !== null) return String(value);
|
|
24150
24563
|
} catch (err) {
|
|
24151
|
-
this.logger.warn(`DescribeLaunchTemplates(${physicalId}) failed for ${attributeName}: ${err instanceof Error ? err.message : String(err)}`);
|
|
24564
|
+
this.logger.warn(`DescribeLaunchTemplates(${physicalId}) failed for ${this.maskSecretsForLog(attributeName, context)}: ${this.maskSecretsForLog(err instanceof Error ? err.message : String(err), context)}`);
|
|
24152
24565
|
}
|
|
24153
24566
|
return attributeName === "LatestVersionNumber" ? "$Latest" : "$Default";
|
|
24154
24567
|
}
|
|
24155
24568
|
if (attributeName === "LaunchTemplateId") return physicalId;
|
|
24156
|
-
return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
|
|
24569
|
+
return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
|
|
24157
24570
|
}
|
|
24158
|
-
return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
|
|
24571
|
+
return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
|
|
24159
24572
|
}
|
|
24160
24573
|
/**
|
|
24161
24574
|
* Shared unknown-attribute physicalId fallback (issues #1106 / #1111).
|
|
@@ -24184,13 +24597,16 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
24184
24597
|
* route through this helper — those are explicit `case`s in the
|
|
24185
24598
|
* per-type handlers.
|
|
24186
24599
|
*/
|
|
24187
|
-
guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId) {
|
|
24600
|
+
guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context) {
|
|
24188
24601
|
const expectsArnShape = attributeName.endsWith("Arn") && !physicalId.startsWith("arn:");
|
|
24189
24602
|
const expectsUrlShape = attributeName.endsWith("Url") && !/^https?:\/\//.test(physicalId);
|
|
24190
|
-
if (expectsArnShape || expectsUrlShape)
|
|
24191
|
-
|
|
24603
|
+
if (expectsArnShape || expectsUrlShape) {
|
|
24604
|
+
const expectedShape = expectsArnShape ? "an ARN (arn:...)" : "a URL (http(s)://...)";
|
|
24605
|
+
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)}.`));
|
|
24606
|
+
}
|
|
24607
|
+
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
24608
|
this.physicalIdFallbackCount++;
|
|
24193
|
-
this.logger.warn(`Unknown attribute ${attributeName} for resource type ${resourceType}, returning physical ID`);
|
|
24609
|
+
this.logger.warn(`Unknown attribute ${this.maskSecretsForLog(attributeName, context)} for resource type ${resourceType}, returning physical ID`);
|
|
24194
24610
|
return physicalId;
|
|
24195
24611
|
}
|
|
24196
24612
|
/**
|
|
@@ -24391,13 +24807,13 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
24391
24807
|
} catch (getAttError) {
|
|
24392
24808
|
if (getAttError instanceof IntrinsicResolutionRefusalError) throw getAttError;
|
|
24393
24809
|
this.rethrowStructuralSubFailure(varNameStr, getAttError, context);
|
|
24394
|
-
this.logger.warn(this.subPlaceholderWarning(varNameStr, getAttError));
|
|
24810
|
+
this.logger.warn(this.maskSecretsForLog(this.subPlaceholderWarning(varNameStr, getAttError), context));
|
|
24395
24811
|
replacement = match[0];
|
|
24396
24812
|
}
|
|
24397
24813
|
else {
|
|
24398
24814
|
if (refError instanceof IntrinsicResolutionRefusalError) throw refError;
|
|
24399
24815
|
this.rethrowStructuralSubFailure(varNameStr, refError, context);
|
|
24400
|
-
this.logger.warn(this.subPlaceholderWarning(varNameStr, refError));
|
|
24816
|
+
this.logger.warn(this.maskSecretsForLog(this.subPlaceholderWarning(varNameStr, refError), context));
|
|
24401
24817
|
replacement = match[0];
|
|
24402
24818
|
}
|
|
24403
24819
|
}
|
|
@@ -24431,7 +24847,7 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
24431
24847
|
return `{{Fn::Select:${index}:OutOfBounds}}`;
|
|
24432
24848
|
}
|
|
24433
24849
|
const result = resolvedList[index];
|
|
24434
|
-
this.logger.debug(`Resolved Fn::Select: index ${index} -> ${
|
|
24850
|
+
this.logger.debug(`Resolved Fn::Select: index ${index} -> ${JSON.stringify(this.maskValueLeaves(result, context))}`);
|
|
24435
24851
|
return result;
|
|
24436
24852
|
}
|
|
24437
24853
|
/**
|
|
@@ -24575,7 +24991,7 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
24575
24991
|
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
24992
|
}
|
|
24577
24993
|
const result = resolvedValue.split(delimiter);
|
|
24578
|
-
this.logger.debug(`Resolved Fn::Split: split by "${delimiter}" -> ${
|
|
24994
|
+
this.logger.debug(`Resolved Fn::Split: split by "${delimiter}" -> ${JSON.stringify(this.maskValueLeaves(result, context))}`);
|
|
24579
24995
|
return result;
|
|
24580
24996
|
}
|
|
24581
24997
|
/**
|
|
@@ -24606,7 +25022,7 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
24606
25022
|
const resolved1 = await this.resolveValue(value1, context);
|
|
24607
25023
|
const resolved2 = await this.resolveValue(value2, context);
|
|
24608
25024
|
const result = JSON.stringify(resolved1) === JSON.stringify(resolved2);
|
|
24609
|
-
this.logger.debug(`Resolved Fn::Equals: ${
|
|
25025
|
+
this.logger.debug(`Resolved Fn::Equals: ${JSON.stringify(this.maskValueLeaves(resolved1, context))} === ${JSON.stringify(this.maskValueLeaves(resolved2, context))} -> ${result}`);
|
|
24610
25026
|
return result;
|
|
24611
25027
|
}
|
|
24612
25028
|
/**
|
|
@@ -24754,7 +25170,13 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
24754
25170
|
if (context.recordedSecretValues) recordMaskOnlyValuesIn(recovered, context.recordedSecretValues);
|
|
24755
25171
|
return recovered;
|
|
24756
25172
|
}
|
|
24757
|
-
if (context.redactedAttributeReads !== void 0
|
|
25173
|
+
if (context.redactedAttributeReads !== void 0) {
|
|
25174
|
+
const loggedOrigin = this.maskSecretsForLog(origin, context);
|
|
25175
|
+
this.pushRedactedAttributeRead(context, {
|
|
25176
|
+
kind: "cross-stack",
|
|
25177
|
+
display: loggedOrigin
|
|
25178
|
+
});
|
|
25179
|
+
}
|
|
24758
25180
|
}
|
|
24759
25181
|
if (!carriesDynamicReference(value)) return value;
|
|
24760
25182
|
const resolver = this.resolverForProducerRegion(producerRegion);
|
|
@@ -24870,12 +25292,12 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
24870
25292
|
try {
|
|
24871
25293
|
entry = await context.exportIndex.lookup(exportName);
|
|
24872
25294
|
} 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`);
|
|
25295
|
+
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
25296
|
entry = void 0;
|
|
24875
25297
|
}
|
|
24876
25298
|
if (entry && (!context.stackName || entry.producerStack !== context.stackName)) {
|
|
24877
25299
|
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"})`);
|
|
25300
|
+
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
25301
|
return await this.reresolveCrossStackValue(entry.value, entry.producerRegion, context, `Fn::ImportValue '${exportName}' (producer ${entry.producerStack} / ${entry.producerRegion})`, sourceKey, {
|
|
24880
25302
|
stackName: entry.producerStack,
|
|
24881
25303
|
region: entry.producerRegion,
|
|
@@ -24889,30 +25311,30 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
24889
25311
|
for (const ref of allStacks) {
|
|
24890
25312
|
const { stackName: refStack, region: refRegion } = ref;
|
|
24891
25313
|
if (context.stackName && refStack === context.stackName) {
|
|
24892
|
-
this.logger.debug(`Skipping current stack: ${refStack}`);
|
|
25314
|
+
this.logger.debug(`Skipping current stack: ${this.maskSecretsForLog(refStack, context)}`);
|
|
24893
25315
|
continue;
|
|
24894
25316
|
}
|
|
24895
25317
|
try {
|
|
24896
25318
|
const lookupRegion = refRegion ?? this.resolverRegion ?? "";
|
|
24897
25319
|
if (!lookupRegion) {
|
|
24898
|
-
this.logger.debug(`No region available for stack '${refStack}' — skipping (cdkd cannot read state without a region)`);
|
|
25320
|
+
this.logger.debug(`No region available for stack '${this.maskSecretsForLog(refStack, context)}' — skipping (cdkd cannot read state without a region)`);
|
|
24899
25321
|
continue;
|
|
24900
25322
|
}
|
|
24901
25323
|
const stateData = await context.stateBackend.getState(refStack, lookupRegion);
|
|
24902
25324
|
if (!stateData) {
|
|
24903
|
-
this.logger.debug(`No state found for stack: ${refStack} (${lookupRegion})`);
|
|
25325
|
+
this.logger.debug(`No state found for stack: ${this.maskSecretsForLog(refStack, context)} (${lookupRegion})`);
|
|
24904
25326
|
continue;
|
|
24905
25327
|
}
|
|
24906
25328
|
const { state } = stateData;
|
|
24907
25329
|
if (importableOutputKeys(state).includes(exportName)) {
|
|
24908
25330
|
const value = state.outputs[exportName];
|
|
24909
|
-
this.logger.info(`Resolved Fn::ImportValue: ${loggedExportName} (from stack: ${refStack} / ${lookupRegion}; ${carriesDynamicReference(value) ? "redacted dynamic reference" : "literal value"})`);
|
|
25331
|
+
this.logger.info(`Resolved Fn::ImportValue: ${loggedExportName} (from stack: ${this.maskSecretsForLog(refStack, context)} / ${lookupRegion}; ${carriesDynamicReference(value) ? "redacted dynamic reference" : "literal value"})`);
|
|
24910
25332
|
if (context.exportIndex) context.exportIndex.patchEntry(exportName, {
|
|
24911
25333
|
value,
|
|
24912
25334
|
producerStack: refStack,
|
|
24913
25335
|
producerRegion: lookupRegion
|
|
24914
25336
|
}).catch((err) => {
|
|
24915
|
-
this.logger.debug(`Failed to patch exports index for '${exportName}': ${err instanceof Error ? err.message : String(err)}`);
|
|
25337
|
+
this.logger.debug(`Failed to patch exports index for '${this.maskSecretsForLog(exportName, context)}': ${this.maskSecretsForLog(err instanceof Error ? err.message : String(err), context)}`);
|
|
24916
25338
|
});
|
|
24917
25339
|
this.recordImport(context, exportName, refStack, lookupRegion);
|
|
24918
25340
|
found = {
|
|
@@ -24923,7 +25345,7 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
24923
25345
|
break;
|
|
24924
25346
|
}
|
|
24925
25347
|
} catch (error) {
|
|
24926
|
-
this.logger.warn(`Failed to read state for stack ${refStack}: ${error instanceof Error ? error.message : String(error)}`);
|
|
25348
|
+
this.logger.warn(`Failed to read state for stack ${this.maskSecretsForLog(refStack, context)}: ${this.maskSecretsForLog(error instanceof Error ? error.message : String(error), context)}`);
|
|
24927
25349
|
continue;
|
|
24928
25350
|
}
|
|
24929
25351
|
}
|
|
@@ -24939,7 +25361,7 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
24939
25361
|
return cfnExport.value;
|
|
24940
25362
|
}
|
|
24941
25363
|
}
|
|
24942
|
-
throw new Error(`Fn::ImportValue: export '${
|
|
25364
|
+
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
25365
|
}
|
|
24944
25366
|
/**
|
|
24945
25367
|
* CloudFormation `ListExports` fallback lookup for `Fn::ImportValue`
|
|
@@ -24970,7 +25392,7 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
24970
25392
|
};
|
|
24971
25393
|
return;
|
|
24972
25394
|
} 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.`);
|
|
25395
|
+
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
25396
|
return;
|
|
24975
25397
|
}
|
|
24976
25398
|
}
|
|
@@ -25040,7 +25462,7 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
25040
25462
|
return await fetch;
|
|
25041
25463
|
} catch (error) {
|
|
25042
25464
|
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.`);
|
|
25465
|
+
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
25466
|
return;
|
|
25045
25467
|
}
|
|
25046
25468
|
}
|
|
@@ -25149,7 +25571,7 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
25149
25571
|
const resolvedRegion = await this.resolveValue(args["Region"], context);
|
|
25150
25572
|
if (typeof resolvedRegion !== "string" || resolvedRegion === "") throw new Error(`Fn::GetStackOutput: Region must resolve to a non-empty string, got ${typeof resolvedRegion}`);
|
|
25151
25573
|
const requestedRegion = canonicalizeRegion(resolvedRegion);
|
|
25152
|
-
if (!isClientSafeRegion(requestedRegion)) throw new Error(`Fn::GetStackOutput: '${
|
|
25574
|
+
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
25575
|
region = requestedRegion;
|
|
25154
25576
|
}
|
|
25155
25577
|
let roleArn;
|
|
@@ -25158,10 +25580,11 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
25158
25580
|
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
25581
|
roleArn = raw;
|
|
25160
25582
|
}
|
|
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}'`);
|
|
25583
|
+
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
25584
|
const loggedStackName = this.maskSecretsForLog(stackName, context);
|
|
25163
25585
|
const loggedOutputName = this.maskSecretsForLog(outputName, context);
|
|
25164
|
-
this.
|
|
25586
|
+
const loggedRegion = this.maskSecretsForLog(region, context);
|
|
25587
|
+
this.logger.debug(`Resolving Fn::GetStackOutput: StackName=${loggedStackName}, Region=${loggedRegion}, OutputName=${loggedOutputName}${roleArn ? `, RoleArn=${roleArn}` : ""}`);
|
|
25165
25588
|
const stateData = roleArn ? await this.getCrossAccountStackState(roleArn, stackName, region, context) : await this.getSameAccountStackState(stackName, region, context);
|
|
25166
25589
|
if (!stateData) {
|
|
25167
25590
|
if (!roleArn && this.cfnFallback) {
|
|
@@ -25169,24 +25592,24 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
25169
25592
|
if (cfnOutputs) {
|
|
25170
25593
|
if (!Object.hasOwn(cfnOutputs, outputName)) {
|
|
25171
25594
|
const available = this.describeAvailableOutputs(Object.keys(cfnOutputs), context);
|
|
25172
|
-
throw new Error(`Fn::GetStackOutput: output '${
|
|
25595
|
+
throw new Error(`Fn::GetStackOutput: output '${loggedOutputName}' not found in CloudFormation stack '${loggedStackName}' (${loggedRegion}). Available outputs: ${available}`);
|
|
25173
25596
|
}
|
|
25174
25597
|
const value = cfnOutputs[outputName];
|
|
25175
|
-
this.logger.info(`Resolved Fn::GetStackOutput: StackName=${loggedStackName}, Region=${
|
|
25598
|
+
this.logger.info(`Resolved Fn::GetStackOutput: StackName=${loggedStackName}, Region=${loggedRegion}, OutputName=${loggedOutputName} (from CloudFormation stack outputs; weak reference — producer is not cdkd-managed)`);
|
|
25176
25599
|
return value;
|
|
25177
25600
|
}
|
|
25178
25601
|
}
|
|
25179
|
-
throw new Error(`Fn::GetStackOutput: stack '${
|
|
25602
|
+
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
25603
|
}
|
|
25181
25604
|
const outputs = stateData.state.outputs ?? {};
|
|
25182
25605
|
if (!Object.hasOwn(outputs, outputName)) {
|
|
25183
25606
|
const available = this.describeAvailableOutputs(Object.keys(outputs), context);
|
|
25184
|
-
throw new Error(`Fn::GetStackOutput: output '${
|
|
25607
|
+
throw new Error(`Fn::GetStackOutput: output '${loggedOutputName}' not found in stack '${loggedStackName}' (${loggedRegion}). Available outputs: ${available}`);
|
|
25185
25608
|
}
|
|
25186
25609
|
const value = outputs[outputName];
|
|
25187
|
-
this.logger.info(`Resolved Fn::GetStackOutput: StackName=${loggedStackName}, Region=${
|
|
25610
|
+
this.logger.info(`Resolved Fn::GetStackOutput: StackName=${loggedStackName}, Region=${loggedRegion}, OutputName=${loggedOutputName}${roleArn ? `, RoleArn=${roleArn}` : ""} (${carriesDynamicReference(value) ? "redacted dynamic reference" : "literal value"})`);
|
|
25188
25611
|
if (!roleArn) this.recordOutputRead(context, stackName, region, outputName);
|
|
25189
|
-
if (roleArn && !context.skipDynamicReferences && carriesDynamicReference(value)) throw markNonRetryable(new CrossAccountSecretRefusalError(`Fn::GetStackOutput: output '${
|
|
25612
|
+
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
25613
|
return await this.reresolveCrossStackValue(value, region, context, `Fn::GetStackOutput '${outputName}' (producer ${stackName} / ${region})`, sourceKey, {
|
|
25191
25614
|
stackName,
|
|
25192
25615
|
region,
|
|
@@ -25304,19 +25727,19 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
25304
25727
|
}
|
|
25305
25728
|
if (!map) {
|
|
25306
25729
|
if (hasDefaultValue) return await resolveDefault();
|
|
25307
|
-
throw new Error(`Fn::FindInMap: mapping '${mapName}' not found in Mappings section`);
|
|
25730
|
+
throw new Error(`Fn::FindInMap: mapping '${this.maskSecretsForLog(mapName, context)}' not found in Mappings section`);
|
|
25308
25731
|
}
|
|
25309
25732
|
const topLevel = Object.hasOwn(map, topLevelKey) ? map[topLevelKey] : void 0;
|
|
25310
25733
|
if (!topLevel || typeof topLevel !== "object") {
|
|
25311
25734
|
if (hasDefaultValue) return await resolveDefault();
|
|
25312
|
-
throw new Error(`Fn::FindInMap: top-level key '${topLevelKey}' not found in mapping '${mapName}'`);
|
|
25735
|
+
throw new Error(`Fn::FindInMap: top-level key '${this.maskSecretsForLog(topLevelKey, context)}' not found in mapping '${this.maskSecretsForLog(mapName, context)}'`);
|
|
25313
25736
|
}
|
|
25314
25737
|
if (!Object.hasOwn(topLevel, secondLevelKey)) {
|
|
25315
25738
|
if (hasDefaultValue) return await resolveDefault();
|
|
25316
|
-
throw new Error(`Fn::FindInMap: second-level key '${secondLevelKey}' not found in mapping '${mapName}' -> '${topLevelKey}'`);
|
|
25739
|
+
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
25740
|
}
|
|
25318
25741
|
const result = topLevel[secondLevelKey];
|
|
25319
|
-
this.logger.debug(`Resolved Fn::FindInMap: ${mapName}.${topLevelKey}.${secondLevelKey} -> ${JSON.stringify(result)}`);
|
|
25742
|
+
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
25743
|
return result;
|
|
25321
25744
|
}
|
|
25322
25745
|
/**
|
|
@@ -25329,6 +25752,7 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
25329
25752
|
const resolvedValue = await this.resolveValue(value, context);
|
|
25330
25753
|
if (typeof resolvedValue !== "string") throw new Error(`Fn::Base64: value must resolve to a string, got ${typeof resolvedValue}`);
|
|
25331
25754
|
const result = Buffer.from(resolvedValue).toString("base64");
|
|
25755
|
+
if (context.recordedSecretValues && this.maskSecretsForLog(resolvedValue, context) !== resolvedValue) recordMaskOnlyValue(context.recordedSecretValues, result);
|
|
25332
25756
|
this.logger.debug(`Resolved Fn::Base64: ${this.maskSecretsForLog(resolvedValue, context)} -> ${this.maskSecretsForLog(result, context)}`);
|
|
25333
25757
|
return result;
|
|
25334
25758
|
}
|
|
@@ -25360,7 +25784,7 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
25360
25784
|
let clientRegion;
|
|
25361
25785
|
if (typeof resolvedValue === "string" && resolvedValue !== "") {
|
|
25362
25786
|
const requested = canonicalizeRegion(resolvedValue);
|
|
25363
|
-
if (!isClientSafeRegion(requested)) throw new Error(`Fn::GetAZs: '${
|
|
25787
|
+
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
25788
|
region = requested;
|
|
25365
25789
|
clientRegion = requested;
|
|
25366
25790
|
} else {
|
|
@@ -25369,7 +25793,7 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
25369
25793
|
}
|
|
25370
25794
|
const cached = cachedAvailabilityZones[region];
|
|
25371
25795
|
if (cached) {
|
|
25372
|
-
this.logger.debug(`Resolved Fn::GetAZs from cache: ${region} -> ${JSON.stringify(cached)}`);
|
|
25796
|
+
this.logger.debug(`Resolved Fn::GetAZs from cache: ${this.maskSecretsForLog(region, context)} -> ${JSON.stringify(this.maskValueLeaves(cached, context))}`);
|
|
25373
25797
|
return cached;
|
|
25374
25798
|
}
|
|
25375
25799
|
const ec2Client = this.clientsForRegion(clientRegion).ec2;
|
|
@@ -25383,11 +25807,11 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
25383
25807
|
Values: ["available"]
|
|
25384
25808
|
}] }))).AvailabilityZones || []).map((az) => az.ZoneName).filter((name) => name !== void 0).sort();
|
|
25385
25809
|
} catch (error) {
|
|
25386
|
-
throw new Error(`Fn::GetAZs: failed to describe availability zones for region '${region}': ${error instanceof Error ? error.message : String(error)}`);
|
|
25810
|
+
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
25811
|
}
|
|
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.`);
|
|
25812
|
+
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
25813
|
cachedAvailabilityZones[region] = azNames;
|
|
25390
|
-
this.logger.debug(`Resolved Fn::GetAZs: ${region} -> ${JSON.stringify(azNames)}`);
|
|
25814
|
+
this.logger.debug(`Resolved Fn::GetAZs: ${this.maskSecretsForLog(region, context)} -> ${JSON.stringify(this.maskValueLeaves(azNames, context))}`);
|
|
25391
25815
|
return azNames;
|
|
25392
25816
|
}
|
|
25393
25817
|
/**
|
|
@@ -25433,6 +25857,95 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
25433
25857
|
if (secrets && secrets.size > 0) masked = maskSecretsInText(masked, secrets);
|
|
25434
25858
|
return masked;
|
|
25435
25859
|
}
|
|
25860
|
+
/**
|
|
25861
|
+
* A copy of `value` with every string LEAF (and every object KEY) masked, for
|
|
25862
|
+
* a caller about to ENCODE it into a message (issue
|
|
25863
|
+
* [#2759](https://github.com/go-to-k/cdkd/issues/2759)).
|
|
25864
|
+
*
|
|
25865
|
+
* `stringifyValue` / `JSON.stringify` ESCAPE a leaf containing `"`, `\` or a
|
|
25866
|
+
* control character, and {@link maskSecretsInText} matches a needle
|
|
25867
|
+
* LITERALLY — so masking the ENCODED text misses exactly the plaintexts the
|
|
25868
|
+
* encoder rewrote (`pa"ss\word12` encodes to `["pa\"ss\\word12"]`, which no
|
|
25869
|
+
* needle matches). Masking each leaf first also buys the WHOLE-VALUE arm,
|
|
25870
|
+
* which has no {@link MIN_NEEDLE_LENGTH} floor, for a leaf that IS the
|
|
25871
|
+
* plaintext.
|
|
25872
|
+
*
|
|
25873
|
+
* Returns the STRUCTURE rather than a rendered string, deliberately: each
|
|
25874
|
+
* call site keeps its own encoder, so this changes which characters are
|
|
25875
|
+
* masked and nothing about how a value RENDERS. Encoding here instead
|
|
25876
|
+
* dropped `JSON.stringify`'s quotes around a bare string and made a `cdkd
|
|
25877
|
+
* scrub` log line unrecognisable to its own test.
|
|
25878
|
+
*
|
|
25879
|
+
* Object KEYS are masked too: a `Fn::Split` / `Fn::GetAtt` chain can put a
|
|
25880
|
+
* resolved value in key position, and an unmasked key discloses exactly as
|
|
25881
|
+
* much as an unmasked value.
|
|
25882
|
+
*
|
|
25883
|
+
* Cycle-safe by MEMOIZATION rather than a depth cap: a self-referential
|
|
25884
|
+
* structure terminates (the replacement is registered before its children are
|
|
25885
|
+
* walked, so the cycle closes on it) and a legal deep one is still walked to
|
|
25886
|
+
* the bottom. A repeated but NON-cyclic sub-object gets its real rendering
|
|
25887
|
+
* rather than a placeholder — see the note at the `Map`.
|
|
25888
|
+
*/
|
|
25889
|
+
maskValueLeaves(value, context) {
|
|
25890
|
+
const done = /* @__PURE__ */ new Map();
|
|
25891
|
+
const walk = (node) => {
|
|
25892
|
+
if (typeof node === "string") return this.maskSecretsForLog(node, context);
|
|
25893
|
+
if (node === null || typeof node !== "object") return node;
|
|
25894
|
+
const memo = done.get(node);
|
|
25895
|
+
if (memo !== void 0) return memo;
|
|
25896
|
+
if (Array.isArray(node)) {
|
|
25897
|
+
const out = [];
|
|
25898
|
+
done.set(node, out);
|
|
25899
|
+
for (const item of node) out.push(walk(item));
|
|
25900
|
+
return out;
|
|
25901
|
+
}
|
|
25902
|
+
const out = Object.create(null);
|
|
25903
|
+
done.set(node, out);
|
|
25904
|
+
for (const [key, child] of Object.entries(node)) out[this.maskSecretsForLog(key, context)] = walk(child);
|
|
25905
|
+
return out;
|
|
25906
|
+
};
|
|
25907
|
+
return walk(value);
|
|
25908
|
+
}
|
|
25909
|
+
/**
|
|
25910
|
+
* Mask `value`, STRIP its control characters, then mask again — the shape a
|
|
25911
|
+
* message that truncates its input needs (issue
|
|
25912
|
+
* [#2827](https://github.com/go-to-k/cdkd/issues/2827) review round 1).
|
|
25913
|
+
*
|
|
25914
|
+
* NEITHER SINGLE ORDER IS CORRECT, and both were measured. Masking AFTER
|
|
25915
|
+
* `stripControlChars` is what this fix was written to avoid: the strip
|
|
25916
|
+
* rewrites the text a literal needle has to match. But masking BEFORE it is
|
|
25917
|
+
* not safe either, because `stripControlChars` DELETES rather than replaces —
|
|
25918
|
+
* so a plaintext SPLIT by an invisible (`S3cret\u200ePassw0rd`) is missed by
|
|
25919
|
+
* the first mask and then RECONSTITUTED contiguous by the strip. That is the
|
|
25920
|
+
* go-to-k/cdkd#2874 class arriving through a different door.
|
|
25921
|
+
*
|
|
25922
|
+
* Masking in BOTH string spaces closes both: the first pass catches a needle
|
|
25923
|
+
* that occurs literally, the second catches one that only becomes contiguous
|
|
25924
|
+
* after stripping. `maskSecretsInText` is idempotent, so the overlap costs
|
|
25925
|
+
* nothing, and the caller truncates AFTERWARDS — never between the two.
|
|
25926
|
+
*
|
|
25927
|
+
* WHICH HALF IS FENCED, stated because a mutation probe made the difference
|
|
25928
|
+
* visible. The FIRST mask is demonstrated by a test: deleting it (masking
|
|
25929
|
+
* only after the strip) reds the split-needle case, because the recorded
|
|
25930
|
+
* needle is then the split form and the strip has destroyed it. The SECOND
|
|
25931
|
+
* mask is NOT reached by any test here and is defence in depth: it earns its
|
|
25932
|
+
* place only when the bag holds a needle that is the STRIPPED form of the
|
|
25933
|
+
* value in hand, and every route through THIS resolver records the value it
|
|
25934
|
+
* actually resolved — so the split copy is itself a needle and the first
|
|
25935
|
+
* mask already catches it. The shape was measured against a hand-built bag
|
|
25936
|
+
* during review, not produced by the resolver. Kept anyway: it is one
|
|
25937
|
+
* idempotent call, and the alternative is re-deciding per future caller
|
|
25938
|
+
* whether the bag and the value can disagree.
|
|
25939
|
+
*
|
|
25940
|
+
* THE BOUND, since this file's job is to state them: this covers a needle
|
|
25941
|
+
* split by a character `stripControlChars` removes. A needle split by
|
|
25942
|
+
* anything else, or one whose canonical form differs for another reason, is
|
|
25943
|
+
* `outputs-export-alias.ts`'s `canonicalForSecretScan` problem and is not
|
|
25944
|
+
* solved here.
|
|
25945
|
+
*/
|
|
25946
|
+
maskThenStripThenMask(value, context) {
|
|
25947
|
+
return this.maskSecretsForLog(stripControlChars(this.maskSecretsForLog(value, context)), context);
|
|
25948
|
+
}
|
|
25436
25949
|
async resolveDynamicReferences(value, context) {
|
|
25437
25950
|
const pattern = /\{\{resolve:([^}]+)\}\}/g;
|
|
25438
25951
|
let result = value;
|
|
@@ -25447,7 +25960,7 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
25447
25960
|
const isKnownSecret = service === "secretsmanager" || service === "ssm-secure" || recordedSecretExpressions.has(fullMatch);
|
|
25448
25961
|
if (isKnownSecret && context?.skipDynamicReferences) continue;
|
|
25449
25962
|
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.`));
|
|
25963
|
+
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
25964
|
if (regionVerdict.kind === "named-region") {
|
|
25452
25965
|
const foreign = await this.resolverForProducerRegion(regionVerdict.region).resolveDynamicReferences(fullMatch, withoutProducerRegions(context));
|
|
25453
25966
|
result = result.replace(fullMatch, () => foreign);
|
|
@@ -25492,7 +26005,7 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
25492
26005
|
resolved = param.value;
|
|
25493
26006
|
} else if (service === "ssm-secure") {
|
|
25494
26007
|
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.`));
|
|
26008
|
+
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
26009
|
isSecret = true;
|
|
25497
26010
|
resolved = param.value;
|
|
25498
26011
|
} else {
|
|
@@ -25556,22 +26069,24 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
25556
26069
|
} else secretId = afterService;
|
|
25557
26070
|
if (!versionStage) versionStage = "AWSCURRENT";
|
|
25558
26071
|
if (!secretId) throw new Error("Dynamic reference: secretsmanager SECRET_ID is required");
|
|
25559
|
-
this.
|
|
26072
|
+
const loggedSecretId = this.maskSecretsForLog(secretId, context);
|
|
26073
|
+
const loggedJsonKey = this.maskSecretsForLog(jsonKey, context);
|
|
26074
|
+
this.logger.debug(`Resolving dynamic reference: secretsmanager:${loggedSecretId}:SecretString:${loggedJsonKey}:${this.maskSecretsForLog(versionStage, context)}:${this.maskSecretsForLog(versionId, context)}`);
|
|
25560
26075
|
const client = this.clientsForRegion(this.explicitRegion).secretsManager;
|
|
25561
26076
|
const command = new GetSecretValueCommand({
|
|
25562
26077
|
SecretId: secretId,
|
|
25563
26078
|
...versionStage && versionStage !== "" && { VersionStage: versionStage },
|
|
25564
26079
|
...versionId && versionId !== "" && { VersionId: versionId }
|
|
25565
26080
|
});
|
|
25566
|
-
const secretString = (await this.sendWithThrottleRetry(() => client.send(command),
|
|
25567
|
-
if (!secretString) throw new Error(`Dynamic reference: secret '${
|
|
26081
|
+
const secretString = (await this.sendWithThrottleRetry(() => client.send(command), `secretsmanager:${loggedSecretId}`)).SecretString;
|
|
26082
|
+
if (!secretString) throw new Error(`Dynamic reference: secret '${loggedSecretId}' does not contain a SecretString value`);
|
|
25568
26083
|
if (jsonKey) try {
|
|
25569
26084
|
const parsed = JSON.parse(secretString);
|
|
25570
26085
|
const keyValue = Object.hasOwn(parsed, jsonKey) ? parsed[jsonKey] : void 0;
|
|
25571
|
-
if (keyValue === void 0) throw new Error(`Dynamic reference: key '${
|
|
26086
|
+
if (keyValue === void 0) throw new Error(`Dynamic reference: key '${loggedJsonKey}' not found in secret '${loggedSecretId}'`);
|
|
25572
26087
|
return stringifyValue(keyValue);
|
|
25573
26088
|
} catch (error) {
|
|
25574
|
-
if (error instanceof SyntaxError) throw new Error(`Dynamic reference: secret '${
|
|
26089
|
+
if (error instanceof SyntaxError) throw new Error(`Dynamic reference: secret '${loggedSecretId}' is not valid JSON but JSON_KEY '${loggedJsonKey}' was specified`);
|
|
25575
26090
|
throw error;
|
|
25576
26091
|
}
|
|
25577
26092
|
return secretString;
|
|
@@ -25597,8 +26112,8 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
25597
26112
|
const ipBlock = await this.resolveValue(rawIpBlock, context);
|
|
25598
26113
|
const count = Number(await this.resolveValue(rawCount, context));
|
|
25599
26114
|
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}`);
|
|
26115
|
+
if (!ipBlock || typeof ipBlock !== "string") throw new Error(`Fn::Cidr: ipBlock must be a string, got ${typeof ipBlock}: ${JSON.stringify(this.maskValueLeaves(ipBlock, context))}`);
|
|
26116
|
+
this.logger.debug(`Resolving Fn::Cidr: ipBlock=${this.maskSecretsForLog(JSON.stringify(this.maskValueLeaves(ipBlock, context)), context)}, count=${count}, cidrBits=${cidrBits}`);
|
|
25602
26117
|
const isIpv6 = ipBlock.includes(":");
|
|
25603
26118
|
const results = [];
|
|
25604
26119
|
if (isIpv6) {
|
|
@@ -25630,7 +26145,7 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
25630
26145
|
results.push(`${a}.${b}.${c}.${d}/${subnetPrefix}`);
|
|
25631
26146
|
}
|
|
25632
26147
|
}
|
|
25633
|
-
this.logger.debug(`Fn::Cidr result: ${JSON.stringify(results)}`);
|
|
26148
|
+
this.logger.debug(`Fn::Cidr result: ${this.maskSecretsForLog(JSON.stringify(this.maskValueLeaves(results, context)), context)}`);
|
|
25634
26149
|
return results;
|
|
25635
26150
|
}
|
|
25636
26151
|
/** Expand IPv6 address to full 8-group form */
|
|
@@ -25720,21 +26235,22 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
25720
26235
|
async resolveSSMReference(parts, decrypt = true, service = "ssm", context) {
|
|
25721
26236
|
const parameterName = parts.slice(1).join(":");
|
|
25722
26237
|
if (!parameterName) throw new Error(`Dynamic reference: ${service} PARAMETER_NAME is required`);
|
|
25723
|
-
this.
|
|
26238
|
+
const loggedParameterName = this.maskSecretsForLog(parameterName, context);
|
|
26239
|
+
this.logger.debug(`Resolving dynamic reference: ${service}:${loggedParameterName}`);
|
|
25724
26240
|
const client = this.clientsForRegion(this.explicitRegion).ssm;
|
|
25725
26241
|
const command = new GetParameterCommand({
|
|
25726
26242
|
Name: parameterName,
|
|
25727
26243
|
WithDecryption: decrypt
|
|
25728
26244
|
});
|
|
25729
|
-
const response = await this.sendWithThrottleRetry(() => client.send(command),
|
|
26245
|
+
const response = await this.sendWithThrottleRetry(() => client.send(command), `${service}:${loggedParameterName}`);
|
|
25730
26246
|
const paramValue = response.Parameter?.Value;
|
|
25731
|
-
if (paramValue === void 0 || paramValue === null) throw new Error(`Dynamic reference: SSM parameter '${
|
|
26247
|
+
if (paramValue === void 0 || paramValue === null) throw new Error(`Dynamic reference: SSM parameter '${loggedParameterName}' not found or has no value`);
|
|
25732
26248
|
const paramType = response.Parameter?.Type;
|
|
25733
26249
|
const secure = paramType !== "String" && paramType !== "StringList";
|
|
25734
26250
|
if (secure && paramType !== "SecureString" && !this.warnedUnrecognizedSsmTypes.has(`${parameterName}\u0000${String(paramType)}`)) {
|
|
25735
26251
|
this.warnedUnrecognizedSsmTypes.add(`${parameterName}\u0000${String(paramType)}`);
|
|
25736
26252
|
const reported = paramType === void 0 ? "(absent)" : `'${String(paramType)}'`;
|
|
25737
|
-
this.logger.warn(
|
|
26253
|
+
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
26254
|
}
|
|
25739
26255
|
return {
|
|
25740
26256
|
value: paramValue,
|
|
@@ -26094,7 +26610,7 @@ async function fetchTopLevelWriteOnlyProperties(resourceType) {
|
|
|
26094
26610
|
if (Array.isArray(writeOnly)) for (const path of writeOnly) {
|
|
26095
26611
|
if (typeof path !== "string") continue;
|
|
26096
26612
|
const match = /^\/properties\/([^/]+)/.exec(path);
|
|
26097
|
-
if (match?.[1]) result.add(unescapeJsonPointerSegment(match[1]));
|
|
26613
|
+
if (match?.[1]) result.add(unescapeJsonPointerSegment$1(match[1]));
|
|
26098
26614
|
}
|
|
26099
26615
|
}
|
|
26100
26616
|
logger.debug(`Resolved ${result.size} top-level write-only properties for ${resourceType}` + (result.size > 0 ? `: ${[...result].join(", ")}` : ""));
|
|
@@ -26103,6 +26619,128 @@ async function fetchTopLevelWriteOnlyProperties(resourceType) {
|
|
|
26103
26619
|
/**
|
|
26104
26620
|
* Unescape an RFC 6901 JSON Pointer segment (`~1` -> `/`, `~0` -> `~`).
|
|
26105
26621
|
*/
|
|
26622
|
+
function unescapeJsonPointerSegment$1(segment) {
|
|
26623
|
+
return segment.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
26624
|
+
}
|
|
26625
|
+
|
|
26626
|
+
//#endregion
|
|
26627
|
+
//#region src/provisioning/read-only-properties.ts
|
|
26628
|
+
/**
|
|
26629
|
+
* Read-only property resolution for `CloudControlProvider.import()` (issue
|
|
26630
|
+
* [#2847](https://github.com/go-to-k/cdkd/issues/2847)) — the read-side sibling
|
|
26631
|
+
* of {@link file://./write-only-properties.ts}, sharing its `DescribeType` +
|
|
26632
|
+
* per-type cache + top-level reduction, and differing from it in exactly one
|
|
26633
|
+
* way that is the point of the module: it distinguishes "this type declares no
|
|
26634
|
+
* read-only properties" from "cdkd could not find out".
|
|
26635
|
+
*
|
|
26636
|
+
* WHY THE DISTINCTION IS LOAD-BEARING. The write-only caller degrades SAFELY to
|
|
26637
|
+
* the empty set (a smaller UPDATE patch). This caller does not: its empty set
|
|
26638
|
+
* would mean "surface no attributes", while its `undefined` means "cdkd cannot
|
|
26639
|
+
* tell an attribute from a property for this type". Collapsing the two into one
|
|
26640
|
+
* `Set` — which is what the write-only sibling's signature does — makes a
|
|
26641
|
+
* missing `cloudformation:DescribeType` permission indistinguishable from a
|
|
26642
|
+
* type with no attributes, and the CALLER's fail-closed arm then cannot be
|
|
26643
|
+
* written at all. So the return type is `ReadonlySet<string> | undefined` and
|
|
26644
|
+
* the failure is the `undefined`.
|
|
26645
|
+
*
|
|
26646
|
+
* WHAT `readOnlyProperties` MEANS HERE. In the CloudFormation registry schema a
|
|
26647
|
+
* type's `readOnlyProperties` are exactly the values `Fn::GetAtt` may read —
|
|
26648
|
+
* that is the definition CloudFormation itself applies, and it is why the
|
|
26649
|
+
* narrowing this module enables is a CORRECTNESS fix as much as a security one.
|
|
26650
|
+
* Cloud Control's `GetResource` returns the whole resource MODEL, whose keys are
|
|
26651
|
+
* every readable property, writable ones included; a `Fn::GetAtt` naming one of
|
|
26652
|
+
* those is rejected by CloudFormation at template validation, so nothing cdkd
|
|
26653
|
+
* drops here was ever a legitimate attribute.
|
|
26654
|
+
*
|
|
26655
|
+
* TOP-LEVEL REDUCTION, deliberately the same convention as the write-only
|
|
26656
|
+
* sibling: a nested pointer `/properties/Endpoint/Address` reduces to
|
|
26657
|
+
* `Endpoint`, so the whole containing property is kept. Keeping the container is
|
|
26658
|
+
* what makes the resolver's nested-path walk (`Endpoint.Address`, issue #381)
|
|
26659
|
+
* keep working; reducing to the leaf instead would drop the object the walk
|
|
26660
|
+
* descends into.
|
|
26661
|
+
*
|
|
26662
|
+
* WHAT THAT COSTS, stated because the caller is a redaction and the reduction
|
|
26663
|
+
* WIDENS what survives it: certifying `Endpoint` certifies the WHOLE container,
|
|
26664
|
+
* including sibling leaves the schema does not declare read-only. A type whose
|
|
26665
|
+
* schema marks `/properties/Endpoint/Address` read-only while `Endpoint` also
|
|
26666
|
+
* carries, say, a credential member would keep that member in the clear. The
|
|
26667
|
+
* alternative — reduce to the leaf and mask the rest of the container — was
|
|
26668
|
+
* rejected because it breaks `Fn::GetAtt Endpoint.Port` for every ordinary
|
|
26669
|
+
* type, which is a certain regression against a hypothetical exposure; a
|
|
26670
|
+
* PATH-precise certification is the real fix and is not what this module does.
|
|
26671
|
+
* The residual sits inside the wider one `import()`'s doc already states (a
|
|
26672
|
+
* read-only attribute that IS a credential is persisted in the clear), so it
|
|
26673
|
+
* adds a shape rather than a new class.
|
|
26674
|
+
*
|
|
26675
|
+
* CACHING matches the sibling exactly, including the part that is easy to get
|
|
26676
|
+
* wrong: the promise stored in the cache is the ALREADY-RECOVERED one, and what
|
|
26677
|
+
* keeps a failure from being cached is the handler DELETING its own entry, not
|
|
26678
|
+
* the cache holding a raw promise. An earlier revision of this module cached
|
|
26679
|
+
* the raw promise and recovered only the calling side — which meant a second
|
|
26680
|
+
* caller arriving inside the failure window received a REJECTION out of a
|
|
26681
|
+
* function whose contract says it never throws. Unreachable at the time (both
|
|
26682
|
+
* `importOne` loops are sequential `for…await`), but the contract is what the
|
|
26683
|
+
* caller's fail-closed arm is written against, so it is honoured here rather
|
|
26684
|
+
* than left to the call graph.
|
|
26685
|
+
*
|
|
26686
|
+
* Only SUCCESSFUL lookups therefore survive in the cache for the process
|
|
26687
|
+
* lifetime, so a transient throttle on the first imported resource of a type
|
|
26688
|
+
* cannot poison every later one. A schema-less response is a SUCCESSFUL lookup
|
|
26689
|
+
* of an empty set, not a failure — a type AWS publishes with no
|
|
26690
|
+
* `readOnlyProperties` genuinely has no attributes, and answering `undefined`
|
|
26691
|
+
* there would make the caller emit a missing-permission warning for a healthy
|
|
26692
|
+
* type.
|
|
26693
|
+
*/
|
|
26694
|
+
/**
|
|
26695
|
+
* Per-type cache of SUCCESSFUL lookups only, holding the in-flight promise so
|
|
26696
|
+
* concurrent imports of the same type share one `DescribeType` call. A failed
|
|
26697
|
+
* lookup removes its own entry so a later call retries.
|
|
26698
|
+
*/
|
|
26699
|
+
const readOnlyPropertiesCache = /* @__PURE__ */ new Map();
|
|
26700
|
+
/**
|
|
26701
|
+
* Resolve the TOP-LEVEL read-only (i.e. `Fn::GetAtt`-able) property names for a
|
|
26702
|
+
* resource type.
|
|
26703
|
+
*
|
|
26704
|
+
* Returns `undefined` when the schema could not be resolved — a missing
|
|
26705
|
+
* `cloudformation:DescribeType` permission, an exhausted throttle retry, or a
|
|
26706
|
+
* type with no registry entry at all. Callers MUST treat `undefined` as "cannot
|
|
26707
|
+
* certify" rather than as "none": the two are different answers and this
|
|
26708
|
+
* function is the only place that can still tell them apart.
|
|
26709
|
+
*
|
|
26710
|
+
* Never throws.
|
|
26711
|
+
*/
|
|
26712
|
+
function getTopLevelReadOnlyProperties(resourceType) {
|
|
26713
|
+
if (hasNoRegistrySchema(resourceType)) return Promise.resolve(void 0);
|
|
26714
|
+
const cached = readOnlyPropertiesCache.get(resourceType);
|
|
26715
|
+
if (cached) return cached;
|
|
26716
|
+
const entry = fetchTopLevelReadOnlyProperties(resourceType).catch((error) => {
|
|
26717
|
+
readOnlyPropertiesCache.delete(resourceType);
|
|
26718
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
26719
|
+
getLogger().child("ReadOnlyProperties").debug(`Failed to resolve read-only properties for ${displaySafe(resourceType, { asciiOnly: true })} via cloudformation:DescribeType (${message}).`);
|
|
26720
|
+
});
|
|
26721
|
+
readOnlyPropertiesCache.set(resourceType, entry);
|
|
26722
|
+
return entry;
|
|
26723
|
+
}
|
|
26724
|
+
/**
|
|
26725
|
+
* Fetch + parse the type's read-only properties. THROWS on a DescribeType
|
|
26726
|
+
* failure — the caller catches, reports `undefined`, and declines to cache.
|
|
26727
|
+
*/
|
|
26728
|
+
async function fetchTopLevelReadOnlyProperties(resourceType) {
|
|
26729
|
+
const logger = getLogger().child("ReadOnlyProperties");
|
|
26730
|
+
const response = await describeTypeWithThrottleRetry(resourceType);
|
|
26731
|
+
const result = /* @__PURE__ */ new Set();
|
|
26732
|
+
if (response.Schema) {
|
|
26733
|
+
const readOnly = JSON.parse(response.Schema).readOnlyProperties;
|
|
26734
|
+
if (Array.isArray(readOnly)) for (const path of readOnly) {
|
|
26735
|
+
if (typeof path !== "string") continue;
|
|
26736
|
+
const match = /^\/properties\/([^/]+)/.exec(path);
|
|
26737
|
+
if (match?.[1]) result.add(unescapeJsonPointerSegment(match[1]));
|
|
26738
|
+
}
|
|
26739
|
+
}
|
|
26740
|
+
logger.debug(`Resolved ${result.size} top-level read-only properties for ${displaySafe(resourceType, { asciiOnly: true })}` + (result.size > 0 ? `: ${[...result].join(", ")}` : ""));
|
|
26741
|
+
return result;
|
|
26742
|
+
}
|
|
26743
|
+
/** Unescape an RFC 6901 JSON Pointer segment (`~1` -> `/`, `~0` -> `~`). */
|
|
26106
26744
|
function unescapeJsonPointerSegment(segment) {
|
|
26107
26745
|
return segment.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
26108
26746
|
}
|
|
@@ -26784,6 +27422,12 @@ var CloudControlProvider = class {
|
|
|
26784
27422
|
cloudControlClient;
|
|
26785
27423
|
logger = getLogger().child("CloudControlProvider");
|
|
26786
27424
|
patchGenerator = new JsonPatchGenerator();
|
|
27425
|
+
/**
|
|
27426
|
+
* Types whose unresolvable-schema import warning has already been printed —
|
|
27427
|
+
* see `maskUncertifiedModelValues`. Per-instance so a test cannot inherit
|
|
27428
|
+
* another test's suppression.
|
|
27429
|
+
*/
|
|
27430
|
+
warnedUnresolvableSchemaTypes = /* @__PURE__ */ new Set();
|
|
26787
27431
|
MAX_WAIT_TIME_MS = 9e5;
|
|
26788
27432
|
INITIAL_POLL_INTERVAL_MS = 1e3;
|
|
26789
27433
|
MAX_POLL_INTERVAL_MS = 1e4;
|
|
@@ -26943,7 +27587,7 @@ var CloudControlProvider = class {
|
|
|
26943
27587
|
const indeterminateGuard = await this.confirmDeleteTargetIdentity(logicalId, resourceType, physicalId, context);
|
|
26944
27588
|
if (context?.removeProtection === true && resourceType === "AWS::AutoScaling::AutoScalingGroup") {
|
|
26945
27589
|
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-
|
|
27590
|
+
const { ASGProvider } = await import("./asg-provider-CC6BMuNK.js").then((n) => n.n);
|
|
26947
27591
|
const asgProvider = new ASGProvider();
|
|
26948
27592
|
return withIndeterminateGuard(await asgProvider.delete(logicalId, physicalId, resourceType, _properties, context), indeterminateGuard);
|
|
26949
27593
|
}
|
|
@@ -27304,10 +27948,18 @@ var CloudControlProvider = class {
|
|
|
27304
27948
|
/**
|
|
27305
27949
|
* Enrich resource attributes with computed values
|
|
27306
27950
|
*
|
|
27307
|
-
* CC API GetResource returns property names that match CloudFormation
|
|
27308
|
-
* Fn::GetAtt attribute names, so all properties are passed through as-is.
|
|
27309
27951
|
* This method adds fallback attributes for edge cases where CC API
|
|
27310
27952
|
* may not return certain values.
|
|
27953
|
+
*
|
|
27954
|
+
* It passes every other key through AS-IS, and on the CREATE / UPDATE path
|
|
27955
|
+
* that bag is still the WHOLE resource model. This comment used to justify
|
|
27956
|
+
* that by saying the model's property names match `Fn::GetAtt` attribute
|
|
27957
|
+
* names; they do not — the model is every readable property, and
|
|
27958
|
+
* CloudFormation rejects a `Fn::GetAtt` naming a writable one. Issue
|
|
27959
|
+
* [#2847](https://github.com/go-to-k/cdkd/issues/2847) narrowed the IMPORT
|
|
27960
|
+
* path for that reason; the deploy path is issue
|
|
27961
|
+
* [#2925](https://github.com/go-to-k/cdkd/issues/2925), which also carries
|
|
27962
|
+
* why the same narrowing cannot simply be copied here.
|
|
27311
27963
|
*/
|
|
27312
27964
|
async enrichResourceAttributes(resourceType, physicalId, attributes) {
|
|
27313
27965
|
const enriched = { ...attributes };
|
|
@@ -27760,7 +28412,9 @@ var CloudControlProvider = class {
|
|
|
27760
28412
|
* - With `knownPhysicalId` (from `--resource <id>=<physicalId>` or
|
|
27761
28413
|
* `--resource-mapping`): call `GetResource(TypeName, Identifier)`,
|
|
27762
28414
|
* parse `ResourceModel` (returned as a JSON string by CC API), and
|
|
27763
|
-
* return
|
|
28415
|
+
* return the ATTRIBUTE keys as `attributes` — see
|
|
28416
|
+
* {@link maskUncertifiedModelValues} for what "attribute" means here and
|
|
28417
|
+
* why every other key comes back MASKED rather than dropped.
|
|
27764
28418
|
* - Without `knownPhysicalId`: return `null`. CC API has no efficient
|
|
27765
28419
|
* `aws:cdk:path`-tag lookup — `ListResources` returns identifiers
|
|
27766
28420
|
* only, so tag lookup would require one `GetResource` per resource
|
|
@@ -27772,6 +28426,107 @@ var CloudControlProvider = class {
|
|
|
27772
28426
|
* SDK providers (S3, Lambda, IAM Role, etc.) implement their own
|
|
27773
28427
|
* `import` with tag-based auto-lookup; this fallback only kicks in for
|
|
27774
28428
|
* resource types that don't have a dedicated SDK provider.
|
|
28429
|
+
*
|
|
28430
|
+
* ---
|
|
28431
|
+
*
|
|
28432
|
+
* The rest of this block is the design of the attribute narrowing
|
|
28433
|
+
* {@link maskUncertifiedModelValues} performs — every LEAF of a model key
|
|
28434
|
+
* cdkd cannot certify is an ATTRIBUTE is replaced with {@link SECRET_MASK},
|
|
28435
|
+
* container shape preserved, and the certified attributes are left untouched
|
|
28436
|
+
* (issue [#2847](https://github.com/go-to-k/cdkd/issues/2847)).
|
|
28437
|
+
*
|
|
28438
|
+
* It lives HERE, on `import()`, rather than in a second block above the
|
|
28439
|
+
* method, and that is deliberate: two consecutive block comments attach only
|
|
28440
|
+
* the LAST one, so splitting this back out silently orphans whichever doc
|
|
28441
|
+
* ends up first. Keep it as ONE block.
|
|
28442
|
+
*
|
|
28443
|
+
* ## What is being fixed
|
|
28444
|
+
*
|
|
28445
|
+
* `GetResource` returns the resource MODEL — every readable property, not
|
|
28446
|
+
* just the attributes. `cdkd import` persisted that model verbatim into
|
|
28447
|
+
* `ResourceState.attributes`, through no redactor, for every
|
|
28448
|
+
* Cloud-Control-routed type. Where the model carries a credential, the
|
|
28449
|
+
* credential landed in `state.json` in the clear.
|
|
28450
|
+
*
|
|
28451
|
+
* ## Why "attribute" means `readOnlyProperties`
|
|
28452
|
+
*
|
|
28453
|
+
* That is CloudFormation's own definition: a type's `readOnlyProperties` are
|
|
28454
|
+
* exactly what `Fn::GetAtt` may read, and CloudFormation REJECTS a
|
|
28455
|
+
* `Fn::GetAtt` naming a writable property at template validation. So a key
|
|
28456
|
+
* outside that set was never a legitimate attribute, and cdkd persisting it
|
|
28457
|
+
* bought nothing a valid template could use.
|
|
28458
|
+
*
|
|
28459
|
+
* ## Why MASK and not DROP — the load-bearing decision
|
|
28460
|
+
*
|
|
28461
|
+
* Dropping looks cleaner and is WRONG here, because there is no live
|
|
28462
|
+
* fallback on the read side. `IntrinsicFunctionResolver.resolveGetAtt` looks
|
|
28463
|
+
* the key up in this bag and, on a miss, falls through to
|
|
28464
|
+
* `constructAttribute` — which synthesizes from `physicalId` alone and, for
|
|
28465
|
+
* an attribute name that is neither `*Arn` nor `*Url`, WARNS and returns the
|
|
28466
|
+
* physical id. That is a silently wrong value shipped to AWS. There is no
|
|
28467
|
+
* `provider.getAttribute` rescue on the deploy path (the only caller of it
|
|
28468
|
+
* outside the providers is `cdkd orphan`), and this class has no
|
|
28469
|
+
* `getAttribute` at all.
|
|
28470
|
+
*
|
|
28471
|
+
* Masking keeps the KEY present, so the lookup HITS and the value flows
|
|
28472
|
+
* through `noteAttributeSecrecy` into `ResolverContext.redactedAttributeReads`,
|
|
28473
|
+
* where `DeployEngine.refuseRedactedAttributeReads` FAILS the resource rather
|
|
28474
|
+
* than sending the mask. So the outcome is a loud, named refusal instead of a
|
|
28475
|
+
* wrong value — which is the trade this repo already made for the mask-only
|
|
28476
|
+
* channel (issue #2274), reusing its machinery rather than inventing a second
|
|
28477
|
+
* sentinel nothing downstream recognises.
|
|
28478
|
+
*
|
|
28479
|
+
* ONE SHAPE ESCAPES THAT, and it is stated rather than left to be discovered:
|
|
28480
|
+
* an uncertified EMPTY container (`{}` / `[]`) has no leaf to mask, so no
|
|
28481
|
+
* `SECRET_MASK` lands under that key and no refusal can fire for it. A DOTTED
|
|
28482
|
+
* read through it breaks at `Object.hasOwn` and falls to `constructAttribute`;
|
|
28483
|
+
* a FLAT read returns the empty container itself. It is not a DISCLOSURE — the
|
|
28484
|
+
* container was empty at AWS, so there was nothing to disclose — but the
|
|
28485
|
+
* refusal genuinely does not fire there.
|
|
28486
|
+
*
|
|
28487
|
+
* ## The unresolvable-schema arm is FAIL-CLOSED
|
|
28488
|
+
*
|
|
28489
|
+
* `getTopLevelReadOnlyProperties` answers `undefined` when it could not find
|
|
28490
|
+
* out — a missing `cloudformation:DescribeType` grant, an exhausted throttle
|
|
28491
|
+
* retry, or a type with no registry entry. cdkd then cannot tell an attribute
|
|
28492
|
+
* from a property for this type, so it certifies NOTHING and masks the whole
|
|
28493
|
+
* model, warning at default verbosity with the grant to add. Failing OPEN
|
|
28494
|
+
* here would make a missing IAM permission silently restore the exact
|
|
28495
|
+
* disclosure this method exists to close.
|
|
28496
|
+
*
|
|
28497
|
+
* ## An UNREADABLE MODEL warns at the same volume as an unreadable schema
|
|
28498
|
+
*
|
|
28499
|
+
* The two arms below the `GetResource` — a `JSON.parse` failure, and a model
|
|
28500
|
+
* that parsed to something other than an object — cannot mask anything (there
|
|
28501
|
+
* is no bag to walk), so they yield `attributes: {}`. That is the DROP
|
|
28502
|
+
* outcome this design rejects one section up: the key is absent,
|
|
28503
|
+
* `resolveGetAtt` falls through to `constructAttribute`, and the physical id
|
|
28504
|
+
* ships. "cdkd could not read the model" is the same epistemic state as "cdkd
|
|
28505
|
+
* could not read the schema", so both report at DEFAULT verbosity. They used
|
|
28506
|
+
* to differ — the schema arm warned while these logged at `debug` — which
|
|
28507
|
+
* meant the one outcome that ships a wrong value silently was the one nobody
|
|
28508
|
+
* was told about. Neither line prints any part of the model: the parse arm
|
|
28509
|
+
* prints the error's NAME only (V8 embeds an input snippet in a
|
|
28510
|
+
* `SyntaxError`'s message) and the non-object arm prints the SHAPE only.
|
|
28511
|
+
*
|
|
28512
|
+
* ## What this does NOT close, stated as the danger direction
|
|
28513
|
+
*
|
|
28514
|
+
* A CREDENTIAL THAT IS ITSELF A READ-ONLY ATTRIBUTE IS STILL PERSISTED IN THE
|
|
28515
|
+
* CLEAR. `readOnlyProperties` is a structural test, not a sensitivity one, and
|
|
28516
|
+
* the registry schema offers nothing better to key on: it has no general
|
|
28517
|
+
* sensitivity marking. The nearest things it does have were both checked and
|
|
28518
|
+
* neither serves — `"format": "password"` is declared by a single property in
|
|
28519
|
+
* AWS's whole published bundle, and `writeOnlyProperties` (a real "cannot be
|
|
28520
|
+
* returned by a read" marker, declared by a minority of types) describes
|
|
28521
|
+
* values `GetResource` never returns, so masking them would be inert here. So
|
|
28522
|
+
* the "mask by the schema's own marking" shape the issue floated is NARROWED
|
|
28523
|
+
* to nothing usable rather than refuted outright. Known members of the
|
|
28524
|
+
* surviving class include `AWS::IAM::AccessKey`'s `SecretAccessKey`,
|
|
28525
|
+
* `AWS::Cognito::UserPoolClient`'s `ClientSecret` and
|
|
28526
|
+
* `AWS::EC2::IpamExternalResourceVerificationToken`'s `TokenValue`. The list
|
|
28527
|
+
* is not claimed to be exhaustive and no count is quoted here, because
|
|
28528
|
+
* nothing in the tree fences one; the derivation and its residual are
|
|
28529
|
+
* recorded on the issue.
|
|
27775
28530
|
*/
|
|
27776
28531
|
async import(input) {
|
|
27777
28532
|
if (!input.knownPhysicalId) return null;
|
|
@@ -27780,14 +28535,21 @@ var CloudControlProvider = class {
|
|
|
27780
28535
|
TypeName: input.resourceType,
|
|
27781
28536
|
Identifier: input.knownPhysicalId
|
|
27782
28537
|
}));
|
|
27783
|
-
|
|
28538
|
+
const safeType = displaySafe(input.resourceType, { asciiOnly: true });
|
|
28539
|
+
const safeId = displaySafe(input.knownPhysicalId, { asciiOnly: true });
|
|
28540
|
+
let parsedModel;
|
|
27784
28541
|
const raw = resp.ResourceDescription?.Properties;
|
|
27785
28542
|
if (typeof raw === "string" && raw.length > 0) try {
|
|
27786
28543
|
const parsed = JSON.parse(raw);
|
|
27787
|
-
if (parsed && typeof parsed === "object" && !Array.isArray(parsed))
|
|
28544
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) parsedModel = parsed;
|
|
28545
|
+
else {
|
|
28546
|
+
const shape = parsed === null ? "null" : Array.isArray(parsed) ? "an array" : typeof parsed;
|
|
28547
|
+
this.logger.warn(`CC API ResourceModel for ${safeType}/${safeId} parsed to ${shape}, not an object — recording no attributes for it. An Fn::GetAtt against this resource will fall back to a value constructed from its physical id.`);
|
|
28548
|
+
}
|
|
27788
28549
|
} catch (parseErr) {
|
|
27789
|
-
this.logger.
|
|
28550
|
+
this.logger.warn(`Failed to parse CC API ResourceModel for ${safeType}/${safeId}: ${parseErr instanceof Error ? parseErr.name : typeof parseErr}. Recording no attributes for it; an Fn::GetAtt against this resource will fall back to a value constructed from its physical id.`);
|
|
27790
28551
|
}
|
|
28552
|
+
const attributes = parsedModel === void 0 ? {} : await this.maskUncertifiedModelValues(parsedModel, input.resourceType, input.knownPhysicalId);
|
|
27791
28553
|
return {
|
|
27792
28554
|
physicalId: input.knownPhysicalId,
|
|
27793
28555
|
attributes
|
|
@@ -27797,7 +28559,81 @@ var CloudControlProvider = class {
|
|
|
27797
28559
|
throw error;
|
|
27798
28560
|
}
|
|
27799
28561
|
}
|
|
28562
|
+
/**
|
|
28563
|
+
* Replace every LEAF cdkd cannot certify belongs to an ATTRIBUTE with
|
|
28564
|
+
* {@link SECRET_MASK}, preserving container SHAPE, and leave the certified
|
|
28565
|
+
* attributes untouched. The argument for masking rather than dropping, and
|
|
28566
|
+
* for the fail-closed `undefined` arm, is on {@link import} above.
|
|
28567
|
+
*
|
|
28568
|
+
* ## Why the walk is RECURSIVE — this was a measured defect, not caution
|
|
28569
|
+
*
|
|
28570
|
+
* The first cut replaced the whole VALUE, so an uncertified `Endpoint`
|
|
28571
|
+
* object became the string `'***'`. `IntrinsicFunctionResolver.resolveGetAtt`
|
|
28572
|
+
* resolves `Endpoint.Address` by WALKING the dotted path: it tests
|
|
28573
|
+
* `typeof cursor === 'object'`, which a string fails, so the walk breaks with
|
|
28574
|
+
* `cursor === undefined`, `noteAttributeSecrecy` is NEVER called, and control
|
|
28575
|
+
* reaches `constructAttribute` — the physical-id fallback. That is precisely
|
|
28576
|
+
* the silently-wrong-value outcome the mask exists to prevent, so
|
|
28577
|
+
* whole-value masking DEFEATED its own justification for every nested
|
|
28578
|
+
* attribute. Masking leaves keeps the containers walkable, so the walk lands
|
|
28579
|
+
* on a `'***'` LEAF and notes it, and the refusal fires — for any container
|
|
28580
|
+
* that HAS a leaf. An EMPTY one has none, and `import()`'s doc carries that
|
|
28581
|
+
* gap; do not read this sentence as covering it.
|
|
28582
|
+
*
|
|
28583
|
+
* Arrays keep their length and element positions for the same reason.
|
|
28584
|
+
*
|
|
28585
|
+
* ## The bag is null-prototype
|
|
28586
|
+
*
|
|
28587
|
+
* `JSON.parse` can yield a legal own key `__proto__`; assigning that on an
|
|
28588
|
+
* ordinary object literal writes the PROTOTYPE instead of an own property and
|
|
28589
|
+
* the key vanishes from the bag — a DROP, the one outcome this method must
|
|
28590
|
+
* never produce. `Object.create(null)` makes the assignment ordinary.
|
|
28591
|
+
*/
|
|
28592
|
+
async maskUncertifiedModelValues(model, resourceType, physicalId) {
|
|
28593
|
+
const safeType = displaySafe(resourceType, { asciiOnly: true });
|
|
28594
|
+
const safeId = displaySafe(physicalId, { asciiOnly: true });
|
|
28595
|
+
const attributeNames = await getTopLevelReadOnlyProperties(resourceType);
|
|
28596
|
+
if (attributeNames === void 0) {
|
|
28597
|
+
if (!this.warnedUnresolvableSchemaTypes.has(resourceType)) {
|
|
28598
|
+
this.warnedUnresolvableSchemaTypes.add(resourceType);
|
|
28599
|
+
this.logger.warn(`Could not resolve the CloudFormation schema for ${safeType}, so cdkd cannot tell which of its Cloud Control model keys are Fn::GetAtt attributes. Every imported attribute for this type is recorded as "${"***"}" rather than risking a credential in state.json; an Fn::GetAtt against such a resource fails with a named refusal until that resource is next created or updated by a deploy. Grant cloudformation:DescribeType and re-import to record its attributes.`);
|
|
28600
|
+
}
|
|
28601
|
+
}
|
|
28602
|
+
const masked = Object.create(null);
|
|
28603
|
+
let maskedCount = 0;
|
|
28604
|
+
for (const [key, value] of Object.entries(model)) if (attributeNames?.has(key)) masked[key] = value;
|
|
28605
|
+
else {
|
|
28606
|
+
masked[key] = maskLeavesDeep(value);
|
|
28607
|
+
maskedCount++;
|
|
28608
|
+
}
|
|
28609
|
+
if (maskedCount > 0 && attributeNames !== void 0) this.logger.debug(`Masked ${maskedCount} non-attribute key(s) out of the ${safeType} Cloud Control model for ${safeId}: they are not in the type's readOnlyProperties, so CloudFormation would reject an Fn::GetAtt naming them and cdkd has no evidence they are safe to persist.`);
|
|
28610
|
+
return masked;
|
|
28611
|
+
}
|
|
27800
28612
|
};
|
|
28613
|
+
/**
|
|
28614
|
+
* Every LEAF of `value` replaced by {@link SECRET_MASK}, with object and array
|
|
28615
|
+
* CONTAINERS rebuilt at the same shape. See
|
|
28616
|
+
* `CloudControlProvider.maskUncertifiedModelValues` for why the shape must
|
|
28617
|
+
* survive.
|
|
28618
|
+
*
|
|
28619
|
+
* A `JSON.parse` result is acyclic by construction, so no visited-set is
|
|
28620
|
+
* needed — that answers CYCLES and nothing else. It is NOT a depth guarantee:
|
|
28621
|
+
* measured on this repo's node, `JSON.parse` survives ~100,000 nesting levels
|
|
28622
|
+
* while this recursion throws `RangeError` between 1,000 and 5,000. No Cloud
|
|
28623
|
+
* Control resource model comes close, and the `try` in `import()` is narrowed
|
|
28624
|
+
* to the parse, so such a `RangeError` would fail the import loudly rather than
|
|
28625
|
+
* degrade it silently — which is the direction this design wants. Recorded so
|
|
28626
|
+
* the acyclic sentence is not read as covering depth.
|
|
28627
|
+
*/
|
|
28628
|
+
function maskLeavesDeep(value) {
|
|
28629
|
+
if (Array.isArray(value)) return value.map((element) => maskLeavesDeep(element));
|
|
28630
|
+
if (value !== null && typeof value === "object") {
|
|
28631
|
+
const out = Object.create(null);
|
|
28632
|
+
for (const [key, nested] of Object.entries(value)) out[key] = maskLeavesDeep(nested);
|
|
28633
|
+
return out;
|
|
28634
|
+
}
|
|
28635
|
+
return "***";
|
|
28636
|
+
}
|
|
27801
28637
|
|
|
27802
28638
|
//#endregion
|
|
27803
28639
|
//#region src/provisioning/interrupt-watch.ts
|
|
@@ -34432,7 +35268,7 @@ async function resolveReplayProps(props, resolvers, secrets, execCtx, logicalId)
|
|
|
34432
35268
|
*/
|
|
34433
35269
|
function refuseMaskedReplayBaseline(props, logicalId) {
|
|
34434
35270
|
if (props === void 0 || !carriesSecretMask(props)) return;
|
|
34435
|
-
throw new CdkdError(`Cannot roll ${logicalId} back: its recorded baseline holds the redaction mask ('${"***"}')
|
|
35271
|
+
throw new CdkdError(`Cannot roll ${logicalId} back: its recorded baseline holds the redaction mask ('${"***"}'), so cdkd would write that literal to the live resource. There are two ways a baseline comes to hold it. (1) A NoEcho custom-resource value was resolved there: restore the property with 'cdkd deploy' AFTER forcing that custom resource to update (change one of its properties, e.g. a nonce), so its handler runs again and supplies the real value — an ordinary re-deploy leaves the resource unchanged, so the handler does not run and the mask stays. (2) The value was SPLICED from a masked record of ANOTHER resource — by 'cdkd orphan --force', or by 'cdkd import' resolving an Fn::GetAtt or a Ref over a value the Cloud Control fallback had masked. Repair the record that HOLDS the mask ('cdkd import <stack> --resource <logicalId>=<physicalId> --force', granting cloudformation:DescribeType first if the import warned that it could not read the schema), then re-run whichever command wrote this property. See https://github.com/go-to-k/cdkd/issues/2449.`, "ROLLBACK_REDACTED_BASELINE");
|
|
34436
35272
|
}
|
|
34437
35273
|
/**
|
|
34438
35274
|
* The replay's resolvers: the stack's own, plus one pinned sibling per FOREIGN
|
|
@@ -35977,7 +36813,7 @@ function unionCrossStackReads(previous, recorded, identity) {
|
|
|
35977
36813
|
}
|
|
35978
36814
|
return out;
|
|
35979
36815
|
}
|
|
35980
|
-
var DeployEngine = class {
|
|
36816
|
+
var DeployEngine = class DeployEngine {
|
|
35981
36817
|
logger = getLogger().child("DeployEngine");
|
|
35982
36818
|
resolver;
|
|
35983
36819
|
interrupted = false;
|
|
@@ -36246,7 +37082,7 @@ var DeployEngine = class {
|
|
|
36246
37082
|
...this.options.inheritedSecrets && this.options.inheritedSecrets.size > 0 && { inheritedSecrets: this.options.inheritedSecrets },
|
|
36247
37083
|
recordedSecretValues,
|
|
36248
37084
|
noEchoAttributeResources: this.noEchoAttributeResources,
|
|
36249
|
-
redactedAttributeReads:
|
|
37085
|
+
...base.redactedAttributeReads && { redactedAttributeReads: base.redactedAttributeReads }
|
|
36250
37086
|
};
|
|
36251
37087
|
}
|
|
36252
37088
|
/**
|
|
@@ -36400,6 +37236,147 @@ var DeployEngine = class {
|
|
|
36400
37236
|
for (const name of names) recordMaskOnlyValuesIn(attributes[name], secrets, excluded);
|
|
36401
37237
|
}
|
|
36402
37238
|
/**
|
|
37239
|
+
* The remedy clause of {@link refuseRedactedAttributeReads}'s import arm,
|
|
37240
|
+
* derived from the `reads` entries rather than described in prose.
|
|
37241
|
+
*
|
|
37242
|
+
* `ResolverContext.redactedAttributeReads` is HETEROGENEOUS, and the split
|
|
37243
|
+
* that matters is NOT which function pushed the entry — it is whether the
|
|
37244
|
+
* masked record lives in THIS stack's state, because only then can a
|
|
37245
|
+
* `--resource` re-import here reach it. FOUR populations reach the bag, and
|
|
37246
|
+
* each entry now says which it is IN ITS OWN FIELDS
|
|
37247
|
+
* ({@link RedactedAttributeRead}) rather than in a rendered string this
|
|
37248
|
+
* function re-parses:
|
|
37249
|
+
*
|
|
37250
|
+
* - `kind: 'attribute'` with a `logicalId` — `noteAttributeSecrecy`, a
|
|
37251
|
+
* resource in this stack. LOCAL.
|
|
37252
|
+
* - `kind: 'attribute'` whose `logicalId` names an
|
|
37253
|
+
* `AWS::CloudFormation::Stack` — ALSO `noteAttributeSecrecy`, and the
|
|
37254
|
+
* reason `kind` alone cannot partition. A nested stack's output attribute
|
|
37255
|
+
* reaches the cross-stack re-resolution arm only when it
|
|
37256
|
+
* `carriesDynamicReference`, and a value that is already `SECRET_MASK`
|
|
37257
|
+
* does NOT (that predicate tests for `{{resolve:`), so a masked child
|
|
37258
|
+
* output falls through to `noteAttributeSecrecy` and is pushed as an
|
|
37259
|
+
* ordinary local attribute read. Its record is the CHILD's
|
|
37260
|
+
* `state.outputs`, from which the parent's attributes are rebuilt every
|
|
37261
|
+
* deploy, so no `--resource` in THIS stack clears it:
|
|
37262
|
+
* `NestedStackProvider` implements no `import()` at all, so the command
|
|
37263
|
+
* would report `skipped-no-impl` and change nothing. FOREIGN, despite
|
|
37264
|
+
* being local by kind.
|
|
37265
|
+
* - `kind: 'cross-stack'` — `reresolveCrossStackValue`'s `Fn::ImportValue` /
|
|
37266
|
+
* `Fn::GetStackOutput` / `nested stack <Child> Outputs.<Key>` forms. It
|
|
37267
|
+
* carries NO `logicalId`, because there is no id in THIS stack to name.
|
|
37268
|
+
* FOREIGN.
|
|
37269
|
+
* - `kind: 'ref-state-key'` — `noteRefStateMask`, a resource in this stack
|
|
37270
|
+
* whose CFn `Ref` value is recovered from a state key rather than from the
|
|
37271
|
+
* physical id. LOCAL, and it earns a sentence of its own: the read is
|
|
37272
|
+
* cdkd's, not the template's, so the `Fn::GetAtt` remedy "stop reading it"
|
|
37273
|
+
* does not apply.
|
|
37274
|
+
*
|
|
37275
|
+
* Successive review rounds tried to express this as an instruction the reader
|
|
37276
|
+
* applies ("the name to the left of the dot"), and each phrasing was wrong
|
|
37277
|
+
* for a shape it had not considered — twice naming a REAL-but-wrong logical
|
|
37278
|
+
* id that the import typo guard ACCEPTS, so following it would
|
|
37279
|
+
* `--force`-overwrite an innocent row. Partitioning here makes each arm say
|
|
37280
|
+
* only what is true of its own shape, and makes a new shape a change to THIS
|
|
37281
|
+
* function rather than a silent widening of a sentence.
|
|
37282
|
+
*
|
|
37283
|
+
* **AND THE PARTITION READS FIELDS, NEVER A REGEX OVER `display`** (round-4
|
|
37284
|
+
* review). Two revisions parsed the rendering back into structure and each
|
|
37285
|
+
* shipped a defect: a hand-spelled pattern that a producer rename disarms,
|
|
37286
|
+
* then an `[A-Za-z0-9]+` id class that a HYPHENATED logical id falls out of —
|
|
37287
|
+
* and cdkd accepts one, because it validates no logical-id charset and never
|
|
37288
|
+
* hands the template to CloudFormation. Here falling out cost a re-import
|
|
37289
|
+
* command withheld; at `resolveOutputs`' guard the same miss cost the REFUSAL
|
|
37290
|
+
* itself. One rendering serving two consumers whose safe directions are
|
|
37291
|
+
* OPPOSITE is not a pattern to tune, so the structure moved into the data.
|
|
37292
|
+
*
|
|
37293
|
+
* The nested-stack case is excluded BY RESOURCE TYPE, not by an `Outputs.`
|
|
37294
|
+
* spelling. Keying on the segment over-reaches: a local
|
|
37295
|
+
* `AWS::ServiceCatalog::CloudFormationProvisionedProduct` documents
|
|
37296
|
+
* `Outputs.<Key>` as a real `Fn::GetAtt` attribute, so a masked one would be
|
|
37297
|
+
* misrouted to the foreign arm and the reachable remedy withheld. `resources`
|
|
37298
|
+
* is on the context already, so the type is available and exact.
|
|
37299
|
+
*
|
|
37300
|
+
* A LOCAL target whose type cannot be repaired by `cdkd import` at all gets
|
|
37301
|
+
* a THIRD arm since the issue #2847 round-2 review — a `Custom::*` /
|
|
37302
|
+
* `AWS::CloudFormation::CustomResource`, whose provider records no
|
|
37303
|
+
* attributes, so the import's same-physical-id carry-over restores the
|
|
37304
|
+
* masked bag and the refusal repeats forever. It is NOT simply excluded from
|
|
37305
|
+
* `isLocal`: that would route it to the FOREIGN arm, which asserts the record
|
|
37306
|
+
* lives in another stack, and for a custom resource in this very template
|
|
37307
|
+
* that is false. Its arm names the resource and withholds the command.
|
|
37308
|
+
*
|
|
37309
|
+
* An entry carrying NO `logicalId` is treated as foreign, which is both
|
|
37310
|
+
* correct (`cross-stack` is the only kind that omits it) and the safe
|
|
37311
|
+
* direction for a kind nobody has added yet: the foreign arm names no
|
|
37312
|
+
* command, so an unrecognised shape costs a vaguer message rather than a
|
|
37313
|
+
* destructive one.
|
|
37314
|
+
*
|
|
37315
|
+
* A logical id spelled `Ref Foo (state key X)`, or `My-Table`, or anything
|
|
37316
|
+
* else cdkd accepts, now routes on the FIELD and cannot be misread as another
|
|
37317
|
+
* row — the misparse the pre-round-4 regexes had to be anchored against.
|
|
37318
|
+
*/
|
|
37319
|
+
static maskedRecordRemedyFor(reads, resources) {
|
|
37320
|
+
const NESTED_STACK_RESOURCE_TYPE = "AWS::CloudFormation::Stack";
|
|
37321
|
+
/**
|
|
37322
|
+
* Types whose `import()` can NEVER clear a mask, so advising a re-import
|
|
37323
|
+
* for them is a guaranteed no-op (issue #2847 round-2 review).
|
|
37324
|
+
*
|
|
37325
|
+
* TRACED, not assumed. `CustomResourceProvider.import` returns
|
|
37326
|
+
* `{ physicalId, attributes: {} }` unconditionally; `import.ts`'s
|
|
37327
|
+
* `rowAttributes` normalises an empty bag to `undefined` and the coalesce
|
|
37328
|
+
* behind it CARRIES FORWARD the prior record's attributes whenever the
|
|
37329
|
+
* physical id matches — which it does, since the command is run with that
|
|
37330
|
+
* very id. So the masked bag is copied back verbatim and the refusal
|
|
37331
|
+
* repeats, forever.
|
|
37332
|
+
*
|
|
37333
|
+
* This is the NoEcho population — arm (1) of the refusal's own message —
|
|
37334
|
+
* which already has the right remedy there (force the custom resource to
|
|
37335
|
+
* update so its handler runs again). It gets its OWN arm rather than being
|
|
37336
|
+
* excluded from `isLocal`: excluding it would route the read to the FOREIGN
|
|
37337
|
+
* arm, which says the record lives in ANOTHER stack, and for a custom
|
|
37338
|
+
* resource sitting in this very template that is simply false. Narrower
|
|
37339
|
+
* than issue #2927, which is about a re-import whose `GetResource` merely
|
|
37340
|
+
* came back empty; here the provider cannot produce attributes at all.
|
|
37341
|
+
*/
|
|
37342
|
+
const importCannotClearMask = (type) => type === "AWS::CloudFormation::CustomResource" || (type?.startsWith("Custom::") ?? false);
|
|
37343
|
+
const typeOf = (read) => read.logicalId !== void 0 && Object.hasOwn(resources, read.logicalId) ? resources[read.logicalId]?.resourceType : void 0;
|
|
37344
|
+
const isLocal = (read) => {
|
|
37345
|
+
if (read.logicalId === void 0) return false;
|
|
37346
|
+
return typeOf(read) !== NESTED_STACK_RESOURCE_TYPE;
|
|
37347
|
+
};
|
|
37348
|
+
/** LOCAL, but no `cdkd import` can rewrite it — see above. */
|
|
37349
|
+
const isUnclearableLocal = (read) => isLocal(read) && importCannotClearMask(typeOf(read));
|
|
37350
|
+
const targetOf = (read) => read.logicalId;
|
|
37351
|
+
const localTargets = [...new Set(reads.filter((read) => isLocal(read) && !isUnclearableLocal(read)).map(targetOf).filter((id) => id !== void 0))];
|
|
37352
|
+
const unclearableTargets = [...new Set(reads.filter(isUnclearableLocal).map(targetOf).filter((id) => id !== void 0))];
|
|
37353
|
+
const foreignReads = reads.filter((read) => !isLocal(read));
|
|
37354
|
+
const hasRefStateRead = reads.some((read) => read.kind === "ref-state-key" && isLocal(read));
|
|
37355
|
+
/**
|
|
37356
|
+
* A logical id rendered inside the single-quoted command below.
|
|
37357
|
+
*
|
|
37358
|
+
* cdkd validates no logical-id charset, so the id can contain a `'` — and
|
|
37359
|
+
* the line it lands in is meant to be COPY-PASTED into a shell, where an
|
|
37360
|
+
* unescaped one closes the quoting early and the rest of the command
|
|
37361
|
+
* reparses as something else. POSIX single-quote escaping (`'` becomes
|
|
37362
|
+
* `'\\''`) is the fix: close, emit an escaped quote, reopen.
|
|
37363
|
+
*
|
|
37364
|
+
* cdkd never EXECUTES this string — it is advice in an error message — so
|
|
37365
|
+
* this is about the pasted command being correct, not about injection into
|
|
37366
|
+
* cdkd itself (issue #2847 round-5 review).
|
|
37367
|
+
*/
|
|
37368
|
+
const quoteSafe = (id) => id.replaceAll("'", `'\\''`);
|
|
37369
|
+
const parts = [];
|
|
37370
|
+
if (localTargets.length > 0) parts.push(`Re-import the record that HOLDS the mask: ` + localTargets.map((id) => `'cdkd import <stack> --resource ${quoteSafe(id)}=<physicalId> --force'`).join(", ") + `.`);
|
|
37371
|
+
if (unclearableTargets.length > 0) parts.push(`Do NOT re-import ${unclearableTargets.join(", ")}: a custom resource's import records no attributes, so the masked bag is carried forward unchanged and the refusal repeats. Cause (1) above is the one that applies to it.`);
|
|
37372
|
+
if (foreignReads.length > 0) {
|
|
37373
|
+
const subject = reads.length === 1 ? "The read above resolves" : foreignReads.length === 1 ? "One of the reads above resolves" : "Some of the reads above resolve";
|
|
37374
|
+
parts.push(`${subject} through ANOTHER stack (an Fn::ImportValue, an Fn::GetStackOutput, or a nested stack's Outputs), whose masked record lives in that stack's state — re-importing anything in this stack cannot clear it; act on the producer stack instead.`);
|
|
37375
|
+
}
|
|
37376
|
+
if (hasRefStateRead) parts.push("A 'Ref <LogicalId> (state key <Key>)' entry above is CDKD's own read, not one the template can stop making: CloudFormation defines that resource type's Ref value as that state key rather than the physical id, so the record must be repaired (re-import it, or let a deploy create or update the resource) — the \"stop reading it\" remedy does not apply to such an entry.");
|
|
37377
|
+
return parts.join(" ");
|
|
37378
|
+
}
|
|
37379
|
+
/**
|
|
36403
37380
|
* Refuse to provision a resource whose resolution served a REDACTED attribute
|
|
36404
37381
|
* out of a previous deploy's state (issue #2274).
|
|
36405
37382
|
*
|
|
@@ -36417,17 +37394,30 @@ var DeployEngine = class {
|
|
|
36417
37394
|
* class. A loud failure naming the remedy is strictly better than a silent
|
|
36418
37395
|
* wrong write.
|
|
36419
37396
|
*
|
|
36420
|
-
* NARROW BY CONSTRUCTION. The bag is only non-empty when a
|
|
36421
|
-
* actually served a masked
|
|
37397
|
+
* NARROW BY CONSTRUCTION. The bag is only non-empty when a resolution
|
|
37398
|
+
* actually served a masked value during THIS resource's resolution, so a
|
|
36422
37399
|
* resource whose properties merely happen to contain the string `***` is
|
|
36423
37400
|
* untouched — which is why the check is not "does `resolvedProps` hold the
|
|
36424
37401
|
* mask". And the diff pass does not consult the bag at all, so an untouched
|
|
36425
37402
|
* stack still reports NO_CHANGE and deploys.
|
|
37403
|
+
*
|
|
37404
|
+
* "A resolution", not "an `Fn::GetAtt`": the pushers are
|
|
37405
|
+
* `noteAttributeSecrecy`, `reresolveCrossStackValue` and — since the issue
|
|
37406
|
+
* #2847 review — `noteRefStateMask`, the `Ref` branch that reads a recovery
|
|
37407
|
+
* key out of the same persisted bags. {@link maskedRecordRemedyFor} is the
|
|
37408
|
+
* authority on the full shape list.
|
|
37409
|
+
*
|
|
37410
|
+
* This block sits DIRECTLY above its subject, and the previous revision's did
|
|
37411
|
+
* not: `maskedRecordRemedyFor` was inserted between the two, so JavaScript's
|
|
37412
|
+
* "only the LAST of two consecutive block comments attaches" rule (the same
|
|
37413
|
+
* one `cloud-control-provider.ts`'s `import()` doc warns about) silently
|
|
37414
|
+
* re-pointed 25 lines of doc at the wrong function and left this one with
|
|
37415
|
+
* none. Keep a new helper OUT of the gap.
|
|
36426
37416
|
*/
|
|
36427
37417
|
refuseRedactedAttributeReads(logicalId, resourceType, context) {
|
|
36428
37418
|
const reads = context.redactedAttributeReads;
|
|
36429
37419
|
if (reads === void 0 || reads.length === 0) return;
|
|
36430
|
-
throw new ProvisioningError(`Cannot resolve ${reads.join(", ")} for ${logicalId}: cdkd's recorded state holds only the redaction mask there, and the value is not recoverable from state.
|
|
37420
|
+
throw new ProvisioningError(`Cannot resolve ${reads.map((read) => read.display).join(", ")} for ${logicalId}: cdkd's recorded state holds only the redaction mask there, and the value is not recoverable from state. There are two ways a record comes to hold the mask. (1) A custom resource handler declared its response NoEcho: true — the value is generated by the handler, so cdkd has nothing to re-derive it from and must not write the literal mask to AWS. Remedies: force that custom resource to update (change one of its properties, e.g. a nonce / version property) so its handler runs again and supplies the value in this same run; or stop setting NoEcho on that response. If the value comes from ANOTHER stack, the producer and this stack must deploy in ONE run (cdkd deploy --all) with the producer's custom resource actually running — re-deploying the producer by itself does not help, because it re-masks the value on the way into its own state. (2) The resource was adopted by 'cdkd import' through the Cloud Control fallback, which records only the attributes the type's CloudFormation schema declares read-only and masks the rest. Either the attribute named above is not one of them — CloudFormation would reject an Fn::GetAtt naming it too, so stop reading it — or cdkd could not read that schema and masked the whole model, which the import warned about when it happened. ${DeployEngine.maskedRecordRemedyFor(reads, context.resources)} If that warning named a missing cloudformation:DescribeType permission, grant it first. See https://github.com/go-to-k/cdkd/issues/2449.`, resourceType, logicalId);
|
|
36431
37421
|
}
|
|
36432
37422
|
redactOperationsForJournal(operations) {
|
|
36433
37423
|
return operations.map((op) => {
|
|
@@ -36438,7 +37428,7 @@ var DeployEngine = class {
|
|
|
36438
37428
|
const next = { ...op };
|
|
36439
37429
|
if (next.properties) next.properties = redactSecretsForState(next.properties, ownSecrets, templateProps);
|
|
36440
37430
|
if (next.attemptedProperties) next.attemptedProperties = redactSecretsForState(markSameGenerationBag({ ...next.attemptedProperties }), ownSecrets, templateProps);
|
|
36441
|
-
if (next.previousState) next.previousState = scrubResourceRecord(next.previousState, ownSecrets);
|
|
37431
|
+
if (next.previousState) next.previousState = scrubResourceRecord(next.previousState, ownSecrets, void 0, STATE_SOURCED_READBACK_RULES);
|
|
36442
37432
|
return next;
|
|
36443
37433
|
});
|
|
36444
37434
|
}
|
|
@@ -37366,6 +38356,8 @@ var DeployEngine = class {
|
|
|
37366
38356
|
renderer.removeTask(logicalId);
|
|
37367
38357
|
const message = error instanceof Error ? error.message : String(error);
|
|
37368
38358
|
this.logger.error(this.maskForResource(logicalId, `Failed to ${change.changeType.toLowerCase()} ${logicalId}: ${message}`));
|
|
38359
|
+
const orphanAdvice = this.orphanedNameCollisionAdvice(change.changeType, logicalId, error);
|
|
38360
|
+
if (orphanAdvice) this.logger.error(this.maskForResource(logicalId, orphanAdvice));
|
|
37369
38361
|
this.recordEvent({
|
|
37370
38362
|
eventType: "RESOURCE_FAILED",
|
|
37371
38363
|
stackName,
|
|
@@ -37557,7 +38549,8 @@ var DeployEngine = class {
|
|
|
37557
38549
|
template,
|
|
37558
38550
|
resources: stateResources,
|
|
37559
38551
|
...parameterValues && { parameters: parameterValues },
|
|
37560
|
-
...conditions && { conditions }
|
|
38552
|
+
...conditions && { conditions },
|
|
38553
|
+
redactedAttributeReads: []
|
|
37561
38554
|
}, stackName);
|
|
37562
38555
|
if (context.recordedSecretValues) this.perResourceSecrets.set(logicalId, context.recordedSecretValues);
|
|
37563
38556
|
const resolvedProps = await this.resolver.resolve(desiredProps, context);
|
|
@@ -37605,7 +38598,8 @@ var DeployEngine = class {
|
|
|
37605
38598
|
template,
|
|
37606
38599
|
resources: stateResources,
|
|
37607
38600
|
...parameterValues && { parameters: parameterValues },
|
|
37608
|
-
...conditions && { conditions }
|
|
38601
|
+
...conditions && { conditions },
|
|
38602
|
+
redactedAttributeReads: []
|
|
37609
38603
|
}, stackName);
|
|
37610
38604
|
const updateSecrets = context.recordedSecretValues ?? /* @__PURE__ */ new Map();
|
|
37611
38605
|
this.perResourceSecrets.set(logicalId, updateSecrets);
|
|
@@ -38058,6 +39052,99 @@ var DeployEngine = class {
|
|
|
38058
39052
|
};
|
|
38059
39053
|
}
|
|
38060
39054
|
/**
|
|
39055
|
+
* The plain-CREATE sibling of {@link replacementNameOrigin} (issue
|
|
39056
|
+
* [#2902](https://github.com/go-to-k/cdkd/issues/2902)).
|
|
39057
|
+
*
|
|
39058
|
+
* A CREATE that collides on a name cdkd DERIVED is very likely a resource
|
|
39059
|
+
* cdkd itself left behind: `DeletionPolicy: Retain` makes a rollback drop
|
|
39060
|
+
* the state record while leaving the resource in AWS (CloudFormation
|
|
39061
|
+
* semantics, and deliberate), and cdkd's generated names carry no random
|
|
39062
|
+
* component (`generateResourceName`) — so the next deploy asks AWS for
|
|
39063
|
+
* exactly the name the orphan still holds, fails, rolls back again, and
|
|
39064
|
+
* repeats forever. Before this, the user saw only the bare AWS sentence:
|
|
39065
|
+
* nothing named the collision's cause and nothing named a way out, so the
|
|
39066
|
+
* reported recovery was hand-deleting resources through the AWS API.
|
|
39067
|
+
*
|
|
39068
|
+
* CloudFormation never shows this because its generated names carry a
|
|
39069
|
+
* random suffix, so a retained orphan cannot collide with a later deploy.
|
|
39070
|
+
* The breakage is that combination — CFn's retain semantics with cdkd's
|
|
39071
|
+
* deterministic naming — rather than either half, which is why the fix here
|
|
39072
|
+
* is a diagnosis and a remedy rather than a behaviour change. Whether cdkd
|
|
39073
|
+
* should instead RE-ADOPT the retained resource is issue
|
|
39074
|
+
* [#2914](https://github.com/go-to-k/cdkd/issues/2914).
|
|
39075
|
+
*
|
|
39076
|
+
* Returns `undefined` — leaving the pre-existing wording untouched — for
|
|
39077
|
+
* every case it cannot vouch for:
|
|
39078
|
+
*
|
|
39079
|
+
* - not a CREATE. A replacement collision DOES arrive here — the
|
|
39080
|
+
* `NAMED_REPLACEMENT_COLLISION` throws happen inside `provisionResourceBody`,
|
|
39081
|
+
* which the caller invokes inside the same `try`, and this method's own
|
|
39082
|
+
* suite asserts their line was logged. What refuses them is the
|
|
39083
|
+
* `ProvisioningError` check below (they throw `CdkdError`), so deleting
|
|
39084
|
+
* EITHER guard alone leaves the suite green. That does not make this one
|
|
39085
|
+
* dead: a non-CREATE `ProvisioningError` whose message carries
|
|
39086
|
+
* `already exists` — an UPDATE-path sub-resource conflict — would reach
|
|
39087
|
+
* the advice without it, and the replacement message's remedy is to
|
|
39088
|
+
* RENAME, which does not recover an orphan. (An earlier revision of this
|
|
39089
|
+
* comment claimed the throws "never reach this catch at all". Three
|
|
39090
|
+
* reviewers disproved it independently.);
|
|
39091
|
+
* - not a name collision;
|
|
39092
|
+
* - no physical id on the error (a create that failed BEFORE the AWS call
|
|
39093
|
+
* never names one). At RUNTIME this is subsumed by the next guard --
|
|
39094
|
+
* `looksLikeCdkdGeneratedName` refuses a falsy id on its own first line,
|
|
39095
|
+
* measured: deleting this check ALONE leaves the suite green, deleting
|
|
39096
|
+
* both together reds it. It stays for the TYPE narrowing the message
|
|
39097
|
+
* interpolation needs, and so the refusal is readable here rather than
|
|
39098
|
+
* inferred from another module;
|
|
39099
|
+
* - a name cdkd did not derive — a user-supplied name may collide with a
|
|
39100
|
+
* resource of someone else's entirely, and telling that user to
|
|
39101
|
+
* `cdkd import` it would be advice to adopt what this stack does not own;
|
|
39102
|
+
* - a NESTED-STACK child. Its stack name is `<parent>~<logicalId>`, and CDK's
|
|
39103
|
+
* own stack-name rule bars `~`, so no Cloud Assembly stack can ever carry
|
|
39104
|
+
* it — `cdkd import` resolves its target from the assembly and walks
|
|
39105
|
+
* top-level stacks only, so the command would be unrunnable. That is the
|
|
39106
|
+
* same #2610 class this method's `canImport` check exists for, one level
|
|
39107
|
+
* down, so the child takes the delete-only arm.
|
|
39108
|
+
*
|
|
39109
|
+
* **A cdkd-DERIVED name is not proof the resource is THIS stack's**, which
|
|
39110
|
+
* the first revision of this advice assumed. Two ways it is not, both
|
|
39111
|
+
* reachable: a globally-namespaced type (`AWS::S3::Bucket` is the documented
|
|
39112
|
+
* exception — see `.claude/rules/provider-resource-identity.md`) can collide
|
|
39113
|
+
* with ANOTHER ACCOUNT's resource, and because the derivation is predictable
|
|
39114
|
+
* that name can be pre-registered by someone else; and the same stack name
|
|
39115
|
+
* deployed in two REGIONS derives the same name for a global type, so the
|
|
39116
|
+
* collision is with a live resource another state file already owns —
|
|
39117
|
+
* importing it would give two stacks one resource, and either `cdkd destroy`
|
|
39118
|
+
* would then delete it out from under the other. So the message names the
|
|
39119
|
+
* orphan as the LIKELY case rather than the certain one, and asks the reader
|
|
39120
|
+
* to confirm ownership before adopting.
|
|
39121
|
+
*/
|
|
39122
|
+
orphanedNameCollisionAdvice(changeType, logicalId, error) {
|
|
39123
|
+
if (changeType !== "CREATE") return void 0;
|
|
39124
|
+
if (!(error instanceof ProvisioningError)) return void 0;
|
|
39125
|
+
const physicalId = error.physicalId;
|
|
39126
|
+
if (!physicalId) return void 0;
|
|
39127
|
+
if (!isNameCollisionError(error.message)) return void 0;
|
|
39128
|
+
const stackName = getCurrentStackName();
|
|
39129
|
+
if (!looksLikeCdkdGeneratedName(physicalId, logicalId, stackName)) return void 0;
|
|
39130
|
+
if (!stackName) return void 0;
|
|
39131
|
+
const safeId = displaySafe(physicalId, { asciiOnly: true });
|
|
39132
|
+
const safeStack = displaySafe(stackName, { asciiOnly: true });
|
|
39133
|
+
const safeLogicalId = displaySafe(logicalId, { asciiOnly: true });
|
|
39134
|
+
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.`;
|
|
39135
|
+
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.`;
|
|
39136
|
+
let canImport;
|
|
39137
|
+
try {
|
|
39138
|
+
canImport = typeof this.providerRegistry.getProvider(error.resourceType).import === "function";
|
|
39139
|
+
} catch {
|
|
39140
|
+
canImport = false;
|
|
39141
|
+
}
|
|
39142
|
+
const commandNamesTheRightResource = safeId === physicalId && safeStack === stackName && safeLogicalId === logicalId;
|
|
39143
|
+
const importableTarget = !stackName.includes("~");
|
|
39144
|
+
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}`;
|
|
39145
|
+
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}`;
|
|
39146
|
+
}
|
|
39147
|
+
/**
|
|
38061
39148
|
* Read `DeletionPolicy` / `UpdateReplacePolicy` from the synth template
|
|
38062
39149
|
* so they can be persisted in `ResourceState` (schema v5+). Always returns
|
|
38063
39150
|
* both keys (`undefined` when the template does not carry the attribute)
|
|
@@ -38349,6 +39436,83 @@ var DeployEngine = class {
|
|
|
38349
39436
|
...parameterValues && { parameters: parameterValues },
|
|
38350
39437
|
...conditions && { conditions }
|
|
38351
39438
|
}, stackName);
|
|
39439
|
+
/**
|
|
39440
|
+
* Fail an output whose resolution served a value out of a MASKED state
|
|
39441
|
+
* record, instead of publishing what the fall-through produced (issue
|
|
39442
|
+
* [#2847](https://github.com/go-to-k/cdkd/issues/2847), independent
|
|
39443
|
+
* round-2 review — the BLOCKER).
|
|
39444
|
+
*
|
|
39445
|
+
* `refuseRedactedAttributeReads` is called at exactly TWO sites, both
|
|
39446
|
+
* per-resource CREATE / UPDATE. `resolveOutputs` builds its own context
|
|
39447
|
+
* and consulted nothing, so the note this pass records was written and
|
|
39448
|
+
* never read — and once `refStateLookupFromResource` learned to refuse a
|
|
39449
|
+
* masked leaf, "never read" stopped being harmless. A `{"Ref": <record
|
|
39450
|
+
* whose TableName / SelectionId / RepositoryId / AppSync ARN is masked>}`
|
|
39451
|
+
* in `Outputs` publishes the RAW PHYSICAL ID: for a Cloud-Control-routed
|
|
39452
|
+
* `AWS::S3Tables::Table`, a UUID-tailed ARN where the table NAME belongs.
|
|
39453
|
+
*
|
|
39454
|
+
* THE ASYMMETRY IS WHY THIS IS A REFUSAL RATHER THAN A WARNING. Before the
|
|
39455
|
+
* refusal existed this published `'***'`, which the CONSUMER stack rejects
|
|
39456
|
+
* — `reresolveCrossStackValue` tests `carriesSecretMask` and refuses. An
|
|
39457
|
+
* ARN passes that test, so the consumer resolves its `Fn::ImportValue` to
|
|
39458
|
+
* a wrong value and sends it to AWS, with both deploys green. Publishing
|
|
39459
|
+
* nothing is the only outcome that keeps the consumer's guard meaningful.
|
|
39460
|
+
*
|
|
39461
|
+
* PER-OUTPUT BY ITS OWN BAG, and a length DELTA over the shared one is
|
|
39462
|
+
* what this replaced (issue #2847 round-3 review, and the reason it is a
|
|
39463
|
+
* bag rather than a `slice`). All three pushers are IDEMPOTENT — each
|
|
39464
|
+
* guards with `if (!reads.includes(read))` — and `resolveOutputs` shares
|
|
39465
|
+
* ONE context across every output, unlike the CREATE / UPDATE arms which
|
|
39466
|
+
* build a fresh context per resource. So a second output reading the SAME
|
|
39467
|
+
* masked record produced an EMPTY delta and was PUBLISHED:
|
|
39468
|
+
*
|
|
39469
|
+
* Outputs:
|
|
39470
|
+
* TableRef: { Value: { Ref: Tbl } } # refused
|
|
39471
|
+
* TableRef2: { Value: { Ref: Tbl } } # published the raw ARN
|
|
39472
|
+
*
|
|
39473
|
+
* `resolveRef` memoizes nothing, so the second resolution really does
|
|
39474
|
+
* re-enter `noteRefStateMask` and really is refused a push. Giving each
|
|
39475
|
+
* output its own array and MERGING the entries back afterwards keeps both
|
|
39476
|
+
* properties: the guard sees exactly this output's reads, and the shared
|
|
39477
|
+
* bag still accumulates for anything reading it later.
|
|
39478
|
+
*
|
|
39479
|
+
* SCOPED TO THE `ref-state-key` KIND, and that is a narrowing rather than
|
|
39480
|
+
* an oversight. The refusal exists because of the FALL-THROUGH: when the
|
|
39481
|
+
* lookup skips a masked leaf, `cfnRefValueFromPhysicalId` emits the raw
|
|
39482
|
+
* physical id, which no downstream reader recognises. Every OTHER pusher —
|
|
39483
|
+
* `noteAttributeSecrecy`'s `Fn::GetAtt`, `reresolveCrossStackValue`'s
|
|
39484
|
+
* `Fn::ImportValue` / `Fn::GetStackOutput` / nested-stack forms — serves
|
|
39485
|
+
* the MASK itself as the value, which `reresolveCrossStackValue` and the
|
|
39486
|
+
* export blocker DO recognise. Firing there would silently change the
|
|
39487
|
+
* pre-existing issue #2274 behaviour (an output that published `'***'`
|
|
39488
|
+
* would vanish) for no safety gain, and would render this message's
|
|
39489
|
+
* "would publish the resource's raw physical id" over a read for which
|
|
39490
|
+
* there is no physical-id fall-through — the wrong-advice class this PR
|
|
39491
|
+
* has spent three rounds removing.
|
|
39492
|
+
*
|
|
39493
|
+
* **SELECTED BY THE `kind` FIELD, NEVER BY A PATTERN OVER `display`**
|
|
39494
|
+
* (round-4 review, BLOCKER). The previous revision filtered with the same
|
|
39495
|
+
* regex `maskedRecordRemedyFor` used, whose id class was `[A-Za-z0-9]+` —
|
|
39496
|
+
* so for `{"Ref": "My-Table"}` (an `overrideLogicalId`, or a migrated
|
|
39497
|
+
* template; cdkd validates no logical-id charset and never hands the
|
|
39498
|
+
* template to CloudFormation) the filter matched NOTHING, this function
|
|
39499
|
+
* returned, and the output published the raw physical id. That is an
|
|
39500
|
+
* earlier round's blocker reopened through a CHARSET. Falling out of the
|
|
39501
|
+
* pattern is the SAFE direction at the remedy — a vaguer message — and the
|
|
39502
|
+
* INVERTED one here, where it is the refusal itself; one rendering cannot
|
|
39503
|
+
* serve two consumers whose safe directions are opposite, so the structure
|
|
39504
|
+
* moved into the entry and both consumers now ask a field.
|
|
39505
|
+
*
|
|
39506
|
+
* It routes through {@link handleOutputResolutionFailure} rather than
|
|
39507
|
+
* throwing its own way out, so it inherits that method's whole contract:
|
|
39508
|
+
* warn-and-skip by default, promoted to a deploy error under
|
|
39509
|
+
* `--strict-getatt`, and masked against both secret bags on the way.
|
|
39510
|
+
*/
|
|
39511
|
+
const refuseMaskedOutputReads = (outputKey, ownReads) => {
|
|
39512
|
+
const added = ownReads.filter((read) => read.kind === "ref-state-key");
|
|
39513
|
+
if (added.length === 0) return;
|
|
39514
|
+
throw markNonRetryable(/* @__PURE__ */ new Error(`Cannot resolve ${added.map((read) => read.display).join(", ")} for output ${outputKey}: cdkd's recorded state holds only the redaction mask there, so this output would publish the resource's raw physical id instead of the value CloudFormation's Ref returns — a wrong value that a consuming stack's Fn::ImportValue would accept and send to AWS. The output is not published. ${DeployEngine.maskedRecordRemedyFor(added, context.resources)}`));
|
|
39515
|
+
};
|
|
38352
39516
|
const outputsPassSecrets = context.recordedSecretValues ?? EMPTY_SECRETS;
|
|
38353
39517
|
const outputsPassInherited = context.inheritedSecrets ?? EMPTY_SECRETS;
|
|
38354
39518
|
const publishedOutputNames = collectPublishedOutputNames(template.Outputs, conditions);
|
|
@@ -38359,8 +39523,14 @@ var DeployEngine = class {
|
|
|
38359
39523
|
this.logger.debug(`Skipping output ${outputKey} — condition ${output.Condition} is false`);
|
|
38360
39524
|
continue;
|
|
38361
39525
|
}
|
|
39526
|
+
const ownReads = [];
|
|
38362
39527
|
try {
|
|
38363
|
-
|
|
39528
|
+
const resolved = await this.resolver.resolve(output.Value, {
|
|
39529
|
+
...context,
|
|
39530
|
+
redactedAttributeReads: ownReads
|
|
39531
|
+
});
|
|
39532
|
+
refuseMaskedOutputReads(outputKey, ownReads);
|
|
39533
|
+
outputs[outputKey] = resolved;
|
|
38364
39534
|
} catch (error) {
|
|
38365
39535
|
this.handleOutputResolutionFailure(error, outputKey, outputs, outputsPassSecrets, outputsPassInherited);
|
|
38366
39536
|
}
|
|
@@ -38371,13 +39541,16 @@ var DeployEngine = class {
|
|
|
38371
39541
|
const value = outputs[outputKey];
|
|
38372
39542
|
if (value === void 0) continue;
|
|
38373
39543
|
const nameSecrets = /* @__PURE__ */ new Map();
|
|
39544
|
+
const nameReads = [];
|
|
38374
39545
|
let exportName;
|
|
38375
39546
|
try {
|
|
38376
39547
|
try {
|
|
38377
39548
|
exportName = typeof output.Export.Name === "string" ? output.Export.Name : await this.resolver.resolve(output.Export.Name, {
|
|
38378
39549
|
...context,
|
|
38379
|
-
recordedSecretValues: nameSecrets
|
|
39550
|
+
recordedSecretValues: nameSecrets,
|
|
39551
|
+
redactedAttributeReads: nameReads
|
|
38380
39552
|
});
|
|
39553
|
+
refuseMaskedOutputReads(outputKey, nameReads);
|
|
38381
39554
|
} finally {
|
|
38382
39555
|
for (const [plaintext, expression] of nameSecrets) context.recordedSecretValues?.set(plaintext, expression);
|
|
38383
39556
|
}
|
|
@@ -38422,5 +39595,5 @@ var DeployEngine = class {
|
|
|
38422
39595
|
};
|
|
38423
39596
|
|
|
38424
39597
|
//#endregion
|
|
38425
|
-
export { ProviderRegistry as $,
|
|
38426
|
-
//# sourceMappingURL=deploy-engine-
|
|
39598
|
+
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 };
|
|
39599
|
+
//# sourceMappingURL=deploy-engine-umIP3xus.js.map
|