codebase-brain 0.1.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.
- brain_cli.py +331 -0
- brain_parser/__init__.py +0 -0
- brain_parser/ast_parser.py +62 -0
- brain_parser/bug_detector.py +212 -0
- brain_parser/codebase_walker.py +101 -0
- brain_parser/file_watcher.py +81 -0
- brain_parser/graph_builder.py +190 -0
- brain_parser/llm_router.py +75 -0
- brain_parser/query_engine.py +127 -0
- brain_parser/root_cause.py +162 -0
- brain_parser/universal_parser.py +215 -0
- codebase_brain-0.1.0.dist-info/METADATA +13 -0
- codebase_brain-0.1.0.dist-info/RECORD +16 -0
- codebase_brain-0.1.0.dist-info/WHEEL +5 -0
- codebase_brain-0.1.0.dist-info/entry_points.txt +2 -0
- codebase_brain-0.1.0.dist-info/top_level.txt +2 -0
brain_cli.py
ADDED
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
import threading
|
|
2
|
+
import subprocess
|
|
3
|
+
import stat
|
|
4
|
+
import argparse
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
from brain_parser.codebase_walker import walk_codebase, save_brain
|
|
8
|
+
from brain_parser.file_watcher import start_watching
|
|
9
|
+
from brain_parser.graph_builder import build_graph, visualize_interactive
|
|
10
|
+
from brain_parser.query_engine import ask_brain
|
|
11
|
+
import io
|
|
12
|
+
|
|
13
|
+
# Force UTF-8 output regardless of parent process encoding
|
|
14
|
+
if sys.stdout.encoding != 'utf-8':
|
|
15
|
+
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
|
16
|
+
if sys.stderr.encoding != 'utf-8':
|
|
17
|
+
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')
|
|
18
|
+
|
|
19
|
+
import argparse
|
|
20
|
+
import os
|
|
21
|
+
# ... rest of your existing imports
|
|
22
|
+
|
|
23
|
+
def cmd_start(path):
|
|
24
|
+
print(" Codebase Brain starting...")
|
|
25
|
+
print(f" Reading codebase at: {path}")
|
|
26
|
+
brain = walk_codebase(path)
|
|
27
|
+
save_brain(brain)
|
|
28
|
+
print(f"Brain built: {len(brain)} files analyzed")
|
|
29
|
+
|
|
30
|
+
print(" Building dependency graph...")
|
|
31
|
+
G = build_graph(brain)
|
|
32
|
+
visualize_interactive(G)
|
|
33
|
+
print("Graph ready: brain_map.html")
|
|
34
|
+
|
|
35
|
+
print("\n Scanning for bugs...")
|
|
36
|
+
from brain_parser.bug_detector import run_all_detectors
|
|
37
|
+
bugs = run_all_detectors(brain, G)
|
|
38
|
+
if bugs:
|
|
39
|
+
print(f"\n Found {len(bugs)} potential bugs:\n")
|
|
40
|
+
for bug in bugs:
|
|
41
|
+
print(f" [{bug['severity']}] {bug['type'].replace('_', ' ').upper()}")
|
|
42
|
+
print(f" {bug['message']}")
|
|
43
|
+
print(f" Fix: {bug['fix']}\n")
|
|
44
|
+
else:
|
|
45
|
+
print(" No bugs detected. Codebase looks clean.\n")
|
|
46
|
+
|
|
47
|
+
print("Starting live file watcher...")
|
|
48
|
+
watcher_thread = threading.Thread(
|
|
49
|
+
target=start_watching,
|
|
50
|
+
args=(path,),
|
|
51
|
+
daemon=True
|
|
52
|
+
)
|
|
53
|
+
watcher_thread.start()
|
|
54
|
+
print("Watching for changes...")
|
|
55
|
+
|
|
56
|
+
print("\n Brain is live. Ask anything.")
|
|
57
|
+
print("Type 'exit' to stop query mode.")
|
|
58
|
+
print("Press CTRL+C anytime to shut down completely.\n")
|
|
59
|
+
while True:
|
|
60
|
+
question = input("Ask your brain: ")
|
|
61
|
+
if question == "exit":
|
|
62
|
+
print("Brain shutting down.")
|
|
63
|
+
break
|
|
64
|
+
answer = ask_brain(question)
|
|
65
|
+
print(f"\nBrain: {answer}\n")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def cmd_impact(filepath=None, staged=False, block=False):
|
|
69
|
+
from brain_parser.codebase_walker import load_brain
|
|
70
|
+
from brain_parser.graph_builder import build_graph, get_impact, calculate_risk
|
|
71
|
+
|
|
72
|
+
brain = load_brain()
|
|
73
|
+
if not brain:
|
|
74
|
+
print("No brain found. Run 'brain start' first.")
|
|
75
|
+
return
|
|
76
|
+
|
|
77
|
+
G = build_graph(brain)
|
|
78
|
+
risk = calculate_risk(G)
|
|
79
|
+
|
|
80
|
+
root = os.path.abspath('.').replace('\\', '/')
|
|
81
|
+
def clean(path):
|
|
82
|
+
return path.replace('\\', '/').replace(root + '/', '')
|
|
83
|
+
|
|
84
|
+
if staged:
|
|
85
|
+
result = subprocess.run(
|
|
86
|
+
["git", "diff", "--name-only", "--cached"],
|
|
87
|
+
capture_output=True, text=True
|
|
88
|
+
)
|
|
89
|
+
staged_files = [f.strip() for f in result.stdout.strip().split("\n") if f.strip()]
|
|
90
|
+
|
|
91
|
+
if not staged_files:
|
|
92
|
+
print("No staged files found.")
|
|
93
|
+
return
|
|
94
|
+
|
|
95
|
+
print(f"\n Staged files: {', '.join(staged_files)}\n")
|
|
96
|
+
|
|
97
|
+
high_risk_found = False
|
|
98
|
+
for f in staged_files:
|
|
99
|
+
print(f"{'='*50}")
|
|
100
|
+
result_data = _print_impact(G, risk, f, clean)
|
|
101
|
+
if result_data and result_data.get('risk') == 'HIGH':
|
|
102
|
+
high_risk_found = True
|
|
103
|
+
|
|
104
|
+
if block and high_risk_found:
|
|
105
|
+
print("\n" + "=" * 50)
|
|
106
|
+
print("BRAIN WARNING: HIGH RISK commit detected.")
|
|
107
|
+
print("This change affects critical files.")
|
|
108
|
+
print("Production incidents have originated from files like these.")
|
|
109
|
+
print("\nType 'yes' to commit anyway, anything else to abort: ", end='', flush=True)
|
|
110
|
+
try:
|
|
111
|
+
# Windows: CON is the console device
|
|
112
|
+
# Unix: /dev/tty is the terminal
|
|
113
|
+
import platform
|
|
114
|
+
if platform.system() == 'Windows':
|
|
115
|
+
with open('CON', 'r') as tty:
|
|
116
|
+
confirm = tty.readline().strip().lower()
|
|
117
|
+
else:
|
|
118
|
+
with open('/dev/tty', 'r') as tty:
|
|
119
|
+
confirm = tty.readline().strip().lower()
|
|
120
|
+
except:
|
|
121
|
+
try:
|
|
122
|
+
confirm = input().strip().lower()
|
|
123
|
+
except EOFError:
|
|
124
|
+
confirm = 'yes'
|
|
125
|
+
if confirm != 'yes':
|
|
126
|
+
print("\nCommit blocked by Codebase Brain.")
|
|
127
|
+
sys.exit(1)
|
|
128
|
+
else:
|
|
129
|
+
print("Override confirmed. Committing anyway.")
|
|
130
|
+
return
|
|
131
|
+
|
|
132
|
+
if not filepath:
|
|
133
|
+
print("Provide --file or --staged.")
|
|
134
|
+
return
|
|
135
|
+
|
|
136
|
+
_print_impact(G, risk, filepath, clean)
|
|
137
|
+
|
|
138
|
+
def _print_impact(G, risk, filepath, clean):
|
|
139
|
+
from brain_parser.graph_builder import get_impact
|
|
140
|
+
|
|
141
|
+
result = get_impact(G, filepath)
|
|
142
|
+
|
|
143
|
+
if not result:
|
|
144
|
+
print(f"File not found in brain: {filepath}")
|
|
145
|
+
return None
|
|
146
|
+
|
|
147
|
+
file_risk = risk.get(result['target'], 'LOW')
|
|
148
|
+
|
|
149
|
+
print(f"\n Change Impact: {filepath}")
|
|
150
|
+
print(f" Risk Level: {file_risk}\n")
|
|
151
|
+
print(f"DIRECT IMPACT ({len(result['direct'])} files):")
|
|
152
|
+
for f in result['direct']:
|
|
153
|
+
print(f" → {clean(f)}")
|
|
154
|
+
print(f"\nINDIRECT IMPACT ({len(result['indirect'])} files):")
|
|
155
|
+
for f in result['indirect']:
|
|
156
|
+
print(f" → {clean(f)}")
|
|
157
|
+
print(f"\nTotal files affected: {result['total_affected']}\n")
|
|
158
|
+
|
|
159
|
+
return {'risk': file_risk, 'total': result['total_affected']}
|
|
160
|
+
|
|
161
|
+
def install_hook(repo_path="."):
|
|
162
|
+
hook_dir = os.path.join(repo_path, ".git", "hooks")
|
|
163
|
+
hook_path = os.path.join(hook_dir, "pre-commit")
|
|
164
|
+
|
|
165
|
+
if not os.path.exists(hook_dir):
|
|
166
|
+
print("Not a git repository. Run this inside your project folder.")
|
|
167
|
+
return
|
|
168
|
+
|
|
169
|
+
hook_script = """#!/bin/sh
|
|
170
|
+
# Codebase Brain — Pre-commit Impact Check
|
|
171
|
+
# https://github.com/guruvarpatel-ai/codebase-brain
|
|
172
|
+
|
|
173
|
+
STAGED=$(git diff --name-only --cached)
|
|
174
|
+
|
|
175
|
+
if [ -z "$STAGED" ]; then
|
|
176
|
+
exit 0
|
|
177
|
+
fi
|
|
178
|
+
|
|
179
|
+
echo ""
|
|
180
|
+
echo "Codebase Brain — Checking blast radius..."
|
|
181
|
+
echo ""
|
|
182
|
+
|
|
183
|
+
brain impact --staged --block
|
|
184
|
+
EXIT_CODE=$?
|
|
185
|
+
|
|
186
|
+
echo ""
|
|
187
|
+
echo "Powered by Codebase Brain"
|
|
188
|
+
echo ""
|
|
189
|
+
|
|
190
|
+
exit $EXIT_CODE
|
|
191
|
+
"""
|
|
192
|
+
|
|
193
|
+
with open(hook_path, "w", newline='\n') as f: # newline='\n' fixes Windows CRLF
|
|
194
|
+
f.write(hook_script)
|
|
195
|
+
|
|
196
|
+
os.chmod(hook_path, os.stat(hook_path).st_mode | stat.S_IEXEC | stat.S_IXGRP | stat.S_IXOTH)
|
|
197
|
+
|
|
198
|
+
print(f"Brain hook installed at {hook_path}")
|
|
199
|
+
print("Every commit now shows blast radius automatically.")
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def cmd_init():
|
|
203
|
+
print("Initializing Codebase Brain...\n")
|
|
204
|
+
print("Choose your LLM provider:")
|
|
205
|
+
print(" 1. Groq — Free, fast. llama-3.3-70b (recommended to start)")
|
|
206
|
+
print(" 2. OpenAI — Reliable, trusted by enterprises. gpt-4o-mini")
|
|
207
|
+
print(" 3. Anthropic — Best reasoning. claude-3-5-haiku (your code stays private)")
|
|
208
|
+
print(" 4. Google — Free tier available. gemini-1.5-flash")
|
|
209
|
+
print(" 5. Ollama — 100% local, no API key, no data leaves your machine")
|
|
210
|
+
print("")
|
|
211
|
+
choice = input("Enter 1-5 (default 1): ").strip() or "1"
|
|
212
|
+
|
|
213
|
+
providers = {
|
|
214
|
+
"1": {
|
|
215
|
+
"name": "groq",
|
|
216
|
+
"key_prompt": "Groq API key (free at console.groq.com): ",
|
|
217
|
+
"models": ["llama-3.3-70b-versatile", "llama-3.1-8b-instant", "mixtral-8x7b-32768"],
|
|
218
|
+
"default_model": "llama-3.3-70b-versatile"
|
|
219
|
+
},
|
|
220
|
+
"2": {
|
|
221
|
+
"name": "openai",
|
|
222
|
+
"key_prompt": "OpenAI API key (platform.openai.com): ",
|
|
223
|
+
"models": ["gpt-4o-mini", "gpt-4o", "gpt-3.5-turbo"],
|
|
224
|
+
"default_model": "gpt-4o-mini"
|
|
225
|
+
},
|
|
226
|
+
"3": {
|
|
227
|
+
"name": "anthropic",
|
|
228
|
+
"key_prompt": "Anthropic API key (console.anthropic.com): ",
|
|
229
|
+
"models": ["claude-3-5-haiku-20241022", "claude-3-5-sonnet-20241022"],
|
|
230
|
+
"default_model": "claude-3-5-haiku-20241022"
|
|
231
|
+
},
|
|
232
|
+
"4": {
|
|
233
|
+
"name": "google",
|
|
234
|
+
"key_prompt": "Google API key (aistudio.google.com): ",
|
|
235
|
+
"models": ["gemini-1.5-flash", "gemini-1.5-pro", "gemini-2.0-flash"],
|
|
236
|
+
"default_model": "gemini-1.5-flash"
|
|
237
|
+
},
|
|
238
|
+
"5": {
|
|
239
|
+
"name": "ollama",
|
|
240
|
+
"key_prompt": None, # no key needed
|
|
241
|
+
"models": ["llama3.2", "codellama", "deepseek-coder"],
|
|
242
|
+
"default_model": "llama3.2"
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if choice not in providers:
|
|
247
|
+
choice = "1"
|
|
248
|
+
|
|
249
|
+
provider = providers[choice]
|
|
250
|
+
|
|
251
|
+
# get API key
|
|
252
|
+
if provider["key_prompt"]:
|
|
253
|
+
api_key = input(provider["key_prompt"]).strip()
|
|
254
|
+
else:
|
|
255
|
+
api_key = "ollama"
|
|
256
|
+
print("Ollama selected — make sure Ollama is running at http://localhost:11434")
|
|
257
|
+
|
|
258
|
+
# pick model
|
|
259
|
+
print(f"\nAvailable models for {provider['name']}:")
|
|
260
|
+
for i, m in enumerate(provider["models"], 1):
|
|
261
|
+
default_tag = " (default)" if m == provider["default_model"] else ""
|
|
262
|
+
print(f" {i}. {m}{default_tag}")
|
|
263
|
+
|
|
264
|
+
model_choice = input("\nEnter model number (press Enter for default): ").strip()
|
|
265
|
+
if model_choice and model_choice.isdigit():
|
|
266
|
+
idx = int(model_choice) - 1
|
|
267
|
+
if 0 <= idx < len(provider["models"]):
|
|
268
|
+
model = provider["models"][idx]
|
|
269
|
+
else:
|
|
270
|
+
model = provider["default_model"]
|
|
271
|
+
else:
|
|
272
|
+
model = provider["default_model"]
|
|
273
|
+
|
|
274
|
+
with open(".env", "w") as f:
|
|
275
|
+
f.write(f"LLM_PROVIDER={provider['name']}\n")
|
|
276
|
+
f.write(f"LLM_API_KEY={api_key}\n")
|
|
277
|
+
f.write(f"LLM_MODEL={model}\n")
|
|
278
|
+
# backwards compatibility
|
|
279
|
+
f.write(f"GROQ_API_KEY={api_key}\n")
|
|
280
|
+
f.write(f"GROQ_MODEL={model}\n")
|
|
281
|
+
|
|
282
|
+
print(f"\nBrain initialized with {provider['name']} — {model}")
|
|
283
|
+
print("Run 'brain start' to begin.\n")
|
|
284
|
+
|
|
285
|
+
def cmd_rootcause(error_text=None):
|
|
286
|
+
from brain_parser.root_cause import find_root_cause
|
|
287
|
+
|
|
288
|
+
if not error_text:
|
|
289
|
+
print("Paste your error/stacktrace below.")
|
|
290
|
+
print("Press Enter twice when done.\n")
|
|
291
|
+
lines = []
|
|
292
|
+
while True:
|
|
293
|
+
line = input()
|
|
294
|
+
if line == "" and lines and lines[-1] == "":
|
|
295
|
+
break
|
|
296
|
+
lines.append(line)
|
|
297
|
+
error_text = "\n".join(lines)
|
|
298
|
+
|
|
299
|
+
print(find_root_cause(error_text))
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def main():
|
|
303
|
+
parser = argparse.ArgumentParser(
|
|
304
|
+
description="Codebase Brain - AI layer over your codebase"
|
|
305
|
+
)
|
|
306
|
+
# ← "install-hook" added here
|
|
307
|
+
parser.add_argument("--path", default=".", help="Path to codebase")
|
|
308
|
+
parser.add_argument("--file", default="", help="File to analyze impact")
|
|
309
|
+
parser.add_argument("--staged", action="store_true", help="Analyze all staged files")
|
|
310
|
+
parser.add_argument("command", choices=["start", "init", "impact", "install-hook", "rootcause"])
|
|
311
|
+
parser.add_argument("--error", default="", help="Stacktrace to analyze")
|
|
312
|
+
parser.add_argument("--block", action="store_true", help="Block HIGH risk commits")
|
|
313
|
+
# ← new
|
|
314
|
+
args = parser.parse_args()
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
if args.command == "init":
|
|
318
|
+
cmd_init()
|
|
319
|
+
elif args.command == "rootcause":
|
|
320
|
+
cmd_rootcause(args.error or None)
|
|
321
|
+
elif args.command == "start":
|
|
322
|
+
cmd_start(args.path)
|
|
323
|
+
elif args.command == "impact":
|
|
324
|
+
cmd_impact(filepath=args.file or None, staged=args.staged, block=args.block)
|
|
325
|
+
elif args.command == "install-hook":
|
|
326
|
+
install_hook()
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
if __name__ == "__main__":
|
|
331
|
+
main()
|
brain_parser/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import ast
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
def parse_file(filepath):
|
|
5
|
+
#hello world
|
|
6
|
+
with open(filepath, "r", encoding="utf-8", errors="ignore") as f:
|
|
7
|
+
source=f.read()
|
|
8
|
+
try:
|
|
9
|
+
tree=ast.parse(source)
|
|
10
|
+
return tree
|
|
11
|
+
except SyntaxError as e:
|
|
12
|
+
print(f"Syntax error in {filepath}: {e}")
|
|
13
|
+
return None
|
|
14
|
+
|
|
15
|
+
def extract_imports(tree):
|
|
16
|
+
imports = []
|
|
17
|
+
for node in ast.walk(tree):
|
|
18
|
+
if isinstance(node, ast.Import):
|
|
19
|
+
for alias in node.names:
|
|
20
|
+
imports.append({
|
|
21
|
+
"name": alias.name,
|
|
22
|
+
"type": "direct"
|
|
23
|
+
})
|
|
24
|
+
if isinstance(node, ast.ImportFrom):
|
|
25
|
+
imports.append({
|
|
26
|
+
"module": node.module or "unknown",
|
|
27
|
+
"type": "from"
|
|
28
|
+
})
|
|
29
|
+
return imports
|
|
30
|
+
|
|
31
|
+
def extract_classes_and_functions(tree):
|
|
32
|
+
classes = []
|
|
33
|
+
functions = []
|
|
34
|
+
for node in ast.walk(tree):
|
|
35
|
+
if isinstance(node, ast.ClassDef):
|
|
36
|
+
classes.append({
|
|
37
|
+
"name": node.name,
|
|
38
|
+
"line": node.lineno
|
|
39
|
+
})
|
|
40
|
+
if isinstance(node, ast.FunctionDef):
|
|
41
|
+
functions.append({
|
|
42
|
+
"name": node.name,
|
|
43
|
+
"args": [arg.id if hasattr(arg, 'id') else arg.arg
|
|
44
|
+
for arg in node.args.args],
|
|
45
|
+
"line": node.lineno
|
|
46
|
+
})
|
|
47
|
+
return {"classes": classes, "functions": functions}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def analyze_file(filepath):
|
|
51
|
+
tree = parse_file(filepath)
|
|
52
|
+
if tree is None:
|
|
53
|
+
return None
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
"file":filepath ,
|
|
57
|
+
"imports":extract_imports(tree),
|
|
58
|
+
"classes_and_functions":extract_classes_and_functions(tree)
|
|
59
|
+
}
|
|
60
|
+
if __name__ == "__main__":
|
|
61
|
+
result = analyze_file("utils.py")
|
|
62
|
+
print(result)
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import networkx as nx
|
|
2
|
+
import os
|
|
3
|
+
from brain_parser.codebase_walker import load_brain
|
|
4
|
+
from brain_parser.graph_builder import build_graph
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def detect_circular_dependencies(brain, temp_path=None):
|
|
9
|
+
G = build_graph(brain)
|
|
10
|
+
bugs = []
|
|
11
|
+
|
|
12
|
+
def clean(path):
|
|
13
|
+
p = path.replace('\\', '/')
|
|
14
|
+
idx = p.find('/temp/')
|
|
15
|
+
if idx != -1:
|
|
16
|
+
after_temp = p[idx + 6:]
|
|
17
|
+
parts = after_temp.split('/', 1)
|
|
18
|
+
if len(parts) > 1:
|
|
19
|
+
return parts[1]
|
|
20
|
+
return parts[0]
|
|
21
|
+
# fallback — just filename
|
|
22
|
+
return p.split('/')[-1]
|
|
23
|
+
|
|
24
|
+
try:
|
|
25
|
+
cycles = list(nx.simple_cycles(G))
|
|
26
|
+
for cycle in cycles:
|
|
27
|
+
clean_cycle = [clean(f) for f in cycle]
|
|
28
|
+
# format as numbered list not one long line
|
|
29
|
+
chain = '\n '.join([f"{i+1}. {f}" for i, f in enumerate(clean_cycle)])
|
|
30
|
+
chain += f"\n → back to {clean_cycle[0]}"
|
|
31
|
+
bugs.append({
|
|
32
|
+
'type': 'circular_dependency',
|
|
33
|
+
'severity': 'HIGH',
|
|
34
|
+
'files': clean_cycle,
|
|
35
|
+
'message': f"Circular dependency ({len(cycle)} files):\n {chain}",
|
|
36
|
+
'fix': 'Extract shared logic into a separate file that both can import from.'
|
|
37
|
+
})
|
|
38
|
+
except Exception as e:
|
|
39
|
+
print(f"Cycle detection error: {e}")
|
|
40
|
+
|
|
41
|
+
return bugs
|
|
42
|
+
def detect_unused_imports(brain):
|
|
43
|
+
# compare imports list against function/class names used in file
|
|
44
|
+
bugs = []
|
|
45
|
+
|
|
46
|
+
for filepath, data in brain.items():
|
|
47
|
+
# skip non-python files until we support body reading
|
|
48
|
+
if data.get('language') != 'python':
|
|
49
|
+
continue
|
|
50
|
+
imports = data.get('imports', [])
|
|
51
|
+
functions = [f['name'].lower() for f in data.get('functions', [])]
|
|
52
|
+
classes = [c['name'].lower() for c in data.get('classes', [])]
|
|
53
|
+
summary = data.get('summary', '').lower()
|
|
54
|
+
|
|
55
|
+
for imp in imports:
|
|
56
|
+
imp_name = imp.get('name', '').lower()
|
|
57
|
+
# extract just the module name
|
|
58
|
+
imp_name = imp.get('name', '').lower()
|
|
59
|
+
|
|
60
|
+
# handle "from x import y" → extract x
|
|
61
|
+
if imp_name.startswith('from '):
|
|
62
|
+
module = imp_name.split('from ')[1].split(' import')[0].strip()
|
|
63
|
+
# handle "import x" → extract x
|
|
64
|
+
elif imp_name.startswith('import '):
|
|
65
|
+
module = imp_name.replace('import ', '').split(' as ')[0].strip()
|
|
66
|
+
else:
|
|
67
|
+
continue
|
|
68
|
+
|
|
69
|
+
if not module:
|
|
70
|
+
continue
|
|
71
|
+
|
|
72
|
+
# check if module appears anywhere in file context
|
|
73
|
+
used = (
|
|
74
|
+
any(module in f for f in functions) or
|
|
75
|
+
any(module in c for c in classes) or
|
|
76
|
+
module in summary
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
if not used:
|
|
80
|
+
bugs.append({
|
|
81
|
+
'type': 'unused_import',
|
|
82
|
+
'severity': 'LOW',
|
|
83
|
+
'file': filepath,
|
|
84
|
+
'import': imp.get('name', ''),
|
|
85
|
+
'line': imp.get('line', 0),
|
|
86
|
+
'message': f"Possibly unused import '{module}' in {filepath}",
|
|
87
|
+
'fix': f"Remove 'import {module}' if not needed."
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
return bugs
|
|
91
|
+
|
|
92
|
+
def detect_bugs_with_llm(brain, G):
|
|
93
|
+
# calculate risk → filter HIGH risk files → send to Groq → find bugs
|
|
94
|
+
from brain_parser.graph_builder import calculate_risk
|
|
95
|
+
from groq import Groq
|
|
96
|
+
from dotenv import load_dotenv
|
|
97
|
+
load_dotenv()
|
|
98
|
+
|
|
99
|
+
risk = calculate_risk(G)
|
|
100
|
+
high_risk_files = [f for f, r in risk.items() if r == "HIGH"]
|
|
101
|
+
|
|
102
|
+
if not high_risk_files:
|
|
103
|
+
return []
|
|
104
|
+
|
|
105
|
+
client = Groq(api_key=os.getenv("GROQ_API_KEY"))
|
|
106
|
+
bugs = []
|
|
107
|
+
|
|
108
|
+
for filepath in high_risk_files:
|
|
109
|
+
data = brain.get(filepath)
|
|
110
|
+
if not data:
|
|
111
|
+
continue
|
|
112
|
+
|
|
113
|
+
summary = data.get('summary', '')
|
|
114
|
+
functions = [f['name'] for f in data.get('functions', [])]
|
|
115
|
+
imports = [i['name'] for i in data.get('imports', [])]
|
|
116
|
+
|
|
117
|
+
prompt = f"""You are a senior code reviewer. Analyze this file for bugs.
|
|
118
|
+
|
|
119
|
+
File: {filepath}
|
|
120
|
+
Summary: {summary}
|
|
121
|
+
Functions: {functions}
|
|
122
|
+
Imports: {imports}
|
|
123
|
+
|
|
124
|
+
Rules:
|
|
125
|
+
- Only report bugs you can prove from the information given
|
|
126
|
+
- Every bug must reference a specific function name from the functions list
|
|
127
|
+
- Do not invent bugs that are not evident from the summary
|
|
128
|
+
- If no provable bugs exist respond with exactly: NO BUGS
|
|
129
|
+
|
|
130
|
+
Format each bug exactly like this:
|
|
131
|
+
BUG: [function_name] description of specific problem
|
|
132
|
+
FIX: specific code change needed"""
|
|
133
|
+
|
|
134
|
+
try:
|
|
135
|
+
response = client.chat.completions.create(
|
|
136
|
+
model="llama-3.3-70b-versatile",
|
|
137
|
+
messages=[{"role": "user", "content": prompt}],
|
|
138
|
+
max_tokens=300,
|
|
139
|
+
temperature = 0
|
|
140
|
+
)
|
|
141
|
+
result = response.choices[0].message.content.strip()
|
|
142
|
+
|
|
143
|
+
if "NO BUGS" not in result:
|
|
144
|
+
bugs.append({
|
|
145
|
+
'type': 'llm_detected_bug',
|
|
146
|
+
'severity': 'HIGH',
|
|
147
|
+
'file': filepath,
|
|
148
|
+
'message': result,
|
|
149
|
+
'fix': 'See details above'
|
|
150
|
+
})
|
|
151
|
+
except Exception as e:
|
|
152
|
+
print(f"LLM bug detection error for {filepath}: {e}")
|
|
153
|
+
|
|
154
|
+
return bugs
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
DANGEROUS_IMPORTS = {
|
|
158
|
+
'pickle': 'Deserialization vulnerability - pickle can execute arbitrary code',
|
|
159
|
+
'subprocess': 'Potential command injection if user input is passed to subprocess',
|
|
160
|
+
'eval': 'Code injection risk - eval executes arbitrary Python code',
|
|
161
|
+
'exec': 'Code injection risk - exec executes arbitrary Python code',
|
|
162
|
+
'hashlib.md5': 'Weak hashing - MD5 is cryptographically broken for security use',
|
|
163
|
+
'tempfile': 'Insecure temporary file handling possible',
|
|
164
|
+
'yaml.load': 'Use yaml.safe_load instead - yaml.load can execute arbitrary code',
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def detect_security_antipatterns(brain):
|
|
169
|
+
# check imports against known dangerous patterns
|
|
170
|
+
bugs = []
|
|
171
|
+
|
|
172
|
+
for filepath, data in brain.items():
|
|
173
|
+
if data.get('language') != 'python':
|
|
174
|
+
continue
|
|
175
|
+
|
|
176
|
+
for imp in data.get('imports', []):
|
|
177
|
+
imp_name = imp.get('name', '').lower()
|
|
178
|
+
|
|
179
|
+
for dangerous, reason in DANGEROUS_IMPORTS.items():
|
|
180
|
+
if dangerous in imp_name:
|
|
181
|
+
bugs.append({
|
|
182
|
+
'type': 'security_antipattern',
|
|
183
|
+
'severity': 'HIGH',
|
|
184
|
+
'file': filepath,
|
|
185
|
+
'import': imp.get('name', ''),
|
|
186
|
+
'line': imp.get('line', 0),
|
|
187
|
+
'message': f"Security risk in {filepath} line {imp.get('line', 0)}: {reason}",
|
|
188
|
+
'fix': f"Review usage of '{dangerous}' and ensure it never processes untrusted input."
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
return bugs
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def run_all_detectors(brain=None, G=None, temp_path=None):
|
|
195
|
+
if not brain:
|
|
196
|
+
brain = load_brain()
|
|
197
|
+
if not brain:
|
|
198
|
+
return []
|
|
199
|
+
|
|
200
|
+
bugs = []
|
|
201
|
+
bugs.extend(detect_circular_dependencies(brain, temp_path))
|
|
202
|
+
bugs.extend(detect_security_antipatterns(brain))
|
|
203
|
+
|
|
204
|
+
return bugs
|
|
205
|
+
|
|
206
|
+
if __name__ == "__main__":
|
|
207
|
+
bugs = run_all_detectors()
|
|
208
|
+
print(f"\nFound {len(bugs)} potential bugs:\n")
|
|
209
|
+
for bug in bugs:
|
|
210
|
+
print(f"[{bug['severity']}] {bug['type']}")
|
|
211
|
+
print(f" {bug['message']}")
|
|
212
|
+
print(f" Fix: {bug['fix']}\n")
|