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,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
|
+
]
|