const { getDatabase } = require('../config/database'); // ── Patch Groups ────────────────────────────────────────────────────────────── const getGroups = (req, res) => { const db = getDatabase(); const groups = db.prepare('SELECT * FROM patch_groups ORDER BY sort_order, name').all(); const policies = db.prepare('SELECT * FROM patch_policies ORDER BY group_id').all(); const agentCounts = db.prepare(` SELECT group_id, COUNT(*) as count FROM patch_agent_groups GROUP BY group_id `).all(); res.json(groups.map(g => ({ ...g, policies: policies.filter(p => p.group_id === g.id), agent_count: agentCounts.find(a => a.group_id === g.id)?.count || 0, }))); }; const createGroup = (req, res) => { const { name, description, color = '#3B82F6' } = req.body; if (!name?.trim()) return res.status(400).json({ error: 'Name erforderlich' }); const db = getDatabase(); const maxOrder = db.prepare('SELECT MAX(sort_order) as m FROM patch_groups').get(); try { const result = db.prepare( 'INSERT INTO patch_groups (name, description, color, sort_order) VALUES (?, ?, ?, ?)' ).run(name.trim(), description || null, color, (maxOrder.m || 0) + 1); res.status(201).json(db.prepare('SELECT * FROM patch_groups WHERE id = ?').get(result.lastInsertRowid)); } catch (e) { if (e.message.includes('UNIQUE')) return res.status(409).json({ error: 'Gruppe existiert bereits' }); throw e; } }; const updateGroup = (req, res) => { const db = getDatabase(); const existing = db.prepare('SELECT * FROM patch_groups WHERE id = ?').get(req.params.id); if (!existing) return res.status(404).json({ error: 'Nicht gefunden' }); const { name, description, color, target_agent_version } = req.body; db.prepare('UPDATE patch_groups SET name=?, description=?, color=?, target_agent_version=? WHERE id=?') .run(name?.trim() ?? existing.name, description ?? existing.description, color ?? existing.color, target_agent_version !== undefined ? (target_agent_version || null) : existing.target_agent_version, req.params.id); res.json(db.prepare('SELECT * FROM patch_groups WHERE id = ?').get(req.params.id)); }; // Rollout-Reihenfolge: Test → Pilot → Produktion // Eine Gruppe darf eine Version nur bekommen wenn die Vorgänger-Gruppe sie bereits hat const ROLLOUT_ORDER = ['Test', 'Pilot', 'Produktion']; const releaseVersionToAll = (req, res) => { const db = getDatabase(); const { version } = req.body; if (!version) return res.status(400).json({ error: 'version erforderlich' }); db.prepare('UPDATE patch_groups SET target_agent_version=?').run(version); res.json({ success: true, version }); }; const releaseVersionToGroup = (req, res) => { const db = getDatabase(); const { version } = req.body; if (!version) return res.status(400).json({ error: 'version erforderlich' }); const group = db.prepare('SELECT * FROM patch_groups WHERE id = ?').get(req.params.id); if (!group) return res.status(404).json({ error: 'Gruppe nicht gefunden' }); const pos = ROLLOUT_ORDER.indexOf(group.name); // Vorgänger-Gruppe muss dieselbe oder höhere Version haben if (pos > 0) { const predecessorName = ROLLOUT_ORDER[pos - 1]; const predecessor = db.prepare('SELECT target_agent_version FROM patch_groups WHERE name = ?').get(predecessorName); if (!predecessor?.target_agent_version || predecessor.target_agent_version !== version) { return res.status(403).json({ error: `Version ${version} muss zuerst für "${predecessorName}" freigegeben sein bevor "${group.name}" aktualisiert werden kann.`, blocked_by: predecessorName, predecessor_version: predecessor?.target_agent_version || null }); } } db.prepare('UPDATE patch_groups SET target_agent_version=? WHERE id=?').run(version, req.params.id); res.json({ success: true, group_id: group.id, group_name: group.name, version }); }; const deleteGroup = (req, res) => { const db = getDatabase(); if (!db.prepare('SELECT id FROM patch_groups WHERE id = ?').get(req.params.id)) { return res.status(404).json({ error: 'Nicht gefunden' }); } db.prepare('DELETE FROM patch_groups WHERE id = ?').run(req.params.id); res.json({ success: true }); }; // ── Patch Policies ──────────────────────────────────────────────────────────── const upsertPolicy = (req, res) => { const { group_id, severity, max_days, notify_email } = req.body; if (!group_id || !severity || !max_days) return res.status(400).json({ error: 'group_id, severity und max_days erforderlich' }); const db = getDatabase(); const existing = db.prepare('SELECT id FROM patch_policies WHERE group_id=? AND severity=?').get(group_id, severity); if (existing) { db.prepare('UPDATE patch_policies SET max_days=?, notify_email=? WHERE id=?') .run(max_days, notify_email || null, existing.id); res.json(db.prepare('SELECT * FROM patch_policies WHERE id=?').get(existing.id)); } else { const result = db.prepare( 'INSERT INTO patch_policies (group_id, severity, max_days, notify_email) VALUES (?,?,?,?)' ).run(group_id, severity, max_days, notify_email || null); res.status(201).json(db.prepare('SELECT * FROM patch_policies WHERE id=?').get(result.lastInsertRowid)); } }; const deletePolicy = (req, res) => { const db = getDatabase(); db.prepare('DELETE FROM patch_policies WHERE id=?').run(req.params.id); res.json({ success: true }); }; // ── Agent Assignment ────────────────────────────────────────────────────────── const assignAgent = (req, res) => { const { agent_id, group_id } = req.body; if (!agent_id || !group_id) return res.status(400).json({ error: 'agent_id und group_id erforderlich' }); const db = getDatabase(); // Ein Gerät kann nur in einer Gruppe sein → alte Zuweisung entfernen db.prepare('DELETE FROM patch_agent_groups WHERE agent_id=?').run(agent_id); if (group_id !== null && group_id !== 'none') { db.prepare('INSERT OR REPLACE INTO patch_agent_groups (agent_id, group_id) VALUES (?,?)').run(agent_id, group_id); } res.json({ success: true }); }; // ── Overview / Compliance ───────────────────────────────────────────────────── const getOverview = (req, res) => { const db = getDatabase(); const agents = db.prepare(` SELECT a.*, pg.id as group_id, pg.name as group_name, pg.color as group_color FROM monitoring_agents a LEFT JOIN patch_agent_groups pag ON a.id = pag.agent_id LEFT JOIN patch_groups pg ON pag.group_id = pg.id ORDER BY pg.sort_order NULLS LAST, a.hostname `).all(); const policies = db.prepare('SELECT * FROM patch_policies').all(); const result = agents.map(agent => { let updates = []; try { updates = JSON.parse(agent.pending_updates || '[]'); } catch {} const agentPolicies = policies.filter(p => p.group_id === agent.group_id); const updateCount = typeof agent.windows_updates_pending === 'number' ? agent.windows_updates_pending : 0; // Compliance-Status berechnen let compliance = 'ok'; let violationReason = null; if (agentPolicies.length > 0 && updateCount > 0) { compliance = 'warn'; violationReason = `${updateCount} ausstehende Update(s)`; } // last_checkin ist UTC ohne Timezone-Marker → explizit als UTC parsen const lastCheckin = agent.last_checkin ? new Date(agent.last_checkin + 'Z') : null; const isOffline = !lastCheckin || (Date.now() - lastCheckin.getTime()) > 15 * 60 * 1000; return { id: agent.id, hostname: agent.hostname, os: agent.os_name, os_version: agent.os_version, last_seen: agent.last_checkin, is_offline: isOffline, pending_updates: updateCount, agent_version: agent.agent_version || '?', tpm_present: !!agent.tpm_present, tpm_version: agent.tpm_version || null, tpm_v2: !!agent.tpm_v2, secure_boot: !!agent.secure_boot, win11_ready: !!agent.win11_ready, last_user: agent.last_user || null, bitlocker_status: agent.bitlocker_status || null, defender_enabled: agent.defender_enabled != null ? !!agent.defender_enabled : null, defender_signatures_age: agent.defender_signatures_age ?? null, hardware_serial: agent.hardware_serial || null, ip_address: agent.ip_address || null, group_id: agent.group_id, group_name: agent.group_name || 'Keine Gruppe', group_color: agent.group_color || '#6B7280', compliance, violation_reason: violationReason, }; }); const stats = { total: result.length, compliant: result.filter(a => a.compliance === 'ok' && !a.is_offline).length, warnings: result.filter(a => a.compliance === 'warn').length, offline: result.filter(a => a.is_offline).length, total_pending_updates: result.reduce((s, a) => s + (a.pending_updates || 0), 0), }; res.json({ agents: result, stats }); }; // ── Commands ────────────────────────────────────────────────────────────────── const triggerCommand = (req, res) => { const { agent_id, command } = req.body; if (!agent_id || !command) return res.status(400).json({ error: 'agent_id und command erforderlich' }); const db = getDatabase(); // Duplikate vermeiden: nur ein pending/sent Command pro Agent+Type db.prepare("DELETE FROM patch_commands WHERE agent_id=? AND command=? AND status IN ('pending','sent')").run(agent_id, command); const result = db.prepare( 'INSERT INTO patch_commands (agent_id, command, triggered_by_user_id) VALUES (?,?,?)' ).run(agent_id, command, req.user.id); res.status(201).json(db.prepare('SELECT * FROM patch_commands WHERE id=?').get(result.lastInsertRowid)); }; const triggerGroupCommand = (req, res) => { const { group_id, command } = req.body; if (!group_id || !command) return res.status(400).json({ error: 'group_id und command erforderlich' }); const db = getDatabase(); const agents = db.prepare('SELECT agent_id FROM patch_agent_groups WHERE group_id=?').all(group_id); let count = 0; for (const { agent_id } of agents) { db.prepare("DELETE FROM patch_commands WHERE agent_id=? AND command=? AND status IN ('pending','sent')").run(agent_id, command); db.prepare('INSERT INTO patch_commands (agent_id, command, triggered_by_user_id) VALUES (?,?,?)').run(agent_id, command, req.user.id); count++; } res.json({ success: true, triggered: count }); }; const getCommands = (req, res) => { const db = getDatabase(); const { agent_id } = req.query; const rows = agent_id ? db.prepare(` SELECT pc.*, a.hostname, u.username as triggered_by_username FROM patch_commands pc JOIN monitoring_agents a ON pc.agent_id = a.id LEFT JOIN users u ON pc.triggered_by_user_id = u.id WHERE pc.agent_id = ? ORDER BY pc.created_at DESC LIMIT 30 `).all(agent_id) : db.prepare(` SELECT pc.*, a.hostname, u.username as triggered_by_username FROM patch_commands pc JOIN monitoring_agents a ON pc.agent_id = a.id LEFT JOIN users u ON pc.triggered_by_user_id = u.id ORDER BY pc.created_at DESC LIMIT 100 `).all(); res.json(rows); }; // Called by the agent after execution const reportCommandResult = (req, res) => { const agentKey = req.headers['x-agent-key']; if (!agentKey || agentKey !== process.env.AGENT_API_KEY) { return res.status(401).json({ error: 'Unauthorized' }); } const { command_id, status, result } = req.body; if (!command_id) return res.status(400).json({ error: 'command_id erforderlich' }); const db = getDatabase(); db.prepare("UPDATE patch_commands SET status=?, result=?, completed_at=CURRENT_TIMESTAMP WHERE id=?") .run(status || 'done', result || null, command_id); res.json({ success: true }); }; // Called by agent check-in – returns pending + running commands for this agent const getPendingCommands = (agentId) => { const db = getDatabase(); const cmds = db.prepare("SELECT * FROM patch_commands WHERE agent_id=? AND status='pending'").all(agentId); if (cmds.length > 0) { db.prepare("UPDATE patch_commands SET status='sent', sent_at=CURRENT_TIMESTAMP WHERE agent_id=? AND status='pending'") .run(agentId); } const running = db.prepare("SELECT * FROM patch_commands WHERE agent_id=? AND status='running'").all(agentId); return { pending: cmds, running }; }; module.exports = { getGroups, createGroup, updateGroup, deleteGroup, releaseVersionToGroup, releaseVersionToAll, upsertPolicy, deletePolicy, assignAgent, getOverview, triggerCommand, triggerGroupCommand, getCommands, reportCommandResult, getPendingCommands, };