SAST ve DAST Arasındaki Fark Nedir? 10 Açıklı Bir Uygulamada Semgrep, Bandit ve OWASP ZAP'ı Denedim

Bir Flask uygulamasına 10 güvenlik açığı koydum. SAST 8'ini, DAST 5'ini buldu; ikisi birlikte 9'unu. Unutulmuş backup dosyasını hiçbiri bulamadı.

Yazan: Mirac Can Yılmaz 19 dk okuma
SAST ve DAST Arasındaki Fark Nedir? 10 Açıklı Bir Uygulamada Semgrep, Bandit ve OWASP ZAP'ı Denedim - Security yazısının kapak görseli

"Pipeline'da güvenlik taraması var" dendiğinde çoğu zaman kastedilen, bir kere eklenip unutulmuş tek bir araç. Neyi kaçırdığına kimse bakmıyor. Kimi ekip kodu tarayan bir araç seçiyor, kimi web uygulamasını tarayan; ikisi de korunduğunu düşünüyor. Ben bunu sayıyla görmek istedim. Küçük bir Flask uygulaması yazıp içine kendi elimle 10 güvenlik açığı koydum. Aynı commit üzerinde iki statik tarayıcıyı ve bir dinamik tarayıcıyı iki farklı modda çalıştırdım, hangi açığı hangisinin bulduğunu tek tek saydım. SAST 8'ini, DAST 5'ini buldu; ikisi birlikte 9'unu. İçinde veritabanı şifresi olan unutulmuş bir backup dosyasını ise hiçbir tarama bulamadı. Bu yazıda SAST ve DAST'ın ne olduğunu, her birinin neyi görüp neyi göremediğini, ikisini GitHub Actions ya da GitLab CI pipeline'ına nasıl koyacağınızı ve lab'ın ne gösterdiğini anlatıyorum.

SAST nedir? DAST nedir?

SAST (Static Application Security Testing) kaynak kodu çalıştırmadan okur. Veriyi girişten (request.args) tehlikeli çağrılara (cursor.execute, subprocess.run, pickle.loads) kadar takip eder, dosya ve satır numarası verir. Pull request'te, daha hiçbir şey build edilmeden, saniyeler içinde çalışır.

DAST (Dynamic Application Security Testing) kodu hiç görmez. Çalışan uygulamayla HTTP üzerinden bir saldırgan gibi konuşur: sayfaları gezer, header'lara ve cookie'lere bakar, bulduğu her parametreye ', <script>, ; cat /etc/passwd gibi payload'lar gönderir. URL ve parametre verir. Bunun için deploy edilmiş bir uygulama gerekiyor, genelde staging ortamı.

Lab'daki araçlar:

  • Semgrep: birçok dil için pattern ve taint kuralları; neye bakacağını p/python, p/secrets, p/security-audit gibi kural paketleri belirliyor.
  • Bandit: PyCQA'nın sadece Python için statik tarayıcısı, sabit bir kontrol listesi var (B101...B703).
  • OWASP ZAP: açık kaynak web tarayıcı. zap-baseline.py sadece gezer ve izler (pasif); zap-full-scan.py bunlara ek olarak saldırı da yapar (aktif).

Bunların yanında bilmekte fayda olan iki yöntem daha var: SCA (software composition analysis, örneğin Trivy ya da Dependabot) bağımlılıklarda bilinen CVE'lere bakar; IAST çalışan uygulamanın içine bir ajan koyup hem isteği hem kod yolunu görür. İkisi de bu lab'ın konusu değil.

SAST ve DAST hangi sorunu çözüyor?

Prod'a çıkan açık pahalıya patlıyor ve review'da kolayca gözden kaçıyor. f-string ile kurulmuş bir SQL sorgusu diff'te masum görünüyor. Eksik bir HttpOnly flag'i ise diff'te hiç görünmüyor, çünkü kimsenin yazmadığı bir varsayılan değer. Güvenlik taraması bunları bir kullanıcıdan ya da saldırgandan önce yakalamak için var.

Her tarama türü başka bir yerde kör. SAST sunucunun gerçekte ne cevap verdiğini göremez: response header'ları, framework'ün koyduğu cookie flag'leri, birinin static/ altına kopyaladığı bir dosya. DAST da ulaşamadığı kod yolunu göremez, sadece gönderdiği payload'ların tetiklediği açıkları bulur. Bunlardan sadece birini koşan ekip, öbürünün bulacağı açığı prod'a taşır.

Ne kadar erken, o kadar ucuz. Pull request'te çalışan SAST, değişiklik henüz yazarın aklındayken sorunlu satırı gösterir ("shift left"). DAST ise build edilip ayarlanmış ve deploy edilmiş uygulamanın gerçekte nasıl davrandığını gösterir; bu, saldırganın gördüğüne daha yakın. İkisine de ihtiyaç var, ama farklı aşamalarda.

SAST ve DAST: avantajlar ve dezavantajlar

SAST DAST
İhtiyacı kaynak kod çalışan uygulama (URL)
Pipeline'da yeri her pull request, build'den önce staging'e deploy'dan sonra
Bu lab'da süre 6–12 sn pasif 60 sn, aktif 731 sn
Raporu dosya ve satır URL, parametre, payload
İyi olduğu yer injection noktaları, koddaki secret'lar, zayıf kripto, debug=True, güvensiz deserialization header'lar, cookie'ler, sunucu ayarı, gerçekten enjekte edilebilen parametreler, hata sayfaları
Kör olduğu yer runtime ayarı, header'lar, kimsenin referans vermediği dosyalar ulaşamadığı kod, denemediği payload türleri
False positive daha çok (veri akışı hakkında tahmin yürütüyor) injection'da daha az (cevabı gördü), header'larda gürültülü
Dil dile özel dilden bağımsız
Düzeltme maliyeti düşük, yazar hâlâ değişikliğin başında daha yüksek, değişiklik merge olmuş

SAST ve DAST bir CI/CD pipeline'ına nasıl yerleşir?

Diyagram: SAST ve DAST'ın pipeline'daki yeri. Pull request önce SAST'a gidiyor (Semgrep ve Bandit kaynağı okuyor, 6–12 saniye, dosya ve satır veriyor, ERROR'da merge'ü durduruyor), sonra image build (SAST kalırsa atlanıyor; açıklı sürüm 8. saniyede durdu), sonra DAST (OWASP ZAP çalışan uygulamaya saldırıyor, baseline 60 saniye, full scan 731 saniye, URL ve parametre veriyor), sonra prod. Altta: SAST kod yollarını, secret'ları, zayıf kriptoyu ve tehlikeli çağrıları görüyor ama header'lara, sunucu ayarına ve referanssız dosyalara kör; DAST sunucunun ne cevap verdiğini görüyor ama ulaşamadığı koda ve denemediği payload'lara kör

Kullandığım kural: SAST, ERROR seviyesinde merge'ü durdurur; DAST, High risk alarmda release'i durdurur. Bunun altındaki her şey rapor, gate değil. Eşiğin neden önemli olduğunu aşağıda, Nerede patladı #5'te anlattım.

GitHub Actions örneği

# .github/workflows/security.yml
name: security
on:
  pull_request:
  push:
    branches: [main]

permissions:
  contents: read

jobs:
  sast:
    runs-on: ubuntu-24.04
    container: semgrep/semgrep:1.182.1
    steps:
      - uses: actions/checkout@v5
      - name: Semgrep
        run: >
          semgrep scan --metrics=off
          --config p/python --config p/secrets --config p/security-audit
          --severity ERROR --error
          --sarif -o semgrep.sarif .
      - uses: actions/upload-artifact@v4
        if: always()
        with:
          name: semgrep
          path: semgrep.sarif

  dast:
    needs: sast
    runs-on: ubuntu-24.04
    steps:
      - uses: actions/checkout@v5
      - name: Build and start the app
        run: |
          docker build -t shop:${{ github.sha }} app
          docker run -d --name shop -p 8080:8080 shop:${{ github.sha }}
          for i in $(seq 30); do curl -sf -o /dev/null http://localhost:8080/ && break; sleep 1; done
      - name: ZAP full scan
        run: |
          mkdir -p zap && chmod 777 zap
          docker run --rm --network host -v "$PWD/zap:/zap/wrk:rw" zaproxy/zap-stable:2.17.0 \
            zap-full-scan.py -t http://localhost:8080 -m 2 -J report.json -r report.html -I
      - name: Fail on High risk alerts
        env:
          ZAP_IGNORE: "43"   # reviewed false positives, reasons in .zap/ignore.tsv
        run: |
          high=$(jq '[.site[].alerts[] | select(.riskcode == "3")
                      | select(.pluginid as $p | env.ZAP_IGNORE | split(",") | index($p) | not)] | length' zap/report.json)
          echo "High risk alerts: $high"
          test "$high" -eq 0
      - uses: actions/upload-artifact@v4
        if: always()
        with:
          name: zap
          path: zap/

-I parametresi ZAP'ın uyarılar yüzünden kendi kendine fail etmesini engelliyor; kararı jq adımı veriyor. ZAP_IGNORE içinde, biri tarafından incelenip false positive olarak işaretlenmiş alarmların plugin id'leri duruyor; her birinin gerekçesi workflow'un yanındaki bir dosyada. Bu listeye neden ihtiyaç duyduğumu Nerede patladı #3'te anlattım. Bir rules dosyası yazmazsanız ZAP'ın kendi exit kodları (FAIL için 1, WARN için 2), eksik bir Permissions-Policy header'ına SQL injection ile aynı muameleyi yapıyor. Ben gate'in okunabilir tek bir satır olmasını istedim. Gerçek bir staging URL'i için uygulamayı başlatan docker run yerine ortamınızın URL'ini yazın.

GitLab CI örneği

# .gitlab-ci.yml
stages: [sast, build, dast]

variables:
  IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA

semgrep:
  stage: sast
  image: semgrep/semgrep:1.182.1
  script:
    - >
      semgrep scan --metrics=off
      --config p/python --config p/secrets --config p/security-audit
      --severity ERROR --error
      --gitlab-sast -o gl-sast-report.json .
  artifacts:
    when: always
    reports:
      sast: gl-sast-report.json

build:
  stage: build
  image: docker:28.5
  services: [docker:28.5-dind]
  script:
    - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" "$CI_REGISTRY"
    - docker build -t "$IMAGE" app
    - docker push "$IMAGE"

zap:
  stage: dast
  image: zaproxy/zap-stable:2.17.0
  services:
    - name: $IMAGE
      alias: shop
  variables:
    ZAP_IGNORE: "43"   # reviewed false positives, reasons in .zap/ignore.tsv
  script:
    - mkdir -p /zap/wrk
    - zap-full-scan.py -t http://shop:8080 -m 2 -J report.json -r report.html -I
    - cp /zap/wrk/report.* .
    - >
      python3 -c "import json,os,sys; ig=os.environ['ZAP_IGNORE'].split(',');
      a=[x for s in json.load(open('report.json'))['site'] for x in s['alerts'] if x['riskcode']=='3' and x['pluginid'] not in ig];
      print('High risk alerts:', len(a)); sys.exit(1 if a else 0)"
  artifacts:
    when: always
    paths: [report.html, report.json]

--gitlab-sast, raporu GitLab'ın merge request widget'ında gösterebileceği formatta yazıyor. GitLab'ın kendi SAST.gitlab-ci.yml ve DAST template'leri de var; ben aynı komutlar iki platformda da, yerelde de çalışsın diye araçları açıkça yazdım. İki dosya da bir YAML parser ile kontrol edildi; bu yazı için GitHub'da ya da GitLab'da koşmadım. Lab aynı komutları yerelde koştu.

Lab: 10 bilinen açık, 5 tarama

Neden OWASP Juice Shop değil de kendi uygulamam? Bir tarayıcının kaç açık bulduğunu ölçmek için kaç açık olduğunu bilmem gerekiyor. Juice Shop'ta yüzden fazla challenge var ve tarayıcı çıktısını onlara eşlemek başlı başına bir proje. Benim uygulamam 90 satır civarında bir Flask uygulaması: üç formlu bir ana sayfa, cookie koyan bir login, o cookie'yi okuyan bir profil sayfası. Koyduğum her açığın satırında # Vn etiketi var, böylece bir script her bulguyu dosya ve satıra (SAST) ya da ZAP kural id'sine (DAST) göre bir açığa eşliyor. Hiçbir açığa eşlenmeyen bulguları "ekstra" olarak ayırıp elle kontrol ettim.

# Koyduğum açık Nerede görünür
V1 SQL injection: /user'da execute() içinde f-string kod ve runtime
V2 OS command injection: /ping'de subprocess.run(..., shell=True) kod ve runtime
V3 Koda gömülü secret'lar: Flask SECRET_KEY ve bir AWS anahtar çifti kod
V4 Güvensiz deserialization: session cookie'sine pickle.loads kod ve runtime
V5 app.run(debug=True): Werkzeug debugger kod ve runtime
V6 Şifreler için md5 kod
V7 Güvenlik header'ları yok (CSP, X-Frame-Options, ...) runtime
V8 HttpOnly, Secure, SameSite olmayan cookie runtime (sonradan anlaşıldı ki kodda da)
V9 Reflected XSS ve template injection: /search'te render_template_string("..." + q) kod ve runtime
V10 Hiçbir yerden link verilmeyen, içinde DB şifresi olan static/config.php.bak runtime

Tüm tarayıcıları aynı Docker ağında container olarak çalıştırdım; ZAP uygulamaya http://vulnapp:8080 adresinden ulaştı.

A. SAST: yaygın kural paketleriyle Semgrep

p/python ve p/secrets ile semgrep scan: 12 saniyede 18 bulgu. 13. ve 14. satırda AWS anahtar çifti için ERROR (V3), 50. satırda tainted-sql-string (V1), 57. satırda subprocess-injection, dangerous-subprocess-use ve subprocess-shell-true (V2); 30. satırda md5 (V6), 64. satırda raw-html-format (V9), 87. satırda debug-enabled (V5) için WARNING; 58. ve 81. satırlarda benim koymadığım gerçek XSS'ler için raw-html-format. Bulunan: 10'da 6. Kaçan: V4 pickle, V8 cookie flag'leri, V7 header'lar, V10 backup dosyası

"CI'a Semgrep ekleyin" rehberlerinin çoğu p/python artı p/secrets ile başlıyor. Bu kurulum 12 saniyede 10 açığın 6'sını buldu ve her birinde tam satırı gösterdi. Benim koymadığım iki XSS açığını da buldu: /ping komut çıktısını HTML olarak dönüyor, /profile cookie'den gelen kullanıcı adını bir f-string'e koyuyor. İkisi de gerçek. Ama uygulamanın en tehlikeli açığını kaçırdı: cookie üzerinden uzaktan kod çalıştırmaya izin veren pickle deserialization'ı.

B. SAST: p/security-audit eklenmiş Semgrep ve Bandit

p/security-audit eklenmiş semgrep: 6 saniyede 25 bulgu. Yeni: 12. satırda avoid_hardcoded_config_SECRET_KEY, 70. ve 80. satırlarda insecure-deserialization ve avoid-pickle (V4), 71. satırda secure-set-cookie (V8), 64. satırda render-template-string. Bulunan: 10'da 8

Bir kural paketi daha, iki açık daha: pickle (V4) ve cookie flag'leri (V8). V8'i "sadece runtime" sütununa koymuştum, ama httponly=True olmadan yazılmış set_cookie("session_data", ...) kodda görünüyor ve audit paketinde bunun için bir kural var. Sonucu araçtan çok, seçilen kural paketi değiştirdi.

bandit -r .: 10 saniyede 11 bulgu. HIGH B324 md5 (V6), B602 shell=True (V2), B201 flask debug (V5); MEDIUM B608 SQL ifadesi (V1), B301 pickle (V4), B104 tüm arayüzlere bind; 12. ve 14. satır için LOW B105 koda gömülü şifre (V3). Gürültü: pickle ve subprocess import'u için B403 ve B404, /tmp için B108. Bulunan: 10'da 6. Kaçan: V9 XSS, V8, V7, V10

Bandit de 6 açık buldu, ama farklı 6 açık: ek paket olmadan pickle'ı yakaladı, render_template_string içindeki XSS'i kaçırdı. Koda gömülü AWS secret'ını ise LOW olarak işaretledi; sadece HIGH'ta duran bir gate bunu geçirirdi.

C. DAST: ZAP baseline (pasif)

http://vulnapp:8080'e zap-baseline.py: 60 saniyede 12 alarm. Cookie No HttpOnly Flag, Missing Anti-clickjacking Header, X-Content-Type-Options yok, Server sürüm sızdırıyor, CSP yok, SameSite'sız cookie, Permissions Policy yok, Cross-Origin header'ları yok. High 0, Medium 2, Low 8, Info 2. Bulunan: V7 ve V8, 10'da 2; hiçbir isteğe saldırılmadı

Baseline tarama sayfaları gezip cevaplara bakar, hiç saldırı göndermez. 60 saniyede tam olarak SAST'ın iyi göremediği iki açığı buldu: eksik header'lar (V7) ve cookie flag'leri (V8). Uygulamada SQL injection ve command injection varken tek bir High alarm bile çıkmadı. Baseline'ın yeşil olması uygulamanın güvenli olduğunu değil, sadece cevapların normal göründüğünü gösteriyor.

D. DAST: ZAP full scan (aktif)

zap-full-scan.py: 731 saniyede 20 alarm. Baseline'a göre yeni: Cross Site Scripting (Reflected), SQL Injection, Remote OS Command Injection, Remote OS Command Injection (Time Based), Server Side Template Injection, Source Code Disclosure - File Inclusion, Cookie Slack Detector. High 6, Medium 2, Low 9, Info 3. Bulunan: V1 V2 V7 V8 V9, 10'da 5

ZAP'ın gönderdikleri: /search?q'ya </p><scrIpt>alert(1);</scRipt><p> ile reflected XSS, /ping?host'a 127.0.0.1&cat /etc/passwd& ile command injection, /ping'e sleep ile zamana dayalı command injection, /user?name'e tek tırnak ile SQL injection, /search'e {{7014*3535}} ile template injection, ve /ping?host=127.0.0.1'de false positive olan bir Source Code Disclosure alarmı

Aktif tarama 12 kat uzun sürdü ama injection'ları gerçekten kanıtladı. 127.0.0.1&cat /etc/passwd& gönderdi, cevapta root:x:0:0 satırı döndü. {{7014*3535}} gönderdi, cevapta 24794490 döndü; bu da açığın sadece XSS değil, template injection olduğunu kanıtlıyor. Review'da bu bulgular tartışma konusu olmaz, çünkü hem payload hem cevap raporda duruyor. Yine de pickle açığını (ZAP pickle payload'u üretmiyor), koda gömülü secret'ları ve md5'i (HTTP üzerinden görünmüyorlar), debug konsolunu (Werkzeug 3, Host header'ı localhost ya da 127.0.0.1 değilse /console'a 400 dönüyor, olsa bile PIN istiyor) ve backup dosyasını kaçırdı.

E. Matris

10 bilinen açık ve 5 taramanın matrisi. python ve secrets ile Semgrep: 12 saniyede 6. security-audit ile Semgrep: 6 saniyede 8. Bandit: 10 saniyede 6. ZAP baseline: 60 saniyede 2. ZAP full scan: 731 saniyede 5. SAST birlikte 8, DAST birlikte 5, hepsi birlikte 9, V10 unutulmuş backup dosyasını kimse bulmadı

  • Sadece SAST buldu: V3 secret'lar, V4 pickle, V5 debug modu, V6 md5.
  • Sadece DAST buldu: V7 güvenlik header'ları.
  • İkisi de buldu: V1 SQL injection, V2 command injection, V8 cookie flag'leri, V9 XSS.
  • Kimse bulmadı: V10, içinde veritabanı şifresi olan backup dosyası.

F. Pipeline gate: açıklı uygulama, düzeltilmiş uygulama ve bir false positive

Sonra on açığın hepsini uygulamanın ikinci bir kopyasında kapattım: parametreli sorgu, liste ile subprocess.run ve host adı allowlist'i, secret'lar environment'tan, pickle yerine imzalı JSON cookie, debug sunucusu yerine waitress, md5 yerine scrypt, güvenlik header'larını koyan bir after_request hook'u, cookie'de httponly/secure/samesite, autoescape'li template'ler; static/ klasörünü de tamamen kaldırdım. make pipeline yukarıdaki YAML'larla aynı gate'leri çalıştırıyor: --severity ERROR --error ile Semgrep, sonra build, sonra ZAP full scan, sonra High alarm kontrolü.

Açıklı uygulamada make pipeline: SAST 8. saniyede 8 engelleyici bulguyla kaldı (koda gömülü SECRET_KEY, AWS key id ve secret, tainted SQL string, subprocess injection, tehlikeli subprocess kullanımı, shell=True, güvensiz deserialization), build ve deploy atlandı. Düzeltilmiş uygulamada ilk koşu: SAST geçti, ZAP full scan 541. saniyede High=1 Medium=1 Low=2 buldu, FAILED. 43 numaralı plugin ci/zap-ignore.tsv'ye eklendikten sonra: SAST 10. saniyede geçti, ZAP 505. saniyede High=0 Medium=1 Low=2, 43 incelenmiş false positive olarak atlandı, pipeline geçti

  • Açıklı uygulama: SAST, pipeline'ı 8. saniyede 8 engelleyici bulguyla durdurdu. Image build edilmedi, ZAP hiç koşmadı. Bir açığı durdurmanın en ucuz yeri burası.
  • Düzeltilmiş uygulama, ilk koşu: SAST geçti, ZAP 541 saniye koştu ve gate 1 High alarmla kaldı. Alarm bir false positive'di (aşağıda Nerede patladı #3).
  • Düzeltilmiş uygulama, incelenmiş tek bir ignore satırından sonra: 505 saniyede geçti. Kalanlar: Medium HTTP Only Site (lab'da TLS yok; staging'de bu gerçek bir bulgu), Low Cookie Slack Detector ve Low Cross-Origin-Embedder-Policy Header Missing; bu da benim eklemediğim bir header. Hiçbiri pipeline'ı durdurmuyor, hepsi raporda duruyor.

Nerede patladı: 6 gerçek sürpriz

1. p/secrets ilk AWS anahtarımı görmezden geldi: sonu EXAMPLEKEY ile bitiyordu

İlk sürümde AWS dokümanındaki örnek anahtarı, wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY'i kullandım. Ne p/secrets ne de p/gitleaks bunu raporladı; secret tarayıcıları dokümandaki örneği bilerek allowlist'e alıyor. Aynı formatta rastgele bir sahte anahtar koydum, iki paket de hemen yakaladı. Bir secret tarayıcıyı test ediyorsanız dokümandaki anahtarla test etmeyin.

2. p/python tek başına cookie'deki pickle.loads çağrısını raporlamadı

Uygulamanın en tehlikeli açığı olan, session cookie'si üzerinden uzaktan kod çalıştırma, en yaygın Semgrep kurulumunda hiç görünmedi. İlgili kurallar (avoid-pickle, insecure-deserialization) p/security-audit paketinde. Bandit ise bunu varsayılan kontrolleriyle buldu. Sadece aracın çalıştığına değil, kural paketinin içinde ne olduğuna bakın.

3. Bir false positive, düzeltilmiş uygulamanın release'ini durdurdu

ZAP, /ping?host=127.0.0.1 ve /search?q=shoes için High seviyesinde Source Code Disclosure - File Inclusion alarmı verdi. Parametre değeri olarak ping ve search kelimelerini gönderdi ve cevabı (ping çıktısını, arama metnini) sızmış kaynak kod sandı. Uygulamada file inclusion yok. Açıklı uygulamada bu alarm beş gerçek High alarmın arasında gözden kaçtı. Düzeltilmiş uygulamada ise tek High alarm buydu ve ortada tek bir gerçek açık yokken pipeline'ı kırdı: High=1, FAILED.

İlk akla gelen çözümler yanlış: gate'i "2 High alarmda dur" diye gevşetmek ya da DAST gate'ini tamamen kaldırmak. Plugin id'sini gerekçesi ve tarihiyle bir ignore dosyasına ekledim, gate sadece o id'yi atlıyor. O dosyadaki her satır, birinin verdiği ve sonradan hesabı sorulabilecek bir karar.

4. Backup dosyası açıkta dururken ZAP PASS: Backup File Disclosure [10095] dedi

ZAP'ın backup dosyaları için bir kuralı var ve bu kural çalıştı da. Zaten bildiği URL'lerin yanında index.bak, index.old gibi isimleri deniyor. /static/config.php.bak'e hiçbir yerden link yoktu, ZAP o klasörde böyle bir dosya olduğunu hiç öğrenmedi, kural da geçti. SAST da görmedi, çünkü bu bir kod dosyası değil. Bunu yakalamanın yolu, image'a neyin girdiğini review etmek (.dockerignore, sadece uygulamanın ihtiyacı olanı COPY etmek) ya da wordlist kullanan bir forced browsing aracı.

5. Düzeltmeden sonra audit kuralları hâlâ 9 bulgu raporladı

Düzeltilmiş uygulamada varsayılan Semgrep seti hiç bulgu vermedi. p/security-audit eklenince 5 WARNING çıktı; hepsi render-template-string kuralından, değerleri autoescape'li {{ }} ile basılan sabit template'ler için. Bandit 4 bulgu verdi: B404 (subprocess import'u), B603 ve B607 (ping'i shell olmadan, adıyla çalıştırmak) ve B104 (0.0.0.0'a bind; bir container zaten bunu yapmak zorunda).

Bunların hiçbiri açık değil. SAST gate'i WARNING'de ya da Bandit LOW'da dursaydı düzeltilmiş uygulama hiç yayına çıkamazdı, ekip de her yere # nosec yazmayı öğrenirdi. Gate'in sadece ERROR olmasının sebebi bu: açıklı uygulamayı durduran 8 bulgunun hepsi ERROR'du, düzeltilmiş uygulamada kalan 9'un hepsi WARNING, LOW ya da MEDIUM. Audit paketini yine de çalıştırmaya değer; gate olarak değil, reviewer'ın okuduğu bir rapor olarak.

6. Server Leaks Version Information debug modunun kanıtı değil

ZAP her cevaptaki Server: Werkzeug/3.1.9 Python/3.13.18 header'ını işaretledi. Önce bunu V5 (debug modu) saydım, sonra kontrol ettim: Werkzeug bu header'ı debug=False ile de gönderiyor. ZAP debugger'ın kendisini hiç bulmadı: /console, Host: vulnapp:8080'e 400 döndü, Host: 127.0.0.1 ile "console is locked" gösterip PIN istedi. Werkzeug 3 debug konsoluna kazara ulaşmayı zorlaştırmış; prod'da debug=True yine de bir açık ve onu sadece SAST raporladı. V5'i DAST'a saymadım; saysaydım matris DAST için daha iyi görünürdü.

Hangi durumda hangisi

Durum Kullan Neden
Her pull request SAST, ERROR'da gate saniyeler, tam satır, yazar merge'den önce düzeltir
Koddaki secret'lar her push'ta SAST secret kuralları (ya da gitleaks) DAST bunları hiç görmez
Staging'e her deploy'dan sonra DAST baseline 60 sn, header ve cookie'leri yakalar, sık koşmak güvenli
Gece ya da release öncesi DAST full scan, High'ta gate bu lab'da 12 dakika sürdü, injection'ı gerçekten kanıtlıyor; saldırı gönderdiği için izin almadan asla prod'da çalıştırılmamalı
Bilinen CVE'li bağımlılıklar SCA (Trivy, Dependabot) ne SAST ne DAST kütüphane sürümlerine bakar
Image'da kimsenin referans vermediği dosyalar image review, .dockerignore, forced browsing beş taramanın hepsi V10'u kaçırdı

Özet

  • SAST kodu okur ve satır verir; DAST çalışan uygulamaya saldırır ve URL verir. 10 açık koyduğum aynı uygulamada SAST 8'ini, DAST 5'ini buldu; ikisi birlikte 9'unu.
  • Tek başına SAST güvenlik header'larını kaçırdı; tek başına DAST secret'ları, pickle'ı, md5'i ve debug modunu kaçırdı. Injection'ları ve cookie flag'lerini ikisi de buldu.
  • Semgrep'te kural paketi seçimi, araç seçiminden daha çok fark yarattı: p/python + p/secrets 6 açık buldu, p/security-audit eklenince 8'e çıktı; pickle üzerinden uzaktan kod çalıştırma da bunlara dahil.
  • Uygulamada SQL ve command injection varken ZAP baseline sadece 2 açık buldu ve hiç High alarm vermedi; injection'ları ancak aktif tarama buldu (60 sn yerine 731 sn).
  • Gate'i SAST'ta ERROR'a, DAST'ta High'a koydum; gerisi rapor. Açıklı uygulama SAST'ta 8 saniyede durdu; düzeltilmiş uygulama ancak incelenmiş bir false positive ignore dosyasına girince geçti. Backup dosyasını kimse bulmadı; o bir image review işi.

Kaynaklar: OWASP Static Code Analysis (Controls for Static Code Analysis); OWASP ZAP dokümanları ZAP Baseline Scan, ZAP Full Scan, Alert details; Semgrep dokümanları Rule packs, CLI reference (--severity, --error, --gitlab-sast); Bandit dokümanları Test plugins; GitHub Docs Workflow syntax for GitHub Actions; GitLab Docs Static application security testing, Dynamic application security testing, Artifacts reports.

Serinin devamı

Bu yazı Temel Konular, Ölçerek serisinin üçüncüsü. Bir önceki yazıda, Git Branching Stratejisi Nedir?, bir değişikliğin prod'a nasıl ulaştığını ölçmüştüm; bu yazıda pipeline'ın yolda neyi kontrol ettiğini ölçtüm. Sıradaki yazının konusu rootless container'lar ve image sıkılaştırma: bu sefer CVE'leri ve root çalışan bir container'ın host'a neler yazabildiğini sayacağım.

Bu lab'ın kodu (açıklı ve düzeltilmiş Flask uygulaması, SAST, DAST ve pipeline script'leri, her bulguyu bir açığa eşleyen puanlama script'i ve Makefile) Buy Me a Coffee'deki Lab Access üyeleriyle paylaşılıyor; üye olduktan sonra repo daveti e-postayla geliyor: https://buymeacoffee.com/miraccanyilmaz/membership

İnsanın en büyük hatalarından biri de doğru zamanı yanlış kişilerle doldurmaktır.
Charles Bukowski