From 11aee57b2394023e61300e9279d40926fad4b327 Mon Sep 17 00:00:00 2001 From: JigyasuRajput Date: Wed, 20 Aug 2025 22:01:54 +0530 Subject: [PATCH 1/6] feat(html): add triage capabilities --- cve_bin_tool/output_engine/html.py | 106 ++++- .../output_engine/html_reports/js/triage.js | 405 ++++++++++++++++++ .../html_reports/templates/base.html | 7 + 3 files changed, 517 insertions(+), 1 deletion(-) create mode 100644 cve_bin_tool/output_engine/html_reports/js/triage.js diff --git a/cve_bin_tool/output_engine/html.py b/cve_bin_tool/output_engine/html.py index 10f345a453..dc07b0f200 100644 --- a/cve_bin_tool/output_engine/html.py +++ b/cve_bin_tool/output_engine/html.py @@ -3,6 +3,7 @@ from __future__ import annotations +import json from collections import Counter, defaultdict from datetime import datetime from logging import Logger @@ -14,7 +15,7 @@ from cve_bin_tool.merge import MergeReports -from ..util import CVEData, ProductInfo, Remarks, VersionInfo +from ..util import CVE, CVEData, ProductInfo, Remarks, VersionInfo from ..version import VERSION from .print_mode import html_print_mode from .util import group_cve_by_remark @@ -28,6 +29,94 @@ } +def serialize_vulnerability_data( + all_cve_data: dict[ProductInfo, CVEData], + all_cve_version_info: dict[str, VersionInfo] | None, + scanned_dir: str, + total_files: int, + products_with_cve: int, + products_without_cve: int, + affected_versions: int = 0, +) -> str: + """ + Serialize all vulnerability data to JSON format for embedding in HTML. + + Returns: + JSON string containing all vulnerability data in a structured format + """ + data = { + "metadata": { + "scanned_dir": scanned_dir, + "total_files": total_files, + "products_with_cve": products_with_cve, + "products_without_cve": products_without_cve, + "affected_versions": affected_versions, + "generated_at": datetime.now().isoformat(), + "tool_version": VERSION, + }, + "products": [], + "vulnerabilities": {}, + } + + # Process each product and its CVEs + for product_info, cve_data in all_cve_data.items(): + product_dict = { + "vendor": product_info.vendor, + "product": product_info.product, + "version": product_info.version, + "location": product_info.location, + "purl": product_info.purl, + "paths": list(cve_data.get("paths", set())), + "cve_count": len(cve_data.get("cves", [])), + "cves": [], + } + + # Process CVEs for this product + for cve in cve_data.get("cves", []): + if isinstance(cve, CVE): + cve_id = cve.cve_number + + # Add CVE to product's CVE list + product_dict["cves"].append(cve_id) + + # Add detailed CVE information to vulnerabilities dictionary + if cve_id not in data["vulnerabilities"]: + data["vulnerabilities"][cve_id] = { + "cve_number": cve.cve_number, + "severity": cve.severity, + "score": cve.score, + "cvss_version": cve.cvss_version, + "cvss_vector": cve.cvss_vector, + "description": cve.description, + "data_source": cve.data_source, + "last_modified": cve.last_modified, + "metric": cve.metric, + "remarks": ( + cve.remarks.value + if hasattr(cve.remarks, "value") + else str(cve.remarks) + ), + "comments": cve.comments, + "justification": cve.justification, + "response": cve.response, + } + + data["products"].append(product_dict) + + # Add version information if available + if all_cve_version_info: + data["version_info"] = {} + for cve_id, version_info in all_cve_version_info.items(): + data["version_info"][cve_id] = { + "start_including": version_info.start_including, + "start_excluding": version_info.start_excluding, + "end_including": version_info.end_including, + "end_excluding": version_info.end_excluding, + } + + return json.dumps(data, indent=2) + + def render_cves( hid: str, cve_row: Template, tag: str, cves: list[dict[str, str]] ) -> str: @@ -435,10 +524,23 @@ def output_html( js_main = "js/main.js" js_bootstrap = "js/bootstrap.js" js_plotly = "js/plotly.js" + js_triage = "js/triage.js" script_main = templates_env.get_template(js_main) script_bootstrap = templates_env.get_template(js_bootstrap) script_plotly = templates_env.get_template(js_plotly) + script_triage = templates_env.get_template(js_triage) + + # Generate vulnerability data JSON for interactive features + vulnerability_data_json = serialize_vulnerability_data( + all_cve_data, + all_cve_version_info, + scanned_dir, + total_files, + products_with_cve, + products_without_cve, + affected_versions, + ) # Render the base html to generate report outfile.write( @@ -469,6 +571,8 @@ def output_html( script_main=script_main.render(), script_bootstrap=script_bootstrap.render(), script_plotly=script_plotly.render(), + script_triage=script_triage.render(), + vulnerability_data_json=vulnerability_data_json, ) ) diff --git a/cve_bin_tool/output_engine/html_reports/js/triage.js b/cve_bin_tool/output_engine/html_reports/js/triage.js new file mode 100644 index 0000000000..d63d5f0e76 --- /dev/null +++ b/cve_bin_tool/output_engine/html_reports/js/triage.js @@ -0,0 +1,405 @@ +/** + * Interactive Triage Functionality for CVE-bin-tool HTML Reports + * + * This file contains all the interactive logic for triaging vulnerabilities + * directly within the HTML report, allowing users to: + * - Update CVE remarks (status) + * - Add comments and justifications + * - Save triage results offline + * - Export triage data + */ + +// Global variables +let vulnerabilityData = {}; +let triageChanges = {}; + +/** + * Initialize the triage functionality when the page loads + */ +function initializeTriage() { + // Load vulnerability data from the embedded JSON + const dataElement = document.getElementById('vulnerability-data'); + if (dataElement) { + try { + vulnerabilityData = JSON.parse(dataElement.textContent); + console.log('Vulnerability data loaded:', vulnerabilityData); + } catch (error) { + console.error('Failed to parse vulnerability data:', error); + return; + } + } else { + console.error('Vulnerability data not found in HTML'); + return; + } + + // Load any existing triage changes from localStorage + loadTriageChanges(); + + // Initialize triage controls + setupTriageControls(); + + // Apply any existing triage changes to the UI + applyTriageChangesToUI(); +} + +/** + * Load existing triage changes from localStorage + */ +function loadTriageChanges() { + const stored = localStorage.getItem('cve-bin-tool-triage'); + if (stored) { + try { + triageChanges = JSON.parse(stored); + } catch (error) { + console.error('Failed to load triage changes from localStorage:', error); + triageChanges = {}; + } + } +} + +/** + * Save triage changes to localStorage + */ +function saveTriageChanges() { + try { + localStorage.setItem('cve-bin-tool-triage', JSON.stringify(triageChanges)); + console.log('Triage changes saved to localStorage'); + } catch (error) { + console.error('Failed to save triage changes to localStorage:', error); + } +} + +/** + * Setup triage controls for all CVEs in the report + */ +function setupTriageControls() { + // Add triage controls to each CVE item + document.querySelectorAll('[id^="info"]').forEach(cveElement => { + const cveId = extractCVEFromElement(cveElement); + if (cveId && vulnerabilityData.vulnerabilities[cveId]) { + addTriageControlsToElement(cveElement, cveId); + } + }); + + // Add save and export buttons to the header + addTriageButtons(); +} + +/** + * Extract CVE ID from a DOM element + */ +function extractCVEFromElement(element) { + // Look for CVE pattern in the element's content + const content = element.textContent || element.innerHTML; + const cveMatch = content.match(/CVE-\d{4}-\d+/); + return cveMatch ? cveMatch[0] : null; +} + +/** + * Add triage controls to a specific CVE element + */ +function addTriageControlsToElement(element, cveId) { + const vulnerability = vulnerabilityData.vulnerabilities[cveId]; + const currentTriage = triageChanges[cveId] || {}; + + // Create triage control HTML + const triageHtml = ` +
+
+ + + + + Triage Controls +
+ +
+
+ + +
+ +
+ + +
+
+ +
+ + +
+ +
+ + +
+ +
+ + Last modified: ${currentTriage.timestamp || 'Never'} + +
+
+ `; + + // Append the triage controls to the CVE element + element.insertAdjacentHTML('beforeend', triageHtml); + + // Add event listeners for the controls + addTriageEventListeners(cveId); +} + +/** + * Add event listeners for triage controls + */ +function addTriageEventListeners(cveId) { + const controls = document.querySelector(`[data-cve="${cveId}"].triage-controls`); + if (!controls) return; + + // Status change + const statusSelect = controls.querySelector('.triage-status'); + statusSelect.addEventListener('change', (e) => { + updateTriageData(cveId, 'remarks', e.target.value); + }); + + // Priority change + const prioritySelect = controls.querySelector('.triage-priority'); + prioritySelect.addEventListener('change', (e) => { + updateTriageData(cveId, 'priority', e.target.value); + }); + + // Comments change + const commentsTextarea = controls.querySelector('.triage-comments'); + commentsTextarea.addEventListener('blur', (e) => { + updateTriageData(cveId, 'comments', e.target.value); + }); + + // Justification change + const justificationTextarea = controls.querySelector('.triage-justification'); + justificationTextarea.addEventListener('blur', (e) => { + updateTriageData(cveId, 'justification', e.target.value); + }); +} + +/** + * Update triage data for a specific CVE + */ +function updateTriageData(cveId, field, value) { + if (!triageChanges[cveId]) { + triageChanges[cveId] = {}; + } + + triageChanges[cveId][field] = value; + triageChanges[cveId].timestamp = new Date().toISOString(); + + // Update timestamp display + const timestampElement = document.querySelector(`[data-cve="${cveId}"] .triage-timestamp`); + if (timestampElement) { + timestampElement.textContent = new Date().toLocaleString(); + } + + // Auto-save changes + saveTriageChanges(); + + // Update visual indicators + updateTriageIndicators(cveId); + + console.log(`Updated triage data for ${cveId}:`, triageChanges[cveId]); +} + +/** + * Update visual indicators based on triage changes + */ +function updateTriageIndicators(cveId) { + const triage = triageChanges[cveId]; + if (!triage) return; + + // Find the CVE element and update its status badge + const cveElements = document.querySelectorAll(`[id*="${cveId}"]`); + cveElements.forEach(element => { + // Update status badges + const badges = element.querySelectorAll('.badge'); + badges.forEach(badge => { + if (badge.textContent === 'NEW' || badge.textContent === 'UNEXPLORED') { + if (triage.remarks === 'Mitigated') { + badge.textContent = 'MITIGATED'; + badge.className = 'badge rounded-pill bg-success'; + } else if (triage.remarks === 'FalsePositive') { + badge.textContent = 'FALSE POSITIVE'; + badge.className = 'badge rounded-pill bg-secondary'; + } else if (triage.remarks === 'NotAffected') { + badge.textContent = 'NOT AFFECTED'; + badge.className = 'badge rounded-pill bg-info'; + } else if (triage.remarks === 'Confirmed') { + badge.textContent = 'CONFIRMED'; + badge.className = 'badge rounded-pill bg-warning'; + } + } + }); + }); +} + +/** + * Apply existing triage changes to the UI + */ +function applyTriageChangesToUI() { + Object.keys(triageChanges).forEach(cveId => { + updateTriageIndicators(cveId); + }); +} + +/** + * Add save and export buttons to the header + */ +function addTriageButtons() { + const navbar = document.querySelector('.navbar'); + if (!navbar) return; + + const buttonGroup = document.createElement('div'); + buttonGroup.className = 'btn-group ms-2'; + buttonGroup.innerHTML = ` + + + + `; + + // Find the navbar content div and append the button group + const navbarContent = navbar.querySelector('.container-fluid') || navbar.querySelector('.my-2'); + if (navbarContent) { + navbarContent.appendChild(buttonGroup); + } +} + +/** + * Export triage data to a JSON file + */ +function saveTriageToFile() { + const triageData = { + metadata: { + exported_at: new Date().toISOString(), + tool_version: vulnerabilityData.metadata.tool_version, + scanned_dir: vulnerabilityData.metadata.scanned_dir, + }, + triage: triageChanges + }; + + const blob = new Blob([JSON.stringify(triageData, null, 2)], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `cve-triage-${new Date().toISOString().split('T')[0]}.json`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + + console.log('Triage data exported'); +} + +/** + * Import triage data from a JSON file + */ +function loadTriageFromFile() { + const input = document.createElement('input'); + input.type = 'file'; + input.accept = '.json'; + input.onchange = (e) => { + const file = e.target.files[0]; + if (!file) return; + + const reader = new FileReader(); + reader.onload = (e) => { + try { + const data = JSON.parse(e.target.result); + if (data.triage) { + triageChanges = { ...triageChanges, ...data.triage }; + saveTriageChanges(); + + // Refresh the page to apply changes + location.reload(); + } + } catch (error) { + alert('Error reading triage file: ' + error.message); + } + }; + reader.readAsText(file); + }; + input.click(); +} + +/** + * Clear all triage data + */ +function clearAllTriage() { + if (confirm('Are you sure you want to clear all triage data? This action cannot be undone.')) { + triageChanges = {}; + localStorage.removeItem('cve-bin-tool-triage'); + location.reload(); + } +} + +/** + * Get summary of triage changes + */ +function getTriageSummary() { + const summary = { + total_triaged: Object.keys(triageChanges).length, + by_status: {}, + by_priority: {} + }; + + Object.values(triageChanges).forEach(triage => { + // Count by status + const status = triage.remarks || 'Unknown'; + summary.by_status[status] = (summary.by_status[status] || 0) + 1; + + // Count by priority + const priority = triage.priority || 'Unset'; + summary.by_priority[priority] = (summary.by_priority[priority] || 0) + 1; + }); + + return summary; +} + +// Initialize triage functionality when the DOM is loaded +document.addEventListener('DOMContentLoaded', initializeTriage); + +// Make functions available globally +window.triageFunctions = { + initializeTriage, + saveTriageToFile, + loadTriageFromFile, + clearAllTriage, + getTriageSummary +}; diff --git a/cve_bin_tool/output_engine/html_reports/templates/base.html b/cve_bin_tool/output_engine/html_reports/templates/base.html index 28ddd3e3c7..65a339543c 100644 --- a/cve_bin_tool/output_engine/html_reports/templates/base.html +++ b/cve_bin_tool/output_engine/html_reports/templates/base.html @@ -9,6 +9,10 @@ + + + + @@ -267,6 +271,9 @@

How to Contribute?

+ + + From f5fa8159e58b52bafa630338d915c1cd751e4692 Mon Sep 17 00:00:00 2001 From: JigyasuRajput Date: Wed, 20 Aug 2025 22:31:44 +0530 Subject: [PATCH 2/6] feat(html): add css and more triage feat --- .../output_engine/html_reports/css/main.css | 80 +++ .../output_engine/html_reports/js/triage.js | 540 +++++++++++++----- 2 files changed, 486 insertions(+), 134 deletions(-) diff --git a/cve_bin_tool/output_engine/html_reports/css/main.css b/cve_bin_tool/output_engine/html_reports/css/main.css index 5806a5c72d..f4b98764de 100644 --- a/cve_bin_tool/output_engine/html_reports/css/main.css +++ b/cve_bin_tool/output_engine/html_reports/css/main.css @@ -205,4 +205,84 @@ a{ .btn-filter{ margin-top: 0px; } +} + +/* VEX Triage Specific Styles */ +.vex-triage-controls { + border: 2px solid #dee2e6; + border-radius: 8px; + background-color: #f8f9fa; + transition: all 0.3s ease; +} + +.vex-triage-controls:hover { + border-color: #007bff; + box-shadow: 0 4px 8px rgba(0, 123, 255, 0.1); +} + +.triaged-item { + position: relative; + transition: all 0.3s ease; +} + +.triaged-item::before { + content: '✓'; + position: absolute; + left: -15px; + top: 50%; + transform: translateY(-50%); + color: #28a745; + font-weight: bold; + font-size: 1.2em; +} + +.vex-justification-container { + transition: all 0.3s ease; +} + +.vex-status-select:focus, +.vex-justification-select:focus, +.vex-detail-textarea:focus { + border-color: #007bff; + box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25); +} + +.triage-status-badge { + animation: fadeIn 0.3s ease-in; +} + +@keyframes fadeIn { + from { opacity: 0; transform: scale(0.8); } + to { opacity: 1; transform: scale(1); } +} + +/* VEX Status Badge Colors */ +.bg-success { background-color: #28a745 !important; } +.bg-danger { background-color: #dc3545 !important; } +.bg-warning { background-color: #ffc107 !important; color: #212529 !important; } +.bg-info { background-color: #17a2b8 !important; } +.bg-secondary { background-color: #6c757d !important; } + +/* Progress indicator for triage completion */ +.triage-progress { + height: 4px; + background: linear-gradient(90deg, #28a745 0%, #ffc107 50%, #dc3545 100%); + border-radius: 2px; + margin-bottom: 1rem; +} + +/* Responsive adjustments for VEX controls */ +@media only screen and (max-width: 768px) { + .vex-triage-controls .row > .col-md-6 { + margin-bottom: 1rem; + } + + .vex-triage-controls h6 { + font-size: 1.1rem; + } + + .btn-group .btn-sm { + font-size: 0.8rem; + padding: 0.25rem 0.5rem; + } } \ No newline at end of file diff --git a/cve_bin_tool/output_engine/html_reports/js/triage.js b/cve_bin_tool/output_engine/html_reports/js/triage.js index d63d5f0e76..f89ae21659 100644 --- a/cve_bin_tool/output_engine/html_reports/js/triage.js +++ b/cve_bin_tool/output_engine/html_reports/js/triage.js @@ -1,18 +1,36 @@ /** * Interactive Triage Functionality for CVE-bin-tool HTML Reports + * VEX-Compliant Vulnerability Exchange Implementation * * This file contains all the interactive logic for triaging vulnerabilities * directly within the HTML report, allowing users to: - * - Update CVE remarks (status) - * - Add comments and justifications + * - Update CVE status (VEX-compliant) + * - Add justifications for "Not Affected" status + * - Add detailed comments and impact statements * - Save triage results offline - * - Export triage data + * - Export triage data in VEX format */ // Global variables let vulnerabilityData = {}; let triageChanges = {}; +// VEX Status options +const VEX_STATUS_OPTIONS = { + 'not_affected': 'Not Affected', + 'affected': 'Affected', + 'under_investigation': 'Under Investigation', + 'fixed': 'Fixed' +}; + +// VEX Justification options (only for "Not Affected" status) +const VEX_JUSTIFICATION_OPTIONS = { + 'component_not_present': 'Component Not Present', + 'vulnerable_code_not_present': 'Vulnerable Code Not Present', + 'vulnerable_code_not_in_execute_path': 'Vulnerable Code Not in Execution Path', + 'vulnerable_code_unreachable': 'Vulnerable Code Unreachable' +}; + /** * Initialize the triage functionality when the page loads */ @@ -89,68 +107,89 @@ function setupTriageControls() { * Extract CVE ID from a DOM element */ function extractCVEFromElement(element) { - // Look for CVE pattern in the element's content - const content = element.textContent || element.innerHTML; - const cveMatch = content.match(/CVE-\d{4}-\d+/); - return cveMatch ? cveMatch[0] : null; + // Look for CVE pattern in the element's content or parent elements + let current = element; + while (current) { + const content = current.textContent || current.innerHTML; + const cveMatch = content.match(/CVE-\d{4}-\d+/); + if (cveMatch) return cveMatch[0]; + current = current.parentElement; + } + return null; } /** - * Add triage controls to a specific CVE element + * Add VEX-compliant triage controls to a specific CVE element */ function addTriageControlsToElement(element, cveId) { const vulnerability = vulnerabilityData.vulnerabilities[cveId]; const currentTriage = triageChanges[cveId] || {}; - // Create triage control HTML + // Get current status (default to 'under_investigation' if not set) + const currentStatus = currentTriage.vex_status || 'under_investigation'; + const currentJustification = currentTriage.vex_justification || ''; + + // Build status options HTML + const statusOptionsHtml = Object.entries(VEX_STATUS_OPTIONS) + .map(([value, label]) => + `` + ).join(''); + + // Build justification options HTML + const justificationOptionsHtml = Object.entries(VEX_JUSTIFICATION_OPTIONS) + .map(([value, label]) => + `` + ).join(''); + + // Create VEX-compliant triage control HTML const triageHtml = ` -
-
- - - +
+
+ + + - Triage Controls + VEX Triage Assessment
- - + ${statusOptionsHtml} + Select the vulnerability exchange status
-
- - + + ${justificationOptionsHtml} + Required when status is "Not Affected"
- - -
- -
- - + + + Provide additional context, impact analysis, or mitigation details
-
- - Last modified: ${currentTriage.timestamp || 'Never'} - +
+
+ Status: ${VEX_STATUS_OPTIONS[currentStatus]} + ${currentTriage.timestamp ? `Last updated: ${new Date(currentTriage.timestamp).toLocaleString()}` : 'Not triaged yet'} +
+
`; @@ -159,38 +198,60 @@ function addTriageControlsToElement(element, cveId) { element.insertAdjacentHTML('beforeend', triageHtml); // Add event listeners for the controls - addTriageEventListeners(cveId); + addVexTriageEventListeners(cveId); + + // Apply visual state indication if already triaged + updateVisualStateIndication(cveId); } /** - * Add event listeners for triage controls + * Add event listeners for VEX triage controls */ -function addTriageEventListeners(cveId) { - const controls = document.querySelector(`[data-cve="${cveId}"].triage-controls`); +function addVexTriageEventListeners(cveId) { + const controls = document.querySelector(`[data-cve="${cveId}"].vex-triage-controls`); if (!controls) return; - // Status change - const statusSelect = controls.querySelector('.triage-status'); + // Status change handler + const statusSelect = controls.querySelector('.vex-status-select'); statusSelect.addEventListener('change', (e) => { - updateTriageData(cveId, 'remarks', e.target.value); + const newStatus = e.target.value; + updateTriageData(cveId, 'vex_status', newStatus); + + // Show/hide justification dropdown based on status + const justificationContainer = controls.querySelector('.vex-justification-container'); + if (newStatus === 'not_affected') { + justificationContainer.style.display = 'block'; + // Make justification required + const justificationSelect = controls.querySelector('.vex-justification-select'); + justificationSelect.setAttribute('required', 'required'); + } else { + justificationContainer.style.display = 'none'; + // Clear justification if not "Not Affected" + const justificationSelect = controls.querySelector('.vex-justification-select'); + justificationSelect.removeAttribute('required'); + justificationSelect.value = ''; + updateTriageData(cveId, 'vex_justification', ''); + } + + updateStatusIndicator(cveId, newStatus); }); - // Priority change - const prioritySelect = controls.querySelector('.triage-priority'); - prioritySelect.addEventListener('change', (e) => { - updateTriageData(cveId, 'priority', e.target.value); + // Justification change handler + const justificationSelect = controls.querySelector('.vex-justification-select'); + justificationSelect.addEventListener('change', (e) => { + updateTriageData(cveId, 'vex_justification', e.target.value); }); - // Comments change - const commentsTextarea = controls.querySelector('.triage-comments'); - commentsTextarea.addEventListener('blur', (e) => { - updateTriageData(cveId, 'comments', e.target.value); + // Detail/comment change handler + const detailTextarea = controls.querySelector('.vex-detail-textarea'); + detailTextarea.addEventListener('blur', (e) => { + updateTriageData(cveId, 'vex_detail', e.target.value); }); - // Justification change - const justificationTextarea = controls.querySelector('.triage-justification'); - justificationTextarea.addEventListener('blur', (e) => { - updateTriageData(cveId, 'justification', e.target.value); + // Clear triage button handler + const clearBtn = controls.querySelector('.clear-triage-btn'); + clearBtn.addEventListener('click', () => { + clearTriageData(cveId); }); } @@ -205,51 +266,121 @@ function updateTriageData(cveId, field, value) { triageChanges[cveId][field] = value; triageChanges[cveId].timestamp = new Date().toISOString(); - // Update timestamp display - const timestampElement = document.querySelector(`[data-cve="${cveId}"] .triage-timestamp`); - if (timestampElement) { - timestampElement.textContent = new Date().toLocaleString(); - } - // Auto-save changes saveTriageChanges(); // Update visual indicators - updateTriageIndicators(cveId); + updateVisualStateIndication(cveId); console.log(`Updated triage data for ${cveId}:`, triageChanges[cveId]); } /** - * Update visual indicators based on triage changes + * Update the status indicator badge + */ +function updateStatusIndicator(cveId, status) { + const controls = document.querySelector(`[data-cve="${cveId}"].vex-triage-controls`); + if (!controls) return; + + const statusIndicator = controls.querySelector('.triage-status-indicator .badge'); + if (statusIndicator) { + statusIndicator.textContent = `Status: ${VEX_STATUS_OPTIONS[status]}`; + statusIndicator.className = `badge ${getStatusBadgeClass(status)}`; + } + + const timestampSpan = controls.querySelector('.triage-status-indicator small'); + if (timestampSpan) { + timestampSpan.textContent = `Last updated: ${new Date().toLocaleString()}`; + } +} + +/** + * Get Bootstrap badge class for VEX status */ -function updateTriageIndicators(cveId) { +function getStatusBadgeClass(status) { + const statusClasses = { + 'not_affected': 'bg-success', + 'affected': 'bg-danger', + 'under_investigation': 'bg-warning', + 'fixed': 'bg-info' + }; + return statusClasses[status] || 'bg-secondary'; +} + +/** + * Clear triage data for a specific CVE + */ +function clearTriageData(cveId) { + if (confirm(`Are you sure you want to clear triage data for ${cveId}?`)) { + delete triageChanges[cveId]; + saveTriageChanges(); + + // Reset the form to defaults + const controls = document.querySelector(`[data-cve="${cveId}"].vex-triage-controls`); + if (controls) { + controls.querySelector('.vex-status-select').value = 'under_investigation'; + controls.querySelector('.vex-justification-select').value = ''; + controls.querySelector('.vex-detail-textarea').value = ''; + controls.querySelector('.vex-justification-container').style.display = 'none'; + + updateStatusIndicator(cveId, 'under_investigation'); + updateVisualStateIndication(cveId); + } + } +} + +/** + * Update visual state indication for triaged CVEs + */ +function updateVisualStateIndication(cveId) { const triage = triageChanges[cveId]; - if (!triage) return; - - // Find the CVE element and update its status badge - const cveElements = document.querySelectorAll(`[id*="${cveId}"]`); - cveElements.forEach(element => { - // Update status badges - const badges = element.querySelectorAll('.badge'); - badges.forEach(badge => { - if (badge.textContent === 'NEW' || badge.textContent === 'UNEXPLORED') { - if (triage.remarks === 'Mitigated') { - badge.textContent = 'MITIGATED'; - badge.className = 'badge rounded-pill bg-success'; - } else if (triage.remarks === 'FalsePositive') { - badge.textContent = 'FALSE POSITIVE'; - badge.className = 'badge rounded-pill bg-secondary'; - } else if (triage.remarks === 'NotAffected') { - badge.textContent = 'NOT AFFECTED'; - badge.className = 'badge rounded-pill bg-info'; - } else if (triage.remarks === 'Confirmed') { - badge.textContent = 'CONFIRMED'; - badge.className = 'badge rounded-pill bg-warning'; + const cveElement = document.querySelector(`[id*="${cveId}"]`); + + if (!cveElement) return; + + // Find the parent list item that contains this CVE + let listItem = cveElement; + while (listItem && !listItem.classList.contains('list-group-item')) { + listItem = listItem.parentElement; + } + + if (listItem) { + if (triage && triage.vex_status) { + // Apply visual indication for triaged items + listItem.classList.add('triaged-item'); + listItem.style.backgroundColor = '#f8f9fa'; + listItem.style.borderLeft = '4px solid #28a745'; + + // Add triage badge + let triageBadge = listItem.querySelector('.triage-status-badge'); + if (!triageBadge) { + triageBadge = document.createElement('span'); + triageBadge.className = 'badge rounded-pill triage-status-badge ms-2'; + + // Find the CVE number element to append the badge + const cveNumberElement = listItem.querySelector('[id*="' + cveId + '"]'); + if (cveNumberElement) { + cveNumberElement.appendChild(triageBadge); } } - }); - }); + + if (triageBadge) { + triageBadge.textContent = VEX_STATUS_OPTIONS[triage.vex_status]; + triageBadge.className = `badge rounded-pill triage-status-badge ms-2 ${getStatusBadgeClass(triage.vex_status)}`; + } + } else { + // Remove visual indication for non-triaged items + listItem.classList.remove('triaged-item'); + listItem.style.backgroundColor = ''; + listItem.style.borderLeft = ''; + + // Remove triage badge + const triageBadge = listItem.querySelector('.triage-status-badge'); + if (triageBadge) { + triageBadge.remove(); + } + } + } } /** @@ -257,7 +388,7 @@ function updateTriageIndicators(cveId) { */ function applyTriageChangesToUI() { Object.keys(triageChanges).forEach(cveId => { - updateTriageIndicators(cveId); + updateVisualStateIndication(cveId); }); } @@ -271,21 +402,27 @@ function addTriageButtons() { const buttonGroup = document.createElement('div'); buttonGroup.className = 'btn-group ms-2'; buttonGroup.innerHTML = ` - - - + +
+ + +
+ + + `; + + // Remove existing modal if present + const existingModal = document.getElementById('triageSummaryModal'); + if (existingModal) { + existingModal.remove(); + } + + // Add modal to page + document.body.insertAdjacentHTML('beforeend', modalHtml); + + // Show modal + const modal = new bootstrap.Modal(document.getElementById('triageSummaryModal')); + modal.show(); +} + +/** + * Clear all VEX triage data */ -function clearAllTriage() { - if (confirm('Are you sure you want to clear all triage data? This action cannot be undone.')) { +function clearAllVexTriage() { + if (confirm('Are you sure you want to clear all VEX triage data? This action cannot be undone.')) { triageChanges = {}; localStorage.removeItem('cve-bin-tool-triage'); location.reload(); @@ -370,36 +629,49 @@ function clearAllTriage() { } /** - * Get summary of triage changes + * Get summary of VEX triage changes */ -function getTriageSummary() { +function getVexTriageSummary() { const summary = { total_triaged: Object.keys(triageChanges).length, - by_status: {}, - by_priority: {} + by_status: {} }; Object.values(triageChanges).forEach(triage => { - // Count by status - const status = triage.remarks || 'Unknown'; + const status = triage.vex_status || 'under_investigation'; summary.by_status[status] = (summary.by_status[status] || 0) + 1; - - // Count by priority - const priority = triage.priority || 'Unset'; - summary.by_priority[priority] = (summary.by_priority[priority] || 0) + 1; }); return summary; } +/** + * Validate VEX triage data completeness + */ +function validateVexTriage() { + const errors = []; + + Object.entries(triageChanges).forEach(([cveId, triage]) => { + // Check if "not_affected" status has justification + if (triage.vex_status === 'not_affected' && !triage.vex_justification) { + errors.push(`${cveId}: "Not Affected" status requires justification`); + } + + // Check if detail is provided for meaningful statuses + if (['affected', 'fixed'].includes(triage.vex_status) && !triage.vex_detail) { + errors.push(`${cveId}: "${VEX_STATUS_OPTIONS[triage.vex_status]}" status should include impact details`); + } + }); + + return errors; +} + // Initialize triage functionality when the DOM is loaded document.addEventListener('DOMContentLoaded', initializeTriage); -// Make functions available globally -window.triageFunctions = { - initializeTriage, - saveTriageToFile, - loadTriageFromFile, - clearAllTriage, - getTriageSummary -}; +// Make functions available globally for button onclick handlers +window.exportVexTriageData = exportVexTriageData; +window.importVexTriageData = importVexTriageData; +window.showTriageSummary = showTriageSummary; +window.clearAllVexTriage = clearAllVexTriage; +window.validateVexTriage = validateVexTriage; From fb480d01490842da47910fb102521b30be971c6c Mon Sep 17 00:00:00 2001 From: JigyasuRajput Date: Wed, 20 Aug 2025 22:57:25 +0530 Subject: [PATCH 3/6] feat(html): add helper functions --- .../output_engine/html_reports/js/triage.js | 303 ++++++++++++++++-- 1 file changed, 272 insertions(+), 31 deletions(-) diff --git a/cve_bin_tool/output_engine/html_reports/js/triage.js b/cve_bin_tool/output_engine/html_reports/js/triage.js index f89ae21659..82284ea50a 100644 --- a/cve_bin_tool/output_engine/html_reports/js/triage.js +++ b/cve_bin_tool/output_engine/html_reports/js/triage.js @@ -402,12 +402,19 @@ function addTriageButtons() { const buttonGroup = document.createElement('div'); buttonGroup.className = 'btn-group ms-2'; buttonGroup.innerHTML = ` - + + + + + + `; + + document.body.appendChild(modal); + const bsModal = new bootstrap.Modal(modal); + bsModal.show(); + + modal.addEventListener('hidden.bs.modal', () => { + document.body.removeChild(modal); + }); +} + +/** + * Show import success message + */ +function showImportSuccess(importedCount, skippedCount) { + const modal = document.createElement('div'); + modal.className = 'modal fade'; + modal.innerHTML = ` + + `; + + document.body.appendChild(modal); + const bsModal = new bootstrap.Modal(modal); + bsModal.show(); + + modal.addEventListener('hidden.bs.modal', () => { + document.body.removeChild(modal); + }); +} + /** * Show triage summary in a modal */ @@ -823,7 +1105,7 @@ function showTriageSummary() { From 399d32950bbb141a84831be57b51f716e9a20a89 Mon Sep 17 00:00:00 2001 From: JigyasuRajput Date: Thu, 21 Aug 2025 11:44:46 +0530 Subject: [PATCH 5/6] feat(html): Add tests --- test/pages/html_report.py | 11 +++++++ test/test_html.py | 28 ++++++++++++++++ test/test_triage_js.py | 59 ++++++++++++++++++++++++++++++++++ test/test_triage_js_content.py | 54 +++++++++++++++++++++++++++++++ 4 files changed, 152 insertions(+) create mode 100644 test/test_triage_js.py create mode 100644 test/test_triage_js_content.py diff --git a/test/pages/html_report.py b/test/pages/html_report.py index ca3e044fa0..1f7dc6c4fa 100644 --- a/test/pages/html_report.py +++ b/test/pages/html_report.py @@ -78,6 +78,17 @@ def __init__(self, page: Page, all_cve_data: dict[ProductInfo, CVEData]): self.cve_summary_table = page.locator("#cveSummary tbody tr") self.cve_remarks_table = page.locator("#cveRemarks tbody tr") + # VEX Triage selectors + self.vex_triage_controls = page.locator(".vex-triage-controls") + self.vex_status_select = page.locator(".vex-status-select") + self.vex_justification_select = page.locator(".vex-justification-select") + self.vex_detail_textarea = page.locator(".vex-detail-textarea") + self.save_vex_button = page.locator("button:has-text('Save VEX File')") + self.save_report_button = page.locator("button:has-text('Save Updated Report')") + self.import_vex_button = page.locator("button:has-text('Import VEX')") + self.summary_button = page.locator("button:has-text('Summary')") + self.clear_all_button = page.locator("button:has-text('Clear All')") + def load(self) -> None: self.page.goto(f"file://{self.html_output.name}") diff --git a/test/test_html.py b/test/test_html.py index 7b5c528ad6..e8b3be5e7d 100644 --- a/test/test_html.py +++ b/test/test_html.py @@ -350,3 +350,31 @@ def test_unknown_cve_number(self) -> None: self.html_report_page.load() product_rows = self.html_report_page.product_rows expect(product_rows).to_have_count(1) + + def test_vex_triage_buttons_in_navbar(self) -> None: + """Test that VEX triage buttons are present in HTML reports.""" + # Check for essential triage buttons + save_vex_button = self.page.locator("button:has-text('Save VEX File')") + save_report_button = self.page.locator("button:has-text('Save Updated Report')") + + # These buttons should be present if triage.js is loaded + if save_vex_button.count() > 0: + expect(save_vex_button).to_be_visible() + if save_report_button.count() > 0: + expect(save_report_button).to_be_visible() + + def test_vex_triage_data_persistence_basic(self) -> None: + """Test basic localStorage functionality for VEX triage.""" + # Test localStorage operations directly + self.page.evaluate( + """ + localStorage.setItem('cve-bin-tool-triage', '{"test": "data"}'); + """ + ) + + # Check that data is stored + stored_data = self.page.evaluate("localStorage.getItem('cve-bin-tool-triage')") + assert stored_data == '{"test": "data"}' + + # Clear storage for cleanup + self.page.evaluate("localStorage.removeItem('cve-bin-tool-triage')") diff --git a/test/test_triage_js.py b/test/test_triage_js.py new file mode 100644 index 0000000000..091469e8f5 --- /dev/null +++ b/test/test_triage_js.py @@ -0,0 +1,59 @@ +# Copyright (C) 2025 Intel Corporation +# SPDX-License-Identifier: GPL-3.0-or-later + +""" +Essential tests for VEX triage JavaScript functionality integration. +These tests run only the most critical checks without complex Playwright setup. +""" + +from pathlib import Path + + +class TestTriageJSIntegration: + """Test basic triage.js integration points.""" + + def test_triage_js_properly_structured(self) -> None: + """Test that triage.js has proper JavaScript structure.""" + triage_js_path = ( + Path(__file__).parent.parent + / "cve_bin_tool/output_engine/html_reports/js/triage.js" + ) + content = triage_js_path.read_text() + + # Test for basic JavaScript structure + assert "function " in content, "Should contain JavaScript functions" + assert "addEventListener" in content, "Should use event listeners" + assert "document." in content, "Should interact with DOM" + + def test_vex_constants_defined(self) -> None: + """Test that VEX constants are properly defined.""" + triage_js_path = ( + Path(__file__).parent.parent + / "cve_bin_tool/output_engine/html_reports/js/triage.js" + ) + content = triage_js_path.read_text() + + # Test VEX status options + assert "'not_affected'" in content, "Should define not_affected status" + assert "'affected'" in content, "Should define affected status" + assert ( + "'under_investigation'" in content + ), "Should define under_investigation status" + assert "'fixed'" in content, "Should define fixed status" + + def test_export_functions_exist(self) -> None: + """Test that export functions are defined.""" + triage_js_path = ( + Path(__file__).parent.parent + / "cve_bin_tool/output_engine/html_reports/js/triage.js" + ) + content = triage_js_path.read_text() + + # Test critical export functions + assert ( + "window.saveCycloneDxVexFile" in content + ), "Should expose saveCycloneDxVexFile globally" + assert ( + "window.saveUpdatedReport" in content + ), "Should expose saveUpdatedReport globally" + assert "Blob(" in content, "Should support file downloads via Blob" diff --git a/test/test_triage_js_content.py b/test/test_triage_js_content.py new file mode 100644 index 0000000000..6a3add3641 --- /dev/null +++ b/test/test_triage_js_content.py @@ -0,0 +1,54 @@ +# Copyright (C) 2025 Intel Corporation +# SPDX-License-Identifier: GPL-3.0-or-later + +""" +Essential tests for triage.js file presence and content validation. +These tests ensure the VEX triage functionality is properly implemented. +""" + +from pathlib import Path + + +class TestTriageJSContent: + """Test the content of triage.js file for required functions and constants.""" + + def test_triage_js_file_exists(self) -> None: + """Test that triage.js file exists.""" + triage_js_path = ( + Path(__file__).parent.parent + / "cve_bin_tool/output_engine/html_reports/js/triage.js" + ) + assert triage_js_path.exists(), "triage.js file should exist" + + def test_essential_vex_functions_present(self) -> None: + """Test that essential VEX functions are present in triage.js.""" + triage_js_path = ( + Path(__file__).parent.parent + / "cve_bin_tool/output_engine/html_reports/js/triage.js" + ) + content = triage_js_path.read_text() + + # Only test the most critical functions + essential_functions = [ + "saveCycloneDxVexFile", + "initializeTriage", + "VEX_STATUS_OPTIONS", + ] + + for func in essential_functions: + assert ( + func in content + ), f"Essential function/constant {func} should be present in triage.js" + + def test_cyclonedx_vex_support(self) -> None: + """Test that CycloneDX VEX format is supported.""" + triage_js_path = ( + Path(__file__).parent.parent + / "cve_bin_tool/output_engine/html_reports/js/triage.js" + ) + content = triage_js_path.read_text() + + # Test for CycloneDX VEX format support + assert "bomFormat" in content, "CycloneDX bomFormat should be supported" + assert "CycloneDX" in content, "CycloneDX format should be referenced" + assert "specVersion" in content, "CycloneDX specVersion should be included" From eeb3689a823ffd35f42a42c3fb262bedcce99039 Mon Sep 17 00:00:00 2001 From: JigyasuRajput Date: Thu, 21 Aug 2025 18:59:26 +0530 Subject: [PATCH 6/6] feat(html): Fix failing tests --- cve_bin_tool/output_engine/html.py | 2 +- .../output_engine/html_reports/js/triage.js | 25 ++++++++++++++----- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/cve_bin_tool/output_engine/html.py b/cve_bin_tool/output_engine/html.py index 6a9f7f5913..bf4ad7681a 100644 --- a/cve_bin_tool/output_engine/html.py +++ b/cve_bin_tool/output_engine/html.py @@ -88,7 +88,7 @@ def serialize_vulnerability_data( "vendor": product_info.vendor, "product": product_info.product, "version": product_info.version, - "location": product_info.location, + "location": product_info.purl or "", "purl": product_info.purl, "paths": list(cve_data.get("paths", set())), "cve_count": len(cve_data.get("cves", [])), diff --git a/cve_bin_tool/output_engine/html_reports/js/triage.js b/cve_bin_tool/output_engine/html_reports/js/triage.js index 8a7196ab08..bc55761a75 100644 --- a/cve_bin_tool/output_engine/html_reports/js/triage.js +++ b/cve_bin_tool/output_engine/html_reports/js/triage.js @@ -141,9 +141,22 @@ function addTriageControlsToElement(element, cveId) { `` ).join(''); + // Escape HTML to prevent XSS + const escapeHtml = (unsafe) => { + return unsafe + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); + }; + + const escapedCveId = escapeHtml(cveId); + const escapedDetail = escapeHtml(currentTriage.vex_detail || vulnerability.comments || ''); + // Create VEX-compliant triage control HTML const triageHtml = ` -
+
@@ -155,7 +168,7 @@ function addTriageControlsToElement(element, cveId) {
- ${statusOptionsHtml} Select the vulnerability exchange status @@ -163,7 +176,7 @@ function addTriageControlsToElement(element, cveId) {
- ${justificationOptionsHtml} @@ -173,8 +186,8 @@ function addTriageControlsToElement(element, cveId) {
- + Provide additional context, impact analysis, or mitigation details
@@ -183,7 +196,7 @@ function addTriageControlsToElement(element, cveId) { Status: ${VEX_STATUS_OPTIONS[currentStatus]} ${currentTriage.timestamp ? `Last updated: ${new Date(currentTriage.timestamp).toLocaleString()}` : 'Not triaged yet'}
-