prev_hash (the previous row’s row_hash, or a genesis constant for the first) and row_hash = sha256(prev_hash + canonical JSON of the row’s own fields).
How to access: GuardForge > Audit log, and the “Verify chain” button on it.
Free/PRO: The log, the chain and the verifier are Free. Only the CSV export is Pro.
Notes:
- What the chain buys: edit one field on one row and that row no longer hashes to its stored
row_hash; delete a row and the next one points at aprev_hashnothing in the table produces. Neither is visible in the table itself, which is the point — an audit log an intruder can tidy up is decoration. - Rows written before the chain shipped have empty hashes.
verify_chain()reads an emptyrow_hashas “unhashed” rather than as a break, and treats the next hashed row as a fresh genesis. The screen reports “the chain restarted N times” only when N > 1: one restart is the first row and is normal. - Writes are serialised with a real cross-connection MySQL named lock (
GET_LOCK/RELEASE_LOCK, 3-second timeout, the name carrying$wpdb->prefixso a multisite install does not serialise unrelated sites). Two concurrent writers cannot read the same tail and fork the chain — a fork is indistinguishable from tampering to the verifier, and reporting an intrusion that did not happen is its own kind of failure. verify_chain()walks at most 50 000 rows in one run and says plainly that it was truncated rather than dying half-way onmax_execution_time. There is no resumable cursor: a second press re-checks the same oldest part.audit_log_keep_daysprunes the table in normal operation, so this ceiling is a guard rail, not the usual path.- The log deliberately skips other plugins’ bookkeeping options (
_last_run,_last_check, transients, cron, rewrite rules, its own keys). On the test stand a sibling plugin’s timer had taken 60 % of the log, and a log nobody can read is not an audit. Settings options are never skipped — those are exactly what an operator wants to see change.