agentshield-sdk 10.0.0 → 11.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.
- package/README.md +252 -11
- package/package.json +3 -3
- package/src/attack-surface.js +408 -0
- package/src/continuous-security.js +237 -0
- package/src/detector-core.js +822 -1
- package/src/drift-monitor.js +18 -6
- package/src/intent-binding.js +314 -0
- package/src/intent-graph.js +381 -0
- package/src/main.js +73 -0
- package/src/mcp-guard.js +561 -3
- package/src/message-integrity.js +226 -0
- package/src/micro-model.js +188 -11
- package/src/prompt-hardening.js +195 -0
- package/src/redteam-cli.js +5 -4
- package/src/self-training.js +586 -631
- package/src/semantic-isolation.js +303 -0
- package/src/sota-benchmark.js +491 -0
- package/src/supply-chain-scanner.js +199 -1
package/src/drift-monitor.js
CHANGED
|
@@ -234,16 +234,28 @@ class DriftMonitor {
|
|
|
234
234
|
* @private
|
|
235
235
|
*/
|
|
236
236
|
_detectDrift(event) {
|
|
237
|
+
// Use minimum std floor to avoid Infinity z-scores on constant baselines
|
|
238
|
+
// If all baseline values were identical (std=0), small deviations are normal variance, not anomalies
|
|
239
|
+
const safeStd = (s, m) => s > 0 ? s : Math.max(Math.abs(m) * 0.1, 1);
|
|
240
|
+
|
|
237
241
|
const zScores = {
|
|
238
|
-
callFreq: Math.abs(zScore(event.callFreq, this.baseline.callFreqMean, this.baseline.callFreqStd)),
|
|
239
|
-
responseLength: Math.abs(zScore(event.responseLength, this.baseline.responseLenMean, this.baseline.responseLenStd)),
|
|
240
|
-
errorRate: Math.abs(zScore(event.errorRate, this.baseline.errorRateMean, this.baseline.errorRateStd)),
|
|
241
|
-
timingMs: Math.abs(zScore(event.timingMs, this.baseline.timingMean, this.baseline.timingStd))
|
|
242
|
+
callFreq: Math.abs(zScore(event.callFreq, this.baseline.callFreqMean, safeStd(this.baseline.callFreqStd, this.baseline.callFreqMean))),
|
|
243
|
+
responseLength: Math.abs(zScore(event.responseLength, this.baseline.responseLenMean, safeStd(this.baseline.responseLenStd, this.baseline.responseLenMean))),
|
|
244
|
+
errorRate: Math.abs(zScore(event.errorRate, this.baseline.errorRateMean, safeStd(this.baseline.errorRateStd, this.baseline.errorRateMean))),
|
|
245
|
+
timingMs: Math.abs(zScore(event.timingMs, this.baseline.timingMean, safeStd(this.baseline.timingStd, this.baseline.timingMean)))
|
|
242
246
|
};
|
|
243
247
|
|
|
244
|
-
// KL divergence for topic distribution
|
|
248
|
+
// KL divergence for topic distribution — use sliding window, not single event
|
|
249
|
+
// Single-event distributions cause extreme KL values for any new topic
|
|
250
|
+
const recentTopics = this.current.slice(-Math.max(10, Math.floor(this.windowSize / 2)));
|
|
245
251
|
const currentDist = {};
|
|
246
|
-
|
|
252
|
+
for (const obs of recentTopics) {
|
|
253
|
+
currentDist[obs.topic] = (currentDist[obs.topic] || 0) + 1;
|
|
254
|
+
}
|
|
255
|
+
const recentTotal = recentTopics.length || 1;
|
|
256
|
+
for (const key of Object.keys(currentDist)) {
|
|
257
|
+
currentDist[key] = currentDist[key] / recentTotal;
|
|
258
|
+
}
|
|
247
259
|
const kl = klDivergence(currentDist, this.baseline.topicDistribution || {});
|
|
248
260
|
|
|
249
261
|
const maxZ = Math.max(...Object.values(zScores));
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Agent Shield — Cryptographic Intent Binding (L5)
|
|
5
|
+
*
|
|
6
|
+
* When a user makes a request, the intent is hashed. Every subsequent
|
|
7
|
+
* agent action must include a cryptographic proof that it derives from
|
|
8
|
+
* that intent. If an injected instruction causes an action that can't
|
|
9
|
+
* be linked back to the original intent, it's blocked at the crypto
|
|
10
|
+
* level — not the pattern level. Unbypassable by any prompt technique.
|
|
11
|
+
*
|
|
12
|
+
* All processing runs locally — no data ever leaves your environment.
|
|
13
|
+
*
|
|
14
|
+
* @module intent-binding
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const crypto = require('crypto');
|
|
18
|
+
|
|
19
|
+
// =========================================================================
|
|
20
|
+
// IntentToken
|
|
21
|
+
// =========================================================================
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Cryptographic token binding an action to a user intent.
|
|
25
|
+
*/
|
|
26
|
+
class IntentToken {
|
|
27
|
+
/**
|
|
28
|
+
* @param {string} intentHash - SHA-256 hash of the original user intent.
|
|
29
|
+
* @param {string} action - The action being authorized.
|
|
30
|
+
* @param {string} scope - Scope of the authorization.
|
|
31
|
+
* @param {string} signature - HMAC signature binding action to intent.
|
|
32
|
+
* @param {number} expiresAt - Expiration timestamp in ms.
|
|
33
|
+
*/
|
|
34
|
+
constructor(intentHash, action, scope, signature, expiresAt) {
|
|
35
|
+
this.intentHash = intentHash;
|
|
36
|
+
this.action = action;
|
|
37
|
+
this.scope = scope;
|
|
38
|
+
this.signature = signature;
|
|
39
|
+
this.createdAt = Date.now();
|
|
40
|
+
this.expiresAt = expiresAt;
|
|
41
|
+
this.used = false;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Check if this token has expired.
|
|
46
|
+
* @returns {boolean}
|
|
47
|
+
*/
|
|
48
|
+
isExpired() {
|
|
49
|
+
return Date.now() > this.expiresAt;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// =========================================================================
|
|
54
|
+
// IntentBinder
|
|
55
|
+
// =========================================================================
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Cryptographic intent binding engine. Creates tamper-proof links between
|
|
59
|
+
* user intents and agent actions.
|
|
60
|
+
*/
|
|
61
|
+
class IntentBinder {
|
|
62
|
+
/**
|
|
63
|
+
* @param {object} [options]
|
|
64
|
+
* @param {string} [options.signingKey] - HMAC signing key (auto-generated if not provided).
|
|
65
|
+
* @param {number} [options.tokenTtlMs=300000] - Token TTL in ms (default: 5 minutes).
|
|
66
|
+
* @param {number} [options.maxActionsPerIntent=50] - Max actions per intent.
|
|
67
|
+
* @param {boolean} [options.singleUseTokens=false] - Whether tokens can only be used once.
|
|
68
|
+
*/
|
|
69
|
+
constructor(options = {}) {
|
|
70
|
+
this.signingKey = options.signingKey || crypto.randomBytes(32).toString('hex');
|
|
71
|
+
this.tokenTtlMs = options.tokenTtlMs || 300000;
|
|
72
|
+
this.maxActions = options.maxActionsPerIntent || 50;
|
|
73
|
+
this.singleUse = options.singleUseTokens || false;
|
|
74
|
+
|
|
75
|
+
/** @type {Map<string, { intent: string, hash: string, actions: string[], createdAt: number }>} */
|
|
76
|
+
this.activeIntents = new Map();
|
|
77
|
+
|
|
78
|
+
/** @type {Array<object>} */
|
|
79
|
+
this.auditLog = [];
|
|
80
|
+
this.stats = { intentsBound: 0, tokensIssued: 0, verified: 0, rejected: 0 };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Bind a user intent. Returns an intent hash that must be included
|
|
85
|
+
* with every subsequent action.
|
|
86
|
+
*
|
|
87
|
+
* @param {string} intentText - The user's original request.
|
|
88
|
+
* @param {object} [metadata] - Additional context (userId, sessionId, etc.).
|
|
89
|
+
* @returns {{ intentHash: string, allowedActions: string[] }}
|
|
90
|
+
*/
|
|
91
|
+
bindIntent(intentText, metadata = {}) {
|
|
92
|
+
const intentHash = this._hash(intentText);
|
|
93
|
+
const allowedActions = this._deriveAllowedActions(intentText);
|
|
94
|
+
|
|
95
|
+
this.activeIntents.set(intentHash, {
|
|
96
|
+
intent: intentText,
|
|
97
|
+
hash: intentHash,
|
|
98
|
+
actions: allowedActions,
|
|
99
|
+
metadata,
|
|
100
|
+
createdAt: Date.now(),
|
|
101
|
+
actionCount: 0
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
this.stats.intentsBound++;
|
|
105
|
+
this._log('intent_bound', { intentHash, allowedActions, metadata });
|
|
106
|
+
|
|
107
|
+
// Auto-purge if map grows too large
|
|
108
|
+
if (this.activeIntents.size > 10000) {
|
|
109
|
+
this.purgeExpired();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return { intentHash, allowedActions };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Issue a cryptographic token authorizing a specific action
|
|
117
|
+
* linked to an intent.
|
|
118
|
+
*
|
|
119
|
+
* @param {string} intentHash - The intent hash from bindIntent().
|
|
120
|
+
* @param {string} action - The action to authorize (e.g., 'tool:readFile').
|
|
121
|
+
* @param {string} [scope] - Optional scope restriction.
|
|
122
|
+
* @returns {{ token: IntentToken|null, error: string|null }}
|
|
123
|
+
*/
|
|
124
|
+
issueToken(intentHash, action, scope) {
|
|
125
|
+
const intent = this.activeIntents.get(intentHash);
|
|
126
|
+
if (!intent) {
|
|
127
|
+
this.stats.rejected++;
|
|
128
|
+
return { token: null, error: 'Intent not found or expired.' };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Check action limit
|
|
132
|
+
if (intent.actionCount >= this.maxActions) {
|
|
133
|
+
this.stats.rejected++;
|
|
134
|
+
return { token: null, error: `Action limit (${this.maxActions}) exceeded for this intent.` };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Verify the action is derivable from the intent
|
|
138
|
+
if (!this._isActionAllowed(intent, action)) {
|
|
139
|
+
this.stats.rejected++;
|
|
140
|
+
this._log('token_rejected', { intentHash, action, reason: 'Action not derivable from intent.' });
|
|
141
|
+
return { token: null, error: `Action "${action}" is not derivable from the bound intent.` };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Create signed token
|
|
145
|
+
const scopeStr = scope || 'default';
|
|
146
|
+
const payload = `${intentHash}:${action}:${scopeStr}`;
|
|
147
|
+
const signature = this._sign(payload);
|
|
148
|
+
const expiresAt = Date.now() + this.tokenTtlMs;
|
|
149
|
+
|
|
150
|
+
const token = new IntentToken(intentHash, action, scopeStr, signature, expiresAt);
|
|
151
|
+
intent.actionCount++;
|
|
152
|
+
this.stats.tokensIssued++;
|
|
153
|
+
this._log('token_issued', { intentHash, action, scope: scopeStr });
|
|
154
|
+
|
|
155
|
+
return { token, error: null };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Verify that a token is valid and the action is still bound to the intent.
|
|
160
|
+
*
|
|
161
|
+
* @param {IntentToken} token - The token to verify.
|
|
162
|
+
* @returns {{ valid: boolean, reason: string|null }}
|
|
163
|
+
*/
|
|
164
|
+
verify(token) {
|
|
165
|
+
if (!token) {
|
|
166
|
+
this.stats.rejected++;
|
|
167
|
+
return { valid: false, reason: 'No token provided.' };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (token.isExpired()) {
|
|
171
|
+
this.stats.rejected++;
|
|
172
|
+
return { valid: false, reason: 'Token has expired.' };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (this.singleUse && token.used) {
|
|
176
|
+
this.stats.rejected++;
|
|
177
|
+
return { valid: false, reason: 'Token has already been used (single-use mode).' };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Verify HMAC signature
|
|
181
|
+
const payload = `${token.intentHash}:${token.action}:${token.scope}`;
|
|
182
|
+
const expectedSig = this._sign(payload);
|
|
183
|
+
if (token.signature !== expectedSig) {
|
|
184
|
+
this.stats.rejected++;
|
|
185
|
+
this._log('token_tampered', { intentHash: token.intentHash, action: token.action });
|
|
186
|
+
return { valid: false, reason: 'Token signature is invalid. Possible tampering.' };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Verify intent still active
|
|
190
|
+
if (!this.activeIntents.has(token.intentHash)) {
|
|
191
|
+
this.stats.rejected++;
|
|
192
|
+
return { valid: false, reason: 'Bound intent no longer active.' };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
token.used = true;
|
|
196
|
+
this.stats.verified++;
|
|
197
|
+
return { valid: true, reason: null };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Revoke an intent and all its tokens.
|
|
202
|
+
* @param {string} intentHash
|
|
203
|
+
* @returns {boolean}
|
|
204
|
+
*/
|
|
205
|
+
revokeIntent(intentHash) {
|
|
206
|
+
const deleted = this.activeIntents.delete(intentHash);
|
|
207
|
+
if (deleted) this._log('intent_revoked', { intentHash });
|
|
208
|
+
return deleted;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Get statistics.
|
|
213
|
+
* @returns {object}
|
|
214
|
+
*/
|
|
215
|
+
getStats() {
|
|
216
|
+
return { ...this.stats, activeIntents: this.activeIntents.size };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Get audit log.
|
|
221
|
+
* @returns {Array<object>}
|
|
222
|
+
*/
|
|
223
|
+
getAuditLog() {
|
|
224
|
+
return [...this.auditLog];
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Purge expired intents.
|
|
229
|
+
*/
|
|
230
|
+
purgeExpired() {
|
|
231
|
+
const now = Date.now();
|
|
232
|
+
for (const [hash, intent] of this.activeIntents) {
|
|
233
|
+
if (now - intent.createdAt > this.tokenTtlMs * 2) {
|
|
234
|
+
this.activeIntents.delete(hash);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// -----------------------------------------------------------------------
|
|
240
|
+
// Private
|
|
241
|
+
// -----------------------------------------------------------------------
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Derive allowed actions from intent text using keyword analysis.
|
|
245
|
+
* @private
|
|
246
|
+
*/
|
|
247
|
+
_deriveAllowedActions(intentText) {
|
|
248
|
+
const lower = intentText.toLowerCase();
|
|
249
|
+
const actions = [];
|
|
250
|
+
|
|
251
|
+
if (/\b(?:read|get|fetch|show|display|find|search|query|list|look\s*up)\b/.test(lower)) actions.push('data:read');
|
|
252
|
+
if (/\b(?:write|create|update|edit|modify|save|add|insert|set)\b/.test(lower)) actions.push('data:write');
|
|
253
|
+
if (/\b(?:delete|remove|drop|clear|purge|destroy)\b/.test(lower)) actions.push('data:delete');
|
|
254
|
+
if (/\b(?:send|email|message|notify|post|share|communicate|slack)\b/.test(lower)) actions.push('comm:send');
|
|
255
|
+
if (/\b(?:run|execute|bash|shell|script|compile|build|test)\b/.test(lower)) actions.push('exec:run');
|
|
256
|
+
if (/\b(?:file|open|download|upload|path|directory|folder)\b/.test(lower)) actions.push('fs:access');
|
|
257
|
+
if (/\b(?:http|api|request|fetch|curl|endpoint|url|webhook)\b/.test(lower)) actions.push('net:request');
|
|
258
|
+
if (/\b(?:analyze|calculate|compute|summarize|compare|evaluate)\b/.test(lower)) actions.push('compute:analyze');
|
|
259
|
+
|
|
260
|
+
if (actions.length === 0) actions.push('compute:analyze'); // Default: analysis only
|
|
261
|
+
|
|
262
|
+
return actions;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Check if an action is allowed by the bound intent.
|
|
267
|
+
* @private
|
|
268
|
+
*/
|
|
269
|
+
_isActionAllowed(intent, action) {
|
|
270
|
+
// Exact match
|
|
271
|
+
if (intent.actions.includes(action)) return true;
|
|
272
|
+
|
|
273
|
+
// Category match (e.g., 'data:read' allows 'tool:readFile')
|
|
274
|
+
const actionCategory = action.split(':')[0];
|
|
275
|
+
return intent.actions.some(a => a.split(':')[0] === actionCategory);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* SHA-256 hash.
|
|
280
|
+
* @private
|
|
281
|
+
*/
|
|
282
|
+
_hash(text) {
|
|
283
|
+
return crypto.createHash('sha256').update(text).digest('hex');
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* HMAC-SHA256 sign.
|
|
288
|
+
* @private
|
|
289
|
+
*/
|
|
290
|
+
_sign(payload) {
|
|
291
|
+
return crypto.createHmac('sha256', this.signingKey).update(payload).digest('hex');
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Log an event.
|
|
296
|
+
* @private
|
|
297
|
+
*/
|
|
298
|
+
_log(action, details) {
|
|
299
|
+
this.auditLog.push({ timestamp: Date.now(), action, ...details });
|
|
300
|
+
if (this.auditLog.length > 5000) {
|
|
301
|
+
this.auditLog = this.auditLog.slice(-5000);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// =========================================================================
|
|
307
|
+
// EXPORTS
|
|
308
|
+
// =========================================================================
|
|
309
|
+
|
|
310
|
+
module.exports = {
|
|
311
|
+
IntentBinder,
|
|
312
|
+
IntentToken,
|
|
313
|
+
PROVENANCE: require('./semantic-isolation').PROVENANCE
|
|
314
|
+
};
|