| /** |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| import { Component, OnInit, OnChanges, SimpleChanges, OnDestroy, Input, EventEmitter, Output } from '@angular/core'; |
| import {Subscription, Observable} from 'rxjs'; |
| |
| import {TableViewComponent} from '../table-view/table-view.component'; |
| import {SearchResponse} from '../../../model/search-response'; |
| import {SearchService} from '../../../service/search.service'; |
| import {TreeGroupData, TreeAlertsSubscription} from './tree-group-data'; |
| import {GroupResponse} from '../../../model/group-response'; |
| import {GroupResult} from '../../../model/group-result'; |
| import {SortField} from '../../../model/sort-field'; |
| import {Sort} from '../../../utils/enums'; |
| import {ElasticsearchUtils} from '../../../utils/elasticsearch-utils'; |
| import {SearchRequest} from '../../../model/search-request'; |
| import {MetaAlertCreateRequest} from '../../../model/meta-alert-create-request'; |
| import {MetaAlertService} from '../../../service/meta-alert.service'; |
| import {INDEXES, MAX_ALERTS_IN_META_ALERTS} from '../../../utils/constants'; |
| import {UpdateService} from '../../../service/update.service'; |
| import {GetRequest} from '../../../model/get-request'; |
| import { GlobalConfigService } from '../../../service/global-config.service'; |
| import { DialogService } from '../../../service/dialog.service'; |
| import { DialogType } from 'app/model/dialog-type'; |
| import { ConfirmationType } from 'app/model/confirmation-type'; |
| import { AlertSource } from '../../../model/alert-source'; |
| import { QueryBuilder } from '../query-builder'; |
| import { GroupRequest } from 'app/model/group-request'; |
| import { Group } from 'app/model/group'; |
| |
| @Component({ |
| selector: 'app-tree-view', |
| templateUrl: './tree-view.component.html', |
| styleUrls: ['./tree-view.component.scss'] |
| }) |
| |
| export class TreeViewComponent extends TableViewComponent implements OnInit, OnChanges, OnDestroy { |
| |
| @Input() globalConfig: {} = {}; |
| @Input() query = ''; |
| @Input() groups: string[] = []; |
| |
| @Output() onMetaAlertCreated = new EventEmitter<boolean>(); |
| |
| @Output() treeViewChange = new EventEmitter<number>(); |
| groupByFields: string[] = []; |
| topGroups: TreeGroupData[] = []; |
| groupResponse: GroupResponse = new GroupResponse(); |
| treeGroupSubscriptionMap: {[key: string]: TreeAlertsSubscription } = {}; |
| alertsChangedSubscription: Subscription; |
| configSubscription: Subscription; |
| dialogService: DialogService; |
| subgroupTotalAlerts = 0; |
| |
| constructor(searchService: SearchService, |
| updateService: UpdateService, |
| metaAlertService: MetaAlertService, |
| globalConfigService: GlobalConfigService, |
| dialogService: DialogService) { |
| super(searchService, updateService, metaAlertService, globalConfigService, dialogService); |
| } |
| |
| addAlertChangedListner() { |
| this.alertsChangedSubscription = this.updateService.alertChanged$.subscribe(alertSource => { |
| this.updateAlert(alertSource); |
| }); |
| } |
| |
| removeAlertChangedLister() { |
| this.alertsChangedSubscription.unsubscribe(); |
| } |
| |
| collapseGroup(groupArray: TreeGroupData[], level: number, index: number) { |
| for (let i = index + 1; i < groupArray.length; i++) { |
| if (groupArray[i].level > (level)) { |
| groupArray[i].show = false; |
| groupArray[i].expand = false; |
| } else { |
| break; |
| } |
| } |
| } |
| |
| createQuery(selectedGroup: TreeGroupData) { |
| let searchQuery = this.query; |
| let groupQery = Object.keys(selectedGroup.groupQueryMap).map(key => { |
| return key.replace(/:/g, '\\:') + |
| ':' + |
| String(selectedGroup.groupQueryMap[key]) |
| .replace(/[\*\+\-=~><\"\?^\${}\(\)\:\!\/[\]\\\s]/g, '\\$&') // replace single special characters |
| .replace(/\|\|/g, '\\||') // replace || |
| .replace(/\&\&/g, '\\&&'); // replace && |
| }).join(' AND '); |
| |
| groupQery += searchQuery === '*' ? '' : (' AND ' + searchQuery); |
| return groupQery; |
| } |
| |
| expandGroup(groupArray: TreeGroupData[], level: number, index: number) { |
| for (let i = index + 1; i < groupArray.length; i++) { |
| if (groupArray[i].level === (level + 1)) { |
| groupArray[i].show = true; |
| } else { |
| break; |
| } |
| } |
| } |
| |
| getAlerts(selectedGroup: TreeGroupData): Subscription { |
| let searchRequest = new SearchRequest(); |
| searchRequest.query = this.createQuery(selectedGroup); |
| searchRequest.from = selectedGroup.pagingData.from; |
| searchRequest.size = selectedGroup.pagingData.size; |
| searchRequest.sort = selectedGroup.sortField ? [selectedGroup.sortField] : []; |
| |
| return this.searchGroup(selectedGroup, searchRequest); |
| } |
| |
| getGroups() { |
| this.searchService.groups(this.getGroupRequest()).subscribe(groupResponse => { |
| this.updateGroupData(groupResponse); |
| }); |
| } |
| |
| updateGroupData(groupResponse) { |
| this.selectedAlerts = []; |
| this.groupResponse = groupResponse; |
| this.parseTopLevelGroup(); |
| } |
| |
| groupPageChange(group: TreeGroupData) { |
| this.getAlerts(group); |
| } |
| |
| createTopGroups(groupByFields: string[]) { |
| this.topGroups = []; |
| this.treeGroupSubscriptionMap = {}; |
| |
| this.groupResponse.groupResults.forEach((groupResult: GroupResult) => { |
| let treeGroupData = new TreeGroupData(groupResult.key, groupResult.total, groupResult.score, 0, false); |
| treeGroupData.isLeafNode = (groupByFields.length === 1); |
| |
| if (groupByFields.length === 1) { |
| treeGroupData.groupQueryMap = this.createTopGroupQueryMap(groupByFields[0], groupResult); |
| } |
| |
| this.topGroups.push(treeGroupData); |
| }); |
| } |
| |
| createTopGroupQueryMap(groupByFields: string, groupResult: GroupResult) { |
| let groupQueryMap = {}; |
| groupQueryMap[groupByFields] = groupResult.key; |
| return groupQueryMap; |
| } |
| |
| initTopGroups() { |
| let groupByFields = this.groups; |
| let currentTopGroupKeys = this.groupResponse.groupResults.map(groupResult => groupResult.key); |
| let previousTopGroupKeys = this.topGroups.map(group => group.key); |
| |
| if (this.topGroups.length === 0 || JSON.stringify(this.groupByFields) !== JSON.stringify(groupByFields) || |
| JSON.stringify(currentTopGroupKeys) !== JSON.stringify(previousTopGroupKeys)) { |
| this.createTopGroups(groupByFields); |
| } |
| |
| this.subgroupTotalAlerts = this.topGroups.reduce((accumulator, currentValue) => { |
| return accumulator + currentValue.total; |
| }, 0); |
| this.treeViewChange.next(this.subgroupTotalAlerts); |
| this.groupByFields = groupByFields; |
| } |
| |
| search(resetPaginationParams = true, pageSize: number = null) { |
| this.getGroups(); |
| } |
| |
| ngOnChanges(changes: SimpleChanges) { |
| if ((changes['alerts'] && changes['alerts'].currentValue)) { |
| this.search(); |
| } |
| } |
| |
| ngOnInit() { |
| this.addAlertChangedListner(); |
| } |
| |
| ngOnDestroy(): void { |
| this.removeAlertChangedLister(); |
| this.treeViewChange.next(0); |
| } |
| |
| searchGroup(selectedGroup: TreeGroupData, searchRequest: SearchRequest): Subscription { |
| return this.searchService.search(searchRequest).subscribe(results => { |
| this.setData(selectedGroup, results); |
| }, error => { |
| this.dialogService.launchDialog(ElasticsearchUtils.extractESErrorMessage(error), DialogType.Error); |
| }); |
| } |
| |
| setData(selectedGroup: TreeGroupData, results: SearchResponse) { |
| selectedGroup.response.results = results.results; |
| selectedGroup.pagingData.total = results.total; |
| selectedGroup.total = results.total; |
| |
| this.topGroups.map(topGroup => { |
| if (topGroup.treeSubGroups.length > 0) { |
| topGroup.total = topGroup.treeSubGroups.reduce((total, subGroup) => { return total + subGroup.total; }, 0); |
| } |
| }); |
| } |
| |
| checkAndToSubscription(group: TreeGroupData) { |
| if (group.isLeafNode) { |
| let key = JSON.stringify(group.groupQueryMap); |
| if (this.treeGroupSubscriptionMap[key]) { |
| this.removeFromSubscription(group); |
| } |
| |
| let subscription = this.getAlerts(group); |
| this.treeGroupSubscriptionMap[key] = new TreeAlertsSubscription(subscription, group); |
| } |
| } |
| |
| removeFromSubscription(group: TreeGroupData) { |
| if (group.isLeafNode) { |
| let key = JSON.stringify(group.groupQueryMap); |
| let subscription = this.treeGroupSubscriptionMap[key].refreshTimer; |
| if (subscription && !subscription.closed) { |
| subscription.unsubscribe(); |
| } |
| delete this.treeGroupSubscriptionMap[key]; |
| } |
| } |
| |
| toggleSubGroups(topLevelGroup: TreeGroupData, selectedGroup: TreeGroupData, index: number) { |
| selectedGroup.expand = !selectedGroup.expand; |
| |
| if (selectedGroup.expand) { |
| this.expandGroup(topLevelGroup.treeSubGroups, selectedGroup.level, index); |
| this.checkAndToSubscription(selectedGroup); |
| } else { |
| this.collapseGroup(topLevelGroup.treeSubGroups, selectedGroup.level, index); |
| this.removeFromSubscription(selectedGroup); |
| } |
| } |
| |
| toggleTopLevelGroup(group: TreeGroupData) { |
| group.expand = !group.expand; |
| group.show = !group.show; |
| |
| if (group.expand) { |
| this.checkAndToSubscription(group); |
| } else { |
| this.removeFromSubscription(group); |
| } |
| } |
| |
| parseSubGroups(group: GroupResult, groupAsArray: TreeGroupData[], |
| parentQueryMap: {[key: string]: string}, currentGroupKey: string, level: number, index: number): number { |
| index++; |
| |
| let currentTreeNodeData = (groupAsArray.length > 0) ? groupAsArray[index] : null; |
| |
| if (currentTreeNodeData && (currentTreeNodeData.key === group.key) && (currentTreeNodeData.level === level)) { |
| currentTreeNodeData.total = group.total; |
| } else { |
| let newTreeNodeData = new TreeGroupData(group.key, group.total, group.score, level, level === 1); |
| if (!currentTreeNodeData) { |
| groupAsArray.push(newTreeNodeData); |
| } else { |
| groupAsArray.splice(index, 1, newTreeNodeData); |
| } |
| } |
| |
| groupAsArray[index].isLeafNode = false; |
| groupAsArray[index].groupQueryMap = JSON.parse(JSON.stringify(parentQueryMap)); |
| groupAsArray[index].groupQueryMap[currentGroupKey] = group.key; |
| |
| if (!group.groupResults) { |
| groupAsArray[index].isLeafNode = true; |
| if (groupAsArray[index].expand && groupAsArray[index].show && groupAsArray[index].groupQueryMap) { |
| this.checkAndToSubscription(groupAsArray[index]); |
| } |
| return index; |
| } |
| |
| group.groupResults.forEach(subGroup => { |
| index = this.parseSubGroups(subGroup, groupAsArray, groupAsArray[index].groupQueryMap, group.groupedBy, level + 1, index); |
| }); |
| |
| return index; |
| } |
| |
| parseTopLevelGroup() { |
| let groupedBy = this.groupResponse.groupedBy; |
| |
| this.initTopGroups(); |
| |
| for (let i = 0; i < this.groupResponse.groupResults.length; i++) { |
| let index = -1; |
| let topGroup = this.topGroups[i]; |
| let resultGroup = this.groupResponse.groupResults[i]; |
| |
| topGroup.total = resultGroup.total; |
| topGroup.groupQueryMap = this.createTopGroupQueryMap(groupedBy, resultGroup); |
| |
| if (resultGroup.groupResults) { |
| resultGroup.groupResults.forEach(subGroup => { |
| index = this.parseSubGroups(subGroup, topGroup.treeSubGroups, topGroup.groupQueryMap, resultGroup.groupedBy, 1, index); |
| }); |
| |
| topGroup.treeSubGroups.splice(index + 1); |
| } |
| } |
| |
| if (this.groupByFields.length === 1) { |
| this.refreshAllExpandedGroups(); |
| } |
| } |
| |
| sortTreeSubGroup($event, treeGroup: TreeGroupData) { |
| let sortBy = $event.sortBy === 'id' ? '_uid' : $event.sortBy; |
| let sortOrder = $event.sortOrder === Sort.ASC ? 'asc' : 'desc'; |
| let sortField = new SortField(sortBy, sortOrder); |
| |
| treeGroup.sortEvent = $event; |
| treeGroup.sortField = sortField; |
| treeGroup.treeSubGroups.forEach(treeSubGroup => treeSubGroup.sortField = sortField); |
| |
| this.refreshAllExpandedGroups(); |
| } |
| |
| selectAllGroupRows($event, group: TreeGroupData) { |
| this.selectedAlerts = []; |
| |
| if ($event.target.checked) { |
| if (group.expand && group.show && group.response) { |
| this.selectedAlerts = group.response.results; |
| } |
| |
| group.treeSubGroups.forEach(subGroup => { |
| if (subGroup.expand && subGroup.show && subGroup.response) { |
| this.selectedAlerts = this.selectedAlerts.concat(subGroup.response.results); |
| } |
| }); |
| } |
| |
| this.onSelectedAlertsChange.emit(this.selectedAlerts); |
| } |
| |
| refreshAllExpandedGroups() { |
| Object.keys(this.treeGroupSubscriptionMap).forEach(key => { |
| this.getAlerts(this.treeGroupSubscriptionMap[key].group); |
| }); |
| } |
| |
| canCreateMetaAlert(count: number) { |
| if (count > MAX_ALERTS_IN_META_ALERTS) { |
| let errorMessage = 'Meta Alert cannot have more than ' + MAX_ALERTS_IN_META_ALERTS + ' alerts within it'; |
| this.dialogService.launchDialog(errorMessage, DialogType.Error); |
| return false; |
| } |
| return true; |
| } |
| |
| createGetRequestArray(searchResponse: SearchResponse): any { |
| return searchResponse.results.map(alert => |
| new GetRequest(alert.source.guid, alert.source[this.globalConfig['source.type.field']], alert.index)); |
| } |
| |
| getAllAlertsForSlectedGroup(group: TreeGroupData): Observable<SearchResponse> { |
| let searchRequest = new SearchRequest(); |
| searchRequest.fields = ['guid', this.globalConfig['source.type.field']]; |
| searchRequest.from = 0; |
| searchRequest.indices = INDEXES; |
| searchRequest.query = this.createQuery(group); |
| searchRequest.size = MAX_ALERTS_IN_META_ALERTS; |
| searchRequest.facetFields = []; |
| return this.searchService.search(searchRequest); |
| } |
| |
| doCreateMetaAlert(group: TreeGroupData, index: number) { |
| this.getAllAlertsForSlectedGroup(group).subscribe((searchResponse: SearchResponse) => { |
| if (this.canCreateMetaAlert(searchResponse.total)) { |
| let metaAlert = new MetaAlertCreateRequest(); |
| metaAlert.alerts = this.createGetRequestArray(searchResponse); |
| metaAlert.groups = this.groups; |
| |
| this.metaAlertService.create(metaAlert).subscribe(() => { |
| setTimeout(() => this.onMetaAlertCreated.emit(true), 1000); |
| console.log('Meta alert created successfully'); |
| }); |
| } |
| }); |
| } |
| |
| getGroupRequest(): GroupRequest { |
| const req = new GroupRequest(); |
| req.groups = this.groups.map(groupName => new Group(groupName)); |
| req.query = this.query; |
| req.scoreField = this.threatScoreFieldName(); |
| return req; |
| } |
| |
| createMetaAlert($event, group: TreeGroupData, index: number) { |
| if (this.canCreateMetaAlert(group.total)) { |
| let confirmationMsg = 'Do you wish to create a meta alert with ' + |
| (group.total === 1 ? ' alert' : group.total + ' selected alerts') + '?'; |
| const confirmedSubscription = this.dialogService.launchDialog(confirmationMsg).subscribe(action => { |
| if (action === ConfirmationType.Confirmed) { |
| this.doCreateMetaAlert(group, index); |
| } |
| confirmedSubscription.unsubscribe(); |
| }); |
| } |
| |
| $event.stopPropagation(); |
| return false; |
| } |
| |
| updateAlert(alertSource: AlertSource) { |
| Object.keys(this.treeGroupSubscriptionMap).forEach(key => { |
| let group = this.treeGroupSubscriptionMap[key].group; |
| if (group.response && group.response.results && group.response.results.length > 0) { |
| group.response.results.filter(alert => alert.source.guid === alertSource.guid) |
| .forEach(alert => alert.source = alertSource); |
| } |
| }); |
| } |
| } |