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,282 @@
1
+ """Board profile-aware rules for embedded device intelligence.
2
+
3
+ These rules leverage board profiles to provide targeted diagnostics for
4
+ specific embedded hardware platforms.
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.profiles import match_board_profile
16
+ from pybinaryguard.rules.base import Rule
17
+
18
+
19
+ class KNOWN_BROKEN_WHEEL(Rule):
20
+ """Detects packages known to be broken on the detected board."""
21
+
22
+ rule_id = "KNOWN_BROKEN_WHEEL"
23
+ description = "Checks packages against board-specific broken wheel database"
24
+
25
+ def is_applicable(self, profile: SystemProfile) -> bool:
26
+ board_profile = match_board_profile(profile)
27
+ return board_profile is not None
28
+
29
+ def evaluate(
30
+ self, profile: SystemProfile, packages: List[PackageBinaryInfo]
31
+ ) -> List[Finding]:
32
+ findings: List[Finding] = []
33
+ board_profile = match_board_profile(profile)
34
+ if board_profile is None:
35
+ return findings
36
+
37
+ for package in packages:
38
+ if not package.has_binaries:
39
+ continue
40
+ package_version = package.package_version or None
41
+ broken = board_profile.is_package_known_broken(
42
+ package.package_name, package_version
43
+ )
44
+ if broken:
45
+ findings.append(
46
+ Finding(
47
+ rule_id=self.rule_id,
48
+ severity=Severity.CRITICAL,
49
+ title=(
50
+ f"{package.package_name} is known broken on "
51
+ f"{board_profile.display_name}"
52
+ ),
53
+ explanation=(
54
+ f"Package {package.package_name} version "
55
+ f"{broken.versions} is incompatible with "
56
+ f"{board_profile.display_name}. "
57
+ f"Reason: {broken.reason}"
58
+ ),
59
+ package=package.package_name,
60
+ suggestion=broken.recommendation,
61
+ confidence=1.0,
62
+ )
63
+ )
64
+ return findings
65
+
66
+
67
+ class BOARD_INCOMPATIBLE_PACKAGE(Rule):
68
+ """Detects packages fundamentally incompatible with the board."""
69
+
70
+ rule_id = "BOARD_INCOMPATIBLE_PACKAGE"
71
+ description = "Checks for packages that cannot work on detected hardware"
72
+
73
+ def is_applicable(self, profile: SystemProfile) -> bool:
74
+ board_profile = match_board_profile(profile)
75
+ return board_profile is not None
76
+
77
+ def evaluate(
78
+ self, profile: SystemProfile, packages: List[PackageBinaryInfo]
79
+ ) -> List[Finding]:
80
+ findings: List[Finding] = []
81
+ board_profile = match_board_profile(profile)
82
+ if board_profile is None:
83
+ return findings
84
+
85
+ for package in packages:
86
+ if board_profile.is_package_incompatible(package.package_name):
87
+ findings.append(
88
+ Finding(
89
+ rule_id=self.rule_id,
90
+ severity=Severity.CRITICAL,
91
+ title=(
92
+ f"{package.package_name} is incompatible with "
93
+ f"{board_profile.display_name}"
94
+ ),
95
+ explanation=(
96
+ f"Package {package.package_name} requires hardware "
97
+ f"features not available on {board_profile.display_name}."
98
+ ),
99
+ package=package.package_name,
100
+ suggestion=(
101
+ f"Remove {package.package_name} or use a board "
102
+ f"with required hardware features"
103
+ ),
104
+ confidence=1.0,
105
+ )
106
+ )
107
+ return findings
108
+
109
+
110
+ class BOARD_CUDA_VERSION_MISMATCH(Rule):
111
+ """Detects CUDA version exceeding board maximum."""
112
+
113
+ rule_id = "BOARD_CUDA_VERSION_MISMATCH"
114
+ description = "Checks if package requires CUDA version beyond board limit"
115
+
116
+ def is_applicable(self, profile: SystemProfile) -> bool:
117
+ board_profile = match_board_profile(profile)
118
+ return (
119
+ board_profile is not None
120
+ and board_profile.max_cuda_version is not None
121
+ )
122
+
123
+ def evaluate(
124
+ self, profile: SystemProfile, packages: List[PackageBinaryInfo]
125
+ ) -> List[Finding]:
126
+ findings: List[Finding] = []
127
+ board_profile = match_board_profile(profile)
128
+ if board_profile is None or board_profile.max_cuda_version is None:
129
+ return findings
130
+
131
+ for package in packages:
132
+ if not package.cuda_version:
133
+ continue
134
+ try:
135
+ pkg_major = int(package.cuda_version.split(".")[0])
136
+ board_max_major = int(
137
+ board_profile.max_cuda_version.split(".")[0]
138
+ )
139
+ if pkg_major > board_max_major:
140
+ findings.append(
141
+ Finding(
142
+ rule_id=self.rule_id,
143
+ severity=Severity.CRITICAL,
144
+ title=(
145
+ f"{package.package_name} requires CUDA "
146
+ f"{package.cuda_version}, but "
147
+ f"{board_profile.display_name} max is "
148
+ f"{board_profile.max_cuda_version}"
149
+ ),
150
+ explanation=(
151
+ f"Package {package.package_name} requires "
152
+ f"CUDA {package.cuda_version}, but "
153
+ f"{board_profile.display_name} only supports "
154
+ f"up to CUDA {board_profile.max_cuda_version}."
155
+ ),
156
+ package=package.package_name,
157
+ suggestion=(
158
+ f"Install a version of {package.package_name} "
159
+ f"compatible with CUDA "
160
+ f"{board_profile.max_cuda_version}"
161
+ ),
162
+ confidence=0.95,
163
+ )
164
+ )
165
+ except (ValueError, IndexError):
166
+ pass
167
+ return findings
168
+
169
+
170
+ class BOARD_GLIBC_MISMATCH(Rule):
171
+ """Detects GLIBC mismatch against board recommendations."""
172
+
173
+ rule_id = "BOARD_GLIBC_MISMATCH"
174
+ description = "Checks if system GLIBC differs from board recommendation"
175
+
176
+ def is_applicable(self, profile: SystemProfile) -> bool:
177
+ board_profile = match_board_profile(profile)
178
+ return (
179
+ board_profile is not None
180
+ and board_profile.recommended_glibc is not None
181
+ and profile.glibc_version is not None
182
+ )
183
+
184
+ def evaluate(
185
+ self, profile: SystemProfile, packages: List[PackageBinaryInfo]
186
+ ) -> List[Finding]:
187
+ findings: List[Finding] = []
188
+ board_profile = match_board_profile(profile)
189
+ if board_profile is None or board_profile.recommended_glibc is None:
190
+ return findings
191
+
192
+ system_glibc = profile.glibc_version
193
+ if system_glibc is None:
194
+ return findings
195
+
196
+ recommended = board_profile.recommended_glibc
197
+ try:
198
+ rec_parts = tuple(int(x) for x in recommended.split(".")[:2])
199
+ if system_glibc != rec_parts:
200
+ findings.append(
201
+ Finding(
202
+ rule_id=self.rule_id,
203
+ severity=Severity.WARNING,
204
+ title=(
205
+ f"GLIBC {system_glibc[0]}.{system_glibc[1]} "
206
+ f"differs from {board_profile.display_name} "
207
+ f"recommended {recommended}"
208
+ ),
209
+ explanation=(
210
+ f"Your system has GLIBC "
211
+ f"{system_glibc[0]}.{system_glibc[1]}, but "
212
+ f"{board_profile.display_name} is tested with "
213
+ f"GLIBC {recommended}."
214
+ ),
215
+ suggestion=(
216
+ f"Consider using "
217
+ f"{board_profile.recommended_os or 'recommended OS'} "
218
+ f"which provides GLIBC {recommended}"
219
+ ),
220
+ confidence=0.7,
221
+ )
222
+ )
223
+ except (ValueError, IndexError):
224
+ pass
225
+ return findings
226
+
227
+
228
+ class BOARD_PYTHON_VERSION_UNSUPPORTED(Rule):
229
+ """Detects Python version not validated for the board."""
230
+
231
+ rule_id = "BOARD_PYTHON_VERSION_UNSUPPORTED"
232
+ description = "Checks if Python version is in board's validated list"
233
+
234
+ def is_applicable(self, profile: SystemProfile) -> bool:
235
+ board_profile = match_board_profile(profile)
236
+ return (
237
+ board_profile is not None
238
+ and len(board_profile.python_versions) > 0
239
+ )
240
+
241
+ def evaluate(
242
+ self, profile: SystemProfile, packages: List[PackageBinaryInfo]
243
+ ) -> List[Finding]:
244
+ findings: List[Finding] = []
245
+ board_profile = match_board_profile(profile)
246
+ if board_profile is None or not board_profile.python_versions:
247
+ return findings
248
+
249
+ py_version = (
250
+ f"{profile.python_version[0]}.{profile.python_version[1]}"
251
+ )
252
+ if py_version not in board_profile.python_versions:
253
+ findings.append(
254
+ Finding(
255
+ rule_id=self.rule_id,
256
+ severity=Severity.WARNING,
257
+ title=(
258
+ f"Python {py_version} not validated for "
259
+ f"{board_profile.display_name}"
260
+ ),
261
+ explanation=(
262
+ f"Python {py_version} has not been validated for "
263
+ f"{board_profile.display_name}. Validated: "
264
+ f"{', '.join(board_profile.python_versions)}"
265
+ ),
266
+ suggestion=(
267
+ f"Use Python {board_profile.python_versions[0]} "
268
+ f"for best compatibility"
269
+ ),
270
+ confidence=0.6,
271
+ )
272
+ )
273
+ return findings
274
+
275
+
276
+ __all__ = [
277
+ "KNOWN_BROKEN_WHEEL",
278
+ "BOARD_INCOMPATIBLE_PACKAGE",
279
+ "BOARD_CUDA_VERSION_MISMATCH",
280
+ "BOARD_GLIBC_MISMATCH",
281
+ "BOARD_PYTHON_VERSION_UNSUPPORTED",
282
+ ]
@@ -0,0 +1,170 @@
1
+ """Container environment compatibility rules.
2
+
3
+ These rules detect GPU-related misconfiguration when running inside a
4
+ container (Docker, Podman, etc.). Containers do not automatically get
5
+ access to the host GPU unless explicitly configured.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import List, Optional, Tuple
11
+
12
+ from pybinaryguard.models.enums import Severity
13
+ from pybinaryguard.models.finding import Finding
14
+ from pybinaryguard.models.package import PackageBinaryInfo
15
+ from pybinaryguard.models.system import SystemProfile
16
+ from pybinaryguard.rules.base import Rule
17
+ from pybinaryguard.rules.builtin.cuda_rules import (
18
+ _driver_major,
19
+ _is_cuda_package,
20
+ _max_cuda_for_driver,
21
+ _fmt_ver,
22
+ )
23
+
24
+
25
+ class ContainerNoGPUMountRule(Rule):
26
+ """Warns when a GPU package runs inside a container without GPU access.
27
+
28
+ Running ``docker run`` without ``--gpus all`` (or the NVIDIA Container
29
+ Toolkit) means the GPU is invisible to the container. The package
30
+ will silently fall back to CPU or raise an error.
31
+ """
32
+
33
+ rule_id = "CONTAINER_NO_GPU_MOUNT"
34
+ description = (
35
+ "Warn when GPU packages run inside a container that lacks GPU "
36
+ "device mounts."
37
+ )
38
+
39
+ def is_applicable(self, profile: SystemProfile) -> bool:
40
+ return profile.is_container and not profile.gpu_available
41
+
42
+ def evaluate(
43
+ self,
44
+ profile: SystemProfile,
45
+ packages: List[PackageBinaryInfo],
46
+ ) -> List[Finding]:
47
+ findings: List[Finding] = []
48
+ for pkg in packages:
49
+ if not _is_cuda_package(pkg):
50
+ continue
51
+ findings.append(
52
+ Finding(
53
+ rule_id=self.rule_id,
54
+ severity=Severity.WARNING,
55
+ title=(
56
+ f"{pkg.package_name} needs a GPU but none is "
57
+ f"visible in this container"
58
+ ),
59
+ explanation=(
60
+ f"You are running inside a container "
61
+ f"({profile.container_runtime.value}) and package "
62
+ f"{pkg.package_name} {pkg.package_version} is a "
63
+ f"GPU-enabled build, but no GPU device was "
64
+ f"detected inside this container. This usually "
65
+ f"means the container was started without GPU "
66
+ f"passthrough."
67
+ ),
68
+ technical_detail=(
69
+ f"Container runtime: "
70
+ f"{profile.container_runtime.value}, "
71
+ f"GPU visible: False"
72
+ ),
73
+ suggestion=(
74
+ f"Run the container with GPU access:\n"
75
+ f" docker run --gpus all ... "
76
+ f"# Docker with NVIDIA Container Toolkit\n"
77
+ f" podman run --device nvidia.com/gpu=all ... "
78
+ f"# Podman with CDI\n\n"
79
+ f"Make sure the NVIDIA Container Toolkit is "
80
+ f"installed on the host:\n"
81
+ f" sudo apt install nvidia-container-toolkit "
82
+ f"# Debian/Ubuntu\n"
83
+ f" sudo systemctl restart docker"
84
+ ),
85
+ package=pkg.package_name,
86
+ package_version=pkg.package_version,
87
+ )
88
+ )
89
+ return findings
90
+
91
+
92
+ class ContainerDriverMismatchRule(Rule):
93
+ """Warns when the container's CUDA may exceed the host driver capability.
94
+
95
+ Inside a container the CUDA toolkit version can be newer than what
96
+ the host GPU driver supports. The container shares the host's
97
+ kernel-mode driver, so if the driver is too old, CUDA calls will
98
+ fail.
99
+ """
100
+
101
+ rule_id = "CONTAINER_DRIVER_MISMATCH"
102
+ description = (
103
+ "Check that the container's CUDA version does not exceed the "
104
+ "host driver's capability."
105
+ )
106
+
107
+ def is_applicable(self, profile: SystemProfile) -> bool:
108
+ return (
109
+ profile.is_container
110
+ and profile.gpu_available
111
+ and profile.gpu_driver_version is not None
112
+ and profile.cuda_runtime_version is not None
113
+ )
114
+
115
+ def evaluate(
116
+ self,
117
+ profile: SystemProfile,
118
+ packages: List[PackageBinaryInfo],
119
+ ) -> List[Finding]:
120
+ findings: List[Finding] = []
121
+ if (
122
+ profile.gpu_driver_version is None
123
+ or profile.cuda_runtime_version is None
124
+ ):
125
+ return findings
126
+
127
+ drv_major = _driver_major(profile.gpu_driver_version)
128
+ if drv_major is None:
129
+ return findings
130
+
131
+ max_cuda = _max_cuda_for_driver(drv_major)
132
+ if max_cuda is None:
133
+ return findings
134
+
135
+ cuda_rt = profile.cuda_runtime_version
136
+ if cuda_rt <= max_cuda:
137
+ return findings
138
+
139
+ findings.append(
140
+ Finding(
141
+ rule_id=self.rule_id,
142
+ severity=Severity.WARNING,
143
+ title=(
144
+ "Container CUDA version exceeds host driver capability"
145
+ ),
146
+ explanation=(
147
+ f"This container has CUDA {_fmt_ver(cuda_rt)} "
148
+ f"installed, but the host's NVIDIA driver "
149
+ f"({profile.gpu_driver_version}) only supports CUDA "
150
+ f"up to {_fmt_ver(max_cuda)}. Because the container "
151
+ f"shares the host's kernel-mode GPU driver, CUDA "
152
+ f"calls will fail even though the CUDA toolkit is "
153
+ f"installed inside the container."
154
+ ),
155
+ technical_detail=(
156
+ f"Container CUDA: {_fmt_ver(cuda_rt)}, "
157
+ f"Host driver: {profile.gpu_driver_version}, "
158
+ f"Max CUDA for driver: {_fmt_ver(max_cuda)}"
159
+ ),
160
+ suggestion=(
161
+ f"Option 1 -- upgrade the GPU driver on the host to "
162
+ f"one that supports CUDA {_fmt_ver(cuda_rt)}.\n\n"
163
+ f"Option 2 -- use a container image with CUDA <= "
164
+ f"{_fmt_ver(max_cuda)}:\n"
165
+ f" docker run --gpus all nvidia/cuda:"
166
+ f"{_fmt_ver(max_cuda)}-runtime-ubuntu22.04"
167
+ ),
168
+ )
169
+ )
170
+ return findings
@@ -0,0 +1,204 @@
1
+ """CPU instruction-set rules.
2
+
3
+ These rules detect packages that require advanced CPU instruction sets
4
+ (AVX, AVX2, AVX-512, etc.) which may not be available on the host. Running
5
+ such a package on an unsupported CPU triggers a ``SIGILL``
6
+ (illegal instruction) signal, causing the process to crash instantly without
7
+ a meaningful Python traceback.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from typing import Dict, List, Set
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
+ # Mapping of well-known packages to the instruction sets they are known
21
+ # to require. An empty set means the package *may* use advanced
22
+ # instructions but does not always require them (build-dependent).
23
+ KNOWN_INSTRUCTION_REQUIREMENTS: Dict[str, Set[str]] = {
24
+ "tensorflow": {"avx"},
25
+ "tensorflow-cpu": {"avx"},
26
+ "tensorflow-gpu": {"avx"},
27
+ "numpy": set(), # modern numpy may need AVX2 on some builds
28
+ "faiss-cpu": {"avx2"},
29
+ "faiss-gpu": {"avx2"},
30
+ "pytorch": set(),
31
+ "torch": set(),
32
+ "scipy": set(),
33
+ "scikit-learn": set(),
34
+ "xgboost": set(),
35
+ "lightgbm": set(),
36
+ "onnxruntime": {"avx"},
37
+ "onnxruntime-gpu": {"avx"},
38
+ }
39
+
40
+ _INSTRUCTION_FRIENDLY_NAMES: Dict[str, str] = {
41
+ "avx": "AVX (Advanced Vector Extensions)",
42
+ "avx2": "AVX2 (Advanced Vector Extensions 2)",
43
+ "avx512": "AVX-512",
44
+ "sse42": "SSE 4.2",
45
+ "neon": "ARM NEON",
46
+ }
47
+
48
+
49
+ def _system_has_instruction(profile: SystemProfile, instruction: str) -> bool:
50
+ """Check whether the system supports the given instruction set."""
51
+ mapping: Dict[str, bool] = {
52
+ "avx": profile.has_avx,
53
+ "avx2": profile.has_avx2,
54
+ "avx512": profile.has_avx512,
55
+ "sse42": profile.has_sse42,
56
+ "neon": profile.has_neon,
57
+ }
58
+ return mapping.get(instruction, False)
59
+
60
+
61
+ class AVX2RequiredRule(Rule):
62
+ """Detects packages that require AVX2 on a CPU without it.
63
+
64
+ AVX2 is commonly used in high-performance numerical libraries to
65
+ speed up vector and matrix operations. Older or low-power CPUs
66
+ (virtual machines on older hosts, Atom processors, etc.) may lack
67
+ AVX2 support.
68
+ """
69
+
70
+ rule_id = "AVX2_REQUIRED"
71
+ description = (
72
+ "Flag packages that require AVX2 when the host CPU lacks it."
73
+ )
74
+
75
+ def is_applicable(self, profile: SystemProfile) -> bool:
76
+ """Skip if the system already has AVX2."""
77
+ return not profile.has_avx2
78
+
79
+ def evaluate(
80
+ self,
81
+ profile: SystemProfile,
82
+ packages: List[PackageBinaryInfo],
83
+ ) -> List[Finding]:
84
+ findings: List[Finding] = []
85
+ for pkg in packages:
86
+ pkg_lower = pkg.package_name.lower()
87
+ requirements = KNOWN_INSTRUCTION_REQUIREMENTS.get(pkg_lower)
88
+ if requirements is None:
89
+ continue
90
+ if "avx2" not in requirements:
91
+ continue
92
+ findings.append(
93
+ Finding(
94
+ rule_id=self.rule_id,
95
+ severity=Severity.CRITICAL,
96
+ title=(
97
+ f"{pkg.package_name} requires AVX2 instructions"
98
+ ),
99
+ explanation=(
100
+ f"Package {pkg.package_name} "
101
+ f"{pkg.package_version} is known to require "
102
+ f"AVX2 (Advanced Vector Extensions 2) CPU "
103
+ f"instructions, but your CPU ({profile.cpu_model or 'unknown'}) "
104
+ f"does not support AVX2. When the package tries "
105
+ f"to execute an AVX2 instruction your process "
106
+ f"will be killed by an 'Illegal instruction' "
107
+ f"signal (SIGILL) with no Python traceback."
108
+ ),
109
+ technical_detail=(
110
+ f"CPU model: {profile.cpu_model or 'unknown'}, "
111
+ f"has_avx2: False"
112
+ ),
113
+ suggestion=(
114
+ f"Option 1 -- look for a build that does not "
115
+ f"require AVX2 (some packages publish 'noavx' "
116
+ f"variants).\n\n"
117
+ f"Option 2 -- use conda-forge, which often provides "
118
+ f"builds for older CPUs:\n"
119
+ f" conda install -c conda-forge "
120
+ f"{pkg.package_name}\n\n"
121
+ f"Option 3 -- build from source with AVX2 "
122
+ f"disabled (if the package supports it)."
123
+ ),
124
+ package=pkg.package_name,
125
+ package_version=pkg.package_version,
126
+ confidence=0.9,
127
+ )
128
+ )
129
+ return findings
130
+
131
+
132
+ class IllegalInstructionRiskRule(Rule):
133
+ """General check for packages that may trigger SIGILL.
134
+
135
+ Consults the ``KNOWN_INSTRUCTION_REQUIREMENTS`` table to determine
136
+ whether the host CPU provides all instruction sets a package needs.
137
+ """
138
+
139
+ rule_id = "ILLEGAL_INSTRUCTION_RISK"
140
+ description = (
141
+ "Detect packages whose instruction-set requirements may not "
142
+ "be met by the host CPU."
143
+ )
144
+
145
+ def evaluate(
146
+ self,
147
+ profile: SystemProfile,
148
+ packages: List[PackageBinaryInfo],
149
+ ) -> List[Finding]:
150
+ findings: List[Finding] = []
151
+ for pkg in packages:
152
+ pkg_lower = pkg.package_name.lower()
153
+ requirements = KNOWN_INSTRUCTION_REQUIREMENTS.get(pkg_lower)
154
+ if requirements is None:
155
+ continue
156
+ missing: List[str] = []
157
+ for instr in sorted(requirements):
158
+ if not _system_has_instruction(profile, instr):
159
+ missing.append(instr)
160
+ if not missing:
161
+ continue
162
+ friendly = ", ".join(
163
+ _INSTRUCTION_FRIENDLY_NAMES.get(m, m.upper()) for m in missing
164
+ )
165
+ findings.append(
166
+ Finding(
167
+ rule_id=self.rule_id,
168
+ severity=Severity.CRITICAL,
169
+ title=(
170
+ f"{pkg.package_name} may crash with "
171
+ f"'Illegal instruction'"
172
+ ),
173
+ explanation=(
174
+ f"Package {pkg.package_name} "
175
+ f"{pkg.package_version} is known to require "
176
+ f"the following CPU instruction set(s): "
177
+ f"{friendly}. Your CPU "
178
+ f"({profile.cpu_model or 'unknown'}) does not "
179
+ f"support {'them' if len(missing) > 1 else 'it'}. "
180
+ f"This will cause the process to be killed with "
181
+ f"a SIGILL (Illegal instruction) signal the "
182
+ f"moment the package tries to use the missing "
183
+ f"instruction."
184
+ ),
185
+ technical_detail=(
186
+ f"Missing instructions: {', '.join(missing)}, "
187
+ f"CPU model: {profile.cpu_model or 'unknown'}, "
188
+ f"CPU flags: {', '.join(sorted(profile.cpu_flags)) if profile.cpu_flags else 'unknown'}"
189
+ ),
190
+ suggestion=(
191
+ f"Option 1 -- install an alternative build that "
192
+ f"does not require {friendly}.\n\n"
193
+ f"Option 2 -- use conda-forge:\n"
194
+ f" conda install -c conda-forge "
195
+ f"{pkg.package_name}\n\n"
196
+ f"Option 3 -- run inside a VM or container on "
197
+ f"hardware that supports these instructions."
198
+ ),
199
+ package=pkg.package_name,
200
+ package_version=pkg.package_version,
201
+ confidence=0.85,
202
+ )
203
+ )
204
+ return findings