Files
BetterDesk/web-nodejs/services/reportEngine.js
T
UNITRONIX e855f5786d Add Go server and security audit; update web console
Add a new betterdesk-server Go codebase (server, api, auth, db, relay, signal, metrics, audit, ratelimit, proto, tools, tests) and related deployment/migration scripts. Add a comprehensive SECURITY_AUDIT_2026-03-01 report and .gitattributes; update copilot-instructions (ALL-IN-ONE v2.4.0), README, VERSION, Dockerfiles, scripts, docker-compose and entrypoint. Large updates to web-nodejs (translations, routes, services, frontend assets and middleware) and numerous new utilities; remove legacy Flask web files and archive hbbs-patch-v2 artifacts. Prepares repository for PostgreSQL support, DB migration tooling and the new Go server as the production backend.
2026-03-02 00:43:04 +01:00

327 lines
9.4 KiB
JavaScript

/**
* BetterDesk Console — Report Engine Service
*
* Generates aggregated reports from all data sources: devices, activity,
* tickets, inventory, network monitoring, DLP events, and alerts.
*
* Each report type has a dedicated generator that queries the DB adapter
* and returns structured data suitable for JSON API responses or CSV export.
*
* @author UNITRONIX
* @version 1.0.0
*/
'use strict';
const { getAdapter } = require('./dbAdapter');
// ---------------------------------------------------------------------------
// Report type registry
// ---------------------------------------------------------------------------
const REPORT_TYPES = {
devices: { label: 'Device Summary', generator: generateDeviceReport },
activity: { label: 'Activity Summary', generator: generateActivityReport },
security: { label: 'Security Report', generator: generateSecurityReport },
tickets: { label: 'Ticket Report', generator: generateTicketReport },
network: { label: 'Network Report', generator: generateNetworkReport },
inventory: { label: 'Inventory Report', generator: generateInventoryReport },
alerts: { label: 'Alert Report', generator: generateAlertReport },
};
/**
* Generate a report by type.
* @param {string} type - One of the REPORT_TYPES keys.
* @param {Object} [opts] - Filters: { from, to, device_id, limit }
* @returns {Promise<Object>} Report payload.
*/
async function generateReport(type, opts = {}) {
const entry = REPORT_TYPES[type];
if (!entry) throw new Error(`Unknown report type: ${type}`);
const data = await entry.generator(opts);
return {
type,
label: entry.label,
generated_at: new Date().toISOString(),
filters: opts,
data,
};
}
/**
* List available report types.
*/
function listReportTypes() {
return Object.entries(REPORT_TYPES).map(([key, v]) => ({ type: key, label: v.label }));
}
// ---------------------------------------------------------------------------
// Generators
// ---------------------------------------------------------------------------
async function generateDeviceReport(opts) {
const db = getAdapter();
const peers = await db.getAllPeers();
const stats = await db.getPeerStats();
const platformCounts = {};
let onlineCount = 0;
let bannedCount = 0;
for (const p of peers) {
const plat = p.platform || 'Unknown';
platformCounts[plat] = (platformCounts[plat] || 0) + 1;
if (p.online) onlineCount++;
if (p.banned) bannedCount++;
}
return {
total_devices: peers.length,
online: onlineCount,
offline: peers.length - onlineCount,
banned: bannedCount,
platform_distribution: platformCounts,
stats,
};
}
async function generateActivityReport(opts) {
const db = getAdapter();
const summaries = await db.getAllActivitySummaries();
let totalActiveMs = 0;
let totalIdleMs = 0;
const appUsage = {};
for (const s of summaries) {
totalActiveMs += s.total_active_ms || 0;
totalIdleMs += s.total_idle_ms || 0;
// Aggregate top apps if available
let apps = [];
if (s.top_apps) {
apps = typeof s.top_apps === 'string' ? JSON.parse(s.top_apps) : s.top_apps;
}
for (const app of apps) {
const name = app.app || app.name || 'Unknown';
appUsage[name] = (appUsage[name] || 0) + (app.duration_ms || app.total_ms || 0);
}
}
// Sort top apps by usage
const topApps = Object.entries(appUsage)
.sort((a, b) => b[1] - a[1])
.slice(0, 20)
.map(([name, ms]) => ({ name, total_ms: ms }));
return {
total_devices_with_activity: summaries.length,
total_active_ms: totalActiveMs,
total_idle_ms: totalIdleMs,
top_applications: topApps,
};
}
async function generateSecurityReport(opts) {
const db = getAdapter();
// DLP stats
let dlpStats = { total: 0, blocked: 0, logged: 0, usb_events: 0, file_events: 0 };
try {
dlpStats = await db.getDlpEventStats();
} catch (_) { /* DLP tables may not exist yet */ }
// Recent DLP events
let recentDlp = [];
try {
recentDlp = await db.getDlpEvents({ limit: 50 });
recentDlp = recentDlp.map(e => ({
...e,
details: typeof e.details === 'string' ? JSON.parse(e.details) : (e.details || {}),
}));
} catch (_) { /* ignore */ }
// Banned devices
const peers = await db.getAllPeers();
const banned = peers.filter(p => p.banned);
// Audit logs (last 100)
let auditLogs = [];
try {
auditLogs = await db.getAuditLogs();
auditLogs = auditLogs.slice(0, 100);
} catch (_) { /* ignore */ }
return {
dlp: {
stats: dlpStats,
recent_events: recentDlp.slice(0, 20),
},
banned_devices: banned.map(p => ({
id: p.id,
hostname: p.hostname,
ip: p.ip,
ban_reason: p.ban_reason,
})),
audit_log_count: auditLogs.length,
recent_audit: auditLogs.slice(0, 20),
};
}
async function generateTicketReport(opts) {
const db = getAdapter();
let stats = { total: 0, open: 0, closed: 0, in_progress: 0 };
try {
stats = await db.getTicketStats();
} catch (_) { /* ignore */ }
let tickets = [];
try {
tickets = await db.getAllTickets();
} catch (_) { /* ignore */ }
// Category distribution
const categories = {};
const priorities = {};
for (const t of tickets) {
const cat = t.category || 'uncategorized';
categories[cat] = (categories[cat] || 0) + 1;
const pri = t.priority || 'normal';
priorities[pri] = (priorities[pri] || 0) + 1;
}
return {
stats,
total_tickets: tickets.length,
category_distribution: categories,
priority_distribution: priorities,
};
}
async function generateNetworkReport(opts) {
const db = getAdapter();
let targets = [];
try {
targets = await db.getNetworkTargets();
} catch (_) { /* ignore */ }
let upCount = 0;
let downCount = 0;
let unknownCount = 0;
for (const t of targets) {
const s = (t.last_status || '').toLowerCase();
if (s === 'up') upCount++;
else if (s === 'down') downCount++;
else unknownCount++;
}
return {
total_targets: targets.length,
up: upCount,
down: downCount,
unknown: unknownCount,
targets: targets.map(t => ({
id: t.id,
name: t.name,
host: t.host,
check_type: t.check_type,
last_status: t.last_status,
last_rtt_ms: t.last_rtt_ms,
last_check_at: t.last_check_at,
})),
};
}
async function generateInventoryReport(opts) {
const db = getAdapter();
let inventories = [];
try {
inventories = await db.getAllInventories();
} catch (_) { /* ignore */ }
const osCounts = {};
let totalRamMb = 0;
let deviceCount = inventories.length;
for (const inv of inventories) {
let data = inv;
if (typeof inv.data === 'string') {
try { data = { ...inv, ...JSON.parse(inv.data) }; } catch (_) {}
}
const os = data.os_name || data.os || 'Unknown';
osCounts[os] = (osCounts[os] || 0) + 1;
totalRamMb += data.ram_total_mb || 0;
}
return {
total_inventoried: deviceCount,
os_distribution: osCounts,
avg_ram_mb: deviceCount > 0 ? Math.round(totalRamMb / deviceCount) : 0,
};
}
async function generateAlertReport(opts) {
const db = getAdapter();
let rules = [];
try {
rules = await db.getAlertRules();
} catch (_) { /* ignore */ }
let history = [];
try {
history = await db.getAlertHistory();
if (Array.isArray(history)) {
history = history.slice(0, 200);
}
} catch (_) { /* ignore */ }
// Count alerts per rule
const perRule = {};
for (const h of history) {
const rid = h.rule_id || 'unknown';
perRule[rid] = (perRule[rid] || 0) + 1;
}
return {
total_rules: rules.length,
active_rules: rules.filter(r => r.enabled || r.active).length,
total_alerts: history.length,
alerts_per_rule: perRule,
recent_alerts: history.slice(0, 20),
};
}
// ---------------------------------------------------------------------------
// CSV export helper
// ---------------------------------------------------------------------------
/**
* Convert an array of objects to CSV string.
* @param {Object[]} rows
* @param {string[]} [columns] - Column order; defaults to keys of first row.
* @returns {string}
*/
function toCsv(rows, columns) {
if (!rows || rows.length === 0) return '';
const cols = columns || Object.keys(rows[0]);
const escape = (v) => {
if (v === null || v === undefined) return '';
const s = String(v);
if (s.includes(',') || s.includes('"') || s.includes('\n')) {
return '"' + s.replace(/"/g, '""') + '"';
}
return s;
};
const header = cols.map(escape).join(',');
const lines = rows.map(r => cols.map(c => escape(r[c])).join(','));
return [header, ...lines].join('\n');
}
module.exports = {
generateReport,
listReportTypes,
toCsv,
REPORT_TYPES,
};