diff --git a/gitnexus-web/src/components/MermaidDiagram.tsx b/gitnexus-web/src/components/MermaidDiagram.tsx index ddc29aac9f..0c6ebc239a 100644 --- a/gitnexus-web/src/components/MermaidDiagram.tsx +++ b/gitnexus-web/src/components/MermaidDiagram.tsx @@ -1,5 +1,6 @@ import { useEffect, useRef, useState } from 'react'; import mermaid from 'mermaid'; +import DOMPurify from 'dompurify'; import { AlertTriangle, Maximize2 } from 'lucide-react'; import { ProcessFlowModal } from './ProcessFlowModal'; import type { ProcessData } from '../lib/mermaid-generator'; @@ -140,7 +141,7 @@ export const MermaidDiagram = ({ code }: MermaidDiagramProps) => {
diff --git a/gitnexus-web/src/components/ProcessFlowModal.tsx b/gitnexus-web/src/components/ProcessFlowModal.tsx index c55bf9ef5c..69de584632 100644 --- a/gitnexus-web/src/components/ProcessFlowModal.tsx +++ b/gitnexus-web/src/components/ProcessFlowModal.tsx @@ -138,8 +138,8 @@ export const ProcessFlowModal = ({ process, onClose, onFocusInGraph, isFullScree const renderDiagram = async () => { try { // Check if we have raw mermaid code (from AI chat) or need to generate it - const mermaidCode = (process as any).rawMermaid - ? (process as any).rawMermaid + const mermaidCode = process.rawMermaid + ? process.rawMermaid : generateProcessMermaid(process); const id = `mermaid-${Date.now()}`; diff --git a/gitnexus-web/src/components/ProcessesPanel.tsx b/gitnexus-web/src/components/ProcessesPanel.tsx index 4f3c851d99..39ff5f7577 100644 --- a/gitnexus-web/src/components/ProcessesPanel.tsx +++ b/gitnexus-web/src/components/ProcessesPanel.tsx @@ -12,7 +12,7 @@ import { ProcessFlowModal } from './ProcessFlowModal'; import type { ProcessData, ProcessStep } from '../lib/mermaid-generator'; /** Validate that an ID contains only expected node identifier characters (no Cypher metacharacters or spaces) */ -const isSafeId = (id: string): boolean => /^[a-zA-Z0-9_:.\-/]+$/.test(id); +const isSafeId = (id: string): boolean => /^[a-zA-Z0-9_:.\-/@]+$/.test(id); export const ProcessesPanel = () => { const { graph, runQuery, setHighlightedNodeIds, highlightedNodeIds } = useAppState(); diff --git a/gitnexus-web/src/lib/mermaid-generator.ts b/gitnexus-web/src/lib/mermaid-generator.ts index 4b34a718d5..d8b6075421 100644 --- a/gitnexus-web/src/lib/mermaid-generator.ts +++ b/gitnexus-web/src/lib/mermaid-generator.ts @@ -26,6 +26,7 @@ export interface ProcessData { steps: ProcessStep[]; edges?: ProcessEdge[]; // CALLS edges between steps for branching clusters?: string[]; + rawMermaid?: string; // AI-generated mermaid code (sanitized before rendering) } /** diff --git a/gitnexus-web/test/unit/security-guards.test.ts b/gitnexus-web/test/unit/security-guards.test.ts index 4f6a2fe06b..46dd18b3f5 100644 --- a/gitnexus-web/test/unit/security-guards.test.ts +++ b/gitnexus-web/test/unit/security-guards.test.ts @@ -18,7 +18,7 @@ const validRelType = (t: string): boolean => (REL_TYPES as readonly string[]).includes(t); const isSafeId = (id: string): boolean => - /^[a-zA-Z0-9_:.\-/]+$/.test(id); + /^[a-zA-Z0-9_:.\-/@]+$/.test(id); const isWriteQuery = (cypher: string): boolean => /\b(CREATE|DELETE|SET|MERGE|REMOVE|DROP|DETACH)\b/.test(cypher.toUpperCase()); @@ -104,13 +104,14 @@ describe('isSafeId – identifier allowlist regex', () => { ['community id', 'comm_5'], ['file path id', 'File:src/index.ts'], ['nested path id', 'Function:src/utils/helpers.ts:doStuff'], + ['scoped npm package', 'Module:@scope/pkg'], + ['angular-style id', 'Module:@angular/core'], ])('accepts valid ID: %s', (_desc, id) => { expect(isSafeId(id)).toBe(true); }); it.each([ ['with spaces', 'Process:my process'], - ['with @ symbol', 'Module:@scope/pkg'], ])('rejects ID with unsafe chars: %s', (_desc, id) => { expect(isSafeId(id)).toBe(false); });