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.
- truthound_dashboard/api/alerts.py +258 -0
- truthound_dashboard/api/anomaly.py +1302 -0
- truthound_dashboard/api/cross_alerts.py +352 -0
- truthound_dashboard/api/deps.py +143 -0
- truthound_dashboard/api/drift_monitor.py +540 -0
- truthound_dashboard/api/lineage.py +1151 -0
- truthound_dashboard/api/maintenance.py +363 -0
- truthound_dashboard/api/middleware.py +373 -1
- truthound_dashboard/api/model_monitoring.py +805 -0
- truthound_dashboard/api/notifications_advanced.py +2452 -0
- truthound_dashboard/api/plugins.py +2096 -0
- truthound_dashboard/api/profile.py +211 -14
- truthound_dashboard/api/reports.py +853 -0
- truthound_dashboard/api/router.py +147 -0
- truthound_dashboard/api/rule_suggestions.py +310 -0
- truthound_dashboard/api/schema_evolution.py +231 -0
- truthound_dashboard/api/sources.py +47 -3
- truthound_dashboard/api/triggers.py +190 -0
- truthound_dashboard/api/validations.py +13 -0
- truthound_dashboard/api/validators.py +333 -4
- truthound_dashboard/api/versioning.py +309 -0
- truthound_dashboard/api/websocket.py +301 -0
- truthound_dashboard/core/__init__.py +27 -0
- truthound_dashboard/core/anomaly.py +1395 -0
- truthound_dashboard/core/anomaly_explainer.py +633 -0
- truthound_dashboard/core/cache.py +206 -0
- truthound_dashboard/core/cached_services.py +422 -0
- truthound_dashboard/core/charts.py +352 -0
- truthound_dashboard/core/connections.py +1069 -42
- truthound_dashboard/core/cross_alerts.py +837 -0
- truthound_dashboard/core/drift_monitor.py +1477 -0
- truthound_dashboard/core/drift_sampling.py +669 -0
- truthound_dashboard/core/i18n/__init__.py +42 -0
- truthound_dashboard/core/i18n/detector.py +173 -0
- truthound_dashboard/core/i18n/messages.py +564 -0
- truthound_dashboard/core/lineage.py +971 -0
- truthound_dashboard/core/maintenance.py +443 -5
- truthound_dashboard/core/model_monitoring.py +1043 -0
- truthound_dashboard/core/notifications/channels.py +1020 -1
- truthound_dashboard/core/notifications/deduplication/__init__.py +143 -0
- truthound_dashboard/core/notifications/deduplication/policies.py +274 -0
- truthound_dashboard/core/notifications/deduplication/service.py +400 -0
- truthound_dashboard/core/notifications/deduplication/stores.py +2365 -0
- truthound_dashboard/core/notifications/deduplication/strategies.py +422 -0
- truthound_dashboard/core/notifications/dispatcher.py +43 -0
- truthound_dashboard/core/notifications/escalation/__init__.py +149 -0
- truthound_dashboard/core/notifications/escalation/backends.py +1384 -0
- truthound_dashboard/core/notifications/escalation/engine.py +429 -0
- truthound_dashboard/core/notifications/escalation/models.py +336 -0
- truthound_dashboard/core/notifications/escalation/scheduler.py +1187 -0
- truthound_dashboard/core/notifications/escalation/state_machine.py +330 -0
- truthound_dashboard/core/notifications/escalation/stores.py +2896 -0
- truthound_dashboard/core/notifications/events.py +49 -0
- truthound_dashboard/core/notifications/metrics/__init__.py +115 -0
- truthound_dashboard/core/notifications/metrics/base.py +528 -0
- truthound_dashboard/core/notifications/metrics/collectors.py +583 -0
- truthound_dashboard/core/notifications/routing/__init__.py +169 -0
- truthound_dashboard/core/notifications/routing/combinators.py +184 -0
- truthound_dashboard/core/notifications/routing/config.py +375 -0
- truthound_dashboard/core/notifications/routing/config_parser.py +867 -0
- truthound_dashboard/core/notifications/routing/engine.py +382 -0
- truthound_dashboard/core/notifications/routing/expression_engine.py +1269 -0
- truthound_dashboard/core/notifications/routing/jinja2_engine.py +774 -0
- truthound_dashboard/core/notifications/routing/rules.py +625 -0
- truthound_dashboard/core/notifications/routing/validator.py +678 -0
- truthound_dashboard/core/notifications/service.py +2 -0
- truthound_dashboard/core/notifications/stats_aggregator.py +850 -0
- truthound_dashboard/core/notifications/throttling/__init__.py +83 -0
- truthound_dashboard/core/notifications/throttling/builder.py +311 -0
- truthound_dashboard/core/notifications/throttling/stores.py +1859 -0
- truthound_dashboard/core/notifications/throttling/throttlers.py +633 -0
- truthound_dashboard/core/openlineage.py +1028 -0
- truthound_dashboard/core/plugins/__init__.py +39 -0
- truthound_dashboard/core/plugins/docs/__init__.py +39 -0
- truthound_dashboard/core/plugins/docs/extractor.py +703 -0
- truthound_dashboard/core/plugins/docs/renderers.py +804 -0
- truthound_dashboard/core/plugins/hooks/__init__.py +63 -0
- truthound_dashboard/core/plugins/hooks/decorators.py +367 -0
- truthound_dashboard/core/plugins/hooks/manager.py +403 -0
- truthound_dashboard/core/plugins/hooks/protocols.py +265 -0
- truthound_dashboard/core/plugins/lifecycle/__init__.py +41 -0
- truthound_dashboard/core/plugins/lifecycle/hot_reload.py +584 -0
- truthound_dashboard/core/plugins/lifecycle/machine.py +419 -0
- truthound_dashboard/core/plugins/lifecycle/states.py +266 -0
- truthound_dashboard/core/plugins/loader.py +504 -0
- truthound_dashboard/core/plugins/registry.py +810 -0
- truthound_dashboard/core/plugins/reporter_executor.py +588 -0
- truthound_dashboard/core/plugins/sandbox/__init__.py +59 -0
- truthound_dashboard/core/plugins/sandbox/code_validator.py +243 -0
- truthound_dashboard/core/plugins/sandbox/engines.py +770 -0
- truthound_dashboard/core/plugins/sandbox/protocols.py +194 -0
- truthound_dashboard/core/plugins/sandbox.py +617 -0
- truthound_dashboard/core/plugins/security/__init__.py +68 -0
- truthound_dashboard/core/plugins/security/analyzer.py +535 -0
- truthound_dashboard/core/plugins/security/policies.py +311 -0
- truthound_dashboard/core/plugins/security/protocols.py +296 -0
- truthound_dashboard/core/plugins/security/signing.py +842 -0
- truthound_dashboard/core/plugins/security.py +446 -0
- truthound_dashboard/core/plugins/validator_executor.py +401 -0
- truthound_dashboard/core/plugins/versioning/__init__.py +51 -0
- truthound_dashboard/core/plugins/versioning/constraints.py +377 -0
- truthound_dashboard/core/plugins/versioning/dependencies.py +541 -0
- truthound_dashboard/core/plugins/versioning/semver.py +266 -0
- truthound_dashboard/core/profile_comparison.py +601 -0
- truthound_dashboard/core/report_history.py +570 -0
- truthound_dashboard/core/reporters/__init__.py +57 -0
- truthound_dashboard/core/reporters/base.py +296 -0
- truthound_dashboard/core/reporters/csv_reporter.py +155 -0
- truthound_dashboard/core/reporters/html_reporter.py +598 -0
- truthound_dashboard/core/reporters/i18n/__init__.py +65 -0
- truthound_dashboard/core/reporters/i18n/base.py +494 -0
- truthound_dashboard/core/reporters/i18n/catalogs.py +930 -0
- truthound_dashboard/core/reporters/json_reporter.py +160 -0
- truthound_dashboard/core/reporters/junit_reporter.py +233 -0
- truthound_dashboard/core/reporters/markdown_reporter.py +207 -0
- truthound_dashboard/core/reporters/pdf_reporter.py +209 -0
- truthound_dashboard/core/reporters/registry.py +272 -0
- truthound_dashboard/core/rule_generator.py +2088 -0
- truthound_dashboard/core/scheduler.py +822 -12
- truthound_dashboard/core/schema_evolution.py +858 -0
- truthound_dashboard/core/services.py +152 -9
- truthound_dashboard/core/statistics.py +718 -0
- truthound_dashboard/core/streaming_anomaly.py +883 -0
- truthound_dashboard/core/triggers/__init__.py +45 -0
- truthound_dashboard/core/triggers/base.py +226 -0
- truthound_dashboard/core/triggers/evaluators.py +609 -0
- truthound_dashboard/core/triggers/factory.py +363 -0
- truthound_dashboard/core/unified_alerts.py +870 -0
- truthound_dashboard/core/validation_limits.py +509 -0
- truthound_dashboard/core/versioning.py +709 -0
- truthound_dashboard/core/websocket/__init__.py +59 -0
- truthound_dashboard/core/websocket/manager.py +512 -0
- truthound_dashboard/core/websocket/messages.py +130 -0
- truthound_dashboard/db/__init__.py +30 -0
- truthound_dashboard/db/models.py +3375 -3
- truthound_dashboard/main.py +22 -0
- truthound_dashboard/schemas/__init__.py +396 -1
- truthound_dashboard/schemas/anomaly.py +1258 -0
- truthound_dashboard/schemas/base.py +4 -0
- truthound_dashboard/schemas/cross_alerts.py +334 -0
- truthound_dashboard/schemas/drift_monitor.py +890 -0
- truthound_dashboard/schemas/lineage.py +428 -0
- truthound_dashboard/schemas/maintenance.py +154 -0
- truthound_dashboard/schemas/model_monitoring.py +374 -0
- truthound_dashboard/schemas/notifications_advanced.py +1363 -0
- truthound_dashboard/schemas/openlineage.py +704 -0
- truthound_dashboard/schemas/plugins.py +1293 -0
- truthound_dashboard/schemas/profile.py +420 -34
- truthound_dashboard/schemas/profile_comparison.py +242 -0
- truthound_dashboard/schemas/reports.py +285 -0
- truthound_dashboard/schemas/rule_suggestion.py +434 -0
- truthound_dashboard/schemas/schema_evolution.py +164 -0
- truthound_dashboard/schemas/source.py +117 -2
- truthound_dashboard/schemas/triggers.py +511 -0
- truthound_dashboard/schemas/unified_alerts.py +223 -0
- truthound_dashboard/schemas/validation.py +25 -1
- truthound_dashboard/schemas/validators/__init__.py +11 -0
- truthound_dashboard/schemas/validators/base.py +151 -0
- truthound_dashboard/schemas/versioning.py +152 -0
- truthound_dashboard/static/index.html +2 -2
- {truthound_dashboard-1.3.1.dist-info → truthound_dashboard-1.4.0.dist-info}/METADATA +142 -22
- truthound_dashboard-1.4.0.dist-info/RECORD +239 -0
- truthound_dashboard/static/assets/index-BZG20KuF.js +0 -586
- truthound_dashboard/static/assets/index-D_HyZ3pb.css +0 -1
- truthound_dashboard/static/assets/unmerged_dictionaries-CtpqQBm0.js +0 -1
- truthound_dashboard-1.3.1.dist-info/RECORD +0 -110
- {truthound_dashboard-1.3.1.dist-info → truthound_dashboard-1.4.0.dist-info}/WHEEL +0 -0
- {truthound_dashboard-1.3.1.dist-info → truthound_dashboard-1.4.0.dist-info}/entry_points.txt +0 -0
- {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
|