From db208b9f5ca238d586383424aa01a3154851092d Mon Sep 17 00:00:00 2001 From: kitos Date: Fri, 29 May 2026 08:38:53 +0200 Subject: [PATCH] feat(frontend): render markdown in description and summary fields MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - New shared MarkdownText component (react-markdown + remark-gfm) that renders links, bold, italic, lists, code, blockquotes. External links open in a new tab with rel=noopener. - Applied to: technique description, threat actor description, test description, campaign description, detection rule descriptions, D3FEND defense descriptions, red/blue summaries and validation notes. - procedure_text (code/commands) stays in
 — not processed as MD.

Co-Authored-By: Claude Sonnet 4.6 
---
 frontend/package.json                         |   4 +-
 frontend/src/components/MarkdownText.tsx      | 108 ++++++++++++++++++
 .../src/components/test-detail/TeamTabs.tsx   |  13 ++-
 frontend/src/pages/CampaignDetailPage.tsx     |   3 +-
 frontend/src/pages/TechniqueDetailPage.tsx    |  12 +-
 frontend/src/pages/TestDetailPage.tsx         |   5 +-
 frontend/src/pages/ThreatActorDetailPage.tsx  |   5 +-
 7 files changed, 132 insertions(+), 18 deletions(-)
 create mode 100644 frontend/src/components/MarkdownText.tsx

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 }) => ( +
    {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} -

    +
    )}