Each subdirectory is a self-contained CDK fixture + a verify.sh that runs the
full real-AWS loop and cleans up after itself (a trap destroys the stack and
removes the baseline even on failure — a failed run leaves no orphans).
Requires AWS credentials and a bootstrapped account (cdk bootstrap).
cd basic
npm install # fixture's aws-cdk-lib + aws-cdk
bash verify.sh # deploy -> record -> check CLEAN -> inject drift -> check DETECTS -> destroyverify.sh exits non-zero (and prints INTEG FAIL: ...) if any assertion fails;
prints INTEG PASS on success.
All record/revert calls in the scripts pass --yes: since the interactive
prompts landed (R28/R38/R45), a write decision without --yes would refuse
(exit 2) when stdin is not a TTY — and stop to wait for input when it is (R50).
Any check in these scripts (or a manual dogfood run) can double as corpus
recording: CDKRD_CORPUS_DIR=/tmp/corpus bash verify.sh writes one JSON case
per readable resource (pipeline inputs + findings, account ids sanitized).
Review a recording, add a description, and commit it under tests/corpus/ —
the offline replay test then locks that classification in CI forever. Integ
fixtures use fictional names, so their recordings are always safe to commit;
never commit recordings from confidential stacks.
These do NOT run in CI (they need credentials and mutate a real account):
- Before every release (the
/verify-prgate): run EVERY fixture —basic(+verify-deleted-guards.sh+verify-vs-cdk-drift.sh+verify-mutation-matrix.sh),iam,lambda,revert,policies,atdefault,noise,readgap, and the false-positive matrix (dynamodb,sqs,securitygroup,cloudwatch-alarm,stepfunctions,ssm,eventbridge,cognito,cloudfront-oai). - After running ANY fixture(s) — as a safety net:
bash sweep-orphans.sh(dry run; add--deleteto remove). Eachverify.shtears down its own stack, but stack teardown only deletes stack MEMBERS — RETAIN-policy stateful resources (Kinesis/RDS/DynamoDB/EFS), auto-created Lambda/APIGW log groups, Secrets in their recovery window, and KMS keys pending deletion outlive the stack and bill silently.sweep-orphans.shfinds and deletes those, scoped strictly to cdkrd's own integ name tokens (Cdkrd/Cdkdrift/CdkRealDrift) and never touching a resource that is still a member of an active stack. Run it after a release suite — and whenever you suspect a fixture left something behind. - After changing
src/read/**,src/revert/**,src/normalize/**, orsrc/commands/gather.ts: run at leastbasic+revert+noise+ the false-positive matrix (below — each asserts tricky declared values normalize equal to live, never a false declared drift) (andpoliciesifwriters.tschanged;harvest3for the multi-type Cloud Control revert matrix) before merging. - After changing the CC identifier adapters (
CC_IDENTIFIER_ADAPTERSinsrc/read/router.ts) or the revert write-only re-inclusion (writeOnlyReincludeOpsinsrc/revert/plan.ts): runecs-writeonly— it deploys an ECS Service with a managed EBS volume, asserts the service is READ via its composite[ServiceArn, Cluster]identifier (not skipped), then driftsDesiredCountand asserts the revert SUCCEEDS without dropping the write-onlyVolumeConfigurations(R102). - After changing the Tags revert path (
tagPreservingOpsinsrc/revert/plan.ts,awsManagedTagsinsrc/normalize/noise.ts, orliveByLogicalplumbing insrc/commands/gather.ts): runsns-topic-tags-revert— it deploys an SNS Topic (which AWS auto-tags withaws:cloudformation:*), records a baseline, adds a USER tag out of band, then assertsrevertREMOVES the user tag while PRESERVING theaws:*managed tags (AWS rejects any write that drops anaws:-prefixed key, R131). - After changing
src/diff/**orsrc/baseline/**: runbasic/verify-mutation-matrix.sh(the drift-direction matrix) before merging. - After changing nested-undeclared detection (
collectNestedUndeclaredinsrc/diff/classify.ts) or the revert nested guard (src/revert/plan.ts): rundynamodb/verify-nested.sh— it deploys a table whose GSI and PITR config materialize nested undeclared values (R96/R98), asserts theinfo: undeclared-subkey=Nfold +--show-allexpansion live, then mutates the undeclaredRecoveryPeriodInDaysout of band and asserts the drift is detected and reported NOT-revertable (R99). - After changing
KNOWN_DEFAULTS(src/normalize/noise.ts) or theatDefaultfold: runatdefault— it asserts the hand-written default shapes still match live Cloud Control output (a shape mismatch would resurface the value as real undeclared drift) and that a value changed away from its default still surfaces. - After changing the nested-schema-default extraction (
collectDefaultPathsinsrc/schema/schema-strip.ts) or the nestedatDefaultfold (src/diff/classify.ts): runcloudfront-atdefault— it deploys a CloudFront Distribution and asserts its schema-annotated nested defaults (CustomOriginConfig.HTTPSPort/OriginReadTimeout,PriceClass, …) fold asatDefaultrather thanundeclared(R103). CloudFront deploy/destroy are slow. - After changing the fully-undeclared-object descend
(
DESCEND_UNDECLARED_OBJECT_PATHSinsrc/normalize/noise.ts, the descend branch insrc/diff/classify.ts): runathena-descend— it deploys a workgroup declaring NOWorkGroupConfiguration(whole default foldsatDefault), then sets ONE non-default sub-key out of band and asserts cdkrd surfaces ONLY the descendedWorkGroupConfiguration.BytesScannedCutoffPerQuery(not the whole object), the constants still folding (#565). - After changing the Lex BotLocales writer (
writeLexBotLocalesinsrc/revert/writers.ts): runlex-structural— it deploys a Lex V2 bot (two user intents + a custom slot type + the built-in FallbackIntent), then DELETES a whole intent out of band and assertsrevertRECREATES it (CreateIntent), and ADDS a whole intent out of band and assertsrevertDELETES it (DeleteIntent), never touching the auto-managed FallbackIntent (#553 update + #564 structural). - Scripts that share a fixture/stack (
basic's four) must run sequentially, never concurrently. - After changing exit-code or baseline semantics (report-only/--fail, the UNRECORDED contract, prompt flows): re-run EVERY script. They assert exit codes and grep output wording, and have now broken on three such changes (R55, R62, R70) — each found only on the next live run.
One versioned S3 bucket. Asserts:
recordthencheckreports CLEAN (exit 0).- After enabling transfer acceleration out-of-band (an undeclared change CFn
drift would not catch),
checkreports drift (exit 1) and namesAccelerateConfiguration.
A second script in the basic fixture (reuses its bucket) covering the deleted
tier and the revert guards:
- R2 revert guard — with NO baseline,
revert --dry-runreports the undeclared value asNOT revertable(unrecorded, R62) while a declared drift is still in the plan;--remove-unrecordedopts in to removing it. - R1 deleted tier — after deleting the bucket out of band,
checkreports thedeletedtier (exit 1) andrevert --dry-runreports it as not revertable (deleted — recreate via cdk deploy).
cd basic && bash verify-deleted-guards.shEmpirical proof of the README capability table, against cdk drift itself
(reuses the basic stack; needs an aws-cdk with the drift command):
- After an undeclared change (transfer acceleration),
cdk drift --failexits 0 (CFn drift detection cannot see it) whilecdkrd checkexits 1 and namesAccelerateConfiguration— the differentiator, demonstrated. - After a declared change (versioning suspended), BOTH detect it — cdkrd is a superset, not a sidegrade.
If cdk drift ever starts detecting the undeclared change, this test fails —
the signal to re-verify the README comparison-table claims.
False-negative matrix (R64): after a FULL record (snapshot-complete baseline), one bucket is walked through every drift direction the model distinguishes; each must be detected, named, and resolved back to CLEAN:
| # | direction | mutation | must name | resolve |
|---|---|---|---|---|
| M1 | declared-change | versioning suspended | VersioningConfiguration |
revert |
| M2 | undeclared-add | acceleration appears | appeared since record (R62) |
record |
| M3 | undeclared-change | recorded acceleration flips | AccelerateConfiguration |
record |
| M4 | undeclared-add | out-of-band CORS config | CorsConfiguration + appeared since record |
record |
| M5 | value-remove | recorded CORS deleted | baseline value removed since record |
record |
Every mutation ends at CLEAN, so the script also exercises the record delta
loop (R39) each round and the declared revert path once. (M4/M5 use CORS, not
tags: S3 refuses a TagSet replacement that drops the CFn aws:* system tags —
found on the first live run, R68.)
cd basic && bash verify-mutation-matrix.shIAM Role (inject a permissions boundary → undeclared drift) and a Node Lambda
(inject reserved concurrency → undeclared drift); each asserts detect + clean destroy.
The IAM fixture role also carries a CDK-generated sibling AWS::IAM::Policy
(addToPolicy → DefaultPolicy), so the boundary test doubles as a no-false-positive
check for the sibling filter.
A second script in the iam fixture covering the sibling-DefaultPolicy blind
spot end-to-end (an out-of-band inline policy on a role whose grants live in a
sibling AWS::IAM::Policy):
recordthencheckreports CLEAN — the sibling DefaultPolicy entry in the role's livePoliciesis filtered by name, not reported as drift.- After
put-role-policyadds a rogue inline policy out-of-band,checkreportsPoliciesdrift (exit 1) naming ONLY the rogue policy — the sibling entry does not leak into the finding. revert --yesdeletes ONLY the rogue policy (per-nameDeleteRolePolicy, not a whole-property Cloud Control patch): the DefaultPolicy survives with its document intact, andcheckis CLEAN again.
cd iam && bash verify-inline-policy.shA third iam script (its own stack CdkRealDriftIntegIamDeclared, a role that
DECLARES an inline policy) covering the DECLARED-side revert: a rogue inline
policy added out-of-band makes the live Policies a length-2 array vs the
declared length-1, a DECLARED whole-array drift. revert --yes must DELETE only
the rogue and keep the declared policy — which needs the declared revert op to
carry the live value as prior (without it the rogue survived: a silent,
security-relevant incomplete revert). PASS confirms rogue gone + declared policy
intact + check CLEAN.
cd iam && bash verify-declared-inline-revert.shA third iam script proving there is no false-negative when the sibling
AWS::IAM::Policy's OWN document is edited out-of-band. The role's by-name filter
drops the sibling entry from the role's live Policies (so it never double-reports
as undeclared) — the question was whether a change to the sibling's document then
goes unnoticed. It does not:
recordthencheckis CLEAN (sibling filtered on the role; document matches).- After
put-role-policyrewrites the sibling's document out-of-band (s3:ListAllMyBuckets→s3:GetObject),checkreports it as DECLARED drift on theAWS::IAM::Policyresource itself (TestRole/DefaultPolicy.PolicyDocument.Statement.0.Action, exit 1) — the resource's own declared check fires, and the role does NOT leak it as undeclared. revert --yesrestores the declared document (writeIamPolicy), andcheckis CLEAN again.
cd iam && bash verify-sibling-policy-doc.shDISCOVERY-only (no deploy): a CDK app with a top-level stack AND a stack nested
inside a Stage (the CDK Pipelines / multi-env pattern). Asserts cdkrd check
enumerates BOTH TopStack and the staged ProdStage-ApiStack. A staged stack is
absent from cloudAssembly.stacks (top-level assembly only), so synthApp must use
stacksRecursively — without it the staged stack is silently never discovered.
Synthesizes locally and runs check (stacks not deployed → each "skipped"); only the
discovered stack NAMES are asserted. Needs AWS creds for the DescribeStacks probe but
deploys/destroys nothing.
cd stages && npm install && bash verify-stages.shA parent stack with a CDK NestedStack (the child has its own SNS topic). A nested
stack deploys as a separate child CloudFormation stack; cdkrd checks the parent's
AWS::CloudFormation::Stack resource but does NOT recurse into the child, so the
child's resources are unchecked. Asserts check prints a prominent warning: line
naming the nested stack (coverage is never silently incomplete) while the parent
itself reads CLEAN (the warning is on stderr, not counted as drift). Deploys +
destroys a real (tiny) stack.
cd nested && npm install && bash verify-nested.shValidates the R86 atDefault fold end-to-end (a default-config Lambda + a bare
L1 S3 bucket, whose undeclared properties all sit at a known AWS default):
- before any baseline, those values FOLD into the
atDefaulttier — they are counted in theinfo:footer but NOT listed in the report body. This proves the hand-writtenKNOWN_DEFAULTSshapes (especially the S3BucketEncryptionshape withBlockedEncryptionTypes) still match what Cloud Control returns; a mismatch would reclassify the value as real undeclared and surface it in the body. --show-allexpands the fold and lists those same values underAT AWS DEFAULT.- after
record,checkis CLEAN (the at-default values fold, never recorded). - mutating one at-default value away from its default (Lambda
TracingConfigPassThrough→Active) makeschecksurface it as real drift — the fold never blinds cdkrd to an actual change (the equality gate has teeth).
cd atdefault && bash verify.shThe false-NEGATIVE guard for cc-api-strip. A Lambda with one declared env var;
the script adds an out-of-band env var whose KEY is an AWS-managed field NAME
(LastModified). cc-api-strip removes managed field names at any depth, so it
used to strip this user env var too — the out-of-band change was SILENTLY
undetectable. The fix stops stripping inside free-form user maps
(Environment.Variables, Glue Parameters, map-shaped Tags, …). Asserts check
DETECTS the injected Environment.Variables.LastModified (exit 1) — previously a
silent CLEAN. Deploys + destroys a real (tiny) Lambda.
cd freeform-strip && npm install && bash verify-freeform-strip.shThe false-NEGATIVE guard for narrow SDK-override projections. An
AWS::Budgets::Budget declaring CostFilters (its SCOPE — which service it
watches). The Budgets override reader used to project a thin model WITHOUT
CostFilters, so an out-of-band scope change was undetectable (the declared
CostFilters became a benign readGap). The script flips the filter from S3 to
EC2 out of band and asserts check DETECTS the change (Budget.CostFilters.Service,
exit 1) — previously a silent CLEAN. Budgets is global (us-east-1). Deploys +
destroys a (free) budget.
cd budget-scope && npm install && bash verify-budget-scope.shThe same false-NEGATIVE guard for the CodeBuild override projection. A
PipelineProject declaring concurrentBuildLimit. The reader used to omit
ConcurrentBuildLimit, VpcConfig, Visibility, and SourceVersion, so an
out-of-band change was undetectable. The script asserts CLEAN after record (FP
guard: the now-read Visibility=PRIVATE folds to atDefault, VpcConfig is
omitted when unused), then bumps ConcurrentBuildLimit out of band and asserts
check DETECTS it (exit 1). Deploys + destroys a CodeBuild project.
cd codebuild-proj && npm install && bash verify-codebuild-proj.shThe false-positive guard. Deploys resources that DECLARE properties whose live
AWS form is textually different from the template but semantically identical —
exactly what the normalize/ layer subtracts:
- an IAM inline policy with an
aws:SecureTransportCondition key (R69 regression: it must survive the live read, NOT be stripped as anaws:*tag), multi-action statements, and a managed policy attached by name (name↔ARN); - resource Tags AWS augments with
aws:cloudformation:*and may reorder; - an S3 CorsConfiguration (ordered array of rules).
The assertion is the strong one: with NO baseline, check --fail must exit 0 —
there is no declared drift, because every declared value normalizes equal to live.
A normalizer regression turns one of these into a false declared drift and fails.
cd noise && bash verify.shThe honest-gap guard (the other direction). Some declared properties genuinely
cannot be read back — a write-only value is the canonical case. A change to one out
of band IS real drift cdkrd cannot verify; the promise is to say so honestly
(readGap) rather than silently pass it as CLEAN. Deploys a SecretsManager secret
with a literal write-only SecretString and asserts it surfaces in the readGap
tier (reason: write-only), never silently absent; --fail still exits 0 (a
readGap is informational, not drift). The cleanup trap force-deletes the secret.
cd readgap && bash verify.shThe false-NEGATIVE guard (the opposite of noise / the false-positive matrix).
Deploys five types, records a CLEAN baseline, then changes one declared property on
each out of band (SQS VisibilityTimeout, SNS DisplayName, Lambda Timeout, S3
VersioningConfiguration, ECR ImageTagMutability) and asserts check --fail DETECTS
every one. A normalizer that wrongly collapses a real change would make cdkrd miss
it and report CLEAN — this catches that (it also guards the R88 array-sort fixes
against over-suppression). Run after changing src/normalize/** or src/diff/**.
cd mutation-multi && npm install && bash verify.shNine focused fixtures (R88), each the same shape as noise: deploy a resource that
DECLARES a property whose live AWS form is textually different but semantically
equal, then assert check reports no false declared drift. They target the specific
normalization classes most likely to regress:
| fixture | resource | noise-prone declared property (class) |
|---|---|---|
dynamodb |
DynamoDB Table | KeySchema / AttributeDefinitions / GSIs (ordered arrays), tags |
sqs |
SQS Queue + DLQ | RedrivePolicy (object↔JSON-string, R75), numeric attributes |
securitygroup |
EC2 SecurityGroup | ingress/egress rules (unordered arrays of CIDR rule objects) |
cloudwatch-alarm |
CloudWatch Alarm | Dimensions (unordered {Name,Value} array — NOT Key/Id-keyed) |
stepfunctions |
Step Functions SM | DefinitionString (JSON string, R75) + auto-role policy |
ssm |
SSM Document + Param | Document Content (object↔JSON-string, R75) |
eventbridge |
EventBridge Rule | EventPattern (object↔JSON-string), Targets array + queue policy |
cognito |
Cognito Pool/Client | OAuth/ExplicitAuthFlows (unordered enums, R74), UserPoolGroup id (R84) |
cloudfront-oai |
S3 BucketPolicy/OAI | OAI principal (CanonicalUser hex vs cloudfront:user ARN) (R101) |
cd dynamodb && bash verify.sh # …and likewise for each fixture aboveAdded by the /hunt-bugs skill — a periodic real-AWS round that deploys
UNCOVERED, richly-configured resources (prioritizing the high-frequency patterns
many users actually deploy) and asserts a freshly recorded stack reports CLEAN.
Each verify.sh is the FP shape (deploy → record --yes → check --fail MUST
exit 0); lambda-rich/verify-detect.sh adds the false-NEGATIVE half.
| fixture | resource(s) | what it stresses |
|---|---|---|
s3-rich |
S3 Bucket (KMS, lifecycle, CORS, intelligent-tiering, enforceSSL, autoDeleteObjects) | the single most-deployed resource, richly configured |
lambda-rich |
Lambda (arm64, env, tracing, ephemeral, reserved-concurrency, FunctionUrl, logGroup) | common Lambda "production" knobs; verify-detect.sh = console-edit detect+revert |
vpc-common |
ec2.Vpc (subnets / NAT / routes / IGW / S3 endpoint) | dense default-folding + unresolved intrinsics across networking |
rich-fp |
Kinesis, SQS FIFO, SNS FIFO, CloudWatch Dashboard, Secrets Manager | JSON-string bodies, FIFO flags, generated secrets |
notation-fp |
SNS/SQS via L1 + Fn::FindInMap / Fn::Sub map / Fn::If / Fn::Select+Fn::Split |
CloudFormation intrinsic resolution against live values |
niche-fp |
ECR, Step Functions, WAFv2 WebACL (regional), EventBridge Rule | nested rule/visibility configs, JSON LifecyclePolicy/Definition |
ddb-rich |
DynamoDB Table (SIA table-class, TTL, stream, PITR, contributor-insights, LSI + 2 GSI) | rich DynamoDB knobs default-folded at once; verify-detect.sh = nested-property (PITR) detect+revert |
sfn-express |
Step Functions EXPRESS SM (CloudWatch LoggingConfiguration + X-Ray tracing) | EXPRESS logging-destination array + tracing default-folding |
sns-fifo |
SNS FIFO Topic (content-based-dedup + KMS SSE) | FIFO flags + KmsMasterKeyId intrinsic ref on a FIFO topic |
kinesis-rich |
Kinesis Data Stream (PROVISIONED mode, 2 shards, 48h retention, KMS SSE) | StreamModeDetails + non-default RetentionPeriodHours + StreamEncryption key ref default-folding |
secrets-rich |
Secrets Manager Secret (GenerateSecretString, customer KMS key, description) |
minted SecretString is opaque/NoEcho on read (a write-only readGap) + KmsKeyId intrinsic ref |
ecr-rich |
ECR Repository (scan-on-push, IMMUTABLE tags, KMS encryption, lifecycle policy) | JSON LifecyclePolicy re-serialization edge; verify-detect.sh = ImageTagMutability enum detect+revert |
cd s3-rich && npm install && bash verify.sh # …and likewise for each fixture above
cd lambda-rich && npm install && bash verify-detect.sh # the detect+revert half
cd ddb-rich && npm install && bash verify-detect.sh # nested-property (PITR) detect+revert
cd ecr-rich && npm install && bash verify-detect.sh # ImageTagMutability enum detect+revertCleanup after any of these is mandatory and gate-enforced. /hunt-bugs arms a
sentinel before deploying (bughunt-track.sh add <stacks>); the
bughunt-clean-gate hook then blocks git commit / gh pr create / gh pr merge
until bughunt-track.sh verify (every tracked stack gone + sweep-orphans.sh
SWEEP CLEAN — it catches the /aws/lambda/* log group an S3 autoDeleteObjects
custom-resource Lambda leaves behind) passes and bughunt-track.sh clear releases
the gate.
A corpus-harvest fixture (R71): ~18 cheap, fast-create/delete types in one stack (DynamoDB, EventBridge Bus+Rule, Step Functions SM+Activity, Athena WorkGroup, CloudWatch Alarm+Dashboard, LogGroup+MetricFilter, ECR, SSM, HTTP API, REST API, CodeBuild, IAM User, EIP, Glue Database+Table). Asserts:
- A FRESH deploy classifies with zero declared drift across every type (the cross-type false-positive test) and exits 0 (inventory is UNRECORDED).
record --yesthencheck --faillands CLEAN across every type.
Run with CDKRD_CORPUS_DIR to record one golden-corpus case per type — the
fixture exists to convert one AWS round trip into permanent offline coverage.
cd harvest && npm install && bash verify-harvest.shWave 2 of the corpus harvest (R73): RICHLY-DECLARED configurations — S3
lifecycle+CORS, DynamoDB GSI+TTL+stream, Lambda with non-default
memory/timeout/arch/tracing/env, FIFO queues with redrive, SNS->SQS
subscription with a filter policy, EventBridge input transformer, WAFv2
WebACL with a managed rule group, ECS cluster (pulls in a full VPC: subnets,
routes, NAT, IGW — all recorded), Kinesis with the aws-managed KMS alias
(exercises the strict alias<->key-ARN match live). Asserts the same two
invariants as harvest: fresh deploy = ZERO declared drift, then
record -> check --fail CLEAN.
cd harvest2 && npm install && bash verify-harvest2.shWave 3 of the corpus harvest (R74), two jobs in one deploy:
- New service families the corpus had never seen live: Cognito
UserPool+Client, KMS Key+Alias, Secrets Manager, EventBridge Scheduler
(group + schedule), Firehose delivery stream, SES configuration set,
Cloud Map HTTP namespace, AppSync GraphQL API, CloudTrail trail, AWS
Backup vault+plan. Same two harvest invariants: fresh deploy = ZERO
declared drift, then record ->
check --failCLEAN. - Multi-type revert matrix — the first live proof of the Cloud Control
write path beyond S3. Five CC-routed declared values are mutated
out-of-band (Lambda
MemorySize, SQSVisibilityTimeout, LogsRetentionInDays, SNSDisplayName, Events RuleState); ONEcheckmust report exactly 5 declared drifts, ONErevert --yesmust restore all five, verified bycheck --failCLEAN and direct per-service AWS reads.
With CDKRD_CORPUS_DIR set, the drift-state recordings are snapshotted to
${CDKRD_CORPUS_DIR}.drifted before the post-revert check overwrites them —
one run yields BOTH a clean and a drifted corpus case per matrix type.
cd harvest3 && npm install && bash verify-harvest3.shWave 4 of the corpus harvest (R75): the remaining high-frequency families —
ALB + target group + listener (1-AZ-pair VPC, no NAT), EFS with mount
targets, Route53 public zone with an ALIAS record to the ALB (runs the
Route53 SDK reader's AliasTarget path live) + TXT record, Cognito
IdentityPool, DynamoDB Application Auto Scaling (ScalableTarget +
target-tracking policy), SSM Document, HTTP API with an explicit throttled
stage, ECR with a lifecycle policy. Same two harvest invariants, plus a
Key-scoped attribute-bag detect-and-revert: the declared idle_timeout
lives inside the {Key,Value}[] LoadBalancerAttributes bag — the template
declares 2 of ~23 attributes — so an out-of-band change to it must surface
as exactly ONE declared drift named by Key
(LoadBalancerAttributes[idle_timeout.timeout_seconds]). revert --yes
then restores it via the ELB SDK writer (ModifyLoadBalancerAttributes
with only the declared Key=Value, NOT a Cloud Control index patch — which
misaligns against the full live bag and exceeds ELB's 20-attribute cap,
R78), confirmed by a direct ELBv2 read. CDKRD_HARVEST4_KEEP=1 keeps the
stack for debug iteration.
cd harvest4 && npm install && bash verify-harvest4.shCloudFront Distribution (R75) — the most config-dense type, previously
covered only by hand-written corpus seeds. Two origins (S3 with OAC + HTTP)
and two behaviors so the Id-keyed Origins sort, the HTTP-method enum-set
sort, and the cache-policy reference shapes all run against real data.
Asserts the two harvest invariants (fresh deploy = ZERO declared drift,
record -> CLEAN). Kept separate from the harvest waves because deploy and
destroy each take minutes. CDKRD_CLOUDFRONT_KEEP=1 keeps the stack.
cd cloudfront && npm install && bash verify-cloudfront.shWave 5 of the corpus harvest (R77): long-tail family breadth — AppConfig
(application + environment + configuration profile + deployment strategy),
EventBridge Connection + ApiDestination + Archive, Glue Job + Trigger,
Lambda Function + Alias, IAM InstanceProfile, Route53 HealthCheck, a
CloudWatch CompositeAlarm over two child alarms, an EXPRESS StateMachine,
and an SSM Parameter. All cheap and fast — no VPC, no NAT, no slow
resources. Same two harvest invariants: fresh deploy = ZERO declared drift,
then record -> check --fail CLEAN.
cd harvest5 && npm install && bash verify-harvest5.shGuards the R95 fix (a console-ADDED tag must be DETECTED, not subset-projected
away). Deploy a bucket with one declared tag, record CLEAN, ADD a second tag out of
band (a Key the template never declared, via the Resource Groups Tagging API so the
aws:* system tags survive), and assert check --fail reports it. Before R95 the
added tag was silently dropped.
cd mutation-tags && npm install && bash verify.shThe highest-yield false-NEGATIVE hunt: properties whose normalizer sorts or
canonicalizes an array/policy (the class where the R88 bugs lived). Deploy an IAM
role with a named inline policy, a SecurityGroup with two ingress rules, and a WAFv2
IPSet with two addresses; record CLEAN; then ADD one element to each out of band (a
policy Action, an ingress rule, an IP address) and assert check --fail DETECTS
every one — proving the normalization does not OVER-suppress and silently hide a
real change. Run after changing src/normalize/**.
cd mutation-arrays && npm install && bash verify.shExercises the one AWS-mutating path across five types via Cloud Control
UpdateResource. Deploy SQS / SNS / Lambda / S3 / ECR, record a CLEAN baseline,
change one declared property on each out of band, then revert --yes and assert the
stack converges to CLEAN AND every live value is restored to its template value
(VisibilityTimeout 30, DisplayName, Timeout 10, VersioningConfiguration Enabled,
ImageTagMutability IMMUTABLE). Run after changing src/revert/**.
cd revert-multi && npm install && bash verify.shWaves 7 and 8 of the corpus harvest (R90): cheap, low-dependency CFn types that
were still uncovered after the corpus crossed 115 distinct types. Wave 7 — WAFv2
RegexPatternSet, Logs QueryDefinition, ServiceDiscovery HttpNamespace + Service,
Glue SecurityConfiguration + Workflow, IAM Group, Route53 CidrCollection,
EventSchemas Registry + Schema, CodeDeploy Application, SES Template, CloudWatch
AnomalyDetector. Wave 8 — ApiGateway Model / RequestValidator / GatewayResponse
(children of a RestApi), a Cognito UserPoolResourceServer, Route53 Resolver DNS
firewall (FirewallDomainList + FirewallRuleGroup), an IAM OIDC provider, and a
public ECR repository. Wave 9 — a single-AZ VPC carrying a NetworkAcl + entry, an S3 gateway VPC endpoint, and an EFS file system + access point. Same two harvest invariants (fresh deploy = ZERO declared
drift, then record -> check --fail CLEAN); the CC-unreadable types among them are
honestly skipped (not recorded, never false drift). Run with
CDKRD_CORPUS_DIR=<dir> to record the readable types as golden cases.
cd harvest7 && npm install && CDKRD_CORPUS_DIR=../../corpus bash verify-harvest7.sh
cd harvest8 && npm install && CDKRD_CORPUS_DIR=../../corpus bash verify-harvest8.shA versioned S3 bucket. Enables acceleration, records (recording it in the
baseline), then injects a DECLARED drift (versioning suspended) + an UNDECLARED
drift (acceleration suspended from its recorded Enabled), asserts check detects
both, runs cdkrd revert --yes, and asserts check is CLEAN and AWS itself
converged (versioning Enabled = template, acceleration Enabled = baseline value).
Proves the Cloud Control UpdateResource write path end-to-end.
One resource per SDK writer (SDK_WRITERS in src/revert/writers.ts):
AWS::S3::BucketPolicy, AWS::SNS::TopicPolicy, AWS::SQS::QueuePolicy,
AWS::IAM::Policy (standalone inline), AWS::IAM::ManagedPolicy. After
record + CLEAN, a CdkrdInjected statement is spliced into EVERY policy
document out of band; asserts check reports all 5 declared drifts, revert --yes converges through all 5 writers, check is CLEAN again, and direct AWS
reads confirm the injected statement is gone while the declared one survived
(for the managed policy: on the new default version). Covers the SDK-override
read path AND the SDK write path for every writer type end-to-end.