@datacules/agent-identity-compliance 0.2.1

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 (3) hide show
  1. package/README.md +188 -0
  2. package/bin/cli.js +319 -0
  3. package/package.json +43 -0
package/README.md ADDED
@@ -0,0 +1,188 @@
1
+ # `@datacules/agent-identity-compliance`
2
+
3
+ Compliance report generation + tamper-evident audit log for [`@datacules/agent-identity`](../../core).
4
+
5
+ Answers regulatory audit questions directly from your audit logs — no custom queries.
6
+ Provides a SHA-256 hash chain logger and CLI verifier for SOC 2, GDPR, and HIPAA evidence.
7
+
8
+ ## Install
9
+
10
+ ```bash
11
+ npm install @datacules/agent-identity-compliance
12
+ ```
13
+
14
+ ## Features
15
+
16
+ | Feature | Description |
17
+ |---------|-------------|
18
+ | `ComplianceReportGenerator` | Generate SOC 2 / GDPR / HIPAA reports from audit logs |
19
+ | `HashChainAuditLogger` | Wraps any audit sink — appends SHA-256 chain fields to every entry |
20
+ | `ChainVerifier` | Replays the chain and returns intact/broken status |
21
+ | CLI `agent-identity audit verify` | Verify a JSONL audit log file from the command line |
22
+ | CLI `agent-identity report` | Generate a compliance report from a JSONL audit log file |
23
+
24
+ ---
25
+
26
+ ## Compliance Reports
27
+
28
+ ```typescript
29
+ import { ComplianceReportGenerator, MemoryReportStore } from '@datacules/agent-identity-compliance';
30
+
31
+ const generator = new ComplianceReportGenerator({
32
+ store: new MemoryReportStore(auditEntries), // or your own ReportStore
33
+ piiTags: ['pii', 'phi', 'personal', 'financial'],
34
+ businessHoursStart: 9,
35
+ businessHoursEnd: 18,
36
+ });
37
+
38
+ // SOC 2 CC6 — Logical and Physical Access Controls
39
+ const report = await generator.generate({
40
+ type: 'soc2',
41
+ from: '2026-01-01T00:00:00Z',
42
+ to: '2026-03-31T23:59:59Z',
43
+ });
44
+
45
+ // GDPR Article 30 — Records of Processing Activities (Markdown output)
46
+ const gdprReport = await generator.generate({
47
+ type: 'gdpr',
48
+ from: '2026-01-01T00:00:00Z',
49
+ to: '2026-03-31T23:59:59Z',
50
+ format: 'markdown',
51
+ });
52
+
53
+ console.log(report.agentAccessSummary); // which agents used which credentials
54
+ console.log(report.piiResourceAccess); // all accesses to PII-tagged resources
55
+ console.log(report.offHoursAccess); // accesses outside business hours
56
+ console.log(report.credentialRotationHistory); // rotation events
57
+ console.log(report.anomalyEvents); // all flagged anomalies
58
+ ```
59
+
60
+ ### Report sections
61
+
62
+ | Section | Description |
63
+ |---------|-------------|
64
+ | `agentAccessSummary` | Per-agent resolution counts, credentials used, resources accessed |
65
+ | `piiResourceAccess` | All resolutions against resources tagged `pii`, `phi`, or `personal` |
66
+ | `offHoursAccess` | Resolutions outside configured business hours (includes weekends) |
67
+ | `credentialRotationHistory` | `credential.rotated` events — when, which credential |
68
+ | `anomalyEvents` | All `credential.anomaly` events with signal and severity |
69
+
70
+ ---
71
+
72
+ ## Tamper-Evident Audit Log (Hash Chain)
73
+
74
+ Wrap any existing audit logger to make every entry part of a SHA-256 linked chain:
75
+
76
+ ```typescript
77
+ import { HashChainAuditLogger } from '@datacules/agent-identity-compliance';
78
+ import { ConsoleAuditLogger } from '@datacules/agent-identity-audit';
79
+ import { createRouter } from '@datacules/agent-identity';
80
+
81
+ // 1. Wrap any existing logger
82
+ const base = new ConsoleAuditLogger();
83
+ const chained = new HashChainAuditLogger(base);
84
+
85
+ // 2. Use the chained logger with the router — everything else is unchanged
86
+ const router = createRouter(credentials, rules, chained);
87
+ ```
88
+
89
+ The underlying sink receives entries with two extra fields:
90
+
91
+ ```json
92
+ {
93
+ "userId": "user-abc",
94
+ "credentialId": "cred-openai",
95
+ "action": "read",
96
+ "timestamp": "2026-05-28T10:00:00.000Z",
97
+ "...": "...",
98
+ "prevHash": "a3f8...",
99
+ "hash": "9c12..."
100
+ }
101
+ ```
102
+
103
+ Any retroactive modification to any field in any entry breaks the chain from that point forward — detectable in O(n) time.
104
+
105
+ ### Verifying the chain programmatically
106
+
107
+ ```typescript
108
+ import { ChainVerifier } from '@datacules/agent-identity-compliance';
109
+ import { readFileSync } from 'node:fs';
110
+
111
+ const jsonl = readFileSync('./audit.jsonl', 'utf8');
112
+ const result = ChainVerifier.verifyJsonl(jsonl);
113
+
114
+ console.log(result.intact); // true / false
115
+ console.log(result.entryCount); // number of entries verified
116
+ console.log(result.rootHash); // SHA-256 of the last entry (publish to an anchor)
117
+ console.log(result.brokenAt); // entry index of first broken link (null if intact)
118
+ console.log(result.brokenReason); // human-readable reason (null if intact)
119
+ ```
120
+
121
+ ---
122
+
123
+ ## CLI
124
+
125
+ The package ships a zero-dependency CLI (`agent-identity`) for offline log verification and report generation.
126
+
127
+ ### Verify an audit log
128
+
129
+ ```bash
130
+ agent-identity audit verify --file ./audit.jsonl
131
+ ```
132
+
133
+ Output:
134
+ ```
135
+ Audit log verification — /path/to/audit.jsonl
136
+ Entries verified : 47382
137
+ Chain status : ✅ INTACT
138
+ Chain root hash : 9c12a3f8...b4e2
139
+ ```
140
+
141
+ If a line has been modified:
142
+ ```
143
+ Chain status : ❌ BROKEN
144
+ Broken at entry : 1204
145
+ Reason : Entry 1204: hash mismatch — entry data appears to have been modified
146
+ ```
147
+
148
+ Exit code 0 = intact, exit code 1 = broken or empty. Suitable for CI gates:
149
+
150
+ ```bash
151
+ agent-identity audit verify --file ./audit.jsonl || { echo "Audit log tampered!"; exit 1; }
152
+ ```
153
+
154
+ ### Generate a compliance report
155
+
156
+ ```bash
157
+ # SOC 2 CC6 — JSON output (default)
158
+ agent-identity report soc2 --file ./audit.jsonl
159
+
160
+ # GDPR Article 30 — Markdown, filtered to Q1 2026
161
+ agent-identity report gdpr \
162
+ --file ./audit.jsonl \
163
+ --from 2026-01-01 \
164
+ --to 2026-03-31 \
165
+ --format markdown
166
+
167
+ # HIPAA §164.312 — save to file
168
+ agent-identity report hipaa --file ./audit.jsonl > ./reports/hipaa-q2.json
169
+ ```
170
+
171
+ ---
172
+
173
+ ## Custom ReportStore
174
+
175
+ ```typescript
176
+ import type { ReportStore } from '@datacules/agent-identity-compliance';
177
+
178
+ class PostgresReportStore implements ReportStore {
179
+ async queryEntries(from: string, to: string) {
180
+ return db.query(
181
+ 'SELECT * FROM audit_log WHERE timestamp BETWEEN $1 AND $2 ORDER BY timestamp ASC',
182
+ [from, to]
183
+ );
184
+ }
185
+ }
186
+
187
+ const generator = new ComplianceReportGenerator({ store: new PostgresReportStore() });
188
+ ```
package/bin/cli.js ADDED
@@ -0,0 +1,319 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * agent-identity CLI
4
+ *
5
+ * Commands:
6
+ *
7
+ * audit verify --file <path> [--quiet]
8
+ * Verify the SHA-256 chain of a JSONL audit log file.
9
+ * Exit 0 if intact, exit 1 if broken or empty.
10
+ *
11
+ * report <type> --file <path> [--from <ISO>] [--to <ISO>] [--format json|markdown]
12
+ * Generate a compliance report from a JSONL audit log file.
13
+ * <type> must be one of: soc2 | gdpr | hipaa | custom
14
+ * Output is written to stdout.
15
+ *
16
+ * Examples:
17
+ *
18
+ * agent-identity audit verify --file ./audit.jsonl
19
+ * agent-identity report soc2 --file ./audit.jsonl --format markdown
20
+ * agent-identity report gdpr --file ./audit.jsonl --from 2026-01-01 --to 2026-03-31
21
+ */
22
+ 'use strict';
23
+
24
+ const fs = require('node:fs');
25
+ const path = require('node:path');
26
+
27
+ // ─── Argument parsing ────────────────────────────────────────────────────────────
28
+
29
+ function parseArgs(argv) {
30
+ const args = {};
31
+ const positional = [];
32
+ for (let i = 0; i < argv.length; i++) {
33
+ const arg = argv[i];
34
+ if (arg.startsWith('--')) {
35
+ const key = arg.slice(2);
36
+ const next = argv[i + 1];
37
+ if (next && !next.startsWith('--')) {
38
+ args[key] = next;
39
+ i++;
40
+ } else {
41
+ args[key] = true;
42
+ }
43
+ } else {
44
+ positional.push(arg);
45
+ }
46
+ }
47
+ return { args, positional };
48
+ }
49
+
50
+ // ─── Helpers ─────────────────────────────────────────────────────────────────────
51
+
52
+ function die(msg) {
53
+ process.stderr.write(`\nError: ${msg}\n\n`);
54
+ printHelp();
55
+ process.exit(1);
56
+ }
57
+
58
+ function printHelp() {
59
+ process.stdout.write(`
60
+ agent-identity — Datacules LLC
61
+
62
+ Usage:
63
+ agent-identity audit verify --file <path> [--quiet]
64
+ agent-identity report <type> --file <path> [--from <ISO>] [--to <ISO>] [--format json|markdown]
65
+
66
+ Commands:
67
+ audit verify Verify the SHA-256 hash chain of a JSONL audit log file.
68
+ Exits 0 if intact, 1 if broken or empty.
69
+
70
+ report <type> Generate a compliance report from a JSONL audit log.
71
+ type: soc2 | gdpr | hipaa | custom
72
+
73
+ Options:
74
+ --file <path> Path to the JSONL audit log file (required)
75
+ --from <ISO> Report period start (ISO 8601, default: beginning of log)
76
+ --to <ISO> Report period end (ISO 8601, default: end of log)
77
+ --format Output format for report: json (default) | markdown
78
+ --quiet Suppress progress output (audit verify only)
79
+
80
+ Examples:
81
+ agent-identity audit verify --file ./audit.jsonl
82
+ agent-identity report soc2 --file ./audit.jsonl --format markdown
83
+ agent-identity report gdpr --file ./audit.jsonl --from 2026-01-01 --to 2026-06-30
84
+ `);
85
+ }
86
+
87
+ function readJsonlFile(filePath) {
88
+ const resolved = path.resolve(filePath);
89
+ if (!fs.existsSync(resolved)) {
90
+ die(`File not found: ${resolved}`);
91
+ }
92
+ return fs.readFileSync(resolved, 'utf8');
93
+ }
94
+
95
+ // ─── SHA-256 chain verification (inline — mirrors ChainVerifier from index.ts) ──
96
+ // We inline a plain-JS version so the CLI works without requiring the TS build.
97
+
98
+ const { createHash } = require('node:crypto');
99
+
100
+ function computeEntryHash(entry, prevHash) {
101
+ const { hash: _h, prevHash: _p, ...coreFields } = entry;
102
+ void _h; void _p;
103
+ const sortedKeys = Object.keys(coreFields).sort();
104
+ const payload = {};
105
+ for (const k of sortedKeys) payload[k] = coreFields[k];
106
+ return createHash('sha256')
107
+ .update(JSON.stringify(payload) + prevHash)
108
+ .digest('hex');
109
+ }
110
+
111
+ function verifyJsonl(jsonl) {
112
+ const entries = [];
113
+ const lines = jsonl.split('\n');
114
+ for (let i = 0; i < lines.length; i++) {
115
+ const line = lines[i].trim();
116
+ if (!line) continue;
117
+ try {
118
+ entries.push(JSON.parse(line));
119
+ } catch {
120
+ return {
121
+ intact: false,
122
+ entryCount: entries.length,
123
+ rootHash: entries[entries.length - 1]?.hash ?? null,
124
+ brokenAt: entries.length,
125
+ brokenReason: `Line ${i + 1}: failed to parse as JSON — log file may be corrupted`,
126
+ };
127
+ }
128
+ }
129
+
130
+ if (entries.length === 0) {
131
+ return { intact: false, entryCount: 0, rootHash: null, brokenAt: null, brokenReason: 'Log is empty — nothing to verify' };
132
+ }
133
+
134
+ let prevHash = '';
135
+ for (let i = 0; i < entries.length; i++) {
136
+ const entry = entries[i];
137
+ if (entry.prevHash !== prevHash) {
138
+ return {
139
+ intact: false,
140
+ entryCount: entries.length,
141
+ rootHash: entries[i - 1]?.hash ?? null,
142
+ brokenAt: i,
143
+ brokenReason: `Entry ${i}: prevHash mismatch — expected ${prevHash.slice(0, 16)}… got ${String(entry.prevHash).slice(0, 16)}…`,
144
+ };
145
+ }
146
+ const expected = computeEntryHash(entry, prevHash);
147
+ if (entry.hash !== expected) {
148
+ return {
149
+ intact: false,
150
+ entryCount: entries.length,
151
+ rootHash: entries[i - 1]?.hash ?? null,
152
+ brokenAt: i,
153
+ brokenReason: `Entry ${i}: hash mismatch — entry data appears to have been modified`,
154
+ };
155
+ }
156
+ prevHash = entry.hash;
157
+ }
158
+
159
+ return { intact: true, entryCount: entries.length, rootHash: prevHash, brokenAt: null, brokenReason: null };
160
+ }
161
+
162
+ // ─── Compliance report (inline — mirrors ComplianceReportGenerator) ─────────────
163
+
164
+ function isOffHours(timestamp, startHour = 9, endHour = 18) {
165
+ const d = new Date(timestamp);
166
+ const hour = d.getUTCHours();
167
+ const day = d.getUTCDay();
168
+ if (day === 0 || day === 6) return true;
169
+ return hour < startHour || hour >= endHour;
170
+ }
171
+
172
+ function generateReport(type, entries, format) {
173
+ const agentMap = new Map();
174
+ const piiAccess = [];
175
+ const offHours = [];
176
+ const rotations = [];
177
+ const anomalies = [];
178
+ const piiTags = ['pii', 'phi', 'personal'];
179
+
180
+ for (const e of entries) {
181
+ if (!e.action.startsWith('credential.')) {
182
+ let s = agentMap.get(e.userId);
183
+ if (!s) {
184
+ s = { userId: e.userId, credentialIds: [], resourceIds: [], actionCounts: {}, resolutionCount: 0, firstSeen: e.timestamp, lastSeen: e.timestamp };
185
+ agentMap.set(e.userId, s);
186
+ }
187
+ if (!s.credentialIds.includes(e.credentialId)) s.credentialIds.push(e.credentialId);
188
+ if (!s.resourceIds.includes(e.resourceId)) s.resourceIds.push(e.resourceId);
189
+ s.actionCounts[e.action] = (s.actionCounts[e.action] ?? 0) + 1;
190
+ s.resolutionCount += 1;
191
+ if (e.timestamp < s.firstSeen) s.firstSeen = e.timestamp;
192
+ if (e.timestamp > s.lastSeen) s.lastSeen = e.timestamp;
193
+
194
+ if (piiTags.some(tag => e.resourceId.toLowerCase().includes(tag))) piiAccess.push(e);
195
+ if (isOffHours(e.timestamp)) offHours.push({ timestamp: e.timestamp, userId: e.userId, action: e.action, resourceId: e.resourceId, credentialId: e.credentialId });
196
+ }
197
+ if (e.action === 'credential.rotated') rotations.push({ credentialId: e.credentialId, rotatedAt: e.timestamp, triggeredBy: e.userId });
198
+ if (e.action === 'credential.anomaly') anomalies.push({ timestamp: e.timestamp, userId: e.userId, credentialId: e.credentialId, signal: e.signal ?? 'unknown', severity: e.severity ?? 'unknown' });
199
+ }
200
+
201
+ const summary = Array.from(agentMap.values()).sort((a, b) => b.resolutionCount - a.resolutionCount);
202
+
203
+ const report = {
204
+ type,
205
+ generatedAt: new Date().toISOString(),
206
+ periodFrom: entries[0]?.timestamp ?? 'n/a',
207
+ periodTo: entries[entries.length - 1]?.timestamp ?? 'n/a',
208
+ agentAccessSummary: summary,
209
+ piiResourceAccess: piiAccess,
210
+ offHoursAccess: offHours,
211
+ credentialRotationHistory: rotations,
212
+ anomalyEvents: anomalies,
213
+ totalEntries: entries.length,
214
+ summary: `${type.toUpperCase()} report: ${entries.length} total resolutions, ${piiAccess.length} PII accesses, ${offHours.length} off-hours accesses, ${anomalies.length} anomaly events`,
215
+ };
216
+
217
+ if (format === 'markdown') {
218
+ const lines = [
219
+ `# Agent Identity — ${type.toUpperCase()} Compliance Report`,
220
+ `**Period:** ${report.periodFrom} – ${report.periodTo}`,
221
+ `**Generated:** ${report.generatedAt}`,
222
+ `**Total resolutions:** ${report.totalEntries}`,
223
+ '',
224
+ '## Agent Access Summary',
225
+ '| Agent | Resolutions | Credentials | Resources | First Seen | Last Seen |',
226
+ '|-------|-------------|-------------|-----------|------------|-----------|',
227
+ ...summary.map(a => `| ${a.userId} | ${a.resolutionCount} | ${a.credentialIds.length} | ${a.resourceIds.length} | ${a.firstSeen.slice(0,10)} | ${a.lastSeen.slice(0,10)} |`),
228
+ '',
229
+ `## PII Resource Access (${piiAccess.length} events)`,
230
+ piiAccess.length === 0 ? '_None_' : piiAccess.map(e => `- ${e.timestamp} | ${e.userId} | ${e.action} | ${e.resourceId}`).join('\n'),
231
+ '',
232
+ `## Off-Hours Access (${offHours.length} events)`,
233
+ offHours.length === 0 ? '_None_' : offHours.map(e => `- ${e.timestamp} | ${e.userId} | ${e.action} | ${e.resourceId}`).join('\n'),
234
+ '',
235
+ `## Credential Rotation History (${rotations.length} rotations)`,
236
+ rotations.length === 0 ? '_None_' : rotations.map(r => `- ${r.rotatedAt} | ${r.credentialId} | triggered by ${r.triggeredBy}`).join('\n'),
237
+ '',
238
+ `## Anomaly Events (${anomalies.length} events)`,
239
+ anomalies.length === 0 ? '_None_' : anomalies.map(a => `- ${a.timestamp} | ${a.userId} | ${a.credentialId} | ${a.signal} (${a.severity})`).join('\n'),
240
+ ];
241
+ return lines.join('\n');
242
+ }
243
+
244
+ return JSON.stringify(report, null, 2);
245
+ }
246
+
247
+ // ─── Main ────────────────────────────────────────────────────────────────────────
248
+
249
+ const argv = process.argv.slice(2);
250
+ const { args, positional } = parseArgs(argv);
251
+
252
+ if (positional.length === 0 || positional[0] === 'help' || args.help || args.h) {
253
+ printHelp();
254
+ process.exit(0);
255
+ }
256
+
257
+ const command = positional[0];
258
+ const subCommand = positional[1];
259
+
260
+ // ── agent-identity audit verify ──────────────────────────────────────────────────
261
+ if (command === 'audit' && subCommand === 'verify') {
262
+ if (!args.file) die('--file <path> is required');
263
+
264
+ const content = readJsonlFile(args.file);
265
+ const result = verifyJsonl(content);
266
+
267
+ if (!args.quiet) {
268
+ process.stdout.write(`\nAudit log verification — ${path.resolve(args.file)}\n`);
269
+ process.stdout.write(`Entries verified : ${result.entryCount}\n`);
270
+ process.stdout.write(`Chain status : ${result.intact ? '\u2705 INTACT' : '\u274C BROKEN'}\n`);
271
+ if (result.rootHash) {
272
+ process.stdout.write(`Chain root hash : ${result.rootHash}\n`);
273
+ }
274
+ if (!result.intact) {
275
+ process.stdout.write(`Broken at entry : ${result.brokenAt ?? 'n/a'}\n`);
276
+ process.stdout.write(`Reason : ${result.brokenReason}\n`);
277
+ }
278
+ process.stdout.write('\n');
279
+ }
280
+
281
+ process.exit(result.intact ? 0 : 1);
282
+ }
283
+
284
+ // ── agent-identity report <type> ────────────────────────────────────────────────
285
+ if (command === 'report') {
286
+ const reportType = subCommand;
287
+ if (!reportType || !['soc2', 'gdpr', 'hipaa', 'custom'].includes(reportType)) {
288
+ die(`report type must be one of: soc2 | gdpr | hipaa | custom (got: ${reportType ?? 'none'})`);
289
+ }
290
+ if (!args.file) die('--file <path> is required');
291
+
292
+ const content = readJsonlFile(args.file);
293
+ const lines = content.split('\n').filter(l => l.trim());
294
+ let entries;
295
+ try {
296
+ entries = lines.map(l => JSON.parse(l));
297
+ } catch (err) {
298
+ die(`Failed to parse JSONL file: ${err.message}`);
299
+ }
300
+
301
+ // Filter by date range if provided
302
+ let filtered = entries;
303
+ if (args.from) {
304
+ const start = new Date(args.from).getTime();
305
+ filtered = filtered.filter(e => new Date(e.timestamp).getTime() >= start);
306
+ }
307
+ if (args.to) {
308
+ const end = new Date(args.to).getTime();
309
+ filtered = filtered.filter(e => new Date(e.timestamp).getTime() <= end);
310
+ }
311
+
312
+ const format = args.format === 'markdown' ? 'markdown' : 'json';
313
+ const output = generateReport(reportType, filtered, format);
314
+ process.stdout.write(output + '\n');
315
+ process.exit(0);
316
+ }
317
+
318
+ // ── Unknown command ──────────────────────────────────────────────────────────────
319
+ die(`Unknown command: ${command} ${subCommand ?? ''}`.trim());
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@datacules/agent-identity-compliance",
3
+ "version": "0.2.1",
4
+ "private": false,
5
+ "description": "Compliance report generator + tamper-evident audit log for @datacules/agent-identity — SOC 2, GDPR, HIPAA reports, SHA-256 chain verification CLI",
6
+ "author": "Datacules LLC",
7
+ "license": "MIT",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/hvrcharon1/agent-identity.git",
11
+ "directory": "packages/integrations/compliance"
12
+ },
13
+ "main": "./dist/cjs/index.js",
14
+ "module": "./dist/esm/index.js",
15
+ "types": "./dist/types/index.d.ts",
16
+ "bin": {
17
+ "agent-identity": "./bin/cli.js"
18
+ },
19
+ "exports": {
20
+ ".": {
21
+ "import": "./dist/esm/index.js",
22
+ "require": "./dist/cjs/index.js",
23
+ "types": "./dist/types/index.d.ts"
24
+ }
25
+ },
26
+ "files": [
27
+ "dist",
28
+ "bin",
29
+ "README.md"
30
+ ],
31
+ "scripts": {
32
+ "build": "tsc -p tsconfig.build.json",
33
+ "type-check": "tsc --noEmit",
34
+ "lint": "eslint src --ext .ts"
35
+ },
36
+ "devDependencies": {
37
+ "@types/node": "^20",
38
+ "typescript": "^5"
39
+ },
40
+ "peerDependencies": {
41
+ "@datacules/agent-identity": "^0.1.0"
42
+ }
43
+ }