METRON-1997 Replace Threat Triage Score Field Slider with Text Box (ruffle1986 via sardell) closes apache/metron#1334
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
@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 {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 @@
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 @@
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 @@
.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 @@
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 {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 @@
@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 @@
this.sensorEnrichmentConfigService.getAvailableThreatTriageAggregators().subscribe(results => {
this.availableAggregators = results;
});
- this.updateBuckets();
- this.onSortOrderChange(null);
}
onClose(): void {
@@ -111,92 +94,6 @@
}
}
- 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 @@
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 @@
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 @@
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 @@
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)\\b"}},s.getStartRule=function(e){return{token:"comment.doc",regex:"\\/\\*(?=\\*)",next:e}},s.getEndRule=function(e){return{token:"comment.doc",regex:"\\*\\/",next:e}},t.DocCommentHighlightRules=s}),ace.define("ace/mode/javascript_highlight_rules",["require","exports","module","ace/lib/oop","ace/mode/doc_comment_highlight_rules","ace/mode/text_highlight_rules"],function(e,t,n){"use strict";function a(){var e=o.replace("\\d","\\d\\-"),t={onMatch:function(e,t,n){var r=e.charAt(1)=="/"?2:1;if(r==1)t!=this.nextState?n.unshift(this.next,this.nextState,0):n.unshift(this.next),n[2]++;else if(r==2&&t==this.nextState){n[1]--;if(!n[1]||n[1]<0)n.shift(),n.shift()}return[{type:"meta.tag.punctuation."+(r==1?"":"end-")+"tag-open.xml",value:e.slice(0,r)},{type:"meta.tag.tag-name.xml",value:e.substr(r)}]},regex:"</?"+e+"",next:"jsxAttributes",nextState:"jsx"};this.$rules.start.unshift(t);var n={regex:"{",token:"paren.quasi.start",push:"start"};this.$rules.jsx=[n,t,{include:"reference"},{defaultToken:"string"}],this.$rules.jsxAttributes=[{token:"meta.tag.punctuation.tag-close.xml",regex:"/?>",onMatch:function(e,t,n){return t==n[0]&&n.shift(),e.length==2&&(n[0]==this.nextState&&n[1]--,(!n[1]||n[1]<0)&&n.splice(0,2)),this.next=n[0]||"start",[{type:this.token,value:e}]},nextState:"jsx"},n,f("jsxAttributes"),{token:"entity.other.attribute-name.xml",regex:e},{token:"keyword.operator.attribute-equals.xml",regex:"="},{token:"text.tag-whitespace.xml",regex:"\\s+"},{token:"string.attribute-value.xml",regex:"'",stateName:"jsx_attr_q",push:[{token:"string.attribute-value.xml",regex:"'",next:"pop"},{include:"reference"},{defaultToken:"string.attribute-value.xml"}]},{token:"string.attribute-value.xml",regex:'"',stateName:"jsx_attr_qq",push:[{token:"string.attribute-value.xml",regex:'"',next:"pop"},{include:"reference"},{defaultToken:"string.attribute-value.xml"}]},t],this.$rules.reference=[{token:"constant.language.escape.reference.xml",regex:"(?:&#[0-9]+;)|(?:&#x[0-9a-fA-F]+;)|(?:&[a-zA-Z0-9_:\\.-]+;)"}]}function f(e){return[{token:"comment",regex:/\/\*/,next:[i.getTagRule(),{token:"comment",regex:"\\*\\/",next:e||"pop"},{defaultToken:"comment",caseInsensitive:!0}]},{token:"comment",regex:"\\/\\/",next:[i.getTagRule(),{token:"comment",regex:"$|^",next:e||"pop"},{defaultToken:"comment",caseInsensitive:!0}]}]}var r=e("../lib/oop"),i=e("./doc_comment_highlight_rules").DocCommentHighlightRules,s=e("./text_highlight_rules").TextHighlightRules,o="[a-zA-Z\\$_\u00a1-\uffff][a-zA-Z\\d\\$_\u00a1-\uffff]*",u=function(e){var t=this.createKeywordMapper({"variable.language":"Array|Boolean|Date|Function|Iterator|Number|Object|RegExp|String|Proxy|Namespace|QName|XML|XMLList|ArrayBuffer|Float32Array|Float64Array|Int16Array|Int32Array|Int8Array|Uint16Array|Uint32Array|Uint8Array|Uint8ClampedArray|Error|EvalError|InternalError|RangeError|ReferenceError|StopIteration|SyntaxError|TypeError|URIError|decodeURI|decodeURIComponent|encodeURI|encodeURIComponent|eval|isFinite|isNaN|parseFloat|parseInt|JSON|Math|this|arguments|prototype|window|document",keyword:"const|yield|import|get|set|async|await|break|case|catch|continue|default|delete|do|else|finally|for|function|if|in|instanceof|new|return|switch|throw|try|typeof|let|var|while|with|debugger|__parent__|__count__|escape|unescape|with|__proto__|class|enum|extends|super|export|implements|private|public|interface|package|protected|static","storage.type":"const|let|var|function","constant.language":"null|Infinity|NaN|undefined","support.function":"alert","constant.language.boolean":"true|false"},"identifier"),n="case|do|else|finally|in|instanceof|return|throw|try|typeof|yield|void",r="\\\\(?:x[0-9a-fA-F]{2}|u[0-9a-fA-F]{4}|u{[0-9a-fA-F]{1,6}}|[0-2][0-7]{0,2}|3[0-7][0-7]?|[4-7][0-7]?|.)";this.$rules={no_regex:[i.getStartRule("doc-start"),f("no_regex"),{token:"string",regex:"'(?=.)",next:"qstring"},{token:"string",regex:'"(?=.)',next:"qqstring"},{token:"constant.numeric",regex:/0(?:[xX][0-9a-fA-F]+|[bB][01]+)\b/},{token:"constant.numeric",regex:/[+-]?\d[\d_]*(?:(?:\.\d*)?(?:[eE][+-]?\d+)?)?\b/},{token:["storage.type","punctuation.operator","support.function","punctuation.operator","entity.name.function","text","keyword.operator"],regex:"("+o+")(\\.)(prototype)(\\.)("+o+")(\\s*)(=)",next:"function_arguments"},{token:["storage.type","punctuation.operator","entity.name.function","text","keyword.operator","text","storage.type","text","paren.lparen"],regex:"("+o+")(\\.)("+o+")(\\s*)(=)(\\s*)(function)(\\s*)(\\()",next:"function_arguments"},{token:["entity.name.function","text","keyword.operator","text","storage.type","text","paren.lparen"],regex:"("+o+")(\\s*)(=)(\\s*)(function)(\\s*)(\\()",next:"function_arguments"},{token:["storage.type","punctuation.operator","entity.name.function","text","keyword.operator","text","storage.type","text","entity.name.function","text","paren.lparen"],regex:"("+o+")(\\.)("+o+")(\\s*)(=)(\\s*)(function)(\\s+)(\\w+)(\\s*)(\\()",next:"function_arguments"},{token:["storage.type","text","entity.name.function","text","paren.lparen"],regex:"(function)(\\s+)("+o+")(\\s*)(\\()",next:"function_arguments"},{token:["entity.name.function","text","punctuation.operator","text","storage.type","text","paren.lparen"],regex:"("+o+")(\\s*)(:)(\\s*)(function)(\\s*)(\\()",next:"function_arguments"},{token:["text","text","storage.type","text","paren.lparen"],regex:"(:)(\\s*)(function)(\\s*)(\\()",next:"function_arguments"},{token:"keyword",regex:"(?:"+n+")\\b",next:"start"},{token:["support.constant"],regex:/that\b/},{token:["storage.type","punctuation.operator","support.function.firebug"],regex:/(console)(\.)(warn|info|log|error|time|trace|timeEnd|assert)\b/},{token:t,regex:o},{token:"punctuation.operator",regex:/[.](?![.])/,next:"property"},{token:"keyword.operator",regex:/--|\+\+|\.{3}|===|==|=|!=|!==|<+=?|>+=?|!|&&|\|\||\?:|[!$%&*+\-~\/^]=?/,next:"start"},{token:"punctuation.operator",regex:/[?:,;.]/,next:"start"},{token:"paren.lparen",regex:/[\[({]/,next:"start"},{token:"paren.rparen",regex:/[\])}]/},{token:"comment",regex:/^#!.*$/}],property:[{token:"text",regex:"\\s+"},{token:["storage.type","punctuation.operator","entity.name.function","text","keyword.operator","text","storage.type","text","entity.name.function","text","paren.lparen"],regex:"("+o+")(\\.)("+o+")(\\s*)(=)(\\s*)(function)(?:(\\s+)(\\w+))?(\\s*)(\\()",next:"function_arguments"},{token:"punctuation.operator",regex:/[.](?![.])/},{token:"support.function",regex:/(s(?:h(?:ift|ow(?:Mod(?:elessDialog|alDialog)|Help))|croll(?:X|By(?:Pages|Lines)?|Y|To)?|t(?:op|rike)|i(?:n|zeToContent|debar|gnText)|ort|u(?:p|b(?:str(?:ing)?)?)|pli(?:ce|t)|e(?:nd|t(?:Re(?:sizable|questHeader)|M(?:i(?:nutes|lliseconds)|onth)|Seconds|Ho(?:tKeys|urs)|Year|Cursor|Time(?:out)?|Interval|ZOptions|Date|UTC(?:M(?:i(?:nutes|lliseconds)|onth)|Seconds|Hours|Date|FullYear)|FullYear|Active)|arch)|qrt|lice|avePreferences|mall)|h(?:ome|andleEvent)|navigate|c(?:har(?:CodeAt|At)|o(?:s|n(?:cat|textual|firm)|mpile)|eil|lear(?:Timeout|Interval)?|a(?:ptureEvents|ll)|reate(?:StyleSheet|Popup|EventObject))|t(?:o(?:GMTString|S(?:tring|ource)|U(?:TCString|pperCase)|Lo(?:caleString|werCase))|est|a(?:n|int(?:Enabled)?))|i(?:s(?:NaN|Finite)|ndexOf|talics)|d(?:isableExternalCapture|ump|etachEvent)|u(?:n(?:shift|taint|escape|watch)|pdateCommands)|j(?:oin|avaEnabled)|p(?:o(?:p|w)|ush|lugins.refresh|a(?:ddings|rse(?:Int|Float)?)|r(?:int|ompt|eference))|e(?:scape|nableExternalCapture|val|lementFromPoint|x(?:p|ec(?:Script|Command)?))|valueOf|UTC|queryCommand(?:State|Indeterm|Enabled|Value)|f(?:i(?:nd|le(?:ModifiedDate|Size|CreatedDate|UpdatedDate)|xed)|o(?:nt(?:size|color)|rward)|loor|romCharCode)|watch|l(?:ink|o(?:ad|g)|astIndexOf)|a(?:sin|nchor|cos|t(?:tachEvent|ob|an(?:2)?)|pply|lert|b(?:s|ort))|r(?:ou(?:nd|teEvents)|e(?:size(?:By|To)|calc|turnValue|place|verse|l(?:oad|ease(?:Capture|Events)))|andom)|g(?:o|et(?:ResponseHeader|M(?:i(?:nutes|lliseconds)|onth)|Se(?:conds|lection)|Hours|Year|Time(?:zoneOffset)?|Da(?:y|te)|UTC(?:M(?:i(?:nutes|lliseconds)|onth)|Seconds|Hours|Da(?:y|te)|FullYear)|FullYear|A(?:ttention|llResponseHeaders)))|m(?:in|ove(?:B(?:y|elow)|To(?:Absolute)?|Above)|ergeAttributes|a(?:tch|rgins|x))|b(?:toa|ig|o(?:ld|rderWidths)|link|ack))\b(?=\()/},{token:"support.function.dom",regex:/(s(?:ub(?:stringData|mit)|plitText|e(?:t(?:NamedItem|Attribute(?:Node)?)|lect))|has(?:ChildNodes|Feature)|namedItem|c(?:l(?:ick|o(?:se|neNode))|reate(?:C(?:omment|DATASection|aption)|T(?:Head|extNode|Foot)|DocumentFragment|ProcessingInstruction|E(?:ntityReference|lement)|Attribute))|tabIndex|i(?:nsert(?:Row|Before|Cell|Data)|tem)|open|delete(?:Row|C(?:ell|aption)|T(?:Head|Foot)|Data)|focus|write(?:ln)?|a(?:dd|ppend(?:Child|Data))|re(?:set|place(?:Child|Data)|move(?:NamedItem|Child|Attribute(?:Node)?)?)|get(?:NamedItem|Element(?:sBy(?:Name|TagName|ClassName)|ById)|Attribute(?:Node)?)|blur)\b(?=\()/},{token:"support.constant",regex:/(s(?:ystemLanguage|cr(?:ipts|ollbars|een(?:X|Y|Top|Left))|t(?:yle(?:Sheets)?|atus(?:Text|bar)?)|ibling(?:Below|Above)|ource|uffixes|e(?:curity(?:Policy)?|l(?:ection|f)))|h(?:istory|ost(?:name)?|as(?:h|Focus))|y|X(?:MLDocument|SLDocument)|n(?:ext|ame(?:space(?:s|URI)|Prop))|M(?:IN_VALUE|AX_VALUE)|c(?:haracterSet|o(?:n(?:structor|trollers)|okieEnabled|lorDepth|mp(?:onents|lete))|urrent|puClass|l(?:i(?:p(?:boardData)?|entInformation)|osed|asses)|alle(?:e|r)|rypto)|t(?:o(?:olbar|p)|ext(?:Transform|Indent|Decoration|Align)|ags)|SQRT(?:1_2|2)|i(?:n(?:ner(?:Height|Width)|put)|ds|gnoreCase)|zIndex|o(?:scpu|n(?:readystatechange|Line)|uter(?:Height|Width)|p(?:sProfile|ener)|ffscreenBuffering)|NEGATIVE_INFINITY|d(?:i(?:splay|alog(?:Height|Top|Width|Left|Arguments)|rectories)|e(?:scription|fault(?:Status|Ch(?:ecked|arset)|View)))|u(?:ser(?:Profile|Language|Agent)|n(?:iqueID|defined)|pdateInterval)|_content|p(?:ixelDepth|ort|ersonalbar|kcs11|l(?:ugins|atform)|a(?:thname|dding(?:Right|Bottom|Top|Left)|rent(?:Window|Layer)?|ge(?:X(?:Offset)?|Y(?:Offset)?))|r(?:o(?:to(?:col|type)|duct(?:Sub)?|mpter)|e(?:vious|fix)))|e(?:n(?:coding|abledPlugin)|x(?:ternal|pando)|mbeds)|v(?:isibility|endor(?:Sub)?|Linkcolor)|URLUnencoded|P(?:I|OSITIVE_INFINITY)|f(?:ilename|o(?:nt(?:Size|Family|Weight)|rmName)|rame(?:s|Element)|gColor)|E|whiteSpace|l(?:i(?:stStyleType|n(?:eHeight|kColor))|o(?:ca(?:tion(?:bar)?|lName)|wsrc)|e(?:ngth|ft(?:Context)?)|a(?:st(?:M(?:odified|atch)|Index|Paren)|yer(?:s|X)|nguage))|a(?:pp(?:MinorVersion|Name|Co(?:deName|re)|Version)|vail(?:Height|Top|Width|Left)|ll|r(?:ity|guments)|Linkcolor|bove)|r(?:ight(?:Context)?|e(?:sponse(?:XML|Text)|adyState))|global|x|m(?:imeTypes|ultiline|enubar|argin(?:Right|Bottom|Top|Left))|L(?:N(?:10|2)|OG(?:10E|2E))|b(?:o(?:ttom|rder(?:Width|RightWidth|BottomWidth|Style|Color|TopWidth|LeftWidth))|ufferDepth|elow|ackground(?:Color|Image)))\b/},{token:"identifier",regex:o},{regex:"",token:"empty",next:"no_regex"}],start:[i.getStartRule("doc-start"),f("start"),{token:"string.regexp",regex:"\\/",next:"regex"},{token:"text",regex:"\\s+|^$",next:"start"},{token:"empty",regex:"",next:"no_regex"}],regex:[{token:"regexp.keyword.operator",regex:"\\\\(?:u[\\da-fA-F]{4}|x[\\da-fA-F]{2}|.)"},{token:"string.regexp",regex:"/[sxngimy]*",next:"no_regex"},{token:"invalid",regex:/\{\d+\b,?\d*\}[+*]|[+*$^?][+*]|[$^][?]|\?{3,}/},{token:"constant.language.escape",regex:/\(\?[:=!]|\)|\{\d+\b,?\d*\}|[+*]\?|[()$^+*?.]/},{token:"constant.language.delimiter",regex:/\|/},{token:"constant.language.escape",regex:/\[\^?/,next:"regex_character_class"},{token:"empty",regex:"$",next:"no_regex"},{defaultToken:"string.regexp"}],regex_character_class:[{token:"regexp.charclass.keyword.operator",regex:"\\\\(?:u[\\da-fA-F]{4}|x[\\da-fA-F]{2}|.)"},{token:"constant.language.escape",regex:"]",next:"regex"},{token:"constant.language.escape",regex:"-"},{token:"empty",regex:"$",next:"no_regex"},{defaultToken:"string.regexp.charachterclass"}],function_arguments:[{token:"variable.parameter",regex:o},{token:"punctuation.operator",regex:"[, ]+"},{token:"punctuation.operator",regex:"$"},{token:"empty",regex:"",next:"no_regex"}],qqstring:[{token:"constant.language.escape",regex:r},{token:"string",regex:"\\\\$",next:"qqstring"},{token:"string",regex:'"|$',next:"no_regex"},{defaultToken:"string"}],qstring:[{token:"constant.language.escape",regex:r},{token:"string",regex:"\\\\$",next:"qstring"},{token:"string",regex:"'|$",next:"no_regex"},{defaultToken:"string"}]};if(!e||!e.noES6)this.$rules.no_regex.unshift({regex:"[{}]",onMatch:function(e,t,n){this.next=e=="{"?this.nextState:"";if(e=="{"&&n.length)n.unshift("start",t);else if(e=="}"&&n.length){n.shift(),this.next=n.shift();if(this.next.indexOf("string")!=-1||this.next.indexOf("jsx")!=-1)return"paren.quasi.end"}return e=="{"?"paren.lparen":"paren.rparen"},nextState:"start"},{token:"string.quasi.start",regex:/`/,push:[{token:"constant.language.escape",regex:r},{token:"paren.quasi.start",regex:/\${/,push:"start"},{token:"string.quasi.end",regex:/`/,next:"pop"},{defaultToken:"string.quasi"}]}),(!e||e.jsx!=0)&&a.call(this);this.embedRules(i,"doc-",[i.getEndRule("no_regex")]),this.normalizeRules()};r.inherits(u,s),t.JavaScriptHighlightRules=u}),ace.define("ace/mode/matching_brace_outdent",["require","exports","module","ace/range"],function(e,t,n){"use strict";var r=e("../range").Range,i=function(){};(function(){this.checkOutdent=function(e,t){return/^\s+$/.test(e)?/^\s*\}/.test(t):!1},this.autoOutdent=function(e,t){var n=e.getLine(t),i=n.match(/^(\s*\})/);if(!i)return 0;var s=i[1].length,o=e.findMatchingBracket({row:t,column:s});if(!o||o.row==t)return 0;var u=this.$getIndent(e.getLine(o.row));e.replace(new r(t,0,t,s-1),u)},this.$getIndent=function(e){return e.match(/^\s*/)[0]}}).call(i.prototype),t.MatchingBraceOutdent=i}),ace.define("ace/mode/folding/cstyle",["require","exports","module","ace/lib/oop","ace/range","ace/mode/folding/fold_mode"],function(e,t,n){"use strict";var r=e("../../lib/oop"),i=e("../../range").Range,s=e("./fold_mode").FoldMode,o=t.FoldMode=function(e){e&&(this.foldingStartMarker=new RegExp(this.foldingStartMarker.source.replace(/\|[^|]*?$/,"|"+e.start)),this.foldingStopMarker=new RegExp(this.foldingStopMarker.source.replace(/\|[^|]*?$/,"|"+e.end)))};r.inherits(o,s),function(){this.foldingStartMarker=/(\{|\[)[^\}\]]*$|^\s*(\/\*)/,this.foldingStopMarker=/^[^\[\{]*(\}|\])|^[\s\*]*(\*\/)/,this.singleLineBlockCommentRe=/^\s*(\/\*).*\*\/\s*$/,this.tripleStarBlockCommentRe=/^\s*(\/\*\*\*).*\*\/\s*$/,this.startRegionRe=/^\s*(\/\*|\/\/)#?region\b/,this._getFoldWidgetBase=this.getFoldWidget,this.getFoldWidget=function(e,t,n){var r=e.getLine(n);if(this.singleLineBlockCommentRe.test(r)&&!this.startRegionRe.test(r)&&!this.tripleStarBlockCommentRe.test(r))return"";var i=this._getFoldWidgetBase(e,t,n);return!i&&this.startRegionRe.test(r)?"start":i},this.getFoldWidgetRange=function(e,t,n,r){var i=e.getLine(n);if(this.startRegionRe.test(i))return this.getCommentRegionBlock(e,i,n);var s=i.match(this.foldingStartMarker);if(s){var o=s.index;if(s[1])return this.openingBracketBlock(e,s[1],n,o);var u=e.getCommentFoldRange(n,o+s[0].length,1);return u&&!u.isMultiLine()&&(r?u=this.getSectionRange(e,n):t!="all"&&(u=null)),u}if(t==="markbegin")return;var s=i.match(this.foldingStopMarker);if(s){var o=s.index+s[0].length;return s[1]?this.closingBracketBlock(e,s[1],n,o):e.getCommentFoldRange(n,o,-1)}},this.getSectionRange=function(e,t){var n=e.getLine(t),r=n.search(/\S/),s=t,o=n.length;t+=1;var u=t,a=e.getLength();while(++t<a){n=e.getLine(t);var f=n.search(/\S/);if(f===-1)continue;if(r>f)break;var l=this.getFoldWidgetRange(e,"all",t);if(l){if(l.start.row<=s)break;if(l.isMultiLine())t=l.end.row;else if(r==f)break}u=t}return new i(s,o,u,e.getLine(u).length)},this.getCommentRegionBlock=function(e,t,n){var r=t.search(/\s*$/),s=e.getLength(),o=n,u=/^\s*(?:\/\*|\/\/|--)#?(end)?region\b/,a=1;while(++n<s){t=e.getLine(n);var f=u.exec(t);if(!f)continue;f[1]?a--:a++;if(!a)break}var l=n;if(l>o)return new i(o,r,l,t.length)}}.call(o.prototype)}),ace.define("ace/mode/javascript",["require","exports","module","ace/lib/oop","ace/mode/text","ace/mode/javascript_highlight_rules","ace/mode/matching_brace_outdent","ace/range","ace/worker/worker_client","ace/mode/behaviour/cstyle","ace/mode/folding/cstyle"],function(e,t,n){"use strict";var r=e("../lib/oop"),i=e("./text").Mode,s=e("./javascript_highlight_rules").JavaScriptHighlightRules,o=e("./matching_brace_outdent").MatchingBraceOutdent,u=e("../range").Range,a=e("../worker/worker_client").WorkerClient,f=e("./behaviour/cstyle").CstyleBehaviour,l=e("./folding/cstyle").FoldMode,c=function(){this.HighlightRules=s,this.$outdent=new o,this.$behaviour=new f,this.foldingRules=new l};r.inherits(c,i),function(){this.lineCommentStart="//",this.blockComment={start:"/*",end:"*/"},this.getNextLineIndent=function(e,t,n){var r=this.$getIndent(t),i=this.getTokenizer().getLineTokens(t,e),s=i.tokens,o=i.state;if(s.length&&s[s.length-1].type=="comment")return r;if(e=="start"||e=="no_regex"){var u=t.match(/^.*(?:\bcase\b.*:|[\{\(\[])\s*$/);u&&(r+=n)}else if(e=="doc-start"){if(o=="start"||o=="no_regex")return"";var u=t.match(/^\s*(\/?)\*/);u&&(u[1]&&(r+=" "),r+="* ")}return r},this.checkOutdent=function(e,t,n){return this.$outdent.checkOutdent(t,n)},this.autoOutdent=function(e,t,n){this.$outdent.autoOutdent(t,n)},this.createWorker=function(e){var t=new a(["ace"],"ace/mode/javascript_worker","JavaScriptWorker");return t.attachToDocument(e.getDocument()),t.on("annotate",function(t){e.setAnnotations(t.data)}),t.on("terminate",function(){e.clearAnnotations()}),t},this.$id="ace/mode/javascript"}.call(c.prototype),t.Mode=c})
\ No newline at end of file