@llnvd/openclaw-url-guard 0.0.1 → 0.1.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 +114 -123
- package/dist/package.json +1 -1
- package/dist/src/index.d.ts +199 -2
- package/dist/src/index.js +117 -4
- package/docs/AGENT-INSTALL.md +59 -80
- package/docs/openclaw-integration.md +128 -96
- package/package.json +1 -1
- package/src/index.ts +190 -7
- package/tests/e2e/cases/backward-compat.test.ts +31 -13
- package/tests/e2e/cases/blocklist-block.test.ts +23 -10
- package/tests/e2e/cases/scoring-suspicious.test.ts +55 -23
- package/tests/e2e/cases/trusted-bypass.test.ts +35 -22
- package/tests/e2e/cases/urlhaus-live.test.ts +36 -26
- package/tests/e2e/harness.ts +95 -59
- package/tests/hook-integration.test.ts +217 -0
- package/tests/integration/allowlist.test.ts +41 -25
- package/tests/integration/blocklist.test.ts +26 -13
- package/tests/integration/bypass-protection.test.ts +52 -110
- package/tests/integration/helpers/client.ts +74 -45
- package/tests/integration/ssrf.test.ts +33 -25
- package/tests/integration.test.ts +4 -2
package/README.md
CHANGED
|
@@ -6,54 +6,45 @@ OpenClaw plugin that guards web tool access with hostname allowlist/blocklist po
|
|
|
6
6
|
> warranty of any kind. We make no guarantees about its security, reliability, or fitness for
|
|
7
7
|
> any particular purpose. Use at your own risk.
|
|
8
8
|
|
|
9
|
-
##
|
|
9
|
+
## How It Works
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
This plugin uses OpenClaw's `before_tool_call` hook to intercept calls to `web_fetch` and
|
|
12
|
+
`web_search`. When either tool is called:
|
|
12
13
|
|
|
13
|
-
The
|
|
14
|
+
1. The plugin extracts the URL from the request
|
|
15
|
+
2. Checks it against your configured allowlist/blocklist policy
|
|
16
|
+
3. Optionally queries URLhaus threat feed
|
|
17
|
+
4. Optionally applies trust scoring
|
|
18
|
+
5. **Blocks** the call if the URL fails any check, or **allows** it to proceed
|
|
14
19
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
git clone https://codeberg.org/llnvd/openclaw-url-guard.git
|
|
18
|
-
cd openclaw-url-guard
|
|
19
|
-
npm install
|
|
20
|
-
npm run build
|
|
21
|
-
|
|
22
|
-
# Install as OpenClaw plugin (from the repo directory)
|
|
23
|
-
openclaw plugins install .
|
|
24
|
-
```
|
|
20
|
+
This approach is transparent to the LLM — it uses the standard `web_fetch` and `web_search`
|
|
21
|
+
tools, but they're guarded by your policy.
|
|
25
22
|
|
|
26
|
-
|
|
23
|
+
## Install
|
|
27
24
|
|
|
28
25
|
```bash
|
|
29
|
-
npm install
|
|
26
|
+
npm install @llnvd/openclaw-url-guard
|
|
30
27
|
```
|
|
31
28
|
|
|
32
|
-
|
|
29
|
+
Or from git:
|
|
33
30
|
|
|
34
31
|
```bash
|
|
35
|
-
|
|
32
|
+
git clone https://codeberg.org/llnvd/openclaw-url-guard.git
|
|
33
|
+
cd openclaw-url-guard
|
|
34
|
+
npm install
|
|
35
|
+
npm run build
|
|
36
|
+
openclaw plugins install .
|
|
36
37
|
```
|
|
37
38
|
|
|
38
|
-
## Quick Start
|
|
39
|
+
## Quick Start
|
|
39
40
|
|
|
40
|
-
|
|
41
|
-
**For full protection, you must disable the native tools** — otherwise the LLM can bypass
|
|
42
|
-
the guard by calling the unprotected tools directly.
|
|
41
|
+
Add to your OpenClaw config (`~/.openclaw/openclaw.json`):
|
|
43
42
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
```json5
|
|
43
|
+
```json
|
|
47
44
|
{
|
|
48
|
-
// Disable native web tools (required for security)
|
|
49
|
-
"tools": {
|
|
50
|
-
"deny": ["web_fetch", "web_search"]
|
|
51
|
-
},
|
|
52
|
-
|
|
53
|
-
// Enable the url-guard plugin
|
|
54
45
|
"plugins": {
|
|
55
46
|
"entries": {
|
|
56
|
-
"
|
|
47
|
+
"openclaw-url-guard": {
|
|
57
48
|
"enabled": true,
|
|
58
49
|
"config": {
|
|
59
50
|
"mode": "allowlist",
|
|
@@ -62,8 +53,10 @@ Add to OpenClaw config (`~/.openclaw/openclaw.json`):
|
|
|
62
53
|
"developer.mozilla.org",
|
|
63
54
|
"en.wikipedia.org",
|
|
64
55
|
"github.com",
|
|
65
|
-
"stackoverflow.com"
|
|
66
|
-
|
|
56
|
+
"stackoverflow.com",
|
|
57
|
+
"*.stackexchange.com"
|
|
58
|
+
],
|
|
59
|
+
"blockPrivateIps": true
|
|
67
60
|
}
|
|
68
61
|
}
|
|
69
62
|
}
|
|
@@ -71,120 +64,118 @@ Add to OpenClaw config (`~/.openclaw/openclaw.json`):
|
|
|
71
64
|
}
|
|
72
65
|
```
|
|
73
66
|
|
|
74
|
-
|
|
75
|
-
1. **Denies** the native `web_fetch` and `web_search` tools (they won't be sent to the LLM)
|
|
76
|
-
2. **Enables** the plugin's `safe_web_fetch` and `safe_web_search` as the only web access tools
|
|
77
|
-
|
|
78
|
-
The guarded tools:
|
|
79
|
-
- `safe_web_fetch` — fetches URLs through the policy filter
|
|
80
|
-
- `safe_web_search` — searches the web and filters results by policy
|
|
81
|
-
|
|
82
|
-
> **⚠️ Security Note:** Without `tools.deny`, the native tools remain available and a
|
|
83
|
-
> prompt injection attack could instruct the LLM to use `web_fetch` instead of
|
|
84
|
-
> `safe_web_fetch`, completely bypassing the guard.
|
|
85
|
-
|
|
86
|
-
Optional threat intel feed settings:
|
|
87
|
-
|
|
88
|
-
```yaml
|
|
89
|
-
plugins:
|
|
90
|
-
- name: "@llnvd/openclaw-url-guard"
|
|
91
|
-
config:
|
|
92
|
-
mode: allowlist
|
|
93
|
-
allowlist:
|
|
94
|
-
- docs.python.org
|
|
95
|
-
threatFeeds:
|
|
96
|
-
urlhaus: true
|
|
97
|
-
mode: fail-open # default: fail-open, alternative: fail-closed
|
|
98
|
-
```
|
|
67
|
+
Restart the gateway and the plugin will automatically guard all `web_fetch` and `web_search` calls.
|
|
99
68
|
|
|
100
|
-
|
|
69
|
+
## Configuration
|
|
101
70
|
|
|
102
|
-
|
|
103
|
-
- runs after local allowlist/blocklist checks pass
|
|
104
|
-
- blocks when URLhaus lists the URL
|
|
105
|
-
- on URLhaus API/network failure:
|
|
106
|
-
- `fail-open`: allow request
|
|
107
|
-
- `fail-closed`: block request
|
|
71
|
+
### Modes
|
|
108
72
|
|
|
109
|
-
|
|
73
|
+
| Mode | Description |
|
|
74
|
+
|------|-------------|
|
|
75
|
+
| `allowlist` | Only URLs matching the allowlist are allowed (default) |
|
|
76
|
+
| `blocklist` | All URLs allowed except those matching the blocklist |
|
|
77
|
+
| `hybrid` | URL must be in allowlist AND not in blocklist |
|
|
110
78
|
|
|
111
|
-
|
|
112
|
-
- private/internal IP targets are blocked by default (`blockPrivateIps: true`)
|
|
113
|
-
- URLhaus lookups time out after 5 seconds to avoid hanging requests
|
|
114
|
-
- blocked tool responses are minimal by default; set `logging.verboseErrors: true` for detailed reasons and score metadata
|
|
79
|
+
### Basic Options
|
|
115
80
|
|
|
116
|
-
|
|
81
|
+
```json
|
|
82
|
+
{
|
|
83
|
+
"mode": "allowlist",
|
|
84
|
+
"allowlist": ["github.com", "*.githubusercontent.com"],
|
|
85
|
+
"blocklist": ["evil.com"],
|
|
86
|
+
"blockPrivateIps": true
|
|
87
|
+
}
|
|
88
|
+
```
|
|
117
89
|
|
|
118
|
-
|
|
90
|
+
- **allowlist/blocklist**: Hostname patterns. Use `*.domain.com` for wildcard subdomains.
|
|
91
|
+
- **blockPrivateIps**: Block requests to private/internal IPs (default: `true`)
|
|
119
92
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
mode: allowlist
|
|
134
|
-
scoring:
|
|
135
|
-
enabled: true
|
|
136
|
-
defaultScore: 0
|
|
137
|
-
minScore: -6
|
|
138
|
-
rules:
|
|
139
|
-
- domain: github.com
|
|
140
|
-
score: 8
|
|
141
|
-
reason: trusted source
|
|
93
|
+
### Threat Feeds
|
|
94
|
+
|
|
95
|
+
Enable URLhaus threat feed lookups:
|
|
96
|
+
|
|
97
|
+
```json
|
|
98
|
+
{
|
|
99
|
+
"mode": "allowlist",
|
|
100
|
+
"allowlist": ["*"],
|
|
101
|
+
"threatFeeds": {
|
|
102
|
+
"urlhaus": true,
|
|
103
|
+
"mode": "fail-open"
|
|
104
|
+
}
|
|
105
|
+
}
|
|
142
106
|
```
|
|
143
107
|
|
|
144
|
-
|
|
108
|
+
- `urlhaus`: Enable URLhaus API lookups
|
|
109
|
+
- `mode`: `fail-open` (allow on API failure) or `fail-closed` (block on API failure)
|
|
145
110
|
|
|
146
|
-
|
|
147
|
-
- [OpenClaw Integration](./docs/openclaw-integration.md) — **start here**
|
|
148
|
-
- [Agent Installation Guide](./docs/AGENT-INSTALL.md) — for AI agents installing this plugin
|
|
149
|
-
- [API Reference](./docs/api-reference.md)
|
|
150
|
-
- [Configuration](./docs/configuration.md)
|
|
151
|
-
- [Trust Scoring](./docs/scoring.md)
|
|
152
|
-
- [Testing](./docs/testing.md)
|
|
153
|
-
- [Usage Modes](./docs/usage-modes.md)
|
|
111
|
+
### Trust Scoring
|
|
154
112
|
|
|
155
|
-
|
|
113
|
+
Optional scoring system for fine-grained control:
|
|
156
114
|
|
|
157
|
-
```
|
|
158
|
-
|
|
115
|
+
```json
|
|
116
|
+
{
|
|
117
|
+
"mode": "allowlist",
|
|
118
|
+
"allowlist": ["*"],
|
|
119
|
+
"scoring": {
|
|
120
|
+
"enabled": true,
|
|
121
|
+
"defaultScore": 0,
|
|
122
|
+
"minScore": -6,
|
|
123
|
+
"rules": [
|
|
124
|
+
{ "domain": "github.com", "score": 10, "reason": "highly trusted" },
|
|
125
|
+
{ "domain": "*.sketch.com", "score": -5, "reason": "known suspicious" }
|
|
126
|
+
]
|
|
127
|
+
}
|
|
128
|
+
}
|
|
159
129
|
```
|
|
160
130
|
|
|
161
|
-
|
|
131
|
+
Score ranges:
|
|
132
|
+
- `+10` to `+7`: Trusted (always allow)
|
|
133
|
+
- `+6` to `+3`: Preferred
|
|
134
|
+
- `+2` to `-2`: Neutral
|
|
135
|
+
- `-3` to `-6`: Suspicious
|
|
136
|
+
- `-7` to `-10`: Blocked
|
|
162
137
|
|
|
163
|
-
|
|
164
|
-
|
|
138
|
+
## Security Defaults
|
|
139
|
+
|
|
140
|
+
- Only `http://` and `https://` protocols accepted
|
|
141
|
+
- Private/internal IP targets blocked by default
|
|
142
|
+
- URLhaus lookups timeout after 5 seconds
|
|
143
|
+
- Invalid URLs are blocked
|
|
144
|
+
|
|
145
|
+
## Logging
|
|
146
|
+
|
|
147
|
+
Enable verbose logging to see guard decisions:
|
|
148
|
+
|
|
149
|
+
```json
|
|
150
|
+
{
|
|
151
|
+
"logging": {
|
|
152
|
+
"enabled": true,
|
|
153
|
+
"logBlocked": true,
|
|
154
|
+
"verboseErrors": true
|
|
155
|
+
}
|
|
156
|
+
}
|
|
165
157
|
```
|
|
166
158
|
|
|
167
|
-
|
|
159
|
+
## Development
|
|
168
160
|
|
|
169
161
|
```bash
|
|
170
|
-
|
|
162
|
+
npm install
|
|
163
|
+
npm run build
|
|
164
|
+
npm test
|
|
171
165
|
```
|
|
172
166
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
## CI Pipeline
|
|
167
|
+
Run E2E tests:
|
|
176
168
|
|
|
177
|
-
|
|
169
|
+
```bash
|
|
170
|
+
npm run test:e2e
|
|
171
|
+
```
|
|
178
172
|
|
|
179
|
-
|
|
180
|
-
|-----|--------|---------|-------------|
|
|
181
|
-
| Lint & Format | `codeberg-tiny` | push, PR | oxlint + prettier |
|
|
182
|
-
| Build | `codeberg-tiny` | push, PR | TypeScript compilation |
|
|
183
|
-
| Unit Tests | `codeberg-small` | push, PR | Core functionality tests |
|
|
184
|
-
| E2E Tests | `codeberg-small` | push, PR | Integration test harness |
|
|
185
|
-
| Live Feed Tests | `codeberg-small` | main push only | URLhaus network tests |
|
|
173
|
+
## Documentation
|
|
186
174
|
|
|
187
|
-
|
|
175
|
+
- [Configuration](./docs/configuration.md)
|
|
176
|
+
- [API Reference](./docs/api-reference.md)
|
|
177
|
+
- [Trust Scoring](./docs/scoring.md)
|
|
178
|
+
- [Testing](./docs/testing.md)
|
|
188
179
|
|
|
189
180
|
## License
|
|
190
181
|
|
package/dist/package.json
CHANGED
package/dist/src/index.d.ts
CHANGED
|
@@ -1,3 +1,200 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
1
|
+
import type { UrlGuardConfig } from './types';
|
|
2
|
+
interface PluginLogger {
|
|
3
|
+
debug?: (message: string) => void;
|
|
4
|
+
info: (message: string) => void;
|
|
5
|
+
warn: (message: string) => void;
|
|
6
|
+
error: (message: string) => void;
|
|
7
|
+
}
|
|
8
|
+
interface BeforeToolCallEvent {
|
|
9
|
+
toolName: string;
|
|
10
|
+
params: Record<string, unknown>;
|
|
11
|
+
runId?: string;
|
|
12
|
+
toolCallId?: string;
|
|
13
|
+
}
|
|
14
|
+
interface BeforeToolCallContext {
|
|
15
|
+
agentId?: string;
|
|
16
|
+
sessionKey?: string;
|
|
17
|
+
sessionId?: string;
|
|
18
|
+
runId?: string;
|
|
19
|
+
toolCallId?: string;
|
|
20
|
+
}
|
|
21
|
+
interface BeforeToolCallResult {
|
|
22
|
+
block?: boolean;
|
|
23
|
+
blockReason?: string;
|
|
24
|
+
params?: Record<string, unknown>;
|
|
25
|
+
}
|
|
26
|
+
type BeforeToolCallHandler = (event: BeforeToolCallEvent, ctx: BeforeToolCallContext) => Promise<BeforeToolCallResult | void> | BeforeToolCallResult | void;
|
|
27
|
+
interface OpenClawPluginApi {
|
|
28
|
+
id: string;
|
|
29
|
+
name: string;
|
|
30
|
+
pluginConfig?: Record<string, unknown>;
|
|
31
|
+
logger: PluginLogger;
|
|
32
|
+
on: (hookName: 'before_tool_call', handler: BeforeToolCallHandler) => void;
|
|
33
|
+
}
|
|
34
|
+
declare const GUARDED_TOOLS: Set<string>;
|
|
35
|
+
/**
|
|
36
|
+
* Check a URL against the guard policy.
|
|
37
|
+
* Returns { allowed: true } or { allowed: false, reason: string }
|
|
38
|
+
*/
|
|
39
|
+
declare function checkUrl(url: string, config: UrlGuardConfig, logger: PluginLogger): Promise<{
|
|
40
|
+
allowed: true;
|
|
41
|
+
} | {
|
|
42
|
+
allowed: false;
|
|
43
|
+
reason: string;
|
|
44
|
+
}>;
|
|
45
|
+
declare const plugin: {
|
|
46
|
+
id: string;
|
|
47
|
+
name: string;
|
|
48
|
+
version: string;
|
|
49
|
+
description: string;
|
|
50
|
+
configSchema: {
|
|
51
|
+
readonly type: "object";
|
|
52
|
+
readonly additionalProperties: false;
|
|
53
|
+
readonly required: readonly ["mode"];
|
|
54
|
+
readonly properties: {
|
|
55
|
+
readonly mode: {
|
|
56
|
+
readonly type: "string";
|
|
57
|
+
readonly enum: readonly ["allowlist", "blocklist", "hybrid"];
|
|
58
|
+
};
|
|
59
|
+
readonly allowlist: {
|
|
60
|
+
readonly type: "array";
|
|
61
|
+
readonly items: {
|
|
62
|
+
readonly type: "string";
|
|
63
|
+
};
|
|
64
|
+
};
|
|
65
|
+
readonly blocklist: {
|
|
66
|
+
readonly type: "array";
|
|
67
|
+
readonly items: {
|
|
68
|
+
readonly type: "string";
|
|
69
|
+
};
|
|
70
|
+
};
|
|
71
|
+
readonly blockPrivateIps: {
|
|
72
|
+
readonly type: "boolean";
|
|
73
|
+
};
|
|
74
|
+
readonly threatFeeds: {
|
|
75
|
+
readonly type: "object";
|
|
76
|
+
readonly additionalProperties: false;
|
|
77
|
+
readonly properties: {
|
|
78
|
+
readonly urlhaus: {
|
|
79
|
+
readonly type: "boolean";
|
|
80
|
+
};
|
|
81
|
+
readonly mode: {
|
|
82
|
+
readonly type: "string";
|
|
83
|
+
readonly enum: readonly ["fail-open", "fail-closed"];
|
|
84
|
+
};
|
|
85
|
+
};
|
|
86
|
+
};
|
|
87
|
+
readonly scoring: {
|
|
88
|
+
readonly type: "object";
|
|
89
|
+
readonly additionalProperties: false;
|
|
90
|
+
readonly properties: {
|
|
91
|
+
readonly enabled: {
|
|
92
|
+
readonly type: "boolean";
|
|
93
|
+
};
|
|
94
|
+
readonly defaultScore: {
|
|
95
|
+
readonly type: "number";
|
|
96
|
+
readonly minimum: -10;
|
|
97
|
+
readonly maximum: 10;
|
|
98
|
+
};
|
|
99
|
+
readonly minScore: {
|
|
100
|
+
readonly type: "number";
|
|
101
|
+
readonly minimum: -10;
|
|
102
|
+
readonly maximum: 10;
|
|
103
|
+
};
|
|
104
|
+
readonly rules: {
|
|
105
|
+
readonly type: "array";
|
|
106
|
+
readonly items: {
|
|
107
|
+
readonly type: "object";
|
|
108
|
+
readonly additionalProperties: false;
|
|
109
|
+
readonly required: readonly ["domain", "score"];
|
|
110
|
+
readonly properties: {
|
|
111
|
+
readonly domain: {
|
|
112
|
+
readonly type: "string";
|
|
113
|
+
};
|
|
114
|
+
readonly score: {
|
|
115
|
+
readonly type: "number";
|
|
116
|
+
readonly minimum: -10;
|
|
117
|
+
readonly maximum: 10;
|
|
118
|
+
};
|
|
119
|
+
readonly reason: {
|
|
120
|
+
readonly type: "string";
|
|
121
|
+
};
|
|
122
|
+
};
|
|
123
|
+
};
|
|
124
|
+
};
|
|
125
|
+
readonly feedScores: {
|
|
126
|
+
readonly type: "object";
|
|
127
|
+
readonly additionalProperties: false;
|
|
128
|
+
readonly properties: {
|
|
129
|
+
readonly urlhaus: {
|
|
130
|
+
readonly type: "object";
|
|
131
|
+
readonly additionalProperties: false;
|
|
132
|
+
readonly properties: {
|
|
133
|
+
readonly online: {
|
|
134
|
+
readonly type: "number";
|
|
135
|
+
readonly minimum: -10;
|
|
136
|
+
readonly maximum: 10;
|
|
137
|
+
};
|
|
138
|
+
readonly offline: {
|
|
139
|
+
readonly type: "number";
|
|
140
|
+
readonly minimum: -10;
|
|
141
|
+
readonly maximum: 10;
|
|
142
|
+
};
|
|
143
|
+
};
|
|
144
|
+
};
|
|
145
|
+
readonly spamhaus: {
|
|
146
|
+
readonly type: "object";
|
|
147
|
+
readonly additionalProperties: false;
|
|
148
|
+
readonly properties: {
|
|
149
|
+
readonly botnet_cc: {
|
|
150
|
+
readonly type: "number";
|
|
151
|
+
readonly minimum: -10;
|
|
152
|
+
readonly maximum: 10;
|
|
153
|
+
};
|
|
154
|
+
readonly phishing_domain: {
|
|
155
|
+
readonly type: "number";
|
|
156
|
+
readonly minimum: -10;
|
|
157
|
+
readonly maximum: 10;
|
|
158
|
+
};
|
|
159
|
+
readonly spammer_domain: {
|
|
160
|
+
readonly type: "number";
|
|
161
|
+
readonly minimum: -10;
|
|
162
|
+
readonly maximum: 10;
|
|
163
|
+
};
|
|
164
|
+
readonly abused_redirector: {
|
|
165
|
+
readonly type: "number";
|
|
166
|
+
readonly minimum: -10;
|
|
167
|
+
readonly maximum: 10;
|
|
168
|
+
};
|
|
169
|
+
readonly 'abused_legit_*': {
|
|
170
|
+
readonly type: "number";
|
|
171
|
+
readonly minimum: -10;
|
|
172
|
+
readonly maximum: 10;
|
|
173
|
+
};
|
|
174
|
+
};
|
|
175
|
+
};
|
|
176
|
+
};
|
|
177
|
+
};
|
|
178
|
+
};
|
|
179
|
+
};
|
|
180
|
+
readonly logging: {
|
|
181
|
+
readonly type: "object";
|
|
182
|
+
readonly additionalProperties: false;
|
|
183
|
+
readonly properties: {
|
|
184
|
+
readonly enabled: {
|
|
185
|
+
readonly type: "boolean";
|
|
186
|
+
};
|
|
187
|
+
readonly logBlocked: {
|
|
188
|
+
readonly type: "boolean";
|
|
189
|
+
};
|
|
190
|
+
readonly verboseErrors: {
|
|
191
|
+
readonly type: "boolean";
|
|
192
|
+
};
|
|
193
|
+
};
|
|
194
|
+
};
|
|
195
|
+
};
|
|
196
|
+
};
|
|
197
|
+
register(api: OpenClawPluginApi): void;
|
|
198
|
+
};
|
|
3
199
|
export default plugin;
|
|
200
|
+
export { checkUrl, GUARDED_TOOLS };
|
package/dist/src/index.js
CHANGED
|
@@ -3,14 +3,127 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.GUARDED_TOOLS = void 0;
|
|
7
|
+
exports.checkUrl = checkUrl;
|
|
6
8
|
const config_1 = require("./config");
|
|
7
|
-
const
|
|
8
|
-
const
|
|
9
|
+
const matcher_1 = require("./filters/matcher");
|
|
10
|
+
const urlhaus_1 = require("./filters/urlhaus");
|
|
11
|
+
const engine_1 = require("./scoring/engine");
|
|
9
12
|
const package_json_1 = __importDefault(require("../package.json"));
|
|
13
|
+
// Tools we intercept
|
|
14
|
+
const GUARDED_TOOLS = new Set(['web_fetch', 'web_search']);
|
|
15
|
+
exports.GUARDED_TOOLS = GUARDED_TOOLS;
|
|
16
|
+
/**
|
|
17
|
+
* Check a URL against the guard policy.
|
|
18
|
+
* Returns { allowed: true } or { allowed: false, reason: string }
|
|
19
|
+
*/
|
|
20
|
+
async function checkUrl(url, config, logger) {
|
|
21
|
+
const hostname = (0, matcher_1.extractHostname)(url);
|
|
22
|
+
if (!hostname) {
|
|
23
|
+
return { allowed: false, reason: 'Invalid URL format' };
|
|
24
|
+
}
|
|
25
|
+
// Check private IP blocking
|
|
26
|
+
if (config.blockPrivateIps && (0, matcher_1.isPrivateIp)(hostname)) {
|
|
27
|
+
return { allowed: false, reason: 'Private/internal IP addresses are blocked' };
|
|
28
|
+
}
|
|
29
|
+
// Check allowlist/blocklist policy
|
|
30
|
+
if (!(0, matcher_1.isAllowed)(url, config)) {
|
|
31
|
+
const reason = (0, matcher_1.getBlockReason)(url, config);
|
|
32
|
+
return { allowed: false, reason: `URL blocked: ${reason}` };
|
|
33
|
+
}
|
|
34
|
+
// Check threat feeds if enabled
|
|
35
|
+
let feedResults;
|
|
36
|
+
if (config.threatFeeds?.urlhaus) {
|
|
37
|
+
try {
|
|
38
|
+
const urlhausResult = await (0, urlhaus_1.lookupUrlhaus)(url);
|
|
39
|
+
if (urlhausResult.listed) {
|
|
40
|
+
return {
|
|
41
|
+
allowed: false,
|
|
42
|
+
reason: `URL blocked by threat feed: ${urlhausResult.reason ?? 'listed in URLhaus'}`,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
if (urlhausResult.feedSignals) {
|
|
46
|
+
feedResults = { signals: urlhausResult.feedSignals };
|
|
47
|
+
}
|
|
48
|
+
if (urlhausResult.unavailable) {
|
|
49
|
+
feedResults = { unavailable: true, reason: urlhausResult.reason };
|
|
50
|
+
if (config.threatFeeds.mode === 'fail-closed') {
|
|
51
|
+
return {
|
|
52
|
+
allowed: false,
|
|
53
|
+
reason: 'Threat feed unavailable and fail-closed mode is enabled',
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
logger.warn?.(`[url-guard] Threat feed unavailable: ${urlhausResult.reason}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
61
|
+
logger.warn?.(`[url-guard] Threat feed lookup failed: ${message}`);
|
|
62
|
+
if (config.threatFeeds.mode === 'fail-closed') {
|
|
63
|
+
return {
|
|
64
|
+
allowed: false,
|
|
65
|
+
reason: 'Threat feed lookup failed and fail-closed mode is enabled',
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
// Apply scoring if enabled
|
|
71
|
+
if (config.scoring?.enabled) {
|
|
72
|
+
const scoreResult = (0, engine_1.calculateScore)(url, config, feedResults);
|
|
73
|
+
if (!scoreResult.allowed) {
|
|
74
|
+
const sources = scoreResult.sources.map((s) => s.source).join(', ');
|
|
75
|
+
return {
|
|
76
|
+
allowed: false,
|
|
77
|
+
reason: `URL blocked by scoring (score: ${scoreResult.finalScore}, sources: ${sources})`,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return { allowed: true };
|
|
82
|
+
}
|
|
10
83
|
const plugin = {
|
|
11
|
-
|
|
84
|
+
id: 'openclaw-url-guard',
|
|
85
|
+
name: 'URL Guard',
|
|
12
86
|
version: package_json_1.default.version,
|
|
13
|
-
|
|
87
|
+
description: 'Guards web tool access with hostname allowlist/blocklist policy and optional URLhaus threat feed integration.',
|
|
14
88
|
configSchema: config_1.configSchema,
|
|
89
|
+
register(api) {
|
|
90
|
+
const config = (0, config_1.normalizeConfig)(api.pluginConfig);
|
|
91
|
+
api.logger.info(`[url-guard] Registering URL Guard plugin v${package_json_1.default.version}`);
|
|
92
|
+
api.logger.info(`[url-guard] Mode: ${config.mode}, Allowlist: ${config.allowlist?.length ?? 0} domains, ` +
|
|
93
|
+
`Blocklist: ${config.blocklist?.length ?? 0} domains`);
|
|
94
|
+
if (config.threatFeeds?.urlhaus) {
|
|
95
|
+
api.logger.info(`[url-guard] URLhaus threat feed enabled (mode: ${config.threatFeeds.mode ?? 'fail-open'})`);
|
|
96
|
+
}
|
|
97
|
+
if (config.scoring?.enabled) {
|
|
98
|
+
api.logger.info(`[url-guard] Scoring enabled (minScore: ${config.scoring.minScore ?? -6}, ` +
|
|
99
|
+
`rules: ${config.scoring.rules?.length ?? 0})`);
|
|
100
|
+
}
|
|
101
|
+
// Register the before_tool_call hook to intercept web_fetch and web_search
|
|
102
|
+
api.on('before_tool_call', async (event, _ctx) => {
|
|
103
|
+
// Only intercept guarded tools
|
|
104
|
+
if (!GUARDED_TOOLS.has(event.toolName)) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
// Extract URL from params
|
|
108
|
+
const url = event.params.url;
|
|
109
|
+
if (!url || typeof url !== 'string') {
|
|
110
|
+
// No URL parameter - let it through (tool will handle the error)
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
// Check the URL against our policy
|
|
114
|
+
const result = await checkUrl(url, config, api.logger);
|
|
115
|
+
if (!result.allowed) {
|
|
116
|
+
api.logger.info?.(`[url-guard] Blocked ${event.toolName}: ${url} - ${result.reason}`);
|
|
117
|
+
return {
|
|
118
|
+
block: true,
|
|
119
|
+
blockReason: result.reason,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
// URL is allowed - let the tool execute
|
|
123
|
+
api.logger.debug?.(`[url-guard] Allowed ${event.toolName}: ${url}`);
|
|
124
|
+
return;
|
|
125
|
+
});
|
|
126
|
+
api.logger.info('[url-guard] Hook registered for: web_fetch, web_search');
|
|
127
|
+
},
|
|
15
128
|
};
|
|
16
129
|
exports.default = plugin;
|