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,146 @@
|
|
|
1
|
+
"""Helper functions for filtering, sorting, grouping, and deduplicating findings."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Dict, List, Optional, Set, Sequence, Tuple
|
|
6
|
+
|
|
7
|
+
from pybinaryguard.models.enums import Severity
|
|
8
|
+
from pybinaryguard.models.finding import Finding
|
|
9
|
+
|
|
10
|
+
# Canonical ordering: CRITICAL (most severe) first, PASSED last.
|
|
11
|
+
_SEVERITY_ORDER: Dict[Severity, int] = {
|
|
12
|
+
Severity.CRITICAL: 0,
|
|
13
|
+
Severity.WARNING: 1,
|
|
14
|
+
Severity.INFO: 2,
|
|
15
|
+
Severity.PASSED: 3,
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def filter_findings(
|
|
20
|
+
findings: Sequence[Finding],
|
|
21
|
+
min_severity: Severity,
|
|
22
|
+
) -> List[Finding]:
|
|
23
|
+
"""Return findings whose severity is at or above *min_severity*.
|
|
24
|
+
|
|
25
|
+
Severity is ordered CRITICAL > WARNING > INFO > PASSED, so calling with
|
|
26
|
+
``min_severity=Severity.WARNING`` keeps CRITICAL and WARNING, dropping
|
|
27
|
+
INFO and PASSED.
|
|
28
|
+
|
|
29
|
+
Parameters
|
|
30
|
+
----------
|
|
31
|
+
findings:
|
|
32
|
+
The collection of findings to filter.
|
|
33
|
+
min_severity:
|
|
34
|
+
The lowest severity to include. Findings with a severity that
|
|
35
|
+
compares *less than or equal to* ``min_severity`` (i.e. more severe
|
|
36
|
+
or equally severe) are included.
|
|
37
|
+
|
|
38
|
+
Returns
|
|
39
|
+
-------
|
|
40
|
+
List[Finding]
|
|
41
|
+
A new list containing only the findings that pass the threshold.
|
|
42
|
+
"""
|
|
43
|
+
if not isinstance(min_severity, Severity):
|
|
44
|
+
raise TypeError(
|
|
45
|
+
f"min_severity must be a Severity enum member, got {type(min_severity).__name__}"
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
threshold = _SEVERITY_ORDER[min_severity]
|
|
49
|
+
return [f for f in findings if _SEVERITY_ORDER.get(f.severity, 99) <= threshold]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def sort_findings(findings: Sequence[Finding]) -> List[Finding]:
|
|
53
|
+
"""Sort findings by severity (CRITICAL first), then alphabetically by package name.
|
|
54
|
+
|
|
55
|
+
Findings without a package name sort after those with one.
|
|
56
|
+
|
|
57
|
+
Parameters
|
|
58
|
+
----------
|
|
59
|
+
findings:
|
|
60
|
+
The collection of findings to sort.
|
|
61
|
+
|
|
62
|
+
Returns
|
|
63
|
+
-------
|
|
64
|
+
List[Finding]
|
|
65
|
+
A new list sorted by severity (descending) then package name (ascending).
|
|
66
|
+
"""
|
|
67
|
+
return sorted(
|
|
68
|
+
findings,
|
|
69
|
+
key=lambda f: (
|
|
70
|
+
_SEVERITY_ORDER.get(f.severity, 99),
|
|
71
|
+
f.package or "\uffff", # None sorts last
|
|
72
|
+
),
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def group_by_package(findings: Sequence[Finding]) -> Dict[str, List[Finding]]:
|
|
77
|
+
"""Group findings by their package name.
|
|
78
|
+
|
|
79
|
+
Findings with ``package=None`` are grouped under the key
|
|
80
|
+
``"<unknown>"`` so that every key is a usable string.
|
|
81
|
+
|
|
82
|
+
Parameters
|
|
83
|
+
----------
|
|
84
|
+
findings:
|
|
85
|
+
The collection of findings to group.
|
|
86
|
+
|
|
87
|
+
Returns
|
|
88
|
+
-------
|
|
89
|
+
Dict[str, List[Finding]]
|
|
90
|
+
A mapping from package name to the list of findings for that package.
|
|
91
|
+
"""
|
|
92
|
+
groups: Dict[str, List[Finding]] = {}
|
|
93
|
+
for finding in findings:
|
|
94
|
+
key = finding.package or "<unknown>"
|
|
95
|
+
groups.setdefault(key, []).append(finding)
|
|
96
|
+
return groups
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def group_by_severity(findings: Sequence[Finding]) -> Dict[Severity, List[Finding]]:
|
|
100
|
+
"""Group findings by their severity level.
|
|
101
|
+
|
|
102
|
+
The returned dict only contains keys for severity levels that have at
|
|
103
|
+
least one finding.
|
|
104
|
+
|
|
105
|
+
Parameters
|
|
106
|
+
----------
|
|
107
|
+
findings:
|
|
108
|
+
The collection of findings to group.
|
|
109
|
+
|
|
110
|
+
Returns
|
|
111
|
+
-------
|
|
112
|
+
Dict[Severity, List[Finding]]
|
|
113
|
+
A mapping from severity level to the list of findings at that level.
|
|
114
|
+
"""
|
|
115
|
+
groups: Dict[Severity, List[Finding]] = {}
|
|
116
|
+
for finding in findings:
|
|
117
|
+
groups.setdefault(finding.severity, []).append(finding)
|
|
118
|
+
return groups
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def deduplicate_findings(findings: Sequence[Finding]) -> List[Finding]:
|
|
122
|
+
"""Remove duplicate findings that share the same rule_id and package.
|
|
123
|
+
|
|
124
|
+
When duplicates exist, the first occurrence (in iteration order) is
|
|
125
|
+
kept. Two findings are considered duplicates when both their
|
|
126
|
+
``rule_id`` and ``package`` fields match exactly (including ``None``).
|
|
127
|
+
|
|
128
|
+
Parameters
|
|
129
|
+
----------
|
|
130
|
+
findings:
|
|
131
|
+
The collection of findings to deduplicate.
|
|
132
|
+
|
|
133
|
+
Returns
|
|
134
|
+
-------
|
|
135
|
+
List[Finding]
|
|
136
|
+
A new list with duplicates removed, preserving the order of first
|
|
137
|
+
occurrences.
|
|
138
|
+
"""
|
|
139
|
+
seen: Set[Tuple[str, Optional[str]]] = set()
|
|
140
|
+
result: List[Finding] = []
|
|
141
|
+
for finding in findings:
|
|
142
|
+
key = (finding.rule_id, finding.package)
|
|
143
|
+
if key not in seen:
|
|
144
|
+
seen.add(key)
|
|
145
|
+
result.append(finding)
|
|
146
|
+
return result
|
|
@@ -0,0 +1,508 @@
|
|
|
1
|
+
"""Context-aware fix suggestion generator.
|
|
2
|
+
|
|
3
|
+
Given a :class:`Finding` and a :class:`SystemProfile`, this module produces
|
|
4
|
+
actionable, copy-pasteable remediation commands tailored to the user's
|
|
5
|
+
specific system (OS, architecture, CUDA version, etc.).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Dict, List, Optional, Tuple
|
|
11
|
+
|
|
12
|
+
from pybinaryguard.models.finding import Finding
|
|
13
|
+
from pybinaryguard.models.system import SystemProfile
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# ---------------------------------------------------------------------------
|
|
17
|
+
# Well-known package index URLs for CUDA-specific wheels
|
|
18
|
+
# ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
PYTORCH_INDEX_URLS: Dict[Tuple[int, int], str] = {
|
|
21
|
+
(11, 8): "https://download.pytorch.org/whl/cu118",
|
|
22
|
+
(12, 1): "https://download.pytorch.org/whl/cu121",
|
|
23
|
+
(12, 4): "https://download.pytorch.org/whl/cu124",
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
PYTORCH_CPU_INDEX_URL: str = "https://download.pytorch.org/whl/cpu"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# ---------------------------------------------------------------------------
|
|
30
|
+
# OS-family detection helper
|
|
31
|
+
# ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
_DEBIAN_LIKE = frozenset({"ubuntu", "debian", "linuxmint", "pop", "elementary", "kali"})
|
|
34
|
+
_RHEL_LIKE = frozenset({"centos", "rhel", "fedora", "rocky", "almalinux", "amazon"})
|
|
35
|
+
_SUSE_LIKE = frozenset({"suse", "opensuse", "sles"})
|
|
36
|
+
_ARCH_LIKE = frozenset({"arch", "manjaro", "endeavouros"})
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _detect_pkg_manager(profile: SystemProfile) -> str:
|
|
40
|
+
"""Return the likely package-manager command for the system.
|
|
41
|
+
|
|
42
|
+
Falls back to ``"apt-get"`` when the OS cannot be identified.
|
|
43
|
+
"""
|
|
44
|
+
os_lower = profile.os_name.lower()
|
|
45
|
+
for name in _DEBIAN_LIKE:
|
|
46
|
+
if name in os_lower:
|
|
47
|
+
return "apt-get"
|
|
48
|
+
for name in _RHEL_LIKE:
|
|
49
|
+
if name in os_lower:
|
|
50
|
+
return "yum"
|
|
51
|
+
for name in _SUSE_LIKE:
|
|
52
|
+
if name in os_lower:
|
|
53
|
+
return "zypper"
|
|
54
|
+
for name in _ARCH_LIKE:
|
|
55
|
+
if name in os_lower:
|
|
56
|
+
return "pacman -S"
|
|
57
|
+
return "apt-get"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _format_glibc_version(ver: Optional[Tuple[int, int]]) -> str:
|
|
61
|
+
"""Format a GLIBC version tuple as ``"X.Y"``."""
|
|
62
|
+
if ver is None:
|
|
63
|
+
return "unknown"
|
|
64
|
+
return f"{ver[0]}.{ver[1]}"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _format_cuda_version(ver: Optional[Tuple[int, int]]) -> str:
|
|
68
|
+
"""Format a CUDA version tuple as ``"X.Y"``."""
|
|
69
|
+
if ver is None:
|
|
70
|
+
return "unknown"
|
|
71
|
+
return f"{ver[0]}.{ver[1]}"
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
# ---------------------------------------------------------------------------
|
|
75
|
+
# Per-rule suggestion generators
|
|
76
|
+
# ---------------------------------------------------------------------------
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _suggest_glibc_fix(finding: Finding, profile: SystemProfile) -> str:
|
|
80
|
+
"""Suggest remediation for GLIBC version mismatches."""
|
|
81
|
+
system_glibc = _format_glibc_version(profile.glibc_version)
|
|
82
|
+
pkg_name = finding.package or "the package"
|
|
83
|
+
|
|
84
|
+
lines: List[str] = [
|
|
85
|
+
f"Your system has GLIBC {system_glibc}, which is too old for {pkg_name}.",
|
|
86
|
+
"",
|
|
87
|
+
"Option 1 -- Install an older version of the package that supports your GLIBC:",
|
|
88
|
+
]
|
|
89
|
+
|
|
90
|
+
if finding.package:
|
|
91
|
+
lines.append(f" pip install '{finding.package}<OLDER_VERSION'")
|
|
92
|
+
else:
|
|
93
|
+
lines.append(" pip install 'PACKAGE<OLDER_VERSION'")
|
|
94
|
+
|
|
95
|
+
lines.extend([
|
|
96
|
+
"",
|
|
97
|
+
"Option 2 -- Upgrade your operating system to get a newer GLIBC:",
|
|
98
|
+
])
|
|
99
|
+
pkg_mgr = _detect_pkg_manager(profile)
|
|
100
|
+
if "apt" in pkg_mgr:
|
|
101
|
+
lines.append(" sudo apt-get update && sudo apt-get dist-upgrade")
|
|
102
|
+
elif "yum" in pkg_mgr:
|
|
103
|
+
lines.append(" sudo yum update")
|
|
104
|
+
elif "zypper" in pkg_mgr:
|
|
105
|
+
lines.append(" sudo zypper update")
|
|
106
|
+
else:
|
|
107
|
+
lines.append(f" sudo {pkg_mgr} --sysupgrade")
|
|
108
|
+
|
|
109
|
+
lines.extend([
|
|
110
|
+
"",
|
|
111
|
+
"Option 3 -- Use a container with a newer base image:",
|
|
112
|
+
" docker run --rm -it python:3.12-bookworm pip install " + (finding.package or "PACKAGE"),
|
|
113
|
+
])
|
|
114
|
+
|
|
115
|
+
return "\n".join(lines)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _suggest_cuda_runtime_fix(finding: Finding, profile: SystemProfile) -> str:
|
|
119
|
+
"""Suggest remediation for CUDA runtime mismatches."""
|
|
120
|
+
system_cuda = _format_cuda_version(profile.cuda_runtime_version)
|
|
121
|
+
pkg_name = finding.package or "the package"
|
|
122
|
+
|
|
123
|
+
lines: List[str] = [
|
|
124
|
+
f"Your system has CUDA {system_cuda}, but {pkg_name} needs a different version.",
|
|
125
|
+
"",
|
|
126
|
+
"Option 1 -- Reinstall the package for your CUDA version:",
|
|
127
|
+
]
|
|
128
|
+
|
|
129
|
+
if profile.cuda_runtime_version and profile.cuda_runtime_version in PYTORCH_INDEX_URLS:
|
|
130
|
+
index_url = PYTORCH_INDEX_URLS[profile.cuda_runtime_version]
|
|
131
|
+
if finding.package:
|
|
132
|
+
lines.append(f" pip install {finding.package} --index-url {index_url}")
|
|
133
|
+
else:
|
|
134
|
+
lines.append(f" pip install PACKAGE --index-url {index_url}")
|
|
135
|
+
elif profile.cuda_runtime_version is None:
|
|
136
|
+
lines.append(" # No CUDA detected. Install the CPU-only version instead:")
|
|
137
|
+
if finding.package:
|
|
138
|
+
lines.append(
|
|
139
|
+
f" pip install {finding.package} --index-url {PYTORCH_CPU_INDEX_URL}"
|
|
140
|
+
)
|
|
141
|
+
else:
|
|
142
|
+
lines.append(f" pip install PACKAGE --index-url {PYTORCH_CPU_INDEX_URL}")
|
|
143
|
+
else:
|
|
144
|
+
cuda_ver = _format_cuda_version(profile.cuda_runtime_version)
|
|
145
|
+
lines.append(
|
|
146
|
+
f" # Look for a wheel built for CUDA {cuda_ver} on the package's "
|
|
147
|
+
f"documentation or PyPI page."
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
lines.extend([
|
|
151
|
+
"",
|
|
152
|
+
"Option 2 -- Install the matching CUDA toolkit:",
|
|
153
|
+
" # Visit https://developer.nvidia.com/cuda-toolkit-archive",
|
|
154
|
+
" # and install the version required by the package.",
|
|
155
|
+
])
|
|
156
|
+
|
|
157
|
+
return "\n".join(lines)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _suggest_cuda_lib_fix(finding: Finding, profile: SystemProfile) -> str:
|
|
161
|
+
"""Suggest remediation for missing CUDA libraries."""
|
|
162
|
+
pkg_mgr = _detect_pkg_manager(profile)
|
|
163
|
+
pkg_name = finding.package or "the package"
|
|
164
|
+
|
|
165
|
+
lines: List[str] = [
|
|
166
|
+
f"{pkg_name} requires CUDA math libraries that are not installed.",
|
|
167
|
+
"",
|
|
168
|
+
"Option 1 -- Install the CUDA toolkit:",
|
|
169
|
+
]
|
|
170
|
+
|
|
171
|
+
if "apt" in pkg_mgr:
|
|
172
|
+
lines.append(" sudo apt-get install nvidia-cuda-toolkit")
|
|
173
|
+
elif "yum" in pkg_mgr:
|
|
174
|
+
lines.append(" sudo yum install cuda")
|
|
175
|
+
else:
|
|
176
|
+
lines.append(f" sudo {pkg_mgr} install cuda")
|
|
177
|
+
|
|
178
|
+
lines.extend([
|
|
179
|
+
"",
|
|
180
|
+
"Option 2 -- Reinstall the package for your CUDA version:",
|
|
181
|
+
])
|
|
182
|
+
|
|
183
|
+
if profile.cuda_runtime_version and profile.cuda_runtime_version in PYTORCH_INDEX_URLS:
|
|
184
|
+
index_url = PYTORCH_INDEX_URLS[profile.cuda_runtime_version]
|
|
185
|
+
if finding.package:
|
|
186
|
+
lines.append(f" pip install --force-reinstall {finding.package} --index-url {index_url}")
|
|
187
|
+
else:
|
|
188
|
+
lines.append(f" pip install --force-reinstall PACKAGE --index-url {index_url}")
|
|
189
|
+
else:
|
|
190
|
+
lines.append(" pip install --force-reinstall PACKAGE # use the correct --index-url for your CUDA version")
|
|
191
|
+
|
|
192
|
+
return "\n".join(lines)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _suggest_cudnn_fix(finding: Finding, profile: SystemProfile) -> str:
|
|
196
|
+
"""Suggest remediation for missing cuDNN libraries."""
|
|
197
|
+
pkg_mgr = _detect_pkg_manager(profile)
|
|
198
|
+
cuda_ver = _format_cuda_version(profile.cuda_runtime_version)
|
|
199
|
+
|
|
200
|
+
lines: List[str] = [
|
|
201
|
+
"The cuDNN library is required for GPU-accelerated deep learning but is not installed.",
|
|
202
|
+
"",
|
|
203
|
+
"Option 1 -- Install cuDNN via your package manager:",
|
|
204
|
+
]
|
|
205
|
+
|
|
206
|
+
if "apt" in pkg_mgr:
|
|
207
|
+
lines.append(" sudo apt-get install libcudnn8 libcudnn8-dev")
|
|
208
|
+
elif "yum" in pkg_mgr:
|
|
209
|
+
lines.append(" sudo yum install libcudnn8 libcudnn8-devel")
|
|
210
|
+
else:
|
|
211
|
+
lines.append(f" sudo {pkg_mgr} install libcudnn8")
|
|
212
|
+
|
|
213
|
+
lines.extend([
|
|
214
|
+
"",
|
|
215
|
+
"Option 2 -- Install cuDNN from NVIDIA:",
|
|
216
|
+
f" # Visit https://developer.nvidia.com/cudnn and download the version for CUDA {cuda_ver}.",
|
|
217
|
+
])
|
|
218
|
+
|
|
219
|
+
return "\n".join(lines)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _suggest_cuda_driver_fix(finding: Finding, profile: SystemProfile) -> str:
|
|
223
|
+
"""Suggest remediation for outdated GPU drivers."""
|
|
224
|
+
pkg_mgr = _detect_pkg_manager(profile)
|
|
225
|
+
driver_ver = profile.gpu_driver_version or "unknown"
|
|
226
|
+
|
|
227
|
+
lines: List[str] = [
|
|
228
|
+
f"Your GPU driver ({driver_ver}) is too old for the installed CUDA version.",
|
|
229
|
+
"",
|
|
230
|
+
"Option 1 -- Update your NVIDIA driver:",
|
|
231
|
+
]
|
|
232
|
+
|
|
233
|
+
if "apt" in pkg_mgr:
|
|
234
|
+
lines.extend([
|
|
235
|
+
" sudo apt-get update",
|
|
236
|
+
" sudo apt-get install --upgrade nvidia-driver-550",
|
|
237
|
+
])
|
|
238
|
+
elif "yum" in pkg_mgr:
|
|
239
|
+
lines.append(" sudo yum install nvidia-driver-latest-dkms")
|
|
240
|
+
else:
|
|
241
|
+
lines.append(f" sudo {pkg_mgr} install nvidia-driver")
|
|
242
|
+
|
|
243
|
+
lines.extend([
|
|
244
|
+
"",
|
|
245
|
+
"Option 2 -- Download the latest driver from NVIDIA:",
|
|
246
|
+
" # Visit https://www.nvidia.com/Download/index.aspx",
|
|
247
|
+
])
|
|
248
|
+
|
|
249
|
+
return "\n".join(lines)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _suggest_container_driver_fix(finding: Finding, profile: SystemProfile) -> str:
|
|
253
|
+
"""Suggest remediation for container/host driver mismatches."""
|
|
254
|
+
lines: List[str] = [
|
|
255
|
+
"The NVIDIA driver inside your container does not match the host driver.",
|
|
256
|
+
"",
|
|
257
|
+
"Option 1 -- Use nvidia-container-toolkit (recommended):",
|
|
258
|
+
" # On the host, install the toolkit:",
|
|
259
|
+
" # https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/install-guide.html",
|
|
260
|
+
" # Then run your container with GPU access:",
|
|
261
|
+
" docker run --gpus all YOUR_IMAGE",
|
|
262
|
+
"",
|
|
263
|
+
"Option 2 -- Match CUDA versions:",
|
|
264
|
+
" # Use a container base image whose CUDA version is supported by your host driver.",
|
|
265
|
+
]
|
|
266
|
+
|
|
267
|
+
if profile.gpu_driver_version:
|
|
268
|
+
lines.append(f" # Your host driver is {profile.gpu_driver_version}.")
|
|
269
|
+
lines.append(" # Check compatibility at https://docs.nvidia.com/deploy/cuda-compatibility/")
|
|
270
|
+
|
|
271
|
+
return "\n".join(lines)
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def _suggest_python_abi_fix(finding: Finding, profile: SystemProfile) -> str:
|
|
275
|
+
"""Suggest remediation for Python ABI mismatches."""
|
|
276
|
+
py_ver = ".".join(str(v) for v in profile.python_version) if profile.python_version != (0, 0, 0) else "unknown"
|
|
277
|
+
pkg_name = finding.package or "the package"
|
|
278
|
+
|
|
279
|
+
lines: List[str] = [
|
|
280
|
+
f"{pkg_name} was compiled for a different Python version (you are running {py_ver}).",
|
|
281
|
+
"",
|
|
282
|
+
"Fix -- Reinstall the package for your current Python:",
|
|
283
|
+
]
|
|
284
|
+
|
|
285
|
+
if finding.package:
|
|
286
|
+
lines.append(f" pip install --force-reinstall --no-cache-dir {finding.package}")
|
|
287
|
+
else:
|
|
288
|
+
lines.append(" pip install --force-reinstall --no-cache-dir PACKAGE")
|
|
289
|
+
|
|
290
|
+
lines.extend([
|
|
291
|
+
"",
|
|
292
|
+
"If that fails, the package may not support your Python version yet.",
|
|
293
|
+
"Check the package's PyPI page for compatible Python versions.",
|
|
294
|
+
])
|
|
295
|
+
|
|
296
|
+
return "\n".join(lines)
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def _suggest_numpy_abi_fix(finding: Finding, profile: SystemProfile) -> str:
|
|
300
|
+
"""Suggest remediation for NumPy C API version mismatches."""
|
|
301
|
+
pkg_name = finding.package or "the package"
|
|
302
|
+
|
|
303
|
+
lines: List[str] = [
|
|
304
|
+
f"{pkg_name} was built against a different NumPy C API version.",
|
|
305
|
+
"",
|
|
306
|
+
"Option 1 -- Reinstall the package (will rebuild against current NumPy):",
|
|
307
|
+
]
|
|
308
|
+
|
|
309
|
+
if finding.package:
|
|
310
|
+
lines.append(f" pip install --force-reinstall --no-cache-dir {finding.package}")
|
|
311
|
+
else:
|
|
312
|
+
lines.append(" pip install --force-reinstall --no-cache-dir PACKAGE")
|
|
313
|
+
|
|
314
|
+
lines.extend([
|
|
315
|
+
"",
|
|
316
|
+
"Option 2 -- Install a compatible NumPy version:",
|
|
317
|
+
" pip install 'numpy<2' # if the package was built for NumPy 1.x",
|
|
318
|
+
" pip install 'numpy>=2' # if the package was built for NumPy 2.x",
|
|
319
|
+
])
|
|
320
|
+
|
|
321
|
+
return "\n".join(lines)
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def _suggest_arch_fix(finding: Finding, profile: SystemProfile) -> str:
|
|
325
|
+
"""Suggest remediation for platform/architecture mismatches."""
|
|
326
|
+
arch = profile.architecture.value if profile.architecture else "unknown"
|
|
327
|
+
pkg_name = finding.package or "the package"
|
|
328
|
+
|
|
329
|
+
lines: List[str] = [
|
|
330
|
+
f"{pkg_name} was built for a different platform (your architecture: {arch}).",
|
|
331
|
+
"",
|
|
332
|
+
"Option 1 -- Install the correct wheel for your platform:",
|
|
333
|
+
]
|
|
334
|
+
|
|
335
|
+
if finding.package:
|
|
336
|
+
lines.append(
|
|
337
|
+
f" pip install --force-reinstall {finding.package} "
|
|
338
|
+
f"--only-binary :all: --platform manylinux_2_17_{arch}"
|
|
339
|
+
)
|
|
340
|
+
else:
|
|
341
|
+
lines.append(
|
|
342
|
+
f" pip install --force-reinstall PACKAGE "
|
|
343
|
+
f"--only-binary :all: --platform manylinux_2_17_{arch}"
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
lines.extend([
|
|
347
|
+
"",
|
|
348
|
+
"Option 2 -- Build from source:",
|
|
349
|
+
])
|
|
350
|
+
|
|
351
|
+
if finding.package:
|
|
352
|
+
lines.append(f" pip install --no-binary :all: {finding.package}")
|
|
353
|
+
else:
|
|
354
|
+
lines.append(" pip install --no-binary :all: PACKAGE")
|
|
355
|
+
|
|
356
|
+
return "\n".join(lines)
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def _suggest_missing_lib_fix(finding: Finding, profile: SystemProfile) -> str:
|
|
360
|
+
"""Suggest installing missing shared libraries."""
|
|
361
|
+
pkg_mgr = _detect_pkg_manager(profile)
|
|
362
|
+
|
|
363
|
+
lines: List[str] = [
|
|
364
|
+
"One or more shared libraries required by the package are not installed.",
|
|
365
|
+
"",
|
|
366
|
+
"Install the missing libraries with your system package manager:",
|
|
367
|
+
]
|
|
368
|
+
|
|
369
|
+
if "apt" in pkg_mgr:
|
|
370
|
+
lines.extend([
|
|
371
|
+
" sudo apt-get update",
|
|
372
|
+
" sudo apt-get install LIBRARY_PACKAGE_NAME",
|
|
373
|
+
"",
|
|
374
|
+
" # To find which package provides a specific .so file:",
|
|
375
|
+
" apt-file search LIBRARY_NAME.so",
|
|
376
|
+
])
|
|
377
|
+
elif "yum" in pkg_mgr:
|
|
378
|
+
lines.extend([
|
|
379
|
+
" sudo yum install LIBRARY_PACKAGE_NAME",
|
|
380
|
+
"",
|
|
381
|
+
" # To find which package provides a specific .so file:",
|
|
382
|
+
" yum provides '*/LIBRARY_NAME.so'",
|
|
383
|
+
])
|
|
384
|
+
elif "zypper" in pkg_mgr:
|
|
385
|
+
lines.extend([
|
|
386
|
+
" sudo zypper install LIBRARY_PACKAGE_NAME",
|
|
387
|
+
"",
|
|
388
|
+
" # To find which package provides a specific .so file:",
|
|
389
|
+
" zypper search --provides LIBRARY_NAME.so",
|
|
390
|
+
])
|
|
391
|
+
elif "pacman" in pkg_mgr:
|
|
392
|
+
lines.extend([
|
|
393
|
+
" sudo pacman -S LIBRARY_PACKAGE_NAME",
|
|
394
|
+
"",
|
|
395
|
+
" # To find which package provides a specific .so file:",
|
|
396
|
+
" pacman -F LIBRARY_NAME.so",
|
|
397
|
+
])
|
|
398
|
+
else:
|
|
399
|
+
lines.append(f" sudo {pkg_mgr} install LIBRARY_PACKAGE_NAME")
|
|
400
|
+
|
|
401
|
+
return "\n".join(lines)
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def _suggest_illegal_instruction_fix(finding: Finding, profile: SystemProfile) -> str:
|
|
405
|
+
"""Suggest remediation for CPU instruction set mismatches."""
|
|
406
|
+
cpu_model = profile.cpu_model or "your CPU"
|
|
407
|
+
pkg_name = finding.package or "the package"
|
|
408
|
+
|
|
409
|
+
lines: List[str] = [
|
|
410
|
+
f"{pkg_name} uses CPU instructions not supported by {cpu_model}.",
|
|
411
|
+
]
|
|
412
|
+
|
|
413
|
+
if not profile.has_avx2:
|
|
414
|
+
lines.append(" Your CPU does NOT support AVX2 instructions.")
|
|
415
|
+
if not profile.has_avx512:
|
|
416
|
+
lines.append(" Your CPU does NOT support AVX-512 instructions.")
|
|
417
|
+
|
|
418
|
+
lines.extend([
|
|
419
|
+
"",
|
|
420
|
+
"Option 1 -- Install a version built without advanced CPU instructions:",
|
|
421
|
+
])
|
|
422
|
+
|
|
423
|
+
if finding.package:
|
|
424
|
+
lines.append(
|
|
425
|
+
f" pip install --force-reinstall --no-cache-dir {finding.package}"
|
|
426
|
+
)
|
|
427
|
+
lines.append(
|
|
428
|
+
" # Look for a build without SIMD optimizations on the package's releases page."
|
|
429
|
+
)
|
|
430
|
+
else:
|
|
431
|
+
lines.append(" pip install --force-reinstall --no-cache-dir PACKAGE")
|
|
432
|
+
|
|
433
|
+
lines.extend([
|
|
434
|
+
"",
|
|
435
|
+
"Option 2 -- Run on a machine with a newer CPU that supports AVX2/AVX-512.",
|
|
436
|
+
])
|
|
437
|
+
|
|
438
|
+
return "\n".join(lines)
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
# ---------------------------------------------------------------------------
|
|
442
|
+
# Rule-ID to handler mapping
|
|
443
|
+
# ---------------------------------------------------------------------------
|
|
444
|
+
|
|
445
|
+
_SUGGESTION_HANDLERS: Dict[str, object] = {
|
|
446
|
+
"GLIBC_VERSION_MISMATCH": _suggest_glibc_fix,
|
|
447
|
+
"CUDA_RUNTIME_MISMATCH": _suggest_cuda_runtime_fix,
|
|
448
|
+
"CUDA_LIB_MISSING": _suggest_cuda_lib_fix,
|
|
449
|
+
"CUDNN_VERSION_MISMATCH": _suggest_cudnn_fix,
|
|
450
|
+
"CUDA_DRIVER_TOO_OLD": _suggest_cuda_driver_fix,
|
|
451
|
+
"CONTAINER_DRIVER_MISMATCH": _suggest_container_driver_fix,
|
|
452
|
+
"PYTHON_ABI_MISMATCH": _suggest_python_abi_fix,
|
|
453
|
+
"NUMPY_ABI_MISMATCH": _suggest_numpy_abi_fix,
|
|
454
|
+
"ARCH_MISMATCH": _suggest_arch_fix,
|
|
455
|
+
"ILLEGAL_INSTRUCTION_RISK": _suggest_illegal_instruction_fix,
|
|
456
|
+
"MISSING_SHARED_LIB": _suggest_missing_lib_fix,
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
# ---------------------------------------------------------------------------
|
|
461
|
+
# Public API
|
|
462
|
+
# ---------------------------------------------------------------------------
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
def suggest_fix(finding: Finding, profile: SystemProfile) -> str:
|
|
466
|
+
"""Generate a context-aware fix suggestion for a finding.
|
|
467
|
+
|
|
468
|
+
The suggestion is tailored to the user's system (OS, CUDA version,
|
|
469
|
+
architecture, etc.) and includes copy-pasteable commands wherever
|
|
470
|
+
possible.
|
|
471
|
+
|
|
472
|
+
Parameters
|
|
473
|
+
----------
|
|
474
|
+
finding:
|
|
475
|
+
The diagnostic finding to remediate.
|
|
476
|
+
profile:
|
|
477
|
+
The system profile describing the target machine.
|
|
478
|
+
|
|
479
|
+
Returns
|
|
480
|
+
-------
|
|
481
|
+
str
|
|
482
|
+
A multi-line string with numbered options and shell commands.
|
|
483
|
+
If no specific handler is registered for the finding's
|
|
484
|
+
``rule_id``, a generic suggestion based on the finding's own
|
|
485
|
+
``suggestion`` field is returned.
|
|
486
|
+
"""
|
|
487
|
+
if not isinstance(finding, Finding):
|
|
488
|
+
raise TypeError(
|
|
489
|
+
f"finding must be a Finding instance, got {type(finding).__name__}"
|
|
490
|
+
)
|
|
491
|
+
if not isinstance(profile, SystemProfile):
|
|
492
|
+
raise TypeError(
|
|
493
|
+
f"profile must be a SystemProfile instance, got {type(profile).__name__}"
|
|
494
|
+
)
|
|
495
|
+
|
|
496
|
+
handler = _SUGGESTION_HANDLERS.get(finding.rule_id)
|
|
497
|
+
if handler is not None:
|
|
498
|
+
# All handlers have the same (Finding, SystemProfile) -> str signature.
|
|
499
|
+
return handler(finding, profile) # type: ignore[operator]
|
|
500
|
+
|
|
501
|
+
# Fallback: use the finding's own suggestion or a generic message.
|
|
502
|
+
if finding.suggestion:
|
|
503
|
+
return finding.suggestion
|
|
504
|
+
|
|
505
|
+
return (
|
|
506
|
+
f"No specific fix suggestion is available for rule '{finding.rule_id}'. "
|
|
507
|
+
f"Check the package documentation or open an issue with the maintainers."
|
|
508
|
+
)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""AI framework deep inspection layer.
|
|
2
|
+
|
|
3
|
+
Framework-specific validators that understand internal structure beyond
|
|
4
|
+
generic binary inspection.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from .pytorch import check_pytorch_cuda_abi, detect_pytorch_build
|
|
10
|
+
from .tensorrt import validate_tensorrt_engine
|
|
11
|
+
from .onnxruntime import check_onnx_runtime_providers
|
|
12
|
+
from .tensorflow import check_tensorflow_compute_capability
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"check_pytorch_cuda_abi",
|
|
16
|
+
"detect_pytorch_build",
|
|
17
|
+
"validate_tensorrt_engine",
|
|
18
|
+
"check_onnx_runtime_providers",
|
|
19
|
+
"check_tensorflow_compute_capability",
|
|
20
|
+
]
|