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.
Files changed (91) hide show
  1. pybinaryguard/__init__.py +78 -0
  2. pybinaryguard/__main__.py +7 -0
  3. pybinaryguard/_compat/__init__.py +0 -0
  4. pybinaryguard/agent/__init__.py +63 -0
  5. pybinaryguard/agent/guard.py +232 -0
  6. pybinaryguard/agent/recommender.py +283 -0
  7. pybinaryguard/agent/schema.py +200 -0
  8. pybinaryguard/agent/simulator.py +430 -0
  9. pybinaryguard/agent/tool_interface.py +474 -0
  10. pybinaryguard/analyzers/__init__.py +209 -0
  11. pybinaryguard/analyzers/base.py +40 -0
  12. pybinaryguard/analyzers/dependency_analyzer.py +336 -0
  13. pybinaryguard/analyzers/elf_analyzer.py +754 -0
  14. pybinaryguard/analyzers/symbol_analyzer.py +280 -0
  15. pybinaryguard/analyzers/wheel_analyzer.py +308 -0
  16. pybinaryguard/cli/__init__.py +5 -0
  17. pybinaryguard/cli/commands.py +414 -0
  18. pybinaryguard/cli/formatters.py +720 -0
  19. pybinaryguard/cli/main.py +250 -0
  20. pybinaryguard/diagnostics/__init__.py +13 -0
  21. pybinaryguard/diagnostics/explainer.py +356 -0
  22. pybinaryguard/diagnostics/findings.py +146 -0
  23. pybinaryguard/diagnostics/suggestions.py +508 -0
  24. pybinaryguard/frameworks/__init__.py +20 -0
  25. pybinaryguard/frameworks/onnxruntime.py +214 -0
  26. pybinaryguard/frameworks/pytorch.py +223 -0
  27. pybinaryguard/frameworks/tensorflow.py +266 -0
  28. pybinaryguard/frameworks/tensorrt.py +189 -0
  29. pybinaryguard/models/__init__.py +19 -0
  30. pybinaryguard/models/enums.py +102 -0
  31. pybinaryguard/models/finding.py +109 -0
  32. pybinaryguard/models/package.py +121 -0
  33. pybinaryguard/models/system.py +118 -0
  34. pybinaryguard/plugins/__init__.py +19 -0
  35. pybinaryguard/plugins/contrib/__init__.py +7 -0
  36. pybinaryguard/plugins/contrib/gstreamer.py +109 -0
  37. pybinaryguard/plugins/contrib/jetson.py +385 -0
  38. pybinaryguard/plugins/contrib/opencv.py +306 -0
  39. pybinaryguard/plugins/contrib/tensorrt.py +426 -0
  40. pybinaryguard/plugins/hooks.py +273 -0
  41. pybinaryguard/plugins/loader.py +190 -0
  42. pybinaryguard/predictor/__init__.py +23 -0
  43. pybinaryguard/predictor/dependency_graph.py +226 -0
  44. pybinaryguard/predictor/linker_simulator.py +161 -0
  45. pybinaryguard/predictor/predictor.py +144 -0
  46. pybinaryguard/predictor/resolver.py +282 -0
  47. pybinaryguard/probes/__init__.py +73 -0
  48. pybinaryguard/probes/base.py +45 -0
  49. pybinaryguard/probes/board_probe.py +248 -0
  50. pybinaryguard/probes/cpu_probe.py +176 -0
  51. pybinaryguard/probes/glibc_probe.py +215 -0
  52. pybinaryguard/probes/gpu_probe.py +430 -0
  53. pybinaryguard/probes/library_probe.py +132 -0
  54. pybinaryguard/probes/os_probe.py +227 -0
  55. pybinaryguard/probes/python_probe.py +135 -0
  56. pybinaryguard/probes/toolchain_probe.py +89 -0
  57. pybinaryguard/probes/venv_probe.py +111 -0
  58. pybinaryguard/profiles/__init__.py +12 -0
  59. pybinaryguard/profiles/engine.py +343 -0
  60. pybinaryguard/rules/__init__.py +24 -0
  61. pybinaryguard/rules/base.py +57 -0
  62. pybinaryguard/rules/builtin/__init__.py +136 -0
  63. pybinaryguard/rules/builtin/arch_rules.py +86 -0
  64. pybinaryguard/rules/builtin/board_profile_rules.py +282 -0
  65. pybinaryguard/rules/builtin/container_rules.py +170 -0
  66. pybinaryguard/rules/builtin/cpu_rules.py +204 -0
  67. pybinaryguard/rules/builtin/cuda_rules.py +760 -0
  68. pybinaryguard/rules/builtin/dependency_rules.py +278 -0
  69. pybinaryguard/rules/builtin/framework_rules.py +252 -0
  70. pybinaryguard/rules/builtin/glibc_rules.py +320 -0
  71. pybinaryguard/rules/builtin/numpy_rules.py +110 -0
  72. pybinaryguard/rules/builtin/predictive_rules.py +165 -0
  73. pybinaryguard/rules/builtin/python_abi_rules.py +259 -0
  74. pybinaryguard/rules/builtin/source_build_rules.py +150 -0
  75. pybinaryguard/rules/builtin/venv_rules.py +159 -0
  76. pybinaryguard/rules/engine.py +123 -0
  77. pybinaryguard/scanner.py +904 -0
  78. pybinaryguard/scoring/__init__.py +19 -0
  79. pybinaryguard/scoring/engine.py +416 -0
  80. pybinaryguard/snapshot/__init__.py +20 -0
  81. pybinaryguard/snapshot/generator.py +168 -0
  82. pybinaryguard/snapshot/lockfile.py +152 -0
  83. pybinaryguard/snapshot/verifier.py +315 -0
  84. pybinaryguard/validators/__init__.py +7 -0
  85. pybinaryguard/validators/import_validator.py +231 -0
  86. pybinaryguard-1.0.0.dist-info/METADATA +876 -0
  87. pybinaryguard-1.0.0.dist-info/RECORD +91 -0
  88. pybinaryguard-1.0.0.dist-info/WHEEL +5 -0
  89. pybinaryguard-1.0.0.dist-info/entry_points.txt +8 -0
  90. pybinaryguard-1.0.0.dist-info/licenses/LICENSE +21 -0
  91. 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(".", "_")