Spaces:
Running
Running
Deploy Ollama cyber security assistant
Browse files- .dockerignore +13 -0
- .env.example +23 -0
- .gitignore +7 -0
- .python-version +1 -0
- Dockerfile +46 -0
- README.md +195 -8
- app.py +233 -0
- main.py +7 -0
- ollama/__init__.py +5 -0
- ollama/cyber_security_tools.py +673 -0
- ollama/ollama_local_model_managment_code.py +234 -0
- pyproject.toml +19 -0
- requirements.txt +2 -0
- start.sh +43 -0
- tests/test_agent.py +128 -0
- uv.lock +0 -0
.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:
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: docker
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
short_description:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
---
|
| 11 |
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
|
|