BT ve Siber Güvenlik Risk Yönetimi Platformu
Kurumsal risk envanteri, 5×5 değerlendirme, aksiyon takibi ve yönetim raporlaması
RiskOps, bir kurumun BT ve siber güvenlik risklerini kayıt altına almak, olasılık × etki üzerinden puanlamak, azaltıcı aksiyonları takip etmek ve yönetime sunulabilir belgeler üretmek için yazılmış bir web uygulamasıdır.
Risk yaşam döngüsünün tamamını kapsar:
Risk kaydı → 5×5 değerlendirme → Seviye ataması → Aksiyon planı
↑ ↓
└──────── Yeniden değerlendirme ←────── Aksiyon tamamlanır
Bu ayrım projenin en önemli tasarım kararıdır, bu yüzden başta duruyor:
| Değildir | Neden önemli |
|---|---|
| Helpdesk / ticket sistemi | Burada "kapatılacak talep" yok; süregiden bir risk durumu var |
| ITSM aracı | Varlık envanteri, değişiklik yönetimi, SLA takibi kapsam dışı |
| Zafiyet tarayıcı | Tarama yapmaz; tarama sonuçlarının yönetildiği yerdir |
| SIEM | Log toplamaz, korelasyon kurmaz |
Bir risk çözülmez, seviyesi düşürülür. Veri modeli bu farkın üzerine kurulu:
bir riskin birden fazla risk_assessment kaydı vardır ve geçmiş değerlendirmeler
silinmez — böylece "bu risk 6 ayda nereden nereye geldi" sorusu cevaplanabilir.
| Panel | Risk Kaydı |
|---|---|
![]() |
![]() |
| 5×5 Risk Matrisi | Yönetici Özeti (yazdırma) |
|---|---|
![]() |
![]() |
| Hata Ayıklama Kipi | Hata Ayıklama Yönetimi |
|---|---|
![]() |
![]() |
Araç çubuğu her sorguyu, süresini ve o sorguyu açan dosya:satır bilgisini gösterir. Menüden süreli olarak açılır, süre dolunca kendiliğinden kapanır. Ayrıntı: Hata ayıklama kipi.
Risk yönetimi
- Otomatik risk kodu (
RISK-2026-0001) — yarış koşuluna dayanıklı sıra üreteci - 5×5 olasılık/etki matrisi, sunucu tarafında üretilir (JavaScript kapalıyken de çalışır)
- Skor
GENERATED ALWAYS AS (likelihood * impact) STORED— veritabanı seviyesinde - Seviye eşikleri ayarlardan yönetilir; eşik değişince tüm kayıtlar yeniden etiketlenir
- 10 filtre + 12 sıralanabilir kolon, FULLTEXT arama
- Yumuşak silme (soft delete) — kayıt kaybolmaz
Aksiyon takibi
- Riske bağlı azaltıcı aksiyonlar, sorumlu ve termin tarihi
- Geciken aksiyonlar panelde ve raporda ayrıca işaretlenir
Raporlama
- 6 hazır rapor + tek sayfalık Yönetici Risk Özeti
- Antetli, gizlilik ibareli, imza bloklu yazdırma çıktısı (A4,
@page) - Çift formatlı CSV:
excel(Türkçe Excel'de çift tıkla açılır) veraw(RFC 4180)
İşbirliği
- Risk kayıtlarına yorum; yazan kişi silinse bile yorum kalır
- Dosya eki: içerik tipi uzantıyla karşılaştırılır, diskteki ad uygulama tarafından üretilir, her ek zorla indirilir (tarayıcıda render edilmez)
- Toplu işlem: seçili risklere sahip atama, durum değiştirme, kapatma
Yönetim
- 4 rol × yetki matrisi (
admin/manager/analyst/viewer) - Departman ve risk kategorisi yönetimi
- Değiştirilemez (append-only) denetim kaydı
- Kullanıcı profil sayfası (rol ve departman salt okunur)
- Silinen riskleri listeleme ve geri alma
- Termini yaklaşan aksiyonlar için günlük e-posta özeti (cron)
Geliştirme ve teşhis
- Hata ayıklama kipi — menüden süreli olarak açılır (1/4/24 saat), süre dolunca kendiliğinden kapanır; açma/kapama denetim kaydına yazılır
- Araç çubuğu: çalışan her SQL ve süresi, bağlanan parametreler, sorguyu açan dosya:satır, yinelenen sorgu (N+1) uyarısı, zaman çizelgesi, istek ve oturum içeriği
- Yığın izli, kaynak parçalı ayrıntılı istisna sayfası
- Parola, CSRF jetonu, oturum kimliği ve veritabanı parolası maskelenir — maskeleme testle doğrulanır
- Tek komutluk sistem teşhis raporu (
tools/debug_report.php)
Projeyi yazarken baştan koyduğum kurallar — hiçbirinden taviz verilmedi:
Asla: düz metin parola · SQL string birleştirme · kaçışsız çıktı · yalnızca arayüzde rol kontrolü · CSRF'siz POST · GET ile silme
Uygulanan önlemler:
| Konu | Uygulama |
|---|---|
| Parola | password_hash() / password_verify(), giriş anında password_needs_rehash kontrolü |
| SQL enjeksiyonu | Tüm sorgular PDO prepared statement; ATTR_EMULATE_PREPARES = false |
| XSS | Çıktıların tamamı e() (htmlspecialchars) üzerinden |
| CSRF | Oturum token'ı + hash_equals(), her POST'ta zorunlu |
| Oturum sabitleme | Giriş ve parola değişiminde session_regenerate_id(true) |
| Yetkilendirme | require_can() / require_role() — her sayfanın başında, sunucuda |
| Kaba kuvvet | login_attempts tablosu, e-posta ve IP için ayrı sayaçlar |
| Kullanıcı sayımı | Sabit maliyetli sahte hash ile zamanlama farkı eşitlenir |
| Oturum geçersizleştirme | auth_revalidate() her istekte rol/durum/parola damgasını doğrular |
| Sıralama enjeksiyonu | ORDER BY yalnızca beyaz listeden |
LIKE kaçışı |
addcslashes($q, '\\%_') |
| CSV formül enjeksiyonu | = + - @ ile başlayan hücreler tek tırnakla metne zorlanır |
| Açık yönlendirme | Giriş sonrası redirect hedefi katı regex'ten geçer |
| Dizin koruması | .htaccess ve VirtualHost — AllowOverride kapatılsa da geçerli |
| Başlıklar | CSP, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy — Apache ve PHP tarafında |
| Dosya yükleme | Uzantı beyaz listesi + finfo ile gerçek içerik doğrulaması; diskteki ad uygulama üretir |
| Ek indirme | Her zaman application/octet-stream + attachment + nosniff — hiçbir ek tarayıcıda render edilmez |
Bilinen sınır: script-src artık 'self' — satır içi betik yok.
Ancak style-src hâlâ 'unsafe-inline' içeriyor: uygulama dinamik ölçü
taşıyan style="" öznitelikleri kullanıyor (ilerleme çubuğu genişliği
gibi) ve bunlar statik sınıfa çevrilemez. XSS yükü betik enjekte eder,
stil değil; asıl kazanç script-src tarafındaydı. Gerekçe
deploy/riskops.conf içinde yorum olarak yazılıdır.
Framework yok. Her şey açıkça görünür olsun diye bilinçli bir tercih.
istek
│
└─ <sayfa>.php
└─ includes/bootstrap.php ← tek giriş noktası
├─ config/ ortam ayarları, hata yöneticileri
├─ db.php lazy PDO singleton — db()
├─ settings.php veritabanından ayarlar
├─ csrf.php token üretimi/doğrulaması
├─ auth.php rol matrisi, auth_revalidate()
├─ audit.php denetim kaydı
├─ risk.php skor, seviye, kod üreteci
└─ ui.php bileşen yardımcıları
└─ iş mantığı + sorgular
└─ layout/header.php → içerik → layout/footer.php
Neden framework yok? Bu proje aynı zamanda bir öğrenme aracı. Composer paketlerinin arkasına saklanmak yerine oturum güvenliğini, CSRF'yi, yetki matrisini ve sorgu katmanını elle yazmak; her kararın neden öyle olduğunu görünür kılıyor. Kod, kararların gerekçesiyle birlikte yorumlanmış durumda.
12 tablo:
users · departments · risk_categories · risks · risk_assessments
· risk_actions · risk_comments · risk_attachments · settings
· audit_logs · login_attempts · risk_sequences
Dikkate değer birkaç karar:
risks.inherent_scoreGENERATED ALWAYS AS (likelihood * impact) STORED— uygulama kodu bu değeri hiç hesaplamaz, tutarsızlık imkânsız.- Seviye (severity) bilinçli olarak generated DEĞİL — eşikler ayarlardan
değişebildiği için üretilmiş kolon yanlış olurdu.
risk_recalculate_severities()eşik değişiminde tüm kayıtları yeniden etiketler. risk_sequencesyarış koşulunu tek sorguyla çözer:INSERT ... ON DUPLICATE KEY UPDATE last_number = LAST_INSERT_ID(last_number + 1)- Yabancı anahtarlar amaca göre ayrışır:
RESTRICT(kullanımdaki departman silinemez),CASCADE(risk silinince değerlendirmeleri de gider),SET NULL(kullanıcı silinince kayıt sahipsiz kalır ama kaybolmaz).
Tam şema: database/schema.sql
- Bootstrap 5.3.3 + Bootstrap Icons + Chart.js 4.4.1 — tamamı kendi sunucusunda
- Inter değişken font,
latin+latin-ext(Türkçe karakterler için) - Hiçbir CDN yok. Kapalı ağda çalışan kurumlarda bu bir zorunluluktur; ayrıca üçüncü taraf bir sunucunun kullanıcıları izlemesini engeller.
- Renk paleti
validate_palette.jsile renk körlüğü açısından doğrulandı; durum renkleri her zaman ikon + etiketle birlikte gelir, renk tek başına anlam taşımaz
Denemek için en kısa yol. Tek komut:
docker compose upSonra http://localhost:8080 — giriş demo@riskops.local / RiskOpsDemo2026
(salt okunur). İlk açılışta şema kurulur ve 18 örnek risk ile 22 aksiyon
yüklenir; sonraki açılışlarda veri korunur.
Sıfırdan başlamak için:
docker compose down -v && docker compose up| Dosya | İşlevi |
|---|---|
Dockerfile |
php:8.3-apache üzerine pdo_mysql, headers, rewrite, expires |
docker-compose.yml |
Uygulama + MariaDB 10.11, adlandırılmış hacimler |
docker/apache-riskops.conf |
AllowOverride All + hassas dizinler için ikinci koruma katmanı |
docker/entrypoint.sh |
Şema hazır olana kadar bekler, ilk açılışta örnek veriyi kurar |
docker/database.php |
Ortam değişkeninden okur — imajda gömülü parola yoktur |
Bu compose dosyası yerel kullanım ve tanıtım içindir. Parolalar dosyada düz metin durur. İnternete açılacak bir kurulum için deploy/DEPLOY.md yordamını izleyin.
Ubuntu Server 24.04 · PHP 8.3 · MariaDB 10.11 · Apache 2.4
# 1. Kodu yerleştirin
sudo git clone https://github.com/<kullanici>/riskops.git /var/www/riskops
sudo chown -R www-data:www-data /var/www/riskops
# 2. Veritabanını oluşturun
sudo mysql -e "CREATE DATABASE riskops CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
sudo mysql -e "CREATE USER 'riskops_user'@'localhost' IDENTIFIED BY 'GUCLU_BIR_PAROLA';"
sudo mysql -e "GRANT SELECT,INSERT,UPDATE,DELETE ON riskops.* TO 'riskops_user'@'localhost';"
# 3. Şemayı ve başlangıç verisini yükleyin
sudo mysql riskops < /var/www/riskops/database/schema.sql
sudo mysql riskops < /var/www/riskops/database/seed.sql
# 4. Yapılandırma
sudo cp /var/www/riskops/config/database.example.php /var/www/riskops/config/database.php
sudo nano /var/www/riskops/config/database.php # 'pass' değerini girin
# 5. Apache
sudo cp /var/www/riskops/deploy/riskops.conf /etc/apache2/sites-available/
sudo a2enmod headers rewrite expires
sudo a2ensite riskops
sudo apache2ctl configtest && sudo systemctl reload apache2İlk giriş: admin@riskops.local / Admin123456 —
uygulama ilk girişte parola değiştirmeye zorlar.
Nasıl göründüğünü hızlıca görmek için 18 gerçekçi risk, 22 aksiyon ve 3 demo kullanıcı yükler:
sudo -u www-data php /var/www/riskops/tools/seed_demo.php
tools/seed_demo.phpbir--purgeanahtarı da kabul eder ve bu anahtar tüm risk verisini siler. Üretim sunucusunda çalıştırmayın; canlıya alırkentools/dizinini tamamen kaldırın.
sudo -u www-data php /var/www/riskops/tools/go_live_check.phpVeri değiştirmeyen bir ön kontroldür: dizin izinleri, güvenlik başlıkları, ortam değişkeni, varsayılan parola, saat dilimi hizası gibi 31 maddeyi denetler ve eksikleri listeler.
Adım adım yordam: deploy/DEPLOY.md — VPS hazırlığı, HTTPS, güvenlik duvarı, gece sıfırlaması ve son kontrol listesi.
Uygulamanın bir demo kipi vardır; ortam değişkeniyle açılır:
SetEnv RISKOPS_DEMO 1Etkisi yalnızca giriş ekranındadır — ziyaretçiye deneme hesabının
bilgilerini gösterir ve o hesabın parolasını değiştirmesini engeller
(yoksa bir ziyaretçi demoyu herkese kapatabilirdi). Yetkilendirmeye
hiçbir etkisi yoktur: ziyaretçinin ne yapabileceğini viewer rolü
belirler, bu bayrak değil.
Geliştirirken "bu sayfa neden yavaş", "bu sorgu neden boş döndü", "bu
oturumda ne var" sorularını var_dump() serpiştirmeden cevaplamak için
bir hata ayıklama kipi vardır.
Yönetim ekranından (önerilen) — kenar çubuğunda Yönetim → Hata Ayıklama. Süre seçilir (1 / 4 / 24 saat), kip o süre için açılır ve kendiliğinden kapanır. Sunucuya girmek, dosya düzenlemek veya Apache'yi yeniden başlatmak gerekmez.
Ortam değişkeniyle (kalıcı, sunucu yöneticisinin kararı):
SetEnv RISKOPS_DEBUG 1Fark: ortamdan açılan kip yönetim ekranından kapatılamaz — o zaman ekran bunu açıkça söyler ve kapatma düğmesini göstermez.
Açıkken sayfanın altında bir araç çubuğu belirir:
| Panel | İçerik |
|---|---|
| Sorgular | Çalışan her SQL, süresi, dönen satır sayısı, bağlanan parametreler ve sorguyu açan dosya:satır. Yavaş olanlar ve aynı SQL'in tekrarı (N+1 işareti) ayrıca işaretlenir. |
| Zaman | İstek süresi, süreölçerler, sayaçlar, debug_mark() ile bırakılan işaretler |
| İstek | $_GET, $_POST, $_FILES, $_COOKIE, ilgili $_SERVER anahtarları |
| Oturum | Giriş yapan kullanıcı, rolünün yetki listesi, $_SESSION içeriği |
| Ortam | APP_ENV, PHP ve MariaDB sürümü, saat dilimi, bellek sınırı, yüklenen dosya sayısı |
| Notlar | dbg() dökümleri ve debug_note() notları |
| Log | storage/logs/app.log kuyruğu |
Kod içinden kullanılan yardımcılar — hepsi kip kapalıyken ilk satırında döner, yani üretimde unutulmuş bir çağrı hiçbir şey yapmaz:
dbg($risk, 'store öncesi'); // değeri araç çubuğuna bas
debug_mark('rapor sorgusu bitti'); // zaman çizelgesine nokta koy
debug_timer_start('csv'); …; debug_timer_stop('csv');
debug_note('mail', 'gönderim atlandı', ['sebep' => 'adres yok']);Ayrıca:
- Ayrıntılı istisna sayfası — yığın izi, hatalı satırın kaynak parçası ve o isteğe kadar çalışmış tüm sorgular. Bozuk bir SQL'in kendisi de listede ve kırmızı görünür.
X-RiskOps-Debugyanıt başlığı — süre, bellek ve sorgu sayısı. CSV dışa aktarma ve yönlendirme gibi araç çubuğu basılamayan yanıtlarda ölçüm buradan okunur.storage/logs/debug.log— istek başına tek satır özet. Yalnızca sorunlu istekler istenirseSetEnv RISKOPS_DEBUG_LOG_ONLY_SLOW 1.- Araç çubuğundaki "Kipi kapat" — kipi gerçekten kapatır
(POST + CSRF). Yanındaki
✕yalnızca gizler, kip açık kalır. ?rkdebug=off— araç çubuğunu bu oturum için gizler (ekran görüntüsü alırken), kipi kapatmaz.
Yönetim ekranından açılan kip bir bitiş zamanı taşır
(storage/debug.flag). Süre dolduğunda dosya silinmemiş olsa bile
yok sayılır — "geçen ay açmıştım, hâlâ açıkmış" durumu oluşamaz.
Ortam değişkeni yolunda: APP_ENV=production iken RISKOPS_DEBUG
tek başına yetmez, ayrıca RISKOPS_DEBUG_PRODUCTION=1 gerekir.
Kopyalanmış bir VirtualHost'ta unutulan tek bir satır canlı sistemi
bilgi sızdıran hâle getirmesin diye böyle.
Yönetim ekranı yolunda bu çifte bayrak aranmaz ve bu kasıtlıdır:
oradaki açma zaten kazara değildir — oturum açmış bir admin,
POST + CSRF ile, süreli olarak yapar ve işlem denetim kaydına yazılır
(debug.enable / debug.disable). Çifte bayrak kuralı unutulmaya
karşıdır, bilinçli açmaya karşı değil.
Hangi yoldan açılırsa açılsın araç çubuğu üretimde yalnızca admin
rolüne gösterilir, tools/go_live_check.php durumu FAIL olarak
raporlar ve nasıl kapatılacağını kaynağına göre söyler.
Araç çubuğu istek ve oturum içeriğini gösterdiği için maskeleme
zorunludur. İki katman çalışır: anahtar adı şüpheliyse (password,
_csrf, PHPSESSID, api_key…) değer *** olur; anahtar masum olsa
bile değerin kendisi veritabanı parolası, oturum kimliği veya CSRF
jetonuyla aynıysa yine *** olur.
php tools/debug_test.php # kip kapalıyken: 109/109
RISKOPS_DEBUG=1 php tools/debug_test.php # kip açıkken: 119/119Hata bildirirken ortamı tarif etmek yerine:
php tools/debug_report.php > rapor.txtPHP sürümü ve eklentileri, MariaDB sürümü ve oturum değişkenleri, tablo satır sayıları ve boyutları, dizin izinleri, PHP–MySQL saat farkı, disk durumu ve log kuyruğu. Bağlantı bilgisi ve parola çıktıya girmez; raporu olduğu gibi paylaşabilirsiniz.
| PHP dosyası | 119 |
| PHP satırı | ~18.500 |
| CSS satırı | ~2.440 (app.css) + araç çubuğu ve yazdırma stili |
| Veritabanı tablosu | 14 |
| Duman testi | 59 doğrulama |
| Hata ayıklama kipi testi | 109 (kapalı) + 119 (açık) |
| Harici PHP bağımlılığı | 0 |
php tools/smoke_test.php # 59/59
php tools/debug_test.php # 109/109
find . -name "*.php" -not -path "./assets/*" -exec php -l {} \;Önceki yol haritasındaki altı maddenin tamamı tamamlandı:
- CSP'den
script-src 'unsafe-inline'kaldırıldı - Risk kayıtlarına yorum ve dosya eki
- Termin yaklaşan aksiyonlar için e-posta bildirimi
- Toplu işlemler (çoklu risk atama / durum değiştirme / kapatma)
- Kullanıcı profil sayfası
- Silinen riski geri alma ekranı
Sırada:
-
style-src 'unsafe-inline'kaldırıldı — satır içi stil özniteliği kalmadı; dinamik renkler nonce taşıyan bir<style>bloğunda - Aksiyonlar için de yorum ve ek (ortak tartışma katmanı)
- Risk değerlendirme hatırlatması (uzun süre gözden geçirilmeyen kayıtlar)
- Dışa aktarılabilir denetim raporu (audit log filtreli CSV)
- [~] Çok dilli arayüz — altyapı tamam, çeviri kısmi (aşağıya bakın)
Sonradan eklenenler:
- Arayüz yenilendi — giriş ekranının açık, ferah dili uygulama geneline taşındı
- Hata ayıklama kipi — sorgu kaydedici, araç çubuğu, ayrıntılı istisna sayfası, teşhis raporu; menüden süreli açılıp kapanıyor
Altyapı çalışır durumda ve test edilmiştir: t() / te() fonksiyonları,
lang/tr.php + lang/en.php sözlükleri, kullanıcı başına kalıcı dil
tercihi (users.locale), kenar çubuğunda dil değiştirici.
Çeviri anahtarı Türkçe metnin kendisidir (t('Yeni Risk')). Bunun
pratik sonucu: sözlükte karşılığı olmayan bir metin bozulmaz, doğru
Türkçesiyle görünür. Yani kısmi çeviri kullanılabilir bir durumdur,
kırık bir durum değil.
Şu an İngilizceye çevrilmiş olanlar:
| Çevrildi | Henüz çevrilmedi |
|---|---|
| Kenar çubuğu ve üst bilgi (tüm menü) | Risk listesi ve formu |
| Giriş ekranı | Risk detayı |
| Profil sayfası | Raporlar ve yönetici özeti |
| Silinen riskler | Yönetim ekranları (kullanıcı, departman, kategori, ayarlar) |
| Yorumlar ve ekler | Değerlendirme ekranları |
| Aksiyon detayı | Denetim kaydı ekranı |
Kalanları çevirmek mekanik bir iştir: metni t('...') ile sarıp
lang/en.php dosyasına bir satır eklemek. Sözlükte 124 kayıt var.
Bilinen sınır: aynı Türkçe kelime farklı bağlamlarda farklı
çevrilmesi gerektiğinde tek anahtar yetmez. Giriş ekranındaki
"Daha Güvenli" bunun örneği: hem başlıkta hem rozette geçiyor, iki
farklı İngilizce karşılığı var. Çözüm, o metinleri tam ifade olarak
anahtarlamak ('Daha Güvenli Operasyonlar').
MIT — bkz. LICENSE.
In English
RiskOps is an IT & cyber security risk management platform: risk register, 5×5 likelihood/impact assessment, mitigation action tracking, and printable executive reporting.
It is deliberately not a helpdesk, ITSM tool, vulnerability scanner, or SIEM. A risk isn't "closed" like a ticket — it's reassessed, and its level goes down. The data model reflects that: every risk owns a history of assessments that is never deleted.
Built with PHP 8.3, MariaDB 10.11 and Apache — no framework, no Composer dependencies, no CDN (all assets self-hosted, which matters for air-gapped corporate networks). Security was a first-class constraint rather than a later pass: prepared statements with emulation disabled, CSRF on every POST, server-side authorisation on every page, per-request session revalidation, rate limiting with timing-equalised login, and CSV formula-injection escaping.
The UI and all documentation are in Turkish.






