Help Center
Audit Log

Audit Log

The audit log records who did what, and when: offboarding actions, access revocations, sign-ins, exports and report downloads, settings changes. It is under Audit Log in the sidebar. Viewing it needs the View audit log permission (Super Admins, Admins and HR by default); exporting needs Export audit log (Super Admins and Admins by default).

The hash chain

Each entry's hash covers the entry and the hash of the entry before it, so changing or removing a past entry breaks the chain. On Team and Enterprise, Verify Chain on the audit log page walks the whole chain on PurpletGo's servers. The chain is keyed with a PurpletGo secret, so only PurpletGo can recompute an entry's hash. What you can check yourself, without that secret, is below.

Checking an export yourself

Export CSV and Export JSON download one ZIP file with:

FileWhat it is
purpletgo-audit-log-<time>.json (or .csv)The export
<export>.sigPurpletGo's Ed25519 signature over the export's exact bytes
verification.jsonWhat was signed, the export's SHA-256 and signature, the last entry's id and hash, PurpletGo's signed chain anchors, and the steps below
signing-key.pemPurpletGo's public key

The export's SHA-256 is also written into the audit log, in the audit_log_exported entry for the download.

What is signed

  • The export: the file's bytes exactly as downloaded. Opening a CSV in a spreadsheet and saving it again changes the bytes, so keep the original.

  • Each chain anchor: an anchor records the chain's newest entry (its id and hash) at a point in time. PurpletGo records one every hour when the chain has moved, every night, and with every archive. Its signature covers this text, in UTF-8, each line ending in a newline:

    purpletgo-audit-anchor/v1
    org_id=<your organization id>
    entry_id=<entry id>
    entry_hash=<entry hash>
    anchored_at=<time, ISO 8601 in UTC with milliseconds>

How to verify

Check the key

Compare the key id in verification.json (public_key.key_id) with the one PurpletGo publishes, and with GET /api/audit-log/signing-key. The key id is the first 16 hex characters of the SHA-256 of the public key (its SubjectPublicKeyInfo, DER).

Check the export's signature

With OpenSSL 3.0 or later (the LibreSSL that ships with macOS can't verify Ed25519; brew install openssl@3):

openssl pkeyutl -verify -pubin -inkey signing-key.pem -rawin \
  -in purpletgo-audit-log-<time>.json -sigfile purpletgo-audit-log-<time>.json.sig

Check the chain against a signed anchor

In a JSON export, each row's prev_hash must equal the entry_hash of the row before it, and the last row's entry_hash must equal the entry_hash of a signed anchor for the same entry. That anchor's signature must verify with the same key over the text above. Then the export is the chain as PurpletGo attested it when the anchor was recorded. Entries written after the newest anchor are covered by the next one, within the hour.

The verifier script

server/scripts/verifyAuditExport.js in the PurpletGo repository runs every check above. It is one file with no dependencies beyond Node.js 18 or later, so you can copy it anywhere and read it first:

node verifyAuditExport.js purpletgo-audit-log-<time>-json.zip
# or, with the key you fetched yourself rather than the one in the bundle:
node verifyAuditExport.js <unzipped folder> --key signing-key.pem

It prints PASS, FAIL or SKIP for each check and exits with 0 only when nothing failed.

Anchors on their own

GET /api/audit-log/anchors lists your organization's anchors, newest first, each with its signature. See the Audit Log API.

Verify the chain from a JSON export. In a CSV, a cell starting with =, +, - or @ gets a leading tab so spreadsheets don't run it as a formula, so those cells no longer match the hashed values. The PDF is a readable copy; its Verification section, before the entries, says what was signed and names the newest signed anchor.

Archives

On Team and Enterprise, PurpletGo archives the audit log to cloud storage every night, and a Super Admin can choose Archive Now. Each archive holds the entries written since the previous one, so every entry is archived once. Each is signed like an export, and its last entry is recorded as a signed anchor.

How long archives are kept

Each archive copy is kept for your organization's archive retention period, counted from the day it is made, and then deleted. The period is 7 years (2,555 days) by default and can't be shorter than 365 days. A Super Admin changes it under Audit Log → Archive Retention; the change applies to archives made from then on, and is recorded in the audit log. The archive list shows when each copy will be deleted.

  • Legal hold: while any case in your organization is under legal hold (or within 7 days of its release), no archive copy is deleted, even past its period. They are deleted once the hold has ended.
  • When your organization is deleted: archive copies are not deleted with it. Each keeps running down its own period and is then deleted. Personal details of erased people in them are already unreadable (Data Retention: the audit log).
  • Every deletion of an archive copy is recorded in PurpletGo's own platform audit log.
⚠️

The retention period and what happens to archives after the end of the service are pending legal review (DPA clause 10.4).

Related pages