CN-1087 fix: 关系图逻辑重构交互优化

This commit is contained in:
chenjinsong
2023-07-30 23:29:32 +08:00
parent 80195f3f37
commit fd6f1b563d
10 changed files with 2017 additions and 3 deletions

View File

@@ -15,6 +15,7 @@
@import 'views/entityExplorer/search/explorer-search';
@import 'views/entityExplorer/entity-filter';
@import 'views/entityExplorer/entity-detail';
@import 'views/entityExplorer/entity-graph';
@import 'views/entityExplorer/entityList/entity-list';
@import './views/entityExplorer/entityList/card';
@import './views/entityExplorer/entityList/row';
@@ -86,5 +87,5 @@
@import 'views/charts2/digitalCertificate';
@import 'views/charts2/entityDetailBasicInfo';
@import "views/charts2/graphRightListBlock";
@import "views/charts2/graphRightDetailBlock";
@import "views/entityExplorer/graphRightListBlock";
@import "views/entityExplorer/graphRightDetailBlock";

View File

@@ -0,0 +1,248 @@
.graph-toolbar {
position: fixed;
top: 100px;
left: 0;
.toolbar__tools {
display: flex;
padding: 0 10px;
li {
display: flex;
justify-content: center;
width: 32px;
margin-right: 4px;
cursor: pointer;
i {
color: #575757;
font-size: 18px;
}
}
}
}
.entity-graph {
display: flex;
.entity-graph__chart {
width: 100%;
.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;
background-color: $positive-light-color;
}
}
}
}
}
}
}

View File

@@ -29,7 +29,7 @@ const routes = [
},
{
path: '/entityGraph',
component: () => import('@/views/entityExplorer/EntityGraph')
component: () => import('@/views/entityExplorer/EntityGraph2')
},
{
path: '/detection',

View File

@@ -0,0 +1,924 @@
<template>
<div class="entity-graph">
<div class="entity-graph__chart" id="entityGraph">
</div>
<div class="entity-graph__right-box">
<el-drawer
v-model="rightBox.show"
direction="rtl"
custom-class="entity-graph__detail"
:close-on-click-modal="true"
:modal="false"
:size="400"
:with-header="false"
destroy-on-close>
<graph-entity-list
v-if="rightBox.mode === 'list'"
:node="rightBox.node"
:loading="rightBox.loading"
@expandList="expandList"
@closeBlock="onCloseBlock"
>
</graph-entity-list>
<graph-entity-detail
v-else-if="rightBox.mode === 'detail'"
:node="rightBox.node"
:loading="rightBox.loading"
@expandDetailList="expandDetailList"
>
</graph-entity-detail>
</el-drawer>
</div>
</div>
</template>
<script>
import GraphEntityList from '@/views/entityExplorer/entityGraph/GraphEntityList'
import GraphEntityDetail from '@/views/entityExplorer/entityGraph/GraphEntityDetail'
import { useRoute } from 'vue-router'
import { ref, shallowRef } from 'vue'
import _ from 'lodash'
import G6, { Algorithm } from '@antv/g6'
import Node, { nodeType, queryRelatedEntity } from './entityGraph/node'
import Edge from './entityGraph/edge'
export default {
name: 'EntityRelationship',
components: {
GraphEntityList,
GraphEntityDetail
},
data () {
return {
chartOption: {
container: 'entityGraph',
layout: {
type: 'force',
preventOverlap: true,
linkDistance: (d) => {
if (d.target.type === nodeType.rootNode || d.target.type === nodeType.listNode) {
return 200
}
return 80
},
nodeStrength: (d) => {
if (d.type === nodeType.rootNode || d.type === nodeType.listNode) {
return -100
}
return -10
},
edgeStrength: (d) => {
if (d.target.type === nodeType.rootNode || d.target.type === nodeType.listNode) {
return 0.1
}
return 0.7
}
},
modes: {
default: ['drag-canvas', 'drag-nodes', 'click-select', 'zoom-canvas']
}
}
}
},
methods: {
async init () {
this.registerElements() // 注册自定义node
const tooltip = this.buildTooltip() // tooltip组件
const toolbar = this.buildToolbar() // 工具栏组装件
this.chartOption.plugins = [tooltip] // 注册组件
this.graph = new G6.Graph(this.chartOption)
const initialData = await this.generateInitialData()// 初始化数据
this.graph.data(initialData)
this.graph.render()
const rootNode = this.graph.findById(this.entity.entityName)
this.bindEvents() // 绑定事件
this.graph.emit('node:click', { item: rootNode, target: rootNode.getKeyShape() }) // 手动触发rootNode的点击事件
},
registerElements () {
const _this = this
G6.registerNode(
'rootNode',
{
draw (cfg, group) {
group.addShape('circle', {
name: 'rootSelectedShadow',
attrs: {
r: 25, // selected时38.5
x: 0,
y: 0,
fill: getRootNodeStyle(cfg.myData.entityType).selectedShadowColor
}
})
group.addShape('circle', {
name: 'rootShadow',
attrs: {
r: 31.5,
x: 0,
y: 0,
fill: getRootNodeStyle(cfg.myData.entityType).shadowColor
}
})
const node = group.addShape('circle', {
name: 'rootInner',
attrs: {
r: 26,
x: 0,
y: 0,
fill: '#FFFFFF',
stroke: getRootNodeStyle(cfg.myData.entityType).borderColor,
lineWidth: 1
}
})
group.addShape('image', {
attrs: {
x: -getRootNodeStyle(cfg.myData.entityType).iconWidth / 2,
y: -getRootNodeStyle(cfg.myData.entityType).iconHeight / 2,
height: getRootNodeStyle(cfg.myData.entityType).iconHeight,
width: getRootNodeStyle(cfg.myData.entityType).iconWidth,
img: getIconUrl(cfg.myData.entityType, true, true)
},
name: 'rootIcon',
draggable: true
})
// 为label建一个box避免鼠标事件中断
group.addShape('rect', {
name: 'rootLabelBox',
attrs: {
width: 64,
height: 40,
x: -32,
y: 10,
fill: 'transparent'
}
})
group.addShape('text', {
attrs: {
text: cfg.label,
x: 0,
y: 42,
fontSize: 12,
textAlign: 'center',
textBaseline: 'middle',
fill: '#353636'
},
name: 'rootLabel'
})
return node
},
setState (name, value, node) {
const group = node.getContainer()
const model = node.getModel()
const selectedShadowShape = group.get('children')[0]
const shadowShape = group.get('children')[1]
if (name === 'hover') {
// 鼠标悬浮时若不处于selected状态才继续处理
if (!node.hasState('mySelected')) {
// hover时加深shadow的颜色
if (value) {
shadowShape.attr('fill', getRootNodeStyle(model.myData.entityType).hoveredShadowColor)
} else {
shadowShape.attr('fill', getRootNodeStyle(model.myData.entityType).shadowColor)
}
}
} else if (name === 'mySelected') {
// select事件取消自身hover状态取消其他node和edge的高亮状态高亮自身和自身相关的edge
node.setState('hover', false)
// 变更样式
if (value) {
// 取消其他高亮
const nodes = _this.graph.getNodes()
const edges = _this.graph.getEdges()
nodes.forEach(n => {
if (n.getModel().id !== node.getModel().id) {
n.setState('mySelected', false)
}
})
edges.forEach(n => {
n.setState('mySelected', false)
})
// 高亮自身
shadowShape.attr('fill', getRootNodeStyle(model.myData.entityType).hoveredShadowColor)
selectedShadowShape.attr('r', 38.5)
// 高亮自身相关的edge
const aboutEdges = node.getEdges()
aboutEdges.forEach(n => {
n.setState('mySelected', true)
})
} else {
shadowShape.attr('fill', getRootNodeStyle(model.myData.entityType).shadowColor)
selectedShadowShape.attr('r', 25)
}
}
}
},
'single-node'
)
G6.registerNode(
'listNode',
{
draw (cfg, group) {
group.addShape('circle', {
name: 'listShadow',
attrs: {
r: 20, // selected时26.5
x: 0,
y: 0,
fill: getListNodeStyle(cfg.myData.entityType).hoveredShadowColor
}
})
const node = group.addShape('circle', {
name: 'listInner',
attrs: {
r: 21,
x: 0,
y: 0,
fill: '#FFFFFF',
stroke: getListNodeStyle(cfg.myData.entityType).borderColor,
lineWidth: 1
}
})
group.addShape('image', {
attrs: {
x: -getListNodeStyle(cfg.myData.entityType).iconWidth / 2,
y: -getListNodeStyle(cfg.myData.entityType).iconHeight / 2,
height: getListNodeStyle(cfg.myData.entityType).iconHeight,
width: getListNodeStyle(cfg.myData.entityType).iconWidth,
img: getIconUrl(cfg.myData.entityType, false, false)
},
name: 'listIcon',
draggable: true
})
// 为label建一个box避免鼠标事件中断
group.addShape('rect', {
name: 'rootLabelBox',
attrs: {
width: 46,
height: 30,
x: -23,
y: 12,
fill: 'transparent'
}
})
group.addShape('text', {
attrs: {
text: cfg.label,
x: 0,
y: 34,
fontSize: 12,
textAlign: 'center',
textBaseline: 'middle',
fill: '#353636'
},
name: 'listLabel'
})
return node
},
setState (name, value, node) {
const group = node.getContainer()
const model = node.getModel()
const shadowShape = group.get('children')[0]
const innerShape = group.get('children')[1]
if (name === 'hover') {
// 鼠标悬浮时若不处于selected状态才继续处理
if (!node.hasState('mySelected')) {
// hover时出现shadow
shadowShape.attr('fill', getListNodeStyle(model.myData.entityType).hoveredShadowColor)
if (value) {
shadowShape.attr('r', 26.5)
} else {
shadowShape.attr('r', 20)
}
}
} else if (name === 'mySelected') {
// select事件取消自身hover状态取消其他node和edge的高亮状态高亮自身和自身相关的edge
node.setState('hover', false)
if (value) {
// 取消其他高亮
const nodes = _this.graph.getNodes()
const edges = _this.graph.getEdges()
nodes.forEach(n => {
if (n.getModel().id !== node.getModel().id) {
n.setState('mySelected', false)
}
})
edges.forEach(n => {
n.setState('mySelected', false)
})
// 高亮自身
shadowShape.attr('fill', getListNodeStyle(model.myData.entityType).selectedShadowColor)
shadowShape.attr('r', 26.5)
innerShape.attr('stroke', getListNodeStyle(model.myData.entityType).selectedBorderColor)
// 高亮自身相关的edge
const aboutEdges = node.getEdges()
aboutEdges.forEach(n => {
n.setState('mySelected', true)
})
} else {
shadowShape.attr('fill', getListNodeStyle(model.myData.entityType).hoveredShadowColor)
shadowShape.attr('r', 20)
innerShape.attr('stroke', getListNodeStyle(model.myData.entityType).borderColor)
}
}
}
},
'single-node'
)
G6.registerNode(
'entityNode',
{
draw (cfg, group) {
group.addShape('circle', {
name: 'entityShadow',
attrs: {
r: 18,
x: 0,
y: 0,
fill: 'transparent'
}
})
return group.addShape('image', {
attrs: {
x: -getEntityNodeStyle(cfg.myData.entityType).iconWidth / 2,
y: -getEntityNodeStyle(cfg.myData.entityType).iconHeight / 2,
height: getEntityNodeStyle(cfg.myData.entityType).iconHeight,
width: getEntityNodeStyle(cfg.myData.entityType).iconWidth,
img: getIconUrl(cfg.myData.entityType, true, false)
},
name: 'entityIcon'
})
},
setState (name, value, node) {
const group = node.getContainer()
const model = node.getModel()
const shadowShape = group.get('children')[0]
if (name === 'mySelected') {
// select事件取消自身hover状态取消其他node和edge的高亮状态高亮自身和自身相关的edge
node.setState('hover', false)
if (value) {
// 取消其他高亮
const nodes = _this.graph.getNodes()
const edges = _this.graph.getEdges()
nodes.forEach(n => {
if (n.getModel().id !== node.getModel().id) {
n.setState('mySelected', false)
}
})
edges.forEach(n => {
n.setState('mySelected', false)
})
// 高亮自身
shadowShape.attr('fill', getEntityNodeStyle(model.myData.entityType).selectedShadowColor)
// 高亮自身相关的edge
const aboutEdges = node.getEdges()
aboutEdges.forEach(n => {
n.setState('mySelected', true)
})
} else {
shadowShape.attr('fill', 'transparent')
}
}
}
},
'single-node'
)
G6.registerNode(
'tempNode',
{
draw (cfg, group) {
group.addShape('text', {
attrs: {
text: cfg.label,
x: 0,
y: 25,
fontSize: 12,
textAlign: 'center',
textBaseline: 'middle',
fill: '#353636',
opacity: 0.7
},
name: 'tempLabel'
})
return group.addShape('image', {
attrs: {
x: -getListNodeStyle(cfg.myData.entityType).iconWidth / 2,
y: -getListNodeStyle(cfg.myData.entityType).iconHeight / 2,
height: getListNodeStyle(cfg.myData.entityType).iconHeight,
width: getListNodeStyle(cfg.myData.entityType).iconWidth,
img: getIconUrl(cfg.myData.entityType, false, false),
opacity: 0.7
},
name: 'tempIcon'
})
},
setState (name, value, node) {
// 重写setState避免鼠标事件导致重新加载图片
}
},
'single-node'
)
function getRootNodeStyle (entityType) {
switch (entityType) {
case 'ip': {
return {
iconWidth: 26,
iconHeight: 23,
borderColor: 'rgba(126,159,84,1)',
shadowColor: 'rgba(126,159,84,0.21)',
hoveredShadowColor: 'rgba(126,159,84,0.36)',
selectedShadowColor: 'rgba(126,159,84,0.1)'
}
}
case 'domain': {
return {
iconWidth: 28,
iconHeight: 24,
borderColor: 'rgba(56,172,210,1)',
shadowColor: 'rgba(56,172,210,0.21)',
hoveredShadowColor: 'rgba(56,172,210,0.36)',
selectedShadowColor: 'rgba(56,172,210,0.1)'
}
}
case 'app': {
return {
iconWidth: 24,
iconHeight: 26,
borderColor: 'rgba(229,162,25,1)',
shadowColor: 'rgba(229,162,25,0.21)',
hoveredShadowColor: 'rgba(229,162,25,0.36)',
selectedShadowColor: 'rgba(229,162,25,0.1)'
}
}
}
return {
iconWidth: 26,
iconHeight: 23,
borderColor: 'rgba(126,159,84,1)',
shadowColor: 'rgba(126,159,84,0.21)',
hoveredShadowColor: 'rgba(126,159,84,0.36)',
selectedShadowColor: 'rgba(126,159,84,0.1)'
}
}
function getListNodeStyle (entityType) {
switch (entityType) {
case 'ip': {
return {
iconWidth: 24,
iconHeight: 21,
borderColor: 'rgba(119,131,145,0.6)',
selectedBorderColor: 'rgba(126,159,84,1)',
hoveredShadowColor: 'rgba(151,151,151,0.21)',
selectedShadowColor: 'rgba(126,159,84,0.21)'
}
}
case 'domain': {
return {
iconWidth: 24,
iconHeight: 24,
borderColor: 'rgba(119,131,145,0.6)',
selectedBorderColor: 'rgba(56,172,210,1)',
hoveredShadowColor: 'rgba(151,151,151,0.21)',
selectedShadowColor: 'rgba(56,172,210,0.21)'
}
}
case 'app': {
return {
iconWidth: 22,
iconHeight: 24,
borderColor: 'rgba(119,131,145,0.6)',
selectedBorderColor: 'rgba(229,162,25,1)',
hoveredShadowColor: 'rgba(151,151,151,0.21)',
selectedShadowColor: 'rgba(229,162,25,0.21)'
}
}
}
return {
iconWidth: 24,
iconHeight: 21,
borderColor: 'rgba(119,131,145,0.6)',
selectedBorderColor: 'rgba(126,159,84,1)',
hoveredShadowColor: 'rgba(151,151,151,0.21)',
selectedShadowColor: 'rgba(126,159,84,0.21)'
}
}
function getEntityNodeStyle (entityType) {
switch (entityType) {
case 'ip': {
return {
iconWidth: 22,
iconHeight: 20,
selectedShadowColor: 'rgba(126,159,84,0.1)'
}
}
case 'domain': {
return {
iconWidth: 22,
iconHeight: 22,
selectedShadowColor: 'rgba(56,172,210,0.1)'
}
}
case 'app': {
return {
iconWidth: 21,
iconHeight: 24,
selectedShadowColor: 'rgba(229,162,25,0.1)'
}
}
}
return {
iconWidth: 22,
iconHeight: 20,
borderColor: 'rgba(119,131,145,1)',
selectedBorderColor: 'rgba(126,159,84,1)',
hoveredShadowColor: 'rgba(151,151,151,0.21)',
selectedShadowColor: 'rgba(126,159,84,0.1)'
}
}
function getIconUrl (entityType, colored, isRoot) {
let suffix = ''
if (entityType === 'domain' && isRoot) {
suffix = '-colored2'
} else {
suffix = colored ? '-colored' : ''
}
return require(`@/assets/img/entity-symbol2/${entityType}${suffix}.svg`)
}
},
bindEvents () {
const _this = this
this.graph.on('node:dragstart', (e) => {
_this.graph.layout()
refreshDraggedNodePosition(e)
})
this.graph.on('node:drag', function (e) {
refreshDraggedNodePosition(e)
})
this.graph.on('node:dragend', function (e) {
e.item.get('model').fx = null
e.item.get('model').fy = null
})
this.graph.on('node:click', async function (e) {
const node = e.item
const nodeModel = node.get('model')
if (nodeModel.type !== 'tempNode') {
cleanTempNodesAndTempEdges()
// 点击entityNode查询数据并根据数据生成tempNode
if (nodeModel.type === nodeType.entityNode) {
_this.rightBox.mode = 'detail'
// 若已查过数据,不重复查询
if (!nodeModel.myData.relatedEntity) {
await nodeModel.queryDetailData()
}
let change = false
Object.keys(nodeModel.myData.relatedEntity).forEach(k => {
if (nodeModel.myData.relatedEntity[k].total) {
// 若已有同级同类型的listNode不生成此tempNode
const neighbors = _this.graph.getNeighbors(nodeModel.id, 'target')
const hasListNode = neighbors.some(b => b.get('model').myData.entityType === k)
if (!hasListNode) {
change = true
const tempNode = new Node(
nodeType.tempNode,
`${nodeModel.id}__${k}__temp`,
{
entityType: k,
// TODO k2-k1=1+k1k2
...generateTempNodeCoordinate(nodeModel.sourceNode, e)
},
nodeModel
)
const tempEdge = new Edge(nodeModel, tempNode, 'temp')
_this.graph.addItem('node', tempNode)
_this.graph.addItem('edge', tempEdge)
}
}
})
change && _this.graph.layout()
} else if (nodeModel.type === nodeType.listNode) {
_this.rightBox.mode = 'list'
} else if (nodeModel.type === nodeType.rootNode) {
_this.rightBox.mode = 'detail'
}
_this.rightBox.node = _.cloneDeep(nodeModel)
} else {
// 点击tempNode根据source生成listNode和entityNode以及对应的edge。查完entityNode的接口再删除临时node和edge。
// 若已达第六层则只生成listNode不再展开entityNode
const nodes = []
const edges = []
const listNode = new Node(
nodeType.listNode,
`${nodeModel.sourceNode.id}__${nodeModel.myData.entityType}-list`,
{
entityType: nodeModel.myData.entityType,
x: nodeModel.x,
y: nodeModel.y
},
nodeModel.sourceNode
)
nodes.push(listNode)
edges.push(new Edge(nodeModel.sourceNode, listNode))
// 判断listNode的sourceNode层级若大于等于10即第6层listNode则不继续拓展entity node并给用户提示。否则拓展entity node
const level = _this.getNodeLevel(listNode.sourceNode.id)
if (level < 10) {
const entities = await queryRelatedEntity(nodeModel.sourceNode, listNode.myData.entityType)
nodeModel.sourceNode.myData.relatedEntity[listNode.myData.entityType].list.push(...entities.list)
entities.list.forEach(entity => {
const entityNode = new Node(nodeType.entityNode, entity.vertex, {
entityType: listNode.myData.entityType,
entityName: entity.vertex,
x: e.x + Math.random() * 100 - 50,
y: e.y + Math.random() * 100 - 50
}, listNode)
nodes.push(entityNode)
edges.push(new Edge(listNode, entityNode))
})
} else {
// TODO 提示大于5层
}
nodes.forEach(n => {
_this.graph.addItem('node', n)
})
edges.forEach(edge => {
_this.graph.addItem('edge', edge)
})
cleanTempNodesAndTempEdges()
_this.graph.layout()
}
node.setState('mySelected', true)
})
this.graph.on('node:mouseenter', function (e) {
e.item.setState('hover', true)
})
this.graph.on('node:mouseleave', function (e) {
e.item.setState('hover', false)
})
function refreshDraggedNodePosition (e) {
const model = e.item.get('model')
model.fx = e.x
model.fy = e.y
}
function cleanTempNodesAndTempEdges () {
// 清除现有tempNode和tempEdge
const tempNodes = _this.graph.findAll('node', node => node.get('model').type === nodeType.tempNode)
tempNodes.forEach(n => {
_this.graph.removeItem(n, false)
})
const tempEdges = _this.graph.findAll('edge', edge => edge.get('model').isTemp)
tempEdges.forEach(n => {
_this.graph.removeItem(n, false)
})
}
function generateTempNodeCoordinate (sourceNode, event) {
const sx = sourceNode.x
const sy = sourceNode.y
const cx = event.x
const cy = event.y
return {
x: cx + cx - sx + Math.random() * 30 - 15,
y: cy + cy - sy + Math.random() * 30 - 15
}
}
},
async generateInitialData () {
const nodes = []
const edges = []
// 生成rootNode并查询相关数据
const rootNode = new Node(nodeType.rootNode, this.entity.entityName, {
entityType: this.entity.entityType,
entityName: this.entity.entityName
})
await rootNode.queryDetailData()
nodes.push(rootNode)
// 生成listNode和entityNode及edge
if (rootNode.myData.relatedEntity) {
// listNode
const listNodes = []
const keys = Object.keys(rootNode.myData.relatedEntity)
keys.forEach(k => {
if (rootNode.myData.relatedEntity[k].total) {
const listNode = new Node(nodeType.listNode, `${rootNode.id}__${k}-list`, { entityType: k }, rootNode)
listNodes.push(listNode)
edges.push(new Edge(rootNode, listNode))
}
})
// entityNode
const entityNodes = []
for (const node of listNodes) {
const entities = await queryRelatedEntity(rootNode, node.myData.entityType)
rootNode.myData.relatedEntity[node.myData.entityType].list = entities.list
entities.list.forEach(entity => {
const entityNode = new Node(
nodeType.entityNode,
entity.vertex,
{
entityType: node.myData.entityType,
entityName: entity.vertex
},
node
)
entityNodes.push(entityNode)
edges.push(new Edge(node, entityNode))
})
}
nodes.push(...listNodes, ...entityNodes)
}
return {
nodes: nodes,
edges: edges
}
},
getNodeLevel (id) {
const { findShortestPath } = Algorithm
const info = findShortestPath(this.graph.save(), this.entity.entityName, id)
return info.length
},
onCloseBlock () {
// todo 关闭右侧graph面板
this.rightBox.mode = ''
this.rightBox.show = false
},
buildTooltip () {
const _this = this
return new G6.Tooltip({
offsetX: 10,
offsetY: 20,
itemTypes: ['node'],
getContent (e) {
const node = e.item.getModel()
if (node.type === nodeType.listNode) {
let iconClass = ''
let title = ''
let total = 0
let loadedCount = 0
switch (node.myData.entityType) {
case 'ip': {
iconClass = 'cn-icon cn-icon-resolve-ip'
title = _this.$t('entities.graph.resolveIp')
total = _.get(node.sourceNode.myData, 'relatedEntity.ip.total', 0)
loadedCount = _.get(node.sourceNode.myData, 'relatedEntity.ip.list', []).length
break
}
case 'domain': {
iconClass = 'cn-icon cn-icon-subdomain'
title = node.isSubdomain() ? _this.$t('entities.subdomain') : _this.$t('entity.graph.resolveDomain')
total = _.get(node.sourceNode.myData, 'relatedEntity.domain.total', 0)
loadedCount = _.get(node.sourceNode.myData, 'relatedEntity.domain.list', []).length
break
}
case 'app': {
iconClass = 'cn-icon cn-icon-app-name'
title = _this.$t('entities.tab.relatedApp')
total = _.get(node.sourceNode.myData, 'relatedEntity.app.total', 0)
loadedCount = _.get(node.sourceNode.myData, 'relatedEntity.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>${_this.$t('entity.graph.associatedCount')}:&nbsp;${total}</span>
<span>${_this.$t('entity.graph.expandedEntityCount')}:&nbsp;${loadedCount}</span>
</div>
</div>`
} else if (node.type === nodeType.entityNode || node.type === nodeType.rootNode) {
if (node.myData && node.myData.tags && node.myData.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>${_this.$t('entity.graph.tags')}</span>
</div>
<div class="content-tag-list">${generateTagHTML(node.myData.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 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
}
}
})
},
buildToolbar () {
const tc = document.createElement('div')
tc.id = 'toolbarContainer'
tc.className = 'graph-toolbar'
document.body.appendChild(tc)
const toolbar = new G6.ToolBar({
container: tc,
className: 'toolbar__tools',
getContent: () => {
return `<ul>
<li code='zoomOut'><i class="cn-icon cn-icon-zoom-in"></i></li>
<li code='zoomIn'><i class="cn-icon cn-icon-zoom-out"></i></li>
<li code='undo'><i class="cn-icon cn-icon-next-step"></i></li>
<li code='redo'><i class="cn-icon cn-icon-pre-step"></i></li>
<li code='autoZoom'><i class="cn-icon cn-icon-auto-zoom"></i></li>
</ul>`
},
handleClick: (code, graph) => {
toolbar.handleDefaultOperator(code)
}
})
return toolbar
},
async expandList (nodeId) {
const node = this.graph.findById(nodeId)
const model = node.getModel()
const sourceNode = this.graph.findById(model.sourceNode.id)
const sourceModel = sourceNode.getModel()
const expandType = sourceModel.myData.entityType
if (sourceModel.myData.relatedEntity[expandType].list.length < 50) {
const entities = await queryRelatedEntity(sourceModel, expandType)
sourceModel.myData.relatedEntity[expandType].list.push(...entities.list)
entities.list.forEach(entity => {
const entityNodeModel = new Node(nodeType.entityNode, entity.vertex, {
entityType: expandType,
entityName: entity.vertex,
x: model.x + Math.random() * 500 - 250,
y: model.y + Math.random() * 500 - 250
}, model)
this.graph.addItem('node', entityNodeModel)
const edge = new Edge(model, entityNodeModel)
this.graph.addItem('edge', edge)
this.graph.layout()
this.graph.setItemState(this.graph.findById(edge.id), 'mySelected', true)
})
this.rightBox.node = _.cloneDeep(model)
} else {
// TODO 提示超过50
}
},
async expandDetailList (nodeId, expandType) {
const node = this.graph.findById(nodeId)
if (node) {
const nodeModel = node.getModel()
if (nodeModel.myData.relatedEntity[expandType].list.length < 50) {
const entities = await queryRelatedEntity(nodeModel, expandType)
nodeModel.myData.relatedEntity[expandType].list.push(...entities.list)
const neighbors = node.getNeighbors('target')
const listNode = neighbors.find(n => n.getModel().myData.entityType === expandType)
entities.list.forEach(entity => {
const entityNodeModel = new Node(nodeType.entityNode, entity.vertex, {
entityType: expandType,
entityName: entity.vertex,
x: listNode.getModel().x + Math.random() * 500 - 250,
y: listNode.getModel().y + Math.random() * 500 - 250
}, listNode.getModel())
this.graph.addItem('node', entityNodeModel)
this.graph.addItem('edge', new Edge(listNode.getModel(), entityNodeModel))
this.graph.layout()
})
this.rightBox.node = _.cloneDeep(nodeModel)
} else {
// TODO 提示超过50
}
}
}
},
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
}
// 关系图
const graph = shallowRef(null)
const rightBox = ref({
mode: 'detail', // list | detail
show: true,
node: null,
loading: false
})
return {
entity,
rightBox,
graph
}
}
}
</script>

View File

@@ -0,0 +1,418 @@
<template>
<!--title-->
<div class="graph-detail-basic-info">
<div style="display: flex">
<div class="graph-detail__icon"><i :class="iconClass"></i></div>
<div class="graph-detail-header">
<div class="entity-graph-type">{{ $_.get(node, 'myData.entityType') ? entityType[$_.get(node, 'myData.entityType')] : '-' }}</div>
<div class="graph-basic-info">
<div class="graph-basic-info-name__block">
<div class="graph-basic-info-name" :title="$_.get(node, 'id', '')" id="entityName">{{ $_.get(node, 'id', '') }}</div>
<div class="graph-basic-info-icon" @click="copyEntityName">
<i class="cn-icon cn-icon-copy"></i>
</div>
</div>
</div>
</div>
</div>
<i class="cn-icon cn-icon-close graph-close" @click="closeBlock"></i>
</div>
<!--basic info-->
<div class="digital-certificate graph-basic-info__block">
<div class="digital-certificate-header padding-b-10">
<div class="digital-certificate-header__icon graph-header__icon"></div>
<div class="graph-basic-info__block-title">
{{ $t('overall.basicInfo') }}
</div>
</div>
<div class="graph-basic-info__block-content">
<div class="graph-content-item" v-for="detail in detailCards" :key="detail.name">
<div class="graph-content-item-label">{{ detail.label }}:</div>
<div class="graph-content-item-value">{{ detail.value || '-' }}</div>
</div>
</div>
</div>
<!--关系拓展-->
<div class="digital-certificate graph-basic-info__block">
<loading :loading="loading" size="small"></loading>
<div class="digital-certificate-header padding-b-10">
<div class="digital-certificate-header__icon graph-header__icon"></div>
<div class="graph-basic-info__block-title">
{{ $t('entity.graph.relationshipExpansion') }}
</div>
</div>
<div class="graph-basic-info__block-content" style="margin-top: -4px;">
<div v-for="item in relationList" :key="item.name">
<div class="graph-content-item graph-content-relationship-item" v-if="item.value === item.total">
<div class="graph-relationship-item-label">
<i class="margin-r-6" :class="item.icon"></i>
<span>{{ item.label }}</span>
</div>
<div class="graph-relationship-item-value">
<span class="margin-r-6">{{ item.value }}/{{ item.total }}</span>
<i class="cn-icon cn-icon-expand-relationship" :style="{color: iconColor(item.value, item.total)}"></i>
</div>
</div>
<div v-else class="graph-content-item graph-content-relationship-item">
<div class="graph-relationship-item-label">
<i class="graph-relationship-item-label-icon margin-r-6" :class="item.icon"></i>
<span>{{ item.label }}</span>
</div>
<div class="graph-relationship-item-value">
<span class="margin-r-6">{{ item.value }}/{{ item.total }}</span>
<i class="cn-icon cn-icon-expand-relationship graph-expand-relationship__icon" :style="{color: iconColor(item.value, item.total)}" @click="expandRelation(item.name)"></i>
</div>
</div>
</div>
</div>
</div>
<!--标签-->
<div v-if="$_.get(node, 'myData.tags', []).length > 0" class="digital-certificate graph-basic-info__block">
<div class="digital-certificate-header padding-b-10">
<div class="digital-certificate-header__icon graph-header__icon"></div>
<div class="graph-basic-info__block-title">
{{ $t('entity.graph.tags') }}
</div>
</div>
<div class="entity-detail graph-basic-info__block-content">
<div class="graph-tag-list">
<div v-for="ic in $_.get(node, 'myData.tags', [])" :key="ic.value">
<div class="entity-tag graph-tag-item" :class="`entity-tag--level-two-${ic.type}`">
<span>{{ic.value}}</span>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import { copySelectionText, selectElementText } from '@/utils/tools'
import { entityType, riskLevelMapping } from '@/utils/constants'
import chartMixin from '@/views/charts2/chart-mixin'
import { dateFormatByAppearance } from '@/utils/date-util'
import { ref } from 'vue'
import _ from 'lodash'
import Loading from '@/components/common/Loading'
export default {
name: 'GraphEntityDetail',
props: {
node: {
type: Object
},
loading: {
type: Boolean
}
},
mixins: [chartMixin],
components: {
Loading
},
data () {
return {
entityType
}
},
computed: {
appRisk () {
return function (level) {
const m = riskLevelMapping.find(mapping => {
return mapping.value == level
})
return (m && m.name) || level
}
},
iconClass () {
let className
switch (_.get(this.node, 'myData.entityType', '')) {
case ('ip'): {
className = 'cn-icon cn-icon-ip2'
break
}
case ('domain'): {
className = 'cn-icon cn-icon-domain2'
break
}
case ('app'): {
className = 'cn-icon cn-icon-app2'
break
}
default:
break
}
return className
},
handleDate () {
return function (key) {
const date = _.get(this.node, key, '')
return date ? dateFormatByAppearance(date) : '-'
}
}
},
watch: {
node: {
deep: true,
handler (n) {
this.handleDetailData(n)
}
}
},
mounted () {
this.handleDetailData(this.node)
},
setup (props) {
const detailCards = ref([])
const relationList = ref([])
const tagList = ref([])
return {
detailCards,
relationList,
tagList
}
},
methods: {
/** 复制实体名称 */
copyEntityName () {
selectElementText(document.getElementById('entityName'))
if (copySelectionText()) {
this.$message.success(this.$t('tip.copySuccess'))
} else {
this.$message.error('Unknown error')
}
},
/** 修改关系拓展图标颜色,全部拓展浅灰色,否则深灰色 */
iconColor (length, total) {
if (length < total) {
if (length === 50) {
return 'rgba(57, 57, 57, 0.5)'
} else {
return 'rgba(57, 57, 57, 1)'
}
} else {
return 'rgba(57, 57, 57, 0.5)'
}
},
// 关闭右侧详情栏
closeBlock () {
this.$emit('closeBlock')
},
/** 构造地址,国-省市-市 */
handleLocation (data) {
const location = []
if (data.country) {
location.push(data.country)
}
if (data.province) {
location.push(data.province)
}
if (data.city) {
location.push(data.city)
}
return location.join(' - ')
},
/** 关系拓展 */
expandRelation (name) {
this.$emit('expandDetailList', this.node.id, name)
},
httpError (e) {
this.isNoData = false
this.showError = true
this.errorMsg = this.errorMsgHandler(e)
},
location (detailData) {
let location = ''
if (detailData) {
const data = detailData.basicInfo.location
if (data) {
if (data.city) {
location = data.city
}
if (data.province) {
location = location ? `${data.province}, ${location}` : data.province
}
if (data.country) {
location = location ? `${data.country}, ${location}` : data.country
}
}
}
return location || '-'
},
handleDetailData (node) {
const n = node
const type = _.get(n, 'myData.entityType', '')
switch (type) {
case 'ip': {
this.detailCards = [
{ name: 'asn', label: this.$t('entities.asNumber'), value: _.get(n, 'myData.basicInfo.asn.asn', '-') },
{
name: 'asOrg',
label: this.$t('entities.asOrg'),
value: _.get(n.myData, 'basicInfo.asn.organization', '-')
},
{
name: 'isp',
label: this.$t('entities.graph.isp'),
value: _.get(n.myData, 'basicInfo.location.isp', '-')
},
{ name: 'location', label: this.$t('overall.location'), value: this.location(n.myData) }
]
this.relationList = [
{
icon: 'cn-icon cn-icon-subdomain',
name: 'domain',
label: this.$t('entity.graph.resolveDomain'),
value: _.get(n.myData, 'relatedEntity.domain.list', []).length,
total: _.get(n.myData, 'relatedEntity.domain.total', 0) || 0
},
{
icon: 'cn-icon cn-icon-app-name',
name: 'app',
label: this.$t('entities.tab.relatedApp'),
value: _.get(n.myData, 'relatedEntity.app.list', []).length,
total: _.get(n.myData, 'relatedEntity.app.total', '0') || 0
}
]
break
}
case 'domain': {
const expireDate = _.get(n.myData, 'basicInfo.whois.expireDate', '')
const createDate = _.get(n.myData, 'basicInfo.whois.createDate', '')
this.detailCards = [
{
name: 'categoryName',
label: this.$t('entities.category'),
value: _.get(n.myData, 'basicInfo.category.name', '-')
},
{
name: 'categoryGroup',
label: this.$t('entities.group'),
value: _.get(n.myData, 'basicInfo.category.group', '-')
},
{
name: 'reputationLevel',
label: this.$t('entities.creditLevel2'),
value: _.get(n.myData, 'basicInfo.category.reputationLevel', '-')
},
{
name: 'expireDate',
label: this.$t('entities.graph.expirationDate'),
value: expireDate ? dateFormatByAppearance(expireDate) : '-'
},
{
name: 'registrarName',
label: this.$t('entities.registrar'),
value: _.get(n.myData, 'basicInfo.whois.registrarName', '-')
},
{
name: 'registrantOrg',
label: this.$t('entities.registry'),
value: _.get(n.myData, 'basicInfo.whois.registrantOrg', '-')
},
{
name: 'registrantCountry',
label: this.$t('entities.registrationCountry'),
value: _.get(n.myData, 'basicInfo.whois.registrantCountry', '-')
},
{
name: 'createDate',
label: this.$t('entities.registrationDate'),
value: createDate ? dateFormatByAppearance(createDate) : '-'
},
{
name: 'email',
label: this.$t('entities.registryEmail'),
value: _.get(n.myData, 'basicInfo.whois.email', '-')
}
]
this.relationList = [
{
icon: 'cn-icon cn-icon-resolve-ip',
name: 'ip',
label: this.$t('entities.graph.resolveIp'),
value: _.get(n.myData, 'relatedEntity.ip.list', []).length,
total: _.get(n.myData, 'relatedEntity.ip.total', '0') || 0
},
{
icon: 'cn-icon cn-icon-subdomain',
name: 'domain',
label: this.$t('entities.subdomain'),
value: _.get(n.myData, 'relatedEntity.domain.list', []).length,
total: _.get(n.myData, 'relatedEntity.domain.total', '0') || 0
},
{
icon: 'cn-icon cn-icon-app-name',
name: 'app',
label: this.$t('entities.tab.relatedApp'),
value: _.get(n.myData, 'relatedEntity.app.list', []).length,
total: _.get(n.myData, 'relatedEntity.app.total', 0) || 0
}
]
break
}
case 'app': {
this.detailCards = [
{
name: 'appCategory',
label: this.$t('entities.category'),
value: _.get(n.myData, 'detail.category.appCategory', '-')
},
{
name: 'appSubcategory',
label: this.$t('entities.subcategory'),
value: _.get(n.myData, 'detail.category.appSubcategory', '-')
},
{
name: 'appRisk',
label: this.$t('entities.riskLevel'),
value: _.get(n.myData, 'detail.category.appRisk', '-')
},
{
name: 'appTechnology',
label: this.$t('overall.technology'),
value: _.get(n.myData, 'detail.category.appTechnology', '-')
},
{
name: 'appLongname',
label: this.$t('overall.appFullName'),
value: _.get(n.myData, 'detail.category.appLongname', '-')
},
{
name: 'appDescription',
label: this.$t('config.dataSource.description'),
value: _.get(n.myData, 'detail.category.appDescription', '-')
}
]
this.relationList = [
{
icon: 'cn-icon cn-icon-resolve-ip',
name: 'ip',
label: this.$t('entities.tab.relatedIp'),
value: _.get(n.myData, 'relatedEntity.ip.list', []).length,
total: _.get(n.myData, 'relatedEntity.ip.total', '0') || 0
},
{
icon: 'cn-icon cn-icon-subdomain',
name: 'domain',
label: this.$t('entities.graph.relatedDomain'),
value: _.get(n.myData, 'relatedEntity.domain.list', []).length,
total: _.get(n.myData, 'relatedEntity.domain.total', '0') || 0
}
]
}
}
}
}
}
</script>

View File

@@ -0,0 +1,189 @@
<template>
<div>
<loading :loading="loading" size="small"></loading>
<div class="graph-list-header">
<div>
<div class="graph-list-header-title">
<i class="cn-icon cn-icon-resolve-ip graph-list-header-icon"></i>
<span>{{ title }}</span>
</div>
<div class="graph-list-header-number">
{{ $t('entity.graph.associatedCount') }}:&nbsp;<span>{{$_.get(node, 'sourceNode.myData.relatedEntity.' + $_.get(node, 'myData.entityType') + '.total', '-')}}</span>
</div>
</div>
<i class="cn-icon cn-icon-close graph-close" @click="closeBlock"></i>
</div>
<div class="graph-list-expand-btn-block">
<div class="graph-list-expand-btn" :class="{ 'graph-list-expand-btn--disabled': expandBtnDisable }" style="display: inline-flex;" @click="expandList">
<i class="cn-icon cn-icon-expand-continue graph-expand-continue"></i>
{{ $t('entity.graph.continueToExpand') }}
</div>
</div>
<div>
<div class="digital-certificate">
<div class="digital-certificate-header padding-b-16">
<div class="digital-certificate-header__icon graph-header__icon"></div>
<div class="graph-list-content-header ">
{{ $t('entity.graph.expandedEntityCount') }}:&nbsp;&nbsp;
<span>{{$_.get(node, 'sourceNode.myData.relatedEntity.' + $_.get(node, 'myData.entityType') + '.list', []).length}}</span>
</div>
</div>
<div class="graph-list-content">
<div v-for="(item, index) in $_.get(node, 'sourceNode.myData.relatedEntity.' + $_.get(node, 'myData.entityType') + '.list', [])" :key="index">
<div class="graph-list-item-ip"><span @click="expandDetail">{{ item.vertex }}</span></div>
<template v-if="$_.get(node, 'myData.entityType', '') === 'ip'">
<div class="graph-list-item-block">
<div class="graph-list-item padding-b-4">
<div class="graph-list-item-label">{{ $t('overall.location') }}:</div>
<div class="graph-list-item-value graph-list-item-value1" style="display: flex;align-items: center;">
<!-- <img :src="require(`../../../../public/images/flag/${item.flag}.svg`)" class="graph-list-country-flag"/>-->
<span>{{ $_.get(item.detail, 'location.city', '-') }}</span>
</div>
</div>
<div class="graph-list-item">
<div class="graph-list-item-label">ASN:</div>
<div class="graph-list-item-value">{{ $_.get(item.detail, 'asn.asn', '-') }}</div>
</div>
</div>
</template>
<template v-else-if="$_.get(node, 'myData.entityType', '') === 'domain'">
<div class="graph-list-item-block">
<div class="graph-list-item__app">
<div class="graph-list-item-label__app">{{$t('entities.category')}}:</div>
<div class="graph-list-item-value">{{ $_.get(item.detail, 'category.name', '-') }}</div>
</div>
<div class="graph-list-item__app">
<div class="graph-list-item-label__app">{{$t('entities.group')}}:</div>
<div class="graph-list-item-value">{{ $_.get(item.detail, 'category.group', '-') }}</div>
</div>
<div class="graph-list-item__app">
<div class="graph-list-item-label__app">{{$t('entities.registration')}}:</div>
<div class="graph-list-item-value">{{ $_.get(item.detail, 'whois.registrantCountry', '-') }}</div>
</div>
<div class="graph-list-item__app">
<div class="graph-list-item-label__app">{{$t('entities.registry')}}:</div>
<div class="graph-list-item-value">{{ $_.get(item.detail, 'whois.registrantOrg', '-') }}</div>
</div>
</div>
</template>
<template v-else-if="$_.get(node, 'myData.entityType', '') === 'app'">
<div class="graph-list-item-block">
<div class="graph-list-item__app">
<div class="graph-list-item-label__app">APP ID:</div>
<div class="graph-list-item-value">{{ $_.get(item.detail, 'category.appId', '-') }}</div>
</div>
<div class="graph-list-item__app">
<div class="graph-list-item-label__app">{{$t('entities.category')}}:</div>
<div class="graph-list-item-value">{{ $_.get(item.detail, 'category.appCategory', '-') }}</div>
</div>
<div class="graph-list-item__app">
<div class="graph-list-item-label__app">{{$t('entities.subcategory')}}:</div>
<div class="graph-list-item-value">{{ $_.get(item.detail, 'category.appSubcategory', '-') }}</div>
</div>
<div class="graph-list-item__app">
<div class="graph-list-item-label__app">{{$t('entities.riskLevel')}}:</div>
<div class="graph-list-item-value">{{ appRisk($_.get(item.detail, 'category.appRisk', '-')) }}</div>
</div>
<div class="graph-list-item__app">
<div class="graph-list-item-label__app">{{$t('config.dataSource.description')}}:</div>
<div class="graph-list-item-value">{{ $_.get(item.detail, 'category.appDescription', '-') }}</div>
</div>
</div>
</template>
<div class="graph-list-dividing-line" v-if="index !== $_.get(node, 'sourceNode.myData.relatedEntity.' + $_.get(node, 'myData.entityType') + '.list', []).length - 1"></div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import Loading from '@/components/common/Loading'
import { riskLevelMapping } from '@/utils/constants'
import { scrollToTop } from '@/utils/tools'
import _ from 'lodash'
export default {
name: 'GraphEntityList',
props: {
node: {
type: Object
}
},
components: {
Loading
},
computed: {
expandBtnDisable () {
const loaded = _.get(this.node, 'sourceNode.myData.relatedEntity.' + _.get(this.node, 'myData.entityType') + '.list', []).length
const total = _.get(this.node, 'sourceNode.myData.relatedEntity.' + _.get(this.node, 'myData.entityType') + '.total', 0)
return !(loaded < total && total > 10 && (loaded && loaded < 50))
},
appRisk () {
return function (level) {
const m = riskLevelMapping.find(mapping => {
return mapping.value == level
})
return (m && m.name) || level
}
},
title () {
let title = ''
if (this.node) {
const entityType = _.get(this.node, 'myData.entityType', '')
if (entityType) {
const sourceType = _.get(this.node, 'sourceNode.myData.entityType', '')
switch (entityType) {
case 'ip': {
if (sourceType === 'domain') {
title = this.$t('entities.graph.resolveIp')
} else {
title = this.$t('entities.tab.relatedIp')
}
break
}
case 'domain': {
if (sourceType === 'ip') {
title = this.$t('entity.graph.resolveDomain')
} else if (sourceType === 'domain') {
title = this.$t('entities.subdomain')
} else {
title = this.$t('entities.relatedDomain')
}
break
}
case 'app': {
title = this.$t('entities.tab.relatedApp')
break
}
}
}
}
return title
}
},
methods: {
closeBlock () {
this.$emit('closeBlock')
},
expandDetail () {
this.$emit('expandDetail', this.node.id)
},
expandList () {
// 继续拓展ip列表传递信息调用接口
this.$emit('expandList', _.get(this.node, 'id'))
}
},
mounted () {
this.$nextTick(() => {
scrollToTop()
})
}
}
</script>

View File

@@ -0,0 +1,52 @@
import G6 from '@antv/g6'
export default class Edge {
constructor (sourceNode, targetNode, type) {
this.id = sourceNode.id + '__' + targetNode.id
this.source = sourceNode.id
this.target = targetNode.id
this.isTemp = type === 'temp'
this.style = type === 'temp' ? tempStyles.style : normalStyles.style
this.stateStyles = type === 'temp' ? tempStyles.stateStyles : normalStyles.stateStyles
}
}
const normalStyles = {
style: {
stroke: '#BEBEBE',
endArrow: {
path: G6.Arrow.triangle(5, 5),
fill: '#BEBEBE'
}
},
stateStyles: {
mySelected: {
stroke: '#778391',
endArrow: {
path: G6.Arrow.triangle(5, 5),
fill: '#778391'
}
}
}
}
const tempStyles = {
style: {
endArrow: {
path: G6.Arrow.triangle(5, 5),
fill: '#DDD'
},
stroke: '#DDD',
lineDash: [3, 2]
},
stateStyles: {
mySelected: {
stroke: '#DDD',
endArrow: {
path: G6.Arrow.triangle(5, 5),
fill: '#DDD'
},
lineDash: [3, 2]
}
}
}

View File

@@ -0,0 +1,182 @@
import _ from 'lodash'
import i18n from '@/i18n'
import axios from 'axios'
import { api } from '@/utils/api'
import { entityDetailTags, psiphon3IpType } from '@/utils/constants'
export default class Node {
/*
* type: 对应nodeType
* cfg: { entityType, entityName, x, y, fx, fy }
* */
constructor (type, id, cfg, sourceNode) {
this.type = type
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()
}
generateLabel () {
switch (this.type) {
case nodeType.rootNode:
case nodeType.entityNode: {
return this.id
}
case nodeType.listNode: {
return `${this.getLabelText()}(${_.get(this.sourceNode.myData, 'relatedEntity.' + 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 ''
}
isSubdomain () {
if (this.sourceNode) {
return this.sourceNode.myData.entityType === 'domain' && this.myData.entityType === 'domain'
} else {
return false
}
}
// 查询basicInfo、tags、关联实体数量
async queryDetailData () {
const entityType = this.myData.entityType
const entityName = this.myData.entityName
this.myData.basicInfo = await this.queryEntityBasicInfo(entityType, entityName)
const tags = await this.queryTags(entityType, entityName)
let _tags = []
Object.keys(tags).forEach(k => {
if (k !== 'userDefinedTags' && tags[k]) {
Object.keys(tags[k]).forEach(k2 => {
const find = entityDetailTags[entityType].find(t => t.name === k2)
if (find) {
_tags.push({ key: k2, value: this.tagValueHandler(k, k2, tags[k][k2]), type: find.type })
}
})
}
})
if (_.isArray(tags.userDefinedTags)) {
_tags = _.concat(_tags, tags.userDefinedTags.map(tag => ({ value: tag.tagValue, type: 'normal' })))
}
this.myData.tags = _tags
const relatedEntityTotalCount = await this.queryRelatedEntityCount(entityType, entityName)
this.myData.relatedEntity = {
ip: { total: relatedEntityTotalCount.ipCount, list: [] },
domain: { total: this.myData.entityType === 'domain' ? relatedEntityTotalCount.subDomainCount : relatedEntityTotalCount.domainCount, list: [] },
app: { total: relatedEntityTotalCount.appCount, list: [] }
}
}
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.data.code === 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.data.code === 200) {
return response.data.data
} else {
console.error(response)
throw response
}
}
async queryRelatedEntityCount (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.data.code === 200) {
return response.data.data
} else {
console.error(response)
throw response
}
}
tagValueHandler (k, k2, value) {
if (k === 'psiphon3Ip') {
if (k2 === 'type') {
const find = psiphon3IpType.find(t => t.value === value)
if (find) {
return find.name
}
}
}
return value
}
}
export const nodeType = {
rootNode: 'rootNode',
listNode: 'listNode',
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.relatedEntity[targetEntityType].list.length
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.data.code === 200) {
return response.data.data
} else {
console.error(response)
throw response
}
}