Tüm yazılar
Veri Otomasyonu 6 dk okuma

Gerçek Veriyle Test Etmeyi Bırakmak: Maskeleme ve Sentetik Veri Üretimi

Test ortamına gerçek müşteri verisi kopyalamak yerine tuzlu HMAC ile tutarlı takma adlandırma ve tohumlu sentetik veri üretimi: Python kodu ve KVKK boyutu.

#python #automation #data-pipeline

Geliştirme, test ve demo ortamlarına üretim veritabanının bir kopyasını atmak hâlâ en yaygın alışkanlık. Veri maskeleme ve sentetik test verisi üretimi, bu alışkanlığın yerine geçen iki yöntemdir: ilkinde gerçek kayıtlar biçimi bozulmadan başka değerlere dönüştürülür, ikincisinde gerçek veriye hiç dokunmadan şemaya ve dağılıma uyan yeni kayıtlar üretilir.

İki yöntemin ortak noktası, geliştiricinin elindeki verinin bir müşteriye ait olmamasıdır. Farkları ise geri döndürülebilirlik ve gerçekçilik ekseninde ortaya çıkar. Maskelenmiş veri gerçek dağılımı korur ama hukuken kişisel veri olmaya devam edebilir; sentetik veri hukuken temizdir ama dağılımı siz ne kadar iyi modellediyseniz o kadar gerçekçidir.

Her iki yöntemi aşağıda yalnızca Python standart kütüphanesiyle kuruyorum: tuzlu HMAC ile tutarlı takma adlandırma, kontrol haneleri doğru TCKN benzeri değer üretimi ve tohumla tekrarlanabilir bir sentetik üretici. Kapanışta KVKK açısından takma adlandırma ile anonimleştirme arasındaki farkı ayırıyorum. Veri ve otomasyon danışmanlığı işlerinde bu ayrım, hangi ortama hangi verinin gidebileceğini belirleyen ilk soru oluyor.

Test ortamına gerçek veri kopyalamak neden sorun?

Test ortamına gerçek veri kopyalamak, üretimdeki tüm güvenlik kontrollerini olmayan bir yere kişisel veriyi taşımak demektir. Üretim veritabanı erişim kaydı tutar, yedekleri şifrelidir, kim ne zaman baktı bellidir. Geliştirici makinesindeki SQLite dosyası, paylaşılan staging sunucusu ya da bir demo laptopu için bu kontrollerin hiçbiri geçerli değildir.

Risk yalnızca sızıntı değildir. Sık görülen tuzaklar şunlardır:

  • Test ortamındaki e-posta gönderim betiği yanlışlıkla gerçek müşterilere mesaj atar.
  • Ekran görüntüsü, hata kaydı ya da demo videosu gerçek ad ve telefon içerir.
  • Geliştirici ayrıldığında dizüstü bilgisayarında kopya kalır; kimse bunu takip etmez.
  • Dış kaynaklı ekip ya da yurt dışı bulut hizmeti veriye erişir; KVKK'daki aktarım kuralları devreye girer.

Maskeleme, takma adlandırma, anonimleştirme ve sentetik veri arasındaki fark nedir?

Dört terim sık sık birbirinin yerine kullanılır ama farklı garantiler verir. Maskeleme, alanı biçimini koruyarak başka bir değerle değiştirmenin genel adıdır. Takma adlandırma, bu değiştirmeyi bir anahtarla yapar ve anahtar elinde olan kişi geri dönebilir. Anonimleştirme, hiçbir ek bilgiyle kişiye dönülemeyen hâldir. Sentetik veri ise gerçek kayıttan değil, modelden türetilir.

YöntemKaynakGeri dönüşDağılımKVKK'da kişisel veri mi?
Basit maskeleme (yıldızlama)Gerçek kayıtKısmen; kalan haneler ipucu verirBozulurÇoğunlukla evet
Takma adlandırma (anahtarlı HMAC)Gerçek kayıtAnahtarla mümkünKorunurEvet
AnonimleştirmeGerçek kayıtHiçbir yolla mümkün değilGenellemeyle kısmen bozulurHayır (koşul sağlanırsa)
Sentetik veriModel / şemaDönülecek kişi yokModelin kalitesine bağlıHayır

Tutarlı takma adlandırma için tuzlu HMAC neden şart?

Tutarlı takma adlandırma, aynı girdinin her koşuda ve her tabloda aynı çıktıyı vermesi demektir; yoksa müşteri tablosundaki kimlik ile sipariş tablosundaki yabancı anahtar birbirini bulamaz. Düz SHA-256 bu tutarlılığı sağlar ama güvenli değildir: TCKN 11 haneli, ilk hanesi sıfır olamayan ve son iki hanesi kontrol hanesi olan bir sayıdır, yani geçerli uzay küçüktür ve tamamı önceden hash'lenip sözlük hâline getirilebilir. Telefon numarası ve yaygın ad-soyad çiftleri için de aynı sözlük saldırısı geçerlidir.

HMAC, hash'e gizli bir anahtar ekler. Anahtarı bilmeyen kişi sözlük kuramaz; anahtar ortam değişkeninden okunur ve depoya girmez. Aşağıdaki fonksiyonlar ad, e-posta, telefon ve TCKN alanlarını biçimlerini bozmadan dönüştürür. Alan adı da HMAC girdisine katılır; böylece aynı sayı telefon sütununda ve müşteri numarası sütununda farklı sonuç verir.

import hmac, hashlib, os, re

_KEY = os.environ.get("MASK_KEY")
if not _KEY:
    raise SystemExit("MASK_KEY ortam degiskeni tanimli degil")
_KEY = _KEY.encode()

def _mac(deger: str, alan: str) -> bytes:
    # alan adi da girdiye katilir: ayni deger farkli sutunlarda farkli sonuc verir
    return hmac.new(_KEY, f"{alan}:{deger}".encode(), hashlib.sha256).digest()

def _sayi(deger: str, alan: str, hane: int) -> str:
    n = int.from_bytes(_mac(deger, alan)[:8], "big")
    return str(n % 10**hane).zfill(hane)

ADLAR = ["Ayse", "Mehmet", "Zeynep", "Ali", "Elif", "Mustafa", "Fatma", "Emre"]
SOYADLAR = ["Yilmaz", "Kaya", "Demir", "Celik", "Sahin", "Arslan", "Dogan", "Kurt"]

def maskele_ad(ad_soyad: str) -> str:
    h = _mac(ad_soyad.strip().lower(), "ad")
    return f"{ADLAR[h[0] % len(ADLAR)]} {SOYADLAR[h[1] % len(SOYADLAR)]}"

def maskele_eposta(eposta: str) -> str:
    yerel, _, alan = eposta.strip().lower().partition("@")
    return f"k{_mac(yerel, 'eposta').hex()[:10]}@{alan or 'ornek.test'}"

def maskele_telefon(tel: str) -> str:
    rakamlar = re.sub(r"\D", "", tel)
    if len(rakamlar) < 10:
        raise ValueError(f"telefon cok kisa: {tel!r}")
    yerel, on_ek = rakamlar[-10:], rakamlar[:-10]      # 5xxxxxxxxx | "", "0" veya "90"
    yeni = iter(on_ek + yerel[0] + _sayi(yerel, "tel", 9))
    return "".join(next(yeni) if c.isdigit() else c for c in tel)

def _tckn_tamamla(ilk9: str) -> str:
    d = [int(c) for c in ilk9]
    d10 = ((d[0] + d[2] + d[4] + d[6] + d[8]) * 7 - (d[1] + d[3] + d[5] + d[7])) % 10
    d11 = (sum(d) + d10) % 10
    return ilk9 + str(d10) + str(d11)

def maskele_tckn(tckn: str) -> str:
    h = _sayi(tckn, "tckn", 9)
    return _tckn_tamamla(str(1 + int(h[0]) % 9) + h[1:])   # ilk hane 0 olamaz

Kod üzerinde dikkat edilecek noktalar:

  • maskele_telefon orijinal noktalama düzenini korur: +90 532 ... girdisi +90 5xx ... biçiminde çıkar, yalnızca rakamlar değişir.
  • maskele_tckn kontrol hanelerini algoritmaya göre hesaplar; çıktı biçim olarak geçerlidir. Bu değerin gerçek bir kişiye ait olup olmadığı bilinemez, dolayısıyla test ortamı asla gerçek kimlik doğrulama servisine bağlanmamalıdır.
  • E-postada alan adı korunur; bu, sağlayıcı bazlı raporları test etmeye yarar ama tek çalışanı olan bir şirket alanı kişiyi işaret edebilir. Şüphede kalırsanız alan adını da HMAC'ten geçirin.
  • Ad listesi küçük tutuldu; sekiz ad ve sekiz soyadla 64 farklı kombinasyon çıkar. Tekilliğe ihtiyaç varsa müşteri numarasını takma ada eklemek yeterlidir.
  • Anahtar döndürülürse eski maskelenmiş kopyalarla yenisi eşleşmez. Anahtarı ortam bazında sabitleyip kasada tutmak gerekir.

Şema uyumlu ve tekrarlanabilir sentetik veri nasıl üretilir?

Şema uyumlu sentetik veri üretimi, gerçek tabloya hiç dokunmadan aynı sütun adlarına, tiplerine ve kısıtlarına sahip kayıtlar üretmektir. Tekrarlanabilirlik için üretici kendi random.Random(tohum) nesnesini kullanır; aynı tohum aynı veriyi verir, bu da bir hatayı meslektaşınızın makinesinde birebir yeniden üretmenizi sağlar. Yabancı anahtar bütünlüğü, sipariş üreticisinin yalnızca var olan müşteri kimliklerinden seçmesiyle korunur.

import random, datetime as dt
from itertools import islice

ADLAR = ["Ayse", "Mehmet", "Zeynep", "Ali", "Elif", "Mustafa", "Fatma", "Emre"]
SOYADLAR = ["Yilmaz", "Kaya", "Demir", "Celik", "Sahin", "Arslan", "Dogan", "Kurt"]
# Ornek agirliklar. Gercek oranlar uretim tablosundan GROUP BY ile cikarilir.
ILLER = {"Istanbul": 40, "Ankara": 15, "Izmir": 12, "Bursa": 8, "Antalya": 7, "Diger": 18}

def _tckn_tamamla(ilk9: str) -> str:
    d = [int(c) for c in ilk9]
    d10 = ((d[0] + d[2] + d[4] + d[6] + d[8]) * 7 - (d[1] + d[3] + d[5] + d[7])) % 10
    return ilk9 + str(d10) + str((sum(d) + d10) % 10)

def musteri_uret(tohum: int):
    r = random.Random(tohum)                      # global random degil: tohum yalitilmis
    baslangic = dt.date(2024, 1, 1)
    musteri_id = 1000
    while True:
        musteri_id += 1
        ad, soyad = r.choice(ADLAR), r.choice(SOYADLAR)
        yield {
            "musteri_id": musteri_id,
            "ad_soyad": f"{ad} {soyad}",
            "eposta": f"{ad.lower()}.{soyad.lower()}{r.randint(1, 999)}@ornek.test",
            "telefon": "05" + "".join(r.choices("0123456789", k=9)),
            "tckn": _tckn_tamamla(str(r.randint(1, 9)) + "".join(r.choices("0123456789", k=8))),
            "il": r.choices(list(ILLER), weights=list(ILLER.values()))[0],
            "kayit_tarihi": (baslangic + dt.timedelta(days=r.randint(0, 730))).isoformat(),
            "bakiye": round(r.lognormvariate(6, 1), 2),   # saga carpik dagilim
        }

def siparis_uret(musteriler, tohum: int):
    r = random.Random(tohum)
    siparis_id = 0
    for m in musteriler:                          # yabanci anahtar hep var olan bir musteriye gider
        for _ in range(r.choices([0, 1, 2, 3, 5], weights=[30, 35, 20, 10, 5])[0]):
            siparis_id += 1
            yield {"siparis_id": siparis_id, "musteri_id": m["musteri_id"],
                   "tutar": round(r.uniform(50, 5000), 2)}

musteriler = list(islice(musteri_uret(42), 1000))
siparisler = list(siparis_uret(musteriler, 43))
assert musteriler[:3] == list(islice(musteri_uret(42), 3))       # ayni tohum, ayni veri
kimlikler = {m["musteri_id"] for m in musteriler}
assert all(s["musteri_id"] in kimlikler for s in siparisler)     # kopuk yabanci anahtar yok

Üreticiyi gerçekçi kılan şey ağırlıklar ve dağılımlardır. Kod içindeki il ağırlıkları ve bakiye parametreleri yalnızca yer tutucudur; doğru değerler üretim tablosundan toplu istatistik olarak çekilir (il başına sayı, bakiye için ortalama ve standart sapma). Toplu istatistik kişisel veri taşımaz, dolayısıyla geliştiriciye verilebilir. Üreticinin şemaya uyduğunu ve tohumla aynı çıktıyı verdiğini test etmek için otomasyon betiklerini pytest ile test etme yaklaşımı doğrudan uygulanır.

Maskeleme yolunu seçtiğinizde en sık kırılan şey ilişkilerdir. Müşteri tablosunu maskeleyip sipariş tablosunu unutursanız yabancı anahtarlar boşa düşer; iki tabloyu farklı anahtarla maskelerseniz aynı sonuç çıkar. Maskeleme işini bir veri göçü gibi ele alıp kaynak ve hedef arasında satır sayısı ve ilişki mutabakatı yapmak gerekir; yöntem veri göçü planı, mutabakat ve geri alma yazısındakiyle aynıdır.

KVKK açısından takma adlandırma ile anonimleştirme arasındaki fark nedir?

KVKK açısından takma adlandırılmış veri hâlâ kişisel veridir, çünkü anahtarı elinde bulunduran taraf kişiye geri dönebilir. Anonimleştirme ise verinin hiçbir yöntemle, başka verilerle eşleştirilse bile, belirli bir kişiyle ilişkilendirilememesi hâlidir; Kişisel Verileri Koruma Kurumu'nun kişisel verilerin silinmesi, yok edilmesi veya anonim hâle getirilmesi rehberi bu geri döndürülemezlik şartını açıkça arar. HMAC ile maskelenmiş bir test veritabanı bu şartı sağlamaz.

Pratik sonuçları şöyle sıralanabilir:

  • HMAC ile maskelenmiş veri işlenen bir ortam, üretimle aynı hukuki yükümlülüklere tabidir; erişim kaydı ve saklama süresi onun için de geçerlidir.
  • Maskeleme anahtarı, o veriyi kişisel veri yapan tek parçadır. Anahtarın kasada tutulması ve test ortamına hiç girmemesi, yükümlülüğü daraltır ama ortadan kaldırmaz.
  • Sentetik veri gerçek kişiden türemediği için kapsam dışında kalır; ancak üreticiyi beslerken kullanılan toplu istatistiğin çok küçük gruplarda (örneğin tek kişilik bir il) kişiyi işaret etmemesi gerekir.
  • Yurt dışı bulut ya da dış ekip söz konusuysa maskelenmiş veri de aktarım sayılır; sentetik veri bu kapıyı hiç açmaz.

Yazdıklarım hukuki tavsiye değildir. Somut bir işleme faaliyeti için KVKK uzmanı ya da avukatla birlikte değerlendirme yapılmalı; teknik yöntem ancak o değerlendirmenin içinde anlam kazanır.

Sonuç: hangi ortama hangi veri?

Karar kuralı sade tutulabilir: dış ekip, demo ve yerel geliştirme ortamları sentetik veriyle çalışır; üretime en yakın kabul testi ortamı, gerekiyorsa HMAC ile takma adlandırılmış kopya kullanır ve üretimle aynı erişim kurallarına tabi tutulur. Gerçek verinin düz kopyası hiçbir ortama gitmez.

İki yöntemi birlikte kurmak, tek başına birine güvenmekten daha sağlamdır: sentetik üretici günlük geliştirmeyi taşır, maskelenmiş kopya dağılıma bağlı hataları yakalar. Kendi şemanız için hangi alanın maskeleneceğine, hangi tabloların ilişkisinin korunacağına ve üreticinin hangi dağılımları öğrenmesi gerektiğine birlikte karar vermek isterseniz iletişim bölümünden yazabilirsiniz.