GitHub Actions ile Güvenlik Taraması: CodeQL Kullanım Rehberi

Kod tabanınızda bir güvenlik açığı keşfetmek, özellikle bu açığın production’a çıkmış olması durumunda, bir sysadmin olarak yaşayabileceğiniz en stresli anlardan biridir. GitHub’ın sunduğu CodeQL entegrasyonu, bu tür sürprizleri CI/CD pipeline’ınıza gömdüğünüz statik analiz araçlarıyla minimize etmenizi sağlıyor. Bu yazıda, GitHub Actions üzerinde CodeQL’i sıfırdan kurarak gerçek dünya senaryolarında nasıl kullanacağınızı ele alacağız.

CodeQL Nedir ve Neden Önemlidir

CodeQL, GitHub’ın satın aldığı Semmle şirketinin geliştirdiği bir kod analiz motorudur. Klasik linter araçlarından farklı olarak kodunuzu bir veritabanına dönüştürür ve bu veritabanı üzerinde SQL benzeri sorgular çalıştırarak güvenlik açıklarını tespit eder. Yani sadece “bu satırda şu pattern var mı?” sorusunu sormakla kalmaz, veri akışını, değişken yaşam döngüsünü ve fonksiyon çağrı zincirlerini analiz edebilir.

Desteklenen diller arasında şunlar var:

  • C/C++: Buffer overflow, use-after-free gibi bellek güvenliği sorunları
  • Java/Kotlin: SQL injection, XXE, deserialization açıkları
  • Python: Code injection, path traversal, SSRF
  • JavaScript/TypeScript: XSS, prototype pollution, ReDoS
  • Go: SQL injection, command injection
  • Ruby: Mass assignment, SQL injection
  • Swift: Path traversal, insecure randomness

Public repository’ler için CodeQL tamamen ücretsizdir. Private repository’ler için ise GitHub Advanced Security lisansı gerekiyor, ancak birçok kurumsal müşteri zaten bu lisansı kullanıyor.

Temel Kurulum: İlk CodeQL Workflow’u

En hızlı başlangıç yöntemi GitHub’ın otomatik kurulum sihirbazını kullanmak. Ancak sysadmin olarak kontrol bizde olsun istiyoruz, o yüzden manuel kurulumla gidelim.

mkdir -p .github/workflows
touch .github/workflows/codeql-analysis.yml

Basit bir workflow dosyası şu şekilde görünür:

name: "CodeQL Security Analysis"

on:
  push:
    branches: [ "main", "develop" ]
  pull_request:
    branches: [ "main" ]
  schedule:
    - cron: '0 2 * * 1'  # Her Pazartesi gece 02:00'da

jobs:
  analyze:
    name: Analyze (${{ matrix.language }})
    runs-on: ubuntu-latest
    timeout-minutes: 360
    permissions:
      security-events: write
      packages: read
      actions: read
      contents: read

    strategy:
      fail-fast: false
      matrix:
        include:
          - language: javascript-typescript
            build-mode: none
          - language: python
            build-mode: none

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Initialize CodeQL
        uses: github/codeql-action/init@v3
        with:
          languages: ${{ matrix.language }}
          build-mode: ${{ matrix.build-mode }}

      - name: Perform CodeQL Analysis
        uses: github/codeql-action/analyze@v3
        with:
          category: "/language:${{ matrix.language }}"

Bu basit konfigürasyon bile çoğu proje için oldukça etkilidir. Ancak production ortamlarında bu kadarıyla yetinmek istemeyiz.

Java Projesi için Build-Mode Konfigürasyonu

Java, C++ gibi derlenen diller için CodeQL’in kodu doğru analiz edebilmesi için build sürecini takip etmesi gerekir. autobuild modu çoğu zaman işe yarasa da özelleştirilmiş build süreçlerinde sorun çıkarabilir.

name: "CodeQL - Java Application"

on:
  push:
    branches: [ "main" ]
  pull_request:
    branches: [ "main" ]

jobs:
  analyze:
    name: CodeQL Java Analysis
    runs-on: ubuntu-latest
    permissions:
      security-events: write
      actions: read
      contents: read

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Set up JDK 17
        uses: actions/setup-java@v4
        with:
          java-version: '17'
          distribution: 'temurin'
          cache: maven

      - name: Initialize CodeQL
        uses: github/codeql-action/init@v3
        with:
          languages: java-kotlin
          build-mode: manual

      - name: Build with Maven
        run: |
          mvn clean compile -B 
            -Dmaven.test.skip=true 
            -Dmaven.javadoc.skip=true

      - name: Perform CodeQL Analysis
        uses: github/codeql-action/analyze@v3
        with:
          category: "/language:java-kotlin"
          upload: always

build-mode: manual seçeneği özellikle şu durumlarda kritik:

  • Multi-module Maven/Gradle projeleri: Modüller arası bağımlılıklar nedeniyle autobuild bazen yalnızca bir kısmını analiz eder
  • Özel kod üretimi yapan projeler: Protobuf, JAXB gibi araçlarla üretilen kodların önce generate edilmesi gerekir
  • Kurumsal artifact repository kullananlar: Nexus veya Artifactory’den bağımlılık çeken projelerde autobuild credentials sorunuyla karşılaşabilir

Özel Query Suite Tanımlamak

CodeQL’in gücünden tam anlamıyla yararlanmak için kendi query suite’inizi tanımlayabilirsiniz. Varsayılan security-extended suite’i iyi bir başlangıç noktası olsa da kimi zaman daha fazlasını istersiniz.

mkdir -p .github/codeql
touch .github/codeql/codeql-config.yml
# .github/codeql/codeql-config.yml
name: "Custom Security Configuration"

disable-default-queries: false

queries:
  - uses: security-and-quality
  - uses: security-extended

query-filters:
  - exclude:
      tags contain: experimental
  - include:
      tags contain: security
      severity: error
  - include:
      tags contain: security
      severity: warning

paths-ignore:
  - "vendor/**"
  - "node_modules/**"
  - "**/*.test.js"
  - "**/*.spec.ts"
  - "test/**"
  - "tests/**"
  - "docs/**"

Bu konfigürasyonu workflow dosyanıza şöyle bağlayın:

      - name: Initialize CodeQL
        uses: github/codeql-action/init@v3
        with:
          languages: javascript-typescript
          config-file: .github/codeql/codeql-config.yml

paths-ignore ayarı özellikle önemli. Test dosyalarını ve vendor klasörlerini analiz dışında bırakmak hem false positive sayısını düşürür hem de analiz süresini kısaltır. Büyük monorepo’larda bu optimizasyon, analiz süresini yarıya indirebilir.

Gerçek Dünya Senaryosu: Mikro Servis Mimarisi

Diyelim ki birden fazla dil kullanan bir mikro servis mimariniz var. Backend’de Python/Django, frontend’de React/TypeScript, ve bir tane de Go ile yazılmış API gateway’iniz var. Her birini ayrı ayrı analiz etmek yerine bir matrix stratejisi kuralım.

name: "Microservices Security Scan"

on:
  push:
    branches: [ "main", "release/**" ]
  pull_request:
    types: [ opened, synchronize, reopened ]
    branches: [ "main" ]

env:
  CODEQL_THREADS: 4
  CODEQL_RAM: 6144

jobs:
  changes:
    runs-on: ubuntu-latest
    outputs:
      python: ${{ steps.filter.outputs.python }}
      javascript: ${{ steps.filter.outputs.javascript }}
      go: ${{ steps.filter.outputs.go }}
    steps:
      - uses: actions/checkout@v4
      - uses: dorny/paths-filter@v3
        id: filter
        with:
          filters: |
            python:
              - 'backend/**'
            javascript:
              - 'frontend/**'
            go:
              - 'gateway/**'

  codeql-analyze:
    needs: changes
    strategy:
      fail-fast: false
      matrix:
        include:
          - language: python
            path: backend
            condition: ${{ needs.changes.outputs.python == 'true' }}
          - language: javascript-typescript
            path: frontend
            condition: ${{ needs.changes.outputs.javascript == 'true' }}
          - language: go
            path: gateway
            condition: ${{ needs.changes.outputs.go == 'true' }}

    name: "CodeQL - ${{ matrix.language }}"
    runs-on: ubuntu-latest
    if: ${{ matrix.condition }}
    permissions:
      security-events: write
      actions: read
      contents: read

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Initialize CodeQL
        uses: github/codeql-action/init@v3
        with:
          languages: ${{ matrix.language }}
          source-root: ${{ matrix.path }}
          queries: security-extended

      - name: Autobuild
        uses: github/codeql-action/autobuild@v3

      - name: Perform CodeQL Analysis
        uses: github/codeql-action/analyze@v3
        with:
          category: "/language:${{ matrix.language }}/service:${{ matrix.path }}"
          output: sarif-results
          upload: always

      - name: Upload SARIF to GitHub Security tab
        uses: github/codeql-action/upload-sarif@v3
        if: always()
        with:
          sarif_file: sarif-results/${{ matrix.language }}.sarif
          category: "/language:${{ matrix.language }}/service:${{ matrix.path }}"

Bu yaklaşımın avantajları:

  • Selective scanning: Sadece değişen servisin kodu analiz edilir, gereksiz CI dakikaları harcanmaz
  • Parallel execution: Üç dil aynı anda analiz edilir
  • Granüler raporlama: Her servis için ayrı SARIF kategorisi, Security tabında filtrelemeyi kolaylaştırır

SARIF Sonuçlarını Dışa Aktarmak ve İşlemek

SARIF (Static Analysis Results Interchange Format) dosyaları JSON tabanlı bir standarttır. Bu dosyaları GitHub Security tab’ında görüntülemenin yanı sıra Jira, Slack veya kendi ticketing sisteminize de gönderebilirsiniz.

      - name: Download SARIF artifact
        uses: actions/download-artifact@v4
        with:
          name: sarif-results

      - name: Parse and notify critical findings
        run: |
          python3 << 'EOF'
          import json
          import sys
          import os

          sarif_file = "javascript-typescript.sarif"

          try:
              with open(sarif_file, 'r') as f:
                  sarif_data = json.load(f)
          except FileNotFoundError:
              print("SARIF dosyasi bulunamadi, analiz atlanmis olabilir.")
              sys.exit(0)

          critical_findings = []
          high_findings = []

          for run in sarif_data.get('runs', []):
              tool_name = run.get('tool', {}).get('driver', {}).get('name', 'Unknown')
              results = run.get('results', [])

              # Rules map olustur
              rules = {}
              for rule in run.get('tool', {}).get('driver', {}).get('rules', []):
                  rules[rule['id']] = rule

              for result in results:
                  rule_id = result.get('ruleId', '')
                  rule_info = rules.get(rule_id, {})
                  severity = rule_info.get('defaultConfiguration', {}).get('level', 'warning')
                  
                  location = result.get('locations', [{}])[0]
                  file_path = location.get('physicalLocation', {}).get('artifactLocation', {}).get('uri', 'unknown')
                  line = location.get('physicalLocation', {}).get('region', {}).get('startLine', 0)
                  
                  finding = {
                      'rule': rule_id,
                      'message': result.get('message', {}).get('text', ''),
                      'file': file_path,
                      'line': line,
                      'severity': severity
                  }
                  
                  if severity == 'error':
                      critical_findings.append(finding)
                  elif severity == 'warning':
                      high_findings.append(finding)

          print(f"Kritik bulgular: {len(critical_findings)}")
          print(f"Uyari seviyesi bulgular: {len(high_findings)}")

          if critical_findings:
              print("n=== KRITIK GUVENLIK BULGULARI ===")
              for f in critical_findings:
                  print(f"[{f['rule']}] {f['file']}:{f['line']} - {f['message'][:100]}")
              
              # GitHub Output olarak ayarla
              with open(os.environ.get('GITHUB_OUTPUT', '/dev/null'), 'a') as fh:
                  print(f"critical_count={len(critical_findings)}", file=fh)
              
              sys.exit(1)  # Kritik bulgu varsa workflow'u fail et

          EOF

Branch Protection ile CodeQL Entegrasyonu

CodeQL sonuçlarını sadece rapor olarak bırakmak yerine pull request’lerin merge edilmesini engellemek için kullanabilirsiniz. Bunun için iki adım gerekiyor:

Birinci adım, workflow’unuzda check’in her zaman sonuç döndürdüğünden emin olun:

      - name: Perform CodeQL Analysis
        uses: github/codeql-action/analyze@v3
        with:
          category: "/language:${{ matrix.language }}"
          upload: always
        continue-on-error: false

İkinci adım, GitHub repository ayarlarından branch protection rules bölümüne gidin. “Require status checks to pass before merging” seçeneğini aktive edin ve CodeQL check’ini required olarak işaretleyin.

Artık CodeQL bulgu tespit ettiğinde PR merge edilemez hale gelir. Ancak burada dikkat edilmesi gereken bir nokta var: CodeQL sadece yeni eklenen kod için uyarı üretir, var olan açıklara karşı silent kalabilir. Bu nedenle scheduled scan’leri de mutlaka aktif tutun.

Performans Optimizasyonu

Büyük projelerde CodeQL analizi ciddi zaman alabilir. 500k+ satır kod içeren projelerde 2 saati aşan analiz süreleri sık karşılaşılan bir durumdur. Bu süreyi kısaltmak için şunları yapabilirsiniz:

      - name: Initialize CodeQL
        uses: github/codeql-action/init@v3
        with:
          languages: java-kotlin
          build-mode: manual
          # RAM ve thread optimizasyonu
          ram: 8192
          threads: 0  # 0 = tum CPU corelerini kullan

      - name: Cache CodeQL databases
        uses: actions/cache@v4
        with:
          path: ~/.codeql/databases
          key: codeql-${{ runner.os }}-${{ hashFiles('**/pom.xml') }}
          restore-keys: |
            codeql-${{ runner.os }}-

      - name: Build Application
        run: |
          # Sadece main source compile et, test kaynaklarini dahil etme
          mvn compile 
            -pl "!integration-tests,!performance-tests" 
            -am 
            -T 1C 
            -B

Özellikle şu ayarlar kritik:

  • threads: 0: Tüm CPU çekirdeklerini kullanır, runner’ın kapasitesine göre bu ciddi hızlanma sağlar
  • ram: 8192: Büyük projelerde yetersiz bellek analizi yarıda kesebilir, en az 6GB ayrılması önerilir
  • Test kaynaklarını dışarıda bırakmak: Test kodunu derlemek ve analiz etmek hem zaman hem kaynak israfı, paths-ignore ile bunu engelleyin

Secret Scanning ile CodeQL’i Tamamlamak

CodeQL güvenlik açıklarını tespit etmekte harika ama hardcoded secret’ları bulma konusunda uzmanlaşmamış. Bu boşluğu doldurmak için workflow’a ek araçlar ekleyebilirsiniz:

  secret-scan:
    name: Secret Scanning
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4
        with:
          fetch-depth: 0  # Tum commit gecmisi icin

      - name: TruffleHog Secret Scan
        uses: trufflesecurity/trufflehog@main
        with:
          path: ./
          base: ${{ github.event.repository.default_branch }}
          head: HEAD
          extra_args: --debug --only-verified

      - name: Detect Hardcoded Secrets with Gitleaks
        uses: gitleaks/gitleaks-action@v2
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          GITLEAKS_LICENSE: ${{ secrets.GITLEAKS_LICENSE }}

Bu iki araç birbirini tamamlar:

  • TruffleHog: Entropy analizi yaparak rastgele görünen string’leri tespit eder, regex tabanlı kuralları da var
  • Gitleaks: Git history’nin tamamını tarar, geçmişte commit edilip sonradan silinen secret’ları bile bulabilir

Sonuç

CodeQL’i CI/CD pipeline’ınıza entegre etmek, güvenlik açıklarını production’a ulaşmadan önce yakalamak için en etkili yöntemlerden biridir. Başlangıç olarak en basit workflow ile başlayın, ekibiniz CodeQL uyarılarına alıştıkça konfigürasyonu derinleştirin.

Pratik önerim şu yönde: İlk iki hafta workflow’u continue-on-error: true ile çalıştırın ve sadece raporlama yapın. Bu sürede hangi bulgu kategorilerinin gerçek tehdit, hangilerinin false positive olduğunu öğrenin. Sonra query filter’larınızı oluşturun ve ancak bundan sonra branch protection rule’larına ekleyin. Ekibi hazırlamadan sert bir kural koymak, güvenlik kültürü yerine direnç yaratır.

Scheduled scan’leri asla ihmal etmeyin. Push tetiklemeli analizler yeni kodu yakalar ama var olan teknik borç için haftalık tam tarama şarttır. Özellikle büyük dependency güncellemelerinden sonra manuel bir tam analiz tetiklemek iyi bir alışkanlıktır.

Son olarak, CodeQL sonuçlarını sadece GitHub Security tabında bırakmayın. SARIF dosyalarını parse ederek Slack’e bildirim göndermek veya Jira ticket açmak, bulguların gerçekten ilgilenilmesini sağlar. Görmediğiniz uyarı, olmayan uyarı kadar değersizdir.

Bir yanıt yazın

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