@llnvd/openclaw-url-guard 0.0.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/.forgejo/workflows/ci.yaml +117 -0
- package/.husky/pre-commit +1 -0
- package/.oxlintrc.json +14 -0
- package/.prettierignore +3 -0
- package/.prettierrc +7 -0
- package/CONTRIBUTING.md +173 -0
- package/LICENSE +21 -0
- package/README.md +191 -0
- package/TASK.md +39 -0
- package/dist/package.json +39 -0
- package/dist/src/config.d.ts +150 -0
- package/dist/src/config.js +206 -0
- package/dist/src/filters/matcher.d.ts +6 -0
- package/dist/src/filters/matcher.js +111 -0
- package/dist/src/filters/urlhaus.d.ts +8 -0
- package/dist/src/filters/urlhaus.js +141 -0
- package/dist/src/index.d.ts +3 -0
- package/dist/src/index.js +16 -0
- package/dist/src/scoring/engine.d.ts +2 -0
- package/dist/src/scoring/engine.js +118 -0
- package/dist/src/tools/safeFetch.d.ts +2 -0
- package/dist/src/tools/safeFetch.js +121 -0
- package/dist/src/tools/safeSearch.d.ts +2 -0
- package/dist/src/tools/safeSearch.js +81 -0
- package/dist/src/types.d.ts +99 -0
- package/dist/src/types.js +2 -0
- package/docs/AGENT-INSTALL.md +118 -0
- package/docs/DEPLOY_GIST.md +84 -0
- package/docs/README.md +40 -0
- package/docs/api-reference.md +274 -0
- package/docs/configuration.md +218 -0
- package/docs/openclaw-integration.md +155 -0
- package/docs/scoring.md +81 -0
- package/docs/testing.md +53 -0
- package/docs/usage-modes.md +127 -0
- package/examples/openclaw-config-avoid-sites.yaml +49 -0
- package/examples/openclaw-config-learning-sites.yaml +43 -0
- package/examples/openclaw-config-scoring.yaml +43 -0
- package/openclaw.plugin.json +66 -0
- package/openspec/changes/e2e-real-openclaw-tests/design.md +106 -0
- package/openspec/changes/e2e-real-openclaw-tests/proposal.md +35 -0
- package/openspec/changes/e2e-real-openclaw-tests/specs/e2e-real-instance/spec.md +197 -0
- package/openspec/changes/e2e-real-openclaw-tests/tasks.md +34 -0
- package/openspec/config.yaml +8 -0
- package/openspec/specs/e2e-real-instance/spec.md +197 -0
- package/package.json +39 -0
- package/src/config.ts +228 -0
- package/src/filters/matcher.ts +126 -0
- package/src/filters/urlhaus.ts +170 -0
- package/src/index.ts +14 -0
- package/src/scoring/engine.ts +144 -0
- package/src/tools/safeFetch.ts +163 -0
- package/src/tools/safeSearch.ts +108 -0
- package/src/types.ts +136 -0
- package/tests/e2e/cases/backward-compat.test.ts +22 -0
- package/tests/e2e/cases/blocklist-block.test.ts +18 -0
- package/tests/e2e/cases/scoring-suspicious.test.ts +30 -0
- package/tests/e2e/cases/trusted-bypass.test.ts +31 -0
- package/tests/e2e/cases/urlhaus-live.test.ts +33 -0
- package/tests/e2e/fixtures/config.ts +60 -0
- package/tests/e2e/fixtures/test-urls.ts +5 -0
- package/tests/e2e/harness.ts +73 -0
- package/tests/integration/allowlist.test.ts +87 -0
- package/tests/integration/blocklist.test.ts +48 -0
- package/tests/integration/bypass-protection.test.ts +151 -0
- package/tests/integration/config-validation.test.ts +55 -0
- package/tests/integration/error-handling.test.ts +57 -0
- package/tests/integration/fixtures/urlhaus-sample.csv +6 -0
- package/tests/integration/helpers/client.ts +185 -0
- package/tests/integration/helpers/config.ts +103 -0
- package/tests/integration/helpers/gateway.ts +185 -0
- package/tests/integration/helpers/test-mode.ts +20 -0
- package/tests/integration/hybrid.test.ts +56 -0
- package/tests/integration/ssrf.test.ts +61 -0
- package/tests/integration/urlhaus.test.ts +56 -0
- package/tests/integration.test.ts +72 -0
- package/tests/matcher.test.ts +62 -0
- package/tests/safeFetch.test.ts +247 -0
- package/tests/safeSearch.test.ts +80 -0
- package/tests/scoring.test.ts +106 -0
- package/tests/security.test.ts +130 -0
- package/tests/urlhaus.test.ts +124 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
# E2E Real Instance Integration Tests
|
|
2
|
+
|
|
3
|
+
## ADDED Requirements
|
|
4
|
+
|
|
5
|
+
### Requirement: Gateway lifecycle management
|
|
6
|
+
|
|
7
|
+
The system SHALL provide helper functions to start, health-check, and stop a real OpenClaw gateway instance for testing purposes.
|
|
8
|
+
|
|
9
|
+
#### Scenario: Start gateway with custom config
|
|
10
|
+
|
|
11
|
+
- **WHEN** a test suite starts with a given `UrlGuardConfig`
|
|
12
|
+
- **THEN** the helper SHALL generate a temp config file with the plugin configured
|
|
13
|
+
- **AND** spawn `openclaw gateway` as a subprocess on a dynamically assigned port
|
|
14
|
+
- **AND** wait for the health endpoint to respond (HTTP 200) with exponential backoff
|
|
15
|
+
- **AND** fail the test suite if the gateway does not become healthy within 30 seconds
|
|
16
|
+
|
|
17
|
+
#### Scenario: Stop gateway on suite teardown
|
|
18
|
+
|
|
19
|
+
- **WHEN** a test suite completes (pass or fail)
|
|
20
|
+
- **THEN** the helper SHALL kill the gateway subprocess and delete temp config files
|
|
21
|
+
- **AND** if the test runner crashes, a `process.on('exit')` handler SHALL kill any tracked gateway PIDs
|
|
22
|
+
|
|
23
|
+
#### Scenario: Port isolation
|
|
24
|
+
|
|
25
|
+
- **WHEN** multiple test suites run (sequentially or in parallel)
|
|
26
|
+
- **THEN** each suite SHALL use a unique port (via port 0 binding or find-free-port)
|
|
27
|
+
- **AND** no port collisions SHALL occur
|
|
28
|
+
|
|
29
|
+
### Requirement: Allowlist enforcement through real gateway
|
|
30
|
+
|
|
31
|
+
The system SHALL verify that allowlist mode works correctly through the real OpenClaw gateway.
|
|
32
|
+
|
|
33
|
+
#### Scenario: Allowed domain passes through
|
|
34
|
+
|
|
35
|
+
- **GIVEN** a gateway configured with mode `allowlist` and allowlist `["example.com"]`
|
|
36
|
+
- **WHEN** a tool call for `web_fetch` with URL `https://example.com/page` is sent to the gateway
|
|
37
|
+
- **THEN** the response SHALL indicate the request was allowed
|
|
38
|
+
|
|
39
|
+
#### Scenario: Unlisted domain is blocked
|
|
40
|
+
|
|
41
|
+
- **GIVEN** a gateway configured with mode `allowlist` and allowlist `["example.com"]`
|
|
42
|
+
- **WHEN** a tool call for `web_fetch` with URL `https://evil.com/payload` is sent
|
|
43
|
+
- **THEN** the response SHALL be a structured block response
|
|
44
|
+
- **AND** the `reason` field SHALL be `"not in allowlist"`
|
|
45
|
+
- **AND** the `policy` field SHALL be `"allowlist"`
|
|
46
|
+
|
|
47
|
+
#### Scenario: Wildcard allowlist matching
|
|
48
|
+
|
|
49
|
+
- **GIVEN** a gateway configured with allowlist `["*.example.com"]`
|
|
50
|
+
- **WHEN** a tool call for `https://api.example.com/data` is sent
|
|
51
|
+
- **THEN** the request SHALL be allowed
|
|
52
|
+
- **AND** a request for `https://example.com/data` (bare domain) SHALL be blocked
|
|
53
|
+
|
|
54
|
+
### Requirement: Blocklist enforcement through real gateway
|
|
55
|
+
|
|
56
|
+
The system SHALL verify blocklist mode through the real gateway.
|
|
57
|
+
|
|
58
|
+
#### Scenario: Blocked domain is rejected
|
|
59
|
+
|
|
60
|
+
- **GIVEN** a gateway configured with mode `blocklist` and blocklist `["evil.com", "*.malware.org"]`
|
|
61
|
+
- **WHEN** a tool call for `https://evil.com/exploit` is sent
|
|
62
|
+
- **THEN** the response SHALL be a structured block response with reason `"in blocklist"`
|
|
63
|
+
|
|
64
|
+
#### Scenario: Unlisted domain passes in blocklist mode
|
|
65
|
+
|
|
66
|
+
- **GIVEN** a gateway configured with mode `blocklist` and blocklist `["evil.com"]`
|
|
67
|
+
- **WHEN** a tool call for `https://safe.com/page` is sent
|
|
68
|
+
- **THEN** the request SHALL be allowed
|
|
69
|
+
|
|
70
|
+
### Requirement: Hybrid mode enforcement through real gateway
|
|
71
|
+
|
|
72
|
+
The system SHALL verify hybrid mode (allowlist + blocklist) through the real gateway.
|
|
73
|
+
|
|
74
|
+
#### Scenario: Allowed and not blocked passes
|
|
75
|
+
|
|
76
|
+
- **GIVEN** a gateway with mode `hybrid`, allowlist `["*.example.com"]`, blocklist `["blocked.example.com"]`
|
|
77
|
+
- **WHEN** a tool call for `https://api.example.com/data` is sent
|
|
78
|
+
- **THEN** the request SHALL be allowed
|
|
79
|
+
|
|
80
|
+
#### Scenario: Allowed but also blocked is rejected
|
|
81
|
+
|
|
82
|
+
- **GIVEN** the same hybrid config
|
|
83
|
+
- **WHEN** a tool call for `https://blocked.example.com/data` is sent
|
|
84
|
+
- **THEN** the response SHALL be a structured block response
|
|
85
|
+
|
|
86
|
+
#### Scenario: Not in allowlist is rejected in hybrid mode
|
|
87
|
+
|
|
88
|
+
- **GIVEN** the same hybrid config
|
|
89
|
+
- **WHEN** a tool call for `https://outside.com/page` is sent
|
|
90
|
+
- **THEN** the response SHALL be blocked with reason `"not in allowlist"`
|
|
91
|
+
|
|
92
|
+
### Requirement: SSRF protection through real gateway
|
|
93
|
+
|
|
94
|
+
The system SHALL verify that private/internal IP addresses are blocked at the integration level.
|
|
95
|
+
|
|
96
|
+
#### Scenario: Localhost is blocked
|
|
97
|
+
|
|
98
|
+
- **GIVEN** a gateway configured with `blockPrivateIps: true`
|
|
99
|
+
- **WHEN** a tool call for `http://127.0.0.1/admin` is sent
|
|
100
|
+
- **THEN** the response SHALL be a structured block response with reason `"private or internal IP blocked"`
|
|
101
|
+
|
|
102
|
+
#### Scenario: Link-local address is blocked
|
|
103
|
+
|
|
104
|
+
- **GIVEN** the same config
|
|
105
|
+
- **WHEN** a tool call for `http://169.254.169.254/metadata` is sent (cloud metadata endpoint)
|
|
106
|
+
- **THEN** the response SHALL be blocked
|
|
107
|
+
|
|
108
|
+
#### Scenario: IPv6 loopback is blocked
|
|
109
|
+
|
|
110
|
+
- **GIVEN** the same config
|
|
111
|
+
- **WHEN** a tool call for `http://[::1]/admin` is sent
|
|
112
|
+
- **THEN** the response SHALL be blocked
|
|
113
|
+
|
|
114
|
+
### Requirement: URLhaus blocking with fixture feed
|
|
115
|
+
|
|
116
|
+
The system SHALL verify URLhaus-based blocking using a deterministic fixture feed.
|
|
117
|
+
|
|
118
|
+
#### Scenario: Known malicious URL is blocked
|
|
119
|
+
|
|
120
|
+
- **GIVEN** a gateway configured with URLhaus enabled, pointing to `tests/integration/fixtures/urlhaus-sample.csv`
|
|
121
|
+
- **WHEN** a tool call for a URL present in the fixture feed is sent
|
|
122
|
+
- **THEN** the response SHALL be blocked with a reason indicating URLhaus match
|
|
123
|
+
|
|
124
|
+
#### Scenario: Clean URL passes URLhaus check
|
|
125
|
+
|
|
126
|
+
- **GIVEN** the same config
|
|
127
|
+
- **WHEN** a tool call for `https://clean-example.com/safe` (not in fixture) is sent
|
|
128
|
+
- **THEN** the request SHALL be allowed (assuming it passes other policy checks)
|
|
129
|
+
|
|
130
|
+
### Requirement: Config validation error handling
|
|
131
|
+
|
|
132
|
+
The system SHALL produce clear errors when given malformed configuration.
|
|
133
|
+
|
|
134
|
+
#### Scenario: Missing required mode field
|
|
135
|
+
|
|
136
|
+
- **GIVEN** a config file with the plugin configured but `mode` field omitted
|
|
137
|
+
- **WHEN** the gateway starts
|
|
138
|
+
- **THEN** the gateway SHALL fail to start or the plugin SHALL report a clear validation error
|
|
139
|
+
- **AND** the error message SHALL mention the missing `mode` field
|
|
140
|
+
|
|
141
|
+
#### Scenario: Invalid mode value
|
|
142
|
+
|
|
143
|
+
- **GIVEN** a config with `mode: "invalid_mode"`
|
|
144
|
+
- **WHEN** the gateway starts
|
|
145
|
+
- **THEN** a clear validation error SHALL be reported
|
|
146
|
+
|
|
147
|
+
#### Scenario: Allowlist mode without allowlist array
|
|
148
|
+
|
|
149
|
+
- **GIVEN** a config with `mode: "allowlist"` but no `allowlist` field
|
|
150
|
+
- **WHEN** a tool call is made
|
|
151
|
+
- **THEN** the behavior SHALL be well-defined (either block all, or error clearly)
|
|
152
|
+
|
|
153
|
+
### Requirement: Structured block response format
|
|
154
|
+
|
|
155
|
+
All blocked requests through the real gateway SHALL return a structured response.
|
|
156
|
+
|
|
157
|
+
#### Scenario: Block response contains required fields
|
|
158
|
+
|
|
159
|
+
- **WHEN** any request is blocked by the plugin
|
|
160
|
+
- **THEN** the response SHALL contain at minimum:
|
|
161
|
+
- `blocked`: `true`
|
|
162
|
+
- `reason`: a human-readable explanation (matching `getBlockReason()` output)
|
|
163
|
+
- `url`: the original requested URL
|
|
164
|
+
|
|
165
|
+
### Requirement: Test gating and CI integration
|
|
166
|
+
|
|
167
|
+
The integration tests SHALL be gated and integrated into CI.
|
|
168
|
+
|
|
169
|
+
#### Scenario: Tests skip when not opted in
|
|
170
|
+
|
|
171
|
+
- **WHEN** `INTEGRATION_TESTS` env var is not set or not `"true"`
|
|
172
|
+
- **THEN** all integration tests SHALL be skipped gracefully (not fail)
|
|
173
|
+
|
|
174
|
+
#### Scenario: npm script available
|
|
175
|
+
|
|
176
|
+
- **WHEN** a developer runs `npm run test:integration`
|
|
177
|
+
- **THEN** integration tests SHALL execute with `INTEGRATION_TESTS=true` set automatically
|
|
178
|
+
|
|
179
|
+
#### Scenario: CI runs on main only
|
|
180
|
+
|
|
181
|
+
- **GIVEN** the CI workflow configuration
|
|
182
|
+
- **THEN** the integration test job SHALL only run on pushes to `main` (not on PRs)
|
|
183
|
+
- **AND** the job SHALL have a timeout of 5 minutes
|
|
184
|
+
- **AND** the job SHALL depend on unit and E2E test jobs passing first
|
|
185
|
+
|
|
186
|
+
### Requirement: Documentation
|
|
187
|
+
|
|
188
|
+
#### Scenario: Testing docs exist
|
|
189
|
+
|
|
190
|
+
- **THEN** `docs/testing.md` SHALL document all test tiers (unit, E2E mock, E2E integration)
|
|
191
|
+
- **AND** explain how to run integration tests locally
|
|
192
|
+
- **AND** explain the env var gating mechanism
|
|
193
|
+
|
|
194
|
+
#### Scenario: README updated
|
|
195
|
+
|
|
196
|
+
- **THEN** the README testing section SHALL reference the integration test tier
|
|
197
|
+
- **AND** include the `npm run test:integration` command
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Tasks
|
|
2
|
+
|
|
3
|
+
## Phase 1: Infrastructure
|
|
4
|
+
|
|
5
|
+
- [ ] Create `tests/integration/helpers/gateway.ts` — gateway lifecycle (start, stop, health check, PID tracking, cleanup)
|
|
6
|
+
- [ ] Create `tests/integration/helpers/config.ts` — temp config file generation from `UrlGuardConfig`
|
|
7
|
+
- [ ] Create `tests/integration/helpers/client.ts` — HTTP client for sending tool calls to gateway
|
|
8
|
+
- [ ] Create `tests/integration/fixtures/urlhaus-sample.csv` — static URLhaus feed snapshot (5-10 entries)
|
|
9
|
+
- [ ] Add `test:integration` script to `package.json` (sets `INTEGRATION_TESTS=true`, runs vitest on `tests/integration/`)
|
|
10
|
+
|
|
11
|
+
## Phase 2: Core Test Suites
|
|
12
|
+
|
|
13
|
+
- [ ] Create `tests/integration/allowlist.test.ts` — allowlist enforcement (allowed, blocked, wildcard, bare domain)
|
|
14
|
+
- [ ] Create `tests/integration/blocklist.test.ts` — blocklist enforcement (blocked, unlisted passes)
|
|
15
|
+
- [ ] Create `tests/integration/hybrid.test.ts` — hybrid mode (allowed+not-blocked passes, allowed+blocked rejected, not-in-allowlist rejected)
|
|
16
|
+
- [ ] Create `tests/integration/ssrf.test.ts` — SSRF protection (localhost, link-local, IPv6 loopback, cloud metadata)
|
|
17
|
+
- [ ] Create `tests/integration/urlhaus.test.ts` — URLhaus fixture-based blocking
|
|
18
|
+
|
|
19
|
+
## Phase 3: Error Handling & Validation
|
|
20
|
+
|
|
21
|
+
- [ ] Create `tests/integration/config-validation.test.ts` — malformed config errors (missing mode, invalid mode, missing allowlist)
|
|
22
|
+
- [ ] Create `tests/integration/error-handling.test.ts` — gateway error behavior, structured block response format validation
|
|
23
|
+
|
|
24
|
+
## Phase 4: CI & Documentation
|
|
25
|
+
|
|
26
|
+
- [ ] Add `test-integration-real` job to `.forgejo/workflows/ci.yaml` (main-only, 5min timeout, depends on unit+e2e)
|
|
27
|
+
- [ ] Create `docs/testing.md` — document all test tiers, local setup, env var gating
|
|
28
|
+
- [ ] Update `README.md` testing section — reference integration tier, `npm run test:integration`
|
|
29
|
+
|
|
30
|
+
## Dependencies
|
|
31
|
+
|
|
32
|
+
- Phase 2 depends on Phase 1 (helpers must exist)
|
|
33
|
+
- Phase 3 depends on Phase 1
|
|
34
|
+
- Phase 4 can start after Phase 1 (CI job) or after Phase 3 (docs)
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
schema: spec-driven
|
|
2
|
+
|
|
3
|
+
context: |
|
|
4
|
+
Tech stack: TypeScript, Vitest, Node.js 20
|
|
5
|
+
OpenClaw plugin for URL security (allowlist/blocklist/hybrid modes)
|
|
6
|
+
Uses URLhaus threat feed, trust scoring, SSRF protection
|
|
7
|
+
CI: Forgejo Actions on Codeberg (codeberg-tiny/codeberg-small runners)
|
|
8
|
+
Conventions: conventional commits, ESLint + Prettier, vitest for testing
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
# E2E Real Instance Integration Tests
|
|
2
|
+
|
|
3
|
+
## ADDED Requirements
|
|
4
|
+
|
|
5
|
+
### Requirement: Gateway lifecycle management
|
|
6
|
+
|
|
7
|
+
The system SHALL provide helper functions to start, health-check, and stop a real OpenClaw gateway instance for testing purposes.
|
|
8
|
+
|
|
9
|
+
#### Scenario: Start gateway with custom config
|
|
10
|
+
|
|
11
|
+
- **WHEN** a test suite starts with a given `UrlGuardConfig`
|
|
12
|
+
- **THEN** the helper SHALL generate a temp config file with the plugin configured
|
|
13
|
+
- **AND** spawn `openclaw gateway` as a subprocess on a dynamically assigned port
|
|
14
|
+
- **AND** wait for the health endpoint to respond (HTTP 200) with exponential backoff
|
|
15
|
+
- **AND** fail the test suite if the gateway does not become healthy within 30 seconds
|
|
16
|
+
|
|
17
|
+
#### Scenario: Stop gateway on suite teardown
|
|
18
|
+
|
|
19
|
+
- **WHEN** a test suite completes (pass or fail)
|
|
20
|
+
- **THEN** the helper SHALL kill the gateway subprocess and delete temp config files
|
|
21
|
+
- **AND** if the test runner crashes, a `process.on('exit')` handler SHALL kill any tracked gateway PIDs
|
|
22
|
+
|
|
23
|
+
#### Scenario: Port isolation
|
|
24
|
+
|
|
25
|
+
- **WHEN** multiple test suites run (sequentially or in parallel)
|
|
26
|
+
- **THEN** each suite SHALL use a unique port (via port 0 binding or find-free-port)
|
|
27
|
+
- **AND** no port collisions SHALL occur
|
|
28
|
+
|
|
29
|
+
### Requirement: Allowlist enforcement through real gateway
|
|
30
|
+
|
|
31
|
+
The system SHALL verify that allowlist mode works correctly through the real OpenClaw gateway.
|
|
32
|
+
|
|
33
|
+
#### Scenario: Allowed domain passes through
|
|
34
|
+
|
|
35
|
+
- **GIVEN** a gateway configured with mode `allowlist` and allowlist `["example.com"]`
|
|
36
|
+
- **WHEN** a tool call for `web_fetch` with URL `https://example.com/page` is sent to the gateway
|
|
37
|
+
- **THEN** the response SHALL indicate the request was allowed
|
|
38
|
+
|
|
39
|
+
#### Scenario: Unlisted domain is blocked
|
|
40
|
+
|
|
41
|
+
- **GIVEN** a gateway configured with mode `allowlist` and allowlist `["example.com"]`
|
|
42
|
+
- **WHEN** a tool call for `web_fetch` with URL `https://evil.com/payload` is sent
|
|
43
|
+
- **THEN** the response SHALL be a structured block response
|
|
44
|
+
- **AND** the `reason` field SHALL be `"not in allowlist"`
|
|
45
|
+
- **AND** the `policy` field SHALL be `"allowlist"`
|
|
46
|
+
|
|
47
|
+
#### Scenario: Wildcard allowlist matching
|
|
48
|
+
|
|
49
|
+
- **GIVEN** a gateway configured with allowlist `["*.example.com"]`
|
|
50
|
+
- **WHEN** a tool call for `https://api.example.com/data` is sent
|
|
51
|
+
- **THEN** the request SHALL be allowed
|
|
52
|
+
- **AND** a request for `https://example.com/data` (bare domain) SHALL be blocked
|
|
53
|
+
|
|
54
|
+
### Requirement: Blocklist enforcement through real gateway
|
|
55
|
+
|
|
56
|
+
The system SHALL verify blocklist mode through the real gateway.
|
|
57
|
+
|
|
58
|
+
#### Scenario: Blocked domain is rejected
|
|
59
|
+
|
|
60
|
+
- **GIVEN** a gateway configured with mode `blocklist` and blocklist `["evil.com", "*.malware.org"]`
|
|
61
|
+
- **WHEN** a tool call for `https://evil.com/exploit` is sent
|
|
62
|
+
- **THEN** the response SHALL be a structured block response with reason `"in blocklist"`
|
|
63
|
+
|
|
64
|
+
#### Scenario: Unlisted domain passes in blocklist mode
|
|
65
|
+
|
|
66
|
+
- **GIVEN** a gateway configured with mode `blocklist` and blocklist `["evil.com"]`
|
|
67
|
+
- **WHEN** a tool call for `https://safe.com/page` is sent
|
|
68
|
+
- **THEN** the request SHALL be allowed
|
|
69
|
+
|
|
70
|
+
### Requirement: Hybrid mode enforcement through real gateway
|
|
71
|
+
|
|
72
|
+
The system SHALL verify hybrid mode (allowlist + blocklist) through the real gateway.
|
|
73
|
+
|
|
74
|
+
#### Scenario: Allowed and not blocked passes
|
|
75
|
+
|
|
76
|
+
- **GIVEN** a gateway with mode `hybrid`, allowlist `["*.example.com"]`, blocklist `["blocked.example.com"]`
|
|
77
|
+
- **WHEN** a tool call for `https://api.example.com/data` is sent
|
|
78
|
+
- **THEN** the request SHALL be allowed
|
|
79
|
+
|
|
80
|
+
#### Scenario: Allowed but also blocked is rejected
|
|
81
|
+
|
|
82
|
+
- **GIVEN** the same hybrid config
|
|
83
|
+
- **WHEN** a tool call for `https://blocked.example.com/data` is sent
|
|
84
|
+
- **THEN** the response SHALL be a structured block response
|
|
85
|
+
|
|
86
|
+
#### Scenario: Not in allowlist is rejected in hybrid mode
|
|
87
|
+
|
|
88
|
+
- **GIVEN** the same hybrid config
|
|
89
|
+
- **WHEN** a tool call for `https://outside.com/page` is sent
|
|
90
|
+
- **THEN** the response SHALL be blocked with reason `"not in allowlist"`
|
|
91
|
+
|
|
92
|
+
### Requirement: SSRF protection through real gateway
|
|
93
|
+
|
|
94
|
+
The system SHALL verify that private/internal IP addresses are blocked at the integration level.
|
|
95
|
+
|
|
96
|
+
#### Scenario: Localhost is blocked
|
|
97
|
+
|
|
98
|
+
- **GIVEN** a gateway configured with `blockPrivateIps: true`
|
|
99
|
+
- **WHEN** a tool call for `http://127.0.0.1/admin` is sent
|
|
100
|
+
- **THEN** the response SHALL be a structured block response with reason `"private or internal IP blocked"`
|
|
101
|
+
|
|
102
|
+
#### Scenario: Link-local address is blocked
|
|
103
|
+
|
|
104
|
+
- **GIVEN** the same config
|
|
105
|
+
- **WHEN** a tool call for `http://169.254.169.254/metadata` is sent (cloud metadata endpoint)
|
|
106
|
+
- **THEN** the response SHALL be blocked
|
|
107
|
+
|
|
108
|
+
#### Scenario: IPv6 loopback is blocked
|
|
109
|
+
|
|
110
|
+
- **GIVEN** the same config
|
|
111
|
+
- **WHEN** a tool call for `http://[::1]/admin` is sent
|
|
112
|
+
- **THEN** the response SHALL be blocked
|
|
113
|
+
|
|
114
|
+
### Requirement: URLhaus blocking with fixture feed
|
|
115
|
+
|
|
116
|
+
The system SHALL verify URLhaus-based blocking using a deterministic fixture feed.
|
|
117
|
+
|
|
118
|
+
#### Scenario: Known malicious URL is blocked
|
|
119
|
+
|
|
120
|
+
- **GIVEN** a gateway configured with URLhaus enabled, pointing to `tests/integration/fixtures/urlhaus-sample.csv`
|
|
121
|
+
- **WHEN** a tool call for a URL present in the fixture feed is sent
|
|
122
|
+
- **THEN** the response SHALL be blocked with a reason indicating URLhaus match
|
|
123
|
+
|
|
124
|
+
#### Scenario: Clean URL passes URLhaus check
|
|
125
|
+
|
|
126
|
+
- **GIVEN** the same config
|
|
127
|
+
- **WHEN** a tool call for `https://clean-example.com/safe` (not in fixture) is sent
|
|
128
|
+
- **THEN** the request SHALL be allowed (assuming it passes other policy checks)
|
|
129
|
+
|
|
130
|
+
### Requirement: Config validation error handling
|
|
131
|
+
|
|
132
|
+
The system SHALL produce clear errors when given malformed configuration.
|
|
133
|
+
|
|
134
|
+
#### Scenario: Missing required mode field
|
|
135
|
+
|
|
136
|
+
- **GIVEN** a config file with the plugin configured but `mode` field omitted
|
|
137
|
+
- **WHEN** the gateway starts
|
|
138
|
+
- **THEN** the gateway SHALL fail to start or the plugin SHALL report a clear validation error
|
|
139
|
+
- **AND** the error message SHALL mention the missing `mode` field
|
|
140
|
+
|
|
141
|
+
#### Scenario: Invalid mode value
|
|
142
|
+
|
|
143
|
+
- **GIVEN** a config with `mode: "invalid_mode"`
|
|
144
|
+
- **WHEN** the gateway starts
|
|
145
|
+
- **THEN** a clear validation error SHALL be reported
|
|
146
|
+
|
|
147
|
+
#### Scenario: Allowlist mode without allowlist array
|
|
148
|
+
|
|
149
|
+
- **GIVEN** a config with `mode: "allowlist"` but no `allowlist` field
|
|
150
|
+
- **WHEN** a tool call is made
|
|
151
|
+
- **THEN** the behavior SHALL be well-defined (either block all, or error clearly)
|
|
152
|
+
|
|
153
|
+
### Requirement: Structured block response format
|
|
154
|
+
|
|
155
|
+
All blocked requests through the real gateway SHALL return a structured response.
|
|
156
|
+
|
|
157
|
+
#### Scenario: Block response contains required fields
|
|
158
|
+
|
|
159
|
+
- **WHEN** any request is blocked by the plugin
|
|
160
|
+
- **THEN** the response SHALL contain at minimum:
|
|
161
|
+
- `blocked`: `true`
|
|
162
|
+
- `reason`: a human-readable explanation (matching `getBlockReason()` output)
|
|
163
|
+
- `url`: the original requested URL
|
|
164
|
+
|
|
165
|
+
### Requirement: Test gating and CI integration
|
|
166
|
+
|
|
167
|
+
The integration tests SHALL be gated and integrated into CI.
|
|
168
|
+
|
|
169
|
+
#### Scenario: Tests skip when not opted in
|
|
170
|
+
|
|
171
|
+
- **WHEN** `INTEGRATION_TESTS` env var is not set or not `"true"`
|
|
172
|
+
- **THEN** all integration tests SHALL be skipped gracefully (not fail)
|
|
173
|
+
|
|
174
|
+
#### Scenario: npm script available
|
|
175
|
+
|
|
176
|
+
- **WHEN** a developer runs `npm run test:integration`
|
|
177
|
+
- **THEN** integration tests SHALL execute with `INTEGRATION_TESTS=true` set automatically
|
|
178
|
+
|
|
179
|
+
#### Scenario: CI runs on main only
|
|
180
|
+
|
|
181
|
+
- **GIVEN** the CI workflow configuration
|
|
182
|
+
- **THEN** the integration test job SHALL only run on pushes to `main` (not on PRs)
|
|
183
|
+
- **AND** the job SHALL have a timeout of 5 minutes
|
|
184
|
+
- **AND** the job SHALL depend on unit and E2E test jobs passing first
|
|
185
|
+
|
|
186
|
+
### Requirement: Documentation
|
|
187
|
+
|
|
188
|
+
#### Scenario: Testing docs exist
|
|
189
|
+
|
|
190
|
+
- **THEN** `docs/testing.md` SHALL document all test tiers (unit, E2E mock, E2E integration)
|
|
191
|
+
- **AND** explain how to run integration tests locally
|
|
192
|
+
- **AND** explain the env var gating mechanism
|
|
193
|
+
|
|
194
|
+
#### Scenario: README updated
|
|
195
|
+
|
|
196
|
+
- **THEN** the README testing section SHALL reference the integration test tier
|
|
197
|
+
- **AND** include the `npm run test:integration` command
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@llnvd/openclaw-url-guard",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "OpenClaw plugin for URL allowlisting/blocklisting in web_fetch and web_search tools",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "tsc -p tsconfig.json",
|
|
9
|
+
"test": "vitest run",
|
|
10
|
+
"test:e2e": "vitest run tests/e2e/",
|
|
11
|
+
"test:integration": "INTEGRATION_TESTS=true vitest run tests/integration/",
|
|
12
|
+
"lint": "oxlint src/ tests/",
|
|
13
|
+
"lint:fix": "oxlint --fix src/ tests/",
|
|
14
|
+
"format": "prettier --write 'src/**/*.ts' 'tests/**/*.ts'",
|
|
15
|
+
"format:check": "prettier --check 'src/**/*.ts' 'tests/**/*.ts'",
|
|
16
|
+
"check": "npm run lint && npm run format:check && npm run test",
|
|
17
|
+
"prepare": "husky"
|
|
18
|
+
},
|
|
19
|
+
"lint-staged": {
|
|
20
|
+
"*.ts": [
|
|
21
|
+
"oxlint --fix",
|
|
22
|
+
"prettier --write"
|
|
23
|
+
]
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@types/node": "^25.3.5",
|
|
27
|
+
"husky": "^9.1.7",
|
|
28
|
+
"lint-staged": "^15.4.3",
|
|
29
|
+
"oxlint": "^1.51.0",
|
|
30
|
+
"prettier": "^3.5.3",
|
|
31
|
+
"typescript": "^5.6.3",
|
|
32
|
+
"vitest": "^2.1.8"
|
|
33
|
+
},
|
|
34
|
+
"openclaw": {
|
|
35
|
+
"extensions": [
|
|
36
|
+
"./dist/src/index.js"
|
|
37
|
+
]
|
|
38
|
+
}
|
|
39
|
+
}
|