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

View File

@@ -1,6 +1,3 @@
$color-business: var(--el-color-business);
$color-primary: var(--el-text-color-primary);
$color-regular: var(--el-text-color-regular);
.graph-toolbar { .graph-toolbar {
position: fixed; position: fixed;
top: 100px; top: 100px;
@@ -18,13 +15,12 @@ $color-regular: var(--el-text-color-regular);
cursor: pointer; cursor: pointer;
i { i {
color: $color-regular; color: #575757;
font-size: 18px; font-size: 18px;
} }
&.toolbar--unactivated { &.toolbar--unactivated {
cursor: default; cursor: default;
i { i {
opacity: .4; opacity: .4;
} }
@@ -32,24 +28,137 @@ $color-regular: var(--el-text-color-regular);
} }
} }
} }
.entity-graph { .entity-graph {
display: flex; display: flex;
background-color:'orange';
border: 'solid 5px red';
.entity-graph__chart { .entity-graph__chart {
width: 100%; width: 100%;
height:100%;
overflow: hidden;
.force-graph-container .graph-tooltip {
padding: 8px 10px;
//border-radius: 10px;
background-color: white;
box-shadow: -1px 1px 10px -1px rgba(205,205,205,0.85);
.primary-node-tooltip {
padding: 5px;
.tooltip__header {
display: flex;
align-items: center;
i {
color: #717171;
font-size: 14px;
}
.tooltip__title {
padding-left: 6px;
font-size: 15px;
line-height: 15px;
color: #111;
}
}
.tooltip__content {
padding-top: 10px;
color: #222;
font-size: 12px;
span:first-of-type {
padding-right: 20px;
}
}
}
.entity-node-tooltip {
width: 300px;
padding: 5px;
.tooltip__header {
display: flex;
align-items: center;
margin-bottom: 20px;
.tooltip__title {
font-size: 15px;
line-height: 15px;
color: #111;
}
}
.tooltip__content {
display: flex;
flex-direction: column;
.content-header {
display: flex;
align-items: center;
.header-icon {
width: 3px !important;
height: 12px !important;
background: #38ACD2;
border-radius: 1px;
margin-right: 6px;
}
}
.content-tag-list {
display: flex;
align-items: flex-start;
flex-wrap: wrap;
.entity-tag {
margin: 10px 9px 10px 0;
padding: 0 8px;
height: 20px;
font-size: 12px;
border: 1px solid;
border-radius: 2px;
$normal-color: #778391;
$normal-light-color: #F7F8F9;
$negative-color: #E26154;
$negative-light-color: #FEF6F5;
$positive-color: #749F4D;
$positive-light-color: #F7FAF5;
&.entity-tag--level-two-normal {
border-color: $normal-color;
color: $normal-color;
background-color: $normal-light-color;
}
&.entity-tag--level-two-negative {
border-color: $negative-color;
color: $negative-color;
background-color: $negative-light-color;
}
&.entity-tag--level-two-positive {
border-color: $positive-color;
color: $positive-color;
background-color: $positive-light-color;
}
}
}
}
}
}
.force-graph-container {
//height:100% !important;
canvas {
//height:100% !important;
}
}
.graph-node { .graph-node {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
background-color: transparent !important; background-color: transparent !important;
transition: linear all var(--el-transition-duration-fast); transition: linear all .2s;
.graph-node__text { .graph-node__text {
width: 120px; width: 120px;
font-size: 12px; font-size: 12px;
color: $color-primary; color: #353636;
} }
&.graph-node--root { &.graph-node--root {
@@ -62,38 +171,30 @@ $color-regular: var(--el-text-color-regular);
box-shadow: none; box-shadow: none;
animation: none; animation: none;
} }
&.graph-node--ip { &.graph-node--ip {
border-color: var(--el-color-success-light-5) !important; border-color: #CBD9BB !important;
box-shadow: 0 0 0 8px rgba(126, 159, 84, 0.14); box-shadow: 0 0 0 8px rgba(126,159,84,0.14);
i { i {
color: var(--el-color-success); color: #7E9F54;
} }
} }
&.graph-node--domain { &.graph-node--domain {
border-color: var(--el-color-primary-light-7) !important; border-color: #AFDEED !important;
box-shadow: 0 0 0 8px rgba(56, 172, 210, 0.14); box-shadow: 0 0 0 8px rgba(56,172,210,0.14);
i { i {
color: $color-business; color: #38ACD2;
} }
} }
&.graph-node--app { &.graph-node--app {
border-color: var(--el-color-warning-light-5) !important; border-color: #F5DAA3 !important;
box-shadow: 0 0 0 8px rgba(229, 162, 25, 0.14); box-shadow: 0 0 0 8px rgba(229,162,25,0.14);
i { i {
color: var(--el-color-warning); color: #E5A219;
} }
} }
i { i {
font-size: 36px; font-size: 36px;
} }
.graph-node__text { .graph-node__text {
padding-top: 30px; padding-top: 30px;
} }
@@ -103,21 +204,19 @@ $color-regular: var(--el-text-color-regular);
padding-top: 20px; padding-top: 20px;
width: 66px; width: 66px;
height: 66px; height: 66px;
border: 1px solid #A7B0B9 !important; // 该class并未使用到 border: 1px solid #A7B0B9 !important;
box-shadow: none; box-shadow: none;
// 覆盖自带的node点击效果 // 覆盖自带的node点击效果
&.rel-node-checked { &.rel-node-checked {
box-shadow: 0 0 0 5px rgba(151, 151, 151, 0.21); box-shadow: 0 0 0 5px rgba(151,151,151,0.21);
animation: none; animation: none;
border-color: $color-regular !important; border-color: #778391 !important;
} }
i { i {
font-size: 24px; font-size: 24px;
color: $color-regular; color: #778391;
} }
.graph-node__text { .graph-node__text {
padding-top: 24px; padding-top: 24px;
} }
@@ -131,22 +230,19 @@ $color-regular: var(--el-text-color-regular);
&.graph-node--ip { &.graph-node--ip {
i { i {
color: var(--el-color-success); color: #7E9F54;
} }
} }
&.graph-node--domain { &.graph-node--domain {
i { i {
color: $color-business; color: #38ACD2;
} }
} }
&.graph-node--app { &.graph-node--app {
i { i {
color: var(--el-color-warning); color: #E5A219;
} }
} }
i { i {
font-size: 21px; font-size: 21px;
} }
@@ -159,7 +255,6 @@ $color-regular: var(--el-text-color-regular);
left: unset !important; left: unset !important;
right: 0 !important; right: 0 !important;
} }
.entity-graph__detail { .entity-graph__detail {
height: calc(100% - 100px) !important; height: calc(100% - 100px) !important;
top: 100px !important; top: 100px !important;
@@ -170,7 +265,7 @@ $color-regular: var(--el-text-color-regular);
} }
.g6-component-tooltip { .g6-component-tooltip {
box-shadow: -1px 1px 10px -1px rgba(205, 205, 205, 0.85); box-shadow: -1px 1px 10px -1px rgba(205,205,205,0.85);
.primary-node-tooltip { .primary-node-tooltip {
padding: 5px; padding: 5px;
@@ -180,21 +275,19 @@ $color-regular: var(--el-text-color-regular);
align-items: center; align-items: center;
i { i {
color: $color-regular; color: #717171;
font-size: 14px; font-size: 14px;
} }
.tooltip__title { .tooltip__title {
padding-left: 6px; padding-left: 6px;
font-size: 15px; font-size: 15px;
line-height: 15px; line-height: 15px;
color: $color-primary; color: #111;
} }
} }
.tooltip__content { .tooltip__content {
padding-top: 10px; padding-top: 10px;
color: $color-primary; color: #222;
font-size: 12px; font-size: 12px;
span:first-of-type { span:first-of-type {
@@ -202,7 +295,6 @@ $color-regular: var(--el-text-color-regular);
} }
} }
} }
.entity-node-tooltip { .entity-node-tooltip {
width: 300px; width: 300px;
padding: 5px; padding: 5px;
@@ -215,7 +307,7 @@ $color-regular: var(--el-text-color-regular);
.tooltip__title { .tooltip__title {
font-size: 15px; font-size: 15px;
line-height: 15px; line-height: 15px;
color: $color-primary; color: #111;
} }
} }
@@ -230,12 +322,11 @@ $color-regular: var(--el-text-color-regular);
.header-icon { .header-icon {
width: 3px !important; width: 3px !important;
height: 12px !important; height: 12px !important;
background: $color-business; background: #38ACD2;
border-radius: 1px; border-radius: 1px;
margin-right: 6px; margin-right: 6px;
} }
} }
.content-tag-list { .content-tag-list {
display: flex; display: flex;
align-items: flex-start; align-items: flex-start;
@@ -249,25 +340,22 @@ $color-regular: var(--el-text-color-regular);
border: 1px solid; border: 1px solid;
border-radius: 2px; border-radius: 2px;
$normal-color: $color-regular; $normal-color: #778391;
$normal-light-color: var(--el-fill-color-light); $normal-light-color: #F7F8F9;
$negative-color: var(--el-color-danger); $negative-color: #E26154;
$negative-light-color: var(--el-color-danger-light-9); $negative-light-color: #FEF6F5;
$positive-color: var(--el-color-success); $positive-color: #749F4D;
$positive-light-color: var(--el-color-success-light-9); $positive-light-color: #F7FAF5;
&.entity-tag--level-two-normal { &.entity-tag--level-two-normal {
border-color: $normal-color; border-color: $normal-color;
color: $normal-color; color: $normal-color;
background-color: $normal-light-color; background-color: $normal-light-color;
} }
&.entity-tag--level-two-negative { &.entity-tag--level-two-negative {
border-color: $negative-color; border-color: $negative-color;
color: $negative-color; color: $negative-color;
background-color: $negative-light-color; background-color: $negative-light-color;
} }
&.entity-tag--level-two-positive { &.entity-tag--level-two-positive {
border-color: $positive-color; border-color: $positive-color;
color: $positive-color; color: $positive-color;

File diff suppressed because it is too large Load Diff

View File

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