Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
143 changes: 53 additions & 90 deletions frontend/app/components/SanctityScore.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,45 +2,22 @@

import { useMemo } from "react";
import type { Finding } from "../types";
import {
calculateScore,
getScoreGrade,
getScoreNarrative,
getSeverityColor,
} from "../lib/report-export";

interface SanctityScoreProps {
findings: Finding[];
}

const SEVERITY_WEIGHTS: Record<string, number> = {
critical: 15,
high: 10,
medium: 5,
low: 2,
};

function calculateScore(findings: Finding[]): number {
let score = 100;
for (const f of findings) {
score -= SEVERITY_WEIGHTS[f.severity] ?? 0;
}
return Math.max(0, Math.min(100, score));
}

function getGrade(score: number): string {
if (score >= 90) return "A";
if (score >= 80) return "B";
if (score >= 65) return "C";
if (score >= 50) return "D";
return "F";
}

function getColor(score: number): string {
if (score >= 76) return "#22c55e";
if (score >= 61) return "#f59e0b";
if (score >= 41) return "#f97316";
return "#ef4444";
}

export function SanctityScore({ findings }: SanctityScoreProps) {
const score = useMemo(() => calculateScore(findings), [findings]);
const grade = getGrade(score);
const color = getColor(score);
const grade = getScoreGrade(score);
const color = getSeverityColor(score);
const narrative = getScoreNarrative(score);

const radius = 70;
const strokeWidth = 12;
Expand All @@ -53,67 +30,53 @@ export function SanctityScore({ findings }: SanctityScoreProps) {
Sanctity Score
</h3>
<div className="flex items-center justify-center">
<svg
viewBox="0 0 180 110"
className="w-full h-auto max-w-[180px]"
role="img"
aria-label={`Sanctity score: ${score} out of 100. Grade: ${grade}. ${
score >= 76
? "Good security posture"
: score >= 50
? "Moderate risk — review findings"
: "High risk — immediate attention needed"
}`}
>
<title>Sanctity Score: {score}/100, Grade {grade}</title>
{/* Background arc */}
<path
d={`M ${90 - radius} 95 A ${radius} ${radius} 0 0 1 ${90 + radius} 95`}
fill="none"
stroke="currentColor"
strokeWidth={strokeWidth}
className="text-zinc-200 dark:text-zinc-700"
strokeLinecap="round"
/>
{/* Progress arc */}
<path
d={`M ${90 - radius} 95 A ${radius} ${radius} 0 0 1 ${90 + radius} 95`}
fill="none"
stroke={color}
strokeWidth={strokeWidth}
strokeLinecap="round"
strokeDasharray={`${progress} ${circumference}`}
/>
{/* Score text */}
<text
x="90"
y="75"
textAnchor="middle"
className="fill-zinc-900 dark:fill-zinc-100"
fontSize="28"
fontWeight="bold"
>
{score}
</text>
{/* Grade label */}
<text
x="90"
y="95"
textAnchor="middle"
fontSize="14"
fontWeight="600"
fill={color}
<svg
viewBox="0 0 180 110"
className="w-full h-auto max-w-[180px]"
role="img"
aria-label={`Sanctity score: ${score} out of 100. Grade: ${grade}. ${narrative}`}
>
Grade: {grade}
</text>
</svg>
<title>Sanctity Score: {score}/100, Grade {grade}</title>
<path
d={`M ${90 - radius} 95 A ${radius} ${radius} 0 0 1 ${90 + radius} 95`}
fill="none"
stroke="currentColor"
strokeWidth={strokeWidth}
className="text-zinc-200 dark:text-zinc-700"
strokeLinecap="round"
/>
<path
d={`M ${90 - radius} 95 A ${radius} ${radius} 0 0 1 ${90 + radius} 95`}
fill="none"
stroke={color}
strokeWidth={strokeWidth}
strokeLinecap="round"
strokeDasharray={`${progress} ${circumference}`}
/>
<text
x="90"
y="75"
textAnchor="middle"
className="fill-zinc-900 dark:fill-zinc-100"
fontSize="28"
fontWeight="bold"
>
{score}
</text>
<text
x="90"
y="95"
textAnchor="middle"
fontSize="14"
fontWeight="600"
fill={color}
>
Grade: {grade}
</text>
</svg>
</div>
<p className="text-center text-xs text-zinc-500 dark:text-zinc-400 mt-2">
{score >= 76
? "Good security posture"
: score >= 50
? "Moderate risk — review findings"
: "High risk — immediate attention needed"}
{narrative}
</p>
</div>
);
Expand Down
30 changes: 26 additions & 4 deletions frontend/app/dashboard/page.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
"use client";

import { useState, useCallback, useTransition, useMemo } from "react";
import { useState, useCallback, useTransition } from "react";
import dynamic from "next/dynamic";
import type { CallGraphNode, CallGraphEdge, Finding, Severity } from "../types";
import type {
AnalysisReport,
CallGraphNode,
CallGraphEdge,
Finding,
Severity,
} from "../types";
import { transformReport, extractCallGraph, normalizeReport } from "../lib/transform";
import { exportToPdf } from "../lib/export-pdf";
import { exportToCsv } from "../lib/export-csv";
import { SeverityFilter } from "../components/SeverityFilter";
import { FindingsList } from "../components/FindingsList";
import { SummaryChart } from "../components/SummaryChart";
Expand Down Expand Up @@ -49,6 +56,7 @@ function extractErrorMessage(payload: unknown, fallback: string): string {

export default function DashboardPage() {
const [findings, setFindings] = useState<Finding[]>([]);
const [report, setReport] = useState<AnalysisReport | null>(null);
const [callGraphNodes, setCallGraphNodes] = useState<CallGraphNode[]>([]);
const [callGraphEdges, setCallGraphEdges] = useState<CallGraphEdge[]>([]);
const [severityFilter, setSeverityFilter] = useState<Severity | "all">("all");
Expand All @@ -65,6 +73,7 @@ export default function DashboardPage() {
const transformed = transformReport(report);
const graph = extractCallGraph(report);

setReport(report);
setFindings(transformed);
setCallGraphNodes(graph.nodes);
setCallGraphEdges(graph.edges);
Expand All @@ -78,6 +87,7 @@ export default function DashboardPage() {
applyReport(JSON.parse(text || SAMPLE_JSON));
} catch (e) {
setError(e instanceof Error ? e.message : "Invalid JSON");
setReport(null);
setFindings([]);
setCallGraphNodes([]);
setCallGraphEdges([]);
Expand Down Expand Up @@ -162,6 +172,9 @@ export default function DashboardPage() {
<p className="text-sm text-zinc-600 dark:text-zinc-400 theme-high-contrast:text-white mb-4">
Paste JSON from <code className="bg-zinc-100 dark:bg-zinc-800 theme-high-contrast:bg-zinc-900 px-1 rounded">sanctifier analyze --format json</code>, upload an existing report, or analyze a Rust contract source file.
</p>
<div className="mb-4 rounded-2xl border border-emerald-200/70 bg-emerald-50/80 px-4 py-3 text-sm text-emerald-900 shadow-sm dark:border-emerald-900/60 dark:bg-emerald-950/40 dark:text-emerald-100">
Audit exports include a compliance-style PDF with Sanctity Score and methodology, plus a CSV findings register for remediation and governance workflows.
</div>
<div className="flex flex-wrap gap-2 sm:gap-4">
<label className="flex-1 sm:flex-none text-center cursor-pointer rounded-lg border border-zinc-300 dark:border-zinc-600 theme-high-contrast:border-white px-4 py-2 text-sm hover:bg-zinc-100 dark:hover:bg-zinc-800 theme-high-contrast:hover:bg-zinc-900 focus-within:outline-none focus-within:ring-2 focus-within:ring-zinc-400 focus-within:ring-offset-2">
Upload JSON
Expand Down Expand Up @@ -193,12 +206,21 @@ export default function DashboardPage() {
</button>
<button
onClick={() => {
exportToPdf(findings);
exportToPdf(findings, report);
}}
disabled={!hasData}
className="flex-1 sm:flex-none rounded-lg border border-zinc-300 dark:border-zinc-600 theme-high-contrast:border-white px-4 py-2 text-sm disabled:opacity-50 hover:bg-zinc-100 dark:hover:bg-zinc-800 theme-high-contrast:hover:bg-zinc-900 focus:outline-none focus-visible:ring-2 focus-visible:ring-zinc-400 focus-visible:ring-offset-2 disabled:focus-visible:ring-0"
>
Export Audit PDF
</button>
<button
onClick={() => {
exportToCsv(findings, report);
}}
disabled={!hasData}
className="flex-1 sm:flex-none rounded-lg border border-zinc-300 dark:border-zinc-600 theme-high-contrast:border-white px-4 py-2 text-sm disabled:opacity-50 hover:bg-zinc-100 dark:hover:bg-zinc-800 theme-high-contrast:hover:bg-zinc-900 focus:outline-none focus-visible:ring-2 focus-visible:ring-zinc-400 focus-visible:ring-offset-2 disabled:focus-visible:ring-0"
>
Export PDF
Export CSV
</button>
</div>
{uploadStatus && (
Expand Down
19 changes: 19 additions & 0 deletions frontend/app/lib/export-csv.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { AnalysisReport, Finding } from "../types";
import { buildCsvContent } from "./report-export";

export function exportToCsv(
findings: Finding[],
report: AnalysisReport | null,
title = "Sanctifier Compliance Audit Report"
): void {
const csv = buildCsvContent(findings, report, title);
const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" });
const url = URL.createObjectURL(blob);
const link = document.createElement("a");

link.href = url;
link.download = "sanctifier-report.csv";
link.click();

URL.revokeObjectURL(url);
}
Loading