Skip to content

[SCAN] Scan history not persisted between sessions — output files exist but UI has no way to browse, reload, or compare past scan results #276

@uv05709

Description

@uv05709

Summary

SecuScan saves scan results to the output/ directory, but the React
frontend has no way to list, reload, or compare past scans. Every
session starts blank — previous findings in output/ are invisible to
the UI unless the user manually digs through files.


🚨 Problem

Current broken flow:

  1. User runs Scan A on target1.local → results saved to output/scan_xyz.json
  2. User applies a fix to the target
  3. User runs Scan B on the same target
  4. Scan A results are gone from the UI — no way to compare before/after
  5. The output/ folder accumulates files that the frontend never reads

This is a fundamental gap for a pentesting tool. Security engineers
always need to compare scan results across time to verify that
vulnerabilities were actually remediated.

Reproduction:

Run any scan via the SecuScan UI
Close the browser tab / refresh the page
Navigate back to the Findings page
→ Expected: previous scan results are listed and selectable
→ Actual: Findings page is empty / shows only current scan state

✅ Proposed Fix — Scan History Sidebar + Result Loader

Backend: API endpoint to list past scan results

# backend/routes/history.py

import os, json
from pathlib import Path
from flask import Blueprint, jsonify   # or FastAPI router equivalent

history_bp = Blueprint("history", __name__)
OUTPUT_DIR = Path(__file__).parent.parent / "output"

@history_bp.route("/api/history", methods=["GET"])
def list_scans():
    """Return metadata for all past scans in output/"""
    scans = []
    for f in sorted(OUTPUT_DIR.glob("*.json"), key=os.path.getmtime, reverse=True):
        try:
            with open(f) as fp:
                data = json.load(fp)
            scans.append({
                "id": f.stem,
                "filename": f.name,
                "target": data.get("target", "unknown"),
                "timestamp": data.get("timestamp", str(f.stat().st_mtime)),
                "finding_count": len(data.get("findings", [])),
                "severity_summary": _count_severities(data.get("findings", []))
            })
        except Exception:
            continue
    return jsonify(scans)

@history_bp.route("/api/history/<scan_id>", methods=["GET"])
def get_scan(scan_id: str):
    """Load a specific past scan by ID"""
    path = OUTPUT_DIR / f"{scan_id}.json"
    if not path.exists():
        return jsonify({"error": "Scan not found"}), 404
    with open(path) as fp:
        return jsonify(json.load(fp))

def _count_severities(findings: list) -> dict:
    counts = {"critical": 0, "high": 0, "medium": 0, "low": 0, "info": 0}
    for f in findings:
        sev = f.get("severity", "info").lower()
        counts[sev] = counts.get(sev, 0) + 1
    return counts

Frontend: Scan History sidebar component

// frontend/src/components/ScanHistory.tsx
import { useEffect, useState } from "react";

interface ScanMeta {
  id: string;
  target: string;
  timestamp: string;
  finding_count: number;
  severity_summary: Record<string, number>;
}

interface Props {
  onSelect: (scanId: string) => void;
  activeScanId?: string;
}

export function ScanHistory({ onSelect, activeScanId }: Props) {
  const [history, setHistory] = useState<ScanMeta[]>([]);

  useEffect(() => {
    fetch("/api/history")
      .then((r) => r.json())
      .then(setHistory)
      .catch(console.error);
  }, []);

  if (history.length === 0) {
    return (
      <div className="text-muted-foreground text-sm p-4">
        No past scans found.
      </div>
    );
  }

  return (
    <div className="flex flex-col gap-1 p-2">
      <h3 className="text-xs font-semibold uppercase tracking-wide text-muted-foreground px-2 mb-1">
        Scan History
      </h3>
      {history.map((scan) => (
        <button
          key={scan.id}
          onClick={() => onSelect(scan.id)}
          className={`text-left rounded-md px-3 py-2 text-sm transition-colors
            ${activeScanId === scan.id
              ? "bg-primary text-primary-foreground"
              : "hover:bg-muted"}`}
        >
          <div className="font-medium truncate">{scan.target}</div>
          <div className="text-xs text-muted-foreground flex gap-2 mt-0.5">
            <span>{new Date(scan.timestamp).toLocaleDateString()}</span>
            <span>·</span>
            <span>{scan.finding_count} findings</span>
            {scan.severity_summary.critical > 0 && (
              <span className="text-red-500 font-semibold">
                {scan.severity_summary.critical} critical
              </span>
            )}
          </div>
        </button>
      ))}
    </div>
  );
}

Wire into the Findings page

// frontend/src/pages/Findings.tsx  (update existing)
import { ScanHistory } from "@/components/ScanHistory";

// Add state for selected historical scan
const [activeScanId, setActiveScanId] = useState<string | undefined>();

// When a history item is selected, fetch and display it
useEffect(() => {
  if (!activeScanId) return;
  fetch(`/api/history/${activeScanId}`)
    .then(r => r.json())
    .then(data => setFindings(data.findings))
    .catch(console.error);
}, [activeScanId]);

// In JSX — add sidebar
<div className="flex gap-4">
  <aside className="w-64 shrink-0 border-r">
    <ScanHistory onSelect={setActiveScanId} activeScanId={activeScanId} />
  </aside>
  <main className="flex-1">
    {/* existing findings table */}
  </main>
</div>

📊 Impact Table

Scenario Before After
View past scan ❌ Impossible ✅ Click in sidebar
Compare before/after fix ❌ Impossible ✅ Switch between scans
output/ folder Fills up silently Browseable in UI
Re-run same target Loses previous data Both scans visible
Critical finding count Only current scan All historical scans

📁 Files to Add / Modify

  • backend/routes/history.py — new API blueprint (2 endpoints)
  • backend/app.py — register history_bp
  • frontend/src/components/ScanHistory.tsx — new sidebar component
  • frontend/src/pages/Findings.tsx (or equivalent) — wire sidebar + history fetch

🧪 How to Test

  1. Run 2 separate scans on different targets
  2. Navigate to Findings page → sidebar should list both scans with target name + date
  3. Click scan 1 → findings table updates to show scan 1 results
  4. Click scan 2 → findings table switches to scan 2 results
  5. Refresh page → sidebar still shows both scans (persisted from output/)

Happy to implement if assigned.

Estimated effort: 4–6 hours | Difficulty: Intermediate
Labels: level:intermediate (35 pts) · type:feature · gssoc

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions