@go-to-k/cdkd 0.283.19 → 0.283.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -0
- package/dist/{asg-provider-B260vt0k.js → asg-provider-DKjoomrX.js} +2 -2
- package/dist/{asg-provider-B260vt0k.js.map → asg-provider-DKjoomrX.js.map} +1 -1
- package/dist/cli.js +266 -5
- package/dist/cli.js.map +1 -1
- package/dist/{deploy-engine-UVLw029j.js → deploy-engine-ISSt01TI.js} +401 -46
- package/dist/deploy-engine-ISSt01TI.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/dist/deploy-engine-UVLw029j.js.map +0 -1
|
@@ -5557,7 +5557,7 @@ var AssetModeResolver = class {
|
|
|
5557
5557
|
* non-`u` pattern. `-` is deliberately absent: it is only special INSIDE a
|
|
5558
5558
|
* character class, and no caller embeds into one.
|
|
5559
5559
|
*/
|
|
5560
|
-
function escapeRegExp(value) {
|
|
5560
|
+
function escapeRegExp$1(value) {
|
|
5561
5561
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
5562
5562
|
}
|
|
5563
5563
|
|
|
@@ -5582,11 +5582,11 @@ function flattenAssetPlaceholders(value, accountId, region, partition = "aws") {
|
|
|
5582
5582
|
* scope and left verbatim.
|
|
5583
5583
|
*/
|
|
5584
5584
|
function isDefaultBootstrapBucketName(name, accountId, region) {
|
|
5585
|
-
return new RegExp(`^cdk-[a-z0-9]+-assets-${accountId}-${escapeRegExp(region)}$`).test(name);
|
|
5585
|
+
return new RegExp(`^cdk-[a-z0-9]+-assets-${accountId}-${escapeRegExp$1(region)}$`).test(name);
|
|
5586
5586
|
}
|
|
5587
5587
|
/** §8 scope rule, container-image leg (`cdk-<qualifier>-container-assets-…`). */
|
|
5588
5588
|
function isDefaultBootstrapRepoName(name, accountId, region) {
|
|
5589
|
-
return new RegExp(`^cdk-[a-z0-9]+-container-assets-${accountId}-${escapeRegExp(region)}$`).test(name);
|
|
5589
|
+
return new RegExp(`^cdk-[a-z0-9]+-container-assets-${accountId}-${escapeRegExp$1(region)}$`).test(name);
|
|
5590
5590
|
}
|
|
5591
5591
|
/**
|
|
5592
5592
|
* Build the §6 asset-location mapping table from a stack's asset manifest.
|
|
@@ -5613,7 +5613,7 @@ function buildAssetRedirectMap(manifest, marker, accountId, region, partition =
|
|
|
5613
5613
|
const addForms = (rawName, flattened, target) => {
|
|
5614
5614
|
sources.set(flattened, target);
|
|
5615
5615
|
if (rawName !== flattened) sources.set(rawName, target);
|
|
5616
|
-
const suffixRe = new RegExp(`-${accountId}-${escapeRegExp(region)}$`);
|
|
5616
|
+
const suffixRe = new RegExp(`-${accountId}-${escapeRegExp$1(region)}$`);
|
|
5617
5617
|
for (const suffix of [
|
|
5618
5618
|
"-${AWS::AccountId}-${AWS::Region}",
|
|
5619
5619
|
`-\${AWS::AccountId}-${region}`,
|
|
@@ -5669,7 +5669,7 @@ function buildAssetRedirectMap(manifest, marker, accountId, region, partition =
|
|
|
5669
5669
|
* with the deploy account+region bootstrap shape — accepted as pathological.
|
|
5670
5670
|
*/
|
|
5671
5671
|
function buildBoundaryRegex(source) {
|
|
5672
|
-
return new RegExp(`(?<![A-Za-z0-9_.-])${escapeRegExp(source)}(?![A-Za-z0-9_-])`, "g");
|
|
5672
|
+
return new RegExp(`(?<![A-Za-z0-9_.-])${escapeRegExp$1(source)}(?![A-Za-z0-9_-])`, "g");
|
|
5673
5673
|
}
|
|
5674
5674
|
function rewriteString(value, map, counter) {
|
|
5675
5675
|
let result = value;
|
|
@@ -9816,6 +9816,95 @@ function s3BucketWebsiteUrl(bucketName, region) {
|
|
|
9816
9816
|
return `http://${bucketName}.s3-website${S3_WEBSITE_ENDPOINT_LEGACY_DASH_REGIONS.has(folded) ? "-" : "."}${folded}.${urlSuffix}`;
|
|
9817
9817
|
}
|
|
9818
9818
|
|
|
9819
|
+
//#endregion
|
|
9820
|
+
//#region src/deployment/secret-redaction.ts
|
|
9821
|
+
/** Fixed marker substituted for a secret value in log / error output. */
|
|
9822
|
+
const SECRET_MASK = "***";
|
|
9823
|
+
/**
|
|
9824
|
+
* A resolved secret value shorter than this is NOT used as a redaction needle:
|
|
9825
|
+
* a 1-2 character plaintext (e.g. a secret whose JSON key holds `"0"`) would
|
|
9826
|
+
* match incidental characters everywhere and mangle unrelated state. Such a
|
|
9827
|
+
* value is still masked at the exact leaf where it was the WHOLE value (handled
|
|
9828
|
+
* by the caller), but is not scanned for as a substring. Real secrets are far
|
|
9829
|
+
* longer than this, so the bound only excludes degenerate cases.
|
|
9830
|
+
*/
|
|
9831
|
+
const MIN_NEEDLE_LENGTH = 4;
|
|
9832
|
+
function escapeRegExp(value) {
|
|
9833
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
9834
|
+
}
|
|
9835
|
+
/**
|
|
9836
|
+
* Build a single alternation regex matching any recorded secret value, longest
|
|
9837
|
+
* first so an overlapping shorter secret cannot pre-empt a longer match. Returns
|
|
9838
|
+
* `undefined` when there is nothing worth scanning for.
|
|
9839
|
+
*/
|
|
9840
|
+
function buildNeedleRegex(values) {
|
|
9841
|
+
const needles = Array.from(new Set(values)).filter((v) => v.length >= MIN_NEEDLE_LENGTH).sort((a, b) => b.length - a.length);
|
|
9842
|
+
if (needles.length === 0) return void 0;
|
|
9843
|
+
return new RegExp(needles.map(escapeRegExp).join("|"), "g");
|
|
9844
|
+
}
|
|
9845
|
+
/**
|
|
9846
|
+
* Deep-clone `bag`, replacing every occurrence of a recorded secret value with
|
|
9847
|
+
* the unresolved `{{resolve:...}}` expression it came from. A string whose WHOLE
|
|
9848
|
+
* value equals a secret is replaced by that secret's expression exactly; a
|
|
9849
|
+
* string that merely CONTAINS one (an `Fn::Join` / `Fn::Sub` result) has the
|
|
9850
|
+
* secret substring replaced in place. Returns the input by identity when there
|
|
9851
|
+
* is nothing to redact, so callers can persist the original object unchanged in
|
|
9852
|
+
* the common no-secret case.
|
|
9853
|
+
*/
|
|
9854
|
+
function redactSecretsForState(bag, secrets) {
|
|
9855
|
+
if (secrets.size === 0) return bag;
|
|
9856
|
+
const regex = buildNeedleRegex(secrets.keys());
|
|
9857
|
+
const wholeValueExpr = (s) => s === "" ? void 0 : secrets.get(s);
|
|
9858
|
+
const walk = (value) => {
|
|
9859
|
+
if (typeof value === "string") {
|
|
9860
|
+
const whole = wholeValueExpr(value);
|
|
9861
|
+
if (whole !== void 0) return whole;
|
|
9862
|
+
if (!regex) return value;
|
|
9863
|
+
regex.lastIndex = 0;
|
|
9864
|
+
if (!regex.test(value)) return value;
|
|
9865
|
+
regex.lastIndex = 0;
|
|
9866
|
+
return value.replace(regex, (m) => secrets.get(m) ?? "***");
|
|
9867
|
+
}
|
|
9868
|
+
if (Array.isArray(value)) return value.map(walk);
|
|
9869
|
+
if (value !== null && typeof value === "object") {
|
|
9870
|
+
const out = {};
|
|
9871
|
+
for (const [k, v] of Object.entries(value)) out[k] = walk(v);
|
|
9872
|
+
return out;
|
|
9873
|
+
}
|
|
9874
|
+
return value;
|
|
9875
|
+
};
|
|
9876
|
+
return walk(bag);
|
|
9877
|
+
}
|
|
9878
|
+
/**
|
|
9879
|
+
* Redact resolved secret plaintext out of one resource state record's
|
|
9880
|
+
* `properties` / `attributes` / `observedProperties`, replacing each secret
|
|
9881
|
+
* value with its unresolved expression. Returns a NEW record when any field
|
|
9882
|
+
* changed, or the input by identity when there are no secrets — so callers can
|
|
9883
|
+
* detect a no-op cheaply. Shared by the deploy engine's save choke point and
|
|
9884
|
+
* the `cdkd scrub` command so both scrub the same three fields identically.
|
|
9885
|
+
*/
|
|
9886
|
+
function scrubResourceRecord(record, secrets) {
|
|
9887
|
+
if (secrets.size === 0) return record;
|
|
9888
|
+
const next = { ...record };
|
|
9889
|
+
next.properties = redactSecretsForState(record.properties, secrets);
|
|
9890
|
+
if (record.attributes) next.attributes = redactSecretsForState(record.attributes, secrets);
|
|
9891
|
+
if (record.observedProperties) next.observedProperties = redactSecretsForState(record.observedProperties, secrets);
|
|
9892
|
+
return next;
|
|
9893
|
+
}
|
|
9894
|
+
/**
|
|
9895
|
+
* Replace every recorded secret value inside `text` with {@link SECRET_MASK}.
|
|
9896
|
+
* Used on log lines and error messages where a resolved secret could otherwise
|
|
9897
|
+
* be echoed. Whole-value and embedded matches are both masked. Returns `text`
|
|
9898
|
+
* unchanged when there is nothing to mask.
|
|
9899
|
+
*/
|
|
9900
|
+
function maskSecretsInText(text, secrets) {
|
|
9901
|
+
if (secrets.size === 0) return text;
|
|
9902
|
+
if (text !== "" && secrets.has(text)) return "***";
|
|
9903
|
+
const regex = buildNeedleRegex(secrets.keys());
|
|
9904
|
+
if (!regex) return text;
|
|
9905
|
+
return text.replace(regex, "***");
|
|
9906
|
+
}
|
|
9907
|
+
|
|
9819
9908
|
//#endregion
|
|
9820
9909
|
//#region src/provisioning/config-shape.ts
|
|
9821
9910
|
/**
|
|
@@ -11405,6 +11494,29 @@ const cachedAvailabilityZones = {};
|
|
|
11405
11494
|
*/
|
|
11406
11495
|
const cachedDynamicReferences = {};
|
|
11407
11496
|
/**
|
|
11497
|
+
* The `{{resolve:ssm:...}}` expressions this process has PROVEN point at a
|
|
11498
|
+
* `SecureString` parameter (issue #1901).
|
|
11499
|
+
*
|
|
11500
|
+
* A plain `ssm` reference is not a secret by SPELLING the way `secretsmanager`
|
|
11501
|
+
* is — whether it resolves to public config or to a decrypted secret depends on
|
|
11502
|
+
* the parameter's `Type`, which is only knowable from the `GetParameter`
|
|
11503
|
+
* response. So secret-ness is discovered on the first resolution and remembered
|
|
11504
|
+
* here, keyed by the full `{{resolve:...}}` expression, for the same lifetime as
|
|
11505
|
+
* {@link cachedDynamicReferences}.
|
|
11506
|
+
*
|
|
11507
|
+
* Two consumers need it AFTER the lookup that populated it: the cache-hit arm
|
|
11508
|
+
* (which must re-record the value as a secret for the current resolution pass)
|
|
11509
|
+
* and the diff / no-op path (which must leave a SecureString reference
|
|
11510
|
+
* unresolved without paying a lookup at all once the type is known). A
|
|
11511
|
+
* reference NOT in this set is only "not known to be secure" — never "proven
|
|
11512
|
+
* public" — so every arm that would leak still asks AWS for the type first.
|
|
11513
|
+
*
|
|
11514
|
+
* Only the TYPE is remembered, never the decrypted value: on the diff path the
|
|
11515
|
+
* lookup is made with `WithDecryption: false`, so the plaintext is never
|
|
11516
|
+
* fetched at all there.
|
|
11517
|
+
*/
|
|
11518
|
+
const secureStringSsmReferences = /* @__PURE__ */ new Set();
|
|
11519
|
+
/**
|
|
11408
11520
|
* Cache for EC2 instance attributes that require a live DescribeInstances
|
|
11409
11521
|
* lookup (PrivateIp / PublicIp / PrivateDnsName / PublicDnsName /
|
|
11410
11522
|
* AvailabilityZone). Keyed by `${physicalId}#${attributeName}`. The IP /
|
|
@@ -11770,7 +11882,7 @@ var IntrinsicFunctionResolver = class {
|
|
|
11770
11882
|
*/
|
|
11771
11883
|
async resolveValue(value, context) {
|
|
11772
11884
|
if (typeof value !== "object" || value === null) {
|
|
11773
|
-
if (typeof value === "string" && value.includes("{{resolve:")) return await this.resolveDynamicReferences(value);
|
|
11885
|
+
if (typeof value === "string" && value.includes("{{resolve:")) return await this.resolveDynamicReferences(value, context);
|
|
11774
11886
|
return value;
|
|
11775
11887
|
}
|
|
11776
11888
|
if (Array.isArray(value)) return (await Promise.all(value.map((v) => this.resolveValue(v, context)))).filter((v) => v !== AWS_NO_VALUE);
|
|
@@ -12373,8 +12485,8 @@ var IntrinsicFunctionResolver = class {
|
|
|
12373
12485
|
const resolved = await this.resolveValue(v, context);
|
|
12374
12486
|
return String(resolved);
|
|
12375
12487
|
}))).join(delimiter);
|
|
12376
|
-
if (result.includes("{{resolve:")) result = await this.resolveDynamicReferences(result);
|
|
12377
|
-
this.logger.debug(`Resolved Fn::Join: ${result}`);
|
|
12488
|
+
if (result.includes("{{resolve:")) result = await this.resolveDynamicReferences(result, context);
|
|
12489
|
+
this.logger.debug(`Resolved Fn::Join: ${this.maskSecretsForLog(result, context)}`);
|
|
12378
12490
|
return result;
|
|
12379
12491
|
}
|
|
12380
12492
|
/**
|
|
@@ -12459,8 +12571,8 @@ var IntrinsicFunctionResolver = class {
|
|
|
12459
12571
|
const entry = replacements[cursor++];
|
|
12460
12572
|
return entry ? entry.replacement : whole;
|
|
12461
12573
|
});
|
|
12462
|
-
if (result.includes("{{resolve:")) result = await this.resolveDynamicReferences(result);
|
|
12463
|
-
this.logger.debug(`Resolved Fn::Sub: ${result}`);
|
|
12574
|
+
if (result.includes("{{resolve:")) result = await this.resolveDynamicReferences(result, context);
|
|
12575
|
+
this.logger.debug(`Resolved Fn::Sub: ${this.maskSecretsForLog(result, context)}`);
|
|
12464
12576
|
return result;
|
|
12465
12577
|
}
|
|
12466
12578
|
/**
|
|
@@ -12478,7 +12590,7 @@ var IntrinsicFunctionResolver = class {
|
|
|
12478
12590
|
return `{{Fn::Select:${index}:OutOfBounds}}`;
|
|
12479
12591
|
}
|
|
12480
12592
|
const result = resolvedList[index];
|
|
12481
|
-
this.logger.debug(`Resolved Fn::Select: index ${index} -> ${JSON.stringify(result)}`);
|
|
12593
|
+
this.logger.debug(`Resolved Fn::Select: index ${index} -> ${this.maskSecretsForLog(JSON.stringify(result), context)}`);
|
|
12482
12594
|
return result;
|
|
12483
12595
|
}
|
|
12484
12596
|
/**
|
|
@@ -12620,7 +12732,7 @@ var IntrinsicFunctionResolver = class {
|
|
|
12620
12732
|
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.`));
|
|
12621
12733
|
}
|
|
12622
12734
|
const result = resolvedValue.split(delimiter);
|
|
12623
|
-
this.logger.debug(`Resolved Fn::Split: split by "${delimiter}" -> ${JSON.stringify(result)}`);
|
|
12735
|
+
this.logger.debug(`Resolved Fn::Split: split by "${delimiter}" -> ${this.maskSecretsForLog(JSON.stringify(result), context)}`);
|
|
12624
12736
|
return result;
|
|
12625
12737
|
}
|
|
12626
12738
|
/**
|
|
@@ -13128,7 +13240,7 @@ var IntrinsicFunctionResolver = class {
|
|
|
13128
13240
|
const resolvedValue = await this.resolveValue(value, context);
|
|
13129
13241
|
if (typeof resolvedValue !== "string") throw new Error(`Fn::Base64: value must resolve to a string, got ${typeof resolvedValue}`);
|
|
13130
13242
|
const result = Buffer.from(resolvedValue).toString("base64");
|
|
13131
|
-
this.logger.debug(`Resolved Fn::Base64: ${resolvedValue} -> ${result}`);
|
|
13243
|
+
this.logger.debug(`Resolved Fn::Base64: ${this.maskSecretsForLog(resolvedValue, context)} -> ${this.maskSecretsForLog(result, context)}`);
|
|
13132
13244
|
return result;
|
|
13133
13245
|
}
|
|
13134
13246
|
/**
|
|
@@ -13195,7 +13307,16 @@ var IntrinsicFunctionResolver = class {
|
|
|
13195
13307
|
*
|
|
13196
13308
|
* Results are cached to avoid repeated API calls.
|
|
13197
13309
|
*/
|
|
13198
|
-
|
|
13310
|
+
/**
|
|
13311
|
+
* Mask any resolved secret value out of a string bound for a log line, using
|
|
13312
|
+
* the secrets recorded on the resolution pass (GHSA fix). No-op when the pass
|
|
13313
|
+
* recorded no secrets.
|
|
13314
|
+
*/
|
|
13315
|
+
maskSecretsForLog(text, context) {
|
|
13316
|
+
const secrets = context?.recordedSecretValues;
|
|
13317
|
+
return secrets ? maskSecretsInText(text, secrets) : text;
|
|
13318
|
+
}
|
|
13319
|
+
async resolveDynamicReferences(value, context) {
|
|
13199
13320
|
const pattern = /\{\{resolve:([^}]+)\}\}/g;
|
|
13200
13321
|
let result = value;
|
|
13201
13322
|
let match;
|
|
@@ -13205,21 +13326,36 @@ var IntrinsicFunctionResolver = class {
|
|
|
13205
13326
|
inner: match[1]
|
|
13206
13327
|
});
|
|
13207
13328
|
for (const { fullMatch, inner } of matches) {
|
|
13329
|
+
const service = inner.split(":")[0];
|
|
13330
|
+
const isKnownSecret = service === "secretsmanager" || secureStringSsmReferences.has(fullMatch);
|
|
13331
|
+
if (isKnownSecret && context?.skipDynamicReferences) continue;
|
|
13208
13332
|
if (fullMatch in cachedDynamicReferences) {
|
|
13209
|
-
|
|
13333
|
+
const cached = cachedDynamicReferences[fullMatch];
|
|
13334
|
+
if (isKnownSecret && cached) context?.recordedSecretValues?.set(cached, fullMatch);
|
|
13335
|
+
result = result.replace(fullMatch, () => cached);
|
|
13210
13336
|
continue;
|
|
13211
13337
|
}
|
|
13212
13338
|
const parts = inner.split(":");
|
|
13213
|
-
const service = parts[0];
|
|
13214
13339
|
let resolved;
|
|
13340
|
+
let isSecret = isKnownSecret;
|
|
13215
13341
|
if (service === "secretsmanager") resolved = await this.resolveSecretsManagerReference(inner);
|
|
13216
|
-
else if (service === "ssm")
|
|
13217
|
-
|
|
13342
|
+
else if (service === "ssm") {
|
|
13343
|
+
const decrypt = context?.skipDynamicReferences !== true;
|
|
13344
|
+
const param = await this.resolveSSMReference(parts, decrypt);
|
|
13345
|
+
if (param.type === "SecureString") secureStringSsmReferences.add(fullMatch);
|
|
13346
|
+
else if (!param.secure) secureStringSsmReferences.delete(fullMatch);
|
|
13347
|
+
isSecret = param.secure;
|
|
13348
|
+
if (param.secure) {
|
|
13349
|
+
if (!decrypt) continue;
|
|
13350
|
+
}
|
|
13351
|
+
resolved = param.value;
|
|
13352
|
+
} else {
|
|
13218
13353
|
this.logger.warn(`Unsupported dynamic reference service: ${service}`);
|
|
13219
13354
|
continue;
|
|
13220
13355
|
}
|
|
13221
13356
|
cachedDynamicReferences[fullMatch] = resolved;
|
|
13222
|
-
|
|
13357
|
+
if (isSecret && resolved) context?.recordedSecretValues?.set(resolved, fullMatch);
|
|
13358
|
+
result = result.replace(fullMatch, () => resolved);
|
|
13223
13359
|
}
|
|
13224
13360
|
return result;
|
|
13225
13361
|
}
|
|
@@ -13372,18 +13508,44 @@ var IntrinsicFunctionResolver = class {
|
|
|
13372
13508
|
for (let i = 7; i >= 0; i--) parts.push((n >> BigInt(i * 16) & BigInt(65535)).toString(16));
|
|
13373
13509
|
return parts.join(":");
|
|
13374
13510
|
}
|
|
13375
|
-
|
|
13511
|
+
/**
|
|
13512
|
+
* Resolve an `{{resolve:ssm:...}}` dynamic reference, reporting whether the
|
|
13513
|
+
* parameter is a `SecureString` (issue #1901).
|
|
13514
|
+
*
|
|
13515
|
+
* `secure` is read off the SAME `GetParameter` response that carries the
|
|
13516
|
+
* value, so classifying a reference costs no extra API call — which is what
|
|
13517
|
+
* makes it affordable on the comparison path too.
|
|
13518
|
+
*
|
|
13519
|
+
* `decrypt` maps straight to `WithDecryption`. SSM ignores it for `String` /
|
|
13520
|
+
* `StringList` (their `Value` is identical either way), so the only thing it
|
|
13521
|
+
* changes is whether a `SecureString`'s `Value` comes back as plaintext or as
|
|
13522
|
+
* its encrypted blob. Callers that only need the TYPE pass `false` and MUST
|
|
13523
|
+
* discard the value when `secure` is set — it is ciphertext, not the resolved
|
|
13524
|
+
* reference.
|
|
13525
|
+
*/
|
|
13526
|
+
async resolveSSMReference(parts, decrypt = true) {
|
|
13376
13527
|
const parameterName = parts.slice(1).join(":");
|
|
13377
13528
|
if (!parameterName) throw new Error("Dynamic reference: ssm PARAMETER_NAME is required");
|
|
13378
13529
|
this.logger.debug(`Resolving dynamic reference: ssm:${parameterName}`);
|
|
13379
13530
|
const client = getAwsClients().ssm;
|
|
13380
13531
|
const command = new GetParameterCommand({
|
|
13381
13532
|
Name: parameterName,
|
|
13382
|
-
WithDecryption:
|
|
13533
|
+
WithDecryption: decrypt
|
|
13383
13534
|
});
|
|
13384
|
-
const
|
|
13535
|
+
const response = await client.send(command);
|
|
13536
|
+
const paramValue = response.Parameter?.Value;
|
|
13385
13537
|
if (paramValue === void 0 || paramValue === null) throw new Error(`Dynamic reference: SSM parameter '${parameterName}' not found or has no value`);
|
|
13386
|
-
|
|
13538
|
+
const paramType = response.Parameter?.Type;
|
|
13539
|
+
const secure = paramType !== "String" && paramType !== "StringList";
|
|
13540
|
+
if (secure && paramType !== "SecureString") {
|
|
13541
|
+
const reported = paramType === void 0 ? "(absent)" : `'${String(paramType)}'`;
|
|
13542
|
+
this.logger.warn(`SSM parameter '${parameterName}' reported an unrecognized Type ${reported} — treating its value as a secret, so cdkd will persist the {{resolve:ssm:...}} expression rather than the resolved value. Declare the parameter as String / StringList if it is public config.`);
|
|
13543
|
+
}
|
|
13544
|
+
return {
|
|
13545
|
+
value: paramValue,
|
|
13546
|
+
secure,
|
|
13547
|
+
type: paramType
|
|
13548
|
+
};
|
|
13387
13549
|
}
|
|
13388
13550
|
};
|
|
13389
13551
|
|
|
@@ -14182,7 +14344,13 @@ var CloudControlProvider = class {
|
|
|
14182
14344
|
};
|
|
14183
14345
|
}
|
|
14184
14346
|
}
|
|
14185
|
-
this.logger.debug(`Generated ${patch.length} patch operations for ${logicalId}: ${JSON.stringify(patch)
|
|
14347
|
+
this.logger.debug(`Generated ${patch.length} patch operations for ${logicalId}: ${JSON.stringify(patch.map((op) => {
|
|
14348
|
+
const anyOp = op;
|
|
14349
|
+
return {
|
|
14350
|
+
op: anyOp.op,
|
|
14351
|
+
path: anyOp.path
|
|
14352
|
+
};
|
|
14353
|
+
}))}`);
|
|
14186
14354
|
const updateResponse = await this.cloudControlClient.send(new UpdateResourceCommand({
|
|
14187
14355
|
TypeName: resourceType,
|
|
14188
14356
|
Identifier: physicalId,
|
|
@@ -14212,7 +14380,7 @@ var CloudControlProvider = class {
|
|
|
14212
14380
|
if (context?.finalSnapshotIdentifier !== void 0) throw new ProvisioningError(`${logicalId} (${resourceType}) requires a final snapshot (DeletionPolicy: Snapshot), but the Cloud Control API delete route has no final-snapshot parameter. Re-run with --skip-final-snapshot after snapshotting manually, or retain the resource.`, resourceType, logicalId, physicalId);
|
|
14213
14381
|
if (context?.removeProtection === true && resourceType === "AWS::AutoScaling::AutoScalingGroup") {
|
|
14214
14382
|
this.logger.debug(`Delegating protected AutoScalingGroup ${logicalId} delete to the SDK ASGProvider (Cloud Control cannot force-delete a protected ASG)`);
|
|
14215
|
-
const { ASGProvider } = await import("./asg-provider-
|
|
14383
|
+
const { ASGProvider } = await import("./asg-provider-DKjoomrX.js").then((n) => n.n);
|
|
14216
14384
|
return await new ASGProvider().delete(logicalId, physicalId, resourceType, _properties, context);
|
|
14217
14385
|
}
|
|
14218
14386
|
const isProtectedEc2Instance = context?.removeProtection === true && resourceType === "AWS::EC2::Instance";
|
|
@@ -20765,6 +20933,65 @@ async function replayRollback(operations, stateResources, stackName, ctx, option
|
|
|
20765
20933
|
* just announced and leaving the record describing something AWS does not
|
|
20766
20934
|
* hold.
|
|
20767
20935
|
*/
|
|
20936
|
+
/**
|
|
20937
|
+
* Re-resolve dynamic-reference SECRET expressions
|
|
20938
|
+
* (`{{resolve:secretsmanager:...}}`) in a property bag being REPLAYED to a
|
|
20939
|
+
* provider during rollback (GHSA fix, issue #1899 review).
|
|
20940
|
+
*
|
|
20941
|
+
* The rollback journal — and the state record the replay writes — store the
|
|
20942
|
+
* redacted EXPRESSION, never the plaintext. But a `provider.update()` /
|
|
20943
|
+
* `create()` / `delete()` call must receive the concrete secret value the
|
|
20944
|
+
* reference points at, exactly as the forward deploy did; replaying the literal
|
|
20945
|
+
* `{{resolve:...}}` string would corrupt the resource (e.g. a Lambda env var or
|
|
20946
|
+
* Cognito `client_secret`). Rollback is synth-free, so re-resolve straight from
|
|
20947
|
+
* the expression string here.
|
|
20948
|
+
*
|
|
20949
|
+
* Records each `plaintext -> expression` into `secrets` so the caller can redact
|
|
20950
|
+
* the persisted state record back to the expression — the same
|
|
20951
|
+
* resolve-for-provider + redact-for-state split the deploy engine applies at its
|
|
20952
|
+
* save choke point. A bag with no `{{resolve:...}}` string resolves to a
|
|
20953
|
+
* structural copy of itself (secrets stays empty), so the non-secret rollback
|
|
20954
|
+
* path is behaviourally unchanged. Which references are RECORDED is the
|
|
20955
|
+
* resolver's own secret gate: every `secretsmanager` one, plus an `ssm` one
|
|
20956
|
+
* whose parameter is a `SecureString` (issue #1901 — that form decrypts to a
|
|
20957
|
+
* real secret, so it is redacted into the journal and must be re-resolved here
|
|
20958
|
+
* exactly like a secretsmanager reference). An ssm reference to a `String` /
|
|
20959
|
+
* `StringList` parameter is public config, stored resolved, and never appears
|
|
20960
|
+
* as an expression in the journal.
|
|
20961
|
+
*/
|
|
20962
|
+
async function resolveReplayProps(props, resolver, secrets) {
|
|
20963
|
+
if (props === void 0) return void 0;
|
|
20964
|
+
const ctx = {
|
|
20965
|
+
template: { Resources: {} },
|
|
20966
|
+
resources: {},
|
|
20967
|
+
recordedSecretValues: secrets
|
|
20968
|
+
};
|
|
20969
|
+
const walk = async (v) => {
|
|
20970
|
+
if (typeof v === "string") return v.includes("{{resolve:") ? await resolver.resolveDynamicReferences(v, ctx) : v;
|
|
20971
|
+
if (Array.isArray(v)) {
|
|
20972
|
+
const out = new Array(v.length);
|
|
20973
|
+
for (let i = 0; i < v.length; i++) out[i] = await walk(v[i]);
|
|
20974
|
+
return out;
|
|
20975
|
+
}
|
|
20976
|
+
if (v !== null && typeof v === "object") {
|
|
20977
|
+
const out = {};
|
|
20978
|
+
for (const [k, val] of Object.entries(v)) out[k] = await walk(val);
|
|
20979
|
+
return out;
|
|
20980
|
+
}
|
|
20981
|
+
return v;
|
|
20982
|
+
};
|
|
20983
|
+
return await walk(props);
|
|
20984
|
+
}
|
|
20985
|
+
/**
|
|
20986
|
+
* Redact resolved secret plaintext back out of a post-rollback state record
|
|
20987
|
+
* (GHSA fix). The record's `properties` may be the provider's
|
|
20988
|
+
* `effectiveProperties`, which can echo the value we just resolved for the
|
|
20989
|
+
* provider call — so scrub it with the same per-op secrets map before it is
|
|
20990
|
+
* persisted. No-op when the op resolved no secret.
|
|
20991
|
+
*/
|
|
20992
|
+
function redactRollbackRecord(record, secrets) {
|
|
20993
|
+
return secrets.size > 0 ? scrubResourceRecord(record, secrets) : record;
|
|
20994
|
+
}
|
|
20768
20995
|
async function updateWithRollbackRetry(provider, args, logicalId, logger, isInterrupted) {
|
|
20769
20996
|
if (provider.disableOuterRetry) return await provider.update(...args);
|
|
20770
20997
|
return await withRetry(() => provider.update(...args), logicalId, {
|
|
@@ -20822,6 +21049,7 @@ function recordedPropertiesAfterReplayCreate(restored, result) {
|
|
|
20822
21049
|
async function replaySingle(op, stateResources, stackName, ctx, orphanLogicalIds, result, afterOp, isInterrupted) {
|
|
20823
21050
|
const action = classifyRollbackOp(op, stateResources, orphanLogicalIds);
|
|
20824
21051
|
const { logger } = ctx;
|
|
21052
|
+
const resolver = new IntrinsicFunctionResolver(ctx.region);
|
|
20825
21053
|
/**
|
|
20826
21054
|
* The route a CREATE-rollback arm resolved for this op (issue #1366) —
|
|
20827
21055
|
* hoisted so the shared catch's ROLLBACK_RESOURCE_FAILED reports the route
|
|
@@ -20946,6 +21174,8 @@ async function replaySingle(op, stateResources, stackName, ctx, orphanLogicalIds
|
|
|
20946
21174
|
case "reverse-replacement": {
|
|
20947
21175
|
const current = stateResources[op.logicalId];
|
|
20948
21176
|
const prev = op.previousState;
|
|
21177
|
+
const secrets = /* @__PURE__ */ new Map();
|
|
21178
|
+
const resolvedPrevProps = await resolveReplayProps(prev.properties, resolver, secrets) ?? {};
|
|
20949
21179
|
logger.info(` Rollback: Reversing replacement of ${op.logicalId} (${op.resourceType}) — re-creating the old resource and deleting the new one`);
|
|
20950
21180
|
if (STATEFUL_TYPES.has(op.resourceType)) logger.warn(` ⚠ ${op.logicalId} (${op.resourceType}) is a stateful type — the old physical resource's data was destroyed by the replacement and CANNOT be recovered; the re-created resource starts empty.`);
|
|
20951
21181
|
const { provider: createProvider } = ctx.providerRegistry.getProviderFor({
|
|
@@ -20959,7 +21189,7 @@ async function replaySingle(op, stateResources, stackName, ctx, orphanLogicalIds
|
|
|
20959
21189
|
let deletedNewFirst = false;
|
|
20960
21190
|
let createResult;
|
|
20961
21191
|
try {
|
|
20962
|
-
createResult = await withRetry(() => createProvider.create(op.logicalId, op.resourceType, { ...
|
|
21192
|
+
createResult = await withRetry(() => createProvider.create(op.logicalId, op.resourceType, { ...resolvedPrevProps }, REPLAYING_STATE_CREATE_CONTEXT), op.logicalId, {
|
|
20963
21193
|
...RECREATE_RETRY_SCHEDULE,
|
|
20964
21194
|
logger,
|
|
20965
21195
|
...isInterrupted && {
|
|
@@ -20982,7 +21212,7 @@ async function replaySingle(op, stateResources, stackName, ctx, orphanLogicalIds
|
|
|
20982
21212
|
delete stateResources[op.logicalId];
|
|
20983
21213
|
await afterOp?.(op.logicalId);
|
|
20984
21214
|
try {
|
|
20985
|
-
createResult = await withRetry(() => createProvider.create(op.logicalId, op.resourceType, { ...
|
|
21215
|
+
createResult = await withRetry(() => createProvider.create(op.logicalId, op.resourceType, { ...resolvedPrevProps }, REPLAYING_STATE_CREATE_CONTEXT), op.logicalId, {
|
|
20986
21216
|
...RECREATE_RETRY_SCHEDULE,
|
|
20987
21217
|
logger,
|
|
20988
21218
|
...isInterrupted && {
|
|
@@ -21001,12 +21231,12 @@ async function replaySingle(op, stateResources, stackName, ctx, orphanLogicalIds
|
|
|
21001
21231
|
result.warnings++;
|
|
21002
21232
|
}
|
|
21003
21233
|
const { observedProperties: _staleObserved, ...prevRecord } = prev;
|
|
21004
|
-
stateResources[op.logicalId] = {
|
|
21234
|
+
stateResources[op.logicalId] = redactRollbackRecord({
|
|
21005
21235
|
...prevRecord,
|
|
21006
21236
|
physicalId: createResult.physicalId,
|
|
21007
21237
|
attributes: createResult.attributes ?? {},
|
|
21008
21238
|
properties: recordedPropertiesAfterReplayCreate(prevRecord, createResult)
|
|
21009
|
-
};
|
|
21239
|
+
}, secrets);
|
|
21010
21240
|
await afterOp?.(op.logicalId);
|
|
21011
21241
|
if (!deletedNewFirst && !adoptedLiveNewResource) try {
|
|
21012
21242
|
const finalSnapshotIdentifier = rollbackFinalSnapshotId(op.resourceType, current, op.provisionedBy);
|
|
@@ -21047,14 +21277,17 @@ async function replaySingle(op, stateResources, stackName, ctx, orphanLogicalIds
|
|
|
21047
21277
|
resourceType: op.resourceType,
|
|
21048
21278
|
provisionedBy: op.provisionedBy
|
|
21049
21279
|
});
|
|
21280
|
+
const secrets = /* @__PURE__ */ new Map();
|
|
21281
|
+
const desiredProps = await resolveReplayProps(previousState.properties, resolver, secrets);
|
|
21282
|
+
const currentProps = await resolveReplayProps(current.properties, resolver, secrets);
|
|
21050
21283
|
const revertResult = await updateWithRollbackRetry(provider, [
|
|
21051
21284
|
op.logicalId,
|
|
21052
21285
|
current.physicalId,
|
|
21053
21286
|
op.resourceType,
|
|
21054
|
-
|
|
21055
|
-
|
|
21287
|
+
desiredProps ?? {},
|
|
21288
|
+
currentProps ?? {}
|
|
21056
21289
|
], op.logicalId, logger, isInterrupted);
|
|
21057
|
-
stateResources[op.logicalId] = recordAfterRollbackUpdate(previousState, revertResult);
|
|
21290
|
+
stateResources[op.logicalId] = redactRollbackRecord(recordAfterRollbackUpdate(previousState, revertResult), secrets);
|
|
21058
21291
|
logger.info(` Rollback: ${op.logicalId} restored successfully`);
|
|
21059
21292
|
await afterOp?.(op.logicalId);
|
|
21060
21293
|
ctx.recordEvent?.({
|
|
@@ -21102,6 +21335,7 @@ async function replayFailedOperations(failedOps, stateResources, stackName, ctx,
|
|
|
21102
21335
|
remainingFailedOps: []
|
|
21103
21336
|
};
|
|
21104
21337
|
const { logger } = ctx;
|
|
21338
|
+
const resolver = new IntrinsicFunctionResolver(ctx.region);
|
|
21105
21339
|
const emitEnvelope = options.emitEnvelope === true && failedOps.length > 0;
|
|
21106
21340
|
if (emitEnvelope) ctx.recordEvent?.({
|
|
21107
21341
|
eventType: "ROLLBACK_STARTED",
|
|
@@ -21183,14 +21417,17 @@ async function replayFailedOperations(failedOps, stateResources, stackName, ctx,
|
|
|
21183
21417
|
resourceType: op.resourceType,
|
|
21184
21418
|
provisionedBy: op.provisionedBy ?? current.provisionedBy
|
|
21185
21419
|
});
|
|
21420
|
+
const secrets = /* @__PURE__ */ new Map();
|
|
21421
|
+
const desiredProps = await resolveReplayProps(prev.properties, resolver, secrets);
|
|
21422
|
+
const attemptedProps = await resolveReplayProps(op.attemptedProperties ?? current.properties, resolver, secrets);
|
|
21186
21423
|
const revertFailedResult = await updateWithRollbackRetry(provider, [
|
|
21187
21424
|
op.logicalId,
|
|
21188
21425
|
current.physicalId,
|
|
21189
21426
|
op.resourceType,
|
|
21190
|
-
|
|
21191
|
-
|
|
21427
|
+
desiredProps ?? {},
|
|
21428
|
+
attemptedProps ?? {}
|
|
21192
21429
|
], op.logicalId, logger, options.isInterrupted);
|
|
21193
|
-
stateResources[op.logicalId] = recordAfterRollbackUpdate(prev, revertFailedResult);
|
|
21430
|
+
stateResources[op.logicalId] = redactRollbackRecord(recordAfterRollbackUpdate(prev, revertFailedResult), secrets);
|
|
21194
21431
|
logger.info(` Rollback: ${op.logicalId} reverted successfully`);
|
|
21195
21432
|
await options.afterOp?.(op.logicalId);
|
|
21196
21433
|
ctx.recordEvent?.({
|
|
@@ -21316,7 +21553,7 @@ const FLUSH_INTERVAL_MS = 2e3;
|
|
|
21316
21553
|
const FLUSH_EVENT_THRESHOLD = 50;
|
|
21317
21554
|
/** Build-time cdkd version, with a dev fallback for non-built contexts. */
|
|
21318
21555
|
function getCdkdVersion() {
|
|
21319
|
-
return "0.283.
|
|
21556
|
+
return "0.283.21";
|
|
21320
21557
|
}
|
|
21321
21558
|
/**
|
|
21322
21559
|
* Generate a time-sortable unique run id, e.g.
|
|
@@ -21913,6 +22150,30 @@ var DeployEngine = class {
|
|
|
21913
22150
|
*/
|
|
21914
22151
|
recordedOutputReads = [];
|
|
21915
22152
|
/**
|
|
22153
|
+
* PER-RESOURCE map of resolved SECRET dynamic-reference values
|
|
22154
|
+
* (plaintext -> `{{resolve:...}}` expression) the resolver records for each
|
|
22155
|
+
* resource's own resolution (GHSA fix). Keyed by logicalId. Per-resource, NOT
|
|
22156
|
+
* session-wide, because a session-wide map cross-contaminates: if resource A
|
|
22157
|
+
* resolves a `{{resolve:...:SecretString}}` whole-secret reference to value V,
|
|
22158
|
+
* and resource B (e.g. the `AWS::SecretsManager::Secret` that OWNS the secret)
|
|
22159
|
+
* carries V as its own LITERAL property, a session-wide value scan would
|
|
22160
|
+
* wrongly rewrite B's literal to A's expression — a false positive that shows
|
|
22161
|
+
* up as a permanent spurious diff. Redacting each resource only with the
|
|
22162
|
+
* secrets substituted during ITS OWN resolution scopes the value match
|
|
22163
|
+
* correctly. Kept on the engine (not just the resolver context) so the async
|
|
22164
|
+
* observed-property capture — which drains after the context is gone — can
|
|
22165
|
+
* still redact an AWS-readback secret (Cognito `client_secret`). Reset per
|
|
22166
|
+
* `deploy()`. See `secret-redaction.ts`.
|
|
22167
|
+
*/
|
|
22168
|
+
perResourceSecrets = /* @__PURE__ */ new Map();
|
|
22169
|
+
/**
|
|
22170
|
+
* Resolved secrets recorded while resolving the stack OUTPUTS (a `CfnOutput`
|
|
22171
|
+
* whose Value resolves a `{{resolve:...}}` reference). Separate from the
|
|
22172
|
+
* per-resource maps for the same anti-cross-contamination reason. Reset per
|
|
22173
|
+
* `deploy()`.
|
|
22174
|
+
*/
|
|
22175
|
+
outputSecrets = /* @__PURE__ */ new Map();
|
|
22176
|
+
/**
|
|
21916
22177
|
* Per-logical-id snapshot of the intrinsic-RESOLVED desired properties
|
|
21917
22178
|
* each CREATE / UPDATE attempted (issue #1198). Written just before the
|
|
21918
22179
|
* provider call; read only when the op FAILS, to journal the failed op's
|
|
@@ -21953,6 +22214,8 @@ var DeployEngine = class {
|
|
|
21953
22214
|
async deploy(stackName, template) {
|
|
21954
22215
|
this.recordedImports = [];
|
|
21955
22216
|
this.recordedOutputReads = [];
|
|
22217
|
+
this.perResourceSecrets = /* @__PURE__ */ new Map();
|
|
22218
|
+
this.outputSecrets = /* @__PURE__ */ new Map();
|
|
21956
22219
|
this.resolver.resetPhysicalIdFallbackCount();
|
|
21957
22220
|
return withStackName(stackName, () => this.doDeploy(stackName, template));
|
|
21958
22221
|
}
|
|
@@ -21972,7 +22235,63 @@ var DeployEngine = class {
|
|
|
21972
22235
|
stackName,
|
|
21973
22236
|
...this.exportIndexStore && { exportIndex: this.exportIndexStore },
|
|
21974
22237
|
recordedImports: this.recordedImports,
|
|
21975
|
-
recordedOutputReads: this.recordedOutputReads
|
|
22238
|
+
recordedOutputReads: this.recordedOutputReads,
|
|
22239
|
+
recordedSecretValues: /* @__PURE__ */ new Map()
|
|
22240
|
+
};
|
|
22241
|
+
}
|
|
22242
|
+
/**
|
|
22243
|
+
* Redact resolved secret plaintext out of a bag about to be PERSISTED to
|
|
22244
|
+
* state, replacing each secret value with the unresolved `{{resolve:...}}`
|
|
22245
|
+
* expression it came from (GHSA fix; see `secret-redaction.ts`). No-op when
|
|
22246
|
+
* the deploy recorded no secrets. The bag sent to the AWS API is the
|
|
22247
|
+
* un-redacted resolved bag; only the persisted copy is rewritten.
|
|
22248
|
+
*/
|
|
22249
|
+
/**
|
|
22250
|
+
* Bag-level redaction of a single properties bag against a specific secrets
|
|
22251
|
+
* map (GHSA fix). Used by the UPDATE no-op re-check so a resolved-plaintext
|
|
22252
|
+
* bag is compared against the expression-bearing stored bag on equal footing.
|
|
22253
|
+
*/
|
|
22254
|
+
redactPropsWith(props, secrets) {
|
|
22255
|
+
if (secrets.size === 0) return props;
|
|
22256
|
+
return redactSecretsForState(props, secrets);
|
|
22257
|
+
}
|
|
22258
|
+
/**
|
|
22259
|
+
* Redact resolved secret plaintext out of rollback-journal operations (GHSA
|
|
22260
|
+
* fix). Each op may carry resolved `properties` / `attemptedProperties` and a
|
|
22261
|
+
* `previousState` snapshot (whose `properties` / `attributes` /
|
|
22262
|
+
* `observedProperties` also hold resolved values). Each op is redacted with
|
|
22263
|
+
* the secrets recorded for ITS OWN resource (`perResourceSecrets`), so a
|
|
22264
|
+
* whole-secret value from one resource cannot rewrite another's literal.
|
|
22265
|
+
* Preserves ops that carry none of those fields, or whose resource recorded
|
|
22266
|
+
* no secret.
|
|
22267
|
+
*/
|
|
22268
|
+
redactOperationsForJournal(operations) {
|
|
22269
|
+
return operations.map((op) => {
|
|
22270
|
+
const secrets = this.perResourceSecrets.get(op.logicalId);
|
|
22271
|
+
if (!secrets || secrets.size === 0) return op;
|
|
22272
|
+
const next = { ...op };
|
|
22273
|
+
if (next.properties) next.properties = redactSecretsForState(next.properties, secrets);
|
|
22274
|
+
if (next.attemptedProperties) next.attemptedProperties = redactSecretsForState(next.attemptedProperties, secrets);
|
|
22275
|
+
if (next.previousState) {
|
|
22276
|
+
const prev = { ...next.previousState };
|
|
22277
|
+
prev.properties = redactSecretsForState(prev.properties, secrets);
|
|
22278
|
+
if (prev.attributes) prev.attributes = redactSecretsForState(prev.attributes, secrets);
|
|
22279
|
+
if (prev.observedProperties) prev.observedProperties = redactSecretsForState(prev.observedProperties, secrets);
|
|
22280
|
+
next.previousState = prev;
|
|
22281
|
+
}
|
|
22282
|
+
return next;
|
|
22283
|
+
});
|
|
22284
|
+
}
|
|
22285
|
+
redactStateForPersist(state) {
|
|
22286
|
+
const resources = {};
|
|
22287
|
+
for (const [logicalId, record] of Object.entries(state.resources)) {
|
|
22288
|
+
const secrets = this.perResourceSecrets.get(logicalId);
|
|
22289
|
+
resources[logicalId] = secrets ? scrubResourceRecord(record, secrets) : record;
|
|
22290
|
+
}
|
|
22291
|
+
return {
|
|
22292
|
+
...state,
|
|
22293
|
+
resources,
|
|
22294
|
+
outputs: this.outputSecrets.size > 0 ? redactSecretsForState(state.outputs, this.outputSecrets) : state.outputs
|
|
21976
22295
|
};
|
|
21977
22296
|
}
|
|
21978
22297
|
/**
|
|
@@ -21981,12 +22300,21 @@ var DeployEngine = class {
|
|
|
21981
22300
|
* constructed with `options.parentStackInfo` (= it's deploying a
|
|
21982
22301
|
* nested-stack child). Returns the state unchanged for top-level
|
|
21983
22302
|
* deploys so the three v6 fields stay absent from non-child state files.
|
|
22303
|
+
*
|
|
22304
|
+
* ALSO the single choke point where resolved SECRET plaintext is redacted out
|
|
22305
|
+
* of the persisted state (GHSA fix): every `stateBackend.saveState` call in
|
|
22306
|
+
* this engine wraps its state through here, so redacting `resources` once here
|
|
22307
|
+
* covers `properties` / `attributes` / `observedProperties` across every
|
|
22308
|
+
* create / update / replacement / rollback / observed-capture path uniformly —
|
|
22309
|
+
* including the async observed-capture drain that runs after the resolver
|
|
22310
|
+
* context is gone (which is why the secret map is session-wide).
|
|
21984
22311
|
*/
|
|
21985
22312
|
withParentInfo(state) {
|
|
21986
|
-
|
|
22313
|
+
const redacted = this.redactStateForPersist(state);
|
|
22314
|
+
if (!this.options.parentStackInfo) return redacted;
|
|
21987
22315
|
const { parentStack, parentLogicalId, parentRegion } = this.options.parentStackInfo;
|
|
21988
22316
|
return {
|
|
21989
|
-
...
|
|
22317
|
+
...redacted,
|
|
21990
22318
|
parentStack,
|
|
21991
22319
|
parentLogicalId,
|
|
21992
22320
|
parentRegion
|
|
@@ -22241,13 +22569,14 @@ var DeployEngine = class {
|
|
|
22241
22569
|
conditions
|
|
22242
22570
|
}, stackName);
|
|
22243
22571
|
diffResolverContext.bestEffort = true;
|
|
22572
|
+
diffResolverContext.skipDynamicReferences = true;
|
|
22244
22573
|
const diffResolveFn = (value) => this.resolver.resolve(value, diffResolverContext);
|
|
22245
22574
|
const changes = await this.diffCalculator.calculateDiff(currentState, effectiveTemplate, diffResolveFn, makeCanonicalizePropertiesFn(this.providerRegistry));
|
|
22246
22575
|
if (!this.diffCalculator.hasChanges(changes)) {
|
|
22247
22576
|
this.logger.info("No changes detected. Stack is up to date.");
|
|
22248
22577
|
let persistedOutputs = currentState.outputs ?? {};
|
|
22249
22578
|
if (!this.options.dryRun) {
|
|
22250
|
-
const resolvedOutputs = await this.resolveOutputs(effectiveTemplate, currentState.resources, stackName, parameterValues, conditions);
|
|
22579
|
+
const resolvedOutputs = this.redactPropsWith(await this.resolveOutputs(effectiveTemplate, currentState.resources, stackName, parameterValues, conditions), this.outputSecrets);
|
|
22251
22580
|
const resolutionFailed = Object.values(resolvedOutputs).some((v) => v === void 0);
|
|
22252
22581
|
const outputsChanged = !resolutionFailed && !outputMapsEqual(persistedOutputs, resolvedOutputs);
|
|
22253
22582
|
if (resolutionFailed && !outputMapsEqual(persistedOutputs, resolvedOutputs)) this.logger.warn("Outputs changed but one or more could not be resolved; keeping the previously persisted outputs. A downstream Fn::ImportValue may fail until the next deploy.");
|
|
@@ -22575,6 +22904,7 @@ var DeployEngine = class {
|
|
|
22575
22904
|
let outputs;
|
|
22576
22905
|
try {
|
|
22577
22906
|
outputs = await this.resolveOutputs(template, newResources, stackName, parameterValues, conditions);
|
|
22907
|
+
outputs = this.redactPropsWith(outputs, this.outputSecrets);
|
|
22578
22908
|
} catch (outputError) {
|
|
22579
22909
|
await this.persistStateAfterOutputFailure(stackName, currentState, newResources, currentEtag, pendingMigration);
|
|
22580
22910
|
await this.writeRollbackJournalSegment(stackName, completedOperations, failedOperations, "no-rollback-failure", currentEtag === void 0);
|
|
@@ -22730,6 +23060,8 @@ var DeployEngine = class {
|
|
|
22730
23060
|
*/
|
|
22731
23061
|
async writeRollbackJournalSegment(stackName, completedOperations, failedOperations, reason, initialDeploy) {
|
|
22732
23062
|
if (completedOperations.length === 0 && failedOperations.length === 0) return;
|
|
23063
|
+
const redactedCompleted = this.redactOperationsForJournal(completedOperations);
|
|
23064
|
+
const redactedFailed = this.redactOperationsForJournal(failedOperations);
|
|
22733
23065
|
try {
|
|
22734
23066
|
const segment = {
|
|
22735
23067
|
...this.options.eventRecorder?.runId !== void 0 && { runId: this.options.eventRecorder.runId },
|
|
@@ -22738,8 +23070,8 @@ var DeployEngine = class {
|
|
|
22738
23070
|
initialDeploy,
|
|
22739
23071
|
...this.options.roleArn && { roleArn: this.options.roleArn },
|
|
22740
23072
|
cdkdVersion: getCdkdVersion(),
|
|
22741
|
-
operations:
|
|
22742
|
-
...
|
|
23073
|
+
operations: redactedCompleted,
|
|
23074
|
+
...redactedFailed.length > 0 && { failedOperations: redactedFailed }
|
|
22743
23075
|
};
|
|
22744
23076
|
await this.stateBackend.appendRollbackJournalSegment(stackName, this.stackRegion, segment);
|
|
22745
23077
|
this.logger.debug(`Rollback journal segment written (${reason})`);
|
|
@@ -22846,10 +23178,29 @@ var DeployEngine = class {
|
|
|
22846
23178
|
recordEvent(event) {
|
|
22847
23179
|
if (!this.options.eventRecorder) return;
|
|
22848
23180
|
try {
|
|
22849
|
-
this.options.eventRecorder.record(event);
|
|
23181
|
+
this.options.eventRecorder.record(this.maskSecretsInEvent(event));
|
|
22850
23182
|
} catch {}
|
|
22851
23183
|
}
|
|
22852
23184
|
/**
|
|
23185
|
+
* Mask any resolved secret value out of an event's human-authored text before
|
|
23186
|
+
* it is persisted to `deployments/*.jsonl` (which outlives `cdkd destroy`)
|
|
23187
|
+
* — GHSA fix. An AWS validation error can quote the offending property value
|
|
23188
|
+
* (`Value '<secret>' at 'X' failed to satisfy ...`), and a provider `reason`
|
|
23189
|
+
* is provider-authored prose; both reach the event store as `error.message` /
|
|
23190
|
+
* `reason`. No-op when the deploy recorded no secrets.
|
|
23191
|
+
*/
|
|
23192
|
+
maskSecretsInEvent(event) {
|
|
23193
|
+
const secrets = event.logicalId ? this.perResourceSecrets.get(event.logicalId) : void 0;
|
|
23194
|
+
if (!secrets || secrets.size === 0) return event;
|
|
23195
|
+
const next = { ...event };
|
|
23196
|
+
if (next.error?.message) next.error = {
|
|
23197
|
+
...next.error,
|
|
23198
|
+
message: maskSecretsInText(next.error.message, secrets)
|
|
23199
|
+
};
|
|
23200
|
+
if (next.reason) next.reason = maskSecretsInText(next.reason, secrets);
|
|
23201
|
+
return next;
|
|
23202
|
+
}
|
|
23203
|
+
/**
|
|
22853
23204
|
* Issue #1002 PR 2 — §7 step 3 post-resolution audit (defense in depth).
|
|
22854
23205
|
* No-op in legacy mode (`options.assetRedirect` unset). In cdkd-assets
|
|
22855
23206
|
* mode, a resolved property still naming a mapped SOURCE (CDK bootstrap)
|
|
@@ -22949,6 +23300,7 @@ var DeployEngine = class {
|
|
|
22949
23300
|
...conditions && { conditions }
|
|
22950
23301
|
}, stackName);
|
|
22951
23302
|
const resolvedProps = await this.resolver.resolve(desiredProps, context);
|
|
23303
|
+
if (context.recordedSecretValues) this.perResourceSecrets.set(logicalId, context.recordedSecretValues);
|
|
22952
23304
|
this.auditResolvedAssetReferences(logicalId, resourceType, resolvedProps);
|
|
22953
23305
|
this.attemptedResolvedProps.set(logicalId, resolvedProps);
|
|
22954
23306
|
const createDecision = this.providerRegistry.getProviderFor({
|
|
@@ -22990,9 +23342,11 @@ var DeployEngine = class {
|
|
|
22990
23342
|
...conditions && { conditions }
|
|
22991
23343
|
}, stackName);
|
|
22992
23344
|
const resolvedProps = await this.resolver.resolve(desiredProps, context);
|
|
23345
|
+
const updateSecrets = context.recordedSecretValues ?? /* @__PURE__ */ new Map();
|
|
23346
|
+
this.perResourceSecrets.set(logicalId, updateSecrets);
|
|
22993
23347
|
this.auditResolvedAssetReferences(logicalId, resourceType, resolvedProps);
|
|
22994
23348
|
this.attemptedResolvedProps.set(logicalId, resolvedProps);
|
|
22995
|
-
if (JSON.stringify(resolvedProps) === JSON.stringify(currentProps)) {
|
|
23349
|
+
if (JSON.stringify(this.redactPropsWith(resolvedProps, updateSecrets)) === JSON.stringify(currentProps)) {
|
|
22996
23350
|
if (change.attributeChanges && change.attributeChanges.length > 0) {
|
|
22997
23351
|
const attrSummary = change.attributeChanges.map((a) => `${a.attribute}: ${a.oldValue ?? "(unset)"} → ${a.newValue ?? "(unset)"}`).join(", ");
|
|
22998
23352
|
this.logger.info(` ↻ ${logicalId} (${resourceType}) attribute update: ${attrSummary}`);
|
|
@@ -23518,6 +23872,7 @@ var DeployEngine = class {
|
|
|
23518
23872
|
outputs[outputKey] = void 0;
|
|
23519
23873
|
}
|
|
23520
23874
|
}
|
|
23875
|
+
if (context.recordedSecretValues) for (const [value, expr] of context.recordedSecretValues) this.outputSecrets.set(value, expr);
|
|
23521
23876
|
return outputs;
|
|
23522
23877
|
}
|
|
23523
23878
|
buildDisplayOutputs(template, resolvedOutputs) {
|
|
@@ -23532,5 +23887,5 @@ var DeployEngine = class {
|
|
|
23532
23887
|
};
|
|
23533
23888
|
|
|
23534
23889
|
//#endregion
|
|
23535
|
-
export { coerceCfnBoolean as $,
|
|
23536
|
-
//# sourceMappingURL=deploy-engine-
|
|
23890
|
+
export { coerceCfnBoolean as $, resolveCaptureObservedState as $t, cyan as A, LocalStartServiceError as An, rewriteTemplateAssetReferences as At, findSilentDropProperties as B, StateError as Bn, buildDockerImage as Bt, makeCanonicalizePropertiesFn as C, AssetError as Cn, shouldRetainResource as Ct, renderStatefulReason as D, DeployCancelledError as Dn, buildAssetRedirectMap as Dt, isStatefulRecreateTargetSync as E, DependencyError as En, WorkGraph as Et, IAMRoleProvider as F, ProvisioningError as Fn, getBootstrapMarkerKey as Ft, IntrinsicFunctionResolver as G, withErrorHandling as Gn, AssetManifestLoader as Gt, slowCcOperationTimeoutMs as H, formatError as Hn, getDockerCmd as Ht, collectInlinePolicyNamesManagedBySiblings as I, ResourceTimeoutError as In, parseBootstrapMarker as It, refStateLookupFromResource as J, isThrottlingError as Jn, synthesisStatusMessage as Jt, cfnRefValueFromPhysicalId as K, isMarkedNonRetryable as Kn, getDockerImageBySourceHash as Kt, clearOnUpdateRemoval as L, ResourceUpdateNotSupportedError as Ln, validateAssetBucketName as Lt, green as M, MissingCdkCliError as Mn, AssetModeResolver as Mt, red as N, NestedStackChildDirectDestroyError as Nn, BOOTSTRAP_MARKER_PREFIX as Nt, formatResourceLine as O, LocalInvokeBuildError as On, createAssetRedirectResolver as Ot, yellow as P, PartialFailureError as Pn, ensureAssetStorage as Pt, assertRegionMatch as Q, resolveAutoAssetStorage as Qt, ProviderRegistry as R, StackHasActiveImportsError as Rn, validateContainerRepoName as Rt, unsupportedFinalSnapshotError as S, setAwsClients as Sn, rebuildClientForBucketRegion as St, MULTI_REGION_RECREATE_BLOCKED_TYPES as T, ConfigError as Tn, stringifyValue as Tt, disableInstanceApiTermination as U, isCdkdError as Un, runDockerForeground as Ut, CloudControlProvider as V, SynthesisError as Vn, formatDockerLoginError as Vt, isTerminationProtectionPropagationError as W, normalizeAwsError as Wn, runDockerStreaming as Wt, normalizeAwsTagsToCfn as X, __exportAll as Xn, getLegacyStateBucketName as Xt, WAFv2WebACLProvider as Y, markNonRetryable as Yn, getDefaultStateBucketName as Yt, resolveExplicitPhysicalId as Z, resolveApp as Zt, buildFinalSnapshotIdentifier as _, clearBucketRegionCache as _n, withRetry as _t, DeploymentEventsStore as a, warnDeprecatedNoPrefixCliFlag as an, requireConfigObject as at, isFinalSnapshotError as b, getAwsClients as bn, LockManager as bt, replayFailedOperations as c, MIGRATE_TMP_PREFIX as cn, scrubResourceRecord as ct, deleteSkipReason as d, expectedOwnerParam as dn, s3BucketDualStackDomainName as dt, resolveSkipPrefix as en, configBooleanRefusal as et, withResourceDeadline as f, PARTITION_TABLE as fn, s3BucketRegionalDomainName as ft, PRE_DELETE_SNAPSHOT_TYPES as g, processStackMessages as gn, describeTypeWithThrottleRetry as gt, ATOMIC_FINAL_SNAPSHOT_TYPES as h, AssemblyReader as hn, DiffCalculator as ht, DeploymentEventsReader as i, stateBucketExistenceConfirmed as in, requireConfigArray as it, gray as j, LockError as jn, escapeRegExp$1 as jt, bold as k, LocalMigrateError as kn, loadPublishableAssetManifest as kt, replayRollback as l, findLargeInlineResources as ln, s3BucketArn as lt, computeImplicitDeleteEdges as m, derivePartitionAndUrlSuffix as mn, applyRoleArnIfSet as mt, DEFAULT_RESOURCE_WARN_AFTER_MS as n, resolveStateBucketWithDefaultAndSource as nn, readConfigString as nt, planFailedOps as o, CFN_TEMPLATE_BODY_LIMIT as on, requireConfigString as ot, IMPLICIT_DELETE_DEPENDENCIES as p, canonicalizeRegion as pn, s3BucketWebsiteUrl as pt, getAccountInfo as q, isRetryableTransientError as qn, Synthesizer as qt, DeployEngine as r, resolveUseCdkBootstrapAssets as rn, replayWarn as rt, planRollback as s, CFN_TEMPLATE_URL_LIMIT as sn, redactSecretsForState as st, DEFAULT_RESOURCE_TIMEOUT_MS as t, resolveStateBucketWithDefault as tn, configStringRefusal as tt, UNSPECIFIED_SKIP_REASON as u, uploadCfnTemplate as un, s3BucketDomainName as ut, ccRoutedFinalSnapshotError as v, resolveBucketRegion as vn, DagBuilder as vt, extractDeploymentEventError as w, CdkdError as wn, AssetPublisher as wt, refusesFinalSnapshot as x, resetAwsClients as xn, S3StateBackend as xt, createPreDeleteFinalSnapshot as y, AwsClients as yn, TemplateParser as yt, findActionableSilentDrops as z, StackTerminationProtectionError as zn, buildDenyExternalAccessPolicy as zt };
|
|
23891
|
+
//# sourceMappingURL=deploy-engine-ISSt01TI.js.map
|