anon-identity
Version:
Decentralized identity framework with DIDs, Verifiable Credentials, and privacy-preserving selective disclosure
501 lines • 21 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.EnhancedAuditTrail = void 0;
const activity_logger_1 = require("../activity/activity-logger");
const types_1 = require("../activity/types");
/**
* Enhanced audit trail system for delegation revocations
* Provides advanced querying, analytics, and monitoring capabilities
*/
class EnhancedAuditTrail {
constructor(activityLogger, config = {
maxEntries: 10000,
alertThresholds: {
highVolumeMinutes: 60,
highVolumeCount: 50,
cascadeFailureThreshold: 0.8,
notificationFailureThreshold: 0.7
},
retentionDays: 365
}) {
this.activityLogger = activityLogger;
this.config = config;
this.auditEntries = new Map();
this.alertSubscribers = new Map();
this.alerts = new Map();
this.lastAnalyticsUpdate = new Date();
this.cachedAnalytics = null;
}
/**
* Adds a revocation audit entry to the trail
*/
async addAuditEntry(entry) {
this.auditEntries.set(entry.id, entry);
// Log audit entry creation
await this.activityLogger.logActivity((0, activity_logger_1.createActivity)(types_1.ActivityType.REVOCATION, {
agentDID: entry.targetAgentDID,
parentDID: '',
serviceDID: entry.serviceDID || 'all',
status: types_1.ActivityStatus.SUCCESS,
scopes: [],
details: {
action: 'audit_entry_created',
auditId: entry.id,
cascading: entry.cascading,
childCount: entry.childRevocations.length
}
}));
// Check for alerts
await this.checkAlerts(entry);
// Invalidate cached analytics
this.cachedAnalytics = null;
// Clean up old entries if needed
await this.cleanupOldEntries();
}
/**
* Updates an existing audit entry
*/
async updateAuditEntry(id, updates) {
const entry = this.auditEntries.get(id);
if (!entry) {
return false;
}
const updatedEntry = { ...entry, ...updates };
this.auditEntries.set(id, updatedEntry);
// Log audit entry update
await this.activityLogger.logActivity((0, activity_logger_1.createActivity)(types_1.ActivityType.REVOCATION, {
agentDID: entry.targetAgentDID,
parentDID: '',
serviceDID: entry.serviceDID || 'all',
status: types_1.ActivityStatus.SUCCESS,
scopes: [],
details: {
action: 'audit_entry_updated',
auditId: id,
updatedFields: Object.keys(updates)
}
}));
// Invalidate cached analytics
this.cachedAnalytics = null;
return true;
}
/**
* Queries audit entries with advanced filtering
*/
queryAuditEntries(query = {}) {
let entries = Array.from(this.auditEntries.values());
// Apply filters
if (query.agentDID) {
entries = entries.filter(entry => entry.targetAgentDID === query.agentDID ||
entry.childRevocations.includes(query.agentDID));
}
if (query.revokedBy) {
entries = entries.filter(entry => entry.revokedBy === query.revokedBy);
}
if (query.serviceDID) {
entries = entries.filter(entry => entry.serviceDID === query.serviceDID);
}
if (query.reason) {
entries = entries.filter(entry => entry.reason.toLowerCase().includes(query.reason.toLowerCase()));
}
if (query.status) {
entries = entries.filter(entry => entry.status === query.status);
}
if (query.dateRange) {
entries = entries.filter(entry => entry.timestamp >= query.dateRange.start &&
entry.timestamp <= query.dateRange.end);
}
if (query.cascading !== undefined) {
entries = entries.filter(entry => entry.cascading === query.cascading);
}
// Sort entries
const sortField = query.sortBy || 'timestamp';
const sortOrder = query.sortOrder || 'desc';
entries.sort((a, b) => {
let aVal, bVal;
switch (sortField) {
case 'timestamp':
aVal = a.timestamp.getTime();
bVal = b.timestamp.getTime();
break;
case 'agentDID':
aVal = a.targetAgentDID;
bVal = b.targetAgentDID;
break;
case 'status':
aVal = a.status;
bVal = b.status;
break;
default:
aVal = a.timestamp.getTime();
bVal = b.timestamp.getTime();
}
if (sortOrder === 'asc') {
return aVal < bVal ? -1 : aVal > bVal ? 1 : 0;
}
else {
return aVal > bVal ? -1 : aVal < bVal ? 1 : 0;
}
});
// Apply pagination
if (query.offset) {
entries = entries.slice(query.offset);
}
if (query.limit) {
entries = entries.slice(0, query.limit);
}
return entries;
}
/**
* Generates comprehensive analytics from audit data
*/
generateAnalytics(forceRefresh = false) {
const cacheValidityMinutes = 15;
const cacheAge = Date.now() - this.lastAnalyticsUpdate.getTime();
if (!forceRefresh && this.cachedAnalytics && cacheAge < cacheValidityMinutes * 60 * 1000) {
return this.cachedAnalytics;
}
const entries = Array.from(this.auditEntries.values());
const analytics = {
totalRevocations: entries.length,
revocationsByReason: new Map(),
revocationsByService: new Map(),
revocationsByUser: new Map(),
revocationsByStatus: new Map(),
cascadingPercentage: 0,
averageChildRevocations: 0,
peakRevocationHours: [],
revocationTrends: {
daily: new Map(),
weekly: new Map(),
monthly: new Map()
},
complianceMetrics: {
auditCompleteness: 0,
notificationSuccessRate: 0,
avgProcessingTime: 0
}
};
if (entries.length === 0) {
this.cachedAnalytics = analytics;
this.lastAnalyticsUpdate = new Date();
return analytics;
}
// Basic aggregations
let totalChildRevocations = 0;
let totalNotificationsSent = 0;
let totalNotificationsAttempted = 0;
let totalProcessingTime = 0;
const hourCounts = new Array(24).fill(0);
entries.forEach(entry => {
// Reason analysis
const count = analytics.revocationsByReason.get(entry.reason) || 0;
analytics.revocationsByReason.set(entry.reason, count + 1);
// Service analysis
const serviceKey = entry.serviceDID || 'global';
const serviceCount = analytics.revocationsByService.get(serviceKey) || 0;
analytics.revocationsByService.set(serviceKey, serviceCount + 1);
// User analysis
const userCount = analytics.revocationsByUser.get(entry.revokedBy) || 0;
analytics.revocationsByUser.set(entry.revokedBy, userCount + 1);
// Status analysis
const statusCount = analytics.revocationsByStatus.get(entry.status) || 0;
analytics.revocationsByStatus.set(entry.status, statusCount + 1);
// Child revocations
totalChildRevocations += entry.childRevocations.length;
// Notifications
totalNotificationsSent += entry.notificationsSent.length;
if (entry.childRevocations.length > 0 || entry.targetAgentDID) {
totalNotificationsAttempted += 1; // Estimate
}
// Time analysis
const hour = entry.timestamp.getHours();
hourCounts[hour]++;
// Processing time (estimate based on complexity)
const complexity = 1 + entry.childRevocations.length * 0.5;
totalProcessingTime += complexity;
// Trends
const dateKey = entry.timestamp.toISOString().split('T')[0];
const weekKey = this.getWeekKey(entry.timestamp);
const monthKey = entry.timestamp.toISOString().substring(0, 7);
analytics.revocationTrends.daily.set(dateKey, (analytics.revocationTrends.daily.get(dateKey) || 0) + 1);
analytics.revocationTrends.weekly.set(weekKey, (analytics.revocationTrends.weekly.get(weekKey) || 0) + 1);
analytics.revocationTrends.monthly.set(monthKey, (analytics.revocationTrends.monthly.get(monthKey) || 0) + 1);
});
// Calculate derived metrics
const cascadingCount = entries.filter(e => e.cascading).length;
analytics.cascadingPercentage = entries.length > 0 ? (cascadingCount / entries.length) * 100 : 0;
analytics.averageChildRevocations = entries.length > 0 ? totalChildRevocations / entries.length : 0;
// Find peak hours (top 3)
const hourIndices = Array.from({ length: 24 }, (_, i) => i);
analytics.peakRevocationHours = hourIndices
.sort((a, b) => hourCounts[b] - hourCounts[a])
.slice(0, 3);
// Compliance metrics
const completedEntries = entries.filter(e => e.status === 'completed' || e.status === 'partial');
analytics.complianceMetrics.auditCompleteness = entries.length > 0 ? (completedEntries.length / entries.length) * 100 : 100;
analytics.complianceMetrics.notificationSuccessRate = totalNotificationsAttempted > 0 ? (totalNotificationsSent / totalNotificationsAttempted) * 100 : 100;
analytics.complianceMetrics.avgProcessingTime = entries.length > 0 ? totalProcessingTime / entries.length : 0;
this.cachedAnalytics = analytics;
this.lastAnalyticsUpdate = new Date();
return analytics;
}
/**
* Subscribes to audit alerts
*/
subscribeToAlerts(subscriberId, callback) {
this.alertSubscribers.set(subscriberId, callback);
}
/**
* Unsubscribes from audit alerts
*/
unsubscribeFromAlerts(subscriberId) {
this.alertSubscribers.delete(subscriberId);
}
/**
* Gets all active alerts
*/
getActiveAlerts() {
return Array.from(this.alerts.values()).filter(alert => !alert.acknowledged);
}
/**
* Acknowledges an alert
*/
async acknowledgeAlert(alertId, acknowledgedBy) {
const alert = this.alerts.get(alertId);
if (!alert) {
return false;
}
alert.acknowledged = true;
alert.acknowledgedBy = acknowledgedBy;
alert.acknowledgedAt = new Date();
// Log alert acknowledgment
await this.activityLogger.logActivity((0, activity_logger_1.createActivity)(types_1.ActivityType.REVOCATION, {
agentDID: 'system',
parentDID: '',
serviceDID: 'monitoring',
status: types_1.ActivityStatus.SUCCESS,
scopes: [],
details: {
action: 'alert_acknowledged',
alertId,
alertType: alert.type,
acknowledgedBy
}
}));
return true;
}
/**
* Exports audit data in various formats
*/
exportAuditData(format, query = {}) {
const entries = this.queryAuditEntries(query);
switch (format) {
case 'json':
return JSON.stringify(entries, null, 2);
case 'csv':
return this.exportToCsv(entries);
case 'xml':
return this.exportToXml(entries);
default:
throw new Error(`Unsupported export format: ${format}`);
}
}
/**
* Generates compliance report
*/
generateComplianceReport(period) {
const entries = this.queryAuditEntries({
dateRange: period
});
const successful = entries.filter(e => e.status === 'completed').length;
const failed = entries.filter(e => e.status === 'failed').length;
const partial = entries.filter(e => e.status === 'partial').length;
const analytics = this.generateAnalytics(true);
const complianceScore = entries.length > 0 ?
((successful + partial * 0.5) / entries.length) * 100 : 100;
const recommendations = [];
const violations = [];
// Generate recommendations based on analytics
if (analytics.complianceMetrics.notificationSuccessRate < 90) {
recommendations.push('Improve notification delivery reliability');
}
if (analytics.cascadingPercentage > 70) {
recommendations.push('Review delegation depth policies to reduce excessive cascading');
}
if (failed > entries.length * 0.1) {
recommendations.push('Investigate high failure rate in revocations');
violations.push({
type: 'high_failure_rate',
severity: 'high',
description: `Failure rate exceeds 10% (${(failed / entries.length * 100).toFixed(1)}%)`,
affectedEntries: entries.filter(e => e.status === 'failed').map(e => e.id)
});
}
return {
summary: {
totalRevocations: entries.length,
successfulRevocations: successful,
failedRevocations: failed,
partialRevocations: partial,
complianceScore
},
details: {
auditCompleteness: analytics.complianceMetrics.auditCompleteness,
notificationDelivery: analytics.complianceMetrics.notificationSuccessRate,
processingTime: analytics.complianceMetrics.avgProcessingTime,
errorRate: entries.length > 0 ? (failed / entries.length) * 100 : 0
},
recommendations,
violations
};
}
// Private helper methods
async checkAlerts(entry) {
const now = new Date();
const recentWindow = new Date(now.getTime() - this.config.alertThresholds.highVolumeMinutes * 60 * 1000);
// Check for high volume
const recentEntries = Array.from(this.auditEntries.values())
.filter(e => e.timestamp >= recentWindow);
if (recentEntries.length >= this.config.alertThresholds.highVolumeCount) {
await this.createAlert('high_volume', 'medium', `High revocation volume: ${recentEntries.length} revocations in ${this.config.alertThresholds.highVolumeMinutes} minutes`, recentEntries.map(e => e.targetAgentDID));
}
// Check for cascade failures
if (entry.cascading && entry.status === 'failed') {
await this.createAlert('cascade_failure', 'high', `Cascading revocation failed for agent ${entry.targetAgentDID}`, [entry.targetAgentDID, ...entry.childRevocations]);
}
// Check for notification failures
const expectedNotifications = entry.childRevocations.length + 1; // +1 for parent chain
const actualNotifications = entry.notificationsSent.length;
if (expectedNotifications > 0 && (actualNotifications / expectedNotifications) < this.config.alertThresholds.notificationFailureThreshold) {
await this.createAlert('notification_failure', 'medium', `Notification delivery below threshold for revocation ${entry.id}`, [entry.targetAgentDID]);
}
}
async createAlert(type, severity, message, affectedAgents) {
const alert = {
id: `alert_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
type,
severity,
message,
timestamp: new Date(),
affectedAgents,
metadata: {},
acknowledged: false
};
this.alerts.set(alert.id, alert);
// Notify subscribers
this.alertSubscribers.forEach(callback => {
try {
callback(alert);
}
catch (error) {
console.error('Error notifying alert subscriber:', error);
}
});
// Log alert creation
await this.activityLogger.logActivity((0, activity_logger_1.createActivity)(types_1.ActivityType.REVOCATION, {
agentDID: 'system',
parentDID: '',
serviceDID: 'monitoring',
status: types_1.ActivityStatus.SUCCESS,
scopes: [],
details: {
action: 'alert_created',
alertId: alert.id,
alertType: type,
severity,
affectedCount: affectedAgents.length
}
}));
}
async cleanupOldEntries() {
if (this.auditEntries.size <= this.config.maxEntries) {
return;
}
const cutoffDate = new Date(Date.now() - this.config.retentionDays * 24 * 60 * 60 * 1000);
const entriesToDelete = [];
for (const [id, entry] of this.auditEntries.entries()) {
if (entry.timestamp < cutoffDate || this.auditEntries.size - entriesToDelete.length > this.config.maxEntries) {
entriesToDelete.push(id);
}
}
entriesToDelete.forEach(id => this.auditEntries.delete(id));
if (entriesToDelete.length > 0) {
await this.activityLogger.logActivity((0, activity_logger_1.createActivity)(types_1.ActivityType.REVOCATION, {
agentDID: 'system',
parentDID: '',
serviceDID: 'audit',
status: types_1.ActivityStatus.SUCCESS,
scopes: [],
details: {
action: 'audit_cleanup',
deletedCount: entriesToDelete.length,
remainingCount: this.auditEntries.size
}
}));
}
}
exportToCsv(entries) {
const headers = [
'id', 'targetAgentDID', 'revokedBy', 'reason', 'timestamp',
'cascading', 'serviceDID', 'effectiveDate', 'childRevocationsCount',
'notificationsSentCount', 'status'
];
const rows = entries.map(entry => [
entry.id,
entry.targetAgentDID,
entry.revokedBy,
entry.reason,
entry.timestamp.toISOString(),
entry.cascading.toString(),
entry.serviceDID || '',
entry.effectiveDate.toISOString(),
entry.childRevocations.length.toString(),
entry.notificationsSent.length.toString(),
entry.status
]);
return [headers, ...rows].map(row => row.join(',')).join('\n');
}
exportToXml(entries) {
const xmlHeader = '<?xml version="1.0" encoding="UTF-8"?>\n<auditTrail>\n';
const xmlFooter = '</auditTrail>';
const xmlEntries = entries.map(entry => {
return ` <entry>
<id>${this.escapeXml(entry.id)}</id>
<targetAgentDID>${this.escapeXml(entry.targetAgentDID)}</targetAgentDID>
<revokedBy>${this.escapeXml(entry.revokedBy)}</revokedBy>
<reason>${this.escapeXml(entry.reason)}</reason>
<timestamp>${entry.timestamp.toISOString()}</timestamp>
<cascading>${entry.cascading}</cascading>
<serviceDID>${this.escapeXml(entry.serviceDID || '')}</serviceDID>
<effectiveDate>${entry.effectiveDate.toISOString()}</effectiveDate>
<childRevocationsCount>${entry.childRevocations.length}</childRevocationsCount>
<notificationsSentCount>${entry.notificationsSent.length}</notificationsSentCount>
<status>${entry.status}</status>
</entry>`;
}).join('\n');
return xmlHeader + xmlEntries + '\n' + xmlFooter;
}
escapeXml(text) {
return text
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
getWeekKey(date) {
const year = date.getFullYear();
const week = this.getWeekNumber(date);
return `${year}-W${week.toString().padStart(2, '0')}`;
}
getWeekNumber(date) {
const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
const dayNum = d.getUTCDay() || 7;
d.setUTCDate(d.getUTCDate() + 4 - dayNum);
const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
return Math.ceil((((d.getTime() - yearStart.getTime()) / 86400000) + 1) / 7);
}
}
exports.EnhancedAuditTrail = EnhancedAuditTrail;
//# sourceMappingURL=audit-trail.js.map