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"> </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;
+ }
+}