Skip to content

Latest commit

 

History

History
627 lines (511 loc) · 33.1 KB

File metadata and controls

627 lines (511 loc) · 33.1 KB

Integration tests (real AWS)

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 -> destroy

verify.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).

Recording golden-corpus cases (R63)

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.

When to run (R50)

These do NOT run in CI (they need credentials and mutate a real account):

  • Before every release (the /verify-pr gate): 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 --delete to remove). Each verify.sh tears 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.sh finds 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/**, or src/commands/gather.ts: run at least basic + revert + noise + the false-positive matrix (below — each asserts tricky declared values normalize equal to live, never a false declared drift) (and policies if writers.ts changed; harvest3 for the multi-type Cloud Control revert matrix) before merging.
  • After changing the CC identifier adapters (CC_IDENTIFIER_ADAPTERS in src/read/router.ts) or the revert write-only re-inclusion (writeOnlyReincludeOps in src/revert/plan.ts): run ecs-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 drifts DesiredCount and asserts the revert SUCCEEDS without dropping the write-only VolumeConfigurations (R102).
  • After changing the Tags revert path (tagPreservingOps in src/revert/plan.ts, awsManagedTags in src/normalize/noise.ts, or liveByLogical plumbing in src/commands/gather.ts): run sns-topic-tags-revert — it deploys an SNS Topic (which AWS auto-tags with aws:cloudformation:*), records a baseline, adds a USER tag out of band, then asserts revert REMOVES the user tag while PRESERVING the aws:* managed tags (AWS rejects any write that drops an aws:-prefixed key, R131).
  • After changing src/diff/** or src/baseline/**: run basic/verify-mutation-matrix.sh (the drift-direction matrix) before merging.
  • After changing nested-undeclared detection (collectNestedUndeclared in src/diff/classify.ts) or the revert nested guard (src/revert/plan.ts): run dynamodb/verify-nested.sh — it deploys a table whose GSI and PITR config materialize nested undeclared values (R96/R98), asserts the info: undeclared-subkey=N fold + --show-all expansion live, then mutates the undeclared RecoveryPeriodInDays out of band and asserts the drift is detected and reported NOT-revertable (R99).
  • After changing KNOWN_DEFAULTS (src/normalize/noise.ts) or the atDefault fold: run atdefault — 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 (collectDefaultPaths in src/schema/schema-strip.ts) or the nested atDefault fold (src/diff/classify.ts): run cloudfront-atdefault — it deploys a CloudFront Distribution and asserts its schema-annotated nested defaults (CustomOriginConfig.HTTPSPort/OriginReadTimeout, PriceClass, …) fold as atDefault rather than undeclared (R103). CloudFront deploy/destroy are slow.
  • After changing the fully-undeclared-object descend (DESCEND_UNDECLARED_OBJECT_PATHS in src/normalize/noise.ts, the descend branch in src/diff/classify.ts): run athena-descend — it deploys a workgroup declaring NO WorkGroupConfiguration (whole default folds atDefault), then sets ONE non-default sub-key out of band and asserts cdkrd surfaces ONLY the descended WorkGroupConfiguration.BytesScannedCutoffPerQuery (not the whole object), the constants still folding (#565).
  • After changing the Lex BotLocales writer (writeLexBotLocales in src/revert/writers.ts): run lex-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 asserts revert RECREATES it (CreateIntent), and ADDS a whole intent out of band and asserts revert DELETES 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.

basic

One versioned S3 bucket. Asserts:

  1. record then check reports CLEAN (exit 0).
  2. After enabling transfer acceleration out-of-band (an undeclared change CFn drift would not catch), check reports drift (exit 1) and names AccelerateConfiguration.

basic / verify-deleted-guards.sh

A second script in the basic fixture (reuses its bucket) covering the deleted tier and the revert guards:

  1. R2 revert guard — with NO baseline, revert --dry-run reports the undeclared value as NOT revertable (unrecorded, R62) while a declared drift is still in the plan; --remove-unrecorded opts in to removing it.
  2. R1 deleted tier — after deleting the bucket out of band, check reports the deleted tier (exit 1) and revert --dry-run reports it as not revertable (deleted — recreate via cdk deploy).
cd basic && bash verify-deleted-guards.sh

basic / verify-vs-cdk-drift.sh

Empirical proof of the README capability table, against cdk drift itself (reuses the basic stack; needs an aws-cdk with the drift command):

  1. After an undeclared change (transfer acceleration), cdk drift --fail exits 0 (CFn drift detection cannot see it) while cdkrd check exits 1 and names AccelerateConfiguration — the differentiator, demonstrated.
  2. 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.

basic / verify-mutation-matrix.sh

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.sh

iam / lambda

IAM 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.

iam / verify-inline-policy.sh

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):

  1. record then check reports CLEAN — the sibling DefaultPolicy entry in the role's live Policies is filtered by name, not reported as drift.
  2. After put-role-policy adds a rogue inline policy out-of-band, check reports Policies drift (exit 1) naming ONLY the rogue policy — the sibling entry does not leak into the finding.
  3. revert --yes deletes ONLY the rogue policy (per-name DeleteRolePolicy, not a whole-property Cloud Control patch): the DefaultPolicy survives with its document intact, and check is CLEAN again.
cd iam && bash verify-inline-policy.sh

iam / verify-declared-inline-revert.sh

A 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.sh

iam / verify-sibling-policy-doc.sh

A 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:

  1. record then check is CLEAN (sibling filtered on the role; document matches).
  2. After put-role-policy rewrites the sibling's document out-of-band (s3:ListAllMyBucketss3:GetObject), check reports it as DECLARED drift on the AWS::IAM::Policy resource 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.
  3. revert --yes restores the declared document (writeIamPolicy), and check is CLEAN again.
cd iam && bash verify-sibling-policy-doc.sh

stages / verify-stages.sh

DISCOVERY-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.sh

nested / verify-nested.sh

A 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.sh

atdefault

Validates 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):

  1. before any baseline, those values FOLD into the atDefault tier — they are counted in the info: footer but NOT listed in the report body. This proves the hand-written KNOWN_DEFAULTS shapes (especially the S3 BucketEncryption shape with BlockedEncryptionTypes) still match what Cloud Control returns; a mismatch would reclassify the value as real undeclared and surface it in the body.
  2. --show-all expands the fold and lists those same values under AT AWS DEFAULT.
  3. after record, check is CLEAN (the at-default values fold, never recorded).
  4. mutating one at-default value away from its default (Lambda TracingConfig PassThroughActive) makes check surface it as real drift — the fold never blinds cdkrd to an actual change (the equality gate has teeth).
cd atdefault && bash verify.sh

freeform-strip / verify-freeform-strip.sh

The 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.sh

budget-scope / verify-budget-scope.sh

The 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.sh

codebuild-proj / verify-codebuild-proj.sh

The 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.sh

noise

The 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:SecureTransport Condition key (R69 regression: it must survive the live read, NOT be stripped as an aws:* 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.sh

readgap

The 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.sh

mutation-multi

The 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.sh

false-positive matrix

Nine 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 above

rich / common false-positive fixtures (integ bug-hunt round)

Added 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 --yescheck --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+revert

Cleanup 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.

harvest

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:

  1. A FRESH deploy classifies with zero declared drift across every type (the cross-type false-positive test) and exits 0 (inventory is UNRECORDED).
  2. record --yes then check --fail lands 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.sh

harvest2

Wave 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.sh

harvest3

Wave 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 --fail CLEAN.
  • 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, SQS VisibilityTimeout, Logs RetentionInDays, SNS DisplayName, Events Rule State); ONE check must report exactly 5 declared drifts, ONE revert --yes must restore all five, verified by check --fail CLEAN 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.sh

harvest4

Wave 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.sh

cloudfront

CloudFront 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.sh

harvest5

Wave 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.sh

mutation-tags

Guards 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.sh

mutation-arrays

The 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.sh

revert-multi

Exercises 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.sh

harvest7 / harvest8 / harvest9

Waves 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.sh

revert

A 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.

policies

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.