Buff Development
Audit log

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.

An expanded entry: all the fields, the details in JSON, the entry hash and the previous hash. At the top — the result of “Verify integrity”.

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:

ColumnContents
IDthe 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, Projectwhere it happened
Actor kind, Actormember / connector / platform operator / system; login or identifier
Sessionthe member’s session identifier
IP, Browserthe connection’s address and User-Agent
Action, Namethe event code and its description from the catalogue
Operation, Severitythe class of action and its severity
Target kind, Targetwhat the action was aimed at
Descriptionthe line a person sees
Result, Failure reasonsuccess / failure and the reason
Details (JSON)structured parameters
Previous entry hash, Entry hashthe 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:

  1. Take the entry fields at, org, project, actor_kind, actor, session, ip, user_agent, action, operation, severity, target_kind, target, summary, detail, ok, error (detail as the object it is).
  2. Serialise them to JSON with keys in alphabetical order, without whitespace, without escaping non-ASCII (ensure_ascii=False in Python).
  3. Hash = SHA-256 of the string prev_hash + "\n" + serialisation (for an organisation’s first entry prev_hash is 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.

On this page