Repository: metron Updated Branches: refs/heads/master bbfe29a97 -> 39bb85676
METRON-1223 Add support to add comments for alerts (iraghumitra via james-sirota) closes apache/metron#788 Project: http://git-wip-us.apache.org/repos/asf/metron/repo Commit: http://git-wip-us.apache.org/repos/asf/metron/commit/39bb8567 Tree: http://git-wip-us.apache.org/repos/asf/metron/tree/39bb8567 Diff: http://git-wip-us.apache.org/repos/asf/metron/diff/39bb8567 Branch: refs/heads/master Commit: 39bb856762739ecd2103e3b67eed76d4fb655eaa Parents: bbfe29a Author: iraghumitra <[email protected]> Authored: Fri Oct 13 15:47:17 2017 -0700 Committer: jsirota <[email protected]> Committed: Fri Oct 13 15:47:17 2017 -0700 ---------------------------------------------------------------------- metron-interface/metron-alerts/README.md | 5 +- .../e2e/alert-details/alert-details.po.ts | 59 ++++++++++++- .../alert-details-status.e2e-spec.ts | 34 +++++++- .../e2e/alerts-list/alerts-list.e2e-spec.ts | 4 +- .../e2e/alerts-list/alerts-list.po.ts | 4 + .../metron-alerts/e2e/login/login.po.ts | 6 +- .../metron-alerts/e2e/utils/e2e_util.ts | 16 +++- metron-interface/metron-alerts/package.json | 2 + .../metron-alerts/src/_variables.scss | 3 + .../app/alerts/alert-details/alert-comment.ts | 29 +++++++ .../alert-details/alert-details.component.html | 91 ++++++++++++++------ .../alert-details/alert-details.component.scss | 77 ++++++++++++++++- .../alert-details/alert-details.component.ts | 80 ++++++++++++++++- .../alert-details/alerts-details.module.ts | 4 +- .../alert-details/alerts-details.routing.ts | 2 +- .../alerts/alerts-list/alerts-list.component.ts | 7 +- .../table-view/table-view.component.html | 4 +- .../src/app/login/login.component.html | 4 +- .../src/app/login/login.component.scss | 4 + .../src/app/login/login.component.ts | 2 +- .../metron-alerts/src/app/model/alert-source.ts | 3 + .../metron-alerts/src/app/model/alert.ts | 1 + .../src/app/model/patch-request.ts | 6 +- .../metron-alerts/src/app/model/patch.ts | 28 ++++++ .../src/app/service/authentication.service.ts | 4 + metron-interface/metron-alerts/src/styles.scss | 10 ++- 26 files changed, 433 insertions(+), 56 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/metron/blob/39bb8567/metron-interface/metron-alerts/README.md ---------------------------------------------------------------------- diff --git a/metron-interface/metron-alerts/README.md b/metron-interface/metron-alerts/README.md index b0433d0..3d43e3e 100644 --- a/metron-interface/metron-alerts/README.md +++ b/metron-interface/metron-alerts/README.md @@ -12,6 +12,9 @@ UI uses local storage to save all the data. A middleware needs to be designed a ### Search for Alert GUIDs Alert GUIDs must be double-quoted when being searched on to ensure correctness of results, e.g. guid:"id1". +### Search for Comments +Users cannot search for the contents of the comment's in the Alerts-UI + ## Prerequisites * The Metron REST application should be up and running and Elasticsearch should have some alerts populated by Metron topologies * The Management UI should be installed (which includes [Express](https://expressjs.com/)) @@ -58,7 +61,7 @@ Alert GUIDs must be double-quoted when being searched on to ensure correctness o ### From Ambari MPack -The Alerts UI is included in the Metron Ambari MPack. It can be accessed through the Quick Links in the Metron service. +The Alerts UI is included in the Metron Ambari MPack. It can be accessed through the Quick Links in the Metron service. ## Configuration http://git-wip-us.apache.org/repos/asf/metron/blob/39bb8567/metron-interface/metron-alerts/e2e/alert-details/alert-details.po.ts ---------------------------------------------------------------------- diff --git a/metron-interface/metron-alerts/e2e/alert-details/alert-details.po.ts b/metron-interface/metron-alerts/e2e/alert-details/alert-details.po.ts index 79a0e1d..39aea0b 100644 --- a/metron-interface/metron-alerts/e2e/alert-details/alert-details.po.ts +++ b/metron-interface/metron-alerts/e2e/alert-details/alert-details.po.ts @@ -17,12 +17,25 @@ */ import {browser, element, by, protractor} from 'protractor'; +import {waitForElementInVisibility, waitForElementPresence, waitForElementVisibility} from '../utils/e2e_util'; export class MetronAlertDetailsPage { navigateTo(alertId: string) { browser.waitForAngularEnabled(false); - return browser.get('/alerts-list(dialog:details/alerts_ui_e2e/'+ alertId +')'); + browser.get('/alerts-list(dialog:details/alerts_ui_e2e/'+ alertId +'/alerts_ui_e2e_index)'); + browser.sleep(2000); + } + + addCommentAndSave(comment: string, index: number) { + let textAreaElement = element(by.css('app-alert-details textarea')); + let addCommentButtonElement = element(by.buttonText('ADD COMMENT')); + let latestCommentEle = element.all(by.css('.comment-container .comment')).get(index); + + textAreaElement.clear() + .then(() => textAreaElement.sendKeys(comment)) + .then(() => addCommentButtonElement.click()) + .then(() => waitForElementPresence(latestCommentEle)); } clickNew() { @@ -45,6 +58,38 @@ export class MetronAlertDetailsPage { element.all(by.css('.metron-slider-pane-details table tbody tr')).get(2).all(by.css('td')).get(1).click(); } + clickCommentsInSideNav() { + return element(by.css('app-alert-details .fa.fa-comment')).click(); + } + + clickNoForConfirmation() { + browser.sleep(1000); + let dialogElement = element(by.css('.metron-dialog .modal-header .close')); + let maskElement = element(by.css('.modal-backdrop.fade')); + waitForElementVisibility(dialogElement).then(() => element(by.css('.metron-dialog')).element(by.buttonText('Cancel')).click()) + .then(() => waitForElementInVisibility(maskElement)); + } + + clickYesForConfirmation() { + browser.sleep(1000); + let dialogElement = element(by.css('.metron-dialog .modal-header .close')); + let maskElement = element(by.css('.modal-backdrop.fade')); + waitForElementVisibility(dialogElement).then(() => element(by.css('.metron-dialog')).element(by.buttonText('OK')).click()) + .then(() => waitForElementInVisibility(maskElement)); + } + + closeDetailPane() { + element(by.css('app-alert-details .close-button')).click(); + browser.sleep(2000); + } + + deleteComment() { + let scrollToEle = element.all(by.css('.comment-container')).get(0); + let trashIcon = element.all(by.css('.fa.fa-trash-o')).get(0); + browser.actions().mouseMove(scrollToEle).perform().then(() => waitForElementVisibility(trashIcon)) + .then(() => element.all(by.css('.fa.fa-trash-o')).get(0).click()); + } + getAlertStatus(previousText) { let alertStatusElement = element.all(by.css('.metron-slider-pane-details .form .row')).get(0).all(by.css('div')).get(1); return this.waitForTextChange(alertStatusElement, previousText).then(() => { @@ -52,6 +97,18 @@ export class MetronAlertDetailsPage { }); } + getCommentsText() { + return element.all(by.css('.comment-container .comment')).getText(); + } + + getCommentsUserNameAndTimeStamp() { + return element.all(by.css('.comment-container .username-timestamp')).getText(); + } + + getCommentIconCountInListView() { + return element.all(by.css('app-table-view .fa.fa-comments-o')).count(); + } + waitForTextChange(element, previousText) { let EC = protractor.ExpectedConditions; return browser.wait(EC.not(EC.textToBePresentInElement(element, previousText))); http://git-wip-us.apache.org/repos/asf/metron/blob/39bb8567/metron-interface/metron-alerts/e2e/alert-details/alert-status/alert-details-status.e2e-spec.ts ---------------------------------------------------------------------- diff --git a/metron-interface/metron-alerts/e2e/alert-details/alert-status/alert-details-status.e2e-spec.ts b/metron-interface/metron-alerts/e2e/alert-details/alert-status/alert-details-status.e2e-spec.ts index 4e7331c..58d4892 100644 --- a/metron-interface/metron-alerts/e2e/alert-details/alert-status/alert-details-status.e2e-spec.ts +++ b/metron-interface/metron-alerts/e2e/alert-details/alert-status/alert-details-status.e2e-spec.ts @@ -65,4 +65,36 @@ describe('metron-alerts alert status', function() { page.clickNew(); }); -}); \ No newline at end of file + it('should add comments', () => { + let comment1 = 'This is a sample comment'; + let comment2 = 'This is a sample comment again'; + let userNameAndTimestamp = '- admin - a few seconds ago'; + + page.clickCommentsInSideNav(); + page.addCommentAndSave(comment1, 0); + + expect(page.getCommentsText()).toEqual([comment1]); + expect(page.getCommentsUserNameAndTimeStamp()).toEqual([userNameAndTimestamp]); + + page.addCommentAndSave(comment2, 1); + expect(page.getCommentsText()).toEqual([comment2, comment1]); + expect(page.getCommentsUserNameAndTimeStamp()).toEqual([userNameAndTimestamp, userNameAndTimestamp]); + + page.deleteComment(); + page.clickNoForConfirmation(); + expect(page.getCommentsText()).toEqual([comment2, comment1]); + + page.deleteComment(); + page.clickYesForConfirmation(); + expect(page.getCommentsText()).toEqual([comment1]); + + expect(page.getCommentIconCountInListView()).toEqual(1); + + page.deleteComment(); + page.clickYesForConfirmation(); + expect(page.getCommentsText()).toEqual([]); + + page.closeDetailPane(); + }); + +}); http://git-wip-us.apache.org/repos/asf/metron/blob/39bb8567/metron-interface/metron-alerts/e2e/alerts-list/alerts-list.e2e-spec.ts ---------------------------------------------------------------------- diff --git a/metron-interface/metron-alerts/e2e/alerts-list/alerts-list.e2e-spec.ts b/metron-interface/metron-alerts/e2e/alerts-list/alerts-list.e2e-spec.ts index 8e92737..43290fe 100644 --- a/metron-interface/metron-alerts/e2e/alerts-list/alerts-list.e2e-spec.ts +++ b/metron-interface/metron-alerts/e2e/alerts-list/alerts-list.e2e-spec.ts @@ -19,13 +19,13 @@ import { MetronAlertsPage } from './alerts-list.po'; import { customMatchers } from '../matchers/custom-matchers'; import { LoginPage } from '../login/login.po'; -import { loadTestData, deleteTestData } from "../utils/e2e_util"; +import { loadTestData, deleteTestData } from '../utils/e2e_util'; describe('metron-alerts App', function() { let page: MetronAlertsPage; let loginPage: LoginPage; let columnNames = [ 'Score', 'id', 'timestamp', 'source:type', 'ip_src_addr', 'enrichm...:country', - 'ip_dst_addr', 'host', 'alert_status', '', '' ]; + 'ip_dst_addr', 'host', 'alert_status', '', '', '' ]; let colNamesColumnConfig = [ 'score', 'id', 'timestamp', 'source:type', 'ip_src_addr', 'enrichments:geo:ip_dst_addr:country', 'ip_dst_addr', 'host', 'alert_status' ]; http://git-wip-us.apache.org/repos/asf/metron/blob/39bb8567/metron-interface/metron-alerts/e2e/alerts-list/alerts-list.po.ts ---------------------------------------------------------------------- diff --git a/metron-interface/metron-alerts/e2e/alerts-list/alerts-list.po.ts b/metron-interface/metron-alerts/e2e/alerts-list/alerts-list.po.ts index 8e09f85..39aefa7 100644 --- a/metron-interface/metron-alerts/e2e/alerts-list/alerts-list.po.ts +++ b/metron-interface/metron-alerts/e2e/alerts-list/alerts-list.po.ts @@ -184,6 +184,10 @@ export class MetronAlertsPage { return element(by.css('.ace_line')).getText(); } + isCommentIconPresentInTable() { + return element.all(by.css('app-table-view .fa.fa-comments-o')).count(); + } + getRecentSearchOptions() { browser.sleep(1000); let map = {}; http://git-wip-us.apache.org/repos/asf/metron/blob/39bb8567/metron-interface/metron-alerts/e2e/login/login.po.ts ---------------------------------------------------------------------- diff --git a/metron-interface/metron-alerts/e2e/login/login.po.ts b/metron-interface/metron-alerts/e2e/login/login.po.ts index c9f8c23..2f0f81d 100644 --- a/metron-interface/metron-alerts/e2e/login/login.po.ts +++ b/metron-interface/metron-alerts/e2e/login/login.po.ts @@ -16,7 +16,7 @@ * limitations under the License. */ import { browser, element, by } from 'protractor'; -import {waitForElementVisibility} from '../utils/e2e_util'; +import {waitForElementVisibility, waitForURL} from '../utils/e2e_util'; export class LoginPage { navigateToLogin() { @@ -35,7 +35,7 @@ export class LoginPage { browser.waitForAngularEnabled(false); element.all(by.css('.alert .close')).click(); element.all(by.css('.logout-link')).click(); - browser.sleep(2000); + waitForURL('http://localhost:4200/login'); } setUserNameAndPassword(userName: string, password: string) { @@ -49,7 +49,7 @@ export class LoginPage { getErrorMessage() { browser.waitForAngularEnabled(false); - let errElement = element(by.css('div[style="color:#a94442"]')); + let errElement = element(by.css('.login-failed-msg')); return waitForElementVisibility(errElement).then(() => { return errElement.getText().then((message) => { return message.replace(/\n/, '').replace(/LOG\ IN$/, ''); http://git-wip-us.apache.org/repos/asf/metron/blob/39bb8567/metron-interface/metron-alerts/e2e/utils/e2e_util.ts ---------------------------------------------------------------------- diff --git a/metron-interface/metron-alerts/e2e/utils/e2e_util.ts b/metron-interface/metron-alerts/e2e/utils/e2e_util.ts index ce5609a..341e668 100644 --- a/metron-interface/metron-alerts/e2e/utils/e2e_util.ts +++ b/metron-interface/metron-alerts/e2e/utils/e2e_util.ts @@ -10,6 +10,16 @@ export function changeURL(url: string) { }); } +export function waitForURL(url: string) { + let EC = protractor.ExpectedConditions; + return browser.wait(EC.urlIs(url)); +} + +export function waitForText(element, text) { + let EC = protractor.ExpectedConditions; + return browser.wait(EC.textToBePresentInElement(element, text)); +} + export function waitForElementInVisibility (_element ) { let EC = protractor.ExpectedConditions; return browser.wait(EC.invisibilityOf(_element)); @@ -32,8 +42,10 @@ export function waitForStalenessOf (_element ) { export function loadTestData() { deleteTestData(); - fs.createReadStream('e2e/mock-data/alerts_ui_e2e_index.template').pipe(request.post('http://node1:9200/_template/alerts_ui_e2e_index')); - fs.createReadStream('e2e/mock-data/alerts_ui_e2e_index.data').pipe(request.post('http://node1:9200/alerts_ui_e2e_index/alerts_ui_e2e_doc/_bulk')); + fs.createReadStream('e2e/mock-data/alerts_ui_e2e_index.template') + .pipe(request.post('http://node1:9200/_template/alerts_ui_e2e_index')); + fs.createReadStream('e2e/mock-data/alerts_ui_e2e_index.data') + .pipe(request.post('http://node1:9200/alerts_ui_e2e_index/alerts_ui_e2e_doc/_bulk')); } export function deleteTestData() { http://git-wip-us.apache.org/repos/asf/metron/blob/39bb8567/metron-interface/metron-alerts/package.json ---------------------------------------------------------------------- diff --git a/metron-interface/metron-alerts/package.json b/metron-interface/metron-alerts/package.json index 446c40d..6ff3c3c 100644 --- a/metron-interface/metron-alerts/package.json +++ b/metron-interface/metron-alerts/package.json @@ -25,6 +25,7 @@ "bootstrap": "4.0.0-alpha.6", "core-js": "^2.4.1", "font-awesome": "^4.7.0", + "moment": "^2.18.1", "rxjs": "^5.1.0", "web-animations-js": "^2.2.2", "zone.js": "^0.8.4" @@ -34,6 +35,7 @@ "@angular/compiler-cli": "^4.0.0", "@types/ace": "0.0.32", "@types/jasmine": "2.5.38", + "@types/moment": "^2.13.0", "@types/node": "~6.0.60", "codelyzer": "~2.0.0", "compression": "1.6.2", http://git-wip-us.apache.org/repos/asf/metron/blob/39bb8567/metron-interface/metron-alerts/src/_variables.scss ---------------------------------------------------------------------- diff --git a/metron-interface/metron-alerts/src/_variables.scss b/metron-interface/metron-alerts/src/_variables.scss index 2705ff1..e4055ce 100644 --- a/metron-interface/metron-alerts/src/_variables.scss +++ b/metron-interface/metron-alerts/src/_variables.scss @@ -72,6 +72,9 @@ $eastern-blue: #1F91BE; $mantis: #80BF4D; $sky-blue: #75D2ED; $outer-space: #2E3A3F; +$rolling-stone: #808285; +$nile-blue: #18404E; +$apple-blossom: #A94442; $eastern-blue-1: #1190C0; $matisse: #1E7490; http://git-wip-us.apache.org/repos/asf/metron/blob/39bb8567/metron-interface/metron-alerts/src/app/alerts/alert-details/alert-comment.ts ---------------------------------------------------------------------- diff --git a/metron-interface/metron-alerts/src/app/alerts/alert-details/alert-comment.ts b/metron-interface/metron-alerts/src/app/alerts/alert-details/alert-comment.ts new file mode 100644 index 0000000..a9cf802 --- /dev/null +++ b/metron-interface/metron-alerts/src/app/alerts/alert-details/alert-comment.ts @@ -0,0 +1,29 @@ +/** + * 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. + */ +export class AlertComment { + comment = ''; + username: string; + timestamp: number; + + constructor(comment: string, username: string, timestamp: number) { + this.comment = comment; + this.username = username; + this.timestamp = timestamp; + } +} + http://git-wip-us.apache.org/repos/asf/metron/blob/39bb8567/metron-interface/metron-alerts/src/app/alerts/alert-details/alert-details.component.html ---------------------------------------------------------------------- diff --git a/metron-interface/metron-alerts/src/app/alerts/alert-details/alert-details.component.html b/metron-interface/metron-alerts/src/app/alerts/alert-details/alert-details.component.html index 8c82fab..1b5330e 100644 --- a/metron-interface/metron-alerts/src/app/alerts/alert-details/alert-details.component.html +++ b/metron-interface/metron-alerts/src/app/alerts/alert-details/alert-details.component.html @@ -12,35 +12,70 @@ the specific language governing permissions and limitations under the License. --> <div class="metron-slider-pane-details load-right-to-left dialog1x"> - <div class="container-fluid"> - <div class="row mb-3"> - <div class="col-md-10"> - <div class="form-title" appAlertSeverity [severity]="alertSource['threat:triage:score']"> {{ alertSource['threat:triage:score'] }} {{ alertSource.guid }}</div> + <div class="container-fluid pl-0 h-100"> + <div class="h-100 d-flex"> + <div class="nav-container"> + <ul class="nav flex-column"> + <li class="nav-item"> + <a class="nav-link" [ngClass]="{'active': activeTab === tabs.DETAILS}" (click)="activeTab=tabs.DETAILS"> + <i class="fa fa-info-circle" aria-hidden="true"></i> + </a> + </li> + <li class="nav-item"> + <a class="nav-link" [ngClass]="{'active': activeTab === tabs.COMMENTS}" (click)="activeTab=tabs.COMMENTS"> + <i class="fa fa-comment" aria-hidden="true"></i> + </a> + </li> + </ul> </div> - <div class="col-md-2"><i class="fa fa-times pull-right close-button" aria-hidden="true" (click)="goBack()"></i></div> - </div> - <div class="row justify-content-center py-4 actions"> - <table> - <tr> - <td class="title"> Status</td> - <td [ngClass]="{'primary': selectedAlertState === alertState.ESCALATE, 'secondary': selectedAlertState !== alertState.ESCALATE}" (click)="processEscalate()">ESCALATE</td> - <td></td> - </tr> - <tr> - <td [ngClass]="{'primary': selectedAlertState === alertState.NEW, 'secondary': selectedAlertState !== alertState.NEW}" (click)="processNew()">NEW</td> - <td [ngClass]="{'primary': selectedAlertState === alertState.OPEN, 'secondary': selectedAlertState !== alertState.OPEN}" (click)="processOpen()">OPEN</td> - <td [ngClass]="{'primary': selectedAlertState === alertState.DISMISS, 'secondary': selectedAlertState !== alertState.DISMISS}" (click)="processDismiss()">DISMISS</td> - </tr> - <tr> - <td></td> - <td [ngClass]="{'primary': selectedAlertState === alertState.RESOLVE, 'secondary': selectedAlertState !== alertState.RESOLVE}" (click)="processResolve()">RESOLVE</td> - <td></td> - </tr> - </table> - </div> - <div class="ml-1 my-4 form"> - <div *ngFor="let field of alertFields" class="row"> - <div class="col-6 mb-1 key">{{ field }}</div> <div class="col-6"> {{ alertSource[field] }} </div> + <div class="details-container"> + <div class="container-fluid h-100"> + <div class="row title-container"> + <div class="col-md-10 px-0"> + <div class="form-title" appAlertSeverity [severity]="alertSource['threat:triage:score']"> {{ alertSource['threat:triage:score'] }} {{ alertSource.guid }}</div> + </div> + <div class="col-md-2 px-0"> + <i class="fa fa-times pull-right close-button" aria-hidden="true" (click)="goBack()"></i> + </div> + </div> + <div class="row justify-content-center py-4 actions"> + <table> + <tr> + <td class="title"> Status</td> + <td [ngClass]="{'primary': selectedAlertState === alertState.ESCALATE, 'secondary': selectedAlertState !== alertState.ESCALATE}" data-name="escalate" (click)="processEscalate()">ESCALATE</td> + <td></td> + </tr> + <tr> + <td [ngClass]="{'primary': selectedAlertState === alertState.NEW, 'secondary': selectedAlertState !== alertState.NEW}" data-name="new" (click)="processNew()">NEW</td> + <td [ngClass]="{'primary': selectedAlertState === alertState.OPEN, 'secondary': selectedAlertState !== alertState.OPEN}" data-name="open" (click)="processOpen()">OPEN</td> + <td [ngClass]="{'primary': selectedAlertState === alertState.DISMISS, 'secondary': selectedAlertState !== alertState.DISMISS}" data-name="dismiss" (click)="processDismiss()">DISMISS</td> + </tr> + <tr> + <td></td> + <td [ngClass]="{'primary': selectedAlertState === alertState.RESOLVE, 'secondary': selectedAlertState !== alertState.RESOLVE}" data-name="resolve" (click)="processResolve()">RESOLVE</td> + <td></td> + </tr> + </table> + </div> + <div class="ml-1 my-4 form" *ngIf="activeTab === tabs.DETAILS"> + <div *ngFor="let field of alertFields" class="row"> + <div class="col-6 mb-1 key">{{ field }}</div> <div class="col-6 value"> {{ alertSource[field] }} </div> + </div> + </div> + <div *ngIf="activeTab === tabs.COMMENTS" class="my-4"> + <div> Comments <span *ngIf="alertCommentsWrapper.length > 0"> ({{alertCommentsWrapper.length}}) </span></div> + <textarea class="form-control" [(ngModel)]="alertCommentStr"> </textarea> + <button class="btn btn-mine_shaft_2" [disabled]="alertCommentStr.trim().length === 0" (click)="onAddComment()">ADD COMMENT</button> + <ng-container *ngFor="let alertCommentWrapper of alertCommentsWrapper; let i = index"> + <hr> + <div class="comment-container"> + <i class="fa fa-trash-o" aria-hidden="true" (click)="onDeleteComment(i)"></i> + <div class="comment"> {{ alertCommentWrapper.alertComment.comment }} </div> + <div class="font-italic username-timestamp"> - {{ alertCommentWrapper.alertComment.username }} - {{alertCommentWrapper.displayTime}}</div> + </div> + </ng-container> + </div> + </div> </div> </div> </div> http://git-wip-us.apache.org/repos/asf/metron/blob/39bb8567/metron-interface/metron-alerts/src/app/alerts/alert-details/alert-details.component.scss ---------------------------------------------------------------------- diff --git a/metron-interface/metron-alerts/src/app/alerts/alert-details/alert-details.component.scss b/metron-interface/metron-alerts/src/app/alerts/alert-details/alert-details.component.scss index 8407bba..5899140 100644 --- a/metron-interface/metron-alerts/src/app/alerts/alert-details/alert-details.component.scss +++ b/metron-interface/metron-alerts/src/app/alerts/alert-details/alert-details.component.scss @@ -18,8 +18,8 @@ @import "../../../slider"; @import "../../../variables"; -.container-fluid { - padding-top: 15px; +.title-container { + margin: 13px 0px; } .form-title { @@ -70,3 +70,76 @@ color: $dove-grey; } } + +.dialog1x { + width: 400px; +} + +.nav-container { + min-height: 99vh; + width: 54px; + background: $mine-shaft-3; + border-right: 1px solid $dove-grey; + + i { + cursor: pointer; + font-size: 19px; + color: $dusty-grey; + } + + .active i { + color: #FFFFFF; + } + + .nav-link { + padding: 15px 1em; + + &.active { + border-left: 4px solid #31ABDF; + background: $mine-shaft-2; + } + } +} + +.details-container { + width: 340px; +} + +textarea { + margin-top: 5px; + height: 100px; +} + +.btn-mine_shaft_2 { + margin-top: 10px; + margin-bottom: 15px; +} + +.username-timestamp { + color: $rolling-stone; +} + +.comment-container { + font-size: 14px; + padding: 10px 0px; + + i { + right: 25px; + display: none; + cursor: pointer; + font-size: 18px; + position: absolute; + } + + .comment { + padding-right: 25px; + } + + &:hover { + background: $nile-blue; + } +} + +.comment-container:hover i { + display: block; +} http://git-wip-us.apache.org/repos/asf/metron/blob/39bb8567/metron-interface/metron-alerts/src/app/alerts/alert-details/alert-details.component.ts ---------------------------------------------------------------------- diff --git a/metron-interface/metron-alerts/src/app/alerts/alert-details/alert-details.component.ts b/metron-interface/metron-alerts/src/app/alerts/alert-details/alert-details.component.ts index 2efb2ce..3d5b70e 100644 --- a/metron-interface/metron-alerts/src/app/alerts/alert-details/alert-details.component.ts +++ b/metron-interface/metron-alerts/src/app/alerts/alert-details/alert-details.component.ts @@ -17,16 +17,37 @@ */ import { Component, OnInit } from '@angular/core'; import {Router, ActivatedRoute} from '@angular/router'; +import * as moment from 'moment/moment'; + import {SearchService} from '../../service/search.service'; import {UpdateService} from '../../service/update.service'; import {Alert} from '../../model/alert'; import {AlertsService} from '../../service/alerts.service'; import {AlertSource} from '../../model/alert-source'; +import {PatchRequest} from '../../model/patch-request'; +import {Patch} from '../../model/patch'; +import {AlertComment} from './alert-comment'; +import {AuthenticationService} from '../../service/authentication.service'; +import {MetronDialogBox} from '../../shared/metron-dialog-box'; export enum AlertState { NEW, OPEN, ESCALATE, DISMISS, RESOLVE } +export enum Tabs { + DETAILS, COMMENTS +} + +class AlertCommentWrapper { + alertComment: AlertComment; + displayTime: string; + + constructor(alertComment: AlertComment, displayTime: string) { + this.alertComment = alertComment; + this.displayTime = displayTime; + } +} + @Component({ selector: 'app-alert-details', templateUrl: './alert-details.component.html', @@ -36,16 +57,25 @@ export class AlertDetailsComponent implements OnInit { alertId = ''; alertSourceType = ''; + alertIndex = ''; alertState = AlertState; + tabs = Tabs; + activeTab = Tabs.DETAILS; selectedAlertState: AlertState = AlertState.NEW; alertSource: AlertSource = new AlertSource(); alertFields: string[] = []; + alertCommentStr = ''; + alertCommentsWrapper: AlertCommentWrapper[] = []; constructor(private router: Router, private activatedRoute: ActivatedRoute, private searchService: SearchService, private updateService: UpdateService, - private alertsService: AlertsService) { } + private alertsService: AlertsService, + private authenticationService: AuthenticationService, + private metronDialogBox: MetronDialogBox) { + + } goBack() { this.router.navigateByUrl('/alerts-list'); @@ -53,13 +83,22 @@ export class AlertDetailsComponent implements OnInit { } getData() { + this.alertCommentStr = ''; this.searchService.getAlert(this.alertSourceType, this.alertId).subscribe(alert => { this.alertSource = alert; - this.alertFields = Object.keys(alert).filter(field => !field.includes(':ts') && field !== 'original_string').sort(); + this.alertFields = Object.keys(alert).filter(field => !field.includes(':ts') && field !== 'original_string' && field !== 'comments') + .sort(); this.selectedAlertState = this.getAlertState(alert['alert_status']); + this.setComments(alert); }); } + setComments(alert) { + let alertComments = alert['comments'] ? alert['comments'] : []; + this.alertCommentsWrapper = alertComments.map(alertComment => + new AlertCommentWrapper(alertComment, moment(new Date(alertComment.timestamp)).fromNow())); + } + getAlertState(alertStatus) { if (alertStatus === 'OPEN') { return AlertState.OPEN; @@ -78,9 +117,10 @@ export class AlertDetailsComponent implements OnInit { this.activatedRoute.params.subscribe(params => { this.alertId = params['guid']; this.alertSourceType = params['sourceType']; + this.alertIndex = params['index']; this.getData(); }); - } + }; processOpen() { let tAlert = new Alert(); @@ -133,6 +173,40 @@ export class AlertDetailsComponent implements OnInit { }); } + onAddComment() { + let alertComment = new AlertComment(this.alertCommentStr, this.authenticationService.getCurrentUserName(), new Date().getTime()); + let tAlertComments = this.alertCommentsWrapper.map(alertsWrapper => alertsWrapper.alertComment); + tAlertComments.unshift(alertComment); + this.patchAlert(new Patch('add', '/comments', tAlertComments)); + } + + patchAlert(patch: Patch) { + let patchRequest = new PatchRequest(); + patchRequest.guid = this.alertSource.guid; + patchRequest.index = this.alertIndex; + patchRequest.patch = [patch]; + patchRequest.sensorType = this.alertSourceType; + + this.updateService.patch(patchRequest).subscribe(() => { + this.getData(); + }); + } + + onDeleteComment(index: number) { + let commentText = 'Do you wish to delete the comment '; + if (this.alertCommentsWrapper[index].alertComment.comment.length > 25 ) { + commentText += ' \'' + this.alertCommentsWrapper[index].alertComment.comment.substr(0, 25) + '...\''; + } else { + commentText += ' \'' + this.alertCommentsWrapper[index].alertComment.comment + '\''; + } + + this.metronDialogBox.showConfirmationMessage(commentText).subscribe(response => { + if (response) { + this.alertCommentsWrapper.splice(index, 1); + this.patchAlert(new Patch('add', '/comments', this.alertCommentsWrapper.map(alertsWrapper => alertsWrapper.alertComment))); + } + }); + } } http://git-wip-us.apache.org/repos/asf/metron/blob/39bb8567/metron-interface/metron-alerts/src/app/alerts/alert-details/alerts-details.module.ts ---------------------------------------------------------------------- diff --git a/metron-interface/metron-alerts/src/app/alerts/alert-details/alerts-details.module.ts b/metron-interface/metron-alerts/src/app/alerts/alert-details/alerts-details.module.ts index 9b4927e..d1e36a6 100644 --- a/metron-interface/metron-alerts/src/app/alerts/alert-details/alerts-details.module.ts +++ b/metron-interface/metron-alerts/src/app/alerts/alert-details/alerts-details.module.ts @@ -20,11 +20,11 @@ import {routing} from './alerts-details.routing'; import {SharedModule} from '../../shared/shared.module'; import {AlertDetailsComponent} from './alert-details.component'; import {AlertsService} from '../../service/alerts.service'; -import {UpdateService} from '../../service/update.service'; +import {AuthenticationService} from '../../service/authentication.service'; @NgModule ({ imports: [ routing, SharedModule], declarations: [ AlertDetailsComponent ], - providers: [ AlertsService, UpdateService ], + providers: [ AuthenticationService, AlertsService ] }) export class AlertDetailsModule { } http://git-wip-us.apache.org/repos/asf/metron/blob/39bb8567/metron-interface/metron-alerts/src/app/alerts/alert-details/alerts-details.routing.ts ---------------------------------------------------------------------- diff --git a/metron-interface/metron-alerts/src/app/alerts/alert-details/alerts-details.routing.ts b/metron-interface/metron-alerts/src/app/alerts/alert-details/alerts-details.routing.ts index bc4dd5b..0cb9c9c 100644 --- a/metron-interface/metron-alerts/src/app/alerts/alert-details/alerts-details.routing.ts +++ b/metron-interface/metron-alerts/src/app/alerts/alert-details/alerts-details.routing.ts @@ -20,5 +20,5 @@ import { RouterModule } from '@angular/router'; import {AlertDetailsComponent} from './alert-details.component'; export const routing: ModuleWithProviders = RouterModule.forChild([ - { path: 'details/:sourceType/:guid', component: AlertDetailsComponent, outlet: 'dialog'} + { path: 'details/:sourceType/:guid/:index', component: AlertDetailsComponent, outlet: 'dialog'} ]); http://git-wip-us.apache.org/repos/asf/metron/blob/39bb8567/metron-interface/metron-alerts/src/app/alerts/alerts-list/alerts-list.component.ts ---------------------------------------------------------------------- diff --git a/metron-interface/metron-alerts/src/app/alerts/alerts-list/alerts-list.component.ts b/metron-interface/metron-alerts/src/app/alerts/alerts-list/alerts-list.component.ts index 189c9ba..46b7796 100644 --- a/metron-interface/metron-alerts/src/app/alerts/alerts-list/alerts-list.component.ts +++ b/metron-interface/metron-alerts/src/app/alerts/alerts-list/alerts-list.component.ts @@ -314,10 +314,11 @@ export class AlertsListComponent implements OnInit, OnDestroy { } showDetails(alert: Alert) { + let url = '/alerts-list(dialog:details/' + alert.source['source:type'] + '/' + alert.source.guid + '/' + alert.index + ')'; this.selectedAlerts = []; this.selectedAlerts = [alert]; this.saveRefreshState(); - this.router.navigateByUrl('/alerts-list(dialog:details/' + alert.source['source:type'] + '/' + alert.source.guid + ')'); + this.router.navigateByUrl(url); } saveRefreshState() { @@ -367,7 +368,7 @@ export class AlertsListComponent implements OnInit, OnDestroy { updateAlert(patchRequest: PatchRequest) { this.searchService.getAlert(patchRequest.sensorType, patchRequest.guid).subscribe(alertSource => { - this.alerts.filter(alert => alert.source.guid == patchRequest.guid) + this.alerts.filter(alert => alert.source.guid === patchRequest.guid) .map(alert => alert.source = alertSource); }); } @@ -375,7 +376,7 @@ export class AlertsListComponent implements OnInit, OnDestroy { updateSelectedAlertStatus(status: string) { for (let selectedAlert of this.selectedAlerts) { selectedAlert.status = status; - this.alerts.filter(alert => alert.source.guid == selectedAlert.source.guid) + this.alerts.filter(alert => alert.source.guid === selectedAlert.source.guid) .map(alert => alert.source['alert_status'] = status); } this.selectedAlerts = []; http://git-wip-us.apache.org/repos/asf/metron/blob/39bb8567/metron-interface/metron-alerts/src/app/alerts/alerts-list/table-view/table-view.component.html ---------------------------------------------------------------------- diff --git a/metron-interface/metron-alerts/src/app/alerts/alerts-list/table-view/table-view.component.html b/metron-interface/metron-alerts/src/app/alerts/alerts-list/table-view/table-view.component.html index ae788fc..561b299 100644 --- a/metron-interface/metron-alerts/src/app/alerts/alerts-list/table-view/table-view.component.html +++ b/metron-interface/metron-alerts/src/app/alerts/alerts-list/table-view/table-view.component.html @@ -17,6 +17,7 @@ <tr> <th style="width:55px"> <metron-config-sorter [type]="'number'" [sortBy]="threatScoreFieldName"> Score </metron-config-sorter> </th> <th *ngFor="let column of alertsColumnsToDisplay" [id]="column.name"> <metron-config-sorter [type]="column.type" [sortBy]="column.name" title="{{column.name}}"> {{ column.name | columnNameTranslate | centerEllipses:15 }}</metron-config-sorter> </th> + <th></th> <th style="width:25px"><i class="fa fa-cog configure-table-icon" (click)="showConfigureTable()"></i></th> <th style="width:25px"><input id="select-deselect-all" class="fontawesome-checkbox" type="checkbox" (click)="selectAllRows($event)"><label for="select-deselect-all"></label></th> </tr> @@ -29,7 +30,8 @@ <td *ngFor="let column of alertsColumnsToDisplay" #cell> <a (click)="addFilter(column.name, getValue(alert, column, false))" title="{{getValue(alert, column, true)}}" style="color:#689AA9">{{ getValue(alert, column, true) | centerEllipses:20:cell }}</a> </td> - <td></td> + <td> <i class="fa fa-comments-o" aria-hidden="true" *ngIf="alert.source.comments && alert.source.comments.length > 0"></i> </td> + <td> </td> <td><input id="{{ alert.id }}" class="fontawesome-checkbox" type="checkbox" name="{{alert.id}}" (click)="selectRow($event, alert)" [checked]="selectedAlerts.indexOf(alert) != -1"><label attr.for="{{ alert.id }}"></label></td> </tr> </tbody> http://git-wip-us.apache.org/repos/asf/metron/blob/39bb8567/metron-interface/metron-alerts/src/app/login/login.component.html ---------------------------------------------------------------------- diff --git a/metron-interface/metron-alerts/src/app/login/login.component.html b/metron-interface/metron-alerts/src/app/login/login.component.html index 5ae746f..edf0c8d 100644 --- a/metron-interface/metron-alerts/src/app/login/login.component.html +++ b/metron-interface/metron-alerts/src/app/login/login.component.html @@ -20,8 +20,8 @@ <input class="form-control" name="user" [(ngModel)]="user" required autofocus> <label class="label"> PASSWORD </label> <input type="password" name="password" class="form-control" [(ngModel)]="password" required> - <div class="my-4" style="color:#a94442"> - {{loginFailure}} + <div class="my-4"> + <div class="login-failed-msg" *ngIf="loginFailure.length > 0">{{loginFailure}}</div> <button class="btn btn-primary pull-right" type="submit" (click)="login()">LOG IN</button> </div> </form> http://git-wip-us.apache.org/repos/asf/metron/blob/39bb8567/metron-interface/metron-alerts/src/app/login/login.component.scss ---------------------------------------------------------------------- diff --git a/metron-interface/metron-alerts/src/app/login/login.component.scss b/metron-interface/metron-alerts/src/app/login/login.component.scss index b8d5416..d11c433 100644 --- a/metron-interface/metron-alerts/src/app/login/login.component.scss +++ b/metron-interface/metron-alerts/src/app/login/login.component.scss @@ -53,3 +53,7 @@ width: 390px; height: 120px; } + +.login-failed-msg { + color: $apple-blossom; +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/metron/blob/39bb8567/metron-interface/metron-alerts/src/app/login/login.component.ts ---------------------------------------------------------------------- diff --git a/metron-interface/metron-alerts/src/app/login/login.component.ts b/metron-interface/metron-alerts/src/app/login/login.component.ts index 1d6c24f..9b6d66c 100644 --- a/metron-interface/metron-alerts/src/app/login/login.component.ts +++ b/metron-interface/metron-alerts/src/app/login/login.component.ts @@ -28,7 +28,7 @@ export class LoginComponent { user: string; password: string; - loginFailure: string = ''; + loginFailure = ''; constructor(private authenticationService: AuthenticationService, private activatedRoute: ActivatedRoute) { this.activatedRoute.queryParams.subscribe(params => { http://git-wip-us.apache.org/repos/asf/metron/blob/39bb8567/metron-interface/metron-alerts/src/app/model/alert-source.ts ---------------------------------------------------------------------- diff --git a/metron-interface/metron-alerts/src/app/model/alert-source.ts b/metron-interface/metron-alerts/src/app/model/alert-source.ts index cbf83d3..4e3a655 100644 --- a/metron-interface/metron-alerts/src/app/model/alert-source.ts +++ b/metron-interface/metron-alerts/src/app/model/alert-source.ts @@ -15,6 +15,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import {AlertComment} from '../alerts/alert-details/alert-comment'; + export class AlertSource { msg: string; sig_rev: number; @@ -42,6 +44,7 @@ export class AlertSource { guid: string; sig_id: number; sig_generator: number; + comments: AlertComment[] = []; 'threat:triage:score': number; 'threatinteljoinbolt:joiner:ts': number; 'enrichmentsplitterbolt:splitter:begin:ts': number; http://git-wip-us.apache.org/repos/asf/metron/blob/39bb8567/metron-interface/metron-alerts/src/app/model/alert.ts ---------------------------------------------------------------------- diff --git a/metron-interface/metron-alerts/src/app/model/alert.ts b/metron-interface/metron-alerts/src/app/model/alert.ts index 59843a0..5a4e73b 100644 --- a/metron-interface/metron-alerts/src/app/model/alert.ts +++ b/metron-interface/metron-alerts/src/app/model/alert.ts @@ -21,4 +21,5 @@ export class Alert { score: number; source: AlertSource; status: string; + index: string; } http://git-wip-us.apache.org/repos/asf/metron/blob/39bb8567/metron-interface/metron-alerts/src/app/model/patch-request.ts ---------------------------------------------------------------------- diff --git a/metron-interface/metron-alerts/src/app/model/patch-request.ts b/metron-interface/metron-alerts/src/app/model/patch-request.ts index 8e984ab..79db2d7 100644 --- a/metron-interface/metron-alerts/src/app/model/patch-request.ts +++ b/metron-interface/metron-alerts/src/app/model/patch-request.ts @@ -15,10 +15,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import {Patch} from './patch'; + export class PatchRequest { guid: string; sensorType: string; index: string; - patch: {}; + patch: Patch[] = []; source: {}; -} \ No newline at end of file +} http://git-wip-us.apache.org/repos/asf/metron/blob/39bb8567/metron-interface/metron-alerts/src/app/model/patch.ts ---------------------------------------------------------------------- diff --git a/metron-interface/metron-alerts/src/app/model/patch.ts b/metron-interface/metron-alerts/src/app/model/patch.ts new file mode 100644 index 0000000..4ce29f1 --- /dev/null +++ b/metron-interface/metron-alerts/src/app/model/patch.ts @@ -0,0 +1,28 @@ +/** + * 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. + */ +export class Patch { + op: 'add' | 'remove'; + path: string; + value: any; + + constructor(op, path: string, value: any) { + this.op = op; + this.path = path; + this.value = value; + } +} http://git-wip-us.apache.org/repos/asf/metron/blob/39bb8567/metron-interface/metron-alerts/src/app/service/authentication.service.ts ---------------------------------------------------------------------- diff --git a/metron-interface/metron-alerts/src/app/service/authentication.service.ts b/metron-interface/metron-alerts/src/app/service/authentication.service.ts index f4924a5..e66725d 100644 --- a/metron-interface/metron-alerts/src/app/service/authentication.service.ts +++ b/metron-interface/metron-alerts/src/app/service/authentication.service.ts @@ -81,6 +81,10 @@ export class AuthenticationService { return this.http.get(this.loginUrl, options); } + public getCurrentUserName(): string { + return this.currentUser; + } + public isAuthenticationChecked(): boolean { return this.currentUser !== AuthenticationService.USER_NOT_VERIFIED; } http://git-wip-us.apache.org/repos/asf/metron/blob/39bb8567/metron-interface/metron-alerts/src/styles.scss ---------------------------------------------------------------------- diff --git a/metron-interface/metron-alerts/src/styles.scss b/metron-interface/metron-alerts/src/styles.scss index 12ac9f7..e3a0622 100644 --- a/metron-interface/metron-alerts/src/styles.scss +++ b/metron-interface/metron-alerts/src/styles.scss @@ -235,10 +235,18 @@ form color: $piction-blue; font-size: 14px; min-width: 85px; - width: 85px; cursor: pointer; &:focus { outline: none; } +} + +hr { + display: block; + height: 1px; + border: 0; + border-top: 1px solid $tundora-1; + margin: 0.3rem 0; + padding: 0; } \ No newline at end of file
