crowdsec-local-mcp 0.2.0__py3-none-any.whl → 0.8.0.post1.dev0__py3-none-any.whl
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.
- crowdsec_local_mcp/__init__.py +6 -1
- crowdsec_local_mcp/__main__.py +1 -3
- crowdsec_local_mcp/_version.py +1 -0
- crowdsec_local_mcp/compose/scenario-test/.gitignore +1 -0
- crowdsec_local_mcp/compose/scenario-test/docker-compose.yml +19 -0
- crowdsec_local_mcp/compose/scenario-test/scenarios/.gitkeep +0 -0
- crowdsec_local_mcp/compose/waf-test/docker-compose.yml +5 -6
- crowdsec_local_mcp/compose/waf-test/nginx/Dockerfile +8 -2
- crowdsec_local_mcp/mcp_core.py +112 -18
- crowdsec_local_mcp/mcp_scenarios.py +579 -23
- crowdsec_local_mcp/mcp_waf.py +774 -402
- crowdsec_local_mcp/prompts/prompt-expr-helpers.txt +514 -0
- crowdsec_local_mcp/prompts/prompt-scenario-deploy.txt +70 -21
- crowdsec_local_mcp/prompts/prompt-scenario.txt +26 -2
- crowdsec_local_mcp/prompts/prompt-waf-pr.txt +10 -0
- crowdsec_local_mcp/prompts/prompt-waf-tests.txt +113 -0
- crowdsec_local_mcp/prompts/prompt-waf-top-level.txt +33 -0
- crowdsec_local_mcp/prompts/prompt-waf.txt +0 -26
- crowdsec_local_mcp/setup_cli.py +98 -29
- crowdsec_local_mcp-0.8.0.post1.dev0.dist-info/METADATA +114 -0
- crowdsec_local_mcp-0.8.0.post1.dev0.dist-info/RECORD +39 -0
- {crowdsec_local_mcp-0.2.0.dist-info → crowdsec_local_mcp-0.8.0.post1.dev0.dist-info}/WHEEL +1 -1
- crowdsec_local_mcp-0.2.0.dist-info/METADATA +0 -74
- crowdsec_local_mcp-0.2.0.dist-info/RECORD +0 -31
- {crowdsec_local_mcp-0.2.0.dist-info → crowdsec_local_mcp-0.8.0.post1.dev0.dist-info}/entry_points.txt +0 -0
- {crowdsec_local_mcp-0.2.0.dist-info → crowdsec_local_mcp-0.8.0.post1.dev0.dist-info}/licenses/LICENSE +0 -0
- {crowdsec_local_mcp-0.2.0.dist-info → crowdsec_local_mcp-0.8.0.post1.dev0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
You are an expert in generating automated test assets for the CrowdSec WAF harness. Given a Nuclei template and the name of the rule under test, produce the artifacts required by the docker-based test workflow. Focus exclusively on generating the test configuration (`config.yaml`) and the adapted Nuclei template that validates the rule blocks the attack with a `403` status code.
|
|
2
|
+
|
|
3
|
+
## Test Artifact Guidelines
|
|
4
|
+
|
|
5
|
+
1. **config.yaml**
|
|
6
|
+
- Must be valid YAML with the keys `appsec-rules:` and `nuclei_template`.
|
|
7
|
+
- `appsec-rules:` is a YAML list that always includes:
|
|
8
|
+
- `./appsec-rules/crowdsecurity/base-config.yaml`
|
|
9
|
+
- The path to the generated rule (e.g. `./appsec-rules/crowdsecurity/vpatch-CVE-2020-17496.yaml`).
|
|
10
|
+
- `nuclei_template` is the filename of the adapted Nuclei template (e.g. `CVE-2020-17496.yaml`).
|
|
11
|
+
- The path in `appsec-rules` is relative to the hub root, because the rule is not on the hub yet.
|
|
12
|
+
- Use this template and replace the placeholders only:
|
|
13
|
+
```
|
|
14
|
+
appsec-rules:
|
|
15
|
+
- ./appsec-rules/crowdsecurity/base-config.yaml
|
|
16
|
+
- <PUT_THE_NEW_RULE_PATH_HERE>
|
|
17
|
+
nuclei_template: <PUT_THE_TEST_TEMPLATE_FILENAME_HERE>
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
2. **Adapted Nuclei Template**
|
|
21
|
+
- Preserve the original request structure that exercises the vulnerability.
|
|
22
|
+
- Normalise metadata so the template identifies itself as a test:
|
|
23
|
+
- `info.name` matches the CVE identifier.
|
|
24
|
+
- `info.author` is `crowdsec`.
|
|
25
|
+
- `info.severity` is `info`.
|
|
26
|
+
- `info.description` briefly states it is for testing (e.g. `CVE-2020-17496 testing`).
|
|
27
|
+
- `info.tags` includes `appsec-testing`.
|
|
28
|
+
- Keep payloads, raw requests, and helper fields that are required for the exploit to function.
|
|
29
|
+
- Force the rule-under-test to respond with a block by ensuring the only matcher checks for HTTP status code `403`. Remove every other matcher from the original template.
|
|
30
|
+
- When the original template lacks an explicit matcher, add a `matchers` block with a single status matcher for `403`.
|
|
31
|
+
- If the template requires `cookie-reuse` or other execution flags, keep them intact.
|
|
32
|
+
|
|
33
|
+
3. **General Expectations**
|
|
34
|
+
- Do not attempt to regenerate the detection rule—only produce the two testing artifacts.
|
|
35
|
+
- Respect YAML indentation (two spaces) and quote strings that contain special characters.
|
|
36
|
+
- Mirror the case and structure of URIs, headers, and payloads from the input template.
|
|
37
|
+
- Provide helpful inline comments only when strictly necessary to explain intentional failures.
|
|
38
|
+
- Place test assets in the hub under `.appsec-tests/<test_name>/`:
|
|
39
|
+
- `config.yaml`
|
|
40
|
+
- `<test_name>.yaml` (the adapted nuclei template)
|
|
41
|
+
- If the rule is for virtual patching, use `CVE-YYYY-XYZ` as the test name and `vpatch-CVE-YYYY-XYZ` as the rule name.
|
|
42
|
+
|
|
43
|
+
## Output Format
|
|
44
|
+
|
|
45
|
+
Always return exactly two sections separated by delimiters:
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
===TEST_CONFIG===
|
|
49
|
+
<config.yaml content>
|
|
50
|
+
|
|
51
|
+
===TEST_NUCLEI===
|
|
52
|
+
<adapted nuclei template content>
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
No additional sections or commentary should be emitted.
|
|
56
|
+
|
|
57
|
+
## Example Input
|
|
58
|
+
|
|
59
|
+
```yaml
|
|
60
|
+
id: CVE-2020-17496
|
|
61
|
+
|
|
62
|
+
info:
|
|
63
|
+
name: vBulletin 5.5.4 - 5.6.2- Remote Command Execution
|
|
64
|
+
author: pussycat0x
|
|
65
|
+
severity: critical
|
|
66
|
+
description: 'vBulletin versions 5.5.4 through 5.6.2 allow remote command execution via crafted subWidgets data in an ajax/render/widget_tabbedcontainer_tab_panel request. NOTE: this issue exists because of an incomplete fix for CVE-2019-16759.'
|
|
67
|
+
|
|
68
|
+
http:
|
|
69
|
+
- raw:
|
|
70
|
+
- |
|
|
71
|
+
POST /ajax/render/widget_tabbedcontainer_tab_panel HTTP/1.1
|
|
72
|
+
Host: {{Hostname}}
|
|
73
|
+
Content-Type: application/x-www-form-urlencoded
|
|
74
|
+
|
|
75
|
+
subWidgets[0][template]=widget_php&subWidgets[0][config][code]=echo shell_exec('cat ../../../../../../../../../../../../etc/passwd'); exit;
|
|
76
|
+
|
|
77
|
+
matchers-condition: and
|
|
78
|
+
matchers:
|
|
79
|
+
- type: status
|
|
80
|
+
status:
|
|
81
|
+
- 200
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Example Output
|
|
85
|
+
|
|
86
|
+
===TEST_CONFIG===
|
|
87
|
+
appsec-rules:
|
|
88
|
+
- ./appsec-rules/crowdsecurity/base-config.yaml
|
|
89
|
+
- ./appsec-rules/crowdsecurity/vpatch-CVE-2020-17496.yaml
|
|
90
|
+
nuclei_template: CVE-2020-17496.yaml
|
|
91
|
+
|
|
92
|
+
===TEST_NUCLEI===
|
|
93
|
+
id: CVE-2020-17496
|
|
94
|
+
info:
|
|
95
|
+
name: CVE-2020-17496
|
|
96
|
+
author: crowdsec
|
|
97
|
+
severity: info
|
|
98
|
+
description: CVE-2020-17496 testing
|
|
99
|
+
tags: appsec-testing
|
|
100
|
+
http:
|
|
101
|
+
- raw:
|
|
102
|
+
- |
|
|
103
|
+
POST /ajax/render/widget_tabbedcontainer_tab_panel HTTP/1.1
|
|
104
|
+
Host: {{Hostname}}
|
|
105
|
+
Content-Type: application/x-www-form-urlencoded
|
|
106
|
+
|
|
107
|
+
subWidgets[0][template]=widget_php&subWidgets[0][config][code]=echo shell_exec('cat ../../../../../../../../../../../../etc/passwd'); exit;
|
|
108
|
+
|
|
109
|
+
cookie-reuse: true
|
|
110
|
+
matchers:
|
|
111
|
+
- type: status
|
|
112
|
+
status:
|
|
113
|
+
- 403
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
You are the top-level cyber security assistant for orchestrating CrowdSec AppSec WAF tasks. Treat this prompt as the first step whenever the user asks about creating, reviewing, testing, or deploying WAF rules so you can decide which specialized tool or prompt to invoke next.
|
|
2
|
+
|
|
3
|
+
The process is the following, and it's CRITICAL to follow it unless asked otherwise by the user:
|
|
4
|
+
|
|
5
|
+
1. **Validate User Intent**: Ensure that you understand user objective and that your rule will follow his directives.
|
|
6
|
+
- If the user provides a CVE, use `fetch_nuclei_exploit` to get a working nuclei template / exploit.
|
|
7
|
+
- If the user provides a natural language description, ask further question if necessary to be sure the rule will be specific enough.
|
|
8
|
+
2. **Generate Initial Rule**: Refer to `get_waf_prompt` tool and prompt for syntax and guidelines. If you want everything bundled (main prompt + examples), call `generate_waf_rule` instead.
|
|
9
|
+
3. **Validate Syntax**: Use the `validate_waf_rule` tool to check YAML syntax
|
|
10
|
+
4. **Lint for Quality**: Use the `lint_waf_rule` tool to check for warnings and improvement hints
|
|
11
|
+
5. **Iterate if Needed**: If validation fails or linter shows warnings, fix the issues and repeat steps 2-3
|
|
12
|
+
6. **Final Output**: Only provide final output when both validation passes and linting shows no critical issues. Summarize results and link the user to next-step tooling options below.
|
|
13
|
+
|
|
14
|
+
⚠️ Final Validation Checklist (REQUIRED before output ends):
|
|
15
|
+
- Confirm validation tool returns "✅ VALIDATION PASSED"
|
|
16
|
+
- Address all warnings from linting tool
|
|
17
|
+
|
|
18
|
+
**IMPORTANT**: If validation or linting fails, you MUST iterate and fix the issues before providing the final output. Do not output rules that fail validation or have critical linting warnings.
|
|
19
|
+
|
|
20
|
+
### After presenting **Final Output** to the user:
|
|
21
|
+
|
|
22
|
+
Once you have successfully generated and validated a WAF rule, propose the user the next steps:
|
|
23
|
+
- Deploy the rule on a live instance:
|
|
24
|
+
- Use tool `manage_waf_stack`
|
|
25
|
+
- Allows you and the user to manually tests for false-positives and false-negatives
|
|
26
|
+
- Create and validate a test for the rule - allows contribution to the hub:
|
|
27
|
+
- Use tool `generate_waf_tests` to get instructions on test creation
|
|
28
|
+
- Use tool `run_waf_tests` to run and validate the tests
|
|
29
|
+
- Once done, suggest help preparing a PR
|
|
30
|
+
- Use tool `get_waf_pr_prompt` to guide PR preparation and draft a PR comment
|
|
31
|
+
- Use tool `prepare_waf_pr` to write the rule and test assets into a local hub clone
|
|
32
|
+
- Help the user to deploy the rule on his setup:
|
|
33
|
+
- Use tool `deploy_waf_rule` to get detailed instructions and guide the user.
|
|
@@ -227,15 +227,6 @@ You are an expert in cybersecurity and threat detection, specializing in automat
|
|
|
227
227
|
value: '/src/options.php'
|
|
228
228
|
```
|
|
229
229
|
|
|
230
|
-
### Validation and Quality Assurance Process:
|
|
231
|
-
**CRITICAL: You MUST follow this iterative validation process:**
|
|
232
|
-
|
|
233
|
-
1. **Generate Initial Rule**: Create the detection rule following all guidelines above
|
|
234
|
-
2. **Validate Syntax**: Use the `validate_waf_rule` tool to check YAML syntax
|
|
235
|
-
3. **Lint for Quality**: Use the `lint_waf_rule` tool to check for warnings and improvement hints
|
|
236
|
-
4. **Iterate if Needed**: If validation fails or linter shows warnings, fix the issues and repeat steps 2-3
|
|
237
|
-
5. **Final Output**: Only provide final output when both validation passes and linting shows no critical issues
|
|
238
|
-
|
|
239
230
|
### Output Format with Delimiters:
|
|
240
231
|
|
|
241
232
|
Output format (use the exact delimiters):
|
|
@@ -262,25 +253,8 @@ Output format (use the exact delimiters):
|
|
|
262
253
|
|
|
263
254
|
- <verbatim output from linter tool>
|
|
264
255
|
|
|
265
|
-
## Next Steps
|
|
266
|
-
- Do you want me to test the WAF rule
|
|
267
|
-
- Do you want some deployment guidance
|
|
268
256
|
```
|
|
269
257
|
|
|
270
|
-
⚠️ Final Validation Checklist (REQUIRED before output ends):
|
|
271
|
-
- Confirm validation tool returns "✅ VALIDATION PASSED"
|
|
272
|
-
- Address all warnings from linting tool
|
|
273
|
-
- Confirm all value: fields are lowercase
|
|
274
|
-
- Confirm transform includes lowercase wherever applicable
|
|
275
|
-
- Confirm no match.value contains capital letters
|
|
276
|
-
- Confirm rule uses contains instead of regex when applicable
|
|
277
|
-
|
|
278
|
-
**IMPORTANT**: If validation or linting fails, you MUST iterate and fix the issues before providing the final output. Do not output rules that fail validation or have critical linting warnings.
|
|
279
|
-
|
|
280
|
-
### After Rule Generation and Validation:
|
|
281
|
-
Once you have successfully generated and validated a WAF rule, remind the user that the next step is to deploy it. Use the `deploy_waf_rule` tool to provide comprehensive deployment instructions.
|
|
282
|
-
|
|
283
|
-
|
|
284
258
|
### Example Input (Nuclei Template):
|
|
285
259
|
```yaml
|
|
286
260
|
id: CVE-2025-24893
|
crowdsec_local_mcp/setup_cli.py
CHANGED
|
@@ -2,27 +2,28 @@ import argparse
|
|
|
2
2
|
import json
|
|
3
3
|
import os
|
|
4
4
|
import platform
|
|
5
|
+
import shlex
|
|
5
6
|
import shutil
|
|
7
|
+
import subprocess
|
|
6
8
|
import sys
|
|
7
9
|
from dataclasses import dataclass
|
|
8
10
|
from pathlib import Path
|
|
9
|
-
from
|
|
11
|
+
from collections.abc import Iterable
|
|
10
12
|
|
|
11
13
|
SERVER_KEY = "crowdsec-local-mcp"
|
|
12
|
-
SERVER_LABEL = "
|
|
13
|
-
|
|
14
|
+
SERVER_LABEL = "CrowdSecMCP"
|
|
14
15
|
|
|
15
16
|
@dataclass
|
|
16
17
|
class CLIArgs:
|
|
17
18
|
target: str
|
|
18
|
-
config_path:
|
|
19
|
+
config_path: Path | None
|
|
19
20
|
dry_run: bool
|
|
20
21
|
force: bool
|
|
21
|
-
command_override:
|
|
22
|
-
cwd_override:
|
|
22
|
+
command_override: str | None
|
|
23
|
+
cwd_override: Path | None
|
|
23
24
|
|
|
24
25
|
|
|
25
|
-
def main(argv:
|
|
26
|
+
def main(argv: Iterable[str] | None = None) -> None:
|
|
26
27
|
args = _parse_args(argv)
|
|
27
28
|
command, cmd_args = _resolve_runner(args.command_override)
|
|
28
29
|
server_payload = {
|
|
@@ -42,6 +43,8 @@ def main(argv: Optional[Iterable[str]] = None) -> None:
|
|
|
42
43
|
|
|
43
44
|
if args.target == "claude-desktop":
|
|
44
45
|
_configure_claude(args, server_payload)
|
|
46
|
+
elif args.target == "claude-code":
|
|
47
|
+
_configure_claude_code(args, server_payload)
|
|
45
48
|
elif args.target == "chatgpt":
|
|
46
49
|
_configure_chatgpt(args, server_payload)
|
|
47
50
|
elif args.target == "vscode":
|
|
@@ -50,17 +53,17 @@ def main(argv: Optional[Iterable[str]] = None) -> None:
|
|
|
50
53
|
raise ValueError(f"Unsupported target '{args.target}'")
|
|
51
54
|
|
|
52
55
|
|
|
53
|
-
def _parse_args(argv:
|
|
56
|
+
def _parse_args(argv: Iterable[str] | None) -> CLIArgs:
|
|
54
57
|
parser = argparse.ArgumentParser(
|
|
55
58
|
prog="init",
|
|
56
59
|
description=(
|
|
57
60
|
"Initialize CrowdSec MCP integration for supported clients "
|
|
58
|
-
"(Claude Desktop, ChatGPT Desktop, Visual Studio Code, or stdio)."
|
|
61
|
+
"(Claude Desktop, Claude Code, ChatGPT Desktop, Visual Studio Code, or stdio)."
|
|
59
62
|
),
|
|
60
63
|
)
|
|
61
64
|
parser.add_argument(
|
|
62
65
|
"target",
|
|
63
|
-
choices=("claude-desktop", "chatgpt", "vscode", "stdio"),
|
|
66
|
+
choices=("claude-desktop", "claude-code", "chatgpt", "vscode", "stdio"),
|
|
64
67
|
help="Client to configure.",
|
|
65
68
|
)
|
|
66
69
|
parser.add_argument(
|
|
@@ -105,7 +108,7 @@ def _parse_args(argv: Optional[Iterable[str]]) -> CLIArgs:
|
|
|
105
108
|
)
|
|
106
109
|
|
|
107
110
|
|
|
108
|
-
def _resolve_runner(command_override:
|
|
111
|
+
def _resolve_runner(command_override: str | None) -> tuple[str, list[str]]:
|
|
109
112
|
if command_override:
|
|
110
113
|
command_parts = command_override.strip().split()
|
|
111
114
|
if not command_parts:
|
|
@@ -129,7 +132,7 @@ def _resolve_runner(command_override: Optional[str]) -> Tuple[str, List[str]]:
|
|
|
129
132
|
return python_executable, ["-m", "crowdsec_local_mcp"]
|
|
130
133
|
|
|
131
134
|
|
|
132
|
-
def _configure_claude(args: CLIArgs, server_payload:
|
|
135
|
+
def _configure_claude(args: CLIArgs, server_payload: dict[str, object]) -> None:
|
|
133
136
|
config_path = _resolve_path(args.config_path, _claude_candidates())
|
|
134
137
|
_write_mcp_config(
|
|
135
138
|
config_path,
|
|
@@ -139,7 +142,52 @@ def _configure_claude(args: CLIArgs, server_payload: Dict[str, object]) -> None:
|
|
|
139
142
|
)
|
|
140
143
|
|
|
141
144
|
|
|
142
|
-
def
|
|
145
|
+
def _configure_claude_code(args: CLIArgs, server_payload: dict[str, object]) -> None:
|
|
146
|
+
runner_command = server_payload["command"]
|
|
147
|
+
if not isinstance(runner_command, str):
|
|
148
|
+
raise TypeError("Server payload 'command' must be a string.")
|
|
149
|
+
runner_args = server_payload.get("args", [])
|
|
150
|
+
if not isinstance(runner_args, list):
|
|
151
|
+
raise TypeError("Server payload 'args' must be a list.")
|
|
152
|
+
|
|
153
|
+
claude_invocation = [
|
|
154
|
+
"claude",
|
|
155
|
+
"mcp",
|
|
156
|
+
"add",
|
|
157
|
+
"--transport",
|
|
158
|
+
"stdio",
|
|
159
|
+
"--scope",
|
|
160
|
+
"user",
|
|
161
|
+
SERVER_LABEL,
|
|
162
|
+
"--",
|
|
163
|
+
runner_command,
|
|
164
|
+
*runner_args,
|
|
165
|
+
]
|
|
166
|
+
quoted_command = " ".join(shlex.quote(part) for part in claude_invocation)
|
|
167
|
+
|
|
168
|
+
if args.dry_run:
|
|
169
|
+
print(
|
|
170
|
+
"Run the following command to register CrowdSec MCP with Claude Code:\n"
|
|
171
|
+
f"{quoted_command}"
|
|
172
|
+
)
|
|
173
|
+
return
|
|
174
|
+
|
|
175
|
+
if shutil.which("claude") is None:
|
|
176
|
+
raise FileNotFoundError(
|
|
177
|
+
"The 'claude' CLI is not available on PATH. Install Claude Code CLI and "
|
|
178
|
+
f"run:\n{quoted_command}"
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
result = subprocess.run(claude_invocation, check=False)
|
|
182
|
+
if result.returncode != 0:
|
|
183
|
+
raise RuntimeError(
|
|
184
|
+
f"'claude mcp add' failed with exit code {result.returncode}. "
|
|
185
|
+
f"Run manually:\n{quoted_command}"
|
|
186
|
+
)
|
|
187
|
+
print("Registered CrowdSec MCP with Claude Code CLI.")
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _configure_chatgpt(args: CLIArgs, server_payload: dict[str, object]) -> None:
|
|
143
191
|
config_path = _resolve_path(args.config_path, _chatgpt_candidates())
|
|
144
192
|
_write_mcp_config(
|
|
145
193
|
config_path,
|
|
@@ -149,9 +197,9 @@ def _configure_chatgpt(args: CLIArgs, server_payload: Dict[str, object]) -> None
|
|
|
149
197
|
)
|
|
150
198
|
|
|
151
199
|
|
|
152
|
-
def _configure_vscode(args: CLIArgs, server_payload:
|
|
200
|
+
def _configure_vscode(args: CLIArgs, server_payload: dict[str, object]) -> None:
|
|
153
201
|
config_path = _resolve_path(args.config_path, _vscode_candidates())
|
|
154
|
-
vscode_payload:
|
|
202
|
+
vscode_payload: dict[str, object] = {
|
|
155
203
|
"command": server_payload["command"],
|
|
156
204
|
"args": server_payload["args"],
|
|
157
205
|
}
|
|
@@ -171,13 +219,22 @@ def _configure_vscode(args: CLIArgs, server_payload: Dict[str, object]) -> None:
|
|
|
171
219
|
|
|
172
220
|
def _write_mcp_config(
|
|
173
221
|
config_path: Path,
|
|
174
|
-
server_payload:
|
|
222
|
+
server_payload: dict[str, object],
|
|
175
223
|
args: CLIArgs,
|
|
176
224
|
*,
|
|
177
225
|
client_name: str,
|
|
178
226
|
servers_key: str = "mcpServers",
|
|
179
227
|
) -> None:
|
|
180
|
-
|
|
228
|
+
manual_snippet = json.dumps(
|
|
229
|
+
{servers_key: {SERVER_KEY: server_payload}}, indent=2
|
|
230
|
+
)
|
|
231
|
+
config, existed = _load_json(
|
|
232
|
+
config_path,
|
|
233
|
+
allow_missing=True,
|
|
234
|
+
fallback_snippet=manual_snippet,
|
|
235
|
+
)
|
|
236
|
+
if config is None:
|
|
237
|
+
return
|
|
181
238
|
if not existed and not (args.force or args.dry_run):
|
|
182
239
|
raise FileNotFoundError(
|
|
183
240
|
f"{config_path} does not exist. Re-run with --force to create it "
|
|
@@ -199,7 +256,7 @@ def _write_mcp_config(
|
|
|
199
256
|
print(f"Configured {client_name} at {config_path}")
|
|
200
257
|
|
|
201
258
|
|
|
202
|
-
def _print_stdio(server_payload:
|
|
259
|
+
def _print_stdio(server_payload: dict[str, object]) -> None:
|
|
203
260
|
snippet = {
|
|
204
261
|
"server": SERVER_KEY,
|
|
205
262
|
"command": server_payload["command"],
|
|
@@ -209,13 +266,19 @@ def _print_stdio(server_payload: Dict[str, object]) -> None:
|
|
|
209
266
|
if cwd is not None:
|
|
210
267
|
snippet["cwd"] = cwd
|
|
211
268
|
|
|
269
|
+
snippet_json = json.dumps(snippet, indent=2)
|
|
212
270
|
print(
|
|
213
271
|
"Use the following configuration with stdio-compatible MCP clients:\n"
|
|
214
|
-
f"{
|
|
272
|
+
f"{snippet_json}"
|
|
215
273
|
)
|
|
216
274
|
|
|
217
275
|
|
|
218
|
-
def _load_json(
|
|
276
|
+
def _load_json(
|
|
277
|
+
path: Path,
|
|
278
|
+
*,
|
|
279
|
+
allow_missing: bool,
|
|
280
|
+
fallback_snippet: str | None = None,
|
|
281
|
+
) -> tuple[dict[str, object] | None, bool]:
|
|
219
282
|
if not path.exists():
|
|
220
283
|
if allow_missing:
|
|
221
284
|
return {}, False
|
|
@@ -228,10 +291,16 @@ def _load_json(path: Path, *, allow_missing: bool) -> Tuple[Dict[str, object], b
|
|
|
228
291
|
try:
|
|
229
292
|
return json.loads(content), True
|
|
230
293
|
except json.JSONDecodeError as exc:
|
|
231
|
-
|
|
294
|
+
manual_msg = (
|
|
295
|
+
f"Couldn't parse {path}: {exc}.\n"
|
|
296
|
+
"Edit it manually to add:\n\n"
|
|
297
|
+
f"{fallback_snippet or '<no snippet available>'}"
|
|
298
|
+
)
|
|
299
|
+
print(manual_msg)
|
|
300
|
+
return None, True
|
|
232
301
|
|
|
233
302
|
|
|
234
|
-
def _resolve_path(explicit:
|
|
303
|
+
def _resolve_path(explicit: Path | None, candidates: list[Path]) -> Path:
|
|
235
304
|
if explicit:
|
|
236
305
|
return explicit.expanduser()
|
|
237
306
|
|
|
@@ -258,7 +327,7 @@ def _backup_file_if_exists(path: Path) -> None:
|
|
|
258
327
|
print(f"Existing configuration backed up to {backup_path}")
|
|
259
328
|
|
|
260
329
|
|
|
261
|
-
def _claude_candidates() ->
|
|
330
|
+
def _claude_candidates() -> list[Path]:
|
|
262
331
|
system = platform.system()
|
|
263
332
|
if system == "Darwin":
|
|
264
333
|
return [
|
|
@@ -274,7 +343,7 @@ def _claude_candidates() -> List[Path]:
|
|
|
274
343
|
return [Path.home() / ".config" / "Claude" / "claude_desktop_config.json"]
|
|
275
344
|
|
|
276
345
|
|
|
277
|
-
def _chatgpt_candidates() ->
|
|
346
|
+
def _chatgpt_candidates() -> list[Path]:
|
|
278
347
|
system = platform.system()
|
|
279
348
|
if system == "Darwin":
|
|
280
349
|
return [
|
|
@@ -290,17 +359,17 @@ def _chatgpt_candidates() -> List[Path]:
|
|
|
290
359
|
return [Path.home() / ".config" / "ChatGPT" / "config.json"]
|
|
291
360
|
|
|
292
361
|
|
|
293
|
-
def _vscode_candidates() ->
|
|
362
|
+
def _vscode_candidates() -> list[Path]:
|
|
294
363
|
system = platform.system()
|
|
295
364
|
if system == "Windows":
|
|
296
365
|
base = Path(os.environ.get("APPDATA", Path.home()))
|
|
297
366
|
return [base / "Code" / "User" / "mcp.json", base / "Code - Insiders" / "User" / "mcp.json"]
|
|
298
|
-
|
|
367
|
+
if system == "Darwin":
|
|
299
368
|
base = Path.home() / "Library" / "Application Support"
|
|
300
369
|
return [base / "Code" / "User" / "mcp.json", base / "Code - Insiders" / "User" / "mcp.json"]
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
370
|
+
# Linux and others
|
|
371
|
+
base = Path.home() / ".config"
|
|
372
|
+
return [base / "Code" / "User" / "mcp.json", base / "Code - Insiders" / "User" / "mcp.json"]
|
|
304
373
|
|
|
305
374
|
if __name__ == "__main__":
|
|
306
375
|
main()
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: crowdsec-local-mcp
|
|
3
|
+
Version: 0.8.0.post1.dev0
|
|
4
|
+
Summary: An MCP exposing prompts and tools to help users write WAF rules, scenarios etc.
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Classifier: Development Status :: 4 - Beta
|
|
7
|
+
Classifier: Intended Audience :: Developers
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Classifier: Topic :: Security
|
|
13
|
+
Classifier: Topic :: System :: Systems Administration
|
|
14
|
+
Classifier: Topic :: Utilities
|
|
15
|
+
Requires-Python: >=3.12
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENSE
|
|
18
|
+
Requires-Dist: jsonschema>=4.25.1
|
|
19
|
+
Requires-Dist: mcp>=1.15.0
|
|
20
|
+
Requires-Dist: pyyaml>=6.0.3
|
|
21
|
+
Requires-Dist: requests>=2.32.5
|
|
22
|
+
Dynamic: license-file
|
|
23
|
+
|
|
24
|
+
<p align="center">
|
|
25
|
+
<img src="https://github.com/crowdsecurity/crowdsec-docs/blob/main/crowdsec-docs/static/img/crowdsec_logo.png" alt="CrowdSec" title="CrowdSec" width="400" height="260"/>
|
|
26
|
+
</p>
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
**Life is too short to write YAML, just ask nicely!**
|
|
30
|
+
|
|
31
|
+
> A Model Context Protocol (MCP) server to generate, validate, and deploy CrowdSec WAF rules & Scenarios.
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
## Features
|
|
35
|
+
|
|
36
|
+
### WAF Rules Features
|
|
37
|
+
|
|
38
|
+
- **WAF Rule Generation**: Generate CrowdSec WAF rules from user input or a CVE reference
|
|
39
|
+
- **Validation**: Validate syntaxical correctness of WAF rules
|
|
40
|
+
- **Linting**: Get warnings and hints to improve your WAF rules
|
|
41
|
+
- **Deployment Guide**: Step-by-step deployment instructions
|
|
42
|
+
- **Docker Test Harness**: Spin up CrowdSec + nginx + bouncer to exercise rules for false positives/negatives
|
|
43
|
+
- **Nuclei Lookup**: Quickly jump to existing templates in the official `projectdiscovery/nuclei-templates` repository for a given CVE
|
|
44
|
+
|
|
45
|
+
### Scenarios Features
|
|
46
|
+
|
|
47
|
+
- **CrowdSec Scenarios Generation**: Generate CrowdSec scenarios
|
|
48
|
+
- **Validation**: Validate syntaxical correctness of scenarios
|
|
49
|
+
- **Linting**: Get warnings and hints to improve your scenarios
|
|
50
|
+
- **Deployment Guide**: Step-by-step deployment instructions
|
|
51
|
+
- **Docker Test Harness**: Spin up CrowdSec to test scenario behavior
|
|
52
|
+
|
|
53
|
+
## Demo
|
|
54
|
+
|
|
55
|
+
### WAF Rules Creation and testing
|
|
56
|
+
|
|
57
|
+
- [Rule creation from natural language](https://claude.ai/share/f0f246b2-6b20-4d70-a16c-c6b627ab2d80)
|
|
58
|
+
- [Rule creation from CVE reference](https://claude.ai/share/b6599407-82dd-443c-a12d-9a9825ed99df)
|
|
59
|
+
|
|
60
|
+
### Scenario Creation and testing
|
|
61
|
+
|
|
62
|
+
- [Rule creation on HTTP events](https://claude.ai/share/3658165a-5636-4a7e-8043-01e7a7517200)
|
|
63
|
+
- [Rule creation based on GeoLocation factors](https://claude.ai/share/ff154e66-3c1a-44e6-a464-b694f65bd67e)
|
|
64
|
+
|
|
65
|
+
## Prerequisites
|
|
66
|
+
|
|
67
|
+
- [uv](https://docs.astral.sh/uv/) 0.4 or newer, which provides the `uvx` runner used in the examples below.
|
|
68
|
+
- Docker with the Compose plugin (Compose v2).
|
|
69
|
+
|
|
70
|
+
## Installation
|
|
71
|
+
|
|
72
|
+
You can install the MCP using `uvx` **or** use packaged `.mcpb` file for claude code.
|
|
73
|
+
|
|
74
|
+
### Using `.mcpb` package
|
|
75
|
+
|
|
76
|
+
If you're using `claude desktop`, you can configure the MCP directly by double-clicking the `.mcpb` file that accompanies the release.
|
|
77
|
+
|
|
78
|
+
> [!IMPORTANT]
|
|
79
|
+
> On MacOS, configure `uv` path in the extension settings if `uv` isn't installed in the standard path.
|
|
80
|
+
|
|
81
|
+
### Using `uvx`
|
|
82
|
+
|
|
83
|
+
- Configure supported clients automatically with `uvx --from crowdsec-local-mcp init <client>`, where `<client>` is one of `claude-desktop`, `claude-code`, `chatgpt`, `vscode`, or `stdio`:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
uvx --from crowdsec-local-mcp init --dry-run claude-code
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Run `uvx --from crowdsec-local-mcp init --help` to see all flags and supported targets.
|
|
90
|
+
|
|
91
|
+
#### What `init` configures
|
|
92
|
+
|
|
93
|
+
The `init` helper writes the CrowdSec MCP server definition into the client’s JSON configuration:
|
|
94
|
+
|
|
95
|
+
- `claude-desktop` → `claude_desktop_config.json` in the Claude Desktop settings directory
|
|
96
|
+
- `claude-code` → invoke `claude mcp` command with needed args
|
|
97
|
+
- `chatgpt` → `config.json` in the ChatGPT Desktop settings directory
|
|
98
|
+
- `vscode` → `mcp.json` for VS Code (stable and insiders are both detected)
|
|
99
|
+
|
|
100
|
+
If the client's configuration file already exists, a `.bak` backup is created before the MCP server block is updated. When the file is missing you can either pass `--force` to create it, or point `--config-path` to a custom location. Combine `--dry-run` with these options to preview the JSON without making any changes.
|
|
101
|
+
|
|
102
|
+
By default the CLI launches the server with `uvx --from crowdsec-local-mcp crowdsec-mcp`. If neither `uvx` nor `uv` is available, it falls back to your current Python interpreter; you can override the executable with `--command` and the working directory with `--cwd`.
|
|
103
|
+
|
|
104
|
+
#### Using the `stdio` target
|
|
105
|
+
|
|
106
|
+
`stdio` does not modify any files. Instead, `init stdio` prints a ready-to-paste JSON snippet that you can drop into any stdio-compatible MCP client configuration. This is useful when you want to manually wire the server into tools that do not have built-in automation support yet.
|
|
107
|
+
|
|
108
|
+
## Troubleshooting
|
|
109
|
+
|
|
110
|
+
If you just installed the mcp extension via `.mcpb` and `uv` or `uvx` isn't in the standard path, check the extension settings to configure `uv` path.
|
|
111
|
+
|
|
112
|
+
## Logging
|
|
113
|
+
|
|
114
|
+
- The MCP server writes its log file to your operating system's temporary directory. On Linux/macOS this is typically `/tmp/crowdsec-mcp.log`; on Windows it resolves via `%TEMP%\crowdsec-mcp.log`.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
crowdsec_local_mcp/__init__.py,sha256=OrcWsdYsNOPPV4W05EsheZKz0kHcYMujo7pfRyHJ_9Q,236
|
|
2
|
+
crowdsec_local_mcp/__main__.py,sha256=Fb43e7FGfENpdWH-4Ojpz52uuT5_GCC8sInr82JOLz4,512
|
|
3
|
+
crowdsec_local_mcp/_version.py,sha256=MW1G6gkS04nDt7uvLYiQhV-JMbiJNiVvFxmhHQQjW4I,33
|
|
4
|
+
crowdsec_local_mcp/mcp_core.py,sha256=FAy5SCzuUPkXdDQhkU72f4XsQZDohIPqZ4f0bIFAriM,8278
|
|
5
|
+
crowdsec_local_mcp/mcp_scenarios.py,sha256=tWoo5N92hEhaOBmbfxccunoxcq1XmfS2ntYbfnlj6eY,37207
|
|
6
|
+
crowdsec_local_mcp/mcp_waf.py,sha256=uhWhaK-og66fXqDoQ9I2ZGAxz5L3ZhDsN6foAFIGBkU,61247
|
|
7
|
+
crowdsec_local_mcp/setup_cli.py,sha256=1RXhVtG3LgqknBkebzz1z6x_VAW51oDmYXTmQEqv6UY,11354
|
|
8
|
+
crowdsec_local_mcp/compose/scenario-test/.gitignore,sha256=hcFPKNf-CWOt-TmuTPZpafoUwaWxYNbJEEiOl9411fs,16
|
|
9
|
+
crowdsec_local_mcp/compose/scenario-test/docker-compose.yml,sha256=JEkCu9P-9Um3_IBw6xeZ8Tsj4jsdNK-Di-U9ktia1xw,580
|
|
10
|
+
crowdsec_local_mcp/compose/scenario-test/scenarios/.gitkeep,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
|
+
crowdsec_local_mcp/compose/waf-test/.gitignore,sha256=BLMbJuVqzOfzHCa3Ru2nmNXaZdbj5P_hliIeIGgptAk,111
|
|
12
|
+
crowdsec_local_mcp/compose/waf-test/docker-compose.yml,sha256=MFMd-6F6e8c2Y54Y73aL46V6R90-gJIEiSjx_joCoCw,2124
|
|
13
|
+
crowdsec_local_mcp/compose/waf-test/crowdsec/init-bouncer.sh,sha256=vI8onvy5V2ENrjwKxTvptBNkTlVhR7S2bK33lekIwWM,578
|
|
14
|
+
crowdsec_local_mcp/compose/waf-test/crowdsec/acquis.d/appsec.yaml,sha256=qg1xZmcDUSaUfX4SCaT7CcCilMWpz91fyvVGl1LUTTA,189
|
|
15
|
+
crowdsec_local_mcp/compose/waf-test/crowdsec/appsec-configs/mcp-appsec.yaml.template,sha256=9PoFbUJ6IJep7vVZ6UPs4-MDOSL320U0x4a5mB2tvp0,330
|
|
16
|
+
crowdsec_local_mcp/compose/waf-test/nginx/Dockerfile,sha256=-6QWfRkv1AhdViYVA6rbmUpd_DeISgLmcQ3cboDpsgw,2050
|
|
17
|
+
crowdsec_local_mcp/compose/waf-test/nginx/nginx.conf,sha256=mZpWFNQK9haOj8Fd-ab4GpC4Li3m0qUJLIpg5StL3pU,472
|
|
18
|
+
crowdsec_local_mcp/compose/waf-test/nginx/crowdsec/crowdsec-openresty-bouncer.conf,sha256=s_53rJk5qcSUG1XuHAh4XRUU84xw_tEcJWOYON6JsdU,630
|
|
19
|
+
crowdsec_local_mcp/compose/waf-test/nginx/site-enabled/default-site.conf,sha256=Rq4_jPkTkEL50YF4pNq2jwTjJmokgniQzKy7Y3ca964,357
|
|
20
|
+
crowdsec_local_mcp/compose/waf-test/rules/.gitkeep,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
21
|
+
crowdsec_local_mcp/compose/waf-test/rules/base-config.yaml,sha256=Xvk_SxmgKPJBWvr1cN9NIKX2A4-pTMmQo4dmIwPC7yA,1066
|
|
22
|
+
crowdsec_local_mcp/prompts/prompt-expr-helpers.txt,sha256=o6g1-fth5XP0KCDtKc85c6Cb4m2bbDvO-oNqCt5_SlA,16613
|
|
23
|
+
crowdsec_local_mcp/prompts/prompt-scenario-deploy.txt,sha256=re2lJiLzvkdlFJBmV57Lhm2zrqFeGnC6oMUtNS0Rq50,2378
|
|
24
|
+
crowdsec_local_mcp/prompts/prompt-scenario-examples.txt,sha256=H-LdAyhhjWBysquIaL90oKGNdZ20L_PqhqJVuCZL6vw,7484
|
|
25
|
+
crowdsec_local_mcp/prompts/prompt-scenario.txt,sha256=XIrgc0Hw0UAwEIuSRkWzZ9BI3qUjZsaZ-zXs_cBWwpM,7059
|
|
26
|
+
crowdsec_local_mcp/prompts/prompt-waf-deploy.txt,sha256=xFotKHMZiSVYZpjC-PItf1Ee0l3PVpof7917bybZtQg,3247
|
|
27
|
+
crowdsec_local_mcp/prompts/prompt-waf-examples.txt,sha256=e76mjm-wQa_clk61_7E6AsRgdt55m3MycY0lBkfL2Mc,11095
|
|
28
|
+
crowdsec_local_mcp/prompts/prompt-waf-pr.txt,sha256=omA1FE68YRUS3egGn6GRLzU81WPb_Iccl3rDrGz7AyM,617
|
|
29
|
+
crowdsec_local_mcp/prompts/prompt-waf-tests.txt,sha256=ZvSKsQ-B44c6BoORXwDiW33297ORuyoSeq-7EW8MaAM,4654
|
|
30
|
+
crowdsec_local_mcp/prompts/prompt-waf-top-level.txt,sha256=U9rUHw8uJDweG0g8NUYGxmAmM1OFH_pBAr9cAk8wW3c,2628
|
|
31
|
+
crowdsec_local_mcp/prompts/prompt-waf.txt,sha256=VxTc7QM3yYtZoj5nYUMAIQJHyeniPstU-cPrHdyq7bs,10920
|
|
32
|
+
crowdsec_local_mcp/yaml-schemas/appsec_rules_schema.yaml,sha256=zXu-ikNT-bZNGWdOi5hOqZjpjM_dOnKIdMTtwng8lOM,10639
|
|
33
|
+
crowdsec_local_mcp/yaml-schemas/scenario_schema.yaml,sha256=k0NYxyOUicmMip3Req3zE2CM7tyy8ARcxHlYkSSXbSI,24078
|
|
34
|
+
crowdsec_local_mcp-0.8.0.post1.dev0.dist-info/licenses/LICENSE,sha256=3UN9Hca_TnpUOAecGoPKh1fjI5Ol-rSoP8epbBuERE4,1065
|
|
35
|
+
crowdsec_local_mcp-0.8.0.post1.dev0.dist-info/METADATA,sha256=N5tWmFfuFBbnq0D97GlAU4ipFnFquY5RBtz5vDoTJAE,5234
|
|
36
|
+
crowdsec_local_mcp-0.8.0.post1.dev0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
37
|
+
crowdsec_local_mcp-0.8.0.post1.dev0.dist-info/entry_points.txt,sha256=EFTrsplHoT4x-GRrip0jxSQmH7NKBb5w5nX0PphGxTY,106
|
|
38
|
+
crowdsec_local_mcp-0.8.0.post1.dev0.dist-info/top_level.txt,sha256=MC0OAZ7qK5gG78swUkedPT3pfekze1NL5cO90s90CYM,19
|
|
39
|
+
crowdsec_local_mcp-0.8.0.post1.dev0.dist-info/RECORD,,
|