Skip to main content

Browsing & Verification

The plugin is the only Filament audit plugin built around cryptographic verification rather than a mutable activity feed.

Read-only by construction

The audit log resource is browse-and-view only. There are no Create or Edit pages, every mutation ability (canCreate/canEdit/canDelete/canDeleteAny/canReplicate) is hard-denied, and an EntryPolicy denies create/update/delete/restore/forceDelete/replicate for any caller - defence in depth behind core's own immutable entries.

Browsing

The Audit Log resource lists entries newest-first with columns for sequence #, recorded time, action, actor, subject, and verification status. Filters cover action, actor type, subject type, recorded date range, and verification state.

Actor and subject labels come from core's Chronicle::resolveReference() (honouring morph maps, no extra query - see Reference Resolution). Override per panel with ->labelResolver().

The detail view is a read-only infolist: Identity, Integrity (current/previous/payload hashes), Signature (algorithm, key id, signature read from the entry's checkpoint, shown as Unanchored when none), Payload, and a Decrypted section rendered through core's decrypted*() accessors with an erased-subject indicator (see Crypto-Shredding).

Verification

Verification is always deliberate - nothing verifies on a read or render path.

ActionWhereScope
Verify chainHeader actionThe full ledger from genesis
Verify entryRow actionA single entry
Verify segmentBulk actionThe selected span

The Verify segment bulk action reduces the selected rows to a [min, max] sequence span and calls core's verifyEntryRange, which anchors on the enclosing signed checkpoints - never on a selected row's stored hash. See Scalable Verification.

Chain and segment verifies covering more than verification.queue_threshold entries (default 1000) are dispatched to the queue and notify you on completion.

Status badges and the health widget

Results are written to a plugin-owned, DB-backed store and surfaced as badges - Verified / Failed / Unverified / Stale (stale once newer entries are appended) - with a tooltip showing the last-verified time and decoded failure. Badge rendering is primed in a single query, so it stays N+1-free at volume.

The VerificationHealthWidget summarises the last-verified time, pass/fail, and a cheap checkpoint spine check (O(checkpoints)).

External anchoring

Since the plugin's v1.1, the panel surfaces core's external checkpoint anchoring. Like verification, anchor checks are deliberate and read-only - nothing contacts an anchor provider on a render path. All anchor surfaces are hidden unless core anchoring is enabled (they follow chronicle.anchoring.enabled; override with ->anchoring(true|false)).

The entry detail view gains a read-only External anchoring section listing the entry's checkpoint's anchors - per anchor the provider, a status badge, anchored_at, reference, and a copyable proof. It reads stored anchor status only, and degrades to Unanchored, No anchors, or Anchoring not configured.

Verification is exposed two ways, both gated by the same ->authorize() closure as the chain/entry/segment actions:

ActionWhereScope
Verify anchorRow + detail headerOne entry's checkpoint, via core's AnchorVerifier::checkpointHasValidAnchor()
Verify all anchorsList-page headerEvery in-scope checkpoint, via AnchorVerifier::verify()

"Verify all anchors" runs synchronously at or below anchoring.verify_all_queue_threshold (default 1000 checkpoints) and on the queue above it, notifying you on completion.

An Anchor badge column and matching filter read the checkpoint's stored anchor status (no provider call, no per-row query), and an AnchorCoverageWidget summarises coverage from cheap aggregates - checkpoints anchored vs total, plus pending/failed counts and the latest anchored_at.

To populate any of this, core anchoring must be configured - RFC 3161 TSA or the S3 Object Lock adapter. With none configured, every entry shows as Unanchored and the surfaces stay hidden.

Signing-key visibility

Since the plugin's v1.2, the panel surfaces signing-key rotation - which key signed each entry, drawn from its checkpoint. This is display-only: signature verification already happens inside chain/entry verification (the verifiers resolve each checkpoint's key through core's KeyRing), so these surfaces read key metadata only and never sign or verify. Toggle them with ->signingKeys(true|false) (default on).

The entry table gains a Signing key column - the entry's checkpoint.key_id as a state-coloured badge with the algorithm, and an Unsigned placeholder when the entry has no checkpoint. A matching filter lists the configured keys (labelled algorithm:keyId, the active one marked (active)) and narrows by key. The column reads the already eager-loaded checkpoint, so there's no per-row query.

Each key reads as one of three states:

StateMeaning
ActiveSigned by the key core is currently signing new checkpoints with
RetiredSigned by an earlier key - still kept in the ring to verify historical entries
UnsignedThe entry has no checkpoint yet

The entry detail view badges the same Active/Retired state beside the key id (with a note that retired keys still verify historical artifacts), and a SigningKeyRingWidget on the list page summarises the ring: the active key, its size, the number of retired keys, and the active key's checkpoint coverage. See core's Signing & Keys and key rotation for how the ring is configured and rotated.

Crypto-shredding & GDPR erasure

Since the plugin's v1.3, the panel surfaces core's crypto-shredding state and can action a GDPR erasure. These surfaces appear only when core encryption is configured; toggle the read-only surfaces with ->cryptoShredding() (defaults to following core).

The read-only surfaces never unwrap a DEK or decrypt anything - they read the subject key's status only:

  • An Erasure column (Encrypted / Erased / Not encrypted, with an On hold indicator) and filters by erasure state and legal hold. State is primed once per page, so the column stays query-flat with no per-row lookup.
  • A Subject erasure detail section - state, wrapping kek_id, erased_at, and active legal-hold status. An erased subject is shown as permanently unreadable while its entry stays intact and still verifies.
  • An Erasure proofs only preset filtering to the subject.erased proof entries, with requester and reason.
  • A CryptoShreddingWidget summarising encrypted / erased / on-hold subjects and the active KEK.

The erase action

The panel's only write is an opt-in Erase subject (GDPR Article 17) action calling core's Chronicle::eraseSubject(). It does not mutate the ledger - core destroys the subject's DEK and appends a hash-chained subject.erased proof, leaving every existing entry (and its hash and signature) unchanged and still verifiable.

It is fenced deliberately, and each guard is enforced in the action's visibility and re-checked at execution:

GuardBehaviour
Off by defaultAbsent unless ->erasure(true) (config default false)
Authorized separately->eraseAuthorize() defaults to deny; never the verify gate
ConfirmationType the exact subject_type:subject_id + a mandatory reason
Legal holdBlocks by default; overridable only via ->eraseAllowHoldOverride() + a distinct checkbox
IdempotentRe-erasing an already-shredded subject is a no-op

Enabling requires both ->erasure(true) and an ->eraseAuthorize() closure that grants - the config flag alone never makes it reachable. See core's GDPR erasure and crypto-shredding for what erasure does to the key store and the audit trail.

Theming

The panel uses Filament's native CSS variables and utility classes only - no npm, no asset compilation, no required custom theme. It adopts your panel's primary color and dark mode automatically.

See also