Add: Agent v2.1.2, WebSocket Live Shell, WebRTC Remote Desktop (Beta), Announcement Push
- Agent v2.1.2: WebSocket ShellService, ACK nach WS-Ankündigungen, shown_announcements Fix - Backend: shellServer.js mit WebSocket-Server (Shell + Announcement Push + RTC Signaling) - Backend: patch.controller.js Fix (command_id=0 Falsy-Bug beim Auto-Update) - Frontend: Remote Desktop Tab (WebRTC Beta) in AgentDetailPage für super_admin/admin - Frontend: PatchManagementPage auf v2.1.2 aktualisiert - WPF Notification: AllowsTransparency=False + kein DropShadowEffect (Dispatcher-Crash Fix) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2041
backend/package-lock.json
generated
2041
backend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -16,29 +16,30 @@
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.39.0",
|
||||
"bcrypt": "^5.1.1",
|
||||
"better-sqlite3": "^12.6.2",
|
||||
"botbuilder": "^4.23.3",
|
||||
"bwip-js": "^4.8.0",
|
||||
"imapflow": "^1.0.0",
|
||||
"node-cron": "^3.0.0",
|
||||
"nodemailer": "^6.9.0",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.3.1",
|
||||
"express": "^4.18.2",
|
||||
"express-rate-limit": "^7.1.5",
|
||||
"express-validator": "^7.0.1",
|
||||
"helmet": "^7.1.0",
|
||||
"imapflow": "^1.0.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"mailparser": "^3.7.1",
|
||||
"mammoth": "^1.8.0",
|
||||
"morgan": "^1.10.0",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"net-snmp": "^3.11.0",
|
||||
"node-cron": "^3.0.0",
|
||||
"nodemailer": "^6.9.0",
|
||||
"pdf-parse": "^1.1.1",
|
||||
"pdfkit": "^0.15.0",
|
||||
"qrcode": "^1.5.4",
|
||||
"@anthropic-ai/sdk": "^0.39.0",
|
||||
"botbuilder": "^4.23.3",
|
||||
"pdf-parse": "^1.1.1",
|
||||
"mammoth": "^1.8.0",
|
||||
"mailparser": "^3.7.1",
|
||||
"net-snmp": "^3.11.0",
|
||||
"multer": "^1.4.5-lts.1"
|
||||
"ws": "^8.21.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^3.0.2"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
const { getDatabase } = require('../config/database');
|
||||
const { pushAnnouncementToAgent, broadcastAnnouncement, getConnectedAgents } = require('../ws/shellServer');
|
||||
|
||||
// Hilfsfunktion: welche Ankündigungen sind für einen Agenten relevant (nach Gruppe)?
|
||||
function getForAgent(agentId) {
|
||||
@@ -94,7 +95,18 @@ const create = (req, res) => {
|
||||
INSERT INTO announcements (title, message, type, target_groups, target_agent_ids, created_by_user_id, expires_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(title.trim(), message.trim(), type, JSON.stringify(target_groups), JSON.stringify(target_agent_ids), req.user.id, expires_at || null);
|
||||
res.status(201).json(db.prepare('SELECT * FROM announcements WHERE id=?').get(result.lastInsertRowid));
|
||||
const ann = db.prepare('SELECT * FROM announcements WHERE id=?').get(result.lastInsertRowid);
|
||||
|
||||
// WebSocket-Push: sofort an verbundene Agents schicken
|
||||
const wsPayload = { id: ann.id, title: ann.title, message: ann.message, type: ann.type };
|
||||
const agentIds = JSON.parse(ann.target_agent_ids || '[]');
|
||||
if (agentIds.length > 0) {
|
||||
agentIds.forEach(id => pushAnnouncementToAgent(id, [wsPayload]));
|
||||
} else {
|
||||
broadcastAnnouncement(wsPayload);
|
||||
}
|
||||
|
||||
res.status(201).json(ann);
|
||||
};
|
||||
|
||||
const update = (req, res) => {
|
||||
|
||||
@@ -7,7 +7,10 @@ class FidoKeyController {
|
||||
* GET /api/fido-keys
|
||||
*/
|
||||
static getAllKeys = asyncHandler(async (req, res) => {
|
||||
const keys = FidoKeyService.getAllKeys();
|
||||
const { user_id } = req.query;
|
||||
const keys = user_id
|
||||
? FidoKeyService.getKeysByUser(parseInt(user_id))
|
||||
: FidoKeyService.getAllKeys();
|
||||
|
||||
res.json({
|
||||
status: 'success',
|
||||
|
||||
@@ -273,11 +273,14 @@ const reportCommandResult = (req, res) => {
|
||||
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 command_id = req.body.command_id ?? req.body.CommandId;
|
||||
const { status, result } = req.body;
|
||||
if (command_id === undefined || command_id === null) 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);
|
||||
if (command_id !== 0) {
|
||||
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 });
|
||||
};
|
||||
|
||||
@@ -293,6 +296,37 @@ const getPendingCommands = (agentId) => {
|
||||
return { pending: cmds, running };
|
||||
};
|
||||
|
||||
// Remote Shell: POST /api/patch/shell/:agentId
|
||||
const triggerShell = (req, res) => {
|
||||
const { agentId } = req.params;
|
||||
const { command } = req.body;
|
||||
if (!command) return res.status(400).json({ error: 'command erforderlich' });
|
||||
const db = getDatabase();
|
||||
const result = db.prepare(
|
||||
"INSERT INTO patch_commands (agent_id, command, params, triggered_by_user_id) VALUES (?,?,?,?)"
|
||||
).run(parseInt(agentId), 'shell_exec', command, req.user.id);
|
||||
res.status(201).json({ id: result.lastInsertRowid });
|
||||
};
|
||||
|
||||
// Remote Shell: GET /api/patch/shell/:agentId/result/:cmdId
|
||||
const getShellResult = (req, res) => {
|
||||
const { cmdId } = req.params;
|
||||
const db = getDatabase();
|
||||
const cmd = db.prepare("SELECT id, status, result, created_at, completed_at FROM patch_commands WHERE id=?").get(parseInt(cmdId));
|
||||
if (!cmd) return res.status(404).json({ error: 'Nicht gefunden' });
|
||||
res.json(cmd);
|
||||
};
|
||||
|
||||
// Remote Shell: GET /api/patch/shell/:agentId/history
|
||||
const getShellHistory = (req, res) => {
|
||||
const { agentId } = req.params;
|
||||
const db = getDatabase();
|
||||
const cmds = db.prepare(
|
||||
"SELECT id, params, status, result, created_at, completed_at FROM patch_commands WHERE agent_id=? AND command='shell_exec' ORDER BY created_at DESC LIMIT 50"
|
||||
).all(parseInt(agentId));
|
||||
res.json(cmds);
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
getGroups, createGroup, updateGroup, deleteGroup, releaseVersionToGroup, releaseVersionToAll,
|
||||
upsertPolicy, deletePolicy,
|
||||
@@ -300,4 +334,5 @@ module.exports = {
|
||||
getOverview,
|
||||
triggerCommand, triggerGroupCommand, getCommands, reportCommandResult,
|
||||
getPendingCommands,
|
||||
triggerShell, getShellResult, getShellHistory,
|
||||
};
|
||||
|
||||
@@ -540,6 +540,8 @@ async function initializeDatabase() {
|
||||
`INSERT OR IGNORE INTO license_prices (sku_part_number, display_name, price_per_month) VALUES ('MCOPSTN1', 'Teams Domestic Calling', 8.00)`,
|
||||
`INSERT OR IGNORE INTO license_prices (sku_part_number, display_name, price_per_month) VALUES ('Remote_', 'Remote Desktop Services', 0.00)`,
|
||||
`INSERT OR IGNORE INTO license_prices (sku_part_number, display_name, price_per_month) VALUES ('SPB', 'Microsoft 365 Business Premium', 22.00)`,
|
||||
// Remote Shell — params Feld für patch_commands
|
||||
`ALTER TABLE patch_commands ADD COLUMN params TEXT`,
|
||||
];
|
||||
for (const migration of migrations) {
|
||||
try {
|
||||
|
||||
@@ -26,4 +26,9 @@ router.post('/commands/trigger', requireAdmin, ctrl.triggerCommand);
|
||||
router.post('/commands/trigger-group', requireAdmin, ctrl.triggerGroupCommand);
|
||||
// (result route is before authenticateToken above)
|
||||
|
||||
// Remote Shell
|
||||
router.post('/shell/:agentId', requireAdmin, ctrl.triggerShell);
|
||||
router.get('/shell/:agentId/result/:cmdId', requireAdmin, ctrl.getShellResult);
|
||||
router.get('/shell/:agentId/history', requireAdmin, ctrl.getShellHistory);
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
const express = require('express');
|
||||
const http = require('http');
|
||||
const cors = require('cors');
|
||||
const helmet = require('helmet');
|
||||
const morgan = require('morgan');
|
||||
@@ -430,12 +431,17 @@ async function startServer() {
|
||||
}
|
||||
|
||||
// Start server
|
||||
app.listen(PORT, () => {
|
||||
const { setupWebSocketServer } = require('./ws/shellServer');
|
||||
const httpServer = http.createServer(app);
|
||||
setupWebSocketServer(httpServer);
|
||||
|
||||
httpServer.listen(PORT, () => {
|
||||
console.log('═══════════════════════════════════════════');
|
||||
console.log(`✅ Server running on http://localhost:${PORT}`);
|
||||
console.log(`📝 Environment: ${process.env.NODE_ENV || 'development'}`);
|
||||
console.log(`🌐 API: http://localhost:${PORT}/api`);
|
||||
console.log(`💚 Health: http://localhost:${PORT}/health`);
|
||||
console.log(`🔌 WebSocket: ws://localhost:${PORT}/ws`);
|
||||
console.log('═══════════════════════════════════════════');
|
||||
console.log('');
|
||||
console.log('Press CTRL+C to stop the server');
|
||||
|
||||
@@ -3,13 +3,14 @@ const AuditLog = require('../models/AuditLog');
|
||||
const { AppError } = require('../middleware/errorHandler');
|
||||
|
||||
class FidoKeyService {
|
||||
/**
|
||||
* Get all FIDO keys
|
||||
*/
|
||||
static getAllKeys() {
|
||||
return FidoKey.getAll();
|
||||
}
|
||||
|
||||
static getKeysByUser(userId) {
|
||||
return FidoKey.getByAssignedUser(userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get FIDO key by ID
|
||||
*/
|
||||
|
||||
157
backend/src/ws/shellServer.js
Normal file
157
backend/src/ws/shellServer.js
Normal file
@@ -0,0 +1,157 @@
|
||||
const WebSocket = require('ws');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const { getDatabase } = require('../config/database');
|
||||
|
||||
// agentId -> WebSocket
|
||||
const agentSockets = new Map();
|
||||
// agentId -> WebSocket
|
||||
const browserSockets = new Map();
|
||||
|
||||
function setupWebSocketServer(httpServer) {
|
||||
const wss = new WebSocket.Server({ server: httpServer, path: '/ws' });
|
||||
|
||||
wss.on('connection', (ws, req) => {
|
||||
let url;
|
||||
try {
|
||||
url = new URL(req.url, 'http://localhost');
|
||||
} catch {
|
||||
ws.close(1008, 'invalid url');
|
||||
return;
|
||||
}
|
||||
const type = url.searchParams.get('type');
|
||||
|
||||
if (type === 'agent') {
|
||||
handleAgent(ws, url);
|
||||
} else if (type === 'shell') {
|
||||
handleBrowser(ws, url);
|
||||
} else {
|
||||
ws.close(1008, 'unknown type');
|
||||
}
|
||||
});
|
||||
|
||||
console.log('[WS] WebSocket server ready at /ws');
|
||||
return wss;
|
||||
}
|
||||
|
||||
function handleAgent(ws, url) {
|
||||
const key = url.searchParams.get('key');
|
||||
if (key !== process.env.AGENT_API_KEY) {
|
||||
ws.close(1008, 'unauthorized');
|
||||
return;
|
||||
}
|
||||
const hostname = url.searchParams.get('hostname');
|
||||
if (!hostname) { ws.close(1008, 'hostname required'); return; }
|
||||
|
||||
const db = getDatabase();
|
||||
const agent = db.prepare('SELECT id FROM monitoring_agents WHERE hostname = ?').get(hostname);
|
||||
if (!agent) { ws.close(1008, 'agent not found'); return; }
|
||||
|
||||
const agentId = agent.id;
|
||||
// Alte Verbindung schließen falls vorhanden
|
||||
const oldWs = agentSockets.get(agentId);
|
||||
if (oldWs && oldWs.readyState === WebSocket.OPEN) oldWs.close();
|
||||
|
||||
agentSockets.set(agentId, ws);
|
||||
console.log(`[WS] Agent verbunden: ${hostname} (id=${agentId})`);
|
||||
|
||||
// Falls Browser bereits wartet → Shell starten + melden
|
||||
const bws = browserSockets.get(agentId);
|
||||
if (bws?.readyState === WebSocket.OPEN) {
|
||||
ws.send(JSON.stringify({ type: 'start_shell' }));
|
||||
bws.send('\r\n\x1b[32m[Agent verbunden — Shell gestartet]\x1b[0m\r\n');
|
||||
}
|
||||
|
||||
ws.on('message', (data) => {
|
||||
const bws = browserSockets.get(agentId);
|
||||
if (!bws || bws.readyState !== WebSocket.OPEN) return;
|
||||
// RTC-Signaling (offer/answer/ice) → transparent weiterleiten
|
||||
try {
|
||||
const msg = JSON.parse(data.toString());
|
||||
if (msg.type?.startsWith('rtc_')) { bws.send(data.toString()); return; }
|
||||
} catch { /* kein JSON → Shell-Output */ }
|
||||
bws.send(data.toString());
|
||||
});
|
||||
|
||||
ws.on('close', () => {
|
||||
agentSockets.delete(agentId);
|
||||
const bws = browserSockets.get(agentId);
|
||||
if (bws?.readyState === WebSocket.OPEN) {
|
||||
bws.send('\r\n\x1b[31m[Agent getrennt]\x1b[0m\r\n');
|
||||
}
|
||||
console.log(`[WS] Agent getrennt: ${hostname}`);
|
||||
});
|
||||
}
|
||||
|
||||
function handleBrowser(ws, url) {
|
||||
const token = url.searchParams.get('token');
|
||||
try {
|
||||
jwt.verify(token, process.env.JWT_SECRET);
|
||||
} catch {
|
||||
ws.close(1008, 'unauthorized');
|
||||
return;
|
||||
}
|
||||
|
||||
const agentId = parseInt(url.searchParams.get('agentId'));
|
||||
if (!agentId) { ws.close(1008, 'agentId required'); return; }
|
||||
|
||||
browserSockets.set(agentId, ws);
|
||||
|
||||
const aws = agentSockets.get(agentId);
|
||||
if (aws?.readyState === WebSocket.OPEN) {
|
||||
aws.send(JSON.stringify({ type: 'start_shell' }));
|
||||
ws.send('\x1b[32m[Verbunden]\x1b[0m\r\n');
|
||||
} else {
|
||||
ws.send('\x1b[33m[Warte auf Agent-Verbindung...]\x1b[0m\r\n');
|
||||
}
|
||||
|
||||
ws.on('message', (data) => {
|
||||
const aws = agentSockets.get(agentId);
|
||||
if (!aws || aws.readyState !== WebSocket.OPEN) return;
|
||||
// RTC-Signaling (offer/answer/ice) → transparent weiterleiten
|
||||
try {
|
||||
const msg = JSON.parse(data.toString());
|
||||
if (msg.type?.startsWith('rtc_')) { aws.send(data.toString()); return; }
|
||||
} catch { /* kein JSON → Tastatureingabe */ }
|
||||
aws.send(JSON.stringify({ type: 'input', data: data.toString() }));
|
||||
});
|
||||
|
||||
ws.on('close', () => {
|
||||
browserSockets.delete(agentId);
|
||||
const aws = agentSockets.get(agentId);
|
||||
if (aws?.readyState === WebSocket.OPEN) {
|
||||
aws.send(JSON.stringify({ type: 'stop_shell' }));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Ankündigung an Agent pushen (für sofortige Zustellung)
|
||||
function pushAnnouncementToAgent(agentId, announcements) {
|
||||
const aws = agentSockets.get(agentId);
|
||||
if (aws?.readyState === WebSocket.OPEN) {
|
||||
aws.send(JSON.stringify({ type: 'announcements', announcements }));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Ankündigung an alle verbundenen Agents pushen
|
||||
function broadcastAnnouncement(announcement) {
|
||||
let pushed = 0;
|
||||
for (const [agentId, aws] of agentSockets.entries()) {
|
||||
if (aws.readyState === WebSocket.OPEN) {
|
||||
aws.send(JSON.stringify({ type: 'announcements', announcements: [announcement] }));
|
||||
pushed++;
|
||||
}
|
||||
}
|
||||
return pushed;
|
||||
}
|
||||
|
||||
function getConnectedAgents() {
|
||||
const ids = [];
|
||||
for (const [agentId, ws] of agentSockets.entries()) {
|
||||
if (ws.readyState === WebSocket.OPEN) ids.push(agentId);
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
module.exports = { setupWebSocketServer, pushAnnouncementToAgent, broadcastAnnouncement, getConnectedAgents };
|
||||
Reference in New Issue
Block a user