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,320 @@
1
+ """GLIBC compatibility rules.
2
+
3
+ These rules detect mismatches between the GLIBC version available on the
4
+ host system and the GLIBC version required by installed binary packages.
5
+ They also flag musl/glibc conflicts and inconsistent manylinux wheel
6
+ metadata.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import re
12
+ from typing import List, Optional, Tuple
13
+
14
+ from pybinaryguard.models.enums import Severity
15
+ from pybinaryguard.models.finding import Finding
16
+ from pybinaryguard.models.package import PackageBinaryInfo
17
+ from pybinaryguard.models.system import SystemProfile
18
+ from pybinaryguard.rules.base import Rule
19
+
20
+
21
+ def _fmt_ver(version: Tuple[int, int]) -> str:
22
+ """Format a (major, minor) version tuple as ``'M.m'``."""
23
+ return f"{version[0]}.{version[1]}"
24
+
25
+
26
+ class GLIBCVersionMismatchRule(Rule):
27
+ """Detects packages that need a newer GLIBC than the system provides.
28
+
29
+ When a shared library is linked against GLIBC symbols that were
30
+ introduced in a release newer than what your OS ships, the library
31
+ will fail to load at import time with an ``ImportError`` referencing
32
+ a missing ``GLIBC_X.Y`` version. This is one of the most common
33
+ causes of "undefined symbol" crashes in Python packages with compiled
34
+ extensions.
35
+ """
36
+
37
+ rule_id = "GLIBC_VERSION_MISMATCH"
38
+ description = (
39
+ "Check that the system GLIBC version satisfies every package's "
40
+ "minimum requirement."
41
+ )
42
+
43
+ def is_applicable(self, profile: SystemProfile) -> bool:
44
+ """Only applicable on glibc-based systems."""
45
+ return profile.glibc_version is not None
46
+
47
+ def evaluate(
48
+ self,
49
+ profile: SystemProfile,
50
+ packages: List[PackageBinaryInfo],
51
+ ) -> List[Finding]:
52
+ findings: List[Finding] = []
53
+ sys_glibc = profile.glibc_version
54
+ if sys_glibc is None:
55
+ return findings
56
+
57
+ for pkg in packages:
58
+ if pkg.required_glibc is None:
59
+ continue
60
+ if pkg.required_glibc > sys_glibc:
61
+ findings.append(
62
+ Finding(
63
+ rule_id=self.rule_id,
64
+ severity=Severity.CRITICAL,
65
+ title=f"{pkg.package_name} requires a newer GLIBC",
66
+ explanation=(
67
+ f"Package {pkg.package_name} {pkg.package_version} "
68
+ f"requires GLIBC >= {_fmt_ver(pkg.required_glibc)} "
69
+ f"but your system has GLIBC "
70
+ f"{_fmt_ver(sys_glibc)}. This means the compiled "
71
+ f"extensions in this package use C library functions "
72
+ f"that do not exist on your OS, so the package will "
73
+ f"crash with an ImportError when you try to import it."
74
+ ),
75
+ technical_detail=(
76
+ f"System GLIBC: {_fmt_ver(sys_glibc)}, "
77
+ f"Required: {_fmt_ver(pkg.required_glibc)}"
78
+ ),
79
+ suggestion=(
80
+ f"Option 1 -- downgrade the package to an older "
81
+ f"version that was built for GLIBC "
82
+ f"{_fmt_ver(sys_glibc)}:\n"
83
+ f" pip install '{pkg.package_name}<"
84
+ f"{pkg.package_version}'\n\n"
85
+ f"Option 2 -- upgrade your OS to one that ships "
86
+ f"GLIBC >= {_fmt_ver(pkg.required_glibc)} "
87
+ f"(see the glibc_distro_map data for which "
88
+ f"distro versions include which GLIBC).\n\n"
89
+ f"Option 3 -- use a container image with a newer "
90
+ f"base OS:\n"
91
+ f" docker run -it python:3.x-bookworm"
92
+ ),
93
+ package=pkg.package_name,
94
+ package_version=pkg.package_version,
95
+ )
96
+ )
97
+ return findings
98
+
99
+
100
+ class MuslGlibcConflictRule(Rule):
101
+ """Detects manylinux packages running on a musl-based system.
102
+
103
+ Manylinux wheels are linked against glibc. Alpine Linux (and other
104
+ musl-based distros) ship musl instead, so these wheels will fail to
105
+ load. The typical symptom is an error like
106
+ ``Error loading shared library: libc.musl-x86_64.so.1``.
107
+ """
108
+
109
+ rule_id = "MUSL_GLIBC_CONFLICT"
110
+ description = (
111
+ "Flag manylinux packages on musl-based systems (e.g. Alpine)."
112
+ )
113
+
114
+ def is_applicable(self, profile: SystemProfile) -> bool:
115
+ """Only applicable on musl-based systems."""
116
+ return profile.musl_version is not None
117
+
118
+ def evaluate(
119
+ self,
120
+ profile: SystemProfile,
121
+ packages: List[PackageBinaryInfo],
122
+ ) -> List[Finding]:
123
+ findings: List[Finding] = []
124
+ for pkg in packages:
125
+ if pkg.manylinux_tag is None:
126
+ continue
127
+ findings.append(
128
+ Finding(
129
+ rule_id=self.rule_id,
130
+ severity=Severity.CRITICAL,
131
+ title=(
132
+ f"{pkg.package_name} is a manylinux wheel "
133
+ f"on a musl system"
134
+ ),
135
+ explanation=(
136
+ f"Package {pkg.package_name} {pkg.package_version} "
137
+ f"was built as a manylinux wheel (tagged "
138
+ f"{pkg.manylinux_tag}), which means it is linked "
139
+ f"against glibc. Your system uses musl libc "
140
+ f"(commonly Alpine Linux), so these compiled "
141
+ f"extensions cannot run."
142
+ ),
143
+ technical_detail=(
144
+ f"Wheel platform tag: {pkg.manylinux_tag}, "
145
+ f"System libc: musl "
146
+ f"{_fmt_ver(profile.musl_version) if profile.musl_version else 'unknown'}"
147
+ ),
148
+ suggestion=(
149
+ f"Option 1 -- install a musllinux or Alpine-specific "
150
+ f"wheel if one is published:\n"
151
+ f" pip install --only-binary :all: "
152
+ f"--platform musllinux_1_1_x86_64 "
153
+ f"{pkg.package_name}\n\n"
154
+ f"Option 2 -- install via conda-forge, which provides "
155
+ f"musl-compatible builds:\n"
156
+ f" conda install -c conda-forge {pkg.package_name}\n\n"
157
+ f"Option 3 -- build from source:\n"
158
+ f" apk add build-base && pip install --no-binary "
159
+ f":all: {pkg.package_name}"
160
+ ),
161
+ package=pkg.package_name,
162
+ package_version=pkg.package_version,
163
+ )
164
+ )
165
+ return findings
166
+
167
+
168
+ class ManylinuxTagViolationRule(Rule):
169
+ """Detects wheels whose .so files need a newer GLIBC than the tag claims.
170
+
171
+ A manylinux tag (e.g. ``manylinux_2_17``) promises that the wheel
172
+ only requires GLIBC 2.17 or later. If the actual shared objects
173
+ inside the wheel reference symbols from a newer GLIBC, the tag is
174
+ wrong: the wheel will fail to load on older glibc systems that the
175
+ tag claims to support.
176
+ """
177
+
178
+ rule_id = "MANYLINUX_TAG_VIOLATION"
179
+ description = (
180
+ "Warn when a wheel's actual GLIBC requirement exceeds its "
181
+ "manylinux platform tag."
182
+ )
183
+
184
+ def evaluate(
185
+ self,
186
+ profile: SystemProfile,
187
+ packages: List[PackageBinaryInfo],
188
+ ) -> List[Finding]:
189
+ findings: List[Finding] = []
190
+ for pkg in packages:
191
+ claimed = pkg.manylinux_glibc
192
+ actual = pkg.required_glibc
193
+ if claimed is None or actual is None:
194
+ continue
195
+ if actual > claimed:
196
+ findings.append(
197
+ Finding(
198
+ rule_id=self.rule_id,
199
+ severity=Severity.WARNING,
200
+ title=(
201
+ f"{pkg.package_name} wheel tag is inconsistent"
202
+ ),
203
+ explanation=(
204
+ f"The wheel for {pkg.package_name} "
205
+ f"{pkg.package_version} claims to be compatible "
206
+ f"with manylinux (GLIBC >= {_fmt_ver(claimed)}), "
207
+ f"but its .so files actually require GLIBC "
208
+ f"{_fmt_ver(actual)}. This means the wheel was "
209
+ f"built incorrectly and will break on older Linux "
210
+ f"distributions that only have GLIBC "
211
+ f"{_fmt_ver(claimed)}."
212
+ ),
213
+ technical_detail=(
214
+ f"Manylinux tag claims GLIBC "
215
+ f"{_fmt_ver(claimed)}, actual requirement "
216
+ f"is GLIBC {_fmt_ver(actual)}"
217
+ ),
218
+ suggestion=(
219
+ f"This is a packaging bug in {pkg.package_name}. "
220
+ f"Consider filing an issue with the package "
221
+ f"maintainers. In the meantime, you can work "
222
+ f"around it by installing a previous version or "
223
+ f"building from source."
224
+ ),
225
+ package=pkg.package_name,
226
+ package_version=pkg.package_version,
227
+ confidence=0.9,
228
+ )
229
+ )
230
+ return findings
231
+
232
+
233
+ class LibstdcxxVersionRule(Rule):
234
+ """Detects packages that need a newer libstdc++ (GLIBCXX) than available.
235
+
236
+ C++ extensions link against ``libstdc++.so`` and may require symbol
237
+ versions (``GLIBCXX_X.Y.Z``) that only exist in newer GCC releases.
238
+ When the system's ``libstdc++`` is too old you get an error like:
239
+ ``version 'GLIBCXX_3.4.30' not found``.
240
+ """
241
+
242
+ rule_id = "LIBSTDCXX_TOO_OLD"
243
+ description = (
244
+ "Check that the system libstdc++ provides all GLIBCXX versions "
245
+ "required by installed packages."
246
+ )
247
+
248
+ def is_applicable(self, profile: SystemProfile) -> bool:
249
+ """Only relevant on glibc-based systems (libstdc++ ships with GCC)."""
250
+ return profile.glibc_version is not None
251
+
252
+ def evaluate(
253
+ self,
254
+ profile: SystemProfile,
255
+ packages: List[PackageBinaryInfo],
256
+ ) -> List[Finding]:
257
+ findings: List[Finding] = []
258
+ for pkg in packages:
259
+ if pkg.required_glibcxx is None:
260
+ continue
261
+ # Check each shared object that has a GLIBCXX requirement.
262
+ for so in pkg.shared_objects:
263
+ if so.required_glibcxx is None:
264
+ continue
265
+ # GLIBCXX versions look like "3.4.30". We compare them
266
+ # lexicographically after splitting into int tuples.
267
+ so_ver = _parse_glibcxx(so.required_glibcxx)
268
+ pkg_ver = _parse_glibcxx(pkg.required_glibcxx)
269
+ if so_ver is None or pkg_ver is None:
270
+ continue
271
+ # The package-level required_glibcxx should already be the
272
+ # max across all .so files, so we report at the package
273
+ # level once.
274
+ if pkg.required_glibcxx is not None:
275
+ findings.append(
276
+ Finding(
277
+ rule_id=self.rule_id,
278
+ severity=Severity.CRITICAL,
279
+ title=(
280
+ f"{pkg.package_name} may need a newer libstdc++"
281
+ ),
282
+ explanation=(
283
+ f"Package {pkg.package_name} "
284
+ f"{pkg.package_version} contains C++ extensions "
285
+ f"that require GLIBCXX version "
286
+ f"{pkg.required_glibcxx}. If your system's "
287
+ f"libstdc++ does not provide this version you "
288
+ f"will see an error like 'version GLIBCXX_"
289
+ f"{pkg.required_glibcxx} not found' when "
290
+ f"importing the package."
291
+ ),
292
+ technical_detail=(
293
+ f"Required GLIBCXX: {pkg.required_glibcxx}"
294
+ ),
295
+ suggestion=(
296
+ f"Option 1 -- upgrade GCC / libstdc++:\n"
297
+ f" sudo apt install libstdc++6 # Debian/Ubuntu\n"
298
+ f" sudo dnf install libstdc++ # Fedora/RHEL\n\n"
299
+ f"Option 2 -- use conda which bundles its own "
300
+ f"libstdc++:\n"
301
+ f" conda install -c conda-forge "
302
+ f"{pkg.package_name}\n\n"
303
+ f"Option 3 -- install an older version of the "
304
+ f"package that was compiled with an older GCC."
305
+ ),
306
+ package=pkg.package_name,
307
+ package_version=pkg.package_version,
308
+ confidence=0.8,
309
+ )
310
+ )
311
+ return findings
312
+
313
+
314
+ def _parse_glibcxx(version_str: str) -> Optional[Tuple[int, ...]]:
315
+ """Parse a GLIBCXX version string like ``'3.4.30'`` into an int tuple."""
316
+ parts = version_str.split(".")
317
+ try:
318
+ return tuple(int(p) for p in parts)
319
+ except (ValueError, TypeError):
320
+ return None
@@ -0,0 +1,110 @@
1
+ """NumPy ABI compatibility rules.
2
+
3
+ NumPy exposes a C API for packages that build compiled extensions against
4
+ it (e.g. SciPy, pandas, scikit-learn). The API version is baked into
5
+ the extension at compile time; if the installed NumPy provides a different
6
+ API version the extension may crash or produce wrong results.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from typing import List
12
+
13
+ from pybinaryguard.models.enums import Severity
14
+ from pybinaryguard.models.finding import Finding
15
+ from pybinaryguard.models.package import PackageBinaryInfo
16
+ from pybinaryguard.models.system import SystemProfile
17
+ from pybinaryguard.rules.base import Rule
18
+
19
+
20
+ def _find_numpy(packages: List[PackageBinaryInfo]) -> PackageBinaryInfo | None:
21
+ """Return the numpy PackageBinaryInfo from the package list, if present."""
22
+ for pkg in packages:
23
+ if pkg.package_name.lower() == "numpy":
24
+ return pkg
25
+ return None
26
+
27
+
28
+ class NumpyABIMismatchRule(Rule):
29
+ """Detects packages compiled against a different NumPy C API version.
30
+
31
+ When a package is compiled against NumPy's C API (``numpy/arrayobject.h``
32
+ etc.), the ``NPY_VERSION`` / ``numpy_api_version`` constant is recorded.
33
+ If the installed NumPy has a different API version, the struct layouts
34
+ may differ and the extension may segfault or produce corrupt data.
35
+
36
+ The typical symptom is a ``RuntimeWarning: numpy.dtype size changed,
37
+ may indicate binary incompatibility`` or a hard crash.
38
+ """
39
+
40
+ rule_id = "NUMPY_ABI_MISMATCH"
41
+ description = (
42
+ "Check that packages compiled against the NumPy C API match the "
43
+ "installed NumPy's API version."
44
+ )
45
+
46
+ def evaluate(
47
+ self,
48
+ profile: SystemProfile,
49
+ packages: List[PackageBinaryInfo],
50
+ ) -> List[Finding]:
51
+ findings: List[Finding] = []
52
+
53
+ numpy_pkg = _find_numpy(packages)
54
+ if numpy_pkg is None:
55
+ # NumPy is not installed; nothing to check.
56
+ return findings
57
+
58
+ numpy_api = numpy_pkg.numpy_api_version
59
+ if numpy_api is None:
60
+ # We do not know NumPy's own API version; skip.
61
+ return findings
62
+
63
+ for pkg in packages:
64
+ if pkg.package_name.lower() == "numpy":
65
+ continue
66
+ if pkg.numpy_api_version is None:
67
+ continue
68
+ if pkg.numpy_api_version != numpy_api:
69
+ findings.append(
70
+ Finding(
71
+ rule_id=self.rule_id,
72
+ severity=Severity.CRITICAL,
73
+ title=(
74
+ f"{pkg.package_name} was built against a "
75
+ f"different NumPy ABI"
76
+ ),
77
+ explanation=(
78
+ f"Package {pkg.package_name} "
79
+ f"{pkg.package_version} was compiled against "
80
+ f"NumPy C API version {pkg.numpy_api_version:#x} "
81
+ f"but the installed NumPy "
82
+ f"({numpy_pkg.package_version}) provides API "
83
+ f"version {numpy_api:#x}. This mismatch means "
84
+ f"the internal array and dtype struct layouts "
85
+ f"may differ, which can cause segmentation "
86
+ f"faults, data corruption, or RuntimeWarnings "
87
+ f"about 'dtype size changed'."
88
+ ),
89
+ technical_detail=(
90
+ f"Package numpy_api_version: "
91
+ f"{pkg.numpy_api_version:#x}, "
92
+ f"Installed NumPy API version: "
93
+ f"{numpy_api:#x}"
94
+ ),
95
+ suggestion=(
96
+ f"Rebuild {pkg.package_name} against the "
97
+ f"currently installed NumPy:\n"
98
+ f" pip install --no-binary :all: --force-reinstall "
99
+ f"{pkg.package_name}\n\n"
100
+ f"Or install a version of {pkg.package_name} "
101
+ f"that was built for NumPy "
102
+ f"{numpy_pkg.package_version}:\n"
103
+ f" pip install --force-reinstall "
104
+ f"{pkg.package_name}"
105
+ ),
106
+ package=pkg.package_name,
107
+ package_version=pkg.package_version,
108
+ )
109
+ )
110
+ return findings
@@ -0,0 +1,165 @@
1
+ """Predictive rules that simulate runtime failures before they occur.
2
+
3
+ These rules use the predictive failure engine to forecast ImportErrors
4
+ and symbol resolution failures.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import List
10
+
11
+ from pybinaryguard.models.enums import Severity
12
+ from pybinaryguard.models.finding import Finding
13
+ from pybinaryguard.models.package import PackageBinaryInfo
14
+ from pybinaryguard.models.system import SystemProfile
15
+ from pybinaryguard.rules.base import Rule
16
+
17
+
18
+ class PREDICTED_IMPORT_ERROR(Rule):
19
+ """Predicts ImportError before runtime using linker simulation."""
20
+
21
+ rule_id = "PREDICTED_IMPORT_ERROR"
22
+ description = "Predicts import failures via linker simulation"
23
+
24
+ def is_applicable(self, profile: SystemProfile) -> bool:
25
+ return True # Checked per-package inside evaluate
26
+
27
+ def evaluate(
28
+ self, profile: SystemProfile, packages: List[PackageBinaryInfo]
29
+ ) -> List[Finding]:
30
+ from pybinaryguard.predictor import predict_import_failures
31
+
32
+ findings: List[Finding] = []
33
+
34
+ for package in packages:
35
+ if not package.has_binaries or len(package.shared_objects) == 0:
36
+ continue
37
+
38
+ try:
39
+ predicted_failures = predict_import_failures(package, profile)
40
+
41
+ for failure in predicted_failures:
42
+ if failure.error_type == "SymbolError":
43
+ explanation = (
44
+ f"Predicted ImportError in {package.package_name} "
45
+ f"due to missing symbol: {failure.missing_symbol}."
46
+ )
47
+ suggestion = (
48
+ f"Install missing library providing symbol "
49
+ f"{failure.missing_symbol}, or use a different "
50
+ f"version of {package.package_name}."
51
+ )
52
+ elif failure.error_type == "LibraryMissing":
53
+ explanation = (
54
+ f"Predicted ImportError in {package.package_name} "
55
+ f"due to missing library: {failure.missing_library}."
56
+ )
57
+ suggestion = (
58
+ f"Install {failure.missing_library} using your "
59
+ f"package manager, or rebuild {package.package_name}."
60
+ )
61
+ else:
62
+ explanation = failure.error_message
63
+ suggestion = (
64
+ f"Review {package.package_name} installation "
65
+ f"and dependencies"
66
+ )
67
+
68
+ findings.append(
69
+ Finding(
70
+ rule_id=self.rule_id,
71
+ severity=Severity.CRITICAL,
72
+ title=f"ImportError predicted: {failure.error_type}",
73
+ explanation=explanation,
74
+ package=package.package_name,
75
+ technical_detail=f"Module: {failure.module_path}",
76
+ suggestion=suggestion,
77
+ confidence=failure.confidence,
78
+ )
79
+ )
80
+ except Exception:
81
+ pass
82
+
83
+ return findings
84
+
85
+
86
+ class UNRESOLVED_DEPENDENCY_CHAIN(Rule):
87
+ """Detects unresolved dependencies in the dependency graph."""
88
+
89
+ rule_id = "UNRESOLVED_DEPENDENCY_CHAIN"
90
+ description = "Checks for unresolved libraries in dependency chain"
91
+
92
+ def is_applicable(self, profile: SystemProfile) -> bool:
93
+ return True
94
+
95
+ def evaluate(
96
+ self, profile: SystemProfile, packages: List[PackageBinaryInfo]
97
+ ) -> List[Finding]:
98
+ from pybinaryguard.predictor import build_dependency_graph
99
+
100
+ findings: List[Finding] = []
101
+
102
+ for package in packages:
103
+ if not package.has_binaries or len(package.shared_objects) == 0:
104
+ continue
105
+
106
+ for so in package.shared_objects:
107
+ if not so.path:
108
+ continue
109
+
110
+ try:
111
+ graph = build_dependency_graph(so.path, None)
112
+ unresolved = graph.get_unresolved_dependencies()
113
+
114
+ if unresolved:
115
+ findings.append(
116
+ Finding(
117
+ rule_id=self.rule_id,
118
+ severity=Severity.WARNING,
119
+ title=(
120
+ f"Unresolved dependencies in "
121
+ f"{package.package_name}"
122
+ ),
123
+ explanation=(
124
+ f"Required but not found: "
125
+ f"{', '.join(unresolved)}."
126
+ ),
127
+ package=package.package_name,
128
+ technical_detail=f"Module: {so.path}",
129
+ suggestion=(
130
+ f"Install missing libraries or use a "
131
+ f"different build of {package.package_name}"
132
+ ),
133
+ confidence=0.85,
134
+ )
135
+ )
136
+
137
+ if graph.has_circular_dependencies():
138
+ findings.append(
139
+ Finding(
140
+ rule_id=self.rule_id,
141
+ severity=Severity.INFO,
142
+ title=(
143
+ f"Circular dependencies in "
144
+ f"{package.package_name}"
145
+ ),
146
+ explanation=(
147
+ "Circular dependencies detected in the "
148
+ "library dependency chain."
149
+ ),
150
+ package=package.package_name,
151
+ technical_detail=f"Module: {so.path}",
152
+ suggestion="Monitor for runtime issues",
153
+ confidence=0.7,
154
+ )
155
+ )
156
+ except Exception:
157
+ pass
158
+
159
+ return findings
160
+
161
+
162
+ __all__ = [
163
+ "PREDICTED_IMPORT_ERROR",
164
+ "UNRESOLVED_DEPENDENCY_CHAIN",
165
+ ]