Aplikasi billing & manajemen ISP (fiber optic) — PWA pelanggan, portal teknisi & kolektor, admin panel lengkap, multi-vendor OLT, RADIUS, WhatsApp/Telegram, pembayaran QRIS. Kredit & lisensi (ISC): - Base kode & repo: alijayanet/billing-rtrw oleh Alijaya Net — terima kasih banyak atas base kode dan repo yang bermanfaat. - Add-on: GoWA, LibreSpeed/OpenSpeedTest, Uptime Kuma, notification-forwarder. - Dikembangkan menjadi OPEN-ISP oleh oxpoe — https://github.com/oxpoe/open-isp
246 lines
6.4 KiB
JavaScript
246 lines
6.4 KiB
JavaScript
/**
|
|
* Settings Audit Trail
|
|
* Log semua perubahan settings untuk compliance & debugging
|
|
*/
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const { logger } = require('./logger');
|
|
const { isSensitiveField, maskValue } = require('./settingsEncryption');
|
|
|
|
const AUDIT_LOG_DIR = path.join(__dirname, '../logs/settings-audit');
|
|
const AUDIT_LOG_FILE = path.join(AUDIT_LOG_DIR, 'settings-changes.jsonl');
|
|
|
|
// Ensure audit log directory exists
|
|
if (!fs.existsSync(AUDIT_LOG_DIR)) {
|
|
fs.mkdirSync(AUDIT_LOG_DIR, { recursive: true });
|
|
}
|
|
|
|
/**
|
|
* Log settings change
|
|
*/
|
|
function logSettingsChange(actor, changes, metadata = {}) {
|
|
try {
|
|
const timestamp = new Date().toISOString();
|
|
|
|
// Mask sensitive values
|
|
const maskedChanges = {};
|
|
Object.keys(changes).forEach(field => {
|
|
if (isSensitiveField(field)) {
|
|
maskedChanges[field] = {
|
|
oldValue: maskValue(changes[field].oldValue),
|
|
newValue: maskValue(changes[field].newValue)
|
|
};
|
|
} else {
|
|
maskedChanges[field] = changes[field];
|
|
}
|
|
});
|
|
|
|
const logEntry = {
|
|
timestamp,
|
|
actor: actor || 'system',
|
|
changes: maskedChanges,
|
|
ip: metadata.ip || 'unknown',
|
|
userAgent: metadata.userAgent || 'unknown',
|
|
metadata
|
|
};
|
|
|
|
// Write to JSONL file
|
|
fs.appendFileSync(AUDIT_LOG_FILE, JSON.stringify(logEntry) + '\n', 'utf-8');
|
|
|
|
logger.info(`[settings-audit] Settings changed by ${actor}`, {
|
|
changes: Object.keys(maskedChanges),
|
|
timestamp
|
|
});
|
|
|
|
return true;
|
|
} catch (error) {
|
|
logger.error(`[settings-audit] Error logging settings change: ${error.message}`);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get settings change history
|
|
*/
|
|
function getChangeHistory(limit = 100) {
|
|
try {
|
|
if (!fs.existsSync(AUDIT_LOG_FILE)) return [];
|
|
|
|
const lines = fs.readFileSync(AUDIT_LOG_FILE, 'utf-8').split('\n').filter(l => l.trim());
|
|
const entries = lines.map(line => JSON.parse(line));
|
|
|
|
// Return latest entries
|
|
return entries.slice(-limit).reverse();
|
|
} catch (error) {
|
|
logger.error(`[settings-audit] Error reading change history: ${error.message}`);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get changes by actor
|
|
*/
|
|
function getChangesByActor(actor, limit = 50) {
|
|
try {
|
|
const history = getChangeHistory(1000);
|
|
return history.filter(entry => entry.actor === actor).slice(0, limit);
|
|
} catch (error) {
|
|
logger.error(`[settings-audit] Error filtering by actor: ${error.message}`);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get changes by field
|
|
*/
|
|
function getChangesByField(field, limit = 50) {
|
|
try {
|
|
const history = getChangeHistory(1000);
|
|
return history.filter(entry => entry.changes[field]).slice(0, limit);
|
|
} catch (error) {
|
|
logger.error(`[settings-audit] Error filtering by field: ${error.message}`);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get changes in date range
|
|
*/
|
|
function getChangesByDateRange(startDate, endDate, limit = 100) {
|
|
try {
|
|
const history = getChangeHistory(1000);
|
|
const start = new Date(startDate).getTime();
|
|
const end = new Date(endDate).getTime();
|
|
|
|
return history.filter(entry => {
|
|
const entryTime = new Date(entry.timestamp).getTime();
|
|
return entryTime >= start && entryTime <= end;
|
|
}).slice(0, limit);
|
|
} catch (error) {
|
|
logger.error(`[settings-audit] Error filtering by date range: ${error.message}`);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Export audit log
|
|
*/
|
|
function exportAuditLog(format = 'json') {
|
|
try {
|
|
const history = getChangeHistory(10000);
|
|
|
|
if (format === 'csv') {
|
|
// Convert to CSV
|
|
const headers = ['Timestamp', 'Actor', 'Field', 'Old Value', 'New Value', 'IP'];
|
|
const rows = [];
|
|
|
|
history.forEach(entry => {
|
|
Object.keys(entry.changes).forEach(field => {
|
|
rows.push([
|
|
entry.timestamp,
|
|
entry.actor,
|
|
field,
|
|
entry.changes[field].oldValue,
|
|
entry.changes[field].newValue,
|
|
entry.ip
|
|
]);
|
|
});
|
|
});
|
|
|
|
return {
|
|
headers,
|
|
rows,
|
|
data: [headers, ...rows].map(row => row.join(',')).join('\n')
|
|
};
|
|
}
|
|
|
|
// Default JSON format
|
|
return {
|
|
format: 'json',
|
|
count: history.length,
|
|
data: history
|
|
};
|
|
} catch (error) {
|
|
logger.error(`[settings-audit] Error exporting audit log: ${error.message}`);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Clear old audit logs (retention policy)
|
|
*/
|
|
function clearOldLogs(daysToKeep = 90) {
|
|
try {
|
|
if (!fs.existsSync(AUDIT_LOG_FILE)) return;
|
|
|
|
const lines = fs.readFileSync(AUDIT_LOG_FILE, 'utf-8').split('\n').filter(l => l.trim());
|
|
const cutoffDate = new Date();
|
|
cutoffDate.setDate(cutoffDate.getDate() - daysToKeep);
|
|
|
|
const recentLines = lines.filter(line => {
|
|
try {
|
|
const entry = JSON.parse(line);
|
|
return new Date(entry.timestamp) > cutoffDate;
|
|
} catch {
|
|
return false;
|
|
}
|
|
});
|
|
|
|
fs.writeFileSync(AUDIT_LOG_FILE, recentLines.join('\n') + '\n', 'utf-8');
|
|
|
|
logger.info(`[settings-audit] Cleared old logs, kept ${recentLines.length} entries`);
|
|
return true;
|
|
} catch (error) {
|
|
logger.error(`[settings-audit] Error clearing old logs: ${error.message}`);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get audit statistics
|
|
*/
|
|
function getAuditStats() {
|
|
try {
|
|
const history = getChangeHistory(10000);
|
|
|
|
const stats = {
|
|
totalChanges: history.length,
|
|
uniqueActors: new Set(history.map(e => e.actor)).size,
|
|
changedFields: new Set(),
|
|
changesByActor: {},
|
|
changesByField: {},
|
|
lastChange: history[0] || null
|
|
};
|
|
|
|
history.forEach(entry => {
|
|
// Count by actor
|
|
stats.changesByActor[entry.actor] = (stats.changesByActor[entry.actor] || 0) + 1;
|
|
|
|
// Count by field
|
|
Object.keys(entry.changes).forEach(field => {
|
|
stats.changedFields.add(field);
|
|
stats.changesByField[field] = (stats.changesByField[field] || 0) + 1;
|
|
});
|
|
});
|
|
|
|
stats.changedFields = Array.from(stats.changedFields);
|
|
|
|
return stats;
|
|
} catch (error) {
|
|
logger.error(`[settings-audit] Error getting audit stats: ${error.message}`);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
logSettingsChange,
|
|
getChangeHistory,
|
|
getChangesByActor,
|
|
getChangesByField,
|
|
getChangesByDateRange,
|
|
exportAuditLog,
|
|
clearOldLogs,
|
|
getAuditStats,
|
|
AUDIT_LOG_FILE
|
|
};
|