@hoanghainh/komagu 2.6.1 → 2.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/README.md +1 -0
- package/assess/lsa/knowledge/path-ssrf-patterns.md +19 -0
- package/assess/lsa/knowledge/severity-and-cwe.md +3 -1
- package/assess/lsa/scripts/find_path_ssrf.py +176 -0
- package/assess/lsa/scripts/findings_to_sarif.py +29 -0
- package/assess/lsa/scripts/merge_findings.py +1 -1
- package/assess/lsa/scripts/run_prescan.py +4 -0
- package/assess/lsa/tests/run_tests.py +4 -0
- package/assess/lsa/tests/test_path_ssrf.py +94 -0
- package/contracts/schemas/finding.schema.json +4 -2
- package/operate/README.md +6 -1
- package/operate/cli/__init__.py +1 -1
- package/operate/cli/commands/fix.py +47 -0
- package/operate/cli/commands/mcp.py +54 -0
- package/operate/cli/main.py +42 -4
- package/operate/mcp/__init__.py +11 -0
- package/operate/mcp/server.py +206 -0
- package/operate/mcp/tools.py +464 -0
- package/operate/remediator/ai_fixer.py +285 -0
- package/operate/remediator/runner.py +11 -0
- package/operate/remediator/strategies.py +64 -0
- package/operate/tests/test_ai_remediation.py +288 -0
- package/operate/tests/test_mcp_server.py +380 -0
- package/package.json +1 -1
- package/remediate/authz/bfla_patcher.py +319 -0
- package/remediate/authz/mass_assignment_patcher.py +188 -0
- package/remediate/authz/patcher.py +8 -0
- package/remediate/authz/tests/test_bfla_and_mass_assignment_patcher.py +213 -0
package/operate/cli/main.py
CHANGED
|
@@ -7,7 +7,20 @@ import sys
|
|
|
7
7
|
from typing import List, Optional
|
|
8
8
|
|
|
9
9
|
from operate.cli import __version__
|
|
10
|
-
from operate.cli.commands import
|
|
10
|
+
from operate.cli.commands import (
|
|
11
|
+
demo,
|
|
12
|
+
devsecops,
|
|
13
|
+
evidence,
|
|
14
|
+
fix,
|
|
15
|
+
init,
|
|
16
|
+
inventory,
|
|
17
|
+
leak_check,
|
|
18
|
+
mcp,
|
|
19
|
+
protect,
|
|
20
|
+
scan,
|
|
21
|
+
simulate,
|
|
22
|
+
test,
|
|
23
|
+
)
|
|
11
24
|
from operate.cli.ui import print_banner
|
|
12
25
|
|
|
13
26
|
|
|
@@ -30,6 +43,7 @@ Quick Start Workflow:
|
|
|
30
43
|
9. Reconcile API inventory: komagu inventory --routes OUT/routes.json --openapi openapi.json
|
|
31
44
|
10. Bundle METI SCS evidence: komagu evidence --audit-dir OUT/ --output-dir ./evidence-pack
|
|
32
45
|
11. Check outputs for leaks: komagu leak-check OUT/ --canary-file canaries.txt
|
|
46
|
+
12. Serve tools to an AI client: komagu mcp --stdio
|
|
33
47
|
|
|
34
48
|
Documentation & Guides:
|
|
35
49
|
Comprehensive User Guide: docs/cli-guide.md
|
|
@@ -68,6 +82,7 @@ Learn more about a specific command:
|
|
|
68
82
|
inventory.register_subcommand(subparsers)
|
|
69
83
|
evidence.register_subcommand(subparsers)
|
|
70
84
|
leak_check.register_subcommand(subparsers)
|
|
85
|
+
mcp.register_subcommand(subparsers)
|
|
71
86
|
|
|
72
87
|
help_parser = subparsers.add_parser(
|
|
73
88
|
"help",
|
|
@@ -77,8 +92,24 @@ Learn more about a specific command:
|
|
|
77
92
|
help_parser.add_argument(
|
|
78
93
|
"topic",
|
|
79
94
|
nargs="?",
|
|
80
|
-
choices=[
|
|
81
|
-
|
|
95
|
+
choices=[
|
|
96
|
+
"scan",
|
|
97
|
+
"protect",
|
|
98
|
+
"test",
|
|
99
|
+
"demo",
|
|
100
|
+
"devsecops",
|
|
101
|
+
"fix",
|
|
102
|
+
"init",
|
|
103
|
+
"simulate",
|
|
104
|
+
"inventory",
|
|
105
|
+
"evidence",
|
|
106
|
+
"leak-check",
|
|
107
|
+
"mcp",
|
|
108
|
+
],
|
|
109
|
+
help=(
|
|
110
|
+
"Subcommand topic to display help for "
|
|
111
|
+
"(scan, protect, test, demo, devsecops, fix, init, simulate, inventory, evidence, leak-check, mcp)"
|
|
112
|
+
),
|
|
82
113
|
)
|
|
83
114
|
|
|
84
115
|
return parser
|
|
@@ -90,7 +121,14 @@ def main(argv: Optional[List[str]] = None) -> int:
|
|
|
90
121
|
args = parser.parse_args(argv)
|
|
91
122
|
|
|
92
123
|
is_json = getattr(args, "format", "") == "json"
|
|
93
|
-
|
|
124
|
+
# The MCP server owns stdout. A banner would break the JSON-RPC stream.
|
|
125
|
+
suppress_banner = getattr(args, "subcommand", None) == "mcp"
|
|
126
|
+
if (
|
|
127
|
+
not getattr(args, "no_banner", False)
|
|
128
|
+
and getattr(args, "subcommand", None) is not None
|
|
129
|
+
and not is_json
|
|
130
|
+
and not suppress_banner
|
|
131
|
+
):
|
|
94
132
|
print_banner()
|
|
95
133
|
|
|
96
134
|
if getattr(args, "subcommand", None) is None:
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""Zero-dependency Model Context Protocol server for the Komagu CLI.
|
|
2
|
+
|
|
3
|
+
Python 3.8+ standard library only. Messages are newline-delimited JSON-RPC
|
|
4
|
+
on stdio. Nothing except protocol messages is written to stdout.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from operate.mcp.server import serve_stdio
|
|
10
|
+
|
|
11
|
+
__all__ = ["serve_stdio"]
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
"""Newline-delimited JSON-RPC session for the Model Context Protocol.
|
|
2
|
+
|
|
3
|
+
Supported protocol revisions: 2024-11-05, 2025-03-26, and 2025-06-18.
|
|
4
|
+
A requested revision we implement is echoed back. Any other revision is
|
|
5
|
+
answered with the newest one this server implements.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import sys
|
|
12
|
+
import traceback
|
|
13
|
+
from typing import Any, Dict, IO, Optional, TextIO
|
|
14
|
+
|
|
15
|
+
from operate.cli import __version__
|
|
16
|
+
from operate.mcp.tools import INSTRUCTIONS, ToolError, ToolRegistry, build_registry
|
|
17
|
+
|
|
18
|
+
JSONRPC = "2.0"
|
|
19
|
+
PROTOCOL_VERSIONS = ("2024-11-05", "2025-03-26", "2025-06-18")
|
|
20
|
+
LATEST_PROTOCOL = PROTOCOL_VERSIONS[-1]
|
|
21
|
+
SERVER_NAME = "komagu"
|
|
22
|
+
MAX_LINE_CHARS = 1_048_576
|
|
23
|
+
TEXT_CAP = 60_000
|
|
24
|
+
|
|
25
|
+
PARSE_ERROR = -32700
|
|
26
|
+
INVALID_REQUEST = -32600
|
|
27
|
+
METHOD_NOT_FOUND = -32601
|
|
28
|
+
INVALID_PARAMS = -32602
|
|
29
|
+
INTERNAL_ERROR = -32603
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def error_response(request_id: Any, code: int, message: str) -> Dict[str, Any]:
|
|
33
|
+
"""JSON-RPC error object. request_id is null when the request id could not be read."""
|
|
34
|
+
return {"jsonrpc": JSONRPC, "id": request_id, "error": {"code": code, "message": message}}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def success_response(request_id: Any, result: Dict[str, Any]) -> Dict[str, Any]:
|
|
38
|
+
"""JSON-RPC result object."""
|
|
39
|
+
return {"jsonrpc": JSONRPC, "id": request_id, "result": result}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def tool_result(payload: Dict[str, Any], is_error: bool) -> Dict[str, Any]:
|
|
43
|
+
"""MCP tools/call result. Text is the payload the client shows the model."""
|
|
44
|
+
if is_error:
|
|
45
|
+
text = payload.get("message", "tool error")
|
|
46
|
+
return {"content": [{"type": "text", "text": text}], "isError": True}
|
|
47
|
+
text = json.dumps(payload, indent=2, ensure_ascii=False, sort_keys=True)
|
|
48
|
+
if len(text) > TEXT_CAP:
|
|
49
|
+
text = text[:TEXT_CAP] + "\n…truncated"
|
|
50
|
+
return {
|
|
51
|
+
"content": [{"type": "text", "text": text}],
|
|
52
|
+
"structuredContent": payload,
|
|
53
|
+
"isError": False,
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class Session:
|
|
58
|
+
"""One stdio client. Requests other than initialize and ping wait for initialized."""
|
|
59
|
+
|
|
60
|
+
def __init__(self, registry: ToolRegistry, version: str, instructions: str) -> None:
|
|
61
|
+
self.registry = registry
|
|
62
|
+
self.version = version
|
|
63
|
+
self.instructions = instructions
|
|
64
|
+
self.initialized = False
|
|
65
|
+
self.negotiated: Optional[str] = None
|
|
66
|
+
|
|
67
|
+
def handle(self, message: Any) -> Optional[Dict[str, Any]]:
|
|
68
|
+
"""Return a response, or None when the message is a notification."""
|
|
69
|
+
if not isinstance(message, dict):
|
|
70
|
+
return error_response(None, INVALID_REQUEST, "Invalid Request")
|
|
71
|
+
|
|
72
|
+
is_request = "id" in message
|
|
73
|
+
request_id = message.get("id") if is_request else None
|
|
74
|
+
if message.get("jsonrpc") != JSONRPC:
|
|
75
|
+
return error_response(request_id, INVALID_REQUEST, "Invalid Request") if is_request else None
|
|
76
|
+
|
|
77
|
+
method = message.get("method")
|
|
78
|
+
if not isinstance(method, str) or not method:
|
|
79
|
+
return error_response(request_id, INVALID_REQUEST, "Invalid Request") if is_request else None
|
|
80
|
+
|
|
81
|
+
params = message.get("params", {})
|
|
82
|
+
if params is None:
|
|
83
|
+
params = {}
|
|
84
|
+
if not isinstance(params, dict):
|
|
85
|
+
return error_response(request_id, INVALID_PARAMS, "Invalid params") if is_request else None
|
|
86
|
+
|
|
87
|
+
if method == "initialize":
|
|
88
|
+
if not is_request:
|
|
89
|
+
return None
|
|
90
|
+
return self._initialize(request_id, params)
|
|
91
|
+
|
|
92
|
+
if method in ("notifications/initialized", "initialized"):
|
|
93
|
+
if self.negotiated is not None:
|
|
94
|
+
self.initialized = True
|
|
95
|
+
return None
|
|
96
|
+
|
|
97
|
+
if method == "ping":
|
|
98
|
+
return success_response(request_id, {}) if is_request else None
|
|
99
|
+
|
|
100
|
+
if method.startswith("notifications/"):
|
|
101
|
+
return None
|
|
102
|
+
|
|
103
|
+
if not is_request:
|
|
104
|
+
return None
|
|
105
|
+
|
|
106
|
+
if not self.initialized:
|
|
107
|
+
return error_response(request_id, INVALID_REQUEST, "Server not initialized")
|
|
108
|
+
|
|
109
|
+
if method == "tools/list":
|
|
110
|
+
cursor = params.get("cursor")
|
|
111
|
+
if cursor not in (None, ""):
|
|
112
|
+
return error_response(request_id, INVALID_PARAMS, "Unknown cursor")
|
|
113
|
+
return success_response(request_id, {"tools": self.registry.descriptors()})
|
|
114
|
+
|
|
115
|
+
if method == "tools/call":
|
|
116
|
+
return self._call_tool(request_id, params)
|
|
117
|
+
|
|
118
|
+
if method == "logging/setLevel":
|
|
119
|
+
return success_response(request_id, {})
|
|
120
|
+
|
|
121
|
+
return error_response(request_id, METHOD_NOT_FOUND, f"Method not found: {method}")
|
|
122
|
+
|
|
123
|
+
def _initialize(self, request_id: Any, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
124
|
+
if self.negotiated is not None:
|
|
125
|
+
return error_response(request_id, INVALID_REQUEST, "Server already initialized")
|
|
126
|
+
version = params.get("protocolVersion")
|
|
127
|
+
if not isinstance(version, str) or not version:
|
|
128
|
+
return error_response(request_id, INVALID_PARAMS, "protocolVersion is required")
|
|
129
|
+
chosen = version if version in PROTOCOL_VERSIONS else LATEST_PROTOCOL
|
|
130
|
+
self.negotiated = chosen
|
|
131
|
+
return success_response(
|
|
132
|
+
request_id,
|
|
133
|
+
{
|
|
134
|
+
"protocolVersion": chosen,
|
|
135
|
+
"capabilities": {"tools": {"listChanged": False}},
|
|
136
|
+
"serverInfo": {"name": SERVER_NAME, "version": self.version},
|
|
137
|
+
"instructions": self.instructions,
|
|
138
|
+
},
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
def _call_tool(self, request_id: Any, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
142
|
+
name = params.get("name")
|
|
143
|
+
if not isinstance(name, str) or not name:
|
|
144
|
+
return error_response(request_id, INVALID_PARAMS, "tool name is required")
|
|
145
|
+
arguments = params.get("arguments", {})
|
|
146
|
+
if arguments is None:
|
|
147
|
+
arguments = {}
|
|
148
|
+
if not isinstance(arguments, dict):
|
|
149
|
+
return error_response(request_id, INVALID_PARAMS, "arguments must be an object")
|
|
150
|
+
try:
|
|
151
|
+
payload = self.registry.call(name, arguments)
|
|
152
|
+
except KeyError:
|
|
153
|
+
return error_response(request_id, INVALID_PARAMS, f"Unknown tool: {name}")
|
|
154
|
+
except ToolError as exc:
|
|
155
|
+
return success_response(request_id, tool_result({"message": str(exc)}, is_error=True))
|
|
156
|
+
except Exception as exc:
|
|
157
|
+
traceback.print_exc(file=sys.stderr)
|
|
158
|
+
return success_response(request_id, tool_result({"message": f"Tool failed: {exc}"}, is_error=True))
|
|
159
|
+
return success_response(request_id, tool_result(payload, is_error=False))
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _write(stdout: TextIO, message: Dict[str, Any]) -> None:
|
|
163
|
+
"""One JSON object per line. ensure_ascii keeps the line free of raw separators."""
|
|
164
|
+
stdout.write(json.dumps(message, ensure_ascii=True, separators=(",", ":")) + "\n")
|
|
165
|
+
flush = getattr(stdout, "flush", None)
|
|
166
|
+
if flush is not None:
|
|
167
|
+
flush()
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def serve(session: Session, stdin: IO[str], stdout: TextIO) -> None:
|
|
171
|
+
"""Read messages until stdin closes. Blank lines are ignored."""
|
|
172
|
+
while True:
|
|
173
|
+
line = stdin.readline()
|
|
174
|
+
if line == "":
|
|
175
|
+
return
|
|
176
|
+
if len(line) > MAX_LINE_CHARS:
|
|
177
|
+
_write(stdout, error_response(None, INVALID_REQUEST, "Message too large"))
|
|
178
|
+
continue
|
|
179
|
+
if not line.strip():
|
|
180
|
+
continue
|
|
181
|
+
try:
|
|
182
|
+
message = json.loads(line)
|
|
183
|
+
except json.JSONDecodeError:
|
|
184
|
+
_write(stdout, error_response(None, PARSE_ERROR, "Parse error"))
|
|
185
|
+
continue
|
|
186
|
+
if isinstance(message, list):
|
|
187
|
+
_write(stdout, error_response(None, INVALID_REQUEST, "Batch requests are not supported"))
|
|
188
|
+
continue
|
|
189
|
+
response = session.handle(message)
|
|
190
|
+
if response is not None:
|
|
191
|
+
_write(stdout, response)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def serve_stdio() -> int:
|
|
195
|
+
"""Serve on process stdin and stdout. Logs go to stderr."""
|
|
196
|
+
if hasattr(sys.stdin, "reconfigure"):
|
|
197
|
+
sys.stdin.reconfigure(encoding="utf-8")
|
|
198
|
+
if hasattr(sys.stdout, "reconfigure"):
|
|
199
|
+
sys.stdout.reconfigure(encoding="utf-8", line_buffering=True)
|
|
200
|
+
print("komagu mcp: serving Model Context Protocol on stdio", file=sys.stderr, flush=True)
|
|
201
|
+
session = Session(build_registry(), __version__, INSTRUCTIONS)
|
|
202
|
+
try:
|
|
203
|
+
serve(session, sys.stdin, sys.stdout)
|
|
204
|
+
except BrokenPipeError:
|
|
205
|
+
return 0
|
|
206
|
+
return 0
|