truthound-dashboard 1.3.1__py3-none-any.whl → 1.4.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 (169) hide show
  1. truthound_dashboard/api/alerts.py +258 -0
  2. truthound_dashboard/api/anomaly.py +1302 -0
  3. truthound_dashboard/api/cross_alerts.py +352 -0
  4. truthound_dashboard/api/deps.py +143 -0
  5. truthound_dashboard/api/drift_monitor.py +540 -0
  6. truthound_dashboard/api/lineage.py +1151 -0
  7. truthound_dashboard/api/maintenance.py +363 -0
  8. truthound_dashboard/api/middleware.py +373 -1
  9. truthound_dashboard/api/model_monitoring.py +805 -0
  10. truthound_dashboard/api/notifications_advanced.py +2452 -0
  11. truthound_dashboard/api/plugins.py +2096 -0
  12. truthound_dashboard/api/profile.py +211 -14
  13. truthound_dashboard/api/reports.py +853 -0
  14. truthound_dashboard/api/router.py +147 -0
  15. truthound_dashboard/api/rule_suggestions.py +310 -0
  16. truthound_dashboard/api/schema_evolution.py +231 -0
  17. truthound_dashboard/api/sources.py +47 -3
  18. truthound_dashboard/api/triggers.py +190 -0
  19. truthound_dashboard/api/validations.py +13 -0
  20. truthound_dashboard/api/validators.py +333 -4
  21. truthound_dashboard/api/versioning.py +309 -0
  22. truthound_dashboard/api/websocket.py +301 -0
  23. truthound_dashboard/core/__init__.py +27 -0
  24. truthound_dashboard/core/anomaly.py +1395 -0
  25. truthound_dashboard/core/anomaly_explainer.py +633 -0
  26. truthound_dashboard/core/cache.py +206 -0
  27. truthound_dashboard/core/cached_services.py +422 -0
  28. truthound_dashboard/core/charts.py +352 -0
  29. truthound_dashboard/core/connections.py +1069 -42
  30. truthound_dashboard/core/cross_alerts.py +837 -0
  31. truthound_dashboard/core/drift_monitor.py +1477 -0
  32. truthound_dashboard/core/drift_sampling.py +669 -0
  33. truthound_dashboard/core/i18n/__init__.py +42 -0
  34. truthound_dashboard/core/i18n/detector.py +173 -0
  35. truthound_dashboard/core/i18n/messages.py +564 -0
  36. truthound_dashboard/core/lineage.py +971 -0
  37. truthound_dashboard/core/maintenance.py +443 -5
  38. truthound_dashboard/core/model_monitoring.py +1043 -0
  39. truthound_dashboard/core/notifications/channels.py +1020 -1
  40. truthound_dashboard/core/notifications/deduplication/__init__.py +143 -0
  41. truthound_dashboard/core/notifications/deduplication/policies.py +274 -0
  42. truthound_dashboard/core/notifications/deduplication/service.py +400 -0
  43. truthound_dashboard/core/notifications/deduplication/stores.py +2365 -0
  44. truthound_dashboard/core/notifications/deduplication/strategies.py +422 -0
  45. truthound_dashboard/core/notifications/dispatcher.py +43 -0
  46. truthound_dashboard/core/notifications/escalation/__init__.py +149 -0
  47. truthound_dashboard/core/notifications/escalation/backends.py +1384 -0
  48. truthound_dashboard/core/notifications/escalation/engine.py +429 -0
  49. truthound_dashboard/core/notifications/escalation/models.py +336 -0
  50. truthound_dashboard/core/notifications/escalation/scheduler.py +1187 -0
  51. truthound_dashboard/core/notifications/escalation/state_machine.py +330 -0
  52. truthound_dashboard/core/notifications/escalation/stores.py +2896 -0
  53. truthound_dashboard/core/notifications/events.py +49 -0
  54. truthound_dashboard/core/notifications/metrics/__init__.py +115 -0
  55. truthound_dashboard/core/notifications/metrics/base.py +528 -0
  56. truthound_dashboard/core/notifications/metrics/collectors.py +583 -0
  57. truthound_dashboard/core/notifications/routing/__init__.py +169 -0
  58. truthound_dashboard/core/notifications/routing/combinators.py +184 -0
  59. truthound_dashboard/core/notifications/routing/config.py +375 -0
  60. truthound_dashboard/core/notifications/routing/config_parser.py +867 -0
  61. truthound_dashboard/core/notifications/routing/engine.py +382 -0
  62. truthound_dashboard/core/notifications/routing/expression_engine.py +1269 -0
  63. truthound_dashboard/core/notifications/routing/jinja2_engine.py +774 -0
  64. truthound_dashboard/core/notifications/routing/rules.py +625 -0
  65. truthound_dashboard/core/notifications/routing/validator.py +678 -0
  66. truthound_dashboard/core/notifications/service.py +2 -0
  67. truthound_dashboard/core/notifications/stats_aggregator.py +850 -0
  68. truthound_dashboard/core/notifications/throttling/__init__.py +83 -0
  69. truthound_dashboard/core/notifications/throttling/builder.py +311 -0
  70. truthound_dashboard/core/notifications/throttling/stores.py +1859 -0
  71. truthound_dashboard/core/notifications/throttling/throttlers.py +633 -0
  72. truthound_dashboard/core/openlineage.py +1028 -0
  73. truthound_dashboard/core/plugins/__init__.py +39 -0
  74. truthound_dashboard/core/plugins/docs/__init__.py +39 -0
  75. truthound_dashboard/core/plugins/docs/extractor.py +703 -0
  76. truthound_dashboard/core/plugins/docs/renderers.py +804 -0
  77. truthound_dashboard/core/plugins/hooks/__init__.py +63 -0
  78. truthound_dashboard/core/plugins/hooks/decorators.py +367 -0
  79. truthound_dashboard/core/plugins/hooks/manager.py +403 -0
  80. truthound_dashboard/core/plugins/hooks/protocols.py +265 -0
  81. truthound_dashboard/core/plugins/lifecycle/__init__.py +41 -0
  82. truthound_dashboard/core/plugins/lifecycle/hot_reload.py +584 -0
  83. truthound_dashboard/core/plugins/lifecycle/machine.py +419 -0
  84. truthound_dashboard/core/plugins/lifecycle/states.py +266 -0
  85. truthound_dashboard/core/plugins/loader.py +504 -0
  86. truthound_dashboard/core/plugins/registry.py +810 -0
  87. truthound_dashboard/core/plugins/reporter_executor.py +588 -0
  88. truthound_dashboard/core/plugins/sandbox/__init__.py +59 -0
  89. truthound_dashboard/core/plugins/sandbox/code_validator.py +243 -0
  90. truthound_dashboard/core/plugins/sandbox/engines.py +770 -0
  91. truthound_dashboard/core/plugins/sandbox/protocols.py +194 -0
  92. truthound_dashboard/core/plugins/sandbox.py +617 -0
  93. truthound_dashboard/core/plugins/security/__init__.py +68 -0
  94. truthound_dashboard/core/plugins/security/analyzer.py +535 -0
  95. truthound_dashboard/core/plugins/security/policies.py +311 -0
  96. truthound_dashboard/core/plugins/security/protocols.py +296 -0
  97. truthound_dashboard/core/plugins/security/signing.py +842 -0
  98. truthound_dashboard/core/plugins/security.py +446 -0
  99. truthound_dashboard/core/plugins/validator_executor.py +401 -0
  100. truthound_dashboard/core/plugins/versioning/__init__.py +51 -0
  101. truthound_dashboard/core/plugins/versioning/constraints.py +377 -0
  102. truthound_dashboard/core/plugins/versioning/dependencies.py +541 -0
  103. truthound_dashboard/core/plugins/versioning/semver.py +266 -0
  104. truthound_dashboard/core/profile_comparison.py +601 -0
  105. truthound_dashboard/core/report_history.py +570 -0
  106. truthound_dashboard/core/reporters/__init__.py +57 -0
  107. truthound_dashboard/core/reporters/base.py +296 -0
  108. truthound_dashboard/core/reporters/csv_reporter.py +155 -0
  109. truthound_dashboard/core/reporters/html_reporter.py +598 -0
  110. truthound_dashboard/core/reporters/i18n/__init__.py +65 -0
  111. truthound_dashboard/core/reporters/i18n/base.py +494 -0
  112. truthound_dashboard/core/reporters/i18n/catalogs.py +930 -0
  113. truthound_dashboard/core/reporters/json_reporter.py +160 -0
  114. truthound_dashboard/core/reporters/junit_reporter.py +233 -0
  115. truthound_dashboard/core/reporters/markdown_reporter.py +207 -0
  116. truthound_dashboard/core/reporters/pdf_reporter.py +209 -0
  117. truthound_dashboard/core/reporters/registry.py +272 -0
  118. truthound_dashboard/core/rule_generator.py +2088 -0
  119. truthound_dashboard/core/scheduler.py +822 -12
  120. truthound_dashboard/core/schema_evolution.py +858 -0
  121. truthound_dashboard/core/services.py +152 -9
  122. truthound_dashboard/core/statistics.py +718 -0
  123. truthound_dashboard/core/streaming_anomaly.py +883 -0
  124. truthound_dashboard/core/triggers/__init__.py +45 -0
  125. truthound_dashboard/core/triggers/base.py +226 -0
  126. truthound_dashboard/core/triggers/evaluators.py +609 -0
  127. truthound_dashboard/core/triggers/factory.py +363 -0
  128. truthound_dashboard/core/unified_alerts.py +870 -0
  129. truthound_dashboard/core/validation_limits.py +509 -0
  130. truthound_dashboard/core/versioning.py +709 -0
  131. truthound_dashboard/core/websocket/__init__.py +59 -0
  132. truthound_dashboard/core/websocket/manager.py +512 -0
  133. truthound_dashboard/core/websocket/messages.py +130 -0
  134. truthound_dashboard/db/__init__.py +30 -0
  135. truthound_dashboard/db/models.py +3375 -3
  136. truthound_dashboard/main.py +22 -0
  137. truthound_dashboard/schemas/__init__.py +396 -1
  138. truthound_dashboard/schemas/anomaly.py +1258 -0
  139. truthound_dashboard/schemas/base.py +4 -0
  140. truthound_dashboard/schemas/cross_alerts.py +334 -0
  141. truthound_dashboard/schemas/drift_monitor.py +890 -0
  142. truthound_dashboard/schemas/lineage.py +428 -0
  143. truthound_dashboard/schemas/maintenance.py +154 -0
  144. truthound_dashboard/schemas/model_monitoring.py +374 -0
  145. truthound_dashboard/schemas/notifications_advanced.py +1363 -0
  146. truthound_dashboard/schemas/openlineage.py +704 -0
  147. truthound_dashboard/schemas/plugins.py +1293 -0
  148. truthound_dashboard/schemas/profile.py +420 -34
  149. truthound_dashboard/schemas/profile_comparison.py +242 -0
  150. truthound_dashboard/schemas/reports.py +285 -0
  151. truthound_dashboard/schemas/rule_suggestion.py +434 -0
  152. truthound_dashboard/schemas/schema_evolution.py +164 -0
  153. truthound_dashboard/schemas/source.py +117 -2
  154. truthound_dashboard/schemas/triggers.py +511 -0
  155. truthound_dashboard/schemas/unified_alerts.py +223 -0
  156. truthound_dashboard/schemas/validation.py +25 -1
  157. truthound_dashboard/schemas/validators/__init__.py +11 -0
  158. truthound_dashboard/schemas/validators/base.py +151 -0
  159. truthound_dashboard/schemas/versioning.py +152 -0
  160. truthound_dashboard/static/index.html +2 -2
  161. {truthound_dashboard-1.3.1.dist-info → truthound_dashboard-1.4.0.dist-info}/METADATA +142 -22
  162. truthound_dashboard-1.4.0.dist-info/RECORD +239 -0
  163. truthound_dashboard/static/assets/index-BZG20KuF.js +0 -586
  164. truthound_dashboard/static/assets/index-D_HyZ3pb.css +0 -1
  165. truthound_dashboard/static/assets/unmerged_dictionaries-CtpqQBm0.js +0 -1
  166. truthound_dashboard-1.3.1.dist-info/RECORD +0 -110
  167. {truthound_dashboard-1.3.1.dist-info → truthound_dashboard-1.4.0.dist-info}/WHEEL +0 -0
  168. {truthound_dashboard-1.3.1.dist-info → truthound_dashboard-1.4.0.dist-info}/entry_points.txt +0 -0
  169. {truthound_dashboard-1.3.1.dist-info → truthound_dashboard-1.4.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,535 @@
1
+ """Security Analyzer for Plugin Code.
2
+
3
+ This module provides comprehensive security analysis for plugin code,
4
+ including AST-based analysis, permission detection, and risk assessment.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import ast
10
+ import hashlib
11
+ import logging
12
+ import re
13
+ from dataclasses import dataclass, field
14
+ from datetime import datetime
15
+ from typing import Any, Callable
16
+
17
+ from .protocols import TrustLevel, SecurityPolicy
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+
22
+ @dataclass
23
+ class CodeAnalysisResult:
24
+ """Result of code analysis.
25
+
26
+ Attributes:
27
+ is_safe: Whether code is considered safe.
28
+ issues: Critical security issues found.
29
+ warnings: Non-critical warnings.
30
+ blocked_constructs: List of blocked constructs found.
31
+ detected_imports: List of detected imports.
32
+ detected_permissions: List of detected permission requirements.
33
+ complexity_score: Code complexity score (0-100).
34
+ """
35
+
36
+ is_safe: bool
37
+ issues: list[str] = field(default_factory=list)
38
+ warnings: list[str] = field(default_factory=list)
39
+ blocked_constructs: list[str] = field(default_factory=list)
40
+ detected_imports: list[str] = field(default_factory=list)
41
+ detected_permissions: list[str] = field(default_factory=list)
42
+ complexity_score: int = 0
43
+
44
+ def to_dict(self) -> dict[str, Any]:
45
+ """Convert to dictionary."""
46
+ return {
47
+ "is_safe": self.is_safe,
48
+ "issues": self.issues,
49
+ "warnings": self.warnings,
50
+ "blocked_constructs": self.blocked_constructs,
51
+ "detected_imports": self.detected_imports,
52
+ "detected_permissions": self.detected_permissions,
53
+ "complexity_score": self.complexity_score,
54
+ }
55
+
56
+
57
+ @dataclass
58
+ class SecurityReport:
59
+ """Comprehensive security report for a plugin.
60
+
61
+ Attributes:
62
+ plugin_id: Plugin identifier.
63
+ analyzed_at: When analysis was performed.
64
+ trust_level: Determined trust level.
65
+ is_safe: Whether plugin is considered safe.
66
+ can_run_in_sandbox: Whether code can run in sandbox.
67
+ code_analysis: Code analysis result.
68
+ signature_valid: Whether signature is valid.
69
+ signature_count: Number of valid signatures.
70
+ required_permissions: Required permissions.
71
+ code_hash: SHA256 hash of analyzed code.
72
+ recommendations: Security recommendations.
73
+ """
74
+
75
+ plugin_id: str
76
+ analyzed_at: datetime
77
+ trust_level: TrustLevel
78
+ is_safe: bool = False
79
+ can_run_in_sandbox: bool = True
80
+ code_analysis: CodeAnalysisResult | None = None
81
+ signature_valid: bool = False
82
+ signature_count: int = 0
83
+ required_permissions: list[str] = field(default_factory=list)
84
+ code_hash: str = ""
85
+ recommendations: list[str] = field(default_factory=list)
86
+
87
+ def to_dict(self) -> dict[str, Any]:
88
+ """Convert to dictionary."""
89
+ return {
90
+ "plugin_id": self.plugin_id,
91
+ "analyzed_at": self.analyzed_at.isoformat(),
92
+ "trust_level": self.trust_level.value,
93
+ "is_safe": self.is_safe,
94
+ "can_run_in_sandbox": self.can_run_in_sandbox,
95
+ "code_analysis": self.code_analysis.to_dict() if self.code_analysis else None,
96
+ "signature_valid": self.signature_valid,
97
+ "signature_count": self.signature_count,
98
+ "required_permissions": self.required_permissions,
99
+ "code_hash": self.code_hash,
100
+ "recommendations": self.recommendations,
101
+ }
102
+
103
+
104
+ class CodeAnalyzer(ast.NodeVisitor):
105
+ """AST-based code analyzer for security issues."""
106
+
107
+ # Dangerous function calls
108
+ BLOCKED_FUNCTIONS = frozenset({
109
+ "eval", "exec", "compile",
110
+ "open", "input",
111
+ "__import__",
112
+ "globals", "locals", "vars", "dir",
113
+ "getattr", "setattr", "delattr",
114
+ "breakpoint", "exit", "quit",
115
+ "memoryview", "bytearray",
116
+ })
117
+
118
+ # Dangerous attribute accesses
119
+ BLOCKED_ATTRIBUTES = frozenset({
120
+ "__class__", "__bases__", "__subclasses__", "__mro__",
121
+ "__code__", "__globals__", "__builtins__",
122
+ "__dict__", "__closure__", "__func__",
123
+ "__self__", "__module__", "__qualname__",
124
+ "__annotations__", "__slots__",
125
+ "__reduce__", "__reduce_ex__",
126
+ "__getstate__", "__setstate__",
127
+ })
128
+
129
+ # Dangerous string patterns
130
+ DANGEROUS_PATTERNS = [
131
+ (r"os\.system", "os.system call"),
132
+ (r"subprocess\.", "subprocess usage"),
133
+ (r"socket\.", "socket usage"),
134
+ (r"__import__", "dynamic import"),
135
+ (r"importlib\.", "importlib usage"),
136
+ (r"ctypes\.", "ctypes usage"),
137
+ (r"pickle\.", "pickle usage"),
138
+ (r"marshal\.", "marshal usage"),
139
+ ]
140
+
141
+ # Permission-related imports
142
+ PERMISSION_IMPORTS = {
143
+ "os": "file_system",
144
+ "shutil": "file_system",
145
+ "pathlib": "file_system",
146
+ "tempfile": "file_system",
147
+ "glob": "file_system",
148
+ "fnmatch": "file_system",
149
+ "subprocess": "execute_code",
150
+ "multiprocessing": "execute_code",
151
+ "concurrent": "execute_code",
152
+ "asyncio": "execute_code",
153
+ "socket": "network_access",
154
+ "http": "network_access",
155
+ "urllib": "network_access",
156
+ "requests": "network_access",
157
+ "httpx": "network_access",
158
+ "aiohttp": "network_access",
159
+ "ssl": "network_access",
160
+ "sqlite3": "database_access",
161
+ "pymysql": "database_access",
162
+ "psycopg": "database_access",
163
+ "pymongo": "database_access",
164
+ "redis": "database_access",
165
+ }
166
+
167
+ def __init__(self, blocked_modules: list[str] | None = None) -> None:
168
+ """Initialize the analyzer.
169
+
170
+ Args:
171
+ blocked_modules: Additional modules to block.
172
+ """
173
+ self.blocked_modules = set(blocked_modules or [])
174
+ self.issues: list[str] = []
175
+ self.warnings: list[str] = []
176
+ self.blocked_constructs: list[str] = []
177
+ self.detected_imports: list[str] = []
178
+ self.detected_permissions: set[str] = set()
179
+ self.complexity_score = 0
180
+ self._depth = 0
181
+ self._loop_depth = 0
182
+
183
+ def visit_Call(self, node: ast.Call) -> None:
184
+ """Check function calls."""
185
+ func_name = None
186
+
187
+ if isinstance(node.func, ast.Name):
188
+ func_name = node.func.id
189
+ elif isinstance(node.func, ast.Attribute):
190
+ func_name = node.func.attr
191
+
192
+ if func_name and func_name in self.BLOCKED_FUNCTIONS:
193
+ self.issues.append(f"Blocked function call: {func_name}")
194
+ self.blocked_constructs.append(f"call:{func_name}")
195
+
196
+ self.generic_visit(node)
197
+
198
+ def visit_Attribute(self, node: ast.Attribute) -> None:
199
+ """Check attribute access."""
200
+ if node.attr in self.BLOCKED_ATTRIBUTES:
201
+ self.issues.append(f"Blocked attribute access: {node.attr}")
202
+ self.blocked_constructs.append(f"attr:{node.attr}")
203
+
204
+ self.generic_visit(node)
205
+
206
+ def visit_Import(self, node: ast.Import) -> None:
207
+ """Check import statements."""
208
+ for alias in node.names:
209
+ module = alias.name.split(".")[0]
210
+ self.detected_imports.append(alias.name)
211
+
212
+ if module in self.blocked_modules:
213
+ self.issues.append(f"Blocked module import: {module}")
214
+ self.blocked_constructs.append(f"import:{module}")
215
+ elif module in self.PERMISSION_IMPORTS:
216
+ self.detected_permissions.add(self.PERMISSION_IMPORTS[module])
217
+ self.warnings.append(f"Import requires permission: {module}")
218
+
219
+ self.generic_visit(node)
220
+
221
+ def visit_ImportFrom(self, node: ast.ImportFrom) -> None:
222
+ """Check from imports."""
223
+ if node.module:
224
+ module = node.module.split(".")[0]
225
+ self.detected_imports.append(node.module)
226
+
227
+ if module in self.blocked_modules:
228
+ self.issues.append(f"Blocked module import: {module}")
229
+ self.blocked_constructs.append(f"import:{module}")
230
+ elif module in self.PERMISSION_IMPORTS:
231
+ self.detected_permissions.add(self.PERMISSION_IMPORTS[module])
232
+ self.warnings.append(f"Import requires permission: {module}")
233
+
234
+ self.generic_visit(node)
235
+
236
+ def visit_FunctionDef(self, node: ast.FunctionDef) -> None:
237
+ """Track function definitions for complexity."""
238
+ self.complexity_score += 1
239
+ self._depth += 1
240
+ self.generic_visit(node)
241
+ self._depth -= 1
242
+
243
+ def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> None:
244
+ """Track async function definitions."""
245
+ self.complexity_score += 1
246
+ self._depth += 1
247
+ self.generic_visit(node)
248
+ self._depth -= 1
249
+
250
+ def visit_ClassDef(self, node: ast.ClassDef) -> None:
251
+ """Track class definitions."""
252
+ self.complexity_score += 2
253
+ self._depth += 1
254
+ self.generic_visit(node)
255
+ self._depth -= 1
256
+
257
+ def visit_For(self, node: ast.For) -> None:
258
+ """Track loops."""
259
+ self.complexity_score += 1
260
+ self._loop_depth += 1
261
+ if self._loop_depth > 3:
262
+ self.warnings.append("Deeply nested loops (depth > 3)")
263
+ self.generic_visit(node)
264
+ self._loop_depth -= 1
265
+
266
+ def visit_While(self, node: ast.While) -> None:
267
+ """Track while loops."""
268
+ self.complexity_score += 2 # While loops are riskier
269
+ self._loop_depth += 1
270
+ if self._loop_depth > 3:
271
+ self.warnings.append("Deeply nested loops (depth > 3)")
272
+ self.generic_visit(node)
273
+ self._loop_depth -= 1
274
+
275
+ def visit_If(self, node: ast.If) -> None:
276
+ """Track conditionals."""
277
+ self.complexity_score += 1
278
+ self.generic_visit(node)
279
+
280
+ def visit_Try(self, node: ast.Try) -> None:
281
+ """Track exception handling."""
282
+ self.complexity_score += 1
283
+ # Bare except is a warning
284
+ for handler in node.handlers:
285
+ if handler.type is None:
286
+ self.warnings.append("Bare except clause found")
287
+ self.generic_visit(node)
288
+
289
+ def visit_With(self, node: ast.With) -> None:
290
+ """Track with statements."""
291
+ self.generic_visit(node)
292
+
293
+ def analyze(self, code: str) -> CodeAnalysisResult:
294
+ """Analyze code for security issues.
295
+
296
+ Args:
297
+ code: Python source code to analyze.
298
+
299
+ Returns:
300
+ CodeAnalysisResult with findings.
301
+ """
302
+ # Reset state
303
+ self.issues = []
304
+ self.warnings = []
305
+ self.blocked_constructs = []
306
+ self.detected_imports = []
307
+ self.detected_permissions = set()
308
+ self.complexity_score = 0
309
+ self._depth = 0
310
+ self._loop_depth = 0
311
+
312
+ # Parse and visit AST
313
+ try:
314
+ tree = ast.parse(code)
315
+ self.visit(tree)
316
+ except SyntaxError as e:
317
+ self.issues.append(f"Syntax error: {e}")
318
+ return CodeAnalysisResult(
319
+ is_safe=False,
320
+ issues=self.issues,
321
+ )
322
+
323
+ # Check for dangerous patterns in source code
324
+ for pattern, description in self.DANGEROUS_PATTERNS:
325
+ if re.search(pattern, code):
326
+ self.warnings.append(f"Potentially dangerous pattern: {description}")
327
+
328
+ # Calculate final complexity score (0-100)
329
+ self.complexity_score = min(100, self.complexity_score)
330
+
331
+ return CodeAnalysisResult(
332
+ is_safe=len(self.issues) == 0,
333
+ issues=self.issues,
334
+ warnings=self.warnings,
335
+ blocked_constructs=self.blocked_constructs,
336
+ detected_imports=self.detected_imports,
337
+ detected_permissions=sorted(self.detected_permissions),
338
+ complexity_score=self.complexity_score,
339
+ )
340
+
341
+
342
+ class SecurityAnalyzer:
343
+ """Comprehensive security analyzer for plugins."""
344
+
345
+ def __init__(
346
+ self,
347
+ policy: SecurityPolicy | None = None,
348
+ blocked_modules: list[str] | None = None,
349
+ ) -> None:
350
+ """Initialize the security analyzer.
351
+
352
+ Args:
353
+ policy: Security policy to apply.
354
+ blocked_modules: Additional blocked modules.
355
+ """
356
+ self.policy = policy
357
+ self.blocked_modules = blocked_modules or (
358
+ policy.blocked_modules if policy else []
359
+ )
360
+
361
+ def analyze_code(self, code: str) -> CodeAnalysisResult:
362
+ """Analyze code for security issues.
363
+
364
+ Args:
365
+ code: Python source code.
366
+
367
+ Returns:
368
+ CodeAnalysisResult with findings.
369
+ """
370
+ analyzer = CodeAnalyzer(blocked_modules=self.blocked_modules)
371
+ return analyzer.analyze(code)
372
+
373
+ def analyze_plugin(
374
+ self,
375
+ plugin_id: str,
376
+ code: str | None = None,
377
+ signature_valid: bool = False,
378
+ signature_count: int = 0,
379
+ ) -> SecurityReport:
380
+ """Perform comprehensive plugin security analysis.
381
+
382
+ Args:
383
+ plugin_id: Plugin identifier.
384
+ code: Plugin code to analyze.
385
+ signature_valid: Whether signatures are valid.
386
+ signature_count: Number of valid signatures.
387
+
388
+ Returns:
389
+ SecurityReport with analysis results.
390
+ """
391
+ code_analysis = None
392
+ code_hash = ""
393
+ required_permissions: list[str] = []
394
+ recommendations: list[str] = []
395
+
396
+ # Analyze code if provided
397
+ if code:
398
+ code_analysis = self.analyze_code(code)
399
+ code_hash = hashlib.sha256(code.encode()).hexdigest()
400
+ required_permissions = code_analysis.detected_permissions.copy()
401
+
402
+ # Generate recommendations based on analysis
403
+ if code_analysis.complexity_score > 50:
404
+ recommendations.append(
405
+ "Consider breaking down complex code into smaller modules"
406
+ )
407
+ if code_analysis.warnings:
408
+ recommendations.append(
409
+ f"Review {len(code_analysis.warnings)} warnings before deployment"
410
+ )
411
+ if not signature_valid:
412
+ recommendations.append("Sign the plugin for production use")
413
+
414
+ # Determine trust level
415
+ trust_level = self._determine_trust_level(
416
+ code_analysis, signature_valid, signature_count
417
+ )
418
+
419
+ # Check sandbox compatibility
420
+ can_run_in_sandbox = self._check_sandbox_compatible(code, code_analysis)
421
+
422
+ # Determine if safe
423
+ is_safe = (
424
+ (code_analysis is None or code_analysis.is_safe)
425
+ and (not self.policy or not self.policy.require_signature or signature_valid)
426
+ )
427
+
428
+ return SecurityReport(
429
+ plugin_id=plugin_id,
430
+ analyzed_at=datetime.utcnow(),
431
+ trust_level=trust_level,
432
+ is_safe=is_safe,
433
+ can_run_in_sandbox=can_run_in_sandbox,
434
+ code_analysis=code_analysis,
435
+ signature_valid=signature_valid,
436
+ signature_count=signature_count,
437
+ required_permissions=required_permissions,
438
+ code_hash=code_hash,
439
+ recommendations=recommendations,
440
+ )
441
+
442
+ def _determine_trust_level(
443
+ self,
444
+ code_analysis: CodeAnalysisResult | None,
445
+ signature_valid: bool,
446
+ signature_count: int,
447
+ ) -> TrustLevel:
448
+ """Determine trust level based on analysis."""
449
+ if signature_valid:
450
+ min_sigs = self.policy.min_signatures if self.policy else 1
451
+ if signature_count >= min_sigs:
452
+ if code_analysis and code_analysis.is_safe:
453
+ return TrustLevel.TRUSTED
454
+ return TrustLevel.VERIFIED
455
+
456
+ if code_analysis:
457
+ if code_analysis.issues:
458
+ return TrustLevel.SANDBOXED
459
+ if code_analysis.warnings:
460
+ return TrustLevel.UNVERIFIED
461
+
462
+ return TrustLevel.UNVERIFIED
463
+
464
+ def _check_sandbox_compatible(
465
+ self,
466
+ code: str | None,
467
+ analysis: CodeAnalysisResult | None,
468
+ ) -> bool:
469
+ """Check if code can run in sandbox."""
470
+ if not code:
471
+ return True
472
+
473
+ if analysis and analysis.blocked_constructs:
474
+ return False
475
+
476
+ # Check for patterns that won't work in sandbox
477
+ sandbox_breaking_patterns = [
478
+ "__import__",
479
+ "importlib",
480
+ "sys.modules",
481
+ "globals()",
482
+ "locals()",
483
+ "__class__.__bases__",
484
+ "__subclasses__",
485
+ "ctypes",
486
+ "cffi",
487
+ ]
488
+
489
+ for pattern in sandbox_breaking_patterns:
490
+ if pattern in code:
491
+ return False
492
+
493
+ return True
494
+
495
+ def validate_for_policy(
496
+ self,
497
+ report: SecurityReport,
498
+ policy: SecurityPolicy | None = None,
499
+ ) -> tuple[bool, list[str]]:
500
+ """Validate a security report against a policy.
501
+
502
+ Args:
503
+ report: Security report to validate.
504
+ policy: Policy to validate against (uses self.policy if None).
505
+
506
+ Returns:
507
+ Tuple of (passes_policy, list of violations).
508
+ """
509
+ policy = policy or self.policy
510
+ if not policy:
511
+ return True, []
512
+
513
+ violations: list[str] = []
514
+
515
+ # Check signature requirement
516
+ if policy.require_signature and not report.signature_valid:
517
+ violations.append("Plugin requires valid signature")
518
+
519
+ # Check minimum signatures
520
+ if report.signature_count < policy.min_signatures:
521
+ violations.append(
522
+ f"Insufficient signatures: {report.signature_count} < {policy.min_signatures}"
523
+ )
524
+
525
+ # Check if sandbox required but incompatible
526
+ if policy.isolation_level != "none" and not report.can_run_in_sandbox:
527
+ violations.append("Plugin not compatible with required isolation level")
528
+
529
+ # Check code analysis
530
+ if report.code_analysis and report.code_analysis.issues:
531
+ violations.append(
532
+ f"Code analysis found {len(report.code_analysis.issues)} critical issues"
533
+ )
534
+
535
+ return len(violations) == 0, violations