Add files via upload

This commit is contained in:
公明
2026-07-16 19:30:29 +08:00
committed by GitHub
parent 2147623543
commit 8e42e72e42
13 changed files with 2283 additions and 67 deletions
+453
View File
@@ -16,6 +16,8 @@
--bg-primary: #ffffff;
--bg-secondary: #f8f9fa;
--bg-tertiary: #f1f3f5;
--card-bg: var(--bg-primary);
--input-bg: var(--bg-primary);
--text-primary: #1a1a1a;
--text-secondary: #6c757d;
--text-muted: #adb5bd;
@@ -48,6 +50,8 @@ html[data-theme="dark"] {
--bg-primary: #111827;
--bg-secondary: #0b1120;
--bg-tertiary: #1f2937;
--card-bg: #111827;
--input-bg: #0b1120;
--text-primary: #e5e7eb;
--text-secondary: #a7b0c0;
--text-muted: #6b7280;
@@ -221,6 +225,36 @@ html[data-theme="dark"] .main-sidebar {
background: rgba(0, 0, 0, 0.3);
}
.nav-section-label {
padding: 16px 19px 6px;
color: var(--text-secondary);
font-size: 0.6875rem;
font-weight: 600;
line-height: 1;
letter-spacing: 0.08em;
text-transform: uppercase;
white-space: nowrap;
opacity: 0.72;
user-select: none;
}
.nav-section-label:first-child {
padding-top: 2px;
}
.main-sidebar.collapsed .nav-section-label {
height: 1px;
margin: 8px 12px;
padding: 0;
overflow: hidden;
background: var(--border-color);
opacity: 0.7;
}
.main-sidebar.collapsed .nav-section-label span {
display: none;
}
.nav-item {
margin-bottom: 0;
}
@@ -31348,6 +31382,7 @@ html[data-theme="dark"] .chat-files-toast.chat-toast--error {
background: #fff;
}
#project-panel-facts,
#project-panel-assets,
#project-panel-conversations,
#project-panel-vulns {
display: flex;
@@ -31357,10 +31392,12 @@ html[data-theme="dark"] .chat-files-toast.chat-toast--error {
}
#project-panel-facts .projects-fact-toolbar,
#project-panel-vulns .projects-fact-toolbar,
#project-panel-assets .projects-panel-toolbar,
#project-panel-conversations .projects-panel-toolbar {
flex: 0 0 auto;
}
#project-panel-facts .projects-table-wrap,
#project-panel-assets .projects-table-wrap,
#project-panel-conversations .projects-table-wrap,
#project-panel-vulns .projects-table-wrap {
flex: 1 1 auto;
@@ -31370,6 +31407,7 @@ html[data-theme="dark"] .chat-files-toast.chat-toast--error {
-webkit-overflow-scrolling: touch;
}
#project-panel-conversations .projects-table-wrap,
#project-panel-assets .projects-table-wrap,
#project-panel-vulns .projects-table-wrap {
overflow-x: hidden;
}
@@ -31377,6 +31415,7 @@ html[data-theme="dark"] .chat-files-toast.chat-toast--error {
overflow-x: auto;
}
#project-panel-facts .projects-table-wrap .data-table--projects thead th,
#project-panel-assets .projects-table-wrap .data-table--projects thead th,
#project-panel-conversations .projects-table-wrap .data-table--projects thead th,
#project-panel-vulns .projects-table-wrap .data-table--projects thead th {
position: sticky;
@@ -31394,6 +31433,16 @@ html[data-theme="dark"] .chat-files-toast.chat-toast--error {
.projects-panel-toolbar--hint .projects-fact-toolbar-hint {
margin: 0;
}
.projects-panel-count { flex-shrink: 0; padding: 4px 9px; border-radius: 999px; color: #2563eb; background: #eff6ff; font-size: .75rem; font-weight: 600; }
.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%; }
#project-panel-assets .data-table--projects th:nth-child(2), #project-panel-assets .data-table--projects td:nth-child(2) { width: 12%; }
#project-panel-assets .data-table--projects th:nth-child(3), #project-panel-assets .data-table--projects td:nth-child(3) { width: 22%; }
#project-panel-assets .data-table--projects th:nth-child(4), #project-panel-assets .data-table--projects td:nth-child(4) { width: 10%; }
#project-panel-assets .data-table--projects th:nth-child(5), #project-panel-assets .data-table--projects td:nth-child(5) { width: 16%; }
#project-panel-assets .data-table--projects th:nth-child(6), #project-panel-assets .data-table--projects td:nth-child(6) { width: 8%; }
#project-panel-assets .data-table--projects th:nth-child(7), #project-panel-assets .data-table--projects td:nth-child(7) { width: 8%; }
#project-panel-facts .data-table--projects .cell-fact-key {
overflow: hidden;
max-width: 0;
@@ -39899,3 +39948,407 @@ html[data-theme="dark"] .workflow-toolbar #workflow-connect-btn[aria-pressed="tr
grid-template-columns: minmax(0, 1fr);
}
}
/* Asset management */
.asset-page-subtitle { margin: 5px 0 0; color: var(--text-secondary, #6b7280); font-size: 13px; font-weight: 400; }
.asset-content { display: flex; flex-direction: column; gap: 20px; }
#page-asset-library .asset-content { overflow: hidden; }
#page-asset-library .asset-toolbar { flex: 0 0 auto; }
.asset-overview-summary { display: grid; grid-template-columns: minmax(0, 1.65fr) minmax(270px, .75fr); gap: 16px; }
.asset-total-card,.asset-recent-card,.asset-panel { border: 1px solid var(--border-color, #e5e7eb); border-radius: 16px; background: var(--card-bg, #fff); box-shadow: 0 8px 24px rgba(15,23,42,.035); }
.asset-total-card { min-width: 0; padding: 22px 24px 20px; }
.asset-total-card__heading { display: flex; align-items: center; gap: 12px; }
.asset-total-card__heading p { margin: 4px 0 0; color: var(--text-secondary, #6b7280); font-size: 12px; }
.asset-overview-icon { display: grid; width: 40px; height: 40px; flex: 0 0 40px; place-items: center; border-radius: 11px; }
.asset-overview-icon svg { width: 21px; height: 21px; }
.asset-overview-icon--blue { background: rgba(37,99,235,.11); color: #3b82f6; }
.asset-overview-icon--violet { background: rgba(99,102,241,.11); color: #6366f1; }
.asset-eyebrow { color: var(--text-primary, #111827); font-size: 14px; font-weight: 650; }
.asset-total-value { display: block; margin: 24px 0 22px; color: var(--text-primary, #111827); font-size: clamp(42px, 4vw, 58px); font-weight: 720; letter-spacing: -.045em; line-height: .9; }
.asset-type-grid { display: grid; grid-template-columns: repeat(3,minmax(0,1fr)); overflow: hidden; border: 1px solid var(--border-color, #e5e7eb); border-radius: 11px; background: var(--bg-secondary, #f8fafc); }
.asset-type-item { display: grid; grid-template-columns: auto 1fr auto; align-items: center; gap: 8px; min-width: 0; padding: 13px 14px; }
.asset-type-item + .asset-type-item { border-left: 1px solid var(--border-color, #e5e7eb); }
.asset-type-item > span:not(.asset-type-dot) { overflow: hidden; color: var(--text-secondary, #64748b); font-size: 12px; text-overflow: ellipsis; white-space: nowrap; }
.asset-type-item strong { color: var(--text-primary, #111827); font-size: 16px; }
.asset-type-dot { width: 7px; height: 7px; border-radius: 50%; background: #3b82f6; box-shadow: 0 0 0 4px rgba(59,130,246,.10); }
.asset-type-item--domain .asset-type-dot { background: #8b5cf6; box-shadow: 0 0 0 4px rgba(139,92,246,.10); }
.asset-type-item--port .asset-type-dot { background: #14b8a6; box-shadow: 0 0 0 4px rgba(20,184,166,.10); }
.asset-recent-card { display: flex; min-width: 0; flex-direction: column; overflow: hidden; padding: 22px; color: var(--text-primary,#111827); }
.asset-recent-card__top { display: flex; align-items: center; justify-content: space-between; }
.asset-recent-badge { padding: 5px 8px; border: 1px solid var(--border-color,#e5e7eb); border-radius: 999px; background: var(--bg-secondary,#f8fafc); color: var(--text-secondary,#64748b); font-size: 10px; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; }
.asset-recent-card__body { display: flex; margin-top: auto; flex-direction: column; gap: 7px; padding: 25px 0 18px; }
.asset-recent-card__body span { color: var(--text-secondary,#64748b); font-size: 13px; }
.asset-recent-card__body strong { font-size: clamp(38px,3.5vw,52px); font-weight: 720; letter-spacing: -.04em; line-height: 1; }
.asset-recent-progress { height: 5px; overflow: hidden; border-radius: 999px; background: var(--bg-secondary,#eef2f7); }
.asset-recent-progress i { display: block; width: 0; height: 100%; border-radius: inherit; background: linear-gradient(90deg,#3b82f6,#8b5cf6); transition: width .35s ease; }
.asset-recent-caption { margin: 9px 0 0; color: var(--text-secondary,#64748b); font-size: 11px; }
.asset-insight-heading { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 4px 2px 0; }
.asset-insight-heading h3 { margin: 0; color: var(--text-primary,#111827); font-size: 17px; }
.asset-insight-heading p { margin: 5px 0 0; color: var(--text-secondary,#64748b); font-size: 12px; }
.asset-period-switch { display: inline-flex; padding: 3px; border: 1px solid var(--border-color,#e5e7eb); border-radius: 9px; background: var(--card-bg,#fff); }
.asset-period-switch button { min-width: 54px; height: 29px; padding: 0 10px; border: 0; border-radius: 6px; background: transparent; color: var(--text-secondary,#64748b); cursor: pointer; font: inherit; font-size: 11px; }
.asset-period-switch button:hover { color: var(--text-primary,#111827); }
.asset-period-switch button.active { background: rgba(59,130,246,.12); color: #2563eb; font-weight: 650; }
.asset-trend-grid { display: grid; grid-template-columns: repeat(2,minmax(0,1fr)); gap: 16px; }
.asset-trend-panel { min-width: 0; }
.asset-trend-panel .asset-panel-header { min-height: 42px; margin-bottom: 14px; }
.asset-trend-header { display: grid; grid-template-columns: minmax(190px,1fr) auto; grid-template-areas: "heading legend" "heading summary"; align-items: center; column-gap: 24px; row-gap: 8px; }
.asset-trend-heading { grid-area: heading; min-width: 0; }
.asset-trend-header .asset-chart-legend { grid-area: legend; }
.asset-trend-header .asset-chart-summary { grid-area: summary; justify-content: flex-end; }
.asset-chart-legend { display: flex; align-items: center; flex-wrap: wrap; justify-content: flex-end; gap: 12px; }
.asset-chart-legend span { display: inline-flex; align-items: center; gap: 5px; color: var(--text-secondary,#64748b); font-size: 10px; white-space: nowrap; }
.asset-chart-legend span::before { width: 7px; height: 7px; border-radius: 50%; background: #3b82f6; content: ''; }
.asset-chart-legend .asset-legend-inactive::before { background: #8b5cf6; }
.asset-chart-legend .asset-legend-risk::before { background: #f59e0b; }
.asset-chart-legend .asset-legend-high::before { background: #ef4444; }
.asset-line-chart { min-height: 220px; }
.asset-line-chart svg { display: block; width: 100%; height: 220px; overflow: visible; color: var(--text-muted,#94a3b8); }
.asset-chart-grid line { stroke: var(--border-color,#e5e7eb); stroke-width: 1; stroke-dasharray: 3 4; }
.asset-chart-grid text { fill: currentColor; font-size: 9px; }
.asset-chart-summary { display: flex; align-items: center; gap: 18px; padding: 0; }
.asset-chart-summary div { display: flex; align-items: baseline; gap: 6px; white-space: nowrap; }
.asset-chart-summary span { color: var(--text-secondary,#64748b); font-size: 10px; }
.asset-chart-summary strong { font-size: 17px; line-height: 1; }
.asset-coverage-layout { display: grid; grid-template-columns: 180px minmax(0,1fr); align-items: center; gap: 28px; }
.asset-coverage-gauge { --asset-coverage-value: 0%; display: grid; width: 142px; height: 142px; margin: 2px auto; place-items: center; border-radius: 50%; background: conic-gradient(#3b82f6 0 var(--asset-coverage-value),var(--bg-secondary,#eef2f7) var(--asset-coverage-value) 100%); }
.asset-coverage-gauge::before { grid-area: 1/1; width: 104px; height: 104px; border-radius: 50%; background: var(--card-bg,#fff); content: ''; }
.asset-coverage-gauge > div { grid-area: 1/1; z-index: 1; text-align: center; }
.asset-coverage-gauge strong,.asset-coverage-gauge span { display: block; }
.asset-coverage-gauge strong { color: var(--text-primary,#111827); font-size: 27px; letter-spacing: -.03em; }
.asset-coverage-gauge span { margin-top: 5px; color: var(--text-secondary,#64748b); font-size: 10px; }
.asset-coverage-metrics { display: grid; grid-template-columns: repeat(4,minmax(0,1fr)); overflow: hidden; border: 1px solid var(--border-color,#e5e7eb); border-radius: 12px; background: var(--bg-secondary,#f8fafc); }
.asset-coverage-metrics > div { min-width: 0; padding: 17px 18px; }
.asset-coverage-metrics > div + div { border-left: 1px solid var(--border-color,#e5e7eb); }
.asset-coverage-metrics span,.asset-coverage-metrics strong,.asset-coverage-metrics small { display: block; }
.asset-coverage-metrics span { color: var(--text-secondary,#64748b); font-size: 11px; }
.asset-coverage-metrics strong { margin-top: 12px; color: var(--text-primary,#111827); font-size: 25px; letter-spacing: -.025em; }
.asset-coverage-metrics small { margin-top: 7px; overflow: hidden; color: var(--text-muted,#94a3b8); font-size: 9px; text-overflow: ellipsis; white-space: nowrap; }
.asset-coverage-metrics .asset-coverage-gap strong { color: #f59e0b; }
.asset-panel { padding: 22px 24px 24px; }
.asset-panel-header { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; margin-bottom: 22px; }
.asset-panel-header h3 { margin: 0; font-size: 16px; }
.asset-panel-header p { margin: 6px 0 0; color: var(--text-secondary, #64748b); font-size: 12px; }
.asset-panel-meta { flex: 0 0 auto; padding: 5px 9px; border-radius: 999px; background: var(--bg-secondary, #f1f5f9); color: var(--text-secondary, #64748b); font-size: 11px; }
.asset-protocol-layout { display: grid; grid-template-columns: 300px minmax(0,1fr); gap: 24px; align-items: stretch; }
.asset-protocol-summary { display: flex; min-height: 320px; flex-direction: column; padding: 20px; border: 1px solid var(--border-color, #e5e7eb); border-radius: 13px; background: var(--bg-secondary, #f8fafc); }
.asset-protocol-summary[hidden] { display: none; }
.asset-protocol-summary__label { color: var(--text-secondary,#64748b); font-size: 11px; font-weight: 600; }
.asset-protocol-summary__main { display: flex; align-items: center; gap: 20px; padding: 24px 0; }
.asset-protocol-donut { display: grid; width: 126px; height: 126px; flex: 0 0 126px; place-items: center; border-radius: 50%; background: conic-gradient(from -90deg,#6366f1 0 0%,var(--border-color,#e5e7eb) 0 100%); }
.asset-protocol-donut::before { content: ''; grid-area: 1/1; width: 92px; height: 92px; border-radius: 50%; background: var(--card-bg,#fff); }
.asset-protocol-donut > div { grid-area: 1/1; z-index: 1; text-align: center; }
.asset-protocol-donut strong,.asset-protocol-donut span { display: block; }
.asset-protocol-donut strong { color: var(--text-primary,#111827); font-size: 25px; }
.asset-protocol-donut span { width: 74px; margin: 4px auto 0; color: var(--text-secondary,#64748b); font-size: 8px; }
.asset-protocol-lead { min-width: 0; }
.asset-protocol-lead span,.asset-protocol-lead strong,.asset-protocol-lead small { display: block; }
.asset-protocol-lead span { color: var(--text-secondary,#64748b); font-size: 10px; }
.asset-protocol-lead strong { margin-top: 8px; overflow: hidden; color: var(--text-primary,#111827); font-size: 20px; text-overflow: ellipsis; white-space: nowrap; }
.asset-protocol-lead small { margin-top: 5px; color: var(--text-secondary,#64748b); font-size: 11px; }
.asset-protocol-top-list { display: flex; margin-top: auto; flex-direction: column; gap: 9px; padding-top: 16px; border-top: 1px solid var(--border-color,#e5e7eb); }
.asset-protocol-top-list div { display: grid; grid-template-columns: 7px 1fr auto; align-items: center; gap: 8px; }
.asset-protocol-top-list i { width: 7px; height: 7px; border-radius: 2px; }
.asset-protocol-top-list span { overflow: hidden; color: var(--text-secondary,#64748b); font-size: 10px; text-overflow: ellipsis; white-space: nowrap; }
.asset-protocol-top-list strong { color: var(--text-primary,#111827); font-size: 10px; }
.asset-protocol-ranking { min-width: 0; padding: 0 2px; }
.asset-protocol-columns { display: grid; grid-template-columns: 26px 94px minmax(140px,1fr) 46px 42px; align-items: center; gap: 12px; margin-bottom: 7px; padding: 0 12px 9px; border-bottom: 1px solid var(--border-color,#e5e7eb); color: var(--text-muted,#94a3b8); font-size: 9px; }
.asset-protocol-columns span:nth-last-child(-n+2) { text-align: right; }
.asset-bar-chart { display: flex; min-width: 0; flex-direction: column; gap: 2px; }
.asset-bar-row { display: grid; grid-template-columns: 26px 94px minmax(140px,1fr) 46px 42px; min-height: 35px; align-items: center; gap: 12px; padding: 3px 12px; border-radius: 7px; transition: background .15s; }
.asset-bar-row:hover { background: var(--bg-secondary,#f8fafc); }
.asset-bar-rank { color: var(--text-muted, #94a3b8); font: 10px ui-monospace,SFMono-Regular,Menlo,monospace; }
.asset-bar-label { display: flex; align-items: center; gap: 7px; color: var(--text-primary,#111827); font-size: 11px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; }
.asset-bar-label::before { width: 6px; height: 6px; flex: 0 0 6px; border-radius: 2px; background: var(--protocol-color,#3b82f6); content: ''; }
.asset-bar-track { height: 6px; overflow: hidden; border-radius: 999px; background: var(--bg-secondary, #eef2f7); }
.asset-bar-track i { display: block; height: 100%; border-radius: inherit; background: var(--protocol-color,#3b82f6); }
.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 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-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-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--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[hidden] { display: none; }
.asset-batch-actions > span { margin-right: auto; color: var(--text-primary, #111827); font-size: 13px; font-weight: 650; }
.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; }
.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 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(8),.asset-table td:nth-child(8) { width: 84px; }
.asset-table th:nth-child(9),.asset-table td:nth-child(9) { width: 176px; }
.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; }
.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; }
.asset-table td small { margin-top: 4px; color: var(--text-secondary, #64748b); }
.asset-tag { display: inline-block; margin: 5px 4px 0 0; padding: 2px 7px; border-radius: 999px; background: rgba(37,99,235,.1); color: #2563eb; font-size: 11px; }
.asset-project-badge { display: inline-flex; max-width: 180px; padding: 3px 8px; border-radius: 6px; background: rgba(99,102,241,.1); color: #4f46e5; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.asset-service { display: block; max-width: 100%; overflow: hidden; color: inherit; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; text-overflow: ellipsis; white-space: nowrap; }
.asset-status { display: inline-flex; padding: 3px 9px; border-radius: 999px; font-size: 12px; background: rgba(16,185,129,.12); color: #059669; }
.asset-status--inactive { background: rgba(100,116,139,.12); color: #64748b; }
.asset-risk { display: inline-flex; justify-content: center; min-width: 46px; padding: 3px 8px; border-radius: 999px; font-size: 12px; font-weight: 650; background: rgba(100,116,139,.12); color: #64748b; }
.asset-risk--critical { background: rgba(190,24,93,.16); color: #be185d; }
.asset-risk--high { background: rgba(239,68,68,.15); color: #dc2626; }
.asset-risk--medium { background: rgba(245,158,11,.16); color: #d97706; }
.asset-risk--low { background: rgba(59,130,246,.14); color: #2563eb; }
.asset-risk--info { background: rgba(99,102,241,.13); color: #4f46e5; }
.asset-risk--normal { background: rgba(16,185,129,.13); color: #059669; }
.asset-row-actions { white-space: nowrap; }
.asset-row-actions .btn-link { margin-right: 8px; }
.asset-row-actions .asset-delete { color: #dc2626; }
.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-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; }
.asset-project-content .settings-custom-select-option.is-disabled { display: none; }
.asset-editor-field > .asset-custom-select { width: 100%; }
.asset-editor-field > .asset-custom-select .settings-custom-select-trigger { width: 100%; }
.asset-scan-targets { display: flex; flex-wrap: wrap; align-items: center; gap: 7px; margin-bottom: 18px; padding: 12px; border: 1px solid var(--border-color, #e5e7eb); border-radius: 10px; background: var(--bg-secondary, #f8fafc); }
.asset-scan-target-chip { max-width: 220px; padding: 4px 9px; border-radius: 999px; background: rgba(37,99,235,.11); color: #2563eb; font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.asset-scan-prompt-label { display: block; margin-bottom: 8px; font-size: 13px; font-weight: 650; }
.asset-scan-prompt { width: 100%; min-height: 150px; padding: 12px; resize: vertical; border: 1px solid var(--border-color, #d8dee8); border-radius: 9px; background: var(--input-bg, #fff); color: var(--text-primary, #111827); font: inherit; line-height: 1.6; }
.asset-scan-modal .form-hint { display: block; margin: 8px 0 14px; color: var(--text-secondary, #64748b); }
.asset-scan-modal #asset-scan-execute-wrap[hidden] { display: none !important; }
.asset-pagination { display: flex; justify-content: flex-end; align-items: center; gap: 12px; }
.asset-editor-content { width: min(840px, calc(100vw - 32px)); max-height: calc(100vh - 48px); margin: 24px auto; }
.asset-editor-content .modal-header { padding: 20px 24px; }
.asset-editor-content .modal-header > div { min-width: 0; }
.asset-editor-content .modal-header h2 { font-size: 20px; }
.asset-editor-subtitle { margin: 5px 0 0; color: var(--text-secondary, #64748b); font-size: 13px; font-weight: 400; }
.asset-editor-body { padding: 22px 24px 24px; overscroll-behavior: contain; scrollbar-gutter: stable; }
.asset-editor-content .modal-footer { position: relative; background: var(--bg-primary, #fff); box-shadow: 0 -8px 24px rgba(15,23,42,.04); }
.asset-editor-primary { padding: 18px; border: 1px solid rgba(37,99,235,.22); border-radius: 12px; background: linear-gradient(135deg, rgba(37,99,235,.065), rgba(99,102,241,.025)); }
.asset-editor-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
.asset-editor-primary-grid { margin-top: 18px; }
.asset-editor-field { display: flex; min-width: 0; flex-direction: column; gap: 7px; font-size: 13px; font-weight: 600; }
.asset-editor-field > span { display: flex; align-items: center; gap: 4px; }
.asset-editor-field > span b { color: #dc2626; font-size: 15px; }
.asset-editor-field input,.asset-editor-field select { box-sizing: border-box; width: 100%; height: 42px; padding: 0 12px; border: 1px solid var(--border-color, #d8dee8); border-radius: 8px; outline: none; background: var(--input-bg, #fff); color: var(--text-primary, #111827); font: inherit; font-weight: 400; transition: border-color .15s, box-shadow .15s, background .15s; }
.asset-editor-field input:focus,.asset-editor-field select:focus,.asset-tag-editor:focus-within { border-color: #2563eb; box-shadow: 0 0 0 3px rgba(37,99,235,.12); }
.asset-editor-field input[aria-invalid="true"] { border-color: #dc2626; box-shadow: 0 0 0 3px rgba(220,38,38,.09); }
.asset-editor-target-field input { height: 48px; font-size: 15px; }
.asset-field-hint { color: var(--text-secondary, #64748b); font-size: 12px; font-weight: 400; line-height: 1.45; }
.asset-field-error { color: #dc2626; font-size: 12px; font-weight: 500; line-height: 1.45; }
.asset-form-error { margin-top: 14px; padding: 10px 12px; border: 1px solid rgba(220,38,38,.24); border-radius: 8px; background: rgba(220,38,38,.07); color: #b91c1c; font-size: 13px; line-height: 1.45; }
.asset-tag-editor { display: flex; min-height: 42px; align-items: center; flex-wrap: wrap; gap: 6px; padding: 5px 8px; border: 1px solid var(--border-color, #d8dee8); border-radius: 8px; background: var(--input-bg, #fff); transition: border-color .15s, box-shadow .15s; }
.asset-tag-chips { display: contents; }
.asset-tag-chip { display: inline-flex; max-width: 160px; align-items: center; gap: 5px; padding: 3px 7px; border-radius: 999px; background: rgba(37,99,235,.11); color: #1d4ed8; font-size: 12px; font-weight: 500; }
.asset-tag-chip span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.asset-tag-chip button { width: 16px; height: 16px; padding: 0; border: 0; border-radius: 50%; background: transparent; color: inherit; cursor: pointer; font-size: 14px; line-height: 14px; }
.asset-tag-chip button:hover { background: rgba(37,99,235,.14); }
.asset-tag-editor #asset-edit-tags { width: 130px; min-width: 90px; height: 30px; flex: 1; padding: 0 3px; border: 0; box-shadow: none; background: transparent; }
.asset-editor-details { margin-top: 24px; }
.asset-editor-section-group + .asset-editor-section-group { margin-top: 24px; padding-top: 24px; border-top: 1px solid var(--border-color, #e5e7eb); }
.asset-editor-group-heading { display: flex; min-width: 0; align-items: baseline; gap: 10px; margin-bottom: 12px; }
.asset-editor-group-heading::after { content: ''; height: 1px; min-width: 24px; flex: 1; background: var(--border-color, #e5e7eb); }
.asset-editor-group-heading h3 { flex: 0 0 auto; margin: 0; color: var(--text-primary, #111827); font-size: 13px; font-weight: 650; line-height: 1.45; }
.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 .asset-detail-content {
position: absolute;
top: 0;
right: 0;
width: min(620px, 92vw);
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;
}
.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-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 management dark theme */
html[data-theme="dark"] .asset-stat-card,
html[data-theme="dark"] .asset-total-card,
html[data-theme="dark"] .asset-recent-card,
html[data-theme="dark"] .asset-panel,
html[data-theme="dark"] .asset-toolbar,
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-detail-item {
background: var(--card-bg);
border-color: var(--border-color);
box-shadow: none;
}
html[data-theme="dark"] .asset-overview-icon--violet {
background: rgba(129, 140, 248, .14);
color: #a5b4fc;
}
html[data-theme="dark"] .asset-type-grid,
html[data-theme="dark"] .asset-coverage-metrics,
html[data-theme="dark"] .asset-protocol-summary {
background: rgba(15, 23, 42, .38);
border-color: var(--border-color);
}
html[data-theme="dark"] .asset-protocol-donut::before {
background: var(--card-bg);
}
html[data-theme="dark"] .asset-coverage-gauge::before {
background: var(--card-bg);
}
html[data-theme="dark"] .asset-period-switch button.active {
background: rgba(96,165,250,.16);
color: #93c5fd;
}
html[data-theme="dark"] .asset-stat-card--accent {
border-color: rgba(96, 165, 250, .38);
background: linear-gradient(135deg, rgba(37, 99, 235, .20), rgba(124, 58, 237, .10));
}
html[data-theme="dark"] .asset-stat-card--accent strong,
html[data-theme="dark"] .asset-target-link,
html[data-theme="dark"] .asset-row-actions .btn-link {
color: #60a5fa;
}
html[data-theme="dark"] .asset-toolbar input,
html[data-theme="dark"] .asset-toolbar select,
html[data-theme="dark"] .asset-editor-field input,
html[data-theme="dark"] .asset-editor-field select,
html[data-theme="dark"] .asset-editor-grid input,
html[data-theme="dark"] .asset-editor-grid select {
background: var(--input-bg);
border-color: var(--border-color);
color: var(--text-primary);
}
html[data-theme="dark"] .asset-toolbar input::placeholder,
html[data-theme="dark"] .asset-editor-field input::placeholder,
html[data-theme="dark"] .asset-editor-grid input::placeholder {
color: var(--text-muted);
}
html[data-theme="dark"] .asset-table tbody tr:hover td {
background: rgba(96, 165, 250, .06);
}
html[data-theme="dark"] .asset-tag {
background: rgba(96, 165, 250, .16);
color: #93c5fd;
}
html[data-theme="dark"] .asset-project-badge {
background: rgba(167, 139, 250, .16);
color: #c4b5fd;
}
html[data-theme="dark"] .asset-status {
background: rgba(52, 211, 153, .14);
color: #6ee7b7;
}
html[data-theme="dark"] .asset-status--inactive {
background: rgba(148, 163, 184, .14);
color: #cbd5e1;
}
html[data-theme="dark"] .asset-risk--critical { color: #f9a8d4; }
html[data-theme="dark"] .asset-risk--high { color: #fca5a5; }
html[data-theme="dark"] .asset-risk--medium { color: #fcd34d; }
html[data-theme="dark"] .asset-risk--low { color: #93c5fd; }
html[data-theme="dark"] .asset-risk--info { color: #c4b5fd; }
html[data-theme="dark"] .asset-risk--normal { color: #6ee7b7; }
html[data-theme="dark"] .asset-row-actions .asset-delete {
color: #f87171;
}
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-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 { box-shadow: 0 14px 32px rgba(0,0,0,.34); }
html[data-theme="dark"] .asset-detail-grid {
border-color: var(--border-color);
background: var(--border-color);
}
html[data-theme="dark"] .asset-detail-item code {
background: var(--bg-secondary);
color: var(--text-secondary);
}
html[data-theme="dark"] .asset-editor-primary { border-color: rgba(96,165,250,.32); background: linear-gradient(135deg, rgba(37,99,235,.18), rgba(99,102,241,.08)); }
html[data-theme="dark"] .asset-tag-editor { border-color: var(--border-color); background: var(--card-bg); }
html[data-theme="dark"] .asset-editor-content .modal-footer { background: var(--bg-primary); box-shadow: 0 -8px 24px rgba(0,0,0,.12); }
html[data-theme="dark"] .asset-tag-chip { background: rgba(96,165,250,.16); color: #93c5fd; }
html[data-theme="dark"] .asset-form-error { color: #fca5a5; }
@media (max-width: 1180px) {
.asset-overview-summary { grid-template-columns: minmax(0,1.45fr) minmax(240px,.75fr); }
.asset-coverage-metrics { grid-template-columns: repeat(2,minmax(0,1fr)); }
.asset-coverage-metrics > div:nth-child(3) { border-top: 1px solid var(--border-color,#e5e7eb); border-left: 0; }
.asset-coverage-metrics > div:nth-child(4) { border-top: 1px solid var(--border-color,#e5e7eb); }
.asset-protocol-layout { grid-template-columns: 1fr; }
.asset-protocol-summary { width: auto; min-height: 0; }
.asset-protocol-summary__main { justify-content: center; }
.asset-protocol-top-list { display: grid; grid-template-columns: repeat(3,minmax(0,1fr)); margin-top: 0; }
}
@media (max-width: 1050px) {
.asset-table th:nth-child(4),.asset-table td:nth-child(4) { display: none; }
}
@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; }
}
@media (max-width: 760px) {
.asset-overview-summary { grid-template-columns: 1fr; }
.asset-trend-grid { grid-template-columns: 1fr; }
.asset-insight-heading { align-items: flex-start; flex-direction: column; }
.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%; } }
+178
View File
@@ -78,6 +78,9 @@
"nav": {
"dashboard": "Dashboard",
"chat": "Chat",
"assets": "Asset Management",
"assetOverview": "Asset Overview",
"assetLibrary": "Asset Library",
"infoCollect": "Recon",
"tasks": "Tasks",
"projects": "Projects",
@@ -109,6 +112,12 @@
"c2Profiles": "Traffic profiles",
"platformRbac": "Platform Access"
},
"navGroups": {
"workbench": "Workspace",
"operations": "Security Operations",
"capabilities": "Capabilities",
"administration": "Administration"
},
"dashboard": {
"title": "Dashboard",
"refresh": "Refresh",
@@ -286,9 +295,13 @@
"addFactCta": "+ Add fact",
"tabFacts": "Fact board",
"tabGraph": "Attack path",
"tabAssets": "Asset library",
"tabConversations": "Bound conversations",
"tabVulns": "Related vulnerabilities",
"tabSettings": "Settings",
"boundAssetsHint": "Only assets bound to this project are shown; click a target for full details",
"noBoundAssets": "No assets are bound to this project",
"assetCount": "{{count}} assets",
"factToolbarHint": "Index includes key and summary only (must include what + where + how to verify); put attack chain / POC in body, and reproduce via get_project_fact.",
"graphToolbarHint": "Graph arrows match stored fact links (source → target). Nodes are layered target→infra→finding→exploit. Dashed edges are tentative.",
"graphView": "View",
@@ -579,6 +592,7 @@
"liveTimelinePrunedRoundRange": "main-agent rounds {{from}}{{to}}",
"toolExecutionsCount": "{{n}} tool runs",
"collapseToolExecutions": "Collapse tool runs",
"toolExecutionDetailPending": "Tool execution details have not synced yet. Please try again shortly.",
"noProcessDetail": "No process details (execution may be too fast or no detailed events)",
"copyMessageTitle": "Copy message",
"deleteTurnTitle": "Delete this turn",
@@ -998,6 +1012,169 @@
"parseModalApply": "Fill into query",
"parseModalApplyRun": "Fill and query"
},
"assets": {
"overviewTitle": "Asset Overview",
"libraryTitle": "Asset Library",
"viewLibrary": "View asset library",
"totalAssets": "Total assets",
"totalAssetsHint": "Assets currently under continuous monitoring",
"ipCount": "IP addresses",
"domainCount": "Domains",
"portCount": "Ports",
"recentCount": "Discovered in 7 days",
"recentWindow": "7 days",
"recentShare": "{{percent}}% of all current assets",
"protocolDistribution": "Protocol distribution",
"protocolDistributionHint": "Exposure composition by identified service",
"protocolKinds": "{{count}} protocols",
"topProtocol": "Top protocol",
"topProtocolShare": "Top protocol share",
"protocolComposition": "Protocol composition",
"protocolRank": "Rank",
"protocolName": "Protocol",
"assetRatio": "Asset share",
"assetAmount": "Count",
"ratio": "Share",
"assetCountUnit": "{{count}} assets",
"postureChanges": "Posture changes",
"postureChangesHint": "Track asset and risk changes over the selected period",
"periodSelection": "Period selection",
"days7": "7 days",
"days30": "30 days",
"days90": "90 days",
"assetTrend": "Asset change trend",
"assetTrendHint": "New discoveries and currently inactive assets",
"addedAssets": "Added assets",
"inactiveAssets": "Inactive assets",
"riskTrend": "Risk discovery trend",
"riskTrendHint": "New vulnerabilities and critical or high-risk findings",
"discoveredRisks": "New vulnerabilities",
"highRisks": "Critical & high",
"scanCoverage": "Scan coverage",
"scanCoverageHint": "Identify assets that are unscanned or overdue for review",
"overallCoverage": "Overall coverage",
"scannedAssets": "Scanned assets",
"scannedAssetsHint": "Scanned at least once",
"recentlyScanned": "Covered in 30 days",
"neverScanned": "Never scanned",
"neverScannedHint": "Prioritize for scanning",
"staleScans": "Not scanned in 30+ days",
"staleScansHint": "Scan results may be stale",
"coverageOfTotal": "{{percent}}% of all assets",
"coverageMeta": "{{scanned}} / {{total}} covered",
"addAsset": "+ Add asset",
"addAssetTitle": "Add asset",
"editAssetTitle": "Edit asset",
"editorSubtitle": "Enter one target; other details are parsed automatically or can be added later",
"assetAddress": "Asset address",
"assetAddressHint": "Any non-empty target is accepted; recognized URLs, domains, IPs, and ports are parsed automatically",
"targetWhitespace": "The asset address cannot contain spaces",
"targetInvalid": "Enter a valid URL, domain, or IP address",
"hostInvalid": "The full URL is invalid or contains credentials",
"ipInvalid": "Invalid IP address",
"domainInvalid": "Invalid domain",
"protocolInvalid": "Invalid protocol",
"portInvalid": "Port must be between 1 and 65535",
"connectionInfo": "Connection",
"connectionInfoHint": "Parsed from the asset address automatically; adjust as needed",
"fingerprintInfo": "Identification",
"fingerprintInfoHint": "Add page and service fingerprints for easier discovery",
"locationAndStatus": "Location and status",
"locationAndStatusHint": "Record the asset location and current availability",
"hostUrl": "Full URL / Host",
"pageTitle": "Page title",
"serverProduct": "Service / product",
"tagsPlaceholder": "Press Enter or comma to add",
"tagsHint": "Classify by environment, business, or ownership",
"removeTag": "Remove tag {{tag}}",
"addAssetAction": "Add asset",
"discardChanges": "Discard unsaved changes?",
"createdSuccessfully": "Asset added",
"updatedSuccessfully": "Asset updated",
"duplicateMerged": "Asset already existed; details were safely merged",
"assetSkipped": "Asset was not saved; it may exist without update permission",
"searchPlaceholder": "Search host, IP, domain, title, service, or tag",
"allStatuses": "All statuses",
"allProjects": "All projects",
"unboundProject": "Not bound",
"project": "Project",
"archived": "Archived",
"statusActive": "Active",
"statusInactive": "Inactive",
"target": "Target",
"service": "Service",
"title": "Title / fingerprint",
"location": "Location",
"country": "Country / region",
"province": "Province / state",
"city": "City",
"source": "Source",
"lastSeen": "Last seen",
"status": "Status",
"domain": "Domain",
"port": "Port",
"protocol": "Protocol",
"server": "Server fingerprint",
"tags": "Tags (comma separated)",
"tagsLabel": "Tags",
"detailTitle": "Asset details",
"sourceQuery": "Source query",
"firstSeen": "First seen",
"totalMeta": "{{count}} assets",
"targetRequired": "Enter an asset address",
"loadFailed": "Failed to load assets",
"saveFailed": "Failed to save asset",
"saved": "Asset saved",
"deleteConfirm": "Delete this asset?",
"deleteFailed": "Failed to delete asset",
"importSelected": "Import selected",
"importOne": "Add to asset library",
"selectFirst": "Select results to import first",
"noValidImportTarget": "None of the selected results has a valid asset target",
"importFailed": "Failed to import assets",
"importDone": "Created {{created}}, updated {{updated}}",
"importDoneWithInvalid": "Created {{created}}, updated {{updated}}, skipped {{invalid}} results without valid targets",
"selectPage": "Select this page",
"selectAsset": "Select asset",
"selectedCount": "{{count}} selected",
"clearSelection": "Clear selection",
"bindProject": "Bind project",
"bindProjectTitle": "Bind project",
"chooseProject": "Choose a project",
"selectProjectRequired": "Choose a project to bind",
"bindProjectCount": "Update {{count}} assets",
"bindProjectHint": "All selected assets will be bound to this project.",
"confirmBinding": "Confirm",
"bindProjectDone": "Bound {{count}} assets",
"bindProjectFailed": "Failed to bind project",
"sendToChat": "Send to chat",
"sendToChatShort": "Scan",
"createScanTask": "Create scan tasks",
"scanTitle": "Scan assets",
"scanAssetCount": "{{count}} assets",
"userPrompt": "User prompt",
"promptHint": "Use {{asset_id}}, {{target}}, {{host}}, {{ip}}, {{domain}}, or {{port}} placeholders. Task mode creates one task per asset.",
"executeNow": "Run immediately after creation",
"confirmSend": "Send",
"confirmCreate": "Create tasks",
"defaultScanPrompt": "Perform an authorized security scan of {{target}} (asset ID: {{asset_id}}). Save confirmed findings with record_vulnerability, then call complete_asset_scan(id={{asset_id}}) to update the last scan time and related vulnerability count.",
"selectAssetsFirst": "Select assets first",
"promptRequired": "Enter a user prompt",
"scanSubmitFailed": "Failed to submit scan",
"scanTaskLinkFailed": "Tasks were created, but asset linking failed",
"scanConversationTitle": "Asset scan: {{targets}}",
"scanQueueTitle": "Batch asset scan",
"lastScan": "Last scan",
"relatedVulnerabilities": "Related findings",
"riskLevel": "Risk",
"riskCritical": "Critical",
"riskHigh": "High",
"riskMedium": "Medium",
"riskLow": "Low",
"riskInfo": "Info",
"riskNormal": "Clear",
"riskUnassessed": "Unassessed"
},
"vulnerability": {
"title": "Vulnerability Management",
"addVuln": "Add vulnerability",
@@ -2496,6 +2673,7 @@
"knowledge": "Knowledge",
"conversation": "Conversation",
"vulnerability": "Vulnerability",
"asset": "Asset",
"externalMcp": "External MCP",
"task": "Tasks",
"tool": "Tools",
+183 -5
View File
@@ -78,6 +78,9 @@
"nav": {
"dashboard": "仪表盘",
"chat": "对话",
"assets": "资产管理",
"assetOverview": "资产概览",
"assetLibrary": "资产库",
"infoCollect": "信息收集",
"tasks": "任务管理",
"projects": "项目管理",
@@ -90,11 +93,11 @@
"knowledge": "知识",
"knowledgeRetrievalLogs": "检索历史",
"knowledgeManagement": "知识管理",
"skills": "Skills",
"skillsMonitor": "Skills状态监控",
"skillsManagement": "Skills管理",
"agents": "Agents",
"agentsManagement": "Agent管理",
"skills": "技能",
"skillsMonitor": "技能状态",
"skillsManagement": "技能管理",
"agents": "智能体",
"agentsManagement": "智能体管理",
"roles": "角色",
"rolesManagement": "角色管理",
"workflows": "工作流",
@@ -109,6 +112,12 @@
"c2Profiles": "流量伪装",
"platformRbac": "平台权限"
},
"navGroups": {
"workbench": "工作台",
"operations": "安全作业",
"capabilities": "能力中心",
"administration": "平台管理"
},
"dashboard": {
"title": "仪表盘",
"refresh": "刷新",
@@ -274,9 +283,13 @@
"addFactCta": "+ 添加事实",
"tabFacts": "事实黑板",
"tabGraph": "攻击路径",
"tabAssets": "资产库",
"tabConversations": "关联对话",
"tabVulns": "关联漏洞",
"tabSettings": "设置",
"boundAssetsHint": "仅展示绑定到当前项目的资产;点击目标可查看完整详情",
"noBoundAssets": "暂无绑定到此项目的资产",
"assetCount": "{{count}} 个资产",
"factToolbarHint": "索引仅含 key 与摘要(须含「什么 + 在哪 + 如何验证」);攻击链 / POC 写在 bodyAgent 通过 get_project_fact 复现",
"graphToolbarHint": "攻击路径图箭头与事实存储方向一致(source → target);节点按 target→infra→finding→exploit 分层排布。虚线边为待确认。",
"graphView": "视图",
@@ -567,6 +580,7 @@
"liveTimelinePrunedRoundRange": "主代理第 {{from}}{{to}} 轮",
"toolExecutionsCount": "{{n}}次工具执行",
"collapseToolExecutions": "收起工具执行",
"toolExecutionDetailPending": "工具执行详情尚未同步,请稍后重试。",
"noProcessDetail": "暂无过程详情(可能执行过快或未触发详细事件)",
"copyMessageTitle": "复制消息内容",
"deleteTurnTitle": "删除本轮对话",
@@ -986,6 +1000,169 @@
"parseModalApply": "填入查询框",
"parseModalApplyRun": "填入并查询"
},
"assets": {
"overviewTitle": "资产概览",
"libraryTitle": "资产库",
"viewLibrary": "查看资产库",
"totalAssets": "资产总数",
"totalAssetsHint": "当前纳入持续监测的资产",
"ipCount": "IP 数量",
"domainCount": "域名数量",
"portCount": "端口数量",
"recentCount": "近 7 天发现",
"recentWindow": "7 天",
"recentShare": "占当前资产总量的 {{percent}}%",
"protocolDistribution": "协议分布",
"protocolDistributionHint": "按已识别服务查看资产暴露构成",
"protocolKinds": "{{count}} 种协议",
"topProtocol": "主要协议",
"topProtocolShare": "主要协议占比",
"protocolComposition": "协议构成",
"protocolRank": "排名",
"protocolName": "协议",
"assetRatio": "资产占比",
"assetAmount": "数量",
"ratio": "占比",
"assetCountUnit": "{{count}} 个资产",
"postureChanges": "态势变化",
"postureChangesHint": "观察资产与风险在选定周期内的变化",
"periodSelection": "周期选择",
"days7": "7 天",
"days30": "30 天",
"days90": "90 天",
"assetTrend": "资产增减趋势",
"assetTrendHint": "新增发现与当前已停用资产",
"addedAssets": "新增资产",
"inactiveAssets": "停用资产",
"riskTrend": "风险发现趋势",
"riskTrendHint": "新增漏洞及其中的高危与严重风险",
"discoveredRisks": "新增漏洞",
"highRisks": "高危及严重",
"scanCoverage": "扫描覆盖",
"scanCoverageHint": "识别未扫描与长期未复查的资产缺口",
"overallCoverage": "总体覆盖率",
"scannedAssets": "已扫描资产",
"scannedAssetsHint": "至少完成过一次扫描",
"recentlyScanned": "近 30 天覆盖",
"neverScanned": "从未扫描",
"neverScannedHint": "建议优先纳入扫描",
"staleScans": "超过 30 天未扫描",
"staleScansHint": "扫描结果可能已过期",
"coverageOfTotal": "占全部资产 {{percent}}%",
"coverageMeta": "{{scanned}} / {{total}} 已覆盖",
"addAsset": "+ 新增资产",
"addAssetTitle": "新增资产",
"editAssetTitle": "编辑资产",
"editorSubtitle": "输入一个目标即可,其余信息可自动解析或稍后补充",
"assetAddress": "资产地址",
"assetAddressHint": "仅需填写非空目标;可识别的 URL、域名、IP 和端口会自动解析",
"targetWhitespace": "资产地址不能包含空格",
"targetInvalid": "请输入有效的 URL、域名或 IP 地址",
"hostInvalid": "完整 URL 格式无效或包含凭据",
"ipInvalid": "IP 地址格式无效",
"domainInvalid": "域名格式无效",
"protocolInvalid": "协议格式无效",
"portInvalid": "端口必须在 165535 之间",
"connectionInfo": "连接信息",
"connectionInfoHint": "由资产地址自动解析,可按需修正",
"fingerprintInfo": "识别信息",
"fingerprintInfoHint": "补充页面与服务指纹,便于检索和识别",
"locationAndStatus": "地理与状态",
"locationAndStatusHint": "记录资产归属地区和当前启用状态",
"hostUrl": "完整 URL / Host",
"pageTitle": "页面标题",
"serverProduct": "服务 / 产品",
"tagsPlaceholder": "输入后按回车或逗号",
"tagsHint": "用于环境、业务或责任范围分类",
"removeTag": "移除标签 {{tag}}",
"addAssetAction": "添加资产",
"discardChanges": "放弃尚未保存的更改吗?",
"createdSuccessfully": "资产已添加",
"updatedSuccessfully": "资产已更新",
"duplicateMerged": "资产已存在,信息已安全合并",
"assetSkipped": "资产未保存,可能已存在且你没有更新权限",
"searchPlaceholder": "搜索主机、IP、域名、标题、服务或标签",
"allStatuses": "全部状态",
"allProjects": "全部项目",
"unboundProject": "暂不绑定",
"project": "所属项目",
"archived": "已归档",
"statusActive": "活跃",
"statusInactive": "停用",
"target": "目标",
"service": "服务",
"title": "标题/指纹",
"location": "地区",
"country": "国家/地区",
"province": "省份/州",
"city": "城市",
"source": "来源",
"lastSeen": "最近发现",
"status": "状态",
"domain": "域名",
"port": "端口",
"protocol": "协议",
"server": "服务指纹",
"tags": "标签(逗号分隔)",
"tagsLabel": "标签",
"detailTitle": "资产详情",
"sourceQuery": "来源查询",
"firstSeen": "首次发现",
"totalMeta": "共 {{count}} 条",
"targetRequired": "请输入资产地址",
"loadFailed": "加载资产失败",
"saveFailed": "保存资产失败",
"saved": "资产已保存",
"deleteConfirm": "确定删除该资产吗?",
"deleteFailed": "删除资产失败",
"importSelected": "入库所选",
"importOne": "加入资产库",
"selectFirst": "请先选择需要入库的结果",
"noValidImportTarget": "所选结果中没有可入库的有效资产目标",
"importFailed": "资产入库失败",
"importDone": "已新增 {{created}} 条,更新 {{updated}} 条",
"importDoneWithInvalid": "已新增 {{created}} 条,更新 {{updated}} 条,跳过 {{invalid}} 条无有效目标的结果",
"selectPage": "选择本页",
"selectAsset": "选择资产",
"selectedCount": "已选择 {{count}} 项",
"clearSelection": "取消选择",
"bindProject": "绑定项目",
"bindProjectTitle": "绑定项目",
"chooseProject": "请选择项目",
"selectProjectRequired": "请选择要绑定的项目",
"bindProjectCount": "将更新 {{count}} 个资产",
"bindProjectHint": "所选资产将统一绑定到该项目。",
"confirmBinding": "确认绑定",
"bindProjectDone": "已绑定 {{count}} 个资产",
"bindProjectFailed": "绑定项目失败",
"sendToChat": "发送到对话",
"sendToChatShort": "扫描",
"createScanTask": "创建扫描任务",
"scanTitle": "扫描资产",
"scanAssetCount": "共 {{count}} 个资产",
"userPrompt": "用户提示词",
"promptHint": "可使用 {{asset_id}}、{{target}}、{{host}}、{{ip}}、{{domain}}、{{port}} 占位符;创建任务时会为每个资产生成一条任务。",
"executeNow": "创建后立即执行",
"confirmSend": "确认发送",
"confirmCreate": "创建任务",
"defaultScanPrompt": "请对资产 {{target}}(资产ID{{asset_id}})进行授权安全扫描,优先检查暴露服务、已知漏洞、弱口令和常见 Web 风险;通过 record_vulnerability 保存确认的漏洞,完成后调用 complete_asset_scan(id={{asset_id}}) 回写上次扫描时间和相关漏洞。",
"selectAssetsFirst": "请先选择资产",
"promptRequired": "请输入用户提示词",
"scanSubmitFailed": "提交扫描失败",
"scanTaskLinkFailed": "任务已创建,但资产关联失败",
"scanConversationTitle": "资产扫描:{{targets}}",
"scanQueueTitle": "资产批量扫描",
"lastScan": "上次扫描",
"relatedVulnerabilities": "相关漏洞",
"riskLevel": "风险等级",
"riskCritical": "严重",
"riskHigh": "高危",
"riskMedium": "中危",
"riskLow": "低危",
"riskInfo": "提示",
"riskNormal": "正常",
"riskUnassessed": "未评估"
},
"vulnerability": {
"title": "漏洞管理",
"addVuln": "添加漏洞",
@@ -2484,6 +2661,7 @@
"knowledge": "知识库",
"conversation": "对话",
"vulnerability": "漏洞",
"asset": "资产",
"externalMcp": "外部 MCP",
"task": "任务",
"tool": "工具",
File diff suppressed because it is too large Load Diff
+3
View File
@@ -360,6 +360,9 @@ const PAGE_PERMISSION_MAP = {
chat: 'chat:read',
hitl: 'hitl:read',
'info-collect': 'fofa:execute',
assets: 'asset:read',
'asset-overview': 'asset:read',
'asset-library': 'asset:read',
tasks: 'tasks:read',
workflows: 'workflow:read',
projects: 'project:read',
+45 -13
View File
@@ -3079,6 +3079,29 @@ function getCachedToolExecutionSummaries(messageElement) {
}
}
/**
* 过程摘要中的早期/快速工具结果可能没有 executionId但消息本身会按调用顺序保存 ID
* 合并两份数据避免渲染摘要时丢失可用的弹窗详情入口
*/
function mergeToolExecutionSummariesWithIds(summaries, executionIds) {
const normalizedSummaries = Array.isArray(summaries)
? summaries.map(normalizeToolExecutionSummaryForButton)
: [];
const normalizedIds = normalizeMcpExecutionIds(executionIds);
const claimedIds = new Set(
normalizedSummaries.map((item) => item.executionId).filter(Boolean)
);
const fallbackIds = normalizedIds.filter((id) => !claimedIds.has(id));
let fallbackIndex = 0;
return normalizedSummaries.map((item) => {
if (item.executionId || fallbackIndex >= fallbackIds.length) return item;
return {
...item,
executionId: fallbackIds[fallbackIndex++]
};
});
}
function setPendingToolExecutionSummaries(messageElement, summaries) {
if (!messageElement || !messageElement.dataset || !Array.isArray(summaries)) return;
const normalized = cacheToolExecutionSummaries(messageElement, summaries);
@@ -3337,9 +3360,28 @@ window.setMcpExecutionSummaryCount = setMcpExecutionSummaryCount;
window.setPendingMcpExecutionIds = setPendingMcpExecutionIds;
window.setPendingToolExecutionSummaries = setPendingToolExecutionSummaries;
async function openTaskToolExecutionDetail(messageElement, item, index) {
let detailItem = item;
if (!detailItem.executionId) {
const refreshedItem = await resolveToolExecutionSummaryForFocus(messageElement, '', index);
const mergedItems = mergeToolExecutionSummariesWithIds(
getCachedToolExecutionSummaries(messageElement),
getCachedMcpExecutionIds(messageElement)
);
detailItem = mergedItems[index] || refreshedItem || detailItem;
}
if (detailItem.executionId) {
await showMCPDetail(detailItem.executionId);
return;
}
alert(typeof window.t === 'function'
? window.t('chat.toolExecutionDetailPending')
: '工具执行详情尚未同步,请稍后重试。');
}
/**
* 声明式渲染工具调用列表
* 过程摘要是展示与定位的唯一模型executionIds 仅在摘要尚未到达时提供占位
* 过程摘要是展示详情入口的唯一模型executionIds 仅在摘要尚未到达时提供占位
* 每次更新整体替换列表避免增量追加产生双重状态
*/
function renderMcpCallButtons(messageElement) {
@@ -3350,7 +3392,7 @@ function renderMcpCallButtons(messageElement) {
const executionIds = getCachedMcpExecutionIds(messageElement);
const summaries = getCachedToolExecutionSummaries(messageElement);
const items = summaries.length > 0
? summaries
? mergeToolExecutionSummariesWithIds(summaries, executionIds)
: executionIds.map((executionId) => normalizeToolExecutionSummaryForButton({ executionId }));
const renderVersion = String((parseInt(toolList.dataset.renderVersion, 10) || 0) + 1);
@@ -3367,17 +3409,7 @@ function renderMcpCallButtons(messageElement) {
if (item.toolCallId) {
btn.dataset.toolCallId = item.toolCallId;
}
btn.onclick = async () => {
let focusItem = item;
if (!focusItem.processDetailId && !focusItem.toolCallId && focusItem.executionId) {
focusItem = await resolveToolExecutionSummaryForFocus(
messageElement,
focusItem.executionId,
index
) || focusItem;
}
await focusToolExecutionInProcessDetails(messageElement, focusItem, index);
};
btn.onclick = () => openTaskToolExecutionDetail(messageElement, item, index);
if (item.toolName) {
renderToolExecutionButtonContent(btn, item.toolName, String(index + 1), item.status);
} else {
+4 -3
View File
@@ -66,7 +66,9 @@
const skipText = el.getAttribute('data-i18n-skip-text') === 'true';
const isFormControl = (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA');
const attrList = el.getAttribute('data-i18n-attr');
const text = i18next.t(key);
const translated = i18next.t(key);
// 缺键时保留模板中的后备文案,避免页面直接显示 assets.project 一类内部键名。
const text = translated && translated !== key ? translated : '';
// 仅当元素无子元素(仅文本或空)时才替换文本,避免覆盖卡片内的数字、子节点等;input/textarea 永不设置 textContent
const hasNoElementChildren = !el.querySelector('*');
if (!skipText && !isFormControl && hasNoElementChildren && text && typeof text === 'string') {
@@ -80,7 +82,7 @@
var val = text;
if (attr === 'title' && titleKey) {
var titleText = i18next.t(titleKey);
if (titleText && typeof titleText === 'string') val = titleText;
if (titleText && titleText !== titleKey && typeof titleText === 'string') val = titleText;
}
if (val && typeof val === 'string') {
el.setAttribute(attr, val);
@@ -233,4 +235,3 @@
});
});
})();
+4 -1
View File
@@ -556,6 +556,9 @@ function renderFofaResults(payload) {
<path d="M16 8h3a4 4 0 0 1 0 8h-3" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>
</button>
<button class="btn-icon" data-require-permission="asset:write" onclick="importFofaRowAsset(${idx}); event.stopPropagation();" title="${escapeHtml(_t('assets.importOne'))}">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none"><path d="M12 3v12m0 0 4-4m-4 4-4-4M4 19h16" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
</div>
`;
@@ -566,6 +569,7 @@ function renderFofaResults(payload) {
// 更新全选框状态
syncSelectAllCheckbox();
if (typeof applyRBACToUI === 'function') applyRBACToUI(els.tbody);
}
function inferTargetFromRow(row, fields) {
@@ -1147,4 +1151,3 @@ if (document.readyState === 'loading') {
} else {
updateSelectedMeta();
}
+49 -1
View File
@@ -7,6 +7,7 @@ const PROJECTS_LIST_PAGE_SIZE_KEY = 'cyberstrike.projects_list_page_size';
let currentProjectId = null;
let currentProjectUpdatedAt = null;
let currentProjectTab = 'facts';
let currentProjectAssets = [];
const projectNameById = {};
let _projectsListReady = false;
let _projectsFetchPromise = null;
@@ -1016,8 +1017,9 @@ async function selectProject(id) {
}
function switchProjectTab(tab) {
if (tab === 'assets' && typeof hasPermission === 'function' && !hasPermission('asset:read')) tab = 'facts';
currentProjectTab = tab;
['facts', 'graph', 'conversations', 'vulns', 'settings'].forEach((t) => {
['facts', 'graph', 'assets', 'conversations', 'vulns', 'settings'].forEach((t) => {
const btn = document.getElementById(`project-tab-${t}`);
const panel = document.getElementById(`project-panel-${t}`);
if (btn) btn.classList.toggle('is-active', t === tab);
@@ -1025,10 +1027,54 @@ function switchProjectTab(tab) {
});
if (tab === 'facts') loadProjectFacts();
if (tab === 'graph') loadProjectFactGraph();
if (tab === 'assets') loadProjectAssets();
if (tab === 'conversations') loadProjectConversations();
if (tab === 'vulns') loadProjectVulnerabilities();
}
async function loadProjectAssets() {
const tbody = document.getElementById('project-assets-tbody');
const countEl = document.getElementById('project-assets-count');
if (!tbody || !currentProjectId) return;
tbody.innerHTML = `<tr class="is-empty-row"><td colspan="7">${escapeHtml(tpFmt('common.loading', '加载中...'))}</td></tr>`;
const qs = new URLSearchParams({ project_id: currentProjectId, page: '1', page_size: '100' });
const res = await apiFetch(`/api/assets?${qs.toString()}`);
if (!res.ok) {
currentProjectAssets = [];
if (countEl) countEl.textContent = '0';
tbody.innerHTML = `<tr class="is-empty-row"><td colspan="7">${escapeHtml(tpFmt('common.loadFailed', '加载失败'))}</td></tr>`;
return;
}
const data = await res.json();
currentProjectAssets = data.assets || [];
if (countEl) countEl.textContent = tpFmt('projects.assetCount', `${data.total || 0} 个资产`, { count: data.total || 0 });
if (!currentProjectAssets.length) {
tbody.innerHTML = `<tr class="is-empty-row"><td colspan="7">${escapeHtml(tpFmt('projects.noBoundAssets', '暂无绑定到此项目的资产'))}</td></tr>`;
return;
}
tbody.innerHTML = currentProjectAssets.map((asset, index) => {
const target = asset.host || asset.domain || asset.ip || '-';
const service = [asset.protocol, asset.port ? ':' + asset.port : ''].join('') || '-';
const fingerprint = [asset.title, asset.server].filter(Boolean).join(' · ') || '-';
const updated = asset.last_seen_at ? new Date(asset.last_seen_at).toLocaleString() : '-';
const status = asset.status === 'inactive' ? tpFmt('assets.statusInactive', '停用') : tpFmt('assets.statusActive', '活跃');
return `<tr>
<td class="cell-summary"><button type="button" class="projects-asset-target" onclick="openProjectAssetDetail(${index})" title="${escapeHtml(target)}">${escapeHtml(target)}</button></td>
<td><code>${escapeHtml(service)}</code></td>
<td class="cell-summary" title="${escapeHtml(fingerprint)}">${escapeHtml(fingerprint)}</td>
<td>${escapeHtml(asset.source || '-')}</td>
<td>${escapeHtml(updated)}</td>
<td><span class="asset-status asset-status--${escapeHtml(asset.status || 'active')}">${escapeHtml(status)}</span></td>
<td class="col-actions"><div class="projects-table-actions"><button type="button" class="projects-action-btn projects-action-btn--view" onclick="openProjectAssetDetail(${index})">${escapeHtml(tpFmt('common.view', '查看'))}</button></div></td>
</tr>`;
}).join('');
}
function openProjectAssetDetail(index) {
const asset = currentProjectAssets[Number(index)];
if (asset && typeof window.openAssetDetailRecord === 'function') window.openAssetDetailRecord(asset);
}
let _selectedGraphFactKey = null;
let _selectedGraphEdgeId = null;
let _currentGraphData = null;
@@ -2759,6 +2805,8 @@ window.viewFactsForVulnerability = viewFactsForVulnerability;
window.openProjectConversation = openProjectConversation;
window.unbindConversationFromProject = unbindConversationFromProject;
window.loadProjectConversations = loadProjectConversations;
window.loadProjectAssets = loadProjectAssets;
window.openProjectAssetDetail = openProjectAssetDetail;
window.loadProjectFactGraph = loadProjectFactGraph;
window.filterProjectFactGraph = filterProjectFactGraph;
window.centerProjectFactGraph = centerProjectFactGraph;
+4
View File
@@ -113,6 +113,10 @@
scanFofaRow: 'fofa:execute',
batchScanSelectedFofaRows: 'fofa:execute',
exportFofaResults: 'fofa:execute',
importSelectedFofaAssets: 'asset:write',
importFofaRowAsset: 'asset:write',
saveAsset: 'asset:write',
deleteAsset: 'asset:delete',
// 任务队列
showBatchImportModal: 'tasks:write',
+1 -1
View File
@@ -47,7 +47,7 @@ const rbacScopeMeta = {
const rbacResourceLabels = {
user: '平台用户',
project: '项目', conversation: '对话', vulnerability: '漏洞', webshell: 'WebShell 连接',
batch_task: '批量任务', c2_listener: 'C2 监听器',
batch_task: '批量任务', c2_listener: 'C2 监听器', asset: '资产',
};
function rbacText(value, fallback = '') {
+17 -3
View File
@@ -81,7 +81,7 @@ function initRouter() {
const hashParts = hash.split('?');
let pageId = hashParts[0];
if (pageId === 'c2') pageId = 'c2-listeners';
if (pageId && ['dashboard', 'chat', 'hitl', 'info-collect', 'projects', 'vulnerabilities', 'webshell', 'chat-files', 'mcp-monitor', 'mcp-management', 'knowledge-management', 'knowledge-retrieval-logs', 'roles-management', 'platform-rbac', 'workflows', 'skills-monitor', 'skills-management', 'agents-management', 'settings', 'tasks', 'c2-listeners', 'c2-sessions', 'c2-tasks', 'c2-payloads', 'c2-events', 'c2-profiles'].includes(pageId)) {
if (pageId && ['dashboard', 'chat', 'hitl', 'asset-overview', 'asset-library', 'info-collect', 'projects', 'vulnerabilities', 'webshell', 'chat-files', 'mcp-monitor', 'mcp-management', 'knowledge-management', 'knowledge-retrieval-logs', 'roles-management', 'platform-rbac', 'workflows', 'skills-monitor', 'skills-management', 'agents-management', 'settings', 'tasks', 'c2-listeners', 'c2-sessions', 'c2-tasks', 'c2-payloads', 'c2-events', 'c2-profiles'].includes(pageId)) {
switchPage(pageId);
if (pageId === 'chat') {
scheduleChatConversationFromHash(500);
@@ -141,7 +141,15 @@ function updateNavState(pageId) {
});
// 设置活动状态
if (pageId === 'mcp-monitor' || pageId === 'mcp-management') {
if (pageId === 'asset-overview' || pageId === 'asset-library' || pageId === 'info-collect') {
const assetItem = document.querySelector('.nav-item[data-page="assets"]');
if (assetItem) {
assetItem.classList.add('active');
assetItem.classList.add('expanded');
}
const submenuItem = document.querySelector(`.nav-submenu-item[data-page="${pageId}"]`);
if (submenuItem) submenuItem.classList.add('active');
} else if (pageId === 'mcp-monitor' || pageId === 'mcp-management') {
// MCP子菜单项
const mcpItem = document.querySelector('.nav-item[data-page="mcp"]');
if (mcpItem) {
@@ -374,6 +382,12 @@ async function initPage(pageId) {
initInfoCollectPage();
}
break;
case 'asset-overview':
if (typeof loadAssetOverview === 'function') loadAssetOverview();
break;
case 'asset-library':
if (typeof loadAssets === 'function') loadAssets();
break;
case 'tasks':
// 初始化任务管理页面
if (typeof initTasksPage === 'function') {
@@ -549,7 +563,7 @@ document.addEventListener('DOMContentLoaded', function() {
let pageId = hashParts[0];
if (pageId === 'c2') pageId = 'c2-listeners';
if (pageId && ['dashboard', 'chat', 'hitl', 'info-collect', 'projects', 'tasks', 'workflows', 'vulnerabilities', 'webshell', 'chat-files', 'mcp-monitor', 'mcp-management', 'knowledge-management', 'knowledge-retrieval-logs', 'roles-management', 'platform-rbac', 'skills-monitor', 'skills-management', 'agents-management', 'settings', 'c2-listeners', 'c2-sessions', 'c2-tasks', 'c2-payloads', 'c2-events', 'c2-profiles'].includes(pageId)) {
if (pageId && ['dashboard', 'chat', 'hitl', 'asset-overview', 'asset-library', 'info-collect', 'projects', 'tasks', 'workflows', 'vulnerabilities', 'webshell', 'chat-files', 'mcp-monitor', 'mcp-management', 'knowledge-management', 'knowledge-retrieval-logs', 'roles-management', 'platform-rbac', 'skills-monitor', 'skills-management', 'agents-management', 'settings', 'c2-listeners', 'c2-sessions', 'c2-tasks', 'c2-payloads', 'c2-events', 'c2-profiles'].includes(pageId)) {
switchPage(pageId);
if (pageId === 'chat') {
scheduleChatConversationFromHash(200);
+285 -40
View File
@@ -23,7 +23,7 @@
}
})();
</script>
<link rel="stylesheet" href="/static/css/style.css">
<link rel="stylesheet" href="/static/css/style.css?v=20260716-9">
<link rel="stylesheet" href="/static/css/c2.css">
<link rel="stylesheet" href="/static/vendor/xterm.css">
<script src="/static/js/router.js"></script>
@@ -179,6 +179,9 @@
</svg>
</div>
<nav class="main-sidebar-nav">
<div class="nav-section-label" role="presentation">
<span data-i18n="navGroups.workbench">工作台</span>
</div>
<div class="nav-item" data-page="dashboard">
<div class="nav-item-content" data-title="仪表盘" onclick="switchPage('dashboard')" data-i18n="nav.dashboard" data-i18n-attr="data-title" data-i18n-skip-text="true">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
@@ -208,13 +211,40 @@
<span data-i18n="nav.hitl">人机协同</span>
</div>
</div>
<div class="nav-item" data-page="info-collect">
<div class="nav-item-content" data-title="信息收集" onclick="switchPage('info-collect')" data-i18n="nav.infoCollect" data-i18n-attr="data-title" data-i18n-skip-text="true">
<div class="nav-section-label" role="presentation">
<span data-i18n="navGroups.operations">安全作业</span>
</div>
<div class="nav-item" data-page="projects">
<div class="nav-item-content" data-title="项目管理" onclick="switchPage('projects')" data-i18n="nav.projects" data-i18n-attr="data-title" data-i18n-skip-text="true">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="11" cy="11" r="8"></circle>
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
<path d="M8 6h11"></path>
<path d="M5 12h10"></path>
<path d="M10 18h9"></path>
</svg>
<span data-i18n="nav.infoCollect">信息收集</span>
<span data-i18n="nav.projects">项目管理</span>
</div>
</div>
<div class="nav-item nav-item-has-submenu" data-page="assets" data-require-permission-any="asset:read fofa:execute">
<div class="nav-item-content" data-title="资产管理" onclick="window.toggleSubmenu('assets')" data-i18n="nav.assets" data-i18n-attr="data-title" data-i18n-skip-text="true">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M3 7l9-4 9 4-9 4-9-4z"></path><path d="M3 12l9 4 9-4"></path><path d="M3 17l9 4 9-4"></path>
</svg>
<span data-i18n="nav.assets">资产管理</span>
<svg class="submenu-arrow" width="16" height="16" viewBox="0 0 24 24" fill="none"><path d="m9 18 6-6-6-6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
</div>
<div class="nav-submenu">
<div class="nav-submenu-item" data-page="asset-overview" onclick="switchPage('asset-overview')" data-i18n="nav.assetOverview">资产概览</div>
<div class="nav-submenu-item" data-page="asset-library" onclick="switchPage('asset-library')" data-i18n="nav.assetLibrary">资产库</div>
<div class="nav-submenu-item" data-page="info-collect" onclick="switchPage('info-collect')" data-i18n="nav.infoCollect">信息收集</div>
</div>
</div>
<div class="nav-item" data-page="vulnerabilities">
<div class="nav-item-content" data-title="漏洞管理" onclick="switchPage('vulnerabilities')" data-i18n="nav.vulnerabilities" data-i18n-attr="data-title" data-i18n-skip-text="true">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path>
<path d="M9 12l2 2 4-4"></path>
</svg>
<span data-i18n="nav.vulnerabilities">漏洞管理</span>
</div>
</div>
<div class="nav-item" data-page="tasks">
@@ -236,33 +266,6 @@
<span data-i18n="nav.workflows">工作流</span>
</div>
</div>
<div class="nav-item" data-page="projects">
<div class="nav-item-content" data-title="项目管理" onclick="switchPage('projects')" data-i18n="nav.projects" data-i18n-attr="data-title" data-i18n-skip-text="true">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polygon points="12 2 2 7 12 12 22 7 12 2"></polygon>
<polyline points="2 17 12 22 22 17"></polyline>
<polyline points="2 12 12 17 22 12"></polyline>
</svg>
<span data-i18n="nav.projects">项目管理</span>
</div>
</div>
<div class="nav-item" data-page="vulnerabilities">
<div class="nav-item-content" data-title="漏洞管理" onclick="switchPage('vulnerabilities')" data-i18n="nav.vulnerabilities" data-i18n-attr="data-title" data-i18n-skip-text="true">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path>
<path d="M9 12l2 2 4-4"></path>
</svg>
<span data-i18n="nav.vulnerabilities">漏洞管理</span>
</div>
</div>
<div class="nav-item" data-page="chat-files">
<div class="nav-item-content" data-title="文件管理" onclick="switchPage('chat-files')" data-i18n="nav.chatFiles" data-i18n-attr="data-title" data-i18n-skip-text="true">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path>
</svg>
<span data-i18n="nav.chatFiles">文件管理</span>
</div>
</div>
<div class="nav-item" data-page="webshell">
<div class="nav-item-content" data-title="WebShell管理" onclick="switchPage('webshell')" data-i18n="nav.webshell" data-i18n-attr="data-title" data-i18n-skip-text="true">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
@@ -296,6 +299,17 @@
<div class="nav-submenu-item" data-page="c2-profiles" onclick="switchPage('c2-profiles')" data-i18n="nav.c2Profiles">流量伪装</div>
</div>
</div>
<div class="nav-item" data-page="chat-files">
<div class="nav-item-content" data-title="文件管理" onclick="switchPage('chat-files')" data-i18n="nav.chatFiles" data-i18n-attr="data-title" data-i18n-skip-text="true">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path>
</svg>
<span data-i18n="nav.chatFiles">文件管理</span>
</div>
</div>
<div class="nav-section-label" role="presentation">
<span data-i18n="navGroups.capabilities">能力中心</span>
</div>
<div class="nav-item nav-item-has-submenu" data-page="mcp">
<div class="nav-item-content" data-title="MCP" onclick="window.toggleSubmenu('mcp')" data-i18n="nav.mcp" data-i18n-attr="data-title" data-i18n-skip-text="true">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
@@ -336,7 +350,7 @@
</div>
</div>
<div class="nav-item nav-item-has-submenu" data-page="skills">
<div class="nav-item-content" data-title="Skills" onclick="window.toggleSubmenu('skills')" data-i18n="nav.skills" data-i18n-attr="data-title" data-i18n-skip-text="true">
<div class="nav-item-content" data-title="技能" onclick="window.toggleSubmenu('skills')" data-i18n="nav.skills" data-i18n-attr="data-title" data-i18n-skip-text="true">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"></path>
<polyline points="14 2 14 8 20 8"></polyline>
@@ -344,35 +358,35 @@
<line x1="16" y1="17" x2="8" y2="17"></line>
<polyline points="10 9 9 9 8 9"></polyline>
</svg>
<span data-i18n="nav.skills">Skills</span>
<span data-i18n="nav.skills">技能</span>
<svg class="submenu-arrow" width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9 18l6-6-6-6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</div>
<div class="nav-submenu">
<div class="nav-submenu-item" data-page="skills-monitor" onclick="switchPage('skills-monitor')">
<span data-i18n="nav.skillsMonitor">Skills状态监控</span>
<span data-i18n="nav.skillsMonitor">技能状态</span>
</div>
<div class="nav-submenu-item" data-page="skills-management" onclick="switchPage('skills-management')">
<span data-i18n="nav.skillsManagement">Skills管理</span>
<span data-i18n="nav.skillsManagement">技能管理</span>
</div>
</div>
</div>
<div class="nav-item nav-item-has-submenu" data-page="agents">
<div class="nav-item-content" data-title="Agents" onclick="window.toggleSubmenu('agents')" data-i18n="nav.agents" data-i18n-attr="data-title" data-i18n-skip-text="true">
<div class="nav-item-content" data-title="智能体" onclick="window.toggleSubmenu('agents')" data-i18n="nav.agents" data-i18n-attr="data-title" data-i18n-skip-text="true">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polygon points="12 2 2 7 12 12 22 7 12 2"></polygon>
<polyline points="2 17 12 22 22 17"></polyline>
<polyline points="2 12 12 17 22 12"></polyline>
</svg>
<span data-i18n="nav.agents">Agents</span>
<span data-i18n="nav.agents">智能体</span>
<svg class="submenu-arrow" width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9 18l6-6-6-6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</div>
<div class="nav-submenu">
<div class="nav-submenu-item" data-page="agents-management" onclick="switchPage('agents-management')">
<span data-i18n="nav.agentsManagement">Agent管理</span>
<span data-i18n="nav.agentsManagement">智能体管理</span>
</div>
</div>
</div>
@@ -395,6 +409,9 @@
</div>
</div>
</div>
<div class="nav-section-label" role="presentation">
<span data-i18n="navGroups.administration">平台管理</span>
</div>
<div class="nav-item" data-page="platform-rbac">
<div class="nav-item-content" data-title="平台权限" onclick="switchPage('platform-rbac')" data-i18n="nav.platformRbac" data-i18n-attr="data-title" data-i18n-skip-text="true">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
@@ -1674,6 +1691,211 @@
</div>
</div>
<!-- 资产概览页面 -->
<div id="page-asset-overview" class="page asset-page">
<div class="page-header">
<div><h2 data-i18n="assets.overviewTitle">资产概览</h2></div>
<div class="page-header-actions">
<button class="btn-secondary" type="button" onclick="loadAssetOverview()" data-i18n="common.refresh">刷新</button>
<button class="btn-primary" type="button" onclick="switchPage('asset-library')" data-i18n="assets.viewLibrary">查看资产库</button>
</div>
</div>
<div class="page-content asset-content">
<div class="asset-overview-summary">
<section class="asset-total-card">
<div class="asset-total-card__heading">
<div class="asset-overview-icon asset-overview-icon--blue" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="m12 2 8 4-8 4-8-4 8-4Z"/><path d="m4 10 8 4 8-4"/><path d="m4 14 8 4 8-4"/></svg></div>
<div><span class="asset-eyebrow" data-i18n="assets.totalAssets">资产总数</span><p data-i18n="assets.totalAssetsHint">当前纳入持续监测的资产</p></div>
</div>
<strong id="asset-stat-total" class="asset-total-value">0</strong>
<div class="asset-type-grid">
<div class="asset-type-item asset-type-item--ip"><span class="asset-type-dot"></span><span data-i18n="assets.ipCount">IP 数量</span><strong id="asset-stat-ips">0</strong></div>
<div class="asset-type-item asset-type-item--domain"><span class="asset-type-dot"></span><span data-i18n="assets.domainCount">域名数量</span><strong id="asset-stat-domains">0</strong></div>
<div class="asset-type-item asset-type-item--port"><span class="asset-type-dot"></span><span data-i18n="assets.portCount">端口数量</span><strong id="asset-stat-ports">0</strong></div>
</div>
</section>
<section class="asset-recent-card">
<div class="asset-recent-card__top">
<div class="asset-overview-icon asset-overview-icon--violet" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="5" width="18" height="16" rx="2"/><path d="M8 3v4M16 3v4M3 10h18M12 14v4M10 16h4"/></svg></div>
<span class="asset-recent-badge" data-i18n="assets.recentWindow">7 DAYS</span>
</div>
<div class="asset-recent-card__body"><span data-i18n="assets.recentCount">近 7 天发现</span><strong id="asset-stat-recent">0</strong></div>
<div class="asset-recent-progress"><i id="asset-recent-progress"></i></div>
<p id="asset-recent-rate" class="asset-recent-caption"></p>
</section>
</div>
<div class="asset-insight-heading">
<div><h3 data-i18n="assets.postureChanges">态势变化</h3><p data-i18n="assets.postureChangesHint">观察资产与风险在选定周期内的变化</p></div>
<div class="asset-period-switch" role="group" data-i18n="assets.periodSelection" data-i18n-attr="aria-label" aria-label="周期选择">
<button type="button" data-days="7" aria-pressed="false" onclick="setAssetOverviewPeriod(7)" data-i18n="assets.days7">7 天</button>
<button type="button" class="active" data-days="30" aria-pressed="true" onclick="setAssetOverviewPeriod(30)" data-i18n="assets.days30">30 天</button>
<button type="button" data-days="90" aria-pressed="false" onclick="setAssetOverviewPeriod(90)" data-i18n="assets.days90">90 天</button>
</div>
</div>
<div class="asset-trend-grid">
<section class="asset-panel asset-trend-panel">
<div class="asset-panel-header asset-trend-header"><div class="asset-trend-heading"><h3 data-i18n="assets.assetTrend">资产增减趋势</h3><p data-i18n="assets.assetTrendHint">新增发现与当前已停用资产</p></div><div id="asset-growth-summary" class="asset-chart-summary"></div><div class="asset-chart-legend"><span class="asset-legend-added" data-i18n="assets.addedAssets">新增资产</span><span class="asset-legend-inactive" data-i18n="assets.inactiveAssets">停用资产</span></div></div>
<div id="asset-growth-chart" class="asset-line-chart"><div class="muted" data-i18n="common.loading">加载中...</div></div>
</section>
<section class="asset-panel asset-trend-panel">
<div class="asset-panel-header asset-trend-header"><div class="asset-trend-heading"><h3 data-i18n="assets.riskTrend">风险发现趋势</h3><p data-i18n="assets.riskTrendHint">新增漏洞及其中的高危与严重风险</p></div><div id="asset-risk-summary" class="asset-chart-summary"></div><div class="asset-chart-legend"><span class="asset-legend-risk" data-i18n="assets.discoveredRisks">新增漏洞</span><span class="asset-legend-high" data-i18n="assets.highRisks">高危及严重</span></div></div>
<div id="asset-risk-chart" class="asset-line-chart"><div class="muted" data-i18n="common.loading">加载中...</div></div>
</section>
</div>
<section class="asset-panel asset-coverage-panel">
<div class="asset-panel-header"><div><h3 data-i18n="assets.scanCoverage">扫描覆盖</h3><p data-i18n="assets.scanCoverageHint">识别未扫描与长期未复查的资产缺口</p></div><span id="asset-coverage-status" class="asset-panel-meta"></span></div>
<div class="asset-coverage-layout">
<div class="asset-coverage-gauge" id="asset-coverage-gauge"><div><strong id="asset-coverage-rate">0%</strong><span data-i18n="assets.overallCoverage">总体覆盖率</span></div></div>
<div class="asset-coverage-metrics">
<div><span data-i18n="assets.scannedAssets">已扫描资产</span><strong id="asset-coverage-scanned">0</strong><small data-i18n="assets.scannedAssetsHint">至少完成过一次扫描</small></div>
<div><span data-i18n="assets.recentlyScanned">近 30 天覆盖</span><strong id="asset-coverage-recent">0</strong><small id="asset-coverage-recent-rate">0%</small></div>
<div class="asset-coverage-gap"><span data-i18n="assets.neverScanned">从未扫描</span><strong id="asset-coverage-never">0</strong><small data-i18n="assets.neverScannedHint">建议优先纳入扫描</small></div>
<div class="asset-coverage-gap"><span data-i18n="assets.staleScans">超过 30 天未扫描</span><strong id="asset-coverage-stale">0</strong><small data-i18n="assets.staleScansHint">扫描结果可能已过期</small></div>
</div>
</div>
</section>
<section class="asset-panel asset-protocol-panel">
<div class="asset-panel-header"><div><h3 data-i18n="assets.protocolDistribution">协议分布</h3><p data-i18n="assets.protocolDistributionHint">按已识别服务查看资产暴露构成</p></div><span class="asset-panel-meta" id="asset-protocol-meta"></span></div>
<div class="asset-protocol-layout">
<div id="asset-protocol-summary" class="asset-protocol-summary" aria-hidden="true">
<div class="asset-protocol-summary__label" data-i18n="assets.protocolComposition">协议构成</div>
<div class="asset-protocol-summary__main">
<div class="asset-protocol-donut"><div><strong>0%</strong><span data-i18n="assets.topProtocolShare">主要协议占比</span></div></div>
<div class="asset-protocol-lead"><span data-i18n="assets.topProtocol">主要协议</span><strong></strong><small></small></div>
</div>
<div id="asset-protocol-top-list" class="asset-protocol-top-list"></div>
</div>
<div class="asset-protocol-ranking">
<div class="asset-protocol-columns"><span data-i18n="assets.protocolRank">排名</span><span data-i18n="assets.protocolName">协议</span><span data-i18n="assets.assetRatio">资产占比</span><span data-i18n="assets.assetAmount">数量</span><span data-i18n="assets.ratio">占比</span></div>
<div id="asset-protocol-chart" class="asset-bar-chart"><div class="muted" data-i18n="common.noData">暂无数据</div></div>
</div>
</div>
</section>
</div>
</div>
<!-- 资产库页面 -->
<div id="page-asset-library" class="page asset-page">
<div class="page-header">
<div><h2 data-i18n="assets.libraryTitle">资产库</h2></div>
<div class="page-header-actions">
<button class="btn-secondary" type="button" onclick="loadAssets()" data-i18n="common.refresh">刷新</button>
<button class="btn-primary" data-require-permission="asset:write" type="button" onclick="openAssetEditor()" data-i18n="assets.addAsset">+ 新增资产</button>
</div>
</div>
<div class="page-content asset-content">
<div class="asset-toolbar">
<input id="asset-search" type="search" data-i18n="assets.searchPlaceholder" data-i18n-attr="placeholder" placeholder="搜索主机、IP、域名、标题、服务或标签" onkeydown="if(event.key==='Enter')loadAssets(1)">
<select id="asset-status-filter" onchange="loadAssets(1)"><option value="" data-i18n="assets.allStatuses">全部状态</option><option value="active" data-i18n="assets.statusActive">活跃</option><option value="inactive" data-i18n="assets.statusInactive">停用</option></select>
<select id="asset-project-filter" onchange="loadAssets(1)"><option value="" data-i18n="assets.allProjects">全部项目</option></select>
<button class="btn-secondary" type="button" onclick="loadAssets(1)" data-i18n="common.search">搜索</button>
<span id="asset-list-meta" class="muted"></span>
</div>
<div id="asset-batch-actions" class="asset-batch-actions" hidden>
<span id="asset-selected-count"></span>
<button class="btn-secondary" type="button" onclick="clearAssetSelection()" data-i18n="assets.clearSelection">取消选择</button>
<button class="btn-secondary" data-require-permission="asset:write" type="button" onclick="openAssetProjectModal()" data-i18n="assets.bindProject">绑定项目</button>
<button class="btn-secondary" type="button" onclick="openAssetScanModal('chat')" data-i18n="assets.sendToChat">发送到对话</button>
<button class="btn-primary" type="button" onclick="openAssetScanModal('task')" data-i18n="assets.createScanTask">创建扫描任务</button>
</div>
<div class="asset-list-card">
<div class="asset-table-wrap">
<table class="asset-table">
<thead><tr><th class="asset-check-cell"><input id="asset-select-page" type="checkbox" onchange="toggleAssetPageSelection(this.checked)" data-i18n="assets.selectPage" data-i18n-attr="aria-label" aria-label="选择本页"></th><th data-i18n="assets.target">目标</th><th data-i18n="assets.service">服务</th><th data-i18n="assets.project">所属项目</th><th data-i18n="assets.lastScan">上次扫描</th><th data-i18n="assets.relatedVulnerabilities">相关漏洞</th><th data-i18n="assets.riskLevel">风险等级</th><th data-i18n="assets.status">状态</th><th data-i18n="infoCollect.actions">操作</th></tr></thead>
<tbody id="asset-table-body"><tr><td colspan="9" class="muted" data-i18n="common.noData">暂无数据</td></tr></tbody>
</table>
</div>
<div id="asset-pagination" class="pagination-container pagination-fixed"></div>
</div>
</div>
</div>
<div id="asset-project-modal" class="modal asset-project-modal" style="display:none" role="dialog" aria-modal="true" aria-labelledby="asset-project-title" onclick="if(event.target===this)closeAssetProjectModal()">
<form class="modal-content asset-project-content" onsubmit="event.preventDefault();submitAssetProjectBinding()" onclick="event.stopPropagation()">
<div class="modal-header"><div><h2 id="asset-project-title" data-i18n="assets.bindProjectTitle">绑定项目</h2><p id="asset-project-subtitle" class="asset-page-subtitle"></p></div><button type="button" class="modal-close" onclick="closeAssetProjectModal()" data-i18n="common.close" data-i18n-attr="aria-label" aria-label="关闭"></button></div>
<div class="modal-body">
<label class="asset-editor-field" for="asset-batch-project"><span data-i18n="assets.project">所属项目</span><select id="asset-batch-project"><option value="" disabled hidden data-i18n="assets.chooseProject">请选择项目</option></select></label>
<p class="form-hint" data-i18n="assets.bindProjectHint">所选资产将统一绑定到该项目。</p>
</div>
<div class="modal-footer"><button type="button" class="btn-secondary" onclick="closeAssetProjectModal()" data-i18n="common.cancel">取消</button><button id="asset-project-submit" type="submit" class="btn-primary" data-require-permission="asset:write" data-i18n="assets.confirmBinding">确认绑定</button></div>
</form>
</div>
<div id="asset-scan-modal" class="modal asset-scan-modal" style="display:none" role="dialog" aria-modal="true" onclick="if(event.target===this)closeAssetScanModal()">
<div class="modal-content asset-scan-content" onclick="event.stopPropagation()">
<div class="modal-header"><div><h2 id="asset-scan-title" data-i18n="assets.scanTitle">扫描资产</h2><p id="asset-scan-subtitle" class="asset-page-subtitle"></p></div><span class="modal-close" onclick="closeAssetScanModal()">&times;</span></div>
<div class="modal-body">
<div id="asset-scan-targets" class="asset-scan-targets"></div>
<label class="asset-scan-prompt-label" for="asset-scan-prompt" data-i18n="assets.userPrompt">用户提示词</label>
<textarea id="asset-scan-prompt" class="asset-scan-prompt" rows="7"></textarea>
<p id="asset-scan-hint" class="form-hint" data-i18n="assets.promptHint">可使用 &#123;&#123;asset_id&#125;&#125;&#123;&#123;target&#125;&#125;&#123;&#123;host&#125;&#125;&#123;&#123;ip&#125;&#125;&#123;&#123;domain&#125;&#125;&#123;&#123;port&#125;&#125; 占位符;创建任务时会为每个资产生成一条任务。</p>
<label id="asset-scan-execute-wrap" class="checkbox-label"><input id="asset-scan-execute-now" type="checkbox" checked><span data-i18n="assets.executeNow">创建后立即执行</span></label>
</div>
<div class="modal-footer"><button class="btn-secondary" onclick="closeAssetScanModal()" data-i18n="common.cancel">取消</button><button id="asset-scan-submit" class="btn-primary" onclick="submitAssetScan()" data-i18n="assets.confirmSend">确认发送</button></div>
</div>
</div>
<div id="asset-editor-modal" class="modal asset-editor-modal" style="display:none" role="dialog" aria-modal="true" aria-labelledby="asset-editor-title" onclick="if(event.target===this)closeAssetEditor()">
<form id="asset-editor-form" class="modal-content asset-editor-content" novalidate onsubmit="event.preventDefault();saveAsset()" onclick="event.stopPropagation()">
<div class="modal-header"><div><h2 id="asset-editor-title" data-i18n="assets.addAssetTitle">新增资产</h2><p class="asset-editor-subtitle" data-i18n="assets.editorSubtitle">输入一个目标即可,其余信息可自动解析或稍后补充</p></div><button type="button" class="modal-close" onclick="closeAssetEditor()" data-i18n="common.close" data-i18n-attr="aria-label" aria-label="关闭"></button></div>
<div class="modal-body asset-editor-body">
<input type="hidden" id="asset-edit-id">
<div class="asset-editor-primary">
<label class="asset-editor-field asset-editor-target-field" for="asset-edit-target">
<span><span data-i18n="assets.assetAddress">资产地址</span><b aria-hidden="true">*</b></span>
<input id="asset-edit-target" type="text" autocomplete="off" spellcheck="false" placeholder="https://example.com:443、example.com 或 1.1.1.1:443" aria-describedby="asset-edit-target-hint asset-edit-target-error" aria-required="true">
<small id="asset-edit-target-hint" class="asset-field-hint" data-i18n="assets.assetAddressHint">仅需填写非空目标;可识别的 URL、域名、IP 和端口会自动解析</small>
<small id="asset-edit-target-error" class="asset-field-error" role="alert" hidden></small>
</label>
<div class="asset-editor-grid asset-editor-primary-grid">
<label class="asset-editor-field"><span data-i18n="assets.project">所属项目</span><select id="asset-edit-project"><option value="" data-i18n="assets.unboundProject">暂不绑定</option></select></label>
<label class="asset-editor-field"><span data-i18n="assets.tagsLabel">标签</span><div id="asset-tag-editor" class="asset-tag-editor"><div id="asset-tag-chips" class="asset-tag-chips"></div><input id="asset-edit-tags" type="text" autocomplete="off" data-i18n="assets.tagsPlaceholder" data-i18n-attr="placeholder" placeholder="输入后按回车或逗号"></div><small class="asset-field-hint" data-i18n="assets.tagsHint">用于环境、业务或责任范围分类</small></label>
</div>
</div>
<section id="asset-editor-details" class="asset-editor-details" aria-labelledby="asset-editor-details-title">
<div class="asset-editor-section-group">
<div class="asset-editor-group-heading"><h3 id="asset-editor-details-title" data-i18n="assets.connectionInfo">连接信息</h3><p data-i18n="assets.connectionInfoHint">由资产地址自动解析,可按需修正</p></div>
<div class="asset-editor-grid">
<label class="asset-editor-field asset-editor-wide"><span data-i18n="assets.hostUrl">完整 URL / Host</span><input id="asset-edit-host" type="text" spellcheck="false" placeholder="https://example.com:443"><small id="asset-edit-host-error" class="asset-field-error" role="alert" hidden></small></label>
<label class="asset-editor-field"><span>IP</span><input id="asset-edit-ip" type="text" spellcheck="false" placeholder="1.1.1.1"><small id="asset-edit-ip-error" class="asset-field-error" role="alert" hidden></small></label>
<label class="asset-editor-field"><span data-i18n="assets.domain">域名</span><input id="asset-edit-domain" type="text" spellcheck="false" placeholder="example.com"><small id="asset-edit-domain-error" class="asset-field-error" role="alert" hidden></small></label>
<label class="asset-editor-field"><span data-i18n="assets.port">端口</span><input id="asset-edit-port" type="number" inputmode="numeric" min="1" max="65535" placeholder="443"><small id="asset-edit-port-error" class="asset-field-error" role="alert" hidden></small></label>
<label class="asset-editor-field"><span data-i18n="assets.protocol">协议</span><input id="asset-edit-protocol" type="text" list="asset-protocol-options" spellcheck="false" placeholder="https"><datalist id="asset-protocol-options"><option value="http"><option value="https"><option value="ssh"><option value="ftp"><option value="smtp"><option value="rdp"><option value="mysql"><option value="postgresql"><option value="redis"><option value="mongodb"></datalist><small id="asset-edit-protocol-error" class="asset-field-error" role="alert" hidden></small></label>
</div>
</div>
<div class="asset-editor-section-group">
<div class="asset-editor-group-heading"><h3 data-i18n="assets.fingerprintInfo">识别信息</h3><p data-i18n="assets.fingerprintInfoHint">补充页面与服务指纹,便于检索和识别</p></div>
<div class="asset-editor-grid">
<label class="asset-editor-field"><span data-i18n="assets.pageTitle">页面标题</span><input id="asset-edit-title-value" type="text" maxlength="500"></label>
<label class="asset-editor-field"><span data-i18n="assets.serverProduct">服务 / 产品</span><input id="asset-edit-server" type="text" maxlength="255" placeholder="nginx"></label>
</div>
</div>
<div class="asset-editor-section-group">
<div class="asset-editor-group-heading"><h3 data-i18n="assets.locationAndStatus">地理与状态</h3><p data-i18n="assets.locationAndStatusHint">记录资产归属地区和当前启用状态</p></div>
<div class="asset-editor-grid">
<label class="asset-editor-field"><span data-i18n="assets.country">国家/地区</span><input id="asset-edit-country" type="text" maxlength="100" placeholder="CN"></label>
<label class="asset-editor-field"><span data-i18n="assets.province">省份/州</span><input id="asset-edit-province" type="text" maxlength="100" placeholder="Beijing"></label>
<label class="asset-editor-field"><span data-i18n="assets.city">城市</span><input id="asset-edit-city" type="text" maxlength="100" placeholder="Beijing"></label>
<label class="asset-editor-field"><span data-i18n="assets.status">状态</span><select id="asset-edit-status"><option value="active" data-i18n="assets.statusActive">活跃</option><option value="inactive" data-i18n="assets.statusInactive">停用</option></select></label>
</div>
</div>
</section>
<div id="asset-editor-form-error" class="asset-form-error" role="alert" hidden></div>
</div>
<div class="modal-footer"><button type="button" class="btn-secondary" onclick="closeAssetEditor()" data-i18n="common.cancel">取消</button><button id="asset-editor-submit" type="submit" class="btn-primary" data-require-permission="asset:write" data-i18n="assets.addAssetAction">添加资产</button></div>
</form>
</div>
<div id="asset-detail-modal" class="modal asset-detail-modal" style="display:none" role="dialog" aria-modal="true" onclick="if(event.target===this)closeAssetDetail()">
<div class="modal-content asset-detail-content" onclick="event.stopPropagation()">
<div class="modal-header"><div><h2 data-i18n="assets.detailTitle">资产详情</h2><p id="asset-detail-subtitle" class="asset-page-subtitle"></p></div><span class="modal-close" onclick="closeAssetDetail()">&times;</span></div>
<div class="modal-body">
<div id="asset-detail-grid" class="asset-detail-grid"></div>
</div>
<div class="modal-footer"><button class="btn-secondary" onclick="closeAssetDetail()" data-i18n="common.close">关闭</button><button id="asset-detail-edit" class="btn-primary" data-require-permission="asset:write" onclick="editAssetFromDetail()" data-i18n="common.edit">编辑</button></div>
</div>
</div>
<!-- 信息收集页面 -->
<div id="page-info-collect" class="page">
<div class="page-header">
@@ -1747,6 +1969,7 @@
<button class="btn-secondary btn-small" type="button" data-require-permission="fofa:execute" onclick="exportFofaResults('csv')" data-i18n="infoCollectPage.exportCsvTitle" data-i18n-attr="title" title="导出当前结果为 CSVUTF-8,兼容中文)"><span data-i18n="infoCollectPage.exportCsv">导出 CSV</span></button>
<button class="btn-secondary btn-small" type="button" data-require-permission="fofa:execute" onclick="exportFofaResults('json')" data-i18n="infoCollectPage.exportJsonTitle" data-i18n-attr="title" title="导出当前结果为 JSON"><span data-i18n="infoCollectPage.exportJson">导出 JSON</span></button>
<button class="btn-secondary btn-small" type="button" data-require-permission="fofa:execute" onclick="exportFofaResults('xlsx')" data-i18n="infoCollectPage.exportXlsxTitle" data-i18n-attr="title" title="导出当前结果为 Excel"><span data-i18n="infoCollectPage.exportXlsx">导出 XLSX</span></button>
<button class="btn-secondary btn-small" data-require-permission="asset:write" type="button" onclick="importSelectedFofaAssets()" data-i18n="assets.importSelected">入库所选</button>
<button class="btn-primary btn-small" data-require-permission="fofa:execute" type="button" onclick="batchScanSelectedFofaRows()" data-i18n="infoCollectPage.batchScanTitle" data-i18n-attr="title" title="将所选行创建为批量任务队列"><span data-i18n="infoCollectPage.batchScan">批量扫描</span></button>
</div>
</div>
@@ -1838,6 +2061,7 @@
<nav class="projects-tabs" role="tablist">
<button type="button" id="project-tab-facts" class="projects-tab is-active" role="tab" onclick="switchProjectTab('facts')" data-i18n="projects.tabFacts">事实黑板</button>
<button type="button" id="project-tab-graph" class="projects-tab" role="tab" onclick="switchProjectTab('graph')" data-i18n="projects.tabGraph">攻击路径</button>
<button type="button" id="project-tab-assets" class="projects-tab" role="tab" data-require-permission="asset:read" onclick="switchProjectTab('assets')" data-i18n="projects.tabAssets">资产库</button>
<button type="button" id="project-tab-conversations" class="projects-tab" role="tab" onclick="switchProjectTab('conversations')" data-i18n="projects.tabConversations">关联对话</button>
<button type="button" id="project-tab-vulns" class="projects-tab" role="tab" onclick="switchProjectTab('vulns')" data-i18n="projects.tabVulns">关联漏洞</button>
<button type="button" id="project-tab-settings" class="projects-tab" role="tab" onclick="switchProjectTab('settings')" data-i18n="projects.tabSettings">设置</button>
@@ -1988,6 +2212,23 @@
</div>
</div>
</div>
<div id="project-panel-assets" class="projects-panel" role="tabpanel" hidden>
<div class="projects-panel-toolbar projects-panel-toolbar--hint">
<p class="projects-fact-toolbar-hint" role="note">
<svg class="projects-fact-toolbar-hint-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" aria-hidden="true" xmlns="http://www.w3.org/2000/svg">
<circle cx="12" cy="12" r="9" stroke="currentColor" stroke-width="2"/><path d="M12 10v6M12 8h.01" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>
<span data-i18n="projects.boundAssetsHint">仅展示绑定到当前项目的资产;点击目标可查看完整详情</span>
</p>
<span id="project-assets-count" class="projects-panel-count">0</span>
</div>
<div class="projects-table-wrap">
<table class="data-table data-table--projects">
<thead><tr><th data-i18n="assets.target">目标</th><th data-i18n="assets.service">服务</th><th data-i18n="assets.title">标题/指纹</th><th data-i18n="assets.source">来源</th><th data-i18n="assets.lastSeen">最近发现</th><th data-i18n="assets.status">状态</th><th class="col-actions" data-i18n="common.actions">操作</th></tr></thead>
<tbody id="project-assets-tbody"></tbody>
</table>
</div>
</div>
<div id="project-panel-conversations" class="projects-panel" role="tabpanel" hidden>
<div class="projects-panel-toolbar projects-panel-toolbar--hint">
<p class="projects-fact-toolbar-hint" role="note">
@@ -4385,6 +4626,7 @@
<option value="project" data-i18n="rbac.resourceTypes.project">项目</option>
<option value="batch_task" data-i18n="rbac.resourceTypes.batch_task">批量任务</option>
<option value="vulnerability" data-i18n="rbac.resourceTypes.vulnerability">漏洞</option>
<option value="asset" data-i18n="rbac.resourceTypes.asset">资产</option>
<option value="webshell" data-i18n="rbac.resourceTypes.webshell">WebShell 连接</option>
<option value="c2_listener" data-i18n="rbac.resourceTypes.c2_listener">C2 监听器</option>
</select>
@@ -4413,6 +4655,7 @@
<option value="project" data-i18n="rbac.resourceTypes.project">项目</option>
<option value="batch_task" data-i18n="rbac.resourceTypes.batch_task">批量任务</option>
<option value="vulnerability" data-i18n="rbac.resourceTypes.vulnerability">漏洞</option>
<option value="asset" data-i18n="rbac.resourceTypes.asset">资产</option>
<option value="webshell" data-i18n="rbac.resourceTypes.webshell">WebShell 连接</option>
<option value="c2_listener" data-i18n="rbac.resourceTypes.c2_listener">C2 监听器</option>
</select>
@@ -4453,6 +4696,7 @@
<option value="project" data-i18n="rbac.resourceTypes.project">项目</option>
<option value="conversation" data-i18n="rbac.resourceTypes.conversation">对话</option>
<option value="vulnerability" data-i18n="rbac.resourceTypes.vulnerability">漏洞</option>
<option value="asset" data-i18n="rbac.resourceTypes.asset">资产</option>
<option value="webshell" data-i18n="rbac.resourceTypes.webshell">WebShell 连接</option>
<option value="batch_task" data-i18n="rbac.resourceTypes.batch_task">批量任务</option>
<option value="c2_listener" data-i18n="rbac.resourceTypes.c2_listener">C2 监听器</option>
@@ -6041,6 +6285,7 @@
<script src="/static/js/modal.js"></script>
<script src="/static/js/notifications.js"></script>
<script src="/static/js/info-collect.js"></script>
<script src="/static/js/assets.js?v=20260716-10"></script>
<script src="/static/js/agents.js"></script>
<script src="/static/js/dashboard.js"></script>
<script src="/static/js/chat-scroll.js"></script>