Export and integrity
Exporting the log to CSV and JSON Lines by the current filter, the file format, and checking the hash chain with your own tools.
The log is exported with the “Export CSV” and “Export JSON” buttons in the section. Everything that matches the current filters is exported — with no size limit: the service walks the log page by page to the end rather than handing over “the first 10,000 rows”. The file is assembled in the browser and downloaded straight away; no “valid for 24 hours” links and no emails.
The export itself is an event too: auditlog.export appears in the log with the format, the filters and the number of rows.
CSV
For spreadsheets and reports. UTF-8 encoding with a BOM (Excel opens Cyrillic without any settings), comma as the separator, rows from oldest to newest. Columns:
| Column | Contents |
|---|---|
| ID | the entry number |
| Time (UTC) | ISO 8601 with microseconds, 2026-09-06T01:22:33.482911+00:00 |
| Time (local) | the same in your time zone — for reading |
| Organisation, Project | where it happened |
| Actor kind, Actor | member / connector / platform operator / system; login or identifier |
| Session | the member’s session identifier |
| IP, Browser | the connection’s address and User-Agent |
| Action, Name | the event code and its description from the catalogue |
| Operation, Severity | the class of action and its severity |
| Target kind, Target | what the action was aimed at |
| Description | the line a person sees |
| Result, Failure reason | success / failure and the reason |
| Details (JSON) | structured parameters |
| Previous entry hash, Entry hash | the links of the integrity chain |
JSON Lines
For a SIEM, scripts and archives: one entry is one line of JSON, exactly as the server stores it, from oldest to newest. The file extension is .jsonl. Fields:
{"id":12,"at":"2026-09-06T01:22:34.101532+00:00","org":"buff-demo","project":null,
"actor_kind":"user","actor":"demo-lead","session":"c2a5…","gid":"9821…","ip":"147.45.197.19",
"user_agent":"Mozilla/5.0 …","action":"auditlog.export","operation":"access","severity":"notice",
"target_kind":null,"target":null,"summary":"Выгружен журнал аудита (json)",
"detail":{"op":"auditlog_list","format":"json","filters":{},"rows":9},"ok":true,"error":null,
"prev_hash":"e5ac…","hash":"81df…"}actor_kind takes the values user, connector, operator, system; severity — info, notice, warning, critical; operation — create, access, modify, remove, authentication, transfer, execute.
Integrity
Each organisation’s entries form a chain: an entry’s hash is computed from the hash of the previous entry and the contents of the current one (SHA-256). Changing, inserting or deleting any entry in the middle breaks the chain — all subsequent hashes stop matching.
In the app. The “Verify integrity” button recomputes the chain on the server and reports how many entries were checked and whether the chain is intact; if not — the number of the first entry that does not match. The check is recorded in the log too (auditlog.verify).
With your own tools. An exported JSON Lines file contains everything needed to check the chain without us. An entry’s hash is computed like this:
- Take the entry fields
at, org, project, actor_kind, actor, session, ip, user_agent, action, operation, severity, target_kind, target, summary, detail, ok, error(detailas the object it is). - Serialise them to JSON with keys in alphabetical order, without whitespace, without escaping non-ASCII (
ensure_ascii=Falsein Python). - Hash = SHA-256 of the string
prev_hash + "\n" + serialisation(for an organisation’s first entryprev_hashis empty).
A check script for the file audit-log.jsonl:
import hashlib, json
FIELDS = ["at", "org", "project", "actor_kind", "actor", "session", "ip", "user_agent",
"action", "operation", "severity", "target_kind", "target", "summary", "detail",
"ok", "error"]
prev = None
for line in open("audit-log.jsonl", encoding="utf-8"):
e = json.loads(line)
body = json.dumps({k: e[k] for k in FIELDS}, sort_keys=True, separators=(",", ":"), ensure_ascii=False)
h = hashlib.sha256(((prev or "") + "\n" + body).encode("utf-8")).hexdigest()
if e["prev_hash"] != prev or e["hash"] != h:
raise SystemExit(f"цепочка нарушена на записи #{e['id']}")
prev = e["hash"]
print("цепочка цела, последний хэш", prev)The script expects a full export of the organisation from the first entry (“All time”, no filters): the chain runs through all entries in a row, and a filtered export can only be checked starting from an entry whose prev_hash you already know. Keep the last hash of every export: the next export must build on it — that way you make sure nothing disappeared in between.
Good to know
- The address is as the perimeter saw it. The IP is taken from the connection to our server (the edge node’s header), not from what the client says. A user behind a corporate proxy or VPN shows up with that proxy’s address.
- Time zone. In the file the time is in UTC; in the interface — in your browser’s time zone. Compare by the “Time (UTC)” column.
- Volume. The export runs in pages of 500 entries to the end; for a log of tens of thousands of entries that takes a few seconds. The button is locked while the export runs, and the number of rows is shown when it finishes.