feat: Add Ollama AI integration for automatic mail template generation

 New Features:
- 🤖 AI-powered mail template generation with Ollama
- 📧 Test mail sending with preview
- 🔧 Ollama server and model management
- 🎨 Beautiful AI generation dialog in Templates page
- ⚙️ Ollama settings panel with connection test

Backend:
- Add ollama.service.js - Ollama API integration
- Add ollama.controller.js - Template generation endpoint
- Add ollama.routes.js - /api/ollama/* routes
- Support for multiple Ollama models (llama3.2, mistral, gemma)
- JSON-formatted AI responses with subject + HTML body
- Configurable server URL and model selection

Frontend:
- Settings: Ollama configuration panel
  - Server URL input
  - Model selection
  - Connection test with model listing
- Templates: AI generation dialog
  - Company name, scenario, employee info inputs
  - Custom prompt for AI instructions
  - Auto-save to database
  - Test mail sending functionality

Documentation:
- OLLAMA_SETUP.md - Comprehensive setup guide
- Installation instructions
- Model recommendations
- Usage examples
- Troubleshooting

Tech Stack:
- Ollama API integration (REST)
- Axios HTTP client
- React dialogs with MUI
- Self-hosted AI (privacy-friendly)
- Zero external API dependencies

Example Usage:
  Company: Garanti Bankası
  Scenario: Account security warning
  → AI generates professional phishing test mail in seconds!
This commit is contained in:
salvacybersec
2025-11-10 21:13:58 +03:00
parent d41ff7671e
commit af0510e486
8 changed files with 1121 additions and 7 deletions

View File

@@ -87,6 +87,7 @@ app.use('/api/companies', require('./routes/company.routes'));
app.use('/api/tokens', require('./routes/token.routes'));
app.use('/api/templates', require('./routes/template.routes'));
app.use('/api/settings', require('./routes/settings.routes'));
app.use('/api/ollama', require('./routes/ollama.routes'));
app.use('/api/stats', require('./routes/stats.routes'));
// Public tracking route (no rate limit on this specific route)

View File

@@ -0,0 +1,188 @@
const ollamaService = require('../services/ollama.service');
const { Settings, MailTemplate } = require('../models');
const logger = require('../utils/logger');
/**
* Test Ollama connection
*/
exports.testConnection = async (req, res, next) => {
try {
const result = await ollamaService.testConnection();
res.json({
success: result.success,
message: result.message,
data: {
models: result.models,
},
});
} catch (error) {
next(error);
}
};
/**
* List available Ollama models
*/
exports.listModels = async (req, res, next) => {
try {
const models = await ollamaService.listModels();
res.json({
success: true,
data: models,
});
} catch (error) {
next(error);
}
};
/**
* Update Ollama settings
*/
exports.updateSettings = async (req, res, next) => {
try {
const { ollama_server_url, ollama_model } = req.body;
if (ollama_server_url !== undefined) {
if (ollama_server_url) {
// Validate URL format
const cleanUrl = ollama_server_url.trim().replace(/\/$/, '');
try {
new URL(cleanUrl);
} catch (e) {
return res.status(400).json({
success: false,
error: 'Geçersiz Ollama URL formatı. Örnek: http://localhost:11434',
});
}
await Settings.upsert({
key: 'ollama_server_url',
value: cleanUrl,
is_encrypted: false,
description: 'Ollama server URL',
});
} else {
await Settings.destroy({ where: { key: 'ollama_server_url' } });
}
}
if (ollama_model !== undefined) {
if (ollama_model) {
await Settings.upsert({
key: 'ollama_model',
value: ollama_model,
is_encrypted: false,
description: 'Ollama model name',
});
} else {
await Settings.destroy({ where: { key: 'ollama_model' } });
}
}
// Reinitialize service with new settings
await ollamaService.initialize();
res.json({
success: true,
message: 'Ollama ayarları güncellendi',
});
} catch (error) {
next(error);
}
};
/**
* Generate mail template with AI
*/
exports.generateTemplate = async (req, res, next) => {
try {
const { company_name, scenario, employee_info, custom_prompt, template_name, template_type } = req.body;
// Validation
if (!company_name || !scenario) {
return res.status(400).json({
success: false,
error: 'company_name ve scenario zorunludur',
});
}
logger.info(`AI template generation requested for: ${company_name} - ${scenario}`);
// Generate template using Ollama
const templateData = await ollamaService.generateMailTemplate({
company_name,
scenario,
employee_info,
custom_prompt,
});
// Save to database if template_name is provided
let savedTemplate = null;
if (template_name && template_type) {
savedTemplate = await MailTemplate.create({
name: template_name,
type: template_type,
subject_template: templateData.subject_template,
body_template: templateData.body_template,
description: `AI tarafından oluşturuldu - ${scenario}`,
});
logger.info(`AI-generated template saved: ${template_name}`);
}
res.json({
success: true,
message: savedTemplate
? 'Template başarıyla oluşturuldu ve kaydedildi'
: 'Template başarıyla oluşturuldu',
data: {
template: savedTemplate || templateData,
generated_by: templateData.model,
},
});
} catch (error) {
logger.error('AI template generation failed:', error);
next(error);
}
};
/**
* Send test mail with generated template
*/
exports.sendTestMail = async (req, res, next) => {
try {
const { test_email, subject, body, company_name, employee_name } = req.body;
if (!test_email || !subject || !body) {
return res.status(400).json({
success: false,
error: 'test_email, subject ve body zorunludur',
});
}
const mailService = require('../services/mail.service');
// Replace placeholders
const finalSubject = subject
.replace(/\{\{company_name\}\}/g, company_name || 'Test Şirketi')
.replace(/\{\{employee_name\}\}/g, employee_name || 'Test Kullanıcı');
const finalBody = body
.replace(/\{\{company_name\}\}/g, company_name || 'Test Şirketi')
.replace(/\{\{employee_name\}\}/g, employee_name || 'Test Kullanıcı')
.replace(/\{\{tracking_url\}\}/g, 'http://example.com/test-tracking-link');
await mailService.sendMail(test_email, finalSubject, finalBody);
logger.info(`Test mail sent to: ${test_email}`);
res.json({
success: true,
message: 'Test maili başarıyla gönderildi',
});
} catch (error) {
logger.error('Test mail sending failed:', error);
next(error);
}
};

View File

@@ -0,0 +1,25 @@
const express = require('express');
const router = express.Router();
const ollamaController = require('../controllers/ollama.controller');
const { isAuthenticated } = require('../middleware/auth.middleware');
// All routes require authentication
router.use(isAuthenticated);
// Test Ollama connection
router.get('/test', ollamaController.testConnection);
// List available models
router.get('/models', ollamaController.listModels);
// Update Ollama settings
router.put('/settings', ollamaController.updateSettings);
// Generate template with AI
router.post('/generate-template', ollamaController.generateTemplate);
// Send test mail
router.post('/send-test-mail', ollamaController.sendTestMail);
module.exports = router;

View File

@@ -0,0 +1,221 @@
const axios = require('axios');
const logger = require('../utils/logger');
const { Settings } = require('../models');
class OllamaService {
constructor() {
this.serverUrl = null;
this.model = null;
this.initialized = false;
}
/**
* Initialize Ollama service with settings from database
*/
async initialize() {
try {
const serverUrlSetting = await Settings.findOne({
where: { key: 'ollama_server_url' },
});
const modelSetting = await Settings.findOne({
where: { key: 'ollama_model' },
});
this.serverUrl = serverUrlSetting?.value || process.env.OLLAMA_URL || 'http://localhost:11434';
this.model = modelSetting?.value || process.env.OLLAMA_MODEL || 'llama3.2';
this.initialized = true;
logger.info(`Ollama service initialized: ${this.serverUrl} with model ${this.model}`);
} catch (error) {
logger.error('Failed to initialize Ollama service:', error);
throw error;
}
}
/**
* Test Ollama connection
*/
async testConnection() {
if (!this.initialized) {
await this.initialize();
}
try {
const response = await axios.get(`${this.serverUrl}/api/tags`, {
timeout: 5000,
});
return {
success: true,
models: response.data.models || [],
message: 'Ollama bağlantısı başarılı',
};
} catch (error) {
logger.error('Ollama connection test failed:', error.message);
return {
success: false,
error: error.message,
message: 'Ollama sunucusuna bağlanılamadı',
};
}
}
/**
* List available models
*/
async listModels() {
if (!this.initialized) {
await this.initialize();
}
try {
const response = await axios.get(`${this.serverUrl}/api/tags`, {
timeout: 5000,
});
return response.data.models || [];
} catch (error) {
logger.error('Failed to list Ollama models:', error.message);
throw new Error('Ollama model listesi alınamadı: ' + error.message);
}
}
/**
* Generate mail template using Ollama
* @param {Object} params - Template generation parameters
* @param {string} params.company_name - Target company name
* @param {string} params.scenario - Phishing scenario type
* @param {string} params.employee_info - Employee information (optional)
* @param {string} params.custom_prompt - Custom instructions (optional)
*/
async generateMailTemplate(params) {
if (!this.initialized) {
await this.initialize();
}
const { company_name, scenario, employee_info, custom_prompt } = params;
// Build the prompt
const systemPrompt = `Sen profesyonel bir güvenlik uzmanısın ve phishing test maileri oluşturuyorsun.
Amacın gerçekçi, ikna edici ancak zararsız test mailleri oluşturmak.
Mail şablonları HTML formatında olmalı ve modern, profesyonel görünmeli.
Şablon içinde {{company_name}} ve {{employee_name}} placeholder'ları kullan.`;
let userPrompt = `Aşağıdaki bilgilere göre bir phishing test mail şablonu oluştur:
Şirket: ${company_name}
Senaryo: ${scenario}`;
if (employee_info) {
userPrompt += `\nÇalışan Bilgisi: ${employee_info}`;
}
if (custom_prompt) {
userPrompt += `\nEk Talimatlar: ${custom_prompt}`;
}
userPrompt += `
ÖNEMLI:
1. Yanıtını JSON formatında ver
2. İki alan olmalı: "subject" (konu) ve "body" (HTML mail içeriği)
3. Body HTML formatında, modern ve profesyonel olmalı
4. {{company_name}} ve {{employee_name}} placeholder'larını kullan
5. Gerçekçi ve ikna edici olmalı
6. Link için {{tracking_url}} placeholder'ını kullan
Örnek format:
{
"subject": "Mail konusu buraya",
"body": "<html>Mail içeriği buraya</html>"
}`;
try {
logger.info(`Generating template for company: ${company_name}, scenario: ${scenario}`);
const response = await axios.post(
`${this.serverUrl}/api/chat`,
{
model: this.model,
messages: [
{
role: 'system',
content: systemPrompt,
},
{
role: 'user',
content: userPrompt,
},
],
stream: false,
format: 'json',
},
{
timeout: 120000, // 2 minutes timeout for AI generation
}
);
const aiResponse = response.data.message.content;
logger.info('Ollama response received');
// Parse JSON response
let templateData;
try {
templateData = JSON.parse(aiResponse);
} catch (parseError) {
// If JSON parsing fails, try to extract JSON from response
const jsonMatch = aiResponse.match(/\{[\s\S]*\}/);
if (jsonMatch) {
templateData = JSON.parse(jsonMatch[0]);
} else {
throw new Error('AI yanıtı JSON formatında değil');
}
}
if (!templateData.subject || !templateData.body) {
throw new Error('AI yanıtında subject veya body eksik');
}
return {
subject_template: templateData.subject,
body_template: templateData.body,
generated_by: 'ollama',
model: this.model,
};
} catch (error) {
logger.error('Failed to generate template with Ollama:', error.message);
throw new Error('Template oluşturulamadı: ' + error.message);
}
}
/**
* Generate a simple text completion
*/
async generate(prompt) {
if (!this.initialized) {
await this.initialize();
}
try {
const response = await axios.post(
`${this.serverUrl}/api/generate`,
{
model: this.model,
prompt: prompt,
stream: false,
},
{
timeout: 60000,
}
);
return response.data.response;
} catch (error) {
logger.error('Ollama generation failed:', error.message);
throw new Error('Ollama yanıt veremedi: ' + error.message);
}
}
}
module.exports = new OllamaService();