feat: 实体关系图优化

This commit is contained in:
hanyuxia
2024-06-13 10:25:52 +08:00
parent 72c33a4e40
commit d63f8a7827
5 changed files with 1146 additions and 742 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,21 @@
export default class Link {
constructor (sourceNode, targetNode, type) {
this.id = sourceNode.id + '__' + targetNode.id
export default class Edge {
constructor (sourceNode, targetNode, type = linkType.normal, distance, level = 1) {
this.source = sourceNode.id
this.target = targetNode.id
this.isTemp = type === 'temp'
this.type = type
this.distance = distance || 20// 连接中心节点的线的distance为60其他节点为20
this.strength = level === 1 ? 0.5 : (level === 2 ? 1 : 1)// level2设置为0.5-1
this.color = '#c8c8cc'// 未点击线所连接的节点时线的颜色
this.clickColor = '#7b7b99'// 点击此线所连接的节点后,线的颜色
// this.width = 1//未点击线所连接的节点时线的宽度
// this.arrowColor = '#c8c8cc'
// this.clickArrowColor = '#7b7b99'
// this.clickWidth = 1//点击节点后,此节点所连接的线的宽度
this.level = level// 1:source为中心节点的线是直线; 2:source为listNode节点的线是直线; 3:target为临时节点的线是虚线
}
}
export const linkType = {
normal: 'normal',
temp: 'temp'
}

View File

@@ -1,141 +1,85 @@
import _ from 'lodash'
import i18n from '@/i18n'
import axios from 'axios'
import { api } from '@/utils/api'
import { entityDefaultColor, intentColor } from '@/utils/constants'
import { entityDefaultColor, entityType } from '@/utils/constants'
import { formatTags } from '@/utils/tools'
import { generateLabel, builtTooltip, queryEntityBasicInfo, queryTags, queryRelatedEntityCount } from '@/views/entityExplorer/entityGraph/utils'
import { api } from '@/utils/api'
import axios from 'axios'
export default class Node {
/*
* type: 对应nodeType
* cfg: { entityType, entityName, x, y, fx, fy }
* cfg: { entityType, entityName }
* */
constructor (type, id, cfg, sourceNode) {
this.type = type
constructor (type, id, data, sourceNode, strength, img, level) {
this.id = id
this.x = _.get(cfg, 'x', null)
this.y = _.get(cfg, 'y', null)
this.fx = _.get(cfg, 'fx', null)
this.fy = _.get(cfg, 'fy', null)
this.myData = {
entityType: cfg.entityType,
entityName: cfg.entityName
}
if (sourceNode) {
this.sourceNode = sourceNode
}
this.label = this.generateLabel()
const img = new Image()
img.src = this.getIconUrl(cfg.entityType, type === nodeType.rootNode)
this.type = type
this.vx = 0
this.vy = 0
this.fx = level === 0 ? 0 : null// 设置为0即可固定中心节点。0为中心节点1为中心节点的子节点2为第三级节点
this.fy = level === 0 ? 0 : null// 设置为0即可固定中心节点。0为中心节点1为中心节点的子节点2为第三级节点
this.level = level
this.img = img
}
generateLabel () {
switch (this.type) {
case nodeType.rootNode:
case nodeType.entityNode: {
return this.id
}
case nodeType.listNode: {
return `${this.getLabelText()}(${_.get(this.sourceNode.myData, 'relatedEntities.' + this.myData.entityType + '.total', 0)})`
}
case nodeType.tempNode: {
return this.getLabelText()
}
this.strength = strength || -10
this.sourceNode = sourceNode
this.isSubdomain = sourceNode ? sourceNode.data.entityType === entityType.domain && data.entityType === entityType.domain : false
this.data = {
entityType: data.entityType,
entityName: data.entityName
// img:data.img
}
return ''
this.label = generateLabel(type, id, data, sourceNode)
this.name = builtTooltip(this)
}
getLabelText () {
if (this.myData.entityType === 'ip') {
if (this.sourceNode.myData.entityType === 'app') {
return i18n.global.t('entities.tab.relatedIp')
} else if (this.sourceNode.myData.entityType === 'domain') {
return i18n.global.t('entities.graph.resolveIp')
}
} else if (this.myData.entityType === 'domain') {
if (this.sourceNode.myData.entityType === 'ip') {
return i18n.global.t('entities.graph.resolvedDomain')
} else if (this.sourceNode.myData.entityType === 'app') {
return i18n.global.t('entities.graph.relatedDomain')
} else if (this.sourceNode.myData.entityType === 'domain') {
return i18n.global.t('entities.subdomain')
}
} else if (this.myData.entityType === 'app') {
return i18n.global.t('entities.tab.relatedApp')
}
return ''
}
getIconUrl (entityType, colored) {
const suffix = colored ? '-colored' : ''
return require(`@/assets/img/entity-symbol2/${entityType}${suffix}.svg`)
}
isSubdomain () {
if (this.sourceNode) {
return this.sourceNode.myData.entityType === 'domain' && this.myData.entityType === 'domain'
} else {
return false
}
}
// 查询basicInfo、tags、关联实体数量
// listNode和entityNode专用查询实体信息
async queryDetailData () {
const entityType = this.myData.entityType
const entityName = this.myData.entityName
const entityType = this.data.entityType
const entityName = this.data.entityName
this.myData.basicInfo = await this.queryEntityBasicInfo(entityType, entityName)
this.data.basicInfo = await queryEntityBasicInfo(entityType, entityName)
const tags = await this.queryTags(entityType, entityName)
const tags = await queryTags(entityType, entityName)
let _tags = []
formatTags(tags, entityType, _tags)
if (_.isArray(tags.tags)) {
_tags = _.concat(_tags, tags.tags.map(tag => ({ value: tag.name, color: intentColor[tag.intent] || entityDefaultColor })))
if (_.isArray(tags.userDefinedTags)) {
_tags = _.concat(_tags, tags.userDefinedTags.map(tag => ({ value: tag.tagValue, color: tag.knowledgeBase ? tag.knowledgeBase.color : entityDefaultColor })))
}
this.myData.tags = _tags
this.data.tags = _tags
const relatedEntityTotalCount = await this.queryRelatedEntitiesCount(entityType, entityName)
this.myData.relatedEntities = {
ip: { total: relatedEntityTotalCount.ipCount, loadedCount: 0 },
domain: { total: this.myData.entityType === 'domain' ? relatedEntityTotalCount.subDomainCount : relatedEntityTotalCount.domainCount, loadedCount: 0 },
app: { total: relatedEntityTotalCount.appCount, loadedCount: 0 }
const relatedEntityTotalCount = await queryRelatedEntityCount(entityType, entityName)
this.data.relatedEntities = {
ip: { total: relatedEntityTotalCount.ipCount, pageNo: 0, list: [] }, //
domain: { total: this.data.entityType === 'domain' ? relatedEntityTotalCount.subDomainCount : relatedEntityTotalCount.domainCount, pageNo: 0, list: [] }, // pageNo: 0,
app: { total: relatedEntityTotalCount.appCount, pageNo: 0, list: [] }// pageNo: 0,
}
}
async queryEntityBasicInfo (entityType, entityName) {
const response = await axios.get(`${api.entity.entityGraph.basicInfo}/${entityType}?resource=${entityName}`).catch(e => {
console.error(e)
throw e
})
if (response.data && response.status === 200) {
return response.data.data
} else {
console.error(response)
throw response
}
}
async queryTags (entityType, entityName) {
const response = await axios.get(`${api.entity.entityGraph.tags}/${entityType}?resource=${entityName}`).catch(e => {
console.error(e)
throw e
})
if (response.data && response.status === 200) {
return response.data.data
} else {
console.error(response)
throw response
}
}
async queryRelatedEntitiesCount (entityType, entityName) {
const response = await axios.get(`${api.entity.entityGraph.relatedEntityCount}/${entityType}?resource=${entityName}`).catch(e => {
getNeighbors (gData) {
const links = gData.links.filter(l => l.source.id === this.id || l.target.id === this.id)
const nodes = gData.nodes.filter(n => links.some(l => l.source.id === n.id || l.target.id === n.id))
return { links, nodes }
}
// 获取唯一sourcelistNode和tempNode专用因为entityNode的source可能有多个rootNode无source
getSourceNode (gData) {
const links = gData.links.filter(l => l.target.id === this.id)
const nodes = gData.nodes.filter(n => links.some(l => l.source.id === n.id))
return nodes.length > 0 ? nodes[0] : null
}
// listNode和entityNode专用查询关联实体列表
async queryRelatedEntities (targetEntityType) {
let _targetEntityType = targetEntityType
if (this.data.entityType === entityType.domain && targetEntityType === entityType.domain) {
_targetEntityType = 'subdomain'
}
const url = `${api.entity.entityGraph[`${this.data.entityType}Related${_.upperFirst(_targetEntityType)}`]}?resource=${this.data.entityName}&pageSize=10&pageNo=${this.data.relatedEntities[targetEntityType].pageNo + 1}`
const response = await axios.get(url).catch(e => {
console.error(e)
throw e
})
if (response.data && response.status === 200) {
this.data.relatedEntities[targetEntityType].pageNo += 1
return response.data.data
} else {
console.error(response)
@@ -143,29 +87,10 @@ export default class Node {
}
}
}
export const nodeType = {
rootNode: 'rootNode',
listNode: 'listNode',
entityNode: 'en-tityNode',
entityNode: 'entityNode',
tempNode: 'tempNode'
}
export async function queryRelatedEntity (node, targetEntityType) {
let _targetEntityType = targetEntityType
if (node.myData.entityType === 'domain' && targetEntityType === 'domain') {
_targetEntityType = 'subdomain'
}
let url = `${api.entity.entityGraph[`${node.myData.entityType}Related${_.upperFirst(_targetEntityType)}`]}?resource=${node.myData.entityName}&pageSize=10`
const current = node.myData.relatedEntities[targetEntityType].loadedCount
const pageNo = parseInt(current / 10) + 1
url += `&pageNo=${pageNo}`
const response = await axios.get(url).catch(e => {
console.error(e)
throw e
})
if (response.data && response.status === 200) {
return response.data.data
} else {
console.error(response)
throw response
}
}

View File

@@ -21,6 +21,78 @@ export function generateLabel (type, id, data, sourceNode) {
return ''
}
export function builtTooltip (node) {
if (node) {
if (node.type === nodeType.listNode) {
let iconClass = ''
let title = ''
let total = 0
let loadedCount = 0
switch (node.data.entityType) {
case 'ip': {
iconClass = 'cn-icon cn-icon-resolve-ip'
title = i18n.global.t('entities.graph.resolveIp')
total = _.get(node.sourceNode.data, 'relatedEntities.ip.total', 0)
loadedCount = _.get(node.sourceNode.data, 'relatedEntities.ip.list', []).length
break
}
case 'domain': {
iconClass = 'cn-icon cn-icon-subdomain'
title = node.isSubdomain ? i18n.global.t('entities.subdomain') : i18n.global.t('entity.graph.resolveDomain')
total = _.get(node.sourceNode.data, 'relatedEntities.domain.total', 0)
loadedCount = _.get(node.sourceNode.data, 'relatedEntities.domain.list', []).length
break
}
case 'app': {
iconClass = 'cn-icon cn-icon-app-name'
title = i18n.global.t('entities.tab.relatedApp')
total = _.get(node.sourceNode.data, 'relatedEntities.app.total', 0)
loadedCount = _.get(node.sourceNode.data, 'relatedEntities.app.list', []).length
break
}
}
return `<div class="primary-node-tooltip">
<div class="tooltip__header"><i class="${iconClass}"></i><span class="tooltip__title">${title}</span></div>
<div class="tooltip__content">
<span>${i18n.global.t('entity.graph.associatedCount')}:&nbsp;${total}</span>
<span>${i18n.global.t('entity.graph.expandedEntityCount')}:&nbsp;${loadedCount}</span>
</div>
</div>`
} else if (node.type === nodeType.entityNode || node.type === nodeType.rootNode) {
if (node.data && node.data.tags && node.data.tags.length > 0) {
return `<div class="entity-node-tooltip">
<div class="tooltip__header">
<span class="tooltip__title">${node.id}</span>
</div>
<div class="tooltip__content">
<div class="content-header">
<div class="header-icon"></div>
<span>${i18n.global.t('entity.graph.tags')}</span>
</div>
<div class="content-tag-list">${this.generateTagHTML(node.data.tags)}</div>
</div>
</div>`
} else {
return `<div style="padding: 0 6px; font-size: 15px; line-height: 15px; color: #111;">${node.id}</div>`
}
} else if (node.type === nodeType.tempNode) {
return `<div style="padding: 0 6px; font-size: 15px; line-height: 15px; color: #111;">${node.id}</div>`
// return node.label
}
}
}
function generateTagHTML (tags) {
let html = ''
if (tags) {
tags.forEach(t => {
html += `<div class="entity-tag entity-tag--level-two-${t.type}">
<span>${t.value}</span>
</div>`
})
}
return html
}
function getLabelText (data, sourceNode) {
if (data.entityType === entityType.ip) {
if (sourceNode.data.entityType === entityType.app) {