Outcome
By the end of this lesson, you will be able to read a captured request/response body in the audit log, predict which bodies are stored, dropped or truncated, and decide what to do when your data policy forbids storing payloads.
| Prerequisites | M4.3.L1: What gets logged |
| Time | 9 minutes |
1. Concept
Recording that something happened is not the same as being able to investigate it.
For every change, ZopNight stores what was asked for and what came back (up to a size cap), alongside the time, the person, the path and the result.
The difference is this. “Somebody created a schedule on 20 May” tells you almost nothing. “Somebody created a schedule called business-hours-eu, with these crons, on these resources” tells you everything you would need to reconstruct it.
WHAT'S CAPTURED EXAMPLE──────────────────────────────────────────────────────────Request body The JSON payload posted to the APIResponse body The JSON response returnedNo headers (so no IP address or user agent) and no query string are stored. Body capture is always on: there is no org-level switch to turn it off, because the forensic value usually exceeds the storage cost.
Sensitive-data redaction
Credentials are kept out of the log in two ways, depending on how the change arrived. Both happen before storage and are irreversible: the value never reaches the log.
HOW IT ARRIVED WHAT HAPPENS──────────────────────────────────────────────────────────────────App or API For the areas that carry secrets, the request AND response bodies are dropped whole: cloud accounts, connect flows, integrations, AI (keys, providers), app secrets, basic auth, notification channels The row still records who, method, path, status and time. Every other body is stored as sent.
MCP tool call The arguments a tool declares as secret are replaced with [REDACTED]; the rest of the request is stored.[REDACTED] therefore only ever appears on MCP rows. There is no masking of emails, account numbers, project IDs or subscription IDs, and no customer-defined redaction policy: the lists are fixed by ZopNight. Anything you send in an ordinary body (a schedule description, a dashboard name) is stored as sent. Notification channel changes are the exception: their bodies are dropped whole, so a webhook URL or Slack token never reaches the log.
Why redaction matters
Three reasons converge:
SECURITY Credentials never persist in the audit log. If the audit log itself is ever exposed (export to a misconfigured SIEM, for example), there are no long-lived credentials to leak.
COMPLIANCE GDPR, CCPA, and similar regulations require data minimization. Storing credentials in audit logs without justification is a compliance finding. Dropping credential bodies is the practical answer.
INCIDENT If the audit log were itself the source of a breach, RESPONSE dropping secrets limits the blast radius. Defense in depth: do not turn one log into a credential dump.The trade-off: some debugging info is lost. For a cloud account, the log tells you who sent a PUT to that account’s path, when, and whether it succeeded, but not what was in the body. The current values are in the cloud-account record; the audit captures the event, not the secret. This is the right balance.
Body size limits
SIZE CAP 10,000 bytes by default, one setting for both the request body and the response bodyMAXIMUM 65,023 bytes (the limit of the storage column)WHO SETS IT ZopNight, for the whole deployment (not per org)A body larger than the cap is stored as its first part, cut cleanly at a character boundary, plus two extra fields: the original size in bytes and a SHA-256 hash of the full original body. There is no marker text inside the body; the presence of those two fields is what tells you it was cut. The hash lets you prove what the full body was if you hold a copy of it. This handles the rare very large payload without letting one outlier inflate audit storage.
In practice most mutations are small, so truncation is rare.
Reading a captured body
The Audit Logs page shows captured bodies inline. Click any audit entry and the request and response bodies render as JSON. The API returns the same data in the body and response_body fields.
AUDIT LOG ENTRY (POST /schedules)─────────────────────────────────────────────────────────Method: POSTPath: /schedulesStatus: 201
REQUEST BODY: { "name": "business-hours-staging", "timezone": "America/New_York", "crons": [ {"cronExpression": "0 8 * * 1-5", "action": "start"}, {"cronExpression": "0 20 * * 1-5", "action": "stop"} ] }
RESPONSE BODY: { "id": "sch_xyz789", "name": "business-hours-staging", "created_at": "2026-05-20T16:42:11Z", "resource_count": 7 }The response body’s id is the handle for tracing follow-up events on the same schedule. Put sch_xyz789 in the resource (path) filter to find every subsequent mutation against this schedule.
Forensic use cases
The patterns that recur:
QUESTION AUDIT-LOG QUERY──────────────────────────────────────────────────────────────────"Who last changed this schedule?" resource = schedules/<id> method = PUT → most recent entry, with user
"What was the schedule like before?" Same query as above; the captured request body is the new value; previous values are in earlier entries (compare)
"Why did the schedule change times?" resource contains the schedule id look for a PUT changing crons
"Who deleted resource X?" method = DELETE; path matches → user + timestamp
"What did this failed API call look like?" status = failed; user = … → error context
"Reproduce a customer's issue" user = customer recent actions in time windowThe combination of mutations-only logging + body capture is what makes these queries productive. Without body capture, you would know an event happened but not its details. With it, the audit log doubles as a state-change history.
If your policy forbids payload storage
There is no switch to turn body capture off and no retention cutoff: rows are kept indefinitely. If your data policy forbids storing request payloads, the practical options are to keep sensitive values out of ordinary request bodies and to raise the requirement with ZopNight before you adopt the product. Most orgs are well served by the default, because the storage cost is modest relative to the forensic value.
How ZopNight uses body capture
Body capture lives in one place: the gateway, which every request passes through. For app and API requests it keeps the raw request and response bodies as they are (or drops them for the credential areas), applies the size cap, and writes the entry to the org’s Redis Stream. Only MCP calls are parsed, so that declared secret arguments can be replaced with [REDACTED]. Because it sits in the gateway, every mutation is captured the same way with no per-service code.
2. Demo
A real-feeling forensic investigation:
USER reports: "Since May 15 my staging resources stay up until after midnight, every day."
INVESTIGATION via Audit Logs:
T+0 Open Activity → Audit Logs Filter: resource contains "sch_xyz789", method = PUT
T+30 sec Found entry on 2026-05-15T14:23:17Z: User: peter@example.com Method: PUT Path: /schedules/sch_xyz789 Status: 200
T+45 sec Click the entry. REQUEST BODY: { "crons": [ {"cronExpression": "0 8 * * 1-5", "action": "start"}, {"cronExpression": "30 0 * * *", "action": "stop"} ← here ] }
T+1 min Compare to the previous entry (creation): second cron was "0 20 * * 1-5" (Mon-Fri 20:00) now it's "30 0 * * *" (every day at 00:30)
Peter accidentally edited the cron and broke the schedule. The captured body confirms exactly what changed.
T+2 min Revert via UI; message Peter; document the lesson.
ELAPSED: ~10 minutes from question to root cause + fix.Without body capture, the audit log would say “Peter sent a PUT to sch_xyz789 on May 15.” The investigation would require finding Peter, asking what he changed, hoping he remembers. With body capture, the change is in the log.
3. Hands-on (5 min)
Pick one mutation from your audit log and read it end-to-end:
PICK an audit entry from the last 30 days. Capture:
Timestamp: __________User: __________Method+path: __________Status: __________
REQUEST BODY (paste a small portion): __________________________________________________________ __________________________________________________________
RESPONSE BODY (paste a small portion): __________________________________________________________
BODY DROPPED or [REDACTED] (if any; which area or tool?): __________
This is the exact form an auditor or incident responder would see.If your team has been using body-capture mode without realizing it, this exercise often surprises people with how much detail is preserved.
4. Knowledge check
Q1
A request body contains a cloud credential. The audit log stores:
A. The full credential, since it would be needed for forensics later on
B. The credential, but encrypted at rest with the organisation’s own key
C. [REDACTED] in place of the credential field; other fields stored unchanged
D. No request or response body at all; just who, method, path, status and time
Show answer
Correct: D. For the credential areas (cloud accounts among them), both bodies are dropped whole before storage. Field-level [REDACTED] (option C) happens only on MCP tool calls. The credential itself lives in the encrypted cloud-account record, never in the audit log.
Q2
A request body of 200 KB is sent. What lands in the audit log?
A. The first 10,000 bytes, plus the original size and a hash of the full body
B. The full 200 KB body, stored exactly as it was sent to the API endpoint
C. Nothing; the request is rejected at the gateway for being too large
D. A random sample of the body, taken at ingest time by the middleware
Show answer
Correct: A. With the default cap, the first 10,000 bytes are kept (more if ZopNight has raised the cap, never above 65,023), and the row records the original size and a SHA-256 of the full body. Those two fields, not a marker in the text, are how an investigator knows the body was cut.
Q3
Body capture applies to GET requests:
A. Yes, with precisely the same redaction pass applied to them
B. No: body capture only applies to mutations (POST/PUT/PATCH/DELETE)
C. Only on those GET requests that happen to carry a request body
D. Only on the GET requests that ultimately returned successfully
Show answer
Correct: B. App and API GETs are not logged at all, refused ones included, so there is nothing to capture. The only GET-labelled rows are MCP read-tool calls, which store the request but not the response.
5. Apply
Click any entry in Activity → Audit Logs to read the captured bodies. The API (GET /orgs/{orgID}/audit-logs) returns the same rows, including the body and response_body fields.
For forensic playbooks, write down a few common filters in your runbook: “who changed this schedule” (resource contains the id, method PUT), “all DELETEs last 7 days” (method DELETE, date range), “all failed mutations” (status = failed). These three cover most ad-hoc investigations.
Related lessons
- L1: What gets logged
- L3: Filtering and export (next)
- L4: Compliance posture
- T4.M4.1.L1: The policy table