aiptx 2.0.7__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.
- aipt_v2/__init__.py +110 -0
- aipt_v2/__main__.py +24 -0
- aipt_v2/agents/AIPTxAgent/__init__.py +10 -0
- aipt_v2/agents/AIPTxAgent/aiptx_agent.py +211 -0
- aipt_v2/agents/__init__.py +46 -0
- aipt_v2/agents/base.py +520 -0
- aipt_v2/agents/exploit_agent.py +688 -0
- aipt_v2/agents/ptt.py +406 -0
- aipt_v2/agents/state.py +168 -0
- aipt_v2/app.py +957 -0
- aipt_v2/browser/__init__.py +31 -0
- aipt_v2/browser/automation.py +458 -0
- aipt_v2/browser/crawler.py +453 -0
- aipt_v2/cli.py +2933 -0
- aipt_v2/compliance/__init__.py +71 -0
- aipt_v2/compliance/compliance_report.py +449 -0
- aipt_v2/compliance/framework_mapper.py +424 -0
- aipt_v2/compliance/nist_mapping.py +345 -0
- aipt_v2/compliance/owasp_mapping.py +330 -0
- aipt_v2/compliance/pci_mapping.py +297 -0
- aipt_v2/config.py +341 -0
- aipt_v2/core/__init__.py +43 -0
- aipt_v2/core/agent.py +630 -0
- aipt_v2/core/llm.py +395 -0
- aipt_v2/core/memory.py +305 -0
- aipt_v2/core/ptt.py +329 -0
- aipt_v2/database/__init__.py +14 -0
- aipt_v2/database/models.py +232 -0
- aipt_v2/database/repository.py +384 -0
- aipt_v2/docker/__init__.py +23 -0
- aipt_v2/docker/builder.py +260 -0
- aipt_v2/docker/manager.py +222 -0
- aipt_v2/docker/sandbox.py +371 -0
- aipt_v2/evasion/__init__.py +58 -0
- aipt_v2/evasion/request_obfuscator.py +272 -0
- aipt_v2/evasion/tls_fingerprint.py +285 -0
- aipt_v2/evasion/ua_rotator.py +301 -0
- aipt_v2/evasion/waf_bypass.py +439 -0
- aipt_v2/execution/__init__.py +23 -0
- aipt_v2/execution/executor.py +302 -0
- aipt_v2/execution/parser.py +544 -0
- aipt_v2/execution/terminal.py +337 -0
- aipt_v2/health.py +437 -0
- aipt_v2/intelligence/__init__.py +194 -0
- aipt_v2/intelligence/adaptation.py +474 -0
- aipt_v2/intelligence/auth.py +520 -0
- aipt_v2/intelligence/chaining.py +775 -0
- aipt_v2/intelligence/correlation.py +536 -0
- aipt_v2/intelligence/cve_aipt.py +334 -0
- aipt_v2/intelligence/cve_info.py +1111 -0
- aipt_v2/intelligence/knowledge_graph.py +590 -0
- aipt_v2/intelligence/learning.py +626 -0
- aipt_v2/intelligence/llm_analyzer.py +502 -0
- aipt_v2/intelligence/llm_tool_selector.py +518 -0
- aipt_v2/intelligence/payload_generator.py +562 -0
- aipt_v2/intelligence/rag.py +239 -0
- aipt_v2/intelligence/scope.py +442 -0
- aipt_v2/intelligence/searchers/__init__.py +5 -0
- aipt_v2/intelligence/searchers/exploitdb_searcher.py +523 -0
- aipt_v2/intelligence/searchers/github_searcher.py +467 -0
- aipt_v2/intelligence/searchers/google_searcher.py +281 -0
- aipt_v2/intelligence/tools.json +443 -0
- aipt_v2/intelligence/triage.py +670 -0
- aipt_v2/interactive_shell.py +559 -0
- aipt_v2/interface/__init__.py +5 -0
- aipt_v2/interface/cli.py +230 -0
- aipt_v2/interface/main.py +501 -0
- aipt_v2/interface/tui.py +1276 -0
- aipt_v2/interface/utils.py +583 -0
- aipt_v2/llm/__init__.py +39 -0
- aipt_v2/llm/config.py +26 -0
- aipt_v2/llm/llm.py +514 -0
- aipt_v2/llm/memory.py +214 -0
- aipt_v2/llm/request_queue.py +89 -0
- aipt_v2/llm/utils.py +89 -0
- aipt_v2/local_tool_installer.py +1467 -0
- aipt_v2/models/__init__.py +15 -0
- aipt_v2/models/findings.py +295 -0
- aipt_v2/models/phase_result.py +224 -0
- aipt_v2/models/scan_config.py +207 -0
- aipt_v2/monitoring/grafana/dashboards/aipt-dashboard.json +355 -0
- aipt_v2/monitoring/grafana/dashboards/default.yml +17 -0
- aipt_v2/monitoring/grafana/datasources/prometheus.yml +17 -0
- aipt_v2/monitoring/prometheus.yml +60 -0
- aipt_v2/orchestration/__init__.py +52 -0
- aipt_v2/orchestration/pipeline.py +398 -0
- aipt_v2/orchestration/progress.py +300 -0
- aipt_v2/orchestration/scheduler.py +296 -0
- aipt_v2/orchestrator.py +2427 -0
- aipt_v2/payloads/__init__.py +27 -0
- aipt_v2/payloads/cmdi.py +150 -0
- aipt_v2/payloads/sqli.py +263 -0
- aipt_v2/payloads/ssrf.py +204 -0
- aipt_v2/payloads/templates.py +222 -0
- aipt_v2/payloads/traversal.py +166 -0
- aipt_v2/payloads/xss.py +204 -0
- aipt_v2/prompts/__init__.py +60 -0
- aipt_v2/proxy/__init__.py +29 -0
- aipt_v2/proxy/history.py +352 -0
- aipt_v2/proxy/interceptor.py +452 -0
- aipt_v2/recon/__init__.py +44 -0
- aipt_v2/recon/dns.py +241 -0
- aipt_v2/recon/osint.py +367 -0
- aipt_v2/recon/subdomain.py +372 -0
- aipt_v2/recon/tech_detect.py +311 -0
- aipt_v2/reports/__init__.py +17 -0
- aipt_v2/reports/generator.py +313 -0
- aipt_v2/reports/html_report.py +378 -0
- aipt_v2/runtime/__init__.py +53 -0
- aipt_v2/runtime/base.py +30 -0
- aipt_v2/runtime/docker.py +401 -0
- aipt_v2/runtime/local.py +346 -0
- aipt_v2/runtime/tool_server.py +205 -0
- aipt_v2/runtime/vps.py +830 -0
- aipt_v2/scanners/__init__.py +28 -0
- aipt_v2/scanners/base.py +273 -0
- aipt_v2/scanners/nikto.py +244 -0
- aipt_v2/scanners/nmap.py +402 -0
- aipt_v2/scanners/nuclei.py +273 -0
- aipt_v2/scanners/web.py +454 -0
- aipt_v2/scripts/security_audit.py +366 -0
- aipt_v2/setup_wizard.py +941 -0
- aipt_v2/skills/__init__.py +80 -0
- aipt_v2/skills/agents/__init__.py +14 -0
- aipt_v2/skills/agents/api_tester.py +706 -0
- aipt_v2/skills/agents/base.py +477 -0
- aipt_v2/skills/agents/code_review.py +459 -0
- aipt_v2/skills/agents/security_agent.py +336 -0
- aipt_v2/skills/agents/web_pentest.py +818 -0
- aipt_v2/skills/prompts/__init__.py +647 -0
- aipt_v2/system_detector.py +539 -0
- aipt_v2/telemetry/__init__.py +7 -0
- aipt_v2/telemetry/tracer.py +347 -0
- aipt_v2/terminal/__init__.py +28 -0
- aipt_v2/terminal/executor.py +400 -0
- aipt_v2/terminal/sandbox.py +350 -0
- aipt_v2/tools/__init__.py +44 -0
- aipt_v2/tools/active_directory/__init__.py +78 -0
- aipt_v2/tools/active_directory/ad_config.py +238 -0
- aipt_v2/tools/active_directory/bloodhound_wrapper.py +447 -0
- aipt_v2/tools/active_directory/kerberos_attacks.py +430 -0
- aipt_v2/tools/active_directory/ldap_enum.py +533 -0
- aipt_v2/tools/active_directory/smb_attacks.py +505 -0
- aipt_v2/tools/agents_graph/__init__.py +19 -0
- aipt_v2/tools/agents_graph/agents_graph_actions.py +69 -0
- aipt_v2/tools/api_security/__init__.py +76 -0
- aipt_v2/tools/api_security/api_discovery.py +608 -0
- aipt_v2/tools/api_security/graphql_scanner.py +622 -0
- aipt_v2/tools/api_security/jwt_analyzer.py +577 -0
- aipt_v2/tools/api_security/openapi_fuzzer.py +761 -0
- aipt_v2/tools/browser/__init__.py +5 -0
- aipt_v2/tools/browser/browser_actions.py +238 -0
- aipt_v2/tools/browser/browser_instance.py +535 -0
- aipt_v2/tools/browser/tab_manager.py +344 -0
- aipt_v2/tools/cloud/__init__.py +70 -0
- aipt_v2/tools/cloud/cloud_config.py +273 -0
- aipt_v2/tools/cloud/cloud_scanner.py +639 -0
- aipt_v2/tools/cloud/prowler_tool.py +571 -0
- aipt_v2/tools/cloud/scoutsuite_tool.py +359 -0
- aipt_v2/tools/executor.py +307 -0
- aipt_v2/tools/parser.py +408 -0
- aipt_v2/tools/proxy/__init__.py +5 -0
- aipt_v2/tools/proxy/proxy_actions.py +103 -0
- aipt_v2/tools/proxy/proxy_manager.py +789 -0
- aipt_v2/tools/registry.py +196 -0
- aipt_v2/tools/scanners/__init__.py +343 -0
- aipt_v2/tools/scanners/acunetix_tool.py +712 -0
- aipt_v2/tools/scanners/burp_tool.py +631 -0
- aipt_v2/tools/scanners/config.py +156 -0
- aipt_v2/tools/scanners/nessus_tool.py +588 -0
- aipt_v2/tools/scanners/zap_tool.py +612 -0
- aipt_v2/tools/terminal/__init__.py +5 -0
- aipt_v2/tools/terminal/terminal_actions.py +37 -0
- aipt_v2/tools/terminal/terminal_manager.py +153 -0
- aipt_v2/tools/terminal/terminal_session.py +449 -0
- aipt_v2/tools/tool_processing.py +108 -0
- aipt_v2/utils/__init__.py +17 -0
- aipt_v2/utils/logging.py +202 -0
- aipt_v2/utils/model_manager.py +187 -0
- aipt_v2/utils/searchers/__init__.py +269 -0
- aipt_v2/verify_install.py +793 -0
- aiptx-2.0.7.dist-info/METADATA +345 -0
- aiptx-2.0.7.dist-info/RECORD +187 -0
- aiptx-2.0.7.dist-info/WHEEL +5 -0
- aiptx-2.0.7.dist-info/entry_points.txt +7 -0
- aiptx-2.0.7.dist-info/licenses/LICENSE +21 -0
- aiptx-2.0.7.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,793 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AIPTX Post-Installation Verification
|
|
3
|
+
=====================================
|
|
4
|
+
|
|
5
|
+
Verifies that AIPTX is correctly installed and configured.
|
|
6
|
+
Tests all components and generates a health report.
|
|
7
|
+
|
|
8
|
+
Features:
|
|
9
|
+
- System requirements check
|
|
10
|
+
- Python dependencies verification
|
|
11
|
+
- Security tools validation
|
|
12
|
+
- LLM connectivity test
|
|
13
|
+
- Configuration validation
|
|
14
|
+
- Performance benchmarks
|
|
15
|
+
|
|
16
|
+
Usage:
|
|
17
|
+
aiptx verify # Run full verification
|
|
18
|
+
aiptx verify --quick # Quick check
|
|
19
|
+
aiptx verify --fix # Auto-fix issues where possible
|
|
20
|
+
aiptx verify --report out.md # Generate markdown report
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
import asyncio
|
|
24
|
+
import importlib
|
|
25
|
+
import json
|
|
26
|
+
import os
|
|
27
|
+
import shutil
|
|
28
|
+
import subprocess
|
|
29
|
+
import sys
|
|
30
|
+
import time
|
|
31
|
+
from dataclasses import dataclass, field
|
|
32
|
+
from datetime import datetime
|
|
33
|
+
from enum import Enum
|
|
34
|
+
from pathlib import Path
|
|
35
|
+
from typing import Dict, List, Optional, Tuple, Any
|
|
36
|
+
|
|
37
|
+
from rich.console import Console
|
|
38
|
+
from rich.panel import Panel
|
|
39
|
+
from rich.table import Table
|
|
40
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn
|
|
41
|
+
from rich import box
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
console = Console()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class CheckStatus(Enum):
|
|
48
|
+
"""Status of a verification check."""
|
|
49
|
+
PASS = "pass"
|
|
50
|
+
WARN = "warn"
|
|
51
|
+
FAIL = "fail"
|
|
52
|
+
SKIP = "skip"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass
|
|
56
|
+
class CheckResult:
|
|
57
|
+
"""Result of a single verification check."""
|
|
58
|
+
name: str
|
|
59
|
+
status: CheckStatus
|
|
60
|
+
message: str
|
|
61
|
+
details: Optional[str] = None
|
|
62
|
+
fix_command: Optional[str] = None
|
|
63
|
+
duration_ms: float = 0
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass
|
|
67
|
+
class VerificationReport:
|
|
68
|
+
"""Complete verification report."""
|
|
69
|
+
timestamp: datetime = field(default_factory=datetime.now)
|
|
70
|
+
system_info: Dict[str, str] = field(default_factory=dict)
|
|
71
|
+
checks: List[CheckResult] = field(default_factory=list)
|
|
72
|
+
tool_status: Dict[str, bool] = field(default_factory=dict)
|
|
73
|
+
summary: Dict[str, int] = field(default_factory=dict)
|
|
74
|
+
|
|
75
|
+
def add_check(self, result: CheckResult):
|
|
76
|
+
"""Add a check result."""
|
|
77
|
+
self.checks.append(result)
|
|
78
|
+
|
|
79
|
+
def compute_summary(self):
|
|
80
|
+
"""Compute summary statistics."""
|
|
81
|
+
self.summary = {
|
|
82
|
+
"total": len(self.checks),
|
|
83
|
+
"passed": sum(1 for c in self.checks if c.status == CheckStatus.PASS),
|
|
84
|
+
"warnings": sum(1 for c in self.checks if c.status == CheckStatus.WARN),
|
|
85
|
+
"failed": sum(1 for c in self.checks if c.status == CheckStatus.FAIL),
|
|
86
|
+
"skipped": sum(1 for c in self.checks if c.status == CheckStatus.SKIP),
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
def is_healthy(self) -> bool:
|
|
90
|
+
"""Check if installation is healthy (no failures)."""
|
|
91
|
+
return all(c.status != CheckStatus.FAIL for c in self.checks)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class InstallVerifier:
|
|
95
|
+
"""
|
|
96
|
+
Verifies AIPTX installation and configuration.
|
|
97
|
+
|
|
98
|
+
Runs a series of checks to ensure the system is properly
|
|
99
|
+
set up and all components are working correctly.
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
def __init__(self, quick: bool = False, auto_fix: bool = False):
|
|
103
|
+
"""
|
|
104
|
+
Initialize verifier.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
quick: Run quick checks only
|
|
108
|
+
auto_fix: Attempt to fix issues automatically
|
|
109
|
+
"""
|
|
110
|
+
self.quick = quick
|
|
111
|
+
self.auto_fix = auto_fix
|
|
112
|
+
self.report = VerificationReport()
|
|
113
|
+
|
|
114
|
+
async def run_all_checks(self) -> VerificationReport:
|
|
115
|
+
"""Run all verification checks."""
|
|
116
|
+
console.print()
|
|
117
|
+
console.print(Panel(
|
|
118
|
+
"[bold cyan]AIPTX Installation Verification[/bold cyan]\n\n"
|
|
119
|
+
"Running comprehensive checks to verify your installation...",
|
|
120
|
+
title="🔍 Verification",
|
|
121
|
+
border_style="cyan"
|
|
122
|
+
))
|
|
123
|
+
console.print()
|
|
124
|
+
|
|
125
|
+
# Collect system info
|
|
126
|
+
await self._collect_system_info()
|
|
127
|
+
|
|
128
|
+
# Run checks with progress
|
|
129
|
+
checks = [
|
|
130
|
+
("Python Version", self._check_python_version),
|
|
131
|
+
("Python Dependencies", self._check_python_deps),
|
|
132
|
+
("AIPTX Package", self._check_aiptx_package),
|
|
133
|
+
("Configuration File", self._check_config_file),
|
|
134
|
+
("LLM Configuration", self._check_llm_config),
|
|
135
|
+
("Go Installation", self._check_go),
|
|
136
|
+
("Docker (optional)", self._check_docker),
|
|
137
|
+
("Core Tools", self._check_core_tools),
|
|
138
|
+
("Path Configuration", self._check_path),
|
|
139
|
+
("Permissions", self._check_permissions),
|
|
140
|
+
]
|
|
141
|
+
|
|
142
|
+
if not self.quick:
|
|
143
|
+
checks.extend([
|
|
144
|
+
("All Security Tools", self._check_all_tools),
|
|
145
|
+
("Network Connectivity", self._check_network),
|
|
146
|
+
("LLM Connectivity", self._check_llm_connectivity),
|
|
147
|
+
])
|
|
148
|
+
|
|
149
|
+
with Progress(
|
|
150
|
+
SpinnerColumn(),
|
|
151
|
+
TextColumn("[progress.description]{task.description}"),
|
|
152
|
+
console=console,
|
|
153
|
+
) as progress:
|
|
154
|
+
task = progress.add_task("Running checks...", total=len(checks))
|
|
155
|
+
|
|
156
|
+
for check_name, check_func in checks:
|
|
157
|
+
progress.update(task, description=f"Checking {check_name}...")
|
|
158
|
+
|
|
159
|
+
start_time = time.time()
|
|
160
|
+
try:
|
|
161
|
+
result = await check_func()
|
|
162
|
+
result.duration_ms = (time.time() - start_time) * 1000
|
|
163
|
+
self.report.add_check(result)
|
|
164
|
+
except Exception as e:
|
|
165
|
+
self.report.add_check(CheckResult(
|
|
166
|
+
name=check_name,
|
|
167
|
+
status=CheckStatus.FAIL,
|
|
168
|
+
message=f"Check failed with error: {str(e)}",
|
|
169
|
+
duration_ms=(time.time() - start_time) * 1000,
|
|
170
|
+
))
|
|
171
|
+
|
|
172
|
+
progress.advance(task)
|
|
173
|
+
|
|
174
|
+
# Compute summary
|
|
175
|
+
self.report.compute_summary()
|
|
176
|
+
|
|
177
|
+
return self.report
|
|
178
|
+
|
|
179
|
+
async def _collect_system_info(self):
|
|
180
|
+
"""Collect system information."""
|
|
181
|
+
import platform
|
|
182
|
+
|
|
183
|
+
self.report.system_info = {
|
|
184
|
+
"os": platform.system(),
|
|
185
|
+
"os_version": platform.version(),
|
|
186
|
+
"architecture": platform.machine(),
|
|
187
|
+
"python_version": platform.python_version(),
|
|
188
|
+
"hostname": platform.node(),
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
# Try to get package manager
|
|
192
|
+
for pm in ["brew", "apt", "dnf", "yum", "pacman"]:
|
|
193
|
+
if shutil.which(pm):
|
|
194
|
+
self.report.system_info["package_manager"] = pm
|
|
195
|
+
break
|
|
196
|
+
|
|
197
|
+
async def _check_python_version(self) -> CheckResult:
|
|
198
|
+
"""Check Python version."""
|
|
199
|
+
version = sys.version_info
|
|
200
|
+
version_str = f"{version.major}.{version.minor}.{version.micro}"
|
|
201
|
+
|
|
202
|
+
if version.major >= 3 and version.minor >= 9:
|
|
203
|
+
return CheckResult(
|
|
204
|
+
name="Python Version",
|
|
205
|
+
status=CheckStatus.PASS,
|
|
206
|
+
message=f"Python {version_str} installed",
|
|
207
|
+
)
|
|
208
|
+
elif version.major >= 3 and version.minor >= 8:
|
|
209
|
+
return CheckResult(
|
|
210
|
+
name="Python Version",
|
|
211
|
+
status=CheckStatus.WARN,
|
|
212
|
+
message=f"Python {version_str} (3.9+ recommended)",
|
|
213
|
+
)
|
|
214
|
+
else:
|
|
215
|
+
return CheckResult(
|
|
216
|
+
name="Python Version",
|
|
217
|
+
status=CheckStatus.FAIL,
|
|
218
|
+
message=f"Python {version_str} (requires 3.9+)",
|
|
219
|
+
fix_command="Install Python 3.9 or higher",
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
async def _check_python_deps(self) -> CheckResult:
|
|
223
|
+
"""Check required Python dependencies."""
|
|
224
|
+
required = [
|
|
225
|
+
"rich", "typer", "httpx", "pydantic", "litellm",
|
|
226
|
+
"sqlalchemy", "structlog", "fastapi",
|
|
227
|
+
]
|
|
228
|
+
|
|
229
|
+
missing = []
|
|
230
|
+
for pkg in required:
|
|
231
|
+
try:
|
|
232
|
+
importlib.import_module(pkg)
|
|
233
|
+
except ImportError:
|
|
234
|
+
missing.append(pkg)
|
|
235
|
+
|
|
236
|
+
if not missing:
|
|
237
|
+
return CheckResult(
|
|
238
|
+
name="Python Dependencies",
|
|
239
|
+
status=CheckStatus.PASS,
|
|
240
|
+
message=f"All {len(required)} core dependencies installed",
|
|
241
|
+
)
|
|
242
|
+
else:
|
|
243
|
+
return CheckResult(
|
|
244
|
+
name="Python Dependencies",
|
|
245
|
+
status=CheckStatus.FAIL,
|
|
246
|
+
message=f"Missing: {', '.join(missing)}",
|
|
247
|
+
fix_command=f"pip install {' '.join(missing)}",
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
async def _check_aiptx_package(self) -> CheckResult:
|
|
251
|
+
"""Check AIPTX package installation."""
|
|
252
|
+
try:
|
|
253
|
+
import aipt_v2
|
|
254
|
+
version = getattr(aipt_v2, "__version__", "unknown")
|
|
255
|
+
return CheckResult(
|
|
256
|
+
name="AIPTX Package",
|
|
257
|
+
status=CheckStatus.PASS,
|
|
258
|
+
message=f"AIPTX version {version} installed",
|
|
259
|
+
)
|
|
260
|
+
except ImportError:
|
|
261
|
+
return CheckResult(
|
|
262
|
+
name="AIPTX Package",
|
|
263
|
+
status=CheckStatus.FAIL,
|
|
264
|
+
message="AIPTX package not found",
|
|
265
|
+
fix_command="pip install aiptx",
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
async def _check_config_file(self) -> CheckResult:
|
|
269
|
+
"""Check configuration file exists."""
|
|
270
|
+
config_paths = [
|
|
271
|
+
Path.home() / ".aiptx" / ".env",
|
|
272
|
+
Path(".env"),
|
|
273
|
+
]
|
|
274
|
+
|
|
275
|
+
for path in config_paths:
|
|
276
|
+
if path.exists():
|
|
277
|
+
return CheckResult(
|
|
278
|
+
name="Configuration File",
|
|
279
|
+
status=CheckStatus.PASS,
|
|
280
|
+
message=f"Config found at {path}",
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
return CheckResult(
|
|
284
|
+
name="Configuration File",
|
|
285
|
+
status=CheckStatus.WARN,
|
|
286
|
+
message="No configuration file found",
|
|
287
|
+
details="Run 'aiptx setup' to configure",
|
|
288
|
+
fix_command="aiptx setup",
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
async def _check_llm_config(self) -> CheckResult:
|
|
292
|
+
"""Check LLM configuration."""
|
|
293
|
+
llm_keys = [
|
|
294
|
+
"ANTHROPIC_API_KEY",
|
|
295
|
+
"OPENAI_API_KEY",
|
|
296
|
+
"DEEPSEEK_API_KEY",
|
|
297
|
+
"LLM_API_KEY",
|
|
298
|
+
]
|
|
299
|
+
|
|
300
|
+
# Check for Ollama
|
|
301
|
+
if shutil.which("ollama"):
|
|
302
|
+
# Check if Ollama is running
|
|
303
|
+
try:
|
|
304
|
+
proc = await asyncio.create_subprocess_shell(
|
|
305
|
+
"curl -s http://localhost:11434/api/version",
|
|
306
|
+
stdout=asyncio.subprocess.PIPE,
|
|
307
|
+
stderr=asyncio.subprocess.PIPE,
|
|
308
|
+
)
|
|
309
|
+
stdout, _ = await asyncio.wait_for(proc.communicate(), timeout=5)
|
|
310
|
+
if proc.returncode == 0 and b"version" in stdout:
|
|
311
|
+
return CheckResult(
|
|
312
|
+
name="LLM Configuration",
|
|
313
|
+
status=CheckStatus.PASS,
|
|
314
|
+
message="Ollama running locally",
|
|
315
|
+
)
|
|
316
|
+
except Exception:
|
|
317
|
+
pass
|
|
318
|
+
|
|
319
|
+
# Check for API keys
|
|
320
|
+
for key in llm_keys:
|
|
321
|
+
if os.environ.get(key):
|
|
322
|
+
return CheckResult(
|
|
323
|
+
name="LLM Configuration",
|
|
324
|
+
status=CheckStatus.PASS,
|
|
325
|
+
message=f"{key} configured",
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
# Check .env file
|
|
329
|
+
env_path = Path.home() / ".aiptx" / ".env"
|
|
330
|
+
if env_path.exists():
|
|
331
|
+
with open(env_path) as f:
|
|
332
|
+
content = f.read()
|
|
333
|
+
for key in llm_keys:
|
|
334
|
+
if key in content:
|
|
335
|
+
return CheckResult(
|
|
336
|
+
name="LLM Configuration",
|
|
337
|
+
status=CheckStatus.PASS,
|
|
338
|
+
message=f"{key} found in config",
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
return CheckResult(
|
|
342
|
+
name="LLM Configuration",
|
|
343
|
+
status=CheckStatus.FAIL,
|
|
344
|
+
message="No LLM API key or Ollama configured",
|
|
345
|
+
fix_command="aiptx setup",
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
async def _check_go(self) -> CheckResult:
|
|
349
|
+
"""Check Go installation."""
|
|
350
|
+
if shutil.which("go"):
|
|
351
|
+
try:
|
|
352
|
+
proc = await asyncio.create_subprocess_shell(
|
|
353
|
+
"go version",
|
|
354
|
+
stdout=asyncio.subprocess.PIPE,
|
|
355
|
+
stderr=asyncio.subprocess.PIPE,
|
|
356
|
+
)
|
|
357
|
+
stdout, _ = await proc.communicate()
|
|
358
|
+
version = stdout.decode().strip()
|
|
359
|
+
return CheckResult(
|
|
360
|
+
name="Go Installation",
|
|
361
|
+
status=CheckStatus.PASS,
|
|
362
|
+
message=version.replace("go version ", ""),
|
|
363
|
+
)
|
|
364
|
+
except Exception:
|
|
365
|
+
pass
|
|
366
|
+
|
|
367
|
+
return CheckResult(
|
|
368
|
+
name="Go Installation",
|
|
369
|
+
status=CheckStatus.WARN,
|
|
370
|
+
message="Go not installed (required for some tools)",
|
|
371
|
+
fix_command="aiptx tools install -t go",
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
async def _check_docker(self) -> CheckResult:
|
|
375
|
+
"""Check Docker installation (optional)."""
|
|
376
|
+
if shutil.which("docker"):
|
|
377
|
+
try:
|
|
378
|
+
proc = await asyncio.create_subprocess_shell(
|
|
379
|
+
"docker --version",
|
|
380
|
+
stdout=asyncio.subprocess.PIPE,
|
|
381
|
+
stderr=asyncio.subprocess.PIPE,
|
|
382
|
+
)
|
|
383
|
+
stdout, _ = await proc.communicate()
|
|
384
|
+
if proc.returncode == 0:
|
|
385
|
+
version = stdout.decode().strip()
|
|
386
|
+
return CheckResult(
|
|
387
|
+
name="Docker (optional)",
|
|
388
|
+
status=CheckStatus.PASS,
|
|
389
|
+
message=version,
|
|
390
|
+
)
|
|
391
|
+
except Exception:
|
|
392
|
+
pass
|
|
393
|
+
|
|
394
|
+
return CheckResult(
|
|
395
|
+
name="Docker (optional)",
|
|
396
|
+
status=CheckStatus.SKIP,
|
|
397
|
+
message="Docker not available (optional for sandboxing)",
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
async def _check_core_tools(self) -> CheckResult:
|
|
401
|
+
"""Check core security tools."""
|
|
402
|
+
core_tools = ["nmap", "nuclei", "sqlmap", "ffuf", "httpx"]
|
|
403
|
+
installed = []
|
|
404
|
+
missing = []
|
|
405
|
+
|
|
406
|
+
for tool in core_tools:
|
|
407
|
+
if shutil.which(tool):
|
|
408
|
+
installed.append(tool)
|
|
409
|
+
else:
|
|
410
|
+
missing.append(tool)
|
|
411
|
+
|
|
412
|
+
self.report.tool_status.update({t: t in installed for t in core_tools})
|
|
413
|
+
|
|
414
|
+
if not missing:
|
|
415
|
+
return CheckResult(
|
|
416
|
+
name="Core Tools",
|
|
417
|
+
status=CheckStatus.PASS,
|
|
418
|
+
message=f"All {len(core_tools)} core tools installed",
|
|
419
|
+
)
|
|
420
|
+
elif len(missing) <= 2:
|
|
421
|
+
return CheckResult(
|
|
422
|
+
name="Core Tools",
|
|
423
|
+
status=CheckStatus.WARN,
|
|
424
|
+
message=f"Missing: {', '.join(missing)}",
|
|
425
|
+
fix_command=f"aiptx tools install -t {' '.join(missing)}",
|
|
426
|
+
)
|
|
427
|
+
else:
|
|
428
|
+
return CheckResult(
|
|
429
|
+
name="Core Tools",
|
|
430
|
+
status=CheckStatus.FAIL,
|
|
431
|
+
message=f"Missing {len(missing)}/{len(core_tools)} core tools",
|
|
432
|
+
fix_command="aiptx tools install --core",
|
|
433
|
+
)
|
|
434
|
+
|
|
435
|
+
async def _check_all_tools(self) -> CheckResult:
|
|
436
|
+
"""Check all available security tools."""
|
|
437
|
+
try:
|
|
438
|
+
from aipt_v2.local_tool_installer import TOOLS
|
|
439
|
+
|
|
440
|
+
installed = 0
|
|
441
|
+
total = len(TOOLS)
|
|
442
|
+
|
|
443
|
+
for tool_name in TOOLS:
|
|
444
|
+
if shutil.which(tool_name):
|
|
445
|
+
installed += 1
|
|
446
|
+
self.report.tool_status[tool_name] = True
|
|
447
|
+
else:
|
|
448
|
+
self.report.tool_status[tool_name] = False
|
|
449
|
+
|
|
450
|
+
coverage = (installed / total * 100) if total > 0 else 0
|
|
451
|
+
|
|
452
|
+
if coverage >= 50:
|
|
453
|
+
return CheckResult(
|
|
454
|
+
name="All Security Tools",
|
|
455
|
+
status=CheckStatus.PASS,
|
|
456
|
+
message=f"{installed}/{total} tools installed ({coverage:.0f}%)",
|
|
457
|
+
)
|
|
458
|
+
elif coverage >= 25:
|
|
459
|
+
return CheckResult(
|
|
460
|
+
name="All Security Tools",
|
|
461
|
+
status=CheckStatus.WARN,
|
|
462
|
+
message=f"{installed}/{total} tools installed ({coverage:.0f}%)",
|
|
463
|
+
fix_command="aiptx tools install --all",
|
|
464
|
+
)
|
|
465
|
+
else:
|
|
466
|
+
return CheckResult(
|
|
467
|
+
name="All Security Tools",
|
|
468
|
+
status=CheckStatus.WARN,
|
|
469
|
+
message=f"Only {installed}/{total} tools installed",
|
|
470
|
+
fix_command="aiptx tools install --core",
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
except ImportError:
|
|
474
|
+
return CheckResult(
|
|
475
|
+
name="All Security Tools",
|
|
476
|
+
status=CheckStatus.SKIP,
|
|
477
|
+
message="Tool catalog not available",
|
|
478
|
+
)
|
|
479
|
+
|
|
480
|
+
async def _check_path(self) -> CheckResult:
|
|
481
|
+
"""Check PATH configuration."""
|
|
482
|
+
path = os.environ.get("PATH", "")
|
|
483
|
+
home = str(Path.home())
|
|
484
|
+
|
|
485
|
+
required_paths = [
|
|
486
|
+
f"{home}/.local/bin",
|
|
487
|
+
f"{home}/go/bin",
|
|
488
|
+
]
|
|
489
|
+
|
|
490
|
+
missing = [p for p in required_paths if p not in path]
|
|
491
|
+
|
|
492
|
+
if not missing:
|
|
493
|
+
return CheckResult(
|
|
494
|
+
name="Path Configuration",
|
|
495
|
+
status=CheckStatus.PASS,
|
|
496
|
+
message="PATH includes required directories",
|
|
497
|
+
)
|
|
498
|
+
else:
|
|
499
|
+
return CheckResult(
|
|
500
|
+
name="Path Configuration",
|
|
501
|
+
status=CheckStatus.WARN,
|
|
502
|
+
message=f"Missing from PATH: {', '.join(missing)}",
|
|
503
|
+
details="Add to your shell profile (.bashrc, .zshrc)",
|
|
504
|
+
fix_command=f"export PATH=$PATH:{':'.join(missing)}",
|
|
505
|
+
)
|
|
506
|
+
|
|
507
|
+
async def _check_permissions(self) -> CheckResult:
|
|
508
|
+
"""Check file permissions."""
|
|
509
|
+
config_dir = Path.home() / ".aiptx"
|
|
510
|
+
|
|
511
|
+
if config_dir.exists():
|
|
512
|
+
# Check if .env has proper permissions
|
|
513
|
+
env_file = config_dir / ".env"
|
|
514
|
+
if env_file.exists():
|
|
515
|
+
mode = env_file.stat().st_mode & 0o777
|
|
516
|
+
if mode == 0o600:
|
|
517
|
+
return CheckResult(
|
|
518
|
+
name="Permissions",
|
|
519
|
+
status=CheckStatus.PASS,
|
|
520
|
+
message="Config file has secure permissions",
|
|
521
|
+
)
|
|
522
|
+
else:
|
|
523
|
+
return CheckResult(
|
|
524
|
+
name="Permissions",
|
|
525
|
+
status=CheckStatus.WARN,
|
|
526
|
+
message=f"Config file permissions: {oct(mode)} (should be 600)",
|
|
527
|
+
fix_command=f"chmod 600 {env_file}",
|
|
528
|
+
)
|
|
529
|
+
|
|
530
|
+
return CheckResult(
|
|
531
|
+
name="Permissions",
|
|
532
|
+
status=CheckStatus.SKIP,
|
|
533
|
+
message="No config files to check",
|
|
534
|
+
)
|
|
535
|
+
|
|
536
|
+
async def _check_network(self) -> CheckResult:
|
|
537
|
+
"""Check network connectivity."""
|
|
538
|
+
try:
|
|
539
|
+
proc = await asyncio.create_subprocess_shell(
|
|
540
|
+
"curl -s --connect-timeout 5 -o /dev/null -w '%{http_code}' https://api.anthropic.com",
|
|
541
|
+
stdout=asyncio.subprocess.PIPE,
|
|
542
|
+
stderr=asyncio.subprocess.PIPE,
|
|
543
|
+
)
|
|
544
|
+
stdout, _ = await asyncio.wait_for(proc.communicate(), timeout=10)
|
|
545
|
+
status_code = stdout.decode().strip()
|
|
546
|
+
|
|
547
|
+
if status_code in ["200", "401", "403"]:
|
|
548
|
+
return CheckResult(
|
|
549
|
+
name="Network Connectivity",
|
|
550
|
+
status=CheckStatus.PASS,
|
|
551
|
+
message="Internet connectivity OK",
|
|
552
|
+
)
|
|
553
|
+
else:
|
|
554
|
+
return CheckResult(
|
|
555
|
+
name="Network Connectivity",
|
|
556
|
+
status=CheckStatus.WARN,
|
|
557
|
+
message=f"API returned status {status_code}",
|
|
558
|
+
)
|
|
559
|
+
|
|
560
|
+
except asyncio.TimeoutError:
|
|
561
|
+
return CheckResult(
|
|
562
|
+
name="Network Connectivity",
|
|
563
|
+
status=CheckStatus.FAIL,
|
|
564
|
+
message="Connection timeout",
|
|
565
|
+
)
|
|
566
|
+
except Exception as e:
|
|
567
|
+
return CheckResult(
|
|
568
|
+
name="Network Connectivity",
|
|
569
|
+
status=CheckStatus.FAIL,
|
|
570
|
+
message=f"Network error: {str(e)}",
|
|
571
|
+
)
|
|
572
|
+
|
|
573
|
+
async def _check_llm_connectivity(self) -> CheckResult:
|
|
574
|
+
"""Test LLM connectivity."""
|
|
575
|
+
# Check Ollama first
|
|
576
|
+
if shutil.which("ollama"):
|
|
577
|
+
try:
|
|
578
|
+
proc = await asyncio.create_subprocess_shell(
|
|
579
|
+
"curl -s http://localhost:11434/api/tags",
|
|
580
|
+
stdout=asyncio.subprocess.PIPE,
|
|
581
|
+
stderr=asyncio.subprocess.PIPE,
|
|
582
|
+
)
|
|
583
|
+
stdout, _ = await asyncio.wait_for(proc.communicate(), timeout=5)
|
|
584
|
+
if proc.returncode == 0:
|
|
585
|
+
data = json.loads(stdout.decode())
|
|
586
|
+
models = len(data.get("models", []))
|
|
587
|
+
return CheckResult(
|
|
588
|
+
name="LLM Connectivity",
|
|
589
|
+
status=CheckStatus.PASS,
|
|
590
|
+
message=f"Ollama running with {models} models",
|
|
591
|
+
)
|
|
592
|
+
except Exception:
|
|
593
|
+
pass
|
|
594
|
+
|
|
595
|
+
# Skip API test if no key configured
|
|
596
|
+
api_keys = ["ANTHROPIC_API_KEY", "OPENAI_API_KEY"]
|
|
597
|
+
has_key = any(os.environ.get(k) for k in api_keys)
|
|
598
|
+
|
|
599
|
+
if not has_key:
|
|
600
|
+
return CheckResult(
|
|
601
|
+
name="LLM Connectivity",
|
|
602
|
+
status=CheckStatus.SKIP,
|
|
603
|
+
message="No API key configured (using Ollama or not configured)",
|
|
604
|
+
)
|
|
605
|
+
|
|
606
|
+
return CheckResult(
|
|
607
|
+
name="LLM Connectivity",
|
|
608
|
+
status=CheckStatus.WARN,
|
|
609
|
+
message="API key configured but not tested",
|
|
610
|
+
)
|
|
611
|
+
|
|
612
|
+
def print_report(self):
|
|
613
|
+
"""Print verification report to console."""
|
|
614
|
+
console.print()
|
|
615
|
+
|
|
616
|
+
# Results table
|
|
617
|
+
table = Table(title="Verification Results", box=box.ROUNDED)
|
|
618
|
+
table.add_column("Check", style="cyan")
|
|
619
|
+
table.add_column("Status", justify="center")
|
|
620
|
+
table.add_column("Message")
|
|
621
|
+
table.add_column("Time", justify="right", style="dim")
|
|
622
|
+
|
|
623
|
+
for check in self.report.checks:
|
|
624
|
+
if check.status == CheckStatus.PASS:
|
|
625
|
+
status = "[green]✓ PASS[/green]"
|
|
626
|
+
elif check.status == CheckStatus.WARN:
|
|
627
|
+
status = "[yellow]⚠ WARN[/yellow]"
|
|
628
|
+
elif check.status == CheckStatus.FAIL:
|
|
629
|
+
status = "[red]✗ FAIL[/red]"
|
|
630
|
+
else:
|
|
631
|
+
status = "[dim]○ SKIP[/dim]"
|
|
632
|
+
|
|
633
|
+
table.add_row(
|
|
634
|
+
check.name,
|
|
635
|
+
status,
|
|
636
|
+
check.message,
|
|
637
|
+
f"{check.duration_ms:.0f}ms"
|
|
638
|
+
)
|
|
639
|
+
|
|
640
|
+
console.print(table)
|
|
641
|
+
|
|
642
|
+
# Summary
|
|
643
|
+
s = self.report.summary
|
|
644
|
+
console.print()
|
|
645
|
+
|
|
646
|
+
if s["failed"] == 0:
|
|
647
|
+
console.print(Panel(
|
|
648
|
+
f"[bold green]✓ Verification Passed[/bold green]\n\n"
|
|
649
|
+
f"Passed: {s['passed']} Warnings: {s['warnings']} Skipped: {s['skipped']}",
|
|
650
|
+
title="Summary",
|
|
651
|
+
border_style="green"
|
|
652
|
+
))
|
|
653
|
+
else:
|
|
654
|
+
console.print(Panel(
|
|
655
|
+
f"[bold red]✗ Verification Failed[/bold red]\n\n"
|
|
656
|
+
f"Passed: {s['passed']} Warnings: {s['warnings']} "
|
|
657
|
+
f"[red]Failed: {s['failed']}[/red] Skipped: {s['skipped']}",
|
|
658
|
+
title="Summary",
|
|
659
|
+
border_style="red"
|
|
660
|
+
))
|
|
661
|
+
|
|
662
|
+
# Show fix commands
|
|
663
|
+
failed_checks = [c for c in self.report.checks if c.status == CheckStatus.FAIL and c.fix_command]
|
|
664
|
+
if failed_checks:
|
|
665
|
+
console.print("\n[bold]Suggested fixes:[/bold]")
|
|
666
|
+
for check in failed_checks:
|
|
667
|
+
console.print(f" [cyan]{check.name}:[/cyan] {check.fix_command}")
|
|
668
|
+
|
|
669
|
+
console.print()
|
|
670
|
+
|
|
671
|
+
def generate_markdown_report(self) -> str:
|
|
672
|
+
"""Generate markdown report."""
|
|
673
|
+
lines = [
|
|
674
|
+
"# AIPTX Installation Verification Report",
|
|
675
|
+
"",
|
|
676
|
+
f"Generated: {self.report.timestamp.strftime('%Y-%m-%d %H:%M:%S')}",
|
|
677
|
+
"",
|
|
678
|
+
"## System Information",
|
|
679
|
+
"",
|
|
680
|
+
]
|
|
681
|
+
|
|
682
|
+
for key, value in self.report.system_info.items():
|
|
683
|
+
lines.append(f"- **{key}**: {value}")
|
|
684
|
+
|
|
685
|
+
lines.extend([
|
|
686
|
+
"",
|
|
687
|
+
"## Verification Checks",
|
|
688
|
+
"",
|
|
689
|
+
"| Check | Status | Message |",
|
|
690
|
+
"|-------|--------|---------|",
|
|
691
|
+
])
|
|
692
|
+
|
|
693
|
+
for check in self.report.checks:
|
|
694
|
+
status_emoji = {
|
|
695
|
+
CheckStatus.PASS: "✅",
|
|
696
|
+
CheckStatus.WARN: "⚠️",
|
|
697
|
+
CheckStatus.FAIL: "❌",
|
|
698
|
+
CheckStatus.SKIP: "⏭️",
|
|
699
|
+
}.get(check.status, "?")
|
|
700
|
+
|
|
701
|
+
lines.append(f"| {check.name} | {status_emoji} {check.status.value.upper()} | {check.message} |")
|
|
702
|
+
|
|
703
|
+
s = self.report.summary
|
|
704
|
+
lines.extend([
|
|
705
|
+
"",
|
|
706
|
+
"## Summary",
|
|
707
|
+
"",
|
|
708
|
+
f"- **Total Checks**: {s['total']}",
|
|
709
|
+
f"- **Passed**: {s['passed']}",
|
|
710
|
+
f"- **Warnings**: {s['warnings']}",
|
|
711
|
+
f"- **Failed**: {s['failed']}",
|
|
712
|
+
f"- **Skipped**: {s['skipped']}",
|
|
713
|
+
"",
|
|
714
|
+
])
|
|
715
|
+
|
|
716
|
+
# Tool status
|
|
717
|
+
if self.report.tool_status:
|
|
718
|
+
installed = sum(1 for v in self.report.tool_status.values() if v)
|
|
719
|
+
total = len(self.report.tool_status)
|
|
720
|
+
lines.extend([
|
|
721
|
+
"## Security Tools",
|
|
722
|
+
"",
|
|
723
|
+
f"Installed: {installed}/{total} ({installed/total*100:.0f}%)",
|
|
724
|
+
"",
|
|
725
|
+
])
|
|
726
|
+
|
|
727
|
+
return "\n".join(lines)
|
|
728
|
+
|
|
729
|
+
|
|
730
|
+
async def verify_installation(
|
|
731
|
+
quick: bool = False,
|
|
732
|
+
auto_fix: bool = False,
|
|
733
|
+
report_file: Optional[str] = None,
|
|
734
|
+
) -> int:
|
|
735
|
+
"""
|
|
736
|
+
Verify AIPTX installation.
|
|
737
|
+
|
|
738
|
+
Args:
|
|
739
|
+
quick: Run quick checks only
|
|
740
|
+
auto_fix: Auto-fix issues
|
|
741
|
+
report_file: Path to save markdown report
|
|
742
|
+
|
|
743
|
+
Returns:
|
|
744
|
+
Exit code (0 = healthy, 1 = issues found)
|
|
745
|
+
"""
|
|
746
|
+
verifier = InstallVerifier(quick=quick, auto_fix=auto_fix)
|
|
747
|
+
report = await verifier.run_all_checks()
|
|
748
|
+
|
|
749
|
+
# Print results
|
|
750
|
+
verifier.print_report()
|
|
751
|
+
|
|
752
|
+
# Save report if requested
|
|
753
|
+
if report_file:
|
|
754
|
+
md_report = verifier.generate_markdown_report()
|
|
755
|
+
Path(report_file).write_text(md_report)
|
|
756
|
+
console.print(f"[dim]Report saved to: {report_file}[/dim]")
|
|
757
|
+
|
|
758
|
+
return 0 if report.is_healthy() else 1
|
|
759
|
+
|
|
760
|
+
|
|
761
|
+
def main():
|
|
762
|
+
"""CLI entry point."""
|
|
763
|
+
import argparse
|
|
764
|
+
|
|
765
|
+
parser = argparse.ArgumentParser(description="Verify AIPTX installation")
|
|
766
|
+
parser.add_argument(
|
|
767
|
+
"--quick", "-q",
|
|
768
|
+
action="store_true",
|
|
769
|
+
help="Run quick checks only"
|
|
770
|
+
)
|
|
771
|
+
parser.add_argument(
|
|
772
|
+
"--fix",
|
|
773
|
+
action="store_true",
|
|
774
|
+
help="Attempt to auto-fix issues"
|
|
775
|
+
)
|
|
776
|
+
parser.add_argument(
|
|
777
|
+
"--report", "-r",
|
|
778
|
+
help="Save markdown report to file"
|
|
779
|
+
)
|
|
780
|
+
|
|
781
|
+
args = parser.parse_args()
|
|
782
|
+
|
|
783
|
+
exit_code = asyncio.run(verify_installation(
|
|
784
|
+
quick=args.quick,
|
|
785
|
+
auto_fix=args.fix,
|
|
786
|
+
report_file=args.report,
|
|
787
|
+
))
|
|
788
|
+
|
|
789
|
+
sys.exit(exit_code)
|
|
790
|
+
|
|
791
|
+
|
|
792
|
+
if __name__ == "__main__":
|
|
793
|
+
main()
|