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} -

    +
    )}