AI Code Security Analyzer
Build a full-stack app that scans Python code for vulnerabilities using AST and LLMs.
Introduction
30 Second Summary
Security vulnerabilities in code are like unlocked doors in a building. You might not notice them during construction, but once someone finds one, the damage can be devastating.
In this project, you will build a full-stack AI-powered code security analyzer that scans Python code for vulnerabilities, scores their severity, and suggests secure fixes automatically.
What You'll Build
A local security analysis prototype with a Django backend, Celery async task processing, LLM-powered analysis via Groq, and a React dashboard with interactive diff visualization.
By the end of this project, you'll have:
- A Django REST API with Celery async task processing that handles code analysis requests using Redis as the message broker.
- A custom AST-based security scanner combined with Groq's Llama 3.3 70B that detects vulnerabilities, maps them to CWE classifications, and generates secure refactored code.
- A React + Tailwind CSS dashboard with severity-scored vulnerability cards and a side-by-side diff viewer comparing original and fixed code.
- Secret Mission: build a vulnerability history feature that stores past scans and displays security score trends over time.
Are there any prerequisites?
This project assumes intermediate Python experience, basic React/JavaScript knowledge, and familiarity with REST APIs. You do not need prior experience with Celery, AST parsing, or LLM APIs.
Set Up the React Frontend
Your backend environment is ready with Django, Celery, and Redis. Now you need a frontend that can accept code input, send it to the backend for analysis, and display the results.
In this step, you'll scaffold a React app using Vite and configure Tailwind CSS for styling. This gives you a fast development server and a utility-first CSS framework for building the security dashboard later.
In this step, get ready to:
- Scaffold a React app with Vite.
- Install frontend dependencies for diff viewing and styling.
- Configure Tailwind CSS and verify the dev server runs.
Scaffold the React app and install dependencies
Your frontend lives in a separate frontend/ folder alongside your Django project. Vite scaffolds the entire React project structure in one command.
- Make sure your terminal is in the project root (the folder containing manage.py).
- Scaffold the React app by running this command:
npm create vite@latest frontend -- --template react
What does this command do?
This tells npm to run the create-vite scaffolding tool. The frontend argument names the folder it creates, and --template react tells Vite to use the React starter template.
Prompted to install a package?
If your terminal asks whether to install the create-vite package, type y and press Enter to proceed.
- Confirm the folder exists by running:
ls frontend
You should see files like package.json, vite.config.js, and a src/ folder listed.
- Move into the frontend/ folder and install the base dependencies by running these commands:
cd frontend
npm install
- Install the additional packages for diff viewing and Tailwind CSS by running:
npm install react-diff-viewer tailwindcss @tailwindcss/vite
What are these packages?
- react-diff-viewer renders a side-by-side comparison of original code vs. AI-refactored code.
- tailwindcss provides utility-first CSS classes so you can style your dashboard without writing custom CSS.
- @tailwindcss/vite integrates Tailwind directly into the Vite build pipeline for instant styling during development.
You should see output ending with a summary of added packages and no errors.
Seeing install errors?
- Check you are inside the frontend/ folder by running pwd (macOS/Linux) or cd (Windows). You should see a path ending in frontend.
- Make sure package.json exists in the current folder. If not, the Vite scaffold step may not have completed.
Help me fix npm install errors in my Vite React project.
Configure Tailwind CSS and verify the dev server
Tailwind CSS v4 uses a Vite plugin instead of a separate config file. You need to register the plugin and update your CSS entry point.
- Open frontend/vite.config.js in your editor.
- Replace the entire contents of the file with this code:
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import tailwindcss from '@tailwindcss/vite';
export default defineConfig({
plugins: [react(), tailwindcss()],
});
What does this code do?
- The react() plugin enables hot module replacement and JSX support.
- The tailwindcss() plugin processes Tailwind utility classes during the build. This means you can use classes like bg-gray-900 directly in your JSX without any extra configuration files.
- Save vite.config.js.
- Open frontend/src/App.css in your editor.
- Delete everything in the file and replace it with this single line:
@import "tailwindcss";
- Save App.css.
- Start the dev server by running:
npm run dev
You should see output showing the dev server is running on http://localhost:5173.
Dev server not starting?
- If you see a module not found error mentioning @tailwindcss/vite, re-run npm install to make sure all packages are installed.
- Check that your vite.config.js matches the code above exactly. A missing comma or import can prevent startup.
Help me troubleshoot my Vite dev server not starting.
✔️ Awesome, I've got everything!
Great. Double check you've saved both files, then press Ctrl+C in your terminal to stop the dev server for now.
ⓧ I'd like to double check the full code
Here is what each file should look like:
frontend/vite.config.js:
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import tailwindcss from '@tailwindcss/vite';
export default defineConfig({
plugins: [react(), tailwindcss()],
});
frontend/src/App.css:
@import "tailwindcss";
Your frontend is scaffolded and ready to go. Next up, you'll build the AST security scanner that detects dangerous patterns in Python code.
Build the AST Security Scanner
Your Django backend is configured, but it has no brain yet. Before the LLM can reason about code, you need a first-pass structural analysis that identifies obvious security anti-patterns without any AI involvement.
Python's ast module lets you parse source code into a tree of nodes representing every function call, variable assignment, and expression. In this step, you'll build a custom scanner that walks this tree and flags dangerous patterns like eval(), hardcoded passwords, and SQL injection.
In this step, get ready to:
- Create a SecurityVisitor class that detects dangerous function calls, hardcoded credentials, and SQL injection patterns.
- Map each finding to an industry-standard CWE classification with severity scores.
- Write a scan_code entry point that parses source code and returns structured findings.
Create the SecurityVisitor class
An Abstract Syntax Tree (AST) is a structured representation of source code. Instead of reading code as raw text, the AST represents it as a tree of typed nodes. A function call becomes a Call node, an assignment becomes an Assign node, and so on.
You'll subclass Python's ast.NodeVisitor to walk the tree. Each visit_* method you define fires automatically when the visitor encounters that node type.
- Create a new file called scanner.py inside your analyzer directory.
- Add the import, class definition, and the visit_Call method that detects dangerous function calls by pasting the following code:
import ast
class SecurityVisitor(ast.NodeVisitor):
def __init__(self):
self.findings = []
def visit_Call(self, node: ast.Call):
# Detect eval() and exec() - arbitrary code execution
if isinstance(node.func, ast.Name):
if node.func.id in ('eval', 'exec'):
self.findings.append({
'line': node.lineno,
'severity': 'Critical',
'cwe': 'CWE-95',
'title': f'Dangerous function: {node.func.id}()',
'description': (
f'Use of {node.func.id}() can lead to arbitrary code '
f'execution if untrusted input is passed.'
),
})
# Detect os.system() - OS command injection
if isinstance(node.func, ast.Attribute):
if (
isinstance(node.func.value, ast.Name)
and node.func.value.id == 'os'
and node.func.attr == 'system'
):
self.findings.append({
'line': node.lineno,
'severity': 'Critical',
'cwe': 'CWE-78',
'title': 'OS Command Injection: os.system()',
'description': (
'os.system() executes shell commands. Use subprocess '
'with a list of arguments instead.'
),
})
# Detect subprocess with shell=True
if (
isinstance(node.func.value, ast.Name)
and node.func.value.id == 'subprocess'
and node.func.attr in ('call', 'run', 'Popen')
):
for keyword in node.keywords:
if keyword.arg == 'shell':
if isinstance(keyword.value, ast.Constant) and keyword.value.value is True:
self.findings.append({
'line': node.lineno,
'severity': 'High',
'cwe': 'CWE-78',
'title': 'Shell injection risk: shell=True',
'description': (
'Using shell=True with subprocess can lead to '
'command injection vulnerabilities.'
),
})
self.generic_visit(node)
What does this code do?
The SecurityVisitor class inherits from ast.NodeVisitor. When you call .visit() on an AST tree, it automatically dispatches to visit_Call every time it encounters a function call node.
Inside visit_Call, there are three detection patterns. Direct calls to eval() or exec() are flagged as Critical because they can execute arbitrary code. Calls to os.system() are flagged because they pass strings directly to the shell. Finally, subprocess calls with shell=True are caught because they reintroduce shell interpretation.
The call to self.generic_visit(node) at the end ensures the visitor continues walking into child nodes. Without it, nested calls would be missed.
Now add the visit_Assign method to detect hardcoded credentials. Paste this below the visit_Call method, inside the SecurityVisitor class:
def visit_Assign(self, node: ast.Assign):
# Detect hardcoded secrets in variable assignments
for target in node.targets:
if isinstance(target, ast.Name):
name_lower = target.id.lower()
secret_keywords = ('password', 'secret', 'api_key', 'token', 'apikey')
if any(kw in name_lower for kw in secret_keywords):
if isinstance(node.value, ast.Constant) and isinstance(node.value.value, str):
if len(node.value.value) > 0:
self.findings.append({
'line': node.lineno,
'severity': 'High',
'cwe': 'CWE-798',
'title': f'Hardcoded credential: {target.id}',
'description': (
'Secrets should not be hardcoded. Use environment '
'variables or a secrets manager.'
),
})
self.generic_visit(node)
What does this code do?
The visit_Assign method fires on every assignment statement. It checks if the variable name contains keywords like password, secret, or api_key. If the assigned value is a non-empty string literal, it flags the line as a hardcoded credential.
This catches patterns like db_password = 'admin123' that developers accidentally leave in source code.
Finally, add the visit_JoinedStr method to detect SQL injection via f-strings. Paste this below visit_Assign, still inside the class:
def visit_JoinedStr(self, node: ast.JoinedStr):
# Detect SQL injection via f-strings
sql_keywords = ('SELECT', 'INSERT', 'UPDATE', 'DELETE', 'DROP')
for value in node.values:
if isinstance(value, ast.Constant) and isinstance(value.value, str):
if any(kw in value.value.upper() for kw in sql_keywords):
self.findings.append({
'line': node.lineno,
'severity': 'Critical',
'cwe': 'CWE-89',
'title': 'SQL Injection via f-string',
'description': (
'SQL queries built with f-strings are vulnerable to '
'injection. Use parameterized queries instead.'
),
})
break
self.generic_visit(node)
What does this code do?
In the AST, f-strings are represented as JoinedStr nodes. This method inspects the static string parts of every f-string looking for SQL keywords like SELECT or DELETE. If found, it flags the line because building SQL with f-strings means user input can be injected directly into the query.
Map findings to CWE classifications
Each finding your scanner produces is tagged with a CWE ID. CWE stands for Common Weakness Enumeration, an industry-standard catalog maintained by MITRE that gives every type of software vulnerability a unique identifier.
What is CWE?
The Common Weakness Enumeration (CWE) is a community-developed dictionary of software weaknesses. Each entry has a numeric ID and a standardized description.
Your scanner uses four CWE IDs. CWE-95 covers Eval Injection (dynamic code execution). CWE-78 covers OS Command Injection. CWE-798 covers Use of Hard-coded Credentials. CWE-89 covers SQL Injection.
By tagging findings with CWE IDs, your scanner produces reports that are interoperable with industry tools like OWASP ZAP and commercial SAST platforms.
The severity scores follow a four-level scale that maps to how much damage the vulnerability can cause:
- Critical: Arbitrary code execution or full data breach (eval(), os.system(), SQL injection).
- High: Credential exposure or elevated injection risk (shell=True, hardcoded secrets).
- Medium: Information disclosure or limited-scope injection.
- Low: Code quality issues or parse errors.
Write the scan_code entry point
The visitor class is ready, but it needs an entry point function that handles parsing and error recovery. If someone submits code with a syntax error, the scanner should report that gracefully instead of crashing.
- Add the scan_code function at the bottom of analyzer/scanner.py, outside the class, by pasting this code:
def scan_code(source: str) -> list[dict]:
# Safely parse the source code into an AST
try:
tree = ast.parse(source)
except SyntaxError as e:
return [{
'line': e.lineno or 0,
'severity': 'Low',
'cwe': 'N/A',
'title': 'Syntax Error',
'description': f'Could not parse code: {e.msg}',
}]
# Walk the AST and collect findings
visitor = SecurityVisitor()
visitor.visit(tree)
return visitor.findings
What does this code do?
The scan_code function is the public API for your scanner. It takes a raw Python source string, parses it with ast.parse(), and runs the SecurityVisitor over the resulting tree.
If the source contains a syntax error, ast.parse() raises a SyntaxError exception. The try/except block catches this and returns a single low-severity finding instead of crashing. This is important because users may paste incomplete or broken code.
Why AST parsing instead of regex?
Regex-based scanners are brittle. They match text patterns without understanding code structure, so they produce many false positives (matching comments or strings that happen to contain the keyword). They also miss renamed or aliased function calls.
AST parsing understands the actual code structure. A Call node is always a real function call, never a comment or a string. This gives you fewer false positives and more accurate results.
- Save analyzer/scanner.py.
- Test the scanner by opening the Django shell. Run this command from your codeaudit project directory:
python manage.py shell
- In the shell, run this test to verify the scanner catches vulnerabilities:
from analyzer.scanner import scan_code
findings = scan_code("password = 'admin123'\neval(input())")
for f in findings:
print(f"{f['severity']}: {f['title']} (Line {f['line']})")
You should see two findings printed: a High severity hardcoded credential on line 1, and a Critical dangerous function on line 2.
- Type exit() to leave the Django shell.
Not seeing any output?
Make sure you saved analyzer/scanner.py before opening the shell. The shell imports the module at the point you launch it, so unsaved changes are not picked up.
Check that your scan_code function is at the top level of the file (not indented inside the class). It should be at the same indentation as class SecurityVisitor.
If you see an IndentationError, double-check that all methods inside the class use consistent 4-space indentation.
Help me debug my AST scanner
✔️ Awesome, I've got everything!
Great. Double check you've saved analyzer/scanner.py and confirmed the test output in the Django shell.
ⓧ I'd like to double check the full code
Here is the complete analyzer/scanner.py file:
import ast
class SecurityVisitor(ast.NodeVisitor):
def __init__(self):
self.findings = []
def visit_Call(self, node: ast.Call):
if isinstance(node.func, ast.Name):
if node.func.id in ('eval', 'exec'):
self.findings.append({
'line': node.lineno,
'severity': 'Critical',
'cwe': 'CWE-95',
'title': f'Dangerous function: {node.func.id}()',
'description': (
f'Use of {node.func.id}() can lead to arbitrary code '
f'execution if untrusted input is passed.'
),
})
if isinstance(node.func, ast.Attribute):
if (
isinstance(node.func.value, ast.Name)
and node.func.value.id == 'os'
and node.func.attr == 'system'
):
self.findings.append({
'line': node.lineno,
'severity': 'Critical',
'cwe': 'CWE-78',
'title': 'OS Command Injection: os.system()',
'description': (
'os.system() executes shell commands. Use subprocess '
'with a list of arguments instead.'
),
})
if (
isinstance(node.func.value, ast.Name)
and node.func.value.id == 'subprocess'
and node.func.attr in ('call', 'run', 'Popen')
):
for keyword in node.keywords:
if keyword.arg == 'shell':
if isinstance(keyword.value, ast.Constant) and keyword.value.value is True:
self.findings.append({
'line': node.lineno,
'severity': 'High',
'cwe': 'CWE-78',
'title': 'Shell injection risk: shell=True',
'description': (
'Using shell=True with subprocess can lead to '
'command injection vulnerabilities.'
),
})
self.generic_visit(node)
def visit_Assign(self, node: ast.Assign):
for target in node.targets:
if isinstance(target, ast.Name):
name_lower = target.id.lower()
secret_keywords = ('password', 'secret', 'api_key', 'token', 'apikey')
if any(kw in name_lower for kw in secret_keywords):
if isinstance(node.value, ast.Constant) and isinstance(node.value.value, str):
if len(node.value.value) > 0:
self.findings.append({
'line': node.lineno,
'severity': 'High',
'cwe': 'CWE-798',
'title': f'Hardcoded credential: {target.id}',
'description': (
'Secrets should not be hardcoded. Use environment '
'variables or a secrets manager.'
),
})
self.generic_visit(node)
def visit_JoinedStr(self, node: ast.JoinedStr):
sql_keywords = ('SELECT', 'INSERT', 'UPDATE', 'DELETE', 'DROP')
for value in node.values:
if isinstance(value, ast.Constant) and isinstance(value.value, str):
if any(kw in value.value.upper() for kw in sql_keywords):
self.findings.append({
'line': node.lineno,
'severity': 'Critical',
'cwe': 'CWE-89',
'title': 'SQL Injection via f-string',
'description': (
'SQL queries built with f-strings are vulnerable to '
'injection. Use parameterized queries instead.'
),
})
break
self.generic_visit(node)
def scan_code(source: str) -> list[dict]:
try:
tree = ast.parse(source)
except SyntaxError as e:
return [{
'line': e.lineno or 0,
'severity': 'Low',
'cwe': 'N/A',
'title': 'Syntax Error',
'description': f'Could not parse code: {e.msg}',
}]
visitor = SecurityVisitor()
visitor.visit(tree)
return visitor.findings
Your AST scanner is complete. It can detect five categories of security vulnerabilities without any AI involvement. Next up, you'll connect this scanner to the Groq LLM so it can validate findings, catch logic flaws the AST missed, and generate secure refactored code.
Integrate Groq LLM for Deep Analysis
Your AST scanner catches structural patterns like eval() calls and hardcoded secrets. But it cannot reason about logic flaws, data flow issues, or subtle vulnerabilities that require understanding what the code actually does.
In this step, you will add a second analysis layer that sends the code and AST findings to a large language model. The LLM validates each AST finding, identifies additional issues the tree-walker missed, and generates a complete refactored version of the code with all vulnerabilities fixed.
In this step, get ready to:
- Get a free Groq API key and set it as an environment variable.
- Build an LLM analysis module that constructs a structured security review prompt and calls Groq's API.
Get your Groq API key
Groq provides ultra-fast LLM inference through their free tier. No credit card is required.
- Open your browser and navigate to console.groq.com.
- Sign up for a free account (or sign in if you already have one).
- Click API Keys in the left sidebar.
- Click Create API Key, give it a name, and copy the key immediately. You will not be able to see it again after closing the dialog.
Why Groq over other LLM providers?
Groq's free tier gives you access to Llama 3.3 70B with no credit card required. Their custom LPU hardware delivers responses in seconds rather than the 10-30 second waits typical of other providers.
For a Celery worker processing code analysis tasks, fast inference means shorter queue times and a better developer experience.
Now set the API key as an environment variable in the terminal where you will run Django and Celery.
- Export your Groq API key by running this command (replace the placeholder with your actual key):
macOS
export GROQ_API_KEY=your-api-key-here
Windows
$env:GROQ_API_KEY = "your-api-key-here"
Key not working later?
The export command only persists for the current terminal session. If you close and reopen your terminal, you will need to run the export command again before starting Django or Celery.
Help me debug my environment variable
Build the LLM analyzer module
This module takes the source code and AST findings, constructs a structured prompt, and calls Groq's OpenAI-compatible API to get deep security analysis back as structured JSON.
- Create a new file called llm_analyzer.py inside your analyzer folder.
- Add the imports, client setup, and prompt construction by pasting this code:
import json
import os
from openai import OpenAI
def analyze_with_llm(source_code: str, ast_findings: list[dict]) -> dict:
# Initialize the Groq client using OpenAI-compatible SDK
client = OpenAI(
api_key=os.environ.get('GROQ_API_KEY', 'your-api-key-here'),
base_url='https://api.groq.com/openai/v1',
)
# Format AST findings into a readable string for the prompt
findings_text = '\n'.join(
f"- Line {f['line']}: [{f['severity']}] {f['title']} ({f['cwe']})"
for f in ast_findings
)
# Construct a structured prompt for the LLM security reviewer
prompt = f"""You are a security code reviewer. Analyze the following Python code and the preliminary AST-based findings.
## Source Code:
```python
{source_code}
```
## Preliminary AST Findings:
{findings_text if findings_text else 'No structural issues detected by AST analysis.'}
## Your Tasks:
1. Validate each AST finding (confirm or mark as false positive)
2. Identify any ADDITIONAL security issues the AST missed (logic flaws, data flow issues, race conditions, etc.)
3. Provide a complete refactored version of the code that fixes ALL security issues
Respond in this exact JSON format (no markdown, just raw JSON):
{{
"validated_findings": [
{{
"line": <int>,
"severity": "<Critical|High|Medium|Low>",
"cwe": "<CWE-ID>",
"title": "<short title>",
"description": "<explanation>",
"confirmed": <true|false>
}}
],
"additional_findings": [
{{
"line": <int>,
"severity": "<Critical|High|Medium|Low>",
"cwe": "<CWE-ID>",
"title": "<short title>",
"description": "<explanation>"
}}
],
"refactored_code": "<complete fixed Python code as a string>",
"summary": "<brief overall security assessment>"
}}"""
What does this code do?
The function uses the openai Python SDK pointed at Groq's endpoint (base_url='https://api.groq.com/openai/v1'). Groq's API is fully OpenAI-compatible, so you get the same SDK interface with Groq's hardware-accelerated speed.
The prompt asks the LLM to do three things: validate whether AST findings are real issues or false positives, find additional logic-level vulnerabilities the AST missed, and produce a complete refactored version with all fixes applied.
The double curly braces ({{ and }}) are f-string escaping so the JSON template renders literal braces in the output.
- Paste the following code directly below the prompt string (after the closing """ on the prompt variable), still indented inside analyze_with_llm:
# Call Groq's LLM for deep security analysis
chat_completion = client.chat.completions.create(
model='llama-3.3-70b-versatile',
messages=[
{
'role': 'system',
'content': 'You are an expert security code reviewer. Always respond with valid JSON only, no markdown formatting.',
},
{
'role': 'user',
'content': prompt,
},
],
temperature=0.1,
max_tokens=4096,
)
response_text = chat_completion.choices[0].message.content
# Parse the JSON response, handling cases where the LLM wraps it in markdown
try:
start = response_text.find('{')
end = response_text.rfind('}') + 1
result = json.loads(response_text[start:end])
except (json.JSONDecodeError, ValueError):
result = {
'validated_findings': [],
'additional_findings': [],
'refactored_code': source_code,
'summary': 'LLM analysis could not be parsed. Showing AST results only.',
}
return result
What does this code do?
The client.chat.completions.create() call sends the prompt to Llama 3.3 70B running on Groq hardware. The low temperature=0.1 keeps responses deterministic and focused on accuracy rather than creativity.
The response parsing uses find('{') and rfind('}') to extract JSON from the response. LLMs sometimes wrap JSON in markdown code fences despite being told not to. This approach handles that gracefully.
If parsing fails entirely, the fallback returns the original source code and empty findings so the pipeline never crashes.
- Save analyzer/llm_analyzer.py.
- Verify the file exists by running:
ls analyzer/llm_analyzer.py
You should see the filename printed in the output, confirming the file was created successfully.
Seeing a "No such file or directory" error?
Make sure you created the file inside the analyzer directory (not the project root). Your terminal should be in the codeaudit project folder where manage.py lives.
Help me find the right directory
✔️ Awesome, I've got everything!
Great. Double check you have saved your file before moving on.
ⓧ I'd like to double check the full code
Here is the complete analyzer/llm_analyzer.py file:
import json
import os
from openai import OpenAI
def analyze_with_llm(source_code: str, ast_findings: list[dict]) -> dict:
client = OpenAI(
api_key=os.environ.get('GROQ_API_KEY', 'your-api-key-here'),
base_url='https://api.groq.com/openai/v1',
)
findings_text = '\n'.join(
f"- Line {f['line']}: [{f['severity']}] {f['title']} ({f['cwe']})"
for f in ast_findings
)
prompt = f"""You are a security code reviewer. Analyze the following Python code and the preliminary AST-based findings.
## Source Code:
```python
{source_code}
```
## Preliminary AST Findings:
{findings_text if findings_text else 'No structural issues detected by AST analysis.'}
## Your Tasks:
1. Validate each AST finding (confirm or mark as false positive)
2. Identify any ADDITIONAL security issues the AST missed (logic flaws, data flow issues, race conditions, etc.)
3. Provide a complete refactored version of the code that fixes ALL security issues
Respond in this exact JSON format (no markdown, just raw JSON):
{{
"validated_findings": [
{{
"line": <int>,
"severity": "<Critical|High|Medium|Low>",
"cwe": "<CWE-ID>",
"title": "<short title>",
"description": "<explanation>",
"confirmed": <true|false>
}}
],
"additional_findings": [
{{
"line": <int>,
"severity": "<Critical|High|Medium|Low>",
"cwe": "<CWE-ID>",
"title": "<short title>",
"description": "<explanation>"
}}
],
"refactored_code": "<complete fixed Python code as a string>",
"summary": "<brief overall security assessment>"
}}"""
chat_completion = client.chat.completions.create(
model='llama-3.3-70b-versatile',
messages=[
{
'role': 'system',
'content': 'You are an expert security code reviewer. Always respond with valid JSON only, no markdown formatting.',
},
{
'role': 'user',
'content': prompt,
},
],
temperature=0.1,
max_tokens=4096,
)
response_text = chat_completion.choices[0].message.content
try:
start = response_text.find('{')
end = response_text.rfind('}') + 1
result = json.loads(response_text[start:end])
except (json.JSONDecodeError, ValueError):
result = {
'validated_findings': [],
'additional_findings': [],
'refactored_code': source_code,
'summary': 'LLM analysis could not be parsed. Showing AST results only.',
}
return result
Your LLM integration is ready. Next up, you will wire the AST scanner and LLM analyzer together with Celery for async processing and expose the pipeline through Django REST API endpoints.
Wire Up the Django + Celery Async Pipeline
You have a scanner that finds structural vulnerabilities and an LLM analyzer that reasons about deeper logic flaws. Right now, these two pieces live in separate files with no way to trigger them from the outside world.
In this step, you will connect everything into an async pipeline. A Celery task will orchestrate the full analysis flow, and two Django REST Framework endpoints will let the React frontend submit code and poll for results.
In this step, get ready to:
- Create a Celery shared task that chains AST scanning, LLM analysis, and score computation.
- Build two DRF endpoints for submitting code and retrieving results.
- Wire up URL routing and verify the server starts cleanly.
Create the async analysis task
The analysis task is the brain of your pipeline. When triggered, it runs the AST scanner, feeds results to the LLM, merges all findings, computes a severity-weighted security score, and saves everything to the database.
- Create a new file called analyzer/tasks.py and add the following code:
from celery import shared_task
from .scanner import scan_code
from .llm_analyzer import analyze_with_llm
from .models import AnalysisResult
@shared_task(bind=True)
def analyze_code_task(self, analysis_id: int):
# Fetch the pending analysis record from the database
result = AnalysisResult.objects.get(id=analysis_id)
result.status = 'processing'
result.task_id = self.request.id
result.save()
try:
# Step 1: Run the AST-based structural scanner
ast_findings = scan_code(result.source_code)
# Step 2: Send code + AST findings to the LLM for deeper analysis
llm_result = analyze_with_llm(result.source_code, ast_findings)
# Merge validated findings (confirmed by LLM) with additional ones
all_findings = []
for f in llm_result.get('validated_findings', []):
if f.get('confirmed', True):
all_findings.append(f)
for f in llm_result.get('additional_findings', []):
all_findings.append(f)
# Fall back to raw AST findings if LLM returned nothing
if not all_findings:
all_findings = ast_findings
# Compute a severity-weighted security score (100 = perfect)
severity_weights = {'Critical': 25, 'High': 15, 'Medium': 8, 'Low': 3}
total_penalty = sum(
severity_weights.get(f.get('severity', 'Low'), 3)
for f in all_findings
)
score = max(0, 100 - total_penalty)
# Persist results to the database
result.set_findings(all_findings)
result.refactored_code = llm_result.get('refactored_code', result.source_code)
result.overall_score = score
result.status = 'completed'
result.save()
except Exception as e:
# Mark the analysis as failed and store the error
result.status = 'failed'
result.set_findings([{
'line': 0,
'severity': 'Low',
'cwe': 'N/A',
'title': 'Analysis Error',
'description': str(e),
}])
result.save()
raise
What does this code do?
The @shared_task(bind=True) decorator registers this function as a Celery task that can run asynchronously in a separate worker process. The bind=True argument gives the task access to self.request.id so it can store its own task ID.
The scoring system subtracts penalty points for each finding based on severity. A Critical vulnerability costs 25 points, High costs 15, Medium costs 8, and Low costs 3. A score of 100 means no issues found.
The try/except block ensures that if the Groq API is unreachable or returns an unexpected response, the task fails gracefully and stores a descriptive error rather than silently crashing.
- Save analyzer/tasks.py.
You will not see a change yet. The task needs API endpoints to trigger it, which you will build next.
Build the REST API views
The React frontend needs two endpoints: one to submit code for analysis, and one to poll for results. The submit endpoint kicks off the Celery task and immediately returns a task ID so the frontend does not have to wait for the LLM to finish.
- Create a new file called analyzer/views.py and add the following code:
from rest_framework.decorators import api_view
from rest_framework.response import Response
from rest_framework import status
from .models import AnalysisResult
from .tasks import analyze_code_task
@api_view(['POST'])
def submit_analysis(request):
# Extract the source code from the request body
source_code = request.data.get('code', '')
if not source_code.strip():
return Response(
{'error': 'No code provided'},
status=status.HTTP_400_BAD_REQUEST,
)
# Create a database record to track this analysis
result = AnalysisResult.objects.create(
source_code=source_code,
task_id='pending',
)
# Dispatch the async Celery task and update the record with the real task ID
task = analyze_code_task.delay(result.id)
result.task_id = task.id
result.save()
return Response({
'analysis_id': result.id,
'task_id': task.id,
'status': 'pending',
}, status=status.HTTP_202_ACCEPTED)
@api_view(['GET'])
def get_result(request, analysis_id):
# Look up the analysis record by its database ID
try:
result = AnalysisResult.objects.get(id=analysis_id)
except AnalysisResult.DoesNotExist:
return Response(
{'error': 'Analysis not found'},
status=status.HTTP_404_NOT_FOUND,
)
# Build the response payload based on current status
response_data = {
'id': result.id,
'status': result.status,
'overall_score': result.overall_score,
'created_at': result.created_at.isoformat(),
}
# Only include findings and refactored code when analysis is complete
if result.status == 'completed':
response_data['findings'] = result.get_findings()
response_data['refactored_code'] = result.refactored_code
response_data['source_code'] = result.source_code
return Response(response_data)
What does this code do?
The submit_analysis view accepts a POST request with a code field, creates a database record, then fires off the Celery task with .delay(). It returns immediately with HTTP 202 (Accepted) so the frontend knows the job is queued.
The get_result view returns the current status of an analysis. While the task is still running, the response shows status: 'processing'. Once complete, it includes the full findings list and refactored code. The frontend polls this endpoint every few seconds until it sees completed or failed.
- Save analyzer/views.py.
You will not see a change yet. The views need URL routing to become accessible.
Create the URL routing and verify the server starts
Django needs a URL configuration file inside the analyzer app to map URL paths to your view functions. The project-level codeaudit/urls.py already includes analyzer.urls from a prior step, so you only need to create the app-level file.
- Create a new file called analyzer/urls.py and add the following code:
from django.urls import path
from . import views
urlpatterns = [
path('analyze/', views.submit_analysis, name='submit_analysis'),
path('results/<int:analysis_id>/', views.get_result, name='get_result'),
]
What does this code do?
This maps two URL patterns under the /api/ prefix (which is already configured in codeaudit/urls.py). A POST to /api/analyze/ triggers code submission. A GET to /api/results/1/ retrieves the result for analysis ID 1.
- Save analyzer/urls.py.
- Verify the Django server starts without import errors by running:
python manage.py runserver
You should see output ending with a line like Starting development server at http://127.0.0.1:8000/ with no tracebacks.
- Press Ctrl+C to stop the dev server.
Seeing an import error?
- Check that analyzer/tasks.py and analyzer/views.py are saved inside the analyzer/ directory (not the project root).
- Verify your virtual environment is active. The prompt should show the environment name.
- Make sure analyzer/urls.py uses from . import views (relative import with the dot).
Help me debug my Django import error
Why async tasks instead of synchronous views?
LLM API calls can take 10 to 30 seconds to complete. If you handled this synchronously, the browser request would hang with no feedback until the analysis finishes.
By dispatching the work to a Celery worker, the API responds instantly with a task ID. The frontend then polls a lightweight status endpoint every couple of seconds. This pattern keeps the user informed and prevents HTTP timeouts.
✔️ Awesome, I've got everything!
Great. Make sure all three files are saved before moving on.
ⓧ I'd like to double check the full code
Here is the complete code for all three new files:
from celery import shared_task
from .scanner import scan_code
from .llm_analyzer import analyze_with_llm
from .models import AnalysisResult
@shared_task(bind=True)
def analyze_code_task(self, analysis_id: int):
result = AnalysisResult.objects.get(id=analysis_id)
result.status = 'processing'
result.task_id = self.request.id
result.save()
try:
ast_findings = scan_code(result.source_code)
llm_result = analyze_with_llm(result.source_code, ast_findings)
all_findings = []
for f in llm_result.get('validated_findings', []):
if f.get('confirmed', True):
all_findings.append(f)
for f in llm_result.get('additional_findings', []):
all_findings.append(f)
if not all_findings:
all_findings = ast_findings
severity_weights = {'Critical': 25, 'High': 15, 'Medium': 8, 'Low': 3}
total_penalty = sum(
severity_weights.get(f.get('severity', 'Low'), 3)
for f in all_findings
)
score = max(0, 100 - total_penalty)
result.set_findings(all_findings)
result.refactored_code = llm_result.get('refactored_code', result.source_code)
result.overall_score = score
result.status = 'completed'
result.save()
except Exception as e:
result.status = 'failed'
result.set_findings([{
'line': 0,
'severity': 'Low',
'cwe': 'N/A',
'title': 'Analysis Error',
'description': str(e),
}])
result.save()
raise
from rest_framework.decorators import api_view
from rest_framework.response import Response
from rest_framework import status
from .models import AnalysisResult
from .tasks import analyze_code_task
@api_view(['POST'])
def submit_analysis(request):
source_code = request.data.get('code', '')
if not source_code.strip():
return Response(
{'error': 'No code provided'},
status=status.HTTP_400_BAD_REQUEST,
)
result = AnalysisResult.objects.create(
source_code=source_code,
task_id='pending',
)
task = analyze_code_task.delay(result.id)
result.task_id = task.id
result.save()
return Response({
'analysis_id': result.id,
'task_id': task.id,
'status': 'pending',
}, status=status.HTTP_202_ACCEPTED)
@api_view(['GET'])
def get_result(request, analysis_id):
try:
result = AnalysisResult.objects.get(id=analysis_id)
except AnalysisResult.DoesNotExist:
return Response(
{'error': 'Analysis not found'},
status=status.HTTP_404_NOT_FOUND,
)
response_data = {
'id': result.id,
'status': result.status,
'overall_score': result.overall_score,
'created_at': result.created_at.isoformat(),
}
if result.status == 'completed':
response_data['findings'] = result.get_findings()
response_data['refactored_code'] = result.refactored_code
response_data['source_code'] = result.source_code
return Response(response_data)
from django.urls import path
from . import views
urlpatterns = [
path('analyze/', views.submit_analysis, name='submit_analysis'),
path('results/<int:analysis_id>/', views.get_result, name='get_result'),
]
Your backend pipeline is complete. Next up, you will build the React dashboard that submits code to these endpoints and displays the vulnerability results in a professional UI.
Build the React Dashboard
Your Django backend can now accept code, run it through AST analysis and LLM reasoning, and return structured vulnerability findings. The missing piece is a frontend where users actually interact with this pipeline.
In this step, you will build a React dashboard that lets users paste Python code, submit it for analysis, and view the results in an interactive UI with color-coded vulnerability cards and a side-by-side code diff viewer.
In this step, get ready to:
- Build the main App component with a code input textarea, an Analyze button, and polling logic that checks for results.
- Build a VulnerabilityCard component that renders color-coded severity badges with CWE IDs and descriptions.
- Integrate react-diff-viewer to display original vs. refactored code in a split view.
Build the main App component with code input and polling
The React dashboard needs to do three things: accept code input, submit it to the Django API, and poll for results until the async Celery task completes.
- Open frontend/src/App.jsx in your editor.
- Select all the existing content and replace it with the following code:
import { useState, useEffect, useRef } from 'react';
import ReactDiffViewer from 'react-diff-viewer';
import './App.css';
const API_BASE = 'http://localhost:8000/api';
const SEVERITY_COLORS = {
Critical: 'bg-red-600',
High: 'bg-orange-500',
Medium: 'bg-yellow-500',
Low: 'bg-blue-500',
};
function VulnerabilityCard({ finding }) {
return (
<div className="border border-gray-700 rounded-lg p-4 mb-3 bg-gray-800">
<div className="flex items-center gap-2 mb-2">
<span className={`px-2 py-1 rounded text-xs font-bold text-white ${SEVERITY_COLORS[finding.severity]}`}>
{finding.severity}
</span>
<span className="text-gray-400 text-sm">{finding.cwe}</span>
<span className="text-gray-400 text-sm ml-auto">Line {finding.line}</span>
</div>
<h3 className="text-white font-semibold text-sm">{finding.title}</h3>
<p className="text-gray-400 text-xs mt-1">{finding.description}</p>
</div>
);
}
What does this code do?
This first section sets up the building blocks for the dashboard.
- API_BASE points to your Django dev server so the frontend knows where to send requests.
- SEVERITY_COLORS maps each vulnerability severity level to a Tailwind CSS background color class (red for Critical, orange for High, yellow for Medium, blue for Low).
- VulnerabilityCard is a reusable component that displays a single finding as a styled card with a severity badge, CWE ID, line number, title, and description.
- Below the VulnerabilityCard function, add the main App component:
function App() {
const [code, setCode] = useState('');
const [loading, setLoading] = useState(false);
const [result, setResult] = useState(null);
const pollRef = useRef(null);
const handleAnalyze = async () => {
if (!code.trim()) return;
setLoading(true);
setResult(null);
try {
const response = await fetch(`${API_BASE}/analyze/`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ code }),
});
const data = await response.json();
pollForResults(data.analysis_id);
} catch (error) {
setLoading(false);
alert('Failed to submit analysis');
}
};
const pollForResults = (analysisId) => {
pollRef.current = setInterval(async () => {
try {
const response = await fetch(`${API_BASE}/results/${analysisId}/`);
const data = await response.json();
if (data.status === 'completed' || data.status === 'failed') {
clearInterval(pollRef.current);
setResult(data);
setLoading(false);
}
} catch (error) {
clearInterval(pollRef.current);
setLoading(false);
}
}, 2000);
};
useEffect(() => {
return () => {
if (pollRef.current) clearInterval(pollRef.current);
};
}, []);
return (
<div className="min-h-screen bg-gray-900 text-white p-6">
<header className="mb-8">
<h1 className="text-3xl font-bold">Code Security Analyzer</h1>
<p className="text-gray-400 mt-1">AI-powered vulnerability detection with AST + LLM analysis</p>
</header>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div>
<h2 className="text-lg font-semibold mb-3">Paste Python Code</h2>
<textarea
className="w-full h-64 bg-gray-800 border border-gray-700 rounded-lg p-4 font-mono text-sm text-green-300 resize-none focus:outline-none focus:border-blue-500"
placeholder="Paste your Python code here..."
value={code}
onChange={(e) => setCode(e.target.value)}
/>
<button
onClick={handleAnalyze}
disabled={loading || !code.trim()}
className="mt-4 px-6 py-3 bg-blue-600 hover:bg-blue-700 disabled:bg-gray-600 rounded-lg font-semibold transition-colors"
>
{loading ? 'Analyzing...' : 'Analyze Security'}
</button>
{result && result.status === 'completed' && (
<div className="mt-6">
<div className="flex items-center gap-4 mb-4">
<h2 className="text-lg font-semibold">Security Score</h2>
<span className={`text-2xl font-bold ${result.overall_score >= 70 ? 'text-green-400' : result.overall_score >= 40 ? 'text-yellow-400' : 'text-red-400'}`}>
{result.overall_score}/100
</span>
</div>
<h3 className="text-md font-semibold mb-3">
Vulnerabilities ({result.findings.length})
</h3>
{result.findings.map((finding, idx) => (
<VulnerabilityCard key={idx} finding={finding} />
))}
</div>
)}
</div>
<div>
{result && result.status === 'completed' && result.refactored_code && (
<div>
<h2 className="text-lg font-semibold mb-3">Suggested Fix (Diff View)</h2>
<div className="rounded-lg overflow-hidden border border-gray-700">
<ReactDiffViewer
oldValue={result.source_code}
newValue={result.refactored_code}
splitView={true}
useDarkTheme={true}
leftTitle="Original (Vulnerable)"
rightTitle="Refactored (Secure)"
/>
</div>
</div>
)}
</div>
</div>
</div>
);
}
export default App;
What does the App component do?
The App component manages the entire analysis workflow through three React hooks.
- code stores whatever the user types in the textarea. loading tracks whether an analysis is in progress. result holds the API response once analysis finishes.
- handleAnalyze sends a POST request to /api/analyze/ with the pasted code, then starts polling.
- pollForResults uses setInterval every 2000ms to check the results endpoint. Once the status is completed or failed, it clears the interval and stores the data.
- pollRef is a ref that holds the interval ID so the useEffect cleanup can cancel polling if the component unmounts.
- Save frontend/src/App.jsx.
- In a terminal, navigate to the frontend folder and start the dev server by running:
cd frontend
npm run dev
- Open the URL shown in the terminal (usually http://localhost:5173) in your browser. You should see a dark page with the title "Code Security Analyzer" and a code textarea with an "Analyze Security" button.
Seeing a blank page or build errors?
- Check your terminal for import errors. Make sure react-diff-viewer is installed. Run npm install react-diff-viewer from the frontend folder if needed.
- Confirm your App.css file contains @import "tailwindcss"; and nothing else. Extra default styles from Vite's scaffold can override Tailwind.
Why isn't my React dashboard rendering?
Understand the vulnerability card rendering
The VulnerabilityCard component you added renders each security finding as a professional-looking card.
- Find the SEVERITY_COLORS object near the top of App.jsx:
const SEVERITY_COLORS = {
Critical: 'bg-red-600',
High: 'bg-orange-500',
Medium: 'bg-yellow-500',
Low: 'bg-blue-500',
};
How does severity mapping work?
Each finding from the backend includes a severity field (Critical, High, Medium, or Low). The SEVERITY_COLORS map converts that string into a Tailwind background class.
Inside VulnerabilityCard, the template literal ${SEVERITY_COLORS[finding.severity]} dynamically applies the correct color. The card also displays the CWE ID (like CWE-78 or CWE-89), the affected line number, a short title, and a longer description.
Integrate react-diff-viewer for code comparison
The right column of your dashboard shows a side-by-side diff comparing the original vulnerable code with the AI-generated secure refactoring.
- Find the ReactDiffViewer section in the second grid column of your App.jsx:
<ReactDiffViewer
oldValue={result.source_code}
newValue={result.refactored_code}
splitView={true}
useDarkTheme={true}
leftTitle="Original (Vulnerable)"
rightTitle="Refactored (Secure)"
/>
How does the diff viewer work?
The react-diff-viewer library compares two strings and renders a GitHub-style diff. Here is what each prop controls.
- oldValue receives the original code the user submitted. newValue receives the LLM's refactored secure version.
- splitView={true} shows the two versions side by side instead of interleaved.
- useDarkTheme={true} applies a dark color scheme that matches the rest of the dashboard.
- leftTitle and rightTitle label each column so the user knows which is the original and which is the fix.
The diff viewer only appears after an analysis completes and the backend returns a refactored_code value.
✔️ Awesome, I've got everything!
Great. Double check you have saved frontend/src/App.jsx and your dev server is running without errors.
ⓧ I'd like to double check the full code
Here is the complete frontend/src/App.jsx file:
import { useState, useEffect, useRef } from 'react';
import ReactDiffViewer from 'react-diff-viewer';
import './App.css';
const API_BASE = 'http://localhost:8000/api';
const SEVERITY_COLORS = {
Critical: 'bg-red-600',
High: 'bg-orange-500',
Medium: 'bg-yellow-500',
Low: 'bg-blue-500',
};
function VulnerabilityCard({ finding }) {
return (
<div className="border border-gray-700 rounded-lg p-4 mb-3 bg-gray-800">
<div className="flex items-center gap-2 mb-2">
<span className={`px-2 py-1 rounded text-xs font-bold text-white ${SEVERITY_COLORS[finding.severity]}`}>
{finding.severity}
</span>
<span className="text-gray-400 text-sm">{finding.cwe}</span>
<span className="text-gray-400 text-sm ml-auto">Line {finding.line}</span>
</div>
<h3 className="text-white font-semibold text-sm">{finding.title}</h3>
<p className="text-gray-400 text-xs mt-1">{finding.description}</p>
</div>
);
}
function App() {
const [code, setCode] = useState('');
const [loading, setLoading] = useState(false);
const [result, setResult] = useState(null);
const pollRef = useRef(null);
const handleAnalyze = async () => {
if (!code.trim()) return;
setLoading(true);
setResult(null);
try {
const response = await fetch(`${API_BASE}/analyze/`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ code }),
});
const data = await response.json();
pollForResults(data.analysis_id);
} catch (error) {
setLoading(false);
alert('Failed to submit analysis');
}
};
const pollForResults = (analysisId) => {
pollRef.current = setInterval(async () => {
try {
const response = await fetch(`${API_BASE}/results/${analysisId}/`);
const data = await response.json();
if (data.status === 'completed' || data.status === 'failed') {
clearInterval(pollRef.current);
setResult(data);
setLoading(false);
}
} catch (error) {
clearInterval(pollRef.current);
setLoading(false);
}
}, 2000);
};
useEffect(() => {
return () => {
if (pollRef.current) clearInterval(pollRef.current);
};
}, []);
return (
<div className="min-h-screen bg-gray-900 text-white p-6">
<header className="mb-8">
<h1 className="text-3xl font-bold">Code Security Analyzer</h1>
<p className="text-gray-400 mt-1">AI-powered vulnerability detection with AST + LLM analysis</p>
</header>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div>
<h2 className="text-lg font-semibold mb-3">Paste Python Code</h2>
<textarea
className="w-full h-64 bg-gray-800 border border-gray-700 rounded-lg p-4 font-mono text-sm text-green-300 resize-none focus:outline-none focus:border-blue-500"
placeholder="Paste your Python code here..."
value={code}
onChange={(e) => setCode(e.target.value)}
/>
<button
onClick={handleAnalyze}
disabled={loading || !code.trim()}
className="mt-4 px-6 py-3 bg-blue-600 hover:bg-blue-700 disabled:bg-gray-600 rounded-lg font-semibold transition-colors"
>
{loading ? 'Analyzing...' : 'Analyze Security'}
</button>
{result && result.status === 'completed' && (
<div className="mt-6">
<div className="flex items-center gap-4 mb-4">
<h2 className="text-lg font-semibold">Security Score</h2>
<span className={`text-2xl font-bold ${result.overall_score >= 70 ? 'text-green-400' : result.overall_score >= 40 ? 'text-yellow-400' : 'text-red-400'}`}>
{result.overall_score}/100
</span>
</div>
<h3 className="text-md font-semibold mb-3">
Vulnerabilities ({result.findings.length})
</h3>
{result.findings.map((finding, idx) => (
<VulnerabilityCard key={idx} finding={finding} />
))}
</div>
)}
</div>
<div>
{result && result.status === 'completed' && result.refactored_code && (
<div>
<h2 className="text-lg font-semibold mb-3">Suggested Fix (Diff View)</h2>
<div className="rounded-lg overflow-hidden border border-gray-700">
<ReactDiffViewer
oldValue={result.source_code}
newValue={result.refactored_code}
splitView={true}
useDarkTheme={true}
leftTitle="Original (Vulnerable)"
rightTitle="Refactored (Secure)"
/>
</div>
</div>
)}
</div>
</div>
</div>
);
}
export default App;
Your frontend is ready to display results. Next, you will start all the services together and test the full pipeline end to end.
Run an End-to-End Integration Test
You have all the individual pieces built. The AST scanner detects structural vulnerabilities, the LLM analyzer reasons about logic flaws, the Celery task chains them together, and the React dashboard displays results.
Now it is time to run all four services simultaneously and watch the full pipeline in action. You will submit deliberately vulnerable Python code through the React UI and verify that every stage works correctly.
In this step, get ready to:
- Start all four services (Redis, Django, Celery, React) simultaneously.
- Submit deliberately vulnerable Python code through the dashboard.
- Verify vulnerability cards, severity scores, and the diff viewer display correctly.
Start all services
This project requires four processes running at the same time. Each one needs its own terminal window or tab.
- Open four separate terminal windows or tabs.
- In each terminal, navigate to your codeaudit project folder on your Desktop.
- Make sure your Python virtual environment is activated in each terminal that runs Python commands.
Why four terminals?
Each service runs as a long-lived process that occupies the terminal. Redis listens for connections, Django serves HTTP requests, Celery polls for tasks, and Vite serves the React app. They all need to run in parallel for the pipeline to work.
- Confirm Redis is still running by running this in your first terminal:
redis-cli ping
- You should see PONG in the output.
Not seeing PONG?
Redis may have stopped since you started it. Restart it by running redis-server in a dedicated terminal (macOS/Linux) or restarting Memurai (Windows).
Help me get Redis running again.
Make sure your GROQ_API_KEY environment variable is set in the terminals where you will run Django and Celery.
- Export your API key by running this in both the Django terminal and the Celery terminal:
macOS
export GROQ_API_KEY=your-api-key-here
Windows
$env:GROQ_API_KEY="your-api-key-here"
- Replace your-api-key-here with the actual Groq API key you created earlier.
- Start the Django development server in your second terminal by running:
python manage.py runserver
- You should see output confirming Django is running at http://127.0.0.1:8000/.
- Start the Celery worker in your third terminal by running:
macOS
celery -A codeaudit worker -l INFO
Linux users can run the same command.
Windows
celery -A codeaudit worker -l INFO -P solo
Why the extra flag?
Celery's default prefork pool does not work on Windows. The -P solo flag tells Celery to use a single-threaded pool instead, which works reliably for development.
- You should see a banner with the Celery logo and a line confirming the worker is ready to accept tasks.
- Start the React development server in your fourth terminal by running:
cd frontend
npm run dev
- You should see Vite output confirming the app is running at http://localhost:5173/.
Django or Celery failing to start?
If Django reports a module not found error, make sure your virtual environment is activated in that terminal. If Celery cannot connect, confirm Redis is running with redis-cli ping.
Help me troubleshoot my services not starting.
Verify the full pipeline with vulnerable test code
With all services running, you can now test the complete analysis pipeline. You will paste deliberately vulnerable Python code into the React dashboard and watch the system detect the flaws.
- Open your browser and navigate to http://localhost:5173/.
- You should see the Code Security Analyzer dashboard with a dark background, a textarea labeled Paste Python Code, and an Analyze Security button.
- Copy the following deliberately vulnerable Python code and paste it into the textarea:
import os
# Hardcoded credential
api_key = "sk-secret-12345-do-not-share"
# Dangerous eval on user input
user_input = input("Enter expression: ")
result = eval(user_input)
# OS command injection
os.system(f"echo {user_input}")
# SQL injection via f-string
username = "admin"
query = f"SELECT * FROM users WHERE name = '{username}'"
What vulnerabilities does this code contain?
- A hardcoded API key (CWE-798) stored directly in source code.
- An eval() call on untrusted input (CWE-95) allowing arbitrary code execution.
- An os.system() call (CWE-78) enabling OS command injection.
- A SQL query built with an f-string (CWE-89) vulnerable to SQL injection.
- Click the Analyze Security button.
The button text changes to Analyzing... while the async task processes. The frontend polls every 2 seconds until results arrive.
- Wait 10-30 seconds for the analysis to complete.
What is happening behind the scenes?
Your React app POSTs the code to /api/analyze/. Django creates an AnalysisResult record and dispatches analyze_code_task to Celery.
The Celery worker first runs the AST scanner to find structural issues. Then it sends the code plus those findings to the Groq API (Llama 3.3 70B) for deeper reasoning. The LLM validates findings, identifies additional issues, and generates a refactored version.
You can watch the Celery terminal to see the task being picked up and completed in real time.
Once the analysis completes, verify these elements on the dashboard:
- A Security Score out of 100 appears. With four serious vulnerabilities, expect a score below 40 (displayed in red).
- Vulnerability cards appear with color-coded severity badges. Critical findings (eval, os.system) show red badges. High findings (hardcoded credential) show orange badges.
- Each card shows the CWE ID, the affected line number, and a description of the vulnerability.
- The right panel shows a Suggested Fix (Diff View) with the original vulnerable code on the left and the AI-refactored secure version on the right.
Not seeing results after 30 seconds?
Check your Celery terminal for error messages. The most common issue is a missing or invalid GROQ_API_KEY environment variable. If you see an authentication error, re-export the key in the terminal running Celery and restart it.
If the Celery terminal shows no activity at all, verify Redis is running. The task cannot be dispatched without the broker.
Help me debug why my analysis is not completing.
Your AI-powered code security analyzer is fully operational. You have a complete pipeline from code submission through AST analysis, LLM reasoning, and interactive result display. This is the architecture pattern used by production security tools.
Secret mission
Vulnerability History & Security Score Trends
Your analyzer runs scans, but once the results disappear, there is no way to track improvement over time. Real DevSecOps teams measure progress. In this mission, you will add a history tracking system that stores every scan and displays a security score trend chart on a new dashboard view.
Clean Up Your Resources
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely locally with free-tier Groq API access, so there are no ongoing financial costs.
Resources you used:
- Running services (Redis server, Django dev server, Celery worker, React dev server).
- Project folder containing codeaudit (Django backend, SQLite database) and frontend (React app).
- Python virtual environment.
- Groq API key (environment variable in your terminal session).
Keep everything running
No action needed. Choose this if you're still actively building or want to continue experimenting with the analyzer.
- Your four services (Redis, Django, Celery, React) will continue running until you close the terminal windows or restart your machine.
- Redis uses a small amount of system memory while running. The Django and React dev servers are lightweight.
- Your Groq API key remains active. Free tier usage resets daily.
Pause - I'll come back to this later
Shut down running processes to free up memory. Your project files stay on disk so you can restart everything later.
- Press Ctrl+C in each terminal window running the Django dev server, Celery worker, and React dev server.
- Stop the Redis server by running:
redis-cli shutdown
- Deactivate your Python virtual environment by running:
deactivate
- To resume later, reactivate your virtual environment, start Redis, then start the Django, Celery, and React dev servers again.
Delete - I don't want to use this again
Remove all project resources and start fresh. This cannot be undone.
- Press Ctrl+C in each terminal window running the Django dev server, Celery worker, and React dev server.
- Stop the Redis server by running:
redis-cli shutdown
- Deactivate your Python virtual environment by running:
deactivate
- Delete the project folder and virtual environment. On macOS/Linux, run:
cd ~/Desktop
rm -rf codeaudit
- On Windows, run:
cd ~\Desktop
rmdir /s /q codeaudit
- Unset the Groq API key environment variable in your current terminal session by running:
unset GROQ_API_KEY
- On Windows, run:
set GROQ_API_KEY=
- If you want to revoke your Groq API key, go to console.groq.com, navigate to API Keys, and delete the key you created for this project.
Nice Work!
Nice Work!
You just built a full-stack AI-powered code security analyzer from scratch. This project demonstrates production-grade architecture patterns that AI startups and DevSecOps teams use every day.
You learned how to:
- Build a custom AST-based security scanner using Python's ast module to detect structural vulnerabilities like command injection, hardcoded credentials, and SQL injection.
- Architect an async processing pipeline with Django, Celery, and Redis that chains structural analysis with LLM-powered deep code reasoning via Groq's Llama 3.3 70B model.
- Connect a React + Tailwind CSS dashboard with severity-scored vulnerability cards and an interactive diff viewer comparing vulnerable code against AI-generated secure alternatives.
- Secret Mission: Add a vulnerability history feature with a Recharts line chart tracking security scores over time.
Ready to quiz yourself?