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>
276 lines
11 KiB
JavaScript
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 };
|