centicodehelp 0.2.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.
- centicodehelp/__init__.py +3 -0
- centicodehelp/ai/llm_client.py +150 -0
- centicodehelp/ai/prompts.py +63 -0
- centicodehelp/ai/providers.py +195 -0
- centicodehelp/ai/reviewer.py +139 -0
- centicodehelp/analysis/binary_analyzer.py +124 -0
- centicodehelp/analysis/detector.py +80 -0
- centicodehelp/analysis/parser.py +41 -0
- centicodehelp/analysis/static_analyzer.py +194 -0
- centicodehelp/cli/banner.py +60 -0
- centicodehelp/cli/commands/ai.py +128 -0
- centicodehelp/cli/commands/analyze.py +389 -0
- centicodehelp/cli/commands/check.py +91 -0
- centicodehelp/cli/commands/config.py +67 -0
- centicodehelp/cli/commands/fix.py +170 -0
- centicodehelp/cli/commands/providers.py +81 -0
- centicodehelp/cli/commands/scan.py +129 -0
- centicodehelp/cli/commands/setup.py +93 -0
- centicodehelp/cli/commands/update.py +51 -0
- centicodehelp/cli/main.py +141 -0
- centicodehelp/cli/theme.py +50 -0
- centicodehelp/core/config.py +58 -0
- centicodehelp/core/models.py +31 -0
- centicodehelp/fixer/applier.py +23 -0
- centicodehelp/fixer/patcher.py +42 -0
- centicodehelp-0.2.0.dist-info/METADATA +79 -0
- centicodehelp-0.2.0.dist-info/RECORD +30 -0
- centicodehelp-0.2.0.dist-info/WHEEL +5 -0
- centicodehelp-0.2.0.dist-info/entry_points.txt +4 -0
- centicodehelp-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import Optional, Dict
|
|
3
|
+
|
|
4
|
+
EXTENSION_MAP: Dict[str, str] = {
|
|
5
|
+
".py": "python",
|
|
6
|
+
".js": "javascript",
|
|
7
|
+
".jsx": "javascript",
|
|
8
|
+
".ts": "typescript",
|
|
9
|
+
".tsx": "typescript",
|
|
10
|
+
".c": "c",
|
|
11
|
+
".h": "c",
|
|
12
|
+
".cpp": "cpp",
|
|
13
|
+
".hpp": "cpp",
|
|
14
|
+
".cc": "cpp",
|
|
15
|
+
".cxx": "cpp",
|
|
16
|
+
".cs": "csharp",
|
|
17
|
+
".java": "java",
|
|
18
|
+
".rs": "rust",
|
|
19
|
+
".go": "go",
|
|
20
|
+
".rb": "ruby",
|
|
21
|
+
".php": "php",
|
|
22
|
+
".sh": "bash",
|
|
23
|
+
".bash": "bash",
|
|
24
|
+
".ps1": "powershell",
|
|
25
|
+
".html": "html",
|
|
26
|
+
".css": "css",
|
|
27
|
+
".json": "json",
|
|
28
|
+
".yaml": "yaml",
|
|
29
|
+
".yml": "yaml",
|
|
30
|
+
".sql": "sql",
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
BINARY_EXTENSIONS = {
|
|
34
|
+
".exe", ".dll", ".so", ".dylib", ".bin", ".elf", ".sys", ".drv", ".ocx"
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
def is_binary_file(file_path: Path) -> bool:
|
|
38
|
+
"""Check if the given file is a binary executable/library."""
|
|
39
|
+
if file_path.suffix.lower() in BINARY_EXTENSIONS:
|
|
40
|
+
return True
|
|
41
|
+
|
|
42
|
+
# Check magic bytes if file exists
|
|
43
|
+
if file_path.exists() and file_path.is_file():
|
|
44
|
+
try:
|
|
45
|
+
with open(file_path, "rb") as f:
|
|
46
|
+
header = f.read(4)
|
|
47
|
+
if header.startswith(b"MZ"): # PE executable
|
|
48
|
+
return True
|
|
49
|
+
if header.startswith(b"\x7fELF"): # ELF executable
|
|
50
|
+
return True
|
|
51
|
+
if header in (b"\xfe\xed\xfa\xce", b"\xfe\xed\xfa\xcf", b"\xce\xfa\xed\xfe", b"\xcf\xfa\xed\xfe"): # Mach-O
|
|
52
|
+
return True
|
|
53
|
+
except Exception:
|
|
54
|
+
pass
|
|
55
|
+
|
|
56
|
+
return False
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def detect_language(file_path: Path) -> Optional[str]:
|
|
60
|
+
"""Detect programming language from file path extension or shebang."""
|
|
61
|
+
if is_binary_file(file_path):
|
|
62
|
+
return "binary"
|
|
63
|
+
|
|
64
|
+
ext = file_path.suffix.lower()
|
|
65
|
+
if ext in EXTENSION_MAP:
|
|
66
|
+
return EXTENSION_MAP[ext]
|
|
67
|
+
|
|
68
|
+
# Inspect first line for shebang
|
|
69
|
+
if file_path.exists() and file_path.is_file():
|
|
70
|
+
try:
|
|
71
|
+
with open(file_path, "r", encoding="utf-8", errors="ignore") as f:
|
|
72
|
+
first_line = f.readline()
|
|
73
|
+
if first_line.startswith("#!"):
|
|
74
|
+
for lang in ["python", "bash", "sh", "ruby", "perl", "node"]:
|
|
75
|
+
if lang in first_line.lower():
|
|
76
|
+
return lang
|
|
77
|
+
except Exception:
|
|
78
|
+
pass
|
|
79
|
+
|
|
80
|
+
return None
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Optional, Any
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
logger = logging.getLogger(__name__)
|
|
6
|
+
|
|
7
|
+
class CodeParser:
|
|
8
|
+
"""Tree-sitter and AST parser for multi-language code analysis."""
|
|
9
|
+
|
|
10
|
+
def __init__(self, language: str):
|
|
11
|
+
self.language = language.lower()
|
|
12
|
+
self.ts_parser = None
|
|
13
|
+
self.ts_language = None
|
|
14
|
+
self._init_parser()
|
|
15
|
+
|
|
16
|
+
def _init_parser(self):
|
|
17
|
+
try:
|
|
18
|
+
import tree_sitter
|
|
19
|
+
if self.language == "python":
|
|
20
|
+
import tree_sitter_python
|
|
21
|
+
from tree_sitter import Language, Parser
|
|
22
|
+
self.ts_language = Language(tree_sitter_python.language())
|
|
23
|
+
self.ts_parser = Parser(self.ts_language)
|
|
24
|
+
# Future language grammar bindings (c, cpp, java, csharp, etc.) can be loaded here
|
|
25
|
+
except Exception as e:
|
|
26
|
+
logger.debug(f"Tree-sitter parser init failed for {self.language}: {e}")
|
|
27
|
+
self.ts_parser = None
|
|
28
|
+
|
|
29
|
+
def parse_bytes(self, source_bytes: bytes) -> Optional[Any]:
|
|
30
|
+
if self.ts_parser:
|
|
31
|
+
try:
|
|
32
|
+
return self.ts_parser.parse(source_bytes)
|
|
33
|
+
except Exception as e:
|
|
34
|
+
logger.error(f"Tree-sitter parse error: {e}")
|
|
35
|
+
return None
|
|
36
|
+
|
|
37
|
+
def parse_file(self, file_path: Path) -> Optional[Any]:
|
|
38
|
+
if not file_path.exists():
|
|
39
|
+
return None
|
|
40
|
+
with open(file_path, "rb") as f:
|
|
41
|
+
return self.parse_bytes(f.read())
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import List, Optional
|
|
4
|
+
from centicodehelp.core.models import Finding, Severity
|
|
5
|
+
from centicodehelp.analysis.parser import CodeParser
|
|
6
|
+
|
|
7
|
+
class StaticAnalyzer:
|
|
8
|
+
"""Multi-language rule-based static analyzer with AST and pattern rules."""
|
|
9
|
+
|
|
10
|
+
def __init__(self):
|
|
11
|
+
pass
|
|
12
|
+
|
|
13
|
+
def analyze_file(self, file_path: Path, language: str) -> List[Finding]:
|
|
14
|
+
findings: List[Finding] = []
|
|
15
|
+
if not file_path.exists():
|
|
16
|
+
return findings
|
|
17
|
+
|
|
18
|
+
try:
|
|
19
|
+
content = file_path.read_text(encoding="utf-8", errors="replace")
|
|
20
|
+
except Exception:
|
|
21
|
+
return findings
|
|
22
|
+
|
|
23
|
+
lines = content.splitlines()
|
|
24
|
+
|
|
25
|
+
# Run language-agnostic & language-specific rules
|
|
26
|
+
findings.extend(self._check_hardcoded_secrets(lines, str(file_path), language))
|
|
27
|
+
findings.extend(self._check_debug_statements(lines, str(file_path), language))
|
|
28
|
+
findings.extend(self._check_empty_exception_blocks(lines, str(file_path), language))
|
|
29
|
+
findings.extend(self._check_insecure_functions(lines, str(file_path), language))
|
|
30
|
+
findings.extend(self._check_todo_placeholders(lines, str(file_path), language))
|
|
31
|
+
|
|
32
|
+
# AST analysis (Tree-sitter Python check)
|
|
33
|
+
if language == "python":
|
|
34
|
+
findings.extend(self._check_python_ast(content, str(file_path)))
|
|
35
|
+
|
|
36
|
+
return findings
|
|
37
|
+
|
|
38
|
+
def _check_hardcoded_secrets(self, lines: List[str], file_path: str, language: str) -> List[Finding]:
|
|
39
|
+
findings = []
|
|
40
|
+
patterns = [
|
|
41
|
+
(r'(?i)(api[_-]?(key|secret)|secret[_-]?key|auth[_-]?token|password)\s*[:=]\s*["\'][a-zA-Z0-9_\-]{8,}["\']', "SEC001", "Hardcoded API key or credential detected", Severity.CRITICAL),
|
|
42
|
+
(r'-----BEGIN\s+(RSA|OPENSSH|EC)?\s*PRIVATE\s+KEY-----', "SEC002", "Hardcoded private cryptographic key found", Severity.CRITICAL),
|
|
43
|
+
]
|
|
44
|
+
for idx, line in enumerate(lines, 1):
|
|
45
|
+
for pat, rule_id, desc, sev in patterns:
|
|
46
|
+
if re.search(pat, line):
|
|
47
|
+
findings.append(Finding(
|
|
48
|
+
file_path=file_path,
|
|
49
|
+
line=idx,
|
|
50
|
+
rule_id=rule_id,
|
|
51
|
+
severity=sev,
|
|
52
|
+
description=desc,
|
|
53
|
+
code_snippet=line.strip(),
|
|
54
|
+
suggested_fix="Store secrets in environment variables or a secure key vault.",
|
|
55
|
+
language=language
|
|
56
|
+
))
|
|
57
|
+
return findings
|
|
58
|
+
|
|
59
|
+
def _check_debug_statements(self, lines: List[str], file_path: str, language: str) -> List[Finding]:
|
|
60
|
+
findings = []
|
|
61
|
+
debug_patterns = {
|
|
62
|
+
"javascript": [(r'console\.(log|debug|info)\s*\(', "DEBUG001", "console.log left in production code")],
|
|
63
|
+
"typescript": [(r'console\.(log|debug|info)\s*\(', "DEBUG001", "console.log left in production code")],
|
|
64
|
+
"python": [(r'breakpoint\(\)|print\s*\(.*DEBUG.*\)', "DEBUG002", "Debug statement/breakpoint found")],
|
|
65
|
+
}
|
|
66
|
+
for pat, rule_id, desc in debug_patterns.get(language, []):
|
|
67
|
+
for idx, line in enumerate(lines, 1):
|
|
68
|
+
if re.search(pat, line):
|
|
69
|
+
findings.append(Finding(
|
|
70
|
+
file_path=file_path,
|
|
71
|
+
line=idx,
|
|
72
|
+
rule_id=rule_id,
|
|
73
|
+
severity=Severity.LOW,
|
|
74
|
+
description=desc,
|
|
75
|
+
code_snippet=line.strip(),
|
|
76
|
+
suggested_fix="Remove debugging calls or replace with proper logging framework.",
|
|
77
|
+
language=language
|
|
78
|
+
))
|
|
79
|
+
return findings
|
|
80
|
+
|
|
81
|
+
def _check_empty_exception_blocks(self, lines: List[str], file_path: str, language: str) -> List[Finding]:
|
|
82
|
+
findings = []
|
|
83
|
+
if language == "python":
|
|
84
|
+
for idx, line in enumerate(lines, 1):
|
|
85
|
+
# Pattern: except ... : followed by pass
|
|
86
|
+
if re.search(r'except(\s+\w+)?:\s*pass\b', line):
|
|
87
|
+
findings.append(Finding(
|
|
88
|
+
file_path=file_path,
|
|
89
|
+
line=idx,
|
|
90
|
+
rule_id=rule_id if (rule_id := "ERR001") else "ERR001",
|
|
91
|
+
severity=Severity.HIGH,
|
|
92
|
+
description="Silent exception suppression ('except: pass') masks errors",
|
|
93
|
+
code_snippet=line.strip(),
|
|
94
|
+
suggested_fix="Handle the exception or log error details.",
|
|
95
|
+
language=language
|
|
96
|
+
))
|
|
97
|
+
elif language in ("javascript", "typescript", "java", "csharp", "cpp"):
|
|
98
|
+
for idx, line in enumerate(lines, 1):
|
|
99
|
+
if re.search(r'catch\s*\([^)]*\)\s*\{\s*\}', line):
|
|
100
|
+
findings.append(Finding(
|
|
101
|
+
file_path=file_path,
|
|
102
|
+
line=idx,
|
|
103
|
+
rule_id="ERR002",
|
|
104
|
+
severity=Severity.HIGH,
|
|
105
|
+
description="Empty catch block silently swallowing exceptions",
|
|
106
|
+
code_snippet=line.strip(),
|
|
107
|
+
suggested_fix="Add error handling, recovery, or logging inside the catch block.",
|
|
108
|
+
language=language
|
|
109
|
+
))
|
|
110
|
+
return findings
|
|
111
|
+
|
|
112
|
+
def _check_insecure_functions(self, lines: List[str], file_path: str, language: str) -> List[Finding]:
|
|
113
|
+
findings = []
|
|
114
|
+
insecure = {
|
|
115
|
+
"python": [
|
|
116
|
+
(r'\beval\s*\(', "SEC101", "eval() is dangerous and allows arbitrary code execution", Severity.HIGH),
|
|
117
|
+
(r'\bexec\s*\(', "SEC102", "exec() is dangerous and allows arbitrary code execution", Severity.HIGH),
|
|
118
|
+
(r'subprocess\.(Popen|call|run)\(.*shell=True.*\)', "SEC103", "subprocess with shell=True is vulnerable to shell injection", Severity.HIGH),
|
|
119
|
+
],
|
|
120
|
+
"javascript": [
|
|
121
|
+
(r'\beval\s*\(', "SEC101", "eval() is dangerous and allows arbitrary code execution", Severity.HIGH),
|
|
122
|
+
],
|
|
123
|
+
"c": [
|
|
124
|
+
(r'\bgets\s*\(', "SEC201", "gets() is deprecated and vulnerable to buffer overflow", Severity.CRITICAL),
|
|
125
|
+
(r'\bstrcpy\s*\(', "SEC202", "strcpy() may cause buffer overflow; use strncpy_s or strlcpy", Severity.MEDIUM),
|
|
126
|
+
],
|
|
127
|
+
"cpp": [
|
|
128
|
+
(r'\bgets\s*\(', "SEC201", "gets() is deprecated and vulnerable to buffer overflow", Severity.CRITICAL),
|
|
129
|
+
(r'\bstrcpy\s*\(', "SEC202", "strcpy() may cause buffer overflow; use strncpy_s or std::string", Severity.MEDIUM),
|
|
130
|
+
]
|
|
131
|
+
}
|
|
132
|
+
for pat, rule_id, desc, sev in insecure.get(language, []):
|
|
133
|
+
for idx, line in enumerate(lines, 1):
|
|
134
|
+
if re.search(pat, line):
|
|
135
|
+
findings.append(Finding(
|
|
136
|
+
file_path=file_path,
|
|
137
|
+
line=idx,
|
|
138
|
+
rule_id=rule_id,
|
|
139
|
+
severity=sev,
|
|
140
|
+
description=desc,
|
|
141
|
+
code_snippet=line.strip(),
|
|
142
|
+
suggested_fix="Use safer alternative function or sanitize inputs.",
|
|
143
|
+
language=language
|
|
144
|
+
))
|
|
145
|
+
return findings
|
|
146
|
+
|
|
147
|
+
def _check_todo_placeholders(self, lines: List[str], file_path: str, language: str) -> List[Finding]:
|
|
148
|
+
findings = []
|
|
149
|
+
for idx, line in enumerate(lines, 1):
|
|
150
|
+
if re.search(r'(?i)\b(TODO|FIXME|XXX|HACK|WIP)\b', line):
|
|
151
|
+
findings.append(Finding(
|
|
152
|
+
file_path=file_path,
|
|
153
|
+
line=idx,
|
|
154
|
+
rule_id="QUAL001",
|
|
155
|
+
severity=Severity.INFO,
|
|
156
|
+
description=f"Incomplete code / unresolved marker found: {line.strip()}",
|
|
157
|
+
code_snippet=line.strip(),
|
|
158
|
+
suggested_fix="Complete implementation and remove TODO.",
|
|
159
|
+
language=language
|
|
160
|
+
))
|
|
161
|
+
return findings
|
|
162
|
+
|
|
163
|
+
def _check_python_ast(self, content: str, file_path: str) -> List[Finding]:
|
|
164
|
+
findings = []
|
|
165
|
+
try:
|
|
166
|
+
import ast
|
|
167
|
+
tree = ast.parse(content)
|
|
168
|
+
for node in ast.walk(tree):
|
|
169
|
+
# Check for bare except:
|
|
170
|
+
if isinstance(node, ast.ExceptHandler) and node.type is None:
|
|
171
|
+
findings.append(Finding(
|
|
172
|
+
file_path=file_path,
|
|
173
|
+
line=node.lineno,
|
|
174
|
+
rule_id="ERR003",
|
|
175
|
+
severity=Severity.MEDIUM,
|
|
176
|
+
description="Bare 'except:' catches SystemExit and KeyboardInterrupt. Use 'except Exception:'.",
|
|
177
|
+
code_snippet="except:",
|
|
178
|
+
suggested_fix="Change 'except:' to 'except Exception:'.",
|
|
179
|
+
language="python"
|
|
180
|
+
))
|
|
181
|
+
except SyntaxError as e:
|
|
182
|
+
findings.append(Finding(
|
|
183
|
+
file_path=file_path,
|
|
184
|
+
line=e.lineno,
|
|
185
|
+
rule_id="SYNTAX001",
|
|
186
|
+
severity=Severity.CRITICAL,
|
|
187
|
+
description=f"Syntax Error: {e.msg}",
|
|
188
|
+
suggested_fix="Fix syntax error.",
|
|
189
|
+
language="python"
|
|
190
|
+
))
|
|
191
|
+
except Exception:
|
|
192
|
+
pass
|
|
193
|
+
return findings
|
|
194
|
+
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""ASCII logo and brand banner generator with smooth gradients for CentiCodeHelp."""
|
|
2
|
+
|
|
3
|
+
from rich.text import Text
|
|
4
|
+
from rich.panel import Panel
|
|
5
|
+
from rich.align import Align
|
|
6
|
+
from centicodehelp import __version__
|
|
7
|
+
from centicodehelp.cli.theme import console
|
|
8
|
+
|
|
9
|
+
# Block letters for CENTICODEHELP
|
|
10
|
+
LOGO_LINES = [
|
|
11
|
+
" ██████╗███████╗███╗ ██╗████████╗██╗ ██████╗ ██████╗ ██████╗ ███████╗██╗ ██╗███████╗██╗ ██████╗ ",
|
|
12
|
+
" ██╔════╝██╔════╝████╗ ██║╚══██╔══╝██║██╔════╝██╔═══██╗██╔══██╗██╔════╝██║ ██║██╔════╝██║ ██╔══██╗",
|
|
13
|
+
" ██║ █████╗ ██╔██╗ ██║ ██║ ██║██║ ██║ ██║██║ ██║█████╗ ███████║█████╗ ██║ ██████╔╝",
|
|
14
|
+
" ██║ ██╔══╝ ██║╚██╗██║ ██║ ██║██║ ██║ ██║██║ ██║██╔══╝ ██╔══██║██╔══╝ ██║ ██╔═══╝ ",
|
|
15
|
+
" ╚██████╗███████╗██║ ╚████║ ██║ ██║╚██████╗╚██████╔╝██████╔╝███████╗██║ ██║███████╗███████╗██║ ",
|
|
16
|
+
" ╚═════╝╚══════╝╚═╝ ╚═══╝ ╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝╚══════╝╚══════╝╚═╝ ",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
def _interpolate_color(start_hex: str, end_hex: str, factor: float) -> str:
|
|
20
|
+
"""Linearly interpolate between two hex colors."""
|
|
21
|
+
s_r, s_g, s_b = int(start_hex[1:3], 16), int(start_hex[3:5], 16), int(start_hex[5:7], 16)
|
|
22
|
+
e_r, e_g, e_b = int(end_hex[1:3], 16), int(end_hex[3:5], 16), int(end_hex[5:7], 16)
|
|
23
|
+
|
|
24
|
+
r = int(s_r + factor * (e_r - s_r))
|
|
25
|
+
g = int(s_g + factor * (e_g - s_g))
|
|
26
|
+
b = int(s_b + factor * (e_b - s_b))
|
|
27
|
+
return f"#{r:02x}{g:02x}{b:02x}"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def get_gradient_logo_text() -> Text:
|
|
31
|
+
"""Generate the ASCII logo with horizontal Purple (#C084FC) to Blue (#38BDF8) gradient."""
|
|
32
|
+
combined_text = Text()
|
|
33
|
+
num_lines = len(LOGO_LINES)
|
|
34
|
+
|
|
35
|
+
for line_idx, line in enumerate(LOGO_LINES):
|
|
36
|
+
line_len = max(len(line), 1)
|
|
37
|
+
for char_idx, char in enumerate(line):
|
|
38
|
+
factor = (char_idx / line_len * 0.75) + (line_idx / num_lines * 0.25)
|
|
39
|
+
factor = max(0.0, min(1.0, factor))
|
|
40
|
+
color = _interpolate_color("#C084FC", "#38BDF8", factor)
|
|
41
|
+
combined_text.append(char, style=f"bold {color}")
|
|
42
|
+
combined_text.append("\n")
|
|
43
|
+
|
|
44
|
+
return combined_text
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def print_banner(include_slogan: bool = True):
|
|
48
|
+
"""Print the complete stylized CentiCodeHelp banner."""
|
|
49
|
+
logo = get_gradient_logo_text()
|
|
50
|
+
|
|
51
|
+
console.print(logo)
|
|
52
|
+
|
|
53
|
+
if include_slogan:
|
|
54
|
+
tagline = Text()
|
|
55
|
+
tagline.append(" ⚡ Dein Code. Besser. Sauber. ", style="bold #e0e0e0")
|
|
56
|
+
tagline.append("• ", style="dim white")
|
|
57
|
+
tagline.append(f"v{__version__} ", style="bold #818CF8")
|
|
58
|
+
tagline.append("• ", style="dim white")
|
|
59
|
+
tagline.append("centi09.de\n", style="italic #C084FC")
|
|
60
|
+
console.print(tagline)
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Optional, List
|
|
4
|
+
import click
|
|
5
|
+
from rich.table import Table
|
|
6
|
+
from rich.panel import Panel
|
|
7
|
+
|
|
8
|
+
from centicodehelp.core.models import Finding, Severity
|
|
9
|
+
from centicodehelp.core.config import load_config
|
|
10
|
+
from centicodehelp.analysis.detector import detect_language
|
|
11
|
+
from centicodehelp.analysis.static_analyzer import StaticAnalyzer
|
|
12
|
+
from centicodehelp.ai.llm_client import LLMClient
|
|
13
|
+
from centicodehelp.ai.reviewer import AIReviewer, AIReviewResult
|
|
14
|
+
from centicodehelp.cli.commands.scan import collect_files
|
|
15
|
+
from centicodehelp.cli.theme import (
|
|
16
|
+
console,
|
|
17
|
+
COLOR_PURPLE,
|
|
18
|
+
COLOR_BLUE,
|
|
19
|
+
COLOR_INDIGO,
|
|
20
|
+
COLOR_SUCCESS,
|
|
21
|
+
COLOR_ERROR,
|
|
22
|
+
COLOR_CRITICAL,
|
|
23
|
+
COLOR_HIGH,
|
|
24
|
+
COLOR_MEDIUM,
|
|
25
|
+
COLOR_LOW,
|
|
26
|
+
COLOR_INFO,
|
|
27
|
+
COLOR_TEXT,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
SEVERITY_STYLES = {
|
|
31
|
+
Severity.CRITICAL: (COLOR_CRITICAL, "🔴 CRITICAL"),
|
|
32
|
+
Severity.HIGH: (f"bold {COLOR_HIGH}", "🟠HIGH"),
|
|
33
|
+
Severity.MEDIUM: (f"bold {COLOR_MEDIUM}", "🟡 MEDIUM"),
|
|
34
|
+
Severity.LOW: (f"{COLOR_LOW}", "🔵 LOW"),
|
|
35
|
+
Severity.INFO: (f"{COLOR_INFO}", "⚪ INFO"),
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
@click.command(name="ai", help="Reine KI-Tiefenanalyse (ohne Code-Änderungen).")
|
|
39
|
+
@click.argument("path", default=".", type=click.Path(exists=False), required=False)
|
|
40
|
+
@click.option("--provider", default=None, help="KI-Anbieter (Standard: ollama).")
|
|
41
|
+
@click.option("--model", default=None, help="Modellname (Standard: llama3.2).")
|
|
42
|
+
def ai_cmd(path: str, provider: Optional[str], model: Optional[str]):
|
|
43
|
+
"""
|
|
44
|
+
ct ai [pfad] - KI-Codeanalyse mit Ollama (oder Cloud-Anbietern).
|
|
45
|
+
Untersucht Code semantisch auf Logikfehler und Sicherheitslücken.
|
|
46
|
+
"""
|
|
47
|
+
target = Path(path)
|
|
48
|
+
if not target.exists():
|
|
49
|
+
console.print(f"\n[bold {COLOR_ERROR}]⌠Pfad nicht gefunden:[/bold {COLOR_ERROR}] [white]{target}[/white]\n")
|
|
50
|
+
return
|
|
51
|
+
|
|
52
|
+
files = collect_files(target)
|
|
53
|
+
if not files:
|
|
54
|
+
console.print(f"\n[bold {COLOR_MEDIUM}]âš ï¸ Keine Dateien in {target} gefunden.[/bold {COLOR_MEDIUM}]\n")
|
|
55
|
+
return
|
|
56
|
+
|
|
57
|
+
config = load_config()
|
|
58
|
+
sel_provider = provider or config.default_provider
|
|
59
|
+
sel_model = model or config.default_model
|
|
60
|
+
|
|
61
|
+
console.print(
|
|
62
|
+
Panel(
|
|
63
|
+
f"[bold]Ziel:[/bold] [yellow]{target}[/yellow] ({len(files)} Datei(en))\n"
|
|
64
|
+
f"[bold]KI-Engine:[/bold] [bold {COLOR_PURPLE}]{sel_provider}/{sel_model}[/bold {COLOR_PURPLE}]",
|
|
65
|
+
title=f"[bold {COLOR_PURPLE}]🤖 centicodehelp KI-ANALYSE[/bold {COLOR_PURPLE}]",
|
|
66
|
+
border_style=COLOR_INDIGO,
|
|
67
|
+
expand=False,
|
|
68
|
+
)
|
|
69
|
+
)
|
|
70
|
+
console.print()
|
|
71
|
+
|
|
72
|
+
try:
|
|
73
|
+
client = LLMClient(provider=sel_provider, model=sel_model)
|
|
74
|
+
reviewer = AIReviewer(client)
|
|
75
|
+
except Exception as e:
|
|
76
|
+
console.print(f"[bold {COLOR_ERROR}]⌠{e}[/bold {COLOR_ERROR}]\n")
|
|
77
|
+
return
|
|
78
|
+
|
|
79
|
+
static_analyzer = StaticAnalyzer()
|
|
80
|
+
analyzed_count = 0
|
|
81
|
+
|
|
82
|
+
for f_path in files[:5]: # KI-Analyse für bis zu 5 Zieldateien
|
|
83
|
+
lang = detect_language(f_path)
|
|
84
|
+
if not lang or lang == "binary":
|
|
85
|
+
continue
|
|
86
|
+
|
|
87
|
+
analyzed_count += 1
|
|
88
|
+
static_findings = static_analyzer.analyze_file(f_path, lang)
|
|
89
|
+
|
|
90
|
+
with console.status(f"[bold {COLOR_PURPLE}]Analysiere {f_path.name} mit {sel_provider}/{sel_model}...[/bold {COLOR_PURPLE}]", spinner="dots12"):
|
|
91
|
+
res: AIReviewResult = reviewer.review_with_details(f_path, lang, static_findings)
|
|
92
|
+
|
|
93
|
+
if res.error:
|
|
94
|
+
console.print(f"[bold {COLOR_ERROR}]⌠Fehler bei {f_path.name}:[/bold {COLOR_ERROR}] {res.error}\n")
|
|
95
|
+
continue
|
|
96
|
+
|
|
97
|
+
lines = [
|
|
98
|
+
f"[bold white]Datei:[/bold white] [cyan]{f_path.name}[/cyan]",
|
|
99
|
+
f"[bold white]Zusammenfassung:[/bold white] {res.summary or 'Keine Auffälligkeiten.'}",
|
|
100
|
+
]
|
|
101
|
+
if res.findings:
|
|
102
|
+
lines.append(f"\n[bold {COLOR_PURPLE}]Erkannte Punkte ({len(res.findings)}):[/bold {COLOR_PURPLE}]")
|
|
103
|
+
for idx, issue in enumerate(res.findings, 1):
|
|
104
|
+
color, badge = SEVERITY_STYLES.get(issue.severity, ("white", issue.severity.value))
|
|
105
|
+
line_info = f"Zeile {issue.line}" if issue.line else "Allgemein"
|
|
106
|
+
lines.append(f" {idx}. [{color}]{badge}[/{color}] [cyan]({line_info})[/cyan]: {issue.description}")
|
|
107
|
+
if issue.suggested_fix:
|
|
108
|
+
lines.append(f" [dim italic]💡 Tipp: {issue.suggested_fix}[/dim italic]")
|
|
109
|
+
else:
|
|
110
|
+
lines.append(f"\n[bold {COLOR_SUCCESS}]✅ Keine zusätzlichen Probleme durch KI gefunden.[/bold {COLOR_SUCCESS}]")
|
|
111
|
+
|
|
112
|
+
lines.append(
|
|
113
|
+
f"\n[dim]Dauer: {res.duration_seconds}s • Tokens: {res.total_tokens} • Modell: {res.model}[/dim]"
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
console.print(
|
|
117
|
+
Panel(
|
|
118
|
+
"\n".join(lines),
|
|
119
|
+
title=f"[bold {COLOR_PURPLE}]🤖 KI-Review für {f_path.name}[/bold {COLOR_PURPLE}]",
|
|
120
|
+
border_style=COLOR_INDIGO,
|
|
121
|
+
expand=False,
|
|
122
|
+
)
|
|
123
|
+
)
|
|
124
|
+
console.print()
|
|
125
|
+
|
|
126
|
+
if analyzed_count == 0:
|
|
127
|
+
console.print(f"[dim]Keine unterstützten Quellcode-Dateien für KI-Analyse gefunden.[/dim]\n")
|
|
128
|
+
|