feat: 实体关系图优化
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -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'
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
|
||||
// 获取唯一source,listNode和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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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')}: ${total}</span>
|
||||
<span>${i18n.global.t('entity.graph.expandedEntityCount')}: ${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) {
|
||||
|
||||
Reference in New Issue
Block a user