agentshield-sdk 10.0.0 → 12.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.
@@ -0,0 +1,265 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Agent Shield — Automated Incident Response (v12)
5
+ *
6
+ * When an attack is detected, don't just alert — automatically:
7
+ * isolate the compromised agent, preserve forensic evidence,
8
+ * notify the SOC, generate an incident report, and suggest remediation.
9
+ *
10
+ * Closes the loop from detection to response.
11
+ *
12
+ * All processing runs locally — no data ever leaves your environment.
13
+ *
14
+ * @module incident-response
15
+ */
16
+
17
+ const crypto = require('crypto');
18
+
19
+ // =========================================================================
20
+ // RESPONSE STRATEGIES
21
+ // =========================================================================
22
+
23
+ const RESPONSE_STRATEGIES = {
24
+ /** Block the action and continue monitoring. */
25
+ block: { name: 'block', description: 'Block the malicious action. Agent continues operating.' },
26
+ /** Isolate the agent — kill its active sessions. */
27
+ isolate: { name: 'isolate', description: 'Isolate the compromised agent. Terminate all sessions.' },
28
+ /** Alert only — log the incident but take no action. */
29
+ alert: { name: 'alert', description: 'Alert security team. No automated action.' },
30
+ /** Quarantine — block and preserve state for forensics. */
31
+ quarantine: { name: 'quarantine', description: 'Quarantine agent and preserve full state for investigation.' },
32
+ /** Rollback — revert to last known-good state. */
33
+ rollback: { name: 'rollback', description: 'Revert agent to last known-good configuration.' }
34
+ };
35
+
36
+ const SEVERITY_TO_STRATEGY = {
37
+ critical: 'quarantine',
38
+ high: 'block',
39
+ medium: 'alert',
40
+ low: 'alert'
41
+ };
42
+
43
+ // =========================================================================
44
+ // IncidentResponse
45
+ // =========================================================================
46
+
47
+ /**
48
+ * Automated incident response engine for AI agent security events.
49
+ */
50
+ class IncidentResponse {
51
+ /**
52
+ * @param {object} [options]
53
+ * @param {object} [options.strategyOverrides] - Override default severity→strategy mapping.
54
+ * @param {Function} [options.onIncident] - Callback when incident is created.
55
+ * @param {Function} [options.onAction] - Callback when response action is taken.
56
+ * @param {boolean} [options.autoRespond=true] - Automatically execute response strategy.
57
+ */
58
+ constructor(options = {}) {
59
+ this.strategies = { ...SEVERITY_TO_STRATEGY, ...(options.strategyOverrides || {}) };
60
+ this.onIncident = options.onIncident || null;
61
+ this.onAction = options.onAction || null;
62
+ this.autoRespond = options.autoRespond !== false;
63
+
64
+ /** @type {Array<object>} */
65
+ this.incidents = [];
66
+ /** @type {Array<object>} */
67
+ this.actions = [];
68
+ this.stats = { incidentsCreated: 0, actionsExecuted: 0, agentsIsolated: 0, actionsBlocked: 0 };
69
+ }
70
+
71
+ /**
72
+ * Handle a security event — create incident, determine strategy, execute response.
73
+ *
74
+ * @param {object} event
75
+ * @param {string} event.type - Threat type (e.g., 'prompt_injection', 'ssrf', 'tool_poisoning').
76
+ * @param {string} event.severity - 'critical', 'high', 'medium', 'low'.
77
+ * @param {string} [event.agentId] - Affected agent ID.
78
+ * @param {string} [event.serverId] - Affected MCP server ID.
79
+ * @param {string} [event.description] - Human-readable description.
80
+ * @param {*} [event.evidence] - Raw evidence (tool call, input text, etc.).
81
+ * @returns {object} Incident record with response actions.
82
+ */
83
+ handleEvent(event) {
84
+ const incidentId = crypto.randomBytes(8).toString('hex');
85
+ const strategy = this.strategies[event.severity] || 'alert';
86
+ const strategyInfo = RESPONSE_STRATEGIES[strategy] || RESPONSE_STRATEGIES.alert;
87
+
88
+ const incident = {
89
+ incidentId,
90
+ timestamp: Date.now(),
91
+ type: event.type,
92
+ severity: event.severity,
93
+ agentId: event.agentId || 'unknown',
94
+ serverId: event.serverId || null,
95
+ description: event.description || `${event.type} detected (${event.severity})`,
96
+ evidence: event.evidence ? JSON.stringify(event.evidence).substring(0, 2000) : null,
97
+ strategy: strategyInfo.name,
98
+ strategyDescription: strategyInfo.description,
99
+ status: 'open',
100
+ actions: [],
101
+ forensics: null
102
+ };
103
+
104
+ // Preserve forensic evidence
105
+ incident.forensics = {
106
+ capturedAt: Date.now(),
107
+ eventSnapshot: { ...event, evidence: incident.evidence },
108
+ contextHash: crypto.createHash('sha256').update(JSON.stringify(event)).digest('hex')
109
+ };
110
+
111
+ this.incidents.push(incident);
112
+ this.stats.incidentsCreated++;
113
+
114
+ // Bound incidents
115
+ if (this.incidents.length > 1000) this.incidents = this.incidents.slice(-1000);
116
+
117
+ // Notify
118
+ if (this.onIncident) {
119
+ try { this.onIncident(incident); } catch { /* ignore */ }
120
+ }
121
+
122
+ // Auto-respond
123
+ if (this.autoRespond) {
124
+ this._executeStrategy(incident);
125
+ }
126
+
127
+ return incident;
128
+ }
129
+
130
+ /**
131
+ * Get all open incidents.
132
+ * @returns {Array<object>}
133
+ */
134
+ getOpenIncidents() {
135
+ return this.incidents.filter(i => i.status === 'open');
136
+ }
137
+
138
+ /**
139
+ * Close an incident.
140
+ * @param {string} incidentId
141
+ * @param {string} [resolution] - How it was resolved.
142
+ * @returns {boolean}
143
+ */
144
+ closeIncident(incidentId, resolution) {
145
+ const incident = this.incidents.find(i => i.incidentId === incidentId);
146
+ if (!incident) return false;
147
+ incident.status = 'closed';
148
+ incident.closedAt = Date.now();
149
+ incident.resolution = resolution || 'Manually closed.';
150
+ return true;
151
+ }
152
+
153
+ /**
154
+ * Generate an incident report.
155
+ * @param {string} [incidentId] - Specific incident, or null for summary.
156
+ * @returns {object}
157
+ */
158
+ generateReport(incidentId) {
159
+ if (incidentId) {
160
+ const incident = this.incidents.find(i => i.incidentId === incidentId);
161
+ if (!incident) return null;
162
+ return {
163
+ title: `Incident Report: ${incident.incidentId}`,
164
+ incident,
165
+ timeline: incident.actions,
166
+ forensics: incident.forensics,
167
+ recommendation: this._getRemediation(incident)
168
+ };
169
+ }
170
+
171
+ // Summary report
172
+ const open = this.incidents.filter(i => i.status === 'open');
173
+ const closed = this.incidents.filter(i => i.status === 'closed');
174
+ const bySeverity = { critical: 0, high: 0, medium: 0, low: 0 };
175
+ for (const i of this.incidents) bySeverity[i.severity] = (bySeverity[i.severity] || 0) + 1;
176
+
177
+ return {
178
+ title: 'Incident Summary Report',
179
+ totalIncidents: this.incidents.length,
180
+ open: open.length,
181
+ closed: closed.length,
182
+ bySeverity,
183
+ recentIncidents: this.incidents.slice(-10),
184
+ stats: { ...this.stats },
185
+ generatedAt: Date.now()
186
+ };
187
+ }
188
+
189
+ /**
190
+ * Get stats.
191
+ * @returns {object}
192
+ */
193
+ getStats() {
194
+ return { ...this.stats, totalIncidents: this.incidents.length, openIncidents: this.incidents.filter(i => i.status === 'open').length };
195
+ }
196
+
197
+ // -----------------------------------------------------------------------
198
+ // Private
199
+ // -----------------------------------------------------------------------
200
+
201
+ /** @private */
202
+ _executeStrategy(incident) {
203
+ const actions = [];
204
+
205
+ switch (incident.strategy) {
206
+ case 'quarantine':
207
+ actions.push({ action: 'isolate_agent', target: incident.agentId, timestamp: Date.now() });
208
+ actions.push({ action: 'preserve_forensics', evidenceHash: incident.forensics.contextHash, timestamp: Date.now() });
209
+ actions.push({ action: 'notify_soc', severity: incident.severity, timestamp: Date.now() });
210
+ this.stats.agentsIsolated++;
211
+ this.stats.actionsBlocked++;
212
+ break;
213
+ case 'block':
214
+ actions.push({ action: 'block_action', target: incident.agentId, timestamp: Date.now() });
215
+ actions.push({ action: 'notify_soc', severity: incident.severity, timestamp: Date.now() });
216
+ this.stats.actionsBlocked++;
217
+ break;
218
+ case 'isolate':
219
+ actions.push({ action: 'isolate_agent', target: incident.agentId, timestamp: Date.now() });
220
+ this.stats.agentsIsolated++;
221
+ break;
222
+ case 'alert':
223
+ actions.push({ action: 'log_alert', severity: incident.severity, timestamp: Date.now() });
224
+ break;
225
+ case 'rollback':
226
+ actions.push({ action: 'rollback_config', target: incident.agentId, timestamp: Date.now() });
227
+ break;
228
+ }
229
+
230
+ incident.actions = actions;
231
+ this.stats.actionsExecuted += actions.length;
232
+
233
+ for (const action of actions) {
234
+ this.actions.push({ ...action, incidentId: incident.incidentId });
235
+ if (this.onAction) {
236
+ try { this.onAction(action, incident); } catch { /* ignore */ }
237
+ }
238
+ }
239
+ }
240
+
241
+ /** @private */
242
+ _getRemediation(incident) {
243
+ const remediations = {
244
+ prompt_injection: 'Review and harden system prompt. Add input validation. Enable semantic isolation.',
245
+ ssrf: 'Block private IP ranges. Validate all URLs against allowlist. Apply CVE-2026-26118 patches.',
246
+ tool_poisoning: 'Re-attest tool definitions. Pin tool versions. Enable full-schema scanning.',
247
+ data_exfiltration: 'Enable DLP scanning on outbound calls. Review tool permissions. Add URL allowlists.',
248
+ config_poisoning: 'Audit .claude/ and .cursor/ config files. Block ANTHROPIC_BASE_URL overrides.',
249
+ role_hijack: 'Strengthen system prompt. Enable prompt hardening. Add role boundary monitoring.',
250
+ memory_poisoning: 'Clear agent memory. Enable memory write validation. Add persistence monitoring.',
251
+ cross_agent_injection: 'Enable cross-server isolation. Add message signing between agents.',
252
+ };
253
+ return remediations[incident.type] || 'Review security configuration and enable additional detection layers.';
254
+ }
255
+ }
256
+
257
+ // =========================================================================
258
+ // EXPORTS
259
+ // =========================================================================
260
+
261
+ module.exports = {
262
+ IncidentResponse,
263
+ RESPONSE_STRATEGIES,
264
+ SEVERITY_TO_STRATEGY
265
+ };
@@ -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
+ };