@go-to-k/cdkd 0.288.5 → 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-C_AHAmgD.js → asg-provider-CC6BMuNK.js} +2 -2
- package/dist/{asg-provider-C_AHAmgD.js.map → asg-provider-CC6BMuNK.js.map} +1 -1
- package/dist/cli.js +2 -2
- package/dist/{deploy-engine-Dsf9bTqH.js → deploy-engine-umIP3xus.js} +774 -36
- package/dist/deploy-engine-umIP3xus.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/{program-CMhpftan.js → program-CxMjCbiG.js} +123 -26
- package/dist/{program-CMhpftan.js.map → program-CxMjCbiG.js.map} +1 -1
- package/dist/{version-BFmq8S23.js → version-4xTzOaA1.js} +2 -2
- package/dist/{version-BFmq8S23.js.map → version-4xTzOaA1.js.map} +1 -1
- package/package.json +1 -1
- package/dist/deploy-engine-Dsf9bTqH.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";
|
|
@@ -12263,7 +12263,7 @@ const TEMPLATE_SOURCED_RULES = {
|
|
|
12263
12263
|
*
|
|
12264
12264
|
* Does NOT fail closed. This is the constant a caller reaches for when it knows
|
|
12265
12265
|
* the SHAPE of its two bags and nothing about where the result lands —
|
|
12266
|
-
* `cdkd drift`'s two writers
|
|
12266
|
+
* `cdkd drift`'s two writers pass it, and so does
|
|
12267
12267
|
* `DeployEngine.redactOperationsForJournal` for the journal's `previousState`
|
|
12268
12268
|
* (issue [#2886](https://github.com/go-to-k/cdkd/issues/2886): a REPLAYED
|
|
12269
12269
|
* baseline must not gain masks a rollback restore then persists). See
|
|
@@ -12285,10 +12285,19 @@ const STATE_SOURCED_READBACK_RULES = {
|
|
|
12285
12285
|
* readback path already had applies unchanged; the only difference is that a
|
|
12286
12286
|
* position this pass cannot certify is written as {@link SECRET_MASK} rather
|
|
12287
12287
|
* than as the DECRYPTED readback. Passed by `cdkd state refresh-observed` and
|
|
12288
|
-
*
|
|
12289
|
-
*
|
|
12290
|
-
*
|
|
12291
|
-
*
|
|
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.
|
|
12292
12301
|
*/
|
|
12293
12302
|
const STATE_SOURCED_BASELINE_RULES = {
|
|
12294
12303
|
descendArrays: false,
|
|
@@ -16589,7 +16598,7 @@ async function fetchCreateOnlyPropertyPaths(resourceType) {
|
|
|
16589
16598
|
if (Array.isArray(createOnly)) for (const path of createOnly) {
|
|
16590
16599
|
if (typeof path !== "string") continue;
|
|
16591
16600
|
if (!path.startsWith("/properties/")) continue;
|
|
16592
|
-
const segments = path.slice(12).split("/").map(unescapeJsonPointerSegment$
|
|
16601
|
+
const segments = path.slice(12).split("/").map(unescapeJsonPointerSegment$2).filter((segment) => segment.length > 0);
|
|
16593
16602
|
if (segments.length > 0) result.push(segments);
|
|
16594
16603
|
}
|
|
16595
16604
|
}
|
|
@@ -16599,7 +16608,7 @@ async function fetchCreateOnlyPropertyPaths(resourceType) {
|
|
|
16599
16608
|
/**
|
|
16600
16609
|
* Unescape an RFC 6901 JSON Pointer segment (`~1` -> `/`, `~0` -> `~`).
|
|
16601
16610
|
*/
|
|
16602
|
-
function unescapeJsonPointerSegment$
|
|
16611
|
+
function unescapeJsonPointerSegment$2(segment) {
|
|
16603
16612
|
return segment.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
16604
16613
|
}
|
|
16605
16614
|
|
|
@@ -22067,16 +22076,106 @@ const REF_RETURNS_ARN_FROM_STATE = /* @__PURE__ */ new Map([
|
|
|
22067
22076
|
* Only non-empty string values qualify — an intrinsic-shaped or empty value is
|
|
22068
22077
|
* skipped so the caller falls back to the raw physical id rather than emitting
|
|
22069
22078
|
* a broken `[object Object]` / `''`.
|
|
22070
|
-
|
|
22071
|
-
|
|
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) {
|
|
22072
22159
|
return (keys) => {
|
|
22160
|
+
let masked;
|
|
22073
22161
|
for (const source of [resource.properties, resource.attributes]) {
|
|
22074
22162
|
if (!source) continue;
|
|
22075
22163
|
for (const key of keys) {
|
|
22076
22164
|
const value = source[key];
|
|
22077
|
-
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
|
+
}
|
|
22078
22176
|
}
|
|
22079
22177
|
}
|
|
22178
|
+
if (masked !== void 0) masked.notify(masked.key);
|
|
22080
22179
|
};
|
|
22081
22180
|
}
|
|
22082
22181
|
/**
|
|
@@ -23825,7 +23924,7 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
23825
23924
|
async resolveRef(logicalId, context) {
|
|
23826
23925
|
const resource = Object.hasOwn(context.resources, logicalId) ? context.resources[logicalId] : void 0;
|
|
23827
23926
|
if (resource) {
|
|
23828
|
-
const refValue = this.resolveRefValue(resource);
|
|
23927
|
+
const refValue = this.resolveRefValue(logicalId, resource, context);
|
|
23829
23928
|
this.logger.debug(`Resolved Ref to resource: ${logicalId} -> ${refValue}`);
|
|
23830
23929
|
return refValue;
|
|
23831
23930
|
}
|
|
@@ -23907,8 +24006,77 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
23907
24006
|
* children, whose `Ref` is an ARN recovered from the provider-recorded ARN
|
|
23908
24007
|
* attribute through the same `stateLookup` seam.
|
|
23909
24008
|
*/
|
|
23910
|
-
resolveRefValue(resource) {
|
|
23911
|
-
|
|
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);
|
|
23912
24080
|
}
|
|
23913
24081
|
/**
|
|
23914
24082
|
* Resolve Fn::GetAtt intrinsic function
|
|
@@ -24013,8 +24181,13 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
24013
24181
|
const declared = context.noEchoAttributeResources?.get(logicalId);
|
|
24014
24182
|
if ((declared === true || declared !== void 0 && declared.has(attributeName)) && context.recordedSecretValues) recordMaskOnlyValuesIn(value, context.recordedSecretValues);
|
|
24015
24183
|
if (context.redactedAttributeReads !== void 0 && carriesSecretMask(value)) {
|
|
24016
|
-
const
|
|
24017
|
-
|
|
24184
|
+
const maskedAttributeName = this.maskSecretsForLog(attributeName, context);
|
|
24185
|
+
this.pushRedactedAttributeRead(context, {
|
|
24186
|
+
kind: "attribute",
|
|
24187
|
+
logicalId,
|
|
24188
|
+
key: maskedAttributeName,
|
|
24189
|
+
display: `${logicalId}.${maskedAttributeName}`
|
|
24190
|
+
});
|
|
24018
24191
|
}
|
|
24019
24192
|
return value;
|
|
24020
24193
|
}
|
|
@@ -24999,7 +25172,10 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
24999
25172
|
}
|
|
25000
25173
|
if (context.redactedAttributeReads !== void 0) {
|
|
25001
25174
|
const loggedOrigin = this.maskSecretsForLog(origin, context);
|
|
25002
|
-
|
|
25175
|
+
this.pushRedactedAttributeRead(context, {
|
|
25176
|
+
kind: "cross-stack",
|
|
25177
|
+
display: loggedOrigin
|
|
25178
|
+
});
|
|
25003
25179
|
}
|
|
25004
25180
|
}
|
|
25005
25181
|
if (!carriesDynamicReference(value)) return value;
|
|
@@ -26434,7 +26610,7 @@ async function fetchTopLevelWriteOnlyProperties(resourceType) {
|
|
|
26434
26610
|
if (Array.isArray(writeOnly)) for (const path of writeOnly) {
|
|
26435
26611
|
if (typeof path !== "string") continue;
|
|
26436
26612
|
const match = /^\/properties\/([^/]+)/.exec(path);
|
|
26437
|
-
if (match?.[1]) result.add(unescapeJsonPointerSegment(match[1]));
|
|
26613
|
+
if (match?.[1]) result.add(unescapeJsonPointerSegment$1(match[1]));
|
|
26438
26614
|
}
|
|
26439
26615
|
}
|
|
26440
26616
|
logger.debug(`Resolved ${result.size} top-level write-only properties for ${resourceType}` + (result.size > 0 ? `: ${[...result].join(", ")}` : ""));
|
|
@@ -26443,6 +26619,128 @@ async function fetchTopLevelWriteOnlyProperties(resourceType) {
|
|
|
26443
26619
|
/**
|
|
26444
26620
|
* Unescape an RFC 6901 JSON Pointer segment (`~1` -> `/`, `~0` -> `~`).
|
|
26445
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` -> `~`). */
|
|
26446
26744
|
function unescapeJsonPointerSegment(segment) {
|
|
26447
26745
|
return segment.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
26448
26746
|
}
|
|
@@ -27124,6 +27422,12 @@ var CloudControlProvider = class {
|
|
|
27124
27422
|
cloudControlClient;
|
|
27125
27423
|
logger = getLogger().child("CloudControlProvider");
|
|
27126
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();
|
|
27127
27431
|
MAX_WAIT_TIME_MS = 9e5;
|
|
27128
27432
|
INITIAL_POLL_INTERVAL_MS = 1e3;
|
|
27129
27433
|
MAX_POLL_INTERVAL_MS = 1e4;
|
|
@@ -27283,7 +27587,7 @@ var CloudControlProvider = class {
|
|
|
27283
27587
|
const indeterminateGuard = await this.confirmDeleteTargetIdentity(logicalId, resourceType, physicalId, context);
|
|
27284
27588
|
if (context?.removeProtection === true && resourceType === "AWS::AutoScaling::AutoScalingGroup") {
|
|
27285
27589
|
this.logger.debug(`Delegating protected AutoScalingGroup ${logicalId} delete to the SDK ASGProvider (Cloud Control cannot force-delete a protected ASG)`);
|
|
27286
|
-
const { ASGProvider } = await import("./asg-provider-
|
|
27590
|
+
const { ASGProvider } = await import("./asg-provider-CC6BMuNK.js").then((n) => n.n);
|
|
27287
27591
|
const asgProvider = new ASGProvider();
|
|
27288
27592
|
return withIndeterminateGuard(await asgProvider.delete(logicalId, physicalId, resourceType, _properties, context), indeterminateGuard);
|
|
27289
27593
|
}
|
|
@@ -27644,10 +27948,18 @@ var CloudControlProvider = class {
|
|
|
27644
27948
|
/**
|
|
27645
27949
|
* Enrich resource attributes with computed values
|
|
27646
27950
|
*
|
|
27647
|
-
* CC API GetResource returns property names that match CloudFormation
|
|
27648
|
-
* Fn::GetAtt attribute names, so all properties are passed through as-is.
|
|
27649
27951
|
* This method adds fallback attributes for edge cases where CC API
|
|
27650
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.
|
|
27651
27963
|
*/
|
|
27652
27964
|
async enrichResourceAttributes(resourceType, physicalId, attributes) {
|
|
27653
27965
|
const enriched = { ...attributes };
|
|
@@ -28100,7 +28412,9 @@ var CloudControlProvider = class {
|
|
|
28100
28412
|
* - With `knownPhysicalId` (from `--resource <id>=<physicalId>` or
|
|
28101
28413
|
* `--resource-mapping`): call `GetResource(TypeName, Identifier)`,
|
|
28102
28414
|
* parse `ResourceModel` (returned as a JSON string by CC API), and
|
|
28103
|
-
* 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.
|
|
28104
28418
|
* - Without `knownPhysicalId`: return `null`. CC API has no efficient
|
|
28105
28419
|
* `aws:cdk:path`-tag lookup — `ListResources` returns identifiers
|
|
28106
28420
|
* only, so tag lookup would require one `GetResource` per resource
|
|
@@ -28112,6 +28426,107 @@ var CloudControlProvider = class {
|
|
|
28112
28426
|
* SDK providers (S3, Lambda, IAM Role, etc.) implement their own
|
|
28113
28427
|
* `import` with tag-based auto-lookup; this fallback only kicks in for
|
|
28114
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.
|
|
28115
28530
|
*/
|
|
28116
28531
|
async import(input) {
|
|
28117
28532
|
if (!input.knownPhysicalId) return null;
|
|
@@ -28120,14 +28535,21 @@ var CloudControlProvider = class {
|
|
|
28120
28535
|
TypeName: input.resourceType,
|
|
28121
28536
|
Identifier: input.knownPhysicalId
|
|
28122
28537
|
}));
|
|
28123
|
-
|
|
28538
|
+
const safeType = displaySafe(input.resourceType, { asciiOnly: true });
|
|
28539
|
+
const safeId = displaySafe(input.knownPhysicalId, { asciiOnly: true });
|
|
28540
|
+
let parsedModel;
|
|
28124
28541
|
const raw = resp.ResourceDescription?.Properties;
|
|
28125
28542
|
if (typeof raw === "string" && raw.length > 0) try {
|
|
28126
28543
|
const parsed = JSON.parse(raw);
|
|
28127
|
-
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
|
+
}
|
|
28128
28549
|
} catch (parseErr) {
|
|
28129
|
-
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.`);
|
|
28130
28551
|
}
|
|
28552
|
+
const attributes = parsedModel === void 0 ? {} : await this.maskUncertifiedModelValues(parsedModel, input.resourceType, input.knownPhysicalId);
|
|
28131
28553
|
return {
|
|
28132
28554
|
physicalId: input.knownPhysicalId,
|
|
28133
28555
|
attributes
|
|
@@ -28137,7 +28559,81 @@ var CloudControlProvider = class {
|
|
|
28137
28559
|
throw error;
|
|
28138
28560
|
}
|
|
28139
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
|
+
}
|
|
28140
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
|
+
}
|
|
28141
28637
|
|
|
28142
28638
|
//#endregion
|
|
28143
28639
|
//#region src/provisioning/interrupt-watch.ts
|
|
@@ -34772,7 +35268,7 @@ async function resolveReplayProps(props, resolvers, secrets, execCtx, logicalId)
|
|
|
34772
35268
|
*/
|
|
34773
35269
|
function refuseMaskedReplayBaseline(props, logicalId) {
|
|
34774
35270
|
if (props === void 0 || !carriesSecretMask(props)) return;
|
|
34775
|
-
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");
|
|
34776
35272
|
}
|
|
34777
35273
|
/**
|
|
34778
35274
|
* The replay's resolvers: the stack's own, plus one pinned sibling per FOREIGN
|
|
@@ -36317,7 +36813,7 @@ function unionCrossStackReads(previous, recorded, identity) {
|
|
|
36317
36813
|
}
|
|
36318
36814
|
return out;
|
|
36319
36815
|
}
|
|
36320
|
-
var DeployEngine = class {
|
|
36816
|
+
var DeployEngine = class DeployEngine {
|
|
36321
36817
|
logger = getLogger().child("DeployEngine");
|
|
36322
36818
|
resolver;
|
|
36323
36819
|
interrupted = false;
|
|
@@ -36586,7 +37082,7 @@ var DeployEngine = class {
|
|
|
36586
37082
|
...this.options.inheritedSecrets && this.options.inheritedSecrets.size > 0 && { inheritedSecrets: this.options.inheritedSecrets },
|
|
36587
37083
|
recordedSecretValues,
|
|
36588
37084
|
noEchoAttributeResources: this.noEchoAttributeResources,
|
|
36589
|
-
redactedAttributeReads:
|
|
37085
|
+
...base.redactedAttributeReads && { redactedAttributeReads: base.redactedAttributeReads }
|
|
36590
37086
|
};
|
|
36591
37087
|
}
|
|
36592
37088
|
/**
|
|
@@ -36740,6 +37236,147 @@ var DeployEngine = class {
|
|
|
36740
37236
|
for (const name of names) recordMaskOnlyValuesIn(attributes[name], secrets, excluded);
|
|
36741
37237
|
}
|
|
36742
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
|
+
/**
|
|
36743
37380
|
* Refuse to provision a resource whose resolution served a REDACTED attribute
|
|
36744
37381
|
* out of a previous deploy's state (issue #2274).
|
|
36745
37382
|
*
|
|
@@ -36757,17 +37394,30 @@ var DeployEngine = class {
|
|
|
36757
37394
|
* class. A loud failure naming the remedy is strictly better than a silent
|
|
36758
37395
|
* wrong write.
|
|
36759
37396
|
*
|
|
36760
|
-
* NARROW BY CONSTRUCTION. The bag is only non-empty when a
|
|
36761
|
-
* 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
|
|
36762
37399
|
* resource whose properties merely happen to contain the string `***` is
|
|
36763
37400
|
* untouched — which is why the check is not "does `resolvedProps` hold the
|
|
36764
37401
|
* mask". And the diff pass does not consult the bag at all, so an untouched
|
|
36765
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.
|
|
36766
37416
|
*/
|
|
36767
37417
|
refuseRedactedAttributeReads(logicalId, resourceType, context) {
|
|
36768
37418
|
const reads = context.redactedAttributeReads;
|
|
36769
37419
|
if (reads === void 0 || reads.length === 0) return;
|
|
36770
|
-
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);
|
|
36771
37421
|
}
|
|
36772
37422
|
redactOperationsForJournal(operations) {
|
|
36773
37423
|
return operations.map((op) => {
|
|
@@ -37899,7 +38549,8 @@ var DeployEngine = class {
|
|
|
37899
38549
|
template,
|
|
37900
38550
|
resources: stateResources,
|
|
37901
38551
|
...parameterValues && { parameters: parameterValues },
|
|
37902
|
-
...conditions && { conditions }
|
|
38552
|
+
...conditions && { conditions },
|
|
38553
|
+
redactedAttributeReads: []
|
|
37903
38554
|
}, stackName);
|
|
37904
38555
|
if (context.recordedSecretValues) this.perResourceSecrets.set(logicalId, context.recordedSecretValues);
|
|
37905
38556
|
const resolvedProps = await this.resolver.resolve(desiredProps, context);
|
|
@@ -37947,7 +38598,8 @@ var DeployEngine = class {
|
|
|
37947
38598
|
template,
|
|
37948
38599
|
resources: stateResources,
|
|
37949
38600
|
...parameterValues && { parameters: parameterValues },
|
|
37950
|
-
...conditions && { conditions }
|
|
38601
|
+
...conditions && { conditions },
|
|
38602
|
+
redactedAttributeReads: []
|
|
37951
38603
|
}, stackName);
|
|
37952
38604
|
const updateSecrets = context.recordedSecretValues ?? /* @__PURE__ */ new Map();
|
|
37953
38605
|
this.perResourceSecrets.set(logicalId, updateSecrets);
|
|
@@ -38784,6 +39436,83 @@ var DeployEngine = class {
|
|
|
38784
39436
|
...parameterValues && { parameters: parameterValues },
|
|
38785
39437
|
...conditions && { conditions }
|
|
38786
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
|
+
};
|
|
38787
39516
|
const outputsPassSecrets = context.recordedSecretValues ?? EMPTY_SECRETS;
|
|
38788
39517
|
const outputsPassInherited = context.inheritedSecrets ?? EMPTY_SECRETS;
|
|
38789
39518
|
const publishedOutputNames = collectPublishedOutputNames(template.Outputs, conditions);
|
|
@@ -38794,8 +39523,14 @@ var DeployEngine = class {
|
|
|
38794
39523
|
this.logger.debug(`Skipping output ${outputKey} — condition ${output.Condition} is false`);
|
|
38795
39524
|
continue;
|
|
38796
39525
|
}
|
|
39526
|
+
const ownReads = [];
|
|
38797
39527
|
try {
|
|
38798
|
-
|
|
39528
|
+
const resolved = await this.resolver.resolve(output.Value, {
|
|
39529
|
+
...context,
|
|
39530
|
+
redactedAttributeReads: ownReads
|
|
39531
|
+
});
|
|
39532
|
+
refuseMaskedOutputReads(outputKey, ownReads);
|
|
39533
|
+
outputs[outputKey] = resolved;
|
|
38799
39534
|
} catch (error) {
|
|
38800
39535
|
this.handleOutputResolutionFailure(error, outputKey, outputs, outputsPassSecrets, outputsPassInherited);
|
|
38801
39536
|
}
|
|
@@ -38806,13 +39541,16 @@ var DeployEngine = class {
|
|
|
38806
39541
|
const value = outputs[outputKey];
|
|
38807
39542
|
if (value === void 0) continue;
|
|
38808
39543
|
const nameSecrets = /* @__PURE__ */ new Map();
|
|
39544
|
+
const nameReads = [];
|
|
38809
39545
|
let exportName;
|
|
38810
39546
|
try {
|
|
38811
39547
|
try {
|
|
38812
39548
|
exportName = typeof output.Export.Name === "string" ? output.Export.Name : await this.resolver.resolve(output.Export.Name, {
|
|
38813
39549
|
...context,
|
|
38814
|
-
recordedSecretValues: nameSecrets
|
|
39550
|
+
recordedSecretValues: nameSecrets,
|
|
39551
|
+
redactedAttributeReads: nameReads
|
|
38815
39552
|
});
|
|
39553
|
+
refuseMaskedOutputReads(outputKey, nameReads);
|
|
38816
39554
|
} finally {
|
|
38817
39555
|
for (const [plaintext, expression] of nameSecrets) context.recordedSecretValues?.set(plaintext, expression);
|
|
38818
39556
|
}
|
|
@@ -38858,4 +39596,4 @@ var DeployEngine = class {
|
|
|
38858
39596
|
|
|
38859
39597
|
//#endregion
|
|
38860
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 };
|
|
38861
|
-
//# sourceMappingURL=deploy-engine-
|
|
39599
|
+
//# sourceMappingURL=deploy-engine-umIP3xus.js.map
|