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,190 @@
|
|
|
1
|
+
"""Plugin discovery and loading via ``importlib.metadata`` entry points.
|
|
2
|
+
|
|
3
|
+
Plugins declare themselves through the ``pybinaryguard.plugins`` entry-point
|
|
4
|
+
group in their ``pyproject.toml`` (or ``setup.cfg`` / ``setup.py``). Each
|
|
5
|
+
entry point must resolve to a module that exposes a ``register(registry)``
|
|
6
|
+
function.
|
|
7
|
+
|
|
8
|
+
Example ``pyproject.toml`` snippet::
|
|
9
|
+
|
|
10
|
+
[project.entry-points."pybinaryguard.plugins"]
|
|
11
|
+
my_plugin = "my_package.pybinaryguard_plugin"
|
|
12
|
+
|
|
13
|
+
The module ``my_package.pybinaryguard_plugin`` must define::
|
|
14
|
+
|
|
15
|
+
def register(registry: HookRegistry) -> None:
|
|
16
|
+
...
|
|
17
|
+
|
|
18
|
+
This module also discovers the built-in contrib plugins that ship with
|
|
19
|
+
PyBinaryGuard itself (Jetson, OpenCV, TensorRT, GStreamer).
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import importlib
|
|
25
|
+
import logging
|
|
26
|
+
import sys
|
|
27
|
+
from typing import List
|
|
28
|
+
|
|
29
|
+
from pybinaryguard.plugins.hooks import HookRegistry
|
|
30
|
+
|
|
31
|
+
logger = logging.getLogger(__name__)
|
|
32
|
+
|
|
33
|
+
ENTRY_POINT_GROUP = "pybinaryguard.plugins"
|
|
34
|
+
|
|
35
|
+
# Built-in contrib plugin module paths, loaded after external entry points.
|
|
36
|
+
_BUILTIN_CONTRIB_MODULES: List[str] = [
|
|
37
|
+
"pybinaryguard.plugins.contrib.jetson",
|
|
38
|
+
"pybinaryguard.plugins.contrib.opencv",
|
|
39
|
+
"pybinaryguard.plugins.contrib.tensorrt",
|
|
40
|
+
"pybinaryguard.plugins.contrib.gstreamer",
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _load_entry_point_plugins(registry: HookRegistry) -> None:
|
|
45
|
+
"""Discover and load external plugins via ``importlib.metadata`` entry points.
|
|
46
|
+
|
|
47
|
+
Handles both the Python 3.9 API (``entry_points()`` returns a dict)
|
|
48
|
+
and the Python 3.10+ API (``entry_points(group=...)``).
|
|
49
|
+
"""
|
|
50
|
+
try:
|
|
51
|
+
from importlib.metadata import entry_points
|
|
52
|
+
except ImportError:
|
|
53
|
+
# Fallback for very old environments that lack importlib.metadata
|
|
54
|
+
# (should not happen on 3.9+ but guard defensively).
|
|
55
|
+
logger.debug("importlib.metadata not available; skipping entry-point discovery")
|
|
56
|
+
return
|
|
57
|
+
|
|
58
|
+
eps: list = []
|
|
59
|
+
try:
|
|
60
|
+
# Python 3.12+ and 3.10+ style: keyword argument filtering
|
|
61
|
+
selected = entry_points(group=ENTRY_POINT_GROUP)
|
|
62
|
+
# On 3.10-3.11 this returns a SelectableGroups or list
|
|
63
|
+
if isinstance(selected, dict):
|
|
64
|
+
# 3.9 fallback: entry_points() returns a dict of lists
|
|
65
|
+
eps = list(selected.get(ENTRY_POINT_GROUP, []))
|
|
66
|
+
else:
|
|
67
|
+
eps = list(selected)
|
|
68
|
+
except TypeError:
|
|
69
|
+
# Python 3.9: entry_points() does not accept keyword arguments.
|
|
70
|
+
all_eps = entry_points()
|
|
71
|
+
if isinstance(all_eps, dict):
|
|
72
|
+
eps = list(all_eps.get(ENTRY_POINT_GROUP, []))
|
|
73
|
+
else:
|
|
74
|
+
# Should not reach here, but handle gracefully
|
|
75
|
+
eps = []
|
|
76
|
+
|
|
77
|
+
for ep in eps:
|
|
78
|
+
try:
|
|
79
|
+
plugin_module = ep.load()
|
|
80
|
+
register_fn = getattr(plugin_module, "register", None)
|
|
81
|
+
if register_fn is None:
|
|
82
|
+
logger.warning(
|
|
83
|
+
"Plugin entry point %r resolved to %r but it has no "
|
|
84
|
+
"'register' function; skipping",
|
|
85
|
+
ep.name,
|
|
86
|
+
plugin_module,
|
|
87
|
+
)
|
|
88
|
+
continue
|
|
89
|
+
if not callable(register_fn):
|
|
90
|
+
logger.warning(
|
|
91
|
+
"Plugin %r has a 'register' attribute but it is not "
|
|
92
|
+
"callable; skipping",
|
|
93
|
+
ep.name,
|
|
94
|
+
)
|
|
95
|
+
continue
|
|
96
|
+
register_fn(registry)
|
|
97
|
+
logger.info("Loaded external plugin: %s", ep.name)
|
|
98
|
+
except Exception:
|
|
99
|
+
logger.warning(
|
|
100
|
+
"Failed to load plugin entry point %r; skipping",
|
|
101
|
+
ep.name,
|
|
102
|
+
exc_info=True,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _load_builtin_contrib_plugins(registry: HookRegistry) -> None:
|
|
107
|
+
"""Load the contrib plugins that ship with PyBinaryGuard.
|
|
108
|
+
|
|
109
|
+
Each contrib module is imported and its ``register(registry)`` function
|
|
110
|
+
is called. Errors in individual contrib plugins are logged and do not
|
|
111
|
+
prevent the rest from loading.
|
|
112
|
+
"""
|
|
113
|
+
for module_path in _BUILTIN_CONTRIB_MODULES:
|
|
114
|
+
try:
|
|
115
|
+
mod = importlib.import_module(module_path)
|
|
116
|
+
except ImportError:
|
|
117
|
+
logger.debug(
|
|
118
|
+
"Could not import contrib plugin %s; skipping",
|
|
119
|
+
module_path,
|
|
120
|
+
)
|
|
121
|
+
continue
|
|
122
|
+
|
|
123
|
+
register_fn = getattr(mod, "register", None)
|
|
124
|
+
if register_fn is None:
|
|
125
|
+
logger.debug(
|
|
126
|
+
"Contrib module %s has no 'register' function; skipping",
|
|
127
|
+
module_path,
|
|
128
|
+
)
|
|
129
|
+
continue
|
|
130
|
+
|
|
131
|
+
if not callable(register_fn):
|
|
132
|
+
logger.debug(
|
|
133
|
+
"Contrib module %s 'register' attribute is not callable; skipping",
|
|
134
|
+
module_path,
|
|
135
|
+
)
|
|
136
|
+
continue
|
|
137
|
+
|
|
138
|
+
try:
|
|
139
|
+
register_fn(registry)
|
|
140
|
+
logger.debug("Loaded contrib plugin: %s", module_path)
|
|
141
|
+
except Exception:
|
|
142
|
+
logger.warning(
|
|
143
|
+
"Contrib plugin %s raised an error during registration; skipping",
|
|
144
|
+
module_path,
|
|
145
|
+
exc_info=True,
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def discover_plugins(
|
|
150
|
+
*,
|
|
151
|
+
load_contrib: bool = True,
|
|
152
|
+
load_external: bool = True,
|
|
153
|
+
) -> HookRegistry:
|
|
154
|
+
"""Discover and load all available plugins.
|
|
155
|
+
|
|
156
|
+
This is the main entry point for the plugin subsystem. It creates a
|
|
157
|
+
fresh :class:`HookRegistry`, populates it from external entry points
|
|
158
|
+
and built-in contrib plugins, and returns it.
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
load_contrib: Whether to load the built-in contrib plugins that
|
|
162
|
+
ship with PyBinaryGuard (Jetson, OpenCV, TensorRT, GStreamer).
|
|
163
|
+
Defaults to ``True``.
|
|
164
|
+
load_external: Whether to discover and load externally-installed
|
|
165
|
+
plugins via ``importlib.metadata`` entry points. Defaults
|
|
166
|
+
to ``True``.
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
A :class:`HookRegistry` populated with all successfully loaded
|
|
170
|
+
plugin extensions.
|
|
171
|
+
"""
|
|
172
|
+
registry = HookRegistry()
|
|
173
|
+
|
|
174
|
+
if load_external:
|
|
175
|
+
_load_entry_point_plugins(registry)
|
|
176
|
+
|
|
177
|
+
if load_contrib:
|
|
178
|
+
_load_builtin_contrib_plugins(registry)
|
|
179
|
+
|
|
180
|
+
total = registry.summary()
|
|
181
|
+
registered = sum(total.values())
|
|
182
|
+
if registered:
|
|
183
|
+
logger.info(
|
|
184
|
+
"Plugin discovery complete: %s",
|
|
185
|
+
", ".join(f"{k}={v}" for k, v in total.items() if v),
|
|
186
|
+
)
|
|
187
|
+
else:
|
|
188
|
+
logger.debug("Plugin discovery complete: no extensions registered")
|
|
189
|
+
|
|
190
|
+
return registry
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Predictive failure engine for runtime simulation.
|
|
2
|
+
|
|
3
|
+
This module predicts ImportErrors and runtime failures before they occur
|
|
4
|
+
by simulating dynamic linker resolution and symbol dependencies.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from .resolver import SymbolResolver, resolve_symbol
|
|
10
|
+
from .dependency_graph import DependencyGraph, build_dependency_graph
|
|
11
|
+
from .linker_simulator import LinkerSimulator, simulate_import
|
|
12
|
+
from .predictor import predict_import_failures, PredictedFailure
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"SymbolResolver",
|
|
16
|
+
"resolve_symbol",
|
|
17
|
+
"DependencyGraph",
|
|
18
|
+
"build_dependency_graph",
|
|
19
|
+
"LinkerSimulator",
|
|
20
|
+
"simulate_import",
|
|
21
|
+
"predict_import_failures",
|
|
22
|
+
"PredictedFailure",
|
|
23
|
+
]
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
"""Dependency graph builder for shared library dependencies.
|
|
2
|
+
|
|
3
|
+
Builds a graph of DT_NEEDED dependencies to visualize and analyze
|
|
4
|
+
library dependency chains.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Dict, List, Optional, Set
|
|
12
|
+
|
|
13
|
+
from pybinaryguard.analyzers.elf_analyzer import MinimalELFParser
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class DependencyNode:
|
|
18
|
+
"""A node in the dependency graph representing a library."""
|
|
19
|
+
|
|
20
|
+
name: str # Library name (e.g., "libc.so.6")
|
|
21
|
+
path: Optional[str] = None # Full path if found
|
|
22
|
+
dependencies: List[str] = field(default_factory=list) # DT_NEEDED entries
|
|
23
|
+
unresolved: bool = False # Whether this library was found
|
|
24
|
+
circular: bool = False # Whether this is part of a circular dependency
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class DependencyGraph:
|
|
28
|
+
"""Dependency graph for analyzing shared library dependencies."""
|
|
29
|
+
|
|
30
|
+
def __init__(self, library_paths: Optional[List[str]] = None):
|
|
31
|
+
"""Initialize the dependency graph builder.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
library_paths: Directories to search for libraries
|
|
35
|
+
"""
|
|
36
|
+
if library_paths is None:
|
|
37
|
+
library_paths = [
|
|
38
|
+
"/lib",
|
|
39
|
+
"/lib64",
|
|
40
|
+
"/usr/lib",
|
|
41
|
+
"/usr/lib64",
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
self.library_paths = [Path(p) for p in library_paths if Path(p).exists()]
|
|
45
|
+
self.nodes: Dict[str, DependencyNode] = {}
|
|
46
|
+
self._visited: Set[str] = set()
|
|
47
|
+
|
|
48
|
+
def build_from_binary(self, binary_path: str) -> DependencyNode:
|
|
49
|
+
"""Build dependency graph starting from a binary.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
binary_path: Path to the ELF binary
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
Root DependencyNode for the binary
|
|
56
|
+
"""
|
|
57
|
+
self.nodes.clear()
|
|
58
|
+
self._visited.clear()
|
|
59
|
+
|
|
60
|
+
root_name = Path(binary_path).name
|
|
61
|
+
root_node = DependencyNode(name=root_name, path=binary_path)
|
|
62
|
+
self.nodes[root_name] = root_node
|
|
63
|
+
|
|
64
|
+
# Build the graph recursively
|
|
65
|
+
self._build_recursive(binary_path, root_node)
|
|
66
|
+
|
|
67
|
+
return root_node
|
|
68
|
+
|
|
69
|
+
def _build_recursive(self, lib_path: str, node: DependencyNode) -> None:
|
|
70
|
+
"""Recursively build dependency graph.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
lib_path: Path to current library
|
|
74
|
+
node: DependencyNode for current library
|
|
75
|
+
"""
|
|
76
|
+
# Avoid infinite recursion
|
|
77
|
+
if lib_path in self._visited:
|
|
78
|
+
node.circular = True
|
|
79
|
+
return
|
|
80
|
+
|
|
81
|
+
self._visited.add(lib_path)
|
|
82
|
+
|
|
83
|
+
try:
|
|
84
|
+
# Parse ELF to get DT_NEEDED
|
|
85
|
+
parser = MinimalELFParser(lib_path)
|
|
86
|
+
needed = parser.get_needed()
|
|
87
|
+
|
|
88
|
+
node.dependencies = needed
|
|
89
|
+
|
|
90
|
+
# Process each dependency
|
|
91
|
+
for dep_name in needed:
|
|
92
|
+
# Check if we already have this node
|
|
93
|
+
if dep_name in self.nodes:
|
|
94
|
+
# Mark as circular if we're revisiting
|
|
95
|
+
if dep_name in self._visited:
|
|
96
|
+
self.nodes[dep_name].circular = True
|
|
97
|
+
continue
|
|
98
|
+
|
|
99
|
+
# Create new node
|
|
100
|
+
dep_node = DependencyNode(name=dep_name)
|
|
101
|
+
|
|
102
|
+
# Try to find the library
|
|
103
|
+
dep_path = self._find_library(dep_name)
|
|
104
|
+
if dep_path:
|
|
105
|
+
dep_node.path = dep_path
|
|
106
|
+
self.nodes[dep_name] = dep_node
|
|
107
|
+
# Recurse
|
|
108
|
+
self._build_recursive(dep_path, dep_node)
|
|
109
|
+
else:
|
|
110
|
+
# Unresolved dependency
|
|
111
|
+
dep_node.unresolved = True
|
|
112
|
+
self.nodes[dep_name] = dep_node
|
|
113
|
+
|
|
114
|
+
except Exception:
|
|
115
|
+
# If we can't parse, mark dependencies as unknown
|
|
116
|
+
node.unresolved = True
|
|
117
|
+
|
|
118
|
+
def _find_library(self, lib_name: str) -> Optional[str]:
|
|
119
|
+
"""Find a library in the search paths.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
lib_name: Library name
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
Full path or None
|
|
126
|
+
"""
|
|
127
|
+
for search_path in self.library_paths:
|
|
128
|
+
lib_path = search_path / lib_name
|
|
129
|
+
if lib_path.exists():
|
|
130
|
+
return str(lib_path)
|
|
131
|
+
|
|
132
|
+
return None
|
|
133
|
+
|
|
134
|
+
def get_unresolved_dependencies(self) -> List[str]:
|
|
135
|
+
"""Get list of unresolved dependencies.
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
List of library names that couldn't be found
|
|
139
|
+
"""
|
|
140
|
+
return [
|
|
141
|
+
node.name
|
|
142
|
+
for node in self.nodes.values()
|
|
143
|
+
if node.unresolved
|
|
144
|
+
]
|
|
145
|
+
|
|
146
|
+
def get_dependency_chain(self, target_lib: str) -> Optional[List[str]]:
|
|
147
|
+
"""Get the dependency chain to a specific library.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
target_lib: Target library name
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
List of library names in the chain, or None if not found
|
|
154
|
+
"""
|
|
155
|
+
# BFS to find shortest path
|
|
156
|
+
from collections import deque
|
|
157
|
+
|
|
158
|
+
if not self.nodes:
|
|
159
|
+
return None
|
|
160
|
+
|
|
161
|
+
# Start from root (first node)
|
|
162
|
+
root_name = next(iter(self.nodes.keys()))
|
|
163
|
+
|
|
164
|
+
queue = deque([(root_name, [root_name])])
|
|
165
|
+
visited = {root_name}
|
|
166
|
+
|
|
167
|
+
while queue:
|
|
168
|
+
current, path = queue.popleft()
|
|
169
|
+
|
|
170
|
+
if current == target_lib:
|
|
171
|
+
return path
|
|
172
|
+
|
|
173
|
+
node = self.nodes.get(current)
|
|
174
|
+
if node:
|
|
175
|
+
for dep in node.dependencies:
|
|
176
|
+
if dep not in visited:
|
|
177
|
+
visited.add(dep)
|
|
178
|
+
queue.append((dep, path + [dep]))
|
|
179
|
+
|
|
180
|
+
return None
|
|
181
|
+
|
|
182
|
+
def has_circular_dependencies(self) -> bool:
|
|
183
|
+
"""Check if the graph has circular dependencies.
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
True if circular dependencies exist
|
|
187
|
+
"""
|
|
188
|
+
return any(node.circular for node in self.nodes.values())
|
|
189
|
+
|
|
190
|
+
def to_dict(self) -> Dict[str, object]:
|
|
191
|
+
"""Convert graph to dictionary representation.
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
Dictionary with graph structure
|
|
195
|
+
"""
|
|
196
|
+
return {
|
|
197
|
+
"nodes": {
|
|
198
|
+
name: {
|
|
199
|
+
"path": node.path,
|
|
200
|
+
"dependencies": node.dependencies,
|
|
201
|
+
"unresolved": node.unresolved,
|
|
202
|
+
"circular": node.circular,
|
|
203
|
+
}
|
|
204
|
+
for name, node in self.nodes.items()
|
|
205
|
+
},
|
|
206
|
+
"unresolved_count": len(self.get_unresolved_dependencies()),
|
|
207
|
+
"has_circular": self.has_circular_dependencies(),
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def build_dependency_graph(
|
|
212
|
+
binary_path: str,
|
|
213
|
+
library_paths: Optional[List[str]] = None
|
|
214
|
+
) -> DependencyGraph:
|
|
215
|
+
"""Convenience function to build a dependency graph.
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
binary_path: Path to the ELF binary
|
|
219
|
+
library_paths: Optional library search paths
|
|
220
|
+
|
|
221
|
+
Returns:
|
|
222
|
+
DependencyGraph instance
|
|
223
|
+
"""
|
|
224
|
+
graph = DependencyGraph(library_paths)
|
|
225
|
+
graph.build_from_binary(binary_path)
|
|
226
|
+
return graph
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""Dynamic linker simulation for predicting runtime failures.
|
|
2
|
+
|
|
3
|
+
Simulates the behavior of ld.so to predict symbol resolution failures
|
|
4
|
+
before they occur at runtime.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from typing import Dict, List, Optional
|
|
11
|
+
|
|
12
|
+
from pybinaryguard.predictor.dependency_graph import DependencyGraph
|
|
13
|
+
from pybinaryguard.predictor.resolver import SymbolResolver
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class LinkageResult:
|
|
18
|
+
"""Result of simulated dynamic linking."""
|
|
19
|
+
|
|
20
|
+
success: bool
|
|
21
|
+
missing_symbols: List[str]
|
|
22
|
+
missing_libraries: List[str]
|
|
23
|
+
symbol_sources: Dict[str, Optional[str]] # symbol -> providing library
|
|
24
|
+
error_message: Optional[str] = None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class LinkerSimulator:
|
|
28
|
+
"""Simulates dynamic linker behavior to predict failures."""
|
|
29
|
+
|
|
30
|
+
def __init__(self, library_paths: Optional[List[str]] = None):
|
|
31
|
+
"""Initialize the linker simulator.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
library_paths: Directories to search for libraries
|
|
35
|
+
"""
|
|
36
|
+
self.resolver = SymbolResolver(library_paths)
|
|
37
|
+
self.library_paths = library_paths
|
|
38
|
+
|
|
39
|
+
def simulate_load(self, binary_path: str) -> LinkageResult:
|
|
40
|
+
"""Simulate loading a binary and resolving all symbols.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
binary_path: Path to the ELF binary
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
LinkageResult with success status and details
|
|
47
|
+
"""
|
|
48
|
+
# Build dependency graph
|
|
49
|
+
dep_graph = DependencyGraph(self.library_paths)
|
|
50
|
+
root_node = dep_graph.build_from_binary(binary_path)
|
|
51
|
+
|
|
52
|
+
# Check for missing libraries
|
|
53
|
+
missing_libs = dep_graph.get_unresolved_dependencies()
|
|
54
|
+
|
|
55
|
+
if missing_libs:
|
|
56
|
+
return LinkageResult(
|
|
57
|
+
success=False,
|
|
58
|
+
missing_symbols=[],
|
|
59
|
+
missing_libraries=missing_libs,
|
|
60
|
+
symbol_sources={},
|
|
61
|
+
error_message=f"Missing libraries: {', '.join(missing_libs)}"
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
# Try to resolve symbols
|
|
65
|
+
needed_libs = root_node.dependencies
|
|
66
|
+
symbol_resolution = self.resolver.resolve_undefined_symbols(
|
|
67
|
+
binary_path,
|
|
68
|
+
needed_libs
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
# Find unresolved symbols
|
|
72
|
+
missing_symbols = [
|
|
73
|
+
sym for sym, provider in symbol_resolution.items()
|
|
74
|
+
if provider is None
|
|
75
|
+
]
|
|
76
|
+
|
|
77
|
+
if missing_symbols:
|
|
78
|
+
# Format error message similar to actual ImportError
|
|
79
|
+
first_missing = missing_symbols[0]
|
|
80
|
+
error_msg = (
|
|
81
|
+
f"ImportError: {binary_path}: "
|
|
82
|
+
f"undefined symbol: {first_missing}"
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
return LinkageResult(
|
|
86
|
+
success=False,
|
|
87
|
+
missing_symbols=missing_symbols,
|
|
88
|
+
missing_libraries=[],
|
|
89
|
+
symbol_sources=symbol_resolution,
|
|
90
|
+
error_message=error_msg
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
# Check for circular dependencies
|
|
94
|
+
if dep_graph.has_circular_dependencies():
|
|
95
|
+
return LinkageResult(
|
|
96
|
+
success=True, # Still successful, but warn
|
|
97
|
+
missing_symbols=[],
|
|
98
|
+
missing_libraries=[],
|
|
99
|
+
symbol_sources=symbol_resolution,
|
|
100
|
+
error_message="Warning: Circular dependencies detected"
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
return LinkageResult(
|
|
104
|
+
success=True,
|
|
105
|
+
missing_symbols=[],
|
|
106
|
+
missing_libraries=[],
|
|
107
|
+
symbol_sources=symbol_resolution,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
def predict_import_error(
|
|
111
|
+
self,
|
|
112
|
+
module_path: str,
|
|
113
|
+
extension_name: str
|
|
114
|
+
) -> Optional[str]:
|
|
115
|
+
"""Predict ImportError for a Python extension module.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
module_path: Path to the extension module (.so file)
|
|
119
|
+
extension_name: Name of the extension (for error message)
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
Predicted error message, or None if import should succeed
|
|
123
|
+
"""
|
|
124
|
+
result = self.simulate_load(module_path)
|
|
125
|
+
|
|
126
|
+
if not result.success:
|
|
127
|
+
if result.missing_libraries:
|
|
128
|
+
# Format like actual ImportError
|
|
129
|
+
missing = result.missing_libraries[0]
|
|
130
|
+
return (
|
|
131
|
+
f"ImportError: {extension_name}: "
|
|
132
|
+
f"cannot open shared object file: {missing}: "
|
|
133
|
+
f"No such file or directory"
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
if result.missing_symbols:
|
|
137
|
+
# Format like actual ImportError
|
|
138
|
+
missing = result.missing_symbols[0]
|
|
139
|
+
return (
|
|
140
|
+
f"ImportError: {module_path}: "
|
|
141
|
+
f"undefined symbol: {missing}"
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
return None
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def simulate_import(
|
|
148
|
+
binary_path: str,
|
|
149
|
+
library_paths: Optional[List[str]] = None
|
|
150
|
+
) -> LinkageResult:
|
|
151
|
+
"""Convenience function to simulate importing a binary.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
binary_path: Path to the ELF binary
|
|
155
|
+
library_paths: Optional library search paths
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
LinkageResult with simulation results
|
|
159
|
+
"""
|
|
160
|
+
simulator = LinkerSimulator(library_paths)
|
|
161
|
+
return simulator.simulate_load(binary_path)
|