@wardnmesh/sdk-node 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +47 -0
- package/README.md +178 -0
- package/bin/setup.js +119 -0
- package/dist/agent-guard.d.ts +28 -0
- package/dist/agent-guard.js +206 -0
- package/dist/config/security-limits.d.ts +42 -0
- package/dist/config/security-limits.js +52 -0
- package/dist/detectors/base.d.ts +22 -0
- package/dist/detectors/base.js +51 -0
- package/dist/detectors/pattern.d.ts +7 -0
- package/dist/detectors/pattern.js +76 -0
- package/dist/detectors/semantic-detector.d.ts +16 -0
- package/dist/detectors/semantic-detector.js +96 -0
- package/dist/detectors/sequence.d.ts +11 -0
- package/dist/detectors/sequence.js +182 -0
- package/dist/detectors/state.d.ts +8 -0
- package/dist/detectors/state.js +86 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +31 -0
- package/dist/integrations/vercel.d.ts +7 -0
- package/dist/integrations/vercel.js +34 -0
- package/dist/middleware/express.d.ts +36 -0
- package/dist/middleware/express.js +54 -0
- package/dist/middleware/nextjs.d.ts +3 -0
- package/dist/middleware/nextjs.js +40 -0
- package/dist/state/session-manager.d.ts +13 -0
- package/dist/state/session-manager.js +22 -0
- package/dist/telemetry/reporter.d.ts +32 -0
- package/dist/telemetry/reporter.js +86 -0
- package/dist/types.d.ts +206 -0
- package/dist/types.js +14 -0
- package/dist/update-checker.d.ts +21 -0
- package/dist/update-checker.js +218 -0
- package/dist/utils/logger.d.ts +40 -0
- package/dist/utils/logger.js +79 -0
- package/dist/utils/rule-validator.d.ts +15 -0
- package/dist/utils/rule-validator.js +143 -0
- package/dist/utils/safe-regex.d.ts +53 -0
- package/dist/utils/safe-regex.js +220 -0
- package/dist/wardn.d.ts +67 -0
- package/dist/wardn.js +443 -0
- package/package.json +47 -0
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Safe Regex Utilities - ReDoS Prevention
|
|
4
|
+
*
|
|
5
|
+
* Provides regex validation and safe execution with timeout protection.
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.convertPCREModifiers = convertPCREModifiers;
|
|
9
|
+
exports.validateRegexPattern = validateRegexPattern;
|
|
10
|
+
exports.getPatternErrorMetrics = getPatternErrorMetrics;
|
|
11
|
+
exports.resetPatternErrorMetrics = resetPatternErrorMetrics;
|
|
12
|
+
exports.getCachedRegex = getCachedRegex;
|
|
13
|
+
exports.safeRegexExec = safeRegexExec;
|
|
14
|
+
exports.safeRegexTest = safeRegexTest;
|
|
15
|
+
const logger_1 = require("./logger");
|
|
16
|
+
// Patterns that are known to cause catastrophic backtracking
|
|
17
|
+
const DANGEROUS_PATTERNS = [
|
|
18
|
+
/\(\?[^)]*\+[^)]*\)\+/, // Nested quantifiers with +
|
|
19
|
+
/\(\?[^)]*\*[^)]*\)\+/, // Nested quantifiers with *
|
|
20
|
+
/\(\?[^)]*\+[^)]*\)\*/, // Nested quantifiers
|
|
21
|
+
/\(\?[^)]*\*[^)]*\)\*/, // Nested quantifiers
|
|
22
|
+
/\([^)]+\)\{[0-9]+,\}/, // Unbounded repetition of groups
|
|
23
|
+
/\.\*\.\*/, // Multiple greedy wildcards
|
|
24
|
+
/\.\+\.\+/, // Multiple greedy wildcards
|
|
25
|
+
/\([^)]*\|[^)]*\)\+/, // Alternation with quantifier
|
|
26
|
+
/\([^)]*\|[^)]*\)\*/, // Alternation with quantifier
|
|
27
|
+
];
|
|
28
|
+
// Maximum allowed regex pattern length
|
|
29
|
+
const MAX_PATTERN_LENGTH = 1000;
|
|
30
|
+
// Maximum allowed quantifier value
|
|
31
|
+
const MAX_QUANTIFIER = 100;
|
|
32
|
+
/**
|
|
33
|
+
* Convert PCRE inline modifiers to JavaScript flags
|
|
34
|
+
* Handles: (?i), (?m), (?s), (?x), and combinations like (?im)
|
|
35
|
+
*/
|
|
36
|
+
function convertPCREModifiers(pattern) {
|
|
37
|
+
let convertedPattern = pattern;
|
|
38
|
+
let flags = "";
|
|
39
|
+
let converted = false;
|
|
40
|
+
// Match PCRE inline modifiers: (?i), (?m), (?s), (?im), etc.
|
|
41
|
+
const pcreModifierRegex = /\(\?([imsx]+)\)/g;
|
|
42
|
+
const matches = [...pattern.matchAll(pcreModifierRegex)];
|
|
43
|
+
if (matches.length === 0) {
|
|
44
|
+
return { pattern, flags: "", converted: false };
|
|
45
|
+
}
|
|
46
|
+
const unsupportedModifiers = [];
|
|
47
|
+
const flagSet = new Set();
|
|
48
|
+
for (const match of matches) {
|
|
49
|
+
const modifiers = match[1];
|
|
50
|
+
for (const modifier of modifiers) {
|
|
51
|
+
switch (modifier) {
|
|
52
|
+
case "i": // Case insensitive - supported
|
|
53
|
+
flagSet.add("i");
|
|
54
|
+
break;
|
|
55
|
+
case "m": // Multiline - supported
|
|
56
|
+
flagSet.add("m");
|
|
57
|
+
break;
|
|
58
|
+
case "s": // Dotall - supported in ES2018+
|
|
59
|
+
flagSet.add("s");
|
|
60
|
+
break;
|
|
61
|
+
case "x": // Extended/verbose - NOT supported in JavaScript
|
|
62
|
+
unsupportedModifiers.push("x (extended/verbose)");
|
|
63
|
+
break;
|
|
64
|
+
default:
|
|
65
|
+
unsupportedModifiers.push(modifier);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// Remove the PCRE modifier from pattern
|
|
69
|
+
convertedPattern = convertedPattern.replace(match[0], "");
|
|
70
|
+
converted = true;
|
|
71
|
+
}
|
|
72
|
+
if (unsupportedModifiers.length > 0) {
|
|
73
|
+
return {
|
|
74
|
+
pattern: convertedPattern,
|
|
75
|
+
flags: "",
|
|
76
|
+
converted: false,
|
|
77
|
+
error: `Unsupported PCRE modifiers: ${unsupportedModifiers.join(", ")}`,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
flags = Array.from(flagSet).join("");
|
|
81
|
+
return {
|
|
82
|
+
pattern: convertedPattern,
|
|
83
|
+
flags,
|
|
84
|
+
converted: true,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Validate a regex pattern for potential ReDoS vulnerabilities
|
|
89
|
+
*/
|
|
90
|
+
function validateRegexPattern(pattern) {
|
|
91
|
+
// Check pattern length
|
|
92
|
+
if (pattern.length > MAX_PATTERN_LENGTH) {
|
|
93
|
+
return {
|
|
94
|
+
valid: false,
|
|
95
|
+
error: `Pattern exceeds maximum length of ${MAX_PATTERN_LENGTH} characters`,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
// Check for dangerous patterns
|
|
99
|
+
for (const dangerous of DANGEROUS_PATTERNS) {
|
|
100
|
+
if (dangerous.test(pattern)) {
|
|
101
|
+
return {
|
|
102
|
+
valid: false,
|
|
103
|
+
error: "Pattern contains potentially dangerous nested quantifiers",
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// Check for large quantifiers like {1000,}
|
|
108
|
+
const quantifierMatch = pattern.match(/\{(\d+)(,(\d*))?\}/g);
|
|
109
|
+
if (quantifierMatch) {
|
|
110
|
+
for (const q of quantifierMatch) {
|
|
111
|
+
const nums = q.match(/\d+/g);
|
|
112
|
+
if (nums) {
|
|
113
|
+
for (const num of nums) {
|
|
114
|
+
if (parseInt(num, 10) > MAX_QUANTIFIER) {
|
|
115
|
+
return {
|
|
116
|
+
valid: false,
|
|
117
|
+
error: `Quantifier value ${num} exceeds maximum of ${MAX_QUANTIFIER}`,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
// Try to compile the regex to catch syntax errors
|
|
125
|
+
try {
|
|
126
|
+
new RegExp(pattern);
|
|
127
|
+
}
|
|
128
|
+
catch (e) {
|
|
129
|
+
return {
|
|
130
|
+
valid: false,
|
|
131
|
+
error: `Invalid regex syntax: ${e instanceof Error ? e.message : "Unknown error"}`,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
return { valid: true };
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Cached regex instances to avoid recompilation
|
|
138
|
+
*/
|
|
139
|
+
const regexCache = new Map();
|
|
140
|
+
const MAX_CACHE_SIZE = 100;
|
|
141
|
+
const errorMetrics = {
|
|
142
|
+
pcreConversionFailures: 0,
|
|
143
|
+
validationFailures: 0,
|
|
144
|
+
regexCompilationFailures: 0,
|
|
145
|
+
totalPatternsProcessed: 0,
|
|
146
|
+
};
|
|
147
|
+
/**
|
|
148
|
+
* Get current pattern error metrics
|
|
149
|
+
*/
|
|
150
|
+
function getPatternErrorMetrics() {
|
|
151
|
+
return { ...errorMetrics };
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Reset pattern error metrics (useful for testing)
|
|
155
|
+
*/
|
|
156
|
+
function resetPatternErrorMetrics() {
|
|
157
|
+
errorMetrics.pcreConversionFailures = 0;
|
|
158
|
+
errorMetrics.validationFailures = 0;
|
|
159
|
+
errorMetrics.regexCompilationFailures = 0;
|
|
160
|
+
errorMetrics.totalPatternsProcessed = 0;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Get a cached regex instance or create a new one
|
|
164
|
+
*/
|
|
165
|
+
function getCachedRegex(pattern, flags = "") {
|
|
166
|
+
errorMetrics.totalPatternsProcessed++;
|
|
167
|
+
// Convert PCRE modifiers first
|
|
168
|
+
const pcreResult = convertPCREModifiers(pattern);
|
|
169
|
+
if (pcreResult.converted && pcreResult.error) {
|
|
170
|
+
// Unsupported PCRE modifiers - skip this pattern
|
|
171
|
+
errorMetrics.pcreConversionFailures++;
|
|
172
|
+
logger_1.logger.warn(`Skipping pattern with unsupported PCRE modifiers: ${pcreResult.error}`);
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
// Use converted pattern and merge flags
|
|
176
|
+
const finalPattern = pcreResult.pattern;
|
|
177
|
+
const finalFlags = pcreResult.converted
|
|
178
|
+
? pcreResult.flags + flags // PCRE flags + explicit flags
|
|
179
|
+
: flags;
|
|
180
|
+
const cacheKey = `${finalPattern}:${finalFlags}`;
|
|
181
|
+
if (regexCache.has(cacheKey)) {
|
|
182
|
+
return regexCache.get(cacheKey);
|
|
183
|
+
}
|
|
184
|
+
const validation = validateRegexPattern(finalPattern);
|
|
185
|
+
if (!validation.valid) {
|
|
186
|
+
errorMetrics.validationFailures++;
|
|
187
|
+
logger_1.logger.warn(`Unsafe regex pattern rejected: ${validation.error}`);
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
try {
|
|
191
|
+
const regex = new RegExp(finalPattern, finalFlags);
|
|
192
|
+
// Evict oldest entries if cache is full
|
|
193
|
+
if (regexCache.size >= MAX_CACHE_SIZE) {
|
|
194
|
+
const firstKey = regexCache.keys().next().value;
|
|
195
|
+
if (firstKey)
|
|
196
|
+
regexCache.delete(firstKey);
|
|
197
|
+
}
|
|
198
|
+
regexCache.set(cacheKey, regex);
|
|
199
|
+
return regex;
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
errorMetrics.regexCompilationFailures++;
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Execute regex with content length limit
|
|
208
|
+
*/
|
|
209
|
+
function safeRegexExec(regex, content, maxLength = 50000) {
|
|
210
|
+
// Truncate content if too long
|
|
211
|
+
const safeContent = content.length > maxLength ? content.substring(0, maxLength) : content;
|
|
212
|
+
return regex.exec(safeContent);
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Safe regex test with content length limit
|
|
216
|
+
*/
|
|
217
|
+
function safeRegexTest(regex, content, maxLength = 50000) {
|
|
218
|
+
const safeContent = content.length > maxLength ? content.substring(0, maxLength) : content;
|
|
219
|
+
return regex.test(safeContent);
|
|
220
|
+
}
|
package/dist/wardn.d.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { WardnConfig, ScanResult, WardnRequest, Rule } from "./types";
|
|
2
|
+
import { PatternErrorMetrics } from "./utils/safe-regex";
|
|
3
|
+
import { SessionStateProvider } from "./state/session-manager";
|
|
4
|
+
export declare class Wardn {
|
|
5
|
+
private static instance;
|
|
6
|
+
private config;
|
|
7
|
+
private stateProvider;
|
|
8
|
+
private telemetry;
|
|
9
|
+
private patternDetector;
|
|
10
|
+
private sequenceDetector;
|
|
11
|
+
private stateDetector;
|
|
12
|
+
private semanticDetector;
|
|
13
|
+
private pollTimer;
|
|
14
|
+
private isShutdown;
|
|
15
|
+
private lastRuleUpdate;
|
|
16
|
+
private updateCount;
|
|
17
|
+
private updateCountResetTime;
|
|
18
|
+
private constructor();
|
|
19
|
+
private initTelemetry;
|
|
20
|
+
static getInstance(): Wardn;
|
|
21
|
+
static init(config: WardnConfig, stateProvider?: SessionStateProvider): Wardn;
|
|
22
|
+
/**
|
|
23
|
+
* Reset the singleton instance (useful for testing)
|
|
24
|
+
*/
|
|
25
|
+
static reset(): void;
|
|
26
|
+
/**
|
|
27
|
+
* Shutdown the Wardn instance, cleaning up resources
|
|
28
|
+
*/
|
|
29
|
+
shutdown(): void;
|
|
30
|
+
/**
|
|
31
|
+
* Scan a request for security violations
|
|
32
|
+
* @param request The agent request context (prompt, tools, etc.)
|
|
33
|
+
* @returns ScanResult with allowed/blocked status and violations
|
|
34
|
+
*/
|
|
35
|
+
scan(request: WardnRequest): Promise<ScanResult>;
|
|
36
|
+
/** Normalize request to ToolData format */
|
|
37
|
+
private normalizeRequest;
|
|
38
|
+
/** Run detector for a single rule */
|
|
39
|
+
private detectViolation;
|
|
40
|
+
/** Handle semantic detection */
|
|
41
|
+
private detectSemanticViolation;
|
|
42
|
+
/** Report violation to telemetry */
|
|
43
|
+
private reportViolation;
|
|
44
|
+
/** Report scan completion to telemetry */
|
|
45
|
+
private reportScanComplete;
|
|
46
|
+
/**
|
|
47
|
+
* Update rules dynamically
|
|
48
|
+
* This allows external systems (like MCP server with Supabase threat rules)
|
|
49
|
+
* to update the rule set without restarting
|
|
50
|
+
*
|
|
51
|
+
* @param rules New rule set to use
|
|
52
|
+
* @returns void
|
|
53
|
+
*/
|
|
54
|
+
updateRules(rules: Rule[]): void;
|
|
55
|
+
/**
|
|
56
|
+
* Get current rules
|
|
57
|
+
* @returns Current rule set
|
|
58
|
+
*/
|
|
59
|
+
getRules(): readonly Rule[];
|
|
60
|
+
/**
|
|
61
|
+
* Get pattern error metrics for monitoring
|
|
62
|
+
* @returns Pattern error statistics
|
|
63
|
+
*/
|
|
64
|
+
getPatternErrorMetrics(): Readonly<PatternErrorMetrics>;
|
|
65
|
+
/** Poll remote rules for updates with validation */
|
|
66
|
+
private pollRemoteRules;
|
|
67
|
+
}
|