Skip to main content
Every decision made through Attesta — whether auto-approved, manually approved, or denied — is recorded in a tamper-proof audit trail. The log uses a SHA-256 hash chain where each entry’s integrity depends on all previous entries, making it impossible to modify or delete records without detection.
Attesta supports pluggable audit backends. The default is a built-in SHA-256 hash-chained JSONL logger (described below). You can also use TrailProof as an alternative backend for enhanced features like HMAC signing and multi-tenancy. See the TrailProof Integration Guide for details.

Hash Chain Mechanism

Each audit entry contains a chain_hash computed from the previous entry’s hash and the current entry’s canonical JSON:
chain_hash = SHA-256(previous_hash + canonical_json(entry))
The first entry in the chain uses a genesis hash of "0" * 64 (64 zeros) as its previous_hash.
Entry 1: hash = SHA-256("0000...0000" + json_1)
Entry 2: hash = SHA-256(hash_1 + json_2)
Entry 3: hash = SHA-256(hash_2 + json_3)
...
Canonical JSON means keys are sorted alphabetically, no extra whitespace, and consistent encoding. This ensures the same data always produces the same hash regardless of serialization order.

Storage Format

The audit trail is stored as JSONL (JSON Lines) — one JSON object per line, one line per entry. This format is:
  • Append-only — New entries are appended, never inserted
  • Streamable — Can be processed line-by-line without loading the entire file
  • Grep-friendly — Standard text tools work out of the box
{"entry_id":"e1a2b3c4","chain_hash":"a9f3...","previous_hash":"0000...","action_name":"get_users","risk_score":0.12,...}
{"entry_id":"f5d6e7a8","chain_hash":"b4c7...","previous_hash":"a9f3...","action_name":"deploy_service","risk_score":0.65,...}
{"entry_id":"c9b0a1d2","chain_hash":"e8f2...","previous_hash":"b4c7...","action_name":"drop_table","risk_score":0.91,...}

AuditEntry Fields

Each entry in the audit trail contains these fields:
FieldTypeDescription
entry_idstringUnique identifier for this entry
chain_hashstringSHA-256 hash of this entry (includes previous hash)
previous_hashstringHash of the preceding entry (genesis: "0" * 64)
action_namestringName of the gated function
action_descriptionstringHuman-readable description of the action
agent_idstringIdentifier of the AI agent that initiated the action
risk_scorefloatComputed risk score (0.0–1.0)
risk_levelstringRisk level: low, medium, high, critical
challenge_typestringChallenge presented: confirm, quiz, teach_back, multi_party, or null
challenge_passedbooleanWhether the challenge was passed
approver_idslist[string]IDs of human approvers (empty for auto-approve)
verdictstringapproved, denied, or auto-approved
review_duration_secondsfloatTime between challenge presentation and response
min_review_metbooleanWhether the minimum review time was met
intercepted_atstringISO 8601 timestamp when the action was intercepted
decided_atstringISO 8601 timestamp when the decision was made
executed_atstringISO 8601 timestamp when the action executed (null if denied)
session_idstringSession identifier for grouping related actions
environmentstringExecution environment (development, staging, production)
metadataobjectArbitrary key-value pairs for custom data

Usage

from attesta import AuditLogger

# Initialize the audit logger
audit = AuditLogger("./audit.jsonl")

# Log an entry (typically called automatically by Attesta)
audit.log(
    action_name="deploy_service",
    action_description="Deploy api-gateway v2.1.0 to production",
    agent_id="deploy-bot",
    risk_score=0.65,
    risk_level="high",
    challenge_type="quiz",
    challenge_passed=True,
    approver_ids=["alice@company.com"],
    verdict="approved",
    review_duration_seconds=14.2,
    min_review_met=True,
    session_id="sess_abc123",
    environment="production",
    metadata={"service": "api-gateway", "version": "2.1.0"},
)

# Verify the entire chain integrity
is_valid = audit.verify_chain()
print(f"Chain valid: {is_valid}")  # True if no tampering

# Query entries
entries = audit.query(
    action_name="deploy_service",
    agent_id="deploy-bot",
    verdict="approved",
)

# Find rubber-stamped approvals (faster than min review time)
stamps = audit.find_rubber_stamps()
for entry in stamps:
    print(f"{entry.action_name}: reviewed in {entry.review_duration_seconds}s")

Chain Verification

The verify_chain() method walks the entire log from the genesis entry and recomputes every hash. If any entry has been modified, inserted, deleted, or reordered, the verification fails.
audit = AuditLogger("./audit.jsonl")

if not audit.verify_chain():
    print("ALERT: Audit trail has been tampered with!")
    # Investigate immediately

What Verification Catches

Tampering TypeDetection
Modified entryHash mismatch at the modified entry
Deleted entryHash mismatch at the entry after the deleted one
Inserted entryHash mismatch at the inserted entry
Reordered entriesHash mismatch at every reordered position
Truncated tailEntry count mismatch (if expected count is known)
Chain verification is a forward-only operation — it can detect tampering but cannot recover the original data. For production use, consider replicating the audit log to an immutable store (S3 with Object Lock, append-only databases, etc.).

CLI Verification

The Attesta CLI provides a convenient command for chain verification:
# Verify the audit trail
attesta audit verify ./audit.jsonl

# Output:
# ✓ Chain integrity verified (1,247 entries)
# ✓ No rubber stamps detected
# ✓ All minimum review times met

# Find rubber stamps
attesta audit rubber-stamps ./audit.jsonl

# Export entries for analysis
attesta audit export --agent deploy-bot --verdict denied ./audit.jsonl

Rubber-Stamp Detection

The find_rubber_stamps() method returns all entries where min_review_met is false — meaning the operator responded faster than the minimum review time for their challenge type.
stamps = audit.find_rubber_stamps()
print(f"Found {len(stamps)} rubber-stamped approvals")

for entry in stamps:
    print(
        f"  {entry.action_name} by {entry.approver_ids}: "
        f"{entry.review_duration_seconds:.1f}s "
        f"(min required: {entry.challenge_type} threshold)"
    )
Set up automated monitoring that runs find_rubber_stamps() on a schedule. A high rubber-stamp rate may indicate that operators are fatigued, that minimum review times are too aggressive, or that the risk scorer is generating too many false positives at higher risk levels.

Example Audit Entry

{
  "entry_id": "aud_7f3a9b2c",
  "chain_hash": "a9f3e7b2c4d6f8a1b3c5d7e9f1a3b5c7d9e1f3a5b7c9d1e3f5a7b9c1d3e5f7",
  "previous_hash": "0000000000000000000000000000000000000000000000000000000000000000",
  "action_name": "delete_user",
  "action_description": "Permanently delete user account usr_12345",
  "agent_id": "admin-bot",
  "risk_score": 0.82,
  "risk_level": "critical",
  "challenge_type": "multi_party",
  "challenge_passed": true,
  "approver_ids": ["alice@company.com", "bob@company.com"],
  "verdict": "approved",
  "review_duration_seconds": 47.3,
  "min_review_met": true,
  "intercepted_at": "2025-01-15T14:30:00.000Z",
  "decided_at": "2025-01-15T14:30:47.300Z",
  "executed_at": "2025-01-15T14:30:47.450Z",
  "session_id": "sess_abc123",
  "environment": "production",
  "metadata": {
    "user_id": "usr_12345",
    "reason": "GDPR right-to-erasure request"
  }
}

Challenge System

How challenges generate the audit data

CLI Audit Commands

Verify and query audit trails from the command line

TrailProof Integration

Switch to TrailProof for HMAC signing and multi-tenancy

Production Deployment

Best practices for audit logs in production