| 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> |
| ); |
| } |
|
|