You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@metron.apache.org by sa...@apache.org on 2019/05/16 13:14:09 UTC

[metron] branch master updated: METRON-1997 Replace Threat Triage Score Field Slider with Text Box (ruffle1986 via sardell) closes apache/metron#1334

This is an automated email from the ASF dual-hosted git repository.

sardell pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/metron.git


The following commit(s) were added to refs/heads/master by this push:
     new fcd681d  METRON-1997 Replace Threat Triage Score Field Slider with Text Box (ruffle1986 via sardell) closes apache/metron#1334
fcd681d is described below

commit fcd681d10256c3cc44722cff694d08649ea27673
Author: ruffle1986 <ft...@gmail.com>
AuthorDate: Thu May 16 15:13:25 2019 +0200

    METRON-1997 Replace Threat Triage Score Field Slider with Text Box (ruffle1986 via sardell) closes apache/metron#1334
---
 .../metron-config/src/app/model/risk-level-rule.ts |   8 +-
 ...sensor-parser-config-readonly.component.spec.ts |   8 +-
 .../rule-editor/sensor-rule-editor.component.html  |  50 +++++--
 .../rule-editor/sensor-rule-editor.component.scss  |  21 +++
 .../sensor-rule-editor.component.spec.ts           |  90 +++++++++++--
 .../rule-editor/sensor-rule-editor.component.ts    |  22 +++-
 .../rule-editor/sensor-rule-editor.module.ts       |   3 +-
 .../sensor-threat-triage.component.html            |  42 ++----
 .../sensor-threat-triage.component.scss            |  63 +++------
 .../sensor-threat-triage.component.spec.ts         | 145 +--------------------
 .../sensor-threat-triage.component.ts              | 105 +--------------
 .../app/shared/ace-editor/ace-editor.component.ts  |  24 +++-
 .../src/assets/ace/mode-javascript.js              |   1 +
 13 files changed, 223 insertions(+), 359 deletions(-)

diff --git a/metron-interface/metron-config/src/app/model/risk-level-rule.ts b/metron-interface/metron-config/src/app/model/risk-level-rule.ts
index 1cd4e8a..5d728d4 100644
--- a/metron-interface/metron-config/src/app/model/risk-level-rule.ts
+++ b/metron-interface/metron-config/src/app/model/risk-level-rule.ts
@@ -16,8 +16,8 @@
  * limitations under the License.
  */
 export class RiskLevelRule {
-  name: string = '';
-  comment: string = '';
-  rule: string = '';
-  score: number = 0;
+  name = '';
+  comment = '';
+  rule = '';
+  score = '';
 }
diff --git a/metron-interface/metron-config/src/app/sensors/sensor-parser-config-readonly/sensor-parser-config-readonly.component.spec.ts b/metron-interface/metron-config/src/app/sensors/sensor-parser-config-readonly/sensor-parser-config-readonly.component.spec.ts
index 1a027f5..a6b540f 100644
--- a/metron-interface/metron-config/src/app/sensors/sensor-parser-config-readonly/sensor-parser-config-readonly.component.spec.ts
+++ b/metron-interface/metron-config/src/app/sensors/sensor-parser-config-readonly/sensor-parser-config-readonly.component.spec.ts
@@ -462,13 +462,13 @@ describe('Component: SensorParserConfigReadonly', () => {
         riskLevelRules: [
           {
             rule: "IN_SUBNET(ip_dst_addr, '192.168.0.0/24')",
-            score: 3,
+            score: '3',
             name: 'test1',
             comment: 'This is a comment'
           },
           {
             rule: "user.type in [ 'admin', 'power' ] and asset.type == 'web'",
-            score: 3,
+            score: '3',
             name: 'test2',
             comment: 'This is another comment'
           }
@@ -480,13 +480,13 @@ describe('Component: SensorParserConfigReadonly', () => {
     let expected: RiskLevelRule[] = [
       {
         rule: "IN_SUBNET(ip_dst_addr, '192.168.0.0/24')",
-        score: 3,
+        score: '3',
         name: 'test1',
         comment: 'This is a comment'
       },
       {
         rule: "user.type in [ 'admin', 'power' ] and asset.type == 'web'",
-        score: 3,
+        score: '3',
         name: 'test2',
         comment: 'This is another comment'
       }
diff --git a/metron-interface/metron-config/src/app/sensors/sensor-threat-triage/rule-editor/sensor-rule-editor.component.html b/metron-interface/metron-config/src/app/sensors/sensor-threat-triage/rule-editor/sensor-rule-editor.component.html
index 512a990..21d0552 100644
--- a/metron-interface/metron-config/src/app/sensors/sensor-threat-triage/rule-editor/sensor-rule-editor.component.html
+++ b/metron-interface/metron-config/src/app/sensors/sensor-threat-triage/rule-editor/sensor-rule-editor.component.html
@@ -25,24 +25,56 @@
           <input type="text" class="form-control" name="ruleName" [(ngModel)]="newRiskLevelRule.name">
         </div>
         <div class="form-group">
-            <label attr.for="statement">TEXT</label>
-            <textarea rows="30" class="form-control" name="ruleValue" [(ngModel)]="newRiskLevelRule.rule" style="height: inherit"></textarea>
+            <label attr.for="statement">RULE</label>
+            <metron-config-ace-editor
+              name="ruleValue"
+              [(ngModel)]="newRiskLevelRule.rule"
+              [liveAutocompletion]="false"
+              [enableSnippets]="false"
+              [useWorker]="false"
+              [type]="'STELLAR'"
+              (onChange)="onRuleChange()"
+              [placeHolder]="'Enter a valid Stellar expression'"
+              [wrapLimitRangeMin]="null"
+              [wrapLimitRangeMax]="null"
+              data-qe-id="score-editor"
+            >
+            </metron-config-ace-editor>
+            <span *ngIf="!isRuleValid" class="warning-text">Invalid Stellar expression</span>
         </div>
     </form>
     <form class="form-inline">
-        <div class="form-group">
-            <label attr.for="statement" style="width: 100%">SCORE ADJUSTMENT</label>
-            <input class="score-slider" name="scoreSlider" type="range" min="0" max="100" [(ngModel)]="newRiskLevelRule.score" style="width: 72%; margin-right: 4px">
-            <div style="width: 25%; display: inline-block">
-                <metron-config-number-spinner name="scoreText" [(ngModel)]="newRiskLevelRule.score" [min]="0" [max]="100"> </metron-config-number-spinner>
-            </div>
+        <div class="form-group score">
+            <label class="score-label" attr.for="statement">SCORE ADJUSTMENT</label>
+            <metron-config-ace-editor
+              name="scoreExpression"
+              [(ngModel)]="newRiskLevelRule.score"
+              [liveAutocompletion]="false"
+              [enableSnippets]="false"
+              [useWorker]="false"
+              [type]="'STELLAR'"
+              (onChange)="onScoreChange()"
+              [placeHolder]="'Enter a valid Stellar expression'"
+              [wrapLimitRangeMin]="null"
+              [wrapLimitRangeMax]="null"
+              data-qe-id="score-editor"
+            >
+            </metron-config-ace-editor>
+            <span *ngIf="!isScoreValid" class="warning-text">Invalid Stellar expression</span>
         </div>
     </form>
 
     <div class="form-group">
         <div class="form-seperator-edit"></div>
         <div class="button-row">
-            <button type="submit" class="btn form-enable-disable-button" (click)="onSave()">SAVE</button>
+            <button
+              type="submit"
+              class="btn form-enable-disable-button"
+              (click)="onSave()"
+              data-qe-id="save-score"
+            >
+              SAVE
+            </button>
             <button class="btn form-enable-disable-button ml-1" (click)="onCancel()" >CANCEL</button>
         </div>
     </div>
diff --git a/metron-interface/metron-config/src/app/sensors/sensor-threat-triage/rule-editor/sensor-rule-editor.component.scss b/metron-interface/metron-config/src/app/sensors/sensor-threat-triage/rule-editor/sensor-rule-editor.component.scss
index ca6c7d7..19d92c5 100644
--- a/metron-interface/metron-config/src/app/sensors/sensor-threat-triage/rule-editor/sensor-rule-editor.component.scss
+++ b/metron-interface/metron-config/src/app/sensors/sensor-threat-triage/rule-editor/sensor-rule-editor.component.scss
@@ -88,3 +88,24 @@ input[type=range]::-moz-range-thumb
   background-color: $range-gradient-start;
 }
 
+.score {
+
+  &.form-group {
+    display: block;
+    width: 100%;
+  }
+  .score-label {
+    display: block;
+    margin-bottom: 5px;
+  }
+  .score-input {
+    width: 100%;
+  }
+}
+
+.warning-text {
+  color: #C0661D;
+  font-size: 12px;
+  font-family: Roboto-Medium;
+}
+
diff --git a/metron-interface/metron-config/src/app/sensors/sensor-threat-triage/rule-editor/sensor-rule-editor.component.spec.ts b/metron-interface/metron-config/src/app/sensors/sensor-threat-triage/rule-editor/sensor-rule-editor.component.spec.ts
index 2f8cd24..d26a526 100644
--- a/metron-interface/metron-config/src/app/sensors/sensor-threat-triage/rule-editor/sensor-rule-editor.component.spec.ts
+++ b/metron-interface/metron-config/src/app/sensors/sensor-threat-triage/rule-editor/sensor-rule-editor.component.spec.ts
@@ -16,29 +16,44 @@
  * limitations under the License.
  */
 
-import {async, TestBed, ComponentFixture} from '@angular/core/testing';
+import {async, TestBed, ComponentFixture, inject} from '@angular/core/testing';
 import {SensorRuleEditorComponent} from './sensor-rule-editor.component';
 import {SharedModule} from '../../../shared/shared.module';
 import {NumberSpinnerComponent} from '../../../shared/number-spinner/number-spinner.component';
 import {RiskLevelRule} from '../../../model/risk-level-rule';
+import {AceEditorModule} from '../../../shared/ace-editor/ace-editor.module';
+import {StellarService} from '../../../service/stellar.service';
+import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
+import { AppConfigService } from 'app/service/app-config.service';
+import { By } from '@angular/platform-browser';
+import { Observable } from 'rxjs';
 
 describe('Component: SensorRuleEditorComponent', () => {
 
     let fixture: ComponentFixture<SensorRuleEditorComponent>;
     let component: SensorRuleEditorComponent;
+    let stellarService: StellarService;
 
     beforeEach(async(() => {
         TestBed.configureTestingModule({
-            imports: [SharedModule
-            ],
+            imports: [SharedModule, AceEditorModule, HttpClientTestingModule],
             declarations: [ SensorRuleEditorComponent, NumberSpinnerComponent ],
             providers: [
-              SensorRuleEditorComponent
-            ]
+              SensorRuleEditorComponent,
+              StellarService,
+              {
+                provide: AppConfigService,
+                useValue: {
+                  appConfigStatic: {},
+                  getApiRoot: () => '/api/v1'
+                }
+            }]
         });
 
         fixture = TestBed.createComponent(SensorRuleEditorComponent);
         component = fixture.componentInstance;
+        stellarService = TestBed.get(StellarService);
+        fixture.detectChanges();
     }));
 
     it('should create an instance', () => {
@@ -48,6 +63,17 @@ describe('Component: SensorRuleEditorComponent', () => {
     it('should edit rules', async(() => {
         let numCancelled = 0;
         let savedRule = new RiskLevelRule();
+
+        stellarService.validateRules = (expressions: string[]) => {
+          return new Observable((observer) => {
+            const response = {
+              [expressions[0]]: true,
+              [expressions[1]]: true,
+            };
+            observer.next(response);
+          });
+        }
+
         component.onCancelTextEditor.subscribe((cancelled: boolean) => {
           numCancelled++;
         });
@@ -55,20 +81,66 @@ describe('Component: SensorRuleEditorComponent', () => {
           savedRule = rule;
         });
 
-        component.riskLevelRule =  {name: 'rule1', rule: 'initial rule', score: 1, comment: ''};
+        component.riskLevelRule =  {name: 'rule1', rule: 'initial rule', score: '1', comment: ''};
         component.ngOnInit();
         component.onSave();
-        let rule1 = Object.assign(new RiskLevelRule(), {name: 'rule1', rule: 'initial rule', score: 1, comment: ''});
+        let rule1 = Object.assign(new RiskLevelRule(), {name: 'rule1', rule: 'initial rule', score: '1', comment: ''});
         expect(savedRule).toEqual(rule1);
 
-        component.riskLevelRule = {name: 'rule2', rule: 'new rule', score: 2, comment: ''};
+        component.riskLevelRule = {name: 'rule2', rule: 'new rule', score: '2', comment: ''};
         component.ngOnInit();
         component.onSave();
-        let rule2 = Object.assign(new RiskLevelRule(), {name: 'rule2', rule: 'new rule', score: 2, comment: ''});
+        let rule2 = Object.assign(new RiskLevelRule(), {name: 'rule2', rule: 'new rule', score: '2', comment: ''});
         expect(savedRule).toEqual(rule2);
 
         expect(numCancelled).toEqual(0);
         component.onCancel();
         expect(numCancelled).toEqual(1);
     }));
+
+    it('should warn if either the rule or the score is invalid', inject(
+      [HttpTestingController],
+      (httpMock: HttpTestingController) => {
+        const saveButton = fixture.debugElement.query(By.css('[data-qe-id="save-score"]'));
+
+        component.newRiskLevelRule.rule = 'value > 10';
+        component.newRiskLevelRule.score = 'value &&&&';
+        fixture.detectChanges();
+        saveButton.nativeElement.click();
+        let validateRequest = httpMock.expectOne('/api/v1/stellar/validate/rules');
+        validateRequest.flush({
+          [component.newRiskLevelRule.rule]: true,
+          [component.newRiskLevelRule.score]: false
+        });
+        fixture.detectChanges();
+        let warning = fixture.debugElement.queryAll(By.css('.warning-text'));
+        expect(warning.length).toBe(1);
+
+        component.newRiskLevelRule.rule = 'value > 10';
+        component.newRiskLevelRule.score = 'value * 10';
+        fixture.detectChanges();
+        saveButton.nativeElement.click();
+        validateRequest = httpMock.expectOne('/api/v1/stellar/validate/rules');
+        validateRequest.flush({
+          [component.newRiskLevelRule.score]: true,
+          [component.newRiskLevelRule.rule]: true
+        });
+        fixture.detectChanges();
+        warning = fixture.debugElement.queryAll(By.css('.warning-text'));
+        expect(warning.length).toBe(0);
+
+        component.newRiskLevelRule.rule = 'value &&&&';
+        component.newRiskLevelRule.score = 'value &&& &&&&';
+        fixture.detectChanges();
+        saveButton.nativeElement.click();
+        validateRequest = httpMock.expectOne('/api/v1/stellar/validate/rules');
+        validateRequest.flush({
+          [component.newRiskLevelRule.score]: false,
+          [component.newRiskLevelRule.rule]: false
+        });
+        fixture.detectChanges();
+        warning = fixture.debugElement.queryAll(By.css('.warning-text'));
+        expect(warning.length).toBe(2);
+      }
+    ));
 });
diff --git a/metron-interface/metron-config/src/app/sensors/sensor-threat-triage/rule-editor/sensor-rule-editor.component.ts b/metron-interface/metron-config/src/app/sensors/sensor-threat-triage/rule-editor/sensor-rule-editor.component.ts
index 1bdfea1..260e1b0 100644
--- a/metron-interface/metron-config/src/app/sensors/sensor-threat-triage/rule-editor/sensor-rule-editor.component.ts
+++ b/metron-interface/metron-config/src/app/sensors/sensor-threat-triage/rule-editor/sensor-rule-editor.component.ts
@@ -17,6 +17,7 @@
  */
 import {Component, Input, EventEmitter, Output, OnInit} from '@angular/core';
 import {RiskLevelRule} from '../../../model/risk-level-rule';
+import {StellarService} from '../../../service/stellar.service';
 
 @Component({
   selector: 'metron-config-sensor-rule-editor',
@@ -31,19 +32,36 @@ export class SensorRuleEditorComponent implements OnInit {
   @Output() onCancelTextEditor: EventEmitter<boolean> = new EventEmitter<boolean>();
   @Output() onSubmitTextEditor: EventEmitter<RiskLevelRule> = new EventEmitter<RiskLevelRule>();
   newRiskLevelRule = new RiskLevelRule();
+  isScoreValid = true;
+  isRuleValid = true;
 
-  constructor() { }
+  constructor(private stellarService: StellarService) { }
 
   ngOnInit() {
     Object.assign(this.newRiskLevelRule, this.riskLevelRule);
   }
 
   onSave(): void {
-    this.onSubmitTextEditor.emit(this.newRiskLevelRule);
+    const score = this.newRiskLevelRule.score;
+    const rule = this.newRiskLevelRule.rule;
+    this.stellarService.validateRules([rule, score]).subscribe((response) => {
+      this.isScoreValid = !!response[score];
+      this.isRuleValid = !!response[rule];
+      if (this.isRuleValid && this.isScoreValid) {
+        this.onSubmitTextEditor.emit(this.newRiskLevelRule);
+      }
+    });
   }
 
   onCancel(): void {
     this.onCancelTextEditor.emit(true);
   }
 
+  onRuleChange = () => {
+    this.isRuleValid = true;
+  }
+
+  onScoreChange = () => {
+    this.isScoreValid = true;
+  }
 }
diff --git a/metron-interface/metron-config/src/app/sensors/sensor-threat-triage/rule-editor/sensor-rule-editor.module.ts b/metron-interface/metron-config/src/app/sensors/sensor-threat-triage/rule-editor/sensor-rule-editor.module.ts
index a99c8cf..16b059f 100644
--- a/metron-interface/metron-config/src/app/sensors/sensor-threat-triage/rule-editor/sensor-rule-editor.module.ts
+++ b/metron-interface/metron-config/src/app/sensors/sensor-threat-triage/rule-editor/sensor-rule-editor.module.ts
@@ -19,9 +19,10 @@ import { NgModule } from '@angular/core';
 import {SharedModule} from '../../../shared/shared.module';
 import {SensorRuleEditorComponent} from './sensor-rule-editor.component';
 import {NumberSpinnerModule} from '../../../shared/number-spinner/number-spinner.module';
+import {AceEditorModule} from '../../../shared/ace-editor/ace-editor.module';
 
 @NgModule ({
-  imports: [ SharedModule, NumberSpinnerModule ],
+  imports: [ SharedModule, NumberSpinnerModule, AceEditorModule ],
   declarations: [ SensorRuleEditorComponent ],
   exports: [ SensorRuleEditorComponent ]
 })
diff --git a/metron-interface/metron-config/src/app/sensors/sensor-threat-triage/sensor-threat-triage.component.html b/metron-interface/metron-config/src/app/sensors/sensor-threat-triage/sensor-threat-triage.component.html
index 96892f7..c3c72c6 100644
--- a/metron-interface/metron-config/src/app/sensors/sensor-threat-triage/sensor-threat-triage.component.html
+++ b/metron-interface/metron-config/src/app/sensors/sensor-threat-triage/sensor-threat-triage.component.html
@@ -34,47 +34,21 @@
         <div class="threat-triage-summary">
             <div class="form-group">
                 <div class="rules-summary-title">Rules</div>
-                <div class="row mx-0">
-                    <div class="btn" (click)="onFilterChange(threatTriageFilter.HIGH)" [ngClass]="{'filter-button': filter != threatTriageFilter.HIGH, 'filter-button-selected': filter == threatTriageFilter.HIGH}">
-                        <i aria-hidden="true" class="fa fa-circle" style="color: red"></i> {{highAlerts}}
-                    </div>
-                    <div class="btn" (click)="onFilterChange(threatTriageFilter.MEDIUM)" [ngClass]="{'filter-button': filter != threatTriageFilter.MEDIUM, 'filter-button-selected': filter == threatTriageFilter.MEDIUM}">
-                        <i aria-hidden="true" class="fa fa-circle" style="color: orange"></i> {{mediumAlerts}}
-                    </div>
-                    <div class="btn" (click)="onFilterChange(threatTriageFilter.LOW)" [ngClass]="{'filter-button': filter != threatTriageFilter.LOW, 'filter-button-selected': filter == threatTriageFilter.LOW}">
-                        <i aria-hidden="true" class="fa fa-circle" style="color: khaki"></i> {{lowAlerts}}
-                    </div>
-                </div>
-                <div class="row mx-0 threat-triage-rules-sort">
-                    <span class="label">Sort by </span>
-                    <li class="nav-item dropdown">
-                        <span class="nav-link dropdown-toggle" data-toggle="dropdown" href="#" role="button" aria-haspopup="true" aria-expanded="false">{{sortOrderOption[sortOrder].replace('_', ' ')}}</span>
-                        <div class="dropdown-menu bg-inverse">
-                            <span class="dropdown-item" (click)="onSortOrderChange(sortOrderOption.Highest_Score)">Highest Score</span>
-                            <span class="dropdown-item" (click)="onSortOrderChange(sortOrderOption.Lowest_Score)">Lowest Score</span>
-                            <span class="dropdown-item" (click)="onSortOrderChange(sortOrderOption.Highest_Name)">Highest Name</span>
-                            <span class="dropdown-item" (click)="onSortOrderChange(sortOrderOption.Lowest_Name)">Lowest Name</span>
-                        </div>
-                    </li>
-                </div>
             </div>
         </div>
         <div class="form-group threat-triage-rules-list">
-            <div *ngFor="let riskLevelRule of this.visibleRules">
-                <div class="row mx-0 py-0">
-                    <div class="threat-triage-rule-row" style="color: khaki; font-size: 30px; margin-top: -7px" [style.color]="getRuleColor(riskLevelRule)">
-                        <b>I</b>
-                    </div>
-                    <div class="threat-triage-rule-row" style="font-size: small; width: 8%">
+            <div class="threat-triage-rule" *ngFor="let riskLevelRule of this.visibleRules">
+                <div class="row mx-0 py-0 threat-triage-rule-wrapper">
+                    <div class="threat-triage-rule-row" style="font-size: small;" [title]="riskLevelRule.score">
                         {{ riskLevelRule.score }}
                     </div>
-                    <div class="threat-triage-rule-row threat-triage-rule-str">
+                    <div class="threat-triage-rule-row threat-triage-rule-str" [title]="getDisplayName(riskLevelRule)">
                         {{ getDisplayName(riskLevelRule) }}
                     </div>
-                    <div class="threat-triage-rule-row" style=""><i class="fa fa-i-cursor" aria-hidden="true"
-                                                                        style="cursor: pointer;" (click)="onEditRule(riskLevelRule)"></i></div>
-
-                    <div class="threat-triage-rule-row" style=""><i  class="fa fa-trash-o" aria-hidden="true" (click)="onDeleteRule(riskLevelRule)" style="cursor: pointer"></i></div>
+                    <div class="threat-triage-rule-row" style="">
+                        <i class="fa fa-i-cursor" aria-hidden="true" (click)="onEditRule(riskLevelRule)"></i>
+                        <i class="fa fa-trash-o" aria-hidden="true" (click)="onDeleteRule(riskLevelRule)"></i>
+                    </div>
                 </div>
                 <div class="form-seperator-edit"></div>
             </div>
diff --git a/metron-interface/metron-config/src/app/sensors/sensor-threat-triage/sensor-threat-triage.component.scss b/metron-interface/metron-config/src/app/sensors/sensor-threat-triage/sensor-threat-triage.component.scss
index bbc17a0..e2ce39b 100644
--- a/metron-interface/metron-config/src/app/sensors/sensor-threat-triage/sensor-threat-triage.component.scss
+++ b/metron-interface/metron-config/src/app/sensors/sensor-threat-triage/sensor-threat-triage.component.scss
@@ -46,49 +46,11 @@ textarea
   padding-bottom: 5px;
 }
 
-.filter-button
-{
-  width: 32%;
-  background: $field-background;
-  border: 1px solid $form-button-border;
-  border-radius: .25em;
-  cursor: default;
-  color: $nav-active-color;
-  i {
-    font-size: smaller;
-  }
-}
-
-.filter-button-selected
-{
-  @extend .filter-button;
-  background-color: #006ea0;
-  color: #bdbdbd;
-}
-
 .threat-triage-aggregator
 {
   padding-bottom: 10px;
 }
 
-.threat-triage-rules-sort
-{
-  padding-top: 5px;
-  font-size: 12px;
-  font-family: Roboto-Regular;
-  .label
-  {
-    display: inline-block;
-    padding-right: 8px;
-  }
-  .dropdown
-  {
-    list-style: none;
-    display: inline-block;
-    color: $field-button-color;
-  }
-}
-
 .rules-summary-title
 {
   font-size: 15px;
@@ -104,10 +66,26 @@ textarea
   display: inline-block;
   position: relative;
   vertical-align: top;
+  padding: 0 10px;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+
+  &:first-child {
+    padding-left: 0;
+  }
+
+  &:last-child {
+    padding-right: 0;
+    overflow: visible;
+    text-overflow: initial;
+  }
 
   .fa {
     color: $nav-active-color;
     font-size: large;
+    cursor: pointer;
+    margin: 0 3px;
   }
 }
 
@@ -124,14 +102,15 @@ textarea
 
 .threat-triage-rule-str
 {
-  width: 64%;
   padding-right: 10px;
   font-size: small;
-  white-space: nowrap;
-  overflow: hidden;
-  text-overflow: ellipsis;
 }
 metron-config-sensor-rule-editor
 {
   @extend .flexbox-row-reverse;
 }
+
+.threat-triage-rule-wrapper {
+  display: flex;
+  flex-wrap: nowrap;
+}
\ No newline at end of file
diff --git a/metron-interface/metron-config/src/app/sensors/sensor-threat-triage/sensor-threat-triage.component.spec.ts b/metron-interface/metron-config/src/app/sensors/sensor-threat-triage/sensor-threat-triage.component.spec.ts
index 43e8e6b..8702ade 100644
--- a/metron-interface/metron-config/src/app/sensors/sensor-threat-triage/sensor-threat-triage.component.spec.ts
+++ b/metron-interface/metron-config/src/app/sensors/sensor-threat-triage/sensor-threat-triage.component.spec.ts
@@ -18,16 +18,7 @@
 import { SimpleChange, SimpleChanges } from '@angular/core';
 import { HttpClient } from '@angular/common/http';
 import { async, TestBed, ComponentFixture } from '@angular/core/testing';
-import {
-  SensorThreatTriageComponent,
-  SortOrderOption,
-  ThreatTriageFilter
-} from './sensor-threat-triage.component';
-import {
-  SensorEnrichmentConfig,
-  ThreatIntelConfig
-} from '../../model/sensor-enrichment-config';
-import { RiskLevelRule } from '../../model/risk-level-rule';
+import { SensorThreatTriageComponent } from './sensor-threat-triage.component';
 import { SensorEnrichmentConfigService } from '../../service/sensor-enrichment-config.service';
 import { Observable } from 'rxjs';
 import { SensorThreatTriageModule } from './sensor-threat-triage.module';
@@ -99,138 +90,4 @@ describe('Component: SensorThreatTriageComponent', () => {
 
     fixture.destroy();
   }));
-
-  it('should get color', async(() => {
-    let sensorEnrichmentConfig = new SensorEnrichmentConfig();
-    sensorEnrichmentConfig.threatIntel = Object.assign(
-      new ThreatIntelConfig(),
-      {
-        triageConfig: {
-          riskLevelRules: {
-            ruleA: 15,
-            ruleB: 95,
-            ruleC: 50
-          },
-          aggregator: 'MAX',
-          aggregationConfig: {}
-        }
-      }
-    );
-    component.sensorEnrichmentConfig = sensorEnrichmentConfig;
-
-    let ruleA = { name: 'ruleA', rule: 'rule A', score: 15, comment: '' };
-    let ruleB = { name: 'ruleB', rule: 'rule B', score: 95, comment: '' };
-    let ruleC = { name: 'ruleC', rule: 'rule C', score: 50, comment: '' };
-
-    expect(component.getRuleColor(ruleA)).toEqual('khaki');
-    expect(component.getRuleColor(ruleB)).toEqual('red');
-    expect(component.getRuleColor(ruleC)).toEqual('orange');
-
-    fixture.destroy();
-  }));
-
-  it('should edit rules', async(() => {
-    let ruleA = { name: 'ruleA', rule: 'rule A', score: 15, comment: '' };
-    let ruleB = { name: 'ruleB', rule: 'rule B', score: 95, comment: '' };
-    let ruleC = { name: 'ruleC', rule: 'rule C', score: 50, comment: '' };
-    let ruleD = { name: 'ruleD', rule: 'rule D', score: 85, comment: '' };
-    let ruleE = { name: 'ruleE', rule: 'rule E', score: 5, comment: '' };
-    let ruleF = { name: 'ruleF', rule: 'rule F', score: 21, comment: '' };
-    let ruleG = { name: 'ruleG', rule: 'rule G', score: 100, comment: '' };
-
-    let sensorEnrichmentConfig = new SensorEnrichmentConfig();
-    sensorEnrichmentConfig.threatIntel = Object.assign(
-      new ThreatIntelConfig(),
-      {
-        triageConfig: {
-          riskLevelRules: [ruleA, ruleB, ruleC, ruleD, ruleE],
-          aggregator: 'MAX',
-          aggregationConfig: {}
-        }
-      }
-    );
-    component.sensorEnrichmentConfig = sensorEnrichmentConfig;
-
-    let changes: SimpleChanges = {
-      showThreatTriage: new SimpleChange(false, true, true)
-    };
-    component.ngOnChanges(changes);
-
-    // sorted by score high to low
-    expect(component.visibleRules).toEqual([ruleB, ruleD, ruleC, ruleA, ruleE]);
-    expect(component.lowAlerts).toEqual(2);
-    expect(component.mediumAlerts).toEqual(1);
-    expect(component.highAlerts).toEqual(2);
-
-    // sorted by name high to low
-    component.onSortOrderChange(SortOrderOption.Highest_Name);
-    expect(component.visibleRules).toEqual([ruleE, ruleD, ruleC, ruleB, ruleA]);
-
-    // sorted by score low to high
-    component.onSortOrderChange(SortOrderOption.Lowest_Score);
-    expect(component.visibleRules).toEqual([ruleE, ruleA, ruleC, ruleD, ruleB]);
-
-    // sorted by name low to high
-    component.onSortOrderChange(SortOrderOption.Lowest_Name);
-    expect(component.visibleRules).toEqual([ruleA, ruleB, ruleC, ruleD, ruleE]);
-
-    component.onNewRule();
-    expect(component.currentRiskLevelRule.name).toEqual('');
-    expect(component.currentRiskLevelRule.rule).toEqual('');
-    expect(component.currentRiskLevelRule.score).toEqual(0);
-    expect(component.showTextEditor).toEqual(true);
-
-    component.currentRiskLevelRule = new RiskLevelRule();
-    component.onCancelTextEditor();
-    expect(component.showTextEditor).toEqual(false);
-    expect(component.visibleRules).toEqual([ruleA, ruleB, ruleC, ruleD, ruleE]);
-
-    component.sortOrder = SortOrderOption.Lowest_Score;
-    component.onNewRule();
-    component.currentRiskLevelRule = ruleF;
-    expect(component.showTextEditor).toEqual(true);
-    component.onSubmitTextEditor(ruleF);
-    expect(component.visibleRules).toEqual([
-      ruleE,
-      ruleA,
-      ruleF,
-      ruleC,
-      ruleD,
-      ruleB
-    ]);
-    expect(component.lowAlerts).toEqual(2);
-    expect(component.mediumAlerts).toEqual(2);
-    expect(component.highAlerts).toEqual(2);
-    expect(component.showTextEditor).toEqual(false);
-
-    component.onDeleteRule(ruleE);
-    expect(component.visibleRules).toEqual([ruleA, ruleF, ruleC, ruleD, ruleB]);
-    expect(component.lowAlerts).toEqual(1);
-    expect(component.mediumAlerts).toEqual(2);
-    expect(component.highAlerts).toEqual(2);
-
-    component.onFilterChange(ThreatTriageFilter.LOW);
-    expect(component.visibleRules).toEqual([ruleA]);
-
-    component.onFilterChange(ThreatTriageFilter.MEDIUM);
-    expect(component.visibleRules).toEqual([ruleF, ruleC]);
-
-    component.onFilterChange(ThreatTriageFilter.HIGH);
-    expect(component.visibleRules).toEqual([ruleD, ruleB]);
-
-    component.onFilterChange(ThreatTriageFilter.HIGH);
-    expect(component.visibleRules).toEqual([ruleA, ruleF, ruleC, ruleD, ruleB]);
-
-    component.onEditRule(ruleC);
-    expect(component.currentRiskLevelRule).toEqual(ruleC);
-    expect(component.showTextEditor).toEqual(true);
-    component.onSubmitTextEditor(ruleG);
-    expect(component.visibleRules).toEqual([ruleA, ruleF, ruleD, ruleB, ruleG]);
-    expect(component.lowAlerts).toEqual(1);
-    expect(component.mediumAlerts).toEqual(1);
-    expect(component.highAlerts).toEqual(3);
-    expect(component.showTextEditor).toEqual(false);
-
-    fixture.destroy();
-  }));
 });
diff --git a/metron-interface/metron-config/src/app/sensors/sensor-threat-triage/sensor-threat-triage.component.ts b/metron-interface/metron-config/src/app/sensors/sensor-threat-triage/sensor-threat-triage.component.ts
index db32b04..c894c0c 100644
--- a/metron-interface/metron-config/src/app/sensors/sensor-threat-triage/sensor-threat-triage.component.ts
+++ b/metron-interface/metron-config/src/app/sensors/sensor-threat-triage/sensor-threat-triage.component.ts
@@ -21,14 +21,6 @@ import {SensorEnrichmentConfig } from '../../model/sensor-enrichment-config';
 import {RiskLevelRule} from '../../model/risk-level-rule';
 import {SensorEnrichmentConfigService} from '../../service/sensor-enrichment-config.service';
 
-export enum SortOrderOption {
-  Lowest_Score, Highest_Score, Lowest_Name, Highest_Name
-}
-
-export enum ThreatTriageFilter {
-  NONE, LOW, MEDIUM, HIGH
-}
-
 @Component({
   selector: 'metron-config-sensor-threat-triage',
   templateUrl: './sensor-threat-triage.component.html',
@@ -39,23 +31,16 @@ export class SensorThreatTriageComponent implements OnChanges {
 
   @Input() showThreatTriage: boolean;
   @Input() sensorEnrichmentConfig: SensorEnrichmentConfig;
-
   @Output() hideThreatTriage: EventEmitter<boolean> = new EventEmitter<boolean>();
+
   availableAggregators = [];
   visibleRules: RiskLevelRule[] = [];
-
   showTextEditor = false;
   currentRiskLevelRule: RiskLevelRule;
-
   lowAlerts = 0;
   mediumAlerts = 0;
   highAlerts = 0;
 
-  sortOrderOption = SortOrderOption;
-  sortOrder = SortOrderOption.Highest_Score;
-  threatTriageFilter = ThreatTriageFilter;
-  filter: ThreatTriageFilter = ThreatTriageFilter.NONE;
-
   constructor(private sensorEnrichmentConfigService: SensorEnrichmentConfigService) { }
 
   ngOnChanges(changes: SimpleChanges) {
@@ -69,8 +54,6 @@ export class SensorThreatTriageComponent implements OnChanges {
     this.sensorEnrichmentConfigService.getAvailableThreatTriageAggregators().subscribe(results => {
       this.availableAggregators = results;
     });
-    this.updateBuckets();
-    this.onSortOrderChange(null);
   }
 
   onClose(): void {
@@ -111,92 +94,6 @@ export class SensorThreatTriageComponent implements OnChanges {
     }
   }
 
-  updateBuckets() {
-    this.lowAlerts = 0;
-    this.mediumAlerts = 0;
-    this.highAlerts = 0;
-    for (let riskLevelRule of this.visibleRules) {
-      if (riskLevelRule.score <= 20) {
-        this.lowAlerts++;
-      } else if (riskLevelRule.score >= 80) {
-        this.highAlerts++;
-      } else {
-        this.mediumAlerts++;
-      }
-    }
-  }
-
-  getRuleColor(riskLevelRule: RiskLevelRule): string {
-    let color: string;
-    if (riskLevelRule.score <= 20) {
-      color = 'khaki';
-    } else if (riskLevelRule.score >= 80) {
-      color = 'red';
-    } else {
-      color = 'orange';
-    }
-    return color;
-  }
-
-  onSortOrderChange(sortOrder: any) {
-    if (sortOrder !== null) {
-      this.sortOrder = sortOrder;
-    }
-
-    // all comparisons with enums must be == and not ===
-    if (this.sortOrder == this.sortOrderOption.Highest_Score) {
-      this.visibleRules.sort((a, b) => {
-        return b.score - a.score;
-      });
-    } else if (this.sortOrder == SortOrderOption.Lowest_Score) {
-      this.visibleRules.sort((a, b) => {
-        return a.score - b.score;
-      });
-    } else if (this.sortOrder == SortOrderOption.Lowest_Name) {
-      this.visibleRules.sort((a, b) => {
-        let aName = a.name ? a.name : '';
-        let bName = b.name ? b.name : '';
-        if (aName.toLowerCase() >= bName.toLowerCase()) {
-          return 1;
-        } else if (aName.toLowerCase() < bName.toLowerCase()) {
-          return -1;
-        }
-      });
-    } else {
-      this.visibleRules.sort((a, b) => {
-        let aName = a.name ? a.name : '';
-        let bName = b.name ? b.name : '';
-        if (aName.toLowerCase() >= bName.toLowerCase()) {
-          return -1;
-        } else if (aName.toLowerCase() < bName.toLowerCase()) {
-          return 1;
-        }
-      });
-    }
-  }
-
-  onFilterChange(filter: ThreatTriageFilter) {
-    if (filter === this.filter) {
-      this.filter = ThreatTriageFilter.NONE;
-    } else {
-      this.filter = filter;
-    }
-    this.visibleRules = this.sensorEnrichmentConfig.threatIntel.triageConfig.riskLevelRules.filter(riskLevelRule => {
-      if (this.filter === ThreatTriageFilter.NONE) {
-        return true;
-      } else {
-        if (this.filter === ThreatTriageFilter.HIGH) {
-          return riskLevelRule.score >= 80;
-        } else if (this.filter === ThreatTriageFilter.LOW) {
-          return riskLevelRule.score <= 20;
-        } else {
-          return riskLevelRule.score < 80 && riskLevelRule.score > 20;
-        }
-      }
-    });
-    this.onSortOrderChange(null);
-  }
-
   getDisplayName(riskLevelRule: RiskLevelRule): string {
     if (riskLevelRule.name) {
       return riskLevelRule.name;
diff --git a/metron-interface/metron-config/src/app/shared/ace-editor/ace-editor.component.ts b/metron-interface/metron-config/src/app/shared/ace-editor/ace-editor.component.ts
index 8422544..21c62f1 100644
--- a/metron-interface/metron-config/src/app/shared/ace-editor/ace-editor.component.ts
+++ b/metron-interface/metron-config/src/app/shared/ace-editor/ace-editor.component.ts
@@ -16,7 +16,7 @@
  * limitations under the License.
  */
 /// <reference path="../../../../node_modules/@types/ace/index.d.ts" />
-import { Component, AfterViewInit, ViewChild, ElementRef, forwardRef, Input} from '@angular/core';
+import { Component, AfterViewInit, ViewChild, ElementRef, forwardRef, Input, Output, EventEmitter} from '@angular/core';
 import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
 import {AutocompleteOption} from '../../model/autocomplete-option';
 
@@ -38,9 +38,15 @@ export class AceEditorComponent implements AfterViewInit, ControlValueAccessor {
 
   inputJson: any = '';
   aceConfigEditor: AceAjax.Editor;
-  @Input() type = 'JSON';
+  @Input() type: 'GROK' | 'JSON' | 'STELLAR' = 'JSON';
   @Input() placeHolder = 'Enter text here';
   @Input() options: AutocompleteOption[] = [];
+  @Input() liveAutocompletion = true;
+  @Input() enableSnippets = true;
+  @Input() useWorker = true;
+  @Input() wrapLimitRangeMin: number | null = 72;
+  @Input() wrapLimitRangeMax: number | null = 72;
+  @Output() onChange = new EventEmitter();
   @ViewChild('aceEditor') aceEditorEle: ElementRef;
 
   private onTouchedCallback;
@@ -101,7 +107,7 @@ export class AceEditorComponent implements AfterViewInit, ControlValueAccessor {
     parserConfigEditor.getSession().setMode(this.getEditorType());
     parserConfigEditor.getSession().setTabSize(2);
     parserConfigEditor.getSession().setUseWrapMode(true);
-    parserConfigEditor.getSession().setWrapLimitRange(72, 72);
+    parserConfigEditor.getSession().setWrapLimitRange(this.wrapLimitRangeMin, this.wrapLimitRangeMax);
 
     parserConfigEditor.$blockScrolling = Infinity;
     parserConfigEditor.setTheme('ace/theme/monokai');
@@ -110,12 +116,16 @@ export class AceEditorComponent implements AfterViewInit, ControlValueAccessor {
       highlightActiveLine: false,
       maxLines: Infinity,
       enableBasicAutocompletion: true,
-      enableSnippets: true,
-      enableLiveAutocompletion: true
+      enableSnippets: this.enableSnippets,
+      useWorker: this.useWorker,
+      enableLiveAutocompletion: this.liveAutocompletion
     });
     parserConfigEditor.on('change', (e: any) => {
       this.inputJson = this.aceConfigEditor.getValue();
-      this.onChangeCallback(this.aceConfigEditor.getValue());
+      this.onChange.emit(this.inputJson);
+      if (typeof this.onChangeCallback === 'function') {
+        this.onChangeCallback(this.inputJson);
+      }
     });
 
     if (this.type === 'GROK') {
@@ -184,6 +194,8 @@ export class AceEditorComponent implements AfterViewInit, ControlValueAccessor {
   private getEditorType() {
       if (this.type === 'GROK') {
         return 'ace/mode/grok';
+      } else if (this.type === 'STELLAR') {
+        return 'ace/mode/javascript';
       }
 
       return 'ace/mode/json';
diff --git a/metron-interface/metron-config/src/assets/ace/mode-javascript.js b/metron-interface/metron-config/src/assets/ace/mode-javascript.js
new file mode 100644
index 0000000..ca37d3e
--- /dev/null
+++ b/metron-interface/metron-config/src/assets/ace/mode-javascript.js
@@ -0,0 +1 @@
+ace.define("ace/mode/doc_comment_highlight_rules",["require","exports","module","ace/lib/oop","ace/mode/text_highlight_rules"],function(e,t,n){"use strict";var r=e("../lib/oop"),i=e("./text_highlight_rules").TextHighlightRules,s=function(){this.$rules={start:[{token:"comment.doc.tag",regex:"@[\\w\\d_]+"},s.getTagRule(),{defaultToken:"comment.doc",caseInsensitive:!0}]}};r.inherits(s,i),s.getTagRule=function(e){return{token:"comment.doc.tag.storage.type",regex:"\\b(?:TODO|FIXME|XXX|HACK)\\ [...]
\ No newline at end of file