You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@metron.apache.org by rm...@apache.org on 2017/04/11 13:51:16 UTC

[05/12] incubator-metron git commit: METRON-623 Management UI [contributed by Raghu Mitra Kandikonda and Ryan Merriman] closes apache/incubator-metron#489

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/sensors/sensor-threat-triage/sensor-threat-triage.component.html
----------------------------------------------------------------------
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
new file mode 100644
index 0000000..9d67df6
--- /dev/null
+++ b/metron-interface/metron-config/src/app/sensors/sensor-threat-triage/sensor-threat-triage.component.html
@@ -0,0 +1,88 @@
+<!--
+  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.
+  -->
+<metron-config-sensor-rule-editor *ngIf="showTextEditor" [riskLevelRule]="currentRiskLevelRule"
+                                  (onCancelTextEditor)="onCancelTextEditor()"
+                                  (onSubmitTextEditor)="onSubmitTextEditor($event)"></metron-config-sensor-rule-editor>
+
+<div class="metron-slider-pane-edit fill load-left-to-right dialog1x">
+
+    <div class="form-title">Threat Triage Rules</div>
+    <i class="fa fa-times pull-right close-button" aria-hidden="true" (click)="onClose()"></i>
+
+
+    <form role="form" class="threat-intel-form">
+        <div class="form-group threat-triage-aggregator">
+            <label attr.for="aggregator">AGGREGATOR</label>
+            <select class="form-control" name="aggregator"
+                    [(ngModel)]="sensorEnrichmentConfig.threatIntel.triageConfig.aggregator">
+                <option *ngFor="let aggregator of availableAggregators" [value]="aggregator">{{aggregator}}</option>
+            </select>
+        </div>
+        <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%">
+                        {{ riskLevelRule.score }}
+                    </div>
+                    <div class="threat-triage-rule-row threat-triage-rule-str">
+                        {{ 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>
+                <div class="form-seperator-edit"></div>
+            </div>
+        </div>
+        <div class="form-group mx-0 py-0">
+            <button class="btn form-enable-disable-button add-button" (click)="onNewRule()"><i
+                    aria-hidden="true" class="fa fa-plus fa-4"></i></button>
+        </div>
+    </form>
+
+</div>

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/sensors/sensor-threat-triage/sensor-threat-triage.component.scss
----------------------------------------------------------------------
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
new file mode 100644
index 0000000..bbc17a0
--- /dev/null
+++ b/metron-interface/metron-config/src/app/sensors/sensor-threat-triage/sensor-threat-triage.component.scss
@@ -0,0 +1,137 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+@import "../../_variables.scss";
+@import "../../../styles.scss";
+
+textarea
+{
+  height: auto;
+}
+
+.form-title
+{
+  padding-left: 25px;
+}
+
+.form-group
+{
+  padding-left: 25px;
+  padding-right: 20px;
+}
+
+.close-button
+{
+  padding-right: 20px;
+}
+
+.threat-triage-summary
+{
+  background-color: $edit-child-highlight;
+  padding-top: 5px;
+  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;
+}
+
+.threat-triage-rules-list
+{
+  padding-top: 10px;
+}
+
+.threat-triage-rule-row
+{
+  display: inline-block;
+  position: relative;
+  vertical-align: top;
+
+  .fa {
+    color: $nav-active-color;
+    font-size: large;
+  }
+}
+
+.add-button
+{
+  font-size: 20px;
+  width: 100%;
+  padding: 2px;
+
+  i {
+    color: $field-button-color;
+  }
+}
+
+.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;
+}

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/sensors/sensor-threat-triage/sensor-threat-triage.component.spec.ts
----------------------------------------------------------------------
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
new file mode 100644
index 0000000..b9e595e
--- /dev/null
+++ b/metron-interface/metron-config/src/app/sensors/sensor-threat-triage/sensor-threat-triage.component.spec.ts
@@ -0,0 +1,211 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {SimpleChange, SimpleChanges} from '@angular/core';
+import {Http} from '@angular/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 {SensorEnrichmentConfigService} from '../../service/sensor-enrichment-config.service';
+import {Observable} from 'rxjs/Observable';
+import '../../rxjs-operators';
+import {SensorThreatTriageModule} from './sensor-threat-triage.module';
+
+class MockSensorEnrichmentConfigService {
+  public getAvailableThreatTriageAggregators(): Observable<string[]> {
+    return Observable.create(observer => {
+      observer.next(['MAX', 'MIN', 'SUM', 'MEAN', 'POSITIVE_MEAN']);
+      observer.complete();
+    });
+  }
+}
+
+describe('Component: SensorThreatTriageComponent', () => {
+
+  let component: SensorThreatTriageComponent;
+  let fixture: ComponentFixture<SensorThreatTriageComponent>;
+  let sensorEnrichmentConfigService: SensorEnrichmentConfigService;
+
+  beforeEach(async(() => {
+    TestBed.configureTestingModule({
+      imports: [SensorThreatTriageModule],
+      providers: [
+        {provide: Http},
+        {provide: SensorEnrichmentConfigService, useClass: MockSensorEnrichmentConfigService},
+      ]
+    }).compileComponents()
+        .then(() => {
+          fixture = TestBed.createComponent(SensorThreatTriageComponent);
+          component = fixture.componentInstance;
+          sensorEnrichmentConfigService = fixture.debugElement.injector.get(SensorEnrichmentConfigService);
+        });
+  }));
+
+  it('should create an instance', () => {
+    expect(component).toBeDefined();
+    fixture.destroy();
+  });
+
+  it('should create an instance', async(() => {
+    spyOn(component, 'init');
+    let changes: SimpleChanges = {'showThreatTriage': new SimpleChange(false, true)};
+
+    component.ngOnChanges(changes);
+    expect(component.init).toHaveBeenCalled();
+
+    changes = {'showStellar': new SimpleChange(true, false)};
+    component.ngOnChanges(changes);
+    expect(component.init['calls'].count()).toEqual(1);
+
+    fixture.destroy();
+  }));
+
+  it('should close panel', async(() => {
+    let numClosed = 0;
+    component.hideThreatTriage.subscribe((closed: boolean) => {
+      numClosed++;
+    });
+
+    component.onClose();
+    expect(numClosed).toEqual(1);
+
+    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)};
+    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();
+  }));
+
+
+});

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/sensors/sensor-threat-triage/sensor-threat-triage.component.ts
----------------------------------------------------------------------
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
new file mode 100644
index 0000000..db32b04
--- /dev/null
+++ b/metron-interface/metron-config/src/app/sensors/sensor-threat-triage/sensor-threat-triage.component.ts
@@ -0,0 +1,208 @@
+/**
+ * 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.
+ */
+/* tslint:disable:triple-equals */
+import {Component, Input, EventEmitter, Output, OnChanges, SimpleChanges} from '@angular/core';
+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',
+  styleUrls: ['./sensor-threat-triage.component.scss']
+})
+
+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) {
+    if (changes['showThreatTriage'] && changes['showThreatTriage'].currentValue) {
+      this.init();
+    }
+  }
+
+  init(): void {
+    this.visibleRules = this.sensorEnrichmentConfig.threatIntel.triageConfig.riskLevelRules;
+    this.sensorEnrichmentConfigService.getAvailableThreatTriageAggregators().subscribe(results => {
+      this.availableAggregators = results;
+    });
+    this.updateBuckets();
+    this.onSortOrderChange(null);
+  }
+
+  onClose(): void {
+    this.hideThreatTriage.emit(true);
+  }
+
+
+  onSubmitTextEditor(riskLevelRule: RiskLevelRule): void {
+    this.deleteRule(this.currentRiskLevelRule);
+    this.sensorEnrichmentConfig.threatIntel.triageConfig.riskLevelRules.push(riskLevelRule);
+    this.showTextEditor = false;
+    this.init();
+  }
+
+  onCancelTextEditor(): void {
+    this.showTextEditor = false;
+  }
+
+  onEditRule(riskLevelRule: RiskLevelRule) {
+    this.currentRiskLevelRule = riskLevelRule;
+    this.showTextEditor = true;
+  }
+
+  onDeleteRule(riskLevelRule: RiskLevelRule) {
+    this.deleteRule(riskLevelRule);
+    this.init();
+  }
+
+  onNewRule(): void {
+    this.currentRiskLevelRule = new RiskLevelRule();
+    this.showTextEditor = true;
+  }
+
+  deleteRule(riskLevelRule: RiskLevelRule) {
+    let index = this.sensorEnrichmentConfig.threatIntel.triageConfig.riskLevelRules.indexOf(riskLevelRule);
+    if (index != -1) {
+      this.sensorEnrichmentConfig.threatIntel.triageConfig.riskLevelRules.splice(index, 1);
+    }
+  }
+
+  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;
+    } else {
+      return riskLevelRule.rule ? riskLevelRule.rule : '';
+    }
+  }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/sensors/sensor-threat-triage/sensor-threat-triage.module.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/sensors/sensor-threat-triage/sensor-threat-triage.module.ts b/metron-interface/metron-config/src/app/sensors/sensor-threat-triage/sensor-threat-triage.module.ts
new file mode 100644
index 0000000..66838d9
--- /dev/null
+++ b/metron-interface/metron-config/src/app/sensors/sensor-threat-triage/sensor-threat-triage.module.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.
+ */
+import { NgModule } from '@angular/core';
+import {SharedModule} from '../../shared/shared.module';
+import {SensorThreatTriageComponent} from './sensor-threat-triage.component';
+import {SensorRuleEditorModule} from './rule-editor/sensor-rule-editor.module';
+
+
+@NgModule ({
+  imports: [ SharedModule, SensorRuleEditorModule ],
+  declarations: [ SensorThreatTriageComponent ],
+  exports: [ SensorThreatTriageComponent ]
+})
+export class SensorThreatTriageModule {}

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/service/authentication.service.spec.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/service/authentication.service.spec.ts b/metron-interface/metron-config/src/app/service/authentication.service.spec.ts
new file mode 100644
index 0000000..7f9b296
--- /dev/null
+++ b/metron-interface/metron-config/src/app/service/authentication.service.spec.ts
@@ -0,0 +1,190 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {Router} from '@angular/router';
+import {async, inject, TestBed} from '@angular/core/testing';
+import {MockBackend, MockConnection} from '@angular/http/testing';
+import {HttpModule, XHRBackend, Response, ResponseOptions, Http} from '@angular/http';
+import '../rxjs-operators';
+import {Observable} from 'rxjs/Observable';
+import {AuthenticationService} from './authentication.service';
+import {APP_CONFIG, METRON_REST_CONFIG} from '../app.config';
+import {IAppConfig} from '../app.config.interface';
+
+class MockRouter {
+
+    navigateByUrl(url: string) {
+
+    }
+}
+
+describe('AuthenticationService', () => {
+
+    beforeEach(async(() => {
+        TestBed.configureTestingModule({
+            imports: [HttpModule],
+            providers: [
+              AuthenticationService,
+              {provide: XHRBackend, useClass: MockBackend},
+              {provide: Router, useClass: MockRouter},
+              {provide: APP_CONFIG, useValue: METRON_REST_CONFIG}
+            ]
+        })
+            .compileComponents();
+    }));
+
+    describe('when service functions', () => {
+        it('can instantiate service when inject service',
+            inject([AuthenticationService], (service: AuthenticationService) => {
+                expect(service instanceof AuthenticationService).toBe(true);
+        }));
+
+    });
+
+    describe('when service functions', () => {
+        let authenticationService: AuthenticationService;
+        let mockBackend: MockBackend;
+        let userResponse: Response;
+        let userName = 'test';
+        let router: MockRouter;
+
+        beforeEach(inject([Http, XHRBackend, Router, AuthenticationService, APP_CONFIG],
+            (http: Http, be: MockBackend, mRouter: MockRouter, service: AuthenticationService, config: IAppConfig) => {
+            mockBackend = be;
+            router = mRouter;
+            authenticationService = service;
+            userResponse = new Response(new ResponseOptions({status: 200, body: userName}));
+        }));
+
+        it('init', async(inject([], () => {
+            let userResponsesuccess = true;
+            spyOn(authenticationService.onLoginEvent, 'emit');
+            spyOn(authenticationService, 'getCurrentUser').and.callFake(function() {
+                if (userResponsesuccess) {
+                    return Observable.create(observer => {
+                        observer.next(userResponse);
+                        observer.complete();
+                    });
+                }
+
+                return Observable.throw('Error');
+            });
+
+            authenticationService.init();
+            expect(authenticationService.onLoginEvent.emit).toHaveBeenCalledWith(true);
+
+            userResponsesuccess = false;
+            authenticationService.init();
+            expect(authenticationService.onLoginEvent.emit['calls'].count()).toEqual(2);
+
+        })));
+
+        it('login', async(inject([], () => {
+            let responseMessageSuccess = true;
+            mockBackend.connections.subscribe((c: MockConnection) => {
+                if (responseMessageSuccess) {
+                    c.mockRespond(userResponse);
+                } else {
+                    c.mockError(new Error('login failed'));
+                }
+            });
+
+            spyOn(router, 'navigateByUrl');
+            spyOn(authenticationService.onLoginEvent, 'emit');
+            authenticationService.login('test', 'test', error => {
+            });
+
+            expect(router.navigateByUrl).toHaveBeenCalledWith('/sensors');
+            expect(authenticationService.onLoginEvent.emit).toHaveBeenCalled();
+
+            responseMessageSuccess = false;
+            let errorSpy = jasmine.createSpy('error');
+            authenticationService.login('test', 'test', errorSpy);
+            expect(errorSpy).toHaveBeenCalledWith(new Error('login failed'));
+
+        })));
+
+        it('logout', async(inject([], () => {
+            let responseMessageSuccess = true;
+            mockBackend.connections.subscribe((c: MockConnection) => {
+                if (responseMessageSuccess) {
+                    c.mockRespond(userResponse);
+                } else {
+                    c.mockError(new Error('login failed'));
+                }
+            });
+
+            spyOn(router, 'navigateByUrl');
+            spyOn(authenticationService.onLoginEvent, 'emit');
+            authenticationService.logout();
+
+            expect(router.navigateByUrl).toHaveBeenCalledWith('/login');
+            expect(authenticationService.onLoginEvent.emit).toHaveBeenCalled();
+
+            responseMessageSuccess = false;
+            spyOn(console, 'log');
+            authenticationService.logout();
+            expect(console.log).toHaveBeenCalled();
+
+        })));
+
+        it('checkAuthentication', async(inject([], () => {
+            let isAuthenticated = false;
+            spyOn(router, 'navigateByUrl');
+            spyOn(authenticationService, 'isAuthenticated').and.callFake(function() {
+                return isAuthenticated;
+            });
+
+            authenticationService.checkAuthentication();
+            expect(router.navigateByUrl).toHaveBeenCalledWith('/login');
+
+            isAuthenticated = true;
+            authenticationService.checkAuthentication();
+            expect(router.navigateByUrl['calls'].count()).toEqual(1);
+        })));
+
+        it('getCurrentUser', async(inject([], () => {
+            mockBackend.connections.subscribe((c: MockConnection) => userResponse);
+            authenticationService.getCurrentUser(null).subscribe(
+                result => {
+                    expect(result).toEqual('');
+                }, error => console.log(error));
+        })));
+
+        it('isAuthenticationChecked', async(inject([], () => {
+            mockBackend.connections.subscribe((c: MockConnection) => c.mockRespond(userResponse));
+
+            expect(authenticationService.isAuthenticationChecked()).toEqual(false);
+
+            authenticationService.login('test', 'test', null);
+            expect(authenticationService.isAuthenticationChecked()).toEqual(true);
+
+        })));
+
+        it('isAuthenticated', async(inject([], () => {
+            mockBackend.connections.subscribe((c: MockConnection) => c.mockRespond(userResponse));
+
+            expect(authenticationService.isAuthenticated()).toEqual(false);
+
+            authenticationService.login('test', 'test', null);
+            expect(authenticationService.isAuthenticated()).toEqual(true);
+
+        })));
+    });
+
+
+});

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/service/authentication.service.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/service/authentication.service.ts b/metron-interface/metron-config/src/app/service/authentication.service.ts
new file mode 100644
index 0000000..5fd50f3
--- /dev/null
+++ b/metron-interface/metron-config/src/app/service/authentication.service.ts
@@ -0,0 +1,92 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {Injectable, EventEmitter, Inject}     from '@angular/core';
+import {Http, Headers, RequestOptions, Response} from '@angular/http';
+import {Router} from '@angular/router';
+import {Observable}     from 'rxjs/Observable';
+import {IAppConfig} from '../app.config.interface';
+import {APP_CONFIG} from '../app.config';
+
+@Injectable()
+export class AuthenticationService {
+
+  private static USER_NOT_VERIFIED: string = 'USER-NOT-VERIFIED';
+  private currentUser: string = AuthenticationService.USER_NOT_VERIFIED;
+  loginUrl: string = this.config.apiEndpoint + '/user';
+  logoutUrl: string = '/logout';
+  defaultHeaders = {'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest'};
+  onLoginEvent: EventEmitter<boolean> = new EventEmitter<boolean>();
+
+  constructor(private http: Http, private router: Router, @Inject(APP_CONFIG) private config: IAppConfig) {
+    this.init();
+  }
+
+  public init() {
+      this.getCurrentUser(new RequestOptions({headers: new Headers(this.defaultHeaders)})).subscribe((response: Response) => {
+        this.currentUser = response.text();
+        if (this.currentUser) {
+          this.onLoginEvent.emit(true);
+        }
+      }, error => {
+        this.onLoginEvent.emit(false);
+      });
+  }
+
+  public login(username: string, password: string, onError): void {
+    let loginHeaders: Headers = new Headers(this.defaultHeaders);
+    loginHeaders.append('authorization', 'Basic ' + btoa(username + ':' + password));
+    let loginOptions: RequestOptions = new RequestOptions({headers: loginHeaders});
+    this.getCurrentUser(loginOptions).subscribe((response: Response) => {
+        this.currentUser = response.text();
+        this.router.navigateByUrl('/sensors');
+        this.onLoginEvent.emit(true);
+      },
+      error => {
+        onError(error);
+      });
+  }
+
+  public logout(): void {
+    this.http.post(this.logoutUrl, {}, new RequestOptions({headers: new Headers(this.defaultHeaders)})).subscribe(response => {
+        this.currentUser = AuthenticationService.USER_NOT_VERIFIED;
+        this.onLoginEvent.emit(false);
+        this.router.navigateByUrl('/login');
+      },
+      error => {
+        console.log(error);
+      });
+  }
+
+  public checkAuthentication() {
+    if (!this.isAuthenticated()) {
+      this.router.navigateByUrl('/login');
+    }
+  }
+
+  public getCurrentUser(options: RequestOptions): Observable<Response> {
+    return this.http.get(this.loginUrl, options);
+  }
+
+  public isAuthenticationChecked(): boolean {
+    return this.currentUser !== AuthenticationService.USER_NOT_VERIFIED;
+  }
+
+  public isAuthenticated(): boolean {
+    return this.currentUser !== AuthenticationService.USER_NOT_VERIFIED && this.currentUser != null;
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/service/global-config.service.spec.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/service/global-config.service.spec.ts b/metron-interface/metron-config/src/app/service/global-config.service.spec.ts
new file mode 100644
index 0000000..f53c3f3
--- /dev/null
+++ b/metron-interface/metron-config/src/app/service/global-config.service.spec.ts
@@ -0,0 +1,99 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {async, inject, TestBed} from '@angular/core/testing';
+import {MockBackend, MockConnection} from '@angular/http/testing';
+import {HttpModule, XHRBackend, Response, ResponseOptions, Http} from '@angular/http';
+import '../rxjs-operators';
+import {APP_CONFIG, METRON_REST_CONFIG} from '../app.config';
+import {IAppConfig} from '../app.config.interface';
+import {GlobalConfigService} from './global-config.service';
+
+describe('GlobalConfigService', () => {
+
+  beforeEach(async(() => {
+    TestBed.configureTestingModule({
+      imports: [HttpModule],
+      providers: [
+        GlobalConfigService,
+        {provide: XHRBackend, useClass: MockBackend},
+        {provide: APP_CONFIG, useValue: METRON_REST_CONFIG}
+      ]
+    })
+      .compileComponents();
+  }));
+
+  it('can instantiate service when inject service',
+    inject([GlobalConfigService], (service: GlobalConfigService) => {
+      expect(service instanceof GlobalConfigService).toBe(true);
+    }));
+
+  it('can instantiate service with "new"', inject([Http, APP_CONFIG], (http: Http, config: IAppConfig) => {
+    expect(http).not.toBeNull('http should be provided');
+    let service = new GlobalConfigService(http, config);
+    expect(service instanceof GlobalConfigService).toBe(true, 'new service should be ok');
+  }));
+
+
+  it('can provide the mockBackend as XHRBackend',
+    inject([XHRBackend], (backend: MockBackend) => {
+      expect(backend).not.toBeNull('backend should be provided');
+    }));
+
+  describe('when service functions', () => {
+    let globalConfigService: GlobalConfigService;
+    let mockBackend: MockBackend;
+    let globalConfig = {'field': 'value'};
+    let globalConfigResponse: Response;
+    let deleteResponse: Response;
+
+    beforeEach(inject([Http, XHRBackend, APP_CONFIG], (http: Http, be: MockBackend, config: IAppConfig) => {
+      mockBackend = be;
+      globalConfigService = new GlobalConfigService(http, config);
+      globalConfigResponse = new Response(new ResponseOptions({status: 200, body: globalConfig}));
+    }));
+
+    it('post', async(inject([], () => {
+      mockBackend.connections.subscribe((c: MockConnection) => c.mockRespond(globalConfigResponse));
+
+      globalConfigService.post(globalConfig).subscribe(
+      result => {
+        expect(result).toEqual(globalConfig);
+      }, error => console.log(error));
+    })));
+
+    it('get', async(inject([], () => {
+      mockBackend.connections.subscribe((c: MockConnection) => c.mockRespond(globalConfigResponse));
+
+      globalConfigService.get().subscribe(
+        result => {
+          expect(result).toEqual(globalConfig);
+        }, error => console.log(error));
+    })));
+
+    it('deleteSensorParserConfigs', async(inject([], () => {
+      mockBackend.connections.subscribe((c: MockConnection) => c.mockRespond(deleteResponse));
+
+      globalConfigService.delete().subscribe(result => {
+        expect(result.status).toEqual(200);
+      });
+    })));
+  });
+
+});
+
+

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/service/global-config.service.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/service/global-config.service.ts b/metron-interface/metron-config/src/app/service/global-config.service.ts
new file mode 100644
index 0000000..1ed4325
--- /dev/null
+++ b/metron-interface/metron-config/src/app/service/global-config.service.ts
@@ -0,0 +1,75 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {Injectable, Inject} from '@angular/core';
+import {Http, Headers, RequestOptions, Response, ResponseOptions} from '@angular/http';
+import {Observable} from 'rxjs/Observable';
+import {HttpUtil} from '../util/httpUtil';
+import {IAppConfig} from '../app.config.interface';
+import {APP_CONFIG} from '../app.config';
+
+@Injectable()
+export class GlobalConfigService {
+  url = this.config.apiEndpoint + '/global/config';
+  defaultHeaders = {'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest'};
+
+  private globalConfig = {
+
+  };
+
+  constructor(private http: Http, @Inject(APP_CONFIG) private config: IAppConfig) {
+    this.globalConfig['solr.collection'] = 'metron';
+    this.globalConfig['storm.indexingWorkers'] = 1;
+    this.globalConfig['storm.indexingExecutors'] = 2;
+    this.globalConfig['hdfs.boltBatchSize'] = 5000;
+    this.globalConfig['hdfs.boltFieldDelimiter'] = '|';
+    this.globalConfig['hdfs.boltFileRotationSize'] = 5;
+    this.globalConfig['hdfs.boltCompressionCodecClass'] = 'org.apache.hadoop.io.compress.SnappyCodec';
+    this.globalConfig['hdfs.indexOutput'] = '/tmp/metron/enriched';
+    this.globalConfig['kafkaWriter.topic'] = 'outputTopic';
+    this.globalConfig['kafkaWriter.keySerializer'] = 'org.apache.kafka.common.serialization.StringSerializer';
+    this.globalConfig['kafkaWriter.valueSerializer'] = 'org.apache.kafka.common.serialization.StringSerializer';
+    this.globalConfig['kafkaWriter.requestRequiredAcks'] = 1;
+    this.globalConfig['solrWriter.indexName'] = 'alfaalfa';
+    this.globalConfig['solrWriter.shards'] = 1;
+    this.globalConfig['solrWriter.replicationFactor'] = 1;
+    this.globalConfig['solrWriter.batchSize'] = 50;
+  }
+
+  public post(globalConfig: {}): Observable<{}> {
+    return this.http.post(this.url, globalConfig, new RequestOptions({headers: new Headers(this.defaultHeaders)}))
+      .map(HttpUtil.extractData)
+      .catch(HttpUtil.handleError);
+  }
+
+  public get(): Observable<{}> {
+    return this.http.get(this.url , new RequestOptions({headers: new Headers(this.defaultHeaders)}))
+      .map(HttpUtil.extractData)
+      .catch(HttpUtil.handleError);
+  }
+
+  public delete(): Observable<Response> {
+    let responseOptions = new ResponseOptions();
+    responseOptions.status = 200;
+    let response = new Response(responseOptions);
+    return Observable.create(observer => {
+      observer.next(response);
+      observer.complete();
+    });
+  }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/service/grok-validation.service.spec.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/service/grok-validation.service.spec.ts b/metron-interface/metron-config/src/app/service/grok-validation.service.spec.ts
new file mode 100644
index 0000000..da45a80
--- /dev/null
+++ b/metron-interface/metron-config/src/app/service/grok-validation.service.spec.ts
@@ -0,0 +1,106 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {async, inject, TestBed} from '@angular/core/testing';
+import {MockBackend, MockConnection} from '@angular/http/testing';
+import {GrokValidationService} from './grok-validation.service';
+import {GrokValidation} from '../model/grok-validation';
+import {HttpModule, XHRBackend, Response, ResponseOptions, Http} from '@angular/http';
+import '../rxjs-operators';
+import {APP_CONFIG, METRON_REST_CONFIG} from '../app.config';
+import {IAppConfig} from '../app.config.interface';
+
+describe('GrokValidationService', () => {
+
+  beforeEach(async(() => {
+    TestBed.configureTestingModule({
+      imports: [HttpModule],
+      providers: [
+        GrokValidationService,
+        {provide: XHRBackend, useClass: MockBackend},
+        {provide: APP_CONFIG, useValue: METRON_REST_CONFIG}
+      ]
+    })
+      .compileComponents();
+  }));
+
+  it('can instantiate service when inject service',
+    inject([GrokValidationService], (service: GrokValidationService) => {
+      expect(service instanceof GrokValidationService).toBe(true);
+    }));
+
+  it('can instantiate service with "new"', inject([Http, APP_CONFIG], (http: Http, config: IAppConfig) => {
+    expect(http).not.toBeNull('http should be provided');
+    let service = new GrokValidationService(http, config);
+    expect(service instanceof GrokValidationService).toBe(true, 'new service should be ok');
+  }));
+
+
+  it('can provide the mockBackend as XHRBackend',
+    inject([XHRBackend], (backend: MockBackend) => {
+      expect(backend).not.toBeNull('backend should be provided');
+    }));
+
+  describe('when service functions', () => {
+    let grokValidationService: GrokValidationService;
+    let mockBackend: MockBackend;
+    let grokValidation = new GrokValidation();
+    grokValidation.statement = 'statement';
+    grokValidation.sampleData = 'sampleData';
+    grokValidation.results = {'results': 'results'};
+    let grokList = ['pattern'];
+    let grokStatement = 'grok statement';
+    let grokValidationResponse: Response;
+    let grokListResponse: Response;
+    let grokGetStatementResponse: Response;
+
+    beforeEach(inject([Http, XHRBackend, APP_CONFIG], (http: Http, be: MockBackend, config: IAppConfig) => {
+      mockBackend = be;
+      grokValidationService = new GrokValidationService(http, config);
+      grokValidationResponse = new Response(new ResponseOptions({status: 200, body: grokValidation}));
+      grokListResponse = new Response(new ResponseOptions({status: 200, body: grokList}));
+      grokGetStatementResponse = new Response(new ResponseOptions({status: 200, body: grokStatement}));
+    }));
+
+    it('validate', async(inject([], () => {
+      mockBackend.connections.subscribe((c: MockConnection) => c.mockRespond(grokValidationResponse));
+
+      grokValidationService.validate(grokValidation).subscribe(
+        result => {
+          expect(result).toEqual(grokValidation);
+        }, error => console.log(error));
+    })));
+
+    it('list', async(inject([], () => {
+      mockBackend.connections.subscribe((c: MockConnection) => c.mockRespond(grokListResponse));
+      grokValidationService.list().subscribe(
+        results => {
+          expect(results).toEqual(grokList);
+        }, error => console.log(error));
+    })));
+
+    it('getStatement', async(inject([], () => {
+      mockBackend.connections.subscribe((c: MockConnection) => c.mockRespond(grokGetStatementResponse));
+      grokValidationService.getStatement('/path').subscribe(
+          results => {
+            expect(results).toEqual(grokStatement);
+          }, error => console.log(error));
+    })));
+  });
+
+
+});

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/service/grok-validation.service.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/service/grok-validation.service.ts b/metron-interface/metron-config/src/app/service/grok-validation.service.ts
new file mode 100644
index 0000000..bcdce82
--- /dev/null
+++ b/metron-interface/metron-config/src/app/service/grok-validation.service.ts
@@ -0,0 +1,56 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {Injectable, Inject} from '@angular/core';
+import {Http, Headers, RequestOptions, URLSearchParams} from '@angular/http';
+import {Observable} from 'rxjs/Observable';
+import {GrokValidation} from '../model/grok-validation';
+import {HttpUtil} from '../util/httpUtil';
+import {IAppConfig} from '../app.config.interface';
+import {APP_CONFIG} from '../app.config';
+
+@Injectable()
+export class GrokValidationService {
+  url = this.config.apiEndpoint + '/grok';
+  defaultHeaders = {'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest'};
+
+  constructor(private http: Http, @Inject(APP_CONFIG) private config: IAppConfig) {
+
+  }
+
+  public validate(grokValidation: GrokValidation): Observable<GrokValidation> {
+    return this.http.post(this.url + '/validate', JSON.stringify(grokValidation),
+      new RequestOptions({headers: new Headers(this.defaultHeaders)}))
+      .map(HttpUtil.extractData)
+      .catch(HttpUtil.handleError);
+  }
+
+  public list(): Observable<string[]> {
+    return this.http.get(this.url + '/list', new RequestOptions({headers: new Headers(this.defaultHeaders)}))
+      .map(HttpUtil.extractData)
+      .catch(HttpUtil.handleError);
+  }
+
+  public getStatement(path: string): Observable<string> {
+    let params: URLSearchParams = new URLSearchParams();
+    params.set('path', path);
+    return this.http.get(this.url + '/get/statement', new RequestOptions({headers: new Headers(this.defaultHeaders), search: params}))
+        .map(HttpUtil.extractString)
+        .catch(HttpUtil.handleError);
+  }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/service/hdfs.service.spec.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/service/hdfs.service.spec.ts b/metron-interface/metron-config/src/app/service/hdfs.service.spec.ts
new file mode 100644
index 0000000..16196ab
--- /dev/null
+++ b/metron-interface/metron-config/src/app/service/hdfs.service.spec.ts
@@ -0,0 +1,110 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {async, inject, TestBed} from '@angular/core/testing';
+import {MockBackend, MockConnection} from '@angular/http/testing';
+import {HttpModule, XHRBackend, Response, ResponseOptions, Http} from '@angular/http';
+import '../rxjs-operators';
+import {APP_CONFIG, METRON_REST_CONFIG} from '../app.config';
+import {IAppConfig} from '../app.config.interface';
+import {HdfsService} from './hdfs.service';
+
+describe('HdfsService', () => {
+
+  beforeEach(async(() => {
+    TestBed.configureTestingModule({
+      imports: [HttpModule],
+      providers: [
+        HdfsService,
+        {provide: XHRBackend, useClass: MockBackend},
+        {provide: APP_CONFIG, useValue: METRON_REST_CONFIG}
+      ]
+    })
+      .compileComponents();
+  }));
+
+  it('can instantiate service when inject service',
+    inject([HdfsService], (service: HdfsService) => {
+      expect(service instanceof HdfsService).toBe(true);
+    }));
+
+  it('can instantiate service with "new"', inject([Http, APP_CONFIG], (http: Http, config: IAppConfig) => {
+    expect(http).not.toBeNull('http should be provided');
+    let service = new HdfsService(http, config);
+    expect(service instanceof HdfsService).toBe(true, 'new service should be ok');
+  }));
+
+
+  it('can provide the mockBackend as XHRBackend',
+    inject([XHRBackend], (backend: MockBackend) => {
+      expect(backend).not.toBeNull('backend should be provided');
+    }));
+
+  describe('when service functions', () => {
+    let hdfsService: HdfsService;
+    let mockBackend: MockBackend;
+    let fileList = ['file1', 'file2'];
+    let contents = 'file contents';
+    let listResponse: Response;
+    let readResponse: Response;
+    let postResponse: Response;
+    let deleteResponse: Response;
+
+    beforeEach(inject([Http, XHRBackend, APP_CONFIG], (http: Http, be: MockBackend, config: IAppConfig) => {
+      mockBackend = be;
+      hdfsService = new HdfsService(http, config);
+      listResponse = new Response(new ResponseOptions({status: 200, body: fileList}));
+      readResponse = new Response(new ResponseOptions({status: 200, body: contents}));
+      postResponse = new Response(new ResponseOptions({status: 200}));
+      deleteResponse = new Response(new ResponseOptions({status: 200}));
+    }));
+
+    it('list', async(inject([], () => {
+      mockBackend.connections.subscribe((c: MockConnection) => c.mockRespond(listResponse));
+      hdfsService.list('/path').subscribe(
+        result => {
+          expect(result).toEqual(fileList);
+        }, error => console.log(error));
+    })));
+
+    it('read', async(inject([], () => {
+      mockBackend.connections.subscribe((c: MockConnection) => c.mockRespond(readResponse));
+      hdfsService.read('/path').subscribe(
+        result => {
+          expect(result).toEqual(contents);
+        }, error => console.log(error));
+    })));
+
+    it('post', async(inject([], () => {
+      mockBackend.connections.subscribe((c: MockConnection) => c.mockRespond(postResponse));
+      hdfsService.post('/path', contents).subscribe(
+          result => {
+            expect(result.status).toEqual(200);
+          }, error => console.log(error));
+    })));
+
+    it('deleteFile', async(inject([], () => {
+      mockBackend.connections.subscribe((c: MockConnection) => c.mockRespond(deleteResponse));
+      hdfsService.deleteFile('/path').subscribe(
+          result => {
+            expect(result.status).toEqual(200);
+          }, error => console.log(error));
+    })));
+  });
+
+
+});

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/service/hdfs.service.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/service/hdfs.service.ts b/metron-interface/metron-config/src/app/service/hdfs.service.ts
new file mode 100644
index 0000000..4e4b808
--- /dev/null
+++ b/metron-interface/metron-config/src/app/service/hdfs.service.ts
@@ -0,0 +1,63 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {Injectable, Inject} from '@angular/core';
+import {Http, Headers, RequestOptions, Response, URLSearchParams} from '@angular/http';
+import {Observable} from 'rxjs/Observable';
+import {HttpUtil} from '../util/httpUtil';
+import {IAppConfig} from '../app.config.interface';
+import {APP_CONFIG} from '../app.config';
+
+@Injectable()
+export class HdfsService {
+  url = this.config.apiEndpoint + '/hdfs';
+  defaultHeaders = {'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest'};
+
+  constructor(private http: Http, @Inject(APP_CONFIG) private config: IAppConfig) {
+  }
+
+  public list(path: string): Observable<string[]> {
+    let params: URLSearchParams = new URLSearchParams();
+    params.set('path', path);
+    return this.http.get(this.url + '/list', new RequestOptions({headers: new Headers(this.defaultHeaders), search: params}))
+      .map(HttpUtil.extractData)
+      .catch(HttpUtil.handleError);
+  }
+
+  public read(path: string): Observable<string> {
+    let params: URLSearchParams = new URLSearchParams();
+    params.set('path', path);
+    return this.http.get(this.url , new RequestOptions({headers: new Headers(this.defaultHeaders), search: params}))
+      .map(HttpUtil.extractString)
+      .catch(HttpUtil.handleError);
+  }
+
+  public post(path: string, contents: string): Observable<Response> {
+    let params: URLSearchParams = new URLSearchParams();
+    params.set('path', path);
+    return this.http.post(this.url, contents, new RequestOptions({headers: new Headers(this.defaultHeaders), search: params}))
+        .catch(HttpUtil.handleError);
+  }
+
+  public deleteFile(path: string): Observable<Response> {
+    let params: URLSearchParams = new URLSearchParams();
+    params.set('path', path);
+    return this.http.delete(this.url, new RequestOptions({headers: new Headers(this.defaultHeaders), search: params}))
+        .catch(HttpUtil.handleError);
+  }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/service/kafka.service.spec.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/service/kafka.service.spec.ts b/metron-interface/metron-config/src/app/service/kafka.service.spec.ts
new file mode 100644
index 0000000..e6f1d7f
--- /dev/null
+++ b/metron-interface/metron-config/src/app/service/kafka.service.spec.ts
@@ -0,0 +1,114 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {async, inject, TestBed} from '@angular/core/testing';
+import {MockBackend, MockConnection} from '@angular/http/testing';
+import {KafkaService} from './kafka.service';
+import {KafkaTopic} from '../model/kafka-topic';
+import {HttpModule, XHRBackend, Response, ResponseOptions, Http} from '@angular/http';
+import '../rxjs-operators';
+import {APP_CONFIG, METRON_REST_CONFIG} from '../app.config';
+import {IAppConfig} from '../app.config.interface';
+
+describe('KafkaService', () => {
+
+  beforeEach(async(() => {
+    TestBed.configureTestingModule({
+      imports: [HttpModule],
+      providers: [
+        KafkaService,
+        {provide: XHRBackend, useClass: MockBackend},
+        {provide: APP_CONFIG, useValue: METRON_REST_CONFIG}
+      ]
+    })
+      .compileComponents();
+  }));
+
+  it('can instantiate service when inject service',
+    inject([KafkaService], (service: KafkaService) => {
+      expect(service instanceof KafkaService).toBe(true);
+    }));
+
+  it('can instantiate service with "new"', inject([Http, APP_CONFIG], (http: Http, config: IAppConfig) => {
+    expect(http).not.toBeNull('http should be provided');
+    let service = new KafkaService(http, config);
+    expect(service instanceof KafkaService).toBe(true, 'new service should be ok');
+  }));
+
+  it('can provide the mockBackend as XHRBackend',
+    inject([XHRBackend], (backend: MockBackend) => {
+      expect(backend).not.toBeNull('backend should be provided');
+    }));
+
+  describe('when service functions', () => {
+    let kafkaService: KafkaService;
+    let mockBackend: MockBackend;
+    let kafkaTopic = new KafkaTopic();
+    kafkaTopic.name = 'bro';
+    kafkaTopic.numPartitions = 1;
+    kafkaTopic.replicationFactor = 1;
+    let sampleMessage = 'sample message';
+    let kafkaResponse: Response;
+    let kafkaListResponse: Response;
+    let sampleMessageResponse: Response;
+
+    beforeEach(inject([Http, XHRBackend, APP_CONFIG], (http: Http, be: MockBackend, config: IAppConfig) => {
+      mockBackend = be;
+      kafkaService = new KafkaService(http, config);
+      kafkaResponse = new Response(new ResponseOptions({status: 200, body: kafkaTopic}));
+      kafkaListResponse = new Response(new ResponseOptions({status: 200, body: [kafkaTopic]}));
+      sampleMessageResponse = new Response(new ResponseOptions({status: 200, body: sampleMessage}));
+    }));
+
+    it('post', async(inject([], () => {
+      mockBackend.connections.subscribe((c: MockConnection) => c.mockRespond(kafkaResponse));
+
+      kafkaService.post(kafkaTopic).subscribe(
+        result => {
+          expect(result).toEqual(kafkaTopic);
+        }, error => console.log(error));
+    })));
+
+    it('get', async(inject([], () => {
+      mockBackend.connections.subscribe((c: MockConnection) => c.mockRespond(kafkaResponse));
+
+      kafkaService.get('bro').subscribe(
+        result => {
+          expect(result).toEqual(kafkaTopic);
+        }, error => console.log(error));
+    })));
+
+    it('list', async(inject([], () => {
+      mockBackend.connections.subscribe((c: MockConnection) => c.mockRespond(kafkaListResponse));
+
+      kafkaService.list().subscribe(
+        result => {
+          expect(result).toEqual([kafkaTopic]);
+        }, error => console.log(error));
+    })));
+
+    it('sample', async(inject([], () => {
+      mockBackend.connections.subscribe((c: MockConnection) => c.mockRespond(sampleMessageResponse));
+      kafkaService.sample('bro').subscribe(
+        result => {
+          expect(result).toEqual(sampleMessage);
+        }, error => console.log(error));
+    })));
+  });
+
+});
+

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/service/kafka.service.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/service/kafka.service.ts b/metron-interface/metron-config/src/app/service/kafka.service.ts
new file mode 100644
index 0000000..ac02366
--- /dev/null
+++ b/metron-interface/metron-config/src/app/service/kafka.service.ts
@@ -0,0 +1,59 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {Injectable, Inject} from '@angular/core';
+import {Http, Headers, RequestOptions} from '@angular/http';
+import {Observable} from 'rxjs/Observable';
+import {KafkaTopic} from '../model/kafka-topic';
+import {HttpUtil} from '../util/httpUtil';
+import {IAppConfig} from '../app.config.interface';
+import {APP_CONFIG} from '../app.config';
+
+@Injectable()
+export class KafkaService {
+  url = this.config.apiEndpoint + '/kafka/topic';
+  defaultHeaders = {'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest'};
+
+  constructor(private http: Http, @Inject(APP_CONFIG) private config: IAppConfig) {
+
+  }
+
+  public post(kafkaTopic: KafkaTopic): Observable<KafkaTopic> {
+    return this.http.post(this.url, JSON.stringify(kafkaTopic), new RequestOptions({headers: new Headers(this.defaultHeaders)}))
+      .map(HttpUtil.extractData)
+      .catch(HttpUtil.handleError);
+  }
+
+  public get(name: string): Observable<KafkaTopic> {
+    return this.http.get(this.url + '/' + name, new RequestOptions({headers: new Headers(this.defaultHeaders)}))
+      .map(HttpUtil.extractData)
+      .catch(HttpUtil.handleError);
+  }
+
+  public list(): Observable<string[]> {
+    return this.http.get(this.url, new RequestOptions({headers: new Headers(this.defaultHeaders)}))
+      .map(HttpUtil.extractData)
+      .catch(HttpUtil.handleError);
+  }
+
+  public sample(name: string): Observable<string> {
+    return this.http.get(this.url + '/' + name + '/sample', new RequestOptions({headers: new Headers(this.defaultHeaders)}))
+      .map(HttpUtil.extractString)
+      .catch(HttpUtil.handleError);
+  }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/service/sensor-enrichment-config.service.spec.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/service/sensor-enrichment-config.service.spec.ts b/metron-interface/metron-config/src/app/service/sensor-enrichment-config.service.spec.ts
new file mode 100644
index 0000000..89863ee
--- /dev/null
+++ b/metron-interface/metron-config/src/app/service/sensor-enrichment-config.service.spec.ts
@@ -0,0 +1,144 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {async, inject, TestBed} from '@angular/core/testing';
+import {MockBackend, MockConnection} from '@angular/http/testing';
+import {SensorEnrichmentConfigService} from './sensor-enrichment-config.service';
+import {SensorEnrichmentConfig, EnrichmentConfig} from '../model/sensor-enrichment-config';
+import {HttpModule, XHRBackend, Response, ResponseOptions, Http} from '@angular/http';
+import '../rxjs-operators';
+import {METRON_REST_CONFIG, APP_CONFIG} from '../app.config';
+import {IAppConfig} from '../app.config.interface';
+
+describe('SensorEnrichmentConfigService', () => {
+
+  beforeEach(async(() => {
+    TestBed.configureTestingModule({
+      imports: [HttpModule],
+      providers: [
+        SensorEnrichmentConfigService,
+        {provide: XHRBackend, useClass: MockBackend},
+        {provide: APP_CONFIG, useValue: METRON_REST_CONFIG}
+      ]
+    })
+        .compileComponents();
+  }));
+
+  it('can instantiate service when inject service',
+      inject([SensorEnrichmentConfigService], (service: SensorEnrichmentConfigService) => {
+        expect(service instanceof SensorEnrichmentConfigService).toBe(true);
+      }));
+
+  it('can instantiate service with "new"', inject([Http, APP_CONFIG], (http: Http, config: IAppConfig) => {
+    expect(http).not.toBeNull('http should be provided');
+    let service = new SensorEnrichmentConfigService(http, config);
+    expect(service instanceof SensorEnrichmentConfigService).toBe(true, 'new service should be ok');
+  }));
+
+
+  it('can provide the mockBackend as XHRBackend',
+      inject([XHRBackend], (backend: MockBackend) => {
+        expect(backend).not.toBeNull('backend should be provided');
+      }));
+
+  describe('when service functions', () => {
+    let sensorEnrichmentConfigService: SensorEnrichmentConfigService;
+    let mockBackend: MockBackend;
+    let sensorEnrichmentConfig1 = new SensorEnrichmentConfig();
+    let enrichmentConfig1 = new EnrichmentConfig();
+    enrichmentConfig1.fieldMap = {'geo': ['ip_dst_addr'], 'host': ['ip_dst_addr']};
+    sensorEnrichmentConfig1.enrichment.fieldMap = enrichmentConfig1;
+    let sensorEnrichmentConfig2 = new SensorEnrichmentConfig();
+    let enrichmentConfig2 = new EnrichmentConfig();
+    enrichmentConfig1.fieldMap = {'whois': ['ip_dst_addr'], 'host': ['ip_src_addr']};
+    sensorEnrichmentConfig2.enrichment = enrichmentConfig2;
+    let availableEnrichments: string[] = ['geo', 'host', 'whois'];
+    let availableThreatTriageAggregators: string[] = ['MAX', 'MIN', 'SUM', 'MEAN', 'POSITIVE_MEAN'];
+    let sensorEnrichmentConfigResponse: Response;
+    let sensorEnrichmentConfigsResponse: Response;
+    let availableEnrichmentsResponse: Response;
+    let availableThreatTriageAggregatorsResponse: Response;
+    let deleteResponse: Response;
+
+    beforeEach(inject([Http, XHRBackend, APP_CONFIG], (http: Http, be: MockBackend, config: IAppConfig) => {
+      mockBackend = be;
+      sensorEnrichmentConfigService = new SensorEnrichmentConfigService(http, config);
+      sensorEnrichmentConfigResponse = new Response(new ResponseOptions({status: 200, body: sensorEnrichmentConfig1}));
+      sensorEnrichmentConfigsResponse = new Response(new ResponseOptions({status: 200, body: [sensorEnrichmentConfig1,
+        sensorEnrichmentConfig2]}));
+      availableEnrichmentsResponse = new Response(new ResponseOptions({status: 200, body: availableEnrichments}));
+      availableThreatTriageAggregatorsResponse = new Response(new ResponseOptions({status: 200, body: availableThreatTriageAggregators}));
+      deleteResponse = new Response(new ResponseOptions({status: 200}));
+    }));
+
+    it('post', async(inject([], () => {
+      mockBackend.connections.subscribe((c: MockConnection) => c.mockRespond(sensorEnrichmentConfigResponse));
+
+      sensorEnrichmentConfigService.post('bro', sensorEnrichmentConfig1).subscribe(
+          result => {
+            expect(result).toEqual(sensorEnrichmentConfig1);
+          }, error => console.log(error));
+    })));
+
+    it('get', async(inject([], () => {
+      mockBackend.connections.subscribe((c: MockConnection) => c.mockRespond(sensorEnrichmentConfigResponse));
+
+      sensorEnrichmentConfigService.get('bro').subscribe(
+          result => {
+            expect(result).toEqual(sensorEnrichmentConfig1);
+          }, error => console.log(error));
+    })));
+
+    it('getAll', async(inject([], () => {
+      mockBackend.connections.subscribe((c: MockConnection) => c.mockRespond(sensorEnrichmentConfigsResponse));
+
+      sensorEnrichmentConfigService.getAll().subscribe(
+          results => {
+            expect(results).toEqual([sensorEnrichmentConfig1, sensorEnrichmentConfig2]);
+          }, error => console.log(error));
+    })));
+
+    it('getAvailableEnrichments', async(inject([], () => {
+      mockBackend.connections.subscribe((c: MockConnection) => c.mockRespond(availableEnrichmentsResponse));
+
+      sensorEnrichmentConfigService.getAvailableEnrichments().subscribe(
+          results => {
+            expect(results).toEqual(availableEnrichments);
+          }, error => console.log(error));
+    })));
+
+    it('getAvailableThreatTriageAggregators', async(inject([], () => {
+      mockBackend.connections.subscribe((c: MockConnection) => c.mockRespond(availableThreatTriageAggregatorsResponse));
+
+      sensorEnrichmentConfigService.getAvailableThreatTriageAggregators().subscribe(
+          results => {
+            expect(results).toEqual(availableThreatTriageAggregators);
+          }, error => console.log(error));
+    })));
+
+    it('deleteSensorEnrichments', async(inject([], () => {
+      mockBackend.connections.subscribe((c: MockConnection) => c.mockRespond(deleteResponse));
+
+      sensorEnrichmentConfigService.deleteSensorEnrichments('bro').subscribe(result => {
+        expect(result.status).toEqual(200);
+      });
+    })));
+  });
+
+});
+
+

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/service/sensor-enrichment-config.service.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/service/sensor-enrichment-config.service.ts b/metron-interface/metron-config/src/app/service/sensor-enrichment-config.service.ts
new file mode 100644
index 0000000..90c314b
--- /dev/null
+++ b/metron-interface/metron-config/src/app/service/sensor-enrichment-config.service.ts
@@ -0,0 +1,71 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {Injectable, Inject} from '@angular/core';
+import {Http, Headers, RequestOptions, Response} from '@angular/http';
+import {Observable} from 'rxjs/Observable';
+import {SensorEnrichmentConfig} from '../model/sensor-enrichment-config';
+import {HttpUtil} from '../util/httpUtil';
+import {IAppConfig} from '../app.config.interface';
+import {APP_CONFIG} from '../app.config';
+
+@Injectable()
+export class SensorEnrichmentConfigService {
+  url = this.config.apiEndpoint + '/sensor/enrichment/config';
+  defaultHeaders = {'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest'};
+
+  constructor(private http: Http, @Inject(APP_CONFIG) private config: IAppConfig) {
+  }
+
+  public post(name: string, sensorEnrichmentConfig: SensorEnrichmentConfig): Observable<SensorEnrichmentConfig> {
+    return this.http.post(this.url + '/' + name, JSON.stringify(sensorEnrichmentConfig),
+                          new RequestOptions({headers: new Headers(this.defaultHeaders)}))
+      .map(HttpUtil.extractData)
+      .catch(HttpUtil.handleError);
+  }
+
+  public get(name: string): Observable<SensorEnrichmentConfig> {
+    return this.http.get(this.url + '/' + name, new RequestOptions({headers: new Headers(this.defaultHeaders)}))
+      .map(HttpUtil.extractData)
+      .catch(HttpUtil.handleError);
+  }
+
+  public getAll(): Observable<SensorEnrichmentConfig[]> {
+    return this.http.get(this.url, new RequestOptions({headers: new Headers(this.defaultHeaders)}))
+      .map(HttpUtil.extractData)
+      .catch(HttpUtil.handleError);
+  }
+
+  public deleteSensorEnrichments(name: string): Observable<Response> {
+    return this.http.delete(this.url + '/' + name, new RequestOptions({headers: new Headers(this.defaultHeaders)}))
+      .catch(HttpUtil.handleError);
+  }
+
+  public getAvailableEnrichments(): Observable<string[]> {
+    return this.http.get(this.url + '/list/available/enrichments', new RequestOptions({headers: new Headers(this.defaultHeaders)}))
+        .map(HttpUtil.extractData)
+        .catch(HttpUtil.handleError);
+  }
+
+  public getAvailableThreatTriageAggregators(): Observable<string[]> {
+    return this.http.get(this.url + '/list/available/threat/triage/aggregators',
+        new RequestOptions({headers: new Headers(this.defaultHeaders)}))
+        .map(HttpUtil.extractData)
+        .catch(HttpUtil.handleError);
+  }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/service/sensor-indexing-config.service.spec.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/service/sensor-indexing-config.service.spec.ts b/metron-interface/metron-config/src/app/service/sensor-indexing-config.service.spec.ts
new file mode 100644
index 0000000..3640162
--- /dev/null
+++ b/metron-interface/metron-config/src/app/service/sensor-indexing-config.service.spec.ts
@@ -0,0 +1,118 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {async, inject, TestBed} from '@angular/core/testing';
+import {MockBackend, MockConnection} from '@angular/http/testing';
+import {HttpModule, XHRBackend, Response, ResponseOptions, Http} from '@angular/http';
+import '../rxjs-operators';
+import {METRON_REST_CONFIG, APP_CONFIG} from '../app.config';
+import {IAppConfig} from '../app.config.interface';
+import {SensorIndexingConfigService} from './sensor-indexing-config.service';
+import {IndexingConfigurations} from '../model/sensor-indexing-config';
+
+describe('SensorIndexingConfigService', () => {
+
+  beforeEach(async(() => {
+    TestBed.configureTestingModule({
+      imports: [HttpModule],
+      providers: [
+        SensorIndexingConfigService,
+        {provide: XHRBackend, useClass: MockBackend},
+        {provide: APP_CONFIG, useValue: METRON_REST_CONFIG}
+      ]
+    })
+        .compileComponents();
+  }));
+
+  it('can instantiate service when inject service',
+      inject([SensorIndexingConfigService], (service: SensorIndexingConfigService) => {
+        expect(service instanceof SensorIndexingConfigService).toBe(true);
+      }));
+
+  it('can instantiate service with "new"', inject([Http, APP_CONFIG], (http: Http, config: IAppConfig) => {
+    expect(http).not.toBeNull('http should be provided');
+    let service = new SensorIndexingConfigService(http, config);
+    expect(service instanceof SensorIndexingConfigService).toBe(true, 'new service should be ok');
+  }));
+
+
+  it('can provide the mockBackend as XHRBackend',
+      inject([XHRBackend], (backend: MockBackend) => {
+        expect(backend).not.toBeNull('backend should be provided');
+      }));
+
+  describe('when service functions', () => {
+    let sensorIndexingConfigService: SensorIndexingConfigService;
+    let mockBackend: MockBackend;
+    let sensorIndexingConfig1 = new IndexingConfigurations();
+    sensorIndexingConfig1.hdfs.index = 'squid';
+    sensorIndexingConfig1.hdfs.batchSize = 1;
+    let sensorIndexingConfig2 = new IndexingConfigurations();
+    sensorIndexingConfig2.hdfs.index = 'yaf';
+    sensorIndexingConfig2.hdfs.batchSize = 2;
+    let sensorIndexingConfigResponse: Response;
+    let sensorIndexingConfigsResponse: Response;
+    let deleteResponse: Response;
+
+    beforeEach(inject([Http, XHRBackend, APP_CONFIG], (http: Http, be: MockBackend, config: IAppConfig) => {
+      mockBackend = be;
+      sensorIndexingConfigService = new SensorIndexingConfigService(http, config);
+      sensorIndexingConfigResponse = new Response(new ResponseOptions({status: 200, body: sensorIndexingConfig1}));
+      sensorIndexingConfigsResponse = new Response(new ResponseOptions({status: 200, body: [sensorIndexingConfig1,
+        sensorIndexingConfig2]}));
+      deleteResponse = new Response(new ResponseOptions({status: 200}));
+    }));
+
+    it('post', async(inject([], () => {
+      mockBackend.connections.subscribe((c: MockConnection) => c.mockRespond(sensorIndexingConfigResponse));
+
+      sensorIndexingConfigService.post('squid', sensorIndexingConfig1).subscribe(
+          result => {
+            expect(result).toEqual(sensorIndexingConfig1);
+          }, error => console.log(error));
+    })));
+
+    it('get', async(inject([], () => {
+      mockBackend.connections.subscribe((c: MockConnection) => c.mockRespond(sensorIndexingConfigResponse));
+
+      sensorIndexingConfigService.get('squid').subscribe(
+          result => {
+            expect(result).toEqual(sensorIndexingConfig1);
+          }, error => console.log(error));
+    })));
+
+    it('getAll', async(inject([], () => {
+      mockBackend.connections.subscribe((c: MockConnection) => c.mockRespond(sensorIndexingConfigsResponse));
+
+      sensorIndexingConfigService.getAll().subscribe(
+          results => {
+            expect(results).toEqual([sensorIndexingConfig1, sensorIndexingConfig2]);
+          }, error => console.log(error));
+    })));
+
+    it('deleteSensorEnrichments', async(inject([], () => {
+      mockBackend.connections.subscribe((c: MockConnection) => c.mockRespond(deleteResponse));
+
+      sensorIndexingConfigService.deleteSensorIndexingConfig('squid').subscribe(result => {
+        expect(result.status).toEqual(200);
+      });
+    })));
+  });
+
+});
+
+