2026-06-01 20:49:07 +02:00
|
|
|
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');
|
2026-06-26 13:50:02 +02:00
|
|
|
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 } });
|
|
|
|
|
});
|
2026-06-01 20:49:07 +02:00
|
|
|
|
|
|
|
|
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' });
|
|
|
|
|
}
|
2026-06-26 13:50:02 +02:00
|
|
|
if (!validateAgentKey(agentKey, hostname)) {
|
|
|
|
|
return res.status(401).json({ status: 'error', message: 'Invalid agent key' });
|
|
|
|
|
}
|
2026-06-01 20:49:07 +02:00
|
|
|
|
|
|
|
|
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' });
|
2026-06-26 13:50:02 +02:00
|
|
|
if (!validateAgentKey(agentKey, hostname)) {
|
|
|
|
|
return res.status(401).json({ error: 'Unauthorized' });
|
|
|
|
|
}
|
2026-06-01 20:49:07 +02:00
|
|
|
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'];
|
2026-06-26 13:50:02 +02:00
|
|
|
const isAgent = validateAgentKey(agentKey, req.query.hostname);
|
2026-06-01 20:49:07 +02:00
|
|
|
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');
|
2026-06-19 09:35:01 +02:00
|
|
|
|
|
|
|
|
// 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 */ }
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-01 20:49:07 +02:00
|
|
|
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);
|
|
|
|
|
});
|
|
|
|
|
|
2026-06-26 13:50:02 +02:00
|
|
|
module.exports = { checkin, announcementsPoll, getAll, getStatistics, getById, deleteAgent, downloadScript, downloadAnnWatcher, downloadSetup, enroll };
|