Seorang pelanggan TokoKita mengeluh pesanannya batal padahal sudah dibayar, dan pertanyaan tim sederhana: siapa yang mengubah status pesanan itu, kapan, dan dari nilai apa menjadi nilai apa? Log aplikasi biasa tidak bisa menjawab — log tersebut dirancang untuk debugging, isinya segera dirotasi, dan siapa pun dengan akses server bisa menyunting barisnya. Audit log berbeda: catatan siapa-melakukan-apa-terhadap-data-apa-kapan yang sifatnya append-only, disimpan lama, dan dilindungi dari modifikasi. Pada bab ini kita membangun audit log berformat JSONL dengan hash chain, lalu membuktikan bahwa perubahan sekecil apa pun pada isi log terdeteksi saat integritasnya diverifikasi.

Log Aplikasi, Log Pipeline, dan Audit Log #

Tiga jenis log ini sering tertukar karena sama-sama berisi kejadian berwaktu. Log aplikasi dan log pipeline (yang sudah kita bahas di Chapter Pipeline Logging) menjawab pertanyaan teknis: apakah job sukses, berapa lama durasinya, galat apa yang muncul. Audit log menjawab pertanyaan akuntabilitas: siapa aktornya, sumber daya apa yang disentuh, dan perubahan apa yang terjadi. Konsekuensinya berbeda pada tiga hal: volume audit log jauh lebih kecil (hanya event bermakna), umur simpannya jauh lebih panjang (retention policy kita di bab sebelumnya justru mengarsipkannya lima tahun), dan akses tulisnya hanya lewat satu jalur.

AspekLog aplikasi/pipelineAudit log
Menjawab“apakah sistem sehat?”“siapa mengubah apa, kapan?”
Isigalat, durasi, metrikaktor, aksi, resource, sebelum-sesudah
Volumebesar, dirotasi cepatkecil, disimpan lama
Sifat tulisboleh ditimpaappend-only, immutable

Event yang Wajib Diaudit #

Tidak semua kejadian perlu masuk audit log — kalau semuanya dicatat, sinyal tenggelam di antara derau. Lima kategori berikut adalah yang kita wajibkan di TokoKita:

EventContoh rekamAlasan wajib
Baca data sensitifsiti.analyst membaca tabel.pelangganmendeteksi akses tidak semestinya
Ubah datastatus pesanan diproses menjadi dikirimrekam sebelum-sesudah untuk sengketa
Export datadewi.auditor mengekspor laporan.bulananjejak data keluar dari sistem
Ubah hak aksesgrant_role analis untuk siti.analystakuntabilitas atas administrasi RBAC
Hapus datapartisi pesanan.tanggal=2025-05-01 dihapusbukti kepatuhan retention policy

Identitas aktor harus berasal dari authentication yang sudah dibahas di Chapter Authentication dan Authorization — nama service account pun dicatat apa adanya, karena “pipeline” bukan jawaban yang diterima auditor. Event penghapusan di baris terakhir adalah penghubung langsung ke bab sebelumnya: setiap eksekusi retention policy wajib menghasilkan rekam audit.

Struktur Rekam: Hash Chain #

Setiap rekam disimpan sebagai satu baris JSON (JSONL) berisi seq, waktu, aktor, aksi, resource, hash isi data sebelum dan sesudah, plus dua hash pengaman: prev_hash yang menyalin hash rekam sebelumnya, dan rekam_hash yang dihitung dari seluruh isi rekam itu sendiri. Hasilnya seperti rantai: rekam kedua menunjuk rekam pertama, rekam ketiga menunjuk rekam kedua, dan seterusnya dari rekam genesis. Mengubah isi satu rekam mana pun akan membuat rekam_hash hasil hitung ulang berbeda dari yang tersimpan, dan verifikasi cukup mengulang hitungan dari genesis untuk menemukannya — pola yang sama dipakai Git pada commit dan blockchain pada blok.

Implementasi: JSONL dengan Hash Chain #

Skrip berikut mencatat lima event sehari di TokoKita, menyimpannya ke audit.jsonl, memverifikasi rantainya, lalu mensimulasikan seorang pengguna yang mengubah hasil rekam kedua diam-diam dan menyimpan ulang berkas:

python
"""Audit log append-only dengan hash chain: catat event, verifikasi, deteksi modifikasi."""
import hashlib
import json
from pathlib import Path

BERKAS = Path(__file__).parent / "audit.jsonl"
GENESIS = "0" * 64


def hash_konten(nilai):
    """Hash isi data (bukan nilai mentahnya) agar PII tidak ikut tertulis ke log."""
    payload = json.dumps(nilai, sort_keys=True, separators=(",", ":"))
    return hashlib.sha256(payload.encode()).hexdigest()


def catat(log, waktu, aktor, aksi, resource, sebelum, sesudah):
    """Tambah satu rekam audit; rekam_hash mengunci seluruh isi rekam."""
    rekam = {
        "seq": len(log) + 1,
        "waktu": waktu,
        "aktor": aktor,
        "aksi": aksi,
        "resource": resource,
        "sebelum_hash": hash_konten(sebelum),
        "sesudah_hash": hash_konten(sesudah),
        "prev_hash": log[-1]["rekam_hash"] if log else GENESIS,
    }
    rekam["rekam_hash"] = hash_konten(rekam)
    log.append(rekam)
    return rekam


def tulis(log):
    BERKAS.write_text("".join(json.dumps(r, sort_keys=True) + "\n" for r in log))


def verifikasi():
    """Ulangi perhitungan hash dari genesis; hentikan di rekam pertama yang tidak cocok."""
    log = [json.loads(baris) for baris in BERKAS.read_text().splitlines()]
    prev = GENESIS
    for rekam in log:
        tersimpan = rekam.pop("rekam_hash")
        if hash_konten(rekam) != tersimpan:
            return f"GAGAL: hash rekam {rekam['seq']} tidak cocok - isi rekam dimodifikasi"
        if rekam["prev_hash"] != prev:
            return f"GAGAL: prev_hash rekam {rekam['seq']} terputus dari rantai"
        prev = tersimpan
    return f"OK: {len(log)} rekam, rantai hash utuh dari genesis"


def jalankan():
    log = []
    catat(log, "2026-08-01T09:02:11+07:00", "siti.analyst", "read",
          "tabel.pelanggan", None, None)
    catat(log, "2026-08-01T09:14:05+07:00", "budi.engineer", "update",
          "pesanan.id=1001", {"status": "diproses"}, {"status": "dikirim"})
    catat(log, "2026-08-01T09:31:40+07:00", "dewi.auditor", "export",
          "laporan.bulanan", None, None)
    catat(log, "2026-08-01T10:05:19+07:00", "admin", "grant_role",
          "role.analyst->siti.analyst", None, None)
    catat(log, "2026-08-01T11:00:00+07:00", "budi.engineer", "delete",
          "pesanan.tanggal=2025-05-01", {"partisi": "aktif"}, {"partisi": "dihapus"})
    tulis(log)
    contoh = log[1]
    print(f"{len(log)} event tercatat ke {BERKAS.name}")
    print(f"Contoh rekam seq={contoh['seq']}: prev_hash={contoh['prev_hash'][:12]}..."
          f" rekam_hash={contoh['rekam_hash'][:12]}...")
    print("Verifikasi:", verifikasi())
    # Skenario: seseorang mengubah hasil rekam ke-2 diam-diam lalu menyimpan ulang.
    log[1]["sesudah_hash"] = hash_konten({"status": "dibatalkan"})
    tulis(log)
    print("Setelah rekam 2 dimodifikasi, verifikasi:", verifikasi())


if __name__ == "__main__":
    jalankan()

Output yang Dihasilkan:

text
5 event tercatat ke audit.jsonl
Contoh rekam seq=2: prev_hash=c55349264a3c... rekam_hash=d6680110c42d...
Verifikasi: OK: 5 rekam, rantai hash utuh dari genesis
Setelah rekam 2 dimodifikasi, verifikasi: GAGAL: hash rekam 2 tidak cocok - isi rekam dimodifikasi

Mari kita telaah baris-baris kode di atas:

  1. Baris 10-13 (hash_konten): satu fungsi hash untuk semua kebutuhan — isi data sebelum/sesudah maupun rekam penuh. sort_keys=True memastikan JSON yang sama selalu terserialisasi identik, syarat mutlak agar hash bisa dihitung ulang.
  2. Baris 24-26 (hash sebelum, sesudah, prev): nilai data tidak disimpan mentah — hanya hash-nya. prev_hash menyalin rekam_hash rekam terakhir, atau GENESIS berupa 64 karakter nol untuk rekam pertama.
  3. Baris 28 (rekam_hash): dihitung setelah semua field lain terisi, sehingga satu hash ini mengunci seluruh isi rekam. Mengubah satu karakter saja di field mana pun membuat hash ini tidak cocok lagi.
  4. Baris 37-48 (verifikasi): memuat ulang berkas, lalu untuk tiap rekam mengeluarkan rekam_hash tersimpan dan menghitung ulang hash isi rekam. Dua kegagalan dibedakan tegas: isi rekam berubah, atau rantai terputus karena rekam disisipkan/dihapus.
  5. Baris 69-72 (skenario modifikasi): sesudah_hash rekam kedua diganti dengan hash status dibatalkan, lalu berkas disimpan ulang. Karena rekam_hash tidak dihitung ulang oleh si pengubah, verifikasi langsung menemukan ketidakcocokan di rekam 2.

Panduan lengkap manajemen log keamanan — retensi, rotasi, dan perlindungan server log — tersedia di NIST SP 800-92 . Audit trail memberi kita jawaban atas “siapa dan kapan”, tetapi pertanyaan berikutnya biasanya “aset data apa saja yang sebenarnya ada di sistem ini” — dan untuk itu kita butuh inventaris bernama Chapter Data Catalog.