Jenkins ile Otomatik Güvenlik Taraması: CI/CD Pipeline’ınızı Güvende Tutun
Güvenlik taraması deyince çoğu ekip “biz zaten manuel kontrol yapıyoruz” der. Sonra bir gün prod ortamına SQL injection açığı olan bir uygulama deploy edilir ve o “manuel kontrol” masalı biter. Jenkins pipeline’ına güvenlik taramasını entegre etmek hem bu tür sürprizleri önler hem de güvenlik kontrolünü geliştirici sürecinin doğal bir parçası haline getirir. Bu yazıda sıfırdan bir güvenlik odaklı Jenkins pipeline’ı nasıl kurarsınız, hangi araçları kullanırsınız ve gerçek hayatta nasıl çalışır, bunları detaylı ele alacağız.
Neden CI/CD’de Güvenlik Taraması?
“Shift left security” kavramını duymuşsunuzdur. Basitçe şu demek: güvenlik açıklarını ne kadar erken yakalarsanız, düzeltme maliyeti o kadar düşük olur. Bir açığı geliştirme aşamasında bulmak ile prod’da bulmak arasındaki maliyet farkı, araştırmalara göre 100 katın üzerinde olabilir.
Jenkins pipeline’ında güvenlik taraması yapmanın pratik faydaları şunlar:
- Otomatik ve tutarlı: Her commit’te aynı kontroller çalışır, insan faktörü ortadan kalkar
- Hızlı geri bildirim: Geliştirici kodu push eder etmez 10-15 dakika içinde güvenlik durumunu görür
- Dokümantasyon: Her build’in güvenlik raporu arşivlenir, audit için hazır olur
- Kapı mekanizması: Kritik açıklar varsa deployment otomatik bloke edilir
Ortam Hazırlığı
Bu yazıda kullanacağımız stack:
- Jenkins 2.400+
- OWASP Dependency Check (bağımlılık taraması)
- Trivy (container güvenlik taraması)
- SonarQube (statik kod analizi)
- OWASP ZAP (dinamik uygulama güvenlik testi)
- Bandit (Python projeleri için)
Önce Jenkins sunucusunda gerekli araçları kuralım. Ubuntu/Debian tabanlı bir sistem varsayıyorum:
# Trivy kurulumu
sudo apt-get install wget apt-transport-https gnupg lsb-release -y
wget -qO - https://aquasecurity.github.io/trivy-repo/deb/public.key | sudo apt-key add -
echo deb https://aquasecurity.github.io/trivy-repo/deb $(lsb_release -sc) main | sudo tee -a /etc/apt/sources.list.d/trivy.list
sudo apt-get update && sudo apt-get install trivy -y
# OWASP ZAP kurulumu (Docker üzerinden)
docker pull ghcr.io/zaproxy/zaproxy:stable
# Bandit kurulumu (Python projeleri için)
pip3 install bandit
# OWASP Dependency Check
wget https://github.com/jeremylong/DependencyCheck/releases/download/v8.4.0/dependency-check-8.4.0-release.zip
unzip dependency-check-8.4.0-release.zip -d /opt/
chmod +x /opt/dependency-check/bin/dependency-check.sh
Jenkins’e gerekli plugin’leri de kurmamız gerekiyor. Jenkins > Manage Jenkins > Plugins menüsünden şunları ekleyin:
- OWASP Dependency-Check Plugin
- HTML Publisher Plugin (raporlar için)
- SonarQube Scanner Plugin
- Warnings Next Generation Plugin
Temel Pipeline Yapısı
Security taramasını birkaç aşamaya bölmek hem okunabilirlik hem de hata yönetimi açısından mantıklı. Şimdi adım adım bir Jenkinsfile oluşturalım:
pipeline {
agent any
environment {
APP_NAME = 'myapp'
DOCKER_IMAGE = "myapp:${BUILD_NUMBER}"
SONAR_HOST = 'http://sonarqube:9000'
SONAR_TOKEN = credentials('sonar-token')
ZAP_TARGET_URL = 'http://staging.myapp.internal'
SEVERITY_THRESHOLD = 'HIGH'
}
options {
buildDiscarder(logRotator(numToKeepStr: '30'))
timeout(time: 60, unit: 'MINUTES')
timestamps()
}
stages {
stage('Checkout') {
steps {
checkout scm
sh 'git log --oneline -5'
}
}
stage('Parallel Security Scans') {
parallel {
stage('Dependency Check') {
steps {
dependencyCheck additionalArguments: '''
--scan .
--format HTML
--format JSON
--out reports/dependency-check
--failOnCVSS 7
''', odcInstallation: 'dependency-check'
}
}
stage('Static Code Analysis') {
steps {
withSonarQubeEnv('SonarQube') {
sh '''
sonar-scanner
-Dsonar.projectKey=${APP_NAME}
-Dsonar.sources=.
-Dsonar.host.url=${SONAR_HOST}
'''
}
}
}
stage('Secret Scanning') {
steps {
sh 'gitleaks detect --source . --report-format json --report-path reports/gitleaks-report.json || true'
}
}
}
}
stage('Build Docker Image') {
steps {
sh "docker build -t ${DOCKER_IMAGE} ."
}
}
stage('Container Security Scan') {
steps {
sh """
trivy image
--exit-code 0
--severity LOW,MEDIUM
--format json
-o reports/trivy-low-medium.json
${DOCKER_IMAGE}
trivy image
--exit-code 1
--severity HIGH,CRITICAL
--format json
-o reports/trivy-critical.json
${DOCKER_IMAGE}
"""
}
}
stage('Quality Gate') {
steps {
timeout(time: 5, unit: 'MINUTES') {
waitForQualityGate abortPipeline: true
}
}
}
stage('DAST - ZAP Scan') {
when {
branch 'main'
}
steps {
sh """
docker run --rm
-v $(pwd)/reports:/zap/wrk:rw
ghcr.io/zaproxy/zaproxy:stable
zap-baseline.py
-t ${ZAP_TARGET_URL}
-r zap-report.html
-J zap-report.json
-l WARN
"""
}
}
}
post {
always {
publishHTML([
allowMissing: true,
alwaysLinkToLastBuild: true,
keepAll: true,
reportDir: 'reports',
reportFiles: 'dependency-check/dependency-check-report.html',
reportName: 'Dependency Check Report'
])
archiveArtifacts artifacts: 'reports/**/*', allowEmptyArchive: true
}
failure {
slackSend(
color: 'danger',
message: "Security scan FAILED: ${env.JOB_NAME} #${env.BUILD_NUMBER}n${env.BUILD_URL}"
)
}
success {
slackSend(
color: 'good',
message: "Security scan PASSED: ${env.JOB_NAME} #${env.BUILD_NUMBER}"
)
}
}
}
Secret Scanning ile Credential Sızıntısı Önleme
Gitleaks, kod repository’sinde gizli anahtar, token, şifre gibi bilgilerin bulunup bulunmadığını kontrol eder. Bu tarama diğer herşeyden önce gelmeli çünkü bir AWS key’i ya da veritabanı şifresi commit’lenmiş olabilir.
# Gitleaks kurulumu
wget https://github.com/gitleaks/gitleaks/releases/download/v8.18.0/gitleaks_8.18.0_linux_x64.tar.gz
tar -xzf gitleaks_8.18.0_linux_x64.tar.gz
sudo mv gitleaks /usr/local/bin/
# Test amaçlı manuel çalıştırma
gitleaks detect --source . --verbose --report-format json --report-path gitleaks-report.json
# Commit geçmişini de tara
gitleaks detect --source . --log-opts="--all" --report-format json --report-path gitleaks-history.json
Özel bir .gitleaks.toml yapılandırması oluşturmak isteyebilirsiniz. Örneğin test dosyalarındaki örnek token’ları whitelist’e almak için:
[allowlist]
description = "Test dosyalari ve ornekler haric tut"
regexes = ['''test-token-.*''', '''example-api-key-.*''']
paths = [
'''tests/fixtures/.*''',
'''docs/examples/.*'''
]
[[rules]]
description = "Ozel API Token Kurali"
id = "custom-api-token"
regex = '''MYAPP_TOKEN_[A-Z0-9]{32}'''
tags = ["api", "custom"]
Python Projeleri için Bandit Entegrasyonu
Eğer Python tabanlı bir uygulama geliştiriyorsanız, Bandit statik güvenlik taraması için vazgeçilmez bir araç. Bilinen güvenlik açığı pattern’larını kod içinde arar.
# Jenkins pipeline içinde Bandit kullanımı
bandit -r ./src
-f json
-o reports/bandit-report.json
-l
--severity-level medium
-x ./tests,./venv
# Sonucu okunabilir HTML'e çevir
bandit -r ./src
-f html
-o reports/bandit-report.html
--severity-level medium
-x ./tests,./venv
# Çıkış kodunu kontrol et (0: temiz, 1: sorun var)
echo "Bandit exit code: $?"
Bandit çıktısında şu severity seviyeleri var:
- HIGH: Hemen düzeltilmeli, blocker olarak işaretle
- MEDIUM: Sprint içinde çözülmeli
- LOW: Teknik borç olarak kayıt altına al
Trivy ile Container Image Taraması
Trivy hem OS paket güvenlik açıklarını hem de uygulama bağımlılıklarını tarar. Docker build’den hemen sonra çalıştırmak en doğrusu:
#!/bin/bash
# trivy-scan.sh
IMAGE_NAME=$1
REPORT_DIR="reports/trivy"
mkdir -p $REPORT_DIR
echo "==> Trivy ile $IMAGE_NAME taranıyor..."
# Önce tüm severity için özet rapor
trivy image
--exit-code 0
--format table
--output $REPORT_DIR/summary.txt
$IMAGE_NAME
# JSON formatında detaylı rapor
trivy image
--exit-code 0
--format json
--output $REPORT_DIR/full-report.json
$IMAGE_NAME
# CRITICAL açıklar için pipeline'ı durdur
trivy image
--exit-code 1
--severity CRITICAL
--ignore-unfixed
$IMAGE_NAME
CRITICAL_EXIT=$?
if [ $CRITICAL_EXIT -ne 0 ]; then
echo "CRITICAL güvenlik açıkları bulundu! Pipeline durduruluyor."
exit 1
fi
echo "==> Trivy taraması tamamlandı. Raporlar: $REPORT_DIR/"
--ignore-unfixed parametresi önemli. Bu parametre, henüz patch’i yayınlanmamış güvenlik açıklarını atlıyor. Yoksa her build’de düzeltemeyeceğiniz açıklar yüzünden pipeline sürekli fail olur.
OWASP ZAP ile Dinamik Tarama
ZAP, çalışan uygulamaya HTTP istekleri göndererek güvenlik açıklarını arar. Bu DAST (Dynamic Application Security Testing) yaklaşımı, statik analizin yakalayamadığı runtime açıklarını bulur. Staging ortamınıza her deploy sonrası çalıştırmak ideal:
#!/bin/bash
# zap-scan.sh
TARGET_URL=$1
REPORT_DIR="reports/zap"
mkdir -p $REPORT_DIR
# ZAP Baseline scan - hızlı ve pasif tarama
docker run --rm
--network host
-v $(pwd)/$REPORT_DIR:/zap/wrk:rw
ghcr.io/zaproxy/zaproxy:stable
zap-baseline.py
-t $TARGET_URL
-r zap-baseline-report.html
-J zap-baseline-report.json
-l WARN
-z "-config api.disablekey=true"
# Full scan - daha kapsamlı, daha uzun sürer
# Sadece haftalık veya release öncesi çalıştırın
# docker run --rm
# -v $(pwd)/$REPORT_DIR:/zap/wrk:rw
# ghcr.io/zaproxy/zaproxy:stable
# zap-full-scan.py
# -t $TARGET_URL
# -r zap-full-report.html
ZAP_EXIT=$?
# ZAP exit kodları:
# 0: Uyarı yok
# 1: Uyarılar var
# 2: Hata oluştu
if [ $ZAP_EXIT -eq 2 ]; then
echo "ZAP tarama hatası! Çıkılıyor."
exit 1
fi
echo "ZAP taraması tamamlandı. Çıkış kodu: $ZAP_EXIT"
ZAP taramasını authenticated oturumlarla çalıştırmak için ek yapılandırma gerekiyor. Eğer uygulamanız login gerektiriyorsa:
# Authenticated ZAP taraması
docker run --rm
-v $(pwd)/reports/zap:/zap/wrk:rw
-v $(pwd)/zap-auth.conf:/zap/auth.conf:ro
ghcr.io/zaproxy/zaproxy:stable
zap-baseline.py
-t $TARGET_URL
-r zap-auth-report.html
--hook=/zap/auth.conf
-z "-config replacer.full_list(0).description=auth-header
-config replacer.full_list(0).enabled=true
-config replacer.full_list(0).matchtype=REQ_HEADER
-config replacer.full_list(0).matchstr=Authorization
-config replacer.full_list(0).replacement=Bearer ${AUTH_TOKEN}"
Güvenlik Raporlarını Merkezi Toplamak
Her araçtan farklı formatta rapor çıkıyor. Bunları anlamlı hale getirmek için basit bir aggregation script’i yazalım:
#!/usr/bin/env python3
# security-report-aggregator.py
import json
import os
import sys
from datetime import datetime
def load_json_report(filepath):
if not os.path.exists(filepath):
return None
with open(filepath, 'r') as f:
return json.load(f)
def parse_trivy_report(report):
findings = {'CRITICAL': 0, 'HIGH': 0, 'MEDIUM': 0, 'LOW': 0}
if not report or 'Results' not in report:
return findings
for result in report.get('Results', []):
for vuln in result.get('Vulnerabilities', []):
severity = vuln.get('Severity', 'UNKNOWN')
if severity in findings:
findings[severity] += 1
return findings
def parse_gitleaks_report(report):
if not report:
return 0
return len(report) if isinstance(report, list) else 0
def generate_summary(build_number, reports_dir):
summary = {
'build_number': build_number,
'timestamp': datetime.now().isoformat(),
'status': 'PASS',
'findings': {}
}
# Trivy sonuclari
trivy_report = load_json_report(f'{reports_dir}/trivy/full-report.json')
trivy_findings = parse_trivy_report(trivy_report)
summary['findings']['container_vulnerabilities'] = trivy_findings
# Gitleaks sonuclari
gitleaks_report = load_json_report(f'{reports_dir}/gitleaks-report.json')
secret_count = parse_gitleaks_report(gitleaks_report)
summary['findings']['exposed_secrets'] = secret_count
# Genel durum
if trivy_findings.get('CRITICAL', 0) > 0 or secret_count > 0:
summary['status'] = 'FAIL'
elif trivy_findings.get('HIGH', 0) > 5:
summary['status'] = 'WARN'
output_path = f'{reports_dir}/security-summary.json'
with open(output_path, 'w') as f:
json.dump(summary, f, indent=2)
print(f"Ozet rapor olusturuldu: {output_path}")
print(f"Genel Durum: {summary['status']}")
print(json.dumps(summary['findings'], indent=2))
return 0 if summary['status'] != 'FAIL' else 1
if __name__ == '__main__':
build_num = os.environ.get('BUILD_NUMBER', 'local')
reports_dir = sys.argv[1] if len(sys.argv) > 1 else 'reports'
sys.exit(generate_summary(build_num, reports_dir))
Gerçek Dünya Senaryosu: E-Ticaret Projesi
Bir e-ticaret platformu için kurduğum pipeline’da karşılaştığım durumu paylaşayım. Uygulama Python/Django, PostgreSQL, Redis ve bir React frontend’den oluşuyordu.
İlk çalıştırmada:
- Trivy, base image olarak kullanılan
python:3.9içinde 47 HIGH, 12 CRITICAL açık buldu. Çözüm:python:3.9-slimkullandık ve açık sayısı 8 HIGH, 2 CRITICAL’a düştü - Gitleaks, 3 yıl önce commit’lenmiş bir AWS test key’i buldu. Key devre dışı bırakıldı, git history’den temizlendi
- Bandit, kullanıcı girdisinin
eval()ile çalıştırıldığı bir yer buldu. Kritik bir açıktı - Dependency Check, kullanılan
Pillowkütüphanesinin eski versiyonunda arbitrary code execution açığı olduğunu gösterdi
Tüm bu bulgular manuel review’da kaçmıştı. Pipeline sayesinde aynı gün tespit edildi ve düzeltildi.
Pipeline’ı ilk kurduğunuzda muhtemelen çok sayıda bulgu göreceksiniz. Bunlarla nasıl başa çıkacaksınız:
- Aşamalı sıkılaştırma: İlk hafta sadece CRITICAL’ları blocker olarak işaretleyin, HIGH’ları warning olarak bırakın. Ekip adapte oldukça eşiği düşürün
- Suppress mekanizması: Bilinen ve kabul edilmiş riskleri suppression dosyasına ekleyin, pipeline gürültüsünü azaltın
- Sprint’e dahil edin: Güvenlik bulgularını backlog’a ticket olarak ekleyin, sprint planlama sürecine dahil edin
Suppression ve Exception Yönetimi
Her güvenlik açığı derhal kapatılamaz. Bazıları yanlış pozitif olabilir, bazıları için henüz patch yayınlanmamıştır. OWASP Dependency Check için suppression dosyası:
<?xml version="1.0" encoding="UTF-8"?>
<suppressions xmlns="https://jeremylong.github.io/DependencyCheck/dependency-suppression.1.3.xsd">
<suppress>
<notes>
Bu CVE bizi etkilemiyor cunku sorunlu ozelligi kullanmiyoruz.
Son kontrol tarihi: 2024-01-15
Sorumlu: [email protected]
</notes>
<cve>CVE-2023-44981</cve>
<until>2024-07-01</until>
</suppress>
<suppress>
<notes>Test bagimliliginda, production build'e dahil degil.</notes>
<filePath regex="true">.*test-only-lib.*.jar</filePath>
<cve>CVE-2023-12345</cve>
</suppress>
</suppressions>
Not: until alanını mutlaka doldurun. Süresiz suppress tehlikelidir, belirli bir tarihe kadar tolere etmek daha sağlıklı.
Pipeline Performans Optimizasyonu
Güvenlik taramaları zaman alır. 60 dakika bekleyen bir pipeline, geliştiricileri bir süre sonra bypass etmeye iter. Hızlandırma için:
# Trivy cache kullanımı - her seferinde DB indirmesin
trivy image
--cache-dir /var/cache/trivy
--download-db-only
# Bu komutu cron ile günde bir çalıştırın
# 0 2 * * * trivy image --cache-dir /var/cache/trivy --download-db-only
# Sonra pipeline'da cache'i kullan
trivy image
--cache-dir /var/cache/trivy
--skip-db-update
--format json
-o reports/trivy-report.json
${DOCKER_IMAGE}
Parallel stage kullanımı, toplam süreyi önemli ölçüde kısaltıyor. Dependency Check ve SonarQube analizi birbirinden bağımsız, aynı anda çalışabilirler. Bu sayede 20 dakikalık iki tarama 20 dakikada tamamlanmış olur.
Sonuç
Jenkins’e güvenlik taraması entegrasyonu başlangıçta karmaşık görünüyor ama doğru planlandığında oldukça yönetilebilir bir yapıya kavuşuyor. Önemli olan “her şeyi mükemmel kur” yerine “önce temel taramaları çalıştır, sonra geliştir” yaklaşımı benimsemek.
Öncelik sırasıyla ilerleyin: Gitleaks ile secret taramasından başlayın, ardından Trivy ile container güvenliğini ekleyin, sonra Dependency Check ile üçüncü parti bağımlılıkları kontrol altına alın. Bunlar çalışır hale geldikten sonra SonarQube ve ZAP entegrasyonuna geçin.
En kritik nokta ise kültürel dönüşüm. Pipeline güvenlik açığı bulduğunda “bu araç hatalı” değil, “bu açığı kapatalım” diye düşünen bir ekip kültürü oluşturmanız gerekiyor. Suppression mekanizmasını titizlikle yönetin, her exception için gerekçe yazın ve düzenli aralıklarla review edin. Güvenlik taraması bir engel değil, kalite güvencesinin doğal bir parçası olduğunda ekip içinde benimsenmesi çok daha kolay oluyor.
