MIT Network Audit docs
How it works

How it works — architecture

When a tool promises to prove your data isn't leaving to MIT, the very next, very fair question is: how is it built, and can I check the build myself? A trust tool that you have to take on faith is just one more black box. So this page opens the lid. It walks through how MIT Network Audit is put together — what watches the network, what writes the log, and where the seam is between the part you can audit standalone and the part that plugs into Odoo.

8 min read

When a tool promises to prove your data isn't leaving to MIT, the very next, very fair question is: how is it built, and can I check the build myself? A trust tool that you have to take on faith is just one more black box. So this page opens the lid. It walks through how MIT Network Audit is put together — what watches the network, what writes the log, and where the seam is between the part you can audit standalone and the part that plugs into Odoo.

The design is deliberately layered for one reason: the piece that actually watches your traffic is pure Python with zero Odoo imports, so it can be read, reasoned about, and even run on its own — without trusting the Odoo plumbing around it.


The big picture in one minute

Every network call your Odoo makes — a connection coming in from a browser, or a connection going out to Gmail, your LLM key, or an MIT licence endpoint — passes through the same short pipeline:

  1. The interceptor catches it at the lowest practical layer (down to the raw socket).
  2. Attribution names the culprit — it walks the live call stack to find which Odoo module made the call.
  3. Redaction throws the body away — only metadata survives; the contents are never captured in the first place.
  4. The sink writes one row to the append-only, hash-chained log (network.egress_log).
  5. A rollup pre-aggregates those rows (network.egress.rollup) so the dashboard is fast.
  6. The Trust Report reads the rollup and shows you a verdict.
flowchart LR
    subgraph traffic[Your Odoo network traffic]
        IN[Inbound HTTP<br/>browser → Odoo]
        OUT[Outbound calls<br/>requests · urllib · httpx<br/>imap/smtp/pop3 · SSL · socket]
    end

    IN -->|WSGI middleware| INT[Interceptor core<br/>pure Python · zero Odoo imports]
    OUT -->|channel patches| INT

    INT -->|attribute + redact| NE[NetEvent<br/>metadata only · no body]
    NE --> SINK[DB sink]
    SINK --> LOG[(network.egress_log<br/>append-only · hash-chained)]
    LOG --> ROLL[(network.egress.rollup<br/>pre-aggregated)]
    ROLL --> TR[Trust Report<br/>OWL dashboard]

Everything below is just the detail of those boxes.


Three layers, one clean seam

The module is split into three layers, and the split is the whole point. The part that does the watching is kept rigorously separate from the part that knows about Odoo — so the trustworthy core stays small, readable, and independent.

Layer Folder What it is Odoo imports?
The interceptor core interceptor/ The pure watcher: channels, the captured event, redaction, the inbound middleware, the outbound patches. None — zero. Auditable as a standalone library.
The glue glue/ The Odoo-coupled injection that wires the core into the running server. Yes — this is where Odoo coupling is allowed to live.
The Odoo model layer models/ (plus views, security) The append-only log, the rollup, attribution, redaction policy, and configuration. Yes — these are Odoo models.

Why the core has zero Odoo imports — and why that matters to you

The interceptor/ folder is pure Python. It does not import odoo anywhere. That is enforced, not just intended — the build pipeline greps the folder and fails if an Odoo import ever sneaks in. The payoff is independence: you can read and run the core that actually touches your traffic without trusting the Odoo glue around it. The trust-critical code is deliberately the smallest, most self-contained part of the module.

How do the two sides talk without the core knowing about Odoo? The pure core is handed two small, well-defined collaborators by the host: an Attributor (which turns a live call stack into the responsible ir.module.module) and a RedactionPolicy (which decides how headers are masked). The core calls them through a plain Python interface; the Odoo glue supplies the real, Odoo-aware implementations and the database sink. That seam is exactly what keeps the auditable library free of Odoo.


What the interceptor catches — the capture channels

To prove your data isn't leaving, the module has to sit in the path of every way data can leave. It does that by wrapping each network channel a module might use, at more than one layer — so even a library it doesn't specifically recognise still produces a row.

Outbound channels

Channel What is wrapped Layer
requests HTTPAdapter.send HTTP library
urllib / http.client the standard-library HTTP path HTTP library
httpx the modern async/sync HTTP client HTTP library
imap / smtp / pop3 the mail protocols Mail protocols
ssl.SSLSocket raw TLS sockets L7 (encrypted socket)
socket.connect / connect_ex the raw socket connect L4 backstop

The last one is the safety net. Wrapping socket.connect at L4 means that even a library MIT Network Audit doesn't specifically know about still gets a destination row when it opens a connection. A call can't slip past simply by being exotic — if it touches the network, it leaves a trace.

Inbound HTTP

Inbound web requests are captured by a pure WSGI middleware (interceptor/wsgi.py). The glue (glue/inbound_http.py) injects that middleware into odoo.http so it sits in front of every incoming request. The middleware itself contains no Odoo code — it is part of the pure core.

Channels are configurable, off by default means off

You can switch individual channels off at runtime from Configuration → Settings → Disabled channels (a comma-separated list such as socket,smtp; empty means capture everything). No restart is needed. See Settings & forensic capture.


Attribution — naming who made the call

Seeing that a connection went out is only half the answer; you want to know who sent it. That is attribution.

At the moment a call is intercepted, the module walks the live call stack and maps the frames back to the Odoo module that owns the code — resolving the result to an ir.module.module record. So each captured row carries the attributed addon, its author, and the call site. That is what lets the Trust Report split traffic into "this came from MailDesk", "this came from the license locker", "this came from Odoo core" — and how it would surface anything reaching the wrong destination, attributed to the module responsible.

The auditor audits itself

By design, this module makes no outbound calls of its own. If attribution ever names a call as coming from mit_network_audit itself, the Trust Report raises an alarm. The watcher is not exempt from its own watching. See Verdicts explained.


What a captured event holds — and what it can't

Redaction in this module is structural, not a setting. The captured event — internally a NetEvent — simply has no body field. There is no toggle to "also store the body" in the default capture path, because there is nowhere to put it. The private contents of your traffic are not recorded, because they are never captured in the first place.

What a captured event does keep is metadata only:

  • direction, channel, protocol, method
  • host, URL path, query keys (keys only)
  • destination and source IP + port
  • status, and request / response sizes
  • timings
  • masked headers — header names are kept, header values are masked
  • a salted HMAC fingerprint of the request
  • the attributed addon + author + call site

There is one — locked-down — exception

An optional forensic body capture mode exists for legal cases. It is a separate, officer-only store (network.egress_body), it is off by default, and every read of a captured body is itself logged to the append-only journal. It does not change the default behaviour: out of the box, no body is ever stored. See Settings & forensic capture.


Fail-open — the auditor never breaks your traffic

A guard rail you should know about: capture is fail-open. Any exception inside capture, attribution, redaction, or the sink is swallowed. If something goes wrong in the auditor, the audited network call still completes normally. The module is built so that the thing watching your traffic can never become the reason your traffic fails.


The append-only log and the rollup

The sink writes each event as one row in network.egress_log — the append-only journal. That table is the source of truth, and it is tamper-evident by construction: it rejects write and unlink (even via sudo) except the retention vacuum, and each row's hash chains the previous row's hash, so any alteration is detectable. The mechanics of that chain, and how verify() and the signed head defeat even truncation, are covered on Why you can trust it.

The journal is exhaustive — one row per call — which is exactly what you want for forensic evidence, but too much to read live for a dashboard. So a separate rollup (network.egress.rollup) pre-aggregates the rows on a cron. The Trust Report and Traffic Summary read that pre-aggregated table, which is how the dashboard stays fast without re-scanning every row on every refresh. Each destination is also classified MIT-bound vs third-party against a seeded catalogue of known MIT endpoints (see Configuration → Egress Components) — that classification is what powers the two-bucket split you see on the report.


Deployment-portable install

The interceptor only works if it is loaded into every worker process before any request is served. The module is built to do that on the two very different ways Odoo gets deployed, both funnelling through a single idempotent post_load():

Deployment How it loads
Self-hosted / Kubernetes Server-wide via post_load, where --load / server_wide_modules is set — every worker is patched at startup.
Managed PaaS (Odoo.sh) Installed from an ordinary Apps install; the interceptor wires itself in via ir.http._register_hook.

You do not have to choose or configure the mechanism — it adapts to where it is running. For the step-by-step, see Installation.


See for yourself

The full Trust Report dashboard showing an all-clear verdict Inbound and outbound calls flow through the interceptor and the sink and surface here, on the Trust Report.

The architecture exists to make one promise checkable: the only MIT-bound traffic is small licence metadata, and everything touching your real content goes to a provider you chose. The next page shows why the log itself can be trusted — the invariants that make the record tamper-evident.