pybinaryguard 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. pybinaryguard/__init__.py +78 -0
  2. pybinaryguard/__main__.py +7 -0
  3. pybinaryguard/_compat/__init__.py +0 -0
  4. pybinaryguard/agent/__init__.py +63 -0
  5. pybinaryguard/agent/guard.py +232 -0
  6. pybinaryguard/agent/recommender.py +283 -0
  7. pybinaryguard/agent/schema.py +200 -0
  8. pybinaryguard/agent/simulator.py +430 -0
  9. pybinaryguard/agent/tool_interface.py +474 -0
  10. pybinaryguard/analyzers/__init__.py +209 -0
  11. pybinaryguard/analyzers/base.py +40 -0
  12. pybinaryguard/analyzers/dependency_analyzer.py +336 -0
  13. pybinaryguard/analyzers/elf_analyzer.py +754 -0
  14. pybinaryguard/analyzers/symbol_analyzer.py +280 -0
  15. pybinaryguard/analyzers/wheel_analyzer.py +308 -0
  16. pybinaryguard/cli/__init__.py +5 -0
  17. pybinaryguard/cli/commands.py +414 -0
  18. pybinaryguard/cli/formatters.py +720 -0
  19. pybinaryguard/cli/main.py +250 -0
  20. pybinaryguard/diagnostics/__init__.py +13 -0
  21. pybinaryguard/diagnostics/explainer.py +356 -0
  22. pybinaryguard/diagnostics/findings.py +146 -0
  23. pybinaryguard/diagnostics/suggestions.py +508 -0
  24. pybinaryguard/frameworks/__init__.py +20 -0
  25. pybinaryguard/frameworks/onnxruntime.py +214 -0
  26. pybinaryguard/frameworks/pytorch.py +223 -0
  27. pybinaryguard/frameworks/tensorflow.py +266 -0
  28. pybinaryguard/frameworks/tensorrt.py +189 -0
  29. pybinaryguard/models/__init__.py +19 -0
  30. pybinaryguard/models/enums.py +102 -0
  31. pybinaryguard/models/finding.py +109 -0
  32. pybinaryguard/models/package.py +121 -0
  33. pybinaryguard/models/system.py +118 -0
  34. pybinaryguard/plugins/__init__.py +19 -0
  35. pybinaryguard/plugins/contrib/__init__.py +7 -0
  36. pybinaryguard/plugins/contrib/gstreamer.py +109 -0
  37. pybinaryguard/plugins/contrib/jetson.py +385 -0
  38. pybinaryguard/plugins/contrib/opencv.py +306 -0
  39. pybinaryguard/plugins/contrib/tensorrt.py +426 -0
  40. pybinaryguard/plugins/hooks.py +273 -0
  41. pybinaryguard/plugins/loader.py +190 -0
  42. pybinaryguard/predictor/__init__.py +23 -0
  43. pybinaryguard/predictor/dependency_graph.py +226 -0
  44. pybinaryguard/predictor/linker_simulator.py +161 -0
  45. pybinaryguard/predictor/predictor.py +144 -0
  46. pybinaryguard/predictor/resolver.py +282 -0
  47. pybinaryguard/probes/__init__.py +73 -0
  48. pybinaryguard/probes/base.py +45 -0
  49. pybinaryguard/probes/board_probe.py +248 -0
  50. pybinaryguard/probes/cpu_probe.py +176 -0
  51. pybinaryguard/probes/glibc_probe.py +215 -0
  52. pybinaryguard/probes/gpu_probe.py +430 -0
  53. pybinaryguard/probes/library_probe.py +132 -0
  54. pybinaryguard/probes/os_probe.py +227 -0
  55. pybinaryguard/probes/python_probe.py +135 -0
  56. pybinaryguard/probes/toolchain_probe.py +89 -0
  57. pybinaryguard/probes/venv_probe.py +111 -0
  58. pybinaryguard/profiles/__init__.py +12 -0
  59. pybinaryguard/profiles/engine.py +343 -0
  60. pybinaryguard/rules/__init__.py +24 -0
  61. pybinaryguard/rules/base.py +57 -0
  62. pybinaryguard/rules/builtin/__init__.py +136 -0
  63. pybinaryguard/rules/builtin/arch_rules.py +86 -0
  64. pybinaryguard/rules/builtin/board_profile_rules.py +282 -0
  65. pybinaryguard/rules/builtin/container_rules.py +170 -0
  66. pybinaryguard/rules/builtin/cpu_rules.py +204 -0
  67. pybinaryguard/rules/builtin/cuda_rules.py +760 -0
  68. pybinaryguard/rules/builtin/dependency_rules.py +278 -0
  69. pybinaryguard/rules/builtin/framework_rules.py +252 -0
  70. pybinaryguard/rules/builtin/glibc_rules.py +320 -0
  71. pybinaryguard/rules/builtin/numpy_rules.py +110 -0
  72. pybinaryguard/rules/builtin/predictive_rules.py +165 -0
  73. pybinaryguard/rules/builtin/python_abi_rules.py +259 -0
  74. pybinaryguard/rules/builtin/source_build_rules.py +150 -0
  75. pybinaryguard/rules/builtin/venv_rules.py +159 -0
  76. pybinaryguard/rules/engine.py +123 -0
  77. pybinaryguard/scanner.py +904 -0
  78. pybinaryguard/scoring/__init__.py +19 -0
  79. pybinaryguard/scoring/engine.py +416 -0
  80. pybinaryguard/snapshot/__init__.py +20 -0
  81. pybinaryguard/snapshot/generator.py +168 -0
  82. pybinaryguard/snapshot/lockfile.py +152 -0
  83. pybinaryguard/snapshot/verifier.py +315 -0
  84. pybinaryguard/validators/__init__.py +7 -0
  85. pybinaryguard/validators/import_validator.py +231 -0
  86. pybinaryguard-1.0.0.dist-info/METADATA +876 -0
  87. pybinaryguard-1.0.0.dist-info/RECORD +91 -0
  88. pybinaryguard-1.0.0.dist-info/WHEEL +5 -0
  89. pybinaryguard-1.0.0.dist-info/entry_points.txt +8 -0
  90. pybinaryguard-1.0.0.dist-info/licenses/LICENSE +21 -0
  91. pybinaryguard-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,414 @@
1
+ """CLI command handlers for PyBinaryGuard."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import sys
7
+ from typing import Any, Dict, List
8
+
9
+ from pybinaryguard.cli.formatters import get_formatter
10
+ from pybinaryguard.models.enums import ScanMode, Severity
11
+
12
+
13
+ # Map CLI severity names to Severity enum values
14
+ _SEVERITY_MAP = {
15
+ "critical": Severity.CRITICAL,
16
+ "warning": Severity.WARNING,
17
+ "info": Severity.INFO,
18
+ "all": Severity.INFO,
19
+ }
20
+
21
+
22
+ def dispatch(args: argparse.Namespace) -> int:
23
+ """Dispatch to the appropriate command handler.
24
+
25
+ Returns the exit code.
26
+ """
27
+ handlers = {
28
+ "scan": cmd_scan,
29
+ "check": cmd_check,
30
+ "profile": cmd_profile,
31
+ "doctor": cmd_doctor,
32
+ "inspect": cmd_inspect,
33
+ "snapshot": cmd_snapshot,
34
+ "verify": cmd_verify,
35
+ "export-tool-schema": cmd_export_tool_schema,
36
+ "simulate": cmd_simulate,
37
+ "validate": cmd_validate,
38
+ }
39
+ handler = handlers.get(args.command)
40
+ if handler is None:
41
+ print(f"Unknown command: {args.command}", file=sys.stderr)
42
+ return 3
43
+ return handler(args)
44
+
45
+
46
+ def _resolve_scan_mode(args: argparse.Namespace) -> ScanMode:
47
+ """Determine scan mode from CLI flags."""
48
+ if getattr(args, "fast", False):
49
+ return ScanMode.FAST
50
+ if getattr(args, "deep", False):
51
+ return ScanMode.DEEP
52
+ return ScanMode.STANDARD
53
+
54
+
55
+ def cmd_scan(args: argparse.Namespace) -> int:
56
+ """Full environment scan."""
57
+ from pybinaryguard.scanner import Scanner
58
+
59
+ formatter = get_formatter(args.format, no_color=args.no_color)
60
+ severity_threshold = _SEVERITY_MAP.get(args.severity, Severity.INFO)
61
+ ignored_rules = set(args.ignore) if args.ignore else set()
62
+ scan_mode = _resolve_scan_mode(args)
63
+
64
+ scanner = Scanner(
65
+ severity_threshold=severity_threshold,
66
+ ignored_rules=ignored_rules,
67
+ timeout=args.timeout,
68
+ scan_mode=scan_mode,
69
+ )
70
+
71
+ report = scanner.run()
72
+ profile = scanner.get_profile()
73
+
74
+ output = formatter.format_scan(report, profile, verbose=args.verbose)
75
+ print(output)
76
+
77
+ return _exit_code(report.critical_count, report.warning_count)
78
+
79
+
80
+ def cmd_check(args: argparse.Namespace) -> int:
81
+ """Check a specific installed package."""
82
+ from pybinaryguard.scanner import Scanner
83
+
84
+ formatter = get_formatter(args.format, no_color=args.no_color)
85
+ ignored_rules = set(args.ignore) if args.ignore else set()
86
+ scan_mode = _resolve_scan_mode(args)
87
+
88
+ scanner = Scanner(
89
+ packages=[args.package],
90
+ ignored_rules=ignored_rules,
91
+ timeout=args.timeout,
92
+ scan_mode=scan_mode,
93
+ )
94
+
95
+ findings = scanner.check_package(args.package)
96
+ profile = scanner.get_profile()
97
+
98
+ output = formatter.format_check(findings, args.package, profile, verbose=args.verbose)
99
+ print(output)
100
+
101
+ critical = sum(1 for f in findings if f.severity == Severity.CRITICAL)
102
+ warning = sum(1 for f in findings if f.severity == Severity.WARNING)
103
+ return _exit_code(critical, warning)
104
+
105
+
106
+ def cmd_profile(args: argparse.Namespace) -> int:
107
+ """Show system profile."""
108
+ from pybinaryguard.scanner import Scanner
109
+
110
+ formatter = get_formatter(args.format, no_color=args.no_color)
111
+
112
+ scanner = Scanner(timeout=args.timeout)
113
+ profile = scanner.get_profile()
114
+
115
+ output = formatter.format_profile(profile)
116
+ print(output)
117
+ return 0
118
+
119
+
120
+ def cmd_doctor(args: argparse.Namespace) -> int:
121
+ """Interactive troubleshooting wizard."""
122
+ from pybinaryguard.diagnostics.explainer import diagnose_error
123
+ from pybinaryguard.scanner import Scanner
124
+
125
+ formatter = get_formatter(args.format, no_color=args.no_color)
126
+
127
+ diagnosis: Dict[str, Any] = {}
128
+
129
+ # If an error message is provided, diagnose it
130
+ if args.error:
131
+ diagnosis["error"] = args.error
132
+ result = diagnose_error(args.error)
133
+ if result:
134
+ diagnosis["suggestions"] = [
135
+ result.get("explanation", ""),
136
+ result.get("fix_hint", ""),
137
+ ]
138
+ else:
139
+ diagnosis["suggestions"] = [
140
+ "Could not match this error to a known pattern.",
141
+ "Try: pybinaryguard scan --verbose",
142
+ ]
143
+
144
+ # If a package is specified, check it
145
+ if args.package:
146
+ diagnosis["package"] = args.package
147
+ scanner = Scanner(
148
+ packages=[args.package],
149
+ timeout=args.timeout,
150
+ )
151
+ findings = scanner.check_package(args.package)
152
+ if findings:
153
+ diagnosis["findings"] = findings
154
+
155
+ # If neither error nor package, prompt for info
156
+ if not args.error and not args.package:
157
+ diagnosis["suggestions"] = [
158
+ "Usage: pybinaryguard doctor --error 'your error message'",
159
+ " pybinaryguard doctor --package torch",
160
+ "Or run: pybinaryguard scan (for a full environment check)",
161
+ ]
162
+
163
+ output = formatter.format_doctor(diagnosis)
164
+ print(output)
165
+
166
+ # Exit code based on findings
167
+ if "findings" in diagnosis:
168
+ critical = sum(1 for f in diagnosis["findings"] if f.severity == Severity.CRITICAL)
169
+ warning = sum(1 for f in diagnosis["findings"] if f.severity == Severity.WARNING)
170
+ return _exit_code(critical, warning)
171
+ return 0
172
+
173
+
174
+ def cmd_inspect(args: argparse.Namespace) -> int:
175
+ """Inspect a .whl or .so file."""
176
+ from pybinaryguard.scanner import Scanner
177
+
178
+ formatter = get_formatter(args.format, no_color=args.no_color)
179
+
180
+ try:
181
+ scanner = Scanner(timeout=args.timeout)
182
+ findings = scanner.inspect_file(args.file)
183
+ profile = scanner.get_profile()
184
+ except FileNotFoundError as exc:
185
+ print(f"Error: {exc}", file=sys.stderr)
186
+ return 3
187
+ except ValueError as exc:
188
+ print(f"Error: {exc}", file=sys.stderr)
189
+ return 3
190
+
191
+ output = formatter.format_check(findings, args.file, profile, verbose=args.verbose)
192
+ print(output)
193
+
194
+ critical = sum(1 for f in findings if f.severity == Severity.CRITICAL)
195
+ warning = sum(1 for f in findings if f.severity == Severity.WARNING)
196
+ return _exit_code(critical, warning)
197
+
198
+
199
+ def cmd_snapshot(args: argparse.Namespace) -> int:
200
+ """Create environment snapshot."""
201
+ from pybinaryguard.scanner import Scanner
202
+ from pybinaryguard.snapshot import create_snapshot
203
+
204
+ # Get system profile and packages
205
+ scanner = Scanner(timeout=args.timeout)
206
+ profile = scanner.get_profile()
207
+
208
+ # Discover packages
209
+ report = scanner.run() # Run full scan to get packages
210
+
211
+ # Get packages from scanner (this is a simplified approach)
212
+ # In a full implementation, we'd need to expose packages from Scanner
213
+ packages = [] # Would be populated from scanner
214
+
215
+ # Create snapshot
216
+ lockfile = create_snapshot(
217
+ profile=profile,
218
+ packages=packages,
219
+ include_hashes=not args.no_hashes
220
+ )
221
+
222
+ # Output lockfile
223
+ if args.output:
224
+ lockfile.save(args.output)
225
+ print(f"Snapshot saved to {args.output}")
226
+ else:
227
+ print(lockfile.to_json())
228
+
229
+ return 0
230
+
231
+
232
+ def cmd_verify(args: argparse.Namespace) -> int:
233
+ """Verify environment against snapshot."""
234
+ from pybinaryguard.scanner import Scanner
235
+ from pybinaryguard.snapshot import load_lockfile, verify_snapshot
236
+
237
+ # Load lockfile
238
+ try:
239
+ lockfile = load_lockfile(args.lockfile)
240
+ except FileNotFoundError:
241
+ print(f"Error: Lockfile not found: {args.lockfile}", file=sys.stderr)
242
+ return 3
243
+ except Exception as exc:
244
+ print(f"Error loading lockfile: {exc}", file=sys.stderr)
245
+ return 3
246
+
247
+ # Get current system state
248
+ scanner = Scanner(timeout=args.timeout)
249
+ profile = scanner.get_profile()
250
+
251
+ # Run scan to get packages
252
+ report = scanner.run()
253
+
254
+ # Verify
255
+ packages = [] # Would be populated from scanner
256
+ result = verify_snapshot(
257
+ lockfile=lockfile,
258
+ profile=profile,
259
+ packages=packages,
260
+ check_hashes=not args.no_hashes,
261
+ run_compatibility_checks=not args.no_compat_checks
262
+ )
263
+
264
+ # Print results
265
+ if result.success:
266
+ print("✅ Verification PASSED")
267
+ print(f" Packages verified: {result.packages_verified}")
268
+ print(f" Binaries verified: {result.binaries_verified}")
269
+ else:
270
+ print("❌ Verification FAILED")
271
+ print(f" Errors: {result.error_count}")
272
+ print(f" Warnings: {result.warning_count}")
273
+ print(f" Hash mismatches: {result.hash_mismatches}")
274
+
275
+ # Print issues
276
+ for issue in result.issues[:10]: # Limit to first 10
277
+ severity_symbol = {"error": "❌", "warning": "⚠️", "info": "ℹ️"}.get(issue.severity, "•")
278
+ print(f"\n{severity_symbol} {issue.category.upper()}: {issue.message}")
279
+ if issue.package:
280
+ print(f" Package: {issue.package}")
281
+ if issue.expected:
282
+ print(f" Expected: {issue.expected}")
283
+ if issue.actual:
284
+ print(f" Actual: {issue.actual}")
285
+
286
+ if len(result.issues) > 10:
287
+ print(f"\n... and {len(result.issues) - 10} more issues")
288
+
289
+ return 0 if result.success else 1
290
+
291
+
292
+ def cmd_export_tool_schema(args: argparse.Namespace) -> int:
293
+ """Export tool schema for agent framework registration."""
294
+ from pybinaryguard.agent.schema import export_tool_schema
295
+
296
+ fmt = getattr(args, "schema_format", "openai")
297
+ print(export_tool_schema(format=fmt))
298
+ return 0
299
+
300
+
301
+ def cmd_simulate(args: argparse.Namespace) -> int:
302
+ """Simulate package installation compatibility."""
303
+ from pybinaryguard.agent import simulate_install
304
+
305
+ result = simulate_install(args.package_spec)
306
+
307
+ fmt = getattr(args, "format", "table")
308
+ if fmt == "json":
309
+ print(result.to_json())
310
+ else:
311
+ # Human-readable output
312
+ status = "COMPATIBLE" if result.predicted_compatible else "INCOMPATIBLE"
313
+ icon = "+" if result.predicted_compatible else "X"
314
+ print(f"[{icon}] {result.package_spec}: {status}")
315
+ print(f" Confidence: {result.confidence:.0%}")
316
+ print(f" Risk Level: {result.risk_level}")
317
+
318
+ if result.blockers:
319
+ print(f"\n Blockers ({len(result.blockers)}):")
320
+ for b in result.blockers:
321
+ print(f" - {b['message']}")
322
+
323
+ if result.warnings:
324
+ print(f"\n Warnings ({len(result.warnings)}):")
325
+ for w in result.warnings:
326
+ print(f" - {w['message']}")
327
+
328
+ return 0 if result.predicted_compatible else 2
329
+
330
+
331
+ def cmd_validate(args: argparse.Namespace) -> int:
332
+ """Test actual imports in isolated subprocesses."""
333
+ import json
334
+
335
+ from pybinaryguard.scanner import Scanner
336
+ from pybinaryguard.validators.import_validator import ImportValidator
337
+
338
+ scanner = Scanner(timeout=args.timeout, scan_mode=ScanMode.FAST)
339
+ report = scanner.run()
340
+
341
+ # Discover packages to test
342
+ validator = ImportValidator(
343
+ timeout=getattr(args, "import_timeout", 10.0),
344
+ )
345
+
346
+ # Get packages with binary extensions
347
+ test_packages = getattr(args, "packages", None)
348
+ packages_to_test = []
349
+
350
+ # Walk site-packages to find packages with top_level.txt
351
+ import os
352
+ for sp in scanner.get_profile().site_packages_paths:
353
+ if not os.path.isdir(sp):
354
+ continue
355
+ for entry in os.listdir(sp):
356
+ if entry.endswith(".dist-info"):
357
+ pkg_name = entry.rsplit("-", 2)[0].replace("_", "-")
358
+ if test_packages and pkg_name not in test_packages:
359
+ continue
360
+ top_level = validator.get_top_level_name(
361
+ os.path.join(sp, entry), pkg_name
362
+ )
363
+ # Only test packages with binary files (unless specific list given)
364
+ if test_packages:
365
+ packages_to_test.append((pkg_name, top_level))
366
+ else:
367
+ # Check if package has .so files
368
+ pkg_dir = os.path.join(sp, top_level)
369
+ has_so = False
370
+ if os.path.isdir(pkg_dir):
371
+ for root, _dirs, files in os.walk(pkg_dir):
372
+ if any(f.endswith(".so") for f in files):
373
+ has_so = True
374
+ break
375
+ if has_so:
376
+ packages_to_test.append((pkg_name, top_level))
377
+
378
+ if not packages_to_test:
379
+ print("No packages with binary extensions found to validate.")
380
+ return 0
381
+
382
+ fmt = getattr(args, "format", "table")
383
+ print(f"Testing {len(packages_to_test)} package imports...\n")
384
+
385
+ results = validator.test_packages(packages_to_test)
386
+ failures = [r for r in results if not r.success]
387
+
388
+ if fmt == "json":
389
+ output = json.dumps([r.as_dict() for r in results], indent=2)
390
+ print(output)
391
+ else:
392
+ for r in results:
393
+ if r.success:
394
+ print(f" [OK] {r.package_name} ({r.duration_ms:.0f}ms)")
395
+ else:
396
+ print(f" [FAIL] {r.package_name}: {r.error_type} — {r.error_message}")
397
+ if r.category:
398
+ print(f" Category: {r.category}")
399
+
400
+ print(f"\n{len(results) - len(failures)} passed, {len(failures)} failed")
401
+
402
+ return 2 if failures else 0
403
+
404
+
405
+ def _exit_code(critical: int, warning: int) -> int:
406
+ """Compute exit code from finding counts.
407
+
408
+ 0 = all passed, 1 = warnings only, 2 = critical issues.
409
+ """
410
+ if critical > 0:
411
+ return 2
412
+ if warning > 0:
413
+ return 1
414
+ return 0