Skip to main content
← Back to list
01Issue
BugShippedExtensions
Assigneesstack72

#363 bucket-policy StateSchema.PolicyDocument declared z.string() but CloudControl returns it as a parsed object

Opened by jentz · 5/16/2026· Shipped 5/18/2026

Summary

@swamp/aws/s3/bucket-policy's StateSchema declares PolicyDocument: z.string().optional(), but AWS CloudControl's GetResource returns the field as a parsed object (the policy JSON already deserialized into {Version, Statement, ...}). The persisted state on disk matches the AWS response shape, so any consumer that safeParses state against the published schema misclassifies the bucket as having no policy.

Where

// .swamp/pulled-extensions/@swamp/aws/s3/models/bucket_policy.ts:33-36
const StateSchema = z.object({
  Bucket: z.string(),
  PolicyDocument: z.string().optional(),   // <- but AWS returns this as an object
}).passthrough();

The GlobalArgsSchema (line 28) and InputsSchema (line 44) correctly declare PolicyDocument: z.string() — at write-time you supply JSON as a string. The mismatch is specifically in StateSchema, which describes what AWS gives back.

Verified unchanged in the latest published 2026.04.23.3 after swamp extension update @swamp/aws/s3.

Reproduction

  1. Pull @swamp/aws/s3 (swamp extension pull @swamp/aws/s3).
  2. Create a bucket-policy instance with placeholder globalArgs and run get against any bucket that has a policy (e.g. a TF-state bucket with a DenyInsecureTransport statement).
  3. Read the persisted state via cat .swamp/data/@swamp/aws/s3/bucket-policy/<id>/<bucket>/<v>/raw.

Actual on-disk shape we observed:

{
  "Bucket": "xyz-iac-026090512999",
  "PolicyDocument": {
    "Version": "2012-10-17",
    "Statement": [
      {
        "Condition": { "Bool": { "aws:SecureTransport": "false" } },
        "Action": "s3:*",
        "Resource": [
          "arn:aws:s3:::xyz-iac-026090512999/*",
          "arn:aws:s3:::xyz-iac-026090512999"
        ],
        "Effect": "Deny",
        "Principal": "*",
        "Sid": "DenyInsecureTransport"
      }
    ]
  }
}

PolicyDocument is an object, not a string.

Expected

A consumer running StateSchema.safeParse(JSON.parse(<raw bytes>)) should succeed and return the policy in a usable form.

Actual

safeParse fails with expected string, received object on PolicyDocument. Strict consumers treat the bucket as having no policy and emit spurious findings (this was the actual reason our first end-to-end TLS-only audit incorrectly flagged a properly- configured bucket).

Impact

Any third-party extension that depends on @swamp/aws/s3/bucket-policy state via its declared schema will misread reality. Affects audit reports, drift detection, anything reasoning about the policy contents. The miss is silent — safeParse reports a validation error but consumers that fall back to null/undefined (the documented shape) treat it as a missing policy rather than as a schema bug.

Workaround

Union schema with normalization in the consumer:

const BucketPolicyStateSchema = z.object({
  Bucket: z.string(),
  PolicyDocument: z.union([
    z.string(),
    z.record(z.string(), z.unknown()),
  ]).optional(),
}).passthrough();

Then JSON.parse the string shape if encountered. The tolerance is safe even after upstream is fixed.

Suggested fix

Change StateSchema.PolicyDocument to match the response shape:

const StateSchema = z.object({
  Bucket: z.string(),
  PolicyDocument: z.record(z.string(), z.unknown()).optional(),
}).passthrough();

GlobalArgsSchema and InputsSchema should stay as z.string() since that's the write-side contract — only StateSchema reflects what AWS returns.

If you prefer to keep tolerance for the historical string shape on disk (in case anyone has older state), a union (z.union([z.string(), z.record(...)])) is also reasonable.

Environment

  • swamp 20260516.045246.0-sha.e6eda98d
  • @swamp/aws/s3 2026.04.23.3 (verified bug present)
  • AWS region eu-west-1, CloudControl path
  • macOS 25.4.0 / Deno bundled with swamp

Upstream repository: https://github.com/systeminit/swamp-extensions

Environment

  • Extension: @swamp/aws/[email protected]
  • swamp: 20260516.045246.0-sha.e6eda98d
  • OS: darwin (aarch64)
  • Deno: 2.7.14+19bd3d8
  • Shell: /bin/zsh
02Bog Flow
OPENTRIAGEDIN PROGRESSSHIPPED+ 1 MOREASSIGNED+ 2 MOREREVIEW+ 3 MOREPR_MERGEDSHIPPED

Shipped

5/18/2026, 11:42:04 PM

Click a lifecycle step above to view its details.

03Sludge Pulse
stack72 assigned stack725/18/2026, 10:43:14 PM

Sign in to post a ripple.