const MonitoringAgent = require('../models/MonitoringAgent'); const Asset = require('../models/Asset'); const { asyncHandler } = require('../middleware/errorHandler'); const { getPendingCommands } = require('./patch.controller'); const { getDatabase } = require('../config/database'); const { getForAgent } = require('./announcement.controller'); const { validateAgentKey } = require('../utils/agentAuth'); const crypto = require('crypto'); // POST /api/monitoring/enroll — Agent tauscht geteilten Bootstrap-Key gegen individuellen Per-Device-Key. // Idempotent: ein bereits enrollter Agent bekommt seinen bestehenden Key einfach erneut zurück. const enroll = asyncHandler(async (req, res) => { const agentKey = req.headers['x-agent-key']; const { hostname } = req.body; if (!hostname) return res.status(400).json({ status: 'error', message: 'hostname required' }); const db = getDatabase(); const existing = db.prepare('SELECT id, agent_key FROM monitoring_agents WHERE hostname = ?').get(hostname); const validBootstrap = agentKey === process.env.AGENT_API_KEY; const validExisting = existing?.agent_key && agentKey === existing.agent_key; if (!validBootstrap && !validExisting) { return res.status(401).json({ status: 'error', message: 'Invalid agent key' }); } let key = existing?.agent_key; if (!key) { key = 'itx-' + crypto.randomBytes(24).toString('hex'); if (existing) { db.prepare('UPDATE monitoring_agents SET agent_key = ? WHERE id = ?').run(key, existing.id); } else { db.prepare('INSERT INTO monitoring_agents (hostname, agent_key) VALUES (?, ?)').run(hostname, key); } console.log(`[Enroll] Neuer Per-Device-Key ausgestellt für ${hostname}`); } res.json({ status: 'success', data: { agent_key: key } }); }); function extractManufacturer(modelStr) { if (!modelStr) return null; const known = ['HP', 'Dell', 'Lenovo', 'Apple', 'Asus', 'Acer', 'Microsoft', 'Samsung', 'Toshiba', 'Fujitsu', 'Panasonic', 'Getac']; const upper = modelStr.toUpperCase(); for (const b of known) { if (upper.startsWith(b.toUpperCase())) return b; } return modelStr.trim().split(/\s+/)[0] || null; } function syncAgentToAsset(data) { try { const db = getDatabase(); const hostname = data.hostname; if (!hostname) return; let asset = Asset.getByName(hostname); // Benutzer anhand last_user (Username) suchen let assignedUserId = null; if (data.last_user) { const cleanUser = data.last_user.replace(/^[^\\]+\\/, ''); // Domain\User → User const userRow = db.prepare('SELECT id FROM users WHERE LOWER(username) = LOWER(?) AND is_active = 1').get(cleanUser); if (userRow) assignedUserId = userRow.id; } const syncData = { serial_number: data.hardware_serial || undefined, os: data.os_name || data.os || undefined, ip_address: data.ip_address || undefined, last_agent_sync: new Date().toISOString(), }; if (assignedUserId) { syncData.assigned_to_user_id = assignedUserId; syncData.status = 'zugewiesen'; } if (asset) { // Hersteller nur setzen wenn noch nicht gesetzt if (!asset.manufacturer && asset.model) { syncData.manufacturer = extractManufacturer(asset.model); } Asset.update(asset.id, syncData, null); } else { // Neues Asset anlegen const manufacturer = data.hardware_serial ? null : null; // Modell noch unbekannt Asset.create({ name: hostname, type: 'Notebook', serial_number: data.hardware_serial || null, model: null, status: 'zugewiesen', os: data.os_name || data.os || null, ip_address: data.ip_address || null, manufacturer: null, assigned_to_user_id: assignedUserId, department: 'IT', last_agent_sync: new Date().toISOString(), created_by_user_id: null, }); console.log(`[AgentSync] Neues Asset erstellt: ${hostname}`); } } catch (err) { console.error('[AgentSync] Fehler beim Asset-Sync:', err.message); } } // POST /api/monitoring/checkin — called by agent (no JWT, uses API key) const checkin = asyncHandler(async (req, res) => { const agentKey = req.headers['x-agent-key']; const { hostname } = req.body; if (!hostname) { return res.status(400).json({ status: 'error', message: 'hostname required' }); } if (!validateAgentKey(agentKey, hostname)) { return res.status(401).json({ status: 'error', message: 'Invalid agent key' }); } const agent = MonitoringAgent.upsert(req.body); // Asset-Sync im Hintergrund (nicht blockend) setImmediate(() => syncAgentToAsset(req.body)); // Pending patch commands für diesen Agenten zurückgeben const { pending, running } = getPendingCommands(agent.id); // Zielversion: Gruppen-spezifisch (staged rollout) oder globaler Default const GLOBAL_VERSION = process.env.AGENT_VERSION || '1.2.4'; let targetVersion = GLOBAL_VERSION; try { const db = getDatabase(); const groupRow = db.prepare(` SELECT pg.target_agent_version FROM patch_agent_groups pag JOIN patch_groups pg ON pg.id = pag.group_id WHERE pag.agent_id = ? ORDER BY pg.sort_order ASC LIMIT 1 `).get(agent.id); if (groupRow?.target_agent_version) targetVersion = groupRow.target_agent_version; } catch { /* kein Gruppe zugewiesen → global */ } const announcements = getForAgent(agent.id); res.json({ status: 'success', data: agent, commands: pending, running_commands: running, agent_version: targetVersion, announcements, }); }); // GET /api/monitoring — list all agents (requires JWT) const getAll = asyncHandler(async (req, res) => { MonitoringAgent.markOffline(); const agents = MonitoringAgent.getAll().map(a => ({ ...a, installed_software: a.installed_software ? JSON.parse(a.installed_software) : [] })); res.json({ status: 'success', data: agents }); }); // POST /api/monitoring/announcements-poll — leichter Poll nur für Ankündigungen const announcementsPoll = asyncHandler(async (req, res) => { const agentKey = req.headers['x-agent-key']; const { hostname } = req.body; if (!hostname) return res.status(400).json({ error: 'hostname required' }); if (!validateAgentKey(agentKey, hostname)) { return res.status(401).json({ error: 'Unauthorized' }); } const agent = MonitoringAgent.getByHostname(hostname); if (!agent) return res.json({ announcements: [] }); const announcements = getForAgent(agent.id); res.json({ announcements }); }); // GET /api/monitoring/statistics const getStatistics = asyncHandler(async (req, res) => { MonitoringAgent.markOffline(); const stats = MonitoringAgent.getStatistics(); res.json({ status: 'success', data: stats }); }); // GET /api/monitoring/:id const getById = asyncHandler(async (req, res) => { const agent = MonitoringAgent.getById(req.params.id); if (!agent) return res.status(404).json({ status: 'error', message: 'Agent not found' }); res.json({ status: 'success', data: { ...agent, installed_software: agent.installed_software ? JSON.parse(agent.installed_software) : [] } }); }); // DELETE /api/monitoring/:id const deleteAgent = asyncHandler(async (req, res) => { const deleted = MonitoringAgent.delete(req.params.id); if (!deleted) return res.status(404).json({ status: 'error', message: 'Agent not found' }); res.json({ status: 'success', message: 'Agent removed' }); }); // GET /api/monitoring/agent-script — agent downloads its own update (X-Agent-Key auth) const downloadScript = asyncHandler(async (req, res) => { const agentKey = req.headers['x-agent-key']; if (!agentKey || agentKey !== process.env.AGENT_API_KEY) { return res.status(401).json({ status: 'error', message: 'Invalid agent key' }); } const path = require('path'); const fs = require('fs'); const scriptPath = path.join(__dirname, '../../agent/it-nexus-agent.ps1'); if (!fs.existsSync(scriptPath)) { return res.status(404).json({ status: 'error', message: 'Script nicht gefunden' }); } res.setHeader('Content-Type', 'text/plain; charset=utf-8'); res.setHeader('X-Agent-Version', process.env.AGENT_VERSION || '1.2.3'); res.sendFile(scriptPath); }); // GET /api/monitoring/ann-watcher — agent downloads announcement watcher script const downloadAnnWatcher = asyncHandler(async (req, res) => { const agentKey = req.headers['x-agent-key']; if (!agentKey || agentKey !== process.env.AGENT_API_KEY) { return res.status(401).json({ status: 'error', message: 'Invalid agent key' }); } const path = require('path'); const fs = require('fs'); const scriptPath = path.join(__dirname, '../../agent/ann-watcher.ps1'); if (!fs.existsSync(scriptPath)) { return res.status(404).json({ status: 'error', message: 'Script nicht gefunden' }); } res.setHeader('Content-Type', 'text/plain; charset=utf-8'); res.sendFile(scriptPath); }); const downloadSetup = asyncHandler(async (req, res) => { // Agent-Key Auth für auto-update vom Agent selbst const agentKey = req.headers['x-agent-key']; const isAgent = validateAgentKey(agentKey, req.query.hostname); const isAdmin = req.user?.role_name === 'admin' || req.user?.role_name === 'super_admin'; if (!isAgent && !isAdmin) { return res.status(401).json({ status: 'error', message: 'Unauthorized' }); } const path = require('path'); const fs = require('fs'); // Zielversion: Gruppen-spezifisch (staged rollout) oder globaler Default — gleiche Logik wie checkin() let version = process.env.AGENT_VERSION || '2.0.0'; const hostname = req.query.hostname; if (hostname) { try { const db = getDatabase(); const agent = db.prepare('SELECT id FROM monitoring_agents WHERE hostname = ?').get(hostname); const groupRow = agent && db.prepare(` SELECT pg.target_agent_version FROM patch_agent_groups pag JOIN patch_groups pg ON pg.id = pag.group_id WHERE pag.agent_id = ? ORDER BY pg.sort_order ASC LIMIT 1 `).get(agent.id); if (groupRow?.target_agent_version) version = groupRow.target_agent_version; } catch { /* kein Gruppe zugewiesen → global */ } } const setupPath = path.join(__dirname, `../../agent/IT-Nexus-Agent-Setup-v${version}.exe`); if (!fs.existsSync(setupPath)) { return res.status(404).json({ status: 'error', message: 'Setup nicht gefunden' }); } res.setHeader('Content-Type', 'application/octet-stream'); res.setHeader('X-Agent-Version', version); res.setHeader('Content-Disposition', `attachment; filename="IT-Nexus-Agent-Setup-v${version}.exe"`); res.sendFile(setupPath); }); module.exports = { checkin, announcementsPoll, getAll, getStatistics, getById, deleteAgent, downloadScript, downloadAnnWatcher, downloadSetup, enroll };