304 lines
14 KiB
JavaScript
304 lines
14 KiB
JavaScript
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,
|
||
};
|