
Audit trail
Every answer, and every refusal. Written down before anybody asks to see it.
Most access logs record what worked. This one records what did not, with equal care, because a pattern of refusals is the more interesting signal – somebody probing at the edge of their access looks exactly like nothing at all in a log that only writes successes.
Most systems log what they did. The refusals are the half that proves the rules were running.
| Time | Person | Tool | Outcome | Reason |
|---|---|---|---|---|
| 09:14:02 | a.morgan | describe_model | allow | – |
| 09:14:38 | a.morgan | read_slice | allow | – |
| 09:15:11 | a.morgan | crm_read_all | deny | skill_not_enabled |
| 09:15:11 | t.walsh | docs_search | allow | – |
| 09:16:40 | t.walsh | read_slice | deny | no_grant_on_project |
| 09:22:03 | – | – | deny | credential_rejected |
The last row has no person on it on purpose. A credential rejected before it reached any permission check is still recorded, so somebody guessing at your connector is visible rather than silent.
The reason codes
Twenty names, and a test that freezes them.
A refusal reason is a stable name, not a sentence someone rephrased. There are twenty of them, and a test fails the build if one changes.
Why not a message
A human-readable message is written for whoever is debugging that week. It gets reworded, it gets translated, and every rewording silently breaks the report somebody built on top of it.
Why a test rather than a convention
Conventions are followed until a deadline. The frozen set means renaming a code is a deliberate, visible act with a failing build attached, rather than a tidy-up in a pull request nobody reads closely.
What you do with them
Group by code. A rise in one specific refusal is a person hitting a boundary repeatedly, and it usually means a grant is wrong rather than that somebody is malicious.
This is data somebody will read three years from now. It has to still mean the same thing then.
Which is the whole argument for a frozen name over a readable sentence. A message gets reworded by whoever is debugging that week, and every rewording silently breaks the report somebody built on top of it.
The record
What a row contains. And what it deliberately does not.
The absence of figures is a deliberate trade. It means the audit trail cannot tell you what a person saw, only what they were permitted to ask for. Recording the values would make the log a second copy of your data, with all of the exposure and none of the controls.
| when | 09:16:40 |
| who | t.walsh |
| environment | planning-test |
| tool | read_slice |
| outcome | deny |
| reason | no_grant_on_project |
On every row
- When – to the second.
- Who – the verified person, by their own identity rather than a shared credential.
- Which environment – never inferred, always named.
- Which tool was called.
- The outcome – allowed or refused.
- The reason – one of the twenty names, on every refusal.
Never on a row
- The figures that came back. The record says a read happened, not what it returned.
- Document contents. Same reason.
- Anything about another organization. Not a name, not an identifier, not a slug.
- Free-text somebody can shape. A log line an attacker can write into is a log line you cannot trust.
Access to the record
It is yours, and reading it is itself governed.
| Question | Answer |
|---|---|
| Who can read it | Your own administrators, through the console, scoped to your organization. There is no cross-organization view, including for us. |
| Can you export it | Yes. It is your record and it leaves in a form you can put in front of an auditor. |
| Can you edit it | No. Neither can we, through the console. |
| Does it survive a revoked grant | Yes. Removing somebody's access does not remove the history of what they reached, which is the point. |
One honest caveat about permanence. The console offers no way to alter a row, and no feature exists to do it. That is an application-level guarantee, not a cryptographic one: it is a database, and a database administrator with direct access is a different threat model. If write-once storage is a requirement in your review, say so early rather than assuming it.
The export
The export is the page's own query. Which is a stronger promise than it sounds.
Most products build an export separately from the screen it exports. The two then drift, and the drift is always in the same direction: the screen shows something the file leaves out. Here they are one query, so a row the page withholds cannot appear in the file, and a row the file omits cannot be on the page.
What comes out
- Every row you can see, filtered exactly as you filtered the screen, in a form you can hand to an auditor without reformatting it.
- Refusals included, with their reason codes intact. An export of successes only would answer the least interesting half of the question.
- Nothing that was not on the screen. No hidden columns, no wider scope than the view you exported from.
What there is not
- No streaming into your SIEM yet. The export is something a person runs, not a feed you subscribe to. If continuous ingestion is a requirement, it is a build rather than a setting, and we would rather say that before you plan around it.
- No alerting on a refusal. The findings sweep raises configuration issues for your administrators to acknowledge; it does not page anybody at three in the morning.
Two things people assume are missing
Rejected credentials. And how long any of it is kept.
A rejected credential is recorded too
A request whose token fails validation never reaches the part of the system that decides permission, so in most designs it lands in an application log and nowhere a client can read. It is written to the trail here, with no person attached because there is no verified person. The practical consequence: somebody running credentials against your connector is visible to you, in the same place as everything else, rather than to us in a log file.
Retention, and why there are two answers
Your business data has no retention period here because no copy of it is kept. The trail itself is retained on a period agreed with you and pruned by a job that runs as a different database principal from the web application, so a compromised web process cannot erase what it did. Both halves of that are in data handling with the current numbers, which is where they can be kept correct.