crowdsec-local-mcp 0.1.0__py3-none-any.whl → 0.7.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 -1
- 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 +3 -2
- crowdsec_local_mcp/mcp_core.py +114 -19
- crowdsec_local_mcp/mcp_scenarios.py +579 -23
- crowdsec_local_mcp/mcp_waf.py +567 -337
- 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-tests.txt +101 -0
- crowdsec_local_mcp/prompts/prompt-waf-top-level.txt +31 -0
- crowdsec_local_mcp/prompts/prompt-waf.txt +0 -26
- crowdsec_local_mcp/setup_cli.py +375 -0
- crowdsec_local_mcp-0.7.0.post1.dev0.dist-info/METADATA +114 -0
- crowdsec_local_mcp-0.7.0.post1.dev0.dist-info/RECORD +38 -0
- {crowdsec_local_mcp-0.1.0.dist-info → crowdsec_local_mcp-0.7.0.post1.dev0.dist-info}/entry_points.txt +1 -0
- crowdsec_local_mcp-0.1.0.dist-info/METADATA +0 -93
- crowdsec_local_mcp-0.1.0.dist-info/RECORD +0 -30
- {crowdsec_local_mcp-0.1.0.dist-info → crowdsec_local_mcp-0.7.0.post1.dev0.dist-info}/WHEEL +0 -0
- {crowdsec_local_mcp-0.1.0.dist-info → crowdsec_local_mcp-0.7.0.post1.dev0.dist-info}/licenses/LICENSE +0 -0
- {crowdsec_local_mcp-0.1.0.dist-info → crowdsec_local_mcp-0.7.0.post1.dev0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,101 @@
|
|
|
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
|
+
|
|
12
|
+
2. **Adapted Nuclei Template**
|
|
13
|
+
- Preserve the original request structure that exercises the vulnerability.
|
|
14
|
+
- Normalise metadata so the template identifies itself as a test:
|
|
15
|
+
- `info.name` matches the CVE identifier.
|
|
16
|
+
- `info.author` is `crowdsec`.
|
|
17
|
+
- `info.severity` is `info`.
|
|
18
|
+
- `info.description` briefly states it is for testing (e.g. `CVE-2020-17496 testing`).
|
|
19
|
+
- `info.tags` includes `appsec-testing`.
|
|
20
|
+
- Keep payloads, raw requests, and helper fields that are required for the exploit to function.
|
|
21
|
+
- 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.
|
|
22
|
+
- When the original template lacks an explicit matcher, add a `matchers` block with a single status matcher for `403`.
|
|
23
|
+
- If the template requires `cookie-reuse` or other execution flags, keep them intact.
|
|
24
|
+
|
|
25
|
+
3. **General Expectations**
|
|
26
|
+
- Do not attempt to regenerate the detection rule—only produce the two testing artifacts.
|
|
27
|
+
- Respect YAML indentation (two spaces) and quote strings that contain special characters.
|
|
28
|
+
- Mirror the case and structure of URIs, headers, and payloads from the input template.
|
|
29
|
+
- Provide helpful inline comments only when strictly necessary to explain intentional failures.
|
|
30
|
+
|
|
31
|
+
## Output Format
|
|
32
|
+
|
|
33
|
+
Always return exactly two sections separated by delimiters:
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
===TEST_CONFIG===
|
|
37
|
+
<config.yaml content>
|
|
38
|
+
|
|
39
|
+
===TEST_NUCLEI===
|
|
40
|
+
<adapted nuclei template content>
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
No additional sections or commentary should be emitted.
|
|
44
|
+
|
|
45
|
+
## Example Input
|
|
46
|
+
|
|
47
|
+
```yaml
|
|
48
|
+
id: CVE-2020-17496
|
|
49
|
+
|
|
50
|
+
info:
|
|
51
|
+
name: vBulletin 5.5.4 - 5.6.2- Remote Command Execution
|
|
52
|
+
author: pussycat0x
|
|
53
|
+
severity: critical
|
|
54
|
+
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.'
|
|
55
|
+
|
|
56
|
+
http:
|
|
57
|
+
- raw:
|
|
58
|
+
- |
|
|
59
|
+
POST /ajax/render/widget_tabbedcontainer_tab_panel HTTP/1.1
|
|
60
|
+
Host: {{Hostname}}
|
|
61
|
+
Content-Type: application/x-www-form-urlencoded
|
|
62
|
+
|
|
63
|
+
subWidgets[0][template]=widget_php&subWidgets[0][config][code]=echo shell_exec('cat ../../../../../../../../../../../../etc/passwd'); exit;
|
|
64
|
+
|
|
65
|
+
matchers-condition: and
|
|
66
|
+
matchers:
|
|
67
|
+
- type: status
|
|
68
|
+
status:
|
|
69
|
+
- 200
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Example Output
|
|
73
|
+
|
|
74
|
+
===TEST_CONFIG===
|
|
75
|
+
appsec-rules:
|
|
76
|
+
- ./appsec-rules/crowdsecurity/base-config.yaml
|
|
77
|
+
- ./appsec-rules/crowdsecurity/vpatch-CVE-2020-17496.yaml
|
|
78
|
+
nuclei_template: CVE-2020-17496.yaml
|
|
79
|
+
|
|
80
|
+
===TEST_NUCLEI===
|
|
81
|
+
id: CVE-2020-17496
|
|
82
|
+
info:
|
|
83
|
+
name: CVE-2020-17496
|
|
84
|
+
author: crowdsec
|
|
85
|
+
severity: info
|
|
86
|
+
description: CVE-2020-17496 testing
|
|
87
|
+
tags: appsec-testing
|
|
88
|
+
http:
|
|
89
|
+
- raw:
|
|
90
|
+
- |
|
|
91
|
+
POST /ajax/render/widget_tabbedcontainer_tab_panel HTTP/1.1
|
|
92
|
+
Host: {{Hostname}}
|
|
93
|
+
Content-Type: application/x-www-form-urlencoded
|
|
94
|
+
|
|
95
|
+
subWidgets[0][template]=widget_php&subWidgets[0][config][code]=echo shell_exec('cat ../../../../../../../../../../../../etc/passwd'); exit;
|
|
96
|
+
|
|
97
|
+
cookie-reuse: true
|
|
98
|
+
matchers:
|
|
99
|
+
- type: status
|
|
100
|
+
status:
|
|
101
|
+
- 403
|
|
@@ -0,0 +1,31 @@
|
|
|
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, you can suggest the user to open a PR to https://github.com/crowdsecurity/hub/
|
|
30
|
+
- Help the user to deploy the rule on his setup:
|
|
31
|
+
- 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
|
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
import platform
|
|
5
|
+
import shlex
|
|
6
|
+
import shutil
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from collections.abc import Iterable
|
|
12
|
+
|
|
13
|
+
SERVER_KEY = "crowdsec-local-mcp"
|
|
14
|
+
SERVER_LABEL = "CrowdSecMCP"
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class CLIArgs:
|
|
18
|
+
target: str
|
|
19
|
+
config_path: Path | None
|
|
20
|
+
dry_run: bool
|
|
21
|
+
force: bool
|
|
22
|
+
command_override: str | None
|
|
23
|
+
cwd_override: Path | None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def main(argv: Iterable[str] | None = None) -> None:
|
|
27
|
+
args = _parse_args(argv)
|
|
28
|
+
command, cmd_args = _resolve_runner(args.command_override)
|
|
29
|
+
server_payload = {
|
|
30
|
+
"command": command,
|
|
31
|
+
"args": cmd_args,
|
|
32
|
+
"metadata": {
|
|
33
|
+
"label": SERVER_LABEL,
|
|
34
|
+
"description": "CrowdSec local MCP server",
|
|
35
|
+
},
|
|
36
|
+
}
|
|
37
|
+
if args.cwd_override:
|
|
38
|
+
server_payload["cwd"] = str(args.cwd_override)
|
|
39
|
+
|
|
40
|
+
if args.target == "stdio":
|
|
41
|
+
_print_stdio(server_payload)
|
|
42
|
+
return
|
|
43
|
+
|
|
44
|
+
if args.target == "claude-desktop":
|
|
45
|
+
_configure_claude(args, server_payload)
|
|
46
|
+
elif args.target == "claude-code":
|
|
47
|
+
_configure_claude_code(args, server_payload)
|
|
48
|
+
elif args.target == "chatgpt":
|
|
49
|
+
_configure_chatgpt(args, server_payload)
|
|
50
|
+
elif args.target == "vscode":
|
|
51
|
+
_configure_vscode(args, server_payload)
|
|
52
|
+
else:
|
|
53
|
+
raise ValueError(f"Unsupported target '{args.target}'")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _parse_args(argv: Iterable[str] | None) -> CLIArgs:
|
|
57
|
+
parser = argparse.ArgumentParser(
|
|
58
|
+
prog="init",
|
|
59
|
+
description=(
|
|
60
|
+
"Initialize CrowdSec MCP integration for supported clients "
|
|
61
|
+
"(Claude Desktop, Claude Code, ChatGPT Desktop, Visual Studio Code, or stdio)."
|
|
62
|
+
),
|
|
63
|
+
)
|
|
64
|
+
parser.add_argument(
|
|
65
|
+
"target",
|
|
66
|
+
choices=("claude-desktop", "claude-code", "chatgpt", "vscode", "stdio"),
|
|
67
|
+
help="Client to configure.",
|
|
68
|
+
)
|
|
69
|
+
parser.add_argument(
|
|
70
|
+
"--config-path",
|
|
71
|
+
type=Path,
|
|
72
|
+
help="Override the configuration file path to update.",
|
|
73
|
+
)
|
|
74
|
+
parser.add_argument(
|
|
75
|
+
"--dry-run",
|
|
76
|
+
action="store_true",
|
|
77
|
+
help="Print the resulting configuration instead of writing it.",
|
|
78
|
+
)
|
|
79
|
+
parser.add_argument(
|
|
80
|
+
"--force",
|
|
81
|
+
action="store_true",
|
|
82
|
+
help="Create configuration even if the file is missing.",
|
|
83
|
+
)
|
|
84
|
+
parser.add_argument(
|
|
85
|
+
"--command",
|
|
86
|
+
dest="command_override",
|
|
87
|
+
help=(
|
|
88
|
+
"Override the command used to launch the MCP server. "
|
|
89
|
+
"Defaults to 'uvx run --from crowdsec-local-mcp crowdsec-mcp' or "
|
|
90
|
+
"falls back to the current Python interpreter."
|
|
91
|
+
),
|
|
92
|
+
)
|
|
93
|
+
parser.add_argument(
|
|
94
|
+
"--cwd",
|
|
95
|
+
dest="cwd_override",
|
|
96
|
+
type=Path,
|
|
97
|
+
help="Set the working directory used when launching the server.",
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
parsed = parser.parse_args(argv)
|
|
101
|
+
return CLIArgs(
|
|
102
|
+
target=parsed.target,
|
|
103
|
+
config_path=parsed.config_path,
|
|
104
|
+
dry_run=parsed.dry_run,
|
|
105
|
+
force=parsed.force,
|
|
106
|
+
command_override=parsed.command_override,
|
|
107
|
+
cwd_override=parsed.cwd_override,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _resolve_runner(command_override: str | None) -> tuple[str, list[str]]:
|
|
112
|
+
if command_override:
|
|
113
|
+
command_parts = command_override.strip().split()
|
|
114
|
+
if not command_parts:
|
|
115
|
+
raise ValueError("Command override cannot be empty.")
|
|
116
|
+
return command_parts[0], command_parts[1:]
|
|
117
|
+
|
|
118
|
+
for executable in ("uvx", "uv"):
|
|
119
|
+
resolved = shutil.which(executable)
|
|
120
|
+
if resolved:
|
|
121
|
+
return resolved, [
|
|
122
|
+
"--from",
|
|
123
|
+
"crowdsec-local-mcp",
|
|
124
|
+
"crowdsec-mcp",
|
|
125
|
+
]
|
|
126
|
+
|
|
127
|
+
python_executable = sys.executable
|
|
128
|
+
if not python_executable:
|
|
129
|
+
raise RuntimeError(
|
|
130
|
+
"Unable to determine a Python interpreter to launch the MCP server."
|
|
131
|
+
)
|
|
132
|
+
return python_executable, ["-m", "crowdsec_local_mcp"]
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _configure_claude(args: CLIArgs, server_payload: dict[str, object]) -> None:
|
|
136
|
+
config_path = _resolve_path(args.config_path, _claude_candidates())
|
|
137
|
+
_write_mcp_config(
|
|
138
|
+
config_path,
|
|
139
|
+
server_payload,
|
|
140
|
+
args,
|
|
141
|
+
client_name="Claude Desktop",
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
|
|
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:
|
|
191
|
+
config_path = _resolve_path(args.config_path, _chatgpt_candidates())
|
|
192
|
+
_write_mcp_config(
|
|
193
|
+
config_path,
|
|
194
|
+
server_payload,
|
|
195
|
+
args,
|
|
196
|
+
client_name="ChatGPT Desktop",
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _configure_vscode(args: CLIArgs, server_payload: dict[str, object]) -> None:
|
|
201
|
+
config_path = _resolve_path(args.config_path, _vscode_candidates())
|
|
202
|
+
vscode_payload: dict[str, object] = {
|
|
203
|
+
"command": server_payload["command"],
|
|
204
|
+
"args": server_payload["args"],
|
|
205
|
+
}
|
|
206
|
+
if "cwd" in server_payload:
|
|
207
|
+
vscode_payload["cwd"] = server_payload["cwd"]
|
|
208
|
+
metadata = server_payload.get("metadata")
|
|
209
|
+
if isinstance(metadata, dict):
|
|
210
|
+
vscode_payload["metadata"] = metadata
|
|
211
|
+
_write_mcp_config(
|
|
212
|
+
config_path,
|
|
213
|
+
vscode_payload,
|
|
214
|
+
args,
|
|
215
|
+
client_name="Visual Studio Code",
|
|
216
|
+
servers_key="servers",
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _write_mcp_config(
|
|
221
|
+
config_path: Path,
|
|
222
|
+
server_payload: dict[str, object],
|
|
223
|
+
args: CLIArgs,
|
|
224
|
+
*,
|
|
225
|
+
client_name: str,
|
|
226
|
+
servers_key: str = "mcpServers",
|
|
227
|
+
) -> None:
|
|
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
|
|
238
|
+
if not existed and not (args.force or args.dry_run):
|
|
239
|
+
raise FileNotFoundError(
|
|
240
|
+
f"{config_path} does not exist. Re-run with --force to create it "
|
|
241
|
+
"or provide --config-path pointing to an existing configuration file."
|
|
242
|
+
)
|
|
243
|
+
server_collection = config.setdefault(servers_key, {})
|
|
244
|
+
if not isinstance(server_collection, dict):
|
|
245
|
+
raise ValueError(f"Expected '{servers_key}' to be an object in {config_path}")
|
|
246
|
+
|
|
247
|
+
server_collection[SERVER_KEY] = server_payload
|
|
248
|
+
|
|
249
|
+
if args.dry_run:
|
|
250
|
+
print(json.dumps(config, indent=2))
|
|
251
|
+
return
|
|
252
|
+
|
|
253
|
+
_ensure_directory(config_path)
|
|
254
|
+
_backup_file_if_exists(config_path)
|
|
255
|
+
config_path.write_text(json.dumps(config, indent=2) + "\n", encoding="utf-8")
|
|
256
|
+
print(f"Configured {client_name} at {config_path}")
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def _print_stdio(server_payload: dict[str, object]) -> None:
|
|
260
|
+
snippet = {
|
|
261
|
+
"server": SERVER_KEY,
|
|
262
|
+
"command": server_payload["command"],
|
|
263
|
+
"args": server_payload["args"],
|
|
264
|
+
}
|
|
265
|
+
cwd = server_payload.get("cwd")
|
|
266
|
+
if cwd is not None:
|
|
267
|
+
snippet["cwd"] = cwd
|
|
268
|
+
|
|
269
|
+
snippet_json = json.dumps(snippet, indent=2)
|
|
270
|
+
print(
|
|
271
|
+
"Use the following configuration with stdio-compatible MCP clients:\n"
|
|
272
|
+
f"{snippet_json}"
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
|
|
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]:
|
|
282
|
+
if not path.exists():
|
|
283
|
+
if allow_missing:
|
|
284
|
+
return {}, False
|
|
285
|
+
raise FileNotFoundError(f"Configuration file {path} does not exist.")
|
|
286
|
+
|
|
287
|
+
content = path.read_text(encoding="utf-8")
|
|
288
|
+
if not content.strip():
|
|
289
|
+
return {}, True
|
|
290
|
+
|
|
291
|
+
try:
|
|
292
|
+
return json.loads(content), True
|
|
293
|
+
except json.JSONDecodeError as exc:
|
|
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
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def _resolve_path(explicit: Path | None, candidates: list[Path]) -> Path:
|
|
304
|
+
if explicit:
|
|
305
|
+
return explicit.expanduser()
|
|
306
|
+
|
|
307
|
+
expanded = [candidate.expanduser() for candidate in candidates]
|
|
308
|
+
for candidate in expanded:
|
|
309
|
+
if candidate.exists():
|
|
310
|
+
return candidate
|
|
311
|
+
|
|
312
|
+
if expanded:
|
|
313
|
+
return expanded[0]
|
|
314
|
+
|
|
315
|
+
raise ValueError("No configuration path candidates were provided.")
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def _ensure_directory(path: Path) -> None:
|
|
319
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def _backup_file_if_exists(path: Path) -> None:
|
|
323
|
+
if not path.exists():
|
|
324
|
+
return
|
|
325
|
+
backup_path = path.with_suffix(path.suffix + ".bak")
|
|
326
|
+
shutil.copy2(path, backup_path)
|
|
327
|
+
print(f"Existing configuration backed up to {backup_path}")
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def _claude_candidates() -> list[Path]:
|
|
331
|
+
system = platform.system()
|
|
332
|
+
if system == "Darwin":
|
|
333
|
+
return [
|
|
334
|
+
Path.home()
|
|
335
|
+
/ "Library"
|
|
336
|
+
/ "Application Support"
|
|
337
|
+
/ "Claude"
|
|
338
|
+
/ "claude_desktop_config.json"
|
|
339
|
+
]
|
|
340
|
+
if system == "Windows":
|
|
341
|
+
base = Path(os.environ.get("APPDATA", Path.home()))
|
|
342
|
+
return [base / "Claude" / "claude_desktop_config.json"]
|
|
343
|
+
return [Path.home() / ".config" / "Claude" / "claude_desktop_config.json"]
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def _chatgpt_candidates() -> list[Path]:
|
|
347
|
+
system = platform.system()
|
|
348
|
+
if system == "Darwin":
|
|
349
|
+
return [
|
|
350
|
+
Path.home()
|
|
351
|
+
/ "Library"
|
|
352
|
+
/ "Application Support"
|
|
353
|
+
/ "ChatGPT"
|
|
354
|
+
/ "config.json"
|
|
355
|
+
]
|
|
356
|
+
if system == "Windows":
|
|
357
|
+
base = Path(os.environ.get("APPDATA", Path.home()))
|
|
358
|
+
return [base / "ChatGPT" / "config.json"]
|
|
359
|
+
return [Path.home() / ".config" / "ChatGPT" / "config.json"]
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def _vscode_candidates() -> list[Path]:
|
|
363
|
+
system = platform.system()
|
|
364
|
+
if system == "Windows":
|
|
365
|
+
base = Path(os.environ.get("APPDATA", Path.home()))
|
|
366
|
+
return [base / "Code" / "User" / "mcp.json", base / "Code - Insiders" / "User" / "mcp.json"]
|
|
367
|
+
if system == "Darwin":
|
|
368
|
+
base = Path.home() / "Library" / "Application Support"
|
|
369
|
+
return [base / "Code" / "User" / "mcp.json", base / "Code - Insiders" / "User" / "mcp.json"]
|
|
370
|
+
# Linux and others
|
|
371
|
+
base = Path.home() / ".config"
|
|
372
|
+
return [base / "Code" / "User" / "mcp.json", base / "Code - Insiders" / "User" / "mcp.json"]
|
|
373
|
+
|
|
374
|
+
if __name__ == "__main__":
|
|
375
|
+
main()
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: crowdsec-local-mcp
|
|
3
|
+
Version: 0.7.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`.
|