Sipariş ve fatura kayıtları, cari hesap hareketleri, stok girişleri, personel giriş-çıkış (PDKS) kayıtları, randevu ve abonelik onayları, bir ERP'deki onay akışı: hepsi bir veri tabanında satır olarak durur. Uyuşmazlık genellikle dört sorudan biriyle gelir. Bu kayıt iddia edilen tarihte var mıydı? Sonradan değiştirildi mi? Silinen kayıt var mı? Değişikliği kim, hangi yoldan yaptı?
Kayıttaki tarih alanı neden tek başına yeterli değildir?
Çünkü o alan, kaydın geçmişini değil, en son yazılan değeri gösterir ve onu yazan da çoğunlukla incelenen uygulamanın kendisidir.
- Kim yazar:
created_atdeğeri ya uygulama kodundan ya da veri tabanının varsayılan değerinden (DEFAULT CURRENT_TIMESTAMP) gelir. İlkinde uygulama sunucusunun, ikincisinde veri tabanı sunucusunun saati kullanılmıştır. - Değiştirilebilir: Yetkili bir hesapla çalıştırılan tek bir
UPDATEile tarih alanı başka bir değere çekilebilir ve tablo bakıldığında hiçbir şey olağan dışı görünmez. - Saat dilimi: MySQL,
TIMESTAMPtüründeki değerleri bağlantının saat diliminden UTC'ye çevirerek saklar ve okurken geri çevirir;DATETIMEiçin bunu yapmaz. Aynı kayıt, farklı saat dilimi ayarıyla bağlanan iki araçta farklı saat gösterebilir. Ayrıntı: zaman damgaları ve saat dilimi. - Birim: Tamsayı sütunlarda saniye, milisaniye ya da mikrosaniye cinsinden Unix zamanı tutulabilir; birim, bilinen bir test kaydıyla doğrulanmadan okunmaz. Tek tek değerleri zaman damgası dönüştürücüyle çevirebilirsiniz.
Bu yüzden tarih alanını bir beyan olarak ele alır, doğrulamasını veri tabanının kendi işlem kayıtlarında ararız.
Veri tabanları değişiklikleri nerede kaydeder?
SQL veri tabanlarının çoğu, veriyi değiştiren işlemleri kurtarma ve replikasyon amacıyla ayrı bir işlem kaydına (transaction log) yazar. Adli açıdan bu kayıt, satırın geçmişine en yakın bağımsız kaynaktır; ne kadar geriye gittiği ise ürüne ve ayarlara bağlıdır.
| Sistem | İşlem kaydı | İnceleme açısından dikkat |
|---|---|---|
| MySQL | Binary log (mysqlbinlog ile okunur) | Yalnızca veriyi değiştiren işlemler; SELECT kaydedilmez. binlog_expire_logs_seconds varsayılanı 2.592.000 saniye (30 gün). Tüm sorguları tutan general query log varsayılan olarak kapalıdır. |
| PostgreSQL | WAL (pg_waldump) | Arşivleme (archive_mode) açık değilse gereksiz hâle gelen WAL dosyaları checkpoint sonrasında silinir ya da yeniden adlandırılıp kullanılır; wal_keep_size ve replikasyon yuvaları daha fazlasını tutabilir. UPDATE ve DELETE eski satır sürümünü hemen silmez; VACUUM çalışana kadar fiziksel sayfalarda kalabilir. |
| SQL Server | Transaction log | Simple recovery modelinde checkpoint sonrasında, full modelde log yedeğinden sonra log alanı boşaltılır (truncation). fn_dblog belgelenmemiş bir fonksiyondur; çıktısı dikkatli yorum ister. |
| Oracle | Redo ve arşivlenmiş redo (LogMiner, V$LOGMNR_CONTENTS) | Supplemental logging açık değilse bazı ayrıntılar eksik kalır. |
| SQLite (mobil ve masaüstü uygulamalar) | -wal dosyası | Veri tabanı -wal ve -shm dosyalarıyla birlikte kopyalanmalı; orijinali açmak checkpoint tetikleyip WAL'ı silebilir. secure_delete genelde kapalıdır, silinen içerik serbest sayfalarda kalabilir. |
| MongoDB | Oplog (local.oplog.rs) | Replika set üyelerinde tutulur. Boyutla sınırlıdır: Linux ve Windows'ta WiredTiger ile varsayılan boyut boş disk alanının %5'i (en az 990 MB, en fazla 50 GB); dolunca en eski girdiler silinir, ayrıca saat cinsinden asgari saklama süresi tanımlanmış olabilir. |
NoSQL veri tabanlarında durum farklı mı?
Çoğu zaman daha zordur, çünkü değişiklik geçmişi ürüne ve kurulum tercihine göre çok değişir. MongoDB'de oplog yalnızca replika set yapısında vardır; tek sunuculu, replika set olarak yapılandırılmamış bir kurulumda bu iz bulunmaz. Belgelerin _id alanındaki ObjectId değerinin ilk 4 baytı, değerin oluşturulduğu anı Unix zamanı cinsinden saniye olarak taşır. Bu yararlı bir işarettir ama iki sınırı var: ObjectId'yi çoğunlukla istemci tarafındaki sürücü üretir (yani uygulama sunucusunun saatini yansıtır) ve uygulama _id alanına istediği değeri yazabilir.
Anahtar-değer depoları, arama motorları ve bulut belge veri tabanlarında hangi geçmişin tutulduğu ürüne ve ayara göre değişir. Ön incelemede ilk sorduğumuz şey, sistemin değişiklik geçmişini nerede ve ne kadar süreyle tuttuğudur.
Audit kayıtları ve kullanıcı hareketleri
Audit kaydı, kimin hangi veriye ne zaman eriştiğini veya onu değiştirdiğini tutan kayıttır; ancak "kim" sorusunun cevabı kaydın hangi katmanda tutulduğuna bağlıdır.
Web ve kurumsal uygulamaların çoğu veri tabanına tek bir hizmet hesabıyla (ör. app_user) bağlanır. Veri tabanının kendi audit kaydında bütün işlemler bu hesapla görünür; işlemi hangi kişinin yaptığını ancak uygulama logları veya uygulamanın kendi tuttuğu işlem tablosu gösterir. Bu yüzden iki katmanı birlikte isteriz.
| Audit türü | Örnek | Zayıf yanı |
|---|---|---|
| Veri tabanına yerleşik | SQL Server Audit, PostgreSQL için pgAudit, MySQL Enterprise Audit | Kurulumda açılmamışsa hiç yoktur; açıksa bile uygulama hesabını gösterir |
| Tetikleyici (trigger) tabanlı | Her değişiklikte bir audit_log tablosuna satır yazan tetikleyici | Yetkili bir hesap tetikleyiciyi kapatabilir ya da audit tablosunu düzenleyebilir |
| Uygulama düzeyinde | Kullanıcı kimliğiyle tutulan işlem geçmişi tablosu | Uygulamayı atlayan doğrudan SQL değişikliklerini görmez |
Audit tablosunun kendisini de inceleriz: sıra numaralarında ve zamanda boşluk var mı? Burada dikkatli olmak gerekir. MySQL InnoDB'de geri alınan (rollback) bir işlemin ürettiği AUTO_INCREMENT değerleri yeniden kullanılmaz; yani ID dizisindeki bir boşluk tek başına silme göstermez.
Silinen ve değiştirilen kayıtlar değerlendirilebilir mi?
Çoğu zaman bir ölçüde; hangi ölçüde olduğunu saklama ayarları ve geçen süre belirler. Baktığımız kaynaklar, güvenilirlik sırasına göre kabaca şunlardır:
- İşlem kaydı: MySQL 8.4'ün varsayılan ayarlarında (
binlog_format=ROW,binlog_row_image=full) binlog, değişen satırın bütün sütunlarının önceki ve sonraki hâlini içerir. WAL ve transaction log da değişikliğin izini taşır, ama okunmaları daha çok uzmanlık ister. - Yedekler: Farklı günlere ait iki yedek karşılaştırıldığında hangi satırın ne zaman eklendiği, değiştiği ya da kaybolduğu dar bir aralıkta tarihlenir.
- Kopyalar: Replikalar, raporlama ve veri ambarı kopyaları, dışa aktarılmış dosyalar, sistemin gönderdiği e-posta bildirimleri.
- "Yumuşak silme": Birçok uygulama satırı silmez,
deleted_atya dais_deletedalanını işaretler. Bu durumda silme zamanı zaten tabloda durur. - Fiziksel izler: PostgreSQL'de VACUUM öncesindeki eski satır sürümleri, SQLite'ta serbest sayfalar. En az güvenilir ve en çok uzmanlık isteyen kaynaktır.
Bir kaydın silindiğini göstermek, silinen içeriği geri getirmekle aynı şey değildir. Raporda ikisini ayrı yazarız.
Veri bütünlüğü nasıl değerlendirilir?
Veri bütünlüğü iki ayrı soruyu kapsar: incelenen kopyanın alındığı andan beri değişmediği ve verinin kendi içinde tutarlı olduğu. Birincisi hash değerleriyle gösterilir (hash ve delil bütünlüğü). İkincisi daha çok şey anlatır, çünkü elle yapılan müdahaleler çoğu zaman bir tutarsızlık bırakır. Baktığımız tipik işaretler:
- Sıra numarası (
AUTO_INCREMENT, sequence) büyük olan bir kaydıncreated_atdeğeri, kendinden önceki kayıtlardan saniyeler değil günlerce eskiyse. Kayıt sonradan eklenip tarihi geriye çekilmiş olabilir; toplu içe aktarma da aynı görüntüyü verir. updated_atdeğericreated_at'ten önceyse.- Fatura toplamı kalemlerle, stok bakiyesi hareketlerle, cari bakiye hesap hareketleriyle tutmuyorsa.
- Başka tabloda karşılığı kalmamış (yetim) kayıtlar: silinmiş bir siparişin ödemesi, silinmiş bir kullanıcının oturumları.
- Ana tablodaki bir değişikliğin tetikleyiciyle tutulan audit tablosunda karşılığı yoksa.
Tutarsızlık tek başına müdahale kanıtı değildir; hatalı bir migration ya da yazılım hatası da aynı izi bırakır. Raporda gözlenen tutarsızlığı, olası açıklamalarıyla ve işlem kaydında doğrulanıp doğrulanamadığıyla birlikte yazarız.
Veri tabanı ve uygulama logları nasıl karşılaştırılır?
Karşılaştırmanın amacı, her veri tabanı değişikliğinin arkasında bir uygulama isteği olup olmadığını görmektir. Adımlar:
- Her kaynağın saat ayarını belirleriz: veri tabanı sunucusu (MySQL'de
@@global.time_zoneve@@session.time_zone, PostgreSQL'deSHOW timezone), uygulama sunucusu, log biçimi. Hepsini UTC'ye çeviririz. - Sorudaki kaydın işlem kaydındaki bütün olaylarını çıkarırız: oluşturma, her güncelleme, silme.
- Uygulama logunda aynı aralıktaki istekleri, kullanıcı kimliklerini ve istek kimliklerini buluruz.
- Olayları eşleştiririz ve eşleşmeyenleri ayırırız. Uygulama isteği olmayan bir değişiklik; doğrudan SQL istemcisi, yönetim aracı, zamanlanmış görev ya da migration betiği olabilir. Hangisi olduğunu bağlantı ve audit kayıtlarından anlamaya çalışırız.
- Sonucu tek bir kronolojide birleştiririz: her satırda UTC ve Türkiye saati, kaynak, olay ve kanıt.
Bir e-ticaret veri tabanında 1042 numaralı siparişin tutarı iki kez değişmiş olsun. Binlog'da 11:02:11 ve 20:47:05 UTC'de iki UPDATE görünür. İlki aynı saniyede PUT /api/orders/1042 isteğiyle ve bir müşteri hizmetleri kullanıcısının oturumuyla eşleşir. İkincisinin karşılığı uygulama logunda yoktur; audit kaydına göre dbadmin hesabıyla 203.0.113.40 adresinden açılmış bir bağlantıdan yapılmıştır. Rapor bu iki değişikliği ayrı ayrı, kanıtlarıyla anlatır; ikincisini kimin yaptığı sorusunu ise o hesabı kimlerin kullanabildiği bilgisine bağlı olarak açık bırakır.
Uygulama, API ve kimlik doğrulama loglarının kendisine ilişkin sorular için log analizi, bu kayıtları üreten kodun davranışı için kaynak kod incelemesi sayfalarımıza bakabilirsiniz.
Materyal nasıl alınmalı?
Önce izler korunur, sonra sorulur. Günlük bakım işleri (log temizliği, VACUUM, yedek rotasyonu) inceleme süresince incelenecek kayıtları silebilir; bunları sistem yöneticinizle birlikte gözden geçiririz.
- Tutarlı bir yedek veya dump; mümkünse ek olarak fiziksel yedek.
- İşlem kayıtları: binlog dosyaları, WAL arşivi, transaction log yedekleri. Süreleri dolmadan.
- Yapılandırma:
my.cnf,postgresql.conf, recovery modeli, saat dilimi ve audit ayarları. - Uygulama logları, şema ve migration geçmişi.
- Veri tabanı hesaplarının ve yetkilerinin listesi: hangi hesapla kimler bağlanabiliyor.
- Mevcut yedeklerin listesi, tarihleriyle.
Her dosyanın SHA-256 değeri alınır ve inceleme kopyalar üzerinde yapılır; SQLite dosyaları ise -wal ve -shm dosyalarıyla birlikte, salt okunur olarak açılır. Dosyaların hash değerini hash hesaplama aracıyla kendiniz de kaydedebilirsiniz.
Raporda neler yer alır?
Kayıt bazında kronoloji tablosu, uygulama isteğiyle eşleşen ve eşleşmeyen değişiklikler, silme göstergeleri ve kurtarılabilen içerik, kullanılan yöntem ve araç sürümleri, erişilemeyen kayıtlar ve bunların hangi soruyu cevapsız bıraktığı. Rapor, kayıtların teknik güvenilirliğine ilişkin tespitleri içerir; bunların hukuki değerlendirmesi avukatınıza ve mahkemeye aittir. Raporumuz avukatınız tarafından uzman görüşü olarak dosyaya sunulabilir.