85 lines
4.4 KiB
TypeScript
85 lines
4.4 KiB
TypeScript
"use client";
|
|
|
|
import React from "react";
|
|
import { ShieldCheck, ShieldAlert, Bug, TestTube2, Lock } from "lucide-react";
|
|
import { Card, CardBody, CardHeader, CardTitle } from "@/components/ui/Card";
|
|
import { Badge } from "@/components/ui/Badge";
|
|
import { EmptyState } from "@/components/ui/EmptyState";
|
|
import { MetricCard } from "@/components/ui/MetricCard";
|
|
import { ProgressRing } from "@/components/ui/ProgressRing";
|
|
|
|
const FINDINGS = [
|
|
{ sev: "critical", title: "Hardcoded secret in asf-core/config.py#L18", rule: "SECRET-LEAK-001", file: "services/asf-core/app/config.py" },
|
|
{ sev: "high", title: "SQL injection vector in raw query", rule: "SQLI-014", file: "services/asf-core/app/repositories/artifact_repo.py" },
|
|
{ sev: "medium", title: "CORS permissive in dev-trust mode", rule: "CORS-002", file: "services/asf-core/app/main.py" },
|
|
{ sev: "medium", title: "Missing rate limit on /ask-asf", rule: "RATE-010", file: "services/asf-core/app/api/v1/ask_asf.py" },
|
|
{ sev: "low", title: "Insecure cookie flags hint", rule: "COOKIE-004", file: "services/asf-core/app/dependencies.py" },
|
|
];
|
|
|
|
const SEV_TONES: Record<string, "red" | "amber" | "neutral"> = {
|
|
critical: "red", high: "red", medium: "amber", low: "neutral",
|
|
};
|
|
|
|
export default function QualityPage() {
|
|
return (
|
|
<div className="p-6 lg:p-8 max-w-[1400px] mx-auto">
|
|
<header className="mb-6 flex flex-wrap items-start justify-between gap-3">
|
|
<div>
|
|
<h1 className="text-2xl font-semibold tracking-tight text-slate-900 flex items-center gap-2">
|
|
<ShieldCheck className="w-6 h-6 text-emerald-600" />
|
|
Quality & Security
|
|
</h1>
|
|
<p className="mt-1 text-sm text-slate-500">
|
|
Static analysis, test coverage, security findings, build health — consolidated from every ASF connector.
|
|
</p>
|
|
</div>
|
|
<Badge tone="neutral">SonarQube · Semgrep connectors</Badge>
|
|
</header>
|
|
|
|
<section className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
|
|
<MetricCard label="Critical findings" value={1} tone="red" icon={<ShieldAlert className="w-5 h-5" />} />
|
|
<MetricCard label="Tests passing" value={498} tone="green" icon={<TestTube2 className="w-5 h-5" />} subtitle="of 512 total" delta={0.6} />
|
|
<MetricCard label="Coverage" value={68.9} isPercentage tone="brand" icon={<Bug className="w-5 h-5" />} />
|
|
<MetricCard label="Reliability score" value="A-" tone="green" icon={<Lock className="w-5 h-5" />} />
|
|
</section>
|
|
|
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4 mb-6">
|
|
<Card>
|
|
<CardHeader><CardTitle>Scores</CardTitle></CardHeader>
|
|
<CardBody className="space-y-4">
|
|
<ProgressRing value={72} tone="green" label="Security" sublabel="Findings + hardening" />
|
|
<ProgressRing value={68.9} tone="brand" label="Coverage" sublabel="Statement coverage" />
|
|
<ProgressRing value={81} tone="amber" label="Maintainability" sublabel="Duplication + complexity" />
|
|
</CardBody>
|
|
</Card>
|
|
<Card className="lg:col-span-2">
|
|
<CardHeader>
|
|
<CardTitle>Security findings</CardTitle>
|
|
<Badge tone="red">{FINDINGS.filter((f) => f.sev === "critical" || f.sev === "high").length} critical/high</Badge>
|
|
</CardHeader>
|
|
<CardBody className="p-0">
|
|
{FINDINGS.length === 0 ? (
|
|
<div className="p-6"><EmptyState title="No findings" description="Your first scan will populate this panel." icon={ShieldCheck} /></div>
|
|
) : (
|
|
<ul className="divide-y divide-slate-100">
|
|
{FINDINGS.map((f, i) => (
|
|
<li key={i} className="p-3 hover:bg-slate-50 flex items-start gap-3">
|
|
<Badge tone={SEV_TONES[f.sev]} className="uppercase mt-0.5">{f.sev}</Badge>
|
|
<div className="min-w-0 flex-1">
|
|
<div className="text-sm font-medium text-slate-800">{f.title}</div>
|
|
<div className="mt-1 text-xs text-slate-500 flex flex-wrap items-center gap-3">
|
|
<span className="asf-monospace">{f.rule}</span>
|
|
<span className="asf-monospace truncate">{f.file}</span>
|
|
</div>
|
|
</div>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
)}
|
|
</CardBody>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|