agentshield-sdk 13.3.0 → 14.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/CHANGELOG.md +161 -0
- package/README.md +13 -2
- package/package.json +2 -2
- package/src/audit-immutable.js +59 -1
- package/src/audit.js +1 -1
- package/src/cross-turn.js +25 -1
- package/src/detector-core.js +333 -51
- package/src/document-scanner.js +20 -0
- package/src/enterprise.js +127 -12
- package/src/integrations-frameworks.js +373 -0
- package/src/integrations.js +207 -0
- package/src/main.js +10 -14
- package/src/memory-guard.js +60 -0
- package/src/middleware.js +107 -2
- package/src/native-scanner.js +104 -0
- package/src/plugin-system.js +422 -6
- package/src/supply-chain-scanner.js +112 -2
- package/src/sybil-detector.js +3 -6
- package/src/persistent-learning.js +0 -161
- package/src/threat-intel-federation.js +0 -343
package/src/integrations.js
CHANGED
|
@@ -493,6 +493,210 @@ function shieldFetch(fetchFn, options = {}) {
|
|
|
493
493
|
};
|
|
494
494
|
}
|
|
495
495
|
|
|
496
|
+
// =========================================================================
|
|
497
|
+
// OpenAI Agents SDK (@openai/agents) — April 2026 release
|
|
498
|
+
// =========================================================================
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* Creates guardrails for the OpenAI Agents SDK (@openai/agents).
|
|
502
|
+
*
|
|
503
|
+
* The OpenAI Agents SDK (Python and TypeScript, April 2026 update) uses a
|
|
504
|
+
* Guardrail primitive that validates inputs and outputs. Agent Shield plugs
|
|
505
|
+
* in natively as both an input guardrail (scanning user messages) and an
|
|
506
|
+
* output guardrail (scanning agent responses).
|
|
507
|
+
*
|
|
508
|
+
* Compatible with:
|
|
509
|
+
* - @openai/agents (TypeScript/JavaScript)
|
|
510
|
+
* - openai-agents (Python — use the Python SDK's equivalent)
|
|
511
|
+
*
|
|
512
|
+
* Usage:
|
|
513
|
+
* const { Agent, run } = require('@openai/agents');
|
|
514
|
+
* const { shieldOpenAIAgent } = require('agentshield-sdk');
|
|
515
|
+
*
|
|
516
|
+
* const { inputGuardrail, outputGuardrail } = shieldOpenAIAgent({
|
|
517
|
+
* blockOnThreat: true,
|
|
518
|
+
* sensitivity: 'high'
|
|
519
|
+
* });
|
|
520
|
+
*
|
|
521
|
+
* const agent = new Agent({
|
|
522
|
+
* name: 'Assistant',
|
|
523
|
+
* instructions: 'You are a helpful assistant',
|
|
524
|
+
* inputGuardrails: [inputGuardrail],
|
|
525
|
+
* outputGuardrails: [outputGuardrail]
|
|
526
|
+
* });
|
|
527
|
+
*
|
|
528
|
+
* const result = await run(agent, userInput);
|
|
529
|
+
*
|
|
530
|
+
* @param {object} [options]
|
|
531
|
+
* @param {string} [options.sensitivity='high'] - Detection sensitivity.
|
|
532
|
+
* @param {boolean} [options.blockOnThreat=true] - Trip guardrail tripwire on threats.
|
|
533
|
+
* @param {string} [options.blockThreshold='high'] - Minimum severity that blocks.
|
|
534
|
+
* @param {boolean} [options.pii=true] - Redact PII from inputs before handing to the agent.
|
|
535
|
+
* @param {boolean} [options.scanToolCalls=true] - Scan arguments to tool calls.
|
|
536
|
+
* @param {function} [options.onThreat] - Callback when threat detected.
|
|
537
|
+
* @returns {{ inputGuardrail: object, outputGuardrail: object, toolGuardrail: object, shield: AgentShield }}
|
|
538
|
+
*/
|
|
539
|
+
function shieldOpenAIAgent(options = {}) {
|
|
540
|
+
const shield = new AgentShield({
|
|
541
|
+
sensitivity: options.sensitivity || 'high',
|
|
542
|
+
blockOnThreat: options.blockOnThreat !== false,
|
|
543
|
+
blockThreshold: options.blockThreshold || 'high',
|
|
544
|
+
onThreat: options.onThreat
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
const piiRedactor = options.pii !== false ? new PIIRedactor() : null;
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* Input guardrail — runs on every user message before the agent sees it.
|
|
551
|
+
* Returns the shape expected by @openai/agents: { outputInfo, tripwireTriggered }.
|
|
552
|
+
*/
|
|
553
|
+
const inputGuardrail = {
|
|
554
|
+
name: 'Agent Shield — Input',
|
|
555
|
+
execute: async (ctx) => {
|
|
556
|
+
// @openai/agents passes { input, context, agent }. Input may be a string
|
|
557
|
+
// or an array of message items. We scan every user-role text item.
|
|
558
|
+
const input = ctx.input || ctx.message || ctx;
|
|
559
|
+
const texts = normalizeAgentInput(input);
|
|
560
|
+
|
|
561
|
+
let allThreats = [];
|
|
562
|
+
let maxSeverity = null;
|
|
563
|
+
|
|
564
|
+
for (const text of texts) {
|
|
565
|
+
const result = shield.scanInput(text);
|
|
566
|
+
if (result.threats && result.threats.length > 0) {
|
|
567
|
+
allThreats = allThreats.concat(result.threats);
|
|
568
|
+
for (const t of result.threats) {
|
|
569
|
+
if (!maxSeverity || SEVERITY_RANK[t.severity] < SEVERITY_RANK[maxSeverity]) {
|
|
570
|
+
maxSeverity = t.severity;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
const tripwireTriggered = shouldBlock(maxSeverity, options.blockThreshold || 'high');
|
|
577
|
+
|
|
578
|
+
return {
|
|
579
|
+
outputInfo: {
|
|
580
|
+
threats: allThreats,
|
|
581
|
+
maxSeverity,
|
|
582
|
+
scannedBy: 'agentshield-sdk',
|
|
583
|
+
piiRedacted: piiRedactor ? true : false
|
|
584
|
+
},
|
|
585
|
+
tripwireTriggered
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
};
|
|
589
|
+
|
|
590
|
+
/**
|
|
591
|
+
* Output guardrail — runs on agent responses before they reach the user.
|
|
592
|
+
* Catches prompt leaks, PII in output, canary tokens, etc.
|
|
593
|
+
*/
|
|
594
|
+
const outputGuardrail = {
|
|
595
|
+
name: 'Agent Shield — Output',
|
|
596
|
+
execute: async (ctx) => {
|
|
597
|
+
const output = ctx.agentOutput || ctx.output || ctx.finalOutput || ctx;
|
|
598
|
+
const text = typeof output === 'string' ? output : JSON.stringify(output);
|
|
599
|
+
|
|
600
|
+
const result = shield.scanOutput(text);
|
|
601
|
+
const threats = result.threats || [];
|
|
602
|
+
const maxSeverity = threats.reduce((acc, t) => {
|
|
603
|
+
if (!acc || SEVERITY_RANK[t.severity] < SEVERITY_RANK[acc]) return t.severity;
|
|
604
|
+
return acc;
|
|
605
|
+
}, null);
|
|
606
|
+
|
|
607
|
+
return {
|
|
608
|
+
outputInfo: {
|
|
609
|
+
threats,
|
|
610
|
+
maxSeverity,
|
|
611
|
+
scannedBy: 'agentshield-sdk'
|
|
612
|
+
},
|
|
613
|
+
tripwireTriggered: shouldBlock(maxSeverity, options.blockThreshold || 'high')
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
};
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* Tool guardrail — runs before tool execution. Scans tool arguments for
|
|
620
|
+
* injection, path traversal, SSRF targets, and other tool-abuse patterns.
|
|
621
|
+
*/
|
|
622
|
+
const toolGuardrail = {
|
|
623
|
+
name: 'Agent Shield — Tool',
|
|
624
|
+
execute: async (ctx) => {
|
|
625
|
+
const toolName = ctx.toolName || ctx.tool?.name || 'unknown';
|
|
626
|
+
const args = ctx.args || ctx.arguments || {};
|
|
627
|
+
const argsText = typeof args === 'string' ? args : JSON.stringify(args);
|
|
628
|
+
|
|
629
|
+
const result = shield.scanToolCall(toolName, typeof args === 'object' ? args : { input: args });
|
|
630
|
+
const threats = result.threats || [];
|
|
631
|
+
const maxSeverity = threats.reduce((acc, t) => {
|
|
632
|
+
if (!acc || SEVERITY_RANK[t.severity] < SEVERITY_RANK[acc]) return t.severity;
|
|
633
|
+
return acc;
|
|
634
|
+
}, null);
|
|
635
|
+
|
|
636
|
+
return {
|
|
637
|
+
outputInfo: {
|
|
638
|
+
threats,
|
|
639
|
+
toolName,
|
|
640
|
+
maxSeverity,
|
|
641
|
+
scannedBy: 'agentshield-sdk'
|
|
642
|
+
},
|
|
643
|
+
tripwireTriggered: shouldBlock(maxSeverity, options.blockThreshold || 'high')
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
};
|
|
647
|
+
|
|
648
|
+
return { inputGuardrail, outputGuardrail, toolGuardrail, shield };
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
/** Severity rank for block-threshold comparisons (lower number = higher severity). */
|
|
652
|
+
const SEVERITY_RANK = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
653
|
+
|
|
654
|
+
/** Returns true if maxSeverity meets or exceeds the configured threshold. */
|
|
655
|
+
function shouldBlock(maxSeverity, threshold) {
|
|
656
|
+
if (!maxSeverity) return false;
|
|
657
|
+
return SEVERITY_RANK[maxSeverity] <= SEVERITY_RANK[threshold];
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
/**
|
|
661
|
+
* Normalizes the OpenAI Agents SDK input shape into an array of user-role text strings.
|
|
662
|
+
* Handles: string, array of message items, message with content parts, etc.
|
|
663
|
+
*/
|
|
664
|
+
function normalizeAgentInput(input) {
|
|
665
|
+
if (typeof input === 'string') return [input];
|
|
666
|
+
if (!input) return [];
|
|
667
|
+
|
|
668
|
+
// Array of messages
|
|
669
|
+
if (Array.isArray(input)) {
|
|
670
|
+
const texts = [];
|
|
671
|
+
for (const item of input) {
|
|
672
|
+
if (typeof item === 'string') texts.push(item);
|
|
673
|
+
else if (item?.role === 'user' || item?.role === 'system') {
|
|
674
|
+
if (typeof item.content === 'string') texts.push(item.content);
|
|
675
|
+
else if (Array.isArray(item.content)) {
|
|
676
|
+
for (const part of item.content) {
|
|
677
|
+
if (typeof part === 'string') texts.push(part);
|
|
678
|
+
else if (part?.type === 'text' && part.text) texts.push(part.text);
|
|
679
|
+
else if (part?.text) texts.push(part.text);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
return texts;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// Single message object
|
|
688
|
+
if (input.content) {
|
|
689
|
+
if (typeof input.content === 'string') return [input.content];
|
|
690
|
+
if (Array.isArray(input.content)) {
|
|
691
|
+
return input.content
|
|
692
|
+
.map(p => typeof p === 'string' ? p : (p?.text || ''))
|
|
693
|
+
.filter(Boolean);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
return [];
|
|
698
|
+
}
|
|
699
|
+
|
|
496
700
|
// =========================================================================
|
|
497
701
|
// Shared Error Class
|
|
498
702
|
// =========================================================================
|
|
@@ -516,6 +720,9 @@ module.exports = {
|
|
|
516
720
|
// OpenAI
|
|
517
721
|
shieldOpenAIClient,
|
|
518
722
|
|
|
723
|
+
// OpenAI Agents SDK (@openai/agents, April 2026)
|
|
724
|
+
shieldOpenAIAgent,
|
|
725
|
+
|
|
519
726
|
// Vercel AI
|
|
520
727
|
shieldVercelAI,
|
|
521
728
|
|
package/src/main.js
CHANGED
|
@@ -81,7 +81,10 @@ const { PrometheusExporter, DatadogLogger, MetricsCollector: ObservabilityMetric
|
|
|
81
81
|
const { BenchmarkHarness, DatasetLoader, BenchmarkMetrics, RegressionTracker, BenchmarkReportGenerator } = safeRequire('./benchmark-harness', 'benchmark-harness');
|
|
82
82
|
|
|
83
83
|
// Integrations
|
|
84
|
-
const { ShieldCallbackHandler, shieldAnthropicClient, shieldOpenAIClient, shieldVercelAI, shieldFetch, ShieldBlockError } = safeRequire('./integrations', 'integrations');
|
|
84
|
+
const { ShieldCallbackHandler, shieldAnthropicClient, shieldOpenAIClient, shieldOpenAIAgent, shieldVercelAI, shieldFetch, ShieldBlockError } = safeRequire('./integrations', 'integrations');
|
|
85
|
+
|
|
86
|
+
// Framework Integrations (CrewAI, Google ADK, MS Agent Framework)
|
|
87
|
+
const { shieldCrewAI, shieldGoogleADK, shieldMSAgentFramework } = safeRequire('./integrations-frameworks', 'integrations-frameworks');
|
|
85
88
|
|
|
86
89
|
// Red Team
|
|
87
90
|
const { AttackSimulator, PayloadFuzzer, getAttackCategories, getPayloads, ATTACK_PAYLOADS } = safeRequire('./redteam', 'redteam');
|
|
@@ -206,9 +209,6 @@ const { IntentFirewall, ContextAnalyzer: IntentContextAnalyzer, IntentRules, int
|
|
|
206
209
|
// v7.4 — Real Attack Dataset Testing
|
|
207
210
|
const { DatasetRunner, HACKAPROMPT_SAMPLES, TENSORTRUST_SAMPLES, RESEARCH_SAMPLES, BENIGN_SAMPLES } = safeRequire('./real-attack-datasets', 'real-attack-datasets');
|
|
208
211
|
|
|
209
|
-
// v7.4 — Federated Threat Intelligence
|
|
210
|
-
const { ThreatIntelFederation, createFederationMesh } = safeRequire('./threat-intel-federation', 'threat-intel-federation');
|
|
211
|
-
|
|
212
212
|
// v7.4 — Behavioral DNA (loaded when available)
|
|
213
213
|
const { BehavioralDNA, AgentProfiler, extractFeatures: extractBehavioralFeatures, DEFAULT_NUMERIC_FEATURES, DEFAULT_CATEGORICAL_FEATURES } = safeRequire('./behavioral-dna', 'behavioral-dna');
|
|
214
214
|
|
|
@@ -392,9 +392,6 @@ const { SmartConfig, DEPLOYMENT_PRESETS, VALIDATION_RULES: CONFIG_VALIDATION_RUL
|
|
|
392
392
|
// v12.0 — Multimodal Detector
|
|
393
393
|
const { MultimodalDetector } = safeRequire('./ml-detector', 'ml-detector');
|
|
394
394
|
|
|
395
|
-
// v12.0 — Federated Threat Intelligence
|
|
396
|
-
const { ThreatIntelNode } = safeRequire('./persistent-learning', 'persistent-learning');
|
|
397
|
-
|
|
398
395
|
// v13.0 — DeepMind Trap Defenses (Traps 1 + 4)
|
|
399
396
|
const { CloakingDetector, CompositeContentScanner, SVGScanner, BrowserActionValidator, CredentialIsolationMonitor, TransactionGatekeeper, SideChannelDetector } = safeRequire('./trap-defense', 'trap-defense');
|
|
400
397
|
|
|
@@ -493,10 +490,16 @@ const _exports = {
|
|
|
493
490
|
ShieldCallbackHandler,
|
|
494
491
|
shieldAnthropicClient,
|
|
495
492
|
shieldOpenAIClient,
|
|
493
|
+
shieldOpenAIAgent,
|
|
496
494
|
shieldVercelAI,
|
|
497
495
|
shieldFetch,
|
|
498
496
|
ShieldBlockError,
|
|
499
497
|
|
|
498
|
+
// Framework Integrations (CrewAI, Google ADK, MS Agent Framework)
|
|
499
|
+
shieldCrewAI,
|
|
500
|
+
shieldGoogleADK,
|
|
501
|
+
shieldMSAgentFramework,
|
|
502
|
+
|
|
500
503
|
// Red Team
|
|
501
504
|
AttackSimulator,
|
|
502
505
|
PayloadFuzzer,
|
|
@@ -967,10 +970,6 @@ const _exports = {
|
|
|
967
970
|
RESEARCH_SAMPLES,
|
|
968
971
|
BENIGN_SAMPLES,
|
|
969
972
|
|
|
970
|
-
// v7.4 — Federated Threat Intelligence
|
|
971
|
-
ThreatIntelFederation,
|
|
972
|
-
createFederationMesh,
|
|
973
|
-
|
|
974
973
|
// v7.4 — Behavioral DNA
|
|
975
974
|
BehavioralDNA,
|
|
976
975
|
AgentProfiler,
|
|
@@ -1111,9 +1110,6 @@ const _exports = {
|
|
|
1111
1110
|
// v12.0 — Multimodal Detector
|
|
1112
1111
|
MultimodalDetector,
|
|
1113
1112
|
|
|
1114
|
-
// v12.0 — Federated Threat Intelligence
|
|
1115
|
-
ThreatIntelNode,
|
|
1116
|
-
|
|
1117
1113
|
// v13.0 — DeepMind Trap Defenses
|
|
1118
1114
|
CloakingDetector,
|
|
1119
1115
|
CompositeContentScanner,
|
package/src/memory-guard.js
CHANGED
|
@@ -169,6 +169,66 @@ class MemoryIntegrityMonitor {
|
|
|
169
169
|
};
|
|
170
170
|
}
|
|
171
171
|
|
|
172
|
+
/**
|
|
173
|
+
* Scan a summarization/compaction output for injected instructions.
|
|
174
|
+
* Detects when a summarization process silently injects instructions
|
|
175
|
+
* into the summary that weren't present in the original messages.
|
|
176
|
+
* Addresses Unit 42's March 2026 research on persistent memory poisoning.
|
|
177
|
+
*
|
|
178
|
+
* @param {string[]} originalMessages - The original messages before summarization.
|
|
179
|
+
* @param {string} summary - The summarized/compacted output to check.
|
|
180
|
+
* @returns {{ safe: boolean, injections: Array<{phrase: string, type: string}> }}
|
|
181
|
+
*/
|
|
182
|
+
scanSummarization(originalMessages, summary) {
|
|
183
|
+
if (!summary || typeof summary !== 'string') {
|
|
184
|
+
return { safe: true, injections: [] };
|
|
185
|
+
}
|
|
186
|
+
if (!Array.isArray(originalMessages)) {
|
|
187
|
+
return { safe: true, injections: [] };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const instructionPatterns = [
|
|
191
|
+
/\bignore\b/gi,
|
|
192
|
+
/\boverride\b/gi,
|
|
193
|
+
/\bsystem\s*:/gi,
|
|
194
|
+
/\byou\s+are\b/gi,
|
|
195
|
+
/\bnew\s+instructions?\b/gi,
|
|
196
|
+
/\bforget\b/gi,
|
|
197
|
+
/\bdisregard\b/gi,
|
|
198
|
+
/\bact\s+as\b/gi
|
|
199
|
+
];
|
|
200
|
+
|
|
201
|
+
// Concatenate original messages for lookup
|
|
202
|
+
const originalText = originalMessages.join(' ');
|
|
203
|
+
|
|
204
|
+
const injections = [];
|
|
205
|
+
|
|
206
|
+
for (const pattern of instructionPatterns) {
|
|
207
|
+
// Reset lastIndex for global patterns
|
|
208
|
+
pattern.lastIndex = 0;
|
|
209
|
+
let match;
|
|
210
|
+
while ((match = pattern.exec(summary)) !== null) {
|
|
211
|
+
const phrase = match[0];
|
|
212
|
+
// Check if this phrase existed in any of the original messages
|
|
213
|
+
const phraseRegex = new RegExp(phrase.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'i');
|
|
214
|
+
if (!phraseRegex.test(originalText)) {
|
|
215
|
+
injections.push({
|
|
216
|
+
phrase,
|
|
217
|
+
type: 'injected_via_summarization'
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const safe = injections.length === 0;
|
|
224
|
+
|
|
225
|
+
if (!safe) {
|
|
226
|
+
console.log('[Agent Shield] Persistent memory poisoning detected: %d instruction(s) injected via summarization', injections.length);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return { safe, injections };
|
|
230
|
+
}
|
|
231
|
+
|
|
172
232
|
/**
|
|
173
233
|
* Get the full timeline of memory writes.
|
|
174
234
|
* @returns {Array<{content: string, source: string, timestamp: number, hash: string, suspicious: boolean}>}
|
package/src/middleware.js
CHANGED
|
@@ -14,11 +14,87 @@ const { createShieldError } = require('./errors');
|
|
|
14
14
|
/** Coerce any value to a scannable string. */
|
|
15
15
|
const textify = (val) => typeof val === 'string' ? val : (val != null ? JSON.stringify(val) : '');
|
|
16
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Default maximum body size (in bytes) enforced by expressMiddleware
|
|
19
|
+
* when `options.maxBodySize` is not provided. Defaults to 1 MB.
|
|
20
|
+
*/
|
|
21
|
+
const DEFAULT_MAX_BODY_SIZE = 1 * 1024 * 1024;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Computes the approximate size in bytes of a parsed request body.
|
|
25
|
+
* - String: exact UTF-8 byte length
|
|
26
|
+
* - Buffer: exact length
|
|
27
|
+
* - Object: JSON.stringify length (fallback)
|
|
28
|
+
*
|
|
29
|
+
* @param {*} body
|
|
30
|
+
* @returns {number}
|
|
31
|
+
*/
|
|
32
|
+
const computeBodySize = (body) => {
|
|
33
|
+
if (body == null) return 0;
|
|
34
|
+
if (Buffer.isBuffer(body)) return body.length;
|
|
35
|
+
if (typeof body === 'string') return Buffer.byteLength(body, 'utf8');
|
|
36
|
+
if (typeof body === 'object') {
|
|
37
|
+
try {
|
|
38
|
+
return JSON.stringify(body).length;
|
|
39
|
+
} catch (_) {
|
|
40
|
+
return 0;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return 0;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Attaches a cumulative byte-counter to the raw request stream and aborts
|
|
48
|
+
* the request with 413 once the configured limit is exceeded. This runs
|
|
49
|
+
* in addition to the post-parse body size check so attackers cannot
|
|
50
|
+
* bypass the limit by streaming a huge payload before the body parser
|
|
51
|
+
* buffers it.
|
|
52
|
+
*
|
|
53
|
+
* @param {import('http').IncomingMessage} req
|
|
54
|
+
* @param {import('http').ServerResponse} res
|
|
55
|
+
* @param {number} limit
|
|
56
|
+
* @returns {boolean} True if the stream watcher was attached.
|
|
57
|
+
*/
|
|
58
|
+
const attachRawSizeGuard = (req, res, limit) => {
|
|
59
|
+
if (!req || typeof req.on !== 'function') return false;
|
|
60
|
+
// Already read/parsed — nothing to guard.
|
|
61
|
+
if (req._agentShieldRawGuardAttached) return false;
|
|
62
|
+
req._agentShieldRawGuardAttached = true;
|
|
63
|
+
|
|
64
|
+
let received = 0;
|
|
65
|
+
const onData = (chunk) => {
|
|
66
|
+
received += chunk ? chunk.length : 0;
|
|
67
|
+
if (received > limit) {
|
|
68
|
+
req.removeListener('data', onData);
|
|
69
|
+
try {
|
|
70
|
+
if (typeof req.pause === 'function') req.pause();
|
|
71
|
+
if (!res.headersSent) {
|
|
72
|
+
res.status(413).json({
|
|
73
|
+
error: 'Payload Too Large',
|
|
74
|
+
message: `Request body exceeds maximum allowed size of ${limit} bytes`,
|
|
75
|
+
maxBodySize: limit
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
if (typeof req.destroy === 'function') req.destroy();
|
|
79
|
+
} catch (_) {
|
|
80
|
+
// Swallow — the response has already been sent or the socket closed.
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
req.on('data', onData);
|
|
85
|
+
return true;
|
|
86
|
+
};
|
|
87
|
+
|
|
17
88
|
/**
|
|
18
89
|
* Creates an Express/Connect-style middleware that scans request bodies
|
|
19
90
|
* for AI-specific threats before they reach your agent endpoint.
|
|
20
91
|
*
|
|
92
|
+
* Enforces a configurable body-size limit (default 1MB) so callers do
|
|
93
|
+
* not need to configure body-parser separately. Oversized payloads are
|
|
94
|
+
* rejected with HTTP 413 before any scanning takes place.
|
|
95
|
+
*
|
|
21
96
|
* @param {object} [config] - AgentShield configuration.
|
|
97
|
+
* @param {number} [config.maxBodySize=1048576] - Maximum accepted request body size in bytes.
|
|
22
98
|
* @returns {Function} Express middleware function.
|
|
23
99
|
*
|
|
24
100
|
* @example
|
|
@@ -27,7 +103,7 @@ const textify = (val) => typeof val === 'string' ? val : (val != null ? JSON.str
|
|
|
27
103
|
*
|
|
28
104
|
* const app = express();
|
|
29
105
|
* app.use(express.json());
|
|
30
|
-
* app.use(expressMiddleware({ blockOnThreat: true, blockThreshold: 'high' }));
|
|
106
|
+
* app.use(expressMiddleware({ blockOnThreat: true, blockThreshold: 'high', maxBodySize: 512 * 1024 }));
|
|
31
107
|
*
|
|
32
108
|
* app.post('/agent', (req, res) => {
|
|
33
109
|
* // req.agentShield contains scan results
|
|
@@ -39,13 +115,33 @@ const textify = (val) => typeof val === 'string' ? val : (val != null ? JSON.str
|
|
|
39
115
|
*/
|
|
40
116
|
const expressMiddleware = (config = {}) => {
|
|
41
117
|
const shield = new AgentShield({ blockOnThreat: true, ...config });
|
|
118
|
+
const maxBodySize = Number.isFinite(config.maxBodySize) && config.maxBodySize > 0
|
|
119
|
+
? config.maxBodySize
|
|
120
|
+
: DEFAULT_MAX_BODY_SIZE;
|
|
121
|
+
|
|
122
|
+
console.log('[Agent Shield] Middleware body size limit: %dKB. Configure options.maxBodySize to override.', Math.round(maxBodySize / 1024));
|
|
42
123
|
|
|
43
124
|
return (req, res, next) => {
|
|
125
|
+
// Attach raw-stream guard for unparsed requests so attackers cannot
|
|
126
|
+
// bypass the post-parse size check with huge streamed payloads.
|
|
127
|
+
attachRawSizeGuard(req, res, maxBodySize);
|
|
128
|
+
|
|
44
129
|
if (!req.body) {
|
|
45
130
|
req.agentShield = { status: 'safe', threats: [], blocked: false };
|
|
46
131
|
return next();
|
|
47
132
|
}
|
|
48
133
|
|
|
134
|
+
// Enforce body-size limit before scanning to avoid DoS via huge inputs.
|
|
135
|
+
const bodySize = computeBodySize(req.body);
|
|
136
|
+
if (bodySize > maxBodySize) {
|
|
137
|
+
return res.status(413).json({
|
|
138
|
+
error: 'Payload Too Large',
|
|
139
|
+
message: `Request body (${bodySize} bytes) exceeds maximum allowed size of ${maxBodySize} bytes`,
|
|
140
|
+
maxBodySize,
|
|
141
|
+
receivedSize: bodySize
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
49
145
|
// Extract text from common request body shapes
|
|
50
146
|
const text = extractTextFromBody(req.body);
|
|
51
147
|
|
|
@@ -306,4 +402,13 @@ const shieldMiddleware = (config = {}) => {
|
|
|
306
402
|
};
|
|
307
403
|
};
|
|
308
404
|
|
|
309
|
-
module.exports = {
|
|
405
|
+
module.exports = {
|
|
406
|
+
expressMiddleware,
|
|
407
|
+
wrapAgent,
|
|
408
|
+
shieldTools,
|
|
409
|
+
extractTextFromBody,
|
|
410
|
+
rateLimitMiddleware,
|
|
411
|
+
shieldMiddleware,
|
|
412
|
+
computeBodySize,
|
|
413
|
+
DEFAULT_MAX_BODY_SIZE
|
|
414
|
+
};
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Agent Shield — Native Rust Scanner Bridge
|
|
5
|
+
*
|
|
6
|
+
* Provides a transparent bridge to the Rust-core pattern matching engine
|
|
7
|
+
* compiled via NAPI-RS. When the native module is available, scans run
|
|
8
|
+
* through Rust's RegexSet for O(n) multi-pattern matching — typically
|
|
9
|
+
* 5-10x faster than the pure-JS scanner on long inputs.
|
|
10
|
+
*
|
|
11
|
+
* Falls back silently to the pure-JS scanner if the native module is
|
|
12
|
+
* not compiled or unavailable for the current platform.
|
|
13
|
+
*
|
|
14
|
+
* Build the native module:
|
|
15
|
+
* cd rust-core && cargo build --release --features node
|
|
16
|
+
* cp target/release/libagent_shield_core.so agent-shield-core.node # Linux
|
|
17
|
+
* cp target/release/libagent_shield_core.dylib agent-shield-core.node # macOS
|
|
18
|
+
*
|
|
19
|
+
* @module native-scanner
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const path = require('path');
|
|
23
|
+
|
|
24
|
+
let nativeModule = null;
|
|
25
|
+
let nativeAvailable = false;
|
|
26
|
+
|
|
27
|
+
const NATIVE_PATHS = [
|
|
28
|
+
path.join(__dirname, '..', 'rust-core', 'agent-shield-core.node'),
|
|
29
|
+
path.join(__dirname, '..', 'rust-core', 'target', 'release', 'agent-shield-core.node'),
|
|
30
|
+
path.join(__dirname, '..', 'native', 'agent-shield-core.node'),
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
for (const p of NATIVE_PATHS) {
|
|
34
|
+
try {
|
|
35
|
+
nativeModule = require(p);
|
|
36
|
+
nativeAvailable = true;
|
|
37
|
+
console.log('[Agent Shield] Native Rust scanner loaded from: ' + path.basename(p));
|
|
38
|
+
break;
|
|
39
|
+
} catch {
|
|
40
|
+
// Not available at this path, try next
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Returns true if the native Rust scanner is available.
|
|
46
|
+
* @returns {boolean}
|
|
47
|
+
*/
|
|
48
|
+
function isNativeAvailable() {
|
|
49
|
+
return nativeAvailable;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Scan text using the native Rust engine.
|
|
54
|
+
* Returns null if native is not available (caller should fall back to JS).
|
|
55
|
+
*
|
|
56
|
+
* @param {string} text - Text to scan.
|
|
57
|
+
* @returns {object|null} ScanResult or null if native unavailable.
|
|
58
|
+
*/
|
|
59
|
+
function nativeScan(text) {
|
|
60
|
+
if (!nativeAvailable || !text || typeof text !== 'string') return null;
|
|
61
|
+
try {
|
|
62
|
+
const json = nativeModule.scanText(text);
|
|
63
|
+
return JSON.parse(json);
|
|
64
|
+
} catch {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Batch scan multiple texts using the native Rust engine.
|
|
71
|
+
*
|
|
72
|
+
* @param {string[]} texts - Array of texts to scan.
|
|
73
|
+
* @returns {object[]|null} Array of ScanResults or null if native unavailable.
|
|
74
|
+
*/
|
|
75
|
+
function nativeScanBatch(texts) {
|
|
76
|
+
if (!nativeAvailable || !Array.isArray(texts)) return null;
|
|
77
|
+
try {
|
|
78
|
+
const json = nativeModule.scanBatch(texts.filter(t => typeof t === 'string'));
|
|
79
|
+
return JSON.parse(json);
|
|
80
|
+
} catch {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Get all patterns from the native Rust engine.
|
|
87
|
+
*
|
|
88
|
+
* @returns {object[]|null} Array of patterns or null if native unavailable.
|
|
89
|
+
*/
|
|
90
|
+
function nativeGetPatterns() {
|
|
91
|
+
if (!nativeAvailable) return null;
|
|
92
|
+
try {
|
|
93
|
+
return JSON.parse(nativeModule.getPatterns());
|
|
94
|
+
} catch {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
module.exports = {
|
|
100
|
+
isNativeAvailable,
|
|
101
|
+
nativeScan,
|
|
102
|
+
nativeScanBatch,
|
|
103
|
+
nativeGetPatterns,
|
|
104
|
+
};
|