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,152 @@
|
|
|
1
|
+
"""Lockfile format for environment snapshots.
|
|
2
|
+
|
|
3
|
+
JSON-based lockfile that captures binary hashes, GPU stack, and system state
|
|
4
|
+
beyond what requirements.txt provides.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any, Dict, List, Optional
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class PackageSnapshot:
|
|
17
|
+
"""Snapshot of a single package's binary state."""
|
|
18
|
+
|
|
19
|
+
name: str
|
|
20
|
+
version: str
|
|
21
|
+
cuda_version: Optional[str] = None
|
|
22
|
+
binary_hashes: Dict[str, str] = field(default_factory=dict) # path -> SHA256
|
|
23
|
+
manylinux_tag: Optional[str] = None
|
|
24
|
+
wheel_tags: List[str] = field(default_factory=list)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class SystemSnapshot:
|
|
29
|
+
"""Snapshot of system configuration."""
|
|
30
|
+
|
|
31
|
+
python_version: str
|
|
32
|
+
platform: str
|
|
33
|
+
architecture: str
|
|
34
|
+
glibc_version: Optional[str] = None
|
|
35
|
+
cuda_runtime: Optional[str] = None
|
|
36
|
+
cuda_driver: Optional[str] = None
|
|
37
|
+
cuda_compute_capability: Optional[str] = None
|
|
38
|
+
tensorrt_version: Optional[str] = None
|
|
39
|
+
cudnn_version: Optional[str] = None
|
|
40
|
+
detected_board: Optional[str] = None
|
|
41
|
+
cpu_flags: List[str] = field(default_factory=list)
|
|
42
|
+
container_runtime: Optional[str] = None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass
|
|
46
|
+
class Lockfile:
|
|
47
|
+
"""Complete environment lockfile.
|
|
48
|
+
|
|
49
|
+
Captures everything needed to reproduce a binary-compatible environment,
|
|
50
|
+
including information that pip freeze and conda export don't provide.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
version: str = "1.0"
|
|
54
|
+
timestamp: str = ""
|
|
55
|
+
system: Optional[SystemSnapshot] = None
|
|
56
|
+
packages: List[PackageSnapshot] = field(default_factory=list)
|
|
57
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
58
|
+
|
|
59
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
60
|
+
"""Convert lockfile to dictionary."""
|
|
61
|
+
return {
|
|
62
|
+
"pybinaryguard_lockfile": self.version,
|
|
63
|
+
"timestamp": self.timestamp,
|
|
64
|
+
"system": {
|
|
65
|
+
"python_version": self.system.python_version if self.system else "",
|
|
66
|
+
"platform": self.system.platform if self.system else "",
|
|
67
|
+
"architecture": self.system.architecture if self.system else "",
|
|
68
|
+
"glibc_version": self.system.glibc_version if self.system else None,
|
|
69
|
+
"cuda_runtime": self.system.cuda_runtime if self.system else None,
|
|
70
|
+
"cuda_driver": self.system.cuda_driver if self.system else None,
|
|
71
|
+
"cuda_compute_capability": self.system.cuda_compute_capability if self.system else None,
|
|
72
|
+
"tensorrt_version": self.system.tensorrt_version if self.system else None,
|
|
73
|
+
"cudnn_version": self.system.cudnn_version if self.system else None,
|
|
74
|
+
"detected_board": self.system.detected_board if self.system else None,
|
|
75
|
+
"cpu_flags": self.system.cpu_flags if self.system else [],
|
|
76
|
+
"container_runtime": self.system.container_runtime if self.system else None,
|
|
77
|
+
},
|
|
78
|
+
"packages": [
|
|
79
|
+
{
|
|
80
|
+
"name": pkg.name,
|
|
81
|
+
"version": pkg.version,
|
|
82
|
+
"cuda_version": pkg.cuda_version,
|
|
83
|
+
"binary_hashes": pkg.binary_hashes,
|
|
84
|
+
"manylinux_tag": pkg.manylinux_tag,
|
|
85
|
+
"wheel_tags": pkg.wheel_tags,
|
|
86
|
+
}
|
|
87
|
+
for pkg in self.packages
|
|
88
|
+
],
|
|
89
|
+
"metadata": self.metadata,
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
def to_json(self, indent: int = 2) -> str:
|
|
93
|
+
"""Convert lockfile to JSON string."""
|
|
94
|
+
return json.dumps(self.to_dict(), indent=indent, default=str)
|
|
95
|
+
|
|
96
|
+
def save(self, path: str) -> None:
|
|
97
|
+
"""Save lockfile to disk."""
|
|
98
|
+
Path(path).write_text(self.to_json())
|
|
99
|
+
|
|
100
|
+
@classmethod
|
|
101
|
+
def from_dict(cls, data: Dict[str, Any]) -> Lockfile:
|
|
102
|
+
"""Load lockfile from dictionary."""
|
|
103
|
+
system_data = data.get("system", {})
|
|
104
|
+
system = SystemSnapshot(
|
|
105
|
+
python_version=system_data.get("python_version", ""),
|
|
106
|
+
platform=system_data.get("platform", ""),
|
|
107
|
+
architecture=system_data.get("architecture", ""),
|
|
108
|
+
glibc_version=system_data.get("glibc_version"),
|
|
109
|
+
cuda_runtime=system_data.get("cuda_runtime"),
|
|
110
|
+
cuda_driver=system_data.get("cuda_driver"),
|
|
111
|
+
cuda_compute_capability=system_data.get("cuda_compute_capability"),
|
|
112
|
+
tensorrt_version=system_data.get("tensorrt_version"),
|
|
113
|
+
cudnn_version=system_data.get("cudnn_version"),
|
|
114
|
+
detected_board=system_data.get("detected_board"),
|
|
115
|
+
cpu_flags=system_data.get("cpu_flags", []),
|
|
116
|
+
container_runtime=system_data.get("container_runtime"),
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
packages = [
|
|
120
|
+
PackageSnapshot(
|
|
121
|
+
name=pkg["name"],
|
|
122
|
+
version=pkg["version"],
|
|
123
|
+
cuda_version=pkg.get("cuda_version"),
|
|
124
|
+
binary_hashes=pkg.get("binary_hashes", {}),
|
|
125
|
+
manylinux_tag=pkg.get("manylinux_tag"),
|
|
126
|
+
wheel_tags=pkg.get("wheel_tags", []),
|
|
127
|
+
)
|
|
128
|
+
for pkg in data.get("packages", [])
|
|
129
|
+
]
|
|
130
|
+
|
|
131
|
+
return cls(
|
|
132
|
+
version=data.get("pybinaryguard_lockfile", "1.0"),
|
|
133
|
+
timestamp=data.get("timestamp", ""),
|
|
134
|
+
system=system,
|
|
135
|
+
packages=packages,
|
|
136
|
+
metadata=data.get("metadata", {}),
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
@classmethod
|
|
140
|
+
def from_json(cls, json_str: str) -> Lockfile:
|
|
141
|
+
"""Load lockfile from JSON string."""
|
|
142
|
+
return cls.from_dict(json.loads(json_str))
|
|
143
|
+
|
|
144
|
+
@classmethod
|
|
145
|
+
def load(cls, path: str) -> Lockfile:
|
|
146
|
+
"""Load lockfile from disk."""
|
|
147
|
+
return cls.from_json(Path(path).read_text())
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def load_lockfile(path: str) -> Lockfile:
|
|
151
|
+
"""Convenience function to load a lockfile."""
|
|
152
|
+
return Lockfile.load(path)
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
"""Environment snapshot verifier.
|
|
2
|
+
|
|
3
|
+
Verifies current environment against a lockfile and runs compatibility checks.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import hashlib
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import List, Optional
|
|
12
|
+
|
|
13
|
+
from pybinaryguard.models.system import SystemProfile
|
|
14
|
+
from pybinaryguard.models.package import PackageBinaryInfo
|
|
15
|
+
from pybinaryguard.snapshot.lockfile import Lockfile
|
|
16
|
+
from pybinaryguard.predictor import predict_import_failures
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class VerificationIssue:
|
|
21
|
+
"""An issue found during verification."""
|
|
22
|
+
|
|
23
|
+
severity: str # "error", "warning", "info"
|
|
24
|
+
category: str # "system", "package", "binary_hash", "compatibility"
|
|
25
|
+
message: str
|
|
26
|
+
package: Optional[str] = None
|
|
27
|
+
expected: Optional[str] = None
|
|
28
|
+
actual: Optional[str] = None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class VerificationResult:
|
|
33
|
+
"""Result of environment verification."""
|
|
34
|
+
|
|
35
|
+
success: bool
|
|
36
|
+
issues: List[VerificationIssue] = field(default_factory=list)
|
|
37
|
+
packages_verified: int = 0
|
|
38
|
+
binaries_verified: int = 0
|
|
39
|
+
hash_mismatches: int = 0
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def error_count(self) -> int:
|
|
43
|
+
return sum(1 for i in self.issues if i.severity == "error")
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def warning_count(self) -> int:
|
|
47
|
+
return sum(1 for i in self.issues if i.severity == "warning")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class SnapshotVerifier:
|
|
51
|
+
"""Verifies environment against a snapshot lockfile."""
|
|
52
|
+
|
|
53
|
+
def __init__(
|
|
54
|
+
self,
|
|
55
|
+
lockfile: Lockfile,
|
|
56
|
+
profile: SystemProfile,
|
|
57
|
+
packages: List[PackageBinaryInfo]
|
|
58
|
+
):
|
|
59
|
+
"""Initialize verifier.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
lockfile: Lockfile to verify against
|
|
63
|
+
profile: Current system profile
|
|
64
|
+
packages: Currently installed packages
|
|
65
|
+
"""
|
|
66
|
+
self.lockfile = lockfile
|
|
67
|
+
self.profile = profile
|
|
68
|
+
self.packages = packages
|
|
69
|
+
|
|
70
|
+
# Build package lookup
|
|
71
|
+
self.package_map = {pkg.name.lower(): pkg for pkg in packages}
|
|
72
|
+
|
|
73
|
+
def verify(
|
|
74
|
+
self,
|
|
75
|
+
check_hashes: bool = True,
|
|
76
|
+
run_compatibility_checks: bool = True
|
|
77
|
+
) -> VerificationResult:
|
|
78
|
+
"""Verify current environment against lockfile.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
check_hashes: Whether to verify binary hashes
|
|
82
|
+
run_compatibility_checks: Whether to run predictive compatibility checks
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
VerificationResult with issues found
|
|
86
|
+
"""
|
|
87
|
+
result = VerificationResult(success=True)
|
|
88
|
+
|
|
89
|
+
# Verify system configuration
|
|
90
|
+
self._verify_system(result)
|
|
91
|
+
|
|
92
|
+
# Verify packages
|
|
93
|
+
self._verify_packages(result, check_hashes)
|
|
94
|
+
|
|
95
|
+
# Run compatibility checks
|
|
96
|
+
if run_compatibility_checks:
|
|
97
|
+
self._run_compatibility_checks(result)
|
|
98
|
+
|
|
99
|
+
# Determine overall success
|
|
100
|
+
result.success = result.error_count == 0
|
|
101
|
+
|
|
102
|
+
return result
|
|
103
|
+
|
|
104
|
+
def _verify_system(self, result: VerificationResult) -> None:
|
|
105
|
+
"""Verify system configuration matches lockfile."""
|
|
106
|
+
if not self.lockfile.system:
|
|
107
|
+
return
|
|
108
|
+
|
|
109
|
+
snapshot_sys = self.lockfile.system
|
|
110
|
+
|
|
111
|
+
# Check Python version
|
|
112
|
+
py_ver = self.profile.python_version
|
|
113
|
+
current_py = f"{py_ver[0]}.{py_ver[1]}.{py_ver[2]}" if py_ver else ""
|
|
114
|
+
|
|
115
|
+
if snapshot_sys.python_version and current_py != snapshot_sys.python_version:
|
|
116
|
+
result.issues.append(
|
|
117
|
+
VerificationIssue(
|
|
118
|
+
severity="error",
|
|
119
|
+
category="system",
|
|
120
|
+
message="Python version mismatch",
|
|
121
|
+
expected=snapshot_sys.python_version,
|
|
122
|
+
actual=current_py,
|
|
123
|
+
)
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
# Check GLIBC version
|
|
127
|
+
if snapshot_sys.glibc_version and self.profile.glibc_version:
|
|
128
|
+
if self.profile.glibc_version != snapshot_sys.glibc_version:
|
|
129
|
+
result.issues.append(
|
|
130
|
+
VerificationIssue(
|
|
131
|
+
severity="warning",
|
|
132
|
+
category="system",
|
|
133
|
+
message="GLIBC version differs from snapshot",
|
|
134
|
+
expected=snapshot_sys.glibc_version,
|
|
135
|
+
actual=self.profile.glibc_version,
|
|
136
|
+
)
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
# Check CUDA runtime
|
|
140
|
+
if snapshot_sys.cuda_runtime and self.profile.cuda_runtime_version:
|
|
141
|
+
if self.profile.cuda_runtime_version != snapshot_sys.cuda_runtime:
|
|
142
|
+
result.issues.append(
|
|
143
|
+
VerificationIssue(
|
|
144
|
+
severity="warning",
|
|
145
|
+
category="system",
|
|
146
|
+
message="CUDA runtime version differs",
|
|
147
|
+
expected=snapshot_sys.cuda_runtime,
|
|
148
|
+
actual=self.profile.cuda_runtime_version,
|
|
149
|
+
)
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
# Check detected board
|
|
153
|
+
from pybinaryguard.profiles import match_board_profile
|
|
154
|
+
board_profile = match_board_profile(self.profile)
|
|
155
|
+
current_board = board_profile.display_name if board_profile else None
|
|
156
|
+
|
|
157
|
+
if snapshot_sys.detected_board and current_board != snapshot_sys.detected_board:
|
|
158
|
+
result.issues.append(
|
|
159
|
+
VerificationIssue(
|
|
160
|
+
severity="error",
|
|
161
|
+
category="system",
|
|
162
|
+
message="Board mismatch - environment was created for different hardware",
|
|
163
|
+
expected=snapshot_sys.detected_board,
|
|
164
|
+
actual=current_board or "Unknown",
|
|
165
|
+
)
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
def _verify_packages(self, result: VerificationResult, check_hashes: bool) -> None:
|
|
169
|
+
"""Verify packages match lockfile."""
|
|
170
|
+
for pkg_snapshot in self.lockfile.packages:
|
|
171
|
+
result.packages_verified += 1
|
|
172
|
+
|
|
173
|
+
# Check if package is installed
|
|
174
|
+
current_pkg = self.package_map.get(pkg_snapshot.name.lower())
|
|
175
|
+
|
|
176
|
+
if not current_pkg:
|
|
177
|
+
result.issues.append(
|
|
178
|
+
VerificationIssue(
|
|
179
|
+
severity="error",
|
|
180
|
+
category="package",
|
|
181
|
+
message=f"Package missing from environment",
|
|
182
|
+
package=pkg_snapshot.name,
|
|
183
|
+
expected=pkg_snapshot.version,
|
|
184
|
+
actual=None,
|
|
185
|
+
)
|
|
186
|
+
)
|
|
187
|
+
continue
|
|
188
|
+
|
|
189
|
+
# Check version
|
|
190
|
+
if current_pkg.version != pkg_snapshot.version:
|
|
191
|
+
result.issues.append(
|
|
192
|
+
VerificationIssue(
|
|
193
|
+
severity="error",
|
|
194
|
+
category="package",
|
|
195
|
+
message=f"Package version mismatch",
|
|
196
|
+
package=pkg_snapshot.name,
|
|
197
|
+
expected=pkg_snapshot.version,
|
|
198
|
+
actual=current_pkg.version,
|
|
199
|
+
)
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
# Check CUDA version for GPU packages
|
|
203
|
+
if pkg_snapshot.cuda_version and current_pkg.cuda_version:
|
|
204
|
+
if current_pkg.cuda_version != pkg_snapshot.cuda_version:
|
|
205
|
+
result.issues.append(
|
|
206
|
+
VerificationIssue(
|
|
207
|
+
severity="warning",
|
|
208
|
+
category="package",
|
|
209
|
+
message=f"CUDA build version differs",
|
|
210
|
+
package=pkg_snapshot.name,
|
|
211
|
+
expected=pkg_snapshot.cuda_version,
|
|
212
|
+
actual=current_pkg.cuda_version,
|
|
213
|
+
)
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
# Verify binary hashes
|
|
217
|
+
if check_hashes and pkg_snapshot.binary_hashes:
|
|
218
|
+
self._verify_hashes(result, pkg_snapshot, current_pkg)
|
|
219
|
+
|
|
220
|
+
def _verify_hashes(
|
|
221
|
+
self,
|
|
222
|
+
result: VerificationResult,
|
|
223
|
+
pkg_snapshot,
|
|
224
|
+
current_pkg: PackageBinaryInfo
|
|
225
|
+
) -> None:
|
|
226
|
+
"""Verify binary file hashes."""
|
|
227
|
+
for rel_path, expected_hash in pkg_snapshot.binary_hashes.items():
|
|
228
|
+
result.binaries_verified += 1
|
|
229
|
+
|
|
230
|
+
# Find matching shared object
|
|
231
|
+
full_path = None
|
|
232
|
+
for so in current_pkg.shared_objects:
|
|
233
|
+
if so.path and rel_path in so.path:
|
|
234
|
+
full_path = so.path
|
|
235
|
+
break
|
|
236
|
+
|
|
237
|
+
if not full_path or not Path(full_path).exists():
|
|
238
|
+
result.issues.append(
|
|
239
|
+
VerificationIssue(
|
|
240
|
+
severity="error",
|
|
241
|
+
category="binary_hash",
|
|
242
|
+
message=f"Binary file missing or moved",
|
|
243
|
+
package=current_pkg.name,
|
|
244
|
+
expected=rel_path,
|
|
245
|
+
)
|
|
246
|
+
)
|
|
247
|
+
result.hash_mismatches += 1
|
|
248
|
+
continue
|
|
249
|
+
|
|
250
|
+
# Compute current hash
|
|
251
|
+
current_hash = self._compute_sha256(full_path)
|
|
252
|
+
|
|
253
|
+
if current_hash != expected_hash:
|
|
254
|
+
result.issues.append(
|
|
255
|
+
VerificationIssue(
|
|
256
|
+
severity="error",
|
|
257
|
+
category="binary_hash",
|
|
258
|
+
message=f"Binary hash mismatch - file may be corrupted or tampered",
|
|
259
|
+
package=current_pkg.name,
|
|
260
|
+
expected=expected_hash[:16] + "...",
|
|
261
|
+
actual=current_hash[:16] + "...",
|
|
262
|
+
)
|
|
263
|
+
)
|
|
264
|
+
result.hash_mismatches += 1
|
|
265
|
+
|
|
266
|
+
def _run_compatibility_checks(self, result: VerificationResult) -> None:
|
|
267
|
+
"""Run predictive compatibility checks."""
|
|
268
|
+
for pkg in self.packages:
|
|
269
|
+
if not pkg.has_binaries:
|
|
270
|
+
continue
|
|
271
|
+
|
|
272
|
+
# Run predictive failure checks
|
|
273
|
+
failures = predict_import_failures(pkg, self.profile)
|
|
274
|
+
|
|
275
|
+
for failure in failures:
|
|
276
|
+
result.issues.append(
|
|
277
|
+
VerificationIssue(
|
|
278
|
+
severity="warning",
|
|
279
|
+
category="compatibility",
|
|
280
|
+
message=failure.error_message,
|
|
281
|
+
package=pkg.name,
|
|
282
|
+
)
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
@staticmethod
|
|
286
|
+
def _compute_sha256(file_path: str) -> str:
|
|
287
|
+
"""Compute SHA256 hash of a file."""
|
|
288
|
+
sha256 = hashlib.sha256()
|
|
289
|
+
with open(file_path, "rb") as f:
|
|
290
|
+
for chunk in iter(lambda: f.read(65536), b""):
|
|
291
|
+
sha256.update(chunk)
|
|
292
|
+
return sha256.hexdigest()
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def verify_snapshot(
|
|
296
|
+
lockfile: Lockfile,
|
|
297
|
+
profile: SystemProfile,
|
|
298
|
+
packages: List[PackageBinaryInfo],
|
|
299
|
+
check_hashes: bool = True,
|
|
300
|
+
run_compatibility_checks: bool = True
|
|
301
|
+
) -> VerificationResult:
|
|
302
|
+
"""Convenience function to verify environment against lockfile.
|
|
303
|
+
|
|
304
|
+
Args:
|
|
305
|
+
lockfile: Lockfile to verify against
|
|
306
|
+
profile: Current system profile
|
|
307
|
+
packages: Currently installed packages
|
|
308
|
+
check_hashes: Whether to verify binary hashes
|
|
309
|
+
run_compatibility_checks: Whether to run compatibility checks
|
|
310
|
+
|
|
311
|
+
Returns:
|
|
312
|
+
VerificationResult
|
|
313
|
+
"""
|
|
314
|
+
verifier = SnapshotVerifier(lockfile, profile, packages)
|
|
315
|
+
return verifier.verify(check_hashes, run_compatibility_checks)
|