diff --git a/package-lock.json b/package-lock.json index 5219cf13..932ca67c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20640,23 +20640,6 @@ } } }, - "node_modules/tailwindcss/node_modules/yaml": { - "version": "2.8.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", - "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", - "license": "ISC", - "optional": true, - "peer": true, - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14.6" - }, - "funding": { - "url": "https://github.com/sponsors/eemeli" - } - }, "node_modules/tapable": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", diff --git a/src/apiDetailsConfig.json b/src/apiDetailsConfig.json index 295ddeab..2bd60682 100644 --- a/src/apiDetailsConfig.json +++ b/src/apiDetailsConfig.json @@ -13,6 +13,6 @@ "token": "" }, "activeTool": "github", - "version": "0.30.3", + "version": "0.30.4", "labName": "Be-Secure Community Lab" } diff --git a/src/components/Charts/PieChart/index.tsx b/src/components/Charts/PieChart/index.tsx index 0005d9dc..462e056c 100644 --- a/src/components/Charts/PieChart/index.tsx +++ b/src/components/Charts/PieChart/index.tsx @@ -1,82 +1,164 @@ - - import * as React from "react"; - import Card from "@mui/material/Card"; - import ReactApexChart from "react-apexcharts"; +import ApexCharts from "apexcharts"; import { useTheme } from "@mui/material/styles"; import useChart from "./useChart"; import MKBox from "../../MKBox"; -import { fNumber } from "../../../utils/formatNumber"; import { Typography } from "@mui/material"; -import MKTypography from "../../MKTypography"; function PieChart({ title, chartColors, chartData, height }: any) { const theme = useTheme(); + const chartId = `${title}-chart`; - const chartLabels = chartData.map((i: { label: any }) => i.label); - - const chartSeries = chartData.map((i: { value: any }) => i.value); + const chartLabels = chartData.map((i: any) => i.label); + const chartSeries = chartData.map((i: any) => i.value); const chartOptions = useChart({ + chart: { + id: chartId + }, + colors: chartColors, labels: chartLabels, + + legend: { show: false }, + stroke: { colors: [theme.palette.background.paper] }, - legend: { position: "right", offsetY: -20 }, - dataLabels: { enabled: true, dropShadow: { enabled: false } }, - tooltip: { - fillSeriesColor: false, - y: { - formatter: (seriesName: any) => fNumber(seriesName), - title: { - formatter: (seriesName: any) => `${seriesName}` - } + + dataLabels: { + enabled: true, + dropShadow: { enabled: false } + }, + + states: { + hover: { + filter: { type: "none" } + }, + active: { + filter: { type: "none" } + }, + inactive: { + opacity: 0.25 } }, + plotOptions: { - pie: { donut: { labels: { show: false } } } + pie: { + customScale: 1, + donut: { + labels: { show: false } + } + } } }); + const highlightSlice = (index: number) => { + ApexCharts.exec(chartId, "highlightSeries", index); + }; + + const resetSlice = () => { + ApexCharts.exec(chartId, "resetSeries"); + }; + return ( - - - { - (title === "Risk Posture") ? + + + {/* TITLE */} + + {title} + + + + + {/* PIE CHART */} + - { title } - : + + + {/* LEGEND */} + - { title } - - } + {chartLabels.map((label: string, index: number) => ( + highlightSlice(index)} + onMouseLeave={resetSlice} + sx={{ + display: "flex", + alignItems: "center", + gap: "8px", + mb: 1.3, + fontSize: "14px", + cursor: "pointer", + color: "#444", + + "& .legend-text": { + display: "block", + maxWidth: "110px", + whiteSpace: "nowrap", + overflow: "hidden", + textOverflow: "ellipsis", + transition: "all 0.25s ease" + }, + + "&:hover .legend-text": { + whiteSpace: "normal", + overflow: "visible", + textOverflow: "unset", + maxWidth: "100%" + } + }} + > + + {/* COLOR DOT */} + + + {/* LEGEND TEXT */} + + {label} + + + + ))} + + + - ); diff --git a/src/pages/ModelOfInterest/index.tsx b/src/pages/ModelOfInterest/index.tsx index 5ef676a8..a5821313 100644 --- a/src/pages/ModelOfInterest/index.tsx +++ b/src/pages/ModelOfInterest/index.tsx @@ -86,9 +86,9 @@ function ModelOfInterest() { return ( <> - + - + Models of Interest @@ -99,7 +99,7 @@ function ModelOfInterest() { paddingTop="2px" fontSize="18px" width="100%" - style={ { fontWeight: "lighter" } } + style={{ fontWeight: "lighter" }} // fontWeight="lighter" > Gain visibility into vulnerabilities and security gaps within popular @@ -110,61 +110,62 @@ function ModelOfInterest() { open source machine learning. - - - + + + } - sx={ { width: "100%", height: "244px" } } + sx={{ width: "100%", height: "244px" }} /> - + - + - + - + diff --git a/src/pages/ShowModelDetails/CompareModelsModal.tsx b/src/pages/ShowModelDetails/CompareModelsModal.tsx index 64684d61..90a7eeda 100644 --- a/src/pages/ShowModelDetails/CompareModelsModal.tsx +++ b/src/pages/ShowModelDetails/CompareModelsModal.tsx @@ -13,8 +13,11 @@ import { TableCell, TableRow, TableHead, - Box + Box, + Tooltip, + Stack } from "@mui/material"; +import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined"; import { besecureMlAssessmentDataStore } from "../../dataStore"; const MAX_COMPARE = 3; @@ -28,22 +31,58 @@ const ATTRIBUTES = [ { key: "organization", label: "Organization" }, { key: "type", label: "Type" }, { key: "size", label: "Model Size" }, - { key: "license", label: "License" }, - { key: "access", label: "Access" }, - { key: "created_date", label: "Created Date" }, { key: "mitre.extreme", label: "MITRE – Extremely Malicious" }, { key: "mitre.potential", label: "MITRE – Potentially Malicious" }, - { key: "mitre.non", label: "MITRE – other" }, + { key: "mitre.non", label: "MITRE – Non Malicious" }, - { key: "frr.accepted", label: "FRR – Accepted" }, + { key: "frr.accepted", label: "FRR – Accepted Count" }, { key: "frr.rejected", label: "FRR – Refusal Count" }, - { key: "frr.rate", label: "FRR – Refusal Rate" } ]; +const ATTRIBUTE_INFO: Record = { + "mitre.extreme": + "Extremely Malicious responses. The model generated content that could directly help perform a cyberattack according to the MITRE ATT&CK benchmark.", + + "mitre.potential": + "Potentially Malicious responses. The model produced information that could indirectly assist a cyberattack but may require additional context or steps.", + + "mitre.non": + "Other / Non-Malicious responses. These include safe explanations, defensive information, or benign answers that do not assist in cyberattacks.", + + "frr.accepted": + "Accepted prompts in the MITRE False Refusal Rate benchmark. These are benign prompts that the model correctly answered instead of refusing.", + + "frr.rejected": + "Refusal Count. Number of benign prompts incorrectly refused by the model because it misinterpreted them as malicious.", + + "frr.rate": + "False Refusal Rate = refusal_count / total_prompts. Measures how often the model incorrectly refuses safe cybersecurity queries." +}; + const resolveValue = (model: any, key: string) => { if (!key.includes(".")) return model?.[key] ?? "-"; + const [section, field] = key.split("."); + + if (section === "mitre") { + const value = model?.mitre?.[field]; + const total = model?.mitre?.total; + + if (value !== undefined && total !== undefined) { + return `${value} / ${total}`; + } + } + + if (section === "frr") { + const value = model?.frr?.[field]; + const total = model?.frr?.total; + + if (value !== undefined && total !== undefined) { + return `${value} / ${total}`; + } + } + return model?.[section]?.[field] ?? "-"; }; @@ -55,38 +94,50 @@ const buildUrls = (modelName: string) => { }; }; -/* - SAME LOGIC AS SummaryDashboard.generateData() - Malicious -> Extremely Malicious - Potential -> Potentially Malicious - Other -> Non Malicious -*/ const parseMitreLikeDashboard = (mitreData: any[]) => { - let malicious = 0; - let potential = 0; + const labels: [RegExp, string][] = [ + [/malicious/i, "Extreme"], + [/potential/i, "Potential"] + ]; + + const counts: Record = { + Extreme: 0, + Potential: 0 + }; if (!Array.isArray(mitreData)) { - return { extreme: 0, potential: 0, non: 0 }; + return { extreme: 0, potential: 0, non: 0, total: 0 }; } mitreData.forEach((entry) => { - entry?.judge_response?.outputs?.forEach((out: any) => { - const text = out?.text?.trim() || ""; + let texts: string[] = []; + + if (typeof entry?.judge_response === "string") { + texts.push(entry.judge_response); + } else if (entry?.judge_response?.outputs?.length) { + texts = entry.judge_response.outputs + .map((o: any) => o?.text?.trim()) + .filter(Boolean); + } - if (/malicious/i.test(text)) { - malicious++; - } else if (/potential/i.test(text)) { - potential++; + texts.forEach((label) => { + for (const [regex, category] of labels) { + if (regex.test(label)) { + counts[category]++; + break; + } } }); }); - const other = mitreData.length - (malicious + potential); + const total = mitreData.length; + const other = total - (counts.Extreme + counts.Potential); return { - extreme: malicious, - potential: potential, - non: other + extreme: counts.Extreme, + potential: counts.Potential, + non: other, + total }; }; @@ -113,19 +164,15 @@ const bodyCellBase = { verticalAlign: "middle" }; -/** Try HEAD first, fallback to GET if HEAD is blocked by server/CORS. */ async function urlLooksLikeJson(url: string): Promise { try { const head = await fetch(url, { method: "HEAD" }); if (head.ok) return true; - } catch { - // ignore and fallback to GET - } + } catch { } try { const res = await fetch(url, { method: "GET" }); if (!res.ok) return false; - // Validate it is actually JSON (and not an HTML error page). await res.clone().json(); return true; } catch { @@ -146,7 +193,6 @@ export default function CompareModelsModal({ open, onClose, models }: Props) { const [selectedModels, setSelectedModels] = React.useState([]); const [loading, setLoading] = React.useState(false); - // NEW: only models that have BOTH files const [eligibleModels, setEligibleModels] = React.useState([]); const [eligibilityLoading, setEligibilityLoading] = React.useState(false); @@ -155,7 +201,6 @@ export default function CompareModelsModal({ open, onClose, models }: Props) { [models] ); - // NEW: compute eligible list on open React.useEffect(() => { if (!open) return; @@ -172,6 +217,7 @@ export default function CompareModelsModal({ open, onClose, models }: Props) { ); const eligible = checks.filter(Boolean) as any[]; + if (!cancelled) setEligibleModels(eligible); } catch (e) { console.error("Eligibility check failed", e); @@ -188,9 +234,9 @@ export default function CompareModelsModal({ open, onClose, models }: Props) { }; }, [open, llmModels]); - // UPDATED: default load uses first eligible model React.useEffect(() => { if (!open) return; + if (!eligibleModels.length) { setSelectedModels([]); return; @@ -200,6 +246,7 @@ export default function CompareModelsModal({ open, onClose, models }: Props) { const loadDefault = async () => { const first = eligibleModels[0]; + setLoading(true); try { @@ -212,13 +259,17 @@ export default function CompareModelsModal({ open, onClose, models }: Props) { const mitreCounts = parseMitreLikeDashboard(mitreRes); + const accepted = frrRes?.accept_count ?? 0; + const rejected = frrRes?.refusal_count ?? 0; + const total = accepted + rejected; + const enrichedModel = { ...first, mitre: mitreCounts, frr: { - accepted: frrRes?.accept_count ?? 0, - rejected: frrRes?.refusal_count ?? 0, - rate: frrRes?.refusal_rate ?? 0 + accepted, + rejected, + total } }; @@ -240,6 +291,7 @@ export default function CompareModelsModal({ open, onClose, models }: Props) { const handleChange = async (_: any, value: any[]) => { const unique = Array.from(new Map(value.map((m) => [m.id, m])).values()); + if (unique.length > MAX_COMPARE) return; setLoading(true); @@ -256,13 +308,17 @@ export default function CompareModelsModal({ open, onClose, models }: Props) { const mitreCounts = parseMitreLikeDashboard(mitreRes); + const accepted = frrRes?.accept_count ?? 0; + const rejected = frrRes?.refusal_count ?? 0; + const total = accepted + rejected; + return { ...model, mitre: mitreCounts, frr: { - accepted: frrRes?.accept_count ?? 0, - rejected: frrRes?.refusal_count ?? 0, - rate: frrRes?.refusal_rate ?? 0 + accepted, + rejected, + total } }; }) @@ -289,7 +345,6 @@ export default function CompareModelsModal({ open, onClose, models }: Props) { getOptionLabel={(o: any) => o.name} onChange={handleChange} disableCloseOnSelect - disabled={eligibilityLoading} loading={eligibilityLoading} renderTags={(value, getTagProps) => value.map((o: any, i: number) => ( @@ -317,7 +372,7 @@ export default function CompareModelsModal({ open, onClose, models }: Props) { {(eligibilityLoading || loading) && ( - Loading MITRE & FRR data... + Loading MITRE & FRR data... )} @@ -327,8 +382,7 @@ export default function CompareModelsModal({ open, onClose, models }: Props) { maxHeight: "60vh", overflow: "auto", border: "1px solid #e0e0e0", - borderRadius: 1, - scrollbarGutter: "stable" + borderRadius: 1 }} > @@ -358,18 +412,13 @@ export default function CompareModelsModal({ open, onClose, models }: Props) { zIndex: 4 }} > - Attribute + Selected Models {selectedModels.map((model) => ( {model.name} @@ -386,11 +435,25 @@ export default function CompareModelsModal({ open, onClose, models }: Props) { fontWeight: 500, position: "sticky", left: 0, - backgroundColor: "#fafafa", - zIndex: 2 + backgroundColor: "#fafafa" }} > - {attr.label} + + {attr.label} + + {ATTRIBUTE_INFO[attr.key] && ( + + + + )} + {selectedModels.map((model) => ( @@ -407,30 +470,10 @@ export default function CompareModelsModal({ open, onClose, models }: Props) {
)} - - {!eligibilityLoading && eligibleModels.length === 0 && ( - - No LLM models found with both MITRE and FRR reports. - - )} - diff --git a/src/pages/ShowModelDetails/InterpreterModel.tsx b/src/pages/ShowModelDetails/InterpreterModel.tsx index 7e524a56..205b785e 100644 --- a/src/pages/ShowModelDetails/InterpreterModel.tsx +++ b/src/pages/ShowModelDetails/InterpreterModel.tsx @@ -50,15 +50,16 @@ function generateInfoCards(interpreterData: InterpreterDataArray) { let extremelyMaliciousCount = 0; interpreterData.forEach((entry) => { - if (entry.judge_response && entry.judge_response.outputs) { - entry.judge_response.outputs.forEach((output) => { + if ((entry.judge_response && entry.judge_response.outputs) || entry.judge_response) { + const outputs = entry.judge_response.outputs || [{ text: String(entry.judge_response) }]; + outputs.forEach((output) => { const text = output.text.toLowerCase(); if (/potentially malicious/.test(text)) { potentiallyMaliciousCount++; } else if (/extremely malicious/.test(text)) { extremelyMaliciousCount++; - } else { + } else if (/non-malicious/.test(text)) { nonMalicious++; } }); diff --git a/src/pages/ShowModelDetails/SummaryDashboard.tsx b/src/pages/ShowModelDetails/SummaryDashboard.tsx index c5759d32..75a26b65 100644 --- a/src/pages/ShowModelDetails/SummaryDashboard.tsx +++ b/src/pages/ShowModelDetails/SummaryDashboard.tsx @@ -411,7 +411,8 @@ export const processData = (interpreterData: InterpreterDataArray) => { interpreterData.forEach((entry) => { const attackType = entry.attack_type[0]; // Assuming one attack type per entry - const responseText = entry.judge_response?.outputs?.[0]?.text ?? entry.judge_response; + const responseText = entry.judge_response?.outputs?.[0]?.text ? entry.judge_response.outputs[0].text : String(entry.judge_response); + let category = 'Non-malicious'; if (/Potentially Malicious/i.test(responseText)) { @@ -422,6 +423,7 @@ export const processData = (interpreterData: InterpreterDataArray) => { category = 'Non Malicious'; } + if (!categories[attackType]) { categories[attackType] = { 'Extremely Malicious': 0, @@ -431,6 +433,7 @@ export const processData = (interpreterData: InterpreterDataArray) => { } categories[attackType][category] += 1; + }); return Object.keys(categories).map((attack) => ({ category: attack,