Initial commit: IT Nexus Web-App

This commit is contained in:
2026-06-01 20:49:07 +02:00
commit 8023765e6c
387 changed files with 106900 additions and 0 deletions

View File

@@ -0,0 +1,303 @@
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,
};