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,144 @@
|
|
|
1
|
+
"""Main predictor module for runtime failure prediction.
|
|
2
|
+
|
|
3
|
+
Combines symbol resolution, dependency analysis, and linker simulation
|
|
4
|
+
to predict ImportErrors before they happen.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from typing import List, Optional
|
|
11
|
+
|
|
12
|
+
from pybinaryguard.models.package import PackageBinaryInfo
|
|
13
|
+
from pybinaryguard.models.system import SystemProfile
|
|
14
|
+
from pybinaryguard.predictor.linker_simulator import LinkerSimulator
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class PredictedFailure:
|
|
19
|
+
"""A predicted runtime failure."""
|
|
20
|
+
|
|
21
|
+
package: str
|
|
22
|
+
module_path: str
|
|
23
|
+
error_type: str # "ImportError", "SymbolError", "LibraryMissing"
|
|
24
|
+
error_message: str
|
|
25
|
+
missing_symbol: Optional[str] = None
|
|
26
|
+
missing_library: Optional[str] = None
|
|
27
|
+
confidence: float = 0.0
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def predict_import_failures(
|
|
31
|
+
package: PackageBinaryInfo,
|
|
32
|
+
profile: SystemProfile
|
|
33
|
+
) -> List[PredictedFailure]:
|
|
34
|
+
"""Predict import failures for a package.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
package: Package binary information
|
|
38
|
+
profile: System profile
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
List of predicted failures
|
|
42
|
+
"""
|
|
43
|
+
failures: List[PredictedFailure] = []
|
|
44
|
+
|
|
45
|
+
# Get library paths from system
|
|
46
|
+
library_paths = profile.library_search_paths or None
|
|
47
|
+
|
|
48
|
+
simulator = LinkerSimulator(library_paths)
|
|
49
|
+
|
|
50
|
+
# Simulate loading each shared object
|
|
51
|
+
for so in package.shared_objects:
|
|
52
|
+
if not so.path:
|
|
53
|
+
continue
|
|
54
|
+
|
|
55
|
+
# Predict import error
|
|
56
|
+
error_msg = simulator.predict_import_error(so.path, f"{package.name}._C")
|
|
57
|
+
|
|
58
|
+
if error_msg:
|
|
59
|
+
# Parse error type
|
|
60
|
+
if "undefined symbol" in error_msg:
|
|
61
|
+
error_type = "SymbolError"
|
|
62
|
+
# Extract symbol name
|
|
63
|
+
if "undefined symbol:" in error_msg:
|
|
64
|
+
parts = error_msg.split("undefined symbol:")
|
|
65
|
+
if len(parts) > 1:
|
|
66
|
+
missing_symbol = parts[1].strip()
|
|
67
|
+
else:
|
|
68
|
+
missing_symbol = "unknown"
|
|
69
|
+
else:
|
|
70
|
+
missing_symbol = "unknown"
|
|
71
|
+
|
|
72
|
+
failures.append(
|
|
73
|
+
PredictedFailure(
|
|
74
|
+
package=package.name,
|
|
75
|
+
module_path=so.path,
|
|
76
|
+
error_type=error_type,
|
|
77
|
+
error_message=error_msg,
|
|
78
|
+
missing_symbol=missing_symbol,
|
|
79
|
+
confidence=0.95,
|
|
80
|
+
)
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
elif "cannot open shared object file" in error_msg:
|
|
84
|
+
error_type = "LibraryMissing"
|
|
85
|
+
# Extract library name
|
|
86
|
+
if "file:" in error_msg:
|
|
87
|
+
parts = error_msg.split("file:")
|
|
88
|
+
if len(parts) > 1:
|
|
89
|
+
lib_part = parts[1].split(":")[0].strip()
|
|
90
|
+
missing_library = lib_part
|
|
91
|
+
else:
|
|
92
|
+
missing_library = "unknown"
|
|
93
|
+
else:
|
|
94
|
+
missing_library = "unknown"
|
|
95
|
+
|
|
96
|
+
failures.append(
|
|
97
|
+
PredictedFailure(
|
|
98
|
+
package=package.name,
|
|
99
|
+
module_path=so.path,
|
|
100
|
+
error_type=error_type,
|
|
101
|
+
error_message=error_msg,
|
|
102
|
+
missing_library=missing_library,
|
|
103
|
+
confidence=0.9,
|
|
104
|
+
)
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
else:
|
|
108
|
+
# Generic import error
|
|
109
|
+
failures.append(
|
|
110
|
+
PredictedFailure(
|
|
111
|
+
package=package.name,
|
|
112
|
+
module_path=so.path,
|
|
113
|
+
error_type="ImportError",
|
|
114
|
+
error_message=error_msg,
|
|
115
|
+
confidence=0.8,
|
|
116
|
+
)
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
return failures
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def format_predicted_failure(failure: PredictedFailure) -> str:
|
|
123
|
+
"""Format a predicted failure as a human-readable message.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
failure: PredictedFailure to format
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
Formatted message string
|
|
130
|
+
"""
|
|
131
|
+
lines = [
|
|
132
|
+
f"Predicted {failure.error_type} in {failure.package}:",
|
|
133
|
+
f" {failure.error_message}",
|
|
134
|
+
]
|
|
135
|
+
|
|
136
|
+
if failure.missing_symbol:
|
|
137
|
+
lines.append(f" Missing symbol: {failure.missing_symbol}")
|
|
138
|
+
|
|
139
|
+
if failure.missing_library:
|
|
140
|
+
lines.append(f" Missing library: {failure.missing_library}")
|
|
141
|
+
|
|
142
|
+
lines.append(f" Confidence: {failure.confidence * 100:.0f}%")
|
|
143
|
+
|
|
144
|
+
return "\n".join(lines)
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
"""Symbol resolution for ELF binaries.
|
|
2
|
+
|
|
3
|
+
Resolves undefined symbols against available shared libraries,
|
|
4
|
+
simulating the behavior of the dynamic linker.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Dict, List, Optional, Set
|
|
13
|
+
|
|
14
|
+
from pybinaryguard.analyzers.elf_analyzer import MinimalELFParser
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class Symbol:
|
|
19
|
+
"""Represents a symbol from an ELF binary."""
|
|
20
|
+
|
|
21
|
+
name: str
|
|
22
|
+
library: Optional[str] = None # Which library provides this symbol
|
|
23
|
+
version: Optional[str] = None # Symbol version (e.g., GLIBC_2.34)
|
|
24
|
+
weak: bool = False # Whether this is a weak symbol
|
|
25
|
+
undefined: bool = False # Whether this symbol needs to be resolved
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class LibrarySymbols:
|
|
30
|
+
"""Symbol table for a shared library."""
|
|
31
|
+
|
|
32
|
+
path: str
|
|
33
|
+
provided_symbols: Set[str] = field(default_factory=set)
|
|
34
|
+
required_symbols: Set[str] = field(default_factory=set)
|
|
35
|
+
versioned_symbols: Dict[str, str] = field(default_factory=dict) # symbol -> version
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class SymbolResolver:
|
|
39
|
+
"""Resolves symbols from ELF binaries against system libraries."""
|
|
40
|
+
|
|
41
|
+
def __init__(self, library_paths: Optional[List[str]] = None):
|
|
42
|
+
"""Initialize the symbol resolver.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
library_paths: List of directories to search for libraries.
|
|
46
|
+
If None, uses standard system paths.
|
|
47
|
+
"""
|
|
48
|
+
if library_paths is None:
|
|
49
|
+
library_paths = self._get_default_library_paths()
|
|
50
|
+
|
|
51
|
+
self.library_paths = library_paths
|
|
52
|
+
self._symbol_cache: Dict[str, LibrarySymbols] = {}
|
|
53
|
+
|
|
54
|
+
def _get_default_library_paths(self) -> List[str]:
|
|
55
|
+
"""Get default library search paths."""
|
|
56
|
+
paths = [
|
|
57
|
+
"/lib",
|
|
58
|
+
"/lib64",
|
|
59
|
+
"/usr/lib",
|
|
60
|
+
"/usr/lib64",
|
|
61
|
+
"/usr/local/lib",
|
|
62
|
+
"/usr/local/lib64",
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
# Add LD_LIBRARY_PATH if set
|
|
66
|
+
ld_lib_path = os.environ.get("LD_LIBRARY_PATH", "")
|
|
67
|
+
if ld_lib_path:
|
|
68
|
+
paths.extend(ld_lib_path.split(":"))
|
|
69
|
+
|
|
70
|
+
# Filter to existing directories
|
|
71
|
+
return [p for p in paths if os.path.isdir(p)]
|
|
72
|
+
|
|
73
|
+
def resolve_undefined_symbols(
|
|
74
|
+
self,
|
|
75
|
+
binary_path: str,
|
|
76
|
+
needed_libraries: List[str]
|
|
77
|
+
) -> Dict[str, Optional[str]]:
|
|
78
|
+
"""Resolve undefined symbols in a binary.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
binary_path: Path to the ELF binary
|
|
82
|
+
needed_libraries: List of DT_NEEDED libraries (from ELF)
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
Dictionary mapping symbol name to providing library (or None if unresolved)
|
|
86
|
+
"""
|
|
87
|
+
# Get undefined symbols from the binary
|
|
88
|
+
undefined_symbols = self._extract_undefined_symbols(binary_path)
|
|
89
|
+
|
|
90
|
+
# Build symbol index from needed libraries
|
|
91
|
+
symbol_index: Dict[str, str] = {}
|
|
92
|
+
for lib_name in needed_libraries:
|
|
93
|
+
lib_path = self._find_library(lib_name)
|
|
94
|
+
if lib_path:
|
|
95
|
+
lib_symbols = self._get_library_symbols(lib_path)
|
|
96
|
+
for sym in lib_symbols.provided_symbols:
|
|
97
|
+
if sym not in symbol_index:
|
|
98
|
+
symbol_index[sym] = lib_name
|
|
99
|
+
|
|
100
|
+
# Resolve each undefined symbol
|
|
101
|
+
resolution: Dict[str, Optional[str]] = {}
|
|
102
|
+
for sym in undefined_symbols:
|
|
103
|
+
resolution[sym] = symbol_index.get(sym)
|
|
104
|
+
|
|
105
|
+
return resolution
|
|
106
|
+
|
|
107
|
+
def _extract_undefined_symbols(self, binary_path: str) -> Set[str]:
|
|
108
|
+
"""Extract undefined symbols from an ELF binary.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
binary_path: Path to the ELF binary
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
Set of undefined symbol names
|
|
115
|
+
"""
|
|
116
|
+
# This is a simplified implementation
|
|
117
|
+
# A full implementation would parse the .dynsym section
|
|
118
|
+
# For now, we return an empty set as a placeholder
|
|
119
|
+
undefined = set()
|
|
120
|
+
|
|
121
|
+
try:
|
|
122
|
+
# Parse ELF to get dynamic symbols
|
|
123
|
+
parser = MinimalELFParser(binary_path)
|
|
124
|
+
# TODO: Extend MinimalELFParser to extract symbol table
|
|
125
|
+
# For now, we can extract from version requirements
|
|
126
|
+
for version in parser.get_version_requirements():
|
|
127
|
+
# Version strings like "GLIBC_2.34" indicate required symbols
|
|
128
|
+
undefined.add(version)
|
|
129
|
+
except Exception:
|
|
130
|
+
pass
|
|
131
|
+
|
|
132
|
+
return undefined
|
|
133
|
+
|
|
134
|
+
def _find_library(self, lib_name: str) -> Optional[str]:
|
|
135
|
+
"""Find a library by name in the search paths.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
lib_name: Library name (e.g., "libc.so.6")
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
Full path to library or None if not found
|
|
142
|
+
"""
|
|
143
|
+
for search_path in self.library_paths:
|
|
144
|
+
lib_path = os.path.join(search_path, lib_name)
|
|
145
|
+
if os.path.exists(lib_path):
|
|
146
|
+
return lib_path
|
|
147
|
+
|
|
148
|
+
# Also try without directory component if lib_name is absolute
|
|
149
|
+
if lib_name.startswith("/") and os.path.exists(lib_name):
|
|
150
|
+
return lib_name
|
|
151
|
+
|
|
152
|
+
return None
|
|
153
|
+
|
|
154
|
+
def _get_library_symbols(self, lib_path: str) -> LibrarySymbols:
|
|
155
|
+
"""Get symbols provided by a library.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
lib_path: Path to the library
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
LibrarySymbols object with provided/required symbols
|
|
162
|
+
"""
|
|
163
|
+
# Check cache first
|
|
164
|
+
if lib_path in self._symbol_cache:
|
|
165
|
+
return self._symbol_cache[lib_path]
|
|
166
|
+
|
|
167
|
+
lib_symbols = LibrarySymbols(path=lib_path)
|
|
168
|
+
|
|
169
|
+
try:
|
|
170
|
+
parser = MinimalELFParser(lib_path)
|
|
171
|
+
|
|
172
|
+
# Get version requirements (symbols this library requires)
|
|
173
|
+
for version in parser.get_version_requirements():
|
|
174
|
+
lib_symbols.required_symbols.add(version)
|
|
175
|
+
lib_symbols.versioned_symbols[version] = version
|
|
176
|
+
|
|
177
|
+
# For provided symbols, we'd need to parse .dynsym
|
|
178
|
+
# This is a simplified version
|
|
179
|
+
# In a full implementation, we'd extract exported symbols
|
|
180
|
+
# For now, mark GLIBC versions as "provided" if this is libc
|
|
181
|
+
if "libc.so" in lib_path:
|
|
182
|
+
for version in parser.get_version_requirements():
|
|
183
|
+
lib_symbols.provided_symbols.add(version)
|
|
184
|
+
|
|
185
|
+
except Exception:
|
|
186
|
+
pass
|
|
187
|
+
|
|
188
|
+
# Cache the result
|
|
189
|
+
self._symbol_cache[lib_path] = lib_symbols
|
|
190
|
+
|
|
191
|
+
return lib_symbols
|
|
192
|
+
|
|
193
|
+
def check_symbol_availability(
|
|
194
|
+
self,
|
|
195
|
+
symbol_name: str,
|
|
196
|
+
version_requirement: Optional[str] = None
|
|
197
|
+
) -> bool:
|
|
198
|
+
"""Check if a symbol is available in system libraries.
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
symbol_name: Name of the symbol
|
|
202
|
+
version_requirement: Optional version requirement (e.g., "GLIBC_2.34")
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
True if symbol is available, False otherwise
|
|
206
|
+
"""
|
|
207
|
+
# Search all system libraries
|
|
208
|
+
for search_path in self.library_paths:
|
|
209
|
+
if not os.path.isdir(search_path):
|
|
210
|
+
continue
|
|
211
|
+
|
|
212
|
+
try:
|
|
213
|
+
for entry in os.listdir(search_path):
|
|
214
|
+
if entry.endswith(".so") or ".so." in entry:
|
|
215
|
+
lib_path = os.path.join(search_path, entry)
|
|
216
|
+
if os.path.isfile(lib_path):
|
|
217
|
+
lib_symbols = self._get_library_symbols(lib_path)
|
|
218
|
+
if symbol_name in lib_symbols.provided_symbols:
|
|
219
|
+
if version_requirement:
|
|
220
|
+
# Check version compatibility
|
|
221
|
+
sym_version = lib_symbols.versioned_symbols.get(symbol_name)
|
|
222
|
+
if sym_version and self._version_gte(sym_version, version_requirement):
|
|
223
|
+
return True
|
|
224
|
+
else:
|
|
225
|
+
return True
|
|
226
|
+
except (OSError, PermissionError):
|
|
227
|
+
continue
|
|
228
|
+
|
|
229
|
+
return False
|
|
230
|
+
|
|
231
|
+
@staticmethod
|
|
232
|
+
def _version_gte(v1: str, v2: str) -> bool:
|
|
233
|
+
"""Compare version strings (simple comparison).
|
|
234
|
+
|
|
235
|
+
Args:
|
|
236
|
+
v1: First version (e.g., "GLIBC_2.35")
|
|
237
|
+
v2: Second version (e.g., "GLIBC_2.34")
|
|
238
|
+
|
|
239
|
+
Returns:
|
|
240
|
+
True if v1 >= v2
|
|
241
|
+
"""
|
|
242
|
+
try:
|
|
243
|
+
# Extract numeric parts (e.g., "GLIBC_2.34" -> [2, 34])
|
|
244
|
+
parts1 = [int(x) for x in v1.split("_")[-1].split(".")]
|
|
245
|
+
parts2 = [int(x) for x in v2.split("_")[-1].split(".")]
|
|
246
|
+
return parts1 >= parts2
|
|
247
|
+
except (ValueError, IndexError):
|
|
248
|
+
return False
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def resolve_symbol(
|
|
252
|
+
symbol_name: str,
|
|
253
|
+
library_paths: Optional[List[str]] = None
|
|
254
|
+
) -> Optional[str]:
|
|
255
|
+
"""Convenience function to resolve a single symbol.
|
|
256
|
+
|
|
257
|
+
Args:
|
|
258
|
+
symbol_name: Name of the symbol to resolve
|
|
259
|
+
library_paths: Optional list of library search paths
|
|
260
|
+
|
|
261
|
+
Returns:
|
|
262
|
+
Path to library providing the symbol, or None if not found
|
|
263
|
+
"""
|
|
264
|
+
resolver = SymbolResolver(library_paths)
|
|
265
|
+
|
|
266
|
+
# Search for the symbol
|
|
267
|
+
for search_path in resolver.library_paths:
|
|
268
|
+
if not os.path.isdir(search_path):
|
|
269
|
+
continue
|
|
270
|
+
|
|
271
|
+
try:
|
|
272
|
+
for entry in os.listdir(search_path):
|
|
273
|
+
if entry.endswith(".so") or ".so." in entry:
|
|
274
|
+
lib_path = os.path.join(search_path, entry)
|
|
275
|
+
if os.path.isfile(lib_path):
|
|
276
|
+
lib_symbols = resolver._get_library_symbols(lib_path)
|
|
277
|
+
if symbol_name in lib_symbols.provided_symbols:
|
|
278
|
+
return lib_path
|
|
279
|
+
except (OSError, PermissionError):
|
|
280
|
+
continue
|
|
281
|
+
|
|
282
|
+
return None
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""System probes for PyBinaryGuard.
|
|
2
|
+
|
|
3
|
+
This package contains all probes that collect information about the
|
|
4
|
+
current system's environment. Each probe is a subclass of
|
|
5
|
+
:class:`ProbeBase` and implements a ``collect()`` method that returns
|
|
6
|
+
a dictionary whose keys correspond to :class:`SystemProfile` fields.
|
|
7
|
+
|
|
8
|
+
Usage::
|
|
9
|
+
|
|
10
|
+
from pybinaryguard.probes import get_all_probes
|
|
11
|
+
|
|
12
|
+
for probe in get_all_probes():
|
|
13
|
+
if probe.is_applicable():
|
|
14
|
+
data = probe.collect()
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from typing import List
|
|
20
|
+
|
|
21
|
+
from .base import ProbeBase
|
|
22
|
+
from .board_probe import BoardProbe
|
|
23
|
+
from .cpu_probe import CpuProbe
|
|
24
|
+
from .glibc_probe import GlibcProbe
|
|
25
|
+
from .gpu_probe import GpuProbe
|
|
26
|
+
from .library_probe import LibraryProbe
|
|
27
|
+
from .os_probe import OsProbe
|
|
28
|
+
from .python_probe import PythonProbe
|
|
29
|
+
from .toolchain_probe import ToolchainProbe
|
|
30
|
+
from .venv_probe import VenvProbe
|
|
31
|
+
|
|
32
|
+
__all__ = [
|
|
33
|
+
"ProbeBase",
|
|
34
|
+
"BoardProbe",
|
|
35
|
+
"CpuProbe",
|
|
36
|
+
"GlibcProbe",
|
|
37
|
+
"GpuProbe",
|
|
38
|
+
"LibraryProbe",
|
|
39
|
+
"OsProbe",
|
|
40
|
+
"PythonProbe",
|
|
41
|
+
"ToolchainProbe",
|
|
42
|
+
"VenvProbe",
|
|
43
|
+
"get_all_probes",
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def get_all_probes() -> List[ProbeBase]:
|
|
48
|
+
"""Return an instance of every available probe.
|
|
49
|
+
|
|
50
|
+
The probes are returned in a deterministic order chosen so that
|
|
51
|
+
cheaper / more fundamental probes run first:
|
|
52
|
+
|
|
53
|
+
1. **PythonProbe** -- interpreter info (fast, no I/O beyond stdlib)
|
|
54
|
+
2. **VenvProbe** -- virtual environment detection (fast)
|
|
55
|
+
3. **CpuProbe** -- CPU architecture and flags (reads /proc/cpuinfo)
|
|
56
|
+
4. **OsProbe** -- OS and container detection
|
|
57
|
+
5. **GlibcProbe** -- C library version
|
|
58
|
+
6. **ToolchainProbe** -- build toolchain (gcc, cmake, etc.)
|
|
59
|
+
7. **LibraryProbe** -- shared library paths and ldconfig cache
|
|
60
|
+
8. **BoardProbe** -- embedded board detection
|
|
61
|
+
9. **GpuProbe** -- GPU / CUDA detection (most expensive)
|
|
62
|
+
"""
|
|
63
|
+
return [
|
|
64
|
+
PythonProbe(),
|
|
65
|
+
VenvProbe(),
|
|
66
|
+
CpuProbe(),
|
|
67
|
+
OsProbe(),
|
|
68
|
+
GlibcProbe(),
|
|
69
|
+
ToolchainProbe(),
|
|
70
|
+
LibraryProbe(),
|
|
71
|
+
BoardProbe(),
|
|
72
|
+
GpuProbe(),
|
|
73
|
+
]
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Abstract base class for all system probes."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
from typing import Any, Dict
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ProbeBase(ABC):
|
|
10
|
+
"""Base class for system probes that collect environment information.
|
|
11
|
+
|
|
12
|
+
Every probe must:
|
|
13
|
+
- Have a unique ``name`` attribute used for identification and logging.
|
|
14
|
+
- Implement ``collect()`` which returns a dict whose keys correspond to
|
|
15
|
+
field names on ``SystemProfile``.
|
|
16
|
+
- Be completely read-only -- probes must never modify system state.
|
|
17
|
+
- Handle all internal errors gracefully and return partial data rather
|
|
18
|
+
than raising.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
name: str # Probe identifier -- must be set by subclasses
|
|
22
|
+
|
|
23
|
+
@abstractmethod
|
|
24
|
+
def collect(self) -> Dict[str, Any]:
|
|
25
|
+
"""Collect system information.
|
|
26
|
+
|
|
27
|
+
Returns
|
|
28
|
+
-------
|
|
29
|
+
Dict[str, Any]
|
|
30
|
+
A dictionary whose keys are ``SystemProfile`` field names and
|
|
31
|
+
whose values are the detected values for those fields. If a
|
|
32
|
+
value cannot be determined, it should be omitted from the dict
|
|
33
|
+
rather than set to ``None`` (unless ``None`` is the intended
|
|
34
|
+
sentinel).
|
|
35
|
+
"""
|
|
36
|
+
...
|
|
37
|
+
|
|
38
|
+
def is_applicable(self) -> bool:
|
|
39
|
+
"""Return whether this probe can run on the current system.
|
|
40
|
+
|
|
41
|
+
The default implementation returns ``True``. Subclasses may
|
|
42
|
+
override to skip probing when the current platform is
|
|
43
|
+
incompatible (e.g. GPU probe on a system with no GPU devices).
|
|
44
|
+
"""
|
|
45
|
+
return True
|