diff --git a/frontend/package.json b/frontend/package.json
index c922063..4da3c9d 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -15,8 +15,10 @@
"lucide-react": "^0.563.0",
"react": "^19.2.4",
"react-dom": "^19.2.4",
+ "react-markdown": "^9.0.1",
"react-router-dom": "^7.13.0",
- "recharts": "^2.15.4"
+ "recharts": "^2.15.4",
+ "remark-gfm": "^4.0.1"
},
"devDependencies": {
"@tailwindcss/vite": "^4.1.18",
diff --git a/frontend/src/components/MarkdownText.tsx b/frontend/src/components/MarkdownText.tsx
new file mode 100644
index 0000000..3068984
--- /dev/null
+++ b/frontend/src/components/MarkdownText.tsx
@@ -0,0 +1,108 @@
+/**
+ * MarkdownText — renders a markdown string as styled HTML.
+ *
+ * Uses react-markdown + remark-gfm (tables, strikethrough, etc.).
+ * External links always open in a new tab with rel="noopener noreferrer".
+ *
+ * Usage:
+ *
+ *
+ */
+
+import ReactMarkdown from "react-markdown";
+import remarkGfm from "remark-gfm";
+import type { Components } from "react-markdown";
+
+interface MarkdownTextProps {
+ content: string | null | undefined;
+ /** Extra classes on the wrapper div */
+ className?: string;
+}
+
+const components: Components = {
+ // Links: always open external URLs in a new tab
+ a: ({ href, children, ...props }) => (
+
+ {children}
+
+ ),
+ // Paragraphs: keep existing text colour, add spacing
+ p: ({ children }) => (
+
{children}
+ ),
+ // Bold
+ strong: ({ children }) => (
+ {children}
+ ),
+ // Italic
+ em: ({ children }) => (
+ {children}
+ ),
+ // Inline code
+ code: ({ children, className }) => {
+ const isBlock = className?.startsWith("language-");
+ if (isBlock) {
+ return (
+
+ {children}
+
+ );
+ }
+ return (
+
+ {children}
+
+ );
+ },
+ // Code blocks
+ pre: ({ children }) => (
+
+ {children}
+
+ ),
+ // Unordered lists
+ ul: ({ children }) => (
+
+ ),
+ // Ordered lists
+ ol: ({ children }) => (
+ {children}
+ ),
+ li: ({ children }) => {children},
+ // Headings (rarely appear in descriptions but handle them gracefully)
+ h1: ({ children }) => (
+ {children}
+ ),
+ h2: ({ children }) => (
+ {children}
+ ),
+ h3: ({ children }) => (
+ {children}
+ ),
+ // Blockquotes
+ blockquote: ({ children }) => (
+
+ {children}
+
+ ),
+ // Horizontal rule
+ hr: () =>
,
+};
+
+export default function MarkdownText({ content, className }: MarkdownTextProps) {
+ if (!content) return null;
+
+ return (
+
+
+ {content}
+
+
+ );
+}
diff --git a/frontend/src/components/test-detail/TeamTabs.tsx b/frontend/src/components/test-detail/TeamTabs.tsx
index c0921d2..4b25291 100644
--- a/frontend/src/components/test-detail/TeamTabs.tsx
+++ b/frontend/src/components/test-detail/TeamTabs.tsx
@@ -1,5 +1,6 @@
import { useState } from "react";
import { useQuery } from "@tanstack/react-query";
+import MarkdownText from "../MarkdownText";
import {
Shield,
ShieldCheck,
@@ -205,7 +206,7 @@ export default function TeamTabs({
placeholder="Summarize the Red Team findings..."
/>
) : (
- {test.red_summary || "No summary yet."}
+
)}
@@ -250,7 +251,7 @@ export default function TeamTabs({
{test.red_validation_notes && (
- {test.red_validation_notes}
+
)}
)}
@@ -317,7 +318,7 @@ export default function TeamTabs({
placeholder="Summarize the Blue Team analysis..."
/>
) : (
- {test.blue_summary || "No summary yet."}
+
)}
@@ -424,7 +425,7 @@ export default function TeamTabs({
{test.blue_validation_notes && (
- {test.blue_validation_notes}
+
)}
)}
@@ -461,7 +462,7 @@ export default function TeamTabs({
Summary
- {test.red_summary || "N/A"}
+
Evidence Files
@@ -512,7 +513,7 @@ export default function TeamTabs({
Summary
- {test.blue_summary || "N/A"}
+
Evidence Files
diff --git a/frontend/src/pages/CampaignDetailPage.tsx b/frontend/src/pages/CampaignDetailPage.tsx
index 0d59327..382df27 100644
--- a/frontend/src/pages/CampaignDetailPage.tsx
+++ b/frontend/src/pages/CampaignDetailPage.tsx
@@ -1,5 +1,6 @@
import { useState } from "react";
import { useParams, useNavigate } from "react-router-dom";
+import MarkdownText from "../components/MarkdownText";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import {
Loader2,
@@ -236,7 +237,7 @@ export default function CampaignDetailPage() {
{campaign.description && (
- {campaign.description}
+
)}
{campaign.threat_actor_name && (
diff --git a/frontend/src/pages/TechniqueDetailPage.tsx b/frontend/src/pages/TechniqueDetailPage.tsx
index 8adb4a0..285437a 100644
--- a/frontend/src/pages/TechniqueDetailPage.tsx
+++ b/frontend/src/pages/TechniqueDetailPage.tsx
@@ -1,5 +1,6 @@
import { useState } from "react";
import { useParams, useNavigate } from "react-router-dom";
+import MarkdownText from "../components/MarkdownText";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import {
Loader2,
@@ -182,9 +183,10 @@ export default function TechniqueDetailPage() {
{/* Description */}
Description
-
- {technique.description || "No description available."}
-
+
{/* Metadata */}
@@ -579,7 +581,7 @@ function DetectionRulesSection({ rules }: { rules: DetectionRule[] }) {
onClick={(e) => e.stopPropagation()}
>
{rule.description && (
-
{rule.description}
+
)}
{rule.rule_format && (
@@ -702,7 +704,7 @@ function D3FENDSection({ defenses }: { defenses: Array<{
{isExpanded && (
{def.description ? (
-
{def.description}
+
) : (
No description available.
)}
diff --git a/frontend/src/pages/TestDetailPage.tsx b/frontend/src/pages/TestDetailPage.tsx
index 5be6620..da6e5e7 100644
--- a/frontend/src/pages/TestDetailPage.tsx
+++ b/frontend/src/pages/TestDetailPage.tsx
@@ -1,4 +1,5 @@
import { useParams, useNavigate } from "react-router-dom";
+import MarkdownText from "../components/MarkdownText";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { useState, useEffect, useCallback } from "react";
import { Loader2, AlertCircle, ArrowLeft } from "lucide-react";
@@ -441,8 +442,8 @@ export default function TestDetailPage() {
- Description
- -
- {test.description || "\u2014"}
+
-
+
diff --git a/frontend/src/pages/ThreatActorDetailPage.tsx b/frontend/src/pages/ThreatActorDetailPage.tsx
index fd1af5f..f4a602f 100644
--- a/frontend/src/pages/ThreatActorDetailPage.tsx
+++ b/frontend/src/pages/ThreatActorDetailPage.tsx
@@ -1,4 +1,5 @@
import { useParams, useNavigate } from "react-router-dom";
+import MarkdownText from "../components/MarkdownText";
import { useQuery } from "@tanstack/react-query";
import {
Loader2,
@@ -263,9 +264,7 @@ export default function ThreatActorDetailPage() {
Description
-
- {actor.description}
-
+
)}