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,426 @@
|
|
|
1
|
+
"""TensorRT plugin for PyBinaryGuard.
|
|
2
|
+
|
|
3
|
+
Provides rules that check TensorRT version compatibility with installed
|
|
4
|
+
deep-learning frameworks and detect serialised engine files built for a
|
|
5
|
+
different GPU architecture.
|
|
6
|
+
|
|
7
|
+
Provides
|
|
8
|
+
--------
|
|
9
|
+
- **TensorRTVersionRule** -- Checks that the installed TensorRT version
|
|
10
|
+
is compatible with the installed versions of TensorFlow, PyTorch, and
|
|
11
|
+
ONNX Runtime.
|
|
12
|
+
- **TensorRTEngineMismatchRule** -- Scans for ``.engine`` / ``.trt``
|
|
13
|
+
files that were serialised for a different GPU compute capability than
|
|
14
|
+
the current device.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import logging
|
|
20
|
+
import os
|
|
21
|
+
import struct
|
|
22
|
+
from typing import Any, Dict, List, Optional, Tuple, TYPE_CHECKING
|
|
23
|
+
|
|
24
|
+
from pybinaryguard.models.enums import Severity
|
|
25
|
+
from pybinaryguard.models.finding import Finding
|
|
26
|
+
from pybinaryguard.models.package import PackageBinaryInfo
|
|
27
|
+
from pybinaryguard.models.system import SystemProfile
|
|
28
|
+
from pybinaryguard.rules.base import Rule
|
|
29
|
+
|
|
30
|
+
if TYPE_CHECKING:
|
|
31
|
+
from pybinaryguard.plugins.hooks import HookRegistry
|
|
32
|
+
|
|
33
|
+
logger = logging.getLogger(__name__)
|
|
34
|
+
|
|
35
|
+
# -- Version compatibility tables -------------------------------------------
|
|
36
|
+
|
|
37
|
+
# Minimum TensorRT version required by framework major.minor releases.
|
|
38
|
+
# Format: {("framework", (major, minor)): (trt_min_major, trt_min_minor)}
|
|
39
|
+
_FRAMEWORK_TRT_COMPAT: Dict[Tuple[str, Tuple[int, int]], Tuple[int, int]] = {
|
|
40
|
+
# PyTorch (torch.tensorrt / torch2trt)
|
|
41
|
+
("pytorch", (2, 0)): (8, 5),
|
|
42
|
+
("pytorch", (2, 1)): (8, 6),
|
|
43
|
+
("pytorch", (2, 2)): (8, 6),
|
|
44
|
+
("pytorch", (2, 3)): (10, 0),
|
|
45
|
+
("pytorch", (2, 4)): (10, 0),
|
|
46
|
+
("pytorch", (2, 5)): (10, 3),
|
|
47
|
+
# TensorFlow
|
|
48
|
+
("tensorflow", (2, 12)): (8, 5),
|
|
49
|
+
("tensorflow", (2, 13)): (8, 6),
|
|
50
|
+
("tensorflow", (2, 14)): (8, 6),
|
|
51
|
+
("tensorflow", (2, 15)): (8, 6),
|
|
52
|
+
("tensorflow", (2, 16)): (8, 6),
|
|
53
|
+
# ONNX Runtime
|
|
54
|
+
("onnxruntime", (1, 15)): (8, 6),
|
|
55
|
+
("onnxruntime", (1, 16)): (8, 6),
|
|
56
|
+
("onnxruntime", (1, 17)): (8, 6),
|
|
57
|
+
("onnxruntime", (1, 18)): (10, 0),
|
|
58
|
+
("onnxruntime", (1, 19)): (10, 0),
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
# Package names that correspond to each framework key above.
|
|
62
|
+
_FRAMEWORK_PACKAGE_NAMES: Dict[str, List[str]] = {
|
|
63
|
+
"pytorch": ["torch"],
|
|
64
|
+
"tensorflow": ["tensorflow", "tensorflow-gpu", "tf-nightly"],
|
|
65
|
+
"onnxruntime": ["onnxruntime", "onnxruntime-gpu"],
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# -- Helpers ----------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _get_tensorrt_version() -> Optional[Tuple[int, int, int]]:
|
|
73
|
+
"""Attempt to determine the installed TensorRT version.
|
|
74
|
+
|
|
75
|
+
Tries the ``tensorrt`` Python package first, then falls back to
|
|
76
|
+
parsing ``libnvinfer.so`` via ``ldconfig -p``.
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
A ``(major, minor, patch)`` tuple, or ``None`` if TensorRT is
|
|
80
|
+
not detected.
|
|
81
|
+
"""
|
|
82
|
+
# Method 1: Python package
|
|
83
|
+
try:
|
|
84
|
+
import tensorrt # type: ignore[import-untyped]
|
|
85
|
+
ver = getattr(tensorrt, "__version__", None)
|
|
86
|
+
if ver:
|
|
87
|
+
parts = ver.split(".")
|
|
88
|
+
if len(parts) >= 3:
|
|
89
|
+
return (int(parts[0]), int(parts[1]), int(parts[2]))
|
|
90
|
+
if len(parts) == 2:
|
|
91
|
+
return (int(parts[0]), int(parts[1]), 0)
|
|
92
|
+
except ImportError:
|
|
93
|
+
pass
|
|
94
|
+
except Exception:
|
|
95
|
+
logger.debug("Failed to read tensorrt.__version__", exc_info=True)
|
|
96
|
+
|
|
97
|
+
# Method 2: Parse libnvinfer soname from ldconfig
|
|
98
|
+
try:
|
|
99
|
+
import subprocess
|
|
100
|
+
result = subprocess.run(
|
|
101
|
+
["ldconfig", "-p"],
|
|
102
|
+
capture_output=True,
|
|
103
|
+
text=True,
|
|
104
|
+
timeout=5,
|
|
105
|
+
)
|
|
106
|
+
if result.returncode == 0:
|
|
107
|
+
import re
|
|
108
|
+
match = re.search(r"libnvinfer\.so\.(\d+)\.(\d+)\.(\d+)", result.stdout)
|
|
109
|
+
if match:
|
|
110
|
+
return (
|
|
111
|
+
int(match.group(1)),
|
|
112
|
+
int(match.group(2)),
|
|
113
|
+
int(match.group(3)),
|
|
114
|
+
)
|
|
115
|
+
# Fallback: just major version from soname
|
|
116
|
+
match = re.search(r"libnvinfer\.so\.(\d+)", result.stdout)
|
|
117
|
+
if match:
|
|
118
|
+
return (int(match.group(1)), 0, 0)
|
|
119
|
+
except (FileNotFoundError, subprocess.TimeoutExpired, OSError):
|
|
120
|
+
pass
|
|
121
|
+
|
|
122
|
+
return None
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _parse_package_version(version_str: str) -> Optional[Tuple[int, int]]:
|
|
126
|
+
"""Parse a package version string into (major, minor).
|
|
127
|
+
|
|
128
|
+
Handles common formats like ``2.1.0``, ``2.1.0+cu118``,
|
|
129
|
+
``2.1.0.post1``, etc.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
version_str: The raw version string.
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
A ``(major, minor)`` tuple, or ``None`` on parse failure.
|
|
136
|
+
"""
|
|
137
|
+
import re
|
|
138
|
+
match = re.match(r"(\d+)\.(\d+)", version_str)
|
|
139
|
+
if match:
|
|
140
|
+
return (int(match.group(1)), int(match.group(2)))
|
|
141
|
+
return None
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
# -- Rules ------------------------------------------------------------------
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class TensorRTVersionRule(Rule):
|
|
148
|
+
"""Check TensorRT version compatibility with installed frameworks.
|
|
149
|
+
|
|
150
|
+
Verifies that the installed TensorRT version meets the minimum
|
|
151
|
+
requirements of installed deep-learning frameworks (PyTorch,
|
|
152
|
+
TensorFlow, ONNX Runtime).
|
|
153
|
+
"""
|
|
154
|
+
|
|
155
|
+
rule_id = "TENSORRT_VERSION_COMPAT"
|
|
156
|
+
description = (
|
|
157
|
+
"Verify that the installed TensorRT version is compatible with "
|
|
158
|
+
"installed deep-learning frameworks."
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
def is_applicable(self, profile: SystemProfile) -> bool:
|
|
162
|
+
"""Only applies when a GPU is available."""
|
|
163
|
+
return profile.gpu_available
|
|
164
|
+
|
|
165
|
+
def evaluate(
|
|
166
|
+
self,
|
|
167
|
+
profile: SystemProfile,
|
|
168
|
+
packages: List[PackageBinaryInfo],
|
|
169
|
+
) -> List[Finding]:
|
|
170
|
+
"""Check TensorRT version against framework requirements."""
|
|
171
|
+
findings: List[Finding] = []
|
|
172
|
+
|
|
173
|
+
trt_version = _get_tensorrt_version()
|
|
174
|
+
if trt_version is None:
|
|
175
|
+
# TensorRT not installed -- nothing to check.
|
|
176
|
+
return findings
|
|
177
|
+
|
|
178
|
+
trt_major, trt_minor = trt_version[0], trt_version[1]
|
|
179
|
+
|
|
180
|
+
# Build a lookup of installed packages for quick access.
|
|
181
|
+
installed: Dict[str, str] = {}
|
|
182
|
+
for pkg in packages:
|
|
183
|
+
installed[pkg.package_name.lower()] = pkg.package_version
|
|
184
|
+
|
|
185
|
+
for framework_key, package_names in _FRAMEWORK_PACKAGE_NAMES.items():
|
|
186
|
+
for pkg_name in package_names:
|
|
187
|
+
version_str = installed.get(pkg_name.lower())
|
|
188
|
+
if version_str is None:
|
|
189
|
+
continue
|
|
190
|
+
|
|
191
|
+
fw_version = _parse_package_version(version_str)
|
|
192
|
+
if fw_version is None:
|
|
193
|
+
continue
|
|
194
|
+
|
|
195
|
+
required = _FRAMEWORK_TRT_COMPAT.get((framework_key, fw_version))
|
|
196
|
+
if required is None:
|
|
197
|
+
# No compatibility data for this version -- skip.
|
|
198
|
+
continue
|
|
199
|
+
|
|
200
|
+
req_major, req_minor = required
|
|
201
|
+
if trt_major < req_major or (trt_major == req_major and trt_minor < req_minor):
|
|
202
|
+
findings.append(Finding(
|
|
203
|
+
rule_id=self.rule_id,
|
|
204
|
+
severity=Severity.WARNING,
|
|
205
|
+
title=(
|
|
206
|
+
f"TensorRT {trt_major}.{trt_minor} may be "
|
|
207
|
+
f"incompatible with {pkg_name} {version_str}"
|
|
208
|
+
),
|
|
209
|
+
explanation=(
|
|
210
|
+
f"{pkg_name}=={version_str} requires TensorRT >= "
|
|
211
|
+
f"{req_major}.{req_minor}, but TensorRT "
|
|
212
|
+
f"{trt_major}.{trt_minor}.{trt_version[2]} is installed."
|
|
213
|
+
),
|
|
214
|
+
technical_detail=(
|
|
215
|
+
f"Framework: {pkg_name}=={version_str}, "
|
|
216
|
+
f"TensorRT: {trt_major}.{trt_minor}.{trt_version[2]}, "
|
|
217
|
+
f"Required: >= {req_major}.{req_minor}"
|
|
218
|
+
),
|
|
219
|
+
suggestion=(
|
|
220
|
+
f"Upgrade TensorRT to version {req_major}.{req_minor} "
|
|
221
|
+
f"or later, or downgrade {pkg_name} to a version "
|
|
222
|
+
f"compatible with TensorRT {trt_major}.{trt_minor}."
|
|
223
|
+
),
|
|
224
|
+
package=pkg_name,
|
|
225
|
+
package_version=version_str,
|
|
226
|
+
))
|
|
227
|
+
else:
|
|
228
|
+
findings.append(Finding(
|
|
229
|
+
rule_id=self.rule_id,
|
|
230
|
+
severity=Severity.PASSED,
|
|
231
|
+
title=(
|
|
232
|
+
f"TensorRT {trt_major}.{trt_minor} is compatible "
|
|
233
|
+
f"with {pkg_name} {version_str}"
|
|
234
|
+
),
|
|
235
|
+
explanation=(
|
|
236
|
+
f"TensorRT {trt_major}.{trt_minor}.{trt_version[2]} "
|
|
237
|
+
f"meets the minimum requirement "
|
|
238
|
+
f"(>= {req_major}.{req_minor}) for "
|
|
239
|
+
f"{pkg_name}=={version_str}."
|
|
240
|
+
),
|
|
241
|
+
package=pkg_name,
|
|
242
|
+
package_version=version_str,
|
|
243
|
+
))
|
|
244
|
+
# Only check the first matching package name per framework.
|
|
245
|
+
break
|
|
246
|
+
|
|
247
|
+
return findings
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
class TensorRTEngineMismatchRule(Rule):
|
|
251
|
+
"""Detect serialised TensorRT engine files built for a different GPU.
|
|
252
|
+
|
|
253
|
+
TensorRT engine files (``.engine``, ``.trt``, ``.plan``) are
|
|
254
|
+
serialised for a specific GPU architecture and are not portable.
|
|
255
|
+
Loading an engine built for a different compute capability will fail
|
|
256
|
+
at runtime.
|
|
257
|
+
|
|
258
|
+
This rule scans site-packages directories for engine files and
|
|
259
|
+
performs a best-effort check of their target compute capability by
|
|
260
|
+
reading the header bytes.
|
|
261
|
+
"""
|
|
262
|
+
|
|
263
|
+
rule_id = "TENSORRT_ENGINE_MISMATCH"
|
|
264
|
+
description = (
|
|
265
|
+
"Detect TensorRT engine files serialised for a different GPU "
|
|
266
|
+
"compute capability."
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
# TensorRT serialised engines have a small header. The exact layout
|
|
270
|
+
# varies by TensorRT version, but a common pattern is that the magic
|
|
271
|
+
# bytes start with "ptrt" (0x70747274) followed by version info.
|
|
272
|
+
# This is a best-effort heuristic.
|
|
273
|
+
_ENGINE_EXTENSIONS = (".engine", ".trt", ".plan")
|
|
274
|
+
_MAX_SCAN_BYTES = 256
|
|
275
|
+
|
|
276
|
+
def is_applicable(self, profile: SystemProfile) -> bool:
|
|
277
|
+
"""Only applies when GPU compute capability is known."""
|
|
278
|
+
return (
|
|
279
|
+
profile.gpu_available
|
|
280
|
+
and profile.gpu_compute_capability is not None
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
def evaluate(
|
|
284
|
+
self,
|
|
285
|
+
profile: SystemProfile,
|
|
286
|
+
packages: List[PackageBinaryInfo],
|
|
287
|
+
) -> List[Finding]:
|
|
288
|
+
"""Scan for engine files in package install paths."""
|
|
289
|
+
findings: List[Finding] = []
|
|
290
|
+
|
|
291
|
+
if profile.gpu_compute_capability is None:
|
|
292
|
+
return findings
|
|
293
|
+
|
|
294
|
+
current_cc = profile.gpu_compute_capability
|
|
295
|
+
|
|
296
|
+
for pkg in packages:
|
|
297
|
+
if not pkg.install_path or not os.path.isdir(pkg.install_path):
|
|
298
|
+
continue
|
|
299
|
+
|
|
300
|
+
engine_files = self._find_engine_files(pkg.install_path)
|
|
301
|
+
for engine_path in engine_files:
|
|
302
|
+
engine_cc = self._read_engine_compute_capability(engine_path)
|
|
303
|
+
if engine_cc is not None and engine_cc != current_cc:
|
|
304
|
+
findings.append(Finding(
|
|
305
|
+
rule_id=self.rule_id,
|
|
306
|
+
severity=Severity.WARNING,
|
|
307
|
+
title=(
|
|
308
|
+
f"TensorRT engine built for different GPU: "
|
|
309
|
+
f"{os.path.basename(engine_path)}"
|
|
310
|
+
),
|
|
311
|
+
explanation=(
|
|
312
|
+
f"Engine file {engine_path} appears to be "
|
|
313
|
+
f"serialised for compute capability "
|
|
314
|
+
f"{engine_cc[0]}.{engine_cc[1]}, but the "
|
|
315
|
+
f"current GPU has compute capability "
|
|
316
|
+
f"{current_cc[0]}.{current_cc[1]}. "
|
|
317
|
+
f"Loading this engine will fail at runtime."
|
|
318
|
+
),
|
|
319
|
+
technical_detail=(
|
|
320
|
+
f"Engine CC: {engine_cc[0]}.{engine_cc[1]}, "
|
|
321
|
+
f"GPU CC: {current_cc[0]}.{current_cc[1]}, "
|
|
322
|
+
f"File: {engine_path}"
|
|
323
|
+
),
|
|
324
|
+
suggestion=(
|
|
325
|
+
"Re-serialise the TensorRT engine on this GPU, "
|
|
326
|
+
"or use the ONNX model to rebuild the engine: "
|
|
327
|
+
"trtexec --onnx=model.onnx --saveEngine=model.engine"
|
|
328
|
+
),
|
|
329
|
+
package=pkg.package_name,
|
|
330
|
+
package_version=pkg.package_version,
|
|
331
|
+
))
|
|
332
|
+
|
|
333
|
+
return findings
|
|
334
|
+
|
|
335
|
+
def _find_engine_files(self, directory: str) -> List[str]:
|
|
336
|
+
"""Recursively find TensorRT engine files in a directory.
|
|
337
|
+
|
|
338
|
+
Limits the search to 3 directory levels deep to avoid traversing
|
|
339
|
+
overly large trees.
|
|
340
|
+
"""
|
|
341
|
+
engine_files: List[str] = []
|
|
342
|
+
try:
|
|
343
|
+
for root, dirs, files in os.walk(directory):
|
|
344
|
+
# Limit depth to avoid excessive traversal.
|
|
345
|
+
depth = root[len(directory):].count(os.sep)
|
|
346
|
+
if depth >= 3:
|
|
347
|
+
dirs.clear()
|
|
348
|
+
continue
|
|
349
|
+
for fname in files:
|
|
350
|
+
if any(fname.endswith(ext) for ext in self._ENGINE_EXTENSIONS):
|
|
351
|
+
engine_files.append(os.path.join(root, fname))
|
|
352
|
+
except (PermissionError, OSError):
|
|
353
|
+
pass
|
|
354
|
+
return engine_files
|
|
355
|
+
|
|
356
|
+
@staticmethod
|
|
357
|
+
def _read_engine_compute_capability(
|
|
358
|
+
path: str,
|
|
359
|
+
) -> Optional[Tuple[int, int]]:
|
|
360
|
+
"""Attempt to read the compute capability from a TensorRT engine file.
|
|
361
|
+
|
|
362
|
+
This is a best-effort heuristic. TensorRT engine files do not
|
|
363
|
+
have a publicly documented header format, but engine files
|
|
364
|
+
serialised by common TensorRT versions embed the compute
|
|
365
|
+
capability as two bytes in the header region.
|
|
366
|
+
|
|
367
|
+
We look for the ``ptrt`` magic sequence and attempt to extract
|
|
368
|
+
CC from known offsets. Returns ``None`` when the format is
|
|
369
|
+
unrecognised.
|
|
370
|
+
"""
|
|
371
|
+
try:
|
|
372
|
+
with open(path, "rb") as fh:
|
|
373
|
+
header = fh.read(256)
|
|
374
|
+
except (FileNotFoundError, PermissionError, OSError):
|
|
375
|
+
return None
|
|
376
|
+
|
|
377
|
+
if len(header) < 32:
|
|
378
|
+
return None
|
|
379
|
+
|
|
380
|
+
# Look for the "ptrt" magic (0x70747274)
|
|
381
|
+
magic_offset = header.find(b"ptrt")
|
|
382
|
+
if magic_offset < 0:
|
|
383
|
+
# Try little-endian uint32 magic
|
|
384
|
+
magic_offset = header.find(b"trtp")
|
|
385
|
+
if magic_offset < 0:
|
|
386
|
+
return None
|
|
387
|
+
|
|
388
|
+
# In several TensorRT versions, the compute capability is stored
|
|
389
|
+
# as two uint8 values at offset magic+20 and magic+21 (major, minor).
|
|
390
|
+
cc_offset = magic_offset + 20
|
|
391
|
+
if cc_offset + 2 > len(header):
|
|
392
|
+
return None
|
|
393
|
+
|
|
394
|
+
cc_major = header[cc_offset]
|
|
395
|
+
cc_minor = header[cc_offset + 1]
|
|
396
|
+
|
|
397
|
+
# Sanity check: compute capability major should be 2-12
|
|
398
|
+
if 2 <= cc_major <= 12 and 0 <= cc_minor <= 9:
|
|
399
|
+
return (cc_major, cc_minor)
|
|
400
|
+
|
|
401
|
+
return None
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
# -- Plugin entry point -----------------------------------------------------
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def register(registry: HookRegistry) -> None:
|
|
408
|
+
"""Register TensorRT rules if TensorRT is available.
|
|
409
|
+
|
|
410
|
+
This function is called by the plugin loader. It activates when
|
|
411
|
+
either the ``tensorrt`` Python package is importable or
|
|
412
|
+
``libnvinfer`` is found in the system library cache.
|
|
413
|
+
"""
|
|
414
|
+
trt_version = _get_tensorrt_version()
|
|
415
|
+
if trt_version is None:
|
|
416
|
+
logger.debug("TensorRT plugin: TensorRT not detected; not activating")
|
|
417
|
+
return
|
|
418
|
+
|
|
419
|
+
registry.add_rule(TensorRTVersionRule())
|
|
420
|
+
registry.add_rule(TensorRTEngineMismatchRule())
|
|
421
|
+
logger.info(
|
|
422
|
+
"TensorRT plugin activated (version %d.%d.%d)",
|
|
423
|
+
trt_version[0],
|
|
424
|
+
trt_version[1],
|
|
425
|
+
trt_version[2],
|
|
426
|
+
)
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
"""Hook registry for the plugin subsystem.
|
|
2
|
+
|
|
3
|
+
Plugins register their extensions (probes, rules, detectors, reporters,
|
|
4
|
+
and lifecycle hooks) with a :class:`HookRegistry` instance that is passed
|
|
5
|
+
to their ``register()`` function during discovery.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
from typing import Any, Callable, Dict, List, Optional, TYPE_CHECKING
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from pybinaryguard.probes.base import ProbeBase
|
|
15
|
+
from pybinaryguard.rules.base import Rule
|
|
16
|
+
from pybinaryguard.models.system import SystemProfile
|
|
17
|
+
from pybinaryguard.models.package import PackageBinaryInfo
|
|
18
|
+
from pybinaryguard.models.finding import Finding
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
# Type aliases for hook signatures.
|
|
23
|
+
BoardDetector = Callable[[Dict[str, Any]], Optional[str]]
|
|
24
|
+
FrameworkChecker = Callable[["SystemProfile", List["PackageBinaryInfo"]], List["Finding"]]
|
|
25
|
+
Reporter = Callable[[List["Finding"], "SystemProfile"], str]
|
|
26
|
+
ScanHook = Callable[["SystemProfile"], None]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class HookRegistry:
|
|
30
|
+
"""Central registry that collects all plugin-provided extensions.
|
|
31
|
+
|
|
32
|
+
An instance is created by the plugin loader and handed to each plugin's
|
|
33
|
+
``register()`` entry point. Plugins call the ``add_*`` methods to
|
|
34
|
+
register their contributions, and the host application reads them back
|
|
35
|
+
via the corresponding read-only properties.
|
|
36
|
+
|
|
37
|
+
Example (inside a plugin's ``register`` function)::
|
|
38
|
+
|
|
39
|
+
def register(registry: HookRegistry) -> None:
|
|
40
|
+
registry.add_probe(MyProbe())
|
|
41
|
+
registry.add_rule(MyRule())
|
|
42
|
+
registry.pre_scan(lambda profile: print("scan starting"))
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
def __init__(self) -> None:
|
|
46
|
+
self._probes: List[ProbeBase] = []
|
|
47
|
+
self._rules: List[Rule] = []
|
|
48
|
+
self._board_detectors: List[BoardDetector] = []
|
|
49
|
+
self._framework_checkers: List[FrameworkChecker] = []
|
|
50
|
+
self._reporters: List[Reporter] = []
|
|
51
|
+
self._pre_scan_hooks: List[ScanHook] = []
|
|
52
|
+
self._post_scan_hooks: List[ScanHook] = []
|
|
53
|
+
|
|
54
|
+
# ------------------------------------------------------------------
|
|
55
|
+
# Registration methods
|
|
56
|
+
# ------------------------------------------------------------------
|
|
57
|
+
|
|
58
|
+
def add_probe(self, probe: ProbeBase) -> None:
|
|
59
|
+
"""Register an additional system probe.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
probe: A :class:`~pybinaryguard.probes.base.ProbeBase` subclass
|
|
63
|
+
instance. Must have a unique ``name`` attribute.
|
|
64
|
+
|
|
65
|
+
Raises:
|
|
66
|
+
TypeError: If *probe* is not a :class:`ProbeBase` instance.
|
|
67
|
+
ValueError: If a probe with the same ``name`` is already registered.
|
|
68
|
+
"""
|
|
69
|
+
from pybinaryguard.probes.base import ProbeBase as _ProbeBase
|
|
70
|
+
|
|
71
|
+
if not isinstance(probe, _ProbeBase):
|
|
72
|
+
raise TypeError(
|
|
73
|
+
f"Expected a ProbeBase instance, got {type(probe).__name__}"
|
|
74
|
+
)
|
|
75
|
+
existing_names = {p.name for p in self._probes}
|
|
76
|
+
if probe.name in existing_names:
|
|
77
|
+
raise ValueError(
|
|
78
|
+
f"A probe named {probe.name!r} is already registered"
|
|
79
|
+
)
|
|
80
|
+
self._probes.append(probe)
|
|
81
|
+
logger.debug("Registered plugin probe: %s", probe.name)
|
|
82
|
+
|
|
83
|
+
def add_rule(self, rule: Rule) -> None:
|
|
84
|
+
"""Register an additional compatibility rule.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
rule: A :class:`~pybinaryguard.rules.base.Rule` subclass instance.
|
|
88
|
+
Must have a unique ``rule_id`` attribute.
|
|
89
|
+
|
|
90
|
+
Raises:
|
|
91
|
+
TypeError: If *rule* is not a :class:`Rule` instance.
|
|
92
|
+
ValueError: If a rule with the same ``rule_id`` is already registered.
|
|
93
|
+
"""
|
|
94
|
+
from pybinaryguard.rules.base import Rule as _Rule
|
|
95
|
+
|
|
96
|
+
if not isinstance(rule, _Rule):
|
|
97
|
+
raise TypeError(
|
|
98
|
+
f"Expected a Rule instance, got {type(rule).__name__}"
|
|
99
|
+
)
|
|
100
|
+
existing_ids = {r.rule_id for r in self._rules}
|
|
101
|
+
if rule.rule_id in existing_ids:
|
|
102
|
+
raise ValueError(
|
|
103
|
+
f"A rule with id {rule.rule_id!r} is already registered"
|
|
104
|
+
)
|
|
105
|
+
self._rules.append(rule)
|
|
106
|
+
logger.debug("Registered plugin rule: %s", rule.rule_id)
|
|
107
|
+
|
|
108
|
+
def add_board_detector(self, detector: BoardDetector) -> None:
|
|
109
|
+
"""Register a board-detection callback.
|
|
110
|
+
|
|
111
|
+
A board detector is a callable that receives a dict of raw probe
|
|
112
|
+
data and returns either a board name string (e.g. ``"Jetson Orin
|
|
113
|
+
Nano"``) or ``None`` if the board was not recognised.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
detector: The detection callable.
|
|
117
|
+
|
|
118
|
+
Raises:
|
|
119
|
+
TypeError: If *detector* is not callable.
|
|
120
|
+
"""
|
|
121
|
+
if not callable(detector):
|
|
122
|
+
raise TypeError(
|
|
123
|
+
f"Expected a callable, got {type(detector).__name__}"
|
|
124
|
+
)
|
|
125
|
+
self._board_detectors.append(detector)
|
|
126
|
+
logger.debug("Registered plugin board detector: %s", detector)
|
|
127
|
+
|
|
128
|
+
def add_framework_checker(self, checker: FrameworkChecker) -> None:
|
|
129
|
+
"""Register a framework-specific compatibility checker.
|
|
130
|
+
|
|
131
|
+
A framework checker receives the system profile and packages list
|
|
132
|
+
and returns additional findings for framework-specific issues
|
|
133
|
+
(e.g. OpenCV build flags, TensorRT version compatibility).
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
checker: The checker callable.
|
|
137
|
+
|
|
138
|
+
Raises:
|
|
139
|
+
TypeError: If *checker* is not callable.
|
|
140
|
+
"""
|
|
141
|
+
if not callable(checker):
|
|
142
|
+
raise TypeError(
|
|
143
|
+
f"Expected a callable, got {type(checker).__name__}"
|
|
144
|
+
)
|
|
145
|
+
self._framework_checkers.append(checker)
|
|
146
|
+
logger.debug("Registered plugin framework checker: %s", checker)
|
|
147
|
+
|
|
148
|
+
def add_reporter(self, reporter: Reporter) -> None:
|
|
149
|
+
"""Register a custom report formatter.
|
|
150
|
+
|
|
151
|
+
A reporter is a callable that takes a list of findings and the
|
|
152
|
+
system profile and returns a formatted report string.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
reporter: The reporter callable.
|
|
156
|
+
|
|
157
|
+
Raises:
|
|
158
|
+
TypeError: If *reporter* is not callable.
|
|
159
|
+
"""
|
|
160
|
+
if not callable(reporter):
|
|
161
|
+
raise TypeError(
|
|
162
|
+
f"Expected a callable, got {type(reporter).__name__}"
|
|
163
|
+
)
|
|
164
|
+
self._reporters.append(reporter)
|
|
165
|
+
logger.debug("Registered plugin reporter: %s", reporter)
|
|
166
|
+
|
|
167
|
+
def pre_scan(self, hook: ScanHook) -> None:
|
|
168
|
+
"""Register a hook that runs before scanning begins.
|
|
169
|
+
|
|
170
|
+
The hook receives the :class:`SystemProfile` and may perform
|
|
171
|
+
logging, telemetry, or side-channel checks. It must not modify
|
|
172
|
+
system state.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
hook: The pre-scan callable.
|
|
176
|
+
|
|
177
|
+
Raises:
|
|
178
|
+
TypeError: If *hook* is not callable.
|
|
179
|
+
"""
|
|
180
|
+
if not callable(hook):
|
|
181
|
+
raise TypeError(
|
|
182
|
+
f"Expected a callable, got {type(hook).__name__}"
|
|
183
|
+
)
|
|
184
|
+
self._pre_scan_hooks.append(hook)
|
|
185
|
+
logger.debug("Registered plugin pre-scan hook: %s", hook)
|
|
186
|
+
|
|
187
|
+
def post_scan(self, hook: ScanHook) -> None:
|
|
188
|
+
"""Register a hook that runs after scanning completes.
|
|
189
|
+
|
|
190
|
+
The hook receives the :class:`SystemProfile`. It must not modify
|
|
191
|
+
system state.
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
hook: The post-scan callable.
|
|
195
|
+
|
|
196
|
+
Raises:
|
|
197
|
+
TypeError: If *hook* is not callable.
|
|
198
|
+
"""
|
|
199
|
+
if not callable(hook):
|
|
200
|
+
raise TypeError(
|
|
201
|
+
f"Expected a callable, got {type(hook).__name__}"
|
|
202
|
+
)
|
|
203
|
+
self._post_scan_hooks.append(hook)
|
|
204
|
+
logger.debug("Registered plugin post-scan hook: %s", hook)
|
|
205
|
+
|
|
206
|
+
# ------------------------------------------------------------------
|
|
207
|
+
# Read-only accessors
|
|
208
|
+
# ------------------------------------------------------------------
|
|
209
|
+
|
|
210
|
+
@property
|
|
211
|
+
def probes(self) -> List[ProbeBase]:
|
|
212
|
+
"""All registered plugin probes (defensive copy)."""
|
|
213
|
+
return list(self._probes)
|
|
214
|
+
|
|
215
|
+
@property
|
|
216
|
+
def rules(self) -> List[Rule]:
|
|
217
|
+
"""All registered plugin rules (defensive copy)."""
|
|
218
|
+
return list(self._rules)
|
|
219
|
+
|
|
220
|
+
@property
|
|
221
|
+
def board_detectors(self) -> List[BoardDetector]:
|
|
222
|
+
"""All registered board-detection callbacks (defensive copy)."""
|
|
223
|
+
return list(self._board_detectors)
|
|
224
|
+
|
|
225
|
+
@property
|
|
226
|
+
def framework_checkers(self) -> List[FrameworkChecker]:
|
|
227
|
+
"""All registered framework checkers (defensive copy)."""
|
|
228
|
+
return list(self._framework_checkers)
|
|
229
|
+
|
|
230
|
+
@property
|
|
231
|
+
def reporters(self) -> List[Reporter]:
|
|
232
|
+
"""All registered report formatters (defensive copy)."""
|
|
233
|
+
return list(self._reporters)
|
|
234
|
+
|
|
235
|
+
@property
|
|
236
|
+
def pre_scan_hooks(self) -> List[ScanHook]:
|
|
237
|
+
"""All registered pre-scan hooks (defensive copy)."""
|
|
238
|
+
return list(self._pre_scan_hooks)
|
|
239
|
+
|
|
240
|
+
@property
|
|
241
|
+
def post_scan_hooks(self) -> List[ScanHook]:
|
|
242
|
+
"""All registered post-scan hooks (defensive copy)."""
|
|
243
|
+
return list(self._post_scan_hooks)
|
|
244
|
+
|
|
245
|
+
# ------------------------------------------------------------------
|
|
246
|
+
# Introspection helpers
|
|
247
|
+
# ------------------------------------------------------------------
|
|
248
|
+
|
|
249
|
+
@property
|
|
250
|
+
def probe_count(self) -> int:
|
|
251
|
+
"""Number of registered probes."""
|
|
252
|
+
return len(self._probes)
|
|
253
|
+
|
|
254
|
+
@property
|
|
255
|
+
def rule_count(self) -> int:
|
|
256
|
+
"""Number of registered rules."""
|
|
257
|
+
return len(self._rules)
|
|
258
|
+
|
|
259
|
+
def summary(self) -> Dict[str, int]:
|
|
260
|
+
"""Return a dict summarising the number of registered extensions."""
|
|
261
|
+
return {
|
|
262
|
+
"probes": len(self._probes),
|
|
263
|
+
"rules": len(self._rules),
|
|
264
|
+
"board_detectors": len(self._board_detectors),
|
|
265
|
+
"framework_checkers": len(self._framework_checkers),
|
|
266
|
+
"reporters": len(self._reporters),
|
|
267
|
+
"pre_scan_hooks": len(self._pre_scan_hooks),
|
|
268
|
+
"post_scan_hooks": len(self._post_scan_hooks),
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
def __repr__(self) -> str:
|
|
272
|
+
parts = ", ".join(f"{k}={v}" for k, v in self.summary().items() if v)
|
|
273
|
+
return f"<HookRegistry({parts})>"
|