Repository: promptfoo/promptfoo
Pull request #3700: chore: expand frameworks section
Author: login: typpo
Created: 2025-04-14T21:41:26Z | Merged: 2025-04-15T02:59:01Z

<img width="1524" alt="image" src="https://github.com/user-attachments/assets/6a5562b9-58dd-42f8-9ca6-9bb6b8d45b5c" />



Diff:
diff --git a/src/app/src/pages/redteam/report/components/FrameworkCard.tsx b/src/app/src/pages/redteam/report/components/FrameworkCard.tsx
new file mode 100644
index 000000000000..981d1f9c4f81
--- /dev/null
+++ b/src/app/src/pages/redteam/report/components/FrameworkCard.tsx
@@ -0,0 +1,666 @@
+import React from 'react';
+import CancelIcon from '@mui/icons-material/Cancel';
+import CheckCircleIcon from '@mui/icons-material/CheckCircle';
+import InfoIcon from '@mui/icons-material/Info';
+import Box from '@mui/material/Box';
+import Card from '@mui/material/Card';
+import CardContent from '@mui/material/CardContent';
+import Chip from '@mui/material/Chip';
+import List from '@mui/material/List';
+import ListItem from '@mui/material/ListItem';
+import ListItemIcon from '@mui/material/ListItemIcon';
+import ListItemText from '@mui/material/ListItemText';
+import Tooltip from '@mui/material/Tooltip';
+import Typography from '@mui/material/Typography';
+import {
+  ALIASED_PLUGIN_MAPPINGS,
+  FRAMEWORK_NAMES,
+  OWASP_LLM_TOP_10_NAMES,
+  OWASP_API_TOP_10_NAMES,
+  riskCategorySeverityMap,
+  Severity,
+} from '@promptfoo/redteam/constants';
+import {
+  type CategoryStats,
+  expandPluginCollections,
+  categorizePlugins,
+  getPluginDisplayName,
+  getSeverityColor,
+  FRAMEWORK_DESCRIPTIONS,
+} from './FrameworkComplianceUtils';
+
+interface FrameworkCardProps {
+  framework: string;
+  isCompliant: boolean;
+  frameworkSeverity: Severity;
+  categoryStats: CategoryStats;
+  pluginPassRateThreshold: number;
+  nonCompliantPlugins: string[];
+  sortedNonCompliantPlugins: (plugins: string[]) => string[];
+  getPluginPassRate: (plugin: string) => { pass: number; total: number; rate: number };
+}
+
+const FrameworkCard: React.FC<FrameworkCardProps> = ({
+  framework,
+  isCompliant,
+  frameworkSeverity,
+  categoryStats,
+  pluginPassRateThreshold,
+  nonCompliantPlugins,
+  sortedNonCompliantPlugins,
+  getPluginPassRate,
+}) => {
+  const sortedPlugins = sortedNonCompliantPlugins(nonCompliantPlugins);
+
+  return (
+    <Card className={`framework-item ${isCompliant ? 'compliant' : 'non-compliant'}`}>
+      <CardContent>
+        <Box display="flex" alignItems="center" justifyContent="space-between" mb={1}>
+          <Box display="flex" alignItems="center">
+            <Typography variant="h6" component="div" sx={{ mr: 1 }}>
+              {FRAMEWORK_NAMES[framework]}
+            </Typography>
+            <Tooltip title={FRAMEWORK_DESCRIPTIONS[framework] || ''} arrow>
+              <InfoIcon fontSize="small" color="action" sx={{ opacity: 0.7 }} />
+            </Tooltip>
+          </Box>
+          <Box display="flex" alignItems="center">
+            {isCompliant ? (
+              <CheckCircleIcon className="icon-compliant" />
+            ) : (
+              <Tooltip
+                title={
+                  frameworkSeverity === Severity.Critical
+                    ? 'Critical: Requires immediate attention - high risk security vulnerabilities'
+                    : frameworkSeverity === Severity.High
+                      ? 'High: Serious security issues that should be prioritized'
+                      : frameworkSeverity === Severity.Medium
+                        ? 'Medium: Moderate security concerns that should be addressed'
+                        : 'Low: Minor issues with limited security impact'
+                }
+                arrow
+              >
+                <Chip
+                  label={frameworkSeverity}
+                  size="small"
+                  sx={{
+                    backgroundColor: getSeverityColor(frameworkSeverity),
+                    color: 'white',
+                    fontWeight: 'bold',
+                  }}
+                />
+              </Tooltip>
+            )}
+          </Box>
+        </Box>
+        {/* Always expanded */}
+        <Box mt={2}>
+          {(framework === 'owasp:api' || framework === 'owasp:llm') &&
+          Object.keys(ALIASED_PLUGIN_MAPPINGS[framework]).length > 0 ? (
+            // Show categorized plugins for OWASP frameworks
+            <div>
+              {Object.entries(ALIASED_PLUGIN_MAPPINGS[framework]).map(
+                ([categoryId, { plugins: categoryPlugins }]) => {
+                  const categoryNumber = categoryId.split(':').pop();
+                  const categoryName =
+                    categoryNumber && framework === 'owasp:llm'
+                      ? OWASP_LLM_TOP_10_NAMES[Number.parseInt(categoryNumber) - 1]
+                      : categoryNumber && framework === 'owasp:api'
+                        ? OWASP_API_TOP_10_NAMES[Number.parseInt(categoryNumber) - 1]
+                        : `Category ${categoryNumber}`;
+
+                  // Expand harmful if present
+                  const expandedPlugins = expandPluginCollections(categoryPlugins, categoryStats);
+
+                  // Categorize all plugins: tested-compliant, tested-non-compliant, and not-tested
+                  const {
+                    compliant: compliantCategoryPlugins,
+                    nonCompliant: nonCompliantCategoryPlugins,
+                    untested: untestedPlugins,
+                  } = categorizePlugins(expandedPlugins, categoryStats, pluginPassRateThreshold);
+
+                  // Sort all sets by severity
+                  const sortedNonCompliantItems = sortedNonCompliantPlugins(
+                    nonCompliantCategoryPlugins,
+                  );
+                  const sortedCompliantItems = sortedNonCompliantPlugins(compliantCategoryPlugins);
+                  const sortedUntestedItems = sortedNonCompliantPlugins(untestedPlugins);
+
+                  // Get all tested plugins
+                  const testedPlugins = [
+                    ...compliantCategoryPlugins,
+                    ...nonCompliantCategoryPlugins,
+                  ];
+
+                  // Are all plugins compliant or are there no tested plugins?
+                  const allCompliant =
+                    testedPlugins.length > 0 && nonCompliantCategoryPlugins.length === 0;
+                  const noTestedPlugins = testedPlugins.length === 0;
+
+                  return (
+                    <Box
+                      key={categoryId}
+                      mb={2}
+                      sx={{
+                        border: '1px solid rgba(0, 0, 0, 0.08)',
+                        borderRadius: 1,
+                        overflow: 'hidden',
+                      }}
+                    >
+                      <Box
+                        display="flex"
+                        alignItems="center"
+                        justifyContent="space-between"
+                        p={1}
+                        sx={{
+                          bgcolor: 'rgba(0, 0, 0, 0.05)',
+                          borderBottom:
+                            allCompliant || noTestedPlugins
+                              ? 'none'
+                              : '1px solid rgba(0, 0, 0, 0.08)',
+                        }}
+                      >
+                        <Typography variant="subtitle2">
+                          {categoryNumber}. {categoryName}
+                        </Typography>
+                        {testedPlugins.length === 0 && untestedPlugins.length === 0 ? (
+                          <Chip
+                            label="No Plugins"
+                            size="small"
+                            sx={{
+                              backgroundColor: '#9e9e9e',
+                              color: 'white',
+                              fontSize: '0.7rem',
+                              height: 20,
+                            }}
+                          />
+                        ) : testedPlugins.length > 0 ? (
+                          <Chip
+                            label={
+                              nonCompliantCategoryPlugins.length === 0
+                                ? '0% ASR'
+                                : `${Math.round((nonCompliantCategoryPlugins.length / testedPlugins.length) * 100)}% ASR`
+                            }
+                            size="small"
+                            sx={{
+                              backgroundColor:
+                                nonCompliantCategoryPlugins.length === 0 ? '#4caf50' : '#f44336',
+                              color: 'white',
+                              fontSize: '0.7rem',
+                              height: 20,
+                            }}
+                          />
+                        ) : (
+                          <Chip
+                            label={`${untestedPlugins.length} Untested`}
+                            size="small"
+                            sx={{
+                              backgroundColor: '#9e9e9e',
+                              color: 'white',
+                              fontSize: '0.7rem',
+                              height: 20,
+                            }}
+                          />
+                        )}
+                      </Box>
+
+                      <List dense sx={{ py: 0 }}>
+                        {/* Failed plugins first */}
+                        {sortedNonCompliantItems.length > 0 && (
+                          <ListItem
+                            sx={{
+                              py: 0.5,
+                              px: 1,
+                              bgcolor: 'rgba(244, 67, 54, 0.05)',
+                            }}
+                          >
+                            <Typography variant="caption" fontWeight="bold" color="error.main">
+                              Failed:
+                            </Typography>
+                          </ListItem>
+                        )}
+                        {sortedNonCompliantItems.map((plugin) => {
+                          const passRate = getPluginPassRate(plugin);
+                          const pluginSeverity =
+                            riskCategorySeverityMap[
+                              plugin as keyof typeof riskCategorySeverityMap
+                            ] || Severity.Low;
+
+                          return (
+                            <ListItem
+                              key={plugin}
+                              sx={{
+                                borderLeft: `3px solid ${getSeverityColor(pluginSeverity)}`,
+                                pl: 2,
+                                mb: 0.5,
+                                bgcolor: 'rgba(0, 0, 0, 0.02)',
+                                borderRadius: '0 4px 4px 0',
+                              }}
+                            >
+                              <ListItemIcon sx={{ minWidth: 30 }}>
+                                <CancelIcon fontSize="small" color="error" />
+                              </ListItemIcon>
+                              <ListItemText
+                                primary={
+                                  <Box
+                                    display="flex"
+                                    alignItems="center"
+                                    justifyContent="space-between"
+                                  >
+                                    <Typography variant="body2">
+                                      {getPluginDisplayName(plugin)}
+                                    </Typography>
+                                    <Tooltip
+                                      title={`${passRate.total - passRate.pass}/${passRate.total} attacks successful`}
+                                    >
+                                      <Typography
+                                        variant="caption"
+                                        sx={{
+                                          fontWeight: 'bold',
+                                          color: 'error.main',
+                                        }}
+                                      >
+                                        {(100 - passRate.rate).toFixed(0)}%
+                                      </Typography>
+                                    </Tooltip>
+                                  </Box>
+                                }
+                              />
+                            </ListItem>
+                          );
+                        })}
+
+                        {/* Passing plugins */}
+                        {sortedCompliantItems.length > 0 && (
+                          <ListItem
+                            sx={{
+                              py: 0.5,
+                              px: 1,
+                              bgcolor: 'rgba(76, 175, 80, 0.05)',
+                              mt: 1,
+                            }}
+                          >
+                            <Typography variant="caption" fontWeight="bold" color="success.main">
+                              Passed:
+                            </Typography>
+                          </ListItem>
+                        )}
+                        {sortedCompliantItems.map((plugin) => {
+                          const passRate = getPluginPassRate(plugin);
+                          const pluginSeverity =
+                            riskCategorySeverityMap[
+                              plugin as keyof typeof riskCategorySeverityMap
+                            ] || Severity.Low;
+
+                          return (
+                            <ListItem
+                              key={plugin}
+                              sx={{
+                                borderLeft: `3px solid ${getSeverityColor(pluginSeverity)}`,
+                                pl: 2,
+                                mb: 0.5,
+                                bgcolor: 'rgba(0, 0, 0, 0.01)',
+                                borderRadius: '0 4px 4px 0',
+                              }}
+                            >
+                              <ListItemIcon sx={{ minWidth: 30 }}>
+                                <CheckCircleIcon fontSize="small" color="success" />
+                              </ListItemIcon>
+                              <ListItemText
+                                primary={
+                                  <Box
+                                    display="flex"
+                                    alignItems="center"
+                                    justifyContent="space-between"
+                                  >
+                                    <Typography variant="body2">
+                                      {getPluginDisplayName(plugin)}
+                                    </Typography>
+                                    <Tooltip
+                                      title={`${passRate.total - passRate.pass}/${passRate.total} attacks successful`}
+                                    >
+                                      <Typography
+                                        variant="caption"
+                                        sx={{
+                                          fontWeight: 'bold',
+                                          color: 'success.main',
+                                        }}
+                                      >
+                                        {(100 - passRate.rate).toFixed(0)}%
+                                      </Typography>
+                                    </Tooltip>
+                                  </Box>
+                                }
+                              />
+                            </ListItem>
+                          );
+                        })}
+
+                        {/* Untested plugins */}
+                        {sortedUntestedItems.length > 0 && (
+                          <>
+                            <ListItem
+                              sx={{
+                                py: 0.5,
+                                px: 1,
+                                bgcolor: 'rgba(158, 158, 158, 0.1)',
+                                mt: 1,
+                              }}
+                            >
+                              <Typography
+                                variant="caption"
+                                fontWeight="bold"
+                                color="text.secondary"
+                              >
+                                Not Tested:
+                              </Typography>
+                            </ListItem>
+                            {sortedUntestedItems.map((plugin) => {
+                              const pluginSeverity =
+                                riskCategorySeverityMap[
+                                  plugin as keyof typeof riskCategorySeverityMap
+                                ] || Severity.Low;
+
+                              return (
+                                <ListItem
+                                  key={plugin}
+                                  sx={{
+                                    borderLeft: `3px solid ${getSeverityColor(pluginSeverity)}`,
+                                    pl: 2,
+                                    mb: 0.5,
+                                    bgcolor: 'rgba(0, 0, 0, 0.01)',
+                                    borderRadius: '0 4px 4px 0',
+                                    opacity: 0.7,
+                                  }}
+                                >
+                                  <ListItemIcon sx={{ minWidth: 30 }}>
+                                    <InfoIcon fontSize="small" color="action" />
+                                  </ListItemIcon>
+                                  <ListItemText
+                                    primary={
+                                      <Box
+                                        display="flex"
+                                        alignItems="center"
+                                        justifyContent="space-between"
+                                      >
+                                        <Typography variant="body2">
+                                          {getPluginDisplayName(plugin)}
+                                        </Typography>
+                                        <Typography
+                                          variant="caption"
+                                          sx={{
+                                            fontWeight: 'medium',
+                                            color: 'text.secondary',
+                                          }}
+                                        >
+                                          Not Tested
+                                        </Typography>
+                                      </Box>
+                                    }
+                                  />
+                                </ListItem>
+                              );
+                            })}
+                          </>
+                        )}
+                      </List>
+                    </Box>
+                  );
+                },
+              )}
+            </div>
+          ) : (
+            // Standard list view for other frameworks but with same format
+            <Box
+              sx={{
+                border: '1px solid rgba(0, 0, 0, 0.08)',
+                borderRadius: 1,
+                overflow: 'hidden',
+              }}
+            >
+              <Box
+                display="flex"
+                alignItems="center"
+                justifyContent="space-between"
+                p={1}
+                sx={{
+                  bgcolor: 'rgba(0, 0, 0, 0.05)',
+                  borderBottom: '1px solid rgba(0, 0, 0, 0.08)',
+                }}
+              >
+                <Typography variant="subtitle2">Framework Results</Typography>
+                {nonCompliantPlugins.length === 0 ? (
+                  <Chip
+                    label="0%"
+                    size="small"
+                    sx={{
+                      backgroundColor: '#4caf50',
+                      color: 'white',
+                      fontSize: '0.7rem',
+                      height: 20,
+                    }}
+                  />
+                ) : (
+                  <Chip
+                    label={`${Math.round((nonCompliantPlugins.length / Object.keys(categoryStats).filter((plugin) => categoryStats[plugin].total > 0).length) * 100)}%`}
+                    size="small"
+                    sx={{
+                      backgroundColor: '#f44336',
+                      color: 'white',
+                      fontSize: '0.7rem',
+                      height: 20,
+                    }}
+                  />
+                )}
+              </Box>
+
+              <List dense sx={{ py: 0 }}>
+                {/* Failed plugins first */}
+                {nonCompliantPlugins.length > 0 && (
+                  <ListItem sx={{ py: 0.5, px: 1, bgcolor: 'rgba(244, 67, 54, 0.05)' }}>
+                    <Typography variant="caption" fontWeight="bold" color="error.main">
+                      Failed:
+                    </Typography>
+                  </ListItem>
+                )}
+                {sortedPlugins.map((plugin) => {
+                  const passRate = getPluginPassRate(plugin);
+                  const pluginSeverity =
+                    riskCategorySeverityMap[plugin as keyof typeof riskCategorySeverityMap] ||
+                    Severity.Low;
+
+                  return (
+                    <ListItem
+                      key={plugin}
+                      sx={{
+                        borderLeft: `3px solid ${getSeverityColor(pluginSeverity)}`,
+                        pl: 2,
+                        mb: 0.5,
+                        bgcolor: 'rgba(0, 0, 0, 0.02)',
+                        borderRadius: '0 4px 4px 0',
+                      }}
+                    >
+                      <ListItemIcon sx={{ minWidth: 30 }}>
+                        <CancelIcon fontSize="small" color="error" />
+                      </ListItemIcon>
+                      <ListItemText
+                        primary={
+                          <Box display="flex" alignItems="center" justifyContent="space-between">
+                            <Typography variant="body2">{getPluginDisplayName(plugin)}</Typography>
+                            <Tooltip
+                              title={`${passRate.total - passRate.pass}/${passRate.total} attacks successful`}
+                            >
+                              <Typography
+                                variant="caption"
+                                sx={{ fontWeight: 'bold', color: 'error.main' }}
+                              >
+                                {(100 - passRate.rate).toFixed(0)}%
+                              </Typography>
+                            </Tooltip>
+                          </Box>
+                        }
+                      />
+                    </ListItem>
+                  );
+                })}
+
+                {/* Passing plugins */}
+                {Object.keys(categoryStats).filter(
+                  (plugin) =>
+                    categoryStats[plugin].total > 0 &&
+                    categoryStats[plugin].pass / categoryStats[plugin].total >=
+                      pluginPassRateThreshold,
+                ).length > 0 && (
+                  <ListItem sx={{ py: 0.5, px: 1, bgcolor: 'rgba(76, 175, 80, 0.05)', mt: 1 }}>
+                    <Typography variant="caption" fontWeight="bold" color="success.main">
+                      Passed:
+                    </Typography>
+                  </ListItem>
+                )}
+                {Object.keys(categoryStats)
+                  .filter(
+                    (plugin) =>
+                      categoryStats[plugin].total > 0 &&
+                      categoryStats[plugin].pass / categoryStats[plugin].total >=
+                        pluginPassRateThreshold,
+                  )
+                  .sort((a, b) => {
+                    // Sort by severity first
+                    const severityA =
+                      riskCategorySeverityMap[a as keyof typeof riskCategorySeverityMap] ||
+                      Severity.Low;
+                    const severityB =
+                      riskCategorySeverityMap[b as keyof typeof riskCategorySeverityMap] ||
+                      Severity.Low;
+
+                    const severityOrder = {
+                      [Severity.Critical]: 0,
+                      [Severity.High]: 1,
+                      [Severity.Medium]: 2,
+                      [Severity.Low]: 3,
+                    };
+
+                    return severityOrder[severityA] - severityOrder[severityB];
+                  })
+                  .map((plugin) => {
+                    const passRate = getPluginPassRate(plugin);
+                    const pluginSeverity =
+                      riskCategorySeverityMap[plugin as keyof typeof riskCategorySeverityMap] ||
+                      Severity.Low;
+
+                    return (
+                      <ListItem
+                        key={plugin}
+                        sx={{
+                          borderLeft: `3px solid ${getSeverityColor(pluginSeverity)}`,
+                          pl: 2,
+                          mb: 0.5,
+                          bgcolor: 'rgba(0, 0, 0, 0.01)',
+                          borderRadius: '0 4px 4px 0',
+                        }}
+                      >
+                        <ListItemIcon sx={{ minWidth: 30 }}>
+                          <CheckCircleIcon fontSize="small" color="success" />
+                        </ListItemIcon>
+                        <ListItemText
+                          primary={
+                            <Box display="flex" alignItems="center" justifyContent="space-between">
+                              <Typography variant="body2">
+                                {getPluginDisplayName(plugin)}
+                              </Typography>
+                              <Tooltip
+                                title={`${passRate.total - passRate.pass}/${passRate.total} attacks successful`}
+                              >
+                                <Typography
+                                  variant="caption"
+                                  sx={{ fontWeight: 'bold', color: 'success.main' }}
+                                >
+                                  {(100 - passRate.rate).toFixed(0)}%
+                                </Typography>
+                              </Tooltip>
+                            </Box>
+                          }
+                        />
+                      </ListItem>
+                    );
+                  })}
+
+                {/* Untested plugins for this framework */}
+                {Object.keys(ALIASED_PLUGIN_MAPPINGS[framework] || {})
+                  .flatMap((categoryId) => {
+                    // Get all plugins from this category
+                    const categoryPlugins =
+                      ALIASED_PLUGIN_MAPPINGS[framework]?.[categoryId]?.plugins || [];
+                    // Expand plugins using the utility function
+                    return Array.from(expandPluginCollections(categoryPlugins, categoryStats));
+                  })
+                  .filter((plugin) => !categoryStats[plugin] || categoryStats[plugin].total === 0)
+                  .sort((a, b) => {
+                    // Sort by severity first
+                    const severityA =
+                      riskCategorySeverityMap[a as keyof typeof riskCategorySeverityMap] ||
+                      Severity.Low;
+                    const severityB =
+                      riskCategorySeverityMap[b as keyof typeof riskCategorySeverityMap] ||
+                      Severity.Low;
+
+                    const severityOrder = {
+                      [Severity.Critical]: 0,
+                      [Severity.High]: 1,
+                      [Severity.Medium]: 2,
+                      [Severity.Low]: 3,
+                    };
+
+                    return severityOrder[severityA] - severityOrder[severityB];
+                  })
+                  .map((plugin) => {
+                    const pluginSeverity =
+                      riskCategorySeverityMap[plugin as keyof typeof riskCategorySeverityMap] ||
+                      Severity.Low;
+
+                    return (
+                      <ListItem
+                        key={plugin}
+                        sx={{
+                          borderLeft: `3px solid ${getSeverityColor(pluginSeverity)}`,
+                          pl: 2,
+                          mb: 0.5,
+                          bgcolor: 'rgba(0, 0, 0, 0.01)',
+                          borderRadius: '0 4px 4px 0',
+                          opacity: 0.7,
+                        }}
+                      >
+                        <ListItemIcon sx={{ minWidth: 30 }}>
+                          <InfoIcon fontSize="small" color="action" />
+                        </ListItemIcon>
+                        <ListItemText
+                          primary={
+                            <Box display="flex" alignItems="center" justifyContent="space-between">
+                              <Typography variant="body2">
+                                {getPluginDisplayName(plugin)}
+                              </Typography>
+                              <Typography
+                                variant="caption"
+                                sx={{
+                                  fontWeight: 'medium',
+                                  color: 'text.secondary',
+                                }}
+                              >
+                                Not Tested
+                              </Typography>
+                            </Box>
+                          }
+                        />
+                      </ListItem>
+                    );
+                  })}
+              </List>
+            </Box>
+          )}
+        </Box>
+      </CardContent>
+    </Card>
+  );
+};
+
+export default FrameworkCard;
diff --git a/src/app/src/pages/redteam/report/components/FrameworkCompliance.css b/src/app/src/pages/redteam/report/components/FrameworkCompliance.css
index 4dac292d7780..30085b283a8b 100644
--- a/src/app/src/pages/redteam/report/components/FrameworkCompliance.css
+++ b/src/app/src/pages/redteam/report/components/FrameworkCompliance.css
@@ -25,20 +25,22 @@
 .framework-item {
   height: 100%;
   transition:
-    transform 0.3s ease,
+    transform 0.2s ease,
     box-shadow 0.3s ease;
+  border-radius: 8px;
+  overflow: hidden;
 }
 
 .framework-item:hover {
-  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
+  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
 }
 
 .framework-item.compliant {
-  background-color: #e8f5e9;
+  background-color: #f8fbf8;
 }
 
 .framework-item.non-compliant {
-  background-color: #ffebee;
+  background-color: #fef8f8;
 }
 
 .icon-compliant {
@@ -51,6 +53,71 @@
   font-size: 24px;
 }
 
+/* Severity colors */
+.severity-critical {
+  background-color: rgba(211, 47, 47, 0.1);
+  border-left: 4px solid #d32f2f;
+}
+
+.severity-high {
+  background-color: rgba(245, 124, 0, 0.1);
+  border-left: 4px solid #f57c00;
+}
+
+.severity-medium {
+  background-color: rgba(251, 192, 45, 0.1);
+  border-left: 4px solid #fbc02d;
+}
+
+.severity-low {
+  background-color: rgba(124, 179, 66, 0.1);
+  border-left: 4px solid #7cb342;
+}
+
+/* Plugin list styles */
+.plugin-list-container {
+  background-color: rgba(0, 0, 0, 0.02);
+  border-radius: 4px;
+  margin-top: 8px;
+  overflow: hidden;
+}
+
+.plugin-toggle-header {
+  cursor: pointer;
+  padding: 8px 12px;
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  background-color: rgba(0, 0, 0, 0.05);
+  transition: background-color 0.2s ease;
+}
+
+.plugin-toggle-header:hover {
+  background-color: rgba(0, 0, 0, 0.08);
+}
+
+.plugin-item {
+  border-bottom: 1px solid rgba(0, 0, 0, 0.03);
+  transition: background-color 0.2s ease;
+}
+
+.plugin-item:last-child {
+  border-bottom: none;
+}
+
+.plugin-item:hover {
+  background-color: rgba(0, 0, 0, 0.04);
+}
+
+.plugin-pass-rate {
+  font-weight: bold;
+  padding: 2px 6px;
+  border-radius: 10px;
+  font-size: 11px;
+  min-width: 24px;
+  text-align: center;
+}
+
 [data-theme='dark'] .framework-compliance-card {
   background-color: #1e1e1e;
 }
@@ -60,11 +127,11 @@
 }
 
 [data-theme='dark'] .framework-item.compliant {
-  background-color: rgba(76, 175, 80, 0.1);
+  background-color: rgba(76, 175, 80, 0.05);
 }
 
 [data-theme='dark'] .framework-item.non-compliant {
-  background-color: rgba(244, 67, 54, 0.1);
+  background-color: rgba(244, 67, 54, 0.05);
 }
 
 [data-theme='dark'] .framework-compliance-card:hover,
@@ -72,15 +139,31 @@
   box-shadow: 0 8px 16px rgba(255, 255, 255, 0.1);
 }
 
-/* New styles for the non-compliant plugins list */
+[data-theme='dark'] .plugin-toggle-header {
+  background-color: rgba(255, 255, 255, 0.05);
+}
+
+[data-theme='dark'] .plugin-toggle-header:hover {
+  background-color: rgba(255, 255, 255, 0.08);
+}
+
+[data-theme='dark'] .plugin-item {
+  border-bottom: 1px solid rgba(255, 255, 255, 0.03);
+}
+
+[data-theme='dark'] .plugin-item:hover {
+  background-color: rgba(255, 255, 255, 0.04);
+}
+
+/* MUI component overrides */
 .framework-item .MuiList-root {
   padding-top: 0;
   padding-bottom: 0;
 }
 
 .framework-item .MuiListItem-root {
-  padding-top: 0;
-  padding-bottom: 0;
+  padding-top: 4px;
+  padding-bottom: 4px;
 }
 
 .framework-item .MuiListItemIcon-root {
@@ -91,3 +174,12 @@
   margin-top: 0;
   margin-bottom: 0;
 }
+
+.framework-item .MuiTooltip-tooltip {
+  font-size: 12px;
+}
+
+.framework-item .MuiChip-root {
+  font-size: 11px;
+  height: 22px;
+}
diff --git a/src/app/src/pages/redteam/report/components/FrameworkCompliance.tsx b/src/app/src/pages/redteam/report/components/FrameworkCompliance.tsx
index 7c9b0e4886e6..af22952b1031 100644
--- a/src/app/src/pages/redteam/report/components/FrameworkCompliance.tsx
+++ b/src/app/src/pages/redteam/report/components/FrameworkCompliance.tsx
@@ -1,26 +1,23 @@
-import React, { useState } from 'react';
-import CancelIcon from '@mui/icons-material/Cancel';
-import CheckCircleIcon from '@mui/icons-material/CheckCircle';
-import ExpandLessIcon from '@mui/icons-material/ExpandLess';
-import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
+import React from 'react';
 import Box from '@mui/material/Box';
 import Card from '@mui/material/Card';
 import CardContent from '@mui/material/CardContent';
 import Grid from '@mui/material/Grid';
-import IconButton from '@mui/material/IconButton';
 import LinearProgress from '@mui/material/LinearProgress';
-import List from '@mui/material/List';
-import ListItem from '@mui/material/ListItem';
-import ListItemIcon from '@mui/material/ListItemIcon';
-import ListItemText from '@mui/material/ListItemText';
 import Typography from '@mui/material/Typography';
 import {
   ALIASED_PLUGIN_MAPPINGS,
-  categoryAliases,
-  displayNameOverrides,
-  FRAMEWORK_NAMES,
   FRAMEWORK_COMPLIANCE_IDS,
+  riskCategorySeverityMap,
+  Severity,
 } from '@promptfoo/redteam/constants';
+import FrameworkCard from './FrameworkCard';
+import {
+  expandPluginCollections,
+  categorizePlugins,
+  getProgressColor,
+} from './FrameworkComplianceUtils';
+import CSVExporter from './FrameworkCsvExporter';
 import { useReportStore } from './store';
 import './FrameworkCompliance.css';
 
@@ -33,8 +30,7 @@ const FrameworkCompliance: React.FC<FrameworkComplianceProps> = ({
   categoryStats,
   strategyStats,
 }) => {
-  const { pluginPassRateThreshold, showComplianceSection } = useReportStore();
-  const [expandedFrameworks, setExpandedFrameworks] = useState<Record<string, boolean>>({});
+  const { pluginPassRateThreshold } = useReportStore();
 
   const getNonCompliantPlugins = React.useCallback(
     (framework: string) => {
@@ -43,19 +39,65 @@ const FrameworkCompliance: React.FC<FrameworkComplianceProps> = ({
         return [];
       }
 
-      return Array.from(
-        new Set(
-          Object.entries(mappings).flatMap(([_, { plugins, strategies }]) => {
-            const nonCompliantItems = [...plugins, ...strategies].filter((item) => {
-              const stats = categoryStats[item] || strategyStats[item];
-              return stats && stats.total > 0 && stats.pass / stats.total < pluginPassRateThreshold;
-            });
-            return nonCompliantItems;
-          }),
-        ),
+      // First, collect all plugins from all subcategories
+      const allPlugins = new Set<string>();
+      Object.values(mappings).forEach(({ plugins }) => {
+        const expanded = expandPluginCollections(plugins, categoryStats);
+        expanded.forEach((plugin) => allPlugins.add(plugin));
+      });
+
+      // Then filter for non-compliant ones
+      const { nonCompliant } = categorizePlugins(
+        allPlugins,
+        categoryStats,
+        pluginPassRateThreshold,
       );
+      return nonCompliant;
+    },
+    [categoryStats, pluginPassRateThreshold],
+  );
+
+  const getPluginPassRate = React.useCallback(
+    (plugin: string): { pass: number; total: number; rate: number } => {
+      const stats = categoryStats[plugin] || { pass: 0, total: 0 };
+      return {
+        pass: stats.pass,
+        total: stats.total,
+        rate: stats.total > 0 ? (stats.pass / stats.total) * 100 : 0,
+      };
+    },
+    [categoryStats],
+  );
+
+  const getFrameworkSeverity = React.useCallback(
+    (framework: string): Severity => {
+      const nonCompliantPlugins = getNonCompliantPlugins(framework);
+
+      if (nonCompliantPlugins.length === 0) {
+        return Severity.Low;
+      }
+
+      // Find the highest severity among non-compliant plugins
+      let highestSeverity = Severity.Low;
+
+      for (const plugin of nonCompliantPlugins) {
+        const pluginSeverity =
+          riskCategorySeverityMap[plugin as keyof typeof riskCategorySeverityMap] || Severity.Low;
+
+        if (pluginSeverity === Severity.Critical) {
+          return Severity.Critical;
+        }
+
+        if (pluginSeverity === Severity.High) {
+          highestSeverity = Severity.High;
+        } else if (pluginSeverity === Severity.Medium && highestSeverity === Severity.Low) {
+          highestSeverity = Severity.Medium;
+        }
+      }
+
+      return highestSeverity;
     },
-    [categoryStats, strategyStats, pluginPassRateThreshold],
+    [getNonCompliantPlugins],
   );
 
   const frameworkCompliance = React.useMemo(() => {
@@ -69,12 +111,9 @@ const FrameworkCompliance: React.FC<FrameworkComplianceProps> = ({
     );
   }, [getNonCompliantPlugins]);
 
-  const totalFrameworks = FRAMEWORK_COMPLIANCE_IDS.length;
-  const compliantFrameworks = Object.values(frameworkCompliance).filter(Boolean).length;
-
   const pluginComplianceStats = React.useMemo(() => {
-    let totalPlugins = 0;
-    let compliantPlugins = 0;
+    // Collect all unique plugins across all frameworks
+    const allFrameworkPlugins = new Set<string>();
 
     FRAMEWORK_COMPLIANCE_IDS.forEach((framework) => {
       const mappings = ALIASED_PLUGIN_MAPPINGS[framework];
@@ -82,125 +121,132 @@ const FrameworkCompliance: React.FC<FrameworkComplianceProps> = ({
         return;
       }
 
-      Object.values(mappings).forEach(({ plugins, strategies }) => {
-        const items = [...plugins, ...strategies];
-        totalPlugins += items.length;
-
-        const passingItems = items.filter((item) => {
-          const stats = categoryStats[item] || strategyStats[item];
-          return stats && stats.total > 0 && stats.pass / stats.total >= pluginPassRateThreshold;
-        });
-        compliantPlugins += passingItems.length;
+      Object.values(mappings).forEach(({ plugins }) => {
+        const expanded = expandPluginCollections(plugins, categoryStats);
+        expanded.forEach((plugin) => allFrameworkPlugins.add(plugin));
       });
     });
 
+    // Filter for plugins that have test data
+    const pluginsWithData = Array.from(allFrameworkPlugins).filter(
+      (plugin) => categoryStats[plugin] && categoryStats[plugin].total > 0,
+    );
+
+    // Count compliant plugins and calculate actual attack success rate
+    let totalTests = 0;
+    let totalFailedTests = 0;
+    const compliantPlugins = pluginsWithData.filter((plugin) => {
+      const stats = categoryStats[plugin];
+      totalTests += stats.total;
+      totalFailedTests += stats.total - stats.pass;
+      return stats.pass / stats.total >= pluginPassRateThreshold;
+    }).length;
+
+    // Calculate the true attack success rate based on all test runs
+    const attackSuccessRate = totalTests > 0 ? (totalFailedTests / totalTests) * 100 : 0;
+
     return {
-      total: totalPlugins,
+      total: pluginsWithData.length,
       compliant: compliantPlugins,
-      percentage: (compliantPlugins / totalPlugins) * 100,
+      percentage:
+        pluginsWithData.length > 0 ? (compliantPlugins / pluginsWithData.length) * 100 : 0,
+      attackSuccessRate,
+      failedTests: totalFailedTests,
+      totalTests,
     };
-  }, [categoryStats, strategyStats, pluginPassRateThreshold]);
+  }, [categoryStats, pluginPassRateThreshold]);
+
+  const sortedNonCompliantPlugins = React.useCallback(
+    (plugins: string[]): string[] => {
+      return [...plugins].sort((a, b) => {
+        // 1. First sort by severity (highest first)
+        const severityA =
+          riskCategorySeverityMap[a as keyof typeof riskCategorySeverityMap] || Severity.Low;
+        const severityB =
+          riskCategorySeverityMap[b as keyof typeof riskCategorySeverityMap] || Severity.Low;
+
+        const severityOrder = {
+          [Severity.Critical]: 0,
+          [Severity.High]: 1,
+          [Severity.Medium]: 2,
+          [Severity.Low]: 3,
+        };
 
-  const toggleFramework = (framework: string) => {
-    setExpandedFrameworks((prev) => ({
-      ...prev,
-      [framework]: !prev[framework],
-    }));
-  };
+        if (severityOrder[severityA] !== severityOrder[severityB]) {
+          return severityOrder[severityA] - severityOrder[severityB];
+        }
 
-  if (!showComplianceSection) {
-    return null;
-  }
+        // 2. Then sort by pass rate (lowest first)
+        const passRateA = getPluginPassRate(a).rate;
+        const passRateB = getPluginPassRate(b).rate;
+
+        return passRateA - passRateB;
+      });
+    },
+    [getPluginPassRate],
+  );
 
   return (
-    <Card className="framework-compliance-card">
-      <CardContent>
-        <Box display="flex" alignItems="center" justifyContent="space-between" mb={1}>
-          <Typography variant="h5">
-            Frameworks ({compliantFrameworks}/{totalFrameworks})
-          </Typography>
-          <Typography variant="h6" color="textSecondary">
-            {pluginComplianceStats.percentage.toFixed(0)}% ({pluginComplianceStats.compliant}/
-            {pluginComplianceStats.total} plugins)
-          </Typography>
-        </Box>
-        <LinearProgress
-          variant="determinate"
-          value={pluginComplianceStats.percentage}
-          sx={{
-            mb: 3,
-            height: 8,
-            borderRadius: 4,
-            backgroundColor: 'rgba(0, 0, 0, 0.1)',
-            '& .MuiLinearProgress-bar': {
-              borderRadius: 4,
-              backgroundColor: pluginComplianceStats.percentage === 100 ? '#4caf50' : '#1976d2',
-            },
-          }}
+    <Box>
+      <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
+        <Typography variant="h5">
+          Framework Compliance ({Object.values(frameworkCompliance).filter(Boolean).length}/
+          {FRAMEWORK_COMPLIANCE_IDS.length})
+        </Typography>
+        <CSVExporter
+          categoryStats={categoryStats}
+          pluginPassRateThreshold={pluginPassRateThreshold}
         />
-        <Grid container spacing={3} className="framework-grid">
-          {FRAMEWORK_COMPLIANCE_IDS.map((framework) => {
-            const nonCompliantPlugins = getNonCompliantPlugins(framework);
-            const isCompliant = frameworkCompliance[framework];
-            const isExpanded = expandedFrameworks[framework];
-            return (
-              <Grid item xs={12} sm={6} md={3} key={framework}>
-                <Card className={`framework-item ${isCompliant ? 'compliant' : 'non-compliant'}`}>
-                  <CardContent>
-                    <Box display="flex" alignItems="center" justifyContent="space-between" mb={1}>
-                      <Typography variant="h6">{FRAMEWORK_NAMES[framework]}</Typography>
-                      {isCompliant ? (
-                        <CheckCircleIcon className="icon-compliant" />
-                      ) : (
-                        <CancelIcon className="icon-non-compliant" />
-                      )}
-                    </Box>
-                    {!isCompliant && (
-                      <Box>
-                        <Box
-                          display="flex"
-                          alignItems="center"
-                          mb={1}
-                          onClick={() => toggleFramework(framework)}
-                          style={{ cursor: 'pointer' }}
-                        >
-                          <Typography variant="body2" fontWeight="bold">
-                            Non-compliant plugins: {nonCompliantPlugins.length}
-                          </Typography>
-                          <IconButton size="small">
-                            {isExpanded ? <ExpandLessIcon /> : <ExpandMoreIcon />}
-                          </IconButton>
-                        </Box>
-                        {isExpanded && (
-                          <List dense>
-                            {nonCompliantPlugins.map((plugin) => (
-                              <ListItem key={plugin}>
-                                <ListItemIcon>
-                                  <CancelIcon fontSize="small" color="error" />
-                                </ListItemIcon>
-                                <ListItemText
-                                  primary={
-                                    displayNameOverrides[
-                                      plugin as keyof typeof displayNameOverrides
-                                    ] ||
-                                    categoryAliases[plugin as keyof typeof categoryAliases] ||
-                                    plugin
-                                  }
-                                />
-                              </ListItem>
-                            ))}
-                          </List>
-                        )}
-                      </Box>
-                    )}
-                  </CardContent>
-                </Card>
-              </Grid>
-            );
-          })}
-        </Grid>
-      </CardContent>
-    </Card>
+      </Box>
+
+      <Card className="framework-compliance-card">
+        <CardContent>
+          <Box display="flex" alignItems="center" sx={{ mb: 1 }}>
+            <Typography variant="subtitle1" color="textSecondary">
+              {pluginComplianceStats.attackSuccessRate.toFixed(1)}% Attack Success Rate (
+              {pluginComplianceStats.failedTests}/{pluginComplianceStats.totalTests} tests failed
+              across {pluginComplianceStats.total} plugins)
+            </Typography>
+          </Box>
+          <LinearProgress
+            variant="determinate"
+            value={pluginComplianceStats.attackSuccessRate} // Show attack success rate (failure rate)
+            sx={{
+              mb: 3,
+              height: 8,
+              borderRadius: 4,
+              backgroundColor: 'rgba(0, 0, 0, 0.1)',
+              '& .MuiLinearProgress-bar': {
+                borderRadius: 4,
+                backgroundColor: getProgressColor(pluginComplianceStats.attackSuccessRate, true), // Invert color scale
+              },
+            }}
+          />
+          <Grid container spacing={3} className="framework-grid">
+            {FRAMEWORK_COMPLIANCE_IDS.map((framework) => {
+              const nonCompliantPlugins = getNonCompliantPlugins(framework);
+              const isCompliant = frameworkCompliance[framework];
+              const frameworkSeverity = getFrameworkSeverity(framework);
+
+              return (
+                <Grid item xs={12} sm={6} md={3} key={framework}>
+                  <FrameworkCard
+                    framework={framework}
+                    isCompliant={isCompliant}
+                    frameworkSeverity={frameworkSeverity}
+                    categoryStats={categoryStats}
+                    pluginPassRateThreshold={pluginPassRateThreshold}
+                    nonCompliantPlugins={nonCompliantPlugins}
+                    sortedNonCompliantPlugins={sortedNonCompliantPlugins}
+                    getPluginPassRate={getPluginPassRate}
+                  />
+                </Grid>
+              );
+            })}
+          </Grid>
+        </CardContent>
+      </Card>
+    </Box>
   );
 };
 
diff --git a/src/app/src/pages/redteam/report/components/FrameworkComplianceUtils.ts b/src/app/src/pages/redteam/report/components/FrameworkComplianceUtils.ts
new file mode 100644
index 000000000000..ce1a96b2fda7
--- /dev/null
+++ b/src/app/src/pages/redteam/report/components/FrameworkComplianceUtils.ts
@@ -0,0 +1,136 @@
+import { categoryAliases, displayNameOverrides, Severity } from '@promptfoo/redteam/constants';
+
+// Types for utility functions
+export type CategoryStats = Record<
+  string,
+  { pass: number; total: number; passWithFilter?: number }
+>;
+export type PluginCategories = {
+  compliant: string[];
+  nonCompliant: string[];
+  untested: string[];
+};
+
+/**
+ * Expands plugin collections like 'harmful' into their individual plugins
+ */
+export const expandPluginCollections = (
+  plugins: string[],
+  categoryStats: CategoryStats,
+): Set<string> => {
+  const expandedPlugins = new Set<string>();
+  plugins.forEach((plugin) => {
+    if (plugin === 'harmful') {
+      // Add all harmful:* plugins that have stats
+      Object.keys(categoryStats)
+        .filter((key) => key.startsWith('harmful:'))
+        .forEach((key) => expandedPlugins.add(key));
+    } else {
+      expandedPlugins.add(plugin);
+    }
+  });
+  return expandedPlugins;
+};
+
+/**
+ * Categorizes plugins into compliant, non-compliant, and untested based on pass rates
+ */
+export const categorizePlugins = (
+  plugins: Set<string> | string[],
+  categoryStats: CategoryStats,
+  passRateThreshold: number,
+): PluginCategories => {
+  const compliantPlugins: string[] = [];
+  const nonCompliantPlugins: string[] = [];
+  const untestedPlugins: string[] = [];
+
+  // Process all plugins in the category
+  Array.from(plugins).forEach((plugin) => {
+    // Check if plugin has test data
+    if (categoryStats[plugin] && categoryStats[plugin].total > 0) {
+      // Plugin was tested
+      const stats = categoryStats[plugin];
+      if (stats.pass / stats.total >= passRateThreshold) {
+        compliantPlugins.push(plugin);
+      } else {
+        nonCompliantPlugins.push(plugin);
+      }
+    } else {
+      // Plugin was not tested
+      untestedPlugins.push(plugin);
+    }
+  });
+
+  return {
+    compliant: compliantPlugins,
+    nonCompliant: nonCompliantPlugins,
+    untested: untestedPlugins,
+  };
+};
+
+/**
+ * Gets a display name for a plugin
+ */
+export const getPluginDisplayName = (plugin: string): string => {
+  return (
+    displayNameOverrides[plugin as keyof typeof displayNameOverrides] ||
+    categoryAliases[plugin as keyof typeof categoryAliases] ||
+    plugin
+  );
+};
+
+export const FRAMEWORK_DESCRIPTIONS: Record<string, string> = {
+  'mitre:atlas': 'MITRE ATLAS framework for adversarial threat landscape for AI systems',
+  'nist:ai:measure': 'NIST AI Risk Management Framework for responsible AI development',
+  'owasp:api': 'OWASP API Top 10 security risks for application programming interfaces',
+  'owasp:llm': 'OWASP LLM Top 10 security vulnerabilities for large language models',
+};
+
+export const getSeverityColor = (severity: Severity): string => {
+  switch (severity) {
+    case Severity.Critical:
+      return '#d32f2f';
+    case Severity.High:
+      return '#f57c00';
+    case Severity.Medium:
+      return '#fbc02d';
+    case Severity.Low:
+      return '#7cb342';
+    default:
+      return '#757575';
+  }
+};
+
+export const getProgressColor = (percentage: number, forAttackRate: boolean = false): string => {
+  if (forAttackRate) {
+    // For attack success rate, high percentages are bad
+    if (percentage >= 75) {
+      return '#d32f2f';
+    } // Dark Red
+    if (percentage >= 50) {
+      return '#f44336';
+    } // Red
+    if (percentage >= 25) {
+      return '#ff9800';
+    } // Orange
+    if (percentage >= 10) {
+      return '#ffc107';
+    } // Amber
+    return '#4caf50'; // Green (low attack success is good)
+  } else {
+    // For pass rate, high percentages are good
+    if (percentage >= 90) {
+      return '#4caf50';
+    } // Green
+    if (percentage >= 75) {
+      return '#8bc34a';
+    } // Light Green
+    if (percentage >= 50) {
+      return '#ffeb3b';
+    } // Yellow
+    if (percentage >= 25) {
+      return '#ff9800';
+    } // Orange
+    return '#f44336'; // Red
+  }
+};
diff --git a/src/app/src/pages/redteam/report/components/FrameworkCsvExporter.tsx b/src/app/src/pages/redteam/report/components/FrameworkCsvExporter.tsx
new file mode 100644
index 000000000000..d37f5ca40dba
--- /dev/null
+++ b/src/app/src/pages/redteam/report/components/FrameworkCsvExporter.tsx
@@ -0,0 +1,203 @@
+import React from 'react';
+import DownloadIcon from '@mui/icons-material/Download';
+import Button from '@mui/material/Button';
+import {
+  ALIASED_PLUGIN_MAPPINGS,
+  FRAMEWORK_COMPLIANCE_IDS,
+  FRAMEWORK_NAMES,
+  OWASP_LLM_TOP_10_NAMES,
+  OWASP_API_TOP_10_NAMES,
+  riskCategorySeverityMap,
+  Severity,
+} from '@promptfoo/redteam/constants';
+import {
+  type CategoryStats,
+  expandPluginCollections,
+  categorizePlugins,
+  getPluginDisplayName,
+} from './FrameworkComplianceUtils';
+
+interface CSVExporterProps {
+  categoryStats: CategoryStats;
+  pluginPassRateThreshold: number;
+}
+
+const CSVExporter: React.FC<CSVExporterProps> = ({ categoryStats, pluginPassRateThreshold }) => {
+  // Function to export framework compliance data to CSV
+  const exportToCSV = () => {
+    // Collect data for all frameworks
+    const csvRows = [
+      // Header row
+      [
+        'Framework',
+        'Category',
+        'Plugin',
+        'Severity',
+        'Tests Run',
+        'Attacks Successful',
+        'Attack Success Rate (%)',
+        'Status',
+      ],
+    ];
+
+    // Add data rows
+    FRAMEWORK_COMPLIANCE_IDS.forEach((frameworkId) => {
+      const framework = FRAMEWORK_NAMES[frameworkId];
+
+      if (frameworkId === 'owasp:api' || frameworkId === 'owasp:llm') {
+        // Add data for categorized OWASP frameworks
+        Object.entries(ALIASED_PLUGIN_MAPPINGS[frameworkId]).forEach(
+          ([categoryId, { plugins: categoryPlugins }]) => {
+            const categoryNumber = categoryId.split(':').pop();
+            const categoryName =
+              categoryNumber && frameworkId === 'owasp:llm'
+                ? OWASP_LLM_TOP_10_NAMES[Number.parseInt(categoryNumber) - 1]
+                : categoryNumber && frameworkId === 'owasp:api'
+                  ? OWASP_API_TOP_10_NAMES[Number.parseInt(categoryNumber) - 1]
+                  : `Category ${categoryNumber}`;
+
+            // Expand plugins if needed
+            const expandedPlugins = expandPluginCollections(categoryPlugins, categoryStats);
+
+            // Categorize plugins into tested and untested
+            const { compliant, nonCompliant, untested } = categorizePlugins(
+              expandedPlugins,
+              categoryStats,
+              pluginPassRateThreshold,
+            );
+
+            const testedPlugins = [...compliant, ...nonCompliant];
+
+            // Add tested plugins
+            testedPlugins.forEach((plugin) => {
+              const stats = categoryStats[plugin];
+              const pluginSeverity =
+                riskCategorySeverityMap[plugin as keyof typeof riskCategorySeverityMap] ||
+                Severity.Low;
+              const pluginName = getPluginDisplayName(plugin);
+              const attacksSuccessful = stats.total - stats.pass;
+              const asr = ((attacksSuccessful / stats.total) * 100).toFixed(2);
+              const status = stats.pass / stats.total >= pluginPassRateThreshold ? 'Pass' : 'Fail';
+
+              csvRows.push([
+                framework,
+                `${categoryNumber}. ${categoryName}`,
+                pluginName,
+                pluginSeverity,
+                stats.total.toString(),
+                attacksSuccessful.toString(),
+                asr,
+                status,
+              ]);
+            });
+
+            // Add untested plugins
+            untested.forEach((plugin) => {
+              const pluginSeverity =
+                riskCategorySeverityMap[plugin as keyof typeof riskCategorySeverityMap] ||
+                Severity.Low;
+              const pluginName = getPluginDisplayName(plugin);
+
+              csvRows.push([
+                framework,
+                `${categoryNumber}. ${categoryName}`,
+                pluginName,
+                pluginSeverity,
+                '0',
+                '0',
+                '0',
+                'Not Tested',
+              ]);
+            });
+          },
+        );
+      } else {
+        // Add data for other frameworks (without categories)
+        // First get all plugins defined for this framework
+        const frameworkPlugins = new Set<string>();
+        if (ALIASED_PLUGIN_MAPPINGS[frameworkId]) {
+          Object.values(ALIASED_PLUGIN_MAPPINGS[frameworkId]).forEach(({ plugins }) => {
+            const expanded = expandPluginCollections(plugins, categoryStats);
+            expanded.forEach((plugin) => frameworkPlugins.add(plugin));
+          });
+        }
+
+        // Categorize plugins
+        const { compliant, nonCompliant, untested } = categorizePlugins(
+          frameworkPlugins,
+          categoryStats,
+          pluginPassRateThreshold,
+        );
+
+        const testedPlugins = [...compliant, ...nonCompliant];
+
+        // Add tested plugins
+        testedPlugins.forEach((plugin) => {
+          const stats = categoryStats[plugin];
+          const pluginSeverity =
+            riskCategorySeverityMap[plugin as keyof typeof riskCategorySeverityMap] || Severity.Low;
+          const pluginName = getPluginDisplayName(plugin);
+          const attacksSuccessful = stats.total - stats.pass;
+          const asr = ((attacksSuccessful / stats.total) * 100).toFixed(2);
+          const status = stats.pass / stats.total >= pluginPassRateThreshold ? 'Pass' : 'Fail';
+
+          csvRows.push([
+            framework,
+            'N/A',
+            pluginName,
+            pluginSeverity,
+            stats.total.toString(),
+            attacksSuccessful.toString(),
+            asr,
+            status,
+          ]);
+        });
+
+        // Add untested plugins
+        untested.forEach((plugin) => {
+          const pluginSeverity =
+            riskCategorySeverityMap[plugin as keyof typeof riskCategorySeverityMap] || Severity.Low;
+          const pluginName = getPluginDisplayName(plugin);
+
+          csvRows.push([framework, 'N/A', pluginName, pluginSeverity, '0', '0', '0', 'Not Tested']);
+        });
+      }
+    });
+
+    // Convert to CSV string
+    const csvContent = csvRows
+      .map((row) =>
+        row
+          .map((cell) => {
+            // Quote cells that contain commas or quotes
+            if (typeof cell === 'string' && (cell.includes(',') || cell.includes('"'))) {
+              return `"${cell.replace(/"/g, '""')}"`;
+            }
+            return cell;
+          })
+          .join(','),
+      )
+      .join('\n');
+
+    // Create and download the file
+    const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
+    const url = URL.createObjectURL(blob);
+    const link = document.createElement('a');
+    const fileName = `framework-compliance-${new Date().toISOString().slice(0, 10)}.csv`;
+
+    link.setAttribute('href', url);
+    link.setAttribute('download', fileName);
+    link.style.visibility = 'hidden';
+    document.body.appendChild(link);
+    link.click();
+    document.body.removeChild(link);
+  };
+
+  return (
+    <Button variant="contained" color="primary" startIcon={<DownloadIcon />} onClick={exportToCSV}>
+      Export framework results to CSV
+    </Button>
+  );
+};
+
+export default CSVExporter;
diff --git a/src/app/src/pages/redteam/report/components/Report.tsx b/src/app/src/pages/redteam/report/components/Report.tsx
index 0332e40afaf1..189637b4224b 100644
--- a/src/app/src/pages/redteam/report/components/Report.tsx
+++ b/src/app/src/pages/redteam/report/components/Report.tsx
@@ -368,7 +368,6 @@ const App: React.FC = () => {
           </Box>
         </Card>
         <Overview categoryStats={categoryStats} plugins={evalData.config.redteam.plugins || []} />
-        <FrameworkCompliance categoryStats={categoryStats} strategyStats={strategyStats} />
         <StrategyStats
           strategyStats={strategyStats}
           failuresByPlugin={failuresByPlugin}
@@ -386,6 +385,7 @@ const App: React.FC = () => {
           categoryStats={categoryStats}
           plugins={evalData.config.redteam.plugins || []}
         />
+        <FrameworkCompliance categoryStats={categoryStats} strategyStats={strategyStats} />
       </Stack>
       <Modal
         open={isPromptModalOpen}
diff --git a/src/app/src/pages/redteam/report/components/ReportSettingsDialogButton.tsx b/src/app/src/pages/redteam/report/components/ReportSettingsDialogButton.tsx
index dfa9eb49864c..eac11cb3757b 100644
--- a/src/app/src/pages/redteam/report/components/ReportSettingsDialogButton.tsx
+++ b/src/app/src/pages/redteam/report/components/ReportSettingsDialogButton.tsx
@@ -17,8 +17,6 @@ const ReportSettingsDialogButton: React.FC = () => {
     setShowPercentagesOnRiskCards,
     pluginPassRateThreshold,
     setPluginPassRateThreshold,
-    showComplianceSection,
-    setShowComplianceSection,
   } = useReportStore();
   const [open, setOpen] = React.useState(false);
 
@@ -40,17 +38,6 @@ const ReportSettingsDialogButton: React.FC = () => {
       <Dialog open={open} onClose={handleClose} fullWidth maxWidth="sm">
         <DialogTitle>Report Settings</DialogTitle>
         <DialogContent>
-          <Typography component="div" sx={{ padding: '16px 0' }}>
-            <label style={{ display: 'flex', alignItems: 'center', cursor: 'pointer' }}>
-              <input
-                type="checkbox"
-                checked={showComplianceSection}
-                onChange={(e) => setShowComplianceSection(e.target.checked)}
-                style={{ marginRight: '10px' }}
-              />
-              Show compliance section (NIST, OWASP)
-            </label>
-          </Typography>
           <Typography component="div" sx={{ padding: '16px 0' }}>
             <label style={{ display: 'flex', alignItems: 'center', cursor: 'pointer' }}>
               <input
diff --git a/src/app/src/pages/redteam/report/components/store.ts b/src/app/src/pages/redteam/report/components/store.ts
index 237ee974fdb3..ed24446b0a97 100644
--- a/src/app/src/pages/redteam/report/components/store.ts
+++ b/src/app/src/pages/redteam/report/components/store.ts
@@ -20,8 +20,6 @@ interface ReportState {
   setShowPercentagesOnRiskCards: (show: boolean) => void;
   pluginPassRateThreshold: number;
   setPluginPassRateThreshold: (threshold: number) => void;
-  showComplianceSection: boolean;
-  setShowComplianceSection: (show: boolean) => void;
 }
 
 export const useReportStore = create<ReportState>()(
@@ -33,8 +31,6 @@ export const useReportStore = create<ReportState>()(
       pluginPassRateThreshold: 1.0,
       setPluginPassRateThreshold: (threshold: number) =>
         set(() => ({ pluginPassRateThreshold: threshold })),
-      showComplianceSection: false,
-      setShowComplianceSection: (show: boolean) => set(() => ({ showComplianceSection: show })),
     }),
     {
       name: 'ReportViewStorage',
diff --git a/src/redteam/constants.ts b/src/redteam/constants.ts
index 3eb93cf1dde1..7d974527f6fb 100644
--- a/src/redteam/constants.ts
+++ b/src/redteam/constants.ts
@@ -200,6 +200,32 @@ export const FRAMEWORK_NAMES: Record<string, string> = {
   'owasp:llm': 'OWASP LLM Top 10',
 };
 
+export const OWASP_LLM_TOP_10_NAMES = [
+  'Prompt Injection',
+  'Sensitive Information Disclosure',
+  'Supply Chain',
+  'Improper Output Handling',
+  'Insecure Output Handling',
+  'Excessive Agency',
+  'System Prompt Leakage',
+  'Vector and Embedding Weaknesses',
+  'Misinformation',
+  'Unbounded Consumption',
+];
+
+export const OWASP_API_TOP_10_NAMES = [
+  'Broken Object Level Authorization',
+  'Broken Authentication',
+  'Broken Object Property Level Authorization',
+  'Unrestricted Resource Consumption',
+  'Broken Function Level Authorization',
+  'Unrestricted Access to Sensitive Business Flows',
+  'Server Side Request Forgery',
+  'Security Misconfiguration',
+  'Improper Inventory Management',
+  'Unsafe Consumption of APIs',
+];
+
 export const OWASP_LLM_TOP_10_MAPPING: Record<
   string,
   { plugins: Plugin[]; strategies: Strategy[] }


Reviews:
user: login: ellipsis-dev[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T21:44:21Z
user: login: ellipsis-dev[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T23:39:58Z
user: login: ellipsis-dev[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T00:33:41Z
user: login: ellipsis-dev[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T00:36:21Z

Review comments:
diff_hunk: @@ -43,19 +234,72 @@ const FrameworkCompliance: React.FC<FrameworkComplianceProps> = ({
         return [];
       }
 
-      return Array.from(
-        new Set(
-          Object.entries(mappings).flatMap(([_, { plugins, strategies }]) => {
-            const nonCompliantItems = [...plugins, ...strategies].filter((item) => {
-              const stats = categoryStats[item] || strategyStats[item];
-              return stats && stats.total > 0 && stats.pass / stats.total < pluginPassRateThreshold;
-            });
-            return nonCompliantItems;
-          }),
-        ),
-      );
+      // First, collect all plugins from all subcategories
+      const allPlugins = new Set<string>();
+      Object.values(mappings).forEach(({ plugins }) => {
+        plugins.forEach((plugin) => {
+          // Special handling for collection plugins like 'harmful'
+          if (plugin === 'harmful') {
+            // Include all harmful:* plugins
+            Object.keys(categoryStats)
+              .filter((key) => key.startsWith('harmful:'))
+              .forEach((key) => allPlugins.add(key));
+          } else {
+            allPlugins.add(plugin);
+          }
+        });
+      });
+
+      // Then filter for non-compliant ones
+      return Array.from(allPlugins).filter((plugin) => {
+        const stats = categoryStats[plugin];
+        return stats && stats.total > 0 && stats.pass / stats.total < pluginPassRateThreshold;
+      });
+    },
+    [categoryStats, pluginPassRateThreshold],
+  );
+
+  const getPluginPassRate = React.useCallback(
path: src/app/src/pages/redteam/report/components/FrameworkCompliance.tsx
user: login: ellipsis-dev[bot]
body: Consider extracting repeated logic for obtaining plugin severity from `riskCategorySeverityMap` into a helper function to improve maintainability.
created_at: 2025-04-14T21:44:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -43,19 +234,72 @@
         return [];
       }
 
-      return Array.from(
-        new Set(
-          Object.entries(mappings).flatMap(([_, { plugins, strategies }]) => {
-            const nonCompliantItems = [...plugins, ...strategies].filter((item) => {
-              const stats = categoryStats[item] || strategyStats[item];
-              return stats && stats.total > 0 && stats.pass / stats.total < pluginPassRateThreshold;
-            });
-            return nonCompliantItems;
-          }),
-        ),
-      );
+      // First, collect all plugins from all subcategories
+      const allPlugins = new Set<string>();
+      Object.values(mappings).forEach(({ plugins }) => {
+        plugins.forEach((plugin) => {
+          // Special handling for collection plugins like 'harmful'
path: src/app/src/pages/redteam/report/components/FrameworkCompliance.tsx
user: login: ellipsis-dev[bot]
body: The handling for collection plugins (like 'harmful') is repeated in several places. Consider refactoring this logic into a helper to avoid duplication and improve clarity.
created_at: 2025-04-14T21:44:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -43,19 +234,72 @@
         return [];
       }
 
-      return Array.from(
-        new Set(
-          Object.entries(mappings).flatMap(([_, { plugins, strategies }]) => {
-            const nonCompliantItems = [...plugins, ...strategies].filter((item) => {
-              const stats = categoryStats[item] || strategyStats[item];
-              return stats && stats.total > 0 && stats.pass / stats.total < pluginPassRateThreshold;
-            });
-            return nonCompliantItems;
-          }),
-        ),
-      );
+      // First, collect all plugins from all subcategories
+      const allPlugins = new Set<string>();
+      Object.values(mappings).forEach(({ plugins }) => {
+        plugins.forEach((plugin) => {
+          // Special handling for collection plugins like 'harmful'
+          if (plugin === 'harmful') {
+            // Include all harmful:* plugins
+            Object.keys(categoryStats)
+              .filter((key) => key.startsWith('harmful:'))
+              .forEach((key) => allPlugins.add(key));
+          } else {
+            allPlugins.add(plugin);
+          }
+        });
+      });
+
+      // Then filter for non-compliant ones
+      return Array.from(allPlugins).filter((plugin) => {
+        const stats = categoryStats[plugin];
+        return stats && stats.total > 0 && stats.pass / stats.total < pluginPassRateThreshold;
+      });
+    },
+    [categoryStats, pluginPassRateThreshold],
+  );
+
+  const getPluginPassRate = React.useCallback(
+    (plugin: string): { pass: number; total: number; rate: number } => {
+      const stats = categoryStats[plugin] || { pass: 0, total: 0 };
+      return {
+        pass: stats.pass,
+        total: stats.total,
+        rate: stats.total > 0 ? (stats.pass / stats.total) * 100 : 0,
+      };
+    },
+    [categoryStats],
+  );
+
+  const getFrameworkSeverity = React.useCallback(
+    (framework: string): Severity => {
+      const nonCompliantPlugins = getNonCompliantPlugins(framework);
+
+      if (nonCompliantPlugins.length === 0) {
+        return Severity.Low;
+      }
+
+      // Find the highest severity among non-compliant plugins
+      let highestSeverity = Severity.Low;
+
+      for (const plugin of nonCompliantPlugins) {
+        const pluginSeverity =
+          riskCategorySeverityMap[plugin as keyof typeof riskCategorySeverityMap] || Severity.Low;
path: src/app/src/pages/redteam/report/components/FrameworkCompliance.tsx
user: login: ellipsis-dev[bot]
body: Multiple type casts like `plugin as keyof typeof riskCategorySeverityMap` are used. Consider refining types to avoid repetitive casts and improve type safety.
created_at: 2025-04-14T21:44:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -29,12 +35,265 @@ interface FrameworkComplianceProps {
   strategyStats: Record<string, { pass: number; total: number }>;
 }
 
+const FRAMEWORK_DESCRIPTIONS: Record<string, string> = {
+  'mitre:atlas': 'MITRE ATLAS framework for adversarial threat landscape for AI systems',
+  'nist:ai:measure': 'NIST AI Risk Management Framework for responsible AI development',
+  'owasp:api': 'OWASP API Top 10 security risks for application programming interfaces',
+  'owasp:llm': 'OWASP LLM Top 10 security vulnerabilities for large language models',
+};
+
+const getSeverityColor = (severity: Severity): string => {
+  switch (severity) {
+    case Severity.Critical:
+      return '#d32f2f';
+    case Severity.High:
+      return '#f57c00';
+    case Severity.Medium:
+      return '#fbc02d';
+    case Severity.Low:
+      return '#7cb342';
+    default:
+      return '#757575';
+  }
+};
+
+const getProgressColor = (percentage: number, forAttackRate: boolean = false): string => {
+  if (forAttackRate) {
+    // For attack success rate, high percentages are bad
+    if (percentage >= 75) {
+      return '#d32f2f';
+    } // Dark Red
+    if (percentage >= 50) {
+      return '#f44336';
+    } // Red
+    if (percentage >= 25) {
+      return '#ff9800';
+    } // Orange
+    if (percentage >= 10) {
+      return '#ffc107';
+    } // Amber
+    return '#4caf50'; // Green (low attack success is good)
+  } else {
+    // For pass rate, high percentages are good
+    if (percentage >= 90) {
+      return '#4caf50';
+    } // Green
+    if (percentage >= 75) {
+      return '#8bc34a';
+    } // Light Green
+    if (percentage >= 50) {
+      return '#ffeb3b';
+    } // Yellow
+    if (percentage >= 25) {
+      return '#ff9800';
+    } // Orange
+    return '#f44336'; // Red
+  }
+};
+
 const FrameworkCompliance: React.FC<FrameworkComplianceProps> = ({
   categoryStats,
   strategyStats,
 }) => {
-  const { pluginPassRateThreshold, showComplianceSection } = useReportStore();
-  const [expandedFrameworks, setExpandedFrameworks] = useState<Record<string, boolean>>({});
+  const { pluginPassRateThreshold } = useReportStore();
+
+  // Function to export framework compliance data to CSV
+  const exportToCSV = () => {
+    // Collect data for all frameworks
+    const csvRows = [
+      // Header row
+      [
+        'Framework',
+        'Category',
+        'Plugin',
+        'Severity',
+        'Tests Run',
+        'Attacks Successful',
+        'Attack Success Rate (%)',
+        'Status',
+      ],
+    ];
+
+    // Add data rows
+    FRAMEWORK_COMPLIANCE_IDS.forEach((frameworkId) => {
+      const framework = FRAMEWORK_NAMES[frameworkId];
+
+      if (frameworkId === 'owasp:api' || frameworkId === 'owasp:llm') {
+        // Add data for categorized OWASP frameworks
+        Object.entries(ALIASED_PLUGIN_MAPPINGS[frameworkId]).forEach(
+          ([categoryId, { plugins: categoryPlugins }]) => {
+            const categoryNumber = categoryId.split(':').pop();
+            const categoryName =
+              categoryNumber && frameworkId === 'owasp:llm'
+                ? OWASP_LLM_TOP_10_NAMES[Number.parseInt(categoryNumber) - 1]
+                : categoryNumber && frameworkId === 'owasp:api'
+                  ? OWASP_API_TOP_10_NAMES[Number.parseInt(categoryNumber) - 1]
+                  : `Category ${categoryNumber}`;
+
+            // Expand plugins if needed
+            const expandedPlugins = new Set<string>();
+            categoryPlugins.forEach((plugin) => {
+              if (plugin === 'harmful') {
+                // Add all harmful:* plugins that have stats
+                Object.keys(categoryStats)
+                  .filter((key) => key.startsWith('harmful:'))
+                  .forEach((key) => expandedPlugins.add(key));
+              } else {
+                expandedPlugins.add(plugin);
+              }
+            });
+
+            // Categorize plugins into tested and untested
+            const testedPlugins = Array.from(expandedPlugins).filter(
path: src/app/src/pages/redteam/report/components/FrameworkCompliance.tsx
user: login: ellipsis-dev[bot]
body: The plugin categorization (tested vs. untested) logic is repeated for categorized and non-categorized frameworks. Consider extracting this logic into a reusable utility function.
created_at: 2025-04-14T23:39:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,203 @@
+import React from 'react';
+import DownloadIcon from '@mui/icons-material/Download';
+import Button from '@mui/material/Button';
+import {
+  ALIASED_PLUGIN_MAPPINGS,
+  FRAMEWORK_COMPLIANCE_IDS,
+  FRAMEWORK_NAMES,
+  OWASP_LLM_TOP_10_NAMES,
+  OWASP_API_TOP_10_NAMES,
+  riskCategorySeverityMap,
+  Severity,
+} from '@promptfoo/redteam/constants';
+import {
+  type CategoryStats,
+  expandPluginCollections,
+  categorizePlugins,
+  getPluginDisplayName,
+} from './FrameworkComplianceUtils';
+
+interface CSVExporterProps {
+  categoryStats: CategoryStats;
+  pluginPassRateThreshold: number;
+}
+
+const CSVExporter: React.FC<CSVExporterProps> = ({ categoryStats, pluginPassRateThreshold }) => {
+  // Function to export framework compliance data to CSV
+  const exportToCSV = () => {
+    // Collect data for all frameworks
+    const csvRows = [
+      // Header row
+      [
+        'Framework',
+        'Category',
+        'Plugin',
+        'Severity',
+        'Tests Run',
+        'Attacks Successful',
+        'Attack Success Rate (%)',
+        'Status',
+      ],
+    ];
+
+    // Add data rows
+    FRAMEWORK_COMPLIANCE_IDS.forEach((frameworkId) => {
+      const framework = FRAMEWORK_NAMES[frameworkId];
+
+      if (frameworkId === 'owasp:api' || frameworkId === 'owasp:llm') {
+        // Add data for categorized OWASP frameworks
+        Object.entries(ALIASED_PLUGIN_MAPPINGS[frameworkId]).forEach(
+          ([categoryId, { plugins: categoryPlugins }]) => {
+            const categoryNumber = categoryId.split(':').pop();
+            const categoryName =
+              categoryNumber && frameworkId === 'owasp:llm'
+                ? OWASP_LLM_TOP_10_NAMES[Number.parseInt(categoryNumber) - 1]
+                : categoryNumber && frameworkId === 'owasp:api'
+                  ? OWASP_API_TOP_10_NAMES[Number.parseInt(categoryNumber) - 1]
+                  : `Category ${categoryNumber}`;
+
+            // Expand plugins if needed
+            const expandedPlugins = expandPluginCollections(categoryPlugins, categoryStats);
+
+            // Categorize plugins into tested and untested
+            const { compliant, nonCompliant, untested } = categorizePlugins(
+              expandedPlugins,
+              categoryStats,
+              pluginPassRateThreshold,
+            );
+
+            const testedPlugins = [...compliant, ...nonCompliant];
+
+            // Add tested plugins
+            testedPlugins.forEach((plugin) => {
+              const stats = categoryStats[plugin];
+              const pluginSeverity =
+                riskCategorySeverityMap[plugin as keyof typeof riskCategorySeverityMap] ||
+                Severity.Low;
+              const pluginName = getPluginDisplayName(plugin);
+              const attacksSuccessful = stats.total - stats.pass;
+              const asr = ((attacksSuccessful / stats.total) * 100).toFixed(2);
+              const status = stats.pass / stats.total >= pluginPassRateThreshold ? 'Pass' : 'Fail';
+
+              csvRows.push([
+                framework,
+                `${categoryNumber}. ${categoryName}`,
+                pluginName,
+                pluginSeverity,
+                stats.total.toString(),
+                attacksSuccessful.toString(),
+                asr,
+                status,
+              ]);
+            });
+
+            // Add untested plugins
+            untested.forEach((plugin) => {
+              const pluginSeverity =
+                riskCategorySeverityMap[plugin as keyof typeof riskCategorySeverityMap] ||
+                Severity.Low;
+              const pluginName = getPluginDisplayName(plugin);
+
+              csvRows.push([
+                framework,
+                `${categoryNumber}. ${categoryName}`,
+                pluginName,
+                pluginSeverity,
+                '0',
+                '0',
+                '0',
+                'Not Tested',
+              ]);
+            });
+          },
+        );
+      } else {
+        // Add data for other frameworks (without categories)
+        // First get all plugins defined for this framework
+        const frameworkPlugins = new Set<string>();
+        if (ALIASED_PLUGIN_MAPPINGS[framework]) {
+          Object.values(ALIASED_PLUGIN_MAPPINGS[framework]).forEach(({ plugins }) => {
+            const expanded = expandPluginCollections(plugins, categoryStats);
+            expanded.forEach((plugin) => frameworkPlugins.add(plugin));
+          });
+        }
+
+        // Categorize plugins
+        const { compliant, nonCompliant, untested } = categorizePlugins(
+          frameworkPlugins,
+          categoryStats,
+          pluginPassRateThreshold,
+        );
+
+        const testedPlugins = [...compliant, ...nonCompliant];
+
+        // Add tested plugins
+        testedPlugins.forEach((plugin) => {
+          const stats = categoryStats[plugin];
+          const pluginSeverity =
+            riskCategorySeverityMap[plugin as keyof typeof riskCategorySeverityMap] || Severity.Low;
+          const pluginName = getPluginDisplayName(plugin);
+          const attacksSuccessful = stats.total - stats.pass;
+          const asr = ((attacksSuccessful / stats.total) * 100).toFixed(2);
+          const status = stats.pass / stats.total >= pluginPassRateThreshold ? 'Pass' : 'Fail';
+
+          csvRows.push([
+            framework,
+            'N/A',
+            pluginName,
+            pluginSeverity,
+            stats.total.toString(),
+            attacksSuccessful.toString(),
+            asr,
+            status,
+          ]);
+        });
+
+        // Add untested plugins
+        untested.forEach((plugin) => {
+          const pluginSeverity =
+            riskCategorySeverityMap[plugin as keyof typeof riskCategorySeverityMap] || Severity.Low;
+          const pluginName = getPluginDisplayName(plugin);
+
+          csvRows.push([framework, 'N/A', pluginName, pluginSeverity, '0', '0', '0', 'Not Tested']);
+        });
+      }
+    });
+
+    // Convert to CSV string
+    const csvContent = csvRows
+      .map((row) =>
+        row
+          .map((cell) => {
+            // Quote cells that contain commas or quotes
+            if (typeof cell === 'string' && (cell.includes(',') || cell.includes('"'))) {
+              return `"${cell.replace(/"/g, '""')}"`;
+            }
+            return cell;
+          })
+          .join(','),
+      )
+      .join('\n');
+
+    // Create and download the file
+    const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
+    const url = URL.createObjectURL(blob);
+    const link = document.createElement('a');
+    const fileName = `framework-compliance-${new Date().toISOString().slice(0, 10)}.csv`;
+
+    link.setAttribute('href', url);
+    link.setAttribute('download', fileName);
+    link.style.visibility = 'hidden';
+    document.body.appendChild(link);
+    link.click();
+    document.body.removeChild(link);
path: src/app/src/pages/redteam/report/components/CSVExporter.tsx
user: login: ellipsis-dev[bot]
body: Consider revoking the generated object URL after download to avoid potential memory leaks.
created_at: 2025-04-15T00:33:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,203 @@
+import React from 'react';
+import DownloadIcon from '@mui/icons-material/Download';
+import Button from '@mui/material/Button';
+import {
+  ALIASED_PLUGIN_MAPPINGS,
+  FRAMEWORK_COMPLIANCE_IDS,
+  FRAMEWORK_NAMES,
+  OWASP_LLM_TOP_10_NAMES,
+  OWASP_API_TOP_10_NAMES,
+  riskCategorySeverityMap,
+  Severity,
+} from '@promptfoo/redteam/constants';
+import {
+  type CategoryStats,
+  expandPluginCollections,
+  categorizePlugins,
+  getPluginDisplayName,
+} from './FrameworkComplianceUtils';
+
+interface CSVExporterProps {
+  categoryStats: CategoryStats;
+  pluginPassRateThreshold: number;
+}
+
+const CSVExporter: React.FC<CSVExporterProps> = ({ categoryStats, pluginPassRateThreshold }) => {
+  // Function to export framework compliance data to CSV
+  const exportToCSV = () => {
+    // Collect data for all frameworks
+    const csvRows = [
+      // Header row
+      [
+        'Framework',
+        'Category',
+        'Plugin',
+        'Severity',
+        'Tests Run',
+        'Attacks Successful',
+        'Attack Success Rate (%)',
+        'Status',
+      ],
+    ];
+
+    // Add data rows
+    FRAMEWORK_COMPLIANCE_IDS.forEach((frameworkId) => {
+      const framework = FRAMEWORK_NAMES[frameworkId];
+
+      if (frameworkId === 'owasp:api' || frameworkId === 'owasp:llm') {
+        // Add data for categorized OWASP frameworks
+        Object.entries(ALIASED_PLUGIN_MAPPINGS[frameworkId]).forEach(
+          ([categoryId, { plugins: categoryPlugins }]) => {
+            const categoryNumber = categoryId.split(':').pop();
+            const categoryName =
+              categoryNumber && frameworkId === 'owasp:llm'
+                ? OWASP_LLM_TOP_10_NAMES[Number.parseInt(categoryNumber) - 1]
+                : categoryNumber && frameworkId === 'owasp:api'
+                  ? OWASP_API_TOP_10_NAMES[Number.parseInt(categoryNumber) - 1]
+                  : `Category ${categoryNumber}`;
+
+            // Expand plugins if needed
+            const expandedPlugins = expandPluginCollections(categoryPlugins, categoryStats);
+
+            // Categorize plugins into tested and untested
+            const { compliant, nonCompliant, untested } = categorizePlugins(
+              expandedPlugins,
+              categoryStats,
+              pluginPassRateThreshold,
+            );
+
+            const testedPlugins = [...compliant, ...nonCompliant];
+
+            // Add tested plugins
+            testedPlugins.forEach((plugin) => {
+              const stats = categoryStats[plugin];
+              const pluginSeverity =
+                riskCategorySeverityMap[plugin as keyof typeof riskCategorySeverityMap] ||
+                Severity.Low;
+              const pluginName = getPluginDisplayName(plugin);
+              const attacksSuccessful = stats.total - stats.pass;
+              const asr = ((attacksSuccessful / stats.total) * 100).toFixed(2);
+              const status = stats.pass / stats.total >= pluginPassRateThreshold ? 'Pass' : 'Fail';
+
+              csvRows.push([
+                framework,
+                `${categoryNumber}. ${categoryName}`,
+                pluginName,
+                pluginSeverity,
+                stats.total.toString(),
+                attacksSuccessful.toString(),
+                asr,
+                status,
+              ]);
+            });
+
+            // Add untested plugins
+            untested.forEach((plugin) => {
+              const pluginSeverity =
+                riskCategorySeverityMap[plugin as keyof typeof riskCategorySeverityMap] ||
+                Severity.Low;
+              const pluginName = getPluginDisplayName(plugin);
+
+              csvRows.push([
+                framework,
+                `${categoryNumber}. ${categoryName}`,
+                pluginName,
+                pluginSeverity,
+                '0',
+                '0',
+                '0',
+                'Not Tested',
+              ]);
+            });
+          },
+        );
+      } else {
+        // Add data for other frameworks (without categories)
+        // First get all plugins defined for this framework
+        const frameworkPlugins = new Set<string>();
+        if (ALIASED_PLUGIN_MAPPINGS[framework]) {
path: src/app/src/pages/redteam/report/components/CSVExporter.tsx
user: login: ellipsis-dev[bot]
body: Use the framework ID (i.e. `'frameworkId'`) as the key to index `ALIASED_PLUGIN_MAPPINGS` instead of the display name `'framework'`.
created_at: 2025-04-15T00:33:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,666 @@
+import React from 'react';
+import CancelIcon from '@mui/icons-material/Cancel';
+import CheckCircleIcon from '@mui/icons-material/CheckCircle';
+import InfoIcon from '@mui/icons-material/Info';
+import Box from '@mui/material/Box';
+import Card from '@mui/material/Card';
+import CardContent from '@mui/material/CardContent';
+import Chip from '@mui/material/Chip';
+import List from '@mui/material/List';
+import ListItem from '@mui/material/ListItem';
+import ListItemIcon from '@mui/material/ListItemIcon';
+import ListItemText from '@mui/material/ListItemText';
+import Tooltip from '@mui/material/Tooltip';
+import Typography from '@mui/material/Typography';
+import {
+  ALIASED_PLUGIN_MAPPINGS,
+  FRAMEWORK_NAMES,
+  OWASP_LLM_TOP_10_NAMES,
+  OWASP_API_TOP_10_NAMES,
+  riskCategorySeverityMap,
+  Severity,
+} from '@promptfoo/redteam/constants';
+import {
+  type CategoryStats,
+  expandPluginCollections,
+  categorizePlugins,
+  getPluginDisplayName,
+  getSeverityColor,
+  FRAMEWORK_DESCRIPTIONS,
+} from './FrameworkComplianceUtils.ts';
path: src/app/src/pages/redteam/report/components/FrameworkCard.tsx
user: login: ellipsis-dev[bot]
body: Avoid specifying file extension in import paths. Remove '.ts' for consistency with TS module resolution.
```suggestion
} from './FrameworkComplianceUtils';
```
created_at: 2025-04-15T00:36:21Z
author_association: CONTRIBUTOR

PR comments:
user: login: gru-agent[bot]
created_at: 2025-04-14T21:41:54Z
body: ## TestGru Assignment

### Summary

| Link | CommitId | Status | Reason |
| ---- | -------- | ------ | ------ |
| [Detail](https://gru.ai/:test/promptfoo@github/promptfoo/0c26c5a9-fe3f-4401-aa88-901e1766ea48) | ff0caceacbdb52a64b303a6bddab9055a1bdb182 | 🚫 Skipped | No files need to be tested {"src/app/src/pages/redteam/report/components/FrameworkCompliance.css":"target file(src/app/src/pages/redteam/report/components/FrameworkCompliance.css) not in work scope \n include: src \n exclude: node_modules,**/*.test.ts,**/*.test.tsx,**/*.spec.ts,**/*.spec.tsx,**/*.d.ts,**/*.test.js,**/*.spec.js,src/app/src","src/app/src/pages/redteam/report/components/FrameworkCompliance.tsx":"target file(src/app/src/pages/redteam/report/components/FrameworkCompliance.tsx) not in work scope \n include: src \n exclude: node_modules,**/*.test.ts,**/*.test.tsx,**/*.spec.ts,**/*.spec.tsx,**/*.d.ts,**/*.test.js,**/*.spec.js,src/app/src","src/app/src/pages/redteam/report/components/Report.tsx":"target file(src/app/src/pages/redteam/report/components/Report.tsx) not in work scope \n include: src \n exclude: node_modules,**/*.test.ts,**/*.test.tsx,**/*.spec.ts,**/*.spec.tsx,**/*.d.ts,**/*.test.js,**/*.spec.js,src/app/src","src/app/src/pages/redteam/report/components/store.ts":"… |

> [!TIP]
> You can `@gru-agent` and leave your feedback. TestGru will make adjustments based on your input

author_association: CONTRIBUTOR
user: login: gru-agent[bot]
created_at: 2025-04-14T21:41:57Z
body: ## TestGru Assignment

### Summary

| Link | CommitId | Status | Reason |
| ---- | -------- | ------ | ------ |
| [Detail](https://gru.ai/:test/promptfoo@github/promptfoo/6c02da43-e424-4715-8762-aa4bcbae6673) | ff0caceacbdb52a64b303a6bddab9055a1bdb182 | 🚫 Skipped | No files need to be tested {"src/app/src/pages/redteam/report/components/FrameworkCompliance.css":"target file(src/app/src/pages/redteam/report/components/FrameworkCompliance.css) not in work scope \n include: src/app/src/**/*.ts \n exclude: node_modules,**/*.test.ts,**/*.test.tsx,**/*.spec.ts,**/*.spec.tsx,**/*.d.ts,**/*.test.js,**/*.spec.js","src/app/src/pages/redteam/report/components/FrameworkCompliance.tsx":"target file(src/app/src/pages/redteam/report/components/FrameworkCompliance.tsx) not in work scope \n include: src/app/src/**/*.ts \n exclude: node_modules,**/*.test.ts,**/*.test.tsx,**/*.spec.ts,**/*.spec.tsx,**/*.d.ts,**/*.test.js,**/*.spec.js","src/app/src/pages/redteam/report/components/Report.tsx":"target file(src/app/src/pages/redteam/report/components/Report.tsx) not in work scope \n include: src/app/src/**/*.ts \n exclude: node_modules,**/*.test.ts,**/*.test.tsx,**/*.spec.ts,**/*.spec.tsx,**/*.d.ts,**/*.test.js,**/*.spec.js","src/app/src/pages/redteam/report/components… |

> [!TIP]
> You can `@gru-agent` and leave your feedback. TestGru will make adjustments based on your input

author_association: CONTRIBUTOR