@datacules/agent-identity-audit 0.11.0 → 0.11.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.
- package/LICENSE +109 -0
- package/dist/cjs/index.js +215 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/esm/index.js +171 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/types/index.d.ts +121 -0
- package/dist/types/index.d.ts.map +1 -0
- package/package.json +32 -3
- package/src/audit.test.ts +0 -213
- package/src/index.ts +0 -258
package/LICENSE
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
Datacules Agent Identity License — Version 1.0
|
|
2
|
+
Copyright (c) 2026 Datacules LLC. All rights reserved.
|
|
3
|
+
|
|
4
|
+
─────────────────────────────────────────────────────────────────────────────
|
|
5
|
+
PREAMBLE
|
|
6
|
+
─────────────────────────────────────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
This software — Agent Identity & Auth Patterns — is developed and owned by
|
|
9
|
+
Datacules LLC. It is made available to the public as open-source software
|
|
10
|
+
under the permissive terms below.
|
|
11
|
+
|
|
12
|
+
Datacules LLC retains ownership and authorship of this software while
|
|
13
|
+
granting broad, royalty-free rights for anyone to use, copy, modify, and
|
|
14
|
+
distribute it — in commercial or non-commercial contexts — without requiring
|
|
15
|
+
that derivative works also become open source.
|
|
16
|
+
|
|
17
|
+
─────────────────────────────────────────────────────────────────────────────
|
|
18
|
+
TERMS AND CONDITIONS
|
|
19
|
+
─────────────────────────────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
1. PERMISSION TO USE
|
|
22
|
+
|
|
23
|
+
Permission is hereby granted, free of charge, to any person or
|
|
24
|
+
organization obtaining a copy of this software and associated
|
|
25
|
+
documentation files (the "Software"), to use, copy, modify, merge,
|
|
26
|
+
publish, distribute, sublicense, and/or sell copies of the Software,
|
|
27
|
+
and to permit persons to whom the Software is furnished to do so,
|
|
28
|
+
subject to the conditions below.
|
|
29
|
+
|
|
30
|
+
2. ATTRIBUTION
|
|
31
|
+
|
|
32
|
+
a. Redistributions of source code must retain this copyright notice,
|
|
33
|
+
this list of conditions, and the disclaimer below.
|
|
34
|
+
|
|
35
|
+
b. Redistributions in binary form or as a product must reproduce this
|
|
36
|
+
copyright notice, this list of conditions, and the disclaimer in the
|
|
37
|
+
documentation and/or other materials provided with the distribution.
|
|
38
|
+
|
|
39
|
+
c. Neither the name "Datacules LLC" nor the names of its contributors
|
|
40
|
+
may be used to endorse or promote products derived from this Software
|
|
41
|
+
without prior written permission from Datacules LLC.
|
|
42
|
+
|
|
43
|
+
3. COMMERCIAL USE
|
|
44
|
+
|
|
45
|
+
Use of this Software in commercial products, SaaS platforms, internal
|
|
46
|
+
enterprise tools, or any revenue-generating context is explicitly
|
|
47
|
+
permitted without royalty, fee, or additional licensing agreement,
|
|
48
|
+
provided that the conditions in Section 2 (Attribution) are met.
|
|
49
|
+
|
|
50
|
+
4. NO COPYLEFT / NO VIRAL REQUIREMENT
|
|
51
|
+
|
|
52
|
+
This license does NOT require that derivative works, modifications,
|
|
53
|
+
or software that uses or embeds this Software be made open source.
|
|
54
|
+
You may incorporate this Software into proprietary or closed-source
|
|
55
|
+
products under your own license terms.
|
|
56
|
+
|
|
57
|
+
5. MODIFICATIONS
|
|
58
|
+
|
|
59
|
+
Modified versions of the Software may be distributed under the same
|
|
60
|
+
terms as this license or under any other permissive open-source
|
|
61
|
+
license (e.g. MIT, Apache 2.0, BSD), provided that:
|
|
62
|
+
|
|
63
|
+
a. The original copyright notice of Datacules LLC is preserved.
|
|
64
|
+
b. Modifications are clearly documented and distinguished from the
|
|
65
|
+
original work.
|
|
66
|
+
|
|
67
|
+
6. COMPATIBILITY
|
|
68
|
+
|
|
69
|
+
This license is compatible with other permissive open-source licenses
|
|
70
|
+
such as MIT, BSD 2-Clause, BSD 3-Clause, and Apache License 2.0. It
|
|
71
|
+
is also GPL-compatible — this Software may coexist with GPL-licensed
|
|
72
|
+
code, though this Software itself is not distributed under the GPL.
|
|
73
|
+
|
|
74
|
+
─────────────────────────────────────────────────────────────────────────────
|
|
75
|
+
DISCLAIMER
|
|
76
|
+
─────────────────────────────────────────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
THIS SOFTWARE IS PROVIDED BY DATACULES LLC AND CONTRIBUTORS "AS IS" AND
|
|
79
|
+
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
80
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE,
|
|
81
|
+
AND NON-INFRINGEMENT ARE DISCLAIMED.
|
|
82
|
+
|
|
83
|
+
IN NO EVENT SHALL DATACULES LLC OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
|
|
84
|
+
INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
|
85
|
+
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
|
86
|
+
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
|
87
|
+
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
88
|
+
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
|
|
89
|
+
THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
90
|
+
|
|
91
|
+
─────────────────────────────────────────────────────────────────────────────
|
|
92
|
+
SUMMARY (non-binding)
|
|
93
|
+
─────────────────────────────────────────────────────────────────────────────
|
|
94
|
+
|
|
95
|
+
✔ Use freely — commercial, proprietary, or open-source projects
|
|
96
|
+
✔ Modify and distribute with or without changes
|
|
97
|
+
✔ Sell products built on this Software
|
|
98
|
+
✔ No royalties or fees
|
|
99
|
+
✔ No requirement to open-source your own code
|
|
100
|
+
✔ Attribution to Datacules LLC required in source and binary distributions
|
|
101
|
+
✗ Do not use "Datacules LLC" to endorse derived products without permission
|
|
102
|
+
|
|
103
|
+
─────────────────────────────────────────────────────────────────────────────
|
|
104
|
+
CONTACT
|
|
105
|
+
─────────────────────────────────────────────────────────────────────────────
|
|
106
|
+
|
|
107
|
+
Datacules LLC
|
|
108
|
+
For licensing enquiries: legal@datacules.com
|
|
109
|
+
Product: https://github.com/hvrcharon1/agent-identity
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.HashChainAuditLogger = exports.S3ChainAnchor = exports.StdoutChainAnchor = exports.CompositeAuditLogger = exports.SplunkAuditLogger = exports.DatadogAuditLogger = exports.WebhookAuditLogger = exports.ConsoleAuditLogger = void 0;
|
|
37
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
38
|
+
// Existing sinks
|
|
39
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
40
|
+
class ConsoleAuditLogger {
|
|
41
|
+
async log(entry) {
|
|
42
|
+
console.log('[agent-identity audit]', JSON.stringify(entry, null, 2));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
exports.ConsoleAuditLogger = ConsoleAuditLogger;
|
|
46
|
+
class WebhookAuditLogger {
|
|
47
|
+
constructor(options) {
|
|
48
|
+
this.options = { secret: '', timeoutMs: 5000, silent: true, ...options };
|
|
49
|
+
}
|
|
50
|
+
async log(entry) {
|
|
51
|
+
const { url, secret, timeoutMs, silent } = this.options;
|
|
52
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
53
|
+
if (secret)
|
|
54
|
+
headers['X-Webhook-Secret'] = secret;
|
|
55
|
+
const controller = new AbortController();
|
|
56
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
57
|
+
try {
|
|
58
|
+
await fetch(url, { method: 'POST', headers, body: JSON.stringify(entry), signal: controller.signal });
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
if (!silent)
|
|
62
|
+
throw err;
|
|
63
|
+
console.warn('[agent-identity] WebhookAuditLogger failed:', err);
|
|
64
|
+
}
|
|
65
|
+
finally {
|
|
66
|
+
clearTimeout(timer);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
exports.WebhookAuditLogger = WebhookAuditLogger;
|
|
71
|
+
class DatadogAuditLogger {
|
|
72
|
+
constructor(options) {
|
|
73
|
+
this.options = { service: 'agent-identity', site: 'datadoghq.com', silent: true, ...options };
|
|
74
|
+
}
|
|
75
|
+
async log(entry) {
|
|
76
|
+
const { apiKey, service, site, silent } = this.options;
|
|
77
|
+
try {
|
|
78
|
+
await fetch(`https://http-intake.logs.${site}/api/v2/logs`, {
|
|
79
|
+
method: 'POST',
|
|
80
|
+
headers: { 'DD-API-KEY': apiKey, 'Content-Type': 'application/json' },
|
|
81
|
+
body: JSON.stringify({ ddsource: 'agent-identity', service, message: JSON.stringify(entry) }),
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
catch (err) {
|
|
85
|
+
if (!silent)
|
|
86
|
+
throw err;
|
|
87
|
+
console.warn('[agent-identity] DatadogAuditLogger failed:', err);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
exports.DatadogAuditLogger = DatadogAuditLogger;
|
|
92
|
+
class SplunkAuditLogger {
|
|
93
|
+
constructor(options) {
|
|
94
|
+
this.options = { sourcetype: 'agent_identity', silent: true, ...options };
|
|
95
|
+
}
|
|
96
|
+
async log(entry) {
|
|
97
|
+
const { hecUrl, token, sourcetype, silent } = this.options;
|
|
98
|
+
try {
|
|
99
|
+
await fetch(hecUrl, {
|
|
100
|
+
method: 'POST',
|
|
101
|
+
headers: { Authorization: `Splunk ${token}`, 'Content-Type': 'application/json' },
|
|
102
|
+
body: JSON.stringify({ event: entry, sourcetype }),
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
catch (err) {
|
|
106
|
+
if (!silent)
|
|
107
|
+
throw err;
|
|
108
|
+
console.warn('[agent-identity] SplunkAuditLogger failed:', err);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
exports.SplunkAuditLogger = SplunkAuditLogger;
|
|
113
|
+
class CompositeAuditLogger {
|
|
114
|
+
constructor(loggers) {
|
|
115
|
+
this.loggers = loggers;
|
|
116
|
+
}
|
|
117
|
+
async log(entry) {
|
|
118
|
+
await Promise.allSettled(this.loggers.map((l) => l.log(entry)));
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
exports.CompositeAuditLogger = CompositeAuditLogger;
|
|
122
|
+
/** Prints the chain root to stdout — suitable for piping to a CI artifact */
|
|
123
|
+
class StdoutChainAnchor {
|
|
124
|
+
async publish(rootHash, sequence, timestamp) {
|
|
125
|
+
console.log(`[agent-identity chain-anchor] seq=${sequence} root=${rootHash} ts=${timestamp}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
exports.StdoutChainAnchor = StdoutChainAnchor;
|
|
129
|
+
class S3ChainAnchor {
|
|
130
|
+
constructor(opts) {
|
|
131
|
+
this.opts = opts;
|
|
132
|
+
}
|
|
133
|
+
async publish(rootHash, sequence, timestamp) {
|
|
134
|
+
const key = `agent-identity/chain-roots/${sequence}-${timestamp}.json`;
|
|
135
|
+
const body = JSON.stringify({ rootHash, sequence, timestamp, publishedAt: new Date().toISOString() });
|
|
136
|
+
// S3 PutObject — in production use @aws-sdk/client-s3
|
|
137
|
+
const url = `https://${this.opts.bucketName}.s3.${this.opts.region}.amazonaws.com/${key}`;
|
|
138
|
+
await fetch(url, {
|
|
139
|
+
method: 'PUT',
|
|
140
|
+
headers: { 'Content-Type': 'application/json', 'Content-Length': String(body.length) },
|
|
141
|
+
body,
|
|
142
|
+
}).catch((err) => console.warn('[S3ChainAnchor] publish failed:', err));
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
exports.S3ChainAnchor = S3ChainAnchor;
|
|
146
|
+
/**
|
|
147
|
+
* Wraps any AuditLogger with tamper-evident SHA-256 hash chaining.
|
|
148
|
+
*
|
|
149
|
+
* Each entry is hashed together with the hash of the previous entry.
|
|
150
|
+
* Any modification to a historical entry breaks the chain from that
|
|
151
|
+
* point forward — detectable by recomputing and comparing hashes.
|
|
152
|
+
*
|
|
153
|
+
* @example
|
|
154
|
+
* const logger = new HashChainAuditLogger({
|
|
155
|
+
* sink: new DatadogAuditLogger({ apiKey: process.env.DD_API_KEY! }),
|
|
156
|
+
* anchor: new S3ChainAnchor({ bucketName: 'my-audit-anchors', region: 'us-east-1' }),
|
|
157
|
+
* });
|
|
158
|
+
*/
|
|
159
|
+
class HashChainAuditLogger {
|
|
160
|
+
constructor(opts) {
|
|
161
|
+
this.opts = opts;
|
|
162
|
+
this.sequence = 0;
|
|
163
|
+
this.previousHash = opts.seedHash ?? '0';
|
|
164
|
+
this.anchorEveryN = opts.anchorEveryN ?? 1000;
|
|
165
|
+
}
|
|
166
|
+
async log(entry) {
|
|
167
|
+
this.sequence += 1;
|
|
168
|
+
const entryHash = await this.sha256(JSON.stringify(entry));
|
|
169
|
+
const chainHash = await this.sha256(`${this.previousHash}:${entryHash}`);
|
|
170
|
+
const chained = {
|
|
171
|
+
...entry,
|
|
172
|
+
entryHash,
|
|
173
|
+
previousHash: this.previousHash,
|
|
174
|
+
sequence: this.sequence,
|
|
175
|
+
};
|
|
176
|
+
this.previousHash = chainHash;
|
|
177
|
+
await this.opts.sink.log(chained);
|
|
178
|
+
if (this.opts.anchor && this.sequence % this.anchorEveryN === 0) {
|
|
179
|
+
await this.opts.anchor
|
|
180
|
+
.publish(chainHash, this.sequence, new Date().toISOString())
|
|
181
|
+
.catch(console.error);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
/** Verify an ordered array of chained entries; returns result with first broken sequence if any */
|
|
185
|
+
async verify(entries) {
|
|
186
|
+
let prevHash = this.opts.seedHash ?? '0';
|
|
187
|
+
for (const entry of entries) {
|
|
188
|
+
const { entryHash, previousHash, sequence, ...data } = entry;
|
|
189
|
+
const expectedEntryHash = await this.sha256(JSON.stringify(data));
|
|
190
|
+
if (expectedEntryHash !== entryHash) {
|
|
191
|
+
return { valid: false, entriesChecked: sequence, brokenAt: sequence, error: `entry hash mismatch at sequence ${sequence}` };
|
|
192
|
+
}
|
|
193
|
+
if (previousHash !== prevHash) {
|
|
194
|
+
return { valid: false, entriesChecked: sequence, brokenAt: sequence, error: `chain break at sequence ${sequence}` };
|
|
195
|
+
}
|
|
196
|
+
prevHash = await this.sha256(`${previousHash}:${entryHash}`);
|
|
197
|
+
}
|
|
198
|
+
return {
|
|
199
|
+
valid: true,
|
|
200
|
+
entriesChecked: entries.length,
|
|
201
|
+
firstEntry: entries[0],
|
|
202
|
+
lastEntry: entries[entries.length - 1],
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
async sha256(data) {
|
|
206
|
+
if (typeof crypto !== 'undefined' && crypto.subtle) {
|
|
207
|
+
const buf = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(data));
|
|
208
|
+
return Array.from(new Uint8Array(buf)).map((b) => b.toString(16).padStart(2, '0')).join('');
|
|
209
|
+
}
|
|
210
|
+
const { createHash } = await Promise.resolve().then(() => __importStar(require('crypto')));
|
|
211
|
+
return createHash('sha256').update(data).digest('hex');
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
exports.HashChainAuditLogger = HashChainAuditLogger;
|
|
215
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
2
|
+
// Existing sinks
|
|
3
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
4
|
+
export class ConsoleAuditLogger {
|
|
5
|
+
async log(entry) {
|
|
6
|
+
console.log('[agent-identity audit]', JSON.stringify(entry, null, 2));
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
export class WebhookAuditLogger {
|
|
10
|
+
constructor(options) {
|
|
11
|
+
this.options = { secret: '', timeoutMs: 5000, silent: true, ...options };
|
|
12
|
+
}
|
|
13
|
+
async log(entry) {
|
|
14
|
+
const { url, secret, timeoutMs, silent } = this.options;
|
|
15
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
16
|
+
if (secret)
|
|
17
|
+
headers['X-Webhook-Secret'] = secret;
|
|
18
|
+
const controller = new AbortController();
|
|
19
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
20
|
+
try {
|
|
21
|
+
await fetch(url, { method: 'POST', headers, body: JSON.stringify(entry), signal: controller.signal });
|
|
22
|
+
}
|
|
23
|
+
catch (err) {
|
|
24
|
+
if (!silent)
|
|
25
|
+
throw err;
|
|
26
|
+
console.warn('[agent-identity] WebhookAuditLogger failed:', err);
|
|
27
|
+
}
|
|
28
|
+
finally {
|
|
29
|
+
clearTimeout(timer);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export class DatadogAuditLogger {
|
|
34
|
+
constructor(options) {
|
|
35
|
+
this.options = { service: 'agent-identity', site: 'datadoghq.com', silent: true, ...options };
|
|
36
|
+
}
|
|
37
|
+
async log(entry) {
|
|
38
|
+
const { apiKey, service, site, silent } = this.options;
|
|
39
|
+
try {
|
|
40
|
+
await fetch(`https://http-intake.logs.${site}/api/v2/logs`, {
|
|
41
|
+
method: 'POST',
|
|
42
|
+
headers: { 'DD-API-KEY': apiKey, 'Content-Type': 'application/json' },
|
|
43
|
+
body: JSON.stringify({ ddsource: 'agent-identity', service, message: JSON.stringify(entry) }),
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
catch (err) {
|
|
47
|
+
if (!silent)
|
|
48
|
+
throw err;
|
|
49
|
+
console.warn('[agent-identity] DatadogAuditLogger failed:', err);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
export class SplunkAuditLogger {
|
|
54
|
+
constructor(options) {
|
|
55
|
+
this.options = { sourcetype: 'agent_identity', silent: true, ...options };
|
|
56
|
+
}
|
|
57
|
+
async log(entry) {
|
|
58
|
+
const { hecUrl, token, sourcetype, silent } = this.options;
|
|
59
|
+
try {
|
|
60
|
+
await fetch(hecUrl, {
|
|
61
|
+
method: 'POST',
|
|
62
|
+
headers: { Authorization: `Splunk ${token}`, 'Content-Type': 'application/json' },
|
|
63
|
+
body: JSON.stringify({ event: entry, sourcetype }),
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
if (!silent)
|
|
68
|
+
throw err;
|
|
69
|
+
console.warn('[agent-identity] SplunkAuditLogger failed:', err);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
export class CompositeAuditLogger {
|
|
74
|
+
constructor(loggers) {
|
|
75
|
+
this.loggers = loggers;
|
|
76
|
+
}
|
|
77
|
+
async log(entry) {
|
|
78
|
+
await Promise.allSettled(this.loggers.map((l) => l.log(entry)));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/** Prints the chain root to stdout — suitable for piping to a CI artifact */
|
|
82
|
+
export class StdoutChainAnchor {
|
|
83
|
+
async publish(rootHash, sequence, timestamp) {
|
|
84
|
+
console.log(`[agent-identity chain-anchor] seq=${sequence} root=${rootHash} ts=${timestamp}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
export class S3ChainAnchor {
|
|
88
|
+
constructor(opts) {
|
|
89
|
+
this.opts = opts;
|
|
90
|
+
}
|
|
91
|
+
async publish(rootHash, sequence, timestamp) {
|
|
92
|
+
const key = `agent-identity/chain-roots/${sequence}-${timestamp}.json`;
|
|
93
|
+
const body = JSON.stringify({ rootHash, sequence, timestamp, publishedAt: new Date().toISOString() });
|
|
94
|
+
// S3 PutObject — in production use @aws-sdk/client-s3
|
|
95
|
+
const url = `https://${this.opts.bucketName}.s3.${this.opts.region}.amazonaws.com/${key}`;
|
|
96
|
+
await fetch(url, {
|
|
97
|
+
method: 'PUT',
|
|
98
|
+
headers: { 'Content-Type': 'application/json', 'Content-Length': String(body.length) },
|
|
99
|
+
body,
|
|
100
|
+
}).catch((err) => console.warn('[S3ChainAnchor] publish failed:', err));
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Wraps any AuditLogger with tamper-evident SHA-256 hash chaining.
|
|
105
|
+
*
|
|
106
|
+
* Each entry is hashed together with the hash of the previous entry.
|
|
107
|
+
* Any modification to a historical entry breaks the chain from that
|
|
108
|
+
* point forward — detectable by recomputing and comparing hashes.
|
|
109
|
+
*
|
|
110
|
+
* @example
|
|
111
|
+
* const logger = new HashChainAuditLogger({
|
|
112
|
+
* sink: new DatadogAuditLogger({ apiKey: process.env.DD_API_KEY! }),
|
|
113
|
+
* anchor: new S3ChainAnchor({ bucketName: 'my-audit-anchors', region: 'us-east-1' }),
|
|
114
|
+
* });
|
|
115
|
+
*/
|
|
116
|
+
export class HashChainAuditLogger {
|
|
117
|
+
constructor(opts) {
|
|
118
|
+
this.opts = opts;
|
|
119
|
+
this.sequence = 0;
|
|
120
|
+
this.previousHash = opts.seedHash ?? '0';
|
|
121
|
+
this.anchorEveryN = opts.anchorEveryN ?? 1000;
|
|
122
|
+
}
|
|
123
|
+
async log(entry) {
|
|
124
|
+
this.sequence += 1;
|
|
125
|
+
const entryHash = await this.sha256(JSON.stringify(entry));
|
|
126
|
+
const chainHash = await this.sha256(`${this.previousHash}:${entryHash}`);
|
|
127
|
+
const chained = {
|
|
128
|
+
...entry,
|
|
129
|
+
entryHash,
|
|
130
|
+
previousHash: this.previousHash,
|
|
131
|
+
sequence: this.sequence,
|
|
132
|
+
};
|
|
133
|
+
this.previousHash = chainHash;
|
|
134
|
+
await this.opts.sink.log(chained);
|
|
135
|
+
if (this.opts.anchor && this.sequence % this.anchorEveryN === 0) {
|
|
136
|
+
await this.opts.anchor
|
|
137
|
+
.publish(chainHash, this.sequence, new Date().toISOString())
|
|
138
|
+
.catch(console.error);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/** Verify an ordered array of chained entries; returns result with first broken sequence if any */
|
|
142
|
+
async verify(entries) {
|
|
143
|
+
let prevHash = this.opts.seedHash ?? '0';
|
|
144
|
+
for (const entry of entries) {
|
|
145
|
+
const { entryHash, previousHash, sequence, ...data } = entry;
|
|
146
|
+
const expectedEntryHash = await this.sha256(JSON.stringify(data));
|
|
147
|
+
if (expectedEntryHash !== entryHash) {
|
|
148
|
+
return { valid: false, entriesChecked: sequence, brokenAt: sequence, error: `entry hash mismatch at sequence ${sequence}` };
|
|
149
|
+
}
|
|
150
|
+
if (previousHash !== prevHash) {
|
|
151
|
+
return { valid: false, entriesChecked: sequence, brokenAt: sequence, error: `chain break at sequence ${sequence}` };
|
|
152
|
+
}
|
|
153
|
+
prevHash = await this.sha256(`${previousHash}:${entryHash}`);
|
|
154
|
+
}
|
|
155
|
+
return {
|
|
156
|
+
valid: true,
|
|
157
|
+
entriesChecked: entries.length,
|
|
158
|
+
firstEntry: entries[0],
|
|
159
|
+
lastEntry: entries[entries.length - 1],
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
async sha256(data) {
|
|
163
|
+
if (typeof crypto !== 'undefined' && crypto.subtle) {
|
|
164
|
+
const buf = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(data));
|
|
165
|
+
return Array.from(new Uint8Array(buf)).map((b) => b.toString(16).padStart(2, '0')).join('');
|
|
166
|
+
}
|
|
167
|
+
const { createHash } = await import('crypto');
|
|
168
|
+
return createHash('sha256').update(data).digest('hex');
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @datacules/agent-identity-audit — extended with hash-chain tamper-evident logging
|
|
3
|
+
*
|
|
4
|
+
* New in this version:
|
|
5
|
+
* HashChainAuditLogger — wraps any existing AuditLogger and appends a SHA-256
|
|
6
|
+
* hash chain to every entry. Detects tampering by recomputing the chain.
|
|
7
|
+
* ChainAnchor interface + built-in S3 and stdout anchors.
|
|
8
|
+
*/
|
|
9
|
+
import type { AuditLogEntry, AuditLogger } from '@datacules/agent-identity';
|
|
10
|
+
export declare class ConsoleAuditLogger implements AuditLogger {
|
|
11
|
+
log(entry: AuditLogEntry): Promise<void>;
|
|
12
|
+
}
|
|
13
|
+
export interface WebhookAuditLoggerOptions {
|
|
14
|
+
url: string;
|
|
15
|
+
secret?: string;
|
|
16
|
+
timeoutMs?: number;
|
|
17
|
+
silent?: boolean;
|
|
18
|
+
}
|
|
19
|
+
export declare class WebhookAuditLogger implements AuditLogger {
|
|
20
|
+
private readonly options;
|
|
21
|
+
constructor(options: WebhookAuditLoggerOptions);
|
|
22
|
+
log(entry: AuditLogEntry): Promise<void>;
|
|
23
|
+
}
|
|
24
|
+
export interface DatadogAuditLoggerOptions {
|
|
25
|
+
apiKey: string;
|
|
26
|
+
service?: string;
|
|
27
|
+
site?: string;
|
|
28
|
+
silent?: boolean;
|
|
29
|
+
}
|
|
30
|
+
export declare class DatadogAuditLogger implements AuditLogger {
|
|
31
|
+
private readonly options;
|
|
32
|
+
constructor(options: DatadogAuditLoggerOptions);
|
|
33
|
+
log(entry: AuditLogEntry): Promise<void>;
|
|
34
|
+
}
|
|
35
|
+
export interface SplunkAuditLoggerOptions {
|
|
36
|
+
hecUrl: string;
|
|
37
|
+
token: string;
|
|
38
|
+
sourcetype?: string;
|
|
39
|
+
silent?: boolean;
|
|
40
|
+
}
|
|
41
|
+
export declare class SplunkAuditLogger implements AuditLogger {
|
|
42
|
+
private readonly options;
|
|
43
|
+
constructor(options: SplunkAuditLoggerOptions);
|
|
44
|
+
log(entry: AuditLogEntry): Promise<void>;
|
|
45
|
+
}
|
|
46
|
+
export declare class CompositeAuditLogger implements AuditLogger {
|
|
47
|
+
private readonly loggers;
|
|
48
|
+
constructor(loggers: AuditLogger[]);
|
|
49
|
+
log(entry: AuditLogEntry): Promise<void>;
|
|
50
|
+
}
|
|
51
|
+
export interface ChainedAuditLogEntry extends AuditLogEntry {
|
|
52
|
+
/** SHA-256 hash of this entry's data fields */
|
|
53
|
+
entryHash: string;
|
|
54
|
+
/** SHA-256 hash of the previous entry (or '0' for the first entry) */
|
|
55
|
+
previousHash: string;
|
|
56
|
+
/** Sequential position in the chain */
|
|
57
|
+
sequence: number;
|
|
58
|
+
}
|
|
59
|
+
export interface ChainVerificationResult {
|
|
60
|
+
valid: boolean;
|
|
61
|
+
entriesChecked: number;
|
|
62
|
+
firstEntry?: ChainedAuditLogEntry;
|
|
63
|
+
lastEntry?: ChainedAuditLogEntry;
|
|
64
|
+
brokenAt?: number;
|
|
65
|
+
error?: string;
|
|
66
|
+
}
|
|
67
|
+
export interface ChainAnchor {
|
|
68
|
+
/** Publish the chain root hash to an immutable external location */
|
|
69
|
+
publish(rootHash: string, sequence: number, timestamp: string): Promise<void>;
|
|
70
|
+
}
|
|
71
|
+
/** Prints the chain root to stdout — suitable for piping to a CI artifact */
|
|
72
|
+
export declare class StdoutChainAnchor implements ChainAnchor {
|
|
73
|
+
publish(rootHash: string, sequence: number, timestamp: string): Promise<void>;
|
|
74
|
+
}
|
|
75
|
+
/** Publishes the chain root hash to an S3 object with Object Lock enabled */
|
|
76
|
+
export interface S3ChainAnchorOptions {
|
|
77
|
+
bucketName: string;
|
|
78
|
+
region: string;
|
|
79
|
+
/** AWS credentials or SDK client — omit to use default credential chain */
|
|
80
|
+
credentialsJson?: string;
|
|
81
|
+
}
|
|
82
|
+
export declare class S3ChainAnchor implements ChainAnchor {
|
|
83
|
+
private readonly opts;
|
|
84
|
+
constructor(opts: S3ChainAnchorOptions);
|
|
85
|
+
publish(rootHash: string, sequence: number, timestamp: string): Promise<void>;
|
|
86
|
+
}
|
|
87
|
+
export interface HashChainOptions {
|
|
88
|
+
/** Downstream sink that receives chained entries */
|
|
89
|
+
sink: AuditLogger;
|
|
90
|
+
/** Publish chain root every N entries (default: 1000) */
|
|
91
|
+
anchorEveryN?: number;
|
|
92
|
+
/** Optional anchor to publish roots externally */
|
|
93
|
+
anchor?: ChainAnchor;
|
|
94
|
+
/** Seed hash for the first entry (default: '0') */
|
|
95
|
+
seedHash?: string;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Wraps any AuditLogger with tamper-evident SHA-256 hash chaining.
|
|
99
|
+
*
|
|
100
|
+
* Each entry is hashed together with the hash of the previous entry.
|
|
101
|
+
* Any modification to a historical entry breaks the chain from that
|
|
102
|
+
* point forward — detectable by recomputing and comparing hashes.
|
|
103
|
+
*
|
|
104
|
+
* @example
|
|
105
|
+
* const logger = new HashChainAuditLogger({
|
|
106
|
+
* sink: new DatadogAuditLogger({ apiKey: process.env.DD_API_KEY! }),
|
|
107
|
+
* anchor: new S3ChainAnchor({ bucketName: 'my-audit-anchors', region: 'us-east-1' }),
|
|
108
|
+
* });
|
|
109
|
+
*/
|
|
110
|
+
export declare class HashChainAuditLogger implements AuditLogger {
|
|
111
|
+
private readonly opts;
|
|
112
|
+
private previousHash;
|
|
113
|
+
private sequence;
|
|
114
|
+
private readonly anchorEveryN;
|
|
115
|
+
constructor(opts: HashChainOptions);
|
|
116
|
+
log(entry: AuditLogEntry): Promise<void>;
|
|
117
|
+
/** Verify an ordered array of chained entries; returns result with first broken sequence if any */
|
|
118
|
+
verify(entries: ChainedAuditLogEntry[]): Promise<ChainVerificationResult>;
|
|
119
|
+
private sha256;
|
|
120
|
+
}
|
|
121
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,18 +1,47 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@datacules/agent-identity-audit",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Pre-built audit logger sinks for @datacules/agent-identity (Console, Webhook, Datadog, Splunk)",
|
|
6
|
+
"author": "Datacules LLC",
|
|
7
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/hvrcharon1/agent-identity.git",
|
|
11
|
+
"directory": "packages/audit"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"agent-identity",
|
|
15
|
+
"audit",
|
|
16
|
+
"logging",
|
|
17
|
+
"datadog",
|
|
18
|
+
"splunk",
|
|
19
|
+
"webhook",
|
|
20
|
+
"ai-agents",
|
|
21
|
+
"datacules"
|
|
22
|
+
],
|
|
6
23
|
"main": "./dist/cjs/index.js",
|
|
7
24
|
"module": "./dist/esm/index.js",
|
|
8
25
|
"types": "./dist/types/index.d.ts",
|
|
26
|
+
"exports": {
|
|
27
|
+
".": {
|
|
28
|
+
"import": "./dist/esm/index.js",
|
|
29
|
+
"require": "./dist/cjs/index.js",
|
|
30
|
+
"types": "./dist/types/index.d.ts"
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"files": [
|
|
34
|
+
"dist",
|
|
35
|
+
"LICENSE",
|
|
36
|
+
"README.md"
|
|
37
|
+
],
|
|
9
38
|
"scripts": {
|
|
10
|
-
"build": "tsc -p tsconfig.build.json",
|
|
39
|
+
"build": "tsc -p tsconfig.build.json && tsc -p tsconfig.cjs.json",
|
|
11
40
|
"test": "vitest run",
|
|
12
41
|
"type-check": "tsc --noEmit"
|
|
13
42
|
},
|
|
14
43
|
"peerDependencies": {
|
|
15
|
-
"@datacules/agent-identity": "^0.
|
|
44
|
+
"@datacules/agent-identity": "^0.11.1"
|
|
16
45
|
},
|
|
17
46
|
"devDependencies": {
|
|
18
47
|
"@datacules/agent-identity": "*",
|
package/src/audit.test.ts
DELETED
|
@@ -1,213 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
-
import {
|
|
3
|
-
ConsoleAuditLogger,
|
|
4
|
-
WebhookAuditLogger,
|
|
5
|
-
DatadogAuditLogger,
|
|
6
|
-
SplunkAuditLogger,
|
|
7
|
-
CompositeAuditLogger,
|
|
8
|
-
} from './index';
|
|
9
|
-
import type { AuditLogEntry } from '@datacules/agent-identity';
|
|
10
|
-
|
|
11
|
-
// All HTTP calls are mocked via vi.stubGlobal('fetch', ...).
|
|
12
|
-
// No live webhook, Datadog, or Splunk endpoint is needed.
|
|
13
|
-
const mockFetch = vi.fn();
|
|
14
|
-
vi.stubGlobal('fetch', mockFetch);
|
|
15
|
-
|
|
16
|
-
const ENTRY: AuditLogEntry = {
|
|
17
|
-
userId: 'user-alice',
|
|
18
|
-
resourceId: 'knowledge-base',
|
|
19
|
-
resourceKind: 'shared',
|
|
20
|
-
provider: 'openai',
|
|
21
|
-
action: 'read',
|
|
22
|
-
credentialId: 'cred-openai-prod',
|
|
23
|
-
credentialKind: 'fixed',
|
|
24
|
-
resolvedFor: 'service',
|
|
25
|
-
traceId: 'trace-abc123',
|
|
26
|
-
sessionId: 'sess-xyz',
|
|
27
|
-
requestedAt: '2026-05-30T12:00:00.000Z',
|
|
28
|
-
model: 'gpt-4o',
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
// ── ConsoleAuditLogger ─────────────────────────────────────────────────────
|
|
32
|
-
|
|
33
|
-
describe('ConsoleAuditLogger', () => {
|
|
34
|
-
it('calls console.log with the [agent-identity audit] prefix and JSON entry', async () => {
|
|
35
|
-
const spy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
36
|
-
const logger = new ConsoleAuditLogger();
|
|
37
|
-
await logger.log(ENTRY);
|
|
38
|
-
expect(spy).toHaveBeenCalledOnce();
|
|
39
|
-
const [prefix, json] = spy.mock.calls[0] as [string, string];
|
|
40
|
-
expect(prefix).toBe('[agent-identity audit]');
|
|
41
|
-
expect(json).toContain('cred-openai-prod');
|
|
42
|
-
spy.mockRestore();
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it('resolves without throwing on any valid AuditLogEntry', async () => {
|
|
46
|
-
vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
47
|
-
const logger = new ConsoleAuditLogger();
|
|
48
|
-
await expect(logger.log(ENTRY)).resolves.not.toThrow();
|
|
49
|
-
vi.restoreAllMocks();
|
|
50
|
-
});
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
// ── WebhookAuditLogger ─────────────────────────────────────────────────────
|
|
54
|
-
|
|
55
|
-
describe('WebhookAuditLogger', () => {
|
|
56
|
-
beforeEach(() => vi.clearAllMocks());
|
|
57
|
-
|
|
58
|
-
it('POSTs the entry as JSON to the configured webhook URL', async () => {
|
|
59
|
-
mockFetch.mockResolvedValueOnce({ ok: true } as Response);
|
|
60
|
-
const logger = new WebhookAuditLogger({ url: 'https://hooks.example.com/agent-identity' });
|
|
61
|
-
await logger.log(ENTRY);
|
|
62
|
-
expect(mockFetch).toHaveBeenCalledWith(
|
|
63
|
-
'https://hooks.example.com/agent-identity',
|
|
64
|
-
expect.objectContaining({
|
|
65
|
-
method: 'POST',
|
|
66
|
-
headers: expect.objectContaining({ 'Content-Type': 'application/json' }),
|
|
67
|
-
body: expect.stringContaining('cred-openai-prod'),
|
|
68
|
-
})
|
|
69
|
-
);
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
it('adds the X-Webhook-Secret header when a secret is configured', async () => {
|
|
73
|
-
mockFetch.mockResolvedValueOnce({ ok: true } as Response);
|
|
74
|
-
const logger = new WebhookAuditLogger({
|
|
75
|
-
url: 'https://hooks.example.com/ai',
|
|
76
|
-
secret: 'hmac-secret-xyz',
|
|
77
|
-
});
|
|
78
|
-
await logger.log(ENTRY);
|
|
79
|
-
expect(mockFetch).toHaveBeenCalledWith(
|
|
80
|
-
expect.any(String),
|
|
81
|
-
expect.objectContaining({
|
|
82
|
-
headers: expect.objectContaining({ 'X-Webhook-Secret': 'hmac-secret-xyz' }),
|
|
83
|
-
})
|
|
84
|
-
);
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
it('resolves without throwing when fetch fails and silent=true (default)', async () => {
|
|
88
|
-
mockFetch.mockRejectedValueOnce(new Error('Network unreachable'));
|
|
89
|
-
vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
90
|
-
const logger = new WebhookAuditLogger({ url: 'https://hooks.example.com/agent-identity' });
|
|
91
|
-
await expect(logger.log(ENTRY)).resolves.not.toThrow();
|
|
92
|
-
vi.restoreAllMocks();
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
it('throws when fetch fails and silent=false', async () => {
|
|
96
|
-
mockFetch.mockRejectedValueOnce(new Error('Connection refused'));
|
|
97
|
-
const logger = new WebhookAuditLogger({
|
|
98
|
-
url: 'https://hooks.example.com/ai',
|
|
99
|
-
silent: false,
|
|
100
|
-
});
|
|
101
|
-
await expect(logger.log(ENTRY)).rejects.toThrow('Connection refused');
|
|
102
|
-
});
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
// ── DatadogAuditLogger ─────────────────────────────────────────────────────
|
|
106
|
-
|
|
107
|
-
describe('DatadogAuditLogger', () => {
|
|
108
|
-
beforeEach(() => vi.clearAllMocks());
|
|
109
|
-
|
|
110
|
-
it('POSTs to the Datadog log intake URL with the DD-API-KEY header', async () => {
|
|
111
|
-
mockFetch.mockResolvedValueOnce({ ok: true } as Response);
|
|
112
|
-
const logger = new DatadogAuditLogger({ apiKey: 'dd-api-key-test-123' });
|
|
113
|
-
await logger.log(ENTRY);
|
|
114
|
-
expect(mockFetch).toHaveBeenCalledWith(
|
|
115
|
-
expect.stringContaining('https://http-intake.logs.datadoghq.com/api/v2/logs'),
|
|
116
|
-
expect.objectContaining({
|
|
117
|
-
headers: expect.objectContaining({ 'DD-API-KEY': 'dd-api-key-test-123' }),
|
|
118
|
-
})
|
|
119
|
-
);
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
it('uses a custom Datadog site when the site option is specified', async () => {
|
|
123
|
-
mockFetch.mockResolvedValueOnce({ ok: true } as Response);
|
|
124
|
-
const logger = new DatadogAuditLogger({ apiKey: 'key', site: 'datadoghq.eu' });
|
|
125
|
-
await logger.log(ENTRY);
|
|
126
|
-
const [url] = mockFetch.mock.calls[0] as [string];
|
|
127
|
-
expect(url).toContain('datadoghq.eu');
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
it('is silent by default when fetch fails (resolves without throwing)', async () => {
|
|
131
|
-
mockFetch.mockRejectedValueOnce(new Error('Datadog unreachable'));
|
|
132
|
-
vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
133
|
-
const logger = new DatadogAuditLogger({ apiKey: 'key' });
|
|
134
|
-
await expect(logger.log(ENTRY)).resolves.not.toThrow();
|
|
135
|
-
vi.restoreAllMocks();
|
|
136
|
-
});
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
// ── SplunkAuditLogger ─────────────────────────────────────────────────────
|
|
140
|
-
|
|
141
|
-
describe('SplunkAuditLogger', () => {
|
|
142
|
-
beforeEach(() => vi.clearAllMocks());
|
|
143
|
-
|
|
144
|
-
it('POSTs to the HEC URL with a Splunk token Authorization header', async () => {
|
|
145
|
-
mockFetch.mockResolvedValueOnce({ ok: true } as Response);
|
|
146
|
-
const logger = new SplunkAuditLogger({
|
|
147
|
-
hecUrl: 'https://splunk.example.com:8088/services/collector',
|
|
148
|
-
token: 'splunk-token-abc',
|
|
149
|
-
});
|
|
150
|
-
await logger.log(ENTRY);
|
|
151
|
-
expect(mockFetch).toHaveBeenCalledWith(
|
|
152
|
-
'https://splunk.example.com:8088/services/collector',
|
|
153
|
-
expect.objectContaining({
|
|
154
|
-
headers: expect.objectContaining({ Authorization: 'Splunk splunk-token-abc' }),
|
|
155
|
-
})
|
|
156
|
-
);
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
it('includes the audit entry inside the Splunk event payload', async () => {
|
|
160
|
-
mockFetch.mockResolvedValueOnce({ ok: true } as Response);
|
|
161
|
-
const logger = new SplunkAuditLogger({
|
|
162
|
-
hecUrl: 'https://splunk.example.com/collector',
|
|
163
|
-
token: 'tok',
|
|
164
|
-
});
|
|
165
|
-
await logger.log(ENTRY);
|
|
166
|
-
const body = JSON.parse(
|
|
167
|
-
(mockFetch.mock.calls[0][1] as RequestInit).body as string
|
|
168
|
-
) as { event: AuditLogEntry; sourcetype: string };
|
|
169
|
-
expect(body.event).toMatchObject({ credentialId: 'cred-openai-prod' });
|
|
170
|
-
expect(body.sourcetype).toBe('agent_identity');
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
it('is silent by default when fetch fails (resolves without throwing)', async () => {
|
|
174
|
-
mockFetch.mockRejectedValueOnce(new Error('HEC unreachable'));
|
|
175
|
-
vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
176
|
-
const logger = new SplunkAuditLogger({
|
|
177
|
-
hecUrl: 'https://splunk.example.com/collector',
|
|
178
|
-
token: 'tok',
|
|
179
|
-
});
|
|
180
|
-
await expect(logger.log(ENTRY)).resolves.not.toThrow();
|
|
181
|
-
vi.restoreAllMocks();
|
|
182
|
-
});
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
// ── CompositeAuditLogger ───────────────────────────────────────────────────
|
|
186
|
-
|
|
187
|
-
describe('CompositeAuditLogger', () => {
|
|
188
|
-
beforeEach(() => vi.clearAllMocks());
|
|
189
|
-
|
|
190
|
-
it('forwards the entry to all registered loggers', async () => {
|
|
191
|
-
const logA = { log: vi.fn<[AuditLogEntry], Promise<void>>().mockResolvedValue(undefined) };
|
|
192
|
-
const logB = { log: vi.fn<[AuditLogEntry], Promise<void>>().mockResolvedValue(undefined) };
|
|
193
|
-
const composite = new CompositeAuditLogger([logA, logB]);
|
|
194
|
-
await composite.log(ENTRY);
|
|
195
|
-
expect(logA.log).toHaveBeenCalledWith(ENTRY);
|
|
196
|
-
expect(logB.log).toHaveBeenCalledWith(ENTRY);
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
it('continues via Promise.allSettled even when one logger rejects', async () => {
|
|
200
|
-
const logA = { log: vi.fn<[AuditLogEntry], Promise<void>>().mockRejectedValue(new Error('Sink A failed')) };
|
|
201
|
-
const logB = { log: vi.fn<[AuditLogEntry], Promise<void>>().mockResolvedValue(undefined) };
|
|
202
|
-
const composite = new CompositeAuditLogger([logA, logB]);
|
|
203
|
-
await expect(composite.log(ENTRY)).resolves.not.toThrow();
|
|
204
|
-
expect(logB.log).toHaveBeenCalledWith(ENTRY);
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
it('works correctly with a single logger', async () => {
|
|
208
|
-
const logA = { log: vi.fn<[AuditLogEntry], Promise<void>>().mockResolvedValue(undefined) };
|
|
209
|
-
const composite = new CompositeAuditLogger([logA]);
|
|
210
|
-
await composite.log(ENTRY);
|
|
211
|
-
expect(logA.log).toHaveBeenCalledOnce();
|
|
212
|
-
});
|
|
213
|
-
});
|
package/src/index.ts
DELETED
|
@@ -1,258 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @datacules/agent-identity-audit — extended with hash-chain tamper-evident logging
|
|
3
|
-
*
|
|
4
|
-
* New in this version:
|
|
5
|
-
* HashChainAuditLogger — wraps any existing AuditLogger and appends a SHA-256
|
|
6
|
-
* hash chain to every entry. Detects tampering by recomputing the chain.
|
|
7
|
-
* ChainAnchor interface + built-in S3 and stdout anchors.
|
|
8
|
-
*/
|
|
9
|
-
import type { AuditLogEntry, AuditLogger } from '@datacules/agent-identity';
|
|
10
|
-
|
|
11
|
-
// ──────────────────────────────────────────────────────────────────────────
|
|
12
|
-
// Existing sinks
|
|
13
|
-
// ──────────────────────────────────────────────────────────────────────────
|
|
14
|
-
|
|
15
|
-
export class ConsoleAuditLogger implements AuditLogger {
|
|
16
|
-
async log(entry: AuditLogEntry): Promise<void> {
|
|
17
|
-
console.log('[agent-identity audit]', JSON.stringify(entry, null, 2));
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export interface WebhookAuditLoggerOptions {
|
|
22
|
-
url: string;
|
|
23
|
-
secret?: string;
|
|
24
|
-
timeoutMs?: number;
|
|
25
|
-
silent?: boolean;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export class WebhookAuditLogger implements AuditLogger {
|
|
29
|
-
private readonly options: Required<WebhookAuditLoggerOptions>;
|
|
30
|
-
constructor(options: WebhookAuditLoggerOptions) {
|
|
31
|
-
this.options = { secret: '', timeoutMs: 5000, silent: true, ...options };
|
|
32
|
-
}
|
|
33
|
-
async log(entry: AuditLogEntry): Promise<void> {
|
|
34
|
-
const { url, secret, timeoutMs, silent } = this.options;
|
|
35
|
-
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
|
|
36
|
-
if (secret) headers['X-Webhook-Secret'] = secret;
|
|
37
|
-
const controller = new AbortController();
|
|
38
|
-
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
39
|
-
try {
|
|
40
|
-
await fetch(url, { method: 'POST', headers, body: JSON.stringify(entry), signal: controller.signal });
|
|
41
|
-
} catch (err) {
|
|
42
|
-
if (!silent) throw err;
|
|
43
|
-
console.warn('[agent-identity] WebhookAuditLogger failed:', err);
|
|
44
|
-
} finally {
|
|
45
|
-
clearTimeout(timer);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export interface DatadogAuditLoggerOptions {
|
|
51
|
-
apiKey: string;
|
|
52
|
-
service?: string;
|
|
53
|
-
site?: string;
|
|
54
|
-
silent?: boolean;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export class DatadogAuditLogger implements AuditLogger {
|
|
58
|
-
private readonly options: Required<DatadogAuditLoggerOptions>;
|
|
59
|
-
constructor(options: DatadogAuditLoggerOptions) {
|
|
60
|
-
this.options = { service: 'agent-identity', site: 'datadoghq.com', silent: true, ...options };
|
|
61
|
-
}
|
|
62
|
-
async log(entry: AuditLogEntry): Promise<void> {
|
|
63
|
-
const { apiKey, service, site, silent } = this.options;
|
|
64
|
-
try {
|
|
65
|
-
await fetch(`https://http-intake.logs.${site}/api/v2/logs`, {
|
|
66
|
-
method: 'POST',
|
|
67
|
-
headers: { 'DD-API-KEY': apiKey, 'Content-Type': 'application/json' },
|
|
68
|
-
body: JSON.stringify({ ddsource: 'agent-identity', service, message: JSON.stringify(entry) }),
|
|
69
|
-
});
|
|
70
|
-
} catch (err) {
|
|
71
|
-
if (!silent) throw err;
|
|
72
|
-
console.warn('[agent-identity] DatadogAuditLogger failed:', err);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
export interface SplunkAuditLoggerOptions {
|
|
78
|
-
hecUrl: string;
|
|
79
|
-
token: string;
|
|
80
|
-
sourcetype?: string;
|
|
81
|
-
silent?: boolean;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
export class SplunkAuditLogger implements AuditLogger {
|
|
85
|
-
private readonly options: Required<SplunkAuditLoggerOptions>;
|
|
86
|
-
constructor(options: SplunkAuditLoggerOptions) {
|
|
87
|
-
this.options = { sourcetype: 'agent_identity', silent: true, ...options };
|
|
88
|
-
}
|
|
89
|
-
async log(entry: AuditLogEntry): Promise<void> {
|
|
90
|
-
const { hecUrl, token, sourcetype, silent } = this.options;
|
|
91
|
-
try {
|
|
92
|
-
await fetch(hecUrl, {
|
|
93
|
-
method: 'POST',
|
|
94
|
-
headers: { Authorization: `Splunk ${token}`, 'Content-Type': 'application/json' },
|
|
95
|
-
body: JSON.stringify({ event: entry, sourcetype }),
|
|
96
|
-
});
|
|
97
|
-
} catch (err) {
|
|
98
|
-
if (!silent) throw err;
|
|
99
|
-
console.warn('[agent-identity] SplunkAuditLogger failed:', err);
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
export class CompositeAuditLogger implements AuditLogger {
|
|
105
|
-
constructor(private readonly loggers: AuditLogger[]) {}
|
|
106
|
-
async log(entry: AuditLogEntry): Promise<void> {
|
|
107
|
-
await Promise.allSettled(this.loggers.map((l) => l.log(entry)));
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// ─── Hash chain types ────────────────────────────────────────────────────────────
|
|
112
|
-
|
|
113
|
-
export interface ChainedAuditLogEntry extends AuditLogEntry {
|
|
114
|
-
/** SHA-256 hash of this entry's data fields */
|
|
115
|
-
entryHash: string;
|
|
116
|
-
/** SHA-256 hash of the previous entry (or '0' for the first entry) */
|
|
117
|
-
previousHash: string;
|
|
118
|
-
/** Sequential position in the chain */
|
|
119
|
-
sequence: number;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
export interface ChainVerificationResult {
|
|
123
|
-
valid: boolean;
|
|
124
|
-
entriesChecked: number;
|
|
125
|
-
firstEntry?: ChainedAuditLogEntry;
|
|
126
|
-
lastEntry?: ChainedAuditLogEntry;
|
|
127
|
-
brokenAt?: number; // sequence number where chain breaks
|
|
128
|
-
error?: string;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// ─── Chain Anchor ────────────────────────────────────────────────────────────────
|
|
132
|
-
|
|
133
|
-
export interface ChainAnchor {
|
|
134
|
-
/** Publish the chain root hash to an immutable external location */
|
|
135
|
-
publish(rootHash: string, sequence: number, timestamp: string): Promise<void>;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/** Prints the chain root to stdout — suitable for piping to a CI artifact */
|
|
139
|
-
export class StdoutChainAnchor implements ChainAnchor {
|
|
140
|
-
async publish(rootHash: string, sequence: number, timestamp: string): Promise<void> {
|
|
141
|
-
console.log(`[agent-identity chain-anchor] seq=${sequence} root=${rootHash} ts=${timestamp}`);
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/** Publishes the chain root hash to an S3 object with Object Lock enabled */
|
|
146
|
-
export interface S3ChainAnchorOptions {
|
|
147
|
-
bucketName: string;
|
|
148
|
-
region: string;
|
|
149
|
-
/** AWS credentials or SDK client — omit to use default credential chain */
|
|
150
|
-
credentialsJson?: string;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
export class S3ChainAnchor implements ChainAnchor {
|
|
154
|
-
constructor(private readonly opts: S3ChainAnchorOptions) {}
|
|
155
|
-
|
|
156
|
-
async publish(rootHash: string, sequence: number, timestamp: string): Promise<void> {
|
|
157
|
-
const key = `agent-identity/chain-roots/${sequence}-${timestamp}.json`;
|
|
158
|
-
const body = JSON.stringify({ rootHash, sequence, timestamp, publishedAt: new Date().toISOString() });
|
|
159
|
-
// S3 PutObject — in production use @aws-sdk/client-s3
|
|
160
|
-
const url = `https://${this.opts.bucketName}.s3.${this.opts.region}.amazonaws.com/${key}`;
|
|
161
|
-
await fetch(url, {
|
|
162
|
-
method: 'PUT',
|
|
163
|
-
headers: { 'Content-Type': 'application/json', 'Content-Length': String(body.length) },
|
|
164
|
-
body,
|
|
165
|
-
}).catch((err) => console.warn('[S3ChainAnchor] publish failed:', err));
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
// ─── HashChainAuditLogger ──────────────────────────────────────────────────────
|
|
170
|
-
|
|
171
|
-
export interface HashChainOptions {
|
|
172
|
-
/** Downstream sink that receives chained entries */
|
|
173
|
-
sink: AuditLogger;
|
|
174
|
-
/** Publish chain root every N entries (default: 1000) */
|
|
175
|
-
anchorEveryN?: number;
|
|
176
|
-
/** Optional anchor to publish roots externally */
|
|
177
|
-
anchor?: ChainAnchor;
|
|
178
|
-
/** Seed hash for the first entry (default: '0') */
|
|
179
|
-
seedHash?: string;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
/**
|
|
183
|
-
* Wraps any AuditLogger with tamper-evident SHA-256 hash chaining.
|
|
184
|
-
*
|
|
185
|
-
* Each entry is hashed together with the hash of the previous entry.
|
|
186
|
-
* Any modification to a historical entry breaks the chain from that
|
|
187
|
-
* point forward — detectable by recomputing and comparing hashes.
|
|
188
|
-
*
|
|
189
|
-
* @example
|
|
190
|
-
* const logger = new HashChainAuditLogger({
|
|
191
|
-
* sink: new DatadogAuditLogger({ apiKey: process.env.DD_API_KEY! }),
|
|
192
|
-
* anchor: new S3ChainAnchor({ bucketName: 'my-audit-anchors', region: 'us-east-1' }),
|
|
193
|
-
* });
|
|
194
|
-
*/
|
|
195
|
-
export class HashChainAuditLogger implements AuditLogger {
|
|
196
|
-
private previousHash: string;
|
|
197
|
-
private sequence = 0;
|
|
198
|
-
private readonly anchorEveryN: number;
|
|
199
|
-
|
|
200
|
-
constructor(private readonly opts: HashChainOptions) {
|
|
201
|
-
this.previousHash = opts.seedHash ?? '0';
|
|
202
|
-
this.anchorEveryN = opts.anchorEveryN ?? 1000;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
async log(entry: AuditLogEntry): Promise<void> {
|
|
206
|
-
this.sequence += 1;
|
|
207
|
-
const entryHash = await this.sha256(JSON.stringify(entry));
|
|
208
|
-
const chainHash = await this.sha256(`${this.previousHash}:${entryHash}`);
|
|
209
|
-
|
|
210
|
-
const chained: ChainedAuditLogEntry = {
|
|
211
|
-
...entry,
|
|
212
|
-
entryHash,
|
|
213
|
-
previousHash: this.previousHash,
|
|
214
|
-
sequence: this.sequence,
|
|
215
|
-
};
|
|
216
|
-
|
|
217
|
-
this.previousHash = chainHash;
|
|
218
|
-
|
|
219
|
-
await this.opts.sink.log(chained as unknown as AuditLogEntry);
|
|
220
|
-
|
|
221
|
-
if (this.opts.anchor && this.sequence % this.anchorEveryN === 0) {
|
|
222
|
-
await this.opts.anchor
|
|
223
|
-
.publish(chainHash, this.sequence, new Date().toISOString())
|
|
224
|
-
.catch(console.error);
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
/** Verify an ordered array of chained entries; returns result with first broken sequence if any */
|
|
229
|
-
async verify(entries: ChainedAuditLogEntry[]): Promise<ChainVerificationResult> {
|
|
230
|
-
let prevHash = this.opts.seedHash ?? '0';
|
|
231
|
-
for (const entry of entries) {
|
|
232
|
-
const { entryHash, previousHash, sequence, ...data } = entry;
|
|
233
|
-
const expectedEntryHash = await this.sha256(JSON.stringify(data));
|
|
234
|
-
if (expectedEntryHash !== entryHash) {
|
|
235
|
-
return { valid: false, entriesChecked: sequence, brokenAt: sequence, error: `entry hash mismatch at sequence ${sequence}` };
|
|
236
|
-
}
|
|
237
|
-
if (previousHash !== prevHash) {
|
|
238
|
-
return { valid: false, entriesChecked: sequence, brokenAt: sequence, error: `chain break at sequence ${sequence}` };
|
|
239
|
-
}
|
|
240
|
-
prevHash = await this.sha256(`${previousHash}:${entryHash}`);
|
|
241
|
-
}
|
|
242
|
-
return {
|
|
243
|
-
valid: true,
|
|
244
|
-
entriesChecked: entries.length,
|
|
245
|
-
firstEntry: entries[0],
|
|
246
|
-
lastEntry: entries[entries.length - 1],
|
|
247
|
-
};
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
private async sha256(data: string): Promise<string> {
|
|
251
|
-
if (typeof crypto !== 'undefined' && crypto.subtle) {
|
|
252
|
-
const buf = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(data));
|
|
253
|
-
return Array.from(new Uint8Array(buf)).map((b) => b.toString(16).padStart(2, '0')).join('');
|
|
254
|
-
}
|
|
255
|
-
const { createHash } = await import('crypto');
|
|
256
|
-
return createHash('sha256').update(data).digest('hex');
|
|
257
|
-
}
|
|
258
|
-
}
|