agentshield-sdk 7.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/CHANGELOG.md +191 -0
  2. package/LICENSE +21 -0
  3. package/README.md +975 -0
  4. package/bin/agent-shield.js +680 -0
  5. package/package.json +118 -0
  6. package/src/adaptive.js +330 -0
  7. package/src/agent-protocol.js +998 -0
  8. package/src/alert-tuning.js +480 -0
  9. package/src/allowlist.js +603 -0
  10. package/src/audit-immutable.js +914 -0
  11. package/src/audit-streaming.js +469 -0
  12. package/src/badges.js +196 -0
  13. package/src/behavior-profiling.js +289 -0
  14. package/src/benchmark-harness.js +804 -0
  15. package/src/canary.js +271 -0
  16. package/src/certification.js +563 -0
  17. package/src/circuit-breaker.js +321 -0
  18. package/src/compliance.js +617 -0
  19. package/src/confidence-tuning.js +324 -0
  20. package/src/confused-deputy.js +624 -0
  21. package/src/context-scoring.js +360 -0
  22. package/src/conversation.js +494 -0
  23. package/src/cost-optimizer.js +1024 -0
  24. package/src/ctf.js +462 -0
  25. package/src/detector-core.js +1999 -0
  26. package/src/distributed.js +359 -0
  27. package/src/document-scanner.js +795 -0
  28. package/src/embedding.js +307 -0
  29. package/src/encoding.js +429 -0
  30. package/src/enterprise.js +405 -0
  31. package/src/errors.js +100 -0
  32. package/src/eu-ai-act.js +523 -0
  33. package/src/fuzzer.js +764 -0
  34. package/src/honeypot.js +328 -0
  35. package/src/i18n-patterns.js +523 -0
  36. package/src/index.js +430 -0
  37. package/src/integrations.js +528 -0
  38. package/src/llm-redteam.js +670 -0
  39. package/src/main.js +741 -0
  40. package/src/main.mjs +38 -0
  41. package/src/mcp-bridge.js +542 -0
  42. package/src/mcp-certification.js +846 -0
  43. package/src/mcp-sdk-integration.js +355 -0
  44. package/src/mcp-security-runtime.js +741 -0
  45. package/src/mcp-server.js +740 -0
  46. package/src/middleware.js +208 -0
  47. package/src/model-finetuning.js +884 -0
  48. package/src/model-fingerprint.js +1042 -0
  49. package/src/multi-agent-trust.js +453 -0
  50. package/src/multi-agent.js +404 -0
  51. package/src/multimodal.js +296 -0
  52. package/src/nist-mapping.js +505 -0
  53. package/src/observability.js +330 -0
  54. package/src/openclaw.js +450 -0
  55. package/src/otel.js +544 -0
  56. package/src/owasp-2025.js +483 -0
  57. package/src/pii.js +390 -0
  58. package/src/plugin-marketplace.js +628 -0
  59. package/src/plugin-system.js +349 -0
  60. package/src/policy-dsl.js +775 -0
  61. package/src/policy-extended.js +635 -0
  62. package/src/policy.js +443 -0
  63. package/src/presets.js +409 -0
  64. package/src/production.js +557 -0
  65. package/src/prompt-leakage.js +321 -0
  66. package/src/rag-vulnerability.js +579 -0
  67. package/src/redteam.js +475 -0
  68. package/src/response-handler.js +429 -0
  69. package/src/scanners.js +357 -0
  70. package/src/self-healing.js +363 -0
  71. package/src/semantic.js +339 -0
  72. package/src/shield-score.js +250 -0
  73. package/src/sso-saml.js +897 -0
  74. package/src/stream-scanner.js +806 -0
  75. package/src/testing.js +505 -0
  76. package/src/threat-encyclopedia.js +629 -0
  77. package/src/threat-intel-network.js +1017 -0
  78. package/src/token-analysis.js +467 -0
  79. package/src/tool-guard.js +412 -0
  80. package/src/tool-output-validator.js +354 -0
  81. package/src/utils.js +83 -0
  82. package/src/watermark.js +235 -0
  83. package/src/worker-scanner.js +601 -0
  84. package/types/index.d.ts +2088 -0
@@ -0,0 +1,914 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Agent Shield — Immutable Hash-Chained Audit Log
5
+ *
6
+ * Production-grade, tamper-evident audit log for enterprise compliance.
7
+ * Every entry is SHA-256 hash-chained to its predecessor, forming a
8
+ * verifiable append-only ledger. Designed for SOC 2 CC7.2 (system
9
+ * monitoring) and CC7.3 (anomaly detection) compliance.
10
+ *
11
+ * Features:
12
+ * - SHA-256 hash chain with genesis block
13
+ * - Tamper detection via full chain verification
14
+ * - Pluggable storage backends (memory, file, custom)
15
+ * - Cryptographic proof export for auditor verification
16
+ * - Retention policies (maxEntries, maxAge, archiveCallback)
17
+ * - Query API with filtering by type, time range, actor, severity
18
+ * - Export in JSON, CSV, JSONL formats
19
+ * - Write serialization (no concurrent write corruption)
20
+ *
21
+ * Zero dependencies — uses Node.js built-in crypto and fs modules.
22
+ */
23
+
24
+ const crypto = require('crypto');
25
+ const fs = require('fs');
26
+ const path = require('path');
27
+
28
+ // =========================================================================
29
+ // CONSTANTS
30
+ // =========================================================================
31
+
32
+ /** @type {string} SHA-256 hash of the string 'AGENT_SHIELD_GENESIS' — deterministic starting point. */
33
+ const GENESIS_HASH = crypto.createHash('sha256').update('AGENT_SHIELD_GENESIS').digest('hex');
34
+
35
+ /** @type {string[]} Valid entry types for the audit log. */
36
+ const ENTRY_TYPES = [
37
+ 'scan_result',
38
+ 'threat_detected',
39
+ 'threat_blocked',
40
+ 'policy_change',
41
+ 'config_change',
42
+ 'auth_event',
43
+ 'manual_review'
44
+ ];
45
+
46
+ // =========================================================================
47
+ // AUDIT ENTRY
48
+ // =========================================================================
49
+
50
+ /**
51
+ * A single immutable entry in the hash-chained audit log.
52
+ */
53
+ class AuditEntry {
54
+ /**
55
+ * @param {object} params
56
+ * @param {string} params.id - Unique entry identifier.
57
+ * @param {string} params.timestamp - ISO 8601 timestamp.
58
+ * @param {string} params.type - Entry type (one of ENTRY_TYPES).
59
+ * @param {object} params.data - Arbitrary payload data.
60
+ * @param {object} params.actor - Who or what triggered this entry.
61
+ * @param {string} params.actor.type - Actor type ('system', 'user', 'agent', 'api').
62
+ * @param {string} params.actor.id - Actor identifier.
63
+ * @param {string} [params.actor.name] - Human-readable actor name.
64
+ * @param {string} params.previousHash - Hash of the preceding entry.
65
+ * @param {string} params.hash - SHA-256 hash of (previousHash + entry data).
66
+ * @param {number} params.sequence - Monotonic sequence number.
67
+ */
68
+ constructor(params) {
69
+ this.id = params.id;
70
+ this.sequence = params.sequence;
71
+ this.timestamp = params.timestamp;
72
+ this.type = params.type;
73
+ this.data = params.data;
74
+ this.actor = params.actor;
75
+ this.previousHash = params.previousHash;
76
+ this.hash = params.hash;
77
+
78
+ // Freeze to prevent mutation after creation.
79
+ Object.freeze(this.actor);
80
+ Object.freeze(this);
81
+ }
82
+
83
+ /**
84
+ * Serialize entry to a plain object suitable for JSON.
85
+ * @returns {object}
86
+ */
87
+ toJSON() {
88
+ return {
89
+ id: this.id,
90
+ sequence: this.sequence,
91
+ timestamp: this.timestamp,
92
+ type: this.type,
93
+ data: this.data,
94
+ actor: this.actor,
95
+ previousHash: this.previousHash,
96
+ hash: this.hash
97
+ };
98
+ }
99
+ }
100
+
101
+ // =========================================================================
102
+ // HASH UTILITIES
103
+ // =========================================================================
104
+
105
+ /**
106
+ * Compute SHA-256 hash for a chain link.
107
+ * The canonical form is: previousHash + JSON-serialized entry content (sorted keys).
108
+ * @param {string} previousHash
109
+ * @param {object} entryContent - { id, sequence, timestamp, type, data, actor }
110
+ * @returns {string} Hex-encoded SHA-256 hash.
111
+ */
112
+ function computeHash(previousHash, entryContent) {
113
+ const canonical = previousHash + canonicalize(entryContent);
114
+ return crypto.createHash('sha256').update(canonical, 'utf8').digest('hex');
115
+ }
116
+
117
+ /**
118
+ * Deterministic JSON serialization with sorted keys for hash stability.
119
+ * @param {*} obj
120
+ * @returns {string}
121
+ */
122
+ function canonicalize(obj) {
123
+ if (obj === null || obj === undefined) return 'null';
124
+ if (typeof obj !== 'object') return JSON.stringify(obj);
125
+ if (Array.isArray(obj)) {
126
+ return '[' + obj.map(canonicalize).join(',') + ']';
127
+ }
128
+ const keys = Object.keys(obj).sort();
129
+ return '{' + keys.map(k => JSON.stringify(k) + ':' + canonicalize(obj[k])).join(',') + '}';
130
+ }
131
+
132
+ /**
133
+ * Standalone chain verification function. Walks the array of entries and
134
+ * checks that every hash is correctly derived from its predecessor.
135
+ * @param {Array<object>} entries - Array of entry objects (or AuditEntry instances).
136
+ * @param {string} [expectedGenesisHash] - Expected hash of the genesis block's previousHash.
137
+ * @returns {{ valid: boolean, length: number, error: string|null, brokenAt: number|null }}
138
+ */
139
+ function verifyChain(entries, expectedGenesisHash) {
140
+ const genesis = expectedGenesisHash || GENESIS_HASH;
141
+
142
+ if (!Array.isArray(entries) || entries.length === 0) {
143
+ return { valid: true, length: 0, error: null, brokenAt: null };
144
+ }
145
+
146
+ for (let i = 0; i < entries.length; i++) {
147
+ const entry = entries[i];
148
+
149
+ // Check previous hash linkage.
150
+ const expectedPrev = i === 0 ? genesis : entries[i - 1].hash;
151
+ if (entry.previousHash !== expectedPrev) {
152
+ return {
153
+ valid: false,
154
+ length: entries.length,
155
+ error: `Entry ${entry.id} (index ${i}): previousHash mismatch. Expected ${expectedPrev}, got ${entry.previousHash}`,
156
+ brokenAt: i
157
+ };
158
+ }
159
+
160
+ // Recompute hash and verify.
161
+ const content = {
162
+ id: entry.id,
163
+ sequence: entry.sequence,
164
+ timestamp: entry.timestamp,
165
+ type: entry.type,
166
+ data: entry.data,
167
+ actor: entry.actor
168
+ };
169
+ const recomputed = computeHash(entry.previousHash, content);
170
+ if (entry.hash !== recomputed) {
171
+ return {
172
+ valid: false,
173
+ length: entries.length,
174
+ error: `Entry ${entry.id} (index ${i}): hash mismatch. Expected ${recomputed}, got ${entry.hash}`,
175
+ brokenAt: i
176
+ };
177
+ }
178
+ }
179
+
180
+ return { valid: true, length: entries.length, error: null, brokenAt: null };
181
+ }
182
+
183
+ // =========================================================================
184
+ // AUDIT PROOF
185
+ // =========================================================================
186
+
187
+ /**
188
+ * Cryptographic proof for a subset of the audit chain.
189
+ * Allows an external auditor to verify a contiguous range of entries
190
+ * without requiring the entire log.
191
+ */
192
+ class AuditProof {
193
+ /**
194
+ * @param {object} params
195
+ * @param {string} params.proofId - Unique proof identifier.
196
+ * @param {string} params.generatedAt - ISO 8601 timestamp of proof generation.
197
+ * @param {string} params.anchorHash - The previousHash of the first entry in the range (chain anchor).
198
+ * @param {Array<object>} params.entries - The entries included in the proof.
199
+ * @param {string} params.startId - First entry ID.
200
+ * @param {string} params.endId - Last entry ID.
201
+ * @param {number} params.entryCount - Number of entries.
202
+ * @param {string} params.chainHead - Hash of the last entry in the proof.
203
+ * @param {string} params.proofHash - SHA-256 hash of the entire proof payload for integrity.
204
+ */
205
+ constructor(params) {
206
+ this.proofId = params.proofId;
207
+ this.generatedAt = params.generatedAt;
208
+ this.anchorHash = params.anchorHash;
209
+ this.entries = params.entries;
210
+ this.startId = params.startId;
211
+ this.endId = params.endId;
212
+ this.entryCount = params.entryCount;
213
+ this.chainHead = params.chainHead;
214
+ this.proofHash = params.proofHash;
215
+ }
216
+
217
+ /**
218
+ * Verify the proof's internal consistency.
219
+ * @returns {{ valid: boolean, error: string|null }}
220
+ */
221
+ verify() {
222
+ // Verify proof hash.
223
+ const payload = canonicalize({
224
+ anchorHash: this.anchorHash,
225
+ entries: this.entries,
226
+ startId: this.startId,
227
+ endId: this.endId,
228
+ entryCount: this.entryCount,
229
+ chainHead: this.chainHead
230
+ });
231
+ const expectedProofHash = crypto.createHash('sha256').update(payload, 'utf8').digest('hex');
232
+ if (this.proofHash !== expectedProofHash) {
233
+ return { valid: false, error: 'Proof envelope hash mismatch — proof itself is tampered' };
234
+ }
235
+
236
+ // Verify the chain within the proof.
237
+ const chainResult = verifyChain(this.entries, this.anchorHash);
238
+ if (!chainResult.valid) {
239
+ return { valid: false, error: chainResult.error };
240
+ }
241
+
242
+ return { valid: true, error: null };
243
+ }
244
+
245
+ /**
246
+ * Serialize proof to JSON.
247
+ * @returns {object}
248
+ */
249
+ toJSON() {
250
+ return {
251
+ proofId: this.proofId,
252
+ generatedAt: this.generatedAt,
253
+ anchorHash: this.anchorHash,
254
+ entries: this.entries,
255
+ startId: this.startId,
256
+ endId: this.endId,
257
+ entryCount: this.entryCount,
258
+ chainHead: this.chainHead,
259
+ proofHash: this.proofHash
260
+ };
261
+ }
262
+ }
263
+
264
+ // =========================================================================
265
+ // STORAGE BACKENDS
266
+ // =========================================================================
267
+
268
+ /**
269
+ * In-memory storage backend. Fast, no persistence.
270
+ * Suitable for testing, development, or short-lived processes.
271
+ */
272
+ class MemoryStore {
273
+ constructor() {
274
+ /** @type {AuditEntry[]} */
275
+ this._entries = [];
276
+ /** @type {Map<string, number>} id -> index */
277
+ this._index = new Map();
278
+ }
279
+
280
+ /**
281
+ * Append an entry.
282
+ * @param {AuditEntry} entry
283
+ */
284
+ async append(entry) {
285
+ this._index.set(entry.id, this._entries.length);
286
+ this._entries.push(entry);
287
+ }
288
+
289
+ /**
290
+ * Get all entries (copy).
291
+ * @returns {AuditEntry[]}
292
+ */
293
+ async getAll() {
294
+ return this._entries.slice();
295
+ }
296
+
297
+ /**
298
+ * Get the last entry in the chain.
299
+ * @returns {AuditEntry|null}
300
+ */
301
+ async getLast() {
302
+ return this._entries.length > 0 ? this._entries[this._entries.length - 1] : null;
303
+ }
304
+
305
+ /**
306
+ * Get entry count.
307
+ * @returns {number}
308
+ */
309
+ async count() {
310
+ return this._entries.length;
311
+ }
312
+
313
+ /**
314
+ * Get entry by ID.
315
+ * @param {string} id
316
+ * @returns {AuditEntry|null}
317
+ */
318
+ async getById(id) {
319
+ const idx = this._index.get(id);
320
+ return idx !== undefined ? this._entries[idx] : null;
321
+ }
322
+
323
+ /**
324
+ * Get entries in a sequence range (inclusive).
325
+ * @param {number} startSeq
326
+ * @param {number} endSeq
327
+ * @returns {AuditEntry[]}
328
+ */
329
+ async getRange(startSeq, endSeq) {
330
+ return this._entries.filter(e => e.sequence >= startSeq && e.sequence <= endSeq);
331
+ }
332
+
333
+ /**
334
+ * Remove entries older than a given timestamp.
335
+ * Returns the removed entries for archiving.
336
+ * @param {string} beforeTimestamp - ISO 8601 timestamp.
337
+ * @returns {AuditEntry[]}
338
+ */
339
+ async removeBefore(beforeTimestamp) {
340
+ const cutoff = new Date(beforeTimestamp).getTime();
341
+ const removed = [];
342
+ const kept = [];
343
+ for (const entry of this._entries) {
344
+ if (new Date(entry.timestamp).getTime() < cutoff) {
345
+ removed.push(entry);
346
+ this._index.delete(entry.id);
347
+ } else {
348
+ kept.push(entry);
349
+ }
350
+ }
351
+ // Rebuild index for kept entries.
352
+ this._entries = kept;
353
+ this._index.clear();
354
+ for (let i = 0; i < kept.length; i++) {
355
+ this._index.set(kept[i].id, i);
356
+ }
357
+ return removed;
358
+ }
359
+
360
+ /**
361
+ * Trim to a maximum number of entries, removing oldest first.
362
+ * Returns the removed entries.
363
+ * @param {number} maxEntries
364
+ * @returns {AuditEntry[]}
365
+ */
366
+ async trimToSize(maxEntries) {
367
+ if (this._entries.length <= maxEntries) return [];
368
+ const removeCount = this._entries.length - maxEntries;
369
+ const removed = this._entries.splice(0, removeCount);
370
+ for (const entry of removed) {
371
+ this._index.delete(entry.id);
372
+ }
373
+ // Rebuild index.
374
+ this._index.clear();
375
+ for (let i = 0; i < this._entries.length; i++) {
376
+ this._index.set(this._entries[i].id, i);
377
+ }
378
+ return removed;
379
+ }
380
+
381
+ /**
382
+ * Clear all entries.
383
+ */
384
+ async clear() {
385
+ this._entries = [];
386
+ this._index.clear();
387
+ }
388
+ }
389
+
390
+ /**
391
+ * Append-only file storage backend. Writes each entry as a JSONL line.
392
+ * On load, reads and verifies the existing chain.
393
+ */
394
+ class FileStore {
395
+ /**
396
+ * @param {object} options
397
+ * @param {string} options.filePath - Path to the JSONL audit log file.
398
+ * @param {boolean} [options.syncWrites=true] - Use synchronous writes for durability.
399
+ */
400
+ constructor(options = {}) {
401
+ if (!options.filePath) {
402
+ throw new Error('[Agent Shield] FileStore requires a filePath option');
403
+ }
404
+ this.filePath = path.resolve(options.filePath);
405
+ this.syncWrites = options.syncWrites !== false;
406
+ /** @type {AuditEntry[]} */
407
+ this._cache = [];
408
+ /** @type {Map<string, number>} */
409
+ this._index = new Map();
410
+ this._loaded = false;
411
+ }
412
+
413
+ /**
414
+ * Load existing entries from the file (idempotent).
415
+ * @returns {Promise<void>}
416
+ */
417
+ async _ensureLoaded() {
418
+ if (this._loaded) return;
419
+ this._loaded = true;
420
+
421
+ if (!fs.existsSync(this.filePath)) return;
422
+
423
+ const content = fs.readFileSync(this.filePath, 'utf8');
424
+ const lines = content.split('\n').filter(line => line.trim().length > 0);
425
+
426
+ for (let i = 0; i < lines.length; i++) {
427
+ try {
428
+ const raw = JSON.parse(lines[i]);
429
+ const entry = new AuditEntry(raw);
430
+ this._index.set(entry.id, this._cache.length);
431
+ this._cache.push(entry);
432
+ } catch (e) {
433
+ console.warn(`[Agent Shield] FileStore: corrupt line ${i + 1} in ${this.filePath}: ${e.message}`);
434
+ }
435
+ }
436
+
437
+ console.log(`[Agent Shield] FileStore: loaded ${this._cache.length} entries from ${this.filePath}`);
438
+ }
439
+
440
+ /**
441
+ * Append an entry to the file and in-memory cache.
442
+ * @param {AuditEntry} entry
443
+ */
444
+ async append(entry) {
445
+ await this._ensureLoaded();
446
+ const line = JSON.stringify(entry.toJSON()) + '\n';
447
+
448
+ if (this.syncWrites) {
449
+ fs.appendFileSync(this.filePath, line, 'utf8');
450
+ } else {
451
+ fs.appendFile(this.filePath, line, 'utf8', (err) => {
452
+ if (err) console.warn('[Agent Shield] FileStore async write error:', err.message);
453
+ });
454
+ }
455
+
456
+ this._index.set(entry.id, this._cache.length);
457
+ this._cache.push(entry);
458
+ }
459
+
460
+ /** @returns {AuditEntry[]} */
461
+ async getAll() {
462
+ await this._ensureLoaded();
463
+ return this._cache.slice();
464
+ }
465
+
466
+ /** @returns {AuditEntry|null} */
467
+ async getLast() {
468
+ await this._ensureLoaded();
469
+ return this._cache.length > 0 ? this._cache[this._cache.length - 1] : null;
470
+ }
471
+
472
+ /** @returns {number} */
473
+ async count() {
474
+ await this._ensureLoaded();
475
+ return this._cache.length;
476
+ }
477
+
478
+ /**
479
+ * @param {string} id
480
+ * @returns {AuditEntry|null}
481
+ */
482
+ async getById(id) {
483
+ await this._ensureLoaded();
484
+ const idx = this._index.get(id);
485
+ return idx !== undefined ? this._cache[idx] : null;
486
+ }
487
+
488
+ /**
489
+ * @param {number} startSeq
490
+ * @param {number} endSeq
491
+ * @returns {AuditEntry[]}
492
+ */
493
+ async getRange(startSeq, endSeq) {
494
+ await this._ensureLoaded();
495
+ return this._cache.filter(e => e.sequence >= startSeq && e.sequence <= endSeq);
496
+ }
497
+
498
+ /**
499
+ * Remove entries before a timestamp. Rewrites the file with remaining entries.
500
+ * @param {string} beforeTimestamp
501
+ * @returns {AuditEntry[]}
502
+ */
503
+ async removeBefore(beforeTimestamp) {
504
+ await this._ensureLoaded();
505
+ const cutoff = new Date(beforeTimestamp).getTime();
506
+ const removed = [];
507
+ const kept = [];
508
+ for (const entry of this._cache) {
509
+ if (new Date(entry.timestamp).getTime() < cutoff) {
510
+ removed.push(entry);
511
+ } else {
512
+ kept.push(entry);
513
+ }
514
+ }
515
+
516
+ if (removed.length > 0) {
517
+ this._cache = kept;
518
+ this._index.clear();
519
+ for (let i = 0; i < kept.length; i++) {
520
+ this._index.set(kept[i].id, i);
521
+ }
522
+ this._rewriteFile();
523
+ }
524
+
525
+ return removed;
526
+ }
527
+
528
+ /**
529
+ * Trim to max entries. Rewrites the file.
530
+ * @param {number} maxEntries
531
+ * @returns {AuditEntry[]}
532
+ */
533
+ async trimToSize(maxEntries) {
534
+ await this._ensureLoaded();
535
+ if (this._cache.length <= maxEntries) return [];
536
+ const removeCount = this._cache.length - maxEntries;
537
+ const removed = this._cache.splice(0, removeCount);
538
+ this._index.clear();
539
+ for (let i = 0; i < this._cache.length; i++) {
540
+ this._index.set(this._cache[i].id, i);
541
+ }
542
+ this._rewriteFile();
543
+ return removed;
544
+ }
545
+
546
+ async clear() {
547
+ this._cache = [];
548
+ this._index.clear();
549
+ if (fs.existsSync(this.filePath)) {
550
+ fs.writeFileSync(this.filePath, '', 'utf8');
551
+ }
552
+ }
553
+
554
+ /** @private Rewrite the file from the in-memory cache. */
555
+ _rewriteFile() {
556
+ const lines = this._cache.map(e => JSON.stringify(e.toJSON())).join('\n');
557
+ const content = lines.length > 0 ? lines + '\n' : '';
558
+ fs.writeFileSync(this.filePath, content, 'utf8');
559
+ }
560
+ }
561
+
562
+ // =========================================================================
563
+ // IMMUTABLE AUDIT LOG
564
+ // =========================================================================
565
+
566
+ /**
567
+ * Immutable, hash-chained audit log for enterprise compliance.
568
+ *
569
+ * Every entry is linked to its predecessor via SHA-256, forming a tamper-evident
570
+ * chain similar to a blockchain. Any modification to any historical entry will
571
+ * break the chain and be detected by verify().
572
+ *
573
+ * @example
574
+ * const log = new ImmutableAuditLog();
575
+ * await log.append('scan_result', { input: '...', status: 'safe' }, { type: 'system', id: 'scanner-1' });
576
+ * const result = await log.verify();
577
+ * console.log(result.valid); // true
578
+ */
579
+ class ImmutableAuditLog {
580
+ /**
581
+ * @param {object} [options]
582
+ * @param {MemoryStore|FileStore|object} [options.store] - Storage backend (must implement append, getAll, getLast, count, getById, getRange, removeBefore, trimToSize, clear).
583
+ * @param {number} [options.maxEntries=0] - Maximum entries to retain (0 = unlimited).
584
+ * @param {number} [options.maxAge=0] - Maximum age in milliseconds (0 = unlimited).
585
+ * @param {function} [options.archiveCallback] - Called with removed entries during retention enforcement. Signature: (entries: AuditEntry[]) => void.
586
+ * @param {string} [options.genesisHash] - Custom genesis hash (defaults to GENESIS_HASH).
587
+ */
588
+ constructor(options = {}) {
589
+ this._store = options.store || new MemoryStore();
590
+ this._maxEntries = options.maxEntries || 0;
591
+ this._maxAge = options.maxAge || 0;
592
+ this._archiveCallback = options.archiveCallback || null;
593
+ this._genesisHash = options.genesisHash || GENESIS_HASH;
594
+ this._sequence = 0;
595
+ this._writeLock = Promise.resolve();
596
+ this._initialized = false;
597
+
598
+ console.log('[Agent Shield] ImmutableAuditLog initialized (store: %s)', this._store.constructor.name);
599
+ }
600
+
601
+ /**
602
+ * Initialize sequence counter from existing store data.
603
+ * @private
604
+ */
605
+ async _ensureInitialized() {
606
+ if (this._initialized) return;
607
+ this._initialized = true;
608
+
609
+ const last = await this._store.getLast();
610
+ if (last) {
611
+ this._sequence = last.sequence;
612
+ }
613
+ }
614
+
615
+ /**
616
+ * Append a new entry to the audit log.
617
+ * This method is serialized — concurrent calls are queued to prevent corruption.
618
+ *
619
+ * @param {string} type - Entry type (one of ENTRY_TYPES, or custom string).
620
+ * @param {object} data - Arbitrary event data payload.
621
+ * @param {object} actor - The actor who triggered this event.
622
+ * @param {string} actor.type - Actor type ('system', 'user', 'agent', 'api').
623
+ * @param {string} actor.id - Actor identifier.
624
+ * @param {string} [actor.name] - Human-readable name.
625
+ * @returns {Promise<AuditEntry>} The appended entry.
626
+ */
627
+ async append(type, data, actor) {
628
+ // Serialize writes through a promise chain.
629
+ const result = this._writeLock.then(async () => {
630
+ await this._ensureInitialized();
631
+
632
+ this._sequence++;
633
+ const timestamp = new Date().toISOString();
634
+ const id = `aud_${Date.now()}_${this._sequence}_${crypto.randomBytes(4).toString('hex')}`;
635
+
636
+ const last = await this._store.getLast();
637
+ const previousHash = last ? last.hash : this._genesisHash;
638
+
639
+ const normalizedActor = {
640
+ type: (actor && actor.type) || 'system',
641
+ id: (actor && actor.id) || 'unknown',
642
+ name: (actor && actor.name) || undefined
643
+ };
644
+ // Strip undefined name to keep canonical form clean.
645
+ if (normalizedActor.name === undefined) {
646
+ delete normalizedActor.name;
647
+ }
648
+
649
+ const entryContent = {
650
+ id,
651
+ sequence: this._sequence,
652
+ timestamp,
653
+ type,
654
+ data: data || {},
655
+ actor: normalizedActor
656
+ };
657
+
658
+ const hash = computeHash(previousHash, entryContent);
659
+
660
+ const entry = new AuditEntry({
661
+ ...entryContent,
662
+ previousHash,
663
+ hash
664
+ });
665
+
666
+ await this._store.append(entry);
667
+
668
+ // Enforce retention policies asynchronously (don't block the append).
669
+ this._enforceRetention().catch(err => {
670
+ console.warn('[Agent Shield] Retention enforcement error:', err.message);
671
+ });
672
+
673
+ return entry;
674
+ });
675
+
676
+ // Update write lock to point at the new tail of the chain.
677
+ this._writeLock = result.then(() => {}, () => {});
678
+
679
+ return result;
680
+ }
681
+
682
+ /**
683
+ * Verify the integrity of the entire chain.
684
+ * Walks every entry from genesis and checks all hashes.
685
+ *
686
+ * @returns {Promise<{ valid: boolean, length: number, error: string|null, brokenAt: number|null }>}
687
+ */
688
+ async verify() {
689
+ const entries = await this._store.getAll();
690
+ return verifyChain(entries.map(e => e.toJSON ? e.toJSON() : e), this._genesisHash);
691
+ }
692
+
693
+ /**
694
+ * Export a cryptographic proof for a contiguous range of entries.
695
+ * The proof includes the anchor hash (the previousHash of the first entry)
696
+ * so an auditor can verify the sub-chain independently.
697
+ *
698
+ * @param {string} startId - ID of the first entry in the range.
699
+ * @param {string} endId - ID of the last entry in the range.
700
+ * @returns {Promise<AuditProof>}
701
+ */
702
+ async exportProof(startId, endId) {
703
+ const allEntries = await this._store.getAll();
704
+ const startIdx = allEntries.findIndex(e => e.id === startId);
705
+ const endIdx = allEntries.findIndex(e => e.id === endId);
706
+
707
+ if (startIdx === -1) throw new Error(`Start entry not found: ${startId}`);
708
+ if (endIdx === -1) throw new Error(`End entry not found: ${endId}`);
709
+ if (startIdx > endIdx) throw new Error('Start entry must come before end entry in the chain');
710
+
711
+ const subset = allEntries.slice(startIdx, endIdx + 1);
712
+ const serialized = subset.map(e => e.toJSON ? e.toJSON() : e);
713
+
714
+ const anchorHash = serialized[0].previousHash;
715
+ const chainHead = serialized[serialized.length - 1].hash;
716
+
717
+ const proofPayload = {
718
+ anchorHash,
719
+ entries: serialized,
720
+ startId,
721
+ endId,
722
+ entryCount: serialized.length,
723
+ chainHead
724
+ };
725
+
726
+ const proofHash = crypto.createHash('sha256')
727
+ .update(canonicalize(proofPayload), 'utf8')
728
+ .digest('hex');
729
+
730
+ return new AuditProof({
731
+ proofId: `proof_${Date.now()}_${crypto.randomBytes(4).toString('hex')}`,
732
+ generatedAt: new Date().toISOString(),
733
+ ...proofPayload,
734
+ proofHash
735
+ });
736
+ }
737
+
738
+ /**
739
+ * Query entries with filtering.
740
+ *
741
+ * @param {object} [filters]
742
+ * @param {string} [filters.type] - Filter by entry type.
743
+ * @param {string} [filters.startTime] - ISO 8601 start time (inclusive).
744
+ * @param {string} [filters.endTime] - ISO 8601 end time (inclusive).
745
+ * @param {string} [filters.actor] - Filter by actor ID.
746
+ * @param {string} [filters.severity] - Filter by data.severity field.
747
+ * @param {number} [filters.limit] - Maximum results to return.
748
+ * @param {number} [filters.offset] - Skip this many results.
749
+ * @returns {Promise<AuditEntry[]>}
750
+ */
751
+ async query(filters = {}) {
752
+ const allEntries = await this._store.getAll();
753
+ const startTime = filters.startTime ? new Date(filters.startTime).getTime() : null;
754
+ const endTime = filters.endTime ? new Date(filters.endTime).getTime() : null;
755
+
756
+ let results = allEntries.filter(entry => {
757
+ if (filters.type && entry.type !== filters.type) return false;
758
+
759
+ if (startTime) {
760
+ const entryTime = new Date(entry.timestamp).getTime();
761
+ if (entryTime < startTime) return false;
762
+ }
763
+ if (endTime) {
764
+ const entryTime = new Date(entry.timestamp).getTime();
765
+ if (entryTime > endTime) return false;
766
+ }
767
+
768
+ if (filters.actor && entry.actor.id !== filters.actor) return false;
769
+
770
+ if (filters.severity && (!entry.data || entry.data.severity !== filters.severity)) return false;
771
+
772
+ return true;
773
+ });
774
+
775
+ if (filters.offset) {
776
+ results = results.slice(filters.offset);
777
+ }
778
+ if (filters.limit) {
779
+ results = results.slice(0, filters.limit);
780
+ }
781
+
782
+ return results;
783
+ }
784
+
785
+ /**
786
+ * Export the entire log in the specified format.
787
+ *
788
+ * @param {'json'|'csv'|'jsonl'} [format='json'] - Output format.
789
+ * @returns {Promise<string>}
790
+ */
791
+ async export(format = 'json') {
792
+ const entries = await this._store.getAll();
793
+ const serialized = entries.map(e => e.toJSON ? e.toJSON() : e);
794
+
795
+ switch (format) {
796
+ case 'json':
797
+ return JSON.stringify(serialized, null, 2);
798
+
799
+ case 'jsonl':
800
+ return serialized.map(e => JSON.stringify(e)).join('\n') + (serialized.length > 0 ? '\n' : '');
801
+
802
+ case 'csv': {
803
+ if (serialized.length === 0) return '';
804
+ const headers = ['id', 'sequence', 'timestamp', 'type', 'actor_type', 'actor_id', 'previousHash', 'hash', 'data'];
805
+ const rows = [headers.join(',')];
806
+ for (const entry of serialized) {
807
+ rows.push([
808
+ entry.id,
809
+ entry.sequence,
810
+ entry.timestamp,
811
+ entry.type,
812
+ entry.actor.type,
813
+ entry.actor.id,
814
+ entry.previousHash,
815
+ entry.hash,
816
+ `"${JSON.stringify(entry.data).replace(/"/g, '""')}"`
817
+ ].join(','));
818
+ }
819
+ return rows.join('\n') + '\n';
820
+ }
821
+
822
+ default:
823
+ throw new Error(`Unsupported export format: ${format}`);
824
+ }
825
+ }
826
+
827
+ /**
828
+ * Get summary statistics for the audit log.
829
+ * @returns {Promise<object>}
830
+ */
831
+ async getStats() {
832
+ const entries = await this._store.getAll();
833
+ const typeCounts = {};
834
+ const actorCounts = {};
835
+
836
+ for (const entry of entries) {
837
+ typeCounts[entry.type] = (typeCounts[entry.type] || 0) + 1;
838
+ actorCounts[entry.actor.id] = (actorCounts[entry.actor.id] || 0) + 1;
839
+ }
840
+
841
+ return {
842
+ totalEntries: entries.length,
843
+ firstEntry: entries.length > 0 ? entries[0].timestamp : null,
844
+ lastEntry: entries.length > 0 ? entries[entries.length - 1].timestamp : null,
845
+ typeCounts,
846
+ actorCounts,
847
+ chainValid: (await this.verify()).valid
848
+ };
849
+ }
850
+
851
+ /**
852
+ * Get the current chain head hash.
853
+ * @returns {Promise<string>}
854
+ */
855
+ async getChainHead() {
856
+ const last = await this._store.getLast();
857
+ return last ? last.hash : this._genesisHash;
858
+ }
859
+
860
+ /**
861
+ * Get the total number of entries.
862
+ * @returns {Promise<number>}
863
+ */
864
+ async count() {
865
+ return this._store.count();
866
+ }
867
+
868
+ /**
869
+ * Enforce retention policies (maxEntries, maxAge).
870
+ * @private
871
+ */
872
+ async _enforceRetention() {
873
+ let archived = [];
874
+
875
+ // Enforce maxAge.
876
+ if (this._maxAge > 0) {
877
+ const cutoff = new Date(Date.now() - this._maxAge).toISOString();
878
+ const removed = await this._store.removeBefore(cutoff);
879
+ if (removed.length > 0) archived = archived.concat(removed);
880
+ }
881
+
882
+ // Enforce maxEntries.
883
+ if (this._maxEntries > 0) {
884
+ const removed = await this._store.trimToSize(this._maxEntries);
885
+ if (removed.length > 0) archived = archived.concat(removed);
886
+ }
887
+
888
+ // Notify archive callback.
889
+ if (archived.length > 0 && this._archiveCallback) {
890
+ try {
891
+ this._archiveCallback(archived);
892
+ } catch (e) {
893
+ console.warn('[Agent Shield] Archive callback error:', e.message);
894
+ }
895
+ }
896
+ }
897
+ }
898
+
899
+ // =========================================================================
900
+ // EXPORTS
901
+ // =========================================================================
902
+
903
+ module.exports = {
904
+ ImmutableAuditLog,
905
+ AuditEntry,
906
+ MemoryStore,
907
+ FileStore,
908
+ AuditProof,
909
+ verifyChain,
910
+ computeHash,
911
+ canonicalize,
912
+ GENESIS_HASH,
913
+ ENTRY_TYPES
914
+ };