Uçtan Uca: Bookstore Pipeline'ında Güvenlik Gate'leri, İstisna Yönetimi ve Ölçüm
Gürültüsüz bir güvenlik pipeline'ı PR'da yalnız geliştiricinin dakikalar içinde düzeltebileceği kontrolleri, main'de derin kontrolleri çalıştırır; fix'i olan critical ve high bulgu kırar, gerisi sahibi ve bitiş tarihi olan istisnalarla izlenir. Bookstore'un tek workflow'unu, süresi dolan istisnayı yakalayan bir gate'i, zafiyet yaşı ve MTTR ölçümünü, OpenSSF Scorecard'ı ve kontrollerin OWASP Top 10:2025 eşlemesini gerçek çıktılarla gösteriyoruz.
Sürüm notu: Bu yazıdaki kodlar .NET 10 (SDK 10.0.402) ile derlenip denenmiştir; workflow'lar actionlint 1.7.12 ile doğrulanmış, image taramaları Trivy 0.76.2 ile yapılmış, OpenSSF Scorecard verisi 13 Kasım 2026'da Scorecard API'sinden alınmıştır.
Önce Tahmin Et
Bookstore'un pipeline'ında artık sekiz yazıdır kurduğumuz kontrollerin hepsi var: secret scanning, SCA (bağımlılıklardaki bilinen zafiyetleri ve lisansları tarayan analiz), SAST (kaynak kodu çalıştırmadan tarayan güvenlik testi), image ve IaC (Infrastructure as Code) taraması, DAST (çalışan uygulamaya dışarıdan saldırarak yapılan güvenlik testi), SBOM (yazılımın içerdiği bileşenlerin listesi), imza. Bir sabah ekip lideri iki soru soruyor: "Geçen ay bir high zafiyeti production'dan çıkarmak kaç gün sürdü?" ve "Şu anda kaç tane 'sonra bakarız' istisnamız var, hangisinin süresi doldu?" Kontrollerin hepsi çalışıyor; tahmin et, bu iki sorunun cevabı pipeline'ın neresinde duruyor?
Serinin son yazısında kontrolleri tek bir workflow'da birleştiriyoruz: PR'da hızlı, main'de derin kontroller, "fix'i olan critical ve high kırar" gate politikası, sahibi ve son kullanma tarihi olan istisnalar, zafiyet yaşı ve MTTR (bir zafiyetin ortalama düzeltilme süresi) ölçümü, OpenSSF (Open Source Security Foundation) Scorecard ile repo hijyeni ve her kontrolün OWASP (web uygulama güvenliği üzerine çalışan açık topluluk) Top 10:2025'te hangi riski kapattığını gösteren bir tablo.
Tek Workflow, İki Hız
İlk yazıda bir iskelet çizmiştik. Şimdi gerçek hâli: önceki yazılardaki SAST, DAST ve release workflow'larına workflow_call trigger'ı ekledim ve onları tek bir orkestrasyon workflow'undan çağırıyorum.
name: security
on:
pull_request:
push:
branches: [main]
permissions:
contents: read
concurrency:
group: security-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
# PR ve main: hızlı kontroller, paralel; bütçe 10 dakika
secrets:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
- name: Install gitleaks
env:
GITLEAKS_VERSION: 8.30.1
run: |
base="https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}"
file="gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz"
curl -sSfL -o "$file" "$base/$file"
curl -sSfL -o checksums.txt "$base/gitleaks_${GITLEAKS_VERSION}_checksums.txt"
grep " $file\$" checksums.txt | sha256sum -c -
tar -xzf "$file" gitleaks
- name: Scan new commits
env:
BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.before }}
run: ./gitleaks git --redact --no-banner --log-opts="$BASE_SHA..$GITHUB_SHA" .
dependencies:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-dotnet@0f4a354dd5547a28c70b7853dbd95d5b67886e73 # v6.1.0
with:
dotnet-version: 10.0.x
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: 24
- run: dotnet restore Bookstore.slnx -warnaserror:NU1903,NU1904
- run: npm ci --prefix web --ignore-scripts
- run: npm audit --prefix web --audit-level=high --omit=dev
sast:
permissions:
contents: read
security-events: write
uses: ./.github/workflows/sast.yml
test-and-exceptions:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-dotnet@0f4a354dd5547a28c70b7853dbd95d5b67886e73 # v6.1.0
with:
dotnet-version: 10.0.x
# DAST bulgularından türeyen regresyon testleri de burada koşar
- run: dotnet test --solution Bookstore.slnx
- name: Validate the exception register
run: dotnet run tools/security-gate.cs
# Yalnız main: derin kontroller, hızlıların hepsi geçtikten sonra
image:
if: github.event_name == 'push'
needs: [secrets, dependencies, sast, test-and-exceptions]
runs-on: ubuntu-latest
timeout-minutes: 15
env:
TRIVY_VERSION: 0.76.2
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-dotnet@0f4a354dd5547a28c70b7853dbd95d5b67886e73 # v6.1.0
with:
dotnet-version: 10.0.x
- uses: sigstore/cosign-installer@756fe2d284c7292c4e9b62c8a28f1e5629c59532 # v4.1.3
- name: Install and verify Trivy
run: |
base="https://github.com/aquasecurity/trivy/releases/download/v${TRIVY_VERSION}"
file="trivy_${TRIVY_VERSION}_Linux-64bit.tar.gz"
curl -sSfL -o "$file" "$base/$file"
curl -sSfL -o "$file.sigstore.json" "$base/$file.sigstore.json"
cosign verify-blob "$file" --bundle "$file.sigstore.json" \
--certificate-oidc-issuer=https://token.actions.githubusercontent.com \
--certificate-identity "https://github.com/aquasecurity/trivy/.github/workflows/reusable-release.yaml@refs/tags/v${TRIVY_VERSION}"
tar -xzf "$file" trivy
- run: docker build -t "bookstore-api:$GITHUB_SHA" .
- run: ./trivy image --scanners vuln -f json -o trivy.json "bookstore-api:$GITHUB_SHA"
# fix'i olan critical/high kırar, süreli istisnalar hariç; gerisi raporlanır
- run: dotnet run tools/security-gate.cs -- trivy.json
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
if: always()
with:
name: trivy-${{ github.sha }}
path: trivy.json
dast:
if: github.event_name == 'push'
needs: [secrets, dependencies, sast, test-and-exceptions]
uses: ./.github/workflows/dast.yml
secrets:
DAST_SIGNING_KEY: ${{ secrets.DAST_SIGNING_KEY }}
release:
needs: [image, dast]
permissions:
contents: read
packages: write
id-token: write
attestations: write
artifact-metadata: write
uses: ./.github/workflows/release-image.yml
secrets: inherit
Dört workflow dosyasını (orkestrasyon, SAST, DAST ve release) actionlint 1.7.12 ile birlikte doğruladım; actionlint reusable workflow çağrılarının secret ve izin uyumunu da kontrol ediyor. GitHub üzerinde çalıştırmadım; adımların komutlarını önceki yazılarda tek tek yerelde koşturmuştuk.
Yapının mantığı şu. PR'da yalnız kodu yazan kişinin dakikalar içinde düzeltebileceği kontroller çalışıyor ve dördü paralel: secret, bağımlılık, SAST ve test. Toplam süre en yavaş job kadar; Bookstore için bütçe 10 dakika ve her job'ın timeout-minutes değeri bunu zorluyor. Main'de image taraması ve DAST, hızlı kontrollerin hepsi geçtikten sonra yine paralel başlıyor; release (imza, attestation, SBOM yüklemesi) ise ikisi de geçerse çalışıyor. Altıncı yazıda aktif ZAP (açık kaynak web güvenlik tarama aracı) taraması tek başına iki dakika sürmüştü; bu yüzden DAST'ı PR'a koymuyoruz. concurrency ayarı aynı PR'a yeni bir commit geldiğinde eski koşuyu iptal ediyor, main'de ise iptal etmiyor; bir main koşusunun yarıda kesilmesi, imzalanmamış bir release bırakabilir. Build'in reusable workflow'da yapılması, sekizinci yazıdaki SLSA (Supply-chain Levels for Software Artifacts) Build L3 yoluna da hazırlık.
Gate Politikası: Neyi Kırarız, Neyi Raporlarız
Yanlış alarm vermeyen bir gate'in kuralı, herkesin ezbere bileceği kadar basit olmalı. Bookstore'unki şu: fix'i olan critical ve high bulgu build'i kırar; fix'i olmayan ya da medium ve altındaki bulgular raporlanır ve izlenir; secret ve SAST'ta yalnız PR'ın eklediği yeni bulgu kırar. "Fix'i olan" kısmı önemli: kırılan bir build'in geliştiriciye bir eylem sunması gerekir. Beşinci yazıdaki libc6 bulgularının fix'i base image'ın yeni sürümünde; geliştirici bugün yapabileceği bir şey olmadığı için onlar build'i kırmıyor ama listede kalıyor.
Ama tek tek araçların kendi ignore mekanizmaları (NuGetAuditSuppress, .trivyignore.yaml, alertFilter) dağınık kalınca "kaç istisnamız var?" sorusunun cevabı yedi dosyaya dağılıyor. Bookstore'da image gate'i için istisnaları tek bir kayıtta topladım.
İstisnalar: Sahibi ve Son Kullanma Tarihi Olan Borç
{
"exceptions": [
{
"id": "CVE-2024-0056",
"package": "Microsoft.Data.SqlClient",
"owner": "team-orders",
"reason": "Upgrade to SqlClient 7 ships in 1.5.0; the major upgrade needs its own PR and test run.",
"expires": "2026-11-30"
},
{
"id": "CVE-2023-36414",
"package": "Azure.Identity",
"owner": "team-orders",
"reason": "Removed together with SqlClient 5.1.1.",
"expires": "2026-11-01"
}
]
}
Gate'i bir .NET 10 file-based app olarak yazdım; repo'da bir proje dosyası olmadan dotnet run tools/security-gate.cs ile çalışıyor:
using System.Globalization;
using System.Text.Json;
using System.Text.Json.Serialization;
// Kullanım: dotnet run tools/security-gate.cs -- [trivy-report.json] [--today yyyy-MM-dd]
// Rapor verilmezse yalnız istisna kaydı doğrulanır (PR job'ı)
string? reportPath = args.Length > 0 && !args[0].StartsWith("--") ? args[0] : null;
DateOnly today = args.Contains("--today")
? DateOnly.ParseExact(args[Array.IndexOf(args, "--today") + 1], "yyyy-MM-dd", CultureInfo.InvariantCulture)
: DateOnly.FromDateTime(DateTime.UtcNow);
// File-based app'ler varsayılan olarak AOT uyumlu: reflection yerine source generator
List<SecurityException> exceptions = JsonSerializer.Deserialize(
File.ReadAllText("security/exceptions.json"), GateJsonContext.Default.ExceptionFile)!.Exceptions;
int invalid = 0;
foreach (SecurityException e in exceptions)
{
// Sahibi, gerekçesi olmayan ya da 90 günden uzun istisna kabul edilmez
if (string.IsNullOrWhiteSpace(e.Owner) || string.IsNullOrWhiteSpace(e.Reason) || e.Expires > today.AddDays(90))
{
Console.WriteLine($"INVALID {e.Id} {e.Package}: owner, reason and an expiry within 90 days are required");
invalid++;
}
else if (e.Expires < today)
{
Console.WriteLine($"EXPIRED {e.Id} {e.Package} expired {e.Expires:yyyy-MM-dd} (owner {e.Owner})");
}
}
int blocking = 0;
using JsonDocument report = JsonDocument.Parse(reportPath is null ? """{"Results":[]}""" : File.ReadAllText(reportPath));
foreach (JsonElement result in report.RootElement.GetProperty("Results").EnumerateArray())
{
if (!result.TryGetProperty("Vulnerabilities", out JsonElement vulnerabilities))
{
continue;
}
foreach (JsonElement v in vulnerabilities.EnumerateArray())
{
string id = v.GetProperty("VulnerabilityID").GetString()!;
string package = v.GetProperty("PkgName").GetString()!;
string severity = v.GetProperty("Severity").GetString()!;
string fix = v.TryGetProperty("FixedVersion", out JsonElement f) ? f.GetString() ?? "" : "";
string finding = $"{severity,-8} {package} {v.GetProperty("InstalledVersion").GetString()} {id}";
SecurityException? active = exceptions.FirstOrDefault(e =>
e.Id == id && e.Package == package && e.Expires >= today && !string.IsNullOrWhiteSpace(e.Owner));
if (active is not null)
{
Console.WriteLine($"EXCEPT {finding} until {active.Expires:yyyy-MM-dd} ({active.Owner})");
}
else if (severity is "CRITICAL" or "HIGH" && fix.Length > 0)
{
Console.WriteLine($"BLOCK {finding} (fix: {fix})");
blocking++;
}
else
{
Console.WriteLine($"REPORT {finding}");
}
}
}
Console.WriteLine($"Result: {blocking} blocking finding(s), {invalid} invalid exception(s)");
return blocking + invalid > 0 ? 1 : 0;
record SecurityException(string Id, string Package, string Owner, string Reason, DateOnly Expires);
record ExceptionFile(List<SecurityException> Exceptions);
[JsonSourceGenerationOptions(JsonSerializerDefaults.Web)]
[JsonSerializable(typeof(ExceptionFile))]
partial class GateJsonContext : JsonSerializerContext;
İlk denemede JsonSerializer.Deserialize<ExceptionFile> kullanmıştım ve araç Reflection-based serialization has been disabled for this application exception'ıyla düştü. .NET 10'un file-based app'leri varsayılan olarak PublishAot açık geliyor ve reflection tabanlı serileştirme kapalı; source generator'lı JsonSerializerContext bunu çözdü. İkinci bir sürpriz NuGetAudit'ten geldi: makinemdeki SDK 10.0.402 ile araç restore edilirken AOT (uygulama çalışmadan önce makine koduna derleme) derleyici paketi Microsoft.DotNet.ILCompiler 10.0.13 için NU1903 (high) uyarısı çıktı; advisory'deki düzeltme 10.0.14'te ve 10 Kasım'da yayımlanan SDK 10.0.404 bunu getiriyor. Güvenlik aracımız da bir bağımlılık; üstelik solution'da olmadığı için dependencies job'ının restore'u onu görmüyor.
Gate'i önce Bookstore 1.4.0 image'ının gerçek Trivy raporuyla, 13 Kasım tarihiyle çalıştırdım:
dotnet run tools/security-gate.cs -- reports/image-1.4.0.json --today 2026-11-13
EXPIRED CVE-2023-36414 Azure.Identity expired 2026-11-01 (owner team-orders)
REPORT MEDIUM libc6 2.39-0ubuntu8.9 CVE-2026-18374
REPORT MEDIUM libc6 2.39-0ubuntu8.9 CVE-2026-84243
REPORT MEDIUM libc6 2.39-0ubuntu8.9 CVE-2026-8674
REPORT MEDIUM libc6 2.39-0ubuntu8.9 CVE-2026-86805
REPORT MEDIUM libc6 2.39-0ubuntu8.9 CVE-2026-95818
REPORT MEDIUM libc6 2.39-0ubuntu8.9 CVE-2026-97399
BLOCK HIGH Azure.Identity 1.7.0 CVE-2023-36414 (fix: 1.10.2)
REPORT MEDIUM Azure.Identity 1.7.0 CVE-2024-29992
REPORT MEDIUM Azure.Identity 1.7.0 CVE-2024-35255
EXCEPT HIGH Microsoft.Data.SqlClient 5.1.1 CVE-2024-0056 until 2026-11-30 (team-orders)
Result: 1 blocking finding(s), 0 invalid exception(s)
Exit code 1. SqlClient bulgusu geçerli bir istisnayla geçti; Azure.Identity istisnasının süresi 1 Kasım'da dolmuştu, bu yüzden aynı bulgu artık kırıyor. Süresi dolan istisnanın sessizce uzamaması bu tasarımın bütün amacı. SqlClient 7'ye geçen 1.5.0 image'ında altı libc6 bulgusu REPORT olarak kaldı ve sonuç 0 blocking finding(s) oldu; EXPIRED satırı ise kayıttan silinmeyi bekleyen bir artık olarak görünmeye devam etti. Sahibi boş ve bitiş tarihi 2027 sonu olan bir istisnayla denediğimde araç INVALID satırı yazıp 1 döndü ve istisna bulguyu bastırmadı.
Yeni Kod ve Mevcut Borç
İlk yazıdaki ilke, "yeni bulguyu engelle, eskiyi backlog'a al", her kontrolde farklı bir mekanizmayla uygulanıyor. Secret ve SAST'ta baseline var: gitleaks yalnız PR'ın commit aralığını, Semgrep yalnız base branch'e göre yeni bulguları kırıyor. Bağımlılık ve image'da ise "yeni" kavramı zayıf: dün temiz olan bir paket bugün yayımlanan bir advisory ile kırmızıya döner ve bu geliştiricinin PR'ının suçu değildir. Bu yüzden orada kırma kararı yeniliğe değil, eylem imkânına bağlı: fix varsa kırar, ekip daha fazla zamana ihtiyaç duyuyorsa sahibi ve tarihi olan bir istisna açar. Mevcut borç böylece görünür ve süreli kalır; gate'i susturmak için kimsenin bir eşiği düşürmesi gerekmez.
Önemli bir ayrım: VEX (bir zafiyetin üründe gerçekten istismar edilebilir olup olmadığını bildiren doküman) ile istisna aynı şey değil. Yedinci yazıdaki VEX "bu sürüm etkilenmiyor, çünkü..." diyen kalıcı bir beyan; istisna ise "etkileniyoruz, riski şu tarihe kadar kabul ediyoruz" diyen geçici bir karar. İlki tarayıcının bulguyu hiç üretmemesini sağlar, ikincisi bulgunun gate'i geçici olarak geçmesini.
Ölçmek: Zafiyet Yaşı, MTTR ve İstisna Sayısı
Açılıştaki soruların cevabı bir metrikte. Üç metrik Bookstore için yeterli: açık zafiyetlerin yaşı (en eski açık high kaç gündür bekliyor), MTTR (bir zafiyetin ilk görüldüğü günden kapandığı güne ortalama süre) ve aktif ile süresi dolmuş istisna sayısı. Bunun için her main koşusunun Trivy raporunu bir geçmiş dosyasıyla karşılaştıran ikinci bir file-based app yazdım; yeni bulguya ilk görülme tarihini, rapordan kaybolan bulguya kapanma tarihini yazıyor. Çekirdeği şu:
// Raporda artık olmayan açık bulgu kapanmıştır
foreach ((string key, Finding finding) in history.Where(h => h.Value.FixedOn is null && !seen.Contains(h.Key)).ToList())
{
history[key] = finding with { FixedOn = date };
}
Üç gerçek raporu, tarihlerini parametreyle vererek sırayla besledim: beşinci yazıdaki eski image (5 Ekim), 1.4.0 (26 Ekim) ve 1.5.0 (13 Kasım):
dotnet run tools/vuln-metrics.cs -- reports/legacy.json history.json --date 2026-10-05
dotnet run tools/vuln-metrics.cs -- reports/image-1.4.0.json history.json --date 2026-10-26
dotnet run tools/vuln-metrics.cs -- reports/image-1.5.0.json history.json --date 2026-11-13
2026-10-05: 12 open, 0 fixed
HIGH 2 open, oldest 0 days; no fixes yet
MEDIUM 10 open, oldest 0 days; no fixes yet
2026-10-26: 10 open, 2 fixed
HIGH 2 open, oldest 21 days; no fixes yet
MEDIUM 8 open, oldest 21 days; MTTR 21.0 days over 2 fixed
2026-11-13: 6 open, 6 fixed
HIGH 0 open; MTTR 39.0 days over 2 fixed
MEDIUM 6 open, oldest 39 days; MTTR 30.0 days over 4 fixed
İki high bulgunun kapanması 39 gün sürdü; gate kuralımız onları ancak istisnanın süresi dolunca zorladı. Altı medium bulgu 39 gündür açık ve fix'leri var: base image'ı yeniden build etmeyi bir alışkanlığa çevirmediğimizin ölçülebilir kanıtı. İlk koşuda MTTR çıktısı 21,0 gibi virgüllü geldi; makinenin kültürü Türkçe olduğu için. Pipeline çıktısı bir dashboard'a ya da başka bir araca gidecekse sayıları CultureInfo.InvariantCulture ile biçimlendir; ben de öyle düzelttim.
Bu küçük araç fikri göstermek için; production'da aynı metrikleri yedinci yazıdaki Dependency-Track'in bulgu geçmişinden ya da GitHub'ın security overview'undan almak daha sağlam. Geçmiş dosyasını actions/cache'te tutma; cache bir depolama değil, silinebilir. Metrikleri sprint review'da konuşulan iki sayıya indir: "en eski açık high kaç gün?" ve "süresi dolmuş kaç istisna var?".
Repo Hijyeni: OpenSSF Scorecard
Buraya kadar ürünü ölçtük; Scorecard ise repo'nun kendisini ölçüyor: workflow token'larının yetkileri, bağımlılıkların hash ile sabitlenmesi, branch protection, imzalı release'ler, SAST kullanımı gibi bir dizi kontrolü 0–10 arası puanlıyor. Herkese açık repo'ların sonuçları bir API'de; beşinci yazıda konuştuğumuz Trivy repo'suna bakıp 8'in altındaki kontrolleri listeledim:
curl -s https://api.scorecard.dev/projects/github.com/aquasecurity/trivy \
| jq -r '.date, .score, (.checks[] | select(.score < 8) | "\(.score)\t\(.name)")'
2026-03-16
6.4
0 CII-Best-Practices
0 Token-Permissions
0 Fuzzing
3 Signed-Releases
4 Branch-Protection
7 Pinned-Dependencies
1 SAST
API'deki son değerlendirme 16 Mart 2026 tarihli; Token-Permissions için gerekçe detected GitHub workflow tokens with excessive permissions. Scorecard bir olayı öngörmez ve bir puandan sorumluluk çıkarmak doğru değil; ama bu kontrolün ölçtüğü şey, yani workflow token'larının en az yetkiyle çalışması, bu serinin ve GitHub Actions serisinin konusu. Bookstore'da Scorecard'ı ossf/scorecard-action ile haftalık bir job'da çalıştırıp SARIF (statik analiz sonuçları için standart JSON formatı) çıktısını code scanning'e yüklemek, repo ayarlarındaki gerilemeyi bir PR yorumu kadar kolay görmeni sağlar; bu job'ı bu yazıda çalıştırmadım.
Hangi Kontrol Hangi Riski Kapatıyor
| Kontrol (yazı) | Pipeline'daki yeri | Kapattığı risk | OWASP Top 10:2025 |
|---|---|---|---|
| Secret scanning (2) | pre-commit, PR, push protection | Koda gömülü credential | A07 (CWE-798), A04 |
| SCA ve önceliklendirme (3) | PR, Dependabot | Bilinen zafiyetli bağımlılık | A03 |
| SAST (4) | PR, baseline'lı | Injection, path traversal, zayıf kripto çağrısı | A05, A01, A04 |
| Image ve IaC taraması (5) | main | İşletim sistemi paketleri, privileged container, açık storage | A02, A03 |
| DAST (6) | main, ephemeral ortam | Header, cookie, hata sızıntısı, reflected XSS (Cross-Site Scripting) | A02, A05, A10 |
| SBOM ve VEX (7) | main, release | "Etkileniyor muyuz?" sorusuna cevapsızlık | A03 |
| İmza, provenance, admission (8) | release, deploy | Değiştirilmiş ya da başka yerde üretilmiş artifact | A08, A03 |
| Gate, istisna, metrik (9) | hepsi | Kontrollerin sessizce kapanması | süreç |
Tablonun boş bıraktığı yerler de bir şey söylüyor. A06 (Insecure Design) ve A09 (Security Logging and Alerting Failures) için pipeline'da bir tarayıcı yok; onlar threat modeling, code review ve runtime izlemeyle kapanıyor. A01'in IDOR (yetki kontrolü yapılmadan id ile başkasının kaydına erişilmesi) kısmını altıncı yazıda gördüğümüz gibi DAST bulamıyor; onu API Güvenliği serisindeki authorization regresyon testleri kapatıyor. Kategorilerin ayrıntısı OWASP serisinin "Web Güvenliğine Giriş: OWASP Top 10:2025 Nedir, Nasıl Okunur?" yazısında.
Kapanış için Bookstore'un kontrol listesi:
- Her geliştirici makinesinde gitleaks pre-commit hook'u var, GitHub'da push protection açık.
- NuGetAudit ve npm audit high ve critical'da restore'u kırıyor; Dependabot security ve version update'leri ayrı.
- SAST PR'da yalnız yeni bulguyu kırıyor; Bookstore'a özel kurallar test ediliyor.
- Image chiseled ve non-root; fix'i olan critical ve high image'ı main'de durduruyor; base image düzenli rebuild ediliyor.
- DAST ephemeral ortamda, authenticated ve oturum kontrolüyle main'de çalışıyor; bulgular regresyon testine dönüyor.
- Her release'in image SBOM'u attestation olarak image'a bağlı ve Dependency-Track'te sorgulanabilir; VEX belgeleri sürümle versiyonlanıyor.
- Image keyless imzalı, cluster imzasız ve tag'li image'ı reddediyor.
- Her istisnanın sahibi, gerekçesi ve en fazla 90 günlük bitiş tarihi var; en eski açık high ve süresi dolmuş istisna sayısı her sprint konuşuluyor.
Üretimde Dikkat
Gate'i ilk gün sıkı açma. Yeni bir kontrolü önce rapor modunda bir iki sprint çalıştır, mevcut borcu istisna kaydına al, sonra kırmaya başla. Bir gün kırmızıya dönen ve kimsenin nedenini bilmediği bir pipeline, ertesi gün continue-on-error: true ile "geçici olarak" kapatılır.
Süre bütçesini ölç ve koru. Job sürelerini haftalık izle; PR'daki hızlı kontrollerin toplamı bütçeyi aşmaya başladığında en yavaşını main'e ya da gecelik zamanlamaya taşı, PR'ı yavaşlatarak değil.
İstisna kaydını code review'dan geçir ve sahibini bir ekip olarak yaz, kişi olarak değil; kişi ekipten ayrıldığında istisna sahipsiz kalır. Süresi dolmak üzere olan istisnalar için bir hatırlatıcı (haftalık bir job ya da issue) koy.
Güvenlik araçlarını da bağımlılık say. Bu yazıda gate aracının kendisi, eski bir SDK yüzünden bir NuGetAudit uyarısı üretti; tarayıcı binary'lerini imzasıyla doğruluyor, action'ları SHA (yaygın kullanılan hash algoritması ailesi) ile sabitliyor ve SDK'yı güncel tutuyoruz.
Sık Düşülen Tuzaklar
Birincisi, her kontrolü PR'a koymak. Yavaş bir PR pipeline'ı, geliştiricileri kontrolleri atlatmanın yollarını aramaya iter; derin kontrollerin yeri main.
İkincisi, istisnaları her aracın kendi dosyasına dağıtmak ve hiçbirine bitiş tarihi koymamak. Sahibi ve tarihi olmayan bir ignore satırı, kalıcı bir kör noktadır.
Üçüncüsü, eşiği düşürerek gate'i susturmak. "Yalnız critical kırsın" kararı, high bulguları sessizce production'a taşır; doğru araç gerekçeli ve süreli istisna.
Dördüncüsü, yalnız bulgu sayısını ölçmek. Sayı, yeni bir tarayıcı eklendiğinde artar, biri kapatıldığında düşer; yaş ve MTTR ise ekibin davranışını ölçer.
Beşincisi, programı bitmiş saymak. Kontroller, araçlar ve tehditler değişiyor; bu serinin araç sürümleri de birkaç ay içinde eskiyecek. Gate'leri ve istisna kaydını yılda en az iki kez baştan gözden geçir.
Küçük Bir Deneme
★☆☆ Projenin pipeline'ındaki her güvenlik kontrolünü PR ve main sütunlarına yerleştir, her birinin son bir aydaki ortalama süresini yaz. PR'ın toplam süresi bütçeni aşıyor mu?
★★☆ Kendi tarayıcının JSON (JavaScript Object Notation) raporu için bu yazıdaki gibi bir gate yaz: fix'i olan critical ve high kırsın, istisnalar tek bir dosyadan gelsin. Süresi geçmiş bir istisnayla gate'in gerçekten kırıldığını gör.
★★★ Son üç ayın tarama raporlarından (ya da Dependabot alert'lerinden) zafiyet yaşı ve MTTR hesapla, ardından kendi repo'nun Scorecard sonucuna bak. En düşük iki kontrol için birer PR aç.
Seri burada bitiyor ama konu bitmiyor. Pipeline'daki secret'ların nerede saklanacağı ve nasıl rotate edileceği ilerideki Configuration ve Secret Management serisinin, analyzer'ların ve kalite kapılarının kurulumu Statik Analiz serisinin, imzaların arkasındaki kriptografi ise Geliştirici için Kriptografi serisinin konusu. Bookstore'un pipeline'ı artık güvenliği bir kontrol listesi değil, ölçülen ve sahibi olan bir süreç olarak taşıyor.