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 {
position: fixed;
top: 100px;
@@ -18,13 +15,12 @@ $color-regular: var(--el-text-color-regular);
cursor: pointer;
i {
color: $color-regular;
color: #575757;
font-size: 18px;
}
&.toolbar--unactivated {
cursor: default;
i {
opacity: .4;
}
@@ -32,145 +28,19 @@ $color-regular: var(--el-text-color-regular);
}
}
}
.entity-graph {
display: flex;
background-color:'orange';
border: 'solid 5px red';
.entity-graph__chart {
width: 100%;
.graph-node {
display: flex;
flex-direction: column;
align-items: center;
background-color: transparent !important;
transition: linear all var(--el-transition-duration-fast);
.graph-node__text {
width: 120px;
font-size: 12px;
color: $color-primary;
}
&.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: var(--el-color-success-light-5) !important;
box-shadow: 0 0 0 8px rgba(126, 159, 84, 0.14);
i {
color: var(--el-color-success);
}
}
&.graph-node--domain {
border-color: var(--el-color-primary-light-7) !important;
box-shadow: 0 0 0 8px rgba(56, 172, 210, 0.14);
i {
color: $color-business;
}
}
&.graph-node--app {
border-color: var(--el-color-warning-light-5) !important;
box-shadow: 0 0 0 8px rgba(229, 162, 25, 0.14);
i {
color: var(--el-color-warning);
}
}
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; // 该class并未使用到
box-shadow: none;
// 覆盖自带的node点击效果
&.rel-node-checked {
box-shadow: 0 0 0 5px rgba(151, 151, 151, 0.21);
animation: none;
border-color: $color-regular !important;
}
i {
font-size: 24px;
color: $color-regular;
}
.graph-node__text {
padding-top: 24px;
}
}
&.graph-node--entity {
width: 21px;
height: 21px;
justify-content: center;
border: none !important;
&.graph-node--ip {
i {
color: var(--el-color-success);
}
}
&.graph-node--domain {
i {
color: $color-business;
}
}
&.graph-node--app {
i {
color: var(--el-color-warning);
}
}
i {
font-size: 21px;
}
}
}
}
.entity-graph__right-box {
& > div {
left: unset !important;
right: 0 !important;
}
.entity-graph__detail {
height: calc(100% - 100px) !important;
top: 100px !important;
// border-left: 1px solid #E2E5EC;
overflow: auto;
padding: 20px;
}
}
.g6-component-tooltip {
box-shadow: -1px 1px 10px -1px rgba(205, 205, 205, 0.85);
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;
@@ -180,21 +50,19 @@ $color-regular: var(--el-text-color-regular);
align-items: center;
i {
color: $color-regular;
color: #717171;
font-size: 14px;
}
.tooltip__title {
padding-left: 6px;
font-size: 15px;
line-height: 15px;
color: $color-primary;
color: #111;
}
}
.tooltip__content {
padding-top: 10px;
color: $color-primary;
color: #222;
font-size: 12px;
span:first-of-type {
@@ -202,7 +70,6 @@ $color-regular: var(--el-text-color-regular);
}
}
}
.entity-node-tooltip {
width: 300px;
padding: 5px;
@@ -215,7 +82,7 @@ $color-regular: var(--el-text-color-regular);
.tooltip__title {
font-size: 15px;
line-height: 15px;
color: $color-primary;
color: #111;
}
}
@@ -230,12 +97,11 @@ $color-regular: var(--el-text-color-regular);
.header-icon {
width: 3px !important;
height: 12px !important;
background: $color-business;
background: #38ACD2;
border-radius: 1px;
margin-right: 6px;
}
}
.content-tag-list {
display: flex;
align-items: flex-start;
@@ -249,25 +115,247 @@ $color-regular: var(--el-text-color-regular);
border: 1px solid;
border-radius: 2px;
$normal-color: $color-regular;
$normal-light-color: var(--el-fill-color-light);
$negative-color: var(--el-color-danger);
$negative-light-color: var(--el-color-danger-light-9);
$positive-color: var(--el-color-success);
$positive-light-color: var(--el-color-success-light-9);
$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 {
display: flex;
flex-direction: column;
align-items: center;
background-color: transparent !important;
transition: linear all .2s;
.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;
box-shadow: 0 0 0 8px rgba(126,159,84,0.14);
i {
color: #7E9F54;
}
}
&.graph-node--domain {
border-color: #AFDEED !important;
box-shadow: 0 0 0 8px rgba(56,172,210,0.14);
i {
color: #38ACD2;
}
}
&.graph-node--app {
border-color: #F5DAA3 !important;
box-shadow: 0 0 0 8px rgba(229,162,25,0.14);
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 {
box-shadow: 0 0 0 5px rgba(151,151,151,0.21);
animation: none;
border-color: #778391 !important;
}
i {
font-size: 24px;
color: #778391;
}
.graph-node__text {
padding-top: 24px;
}
}
&.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;
}
}
}
}
.entity-graph__right-box {
& > div {
left: unset !important;
right: 0 !important;
}
.entity-graph__detail {
height: calc(100% - 100px) !important;
top: 100px !important;
// border-left: 1px solid #E2E5EC;
overflow: auto;
padding: 20px;
}
}
.g6-component-tooltip {
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;

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,171 +1,96 @@
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
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
}
this.label = generateLabel(type, id, data, sourceNode)
this.name = builtTooltip(this)
}
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()
}
}
return ''
}
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
}
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 }
}
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
}
// 获取唯一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
}
async queryRelatedEntitiesCount (entityType, entityName) {
const response = await axios.get(`${api.entity.entityGraph.relatedEntityCount}/${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
}
}
}
export const nodeType = {
rootNode: 'rootNode',
listNode: 'listNode',
entityNode: 'en-tityNode',
tempNode: 'tempNode'
}
export async function queryRelatedEntity (node, targetEntityType) {
// listNode和entityNode专用查询关联实体列表
async queryRelatedEntities (targetEntityType) {
let _targetEntityType = targetEntityType
if (node.myData.entityType === 'domain' && targetEntityType === 'domain') {
if (this.data.entityType === entityType.domain && targetEntityType === entityType.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 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)
throw response
}
}
}
export const nodeType = {
rootNode: 'rootNode',
listNode: 'listNode',
entityNode: 'entityNode',
tempNode: 'tempNode'
}

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) {