更多
+ +| 目标 | 服务 | 所属项目 | 上次扫描 | 相关漏洞 | 风险等级 | 状态 | 操作 | ||
|---|---|---|---|---|---|---|---|---|---|
| 暂无数据 | |||||||||
| 目标 | 服务 | 所属项目 | 归属 | 上次扫描 | 相关漏洞 | 风险等级 | 状态 | 操作 | |
| 暂无数据 | |||||||||
From 9f092388e13c3ffcfe4117df45d4f507d2421f2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=85=AC=E6=98=8E?= <83812544+Ed1s0nZ@users.noreply.github.com> Date: Fri, 17 Jul 2026 16:51:35 +0800 Subject: [PATCH] Add files via upload --- web/static/css/style.css | 447 +++++++++++++++++---- web/static/i18n/en-US.json | 122 +++++- web/static/i18n/zh-CN.json | 124 +++++- web/static/js/api-docs.js | 5 +- web/static/js/assets.js | 737 +++++++++++++++++++++++++++++++++- web/static/js/chat.js | 22 +- web/static/js/info-collect.js | 4 +- web/static/js/monitor.js | 336 ++++++++++++---- web/static/js/projects.js | 76 +++- web/static/js/rbac-guards.js | 2 + web/static/js/router.js | 42 +- web/static/js/settings.js | 74 +++- web/templates/index.html | 177 ++++++-- 13 files changed, 1913 insertions(+), 255 deletions(-) diff --git a/web/static/css/style.css b/web/static/css/style.css index b0b98acc..de0f55e7 100644 --- a/web/static/css/style.css +++ b/web/static/css/style.css @@ -3957,11 +3957,61 @@ html[data-theme="dark"] .new-chat-btn:focus-visible { } .process-details-content .progress-timeline.expanded { - max-height: 2000px; + max-height: min(70vh, 760px); overflow-x: hidden; overflow-y: auto; opacity: 1; margin-top: 12px; + scroll-behavior: smooth; + /* 详情内部可滚动时优先滚详情;到达边界或运行中取消内层滚动时, + 继续把滚轮自然传给外层 #chat-messages,避免鼠标悬停卡片后“滚不动”。 */ + overscroll-behavior: auto; + scrollbar-gutter: stable; +} + +.process-details-auto-sentinel { + display: block; + width: 100%; + min-height: 34px; + margin: 6px 0 10px; + border: 0; + border-radius: 8px; + background: transparent; + color: var(--text-secondary); + font-size: 12px; + cursor: default; +} + +.process-details-auto-sentinel.is-error { + color: var(--danger-color, #dc2626); + background: rgba(220, 38, 38, 0.08); + cursor: pointer; +} + +.process-details-jump-latest { + position: sticky; + bottom: 12px; + z-index: 4; + display: block; + width: max-content; + max-width: calc(100% - 24px); + margin: -42px 12px 10px auto; + padding: 7px 12px; + border: 1px solid rgba(59, 130, 246, 0.35); + border-radius: 999px; + background: var(--bg-primary); + color: var(--primary-color, #2563eb); + box-shadow: 0 4px 14px rgba(15, 23, 42, 0.14); + opacity: 0; + pointer-events: none; + transform: translateY(6px); + transition: opacity 0.18s ease, transform 0.18s ease; +} + +.process-details-jump-latest.visible { + opacity: 1; + pointer-events: auto; + transform: translateY(0); } .chat-input-container { @@ -5555,6 +5605,7 @@ html[data-theme="dark"] .login-card .login-submit:disabled { max-height: none; overflow-x: hidden; overflow-y: visible; + overscroll-behavior: auto; } .timeline-item { @@ -9142,6 +9193,10 @@ html[data-theme="dark"] .robot-binding-service-hint-icon { flex-shrink: 0; } +.tool-item > .theme-checkbox { + --theme-checkbox-size: 18px; +} + .tool-item-info { flex: 1; min-width: 0; @@ -9177,6 +9232,11 @@ html[data-theme="dark"] .robot-binding-service-hint-icon { margin: 0; } +.tool-resident-toggle .theme-checkbox { + --theme-checkbox-size: 14px; + border-radius: 3px; +} + .external-tool-badge { display: inline-flex; align-items: center; @@ -12932,35 +12992,33 @@ html[data-theme="dark"] .robot-binding-service-hint-icon { display: flex; gap: 6px; min-width: 0; - overflow-x: auto; - overflow-y: hidden; flex: 1 1 auto; - flex-wrap: nowrap; + flex-wrap: wrap; padding: 1px 1px 3px; - scrollbar-width: thin; - scrollbar-color: rgba(148, 163, 184, 0.45) transparent; } .mcp-stats-timeline-moment { display: inline-flex; align-items: center; gap: 6px; - min-width: 156px; - max-width: none; + width: fit-content; + min-width: 0; + max-width: 100%; padding: 4px 8px; border-radius: 999px; background: rgba(59, 130, 246, 0.08); color: #1d4ed8; font-size: 0.6875rem; font-weight: 600; - flex: 1 0 156px; + flex: 0 0 auto; box-sizing: border-box; overflow: hidden; } .mcp-stats-timeline-moment__time { min-width: 0; - flex: 1 1 auto; + max-width: 10rem; + flex: 0 1 auto; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; @@ -13718,6 +13776,7 @@ tr.mcp-stats-tool-row[data-tool-name]:focus-visible { } .monitor-execution-checkbox { + --theme-checkbox-size: 18px; cursor: pointer; width: 18px; height: 18px; @@ -13736,6 +13795,7 @@ tr.mcp-stats-tool-row[data-tool-name]:focus-visible { /* 统一表头复选框大小 */ #monitor-select-all { + --theme-checkbox-size: 18px; width: 18px; height: 18px; cursor: pointer; @@ -16839,6 +16899,10 @@ tr.mcp-stats-tool-row[data-tool-name]:focus-visible { cursor: pointer; } +.batch-table-col-checkbox .theme-checkbox { + --theme-checkbox-size: 16px; +} + .batch-footer-actions { display: flex; gap: 12px; @@ -22222,16 +22286,21 @@ tr.mcp-stats-tool-row[data-tool-name]:focus-visible { /* 关键提醒条 */ .dashboard-alert-banner { + --dashboard-alert-accent: #3b82f6; + --dashboard-alert-accent-soft: rgba(59, 130, 246, 0.07); + --dashboard-alert-accent-border: rgba(59, 130, 246, 0.18); + position: relative; + overflow: hidden; display: flex; align-items: center; gap: 14px; - padding: 14px 18px; - border-radius: 14px; + padding: 14px 16px; + border-radius: 12px; margin-bottom: 18px; - background: linear-gradient(135deg, #fff5f5 0%, #fee2e2 100%); - border: 1px solid rgba(239, 68, 68, 0.25); - color: #7f1d1d; - box-shadow: 0 2px 8px rgba(239, 68, 68, 0.08); + background: linear-gradient(105deg, var(--dashboard-alert-accent-soft) 0%, rgba(255, 255, 255, 0.98) 28%, #fff 100%); + border: 1px solid var(--dashboard-alert-accent-border); + color: var(--text-primary, #111827); + box-shadow: 0 5px 18px rgba(15, 23, 42, 0.045), inset 0 1px 0 rgba(255, 255, 255, 0.68); animation: dashboard-alert-slide-in 0.3s ease-out; } @@ -22243,30 +22312,34 @@ tr.mcp-stats-tool-row[data-tool-name]:focus-visible { .dashboard-alert-banner[hidden] { display: none; } .dashboard-alert-banner.is-warning { - background: linear-gradient(135deg, #fffbeb 0%, #fef3c7 100%); - border-color: rgba(245, 158, 11, 0.3); - color: #78350f; + --dashboard-alert-accent: #f59e0b; + --dashboard-alert-accent-soft: rgba(245, 158, 11, 0.07); + --dashboard-alert-accent-border: rgba(245, 158, 11, 0.18); } .dashboard-alert-banner.is-danger { - background: linear-gradient(135deg, #fff5f5 0%, #fee2e2 100%); - border-color: rgba(239, 68, 68, 0.3); - color: #7f1d1d; + --dashboard-alert-accent: #ef4444; + --dashboard-alert-accent-soft: rgba(239, 68, 68, 0.07); + --dashboard-alert-accent-border: rgba(239, 68, 68, 0.18); } .dashboard-alert-icon { width: 36px; height: 36px; border-radius: 10px; - background: rgba(255, 255, 255, 0.7); + background: var(--dashboard-alert-accent-soft); + color: var(--dashboard-alert-accent); + box-shadow: inset 0 0 0 1px var(--dashboard-alert-accent-border); display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0; } -.dashboard-alert-banner.is-danger .dashboard-alert-icon { color: #dc2626; } -.dashboard-alert-banner.is-warning .dashboard-alert-icon { color: #d97706; } +.dashboard-alert-icon svg { + width: 19px; + height: 19px; +} .dashboard-alert-content { flex: 1; @@ -22274,16 +22347,17 @@ tr.mcp-stats-tool-row[data-tool-name]:focus-visible { } .dashboard-alert-title { - font-size: 0.9375rem; + font-size: 0.9rem; font-weight: 700; - line-height: 1.3; - margin-bottom: 2px; + line-height: 1.35; + margin-bottom: 3px; + letter-spacing: 0.01em; } .dashboard-alert-desc { - font-size: 0.8125rem; + font-size: 0.8rem; line-height: 1.45; - opacity: 0.92; + color: var(--text-secondary, #64748b); } .dashboard-alert-actions { @@ -22294,28 +22368,35 @@ tr.mcp-stats-tool-row[data-tool-name]:focus-visible { .dashboard-alert-btn { appearance: none; - border: 1px solid currentColor; - background: rgba(255, 255, 255, 0.85); - color: inherit; + border: 1px solid var(--dashboard-alert-accent-border); + background: var(--dashboard-alert-accent-soft); + color: var(--dashboard-alert-accent); font-size: 0.8125rem; font-weight: 600; - padding: 6px 14px; - border-radius: 8px; + padding: 6px 12px; + border-radius: 7px; cursor: pointer; - transition: background 0.2s, transform 0.15s; + box-shadow: none; + transition: background 0.16s, border-color 0.16s, color 0.16s, filter 0.16s; white-space: nowrap; } .dashboard-alert-btn:hover { - background: #fff; - transform: translateY(-1px); + filter: saturate(1.15) brightness(0.98); + border-color: var(--dashboard-alert-accent); +} + +.dashboard-alert-btn:focus-visible, +.dashboard-alert-close:focus-visible { + outline: 2px solid var(--dashboard-alert-accent); + outline-offset: 2px; } .dashboard-alert-btn-secondary { background: transparent; - border-color: rgba(0, 0, 0, 0.15); - color: inherit; - opacity: 0.85; + border-color: var(--dashboard-alert-accent-border); + color: var(--dashboard-alert-accent); + box-shadow: none; } /* 告警条「× 忽略」按钮:低权重,hover 时才显形 */ @@ -22630,9 +22711,20 @@ tr.mcp-stats-tool-row[data-tool-name]:focus-visible { @media (max-width: 720px) { .dashboard-alert-banner { flex-wrap: wrap; + align-items: flex-start; + padding: 14px; } .dashboard-alert-actions { width: 100%; + padding-left: 52px; + } + .dashboard-alert-close { + position: absolute; + top: 10px; + right: 10px; + } + .dashboard-alert-content { + padding-right: 28px; } .dashboard-recommend-list { grid-template-columns: 1fr; @@ -28769,9 +28861,14 @@ html[data-theme="dark"] .skills-management-page .skill-card-actions .btn-seconda } .info-collect-results-table-wrap { + --table-scroll-header-height: 41px; overflow: auto; max-height: 60vh; position: relative; + scrollbar-gutter: stable; + background: var(--bg-primary); + scrollbar-width: thin; + scrollbar-color: color-mix(in srgb, var(--text-muted, #94a3b8) 52%, transparent) var(--bg-primary); } .info-collect-table { @@ -28898,6 +28995,10 @@ html[data-theme="dark"] .skills-management-page .skill-card-actions .btn-seconda text-align: center; } +.info-collect-col-select .theme-checkbox { + --theme-checkbox-size: 16px; +} + .info-collect-table thead th.info-collect-col-select, .info-collect-table tbody td.info-collect-col-select { position: sticky; @@ -31406,6 +31507,23 @@ html[data-theme="dark"] .chat-files-toast.chat-toast--error { overscroll-behavior: contain; -webkit-overflow-scrolling: touch; } +.projects-panel-pagination { + flex: 0 0 auto; + border-top: 0; + border-radius: 0 0 12px 12px; +} +#project-panel-assets .projects-table-wrap--with-pagination { + border-radius: 12px 12px 0 0; +} +#project-panel-assets .projects-panel-pagination.pagination-fixed { + border-color: var(--border-color, #e2e8f0); + border-top: 0; + box-shadow: none; +} +#project-panel-assets .projects-panel-pagination.pagination-fixed .pagination { + border-top: 1px solid var(--border-color, #e2e8f0); + border-radius: 0 0 12px 12px; +} #project-panel-conversations .projects-table-wrap, #project-panel-assets .projects-table-wrap, #project-panel-vulns .projects-table-wrap { @@ -31434,6 +31552,7 @@ html[data-theme="dark"] .chat-files-toast.chat-toast--error { margin: 0; } .projects-panel-count { flex-shrink: 0; padding: 4px 9px; border-radius: 999px; color: #2563eb; background: #eff6ff; font-size: .75rem; font-weight: 600; } +html[data-theme="dark"] .projects-panel-count { color: var(--accent-color); background: rgba(96, 165, 250, 0.14); } .projects-asset-target { display: block; max-width: 100%; padding: 0; border: 0; background: transparent; color: #2563eb; font: inherit; font-weight: 600; cursor: pointer; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .projects-asset-target:hover { text-decoration: underline; } #project-panel-assets .data-table--projects th:nth-child(1), #project-panel-assets .data-table--projects td:nth-child(1) { width: 24%; } @@ -33944,17 +34063,51 @@ html[data-theme="dark"] .dashboard-overview-status { border-color: rgba(96, 165, 250, 0.24); } -html[data-theme="dark"] .dashboard-alert-banner, -html[data-theme="dark"] .dashboard-alert-banner.is-warning, -html[data-theme="dark"] .dashboard-alert-banner.is-danger { - background: linear-gradient(135deg, rgba(127, 29, 29, 0.32) 0%, rgba(120, 53, 15, 0.24) 100%); - color: #fecaca; - border-color: rgba(248, 113, 113, 0.35); +html[data-theme="dark"] .dashboard-alert-banner { + --dashboard-alert-accent: #60a5fa; + --dashboard-alert-accent-soft: rgba(96, 165, 250, 0.07); + --dashboard-alert-accent-border: rgba(96, 165, 250, 0.16); + background: linear-gradient(105deg, var(--dashboard-alert-accent-soft) 0%, rgba(17, 27, 45, 0.98) 30%, rgba(15, 24, 40, 0.98) 100%); + color: #f1f5f9; + border-color: var(--dashboard-alert-accent-border); + box-shadow: 0 6px 20px rgba(2, 6, 23, 0.18), inset 0 1px 0 rgba(255, 255, 255, 0.025); +} + +html[data-theme="dark"] .dashboard-alert-banner.is-warning { + --dashboard-alert-accent: #fbbf24; + --dashboard-alert-accent-soft: rgba(251, 191, 36, 0.07); + --dashboard-alert-accent-border: rgba(251, 191, 36, 0.17); +} + +html[data-theme="dark"] .dashboard-alert-banner.is-danger { + --dashboard-alert-accent: #f87171; + --dashboard-alert-accent-soft: rgba(248, 113, 113, 0.075); + --dashboard-alert-accent-border: rgba(248, 113, 113, 0.18); +} + +html[data-theme="dark"] .dashboard-alert-desc { + color: #aebbd0; +} + +html[data-theme="dark"] .dashboard-alert-icon { + background: var(--dashboard-alert-accent-soft); } -html[data-theme="dark"] .dashboard-alert-icon, html[data-theme="dark"] .dashboard-alert-btn { - background: rgba(15, 23, 42, 0.72); + color: var(--dashboard-alert-accent); + background: var(--dashboard-alert-accent-soft); + border-color: var(--dashboard-alert-accent-border); + box-shadow: none; +} + +html[data-theme="dark"] .dashboard-alert-btn:hover { + filter: saturate(1.1) brightness(1.18); + border-color: var(--dashboard-alert-accent); +} + +html[data-theme="dark"] .dashboard-alert-btn-secondary { + color: var(--dashboard-alert-accent); + background: transparent; } html[data-theme="dark"] .dashboard-recommend-item.lvl-urgent { @@ -40064,49 +40217,146 @@ html[data-theme="dark"] .workflow-toolbar #workflow-connect-btn[aria-pressed="tr .asset-bar-row strong,.asset-bar-row small { text-align: right; } .asset-bar-row strong { font-size: 11px; } .asset-bar-row small { color: var(--text-secondary, #6b7280); font-size: 11px; } -.asset-toolbar { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; padding: 14px; border: 1px solid var(--border-color, #e5e7eb); border-radius: 12px; background: var(--card-bg, #fff); } -.asset-toolbar input { min-width: 300px; flex: 1; } +.asset-toolbar { display: flex; align-items: stretch; gap: 12px; flex-direction: column; padding: 14px; border: 1px solid var(--border-color, #e5e7eb); border-radius: 12px; background: var(--card-bg, #fff); } +.asset-toolbar-main { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; } +.asset-toolbar-main > input { min-width: 300px; flex: 1; } .asset-toolbar select { min-width: 130px; } .asset-toolbar input,.asset-toolbar select { height: 38px; padding: 0 12px; border: 1px solid var(--border-color, #d8dee8); border-radius: 8px; background: var(--input-bg, #fff); color: var(--text-primary, #111827); } +.asset-advanced-filters { display: grid; grid-template-columns: repeat(5,minmax(140px,1fr)); gap: 12px; padding-top: 14px; border-top: 1px solid var(--border-color,#e5e7eb); } +.asset-advanced-filters[hidden] { display: none; } +.asset-advanced-filters label { display: flex; min-width: 0; flex-direction: column; gap: 6px; color: var(--text-secondary,#64748b); font-size: 11px; font-weight: 600; } +.asset-advanced-filters input,.asset-advanced-filters select { box-sizing: border-box; width: 100%; min-width: 0; font-size: 12px; font-weight: 400; } +.asset-saved-views { display: flex; align-items: center; gap: 9px; padding-top: 12px; border-top: 1px dashed var(--border-color,#e5e7eb); } +.asset-saved-views select { min-width: 220px; } .asset-custom-select { width: 100%; min-width: 0; font-weight: 400; } .asset-custom-select--filter { width: auto; min-width: 150px; flex: 0 0 auto; } -.asset-custom-select .settings-custom-select-trigger { height: 42px; min-height: 42px; border-radius: 8px; background: var(--input-bg, #fff); font-size: 13px; font-weight: 400; } +.asset-advanced-filters .asset-custom-select--filter { width: 100%; min-width: 0; } +.asset-custom-select--saved-view { width: 220px; min-width: 220px; flex: 0 0 220px; } +.asset-custom-select .settings-custom-select-trigger { height: 42px; min-height: 42px; border-color: var(--border-color, #d8dee8); border-radius: 8px; background: var(--input-bg, #fff); color: var(--text-primary, #111827); font-size: 13px; font-weight: 400; } .asset-custom-select .settings-custom-select-caret { width: 16px; height: 16px; color: var(--text-secondary, #64748b); font-size: 0; } .asset-custom-select .settings-custom-select-caret::before { display: block; width: 7px; height: 7px; margin: 2px auto 0; border-right: 1.5px solid currentColor; border-bottom: 1.5px solid currentColor; content: ''; transform: rotate(45deg); } .asset-custom-select.open .settings-custom-select-caret { transform: rotate(180deg); } -.asset-custom-select .settings-custom-select-menu { min-width: 100%; padding: 5px; border-radius: 9px; background: var(--card-bg, #fff); box-shadow: 0 12px 28px rgba(15,23,42,.16); } -.asset-custom-select .settings-custom-select-option { min-height: 34px; border-radius: 6px; font-size: 13px; } +.asset-custom-select .settings-custom-select-menu { min-width: 100%; padding: 5px; border-color: var(--border-color, #d8dee8); border-radius: 9px; background: var(--input-bg, #fff); color: var(--text-primary, #111827); box-shadow: 0 12px 28px rgba(15,23,42,.16); } +.asset-custom-select .settings-custom-select-option { min-height: 34px; border-radius: 6px; color: var(--text-primary, #111827); font-size: 13px; } +.asset-custom-select .settings-custom-select-option:hover:not(:disabled) { background: var(--bg-secondary, #f4f6f8); } +.asset-custom-select .settings-custom-select-option.is-selected { background: rgba(59,130,246,.12); color: var(--accent-color,#2563eb); } .asset-custom-select--filter .settings-custom-select-trigger { height: 38px; min-height: 38px; padding: 7px 11px; } .asset-custom-select--pagination { width: 76px; flex: 0 0 76px; } .asset-custom-select--pagination .settings-custom-select-trigger { height: 32px; min-height: 32px; padding: 5px 9px; } .asset-custom-select--pagination .settings-custom-select-menu { top: auto; bottom: calc(100% + 6px); } -.asset-batch-actions { display: flex; align-items: center; gap: 10px; padding: 10px 14px; border: 1px solid rgba(37,99,235,.32); border-radius: 10px; background: rgba(37,99,235,.07); } +.asset-batch-actions { position: relative; z-index: 4; display: flex; align-items: center; justify-content: space-between; gap: 14px; padding: 10px 12px; border: 1px solid rgba(37,99,235,.32); border-radius: 10px; background: rgba(37,99,235,.07); } .asset-batch-actions[hidden] { display: none; } -.asset-batch-actions > span { margin-right: auto; color: var(--text-primary, #111827); font-size: 13px; font-weight: 650; } +.asset-batch-selection { display: flex; min-width: 0; align-items: center; gap: 9px; color: var(--text-primary, #111827); font-size: 13px; } +.asset-batch-selection-mark { position: relative; width: 20px; height: 20px; flex: 0 0 20px; border-radius: 6px; background: var(--accent-color, #2563eb); } +.asset-batch-selection-mark::after { position: absolute; top: 4px; left: 7px; width: 5px; height: 9px; border: solid #fff; border-width: 0 2px 2px 0; content: ''; transform: rotate(45deg); } +.asset-batch-selection strong { flex: 0 0 auto; font-size: 13px; font-weight: 700; white-space: nowrap; } +.asset-batch-text-action { padding: 2px 0; border: 0; background: transparent; color: var(--text-secondary, #64748b); cursor: pointer; font: inherit; font-size: 12px; white-space: nowrap; } +.asset-batch-text-action:hover { color: var(--accent-color, #2563eb); } +.asset-batch-text-action:focus-visible { border-radius: 4px; outline: 2px solid rgba(37,99,235,.45); outline-offset: 2px; } +.asset-batch-select-all { padding-left: 10px; border-left: 1px solid rgba(37,99,235,.22); color: var(--accent-color, #2563eb); font-weight: 600; } +.asset-batch-all-selected { overflow: hidden; color: var(--accent-color, #2563eb); font-size: 12px; font-weight: 600; text-overflow: ellipsis; white-space: nowrap; } +.asset-batch-primary-actions { display: flex; flex: 0 0 auto; align-items: center; gap: 8px; } +.asset-batch-more { position: relative; } +.asset-batch-more > summary { display: inline-flex; min-width: 72px; align-items: center; justify-content: center; gap: 8px; box-sizing: border-box; list-style: none; cursor: pointer; user-select: none; } +.asset-batch-more > summary::-webkit-details-marker { display: none; } +.asset-batch-more > summary::after { width: 6px; height: 6px; margin-top: -3px; border-right: 1.5px solid currentColor; border-bottom: 1.5px solid currentColor; content: ''; transform: rotate(45deg); transition: transform .15s ease; } +.asset-batch-more[open] > summary::after { margin-top: 3px; transform: rotate(225deg); } +.asset-batch-more[open] > summary { border-color: var(--accent-color, #2563eb); color: var(--accent-color, #2563eb); } +.asset-batch-menu { position: absolute; top: calc(100% + 8px); right: 0; width: 210px; padding: 6px; border: 1px solid var(--border-color, #d8dee8); border-radius: 10px; background: var(--card-bg, #fff); box-shadow: 0 16px 36px rgba(15,23,42,.18); } +.asset-batch-menu button { display: flex; box-sizing: border-box; width: 100%; min-height: 36px; align-items: center; padding: 8px 10px; border: 0; border-radius: 7px; background: transparent; color: var(--text-primary, #111827); cursor: pointer; font: inherit; font-size: 13px; text-align: left; } +.asset-batch-menu button:hover:not(:disabled),.asset-batch-menu button:focus-visible { outline: none; background: var(--bg-secondary, #f4f6f8); } +.asset-batch-menu button:disabled { cursor: not-allowed; opacity: .42; } +.asset-batch-menu-label { padding: 5px 10px 3px; color: var(--text-muted, #94a3b8); font-size: 10px; font-weight: 700; letter-spacing: .07em; text-transform: uppercase; } +.asset-batch-menu-separator { height: 1px; margin: 5px 4px; background: var(--border-color, #e5e7eb); } +.asset-batch-menu .asset-batch-menu-danger { color: #dc2626; } +.asset-batch-menu .asset-batch-menu-danger:hover:not(:disabled),.asset-batch-menu .asset-batch-menu-danger:focus-visible { background: rgba(220,38,38,.08); } +.asset-batch-permission-action { display: contents; } .asset-list-meta { margin-left: auto; } .asset-list-card { overflow: hidden; border: 1px solid var(--border-color, #e5e7eb); border-radius: 12px; background: var(--card-bg, #fff); box-shadow: 0 1px 3px rgba(15,23,42,.03); } #page-asset-library .asset-list-card { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; } .asset-table-wrap { overflow: auto; border: 1px solid var(--border-color, #e5e7eb); border-radius: 12px; background: var(--card-bg, #fff); } .asset-list-card .asset-table-wrap { border: 0; border-radius: 0; } -#page-asset-library .asset-list-card .asset-table-wrap { flex: 1 1 auto; min-height: 0; overflow-y: auto; overflow-x: hidden; } +#page-asset-library .asset-list-card .asset-table-wrap { --table-scroll-header-height: 41px; flex: 1 1 auto; min-height: 0; overflow-y: auto; overflow-x: hidden; scrollbar-gutter: stable; } +#page-asset-library .asset-list-card .asset-table-wrap::-webkit-scrollbar, +.info-collect-results-table-wrap::-webkit-scrollbar { width: 10px; } +#page-asset-library .asset-list-card .asset-table-wrap::-webkit-scrollbar-track { margin-top: var(--table-scroll-header-height); background: transparent; } +.info-collect-results-table-wrap::-webkit-scrollbar-track { + background: linear-gradient( + to bottom, + var(--bg-secondary) 0 var(--table-scroll-header-height), + var(--bg-primary) var(--table-scroll-header-height) 100% + ); +} +#page-asset-library .asset-list-card .asset-table-wrap::-webkit-scrollbar-thumb, +.info-collect-results-table-wrap::-webkit-scrollbar-thumb { + border: 3px solid transparent; + border-radius: 999px; + background: color-mix(in srgb, var(--text-muted, #94a3b8) 62%, transparent); + background-clip: content-box; +} +#page-asset-library .asset-list-card .asset-table-wrap::-webkit-scrollbar-thumb:hover, +.info-collect-results-table-wrap::-webkit-scrollbar-thumb:hover { + background: color-mix(in srgb, var(--text-secondary, #64748b) 82%, transparent); + background-clip: content-box; +} .asset-list-card #asset-pagination.pagination-fixed { border: 0; border-top: 1px solid var(--border-color, #e5e7eb); border-radius: 0; box-shadow: none; } #page-asset-library .asset-list-card #asset-pagination { flex: 0 0 auto; } .asset-list-card #asset-pagination.pagination-fixed .pagination { border-radius: 0; } .asset-table { width: 100%; min-width: 0; table-layout: fixed; border-collapse: collapse; } .asset-table th,.asset-table td { padding: 13px 14px; border-bottom: 1px solid var(--border-color, #edf0f4); text-align: left; vertical-align: middle; } -.asset-table th { position: sticky; top: 0; background: var(--card-bg, #fff); color: var(--text-secondary, #64748b); font-size: 12px; font-weight: 600; z-index: 1; } +.asset-table th { position: sticky; top: 0; box-sizing: border-box; height: var(--table-scroll-header-height, auto); background: var(--card-bg, #fff); color: var(--text-secondary, #64748b); font-size: 12px; font-weight: 600; z-index: 1; } .asset-table td { font-size: 13px; } .asset-table td:nth-child(2) { width: 220px; max-width: 280px; } .asset-table th:nth-child(2),.asset-table td:nth-child(2) { width: auto; } .asset-table th:nth-child(3),.asset-table td:nth-child(3) { width: 112px; } -.asset-table th:nth-child(4),.asset-table td:nth-child(4) { width: 140px; } -.asset-table th:nth-child(5),.asset-table td:nth-child(5) { width: 180px; } -.asset-table th:nth-child(6),.asset-table td:nth-child(6) { width: 96px; } -.asset-table th:nth-child(7),.asset-table td:nth-child(7) { width: 84px; } +.asset-table th:nth-child(4),.asset-table td:nth-child(4) { width: 126px; } +.asset-table th:nth-child(5),.asset-table td:nth-child(5) { width: 132px; } +.asset-table th:nth-child(6),.asset-table td:nth-child(6) { width: 160px; } +.asset-table th:nth-child(7),.asset-table td:nth-child(7) { width: 96px; } .asset-table th:nth-child(8),.asset-table td:nth-child(8) { width: 84px; } -.asset-table th:nth-child(9),.asset-table td:nth-child(9) { width: 176px; } +.asset-table th:nth-child(9),.asset-table td:nth-child(9) { width: 84px; } +.asset-table th:nth-child(10),.asset-table td:nth-child(10) { width: 166px; } +.asset-ownership { display: flex; min-width: 0; flex-direction: column; gap: 3px; line-height: 1.2; } +.asset-ownership__primary,.asset-ownership__secondary { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.asset-ownership__primary { color: var(--text-primary, #111827); font-weight: 600; } +.asset-ownership__secondary { color: var(--text-secondary, #64748b); font-size: 11px; } +.asset-ownership-empty { color: var(--text-muted, #94a3b8); } .asset-check-cell { width: 42px; min-width: 42px; text-align: center !important; } -.asset-check-cell input { width: 15px; height: 15px; cursor: pointer; accent-color: #2563eb; } +.theme-checkbox { + width: var(--theme-checkbox-size, 16px); + height: var(--theme-checkbox-size, 16px); + margin: 0; + border: 1px solid var(--border-color, #cbd5e1); + border-radius: 4px; + appearance: none; + -webkit-appearance: none; + background-color: var(--bg-primary, #fff); + background-position: center; + background-repeat: no-repeat; + cursor: pointer; + vertical-align: middle; + transition: background-color .15s, border-color .15s, box-shadow .15s; +} +.theme-checkbox:hover { border-color: var(--accent-color, #2563eb); } +.theme-checkbox:focus-visible { + outline: none; + border-color: var(--accent-color, #2563eb); + box-shadow: 0 0 0 3px rgba(37,99,235,.18); +} +.theme-checkbox:checked { + border-color: var(--accent-color, #2563eb); + background-color: var(--accent-color, #2563eb); + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='m3.5 8.2 2.8 2.8 6.2-6.2' fill='none' stroke='%23fff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); +} +.theme-checkbox:indeterminate { + border-color: var(--accent-color, #2563eb); + background-color: var(--accent-color, #2563eb); + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M4 8h8' fill='none' stroke='%23fff' stroke-width='2' stroke-linecap='round'/%3E%3C/svg%3E"); +} +input.theme-checkbox:disabled { + opacity: .5; + cursor: not-allowed; +} .asset-table td strong,.asset-table td small { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .asset-target-link { display: block; max-width: 100%; padding: 0; border: 0; background: transparent; color: #2563eb; font: inherit; font-weight: 650; text-align: left; cursor: pointer; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .asset-target-link:hover { text-decoration: underline; } @@ -40129,6 +40379,38 @@ html[data-theme="dark"] .workflow-toolbar #workflow-connect-btn[aria-pressed="tr .asset-vulnerability-link { min-width: 28px; padding: 3px 8px; border: 1px solid rgba(239,68,68,.28); border-radius: 999px; background: rgba(239,68,68,.10); color: #dc2626; font-weight: 700; cursor: pointer; } .asset-scan-content { width: min(700px, calc(100vw - 32px)); } .asset-project-content { width: min(500px, calc(100vw - 32px)); } +.asset-bulk-edit-content { width: min(760px, calc(100vw - 32px)); } +.asset-import-content { width: min(920px, calc(100vw - 32px)); max-height: calc(100vh - 48px); margin: 24px auto; } +.asset-import-body { display: flex; flex-direction: column; gap: 14px; } +.asset-import-step { padding: 16px; border: 1px solid var(--border-color, #e5e7eb); border-radius: 12px; background: var(--card-bg, #fff); } +.asset-import-step-heading { display: flex; align-items: center; gap: 11px; margin-bottom: 13px; } +.asset-import-step-heading > span { display: grid; width: 25px; height: 25px; flex: 0 0 25px; place-items: center; border-radius: 50%; background: rgba(37,99,235,.11); color: #2563eb; font-size: 12px; font-weight: 700; } +.asset-import-step-heading > div { display: flex; min-width: 0; flex-direction: column; gap: 3px; } +.asset-import-step-heading strong { color: var(--text-primary, #111827); font-size: 13px; } +.asset-import-step-heading small { color: var(--text-secondary, #64748b); font-size: 12px; font-weight: 400; } +.asset-import-template-actions { display: flex; align-items: center; gap: 12px; padding-left: 36px; } +.asset-import-dropzone { display: flex; box-sizing: border-box; width: 100%; min-height: 118px; align-items: center; justify-content: center; flex-direction: column; gap: 6px; border: 1.5px dashed var(--border-color, #cbd5e1); border-radius: 10px; background: var(--bg-secondary, #f8fafc); color: var(--text-primary, #111827); cursor: pointer; transition: border-color .15s, background .15s, box-shadow .15s; } +.asset-import-dropzone:hover,.asset-import-dropzone.is-dragging { border-color: #2563eb; background: rgba(37,99,235,.055); box-shadow: 0 0 0 3px rgba(37,99,235,.08); } +.asset-import-dropzone:disabled { cursor: wait; opacity: .65; } +.asset-import-dropzone-icon { display: grid; width: 30px; height: 30px; place-items: center; border-radius: 9px; background: rgba(37,99,235,.11); color: #2563eb; font-size: 18px; font-weight: 700; } +.asset-import-dropzone strong { font-size: 13px; } +.asset-import-dropzone small { max-width: 90%; overflow: hidden; color: var(--text-secondary, #64748b); font-size: 12px; text-overflow: ellipsis; white-space: nowrap; } +.asset-import-preview-section { min-height: 0; } +.asset-import-preview-wrap { max-height: 260px; overflow: auto; border: 1px solid var(--border-color, #e5e7eb); border-radius: 9px; } +.asset-import-preview-table { width: 100%; table-layout: fixed; border-collapse: collapse; } +.asset-import-preview-table th,.asset-import-preview-table td { padding: 9px 11px; border-bottom: 1px solid var(--border-color, #edf0f4); text-align: left; font-size: 12px; } +.asset-import-preview-table th { position: sticky; top: 0; z-index: 1; background: var(--card-bg, #fff); color: var(--text-secondary, #64748b); font-weight: 650; } +.asset-import-preview-table th:first-child,.asset-import-preview-table td:first-child { width: 52px; text-align: center; } +.asset-import-preview-table th:nth-child(2),.asset-import-preview-table td:nth-child(2) { width: 30%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.asset-import-preview-table th:nth-child(3),.asset-import-preview-table td:nth-child(3) { width: 18%; } +.asset-import-preview-table th:nth-child(4),.asset-import-preview-table td:nth-child(4) { width: 72px; } +.asset-import-preview-table tr.has-error td { background: rgba(220,38,38,.035); } +.asset-import-result { display: inline-flex; max-width: 100%; align-items: center; gap: 5px; font-size: 12px; overflow-wrap: anywhere; } +.asset-import-result::before { flex: 0 0 auto; font-weight: 800; content: '✓'; } +.asset-import-result--ok { color: #059669; } +.asset-import-result--error { color: #dc2626; } +.asset-import-result--error::before { content: '!'; } +.asset-import-preview-section .form-hint { margin: 9px 0 0; color: var(--text-secondary, #64748b); font-size: 11px; } .asset-project-content .form-hint { margin: 10px 0 0; color: var(--text-secondary, #64748b); font-size: 12px; line-height: 1.5; } .asset-project-content .settings-custom-select-trigger { height: 42px; border-radius: 8px; background: var(--input-bg, var(--bg-primary)); } .asset-project-content .settings-custom-select-menu { z-index: 2700; } @@ -40212,6 +40494,8 @@ html[data-theme="dark"] .asset-list-card, html[data-theme="dark"] .asset-table-wrap, html[data-theme="dark"] .asset-table th, html[data-theme="dark"] .asset-scan-targets, +html[data-theme="dark"] .asset-import-step, +html[data-theme="dark"] .asset-import-preview-table th, html[data-theme="dark"] .asset-detail-item { background: var(--card-bg); border-color: var(--border-color); @@ -40306,11 +40590,35 @@ html[data-theme="dark"] .asset-row-actions .asset-delete { } html[data-theme="dark"] .asset-batch-actions { border-color: rgba(96,165,250,.36); background: rgba(37,99,235,.13); } +html[data-theme="dark"] .asset-batch-selection-mark::after { border-color: #0b1120; } +html[data-theme="dark"] .asset-batch-menu { background: #0f172a; border-color: var(--border-color); box-shadow: 0 16px 36px rgba(0,0,0,.38); } +html[data-theme="dark"] .asset-batch-menu button:hover:not(:disabled),html[data-theme="dark"] .asset-batch-menu button:focus-visible { background: rgba(30,41,59,.9); } +html[data-theme="dark"] .asset-batch-menu .asset-batch-menu-danger:hover:not(:disabled),html[data-theme="dark"] .asset-batch-menu .asset-batch-menu-danger:focus-visible { background: rgba(248,113,113,.1); } +html[data-theme="dark"] .asset-import-dropzone { border-color: var(--border-color); background: rgba(15,23,42,.38); color: var(--text-primary); } +html[data-theme="dark"] .asset-import-dropzone:hover, +html[data-theme="dark"] .asset-import-dropzone.is-dragging { border-color: #60a5fa; background: rgba(37,99,235,.12); } +html[data-theme="dark"] .asset-import-preview-table tr.has-error td { background: rgba(248,113,113,.055); } html[data-theme="dark"] .asset-scan-prompt { background: var(--input-bg); border-color: var(--border-color); color: var(--text-primary); } html[data-theme="dark"] .asset-vulnerability-link { color: #fca5a5; border-color: rgba(248,113,113,.35); background: rgba(248,113,113,.12); } html[data-theme="dark"] .asset-custom-select .settings-custom-select-trigger, -html[data-theme="dark"] .asset-custom-select .settings-custom-select-menu { background: var(--input-bg); border-color: var(--border-color); color: var(--text-primary); } +html[data-theme="dark"] .asset-custom-select .settings-custom-select-menu { background: #0f172a; border-color: var(--border-color); color: var(--text-primary); } html[data-theme="dark"] .asset-custom-select .settings-custom-select-menu { box-shadow: 0 14px 32px rgba(0,0,0,.34); } +html[data-theme="dark"] .asset-custom-select .settings-custom-select-option:hover:not(:disabled) { background: rgba(30,41,59,.78); } +html[data-theme="dark"] .asset-custom-select .settings-custom-select-option.is-selected { background: rgba(59,130,246,.18); color: #93c5fd; } +html[data-theme="dark"] .theme-checkbox { + background-color: var(--bg-primary); +} +html[data-theme="dark"] .theme-checkbox:focus-visible { + box-shadow: 0 0 0 3px rgba(96,165,250,.22); +} +html[data-theme="dark"] .theme-checkbox:checked { + background-color: var(--accent-color); + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='m3.5 8.2 2.8 2.8 6.2-6.2' fill='none' stroke='%230b1120' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); +} +html[data-theme="dark"] .theme-checkbox:indeterminate { + background-color: var(--accent-color); + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M4 8h8' fill='none' stroke='%230b1120' stroke-width='2' stroke-linecap='round'/%3E%3C/svg%3E"); +} html[data-theme="dark"] .asset-detail-grid { border-color: var(--border-color); @@ -40342,7 +40650,11 @@ html[data-theme="dark"] .asset-form-error { color: #fca5a5; } @media (max-width: 820px) { .asset-table th:nth-child(3),.asset-table td:nth-child(3) { display: none; } .asset-table th:nth-child(5),.asset-table td:nth-child(5) { width: 145px; } - .asset-table th:nth-child(9),.asset-table td:nth-child(9) { width: 150px; } + .asset-table th:nth-child(10),.asset-table td:nth-child(10) { width: 150px; } + .asset-batch-actions { align-items: stretch; flex-direction: column; } + .asset-batch-selection { min-height: 30px; } + .asset-batch-primary-actions { width: 100%; } + .asset-batch-primary-actions > button { flex: 1 1 auto; } } @media (max-width: 760px) { .asset-overview-summary { grid-template-columns: 1fr; } @@ -40351,4 +40663,5 @@ html[data-theme="dark"] .asset-form-error { color: #fca5a5; } .asset-coverage-layout { grid-template-columns: 1fr; } .asset-recent-card { min-height: 230px; } } -@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,.asset-coverage-metrics > div + div,.asset-coverage-metrics > div:nth-child(4) { 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-summary { box-sizing: border-box; width: 100%; } .asset-protocol-summary__main { align-items: flex-start; flex-direction: column; } .asset-protocol-top-list { grid-template-columns: 1fr; } .asset-bar-row,.asset-protocol-columns { grid-template-columns: 20px 64px 1fr 36px; gap: 8px; padding-right: 4px; padding-left: 4px; } .asset-bar-row small,.asset-protocol-columns span:last-child { display: none; } .asset-editor-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 input { min-width: 100%; } } +@media (max-width: 1100px) { .asset-advanced-filters { grid-template-columns: repeat(3,minmax(140px,1fr)); } } +@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,.asset-coverage-metrics > div + div,.asset-coverage-metrics > div:nth-child(4) { 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-summary { box-sizing: border-box; width: 100%; } .asset-protocol-summary__main { align-items: flex-start; flex-direction: column; } .asset-protocol-top-list { grid-template-columns: 1fr; } .asset-bar-row,.asset-protocol-columns { grid-template-columns: 20px 64px 1fr 36px; gap: 8px; padding-right: 4px; padding-left: 4px; } .asset-bar-row small,.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)); } } diff --git a/web/static/i18n/en-US.json b/web/static/i18n/en-US.json index af1e63dd..66844eb6 100644 --- a/web/static/i18n/en-US.json +++ b/web/static/i18n/en-US.json @@ -585,6 +585,9 @@ "penetrationTestDetail": "Task execution details", "expandDetail": "Expand details", "expandDetailLazyHint": "Expand details (loads iteration details on click)", + "loadingEarlierDetails": "Loading earlier entries…", + "loadingLaterDetails": "Loading newer entries…", + "backToLatestProgress": "↓ Back to latest", "viewToolDetail": "View details", "collapseToolDetail": "Collapse", "liveTimelinePruned": "Collapsed the first {{count}} live process details. View the full record page by page after the task completes.", @@ -1063,6 +1066,47 @@ "coverageOfTotal": "{{percent}}% of all assets", "coverageMeta": "{{scanned}} / {{total}} covered", "addAsset": "+ Add asset", + "bulkImport": "Bulk import", + "bulkImportTitle": "Bulk import assets", + "bulkImportSubtitle": "Fill in a template, upload it, then review validation results before importing", + "downloadTemplate": "Download template", + "downloadTemplateHint": "XLSX is recommended; CSV is convenient for exports from other systems", + "downloadXlsx": "Download XLSX template", + "downloadCsv": "Download CSV template", + "uploadFile": "Upload file", + "uploadFileHint": "Supports .xlsx and .csv, up to 100,000 rows and 100 MB", + "chooseOrDropFile": "Choose a file or drop it here", + "fileNotSelected": "No file selected", + "dataPreview": "Data preview", + "rowNumber": "Row", + "validationResult": "Validation", + "validationPassed": "Valid", + "importValidRows": "Import valid rows", + "importValidRowsCount": "Import {{count}} valid rows", + "importPreviewSummary": "{{total}} rows: {{valid}} valid, {{invalid}} need attention", + "previewLimited": "Showing the first 100 rows; all {{count}} rows will be processed", + "fileTypeInvalid": "Only .xlsx and .csv files are supported", + "fileTooLarge": "The file must not exceed 100 MB", + "spreadsheetUnavailable": "The spreadsheet component failed to load; refresh and try again", + "fileParseFailed": "Could not parse the file; make sure it is valid and not corrupted", + "fileHasNoData": "The file has no rows to import", + "headerInvalid": "No template columns were recognized; use the downloaded template", + "tooManyRows": "Data must not exceed {{count}} rows", + "importTargetRequired": "Target address is missing", + "importStatusInvalid": "Status must be active or inactive", + "importProjectNotFound": "Project does not exist or is not accessible: {{project}}", + "importTagsInvalid": "Use at most 30 tags, with at most 64 characters per tag", + "importFieldTooLong": "Field {{field}} is too long", + "duplicateFileRow": "Duplicates row {{row}}", + "bulkImportDone": "Import complete: {{created}} created, {{updated}} updated, {{skipped}} skipped", + "templateGuideTitle": "Asset bulk import instructions", + "templateGuideRequired": "Required: target, or at least one of host / ip / domain", + "templateGuideTarget": "target examples: https://example.com:443, example.com, 1.1.1.1:22", + "templateGuideProject": "project accepts an existing project name or ID; leave blank for no project", + "templateGuideTags": "Separate tags with commas; at most 30 tags", + "templateGuideStatus": "status accepts active / inactive and defaults to active", + "templateGuideLimit": "Enter data in the Assets sheet; up to 100,000 rows", + "instructionsSheet": "Instructions", "addAssetTitle": "Add asset", "editAssetTitle": "Edit asset", "editorSubtitle": "Enter one target; other details are parsed automatically or can be added later", @@ -1138,6 +1182,8 @@ "selectAsset": "Select asset", "selectedCount": "{{count}} selected", "clearSelection": "Clear selection", + "selectAllResults": "Select all {{count}}", + "allResultsSelected": "All {{count}} matching assets selected", "bindProject": "Bind project", "bindProjectTitle": "Bind project", "chooseProject": "Choose a project", @@ -1173,7 +1219,62 @@ "riskLow": "Low", "riskInfo": "Info", "riskNormal": "Clear", - "riskUnassessed": "Unassessed" + "riskUnassessed": "Unassessed", + "advancedFilters": "Advanced filters", + "resetFilters": "Reset", + "allRisks": "All risk levels", + "minVulnerabilityCount": "Minimum findings", + "scanState": "Scan status", + "allScanStates": "All", + "neverScannedFilter": "Never scanned", + "overdue30": "Not scanned for 30 days", + "overdue60": "Not scanned for 60 days", + "overdue90": "Not scanned for 90 days", + "scannedFilter": "Scanned", + "responsiblePerson": "Owner", + "department": "Department", + "ownership": "Ownership", + "unassigned": "Unassigned", + "businessSystem": "Business system", + "environment": "Environment", + "criticality": "Criticality", + "allEnvironments": "All environments", + "environmentProduction": "Production", + "environmentStaging": "Staging", + "environmentTesting": "Testing", + "environmentDevelopment": "Development", + "environmentOther": "Other", + "allCriticalities": "All criticalities", + "criticalityCritical": "Mission critical", + "criticalityHigh": "High", + "criticalityMedium": "Medium", + "criticalityLow": "Low", + "firstSeenStart": "First seen from", + "firstSeenEnd": "First seen to", + "lastSeenStart": "Last seen from", + "lastSeenEnd": "Last seen to", + "sort": "Sort", + "sortLastSeenDesc": "Last seen (newest)", + "sortLastScanAsc": "Most overdue scan", + "sortLastScanDesc": "Most recently scanned", + "sortRiskDesc": "Risk (high to low)", + "sortVulnerabilityDesc": "Findings (most first)", + "sortFirstSeenDesc": "First seen (newest)", + "sortHostAsc": "Target name", + "sortPortAsc": "Port", + "savedViews": "Saved filter views", + "saveCurrentView": "Save current view", + "deleteView": "Delete view", + "bulkEdit": "Bulk edit", + "moreActions": "More", + "export": "Export", + "exportCsv": "Export CSV", + "exportXlsx": "Export XLSX", + "mergeDuplicates": "Merge duplicates", + "mergeRequiresMultiple": "Select at least two duplicate assets", + "batchDelete": "Bulk delete…", + "responsibilityBusiness": "Ownership and business context", + "responsibilityBusinessHint": "Use ownership and business priority to drive operations and risk ranking" }, "vulnerability": { "title": "Vulnerability Management", @@ -1431,10 +1532,10 @@ "security": "Security", "rbac": "Platform permissions", "audit": "Audit logs", - "infocollect": "Recon" + "infocollect": "Asset management" }, "infocollect": { - "title": "Reconnaissance" + "title": "Asset management" }, "hitl": { "title": "Human-in-the-loop", @@ -1772,7 +1873,8 @@ "chatUploads": "Chat Uploads", "robotIntegration": "Robot Integration", "markdownAgents": "Markdown Agents", - "projectManagement": "Project Management" + "projectManagement": "Project Management", + "assetManagement": "Asset Management" }, "summary": { "login": "User login", @@ -1930,7 +2032,8 @@ "listProjectFactEdges": "List all project fact edges", "createProjectFactEdge": "Add fact edge", "deleteProjectFactEdge": "Delete fact edge", - "promoteAttackChainToProject": "Promote conversation attack chain to project fact graph" + "promoteAttackChainToProject": "Promote conversation attack chain to project fact graph", + "importAssets": "Import assets in bulk" }, "response": { "getSuccess": "Success", @@ -1995,7 +2098,11 @@ "factGraphNodesEdges": "nodes + edges", "edgeList": "Edge list", "edgeCreated": "Edge created", - "promoteAttackChainResult": "Promotion result (facts/edges/graph)" + "promoteAttackChainResult": "Promotion result (facts/edges/graph)", + "assetImportCompleted": "Import completed", + "assetImportValidationFailed": "Asset count or field validation failed", + "assetImportForbidden": "Missing asset:write permission or access to the specified project", + "assetImportTransactionFailed": "Import transaction failed" } }, "chatGroup": { @@ -2445,6 +2552,7 @@ "providerClaude": "Claude (Anthropic Messages API)", "visionProviderReuseOpenAI": "Reuse OpenAI config (leave empty)", "fofaConfig": "FOFA config", + "fofaConfigHint": "Used for asset discovery and import; only an API key is required.", "agentConfig": "Agent config", "knowledgeConfig": "Knowledge base config", "baseUrl": "Base URL", @@ -2469,8 +2577,6 @@ "openaiReasoningAllowClient": "Allow chat page to override reasoning options", "fofaBaseUrlPlaceholder": "https://fofa.info/api/v1/search/all (optional)", "fofaBaseUrlHint": "Leave empty for default.", - "email": "Email", - "fofaEmailPlaceholder": "Enter FOFA email", "fofaApiKeyPlaceholder": "Enter FOFA API Key", "fofaApiKeyHint": "Stored in server config (config.yaml) only.", "maxIterations": "Max iterations", diff --git a/web/static/i18n/zh-CN.json b/web/static/i18n/zh-CN.json index 0fd17866..9a82b07a 100644 --- a/web/static/i18n/zh-CN.json +++ b/web/static/i18n/zh-CN.json @@ -573,6 +573,9 @@ "penetrationTestDetail": "任务执行详情", "expandDetail": "展开详情", "expandDetailLazyHint": "展开详情(点击后加载迭代详情)", + "loadingEarlierDetails": "正在加载更早记录…", + "loadingLaterDetails": "正在加载更新记录…", + "backToLatestProgress": "↓ 回到最新进度", "viewToolDetail": "查看详情", "collapseToolDetail": "收起", "liveTimelinePruned": "已收起前 {{count}} 条实时过程详情,任务完成后可按页查看完整记录", @@ -1051,6 +1054,47 @@ "coverageOfTotal": "占全部资产 {{percent}}%", "coverageMeta": "{{scanned}} / {{total}} 已覆盖", "addAsset": "+ 新增资产", + "bulkImport": "批量导入", + "bulkImportTitle": "批量导入资产", + "bulkImportSubtitle": "下载模板填写后上传,提交前会先校验并预览数据", + "downloadTemplate": "下载模板", + "downloadTemplateHint": "推荐 XLSX;CSV 适合从其他系统快速导出", + "downloadXlsx": "下载 XLSX 模板", + "downloadCsv": "下载 CSV 模板", + "uploadFile": "上传文件", + "uploadFileHint": "支持 .xlsx 和 .csv,最多 100000 行、100 MB", + "chooseOrDropFile": "选择文件,或拖拽到此处", + "fileNotSelected": "尚未选择文件", + "dataPreview": "数据预览", + "rowNumber": "行号", + "validationResult": "校验结果", + "validationPassed": "通过", + "importValidRows": "导入有效数据", + "importValidRowsCount": "导入 {{count}} 条有效数据", + "importPreviewSummary": "共 {{total}} 行,{{valid}} 行有效,{{invalid}} 行需修正", + "previewLimited": "仅展示前 100 行;提交时将处理全部 {{count}} 行", + "fileTypeInvalid": "仅支持 .xlsx 和 .csv 文件", + "fileTooLarge": "文件不能超过 100 MB", + "spreadsheetUnavailable": "表格组件加载失败,请刷新后重试", + "fileParseFailed": "无法解析文件,请确认文件未损坏且格式正确", + "fileHasNoData": "文件中没有可导入的数据", + "headerInvalid": "未识别到模板字段,请使用下载的模板填写", + "tooManyRows": "数据不能超过 {{count}} 行", + "importTargetRequired": "缺少目标地址", + "importStatusInvalid": "状态仅支持 active 或 inactive", + "importProjectNotFound": "项目不存在或无权访问:{{project}}", + "importTagsInvalid": "标签最多 30 个,单个标签最多 64 个字符", + "importFieldTooLong": "字段 {{field}} 内容过长", + "duplicateFileRow": "与第 {{row}} 行重复", + "bulkImportDone": "导入完成:新增 {{created}} 条,更新 {{updated}} 条,跳过 {{skipped}} 条", + "templateGuideTitle": "资产批量导入填写说明", + "templateGuideRequired": "必填:target,或 host / ip / domain 中至少一项", + "templateGuideTarget": "target 示例:https://example.com:443、example.com、1.1.1.1:22", + "templateGuideProject": "project 填写系统中已有的项目名称或项目 ID,留空表示不绑定", + "templateGuideTags": "tags 使用逗号分隔,最多 30 个", + "templateGuideStatus": "status 仅支持 active / inactive,留空默认为 active", + "templateGuideLimit": "请在“Assets”工作表中填写,最多 100000 行", + "instructionsSheet": "填写说明", "addAssetTitle": "新增资产", "editAssetTitle": "编辑资产", "editorSubtitle": "输入一个目标即可,其余信息可自动解析或稍后补充", @@ -1125,7 +1169,9 @@ "selectPage": "选择本页", "selectAsset": "选择资产", "selectedCount": "已选择 {{count}} 项", - "clearSelection": "取消选择", + "clearSelection": "清除选择", + "selectAllResults": "选择全部 {{count}} 项", + "allResultsSelected": "已选择当前筛选结果中的 {{count}} 项", "bindProject": "绑定项目", "bindProjectTitle": "绑定项目", "chooseProject": "请选择项目", @@ -1161,7 +1207,62 @@ "riskLow": "低危", "riskInfo": "提示", "riskNormal": "正常", - "riskUnassessed": "未评估" + "riskUnassessed": "未评估", + "advancedFilters": "高级筛选", + "resetFilters": "重置", + "allRisks": "全部风险", + "minVulnerabilityCount": "漏洞数量至少", + "scanState": "扫描状态", + "allScanStates": "全部", + "neverScannedFilter": "从未扫描", + "overdue30": "30 天未扫描", + "overdue60": "60 天未扫描", + "overdue90": "90 天未扫描", + "scannedFilter": "已扫描", + "responsiblePerson": "负责人", + "department": "部门", + "ownership": "归属", + "unassigned": "未分配", + "businessSystem": "业务系统", + "environment": "环境", + "criticality": "重要性", + "allEnvironments": "全部环境", + "environmentProduction": "生产", + "environmentStaging": "预发布", + "environmentTesting": "测试", + "environmentDevelopment": "开发", + "environmentOther": "其他", + "allCriticalities": "全部级别", + "criticalityCritical": "核心", + "criticalityHigh": "重要", + "criticalityMedium": "一般", + "criticalityLow": "低", + "firstSeenStart": "首次发现开始", + "firstSeenEnd": "首次发现结束", + "lastSeenStart": "最近发现开始", + "lastSeenEnd": "最近发现结束", + "sort": "排序", + "sortLastSeenDesc": "最近发现(新到旧)", + "sortLastScanAsc": "最久未扫描优先", + "sortLastScanDesc": "最近扫描优先", + "sortRiskDesc": "风险从高到低", + "sortVulnerabilityDesc": "漏洞数量从多到少", + "sortFirstSeenDesc": "首次发现(新到旧)", + "sortHostAsc": "目标名称", + "sortPortAsc": "端口", + "savedViews": "保存的筛选视图", + "saveCurrentView": "保存当前视图", + "deleteView": "删除视图", + "bulkEdit": "批量编辑", + "moreActions": "更多", + "export": "导出", + "exportCsv": "导出 CSV", + "exportXlsx": "导出 XLSX", + "mergeDuplicates": "合并重复资产", + "mergeRequiresMultiple": "请至少选择两个重复资产", + "batchDelete": "批量删除…", + "responsibilityBusiness": "责任与业务属性", + "responsibilityBusinessHint": "用于资产归属、运营分级与风险排序" }, "vulnerability": { "title": "漏洞管理", @@ -1419,10 +1520,10 @@ "security": "安全设置", "rbac": "平台权限", "audit": "日志审计", - "infocollect": "信息收集" + "infocollect": "资产管理" }, "infocollect": { - "title": "信息收集" + "title": "资产管理" }, "hitl": { "title": "人机协同", @@ -1760,7 +1861,8 @@ "chatUploads": "对话附件", "robotIntegration": "机器人集成", "markdownAgents": "多代理Markdown", - "projectManagement": "项目管理" + "projectManagement": "项目管理", + "assetManagement": "资产管理" }, "summary": { "login": "用户登录", @@ -1918,7 +2020,8 @@ "listProjectFactEdges": "列出项目全部事实边", "createProjectFactEdge": "添加事实边", "deleteProjectFactEdge": "删除事实边", - "promoteAttackChainToProject": "将对话攻击链沉淀到项目事实图" + "promoteAttackChainToProject": "将对话攻击链沉淀到项目事实图", + "importAssets": "批量导入资产" }, "response": { "getSuccess": "获取成功", @@ -1983,7 +2086,11 @@ "factGraphNodesEdges": "nodes + edges", "edgeList": "边列表", "edgeCreated": "边已创建", - "promoteAttackChainResult": "沉淀结果(facts/edges/graph)" + "promoteAttackChainResult": "沉淀结果(facts/edges/graph)", + "assetImportCompleted": "导入完成", + "assetImportValidationFailed": "数量或资产字段校验失败", + "assetImportForbidden": "缺少 asset:write 权限或无权访问指定项目", + "assetImportTransactionFailed": "导入事务失败" } }, "chatGroup": { @@ -2433,6 +2540,7 @@ "providerClaude": "Claude (Anthropic Messages API)", "visionProviderReuseOpenAI": "OpenAI 配置(留空复用)", "fofaConfig": "FOFA 配置", + "fofaConfigHint": "用于资产发现与导入,仅需配置 API Key。", "agentConfig": "Agent 配置", "knowledgeConfig": "知识库配置", "baseUrl": "Base URL", @@ -2457,8 +2565,6 @@ "openaiReasoningAllowClient": "允许对话页覆盖推理选项", "fofaBaseUrlPlaceholder": "https://fofa.info/api/v1/search/all(可选)", "fofaBaseUrlHint": "留空则使用默认地址。", - "email": "Email", - "fofaEmailPlaceholder": "输入 FOFA 账号邮箱", "fofaApiKeyPlaceholder": "输入 FOFA API Key", "fofaApiKeyHint": "仅保存在服务器配置中(`config.yaml`)。", "maxIterations": "最大迭代次数", diff --git a/web/static/js/api-docs.js b/web/static/js/api-docs.js index f8e281e8..bd80b8e0 100644 --- a/web/static/js/api-docs.js +++ b/web/static/js/api-docs.js @@ -41,7 +41,10 @@ function buildApiSpecTagToKey() { function translateApiDocTag(tag) { if (!tag) return tag; var key = apiSpecTagToKey[tag]; - return key ? _t('apiDocs.tags.' + key) : tag; + if (!key) return tag; + var i18nKey = 'apiDocs.tags.' + key; + var translated = _t(i18nKey); + return translated === i18nKey ? tag : translated; } function translateApiDocSummaryFromOp(op) { var key = op && op['x-i18n-summary']; diff --git a/web/static/js/assets.js b/web/static/js/assets.js index f72aa507..37150705 100644 --- a/web/static/js/assets.js +++ b/web/static/js/assets.js @@ -1,4 +1,5 @@ const ASSET_PAGE_SIZE_KEY = 'cyberstrike.asset_page_size'; +const ASSET_SAVED_VIEWS_KEY = 'cyberstrike.asset_saved_views'; function getAssetPageSize() { try { const value = Number(localStorage.getItem(ASSET_PAGE_SIZE_KEY)); @@ -7,15 +8,26 @@ function getAssetPageSize() { return 20; } } -const assetPageState = { page: 1, pageSize: getAssetPageSize(), total: 0, totalPages: 1, items: [], projects: [], projectsLoaded: false, detailIndex: -1, editIndex: -1, detailAsset: null, editAsset: null, selected: new Map(), scanMode: 'chat', scanAssets: [], editorTags: [], editorDirty: false, editorBusy: false, editorReturnFocus: null, editorInteractionsReady: false, editorParsedTarget: '' }; +const assetPageState = { page: 1, pageSize: getAssetPageSize(), total: 0, totalPages: 1, items: [], projects: [], projectsLoaded: false, detailIndex: -1, editIndex: -1, detailAsset: null, editAsset: null, selected: new Map(), selectionQuery: '', allMatchingSelected: false, scanMode: 'chat', scanAssets: [], editorTags: [], editorDirty: false, editorBusy: false, editorReturnFocus: null, editorInteractionsReady: false, editorParsedTarget: '', importRows: [], importFileName: '', importBusy: false, importInteractionsReady: false, importReturnFocus: null }; let assetOverviewDays = 30; const ASSET_CUSTOM_SELECT_IDS = [ 'asset-status-filter', 'asset-project-filter', + 'asset-risk-filter', + 'asset-scan-filter', + 'asset-environment-filter', + 'asset-criticality-filter', + 'asset-sort-filter', + 'asset-saved-view-select', 'asset-batch-project', 'asset-edit-project', 'asset-edit-status', + 'asset-edit-environment', + 'asset-edit-criticality', + 'asset-bulk-status', + 'asset-bulk-environment', + 'asset-bulk-criticality', 'asset-page-size-pagination' ]; @@ -25,7 +37,8 @@ function enhanceAssetSelect(select) { const wrapper = select.closest('.settings-custom-select'); if (!wrapper) return; wrapper.classList.add('asset-custom-select'); - wrapper.classList.toggle('asset-custom-select--filter', select.id === 'asset-status-filter' || select.id === 'asset-project-filter'); + wrapper.classList.toggle('asset-custom-select--filter', Boolean(select.closest('.asset-toolbar'))); + wrapper.classList.toggle('asset-custom-select--saved-view', select.id === 'asset-saved-view-select'); wrapper.classList.toggle('asset-custom-select--pagination', select.id === 'asset-page-size-pagination'); } @@ -52,6 +65,319 @@ function assetT(key, fallback, options) { return fallback; } +const ASSET_IMPORT_MAX_ROWS = 100000; +const ASSET_IMPORT_MAX_BYTES = 100 * 1024 * 1024; +const ASSET_IMPORT_COLUMNS = ['target', 'project', 'tags', 'host', 'ip', 'domain', 'port', 'protocol', 'title', 'server', 'country', 'province', 'city', 'responsible_person', 'department', 'business_system', 'environment', 'criticality', 'status']; +const ASSET_IMPORT_HEADER_ALIASES = { + target: ['target', 'asset', 'assetaddress', '目标', '资产', '资产地址'], + project: ['project', 'projectname', 'projectid', '项目', '项目名称', '项目id', '所属项目'], + tags: ['tags', 'tag', '标签'], + host: ['host', 'url', '完整url', '主机'], + ip: ['ip', 'ipaddress', 'ip地址'], + domain: ['domain', '域名'], + port: ['port', '端口'], + protocol: ['protocol', 'scheme', '协议'], + title: ['title', 'pagetitle', '标题', '页面标题'], + server: ['server', 'service', 'product', '服务', '产品', '服务指纹'], + country: ['country', 'countryregion', '国家', '国家地区'], + province: ['province', 'state', '省份', '州'], + city: ['city', '城市'], + responsible_person: ['responsibleperson', 'owner', '负责人', '责任人'], + department: ['department', '部门'], + business_system: ['businesssystem', 'system', '业务系统', '系统'], + environment: ['environment', 'env', '环境'], + criticality: ['criticality', 'importance', '重要性', '重要等级'], + status: ['status', '状态'] +}; + +function normalizeAssetImportHeader(value) { + return String(value == null ? '' : value).replace(/^\uFEFF/, '').trim().toLowerCase().replace(/[\s_\-/.()()]+/g, ''); +} + +function assetImportColumnForHeader(value) { + const normalized = normalizeAssetImportHeader(value); + return ASSET_IMPORT_COLUMNS.find(column => ASSET_IMPORT_HEADER_ALIASES[column].includes(normalized)) || ''; +} + +function assetImportTemplateHeaders() { + return ASSET_IMPORT_COLUMNS.slice(); +} + +function downloadAssetTemplate(format) { + const headers = assetImportTemplateHeaders(); + if (format === 'csv') { + const csv = '\uFEFF' + headers.join(',') + '\r\n'; + const link = document.createElement('a'); + link.href = URL.createObjectURL(new Blob([csv], { type: 'text/csv;charset=utf-8' })); + link.download = 'asset-import-template.csv'; + link.click(); + setTimeout(() => URL.revokeObjectURL(link.href), 0); + return; + } + if (!window.XLSX) { + setAssetImportError(assetT('assets.spreadsheetUnavailable', '表格组件加载失败,请刷新后重试')); + return; + } + const workbook = XLSX.utils.book_new(); + const assetSheet = XLSX.utils.aoa_to_sheet([headers]); + assetSheet['!cols'] = headers.map(name => ({ wch: ['target', 'host'].includes(name) ? 28 : 16 })); + assetSheet['!autofilter'] = { ref: `A1:${XLSX.utils.encode_col(headers.length - 1)}1` }; + const instructions = [ + [assetT('assets.templateGuideTitle', '资产批量导入填写说明')], + [assetT('assets.templateGuideRequired', '必填:target,或 host / ip / domain 中至少一项')], + [assetT('assets.templateGuideTarget', 'target 示例:https://example.com:443、example.com、1.1.1.1:22')], + [assetT('assets.templateGuideProject', 'project 填写系统中已有的项目名称或项目 ID,留空表示不绑定')], + [assetT('assets.templateGuideTags', 'tags 使用逗号分隔,最多 30 个')], + [assetT('assets.templateGuideStatus', 'status 仅支持 active / inactive,留空默认为 active')], + [assetT('assets.templateGuideLimit', '请在“Assets”工作表中填写,最多 100000 行')] + ]; + const guideSheet = XLSX.utils.aoa_to_sheet(instructions); + guideSheet['!cols'] = [{ wch: 86 }]; + XLSX.utils.book_append_sheet(workbook, assetSheet, 'Assets'); + XLSX.utils.book_append_sheet(workbook, guideSheet, assetT('assets.instructionsSheet', '填写说明').slice(0, 31)); + XLSX.writeFile(workbook, 'asset-import-template.xlsx'); +} + +async function openAssetImport() { + assetPageState.importReturnFocus = document.activeElement; + await ensureAssetProjects(true); + resetAssetImport(); + ensureAssetImportInteractions(); + if (typeof openAppModal === 'function') openAppModal('asset-import-modal', { focusEl: document.getElementById('asset-import-dropzone') }); + else document.getElementById('asset-import-modal').style.display = 'flex'; +} + +function closeAssetImport(force) { + if (assetPageState.importBusy && !force) return; + if (typeof closeAppModal === 'function') closeAppModal('asset-import-modal'); + else document.getElementById('asset-import-modal').style.display = 'none'; + const returnFocus = assetPageState.importReturnFocus; + if (returnFocus && typeof returnFocus.focus === 'function') requestAnimationFrame(() => returnFocus.focus()); +} + +function resetAssetImport() { + assetPageState.importRows = []; + assetPageState.importFileName = ''; + assetPageState.importBusy = false; + const file = document.getElementById('asset-import-file'); + if (file) file.value = ''; + const name = document.getElementById('asset-import-file-name'); + if (name) name.textContent = assetT('assets.fileNotSelected', '尚未选择文件'); + const preview = document.getElementById('asset-import-preview-section'); + if (preview) preview.hidden = true; + const body = document.getElementById('asset-import-preview-body'); + if (body) body.innerHTML = ''; + setAssetImportError(''); + setAssetImportBusy(false); +} + +function ensureAssetImportInteractions() { + if (assetPageState.importInteractionsReady) return; + assetPageState.importInteractionsReady = true; + const dropzone = document.getElementById('asset-import-dropzone'); + if (!dropzone) return; + ['dragenter', 'dragover'].forEach(type => dropzone.addEventListener(type, event => { + event.preventDefault(); + dropzone.classList.add('is-dragging'); + })); + ['dragleave', 'drop'].forEach(type => dropzone.addEventListener(type, event => { + event.preventDefault(); + dropzone.classList.remove('is-dragging'); + })); + dropzone.addEventListener('drop', event => handleAssetImportFile(event.dataTransfer?.files?.[0])); +} + +function setAssetImportError(message) { + const root = document.getElementById('asset-import-error'); + if (!root) return; + root.textContent = message || ''; + root.hidden = !message; +} + +function setAssetImportBusy(busy) { + assetPageState.importBusy = Boolean(busy); + const submit = document.getElementById('asset-import-submit'); + if (submit) submit.disabled = Boolean(busy) || !assetPageState.importRows.some(row => !row.error); + const dropzone = document.getElementById('asset-import-dropzone'); + if (dropzone) dropzone.disabled = Boolean(busy); +} + +async function handleAssetImportFile(file) { + setAssetImportError(''); + if (!file) return; + assetPageState.importRows = []; + assetPageState.importFileName = ''; + const previousPreview = document.getElementById('asset-import-preview-section'); + if (previousPreview) previousPreview.hidden = true; + const selectedName = document.getElementById('asset-import-file-name'); + if (selectedName) selectedName.textContent = file.name || assetT('assets.fileNotSelected', '尚未选择文件'); + setAssetImportBusy(false); + const extension = String(file.name || '').split('.').pop().toLowerCase(); + if (!['xlsx', 'csv'].includes(extension)) { + setAssetImportError(assetT('assets.fileTypeInvalid', '仅支持 .xlsx 和 .csv 文件')); + return; + } + if (file.size > ASSET_IMPORT_MAX_BYTES) { + setAssetImportError(assetT('assets.fileTooLarge', '文件不能超过 100 MB')); + return; + } + if (!window.XLSX) { + setAssetImportError(assetT('assets.spreadsheetUnavailable', '表格组件加载失败,请刷新后重试')); + return; + } + setAssetImportBusy(true); + try { + const workbook = XLSX.read(await file.arrayBuffer(), { type: 'array', cellDates: false }); + const preferred = workbook.SheetNames.find(name => /^(assets?|资产)$/i.test(String(name).trim())); + const sheet = workbook.Sheets[preferred || workbook.SheetNames[0]]; + const matrix = sheet ? XLSX.utils.sheet_to_json(sheet, { header: 1, defval: '', raw: false, blankrows: false }) : []; + parseAssetImportMatrix(matrix, file.name); + } catch (error) { + console.error('解析资产导入文件失败:', error); + setAssetImportError(error?.message || assetT('assets.fileParseFailed', '无法解析文件,请确认文件未损坏且格式正确')); + } finally { + setAssetImportBusy(false); + } +} + +function parseAssetImportMatrix(matrix, fileName) { + if (!Array.isArray(matrix) || matrix.length < 2) { + throw new Error(assetT('assets.fileHasNoData', '文件中没有可导入的数据')); + } + const headers = matrix[0].map(assetImportColumnForHeader); + if (!headers.some(Boolean)) { + throw new Error(assetT('assets.headerInvalid', '未识别到模板字段,请使用下载的模板填写')); + } + const dataRows = matrix.slice(1).filter(row => Array.isArray(row) && row.some(value => String(value ?? '').trim() !== '')); + if (!dataRows.length) throw new Error(assetT('assets.fileHasNoData', '文件中没有可导入的数据')); + if (dataRows.length > ASSET_IMPORT_MAX_ROWS) { + throw new Error(assetT('assets.tooManyRows', `数据不能超过 ${ASSET_IMPORT_MAX_ROWS} 行`, { count: ASSET_IMPORT_MAX_ROWS })); + } + const seen = new Map(); + assetPageState.importRows = dataRows.map((row, index) => { + const values = {}; + headers.forEach((column, columnIndex) => { + if (column && values[column] == null) values[column] = String(row[columnIndex] ?? '').trim(); + }); + const parsed = assetImportRecord(values, index + 2); + if (!parsed.error) { + const asset = parsed.asset; + const key = `${String(asset.domain || asset.ip || asset.host).toLowerCase()}|${asset.port || 0}|${asset.protocol || ''}`; + if (seen.has(key)) parsed.error = assetT('assets.duplicateFileRow', `与第 ${seen.get(key)} 行重复`, { row: seen.get(key) }); + else seen.set(key, parsed.rowNumber); + } + return parsed; + }); + assetPageState.importFileName = fileName; + const name = document.getElementById('asset-import-file-name'); + if (name) name.textContent = fileName; + renderAssetImportPreview(); +} + +function assetImportRecord(values, rowNumber) { + const fail = message => ({ rowNumber, values, asset: null, error: message }); + const target = values.target || values.host || values.domain || values.ip || ''; + if (!target) return fail(assetT('assets.importTargetRequired', '缺少目标地址')); + let parsed; + try { parsed = parseAssetEditorTarget(target); } + catch (error) { return fail(error.message); } + const rawPort = values.port; + const port = rawPort === '' || rawPort == null ? Number(parsed.port || 0) : Number(rawPort); + if (!Number.isInteger(port) || port < 0 || port > 65535) return fail(assetT('assets.portInvalid', '端口必须在 1–65535 之间')); + const ip = (values.ip || parsed.ip || '').toLowerCase(); + if (ip && !assetEditorIsIPv4(ip) && !assetEditorIsIPv6(ip)) return fail(assetT('assets.ipInvalid', 'IP 地址格式无效')); + const rawDomain = values.domain || parsed.domain || ''; + const domain = rawDomain ? assetEditorNormalizeDomain(rawDomain) : ''; + if (rawDomain && !domain) return fail(assetT('assets.domainInvalid', '域名格式无效')); + const protocol = (values.protocol || parsed.protocol || '').toLowerCase(); + if (protocol && !/^[a-z][a-z0-9+.-]{0,31}$/.test(protocol)) return fail(assetT('assets.protocolInvalid', '协议格式无效')); + const statuses = { active: 'active', inactive: 'inactive', '活跃': 'active', '停用': 'inactive' }; + const status = statuses[String(values.status || 'active').toLowerCase()]; + if (!status) return fail(assetT('assets.importStatusInvalid', '状态仅支持 active 或 inactive')); + const environments = { '': '', production: 'production', staging: 'staging', testing: 'testing', development: 'development', other: 'other', '生产': 'production', '预发布': 'staging', '测试': 'testing', '开发': 'development', '其他': 'other' }; + const environment = environments[String(values.environment || '').toLowerCase()]; + if (environment == null) return fail('环境值无效'); + const criticalities = { '': '', critical: 'critical', high: 'high', medium: 'medium', low: 'low', '核心': 'critical', '重要': 'high', '一般': 'medium', '低': 'low' }; + const criticality = criticalities[String(values.criticality || '').toLowerCase()]; + if (criticality == null) return fail('重要性值无效'); + let projectId = ''; + let projectName = ''; + if (values.project) { + const projectValue = String(values.project).trim().toLowerCase(); + const project = assetPageState.projects.find(item => String(item.id).toLowerCase() === projectValue || String(item.name).trim().toLowerCase() === projectValue); + if (!project) return fail(assetT('assets.importProjectNotFound', `项目不存在或无权访问:${values.project}`, { project: values.project })); + projectId = project.id; + projectName = project.name; + } + const tags = String(values.tags || '').split(/[,,;;|]/).map(tag => tag.trim()).filter(Boolean); + if (tags.length > 30 || tags.some(tag => Array.from(tag).length > 64)) return fail(assetT('assets.importTagsInvalid', '标签最多 30 个,单个标签最多 64 个字符')); + const asset = { + host: values.host || parsed.host || '', ip, domain, port, protocol, + title: values.title || '', server: values.server || '', country: values.country || '', + province: values.province || '', city: values.city || '', responsible_person: values.responsible_person || '', + department: values.department || '', business_system: values.business_system || '', environment, criticality, status, tags, + project_id: projectId, source: 'manual-import' + }; + const limits = { host: 500, title: 500, domain: 255, protocol: 255, server: 255, country: 255, province: 255, city: 255, responsible_person: 255, department: 255, business_system: 255 }; + const oversized = Object.keys(limits).find(key => Array.from(String(asset[key] || '')).length > limits[key]); + if (oversized) return fail(assetT('assets.importFieldTooLong', `字段 ${oversized} 内容过长`, { field: oversized })); + return { rowNumber, values, asset, projectName, error: '' }; +} + +function renderAssetImportPreview() { + const rows = assetPageState.importRows; + const valid = rows.filter(row => !row.error).length; + const invalid = rows.length - valid; + const preview = document.getElementById('asset-import-preview-section'); + if (preview) preview.hidden = false; + const summary = document.getElementById('asset-import-summary'); + if (summary) summary.textContent = assetT('assets.importPreviewSummary', `共 ${rows.length} 行,${valid} 行有效,${invalid} 行需修正`, { total: rows.length, valid, invalid }); + const body = document.getElementById('asset-import-preview-body'); + if (body) body.innerHTML = rows.slice(0, 100).map(row => { + const asset = row.asset || {}; + const target = row.values.target || row.values.host || row.values.domain || row.values.ip || '-'; + const result = row.error + ? `${escapeHtml(row.error)}` + : `${escapeHtml(assetT('assets.validationPassed', '通过'))}`; + return `
${escapeHtml(asset.source_query)}` : ''),
assetDetailItem(assetT('assets.tagsLabel', '标签'), tags, true),
@@ -1040,12 +1718,30 @@ window.submitAssetScan = submitAssetScan;
window.toggleAssetSelection = toggleAssetSelection;
window.toggleAssetPageSelection = toggleAssetPageSelection;
window.clearAssetSelection = clearAssetSelection;
+window.closeAssetBatchMenu = closeAssetBatchMenu;
+window.selectAllMatchingAssets = selectAllMatchingAssets;
+window.toggleAssetAdvancedFilters = toggleAssetAdvancedFilters;
+window.resetAssetFilters = resetAssetFilters;
+window.saveCurrentAssetView = saveCurrentAssetView;
+window.applyAssetSavedView = applyAssetSavedView;
+window.deleteCurrentAssetView = deleteCurrentAssetView;
+window.openAssetBulkEdit = openAssetBulkEdit;
+window.closeAssetBulkEdit = closeAssetBulkEdit;
+window.submitAssetBulkEdit = submitAssetBulkEdit;
+window.exportSelectedAssets = exportSelectedAssets;
+window.deleteSelectedAssets = deleteSelectedAssets;
+window.mergeSelectedAssets = mergeSelectedAssets;
window.openAssetVulnerabilities = openAssetVulnerabilities;
window.changeAssetPageSize = changeAssetPageSize;
window.openAssetEditor = openAssetEditor;
window.closeAssetEditor = closeAssetEditor;
window.saveAsset = saveAsset;
window.deleteAsset = deleteAsset;
+window.openAssetImport = openAssetImport;
+window.closeAssetImport = closeAssetImport;
+window.downloadAssetTemplate = downloadAssetTemplate;
+window.handleAssetImportFile = handleAssetImportFile;
+window.submitAssetImport = submitAssetImport;
window.importSelectedFofaAssets = importSelectedFofaAssets;
window.importFofaRowAsset = importFofaRowAsset;
window.openAssetDetail = openAssetDetail;
@@ -1053,5 +1749,16 @@ window.openAssetDetailRecord = openAssetDetailRecord;
window.closeAssetDetail = closeAssetDetail;
window.editAssetFromDetail = editAssetFromDetail;
-document.addEventListener('DOMContentLoaded', () => initAssetCustomSelects());
+document.addEventListener('DOMContentLoaded', () => {
+ ensureAssetOwnershipColumn();
+ initAssetCustomSelects();
+ renderAssetSavedViews();
+ document.addEventListener('click', event => {
+ const menu = document.getElementById('asset-batch-more');
+ if (menu?.open && event.target instanceof Node && !menu.contains(event.target)) closeAssetBatchMenu();
+ });
+ document.addEventListener('keydown', event => {
+ if (event.key === 'Escape') closeAssetBatchMenu();
+ });
+});
document.addEventListener('languagechange', () => ASSET_CUSTOM_SELECT_IDS.forEach(syncAssetSelect));
diff --git a/web/static/js/chat.js b/web/static/js/chat.js
index d35449d0..16580d5b 100644
--- a/web/static/js/chat.js
+++ b/web/static/js/chat.js
@@ -2377,11 +2377,19 @@ async function syncAssistantReasoningContentFromServer(backendMessageId, domAssi
const msg = conv.messages.find((m) => m && String(m.id) === String(backendMessageId));
if (!msg || !msg.reasoningContent) return;
setMessageReasoningContent(domAssistantId, msg.reasoningContent);
- const pdRes = await apiFetch(`/api/messages/${encodeURIComponent(String(backendMessageId))}/process-details`);
- const pdJson = await pdRes.json().catch(() => ({}));
- const details = pdRes.ok && Array.isArray(pdJson.processDetails) ? pdJson.processDetails : [];
- if (typeof renderProcessDetails === 'function') {
- renderProcessDetails(domAssistantId, details);
+ // 最终回复到达后同样必须完整恢复过程详情;无参数接口默认仅返回前 50 条,
+ // 否则这里会把 task-events 恢复出的完整时间线再次覆盖成第一页。
+ if (typeof window.loadProcessDetailsPaginated === 'function') {
+ await window.loadProcessDetailsPaginated(domAssistantId, String(backendMessageId));
+ } else {
+ const pdRes = await apiFetch(
+ `/api/messages/${encodeURIComponent(String(backendMessageId))}/process-details?full=1`
+ );
+ const pdJson = await pdRes.json().catch(() => ({}));
+ const details = pdRes.ok && Array.isArray(pdJson.processDetails) ? pdJson.processDetails : [];
+ if (typeof renderProcessDetails === 'function') {
+ renderProcessDetails(domAssistantId, details);
+ }
}
} catch (e) {
console.warn('syncAssistantReasoningContentFromServer failed', e);
@@ -4151,7 +4159,7 @@ async function prefetchLastAssistantProcessDetails() {
await window.loadProcessDetailsPaginated(last.id, backendId);
return;
}
- const res = await apiFetch('/api/messages/' + encodeURIComponent(String(backendId)) + '/process-details');
+ const res = await apiFetch('/api/messages/' + encodeURIComponent(String(backendId)) + '/process-details?full=1');
const j = await res.json().catch(() => ({}));
if (!res.ok || !Array.isArray(j.processDetails) || j.processDetails.length === 0) return;
if (typeof renderProcessDetails === 'function') {
@@ -9374,7 +9382,7 @@ function renderBatchConversations(filtered = null) {
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
- checkbox.className = 'batch-conversation-checkbox';
+ checkbox.className = 'batch-conversation-checkbox theme-checkbox';
checkbox.dataset.conversationId = conv.id;
checkbox.addEventListener('change', syncSelectAllBatchCheckbox);
diff --git a/web/static/js/info-collect.js b/web/static/js/info-collect.js
index 959fd2df..38eb310e 100644
--- a/web/static/js/info-collect.js
+++ b/web/static/js/info-collect.js
@@ -506,7 +506,7 @@ function renderFofaResults(payload) {
// 表头(左:勾选列;右:操作列固定)
const headerCells = [
- '| 目标 | 服务 | 所属项目 | 上次扫描 | 相关漏洞 | 风险等级 | 状态 | 操作 | ||
|---|---|---|---|---|---|---|---|---|---|
| 暂无数据 | |||||||||
| 目标 | 服务 | 所属项目 | 归属 | 上次扫描 | 相关漏洞 | 风险等级 | 状态 | 操作 | |
| 暂无数据 | |||||||||
用于资产归属、运营分级与风险排序
| 目标 | 服务 | 标题/指纹 | 来源 | 最近发现 | 状态 | 操作 |
|---|
用于资产发现与导入,仅需配置 API Key。