samliumay commited on
Commit
efd7792
·
1 Parent(s): b2ac7b8

Deploy Ollama cyber security assistant

Browse files
.dockerignore ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .git
2
+ .gitignore
3
+ .env
4
+ .env.*
5
+ !.env.example
6
+ .venv
7
+ __pycache__
8
+ *.py[cod]
9
+ .pytest_cache
10
+ .ruff_cache
11
+ .coverage
12
+ tests
13
+ uv.lock
.env.example ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Yerel Ollama
2
+ OLLAMA_BASE_URL=http://127.0.0.1:11434
3
+ OLLAMA_MODEL=qwen3.6:latest
4
+ OLLAMA_TIMEOUT_SECONDS=300
5
+ OLLAMA_NUM_CTX=16384
6
+ OLLAMA_TEMPERATURE=0.2
7
+ # Yalnızca kimlik doğrulayan uzak bir Ollama geçidi kullanılıyorsa:
8
+ # OLLAMA_API_KEY=
9
+
10
+ # Harici veri kaynaklarının ağ toleransı ve haber önbelleği
11
+ CYBER_API_CONNECT_TIMEOUT=20
12
+ CYBER_API_READ_TIMEOUT=60
13
+ GDELT_CONNECT_TIMEOUT=30
14
+ GDELT_READ_TIMEOUT=75
15
+ CYBER_NEWS_CACHE_SECONDS=300
16
+
17
+ # İsteğe bağlı veri kaynakları
18
+ # NEWSAPI_KEY=
19
+ # NVD_API_KEY=
20
+ # OTX_API_KEY=
21
+ # MISP_BASE_URL=https://misp.example.org
22
+ # MISP_API_KEY=
23
+ # MISP_VERIFY_TLS=true
.gitignore ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ .env
2
+ .venv/
3
+ __pycache__/
4
+ *.py[cod]
5
+ .pytest_cache/
6
+ .ruff_cache/
7
+ .coverage
.python-version ADDED
@@ -0,0 +1 @@
 
 
1
+ 3.11
Dockerfile ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM ollama/ollama:latest
2
+
3
+ # Ollama imajındaki varsayılan entrypoint, verilen her komutu `ollama ...`
4
+ # olarak çalıştırır. Space başlangıç betiğini doğrudan çalıştırabilmek için sıfırla.
5
+ ENTRYPOINT []
6
+
7
+ ENV DEBIAN_FRONTEND=noninteractive \
8
+ PYTHONDONTWRITEBYTECODE=1 \
9
+ PYTHONUNBUFFERED=1 \
10
+ PATH="/opt/venv/bin:${PATH}" \
11
+ GRADIO_SERVER_NAME=0.0.0.0 \
12
+ GRADIO_SERVER_PORT=7860 \
13
+ OLLAMA_BASE_URL=http://127.0.0.1:11434 \
14
+ OLLAMA_HOST=127.0.0.1:11434 \
15
+ OLLAMA_MODEL=qwen3.6:latest \
16
+ OLLAMA_NUM_PARALLEL=1 \
17
+ OLLAMA_MAX_LOADED_MODELS=1
18
+
19
+ RUN apt-get update \
20
+ && apt-get install --yes --no-install-recommends \
21
+ python3 \
22
+ python3-pip \
23
+ python3-venv \
24
+ ca-certificates \
25
+ && python3 -m venv /opt/venv \
26
+ && /opt/venv/bin/pip install --no-cache-dir --upgrade pip \
27
+ && rm -rf /var/lib/apt/lists/*
28
+
29
+ WORKDIR /app
30
+
31
+ COPY requirements.txt /app/requirements.txt
32
+ RUN /opt/venv/bin/pip install --no-cache-dir -r /app/requirements.txt
33
+
34
+ # Güncel Ollama image'ında UID 1000, `ubuntu` kullanıcısı olarak hazır gelir.
35
+ RUN mkdir -p /home/ubuntu/.ollama \
36
+ && chown -R ubuntu:ubuntu /home/ubuntu /app
37
+
38
+ COPY --chown=ubuntu:ubuntu . /app
39
+ RUN chmod +x /app/start.sh
40
+
41
+ USER ubuntu
42
+ ENV HOME=/home/ubuntu
43
+
44
+ EXPOSE 7860
45
+
46
+ CMD ["/app/start.sh"]
README.md CHANGED
@@ -1,12 +1,199 @@
1
  ---
2
- title: CI Assistani
3
- emoji:
4
- colorFrom: gray
5
- colorTo: green
6
  sdk: docker
7
- pinned: false
8
- license: mit
9
- short_description: Cyber Inteligence için küçük bir model
 
 
 
 
 
 
10
  ---
11
 
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: Siber Güvenlik Haber Asistanı
3
+ emoji: 🛡️
4
+ colorFrom: blue
5
+ colorTo: indigo
6
  sdk: docker
7
+ app_port: 7860
8
+ fullWidth: true
9
+ short_description: Ollama ve araç çağrılarıyla güncel CVE ve siber güvenlik haberleri
10
+ tags:
11
+ - ollama
12
+ - cybersecurity
13
+ - tool-calling
14
+ - cve
15
+ - turkish
16
  ---
17
 
18
+ # Siber Güvenlik Haber Asistanı
19
+
20
+ Yerel bir Ollama modelinin güncel siber güvenlik kaynaklarını araç çağrılarıyla
21
+ sorguladığı Türkçe Gradio uygulaması. Arayüz; modelin çağırdığı aracı,
22
+ parametrelerini, dönen sonucu, tur numarasını ve nihai yanıtı ayrı ayrı gösterir.
23
+ Modelin gizli düşünce metni gösterilmez.
24
+
25
+ ## Veri kaynakları
26
+
27
+ Anahtarsız kullanılabilen kaynaklar:
28
+
29
+ - GDELT — güncel haber araması
30
+ - CISA KEV — aktif istismar edildiği bilinen zafiyetler
31
+ - NIST NVD — CVE arama ve ayrıntıları
32
+ - CISA RSS ve CERT/CC Atom — resmi güvenlik duyuruları
33
+ - MITRE ATT&CK TAXII 2.1 — Enterprise teknikleri
34
+
35
+ İsteğe bağlı kaynaklar:
36
+
37
+ - NewsAPI (`NEWSAPI_KEY`)
38
+ - AlienVault OTX (`OTX_API_KEY`)
39
+ - NVD yüksek istek limiti (`NVD_API_KEY`)
40
+ - Kurumsal MISP (`MISP_BASE_URL`, `MISP_API_KEY`)
41
+
42
+ API anahtarları hiçbir zaman kaynak koda veya model mesajına eklenmez; yalnızca
43
+ sunucu tarafındaki ortam değişkenlerinden okunur.
44
+
45
+ ## Yerel çalıştırma
46
+
47
+ Gereksinimler:
48
+
49
+ - Python 3.11+
50
+ - Çalışan bir [Ollama](https://docs.ollama.com/) sunucusu
51
+ - Araç çağrısını destekleyen bir Ollama modeli
52
+
53
+ ```bash
54
+ ollama serve
55
+ ollama pull qwen3.6:latest
56
+ cp .env.example .env
57
+ uv sync
58
+ uv run python app.py
59
+ ```
60
+
61
+ `uv` kullanmıyorsanız:
62
+
63
+ ```bash
64
+ python -m venv .venv
65
+ source .venv/bin/activate
66
+ pip install -r requirements.txt
67
+ python app.py
68
+ ```
69
+
70
+ Uygulama varsayılan olarak `http://127.0.0.1:11434` ve
71
+ `qwen3.6:latest` kullanır. Ayarlar kabuktan verilebilir:
72
+
73
+ ```bash
74
+ export OLLAMA_MODEL=qwen3.6:latest
75
+ export NEWSAPI_KEY=...
76
+ export NVD_API_KEY=...
77
+ python app.py
78
+ ```
79
+
80
+ `.env` dosyası örnek amaçlıdır; Python uygulaması bu dosyayı kendiliğinden
81
+ yüklemez. Değerleri kabuğa aktarın veya dağıtım platformunun secret yönetimini
82
+ kullanın.
83
+
84
+ ## Çalışma biçimi
85
+
86
+ Örnek soru:
87
+
88
+ > Son 7 günde yayımlanan kritik CVE'leri bul ve aktif istismar durumlarını kontrol et.
89
+
90
+ Beklenen akış:
91
+
92
+ 1. Model `son_cve_kayitlarini_getir` aracını çağırır.
93
+ 2. Dönen CVE'lerden gerekli gördükleri için `cve_detayi_getir` veya
94
+ `aktif_istismar_edilenleri_ara` çağrısını yapar.
95
+ 3. Arayüz her çağrıyı ve sonucu tur bazında gösterir.
96
+ 4. Model, tarih ve kaynak bağlantıları içeren Türkçe nihai yanıtı üretir.
97
+
98
+ Model en fazla altı araç turu çalıştırabilir. Bu sınır hatalı veya sonsuz araç
99
+ döngülerini engeller.
100
+
101
+ ## Yapılandırma
102
+
103
+ | Değişken | Varsayılan | Açıklama |
104
+ |---|---:|---|
105
+ | `OLLAMA_BASE_URL` | `http://127.0.0.1:11434` | Ollama veya uyumlu güvenli geçit |
106
+ | `OLLAMA_MODEL` | `qwen3.6:latest` | Kullanılacak model |
107
+ | `OLLAMA_API_KEY` | boş | Uzak geçit için Bearer anahtarı |
108
+ | `OLLAMA_TIMEOUT_SECONDS` | `300` | Model yanıt zaman aşımı |
109
+ | `OLLAMA_NUM_CTX` | `16384` | Bağlam penceresi |
110
+ | `OLLAMA_TEMPERATURE` | `0.2` | Üretim sıcaklığı |
111
+ | `CYBER_API_CONNECT_TIMEOUT` | `20` | Genel API bağlantı zaman aşımı |
112
+ | `CYBER_API_READ_TIMEOUT` | `60` | Genel API okuma zaman aşımı |
113
+ | `GDELT_CONNECT_TIMEOUT` | `30` | GDELT için bağlantı zaman aşımı |
114
+ | `GDELT_READ_TIMEOUT` | `75` | GDELT için okuma zaman aşımı |
115
+ | `CYBER_NEWS_CACHE_SECONDS` | `300` | Aynı haber sorgusunun önbellek süresi |
116
+ | `NEWSAPI_KEY` | boş | NewsAPI erişimi |
117
+ | `NVD_API_KEY` | boş | Daha yüksek NVD istek limiti |
118
+ | `OTX_API_KEY` | boş | AlienVault OTX erişimi |
119
+ | `MISP_BASE_URL` | boş | Kurumsal MISP kök adresi |
120
+ | `MISP_API_KEY` | boş | MISP erişim anahtarı |
121
+ | `MISP_VERIFY_TLS` | `true` | MISP TLS sertifika doğrulaması |
122
+
123
+ ## Hugging Face Docker Space
124
+
125
+ Depo, Ollama ile Gradio'yu aynı container içinde çalıştıran Docker Space düzenine
126
+ hazırdır. `start.sh` önce Ollama'yı başlatır, `OLLAMA_MODEL` ile seçilen modeli
127
+ indirir ve ardından Gradio'yu `0.0.0.0:7860` üzerinde açar. Yalnızca Gradio portu
128
+ dışarı açılır; Ollama container içinde `127.0.0.1:11434` adresinde kalır.
129
+
130
+ Space oluşturma ve yükleme örneği:
131
+
132
+ ```bash
133
+ hf auth login --add-to-git-credential
134
+ hf repos create KULLANICI_ADI/siber-guvenlik-asistani --repo-type space --sdk docker
135
+ git remote add space https://huggingface.co/spaces/KULLANICI_ADI/siber-guvenlik-asistani
136
+ git push space main
137
+ ```
138
+
139
+ Space secret değerlerini Hugging Face web arayüzündeki **Settings → Variables and
140
+ secrets** bölümünden ekleyin. Her push sonrasında Space otomatik olarak yeniden
141
+ oluşturulur.
142
+
143
+ Önerilen Space değişkenleri:
144
+
145
+ ```text
146
+ OLLAMA_MODEL=qwen3.6:latest
147
+ OLLAMA_NUM_CTX=16384
148
+ ```
149
+
150
+ `NEWSAPI_KEY`, `NVD_API_KEY`, `OTX_API_KEY` ve MISP erişim bilgilerini yalnızca
151
+ Space secret olarak ekleyin. Bunları Dockerfile'a veya Git deposuna yazmayın.
152
+
153
+ `qwen3.6:latest` yaklaşık 24 GB'tır. Model ağırlıkları ile çalışma belleğine yer
154
+ kalması için 48 GB VRAM'li bir GPU güvenli tercihtir. Daha küçük donanım ve daha
155
+ düşük maliyet için örneğin `OLLAMA_MODEL=qwen3.5:9b` kullanılabilir.
156
+
157
+ Space diski geçiciyse model her yeniden oluşturmada tekrar indirilir. Bir Storage
158
+ Bucket `/data` yoluna bağlanırsa Space değişkenini aşağıdaki gibi ayarlayarak model
159
+ önbelleği kalıcı tutulabilir:
160
+
161
+ ```text
162
+ OLLAMA_MODELS=/data/ollama-models
163
+ ```
164
+
165
+ ### Docker'ı yerelde sınama
166
+
167
+ CPU ile yalnızca container başlangıcını sınamak için:
168
+
169
+ ```bash
170
+ docker build -t siber-guvenlik-asistani .
171
+ docker run --rm -p 7860:7860 \
172
+ -e OLLAMA_MODEL=qwen3.5:4b \
173
+ siber-guvenlik-asistani
174
+ ```
175
+
176
+ NVIDIA Container Toolkit kurulu bir makinede GPU ile:
177
+
178
+ ```bash
179
+ docker run --rm --gpus all -p 7860:7860 \
180
+ -e OLLAMA_MODEL=qwen3.6:latest \
181
+ siber-guvenlik-asistani
182
+ ```
183
+
184
+ ## Testler
185
+
186
+ ```bash
187
+ python -m unittest discover -s tests -v
188
+ ```
189
+
190
+ Testler gerçek API veya Ollama bağlantısı kurmadan araç şemalarını, CVE
191
+ normalizasyonunu ve çok turlu araç döngüsünü doğrular.
192
+
193
+ ## Güvenlik ve doğruluk
194
+
195
+ - Uygulama savunma ve haber analizi amaçlıdır.
196
+ - Güncel veri kaynakları gecikebilir veya geçici olarak hata verebilir.
197
+ - CVSS puanı tek başına aktif istismar kanıtı değildir; KEV durumu ayrıca gösterilir.
198
+ - Kritik operasyonel kararlar özgün CISA, NVD, CERT/CC veya üretici duyurusundan
199
+ doğrulanmalıdır.
app.py ADDED
@@ -0,0 +1,233 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Gradio arayüzü ve Hugging Face Spaces giriş noktası."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ from typing import Any
8
+
9
+ import gradio as gr
10
+
11
+ from ollama import OllamaLocalModelManagement
12
+
13
+
14
+ MODEL = OllamaLocalModelManagement()
15
+
16
+ CSS = """
17
+ .gradio-container { max-width: 1120px !important; }
18
+ .hero { text-align: center; margin: 0 auto 1rem; }
19
+ .hero h1 { margin-bottom: .35rem; }
20
+ .hero p { color: var(--body-text-color-subdued); }
21
+ .status-card { border: 1px solid var(--border-color-primary); border-radius: 12px;
22
+ padding: 12px 16px; background: var(--background-fill-secondary); }
23
+ """
24
+
25
+
26
+ def _json_block(value: Any, max_chars: int = 14_000) -> str:
27
+ text = json.dumps(value, ensure_ascii=False, indent=2, default=str)
28
+ if len(text) > max_chars:
29
+ text = f"{text[:max_chars]}\n… (arayüz için kısaltıldı)"
30
+ return f"```json\n{text}\n```"
31
+
32
+
33
+ def _tool_message(event: dict[str, Any]) -> dict[str, Any]:
34
+ name = event["name"]
35
+ content = (
36
+ f"**Çağrı**\n\n`{name}`\n\n"
37
+ f"**Parametreler**\n\n{_json_block(event['arguments'])}\n\n"
38
+ f"**Sonuç**\n\n{_json_block(event['result'])}"
39
+ )
40
+ return {
41
+ "role": "assistant",
42
+ "content": content,
43
+ "metadata": {
44
+ "title": f"🛠️ Tur {event['turn']} · {name}",
45
+ "duration": event["duration_seconds"],
46
+ "status": "done",
47
+ },
48
+ }
49
+
50
+
51
+ def respond(
52
+ message: str,
53
+ display_history: list[dict[str, Any]] | None,
54
+ conversation: list[dict[str, str]] | None,
55
+ ):
56
+ """Kullanıcı sorusunu işle ve her araç eyleminden sonra arayüzü güncelle."""
57
+ display = list(display_history or [])
58
+ state = list(conversation or [])
59
+ if not message or not message.strip():
60
+ yield "", display, state
61
+ return
62
+
63
+ user_text = message.strip()
64
+ display.append({"role": "user", "content": user_text})
65
+ yield "", display, state
66
+
67
+ final_text: str | None = None
68
+ for event in MODEL.run(user_text, state):
69
+ if event["type"] == "tool":
70
+ display.append(_tool_message(event))
71
+ elif event["type"] == "note":
72
+ display.append(
73
+ {
74
+ "role": "assistant",
75
+ "content": event["content"],
76
+ "metadata": {
77
+ "title": f"ℹ️ Tur {event['turn']} · Model notu",
78
+ "status": "done",
79
+ },
80
+ }
81
+ )
82
+ elif event["type"] == "final":
83
+ final_text = event["content"]
84
+ metrics = event.get("metrics", {})
85
+ metric_text = " · ".join(
86
+ part
87
+ for part in [
88
+ f"girdi: {metrics.get('prompt_tokens')} token"
89
+ if metrics.get("prompt_tokens") is not None
90
+ else "",
91
+ f"çıktı: {metrics.get('response_tokens')} token"
92
+ if metrics.get("response_tokens") is not None
93
+ else "",
94
+ f"{metrics.get('total_duration_seconds')} sn"
95
+ if metrics.get("total_duration_seconds")
96
+ else "",
97
+ ]
98
+ if part
99
+ )
100
+ display.append(
101
+ {
102
+ "role": "assistant",
103
+ "content": final_text,
104
+ "metadata": (
105
+ {"title": f"✅ Nihai yanıt · Tur {event['turn']}", "log": metric_text}
106
+ if metric_text
107
+ else {"title": f"✅ Nihai yanıt · Tur {event['turn']}"}
108
+ ),
109
+ }
110
+ )
111
+ elif event["type"] == "error":
112
+ final_text = f"⚠️ {event['content']}"
113
+ display.append(
114
+ {
115
+ "role": "assistant",
116
+ "content": final_text,
117
+ "metadata": {"title": "Hata", "status": "done"},
118
+ }
119
+ )
120
+ yield "", display, state
121
+
122
+ if final_text:
123
+ state.extend(
124
+ [
125
+ {"role": "user", "content": user_text},
126
+ {"role": "assistant", "content": final_text},
127
+ ]
128
+ )
129
+ state = state[-20:]
130
+ yield "", display, state
131
+
132
+
133
+ def clear_chat():
134
+ return [], []
135
+
136
+
137
+ def configuration_text() -> str:
138
+ optional = {
139
+ "NewsAPI": bool(os.getenv("NEWSAPI_KEY")),
140
+ "NVD API anahtarı": bool(os.getenv("NVD_API_KEY")),
141
+ "AlienVault OTX": bool(os.getenv("OTX_API_KEY")),
142
+ "MISP": bool(os.getenv("MISP_BASE_URL") and os.getenv("MISP_API_KEY")),
143
+ }
144
+ lines = [
145
+ f"**Model:** `{MODEL.model_name}`",
146
+ f"**Ollama:** `{MODEL.base_url}`",
147
+ "**Anahtarsız kaynaklar:** GDELT, CISA KEV, NVD, CISA RSS, CERT/CC, MITRE ATT&CK",
148
+ "**İsteğe bağlı kaynaklar:** "
149
+ + ", ".join(f"{name} {'✅' if enabled else '—'}" for name, enabled in optional.items()),
150
+ ]
151
+ return "\n\n".join(lines)
152
+
153
+
154
+ with gr.Blocks(title="Siber Güvenlik Haber Asistanı") as demo:
155
+ gr.HTML(
156
+ """
157
+ <div class="hero">
158
+ <h1>🛡️ Siber Güvenlik Haber Asistanı</h1>
159
+ <p>Yerel Ollama modeli · Güncel CVE, KEV, haber ve tehdit istihbaratı</p>
160
+ </div>
161
+ """
162
+ )
163
+ conversation_state = gr.State([])
164
+ with gr.Row():
165
+ with gr.Column(scale=4):
166
+ chatbot = gr.Chatbot(
167
+ height=650,
168
+ layout="panel",
169
+ placeholder=(
170
+ "Bir soru sorun. Modelin yaptığı araç çağrıları ve aldığı sonuçlar "
171
+ "burada adım adım görünecek."
172
+ ),
173
+ show_label=False,
174
+ buttons=["copy", "copy_all"],
175
+ )
176
+ with gr.Row():
177
+ prompt = gr.Textbox(
178
+ placeholder="Örn. Son 24 saatteki önemli siber güvenlik haberleri neler?",
179
+ show_label=False,
180
+ lines=2,
181
+ max_lines=6,
182
+ scale=8,
183
+ )
184
+ send = gr.Button("Gönder", variant="primary", scale=1)
185
+ clear = gr.Button("Sohbeti temizle", variant="secondary")
186
+ with gr.Column(scale=1, min_width=260):
187
+ gr.Markdown("### Yapılandırma")
188
+ gr.Markdown(configuration_text(), elem_classes=["status-card"])
189
+ gr.Markdown(
190
+ """
191
+ ### Örnek sorular
192
+
193
+ - Son 24 saatteki önemli fidye yazılımı haberlerini özetle.
194
+ - Son 7 günde yayımlanan kritik CVE'leri bul ve KEV durumlarını kontrol et.
195
+ - CVE-2025-XXXX hakkında ne biliniyor?
196
+ - Credential dumping için MITRE ATT&CK tekniklerini getir.
197
+
198
+ > Kaynak sonuçları güncel olsa da kritik kararları özgün kaynaklardan doğrulayın.
199
+ """
200
+ )
201
+
202
+ submit_event = prompt.submit(
203
+ respond,
204
+ inputs=[prompt, chatbot, conversation_state],
205
+ outputs=[prompt, chatbot, conversation_state],
206
+ api_name="sor",
207
+ )
208
+ send.click(
209
+ respond,
210
+ inputs=[prompt, chatbot, conversation_state],
211
+ outputs=[prompt, chatbot, conversation_state],
212
+ api_name=False,
213
+ )
214
+ clear.click(
215
+ clear_chat,
216
+ outputs=[chatbot, conversation_state],
217
+ api_name=False,
218
+ cancels=[submit_event],
219
+ )
220
+
221
+
222
+ def launch_demo() -> None:
223
+ """Yerel ve Docker Space ortamlarında Gradio sunucusunu başlat."""
224
+ demo.queue(default_concurrency_limit=4).launch(
225
+ css=CSS,
226
+ theme=gr.themes.Soft(),
227
+ server_name=os.getenv("GRADIO_SERVER_NAME", "127.0.0.1"),
228
+ server_port=int(os.getenv("GRADIO_SERVER_PORT", "7860")),
229
+ )
230
+
231
+
232
+ if __name__ == "__main__":
233
+ launch_demo()
main.py ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ """Yerel çalıştırma giriş noktası."""
2
+
3
+ from app import launch_demo
4
+
5
+
6
+ if __name__ == "__main__":
7
+ launch_demo()
ollama/__init__.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ """Yerel Ollama siber güvenlik asistanı."""
2
+
3
+ from .ollama_local_model_managment_code import OllamaLocalModelManagement
4
+
5
+ __all__ = ["OllamaLocalModelManagement"]
ollama/cyber_security_tools.py ADDED
@@ -0,0 +1,673 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Siber güvenlik veri kaynakları ve Ollama araç şemaları."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import re
8
+ import time
9
+ from datetime import UTC, datetime, timedelta
10
+ from typing import Any
11
+ from urllib.parse import urljoin
12
+ from xml.etree import ElementTree
13
+
14
+ import requests
15
+ from requests.adapters import HTTPAdapter
16
+ from urllib3.util.retry import Retry
17
+
18
+
19
+ USER_AGENT = "SiberGuvenlikAsistani/1.0"
20
+ CVE_PATTERN = re.compile(r"^CVE-\d{4}-\d{4,}$", re.IGNORECASE)
21
+
22
+
23
+ def _clamp(value: int, lower: int = 1, upper: int = 50) -> int:
24
+ return max(lower, min(int(value), upper))
25
+
26
+
27
+ def _compact(value: Any, limit: int = 700) -> str:
28
+ text = re.sub(r"\s+", " ", str(value or "")).strip()
29
+ return text if len(text) <= limit else f"{text[: limit - 1]}…"
30
+
31
+
32
+ class CyberSecurityTools:
33
+ """Harici kaynakları küçük, modele uygun JSON sonuçlarına dönüştürür."""
34
+
35
+ def __init__(self, session: requests.Session | None = None) -> None:
36
+ self.session = session or requests.Session()
37
+ self.session.headers.update({"User-Agent": USER_AGENT})
38
+ retry = Retry(
39
+ total=2,
40
+ connect=2,
41
+ read=1,
42
+ status=2,
43
+ backoff_factor=3,
44
+ status_forcelist=(429, 500, 502, 503, 504),
45
+ allowed_methods=frozenset({"GET"}),
46
+ respect_retry_after_header=True,
47
+ raise_on_status=False,
48
+ )
49
+ self.session.mount("https://", HTTPAdapter(max_retries=retry))
50
+ self.session.mount("http://", HTTPAdapter(max_retries=retry))
51
+ self._news_cache: dict[tuple[str, int, int], tuple[float, dict[str, Any]]] = {}
52
+
53
+ @property
54
+ def schemas(self) -> list[dict[str, Any]]:
55
+ return [
56
+ self._schema(
57
+ "siber_haberlerini_ara",
58
+ "GDELT ve yapılandırılmışsa NewsAPI üzerinde güncel "
59
+ "siber güvenlik haberlerini ara.",
60
+ {
61
+ "sorgu": {
62
+ "type": "string",
63
+ "description": "Örn. ransomware, veri ihlali veya belirli bir şirket.",
64
+ },
65
+ "son_saat": {
66
+ "type": "integer",
67
+ "description": "Kaç saat geriye gidileceği (1-168).",
68
+ "default": 24,
69
+ },
70
+ "sonuc_sayisi": {
71
+ "type": "integer",
72
+ "description": "Döndürülecek en fazla sonuç (1-20).",
73
+ "default": 10,
74
+ },
75
+ },
76
+ ["sorgu"],
77
+ ),
78
+ self._schema(
79
+ "son_cve_kayitlarini_getir",
80
+ "NVD'den yakın zamanda yayımlanan CVE kayıtlarını getir; "
81
+ "anahtar kelimeyle daraltılabilir.",
82
+ {
83
+ "son_gun": {
84
+ "type": "integer",
85
+ "description": "Kaç gün geriye gidileceği (1-120).",
86
+ "default": 7,
87
+ },
88
+ "anahtar_kelime": {
89
+ "type": "string",
90
+ "description": "İsteğe bağlı ürün, üretici veya zafiyet anahtar kelimesi.",
91
+ "default": "",
92
+ },
93
+ "sonuc_sayisi": {
94
+ "type": "integer",
95
+ "description": "Döndürülecek en fazla sonuç (1-20).",
96
+ "default": 10,
97
+ },
98
+ },
99
+ ),
100
+ self._schema(
101
+ "cve_detayi_getir",
102
+ "Bir CVE'yi NVD, CISA KEV ve yapılandırılmışsa OTX verileriyle ayrıntılandır.",
103
+ {
104
+ "cve_id": {
105
+ "type": "string",
106
+ "description": "CVE-2025-12345 biçimindeki kimlik.",
107
+ }
108
+ },
109
+ ["cve_id"],
110
+ ),
111
+ self._schema(
112
+ "aktif_istismar_edilenleri_ara",
113
+ "CISA Known Exploited Vulnerabilities (KEV) kataloğunda "
114
+ "aktif istismar edilen CVE'leri ara.",
115
+ {
116
+ "sorgu": {
117
+ "type": "string",
118
+ "description": "CVE, üretici veya ürün; boş bırakılırsa en yeni kayıtlar.",
119
+ "default": "",
120
+ },
121
+ "sonuc_sayisi": {
122
+ "type": "integer",
123
+ "description": "Döndürülecek en fazla sonuç (1-20).",
124
+ "default": 10,
125
+ },
126
+ },
127
+ ),
128
+ self._schema(
129
+ "guvenlik_duyurularini_getir",
130
+ "CISA ve CERT/CC kaynaklarındaki en güncel güvenlik duyurularını getir.",
131
+ {
132
+ "sonuc_sayisi": {
133
+ "type": "integer",
134
+ "description": "Her kaynaktan alınacak en fazla sonuç (1-20).",
135
+ "default": 8,
136
+ }
137
+ },
138
+ ),
139
+ self._schema(
140
+ "tehdit_istihbarati_ara",
141
+ "Yapılandırılmış AlienVault OTX ve MISP kaynaklarında tehdit istihbaratı ara.",
142
+ {
143
+ "sorgu": {
144
+ "type": "string",
145
+ "description": (
146
+ "Tehdit aktörü, zararlı yazılım, CVE, alan adı veya kampanya."
147
+ ),
148
+ },
149
+ "sonuc_sayisi": {
150
+ "type": "integer",
151
+ "description": "Döndürülecek en fazla sonuç (1-20).",
152
+ "default": 10,
153
+ },
154
+ },
155
+ ["sorgu"],
156
+ ),
157
+ self._schema(
158
+ "mitre_attack_tekniklerini_ara",
159
+ "MITRE ATT&CK Enterprise koleksiyonunda teknik adı, "
160
+ "açıklaması veya kimliğiyle ara.",
161
+ {
162
+ "sorgu": {
163
+ "type": "string",
164
+ "description": "Örn. phishing, credential dumping veya T1059.",
165
+ },
166
+ "sonuc_sayisi": {
167
+ "type": "integer",
168
+ "description": "Döndürülecek en fazla sonuç (1-20).",
169
+ "default": 10,
170
+ },
171
+ },
172
+ ["sorgu"],
173
+ ),
174
+ ]
175
+
176
+ @property
177
+ def available_functions(self) -> dict[str, Any]:
178
+ return {
179
+ "siber_haberlerini_ara": self.siber_haberlerini_ara,
180
+ "son_cve_kayitlarini_getir": self.son_cve_kayitlarini_getir,
181
+ "cve_detayi_getir": self.cve_detayi_getir,
182
+ "aktif_istismar_edilenleri_ara": self.aktif_istismar_edilenleri_ara,
183
+ "guvenlik_duyurularini_getir": self.guvenlik_duyurularini_getir,
184
+ "tehdit_istihbarati_ara": self.tehdit_istihbarati_ara,
185
+ "mitre_attack_tekniklerini_ara": self.mitre_attack_tekniklerini_ara,
186
+ }
187
+
188
+ @staticmethod
189
+ def _schema(
190
+ name: str,
191
+ description: str,
192
+ properties: dict[str, Any],
193
+ required: list[str] | None = None,
194
+ ) -> dict[str, Any]:
195
+ return {
196
+ "type": "function",
197
+ "function": {
198
+ "name": name,
199
+ "description": description,
200
+ "parameters": {
201
+ "type": "object",
202
+ "properties": properties,
203
+ "required": required or [],
204
+ },
205
+ },
206
+ }
207
+
208
+ @staticmethod
209
+ def _default_timeout() -> tuple[int, int]:
210
+ return (
211
+ int(os.getenv("CYBER_API_CONNECT_TIMEOUT", "20")),
212
+ int(os.getenv("CYBER_API_READ_TIMEOUT", "60")),
213
+ )
214
+
215
+ def _get(
216
+ self, url: str, timeout: tuple[int, int] | None = None, **kwargs: Any
217
+ ) -> requests.Response:
218
+ response = self.session.get(
219
+ url, timeout=timeout or self._default_timeout(), **kwargs
220
+ )
221
+ response.raise_for_status()
222
+ return response
223
+
224
+ def _post(self, url: str, **kwargs: Any) -> requests.Response:
225
+ response = self.session.post(
226
+ url, timeout=self._default_timeout(), **kwargs
227
+ )
228
+ response.raise_for_status()
229
+ return response
230
+
231
+ @staticmethod
232
+ def _error(source: str, exc: Exception) -> dict[str, Any]:
233
+ return {"kaynak": source, "hata": _compact(exc, 300)}
234
+
235
+ def siber_haberlerini_ara(
236
+ self, sorgu: str, son_saat: int = 24, sonuc_sayisi: int = 10
237
+ ) -> dict[str, Any]:
238
+ limit = _clamp(sonuc_sayisi, upper=20)
239
+ hours = _clamp(son_saat, upper=168)
240
+ query = sorgu.strip() or "cybersecurity"
241
+ cache_key = (query.casefold(), hours, limit)
242
+ cached = self._news_cache.get(cache_key)
243
+ cache_seconds = int(os.getenv("CYBER_NEWS_CACHE_SECONDS", "300"))
244
+ if cached and time.monotonic() - cached[0] < cache_seconds:
245
+ return {**cached[1], "onbellekten": True}
246
+
247
+ results: list[dict[str, Any]] = []
248
+ errors: list[dict[str, Any]] = []
249
+
250
+ try:
251
+ data = self._get(
252
+ "https://api.gdeltproject.org/api/v2/doc/doc",
253
+ timeout=(
254
+ int(os.getenv("GDELT_CONNECT_TIMEOUT", "30")),
255
+ int(os.getenv("GDELT_READ_TIMEOUT", "75")),
256
+ ),
257
+ params={
258
+ "query": query,
259
+ "mode": "ArtList",
260
+ "format": "json",
261
+ "maxrecords": limit,
262
+ "sort": "DateDesc",
263
+ "timespan": f"{hours}h",
264
+ },
265
+ ).json()
266
+ for article in data.get("articles", [])[:limit]:
267
+ results.append(
268
+ {
269
+ "kaynak": "GDELT",
270
+ "baslik": _compact(article.get("title"), 250),
271
+ "yayinci": article.get("domain"),
272
+ "tarih": article.get("seendate"),
273
+ "dil": article.get("language"),
274
+ "url": article.get("url"),
275
+ }
276
+ )
277
+ except (requests.RequestException, ValueError) as exc:
278
+ errors.append(self._error("GDELT", exc))
279
+
280
+ news_key = os.getenv("NEWSAPI_KEY")
281
+ if news_key:
282
+ try:
283
+ data = self._get(
284
+ "https://newsapi.org/v2/everything",
285
+ headers={"X-Api-Key": news_key},
286
+ params={
287
+ "q": query,
288
+ "language": "en",
289
+ "sortBy": "publishedAt",
290
+ "pageSize": limit,
291
+ },
292
+ ).json()
293
+ if data.get("status") == "error":
294
+ raise ValueError(data.get("message", "NewsAPI hatası"))
295
+ for article in data.get("articles", [])[:limit]:
296
+ results.append(
297
+ {
298
+ "kaynak": "NewsAPI",
299
+ "baslik": _compact(article.get("title"), 250),
300
+ "yayinci": (article.get("source") or {}).get("name"),
301
+ "tarih": article.get("publishedAt"),
302
+ "ozet": _compact(article.get("description"), 400),
303
+ "url": article.get("url"),
304
+ }
305
+ )
306
+ except (requests.RequestException, ValueError) as exc:
307
+ errors.append(self._error("NewsAPI", exc))
308
+
309
+ results.sort(key=lambda item: item.get("tarih") or "", reverse=True)
310
+ response_data = {
311
+ "sorgu": query,
312
+ "zaman_araligi_saat": hours,
313
+ "sonuclar": results[:limit],
314
+ "hatalar": errors,
315
+ "not": None if news_key else "NEWSAPI_KEY tanımlı olmadığı için NewsAPI atlandı.",
316
+ }
317
+ if not results:
318
+ response_data["alternatif_resmi_duyurular"] = self.guvenlik_duyurularini_getir(
319
+ min(limit, 8)
320
+ )
321
+ response_data["uyari"] = (
322
+ "Haber kaynakları sonuç vermedi. Aşağıdaki kayıtlar haber değil, "
323
+ "CISA ve CERT/CC resmi güvenlik duyurularıdır."
324
+ )
325
+ else:
326
+ self._news_cache[cache_key] = (time.monotonic(), response_data)
327
+ return response_data
328
+
329
+ def son_cve_kayitlarini_getir(
330
+ self, son_gun: int = 7, anahtar_kelime: str = "", sonuc_sayisi: int = 10
331
+ ) -> dict[str, Any]:
332
+ days = _clamp(son_gun, upper=120)
333
+ limit = _clamp(sonuc_sayisi, upper=20)
334
+ end = datetime.now(UTC)
335
+ start = end - timedelta(days=days)
336
+ params: dict[str, Any] = {
337
+ "pubStartDate": start.isoformat(timespec="milliseconds").replace("+00:00", "Z"),
338
+ "pubEndDate": end.isoformat(timespec="milliseconds").replace("+00:00", "Z"),
339
+ "resultsPerPage": limit,
340
+ }
341
+ if anahtar_kelime.strip():
342
+ params["keywordSearch"] = anahtar_kelime.strip()
343
+ try:
344
+ data = self._get(
345
+ "https://services.nvd.nist.gov/rest/json/cves/2.0",
346
+ headers=self._nvd_headers(),
347
+ params=params,
348
+ ).json()
349
+ return {
350
+ "kaynak": "NVD",
351
+ "sorgu_zamani_utc": end.isoformat(),
352
+ "toplam_eslesme": data.get("totalResults", 0),
353
+ "sonuclar": [
354
+ self._normalize_nvd(item.get("cve", {}))
355
+ for item in data.get("vulnerabilities", [])[:limit]
356
+ ],
357
+ }
358
+ except (requests.RequestException, ValueError) as exc:
359
+ return self._error("NVD", exc)
360
+
361
+ def cve_detayi_getir(self, cve_id: str) -> dict[str, Any]:
362
+ cve = cve_id.strip().upper()
363
+ if not CVE_PATTERN.fullmatch(cve):
364
+ return {"hata": "Geçersiz CVE kimliği.", "beklenen_bicim": "CVE-YYYY-NNNN"}
365
+ result: dict[str, Any] = {"cve_id": cve}
366
+ try:
367
+ data = self._get(
368
+ "https://services.nvd.nist.gov/rest/json/cves/2.0",
369
+ headers=self._nvd_headers(),
370
+ params={"cveId": cve},
371
+ ).json()
372
+ items = data.get("vulnerabilities", [])
373
+ result["nvd"] = (
374
+ self._normalize_nvd(items[0].get("cve", {}))
375
+ if items
376
+ else {"bulundu": False}
377
+ )
378
+ except (requests.RequestException, ValueError) as exc:
379
+ result["nvd"] = self._error("NVD", exc)
380
+
381
+ kev = self.aktif_istismar_edilenleri_ara(cve, 1)
382
+ result["cisa_kev"] = (kev.get("sonuclar") or [{"bulundu": False}])[0]
383
+
384
+ otx_key = os.getenv("OTX_API_KEY")
385
+ if otx_key:
386
+ try:
387
+ data = self._get(
388
+ f"https://otx.alienvault.com/api/v1/indicators/cve/{cve}/general",
389
+ headers={"X-OTX-API-KEY": otx_key},
390
+ ).json()
391
+ result["otx"] = {
392
+ "pulse_sayisi": (data.get("pulse_info") or {}).get("count", 0),
393
+ "pulses": [
394
+ {
395
+ "ad": _compact(pulse.get("name"), 250),
396
+ "olusturulma": pulse.get("created"),
397
+ "degistirilme": pulse.get("modified"),
398
+ "etiketler": pulse.get("tags", [])[:12],
399
+ }
400
+ for pulse in (data.get("pulse_info") or {}).get("pulses", [])[:5]
401
+ ],
402
+ }
403
+ except (requests.RequestException, ValueError) as exc:
404
+ result["otx"] = self._error("AlienVault OTX", exc)
405
+ else:
406
+ result["otx"] = {"not": "OTX_API_KEY tanımlı değil; OTX sorgusu atlandı."}
407
+ return result
408
+
409
+ def aktif_istismar_edilenleri_ara(
410
+ self, sorgu: str = "", sonuc_sayisi: int = 10
411
+ ) -> dict[str, Any]:
412
+ limit = _clamp(sonuc_sayisi, upper=20)
413
+ try:
414
+ data = self._get(
415
+ "https://www.cisa.gov/sites/default/files/feeds/"
416
+ "known_exploited_vulnerabilities.json"
417
+ ).json()
418
+ needle = sorgu.casefold().strip()
419
+ matches = []
420
+ for item in data.get("vulnerabilities", []):
421
+ haystack = " ".join(
422
+ str(item.get(key, ""))
423
+ for key in ("cveID", "vendorProject", "product", "vulnerabilityName")
424
+ ).casefold()
425
+ if needle and needle not in haystack:
426
+ continue
427
+ matches.append(
428
+ {
429
+ "cve_id": item.get("cveID"),
430
+ "uretici": item.get("vendorProject"),
431
+ "urun": item.get("product"),
432
+ "zafiyet": item.get("vulnerabilityName"),
433
+ "kataloga_eklenme": item.get("dateAdded"),
434
+ "aksiyon_son_tarihi": item.get("dueDate"),
435
+ "bilinen_fidye_yazilimi_kullanimi": item.get(
436
+ "knownRansomwareCampaignUse"
437
+ ),
438
+ "gerekli_aksiyon": _compact(item.get("requiredAction"), 400),
439
+ "notlar": _compact(item.get("notes"), 400),
440
+ }
441
+ )
442
+ matches.sort(key=lambda item: item.get("kataloga_eklenme") or "", reverse=True)
443
+ return {
444
+ "kaynak": "CISA KEV",
445
+ "katalog_surumu": data.get("version"),
446
+ "katalog_tarihi": data.get("dateReleased"),
447
+ "sonuclar": matches[:limit],
448
+ }
449
+ except (requests.RequestException, ValueError) as exc:
450
+ return self._error("CISA KEV", exc)
451
+
452
+ def guvenlik_duyurularini_getir(self, sonuc_sayisi: int = 8) -> dict[str, Any]:
453
+ limit = _clamp(sonuc_sayisi, upper=20)
454
+ sources = [
455
+ ("CISA", "https://www.cisa.gov/cybersecurity-advisories/all.xml"),
456
+ ("CERT/CC", "https://www.kb.cert.org/vuls/atomfeed/"),
457
+ ]
458
+ results: list[dict[str, Any]] = []
459
+ errors: list[dict[str, Any]] = []
460
+ for source, url in sources:
461
+ try:
462
+ root = ElementTree.fromstring(self._get(url).content)
463
+ entries = root.findall(".//item") or root.findall(
464
+ ".//{http://www.w3.org/2005/Atom}entry"
465
+ )
466
+ for entry in entries[:limit]:
467
+ title = self._xml_text(entry, "title")
468
+ published = (
469
+ self._xml_text(entry, "pubDate")
470
+ or self._xml_text(entry, "updated")
471
+ or self._xml_text(entry, "published")
472
+ )
473
+ link = self._xml_text(entry, "link")
474
+ if not link:
475
+ link_node = entry.find("{http://www.w3.org/2005/Atom}link")
476
+ link = link_node.get("href") if link_node is not None else None
477
+ results.append(
478
+ {
479
+ "kaynak": source,
480
+ "baslik": _compact(title, 250),
481
+ "tarih": published,
482
+ "url": link,
483
+ }
484
+ )
485
+ except (requests.RequestException, ElementTree.ParseError) as exc:
486
+ errors.append(self._error(source, exc))
487
+ return {"sonuclar": results, "hatalar": errors}
488
+
489
+ def tehdit_istihbarati_ara(
490
+ self, sorgu: str, sonuc_sayisi: int = 10
491
+ ) -> dict[str, Any]:
492
+ query = sorgu.strip()
493
+ if not query:
494
+ return {"hata": "Sorgu boş olamaz."}
495
+ limit = _clamp(sonuc_sayisi, upper=20)
496
+ results: dict[str, Any] = {}
497
+
498
+ otx_key = os.getenv("OTX_API_KEY")
499
+ if otx_key:
500
+ try:
501
+ data = self._get(
502
+ "https://otx.alienvault.com/api/v1/search/pulses",
503
+ headers={"X-OTX-API-KEY": otx_key},
504
+ params={"q": query, "sort": "modified", "limit": limit},
505
+ ).json()
506
+ results["otx"] = [
507
+ {
508
+ "id": item.get("id"),
509
+ "ad": _compact(item.get("name"), 250),
510
+ "aciklama": _compact(item.get("description"), 500),
511
+ "degistirilme": item.get("modified"),
512
+ "etiketler": item.get("tags", [])[:12],
513
+ }
514
+ for item in data.get("results", [])[:limit]
515
+ ]
516
+ except (requests.RequestException, ValueError) as exc:
517
+ results["otx"] = self._error("AlienVault OTX", exc)
518
+ else:
519
+ results["otx"] = {"not": "OTX_API_KEY tanımlı değil."}
520
+
521
+ misp_url, misp_key = os.getenv("MISP_BASE_URL"), os.getenv("MISP_API_KEY")
522
+ if misp_url and misp_key:
523
+ try:
524
+ data = self._post(
525
+ urljoin(misp_url.rstrip("/") + "/", "events/restSearch"),
526
+ headers={
527
+ "Authorization": misp_key,
528
+ "Accept": "application/json",
529
+ "Content-Type": "application/json",
530
+ },
531
+ json={
532
+ "returnFormat": "json",
533
+ "searchall": query,
534
+ "published": True,
535
+ "limit": limit,
536
+ "page": 1,
537
+ },
538
+ verify=os.getenv("MISP_VERIFY_TLS", "true").lower() != "false",
539
+ ).json()
540
+ events = data.get("response", data if isinstance(data, list) else [])
541
+ results["misp"] = [
542
+ {
543
+ "id": (item.get("Event") or item).get("id"),
544
+ "bilgi": _compact((item.get("Event") or item).get("info"), 350),
545
+ "tarih": (item.get("Event") or item).get("date"),
546
+ "tehdit_seviyesi": (item.get("Event") or item).get(
547
+ "threat_level_id"
548
+ ),
549
+ }
550
+ for item in events[:limit]
551
+ ]
552
+ except (requests.RequestException, ValueError) as exc:
553
+ results["misp"] = self._error("MISP", exc)
554
+ else:
555
+ results["misp"] = {
556
+ "not": "MISP_BASE_URL ve MISP_API_KEY birlikte tanımlı değil."
557
+ }
558
+ return {"sorgu": query, "kaynaklar": results}
559
+
560
+ def mitre_attack_tekniklerini_ara(
561
+ self, sorgu: str, sonuc_sayisi: int = 10
562
+ ) -> dict[str, Any]:
563
+ query = sorgu.casefold().strip()
564
+ if not query:
565
+ return {"hata": "Sorgu boş olamaz."}
566
+ limit = _clamp(sonuc_sayisi, upper=20)
567
+ url = (
568
+ "https://attack-taxii.mitre.org/api/v21/collections/"
569
+ "x-mitre-collection--1f5f1533-f617-4ca8-9ab4-6a02367fa019/objects"
570
+ )
571
+ try:
572
+ data = self._get(
573
+ url,
574
+ headers={"Accept": "application/taxii+json;version=2.1"},
575
+ params={"match[type]": "attack-pattern"},
576
+ ).json()
577
+ matches = []
578
+ for item in data.get("objects", []):
579
+ external = (item.get("external_references") or [{}])[0]
580
+ haystack = " ".join(
581
+ [
582
+ str(item.get("name", "")),
583
+ str(item.get("description", "")),
584
+ str(external.get("external_id", "")),
585
+ ]
586
+ ).casefold()
587
+ if query not in haystack:
588
+ continue
589
+ matches.append(
590
+ {
591
+ "attack_id": external.get("external_id"),
592
+ "ad": item.get("name"),
593
+ "aciklama": _compact(item.get("description"), 600),
594
+ "platformlar": item.get("x_mitre_platforms", []),
595
+ "url": external.get("url"),
596
+ }
597
+ )
598
+ if len(matches) >= limit:
599
+ break
600
+ return {"kaynak": "MITRE ATT&CK TAXII 2.1", "sonuclar": matches}
601
+ except (requests.RequestException, ValueError) as exc:
602
+ return self._error("MITRE ATT&CK", exc)
603
+
604
+ @staticmethod
605
+ def _xml_text(entry: ElementTree.Element, name: str) -> str | None:
606
+ node = entry.find(name)
607
+ if node is None:
608
+ node = entry.find(f"{{http://www.w3.org/2005/Atom}}{name}")
609
+ return node.text.strip() if node is not None and node.text else None
610
+
611
+ @staticmethod
612
+ def _nvd_headers() -> dict[str, str]:
613
+ key = os.getenv("NVD_API_KEY")
614
+ return {"apiKey": key} if key else {}
615
+
616
+ @staticmethod
617
+ def _normalize_nvd(cve: dict[str, Any]) -> dict[str, Any]:
618
+ descriptions = cve.get("descriptions", [])
619
+ description = next(
620
+ (item.get("value") for item in descriptions if item.get("lang") == "en"),
621
+ descriptions[0].get("value") if descriptions else "",
622
+ )
623
+ metrics = cve.get("metrics", {})
624
+ metric = next(
625
+ (
626
+ entries[0]
627
+ for name in ("cvssMetricV40", "cvssMetricV31", "cvssMetricV30", "cvssMetricV2")
628
+ if (entries := metrics.get(name))
629
+ ),
630
+ {},
631
+ )
632
+ cvss = metric.get("cvssData", {})
633
+ weaknesses = [
634
+ desc.get("value")
635
+ for weakness in cve.get("weaknesses", [])
636
+ for desc in weakness.get("description", [])
637
+ if desc.get("lang") == "en"
638
+ ]
639
+ references = [
640
+ {"url": ref.get("url"), "etiketler": ref.get("tags", [])}
641
+ for ref in cve.get("references", [])[:8]
642
+ ]
643
+ return {
644
+ "cve_id": cve.get("id"),
645
+ "yayim_tarihi": cve.get("published"),
646
+ "degistirilme_tarihi": cve.get("lastModified"),
647
+ "durum": cve.get("vulnStatus"),
648
+ "aciklama": _compact(description, 900),
649
+ "cvss_puani": cvss.get("baseScore"),
650
+ "cvss_seviyesi": cvss.get("baseSeverity") or metric.get("baseSeverity"),
651
+ "cvss_vektoru": cvss.get("vectorString"),
652
+ "cwe": list(dict.fromkeys(filter(None, weaknesses)))[:8],
653
+ "referanslar": references,
654
+ "nvd_url": f"https://nvd.nist.gov/vuln/detail/{cve.get('id')}",
655
+ }
656
+
657
+ def execute(self, name: str, arguments: dict[str, Any]) -> dict[str, Any]:
658
+ function = self.available_functions.get(name)
659
+ if not function:
660
+ return {"hata": f"Bilinmeyen araç: {name}"}
661
+ try:
662
+ return function(**arguments)
663
+ except TypeError as exc:
664
+ return {"hata": "Araç parametreleri geçersiz.", "ayrinti": _compact(exc, 300)}
665
+ except Exception as exc: # Aracın bir hatası ajan döngüsünü sonlandırmamalı.
666
+ return {
667
+ "hata": "Araç çalıştırılırken beklenmeyen hata oluştu.",
668
+ "ayrinti": _compact(exc, 300),
669
+ }
670
+
671
+ @staticmethod
672
+ def to_json(result: dict[str, Any]) -> str:
673
+ return json.dumps(result, ensure_ascii=False, default=str)
ollama/ollama_local_model_managment_code.py ADDED
@@ -0,0 +1,234 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Ollama üzerinde çok adımlı, araç kullanan siber güvenlik asistanı."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import time
8
+ from copy import deepcopy
9
+ from collections.abc import Generator
10
+ from datetime import UTC, datetime
11
+ from typing import Any
12
+
13
+ import requests
14
+
15
+ from .cyber_security_tools import CyberSecurityTools
16
+
17
+
18
+ SYSTEM_PROMPT = """Sen güncel siber güvenlik haberleri ve zafiyetleri konusunda çalışan,
19
+ Türkçe yanıt veren bir analiz asistanısın.
20
+
21
+ Kurallar:
22
+ 1. Güncellik içeren sorularda belleğine güvenme; uygun aracı mutlaka kullan.
23
+ 2. Gerekirse birden fazla aracı ve birden fazla turu kullan. Örneğin önce son CVE'leri
24
+ bul, sonra önemli bir CVE için ayrıntı aracını çağır.
25
+ 3. Araç sonucu olmayan güncel bir iddiayı kesin bilgi gibi sunma.
26
+ 4. Kaynakların tarihini, kaynak adını ve varsa doğrudan URL'sini nihai yanıtta belirt.
27
+ 5. CVSS puanı ile aktif istismarı birbirine karıştırma. CISA KEV kaydı aktif istismar
28
+ açısından daha güçlü bir göstergedir.
29
+ 6. Veri bulunamazsa veya kaynak hata verirse bunu açıkça söyle; bilgi uydurma.
30
+ 7. Yanıtı Türkçe, öz ve okunaklı yaz. CVE kimliklerini aynen koru.
31
+ 8. Kullanıcı savunma ve risk azaltma önerileri isterse uygulanabilir öneriler sun.
32
+ 9. Zararlı veya yetkisiz eylemleri kolaylaştıran operasyonel talimatlar verme.
33
+ 10. Araç çağrılarının kullanıcı arayüzünde ayrıca gösterildiğini bil; nihai cevapta
34
+ ham JSON'u tekrar etme, bulguları sentezle.
35
+ """
36
+
37
+
38
+ class OllamaConnectionError(RuntimeError):
39
+ """Ollama sunucusuna erişilemediğinde kullanılır."""
40
+
41
+
42
+ class OllamaLocalModelManagement:
43
+ """Ollama `/api/chat` uç noktası için sınırlı bir ajan döngüsü."""
44
+
45
+ def __init__(
46
+ self,
47
+ model_name: str | None = None,
48
+ base_url: str | None = None,
49
+ tools: CyberSecurityTools | None = None,
50
+ session: requests.Session | None = None,
51
+ max_tool_rounds: int = 6,
52
+ ) -> None:
53
+ self.model_name = model_name or os.getenv("OLLAMA_MODEL", "qwen3.6:latest")
54
+ self.base_url = (base_url or os.getenv("OLLAMA_BASE_URL", "http://127.0.0.1:11434")).rstrip(
55
+ "/"
56
+ )
57
+ self.session = session or requests.Session()
58
+ self.tools = tools or CyberSecurityTools()
59
+ self.max_tool_rounds = max(1, min(max_tool_rounds, 10))
60
+
61
+ def _system_message(self) -> dict[str, str]:
62
+ now = datetime.now(UTC).isoformat(timespec="seconds")
63
+ return {
64
+ "role": "system",
65
+ "content": f"{SYSTEM_PROMPT}\nŞu anki UTC zaman: {now}",
66
+ }
67
+
68
+ def _chat_request(self, messages: list[dict[str, Any]]) -> dict[str, Any]:
69
+ payload = {
70
+ "model": self.model_name,
71
+ # Ajan döngüsü `messages` listesini daha sonra büyütür; istek anındaki
72
+ # denetlenebilir görüntüyü korumak için kopyala.
73
+ "messages": deepcopy(messages),
74
+ "tools": self.tools.schemas,
75
+ "stream": False,
76
+ "think": False,
77
+ "options": {
78
+ "temperature": float(os.getenv("OLLAMA_TEMPERATURE", "0.2")),
79
+ "num_ctx": int(os.getenv("OLLAMA_NUM_CTX", "16384")),
80
+ },
81
+ "keep_alive": os.getenv("OLLAMA_KEEP_ALIVE", "10m"),
82
+ }
83
+ headers = {}
84
+ if api_key := os.getenv("OLLAMA_API_KEY"):
85
+ headers["Authorization"] = f"Bearer {api_key}"
86
+ try:
87
+ response = self.session.post(
88
+ f"{self.base_url}/api/chat",
89
+ json=payload,
90
+ headers=headers,
91
+ timeout=(10, int(os.getenv("OLLAMA_TIMEOUT_SECONDS", "300"))),
92
+ )
93
+ response.raise_for_status()
94
+ data = response.json()
95
+ except requests.ConnectionError as exc:
96
+ raise OllamaConnectionError(
97
+ f"Ollama sunucusuna ulaşılamadı ({self.base_url}). "
98
+ "`ollama serve` komutuyla sunucuyu başlatın."
99
+ ) from exc
100
+ except requests.Timeout as exc:
101
+ raise OllamaConnectionError("Ollama yanıt süresini aştı.") from exc
102
+ except (requests.HTTPError, ValueError) as exc:
103
+ detail = ""
104
+ if "response" in locals():
105
+ detail = response.text[:500]
106
+ raise OllamaConnectionError(
107
+ f"Ollama isteği başarısız oldu: {detail or exc}"
108
+ ) from exc
109
+ if not isinstance(data.get("message"), dict):
110
+ raise OllamaConnectionError("Ollama geçerli bir `message` alanı döndürmedi.")
111
+ return data
112
+
113
+ @staticmethod
114
+ def _conversation_messages(history: list[dict[str, Any]] | None) -> list[dict[str, str]]:
115
+ clean: list[dict[str, str]] = []
116
+ for item in history or []:
117
+ role, content = item.get("role"), item.get("content")
118
+ if role in {"user", "assistant"} and isinstance(content, str) and content.strip():
119
+ clean.append({"role": role, "content": content})
120
+ return clean[-20:]
121
+
122
+ def run(
123
+ self, user_message: str, history: list[dict[str, Any]] | None = None
124
+ ) -> Generator[dict[str, Any], None, None]:
125
+ """Araç ve nihai yanıt olaylarını sırayla üretir."""
126
+ if not user_message or not user_message.strip():
127
+ yield {"type": "error", "content": "Lütfen bir soru yazın."}
128
+ return
129
+
130
+ messages: list[dict[str, Any]] = [
131
+ self._system_message(),
132
+ *self._conversation_messages(history),
133
+ {"role": "user", "content": user_message.strip()},
134
+ ]
135
+
136
+ for turn in range(1, self.max_tool_rounds + 2):
137
+ try:
138
+ response = self._chat_request(messages)
139
+ except OllamaConnectionError as exc:
140
+ yield {"type": "error", "content": str(exc)}
141
+ return
142
+
143
+ assistant_message = response["message"]
144
+ messages.append(assistant_message)
145
+ tool_calls = assistant_message.get("tool_calls") or []
146
+
147
+ if not tool_calls:
148
+ content = (assistant_message.get("content") or "").strip()
149
+ if not content:
150
+ content = "Model boş bir yanıt döndürdü."
151
+ yield {
152
+ "type": "final",
153
+ "turn": turn,
154
+ "content": content,
155
+ "metrics": {
156
+ "prompt_tokens": response.get("prompt_eval_count"),
157
+ "response_tokens": response.get("eval_count"),
158
+ "total_duration_seconds": round(
159
+ (response.get("total_duration") or 0) / 1_000_000_000, 2
160
+ ),
161
+ },
162
+ }
163
+ return
164
+
165
+ if turn > self.max_tool_rounds:
166
+ yield {
167
+ "type": "error",
168
+ "content": (
169
+ f"Güvenlik sınırı nedeniyle {self.max_tool_rounds} araç turundan "
170
+ "sonra işlem durduruldu."
171
+ ),
172
+ }
173
+ return
174
+
175
+ visible_note = (assistant_message.get("content") or "").strip()
176
+ if visible_note:
177
+ yield {"type": "note", "turn": turn, "content": visible_note}
178
+
179
+ for index, call in enumerate(tool_calls, start=1):
180
+ function = call.get("function") or {}
181
+ name = function.get("name", "")
182
+ arguments = function.get("arguments") or {}
183
+ if isinstance(arguments, str):
184
+ try:
185
+ arguments = json.loads(arguments)
186
+ except json.JSONDecodeError:
187
+ arguments = {"_gecersiz_ham_parametre": arguments}
188
+ if not isinstance(arguments, dict):
189
+ arguments = {"_gecersiz_parametre": arguments}
190
+
191
+ started = time.monotonic()
192
+ result = self.tools.execute(name, arguments)
193
+ duration = round(time.monotonic() - started, 2)
194
+ messages.append(
195
+ {
196
+ "role": "tool",
197
+ "tool_name": name,
198
+ "content": self.tools.to_json(result),
199
+ }
200
+ )
201
+ yield {
202
+ "type": "tool",
203
+ "turn": turn,
204
+ "index": index,
205
+ "name": name,
206
+ "arguments": arguments,
207
+ "result": result,
208
+ "duration_seconds": duration,
209
+ }
210
+
211
+ yield {"type": "error", "content": "Beklenmeyen ajan döngüsü sonu."}
212
+
213
+ def health(self) -> dict[str, Any]:
214
+ try:
215
+ headers = {}
216
+ if api_key := os.getenv("OLLAMA_API_KEY"):
217
+ headers["Authorization"] = f"Bearer {api_key}"
218
+ response = self.session.get(
219
+ f"{self.base_url}/api/tags", headers=headers, timeout=(3, 10)
220
+ )
221
+ response.raise_for_status()
222
+ models = [item.get("name") for item in response.json().get("models", [])]
223
+ return {
224
+ "ok": self.model_name in models,
225
+ "model": self.model_name,
226
+ "available_models": models,
227
+ "base_url": self.base_url,
228
+ }
229
+ except (requests.RequestException, ValueError) as exc:
230
+ return {"ok": False, "model": self.model_name, "error": str(exc)}
231
+
232
+
233
+ # Eski dosyayı kullanan kodlar için geriye dönük uyumluluk.
234
+ ollama_local_model_managment = OllamaLocalModelManagement
pyproject.toml ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [project]
2
+ name = "local-models-make-tc-and-so"
3
+ version = "0.1.0"
4
+ description = "Araç çağrıları kullanan yerel Ollama siber güvenlik haber asistanı"
5
+ readme = "README.md"
6
+ requires-python = ">=3.11"
7
+ dependencies = [
8
+ "gradio>=6.0,<7",
9
+ "requests>=2.32,<3",
10
+ ]
11
+
12
+ [dependency-groups]
13
+ dev = [
14
+ "ruff>=0.12",
15
+ ]
16
+
17
+ [tool.ruff]
18
+ line-length = 100
19
+ target-version = "py311"
requirements.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ gradio>=6.0,<7
2
+ requests>=2.32,<3
start.sh ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ set -Eeuo pipefail
3
+
4
+ MODEL_NAME="${OLLAMA_MODEL:-qwen3.6:latest}"
5
+ MODEL_DIR="${OLLAMA_MODELS:-${HOME}/.ollama/models}"
6
+
7
+ export OLLAMA_HOST="${OLLAMA_HOST:-127.0.0.1:11434}"
8
+ export OLLAMA_BASE_URL="${OLLAMA_BASE_URL:-http://127.0.0.1:11434}"
9
+ export OLLAMA_MODELS="${MODEL_DIR}"
10
+
11
+ mkdir -p "${OLLAMA_MODELS}"
12
+
13
+ echo "Ollama sunucusu başlatılıyor: ${OLLAMA_HOST}"
14
+ ollama serve &
15
+ OLLAMA_PID=$!
16
+
17
+ READY=false
18
+ for _ in $(seq 1 120); do
19
+ if ollama list >/dev/null 2>&1; then
20
+ READY=true
21
+ break
22
+ fi
23
+ if ! kill -0 "${OLLAMA_PID}" >/dev/null 2>&1; then
24
+ echo "Hata: Ollama sunucusu beklenmedik şekilde kapandı." >&2
25
+ exit 1
26
+ fi
27
+ sleep 1
28
+ done
29
+
30
+ if [[ "${READY}" != "true" ]]; then
31
+ echo "Hata: Ollama sunucusu 120 saniye içinde hazır olmadı." >&2
32
+ exit 1
33
+ fi
34
+
35
+ if ollama show "${MODEL_NAME}" >/dev/null 2>&1; then
36
+ echo "Model hazır: ${MODEL_NAME}"
37
+ else
38
+ echo "Model indiriliyor: ${MODEL_NAME}"
39
+ ollama pull "${MODEL_NAME}"
40
+ fi
41
+
42
+ echo "Gradio başlatılıyor: ${GRADIO_SERVER_NAME:-0.0.0.0}:${GRADIO_SERVER_PORT:-7860}"
43
+ exec python /app/app.py
tests/test_agent.py ADDED
@@ -0,0 +1,128 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import unittest
4
+ from unittest.mock import patch
5
+ from unittest.mock import Mock
6
+
7
+ import requests
8
+
9
+ from ollama.cyber_security_tools import CyberSecurityTools
10
+ from ollama.ollama_local_model_managment_code import OllamaLocalModelManagement
11
+
12
+
13
+ class FakeResponse:
14
+ def __init__(self, payload, status_code=200):
15
+ self._payload = payload
16
+ self.status_code = status_code
17
+ self.text = str(payload)
18
+
19
+ def json(self):
20
+ return self._payload
21
+
22
+ def raise_for_status(self):
23
+ if self.status_code >= 400:
24
+ raise RuntimeError(self.text)
25
+
26
+
27
+ class ToolTests(unittest.TestCase):
28
+ def test_all_schemas_have_registered_function(self):
29
+ tools = CyberSecurityTools()
30
+ names = {item["function"]["name"] for item in tools.schemas}
31
+ self.assertEqual(names, set(tools.available_functions))
32
+
33
+ def test_invalid_cve_is_rejected_without_network(self):
34
+ tools = CyberSecurityTools(session=Mock())
35
+ result = tools.cve_detayi_getir("not-a-cve")
36
+ self.assertIn("hata", result)
37
+ tools.session.get.assert_not_called()
38
+
39
+ def test_nvd_normalization(self):
40
+ result = CyberSecurityTools._normalize_nvd(
41
+ {
42
+ "id": "CVE-2026-1234",
43
+ "published": "2026-01-01T00:00:00Z",
44
+ "descriptions": [{"lang": "en", "value": "Example flaw"}],
45
+ "metrics": {
46
+ "cvssMetricV31": [
47
+ {
48
+ "cvssData": {
49
+ "baseScore": 9.8,
50
+ "baseSeverity": "CRITICAL",
51
+ "vectorString": "CVSS:3.1/AV:N",
52
+ }
53
+ }
54
+ ]
55
+ },
56
+ }
57
+ )
58
+ self.assertEqual(result["cve_id"], "CVE-2026-1234")
59
+ self.assertEqual(result["cvss_puani"], 9.8)
60
+
61
+ @patch.dict("os.environ", {}, clear=True)
62
+ def test_news_failure_returns_official_advisory_fallback(self):
63
+ session = Mock()
64
+ session.get.side_effect = requests.Timeout("test timeout")
65
+ tools = CyberSecurityTools(session=session)
66
+ tools.guvenlik_duyurularini_getir = Mock(
67
+ return_value={"sonuclar": [{"kaynak": "CISA"}], "hatalar": []}
68
+ )
69
+
70
+ result = tools.siber_haberlerini_ara("ransomware", 24, 5)
71
+
72
+ self.assertEqual(
73
+ result["alternatif_resmi_duyurular"]["sonuclar"][0]["kaynak"], "CISA"
74
+ )
75
+ self.assertIn("haber değil", result["uyari"])
76
+
77
+
78
+ class AgentLoopTests(unittest.TestCase):
79
+ def test_multi_round_tool_call_and_final_answer(self):
80
+ session = Mock()
81
+ session.post.side_effect = [
82
+ FakeResponse(
83
+ {
84
+ "message": {
85
+ "role": "assistant",
86
+ "content": "",
87
+ "tool_calls": [
88
+ {
89
+ "function": {
90
+ "name": "ornek_arac",
91
+ "arguments": {"sorgu": "CVE"},
92
+ }
93
+ }
94
+ ],
95
+ }
96
+ }
97
+ ),
98
+ FakeResponse(
99
+ {
100
+ "message": {
101
+ "role": "assistant",
102
+ "content": "Türkçe nihai yanıt.",
103
+ },
104
+ "prompt_eval_count": 42,
105
+ "eval_count": 8,
106
+ }
107
+ ),
108
+ ]
109
+ tools = Mock()
110
+ tools.schemas = []
111
+ tools.execute.return_value = {"sonuclar": ["CVE-2026-1234"]}
112
+ tools.to_json.side_effect = CyberSecurityTools.to_json
113
+ agent = OllamaLocalModelManagement(
114
+ session=session, tools=tools, model_name="test-model"
115
+ )
116
+
117
+ events = list(agent.run("En son CVE nedir?"))
118
+
119
+ self.assertEqual([event["type"] for event in events], ["tool", "final"])
120
+ self.assertEqual(events[0]["name"], "ornek_arac")
121
+ self.assertEqual(events[1]["content"], "Türkçe nihai yanıt.")
122
+ second_messages = session.post.call_args_list[1].kwargs["json"]["messages"]
123
+ self.assertEqual(second_messages[-1]["role"], "tool")
124
+ self.assertEqual(second_messages[-1]["tool_name"], "ornek_arac")
125
+
126
+
127
+ if __name__ == "__main__":
128
+ unittest.main()
uv.lock ADDED
The diff for this file is too large to render. See raw diff