This repository has been archived on 2025-09-14. You can view files and clone it, but cannot push or open issues or pull requests.
Files
cyber-narrator-cn-ui/src/views/entityExplorer/EntityGraph.vue

678 lines
18 KiB
Vue
Raw Normal View History

2023-06-16 17:18:58 +08:00
<template>
<div class="entity-graph">
2023-06-29 14:40:50 +08:00
<div class="entity-graph__chart" id="entityGraph">
</div>
2023-07-02 22:38:59 +08:00
<div class="entity-graph__right-box">
<el-drawer
v-model="rightBox.show"
direction="rtl"
custom-class="entity-graph__detail"
:modal="false"
:size="360"
:with-header="false"
destroy-on-close>
<ip-list
v-if="rightBox.mode === 'ipList'"
:entity="entity"
@closeBlock="onCloseBlock"
@mouseenter="onMouseenter"
@expandDetail="onExpandDetail">
</ip-list>
<app-or-domain-list
v-if="rightBox.mode === 'appList' || rightBox.mode === 'domainList'"
:entity="entity"
@expandDetail="onExpandDetail"
@mouseenter="onMouseenter"
@closeBlock="onCloseBlock">
</app-or-domain-list>
<graph-detail
v-if="rightBox.mode === 'appDetail' || rightBox.mode === 'ipDetail' || rightBox.mode === 'domainDetail'"
:entity="entity"
@expand="onExpand"
@closeBlock="onCloseBlock">
</graph-detail>
</el-drawer>
2023-06-16 17:18:58 +08:00
</div>
</div>
</template>
<script>
import IpList from '@/views/entityExplorer/entityGraphDetail/IpList'
import GraphDetail from '@/views/entityExplorer/entityGraphDetail/GraphDetail'
import AppOrDomainList from '@/views/entityExplorer/entityGraphDetail/AppOrDomainList'
import { useRoute } from 'vue-router'
2023-06-29 14:40:50 +08:00
import { ref, shallowRef } from 'vue'
import _ from 'lodash'
import { api } from '@/utils/api'
import axios from 'axios'
2023-07-02 22:38:59 +08:00
import G6 from '@antv/g6'
import testData from './testData'
2023-06-16 17:18:58 +08:00
export default {
name: 'EntityRelationship',
components: {
IpList,
GraphDetail,
2023-07-02 22:38:59 +08:00
AppOrDomainList
2023-06-16 17:18:58 +08:00
},
data () {
return {
2023-07-02 22:38:59 +08:00
rightBox: {
show: false
},
2023-06-30 18:43:02 +08:00
chartOption: {
2023-07-02 22:38:59 +08:00
container: 'entityGraph',
layout: {
type: 'force',
preventOverlap: true,
linkDistance: (d) => {
if (!d.target.isRoot && d.target.data.level && d.target.data.level.length === 1) {
return 300
}
return 80
},
nodeStrength: (d) => {
if (!d.isRoot && d.data.level && d.data.level.length === 1) {
return -50
}
return -10
},
edgeStrength: (d) => {
if (!d.target.isRoot && d.target.data.level && d.target.data.level.length === 1) {
return 0.1
}
return 0.7
}
},
modes: {
default: ['drag-nodes', 'click-select', 'zoom-canvas']
}
2023-06-30 18:43:02 +08:00
}
}
},
methods: {
2023-06-29 14:40:50 +08:00
async init () {
2023-07-02 22:38:59 +08:00
const _this = this
const graph = new G6.Graph(this.chartOption)
/*const rootNode = this.generateRootNode()
2023-06-30 18:43:02 +08:00
const secondLevelNodes = await this.generateSecondLevelNodes(rootNode)
const secondHalfLevelNodes = await this.generateHalfLevelNodes(secondLevelNodes)
this.graphData.nodes = [rootNode, ...secondLevelNodes, ...secondHalfLevelNodes]
2023-07-02 22:38:59 +08:00
const rootEdges = this.generateEdges(rootNode)
const secondEdges = this.generateEdges(secondLevelNodes)
this.graphData.edges = [...rootEdges, ...secondEdges]
graph.data(this.graphData)*/
graph.data(testData)
graph.render()
2023-06-30 18:43:02 +08:00
2023-07-02 22:38:59 +08:00
graph.on('node:dragstart', function (e) {
graph.layout()
refreshDragedNodePosition(e)
})
graph.on('node:drag', function (e) {
refreshDragedNodePosition(e)
})
graph.on('node:dragend', function (e) {
e.item.get('model').fx = null
e.item.get('model').fy = null
})
graph.on('node:click', function (e) {
const node = e.item.get('model')
if (!_this.rightBox.show) {
_this.rightBox.show = true
}
if (node.data.level && node.data.level.length === 1) {
_this.rightBox.mode = `${node.data.type}List`
} else {
_this.rightBox.mode = `${node.data.type}Detail`
}
})
function refreshDragedNodePosition (e) {
const model = e.item.get('model')
model.fx = e.x
model.fy = e.y
}
},
generateIconUrl (entityType, colored, isRoot) {
let subfix = ''
if (entityType === 'domain' && isRoot) {
subfix = '-colored2'
} else {
subfix = colored ? '-colored' : ''
}
return `${window.location.protocol}//${window.location.host}/images/entity-symbol2/${entityType}${subfix}.svg`
2023-06-29 14:40:50 +08:00
},
generateRootNode () {
2023-07-02 22:38:59 +08:00
let borderColor = ''
let shadowColor = ''
let width = 0
let height = 0
2023-06-29 14:40:50 +08:00
switch (this.entity.entityType) {
case 'ip': {
2023-07-02 22:38:59 +08:00
borderColor = '#CBD9BB'
shadowColor = 'rgba(126,159,84,0.14)'
width = 36
height = 33
2023-06-29 14:40:50 +08:00
break
}
case 'domain': {
2023-07-02 22:38:59 +08:00
borderColor = '#AFDEED'
shadowColor = 'rgba(56,172,210,0.14)'
width = 36
height = 30
2023-06-29 14:40:50 +08:00
break
}
case 'app': {
2023-07-02 22:38:59 +08:00
borderColor = '#F5DAA3'
shadowColor = 'rgba(229,162,25,0.14)'
width = 30
height = 36
2023-06-29 14:40:50 +08:00
break
}
}
return {
2023-07-02 22:38:59 +08:00
isRoot: true,
type: 'circle',
2023-06-29 14:40:50 +08:00
id: this.entity.entityName,
2023-07-02 22:38:59 +08:00
label: this.entity.entityName,
size: 82,
x: 0,
y: 0,
icon: {
show: true,
img: this.generateIconUrl(this.entity.entityType, true, true),
width,
height
},
style: {
fill: 'white',
stroke: borderColor,
lineWidth: 5
},
labelCfg: {
position: 'bottom',
offset: 10,
style: {
fill: '#353636',
fontSize: 12
}
},
data: {
level: '1'
}
2023-06-29 14:40:50 +08:00
}
},
generatePrimaryNode (props) {
2023-07-02 22:38:59 +08:00
let width = 0
let height = 0
switch (props.data.type) {
case 'ip': {
width = 24
height = 22
break
}
case 'domain': {
width = 24
height = 24
break
}
case 'app': {
width = 20
height = 24
break
}
2023-06-29 14:40:50 +08:00
}
return {
2023-07-02 22:38:59 +08:00
...props,
type: 'circle',
size: 66,
icon: {
show: true,
img: this.generateIconUrl(props.data.type, false),
width,
height
},
style: {
fill: 'white',
stroke: '#A7B0B9',
lineWidth: 1
},
labelCfg: {
position: 'bottom',
offset: 10,
style: {
fill: '#353636',
fontSize: 12
}
}
2023-06-29 14:40:50 +08:00
}
},
2023-07-02 22:38:59 +08:00
generateEntityNode (level, type, data) {
let width = 0
let height = 0
switch (type) {
2023-06-30 18:43:02 +08:00
case 'ip': {
2023-07-02 22:38:59 +08:00
width = 20
height = 18
2023-06-30 18:43:02 +08:00
break
}
case 'domain': {
2023-07-02 22:38:59 +08:00
width = 20
height = 20
2023-06-30 18:43:02 +08:00
break
}
case 'app': {
2023-07-02 22:38:59 +08:00
width = 17
height = 20
2023-06-30 18:43:02 +08:00
break
}
}
return {
id: data.vertex,
2023-07-02 22:38:59 +08:00
type: 'circle',
size: 28,
icon: {
show: true,
img: this.generateIconUrl(type, true),
width,
height
},
style: {
fill: 'transparent',
stroke: 'transparent',
lineWidth: 0
},
labelCfg: {
position: 'bottom',
offset: 10,
style: {
fill: '#353636',
fontSize: 12
}
},
2023-06-30 18:43:02 +08:00
data: {
level,
2023-07-02 22:38:59 +08:00
type,
name: data.vertex,
data
2023-06-30 18:43:02 +08:00
}
}
},
async generateSecondLevelNodes (rootNode) {
2023-06-29 14:40:50 +08:00
/*
2023-06-30 18:43:02 +08:00
* 2级及以上的整数层节点先查数量大于0的才展示
2023-06-29 14:40:50 +08:00
* */
const relatedEntityCount = await this.queryRelatedEntityCount(this.entity.entityType, this.entity.entityName)
const nodes = []
2023-06-30 18:43:02 +08:00
if (relatedEntityCount) {
const ipNode = this.generatePrimaryNode({
id: 'ip-1',
2023-07-02 22:38:59 +08:00
label: this.$t('entities.graph.resolveIp'),
x: 0,
y: 300,
2023-06-30 18:43:02 +08:00
data: {
level: '2',
2023-07-02 22:38:59 +08:00
sourceName: this.entity.entityName,
sourceType: this.entity.entityType,
type: 'ip'
2023-06-29 14:40:50 +08:00
}
2023-06-30 18:43:02 +08:00
})
const domainNode = this.generatePrimaryNode({
id: 'domain-1',
2023-07-02 22:38:59 +08:00
label: this.$t('entity.graph.resolveDomain'),
x: 260,
y: -150,
2023-06-30 18:43:02 +08:00
data: {
level: '2',
2023-07-02 22:38:59 +08:00
sourceName: this.entity.entityName,
sourceType: this.entity.entityType,
type: 'domain'
2023-06-29 14:40:50 +08:00
}
2023-06-30 18:43:02 +08:00
})
const subdomainNode = this.generatePrimaryNode({
id: 'domain-1',
2023-07-02 22:38:59 +08:00
label: this.$t('entities.subdomain'),
x: 260,
y: -150,
2023-06-30 18:43:02 +08:00
data: {
level: '2',
2023-07-02 22:38:59 +08:00
sourceName: this.entity.entityName,
sourceType: this.entity.entityType,
type: 'domain'
2023-06-29 14:40:50 +08:00
}
2023-06-30 18:43:02 +08:00
})
const appNode = this.generatePrimaryNode({
id: 'app-1',
2023-07-02 22:38:59 +08:00
label: this.$t('entities.tab.relatedApp'),
x: -260,
y: -150,
2023-06-30 18:43:02 +08:00
data: {
level: '2',
2023-07-02 22:38:59 +08:00
sourceName: this.entity.entityName,
sourceType: this.entity.entityType,
type: 'app'
2023-06-29 14:40:50 +08:00
}
2023-06-30 18:43:02 +08:00
})
switch (this.entity.entityType) {
case 'ip': {
if (relatedEntityCount.domainCount) {
domainNode.data.count = relatedEntityCount.domainCount
2023-07-02 22:38:59 +08:00
domainNode.label += `(${relatedEntityCount.domainCount})`
2023-06-30 18:43:02 +08:00
nodes.push(domainNode)
}
if (relatedEntityCount.appCount) {
2023-07-02 22:38:59 +08:00
appNode.data.count = relatedEntityCount.appCount
appNode.label += `(${relatedEntityCount.appCount})`
2023-06-30 18:43:02 +08:00
nodes.push(appNode)
}
break
2023-06-29 14:40:50 +08:00
}
2023-06-30 18:43:02 +08:00
case 'domain': {
if (relatedEntityCount.ipCount) {
ipNode.data.count = relatedEntityCount.ipCount
2023-07-02 22:38:59 +08:00
ipNode.label += `(${relatedEntityCount.ipCount})`
2023-06-30 18:43:02 +08:00
nodes.push(ipNode)
}
if (relatedEntityCount.subDomainCount) {
subdomainNode.data.count = relatedEntityCount.subDomainCount
2023-07-02 22:38:59 +08:00
subdomainNode.label += `(${relatedEntityCount.subDomainCount})`
2023-06-30 18:43:02 +08:00
nodes.push(subdomainNode)
}
if (relatedEntityCount.appCount) {
appNode.data.count = relatedEntityCount.appCount
2023-07-02 22:38:59 +08:00
appNode.label += `(${relatedEntityCount.appCount})`
2023-06-30 18:43:02 +08:00
nodes.push(appNode)
}
break
2023-06-29 14:40:50 +08:00
}
2023-06-30 18:43:02 +08:00
case 'app': {
if (relatedEntityCount.ipCount) {
ipNode.data.count = relatedEntityCount.ipCount
2023-07-02 22:38:59 +08:00
ipNode.label += `(${relatedEntityCount.ipCount})`
2023-06-30 18:43:02 +08:00
nodes.push(ipNode)
}
if (relatedEntityCount.domainCount) {
domainNode.data.count = relatedEntityCount.domainCount
2023-07-02 22:38:59 +08:00
domainNode.label += `(${relatedEntityCount.domainCount})`
2023-06-30 18:43:02 +08:00
nodes.push(domainNode)
}
break
2023-06-29 14:40:50 +08:00
}
}
2023-06-30 18:43:02 +08:00
rootNode.data.childNodes = nodes
2023-06-29 14:40:50 +08:00
}
2023-07-02 22:38:59 +08:00
console.info(nodes)
2023-06-29 14:40:50 +08:00
return nodes
},
2023-06-30 18:43:02 +08:00
async generateHalfLevelNodes (nodes) {
const newNodes = []
for (const node of nodes) {
const newNodes2 = []
2023-07-02 22:38:59 +08:00
const data = await this.queryRelatedEntity(node.data.sourceType, node.data.sourceName, node.data.type)
2023-06-30 18:43:02 +08:00
if (data) {
// 生成节点
data.list.forEach(d => {
2023-07-02 22:38:59 +08:00
newNodes2.push(this.generateEntityNode(String(parseInt(node.data.level) + 0.5), node.data.type, d))
2023-06-30 18:43:02 +08:00
})
// 更新源节点的已拓展数和列表
this.handleLoaded(node, data, newNodes2)
newNodes.push(...newNodes2)
}
}
return newNodes
},
2023-06-29 14:40:50 +08:00
async queryRelatedEntityCount (entityType, entityName) {
const response = await axios.get(`${api.entity.entityGraph.relatedEntityCount}/${entityType}?resource=${entityName}`).catch(e => {
console.error(e)
this.showError = true
this.errorMsg = this.errorMsgHandler(e)
})
if (response.data && response.data.code === 200) {
return response.data.data
} else {
console.error(response)
this.showError = true
this.errorMsg = this.errorMsgHandler(response)
return null
}
},
2023-07-02 22:38:59 +08:00
async queryRelatedEntity (sourceType, sourceName, type) {
if (sourceType === type) {
2023-06-30 18:43:02 +08:00
// 若源type和关联type都是domain说明关联type是subdomain
2023-07-02 22:38:59 +08:00
type = 'subdomain'
2023-06-30 18:43:02 +08:00
}
2023-07-02 22:38:59 +08:00
const response = await axios.get(`${api.entity.entityGraph[`${sourceType}Related${_.upperFirst(type)}`]}?resource=${sourceName}`).catch(e => {
2023-06-30 18:43:02 +08:00
console.error(e)
this.showError = true
this.errorMsg = this.errorMsgHandler(e)
})
if (response.data && response.data.code === 200) {
return response.data.data
} else {
console.error(response)
this.showError = true
this.errorMsg = this.errorMsgHandler(response)
return null
}
},
/*
* 若只有一个参数则取参数的childNodes集合作为line终点
* 若有两个参数则以第一个参数作为line起点第二个参数作为终点
* */
2023-07-02 22:38:59 +08:00
generateEdges (source, target) {
const edges = []
2023-06-30 18:43:02 +08:00
if (!target) {
const sourceArr = []
if (!source.length) {
sourceArr.push(source)
} else {
sourceArr.push(...source)
}
sourceArr.forEach(s => {
if (s.data && s.data.childNodes) {
2023-07-02 22:38:59 +08:00
const edges2 = []
2023-06-30 18:43:02 +08:00
s.data.childNodes.forEach(c => {
2023-07-02 22:38:59 +08:00
edges2.push({
id: `${s.id}-${c.id}`,
source: s.id,
target: c.id,
style: {
stroke: '#BEBEBE',
endArrow: {
path: G6.Arrow.triangle(5, 5)
}
}
2023-06-30 18:43:02 +08:00
})
})
2023-07-02 22:38:59 +08:00
s.edges = edges2
edges.push(...edges2)
2023-06-30 18:43:02 +08:00
}
2023-06-29 14:40:50 +08:00
})
2023-06-30 18:43:02 +08:00
}
2023-07-02 22:38:59 +08:00
return edges
2023-06-29 14:40:50 +08:00
},
2023-06-30 18:43:02 +08:00
handleLoaded (node, data, childNodes) {
if (!node.data.loaded) {
node.data.loaded = []
}
node.data.loaded.push(...data.list)
if (!node.data.childNodes) {
node.data.childNodes = []
}
node.data.childNodes.push(...childNodes)
},
onCloseBlock () {
// todo 关闭右侧graph面板
2023-07-02 22:38:59 +08:00
this.rightBox.mode = ''
this.rightBox.show = false
},
onExpandDetail (mode) {
2023-07-02 22:38:59 +08:00
this.rightBox.mode = mode
},
onExpand (val) {
// todo 调用接口,拓展关系
},
onMouseenter (val) {
// todo 鼠标移动过graph列表名称时graph图的分支图形会变大一点
if (!val.isTrusted) {
// 鼠标移动反馈
}
2023-06-16 17:18:58 +08:00
}
2023-06-29 14:40:50 +08:00
},
async mounted () {
if (this.entity.entityType && this.entity.entityName) {
await this.init()
}
},
setup () {
const route = useRoute()
const { entityType, entityName } = route.query
const entity = {
entityType,
entityName
}
2023-07-02 22:38:59 +08:00
const rightBox = ref({
mode: 'ipList', // ipList, ipDetail, domainList, domainDetail, appList, appDetail
show: false
})
2023-06-29 14:40:50 +08:00
// echarts关系图的节点和连线
const graphData = ref({})
// 从url获取实体类型、名称若为空直接报错
const myChart = shallowRef(null)
return {
graphData,
entity,
myChart,
2023-07-02 22:38:59 +08:00
rightBox
2023-06-29 14:40:50 +08:00
}
2023-06-16 17:18:58 +08:00
}
}
</script>
<style lang="scss">
.entity-graph {
display: flex;
.entity-graph__chart {
2023-07-02 22:38:59 +08:00
width: 100%;
2023-06-29 14:40:50 +08:00
.graph-node {
display: flex;
flex-direction: column;
align-items: center;
background-color: transparent !important;
2023-06-30 18:43:02 +08:00
transition: linear all .2s;
2023-06-29 14:40:50 +08:00
.graph-node__text {
width: 120px;
font-size: 12px;
color: #353636;
}
&.graph-node--root {
padding-top: 18px;
width: 82px;
height: 82px;
border: 5px solid !important;
// 覆盖自带的node点击效果
&.rel-node-checked {
box-shadow: none;
animation: none;
}
&.graph-node--ip {
border-color: #CBD9BB !important;
2023-07-02 22:38:59 +08:00
box-shadow: 0 0 0 8px rgba(126,159,84,0.14);
2023-06-29 14:40:50 +08:00
i {
color: #7E9F54;
}
}
&.graph-node--domain {
border-color: #AFDEED !important;
2023-06-30 18:43:02 +08:00
box-shadow: 0 0 0 8px rgba(56,172,210,0.14);
2023-06-29 14:40:50 +08:00
i {
color: #38ACD2;
}
}
&.graph-node--app {
border-color: #F5DAA3 !important;
2023-07-02 22:38:59 +08:00
box-shadow: 0 0 0 8px rgba(229,162,25,0.14);
2023-06-29 14:40:50 +08:00
i {
color: #E5A219;
}
}
i {
font-size: 36px;
}
.graph-node__text {
padding-top: 30px;
}
}
&.graph-node--primary {
padding-top: 20px;
width: 66px;
height: 66px;
border: 1px solid #A7B0B9 !important;
box-shadow: none;
// 覆盖自带的node点击效果
&.rel-node-checked {
2023-06-30 18:43:02 +08:00
box-shadow: 0 0 0 5px rgba(151,151,151,0.21);
2023-06-29 14:40:50 +08:00
animation: none;
border-color: #778391 !important;
}
i {
font-size: 24px;
color: #778391;
}
.graph-node__text {
padding-top: 24px;
}
}
2023-06-30 18:43:02 +08:00
&.graph-node--entity {
width: 21px;
height: 21px;
justify-content: center;
border: none !important;
&.graph-node--ip {
i {
color: #7E9F54;
}
}
&.graph-node--domain {
i {
color: #38ACD2;
}
}
&.graph-node--app {
i {
color: #E5A219;
}
}
i {
font-size: 21px;
}
}
2023-06-29 14:40:50 +08:00
}
2023-06-16 17:18:58 +08:00
}
2023-07-02 22:38:59 +08:00
.entity-graph__right-box {
& > div {
left: unset !important;
right: 0 !important;
width: 360px;
}
.entity-graph__detail {
height: calc(100% - 100px) !important;
top: 100px !important;
border-left: 1px solid #E2E5EC;
overflow: auto;
padding: 20px;
}
2023-06-16 17:18:58 +08:00
}
}
</style>