Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude/rules/provider-delete-path.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ Region check: [layout-provisioning.md](layout-provisioning.md). Masking: [provid
- `stackDestroy` — set only by `cdkd destroy` / `cdkd state destroy` and their nested children. `delete()` also runs for a deploy's removal, replacement and rollback, so behavior right only on a destroy (skip instead of drop, a whole-stack `state orphan` remedy) is gated on it (#4596).
- `resolveSecretDerivedPrincipals` — a top-level or cascading DESTROY's vouch that nothing else in the run attached the same inline policy / membership, so a secret-reference principal list may be resolved against its producer regions; a RESOLVED principal without the grant is a skip, never success (the value may have rotated; `RemoveUserFromGroup` SUCCEEDS for a non-member, so membership is read first), unless this delete's own earlier attempt detached it (`retryMemo`, one per resource across the runner's retries) (#4150). A deploy (its nested-stack removal included) or rollback never sets it.

A delete bag CAN carry plaintext and `delete()` has no masker: thread one before logging a property value (#2007).
A delete bag CAN carry plaintext: use `maskSecrets` on any line naming a bag value or the physical id (#2007).

## Outcomes

Expand Down
2 changes: 1 addition & 1 deletion .claude/rules/provider-masking.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ paths:

A provider's `properties` bag arrives RESOLVED, so a `{{resolve:secretsmanager:...}}` scalar is PLAINTEXT in `create()` / `update()`. `CreateContext` / `UpdateContext` extend `SecretMaskingContext` (`maskSecrets?: (text: string) => string`), threaded by `deploy-engine.ts`, `rollback-executor.ts` and `drift --revert`, each from its own resolution bag ([#1932](https://github.com/go-to-k/cdkd/issues/1932)). A provider re-creating inside its own `update()` forwards at most the masker, never `replayingState`.

- **Apply it to any line interpolating a `properties` value.** `ConsoleLogger` masks every line emitted inside `withCurrentResourceSecrets` with the bound bag ([#2177](https://github.com/go-to-k/cdkd/issues/2177)), but that sink sees only the bags bound: no JSON-escaped spelling, and on a `delete()` only the derived names a deploy or destroy binds (#3869). A THROWN message is already masked by `DeployEngine`. One sink set per operation: `createMaskedLogSinks` (`src/provisioning/masked-retry-logger.ts`). A physical name `generateResourceNameWithFallback` REWROTE from a secret matches neither masker arm: add it with `withDerivedNameMasks`. **A derived-name needle list errs toward OVER-masking** (#4141, `idNameSegments`): an exclusion or replacement that only spares an innocent word (a shape test for "AWS-generated", a needle REPLACED rather than added) must never drop a candidate that may be the name.
- **Apply it to any line interpolating a `properties` value.** `ConsoleLogger` masks every line emitted inside `withCurrentResourceSecrets` with the bound bag ([#2177](https://github.com/go-to-k/cdkd/issues/2177)), but that sink sees only the bags bound: no JSON-escaped spelling, and on a `delete()` only the printing bags its caller binds (#3869, #2007). A THROWN message is already masked by `DeployEngine`. One sink set per operation: `createMaskedLogSinks` (`src/provisioning/masked-retry-logger.ts`). A physical name `generateResourceNameWithFallback` REWROTE from a secret matches neither masker arm: add it with `withDerivedNameMasks`. **A derived-name needle list errs toward OVER-masking** (#4141, `idNameSegments`): an exclusion or replacement that only spares an innocent word (a shape test for "AWS-generated", a needle REPLACED rather than added) must never drop a candidate that may be the name.
- **Mask the VALUE before stringifying; the message is a fallback.** A masker matches literal occurrences, and `JSON.stringify` escapes `"`, `\` and newlines out of the finished line. And `maskSecretsInText` scans substrings only at >= `MIN_NEEDLE_LENGTH` (4), so a short secret survives there. Do both: walk the value with `maskDeep` (`src/provisioning/masked-retry-logger.ts`; never hand-roll one), masking every string leaf AND key, then route the message through the masker.
- **`vp run audit:provider-secret-mask:check` (CI) fails a `${JSON.stringify(X)}` interpolated into a message where `X` reaches no masker.** Dataflow-aware; it REFUSES an identity masker: thread the capability or use its `EXEMPT` list. A no-op DECLARED to be the capability is believed, so green proves only that the value reached the declared masker.
- **Read it defensively** (`context?.maskSecrets ?? ((t: string) => t)`); **never cache it on `this`** — providers are SINGLETONS serving concurrent resources.
Expand Down
2 changes: 1 addition & 1 deletion .claude/rules/providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ paths:

# Provider Pattern

`ResourceProvider` (create / update / delete, plus the OPTIONAL `getAttribute?`) is declared in `src/types/resource.ts`. `CreateContext` and `UpdateContext` both extend `SecretMaskingContext`: an optional `maskSecrets?: (text: string) => string` a provider MUST apply to any log line interpolating a RESOLVED property value. `ProviderRegistry` has a plain constructor and NO `getInstance()` — each command builds `new ProviderRegistry()`, and every type is registered in one place, `registerAllProviders()` (`src/provisioning/register-providers.ts`), as `registry.register('AWS::IAM::Role', new IAMRoleProvider())`.
`ResourceProvider` (create / update / delete, plus the OPTIONAL `getAttribute?`) is declared in `src/types/resource.ts`. `CreateContext`, `UpdateContext` and `DeleteContext` extend `SecretMaskingContext`: an optional `maskSecrets?` function a provider MUST apply to any log line interpolating a RESOLVED property value. `ProviderRegistry` has a plain constructor and NO `getInstance()` — each command builds `new ProviderRegistry()`, and every type is registered in one place, `registerAllProviders()` (`src/provisioning/register-providers.ts`), as `registry.register('AWS::IAM::Role', new IAMRoleProvider())`.

## Where the rest lives

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- **A provider's delete is handed the secret masker, so it can tell a name is secret-derived (issue [#2007](https://github.com/go-to-k/cdkd/issues/2007))** -- `src/provisioning/region-check.ts`, `src/types/resource.ts`, `src/deployment/deploy-engine/{delete,replacement,update-replace,update-in-place}.ts`, `src/cli/commands/destroy-runner.ts`, `src/deployment/rollback-executor.ts`, `src/deployment/rollback-executor/{names,replay-scope,replay-revert,replay-reverse-replacement,replay-orphan-delete}.ts`, `src/provisioning/cloud-control-provider.ts` and the custom-resource, S3 Express directory bucket, IAM role / managed policy / user / group, DynamoDB global table, Glue, RDS DB proxy target group, SNS subscription and ACM certificate providers. `DeleteContext` had no `maskSecrets`, so a provider could not tell that a name it printed was derived from a secret. The S3 Express "bucket is not empty" refusal therefore printed `aws s3 rm s3://*** --recursive` once the log masked the name: a command that names no bucket. It now says the name is secret-derived and prints no command. A deploy's replacement delete also ran with no resolved value bound for the logger, so a provider's own delete lines could print one; they are masked now. Every cdkd delete site passes a masker: a replacement over the deploy's resolved secrets and its secret-named records, a rollback over both the re-resolved previous generation and the failed deploy's values (its own failure lines and events included), and `cdkd destroy` or a template-removal delete over the record's secret-derived names. A provider's internal delete during an update or a failed create's cleanup is handed the operation's masker too.
7 changes: 6 additions & 1 deletion docs/_generated/cli-flag-coverage.json
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,7 @@
"route53",
"s3-analytics-inventory",
"s3-cloudfront",
"s3-directory-bucket",
"s3-event-notification",
"s3-fix-forward-orphan",
"s3-lifecycle",
Expand Down Expand Up @@ -474,6 +475,7 @@
"rollback-nested-cross-region-secret",
"rollback-sqs-cooldown",
"s3-analytics-inventory",
"s3-directory-bucket",
"s3-fix-forward-orphan",
"s3-lifecycle",
"s3-retry-own-bucket",
Expand Down Expand Up @@ -1643,6 +1645,7 @@
"remove-protection",
"rollback-sqs-cooldown",
"route53",
"s3-directory-bucket",
"s3-lifecycle",
"s3-retry-own-bucket",
"secret-derived-immutable-names",
Expand Down Expand Up @@ -2363,6 +2366,8 @@
"--wait-time-seconds",
"--watch",
"--ws",
"--zip-file"
"--x-s3",
"--zip-file",
"--zone-names"
]
}
4 changes: 4 additions & 0 deletions docs/_generated/integ-coverage.json
Original file line number Diff line number Diff line change
Expand Up @@ -7054,6 +7054,7 @@
"rollback-failure-injection",
"rollback-nested-cross-region-secret",
"rollback-replacement-retain",
"s3-directory-bucket",
"s3-tables",
"schema-v10-to-v11-migration",
"schema-v5-to-v6-migration",
Expand Down Expand Up @@ -7359,6 +7360,9 @@
"l1",
"literal"
],
"s3-directory-bucket": [
"l1"
],
"s3-tables": [
"l1"
],
Expand Down
Loading
Loading