@wardnmesh/sdk-node 0.2.3 → 0.4.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/README.md CHANGED
@@ -1,7 +1,20 @@
1
1
  # @wardnmesh/sdk-node
2
2
 
3
+ > **Latest Version: v0.4.0** (Released 2026-01-17)
4
+
3
5
  **WardnMesh.AI** (formerly AgentGuard) is an active defense middleware for AI Agents. This SDK allows you to verify LLM inputs/outputs, block prompt injections, and prevent data exfiltration in real-time.
4
6
 
7
+ ## ✨ What's New in v0.4.0
8
+
9
+ 🚨 **Breaking Changes**: Action-based architecture replaces boolean `allowed` field.
10
+
11
+ - ✅ **Granular Actions**: `block`, `confirm`, `warn`, `log`, `allow` instead of `true`/`false`
12
+ - ✅ **Confirmation Support**: Native user confirmation for high-risk operations
13
+ - ✅ **Enhanced Context**: Richer violation metadata with `recommendedAction` and `scope`
14
+ - ✅ **Fail-Closed Security**: Defensive design defaults to `'block'` on invalid states
15
+
16
+ 📖 [Migration Guide](MIGRATION_v0.4.0.md) | [CHANGELOG](CHANGELOG.md)
17
+
5
18
  ## Features
6
19
 
7
20
  - 🛡️ **Active Defense**: Blocks prompt injections, jailbreaks, and PII leaks.
@@ -13,9 +26,68 @@
13
26
  ## Installation
14
27
 
15
28
  ```bash
16
- npm install @wardnmesh/sdk-node
29
+ npm install @wardnmesh/sdk-node@0.4.0
17
30
  # or
18
- yarn add @wardnmesh/sdk-node
31
+ yarn add @wardnmesh/sdk-node@0.4.0
32
+ ```
33
+
34
+ ## v0.4.0 API Overview
35
+
36
+ The new action-based API provides fine-grained control over threat responses:
37
+
38
+ ```typescript
39
+ import { Wardn } from '@wardnmesh/sdk-node';
40
+
41
+ const guard = Wardn.getInstance();
42
+ const result = await guard.scan({ prompt: userInput });
43
+
44
+ // Handle different threat levels
45
+ switch (result.action) {
46
+ case 'block':
47
+ // Critical violation - deny immediately
48
+ throw new Error('Security violation');
49
+
50
+ case 'confirm':
51
+ // High-risk - request user approval
52
+ const approved = await getUserConfirmation(result.confirmationDetails);
53
+ if (!approved) throw new Error('Operation denied');
54
+ break;
55
+
56
+ case 'warn':
57
+ // Medium-risk - log warning and allow
58
+ console.warn('Security warning:', result.violations);
59
+ break;
60
+
61
+ case 'log':
62
+ // Low-risk - log for monitoring
63
+ console.log('Security event:', result.violations);
64
+ break;
65
+
66
+ case 'allow':
67
+ // No violations - safe to proceed
68
+ break;
69
+ }
70
+
71
+ // Continue with your logic...
72
+ ```
73
+
74
+ ### Confirmation Dialog Example
75
+
76
+ When `action === 'confirm'`, the SDK provides pre-formatted confirmation context:
77
+
78
+ ```typescript
79
+ if (result.action === 'confirm') {
80
+ const { message, timeout, defaultAction } = result.confirmationDetails;
81
+
82
+ console.log(message);
83
+ // ⚠️ Security Alert
84
+ // Rule: recursive_delete
85
+ // Severity: HIGH
86
+ // Description: Detected dangerous recursive delete operation
87
+
88
+ const approved = await getUserInput(); // Your confirmation UI
89
+ if (!approved) throw new Error('Operation denied by user');
90
+ }
19
91
  ```
20
92
 
21
93
  ## Quick Start
@@ -1,18 +1,33 @@
1
- import { ToolData, Violation, Rule, Detector, StateProvider, DetectorType } from '../types';
1
+ import { ToolData, Violation, Rule, Detector, StateProvider, DetectorType, ThreatAction } from '../types';
2
2
  /**
3
- * Abstract base detector class
3
+ * Map rule severity to recommended action, with optional rule action override.
4
4
  *
5
+ * Default mapping:
6
+ * - critical -> block (immediate threat)
7
+ * - high -> confirm (needs user approval)
8
+ * - medium -> warn (notify but allow)
9
+ * - low -> log (record only)
10
+ *
11
+ * Rules can override default mapping via rule.action.
12
+ */
13
+ export declare function mapSeverityToAction(rule: Rule): ThreatAction;
14
+ /**
15
+ * Map rule category to scope description.
16
+ */
17
+ export declare function mapCategoryToScope(category: string): string;
18
+ /**
19
+ * Abstract base detector class.
5
20
  * Provides common functionality for all detectors.
6
21
  */
7
22
  export declare abstract class BaseDetector implements Detector {
8
23
  abstract detect(toolData: ToolData, rule: Rule, sessionState: StateProvider): Violation | null;
9
24
  abstract getType(): DetectorType | string;
10
25
  /**
11
- * Generate unique violation ID
26
+ * Generate unique violation ID using cryptographically secure random.
12
27
  */
13
28
  protected generateViolationId(): string;
14
29
  /**
15
- * Create violation object
30
+ * Create violation object with standardized structure.
16
31
  */
17
32
  protected createViolation(rule: Rule, toolData: ToolData, additionalInfo?: Record<string, unknown>): Violation;
18
33
  /**
@@ -1,23 +1,64 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.BaseDetector = void 0;
4
+ exports.mapSeverityToAction = mapSeverityToAction;
5
+ exports.mapCategoryToScope = mapCategoryToScope;
6
+ const crypto_1 = require("crypto");
4
7
  /**
5
- * Abstract base detector class
8
+ * Map rule severity to recommended action, with optional rule action override.
6
9
  *
10
+ * Default mapping:
11
+ * - critical -> block (immediate threat)
12
+ * - high -> confirm (needs user approval)
13
+ * - medium -> warn (notify but allow)
14
+ * - low -> log (record only)
15
+ *
16
+ * Rules can override default mapping via rule.action.
17
+ */
18
+ function mapSeverityToAction(rule) {
19
+ if (rule.action) {
20
+ return rule.action;
21
+ }
22
+ switch (rule.severity) {
23
+ case 'critical': return 'block';
24
+ case 'high': return 'confirm';
25
+ case 'medium': return 'warn';
26
+ case 'low': return 'log';
27
+ default: return 'block';
28
+ }
29
+ }
30
+ /**
31
+ * Map rule category to scope description.
32
+ */
33
+ function mapCategoryToScope(category) {
34
+ switch (category) {
35
+ case 'workflow': return 'Workflow safety';
36
+ case 'quality': return 'Code quality';
37
+ case 'safety': return 'Security';
38
+ case 'network_boundary': return 'Network access';
39
+ case 'supply_chain': return 'Supply chain';
40
+ default: return 'Security violation';
41
+ }
42
+ }
43
+ /**
44
+ * Abstract base detector class.
7
45
  * Provides common functionality for all detectors.
8
46
  */
9
47
  class BaseDetector {
10
48
  /**
11
- * Generate unique violation ID
49
+ * Generate unique violation ID using cryptographically secure random.
12
50
  */
13
51
  generateViolationId() {
14
- // Note: Using substring() instead of deprecated substr()
15
- return `violation_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
52
+ return `violation_${(0, crypto_1.randomUUID)()}`;
16
53
  }
17
54
  /**
18
- * Create violation object
55
+ * Create violation object with standardized structure.
19
56
  */
20
57
  createViolation(rule, toolData, additionalInfo) {
58
+ const filePath = (toolData.parameters.file_path ||
59
+ toolData.parameters.TargetFile ||
60
+ toolData.parameters.AbsolutePath ||
61
+ toolData.parameters.path);
21
62
  return {
22
63
  id: this.generateViolationId(),
23
64
  ruleId: rule.id,
@@ -27,10 +68,12 @@ class BaseDetector {
27
68
  context: {
28
69
  toolName: toolData.toolName,
29
70
  toolData,
30
- filePath: (toolData.parameters.file_path || toolData.parameters.TargetFile || toolData.parameters.AbsolutePath || toolData.parameters.path),
31
- additionalInfo
71
+ filePath,
72
+ additionalInfo,
32
73
  },
33
- timestamp: new Date().toISOString()
74
+ timestamp: new Date().toISOString(),
75
+ recommendedAction: mapSeverityToAction(rule),
76
+ scope: mapCategoryToScope(rule.category),
34
77
  };
35
78
  }
36
79
  /**
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.SequenceDetector = void 0;
4
4
  const base_1 = require("./base");
5
+ const safe_regex_1 = require("../utils/safe-regex");
5
6
  class SequenceDetector extends base_1.BaseDetector {
6
7
  getType() {
7
8
  return 'sequence';
@@ -60,7 +61,12 @@ class SequenceDetector extends base_1.BaseDetector {
60
61
  }
61
62
  if (step.matchesPattern) {
62
63
  const currentValue = this.extractValue(currentTool, step.extractPath);
63
- const regex = new RegExp(step.matchesPattern);
64
+ // SECURITY FIX Round 16: Use getCachedRegex for ReDoS protection
65
+ const regex = (0, safe_regex_1.getCachedRegex)(step.matchesPattern);
66
+ if (!regex) {
67
+ // Invalid or unsafe pattern - skip this check
68
+ return { matched: true };
69
+ }
64
70
  if (typeof currentValue === 'string' && !regex.test(currentValue)) {
65
71
  return {
66
72
  matched: false,
@@ -103,7 +109,12 @@ class SequenceDetector extends base_1.BaseDetector {
103
109
  }
104
110
  }
105
111
  if (step.matchesPattern) {
106
- const regex = new RegExp(step.matchesPattern);
112
+ // SECURITY FIX Round 16: Use getCachedRegex for ReDoS protection
113
+ const regex = (0, safe_regex_1.getCachedRegex)(step.matchesPattern);
114
+ if (!regex) {
115
+ // Invalid or unsafe pattern - skip this tool
116
+ continue;
117
+ }
107
118
  if (typeof value === 'string' && !regex.test(value)) {
108
119
  continue;
109
120
  }
@@ -123,7 +134,13 @@ class SequenceDetector extends base_1.BaseDetector {
123
134
  }
124
135
  if (lastStep.matchesPattern) {
125
136
  const val = this.extractValue(toolData, lastStep.extractPath);
126
- if (typeof val === 'string' && !new RegExp(lastStep.matchesPattern).test(val)) {
137
+ // SECURITY FIX Round 16: Use getCachedRegex for ReDoS protection
138
+ const regex = (0, safe_regex_1.getCachedRegex)(lastStep.matchesPattern);
139
+ if (!regex) {
140
+ // Invalid or unsafe pattern - skip this check (allow through)
141
+ return null;
142
+ }
143
+ if (typeof val === 'string' && !regex.test(val)) {
127
144
  return null;
128
145
  }
129
146
  }
@@ -5,4 +5,5 @@ export declare class StateDetector extends BaseDetector {
5
5
  detect(toolData: ToolData, rule: Rule, sessionState: StateProvider): Violation | null;
6
6
  private isTriggerEvent;
7
7
  private updateStateFromTool;
8
+ private setStateWithTimestamp;
8
9
  }
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.StateDetector = void 0;
4
4
  const base_1 = require("./base");
5
+ const safe_regex_1 = require("../utils/safe-regex");
5
6
  class StateDetector extends base_1.BaseDetector {
6
7
  getType() {
7
8
  return 'state';
@@ -9,78 +10,56 @@ class StateDetector extends base_1.BaseDetector {
9
10
  detect(toolData, rule, sessionState) {
10
11
  const config = rule.detector.config;
11
12
  this.updateStateFromTool(config, toolData, sessionState);
12
- if (this.isTriggerEvent(config, toolData)) {
13
- const currentState = sessionState.getCustomState(config.requiredState);
14
- if (currentState !== config.targetStateValue) {
15
- return {
16
- id: this.generateViolationId(),
17
- ruleId: rule.id,
18
- ruleName: rule.name,
19
- severity: rule.severity,
20
- timestamp: new Date().toISOString(),
21
- description: rule.description,
22
- context: {
23
- toolName: toolData.toolName,
24
- toolData: toolData,
25
- additionalInfo: {
26
- message: `Required state '${config.requiredState}' is '${currentState || 'undefined'}', expected '${config.targetStateValue}'.`
27
- }
28
- }
29
- };
30
- }
31
- if (config.stateDerivation?.validityDurationMs) {
32
- const lastUpdate = sessionState.getCustomState(`${config.requiredState}_timestamp`);
33
- if (lastUpdate && typeof lastUpdate === 'string') {
34
- const timeDiff = new Date().getTime() - new Date(lastUpdate).getTime();
35
- if (timeDiff > config.stateDerivation.validityDurationMs) {
36
- return {
37
- id: this.generateViolationId(),
38
- ruleId: rule.id,
39
- ruleName: rule.name,
40
- severity: rule.severity,
41
- timestamp: new Date().toISOString(),
42
- description: rule.description,
43
- context: {
44
- toolName: toolData.toolName,
45
- toolData: toolData,
46
- additionalInfo: {
47
- message: `Required state '${config.requiredState}' has expired (last verified ${Math.floor(timeDiff / 1000)}s ago).`
48
- }
49
- }
50
- };
51
- }
13
+ if (!this.isTriggerEvent(config, toolData)) {
14
+ return null;
15
+ }
16
+ const currentState = sessionState.getCustomState(config.requiredState);
17
+ if (currentState !== config.targetStateValue) {
18
+ const message = `Required state '${config.requiredState}' is '${currentState ?? 'undefined'}', expected '${config.targetStateValue}'.`;
19
+ return this.createViolation(rule, toolData, { message });
20
+ }
21
+ if (config.stateDerivation?.validityDurationMs) {
22
+ const lastUpdate = sessionState.getCustomState(`${config.requiredState}_timestamp`);
23
+ if (lastUpdate && typeof lastUpdate === 'string') {
24
+ const timeDiff = Date.now() - new Date(lastUpdate).getTime();
25
+ if (timeDiff > config.stateDerivation.validityDurationMs) {
26
+ const message = `Required state '${config.requiredState}' has expired (last verified ${Math.floor(timeDiff / 1000)}s ago).`;
27
+ return this.createViolation(rule, toolData, { message });
52
28
  }
53
29
  }
54
30
  }
55
31
  return null;
56
32
  }
57
33
  isTriggerEvent(config, toolData) {
58
- if (toolData.toolName !== config.trigger.tool)
34
+ if (toolData.toolName !== config.trigger.tool) {
35
+ return false;
36
+ }
37
+ if (!config.trigger.parameterMatch) {
38
+ return true;
39
+ }
40
+ const paramValue = this.extractValue(toolData.parameters, config.trigger.parameterMatch.key);
41
+ if (!paramValue || typeof paramValue !== 'string') {
59
42
  return false;
60
- if (config.trigger.parameterMatch) {
61
- const paramValue = this.extractValue(toolData.parameters, config.trigger.parameterMatch.key);
62
- if (!paramValue || typeof paramValue !== 'string')
63
- return false;
64
- const regex = new RegExp(config.trigger.parameterMatch.valuePattern, 'i');
65
- return regex.test(paramValue);
66
43
  }
67
- return true;
44
+ const regex = (0, safe_regex_1.getCachedRegex)(config.trigger.parameterMatch.valuePattern, 'i');
45
+ return regex ? regex.test(paramValue) : false;
68
46
  }
69
47
  updateStateFromTool(config, toolData, sessionState) {
70
- if (config.stateDerivation && toolData.toolName === config.stateDerivation.fromTool) {
71
- // Heuristic check for 'test' in command line if it's run_command
72
- if (toolData.toolName === 'run_command' && toolData.parameters.CommandLine) {
73
- const commandLine = toolData.parameters.CommandLine;
74
- if (commandLine.includes('test') || commandLine.includes('vitest')) {
75
- sessionState.setCustomState(config.stateDerivation.setState, config.stateDerivation.setValue);
76
- sessionState.setCustomState(`${config.stateDerivation.setState}_timestamp`, new Date().toISOString());
77
- }
78
- }
79
- else {
80
- sessionState.setCustomState(config.stateDerivation.setState, config.stateDerivation.setValue);
81
- sessionState.setCustomState(`${config.stateDerivation.setState}_timestamp`, new Date().toISOString());
48
+ if (!config.stateDerivation || toolData.toolName !== config.stateDerivation.fromTool) {
49
+ return;
50
+ }
51
+ // For run_command tool, only update state if command contains test keywords
52
+ if (toolData.toolName === 'run_command' && toolData.parameters.CommandLine) {
53
+ const commandLine = toolData.parameters.CommandLine;
54
+ if (!commandLine.includes('test') && !commandLine.includes('vitest')) {
55
+ return;
82
56
  }
83
57
  }
58
+ this.setStateWithTimestamp(sessionState, config.stateDerivation.setState, config.stateDerivation.setValue);
59
+ }
60
+ setStateWithTimestamp(sessionState, key, value) {
61
+ sessionState.setCustomState(key, value);
62
+ sessionState.setCustomState(`${key}_timestamp`, new Date().toISOString());
84
63
  }
85
64
  }
86
65
  exports.StateDetector = StateDetector;
@@ -27,7 +27,8 @@ function createWardnMiddleware(config) {
27
27
  prompt: content,
28
28
  context: { source: "vercel-ai-sdk" },
29
29
  });
30
- if (!result.allowed) {
30
+ // v0.3.0: Check action instead of allowed
31
+ if (result.action === 'block') {
31
32
  throw new Error(`Security Violation: ${result.violations[0]?.description || "Blocked by Wardn"}`);
32
33
  }
33
34
  };
@@ -29,7 +29,8 @@ function createExpressMiddleware(guard, config) {
29
29
  return next();
30
30
  }
31
31
  const result = await guard.scan(wardnRequest);
32
- if (!result.allowed) {
32
+ // v0.3.0: Check action instead of allowed
33
+ if (result.action === 'block') {
33
34
  if (config?.onBlock) {
34
35
  return config.onBlock(req, res, result);
35
36
  }
@@ -28,7 +28,8 @@ function createNextMiddleware(config) {
28
28
  ip: req.ip || req.headers.get("x-forwarded-for") || "unknown",
29
29
  },
30
30
  });
31
- if (!result.allowed) {
31
+ // v0.3.0: Check action instead of allowed
32
+ if (result.action === 'block') {
32
33
  return server_1.NextResponse.json({
33
34
  error: "Request blocked by Wardn Security Policy",
34
35
  code: "WARDN_BLOCK",
package/dist/types.d.ts CHANGED
@@ -2,8 +2,9 @@
2
2
  * Rule Schema - Core type definitions for WardnMesh rules
3
3
  */
4
4
  export type RuleCategory = "workflow" | "quality" | "safety" | "network_boundary" | "supply_chain";
5
- export type Severity = "critical" | "major" | "minor";
5
+ export type Severity = "critical" | "high" | "medium" | "low";
6
6
  export type DetectorType = "sequence" | "state" | "pattern" | "content_analysis" | "semantic";
7
+ export type ThreatAction = "allow" | "block" | "warn" | "log" | "confirm";
7
8
  export type EscalationLevel = "none" | "warning" | "critical" | "block";
8
9
  export interface SequenceDetectorConfig {
9
10
  lookback: number;
@@ -88,6 +89,7 @@ export interface Rule {
88
89
  };
89
90
  escalation: EscalationConfig;
90
91
  autofix?: AutofixConfig;
92
+ action?: ThreatAction;
91
93
  }
92
94
  export interface ToolData {
93
95
  toolName: string;
@@ -110,6 +112,8 @@ export interface Violation {
110
112
  description: string;
111
113
  context: ViolationContext;
112
114
  timestamp: string;
115
+ recommendedAction: ThreatAction;
116
+ scope?: string;
113
117
  }
114
118
  export interface ViolationContext {
115
119
  toolName: string;
@@ -198,9 +202,17 @@ export interface WardnRequest {
198
202
  metadata?: Record<string, unknown>;
199
203
  }
200
204
  export interface ScanResult {
201
- allowed: boolean;
205
+ action: ThreatAction;
202
206
  violations: Violation[];
203
207
  latencyMs: number;
204
208
  metadata: Record<string, unknown>;
209
+ confirmationDetails?: {
210
+ message: string;
211
+ timeout: number;
212
+ defaultAction: 'allow' | 'block';
213
+ ruleId: string;
214
+ ruleName: string;
215
+ severity: Severity;
216
+ };
205
217
  }
206
218
  export type PatternConfig = PatternDetectorConfig;
@@ -12,7 +12,7 @@ const VALID_DETECTOR_TYPES = [
12
12
  "content_analysis",
13
13
  "semantic",
14
14
  ];
15
- const VALID_SEVERITIES = ["critical", "major", "minor"];
15
+ const VALID_SEVERITIES = ["critical", "high", "medium", "low"];
16
16
  const VALID_CATEGORIES = [
17
17
  "workflow",
18
18
  "quality",
@@ -51,3 +51,12 @@ export declare function safeRegexExec(regex: RegExp, content: string, maxLength?
51
51
  * Safe regex test with content length limit
52
52
  */
53
53
  export declare function safeRegexTest(regex: RegExp, content: string, maxLength?: number): boolean;
54
+ /**
55
+ * Create a safe regex with validation
56
+ * Throws error if pattern is unsafe
57
+ */
58
+ export declare function createSafeRegex(pattern: string, flags?: string): RegExp;
59
+ /**
60
+ * Validate multiple regex patterns
61
+ */
62
+ export declare function validateRegexPatterns(patterns: string[]): RegexValidationResult;
@@ -12,18 +12,23 @@ exports.resetPatternErrorMetrics = resetPatternErrorMetrics;
12
12
  exports.getCachedRegex = getCachedRegex;
13
13
  exports.safeRegexExec = safeRegexExec;
14
14
  exports.safeRegexTest = safeRegexTest;
15
+ exports.createSafeRegex = createSafeRegex;
16
+ exports.validateRegexPatterns = validateRegexPatterns;
15
17
  const logger_1 = require("./logger");
16
18
  // Patterns that are known to cause catastrophic backtracking
19
+ // SECURITY FIX: Detect dangerous patterns in BOTH non-capturing (?...) AND capturing (...) groups
20
+ // VULNERABILITY (v1): Required `\?` which only matched (?...) groups, missing dangerous (...) groups
21
+ // FIX (v2): Remove `\?` requirement to catch all dangerous nested quantifiers
17
22
  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
23
+ /\([^)]*\+[^)]*\)\+/, // Nested quantifiers: (...+...)+ or (?...+...)+
24
+ /\([^)]*\*[^)]*\)\+/, // Nested quantifiers: (...*...)+ or (?...*...)+
25
+ /\([^)]*\+[^)]*\)\*/, // Nested quantifiers: (...+...)* or (?...+...)*
26
+ /\([^)]*\*[^)]*\)\*/, // Nested quantifiers: (...*...)* or (?...*...)*
27
+ /\([^)]+\)\{[0-9]+,\}/, // Unbounded repetition of groups: (...){10,}
28
+ /\.\*\.\*/, // Multiple greedy wildcards: .*.*
29
+ /\.\+\.\+/, // Multiple greedy wildcards: .+.+
30
+ /\([^)]*\|[^)]*\)\+/, // Alternation with quantifier: (a|b)+
31
+ /\([^)]*\|[^)]*\)\*/, // Alternation with quantifier: (a|b)*
27
32
  ];
28
33
  // Maximum allowed regex pattern length
29
34
  const MAX_PATTERN_LENGTH = 1000;
@@ -166,7 +171,12 @@ function getCachedRegex(pattern, flags = "") {
166
171
  errorMetrics.totalPatternsProcessed++;
167
172
  // Convert PCRE modifiers first
168
173
  const pcreResult = convertPCREModifiers(pattern);
169
- if (pcreResult.converted && pcreResult.error) {
174
+ // SECURITY FIX: Check for PCRE conversion errors
175
+ // VULNERABILITY (v1): Condition `converted && error` was always false when error exists
176
+ // - When unsupported modifiers found: converted=false, error=truthy
177
+ // - Condition: false && truthy = false (check skipped!)
178
+ // FIX (v2): Check error first, regardless of converted status
179
+ if (pcreResult.error) {
170
180
  // Unsupported PCRE modifiers - skip this pattern
171
181
  errorMetrics.pcreConversionFailures++;
172
182
  logger_1.logger.warn(`Skipping pattern with unsupported PCRE modifiers: ${pcreResult.error}`);
@@ -218,3 +228,26 @@ function safeRegexTest(regex, content, maxLength = 50000) {
218
228
  const safeContent = content.length > maxLength ? content.substring(0, maxLength) : content;
219
229
  return regex.test(safeContent);
220
230
  }
231
+ /**
232
+ * Create a safe regex with validation
233
+ * Throws error if pattern is unsafe
234
+ */
235
+ function createSafeRegex(pattern, flags = "") {
236
+ const validation = validateRegexPattern(pattern);
237
+ if (!validation.valid) {
238
+ throw new Error(validation.error || "Invalid regex pattern");
239
+ }
240
+ return new RegExp(pattern, flags);
241
+ }
242
+ /**
243
+ * Validate multiple regex patterns
244
+ */
245
+ function validateRegexPatterns(patterns) {
246
+ for (const pattern of patterns) {
247
+ const result = validateRegexPattern(pattern);
248
+ if (!result.valid) {
249
+ return result;
250
+ }
251
+ }
252
+ return { valid: true };
253
+ }
package/dist/wardn.d.ts CHANGED
@@ -33,13 +33,21 @@ export declare class Wardn {
33
33
  * @returns ScanResult with allowed/blocked status and violations
34
34
  */
35
35
  scan(request: WardnRequest): Promise<ScanResult>;
36
+ /**
37
+ * v0.3.0: Determine final action based on all violations
38
+ * Priority: block > confirm > warn > log > allow
39
+ *
40
+ * Defensive design: Uses fail-closed approach - defaults to 'block' if
41
+ * recommendedAction is missing or invalid, ensuring security by default.
42
+ */
43
+ private determineAction;
36
44
  /** Normalize request to ToolData format */
37
45
  private normalizeRequest;
38
46
  /** Run detector for a single rule */
39
47
  private detectViolation;
40
48
  /** Handle semantic detection */
41
49
  private detectSemanticViolation;
42
- /** Report violation to telemetry */
50
+ /** Report violation to telemetry (redacts sensitive toolData) */
43
51
  private reportViolation;
44
52
  /** Report scan completion to telemetry */
45
53
  private reportScanComplete;
package/dist/wardn.js CHANGED
@@ -4,6 +4,7 @@ exports.Wardn = void 0;
4
4
  const pattern_1 = require("./detectors/pattern");
5
5
  const sequence_1 = require("./detectors/sequence");
6
6
  const state_1 = require("./detectors/state");
7
+ const base_1 = require("./detectors/base");
7
8
  const safe_regex_1 = require("./utils/safe-regex");
8
9
  const update_checker_1 = require("./update-checker");
9
10
  const session_manager_1 = require("./state/session-manager");
@@ -175,7 +176,7 @@ class Wardn {
175
176
  async scan(request) {
176
177
  if (this.isShutdown) {
177
178
  return {
178
- allowed: true,
179
+ action: 'allow',
179
180
  violations: [],
180
181
  latencyMs: 0,
181
182
  metadata: { error: true, errorDetails: "Wardn instance is shutdown" },
@@ -201,12 +202,14 @@ class Wardn {
201
202
  if (violation) {
202
203
  violations.push(violation);
203
204
  stateAdapter.addViolation(violation);
204
- this.reportViolation(violation, toolData, currentSessionId);
205
+ this.reportViolation(violation, currentSessionId);
205
206
  }
206
207
  }
207
208
  await this.stateProvider.setState(currentSessionId, stateAdapter.exportState());
209
+ // v0.3.0: Determine action based on violations
210
+ const action = this.determineAction(violations);
208
211
  const result = {
209
- allowed: !violations.some((v) => v.severity === "critical"),
212
+ action,
210
213
  violations,
211
214
  latencyMs: Date.now() - startTime,
212
215
  metadata: {
@@ -214,6 +217,35 @@ class Wardn {
214
217
  sessionId: currentSessionId,
215
218
  },
216
219
  };
220
+ // v0.4.0: Add confirmation details if action is 'confirm'
221
+ if (action === 'confirm') {
222
+ const confirmViolation = violations.find(v => v.recommendedAction === 'confirm');
223
+ if (confirmViolation) {
224
+ // Extract metadata if available, otherwise use defaults
225
+ const metadata = (confirmViolation.context.additionalInfo?.metadata || {});
226
+ // Generate enhanced default message with violation context
227
+ const additionalDetails = confirmViolation.context.additionalInfo?.message
228
+ ? `\nDetails: ${confirmViolation.context.additionalInfo.message}`
229
+ : '';
230
+ const defaultMessage = `⚠️ Security Alert
231
+
232
+ Rule: ${confirmViolation.ruleName}
233
+ Severity: ${confirmViolation.severity.toUpperCase()}
234
+ Tool: ${confirmViolation.context.toolName}
235
+
236
+ Description: ${confirmViolation.description}${additionalDetails}
237
+
238
+ This operation requires your approval to proceed.`;
239
+ result.confirmationDetails = {
240
+ message: metadata.confirmationMessage || defaultMessage,
241
+ timeout: metadata.timeout || 30000,
242
+ defaultAction: metadata.defaultAction || 'block',
243
+ ruleId: confirmViolation.ruleId,
244
+ ruleName: confirmViolation.ruleName,
245
+ severity: confirmViolation.severity,
246
+ };
247
+ }
248
+ }
217
249
  this.reportScanComplete(result, currentSessionId);
218
250
  return result;
219
251
  }
@@ -229,9 +261,9 @@ class Wardn {
229
261
  },
230
262
  });
231
263
  // Configurable fail mode
232
- const allowed = failMode === "open";
264
+ const action = failMode === "open" ? "allow" : "block";
233
265
  return {
234
- allowed,
266
+ action,
235
267
  violations: [],
236
268
  latencyMs: Date.now() - startTime,
237
269
  metadata: {
@@ -242,6 +274,40 @@ class Wardn {
242
274
  };
243
275
  }
244
276
  }
277
+ /**
278
+ * v0.3.0: Determine final action based on all violations
279
+ * Priority: block > confirm > warn > log > allow
280
+ *
281
+ * Defensive design: Uses fail-closed approach - defaults to 'block' if
282
+ * recommendedAction is missing or invalid, ensuring security by default.
283
+ */
284
+ determineAction(violations) {
285
+ if (violations.length === 0) {
286
+ return 'allow';
287
+ }
288
+ // Define action priority order
289
+ const actionPriority = ['block', 'confirm', 'warn', 'log', 'allow'];
290
+ const validActions = new Set(actionPriority);
291
+ // Find highest priority action from all violations
292
+ for (const action of actionPriority) {
293
+ const hasAction = violations.some(v => {
294
+ // Defensive: validate recommendedAction exists and is valid
295
+ const recommended = v.recommendedAction;
296
+ if (!recommended || !validActions.has(recommended)) {
297
+ // Fail-closed: log warning and treat as 'block'
298
+ logger_1.logger.warn(`Invalid recommendedAction '${recommended}' in violation ${v.id}, defaulting to 'block'`);
299
+ return action === 'block';
300
+ }
301
+ return recommended === action;
302
+ });
303
+ if (hasAction) {
304
+ return action;
305
+ }
306
+ }
307
+ // Fallback: should never reach here, but fail-closed for safety
308
+ logger_1.logger.warn('determineAction fallback: no valid action found, defaulting to block');
309
+ return 'block';
310
+ }
245
311
  /** Normalize request to ToolData format */
246
312
  normalizeRequest(request) {
247
313
  return {
@@ -287,17 +353,17 @@ class Wardn {
287
353
  additionalInfo: { score },
288
354
  },
289
355
  timestamp: new Date().toISOString(),
356
+ recommendedAction: (0, base_1.mapSeverityToAction)(rule),
357
+ scope: 'Semantic analysis',
290
358
  };
291
359
  }
292
- /** Report violation to telemetry */
293
- reportViolation(violation, toolData, sessionId) {
294
- // REDACTION: Create a safe copy for the cloud
295
- const safeViolation = { ...violation };
296
- if (safeViolation.context) {
297
- // Remove raw toolData (prompts) from the cloud payload
298
- const { toolData: _removed, ...safeContext } = safeViolation.context;
299
- safeViolation.context = safeContext;
300
- }
360
+ /** Report violation to telemetry (redacts sensitive toolData) */
361
+ reportViolation(violation, sessionId) {
362
+ const { toolData: _redacted, ...safeContext } = violation.context;
363
+ const safeViolation = {
364
+ ...violation,
365
+ context: safeContext,
366
+ };
301
367
  this.telemetry.emit({
302
368
  eventType: "violation_detected",
303
369
  timestamp: new Date().toISOString(),
@@ -311,7 +377,7 @@ class Wardn {
311
377
  eventType: "scan_complete",
312
378
  timestamp: new Date().toISOString(),
313
379
  data: {
314
- allowed: result.allowed,
380
+ action: result.action,
315
381
  latencyMs: result.latencyMs,
316
382
  violationCount: result.violations.length,
317
383
  },
@@ -393,14 +459,12 @@ class Wardn {
393
459
  async pollRemoteRules() {
394
460
  if (!this.config.remoteRulesUrl || this.isShutdown)
395
461
  return;
396
- const FETCH_TIMEOUT_MS = 10000; // 10 second timeout
397
462
  const poll = async () => {
398
463
  if (this.isShutdown)
399
464
  return;
400
465
  try {
401
- // Create AbortController for timeout
402
466
  const controller = new AbortController();
403
- const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
467
+ const timeoutId = setTimeout(() => controller.abort(), security_limits_1.SECURITY_LIMITS.FETCH_TIMEOUT_MS);
404
468
  const response = await fetch(this.config.remoteRulesUrl, {
405
469
  headers: {
406
470
  "Accept": "application/json",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wardnmesh/sdk-node",
3
- "version": "0.2.3",
3
+ "version": "0.4.1",
4
4
  "description": "WardnMesh.AI Node.js SDK - Active Defense Middleware for AI Agents",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -68,4 +68,4 @@
68
68
  "dependencies": {
69
69
  "@xenova/transformers": "^2.17.2"
70
70
  }
71
- }
71
+ }