diff --git a/src/assets/css/components/index.scss b/src/assets/css/components/index.scss index a5caf1e8..bf28e8ed 100644 --- a/src/assets/css/components/index.scss +++ b/src/assets/css/components/index.scss @@ -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"; diff --git a/src/assets/css/components/views/entityExplorer/entity-graph.scss b/src/assets/css/components/views/entityExplorer/entity-graph.scss new file mode 100644 index 00000000..61cdb7d6 --- /dev/null +++ b/src/assets/css/components/views/entityExplorer/entity-graph.scss @@ -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; + } + } + } + } + } + } +} diff --git a/src/assets/css/components/views/charts2/graphRightDetailBlock.scss b/src/assets/css/components/views/entityExplorer/graphRightDetailBlock.scss similarity index 100% rename from src/assets/css/components/views/charts2/graphRightDetailBlock.scss rename to src/assets/css/components/views/entityExplorer/graphRightDetailBlock.scss diff --git a/src/assets/css/components/views/charts2/graphRightListBlock.scss b/src/assets/css/components/views/entityExplorer/graphRightListBlock.scss similarity index 100% rename from src/assets/css/components/views/charts2/graphRightListBlock.scss rename to src/assets/css/components/views/entityExplorer/graphRightListBlock.scss diff --git a/src/router/index.js b/src/router/index.js index 9a170143..9339213e 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -29,7 +29,7 @@ const routes = [ }, { path: '/entityGraph', - component: () => import('@/views/entityExplorer/EntityGraph') + component: () => import('@/views/entityExplorer/EntityGraph2') }, { path: '/detection', diff --git a/src/views/entityExplorer/EntityGraph2.vue b/src/views/entityExplorer/EntityGraph2.vue new file mode 100644 index 00000000..9f2d99d6 --- /dev/null +++ b/src/views/entityExplorer/EntityGraph2.vue @@ -0,0 +1,924 @@ + + + diff --git a/src/views/entityExplorer/entityGraph/GraphEntityDetail.vue b/src/views/entityExplorer/entityGraph/GraphEntityDetail.vue new file mode 100644 index 00000000..8d23e2b3 --- /dev/null +++ b/src/views/entityExplorer/entityGraph/GraphEntityDetail.vue @@ -0,0 +1,418 @@ + + + diff --git a/src/views/entityExplorer/entityGraph/GraphEntityList.vue b/src/views/entityExplorer/entityGraph/GraphEntityList.vue new file mode 100644 index 00000000..bd859aea --- /dev/null +++ b/src/views/entityExplorer/entityGraph/GraphEntityList.vue @@ -0,0 +1,189 @@ + + + diff --git a/src/views/entityExplorer/entityGraph/edge.js b/src/views/entityExplorer/entityGraph/edge.js new file mode 100644 index 00000000..35cede31 --- /dev/null +++ b/src/views/entityExplorer/entityGraph/edge.js @@ -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] + } + } +} diff --git a/src/views/entityExplorer/entityGraph/node.js b/src/views/entityExplorer/entityGraph/node.js new file mode 100644 index 00000000..9da96edf --- /dev/null +++ b/src/views/entityExplorer/entityGraph/node.js @@ -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 + } +}