From ebc772752fb292284870299f857c6e0be9b06eee Mon Sep 17 00:00:00 2001 From: Sanjana S Aithal Date: Tue, 2 Jun 2026 15:04:58 +0530 Subject: [PATCH 1/2] feat: implement collapsible full-stack session history index to findings desk --- backend/routes/history.py | 26 + backend/secuscan/main.py | 3 +- frontend/package-lock.json | 33 +- frontend/src/components/ScanHistory.tsx | 123 +++++ frontend/src/pages/Findings.tsx | 690 ++++++++++++------------ 5 files changed, 516 insertions(+), 359 deletions(-) create mode 100644 backend/routes/history.py create mode 100644 frontend/src/components/ScanHistory.tsx diff --git a/backend/routes/history.py b/backend/routes/history.py new file mode 100644 index 00000000..29814045 --- /dev/null +++ b/backend/routes/history.py @@ -0,0 +1,26 @@ +import os +import json +from pathlib import Path +from fastapi import APIRouter, HTTPException + + +router = APIRouter(prefix="/api/history", tags=["History"]) + +@router.get("") +def list_scans(): + """Scan history index — reads all logs in output directory and summarizes them""" + return scans + +@router.get("/{scan_id}") +def get_scan(scan_id: str): + """Scan details engine — returns the complete structural payload of a specific file""" + path = OUTPUT_DIR / f"{scan_id}.json" + + if not path.exists(): + raise HTTPException(status_code=404, detail="Scan record not found") + + try: + with open(path, "r") as fp: + return json.load(fp) + except (json.JSONDecodeError, IOError): + raise HTTPException(status_code=500, detail="Failed to parse scan file") \ No newline at end of file diff --git a/backend/secuscan/main.py b/backend/secuscan/main.py index 08eb02c2..8b89a80f 100644 --- a/backend/secuscan/main.py +++ b/backend/secuscan/main.py @@ -18,6 +18,7 @@ from .plugins import init_plugins from .routes import router from .workflows import scheduler +from backend.routes.history import router as history_router # Configure logging @@ -116,7 +117,7 @@ async def redirect_api_openapi(): # Include API routes app.include_router(router) - +app.include_router(history_router) # Health check endpoint @app.get("/api/v1/health") diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 90a66524..6267a517 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -143,7 +143,6 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -505,7 +504,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=20.19.0" }, @@ -554,7 +552,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=20.19.0" } @@ -564,7 +561,6 @@ "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.4.0.tgz", "integrity": "sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw==", "license": "MIT", - "peer": true, "dependencies": { "@emotion/memoize": "^0.9.0" } @@ -1670,7 +1666,8 @@ "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@types/babel__core": { "version": "7.20.5", @@ -1793,7 +1790,6 @@ "integrity": "sha512-z+pdZyxE+RTQE9AcboAZCb4otwcrvgHD+GlBpPgn0emDVt0ohrTMhAwlr2Wd9nZ+nihhYFxO2pThz3C5qSu2Eg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.21.0" } @@ -1824,7 +1820,6 @@ "integrity": "sha512-WeqMfGJLGuLCqHGYRGHxnKrXcTitc6L/nBUWfWPcTarG3t9PsquqUMuVeXZeca+mglY4Vo5GZjCi0A3Or2lnxA==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -1836,7 +1831,6 @@ "integrity": "sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/react": "*" } @@ -1967,6 +1961,7 @@ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=8" } @@ -1977,6 +1972,7 @@ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=10" }, @@ -2147,7 +2143,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", @@ -2642,7 +2637,8 @@ "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/dompurify": { "version": "3.4.2", @@ -3090,7 +3086,6 @@ "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", "dev": true, "license": "MIT", - "peer": true, "bin": { "jiti": "bin/jiti.js" } @@ -3259,6 +3254,7 @@ "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "dev": true, "license": "MIT", + "peer": true, "bin": { "lz-string": "bin/bin.js" } @@ -3551,7 +3547,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -3699,6 +3694,7 @@ "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", @@ -3713,7 +3709,8 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/punycode": { "version": "2.3.1", @@ -3761,7 +3758,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -3774,7 +3770,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -3804,7 +3799,6 @@ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", "license": "MIT", - "peer": true, "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" @@ -3935,8 +3929,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/redux-thunk": { "version": "3.1.0", @@ -4432,7 +4425,6 @@ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -4657,7 +4649,6 @@ "integrity": "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", @@ -5294,7 +5285,6 @@ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -5846,7 +5836,6 @@ "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", diff --git a/frontend/src/components/ScanHistory.tsx b/frontend/src/components/ScanHistory.tsx new file mode 100644 index 00000000..6e0456e3 --- /dev/null +++ b/frontend/src/components/ScanHistory.tsx @@ -0,0 +1,123 @@ +import { useEffect, useState } from "react"; + +interface ScanMeta { + id: string; + filename: string; + target: string; + timestamp: number; + finding_count: number; + severity_summary: { + critical: number; + high: number; + medium: number; + low: number; + info: number; + }; +} + +interface Props { + onSelect: (scanId: string) => void; + activeScanId?: string; +} + +export default function ScanHistory({ onSelect, activeScanId }: Props) { + const [history, setHistory] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + fetch("http://127.0.0.1:8000/api/history") + .then((res) => { + if (!res.ok) throw new Error("Failed to load audit records"); + return res.json(); + }) + .then((data) => { + setHistory(data); + setLoading(false); + }) + .catch((err) => { + console.error("Error connecting to history module:", err); + setLoading(false); + }); + }, []); + + // Sleek cyber-themed loading skeleton to match your design system + if (loading) { + return ( +
+ // SYNCING_MATRIX_INDEX... +
+ ); + } + + return ( +
+ {/* Title block formatted exactly like your main filter labels */} +
+

+ // Session Index +

+
+ +
+ {history.length === 0 ? ( + // Boxed fallback tile using your layout's exact background classes +
+

No archived logs

+
+ ) : ( + history.map((scan) => { + const isSelected = activeScanId === scan.id; + const formattedDate = new Date(scan.timestamp * 1000).toLocaleDateString(undefined, { + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit' + }); + + return ( + + ); + }) + )} +
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/pages/Findings.tsx b/frontend/src/pages/Findings.tsx index cfc78824..8eb6065b 100644 --- a/frontend/src/pages/Findings.tsx +++ b/frontend/src/pages/Findings.tsx @@ -2,6 +2,8 @@ import React, { useEffect, useMemo, useState } from 'react' import { motion } from 'framer-motion' import { getFindings } from '../api' import { formatLocaleDate, parseDateSafe, getCurrentTimeZone } from '../utils/date' +import ScanHistory from "../components/ScanHistory" + type Finding = { id: string severity: string @@ -17,7 +19,6 @@ type Finding = { } type FindingStatus = 'new' | 'reviewed' | 'suppressed' - type ReviewState = Record const severityOrder = ['critical', 'high', 'medium', 'low', 'info'] as const @@ -103,6 +104,8 @@ export default function Findings() { const [selectedFindingId, setSelectedFindingId] = useState(null) const [reviewState, setReviewState] = useState({}) const [copiedFindingId, setCopiedFindingId] = useState(null) + const [activeScanId, setActiveScanId] = useState() + const [showHistory, setShowHistory] = useState(false) useEffect(() => { setLoading(true) @@ -126,6 +129,24 @@ export default function Findings() { } }, []) + useEffect(() => { + if (!activeScanId) return + + setLoading(true) + fetch(`http://127.0.0.1:8000/api/history/${activeScanId}`) + .then((res) => { + if (!res.ok) throw new Error("Failed to load historical scan") + return res.json() + }) + .then((data) => { + const nextFindings = data.findings || [] + setFindings(nextFindings) + setSelectedFindingId(nextFindings[0]?.id ?? null) + }) + .catch((err) => console.error("Error connecting to history file loader:", err)) + .finally(() => setLoading(false)) + }, [activeScanId]) + useEffect(() => { localStorage.setItem('secuscan-finding-review-state', JSON.stringify(reviewState)) }, [reviewState]) @@ -140,7 +161,6 @@ export default function Findings() { [findings, reviewState], ) - // Collect unique targets and categories so we can build filter dropdowns. const uniqueTargets = useMemo(() => { const seen = new Set() for (const f of enrichedFindings) { @@ -149,7 +169,6 @@ export default function Findings() { return Array.from(seen).sort() }, [enrichedFindings]) - // plugin_id values serve as the "scanner/tool" filter per issue #43 const uniqueScanners = useMemo(() => { const seen = new Set() for (const f of enrichedFindings) { @@ -160,10 +179,6 @@ export default function Findings() { const filteredFindings = useMemo(() => { const query = searchQuery.trim().toLowerCase() - - // Compare dates using the *displayed* calendar day in the user's configured - // timezone, not raw UTC timestamps. This way a finding at 2026-05-13T20:00:00Z - // that shows as May 14 in IST correctly matches a From Date of 2026-05-14. const tz = getCurrentTimeZone() const dateFormatter = new Intl.DateTimeFormat('en-CA', { timeZone: tz }) @@ -172,11 +187,9 @@ export default function Findings() { const matchesTarget = filterTarget === 'all' || finding.target === filterTarget const matchesScanner = filterScanner === 'all' || finding.plugin_id === filterScanner - // Date range check — derive the calendar day in the display timezone if (dateFrom || dateTo) { const parsed = parseDateSafe(finding.discovered_at) if (!parsed) return false - // en-CA locale gives us YYYY-MM-DD which matches the value const displayDay = dateFormatter.format(parsed) if (dateFrom && displayDay < dateFrom) return false if (dateTo && displayDay > dateTo) return false @@ -214,12 +227,9 @@ export default function Findings() { return da - db }) case 'target': - return items.sort((a, b) => - (a.target || '').localeCompare(b.target || '') - ) + return items.sort((a, b) => (a.target || '').localeCompare(b.target || '')) case 'severity': default: - // Keep the original severity-group ordering; groupedFindings handles it. return items } }, [filteredFindings, sortMode]) @@ -266,7 +276,6 @@ export default function Findings() { [enrichedFindings, filteredFindings, countsBySeverity], ) - // Derives a flat list of active filter chips from non-default filter state. const activeFilters = useMemo(() => { const chips: { key: string; label: string }[] = [] if (searchQuery.trim()) chips.push({ key: 'search', label: `Search: "${searchQuery.trim()}"` }) @@ -278,7 +287,6 @@ export default function Findings() { return chips }, [searchQuery, filterTarget, filterScanner, sortMode, dateFrom, dateTo]) - function resetAllFilters() { setFilterSeverity('all') setFilterTarget('all') @@ -382,363 +390,373 @@ export default function Findings() { return (
-
-
-
- Triage Workspace v5.1 -
-
-
-

- Findings Desk -

-

- Active triage feed // {triageMetrics.total} total signals // {triageMetrics.unresolved} awaiting analyst action -

-
+
+ + {/* Dropdown Toggle Controller Bar */} +
+ +
-
- {[ - { label: 'Visible', value: triageMetrics.visible, tone: 'text-silver-bright' }, - { label: 'Critical + High', value: triageMetrics.active, tone: 'text-rag-red' }, - { label: 'Unresolved', value: triageMetrics.unresolved, tone: 'text-rag-amber' }, - { label: 'Reviewed', value: enrichedFindings.filter((finding) => finding.status === 'reviewed').length, tone: 'text-rag-green' }, - ].map((metric) => ( -
-

{metric.label}

-

{String(metric.value).padStart(2, '0')}

-
- ))} -
-
-
- -
-
-
-
- - -
- setSearchQuery(event.target.value)} - placeholder="Title, target, CVE, remediation..." - className={`${filterControlClass} px-4 pr-12 placeholder:text-silver/20`} - /> - - {searchQuery.trim() && ( - - )} -
-
- -
- - {severityOrder.map((severity) => ( - - ))} + {/* Dynamic Workspace Container */} +
+ + {showHistory && ( + + )} + + {/* Right Workspace Grid Content: Scales to 100% full-width when drawer closes */} +
+
+
+ Triage Workspace v5.1
-
+
+
+

+ Findings Desk +

+

+ Active triage feed // {triageMetrics.total} total signals // {triageMetrics.unresolved} awaiting analyst action +

+
-
-
-
- - +
+ {[ + { label: 'Visible', value: triageMetrics.visible, tone: 'text-silver-bright' }, + { label: 'Critical + High', value: triageMetrics.active, tone: 'text-rag-red' }, + { label: 'Unresolved', value: triageMetrics.unresolved, tone: 'text-rag-amber' }, + { label: 'Reviewed', value: enrichedFindings.filter((finding) => finding.status === 'reviewed').length, tone: 'text-rag-green' }, + ].map((metric) => ( +
+

{metric.label}

+

{String(metric.value).padStart(2, '0')}

+
+ ))}
+
+ + +
+
+
+
+ +
+ setSearchQuery(event.target.value)} + placeholder="Title, target, CVE, remediation..." + className={`${filterControlClass} px-4 pr-12 placeholder:text-silver/20`} + /> + {searchQuery.trim() && ( + + )} +
+
-
- - +
-
- - -
+
+
+
+ + +
-
- - setDateFrom(e.target.value)} - className={`${filterControlClass} [color-scheme:dark]`} - /> -
+
+ + +
-
- - setDateTo(e.target.value)} - className={`${filterControlClass} [color-scheme:dark]`} - /> -
-
+
+ + +
- -
-
-
- - {/* ── Active filter summary strip ──────────────────────────────────────── - Hidden when all filters are at their default values. */} - {activeFilters.length > 0 && ( -
- - Active Filters - - {activeFilters.map(({ key, label }) => ( - - {label} - - ))} -
- )} +
+ + setDateFrom(e.target.value)} + className={`${filterControlClass} [color-scheme:dark]`} + /> +
+ +
+ + setDateTo(e.target.value)} + className={`${filterControlClass} [color-scheme:dark]`} + /> +
+
-
- - {loading ? ( -
-

Synchronizing findings feed...

+ +
- ) : filteredFindings.length === 0 ? ( -
-

No Findings Match

-

Adjust filters to reopen the queue.

+
+ + {activeFilters.length > 0 && ( +
+ + Active Filters + + {activeFilters.map(({ key, label }) => ( + + {label} + + ))}
- ) : sortMode === 'severity' ? ( - groupedFindings.map(({ severity, items }) => { - if (items.length === 0) return null + )} - const config = severityConfig[severity] +
+ + {loading ? ( +
+

Synchronizing findings feed...

+
+ ) : filteredFindings.length === 0 ? ( +
+

No Findings Match

+

Adjust filters to reopen the queue.

+
+ ) : sortMode === 'severity' ? ( + groupedFindings.map(({ severity, items }) => { + if (items.length === 0) return null + const config = severityConfig[severity] + + return ( +
+
+
+ +
+

{config.label}

+

{items.length} visible in queue

+
+
+
- return ( -
+
+ {items.map((finding) => renderFindingRow(finding))} +
+
+ ) + }) + ) : ( +
- +
-

{config.label}

-

{items.length} visible in queue

+

+ {sortMode === 'newest' ? 'Newest First' : sortMode === 'oldest' ? 'Oldest First' : 'By Target'} +

+

{sortedFindings.length} visible in queue

-
- {items.map((finding) => renderFindingRow(finding))} + {sortedFindings.map((finding) => renderFindingRow(finding))}
- ) - }) - ) : ( -
-
-
- -
-

- {sortMode === 'newest' ? 'Newest First' : sortMode === 'oldest' ? 'Oldest First' : 'By Target'} -

-

{sortedFindings.length} visible in queue

-
-
-
-
- {sortedFindings.map((finding) => renderFindingRow(finding))} -
-
- )} - - - -
- {selectedFinding ? ( -
-
-
- - {severityConfig[selectedFinding.severity].label} - - - {selectedFinding.status} - - {selectedFinding.cve ? ( - - {selectedFinding.cve} - - ) : null} -
+ )} + + + +
+ {selectedFinding ? ( +
+
+
+ + {severityConfig[selectedFinding.severity].label} + + + {selectedFinding.status} + + {selectedFinding.cve ? ( + + {selectedFinding.cve} + + ) : null} +
-
-

Selected Finding

-

{selectedFinding.title}

-
+
+

Selected Finding

+

{selectedFinding.title}

+
-
-
-

Target

-

{selectedFinding.target || 'Unknown'}

-
-
-

Category

-

{selectedFinding.category || 'Uncategorized'}

-
-
-

Observed

-

- {formatLocaleDate(selectedFinding.discovered_at)} -

-
-
-

Severity Score

-

- {typeof selectedFinding.cvss === 'number' ? selectedFinding.cvss.toFixed(1) : 'N/A'} -

+
+
+

Target

+

{selectedFinding.target || 'Unknown'}

+
+
+

Category

+

{selectedFinding.category || 'Uncategorized'}

+
+
+

Observed

+

+ {formatLocaleDate(selectedFinding.discovered_at)} +

+
+
+

Severity Score

+

+ {typeof selectedFinding.cvss === 'number' ? selectedFinding.cvss.toFixed(1) : 'N/A'} +

+
+
-
-
-
-
-

Evidence Brief

-
-

{selectedFinding.description || 'No description provided.'}

+
+
+

Evidence Brief

+
+

{selectedFinding.description || 'No description provided.'}

+
+
+ +
+

Remediation

+
+

+ {selectedFinding.remediation || 'No remediation guidance captured.'} +

+
+
-
-
-

Remediation

-
-

- {selectedFinding.remediation || 'No remediation guidance captured.'} -

+
+

Workflow Actions

+
+ + + + +
-
- -
-

Workflow Actions

-
- - - - + ) : ( +
+

Queue Clear

+

+ Select a finding to review evidence and remediation. +

-
-
- ) : ( -
-

Queue Clear

-

- Select a finding to review evidence and remediation. -

+ )}
- )} +
- -
+ +
+
) -} +} \ No newline at end of file From 38018287f041ae0d2ff392f9d2c05cd1e66a6b01 Mon Sep 17 00:00:00 2001 From: Sanjana S Aithal Date: Tue, 2 Jun 2026 15:51:15 +0530 Subject: [PATCH 2/2] fix: resolve backend json parsing imports and clean up formatting hygiene --- backend/routes/history.py | 20 +++++++++++++------- frontend/src/components/ScanHistory.tsx | 10 ++++------ frontend/src/pages/Findings.tsx | 12 ++++++------ 3 files changed, 23 insertions(+), 19 deletions(-) diff --git a/backend/routes/history.py b/backend/routes/history.py index 29814045..fc45afce 100644 --- a/backend/routes/history.py +++ b/backend/routes/history.py @@ -1,24 +1,30 @@ import os import json from pathlib import Path -from fastapi import APIRouter, HTTPException +from fastapi import APIRouter, HTTPException +router = APIRouter(prefix="/api/history", tags=["history"]) -router = APIRouter(prefix="/api/history", tags=["History"]) +OUTPUT_DIR = Path("backend/output") @router.get("") def list_scans(): - """Scan history index — reads all logs in output directory and summarizes them""" + """Scan history index - reads all logs in output directory and summarizes them""" + + if not OUTPUT_DIR.exists(): + return [] + + scans = [] + return scans @router.get("/{scan_id}") def get_scan(scan_id: str): - """Scan details engine — returns the complete structural payload of a specific file""" + """Scan details engine - returns the complete structural payload of a specific file""" path = OUTPUT_DIR / f"{scan_id}.json" - + if not path.exists(): - raise HTTPException(status_code=404, detail="Scan record not found") - + raise HTTPException(status_code=404, detail="Requested session log packet not found") try: with open(path, "r") as fp: return json.load(fp) diff --git a/frontend/src/components/ScanHistory.tsx b/frontend/src/components/ScanHistory.tsx index 6e0456e3..9908a83b 100644 --- a/frontend/src/components/ScanHistory.tsx +++ b/frontend/src/components/ScanHistory.tsx @@ -40,7 +40,6 @@ export default function ScanHistory({ onSelect, activeScanId }: Props) { }); }, []); - // Sleek cyber-themed loading skeleton to match your design system if (loading) { return (
@@ -57,10 +56,9 @@ export default function ScanHistory({ onSelect, activeScanId }: Props) { // Session Index
- +
{history.length === 0 ? ( - // Boxed fallback tile using your layout's exact background classes

No archived logs

@@ -89,12 +87,12 @@ export default function ScanHistory({ onSelect, activeScanId }: Props) { > {/* Visual marker bar matching your findings rows */} - +
{scan.target}
- +
{formattedDate}
@@ -105,7 +103,7 @@ export default function ScanHistory({ onSelect, activeScanId }: Props) { }`}> {scan.finding_count} hits - + {scan.severity_summary.critical > 0 && ( CRIT diff --git a/frontend/src/pages/Findings.tsx b/frontend/src/pages/Findings.tsx index aa62f8fc..9324df9c 100644 --- a/frontend/src/pages/Findings.tsx +++ b/frontend/src/pages/Findings.tsx @@ -430,7 +430,7 @@ export default function Findings() { return (
- + {/* Dropdown Toggle Controller Bar */}
-
+
+
)