@go-to-k/cdkd 0.283.24 → 0.283.26
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-C9Mq493c.js → asg-provider-CshsD-Pi.js} +2 -2
- package/dist/{asg-provider-C9Mq493c.js.map → asg-provider-CshsD-Pi.js.map} +1 -1
- package/dist/cli.js +334 -18
- package/dist/cli.js.map +1 -1
- package/dist/{deploy-engine-Cu6OO-8t.js → deploy-engine-B6z9CCAL.js} +508 -69
- package/dist/deploy-engine-B6z9CCAL.js.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/dist/deploy-engine-Cu6OO-8t.js.map +0 -1
|
@@ -9821,6 +9821,81 @@ function s3BucketWebsiteUrl(bucketName, region) {
|
|
|
9821
9821
|
/** Fixed marker substituted for a secret value in log / error output. */
|
|
9822
9822
|
const SECRET_MASK = "***";
|
|
9823
9823
|
/**
|
|
9824
|
+
* Every `{{resolve:...}}` expression this process has PROVEN resolves to a
|
|
9825
|
+
* secret, as a SET — uncollapsed by resolved value (issue #1910).
|
|
9826
|
+
*
|
|
9827
|
+
* This is the piece {@link RecordedSecretValues} structurally cannot supply.
|
|
9828
|
+
* That map is keyed by the resolved PLAINTEXT, so when two expressions resolve
|
|
9829
|
+
* to the same value it keeps only the last, and asking it whether the LOSING
|
|
9830
|
+
* expression was a secret answers "no" — for precisely the pair the path-based
|
|
9831
|
+
* redaction exists to separate. The losing leaf then falls through to the value
|
|
9832
|
+
* scan and is persisted holding its SIBLING's expression: a permanent spurious
|
|
9833
|
+
* UPDATE, and on the rollback-journal replay path a re-resolution of the wrong
|
|
9834
|
+
* reference against the live resource.
|
|
9835
|
+
*
|
|
9836
|
+
* EVERY secret expression is recorded, not only the `ssm` ones (issue
|
|
9837
|
+
* [#1916](https://github.com/go-to-k/cdkd/issues/1916)). Only `ssm` needs an
|
|
9838
|
+
* entry to answer "is this secret?" — a `secretsmanager` reference is secret by
|
|
9839
|
+
* SPELLING (see {@link isKnownSecretExpression}), decidable with no lookup and
|
|
9840
|
+
* no memory, while an `ssm` one is secret only when its parameter is a
|
|
9841
|
+
* `SecureString`, knowable only from the `GetParameter` response. But this set
|
|
9842
|
+
* answers a SECOND question: it is the CANDIDATE LIST
|
|
9843
|
+
* {@link positionByIntrinsicSkeleton} matches an intrinsic source leaf against,
|
|
9844
|
+
* and there the losing member of a collapsed
|
|
9845
|
+
* secretsmanager/secretsmanager pair must be nameable too. Holding only the ssm
|
|
9846
|
+
* half made the set's name a lie and left that pair unpositionable.
|
|
9847
|
+
*
|
|
9848
|
+
* One kind is deliberately still absent: an `ssm` reference whose `Type` came
|
|
9849
|
+
* back unclassifiable is treated as secret for THAT resolution but not pinned,
|
|
9850
|
+
* so the next pass re-asks AWS rather than inheriting a transient answer
|
|
9851
|
+
* (issue #1901). Recording it here would pin it for the process.
|
|
9852
|
+
*
|
|
9853
|
+
* It lives in THIS module rather than in the resolver even though the resolver
|
|
9854
|
+
* is its only writer, for two reasons. This module is the LEAF — the resolver
|
|
9855
|
+
* already imports it, so the store is reachable from the writer without adding
|
|
9856
|
+
* an edge, while the reverse (a leaf importing the resolver) would close a
|
|
9857
|
+
* cycle. And the READER is {@link isKnownSecretExpression} right here, so
|
|
9858
|
+
* homing it here means no call site has to thread it: the four sibling writers
|
|
9859
|
+
* #1910 fixes pass a position SOURCE and nothing else.
|
|
9860
|
+
*
|
|
9861
|
+
* Its lifetime matches the resolver's `cachedDynamicReferences` — the resolver
|
|
9862
|
+
* clears both together, and that shared lifetime is the point: this set IS the
|
|
9863
|
+
* resolver's own SecureString verdict store, which already had exactly this
|
|
9864
|
+
* scope, so nothing is widened by homing it here.
|
|
9865
|
+
*
|
|
9866
|
+
* Process-wide is therefore INHERITED, not claimed to be ideal. It is not
|
|
9867
|
+
* strictly sound across regions or accounts in one run — the same expression
|
|
9868
|
+
* can name a `SecureString` in one region and a plain `String` in another — but
|
|
9869
|
+
* `cachedDynamicReferences` caches the resolved VALUE under the same
|
|
9870
|
+
* assumption, so a per-region store here would fix nothing on its own. Note
|
|
9871
|
+
* which way the imprecision points: an entry only ever GRANTS "persist the
|
|
9872
|
+
* source leaf verbatim", and the leaf is the resource's own template
|
|
9873
|
+
* expression, so the failure mode is a public reference stored as an expression
|
|
9874
|
+
* (a spurious UPDATE, issue #1901's class), never a secret stored as plaintext.
|
|
9875
|
+
*/
|
|
9876
|
+
const recordedSecretExpressions$1 = /* @__PURE__ */ new Set();
|
|
9877
|
+
/** Remember that `expression` resolves to a secret. Called by the resolver. */
|
|
9878
|
+
function recordSecretExpression(expression) {
|
|
9879
|
+
recordedSecretExpressions$1.add(expression);
|
|
9880
|
+
}
|
|
9881
|
+
/**
|
|
9882
|
+
* Forget a previously recorded expression — the resolver's `SecureString`
|
|
9883
|
+
* verdict going the other way (an ssm parameter that turns out to be a plain
|
|
9884
|
+
* `String` / `StringList`, i.e. public config that must stay RESOLVED in
|
|
9885
|
+
* state).
|
|
9886
|
+
*/
|
|
9887
|
+
function forgetSecretExpression(expression) {
|
|
9888
|
+
recordedSecretExpressions$1.delete(expression);
|
|
9889
|
+
}
|
|
9890
|
+
/** Has `expression` been PROVEN to resolve to a secret this process? */
|
|
9891
|
+
function isRecordedSecretExpression(expression) {
|
|
9892
|
+
return recordedSecretExpressions$1.has(expression);
|
|
9893
|
+
}
|
|
9894
|
+
/** Drop every remembered verdict. Paired with the resolver's cache reset. */
|
|
9895
|
+
function clearRecordedSecretExpressions() {
|
|
9896
|
+
recordedSecretExpressions$1.clear();
|
|
9897
|
+
}
|
|
9898
|
+
/**
|
|
9824
9899
|
* A resolved secret value shorter than this is NOT used as a redaction needle:
|
|
9825
9900
|
* a 1-2 character plaintext (e.g. a secret whose JSON key holds `"0"`) would
|
|
9826
9901
|
* match incidental characters everywhere and mangle unrelated state. Such a
|
|
@@ -9857,6 +9932,26 @@ const TEMPLATE_SOURCED_READBACK_RULES = {
|
|
|
9857
9932
|
descendArrays: false,
|
|
9858
9933
|
trustAnyExpression: false
|
|
9859
9934
|
};
|
|
9935
|
+
/**
|
|
9936
|
+
* The bag was produced by resolving a STATE source — both relaxations apply.
|
|
9937
|
+
*
|
|
9938
|
+
* The rollback replay is the case (issue #1910): `resolveReplayProps` resolves
|
|
9939
|
+
* the JOURNALED bag and the provider's `effectiveProperties` come back from
|
|
9940
|
+
* that, so the two have identical structure and positional array descent is
|
|
9941
|
+
* sound exactly as it is for a template-derived bag. And the source is a
|
|
9942
|
+
* persisted record, which holds no PUBLIC expressions — a `String` ssm
|
|
9943
|
+
* reference is stored resolved — so any `{{resolve:...}}` in it is by
|
|
9944
|
+
* construction a secret.
|
|
9945
|
+
*
|
|
9946
|
+
* Using `STATE_SOURCED_READBACK_RULES` here instead would be wrong in the
|
|
9947
|
+
* quiet direction: it turns array descent OFF, so a secret nested in a
|
|
9948
|
+
* `Tags[]` / ECS `secrets[]` element falls to the value scan and a colliding
|
|
9949
|
+
* pair still collapses there.
|
|
9950
|
+
*/
|
|
9951
|
+
const STATE_DERIVED_RULES = {
|
|
9952
|
+
descendArrays: true,
|
|
9953
|
+
trustAnyExpression: true
|
|
9954
|
+
};
|
|
9860
9955
|
function isDynamicReferenceString(value) {
|
|
9861
9956
|
return typeof value === "string" && value.includes("{{resolve:");
|
|
9862
9957
|
}
|
|
@@ -9874,14 +9969,16 @@ function isDynamicReferenceString(value) {
|
|
|
9874
9969
|
* (issue #1901), which is not derivable from the string, so that arm consults
|
|
9875
9970
|
* what the resolver actually recorded.
|
|
9876
9971
|
*
|
|
9877
|
-
*
|
|
9878
|
-
*
|
|
9879
|
-
*
|
|
9880
|
-
* (
|
|
9881
|
-
*
|
|
9972
|
+
* `secretExpressions` is what closes the ssm/ssm case (issue #1910). Derived
|
|
9973
|
+
* from the value-keyed map it is useless for exactly this question — the map
|
|
9974
|
+
* already collapsed the pair, so the losing expression is absent from
|
|
9975
|
+
* `secrets.values()` — which is why callers pass the resolver's own SET of
|
|
9976
|
+
* secret expressions instead. Callers that pass nothing fall back to the map's
|
|
9977
|
+
* values, i.e. to the pre-#1910 behavior: the pair still collapses, but nothing
|
|
9978
|
+
* leaks (both leaves are redacted, just onto one expression).
|
|
9882
9979
|
*/
|
|
9883
9980
|
function isKnownSecretExpression(expression, secretExpressions) {
|
|
9884
|
-
return expression.startsWith("{{resolve:secretsmanager:") || secretExpressions.has(expression);
|
|
9981
|
+
return expression.startsWith("{{resolve:secretsmanager:") || secretExpressions.has(expression) || isRecordedSecretExpression(expression);
|
|
9885
9982
|
}
|
|
9886
9983
|
/**
|
|
9887
9984
|
* Is this source leaf a SINGLE complete `{{resolve:...}}` token and nothing
|
|
@@ -9901,6 +9998,252 @@ function isPlainObject$2(value) {
|
|
|
9901
9998
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
9902
9999
|
}
|
|
9903
10000
|
/**
|
|
10001
|
+
* Stands in for a source part the skeleton cannot know — an `Fn::Join` element
|
|
10002
|
+
* that is itself an intrinsic, or an `Fn::Sub` `${...}` variable.
|
|
10003
|
+
*
|
|
10004
|
+
* `[^}]*` rather than `.*` because a recorded expression's INNER text never
|
|
10005
|
+
* contains `}`: the resolver matches them with `/\{\{resolve:([^}]+)\}\}/`, so
|
|
10006
|
+
* the first `}` after `{{resolve:` is already the terminator. Excluding it
|
|
10007
|
+
* means a wildcard can never swallow one token's terminator and run into the
|
|
10008
|
+
* next, so a skeleton for ONE reference cannot match a candidate built from a
|
|
10009
|
+
* different one.
|
|
10010
|
+
*/
|
|
10011
|
+
const SKELETON_WILDCARD = "[^}]*";
|
|
10012
|
+
/**
|
|
10013
|
+
* More wildcards than this and the skeleton is REFUSED outright.
|
|
10014
|
+
*
|
|
10015
|
+
* The bound is what actually closes the backtracking class; the collapse below
|
|
10016
|
+
* only closes one ARRANGEMENT of it. A skeleton with N wildcards makes a
|
|
10017
|
+
* FAILING match exponential in N — and failing is the common case, since the
|
|
10018
|
+
* pattern is tried against every recorded expression that is NOT this leaf's.
|
|
10019
|
+
* The collapse merges ADJACENT wildcards, so `${a}${b}${c}` becomes one; but
|
|
10020
|
+
* wildcards separated by a literal cannot merge and backtrack identically.
|
|
10021
|
+
* Measured against a failing candidate: `Fn::Sub '${a}x${b}x…'` at 6 wildcards
|
|
10022
|
+
* 17.7s and at 8 did not finish in two minutes, and a realistic
|
|
10023
|
+
* `Fn::Join['-', 9 x {Ref}]` against a hyphen-rich secret ARN took 855ms per
|
|
10024
|
+
* candidate. So the producer is the wildcard COUNT, not adjacency.
|
|
10025
|
+
*
|
|
10026
|
+
* What refusing costs is worth stating plainly rather than waving away, because
|
|
10027
|
+
* it is the SAME cost this module's residual note describes: a refused leaf
|
|
10028
|
+
* falls to the value scan, and for a colliding pair that means state holds the
|
|
10029
|
+
* SIBLING's reference, which `resolveReplayProps` re-resolves and applies. A
|
|
10030
|
+
* four-wildcard join carrying substantive literals CAN position uniquely, so
|
|
10031
|
+
* the bound does give something up. It is set where it is because the dominant
|
|
10032
|
+
* CDK shape carries exactly ONE wildcard (the secret ARN's `{Ref}`) and two or
|
|
10033
|
+
* three covers an account plus a region — and because the alternative at the
|
|
10034
|
+
* top of that trade is not "slightly better redaction" but a deploy that hangs
|
|
10035
|
+
* after its AWS mutations.
|
|
10036
|
+
*/
|
|
10037
|
+
const MAX_SKELETON_WILDCARDS = 3;
|
|
10038
|
+
/**
|
|
10039
|
+
* A candidate longer than this REFUSES the whole positioning pass.
|
|
10040
|
+
*
|
|
10041
|
+
* The wildcard cap bounds the EXPONENT; this bounds the BASE. Cost at the cap
|
|
10042
|
+
* is polynomial in the candidate's length (~cubic, measured: 200 chars 4.9ms,
|
|
10043
|
+
* 1000 chars 192ms, and an adversarial all-separator 3000-char candidate 4.1s),
|
|
10044
|
+
* and candidate length is template-authored — `[^}]*` never crosses a `}`, so
|
|
10045
|
+
* the whole expression is one backtracking run. A real `{{resolve:...}}` is
|
|
10046
|
+
* 100-250 characters even with a full ARN and a JSON key, so this only excludes
|
|
10047
|
+
* the pathological.
|
|
10048
|
+
*
|
|
10049
|
+
* The whole pass is refused rather than that one candidate being SKIPPED, and
|
|
10050
|
+
* the difference is load-bearing: skipping shrinks the candidate set, so a
|
|
10051
|
+
* second match could go unseen and condition 2's "exactly one" would be decided
|
|
10052
|
+
* over a filtered list — turning a bound meant for speed into a silent
|
|
10053
|
+
* weakening of the fence. Refusing degrades to the value scan like every other
|
|
10054
|
+
* refusal here.
|
|
10055
|
+
*/
|
|
10056
|
+
const MAX_SKELETON_CANDIDATE_LENGTH = 512;
|
|
10057
|
+
/**
|
|
10058
|
+
* Concatenate skeleton segments, dropping an empty one and COLLAPSING a run of
|
|
10059
|
+
* consecutive wildcards into one — then REFUSE (return `undefined`) when more
|
|
10060
|
+
* than {@link MAX_SKELETON_WILDCARDS} survive.
|
|
10061
|
+
*
|
|
10062
|
+
* The collapse is a correctness fix, not tidiness: `[^}]*[^}]*` is semantically
|
|
10063
|
+
* identical to `[^}]*`, but the engine has to try every split of the input
|
|
10064
|
+
* between them. Measured on a ~120-char candidate, before the collapse:
|
|
10065
|
+
* 4 adjacent wildcards 39ms, 5 ~1s, 6 20s, 8 did not finish in two minutes. Two
|
|
10066
|
+
* legal CFn shapes CDK can emit produce ADJACENT ones — an `Fn::Join` with an
|
|
10067
|
+
* EMPTY delimiter and consecutive non-string parts, and an `Fn::Sub` with
|
|
10068
|
+
* adjacent `${a}${b}` variables — and the collapse takes both to a single
|
|
10069
|
+
* wildcard. It does NOT close the class, which is why the cap exists beside it.
|
|
10070
|
+
*
|
|
10071
|
+
* This runs on the state-persist choke point, i.e. AFTER the AWS mutations and
|
|
10072
|
+
* BEFORE state is written, so a hang there strands real resources.
|
|
10073
|
+
*
|
|
10074
|
+
* An empty segment is dropped rather than appended because an empty DELIMITER
|
|
10075
|
+
* would otherwise sit between two wildcards and defeat the collapse. A
|
|
10076
|
+
* non-empty literal between two wildcards is what anchors the match, and those
|
|
10077
|
+
* are left exactly as they are — and counted.
|
|
10078
|
+
*/
|
|
10079
|
+
function joinSkeletonSegments(segments) {
|
|
10080
|
+
const out = [];
|
|
10081
|
+
let wildcards = 0;
|
|
10082
|
+
for (const segment of segments) {
|
|
10083
|
+
if (segment === "") continue;
|
|
10084
|
+
if (segment === SKELETON_WILDCARD) {
|
|
10085
|
+
if (out[out.length - 1] === SKELETON_WILDCARD) continue;
|
|
10086
|
+
wildcards += 1;
|
|
10087
|
+
if (wildcards > MAX_SKELETON_WILDCARDS) return void 0;
|
|
10088
|
+
}
|
|
10089
|
+
out.push(segment);
|
|
10090
|
+
}
|
|
10091
|
+
return out.join("");
|
|
10092
|
+
}
|
|
10093
|
+
/**
|
|
10094
|
+
* Build an anchored pattern describing what a `{{resolve:...}}` expression at
|
|
10095
|
+
* this INTRINSIC source leaf must look like — literal parts kept verbatim,
|
|
10096
|
+
* unknowable parts wildcarded (issue #1916).
|
|
10097
|
+
*
|
|
10098
|
+
* The dominant CDK shape is an `Fn::Join`, because `secret.secretValueFromJson(...)`
|
|
10099
|
+
* renders the secret's ARN as a `Ref` and CDK joins the pieces:
|
|
10100
|
+
*
|
|
10101
|
+
* ```json
|
|
10102
|
+
* {"Fn::Join": ["", ["{{resolve:secretsmanager:my-secret-", {"Ref": "AWS::AccountId"},
|
|
10103
|
+
* ":SecretString:password}}"]]}
|
|
10104
|
+
* ```
|
|
10105
|
+
*
|
|
10106
|
+
* which yields `^\{\{resolve:secretsmanager:my\-secret\-[^}]*:SecretString:password\}\}$`
|
|
10107
|
+
* — enough to tell that leaf's expression from its `:AWSCURRENT`-suffixed
|
|
10108
|
+
* sibling, which is precisely what the value map cannot do once the two
|
|
10109
|
+
* resolve to the same plaintext. `Fn::Sub` has the same shape via its literal
|
|
10110
|
+
* template string.
|
|
10111
|
+
*
|
|
10112
|
+
* Returns `undefined` for any source this cannot describe (a delimiter that is
|
|
10113
|
+
* itself an intrinsic, a non-array `Fn::Join`, a non-string `Fn::Sub`
|
|
10114
|
+
* template, an object that is not a single-key intrinsic at all), and the
|
|
10115
|
+
* caller then falls back to the value scan — i.e. to the pre-#1916 behavior.
|
|
10116
|
+
*
|
|
10117
|
+
* Exported for its TEST only. The reason is now CONVENIENCE rather than
|
|
10118
|
+
* necessity, and the distinction is worth keeping straight: before the wildcard
|
|
10119
|
+
* cap existed, driving the collapse through `redactSecretsForState` did not
|
|
10120
|
+
* fail on a timeout — catastrophic backtracking is SYNCHRONOUS, so it wedged
|
|
10121
|
+
* the vitest worker and the run never ended, which is a worse CI outcome than
|
|
10122
|
+
* the defect and indistinguishable from a slow machine. With the cap, dropping
|
|
10123
|
+
* the collapse turns that same input into a REFUSAL, so the behavior IS
|
|
10124
|
+
* observable through the public entry point. Asserting on the returned
|
|
10125
|
+
* pattern's shape is still the better fence — it names the invariant (how many
|
|
10126
|
+
* unknown spans survived) instead of a downstream consequence — but it is a
|
|
10127
|
+
* choice, not the only option.
|
|
10128
|
+
*/
|
|
10129
|
+
function intrinsicSkeletonPattern(source) {
|
|
10130
|
+
const keys = Object.keys(source);
|
|
10131
|
+
if (keys.length !== 1) return void 0;
|
|
10132
|
+
const key = keys[0];
|
|
10133
|
+
if (key === "Fn::Join") {
|
|
10134
|
+
const args = source[key];
|
|
10135
|
+
if (!Array.isArray(args) || args.length !== 2) return void 0;
|
|
10136
|
+
const [delimiter, parts] = args;
|
|
10137
|
+
if (typeof delimiter !== "string" || !Array.isArray(parts)) return void 0;
|
|
10138
|
+
const separator = escapeRegExp(delimiter);
|
|
10139
|
+
const segments = [];
|
|
10140
|
+
parts.forEach((part, index) => {
|
|
10141
|
+
if (index > 0) segments.push(separator);
|
|
10142
|
+
segments.push(typeof part === "string" ? escapeRegExp(part) : SKELETON_WILDCARD);
|
|
10143
|
+
});
|
|
10144
|
+
const body = joinSkeletonSegments(segments);
|
|
10145
|
+
return body === void 0 ? void 0 : new RegExp(`^${body}$`);
|
|
10146
|
+
}
|
|
10147
|
+
if (key === "Fn::Sub") {
|
|
10148
|
+
const args = source[key];
|
|
10149
|
+
const template = typeof args === "string" ? args : Array.isArray(args) ? args[0] : void 0;
|
|
10150
|
+
if (typeof template !== "string") return void 0;
|
|
10151
|
+
const segments = [];
|
|
10152
|
+
let cursor = 0;
|
|
10153
|
+
const variable = /\$\{([^}]*)\}/g;
|
|
10154
|
+
let hit;
|
|
10155
|
+
while ((hit = variable.exec(template)) !== null) {
|
|
10156
|
+
segments.push(escapeRegExp(template.slice(cursor, hit.index)));
|
|
10157
|
+
const inner = hit[1];
|
|
10158
|
+
segments.push(inner.startsWith("!") ? escapeRegExp(`\${${inner.slice(1)}}`) : SKELETON_WILDCARD);
|
|
10159
|
+
cursor = hit.index + hit[0].length;
|
|
10160
|
+
}
|
|
10161
|
+
segments.push(escapeRegExp(template.slice(cursor)));
|
|
10162
|
+
const body = joinSkeletonSegments(segments);
|
|
10163
|
+
return body === void 0 ? void 0 : new RegExp(`^${body}$`);
|
|
10164
|
+
}
|
|
10165
|
+
}
|
|
10166
|
+
/**
|
|
10167
|
+
* Position a leaf whose SOURCE is an intrinsic OBJECT, by matching the shape of
|
|
10168
|
+
* that intrinsic against the expressions this process recorded as secrets
|
|
10169
|
+
* (issue #1916).
|
|
10170
|
+
*
|
|
10171
|
+
* This is the residual {@link redactByPath} left behind. Its plain-string arm
|
|
10172
|
+
* persists the source leaf VERBATIM, which needs a source string to copy; when
|
|
10173
|
+
* the source leaf is an `Fn::Join` / `Fn::Sub` there is none, so the leaf fell
|
|
10174
|
+
* to the value scan — and the value map is keyed by the resolved PLAINTEXT, so
|
|
10175
|
+
* a colliding pair collapses there exactly as it did before #1904. That is the
|
|
10176
|
+
* DOMINANT CDK shape rather than an edge case: any secret reached through an L2
|
|
10177
|
+
* token renders the secret ARN as a `Ref`, hence an `Fn::Join`.
|
|
10178
|
+
*
|
|
10179
|
+
* Three conditions must ALL hold before an expression is persisted, and each
|
|
10180
|
+
* removes a different way of being wrong:
|
|
10181
|
+
*
|
|
10182
|
+
* 1. The bag leaf's WHOLE value is a recorded secret plaintext. The skeleton
|
|
10183
|
+
* describes ONE complete `{{resolve:...}}` token, so a leaf that merely
|
|
10184
|
+
* EMBEDS a secret (a join with surrounding text) is not this shape at all
|
|
10185
|
+
* and must keep going to the value scan, which rewrites just the substring.
|
|
10186
|
+
* 2. EXACTLY ONE candidate matches. Two matching candidates mean the skeleton
|
|
10187
|
+
* genuinely cannot separate them (`{Ref}` in the position that differs), and
|
|
10188
|
+
* guessing would be the collapse this fix exists to remove, one step over.
|
|
10189
|
+
* 3. The match is not DEMONSTRABLY another value's expression. The pass's own
|
|
10190
|
+
* map holds each surviving expression against the plaintext it resolved to,
|
|
10191
|
+
* so a candidate recorded there under a DIFFERENT plaintext is refused. That
|
|
10192
|
+
* is what fences a bag/source misalignment: a shape-plausible expression is
|
|
10193
|
+
* rejected outright when the pass can see it resolved to something else. A
|
|
10194
|
+
* candidate absent from that map is a collapsed LOSER, which is the case
|
|
10195
|
+
* this whole function exists to serve, so it is accepted.
|
|
10196
|
+
*
|
|
10197
|
+
* Every rejection degrades to the value scan, i.e. to today's behavior, so no
|
|
10198
|
+
* case gets worse than it is without this pass.
|
|
10199
|
+
*
|
|
10200
|
+
* One residual is worth naming rather than leaving to be rediscovered: a single
|
|
10201
|
+
* WRONG candidate can win only when this leaf's own expression is in NEITHER
|
|
10202
|
+
* store — which is exactly an `ssm` reference whose `Type` came back
|
|
10203
|
+
* unclassifiable (deliberately unpinned per #1901) and which then lost the
|
|
10204
|
+
* value collapse. `recordedSecretExpressions` is process-wide, so the winner
|
|
10205
|
+
* could in principle come from another resource, and condition 3 cannot refuse
|
|
10206
|
+
* one the pass never recorded.
|
|
10207
|
+
*
|
|
10208
|
+
* **What that costs is a WRONG REFERENCE in state, not merely a noisy diff.**
|
|
10209
|
+
* Persisting another leaf's expression is the pre-#1904 failure exactly:
|
|
10210
|
+
* `resolveReplayProps` RE-RESOLVES the persisted expression against AWS and
|
|
10211
|
+
* hands the result to `provider.update`, so a rollback replays the wrong secret
|
|
10212
|
+
* — immediately, if the two references already resolve to different values. It
|
|
10213
|
+
* is narrow (unclassifiable-`ssm` leaves only) but it is not cosmetic, and an
|
|
10214
|
+
* earlier draft of this paragraph called it "a spurious UPDATE rather than a
|
|
10215
|
+
* disclosure", which understated it.
|
|
10216
|
+
*
|
|
10217
|
+
* There is deliberately NO {@link isKnownSecretExpression} check here, and the
|
|
10218
|
+
* asymmetry with {@link redactByPath}'s plain-string arm is principled rather
|
|
10219
|
+
* than an oversight. That arm's candidate is the SOURCE LEAF itself — arbitrary
|
|
10220
|
+
* template text, which genuinely can be a PUBLIC ssm reference that must stay
|
|
10221
|
+
* resolved in state (issue #1901), so it has to be tested. Here the candidates
|
|
10222
|
+
* come only from {@link recordedSecretExpressions} and from the values of a
|
|
10223
|
+
* {@link RecordedSecretValues} map, both of which the resolver populates ONLY
|
|
10224
|
+
* on a proven-secret verdict — so the test could never answer `false`, and an
|
|
10225
|
+
* unfalsifiable guard reads as protection while fencing nothing. Widening
|
|
10226
|
+
* either candidate source is what would make it necessary again.
|
|
10227
|
+
*/
|
|
10228
|
+
function positionByIntrinsicSkeleton(bag, source, secrets, secretExpressions) {
|
|
10229
|
+
if (bag === "" || !secrets.has(bag)) return void 0;
|
|
10230
|
+
const pattern = intrinsicSkeletonPattern(source);
|
|
10231
|
+
if (!pattern) return void 0;
|
|
10232
|
+
const CONFLICTING = Symbol("conflicting plaintext");
|
|
10233
|
+
const plaintextOf = /* @__PURE__ */ new Map();
|
|
10234
|
+
for (const [plaintext, expression] of secrets) plaintextOf.set(expression, plaintextOf.has(expression) ? CONFLICTING : plaintext);
|
|
10235
|
+
let matched;
|
|
10236
|
+
for (const candidate of /* @__PURE__ */ new Set([...secretExpressions, ...recordedSecretExpressions$1])) {
|
|
10237
|
+
if (candidate.length > MAX_SKELETON_CANDIDATE_LENGTH) return void 0;
|
|
10238
|
+
if (!pattern.test(candidate)) continue;
|
|
10239
|
+
const recordedPlaintext = plaintextOf.get(candidate);
|
|
10240
|
+
if (recordedPlaintext !== void 0 && recordedPlaintext !== bag) continue;
|
|
10241
|
+
if (matched !== void 0) return void 0;
|
|
10242
|
+
matched = candidate;
|
|
10243
|
+
}
|
|
10244
|
+
return matched;
|
|
10245
|
+
}
|
|
10246
|
+
/**
|
|
9904
10247
|
* PATH-based redaction: walk `bag` alongside a SOURCE bag that still carries the
|
|
9905
10248
|
* unresolved `{{resolve:...}}` expressions, and wherever the source leaf is such
|
|
9906
10249
|
* a string, persist THAT string verbatim.
|
|
@@ -9919,17 +10262,29 @@ function isPlainObject$2(value) {
|
|
|
9919
10262
|
* record — which already holds the expressions — redacts it with no secret
|
|
9920
10263
|
* fetch and no value matching.
|
|
9921
10264
|
*
|
|
9922
|
-
*
|
|
9923
|
-
*
|
|
9924
|
-
*
|
|
9925
|
-
*
|
|
9926
|
-
*
|
|
10265
|
+
* A source leaf that is an intrinsic OBJECT (`Fn::Join` / `Fn::Sub`) has no
|
|
10266
|
+
* string to copy, so it goes through {@link positionByIntrinsicSkeleton} first
|
|
10267
|
+
* (issue #1916): when the intrinsic's literal parts describe exactly one of the
|
|
10268
|
+
* recorded secret expressions, THAT is persisted. This is the dominant CDK
|
|
10269
|
+
* shape — an L2 secret token renders the ARN as a `Ref`, hence a join.
|
|
10270
|
+
*
|
|
10271
|
+
* The value scan is still applied wherever neither can answer: a leaf that
|
|
10272
|
+
* merely EMBEDS a secret inside surrounding text, an intrinsic whose skeleton
|
|
10273
|
+
* matches zero or several candidates, a diverged shape, a key the source lacks.
|
|
10274
|
+
* So the passes are complementary rather than alternatives — path where
|
|
10275
|
+
* position is knowable, skeleton where the position is an intrinsic, value
|
|
10276
|
+
* where neither is.
|
|
9927
10277
|
*/
|
|
9928
10278
|
function redactByPath(bag, source, secrets, rules, secretExpressions) {
|
|
10279
|
+
if (typeof bag === "string" && isSingleDynamicReferenceToken(bag)) return bag;
|
|
9929
10280
|
if (isDynamicReferenceString(source) && typeof bag === "string") {
|
|
9930
10281
|
if (isSingleDynamicReferenceToken(source) && (rules.trustAnyExpression || isKnownSecretExpression(source, secretExpressions))) return source;
|
|
9931
10282
|
return redactSecretsForState(bag, secrets);
|
|
9932
10283
|
}
|
|
10284
|
+
if (typeof bag === "string" && isPlainObject$2(source)) {
|
|
10285
|
+
const positioned = positionByIntrinsicSkeleton(bag, source, secrets, secretExpressions);
|
|
10286
|
+
if (positioned !== void 0) return positioned;
|
|
10287
|
+
}
|
|
9933
10288
|
if (rules.descendArrays && Array.isArray(bag) && Array.isArray(source) && bag.length === source.length) return bag.map((item, i) => redactByPath(item, source[i], secrets, rules, secretExpressions));
|
|
9934
10289
|
if (isPlainObject$2(bag) && isPlainObject$2(source)) {
|
|
9935
10290
|
const out = {};
|
|
@@ -11591,28 +11946,51 @@ const cachedAvailabilityZones = {};
|
|
|
11591
11946
|
*/
|
|
11592
11947
|
const cachedDynamicReferences = {};
|
|
11593
11948
|
/**
|
|
11594
|
-
* The `{{resolve
|
|
11595
|
-
*
|
|
11596
|
-
*
|
|
11597
|
-
*
|
|
11598
|
-
*
|
|
11599
|
-
*
|
|
11600
|
-
*
|
|
11601
|
-
*
|
|
11602
|
-
*
|
|
11603
|
-
*
|
|
11604
|
-
*
|
|
11605
|
-
*
|
|
11606
|
-
*
|
|
11607
|
-
*
|
|
11608
|
-
*
|
|
11609
|
-
*
|
|
11610
|
-
*
|
|
11611
|
-
* Only the TYPE is remembered, never the decrypted value:
|
|
11612
|
-
* lookup is made with `WithDecryption: false`, so the
|
|
11613
|
-
* fetched at all there.
|
|
11614
|
-
|
|
11615
|
-
|
|
11949
|
+
* The `{{resolve:...}}` expressions this process has PROVEN resolve to a
|
|
11950
|
+
* SECRET, reached through `secret-redaction.ts`'s `recordedSecretExpressions`
|
|
11951
|
+
* store. Lifetime is {@link cachedDynamicReferences}'s — `resetAccountInfoCache`
|
|
11952
|
+
* clears both, so a stale verdict can never decide secret-ness for a reference
|
|
11953
|
+
* whose resolved value that call just asked to forget.
|
|
11954
|
+
*
|
|
11955
|
+
* `ssm` is the kind that NEEDS the memory (issue #1901). A plain `ssm`
|
|
11956
|
+
* reference is not a secret by SPELLING the way `secretsmanager` is — whether
|
|
11957
|
+
* it resolves to public config or to a decrypted secret depends on the
|
|
11958
|
+
* parameter's `Type`, which is only knowable from the `GetParameter` response.
|
|
11959
|
+
* So secret-ness is discovered on the first resolution and remembered, keyed by
|
|
11960
|
+
* the full `{{resolve:...}}` expression. Two consumers need it AFTER the lookup
|
|
11961
|
+
* that populated it: the cache-hit arm (which must re-record the value as a
|
|
11962
|
+
* secret for the current resolution pass) and the diff / no-op path (which must
|
|
11963
|
+
* leave a SecureString reference unresolved without paying a lookup at all once
|
|
11964
|
+
* the type is known). A reference NOT in the set is only "not known to be
|
|
11965
|
+
* secure" — never "proven public" — so every arm that would leak still asks AWS
|
|
11966
|
+
* for the type first. Only the TYPE is remembered, never the decrypted value:
|
|
11967
|
+
* on the diff path the lookup is made with `WithDecryption: false`, so the
|
|
11968
|
+
* plaintext is never fetched at all there.
|
|
11969
|
+
*
|
|
11970
|
+
* A `secretsmanager` reference is recorded TOO, and that is issue
|
|
11971
|
+
* [#1916](https://github.com/go-to-k/cdkd/issues/1916). It needed no memory
|
|
11972
|
+
* while the only question asked of the set was "is this expression secret?",
|
|
11973
|
+
* which its spelling settles — but the set is ALSO the candidate list the
|
|
11974
|
+
* redaction path matches an INTRINSIC source leaf against
|
|
11975
|
+
* ({@link secret-redaction.redactSecretsForState}), and a list holding only the
|
|
11976
|
+
* ssm half cannot name the losing member of a collapsed
|
|
11977
|
+
* secretsmanager/secretsmanager pair. Recording every kind is what makes the
|
|
11978
|
+
* set mean what its name says. It changes no verdict here: every arm that reads
|
|
11979
|
+
* it for secret-ness already answers `true` on a `secretsmanager` spelling
|
|
11980
|
+
* before consulting it.
|
|
11981
|
+
*
|
|
11982
|
+
* The store lives in `secret-redaction.ts` rather than here (issue
|
|
11983
|
+
* [#1910](https://github.com/go-to-k/cdkd/issues/1910)) because the redaction
|
|
11984
|
+
* path is the other consumer: one set in the LEAF module means the redactor can
|
|
11985
|
+
* answer with no caller threading it, and the resolver reaches it along an
|
|
11986
|
+
* import edge it already has — the reverse would close a cycle.
|
|
11987
|
+
*/
|
|
11988
|
+
const recordedSecretExpressions = {
|
|
11989
|
+
has: (expression) => isRecordedSecretExpression(expression),
|
|
11990
|
+
add: (expression) => recordSecretExpression(expression),
|
|
11991
|
+
delete: (expression) => forgetSecretExpression(expression),
|
|
11992
|
+
clear: () => clearRecordedSecretExpressions()
|
|
11993
|
+
};
|
|
11616
11994
|
/**
|
|
11617
11995
|
* Cache for EC2 instance attributes that require a live DescribeInstances
|
|
11618
11996
|
* lookup (PrivateIp / PublicIp / PrivateDnsName / PublicDnsName /
|
|
@@ -13424,7 +13802,7 @@ var IntrinsicFunctionResolver = class {
|
|
|
13424
13802
|
});
|
|
13425
13803
|
for (const { fullMatch, inner } of matches) {
|
|
13426
13804
|
const service = inner.split(":")[0];
|
|
13427
|
-
const isKnownSecret = service === "secretsmanager" ||
|
|
13805
|
+
const isKnownSecret = service === "secretsmanager" || recordedSecretExpressions.has(fullMatch);
|
|
13428
13806
|
if (isKnownSecret && context?.skipDynamicReferences) continue;
|
|
13429
13807
|
if (fullMatch in cachedDynamicReferences) {
|
|
13430
13808
|
const cached = cachedDynamicReferences[fullMatch];
|
|
@@ -13439,8 +13817,8 @@ var IntrinsicFunctionResolver = class {
|
|
|
13439
13817
|
else if (service === "ssm") {
|
|
13440
13818
|
const decrypt = context?.skipDynamicReferences !== true;
|
|
13441
13819
|
const param = await this.resolveSSMReference(parts, decrypt);
|
|
13442
|
-
if (param.type === "SecureString")
|
|
13443
|
-
else if (!param.secure)
|
|
13820
|
+
if (param.type === "SecureString") recordedSecretExpressions.add(fullMatch);
|
|
13821
|
+
else if (!param.secure) recordedSecretExpressions.delete(fullMatch);
|
|
13444
13822
|
isSecret = param.secure;
|
|
13445
13823
|
if (param.secure) {
|
|
13446
13824
|
if (!decrypt) continue;
|
|
@@ -13451,7 +13829,10 @@ var IntrinsicFunctionResolver = class {
|
|
|
13451
13829
|
continue;
|
|
13452
13830
|
}
|
|
13453
13831
|
cachedDynamicReferences[fullMatch] = resolved;
|
|
13454
|
-
if (isSecret && resolved)
|
|
13832
|
+
if (isSecret && resolved) {
|
|
13833
|
+
context?.recordedSecretValues?.set(resolved, fullMatch);
|
|
13834
|
+
if (service === "secretsmanager") recordedSecretExpressions.add(fullMatch);
|
|
13835
|
+
}
|
|
13455
13836
|
result = result.replace(fullMatch, () => resolved);
|
|
13456
13837
|
}
|
|
13457
13838
|
return result;
|
|
@@ -14506,7 +14887,7 @@ var CloudControlProvider = class {
|
|
|
14506
14887
|
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);
|
|
14507
14888
|
if (context?.removeProtection === true && resourceType === "AWS::AutoScaling::AutoScalingGroup") {
|
|
14508
14889
|
this.logger.debug(`Delegating protected AutoScalingGroup ${logicalId} delete to the SDK ASGProvider (Cloud Control cannot force-delete a protected ASG)`);
|
|
14509
|
-
const { ASGProvider } = await import("./asg-provider-
|
|
14890
|
+
const { ASGProvider } = await import("./asg-provider-CshsD-Pi.js").then((n) => n.n);
|
|
14510
14891
|
return await new ASGProvider().delete(logicalId, physicalId, resourceType, _properties, context);
|
|
14511
14892
|
}
|
|
14512
14893
|
const isProtectedEc2Instance = context?.removeProtection === true && resourceType === "AWS::EC2::Instance";
|
|
@@ -21127,10 +21508,35 @@ async function resolveReplayProps(props, resolver, secrets) {
|
|
|
21127
21508
|
* (GHSA fix). The record's `properties` may be the provider's
|
|
21128
21509
|
* `effectiveProperties`, which can echo the value we just resolved for the
|
|
21129
21510
|
* provider call — so scrub it with the same per-op secrets map before it is
|
|
21130
|
-
* persisted.
|
|
21131
|
-
|
|
21132
|
-
|
|
21133
|
-
|
|
21511
|
+
* persisted.
|
|
21512
|
+
*
|
|
21513
|
+
* `journaledProps` is the POSITION source (issue #1910): the JOURNALED previous
|
|
21514
|
+
* properties, whose leaves still carry the unresolved `{{resolve:...}}`
|
|
21515
|
+
* expressions this replay resolved FROM. Without it two expressions sharing one
|
|
21516
|
+
* resolved value collapse onto whichever the replay recorded last, so the state
|
|
21517
|
+
* this rollback writes disagrees with the template at one leaf and the next
|
|
21518
|
+
* deploy reports a change that never converges — the same defect the four
|
|
21519
|
+
* deploy-side writers had, arriving here through the replay instead.
|
|
21520
|
+
*
|
|
21521
|
+
* It takes `STATE_DERIVED_RULES`, which is BOTH relaxations. The source is a
|
|
21522
|
+
* persisted record, so it holds no PUBLIC expressions (a `String` ssm reference
|
|
21523
|
+
* is stored resolved) and any `{{resolve:...}}` in it is by construction a
|
|
21524
|
+
* secret — that is `trustAnyExpression`. And the bag WAS produced by resolving
|
|
21525
|
+
* that source (`resolveReplayProps` -> the provider's `effectiveProperties`),
|
|
21526
|
+
* so the two have identical structure and positional array descent is sound —
|
|
21527
|
+
* that is `descendArrays`. Using `STATE_SOURCED_READBACK_RULES` here reads
|
|
21528
|
+
* plausible and is wrong in the quiet direction: it turns array descent off, so
|
|
21529
|
+
* a secret nested in a `Tags[]` / ECS `secrets[]` element would still collapse
|
|
21530
|
+
* on the replay path.
|
|
21531
|
+
*
|
|
21532
|
+
* No-op when the op resolved no secret.
|
|
21533
|
+
*/
|
|
21534
|
+
function redactRollbackRecord(record, secrets, journaledProps) {
|
|
21535
|
+
if (secrets.size === 0) return record;
|
|
21536
|
+
return scrubResourceRecord(journaledProps === void 0 ? record : {
|
|
21537
|
+
...record,
|
|
21538
|
+
properties: redactSecretsForState(record.properties, secrets, journaledProps, STATE_DERIVED_RULES)
|
|
21539
|
+
}, secrets);
|
|
21134
21540
|
}
|
|
21135
21541
|
async function updateWithRollbackRetry(provider, args, logicalId, logger, isInterrupted) {
|
|
21136
21542
|
if (provider.disableOuterRetry) return await provider.update(...args);
|
|
@@ -21376,7 +21782,7 @@ async function replaySingle(op, stateResources, stackName, ctx, orphanLogicalIds
|
|
|
21376
21782
|
physicalId: createResult.physicalId,
|
|
21377
21783
|
attributes: createResult.attributes ?? {},
|
|
21378
21784
|
properties: recordedPropertiesAfterReplayCreate(prevRecord, createResult)
|
|
21379
|
-
}, secrets);
|
|
21785
|
+
}, secrets, prevRecord.properties);
|
|
21380
21786
|
await afterOp?.(op.logicalId);
|
|
21381
21787
|
if (!deletedNewFirst && !adoptedLiveNewResource) try {
|
|
21382
21788
|
const finalSnapshotIdentifier = rollbackFinalSnapshotId(op.resourceType, current, op.provisionedBy);
|
|
@@ -21427,7 +21833,7 @@ async function replaySingle(op, stateResources, stackName, ctx, orphanLogicalIds
|
|
|
21427
21833
|
desiredProps ?? {},
|
|
21428
21834
|
currentProps ?? {}
|
|
21429
21835
|
], op.logicalId, logger, isInterrupted);
|
|
21430
|
-
stateResources[op.logicalId] = redactRollbackRecord(recordAfterRollbackUpdate(previousState, revertResult), secrets);
|
|
21836
|
+
stateResources[op.logicalId] = redactRollbackRecord(recordAfterRollbackUpdate(previousState, revertResult), secrets, previousState.properties);
|
|
21431
21837
|
logger.info(` Rollback: ${op.logicalId} restored successfully`);
|
|
21432
21838
|
await afterOp?.(op.logicalId);
|
|
21433
21839
|
ctx.recordEvent?.({
|
|
@@ -21567,7 +21973,7 @@ async function replayFailedOperations(failedOps, stateResources, stackName, ctx,
|
|
|
21567
21973
|
desiredProps ?? {},
|
|
21568
21974
|
attemptedProps ?? {}
|
|
21569
21975
|
], op.logicalId, logger, options.isInterrupted);
|
|
21570
|
-
stateResources[op.logicalId] = redactRollbackRecord(recordAfterRollbackUpdate(prev, revertFailedResult), secrets);
|
|
21976
|
+
stateResources[op.logicalId] = redactRollbackRecord(recordAfterRollbackUpdate(prev, revertFailedResult), secrets, prev.properties);
|
|
21571
21977
|
logger.info(` Rollback: ${op.logicalId} reverted successfully`);
|
|
21572
21978
|
await options.afterOp?.(op.logicalId);
|
|
21573
21979
|
ctx.recordEvent?.({
|
|
@@ -21693,7 +22099,7 @@ const FLUSH_INTERVAL_MS = 2e3;
|
|
|
21693
22099
|
const FLUSH_EVENT_THRESHOLD = 50;
|
|
21694
22100
|
/** Build-time cdkd version, with a dev fallback for non-built contexts. */
|
|
21695
22101
|
function getCdkdVersion() {
|
|
21696
|
-
return "0.283.
|
|
22102
|
+
return "0.283.26";
|
|
21697
22103
|
}
|
|
21698
22104
|
/**
|
|
21699
22105
|
* Generate a time-sortable unique run id, e.g.
|
|
@@ -22325,6 +22731,15 @@ var DeployEngine = class {
|
|
|
22325
22731
|
*/
|
|
22326
22732
|
outputSecrets = /* @__PURE__ */ new Map();
|
|
22327
22733
|
/**
|
|
22734
|
+
* UNRESOLVED template `Outputs` values, keyed by output name (issue #1910) —
|
|
22735
|
+
* the outputs' POSITION source, the sibling of `perResourceTemplateProps` for
|
|
22736
|
+
* the bag `resolveOutputs` produces. Without it two outputs resolving one
|
|
22737
|
+
* secret collapse onto whichever expression was recorded last, exactly as two
|
|
22738
|
+
* resource properties did before #1904. Captured in `resolveOutputs` at the
|
|
22739
|
+
* same point `outputSecrets` is accumulated. Reset per `deploy()`.
|
|
22740
|
+
*/
|
|
22741
|
+
outputsTemplateSource = {};
|
|
22742
|
+
/**
|
|
22328
22743
|
* Per-logical-id snapshot of the intrinsic-RESOLVED desired properties
|
|
22329
22744
|
* each CREATE / UPDATE attempted (issue #1198). Written just before the
|
|
22330
22745
|
* provider call; read only when the op FAILS, to journal the failed op's
|
|
@@ -22368,6 +22783,7 @@ var DeployEngine = class {
|
|
|
22368
22783
|
this.perResourceSecrets = /* @__PURE__ */ new Map();
|
|
22369
22784
|
this.perResourceTemplateProps = /* @__PURE__ */ new Map();
|
|
22370
22785
|
this.outputSecrets = /* @__PURE__ */ new Map();
|
|
22786
|
+
this.outputsTemplateSource = {};
|
|
22371
22787
|
this.resolver.resetPhysicalIdFallbackCount();
|
|
22372
22788
|
return withStackName(stackName, () => this.doDeploy(stackName, template));
|
|
22373
22789
|
}
|
|
@@ -22399,13 +22815,19 @@ var DeployEngine = class {
|
|
|
22399
22815
|
* un-redacted resolved bag; only the persisted copy is rewritten.
|
|
22400
22816
|
*/
|
|
22401
22817
|
/**
|
|
22402
|
-
*
|
|
22403
|
-
*
|
|
22404
|
-
*
|
|
22818
|
+
* Redact the resolved stack OUTPUTS bag, positioned by the unresolved
|
|
22819
|
+
* template `Outputs` values (issue #1910).
|
|
22820
|
+
*
|
|
22821
|
+
* A single entry point because THREE call sites redact this same bag — the
|
|
22822
|
+
* state-persist choke point, the no-change re-check, and the post-deploy
|
|
22823
|
+
* publish that feeds the exports index / deploy summary — and before this
|
|
22824
|
+
* they each spelled the value-only redaction separately. Two outputs
|
|
22825
|
+
* resolving one secret collapsed onto whichever expression was recorded last
|
|
22826
|
+
* at all three.
|
|
22405
22827
|
*/
|
|
22406
|
-
|
|
22407
|
-
if (
|
|
22408
|
-
return redactSecretsForState(
|
|
22828
|
+
redactOutputs(outputs) {
|
|
22829
|
+
if (this.outputSecrets.size === 0) return outputs;
|
|
22830
|
+
return redactSecretsForState(outputs, this.outputSecrets, this.outputsTemplateSource);
|
|
22409
22831
|
}
|
|
22410
22832
|
/**
|
|
22411
22833
|
* Redact resolved secret plaintext out of rollback-journal operations (GHSA
|
|
@@ -22416,21 +22838,34 @@ var DeployEngine = class {
|
|
|
22416
22838
|
* whole-secret value from one resource cannot rewrite another's literal.
|
|
22417
22839
|
* Preserves ops that carry none of those fields, or whose resource recorded
|
|
22418
22840
|
* no secret.
|
|
22841
|
+
*
|
|
22842
|
+
* The POSITION source matters MORE here than anywhere else (issue #1910).
|
|
22843
|
+
* This journal is not just persisted, it is REPLAYED to AWS by the rollback
|
|
22844
|
+
* executor's `resolveReplayProps`, so a leaf redacted onto a SIBLING's
|
|
22845
|
+
* expression — which is what the value-keyed map does when two expressions
|
|
22846
|
+
* resolve to one value — re-resolves at replay time to the wrong reference.
|
|
22847
|
+
* For two `:AWSCURRENT` / `:AWSPREVIOUS` stages of one secret that is the
|
|
22848
|
+
* wrong VERSION shipped to the live resource the moment the two diverge.
|
|
22849
|
+
*
|
|
22850
|
+
* The two SIDES take different sources, and conflating them would be a fresh
|
|
22851
|
+
* defect rather than a simplification: `properties` / `attemptedProperties`
|
|
22852
|
+
* are this deploy's DESIRED bags, produced by resolving the CURRENT template,
|
|
22853
|
+
* so the template bag positions them. `previousState` is a record read back
|
|
22854
|
+
* from STATE, whose leaves already hold expressions from whenever it was
|
|
22855
|
+
* written — the current template is not its source and may not even have the
|
|
22856
|
+
* same shape — so it is redacted against ITSELF via `scrubResourceRecord`,
|
|
22857
|
+
* the same #1900 fallback an UNCHANGED resource takes.
|
|
22419
22858
|
*/
|
|
22420
22859
|
redactOperationsForJournal(operations) {
|
|
22421
22860
|
return operations.map((op) => {
|
|
22422
22861
|
const secrets = this.perResourceSecrets.get(op.logicalId);
|
|
22423
|
-
|
|
22862
|
+
const templateProps = this.perResourceTemplateProps.get(op.logicalId);
|
|
22863
|
+
if ((!secrets || secrets.size === 0) && !op.previousState) return op;
|
|
22864
|
+
const ownSecrets = secrets ?? /* @__PURE__ */ new Map();
|
|
22424
22865
|
const next = { ...op };
|
|
22425
|
-
if (next.properties) next.properties = redactSecretsForState(next.properties,
|
|
22426
|
-
if (next.attemptedProperties) next.attemptedProperties = redactSecretsForState(next.attemptedProperties,
|
|
22427
|
-
if (next.previousState)
|
|
22428
|
-
const prev = { ...next.previousState };
|
|
22429
|
-
prev.properties = redactSecretsForState(prev.properties, secrets);
|
|
22430
|
-
if (prev.attributes) prev.attributes = redactSecretsForState(prev.attributes, secrets);
|
|
22431
|
-
if (prev.observedProperties) prev.observedProperties = redactSecretsForState(prev.observedProperties, secrets);
|
|
22432
|
-
next.previousState = prev;
|
|
22433
|
-
}
|
|
22866
|
+
if (next.properties) next.properties = redactSecretsForState(next.properties, ownSecrets, templateProps);
|
|
22867
|
+
if (next.attemptedProperties) next.attemptedProperties = redactSecretsForState(next.attemptedProperties, ownSecrets, templateProps);
|
|
22868
|
+
if (next.previousState) next.previousState = scrubResourceRecord(next.previousState, ownSecrets);
|
|
22434
22869
|
return next;
|
|
22435
22870
|
});
|
|
22436
22871
|
}
|
|
@@ -22444,7 +22879,7 @@ var DeployEngine = class {
|
|
|
22444
22879
|
return {
|
|
22445
22880
|
...state,
|
|
22446
22881
|
resources,
|
|
22447
|
-
outputs: this.
|
|
22882
|
+
outputs: this.redactOutputs(state.outputs)
|
|
22448
22883
|
};
|
|
22449
22884
|
}
|
|
22450
22885
|
/**
|
|
@@ -22729,7 +23164,7 @@ var DeployEngine = class {
|
|
|
22729
23164
|
this.logger.info("No changes detected. Stack is up to date.");
|
|
22730
23165
|
let persistedOutputs = currentState.outputs ?? {};
|
|
22731
23166
|
if (!this.options.dryRun) {
|
|
22732
|
-
const resolvedOutputs = this.
|
|
23167
|
+
const resolvedOutputs = this.redactOutputs(await this.resolveOutputs(effectiveTemplate, currentState.resources, stackName, parameterValues, conditions));
|
|
22733
23168
|
const resolutionFailed = Object.values(resolvedOutputs).some((v) => v === void 0);
|
|
22734
23169
|
const outputsChanged = !resolutionFailed && !outputMapsEqual(persistedOutputs, resolvedOutputs);
|
|
22735
23170
|
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.");
|
|
@@ -23057,7 +23492,7 @@ var DeployEngine = class {
|
|
|
23057
23492
|
let outputs;
|
|
23058
23493
|
try {
|
|
23059
23494
|
outputs = await this.resolveOutputs(template, newResources, stackName, parameterValues, conditions);
|
|
23060
|
-
outputs = this.
|
|
23495
|
+
outputs = this.redactOutputs(outputs);
|
|
23061
23496
|
} catch (outputError) {
|
|
23062
23497
|
await this.persistStateAfterOutputFailure(stackName, currentState, newResources, currentEtag, pendingMigration);
|
|
23063
23498
|
await this.writeRollbackJournalSegment(stackName, completedOperations, failedOperations, "no-rollback-failure", currentEtag === void 0);
|
|
@@ -23501,7 +23936,7 @@ var DeployEngine = class {
|
|
|
23501
23936
|
this.perResourceTemplateProps.set(logicalId, desiredProps);
|
|
23502
23937
|
this.auditResolvedAssetReferences(logicalId, resourceType, resolvedProps);
|
|
23503
23938
|
this.attemptedResolvedProps.set(logicalId, resolvedProps);
|
|
23504
|
-
if (JSON.stringify(
|
|
23939
|
+
if (JSON.stringify(redactSecretsForState(resolvedProps, updateSecrets, desiredProps)) === JSON.stringify(currentProps)) {
|
|
23505
23940
|
if (change.attributeChanges && change.attributeChanges.length > 0) {
|
|
23506
23941
|
const attrSummary = change.attributeChanges.map((a) => `${a.attribute}: ${a.oldValue ?? "(unset)"} → ${a.newValue ?? "(unset)"}`).join(", ");
|
|
23507
23942
|
this.logger.info(` ↻ ${logicalId} (${resourceType}) attribute update: ${attrSummary}`);
|
|
@@ -24019,7 +24454,10 @@ var DeployEngine = class {
|
|
|
24019
24454
|
outputs[outputKey] = value;
|
|
24020
24455
|
if (output.Export?.Name) {
|
|
24021
24456
|
const exportName = typeof output.Export.Name === "string" ? output.Export.Name : await this.resolver.resolve(output.Export.Name, context);
|
|
24022
|
-
if (typeof exportName === "string")
|
|
24457
|
+
if (typeof exportName === "string") {
|
|
24458
|
+
outputs[exportName] = value;
|
|
24459
|
+
this.outputsTemplateSource[exportName] = output.Value;
|
|
24460
|
+
}
|
|
24023
24461
|
}
|
|
24024
24462
|
} catch (error) {
|
|
24025
24463
|
if (this.options.strictGetAtt) throw new Error(`Failed to resolve output ${outputKey}: ${error instanceof Error ? error.message : String(error)} (--strict-getatt promotes output resolution failures to deploy errors; drop the flag to warn and skip the output instead)`, { cause: error });
|
|
@@ -24028,6 +24466,7 @@ var DeployEngine = class {
|
|
|
24028
24466
|
}
|
|
24029
24467
|
}
|
|
24030
24468
|
if (context.recordedSecretValues) for (const [value, expr] of context.recordedSecretValues) this.outputSecrets.set(value, expr);
|
|
24469
|
+
for (const [outputKey, output] of Object.entries(template.Outputs)) this.outputsTemplateSource[outputKey] = output.Value;
|
|
24031
24470
|
return outputs;
|
|
24032
24471
|
}
|
|
24033
24472
|
buildDisplayOutputs(template, resolvedOutputs) {
|
|
@@ -24042,5 +24481,5 @@ var DeployEngine = class {
|
|
|
24042
24481
|
};
|
|
24043
24482
|
|
|
24044
24483
|
//#endregion
|
|
24045
|
-
export { coerceCfnBoolean as $,
|
|
24046
|
-
//# sourceMappingURL=deploy-engine-
|
|
24484
|
+
export { coerceCfnBoolean as $, resolveAutoAssetStorage as $t, cyan as A, LocalMigrateError as An, loadPublishableAssetManifest as At, findSilentDropProperties as B, StackTerminationProtectionError as Bn, buildDenyExternalAccessPolicy as Bt, makeCanonicalizePropertiesFn as C, setAwsClients as Cn, rebuildClientForBucketRegion as Ct, renderStatefulReason as D, DependencyError as Dn, WorkGraph as Dt, isStatefulRecreateTargetSync as E, ConfigError as En, stringifyValue as Et, IAMRoleProvider as F, PartialFailureError as Fn, ensureAssetStorage as Ft, IntrinsicFunctionResolver as G, normalizeAwsError as Gn, runDockerStreaming as Gt, slowCcOperationTimeoutMs as H, SynthesisError as Hn, formatDockerLoginError as Ht, collectInlinePolicyNamesManagedBySiblings as I, ProvisioningError as In, getBootstrapMarkerKey as It, refStateLookupFromResource as J, isRetryableTransientError as Jn, Synthesizer as Jt, cfnRefValueFromPhysicalId as K, withErrorHandling as Kn, AssetManifestLoader as Kt, clearOnUpdateRemoval as L, ResourceTimeoutError as Ln, parseBootstrapMarker as Lt, green as M, LockError as Mn, escapeRegExp$1 as Mt, red as N, MissingCdkCliError as Nn, AssetModeResolver as Nt, formatResourceLine as O, DeployCancelledError as On, buildAssetRedirectMap as Ot, yellow as P, NestedStackChildDirectDestroyError as Pn, BOOTSTRAP_MARKER_PREFIX as Pt, assertRegionMatch as Q, resolveApp as Qt, ProviderRegistry as R, ResourceUpdateNotSupportedError as Rn, validateAssetBucketName as Rt, unsupportedFinalSnapshotError as S, resetAwsClients as Sn, S3StateBackend as St, MULTI_REGION_RECREATE_BLOCKED_TYPES as T, CdkdError as Tn, AssetPublisher as Tt, disableInstanceApiTermination as U, formatError as Un, getDockerCmd as Ut, CloudControlProvider as V, StateError as Vn, buildDockerImage as Vt, isTerminationProtectionPropagationError as W, isCdkdError as Wn, runDockerForeground as Wt, normalizeAwsTagsToCfn as X, markNonRetryable as Xn, getDefaultStateBucketName as Xt, WAFv2WebACLProvider as Y, isThrottlingError as Yn, synthesisStatusMessage as Yt, resolveExplicitPhysicalId as Z, __exportAll as Zn, getLegacyStateBucketName as Zt, buildFinalSnapshotIdentifier as _, processStackMessages as _n, describeTypeWithThrottleRetry as _t, DeploymentEventsStore as a, stateBucketExistenceConfirmed as an, requireConfigObject as at, isFinalSnapshotError as b, AwsClients as bn, TemplateParser as bt, replayFailedOperations as c, CFN_TEMPLATE_URL_LIMIT as cn, redactSecretsForState as ct, deleteSkipReason as d, uploadCfnTemplate as dn, s3BucketDomainName as dt, resolveCaptureObservedState as en, configBooleanRefusal as et, withResourceDeadline as f, expectedOwnerParam as fn, s3BucketDualStackDomainName as ft, PRE_DELETE_SNAPSHOT_TYPES as g, AssemblyReader as gn, DiffCalculator as gt, ATOMIC_FINAL_SNAPSHOT_TYPES as h, derivePartitionAndUrlSuffix as hn, applyRoleArnIfSet as ht, DeploymentEventsReader as i, resolveUseCdkBootstrapAssets as in, requireConfigArray as it, gray as j, LocalStartServiceError as jn, rewriteTemplateAssetReferences as jt, bold as k, LocalInvokeBuildError as kn, createAssetRedirectResolver as kt, replayRollback as l, MIGRATE_TMP_PREFIX as ln, scrubResourceRecord as lt, computeImplicitDeleteEdges as m, canonicalizeRegion as mn, s3BucketWebsiteUrl as mt, DEFAULT_RESOURCE_WARN_AFTER_MS as n, resolveStateBucketWithDefault as nn, readConfigString as nt, planFailedOps as o, warnDeprecatedNoPrefixCliFlag as on, requireConfigString as ot, IMPLICIT_DELETE_DEPENDENCIES as p, PARTITION_TABLE as pn, s3BucketRegionalDomainName as pt, getAccountInfo as q, isMarkedNonRetryable as qn, getDockerImageBySourceHash as qt, DeployEngine as r, resolveStateBucketWithDefaultAndSource as rn, replayWarn as rt, planRollback as s, CFN_TEMPLATE_BODY_LIMIT as sn, TEMPLATE_SOURCED_READBACK_RULES as st, DEFAULT_RESOURCE_TIMEOUT_MS as t, resolveSkipPrefix as tn, configStringRefusal as tt, UNSPECIFIED_SKIP_REASON as u, findLargeInlineResources as un, s3BucketArn as ut, ccRoutedFinalSnapshotError as v, clearBucketRegionCache as vn, withRetry as vt, extractDeploymentEventError as w, AssetError as wn, shouldRetainResource as wt, refusesFinalSnapshot as x, getAwsClients as xn, LockManager as xt, createPreDeleteFinalSnapshot as y, resolveBucketRegion as yn, DagBuilder as yt, findActionableSilentDrops as z, StackHasActiveImportsError as zn, validateContainerRepoName as zt };
|
|
24485
|
+
//# sourceMappingURL=deploy-engine-B6z9CCAL.js.map
|