Add files via upload

This commit is contained in:
公明
2026-08-06 17:12:21 +08:00
committed by GitHub
parent 30513dbbfd
commit d3714c9913
15 changed files with 435 additions and 214 deletions
+28 -15
View File
@@ -1385,18 +1385,19 @@ function renderVulnerabilities(vulnerabilities, renderOptions) {
? escapeHtml(typeof getProjectName === 'function' ? getProjectName(vuln.project_id) : vuln.project_id)
: escapeHtml(vulnT('vulnerabilityPage.projectUnbound'));
const projectBadge = vuln.project_id
? `<span class="vulnerability-project-badge" title="${escapeHtml(vuln.project_id)}">${escapeHtml(vulnT('vulnerabilityPage.detailProject'))}: ${projectLabel}</span>`
? `<span class="vulnerability-project-badge" title="${escapeAttr(vuln.project_id)}">${escapeHtml(vulnT('vulnerabilityPage.detailProject'))}: ${projectLabel}</span>`
: `<span class="vulnerability-project-badge vulnerability-project-badge--unbound">${escapeHtml(vulnT('vulnerabilityPage.projectUnbound'))}</span>`;
const dlTitle = escapeHtml(vulnT('vulnerabilityPage.downloadMarkdownTitle'));
const editTitle = escapeHtml(vulnT('common.edit'));
const deleteTitle = escapeHtml(vulnT('common.delete'));
const vulnIdJs = escapeJsStringAttr(vuln.id);
return `
<div class="vulnerability-card ${severityClass}" id="vulnerability-card-${vuln.id}" data-vuln-id="${escapeHtml(vuln.id)}">
<div class="vulnerability-header" onclick="toggleVulnerabilityDetails('${vuln.id}')" style="cursor: pointer;">
<div class="vulnerability-card ${severityClass}" id="vulnerability-card-${escapeAttr(vuln.id)}" data-vuln-id="${escapeAttr(vuln.id)}">
<div class="vulnerability-header" onclick="toggleVulnerabilityDetails(${vulnIdJs})" style="cursor: pointer;">
<div class="vulnerability-title-section">
<div style="display: flex; align-items: center; gap: 8px;">
<svg class="vulnerability-expand-icon" id="expand-icon-${vuln.id}" width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" style="transition: transform 0.2s ease; flex-shrink: 0;">
<svg class="vulnerability-expand-icon" id="expand-icon-${escapeAttr(vuln.id)}" width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" style="transition: transform 0.2s ease; flex-shrink: 0;">
<path d="M9 18l6-6-6-6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<h3 class="vulnerability-title">${escapeHtml(vuln.title)}</h3>
@@ -1409,20 +1410,20 @@ function renderVulnerabilities(vulnerabilities, renderOptions) {
</div>
</div>
<div class="vulnerability-actions" onclick="event.stopPropagation();">
<button class="btn-ghost" onclick="downloadVulnerabilityAsMarkdown('${vuln.id}', event)" title="${dlTitle}">
<button class="btn-ghost" onclick="downloadVulnerabilityAsMarkdown(${vulnIdJs}, event)" title="${dlTitle}">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<polyline points="7 10 12 15 17 10" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<line x1="12" y1="15" x2="12" y2="3" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button>
<button class="btn-ghost" onclick="editVulnerability('${vuln.id}')" title="${editTitle}">
<button class="btn-ghost" onclick="editVulnerability(${vulnIdJs})" title="${editTitle}">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button>
<button class="btn-ghost" data-require-permission="vulnerability:delete" onclick="deleteVulnerability('${vuln.id}')" title="${deleteTitle}">
<button class="btn-ghost" data-require-permission="vulnerability:delete" onclick="deleteVulnerability(${vulnIdJs})" title="${deleteTitle}">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 6h18M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2m3 0v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6h14z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
@@ -1450,7 +1451,7 @@ function renderVulnerabilities(vulnerabilities, renderOptions) {
${vulnNarrativeSection(vulnT('vulnerabilityPage.detailRecommendation'), vuln.recommendation)}
${vulnNarrativeSection(vulnT('vulnerabilityPage.detailRetestNotes'), vuln.retest_notes)}
</div>
<div class="vulnerability-related-facts" id="vuln-related-facts-${vuln.id}" data-project-id="${escapeHtml(vuln.project_id || '')}" data-vuln-id="${escapeHtml(vuln.id)}" hidden></div>
<div class="vulnerability-related-facts" id="vuln-related-facts-${escapeAttr(vuln.id)}" data-project-id="${escapeAttr(vuln.project_id || '')}" data-vuln-id="${escapeAttr(vuln.id)}" hidden></div>
</div>
</div>
`;
@@ -1557,10 +1558,10 @@ function buildVulnerabilityProjectOptionsHtml(selectedId) {
entries.forEach(([id, name]) => {
if (!id) return;
const selected = id === sel ? ' selected' : '';
html += `<option value="${escapeHtml(id)}"${selected}>${escapeHtml(name || id)}</option>`;
html += `<option value="${escapeAttr(id)}"${selected}>${escapeHtml(name || id)}</option>`;
});
if (sel && !entries.some(([id]) => id === sel)) {
html += `<option value="${escapeHtml(sel)}" selected>${escapeHtml(sel)}</option>`;
html += `<option value="${escapeAttr(sel)}" selected>${escapeHtml(sel)}</option>`;
}
return html;
}
@@ -1974,8 +1975,8 @@ async function loadVulnerabilityRelatedFacts(vulnId) {
.map((f) => {
const key = escapeHtml(f.fact_key);
const sum = escapeHtml((f.summary || '').slice(0, 120));
const pid = escapeHtml(projectId);
const rawKey = escapeHtml(f.fact_key);
const pid = escapeAttr(projectId);
const rawKey = escapeAttr(f.fact_key);
return `<li><a role="button" href="#" data-project-id="${pid}" data-fact-key="${rawKey}" onclick="event.preventDefault();openProjectFactFromVulnerability(this.dataset.projectId,this.dataset.factKey)"><code>${key}</code></a> — ${sum}</li>`;
})
.join('');
@@ -2016,6 +2017,18 @@ function escapeHtml(text) {
return div.innerHTML;
}
function escapeJsString(text) {
return JSON.stringify(String(text == null ? '' : text));
}
function escapeAttr(text) {
return escapeHtml(text).replace(/"/g, '&quot;').replace(/'/g, '&#39;');
}
function escapeJsStringAttr(text) {
return escapeAttr(escapeJsString(text));
}
/** 复制详情字段(编码由 encodeURIComponent 传入,避免引号截断) */
function vulnerabilityCopyEncoded(evt, encoded) {
if (evt && evt.stopPropagation) {
@@ -2076,9 +2089,9 @@ function vulnDetailProjectField(vuln) {
return `<div class="vuln-detail-field">
<div class="vuln-detail-field__label">${escapeHtml(label)}</div>
<div class="vuln-detail-field__row">
<select class="vulnerability-project-bind-select" data-vuln-id="${escapeHtml(vuln.id)}"
<select class="vulnerability-project-bind-select" data-vuln-id="${escapeAttr(vuln.id)}"
onchange="bindVulnerabilityProject(this.dataset.vulnId, this.value, true)"
title="${hint}" aria-label="${escapeHtml(label)}">
title="${escapeAttr(hint)}" aria-label="${escapeAttr(label)}">
${buildVulnerabilityProjectOptionsHtml(vuln.project_id || '')}
</select>
<span class="vuln-detail-field__copy-spacer" aria-hidden="true"></span>
@@ -2450,7 +2463,7 @@ async function refreshVulnerabilityProjectFilter() {
if (!p.id) return;
const selected = p.id === cur ? ' selected' : '';
const arch = p.status === 'archived' ? ' [' + vulnT('projects.archived') + ']' : '';
html += `<option value="${escapeHtml(p.id)}"${selected}>${escapeHtml(p.name || p.id)}${arch}</option>`;
html += `<option value="${escapeAttr(p.id)}"${selected}>${escapeHtml(p.name || p.id)}${arch}</option>`;
});
sel.innerHTML = html;
if (cur) sel.value = cur;