ewdlop's picture
App.tsx
42ae0b9
import { Shield, ShieldAlert, ShieldCheck, ShieldX, Info } from "lucide-react";
import type { InjectionResult, InjectionPattern } from "@/lib/api";
interface InjectionPanelProps {
result: InjectionResult | null;
onJumpToLine?: (line: number) => void;
}
const RISK_CONFIG = {
critical: {
label: "CRITICAL",
color: "oklch(0.60 0.20 25)",
bg: "oklch(0.60 0.20 25 / 0.12)",
border: "oklch(0.60 0.20 25 / 0.35)",
icon: ShieldX,
},
high: {
label: "HIGH",
color: "oklch(0.72 0.18 55)",
bg: "oklch(0.72 0.18 55 / 0.12)",
border: "oklch(0.72 0.18 55 / 0.35)",
icon: ShieldAlert,
},
medium: {
label: "MEDIUM",
color: "oklch(0.78 0.18 90)",
bg: "oklch(0.78 0.18 90 / 0.12)",
border: "oklch(0.78 0.18 90 / 0.35)",
icon: ShieldAlert,
},
low: {
label: "LOW",
color: "oklch(0.68 0.16 210)",
bg: "oklch(0.68 0.16 210 / 0.12)",
border: "oklch(0.68 0.16 210 / 0.35)",
icon: Shield,
},
};
function RiskMeter({ score }: { score: number }) {
const color =
score >= 75 ? "oklch(0.60 0.20 25)" :
score >= 50 ? "oklch(0.72 0.18 55)" :
score >= 25 ? "oklch(0.78 0.18 90)" :
"oklch(0.72 0.17 160)";
return (
<div className="flex items-center gap-3">
<div className="flex-1 h-1.5 rounded-full bg-[oklch(0.20_0.010_264)] overflow-hidden">
<div
className="h-full rounded-full transition-all duration-500"
style={{ width: `${score}%`, background: color }}
/>
</div>
<span className="text-sm font-mono font-semibold w-12 text-right" style={{ color }}>
{score}/100
</span>
</div>
);
}
function PatternCard({ pattern, onJumpToLine }: { pattern: InjectionPattern; onJumpToLine?: (line: number) => void }) {
const cfg = RISK_CONFIG[pattern.risk_level] ?? RISK_CONFIG.low;
const Icon = cfg.icon;
return (
<div
className="mx-3 mb-2 rounded-lg border overflow-hidden"
style={{ borderColor: cfg.border, background: cfg.bg }}
>
{/* Header */}
<div className="flex items-center gap-2 px-3 py-2 border-b" style={{ borderColor: cfg.border }}>
<Icon size={14} style={{ color: cfg.color }} className="flex-shrink-0" />
<span className="text-xs font-semibold flex-1" style={{ color: cfg.color }}>
{pattern.description}
</span>
<span
className="text-[10px] font-bold px-1.5 py-0.5 rounded"
style={{ background: cfg.bg, color: cfg.color, border: `1px solid ${cfg.border}` }}
>
{cfg.label}
</span>
</div>
{/* Body */}
<div className="px-3 py-2 space-y-2">
{/* Category */}
<div className="flex items-center gap-2">
<span className="text-[10px] text-[oklch(0.45_0.010_264)]">Category:</span>
<span className="text-[10px] font-mono font-medium" style={{ color: cfg.color }}>
{pattern.category}
</span>
{pattern.line_no && (
<button
className="text-[10px] font-mono ml-auto text-[oklch(0.45_0.010_264)] hover:text-[oklch(0.68_0.16_210)] transition-colors"
onClick={() => pattern.line_no && onJumpToLine?.(pattern.line_no)}
>
L{pattern.line_no}:{pattern.line_pos}
</button>
)}
</div>
{/* Detail */}
<p className="text-xs text-[oklch(0.72_0.010_264)] leading-relaxed">{pattern.detail}</p>
{/* Offending token */}
{pattern.offending_token && (
<div className="rounded px-2 py-1.5" style={{ background: "oklch(0.10 0.008 264)" }}>
<p className="text-[10px] text-[oklch(0.45_0.010_264)] mb-1">Offending token:</p>
<code className="text-xs font-mono break-all" style={{ color: cfg.color }}>
{pattern.offending_token}
</code>
</div>
)}
{/* Recommendation */}
<div className="flex items-start gap-1.5 pt-1">
<Info size={10} className="flex-shrink-0 mt-0.5 text-[oklch(0.45_0.010_264)]" />
<p className="text-[10px] text-[oklch(0.55_0.010_264)] leading-relaxed">{pattern.recommendation}</p>
</div>
</div>
</div>
);
}
export function InjectionPanel({ result, onJumpToLine }: InjectionPanelProps) {
if (!result) {
return (
<div className="flex flex-col items-center justify-center h-full gap-3 text-[oklch(0.45_0.010_264)]">
<Shield size={32} className="opacity-30" />
<p className="text-sm">Run analysis to check for injection risks</p>
</div>
);
}
const { safe, risk_score, patterns, summary } = result;
const summaryColor =
risk_score >= 75 ? "oklch(0.60 0.20 25)" :
risk_score >= 50 ? "oklch(0.72 0.18 55)" :
risk_score >= 25 ? "oklch(0.78 0.18 90)" :
"oklch(0.72 0.17 160)";
const SummaryIcon = safe ? ShieldCheck : risk_score >= 75 ? ShieldX : ShieldAlert;
return (
<div className="flex flex-col h-full">
{/* Summary header */}
<div className="px-3 py-3 border-b border-[oklch(0.22_0.010_264)] flex-shrink-0 space-y-2">
<div className="flex items-center gap-2">
<SummaryIcon size={16} style={{ color: summaryColor }} />
<span className="text-xs font-semibold" style={{ color: summaryColor }}>
{safe ? "No injection patterns detected" : `${patterns.length} pattern${patterns.length !== 1 ? "s" : ""} detected`}
</span>
</div>
<RiskMeter score={risk_score} />
<p className="text-xs text-[oklch(0.60_0.010_264)] leading-relaxed">{summary}</p>
</div>
{/* Pattern cards */}
{safe ? (
<div className="flex flex-col items-center justify-center flex-1 gap-3">
<ShieldCheck size={40} style={{ color: "oklch(0.72 0.17 160)" }} className="opacity-60" />
<p className="text-sm font-medium" style={{ color: "oklch(0.72 0.17 160)" }}>SQL appears safe</p>
<p className="text-xs text-[oklch(0.45_0.010_264)]">No known injection patterns were found</p>
</div>
) : (
<div className="flex-1 overflow-auto pt-2">
{patterns.map((p) => (
<PatternCard key={p.pattern_id} pattern={p} onJumpToLine={onJumpToLine} />
))}
</div>
)}
</div>
);
}