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:20 UTC

[09/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-field-schema/sensor-field-schema.component.spec.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/sensors/sensor-field-schema/sensor-field-schema.component.spec.ts b/metron-interface/metron-config/src/app/sensors/sensor-field-schema/sensor-field-schema.component.spec.ts
new file mode 100644
index 0000000..d2066ea
--- /dev/null
+++ b/metron-interface/metron-config/src/app/sensors/sensor-field-schema/sensor-field-schema.component.spec.ts
@@ -0,0 +1,523 @@
+/**
+ * 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:no-unused-variable */
+/* tslint:disable:max-line-length */
+
+import { TestBed, async, ComponentFixture } from '@angular/core/testing';
+import {Http} from '@angular/http';
+import {SimpleChanges, SimpleChange} from '@angular/core';
+import {SensorParserConfigService} from '../../service/sensor-parser-config.service';
+import {StellarService} from '../../service/stellar.service';
+import {MetronAlerts} from '../../shared/metron-alerts';
+import {SensorFieldSchemaModule} from './sensor-field-schema.module';
+import {SensorFieldSchemaComponent, FieldSchemaRow} from './sensor-field-schema.component';
+import {KafkaService} from '../../service/kafka.service';
+import {Observable} from 'rxjs/Observable';
+import {StellarFunctionDescription} from '../../model/stellar-function-description';
+import {SensorParserConfig} from '../../model/sensor-parser-config';
+import {SensorEnrichmentConfig, EnrichmentConfig, ThreatIntelConfig} from '../../model/sensor-enrichment-config';
+import {ParseMessageRequest} from '../../model/parse-message-request';
+import {AutocompleteOption} from '../../model/autocomplete-option';
+import {FieldTransformer} from '../../model/field-transformer';
+import {SensorEnrichmentConfigService} from '../../service/sensor-enrichment-config.service';
+
+
+class MockSensorParserConfigService {
+
+    parseMessage(parseMessageRequest: ParseMessageRequest): Observable<{}> {
+        let parsedJson = {
+            'elapsed': 415,
+            'code': 200,
+            'ip_dst_addr': '207.109.73.154',
+            'original_string': '1467011157.401 415 127.0.0.1 TCP_MISS/200 337891 GET http://www.aliexpress.com/',
+            'method': 'GET',
+            'bytes': 337891,
+            'action': 'TCP_MISS',
+            'ip_src_addr': '127.0.0.1',
+            'url': 'http://www.aliexpress.com/af/shoes.html?',
+            'timestamp': '1467011157.401'
+        };
+        return Observable.create((observable) => {
+            observable.next(parsedJson);
+            observable.complete();
+        });
+    }
+}
+
+class MockTransformationValidationService {
+    public listSimpleFunctions(): Observable<StellarFunctionDescription[]> {
+        let stellarFunctionDescription: StellarFunctionDescription[] = [];
+        stellarFunctionDescription.push(new StellarFunctionDescription('TO_LOWER', 'TO_LOWER description', ['input - input field']));
+        stellarFunctionDescription.push(new StellarFunctionDescription('TO_UPPER', 'TO_UPPER description', ['input - input field']));
+        stellarFunctionDescription.push(new StellarFunctionDescription('TRIM', 'Lazy to copy desc', ['input - input field']));
+        return Observable.create((observer) => {
+            observer.next(stellarFunctionDescription);
+            observer.complete();
+        });
+    }
+}
+
+class MockSensorEnrichmentConfigService {
+    public getAvailableEnrichments(): Observable<string[]> {
+        return Observable.create((observer) => {
+            observer.next(['geo', 'host', 'whois']);
+            observer.complete();
+        });
+    }
+}
+
+class MockKafkaService {
+
+}
+
+describe('Component: SensorFieldSchema', () => {
+    let component: SensorFieldSchemaComponent;
+    let sensorEnrichmentConfigService: SensorEnrichmentConfigService;
+    let sensorParserConfigService: SensorParserConfigService;
+    let fixture: ComponentFixture<SensorFieldSchemaComponent>;
+    let transformationValidationService: StellarService;
+
+    let squidSensorConfigJson = {
+        'parserClassName': 'org.apache.metron.parsers.GrokParser',
+        'sensorTopic': 'squid',
+        'parserConfig': {
+            'grokPath': 'target/patterns/squid',
+            'grokStatement': '%{NUMBER:timestamp} %{INT:elapsed} %{IPV4:ip_src_addr} %{WORD:action}/%{NUMBER:code} ' +
+                             '%{NUMBER:bytes} %{WORD:method} %{NOTSPACE:url} - %{WORD:UNWANTED}\\/%{IPV4:ip_dst_addr} ' +
+                             '%{WORD:UNWANTED}\\/%{WORD:UNWANTED}'
+        },
+        'fieldTransformations': [
+            {
+                'input': [],
+                'output': ['method'],
+                'transformation': 'STELLAR',
+                'config': {
+                    'method': 'TRIM(TO_LOWER(method))'
+                }
+            },
+            {
+                'input': ['code'],
+                'output': null,
+                'transformation': 'REMOVE',
+                'config': {
+                    'condition': 'exists(field2)'
+                }
+            },
+            {
+                'input': ['ip_src_addr'],
+                'output': null,
+                'transformation': 'REMOVE'
+            }
+        ]
+    };
+    let squidEnrichmentJson = {
+        'index': 'squid',
+        'batchSize': 1,
+        'enrichment': {
+            'fieldMap': {
+                'geo': ['ip_dst_addr', 'ip_src_addr'],
+                'host': ['ip_dst_addr'],
+                'whois': ['ip_src_addr']
+            },
+            'fieldToTypeMap': {},
+            'config': {}
+        },
+        'threatIntel': {
+            'fieldMap': {
+                'hbaseThreatIntel': ['ip_dst_addr']
+            },
+            'fieldToTypeMap': {
+                'ip_dst_addr': ['malicious_ip']
+            },
+            'config': {},
+            'triageConfig': {
+                'riskLevelRules': {},
+                'aggregator': 'MAX',
+                'aggregationConfig': {}
+            }
+        },
+        'configuration': {}
+    };
+    let sensorParserConfig = Object.assign(new SensorParserConfig(), squidSensorConfigJson);
+    let sensorEnrichmentConfig = Object.assign(new SensorEnrichmentConfig(), squidEnrichmentJson);
+
+    beforeEach(async(() => {
+      TestBed.configureTestingModule({
+        imports: [SensorFieldSchemaModule],
+        providers: [
+          MetronAlerts,
+          {provide: Http},
+          {provide: KafkaService, useClass: MockKafkaService},
+          {provide: SensorEnrichmentConfigService, useClass: MockSensorEnrichmentConfigService},
+          {provide: SensorParserConfigService, useClass: MockSensorParserConfigService},
+          {provide: StellarService, useClass: MockTransformationValidationService},
+
+        ]
+      }).compileComponents()
+          .then(() => {
+            fixture = TestBed.createComponent(SensorFieldSchemaComponent);
+            component = fixture.componentInstance;
+            sensorParserConfigService = fixture.debugElement.injector.get(SensorParserConfigService);
+            transformationValidationService = fixture.debugElement.injector.get(StellarService);
+            sensorEnrichmentConfigService = fixture.debugElement.injector.get(SensorEnrichmentConfigService);
+          });
+    }));
+
+    it('should create an instance', () => {
+      expect(component).toBeDefined();
+      fixture.destroy();
+    });
+
+
+
+    it('should read TransformFunctions, EnrichmentFunctions, ThreatIntelfunctions', () => {
+        component.ngOnInit();
+
+        expect(component.transformOptions.length).toEqual(3);
+        expect(Object.keys(component.transformFunctions).length).toEqual(3);
+        expect(component.enrichmentOptions.length).toEqual(3);
+        expect(component.threatIntelOptions.length).toEqual(1);
+
+        fixture.destroy();
+    });
+
+    it('should call getSampleData if showFieldSchema', () => {
+        spyOn(component.sampleData, 'getNextSample');
+
+        let changes: SimpleChanges = {
+            'showFieldSchema': new SimpleChange(false, true)
+        };
+        component.ngOnChanges(changes);
+        expect(component.sampleData.getNextSample['calls'].count()).toEqual(1);
+
+        changes = {
+            'showFieldSchema': new SimpleChange(true, false)
+        };
+        component.ngOnChanges(changes);
+        expect(component.sampleData.getNextSample['calls'].count()).toEqual(1);
+
+        fixture.destroy();
+    });
+
+    it('should return isSimple function', () => {
+        component.ngOnInit();
+
+        expect(component.isSimpleFunction(['TO_LOWER', 'TO_UPPER'])).toEqual(true);
+        expect(component.isSimpleFunction(['TO_LOWER', 'TO_UPPER', 'TEST'])).toEqual(false);
+
+        fixture.destroy();
+    });
+
+    it('should create FieldSchemaRows', () => {
+        component.ngOnInit();
+
+        component.sensorParserConfig = sensorParserConfig;
+        component.sensorEnrichmentConfig = sensorEnrichmentConfig;
+        component.onSampleDataChanged('DoctorStrange');
+        component.createFieldSchemaRows();
+
+        expect(component.fieldSchemaRows.length).toEqual(10);
+        expect(component.savedFieldSchemaRows.length).toEqual(10);
+
+        let methodFieldSchemaRow: FieldSchemaRow = component.fieldSchemaRows.filter(row => row.inputFieldName === 'method')[0];
+        expect(methodFieldSchemaRow).toBeDefined();
+        expect(methodFieldSchemaRow.transformConfigured.length).toEqual(2);
+        expect(methodFieldSchemaRow.enrichmentConfigured.length).toEqual(0);
+        expect(methodFieldSchemaRow.threatIntelConfigured.length).toEqual(0);
+
+        let ipSrcAddrFieldSchemaRow: FieldSchemaRow = component.fieldSchemaRows.filter(row => row.inputFieldName === 'ip_src_addr')[0];
+        expect(ipSrcAddrFieldSchemaRow).toBeDefined();
+        expect(ipSrcAddrFieldSchemaRow.transformConfigured.length).toEqual(0);
+        expect(ipSrcAddrFieldSchemaRow.enrichmentConfigured.length).toEqual(2);
+        expect(ipSrcAddrFieldSchemaRow.threatIntelConfigured.length).toEqual(0);
+
+        let ipDstAddrFieldSchemaRow: FieldSchemaRow = component.fieldSchemaRows.filter(row => row.inputFieldName === 'ip_dst_addr')[0];
+        expect(ipDstAddrFieldSchemaRow).toBeDefined();
+        expect(ipDstAddrFieldSchemaRow.transformConfigured.length).toEqual(0);
+        expect(ipDstAddrFieldSchemaRow.enrichmentConfigured.length).toEqual(2);
+        expect(ipDstAddrFieldSchemaRow.threatIntelConfigured.length).toEqual(1);
+
+        let codeSchemaRow: FieldSchemaRow = component.fieldSchemaRows.filter(row => row.inputFieldName === 'code')[0];
+        expect(codeSchemaRow).toBeDefined();
+        expect(codeSchemaRow.isRemoved).toEqual(true);
+        expect(codeSchemaRow.conditionalRemove).toEqual(true);
+        expect(codeSchemaRow.transformConfigured.length).toEqual(0);
+        expect(codeSchemaRow.enrichmentConfigured.length).toEqual(0);
+        expect(codeSchemaRow.threatIntelConfigured.length).toEqual(0);
+
+        fixture.destroy();
+    });
+
+    it('should  return getChanges', () => {
+        let fieldSchemaRow = new FieldSchemaRow('method');
+        fieldSchemaRow.transformConfigured = [];
+        fieldSchemaRow.enrichmentConfigured = [new AutocompleteOption('GEO'), new AutocompleteOption('WHOIS')];
+        fieldSchemaRow.threatIntelConfigured = [new AutocompleteOption('MALICIOUS-IP')];
+
+        expect(component.getChanges(fieldSchemaRow)).toEqual('Enrichments: GEO, WHOIS <br> Threat Intel: MALICIOUS-IP');
+
+        fieldSchemaRow.transformConfigured = [new AutocompleteOption('TO_STRING')];
+        fieldSchemaRow.enrichmentConfigured = [new AutocompleteOption('GEO')];
+        fieldSchemaRow.threatIntelConfigured = [new AutocompleteOption('MALICIOUS-IP'), new AutocompleteOption('MALICIOUS-IP')];
+
+        expect(component.getChanges(fieldSchemaRow)).toEqual('Transforms: TO_STRING(method) <br> Enrichments: GEO <br> Threat Intel: MALICIOUS-IP, MALICIOUS-IP');
+
+
+        fieldSchemaRow.transformConfigured = [new AutocompleteOption('TO_STRING'), new AutocompleteOption('TO_STRING')];
+        fieldSchemaRow.enrichmentConfigured = [];
+        fieldSchemaRow.threatIntelConfigured = [new AutocompleteOption('MALICIOUS-IP'), new AutocompleteOption('MALICIOUS-IP')];
+
+        expect(component.getChanges(fieldSchemaRow)).toEqual('Transforms: TO_STRING(TO_STRING(method)) <br> Threat Intel: MALICIOUS-IP, MALICIOUS-IP');
+
+        fieldSchemaRow.transformConfigured = [new AutocompleteOption('TO_STRING'), new AutocompleteOption('TO_STRING')];
+        fieldSchemaRow.enrichmentConfigured = [];
+        fieldSchemaRow.threatIntelConfigured = [];
+
+        expect(component.getChanges(fieldSchemaRow)).toEqual('Transforms: TO_STRING(TO_STRING(method)) <br> ');
+
+        fieldSchemaRow.transformConfigured = [new AutocompleteOption('TO_STRING'), new AutocompleteOption('TO_STRING')];
+        fieldSchemaRow.isRemoved = true;
+        expect(component.getChanges(fieldSchemaRow)).toEqual('Disabled');
+
+        fixture.destroy();
+    });
+
+    it('should call appropriate functions when onSampleDataChanged is called ', () => {
+        let returnSuccess = true;
+        spyOn(component, 'createFieldSchemaRows');
+        spyOn(component, 'onSampleDataNotAvailable');
+        spyOn(sensorParserConfigService, 'parseMessage').and.callFake(function(parseMessageRequest: ParseMessageRequest) {
+            expect(parseMessageRequest.sensorParserConfig.parserConfig['patternLabel']).toEqual(parseMessageRequest.sensorParserConfig.sensorTopic.toUpperCase());
+            expect(parseMessageRequest.sensorParserConfig.parserConfig['grokPath']).toEqual('./' + parseMessageRequest.sensorParserConfig.sensorTopic);
+            if (returnSuccess) {
+                return Observable.create(observer => {
+                    observer.next({'a': 'b', 'c': 'd'});
+                    observer.complete();
+                });
+            }
+            return Observable.throw('Error');
+        });
+
+        component.sensorParserConfig = sensorParserConfig;
+        component.sensorParserConfig.parserConfig['patternLabel'] = null;
+        component.onSampleDataChanged('DoctorStrange');
+        expect(component.parserResult).toEqual({'a': 'b', 'c': 'd'});
+        expect(component.createFieldSchemaRows).toHaveBeenCalled();
+        expect(component.onSampleDataNotAvailable).not.toHaveBeenCalled();
+
+        returnSuccess = false;
+        component.parserResult = {};
+        component.onSampleDataChanged('DoctorStrange');
+        expect(component.parserResult).toEqual({});
+        expect(component.onSampleDataNotAvailable).toHaveBeenCalled();
+        expect(component.onSampleDataNotAvailable['calls'].count()).toEqual(1);
+
+        fixture.destroy();
+    });
+
+    it('should onSampleDataChanged available and onSampleDataNotAvailable ', () => {
+        let returnSuccess = true;
+        spyOn(component, 'createFieldSchemaRows');
+
+        component.onSampleDataNotAvailable();
+        expect(component.createFieldSchemaRows['calls'].count()).toEqual(1);
+
+        fixture.destroy();
+    });
+
+    it('should call onSaveChange on onRemove/onEnable ', () => {
+        spyOn(component, 'onSave');
+
+        let fieldSchemaRow = new FieldSchemaRow('method');
+        fieldSchemaRow.outputFieldName = 'copy-of-method';
+        fieldSchemaRow.preview = 'TRIM(TO_LOWER(method))';
+        fieldSchemaRow.isRemoved = false;
+
+        component.savedFieldSchemaRows = [fieldSchemaRow];
+
+        let removeFieldSchemaRow = JSON.parse(JSON.stringify(fieldSchemaRow));
+        component.onRemove(removeFieldSchemaRow);
+        expect(removeFieldSchemaRow.isRemoved).toEqual(true);
+        expect(component.savedFieldSchemaRows[0].isRemoved).toEqual(true);
+        expect(component.onSave['calls'].count()).toEqual(1);
+
+        fieldSchemaRow.isRemoved = true;
+        let enableFieldSchemaRow = JSON.parse(JSON.stringify(fieldSchemaRow));
+        component.onEnable(enableFieldSchemaRow);
+        expect(fieldSchemaRow.isRemoved).toEqual(false);
+        expect(component.savedFieldSchemaRows[0].isRemoved).toEqual(false);
+        expect(component.onSave['calls'].count()).toEqual(2);
+
+        fixture.destroy();
+    });
+
+    it('should revert changes on cancel ', () => {
+        let fieldSchemaRow = new FieldSchemaRow('method');
+        fieldSchemaRow.showConfig = true;
+        fieldSchemaRow.outputFieldName = 'method';
+        fieldSchemaRow.preview = 'TRIM(TO_LOWER(method))';
+        fieldSchemaRow.isRemoved = false;
+        fieldSchemaRow.isSimple = true;
+        fieldSchemaRow.transformConfigured = [new AutocompleteOption('TO_LOWER'), new AutocompleteOption('TRIM')];
+
+        component.savedFieldSchemaRows.push(fieldSchemaRow);
+
+        component.onCancelChange(fieldSchemaRow);
+        expect(fieldSchemaRow.showConfig).toEqual(false);
+
+        component.hideFieldSchema.emit = jasmine.createSpy('emit');
+        component.onCancel();
+        expect(component.hideFieldSchema.emit).toHaveBeenCalled();
+
+        fixture.destroy();
+    });
+
+    it('should return formatted function on createTransformFunction call ', () => {
+        let fieldSchemaRow = new FieldSchemaRow('method');
+        fieldSchemaRow.transformConfigured = [new AutocompleteOption('TRIM'), new AutocompleteOption('TO_STRING')];
+
+        expect(component.createTransformFunction(fieldSchemaRow)).toEqual('TO_STRING(TRIM(method))');
+
+        fixture.destroy();
+    });
+
+    it('should set preview value for FieldSchemaRow ', () => {
+        let fieldSchemaRow = new FieldSchemaRow('method');
+        fieldSchemaRow.transformConfigured = [new AutocompleteOption('TRIM'), new AutocompleteOption('TO_STRING')];
+
+        component.onTransformsChange(fieldSchemaRow);
+        expect(fieldSchemaRow.preview).toEqual('TO_STRING(TRIM(method))');
+
+        fieldSchemaRow.transformConfigured = [new AutocompleteOption('TRIM')];
+        component.onTransformsChange(fieldSchemaRow);
+        expect(fieldSchemaRow.preview).toEqual('TRIM(method)');
+
+        fieldSchemaRow.transformConfigured = [];
+        component.onTransformsChange(fieldSchemaRow);
+        expect(fieldSchemaRow.preview).toEqual('');
+
+        fixture.destroy();
+    });
+
+    it('isConditionalRemoveTransform ', () => {
+        let fieldTransformationJson = {
+            'input': ['method'],
+            'transformation': 'REMOVE',
+            'config':
+            {
+                'condition': 'IS_DOMAIN(elapsed)'
+            }
+        };
+        let simpleFieldTransformationJson = {
+          'input': ['method'],
+          'transformation': 'REMOVE'
+        };
+        let fieldTransformation: FieldTransformer = Object.assign(new FieldTransformer(), fieldTransformationJson);
+        expect(component.isConditionalRemoveTransform(fieldTransformation)).toEqual(true);
+
+        let simpleFieldTransformation: FieldTransformer = Object.assign(new FieldTransformer(), simpleFieldTransformationJson);
+        expect(component.isConditionalRemoveTransform(simpleFieldTransformation)).toEqual(false);
+
+        fixture.destroy();
+    });
+
+    it('should save data ', () => {
+        let methodFieldSchemaRow = new FieldSchemaRow('method');
+        methodFieldSchemaRow.outputFieldName = 'method';
+        methodFieldSchemaRow.preview = 'TRIM(TO_LOWER(method))';
+        methodFieldSchemaRow.isRemoved = false;
+        methodFieldSchemaRow.isSimple = true;
+        methodFieldSchemaRow.transformConfigured = [new AutocompleteOption('TO_LOWER'), new AutocompleteOption('TRIM')];
+
+        let elapsedFieldSchemaRow = new FieldSchemaRow('elapsed');
+        elapsedFieldSchemaRow.outputFieldName = 'elapsed';
+        elapsedFieldSchemaRow.preview = 'IS_DOMAIN(elapsed)';
+        elapsedFieldSchemaRow.isRemoved = true;
+        elapsedFieldSchemaRow.isSimple = true;
+        elapsedFieldSchemaRow.transformConfigured = [new AutocompleteOption('IS_DOMAIN')];
+        elapsedFieldSchemaRow.enrichmentConfigured = [new AutocompleteOption('host')];
+
+        let ipDstAddrFieldSchemaRow = new FieldSchemaRow('ip_dst_addr');
+        ipDstAddrFieldSchemaRow.outputFieldName = 'ip_dst_addr';
+        ipDstAddrFieldSchemaRow.preview = 'IS_DOMAIN(elapsed)';
+        ipDstAddrFieldSchemaRow.isRemoved = false;
+        ipDstAddrFieldSchemaRow.isSimple = false;
+        ipDstAddrFieldSchemaRow.threatIntelConfigured = [new AutocompleteOption('malicious_ip')];
+        ipDstAddrFieldSchemaRow.enrichmentConfigured = [new AutocompleteOption('host')];
+
+        let codeFieldSchemaRow = new FieldSchemaRow('code');
+        codeFieldSchemaRow.outputFieldName = 'code';
+        codeFieldSchemaRow.isRemoved = true;
+        codeFieldSchemaRow.conditionalRemove = true;
+
+        component.savedFieldSchemaRows = [methodFieldSchemaRow, elapsedFieldSchemaRow, ipDstAddrFieldSchemaRow, codeFieldSchemaRow];
+
+        component.sensorParserConfig = new SensorParserConfig();
+        component.sensorParserConfig.parserClassName = 'org.apache.metron.parsers.GrokParser';
+        component.sensorParserConfig.sensorTopic = 'squid';
+
+      component.sensorParserConfig.fieldTransformations = [new FieldTransformer()];
+      component.sensorParserConfig.fieldTransformations[0].transformation = 'REMOVE';
+      component.sensorParserConfig.fieldTransformations[0].input = ['code'];
+      component.sensorParserConfig.fieldTransformations[0].config = {'condition': 'exists(method)'};
+
+        component.sensorEnrichmentConfig = new SensorEnrichmentConfig();
+        component.sensorEnrichmentConfig.enrichment = new  EnrichmentConfig();
+        component.sensorEnrichmentConfig.threatIntel = new ThreatIntelConfig();
+        component.sensorEnrichmentConfig.configuration = {};
+
+        component.onSave();
+
+        let fieldTransformationJson = {
+            'output': ['method', 'elapsed'],
+            'transformation': 'STELLAR',
+            'config':
+            {
+                    'method': 'TRIM(TO_LOWER(method))',
+                'elapsed': 'IS_DOMAIN(elapsed)'
+            }
+        };
+
+        let fieldTransformationRemoveJson = {
+            'input': ['elapsed'],
+            'transformation': 'REMOVE',
+        };
+
+        let conditionalFieldTransformationRemoveJson = {
+          'input': ['code'],
+          'transformation': 'REMOVE',
+          'config': {
+            'condition': 'exists(method)'
+          }
+        };
+
+        let fieldTransformation = Object.assign(new FieldTransformer(), fieldTransformationJson);
+        let fieldTransformationRemove = Object.assign(new FieldTransformer(), fieldTransformationRemoveJson);
+      let conditionalFieldTransformationRemove = Object.assign(new FieldTransformer(), conditionalFieldTransformationRemoveJson);
+
+        expect(component.sensorParserConfig.fieldTransformations.length).toEqual(3);
+        let expectedStellar = component.sensorParserConfig.fieldTransformations.filter(transform => transform.transformation === 'STELLAR')[0];
+        let expectedRemove = component.sensorParserConfig.fieldTransformations.filter(transform => transform.transformation === 'REMOVE' && !transform.config)[0];
+        let expectedConditionalRemove = component.sensorParserConfig.fieldTransformations.filter(transform => transform.transformation === 'REMOVE' && transform.config)[0];
+        expect(expectedStellar).toEqual(fieldTransformation);
+        expect(expectedRemove).toEqual(fieldTransformationRemove);
+      expect(expectedConditionalRemove).toEqual(conditionalFieldTransformationRemove);
+
+        fixture.destroy();
+    });
+});

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/sensors/sensor-field-schema/sensor-field-schema.component.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/sensors/sensor-field-schema/sensor-field-schema.component.ts b/metron-interface/metron-config/src/app/sensors/sensor-field-schema/sensor-field-schema.component.ts
new file mode 100644
index 0000000..8d9dd11
--- /dev/null
+++ b/metron-interface/metron-config/src/app/sensors/sensor-field-schema/sensor-field-schema.component.ts
@@ -0,0 +1,435 @@
+/**
+ * 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:max-line-length */
+import { Component, OnInit, Input, OnChanges, ViewChild, SimpleChanges, Output, EventEmitter } from '@angular/core';
+import {SensorParserConfig} from '../../model/sensor-parser-config';
+import {ParseMessageRequest} from '../../model/parse-message-request';
+import {SensorParserConfigService} from '../../service/sensor-parser-config.service';
+import {StellarService} from '../../service/stellar.service';
+import {AutocompleteOption} from '../../model/autocomplete-option';
+import {StellarFunctionDescription} from '../../model/stellar-function-description';
+import {SensorEnrichmentConfig, EnrichmentConfig, ThreatIntelConfig} from '../../model/sensor-enrichment-config';
+import {FieldTransformer} from '../../model/field-transformer';
+import {SampleDataComponent} from '../../shared/sample-data/sample-data.component';
+import {MetronAlerts} from '../../shared/metron-alerts';
+import {SensorEnrichmentConfigService} from '../../service/sensor-enrichment-config.service';
+
+export class FieldSchemaRow {
+  inputFieldName: string;
+  outputFieldName: string;
+  preview: string;
+  showConfig: boolean;
+  isRemoved: boolean;
+  isSimple: boolean;
+  isNew: boolean;
+  isParserGenerated: boolean;
+  conditionalRemove: boolean;
+  transformConfigured: AutocompleteOption[] = [];
+  enrichmentConfigured: AutocompleteOption[] = [];
+  threatIntelConfigured: AutocompleteOption[] = [];
+
+  constructor(fieldName: string) {
+    this.inputFieldName = fieldName;
+    this.outputFieldName = fieldName;
+    this.conditionalRemove = false;
+    this.isParserGenerated = false;
+    this.showConfig = false;
+    this.isSimple = true;
+    this.isRemoved = false;
+    this.preview = '';
+  }
+}
+
+@Component({
+  selector: 'metron-config-sensor-field-schema',
+  templateUrl: './sensor-field-schema.component.html',
+  styleUrls: ['./sensor-field-schema.component.scss']
+})
+export class SensorFieldSchemaComponent implements OnInit, OnChanges {
+
+  @Input() sensorParserConfig: SensorParserConfig;
+  @Input() sensorEnrichmentConfig: SensorEnrichmentConfig;
+  @Input() showFieldSchema: boolean;
+  @Input() grokStatement: string;
+
+  parserResult: any = {};
+  fieldSchemaRows: FieldSchemaRow[] = [];
+  savedFieldSchemaRows: FieldSchemaRow[] = [];
+
+  transformOptions: AutocompleteOption[] = [];
+  enrichmentOptions: AutocompleteOption[] = [];
+  threatIntelOptions: AutocompleteOption[] = [];
+
+  transformFunctions: StellarFunctionDescription[];
+
+  @ViewChild(SampleDataComponent) sampleData: SampleDataComponent;
+  @Output() hideFieldSchema: EventEmitter<boolean> = new EventEmitter<boolean>();
+  @Output() onFieldSchemaChanged: EventEmitter<boolean> = new EventEmitter<boolean>();
+
+  sampleThreatIntels: string[] = ['malicious_ip'];
+
+  constructor(private sensorParserConfigService: SensorParserConfigService,
+              private transformationValidationService: StellarService,
+              private sensorEnrichmentConfigService: SensorEnrichmentConfigService,
+              private metronAlerts: MetronAlerts) { }
+
+  ngOnChanges(changes: SimpleChanges) {
+    if (changes['showFieldSchema'] && changes['showFieldSchema'].currentValue) {
+      this.sampleData.getNextSample();
+    }
+  }
+
+  ngOnInit() {
+    this.getTransformFunctions();
+    this.getEnrichmentFunctions();
+    this.getThreatIntelfunctions();
+  }
+
+  getTransformFunctions() {
+    this.transformOptions = [];
+
+    this.transformationValidationService.listSimpleFunctions().subscribe((result: StellarFunctionDescription[]) => {
+      this.transformFunctions = result;
+      for (let fun of result) {
+        this.transformOptions.push(new AutocompleteOption(fun.name, fun.name, fun.description));
+      }
+    });
+  }
+
+  getEnrichmentFunctions() {
+    this.enrichmentOptions = [];
+
+    this.sensorEnrichmentConfigService.getAvailableEnrichments().subscribe((result: string[]) => {
+      for (let fun of result) {
+        this.enrichmentOptions.push(new AutocompleteOption(fun));
+      }
+    });
+  }
+
+  getThreatIntelfunctions() {
+    this.threatIntelOptions = [];
+    for (let threatName of this.sampleThreatIntels) {
+      this.threatIntelOptions.push(new AutocompleteOption(threatName));
+    }
+
+  }
+
+  isSimpleFunction(configuredFunctions: string[]) {
+    for (let configuredFunction of configuredFunctions) {
+      if (this.transformFunctions.filter(stellarFunctionDescription => stellarFunctionDescription.name === configuredFunction).length === 0) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  isConditionalRemoveTransform(fieldTransformer: FieldTransformer): boolean {
+    if (fieldTransformer && fieldTransformer.transformation === 'REMOVE' &&
+        fieldTransformer.config && fieldTransformer.config['condition']) {
+      return true;
+    }
+
+    return false;
+  }
+
+  createFieldSchemaRows() {
+    this.fieldSchemaRows = [];
+    this.savedFieldSchemaRows = [];
+    let fieldSchemaRowsCreated = {};
+
+    // Update rows with Stellar transformations
+    let stellarTransformations = this.sensorParserConfig.fieldTransformations.filter(fieldTransformer => fieldTransformer.transformation === 'STELLAR');
+    for (let fieldTransformer of stellarTransformations) {
+      if (fieldTransformer.config) {
+        for (let outputFieldName of Object.keys(fieldTransformer.config)) {
+          let stellarFunctionStatement = fieldTransformer.config[outputFieldName];
+          let configuredFunctions = stellarFunctionStatement.split('(');
+          let inputFieldName = configuredFunctions.splice(-1, 1)[0].replace(new RegExp('\\)', 'g'), '');
+          configuredFunctions.reverse();
+          if (!fieldSchemaRowsCreated[inputFieldName]) {
+            fieldSchemaRowsCreated[inputFieldName] = new FieldSchemaRow(inputFieldName);
+          }
+          fieldSchemaRowsCreated[inputFieldName].outputFieldName = outputFieldName;
+          fieldSchemaRowsCreated[inputFieldName].preview = stellarFunctionStatement;
+          fieldSchemaRowsCreated[inputFieldName].isSimple = this.isSimpleFunction(configuredFunctions);
+          if (fieldSchemaRowsCreated[inputFieldName].isSimple) {
+            for (let configuredFunction of configuredFunctions) {
+              fieldSchemaRowsCreated[inputFieldName].transformConfigured.push(new AutocompleteOption(configuredFunction));
+            }
+          }
+        }
+      }
+    }
+
+    // Update rows with Remove Transformations
+    let removeTransformations = this.sensorParserConfig.fieldTransformations.filter(fieldTransformer => fieldTransformer.transformation === 'REMOVE');
+    for (let fieldTransformer of removeTransformations) {
+      for (let inputFieldName of fieldTransformer.input) {
+        if (!fieldSchemaRowsCreated[inputFieldName]) {
+          fieldSchemaRowsCreated[inputFieldName] = new FieldSchemaRow(inputFieldName);
+        }
+        fieldSchemaRowsCreated[inputFieldName].isRemoved = true;
+        if (fieldTransformer.config && fieldTransformer.config['condition']) {
+          fieldSchemaRowsCreated[inputFieldName].conditionalRemove = true;
+        }
+
+      }
+    }
+
+    // Update rows with enrichments
+    if (this.sensorEnrichmentConfig.enrichment.fieldMap) {
+      for (let enrichment in this.sensorEnrichmentConfig.enrichment.fieldMap) {
+        if (enrichment !== 'hbaseEnrichment' && enrichment !== 'stellar') {
+          let fieldNames = this.sensorEnrichmentConfig.enrichment.fieldMap[enrichment];
+          for (let fieldName of fieldNames) {
+            if (!fieldSchemaRowsCreated[fieldName]) {
+              fieldSchemaRowsCreated[fieldName] = new FieldSchemaRow(fieldName);
+            }
+            fieldSchemaRowsCreated[fieldName].enrichmentConfigured.push(new AutocompleteOption(enrichment));
+          }
+        }
+      }
+    }
+
+    // Update rows with HBase enrichments
+    if (this.sensorEnrichmentConfig.enrichment.fieldToTypeMap) {
+      for (let fieldName of Object.keys(this.sensorEnrichmentConfig.enrichment.fieldToTypeMap)) {
+        let enrichments = this.sensorEnrichmentConfig.enrichment.fieldToTypeMap[fieldName];
+        if (!fieldSchemaRowsCreated[fieldName]) {
+          fieldSchemaRowsCreated[fieldName] = new FieldSchemaRow(fieldName);
+        }
+        for (let enrichment of enrichments) {
+          fieldSchemaRowsCreated[fieldName].enrichmentConfigured.push(new AutocompleteOption(enrichment));
+        }
+      }
+    }
+
+    // Update rows with threatIntels
+    if (this.sensorEnrichmentConfig.threatIntel.fieldToTypeMap) {
+      for (let fieldName of  Object.keys(this.sensorEnrichmentConfig.threatIntel.fieldToTypeMap)) {
+        let threatIntels = this.sensorEnrichmentConfig.threatIntel.fieldToTypeMap[fieldName];
+
+        if (!fieldSchemaRowsCreated[fieldName]) {
+          fieldSchemaRowsCreated[fieldName] = new FieldSchemaRow(fieldName);
+        }
+
+        for (let threatIntel of threatIntels) {
+          fieldSchemaRowsCreated[fieldName].threatIntelConfigured.push(new AutocompleteOption(threatIntel));
+        }
+      }
+    }
+
+    this.fieldSchemaRows = Object.keys(fieldSchemaRowsCreated).map(key => fieldSchemaRowsCreated[key]);
+
+    // Adds rows from parseResult with no transformations/enrichments/threatIntels
+    let fieldSchemaRowsCreatedKeys = Object.keys(fieldSchemaRowsCreated);
+    for (let fieldName of Object.keys(this.parserResult).filter(fieldName => fieldSchemaRowsCreatedKeys.indexOf(fieldName) === -1)) {
+        let field = new FieldSchemaRow(fieldName);
+        field.isParserGenerated = true;
+        this.fieldSchemaRows.push(field);
+    }
+
+    // save the initial fieldSchemaRows
+    for (let fieldSchemaRow of this.fieldSchemaRows) {
+      this.savedFieldSchemaRows.push(JSON.parse(JSON.stringify(fieldSchemaRow)));
+    }
+  }
+
+  getChanges(fieldSchemaRow: FieldSchemaRow): string {
+
+    if (fieldSchemaRow.isRemoved) {
+      return 'Disabled';
+    }
+
+    let transformFunction = fieldSchemaRow.transformConfigured.length > 0 ? this.createTransformFunction(fieldSchemaRow) : '';
+    let enrichments = fieldSchemaRow.enrichmentConfigured.map(autocomplete => autocomplete.name).join(', ');
+    let threatIntel = fieldSchemaRow.threatIntelConfigured.map(autocomplete => autocomplete.name).join(', ');
+
+    transformFunction = transformFunction.length > 30 ? (transformFunction.substring(0, 25) + '...') : transformFunction;
+
+    let displayString = transformFunction.length > 0 ? ('Transforms: ' + transformFunction) : '';
+    displayString += (transformFunction.length > 0 ? ' <br> ' : '') + (enrichments.length > 0 ? ('Enrichments: ' + enrichments) : '');
+    displayString += (enrichments.length > 0 ? ' <br> ' : '') + (threatIntel.length > 0 ? ('Threat Intel: ' + threatIntel) : '');
+
+    return displayString;
+  }
+
+  onSampleDataChanged(sampleData: string) {
+    let sensorTopicUpperCase = this.sensorParserConfig.sensorTopic.toUpperCase();
+    let parseMessageRequest = new ParseMessageRequest();
+    parseMessageRequest.sensorParserConfig = JSON.parse(JSON.stringify(this.sensorParserConfig));
+    parseMessageRequest.grokStatement = this.grokStatement;
+    parseMessageRequest.sampleData = sampleData;
+
+    if (parseMessageRequest.sensorParserConfig.parserConfig['patternLabel'] == null) {
+      parseMessageRequest.sensorParserConfig.parserConfig['patternLabel'] = sensorTopicUpperCase;
+    }
+    parseMessageRequest.sensorParserConfig.parserConfig['grokPath'] = './' + parseMessageRequest.sensorParserConfig.sensorTopic;
+
+    this.sensorParserConfigService.parseMessage(parseMessageRequest).subscribe(
+        parserResult => {
+          this.parserResult = parserResult;
+          this.createFieldSchemaRows();
+        },
+        error => {
+          this.onSampleDataNotAvailable();
+        });
+  }
+
+  onSampleDataNotAvailable() {
+    this.createFieldSchemaRows();
+  }
+
+  onDelete(fieldSchemaRow: FieldSchemaRow) {
+    this.fieldSchemaRows.splice(this.fieldSchemaRows.indexOf(fieldSchemaRow), 1);
+    this.savedFieldSchemaRows.splice(this.fieldSchemaRows.indexOf(fieldSchemaRow), 1);
+  }
+
+  onRemove(fieldSchemaRow: FieldSchemaRow) {
+    fieldSchemaRow.isRemoved = true;
+    this.onSaveChange(fieldSchemaRow);
+  }
+
+  onEnable(fieldSchemaRow: FieldSchemaRow) {
+    if (fieldSchemaRow.conditionalRemove) {
+      this.metronAlerts.showErrorMessage('The "' + fieldSchemaRow.outputFieldName + '" field cannot be enabled because the REMOVE transformation has a condition.  Please remove the condition in the RAW JSON editor.');
+      return;
+    }
+    fieldSchemaRow.isRemoved = false;
+    this.onSaveChange(fieldSchemaRow);
+  }
+
+  onSaveChange(savedFieldSchemaRow: FieldSchemaRow) {
+    savedFieldSchemaRow.showConfig = false;
+    savedFieldSchemaRow.isNew = false;
+    let initialSchemaRow = this.savedFieldSchemaRows.filter(fieldSchemaRow => fieldSchemaRow.inputFieldName === savedFieldSchemaRow.inputFieldName)[0];
+    Object.assign(initialSchemaRow, JSON.parse(JSON.stringify(savedFieldSchemaRow)));
+
+    this.onSave();
+  }
+
+  onCancelChange(cancelledFieldSchemaRow: FieldSchemaRow) {
+    cancelledFieldSchemaRow.showConfig = false;
+    let initialSchemaRow = this.savedFieldSchemaRows.filter(fieldSchemaRow => fieldSchemaRow.inputFieldName === cancelledFieldSchemaRow.inputFieldName)[0];
+    Object.assign(cancelledFieldSchemaRow, JSON.parse(JSON.stringify(initialSchemaRow)));
+  }
+
+  onCancel(): void {
+    this.hideFieldSchema.emit(true);
+  }
+
+  createTransformFunction(fieldSchemaRow: FieldSchemaRow): string {
+    let func = fieldSchemaRow.inputFieldName;
+
+    for (let config of fieldSchemaRow.transformConfigured) {
+      func = config.name + '(' + func + ')';
+    }
+
+    return func;
+  }
+
+  onTransformsChange(fieldSchemaRow: FieldSchemaRow): void {
+    fieldSchemaRow.preview = fieldSchemaRow.transformConfigured.length === 0 ? '' : this.createTransformFunction(fieldSchemaRow);
+  }
+
+  addNewRule() {
+    let fieldSchemaRow = new FieldSchemaRow('new');
+    fieldSchemaRow.isNew = true;
+    fieldSchemaRow.showConfig = true;
+    fieldSchemaRow.inputFieldName = '';
+    this.fieldSchemaRows.push(fieldSchemaRow);
+  }
+
+  onSave() {
+    let removeTransformations: string[] = [];
+
+    // Remove all STELLAR functions and retain only the REMOVE objects
+    this.sensorParserConfig.fieldTransformations = this.sensorParserConfig.fieldTransformations.filter(fieldTransformer => {
+      if (this.isConditionalRemoveTransform(fieldTransformer)) {
+        return true;
+      }
+      return false;
+    });
+
+    let transformConfigObject = new FieldTransformer();
+    transformConfigObject.output = [];
+    transformConfigObject.config = {};
+    transformConfigObject.transformation = 'STELLAR';
+
+    let enrichmentConfigObject = new EnrichmentConfig();
+    enrichmentConfigObject.config = {};
+    let threatIntelConfigObject = new ThreatIntelConfig();
+    threatIntelConfigObject.triageConfig = this.sensorEnrichmentConfig.threatIntel.triageConfig;
+
+
+    for (let fieldSchemaRow of this.savedFieldSchemaRows) {
+      if (fieldSchemaRow.transformConfigured.length > 0) {
+        transformConfigObject.output.push(fieldSchemaRow.outputFieldName);
+        transformConfigObject.config[fieldSchemaRow.outputFieldName] = this.createTransformFunction(fieldSchemaRow);
+      }
+      if (fieldSchemaRow.isRemoved && !fieldSchemaRow.conditionalRemove) {
+        removeTransformations.push(fieldSchemaRow.inputFieldName);
+      }
+      if (fieldSchemaRow.enrichmentConfigured.length > 0) {
+        for (let option of fieldSchemaRow.enrichmentConfigured) {
+          if (option.name === 'geo' || option.name === 'host') {
+            if (!enrichmentConfigObject.fieldMap[option.name]) {
+              enrichmentConfigObject.fieldMap[option.name] = [];
+            }
+            enrichmentConfigObject.fieldMap[option.name].push(fieldSchemaRow.inputFieldName);
+          } else {
+            if (!enrichmentConfigObject.fieldMap['hbaseEnrichment']) {
+              enrichmentConfigObject.fieldMap['hbaseEnrichment'] = [];
+            }
+            enrichmentConfigObject.fieldMap['hbaseEnrichment'].push(fieldSchemaRow.inputFieldName);
+            if (!enrichmentConfigObject.fieldToTypeMap[fieldSchemaRow.inputFieldName]) {
+              enrichmentConfigObject.fieldToTypeMap[fieldSchemaRow.inputFieldName] = [];
+            }
+            enrichmentConfigObject.fieldToTypeMap[fieldSchemaRow.inputFieldName].push(option.name);
+          }
+        }
+      }
+      if (fieldSchemaRow.threatIntelConfigured.length > 0) {
+        for (let option of fieldSchemaRow.threatIntelConfigured) {
+          if (!threatIntelConfigObject.fieldMap['hbaseThreatIntel']) {
+            threatIntelConfigObject.fieldMap['hbaseThreatIntel'] = [];
+          }
+          threatIntelConfigObject.fieldMap['hbaseThreatIntel'].push(fieldSchemaRow.inputFieldName);
+          if (!threatIntelConfigObject.fieldToTypeMap[fieldSchemaRow.inputFieldName]) {
+            threatIntelConfigObject.fieldToTypeMap[fieldSchemaRow.inputFieldName] = [];
+          }
+          threatIntelConfigObject.fieldToTypeMap[fieldSchemaRow.inputFieldName].push(option.name);
+        }
+      }
+    }
+
+    if (Object.keys(transformConfigObject.config).length > 0) {
+      this.sensorParserConfig.fieldTransformations.push(transformConfigObject);
+    }
+
+    if (removeTransformations.length > 0) {
+      let removeConfigObject = new FieldTransformer();
+      removeConfigObject.transformation = 'REMOVE';
+      removeConfigObject.input = removeTransformations;
+      this.sensorParserConfig.fieldTransformations.push(removeConfigObject);
+    }
+
+    this.sensorEnrichmentConfig.enrichment = enrichmentConfigObject;
+    this.sensorEnrichmentConfig.threatIntel = threatIntelConfigObject;
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/sensors/sensor-field-schema/sensor-field-schema.module.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/sensors/sensor-field-schema/sensor-field-schema.module.ts b/metron-interface/metron-config/src/app/sensors/sensor-field-schema/sensor-field-schema.module.ts
new file mode 100644
index 0000000..afa3d55
--- /dev/null
+++ b/metron-interface/metron-config/src/app/sensors/sensor-field-schema/sensor-field-schema.module.ts
@@ -0,0 +1,30 @@
+/**
+ * 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 {MultipleInputModule} from '../../shared/multiple-input/multiple-input.module';
+import {SampleDataModule} from '../../shared/sample-data/sample-data.module';
+import {SensorFieldSchemaComponent} from './sensor-field-schema.component';
+
+@NgModule ({
+    imports: [ SharedModule, MultipleInputModule, SampleDataModule ],
+    declarations: [ SensorFieldSchemaComponent ],
+    exports: [ SensorFieldSchemaComponent ]
+})
+
+export class SensorFieldSchemaModule { }

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/sensors/sensor-grok/index.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/sensors/sensor-grok/index.ts b/metron-interface/metron-config/src/app/sensors/sensor-grok/index.ts
new file mode 100644
index 0000000..03c0507
--- /dev/null
+++ b/metron-interface/metron-config/src/app/sensors/sensor-grok/index.ts
@@ -0,0 +1,18 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+export * from './sensor-grok.component';

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/sensors/sensor-grok/sensor-grok.component.html
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/sensors/sensor-grok/sensor-grok.component.html b/metron-interface/metron-config/src/app/sensors/sensor-grok/sensor-grok.component.html
new file mode 100644
index 0000000..c9bdc06
--- /dev/null
+++ b/metron-interface/metron-config/src/app/sensors/sensor-grok/sensor-grok.component.html
@@ -0,0 +1,46 @@
+<!--
+  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.
+  -->
+<div class="metron-slider-pane-edit fill load-left-to-right dialog2x">
+
+    <div class="form-title">Grok Validator</div>
+    <i class="fa fa-times pull-right close-button" aria-hidden="true" (click)="onCancelGrok()"></i>
+
+    <form role="form" class="grok-form">
+        <metron-config-sample-data [topic]="sensorParserConfig.sensorTopic"
+                                   (onSampleDataChanged)="onSampleDataChanged($event)"></metron-config-sample-data>
+        <label attr.for="patternLabel">PATTERN LABEL</label>
+        <select class="form-control pattern-label-dropdown" [ngModelOptions]="{standalone: true}" [(ngModel)]="newPatternLabel">
+          <option *ngFor="let patternLabel of availablePatternLabels" [value]="patternLabel"> {{ patternLabel }} </option>
+        </select>
+        <label attr.for="grokStatement">STATEMENT</label>
+        <metron-config-ace-editor [(ngModel)]="newGrokStatement" [ngModelOptions]="{standalone: true}" [type]="'GROK'" [options]="grokFunctionList" [placeHolder]="'Enter Grok statement'" (ngModelChange)="getAvailablePatternLabels()"> </metron-config-ace-editor>
+
+        <div class="buttons-bar">
+            <button type="submit" class="btn form-enable-disable-button" [disabled]="isTestDisabled()" (click)="onTestGrokStatement()">TEST</button>
+            <button type="submit" class="btn form-enable-disable-button" [disabled]="isSaveDisabled()" (click)="onSaveGrok()">SAVE</button>
+        </div>
+
+        <label> PREVIEW </label>
+        <table class="table form-table" #table>
+            <tbody>
+            <tr *ngFor="let key of parsedMessageKeys">
+                <td>{{ key }}</td>
+                <td>{{ parsedMessage[key] }}</td>
+            </tr>
+            </tbody>
+        </table>
+    </form>
+</div>

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/sensors/sensor-grok/sensor-grok.component.scss
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/sensors/sensor-grok/sensor-grok.component.scss b/metron-interface/metron-config/src/app/sensors/sensor-grok/sensor-grok.component.scss
new file mode 100644
index 0000000..924d6a4
--- /dev/null
+++ b/metron-interface/metron-config/src/app/sensors/sensor-grok/sensor-grok.component.scss
@@ -0,0 +1,67 @@
+/**
+ * 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.
+ */
+.form-title
+{
+  padding-left: 25px;
+}
+
+.grok-form
+{
+  padding-left: 25px;
+  padding-right: 20px;
+}
+
+.close-button
+{
+  padding-right: 20px;
+}
+
+
+.form-table
+{
+  display: inline-block;
+  min-height: 100px;
+  background: #033339;
+  margin-bottom: 100px;
+}
+.table, table
+{
+  margin-top: 0px;
+  tr td
+  {
+    border-bottom: 1px solid #3F4748;
+  }
+  tr:last-child td
+  {
+    border-bottom: none;
+  }
+  td:last-child {
+    width: 100%;
+  }
+
+}
+.buttons-bar
+{
+  margin-top: 10px;
+  margin-bottom: 15px;
+}
+
+.pattern-label-dropdown
+{
+  width: 40%;
+}

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/sensors/sensor-grok/sensor-grok.component.spec.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/sensors/sensor-grok/sensor-grok.component.spec.ts b/metron-interface/metron-config/src/app/sensors/sensor-grok/sensor-grok.component.spec.ts
new file mode 100644
index 0000000..eac839e
--- /dev/null
+++ b/metron-interface/metron-config/src/app/sensors/sensor-grok/sensor-grok.component.spec.ts
@@ -0,0 +1,234 @@
+/**
+ * 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 { TestBed, async, ComponentFixture } from '@angular/core/testing';
+import {SimpleChange} from '@angular/core';
+import {Http} from '@angular/http';
+import {SensorParserConfigService} from '../../service/sensor-parser-config.service';
+import {MetronAlerts} from '../../shared/metron-alerts';
+import {KafkaService} from '../../service/kafka.service';
+import {Observable} from 'rxjs/Observable';
+import {ParseMessageRequest} from '../../model/parse-message-request';
+import {SensorGrokComponent} from './sensor-grok.component';
+import {GrokValidationService} from '../../service/grok-validation.service';
+import {SensorGrokModule} from './sensor-grok.module';
+import {SensorParserConfig} from '../../model/sensor-parser-config';
+import '../../rxjs-operators';
+
+class MockSensorParserConfigService {
+
+  private parsedMessage: string;
+
+  public parseMessage(parseMessageRequest: ParseMessageRequest): Observable<{}> {
+    if (this.parsedMessage === 'ERROR') {
+      return Observable.throw({'_body': JSON.stringify({'abc': 'def'}) });
+    }
+
+    return Observable.create(observer => {
+      observer.next(this.parsedMessage);
+      observer.complete();
+    });
+  }
+
+  public setParsedMessage(parsedMessage: any) {
+    this.parsedMessage = parsedMessage;
+  }
+}
+
+class MockGrokValidationService {
+  public list(): Observable<string[]> {
+    return Observable.create(observer => {
+      observer.next({
+        'BASE10NUM': '(?<![0-9.+-])(?>[+-]?(?:(?:[0-9]+(?:\\.[0-9]+)?)|(?:\\.[0-9]+)))',
+        'BASE16FLOAT': '\\b(?<![0-9A-Fa-f.])(?:[+-]?(?:0x)?(?:(?:[0-9A-Fa-f]+(?:\\.[0-9A-Fa-f]*)?)|(?:\\.[0-9A-Fa-f]+)))\\b',
+        'BASE16NUM': '(?<![0-9A-Fa-f])(?:[+-]?(?:0x)?(?:[0-9A-Fa-f]+))',
+        'CISCOMAC': '(?:(?:[A-Fa-f0-9]{4}\\.){2}[A-Fa-f0-9]{4})',
+        'COMMONMAC': '(?:(?:[A-Fa-f0-9]{2}:){5}[A-Fa-f0-9]{2})',
+        'DATA': '.*?'
+      });
+      observer.complete();
+    });
+  }
+}
+
+class MockKafkaService {
+
+}
+
+describe('Component: SensorGrok', () => {
+  let component: SensorGrokComponent;
+  let grokValidationService: GrokValidationService;
+  let fixture: ComponentFixture<SensorGrokComponent>;
+  let sensorParserConfigService: MockSensorParserConfigService;
+
+  beforeEach(async(() => {
+    TestBed.configureTestingModule({
+      imports: [SensorGrokModule],
+      providers: [
+        MetronAlerts,
+        {provide: Http},
+        {provide: KafkaService, useClass: MockKafkaService},
+        {provide: SensorParserConfigService, useClass: MockSensorParserConfigService},
+        {provide: GrokValidationService, useClass: MockGrokValidationService},
+
+      ]
+    }).compileComponents()
+        .then(() => {
+          fixture = TestBed.createComponent(SensorGrokComponent);
+          component = fixture.componentInstance;
+          sensorParserConfigService = fixture.debugElement.injector.get(SensorParserConfigService);
+          grokValidationService = fixture.debugElement.injector.get(GrokValidationService);
+        });
+  }));
+
+  it('should create an instance', () => {
+    expect(component).toBeDefined();
+    fixture.destroy();
+  });
+
+  it('should handle ngOnInit', async(() => {
+    component.ngOnInit();
+
+    expect(Object.keys(component.grokFunctionList).length).toEqual(6);
+
+    fixture.destroy();
+  }));
+
+  it('should handle ngOnChanges', async(() => {
+    spyOn(component.sampleData, 'getNextSample');
+
+    let changes = {
+      'showGrok': new SimpleChange(true, false)
+    };
+    component.ngOnChanges(changes);
+    expect(component.sampleData.getNextSample['calls'].count()).toEqual(0);
+
+    changes = {
+      'showGrok': new SimpleChange(false, true)
+    };
+
+    component.grokStatement = 'STATEMENT_1 grok statement 1\nSTATEMENT_2 grok statement 2\n';
+    component.patternLabel = 'STATEMENT_2';
+    component.ngOnChanges(changes);
+    expect(component.newGrokStatement).toEqual('STATEMENT_1 grok statement 1\nSTATEMENT_2 grok statement 2\n');
+    expect(component.newPatternLabel).toEqual('STATEMENT_2');
+    expect(component.availablePatternLabels).toEqual(['STATEMENT_1', 'STATEMENT_2']);
+
+    component.grokStatement = '';
+    component.patternLabel = 'PATTERN_LABEL';
+    component.ngOnChanges(changes);
+    expect(component.newGrokStatement).toEqual('PATTERN_LABEL ');
+    expect(component.newPatternLabel).toEqual('PATTERN_LABEL');
+    expect(component.availablePatternLabels).toEqual(['PATTERN_LABEL']);
+
+    expect(component.sampleData.getNextSample['calls'].count()).toEqual(2);
+
+    fixture.destroy();
+  }));
+
+  it('should test grok statement validation', async(() => {
+
+    let parsedMessage = {
+      'action': 'TCP_MISS',
+      'bytes': 337891,
+      'code': 200,
+      'elapsed': 415,
+      'ip_dst_addr': '207.109.73.154',
+      'ip_src_addr': '127.0.0.1',
+      'method': 'GET',
+      'timestamp': '1467011157.401',
+      'url': 'http://www.aliexpress.com/af/shoes.html?'
+    };
+    sensorParserConfigService.setParsedMessage(parsedMessage);
+
+    let sampleData = '1467011157.401 415 127.0.0.1 TCP_MISS/200 337891 GET http://www.aliexpress.com/af/shoes.html? ' +
+      '- DIRECT/207.109.73.154 text/html';
+    let grokStatement = 'SQUID_DELIMITED %{NUMBER:timestamp} %{INT:elapsed} %{IPV4:ip_src_addr} %{WORD:action}/%{NUMBER:code} ' +
+      '%{NUMBER:bytes} %{WORD:method} %{NOTSPACE:url} - %{WORD:UNWANTED}\/%{IPV4:ip_dst_addr} %{WORD:UNWANTED}\/%{WORD:UNWANTED}';
+
+    component.sensorParserConfig = new SensorParserConfig();
+    component.sensorParserConfig.sensorTopic = 'squid';
+    component.newGrokStatement = grokStatement;
+
+    component.onSampleDataChanged('');
+    expect(component.parsedMessage).toEqual({});
+    expect(component.parsedMessageKeys).toEqual([]);
+
+    component.onSampleDataChanged(sampleData);
+    expect(component.parsedMessage).toEqual(parsedMessage);
+    expect(component.parsedMessageKeys).toEqual(['action', 'bytes', 'code', 'elapsed', 'ip_dst_addr',
+      'ip_src_addr', 'method', 'timestamp', 'url']);
+
+    sensorParserConfigService.setParsedMessage('ERROR');
+    component.onTestGrokStatement();
+
+    expect(component.parsedMessage).toEqual({});
+
+    component.newGrokStatement = '';
+    component.onTestGrokStatement();
+    expect(component.parsedMessage).toEqual({});
+
+    fixture.destroy();
+  }));
+
+  it('should call appropriate functions on save ', () => {
+    spyOn(component.hideGrok, 'emit');
+    spyOn(component.onSaveGrokStatement, 'emit');
+    spyOn(component.onSavePatternLabel, 'emit');
+    component.newGrokStatement = 'grok statement';
+    component.newPatternLabel = 'PATTERN_LABEL';
+
+    component.onSaveGrok();
+
+    expect(component.onSaveGrokStatement.emit).toHaveBeenCalledWith('grok statement');
+    expect(component.onSavePatternLabel.emit).toHaveBeenCalledWith('PATTERN_LABEL');
+    expect(component.hideGrok.emit).toHaveBeenCalled();
+    fixture.destroy();
+  });
+
+  it('should call appropriate functions on cancel ', () => {
+    spyOn(component.hideGrok, 'emit');
+    spyOn(component.onSaveGrokStatement, 'emit');
+    spyOn(component.onSavePatternLabel, 'emit');
+
+    component.onCancelGrok();
+
+    expect(component.onSaveGrokStatement.emit).not.toHaveBeenCalled();
+    expect(component.onSavePatternLabel.emit).not.toHaveBeenCalled();
+    expect(component.hideGrok.emit).toHaveBeenCalled();
+    fixture.destroy();
+  });
+
+  it('should disable test', () => {
+    expect(component.isTestDisabled()).toEqual(true);
+    component.newGrokStatement = 'new grok statement';
+    expect(component.isTestDisabled()).toEqual(true);
+    component.parseMessageRequest.sampleData = 'sample data';
+    expect(component.isTestDisabled()).toEqual(false);
+  });
+
+  it('should disable save', () => {
+    component.availablePatternLabels = ['LABEL_1', 'LABEL_2'];
+    expect(component.isSaveDisabled()).toEqual(true);
+    component.newGrokStatement = 'new grok statement';
+    expect(component.isSaveDisabled()).toEqual(true);
+    component.newPatternLabel = 'LABEL_2';
+    expect(component.isSaveDisabled()).toEqual(false);
+  });
+
+});

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/sensors/sensor-grok/sensor-grok.component.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/sensors/sensor-grok/sensor-grok.component.ts b/metron-interface/metron-config/src/app/sensors/sensor-grok/sensor-grok.component.ts
new file mode 100644
index 0000000..c8bf513
--- /dev/null
+++ b/metron-interface/metron-config/src/app/sensors/sensor-grok/sensor-grok.component.ts
@@ -0,0 +1,135 @@
+import { Component, OnInit, Input, OnChanges, SimpleChanges, ViewChild, EventEmitter, Output} from '@angular/core';
+import {SensorParserConfig} from '../../model/sensor-parser-config';
+import {ParseMessageRequest} from '../../model/parse-message-request';
+import {SensorParserConfigService} from '../../service/sensor-parser-config.service';
+import {AutocompleteOption} from '../../model/autocomplete-option';
+import {GrokValidationService} from '../../service/grok-validation.service';
+import {SampleDataComponent} from '../../shared/sample-data/sample-data.component';
+import {MetronAlerts} from '../../shared/metron-alerts';
+
+@Component({
+  selector: 'metron-config-sensor-grok',
+  templateUrl: './sensor-grok.component.html',
+  styleUrls: ['./sensor-grok.component.scss']
+})
+export class SensorGrokComponent implements OnInit, OnChanges {
+
+  @Input() showGrok; boolean;
+  @Input() sensorParserConfig: SensorParserConfig;
+  @Input() grokStatement: string;
+  @Input() patternLabel: string;
+
+  @Output() hideGrok = new EventEmitter<void>();
+  @Output() onSaveGrokStatement = new EventEmitter<string>();
+  @Output() onSavePatternLabel = new EventEmitter<string>();
+
+  @ViewChild(SampleDataComponent) sampleData: SampleDataComponent;
+
+  newGrokStatement = '';
+  newPatternLabel = '';
+  availablePatternLabels = [];
+  parsedMessage: any = {};
+  parsedMessageKeys: string[] = [];
+  grokFunctionList: AutocompleteOption[] = [];
+  parseMessageRequest: ParseMessageRequest = new ParseMessageRequest();
+
+  constructor(private sensorParserConfigService: SensorParserConfigService, private grokValidationService: GrokValidationService,
+              private metronAlerts: MetronAlerts) {
+    this.parseMessageRequest.sampleData = '';
+  }
+
+  ngOnInit() {
+    this.getGrokFunctions();
+  }
+
+  ngOnChanges(changes: SimpleChanges) {
+    if (changes['showGrok'] && changes['showGrok'].currentValue) {
+      this.newPatternLabel = this.patternLabel;
+      if (this.grokStatement) {
+        this.newGrokStatement = this.grokStatement;
+      } else {
+        this.newGrokStatement = this.newPatternLabel + ' ';
+      }
+      this.getAvailablePatternLabels();
+      this.sampleData.getNextSample();
+    }
+  }
+
+  onSampleDataChanged(sampleData: string) {
+    if (sampleData) {
+      this.parseMessageRequest.sampleData = sampleData;
+      this.onTestGrokStatement();
+    }
+  }
+
+  onTestGrokStatement() {
+    this.parsedMessage = {};
+
+    if (this.newGrokStatement.indexOf('%{') === -1) {
+      return;
+    }
+
+    this.parseMessageRequest.sensorParserConfig = JSON.parse(JSON.stringify(this.sensorParserConfig));
+    this.parseMessageRequest.grokStatement = this.newGrokStatement;
+    this.parseMessageRequest.sensorParserConfig.parserConfig['patternLabel'] = this.newPatternLabel;
+    this.parseMessageRequest.sensorParserConfig.parserConfig['grokPath'] = './' + this.parseMessageRequest.sensorParserConfig.sensorTopic;
+
+    this.sensorParserConfigService.parseMessage(this.parseMessageRequest).subscribe(
+        result => {
+          this.parsedMessage = result;
+          this.setParsedMessageKeys();
+        }, error => {
+          this.metronAlerts.showErrorMessage(error.message);
+          this.setParsedMessageKeys();
+        });
+  }
+
+  private getGrokFunctions() {
+    this.grokValidationService.list().subscribe(result => {
+      Object.keys(result).forEach(name => {
+        let autocompleteOption: AutocompleteOption = new AutocompleteOption();
+        autocompleteOption.name = name;
+        this.grokFunctionList.push(autocompleteOption);
+      });
+    });
+  }
+
+  private setParsedMessageKeys() {
+    try {
+      this.parsedMessageKeys = Object.keys(this.parsedMessage).sort();
+    } catch (e) {
+      this.parsedMessageKeys = [];
+    }
+  }
+
+  onSaveGrok(): void {
+    this.onSaveGrokStatement.emit(this.newGrokStatement);
+    this.onSavePatternLabel.emit(this.newPatternLabel);
+    this.hideGrok.emit();
+  }
+
+  onCancelGrok(): void {
+    this.hideGrok.emit();
+  }
+
+  private getAvailablePatternLabels() {
+    this.availablePatternLabels = [];
+    let statements = this.newGrokStatement.split('\n');
+    for (let statement of statements) {
+      if (statement) {
+        let patternLabel = statement.split(' ')[0];
+        this.availablePatternLabels.push(patternLabel);
+      }
+    }
+  }
+
+  isTestDisabled() {
+    return this.parseMessageRequest.sampleData.length === 0 || this.newGrokStatement.length === 0;
+  }
+
+  isSaveDisabled() {
+    return this.newGrokStatement.length === 0 || this.availablePatternLabels.indexOf(this.newPatternLabel) === -1;
+  }
+
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/sensors/sensor-grok/sensor-grok.module.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/sensors/sensor-grok/sensor-grok.module.ts b/metron-interface/metron-config/src/app/sensors/sensor-grok/sensor-grok.module.ts
new file mode 100644
index 0000000..ef31798
--- /dev/null
+++ b/metron-interface/metron-config/src/app/sensors/sensor-grok/sensor-grok.module.ts
@@ -0,0 +1,30 @@
+/**
+ * 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 {SensorGrokComponent} from './sensor-grok.component';
+import {AceEditorModule} from '../../shared/ace-editor/ace-editor.module';
+import {SampleDataModule} from '../../shared/sample-data/sample-data.module';
+
+@NgModule ({
+    imports: [ SharedModule, AceEditorModule, SampleDataModule ],
+    declarations: [ SensorGrokComponent ],
+    exports: [ SensorGrokComponent ]
+})
+
+export class SensorGrokModule { }

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/sensors/sensor-parser-config-readonly/index.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/sensors/sensor-parser-config-readonly/index.ts b/metron-interface/metron-config/src/app/sensors/sensor-parser-config-readonly/index.ts
new file mode 100644
index 0000000..3a63c39
--- /dev/null
+++ b/metron-interface/metron-config/src/app/sensors/sensor-parser-config-readonly/index.ts
@@ -0,0 +1 @@
+export * from './sensor-parser-config-readonly.component';

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/sensors/sensor-parser-config-readonly/sensor-parser-config-readonly.component.html
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/sensors/sensor-parser-config-readonly/sensor-parser-config-readonly.component.html b/metron-interface/metron-config/src/app/sensors/sensor-parser-config-readonly/sensor-parser-config-readonly.component.html
new file mode 100644
index 0000000..d988dd1
--- /dev/null
+++ b/metron-interface/metron-config/src/app/sensors/sensor-parser-config-readonly/sensor-parser-config-readonly.component.html
@@ -0,0 +1,108 @@
+<!--
+  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-metron-modal [backgroundMasked]="false">
+
+    <div class="metron-slider-pane fill load-right-to-left dialog1x">
+      <div class="metron-readonly-pane">
+      <div class="row">
+        <div class="col-xs-12 form-title">{{selectedSensorName}}
+          <i class="fa fa-times pull-right close-button" aria-hidden="true" (click)="goBack()"></i>
+        </div>
+      </div>
+
+      <div *ngFor="let item of editViewMetaData">
+        <div [ngSwitch]="item.type">
+
+          <div *ngSwitchCase="'SEPARATOR'" class="row form-seperator">
+          </div>
+
+            <div  *ngSwitchCase="'SPACER'" class="row">
+                <div class="col-xs-12">&nbsp;</div>
+            </div>
+
+          <div  *ngSwitchCase="'TITLE'" class="row">
+            <div class="col-xs-12 form-sub-title">{{ item.value }}</div>
+          </div>
+
+          <div *ngSwitchDefault class="row">
+            <div *ngIf="item.label!=''" class="col-xs-6 form-label" [ngClass]="{'form-value font-weight-bold': item.boldTitle}">{{ item.label }}</div>
+            <div *ngIf="item.model == 'sensorParserConfigHistory'" class="col-xs-6  px-0 pull-left form-value">{{ sensorParserConfigHistory[item.value] ? sensorParserConfigHistory[item.value] : "-" }}</div>
+            <div *ngIf="item.model == 'kafkaTopic'" class="col-xs-6  px-0 pull-left form-value">{{ kafkaTopic[item.value] ? kafkaTopic[item.value] : "-" }}</div>
+            <div *ngIf="item.model == 'topologyStatus'" class="col-xs-6  px-0  pull-left form-value">{{ getTopologyStatus(item.value) }}</div>
+
+            <div *ngIf="item.model == 'grokStatement' && sensorParserConfigHistory.config.parserClassName === 'org.apache.metron.parsers.GrokParser'" style="border: none">
+              <div class="col-xs-12 form-sub-title">Grok Statement</div>
+              <div id="collapseGrok" class="col-xs-12  pull-left form-value panel-collapse collapse"></div>
+              <div class="col-xs-12  pull-left form-value grok" [innerHtml]="grokStatement"></div>
+              <a *ngIf="grokStatement.length>0" class="collapsed blue-label font-weight-bold col-xs-8 col-xs-push-4" data-toggle="collapse" href="#collapseGrok" aria-expanded="false" aria-controls="collapseGrok"  #grokLink (click)="grokLink.text=(grokLink.text==='show more')?'show less':'show more'">show more</a>
+              <div class="px-1"> <div class="col-xs-12 form-seperator"></div> </div>
+            </div>
+
+            <div *ngIf="item.model == 'transforms'">
+
+              <div id="collapseTransform" class="col-xs-12  pull-left form-value panel-collapse collapse">
+                <div class="form-sub-sub-title">Transforms</div>
+                <div>
+                  <div *ngFor="let results of transformsConfigKeys" >
+                    <div class="form-label">{{ results }}</div>
+                    <div class="form-value">{{ transformsConfigMap[results] }}</div>
+                  </div>
+                </div>
+              </div>
+
+              <div class="transforms">
+                <div class="col-xs-12 form-sub-sub-title">Transforms</div>
+                <div class="col-xs-12 form-label " [innerHtml]="getTransformsOutput()"></div>
+              </div>
+              <a *ngIf="transformsConfigKeys.length>0"  class="collapsed blue-label font-weight-bold col-xs-8 col-xs-push-4" data-toggle="collapse" href="#collapseTransform" aria-expanded="false" aria-controls="collapseTransform" (click)="toggleTransformLink()">{{transformLinkText}}</a>
+            </div>
+            <div *ngIf="item.model == 'threatTriageRules'" class="threat-triage-rules">
+              <div class="col-xs-6 form-label">AGGREGATOR</div><div class="col-xs-6 form-value">{{sensorEnrichmentConfig.threatIntel.triageConfig.aggregator}}</div>
+              <div id="collapseThreatTriage" class="col-xs-12  pull-left form-value panel-collapse collapse">
+                <div>
+                  <div class="col-xs-6 form-sub-sub-title">NAME</div><div class="col-xs-6 form-sub-sub-title">SCORE</div>
+                  <div *ngFor="let riskLevelRule of rules" >
+                    <div class="col-xs-6 form-label">{{ getDisplayName(riskLevelRule) }}</div>
+                    <div class="col-xs-6 form-value">{{ riskLevelRule.score }}</div>
+                  </div>
+                </div>
+              </div>
+              <div  class="col-xs-12 form-label threatIntel">{{ getRuleDisplayName() }}</div>
+              <a *ngIf="rules.length>0" class="collapsed blue-label font-weight-bold col-xs-8 col-xs-push-4" data-toggle="collapse" href="#collapseThreatTriage" aria-expanded="false" aria-controls="collapseThreatTriage" (click)="toggleThreatTriageLink()">{{threatTriageLinkText}}</a>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+
+      <div class="metron-button-bar" >
+          <div class="row pl-0 py-1">
+            <button class="btn btn-primary" type="button" (click)="onEditSensor()"  [disabled]="startStopInProgress">EDIT</button>
+
+            <button class="btn btn-primary" type="button" (click)="onStartSensor()" [disabled]="startStopInProgress" [hidden]="isStartHidden()">START</button>
+            <button class="btn form-enable-disable-button" type="button" (click)="onStopSensor()" [disabled]="startStopInProgress" [hidden]="isStopHidden()">STOP</button>
+
+            <button class="btn btn-primary" type="button" (click)="onEnableSensor()" [disabled]="startStopInProgress" [hidden]="isEnableHidden()" >ENABLE</button>
+            <button class="btn form-enable-disable-button" type="button" (click)="onDisableSensor()" [disabled]="startStopInProgress" [hidden]="isDisableHidden()">DISABLE</button>
+
+            <button class="btn btn-link delete" type="button" (click)="onDeleteSensor()"> Delete </button>
+
+            <div class="start-stop-progress"> <i class="fa fa-circle-o-notch fa-spin fa-lg fa-fw" [hidden]="!startStopInProgress"></i> </div>
+          </div>
+      </div>
+    </div>
+
+</metron-config-metron-modal>

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/sensors/sensor-parser-config-readonly/sensor-parser-config-readonly.component.scss
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/sensors/sensor-parser-config-readonly/sensor-parser-config-readonly.component.scss b/metron-interface/metron-config/src/app/sensors/sensor-parser-config-readonly/sensor-parser-config-readonly.component.scss
new file mode 100644
index 0000000..b78b559
--- /dev/null
+++ b/metron-interface/metron-config/src/app/sensors/sensor-parser-config-readonly/sensor-parser-config-readonly.component.scss
@@ -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 "../../_variables.scss";
+@import "../../../styles.scss";
+
+.metron-readonly-pane {
+  margin-bottom: $button-bar-height + 10px;
+}
+
+.metron-button-bar
+{
+  background: $gray-light;
+  border-top: solid 2px $gray-border;
+  border-left: solid 1px $gray-border;
+  position: fixed;
+  width: inherit;
+}
+
+.grok
+{
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  font-family: Roboto-Regular;
+  font-size: 13px;
+  color: $text-color-white
+}
+
+.in + .grok
+{
+  font-size: 13px;
+  overflow: auto;
+  display: inline-block;
+  white-space: normal;
+  font-family: Roboto-Regular;
+  color: $text-color-white;
+}
+
+.threatIntel
+{
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+
+.in ~ .transforms, .in + .threatIntel
+{
+  display: none;
+}
+
+a
+{
+  width: 100%;
+  text-align: center;
+  font-family: Roboto-Regular;
+}
+
+.form-enable-disable-button
+{
+  min-width: 50px;
+}
+
+.delete
+{
+  color:#E45D55;
+  font-weight: bold;
+  padding: 0.1rem;
+}
+
+.form-sub-sub-title
+{
+  font-size: 14px;
+  font-family: Roboto-Regular;
+  color: $text-color-white;
+}
+
+.form-sub-title
+{
+  font-size: 16px;
+  font-family: Roboto-Regular;
+  color: $form-field-text-color;
+}
+
+.start-stop-progress
+{
+  position: absolute;
+  top: 20px;
+  right: 140px;
+}
+
+.threat-triage-rules {
+  .form-label {
+    word-wrap: initial;
+  }
+}