feat(go,terraform): mask sensitive body values in HTTP logs

Merged
TH
10d ago
+3783-19

Why

Terraform's HTTP debug logging writes raw request and response bodies before responses are decoded, so values marked sensitive (x-speakeasy-param-sensitive or format: password) that the API returns for the first time are not yet known to value-based redaction. API error strings can also contain response bodies.

What changed

  • Generator AST: Operation.SensitiveRequestBodyGraph(), SensitiveResponseBodyGraph() and HasSensitiveBodies() (internal/ast/sensitive_body.go) compute a language-agnostic graph of where sensitive values sit in an operation's bodies, by property name. The graph is built along TypeDef.Children, and TypeDef.Walk skips bodies with nothing sensitive. They handle objects, error types, arrays, maps and additionalProperties, unions, recursive schemas, and SSE/JSONL streams (masked as a whole). They are declared in typeDefs.d.ts, so any target can render them.
  • Go SDK: a public redact package holds the rendered graphs and exposes RequestBody(ctx, contentType, body) / ResponseBody(ctx, contentType, body). Methods of operations with sensitive bodies stamp the operation ID on the request context at request construction (utils.WithOperation), so a transport can find it from the request or res.Request. The transport and serialization are unchanged. Operations without sensitive bodies generate byte-identical code.
  • Go APIError.Error() masks the body when the SDK has any sensitive bodies; the Body field stays raw.
  • Terraform: the logging transport and debugResponse mask request and response bodies through redact. The existing known-value redaction is kept.

JSON bodies are masked value by value. application/x-www-form-urlencoded fields and multipart parts are matched by name: a sensitive field, or a field repeated for an array of sensitive values, is masked, and a JSON-encoded field or part is masked within. The masked body stays a valid form or multipart body with the same boundary. Other media types, and bodies that cannot be parsed, are masked as a whole for operations that have sensitive values. Unchanged bodies are returned byte-identical. Split request bodies (one schema per content type) are separate methods with their own operation IDs, so each is masked with its own schema.

Testing

  • internal/ast unit tests for the graph: nested, array items, map values beside a declared plain property, format: password with a renamed property, union variants, recursion, error types, streams, and a plain property sharing a sensitive one's name.
  • New primary fragment sensitive-body-redaction.yaml with Go runtime tests (redact_additional_test.go) for JSON, form, multipart and split request bodies sent through the SDK, the context stamp, and APIError.Error(). The form and multipart tests fail when their masking is disabled. TARGET=primary make test-go passes.
  • New Terraform review test checking that the transport log, error strings, and debugResponse hide computed sensitive values while decoded values stay raw. The full ./scripts/build-review-terraform.sh acceptance suite passes.
  • go test ./pkg/generate/... (the Python snapshot tests fail on main too, on a mypy _DURATION_ADAPTER annotation error), and make check-template-{go,terraform,cli}.

Known limitations

  • Union variants are matched by property name, so a property sensitive in one variant is masked in all.
  • Typed errors without a message field still json.Marshal themselves in Error().
  • Custom hooks that build their own error strings from bodies need to call redact.ResponseBody.
  • The CLI keeps its own request-body graph from #91 for now; it could switch to the AST methods.