业务逻辑:根据左侧的攻击链路展示对应的场景溯源报告,切换链路展示对应的链路拓扑视图;视图中的节点与溯源以及所处的攻击阶段相关联,切换不同的攻击阶段,对应的告警溯源流会发生变化
界面展示效果

界面实现
<template>
<div class="attack-chain-page">
<!-- 左侧:攻击链列表 -->
<div class="left-sidebar">
<div class="sidebar-header">
<span class="sidebar-title">攻击链列表 ({
{ chainTotal }})</span>
<div class="filter-tools">
<el-icon class="icon-btn"><Search /></el-icon>
<el-popover
placement="bottom"
:width="320"
trigger="click"
v-model:visible="filterVisible"
@show="resetNewFilterForm"
>
<template #reference>
<el-button link type="primary" size="small">筛选</el-button>
</template>
<el-form
:model="filterForm"
label-position="top"
size="small"
ref="newFilterFormRef"
>
<el-form-item label="时间范围">
<el-date-picker
v-model="dateRange"
type="datetimerange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
style="width: 100%"
value-format="YYYY-MM-DD HH:mm:ss"
:teleported="false"
:shortcuts="timeShortcuts"
/>
</el-form-item>
<el-form-item label="页数选择">
<el-select
v-model="filterForm.pageSize"
placeholder="请选择"
style="width: 100%"
:teleported="false"
>
<el-option label="10条/页" :value="10" />
<el-option label="20条/页" :value="20" />
<el-option label="50条/页" :value="50" />
</el-select>
</el-form-item>
<el-form-item label="攻击方向">
<el-select
v-model="filterForm.direction"
placeholder="请选择"
style="width: 100%"
clearable
:teleported="false"
>
<el-option label="全部" value="" />
<el-option label="外网 → 内网" value="1" />
<el-option label="内网 → 外网" value="2" />
<el-option label="内网横向" value="3" />
</el-select>
</el-form-item>
</el-form>
<div class="popover-footer" style="float: right">
<el-button size="small" @click="resetForm">重置</el-button>
<el-button size="small" type="primary" @click="blurPopover"
>确定</el-button
>
</div>
</el-popover>
</div>
</div>
<div class="filter-tags" v-if="selectedFilters.length">
<el-tag
v-for="item in selectedFilters"
:key="item.id"
size="small"
closable
@close.stop="removeSelectedFilter(item.id)"
>
{
{ item.tagText || item.label }}
</el-tag>
</div>
<div class="filter-panel" v-if="showFilterPanel">
<!-- <el-button
type="primary"
class="new-filter-btn"
@click.stop="openNewFilterDialog"
>+ 新建过滤器</el-button
>
<div class="filter-section">
<div class="section-title">快捷预设</div>
<div
v-for="item in quickFilters"
:key="item.id"
class="filter-item"
:class="{ active: isFilterSelected(item.id) }"
@click="selectFilter(item)"
>
<span class="label">{
{ item.label }}</span>
<span class="extra" v-if="item.extra">{
{ item.extra }}</span>
<span class="dot" v-if="item.showDot"></span>
</div>
</div>
<div class="filter-section">
<div class="section-title">业务维度</div>
<div
v-for="item in businessFilters"
:key="item.id"
class="filter-item"
:class="{ active: isFilterSelected(item.id) }"
@click="selectFilter(item)"
>
<span class="label">{
{ item.label }}</span>
<span class="en" v-if="item.enText">{
{ item.enText }}</span>
</div>
</div>
<div class="filter-section">
<div class="section-title">我的过滤器</div>
<div v-if="!customFilters.length" class="empty-custom-filter">
暂无自定义过滤器
</div>
<div v-else>
<div
v-for="item in customFilters"
:key="item.id"
class="filter-item"
:class="{ active: isFilterSelected(item.id) }"
@click="selectFilter(item)"
>
<span class="label">{
{ item.label }}</span>
</div>
</div>
</div> -->
</div>
<div class="chain-list" @scroll="handleChainListScroll">
<div v-if="chainList.length">
<div
class="chain-card"
v-for="(item, idx) in chainList"
:key="idx"
@click="handleChainClick(item)"
:class="{ active: activeChainIndex === idx }"
>
<div class="card-header">
<span class="chain-time">{
{ item.createTime }}</span>
</div>
<div class="chain-name">{
{ item.chainName }}</div>
<div class="card-tags">
<el-tag
size="small"
:class="'tag-' + getAlertLevelClass(item.alertLevel)"
>{
{ item.alertLevel }}</el-tag
>
<span class="alert-count">告警: {
{ item.alertCount }}</span>
</div>
</div>
</div>
<div v-else><el-empty description="暂无数据" /></div>
</div>
</div>
<!-- 中间:拓扑分析区 -->
<div class="center-panel">
<!-- 顶部:场景描述与进度 -->
<div class="panel-header">
<div class="scene-info">
<div class="scene-title">
<span class="title-bar">|</span> 攻击场景溯源分析报告
</div>
<div class="scene-desc">
<span class="label">场景描述:</span>
{
{
currentChain && currentChain.attackDescription
? currentChain.attackDescription
: "_"
}}
</div>
<div class="scene-meta">
<span
>持续时间:<span class="chainValue">{
{
currentChain &&
currentChain.timeline &&
currentChain.timeline.durationSeconds
? formatDuration(currentChain.timeline.durationSeconds)
: "_"
}}</span></span
>
<span
>受影响资产:<span class="chainValue"
>{
{
currentChain && currentChain.assetList
? currentChain.assetList.length
: ""
}}台<span
style="margin: 0 2px"
v-if="
currentChain &&
currentChain.assetList &&
currentChain.assetList.length
"
>({
{
currentChain && currentChain.assetList
? currentChain.assetList.join(",")
: "_"
}})</span
></span
>
</span>
<span
>攻击阶段:<span class="chainValue"
>{
{
currentChain && currentChain.attackPhaseList
? currentChain.attackPhaseList.length
: "_"
}}个阶段</span
></span
>
<span
>危害等级:
<el-tag
size="small"
:class="'tag-' + getAlertLevelClass(currentChain.riskLevel)"
>{
{
currentChain && currentChain.riskLevel
? currentChain.riskLevel
: "_"
}}</el-tag
>
<!-- <span class="danger-text">{
{
currentChain && currentChain.riskLevel
? currentChain.riskLevel
: "_"
}}</span> -->
</span>
</div>
</div>
<div class="stage-progress">
<div class="stage-track">
<!-- 'is-disabled': !(
currentChain &&
currentChain.attackPhaseList &&
currentChain.attackPhaseList.includes(stage.value)
), -->
<!-- 'has-data':
currentChain &&
currentChain.attackPhaseList &&
currentChain.attackPhaseList.includes(stage.value), -->
<div
class="stage-item"
v-for="(stage, index) in stages"
:key="index"
:class="['has-data', currentStage === index ? 'is-selected' : '']"
@click="handleStageClick(index, stage.value)"
>
<div class="stage-content-box">
<div class="stage-dot"></div>
<div class="stage-name">{
{ stage.name }}</div>
</div>
<div class="stage-line" v-if="index < stages.length - 1"></div>
</div>
</div>
</div>
<div class="view-tabs">
<div
class="tab-item"
:class="{ active: currentTab === 'topology' }"
@click="currentTab = 'topology'"
>
<el-icon class="tab-icon"><Connection /></el-icon>
攻击链路拓扑视图
</div>
<div
class="tab-item"
:class="{ active: currentTab === 'mitre' }"
@click="currentTab = 'mitre'"
>
<el-icon class="tab-icon"><Grid /></el-icon>
MITRE ATT&CK 矩阵
</div>
</div>
</div>
<!-- 拓扑图区域 -->
<div class="panel-body" v-if="currentTab === 'topology'">
<div class="graph-area">
<div
class="graph-viewport"
ref="graphViewport"
@mousedown="startDrag"
@mousemove="onDrag"
@mouseup="endDrag"
@mouseleave="endDrag"
@wheel.prevent="onWheel"
>
<div class="graph-content" :style="graphContentStyle">
<svg
class="graph-svg"
:width="canvas.width"
:height="canvas.height"
>
<!-- 箭头区域 -->
<defs>
<!-- <marker
id="arrow"
markerWidth="10"
markerHeight="10"
refX="18"
refY="3"
orient="auto"
markerUnits="strokeWidth"
>
<path d="M0,0 L0,6 L9,3 z" fill="#c0c4cc" />
</marker> -->
<!-- <marker
id="arrow-blue"
markerWidth="10"
markerHeight="10"
refX="18"
refY="3"
orient="auto"
markerUnits="strokeWidth"
>
<path d="M0,0 L0,6 L9,3 z" fill="#409eff" />
</marker> -->
<!-- 红色箭头 (Critical) -->
<!-- <marker
id="arrow-critical"
markerWidth="10"
markerHeight="10"
refX="18"
refY="3"
orient="auto"
markerUnits="strokeWidth"
>
<path d="M0,0 L0,6 L9,3 z" fill="#f56c6c" />
</marker> -->
<!-- 橙色箭头 (High) -->
<!-- <marker
id="arrow-high"
markerWidth="10"
markerHeight="10"
refX="18"
refY="3"
orient="auto"
markerUnits="strokeWidth"
>
<path d="M0,0 L0,6 L9,3 z" fill="#e6a23c" />
</marker> -->
<!-- 动态流光效果的渐变定义 -->
<linearGradient
id="flowGradient"
x1="0%"
y1="0%"
x2="100%"
y2="0%"
>
<stop offset="0%" stop-color="transparent" />
<stop offset="50%" stop-color="#409eff" />
<stop offset="100%" stop-color="transparent" />
</linearGradient>
</defs>
<!-- 连线组 -->
<g v-for="e in edges" :key="e.id">
<!-- 基础线 (半透明同色系背景) -->
<path
:d="edgePath(e)"
fill="none"
:stroke="getLineColor(e.level)"
stroke-width="1"
stroke-dasharray="3,2"
stroke-opacity="0.4"
/>
<!-- 动态流动线 -->
<path
class="flow-line"
:d="edgePath(e)"
fill="none"
:stroke="getLineColor(e.level)"
stroke-width="2"
stroke-dasharray="5,3"
:marker-end="getMarkerId(e.level)"
/>
<!-- 连线上的标签/徽章 -->
<!-- <foreignObject
:x="getEdgeCenter(e).x - 60"
:y="getEdgeCenter(e).y - 15"
width="120"
height="30"
>
<div class="edge-label" :class="e.level">
{
{ e.label }}
</div>
</foreignObject> -->
</g>
</svg>
<!-- 节点组 -->
<div class="graph-nodes" v-if="nodes.length">
<div
v-for="n in nodes"
:key="n.id"
class="g-node"
:class="[
n.type,
{
danger: n.danger,
warning: n.warning,
'is-selected': selectedNodeId === n.id,
},
]"
:style="{
left: n.x + 'px',
top: n.y + 'px',
}"
@click.stop="handleNodeClick(n)"
>
<!-- 涟漪光环 -->
<div
class="ripple-ring"
v-if="n.danger || selectedNodeId === n.id"
></div>
<div class="node-icon-wrapper">
<img :src="https://blog.csdn.net/getSvgIcon(n.icon)" class="node-icon" />
</div>
<div class="node-info">
<div class="node-name">{
{ n.name }}</div>
<div class="node-ip" v-if="n.ip">{
{ n.ip }}</div>
</div>
<!-- 节点旁边的告警标签 -->
<div class="node-alerts" v-if="n.alerts && n.alerts.length">
<div
v-for="(alert, ai) in n.expanded
? n.alerts
: n.alerts.slice(0, 2)"
:key="ai"
class="alert-tag"
:class="getAlertLevelClass(alert.level)"
>
{
{ alert.text }}
</div>
<div
v-if="n.alerts.length > 2"
class="expand-btn"
@click.stop="n.expanded = !n.expanded"
>
{
{
n.expanded
? "收起"
: "展开 (" + (n.alerts.length - 2) + ")"
}}
</div>
</div>
</div>
</div>
</div>
</div>
<div class="graph-empty" v-if="!nodes.length">
<el-empty

678

被折叠的 条评论
为什么被折叠?



