RE Analysis Dashboard with Local LLM
Build an IDA Pro export pipeline and AI-powered binary analysis dashboard.
Introduction
30 Second Summary
Reverse engineering a binary means staring at hundreds of decompiled functions and trying to figure out what each one does. For pentesters, that process is slow and repetitive, especially when you need to keep analysis completely offline.
In this project, you will build a local reverse engineering analysis dashboard that takes exported IDA Pro data and sends it to a local LLM for vulnerability detection, function renaming suggestions, and code summarization.
What You'll Build
A pentester-oriented web application that bridges IDA Pro and a local Ollama LLM for private, offline binary analysis.
By the end of this project, you'll have:
- An IDAPython export script that extracts decompiled functions, cross-references, strings, and metadata from any binary in IDA Pro as structured JSON.
- A FastAPI backend with an LLM analysis engine that performs vulnerability detection, function renaming, and code summarization using a local model.
- A web UI dashboard for browsing exported binary data and viewing AI-powered analysis results on each function.
- Secret Mission: Add batch analysis with progress tracking and export results to a Markdown report.
Are there any prerequisites?
This project assumes you have IDA Pro (with the Hex-Rays decompiler) installed on a separate machine, Python 3.12+ installed on your workstation, and Ollama installed and running locally.
You should be comfortable with Python and have basic familiarity with web development concepts.
Build the IDAPython Export Script
When you reverse engineer a binary in IDA Pro, the most valuable data lives inside the IDA database: decompiled pseudocode, cross-references between functions, and metadata about the binary's architecture.
In this step, you will write an IDAPython script that programmatically extracts all of that data into a structured JSON file. This JSON becomes the bridge between your air-gapped IDA machine and the local LLM analysis dashboard on your workstation.
In this step, get ready to:
- Write an IDAPython export script that iterates functions, extracts pseudocode, and collects cross-references.
- Run the script in IDA Pro on a sample binary and transfer the resulting JSON to your workstation.
Write the IDAPython export script
The export script uses IDA's internal APIs to walk every function in the binary, decompile each one using the Hex-Rays decompiler, and package the results as JSON. You will build it in four sections: metadata extraction, relationship helpers, the decompilation loop, and the entry point.
- In your editor, create a new file called ida_export.py in your project directory.
- Add the script docstring, imports, and the binary metadata function by pasting this code:
"""\nIDA Pro Export Script for RE Analysis Dashboard\nRun inside IDA Pro: File > Script file > ida_export.py\nRequires Hex-Rays decompiler for pseudocode extraction.\n"""\n\nimport json\nimport os\nimport ida_funcs\nimport ida_hexrays\nimport ida_name\nimport ida_nalt\nimport ida_ida\nimport ida_segment\nimport idautils\nimport idc\n\n\ndef get_binary_metadata():\n # Collect architecture and file info from IDA's database\n info = ida_ida.inf_get_procname()\n return {\n "file_name": ida_nalt.get_root_filename(),\n "architecture": info,\n "bits": 64 if ida_ida.inf_is_64bit() else 32,\n "image_base": hex(ida_nalt.get_imagebase()),\n }
What does this code do?
The imports pull in IDA's core modules: ida_funcs for function iteration, ida_hexrays for the decompiler API, idautils for high-level helpers like XrefsTo, and ida_name for resolving symbol names.
The get_binary_metadata() function reads the processor type, bitness, and image base address directly from IDA's internal database state. These fields identify the binary in the exported JSON.
- You won't see a change yet. The next chunk adds the relationship-extraction helpers this function depends on.
- Below get_binary_metadata(), add the two helper functions that extract cross-references and called functions:
\ndef get_function_xrefs_to(ea):\n # Collect up to 10 callers of this function\n xrefs = []\n for xref in idautils.XrefsTo(ea):\n caller_name = ida_name.get_ea_name(xref.frm)\n if caller_name:\n xrefs.append({"address": hex(xref.frm), "name": caller_name})\n return xrefs[:10]\n\n\ndef get_function_calls(ea):\n # Walk every instruction in the function to find outgoing calls\n calls = []\n func = ida_funcs.get_func(ea)\n if not func:\n return calls\n for head in idautils.Heads(func.start_ea, func.end_ea):\n for xref in idautils.XrefsFrom(head):\n target_func = ida_funcs.get_func(xref.to)\n if target_func and target_func.start_ea != ea:\n name = ida_name.get_ea_name(target_func.start_ea)\n if name and name not in [c["name"] for c in calls]:\n calls.append({"address": hex(target_func.start_ea), "name": name})\n return calls[:20]
What does this code do?
These two functions capture the call graph around each function. get_function_xrefs_to() finds up to 10 callers of a function using idautils.XrefsTo(). This tells you where a function is referenced from.
The get_function_calls() function walks every instruction head within the function's address range and checks outgoing cross-references. It collects up to 20 unique function calls. Together, these give the LLM context about how a function fits into the binary's overall control flow.
- You won't see a change yet. The next chunk adds the decompilation logic that uses these helpers.
- Below get_function_calls(), add the decompilation helper and the main export loop:
\ndef decompile_function(ea):\n # Attempt to decompile a function; return None on failure\n try:\n cfunc = ida_hexrays.decompile(ea)\n if cfunc:\n return str(cfunc)\n except ida_hexrays.DecompilationFailure:\n pass\n except Exception:\n pass\n return None\n\n\ndef export_functions():\n # Iterate all functions, skip library/thunk stubs, collect data\n functions = []\n for ea in idautils.Functions():\n func = ida_funcs.get_func(ea)\n if not func:\n continue\n\n flags = idc.get_func_flags(ea)\n if flags & (idc.FUNC_LIB | idc.FUNC_THUNK):\n continue\n\n name = ida_name.get_ea_name(ea)\n pseudocode = decompile_function(ea)\n\n if not pseudocode:\n continue\n\n func_data = {\n "address": hex(ea),\n "name": name or f"sub_{ea:X}",\n "size": func.size(),\n "pseudocode": pseudocode,\n "xrefs_to": get_function_xrefs_to(ea),\n "calls": get_function_calls(ea),\n }\n functions.append(func_data)\n\n return functions
What does this code do?
The decompile_function() wrapper calls ida_hexrays.decompile(ea) and converts the result to a string of pseudocode. It gracefully handles functions that fail to decompile.
The export_functions() loop is the core of the script. It iterates every function address via idautils.Functions(), skips library stubs and thunks (using FUNC_LIB and FUNC_THUNK flags), and only keeps functions that successfully decompile. This filters out noise and ensures the exported JSON contains only meaningful analysis targets.
- You won't see a change yet. The next chunk adds the entry point that ties everything together and writes the JSON file.
- Below export_functions(), add the main() entry point that orchestrates the export:
\ndef main():\n # Verify Hex-Rays is available before starting\n if not ida_hexrays.init_hexrays_plugin():\n print("[!] Hex-Rays decompiler not available.")\n return\n\n print("[*] Exporting binary analysis data...")\n metadata = get_binary_metadata()\n functions = export_functions()\n\n export_data = {\n "metadata": metadata,\n "functions": functions,\n "total_functions": len(functions),\n }\n\n # Write JSON to the same directory as the input binary\n output_dir = os.path.dirname(ida_nalt.get_input_file_path())\n output_file = os.path.join(output_dir, f"{metadata['file_name']}_export.json")\n\n with open(output_file, "w", encoding="utf-8") as f:\n json.dump(export_data, f, indent=2, ensure_ascii=False)\n\n print(f"[+] Exported {len(functions)} functions to: {output_file}")\n\n\nif __name__ == "__main__":\n main()
What does this code do?
The main() function first verifies that the Hex-Rays decompiler plugin is loaded. Without it, pseudocode extraction is impossible. It then calls get_binary_metadata() and export_functions() to collect all the data.
The output JSON is saved to the same directory as the input binary file. The filename is derived from the binary's name with an _export.json suffix. This makes it easy to locate after the script finishes.
- Save ida_export.py.
Seeing syntax highlighting errors?
Your local editor will show import errors for ida_funcs, ida_hexrays, and other IDA modules. This is expected. These modules only exist inside IDA Pro's embedded Python environment, not on your workstation.
The script will run correctly once loaded into IDA Pro in the next substep.
help me understand why my IDAPython imports show errors in VS Code
✔️ Awesome, I've got everything!
Great. Double check you have saved ida_export.py and move on to running it in IDA Pro.
ⓧ I'd like to double check the full code
"""\nIDA Pro Export Script for RE Analysis Dashboard\nRun inside IDA Pro: File > Script file > ida_export.py\nRequires Hex-Rays decompiler for pseudocode extraction.\n"""\n\nimport json\nimport os\nimport ida_funcs\nimport ida_hexrays\nimport ida_name\nimport ida_nalt\nimport ida_ida\nimport ida_segment\nimport idautils\nimport idc\n\n\ndef get_binary_metadata():\n info = ida_ida.inf_get_procname()\n return {\n "file_name": ida_nalt.get_root_filename(),\n "architecture": info,\n "bits": 64 if ida_ida.inf_is_64bit() else 32,\n "image_base": hex(ida_nalt.get_imagebase()),\n }\n\n\ndef get_function_xrefs_to(ea):\n xrefs = []\n for xref in idautils.XrefsTo(ea):\n caller_name = ida_name.get_ea_name(xref.frm)\n if caller_name:\n xrefs.append({"address": hex(xref.frm), "name": caller_name})\n return xrefs[:10]\n\n\ndef get_function_calls(ea):\n calls = []\n func = ida_funcs.get_func(ea)\n if not func:\n return calls\n for head in idautils.Heads(func.start_ea, func.end_ea):\n for xref in idautils.XrefsFrom(head):\n target_func = ida_funcs.get_func(xref.to)\n if target_func and target_func.start_ea != ea:\n name = ida_name.get_ea_name(target_func.start_ea)\n if name and name not in [c["name"] for c in calls]:\n calls.append({"address": hex(target_func.start_ea), "name": name})\n return calls[:20]\n\n\ndef decompile_function(ea):\n try:\n cfunc = ida_hexrays.decompile(ea)\n if cfunc:\n return str(cfunc)\n except ida_hexrays.DecompilationFailure:\n pass\n except Exception:\n pass\n return None\n\n\ndef export_functions():\n functions = []\n for ea in idautils.Functions():\n func = ida_funcs.get_func(ea)\n if not func:\n continue\n\n flags = idc.get_func_flags(ea)\n if flags & (idc.FUNC_LIB | idc.FUNC_THUNK):\n continue\n\n name = ida_name.get_ea_name(ea)\n pseudocode = decompile_function(ea)\n\n if not pseudocode:\n continue\n\n func_data = {\n "address": hex(ea),\n "name": name or f"sub_{ea:X}",\n "size": func.size(),\n "pseudocode": pseudocode,\n "xrefs_to": get_function_xrefs_to(ea),\n "calls": get_function_calls(ea),\n }\n functions.append(func_data)\n\n return functions\n\n\ndef main():\n if not ida_hexrays.init_hexrays_plugin():\n print("[!] Hex-Rays decompiler not available.")\n return\n\n print("[*] Exporting binary analysis data...")\n metadata = get_binary_metadata()\n functions = export_functions()\n\n export_data = {\n "metadata": metadata,\n "functions": functions,\n "total_functions": len(functions),\n }\n\n output_dir = os.path.dirname(ida_nalt.get_input_file_path())\n output_file = os.path.join(output_dir, f"{metadata['file_name']}_export.json")\n\n with open(output_file, "w", encoding="utf-8") as f:\n json.dump(export_data, f, indent=2, ensure_ascii=False)\n\n print(f"[+] Exported {len(functions)} functions to: {output_file}")\n\n\nif __name__ == "__main__":\n main()
Run the script in IDA Pro and transfer the JSON
Now you will execute the export script inside IDA Pro on a sample binary. The script runs within IDA's embedded Python interpreter, which has access to all the ida_* modules.
- On your IDA Pro machine, open a binary you want to analyze. Wait for IDA's auto-analysis to complete (the status bar at the bottom should show "idle").
- Transfer ida_export.py to your IDA machine (via SCP, USB, shared folder, or paste the contents into a new file).
- Click File in the top menu bar.
- Click Script file....
- Select ida_export.py and click Open.
What should I see?
IDA's output window will display progress messages. First you will see [*] Exporting binary analysis data... followed by a final message like [+] Exported 42 functions to: /path/to/binary_export.json. The exact number depends on your binary.
The JSON file is written to the same directory as the input binary. For large binaries, the script may take 30-60 seconds while Hex-Rays decompiles each function.
- Locate the _export.json file on the IDA machine (it is in the same directory as your binary).
- Transfer the JSON file back to your workstation and place it somewhere accessible (your Desktop or project folder works fine).
- Open the JSON file in your editor and confirm it contains a metadata object with file_name, architecture, bits, and image_base fields, plus a functions array where each entry has a pseudocode field.
Script prints "Hex-Rays decompiler not available"?
This means the Hex-Rays decompiler plugin is not loaded for the current binary. Make sure your IDA Pro license includes the decompiler and that the binary's architecture matches (x86/x64 for most Hex-Rays licenses).
If you see zero functions exported, your binary may consist entirely of library stubs. Try a different binary with user-written code.
help me troubleshoot why my IDA export script is not producing pseudocode
Your export pipeline is complete. You now have structured analysis data ready to feed into a local LLM. Next up, you will build the FastAPI backend and analysis engine that consumes this JSON and generates vulnerability insights.
Build the FastAPI Backend and LLM Analysis Engine
You have structured export data from IDA Pro sitting on your workstation. Now you need something to actually do things with that data.
In this step, you will build two Python files that form the core of your dashboard. The first is an analysis engine that sends decompiled pseudocode to your local Ollama model with specialized prompts. The second is a FastAPI server that exposes REST endpoints for uploading exports, browsing functions, and triggering analysis.
In this step, get ready to:
- Build an LLM analysis engine with three specialized RE prompts.
- Create a FastAPI backend with endpoints for upload, browsing, and analysis.
- Start the server and confirm the API responds.
Create the LLM analysis engine
The analysis engine is a standalone module that knows how to talk to Ollama. It contains three specialized system prompts and three matching functions, each tailored for a different RE analysis task.
- Create a new file called analyzer.py in your project root directory (the same folder as requirements.txt).
- Add the imports, model constant, and all three system prompts by pasting this code:
"""LLM Analysis Engine using Ollama for reverse engineering tasks."""
import ollama
MODEL = "qwen2.5-coder:7b"
VULN_SYSTEM_PROMPT = """You are an expert binary security researcher analyzing decompiled pseudocode from IDA Pro.
Analyze the function for potential security vulnerabilities. Focus on:
- Buffer overflows (stack and heap)
- Integer overflows/underflows
- Use-after-free conditions
- Format string vulnerabilities
- Command injection
- Unvalidated input handling
- Dangerous API usage (strcpy, sprintf, gets, etc.)
- Race conditions
- Memory leaks that could lead to DoS
For each finding, provide:
1. Vulnerability type
2. Affected code line or pattern
3. Severity (Critical/High/Medium/Low/Info)
4. Brief explanation of the risk
If no vulnerabilities are found, state that the function appears safe and explain why.
Be concise and actionable. This is for a pentester triaging a binary."""
RENAME_SYSTEM_PROMPT = """You are an expert reverse engineer analyzing decompiled pseudocode from IDA Pro.
Suggest meaningful names for the function and its variables based on the code behavior.
Provide your response in this exact format:
Function name: <suggested_name>
Variables:
- <original_name> -> <suggested_name>: <reason>
Only suggest renames where you have reasonable confidence. Do not rename variables
where the purpose is unclear. Focus on the most impactful renames that improve readability.
Keep names in snake_case for C conventions."""
SUMMARY_SYSTEM_PROMPT = """You are an expert reverse engineer analyzing decompiled pseudocode from IDA Pro.
Provide a concise summary of what this function does. Include:
1. Purpose: One sentence describing the function's role
2. Inputs: What parameters it takes and their likely meaning
3. Outputs: What it returns or modifies
4. Key operations: The main logic flow in 2-3 bullet points
5. Notable patterns: Any interesting patterns (crypto, networking, file I/O, anti-debug, etc.)
Be concise. This is for a pentester who needs to quickly understand function behavior."""
What do these prompts do?
Each system prompt gives the LLM a specific persona and output format for one type of RE analysis:
- VULN_SYSTEM_PROMPT tells the model to hunt for security bugs like buffer overflows, format strings, and dangerous API usage.
- RENAME_SYSTEM_PROMPT asks for meaningful function and variable names based on observed behavior.
- SUMMARY_SYSTEM_PROMPT requests a structured breakdown of what the function does.
The MODEL constant points at qwen2.5-coder:7b which you already pulled in Step 1. This model was identified as the top performer for x86 reverse engineering tasks among local open-weight LLMs.
- Below the system prompts, add the three analysis functions:
def analyze_vulnerabilities(pseudocode, func_name="unknown"):
context = f"Function: {func_name}\n\nPseudocode:\n{pseudocode}"
response = ollama.chat(
model=MODEL,
messages=[
{"role": "system", "content": VULN_SYSTEM_PROMPT},
{"role": "user", "content": context},
],
)
return response.message.content
def suggest_rename(pseudocode, func_name="unknown"):
context = f"Current function name: {func_name}\n\nPseudocode:\n{pseudocode}"
response = ollama.chat(
model=MODEL,
messages=[
{"role": "system", "content": RENAME_SYSTEM_PROMPT},
{"role": "user", "content": context},
],
)
return response.message.content
def summarize_function(pseudocode, func_name="unknown"):
context = f"Function: {func_name}\n\nPseudocode:\n{pseudocode}"
response = ollama.chat(
model=MODEL,
messages=[
{"role": "system", "content": SUMMARY_SYSTEM_PROMPT},
{"role": "user", "content": context},
],
)
return response.message.content
What does this code do?
Each function follows the same pattern: build a context string from the function name and pseudocode, send it to Ollama via ollama.chat() with the matching system prompt, and return the raw text response.
The ollama.chat() call sends a messages array (system + user) to the locally running Ollama server. The response object contains response.message.content with the LLM's text output.
- Save analyzer.py.
Seeing an import error for ollama?
Make sure you installed the dependencies from Step 1 in the same Python environment you are working in. Run pip install ollama again if needed.
help me fix an import error for the ollama package in my analyzer.py file
✔️ Awesome, I've got everything!
Great. Double check you have saved analyzer.py before moving on.
ⓧ I'd like to double check the full code
"""LLM Analysis Engine using Ollama for reverse engineering tasks."""
import ollama
MODEL = "qwen2.5-coder:7b"
VULN_SYSTEM_PROMPT = """You are an expert binary security researcher analyzing decompiled pseudocode from IDA Pro.
Analyze the function for potential security vulnerabilities. Focus on:
- Buffer overflows (stack and heap)
- Integer overflows/underflows
- Use-after-free conditions
- Format string vulnerabilities
- Command injection
- Unvalidated input handling
- Dangerous API usage (strcpy, sprintf, gets, etc.)
- Race conditions
- Memory leaks that could lead to DoS
For each finding, provide:
1. Vulnerability type
2. Affected code line or pattern
3. Severity (Critical/High/Medium/Low/Info)
4. Brief explanation of the risk
If no vulnerabilities are found, state that the function appears safe and explain why.
Be concise and actionable. This is for a pentester triaging a binary."""
RENAME_SYSTEM_PROMPT = """You are an expert reverse engineer analyzing decompiled pseudocode from IDA Pro.
Suggest meaningful names for the function and its variables based on the code behavior.
Provide your response in this exact format:
Function name: <suggested_name>
Variables:
- <original_name> -> <suggested_name>: <reason>
Only suggest renames where you have reasonable confidence. Do not rename variables
where the purpose is unclear. Focus on the most impactful renames that improve readability.
Keep names in snake_case for C conventions."""
SUMMARY_SYSTEM_PROMPT = """You are an expert reverse engineer analyzing decompiled pseudocode from IDA Pro.
Provide a concise summary of what this function does. Include:
1. Purpose: One sentence describing the function's role
2. Inputs: What parameters it takes and their likely meaning
3. Outputs: What it returns or modifies
4. Key operations: The main logic flow in 2-3 bullet points
5. Notable patterns: Any interesting patterns (crypto, networking, file I/O, anti-debug, etc.)
Be concise. This is for a pentester who needs to quickly understand function behavior."""
def analyze_vulnerabilities(pseudocode, func_name="unknown"):
context = f"Function: {func_name}\n\nPseudocode:\n{pseudocode}"
response = ollama.chat(
model=MODEL,
messages=[
{"role": "system", "content": VULN_SYSTEM_PROMPT},
{"role": "user", "content": context},
],
)
return response.message.content
def suggest_rename(pseudocode, func_name="unknown"):
context = f"Current function name: {func_name}\n\nPseudocode:\n{pseudocode}"
response = ollama.chat(
model=MODEL,
messages=[
{"role": "system", "content": RENAME_SYSTEM_PROMPT},
{"role": "user", "content": context},
],
)
return response.message.content
def summarize_function(pseudocode, func_name="unknown"):
context = f"Function: {func_name}\n\nPseudocode:\n{pseudocode}"
response = ollama.chat(
model=MODEL,
messages=[
{"role": "system", "content": SUMMARY_SYSTEM_PROMPT},
{"role": "user", "content": context},
],
)
return response.message.content
Create the FastAPI backend
The backend is a FastAPI application that receives the exported JSON, stores it in memory, and exposes endpoints for the web UI to call. It imports your analyzer module to trigger LLM analysis on demand.
- Create the templates/ and static/ directories that FastAPI needs for serving the UI (you will fill these in the next step) by running:
mkdir templates static
- Confirm the directories exist by running:
ls
You should see templates and static listed alongside your existing files.
- Create a new file called main.py in your project root. Add the imports, app setup, and the upload endpoint by pasting this code:
"""FastAPI backend for the RE Analysis Dashboard."""
import json
from fastapi import FastAPI, File, Request, UploadFile
from fastapi.responses import HTMLResponse, JSONResponse, PlainTextResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from analyzer import analyze_vulnerabilities, suggest_rename, summarize_function
app = FastAPI(title="RE Analysis Dashboard")
app.mount("/static", StaticFiles(directory="static"), name="static")
templates = Jinja2Templates(directory="templates")
binary_data = {"metadata": {}, "functions": [], "analysis_results": {}}
@app.get("/", response_class=HTMLResponse)
async def dashboard(request: Request):
return templates.TemplateResponse("index.html", {"request": request})
@app.post("/upload")
async def upload_export(file: UploadFile = File(...)):
content = await file.read()
data = json.loads(content)
binary_data["metadata"] = data.get("metadata", {})
binary_data["functions"] = data.get("functions", [])
binary_data["analysis_results"] = {}
return {
"status": "ok",
"metadata": binary_data["metadata"],
"total_functions": len(binary_data["functions"]),
}
What does this code do?
- binary_data is a simple in-memory dictionary that holds the uploaded export. No database needed for a local tool.
- The POST /upload endpoint reads the uploaded JSON file, parses it, and stores the metadata and functions list. It also resets any previous analysis results.
- StaticFiles and Jinja2Templates point at the directories you just created. The UI files will go there in the next step.
- Below the upload endpoint, add the function browsing endpoints:
@app.get("/functions")
async def list_functions():
func_list = []
for func in binary_data["functions"]:
func_list.append(
{
"address": func["address"],
"name": func["name"],
"size": func["size"],
"has_analysis": func["address"] in binary_data["analysis_results"],
}
)
return {"functions": func_list}
@app.get("/function/{address}")
async def get_function(address: str):
for func in binary_data["functions"]:
if func["address"] == address:
return {
"address": func["address"],
"name": func["name"],
"size": func["size"],
"pseudocode": func["pseudocode"],
"xrefs_to": func.get("xrefs_to", []),
"calls": func.get("calls", []),
"analysis": binary_data["analysis_results"].get(address),
}
return JSONResponse(status_code=404, content={"error": "Function not found"})
What do these endpoints do?
- GET /functions returns a lightweight list of all functions (address, name, size) plus whether each one has been analyzed yet. The UI uses this to populate the sidebar.
- GET /function/{address} returns full details for a single function including pseudocode, cross-references, calls, and any existing analysis results.
- Below the function browsing endpoints, add the analysis trigger endpoint:
@app.post("/analyze/{address}")
async def analyze_function(address: str, analysis_type: str = "all"):
target = None
for func in binary_data["functions"]:
if func["address"] == address:
target = func
break
if not target:
return JSONResponse(status_code=404, content={"error": "Function not found"})
pseudocode = target["pseudocode"]
func_name = target["name"]
results = {}
if analysis_type in ("all", "vulnerabilities"):
results["vulnerabilities"] = analyze_vulnerabilities(pseudocode, func_name)
if analysis_type in ("all", "rename"):
results["rename"] = suggest_rename(pseudocode, func_name)
if analysis_type in ("all", "summary"):
results["summary"] = summarize_function(pseudocode, func_name)
binary_data["analysis_results"][address] = results
return {"address": address, "analysis": results}
What does this endpoint do?
POST /analyze/{address} is the core of the dashboard. It looks up the function by address, then calls whichever analyzer functions the analysis_type query parameter requests. Passing all runs all three analyses in sequence.
Results are stored in binary_data["analysis_results"] keyed by address so the UI can retrieve them later without re-running inference.
- Finally, add the report export endpoint at the bottom of the file:
@app.get("/export/report", response_class=PlainTextResponse)
async def export_report():
meta = binary_data["metadata"]
lines = [
f"# RE Analysis Report: {meta.get('file_name', 'Unknown')}",
"",
f"- Architecture: {meta.get('architecture', 'N/A')}",
f"- Bits: {meta.get('bits', 'N/A')}",
f"- Image Base: {meta.get('image_base', 'N/A')}",
f"- Functions Analyzed: {len(binary_data['analysis_results'])}",
"",
"---",
"",
]
for address, results in binary_data["analysis_results"].items():
func_name = address
for func in binary_data["functions"]:
if func["address"] == address:
func_name = func["name"]
break
lines.append(f"## {func_name} ({address})")
lines.append("")
if "summary" in results:
lines.append("### Summary")
lines.append(results["summary"])
lines.append("")
if "vulnerabilities" in results:
lines.append("### Vulnerabilities")
lines.append(results["vulnerabilities"])
lines.append("")
if "rename" in results:
lines.append("### Rename Suggestions")
lines.append(results["rename"])
lines.append("")
lines.append("---")
lines.append("")
return "\n".join(lines)
What does the export endpoint do?
GET /export/report assembles all completed analysis results into a Markdown document. It includes binary metadata at the top and then each analyzed function with its summary, vulnerability findings, and rename suggestions.
This gives you a downloadable report you can attach to a pentest engagement or share with your team.
- Save main.py.
Seeing a syntax error when you save?
Make sure each code chunk was pasted below the previous one in the correct order. The endpoints should appear one after another with no duplicate function definitions.
help me fix a syntax error in my main.py FastAPI backend
✔️ Awesome, I've got everything!
Great. Double check you have saved main.py before moving on.
ⓧ I'd like to double check the full code
"""FastAPI backend for the RE Analysis Dashboard."""
import json
from fastapi import FastAPI, File, Request, UploadFile
from fastapi.responses import HTMLResponse, JSONResponse, PlainTextResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from analyzer import analyze_vulnerabilities, suggest_rename, summarize_function
app = FastAPI(title="RE Analysis Dashboard")
app.mount("/static", StaticFiles(directory="static"), name="static")
templates = Jinja2Templates(directory="templates")
binary_data = {"metadata": {}, "functions": [], "analysis_results": {}}
@app.get("/", response_class=HTMLResponse)
async def dashboard(request: Request):
return templates.TemplateResponse("index.html", {"request": request})
@app.post("/upload")
async def upload_export(file: UploadFile = File(...)):
content = await file.read()
data = json.loads(content)
binary_data["metadata"] = data.get("metadata", {})
binary_data["functions"] = data.get("functions", [])
binary_data["analysis_results"] = {}
return {
"status": "ok",
"metadata": binary_data["metadata"],
"total_functions": len(binary_data["functions"]),
}
@app.get("/functions")
async def list_functions():
func_list = []
for func in binary_data["functions"]:
func_list.append(
{
"address": func["address"],
"name": func["name"],
"size": func["size"],
"has_analysis": func["address"] in binary_data["analysis_results"],
}
)
return {"functions": func_list}
@app.get("/function/{address}")
async def get_function(address: str):
for func in binary_data["functions"]:
if func["address"] == address:
return {
"address": func["address"],
"name": func["name"],
"size": func["size"],
"pseudocode": func["pseudocode"],
"xrefs_to": func.get("xrefs_to", []),
"calls": func.get("calls", []),
"analysis": binary_data["analysis_results"].get(address),
}
return JSONResponse(status_code=404, content={"error": "Function not found"})
@app.post("/analyze/{address}")
async def analyze_function(address: str, analysis_type: str = "all"):
target = None
for func in binary_data["functions"]:
if func["address"] == address:
target = func
break
if not target:
return JSONResponse(status_code=404, content={"error": "Function not found"})
pseudocode = target["pseudocode"]
func_name = target["name"]
results = {}
if analysis_type in ("all", "vulnerabilities"):
results["vulnerabilities"] = analyze_vulnerabilities(pseudocode, func_name)
if analysis_type in ("all", "rename"):
results["rename"] = suggest_rename(pseudocode, func_name)
if analysis_type in ("all", "summary"):
results["summary"] = summarize_function(pseudocode, func_name)
binary_data["analysis_results"][address] = results
return {"address": address, "analysis": results}
@app.get("/export/report", response_class=PlainTextResponse)
async def export_report():
meta = binary_data["metadata"]
lines = [
f"# RE Analysis Report: {meta.get('file_name', 'Unknown')}",
"",
f"- Architecture: {meta.get('architecture', 'N/A')}",
f"- Bits: {meta.get('bits', 'N/A')}",
f"- Image Base: {meta.get('image_base', 'N/A')}",
f"- Functions Analyzed: {len(binary_data['analysis_results'])}",
"",
"---",
"",
]
for address, results in binary_data["analysis_results"].items():
func_name = address
for func in binary_data["functions"]:
if func["address"] == address:
func_name = func["name"]
break
lines.append(f"## {func_name} ({address})")
lines.append("")
if "summary" in results:
lines.append("### Summary")
lines.append(results["summary"])
lines.append("")
if "vulnerabilities" in results:
lines.append("### Vulnerabilities")
lines.append(results["vulnerabilities"])
lines.append("")
if "rename" in results:
lines.append("### Rename Suggestions")
lines.append(results["rename"])
lines.append("")
lines.append("---")
lines.append("")
return "\n".join(lines)
Start the server and test the API
With both files in place, you can start the FastAPI development server and confirm the API is responding.
- Start the development server by running:
uvicorn main:app --reload
What does this command do?
uvicorn is the ASGI server that runs your FastAPI application. main:app tells it to look for the app object in main.py. The --reload flag makes it restart automatically when you change code.
You should see output indicating the server is running on http://127.0.0.1:8000.
- Open a second terminal and test the functions endpoint by running:
curl http://127.0.0.1:8000/functions
You should see {"functions":[]} as the response. The empty list is expected because you have not uploaded any data yet.
Avoid hitting the homepage for now
The GET / route will return a template error because templates/index.html does not exist yet. You will create it in the next step. The API endpoints (/functions, /upload, /analyze/{address}) all work independently of the template.
Server won't start?
- If you see a RuntimeError about the static directory, make sure you ran the mkdir templates static command earlier. FastAPI's StaticFiles check requires the directory to exist at startup.
- If you see an ImportError for analyzer, confirm analyzer.py is in the same folder as main.py.
help me fix an error starting my FastAPI uvicorn server for the RE Analysis Dashboard
Your backend is live and ready to receive data. In the next step, you will build the web UI that ties everything together into a visual dashboard.
Build the Web UI Dashboard
Your backend is fully wired up. The FastAPI server can receive exported JSON, serve individual functions, and trigger LLM analysis on demand.
Now you need a way to actually interact with all of that. In this step, you will build a browser-based dashboard where you can upload your IDA export, browse functions, read their pseudocode, and trigger vulnerability analysis with a single click.
In this step, get ready to:
- Create the HTML template with a pentester-oriented split-panel layout.
- Add JavaScript for file upload, function selection, and LLM analysis triggers.
- Style the dashboard with a dark theme suited for security tooling.
Create the HTML template
The dashboard layout follows a common pattern for security tools: a narrow left panel for navigation and a wide right panel for content. The header holds the file upload control so you can load new exports at any time.
- Create a new file called index.html inside the templates folder, then paste the following code:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>RE Analysis Dashboard</title>
<link rel="stylesheet" href="/static/style.css">
</head>
<body>
<header>
<h1>RE Analysis Dashboard</h1>
<div id="upload-area">
<input type="file" id="file-input" accept=".json">
<button id="upload-btn" onclick="uploadFile()">Upload IDA Export</button>
<span id="upload-status"></span>
</div>
<div id="binary-info"></div>
</header>
<main>
<aside id="function-list">
<h2>Functions</h2>
<input type="text" id="search-box" placeholder="Filter functions..." oninput="filterFunctions()">
<ul id="func-list"></ul>
</aside>
<section id="analysis-panel">
<div id="function-header">
<h2 id="func-title">Select a function</h2>
<div id="func-meta"></div>
</div>
<div id="tabs">
<button class="tab active" onclick="switchTab('pseudocode')">Pseudocode</button>
<button class="tab" onclick="switchTab('vulnerabilities')">Vulnerabilities</button>
<button class="tab" onclick="switchTab('rename')">Rename</button>
<button class="tab" onclick="switchTab('summary')">Summary</button>
</div>
<div id="tab-content">
<div id="tab-pseudocode" class="tab-pane active">
<pre><code id="pseudocode-content">No function selected.</code></pre>
</div>
<div id="tab-vulnerabilities" class="tab-pane">
<button class="analyze-btn" onclick="runAnalysis('vulnerabilities')">Analyze Vulnerabilities</button>
<div id="vuln-results" class="results"></div>
</div>
<div id="tab-rename" class="tab-pane">
<button class="analyze-btn" onclick="runAnalysis('rename')">Suggest Renames</button>
<div id="rename-results" class="results"></div>
</div>
<div id="tab-summary" class="tab-pane">
<button class="analyze-btn" onclick="runAnalysis('summary')">Summarize Function</button>
<div id="summary-results" class="results"></div>
</div>
</div>
<div id="action-bar">
<button onclick="runAnalysis('all')">Run Full Analysis</button>
<button onclick="exportReport()">Export Report</button>
</div>
</section>
</main>
<script src="/static/app.js"></script>
</body>
</html>
What does this template do?
- The <header> contains the file picker and upload button. The binary-info span displays metadata after a successful upload.
- The <aside> is the left sidebar with a search box and a scrollable <ul> where function names populate dynamically.
- The right panel uses four tab-pane divs. Only the active pane is visible. Each analysis tab has its own trigger button so you can run analyses selectively.
- The action-bar at the bottom provides "Run Full Analysis" (all three checks at once) and "Export Report" (downloads a Markdown file).
- Save templates/index.html.
You will not see a visual change yet. The JavaScript and CSS files are not in place.
✔️ Awesome, I've got everything!
Great. Make sure you saved the file before moving on.
ⓧ I'd like to double check the full code
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>RE Analysis Dashboard</title>
<link rel="stylesheet" href="/static/style.css">
</head>
<body>
<header>
<h1>RE Analysis Dashboard</h1>
<div id="upload-area">
<input type="file" id="file-input" accept=".json">
<button id="upload-btn" onclick="uploadFile()">Upload IDA Export</button>
<span id="upload-status"></span>
</div>
<div id="binary-info"></div>
</header>
<main>
<aside id="function-list">
<h2>Functions</h2>
<input type="text" id="search-box" placeholder="Filter functions..." oninput="filterFunctions()">
<ul id="func-list"></ul>
</aside>
<section id="analysis-panel">
<div id="function-header">
<h2 id="func-title">Select a function</h2>
<div id="func-meta"></div>
</div>
<div id="tabs">
<button class="tab active" onclick="switchTab('pseudocode')">Pseudocode</button>
<button class="tab" onclick="switchTab('vulnerabilities')">Vulnerabilities</button>
<button class="tab" onclick="switchTab('rename')">Rename</button>
<button class="tab" onclick="switchTab('summary')">Summary</button>
</div>
<div id="tab-content">
<div id="tab-pseudocode" class="tab-pane active">
<pre><code id="pseudocode-content">No function selected.</code></pre>
</div>
<div id="tab-vulnerabilities" class="tab-pane">
<button class="analyze-btn" onclick="runAnalysis('vulnerabilities')">Analyze Vulnerabilities</button>
<div id="vuln-results" class="results"></div>
</div>
<div id="tab-rename" class="tab-pane">
<button class="analyze-btn" onclick="runAnalysis('rename')">Suggest Renames</button>
<div id="rename-results" class="results"></div>
</div>
<div id="tab-summary" class="tab-pane">
<button class="analyze-btn" onclick="runAnalysis('summary')">Summarize Function</button>
<div id="summary-results" class="results"></div>
</div>
</div>
<div id="action-bar">
<button onclick="runAnalysis('all')">Run Full Analysis</button>
<button onclick="exportReport()">Export Report</button>
</div>
</section>
</main>
<script src="/static/app.js"></script>
</body>
</html>
Add JavaScript interactivity
The JavaScript handles all communication between the browser and your FastAPI backend. It manages file uploads, populates the function list, and sends analysis requests to the LLM endpoints.
- Create a new file called app.js inside the static folder.
- Add the state variables and upload/list management functions by pasting this code:
let currentAddress = null;
let functions = [];
async function uploadFile() {
const fileInput = document.getElementById("file-input");
const status = document.getElementById("upload-status");
if (!fileInput.files.length) {
status.textContent = "No file selected.";
status.style.color = "#f44336";
return;
}
const formData = new FormData();
formData.append("file", fileInput.files[0]);
status.textContent = "Uploading...";
status.style.color = "#ff9800";
const response = await fetch("/upload", { method: "POST", body: formData });
const data = await response.json();
if (data.status === "ok") {
status.textContent = `Loaded ${data.total_functions} functions`;
status.style.color = "#4caf50";
const meta = data.metadata;
document.getElementById("binary-info").textContent =
`${meta.file_name} | ${meta.architecture} | ${meta.bits}-bit | Base: ${meta.image_base}`;
await loadFunctions();
} else {
status.textContent = "Upload failed.";
status.style.color = "#f44336";
}
}
async function loadFunctions() {
const response = await fetch("/functions");
const data = await response.json();
functions = data.functions;
renderFunctionList(functions);
}
function renderFunctionList(funcs) {
const list = document.getElementById("func-list");
list.innerHTML = "";
for (const func of funcs) {
const li = document.createElement("li");
li.innerHTML = `${func.name} <span class="addr">${func.address}</span>`;
li.dataset.address = func.address;
if (func.has_analysis) li.classList.add("analyzed");
if (func.address === currentAddress) li.classList.add("active");
li.onclick = () => selectFunction(func.address);
list.appendChild(li);
}
}
function filterFunctions() {
const query = document.getElementById("search-box").value.toLowerCase();
const filtered = functions.filter(
(f) => f.name.toLowerCase().includes(query) || f.address.toLowerCase().includes(query)
);
renderFunctionList(filtered);
}
What does this code do?
- currentAddress and functions track global state. The currently selected function address and the full list of loaded functions live here.
- uploadFile() sends the JSON file to POST /upload, updates the status text with color feedback, then calls loadFunctions() to populate the sidebar.
- renderFunctionList() builds the sidebar <li> elements dynamically. It marks previously-analyzed functions with the analyzed class so CSS can show an indicator.
- filterFunctions() runs on every keystroke in the search box, filtering by name or address.
You will not see a change yet. The next chunk adds the selection and analysis logic that makes the UI functional.
- Below the filterFunctions() function, add the selection, analysis, and export functions:
async function selectFunction(address) {
currentAddress = address;
document.querySelectorAll("#func-list li").forEach((li) => {
li.classList.toggle("active", li.dataset.address === address);
});
const response = await fetch(`/function/${address}`);
const func = await response.json();
document.getElementById("func-title").textContent = func.name;
document.getElementById("func-meta").textContent =
`Address: ${func.address} | Size: ${func.size} bytes | XRefs: ${func.xrefs_to.length} | Calls: ${func.calls.length}`;
document.getElementById("pseudocode-content").textContent = func.pseudocode;
if (func.analysis) {
if (func.analysis.vulnerabilities) {
document.getElementById("vuln-results").textContent = func.analysis.vulnerabilities;
}
if (func.analysis.rename) {
document.getElementById("rename-results").textContent = func.analysis.rename;
}
if (func.analysis.summary) {
document.getElementById("summary-results").textContent = func.analysis.summary;
}
} else {
document.getElementById("vuln-results").textContent = "";
document.getElementById("rename-results").textContent = "";
document.getElementById("summary-results").textContent = "";
}
switchTab("pseudocode");
}
function switchTab(tabName) {
document.querySelectorAll(".tab").forEach((t) => t.classList.remove("active"));
document.querySelectorAll(".tab-pane").forEach((p) => p.classList.remove("active"));
document.querySelector(`.tab[onclick="switchTab('${tabName}')"]`).classList.add("active");
document.getElementById(`tab-${tabName}`).classList.add("active");
}
async function runAnalysis(type) {
if (!currentAddress) return;
const buttons = document.querySelectorAll(".analyze-btn, #action-bar button");
buttons.forEach((b) => (b.disabled = true));
const resultMap = {
vulnerabilities: "vuln-results",
rename: "rename-results",
summary: "summary-results",
};
if (type === "all") {
for (const key of Object.keys(resultMap)) {
document.getElementById(resultMap[key]).textContent = "Analyzing...";
}
} else {
document.getElementById(resultMap[type]).textContent = "Analyzing...";
}
const response = await fetch(`/analyze/${currentAddress}?analysis_type=${type}`, {
method: "POST",
});
const data = await response.json();
if (data.analysis) {
if (data.analysis.vulnerabilities) {
document.getElementById("vuln-results").textContent = data.analysis.vulnerabilities;
}
if (data.analysis.rename) {
document.getElementById("rename-results").textContent = data.analysis.rename;
}
if (data.analysis.summary) {
document.getElementById("summary-results").textContent = data.analysis.summary;
}
}
buttons.forEach((b) => (b.disabled = false));
await loadFunctions();
}
async function exportReport() {
const response = await fetch("/export/report");
const text = await response.text();
const blob = new Blob([text], { type: "text/markdown" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "re_analysis_report.md";
a.click();
URL.revokeObjectURL(url);
}
What does this code do?
- selectFunction() fetches a single function's details from GET /function/{address} and fills in the pseudocode panel. If analysis results already exist, it displays them immediately.
- runAnalysis() disables all buttons (preventing double-clicks), shows "Analyzing..." in the result divs, then POSTs to /analyze/{address}. When the LLM response returns, it populates the correct tab.
- exportReport() fetches the Markdown report from GET /export/report and triggers a browser download. No server-side file writing needed.
- Save static/app.js.
The dashboard will not look right until you add the stylesheet. The next chunk handles that.
Seeing a syntax error in your editor?
Template literal syntax (backtick strings with ${...}) requires ES6. Make sure your editor is not set to an older JavaScript standard.
Also confirm you have matching backticks around all template literals. A single missing backtick breaks the entire file.
Help me debug a syntax error in my app.js file for the RE Analysis Dashboard
✔️ Awesome, I've got everything!
Great. Double check you saved the file before continuing.
ⓧ I'd like to double check the full code
let currentAddress = null;
let functions = [];
async function uploadFile() {
const fileInput = document.getElementById("file-input");
const status = document.getElementById("upload-status");
if (!fileInput.files.length) {
status.textContent = "No file selected.";
status.style.color = "#f44336";
return;
}
const formData = new FormData();
formData.append("file", fileInput.files[0]);
status.textContent = "Uploading...";
status.style.color = "#ff9800";
const response = await fetch("/upload", { method: "POST", body: formData });
const data = await response.json();
if (data.status === "ok") {
status.textContent = `Loaded ${data.total_functions} functions`;
status.style.color = "#4caf50";
const meta = data.metadata;
document.getElementById("binary-info").textContent =
`${meta.file_name} | ${meta.architecture} | ${meta.bits}-bit | Base: ${meta.image_base}`;
await loadFunctions();
} else {
status.textContent = "Upload failed.";
status.style.color = "#f44336";
}
}
async function loadFunctions() {
const response = await fetch("/functions");
const data = await response.json();
functions = data.functions;
renderFunctionList(functions);
}
function renderFunctionList(funcs) {
const list = document.getElementById("func-list");
list.innerHTML = "";
for (const func of funcs) {
const li = document.createElement("li");
li.innerHTML = `${func.name} <span class="addr">${func.address}</span>`;
li.dataset.address = func.address;
if (func.has_analysis) li.classList.add("analyzed");
if (func.address === currentAddress) li.classList.add("active");
li.onclick = () => selectFunction(func.address);
list.appendChild(li);
}
}
function filterFunctions() {
const query = document.getElementById("search-box").value.toLowerCase();
const filtered = functions.filter(
(f) => f.name.toLowerCase().includes(query) || f.address.toLowerCase().includes(query)
);
renderFunctionList(filtered);
}
async function selectFunction(address) {
currentAddress = address;
document.querySelectorAll("#func-list li").forEach((li) => {
li.classList.toggle("active", li.dataset.address === address);
});
const response = await fetch(`/function/${address}`);
const func = await response.json();
document.getElementById("func-title").textContent = func.name;
document.getElementById("func-meta").textContent =
`Address: ${func.address} | Size: ${func.size} bytes | XRefs: ${func.xrefs_to.length} | Calls: ${func.calls.length}`;
document.getElementById("pseudocode-content").textContent = func.pseudocode;
if (func.analysis) {
if (func.analysis.vulnerabilities) {
document.getElementById("vuln-results").textContent = func.analysis.vulnerabilities;
}
if (func.analysis.rename) {
document.getElementById("rename-results").textContent = func.analysis.rename;
}
if (func.analysis.summary) {
document.getElementById("summary-results").textContent = func.analysis.summary;
}
} else {
document.getElementById("vuln-results").textContent = "";
document.getElementById("rename-results").textContent = "";
document.getElementById("summary-results").textContent = "";
}
switchTab("pseudocode");
}
function switchTab(tabName) {
document.querySelectorAll(".tab").forEach((t) => t.classList.remove("active"));
document.querySelectorAll(".tab-pane").forEach((p) => p.classList.remove("active"));
document.querySelector(`.tab[onclick="switchTab('${tabName}')"]`).classList.add("active");
document.getElementById(`tab-${tabName}`).classList.add("active");
}
async function runAnalysis(type) {
if (!currentAddress) return;
const buttons = document.querySelectorAll(".analyze-btn, #action-bar button");
buttons.forEach((b) => (b.disabled = true));
const resultMap = {
vulnerabilities: "vuln-results",
rename: "rename-results",
summary: "summary-results",
};
if (type === "all") {
for (const key of Object.keys(resultMap)) {
document.getElementById(resultMap[key]).textContent = "Analyzing...";
}
} else {
document.getElementById(resultMap[type]).textContent = "Analyzing...";
}
const response = await fetch(`/analyze/${currentAddress}?analysis_type=${type}`, {
method: "POST",
});
const data = await response.json();
if (data.analysis) {
if (data.analysis.vulnerabilities) {
document.getElementById("vuln-results").textContent = data.analysis.vulnerabilities;
}
if (data.analysis.rename) {
document.getElementById("rename-results").textContent = data.analysis.rename;
}
if (data.analysis.summary) {
document.getElementById("summary-results").textContent = data.analysis.summary;
}
}
buttons.forEach((b) => (b.disabled = false));
await loadFunctions();
}
async function exportReport() {
const response = await fetch("/export/report");
const text = await response.text();
const blob = new Blob([text], { type: "text/markdown" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "re_analysis_report.md";
a.click();
URL.revokeObjectURL(url);
}
Style the dashboard with a dark theme
Security tools look best on a dark background. The stylesheet uses a monospace font stack and a navy/cyan color scheme that is easy on the eyes during long analysis sessions.
- Create a new file called style.css inside the static folder, then paste the following code:
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: "JetBrains Mono", "Fira Code", monospace;
background: #1a1a2e;
color: #e0e0e0;
height: 100vh;
display: flex;
flex-direction: column;
}
header {
background: #16213e;
padding: 12px 20px;
border-bottom: 1px solid #0f3460;
display: flex;
align-items: center;
gap: 20px;
flex-wrap: wrap;
}
header h1 {
color: #00d4ff;
font-size: 1.2rem;
}
#upload-area {
display: flex;
align-items: center;
gap: 10px;
}
#upload-area input[type="file"] {
color: #e0e0e0;
font-size: 0.85rem;
}
#upload-status {
color: #4caf50;
font-size: 0.85rem;
}
#binary-info {
color: #aaa;
font-size: 0.8rem;
margin-left: auto;
}
main {
display: flex;
flex: 1;
overflow: hidden;
}
aside#function-list {
width: 280px;
background: #16213e;
border-right: 1px solid #0f3460;
display: flex;
flex-direction: column;
overflow: hidden;
}
aside h2 {
padding: 12px 16px;
font-size: 0.9rem;
color: #00d4ff;
border-bottom: 1px solid #0f3460;
}
#search-box {
margin: 8px;
padding: 6px 10px;
background: #1a1a2e;
border: 1px solid #0f3460;
color: #e0e0e0;
border-radius: 4px;
font-size: 0.8rem;
}
#func-list {
list-style: none;
overflow-y: auto;
flex: 1;
}
#func-list li {
padding: 8px 16px;
cursor: pointer;
border-bottom: 1px solid #0f3460;
font-size: 0.8rem;
transition: background 0.15s;
}
#func-list li:hover {
background: #1a1a2e;
}
#func-list li.active {
background: #0f3460;
border-left: 3px solid #00d4ff;
}
#func-list li .addr {
color: #888;
font-size: 0.7rem;
}
#func-list li.analyzed::after {
content: " [A]";
color: #4caf50;
font-size: 0.7rem;
}
section#analysis-panel {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
#function-header {
padding: 12px 20px;
border-bottom: 1px solid #0f3460;
}
#func-title {
font-size: 1rem;
color: #e0e0e0;
}
#func-meta {
font-size: 0.8rem;
color: #888;
margin-top: 4px;
}
#tabs {
display: flex;
border-bottom: 1px solid #0f3460;
background: #16213e;
}
#tabs .tab {
padding: 10px 20px;
background: none;
border: none;
color: #888;
cursor: pointer;
font-size: 0.85rem;
border-bottom: 2px solid transparent;
transition: all 0.15s;
}
#tabs .tab:hover {
color: #e0e0e0;
}
#tabs .tab.active {
color: #00d4ff;
border-bottom-color: #00d4ff;
}
#tab-content {
flex: 1;
overflow-y: auto;
padding: 16px 20px;
}
.tab-pane {
display: none;
}
.tab-pane.active {
display: block;
}
#tab-pseudocode pre {
background: #0d1117;
padding: 16px;
border-radius: 6px;
overflow-x: auto;
font-size: 0.82rem;
line-height: 1.5;
}
.analyze-btn {
margin-bottom: 12px;
padding: 8px 16px;
background: #0f3460;
color: #00d4ff;
border: 1px solid #00d4ff;
border-radius: 4px;
cursor: pointer;
font-size: 0.85rem;
transition: background 0.15s;
}
.analyze-btn:hover {
background: #1a4a7a;
}
.analyze-btn:disabled {
opacity: 0.5;
cursor: wait;
}
.results {
background: #0d1117;
padding: 16px;
border-radius: 6px;
white-space: pre-wrap;
font-size: 0.82rem;
line-height: 1.5;
}
#action-bar {
padding: 12px 20px;
border-top: 1px solid #0f3460;
display: flex;
gap: 10px;
}
#action-bar button {
padding: 8px 16px;
background: #0f3460;
color: #00d4ff;
border: 1px solid #00d4ff;
border-radius: 4px;
cursor: pointer;
font-size: 0.85rem;
}
#action-bar button:hover {
background: #1a4a7a;
}
button {
font-family: inherit;
}
What does this stylesheet do?
- The global reset and body rule set up a full-viewport flexbox layout with a dark navy background (#1a1a2e) and monospace font stack.
- The sidebar is fixed at 280px wide with a scrollable function list. Active items get a cyan left border to mark selection.
- Tabs use an underline indicator pattern. The active tab shows a cyan bottom border. Analysis result panes have a darker inset background (#0d1117) with white-space: pre-wrap to preserve the LLM output formatting.
- Buttons use a consistent outlined style with hover transitions. Disabled buttons (during analysis) reduce opacity and swap the cursor to wait.
- Save static/style.css.
- Return to your terminal and stop the running server with Ctrl+C, then restart it by running:
uvicorn main:app --reload
- Open your browser to http://127.0.0.1:8000.
You should see a dark-themed dashboard with the "RE Analysis Dashboard" title in cyan, an upload area in the header, an empty Functions sidebar on the left, and a "Select a function" prompt in the main panel.
- Click the file input and select your exported JSON file from Step 2.
- Click Upload IDA Export.
The status should turn green and say "Loaded X functions". The left sidebar populates with function names and addresses.
- Click any function in the sidebar to see its pseudocode in the right panel.
- Switch to the Vulnerabilities tab and click Analyze Vulnerabilities.
The button disables while the LLM processes the function. After a few seconds, you should see vulnerability findings (or a "function appears safe" message) rendered in the results area.
Why does analysis take a few seconds?
Each analysis request sends the full pseudocode to your local Ollama instance. The qwen2.5-coder:7b model processes the code and generates a response, which typically takes 5-15 seconds depending on your hardware. The "Run Full Analysis" button in the action bar runs all three analyses sequentially, so expect it to take longer.
Dashboard not loading or showing errors?
- If you see a blank page, check the browser console (F12) for 404 errors on /static/style.css or /static/app.js. This means your files are not in the correct static/ directory.
- If the upload button does nothing, check the browser console for JavaScript errors. A missing closing brace or quote in app.js prevents all functions from loading.
- If analysis returns an error, confirm Ollama is still running and the qwen2.5-coder:7b model is available. The FastAPI server logs in your terminal show the exact error.
Help me debug my RE Analysis Dashboard that is not loading properly in the browser
✔️ Awesome, I've got everything!
Great. Make sure all three files are saved.
ⓧ I'd like to double check the full code
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: "JetBrains Mono", "Fira Code", monospace;
background: #1a1a2e;
color: #e0e0e0;
height: 100vh;
display: flex;
flex-direction: column;
}
header {
background: #16213e;
padding: 12px 20px;
border-bottom: 1px solid #0f3460;
display: flex;
align-items: center;
gap: 20px;
flex-wrap: wrap;
}
header h1 {
color: #00d4ff;
font-size: 1.2rem;
}
#upload-area {
display: flex;
align-items: center;
gap: 10px;
}
#upload-area input[type="file"] {
color: #e0e0e0;
font-size: 0.85rem;
}
#upload-status {
color: #4caf50;
font-size: 0.85rem;
}
#binary-info {
color: #aaa;
font-size: 0.8rem;
margin-left: auto;
}
main {
display: flex;
flex: 1;
overflow: hidden;
}
aside#function-list {
width: 280px;
background: #16213e;
border-right: 1px solid #0f3460;
display: flex;
flex-direction: column;
overflow: hidden;
}
aside h2 {
padding: 12px 16px;
font-size: 0.9rem;
color: #00d4ff;
border-bottom: 1px solid #0f3460;
}
#search-box {
margin: 8px;
padding: 6px 10px;
background: #1a1a2e;
border: 1px solid #0f3460;
color: #e0e0e0;
border-radius: 4px;
font-size: 0.8rem;
}
#func-list {
list-style: none;
overflow-y: auto;
flex: 1;
}
#func-list li {
padding: 8px 16px;
cursor: pointer;
border-bottom: 1px solid #0f3460;
font-size: 0.8rem;
transition: background 0.15s;
}
#func-list li:hover {
background: #1a1a2e;
}
#func-list li.active {
background: #0f3460;
border-left: 3px solid #00d4ff;
}
#func-list li .addr {
color: #888;
font-size: 0.7rem;
}
#func-list li.analyzed::after {
content: " [A]";
color: #4caf50;
font-size: 0.7rem;
}
section#analysis-panel {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
#function-header {
padding: 12px 20px;
border-bottom: 1px solid #0f3460;
}
#func-title {
font-size: 1rem;
color: #e0e0e0;
}
#func-meta {
font-size: 0.8rem;
color: #888;
margin-top: 4px;
}
#tabs {
display: flex;
border-bottom: 1px solid #0f3460;
background: #16213e;
}
#tabs .tab {
padding: 10px 20px;
background: none;
border: none;
color: #888;
cursor: pointer;
font-size: 0.85rem;
border-bottom: 2px solid transparent;
transition: all 0.15s;
}
#tabs .tab:hover {
color: #e0e0e0;
}
#tabs .tab.active {
color: #00d4ff;
border-bottom-color: #00d4ff;
}
#tab-content {
flex: 1;
overflow-y: auto;
padding: 16px 20px;
}
.tab-pane {
display: none;
}
.tab-pane.active {
display: block;
}
#tab-pseudocode pre {
background: #0d1117;
padding: 16px;
border-radius: 6px;
overflow-x: auto;
font-size: 0.82rem;
line-height: 1.5;
}
.analyze-btn {
margin-bottom: 12px;
padding: 8px 16px;
background: #0f3460;
color: #00d4ff;
border: 1px solid #00d4ff;
border-radius: 4px;
cursor: pointer;
font-size: 0.85rem;
transition: background 0.15s;
}
.analyze-btn:hover {
background: #1a4a7a;
}
.analyze-btn:disabled {
opacity: 0.5;
cursor: wait;
}
.results {
background: #0d1117;
padding: 16px;
border-radius: 6px;
white-space: pre-wrap;
font-size: 0.82rem;
line-height: 1.5;
}
#action-bar {
padding: 12px 20px;
border-top: 1px solid #0f3460;
display: flex;
gap: 10px;
}
#action-bar button {
padding: 8px 16px;
background: #0f3460;
color: #00d4ff;
border: 1px solid #00d4ff;
border-radius: 4px;
cursor: pointer;
font-size: 0.85rem;
}
#action-bar button:hover {
background: #1a4a7a;
}
button {
font-family: inherit;
}
Your dashboard is fully functional. You can upload any IDA export, browse functions, and get AI-powered vulnerability analysis entirely offline. Next up, you will extend this with batch processing and a downloadable Markdown report in the Secret Mission.
Secret mission
Batch Analysis with Markdown Export
Your dashboard can analyze one function at a time, but a real pentesting workflow needs to triage an entire binary in one pass. Can you add batch analysis with progress tracking and export the results as a Markdown report?
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally, so there are no ongoing cloud costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- Project directory (Python files, templates, static assets, exported JSON).
- Ollama qwen2.5-coder:7b model (~4.4GB on disk).
- Python packages (fastapi, uvicorn, ollama, jinja2, python-multipart).
Keep everything running
No action needed. Choose this if you plan to continue analyzing binaries or experiment with different prompts.
- Ollama will idle in the background and consume minimal resources when no model is loaded.
- The qwen2.5-coder:7b model stays on disk (~4.4GB) and loads on demand when you start the dashboard.
- Your project files remain on your Desktop ready for the next session.
Pause - I'll come back to this later
Shut down running processes to free up memory while keeping your files intact.
- Stop the FastAPI server by pressing Ctrl+C in the terminal where it is running.
- Unload the model from memory by running:
ollama stop qwen2.5-coder:7b
- Your project folder and the downloaded model remain on disk for when you return.
Delete - I don't want to use this again
Remove all project resources and reclaim disk space.
- Stop the FastAPI server by pressing Ctrl+C if it is still running.
- Remove the Ollama model to free ~4.4GB of disk space by running:
ollama rm qwen2.5-coder:7b
- Delete your project folder by running:
cd ~/Desktop
rm -rf [[PROJECT_DIR="your project folder name"]]
- Uninstall the Python packages by running:
pip uninstall fastapi uvicorn ollama jinja2 python-multipart -y
Nice Work!
Nice Work!
Nice work! You just built a fully local reverse engineering analysis dashboard that bridges IDA Pro and a local LLM for private, offline binary analysis.
You've learned how to:
- Build an IDAPython export script that programmatically extracts decompiled pseudocode, cross-references, and metadata from IDA Pro as structured JSON.
- Build a FastAPI backend integrated with a local Ollama LLM for private, offline vulnerability detection, function renaming, and code summarization.
- Build a web dashboard for browsing exported binary data and viewing AI-powered analysis results in a pentester-oriented UI.
- Secret Mission: Added batch analysis with progress tracking and Markdown report export for documenting findings.
Ready to quiz yourself?