From 08c4540ea68102981114a18ba2d29bac297da13c Mon Sep 17 00:00:00 2001 From: Daniel Rosenthal Date: Sat, 31 Jan 2026 04:01:29 +0100 Subject: [PATCH 1/8] added relationship explorer --- explorer.html | 979 ++++++++++++++++++++++++++++++++++++++++++++++++++ favicon.ico | Bin 0 -> 15406 bytes index.html | 95 +++-- 3 files changed, 1052 insertions(+), 22 deletions(-) create mode 100644 explorer.html create mode 100644 favicon.ico diff --git a/explorer.html b/explorer.html new file mode 100644 index 0000000..33721d2 --- /dev/null +++ b/explorer.html @@ -0,0 +1,979 @@ + + + + + + Relationship Explorer - AttackFlow + + + +
+
+
+
Relationship Explorer
+
+ +
+ +
+
+
+ +
+
+
+
+ +
+
Relationship Graph
+
+
+
+
+ +
+
Details
+
+
Loading data...
+
+
+
+ + + + diff --git a/favicon.ico b/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..0432a0411ffb5c933db30126b2ce0cb04d72d79a GIT binary patch literal 15406 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 literal 0 HcmV?d00001 diff --git a/index.html b/index.html index 2cd497f..829eec0 100644 --- a/index.html +++ b/index.html @@ -62,6 +62,18 @@ gap: 12px; } + .header-brand { + display: flex; + align-items: center; + gap: 8px; + } + + .header-nav { + display: flex; + align-items: center; + gap: 8px; + } + .header-meta { display: flex; align-items: center; @@ -254,6 +266,31 @@ overflow: hidden; } + .explorer-container { + flex: 1; + display: none; + background: var(--bg-dark); + } + + .explorer-frame { + width: 100%; + height: 100%; + border: none; + } + + .app.explorer-view .sidebar, + .app.explorer-view .content { + display: none; + } + + .app.explorer-view .header-controls { + display: none; + } + + .app.explorer-view .explorer-container { + display: block; + } + /* Sidebar */ .sidebar { width: 340px; @@ -1723,22 +1760,22 @@
-
Kill Chain Editor
-
- v2.4.2 - ? +
+
AttackFlow
+
+ v2.4.2 + ? +
-
- -
-
- View +
- - + +
- +
+ +
Layers
@@ -1843,6 +1880,10 @@
+
+ +
+
@@ -2765,15 +2806,19 @@

Tips

// ============================================================ function setView(view) { state.view = view; - document.getElementById('view-killchain').classList.toggle('active', view === 'killchain'); - document.getElementById('view-relationship').classList.toggle('active', view === 'relationship'); - document.getElementById('kill-chain-container').classList.toggle('hidden', view !== 'killchain'); - document.getElementById('relationship-container').classList.toggle('visible', view === 'relationship'); - document.getElementById('content-title').textContent = view === 'killchain' ? 'Unified Kill Chain' : 'CAPEC → CWE → ATT&CK → Phase'; - document.getElementById('btn-group-capec').style.display = view === 'killchain' ? 'inline-block' : 'none'; - document.getElementById('btn-hide-empty').style.display = view === 'killchain' ? 'inline-block' : 'none'; - - if (view === 'relationship') { + const isExplorer = view === 'explorer'; + const isRelationship = view === 'relationship'; + document.getElementById('view-killchain').classList.toggle('active', view === 'killchain' || isRelationship); + document.getElementById('view-explorer').classList.toggle('active', isExplorer); + document.getElementById('kill-chain-container').classList.toggle('hidden', isExplorer || isRelationship); + document.getElementById('relationship-container').classList.toggle('visible', isRelationship); + document.getElementById('content-title').textContent = isRelationship ? 'CAPEC → CWE → ATT&CK → Phase' : 'Unified Kill Chain'; + document.getElementById('btn-group-capec').style.display = !isExplorer && !isRelationship ? 'inline-block' : 'none'; + document.getElementById('btn-hide-empty').style.display = !isExplorer && !isRelationship ? 'inline-block' : 'none'; + document.getElementById('stats-bar').style.display = isExplorer ? 'none' : 'flex'; + document.getElementById('score-legend').style.display = isExplorer ? 'none' : 'flex'; + document.querySelector('.app').classList.toggle('explorer-view', isExplorer); + if (isRelationship) { renderRelationshipView(); } } @@ -2798,6 +2843,12 @@

Tips

renderKillChain(); } + function openMitigationExplorer(mitigationId) { + if (!mitigationId) return; + const url = `explorer.html?mitigation=${encodeURIComponent(mitigationId)}`; + window.open(url, '_blank', 'noopener'); + } + // ============================================================ // TAB & FILTER CONTROLS // ============================================================ @@ -2976,7 +3027,7 @@

Tips

const mitigations = entity?.mitigations || []; const mitigationsHtml = mitigations.length > 0 ? mitigations.slice(0, 8).map(m => ` -
+
${esc(m.id)} ${esc(m.name)}
From 6740ee477c70fa1639078022c625d90bdf729108 Mon Sep 17 00:00:00 2001 From: Daniel Rosenthal Date: Sat, 31 Jan 2026 04:16:20 +0100 Subject: [PATCH 2/8] testing relationships view --- CHANGELOG.md | 9 ++++++++- README.md | 9 +++++++-- index.html | 4 ++++ 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d89d9a..9f3f699 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ This changelog also serves as a development context document for AI agents conti --- -## [2.4.3] - 2026-01-30 +## [2.4.3] - 2026-01-31 ### Fixed - Import & Visual Corrections @@ -28,6 +28,13 @@ This changelog also serves as a development context document for AI agents conti - ID badges show pointer cursor and highlight on hover - Clicking the rest of the phase item still opens the metadata editor as before +### Added - Relationship Explorer Integration + +- **NEW**: Relationship Explorer integrated as a second main view +- Header navigation now shows AttackFlow branding with primary view buttons +- Explorer embedded via `explorer.html` with full-screen layout +- Mitigation items in the detail panel open the explorer in a new tab, deep-linked to the mitigation + --- ## [2.4.2] - 2026-01-29 diff --git a/README.md b/README.md index 565a215..5e71b31 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ An editor for creating enriched Cyber Kill Chain assessments by mapping MITRE AT ### Work in progress -![Version](https://img.shields.io/badge/version-2.4.2-blue) +![Version](https://img.shields.io/badge/version-2.4.3-blue) ![License](https://img.shields.io/badge/license-Apache%202.0-green) ![Dependencies](https://img.shields.io/badge/dependencies-none-brightgreen) @@ -20,6 +20,7 @@ An editor for creating enriched Cyber Kill Chain assessments by mapping MITRE AT - **Rich Metadata** — Comments, hyperlinks, observables, CVE/CVSS references - **Score & Confidence** — Rate items by severity and assessment confidence - **Visual Indicators** — Color-coded ribbons, CVE badges, and metadata icons +- **Relationship Explorer** — Browse ATT&CK ↔ CAPEC ↔ CWE ↔ Mitigations in a dedicated view - **Hide Empty Phases** — Focus on active phases by hiding empty ones - **Import/Export** — Lightweight JSON sharing and CSV exports with metadata - **Navigator Layers** — Import ATT&CK Navigator JSON exports @@ -28,17 +29,21 @@ An editor for creating enriched Cyber Kill Chain assessments by mapping MITRE AT ## Quick Start +The Relationship Explorer is a second main view that lets you investigate ATT&CK ↔ CAPEC ↔ CWE ↔ Mitigations without assigning items first. Use the header navigation to switch views, click any node to load its details in the right panel, and open mitigation links to explore cross‑framework relationships in context. + 1. Clone or download this repository 2. Open `index.html` in a browser, or deploy to a web server 3. Browse techniques in the left sidebar 4. Drag items onto kill chain phases 5. Click items in the diagram to add metadata -6. Export your attack chain as JSON or CSV +6. Use the header navigation to switch to Relationship Explorer +7. Export your attack chain as JSON or CSV ## Project Structure ``` ├── index.html # Main application +├── explorer.html # Relationship Explorer view ├── config.js # Centralized configuration (paths, colors, settings) ├── kill-chain-visualizer.js # Core visualization component ├── scripts/ diff --git a/index.html b/index.html index 829eec0..2a9d6ac 100644 --- a/index.html +++ b/index.html @@ -1770,6 +1770,7 @@
+
@@ -2108,6 +2109,7 @@

Getting Started

  • Browse ATT&CK, CAPEC, and CWE tabs in the left sidebar
  • Use the search bar to find specific techniques, patterns, or weaknesses
  • Click any entity to view detailed information in the right panel
  • +
  • Switch to Relationship Explorer to investigate ATT&CK ↔ CAPEC ↔ CWE ↔ Mitigations without assigning items
  • @@ -2151,6 +2153,7 @@

    View Controls

  • Enable Hide Empty to focus on phases with assignments
  • Enable Group by CAPEC to nest techniques under patterns
  • Switch to Relationships view for CAPEC→CWE→ATT&CK chains
  • +
  • Use Relationship Explorer for full entity and mitigation browsing
  • @@ -2809,6 +2812,7 @@

    Tips

    const isExplorer = view === 'explorer'; const isRelationship = view === 'relationship'; document.getElementById('view-killchain').classList.toggle('active', view === 'killchain' || isRelationship); + document.getElementById('view-relationship').classList.toggle('active', isRelationship); document.getElementById('view-explorer').classList.toggle('active', isExplorer); document.getElementById('kill-chain-container').classList.toggle('hidden', isExplorer || isRelationship); document.getElementById('relationship-container').classList.toggle('visible', isRelationship); From 1bf476281a19c82d4b363d5f251cccfeacad2b94 Mon Sep 17 00:00:00 2001 From: Daniel Rosenthal Date: Sat, 31 Jan 2026 04:26:10 +0100 Subject: [PATCH 3/8] header --- index.html | 51 ++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 48 insertions(+), 3 deletions(-) diff --git a/index.html b/index.html index 2a9d6ac..2590480 100644 --- a/index.html +++ b/index.html @@ -137,11 +137,24 @@ /* View Toggle */ .view-toggle { display: flex; + gap: 4px; background: var(--bg-phase); border-radius: 4px; padding: 2px; } + .view-tab-group { + display: flex; + align-items: center; + gap: 6px; + padding: 2px; + border-radius: 3px; + } + + .view-tab-group.active { + background: rgba(113, 113, 122, 0.25); + } + .view-btn { padding: 5px 12px; border: none; @@ -159,6 +172,29 @@ color: var(--text-primary); } + .view-breadcrumb { + display: inline-flex; + align-items: center; + gap: 4px; + } + + .breadcrumb-sep { + color: var(--text-secondary); + font-size: 0.7rem; + } + + .view-btn.view-btn-crumb { + padding: 3px 8px; + font-size: 0.7rem; + border: 1px solid transparent; + } + + .view-btn.view-btn-crumb.active { + background: rgba(113, 113, 122, 0.35); + color: var(--text-primary); + border-color: var(--border-color); + } + /* Layer Toggles */ .layer-toggles { display: flex; @@ -1769,9 +1805,16 @@
    - - - +
    + + + + + +
    +
    + +
    @@ -2814,6 +2857,8 @@

    Tips

    document.getElementById('view-killchain').classList.toggle('active', view === 'killchain' || isRelationship); document.getElementById('view-relationship').classList.toggle('active', isRelationship); document.getElementById('view-explorer').classList.toggle('active', isExplorer); + document.getElementById('view-editor-group').classList.toggle('active', view === 'killchain' || isRelationship); + document.getElementById('view-explorer-group').classList.toggle('active', isExplorer); document.getElementById('kill-chain-container').classList.toggle('hidden', isExplorer || isRelationship); document.getElementById('relationship-container').classList.toggle('visible', isRelationship); document.getElementById('content-title').textContent = isRelationship ? 'CAPEC → CWE → ATT&CK → Phase' : 'Unified Kill Chain'; From 8faaafc32575ad97d5130ddf3e0f594756250fe5 Mon Sep 17 00:00:00 2001 From: Daniel Rosenthal Date: Sat, 31 Jan 2026 04:44:55 +0100 Subject: [PATCH 4/8] layout fix --- CHANGELOG.md | 7 +++++++ config.js | 5 +++++ index.html | 32 ++++++++++++++++++++++++++++++-- 3 files changed, 42 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f3f699..a0218d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,13 @@ This changelog also serves as a development context document for AI agents conti - Explorer embedded via `explorer.html` with full-screen layout - Mitigation items in the detail panel open the explorer in a new tab, deep-linked to the mitigation +### Changed - Navigation & Safety + +- **CHANGED**: Mitigation clicks now open the embedded Relationship Explorer in the current tab (preserves kill chain state) +- **NEW**: Leave-site confirmation dialog to prevent accidental data loss +- **NEW**: Confirmation behavior configurable via `CONFIG.navigation.confirmOnLeave` (default enabled) +- **CHANGED**: Editor view now renders a breadcrumbed Relationships sub-tab with Explorer as a separate tab group + --- ## [2.4.2] - 2026-01-29 diff --git a/config.js b/config.js index b9274ed..8df2455 100644 --- a/config.js +++ b/config.js @@ -70,6 +70,11 @@ const CONFIG = { maxDescLength: 800, // Max description length in detail panel maxMitigations: 8, // Max mitigations to show maxReferences: 3 // Max references to show + }, + + // Navigation behavior + navigation: { + confirmOnLeave: true // Show confirmation dialog before leaving the page } }; diff --git a/index.html b/index.html index 2590480..44b47da 100644 --- a/index.html +++ b/index.html @@ -2894,8 +2894,18 @@

    Tips

    function openMitigationExplorer(mitigationId) { if (!mitigationId) return; - const url = `explorer.html?mitigation=${encodeURIComponent(mitigationId)}`; - window.open(url, '_blank', 'noopener'); + + // Validate mitigationId to ensure it matches the expected ID format + const mitigationIdPattern = /^[A-Za-z0-9_-]+$/; + if (!mitigationIdPattern.test(mitigationId)) { + console.error('Invalid mitigationId provided to openMitigationExplorer:', mitigationId); + return; + } + const frame = document.querySelector('.explorer-frame'); + if (frame) { + frame.src = `explorer.html?mitigation=${encodeURIComponent(mitigationId)}`; + } + setView('explorer'); } // ============================================================ @@ -4483,12 +4493,30 @@

    Related CAPEC Patterns (${relatedCapecs.length})

    renderKillChain(); } + // ============================================================ + // LEAVE SITE CONFIRMATION + // ============================================================ + function enableLeaveSiteConfirmation() { + window.addEventListener('beforeunload', (e) => { + e.preventDefault(); + e.returnValue = ''; + return ''; + }); + } + // ============================================================ // INITIALIZATION // ============================================================ if (typeof applyConfigColors === 'function') { applyConfigColors(); } + if (typeof CONFIG !== 'undefined') { + if (CONFIG.navigation?.confirmOnLeave !== false) { + enableLeaveSiteConfirmation(); + } + } else { + enableLeaveSiteConfirmation(); + } initAssignments(); applyInputGuards(); loadVersion(); From 4b65657344fa2303e4f96875e0843caf01d6b014 Mon Sep 17 00:00:00 2001 From: Daniel Rosenthal Date: Sat, 31 Jan 2026 05:16:32 +0100 Subject: [PATCH 5/8] escaped ids and names in html and onclick handlers in renderRelationshipView() --- index.html | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/index.html b/index.html index 44b47da..7c7c69e 100644 --- a/index.html +++ b/index.html @@ -3602,35 +3602,35 @@

    Related CAPEC Patterns (${relatedCapecs.length})

    CAPEC
    ${chain.capec ? ` -
    - ${chain.capec.id} - ${chain.capec.name} +
    + ${esc(chain.capec.id)} + ${esc(chain.capec.name)}
    ` : '
    '}
    CWE
    ${chain.cwes.length > 0 ? chain.cwes.map(cwe => ` -
    - ${cwe.id} - ${cwe.name} +
    + ${esc(cwe.id)} + ${esc(cwe.name)}
    `).join('') : '
    '}
    ATT&CK
    ${chain.techniques.map(tech => ` -
    - ${tech.id} - ${tech.name} +
    + ${esc(tech.id)} + ${esc(tech.name)}
    `).join('')}
    Phase
    - ${chain.phase.key.split(':')[0]} - ${chain.phase.name} + ${esc(chain.phase.key.split(':')[0])} + ${esc(chain.phase.name)}
    From bf26fd317805607e112a5980b7e433a826df4610 Mon Sep 17 00:00:00 2001 From: Daniel Rosenthal Date: Sat, 31 Jan 2026 08:54:31 +0100 Subject: [PATCH 6/8] styling --- explorer.html | 21 +- index.html | 558 +++++++++++++++++++++++++++++++++++--------------- 2 files changed, 415 insertions(+), 164 deletions(-) diff --git a/explorer.html b/explorer.html index 33721d2..1ac22af 100644 --- a/explorer.html +++ b/explorer.html @@ -963,7 +963,26 @@ const params = new URLSearchParams(window.location.search); const mitigationId = params.get('mitigation'); - if (mitigationId && state.data.mitigations[mitigationId]) { + const entityParam = params.get('entity'); + if (entityParam) { + const [typeRaw, ...rest] = entityParam.split(':'); + const type = typeRaw?.toLowerCase(); + const id = rest.join(':'); + const validTypes = new Set(['attack', 'capec', 'cwe', 'mitigation']); + if (validTypes.has(type)) { + const store = type === 'attack' + ? state.data.techniques + : type === 'capec' + ? state.data.capecs + : type === 'cwe' + ? state.data.cwes + : state.data.mitigations; + if (store && store[id]) { + state.activeTab = type; + selectEntity(type, id); + } + } + } else if (mitigationId && state.data.mitigations[mitigationId]) { state.activeTab = 'mitigation'; selectEntity('mitigation', mitigationId); } diff --git a/index.html b/index.html index 7c7c69e..8898951 100644 --- a/index.html +++ b/index.html @@ -721,10 +721,11 @@ .technique-tag:hover { border-color: var(--attack-color); background: rgba(59, 130, 246, 0.25); } - .technique-tag .tag-header { + .technique-tag .tag-header-row { display: flex; align-items: center; - gap: 3px; + justify-content: space-between; + gap: 6px; } .technique-tag .id { @@ -735,6 +736,7 @@ padding: 1px 4px; border-radius: 2px; cursor: pointer; + font-size: 0.8rem; } .technique-tag .id:hover { @@ -743,20 +745,20 @@ .technique-tag .name { color: var(--text-primary); - max-width: 200px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; + max-width: 260px; + white-space: normal; + overflow: visible; + text-overflow: initial; + word-break: break-word; } .technique-tag .remove { - margin-left: auto; color: #ef4444; font-weight: bold; font-size: 1rem; width: 18px; height: 18px; - display: flex; + display: inline-flex; align-items: center; justify-content: center; border-radius: 3px; @@ -786,10 +788,11 @@ .cwe-tag:hover { border-color: var(--cwe-color); background: rgba(245, 158, 11, 0.2); } - .cwe-tag .tag-header { + .cwe-tag .tag-header-row { display: flex; align-items: center; - gap: 3px; + justify-content: space-between; + gap: 6px; } .cwe-tag .id { @@ -800,6 +803,7 @@ padding: 1px 4px; border-radius: 2px; cursor: pointer; + font-size: 0.8rem; } .cwe-tag .id:hover { @@ -808,20 +812,20 @@ .cwe-tag .name { color: var(--text-primary); - max-width: 200px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; + max-width: 260px; + white-space: normal; + overflow: visible; + text-overflow: initial; + word-break: break-word; } .cwe-tag .remove { - margin-left: auto; color: #ef4444; font-weight: bold; font-size: 1rem; width: 18px; height: 18px; - display: flex; + display: inline-flex; align-items: center; justify-content: center; border-radius: 3px; @@ -851,10 +855,11 @@ .capec-tag:hover { border-color: var(--capec-color); background: rgba(139, 92, 246, 0.2); } - .capec-tag .tag-header { + .capec-tag .tag-header-row { display: flex; align-items: center; - gap: 3px; + justify-content: space-between; + gap: 6px; } .capec-tag .id { @@ -865,6 +870,7 @@ padding: 1px 4px; border-radius: 2px; cursor: pointer; + font-size: 0.8rem; } .capec-tag .id:hover { @@ -873,20 +879,20 @@ .capec-tag .name { color: var(--text-primary); - max-width: 200px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; + max-width: 260px; + white-space: normal; + overflow: visible; + text-overflow: initial; + word-break: break-word; } .capec-tag .remove { - margin-left: auto; color: #ef4444; font-weight: bold; font-size: 1rem; width: 18px; height: 18px; - display: flex; + display: inline-flex; align-items: center; justify-content: center; border-radius: 3px; @@ -899,8 +905,56 @@ background: rgba(239, 68, 68, 0.2); } + .remove svg { + fill: #ff6565f7; + } + .capec-tag:hover .remove { opacity: 1; } + .technique-tag .name, + .capec-tag .name, + .cwe-tag .name { + flex: 1; + min-width: 0; + font-size: 0.8rem; + } + + .tag-actions { + display: inline-flex; + align-items: center; + gap: 4px; + margin-left: auto; + } + + .tag-body { + margin-top: 4px; + } + + .tag-footer { + margin-top: 4px; + } + + .tag-action-btn { + border: 1px solid var(--border-color); + background: rgba(0, 0, 0, 0.2); + color: var(--text-secondary); + font-size: 0.7rem; + padding: 3px 10px; + border-radius: 4px; + cursor: pointer; + transition: all 0.15s; + line-height: 1.1; + } + + .tag-action-btn:hover { + color: var(--text-primary); + border-color: var(--accent); + background: rgba(113, 113, 122, 0.25); + } + + .tag-action-btn.explore { color: #93c5fd; } + .tag-action-btn.edit { color: #fcd34d; } + /* ============================================================ SCORE RIBBONS & METADATA INDICATORS ============================================================ */ @@ -947,10 +1001,25 @@ justify-content: center; background: rgba(255,255,255,0.15); border-radius: 3px; - font-size: 10px; + font-size: 12px; font-weight: 600; color: var(--text-primary); white-space: nowrap; + gap: 4px; + } + + .meta-icon svg, + .tag-action-btn svg { + width: 16px; + height: 16px; + fill: currentColor; + stroke: currentColor; + flex: 0 0 auto; + } + + .tag-action-btn:hover svg { + fill: currentColor; + stroke: currentColor; } .metadata-icons .meta-icon.has-cve { background: rgba(239, 68, 68, 0.3); color: #fca5a5; } @@ -969,6 +1038,53 @@ font-size: 0.7rem; flex-wrap: wrap; } + + .legend-row { + display: flex; + align-items: center; + gap: 12px; + flex-wrap: wrap; + } + + .metadata-legend { + display: flex; + gap: 10px; + padding: 8px 12px; + background: var(--bg-card); + border-radius: 4px; + font-size: 0.7rem; + flex-wrap: wrap; + align-items: center; + } + + .metadata-legend .legend-item { + display: inline-flex; + align-items: center; + gap: 4px; + } + + .legend-icon { + width: 14px; + height: 14px; + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 3px; + background: rgba(255,255,255,0.15); + color: var(--text-primary); + } + + .legend-icon svg { + width: 12px; + height: 12px; + fill: currentColor; + } + + .legend-icon.has-cve { background: rgba(239, 68, 68, 0.3); color: #fca5a5; } + .legend-icon.has-observable { background: rgba(59, 130, 246, 0.3); color: #93c5fd; } + .legend-icon.has-link { background: rgba(139, 92, 246, 0.3); color: #c4b5fd; } + .legend-icon.has-comment { background: rgba(34, 197, 94, 0.3); color: #86efac; } + .legend-icon.has-confidence { background: rgba(251, 191, 36, 0.3); color: #fcd34d; } .legend-item { display: flex; @@ -1934,34 +2050,68 @@ Unified Kill Chain
    -
    -
    - Score: -
    -
    - Unclassified -
    -
    -
    - Low -
    -
    -
    - Medium -
    -
    -
    - High +
    +
    + Score: +
    +
    + Unclassified +
    +
    +
    + Low +
    +
    +
    + Medium +
    +
    +
    + High +
    +
    +
    + Critical +
    -
    -
    - Critical +
    @@ -2047,6 +2197,20 @@

    Assign to Phase

    + + + ` : ''; }).join(''); - + html = `

    Description

    @@ -3119,7 +3288,7 @@

    Attributes

    Domain:${esc((entity?.domain || 'enterprise').toUpperCase())}
    Platforms:${esc(platforms)}
    Tactics:${esc(tactics)}
    - ${entity?.isSubtechnique ? `
    Parent:${esc(entity.parentTechnique)}
    ` : ''} + ${entity?.isSubtechnique ? `
    Parent:${esc(entity.parentTechnique)}
    ` : ''}
    Version:${esc(entity?.version || '1.0')}
    @@ -3139,7 +3308,7 @@

    Mitigations (${mitigations.length})

    Related CAPEC Patterns (${relatedCapecs.length})

    ${relatedCapecs.map(c => ` -
    +
    ${esc(c)} ${esc(state.library.capecs[c]?.name || 'Unknown')}
    @@ -3161,9 +3330,8 @@

    References

    `; } else if (type === 'capec') { entity = state.library.capecs[id]; - idEl.textContent = id; - nameEl.textContent = entity?.name || 'Unknown'; - + name = entity?.name || 'Unknown'; + html = `

    Description

    @@ -3181,7 +3349,7 @@

    Attributes

    Related Techniques (${entity?.techniques?.length || 0})

    ${(entity?.techniques || []).map(t => ` -
    +
    ${esc(t)} ${esc(state.library.techniques[t]?.name || getTechniqueName(t))}
    @@ -3192,7 +3360,7 @@

    Related Techniques (${entity?.techniques?.length || 0})

    Related CWEs (${entity?.cwes?.length || 0})

    ${(entity?.cwes || []).slice(0, 10).map(c => ` -
    +
    ${esc(c)} ${esc(state.library.cwes[c]?.name || 'Unknown')}
    @@ -3206,11 +3374,9 @@

    Related CWEs (${entity?.cwes?.length || 0})

    `; } else if (type === 'cwe') { entity = state.library.cwes[id]; - idEl.textContent = id; - nameEl.textContent = entity?.name || 'Unknown'; - + name = entity?.name || 'Unknown'; const relatedCapecs = state.cweToCapec[id] || entity?.capecs || []; - + html = `

    Description

    @@ -3224,7 +3390,7 @@

    Abstraction

    Related CAPEC Patterns (${relatedCapecs.length})

    ${relatedCapecs.slice(0, 10).map(c => ` -
    +
    ${esc(c)} ${esc(state.library.capecs[c]?.name || 'Unknown')}
    @@ -3237,8 +3403,102 @@

    Related CAPEC Patterns (${relatedCapecs.length})

    `; } + + return { id, name, html }; + } + + function buildMetadataSummary(type, id, phaseKey) { + const assignment = phaseKey ? findAssignment(phaseKey, type, id) : null; + const metadata = assignment ? getAssignmentMetadata(assignment) : createDefaultMetadata(); + const score = metadata.score || 'unclassified'; + const scoreLabel = SCORE_LEVELS[score]?.label || 'Unclassified'; + const confidence = metadata.confidence ? `${metadata.confidence}% (${getConfidenceLabel(metadata.confidence)})` : 'Unknown'; + const safePhase = phaseKey ? `${phaseKey.split(':')[0]} — ${formatPhaseName(phaseKey.split(':')[1])}` : ''; + + const links = (metadata.hyperlinks || []).filter(l => l?.url && /^https?:\/\//i.test(l.url)); + const linksHtml = links.length + ? links.map(l => ` + + `).join('') + : '
    No hyperlinks
    '; + + const observables = metadata.observables || []; + const observablesHtml = observables.length + ? observables.map(o => ` +
    + ${esc(o.type)} + ${esc(o.value)} +
    + `).join('') + : '
    No observables
    '; + + return ` +
    +

    Metadata

    +
    +
    Score:${esc(scoreLabel)}
    +
    Confidence:${esc(confidence)}
    + ${safePhase ? `
    Phase:${esc(safePhase)}
    ` : ''} + ${metadata.cveId ? `
    CVE:${esc(metadata.cveId)}
    ` : ''} + ${metadata.cvssVector ? `
    CVSS:${esc(metadata.cvssVector)}
    ` : ''} +
    +
    + ${metadata.comments ? ` +
    +

    Comments

    +

    ${esc(metadata.comments).replace(/\n/g, '
    ')}

    +
    + ` : ''} +
    +

    Hyperlinks

    +
    + ${linksHtml} +
    +
    +
    +

    Observables

    +
    + ${observablesHtml} +
    +
    + `; + } + + function openEntityModal(type, id, phaseKey) { + if (!type || !id) return; + const detail = buildEntityDetail(type, id); + document.getElementById('entity-modal-id').textContent = detail.id; + document.getElementById('entity-modal-name').textContent = detail.name; + const content = document.getElementById('entity-modal-content'); + content.innerHTML = detail.html + buildMetadataSummary(type, id, phaseKey); + document.getElementById('entity-modal').classList.add('visible'); + document.getElementById('detail-panel').classList.remove('visible'); + state.selection = { type, id }; + } + + function closeEntityModal(event) { + if (event && event.target !== event.currentTarget) return; + document.getElementById('entity-modal').classList.remove('visible'); + } + + function showDetail(type, id) { + const panel = document.getElementById('detail-panel'); + const idEl = document.getElementById('detail-id'); + const nameEl = document.getElementById('detail-name'); + const contentEl = document.getElementById('detail-content'); + const selectEl = document.getElementById('phase-select'); - contentEl.innerHTML = html; + panel.classList.add('visible'); + idEl.className = `detail-id ${type}`; + + const detail = buildEntityDetail(type, id); + idEl.textContent = detail.id; + nameEl.textContent = detail.name; + contentEl.innerHTML = detail.html; // Set current assignment in dropdown const currentPhase = findEntityPhase(type, id); @@ -3404,40 +3664,8 @@

    Related CAPEC Patterns (${relatedCapecs.length})

    `; - // Render CAPECs (grouped) - if (state.layers.capec && state.groupByCapec && phaseData.capecs.length > 0) { - for (const capecAssignment of phaseData.capecs) { - const capecId = getAssignmentId(capecAssignment); - const capec = state.library.capecs[capecId]; - if (!capec) continue; - - // Find techniques in this phase that belong to this CAPEC - const capecTechs = (capec.techniques || []).filter(t => - phaseData.techniques.some(a => getAssignmentId(a) === t) - ); - - html += ` -
    -
    - ${InputSecurity.escapeHtml(capecId)} - ${InputSecurity.escapeHtml(capec.name)} -
    -
    - `; - - for (const techId of capecTechs) { - const techAssignment = phaseData.techniques.find(a => getAssignmentId(a) === techId); - const tech = state.library.techniques[techId]; - const meta = getAssignmentMetadata(techAssignment); - html += renderEntityTag('attack', techId, tech?.name || '', meta, phaseKey); - } - - html += `
    `; - } - } - - // Render CAPECs (not grouped) - if (state.layers.capec && !state.groupByCapec && phaseData.capecs.length > 0) { + // Render CAPECs + if (state.layers.capec && phaseData.capecs.length > 0) { for (const capecAssignment of phaseData.capecs) { const capecId = getAssignmentId(capecAssignment); const capec = state.library.capecs[capecId]; @@ -3448,18 +3676,8 @@

    Related CAPEC Patterns (${relatedCapecs.length})

    // Render techniques if (state.layers.attack) { - const groupedTechs = new Set(); - if (state.layers.capec && state.groupByCapec) { - for (const capecAssignment of phaseData.capecs) { - const capecId = getAssignmentId(capecAssignment); - const capec = state.library.capecs[capecId]; - (capec?.techniques || []).forEach(t => groupedTechs.add(t)); - } - } - for (const techAssignment of phaseData.techniques) { const techId = getAssignmentId(techAssignment); - if (state.groupByCapec && groupedTechs.has(techId)) continue; const tech = state.library.techniques[techId]; const meta = getAssignmentMetadata(techAssignment); html += renderEntityTag('attack', techId, tech?.name || '', meta, phaseKey); @@ -3495,7 +3713,10 @@

    Related CAPEC Patterns (${relatedCapecs.length})

    const tagClass = type === 'attack' ? 'technique-tag' : type === 'capec' ? 'capec-tag' : 'cwe-tag'; const score = metadata.score || 'unclassified'; const safeId = InputSecurity.escapeHtml(id); - const safeName = InputSecurity.escapeHtml(name); + const displayName = truncateAtBoundary(name || '', 500); + const safeName = InputSecurity.escapeHtml(displayName || ''); + const safeType = InputSecurity.sanitizeAttr(type, 20); + const safeIdAttr = InputSecurity.sanitizeAttr(id); const safePhaseKey = InputSecurity.sanitizeAttr(phaseKey); // Build metadata icons @@ -3508,23 +3729,40 @@

    Related CAPEC Patterns (${relatedCapecs.length})

    if (hasCve || hasObservables || hasLinks || hasComment || hasConfidence) { metaIcons = ''; } return `
    - - ${safeId} - ${safeName} - × + onclick="openEntityModal('${safeType}', '${safeIdAttr}', '${safePhaseKey}')"> + + ${safeId} + + + + + + + - ${metaIcons} +
    + ${safeName} +
    +
    `; } @@ -3710,7 +3948,6 @@

    Related CAPEC Patterns (${relatedCapecs.length})

    activeTab: state.activeTab, filters: state.filters, layers: state.layers, - groupByCapec: state.groupByCapec, hideEmpty: state.hideEmpty, assignments: state.assignments, selection: state.selection @@ -3951,10 +4188,6 @@

    Related CAPEC Patterns (${relatedCapecs.length})

    }; } - if (typeof data.groupByCapec === 'boolean') { - sanitized.groupByCapec = data.groupByCapec; - } - if (typeof data.hideEmpty === 'boolean') { sanitized.hideEmpty = data.hideEmpty; } @@ -4009,7 +4242,6 @@

    Related CAPEC Patterns (${relatedCapecs.length})

    // Restore sanitized view state if (sanitized.layers) state.layers = sanitized.layers; - if (sanitized.groupByCapec !== undefined) state.groupByCapec = sanitized.groupByCapec; if (sanitized.hideEmpty !== undefined) state.hideEmpty = sanitized.hideEmpty; if (sanitized.activeTab) state.activeTab = sanitized.activeTab; @@ -4017,7 +4249,6 @@

    Related CAPEC Patterns (${relatedCapecs.length})

    document.getElementById('layer-attack').checked = !!state.layers.attack; document.getElementById('layer-capec').checked = !!state.layers.capec; document.getElementById('layer-cwe').checked = !!state.layers.cwe; - document.getElementById('btn-group-capec').classList.toggle('active', !!state.groupByCapec); document.getElementById('btn-hide-empty').classList.toggle('active', !!state.hideEmpty); if (sanitized.view) { @@ -4340,7 +4571,7 @@

    Related CAPEC Patterns (${relatedCapecs.length})

    // Get CVE (with validation) const cveInput = document.getElementById('meta-cve-id'); - const cveValue = InputSecurity.sanitize(cveInput.value, 50); + const cveValue = sanitizeForStorage(cveInput.value, 50); const cveError = document.getElementById('meta-cve-id-error'); if (cveValue && !InputSecurity.validators.cveId(cveValue).valid) { cveError.textContent = 'Invalid CVE format (CVE-YYYY-NNNNN)'; @@ -4352,7 +4583,7 @@

    Related CAPEC Patterns (${relatedCapecs.length})

    // Get CVSS (with validation) const cvssInput = document.getElementById('meta-cvss-vector'); - const cvssValue = InputSecurity.sanitize(cvssInput.value, 100); + const cvssValue = sanitizeForStorage(cvssInput.value, 100); const cvssError = document.getElementById('meta-cvss-vector-error'); if (cvssValue && !InputSecurity.validators.cvssVector(cvssValue).valid) { cvssError.textContent = 'Invalid CVSS 3.x vector'; @@ -4363,14 +4594,14 @@

    Related CAPEC Patterns (${relatedCapecs.length})

    cvssInput.classList.remove('invalid'); // Get comments - const comments = InputSecurity.sanitize(document.getElementById('meta-comments').value, 2000); + const comments = sanitizeForStorage(document.getElementById('meta-comments').value, 2000); // Get hyperlinks (with validation) const hyperlinks = []; document.querySelectorAll('#hyperlink-list .hyperlink-item').forEach(row => { const inputs = row.querySelectorAll('input'); - const label = InputSecurity.sanitize(inputs[0].value, 100); - const url = InputSecurity.sanitize(inputs[1].value, 500); + const label = sanitizeForStorage(inputs[0].value, 100); + const url = sanitizeForStorage(inputs[1].value, 500); if (label && url) { if (InputSecurity.validators.url(url).valid) { hyperlinks.push({ label, url }); @@ -4385,7 +4616,7 @@

    Related CAPEC Patterns (${relatedCapecs.length})

    const select = row.querySelector('select'); const input = row.querySelector('input'); const obsType = select.value; - const obsValue = InputSecurity.sanitize(input.value, 500); + const obsValue = sanitizeForStorage(input.value, 500); if (obsValue) { const validation = InputSecurity.validateObservable(obsType, obsValue); @@ -4475,6 +4706,7 @@

    Related CAPEC Patterns (${relatedCapecs.length})

    if (e.key === 'Escape') { closeUsageGuide(); closeChangelog(); + closeEntityModal(); closeMetadataEditor(); } }); From e71835431a92dc7fa170bbcdf7b982e798815e80 Mon Sep 17 00:00:00 2001 From: Daniel Rosenthal Date: Sat, 31 Jan 2026 09:19:53 +0100 Subject: [PATCH 7/8] fix tooltip position --- index.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/index.html b/index.html index 8898951..c596ffc 100644 --- a/index.html +++ b/index.html @@ -3744,8 +3744,8 @@

    Observables

    - ${safeId} - + ${safeId} + From 162b392630df80b02884b6f2967c6fd82df1e550 Mon Sep 17 00:00:00 2001 From: Daniel Rosenthal Date: Sat, 31 Jan 2026 11:42:49 +0100 Subject: [PATCH 8/8] styling --- explorer.html | 2 +- favicon.ico | Bin 15406 -> 9662 bytes index.html | 37 +------------------------------------ 3 files changed, 2 insertions(+), 37 deletions(-) diff --git a/explorer.html b/explorer.html index 1ac22af..59b6fe6 100644 --- a/explorer.html +++ b/explorer.html @@ -3,7 +3,7 @@ - Relationship Explorer - AttackFlow + AttackFlow - Relationship Explorer