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.
- kali_mcp/__init__.py +3 -0
- kali_mcp/__main__.py +28 -0
- kali_mcp/_version.py +24 -0
- kali_mcp/cli.py +169 -0
- kali_mcp/config.py +95 -0
- kali_mcp/db/__init__.py +1 -0
- kali_mcp/db/manager.py +248 -0
- kali_mcp/execution.py +439 -0
- kali_mcp/logging_utils.py +74 -0
- kali_mcp/models.py +51 -0
- kali_mcp/monitor/__init__.py +105 -0
- kali_mcp/monitor/sessions.py +70 -0
- kali_mcp/monitor/sse_client.py +87 -0
- kali_mcp/monitor/tui.py +364 -0
- kali_mcp/notifications.py +130 -0
- kali_mcp/prompts.py +184 -0
- kali_mcp/registry.py +33 -0
- kali_mcp/resources.py +263 -0
- kali_mcp/responses.py +39 -0
- kali_mcp/security.py +15 -0
- kali_mcp/server.py +535 -0
- kali_mcp/tools/__init__.py +125 -0
- kali_mcp/tools/amass.py +83 -0
- kali_mcp/tools/arjun.py +86 -0
- kali_mcp/tools/base.py +100 -0
- kali_mcp/tools/bettercap.py +99 -0
- kali_mcp/tools/bloodhound.py +68 -0
- kali_mcp/tools/commix.py +73 -0
- kali_mcp/tools/crackmapexec.py +100 -0
- kali_mcp/tools/dirb.py +75 -0
- kali_mcp/tools/dursgo.py +156 -0
- kali_mcp/tools/enum4linux.py +59 -0
- kali_mcp/tools/farsight.py +127 -0
- kali_mcp/tools/ffuf.py +85 -0
- kali_mcp/tools/file_download.py +119 -0
- kali_mcp/tools/file_read.py +144 -0
- kali_mcp/tools/file_write.py +141 -0
- kali_mcp/tools/flowlyt.py +88 -0
- kali_mcp/tools/generic_command.py +95 -0
- kali_mcp/tools/gobuster.py +84 -0
- kali_mcp/tools/hashcat.py +101 -0
- kali_mcp/tools/httpx.py +85 -0
- kali_mcp/tools/hydra.py +99 -0
- kali_mcp/tools/john.py +100 -0
- kali_mcp/tools/katana.py +81 -0
- kali_mcp/tools/masscan.py +85 -0
- kali_mcp/tools/medusa.py +102 -0
- kali_mcp/tools/msfconsole.py +59 -0
- kali_mcp/tools/naabu.py +100 -0
- kali_mcp/tools/netexec.py +86 -0
- kali_mcp/tools/nmap.py +94 -0
- kali_mcp/tools/normalization.py +18 -0
- kali_mcp/tools/nuclei.py +87 -0
- kali_mcp/tools/parallel_execute.py +246 -0
- kali_mcp/tools/persist.py +436 -0
- kali_mcp/tools/playbook_ad.py +99 -0
- kali_mcp/tools/playbook_engine.py +142 -0
- kali_mcp/tools/playbook_engine_tool.py +72 -0
- kali_mcp/tools/playbook_recon.py +115 -0
- kali_mcp/tools/playbook_tools.py +125 -0
- kali_mcp/tools/playbook_webapp.py +105 -0
- kali_mcp/tools/python_command.py +100 -0
- kali_mcp/tools/responder.py +85 -0
- kali_mcp/tools/searchsploit.py +123 -0
- kali_mcp/tools/spiderfoot.py +75 -0
- kali_mcp/tools/sqlmap.py +73 -0
- kali_mcp/tools/subfinder.py +69 -0
- kali_mcp/tools/theharvester.py +80 -0
- kali_mcp/tools/vault_tool.py +489 -0
- kali_mcp/tools/wfuzz.py +87 -0
- kali_mcp/tools/whatweb.py +82 -0
- kali_mcp/tools/wpscan.py +76 -0
- kali_mcp/tools/zap.py +79 -0
- kali_mcp/tools/zighound.py +139 -0
- kali_mcp/tools/zizmor.py +98 -0
- kali_mcp/utils/__init__.py +0 -0
- kali_mcp/utils/process.py +36 -0
- kali_mcp/utils/vault.py +471 -0
- kali_mcp/validation.py +82 -0
- kmcp-0.1.5.dev0.dist-info/METADATA +251 -0
- kmcp-0.1.5.dev0.dist-info/RECORD +84 -0
- kmcp-0.1.5.dev0.dist-info/WHEEL +5 -0
- kmcp-0.1.5.dev0.dist-info/entry_points.txt +4 -0
- 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)
|