Güvenlik Taraması: GitLab CI/CD ile SAST ve DAST Kullanımı

Bir sabah uyandığınızda production ortamınızda kritik bir güvenlik açığı bulunduğunu öğrenmek, hiç bir sysadmin’in yaşamak istemeyeceği bir senaryodur. Oysa bu açıkların büyük çoğunluğu, kod geliştirme sürecinin erken aşamalarında tespit edilebilir. GitLab CI/CD pipeline’larına entegre edilecek SAST ve DAST araçları, güvenlik testlerini “birinin yapacağı bir iş” olmaktan çıkarıp otomatik ve sürekli bir süreç haline getiriyor. Bu yazıda, gerçek dünya senaryoları üzerinden GitLab’ın güvenlik tarama özelliklerini nasıl etkin kullanabileceğinizi ele alacağız.

SAST ve DAST Nedir, Neden İkisine de İhtiyaç Var?

SAST (Static Application Security Testing), uygulamanızın kaynak kodunu çalıştırmadan analiz eden bir güvenlik tarama yöntemidir. SQL injection, XSS açıkları, hardcoded credentials gibi sorunları kod üzerinde tespit eder. Geliştirici bir değişiklik yaptığı anda, henüz kod merge bile edilmeden bu tarama çalışabilir.

DAST (Dynamic Application Security Testing) ise tam tersine, çalışan uygulamaya karşı gerçek saldırı simülasyonları yapar. OWASP ZAP veya Nikto gibi araçlar, deploy edilmiş uygulamayı dışarıdan tararak runtime’da ortaya çıkan açıkları bulur. Authentication bypass, session management sorunları ve injection açıkları DAST’ın güçlü olduğu alanlardır.

İkisi birbirinin yerine geçemez. SAST kodu erken dönemde yakalar, DAST ise uygulamanın gerçek çalışma ortamındaki davranışını test eder. Bir Python Flask uygulamasında eval() kullanımı SAST tarafından tespit edilirken, yanlış konfigüre edilmiş bir HTTP header’ı ancak DAST ile ortaya çıkabilir.

GitLab’ın Dahili Güvenlik Özellikleri

GitLab, Ultimate ve bazı durumlarda Free tier üzerinde dahili güvenlik tarama şablonları sunar. Ancak bu şablonların tamamı için lisans gereksinimleri farklılık gösterir. Biz bu yazıda hem GitLab’ın native araçlarını hem de açık kaynak alternatifleri olan Semgrep, Bandit ve OWASP ZAP’ı kullanarak tam kapsamlı bir pipeline kuracağız.

GitLab’ın CI/CD şablonlarını projenize dahil etmek için .gitlab-ci.yml dosyanıza şu satırları ekleyebilirsiniz:

# .gitlab-ci.yml - Temel yapı
include:
  - template: Security/SAST.gitlab-ci.yml
  - template: Security/DAST.gitlab-ci.yml
  - template: Security/Secret-Detection.gitlab-ci.yml
  - template: Security/Dependency-Scanning.gitlab-ci.yml

variables:
  SAST_EXCLUDED_PATHS: "spec, test, tests, tmp"
  SECURE_LOG_LEVEL: "info"

Bu tek adımla GitLab’ın kendi analiz motorları devreye girer. Ancak daha fazla kontrol ve özelleştirme istiyorsanız, araçları kendiniz yapılandırmanız gerekir.

Pipeline Mimarisi: Aşamalar Nasıl Tasarlanmalı?

Güvenlik testlerini pipeline’ınıza entegre ederken dikkat etmeniz gereken en önemli şey hız ve kapsamlılık dengesidir. Her commit’te tam bir DAST taraması yapmak pratik değildir. Aşağıdaki mimari, gerçek dünya projelerinde iyi çalışan bir yaklaşımdır:

  • build: Uygulama derleme ve imaj oluşturma
  • test: Unit testler ve kod kalite kontrolleri
  • security-scan: SAST, secret detection, dependency scanning
  • deploy-staging: Staging ortamına deploy
  • dast: Çalışan uygulamaya karşı dinamik tarama
  • security-report: Bulguların raporlanması ve threshold kontrolü
# .gitlab-ci.yml - Tam pipeline tanımı
stages:
  - build
  - test
  - security-scan
  - deploy-staging
  - dast
  - security-report

variables:
  DOCKER_DRIVER: overlay2
  DOCKER_TLS_CERTDIR: "/certs"
  APP_IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
  STAGING_URL: "https://staging.example.com"

default:
  image: docker:24.0
  services:
    - docker:24.0-dind
  before_script:
    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY

SAST Yapılandırması: Bandit ve Semgrep ile Derinlemesine Analiz

GitLab’ın dahili SAST analiz motorları dil tespiti yaparak otomatik araç seçimi yapar. Ancak Python projelerinde Bandit’i, çoklu dil projelerinde Semgrep’i manuel olarak yapılandırmak çok daha esnek bir yapı sağlar.

# Bandit ile Python SAST taraması
sast-bandit:
  stage: security-scan
  image: python:3.11-slim
  before_script:
    - pip install bandit[toml] --quiet
  script:
    - |
      bandit -r ./src 
        -f json 
        -o bandit-report.json 
        -l 
        --severity-level medium 
        -x ./src/tests,./src/migrations || BANDIT_EXIT=$?
      
      # Kritik bulgular varsa pipeline'ı durdur
      HIGH_COUNT=$(python3 -c "
      import json, sys
      with open('bandit-report.json') as f:
          data = json.load(f)
      high = sum(1 for r in data['results'] if r['issue_severity'] == 'HIGH')
      print(high)
      ")
      
      echo "Yüksek öncelikli bulgu sayısı: $HIGH_COUNT"
      if [ "$HIGH_COUNT" -gt "0" ]; then
        echo "HATA: Kritik güvenlik açıkları tespit edildi!"
        exit 1
      fi
  artifacts:
    reports:
      sast: bandit-report.json
    paths:
      - bandit-report.json
    when: always
    expire_in: 1 week
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

Semgrep, çok daha geniş bir kural seti sunuyor ve özellikle polyglot projelerde çok işe yarıyor:

# Semgrep ile çoklu dil SAST taraması
sast-semgrep:
  stage: security-scan
  image: returntocorp/semgrep:latest
  script:
    - |
      semgrep 
        --config=p/owasp-top-ten 
        --config=p/cwe-top-25 
        --config=p/secrets 
        --json 
        --output=semgrep-report.json 
        --severity=WARNING 
        --exclude="node_modules" 
        --exclude="*.test.js" 
        --exclude="vendor/" 
        ./
      
      # Bulgu özetini terminale yaz
      python3 - <<'EOF'
      import json
      with open('semgrep-report.json') as f:
          data = json.load(f)
      results = data.get('results', [])
      errors = [r for r in results if r.get('extra', {}).get('severity') == 'ERROR']
      warnings = [r for r in results if r.get('extra', {}).get('severity') == 'WARNING']
      print(f"Toplam bulgu: {len(results)}")
      print(f"Kritik (ERROR): {len(errors)}")
      print(f"Uyarı (WARNING): {len(warnings)}")
      if len(errors) > 0:
          print("nKritik bulgular:")
          for e in errors[:5]:
              print(f"  - {e['path']}:{e['start']['line']} - {e['check_id']}")
      EOF
  artifacts:
    reports:
      sast: semgrep-report.json
    paths:
      - semgrep-report.json
    when: always
  allow_failure: false

Secret Detection: Hardcoded Credentials’ı Yakalamak

Üretim ortamlarında yaşanan en yaygın ve utanç verici güvenlik ihlallerinden biri, kaynak koda gömülü API anahtarları ve şifrelerin repository’de kalmasıdır. GitLab’ın secret detection şablonu bunu ele alır, ama özelleştirme eklemek işleri daha da sağlamlaştırır:

# Secret Detection - Özelleştirilmiş kural seti ile
secret-detection:
  stage: security-scan
  image: python:3.11-slim
  before_script:
    - pip install detect-secrets --quiet
  script:
    - |
      # Mevcut baseline ile karşılaştır
      if [ -f .secrets.baseline ]; then
        detect-secrets scan 
          --baseline .secrets.baseline 
          --exclude-files '.lock$' 
          --exclude-files 'package-lock.json' 
          --exclude-files '.*.test.*' 
          > /tmp/current-secrets.json
        
        NEW_SECRETS=$(python3 -c "
      import json
      with open('/tmp/current-secrets.json') as f:
          data = json.load(f)
      count = sum(len(v) for v in data.get('results', {}).values())
      print(count)
      ")
        
        echo "Tespit edilen potansiyel secret sayısı: $NEW_SECRETS"
        
        if [ "$NEW_SECRETS" -gt "0" ]; then
          echo "UYARI: Kodda potansiyel secret tespit edildi!"
          cat /tmp/current-secrets.json | python3 -m json.tool
          exit 1
        fi
      else
        echo "Baseline dosyası bulunamadı, ilk tarama yapılıyor..."
        detect-secrets scan . > .secrets.baseline
        echo "Baseline oluşturuldu. Lütfen .secrets.baseline dosyasını commit edin."
      fi
  artifacts:
    paths:
      - .secrets.baseline
    when: always

Dependency Scanning: Bağımlılık Açıklarını Yakalamak

Trivy, hem container imajlarını hem de uygulama bağımlılıklarını tarayabilen güçlü bir araçtır:

# Trivy ile dependency ve container taraması
dependency-scan:
  stage: security-scan
  image:
    name: aquasec/trivy:latest
    entrypoint: [""]
  script:
    - |
      # Uygulama bağımlılıklarını tara
      trivy fs 
        --exit-code 1 
        --severity HIGH,CRITICAL 
        --format json 
        --output trivy-deps-report.json 
        --ignore-unfixed 
        ./
      
      # Container imajını tara (build aşamasından gelir)
      trivy image 
        --exit-code 1 
        --severity CRITICAL 
        --format json 
        --output trivy-image-report.json 
        --ignore-unfixed 
        $APP_IMAGE
      
      echo "Bağımlılık taraması tamamlandı"
      
      # Özet rapor
      VULN_COUNT=$(cat trivy-deps-report.json | python3 -c "
      import json, sys
      data = json.load(sys.stdin)
      count = 0
      for result in data.get('Results', []):
          count += len(result.get('Vulnerabilities', []) or [])
      print(count)
      ")
      echo "Toplam zafiyet: $VULN_COUNT"
  artifacts:
    paths:
      - trivy-deps-report.json
      - trivy-image-report.json
    when: always
    expire_in: 1 week
  allow_failure: false

DAST Yapılandırması: OWASP ZAP ile Dinamik Tarama

DAST, staging ortamınıza deploy ettiğiniz uygulamayı gerçek saldırı vektörleriyle test eder. OWASP ZAP, bu iş için endüstri standardı haline gelmiş açık kaynaklı bir araçtır:

# OWASP ZAP ile DAST taraması
dast-zap-scan:
  stage: dast
  image: ghcr.io/zaproxy/zaproxy:stable
  variables:
    ZAP_TARGET: $STAGING_URL
    ZAP_RULES_FILE: ".zap/rules.tsv"
  script:
    - mkdir -p /zap/wrk
    - |
      # Temel Spider ve aktif tarama
      zap-baseline.py 
        -t $ZAP_TARGET 
        -J zap-baseline-report.json 
        -r zap-baseline-report.html 
        -l WARN 
        -z "-config scanner.strength=HIGH" 
        --auto || ZAP_EXIT=$?
      
      cp /zap/wrk/zap-baseline-report.json ./
      cp /zap/wrk/zap-baseline-report.html ./
      
      # Yüksek risk bulguları kontrol et
      HIGH_RISKS=$(python3 -c "
      import json
      with open('zap-baseline-report.json') as f:
          data = json.load(f)
      high = sum(1 for site in data.get('site', [])
                 for alert in site.get('alerts', [])
                 if alert.get('riskcode', '0') in ['3', '4'])
      print(high)
      ")
      
      echo "Yüksek risk bulgu sayısı: $HIGH_RISKS"
      if [ "$HIGH_RISKS" -gt "3" ]; then
        echo "HATA: Çok fazla yüksek riskli bulgu tespit edildi!"
        exit 1
      fi
  artifacts:
    paths:
      - zap-baseline-report.json
      - zap-baseline-report.html
    when: always
    expire_in: 2 weeks
  needs:
    - job: deploy-staging
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
    - if: $CI_COMMIT_BRANCH =~ /^release/.*/

Authenticated DAST: Giriş Gerektiren Sayfaların Taranması

Birçok uygulama authentication gerektiriyor. ZAP’ı kimlik doğrulamalı şekilde çalıştırmak için bir script dosyası oluşturmanız gerekir:

# ZAP Authentication Script
dast-authenticated:
  stage: dast
  image: ghcr.io/zaproxy/zaproxy:stable
  variables:
    ZAP_USERNAME: $STAGING_TEST_USER
    ZAP_PASSWORD: $STAGING_TEST_PASS
    ZAP_LOGIN_URL: "$STAGING_URL/api/auth/login"
  script:
    - mkdir -p /zap/wrk
    - |
      cat > /zap/wrk/auth-script.js << 'AUTHSCRIPT'
      var HttpRequestHeader = Java.type("org.parosproxy.paros.network.HttpRequestHeader");
      var URI = Java.type("org.apache.commons.httpclient.URI");
      
      function authenticate(helper, paramsValues, credentials) {
          var loginUrl = paramsValues.get("Login URL");
          var requestUri = new URI(loginUrl, false);
          var requestMethod = HttpRequestHeader.POST;
          
          var msg = helper.prepareMessage();
          var requestBody = '{"username":"' + credentials.getParam("Username") + 
                           '","password":"' + credentials.getParam("Password") + '"}';
          
          msg.setRequestHeader(new HttpRequestHeader(requestMethod, requestUri, 
                               HttpRequestHeader.HTTP11));
          msg.getRequestHeader().setHeader("Content-Type", "application/json");
          msg.setRequestBody(requestBody);
          helper.sendAndReceive(msg);
          return msg;
      }
      AUTHSCRIPT
      
      # Full scan authenticated
      zap-full-scan.py 
        -t $STAGING_URL 
        -J zap-auth-report.json 
        -l WARN 
        -z "-config scanner.strength=MEDIUM 
            -config connection.timeoutInSecs=30" 
        || ZAP_EXIT=$?
      
      cp /zap/wrk/zap-auth-report.json ./ 2>/dev/null || true
      echo "Kimlik doğrulamalı tarama tamamlandı. Exit code: $ZAP_EXIT"
  artifacts:
    paths:
      - zap-auth-report.json
    when: always
  allow_failure: true

Güvenlik Raporu Toplama ve Merge Request Entegrasyonu

Tüm bu taramaları ayrı ayrı yapmak yeterli değil, bulguları anlamlı bir şekilde sunmak gerekiyor. GitLab’ın MR widget’larını aktive etmek için artifact formatlarının doğru olması kritik:

# Güvenlik raporlarını birleştir ve özet oluştur
security-summary:
  stage: security-report
  image: python:3.11-slim
  before_script:
    - pip install jinja2 --quiet
  script:
    - |
      python3 << 'EOF'
      import json, os
      from datetime import datetime
      
      summary = {
          "generated_at": datetime.now().isoformat(),
          "pipeline_id": os.environ.get("CI_PIPELINE_ID"),
          "commit_sha": os.environ.get("CI_COMMIT_SHA"),
          "findings": {
              "sast": {"total": 0, "critical": 0, "high": 0},
              "dast": {"total": 0, "critical": 0, "high": 0},
              "secrets": {"total": 0},
              "dependencies": {"total": 0, "critical": 0}
          }
      }
      
      # Bandit bulgularını oku
      if os.path.exists("bandit-report.json"):
          with open("bandit-report.json") as f:
              data = json.load(f)
          results = data.get("results", [])
          summary["findings"]["sast"]["total"] = len(results)
          summary["findings"]["sast"]["critical"] = sum(
              1 for r in results if r.get("issue_severity") == "HIGH"
          )
      
      # ZAP bulgularını oku
      if os.path.exists("zap-baseline-report.json"):
          with open("zap-baseline-report.json") as f:
              data = json.load(f)
          for site in data.get("site", []):
              for alert in site.get("alerts", []):
                  summary["findings"]["dast"]["total"] += 1
                  if alert.get("riskcode") in ["3", "4"]:
                      summary["findings"]["dast"]["critical"] += 1
      
      print(json.dumps(summary, indent=2))
      
      with open("security-summary.json", "w") as f:
          json.dump(summary, f, indent=2)
      
      # Eşik kontrolü
      total_critical = (
          summary["findings"]["sast"]["critical"] +
          summary["findings"]["dast"]["critical"]
      )
      
      print(f"nToplam kritik bulgu: {total_critical}")
      if total_critical > 5:
          print("HATA: Kritik bulgu eşiği aşıldı!")
          exit(1)
      
      print("Güvenlik taraması başarıyla tamamlandı.")
      EOF
  artifacts:
    paths:
      - security-summary.json
    when: always
    expire_in: 1 month
  needs:
    - job: sast-bandit
      artifacts: true
    - job: dast-zap-scan
      artifacts: true
      optional: true

Gerçek Dünya Senaryosu: E-Ticaret Uygulaması

Diyelim ki bir e-ticaret uygulaması üzerinde çalışıyorsunuz. Payment endpoint’leri içeriyor, kullanıcı verilerini işliyor ve üçüncü parti API’lere bağlanıyor. Bu senaryo için pipeline’ınızı nasıl şekillendirmelisiniz?

  • merge_request üzerinde sadece SAST ve secret detection çalıştırın. Hız kritik, 3-5 dakikada sonuç alın.
  • main branch commit’lerinde SAST + dependency scan + container scan çalıştırın.
  • release branch oluşturulduğunda staging deploy + tam DAST taraması başlatın.
  • scheduled pipeline ile haftada bir gece tüm kapsamlı taramaları çalıştırın.

Ödeme sayfası gibi kritik endpoint’ler için ZAP’ın context dosyasını yapılandırarak sadece belirli URL’leri hedef alabilirsiniz. Bu hem tarama süresini kısaltır hem de false positive oranını düşürür.

False Positive Yönetimi

Güvenlik taramalarında en büyük zorluklardan biri false positive’lerdir. Sürekli yanlış alarmlar ekibin araçlara olan güvenini sarsar. Bunları yönetmek için:

  • Bandit için .bandit dosyası oluşturun ve skips = B101,B601 gibi kuralları projenize özel devre dışı bırakın.
  • Semgrep için # nosemgrep: kural-adi inline yorumu kullanın, ancak bunları code review’da sorgulayın.
  • ZAP için .zap/rules.tsv dosyasında bilinen false positive’leri IGNORE olarak işaretleyin.
  • Tüm bu istisnaları bir güvenlik kaydında belgelendirin ve düzenli gözden geçirin.

Her istisnayı körü körüne kabul etmek yerine, neden false positive olduğunu takım içinde tartışın ve belgelendirin. Altı ay sonra o kodu değiştirdiğinizde neden o istisnayı eklediğinizi hatırlamanız gerekecek.

GitLab Security Dashboard ve Raporlama

Ultimate lisansı varsa GitLab’ın Security Dashboard’u tüm bulguları merkezi bir yerde toplar. Ancak açık kaynak araçlarla da benzer bir görünürlük elde edebilirsiniz:

GitLab’ın GL-SAST format şemasına uygun JSON üretmek için araç çıktılarını dönüştürücüler yazabilirsiniz. Bu sayede Merge Request üzerindeki Security widget çalışır ve güvenlik bulguları doğrudan MR içinde görünür hale gelir. Ekibinizin her değişikliği review ederken güvenlik bulgularını da görmesi, güvenlik kültürünün oluşmasında kritik bir rol oynar.

Pipeline Performansını Optimize Etmek

Güvenlik taramaları pipeline süresini uzatabilir. Bunu minimize etmek için:

  • SAST araçlarını paralel olarak çalıştırın, needs direktifiyle bağımlılıkları net tanımlayın.
  • Büyük projelerde SAST_EXCLUDED_PATHS ile test dizinlerini tarama dışında bırakın.
  • Semgrep için --max-memory parametresiyle bellek kullanımını sınırlayın.
  • Docker layer caching kullanarak araç imajlarının tekrar indirilmesini engelleyin.
  • Değişmeyen dosyalar için rules: changes: direktifiyle gereksiz tarama çalıştırmaktan kaçının.

Tipik olarak SAST taraması 2-4 dakika, DAST taraması ise 15-30 dakika alır. DAST’ı sadece belirli branch’lerde çalıştırmak bu maliyeti makul düzeyde tutar.

Sonuç

GitLab CI/CD pipeline’ınıza SAST ve DAST entegre etmek, güvenliği “sonradan düşünülen bir şey” olmaktan çıkarıp geliştirme sürecinin ayrılmaz bir parçası haline getirir. Bandit ve Semgrep ile statik analiz, OWASP ZAP ile dinamik tarama ve Trivy ile bağımlılık taramasını birleştirdiğinizde oldukça kapsamlı bir güvenlik ağı oluşturursunuz.

Bu yazıda anlattığımız yaklaşımın en önemli yan etkisi, güvenlik konusunda ekip içinde farkındalık yaratmasıdır. Bir geliştirici MR açtığında ve pipeline “Bu kodda SQL injection riski var” dediğinde, o geliştirici bir sonraki seferinde daha dikkatli kod yazar. Araçlar kural koyar, ama asıl değişimi kültür yaratır.

Başlangıç için her şeyi birden uygulamaya çalışmayın. Önce secret detection ve temel SAST ile başlayın, ekibi alıştırın, false positive yönetimini oturtun. Ardından DAST ve dependency scanning ekleyin. Küçük adımlarla ilerleyen bir güvenlik programı, bir gecede kurulan ama ertesi gün abandon edilen kompleks bir yapıdan her zaman daha değerlidir.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir