# Denetim zinciri ve SIEM dışa aktarımı

> QueryProxy denetim kayıtlarını nasıl zincirleyip doğrular ve her denetim olayını SIEM'iniz için stdout, syslog ya da imzalı bir webhook'a nasıl gönderirsiniz.

- Dokümantasyon: Kılavuzlar
- Güncelleme: 2026-10-25
- Kaynak: https://queryproxy.com/tr/docs/denetim-kaydi-ve-siem/
- Dil: tr-TR
- Yazar: Muhammet Şafak

---
Her denetim kaydı iki kez korunur: uygulama veritabanı kayıtları bir hash
zincirine bağlar ve isteğe bağlı **sink**'ler her olayı veritabanı
yöneticisinin denetlemediği bir sisteme kopyalar. İkinci kısım önemlidir; çünkü
yalnızca veritabanı içinde tutulan bir zinciri, veritabanının sahibi olan biri
yeniden yazabilir.

## Hash zinciri

Her kayıt `prev_hash` ve `hash` saklar; burada
`hash = SHA-256(prev_hash || canonical_json)` kaydın alanları (takım,
kullanıcı, eylem, talep, bağlantı, SQL, anahtarları sıralı metadata, IP ve
zaman) üzerinden hesaplanır. Kayıtlar, bir kilit satırını da alan tek bir
transaction içinde bağlanır; böylece zincir çatallanamaz. İki hash üzerindeki
unique index'ler, aynı öncülü olan ikinci bir kaydı reddeder.

Veritabanı trigger'ları denetim tablosunda `UPDATE` ve `DELETE`'i, PostgreSQL'de
ayrıca `TRUNCATE`'i reddeder (MySQL ve MariaDB trigger'ları bunu engelleyemez).

### Doğrulama

```bash
docker exec queryproxy php artisan audit:verify
```

Komut zinciri ilk kayıttan itibaren dolaşır. Zincir sağlamsa kayıt sayısını ve
zincirin başını basar ve `0` ile çıkar. Değilse ilk bozuk kaydın id'sini ve
nedenini basar ve `1` ile çıkar. Değiştirilmiş kayıtları, kaldırılmış ya da
araya eklenmiş kayıtları, yeniden sıralanmış bir zinciri, ilk kayıttan
başlamayan bir zinciri ve yazılan kayıt sayısından kısa bir zinciri (kaldırılmış
bir kuyruk ya da boşaltılmış bir tablo) saptar. `queryproxy:doctor` en yeni
kayıtların ve zincir başının daha hızlı bir kontrolünü yapar;
`queryproxy:doctor --full-audit` tüm zinciri dolaşır.

### Checkpoint'ler

Zamanlayıcı her gün `audit:checkpoint` çalıştırır. Son kayıt id'sini, hash'ini ve
kayıt sayısını içeren bir `audit.checkpoint` olayı yazar; bir öncekinden beri
hiçbir şey yazılmadıysa yazmaz. Checkpoint sıradan bir denetim olayıdır; bu
yüzden sink'lerinize de gider.

### Zincirin yapamadıkları

- **Trigger'ları kaldırıp zinciri yeniden hesaplayan bir veritabanı sahibini
  yakalayamaz.** Bu güce sahip biri tutarlı bir geçmiş yazabilir. Farkı yalnızca
  veritabanının dışında, SIEM'inizde tutulan bir kopya gösterir. Checkpoint'lerin
  ve aşağıdaki sink'lerin amacı budur.
- **MySQL ve MariaDB**: `created_at` UTC duvar saati olarak hash'lendiği için
  bağlantı saat dilimi UTC'ye sabitlenir. QueryProxy'nin arkasından sunucunun ya
  da oturumun saat dilimini değiştirmeyin.
- Denetim yazıcısını çağıran **uzun dış transaction'lar**, bitene kadar kilit
  satırını tutar; bu yüzden diğer yazıcılar bekler.

## SIEM'e aktarım

Her sink varsayılan olarak kapalıdır. `QUERYPROXY_SIEM_SINKS` onları açar:
`stdout`, `syslog` ve `webhook` değerlerinin virgülle ayrılmış, herhangi bir
kombinasyonlu listesi.

Teslim, denetim kaydı commit edildikten sonra kuyrukta çalışır; bu yüzden
çökmüş bir sink bir denetim yazmasını asla engellemez ya da geri almaz. Teslim
**en az bir kez** yapılır: alıcı olayın `id`'si üzerinden tekilleştirme yapmalıdır.
Başarısız olmaya devam eden bir webhook ya da syslog teslimi, ölü mektup kuyruğu
(dead-letter queue) görevi gören `failed_jobs` içine düşer
(`php artisan queue:failed`). Kuyruk backend'inin kendisi çökmüşse olay dışa
aktarılmaz ve sorun loglanır; kayıt sistemi veritabanı olarak kalır ve zinciri
eksiksiz kalır. Boşluk, SIEM kopyasında dizide eksik bir `id` olarak görünür.

| Değişken | Varsayılan | Anlamı |
| :-- | :-- | :-- |
| `QUERYPROXY_SIEM_SINKS` | yok | Açılacak sink'ler: `stdout`, `syslog`, `webhook` |
| `QUERYPROXY_SIEM_QUEUE` | `default` | Teslimlerin çalıştığı kuyruk. Hazır işçi `default`'a hizmet verir; farklı bir ad için ona ait bir işçi gerekir |
| `QUERYPROXY_SIEM_STDOUT_STREAM` | `php://stdout` | `stdout` sink'inin yazdığı stream |
| `QUERYPROXY_SIEM_SYSLOG_HOST` | yok | Syslog alıcısının host'u |
| `QUERYPROXY_SIEM_SYSLOG_PORT` | `514` (`tls` için `6514`) | Alıcı portu |
| `QUERYPROXY_SIEM_SYSLOG_TRANSPORT` | `udp` | `udp`, `tcp` veya `tls` |
| `QUERYPROXY_SIEM_SYSLOG_FRAMING` | `octet-counting` | `tcp` ve `tls` için: `octet-counting` veya `newline` |
| `QUERYPROXY_SIEM_SYSLOG_FACILITY` | `13` | Syslog facility (13 "log audit"tir) |
| `QUERYPROXY_SIEM_SYSLOG_APP_NAME` | `queryproxy` | APP-NAME alanı |
| `QUERYPROXY_SIEM_SYSLOG_HOSTNAME` | yok | HOSTNAME alanı |
| `QUERYPROXY_SIEM_SYSLOG_TLS_CA_FILE` | yok | Bir `tls` alıcısını doğrulamak için CA dosyası; boşsa sistem güven deposu kullanılır |
| `QUERYPROXY_SIEM_SYSLOG_TIMEOUT` | `5` | Saniye |
| `QUERYPROXY_SIEM_WEBHOOK_URL` | yok | Her olayı alan `http://` veya `https://` URL'si |
| `QUERYPROXY_SIEM_WEBHOOK_SECRET` | yok | İmzalama secret'ı, en az 16 karakter. `QUERYPROXY_SIEM_WEBHOOK_SECRET_FILE` onu bir dosyadan okur |
| `QUERYPROXY_SIEM_WEBHOOK_TIMEOUT` | `5` | Saniye |
| `QUERYPROXY_SIEM_WEBHOOK_TRIES` | `6` | İş başarısız olmadan önceki teslim denemeleri |

Yapılandırması hatalı bir sink kullanılmaz ve sorun süreç başına bir kez
loglanır. `queryproxy:doctor` bunu `siem_sinks` olarak bildirir.

### Olay

Tüm sink'ler aynı JSON belgesini gönderir; belge
[`docs/audit-event.schema.json`](https://github.com/QueryProxy/QueryProxy/blob/main/docs/audit-event.schema.json)
ile tanımlanır (JSON Schema Draft 2020-12, `additionalProperties: false`,
`schema_version: 1`).

| Alan | Anlamı |
| :-- | :-- |
| `id` | Denetim kaydı id'si; kurulum başına benzersiz. Buna göre tekilleştirin |
| `timestamp` | UTC, saniye hassasiyetinde |
| `instance` | Kurulumun `APP_URL`'inin host'u; ayarlı değilse `null` |
| `action` | Olay adı, örneğin `request.approved` |
| `category` | `action`'ın noktadan önceki kısmı |
| `actor` | `{ "id", "email" }`, sistem olayları için `null` |
| `team_id`, `connection_id`, `query_request_id` | İlişkili nesneler ya da `null` |
| `ip` | İstemci adresi; konsol ve kuyruk olayları için `null` |
| `sql_redacted` | Literal'leri yer tutucularla değiştirilmiş SQL ya da `null` |
| `sql_hidden` | SQL var ama güvenle redakte edilemediğinde `true` |
| `metadata` | Olaya özgü ayrıntılar ya da `null` |

Ham SQL asla dışa aktarılmaz. Şemanın `action` listesi olay kataloğundan üretilir
ve `user`, `request`, `connection`, `masking_rule`, `team`, `auth`,
`chat_integration`, `chat_identity`, `execution`, `audit`, `masking_key`,
`error_messages` ve `result` kategorilerinde gruplanmış 50 olay içerir.
`actor.email`, olay teslim edilirken okunur; bu yüzden denetim yazısı ile teslim
arasında e-posta adresini değiştiren bir kullanıcı yeni adresiyle görünür.

### stdout

Konteynerin standart çıktısında satır başına bir JSON nesnesi. Aynı stream
nginx, php-fpm ve kuyruk işçisinin çıktısını da (`RUNNING` ve `DONE` satırları)
taşır; bu yüzden toplayıcı JSON olmayan satırları atlamalıdır. Konteyner log
driver'ları ve Docker logunu okuyan agent'lar bunu alır.

### syslog

Gövdesi JSON olay olan bir RFC 5424 mesajı; UDP, TCP ya da TLS üzerinden. TCP ve
TLS, `newline` seçmediğiniz sürece octet-counting framing (RFC 6587) kullanır.
`tls` ile QueryProxy alıcının sertifikasını ve adını doğrular; alıcının CA'sı
sistem güven deposunda değilse `QUERYPROXY_SIEM_SYSLOG_TLS_CA_FILE` ayarlayın.
Standart TLS syslog portu `6514`'tür.

### İmzalı webhook

QueryProxy, JSON olayı şu başlıklarla POST eder ve yönlendirmeleri izlemez:

| Başlık | İçerik |
| :-- | :-- |
| `X-QueryProxy-Timestamp` | Teslimin yapıldığı Unix saniyesi |
| `X-QueryProxy-Signature` | `sha256=` ve ardından secret ile `"{timestamp}.{body}"` üzerinden hesaplanan HMAC-SHA256'nın hex değeri |
| `X-QueryProxy-Event` | `action` |
| `X-QueryProxy-Event-Id` | Denetim kaydı id'si |
| `User-Agent` | `QueryProxy-Audit/1` |

En az 16 karakterlik secret'ı olmayan bir webhook asla imzasız gönderilmez.
2xx dışındaki her HTTP durumu başarısızlık sayılır ve artan bir gecikmeyle
yeniden denenir (10 sn, 1 dk, 5 dk, 15 dk, ardından 1 sa). Bir teslime
güvenmeden önce doğrulayın:

```python
import hashlib
import hmac
import time

def is_valid(secret: str, timestamp: str, raw_body: bytes, signature: str) -> bool:
    # Reject stale deliveries; a retry carries a fresh timestamp.
    if abs(time.time() - int(timestamp)) > 300:
        return False
    message = timestamp.encode() + b"." + raw_body
    expected = "sha256=" + hmac.new(secret.encode(), message, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature)
```

İmzayı, yeniden serileştirilmiş bir kopya üzerinden değil, alındığı haliyle ham
gövde üzerinden hesaplayın.

Ürüne özgü connector'lar (Splunk, Elastic, Wazuh, Microsoft Sentinel) ile
gelmez. Syslog kabul eden, container loglarını okuyan ya da imzalı bir HTTP POST
alan herhangi bir toplayıcı bu olayları içeri alabilir.

## Sonraki adımlar

- [İzleme](/tr/docs/izleme/)
- [Tehdit modeli](/tr/docs/tehdit-modeli/)
- [Yapılandırma](/tr/docs/yapilandirma/)
