kmcp 0.1.5.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.
Files changed (84) hide show
  1. kali_mcp/__init__.py +3 -0
  2. kali_mcp/__main__.py +28 -0
  3. kali_mcp/_version.py +24 -0
  4. kali_mcp/cli.py +169 -0
  5. kali_mcp/config.py +95 -0
  6. kali_mcp/db/__init__.py +1 -0
  7. kali_mcp/db/manager.py +248 -0
  8. kali_mcp/execution.py +439 -0
  9. kali_mcp/logging_utils.py +74 -0
  10. kali_mcp/models.py +51 -0
  11. kali_mcp/monitor/__init__.py +105 -0
  12. kali_mcp/monitor/sessions.py +70 -0
  13. kali_mcp/monitor/sse_client.py +87 -0
  14. kali_mcp/monitor/tui.py +364 -0
  15. kali_mcp/notifications.py +130 -0
  16. kali_mcp/prompts.py +184 -0
  17. kali_mcp/registry.py +33 -0
  18. kali_mcp/resources.py +263 -0
  19. kali_mcp/responses.py +39 -0
  20. kali_mcp/security.py +15 -0
  21. kali_mcp/server.py +535 -0
  22. kali_mcp/tools/__init__.py +125 -0
  23. kali_mcp/tools/amass.py +83 -0
  24. kali_mcp/tools/arjun.py +86 -0
  25. kali_mcp/tools/base.py +100 -0
  26. kali_mcp/tools/bettercap.py +99 -0
  27. kali_mcp/tools/bloodhound.py +68 -0
  28. kali_mcp/tools/commix.py +73 -0
  29. kali_mcp/tools/crackmapexec.py +100 -0
  30. kali_mcp/tools/dirb.py +75 -0
  31. kali_mcp/tools/dursgo.py +156 -0
  32. kali_mcp/tools/enum4linux.py +59 -0
  33. kali_mcp/tools/farsight.py +127 -0
  34. kali_mcp/tools/ffuf.py +85 -0
  35. kali_mcp/tools/file_download.py +119 -0
  36. kali_mcp/tools/file_read.py +144 -0
  37. kali_mcp/tools/file_write.py +141 -0
  38. kali_mcp/tools/flowlyt.py +88 -0
  39. kali_mcp/tools/generic_command.py +95 -0
  40. kali_mcp/tools/gobuster.py +84 -0
  41. kali_mcp/tools/hashcat.py +101 -0
  42. kali_mcp/tools/httpx.py +85 -0
  43. kali_mcp/tools/hydra.py +99 -0
  44. kali_mcp/tools/john.py +100 -0
  45. kali_mcp/tools/katana.py +81 -0
  46. kali_mcp/tools/masscan.py +85 -0
  47. kali_mcp/tools/medusa.py +102 -0
  48. kali_mcp/tools/msfconsole.py +59 -0
  49. kali_mcp/tools/naabu.py +100 -0
  50. kali_mcp/tools/netexec.py +86 -0
  51. kali_mcp/tools/nmap.py +94 -0
  52. kali_mcp/tools/normalization.py +18 -0
  53. kali_mcp/tools/nuclei.py +87 -0
  54. kali_mcp/tools/parallel_execute.py +246 -0
  55. kali_mcp/tools/persist.py +436 -0
  56. kali_mcp/tools/playbook_ad.py +99 -0
  57. kali_mcp/tools/playbook_engine.py +142 -0
  58. kali_mcp/tools/playbook_engine_tool.py +72 -0
  59. kali_mcp/tools/playbook_recon.py +115 -0
  60. kali_mcp/tools/playbook_tools.py +125 -0
  61. kali_mcp/tools/playbook_webapp.py +105 -0
  62. kali_mcp/tools/python_command.py +100 -0
  63. kali_mcp/tools/responder.py +85 -0
  64. kali_mcp/tools/searchsploit.py +123 -0
  65. kali_mcp/tools/spiderfoot.py +75 -0
  66. kali_mcp/tools/sqlmap.py +73 -0
  67. kali_mcp/tools/subfinder.py +69 -0
  68. kali_mcp/tools/theharvester.py +80 -0
  69. kali_mcp/tools/vault_tool.py +489 -0
  70. kali_mcp/tools/wfuzz.py +87 -0
  71. kali_mcp/tools/whatweb.py +82 -0
  72. kali_mcp/tools/wpscan.py +76 -0
  73. kali_mcp/tools/zap.py +79 -0
  74. kali_mcp/tools/zighound.py +139 -0
  75. kali_mcp/tools/zizmor.py +98 -0
  76. kali_mcp/utils/__init__.py +0 -0
  77. kali_mcp/utils/process.py +36 -0
  78. kali_mcp/utils/vault.py +471 -0
  79. kali_mcp/validation.py +82 -0
  80. kmcp-0.1.5.dev0.dist-info/METADATA +251 -0
  81. kmcp-0.1.5.dev0.dist-info/RECORD +84 -0
  82. kmcp-0.1.5.dev0.dist-info/WHEEL +5 -0
  83. kmcp-0.1.5.dev0.dist-info/entry_points.txt +4 -0
  84. kmcp-0.1.5.dev0.dist-info/top_level.txt +1 -0
kali_mcp/prompts.py ADDED
@@ -0,0 +1,184 @@
1
+ """MCP prompts — guided workflow templates for clients.
2
+
3
+ Each prompt renders a short instruction + tool-suggestions message so an
4
+ MCP client can drive a standard pentest workflow step by step.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import logging
10
+
11
+ logger = logging.getLogger("kali_mcp.prompts")
12
+
13
+
14
+ def _text(msg: str) -> dict:
15
+ """Build an assistant/user content block."""
16
+ return {"type": "text", "text": msg}
17
+
18
+
19
+ def _user(msg: str) -> dict:
20
+ return {"role": "user", "content": _text(msg)}
21
+
22
+
23
+ def _assistant(msg: str) -> dict:
24
+ return {"role": "assistant", "content": _text(msg)}
25
+
26
+
27
+ def _recon(target: str, depth: str = "standard", scope: str = "") -> list[dict]:
28
+ msgs = [
29
+ _user(
30
+ "You are an expert penetration tester. Begin external reconnaissance "
31
+ f"against the target **{target}**"
32
+ + (f" with scope notes: {scope}" if scope else "")
33
+ + ". Run one phase at a time and pause for my approval between phases."
34
+ ),
35
+ _assistant(
36
+ "Phase 1 — asset discovery. I will resolve the target, identify "
37
+ "infrastructure, and map services using the `dns_recon`, `whois_lookup`, "
38
+ "and `ping_sweep` tools, then `nmap_scan` for open ports."
39
+ + (
40
+ " Given the 'deep' depth, I will also enumerate subdomains and "
41
+ "technology fingerprints."
42
+ if depth == "deep"
43
+ else " I will keep verbosity low."
44
+ )
45
+ + "\nShall I start?"
46
+ ),
47
+ ]
48
+ return msgs
49
+
50
+
51
+ def _webapp(url: str, auth: str = "no") -> list[dict]:
52
+ msgs = [
53
+ _user(
54
+ f"Perform a web application security assessment of **{url}** (authenticated: {auth})."
55
+ ),
56
+ _assistant(
57
+ "I will follow this checklist, running one item at a time and reporting "
58
+ "evidence for each:\n"
59
+ "1. `webapp_crawl` — enumerate endpoints, parameters, and tech stack\n"
60
+ "2. `webapp_scan` — active vulnerability scan\n"
61
+ "3. Manual review of findings before any exploitation\n"
62
+ "4. Retest and summary report\n"
63
+ f"Authenticated assessment: {auth}. Starting with the crawl."
64
+ ),
65
+ ]
66
+ return msgs
67
+
68
+
69
+ def _ad(domain: str, credentials: str = "") -> list[dict]:
70
+ msgs = [
71
+ _user(
72
+ "Assess the Active Directory environment for domain "
73
+ f"**{domain}**" + (f" using credentials {credentials}" if credentials else "") + "."
74
+ ),
75
+ _assistant(
76
+ "I will enumerate in this order:\n"
77
+ "1. Domain and trust discovery\n"
78
+ "2. User/group/computer enumeration\n"
79
+ "3. ACL and delegation analysis\n"
80
+ "4. Kerberos attack surface (AS-REP roast, Kerberoasting checks)\n"
81
+ "5. Report with remediation\n"
82
+ "Beginning with domain discovery."
83
+ ),
84
+ ]
85
+ return msgs
86
+
87
+
88
+ def _osint(subject: str, sources: str = "default") -> list[dict]:
89
+ msgs = [
90
+ _user(
91
+ f"Gather open-source intelligence on **{subject}** using the '{sources}' source set."
92
+ ),
93
+ _assistant(
94
+ "OSINT collection plan:\n"
95
+ "1. Web/DNS footprint — `whois_lookup`, `dns_recon`, certificate search\n"
96
+ "2. Data breach / credential exposure checks (with the target's consent)\n"
97
+ "3. Social footprint review\n"
98
+ "4. Organized intelligence report with sources\n"
99
+ f"Sources: {sources}. Starting with the DNS/web footprint."
100
+ ),
101
+ ]
102
+ return msgs
103
+
104
+
105
+ def _quick_scan(target: str, ports: str = "top1000", profile: str = "fast") -> list[dict]:
106
+ msgs = [
107
+ _user(
108
+ f"Run a {profile} nmap scan of **{target}** on ports **{ports}** "
109
+ "and summarize findings."
110
+ ),
111
+ _assistant(
112
+ f"Using `nmap_scan` with target={target}, ports={ports}, profile={profile}. "
113
+ "I'll run the scan and give you an open-port/service summary with "
114
+ "next-step recommendations."
115
+ ),
116
+ ]
117
+ return msgs
118
+
119
+
120
+ def _report(engagement: str, findings: str = "") -> list[dict]:
121
+ msgs = [
122
+ _user(
123
+ f"Generate a professional penetration test report for **{engagement}**"
124
+ + (f" from these findings: {findings}" if findings else ".")
125
+ ),
126
+ _assistant(
127
+ "Report structure:\n"
128
+ "1. Executive summary (business impact)\n"
129
+ "2. Scope and methodology\n"
130
+ "3. Findings with severity, evidence, and reproduction steps\n"
131
+ "4. Remediation plan\n"
132
+ "5. Appendix (raw data)\n"
133
+ "I'll ask for each finding's evidence as I draft."
134
+ ),
135
+ ]
136
+ return msgs
137
+
138
+
139
+ def register_prompts(mcp) -> None:
140
+ """Register all workflow prompt templates on the FastMCP server."""
141
+
142
+ @mcp.prompt(
143
+ name="recon_workflow",
144
+ description="Guided external reconnaissance workflow for a target host/domain",
145
+ )
146
+ def recon_workflow(target: str, depth: str = "standard", scope: str = "") -> list[dict]:
147
+ return _recon(target, depth, scope)
148
+
149
+ @mcp.prompt(
150
+ name="webapp_assessment",
151
+ description="Guided web application security assessment workflow",
152
+ )
153
+ def webapp_assessment(url: str, auth: str = "no") -> list[dict]:
154
+ return _webapp(url, auth)
155
+
156
+ @mcp.prompt(
157
+ name="ad_assessment",
158
+ description="Guided Active Directory security assessment workflow",
159
+ )
160
+ def ad_assessment(domain: str, credentials: str = "") -> list[dict]:
161
+ return _ad(domain, credentials)
162
+
163
+ @mcp.prompt(
164
+ name="osint_harvest",
165
+ description="Guided OSINT gathering workflow for a subject",
166
+ )
167
+ def osint_harvest(subject: str, sources: str = "default") -> list[dict]:
168
+ return _osint(subject, sources)
169
+
170
+ @mcp.prompt(
171
+ name="quick_scan",
172
+ description="Quick nmap scan with findings summary",
173
+ )
174
+ def quick_scan(target: str, ports: str = "top1000", profile: str = "fast") -> list[dict]:
175
+ return _quick_scan(target, ports, profile)
176
+
177
+ @mcp.prompt(
178
+ name="report_template",
179
+ description="Professional penetration test report generation",
180
+ )
181
+ def report_template(engagement: str, findings: str = "") -> list[dict]:
182
+ return _report(engagement, findings)
183
+
184
+ logger.info("Registered %d prompt templates", 6)
kali_mcp/registry.py ADDED
@@ -0,0 +1,33 @@
1
+ """Tool registry — maps tool names to BaseTool instances."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+
7
+ from kali_mcp.tools.base import BaseTool
8
+
9
+ logger = logging.getLogger("kali_mcp.registry")
10
+
11
+
12
+ class ToolRegistry:
13
+ def __init__(self) -> None:
14
+ self._tools: dict[str, BaseTool] = {}
15
+
16
+ def register(self, tool: BaseTool) -> None:
17
+ """Register a tool instance. Overwrites if name already exists."""
18
+ if tool.name in self._tools:
19
+ logger.warning("Overwriting tool: %s", tool.name)
20
+ self._tools[tool.name] = tool
21
+ logger.info("Registered tool: %s", tool.name)
22
+
23
+ def get(self, name: str) -> BaseTool | None:
24
+ return self._tools.get(name)
25
+
26
+ def list_all(self) -> list[BaseTool]:
27
+ return list(self._tools.values())
28
+
29
+ def tool_names(self) -> list[str]:
30
+ return list(self._tools.keys())
31
+
32
+
33
+ registry = ToolRegistry()
kali_mcp/resources.py ADDED
@@ -0,0 +1,263 @@
1
+ """MCP resources — server metadata exposed to clients.
2
+
3
+ Resources (URIs, not tools) give the MCP client a browseable view of:
4
+ - live server configuration
5
+ - registered tool inventory
6
+ - health status
7
+ - execution/server logs (JSON lines)
8
+ - generated artifacts
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ import logging
15
+ from pathlib import Path
16
+ from typing import Any
17
+
18
+ from kali_mcp.config import config
19
+
20
+ logger = logging.getLogger("kali_mcp.resources")
21
+
22
+ RESOURCE_NAMESPACE = "kali"
23
+
24
+ # Templates registered for template listing (uriTemplate → {name, description})
25
+ TEMPLATES: dict[str, dict[str, str]] = {
26
+ "kali://artifacts/{artifact}": {
27
+ "name": "kali_artifact",
28
+ "description": "Read a generated artifact from the artifact directory",
29
+ },
30
+ }
31
+
32
+
33
+ def _config_resource() -> str:
34
+ """Live server configuration as JSON."""
35
+ cfg = {
36
+ "server_name": config.server_name,
37
+ "server_version": config.server_version,
38
+ "host": config.host,
39
+ "port": config.port,
40
+ "transport": config.transport,
41
+ "default_timeout": config.default_timeout,
42
+ "max_timeout": config.max_timeout,
43
+ "max_concurrent": config.max_concurrent,
44
+ "log_dir": str(config.log_dir),
45
+ "artifact_dir": str(config.artifact_dir),
46
+ "debug": config.debug,
47
+ "rate_limit_per_minute": config.rate_limit_per_minute,
48
+ "auth_enabled": bool(config.api_token),
49
+ }
50
+ return json.dumps(cfg, indent=2)
51
+
52
+
53
+ def _tools_resource() -> str:
54
+ """Registered tool inventory as JSON."""
55
+ from kali_mcp.registry import registry
56
+
57
+ tools = []
58
+ for tool in registry.list_all():
59
+ tools.append(
60
+ {
61
+ "name": tool.name,
62
+ "description": tool.description,
63
+ "schema": tool.input_schema(),
64
+ }
65
+ )
66
+ return json.dumps({"count": len(tools), "tools": tools}, indent=2)
67
+
68
+
69
+ def _make_health_resource(health):
70
+ """Build a resource reader returning live health status as JSON."""
71
+
72
+ def _read() -> str:
73
+ return json.dumps(health.get_status(), indent=2)
74
+
75
+ return _read
76
+
77
+
78
+ def _tail_file(path: Path, max_lines: int = 2000) -> str:
79
+ """Return the tail of a JSON-lines log file as pretty JSON."""
80
+ if not path.exists():
81
+ return json.dumps({"error": f"no log file at {path}"}, indent=2)
82
+ lines: list = []
83
+ try:
84
+ with open(path, "r", encoding="utf-8", errors="replace") as fh:
85
+ for line in fh:
86
+ line = line.strip()
87
+ if not line:
88
+ continue
89
+ try:
90
+ lines.append(json.loads(line))
91
+ except json.JSONDecodeError:
92
+ lines.append({"raw": line})
93
+ except OSError as exc:
94
+ return json.dumps({"error": str(exc)}, indent=2)
95
+ return json.dumps({"count": len(lines), "tail": lines[-max_lines:]}, indent=2)
96
+
97
+
98
+ def _executions_resource() -> str:
99
+ """Tail of the executions log."""
100
+ return _tail_file(config.log_dir / "executions.jsonl")
101
+
102
+
103
+ def _server_log_resource() -> str:
104
+ """Tail of the server log."""
105
+ return _tail_file(config.log_dir / "server.log")
106
+
107
+
108
+ def _artifacts_index_resource() -> str:
109
+ """List artifacts in the artifact directory."""
110
+ entries = []
111
+ if config.artifact_dir.exists():
112
+ for p in sorted(config.artifact_dir.iterdir()):
113
+ if p.is_file():
114
+ entries.append(
115
+ {
116
+ "name": p.name,
117
+ "size_bytes": p.stat().st_size,
118
+ "uri": f"{RESOURCE_NAMESPACE}://artifacts/{p.name}",
119
+ }
120
+ )
121
+ return json.dumps({"count": len(entries), "artifacts": entries}, indent=2)
122
+
123
+
124
+ def _artifact_resource(artifact: str) -> str:
125
+ """Read a single artifact, safely (no path traversal)."""
126
+ name = Path(artifact).name
127
+ if name != artifact:
128
+ return json.dumps({"error": "invalid artifact name"}, indent=2)
129
+ path = config.artifact_dir / name
130
+ if not path.exists() or not path.is_file():
131
+ return json.dumps({"error": f"artifact not found: {name}"}, indent=2)
132
+ if path.stat().st_size > 1_000_000:
133
+ return json.dumps({"error": "artifact too large to read"}, indent=2)
134
+ try:
135
+ content = path.read_text(encoding="utf-8", errors="replace")
136
+ except OSError as exc:
137
+ return json.dumps({"error": str(exc)}, indent=2)
138
+ try:
139
+ payload = json.loads(content)
140
+ except json.JSONDecodeError:
141
+ payload = content
142
+ return json.dumps(payload, indent=2, default=str)
143
+
144
+
145
+ def _db_stats_resource() -> str:
146
+ """SQLite persistence statistics as JSON."""
147
+ try:
148
+ from kali_mcp.db import manager as dbm
149
+
150
+ db = dbm.get_db()
151
+ if db is None:
152
+ return json.dumps({"error": "database disabled or unavailable"}, indent=2)
153
+ stats: dict[str, Any] = {"db_path": str(db.path)}
154
+ for table in ("targets", "scans", "findings", "credentials"):
155
+ try:
156
+ stats[table] = db.conn.execute(f"SELECT COUNT(*) FROM {table}").fetchone()[0]
157
+ except Exception: # noqa: BLE001
158
+ stats[table] = "error"
159
+ try:
160
+ stats["findings_by_severity"] = {
161
+ row[0]: row[1]
162
+ for row in db.conn.execute(
163
+ "SELECT severity, COUNT(*) FROM findings GROUP BY severity"
164
+ ).fetchall()
165
+ }
166
+ except Exception: # noqa: BLE001, S110
167
+ pass
168
+ return json.dumps(stats, indent=2)
169
+ except Exception as exc: # noqa: BLE001
170
+ return json.dumps({"error": str(exc)}, indent=2)
171
+
172
+
173
+ def _vault_index_resource() -> str:
174
+ """Vault index — the inter-agent knowledge-sharing catalog."""
175
+ try:
176
+ from kali_mcp.utils.vault import VAULT_ROOT, ensure_vault_dirs
177
+
178
+ ensure_vault_dirs()
179
+ index_path = VAULT_ROOT / "index.json"
180
+ if not index_path.exists():
181
+ return json.dumps({"count": 0, "entries": []}, indent=2)
182
+ data = json.loads(index_path.read_text())
183
+ # Enrich entries with browseable resource URIs where possible
184
+ for entry in data.get("entries", []):
185
+ path = Path(entry.get("path", ""))
186
+ if path.is_file() and config.artifact_dir in path.parents:
187
+ rel = path.relative_to(config.artifact_dir)
188
+ entry["uri"] = f"{RESOURCE_NAMESPACE}://artifacts/{rel}"
189
+ return json.dumps(data, indent=2)
190
+ except Exception as exc: # noqa: BLE001
191
+ return json.dumps({"error": str(exc)}, indent=2)
192
+
193
+
194
+ def register_resources(mcp, health=None) -> None:
195
+ """Register all kali:// resources and templates on the FastMCP server."""
196
+ mcp.resource(
197
+ f"{RESOURCE_NAMESPACE}://config",
198
+ name="kali_config",
199
+ description="Live server configuration",
200
+ mime_type="application/json",
201
+ )(_config_resource)
202
+
203
+ mcp.resource(
204
+ f"{RESOURCE_NAMESPACE}://tools",
205
+ name="kali_tools",
206
+ description="Registered tool inventory with schemas",
207
+ mime_type="application/json",
208
+ )(_tools_resource)
209
+
210
+ mcp.resource(
211
+ f"{RESOURCE_NAMESPACE}://health",
212
+ name="kali_health",
213
+ description="Live server health status",
214
+ mime_type="application/json",
215
+ )(_make_health_resource(health))
216
+
217
+ mcp.resource(
218
+ f"{RESOURCE_NAMESPACE}://logs/executions",
219
+ name="kali_executions_log",
220
+ description="Tail of the executions log (JSON lines)",
221
+ mime_type="application/json",
222
+ )(_executions_resource)
223
+
224
+ mcp.resource(
225
+ f"{RESOURCE_NAMESPACE}://logs/server",
226
+ name="kali_server_log",
227
+ description="Tail of the server log (JSON lines)",
228
+ mime_type="application/json",
229
+ )(_server_log_resource)
230
+
231
+ mcp.resource(
232
+ f"{RESOURCE_NAMESPACE}://artifacts",
233
+ name="kali_artifacts",
234
+ description="List generated artifacts",
235
+ mime_type="application/json",
236
+ )(_artifacts_index_resource)
237
+
238
+ mcp.resource(
239
+ f"{RESOURCE_NAMESPACE}://artifacts/{{artifact}}",
240
+ name="kali_artifact",
241
+ description="Read a generated artifact by name",
242
+ mime_type="application/json",
243
+ )(_artifact_resource)
244
+
245
+ mcp.resource(
246
+ f"{RESOURCE_NAMESPACE}://db/stats",
247
+ name="kali_db_stats",
248
+ description="SQLite persistence statistics (targets, scans, findings, credentials)",
249
+ mime_type="application/json",
250
+ )(_db_stats_resource)
251
+
252
+ mcp.resource(
253
+ f"{RESOURCE_NAMESPACE}://vault/index",
254
+ name="kali_vault_index",
255
+ description="Index of the inter-agent vault (findings, credentials, evidence, notes)",
256
+ mime_type="application/json",
257
+ )(_vault_index_resource)
258
+
259
+ logger.info(
260
+ "Registered %d resources + %d templates",
261
+ 9,
262
+ len(TEMPLATES),
263
+ )
kali_mcp/responses.py ADDED
@@ -0,0 +1,39 @@
1
+ """Standardized MCP response builders."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from typing import Any
7
+
8
+ from kali_mcp.models import ExecutionResult, ToolError
9
+
10
+
11
+ def success_response(result: ExecutionResult) -> dict[str, Any]:
12
+ """Build a successful tool result for MCP."""
13
+ return {
14
+ "content": [
15
+ {
16
+ "type": "text",
17
+ "text": json.dumps(result.to_dict(), indent=2),
18
+ }
19
+ ],
20
+ "isError": False,
21
+ }
22
+
23
+
24
+ def error_response(error: ToolError) -> dict[str, Any]:
25
+ """Build an error tool result for MCP."""
26
+ return {
27
+ "content": [
28
+ {
29
+ "type": "text",
30
+ "text": json.dumps(error.to_dict(), indent=2),
31
+ }
32
+ ],
33
+ "isError": True,
34
+ }
35
+
36
+
37
+ def error_response_from_exception(exc: Exception) -> dict[str, Any]:
38
+ """Build error response from caught exception — never exposes traceback."""
39
+ return error_response(ToolError(error=str(exc)))
kali_mcp/security.py ADDED
@@ -0,0 +1,15 @@
1
+ """Security helpers for command sanitization."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import shlex
6
+
7
+
8
+ def sanitize_command_parts(parts: list[str]) -> list[str]:
9
+ """Ensure command parts are safe for subprocess exec (no shell injection)."""
10
+ return [str(p) for p in parts]
11
+
12
+
13
+ def quote_argument(arg: str) -> str:
14
+ """Shell-safe quote a single argument."""
15
+ return shlex.quote(arg)