localgreenchain/lib/transparency/AuditLog.ts
Claude 0fcc2763fe
Add comprehensive transparency system for LocalGreenChain
This commit introduces a complete transparency infrastructure including:

Core Transparency Modules:
- AuditLog: Immutable, cryptographically-linked audit trail for all actions
- EventStream: Real-time SSE streaming and webhook support
- TransparencyDashboard: Aggregated metrics and system health monitoring
- DigitalSignatures: Cryptographic verification for handoffs and certificates

API Endpoints:
- /api/transparency/dashboard - Full platform metrics
- /api/transparency/audit - Query and log audit entries
- /api/transparency/events - SSE stream and event history
- /api/transparency/webhooks - Webhook management
- /api/transparency/signatures - Digital signature operations
- /api/transparency/certificate/[plantId] - Plant authenticity certificates
- /api/transparency/export - Multi-format data export
- /api/transparency/report - Compliance reporting
- /api/transparency/health - System health checks

Features:
- Immutable audit logging with chain integrity verification
- Real-time event streaming via Server-Sent Events
- Webhook support with HMAC signature verification
- Digital signatures for transport handoffs and ownership transfers
- Certificate of Authenticity generation for plants
- Multi-format data export (JSON, CSV, summary)
- Public transparency portal at /transparency
- System health monitoring for all components

Documentation:
- Comprehensive TRANSPARENCY.md guide with API examples
2025-11-23 03:29:56 +00:00

720 lines
20 KiB
TypeScript

/**
* Comprehensive Audit Logging System for LocalGreenChain
*
* Provides complete transparency through immutable audit trails
* tracking all system actions, data modifications, and user activities.
*/
import * as crypto from 'crypto';
import * as fs from 'fs';
import * as path from 'path';
// Audit Entry Types
export type AuditAction =
| 'CREATE' | 'UPDATE' | 'DELETE' | 'READ' | 'QUERY'
| 'TRANSFER' | 'VERIFY' | 'SIGN' | 'EXPORT'
| 'LOGIN' | 'LOGOUT' | 'API_CALL' | 'ERROR'
| 'PLANT_REGISTER' | 'PLANT_CLONE' | 'PLANT_TRANSFER'
| 'TRANSPORT_EVENT' | 'TRANSPORT_VERIFY'
| 'DEMAND_SIGNAL' | 'SUPPLY_COMMIT'
| 'FARM_UPDATE' | 'BATCH_CREATE' | 'BATCH_HARVEST'
| 'AGENT_TASK' | 'AGENT_ALERT'
| 'SYSTEM_EVENT' | 'CONFIG_CHANGE';
export type AuditSeverity = 'DEBUG' | 'INFO' | 'WARNING' | 'ERROR' | 'CRITICAL';
export type AuditCategory =
| 'PLANT' | 'TRANSPORT' | 'DEMAND' | 'SUPPLY'
| 'FARM' | 'ENVIRONMENT' | 'USER' | 'SYSTEM'
| 'AGENT' | 'BLOCKCHAIN' | 'API' | 'SECURITY';
export interface AuditActor {
id: string;
type: 'USER' | 'SYSTEM' | 'AGENT' | 'API' | 'ANONYMOUS';
name?: string;
ip?: string;
userAgent?: string;
sessionId?: string;
}
export interface AuditResource {
type: string;
id: string;
name?: string;
previousState?: any;
newState?: any;
}
export interface AuditEntry {
id: string;
timestamp: string;
action: AuditAction;
category: AuditCategory;
severity: AuditSeverity;
actor: AuditActor;
resource?: AuditResource;
description: string;
metadata?: Record<string, any>;
correlationId?: string;
parentId?: string;
hash: string;
previousHash: string;
}
export interface AuditQuery {
startDate?: string;
endDate?: string;
actions?: AuditAction[];
categories?: AuditCategory[];
severities?: AuditSeverity[];
actorId?: string;
actorType?: AuditActor['type'];
resourceType?: string;
resourceId?: string;
correlationId?: string;
searchTerm?: string;
limit?: number;
offset?: number;
}
export interface AuditStats {
totalEntries: number;
entriesByAction: Record<AuditAction, number>;
entriesByCategory: Record<AuditCategory, number>;
entriesBySeverity: Record<AuditSeverity, number>;
entriesByActorType: Record<AuditActor['type'], number>;
entriesLast24h: number;
entriesLast7d: number;
entriesLast30d: number;
topActors: Array<{ id: string; count: number }>;
topResources: Array<{ type: string; count: number }>;
errorRate24h: number;
lastEntryAt: string | null;
}
export interface AuditReport {
generatedAt: string;
period: { start: string; end: string };
stats: AuditStats;
highlights: string[];
anomalies: AuditEntry[];
complianceStatus: {
dataIntegrity: boolean;
chainValid: boolean;
noTampering: boolean;
};
}
class AuditLog {
private entries: AuditEntry[] = [];
private dataDir: string;
private dataFile: string;
private autoSaveInterval: NodeJS.Timeout | null = null;
private genesisHash = '0';
constructor() {
this.dataDir = path.join(process.cwd(), 'data');
this.dataFile = path.join(this.dataDir, 'audit-log.json');
this.load();
this.startAutoSave();
}
private load(): void {
try {
if (fs.existsSync(this.dataFile)) {
const data = JSON.parse(fs.readFileSync(this.dataFile, 'utf-8'));
this.entries = data.entries || [];
console.log(`[AuditLog] Loaded ${this.entries.length} audit entries`);
} else {
this.entries = [];
this.logSystemEvent('Audit log initialized', 'INFO');
}
} catch (error) {
console.error('[AuditLog] Error loading audit log:', error);
this.entries = [];
}
}
private save(): void {
try {
if (!fs.existsSync(this.dataDir)) {
fs.mkdirSync(this.dataDir, { recursive: true });
}
fs.writeFileSync(this.dataFile, JSON.stringify({
entries: this.entries,
exportedAt: new Date().toISOString(),
version: '1.0.0'
}, null, 2));
} catch (error) {
console.error('[AuditLog] Error saving audit log:', error);
}
}
private startAutoSave(): void {
// Auto-save every 30 seconds
this.autoSaveInterval = setInterval(() => this.save(), 30000);
}
private generateId(): string {
return `audit_${Date.now()}_${crypto.randomBytes(8).toString('hex')}`;
}
private calculateHash(entry: Omit<AuditEntry, 'hash'>): string {
const data = JSON.stringify({
id: entry.id,
timestamp: entry.timestamp,
action: entry.action,
category: entry.category,
severity: entry.severity,
actor: entry.actor,
resource: entry.resource,
description: entry.description,
metadata: entry.metadata,
correlationId: entry.correlationId,
parentId: entry.parentId,
previousHash: entry.previousHash
});
return crypto.createHash('sha256').update(data).digest('hex');
}
private getPreviousHash(): string {
if (this.entries.length === 0) {
return this.genesisHash;
}
return this.entries[this.entries.length - 1].hash;
}
/**
* Log an audit entry
*/
log(
action: AuditAction,
category: AuditCategory,
description: string,
options: {
severity?: AuditSeverity;
actor?: Partial<AuditActor>;
resource?: AuditResource;
metadata?: Record<string, any>;
correlationId?: string;
parentId?: string;
} = {}
): AuditEntry {
const entry: Omit<AuditEntry, 'hash'> = {
id: this.generateId(),
timestamp: new Date().toISOString(),
action,
category,
severity: options.severity || 'INFO',
actor: {
id: options.actor?.id || 'system',
type: options.actor?.type || 'SYSTEM',
name: options.actor?.name,
ip: options.actor?.ip,
userAgent: options.actor?.userAgent,
sessionId: options.actor?.sessionId
},
resource: options.resource,
description,
metadata: options.metadata,
correlationId: options.correlationId,
parentId: options.parentId,
previousHash: this.getPreviousHash()
};
const hash = this.calculateHash(entry);
const fullEntry: AuditEntry = { ...entry, hash };
this.entries.push(fullEntry);
// Log critical events immediately
if (fullEntry.severity === 'CRITICAL' || fullEntry.severity === 'ERROR') {
console.log(`[AuditLog] ${fullEntry.severity}: ${fullEntry.description}`);
this.save();
}
return fullEntry;
}
/**
* Log a system event
*/
logSystemEvent(description: string, severity: AuditSeverity = 'INFO', metadata?: Record<string, any>): AuditEntry {
return this.log('SYSTEM_EVENT', 'SYSTEM', description, { severity, metadata });
}
/**
* Log an API call
*/
logApiCall(
endpoint: string,
method: string,
actor: Partial<AuditActor>,
options: {
statusCode?: number;
responseTime?: number;
error?: string;
requestBody?: any;
queryParams?: any;
} = {}
): AuditEntry {
const severity: AuditSeverity = options.error ? 'ERROR' :
(options.statusCode && options.statusCode >= 400) ? 'WARNING' : 'INFO';
return this.log('API_CALL', 'API', `${method} ${endpoint}`, {
severity,
actor,
metadata: {
endpoint,
method,
statusCode: options.statusCode,
responseTime: options.responseTime,
error: options.error,
requestBody: options.requestBody,
queryParams: options.queryParams
}
});
}
/**
* Log a plant action
*/
logPlantAction(
action: 'PLANT_REGISTER' | 'PLANT_CLONE' | 'PLANT_TRANSFER',
plantId: string,
actor: Partial<AuditActor>,
options: {
previousState?: any;
newState?: any;
metadata?: Record<string, any>;
} = {}
): AuditEntry {
const descriptions: Record<string, string> = {
PLANT_REGISTER: `Plant ${plantId} registered`,
PLANT_CLONE: `Plant ${plantId} cloned`,
PLANT_TRANSFER: `Plant ${plantId} transferred`
};
return this.log(action, 'PLANT', descriptions[action], {
actor,
resource: {
type: 'plant',
id: plantId,
previousState: options.previousState,
newState: options.newState
},
metadata: options.metadata
});
}
/**
* Log a transport event
*/
logTransportEvent(
eventType: string,
plantId: string,
actor: Partial<AuditActor>,
options: {
fromLocation?: { lat: number; lng: number };
toLocation?: { lat: number; lng: number };
distance?: number;
carbonFootprint?: number;
metadata?: Record<string, any>;
} = {}
): AuditEntry {
return this.log('TRANSPORT_EVENT', 'TRANSPORT', `Transport event: ${eventType} for plant ${plantId}`, {
actor,
resource: {
type: 'transport',
id: plantId
},
metadata: {
eventType,
...options
}
});
}
/**
* Log an agent action
*/
logAgentAction(
agentName: string,
taskType: string,
success: boolean,
options: {
executionTime?: number;
error?: string;
metadata?: Record<string, any>;
} = {}
): AuditEntry {
return this.log('AGENT_TASK', 'AGENT', `Agent ${agentName} executed ${taskType}`, {
severity: success ? 'INFO' : 'WARNING',
actor: {
id: agentName,
type: 'AGENT',
name: agentName
},
metadata: {
taskType,
success,
executionTime: options.executionTime,
error: options.error,
...options.metadata
}
});
}
/**
* Log a blockchain action
*/
logBlockchainAction(
action: 'CREATE' | 'VERIFY' | 'UPDATE',
blockIndex: number,
blockHash: string,
options: {
valid?: boolean;
error?: string;
metadata?: Record<string, any>;
} = {}
): AuditEntry {
const severity: AuditSeverity = options.error ? 'ERROR' :
options.valid === false ? 'WARNING' : 'INFO';
return this.log(action, 'BLOCKCHAIN', `Block ${blockIndex} ${action.toLowerCase()}d`, {
severity,
resource: {
type: 'block',
id: blockIndex.toString(),
name: blockHash.substring(0, 16)
},
metadata: {
blockHash,
valid: options.valid,
error: options.error,
...options.metadata
}
});
}
/**
* Query audit entries
*/
query(queryParams: AuditQuery): { entries: AuditEntry[]; total: number } {
let filtered = [...this.entries];
if (queryParams.startDate) {
filtered = filtered.filter(e => e.timestamp >= queryParams.startDate!);
}
if (queryParams.endDate) {
filtered = filtered.filter(e => e.timestamp <= queryParams.endDate!);
}
if (queryParams.actions && queryParams.actions.length > 0) {
filtered = filtered.filter(e => queryParams.actions!.includes(e.action));
}
if (queryParams.categories && queryParams.categories.length > 0) {
filtered = filtered.filter(e => queryParams.categories!.includes(e.category));
}
if (queryParams.severities && queryParams.severities.length > 0) {
filtered = filtered.filter(e => queryParams.severities!.includes(e.severity));
}
if (queryParams.actorId) {
filtered = filtered.filter(e => e.actor.id === queryParams.actorId);
}
if (queryParams.actorType) {
filtered = filtered.filter(e => e.actor.type === queryParams.actorType);
}
if (queryParams.resourceType) {
filtered = filtered.filter(e => e.resource?.type === queryParams.resourceType);
}
if (queryParams.resourceId) {
filtered = filtered.filter(e => e.resource?.id === queryParams.resourceId);
}
if (queryParams.correlationId) {
filtered = filtered.filter(e => e.correlationId === queryParams.correlationId);
}
if (queryParams.searchTerm) {
const term = queryParams.searchTerm.toLowerCase();
filtered = filtered.filter(e =>
e.description.toLowerCase().includes(term) ||
e.actor.name?.toLowerCase().includes(term) ||
e.resource?.name?.toLowerCase().includes(term)
);
}
const total = filtered.length;
// Sort by timestamp descending (newest first)
filtered.sort((a, b) => b.timestamp.localeCompare(a.timestamp));
// Apply pagination
const offset = queryParams.offset || 0;
const limit = queryParams.limit || 100;
filtered = filtered.slice(offset, offset + limit);
return { entries: filtered, total };
}
/**
* Get audit statistics
*/
getStats(): AuditStats {
const now = new Date();
const day = 24 * 60 * 60 * 1000;
const entriesByAction: Record<string, number> = {};
const entriesByCategory: Record<string, number> = {};
const entriesBySeverity: Record<string, number> = {};
const entriesByActorType: Record<string, number> = {};
const actorCounts: Record<string, number> = {};
const resourceCounts: Record<string, number> = {};
let entriesLast24h = 0;
let entriesLast7d = 0;
let entriesLast30d = 0;
let errorsLast24h = 0;
for (const entry of this.entries) {
// Count by action
entriesByAction[entry.action] = (entriesByAction[entry.action] || 0) + 1;
// Count by category
entriesByCategory[entry.category] = (entriesByCategory[entry.category] || 0) + 1;
// Count by severity
entriesBySeverity[entry.severity] = (entriesBySeverity[entry.severity] || 0) + 1;
// Count by actor type
entriesByActorType[entry.actor.type] = (entriesByActorType[entry.actor.type] || 0) + 1;
// Count by actor
actorCounts[entry.actor.id] = (actorCounts[entry.actor.id] || 0) + 1;
// Count by resource type
if (entry.resource?.type) {
resourceCounts[entry.resource.type] = (resourceCounts[entry.resource.type] || 0) + 1;
}
// Time-based counts
const entryTime = new Date(entry.timestamp).getTime();
const age = now.getTime() - entryTime;
if (age <= day) {
entriesLast24h++;
if (entry.severity === 'ERROR' || entry.severity === 'CRITICAL') {
errorsLast24h++;
}
}
if (age <= 7 * day) {
entriesLast7d++;
}
if (age <= 30 * day) {
entriesLast30d++;
}
}
const topActors = Object.entries(actorCounts)
.sort((a, b) => b[1] - a[1])
.slice(0, 10)
.map(([id, count]) => ({ id, count }));
const topResources = Object.entries(resourceCounts)
.sort((a, b) => b[1] - a[1])
.slice(0, 10)
.map(([type, count]) => ({ type, count }));
return {
totalEntries: this.entries.length,
entriesByAction: entriesByAction as Record<AuditAction, number>,
entriesByCategory: entriesByCategory as Record<AuditCategory, number>,
entriesBySeverity: entriesBySeverity as Record<AuditSeverity, number>,
entriesByActorType: entriesByActorType as Record<AuditActor['type'], number>,
entriesLast24h,
entriesLast7d,
entriesLast30d,
topActors,
topResources,
errorRate24h: entriesLast24h > 0 ? errorsLast24h / entriesLast24h : 0,
lastEntryAt: this.entries.length > 0 ? this.entries[this.entries.length - 1].timestamp : null
};
}
/**
* Verify the integrity of the audit chain
*/
verifyIntegrity(): { valid: boolean; errors: string[] } {
const errors: string[] = [];
for (let i = 0; i < this.entries.length; i++) {
const entry = this.entries[i];
// Verify hash
const { hash, ...entryWithoutHash } = entry;
const calculatedHash = this.calculateHash(entryWithoutHash as any);
if (calculatedHash !== hash) {
errors.push(`Entry ${entry.id} has invalid hash (index ${i})`);
}
// Verify chain linkage
if (i === 0) {
if (entry.previousHash !== this.genesisHash) {
errors.push(`First entry should have genesis hash as previousHash`);
}
} else {
if (entry.previousHash !== this.entries[i - 1].hash) {
errors.push(`Entry ${entry.id} has broken chain link at index ${i}`);
}
}
}
return {
valid: errors.length === 0,
errors
};
}
/**
* Generate an audit report
*/
generateReport(startDate: string, endDate: string): AuditReport {
const { entries } = this.query({ startDate, endDate, limit: 10000 });
const stats = this.getStats();
const integrity = this.verifyIntegrity();
// Find anomalies (errors, critical events)
const anomalies = entries.filter(e =>
e.severity === 'ERROR' || e.severity === 'CRITICAL'
).slice(0, 50);
// Generate highlights
const highlights: string[] = [];
if (stats.entriesLast24h > 0) {
highlights.push(`${stats.entriesLast24h} events logged in the last 24 hours`);
}
if (stats.errorRate24h > 0.05) {
highlights.push(`Warning: Error rate is ${(stats.errorRate24h * 100).toFixed(1)}% in last 24h`);
}
if (anomalies.length > 0) {
highlights.push(`${anomalies.length} anomalies detected in the reporting period`);
}
if (!integrity.valid) {
highlights.push(`ALERT: Audit chain integrity compromised - ${integrity.errors.length} issues found`);
}
return {
generatedAt: new Date().toISOString(),
period: { start: startDate, end: endDate },
stats,
highlights,
anomalies,
complianceStatus: {
dataIntegrity: integrity.valid,
chainValid: integrity.valid,
noTampering: integrity.errors.length === 0
}
};
}
/**
* Get recent entries
*/
getRecent(limit: number = 50): AuditEntry[] {
return this.entries.slice(-limit).reverse();
}
/**
* Get entries by correlation ID (for tracking related actions)
*/
getByCorrelation(correlationId: string): AuditEntry[] {
return this.entries.filter(e => e.correlationId === correlationId);
}
/**
* Export audit log to various formats
*/
export(format: 'json' | 'csv' | 'summary', query?: AuditQuery): string {
const { entries } = this.query(query || { limit: 10000 });
switch (format) {
case 'json':
return JSON.stringify(entries, null, 2);
case 'csv':
const headers = ['Timestamp', 'Action', 'Category', 'Severity', 'Actor', 'Actor Type', 'Resource', 'Description'];
const rows = entries.map(e => [
e.timestamp,
e.action,
e.category,
e.severity,
e.actor.id,
e.actor.type,
e.resource?.id || '',
`"${e.description.replace(/"/g, '""')}"`
]);
return [headers.join(','), ...rows.map(r => r.join(','))].join('\n');
case 'summary':
const stats = this.getStats();
return `
AUDIT LOG SUMMARY
=================
Generated: ${new Date().toISOString()}
Total Entries: ${stats.totalEntries}
Last 24 Hours: ${stats.entriesLast24h}
Last 7 Days: ${stats.entriesLast7d}
Last 30 Days: ${stats.entriesLast30d}
Error Rate (24h): ${(stats.errorRate24h * 100).toFixed(2)}%
Top Actions:
${Object.entries(stats.entriesByAction)
.sort((a, b) => b[1] - a[1])
.slice(0, 5)
.map(([action, count]) => ` - ${action}: ${count}`)
.join('\n')}
Top Categories:
${Object.entries(stats.entriesByCategory)
.sort((a, b) => b[1] - a[1])
.slice(0, 5)
.map(([cat, count]) => ` - ${cat}: ${count}`)
.join('\n')}
`.trim();
default:
return JSON.stringify(entries);
}
}
/**
* Force save (for shutdown)
*/
forceSave(): void {
this.save();
}
/**
* Cleanup on shutdown
*/
shutdown(): void {
if (this.autoSaveInterval) {
clearInterval(this.autoSaveInterval);
}
this.save();
}
}
// Singleton instance
let auditLogInstance: AuditLog | null = null;
export function getAuditLog(): AuditLog {
if (!auditLogInstance) {
auditLogInstance = new AuditLog();
}
return auditLogInstance;
}
export default AuditLog;