Files
IT-Nexus/backend/src/controllers/patch.controller.js

304 lines
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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,
};