Files
IT-Nexus/backend/src/controllers/monitoringAgent.controller.js
Simon Grüssing f3bd8d0910 Security: Pro-Geräte Agent-Keys statt geteiltem AGENT_API_KEY (v2.8.0)
Agent v2.8.0 tauscht beim Start automatisch den geteilten Bootstrap-Key
gegen einen individuellen Per-Device-Key (POST /api/monitoring/enroll,
idempotent). Checkin/Announcements-Poll/Setup-Download/WS-Agent-Verbindungen
validieren den Key jetzt gegen den jeweiligen Hostname — ein gestohlener
Key kann sich nicht mehr als anderer Agent ausgeben (manuell verifiziert).

Alte Agents mit dem geteilten Key funktionieren während der Übergangsphase
weiter (validateAgentKey() akzeptiert beides), damit der Rollout die Fleet
nicht abrupt bricht — Migration läuft über den bestehenden Staged-Rollout
(Test → Pilot → Produktion).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-26 13:50:02 +02:00

276 lines
11 KiB
JavaScript

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 };