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,343 @@
|
|
|
1
|
+
"""Board profile engine for embedded device intelligence.
|
|
2
|
+
|
|
3
|
+
This module provides the core profile system that loads board-specific
|
|
4
|
+
configuration and provides intelligent recommendations based on detected hardware.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any, Dict, List, Optional
|
|
13
|
+
|
|
14
|
+
from pybinaryguard.models.system import SystemProfile
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class BrokenWheel:
|
|
19
|
+
"""Information about a known broken wheel for a specific board."""
|
|
20
|
+
|
|
21
|
+
package: str
|
|
22
|
+
versions: List[str]
|
|
23
|
+
reason: str
|
|
24
|
+
recommendation: str
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class ValidatedStack:
|
|
29
|
+
"""A validated combination of packages known to work together."""
|
|
30
|
+
|
|
31
|
+
name: str
|
|
32
|
+
packages: Dict[str, str]
|
|
33
|
+
build_opencv: bool = False
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class BoardProfile:
|
|
38
|
+
"""Comprehensive profile for a specific embedded board.
|
|
39
|
+
|
|
40
|
+
Contains hardware specifications, compatibility matrices, known issues,
|
|
41
|
+
and validated software stacks for embedded devices.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
board_id: str
|
|
45
|
+
display_name: str
|
|
46
|
+
vendor: str
|
|
47
|
+
architecture: str
|
|
48
|
+
|
|
49
|
+
# Detection patterns
|
|
50
|
+
dt_model_patterns: List[str] = field(default_factory=list)
|
|
51
|
+
tegra_version: Optional[str] = None
|
|
52
|
+
cpu_model_patterns: List[str] = field(default_factory=list)
|
|
53
|
+
revision_codes: List[str] = field(default_factory=list)
|
|
54
|
+
|
|
55
|
+
# Hardware specs
|
|
56
|
+
cuda_compute_capability: Optional[str] = None
|
|
57
|
+
max_cuda_version: Optional[str] = None
|
|
58
|
+
recommended_jetpack: Optional[str] = None
|
|
59
|
+
recommended_os: Optional[str] = None
|
|
60
|
+
gpu_model: Optional[str] = None
|
|
61
|
+
tpu: Optional[str] = None
|
|
62
|
+
ram_gb: Optional[int] = None
|
|
63
|
+
recommended_glibc: Optional[str] = None
|
|
64
|
+
kernel_version_min: Optional[str] = None
|
|
65
|
+
|
|
66
|
+
# Compatibility
|
|
67
|
+
python_versions: List[str] = field(default_factory=list)
|
|
68
|
+
manylinux_tags: List[str] = field(default_factory=list)
|
|
69
|
+
cuda_versions: List[str] = field(default_factory=list)
|
|
70
|
+
tensorrt_versions: List[str] = field(default_factory=list)
|
|
71
|
+
opencv_backends: List[str] = field(default_factory=list)
|
|
72
|
+
tflite_versions: List[str] = field(default_factory=list)
|
|
73
|
+
required_packages: List[str] = field(default_factory=list)
|
|
74
|
+
|
|
75
|
+
# Known issues
|
|
76
|
+
broken_wheels: List[BrokenWheel] = field(default_factory=list)
|
|
77
|
+
incompatible_packages: List[str] = field(default_factory=list)
|
|
78
|
+
|
|
79
|
+
# Recommendations
|
|
80
|
+
recommendations: Dict[str, Any] = field(default_factory=dict)
|
|
81
|
+
|
|
82
|
+
# Validated stacks
|
|
83
|
+
validated_stacks: List[ValidatedStack] = field(default_factory=list)
|
|
84
|
+
|
|
85
|
+
@classmethod
|
|
86
|
+
def from_dict(cls, data: Dict[str, Any]) -> BoardProfile:
|
|
87
|
+
"""Create a BoardProfile from a dictionary (loaded from JSON)."""
|
|
88
|
+
# Extract top-level fields
|
|
89
|
+
board_id = data["board_id"]
|
|
90
|
+
display_name = data["display_name"]
|
|
91
|
+
vendor = data["vendor"]
|
|
92
|
+
architecture = data["architecture"]
|
|
93
|
+
|
|
94
|
+
# Extract detection patterns
|
|
95
|
+
detection = data.get("detection", {})
|
|
96
|
+
dt_model_patterns = detection.get("dt_model_patterns", [])
|
|
97
|
+
tegra_version = detection.get("tegra_version")
|
|
98
|
+
cpu_model_patterns = detection.get("cpu_model_patterns", [])
|
|
99
|
+
revision_codes = detection.get("revision_codes", [])
|
|
100
|
+
|
|
101
|
+
# Extract specs
|
|
102
|
+
specs = data.get("specs", {})
|
|
103
|
+
cuda_compute_capability = specs.get("cuda_compute_capability")
|
|
104
|
+
max_cuda_version = specs.get("max_cuda_version")
|
|
105
|
+
recommended_jetpack = specs.get("recommended_jetpack")
|
|
106
|
+
recommended_os = specs.get("recommended_os")
|
|
107
|
+
gpu_model = specs.get("gpu_model")
|
|
108
|
+
tpu = specs.get("tpu")
|
|
109
|
+
ram_gb = specs.get("ram_gb")
|
|
110
|
+
recommended_glibc = specs.get("recommended_glibc")
|
|
111
|
+
kernel_version_min = specs.get("kernel_version_min")
|
|
112
|
+
|
|
113
|
+
# Extract compatibility
|
|
114
|
+
compat = data.get("compatibility", {})
|
|
115
|
+
python_versions = compat.get("python_versions", [])
|
|
116
|
+
manylinux_tags = compat.get("manylinux_tags", [])
|
|
117
|
+
cuda_versions = compat.get("cuda_versions", [])
|
|
118
|
+
tensorrt_versions = compat.get("tensorrt_versions", [])
|
|
119
|
+
opencv_backends = compat.get("opencv_backends", [])
|
|
120
|
+
tflite_versions = compat.get("tflite_versions", [])
|
|
121
|
+
required_packages = compat.get("required_packages", [])
|
|
122
|
+
|
|
123
|
+
# Extract known issues
|
|
124
|
+
issues = data.get("known_issues", {})
|
|
125
|
+
broken_wheels_data = issues.get("broken_wheels", [])
|
|
126
|
+
broken_wheels = [
|
|
127
|
+
BrokenWheel(
|
|
128
|
+
package=bw["package"],
|
|
129
|
+
versions=bw["versions"],
|
|
130
|
+
reason=bw["reason"],
|
|
131
|
+
recommendation=bw["recommendation"]
|
|
132
|
+
)
|
|
133
|
+
for bw in broken_wheels_data
|
|
134
|
+
]
|
|
135
|
+
incompatible_packages = issues.get("incompatible_packages", [])
|
|
136
|
+
|
|
137
|
+
# Extract recommendations
|
|
138
|
+
recommendations = data.get("recommendations", {})
|
|
139
|
+
|
|
140
|
+
# Extract validated stacks
|
|
141
|
+
stacks_data = data.get("validated_stacks", [])
|
|
142
|
+
validated_stacks = [
|
|
143
|
+
ValidatedStack(
|
|
144
|
+
name=stack["name"],
|
|
145
|
+
packages=stack["packages"],
|
|
146
|
+
build_opencv=stack.get("build_opencv", False)
|
|
147
|
+
)
|
|
148
|
+
for stack in stacks_data
|
|
149
|
+
]
|
|
150
|
+
|
|
151
|
+
return cls(
|
|
152
|
+
board_id=board_id,
|
|
153
|
+
display_name=display_name,
|
|
154
|
+
vendor=vendor,
|
|
155
|
+
architecture=architecture,
|
|
156
|
+
dt_model_patterns=dt_model_patterns,
|
|
157
|
+
tegra_version=tegra_version,
|
|
158
|
+
cpu_model_patterns=cpu_model_patterns,
|
|
159
|
+
revision_codes=revision_codes,
|
|
160
|
+
cuda_compute_capability=cuda_compute_capability,
|
|
161
|
+
max_cuda_version=max_cuda_version,
|
|
162
|
+
recommended_jetpack=recommended_jetpack,
|
|
163
|
+
recommended_os=recommended_os,
|
|
164
|
+
gpu_model=gpu_model,
|
|
165
|
+
tpu=tpu,
|
|
166
|
+
ram_gb=ram_gb,
|
|
167
|
+
recommended_glibc=recommended_glibc,
|
|
168
|
+
kernel_version_min=kernel_version_min,
|
|
169
|
+
python_versions=python_versions,
|
|
170
|
+
manylinux_tags=manylinux_tags,
|
|
171
|
+
cuda_versions=cuda_versions,
|
|
172
|
+
tensorrt_versions=tensorrt_versions,
|
|
173
|
+
opencv_backends=opencv_backends,
|
|
174
|
+
tflite_versions=tflite_versions,
|
|
175
|
+
required_packages=required_packages,
|
|
176
|
+
broken_wheels=broken_wheels,
|
|
177
|
+
incompatible_packages=incompatible_packages,
|
|
178
|
+
recommendations=recommendations,
|
|
179
|
+
validated_stacks=validated_stacks,
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
def matches_system(self, profile: SystemProfile) -> bool:
|
|
183
|
+
"""Check if this board profile matches the given system profile.
|
|
184
|
+
|
|
185
|
+
Uses getattr for board-specific fields that may not be present on
|
|
186
|
+
every SystemProfile (dt_model, tegra_release, rpi_revision are
|
|
187
|
+
populated only by the BoardProbe on embedded hardware).
|
|
188
|
+
"""
|
|
189
|
+
# Check device tree model
|
|
190
|
+
dt_model = getattr(profile, "dt_model", None) or ""
|
|
191
|
+
if dt_model:
|
|
192
|
+
for pattern in self.dt_model_patterns:
|
|
193
|
+
if pattern.lower() in dt_model.lower():
|
|
194
|
+
return True
|
|
195
|
+
|
|
196
|
+
# Check Tegra version (Jetson-specific)
|
|
197
|
+
tegra_release = getattr(profile, "tegra_release", None) or ""
|
|
198
|
+
if self.tegra_version and tegra_release:
|
|
199
|
+
if self.tegra_version in tegra_release:
|
|
200
|
+
return True
|
|
201
|
+
|
|
202
|
+
# Check CPU model
|
|
203
|
+
if profile.cpu_model:
|
|
204
|
+
for pattern in self.cpu_model_patterns:
|
|
205
|
+
if pattern.lower() in profile.cpu_model.lower():
|
|
206
|
+
# Additional architecture check
|
|
207
|
+
if profile.architecture.value == self.architecture:
|
|
208
|
+
return True
|
|
209
|
+
|
|
210
|
+
# Check revision codes (Raspberry Pi-specific)
|
|
211
|
+
rpi_revision = getattr(profile, "rpi_revision", None) or ""
|
|
212
|
+
if rpi_revision:
|
|
213
|
+
if rpi_revision.lower() in [rc.lower() for rc in self.revision_codes]:
|
|
214
|
+
return True
|
|
215
|
+
|
|
216
|
+
return False
|
|
217
|
+
|
|
218
|
+
def is_package_known_broken(self, package_name: str, version: Optional[str] = None) -> Optional[BrokenWheel]:
|
|
219
|
+
"""Check if a package version is known to be broken on this board."""
|
|
220
|
+
for broken in self.broken_wheels:
|
|
221
|
+
if broken.package.lower() == package_name.lower():
|
|
222
|
+
if version is None or "*" in broken.versions:
|
|
223
|
+
return broken
|
|
224
|
+
# Simple version matching (could be enhanced)
|
|
225
|
+
for ver_pattern in broken.versions:
|
|
226
|
+
if ver_pattern.startswith(">=") and version:
|
|
227
|
+
min_ver = ver_pattern[2:]
|
|
228
|
+
if self._version_gte(version, min_ver):
|
|
229
|
+
return broken
|
|
230
|
+
elif ver_pattern.startswith("<") and version:
|
|
231
|
+
max_ver = ver_pattern[1:]
|
|
232
|
+
if self._version_lt(version, max_ver):
|
|
233
|
+
return broken
|
|
234
|
+
elif version and ver_pattern == version:
|
|
235
|
+
return broken
|
|
236
|
+
return None
|
|
237
|
+
|
|
238
|
+
def is_package_incompatible(self, package_name: str) -> bool:
|
|
239
|
+
"""Check if a package is fundamentally incompatible with this board."""
|
|
240
|
+
for pkg in self.incompatible_packages:
|
|
241
|
+
if pkg.endswith("*"):
|
|
242
|
+
prefix = pkg[:-1]
|
|
243
|
+
if package_name.lower().startswith(prefix.lower()):
|
|
244
|
+
return True
|
|
245
|
+
elif pkg.lower() == package_name.lower():
|
|
246
|
+
return True
|
|
247
|
+
return False
|
|
248
|
+
|
|
249
|
+
@staticmethod
|
|
250
|
+
def _version_gte(v1: str, v2: str) -> bool:
|
|
251
|
+
"""Compare versions (simple numeric comparison)."""
|
|
252
|
+
try:
|
|
253
|
+
parts1 = [int(p) for p in v1.split(".")[:3]]
|
|
254
|
+
parts2 = [int(p) for p in v2.split(".")[:3]]
|
|
255
|
+
return parts1 >= parts2
|
|
256
|
+
except (ValueError, IndexError):
|
|
257
|
+
return False
|
|
258
|
+
|
|
259
|
+
@staticmethod
|
|
260
|
+
def _version_lt(v1: str, v2: str) -> bool:
|
|
261
|
+
"""Compare versions (simple numeric comparison)."""
|
|
262
|
+
try:
|
|
263
|
+
parts1 = [int(p) for p in v1.split(".")[:3]]
|
|
264
|
+
parts2 = [int(p) for p in v2.split(".")[:3]]
|
|
265
|
+
return parts1 < parts2
|
|
266
|
+
except (ValueError, IndexError):
|
|
267
|
+
return False
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
class ProfileEngine:
|
|
271
|
+
"""Engine for loading and matching board profiles."""
|
|
272
|
+
|
|
273
|
+
def __init__(self, profile_dir: Optional[Path] = None):
|
|
274
|
+
"""Initialize the profile engine.
|
|
275
|
+
|
|
276
|
+
Args:
|
|
277
|
+
profile_dir: Directory containing profile JSON files.
|
|
278
|
+
If None, uses the built-in profiles directory.
|
|
279
|
+
"""
|
|
280
|
+
if profile_dir is None:
|
|
281
|
+
# Use built-in profiles
|
|
282
|
+
profile_dir = Path(__file__).parent
|
|
283
|
+
|
|
284
|
+
self.profile_dir = Path(profile_dir)
|
|
285
|
+
self.profiles: Dict[str, BoardProfile] = {}
|
|
286
|
+
self._load_all_profiles()
|
|
287
|
+
|
|
288
|
+
def _load_all_profiles(self) -> None:
|
|
289
|
+
"""Load all board profiles from the profile directory."""
|
|
290
|
+
if not self.profile_dir.exists():
|
|
291
|
+
return
|
|
292
|
+
|
|
293
|
+
for json_file in self.profile_dir.glob("*.json"):
|
|
294
|
+
try:
|
|
295
|
+
profile = self._load_profile_file(json_file)
|
|
296
|
+
self.profiles[profile.board_id] = profile
|
|
297
|
+
except Exception:
|
|
298
|
+
# Skip invalid profiles silently
|
|
299
|
+
pass
|
|
300
|
+
|
|
301
|
+
def _load_profile_file(self, path: Path) -> BoardProfile:
|
|
302
|
+
"""Load a single profile from a JSON file."""
|
|
303
|
+
with open(path, "r") as f:
|
|
304
|
+
data = json.load(f)
|
|
305
|
+
return BoardProfile.from_dict(data)
|
|
306
|
+
|
|
307
|
+
def match_profile(self, system: SystemProfile) -> Optional[BoardProfile]:
|
|
308
|
+
"""Find the board profile that matches the given system profile."""
|
|
309
|
+
for profile in self.profiles.values():
|
|
310
|
+
if profile.matches_system(system):
|
|
311
|
+
return profile
|
|
312
|
+
return None
|
|
313
|
+
|
|
314
|
+
def get_profile(self, board_id: str) -> Optional[BoardProfile]:
|
|
315
|
+
"""Get a specific board profile by ID."""
|
|
316
|
+
return self.profiles.get(board_id)
|
|
317
|
+
|
|
318
|
+
def list_profiles(self) -> List[str]:
|
|
319
|
+
"""List all available board profile IDs."""
|
|
320
|
+
return list(self.profiles.keys())
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
# Module-level convenience functions
|
|
324
|
+
|
|
325
|
+
_engine: Optional[ProfileEngine] = None
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def _get_engine() -> ProfileEngine:
|
|
329
|
+
"""Get or create the global profile engine."""
|
|
330
|
+
global _engine
|
|
331
|
+
if _engine is None:
|
|
332
|
+
_engine = ProfileEngine()
|
|
333
|
+
return _engine
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def load_profile(board_id: str) -> Optional[BoardProfile]:
|
|
337
|
+
"""Load a board profile by ID."""
|
|
338
|
+
return _get_engine().get_profile(board_id)
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def match_board_profile(system: SystemProfile) -> Optional[BoardProfile]:
|
|
342
|
+
"""Match a system profile to a board profile."""
|
|
343
|
+
return _get_engine().match_profile(system)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Rules subsystem for PyBinaryGuard.
|
|
2
|
+
|
|
3
|
+
This package provides the rule engine and all built-in compatibility rules
|
|
4
|
+
that PyBinaryGuard uses to detect binary incompatibilities.
|
|
5
|
+
|
|
6
|
+
Quick start::
|
|
7
|
+
|
|
8
|
+
from pybinaryguard.rules import RuleEngine
|
|
9
|
+
|
|
10
|
+
engine = RuleEngine.with_builtin_rules()
|
|
11
|
+
findings = engine.evaluate(system_profile, packages)
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from pybinaryguard.rules.base import Rule
|
|
17
|
+
from pybinaryguard.rules.builtin import get_all_builtin_rules
|
|
18
|
+
from pybinaryguard.rules.engine import RuleEngine
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"Rule",
|
|
22
|
+
"RuleEngine",
|
|
23
|
+
"get_all_builtin_rules",
|
|
24
|
+
]
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Base rule interface for compatibility checks."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
from typing import List
|
|
7
|
+
|
|
8
|
+
from pybinaryguard.models.system import SystemProfile
|
|
9
|
+
from pybinaryguard.models.package import PackageBinaryInfo
|
|
10
|
+
from pybinaryguard.models.finding import Finding
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Rule(ABC):
|
|
14
|
+
"""Base class for all binary compatibility rules.
|
|
15
|
+
|
|
16
|
+
Every rule has a unique ``rule_id`` (e.g. ``"GLIBC_VERSION_MISMATCH"``),
|
|
17
|
+
a human-readable ``description``, and an ``evaluate`` method that inspects
|
|
18
|
+
the system profile together with a list of package binary analyses and
|
|
19
|
+
returns zero or more :class:`Finding` objects.
|
|
20
|
+
|
|
21
|
+
Subclasses may override :meth:`is_applicable` to skip evaluation when the
|
|
22
|
+
rule cannot possibly apply (e.g. CUDA rules when no GPU is present).
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
rule_id: str
|
|
26
|
+
description: str
|
|
27
|
+
|
|
28
|
+
def is_applicable(self, profile: SystemProfile) -> bool:
|
|
29
|
+
"""Whether this rule should run given the system profile.
|
|
30
|
+
|
|
31
|
+
Override in subclasses to gate on system capabilities such as the
|
|
32
|
+
presence of a GPU, a specific libc implementation, etc.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
profile: The current system profile.
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
``True`` if the rule should be evaluated; ``False`` to skip.
|
|
39
|
+
"""
|
|
40
|
+
return True
|
|
41
|
+
|
|
42
|
+
@abstractmethod
|
|
43
|
+
def evaluate(
|
|
44
|
+
self,
|
|
45
|
+
profile: SystemProfile,
|
|
46
|
+
packages: List[PackageBinaryInfo],
|
|
47
|
+
) -> List[Finding]:
|
|
48
|
+
"""Evaluate this rule and return findings.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
profile: The current system profile.
|
|
52
|
+
packages: List of packages with their binary analysis results.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
A (possibly empty) list of findings produced by this rule.
|
|
56
|
+
"""
|
|
57
|
+
...
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""Built-in compatibility rules for PyBinaryGuard."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import List
|
|
6
|
+
|
|
7
|
+
from pybinaryguard.rules.base import Rule
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def get_all_builtin_rules() -> List[Rule]:
|
|
11
|
+
"""Instantiate and return all built-in rule classes.
|
|
12
|
+
|
|
13
|
+
Returns:
|
|
14
|
+
One instance of each built-in rule, ready for registration with a
|
|
15
|
+
:class:`~pybinaryguard.rules.engine.RuleEngine`.
|
|
16
|
+
"""
|
|
17
|
+
from pybinaryguard.rules.builtin.arch_rules import ArchMismatchRule
|
|
18
|
+
from pybinaryguard.rules.builtin.container_rules import (
|
|
19
|
+
ContainerDriverMismatchRule,
|
|
20
|
+
ContainerNoGPUMountRule,
|
|
21
|
+
)
|
|
22
|
+
from pybinaryguard.rules.builtin.cpu_rules import (
|
|
23
|
+
AVX2RequiredRule,
|
|
24
|
+
IllegalInstructionRiskRule,
|
|
25
|
+
)
|
|
26
|
+
from pybinaryguard.rules.builtin.cuda_rules import (
|
|
27
|
+
ComputeCapabilityLowRule,
|
|
28
|
+
CUDADriverTooOldRule,
|
|
29
|
+
CUDALibMissingRule,
|
|
30
|
+
CUDAMinorMismatchRule,
|
|
31
|
+
CUDANotFoundRule,
|
|
32
|
+
CUDARuntimeMismatchRule,
|
|
33
|
+
CUDNNVersionMismatchRule,
|
|
34
|
+
)
|
|
35
|
+
from pybinaryguard.rules.builtin.glibc_rules import (
|
|
36
|
+
GLIBCVersionMismatchRule,
|
|
37
|
+
LibstdcxxVersionRule,
|
|
38
|
+
ManylinuxTagViolationRule,
|
|
39
|
+
MuslGlibcConflictRule,
|
|
40
|
+
)
|
|
41
|
+
from pybinaryguard.rules.builtin.numpy_rules import NumpyABIMismatchRule
|
|
42
|
+
from pybinaryguard.rules.builtin.python_abi_rules import (
|
|
43
|
+
DebugReleaseMixRule,
|
|
44
|
+
PythonABIMismatchRule,
|
|
45
|
+
PythonVersionMismatchRule,
|
|
46
|
+
)
|
|
47
|
+
from pybinaryguard.rules.builtin.board_profile_rules import (
|
|
48
|
+
BOARD_CUDA_VERSION_MISMATCH,
|
|
49
|
+
BOARD_GLIBC_MISMATCH,
|
|
50
|
+
BOARD_INCOMPATIBLE_PACKAGE,
|
|
51
|
+
BOARD_PYTHON_VERSION_UNSUPPORTED,
|
|
52
|
+
KNOWN_BROKEN_WHEEL,
|
|
53
|
+
)
|
|
54
|
+
from pybinaryguard.rules.builtin.framework_rules import (
|
|
55
|
+
ONNX_RUNTIME_PROVIDER_MISMATCH,
|
|
56
|
+
PYTORCH_CUDA_ABI_MISMATCH,
|
|
57
|
+
PYTORCH_TORCHVISION_INCOMPATIBLE,
|
|
58
|
+
TENSORFLOW_COMPUTE_CAPABILITY_LOW,
|
|
59
|
+
TENSORRT_INCOMPATIBLE,
|
|
60
|
+
)
|
|
61
|
+
from pybinaryguard.rules.builtin.predictive_rules import (
|
|
62
|
+
PREDICTED_IMPORT_ERROR,
|
|
63
|
+
UNRESOLVED_DEPENDENCY_CHAIN,
|
|
64
|
+
)
|
|
65
|
+
from pybinaryguard.rules.builtin.source_build_rules import (
|
|
66
|
+
MissingPythonHeadersRule,
|
|
67
|
+
SourceBuildDetectionRule,
|
|
68
|
+
SourceBuildNoCompilerRule,
|
|
69
|
+
)
|
|
70
|
+
from pybinaryguard.rules.builtin.dependency_rules import (
|
|
71
|
+
DependencyConflictRule,
|
|
72
|
+
MissingDependencyRule,
|
|
73
|
+
)
|
|
74
|
+
from pybinaryguard.rules.builtin.venv_rules import (
|
|
75
|
+
CondaPipMixingRule,
|
|
76
|
+
MixedEnvironmentRule,
|
|
77
|
+
SystemPythonWarningRule,
|
|
78
|
+
UserSiteLeakRule,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
return [
|
|
82
|
+
# GLIBC rules
|
|
83
|
+
GLIBCVersionMismatchRule(),
|
|
84
|
+
MuslGlibcConflictRule(),
|
|
85
|
+
ManylinuxTagViolationRule(),
|
|
86
|
+
LibstdcxxVersionRule(),
|
|
87
|
+
# Python ABI rules
|
|
88
|
+
PythonABIMismatchRule(),
|
|
89
|
+
PythonVersionMismatchRule(),
|
|
90
|
+
DebugReleaseMixRule(),
|
|
91
|
+
# Architecture rules
|
|
92
|
+
ArchMismatchRule(),
|
|
93
|
+
# CPU instruction-set rules
|
|
94
|
+
AVX2RequiredRule(),
|
|
95
|
+
IllegalInstructionRiskRule(),
|
|
96
|
+
# CUDA / GPU rules
|
|
97
|
+
CUDADriverTooOldRule(),
|
|
98
|
+
CUDARuntimeMismatchRule(),
|
|
99
|
+
CUDAMinorMismatchRule(),
|
|
100
|
+
CUDNNVersionMismatchRule(),
|
|
101
|
+
CUDANotFoundRule(),
|
|
102
|
+
ComputeCapabilityLowRule(),
|
|
103
|
+
CUDALibMissingRule(),
|
|
104
|
+
# NumPy ABI rules
|
|
105
|
+
NumpyABIMismatchRule(),
|
|
106
|
+
# Container rules
|
|
107
|
+
ContainerNoGPUMountRule(),
|
|
108
|
+
ContainerDriverMismatchRule(),
|
|
109
|
+
# Board profile rules (embedded intelligence)
|
|
110
|
+
KNOWN_BROKEN_WHEEL(),
|
|
111
|
+
BOARD_INCOMPATIBLE_PACKAGE(),
|
|
112
|
+
BOARD_CUDA_VERSION_MISMATCH(),
|
|
113
|
+
BOARD_GLIBC_MISMATCH(),
|
|
114
|
+
BOARD_PYTHON_VERSION_UNSUPPORTED(),
|
|
115
|
+
# AI framework rules (deep inspection)
|
|
116
|
+
PYTORCH_CUDA_ABI_MISMATCH(),
|
|
117
|
+
PYTORCH_TORCHVISION_INCOMPATIBLE(),
|
|
118
|
+
TENSORFLOW_COMPUTE_CAPABILITY_LOW(),
|
|
119
|
+
TENSORRT_INCOMPATIBLE(),
|
|
120
|
+
ONNX_RUNTIME_PROVIDER_MISMATCH(),
|
|
121
|
+
# Predictive rules (runtime simulation)
|
|
122
|
+
PREDICTED_IMPORT_ERROR(),
|
|
123
|
+
UNRESOLVED_DEPENDENCY_CHAIN(),
|
|
124
|
+
# Source build rules
|
|
125
|
+
SourceBuildDetectionRule(),
|
|
126
|
+
SourceBuildNoCompilerRule(),
|
|
127
|
+
MissingPythonHeadersRule(),
|
|
128
|
+
# Dependency conflict rules
|
|
129
|
+
DependencyConflictRule(),
|
|
130
|
+
MissingDependencyRule(),
|
|
131
|
+
# Virtual environment rules
|
|
132
|
+
SystemPythonWarningRule(),
|
|
133
|
+
MixedEnvironmentRule(),
|
|
134
|
+
UserSiteLeakRule(),
|
|
135
|
+
CondaPipMixingRule(),
|
|
136
|
+
]
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""Architecture compatibility rules.
|
|
2
|
+
|
|
3
|
+
These rules detect when a package contains shared objects compiled for a
|
|
4
|
+
different CPU architecture than the one running on the host. For example,
|
|
5
|
+
an x86_64 ``.so`` file will not load on an ARM system.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import List
|
|
11
|
+
|
|
12
|
+
from pybinaryguard.models.enums import Architecture, Severity
|
|
13
|
+
from pybinaryguard.models.finding import Finding
|
|
14
|
+
from pybinaryguard.models.package import PackageBinaryInfo
|
|
15
|
+
from pybinaryguard.models.system import SystemProfile
|
|
16
|
+
from pybinaryguard.rules.base import Rule
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ArchMismatchRule(Rule):
|
|
20
|
+
"""Detects packages containing binaries for the wrong CPU architecture.
|
|
21
|
+
|
|
22
|
+
Every ELF shared object records the target architecture in its header.
|
|
23
|
+
If that does not match the host CPU, the dynamic linker will refuse to
|
|
24
|
+
load it, resulting in an ``OSError`` or ``ImportError``.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
rule_id = "ARCH_MISMATCH"
|
|
28
|
+
description = (
|
|
29
|
+
"Check that each package's binary architecture matches the host CPU."
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
def evaluate(
|
|
33
|
+
self,
|
|
34
|
+
profile: SystemProfile,
|
|
35
|
+
packages: List[PackageBinaryInfo],
|
|
36
|
+
) -> List[Finding]:
|
|
37
|
+
findings: List[Finding] = []
|
|
38
|
+
sys_arch = profile.architecture
|
|
39
|
+
|
|
40
|
+
if sys_arch == Architecture.UNKNOWN:
|
|
41
|
+
return findings
|
|
42
|
+
|
|
43
|
+
for pkg in packages:
|
|
44
|
+
if pkg.target_architecture is None:
|
|
45
|
+
continue
|
|
46
|
+
if pkg.target_architecture == Architecture.UNKNOWN:
|
|
47
|
+
continue
|
|
48
|
+
if pkg.target_architecture != sys_arch:
|
|
49
|
+
findings.append(
|
|
50
|
+
Finding(
|
|
51
|
+
rule_id=self.rule_id,
|
|
52
|
+
severity=Severity.CRITICAL,
|
|
53
|
+
title=(
|
|
54
|
+
f"{pkg.package_name} is built for "
|
|
55
|
+
f"{pkg.target_architecture.value}"
|
|
56
|
+
),
|
|
57
|
+
explanation=(
|
|
58
|
+
f"Package {pkg.package_name} "
|
|
59
|
+
f"{pkg.package_version} contains binaries "
|
|
60
|
+
f"compiled for {pkg.target_architecture.value} "
|
|
61
|
+
f"but your system is {sys_arch.value}. "
|
|
62
|
+
f"Binary code is architecture-specific: an "
|
|
63
|
+
f"x86_64 library cannot run on an ARM CPU and "
|
|
64
|
+
f"vice versa. The package will fail to import "
|
|
65
|
+
f"with an error like 'wrong ELF class' or "
|
|
66
|
+
f"'cannot open shared object file'."
|
|
67
|
+
),
|
|
68
|
+
technical_detail=(
|
|
69
|
+
f"Package arch: {pkg.target_architecture.value}, "
|
|
70
|
+
f"System arch: {sys_arch.value}"
|
|
71
|
+
),
|
|
72
|
+
suggestion=(
|
|
73
|
+
f"Reinstall the package so pip selects the "
|
|
74
|
+
f"correct wheel for your architecture:\n"
|
|
75
|
+
f" pip install --force-reinstall "
|
|
76
|
+
f"{pkg.package_name}=={pkg.package_version}\n\n"
|
|
77
|
+
f"If no pre-built wheel exists for "
|
|
78
|
+
f"{sys_arch.value}, build from source:\n"
|
|
79
|
+
f" pip install --no-binary :all: "
|
|
80
|
+
f"{pkg.package_name}"
|
|
81
|
+
),
|
|
82
|
+
package=pkg.package_name,
|
|
83
|
+
package_version=pkg.package_version,
|
|
84
|
+
)
|
|
85
|
+
)
|
|
86
|
+
return findings
|