Mert Özen Her Satırda Daha İleri
Güvenlik Threat Modeling ve Güvenli Geliştirme Süreci — #05

Pentest ile Çalışmak: Kapsam, Rules of Engagement, Bulguları Okumak, Retest ve Rapor Sonrası

Mert Özen 15 Kas 2026 12 dk 8 görüntülenme
Pentest ile Çalışmak: Kapsam, Rules of Engagement, Bulguları Okumak, Retest ve Rapor Sonrası

Pentest'in değeri, test başlamadan yazılan kapsam ve rules of engagement ile test bittikten sonra her bulgunun bir regression testine ve bir karara dönüşmesine bağlıdır. Pentest, DAST, bug bounty ve red team farkı; Bookstore'un Kurumsal Sipariş pentest'inde kapsam, test hesapları ve durma koşulları, severity itirazı, kanıtlı false positive, kök nedene göre gruplama, F-01 ve F-02 için düzeltme ve testler, retest ve threat model güncellemesi.

Sürüm notu: Bu yazıdaki kodlar .NET 10 ile derlenip denenmiştir; testler xUnit v3 4.0.3 ile çalıştırılmıştır. Anlatılan pentest bir senaryodur: bulgular Bookstore'un yerel kopyasında 127.0.0.1 üzerinde yeniden üretilmiş, regression testleri ve log'lar gerçek çıktılardır.

Önce Tahmin Et

Sitedeki serilerin ortak örnek uygulaması Bookstore (bir online kitapçı), Kurumsal Sipariş'i Lakeside School'a satmak üzere. Okulun satın alma sorumlusu Nevin Hanım sözleşmeden önce bir şey istiyor: bağımsız bir firmanın yaptığı pentest'in (penetration test; yetkili kişilerin sisteme saldırgan gibi yaklaşarak yaptığı güvenlik testi) raporu. Ekipte ilk tepki şu: "Pipeline'da zaten ZAP (açık kaynak web güvenlik tarayıcısı) ile DAST (çalışan uygulamayı dışarıdan tarayan otomatik güvenlik testi) çalışıyor, raporunu gönderelim."

Tahmin et: ZAP'in her gece temiz geçtiği bir API'de bir pentest ne bulabilir? Bu yazıdaki senaryoda pentest dört bulgu getirdi; ikisi gerçek ve önemliydi, biri bilgilendirmeydi, biri de false positive'di. Gerçek olan ikisinin hiçbirini bir tarayıcı bulamazdı, çünkü ikisi de "bu istek geçerli mi?" değil "bu iş kuralı yeterli mi?" sorusunun cevabıydı. Bu yazıda pentest'e hazırlanmayı, kapsamı yazmayı, raporu okumayı ve bulguları teste çevirmeyi konuşuyoruz.

Pentest, DAST, Bug Bounty ve Red Team

Dördü de "dışarıdan bakan biri" fikrine dayanır ama farklı soruları cevaplar:

YöntemKim yaparNe zamanEn iyi bulduğu
DASTPipeline'daki araçHer build ya da her geceBilinen kalıplar: eksik header, injection imzaları, yanlış configuration
PentestBelirli süre için tutulan uzmanBüyük release, müşteri ya da denetim talebiİş mantığı, yetki atlatma, araçların göremediği zincirler
Bug bountyÖdül karşılığı dış araştırmacılarSürekliÇok sayıda gözün bulduğu çeşitli hatalar
Red teamKurumun savunmasını test eden ekipOlgun güvenlik programlarında, periyodikTespit ve müdahale yeteneğinin açıkları

ZAP ile DAST DevSecOps serisinin altıncı yazısında anlatıldı; bug bounty'yi bir önceki yazıda konuştuk. Red team, uygulamayı değil kurumu test eder: saldırının fark edilip edilmediğine bakar ve küçük bir ekip için genellikle erken bir adımdır. Pentest'in ne zaman gerektiğine gelince, Bookstore için üç tetikleyici var: kurumsal bir müşterinin sözleşme şartı, bir denetim ve trust boundary'leri değiştiren büyük bir release. Kurumsal Sipariş üçünü de karşılıyor.

Pentest firmaları genellikle kamuya açık metodolojilere atıf yapar: OWASP'ın (Open Worldwide Application Security Project; açık uygulama güvenliği topluluğu) Web Security Testing Guide'ı (son kararlı sürümü Aralık 2020 tarihli 4.2; 5.0 üzerinde çalışılıyor), NIST'in (ABD Ulusal Standartlar ve Teknoloji Enstitüsü) Eylül 2008 tarihli ve hâlâ yürürlükte olan SP 800-115'i ya da PTES (Penetration Testing Execution Standard; pentest'in aşamalarını tanımlayan topluluk standardı). Teklif alırken hangi metodolojiyi kullanacaklarını sormak, raporu okurken neyin test edilip neyin edilmediğini anlamanı kolaylaştırır.

Kapsam ve Rules of Engagement

Pentest'in kalitesi test başlamadan, imzalanan belgeyle belirlenir. Rules of engagement (testin kurallarını, sınırlarını ve iletişimini tanımlayan anlaşma) Bookstore'da repoda, docs/security/pentest-2026-11/ altında duruyor. Kapsam kısmı:

| In scope | Notes |
|---|---|
| `https://staging-api.bookstore.example.com` | Bookstore.Api, same release as production |
| Institutional purchase requests, approval and ordering | Main focus |
| Customer checkout up to the payment redirect | |

| Out of scope | Why |
|---|---|
| Production (`bookstore.example.com`) | Real customer data and real orders |
| Payment provider's hosted page | Third party; needs the provider's own permission |
| Shipping carrier API | Third party |
| Denial-of-service by volume, social engineering, physical access | Not part of this engagement |

Birkaç karar dikkat istiyor. Test ortamı production ile aynı sürümü çalıştırıyor ama içinde yalnız sentetik veri var; production'ın kopyası değil. Ödeme sağlayıcının sayfası kapsam dışında, çünkü o sistem bizim değil; onu test etmek için sağlayıcının kendi izni gerekir. Test hesapları rollere göre hazırlandı ve listede bilerek hem InstitutionBuyer hem InstitutionApprover rolüne sahip bir hesap var: ikinci yazıdaki dört göz kuralını test edebilmeleri için bu hesaba ihtiyaçları var. Hesap şifreleri e-postayla değil, firmanın güvenli kanalıyla gidiyor ve test bitince sıfırlanıyor.

Rate limit konusu ince bir denge. Limitleri tamamen kapatırsan rate limit eksikliği bulunamaz; açık bırakırsan test her dakika duvara çarpar. Bookstore'un kararı: limitler açık, testçilerin kaynak adresleri yalnız staging'de daha yüksek bir limit alıyor ve istenirse düşük limitle test yapılabiliyor. Belgenin en önemli kısmı ise "dur ve hemen haber ver" listesi: staging'de gerçek bir kişisel veri ya da ödeme verisi görülürse, başka bir kurumun siparişine erişmek gibi kritik bir bulgu çıkarsa ya da ortam kararsızlaşırsa test durur ve aynı gün telefonla haber verilir.

Kurumsal Müşterinin İstediği Rapor

Nevin Hanım'ın ekibi raporun tamamını okumayacak; okul için önemli olan üç şey var. Bir yönetici özeti: test kim tarafından, hangi tarihlerde, hangi kapsamda yapıldı ve genel sonuç ne. Bulguların listesi ve her birinin son durumu. Ve retest'ten sonra firmanın imzaladığı kısa bir mektup: "şu bulgular düzeltildi ve doğrulandı, şunlar kabul edildi". Bookstore ekibi için ise asıl değer ayrıntılı kısımdadır: her bulgunun yeniden üretme adımları ve CVSS (Common Vulnerability Scoring System; zafiyetler için puanlama sistemi) 4.0 puanı. Ayrıntılı raporu müşteriyle bir gizlilik anlaşması altında paylaşmak ya da hiç paylaşmamak yaygın bir uygulama; yönetici özeti ve retest mektubu çoğu müşteriye yeter.

Bulguları Okumak: Severity İtirazı, False Positive ve Kök Neden

Rapor geldiğinde Emre bulguları bir tabloya taşıdı. Raporun severity'si ile ekibin severity'si ayrı sütunlarda duruyor; ikisi farklıysa nedeni yazılıyor:

| Id | Report severity | Our severity | Finding | Root cause group | Decision | Regression test | Retest |
|---|---|---|---|---|---|---|---|
| F-01 | Medium | Medium | Splitting a large purchase into requests under the unit limit avoids approval | Documented limit not enforced (ASVS 2.1.3 / 2.3.2) | Fix: rolling 24 h unit total | `PentestRegressionTests` F-01 | Passed |
| F-02 | High | Medium | No limit on `POST /api/orders`; scripted orders reserve stock | Documented limit not enforced (ASVS 2.4.1) | Fix: fixed-window rate limit per client | `PentestRegressionTests` F-02 | Passed |
| F-03 | Info | Info | `Server: Kestrel` header reveals the web server | None (no exploit path) | Accept; listed as out of scope in SECURITY.md | None | n/a |
| F-04 | High | False positive | Possible SQL injection in `GET /api/books?search=` (response time differs with `'`) | None: EF Core parameters; `%` and `_` are escaped | Dispute with evidence (query log + test) | `PentestRegressionTests` F-04 | Accepted by tester |

F-02'de rapor High diyor, ekip Medium. İtiraz bulgunun gerçek olmadığını söylemiyor; etkisini tartışıyor. Script'le atılan siparişler stok rezervasyonlarını tüketiyor ama veri sızdırmıyor, para kaybettirmiyor ve rezervasyonlar iptal edilebiliyor. Bu, ikinci yazıdaki T03 tehdidinin aynısı ve orada da Medium puanlanmıştı. Severity farklı olsa da karar aynı: düzeltilecek. Severity tartışması önceliği belirler, düzeltip düzeltmeyeceğini değil.

F-04'te ise rapor bir SQL injection şüphesi bildiriyor: arama parametresine tek tırnak eklenince yanıt süresi değişiyor. Bu tür bir bulguya itiraz ederken kanıt göstermek gerekir; "Entity Framework Core (EF Core) kullanıyoruz" yetmez. Bookstore ekibi API'yi yerelde EF Core komut log'larıyla çalıştırdı ve aynı isteği attı:

curl -s "http://127.0.0.1:52865/api/books?search=austen%27%20OR%20%271%27%3D%271"
{"items":[],"total":0,"page":1,"pageSize":12}
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (1ms) [Parameters=[@pattern='?' (Size = 19), @term='?' (Size = 17)], CommandType='Text', CommandTimeout='30']
      SELECT COUNT(*)
      FROM "Books" AS "b"
      INNER JOIN "Authors" AS "a" ON "b"."AuthorId" = "a"."Id"
      WHERE "b"."Title" LIKE @pattern ESCAPE '\' OR "a"."Name" LIKE @pattern ESCAPE '\' OR "b"."Isbn" = @term

Girdi SQL metnine hiç girmiyor, @pattern ve @term parametreleri olarak gidiyor; yanıt süresindeki fark, sonuç sayısındaki farktan kaynaklanıyor. Yine de log bir şey daha hatırlattı: arama LIKE kullandığı için % ve _ karakterleri escape edilmezse kullanıcı joker karakterle bütün kataloğu tek istekte çekebilir. Bookstore'da bu karakterler zaten escape ediliyor; bunu kalıcı kılmak için bulgu, SQL sözdizimini ve joker karakteri düz metin olarak arayan bir regression testiyle kapandı. Testçi kanıtı görünce bulguyu false positive olarak kabul etti. Injection'ın kendisi Web Güvenliği (OWASP Top 10) serisinin injection yazısında.

Bulguları tek tek düzeltmeden önce kök nedene göre gruplamak, aynı hatanın başka yerlerdeki kopyalarını bulmanın yoludur. F-01 ve F-02'nin kök nedeni aynı: docs/security/authorization-rules.md'de yazan bir limit kodda uygulanmıyor. Bu yüzden düzeltmenin yanında bir iş daha açıldı: dokümandaki her limit için kodda karşılığını ve testini bul. F-01'in hikâyesi ayrıca öğretici: bu, ikinci yazıda bilerek kabul ettiğimiz T12 tehdidi. Kabul kararı "günlük raporda görünür" varsayımına dayanıyordu; pentest raporu kurumsal müşterinin bu varsayımı yeterli bulmadığını gösterdi ve Selin kararı değiştirdi. Kabul edilen bir risk, müşterinin de göreceği bir risktir.

Her Bulgu için Bir Regression Testi

Bir bulgu, onu yeniden üreten test yazılmadan kapanmaz. Testler bulgu numarasıyla işaretlendi; böylece retest öncesi dotnet test --filter-trait "pentest=F-01" ile yalnız o bulgunun testleri çalışıyor. F-01 için önce test yazıldı. Aynı birimde, her biri limitin altında kalan iki istek 24 saat içinde limiti aşıyorsa ikincisi onay beklemeli:

[Fact]
[Trait("pentest", "F-01")]
[Trait("threat", "T12")]
[Trait("asvs", "v5.0.0-2.3.2")]
public async Task Requests_split_under_the_limit_need_approval_once_the_unit_total_passes_it()
{
    // Lakeside School Fen Bölümü: limit 5.000,00 TL
    HttpClient margaret = factory.CreateClientAs(new TestUser("margaret", 1, 3, "InstitutionBuyer"));

    PurchaseRequestView first = await CreateAsync(margaret, Frankenstein, 20);    // 3.958,20
    PurchaseRequestView second = await CreateAsync(margaret, Frankenstein, 20);   // 24 saatte 7.916,40

    Assert.Equal("Approved", first.Status);
    Assert.Equal("PendingApproval", second.Status);
}
failed Bookstore.Api.Tests.PentestRegressionTests.Requests_split_under_the_limit_need_approval_once_the_unit_total_passes_it (219ms)
  Assert.Equal() Failure: Strings differ
  Expected: "PendingApproval"
  Actual:   "Approved"
Test run summary: Failed!
  total: 1
  failed: 1
  succeeded: 0

Düzeltme, onay kuralını tek isteğin toplamından birimin son 24 saatteki otomatik onaylanmış isteklerinin toplamına taşıyor. SQLite, EF Core üzerinden decimal toplamını SQL'de hesaplayamadığı için toplamlar listeye çekilip bellekte toplanıyor; liste küçük. SQL Server'da SumAsync yeterli olurdu:

private async Task ApplyApprovalRuleAsync(PurchaseRequest request, CancellationToken ct)
{
    InstitutionUnit unit = await db.InstitutionUnits.SingleAsync(u => u.Id == request.UnitId, ct);
    // Pentest F-01: limit tek isteğe değil, birimin son 24 saatte limitle onaylanan isteklerine uygulanır.
    DateTime since = clock.GetUtcNow().UtcDateTime.AddHours(-24);
    List<decimal> recentTotals = await db.PurchaseRequests
        .Where(r => r.UnitId == request.UnitId && r.Id != request.Id
            && r.ApprovedBy == AutoApprover && r.CreatedAtUtc >= since)
        .Select(r => r.Total)
        .ToListAsync(ct);   // SQLite decimal'ı SQL'de toplayamaz; liste küçük
    bool needsApproval = recentTotals.Sum() + request.Total > unit.ApprovalLimit;
    request.Status = needsApproval ? PurchaseRequestStatus.PendingApproval : PurchaseRequestStatus.Approved;
    request.ApprovedBy = needsApproval ? null : AutoApprover;
    request.ApprovedAtUtc = needsApproval ? null : clock.GetUtcNow().UtcDateTime;
    request.ApprovedLinesHash = needsApproval ? null : request.ComputeLinesHash();
}

F-02 için ASP.NET Core'un rate limiter'ı sipariş endpoint'ine bağlandı. Algoritmalar ve partition key seçimi ASP.NET Core Güvenliği serisinin rate limiting yazısında ayrıntılı; burada istemci adresine göre dakikada 10 istek yeterli:

// Pentest F-02 / ASVS v5.0.0-2.4.1: sipariş verme istemci adresine göre sınırlanır.
int placeOrderLimit = builder.Configuration.GetValue("RateLimiting:PlaceOrder:PermitLimit", 10);
builder.Services.AddRateLimiter(options =>
{
    options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
    options.AddPolicy(OrderingModule.PlaceOrderPolicy, context => RateLimitPartition.GetFixedWindowLimiter(
        context.Connection.RemoteIpAddress?.ToString() ?? "unknown",
        _ => new FixedWindowRateLimiterOptions { PermitLimit = placeOrderLimit, Window = TimeSpan.FromMinutes(1) }));
});

Testi limiti 3'e düşüren ayrı bir host üzerinde çalışıyor; dördüncü istek 429 almalı. Diğer testlerin aynı limiti paylaşmaması için bu önemli:

[Fact]
[Trait("pentest", "F-02")]
[Trait("threat", "T03")]
[Trait("asvs", "v5.0.0-2.4.1")]
public async Task Placing_orders_is_rate_limited_per_client()
{
    HttpClient client = factory
        .WithWebHostBuilder(b => b.UseSetting("RateLimiting:PlaceOrder:PermitLimit", "3"))
        .CreateClient();

    List<HttpStatusCode> codes = [];
    for (int i = 0; i < 4; i++)
    {
        HttpResponseMessage response = await client.PostAsJsonAsync("/api/orders",
            new PlaceOrderRequest(Guid.NewGuid(), "Ada Lovelace", "ada@example.com"), ct);
        codes.Add(response.StatusCode);
    }

    Assert.Equal([HttpStatusCode.NotFound, HttpStatusCode.NotFound, HttpStatusCode.NotFound, HttpStatusCode.TooManyRequests], codes);
}

v5.0.0-2.4.1 üçüncü yazıda planned durumundaydı; şimdi requirement dosyasında test oldu ve traceability testi bunu doğruluyor. Bütün suite'in sonucu:

Test run summary: Passed!
  total: 25
  failed: 0
  succeeded: 25
  skipped: 0
  duration: 1s 135ms

Yetkilendirme bulgularının sürekli regresyon testi ve OpenAPI tabanlı tarama API Güvenliği serisinin son yazısında; buradaki testler o altyapının içine eklenecek türden.

Retest ve Rapor Sonrası

Retest, düzeltmelerin production'a çıkmasından sonra ve rules of engagement'ta yazan 30 gün içinde yapılır. Testçi her bulguyu kendi adımlarıyla yeniden dener; Bookstore'un regression testleri bu adımların otomatik kopyası olduğu için retest sürpriz üretmez. Retest sonrası imzalanan mektup Nevin Hanım'a gider.

Rapor kapandıktan sonra iki iş kalır ve ikisi de en çok atlananlardır. Birincisi threat model'i güncellemek: T03 ve T12 artık Mitigated, mitigation alanlarında testlerin adı var ve risk register'daki R-02 satırı kapatıldı. Model güncel mi diye bakmak için:

jq -r '[.detail.diagrams[].cells[].data.threats[]?] | group_by(.status)[] | "\(.[0].status)\t\(length)"' threat-model/bookstore-checkout.json
Mitigated	7
Open	5

İkincisi sürece dönmek: pentest'in bulduğu her şey için "bunu neden biz bulamadık?" sorusu sorulur. F-01 modelde vardı ama kabul edilmişti; F-02 modelde vardı ama backlog'da bekliyordu. İkisi de süreç içinde görünürdü; eksik olan önceliklendirmeydi. Bu, NIST SSDF'in (Secure Software Development Framework; güvenli yazılım geliştirme çerçevesi) kök neden analizi ve süreç iyileştirme pratiklerinin küçük bir ekipteki karşılığı.

Üretimde Dikkat

Pentest penceresinde staging'in log'ları ve alarmları normalden farklı görünür: yetkisiz erişim denemeleri, hatalı girdiler, 429'lar. Bu sinyallerin bir incident ile karışmaması için test penceresi on-call ekibine önceden duyurulur ve testçilerin kaynak adresleri alarm kurallarında etiketlenir. Etiketlemek, alarmı kapatmak değildir; testçinin göremeden geçtiği bir gerçek saldırı aynı pencereye denk gelebilir.

Test bitince test hesaplarının şifreleri sıfırlanır, gerekmiyorsa hesaplar kapatılır ve testçilere verilen her erişim geri alınır. Staging production'dan ayrı bir kimlik sağlayıcı kullanmıyorsa bu adım daha da kritik.

Pentest bir anlık fotoğraftır. Rapor tarihinden sonraki ilk release'te değişen her data flow, raporun kapsamı dışında kalır. Bookstore'da bu yüzden pentest bir yıllık bir döngüye ve büyük release'lere bağlı; aradaki zamanı threat model, regression testleri ve pipeline taramaları dolduruyor.

Sık Düşülen Tuzaklar

Production'da habersiz test, en tehlikeli hatadır. Kimseye haber verilmeden production'a karşı yapılan bir test, ekibin gerçek bir saldırıyla uğraşmasına, gerçek müşteri verisinin görülmesine ve hatta servisin aksamasına yol açabilir; rules of engagement'ın var olma nedeni budur. Testçi "production'ı da deneyelim mi?" diye sorarsa cevap ayrı, yazılı bir izindir.

İkinci tuzak yalnız "critical" bulguları kapatıp raporu arşivlemek. Medium ve Low bulgular çoğu zaman bir sonraki pentest'te aynı hâliyle tekrar çıkar ve müşterinin gözünde ekibin bulgularla ilgilenmediğini gösterir. Her bulgu ya düzeltilir ya da yazılı bir kararla kabul edilir; arada bir durum yoktur.

Üçüncüsü her bulguya itiraz etmek. Kanıtı olmayan itiraz güven kaybettirir; F-04 gibi kanıtlı itiraz ise testçinin de işine yarar. Dördüncüsü pentest raporunu güvenlik programının kendisi saymak: rapor, ekibin kendi süreçlerinin ne kadar çalıştığını gösteren bir ölçümdür.

Küçük Bir Deneme

★☆☆ F-03 için kararı değiştir: Kestrel'in Server header'ını kapatan ayarı bul, uygula ve header'ın gelmediğini doğrulayan bir test yaz. Bu değişiklik bulgunun severity'sini değiştirir mi?

★★☆ docs/security/authorization-rules.md'deki her limit için kodda karşılığını ve testini bul. Testi olmayan limit kaldı mı? Kök neden grubunun önerdiği bu taramayı bir checklist'e dönüştür.

★★★ Bookstore için bir sonraki pentest'in rules of engagement'ını yaz: kapsam bu sefer back office. Hangi test hesapları gerekir, hangi veriler sentetik olmalı, durma koşulları neler olmalı?

Pentest, bulunmasını istediğimiz şeyleri bulmamız için yaptığımız kontrollü bir test. Serinin son yazısında kontrolün dışında olanı konuşuyoruz: production'da bir kurumun siparişlerinin başka bir kurum tarafından okunduğu bir güvenlik incident'ı ve ona tespit, kapsam belirleme, kanıtı koruma, iletişim ve bildirim adımlarıyla müdahale.