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.
@@ -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
+