Skip to content

Add FastAPI server and visualization for SimpleAudit results#6

Merged
SushantGautam merged 2 commits intomainfrom
dev
Feb 16, 2026
Merged

Add FastAPI server and visualization for SimpleAudit results#6
SushantGautam merged 2 commits intomainfrom
dev

Conversation

@SushantGautam
Copy link
Copy Markdown
Collaborator

  • Implemented a FastAPI server in server.py to serve visualization pages and handle API requests for JSON audit results.
  • Created HTML template visualizer.html for displaying audit results with a responsive design using Tailwind CSS.
  • Added a favicon image thumbnail.png for the web application.
  • Introduced API endpoints to retrieve the file tree of JSON files and serve individual JSON file contents.
  • Implemented validation for JSON files to ensure they contain valid audit results.

- Implemented a FastAPI server in `server.py` to serve visualization pages and handle API requests for JSON audit results.
- Created HTML template `visualizer.html` for displaying audit results with a responsive design using Tailwind CSS.
- Added a favicon image `thumbnail.png` for the web application.
- Introduced API endpoints to retrieve the file tree of JSON files and serve individual JSON file contents.
- Implemented validation for JSON files to ensure they contain valid audit results.
Copilot AI review requested due to automatic review settings February 16, 2026 12:47
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a visualization feature set to SimpleAudit, including a FastAPI-powered results browser and standalone HTML viewers, plus CLI/packaging updates to make it runnable via simpleaudit serve.

Changes:

  • Introduces a FastAPI server to browse a results directory and serve JSON + visualization pages.
  • Adds/updates HTML visualizers for multi-file browsing and standalone scenario viewing.
  • Adds a CLI entrypoint + visualize optional dependencies; improves results save/plot to auto-create output directories.

Reviewed changes

Copilot reviewed 8 out of 9 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
simpleaudit/visualization/visualizer.html New multi-file UI (file tree + scenario list/detail + stats).
simpleaudit/visualization/server.py New FastAPI server exposing UI routes and JSON/file-tree APIs.
simpleaudit/visualization/scenario_viewer.html Updates standalone viewer UX (upload section, mobile behavior, sample data).
simpleaudit/visualization/README.md Documentation for using the server and standalone viewer.
simpleaudit/results.py Ensure parent directories exist for save() and plot(save_path=...).
simpleaudit/cli.py New simpleaudit CLI with serve command.
pyproject.toml Adds console script and visualize optional dependencies.
README.md Updates usage examples and links to visualization docs/UI.
Comments suppressed due to low confidence (2)

simpleaudit/visualization/scenario_viewer.html:456

  • Severity breakdown doesn’t account for severity "ERROR" results (supported elsewhere in the project): the bar/legend are built only from counts keys, so ERROR scenarios are silently omitted and the dashboard becomes misleading. Include ERROR (and normalize casing consistently) in the counted severities and color mapping.
    simpleaudit/visualization/scenario_viewer.html:488
  • Within renderList, scenario name/description are later interpolated into innerHTML (including a title attribute) without escaping, which enables HTML/JS injection if an untrusted JSON file is opened. Prefer textContent/setAttribute or escape the strings before inserting them.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +743 to +752
el.innerHTML = `
<div class="w-1.5 self-stretch rounded-full ${statusColor} shrink-0"></div>
<div class="flex-1 min-w-0">
<div class="flex justify-between items-start">
<h3 class="font-medium text-gray-900 truncate text-sm" title="${s.scenario_name || s.name}">${s.scenario_name || s.name || 'Untitled Scenario'}</h3>
<span class="text-xs font-bold ${textColor} ml-2 shrink-0">${s.score !== undefined ? s.score + '/10' : ''}</span>
</div>
<p class="text-xs text-gray-500 mt-1 line-clamp-2">${s.scenario_description || s.description || 'No description'}</p>
</div>
`;
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The scenario list is built via innerHTML using unescaped scenario_name/name and scenario_description/description (including in the title attribute). This allows HTML/JS injection if a JSON file contains malicious strings. Escape these fields (or use textContent/setAttribute) before inserting into the DOM.

Suggested change
el.innerHTML = `
<div class="w-1.5 self-stretch rounded-full ${statusColor} shrink-0"></div>
<div class="flex-1 min-w-0">
<div class="flex justify-between items-start">
<h3 class="font-medium text-gray-900 truncate text-sm" title="${s.scenario_name || s.name}">${s.scenario_name || s.name || 'Untitled Scenario'}</h3>
<span class="text-xs font-bold ${textColor} ml-2 shrink-0">${s.score !== undefined ? s.score + '/10' : ''}</span>
</div>
<p class="text-xs text-gray-500 mt-1 line-clamp-2">${s.scenario_description || s.description || 'No description'}</p>
</div>
`;
// Build inner content safely without using innerHTML for untrusted data
const statusDiv = document.createElement('div');
statusDiv.className = `w-1.5 self-stretch rounded-full ${statusColor} shrink-0`;
const flexContainer = document.createElement('div');
flexContainer.className = 'flex-1 min-w-0';
const headerRow = document.createElement('div');
headerRow.className = 'flex justify-between items-start';
const titleEl = document.createElement('h3');
titleEl.className = 'font-medium text-gray-900 truncate text-sm';
const scenarioTitle = s.scenario_name || s.name || 'Untitled Scenario';
titleEl.textContent = scenarioTitle;
titleEl.setAttribute('title', s.scenario_name || s.name || '');
const scoreEl = document.createElement('span');
scoreEl.className = `text-xs font-bold ${textColor} ml-2 shrink-0`;
scoreEl.textContent = s.score !== undefined ? s.score + '/10' : '';
headerRow.appendChild(titleEl);
headerRow.appendChild(scoreEl);
const descEl = document.createElement('p');
descEl.className = 'text-xs text-gray-500 mt-1 line-clamp-2';
descEl.textContent = s.scenario_description || s.description || 'No description';
flexContainer.appendChild(headerRow);
flexContainer.appendChild(descEl);
el.appendChild(statusDiv);
el.appendChild(flexContainer);

Copilot uses AI. Check for mistakes.
Comment on lines +816 to +825
<h2 class="text-2xl font-bold text-gray-900 leading-tight">${name}</h2>
<div class="flex items-center gap-3 mt-2">
<span class="px-2.5 py-0.5 rounded-full text-xs font-medium uppercase tracking-wide ${statusColor} text-white bg-opacity-90">
${severity}
</span>
<span class="text-sm text-gray-500">Score: <strong class="${textColor}">${score !== undefined ? score + '/10' : 'N/A'}</strong></span>
</div>
</div>
</div>
<p class="text-gray-600 leading-relaxed">${desc}</p>
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the detail view, name and desc are interpolated into HTML without escaping, which enables XSS from a crafted JSON results file. Use the existing escapeHtml helper (or DOM APIs like textContent) for these fields (and consider escaping severity too).

Suggested change
<h2 class="text-2xl font-bold text-gray-900 leading-tight">${name}</h2>
<div class="flex items-center gap-3 mt-2">
<span class="px-2.5 py-0.5 rounded-full text-xs font-medium uppercase tracking-wide ${statusColor} text-white bg-opacity-90">
${severity}
</span>
<span class="text-sm text-gray-500">Score: <strong class="${textColor}">${score !== undefined ? score + '/10' : 'N/A'}</strong></span>
</div>
</div>
</div>
<p class="text-gray-600 leading-relaxed">${desc}</p>
<h2 class="text-2xl font-bold text-gray-900 leading-tight">${escapeHtml(name)}</h2>
<div class="flex items-center gap-3 mt-2">
<span class="px-2.5 py-0.5 rounded-full text-xs font-medium uppercase tracking-wide ${statusColor} text-white bg-opacity-90">
${escapeHtml(severity)}
</span>
<span class="text-sm text-gray-500">Score: <strong class="${textColor}">${score !== undefined ? score + '/10' : 'N/A'}</strong></span>
</div>
</div>
</div>
<p class="text-gray-600 leading-relaxed">${escapeHtml(desc)}</p>

Copilot uses AI. Check for mistakes.
Comment on lines +668 to +674
const counts = {
'critical': 0, 'high': 0, 'medium': 0, 'low': 0, 'pass': 0
};
allScenarios.forEach(s => {
const sev = (s.severity || 'low').toLowerCase();
if (counts[sev] !== undefined) counts[sev]++;
});
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity breakdown ignores ERROR results: counts only includes critical/high/medium/low/pass, while data can contain severity "ERROR" (and you already define color mappings for it). This makes the dashboard inaccurate (segments won't sum to total). Include ERROR in counts and handle consistent casing.

Copilot uses AI. Check for mistakes.
Comment on lines +7 to +13
from typing import Dict, List, Optional
from fastapi import FastAPI, HTTPException
from fastapi.responses import HTMLResponse, JSONResponse, FileResponse
from fastapi.staticfiles import StaticFiles
import uvicorn


Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are unused imports here (e.g., Optional and StaticFiles). This will fail ruff/flake checks (F401) in CI/lint. Remove unused imports or wire them up (e.g., mount StaticFiles if intended).

Suggested change
from typing import Dict, List, Optional
from fastapi import FastAPI, HTTPException
from fastapi.responses import HTMLResponse, JSONResponse, FileResponse
from fastapi.staticfiles import StaticFiles
import uvicorn
from typing import Dict, List
from fastapi import FastAPI, HTTPException
from fastapi.responses import HTMLResponse, JSONResponse, FileResponse

Copilot uses AI. Check for mistakes.
Comment on lines +93 to +107
@app.get("/")
async def root():
"""Serve the main visualization page."""
html_path = Path(__file__).parent / "visualizer.html"

if not html_path.exists():
return HTMLResponse(
content="<h1>Error: Visualization template not found</h1>",
status_code=500
)

with open(html_path, "r", encoding="utf-8") as f:
content = f.read()

return HTMLResponse(content=content)
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Packaging concern: server.py loads visualizer.html, scenario_viewer.html, and thumbnail.png from Path(__file__).parent, but simpleaudit/visualization/ currently has no __init__.py and there’s no setuptools package-data configuration in pyproject. As a result, these assets (and possibly server.py itself) may be omitted from the built wheel, breaking simpleaudit serve after installation. Add simpleaudit/visualization/__init__.py and configure package data to include the HTML/PNG/README files.

Copilot uses AI. Check for mistakes.
Comment on lines +72 to +78
**Features:**
- ✨ Works completely offline
- 🔒 No data leaves your computer
- 📤 Drag & drop JSON files
- 🎯 Sample data included for demo
- 📱 Mobile friendly
- 🔄 Upload multiple files sequentially
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The README claims the standalone viewer "works completely offline", but scenario_viewer.html pulls Tailwind from https://cdn.tailwindcss.com, so fully offline use will lose styling (and some Tailwind-dependent layout). Either vendor Tailwind locally / inline minimal CSS, or adjust the docs to clarify that an internet connection is required for styling.

Copilot uses AI. Check for mistakes.
Comment on lines +159 to +171
full_path = os.path.normpath(os.path.join(RESULTS_DIR, file_path))

if not full_path.startswith(os.path.normpath(RESULTS_DIR)):
raise HTTPException(status_code=403, detail="Access denied")

if not os.path.exists(full_path):
raise HTTPException(status_code=404, detail="File not found")

if not full_path.endswith('.json'):
raise HTTPException(status_code=400, detail="Not a JSON file")

try:
with open(full_path, "r", encoding="utf-8") as f:
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The path traversal protection is brittle: full_path.startswith(normpath(RESULTS_DIR)) can be bypassed with prefix paths like /base/results_evil/... and doesn’t account for symlinks or case-insensitive filesystems. Use resolved Path objects and check requested.resolve().is_relative_to(results_dir.resolve()) (or compare os.path.commonpath).

Suggested change
full_path = os.path.normpath(os.path.join(RESULTS_DIR, file_path))
if not full_path.startswith(os.path.normpath(RESULTS_DIR)):
raise HTTPException(status_code=403, detail="Access denied")
if not os.path.exists(full_path):
raise HTTPException(status_code=404, detail="File not found")
if not full_path.endswith('.json'):
raise HTTPException(status_code=400, detail="Not a JSON file")
try:
with open(full_path, "r", encoding="utf-8") as f:
base_dir = Path(RESULTS_DIR).resolve()
requested_path = (base_dir / file_path).resolve()
# Confirm that the requested path is within the results directory
try:
requested_path.relative_to(base_dir)
except ValueError:
raise HTTPException(status_code=403, detail="Access denied")
if not requested_path.exists():
raise HTTPException(status_code=404, detail="File not found")
if requested_path.suffix != '.json':
raise HTTPException(status_code=400, detail="Not a JSON file")
try:
with requested_path.open("r", encoding="utf-8") as f:

Copilot uses AI. Check for mistakes.
Comment on lines +44 to +52
from .visualization.server import start_server

# Default to current directory if not specified
results_dir = args.results_dir
if results_dir is None:
results_dir = "."
print("⚠️ Warning: --results_dir not specified, using current directory '.'")
print(" Recommended: explicitly set --results_dir to avoid confusion\n")

Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

simpleaudit serve will import the visualization server unconditionally; if users installed the base package without the visualize extra, this will raise ImportError for fastapi/uvicorn. Catch that import error and print a clear message telling the user to install simpleaudit[visualize].

Suggested change
from .visualization.server import start_server
# Default to current directory if not specified
results_dir = args.results_dir
if results_dir is None:
results_dir = "."
print("⚠️ Warning: --results_dir not specified, using current directory '.'")
print(" Recommended: explicitly set --results_dir to avoid confusion\n")
try:
from .visualization.server import start_server
except ImportError:
print(
"Error: Visualization server dependencies are not installed.\n"
"Please install the visualize extra, e.g.:\n"
" pip install 'simpleaudit[visualize]'",
file=sys.stderr,
)
sys.exit(1)
# Default to current directory if not specified
results_dir = args.results_dir
if results_dir is None:
results_dir = "."
print("⚠️ Warning: --results_dir not specified, using current directory '.'")
print(" Recommended: explicitly set --results_dir to avoid confusion\n")

Copilot uses AI. Check for mistakes.
Comment on lines +8 to +56
def main():
"""Main entry point for simpleaudit CLI."""
parser = argparse.ArgumentParser(
prog="simpleaudit",
description="SimpleAudit CLI - AI Safety Auditing Tools"
)

subparsers = parser.add_subparsers(dest="command", help="Available commands")

# Serve command
serve_parser = subparsers.add_parser(
"serve",
help="Start a web server to visualize audit results"
)
serve_parser.add_argument(
"--results_dir",
type=str,
default=None,
help="Directory containing JSON result files to visualize (default: current directory)"
)
serve_parser.add_argument(
"--port",
type=int,
default=8000,
help="Port to run the server on (default: 8000)"
)
serve_parser.add_argument(
"--host",
type=str,
default="127.0.0.1",
help="Host to bind the server to (default: 127.0.0.1)"
)

args = parser.parse_args()

if args.command == "serve":
from .visualization.server import start_server

# Default to current directory if not specified
results_dir = args.results_dir
if results_dir is None:
results_dir = "."
print("⚠️ Warning: --results_dir not specified, using current directory '.'")
print(" Recommended: explicitly set --results_dir to avoid confusion\n")

start_server(results_dir, args.host, args.port)
else:
parser.print_help()
sys.exit(1)
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New CLI entrypoint behavior is introduced (arg parsing + serve command), but there are no tests covering basic CLI invocation/error cases. Since the repo has an existing pytest suite, add a small test module that exercises simpleaudit.cli:main (e.g., help output, unknown command exit code, serve import failure messaging).

Copilot uses AI. Check for mistakes.
Updated wording for clarity regarding the local web server.
@SushantGautam SushantGautam merged commit b9a877f into main Feb 16, 2026
2 checks passed
SushantGautam added a commit that referenced this pull request Feb 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants