Skip to content

Audit System

lash includes a tamper-evident audit system for logging hook events. The audit log is a single append-only JSONL file at ~/.lash/audit.log, protected by a secret-ratcheted hash chain. Implemented as a built-in plugin.

salt = random 16-byte hex
secret_0 = KDF(password, salt) -- 600,000 iterations HMAC-SHA256
hash_1 = HMAC-SHA256(secret_0, content_1)
secret_1 = HMAC-SHA256(secret_0, hash_1) -- ratchet forward
hash_2 = HMAC-SHA256(secret_1, content_2)
secret_2 = HMAC-SHA256(secret_1, hash_2)
...

Each secret ratchets forward — only the current value exists. Previous secrets are destroyed.

ScenarioOutcome
Attacker has log onlyCannot forge any hash
Attacker has key file onlyCan forge future, not past
Attacker has bothCan forge future, past is locked
User verifies with passwordFull chain validated

The password is never stored. A verification hash (derived from password + ":verify") is stored for password-gated role escalation.

~/.lash/audit.key (permissions 0600):

<salt>:<64-char hex secret>:<entry count>:<password verify hash>

Updated atomically (write .tmp, then rename()). On startup, entry count is compared against actual log lines; a mismatch indicates tampering.

{
"action": "pipeline.pre_execute",
"ts": "2026-03-06T14:30:00.123",
"seq": "42",
"sid": "s_123",
"command": "ls -la",
"cwd": "/home/user",
"hash": "a3f2...c9d8"
}
FieldDescription
actionHook event name
tsTimestamp (ISO 8601 extended)
seqSequence number (monotonically increasing)
sidSession ID
hashHMAC-SHA256 hash from the ratcheted secret chain

The audit plugin observes: session.connect, session.disconnect, pipeline.pre_execute, pipeline.post_execute, variable.set, variable.drop, job.background, job.foreground, error.dispatch.

All audit commands require the auditAccess capability. The password is collected interactively (echo disabled) — never via CLI args or env vars.

CommandDescription
audit initInitialize audit system. Generates salt, derives secret, writes key file.
audit verifyRecomputes full ratchet chain, verifies every entry. Exit 0 if intact, 1 if tampered.
audit rotateVerifies log, deletes if valid, resets chain with new salt.
Audit Report
===================================
Entries: 1247
Period: 2026-01-15T08:00:00 -> 2026-03-06T14:30:00
Status: INTACT
Events by type:
pipeline.post_execute: 489
pipeline.pre_execute: 489
session.connect: 120
session.disconnect: 118
Violations: 0

On startup, the audit plugin warns via stderr when thresholds are exceeded:

ConditionDefault Threshold
File size50 MB
File age7 days
Entry count mismatch(always)

Configure thresholds in ~/.lash/config:

[settings]
audit.max_size_mb = 50
audit.max_age_days = 7
Audit LogSession Logs
PurposeTamper-evident security trailCommand history/replay
ScopeAll sessions, all hook eventsOne session’s commands
IntegritySecret-ratcheted hash chainPlain JSONL
AudienceSysadmins, complianceThe user
Location~/.lash/audit.log~/.lash/sessions/*.jsonl