@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 +74 -2
- package/dist/detectors/base.d.ts +19 -4
- package/dist/detectors/base.js +51 -8
- package/dist/detectors/sequence.js +20 -3
- package/dist/detectors/state.d.ts +1 -0
- package/dist/detectors/state.js +39 -60
- package/dist/integrations/vercel.js +2 -1
- package/dist/middleware/express.js +2 -1
- package/dist/middleware/nextjs.js +2 -1
- package/dist/types.d.ts +14 -2
- package/dist/utils/rule-validator.js +1 -1
- package/dist/utils/safe-regex.d.ts +9 -0
- package/dist/utils/safe-regex.js +43 -10
- package/dist/wardn.d.ts +9 -1
- package/dist/wardn.js +82 -18
- package/package.json +2 -2
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
|
package/dist/detectors/base.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
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
|
/**
|
package/dist/detectors/base.js
CHANGED
|
@@ -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
|
-
*
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
package/dist/detectors/state.js
CHANGED
|
@@ -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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
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
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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" | "
|
|
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
|
-
|
|
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", "
|
|
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;
|
package/dist/utils/safe-regex.js
CHANGED
|
@@ -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
|
-
/\(
|
|
19
|
-
/\(
|
|
20
|
-
/\(
|
|
21
|
-
/\(
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
|
264
|
+
const action = failMode === "open" ? "allow" : "block";
|
|
233
265
|
return {
|
|
234
|
-
|
|
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,
|
|
294
|
-
|
|
295
|
-
const safeViolation = {
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
+
}
|