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,19 @@
|
|
|
1
|
+
"""Advanced Health Scoring v2 — multi-dimensional weighted environment scoring.
|
|
2
|
+
|
|
3
|
+
Provides category-based scoring across four dimensions:
|
|
4
|
+
- Binary Stability (GLIBC, architecture, ELF integrity)
|
|
5
|
+
- GPU Compatibility (CUDA stack, cuDNN, compute capability)
|
|
6
|
+
- Dependency Health (missing libraries, symbol resolution)
|
|
7
|
+
- Platform Risk (embedded thermal, container issues, board compat)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from .engine import HealthScoreV2, ScoreBreakdown, CategoryScore, compute_health_score
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"HealthScoreV2",
|
|
16
|
+
"ScoreBreakdown",
|
|
17
|
+
"CategoryScore",
|
|
18
|
+
"compute_health_score",
|
|
19
|
+
]
|
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
"""Advanced Health Scoring v2 engine.
|
|
2
|
+
|
|
3
|
+
Multi-dimensional weighted scoring that evaluates environment health
|
|
4
|
+
across four categories rather than a simple linear penalty.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from typing import Dict, List, Optional, Set, Tuple
|
|
11
|
+
|
|
12
|
+
from pybinaryguard.models.enums import Severity
|
|
13
|
+
from pybinaryguard.models.finding import Finding
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# ---------------------------------------------------------------------------
|
|
17
|
+
# Category definitions
|
|
18
|
+
# ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
# Map rule_id prefixes/patterns to scoring categories
|
|
21
|
+
_CATEGORY_MAP: Dict[str, str] = {
|
|
22
|
+
# Binary Stability
|
|
23
|
+
"GLIBC_": "binary_stability",
|
|
24
|
+
"MUSL_": "binary_stability",
|
|
25
|
+
"MANYLINUX_": "binary_stability",
|
|
26
|
+
"ARCH_": "binary_stability",
|
|
27
|
+
"PYTHON_ABI_": "binary_stability",
|
|
28
|
+
"PYTHON_VERSION_": "binary_stability",
|
|
29
|
+
"ABI3_": "binary_stability",
|
|
30
|
+
"DEBUG_RELEASE_": "binary_stability",
|
|
31
|
+
"ELF_": "binary_stability",
|
|
32
|
+
"LIBSTDCXX_": "binary_stability",
|
|
33
|
+
# GPU Compatibility
|
|
34
|
+
"CUDA_": "gpu_compat",
|
|
35
|
+
"CUDNN_": "gpu_compat",
|
|
36
|
+
"COMPUTE_CAPABILITY_": "gpu_compat",
|
|
37
|
+
"PYTORCH_CUDA_": "gpu_compat",
|
|
38
|
+
"TENSORFLOW_COMPUTE_": "gpu_compat",
|
|
39
|
+
"TENSORFLOW_CUDA_": "gpu_compat",
|
|
40
|
+
"TENSORRT_": "gpu_compat",
|
|
41
|
+
"ONNX_RUNTIME_": "gpu_compat",
|
|
42
|
+
"PYTORCH_TORCHVISION_": "gpu_compat",
|
|
43
|
+
# Dependency Health
|
|
44
|
+
"MISSING_SHARED_LIB": "dependency_health",
|
|
45
|
+
"NUMPY_ABI_": "dependency_health",
|
|
46
|
+
"PREDICTED_IMPORT_": "dependency_health",
|
|
47
|
+
"UNRESOLVED_DEPENDENCY_": "dependency_health",
|
|
48
|
+
"PACKAGE_NOT_FOUND": "dependency_health",
|
|
49
|
+
# Platform Risk
|
|
50
|
+
"CONTAINER_": "platform_risk",
|
|
51
|
+
"JETPACK_": "platform_risk",
|
|
52
|
+
"JETSON_": "platform_risk",
|
|
53
|
+
"OPENCV_": "platform_risk",
|
|
54
|
+
"BOARD_": "platform_risk",
|
|
55
|
+
"KNOWN_BROKEN_": "platform_risk",
|
|
56
|
+
"AVX": "binary_stability",
|
|
57
|
+
"ILLEGAL_INSTRUCTION_": "binary_stability",
|
|
58
|
+
"CPU_": "binary_stability",
|
|
59
|
+
# Source build
|
|
60
|
+
"SOURCE_BUILD_": "binary_stability",
|
|
61
|
+
# Dependency health
|
|
62
|
+
"DEPENDENCY_": "dependency_health",
|
|
63
|
+
# Virtual environment
|
|
64
|
+
"VENV_": "platform_risk",
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
# Category display metadata
|
|
68
|
+
_CATEGORY_INFO: Dict[str, Tuple[str, str]] = {
|
|
69
|
+
"binary_stability": ("Binary Stability", "GLIBC, architecture, ABI, and ELF integrity"),
|
|
70
|
+
"gpu_compat": ("GPU Compatibility", "CUDA stack, cuDNN, compute capability, framework builds"),
|
|
71
|
+
"dependency_health": ("Dependency Health", "Shared library resolution and symbol availability"),
|
|
72
|
+
"platform_risk": ("Platform Risk", "Container, embedded board, and platform-specific issues"),
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
# Default weights — sum to 1.0
|
|
76
|
+
_DEFAULT_WEIGHTS: Dict[str, float] = {
|
|
77
|
+
"binary_stability": 0.35,
|
|
78
|
+
"gpu_compat": 0.30,
|
|
79
|
+
"dependency_health": 0.25,
|
|
80
|
+
"platform_risk": 0.10,
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
# Severity penalties (applied within each category, out of 100)
|
|
84
|
+
_SEVERITY_PENALTY: Dict[Severity, float] = {
|
|
85
|
+
Severity.CRITICAL: 30.0,
|
|
86
|
+
Severity.WARNING: 10.0,
|
|
87
|
+
Severity.INFO: 2.0,
|
|
88
|
+
Severity.PASSED: 0.0,
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
# ---------------------------------------------------------------------------
|
|
93
|
+
# Data classes
|
|
94
|
+
# ---------------------------------------------------------------------------
|
|
95
|
+
|
|
96
|
+
@dataclass
|
|
97
|
+
class CategoryScore:
|
|
98
|
+
"""Score for a single health category."""
|
|
99
|
+
|
|
100
|
+
name: str
|
|
101
|
+
display_name: str
|
|
102
|
+
description: str
|
|
103
|
+
score: float # 0-100
|
|
104
|
+
weight: float
|
|
105
|
+
weighted_score: float # score * weight
|
|
106
|
+
finding_count: int = 0
|
|
107
|
+
critical_count: int = 0
|
|
108
|
+
warning_count: int = 0
|
|
109
|
+
top_issues: List[str] = field(default_factory=list)
|
|
110
|
+
|
|
111
|
+
@property
|
|
112
|
+
def label(self) -> str:
|
|
113
|
+
if self.score >= 90:
|
|
114
|
+
return "Excellent"
|
|
115
|
+
elif self.score >= 70:
|
|
116
|
+
return "Good"
|
|
117
|
+
elif self.score >= 50:
|
|
118
|
+
return "Fair"
|
|
119
|
+
elif self.score >= 30:
|
|
120
|
+
return "Poor"
|
|
121
|
+
else:
|
|
122
|
+
return "Critical"
|
|
123
|
+
|
|
124
|
+
def as_dict(self) -> Dict[str, object]:
|
|
125
|
+
return {
|
|
126
|
+
"name": self.name,
|
|
127
|
+
"display_name": self.display_name,
|
|
128
|
+
"score": round(self.score, 1),
|
|
129
|
+
"weight": self.weight,
|
|
130
|
+
"weighted_score": round(self.weighted_score, 1),
|
|
131
|
+
"label": self.label,
|
|
132
|
+
"finding_count": self.finding_count,
|
|
133
|
+
"critical_count": self.critical_count,
|
|
134
|
+
"warning_count": self.warning_count,
|
|
135
|
+
"top_issues": self.top_issues,
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@dataclass
|
|
140
|
+
class ScoreBreakdown:
|
|
141
|
+
"""Complete health score breakdown across all categories."""
|
|
142
|
+
|
|
143
|
+
overall_score: float # 0-100
|
|
144
|
+
overall_label: str
|
|
145
|
+
categories: Dict[str, CategoryScore] = field(default_factory=dict)
|
|
146
|
+
total_findings: int = 0
|
|
147
|
+
total_critical: int = 0
|
|
148
|
+
total_warnings: int = 0
|
|
149
|
+
|
|
150
|
+
@property
|
|
151
|
+
def weakest_category(self) -> Optional[CategoryScore]:
|
|
152
|
+
if not self.categories:
|
|
153
|
+
return None
|
|
154
|
+
return min(self.categories.values(), key=lambda c: c.score)
|
|
155
|
+
|
|
156
|
+
@property
|
|
157
|
+
def strongest_category(self) -> Optional[CategoryScore]:
|
|
158
|
+
if not self.categories:
|
|
159
|
+
return None
|
|
160
|
+
return max(self.categories.values(), key=lambda c: c.score)
|
|
161
|
+
|
|
162
|
+
def as_dict(self) -> Dict[str, object]:
|
|
163
|
+
result: Dict[str, object] = {
|
|
164
|
+
"overall_score": round(self.overall_score, 1),
|
|
165
|
+
"overall_label": self.overall_label,
|
|
166
|
+
"total_findings": self.total_findings,
|
|
167
|
+
"total_critical": self.total_critical,
|
|
168
|
+
"total_warnings": self.total_warnings,
|
|
169
|
+
"categories": {
|
|
170
|
+
name: cat.as_dict() for name, cat in self.categories.items()
|
|
171
|
+
},
|
|
172
|
+
}
|
|
173
|
+
weakest = self.weakest_category
|
|
174
|
+
if weakest:
|
|
175
|
+
result["weakest_category"] = weakest.name
|
|
176
|
+
return result
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
# ---------------------------------------------------------------------------
|
|
180
|
+
# Scoring engine
|
|
181
|
+
# ---------------------------------------------------------------------------
|
|
182
|
+
|
|
183
|
+
class HealthScoreV2:
|
|
184
|
+
"""Multi-dimensional health scoring engine.
|
|
185
|
+
|
|
186
|
+
Evaluates findings across four categories with configurable weights,
|
|
187
|
+
producing a breakdown that shows exactly where environment health
|
|
188
|
+
is strong and where it needs attention.
|
|
189
|
+
"""
|
|
190
|
+
|
|
191
|
+
def __init__(
|
|
192
|
+
self,
|
|
193
|
+
weights: Optional[Dict[str, float]] = None,
|
|
194
|
+
has_gpu: bool = False,
|
|
195
|
+
is_embedded: bool = False,
|
|
196
|
+
) -> None:
|
|
197
|
+
self._weights = dict(weights or _DEFAULT_WEIGHTS)
|
|
198
|
+
self._has_gpu = has_gpu
|
|
199
|
+
self._is_embedded = is_embedded
|
|
200
|
+
|
|
201
|
+
# Adjust weights based on environment
|
|
202
|
+
self._adjust_weights()
|
|
203
|
+
|
|
204
|
+
def _adjust_weights(self) -> None:
|
|
205
|
+
"""Redistribute weights based on environment context.
|
|
206
|
+
|
|
207
|
+
If no GPU is present, GPU weight is redistributed.
|
|
208
|
+
If not on embedded hardware, platform risk weight is redistributed.
|
|
209
|
+
"""
|
|
210
|
+
redistribute = 0.0
|
|
211
|
+
|
|
212
|
+
if not self._has_gpu and self._weights.get("gpu_compat", 0) > 0:
|
|
213
|
+
redistribute += self._weights["gpu_compat"]
|
|
214
|
+
self._weights["gpu_compat"] = 0.0
|
|
215
|
+
|
|
216
|
+
if not self._is_embedded and self._weights.get("platform_risk", 0) > 0:
|
|
217
|
+
extra = self._weights["platform_risk"] * 0.5
|
|
218
|
+
redistribute += extra
|
|
219
|
+
self._weights["platform_risk"] -= extra
|
|
220
|
+
|
|
221
|
+
if redistribute > 0:
|
|
222
|
+
# Distribute to non-zero categories
|
|
223
|
+
active = [k for k, v in self._weights.items() if v > 0]
|
|
224
|
+
if active:
|
|
225
|
+
per_category = redistribute / len(active)
|
|
226
|
+
for key in active:
|
|
227
|
+
self._weights[key] += per_category
|
|
228
|
+
|
|
229
|
+
# Normalize to sum to 1.0
|
|
230
|
+
total = sum(self._weights.values())
|
|
231
|
+
if total > 0:
|
|
232
|
+
for key in self._weights:
|
|
233
|
+
self._weights[key] /= total
|
|
234
|
+
|
|
235
|
+
def score(self, findings: List[Finding]) -> ScoreBreakdown:
|
|
236
|
+
"""Compute health score breakdown from findings.
|
|
237
|
+
|
|
238
|
+
Parameters
|
|
239
|
+
----------
|
|
240
|
+
findings:
|
|
241
|
+
All findings from the scan.
|
|
242
|
+
|
|
243
|
+
Returns
|
|
244
|
+
-------
|
|
245
|
+
ScoreBreakdown
|
|
246
|
+
Detailed breakdown with per-category scores and overall score.
|
|
247
|
+
"""
|
|
248
|
+
# Categorize findings
|
|
249
|
+
categorized: Dict[str, List[Finding]] = {
|
|
250
|
+
"binary_stability": [],
|
|
251
|
+
"gpu_compat": [],
|
|
252
|
+
"dependency_health": [],
|
|
253
|
+
"platform_risk": [],
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
uncategorized: List[Finding] = []
|
|
257
|
+
|
|
258
|
+
for finding in findings:
|
|
259
|
+
category = self._classify_finding(finding)
|
|
260
|
+
if category and category in categorized:
|
|
261
|
+
categorized[category].append(finding)
|
|
262
|
+
else:
|
|
263
|
+
uncategorized.append(finding)
|
|
264
|
+
|
|
265
|
+
# Assign uncategorized to binary_stability (safest default)
|
|
266
|
+
categorized["binary_stability"].extend(uncategorized)
|
|
267
|
+
|
|
268
|
+
# Score each category
|
|
269
|
+
categories: Dict[str, CategoryScore] = {}
|
|
270
|
+
for cat_name, cat_findings in categorized.items():
|
|
271
|
+
display_name, description = _CATEGORY_INFO.get(
|
|
272
|
+
cat_name, (cat_name, "")
|
|
273
|
+
)
|
|
274
|
+
weight = self._weights.get(cat_name, 0.0)
|
|
275
|
+
cat_score = self._score_category(
|
|
276
|
+
cat_name, display_name, description, cat_findings, weight
|
|
277
|
+
)
|
|
278
|
+
categories[cat_name] = cat_score
|
|
279
|
+
|
|
280
|
+
# Compute overall weighted score
|
|
281
|
+
overall = sum(cat.weighted_score for cat in categories.values())
|
|
282
|
+
overall = max(0.0, min(100.0, overall))
|
|
283
|
+
|
|
284
|
+
# Determine overall label
|
|
285
|
+
overall_label = self._score_label(overall)
|
|
286
|
+
|
|
287
|
+
total_findings = sum(
|
|
288
|
+
1 for f in findings if f.severity != Severity.PASSED
|
|
289
|
+
)
|
|
290
|
+
total_critical = sum(
|
|
291
|
+
1 for f in findings if f.severity == Severity.CRITICAL
|
|
292
|
+
)
|
|
293
|
+
total_warnings = sum(
|
|
294
|
+
1 for f in findings if f.severity == Severity.WARNING
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
return ScoreBreakdown(
|
|
298
|
+
overall_score=overall,
|
|
299
|
+
overall_label=overall_label,
|
|
300
|
+
categories=categories,
|
|
301
|
+
total_findings=total_findings,
|
|
302
|
+
total_critical=total_critical,
|
|
303
|
+
total_warnings=total_warnings,
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
def _classify_finding(self, finding: Finding) -> Optional[str]:
|
|
307
|
+
"""Classify a finding into a scoring category."""
|
|
308
|
+
rule_id = finding.rule_id
|
|
309
|
+
for prefix, category in _CATEGORY_MAP.items():
|
|
310
|
+
if rule_id.startswith(prefix):
|
|
311
|
+
return category
|
|
312
|
+
return None
|
|
313
|
+
|
|
314
|
+
@staticmethod
|
|
315
|
+
def _score_category(
|
|
316
|
+
name: str,
|
|
317
|
+
display_name: str,
|
|
318
|
+
description: str,
|
|
319
|
+
findings: List[Finding],
|
|
320
|
+
weight: float,
|
|
321
|
+
) -> CategoryScore:
|
|
322
|
+
"""Score a single category from its findings."""
|
|
323
|
+
# Start at 100, subtract penalties
|
|
324
|
+
score = 100.0
|
|
325
|
+
|
|
326
|
+
critical_count = 0
|
|
327
|
+
warning_count = 0
|
|
328
|
+
top_issues: List[str] = []
|
|
329
|
+
|
|
330
|
+
for finding in findings:
|
|
331
|
+
if finding.severity == Severity.PASSED:
|
|
332
|
+
continue
|
|
333
|
+
|
|
334
|
+
penalty = _SEVERITY_PENALTY.get(finding.severity, 0.0)
|
|
335
|
+
|
|
336
|
+
# Scale penalty by confidence
|
|
337
|
+
penalty *= finding.confidence
|
|
338
|
+
|
|
339
|
+
score -= penalty
|
|
340
|
+
|
|
341
|
+
if finding.severity == Severity.CRITICAL:
|
|
342
|
+
critical_count += 1
|
|
343
|
+
if len(top_issues) < 3:
|
|
344
|
+
top_issues.append(finding.title)
|
|
345
|
+
elif finding.severity == Severity.WARNING:
|
|
346
|
+
warning_count += 1
|
|
347
|
+
if len(top_issues) < 3 and critical_count == 0:
|
|
348
|
+
top_issues.append(finding.title)
|
|
349
|
+
|
|
350
|
+
score = max(0.0, min(100.0, score))
|
|
351
|
+
|
|
352
|
+
finding_count = sum(
|
|
353
|
+
1 for f in findings if f.severity != Severity.PASSED
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
return CategoryScore(
|
|
357
|
+
name=name,
|
|
358
|
+
display_name=display_name,
|
|
359
|
+
description=description,
|
|
360
|
+
score=score,
|
|
361
|
+
weight=weight,
|
|
362
|
+
weighted_score=score * weight,
|
|
363
|
+
finding_count=finding_count,
|
|
364
|
+
critical_count=critical_count,
|
|
365
|
+
warning_count=warning_count,
|
|
366
|
+
top_issues=top_issues,
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
@staticmethod
|
|
370
|
+
def _score_label(score: float) -> str:
|
|
371
|
+
if score >= 90:
|
|
372
|
+
return "Excellent"
|
|
373
|
+
elif score >= 70:
|
|
374
|
+
return "Good"
|
|
375
|
+
elif score >= 50:
|
|
376
|
+
return "Needs Attention"
|
|
377
|
+
elif score >= 30:
|
|
378
|
+
return "Poor"
|
|
379
|
+
else:
|
|
380
|
+
return "Critical"
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
# ---------------------------------------------------------------------------
|
|
384
|
+
# Convenience function
|
|
385
|
+
# ---------------------------------------------------------------------------
|
|
386
|
+
|
|
387
|
+
def compute_health_score(
|
|
388
|
+
findings: List[Finding],
|
|
389
|
+
has_gpu: bool = False,
|
|
390
|
+
is_embedded: bool = False,
|
|
391
|
+
weights: Optional[Dict[str, float]] = None,
|
|
392
|
+
) -> ScoreBreakdown:
|
|
393
|
+
"""Compute the v2 health score breakdown.
|
|
394
|
+
|
|
395
|
+
Parameters
|
|
396
|
+
----------
|
|
397
|
+
findings:
|
|
398
|
+
All findings from the scan.
|
|
399
|
+
has_gpu:
|
|
400
|
+
Whether the system has a GPU (affects weight distribution).
|
|
401
|
+
is_embedded:
|
|
402
|
+
Whether running on an embedded board (affects weight distribution).
|
|
403
|
+
weights:
|
|
404
|
+
Optional custom category weights (must sum to 1.0).
|
|
405
|
+
|
|
406
|
+
Returns
|
|
407
|
+
-------
|
|
408
|
+
ScoreBreakdown
|
|
409
|
+
Complete health score breakdown.
|
|
410
|
+
"""
|
|
411
|
+
engine = HealthScoreV2(
|
|
412
|
+
weights=weights,
|
|
413
|
+
has_gpu=has_gpu,
|
|
414
|
+
is_embedded=is_embedded,
|
|
415
|
+
)
|
|
416
|
+
return engine.score(findings)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Environment snapshot and verification system.
|
|
2
|
+
|
|
3
|
+
Captures complete binary compatibility state including hashes, GPU stack,
|
|
4
|
+
and system libraries - information that pip freeze/conda export don't provide.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from .generator import SnapshotGenerator, create_snapshot
|
|
10
|
+
from .verifier import SnapshotVerifier, verify_snapshot
|
|
11
|
+
from .lockfile import Lockfile, load_lockfile
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"SnapshotGenerator",
|
|
15
|
+
"create_snapshot",
|
|
16
|
+
"SnapshotVerifier",
|
|
17
|
+
"verify_snapshot",
|
|
18
|
+
"Lockfile",
|
|
19
|
+
"load_lockfile",
|
|
20
|
+
]
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"""Environment snapshot generator.
|
|
2
|
+
|
|
3
|
+
Creates lockfiles with binary hashes, GPU stack info, and system configuration.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import hashlib
|
|
9
|
+
import platform
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import List, Optional
|
|
13
|
+
|
|
14
|
+
from pybinaryguard.models.system import SystemProfile
|
|
15
|
+
from pybinaryguard.models.package import PackageBinaryInfo
|
|
16
|
+
from pybinaryguard.snapshot.lockfile import Lockfile, PackageSnapshot, SystemSnapshot
|
|
17
|
+
from pybinaryguard.profiles import match_board_profile
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class SnapshotGenerator:
|
|
21
|
+
"""Generates environment snapshots with binary fingerprints."""
|
|
22
|
+
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
profile: SystemProfile,
|
|
26
|
+
packages: List[PackageBinaryInfo]
|
|
27
|
+
):
|
|
28
|
+
"""Initialize snapshot generator.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
profile: System profile
|
|
32
|
+
packages: List of installed packages
|
|
33
|
+
"""
|
|
34
|
+
self.profile = profile
|
|
35
|
+
self.packages = packages
|
|
36
|
+
|
|
37
|
+
def generate(self, include_hashes: bool = True) -> Lockfile:
|
|
38
|
+
"""Generate a complete environment snapshot.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
include_hashes: Whether to compute binary hashes (slower but more secure)
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
Lockfile with complete environment state
|
|
45
|
+
"""
|
|
46
|
+
# Build system snapshot
|
|
47
|
+
system_snapshot = self._build_system_snapshot()
|
|
48
|
+
|
|
49
|
+
# Build package snapshots
|
|
50
|
+
package_snapshots = []
|
|
51
|
+
for pkg in self.packages:
|
|
52
|
+
pkg_snapshot = self._build_package_snapshot(pkg, include_hashes)
|
|
53
|
+
package_snapshots.append(pkg_snapshot)
|
|
54
|
+
|
|
55
|
+
# Sort packages by name for consistency
|
|
56
|
+
package_snapshots.sort(key=lambda p: p.name.lower())
|
|
57
|
+
|
|
58
|
+
# Create lockfile
|
|
59
|
+
lockfile = Lockfile(
|
|
60
|
+
version="1.0",
|
|
61
|
+
timestamp=datetime.utcnow().isoformat() + "Z",
|
|
62
|
+
system=system_snapshot,
|
|
63
|
+
packages=package_snapshots,
|
|
64
|
+
metadata={
|
|
65
|
+
"generated_by": "pybinaryguard",
|
|
66
|
+
"total_packages": len(package_snapshots),
|
|
67
|
+
"packages_with_binaries": sum(
|
|
68
|
+
1 for p in package_snapshots if p.binary_hashes
|
|
69
|
+
),
|
|
70
|
+
},
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
return lockfile
|
|
74
|
+
|
|
75
|
+
def _build_system_snapshot(self) -> SystemSnapshot:
|
|
76
|
+
"""Build system configuration snapshot."""
|
|
77
|
+
# Detect board profile
|
|
78
|
+
board_profile = match_board_profile(self.profile)
|
|
79
|
+
detected_board = board_profile.display_name if board_profile else None
|
|
80
|
+
|
|
81
|
+
# Get CPU flags
|
|
82
|
+
cpu_flags = []
|
|
83
|
+
if self.profile.cpu_flags:
|
|
84
|
+
# Extract key flags
|
|
85
|
+
flags_str = self.profile.cpu_flags.lower()
|
|
86
|
+
for flag in ["avx", "avx2", "avx512", "sse4_2", "neon", "fma"]:
|
|
87
|
+
if flag in flags_str:
|
|
88
|
+
cpu_flags.append(flag)
|
|
89
|
+
|
|
90
|
+
# Build Python version string
|
|
91
|
+
py_ver = self.profile.python_version
|
|
92
|
+
python_version = f"{py_ver[0]}.{py_ver[1]}.{py_ver[2]}" if py_ver else ""
|
|
93
|
+
|
|
94
|
+
return SystemSnapshot(
|
|
95
|
+
python_version=python_version,
|
|
96
|
+
platform=platform.system(),
|
|
97
|
+
architecture=self.profile.architecture.value if self.profile.architecture else "",
|
|
98
|
+
glibc_version=self.profile.glibc_version,
|
|
99
|
+
cuda_runtime=self.profile.cuda_runtime_version,
|
|
100
|
+
cuda_driver=self.profile.cuda_driver_version,
|
|
101
|
+
cuda_compute_capability=self.profile.cuda_compute_capability,
|
|
102
|
+
tensorrt_version=self.profile.tensorrt_version,
|
|
103
|
+
cudnn_version=self.profile.cudnn_version,
|
|
104
|
+
detected_board=detected_board,
|
|
105
|
+
cpu_flags=cpu_flags,
|
|
106
|
+
container_runtime=self.profile.container_runtime.value if self.profile.container_runtime else None,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
def _build_package_snapshot(
|
|
110
|
+
self,
|
|
111
|
+
package: PackageBinaryInfo,
|
|
112
|
+
include_hashes: bool
|
|
113
|
+
) -> PackageSnapshot:
|
|
114
|
+
"""Build snapshot for a single package."""
|
|
115
|
+
binary_hashes = {}
|
|
116
|
+
|
|
117
|
+
if include_hashes and package.has_binaries:
|
|
118
|
+
for so in package.shared_objects:
|
|
119
|
+
if so.path and Path(so.path).exists():
|
|
120
|
+
# Compute SHA256 hash
|
|
121
|
+
hash_value = self._compute_sha256(so.path)
|
|
122
|
+
# Use relative path as key
|
|
123
|
+
rel_path = so.path.replace(package.install_path, "").lstrip("/")
|
|
124
|
+
binary_hashes[rel_path] = hash_value
|
|
125
|
+
|
|
126
|
+
# Get wheel tags if available
|
|
127
|
+
wheel_tags = []
|
|
128
|
+
if hasattr(package, 'wheel_tags') and package.wheel_tags:
|
|
129
|
+
for tag in package.wheel_tags:
|
|
130
|
+
wheel_tags.append(f"{tag.python_tag}-{tag.abi_tag}-{tag.platform_tag}")
|
|
131
|
+
|
|
132
|
+
return PackageSnapshot(
|
|
133
|
+
name=package.name,
|
|
134
|
+
version=package.version or "unknown",
|
|
135
|
+
cuda_version=package.cuda_version,
|
|
136
|
+
binary_hashes=binary_hashes,
|
|
137
|
+
manylinux_tag=package.manylinux_tag,
|
|
138
|
+
wheel_tags=wheel_tags,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
@staticmethod
|
|
142
|
+
def _compute_sha256(file_path: str) -> str:
|
|
143
|
+
"""Compute SHA256 hash of a file."""
|
|
144
|
+
sha256 = hashlib.sha256()
|
|
145
|
+
with open(file_path, "rb") as f:
|
|
146
|
+
# Read in chunks to handle large files
|
|
147
|
+
for chunk in iter(lambda: f.read(65536), b""):
|
|
148
|
+
sha256.update(chunk)
|
|
149
|
+
return sha256.hexdigest()
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def create_snapshot(
|
|
153
|
+
profile: SystemProfile,
|
|
154
|
+
packages: List[PackageBinaryInfo],
|
|
155
|
+
include_hashes: bool = True
|
|
156
|
+
) -> Lockfile:
|
|
157
|
+
"""Convenience function to create an environment snapshot.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
profile: System profile
|
|
161
|
+
packages: List of installed packages
|
|
162
|
+
include_hashes: Whether to compute binary hashes
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
Generated lockfile
|
|
166
|
+
"""
|
|
167
|
+
generator = SnapshotGenerator(profile, packages)
|
|
168
|
+
return generator.generate(include_hashes)
|