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,3 @@
1
+ """CentiCodeHelp - Dein Code. Besser. Sauber."""
2
+
3
+ __version__ = "0.2.0"
@@ -0,0 +1,150 @@
1
+ import os
2
+ import json
3
+ import time
4
+ import logging
5
+ from typing import Optional, Dict, Any, List
6
+ from pydantic import BaseModel, Field
7
+ from centicodehelp.core.config import load_config, centicodehelpConfig
8
+ from centicodehelp.ai.providers import SUPPORTED_PROVIDERS, get_provider
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+ class LLMResponse(BaseModel):
13
+ content: str
14
+ model: str
15
+ duration_seconds: float = 0.0
16
+ prompt_tokens: int = 0
17
+ completion_tokens: int = 0
18
+ total_tokens: int = 0
19
+ raw_response: Optional[Any] = None
20
+
21
+
22
+ class LLMClient:
23
+ """Unified client wrapping LiteLLM for all AI providers."""
24
+
25
+ def __init__(
26
+ self,
27
+ provider: Optional[str] = None,
28
+ model: Optional[str] = None,
29
+ api_key: Optional[str] = None,
30
+ api_base: Optional[str] = None,
31
+ config: Optional[centicodehelpConfig] = None,
32
+ ):
33
+ self.config = config or load_config()
34
+ self.provider_id = (provider or self.config.default_provider).lower()
35
+ self.provider_info = get_provider(self.provider_id)
36
+
37
+ # Resolve model name
38
+ if model:
39
+ self.model = model
40
+ elif self.provider_info:
41
+ self.model = self.provider_info.default_model
42
+ else:
43
+ self.model = self.config.default_model
44
+
45
+ # Determine full LiteLLM model identifier
46
+ self.litellm_model = self._resolve_litellm_model(self.provider_id, self.model)
47
+
48
+ # Resolve API Key
49
+ self.api_key = api_key or self._resolve_api_key(self.provider_id)
50
+
51
+ # Resolve API Base
52
+ self.api_base = api_base or self._resolve_api_base(self.provider_id)
53
+
54
+ def _resolve_litellm_model(self, provider_id: str, model: str) -> str:
55
+ """Resolve to litellm standard format (e.g. openai/gpt-4o, ollama/llama3.2)."""
56
+ if "/" in model:
57
+ return model
58
+
59
+ if self.provider_info:
60
+ for m in self.provider_info.models:
61
+ if m.name.lower() == model.lower():
62
+ return m.litellm_id
63
+
64
+ # Fallback convention: <provider>/<model>
65
+ return f"{provider_id}/{model}"
66
+
67
+ def _resolve_api_key(self, provider_id: str) -> Optional[str]:
68
+ if provider_id in self.config.api_keys:
69
+ return self.config.api_keys[provider_id]
70
+
71
+ if self.provider_info:
72
+ for env_var in self.provider_info.env_keys:
73
+ val = os.getenv(env_var)
74
+ if val:
75
+ return val
76
+
77
+ return os.getenv(f"{provider_id.upper()}_API_KEY")
78
+
79
+ def _resolve_api_base(self, provider_id: str) -> Optional[str]:
80
+ if provider_id in self.config.api_bases:
81
+ return self.config.api_bases[provider_id]
82
+
83
+ if provider_id == "ollama":
84
+ return os.getenv("OLLAMA_API_BASE") or self.config.ollama_base_url
85
+
86
+ if self.provider_info and self.provider_info.default_api_base:
87
+ return self.provider_info.default_api_base
88
+
89
+ return None
90
+
91
+ def complete_with_metadata(
92
+ self, prompt: str, system: Optional[str] = None, temperature: float = 0.2
93
+ ) -> LLMResponse:
94
+ """Send completion request via LiteLLM and return rich telemetry."""
95
+ try:
96
+ import litellm
97
+ litellm.suppress_debug_info = True
98
+ except ImportError:
99
+ raise RuntimeError("LiteLLM is not installed. Run 'pip install litellm' first.")
100
+
101
+ messages = []
102
+ if system:
103
+ messages.append({"role": "system", "content": system})
104
+ messages.append({"role": "user", "content": prompt})
105
+
106
+ kwargs: Dict[str, Any] = {
107
+ "model": self.litellm_model,
108
+ "messages": messages,
109
+ "temperature": temperature,
110
+ }
111
+
112
+ if self.api_key:
113
+ kwargs["api_key"] = self.api_key
114
+ if self.api_base:
115
+ kwargs["api_base"] = self.api_base
116
+
117
+ start_time = time.time()
118
+ try:
119
+ response = litellm.completion(**kwargs)
120
+ duration = round(time.time() - start_time, 2)
121
+
122
+ content = response.choices[0].message.content or ""
123
+ usage = getattr(response, "usage", None)
124
+
125
+ p_tokens = getattr(usage, "prompt_tokens", 0) if usage else 0
126
+ c_tokens = getattr(usage, "completion_tokens", 0) if usage else 0
127
+ t_tokens = getattr(usage, "total_tokens", 0) if usage else (p_tokens + c_tokens)
128
+
129
+ return LLMResponse(
130
+ content=content,
131
+ model=self.litellm_model,
132
+ duration_seconds=duration,
133
+ prompt_tokens=p_tokens,
134
+ completion_tokens=c_tokens,
135
+ total_tokens=t_tokens,
136
+ raw_response=response,
137
+ )
138
+ except Exception as e:
139
+ err_str = str(e)
140
+ if self.provider_id == "ollama" and ("Connection" in err_str or "10061" in err_str or "ConnectError" in err_str):
141
+ raise RuntimeError(
142
+ "❌ Ollama läuft nicht. Starte es mit 'ollama serve' oder nutze 'ct setup' für einen Cloud-Anbieter."
143
+ ) from e
144
+ logger.error(f"LiteLLM completion error for model {self.litellm_model}: {e}")
145
+ raise
146
+
147
+ def complete(self, prompt: str, system: Optional[str] = None, temperature: float = 0.2) -> str:
148
+ """Backward-compatible complete method returning pure text."""
149
+ return self.complete_with_metadata(prompt, system=system, temperature=temperature).content
150
+
@@ -0,0 +1,63 @@
1
+ """Prompt templates for AI-assisted code review and auto-fixing."""
2
+
3
+ REVIEW_PROMPT_TEMPLATE = """You are an expert static analysis and code security auditor.
4
+ Analyze the following source code file and the static analysis findings below.
5
+ Your task:
6
+ 1. Validate whether the reported static findings are true positives.
7
+ 2. Identify any subtle logic bugs, unhandled edge cases, performance issues, or security flaws that were missed.
8
+ 3. Suggest concrete fixes for each issue.
9
+
10
+ File: {file_path}
11
+ Language: {language}
12
+
13
+ --- STATIC ANALYSIS FINDINGS ---
14
+ {static_findings}
15
+
16
+ --- SOURCE CODE ---
17
+ ```{language}
18
+ {source_code}
19
+ ```
20
+
21
+ Format your response strictly as a JSON object with this structure:
22
+ {{
23
+ "summary": "Brief summary of code health and key problems",
24
+ "issues": [
25
+ {{
26
+ "line": <line_number_or_null>,
27
+ "severity": "INFO" | "LOW" | "MEDIUM" | "HIGH" | "CRITICAL",
28
+ "rule_id": "<rule_identifier>",
29
+ "description": "<detailed problem description>",
30
+ "suggested_fix": "<how to fix it>"
31
+ }}
32
+ ]
33
+ }}
34
+ Only output valid JSON. Do not wrap in extra prose.
35
+ """
36
+
37
+ FIX_SYSTEM_PROMPT = """Du bist ein erstklassiger Code-Sicherheitsexperte und Entwickler. Deine Aufgabe ist es, den fehlerhaften Quellcode strikt nach Sicherheits- und Qualitätsregeln zu reparieren. Gib ausschließlich den fertig korrigierten Quellcode zurück."""
38
+
39
+ FIX_PROMPT_TEMPLATE = """Du bist ein Sicherheitsexperte. Behebe die folgenden Probleme im Code:
40
+ 1. SEC001: Ersetze hardcodierte Secrets durch os.environ.get().
41
+ 2. SEC101: Ersetze eval() IMMER durch ast.literal_eval(). Wenn das nicht möglich ist, entferne eval() komplett und wirf einen Fehler (ValueError). eval() darf unter KEINEN Umständen mehr im Code vorkommen! Stelle sicher, dass 'import ast' vorhanden ist.
42
+ 3. DEBUG002: Entferne ALLE print()-Debug-Statements KOMPLETT. Nicht auskommentieren, nicht ersetzen – LÖSCHEN. Die Zeile darf im Ergebnis nicht mehr vorkommen.
43
+ 4. ERR001/ERR003: Ersetze bare except durch except Exception as e.
44
+ 5. QUAL001: Implementiere die fehlende Logik (z. B. Validierung für negative Preise mit ValueError) und entferne das TODO.
45
+
46
+ WICHTIGE ANWEISUNGEN:
47
+ - Der Aufruf 'eval(...)' ist STRENG VERBOTEN. Verwende ausschließlich 'ast.literal_eval(...)'.
48
+ - Debug-Prints mit 'DEBUG' MÜSSEN komplett gelöscht werden.
49
+ - Gib NUR den korrigierten Code in einem Code-Block ``` zurück. Keine Erklärungen, kein Begleittext.
50
+
51
+ Datei: {file_path}
52
+ Sprache: {language}
53
+
54
+ --- ZU BEHEBENDE PROBLEME ---
55
+ {findings_text}
56
+
57
+ --- ORIGINALER CODE ---
58
+ ```{language}
59
+ {source_code}
60
+ ```
61
+
62
+ Gib NUR den korrigierten Code zurück:
63
+ """
@@ -0,0 +1,195 @@
1
+ from typing import Dict, List, Optional
2
+ from pydantic import BaseModel, Field
3
+
4
+ class ModelInfo(BaseModel):
5
+ name: str
6
+ litellm_id: str
7
+ description: str
8
+ is_default: bool = False
9
+ context_window: Optional[int] = None
10
+
11
+
12
+ class ProviderInfo(BaseModel):
13
+ id: str
14
+ name: str
15
+ env_keys: List[str]
16
+ default_model: str
17
+ description: str
18
+ models: List[ModelInfo]
19
+ requires_api_base: bool = False
20
+ default_api_base: Optional[str] = None
21
+
22
+
23
+ SUPPORTED_PROVIDERS: Dict[str, ProviderInfo] = {
24
+ "openai": ProviderInfo(
25
+ id="openai",
26
+ name="OpenAI",
27
+ env_keys=["OPENAI_API_KEY"],
28
+ default_model="gpt-4o",
29
+ description="OpenAI GPT-4o, GPT-4o-mini, o1, o3 models",
30
+ models=[
31
+ ModelInfo(name="gpt-4o", litellm_id="openai/gpt-4o", description="Flagship high-intelligence model", is_default=True),
32
+ ModelInfo(name="gpt-4o-mini", litellm_id="openai/gpt-4o-mini", description="Fast and cost-effective model"),
33
+ ModelInfo(name="o1-preview", litellm_id="openai/o1-preview", description="Reasoning model for hard problems"),
34
+ ModelInfo(name="o3-mini", litellm_id="openai/o3-mini", description="High-speed reasoning model"),
35
+ ]
36
+ ),
37
+ "anthropic": ProviderInfo(
38
+ id="anthropic",
39
+ name="Anthropic Claude",
40
+ env_keys=["ANTHROPIC_API_KEY"],
41
+ default_model="claude-3-5-sonnet",
42
+ description="Anthropic Claude 3.5 Sonnet / Haiku series",
43
+ models=[
44
+ ModelInfo(name="claude-3-5-sonnet", litellm_id="anthropic/claude-3-5-sonnet-20241022", description="State-of-the-art coding and analysis", is_default=True),
45
+ ModelInfo(name="claude-3-5-haiku", litellm_id="anthropic/claude-3-5-haiku-20241022", description="High-speed code reviewer"),
46
+ ]
47
+ ),
48
+ "gemini": ProviderInfo(
49
+ id="gemini",
50
+ name="Google Gemini",
51
+ env_keys=["GEMINI_API_KEY", "GOOGLE_API_KEY"],
52
+ default_model="gemini-1.5-pro",
53
+ description="Google Gemini 1.5 Pro and Flash with large context windows",
54
+ models=[
55
+ ModelInfo(name="gemini-1.5-pro", litellm_id="gemini/gemini-1.5-pro", description="Multimodal deep code reasoning", is_default=True),
56
+ ModelInfo(name="gemini-1.5-flash", litellm_id="gemini/gemini-1.5-flash", description="Ultra-fast code auditor"),
57
+ ModelInfo(name="gemini-2.0-flash-exp", litellm_id="gemini/gemini-2.0-flash-exp", description="Next-gen experimental Gemini"),
58
+ ]
59
+ ),
60
+ "deepseek": ProviderInfo(
61
+ id="deepseek",
62
+ name="DeepSeek",
63
+ env_keys=["DEEPSEEK_API_KEY"],
64
+ default_model="deepseek-coder",
65
+ description="Specialized code and reasoning models",
66
+ models=[
67
+ ModelInfo(name="deepseek-coder", litellm_id="deepseek/deepseek-coder", description="DeepSeek Coder V2 model", is_default=True),
68
+ ModelInfo(name="deepseek-chat", litellm_id="deepseek/deepseek-chat", description="General DeepSeek V3 chat model"),
69
+ ModelInfo(name="deepseek-reasoner", litellm_id="deepseek/deepseek-reasoner", description="DeepSeek R1 reasoning model"),
70
+ ]
71
+ ),
72
+ "ollama": ProviderInfo(
73
+ id="ollama",
74
+ name="Ollama (Local)",
75
+ env_keys=[],
76
+ default_model="qwen2.5-coder:7b",
77
+ description="Locally run open models without cloud API keys",
78
+ requires_api_base=True,
79
+ default_api_base="http://localhost:11434",
80
+ models=[
81
+ ModelInfo(name="qwen2.5-coder:7b", litellm_id="ollama/qwen2.5-coder:7b", description="High performance open coding LLM", is_default=True),
82
+ ModelInfo(name="qwen2.5-coder", litellm_id="ollama/qwen2.5-coder:7b", description="High performance open coding LLM"),
83
+ ModelInfo(name="llama3.2", litellm_id="ollama/llama3.2", description="Meta Llama 3.2 lightweight local model"),
84
+ ModelInfo(name="deepseek-coder-v2", litellm_id="ollama/deepseek-coder-v2", description="Local DeepSeek coder model"),
85
+ ModelInfo(name="mistral", litellm_id="ollama/mistral", description="Mistral 7B local model"),
86
+ ]
87
+ ),
88
+ "openrouter": ProviderInfo(
89
+ id="openrouter",
90
+ name="OpenRouter",
91
+ env_keys=["OPENROUTER_API_KEY"],
92
+ default_model="anthropic/claude-3.5-sonnet",
93
+ description="Universal router for hundreds of models with unified credits",
94
+ models=[
95
+ ModelInfo(name="claude-3.5-sonnet", litellm_id="openrouter/anthropic/claude-3.5-sonnet", description="Claude 3.5 Sonnet via OpenRouter", is_default=True),
96
+ ModelInfo(name="deepseek-v3", litellm_id="openrouter/deepseek/deepseek-chat", description="DeepSeek V3 via OpenRouter"),
97
+ ModelInfo(name="meta-llama-3.3-70b", litellm_id="openrouter/meta-llama/llama-3.3-70b-instruct", description="Llama 3.3 70B via OpenRouter"),
98
+ ]
99
+ ),
100
+ "groq": ProviderInfo(
101
+ id="groq",
102
+ name="Groq (LPU Inference)",
103
+ env_keys=["GROQ_API_KEY"],
104
+ default_model="llama-3.3-70b-versatile",
105
+ description="Ultra-fast LPU hardware accelerated inference",
106
+ models=[
107
+ ModelInfo(name="llama-3.3-70b-versatile", litellm_id="groq/llama-3.3-70b-versatile", description="Fast Llama 3.3 70B inference", is_default=True),
108
+ ModelInfo(name="mixtral-8x7b-32768", litellm_id="groq/mixtral-8x7b-32768", description="Mixtral 8x7B high speed"),
109
+ ]
110
+ ),
111
+ "mistral": ProviderInfo(
112
+ id="mistral",
113
+ name="Mistral AI",
114
+ env_keys=["MISTRAL_API_KEY"],
115
+ default_model="codestral-latest",
116
+ description="Mistral AI European enterprise models & Codestral",
117
+ models=[
118
+ ModelInfo(name="codestral-latest", litellm_id="mistral/codestral-latest", description="Dedicated code generation/fixing model", is_default=True),
119
+ ModelInfo(name="mistral-large-latest", litellm_id="mistral/mistral-large-latest", description="Mistral flagship model"),
120
+ ]
121
+ ),
122
+ "cohere": ProviderInfo(
123
+ id="cohere",
124
+ name="Cohere",
125
+ env_keys=["COHERE_API_KEY"],
126
+ default_model="command-r-plus",
127
+ description="Cohere Command R / Command R+ models",
128
+ models=[
129
+ ModelInfo(name="command-r-plus", litellm_id="cohere/command-r-plus", description="Enterprise reasoning and search", is_default=True),
130
+ ModelInfo(name="command-r", litellm_id="cohere/command-r", description="Fast Command R model"),
131
+ ]
132
+ ),
133
+ "xai": ProviderInfo(
134
+ id="xai",
135
+ name="xAI (Grok)",
136
+ env_keys=["XAI_API_KEY"],
137
+ default_model="grok-beta",
138
+ description="xAI Grok models",
139
+ models=[
140
+ ModelInfo(name="grok-beta", litellm_id="xai/grok-beta", description="xAI Grok Beta LLM", is_default=True),
141
+ ModelInfo(name="grok-2-latest", litellm_id="xai/grok-2-latest", description="xAI Grok 2 reasoning"),
142
+ ]
143
+ ),
144
+ "together": ProviderInfo(
145
+ id="together",
146
+ name="Together AI",
147
+ env_keys=["TOGETHERAI_API_KEY", "TOGETHER_API_KEY"],
148
+ default_model="meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo",
149
+ description="Together AI cloud inference for open weights",
150
+ models=[
151
+ ModelInfo(name="llama-3.1-70b-turbo", litellm_id="together_ai/meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo", description="Llama 3.1 70B Turbo", is_default=True),
152
+ ModelInfo(name="qwen-2.5-coder-32b", litellm_id="together_ai/Qwen/Qwen2.5-Coder-32B-Instruct", description="Qwen 2.5 Coder 32B Turbo"),
153
+ ]
154
+ ),
155
+ "perplexity": ProviderInfo(
156
+ id="perplexity",
157
+ name="Perplexity AI",
158
+ env_keys=["PERPLEXITYAI_API_KEY", "PPLX_API_KEY"],
159
+ default_model="sonar-pro",
160
+ description="Perplexity Sonar online search models",
161
+ models=[
162
+ ModelInfo(name="sonar-pro", litellm_id="perplexity/sonar-pro", description="Sonar Pro online search model", is_default=True),
163
+ ModelInfo(name="sonar", litellm_id="perplexity/sonar", description="Standard Sonar search model"),
164
+ ]
165
+ ),
166
+ "azure": ProviderInfo(
167
+ id="azure",
168
+ name="Azure OpenAI",
169
+ env_keys=["AZURE_API_KEY"],
170
+ default_model="azure-gpt-4o",
171
+ description="Microsoft Azure OpenAI enterprise deployments",
172
+ requires_api_base=True,
173
+ models=[
174
+ ModelInfo(name="azure-gpt-4o", litellm_id="azure/gpt-4o", description="Azure deployed GPT-4o", is_default=True),
175
+ ]
176
+ ),
177
+ "bedrock": ProviderInfo(
178
+ id="bedrock",
179
+ name="AWS Bedrock",
180
+ env_keys=["AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY"],
181
+ default_model="anthropic.claude-3-5-sonnet",
182
+ description="Amazon Web Services Bedrock foundation models",
183
+ models=[
184
+ ModelInfo(name="claude-3-5-sonnet", litellm_id="bedrock/anthropic.claude-3-5-sonnet-20241022-v2:0", description="Claude 3.5 Sonnet on AWS Bedrock", is_default=True),
185
+ ]
186
+ ),
187
+ }
188
+
189
+
190
+ def get_provider(provider_id: str) -> Optional[ProviderInfo]:
191
+ return SUPPORTED_PROVIDERS.get(provider_id.lower())
192
+
193
+
194
+ def list_providers() -> List[ProviderInfo]:
195
+ return list(SUPPORTED_PROVIDERS.values())
@@ -0,0 +1,139 @@
1
+ import json
2
+ import logging
3
+ from pathlib import Path
4
+ from typing import List, Tuple, Optional
5
+ from pydantic import BaseModel, Field
6
+ from centicodehelp.core.models import Finding, Severity
7
+ from centicodehelp.ai.llm_client import LLMClient, LLMResponse
8
+ from centicodehelp.ai.prompts import REVIEW_PROMPT_TEMPLATE
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+ class AIReviewResult(BaseModel):
13
+ findings: List[Finding] = Field(default_factory=list)
14
+ summary: str = ""
15
+ raw_response: str = ""
16
+ model: str = ""
17
+ provider: str = ""
18
+ duration_seconds: float = 0.0
19
+ prompt_tokens: int = 0
20
+ completion_tokens: int = 0
21
+ total_tokens: int = 0
22
+ error: Optional[str] = None
23
+
24
+
25
+ class AIReviewer:
26
+ """Combines static findings with LLM reasoning for deep analysis."""
27
+
28
+ def __init__(self, llm_client: LLMClient):
29
+ self.llm = llm_client
30
+
31
+ def review_with_details(
32
+ self, file_path: Path, language: str, static_findings: List[Finding]
33
+ ) -> AIReviewResult:
34
+ """Run AI review and return structured telemetry and findings."""
35
+ result = AIReviewResult(
36
+ model=self.llm.model,
37
+ provider=self.llm.provider_id,
38
+ )
39
+
40
+ if not file_path.exists():
41
+ result.error = f"Datei {file_path} existiert nicht."
42
+ return result
43
+
44
+ try:
45
+ source_code = file_path.read_text(encoding="utf-8", errors="replace")
46
+ except Exception as e:
47
+ result.error = f"Fehler beim Lesen der Datei: {e}"
48
+ return result
49
+
50
+ # Format static findings for prompt
51
+ findings_str = "\n".join([
52
+ f"- Line {f.line or '?'}: [{f.rule_id}] ({f.severity.value}) {f.description}"
53
+ for f in static_findings
54
+ ]) or "Keine statischen Regelverletzungen gefunden."
55
+
56
+ prompt = REVIEW_PROMPT_TEMPLATE.format(
57
+ file_path=str(file_path),
58
+ language=language,
59
+ static_findings=findings_str,
60
+ source_code=source_code,
61
+ )
62
+
63
+ try:
64
+ resp: LLMResponse = self.llm.complete_with_metadata(
65
+ prompt, system="Du bist ein erfahrener Sicherheits- und Code-Qualitäts-Auditor."
66
+ )
67
+ result.raw_response = resp.content
68
+ result.duration_seconds = resp.duration_seconds
69
+ result.prompt_tokens = resp.prompt_tokens
70
+ result.completion_tokens = resp.completion_tokens
71
+ result.total_tokens = resp.total_tokens
72
+
73
+ ai_findings, summary = self._parse_ai_response(resp.content, str(file_path), language)
74
+ result.findings = ai_findings
75
+ result.summary = summary or "KI-Codeanalyse abgeschlossen."
76
+ return result
77
+ except Exception as e:
78
+ result.error = str(e)
79
+ return result
80
+
81
+ def review(self, file_path: Path, language: str, static_findings: List[Finding]) -> List[Finding]:
82
+ """Backward compatible review method merging static and AI findings."""
83
+ res = self.review_with_details(file_path, language, static_findings)
84
+ if res.findings:
85
+ return self._merge_findings(static_findings, res.findings)
86
+ return static_findings
87
+
88
+ def _parse_ai_response(
89
+ self, response_text: str, file_path: str, language: str
90
+ ) -> Tuple[List[Finding], str]:
91
+ findings: List[Finding] = []
92
+ summary = ""
93
+ cleaned = response_text.strip()
94
+
95
+ # Strip markdown wrapping if model output was enclosed
96
+ if "```json" in cleaned:
97
+ parts = cleaned.split("```json")
98
+ if len(parts) > 1:
99
+ cleaned = parts[1].split("```")[0].strip()
100
+ elif cleaned.startswith("```"):
101
+ parts = cleaned.split("```")
102
+ if len(parts) > 1:
103
+ cleaned = parts[1].strip()
104
+
105
+ try:
106
+ data = json.loads(cleaned)
107
+ summary = str(data.get("summary", ""))
108
+ issues = data.get("issues", [])
109
+ for item in issues:
110
+ sev_str = str(item.get("severity", "MEDIUM")).upper()
111
+ sev = Severity.__members__.get(sev_str, Severity.MEDIUM)
112
+ rule = str(item.get("rule_id", "AI_ANALYSIS"))
113
+ desc = str(item.get("description", "Von KI identifiziertes Problem"))
114
+ fix = item.get("suggested_fix")
115
+ line = item.get("line")
116
+
117
+ findings.append(Finding(
118
+ file_path=file_path,
119
+ line=line,
120
+ rule_id=rule,
121
+ severity=sev,
122
+ description=desc,
123
+ suggested_fix=fix,
124
+ language=language,
125
+ ))
126
+ except Exception as e:
127
+ logger.debug(f"JSON parsing error from LLM response: {e}")
128
+ summary = cleaned[:200]
129
+
130
+ return findings, summary
131
+
132
+ def _merge_findings(self, static_findings: List[Finding], ai_findings: List[Finding]) -> List[Finding]:
133
+ merged = list(static_findings)
134
+ existing_rules = {(f.line, f.rule_id) for f in static_findings}
135
+ for af in ai_findings:
136
+ if (af.line, af.rule_id) not in existing_rules:
137
+ merged.append(af)
138
+ return merged
139
+
@@ -0,0 +1,124 @@
1
+ import math
2
+ from pathlib import Path
3
+ from typing import List, Dict, Any, Optional
4
+ from centicodehelp.core.models import Finding, Severity
5
+
6
+ class BinaryAnalyzer:
7
+ """Analyzer for PE, ELF, Mach-O binaries utilizing NusantaraScan or fallback heuristics."""
8
+
9
+ def __init__(self):
10
+ self.nusantara_available = self._check_nusantara()
11
+
12
+ def _check_nusantara(self) -> bool:
13
+ try:
14
+ import nusantarascan # type: ignore
15
+ return True
16
+ except ImportError:
17
+ return False
18
+
19
+ def analyze_binary(self, file_path: Path) -> List[Finding]:
20
+ findings: List[Finding] = []
21
+ if not file_path.exists():
22
+ return findings
23
+
24
+ # Run NusantaraScan if present
25
+ if self.nusantara_available:
26
+ try:
27
+ import nusantarascan # type: ignore
28
+ scanner = nusantarascan.Scanner()
29
+ result = scanner.scan_file(str(file_path))
30
+ # Map nusantara results to findings
31
+ if hasattr(result, "detections"):
32
+ for d in result.detections:
33
+ findings.append(Finding(
34
+ file_path=str(file_path),
35
+ rule_id=getattr(d, "rule_id", "BIN_NUSANTARA"),
36
+ severity=Severity.HIGH,
37
+ description=str(getattr(d, "description", "Suspicious binary signature detected")),
38
+ language="binary"
39
+ ))
40
+ return findings
41
+ except Exception:
42
+ pass
43
+
44
+ # Heuristic analysis: entropy, headers, suspicious strings
45
+ findings.extend(self._heuristic_scan(file_path))
46
+ return findings
47
+
48
+ def _heuristic_scan(self, file_path: Path) -> List[Finding]:
49
+ findings = []
50
+ try:
51
+ with open(file_path, "rb") as f:
52
+ data = f.read()
53
+
54
+ entropy = self._calculate_entropy(data)
55
+ if entropy > 7.2:
56
+ findings.append(Finding(
57
+ file_path=str(file_path),
58
+ rule_id="BIN_ENTROPY_HIGH",
59
+ severity=Severity.HIGH,
60
+ description=f"High Shannon entropy detected ({entropy:.2f} / 8.0). Binary is likely packed, compressed, or encrypted (UPX/Themida/Custom).",
61
+ suggested_fix="Unpack binary before performing deep disassembly or vulnerability scanning.",
62
+ language="binary"
63
+ ))
64
+
65
+ # Inspect common packers / debug strings
66
+ packer_signatures = [
67
+ (b"UPX0", "UPX packer section detected"),
68
+ (b"UPX1", "UPX packer section detected"),
69
+ (b".aspack", "Aspack packer signature detected"),
70
+ (b".themida", "Themida protector signature detected"),
71
+ ]
72
+ for sig, desc in packer_signatures:
73
+ if sig in data:
74
+ findings.append(Finding(
75
+ file_path=str(file_path),
76
+ rule_id="BIN_PACKER_DETECTED",
77
+ severity=Severity.MEDIUM,
78
+ description=desc,
79
+ suggested_fix="Use specific unpacker tool.",
80
+ language="binary"
81
+ ))
82
+
83
+ # Suspicious Windows API imports / strings
84
+ suspicious_apis = [
85
+ (b"VirtualProtect", "Dynamic memory permission change (VirtualProtect) - commonly used for code injection/shellcode execution"),
86
+ (b"CreateRemoteThread", "Process injection capability (CreateRemoteThread) found"),
87
+ (b"WriteProcessMemory", "Memory modification capability (WriteProcessMemory) found"),
88
+ (b"URLDownloadToFile", "Binary contains direct external download capability"),
89
+ ]
90
+ for api, desc in suspicious_apis:
91
+ if api in data:
92
+ findings.append(Finding(
93
+ file_path=str(file_path),
94
+ rule_id="BIN_SUSPICIOUS_API",
95
+ severity=Severity.LOW,
96
+ description=desc,
97
+ suggested_fix="Inspect cross-references in a disassembler (Ghidra / IDA Pro).",
98
+ language="binary"
99
+ ))
100
+
101
+ except Exception as e:
102
+ findings.append(Finding(
103
+ file_path=str(file_path),
104
+ rule_id="BIN_READ_ERR",
105
+ severity=Severity.INFO,
106
+ description=f"Error reading binary: {e}",
107
+ language="binary"
108
+ ))
109
+
110
+ return findings
111
+
112
+ def _calculate_entropy(self, data: bytes) -> float:
113
+ if not data:
114
+ return 0.0
115
+ entropy = 0.0
116
+ data_len = len(data)
117
+ freq: Dict[int, int] = {}
118
+ for b in data:
119
+ freq[b] = freq.get(b, 0) + 1
120
+ for count in freq.values():
121
+ p = count / data_len
122
+ entropy -= p * math.log2(p)
123
+ return entropy
124
+