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,40 @@
1
+ """Abstract base class for all binary analyzers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from abc import ABC, abstractmethod
6
+
7
+ from pybinaryguard.models.package import PackageBinaryInfo
8
+
9
+
10
+ class AnalyzerBase(ABC):
11
+ """Base class for binary analyzers.
12
+
13
+ Every analyzer must:
14
+ - Have a unique ``name`` attribute used for identification and logging.
15
+ - Implement ``analyze()`` which enriches a ``PackageBinaryInfo`` in place
16
+ and returns it.
17
+ - Be completely read-only -- analyzers must never modify files on disk.
18
+ - Handle all internal I/O errors gracefully rather than raising.
19
+ """
20
+
21
+ name: str # Analyzer identifier -- must be set by subclasses
22
+
23
+ @abstractmethod
24
+ def analyze(self, package_info: PackageBinaryInfo) -> PackageBinaryInfo:
25
+ """Analyze and enrich the *package_info* with findings.
26
+
27
+ Parameters
28
+ ----------
29
+ package_info:
30
+ The package descriptor to populate. The analyzer reads
31
+ binary files referenced by the descriptor, extracts metadata,
32
+ and mutates the descriptor's fields accordingly.
33
+
34
+ Returns
35
+ -------
36
+ PackageBinaryInfo
37
+ The same *package_info* instance (mutated in place) so callers
38
+ can chain analyzers conveniently.
39
+ """
40
+ ...
@@ -0,0 +1,336 @@
1
+ """Dependency analyzer -- resolves DT_NEEDED chains statically.
2
+
3
+ For each shared library required by a package's ``.so`` files, this
4
+ analyzer checks whether the library can be found at runtime without
5
+ executing any binaries. Libraries that cannot be resolved are added to
6
+ ``PackageBinaryInfo.missing_libraries``.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import logging
12
+ import os
13
+ import platform
14
+ from typing import Dict, List, Optional, Set, Tuple
15
+
16
+ from pybinaryguard.analyzers.base import AnalyzerBase
17
+ from pybinaryguard.models.package import PackageBinaryInfo
18
+ from pybinaryguard.models.system import SystemProfile
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+ # Standard library search paths per architecture
23
+ _STANDARD_LIB_PATHS: Tuple[str, ...] = (
24
+ "/lib",
25
+ "/lib64",
26
+ "/usr/lib",
27
+ "/usr/lib64",
28
+ "/usr/local/lib",
29
+ "/usr/local/lib64",
30
+ )
31
+
32
+ # Architecture-specific multilib directories
33
+ _ARCH_LIB_DIRS: Dict[str, Tuple[str, ...]] = {
34
+ "x86_64": (
35
+ "/lib/x86_64-linux-gnu",
36
+ "/usr/lib/x86_64-linux-gnu",
37
+ ),
38
+ "aarch64": (
39
+ "/lib/aarch64-linux-gnu",
40
+ "/usr/lib/aarch64-linux-gnu",
41
+ ),
42
+ "armv7l": (
43
+ "/lib/arm-linux-gnueabihf",
44
+ "/usr/lib/arm-linux-gnueabihf",
45
+ ),
46
+ "i686": (
47
+ "/lib/i386-linux-gnu",
48
+ "/usr/lib/i386-linux-gnu",
49
+ "/lib32",
50
+ "/usr/lib32",
51
+ ),
52
+ "ppc64le": (
53
+ "/lib/powerpc64le-linux-gnu",
54
+ "/usr/lib/powerpc64le-linux-gnu",
55
+ ),
56
+ "s390x": (
57
+ "/lib/s390x-linux-gnu",
58
+ "/usr/lib/s390x-linux-gnu",
59
+ ),
60
+ }
61
+
62
+ # Libraries that are always expected to be provided by the C runtime and
63
+ # should not be flagged as missing.
64
+ _ALWAYS_PRESENT: frozenset = frozenset({
65
+ "linux-vdso.so.1",
66
+ "linux-gate.so.1",
67
+ "linux-vdso64.so.1",
68
+ "ld-linux-x86-64.so.2",
69
+ "ld-linux.so.2",
70
+ "ld-linux-aarch64.so.1",
71
+ "ld-linux-armhf.so.3",
72
+ "ld-linux-riscv64-lp64d.so.1",
73
+ "ld64.so.1",
74
+ "ld64.so.2",
75
+ })
76
+
77
+
78
+ class DependencyAnalyzer(AnalyzerBase):
79
+ """Resolves DT_NEEDED library dependencies through static path searching.
80
+
81
+ For each library referenced by a package's shared objects, the analyzer
82
+ searches (in order):
83
+
84
+ 1. ``DT_RPATH`` / ``DT_RUNPATH`` from the binary itself
85
+ 2. ``LD_LIBRARY_PATH`` from the system profile
86
+ 3. The ldconfig cache (``SystemProfile.ldconfig_cache``)
87
+ 4. Standard library directories (``/lib``, ``/usr/lib``, arch-specific)
88
+
89
+ Libraries that cannot be found are added to
90
+ ``PackageBinaryInfo.missing_libraries``.
91
+
92
+ This analyzer never executes any binary (no ``ldd``, no ``ldconfig``).
93
+ """
94
+
95
+ name: str = "dependency"
96
+
97
+ def __init__(self, system_profile: Optional[SystemProfile] = None) -> None:
98
+ self._profile = system_profile
99
+
100
+ def analyze(self, package_info: PackageBinaryInfo) -> PackageBinaryInfo:
101
+ """Resolve library dependencies and flag missing ones.
102
+
103
+ Parameters
104
+ ----------
105
+ package_info:
106
+ The package descriptor whose ``shared_objects`` list has
107
+ already been populated by the ELF analyzer.
108
+
109
+ Returns
110
+ -------
111
+ PackageBinaryInfo
112
+ The same (mutated) instance.
113
+ """
114
+ if not package_info.shared_objects:
115
+ return package_info
116
+
117
+ # Build the set of all DT_NEEDED libraries across the package
118
+ all_needed: Set[str] = set()
119
+ for so in package_info.shared_objects:
120
+ for lib in so.dt_needed:
121
+ all_needed.add(lib)
122
+
123
+ if not all_needed:
124
+ return package_info
125
+
126
+ # Collect search paths from system profile
127
+ ld_library_paths = self._get_ld_library_path()
128
+ ldconfig_cache = self._get_ldconfig_cache()
129
+ arch_dirs = self._get_arch_lib_dirs()
130
+
131
+ # Collect per-binary RPATH/RUNPATH
132
+ rpath_dirs: List[str] = []
133
+ runpath_dirs: List[str] = []
134
+ for so in package_info.shared_objects:
135
+ if so.dt_rpath:
136
+ for p in so.dt_rpath.split(":"):
137
+ resolved = self._resolve_rpath_token(p, so.path)
138
+ if resolved and resolved not in rpath_dirs:
139
+ rpath_dirs.append(resolved)
140
+ if so.dt_runpath:
141
+ for p in so.dt_runpath.split(":"):
142
+ resolved = self._resolve_rpath_token(p, so.path)
143
+ if resolved and resolved not in runpath_dirs:
144
+ runpath_dirs.append(resolved)
145
+
146
+ # Also include the package's own install path as a search location
147
+ # (many packages bundle their own .so files alongside each other)
148
+ package_lib_dirs: List[str] = []
149
+ if os.path.isdir(package_info.install_path):
150
+ package_lib_dirs.append(package_info.install_path)
151
+ # Also check common subdirectories
152
+ for subdir in ("lib", "libs", ".libs"):
153
+ candidate = os.path.join(package_info.install_path, subdir)
154
+ if os.path.isdir(candidate):
155
+ package_lib_dirs.append(candidate)
156
+
157
+ # Build the names of libraries that the package itself provides
158
+ # (so intra-package deps don't get flagged as missing)
159
+ provided_by_package: Set[str] = set()
160
+ for so in package_info.shared_objects:
161
+ provided_by_package.add(so.filename)
162
+ if so.dt_soname:
163
+ provided_by_package.add(so.dt_soname)
164
+
165
+ # Resolve each library
166
+ missing: Set[str] = set()
167
+ for lib_name in sorted(all_needed):
168
+ if lib_name in _ALWAYS_PRESENT:
169
+ continue
170
+ if lib_name in provided_by_package:
171
+ continue
172
+ if not self._find_library(
173
+ lib_name,
174
+ rpath_dirs=rpath_dirs,
175
+ runpath_dirs=runpath_dirs,
176
+ ld_library_paths=ld_library_paths,
177
+ ldconfig_cache=ldconfig_cache,
178
+ package_dirs=package_lib_dirs,
179
+ arch_dirs=arch_dirs,
180
+ ):
181
+ missing.add(lib_name)
182
+
183
+ package_info.missing_libraries = missing
184
+ return package_info
185
+
186
+ # ------------------------------------------------------------------
187
+ # Library resolution
188
+ # ------------------------------------------------------------------
189
+
190
+ @staticmethod
191
+ def _find_library(
192
+ name: str,
193
+ *,
194
+ rpath_dirs: List[str],
195
+ runpath_dirs: List[str],
196
+ ld_library_paths: List[str],
197
+ ldconfig_cache: Dict[str, str],
198
+ package_dirs: List[str],
199
+ arch_dirs: List[str],
200
+ ) -> bool:
201
+ """Search for a library by name using the ELF resolution order.
202
+
203
+ The search order mirrors the dynamic linker (ld.so) behavior:
204
+
205
+ 1. DT_RPATH (deprecated but still used)
206
+ 2. LD_LIBRARY_PATH
207
+ 3. DT_RUNPATH
208
+ 4. ldconfig cache
209
+ 5. Package-internal directories
210
+ 6. Standard and architecture-specific paths
211
+
212
+ Parameters
213
+ ----------
214
+ name:
215
+ The library file name (e.g. ``libz.so.1``).
216
+
217
+ Returns
218
+ -------
219
+ bool
220
+ ``True`` if the library was found.
221
+ """
222
+ # 1. RPATH
223
+ for d in rpath_dirs:
224
+ if _file_exists(os.path.join(d, name)):
225
+ return True
226
+
227
+ # 2. LD_LIBRARY_PATH
228
+ for d in ld_library_paths:
229
+ if _file_exists(os.path.join(d, name)):
230
+ return True
231
+
232
+ # 3. RUNPATH
233
+ for d in runpath_dirs:
234
+ if _file_exists(os.path.join(d, name)):
235
+ return True
236
+
237
+ # 4. ldconfig cache
238
+ if name in ldconfig_cache:
239
+ path = ldconfig_cache[name]
240
+ if _file_exists(path):
241
+ return True
242
+
243
+ # 5. Package-internal directories
244
+ for d in package_dirs:
245
+ if _file_exists(os.path.join(d, name)):
246
+ return True
247
+
248
+ # 6. Standard + arch-specific paths
249
+ for d in list(_STANDARD_LIB_PATHS) + arch_dirs:
250
+ if _file_exists(os.path.join(d, name)):
251
+ return True
252
+
253
+ return False
254
+
255
+ # ------------------------------------------------------------------
256
+ # RPATH token resolution
257
+ # ------------------------------------------------------------------
258
+
259
+ @staticmethod
260
+ def _resolve_rpath_token(path: str, binary_path: str) -> Optional[str]:
261
+ """Resolve ``$ORIGIN`` and ``${ORIGIN}`` tokens in RPATH/RUNPATH.
262
+
263
+ Parameters
264
+ ----------
265
+ path:
266
+ A single path component from RPATH/RUNPATH.
267
+ binary_path:
268
+ The absolute path to the binary that contains the RPATH.
269
+
270
+ Returns
271
+ -------
272
+ str or None
273
+ The resolved absolute path, or ``None`` if invalid.
274
+ """
275
+ if not path:
276
+ return None
277
+
278
+ origin = os.path.dirname(os.path.abspath(binary_path))
279
+ resolved = path.replace("$ORIGIN", origin).replace("${ORIGIN}", origin)
280
+ resolved = os.path.normpath(resolved)
281
+
282
+ if os.path.isdir(resolved):
283
+ return resolved
284
+
285
+ # The directory might not exist yet on the system being analyzed,
286
+ # but we still return it so the caller can check
287
+ return resolved
288
+
289
+ # ------------------------------------------------------------------
290
+ # Profile helpers
291
+ # ------------------------------------------------------------------
292
+
293
+ def _get_ld_library_path(self) -> List[str]:
294
+ """Return LD_LIBRARY_PATH entries from the system profile or env."""
295
+ if self._profile is not None and self._profile.ld_library_path:
296
+ return list(self._profile.ld_library_path)
297
+
298
+ # Fallback: read from the current environment
299
+ env_val = os.environ.get("LD_LIBRARY_PATH", "")
300
+ if env_val:
301
+ return [p for p in env_val.split(":") if p]
302
+ return []
303
+
304
+ def _get_ldconfig_cache(self) -> Dict[str, str]:
305
+ """Return the ldconfig cache mapping from the system profile."""
306
+ if self._profile is not None:
307
+ return dict(self._profile.ldconfig_cache)
308
+ return {}
309
+
310
+ def _get_arch_lib_dirs(self) -> List[str]:
311
+ """Return architecture-specific library directories."""
312
+ arch = ""
313
+ if self._profile is not None:
314
+ arch = self._profile.architecture.value
315
+ else:
316
+ arch = platform.machine()
317
+
318
+ dirs = list(_ARCH_LIB_DIRS.get(arch, ()))
319
+
320
+ # Also check the current machine if different
321
+ if not dirs:
322
+ machine = platform.machine()
323
+ dirs = list(_ARCH_LIB_DIRS.get(machine, ()))
324
+
325
+ return dirs
326
+
327
+
328
+ def _file_exists(path: str) -> bool:
329
+ """Check if *path* exists and is a file (or symlink to a file).
330
+
331
+ Silently returns ``False`` on permission errors.
332
+ """
333
+ try:
334
+ return os.path.isfile(path)
335
+ except OSError:
336
+ return False