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,