Ajan Tabanlı Statik Kod Analizi

Kodu kurallar yerine LLM ile okuyan ajan tabanlı SAST'ın mimarisini, model seçimini ve yetkilendirme, servisler arası güven, iş mantığı ve secret tespitindeki katkısını örnek kodlarla anlatıyoruz.

LLM’lerin gelişiminden önce SAST araçları kodu kural setleriyle tarar ve kullanıcı girdisini source’tan sink’e kadar izleyerek (taint analysis) zafiyet arardı. Bu yöntem SQL injection veya command injection gibi veri akışı olarak görünen zafiyetlerde hızlı ve tutarlı sonuç veriyordu. Bir endpoint’te unutulmuş yetkilendirme kontrolü, servisler arasında yanlış kurulmuş bir güven ilişkisi veya bir ödeme akışında atlanan durum kontrolü ise veri akışında iz bırakmadığı için bu araçlara görünmüyordu. Bu tür hatalar kodun ne yapması gerektiğini anlamayı gerektirdiği için ancak kod incelemesi ve sızma testi ile yakalanabiliyordu.

LLM’lerin kod okuma ve muhakeme yeteneği geliştikçe bu işin bir kısmı otomatik taramaya taşınabilir hale geldi. Ajan tabanlı SAST, kodu kurallar yerine bir LLM ile okuyup yorumlayan bir ajan kullanarak bu sınıftaki zafiyetleri hedefliyor. Bu yazıda yaklaşımın mimarisini, model seçeneklerini ve yetkilendirme, servisler arası güven, iş mantığı ve secret tespitindeki katkısını Fortify Agentic Analyzer (FAA) üzerinden örnek kodlarla anlatıyoruz.

Mimari

Ajan tabanlı SAST klasik SAST’ın yerini almaz, onun göremediği zafiyet sınıflarını kapsar. Klasik SAST kurallara dayandığı için aynı kodda her seferinde aynı sonucu verir ve veri akışı analiziyle bulunan zafiyetlerde geniş bir kapsam sağlar. Ajan tabanlı SAST’ta ise bir LLM kodu çok sayıda adımda okuyup yorumlar. Bu sayede kodun amacını anlamayı gerektiren tasarım ve iş mantığı hatalarını bulur, karşılığında her taramada LLM kullanımı kadar maliyet oluşur.

FAA, kodun bulunduğu makinede tek bir çalıştırılabilir dosya olarak çalışır. İçinde Pi adlı gömülü bir kodlama ajanı taşır ve LLM çağrılarını seçilen endpoint’e gönderir. İstenirse Pi yerine Claude Code da kullanılabilir. Taramaya kaynak kodun yanında mevcut SAST sonuçları ve önceki taramanın SARIF dosyası da girdi olarak verilebilir.

FAA mimarisi

Taranan kod güvenilmeyen içerik sayılır, çünkü koda gömülmüş bir prompt injection ajanı yönlendirmeye çalışabilir. Bu yüzden ajan sadece dosya okuma ve arama tool’larıyla çalışır, dosya yazma, komut çalıştırma ve ağ erişimi yetkisi yok.

Tarama sırasında dışarıya sadece LLM çağrıları ve lisans kontrolü çıkar. Kod yalnızca seçilen LLM endpoint’ine gider, Fortify’a gönderilmez. LLM kurum içinde çalışıyorsa kod kurum dışına hiç çıkmaz.

Tarama üç aşamada ilerler ve her aşama için farklı bir model seçilebilir. Zafiyetleri asıl analiz aşaması arar ve maliyetin büyük kısmı da bu aşamada oluşur.

Tarama aşamaları

Doğrulama aşaması her bulguyu yeniden değerlendirir ve istismar yolu kodda gerçekten mümkün değilse bulguyu eler. Mevcut SAST sonuçları verilirse SAST’ın zaten bulduğu bulgular da duplike olarak çıkarılır. Böylece raporda sadece SAST’ın göremediği bulgular kalır.

Model seçimi

FAA, AWS Bedrock, Google Vertex AI, Azure OpenAI, GitHub Copilot ve Anthropic gibi bulut sağlayıcılarıyla ve OpenAI uyumlu API sunan kurum içi endpoint’lerle çalışır. Model her aşama için ayrı seçilir. Varsayılan ayarda plan için küçük ve ucuz bir model, analiz için kod analizinde güçlü ve maliyeti makul bir model, doğrulama için en güçlü model kullanılır.

Kurum içi bir LLM kullanmak için endpoint pi_models.json dosyasında tanımlanır. Aşağıdaki örnekte OpenAI uyumlu bir sunucu üzerinden çalışan bir Gemma modeli tanımlanıyor.

{
  "providers": {
    "kurum-llm": {
      "baseUrl": "https://<llm-endpoint>/v1",
      "api": "openai-completions",
      "apiKey": "$LLM_API_TOKEN",
      "models": [
        {
          "id": "gemma-4-31b-it",
          "reasoning": true,
          "input": ["text"],
          "contextWindow": 262144,
          "maxTokens": 8192,
          "cost": { "input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0 }
        }
      ]
    }
  }
}

Policy dosyasında bu sağlayıcı ve model her aşamaya atanır.

agent=pi
pi.provider=kurum-llm
plan.model.family=gemma-4-31b-it
analysis.model.family=gemma-4-31b-it
validation.model.family=gemma-4-31b-it

# Kurum içi model için token ücreti yok, bütçe tavanı kapalı
maxcost=0

# Endpoint kapasitesine göre paralel istek sayısı
analysis.maxconcurrency=4

Bulut modelleriyle maxcost değeri tarama başına bir bütçe tavanı koyar. Kurum içi modellerde token ücreti olmadığı için asıl sınırı endpoint’in kapasitesi belirler, bu yüzden maxconcurrency değerleri sunucunun kaldırabileceği eşzamanlı istek sayısına göre ayarlanır. Tespit kalitesi doğrudan modele bağlı olduğu için FAA’nın test edilmiş kataloğu dışındaki bir modeli önce bilinen zafiyetler içeren bir uygulama üzerinde denemek gerekir.

Örnek uygulama

Bu yazıdaki örnekler, iki servisten oluşan sade bir e-ticaret backend’inden alındı.

  • gateway (Python) dışarıya açılan tek servis olarak JWT’yi doğrular ve isteği orders servisine iletir.
  • orders (Node.js) sipariş, ödeme, iptal ve iade işlemlerini yapar ve kullanıcı kimliğini gateway’in eklediği header’dan okur.

Uygulama FAA ile SAST sonuçları verilerek tarandı.

fortifyaa -scan ornek-magaza --fpr sast.fpr --output faa.sarif

Yetkilendirme tespiti

Orders servisinde aynı siparişe erişen iki endpoint var. /orders/:id siparişin isteği yapan kullanıcıya ait olup olmadığını kontrol ediyor, /orders/:id/invoice ise bu kontrolü yapmıyor.

app.get("/orders/:id", async (req, res) => {
  const order = await db.getOrder(req.params.id);
  if (!order || order.userId !== user(req).id) return res.sendStatus(404);
  res.json(order);
});

app.get("/orders/:id/invoice", async (req, res) => {
  const order = await db.getOrder(req.params.id);
  if (!order) return res.sendStatus(404);
  res.json({ address: order.address, total: order.total, card: order.cardLast4 });
});

Zafiyet fatura endpoint’indeki if (!order) satırından kaynaklanıyor. Bu satır sadece siparişin var olup olmadığına bakıyor. URL’deki sipariş id’sini değiştiren herhangi bir kullanıcı başka kullanıcıların teslimat adresini ve kartının son dört hanesini okuyabiliyor (IDOR).

Kodda kullanıcı girdisinin ulaştığı tehlikeli bir sink olmadığı için taint analysis bu zafiyeti göremiyor. Eksik olan bir kontrol ve bunu fark etmek için aynı kaynağa erişen diğer endpoint’in nasıl korunduğunu bilmek gerekiyor. FAA iki endpoint’i karşılaştırarak fatura endpoint’ini Critical seviyede raporladı.

FAA, /orders/:id endpoint’indeki sahiplik kontrolünün fatura endpoint’ine de eklenmesini önerdi. Öneri uygulandığında sipariş başka bir kullanıcıya aitse endpoint veri dönmeden 404 dönüyor.

app.get("/orders/:id/invoice", async (req, res) => {
  const order = await db.getOrder(req.params.id);
  if (!order || order.userId !== user(req).id) return res.sendStatus(404);
  res.json({ address: order.address, total: order.total, card: order.cardLast4 });
});

Servisler arası güven

Gateway JWT’yi doğruladıktan sonra kullanıcının kimliğini X-User-Id header’ı ile orders servisine iletiyor. Orders servisi kimliği ve rolü bu header’lardan okuyor, yani gateway’in gönderdiği değerlere güveniyor.

Zafiyet gateway’in iç servise gidecek header’ları hazırladığı iki satırdan kaynaklanıyor.

# gateway/app.py
headers = {k: v for k, v in request.headers.items() if k.lower() not in HOP_BY_HOP}
headers.setdefault("X-User-Id", str(claims["sub"]))

İlk satır client’ın gönderdiği bütün header’ları kopyalıyor. İkinci satırdaki setdefault sadece header yoksa değer yazdığı için, client kendi X-User-Id header’ını gönderirse gateway onu JWT’deki değerle değiştirmiyor. X-User-Role header’ını ise gateway hiç yazmıyor ve client ne gönderirse orders servisine o gidiyor.

// orders/server.js
function user(req) {
  return { id: req.header("x-user-id"), role: req.header("x-user-role") || "customer" };
}

app.post("/orders/:id/refund", async (req, res) => {
  if (user(req).role !== "admin") return res.sendStatus(403);
  ...

Orders servisi bu header’lara güvendiği için geçerli bir JWT’si olan her kullanıcı X-User-Id ile başka bir kullanıcı gibi işlem yapabiliyor, X-User-Role: admin ile de sadece adminlere açık iade endpoint’ini çağırabiliyor.

Servisler arası güven zafiyetinin istismarı

Gateway’deki kod ve orders servisindeki kod ayrı ayrı okunduğunda hatalı görünmüyor, zafiyet iki servis birlikte okunduğunda ortaya çıkıyor. FAA iki tarafı birlikte değerlendirip gateway’deki header iletimini, orders servisinin header’a güvenmesini ve iade endpoint’indeki rol kontrolünü üç ayrı bulgu olarak raporladı.

FAA, client’tan gelen kimlik header’larının gateway’de silinmesini ve doğrulanmış JWT claim’lerinden her istekte yeniden yazılmasını önerdi. Öneri uygulandığında orders servisine ulaşan kimlik ve rol değerleri her zaman JWT’den geliyor.

IDENTITY_HEADERS = {"x-user-id", "x-user-role"}
headers = {k: v for k, v in request.headers.items()
           if k.lower() not in HOP_BY_HOP | IDENTITY_HEADERS}
headers["X-User-Id"] = str(claims["sub"])
headers["X-User-Role"] = claims.get("role", "customer")

FAA ayrıca orders servisinin ham header yerine gateway’in imzaladığı bir kimlik token’ını doğrulamasını veya çağıran servisi mTLS ile doğrulamasını önerdi. Bu önlem, iç ağa erişip gateway’i atlayarak doğrudan orders servisine header gönderen birinin kimlik taklidi yapmasını engelliyor.

İş mantığı tespiti

Ödeme endpoint’i kullanıcının bakiyesini okuyor, yeterli olup olmadığını kontrol ediyor ve yeni bakiyeyi ayrı bir sorguyla yazıyor.

const wallet = await db.getWallet(u.id);
if (wallet.balance < order.total) return res.status(402).end();
await payments.capture(order.id, order.total);
await db.setBalance(u.id, wallet.balance - order.total);

Zafiyet bakiyenin okunduğu satır ile yazıldığı satır arasındaki boşluktan kaynaklanıyor. Aynı anda gönderilen iki ödeme isteği aynı bakiyeyi okuyor ve ikisi de kontrolü geçiyor. Örneğin 100 TL bakiyesi olan bir kullanıcı 100 TL’lik iki siparişi aynı anda ödeyebiliyor. FAA bunu race condition olarak raporladı.

FAA, bakiye kontrolü ile düşümünün tek bir atomik sorguda yapılmasını ve sorgunun etkilediği satır sayısının kontrol edilmesini önerdi. Öneri uygulandığında kontrol WHERE koşulunda yapılıyor ve veritabanı iki isteği sırayla işliyor. Bakiyesi yetmeyen ikinci istek hiçbir satırı güncellemiyor ve ödeme alınmadan 402 dönüyor.

const r = await db.query(
  "UPDATE wallets SET balance = balance - $2 WHERE user_id = $1 AND balance >= $2",
  [u.id, order.total]);
if (r.rowCount === 0) return res.status(402).end();
await payments.capture(order.id, order.total);

Secret tespiti

Kalıp tabanlı secret kuralları kodda düz metin olarak geçen ve bilinen bir formata uyan değerleri arar. Ajan tabanlı analiz ise bir değerin nasıl kullanıldığını okuduğu için bu kuralların dışında kalan durumları da yakalar. FAA örnekte aşağıdaki üç satırı raporladı.

# gateway/config.py
JWT_SECRET = os.getenv("JWT_SECRET", "mgz-jwt-7c1e9a4b2f6d8e03a5b7c9d1f2e4a6b8")

# gateway/app.py
log.info("proxy %s %s headers=%s", request.method, path, headers)
// orders/payments.js
const PSP_KEY = Buffer.from("cGF5X2xpdmVfNGVjOTFk...", "base64").toString();
  • JWT_SECRET ortam değişkeni tanımlı değilse koddaki sabit anahtar kullanılıyor ve bu anahtarı bilen biri istediği kullanıcı için geçerli JWT üretebiliyor.
  • Gateway bütün header’ları logladığı için Authorization header’ındaki JWT’ler loglara yazılıyor ve loglara erişen biri bu token’larla kullanıcı adına istek yapabiliyor.
  • Ödeme sağlayıcısı anahtarı base64 ile gizlendiği için kalıp tabanlı kurallara takılmıyor, ama base64 geri çevrilebildiği için kodu okuyan herkes anahtarı elde edebiliyor.

FAA, sabit anahtarın kaldırılıp ortam değişkeni yoksa uygulamanın başlamamasını, hassas header’ların loglanmadan önce maskelenmesini ve PSP anahtarının secret manager’a taşınıp değiştirilmesini önerdi. Öneriler uygulandığında kod şu şekilde olur.

JWT_SECRET = os.environ["JWT_SECRET"]  # tanımlı değilse uygulama başlamaz

SENSITIVE_HEADERS = {"authorization", "cookie", "x-user-id", "x-user-role"}
safe = {k: "***" if k.lower() in SENSITIVE_HEADERS else v for k, v in headers.items()}
log.info("proxy %s %s headers=%s", request.method, path, safe)
const PSP_KEY = process.env.PSP_KEY;  // secret manager'dan ortama aktarılır

Pipeline’da kullanım

LLM maliyeti analiz edilen dosya sayısıyla arttığı için FAA pipeline’da her tetikleyicide farklı bir tarama moduyla çalıştırılır.

Pipeline'da FAA tarama modları

  • differential pull request’te çalışır ve sadece PR’ın değiştirdiği satırlardaki yeni bulguları raporlar. Geliştiriciye hızlı geri bildirim verir, sonucu SSC’ye yüklenmez.
  • incremental main dalına merge’de çalışır. Önceki taramadan bu yana değişen dosyaları analiz edip sonucu önceki taramayla birleştirir ve SSC’deki bulgu listesini güncel tutar.
  • full haftalık çalışır ve bütün kodu analiz eder. Diğer iki mod sadece değişen dosyalara baktığı için bir dosyadaki değişikliğin başka bir dosyada yarattığı zafiyeti bu tarama yakalar.

Her tarama önceki taramanın sonucunu baseline olarak alır. Bu sayede SSC’de bir bulguya verilen false positive veya audit kararı sonraki taramalarda korunur ve gate sadece açık bulgulara göre karar verir.