codeguard-pro 0.3.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.
- agent_analyzer.py +291 -0
- autofix.py +146 -0
- cli.py +465 -0
- codeguard_pro-0.3.0.dist-info/METADATA +398 -0
- codeguard_pro-0.3.0.dist-info/RECORD +15 -0
- codeguard_pro-0.3.0.dist-info/WHEEL +5 -0
- codeguard_pro-0.3.0.dist-info/entry_points.txt +2 -0
- codeguard_pro-0.3.0.dist-info/top_level.txt +10 -0
- hook.py +107 -0
- learning_loop.py +168 -0
- secret_scanner.py +273 -0
- server.py +544 -0
- supply_chain.py +744 -0
- tools_review.py +283 -0
- tools_security.py +668 -0
agent_analyzer.py
ADDED
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
"""AI-powered deep security analysis [BETA] — MiniMax 2.7 direct API.
|
|
2
|
+
Cost: ~$0.02 per scan. Regex does fast pass, LLM catches taint flow + obfuscation.
|
|
3
|
+
"""
|
|
4
|
+
import json, os, re, requests
|
|
5
|
+
from typing import Dict, List, Optional
|
|
6
|
+
from tools_security import scan_security
|
|
7
|
+
|
|
8
|
+
MINIMAX_HOST = os.environ.get("MINIMAX_API_HOST", "https://api.minimax.io").rstrip("/")
|
|
9
|
+
MINIMAX_URL = f"{MINIMAX_HOST}/v1/text/chatcompletion_v2"
|
|
10
|
+
MODEL = "MiniMax-M2.7"
|
|
11
|
+
_LAST_PROVIDER_ERROR = ""
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _load_api_key() -> Optional[str]:
|
|
15
|
+
"""Load MiniMax API key with a legacy fallback env var."""
|
|
16
|
+
return os.environ.get("MINIMAX_API_KEY") or os.environ.get("OPENROUTER_API_KEY")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _set_provider_error(message: str) -> None:
|
|
20
|
+
global _LAST_PROVIDER_ERROR
|
|
21
|
+
_LAST_PROVIDER_ERROR = message
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _get_provider_error() -> str:
|
|
25
|
+
return _LAST_PROVIDER_ERROR or "Provider error"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _missing_key_message(default_message: str) -> str:
|
|
29
|
+
"""Prefer a concrete provider error if one exists; otherwise show missing-key guidance."""
|
|
30
|
+
return _get_provider_error() if _LAST_PROVIDER_ERROR else default_message
|
|
31
|
+
|
|
32
|
+
def _call_minimax(system: str, user: str, json_mode: bool = True) -> Optional[str]:
|
|
33
|
+
api_key = _load_api_key()
|
|
34
|
+
if not api_key:
|
|
35
|
+
_set_provider_error("Missing MINIMAX_API_KEY")
|
|
36
|
+
return None
|
|
37
|
+
_set_provider_error("")
|
|
38
|
+
payload = {"model": MODEL, "messages": [{"role": "system", "content": system}, {"role": "user", "content": user}], "temperature": 0.1, "max_tokens": 4096}
|
|
39
|
+
try:
|
|
40
|
+
resp = requests.post(MINIMAX_URL, headers={"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}, json=payload, timeout=30)
|
|
41
|
+
if resp.status_code != 200:
|
|
42
|
+
body = resp.text[:300].replace("\n", " ").strip()
|
|
43
|
+
_set_provider_error(f"MiniMax API returned {resp.status_code}: {body}")
|
|
44
|
+
return None
|
|
45
|
+
data = resp.json()
|
|
46
|
+
try:
|
|
47
|
+
return data["choices"][0]["message"]["content"]
|
|
48
|
+
except Exception:
|
|
49
|
+
_set_provider_error(f"MiniMax response missing content: {json.dumps(data)[:300]}")
|
|
50
|
+
return None
|
|
51
|
+
except Exception as e:
|
|
52
|
+
_set_provider_error(f"MiniMax request failed: {e.__class__.__name__}: {e}")
|
|
53
|
+
return None
|
|
54
|
+
|
|
55
|
+
def _strip_codeblock(text: str) -> str:
|
|
56
|
+
"""Strip ```json ... ``` wrappers that MiniMax adds to responses."""
|
|
57
|
+
t = text.strip()
|
|
58
|
+
if t.startswith("```"):
|
|
59
|
+
t = t.split("\n", 1)[1] if "\n" in t else t[3:]
|
|
60
|
+
if t.endswith("```"):
|
|
61
|
+
t = t[:-3]
|
|
62
|
+
return t.strip()
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _parse_regex_report(regex_report: str) -> List[Dict]:
|
|
66
|
+
"""Extract structured findings from the human-readable regex report."""
|
|
67
|
+
findings = []
|
|
68
|
+
for line in regex_report.split("\n"):
|
|
69
|
+
m = re.match(r'\[(\w+)\]\s+(\S+?)(?:\s+\(line\s+(\d+)\))?$', line.strip())
|
|
70
|
+
if m:
|
|
71
|
+
findings.append({
|
|
72
|
+
"severity": m.group(1),
|
|
73
|
+
"category": m.group(2),
|
|
74
|
+
"line": int(m.group(3)) if m.group(3) else None,
|
|
75
|
+
"source": "[REGEX]",
|
|
76
|
+
})
|
|
77
|
+
return findings
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _looks_like_setup_behavior(code: str) -> bool:
|
|
81
|
+
lower = code.lower()
|
|
82
|
+
indicators = (
|
|
83
|
+
"setup(",
|
|
84
|
+
"pyproject",
|
|
85
|
+
"cmdclass",
|
|
86
|
+
"setuptools.command.install",
|
|
87
|
+
"base64.b64decode",
|
|
88
|
+
"exec(",
|
|
89
|
+
"os.system",
|
|
90
|
+
"subprocess",
|
|
91
|
+
"urllib.request",
|
|
92
|
+
"requests.post",
|
|
93
|
+
"os.environ",
|
|
94
|
+
)
|
|
95
|
+
return any(token in lower for token in indicators)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _should_escalate(
|
|
99
|
+
code: str,
|
|
100
|
+
regex_findings: List[Dict],
|
|
101
|
+
explain_requested: bool = False,
|
|
102
|
+
) -> Dict:
|
|
103
|
+
"""Decide whether MiniMax should be invoked after deterministic triage."""
|
|
104
|
+
reasons = []
|
|
105
|
+
high_risk_categories = {
|
|
106
|
+
"sql-injection", "command-injection", "ssrf", "path-traversal",
|
|
107
|
+
"deserialization", "email-injection", "template-injection",
|
|
108
|
+
"header-injection", "ldap-injection",
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if explain_requested:
|
|
112
|
+
reasons.append("user-requested explanation")
|
|
113
|
+
|
|
114
|
+
if any(
|
|
115
|
+
f.get("severity") in {"CRITICAL", "HIGH"} and f.get("category") in high_risk_categories
|
|
116
|
+
for f in regex_findings
|
|
117
|
+
):
|
|
118
|
+
reasons.append("high-risk regex finding needs deeper analysis")
|
|
119
|
+
|
|
120
|
+
if _looks_like_setup_behavior(code):
|
|
121
|
+
reasons.append("behavioral setup/install pattern")
|
|
122
|
+
|
|
123
|
+
if not regex_findings and (
|
|
124
|
+
"getattr(" in code or "base64.b64decode" in code or "os.environ" in code or "urllib.request.urlopen" in code
|
|
125
|
+
):
|
|
126
|
+
reasons.append("suspicious behavior without deterministic hit")
|
|
127
|
+
|
|
128
|
+
return {"should_escalate": bool(reasons), "reasons": reasons}
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _augment_crypto_indirection(code: str) -> List[Dict]:
|
|
132
|
+
"""Catch a narrow set of crypto-indirection cases that merit deep review.
|
|
133
|
+
|
|
134
|
+
This is not a replacement for the LLM. It is a targeted augment for cases
|
|
135
|
+
where the code clearly hides crypto intent through indirection and the model
|
|
136
|
+
may return an empty finding list.
|
|
137
|
+
"""
|
|
138
|
+
findings = []
|
|
139
|
+
lower = code.lower()
|
|
140
|
+
|
|
141
|
+
if "getattr(" in code and "generate_" in code and "private_key" in code and "rsa" in lower:
|
|
142
|
+
findings.append({
|
|
143
|
+
"severity": "CRITICAL",
|
|
144
|
+
"category": "crypto-indirection",
|
|
145
|
+
"line": None,
|
|
146
|
+
"message": "Indirect RSA private key generation via getattr hides quantum-vulnerable crypto usage.",
|
|
147
|
+
"taint_chain": "rsa module -> getattr(...) -> generate_private_key",
|
|
148
|
+
"source": "[AI-AUGMENT]",
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
if "jwt.encode" in code and ("rs" in lower or "es" in lower) and ("algorithm = f" in lower or "algorithm=f" in lower or 'f"{prefix}{suffix}"' in lower):
|
|
152
|
+
findings.append({
|
|
153
|
+
"severity": "HIGH",
|
|
154
|
+
"category": "crypto-indirection",
|
|
155
|
+
"line": None,
|
|
156
|
+
"message": "JWT algorithm is assembled dynamically, hiding RSA/ECDSA usage behind string construction.",
|
|
157
|
+
"taint_chain": "prefix/suffix -> algorithm -> jwt.encode",
|
|
158
|
+
"source": "[AI-AUGMENT]",
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
return findings
|
|
162
|
+
|
|
163
|
+
TAINT_PROMPT = """Expert security reviewer. Find what REGEX MISSES:
|
|
164
|
+
1. TAINT FLOW: user input (request.args/form/POST, req.body, input()) through variables to dangerous sinks (exec, eval, os.system, subprocess, render_template_string, send_mail, cursor.execute, redirect, smtplib, ldap)
|
|
165
|
+
2. BEHAVIORAL: base64+exec, network calls in setup.py, credential harvesting, obfuscation
|
|
166
|
+
3. CRYPTO INDIRECTION: constructed algorithm names, getattr-based crypto calls, string-built JWT algorithms, indirect RSA/ECDSA/JWT usage hidden through variables or concatenation
|
|
167
|
+
3. CONTEXT: lower severity if in tests
|
|
168
|
+
Return JSON: {"findings": [{"severity":"CRITICAL|HIGH|MEDIUM|LOW","category":"taint-flow|behavioral|obfuscation|crypto-indirection","line":null,"message":"...","taint_chain":"source->sink"}]}
|
|
169
|
+
Empty if clean. No speculation."""
|
|
170
|
+
|
|
171
|
+
def deep_analyze(code: str, language: str = "python") -> Dict:
|
|
172
|
+
regex_report = scan_security(code, language)
|
|
173
|
+
regex_findings = _parse_regex_report(regex_report)
|
|
174
|
+
result = {"regex_findings": len(regex_findings), "ai_findings": 0, "ai_enabled": False, "model": MODEL, "findings": regex_findings}
|
|
175
|
+
llm = _call_minimax(TAINT_PROMPT, f"Lang: {language}\n```\n{code}\n```")
|
|
176
|
+
if not llm:
|
|
177
|
+
result["ai_note"] = _missing_key_message("Set MINIMAX_API_KEY for AI taint analysis") if not _load_api_key() else _get_provider_error()
|
|
178
|
+
return result
|
|
179
|
+
result["ai_enabled"] = True
|
|
180
|
+
try:
|
|
181
|
+
ai = json.loads(_strip_codeblock(llm)).get("findings", [])
|
|
182
|
+
seen = {(f.get("line"), f.get("category")) for f in regex_findings}
|
|
183
|
+
for f in ai:
|
|
184
|
+
if (f.get("line"), f.get("category")) not in seen:
|
|
185
|
+
f["source"] = "[AI-BETA]"
|
|
186
|
+
result["findings"].append(f)
|
|
187
|
+
if not any(f.get("source") == "[AI-BETA]" for f in result["findings"]):
|
|
188
|
+
result["findings"].extend(_augment_crypto_indirection(code))
|
|
189
|
+
result["ai_findings"] = len([f for f in result["findings"] if f.get("source") in {"[AI-BETA]", "[AI-AUGMENT]"}])
|
|
190
|
+
except Exception:
|
|
191
|
+
result["ai_note"] = "AI malformed response — regex only"
|
|
192
|
+
return result
|
|
193
|
+
|
|
194
|
+
SETUP_PROMPT = """Supply chain analyst. Is this setup.py MALICIOUS?
|
|
195
|
+
MALICIOUS: os.system/subprocess/exec/eval, network calls during install, credential harvesting, obfuscated payloads
|
|
196
|
+
SUSPICIOUS: unusual imports (socket/base64/ctypes), external binary downloads
|
|
197
|
+
SAFE: normal setup
|
|
198
|
+
Return JSON: {"verdict":"SAFE|SUSPICIOUS|MALICIOUS","explanation":"2 sentences","findings":[{"severity":"CRITICAL|HIGH","message":"...","line":null}]}"""
|
|
199
|
+
|
|
200
|
+
def analyze_setup_py(code: str) -> Dict:
|
|
201
|
+
llm = _call_minimax(SETUP_PROMPT, f"```python\n{code}\n```")
|
|
202
|
+
if not llm:
|
|
203
|
+
return {"verdict": "UNKNOWN", "explanation": _missing_key_message("Set MINIMAX_API_KEY") if not _load_api_key() else _get_provider_error(), "findings": []}
|
|
204
|
+
try:
|
|
205
|
+
return json.loads(_strip_codeblock(llm))
|
|
206
|
+
except Exception:
|
|
207
|
+
return {"verdict": "UNKNOWN", "explanation": "Malformed response", "findings": []}
|
|
208
|
+
|
|
209
|
+
EXPLAIN_PROMPT = """Security educator. Given a finding + code, explain in <150 words:
|
|
210
|
+
1. WHAT the vulnerability is
|
|
211
|
+
2. HOW to exploit (concrete steps)
|
|
212
|
+
3. IMPACT
|
|
213
|
+
4. FIX (exact code change)
|
|
214
|
+
No jargon. Write for devs."""
|
|
215
|
+
|
|
216
|
+
def explain_vulnerability(finding: str, code_context: str) -> str:
|
|
217
|
+
llm = _call_minimax(EXPLAIN_PROMPT, f"Finding: {finding}\nCode:\n```\n{code_context}\n```", json_mode=False)
|
|
218
|
+
return llm or (_missing_key_message("Set MINIMAX_API_KEY") if not _load_api_key() else _get_provider_error())
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def smart_analyze(
|
|
222
|
+
code: str,
|
|
223
|
+
language: str = "python",
|
|
224
|
+
explain_requested: bool = False,
|
|
225
|
+
record_learning: bool = False,
|
|
226
|
+
title: str = "",
|
|
227
|
+
source: str = "manual",
|
|
228
|
+
expected_behavior: str = "",
|
|
229
|
+
) -> Dict:
|
|
230
|
+
"""Fast path first, then escalate to AI only when justified.
|
|
231
|
+
|
|
232
|
+
This is the orchestration entry point for CodeGuard's layered architecture:
|
|
233
|
+
deterministic scanners run first, MiniMax is invoked only for incomplete or
|
|
234
|
+
suspicious cases, and novel misses can be stored in the learning corpus.
|
|
235
|
+
"""
|
|
236
|
+
regex_report = scan_security(code, language)
|
|
237
|
+
regex_findings = _parse_regex_report(regex_report)
|
|
238
|
+
escalation = _should_escalate(code, regex_findings, explain_requested=explain_requested)
|
|
239
|
+
|
|
240
|
+
result = {
|
|
241
|
+
"path": "fast-path",
|
|
242
|
+
"language": language,
|
|
243
|
+
"regex_report": regex_report,
|
|
244
|
+
"regex_findings": len(regex_findings),
|
|
245
|
+
"findings": regex_findings[:],
|
|
246
|
+
"escalation": escalation,
|
|
247
|
+
"ai_enabled": False,
|
|
248
|
+
"ai_findings": 0,
|
|
249
|
+
"model": MODEL,
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if not escalation["should_escalate"]:
|
|
253
|
+
return result
|
|
254
|
+
|
|
255
|
+
result["path"] = "escalation"
|
|
256
|
+
|
|
257
|
+
if _looks_like_setup_behavior(code):
|
|
258
|
+
setup_result = analyze_setup_py(code)
|
|
259
|
+
result["setup_analysis"] = setup_result
|
|
260
|
+
if setup_result.get("verdict") not in {"UNKNOWN", "SAFE"}:
|
|
261
|
+
result["ai_enabled"] = True
|
|
262
|
+
result["ai_findings"] = len(setup_result.get("findings", []))
|
|
263
|
+
result["findings"].extend([
|
|
264
|
+
{"source": "[AI-BETA]", **f} for f in setup_result.get("findings", [])
|
|
265
|
+
])
|
|
266
|
+
else:
|
|
267
|
+
ai_result = deep_analyze(code, language)
|
|
268
|
+
result["ai_enabled"] = ai_result.get("ai_enabled", False)
|
|
269
|
+
result["ai_findings"] = ai_result.get("ai_findings", 0)
|
|
270
|
+
result["findings"] = ai_result.get("findings", result["findings"])
|
|
271
|
+
if ai_result.get("ai_note"):
|
|
272
|
+
result["ai_note"] = ai_result["ai_note"]
|
|
273
|
+
|
|
274
|
+
if record_learning:
|
|
275
|
+
try:
|
|
276
|
+
from learning_loop import record_candidate
|
|
277
|
+
if result["ai_findings"] > 0 or (result["escalation"]["should_escalate"] and not result["regex_findings"]):
|
|
278
|
+
learning_result = record_candidate(
|
|
279
|
+
title=title or "suspicious-sample",
|
|
280
|
+
code=code,
|
|
281
|
+
language=language,
|
|
282
|
+
source=source,
|
|
283
|
+
expected_behavior=expected_behavior,
|
|
284
|
+
regex_findings=[f for f in result["findings"] if f.get("source") == "[REGEX]"],
|
|
285
|
+
ai_findings=[f for f in result["findings"] if f.get("source") == "[AI-BETA]"],
|
|
286
|
+
)
|
|
287
|
+
result["learning"] = learning_result
|
|
288
|
+
except Exception as e:
|
|
289
|
+
result["learning_error"] = str(e)
|
|
290
|
+
|
|
291
|
+
return result
|
autofix.py
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"""LLM-powered auto-fix engine for CodeGuard Pro.
|
|
2
|
+
|
|
3
|
+
Takes code + security findings, generates exact fixes via MiniMax M2.7.
|
|
4
|
+
Falls back to pattern-based fixes when LLM is unavailable.
|
|
5
|
+
"""
|
|
6
|
+
import json, os, re, urllib.request, urllib.error
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from typing import List, Optional
|
|
9
|
+
from secret_scanner import SecretFinding, scan_secrets
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True)
|
|
12
|
+
class AutofixResult:
|
|
13
|
+
"""Immutable result from the autofix engine."""
|
|
14
|
+
original_code: str
|
|
15
|
+
fixed_code: str
|
|
16
|
+
explanation: str
|
|
17
|
+
used_llm: bool
|
|
18
|
+
findings_fixed: int
|
|
19
|
+
|
|
20
|
+
def _load_api_key() -> Optional[str]:
|
|
21
|
+
"""Load MiniMax API key from env or /root/ai-factory/.env."""
|
|
22
|
+
key = os.environ.get("MINIMAX_API_KEY")
|
|
23
|
+
if key:
|
|
24
|
+
return key
|
|
25
|
+
env_path = "/root/ai-factory/.env"
|
|
26
|
+
if os.path.exists(env_path):
|
|
27
|
+
with open(env_path) as f:
|
|
28
|
+
for line in f:
|
|
29
|
+
if line.strip().startswith("MINIMAX_API_KEY="):
|
|
30
|
+
return line.strip().split("=", 1)[1].strip("'\" \n")
|
|
31
|
+
return None
|
|
32
|
+
|
|
33
|
+
def _call_minimax(code: str, findings: List[SecretFinding]) -> Optional[str]:
|
|
34
|
+
"""Call MiniMax M2.7 to generate fixed code. Returns fixed code or None."""
|
|
35
|
+
api_key = _load_api_key()
|
|
36
|
+
if not api_key:
|
|
37
|
+
return None
|
|
38
|
+
findings_text = "\n".join(
|
|
39
|
+
f"- Line {f.line}: {f.secret_type} ({f.severity}) — {f.fix}" for f in findings
|
|
40
|
+
)
|
|
41
|
+
messages = [
|
|
42
|
+
{"role": "system", "content": (
|
|
43
|
+
"You are a security-focused code fixer. Return ONLY the fixed code. "
|
|
44
|
+
"No markdown fences, no explanation. Preserve formatting, comments, and logic. "
|
|
45
|
+
"Only change lines with security issues. Add 'import os' if needed."
|
|
46
|
+
)},
|
|
47
|
+
{"role": "user", "content": (
|
|
48
|
+
f"Fix these security issues:\n\nFINDINGS:\n{findings_text}\n\nCODE:\n{code}"
|
|
49
|
+
)},
|
|
50
|
+
]
|
|
51
|
+
payload = json.dumps({
|
|
52
|
+
"model": "MiniMax-M2.7", "messages": messages,
|
|
53
|
+
"temperature": 0.1, "max_tokens": 4096,
|
|
54
|
+
}).encode()
|
|
55
|
+
req = urllib.request.Request(
|
|
56
|
+
"https://api.minimaxi.chat/v1/text/chatcompletion_v2",
|
|
57
|
+
data=payload, method="POST",
|
|
58
|
+
headers={"Content-Type": "application/json", "Authorization": f"Bearer {api_key}"},
|
|
59
|
+
)
|
|
60
|
+
try:
|
|
61
|
+
with urllib.request.urlopen(req, timeout=30) as resp:
|
|
62
|
+
data = json.loads(resp.read().decode())
|
|
63
|
+
content = data["choices"][0]["message"]["content"]
|
|
64
|
+
content = re.sub(r"^```\w*\n", "", content)
|
|
65
|
+
content = re.sub(r"\n```$", "", content)
|
|
66
|
+
return content.strip()
|
|
67
|
+
except (urllib.error.URLError, KeyError, IndexError, json.JSONDecodeError):
|
|
68
|
+
return None
|
|
69
|
+
|
|
70
|
+
# --- Pattern-based fallback fixes ---
|
|
71
|
+
_SECRET_TO_ENV = {
|
|
72
|
+
"OpenAI API Key": "OPENAI_API_KEY", "Anthropic API Key": "ANTHROPIC_API_KEY",
|
|
73
|
+
"GitHub Token": "GITHUB_TOKEN", "GitHub OAuth": "GITHUB_OAUTH_TOKEN",
|
|
74
|
+
"Google API Key": "GOOGLE_API_KEY", "Stripe Secret Key": "STRIPE_SECRET_KEY",
|
|
75
|
+
"Slack Token": "SLACK_TOKEN", "MiniMax API Key": "MINIMAX_API_KEY",
|
|
76
|
+
"SendGrid Key": "SENDGRID_API_KEY", "Supabase Key": "SUPABASE_KEY",
|
|
77
|
+
"Vercel Token": "VERCEL_TOKEN", "Generic API Key": "API_KEY",
|
|
78
|
+
"Hardcoded Password": "APP_PASSWORD", "Database URL": "DATABASE_URL",
|
|
79
|
+
"AWS Access Key": "AWS_ACCESS_KEY_ID",
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _fallback_fix(code: str, findings: List[SecretFinding]) -> str:
|
|
84
|
+
"""Apply pattern-based fixes when LLM is unavailable."""
|
|
85
|
+
lines, needs_os = code.split("\n"), False
|
|
86
|
+
for f in findings:
|
|
87
|
+
if f.line < 1 or f.line > len(lines):
|
|
88
|
+
continue
|
|
89
|
+
env_name = _SECRET_TO_ENV.get(f.secret_type)
|
|
90
|
+
if not env_name:
|
|
91
|
+
m = re.search(r'os\.environ\["([^"]+)"\]', f.fix)
|
|
92
|
+
env_name = m.group(1) if m else f.secret_type.upper().replace(" ", "_")
|
|
93
|
+
replaced = re.sub(r"""(['"])[^\s'"]{8,}\1""", f'os.environ["{env_name}"]', lines[f.line - 1], count=1)
|
|
94
|
+
if replaced != lines[f.line - 1]:
|
|
95
|
+
needs_os = True
|
|
96
|
+
lines[f.line - 1] = replaced
|
|
97
|
+
# Fix SQL injection: f-string in cursor.execute -> parameterized
|
|
98
|
+
for i, line in enumerate(lines):
|
|
99
|
+
if "cursor.execute" in line and ("f'" in line or 'f"' in line):
|
|
100
|
+
fixed = re.sub(
|
|
101
|
+
r'''f(["\'])(.*?)\{(\w+)\}(.*?)\1''',
|
|
102
|
+
lambda m: f'{m.group(1)}{m.group(2)}?{m.group(4)}{m.group(1)}, ({m.group(3)},)', line)
|
|
103
|
+
if fixed != line:
|
|
104
|
+
lines[i] = fixed
|
|
105
|
+
result = "\n".join(lines)
|
|
106
|
+
if needs_os and not re.search(r"^import os\b", result, re.MULTILINE):
|
|
107
|
+
result = "import os\n" + result
|
|
108
|
+
return result
|
|
109
|
+
|
|
110
|
+
def autofix(code: str, findings: Optional[List[SecretFinding]] = None) -> AutofixResult:
|
|
111
|
+
"""Generate fixes for security findings in code."""
|
|
112
|
+
if findings is None:
|
|
113
|
+
findings = scan_secrets(code)
|
|
114
|
+
if not findings:
|
|
115
|
+
return AutofixResult(original_code=code, fixed_code=code,
|
|
116
|
+
explanation="No security issues found.", used_llm=False, findings_fixed=0)
|
|
117
|
+
llm_result = _call_minimax(code, findings)
|
|
118
|
+
if llm_result:
|
|
119
|
+
return AutofixResult(original_code=code, fixed_code=llm_result,
|
|
120
|
+
explanation=f"Fixed {len(findings)} finding(s) using MiniMax M2.7.",
|
|
121
|
+
used_llm=True, findings_fixed=len(findings))
|
|
122
|
+
fixed = _fallback_fix(code, findings)
|
|
123
|
+
return AutofixResult(original_code=code, fixed_code=fixed,
|
|
124
|
+
explanation=f"Fixed {len(findings)} finding(s) using pattern-based fallback.",
|
|
125
|
+
used_llm=False, findings_fixed=len(findings))
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
if __name__ == "__main__":
|
|
129
|
+
sample = (
|
|
130
|
+
'import requests\n\n'
|
|
131
|
+
'API_KEY = "sk-proj-abc123def456ghi789jkl012mno345"\n'
|
|
132
|
+
'DB_URL = "postgres://admin:supersecret@db.example.com:5432/prod"\n'
|
|
133
|
+
'password = "hunter2isMyP@ss!"\n\n'
|
|
134
|
+
'def get_data():\n'
|
|
135
|
+
' headers = {"Authorization": f"Bearer {API_KEY}"}\n'
|
|
136
|
+
' return requests.get("https://api.example.com/data", headers=headers)\n'
|
|
137
|
+
)
|
|
138
|
+
print("=" * 60)
|
|
139
|
+
print("CodeGuard Pro — Auto-Fix Engine Demo")
|
|
140
|
+
print("=" * 60)
|
|
141
|
+
print(f"\nORIGINAL CODE:\n{sample}")
|
|
142
|
+
result = autofix(sample)
|
|
143
|
+
engine = "MiniMax M2.7" if result.used_llm else "Pattern-based fallback"
|
|
144
|
+
print(f"Engine: {engine} | Findings fixed: {result.findings_fixed}")
|
|
145
|
+
print(f"\nFIXED CODE:\n{result.fixed_code}")
|
|
146
|
+
print(f"\n{result.explanation}")
|