cyguide 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.
- cyguide/__init__.py +0 -0
- cyguide/app.py +154 -0
- cyguide/engine/__init__.py +0 -0
- cyguide/engine/adapter.py +40 -0
- cyguide/engine/canonicalize.py +56 -0
- cyguide/engine/diagnostics.py +327 -0
- cyguide/engine/executor.py +236 -0
- cyguide/engine/explainer.py +144 -0
- cyguide/engine/gateway.py +103 -0
- cyguide/engine/manifest.py +40 -0
- cyguide/engine/power_facade.py +436 -0
- cyguide/engine/recommendation.py +184 -0
- cyguide/engine/registry.py +107 -0
- cyguide/engine/reporter.py +217 -0
- cyguide/engine/store.py +655 -0
- cyguide/main.py +153 -0
- cyguide/modes/__init__.py +0 -0
- cyguide/modes/learning.py +68 -0
- cyguide/modes/power.py +47 -0
- cyguide/schemas/__init__.py +0 -0
- cyguide/schemas/base.py +37 -0
- cyguide/schemas/credential.py +33 -0
- cyguide/schemas/dns.py +65 -0
- cyguide/schemas/entity.py +18 -0
- cyguide/schemas/host_internal.py +48 -0
- cyguide/schemas/netblock.py +22 -0
- cyguide/schemas/network.py +55 -0
- cyguide/schemas/power.py +63 -0
- cyguide/schemas/tls.py +26 -0
- cyguide/schemas/vulnerability.py +35 -0
- cyguide/schemas/web.py +64 -0
- cyguide/ui/__init__.py +0 -0
- cyguide/ui/screens/__init__.py +0 -0
- cyguide/ui/screens/home.py +643 -0
- cyguide/ui/screens/learning.py +527 -0
- cyguide/ui/screens/power.py +787 -0
- cyguide/ui/screens/tool_browser.py +102 -0
- cyguide/ui/style.css +831 -0
- cyguide/ui/widgets/__init__.py +0 -0
- cyguide/ui/widgets/home.py +9 -0
- cyguide/ui/widgets/power.py +568 -0
- cyguide-1.0.0.dist-info/METADATA +166 -0
- cyguide-1.0.0.dist-info/RECORD +64 -0
- cyguide-1.0.0.dist-info/WHEEL +5 -0
- cyguide-1.0.0.dist-info/entry_points.txt +2 -0
- cyguide-1.0.0.dist-info/licenses/LICENSE +21 -0
- cyguide-1.0.0.dist-info/top_level.txt +2 -0
- tools/__init__.py +0 -0
- tools/gobuster/__init__.py +0 -0
- tools/gobuster/adapter.py +94 -0
- tools/gobuster/manifest.toml +34 -0
- tools/gobuster/wordlist.txt +11 -0
- tools/nikto/__init__.py +0 -0
- tools/nikto/adapter.py +144 -0
- tools/nikto/manifest.toml +34 -0
- tools/nmap/__init__.py +0 -0
- tools/nmap/adapter.py +83 -0
- tools/nmap/manifest.toml +41 -0
- tools/subfinder/__init__.py +0 -0
- tools/subfinder/adapter.py +63 -0
- tools/subfinder/manifest.toml +34 -0
- tools/whois/__init__.py +0 -0
- tools/whois/adapter.py +68 -0
- tools/whois/manifest.toml +29 -0
cyguide/__init__.py
ADDED
|
File without changes
|
cyguide/app.py
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""Main CyGuide TUI Application."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from textual.app import App, SystemCommand
|
|
5
|
+
from cyguide.ui.screens.home import DashboardScreen
|
|
6
|
+
from cyguide.ui.screens.learning import LearningModeScreen
|
|
7
|
+
from cyguide.ui.screens.tool_browser import ToolBrowserScreen
|
|
8
|
+
from cyguide.ui.screens.power import PowerModeScreen
|
|
9
|
+
|
|
10
|
+
from typing import Optional, Dict, Any, List
|
|
11
|
+
import subprocess
|
|
12
|
+
|
|
13
|
+
from cyguide.engine.registry import ToolRegistry
|
|
14
|
+
from cyguide.engine.store import GraphStore
|
|
15
|
+
from cyguide.engine.executor import Executor
|
|
16
|
+
from cyguide.engine.explainer import LearningExplainer, OllamaBackend
|
|
17
|
+
from cyguide.modes.learning import LearningModeEngine
|
|
18
|
+
from cyguide.modes.power import PowerModeEngine
|
|
19
|
+
from cyguide.engine.power_facade import PowerWorkspaceFacade
|
|
20
|
+
|
|
21
|
+
class CyGuideApp(App):
|
|
22
|
+
"""The central orchestrator for the CyGuide TUI."""
|
|
23
|
+
|
|
24
|
+
CSS_PATH = "ui/style.css"
|
|
25
|
+
TITLE = "CyGuide"
|
|
26
|
+
|
|
27
|
+
# Register screens so they can be switched by name
|
|
28
|
+
SCREENS = {
|
|
29
|
+
"dashboard": DashboardScreen,
|
|
30
|
+
"tool_browser": ToolBrowserScreen,
|
|
31
|
+
"learning_mode": LearningModeScreen,
|
|
32
|
+
"power_mode": PowerModeScreen,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
def __init__(self, db_path: str = "data/cyguide.db", use_ollama: bool = False, ollama_model: str = "gemma", tools_dir: str = None, **kwargs):
|
|
36
|
+
super().__init__(**kwargs)
|
|
37
|
+
self.db_path = db_path
|
|
38
|
+
self.use_ollama = use_ollama
|
|
39
|
+
self.ollama_model = ollama_model
|
|
40
|
+
self.tools_dir = tools_dir
|
|
41
|
+
self.selected_session_id: Optional[str] = None
|
|
42
|
+
self.selected_workspace_id: Optional[str] = None
|
|
43
|
+
self.user_env: Dict[str, str] = os.environ.copy()
|
|
44
|
+
self.user_shell: str = "/bin/bash"
|
|
45
|
+
# Ensure the directory for the database exists
|
|
46
|
+
db_dir = os.path.dirname(os.path.abspath(self.db_path))
|
|
47
|
+
os.makedirs(db_dir, exist_ok=True)
|
|
48
|
+
|
|
49
|
+
def detect_user_shell(self) -> str:
|
|
50
|
+
"""Find the user's default shell."""
|
|
51
|
+
return os.environ.get("SHELL", "/bin/bash")
|
|
52
|
+
|
|
53
|
+
def get_shell_environment(self) -> Dict[str, str]:
|
|
54
|
+
"""Capture the full login shell environment."""
|
|
55
|
+
shell = self.detect_user_shell()
|
|
56
|
+
try:
|
|
57
|
+
# Run a login shell to capture the fully resolved environment
|
|
58
|
+
result = subprocess.run(
|
|
59
|
+
[shell, "-l", "-c", "env"],
|
|
60
|
+
capture_output=True, text=True, timeout=5
|
|
61
|
+
)
|
|
62
|
+
env = {}
|
|
63
|
+
for line in result.stdout.splitlines():
|
|
64
|
+
if "=" in line:
|
|
65
|
+
key, _, val = line.partition("=")
|
|
66
|
+
env[key] = val
|
|
67
|
+
return env if env else os.environ.copy()
|
|
68
|
+
except Exception:
|
|
69
|
+
return os.environ.copy()
|
|
70
|
+
|
|
71
|
+
async def on_mount(self) -> None:
|
|
72
|
+
"""Initialize the app with core components and the dashboard."""
|
|
73
|
+
self.user_shell = self.detect_user_shell()
|
|
74
|
+
self.user_env = self.get_shell_environment()
|
|
75
|
+
|
|
76
|
+
self.learning_mode_tool = "nmap" # Default
|
|
77
|
+
# 1. Initialize Engines
|
|
78
|
+
self.registry = ToolRegistry(tools_dir=self.tools_dir)
|
|
79
|
+
self.registry.load_tools()
|
|
80
|
+
|
|
81
|
+
self.store = GraphStore(self.db_path)
|
|
82
|
+
await self.store.initialize()
|
|
83
|
+
|
|
84
|
+
# Ensure singleton Learning Sandbox exists
|
|
85
|
+
self.learning_ws_id, self.learning_session_id = await self.store.get_or_create_learning_sandbox()
|
|
86
|
+
|
|
87
|
+
from cyguide.engine.recommendation import RecommendationEngine
|
|
88
|
+
self.recommendation_engine = RecommendationEngine(self.store)
|
|
89
|
+
self.store.on_upsert(self.recommendation_engine.handle_upsert)
|
|
90
|
+
|
|
91
|
+
self.executor = Executor(self.store)
|
|
92
|
+
|
|
93
|
+
# Setup Explainer
|
|
94
|
+
from dotenv import load_dotenv
|
|
95
|
+
load_dotenv()
|
|
96
|
+
if self.use_ollama:
|
|
97
|
+
self.explainer_backend = OllamaBackend(model=self.ollama_model)
|
|
98
|
+
else:
|
|
99
|
+
api_key = os.environ.get("CYGUIDE_API_KEY") or os.environ.get("GROQ_API_KEY")
|
|
100
|
+
if api_key:
|
|
101
|
+
from cyguide.engine.explainer import CloudAPIBackend
|
|
102
|
+
api_type = os.environ.get("CYGUIDE_API_TYPE", "openai")
|
|
103
|
+
if os.environ.get("GROQ_API_KEY") and not os.environ.get("CYGUIDE_API_BASE"):
|
|
104
|
+
api_base = "https://api.groq.com/openai/v1"
|
|
105
|
+
api_model = os.environ.get("CYGUIDE_API_MODEL", "llama-3.3-70b-versatile")
|
|
106
|
+
else:
|
|
107
|
+
api_base = os.environ.get("CYGUIDE_API_BASE", "https://api.openai.com/v1")
|
|
108
|
+
api_model = os.environ.get("CYGUIDE_API_MODEL", "gpt-4o")
|
|
109
|
+
self.explainer_backend = CloudAPIBackend(api_type, api_base, api_key, api_model)
|
|
110
|
+
else:
|
|
111
|
+
# Fallback to Ollama if no API key is provided and use_ollama is false
|
|
112
|
+
self.explainer_backend = OllamaBackend(model=self.ollama_model)
|
|
113
|
+
|
|
114
|
+
self.explainer = LearningExplainer(self.explainer_backend)
|
|
115
|
+
|
|
116
|
+
self.learning_engine = LearningModeEngine(self.registry, self.executor, self.explainer)
|
|
117
|
+
self.power_engine = PowerModeEngine(self.registry, self.executor)
|
|
118
|
+
self.power_facade = PowerWorkspaceFacade(self.store, self.executor, self.registry)
|
|
119
|
+
self.power_facade.user_env = self.user_env
|
|
120
|
+
self.power_facade.user_shell = self.user_shell
|
|
121
|
+
|
|
122
|
+
# 2. Setup UI
|
|
123
|
+
self.push_screen("dashboard")
|
|
124
|
+
self.animations_enabled = True
|
|
125
|
+
|
|
126
|
+
async def on_unmount(self) -> None:
|
|
127
|
+
"""Cleanup resources before exit."""
|
|
128
|
+
if hasattr(self, "executor"):
|
|
129
|
+
await self.executor.shutdown()
|
|
130
|
+
if hasattr(self, "store"):
|
|
131
|
+
await self.store.close()
|
|
132
|
+
|
|
133
|
+
def action_toggle_animations(self) -> None:
|
|
134
|
+
"""Toggle UI animations on or off via the command palette."""
|
|
135
|
+
self.animations_enabled = not self.animations_enabled
|
|
136
|
+
if self.animations_enabled:
|
|
137
|
+
self.remove_class("no-animations")
|
|
138
|
+
self.notify("Animations Enabled")
|
|
139
|
+
else:
|
|
140
|
+
self.add_class("no-animations")
|
|
141
|
+
self.notify("Animations Disabled")
|
|
142
|
+
|
|
143
|
+
def get_system_commands(self, screen):
|
|
144
|
+
"""Add custom commands to the Command Palette (Ctrl+P)."""
|
|
145
|
+
yield from super().get_system_commands(screen)
|
|
146
|
+
yield SystemCommand(
|
|
147
|
+
"Toggle Animations",
|
|
148
|
+
"Turn sidebar animations on or off",
|
|
149
|
+
self.action_toggle_animations
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
if __name__ == "__main__":
|
|
153
|
+
app = CyGuideApp()
|
|
154
|
+
app.run()
|
|
File without changes
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Abstract base class for all tool plugins."""
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from typing import AsyncIterator, List, Dict, Any
|
|
5
|
+
from cyguide.schemas.base import BaseFinding
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ToolAdapter(ABC):
|
|
9
|
+
"""
|
|
10
|
+
Contract for tool contributors.
|
|
11
|
+
Every tool in tools/ must implement this in adapter.py.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
@abstractmethod
|
|
15
|
+
def validate_install(self) -> bool:
|
|
16
|
+
"""Check if the tool binary is available on the system."""
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
@abstractmethod
|
|
20
|
+
def build_command(self, target: BaseFinding, params: Dict[str, Any]) -> List[str]:
|
|
21
|
+
"""
|
|
22
|
+
Build the command line arguments for the tool.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
target: The finding being scanned (e.g. a network.host)
|
|
26
|
+
params: User-provided flags or options from the TUI
|
|
27
|
+
"""
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
@abstractmethod
|
|
31
|
+
async def parse_output(self, raw_stdout: str, target: BaseFinding, context: Dict[str, Any] = None) -> AsyncIterator[BaseFinding]:
|
|
32
|
+
"""
|
|
33
|
+
Parse tool output and yield standardized findings.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
raw_stdout: The raw output string from the tool
|
|
37
|
+
target: The original target finding (for context/parenting)
|
|
38
|
+
context: Optional dictionary for tracking state across lines (e.g. current host)
|
|
39
|
+
"""
|
|
40
|
+
pass
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# PROTECTED FILE — See CONTRIBUTING.md before modifying.
|
|
2
|
+
# Changes to this file require team agreement.
|
|
3
|
+
"""Normalization filters for finding data."""
|
|
4
|
+
|
|
5
|
+
import urllib.parse
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def canonicalize(value):
|
|
9
|
+
"""Apply normalization rules based on the type of value."""
|
|
10
|
+
if isinstance(value, str):
|
|
11
|
+
# General string cleaning
|
|
12
|
+
value = value.strip()
|
|
13
|
+
|
|
14
|
+
# Lowercase hostnames (heuristic: no spaces, contains dots or is localhost)
|
|
15
|
+
if "." in value or value.lower() == "localhost":
|
|
16
|
+
if " " not in value and ":" not in value:
|
|
17
|
+
return value.lower()
|
|
18
|
+
|
|
19
|
+
return value
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def canonicalize_host(ip_or_name: str) -> str:
|
|
23
|
+
"""Normalize IP addresses and hostnames."""
|
|
24
|
+
val = ip_or_name.strip().lower()
|
|
25
|
+
if val in ("localhost", "127.0.0.1", "::1"):
|
|
26
|
+
return "127.0.0.1"
|
|
27
|
+
# Basic IP normalization (lowercase IPv6, etc)
|
|
28
|
+
return val
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def canonicalize_url(url: str) -> str:
|
|
32
|
+
"""Normalize a URL: lowercase scheme/host, strip default ports."""
|
|
33
|
+
try:
|
|
34
|
+
parsed = urllib.parse.urlparse(url)
|
|
35
|
+
scheme = parsed.scheme.lower()
|
|
36
|
+
netloc = parsed.netloc.lower()
|
|
37
|
+
|
|
38
|
+
# Strip default ports
|
|
39
|
+
if scheme == "http" and netloc.endswith(":80"):
|
|
40
|
+
netloc = netloc[:-3]
|
|
41
|
+
elif scheme == "https" and netloc.endswith(":443"):
|
|
42
|
+
netloc = netloc[:-4]
|
|
43
|
+
|
|
44
|
+
return urllib.parse.urlunparse(
|
|
45
|
+
(scheme, netloc, parsed.path, parsed.params, parsed.query, parsed.fragment)
|
|
46
|
+
)
|
|
47
|
+
except Exception:
|
|
48
|
+
return url
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def canonicalize_port(port) -> int:
|
|
52
|
+
"""Ensure port is an integer."""
|
|
53
|
+
try:
|
|
54
|
+
return int(port)
|
|
55
|
+
except (ValueError, TypeError):
|
|
56
|
+
return 0
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
"""Environment and dependency diagnostic checks for CyGuide."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
import shutil
|
|
6
|
+
import platform
|
|
7
|
+
import asyncio
|
|
8
|
+
from typing import Optional, Dict, List, Any
|
|
9
|
+
from dataclasses import dataclass, field, asdict
|
|
10
|
+
from datetime import datetime, UTC
|
|
11
|
+
|
|
12
|
+
import httpx
|
|
13
|
+
from cyguide.engine.registry import ToolRegistry
|
|
14
|
+
from cyguide.engine.store import GraphStore
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
INSTALLATION_HINTS: Dict[str, str] = {
|
|
18
|
+
"nmap": "sudo apt install nmap (or: brew install nmap)",
|
|
19
|
+
"gobuster": "sudo apt install gobuster (or: go install github.com/OJ/gobuster/v3@v3.6.0)",
|
|
20
|
+
"nikto": "sudo apt install nikto (or: brew install nikto)",
|
|
21
|
+
"subfinder": "go install -v github.com/projectdiscovery/subfinder/v2/cmd/subfinder@v2.6.6",
|
|
22
|
+
"whois": "sudo apt install whois (or: brew install whois)",
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class DiagnosticCheck:
|
|
28
|
+
name: str
|
|
29
|
+
category: str
|
|
30
|
+
status: str # "ok", "warning", "error", "info"
|
|
31
|
+
message: str
|
|
32
|
+
details: Optional[Dict[str, Any]] = None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class DiagnosticReport:
|
|
37
|
+
timestamp: str = field(default_factory=lambda: datetime.now(UTC).isoformat())
|
|
38
|
+
system_info: Dict[str, Any] = field(default_factory=dict)
|
|
39
|
+
checks: List[DiagnosticCheck] = field(default_factory=list)
|
|
40
|
+
healthy: bool = True
|
|
41
|
+
tool_counts: Dict[str, int] = field(default_factory=dict)
|
|
42
|
+
|
|
43
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
44
|
+
return {
|
|
45
|
+
"timestamp": self.timestamp,
|
|
46
|
+
"system_info": self.system_info,
|
|
47
|
+
"healthy": self.healthy,
|
|
48
|
+
"tool_counts": self.tool_counts,
|
|
49
|
+
"checks": [asdict(c) for c in self.checks]
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
async def run_diagnostics(
|
|
54
|
+
db_path: str = "data/cyguide.db",
|
|
55
|
+
tools_dir: Optional[str] = None
|
|
56
|
+
) -> DiagnosticReport:
|
|
57
|
+
"""Run all system, database, tool dependency, and explainer checks."""
|
|
58
|
+
report = DiagnosticReport()
|
|
59
|
+
|
|
60
|
+
# 1. System Environment
|
|
61
|
+
py_version = sys.version_info
|
|
62
|
+
py_version_str = f"{py_version.major}.{py_version.minor}.{py_version.micro}"
|
|
63
|
+
report.system_info = {
|
|
64
|
+
"python_version": py_version_str,
|
|
65
|
+
"platform": platform.platform(),
|
|
66
|
+
"executable": sys.executable,
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if py_version >= (3, 11):
|
|
70
|
+
report.checks.append(DiagnosticCheck(
|
|
71
|
+
name="Python Version",
|
|
72
|
+
category="System",
|
|
73
|
+
status="ok",
|
|
74
|
+
message=f"Python {py_version_str} (>= 3.11 requirement satisfied)"
|
|
75
|
+
))
|
|
76
|
+
else:
|
|
77
|
+
report.checks.append(DiagnosticCheck(
|
|
78
|
+
name="Python Version",
|
|
79
|
+
category="System",
|
|
80
|
+
status="error",
|
|
81
|
+
message=f"Python {py_version_str} is below minimum requirement 3.11"
|
|
82
|
+
))
|
|
83
|
+
report.healthy = False
|
|
84
|
+
|
|
85
|
+
# Textual dependency check
|
|
86
|
+
try:
|
|
87
|
+
import textual
|
|
88
|
+
report.checks.append(DiagnosticCheck(
|
|
89
|
+
name="Textual TUI Framework",
|
|
90
|
+
category="System",
|
|
91
|
+
status="ok",
|
|
92
|
+
message=f"Textual v{textual.__version__} installed"
|
|
93
|
+
))
|
|
94
|
+
except Exception as e:
|
|
95
|
+
report.checks.append(DiagnosticCheck(
|
|
96
|
+
name="Textual TUI Framework",
|
|
97
|
+
category="System",
|
|
98
|
+
status="error",
|
|
99
|
+
message=f"Failed to import textual: {str(e)}"
|
|
100
|
+
))
|
|
101
|
+
report.healthy = False
|
|
102
|
+
|
|
103
|
+
# 2. Storage & SQLite Database Health
|
|
104
|
+
db_dir = os.path.dirname(os.path.abspath(db_path)) if db_path != ":memory:" else None
|
|
105
|
+
if db_dir:
|
|
106
|
+
os.makedirs(db_dir, exist_ok=True)
|
|
107
|
+
is_writable = os.access(db_dir, os.W_OK)
|
|
108
|
+
if is_writable:
|
|
109
|
+
report.checks.append(DiagnosticCheck(
|
|
110
|
+
name="Storage Directory",
|
|
111
|
+
category="Database",
|
|
112
|
+
status="ok",
|
|
113
|
+
message=f"Directory '{db_dir}' is writable"
|
|
114
|
+
))
|
|
115
|
+
else:
|
|
116
|
+
report.checks.append(DiagnosticCheck(
|
|
117
|
+
name="Storage Directory",
|
|
118
|
+
category="Database",
|
|
119
|
+
status="error",
|
|
120
|
+
message=f"Directory '{db_dir}' is not writable"
|
|
121
|
+
))
|
|
122
|
+
report.healthy = False
|
|
123
|
+
|
|
124
|
+
try:
|
|
125
|
+
store = GraphStore(db_path)
|
|
126
|
+
await store.initialize()
|
|
127
|
+
|
|
128
|
+
async with store._get_conn() as db:
|
|
129
|
+
cursor = await db.execute("PRAGMA integrity_check")
|
|
130
|
+
row = await cursor.fetchone()
|
|
131
|
+
integrity_status = row[0] if row else "unknown"
|
|
132
|
+
|
|
133
|
+
cursor = await db.execute("SELECT name FROM sqlite_master WHERE type='table'")
|
|
134
|
+
tables = [r[0] for r in await cursor.fetchall()]
|
|
135
|
+
|
|
136
|
+
await store.close()
|
|
137
|
+
|
|
138
|
+
if integrity_status == "ok":
|
|
139
|
+
report.checks.append(DiagnosticCheck(
|
|
140
|
+
name="SQLite Integrity",
|
|
141
|
+
category="Database",
|
|
142
|
+
status="ok",
|
|
143
|
+
message=f"Database '{db_path}' integrity verified ({len(tables)} tables present)",
|
|
144
|
+
details={"tables": tables}
|
|
145
|
+
))
|
|
146
|
+
else:
|
|
147
|
+
report.checks.append(DiagnosticCheck(
|
|
148
|
+
name="SQLite Integrity",
|
|
149
|
+
category="Database",
|
|
150
|
+
status="error",
|
|
151
|
+
message=f"Database integrity check failed: {integrity_status}"
|
|
152
|
+
))
|
|
153
|
+
report.healthy = False
|
|
154
|
+
except Exception as e:
|
|
155
|
+
report.checks.append(DiagnosticCheck(
|
|
156
|
+
name="SQLite Database",
|
|
157
|
+
category="Database",
|
|
158
|
+
status="error",
|
|
159
|
+
message=f"Failed to connect to database '{db_path}': {str(e)}"
|
|
160
|
+
))
|
|
161
|
+
report.healthy = False
|
|
162
|
+
|
|
163
|
+
# 3. Security Tool Dependencies
|
|
164
|
+
registry = ToolRegistry(tools_dir=tools_dir)
|
|
165
|
+
registry.load_tools()
|
|
166
|
+
tool_names = registry.list_tools()
|
|
167
|
+
|
|
168
|
+
installed_count = 0
|
|
169
|
+
total_tools = len(tool_names)
|
|
170
|
+
|
|
171
|
+
for name in tool_names:
|
|
172
|
+
tool_data = registry.get_tool(name)
|
|
173
|
+
adapter = tool_data.get("adapter") if tool_data else None
|
|
174
|
+
manifest = tool_data.get("manifest") if tool_data else None
|
|
175
|
+
description = manifest.meta.description if manifest else ""
|
|
176
|
+
|
|
177
|
+
is_installed = False
|
|
178
|
+
binary_path = None
|
|
179
|
+
|
|
180
|
+
if adapter:
|
|
181
|
+
try:
|
|
182
|
+
is_installed = adapter.validate_install()
|
|
183
|
+
except Exception:
|
|
184
|
+
is_installed = False
|
|
185
|
+
|
|
186
|
+
binary_path = shutil.which(name)
|
|
187
|
+
|
|
188
|
+
if is_installed:
|
|
189
|
+
installed_count += 1
|
|
190
|
+
report.checks.append(DiagnosticCheck(
|
|
191
|
+
name=f"Tool: {name}",
|
|
192
|
+
category="Tools",
|
|
193
|
+
status="ok",
|
|
194
|
+
message=f"Installed at {binary_path or 'PATH'}",
|
|
195
|
+
details={"description": description, "binary": binary_path}
|
|
196
|
+
))
|
|
197
|
+
else:
|
|
198
|
+
hint = INSTALLATION_HINTS.get(name, f"Install '{name}' using your system package manager.")
|
|
199
|
+
report.checks.append(DiagnosticCheck(
|
|
200
|
+
name=f"Tool: {name}",
|
|
201
|
+
category="Tools",
|
|
202
|
+
status="warning",
|
|
203
|
+
message=f"Not found in PATH. Install hint: {hint}",
|
|
204
|
+
details={"description": description, "install_hint": hint}
|
|
205
|
+
))
|
|
206
|
+
|
|
207
|
+
report.tool_counts = {
|
|
208
|
+
"installed": installed_count,
|
|
209
|
+
"total": total_tools,
|
|
210
|
+
"missing": total_tools - installed_count
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
# 4. Explainer & AI Backends
|
|
214
|
+
# Ollama check
|
|
215
|
+
try:
|
|
216
|
+
async with httpx.AsyncClient(timeout=1.0) as client:
|
|
217
|
+
resp = await client.get("http://localhost:11434/api/tags")
|
|
218
|
+
if resp.status_code == 200:
|
|
219
|
+
models = [m.get("name") for m in resp.json().get("models", [])]
|
|
220
|
+
report.checks.append(DiagnosticCheck(
|
|
221
|
+
name="Ollama Local LLM",
|
|
222
|
+
category="Explainer",
|
|
223
|
+
status="ok",
|
|
224
|
+
message=f"Connected to local Ollama ({len(models)} models available: {', '.join(models[:3])})",
|
|
225
|
+
details={"models": models}
|
|
226
|
+
))
|
|
227
|
+
else:
|
|
228
|
+
report.checks.append(DiagnosticCheck(
|
|
229
|
+
name="Ollama Local LLM",
|
|
230
|
+
category="Explainer",
|
|
231
|
+
status="info",
|
|
232
|
+
message="Ollama returned non-200 status (optional backend)"
|
|
233
|
+
))
|
|
234
|
+
except Exception:
|
|
235
|
+
report.checks.append(DiagnosticCheck(
|
|
236
|
+
name="Ollama Local LLM",
|
|
237
|
+
category="Explainer",
|
|
238
|
+
status="info",
|
|
239
|
+
message="Ollama service is not running on http://localhost:11434 (optional backend)"
|
|
240
|
+
))
|
|
241
|
+
|
|
242
|
+
# Cloud API Keys check
|
|
243
|
+
if os.getenv("CYGUIDE_API_KEY"):
|
|
244
|
+
report.checks.append(DiagnosticCheck(
|
|
245
|
+
name="Cloud LLM Keys",
|
|
246
|
+
category="Explainer",
|
|
247
|
+
status="ok",
|
|
248
|
+
message=(
|
|
249
|
+
f"CYGUIDE_API_KEY set (type="
|
|
250
|
+
f"{os.getenv('CYGUIDE_API_TYPE', 'openai')}, "
|
|
251
|
+
f"model={os.getenv('CYGUIDE_API_MODEL', 'gpt-4o')})"
|
|
252
|
+
)
|
|
253
|
+
))
|
|
254
|
+
else:
|
|
255
|
+
cloud_keys = [
|
|
256
|
+
"GROQ_API_KEY",
|
|
257
|
+
"OPENAI_API_KEY",
|
|
258
|
+
"ANTHROPIC_API_KEY",
|
|
259
|
+
"OPENROUTER_API_KEY",
|
|
260
|
+
"GROK_API_KEY",
|
|
261
|
+
"GEMINI_API_KEY",
|
|
262
|
+
]
|
|
263
|
+
configured_keys = [k for k in cloud_keys if os.getenv(k)]
|
|
264
|
+
if configured_keys:
|
|
265
|
+
report.checks.append(DiagnosticCheck(
|
|
266
|
+
name="Cloud LLM Keys",
|
|
267
|
+
category="Explainer",
|
|
268
|
+
status="ok",
|
|
269
|
+
message=f"Configured API keys: {', '.join(configured_keys)}"
|
|
270
|
+
))
|
|
271
|
+
else:
|
|
272
|
+
report.checks.append(DiagnosticCheck(
|
|
273
|
+
name="Cloud LLM Keys",
|
|
274
|
+
category="Explainer",
|
|
275
|
+
status="info",
|
|
276
|
+
message="CYGUIDE_API_KEY is not set (optional backend)"
|
|
277
|
+
))
|
|
278
|
+
|
|
279
|
+
return report
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def format_terminal_report(report: DiagnosticReport) -> str:
|
|
283
|
+
"""Format the diagnostic report for terminal display."""
|
|
284
|
+
lines = []
|
|
285
|
+
lines.append("=" * 68)
|
|
286
|
+
lines.append(" 🩺 CYGUIDE SYSTEM & ENVIRONMENT DIAGNOSTICS")
|
|
287
|
+
lines.append("=" * 68)
|
|
288
|
+
lines.append(f" Timestamp : {report.timestamp}")
|
|
289
|
+
lines.append(f" Platform : {report.system_info.get('platform', 'unknown')}")
|
|
290
|
+
lines.append(f" Python : {report.system_info.get('python_version', 'unknown')} ({report.system_info.get('executable', '')})")
|
|
291
|
+
lines.append("-" * 68)
|
|
292
|
+
|
|
293
|
+
# Group by category
|
|
294
|
+
categories = ["System", "Database", "Tools", "Explainer"]
|
|
295
|
+
for cat in categories:
|
|
296
|
+
cat_checks = [c for c in report.checks if c.category == cat]
|
|
297
|
+
if not cat_checks:
|
|
298
|
+
continue
|
|
299
|
+
|
|
300
|
+
header_icon = {
|
|
301
|
+
"System": "🖥️ ",
|
|
302
|
+
"Database": "💾",
|
|
303
|
+
"Tools": "🛠️ ",
|
|
304
|
+
"Explainer": "🤖"
|
|
305
|
+
}.get(cat, "🔍")
|
|
306
|
+
|
|
307
|
+
lines.append(f"\n{header_icon} {cat.upper()}")
|
|
308
|
+
for c in cat_checks:
|
|
309
|
+
if c.status == "ok":
|
|
310
|
+
badge = "[✓]"
|
|
311
|
+
elif c.status == "warning":
|
|
312
|
+
badge = "[!]"
|
|
313
|
+
elif c.status == "error":
|
|
314
|
+
badge = "[✗]"
|
|
315
|
+
else:
|
|
316
|
+
badge = "[-]"
|
|
317
|
+
lines.append(f" {badge:<4} {c.name:<24} : {c.message}")
|
|
318
|
+
|
|
319
|
+
lines.append("\n" + "=" * 68)
|
|
320
|
+
tool_stat = f"{report.tool_counts.get('installed', 0)}/{report.tool_counts.get('total', 0)} security tools installed"
|
|
321
|
+
if report.healthy:
|
|
322
|
+
lines.append(f" STATUS: HEALTHY ({tool_stat})")
|
|
323
|
+
else:
|
|
324
|
+
lines.append(f" STATUS: ATTENTION REQUIRED ({tool_stat})")
|
|
325
|
+
lines.append("=" * 68)
|
|
326
|
+
|
|
327
|
+
return "\n".join(lines)
|