prooflayer-runtime 0.1.0__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.
- prooflayer/__init__.py +50 -0
- prooflayer/cli.py +362 -0
- prooflayer/config/__init__.py +6 -0
- prooflayer/config/allowlist.py +138 -0
- prooflayer/config/loader.py +29 -0
- prooflayer/detection/__init__.py +21 -0
- prooflayer/detection/engine.py +783 -0
- prooflayer/detection/models.py +49 -0
- prooflayer/detection/normalizer.py +245 -0
- prooflayer/detection/rules.py +104 -0
- prooflayer/detection/scanner.py +160 -0
- prooflayer/detection/scorer.py +65 -0
- prooflayer/detection/semantic.py +73 -0
- prooflayer/metrics.py +266 -0
- prooflayer/reporting/__init__.py +5 -0
- prooflayer/reporting/reporter.py +190 -0
- prooflayer/response/__init__.py +6 -0
- prooflayer/response/actions.py +152 -0
- prooflayer/response/killer.py +73 -0
- prooflayer/rules/command-injection.yaml +123 -0
- prooflayer/rules/data-exfiltration.yaml +83 -0
- prooflayer/rules/jailbreaks.yaml +67 -0
- prooflayer/rules/prompt-injection.yaml +99 -0
- prooflayer/rules/role-manipulation.yaml +60 -0
- prooflayer/rules/sql-injection.yaml +51 -0
- prooflayer/rules/ssrf-xxe.yaml +51 -0
- prooflayer/rules/tool-poisoning.yaml +46 -0
- prooflayer/runtime/__init__.py +21 -0
- prooflayer/runtime/interceptor.py +91 -0
- prooflayer/runtime/mcp_wrapper.py +395 -0
- prooflayer/runtime/middleware.py +86 -0
- prooflayer/runtime/transport.py +306 -0
- prooflayer/runtime/wrapper.py +265 -0
- prooflayer/utils/__init__.py +21 -0
- prooflayer/utils/encoding.py +87 -0
- prooflayer/utils/entropy.py +51 -0
- prooflayer/utils/logging.py +86 -0
- prooflayer/utils/masking.py +72 -0
- prooflayer/version.py +6 -0
- prooflayer_runtime-0.1.0.dist-info/METADATA +266 -0
- prooflayer_runtime-0.1.0.dist-info/RECORD +45 -0
- prooflayer_runtime-0.1.0.dist-info/WHEEL +5 -0
- prooflayer_runtime-0.1.0.dist-info/entry_points.txt +2 -0
- prooflayer_runtime-0.1.0.dist-info/licenses/LICENSE +4 -0
- prooflayer_runtime-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ProofLayer MCP SDK Wrapper
|
|
3
|
+
===========================
|
|
4
|
+
|
|
5
|
+
Async-compatible wrapper that integrates with the real MCP Python SDK.
|
|
6
|
+
Intercepts tool calls and tool listings for security scanning.
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
from prooflayer.runtime.mcp_wrapper import ProofLayerMCPWrapper
|
|
10
|
+
|
|
11
|
+
wrapper = ProofLayerMCPWrapper(config=config)
|
|
12
|
+
protected_server = wrapper.wrap(mcp_server)
|
|
13
|
+
|
|
14
|
+
Requires: pip install prooflayer-runtime[mcp]
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import logging
|
|
18
|
+
import functools
|
|
19
|
+
from typing import Any, Dict, List, Optional, Sequence
|
|
20
|
+
|
|
21
|
+
from ..detection.engine import DetectionEngine
|
|
22
|
+
from ..detection.rules import RuleLoadError
|
|
23
|
+
from ..response.actions import ResponseAction, ThreatAction
|
|
24
|
+
from ..reporting.reporter import SecurityReporter
|
|
25
|
+
from ..config.loader import ConfigLoader
|
|
26
|
+
|
|
27
|
+
logger = logging.getLogger(__name__)
|
|
28
|
+
|
|
29
|
+
# Lazy import for MCP SDK — it's an optional dependency
|
|
30
|
+
_mcp_available = None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _check_mcp_available():
|
|
34
|
+
"""Check if the MCP SDK is installed, caching the result."""
|
|
35
|
+
global _mcp_available
|
|
36
|
+
if _mcp_available is None:
|
|
37
|
+
try:
|
|
38
|
+
import mcp # noqa: F401
|
|
39
|
+
_mcp_available = True
|
|
40
|
+
except ImportError:
|
|
41
|
+
_mcp_available = False
|
|
42
|
+
return _mcp_available
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class MCPDependencyError(ImportError):
|
|
46
|
+
"""Raised when the MCP SDK is required but not installed."""
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class ProofLayerMCPWrapper:
|
|
51
|
+
"""
|
|
52
|
+
Async-compatible security wrapper for MCP Python SDK servers.
|
|
53
|
+
|
|
54
|
+
Intercepts:
|
|
55
|
+
- call_tool: scans tool call arguments before execution, scans outputs after
|
|
56
|
+
- list_tools: scans tool descriptions for prompt injection (tool poisoning)
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
def __init__(
|
|
60
|
+
self,
|
|
61
|
+
config: Optional[Dict[str, Any]] = None,
|
|
62
|
+
config_path: Optional[str] = None,
|
|
63
|
+
detection_rules_dir: Optional[str] = None,
|
|
64
|
+
action_on_threat: str = "block",
|
|
65
|
+
report_dir: Optional[str] = None,
|
|
66
|
+
scan_tool_descriptions: bool = True,
|
|
67
|
+
scan_tool_outputs: bool = True,
|
|
68
|
+
fail_closed: bool = True,
|
|
69
|
+
):
|
|
70
|
+
"""
|
|
71
|
+
Initialize the MCP wrapper.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
config: Configuration dict (takes precedence over config_path)
|
|
75
|
+
config_path: Path to YAML config file
|
|
76
|
+
detection_rules_dir: Directory containing YAML rule files
|
|
77
|
+
action_on_threat: Action on threat detection ("allow", "warn", "block", "kill")
|
|
78
|
+
report_dir: Directory for security reports
|
|
79
|
+
scan_tool_descriptions: Scan tool descriptions for prompt injection
|
|
80
|
+
scan_tool_outputs: Scan tool outputs before returning to LLM
|
|
81
|
+
fail_closed: Block all requests if rules fail to load (default True)
|
|
82
|
+
"""
|
|
83
|
+
if not _check_mcp_available():
|
|
84
|
+
raise MCPDependencyError(
|
|
85
|
+
"The 'mcp' package is required for MCP SDK integration. "
|
|
86
|
+
"Install it with: pip install prooflayer-runtime[mcp]"
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
# Build config from file or dict, with parameter overrides
|
|
90
|
+
if config:
|
|
91
|
+
self.config = config
|
|
92
|
+
elif config_path:
|
|
93
|
+
self.config = ConfigLoader.load(config_path)
|
|
94
|
+
else:
|
|
95
|
+
self.config = self._default_config()
|
|
96
|
+
|
|
97
|
+
# Apply parameter overrides
|
|
98
|
+
detection_cfg = self.config.setdefault("detection", {})
|
|
99
|
+
response_cfg = self.config.setdefault("response", {})
|
|
100
|
+
|
|
101
|
+
if detection_rules_dir:
|
|
102
|
+
detection_cfg["rules_dir"] = detection_rules_dir
|
|
103
|
+
if action_on_threat:
|
|
104
|
+
response_cfg["on_threat"] = action_on_threat
|
|
105
|
+
if report_dir:
|
|
106
|
+
response_cfg["report_dir"] = report_dir
|
|
107
|
+
|
|
108
|
+
detection_cfg["fail_closed"] = fail_closed
|
|
109
|
+
|
|
110
|
+
self.scan_tool_descriptions = scan_tool_descriptions
|
|
111
|
+
self.scan_tool_outputs = scan_tool_outputs
|
|
112
|
+
|
|
113
|
+
# Initialize detection engine
|
|
114
|
+
self.detection_engine = DetectionEngine(
|
|
115
|
+
rules_dir=detection_cfg.get("rules_dir"),
|
|
116
|
+
score_threshold=detection_cfg.get("score_threshold"),
|
|
117
|
+
fail_closed=fail_closed,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
self.reporter = SecurityReporter(
|
|
121
|
+
report_dir=response_cfg.get("report_dir", "./security-reports")
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
self.response_action = ResponseAction(
|
|
125
|
+
default_action=response_cfg.get("on_threat", "block"),
|
|
126
|
+
reporter=self.reporter,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
logger.info(
|
|
130
|
+
"ProofLayer MCP wrapper initialized with %d rules",
|
|
131
|
+
len(self.detection_engine.rules),
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
@staticmethod
|
|
135
|
+
def _default_config() -> Dict[str, Any]:
|
|
136
|
+
return {
|
|
137
|
+
"detection": {
|
|
138
|
+
"enabled": True,
|
|
139
|
+
"rules_dir": None,
|
|
140
|
+
"fail_closed": True,
|
|
141
|
+
"score_threshold": {
|
|
142
|
+
"allow": (0, 29),
|
|
143
|
+
"warn": (30, 69),
|
|
144
|
+
"block": (70, 100),
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
"response": {
|
|
148
|
+
"on_threat": "block",
|
|
149
|
+
"report_dir": "./security-reports",
|
|
150
|
+
"alert_webhook": None,
|
|
151
|
+
},
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
def wrap(self, server: Any) -> Any:
|
|
155
|
+
"""
|
|
156
|
+
Wrap an MCP Server instance with ProofLayer security scanning.
|
|
157
|
+
|
|
158
|
+
This hooks into the server's call_tool and list_tools handlers
|
|
159
|
+
by registering wrapped handlers that scan inputs/outputs.
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
server: An mcp.server.Server (or mcp.server.fastmcp.FastMCP) instance
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
The same server instance, with security hooks installed
|
|
166
|
+
"""
|
|
167
|
+
from mcp.server import Server as MCPServer
|
|
168
|
+
|
|
169
|
+
if not isinstance(server, MCPServer):
|
|
170
|
+
# FastMCP or other wrappers might have a ._mcp_server attribute
|
|
171
|
+
inner = getattr(server, "_mcp_server", None)
|
|
172
|
+
if inner is not None and isinstance(inner, MCPServer):
|
|
173
|
+
self._install_hooks(inner)
|
|
174
|
+
else:
|
|
175
|
+
# Attempt to install hooks directly — duck typing
|
|
176
|
+
self._install_hooks(server)
|
|
177
|
+
else:
|
|
178
|
+
self._install_hooks(server)
|
|
179
|
+
|
|
180
|
+
logger.info("ProofLayer security hooks installed on MCP server")
|
|
181
|
+
return server
|
|
182
|
+
|
|
183
|
+
def _install_hooks(self, server: Any) -> None:
|
|
184
|
+
"""Install call_tool and list_tools security hooks on the server."""
|
|
185
|
+
self._wrap_call_tool(server)
|
|
186
|
+
if self.scan_tool_descriptions:
|
|
187
|
+
self._wrap_list_tools(server)
|
|
188
|
+
|
|
189
|
+
def _wrap_call_tool(self, server: Any) -> None:
|
|
190
|
+
"""
|
|
191
|
+
Wrap the server's call_tool handler to scan inputs and outputs.
|
|
192
|
+
|
|
193
|
+
The MCP SDK uses @server.call_tool() as a decorator that registers
|
|
194
|
+
a handler. We intercept the registration to wrap the handler.
|
|
195
|
+
"""
|
|
196
|
+
original_call_tool_decorator = getattr(server, "call_tool", None)
|
|
197
|
+
if original_call_tool_decorator is None:
|
|
198
|
+
logger.warning(
|
|
199
|
+
"MCP server has no call_tool method; cannot install input scanning"
|
|
200
|
+
)
|
|
201
|
+
return
|
|
202
|
+
|
|
203
|
+
wrapper_self = self
|
|
204
|
+
|
|
205
|
+
def secured_call_tool_decorator():
|
|
206
|
+
"""Replacement decorator that wraps the user's handler with security."""
|
|
207
|
+
|
|
208
|
+
def decorator(handler):
|
|
209
|
+
@functools.wraps(handler)
|
|
210
|
+
async def secured_handler(name: str, arguments: Optional[Dict[str, Any]] = None):
|
|
211
|
+
arguments = arguments or {}
|
|
212
|
+
|
|
213
|
+
# --- Input scanning ---
|
|
214
|
+
risk_score, matched_rules = wrapper_self.detection_engine.scan(
|
|
215
|
+
tool_name=name,
|
|
216
|
+
arguments=arguments,
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
action = wrapper_self.response_action.decide_action(risk_score)
|
|
220
|
+
|
|
221
|
+
if action in (ThreatAction.BLOCK, ThreatAction.KILL):
|
|
222
|
+
wrapper_self.reporter.generate_report(
|
|
223
|
+
threat_type="prompt_injection",
|
|
224
|
+
tool_name=name,
|
|
225
|
+
arguments=arguments,
|
|
226
|
+
risk_score=risk_score,
|
|
227
|
+
matched_rules=matched_rules,
|
|
228
|
+
action=action.value,
|
|
229
|
+
)
|
|
230
|
+
logger.error(
|
|
231
|
+
"BLOCKED tool call: %s (score=%d, action=%s, rules=%s)",
|
|
232
|
+
name,
|
|
233
|
+
risk_score,
|
|
234
|
+
action.value,
|
|
235
|
+
[r.id for r in matched_rules],
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
from mcp.types import TextContent, CallToolResult
|
|
239
|
+
|
|
240
|
+
return CallToolResult(
|
|
241
|
+
content=[
|
|
242
|
+
TextContent(
|
|
243
|
+
type="text",
|
|
244
|
+
text=f"Tool call blocked by ProofLayer: {name} "
|
|
245
|
+
f"(risk score: {risk_score})",
|
|
246
|
+
)
|
|
247
|
+
],
|
|
248
|
+
isError=True,
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
if action == ThreatAction.WARN:
|
|
252
|
+
logger.warning(
|
|
253
|
+
"SUSPICIOUS tool call: %s (score=%d, rules=%s)",
|
|
254
|
+
name,
|
|
255
|
+
risk_score,
|
|
256
|
+
[r.id for r in matched_rules],
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
# --- Execute the original handler ---
|
|
260
|
+
result = await handler(name, arguments)
|
|
261
|
+
|
|
262
|
+
# --- Output scanning ---
|
|
263
|
+
if wrapper_self.scan_tool_outputs and result is not None:
|
|
264
|
+
result = await wrapper_self._scan_tool_output(name, result)
|
|
265
|
+
|
|
266
|
+
return result
|
|
267
|
+
|
|
268
|
+
# Register the secured handler using the original decorator
|
|
269
|
+
return original_call_tool_decorator()(secured_handler)
|
|
270
|
+
|
|
271
|
+
return decorator
|
|
272
|
+
|
|
273
|
+
server.call_tool = secured_call_tool_decorator
|
|
274
|
+
|
|
275
|
+
def _wrap_list_tools(self, server: Any) -> None:
|
|
276
|
+
"""
|
|
277
|
+
Wrap the server's list_tools handler to scan tool descriptions
|
|
278
|
+
for prompt injection (tool poisoning attacks).
|
|
279
|
+
"""
|
|
280
|
+
original_list_tools_decorator = getattr(server, "list_tools", None)
|
|
281
|
+
if original_list_tools_decorator is None:
|
|
282
|
+
logger.warning(
|
|
283
|
+
"MCP server has no list_tools method; cannot install description scanning"
|
|
284
|
+
)
|
|
285
|
+
return
|
|
286
|
+
|
|
287
|
+
wrapper_self = self
|
|
288
|
+
|
|
289
|
+
def secured_list_tools_decorator():
|
|
290
|
+
"""Replacement decorator that scans tool descriptions."""
|
|
291
|
+
|
|
292
|
+
def decorator(handler):
|
|
293
|
+
@functools.wraps(handler)
|
|
294
|
+
async def secured_handler():
|
|
295
|
+
result = await handler()
|
|
296
|
+
|
|
297
|
+
# Scan each tool's description for prompt injection
|
|
298
|
+
if result is not None:
|
|
299
|
+
tools = result if isinstance(result, list) else getattr(result, "tools", [])
|
|
300
|
+
for tool in tools:
|
|
301
|
+
desc = getattr(tool, "description", None) or ""
|
|
302
|
+
if not desc:
|
|
303
|
+
continue
|
|
304
|
+
|
|
305
|
+
score, matched = wrapper_self.detection_engine.scan(
|
|
306
|
+
tool_name=getattr(tool, "name", "unknown"),
|
|
307
|
+
arguments={"description": desc},
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
if score >= 30:
|
|
311
|
+
tool_name = getattr(tool, "name", "unknown")
|
|
312
|
+
logger.warning(
|
|
313
|
+
"TOOL POISONING detected in '%s' description "
|
|
314
|
+
"(score=%d, rules=%s)",
|
|
315
|
+
tool_name,
|
|
316
|
+
score,
|
|
317
|
+
[r.id for r in matched],
|
|
318
|
+
)
|
|
319
|
+
wrapper_self.reporter.generate_report(
|
|
320
|
+
threat_type="tool_poisoning",
|
|
321
|
+
tool_name=tool_name,
|
|
322
|
+
arguments={"description": desc},
|
|
323
|
+
risk_score=score,
|
|
324
|
+
matched_rules=matched,
|
|
325
|
+
action="WARN",
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
return result
|
|
329
|
+
|
|
330
|
+
return original_list_tools_decorator()(secured_handler)
|
|
331
|
+
|
|
332
|
+
return decorator
|
|
333
|
+
|
|
334
|
+
server.list_tools = secured_list_tools_decorator
|
|
335
|
+
|
|
336
|
+
async def _scan_tool_output(self, tool_name: str, result: Any) -> Any:
|
|
337
|
+
"""
|
|
338
|
+
Scan tool output for threats before it is returned to the LLM.
|
|
339
|
+
|
|
340
|
+
Checks for data exfiltration payloads, injected instructions, etc.
|
|
341
|
+
in the tool's response content.
|
|
342
|
+
"""
|
|
343
|
+
from mcp.types import CallToolResult, TextContent
|
|
344
|
+
|
|
345
|
+
if not isinstance(result, CallToolResult):
|
|
346
|
+
return result
|
|
347
|
+
|
|
348
|
+
for content_item in result.content:
|
|
349
|
+
if not isinstance(content_item, TextContent):
|
|
350
|
+
continue
|
|
351
|
+
|
|
352
|
+
text = content_item.text or ""
|
|
353
|
+
if not text:
|
|
354
|
+
continue
|
|
355
|
+
|
|
356
|
+
score, matched = self.detection_engine.scan(
|
|
357
|
+
tool_name=f"{tool_name}:output",
|
|
358
|
+
arguments={"response_text": text},
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
if score >= 70:
|
|
362
|
+
logger.error(
|
|
363
|
+
"BLOCKED tool output from '%s' (score=%d, rules=%s)",
|
|
364
|
+
tool_name,
|
|
365
|
+
score,
|
|
366
|
+
[r.id for r in matched],
|
|
367
|
+
)
|
|
368
|
+
self.reporter.generate_report(
|
|
369
|
+
threat_type="malicious_output",
|
|
370
|
+
tool_name=tool_name,
|
|
371
|
+
arguments={"response_text": text[:500]},
|
|
372
|
+
risk_score=score,
|
|
373
|
+
matched_rules=matched,
|
|
374
|
+
action="BLOCK",
|
|
375
|
+
)
|
|
376
|
+
return CallToolResult(
|
|
377
|
+
content=[
|
|
378
|
+
TextContent(
|
|
379
|
+
type="text",
|
|
380
|
+
text=f"Tool output blocked by ProofLayer: {tool_name} "
|
|
381
|
+
f"output contained suspicious content (risk score: {score})",
|
|
382
|
+
)
|
|
383
|
+
],
|
|
384
|
+
isError=True,
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
if score >= 30:
|
|
388
|
+
logger.warning(
|
|
389
|
+
"SUSPICIOUS tool output from '%s' (score=%d, rules=%s)",
|
|
390
|
+
tool_name,
|
|
391
|
+
score,
|
|
392
|
+
[r.id for r in matched],
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
return result
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ProofLayer Middleware
|
|
3
|
+
=====================
|
|
4
|
+
|
|
5
|
+
Reusable scan-and-decide logic shared between the HTTP transport proxy
|
|
6
|
+
and the MCP SDK wrapper.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
from typing import Optional, Dict, Any, Tuple
|
|
11
|
+
|
|
12
|
+
from ..detection.engine import DetectionEngine
|
|
13
|
+
from ..detection.models import ScanResult
|
|
14
|
+
from ..reporting.reporter import SecurityReporter
|
|
15
|
+
from ..response.actions import ResponseAction, ThreatAction
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ProofLayerMiddleware:
|
|
21
|
+
"""
|
|
22
|
+
Encapsulates the core scan → decide → report pipeline.
|
|
23
|
+
|
|
24
|
+
Used by both ProofLayerTransportProxy and ProofLayerMCPWrapper
|
|
25
|
+
to share security scanning logic.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
engine: DetectionEngine,
|
|
31
|
+
reporter: SecurityReporter,
|
|
32
|
+
response_action: ResponseAction,
|
|
33
|
+
):
|
|
34
|
+
self.engine = engine
|
|
35
|
+
self.reporter = reporter
|
|
36
|
+
self.response_action = response_action
|
|
37
|
+
|
|
38
|
+
def check_tool_call(
|
|
39
|
+
self,
|
|
40
|
+
tool_name: str,
|
|
41
|
+
arguments: Dict[str, Any],
|
|
42
|
+
) -> Tuple[ScanResult, ThreatAction, Optional[Dict[str, Any]]]:
|
|
43
|
+
"""
|
|
44
|
+
Scan a tool call and decide on an action.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
tool_name: MCP tool name.
|
|
48
|
+
arguments: Tool call arguments.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
(scan_result, action, report_or_None)
|
|
52
|
+
"""
|
|
53
|
+
result = self.engine.scan(tool_name=tool_name, arguments=arguments)
|
|
54
|
+
action = self.response_action.decide_action(result.score)
|
|
55
|
+
|
|
56
|
+
report = None
|
|
57
|
+
if action in (ThreatAction.BLOCK, ThreatAction.KILL):
|
|
58
|
+
threat_type = "unknown"
|
|
59
|
+
if result.matched_rules:
|
|
60
|
+
top_rule = max(result.matched_rules, key=lambda r: r.score)
|
|
61
|
+
threat_type = top_rule.category
|
|
62
|
+
|
|
63
|
+
report = self.reporter.generate_report(
|
|
64
|
+
threat_type=threat_type,
|
|
65
|
+
tool_name=tool_name,
|
|
66
|
+
arguments=arguments,
|
|
67
|
+
risk_score=result.score,
|
|
68
|
+
matched_rules=result.matched_rules,
|
|
69
|
+
action=action.value,
|
|
70
|
+
scan_result=result,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
logger.warning(
|
|
74
|
+
"BLOCKED tool call: %s (score=%d, action=%s, rules=%s)",
|
|
75
|
+
tool_name, result.score, action.value,
|
|
76
|
+
[r.id for r in result.matched_rules],
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
elif action == ThreatAction.WARN:
|
|
80
|
+
logger.warning(
|
|
81
|
+
"SUSPICIOUS tool call: %s (score=%d, rules=%s)",
|
|
82
|
+
tool_name, result.score,
|
|
83
|
+
[r.id for r in result.matched_rules],
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
return result, action, report
|