From 75ec563bd3d9e5054ab3a6903947783b72e00665 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=85=AC=E6=98=8E?= <83812544+Ed1s0nZ@users.noreply.github.com> Date: Mon, 20 Jul 2026 11:47:05 +0800 Subject: [PATCH] Add files via upload --- web/static/css/style.css | 168 ++++++++++++++++++++++++++++++++++--- web/static/i18n/en-US.json | 2 + web/static/i18n/zh-CN.json | 2 + web/static/js/assets.js | 36 +++++++- web/static/js/projects.js | 26 +++++- 5 files changed, 216 insertions(+), 18 deletions(-) diff --git a/web/static/css/style.css b/web/static/css/style.css index b4ab4d9a..659e0e36 100644 --- a/web/static/css/style.css +++ b/web/static/css/style.css @@ -40460,30 +40460,88 @@ input.theme-checkbox:disabled { .asset-editor-group-heading p { min-width: 0; margin: 0; color: var(--text-secondary, #64748b); font-size: 12px; font-weight: 400; line-height: 1.45; } .asset-editor-wide { grid-column: 1 / -1; } .asset-editor-submit-busy { pointer-events: none; opacity: .7; } -.asset-detail-modal { overflow: hidden; } +.asset-detail-modal { overflow: hidden; background: rgba(15,23,42,.48); } .asset-detail-modal .asset-detail-content { position: absolute; top: 0; right: 0; - width: min(620px, 92vw); + width: min(680px, 94vw); max-width: none; height: 100%; max-height: none; margin: 0; border-width: 0 0 0 1px; - border-radius: 18px 0 0 18px; - animation: assetDrawerIn .22s ease-out; + border-color: var(--border-color, #e5e7eb); + border-radius: 16px 0 0 16px; + background: var(--bg-primary, #fff); + box-shadow: -12px 0 32px rgba(15,23,42,.14); + animation: assetDrawerIn .20s ease-out; +} +.asset-detail-modal .modal-header { + align-items: flex-start; + padding: 20px 24px 16px; + border-radius: 16px 0 0 0; + background: var(--bg-primary, #fff); + box-shadow: none; +} +.asset-detail-modal .modal-header h2 { font-size: 19px; letter-spacing: 0; } +.asset-detail-modal .modal-header .asset-page-subtitle { max-width: 520px; margin-top: 6px; font-size: 13px; color: var(--text-secondary, #64748b); overflow-wrap: anywhere; } +.asset-detail-modal .modal-close { margin-top: -2px; } +.asset-detail-modal .modal-body { + padding: 18px 24px 22px; + background: transparent; + overscroll-behavior: contain; + scrollbar-gutter: stable; +} +.asset-detail-modal .modal-footer { + flex-shrink: 0; + padding: 14px 24px; + border-top: 1px solid var(--border-color, #e5e7eb); + background: var(--bg-primary, #fff); + box-shadow: 0 -6px 18px rgba(15,23,42,.04); } -.asset-detail-modal .modal-header { border-radius: 18px 0 0 0; } -.asset-detail-modal .modal-body { padding: 20px; } -.asset-detail-modal .modal-footer { flex-shrink: 0; } @keyframes assetDrawerIn { from { opacity: .7; transform: translateX(100%); } to { opacity: 1; transform: translateX(0); } } -.asset-detail-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 1px; overflow: hidden; border: 1px solid var(--border-color, #e5e7eb); border-radius: 12px; background: var(--border-color, #e5e7eb); } -.asset-detail-item { min-width: 0; padding: 15px 16px; background: var(--card-bg, #fff); } +.asset-detail-grid { display: flex; flex-direction: column; gap: 14px; } +.asset-detail-overview { + padding: 16px; + border: 1px solid var(--border-color, #e5e7eb); + border-radius: 8px; + background: var(--card-bg, #fff); + box-shadow: none; +} +.asset-detail-overview-main { min-width: 0; } +.asset-detail-target { color: var(--text-primary, #111827); font-size: 18px; font-weight: 700; line-height: 1.35; overflow-wrap: anywhere; } +.asset-detail-meta-line { margin-top: 6px; color: var(--text-secondary, #64748b); font-size: 13px; line-height: 1.45; overflow-wrap: anywhere; } +.asset-detail-overview-badges { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 12px; } +.asset-detail-overview-badge { + display: inline-flex; + max-width: 100%; + align-items: center; + gap: 5px; + padding: 3px 8px; + border: 1px solid var(--border-color, #e5e7eb); + border-radius: 6px; + background: var(--bg-secondary, #f8fafc); + color: var(--text-primary, #111827); + font-size: 12px; + font-weight: 600; + overflow-wrap: anywhere; +} +.asset-detail-overview-badge span { color: var(--text-secondary, #64748b); font-weight: 500; } +.asset-detail-overview-badge--success { color: #047857; } +.asset-detail-overview-badge--neutral { color: #475569; } +.asset-detail-overview-badge--critical,.asset-detail-overview-badge--high { color: #b91c1c; } +.asset-detail-overview-badge--medium { color: #b45309; } +.asset-detail-overview-badge--low,.asset-detail-overview-badge--info,.asset-detail-overview-badge--normal { color: #2563eb; } +.asset-detail-fields { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; } +.asset-detail-item { min-width: 0; padding: 14px 15px; border: 1px solid var(--border-color, #e5e7eb); border-radius: 8px; background: var(--card-bg, #fff); box-shadow: none; } +.asset-detail-item:hover { border-color: var(--border-color, #e5e7eb); box-shadow: none; transform: none; } .asset-detail-item--wide { grid-column: 1 / -1; } -.asset-detail-item > span { display: block; margin-bottom: 7px; color: var(--text-secondary, #64748b); font-size: 12px; } -.asset-detail-item > div { min-height: 20px; color: var(--text-primary, #111827); overflow-wrap: anywhere; } -.asset-detail-item code { display: block; padding: 9px; border-radius: 7px; background: var(--bg-secondary, #f4f6f8); white-space: pre-wrap; } +.asset-detail-item > span { display: block; margin-bottom: 7px; color: var(--text-secondary, #64748b); font-size: 12px; font-weight: 650; } +.asset-detail-item > div { min-height: 20px; color: var(--text-primary, #111827); font-size: 14px; line-height: 1.45; overflow-wrap: anywhere; } +.asset-detail-item .muted { color: var(--text-muted, #94a3b8); } +.asset-detail-item--code { background: var(--card-bg, #fff); } +.asset-detail-item code { display: block; max-height: 170px; padding: 10px; overflow: auto; border: 1px solid var(--border-color, #e5e7eb); border-radius: 6px; background: var(--bg-secondary, #f4f6f8); color: var(--text-primary, #111827); font-size: 12px; line-height: 1.55; white-space: pre-wrap; } /* Asset management dark theme */ html[data-theme="dark"] .asset-stat-card, @@ -40617,11 +40675,69 @@ html[data-theme="dark"] .theme-checkbox:indeterminate { } html[data-theme="dark"] .asset-detail-grid { + background: transparent; +} + +html[data-theme="dark"] .asset-detail-modal { + background: rgba(2,6,23,.62); +} + +html[data-theme="dark"] .asset-detail-modal .asset-detail-content { border-color: var(--border-color); - background: var(--border-color); + background: var(--bg-primary); + box-shadow: -12px 0 34px rgba(0,0,0,.32); +} + +html[data-theme="dark"] .asset-detail-modal .modal-header { + background: var(--bg-primary); + box-shadow: none; +} + +html[data-theme="dark"] .asset-detail-modal .modal-footer { + border-color: var(--border-color); + background: var(--bg-primary); + box-shadow: 0 -6px 18px rgba(0,0,0,.16); +} + +html[data-theme="dark"] .asset-detail-overview { + border-color: var(--border-color); + background: var(--card-bg); + box-shadow: none; +} + +html[data-theme="dark"] .asset-detail-overview-badge { + border-color: var(--border-color); + background: var(--bg-secondary); + color: var(--text-primary); +} + +html[data-theme="dark"] .asset-detail-overview-badge span { + color: #94a3b8; +} + +html[data-theme="dark"] .asset-detail-overview-badge--success { color: #6ee7b7; } +html[data-theme="dark"] .asset-detail-overview-badge--neutral { color: #cbd5e1; } +html[data-theme="dark"] .asset-detail-overview-badge--critical, +html[data-theme="dark"] .asset-detail-overview-badge--high { color: #fca5a5; } +html[data-theme="dark"] .asset-detail-overview-badge--medium { color: #fcd34d; } + +html[data-theme="dark"] .asset-detail-item { + border-color: var(--border-color); + background: var(--card-bg); + box-shadow: none; +} + +html[data-theme="dark"] .asset-detail-item:hover { + border-color: var(--border-color); + box-shadow: none; +} + +html[data-theme="dark"] .asset-detail-item--code { + background: var(--card-bg); } html[data-theme="dark"] .asset-detail-item code { + border-color: var(--border-color); background: var(--bg-secondary); color: var(--text-secondary); } @@ -40660,5 +40776,31 @@ html[data-theme="dark"] .asset-form-error { color: #fca5a5; } .asset-recent-card { min-height: 230px; } } @media (max-width: 1100px) { .asset-advanced-filters { grid-template-columns: repeat(3,minmax(140px,1fr)); } } +@media (max-width: 760px) { + .asset-detail-modal .asset-detail-content { + width: 100%; + border-left: 0; + border-radius: 0; + } + .asset-detail-modal .modal-header { + padding: 18px 18px 15px; + border-radius: 0; + } + .asset-detail-modal .modal-body { + padding: 16px 18px 20px; + } + .asset-detail-modal .modal-footer { + padding: 14px 18px; + } + .asset-detail-target { + font-size: 19px; + } + .asset-detail-fields { + grid-template-columns: 1fr; + } + .asset-detail-item--wide { + grid-column: auto; + } +} @media (max-width: 640px) { .asset-editor-grid,.asset-detail-grid { grid-template-columns: 1fr; } .asset-total-card,.asset-recent-card,.asset-panel { padding: 18px; } .asset-type-grid,.asset-coverage-metrics { grid-template-columns: 1fr; } .asset-type-item + .asset-type-item { border-top: 1px solid var(--border-color,#e5e7eb); border-left: 0; } .asset-trend-header { grid-template-columns: 1fr auto; grid-template-areas: "heading summary"; column-gap: 12px; } .asset-chart-legend { display: none; } .asset-chart-summary { justify-content: flex-end; padding: 0; } .asset-chart-summary span { display: none; } .asset-protocol-compact .asset-bar-row,.asset-protocol-compact .asset-protocol-columns { grid-template-columns: 72px 1fr 42px; gap: 8px; padding-right: 4px; padding-left: 4px; } .asset-protocol-compact .asset-bar-row small,.asset-protocol-compact .asset-protocol-columns span:last-child { display: none; } .asset-editor-content,.asset-import-content,.asset-bulk-edit-content { width: 100%; max-height: 100vh; min-height: 100vh; margin: 0; border-radius: 0; } .asset-editor-content .modal-header,.asset-editor-body,.asset-editor-content .modal-footer { padding-left: 16px; padding-right: 16px; } .asset-editor-subtitle { max-width: 270px; } .asset-editor-primary { padding: 14px; } .asset-editor-group-heading { align-items: flex-start; flex-wrap: wrap; gap: 3px 8px; } .asset-editor-group-heading p { width: 100%; } .asset-editor-section-group + .asset-editor-section-group { margin-top: 20px; padding-top: 20px; } .asset-editor-wide,.asset-detail-item--wide { grid-column: auto; } .asset-toolbar-main > input { min-width: 100%; } .asset-advanced-filters { grid-template-columns: 1fr 1fr; } .asset-saved-views { align-items: stretch; flex-direction: column; } .asset-saved-views select { width: 100%; min-width: 0; } .asset-custom-select--saved-view { width: 100%; min-width: 0; flex-basis: auto; } .asset-import-template-actions { align-items: flex-start; flex-direction: column; padding-left: 0; } .asset-import-preview-table th:nth-child(3),.asset-import-preview-table td:nth-child(3),.asset-import-preview-table th:nth-child(4),.asset-import-preview-table td:nth-child(4) { display: none; } .asset-batch-selection { flex-wrap: wrap; } .asset-batch-primary-actions { display: grid; grid-template-columns: 1fr 1fr; } .asset-batch-primary-actions > .btn-primary { grid-column: 1 / -1; grid-row: 1; } .asset-batch-more { width: 100%; } .asset-batch-more > summary { width: 100%; } .asset-batch-menu { right: auto; left: 0; width: min(260px,calc(100vw - 48px)); } } @media (max-width: 640px) { .asset-coverage-metrics > div + div,.asset-coverage-metrics > div:nth-child(4) { border-top: 1px solid var(--border-color,#e5e7eb); border-left: 0; } } diff --git a/web/static/i18n/en-US.json b/web/static/i18n/en-US.json index 66844eb6..5c7022e3 100644 --- a/web/static/i18n/en-US.json +++ b/web/static/i18n/en-US.json @@ -440,6 +440,8 @@ "open": "Open", "unbindProjectTitle": "Unbind project", "unbind": "Unbind", + "unbindAssetConfirm": "Unbind “{{target}}” from this project? The asset will not be deleted.", + "unbindAssetDone": "Asset unbound from project", "confirmUnbindConversation": "Unbind this conversation from current project?", "unbindFailed": "Unbind failed", "factMetaCategory": "Category: {{value}}", diff --git a/web/static/i18n/zh-CN.json b/web/static/i18n/zh-CN.json index 9a82b07a..06126cbc 100644 --- a/web/static/i18n/zh-CN.json +++ b/web/static/i18n/zh-CN.json @@ -428,6 +428,8 @@ "open": "打开", "unbindProjectTitle": "解除项目绑定", "unbind": "解绑", + "unbindAssetConfirm": "确定将“{{target}}”从当前项目解绑吗?资产不会被删除。", + "unbindAssetDone": "已从项目解绑资产", "confirmUnbindConversation": "解除该对话与当前项目的绑定?", "unbindFailed": "解绑失败", "factMetaCategory": "分类: {{value}}", diff --git a/web/static/js/assets.js b/web/static/js/assets.js index ab352886..94f9cf40 100644 --- a/web/static/js/assets.js +++ b/web/static/js/assets.js @@ -1630,8 +1630,36 @@ function importFofaRowAsset(index) { return importFofaAssetsByIndexes([Number(index)]); } -function assetDetailItem(label, value, wide) { - return `
${escapeHtml(label)}
${value || '-'}
`; +function assetDetailItem(label, value, wide, options) { + const itemClass = ['asset-detail-item']; + if (wide) itemClass.push('asset-detail-item--wide'); + if (options?.className) itemClass.push(options.className); + return `
${escapeHtml(label)}
${value || '-'}
`; +} + +function assetDetailBadge(label, value, modifier) { + if (!value) return ''; + const badgeClass = modifier ? ` asset-detail-overview-badge--${modifier}` : ''; + return `${escapeHtml(label)}${escapeHtml(value)}`; +} + +function assetDetailOverview(asset) { + const service = [asset.protocol, asset.port ? ':' + asset.port : ''].join(''); + const location = [asset.country, asset.province, asset.city].filter(Boolean).join(' / '); + const statusLabel = asset.status === 'inactive' ? assetT('assets.statusInactive', '停用') : assetT('assets.statusActive', '活跃'); + const risk = assetRiskPresentation(asset.risk_level); + return `
+
+
${escapeHtml(assetTargetLabel(asset))}
+
${escapeHtml([asset.ip, asset.domain, service].filter(Boolean).join(' · ') || '-')}
+
+
+ ${assetDetailBadge(assetT('assets.status', '状态'), statusLabel, asset.status === 'inactive' ? 'neutral' : 'success')} + ${assetDetailBadge(assetT('assets.risk', '风险'), risk.label, risk.level)} + ${assetDetailBadge(assetT('assets.source', '来源'), asset.source || '', 'source')} + ${assetDetailBadge(assetT('assets.location', '地区'), location, 'location')} +
+
`; } function openAssetDetail(index) { @@ -1662,7 +1690,7 @@ function openAssetDetailRecord(asset) { assetDetailItem(assetT('assets.environment', '环境'), escapeHtml(asset.environment || '')), assetDetailItem(assetT('assets.criticality', '重要性'), escapeHtml(asset.criticality || '')), assetDetailItem(assetT('assets.source', '来源'), escapeHtml(asset.source || '')), - assetDetailItem(assetT('assets.sourceQuery', '来源查询'), asset.source_query ? `${escapeHtml(asset.source_query)}` : ''), + assetDetailItem(assetT('assets.sourceQuery', '来源查询'), asset.source_query ? `${escapeHtml(asset.source_query)}` : '', true, { className: 'asset-detail-item--code' }), assetDetailItem(assetT('assets.tagsLabel', '标签'), tags, true), assetDetailItem(assetT('assets.firstSeen', '首次发现'), escapeHtml(asset.first_seen_at ? new Date(asset.first_seen_at).toLocaleString() : '')), assetDetailItem(assetT('assets.lastSeen', '最近发现'), escapeHtml(asset.last_seen_at ? new Date(asset.last_seen_at).toLocaleString() : '')), @@ -1670,7 +1698,7 @@ function openAssetDetailRecord(asset) { assetDetailItem(assetT('assets.relatedVulnerabilities', '相关漏洞'), String(Number(asset.vulnerability_count || 0))) ]; const grid = document.getElementById('asset-detail-grid'); - if (grid) grid.innerHTML = values.join(''); + if (grid) grid.innerHTML = assetDetailOverview(asset) + `
${values.join('')}
`; if (typeof applyRBACToUI === 'function') applyRBACToUI(document.getElementById('asset-detail-modal')); if (typeof openAppModal === 'function') openAppModal('asset-detail-modal'); else document.getElementById('asset-detail-modal').style.display = 'flex'; diff --git a/web/static/js/projects.js b/web/static/js/projects.js index d7d5e250..02557d5f 100644 --- a/web/static/js/projects.js +++ b/web/static/js/projects.js @@ -1096,7 +1096,7 @@ async function loadProjectAssets(page) { ${escapeHtml(asset.source || '-')} ${escapeHtml(updated)} ${escapeHtml(status)} -
+
`; }).join(''); renderProjectAssetsPagination(); @@ -1147,6 +1147,29 @@ function openProjectAssetDetail(index) { if (asset && typeof window.openAssetDetailRecord === 'function') window.openAssetDetailRecord(asset); } +async function unbindAssetFromProject(index) { + const asset = currentProjectAssets[Number(index)]; + if (!asset || !asset.id || !currentProjectId) return; + const target = asset.host || asset.domain || asset.ip || asset.id; + const message = tpFmt('projects.unbindAssetConfirm', `确定将“${target}”从当前项目解绑吗?资产不会被删除。`, { target }); + if (!confirm(message)) return; + try { + const res = await apiFetch('/api/assets/project-binding', { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ asset_ids: [asset.id], project_id: '' }) + }); + if (!res.ok) throw new Error(await res.text()); + if (typeof showInlineToast === 'function') { + showInlineToast(tpFmt('projects.unbindAssetDone', '已从项目解绑资产', { target })); + } + await loadProjectAssets(projectAssetsPagination.page); + await refreshProjectHeaderStats(); + } catch (error) { + alert(`${tp('projects.unbindFailed')}: ${error.message || error}`); + } +} + let _selectedGraphFactKey = null; let _selectedGraphEdgeId = null; let _currentGraphData = null; @@ -2879,6 +2902,7 @@ window.unbindConversationFromProject = unbindConversationFromProject; window.loadProjectConversations = loadProjectConversations; window.loadProjectAssets = loadProjectAssets; window.openProjectAssetDetail = openProjectAssetDetail; +window.unbindAssetFromProject = unbindAssetFromProject; window.loadProjectFactGraph = loadProjectFactGraph; window.filterProjectFactGraph = filterProjectFactGraph; window.centerProjectFactGraph = centerProjectFactGraph;