# 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.

_Illustration: 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:

| 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](/en/docs/audit-log/events) |
| 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:

```json
{"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`:

```python
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.
