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.
Files changed (45) hide show
  1. prooflayer/__init__.py +50 -0
  2. prooflayer/cli.py +362 -0
  3. prooflayer/config/__init__.py +6 -0
  4. prooflayer/config/allowlist.py +138 -0
  5. prooflayer/config/loader.py +29 -0
  6. prooflayer/detection/__init__.py +21 -0
  7. prooflayer/detection/engine.py +783 -0
  8. prooflayer/detection/models.py +49 -0
  9. prooflayer/detection/normalizer.py +245 -0
  10. prooflayer/detection/rules.py +104 -0
  11. prooflayer/detection/scanner.py +160 -0
  12. prooflayer/detection/scorer.py +65 -0
  13. prooflayer/detection/semantic.py +73 -0
  14. prooflayer/metrics.py +266 -0
  15. prooflayer/reporting/__init__.py +5 -0
  16. prooflayer/reporting/reporter.py +190 -0
  17. prooflayer/response/__init__.py +6 -0
  18. prooflayer/response/actions.py +152 -0
  19. prooflayer/response/killer.py +73 -0
  20. prooflayer/rules/command-injection.yaml +123 -0
  21. prooflayer/rules/data-exfiltration.yaml +83 -0
  22. prooflayer/rules/jailbreaks.yaml +67 -0
  23. prooflayer/rules/prompt-injection.yaml +99 -0
  24. prooflayer/rules/role-manipulation.yaml +60 -0
  25. prooflayer/rules/sql-injection.yaml +51 -0
  26. prooflayer/rules/ssrf-xxe.yaml +51 -0
  27. prooflayer/rules/tool-poisoning.yaml +46 -0
  28. prooflayer/runtime/__init__.py +21 -0
  29. prooflayer/runtime/interceptor.py +91 -0
  30. prooflayer/runtime/mcp_wrapper.py +395 -0
  31. prooflayer/runtime/middleware.py +86 -0
  32. prooflayer/runtime/transport.py +306 -0
  33. prooflayer/runtime/wrapper.py +265 -0
  34. prooflayer/utils/__init__.py +21 -0
  35. prooflayer/utils/encoding.py +87 -0
  36. prooflayer/utils/entropy.py +51 -0
  37. prooflayer/utils/logging.py +86 -0
  38. prooflayer/utils/masking.py +72 -0
  39. prooflayer/version.py +6 -0
  40. prooflayer_runtime-0.1.0.dist-info/METADATA +266 -0
  41. prooflayer_runtime-0.1.0.dist-info/RECORD +45 -0
  42. prooflayer_runtime-0.1.0.dist-info/WHEEL +5 -0
  43. prooflayer_runtime-0.1.0.dist-info/entry_points.txt +2 -0
  44. prooflayer_runtime-0.1.0.dist-info/licenses/LICENSE +4 -0
  45. prooflayer_runtime-0.1.0.dist-info/top_level.txt +1 -0
prooflayer/metrics.py ADDED
@@ -0,0 +1,266 @@
1
+ """
2
+ Prometheus Metrics
3
+ ==================
4
+
5
+ Lightweight metrics collection and exposition for ProofLayer.
6
+ Uses only stdlib (no prometheus_client dependency).
7
+
8
+ Exposes metrics at /metrics in Prometheus text exposition format via http.server.
9
+
10
+ Usage:
11
+ from prooflayer.metrics import metrics, start_metrics_server
12
+
13
+ # Record a scan
14
+ metrics.record_scan(action="block", duration=0.003, risk_score=85,
15
+ matched_rules=[("cmd-inject-curl", "command_injection")])
16
+
17
+ # Start HTTP endpoint
18
+ start_metrics_server(port=9090)
19
+ """
20
+
21
+ import threading
22
+ import time
23
+ import math
24
+ from http.server import HTTPServer, BaseHTTPRequestHandler
25
+ from typing import Dict, List, Tuple, Optional
26
+
27
+
28
+ class _Counter:
29
+ """Thread-safe counter with optional labels."""
30
+
31
+ def __init__(self):
32
+ self._lock = threading.Lock()
33
+ self._values: Dict[tuple, float] = {}
34
+
35
+ def inc(self, labels: Optional[Dict[str, str]] = None, value: float = 1.0):
36
+ key = tuple(sorted((labels or {}).items()))
37
+ with self._lock:
38
+ self._values[key] = self._values.get(key, 0.0) + value
39
+
40
+ def collect(self) -> Dict[tuple, float]:
41
+ with self._lock:
42
+ return dict(self._values)
43
+
44
+
45
+ class _Gauge:
46
+ """Thread-safe gauge."""
47
+
48
+ def __init__(self):
49
+ self._lock = threading.Lock()
50
+ self._values: Dict[tuple, float] = {}
51
+
52
+ def set(self, value: float, labels: Optional[Dict[str, str]] = None):
53
+ key = tuple(sorted((labels or {}).items()))
54
+ with self._lock:
55
+ self._values[key] = value
56
+
57
+ def collect(self) -> Dict[tuple, float]:
58
+ with self._lock:
59
+ return dict(self._values)
60
+
61
+
62
+ class _HistogramLike:
63
+ """Tracks min, max, sum, count, and sorted observations for percentile computation."""
64
+
65
+ def __init__(self):
66
+ self._lock = threading.Lock()
67
+ self._observations: List[float] = []
68
+ self._sum: float = 0.0
69
+ self._count: int = 0
70
+ self._min: float = float("inf")
71
+ self._max: float = float("-inf")
72
+
73
+ def observe(self, value: float):
74
+ with self._lock:
75
+ self._observations.append(value)
76
+ self._sum += value
77
+ self._count += 1
78
+ if value < self._min:
79
+ self._min = value
80
+ if value > self._max:
81
+ self._max = value
82
+
83
+ def collect(self) -> Dict[str, float]:
84
+ with self._lock:
85
+ if self._count == 0:
86
+ return {
87
+ "count": 0, "sum": 0, "min": 0, "max": 0,
88
+ "avg": 0, "p50": 0, "p95": 0, "p99": 0,
89
+ }
90
+ sorted_obs = sorted(self._observations)
91
+ return {
92
+ "count": self._count,
93
+ "sum": self._sum,
94
+ "min": self._min,
95
+ "max": self._max,
96
+ "avg": self._sum / self._count,
97
+ "p50": _percentile(sorted_obs, 0.50),
98
+ "p95": _percentile(sorted_obs, 0.95),
99
+ "p99": _percentile(sorted_obs, 0.99),
100
+ }
101
+
102
+
103
+ def _percentile(sorted_data: List[float], pct: float) -> float:
104
+ """Compute percentile from sorted list using nearest-rank method."""
105
+ if not sorted_data:
106
+ return 0.0
107
+ idx = int(math.ceil(pct * len(sorted_data))) - 1
108
+ return sorted_data[max(0, idx)]
109
+
110
+
111
+ class MetricsCollector:
112
+ """Central metrics collector for ProofLayer."""
113
+
114
+ def __init__(self):
115
+ self.scans_total = _Counter()
116
+ self.scan_duration = _HistogramLike()
117
+ self.rules_matched_total = _Counter()
118
+ self.risk_score_last = _Gauge()
119
+ self.risk_score_avg = _Gauge()
120
+ self.active_rules = _Gauge()
121
+
122
+ self._lock = threading.Lock()
123
+ self._score_sum: float = 0.0
124
+ self._score_count: int = 0
125
+ self.enabled: bool = False
126
+
127
+ def record_scan(
128
+ self,
129
+ action: str,
130
+ duration: float,
131
+ risk_score: int,
132
+ matched_rules: Optional[List[Tuple[str, str]]] = None,
133
+ ):
134
+ """
135
+ Record a completed scan.
136
+
137
+ Args:
138
+ action: The action taken (allow, warn, block, kill)
139
+ duration: Scan duration in seconds
140
+ risk_score: Computed risk score (0-100)
141
+ matched_rules: List of (rule_id, category) tuples
142
+ """
143
+ if not self.enabled:
144
+ return
145
+
146
+ self.scans_total.inc(labels={"action": action.lower()})
147
+ self.scan_duration.observe(duration)
148
+
149
+ self.risk_score_last.set(float(risk_score))
150
+ with self._lock:
151
+ self._score_sum += risk_score
152
+ self._score_count += 1
153
+ self.risk_score_avg.set(self._score_sum / self._score_count)
154
+
155
+ for rule_id, category in (matched_rules or []):
156
+ self.rules_matched_total.inc(
157
+ labels={"rule_id": rule_id, "category": category}
158
+ )
159
+
160
+ def set_active_rules(self, count: int):
161
+ """Set the number of currently active rules."""
162
+ if not self.enabled:
163
+ return
164
+ self.active_rules.set(float(count))
165
+
166
+ def render_prometheus(self) -> str:
167
+ """Render all metrics in Prometheus text exposition format."""
168
+ lines: List[str] = []
169
+
170
+ # prooflayer_scans_total
171
+ lines.append("# HELP prooflayer_scans_total Total number of scans performed.")
172
+ lines.append("# TYPE prooflayer_scans_total counter")
173
+ for labels, value in self.scans_total.collect().items():
174
+ label_str = _labels_to_str(labels)
175
+ lines.append(f"prooflayer_scans_total{{{label_str}}} {value}")
176
+
177
+ # prooflayer_scan_duration_seconds
178
+ dur = self.scan_duration.collect()
179
+ lines.append("# HELP prooflayer_scan_duration_seconds Scan duration statistics.")
180
+ lines.append("# TYPE prooflayer_scan_duration_seconds summary")
181
+ lines.append(f'prooflayer_scan_duration_seconds{{quantile="0.5"}} {dur["p50"]}')
182
+ lines.append(f'prooflayer_scan_duration_seconds{{quantile="0.95"}} {dur["p95"]}')
183
+ lines.append(f'prooflayer_scan_duration_seconds{{quantile="0.99"}} {dur["p99"]}')
184
+ lines.append(f'prooflayer_scan_duration_seconds_count {dur["count"]}')
185
+ lines.append(f'prooflayer_scan_duration_seconds_sum {dur["sum"]}')
186
+ lines.append(f'prooflayer_scan_duration_seconds_min {dur["min"]}')
187
+ lines.append(f'prooflayer_scan_duration_seconds_max {dur["max"]}')
188
+ lines.append(f'prooflayer_scan_duration_seconds_avg {dur["avg"]}')
189
+
190
+ # prooflayer_rules_matched_total
191
+ lines.append("# HELP prooflayer_rules_matched_total Total rules matched.")
192
+ lines.append("# TYPE prooflayer_rules_matched_total counter")
193
+ for labels, value in self.rules_matched_total.collect().items():
194
+ label_str = _labels_to_str(labels)
195
+ lines.append(f"prooflayer_rules_matched_total{{{label_str}}} {value}")
196
+
197
+ # prooflayer_risk_score
198
+ lines.append("# HELP prooflayer_risk_score Risk score gauge.")
199
+ lines.append("# TYPE prooflayer_risk_score gauge")
200
+ for labels, value in self.risk_score_last.collect().items():
201
+ lines.append(f'prooflayer_risk_score{{stat="last"}} {value}')
202
+ for labels, value in self.risk_score_avg.collect().items():
203
+ lines.append(f'prooflayer_risk_score{{stat="avg"}} {value}')
204
+
205
+ # prooflayer_active_rules
206
+ lines.append("# HELP prooflayer_active_rules Number of active detection rules.")
207
+ lines.append("# TYPE prooflayer_active_rules gauge")
208
+ for labels, value in self.active_rules.collect().items():
209
+ lines.append(f"prooflayer_active_rules {value}")
210
+
211
+ lines.append("") # trailing newline
212
+ return "\n".join(lines)
213
+
214
+
215
+ def _labels_to_str(labels: tuple) -> str:
216
+ """Convert label tuple to Prometheus label string."""
217
+ parts = [f'{k}="{v}"' for k, v in labels]
218
+ return ",".join(parts)
219
+
220
+
221
+ # Singleton collector
222
+ metrics = MetricsCollector()
223
+
224
+
225
+ class _MetricsHandler(BaseHTTPRequestHandler):
226
+ """HTTP handler that serves Prometheus metrics."""
227
+
228
+ def do_GET(self):
229
+ if self.path == "/metrics":
230
+ body = metrics.render_prometheus().encode("utf-8")
231
+ self.send_response(200)
232
+ self.send_header("Content-Type", "text/plain; version=0.0.4; charset=utf-8")
233
+ self.send_header("Content-Length", str(len(body)))
234
+ self.end_headers()
235
+ self.wfile.write(body)
236
+ else:
237
+ self.send_response(404)
238
+ self.end_headers()
239
+
240
+ def log_message(self, format, *args):
241
+ # Suppress default access log noise
242
+ pass
243
+
244
+
245
+ _server_instance: Optional[HTTPServer] = None
246
+
247
+
248
+ def start_metrics_server(port: int = 9090) -> HTTPServer:
249
+ """
250
+ Start the Prometheus metrics HTTP server on a background thread.
251
+
252
+ Args:
253
+ port: TCP port to listen on (default 9090)
254
+
255
+ Returns:
256
+ HTTPServer instance (call .shutdown() to stop)
257
+ """
258
+ global _server_instance
259
+ if _server_instance is not None:
260
+ return _server_instance
261
+
262
+ server = HTTPServer(("0.0.0.0", port), _MetricsHandler)
263
+ thread = threading.Thread(target=server.serve_forever, daemon=True)
264
+ thread.start()
265
+ _server_instance = server
266
+ return server
@@ -0,0 +1,5 @@
1
+ """Security report generation."""
2
+
3
+ from .reporter import SecurityReporter
4
+
5
+ __all__ = ["SecurityReporter"]
@@ -0,0 +1,190 @@
1
+ """
2
+ Security Reporter
3
+ =================
4
+
5
+ Generates security reports in JSON and SARIF formats.
6
+ """
7
+
8
+ import json
9
+ import os
10
+ import threading
11
+ from datetime import datetime, timezone
12
+ from pathlib import Path
13
+ from typing import Dict, List, Any
14
+
15
+ from ..utils.masking import mask_sensitive_data
16
+
17
+
18
+ class SecurityReporter:
19
+ """
20
+ Generates security reports for detected threats.
21
+ """
22
+
23
+ def __init__(self, report_dir: str = "./security-reports"):
24
+ """
25
+ Initialize security reporter.
26
+
27
+ Args:
28
+ report_dir: Directory to write reports
29
+ """
30
+ self._lock = threading.Lock()
31
+ self.report_dir = Path(report_dir)
32
+ self.report_dir.mkdir(parents=True, exist_ok=True)
33
+ # Restrict report directory to owner only
34
+ os.chmod(self.report_dir, 0o700)
35
+
36
+ def generate_report(
37
+ self,
38
+ threat_type: str,
39
+ tool_name: str,
40
+ arguments: Dict[str, Any],
41
+ risk_score: int,
42
+ matched_rules: List[Any],
43
+ action: str,
44
+ scan_result: Any = None,
45
+ ) -> Dict[str, Any]:
46
+ """
47
+ Generate a security report.
48
+
49
+ Args:
50
+ threat_type: Type of threat detected
51
+ tool_name: MCP tool name
52
+ arguments: Tool arguments
53
+ risk_score: Calculated risk score
54
+ matched_rules: List of matched detection rules
55
+ action: Action taken (ALLOW/WARN/BLOCK/KILL)
56
+ scan_result: Optional ScanResult to auto-populate extended fields
57
+
58
+ Returns:
59
+ Dict containing report data and file path
60
+ """
61
+ timestamp = datetime.now(timezone.utc)
62
+ timestamp_str = timestamp.strftime("%Y-%m-%dT%H:%M:%S.%fZ")
63
+
64
+ # Mask sensitive data before writing to report
65
+ masked_arguments = mask_sensitive_data(arguments) if arguments else {}
66
+
67
+ # Build scoring breakdown from scan_result if available
68
+ scoring_breakdown: Dict[str, Any] = {}
69
+ owasp_mapping: List[str] = []
70
+ latency_ms = 0.0
71
+ scan_timestamp = timestamp_str
72
+ if scan_result is not None:
73
+ scoring_breakdown = getattr(scan_result, "scoring_breakdown", {})
74
+ owasp_mapping = getattr(scan_result, "owasp_mapping", [])
75
+ latency_ms = getattr(scan_result, "latency_ms", 0.0)
76
+ scan_timestamp = getattr(scan_result, "timestamp", timestamp_str)
77
+
78
+ report = {
79
+ "prooflayer_version": "0.1.0",
80
+ "timestamp": timestamp_str,
81
+ "threat": {
82
+ "type": threat_type,
83
+ "tool": tool_name,
84
+ "arguments": masked_arguments,
85
+ "risk_score": risk_score,
86
+ "action": action
87
+ },
88
+ "detection": {
89
+ "rules_matched": [
90
+ {
91
+ "id": rule.id,
92
+ "severity": rule.severity,
93
+ "message": rule.message,
94
+ "category": rule.category
95
+ }
96
+ for rule in matched_rules
97
+ ],
98
+ "confidence": "HIGH" if risk_score > 70 else "MEDIUM" if risk_score > 30 else "LOW",
99
+ "scoring_breakdown": scoring_breakdown,
100
+ },
101
+ "context": {
102
+ "timestamp_detected": timestamp_str,
103
+ "timestamp_received": scan_timestamp,
104
+ "latency_ms": round(latency_ms, 3),
105
+ "mcp_message_id": None,
106
+ "mcp_session_id": None,
107
+ "server": os.environ.get("MCP_SERVER_NAME", "unknown")
108
+ },
109
+ "owasp_mapping": owasp_mapping,
110
+ }
111
+
112
+ # Write JSON report with restricted permissions
113
+ report_filename = f"prooflayer-{timestamp.strftime('%Y%m%d-%H%M%S')}-{threat_type}.json"
114
+ report_path = self.report_dir / report_filename
115
+
116
+ with self._lock:
117
+ with open(report_path, "w") as f:
118
+ json.dump(report, f, indent=2)
119
+ os.chmod(report_path, 0o600)
120
+
121
+ report["report_path"] = str(report_path)
122
+
123
+ return report
124
+
125
+ def generate_sarif_report(
126
+ self,
127
+ threats: List[Dict[str, Any]]
128
+ ) -> str:
129
+ """
130
+ Generate SARIF format report for CI/CD integration.
131
+
132
+ Args:
133
+ threats: List of threat detections
134
+
135
+ Returns:
136
+ Path to SARIF report file
137
+ """
138
+ sarif = {
139
+ "$schema": "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json",
140
+ "version": "2.1.0",
141
+ "runs": [
142
+ {
143
+ "tool": {
144
+ "driver": {
145
+ "name": "ProofLayer Runtime Security",
146
+ "version": "0.1.0",
147
+ "informationUri": "https://www.proof-layer.com"
148
+ }
149
+ },
150
+ "results": [
151
+ {
152
+ "ruleId": threat["threat"]["type"],
153
+ "level": self._sarif_level(threat["threat"]["risk_score"]),
154
+ "message": {
155
+ "text": f"Security threat detected in {threat['threat']['tool']}"
156
+ },
157
+ "locations": [
158
+ {
159
+ "physicalLocation": {
160
+ "artifactLocation": {
161
+ "uri": threat["threat"]["tool"]
162
+ }
163
+ }
164
+ }
165
+ ]
166
+ }
167
+ for threat in threats
168
+ ]
169
+ }
170
+ ]
171
+ }
172
+
173
+ timestamp = datetime.now(timezone.utc).strftime("%Y%m%d-%H%M%S")
174
+ sarif_path = self.report_dir / f"sarif-report-{timestamp}.sarif"
175
+
176
+ with self._lock:
177
+ with open(sarif_path, "w") as f:
178
+ json.dump(sarif, f, indent=2)
179
+ os.chmod(sarif_path, 0o600)
180
+
181
+ return str(sarif_path)
182
+
183
+ def _sarif_level(self, risk_score: int) -> str:
184
+ """Convert risk score to SARIF severity level."""
185
+ if risk_score >= 70:
186
+ return "error"
187
+ elif risk_score >= 30:
188
+ return "warning"
189
+ else:
190
+ return "note"
@@ -0,0 +1,6 @@
1
+ """Response actions for threat detection."""
2
+
3
+ from .actions import ResponseAction, ThreatAction, SecurityViolation
4
+ from .killer import ServerKiller
5
+
6
+ __all__ = ["ResponseAction", "ThreatAction", "SecurityViolation", "ServerKiller"]
@@ -0,0 +1,152 @@
1
+ """
2
+ Response Actions
3
+ ================
4
+
5
+ Handles ALLOW/WARN/BLOCK/KILL actions based on threat detection.
6
+ """
7
+
8
+ import os
9
+ import sys
10
+ import signal
11
+ import logging
12
+ from enum import Enum
13
+ from typing import Dict, Any, Optional
14
+
15
+ from .killer import ServerKiller
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ class ThreatAction(Enum):
21
+ """Possible actions in response to a detected threat."""
22
+ ALLOW = "ALLOW"
23
+ WARN = "WARN"
24
+ BLOCK = "BLOCK"
25
+ KILL = "KILL"
26
+
27
+
28
+ class ResponseAction:
29
+ """
30
+ Manages response actions for detected threats.
31
+ """
32
+
33
+ def __init__(
34
+ self,
35
+ default_action: str = "warn",
36
+ reporter: Optional[Any] = None
37
+ ):
38
+ """
39
+ Initialize response action handler.
40
+
41
+ Args:
42
+ default_action: Default action ("allow", "warn", "block", "kill")
43
+ reporter: SecurityReporter instance
44
+ """
45
+ self.default_action = default_action.upper()
46
+ self.reporter = reporter
47
+
48
+ def decide_action(self, risk_score: int) -> ThreatAction:
49
+ """
50
+ Decide action based on risk score.
51
+
52
+ Args:
53
+ risk_score: Risk score (0-100)
54
+
55
+ Returns:
56
+ ThreatAction to take
57
+ """
58
+ if risk_score <= 29:
59
+ return ThreatAction.ALLOW
60
+ elif risk_score <= 69:
61
+ return ThreatAction.WARN
62
+ elif risk_score <= 89:
63
+ return ThreatAction.BLOCK
64
+ else:
65
+ # Score 90-100 = KILL
66
+ if self.default_action == "KILL":
67
+ return ThreatAction.KILL
68
+ else:
69
+ return ThreatAction.BLOCK
70
+
71
+ def execute(
72
+ self,
73
+ action: ThreatAction,
74
+ context: Dict[str, Any]
75
+ ) -> None:
76
+ """
77
+ Execute the chosen action.
78
+
79
+ Args:
80
+ action: Action to execute
81
+ context: Context information (tool, arguments, risk_score, etc.)
82
+ """
83
+ if action == ThreatAction.ALLOW:
84
+ logger.info(f"ALLOW: {context.get('tool_name')} (score: {context.get('risk_score')})")
85
+
86
+ elif action == ThreatAction.WARN:
87
+ logger.warning(
88
+ f"WARN: {context.get('tool_name')} "
89
+ f"(score: {context.get('risk_score')}) - Suspicious activity detected"
90
+ )
91
+ if self.reporter:
92
+ self.reporter.generate_report(
93
+ threat_type="suspicious_activity",
94
+ tool_name=context.get("tool_name"),
95
+ arguments=context.get("arguments", {}),
96
+ risk_score=context.get("risk_score", 0),
97
+ matched_rules=context.get("matched_rules", []),
98
+ action="WARN"
99
+ )
100
+
101
+ elif action == ThreatAction.BLOCK:
102
+ logger.error(
103
+ f"BLOCK: {context.get('tool_name')} "
104
+ f"(score: {context.get('risk_score')}) - Threat blocked"
105
+ )
106
+ if self.reporter:
107
+ report = self.reporter.generate_report(
108
+ threat_type="prompt_injection",
109
+ tool_name=context.get("tool_name"),
110
+ arguments=context.get("arguments", {}),
111
+ risk_score=context.get("risk_score", 0),
112
+ matched_rules=context.get("matched_rules", []),
113
+ action="BLOCK"
114
+ )
115
+ raise SecurityViolation(
116
+ f"Tool call blocked: {context.get('tool_name')} "
117
+ f"(risk score: {context.get('risk_score')})"
118
+ )
119
+
120
+ elif action == ThreatAction.KILL:
121
+ logger.critical(
122
+ f"KILL: {context.get('tool_name')} "
123
+ f"(score: {context.get('risk_score')}) - Terminating MCP server"
124
+ )
125
+ if self.reporter:
126
+ report = self.reporter.generate_report(
127
+ threat_type="critical_threat",
128
+ tool_name=context.get("tool_name"),
129
+ arguments=context.get("arguments", {}),
130
+ risk_score=context.get("risk_score", 0),
131
+ matched_rules=context.get("matched_rules", []),
132
+ action="SERVER_KILLED"
133
+ )
134
+ logger.critical(f"Security report written: {report['report_path']}")
135
+
136
+ self.kill_server(context)
137
+
138
+ def kill_server(self, context: Dict[str, Any]) -> None:
139
+ """
140
+ Terminate the MCP server process.
141
+
142
+ Delegates to ServerKiller.kill().
143
+
144
+ Args:
145
+ context: Context information for logging
146
+ """
147
+ ServerKiller().kill(context)
148
+
149
+
150
+ class SecurityViolation(Exception):
151
+ """Raised when a security threat is detected and blocked."""
152
+ pass
@@ -0,0 +1,73 @@
1
+ """
2
+ Server Killer
3
+ =============
4
+
5
+ Emergency MCP server termination for critical threats.
6
+ """
7
+
8
+ import os
9
+ import sys
10
+ import signal
11
+ import logging
12
+ import time
13
+ from typing import Dict, Any
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class ServerKiller:
19
+ """Handles emergency termination of MCP server processes."""
20
+
21
+ def kill(self, context: Dict[str, Any]) -> None:
22
+ """
23
+ Terminate the MCP server process.
24
+
25
+ Writes an emergency log, sends SIGTERM, then SIGKILL if needed.
26
+
27
+ Args:
28
+ context: Context information (tool_name, risk_score, arguments, etc.)
29
+ """
30
+ logger.critical("=" * 80)
31
+ logger.critical("PROOFLAYER RUNTIME SECURITY: CRITICAL THREAT DETECTED")
32
+ logger.critical("Tool: %s", context.get("tool_name"))
33
+ logger.critical("Risk Score: %s", context.get("risk_score"))
34
+ logger.critical("Action: MCP SERVER TERMINATED")
35
+ logger.critical("=" * 80)
36
+
37
+ # Write emergency log with restricted permissions
38
+ emergency_path = "/tmp/prooflayer-emergency.log"
39
+ try:
40
+ fd = os.open(
41
+ emergency_path,
42
+ os.O_WRONLY | os.O_CREAT | os.O_APPEND,
43
+ 0o600,
44
+ )
45
+ with os.fdopen(fd, "a") as f:
46
+ f.write(f"\n{'=' * 80}\n")
47
+ f.write(f"TIMESTAMP: {context.get('timestamp', 'N/A')}\n")
48
+ f.write(f"TOOL: {context.get('tool_name')}\n")
49
+ f.write(f"RISK SCORE: {context.get('risk_score')}\n")
50
+ f.write(f"ARGUMENTS: {context.get('arguments')}\n")
51
+ f.write(f"{'=' * 80}\n")
52
+ os.chmod(emergency_path, 0o600)
53
+ except Exception as e:
54
+ logger.error("Failed to write emergency log: %s", e)
55
+
56
+ # Kill the process
57
+ pid = os.getpid()
58
+ logger.critical("Sending SIGTERM to process %d", pid)
59
+
60
+ sys.stdout.flush()
61
+ sys.stderr.flush()
62
+
63
+ try:
64
+ os.killpg(os.getpgid(pid), signal.SIGTERM)
65
+ except (PermissionError, ProcessLookupError):
66
+ os.kill(pid, signal.SIGTERM)
67
+
68
+ # If still running after 1 second, force kill
69
+ time.sleep(1)
70
+ try:
71
+ os.killpg(os.getpgid(pid), signal.SIGKILL)
72
+ except (PermissionError, ProcessLookupError):
73
+ os.kill(pid, signal.SIGKILL)