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,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
|
+
|