pybinaryguard 1.0.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.
- pybinaryguard/__init__.py +78 -0
- pybinaryguard/__main__.py +7 -0
- pybinaryguard/_compat/__init__.py +0 -0
- pybinaryguard/agent/__init__.py +63 -0
- pybinaryguard/agent/guard.py +232 -0
- pybinaryguard/agent/recommender.py +283 -0
- pybinaryguard/agent/schema.py +200 -0
- pybinaryguard/agent/simulator.py +430 -0
- pybinaryguard/agent/tool_interface.py +474 -0
- pybinaryguard/analyzers/__init__.py +209 -0
- pybinaryguard/analyzers/base.py +40 -0
- pybinaryguard/analyzers/dependency_analyzer.py +336 -0
- pybinaryguard/analyzers/elf_analyzer.py +754 -0
- pybinaryguard/analyzers/symbol_analyzer.py +280 -0
- pybinaryguard/analyzers/wheel_analyzer.py +308 -0
- pybinaryguard/cli/__init__.py +5 -0
- pybinaryguard/cli/commands.py +414 -0
- pybinaryguard/cli/formatters.py +720 -0
- pybinaryguard/cli/main.py +250 -0
- pybinaryguard/diagnostics/__init__.py +13 -0
- pybinaryguard/diagnostics/explainer.py +356 -0
- pybinaryguard/diagnostics/findings.py +146 -0
- pybinaryguard/diagnostics/suggestions.py +508 -0
- pybinaryguard/frameworks/__init__.py +20 -0
- pybinaryguard/frameworks/onnxruntime.py +214 -0
- pybinaryguard/frameworks/pytorch.py +223 -0
- pybinaryguard/frameworks/tensorflow.py +266 -0
- pybinaryguard/frameworks/tensorrt.py +189 -0
- pybinaryguard/models/__init__.py +19 -0
- pybinaryguard/models/enums.py +102 -0
- pybinaryguard/models/finding.py +109 -0
- pybinaryguard/models/package.py +121 -0
- pybinaryguard/models/system.py +118 -0
- pybinaryguard/plugins/__init__.py +19 -0
- pybinaryguard/plugins/contrib/__init__.py +7 -0
- pybinaryguard/plugins/contrib/gstreamer.py +109 -0
- pybinaryguard/plugins/contrib/jetson.py +385 -0
- pybinaryguard/plugins/contrib/opencv.py +306 -0
- pybinaryguard/plugins/contrib/tensorrt.py +426 -0
- pybinaryguard/plugins/hooks.py +273 -0
- pybinaryguard/plugins/loader.py +190 -0
- pybinaryguard/predictor/__init__.py +23 -0
- pybinaryguard/predictor/dependency_graph.py +226 -0
- pybinaryguard/predictor/linker_simulator.py +161 -0
- pybinaryguard/predictor/predictor.py +144 -0
- pybinaryguard/predictor/resolver.py +282 -0
- pybinaryguard/probes/__init__.py +73 -0
- pybinaryguard/probes/base.py +45 -0
- pybinaryguard/probes/board_probe.py +248 -0
- pybinaryguard/probes/cpu_probe.py +176 -0
- pybinaryguard/probes/glibc_probe.py +215 -0
- pybinaryguard/probes/gpu_probe.py +430 -0
- pybinaryguard/probes/library_probe.py +132 -0
- pybinaryguard/probes/os_probe.py +227 -0
- pybinaryguard/probes/python_probe.py +135 -0
- pybinaryguard/probes/toolchain_probe.py +89 -0
- pybinaryguard/probes/venv_probe.py +111 -0
- pybinaryguard/profiles/__init__.py +12 -0
- pybinaryguard/profiles/engine.py +343 -0
- pybinaryguard/rules/__init__.py +24 -0
- pybinaryguard/rules/base.py +57 -0
- pybinaryguard/rules/builtin/__init__.py +136 -0
- pybinaryguard/rules/builtin/arch_rules.py +86 -0
- pybinaryguard/rules/builtin/board_profile_rules.py +282 -0
- pybinaryguard/rules/builtin/container_rules.py +170 -0
- pybinaryguard/rules/builtin/cpu_rules.py +204 -0
- pybinaryguard/rules/builtin/cuda_rules.py +760 -0
- pybinaryguard/rules/builtin/dependency_rules.py +278 -0
- pybinaryguard/rules/builtin/framework_rules.py +252 -0
- pybinaryguard/rules/builtin/glibc_rules.py +320 -0
- pybinaryguard/rules/builtin/numpy_rules.py +110 -0
- pybinaryguard/rules/builtin/predictive_rules.py +165 -0
- pybinaryguard/rules/builtin/python_abi_rules.py +259 -0
- pybinaryguard/rules/builtin/source_build_rules.py +150 -0
- pybinaryguard/rules/builtin/venv_rules.py +159 -0
- pybinaryguard/rules/engine.py +123 -0
- pybinaryguard/scanner.py +904 -0
- pybinaryguard/scoring/__init__.py +19 -0
- pybinaryguard/scoring/engine.py +416 -0
- pybinaryguard/snapshot/__init__.py +20 -0
- pybinaryguard/snapshot/generator.py +168 -0
- pybinaryguard/snapshot/lockfile.py +152 -0
- pybinaryguard/snapshot/verifier.py +315 -0
- pybinaryguard/validators/__init__.py +7 -0
- pybinaryguard/validators/import_validator.py +231 -0
- pybinaryguard-1.0.0.dist-info/METADATA +876 -0
- pybinaryguard-1.0.0.dist-info/RECORD +91 -0
- pybinaryguard-1.0.0.dist-info/WHEEL +5 -0
- pybinaryguard-1.0.0.dist-info/entry_points.txt +8 -0
- pybinaryguard-1.0.0.dist-info/licenses/LICENSE +21 -0
- pybinaryguard-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
"""Import validator — actually tries importing packages to catch real failures.
|
|
2
|
+
|
|
3
|
+
This is the nuclear option: instead of predicting failures from metadata,
|
|
4
|
+
we import each package in an isolated subprocess and capture the exact error.
|
|
5
|
+
This catches everything — GLIBC mismatches, missing .so files, CUDA errors,
|
|
6
|
+
illegal instructions, segfaults — because we're running the actual code.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
import subprocess
|
|
13
|
+
import sys
|
|
14
|
+
import time
|
|
15
|
+
from dataclasses import dataclass, field
|
|
16
|
+
from typing import Dict, List, Optional, Tuple
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class ImportTestResult:
|
|
21
|
+
"""Result of attempting to import a single package."""
|
|
22
|
+
|
|
23
|
+
package_name: str
|
|
24
|
+
top_level_name: str
|
|
25
|
+
success: bool
|
|
26
|
+
error_type: Optional[str] = None # "ImportError", "OSError", "Signal", etc.
|
|
27
|
+
error_message: Optional[str] = None
|
|
28
|
+
category: Optional[str] = None # "glibc_mismatch", "missing_lib", etc.
|
|
29
|
+
exit_code: int = 0
|
|
30
|
+
duration_ms: float = 0.0
|
|
31
|
+
|
|
32
|
+
def as_dict(self) -> Dict[str, object]:
|
|
33
|
+
result: Dict[str, object] = {
|
|
34
|
+
"package": self.package_name,
|
|
35
|
+
"import_name": self.top_level_name,
|
|
36
|
+
"success": self.success,
|
|
37
|
+
}
|
|
38
|
+
if not self.success:
|
|
39
|
+
result["error_type"] = self.error_type
|
|
40
|
+
result["error_message"] = self.error_message
|
|
41
|
+
if self.category:
|
|
42
|
+
result["category"] = self.category
|
|
43
|
+
result["exit_code"] = self.exit_code
|
|
44
|
+
result["duration_ms"] = round(self.duration_ms, 1)
|
|
45
|
+
return result
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class ImportValidator:
|
|
49
|
+
"""Test imports by actually running them in isolated subprocesses.
|
|
50
|
+
|
|
51
|
+
Each import test runs in a fresh Python subprocess with a timeout,
|
|
52
|
+
capturing stdout/stderr for error analysis. Signal-based crashes
|
|
53
|
+
(SIGILL, SIGSEGV) are detected from the exit code.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
def __init__(
|
|
57
|
+
self,
|
|
58
|
+
timeout: float = 10.0,
|
|
59
|
+
python_executable: Optional[str] = None,
|
|
60
|
+
) -> None:
|
|
61
|
+
self._timeout = timeout
|
|
62
|
+
self._python = python_executable or sys.executable
|
|
63
|
+
|
|
64
|
+
def test_import(self, package_name: str, top_level: Optional[str] = None) -> ImportTestResult:
|
|
65
|
+
"""Test importing a single package in an isolated subprocess.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
package_name: The pip package name.
|
|
69
|
+
top_level: The actual importable module name (if different from package_name).
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
ImportTestResult with success/failure details.
|
|
73
|
+
"""
|
|
74
|
+
import_name = top_level or package_name.replace("-", "_").replace(".", "_")
|
|
75
|
+
|
|
76
|
+
# Build the test script
|
|
77
|
+
script = (
|
|
78
|
+
f"import sys; "
|
|
79
|
+
f"try:\n"
|
|
80
|
+
f" import {import_name}\n"
|
|
81
|
+
f" print('OK')\n"
|
|
82
|
+
f"except ImportError as e:\n"
|
|
83
|
+
f" print(f'ImportError: {{e}}', file=sys.stderr)\n"
|
|
84
|
+
f" sys.exit(10)\n"
|
|
85
|
+
f"except OSError as e:\n"
|
|
86
|
+
f" print(f'OSError: {{e}}', file=sys.stderr)\n"
|
|
87
|
+
f" sys.exit(11)\n"
|
|
88
|
+
f"except Exception as e:\n"
|
|
89
|
+
f" print(f'{{type(e).__name__}}: {{e}}', file=sys.stderr)\n"
|
|
90
|
+
f" sys.exit(12)\n"
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
start = time.monotonic()
|
|
94
|
+
|
|
95
|
+
try:
|
|
96
|
+
result = subprocess.run(
|
|
97
|
+
[self._python, "-c", script],
|
|
98
|
+
capture_output=True,
|
|
99
|
+
text=True,
|
|
100
|
+
timeout=self._timeout,
|
|
101
|
+
env={**os.environ, "PYTHONDONTWRITEBYTECODE": "1"},
|
|
102
|
+
)
|
|
103
|
+
duration_ms = (time.monotonic() - start) * 1000
|
|
104
|
+
|
|
105
|
+
if result.returncode == 0:
|
|
106
|
+
return ImportTestResult(
|
|
107
|
+
package_name=package_name,
|
|
108
|
+
top_level_name=import_name,
|
|
109
|
+
success=True,
|
|
110
|
+
duration_ms=duration_ms,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
# Parse error
|
|
114
|
+
stderr = result.stderr.strip()
|
|
115
|
+
error_type, error_msg, category = self._classify_error(
|
|
116
|
+
result.returncode, stderr
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
return ImportTestResult(
|
|
120
|
+
package_name=package_name,
|
|
121
|
+
top_level_name=import_name,
|
|
122
|
+
success=False,
|
|
123
|
+
error_type=error_type,
|
|
124
|
+
error_message=error_msg,
|
|
125
|
+
category=category,
|
|
126
|
+
exit_code=result.returncode,
|
|
127
|
+
duration_ms=duration_ms,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
except subprocess.TimeoutExpired:
|
|
131
|
+
duration_ms = (time.monotonic() - start) * 1000
|
|
132
|
+
return ImportTestResult(
|
|
133
|
+
package_name=package_name,
|
|
134
|
+
top_level_name=import_name,
|
|
135
|
+
success=False,
|
|
136
|
+
error_type="Timeout",
|
|
137
|
+
error_message=f"Import timed out after {self._timeout}s",
|
|
138
|
+
category="timeout",
|
|
139
|
+
exit_code=-1,
|
|
140
|
+
duration_ms=duration_ms,
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
def test_packages(
|
|
144
|
+
self,
|
|
145
|
+
packages: List[Tuple[str, Optional[str]]],
|
|
146
|
+
) -> List[ImportTestResult]:
|
|
147
|
+
"""Test importing multiple packages sequentially.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
packages: List of (package_name, top_level_name) tuples.
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
List of ImportTestResult for each package.
|
|
154
|
+
"""
|
|
155
|
+
results = []
|
|
156
|
+
for pkg_name, top_level in packages:
|
|
157
|
+
results.append(self.test_import(pkg_name, top_level))
|
|
158
|
+
return results
|
|
159
|
+
|
|
160
|
+
@staticmethod
|
|
161
|
+
def _classify_error(
|
|
162
|
+
exit_code: int, stderr: str
|
|
163
|
+
) -> Tuple[str, str, Optional[str]]:
|
|
164
|
+
"""Classify the error from exit code and stderr output."""
|
|
165
|
+
stderr_lower = stderr.lower()
|
|
166
|
+
|
|
167
|
+
# Signal-based crashes (negative exit codes or 128+signal on Linux)
|
|
168
|
+
signal_codes = {
|
|
169
|
+
-4: "illegal_instruction", 132: "illegal_instruction", # SIGILL
|
|
170
|
+
-11: "segfault", 139: "segfault", # SIGSEGV
|
|
171
|
+
-6: "abort", 134: "abort", # SIGABRT
|
|
172
|
+
-9: "killed", 137: "killed", # SIGKILL
|
|
173
|
+
}
|
|
174
|
+
if exit_code in signal_codes:
|
|
175
|
+
category = signal_codes[exit_code]
|
|
176
|
+
messages = {
|
|
177
|
+
"illegal_instruction": (
|
|
178
|
+
"Illegal instruction (SIGILL) — binary compiled for "
|
|
179
|
+
"incompatible CPU architecture or instruction set"
|
|
180
|
+
),
|
|
181
|
+
"segfault": "Segmentation fault (SIGSEGV) — binary memory access violation",
|
|
182
|
+
"abort": "Aborted (SIGABRT) — binary assertion or abort() call",
|
|
183
|
+
"killed": "Killed (SIGKILL) — process was killed (out of memory?)",
|
|
184
|
+
}
|
|
185
|
+
return ("Signal", messages.get(category, f"Signal {abs(exit_code)}"), category)
|
|
186
|
+
if exit_code < 0 or exit_code > 128:
|
|
187
|
+
if exit_code < 0:
|
|
188
|
+
sig = abs(exit_code)
|
|
189
|
+
else:
|
|
190
|
+
sig = exit_code - 128
|
|
191
|
+
if sig > 0 and sig < 32:
|
|
192
|
+
return ("Signal", f"Process killed by signal {sig}", "signal")
|
|
193
|
+
|
|
194
|
+
# ImportError (exit code 10)
|
|
195
|
+
if exit_code == 10:
|
|
196
|
+
if "glibc" in stderr_lower or "version" in stderr_lower:
|
|
197
|
+
return ("ImportError", stderr, "glibc_mismatch")
|
|
198
|
+
elif "libcuda" in stderr_lower or "cuda" in stderr_lower:
|
|
199
|
+
return ("ImportError", stderr, "cuda_missing")
|
|
200
|
+
elif "cannot open shared object" in stderr_lower:
|
|
201
|
+
return ("ImportError", stderr, "missing_shared_library")
|
|
202
|
+
elif "undefined symbol" in stderr_lower:
|
|
203
|
+
return ("ImportError", stderr, "undefined_symbol")
|
|
204
|
+
return ("ImportError", stderr, "import_error")
|
|
205
|
+
|
|
206
|
+
# OSError (exit code 11)
|
|
207
|
+
if exit_code == 11:
|
|
208
|
+
if "cannot open shared object" in stderr_lower:
|
|
209
|
+
return ("OSError", stderr, "missing_shared_library")
|
|
210
|
+
return ("OSError", stderr, "os_error")
|
|
211
|
+
|
|
212
|
+
# Other exceptions (exit code 12)
|
|
213
|
+
if exit_code == 12:
|
|
214
|
+
return ("Exception", stderr, "runtime_error")
|
|
215
|
+
|
|
216
|
+
return ("Unknown", stderr or f"Exit code {exit_code}", None)
|
|
217
|
+
|
|
218
|
+
@staticmethod
|
|
219
|
+
def get_top_level_name(dist_info_path: str, package_name: str) -> str:
|
|
220
|
+
"""Get the importable module name from top_level.txt or package name."""
|
|
221
|
+
top_level_file = os.path.join(dist_info_path, "top_level.txt")
|
|
222
|
+
if os.path.isfile(top_level_file):
|
|
223
|
+
try:
|
|
224
|
+
with open(top_level_file, "r") as f:
|
|
225
|
+
names = [line.strip() for line in f if line.strip()]
|
|
226
|
+
if names:
|
|
227
|
+
return names[0]
|
|
228
|
+
except OSError:
|
|
229
|
+
pass
|
|
230
|
+
# Fallback: normalize package name
|
|
231
|
+
return package_name.replace("-", "_").replace(".", "_")
|