MIT Network Audit docs
The audit log

The forensic record

You found the row that matters — a call to a host you do not recognise, or one you simply want to understand. Now the question is: what exactly does the log know about it, and can I trust that what it shows me has not been quietly edited? This page opens a single row of the audit log and walks every field on the Forensic Record form, in plain language, so you know precisely what was captured, what was deliberately not captured, and how the row proves it has not been tampered

8 min read

You found the row that matters — a call to a host you do not recognise, or one you simply want to understand. Now the question is: what exactly does the log know about it, and can I trust that what it shows me has not been quietly edited? This page opens a single row of the audit log and walks every field on the Forensic Record form, in plain language, so you know precisely what was captured, what was deliberately not captured, and how the row proves it has not been tampered with.

Open any row from Network Audit → Audit Log to land on this form.

One row of the audit log opened as the Forensic Record form, showing the tamper-evident chain fields The Forensic Record form: one network event, every field — and the hash chain that seals it.

Metadata only — by design

Before you read another line: this form has no body field, and that is not a setting you can flip. The captured event simply has nowhere to store a request or response body. What you see below is all the log ever keeps about a call. See SIZE & TIMING for why that is the whole point.


WHAT happened — the shape of the call

The first thing you want from any log line is the gist: which way did the data go, over what kind of connection, and did it succeed? The Forensic Record answers that in five fields.

Field What it tells you
Direction Whether this was traffic leaving your Odoo (outbound) or arriving at it (inbound).
Channel Which network layer captured the call — for example requests, httpx, urllib, imap, smtp, pop3, the raw TLS socket, or the low-level socket backstop.
Protocol The protocol seen on the wire (for example HTTP/HTTPS for web calls, or the mail protocol for a mailbox sync).
Method The verb of the call — an HTTP method like GET or POST for web traffic, or the equivalent operation for the channel.
Status The outcome — the HTTP status code for web calls, so you can tell a clean 200 from a 403 or a failure.

Read together, these five tell you the story of the call before you ever look at the address: "an outbound HTTPS POST over requests that returned 200" is already a complete sentence.

Why the channel matters

The Channel field is your evidence that the capture went deep. A call can be seen by a high-level library hook (requests, httpx) and, if it slips past one, by the raw socket backstop — so even a library the auditor does not specifically know still produces a row with a destination. A channel of socket is not an error; it is the safety net doing its job.


WHERE it went — the destination

This is the section most people open the record for: where did my data actually go?

Field What it tells you
Host The destination hostname the call was made to.
URL path The path portion of the URL (no body, no secrets in the path beyond what was on the wire).
Query keys The names of any query-string parameters — the keys only, never their values.
Destination IP / port The resolved IP address and port the connection actually reached.
Source IP / port The local address the connection went out from.

The host is matched against the seeded Egress Components catalogue to decide whether the destination is MIT-bound or third-party — the same classification that drives the verdict on the Trust Report. On a row attributed to one of your own modules talking to your own Gmail, IMAP, or LLM endpoint, you should see your provider's host here — not an MIT one.

Keys, not values

The Query keys field keeps parameter names (so you can see the shape of a request) but never the values. A ?token=… becomes a record that a token parameter was present, not the token itself.


WHO made the call — attribution

A destination on its own is only half the answer. The field that makes this module a trust tool rather than a packet sniffer is attribution: every row names the Odoo module responsible for the call.

Field What it tells you
Addon (technical name) The technical name of the Odoo module that originated the call (for example maildesk_mail_client).
Module The human-readable module the call belongs to.
Author The author declared by that module's manifest.
Call site Where in the code the call was made — the originating location identified by walking the live call stack.

This is captured by walking the live call stack at the moment of the network operation and matching the responsible frame back to an installed ir.module.module. It is what lets you say, with evidence, "this call to my mail provider came from MailDesk, authored by Metzler IT" — and, just as importantly, "no call on this list is attributed to a module I did not install."

The self-egress check

mit_network_audit makes no outbound calls of its own. If you ever see a row whose Addon is mit_network_audit, the Trust Report raises an alarm — the auditor catching itself would be the one thing it must never do silently.


SIZE & TIMING — bytes, no body stored

Here is where the redaction-by-design shows up on the form. You get the measurements of the payload, never the payload.

Field What it tells you
Request size How many bytes the request carried.
Response size How many bytes the response carried.
Timings How long the call took.

That is deliberately everything. The sizes let you reason about volume — a sudden large upload to an unexpected host is visible as a number — without the log ever holding the content of that upload. No request or response body is stored on this record. The captured event has no field for one; the redaction is structural, baked into the data model, not a checkbox an administrator could uncheck.

The one exception, stated plainly

There is a separate, off-by-default, officer-only capability — forensic body capture — that can store bodies for a legal case. It is not part of this record, it is gated to the Network Forensic Officer role, and every read of a captured body is itself logged to this same append-only journal. If you have not turned it on, no bodies exist to read.


PROVENANCE — where the record itself came from

These fields are about the recording, not the call: they let an auditor place the row in time and on a specific machine.

Field What it tells you
Logged at When the event was recorded.
Worker PID The process ID of the Odoo worker that captured it.
DB name The database the event was logged in.

Provenance is what makes the log usable in a real investigation: you can correlate a row to a particular worker process and a particular database, not just to "the system."


TAMPER-EVIDENT CHAIN — the row that seals onto the last

This is the heart of why you can trust the log. Three fields turn an ordinary table into a tamper-evident chain.

Field What it tells you
Chain Seq A database-unique serial number that fixes this row's position in the chain.
Prev Hash The hash of the previous row in the chain.
Row Hash This row's own hash — a SHA-256 computed over this row's contents plus the previous row's hash.

The tamper-evident chain fields and the masked request/response headers on a Forensic Record The seal in close-up: Chain Seq, Prev Hash and Row Hash — and below them, headers kept by name but with their values masked.

What the chain means, in plain terms

Think of the log as a stack of wax seals. Each new row is stamped with a seal that is made from its own contents and an imprint of the seal below it (its Prev Hash). Because every row's Row Hash folds in the one before it, you cannot quietly change an old row without breaking the seal on every row stacked on top of it. To forge one line, an attacker would have to re-compute the entire chain from that point forward — and even then, two further defences stand in the way:

  • The journal is append-only. The network.egress_log table rejects write and unlink — even with sudo — for everything except the retention vacuum. There is no supported path to edit a row in place.
  • A daily job signs the chain head. The current top-of-chain is HMAC-signed into a stored parameter, so even deleting rows is detectable: the signed head outlives the rows it covers. (More on this on Verify & export.)

When you press Verify chain on the Trust Report, the module recomputes the Row Hash of every row from the chain itself and compares. If everything matches you get "Audit chain verified OK — no tampering detected." If a single row was altered, the verifier names the first row where the chain breaks.

What this proves, and what it does not

The chain proves the log has not been edited after the fact — it is tamper-evident. It does not, on its own, prove that every call your system ever made was captured. The capture is deep (down to the socket), but this is a transparency-and-detection tool, not a sealed sandbox. We say so on How it works rather than oversell it.


Masked request and response headers

The record keeps the headers of both the request and the response — but masked. You see the header names (so you can confirm, for instance, that an Authorization header was present and what content type was negotiated) while the sensitive values are masked at the moment of capture. The names tell you the shape of the exchange; the masked values mean a bearer token or cookie never lands in the log in the clear.

Strict redaction goes further

If Strict redaction is enabled in Settings, capture keeps only host, IP, port and sizes and masks everything else at capture time — so on a strict-mode deployment the header detail above is intentionally minimal. That is a stronger privacy posture, not a malfunction.


From one row to proof

Reading a single record tells you what happened. Turning that into something you can hand to an auditor — a signed, independently checkable export — is the next step.