metron-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From rmerri...@apache.org
Subject [07/12] incubator-metron git commit: METRON-623 Management UI [contributed by Raghu Mitra Kandikonda and Ryan Merriman] closes apache/incubator-metron#489
Date Tue, 11 Apr 2017 13:51:18 GMT
http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/sensors/sensor-parser-config/sensor-parser-config.component.spec.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/sensors/sensor-parser-config/sensor-parser-config.component.spec.ts b/metron-interface/metron-config/src/app/sensors/sensor-parser-config/sensor-parser-config.component.spec.ts
new file mode 100644
index 0000000..6c4eab1
--- /dev/null
+++ b/metron-interface/metron-config/src/app/sensors/sensor-parser-config/sensor-parser-config.component.spec.ts
@@ -0,0 +1,1014 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {async, ComponentFixture, TestBed} from '@angular/core/testing';
+import {Inject} from '@angular/core';
+import {Observable} from 'rxjs/Observable';
+import {Router, ActivatedRoute, Params} from '@angular/router';
+import {Http, RequestOptions, Response, ResponseOptions} from '@angular/http';
+import {SensorParserConfigComponent, Pane, KafkaStatus} from './sensor-parser-config.component';
+import {StellarService} from '../../service/stellar.service';
+import {SensorParserConfigService} from '../../service/sensor-parser-config.service';
+import {KafkaService} from '../../service/kafka.service';
+import {KafkaTopic} from '../../model/kafka-topic';
+import {GrokValidationService} from '../../service/grok-validation.service';
+import {MetronAlerts} from '../../shared/metron-alerts';
+import {SensorParserConfig} from '../../model/sensor-parser-config';
+import {ParseMessageRequest} from '../../model/parse-message-request';
+import {SensorParserContext} from '../../model/sensor-parser-context';
+import {AuthenticationService} from '../../service/authentication.service';
+import {FieldTransformer} from '../../model/field-transformer';
+import {SensorParserConfigModule} from './sensor-parser-config.module';
+import {SensorEnrichmentConfigService} from '../../service/sensor-enrichment-config.service';
+import {SensorEnrichmentConfig} from '../../model/sensor-enrichment-config';
+import {APP_CONFIG, METRON_REST_CONFIG} from '../../app.config';
+import {IAppConfig} from '../../app.config.interface';
+import {SensorIndexingConfigService} from '../../service/sensor-indexing-config.service';
+import {IndexingConfigurations} from '../../model/sensor-indexing-config';
+import '../../rxjs-operators';
+import 'rxjs/add/observable/of';
+import {HdfsService} from '../../service/hdfs.service';
+import {RestError} from '../../model/rest-error';
+import {RiskLevelRule} from '../../model/risk-level-rule';
+
+
+class MockRouter {
+  navigateByUrl(url: string) {}
+}
+
+class MockActivatedRoute {
+  private name: string;
+  params: Observable<Params>;
+
+  setNameForTest(name: string) {
+    this.name = name;
+    this.params = Observable.create(observer => {
+      observer.next({id: this.name});
+      observer.complete();
+    });
+  }
+}
+
+class MockSensorParserConfigService extends SensorParserConfigService {
+  private sensorParserConfig: SensorParserConfig;
+  private parsedMessage: any;
+  private postedSensorParserConfig: SensorParserConfig;
+  private throwError: boolean;
+
+  constructor(private http2: Http, @Inject(APP_CONFIG) private config2: IAppConfig) {
+    super(http2, config2);
+  }
+
+  public post(sensorParserConfig: SensorParserConfig): Observable<SensorParserConfig> {
+    if (this.throwError) {
+      let error = new RestError();
+      error.message = 'SensorParserConfig post error';
+      return Observable.throw(error);
+    }
+    this.postedSensorParserConfig = sensorParserConfig;
+    return Observable.create(observer => {
+      observer.next(sensorParserConfig);
+      observer.complete();
+    });
+  }
+
+  public get(name: string): Observable<SensorParserConfig> {
+    return Observable.create(observer => {
+      observer.next(this.sensorParserConfig);
+      observer.complete();
+    });
+  }
+
+  public getAvailableParsers(): Observable<{}> {
+    return Observable.create(observer => {
+      observer.next({
+        'Bro': 'org.apache.metron.parsers.bro.BasicBroParser',
+        'Grok': 'org.apache.metron.parsers.GrokParser'
+      });
+      observer.complete();
+    });
+  }
+
+  public parseMessage(parseMessageRequest: ParseMessageRequest): Observable<{}> {
+    return Observable.create(observer => {
+      observer.next(this.parsedMessage);
+      observer.complete();
+    });
+  }
+
+  public setSensorParserConfig(result: any) {
+    this.sensorParserConfig = result;
+  }
+
+  public setParsedMessage(parsedMessage: any) {
+    this.parsedMessage = parsedMessage;
+  }
+
+  public setThrowError(throwError: boolean) {
+    this.throwError = throwError;
+  }
+
+  public getPostedSensorParserConfig() {
+    return this.postedSensorParserConfig;
+  }
+}
+
+class MockSensorIndexingConfigService extends SensorIndexingConfigService {
+  private name: string;
+  private postedIndexingConfigurations: IndexingConfigurations;
+  private sensorIndexingConfig: IndexingConfigurations;
+  private throwError: boolean;
+
+  constructor(private http2: Http, @Inject(APP_CONFIG) private config2: IAppConfig) {
+    super(http2, config2);
+  }
+
+  public post(name: string, sensorIndexingConfig: IndexingConfigurations): Observable<IndexingConfigurations> {
+    if (this.throwError) {
+      let error = new RestError();
+      error.message = 'IndexingConfigurations post error';
+      return Observable.throw(error);
+    }
+    this.postedIndexingConfigurations = sensorIndexingConfig;
+    return Observable.create(observer => {
+      observer.next(sensorIndexingConfig);
+      observer.complete();
+    });
+  }
+
+  public get(name: string): Observable<IndexingConfigurations> {
+    if (this.sensorIndexingConfig === null) {
+      let error = new RestError();
+      error.message = 'IndexingConfigurations get error';
+      return Observable.throw(error);
+    }
+    return Observable.create(observer => {
+      if (name === this.name) {
+        observer.next(this.sensorIndexingConfig);
+      }
+      observer.complete();
+    });
+  }
+
+  public setSensorIndexingConfig(name: string, result: IndexingConfigurations) {
+    this.name = name;
+    this.sensorIndexingConfig = result;
+  }
+
+  public setThrowError(throwError: boolean) {
+    this.throwError = throwError;
+  }
+
+  public getPostedIndexingConfigurations(): IndexingConfigurations {
+    return this.postedIndexingConfigurations;
+  }
+}
+
+class MockKafkaService extends KafkaService {
+  private name: string;
+  private kafkaTopic: KafkaTopic;
+  private kafkaTopicForPost: KafkaTopic;
+  private sampleData = {'key1': 'value1', 'key2': 'value2'};
+
+  constructor(private http2: Http, @Inject(APP_CONFIG) private config2: IAppConfig) {
+    super(http2, config2);
+  }
+
+  public setKafkaTopic(name: string, kafkaTopic: KafkaTopic) {
+    this.name = name;
+    this.kafkaTopic = kafkaTopic;
+  }
+
+  public setSampleData(name: string, sampleData?: any) {
+    this.name = name;
+    this.sampleData = sampleData;
+  }
+
+  public sample(name: string): Observable<string> {
+    if (this.sampleData === null) {
+      return Observable.throw(new RestError());
+    }
+    return Observable.create(observer => {
+      if (name === this.name) {
+        observer.next(JSON.stringify(this.sampleData));
+      }
+      observer.complete();
+    });
+  }
+
+  public get(name: string): Observable<KafkaTopic> {
+    if (this.kafkaTopic === null) {
+      return Observable.throw(new RestError());
+    }
+    return Observable.create(observer => {
+      if (name === this.name) {
+        observer.next(this.kafkaTopic);
+      }
+      observer.complete();
+    });
+  }
+
+  public post(k: KafkaTopic): Observable<KafkaTopic> {
+    this.kafkaTopicForPost = k;
+    console.log('called post MockKafkaService: ' + this.kafkaTopicForPost);
+    return Observable.create(observer => {
+      observer.next({});
+      observer.complete();
+    });
+  }
+
+  public getKafkaTopicForPost(): KafkaTopic {
+    console.log('Called get MockKafkaService: ' + this.kafkaTopicForPost);
+    return this.kafkaTopicForPost;
+  }
+}
+
+class MockGrokValidationService extends GrokValidationService {
+
+  private path: string;
+  private contents: string;
+
+  constructor(private http2: Http, @Inject(APP_CONFIG) private config2: IAppConfig) {
+    super(http2, config2);
+  }
+
+  public setContents(path: string, contents: string) {
+    this.path = path;
+    this.contents = contents;
+  }
+
+  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();
+    });
+  }
+
+
+  public getStatement(path: string): Observable<string> {
+    if (this.contents === null) {
+      return Observable.throw('Error');
+    }
+    return Observable.create(observer => {
+      if (path === this.path) {
+        observer.next(this.contents);
+      }
+      observer.complete();
+    });
+  }
+}
+
+class MockHdfsService extends HdfsService {
+  private fileList: string[];
+  private path: string;
+  private contents: string;
+  private postedContents: string;
+
+  constructor(private http2: Http, @Inject(APP_CONFIG) private config2: IAppConfig) {
+    super(http2, config2);
+  }
+
+  public setFileList(path: string, fileList: string[]) {
+    this.path = path;
+    this.fileList = fileList;
+  }
+
+  public setContents(path: string, contents: string) {
+    this.path = path;
+    this.contents = contents;
+  }
+
+  public list(path: string): Observable<string[]> {
+    if (this.fileList === null) {
+      return Observable.throw('Error');
+    }
+    return Observable.create(observer => {
+      if (path === this.path) {
+        observer.next(this.fileList);
+      }
+      observer.complete();
+    });
+  }
+
+  public read(path: string): Observable<string> {
+    if (this.contents === null) {
+      return Observable.throw('Error');
+    }
+    return Observable.create(observer => {
+      if (path === this.path) {
+        observer.next(this.contents);
+      }
+      observer.complete();
+    });
+  }
+
+  public post(path: string, contents: string): Observable<{}> {
+    if (this.contents === null) {
+      let error = new RestError();
+      error.message = 'HDFS post Error';
+      return Observable.throw(error);
+    }
+    this.postedContents = contents;
+    return Observable.create(observer => {
+      observer.next(contents);
+      observer.complete();
+    });
+  }
+
+  public deleteFile(path: string): Observable<Response> {
+    return Observable.create(observer => {
+      observer.next({});
+      observer.complete();
+    });
+  }
+
+  public getPostedContents() {
+    return this.postedContents;
+  }
+}
+
+class MockAuthenticationService extends AuthenticationService {
+
+  constructor(private http2: Http, private router2: Router, @Inject(APP_CONFIG) private config2: IAppConfig) {
+    super(http2, router2, config2);
+  }
+
+  public getCurrentUser(options: RequestOptions): Observable<Response> {
+    let responseOptions: ResponseOptions = new ResponseOptions();
+    responseOptions.body = 'user';
+    let response: Response = new Response(responseOptions);
+    return Observable.of(response);
+  };
+}
+
+class MockTransformationValidationService extends StellarService {
+
+  private transformationValidationResult: any;
+  private transformationValidationForValidate: SensorParserContext;
+
+  constructor(private http2: Http, @Inject(APP_CONFIG) private config2: IAppConfig) {
+    super(http2, config2);
+  }
+
+  public setTransformationValidationResultForTest(transformationValidationResult: any): void {
+    this.transformationValidationResult = transformationValidationResult;
+  }
+
+  public getTransformationValidationForValidate(): SensorParserContext {
+    return this.transformationValidationForValidate;
+  }
+
+  public validate(t: SensorParserContext): Observable<{}> {
+    this.transformationValidationForValidate = t;
+    return Observable.create(observer => {
+      observer.next(this.transformationValidationResult);
+      observer.complete();
+    });
+  }
+}
+
+export class MockSensorEnrichmentConfigService {
+  private name: string;
+  private sensorEnrichmentConfig: SensorEnrichmentConfig;
+  private postedSensorEnrichmentConfig: SensorEnrichmentConfig;
+  private throwError: boolean;
+
+  public setSensorEnrichmentConfig(name: string, sensorEnrichmentConfig: SensorEnrichmentConfig) {
+    this.name = name;
+    this.sensorEnrichmentConfig = sensorEnrichmentConfig;
+  }
+
+  public get(name: string): Observable<SensorEnrichmentConfig> {
+    if (this.sensorEnrichmentConfig === null) {
+      let error = new RestError();
+      error.message = 'SensorEnrichmentConfig get error';
+      return Observable.throw(error);
+    }
+    return Observable.create(observer => {
+      if (name === this.name) {
+        observer.next(this.sensorEnrichmentConfig);
+      }
+      observer.complete();
+    });
+  }
+
+  public post(name: string, sensorEnrichmentConfig: SensorEnrichmentConfig): Observable<SensorEnrichmentConfig> {
+    if (this.throwError) {
+      let error = new RestError();
+      error.message = 'SensorEnrichmentConfig post error';
+      return Observable.throw(error);
+    }
+    this.postedSensorEnrichmentConfig = sensorEnrichmentConfig;
+    return  Observable.create(observer => {
+      observer.next(sensorEnrichmentConfig);
+      observer.complete();
+    });
+  }
+
+  public setThrowError(throwError: boolean) {
+    this.throwError = throwError;
+  }
+
+  public getPostedSensorEnrichmentConfig() {
+    return this.postedSensorEnrichmentConfig;
+  }
+}
+
+describe('Component: SensorParserConfig', () => {
+
+  let component: SensorParserConfigComponent;
+  let fixture: ComponentFixture<SensorParserConfigComponent>;
+  let sensorParserConfigService: MockSensorParserConfigService;
+  let sensorEnrichmentConfigService: MockSensorEnrichmentConfigService;
+  let sensorIndexingConfigService: MockSensorIndexingConfigService;
+  let transformationValidationService: MockTransformationValidationService;
+  let kafkaService: MockKafkaService;
+  let hdfsService: MockHdfsService;
+  let grokValidationService: MockGrokValidationService;
+  let activatedRoute: MockActivatedRoute;
+  let metronAlerts: MetronAlerts;
+  let router: MockRouter;
+
+  let squidSensorParserConfig: any = {
+    'parserClassName': 'org.apache.metron.parsers.GrokParser',
+    'sensorTopic': 'squid',
+    'parserConfig': {
+      'grokPath': '/apps/metron/patterns/squid',
+      'patternLabel': 'SQUID_DELIMITED',
+      'timestampField': 'timestamp'
+    },
+    'fieldTransformations': [
+      {
+        'input': [],
+        'output': ['full_hostname', 'domain_without_subdomains', 'hostname'],
+        'transformation': 'STELLAR',
+        'config': {
+          'full_hostname': 'URL_TO_HOST(url)',
+          'domain_without_subdomains': 'DOMAIN_REMOVE_SUBDOMAINS(full_hostname)'
+        }
+      }
+    ],
+  };
+
+  let squidSensorEnrichmentConfig = {
+    'enrichment': {
+      'fieldMap': {
+        'geo': ['ip_dst_addr'],
+        'host': ['ip_dst_addr'],
+        'whois': [],
+        'stellar': { 'config': { 'group1': {} }}
+      },
+      'fieldToTypeMap': {}, 'config': {}
+    },
+    'threatIntel': {
+      'threatIntel': {
+        'fieldMap': { 'hbaseThreatIntel': ['ip_dst_addr'] },
+        'fieldToTypeMap': { 'ip_dst_addr': ['malicious_ip'] }
+      }
+    }
+  };
+
+  let squidIndexingConfigurations = {
+    'hdfs': {
+      'index': 'squid',
+      'batchSize': 5,
+      'enabled': true
+    },
+    'elasticsearch': {
+      'index': 'squid',
+      'batchSize': 10,
+      'enabled': true
+    },
+    'solr': {
+      'index': 'squid',
+      'batchSize': 1,
+      'enabled': false
+    },
+  };
+
+  beforeEach(async(() => {
+
+    TestBed.configureTestingModule({
+      imports: [SensorParserConfigModule],
+      providers: [
+        MetronAlerts,
+        {provide: Http},
+        {provide: SensorParserConfigService, useClass: MockSensorParserConfigService},
+        {provide: SensorIndexingConfigService, useClass: MockSensorIndexingConfigService},
+        {provide: KafkaService, useClass: MockKafkaService},
+        {provide: HdfsService, useClass: MockHdfsService},
+        {provide: GrokValidationService, useClass: MockGrokValidationService},
+        {provide: StellarService, useClass: MockTransformationValidationService},
+        {provide: ActivatedRoute, useClass: MockActivatedRoute},
+        {provide: Router, useClass: MockRouter},
+        {provide: AuthenticationService, useClass: MockAuthenticationService},
+        {provide: SensorEnrichmentConfigService, useClass: MockSensorEnrichmentConfigService},
+        {provide: APP_CONFIG, useValue: METRON_REST_CONFIG}
+      ]
+    }).compileComponents()
+      .then(() => {
+        fixture = TestBed.createComponent(SensorParserConfigComponent);
+        component = fixture.componentInstance;
+        sensorParserConfigService = fixture.debugElement.injector.get(SensorParserConfigService);
+        sensorIndexingConfigService = fixture.debugElement.injector.get(SensorIndexingConfigService);
+        transformationValidationService = fixture.debugElement.injector.get(StellarService);
+        kafkaService = fixture.debugElement.injector.get(KafkaService);
+        hdfsService = fixture.debugElement.injector.get(HdfsService);
+        grokValidationService = fixture.debugElement.injector.get(GrokValidationService);
+        sensorEnrichmentConfigService = fixture.debugElement.injector.get(SensorEnrichmentConfigService);
+        activatedRoute = fixture.debugElement.injector.get(ActivatedRoute);
+        metronAlerts = fixture.debugElement.injector.get(MetronAlerts);
+        router = fixture.debugElement.injector.get(Router);
+      });
+
+  }));
+
+  it('should create an instance of SensorParserConfigComponent', async(() => {
+    expect(component).toBeDefined();
+
+    fixture.destroy();
+  }));
+
+  it('should handle ngOnInit', async(() => {
+    spyOn(component, 'init');
+    spyOn(component, 'createForms');
+    spyOn(component, 'getAvailableParsers');
+
+    activatedRoute.setNameForTest('squid');
+
+    component.ngOnInit();
+
+    expect(component.init).toHaveBeenCalledWith('squid');
+    expect(component.createForms).toHaveBeenCalled();
+    expect(component.getAvailableParsers).toHaveBeenCalled();
+
+    fixture.destroy();
+  }));
+
+  it('should createForms', async(() => {
+    component.sensorParserConfig = Object.assign(new SensorParserConfig(), squidSensorParserConfig);
+    component.createForms();
+
+    expect(Object.keys(component.sensorConfigForm.controls).length).toEqual(15);
+    expect(Object.keys(component.transformsValidationForm.controls).length).toEqual(2);
+    expect(component.showAdvancedParserConfiguration).toEqual(true);
+
+    component.sensorParserConfig.parserConfig = {};
+    component.showAdvancedParserConfiguration = false;
+    component.createForms();
+    expect(component.showAdvancedParserConfiguration).toEqual(false);
+
+    fixture.destroy();
+  }));
+
+  it('should getAvailableParsers', async(() => {
+    component.getAvailableParsers();
+    expect(component.availableParsers).toEqual({
+      'Bro': 'org.apache.metron.parsers.bro.BasicBroParser',
+      'Grok': 'org.apache.metron.parsers.GrokParser'
+    });
+    expect(component.availableParserNames).toEqual(['Bro', 'Grok']);
+
+    fixture.destroy();
+  }));
+
+  it('should init', async(() => {
+    component.init('new');
+
+    let expectedSensorParserConfig = new SensorParserConfig();
+    expectedSensorParserConfig.parserClassName = 'org.apache.metron.parsers.GrokParser';
+    expect(component.sensorParserConfig).toEqual(expectedSensorParserConfig);
+    expect(component.sensorEnrichmentConfig).toEqual(new SensorEnrichmentConfig());
+    expect(component.indexingConfigurations).toEqual(new IndexingConfigurations());
+    expect(component.editMode).toEqual(false);
+
+    spyOn(component, 'getKafkaStatus');
+    let sensorParserConfig = Object.assign(new SensorParserConfig(), squidSensorParserConfig);
+    sensorParserConfigService.setSensorParserConfig(sensorParserConfig);
+    sensorEnrichmentConfigService.setSensorEnrichmentConfig('squid',
+        Object.assign(new SensorEnrichmentConfig(), squidSensorEnrichmentConfig));
+    sensorIndexingConfigService.setSensorIndexingConfig('squid',
+        Object.assign(new IndexingConfigurations(), squidIndexingConfigurations));
+    hdfsService.setContents('/apps/metron/patterns/squid', 'SQUID_DELIMITED grok statement');
+
+    component.init('squid');
+    expect(component.sensorParserConfig).toEqual(Object.assign(new SensorParserConfig(), squidSensorParserConfig));
+    expect(component.sensorNameValid).toEqual(true);
+    expect(component.getKafkaStatus).toHaveBeenCalled();
+    expect(component.showAdvancedParserConfiguration).toEqual(true);
+    expect(component.grokStatement).toEqual('SQUID_DELIMITED grok statement');
+    expect(component.patternLabel).toEqual('SQUID_DELIMITED');
+    expect(component.sensorEnrichmentConfig).toEqual(Object.assign(new SensorEnrichmentConfig(), squidSensorEnrichmentConfig));
+    expect(component.indexingConfigurations).toEqual(Object.assign(new IndexingConfigurations(), squidIndexingConfigurations));
+
+    component.sensorParserConfig.parserConfig['grokPath'] = '/patterns/squid';
+    hdfsService.setContents('/patterns/squid', null);
+    grokValidationService.setContents('/patterns/squid', 'SQUID grok statement from classpath');
+
+    component.init('squid');
+    expect(component.grokStatement).toEqual('SQUID grok statement from classpath');
+
+    spyOn(metronAlerts, 'showErrorMessage');
+
+    sensorEnrichmentConfigService.setSensorEnrichmentConfig('squid', null);
+    component.init('squid');
+    expect(metronAlerts.showErrorMessage).toHaveBeenCalledWith('SensorEnrichmentConfig get error');
+
+    sensorIndexingConfigService.setSensorIndexingConfig('squid', null);
+    component.init('squid');
+    expect(metronAlerts.showErrorMessage).toHaveBeenCalledWith('IndexingConfigurations get error');
+
+    grokValidationService.setContents('/patterns/squid', null);
+
+    component.init('squid');
+    expect(metronAlerts.showErrorMessage).toHaveBeenCalledWith('Could not find grok statement in HDFS or classpath at /patterns/squid');
+
+    sensorParserConfig = new SensorParserConfig();
+    sensorParserConfig.sensorTopic = 'bro';
+    sensorParserConfigService.setSensorParserConfig(sensorParserConfig);
+    component.showAdvancedParserConfiguration = false;
+
+    component.init('bro');
+    expect(component.showAdvancedParserConfiguration).toEqual(false);
+
+    fixture.destroy();
+  }));
+
+  it('should getMessagePrefix', async(() => {
+    component.getAvailableParsers();
+    expect(component.getMessagePrefix()).toEqual('Created');
+    component.editMode = true;
+    expect(component.getMessagePrefix()).toEqual('Modified');
+
+    fixture.destroy();
+  }));
+
+  it('should handle onSetSensorName', async(() => {
+    spyOn(component, 'getKafkaStatus');
+    spyOn(component, 'isConfigValid');
+
+    component.onSetSensorName();
+    expect(component.getKafkaStatus).not.toHaveBeenCalled();
+    expect(component.isConfigValid).toHaveBeenCalled();
+
+    component.sensorParserConfig.sensorTopic = 'bro';
+    component.onSetSensorName();
+    expect(component.sensorNameValid).toEqual(true);
+    expect(component.getKafkaStatus).toHaveBeenCalled();
+
+    fixture.destroy();
+  }));
+
+  it('should handle onParserTypeChange', async(() => {
+    spyOn(component, 'hidePane');
+    spyOn(component, 'isConfigValid');
+
+    component.onParserTypeChange();
+    expect(component.hidePane).not.toHaveBeenCalled();
+    expect(component.isConfigValid).toHaveBeenCalled();
+
+    component.sensorParserConfig.parserClassName = 'org.apache.metron.parsers.GrokParser';
+    component.onParserTypeChange();
+    expect(component.parserClassValid).toEqual(true);
+    expect(component.hidePane).not.toHaveBeenCalled();
+
+    component.sensorParserConfig.parserClassName = 'org.apache.metron.parsers.bro.BasicBroParser';
+    component.onParserTypeChange();
+    expect(component.hidePane).toHaveBeenCalledWith(Pane.GROK);
+
+    fixture.destroy();
+  }));
+
+  it('should handle onGrokStatementChange', async(() => {
+    spyOn(component, 'isConfigValid');
+
+    component.onGrokStatementChange();
+    expect(component.grokStatementValid).toEqual(false);
+    expect(component.isConfigValid).toHaveBeenCalled();
+
+    component.grokStatement = 'grok statement';
+    component.onGrokStatementChange();
+    expect(component.grokStatementValid).toEqual(true);
+
+    fixture.destroy();
+  }));
+
+  it('should handle isConfigValid', async(() => {
+    component.isConfigValid();
+    expect(component.configValid).toEqual(false);
+
+    component.sensorNameValid = true;
+    component.parserClassValid = true;
+
+    component.isConfigValid();
+    expect(component.configValid).toEqual(true);
+
+    component.sensorParserConfig.parserClassName = 'org.apache.metron.parsers.GrokParser';
+    component.isConfigValid();
+    expect(component.configValid).toEqual(false);
+
+    component.grokStatementValid = true;
+    component.isConfigValid();
+    expect(component.configValid).toEqual(true);
+
+    fixture.destroy();
+  }));
+
+  it('should getKafkaStatus', async(() => {
+    component.getKafkaStatus();
+    expect(component.currentKafkaStatus).toEqual(null);
+
+    component.sensorParserConfig.sensorTopic = 'squid';
+    kafkaService.setKafkaTopic('squid', null);
+
+    component.getKafkaStatus();
+    expect(component.currentKafkaStatus).toEqual(KafkaStatus.NO_TOPIC);
+
+    kafkaService.setKafkaTopic('squid', new KafkaTopic());
+    kafkaService.setSampleData('squid', null);
+
+    component.getKafkaStatus();
+    expect(component.currentKafkaStatus).toEqual(KafkaStatus.NOT_EMITTING);
+
+    kafkaService.setSampleData('squid', 'message');
+    component.getKafkaStatus();
+    expect(component.currentKafkaStatus).toEqual(KafkaStatus.EMITTING);
+
+    fixture.destroy();
+  }));
+
+  it('should getTransforms', async(() => {
+    expect(component.getTransforms()).toEqual('0 Transformations Applied');
+
+    component.sensorParserConfig.fieldTransformations.push(Object.assign(new FieldTransformer(), {'output': ['field1', 'field2']}));
+    component.sensorParserConfig.fieldTransformations.push(Object.assign(new FieldTransformer(), {'output': ['field3']}));
+
+    expect(component.getTransforms()).toEqual('3 Transformations Applied');
+
+    fixture.destroy();
+  }));
+
+  it('should handle onSaveGrokStatement', async(() => {
+    component.sensorParserConfig.sensorTopic = 'squid';
+
+    component.onSaveGrokStatement('grok statement');
+    expect(component.grokStatement).toEqual('grok statement');
+    expect(component.sensorParserConfig.parserConfig['grokPath']).toEqual('/apps/metron/patterns/squid');
+
+    component.sensorParserConfig.parserConfig['grokPath'] = '/patterns/squid';
+    component.onSaveGrokStatement('grok statement');
+    expect(component.sensorParserConfig.parserConfig['grokPath']).toEqual('/apps/metron/patterns/squid');
+
+    component.sensorParserConfig.parserConfig['grokPath'] = '/custom/grok/path';
+    component.onSaveGrokStatement('grok statement');
+    expect(component.sensorParserConfig.parserConfig['grokPath']).toEqual('/custom/grok/path');
+
+    fixture.destroy();
+  }));
+
+  it('should onSavePatternLabel', async(() => {
+    component.onSavePatternLabel('PATTERN_LABEL');
+    expect(component.patternLabel).toEqual('PATTERN_LABEL');
+    expect(component.sensorParserConfig.parserConfig['patternLabel']).toEqual('PATTERN_LABEL');
+
+    fixture.destroy();
+  }));
+
+  it('should goBack', async(() => {
+    activatedRoute.setNameForTest('new');
+
+    router.navigateByUrl = jasmine.createSpy('navigateByUrl');
+    component.goBack();
+    expect(router.navigateByUrl).toHaveBeenCalledWith('/sensors');
+
+    fixture.destroy();
+  }));
+
+  it('should save sensor configuration', async(() => {
+    let fieldTransformer = Object.assign(new FieldTransformer(), {
+      'input': [],
+      'output': ['url_host'],
+      'transformation': 'MTL',
+      'config': {'url_host': 'TO_LOWER(URL_TO_HOST(url))'}
+    });
+    let sensorParserConfigSave: SensorParserConfig = new SensorParserConfig();
+    sensorParserConfigSave.sensorTopic = 'squid';
+    sensorParserConfigSave.parserClassName = 'org.apache.metron.parsers.GrokParser';
+    sensorParserConfigSave.parserConfig['grokPath'] = '/apps/metron/patterns/squid';
+    sensorParserConfigSave.fieldTransformations = [fieldTransformer];
+    activatedRoute.setNameForTest('new');
+    sensorParserConfigService.setThrowError(true);
+
+    spyOn(metronAlerts, 'showSuccessMessage');
+    spyOn(metronAlerts, 'showErrorMessage');
+
+    component.sensorParserConfig.sensorTopic = 'squid';
+    component.sensorParserConfig.parserClassName = 'org.apache.metron.parsers.GrokParser';
+    component.sensorParserConfig.parserConfig['grokPath'] = '/apps/metron/patterns/squid';
+    component.sensorParserConfig.fieldTransformations = [fieldTransformer];
+
+    component.onSave();
+    expect(metronAlerts.showErrorMessage['calls'].mostRecent().args[0])
+        .toEqual('Unable to save sensor config: SensorParserConfig post error');
+
+    component.sensorEnrichmentConfig = Object.assign(new SensorEnrichmentConfig(), squidSensorEnrichmentConfig);
+    component.indexingConfigurations = Object.assign(new IndexingConfigurations(), squidIndexingConfigurations);
+    sensorParserConfigService.setThrowError(false);
+    hdfsService.setContents('/apps/metron/patterns/squid', 'SQUID grok statement');
+    component.grokStatement = 'SQUID grok statement';
+
+    component.onSave();
+    expect(sensorParserConfigService.getPostedSensorParserConfig()).toEqual(sensorParserConfigSave);
+    expect(sensorEnrichmentConfigService.getPostedSensorEnrichmentConfig())
+        .toEqual(Object.assign(new SensorEnrichmentConfig(), squidSensorEnrichmentConfig));
+    expect(sensorIndexingConfigService.getPostedIndexingConfigurations())
+        .toEqual(Object.assign(new IndexingConfigurations(), squidIndexingConfigurations));
+    expect(hdfsService.getPostedContents()).toEqual('SQUID grok statement');
+
+    hdfsService.setContents('/apps/metron/patterns/squid', null);
+
+    component.onSave();
+    expect(metronAlerts.showErrorMessage['calls'].mostRecent().args[0]).toEqual('HDFS post Error');
+
+    sensorEnrichmentConfigService.setThrowError(true);
+
+    component.onSave();
+    expect(metronAlerts.showErrorMessage['calls'].mostRecent().args[0])
+        .toEqual('Created Sensor parser config but unable to save enrichment configuration: SensorEnrichmentConfig post error');
+
+    sensorIndexingConfigService.setThrowError(true);
+
+    component.onSave();
+    expect(metronAlerts.showErrorMessage['calls'].mostRecent().args[0])
+        .toEqual('Created Sensor parser config but unable to save indexing configuration: IndexingConfigurations post error');
+
+    fixture.destroy();
+  }));
+
+  it('should getTransformationCount', async(() => {
+    let transforms =
+    [
+      Object.assign(new FieldTransformer(), {
+        'input': [
+          'method'
+        ],
+        'output': null,
+        'transformation': 'REMOVE',
+        'config': {
+          'condition': 'exists(method) and method == "foo"'
+        }
+      }),
+      Object.assign(new FieldTransformer(), {
+        'input': [],
+        'output': [
+          'method',
+          'status_code',
+          'url'
+        ],
+        'transformation': 'STELLAR',
+        'config': {
+          'method': 'TO_UPPER(method)',
+          'status_code': 'TO_LOWER(code)',
+          'url': 'TO_STRING(TRIM(url))'
+        }
+      })
+    ];
+
+
+
+    expect(component.getTransformationCount()).toEqual(0);
+
+    fixture.componentInstance.sensorParserConfig.fieldTransformations = transforms;
+    expect(component.getTransformationCount()).toEqual(3);
+
+    fixture.componentInstance.sensorParserConfig.fieldTransformations = [transforms[0]];
+    expect(component.getTransformationCount()).toEqual(0);
+    fixture.destroy();
+  }));
+
+  it('should getEnrichmentCount', async(() => {
+
+    component.sensorEnrichmentConfig.enrichment.fieldMap['geo'] = ['ip_src_addr', 'ip_dst_addr'];
+    component.sensorEnrichmentConfig.enrichment.fieldToTypeMap['hbaseenrichment'] = ['ip_src_addr', 'ip_dst_addr'];
+
+    expect(component.getEnrichmentCount()).toEqual(4);
+
+    fixture.destroy();
+  }));
+
+  it('should getThreatIntelCount', async(() => {
+
+    component.sensorEnrichmentConfig.threatIntel.fieldToTypeMap['hbaseenrichment'] = ['ip_src_addr', 'ip_dst_addr'];
+
+    expect(component.getThreatIntelCount()).toEqual(2);
+
+    fixture.destroy();
+  }));
+
+  it('should getRuleCount', async(() => {
+    let rule1 = Object.assign(new RiskLevelRule(), {'name': 'rule1', 'rule': 'some rule', 'score': 50});
+    let rule2 = Object.assign(new RiskLevelRule(), {'name': 'rule2', 'rule': 'some other rule', 'score': 80});
+    component.sensorEnrichmentConfig.threatIntel.triageConfig.riskLevelRules.push(rule1);
+    component.sensorEnrichmentConfig.threatIntel.triageConfig.riskLevelRules.push(rule2);
+
+    expect(component.getRuleCount()).toEqual(2);
+
+    fixture.destroy();
+  }));
+
+  it('should showPane', async(() => {
+
+    component.showPane(Pane.GROK);
+    expect(component.showGrokValidator).toEqual(true);
+    expect(component.showFieldSchema).toEqual(false);
+    expect(component.showRawJson).toEqual(false);
+
+    component.showPane(Pane.FIELDSCHEMA);
+    expect(component.showGrokValidator).toEqual(false);
+    expect(component.showFieldSchema).toEqual(true);
+    expect(component.showRawJson).toEqual(false);
+
+    component.showPane(Pane.RAWJSON);
+    expect(component.showGrokValidator).toEqual(false);
+    expect(component.showFieldSchema).toEqual(false);
+    expect(component.showRawJson).toEqual(true);
+
+    fixture.destroy();
+  }));
+
+  it('should hidePane', async(() => {
+
+    component.hidePane(Pane.GROK);
+    expect(component.showGrokValidator).toEqual(false);
+    expect(component.showFieldSchema).toEqual(false);
+    expect(component.showRawJson).toEqual(false);
+
+    component.hidePane(Pane.FIELDSCHEMA);
+    expect(component.showGrokValidator).toEqual(false);
+    expect(component.showFieldSchema).toEqual(false);
+    expect(component.showRawJson).toEqual(false);
+
+    component.hidePane(Pane.RAWJSON);
+    expect(component.showGrokValidator).toEqual(false);
+    expect(component.showFieldSchema).toEqual(false);
+    expect(component.showRawJson).toEqual(false);
+
+    fixture.destroy();
+  }));
+
+  it('should handle onShowGrokPane', async(() => {
+    spyOn(component, 'showPane');
+    component.sensorParserConfig.sensorTopic = 'squid';
+
+    component.onShowGrokPane();
+    expect(component.patternLabel).toEqual('SQUID');
+    expect(component.showPane).toHaveBeenCalledWith(component.pane.GROK);
+
+    component.patternLabel = 'PATTERN_LABEL';
+
+    component.onShowGrokPane();
+    expect(component.patternLabel).toEqual('PATTERN_LABEL');
+
+    fixture.destroy();
+  }));
+
+  it('should handle onRawJsonChanged', async(() => {
+    spyOn(component.sensorFieldSchema, 'createFieldSchemaRows');
+
+    component.onRawJsonChanged();
+
+    expect(component.sensorFieldSchema.createFieldSchemaRows).toHaveBeenCalled();
+
+    fixture.destroy();
+  }));
+
+  it('should handle onAdvancedConfigFormClose', async(() => {
+    component.onAdvancedConfigFormClose();
+
+    expect(component.showAdvancedParserConfiguration).toEqual(false);
+
+    fixture.destroy();
+  }));
+
+});

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/sensors/sensor-parser-config/sensor-parser-config.component.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/sensors/sensor-parser-config/sensor-parser-config.component.ts b/metron-interface/metron-config/src/app/sensors/sensor-parser-config/sensor-parser-config.component.ts
new file mode 100644
index 0000000..a4192f1
--- /dev/null
+++ b/metron-interface/metron-config/src/app/sensors/sensor-parser-config/sensor-parser-config.component.ts
@@ -0,0 +1,421 @@
+/**
+ * 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 {Component, OnInit, ViewChild} from '@angular/core';
+import {FormGroup, Validators, FormControl} from '@angular/forms';
+import {SensorParserConfig} from '../../model/sensor-parser-config';
+import {SensorParserConfigService} from '../../service/sensor-parser-config.service';
+import {Router, ActivatedRoute} from '@angular/router';
+import {MetronAlerts} from '../../shared/metron-alerts';
+import {SensorParserContext} from '../../model/sensor-parser-context';
+import {SensorEnrichmentConfigService} from '../../service/sensor-enrichment-config.service';
+import {SensorEnrichmentConfig} from '../../model/sensor-enrichment-config';
+import {SensorFieldSchemaComponent} from '../sensor-field-schema/sensor-field-schema.component';
+import {SensorRawJsonComponent} from '../sensor-raw-json/sensor-raw-json.component';
+import {KafkaService} from '../../service/kafka.service';
+import {SensorIndexingConfigService} from '../../service/sensor-indexing-config.service';
+import {IndexingConfigurations} from '../../model/sensor-indexing-config';
+import {RestError} from '../../model/rest-error';
+import {HdfsService} from '../../service/hdfs.service';
+import {GrokValidationService} from '../../service/grok-validation.service';
+
+export enum Pane {
+  GROK, RAWJSON, FIELDSCHEMA, THREATTRIAGE
+}
+
+export enum KafkaStatus {
+  NO_TOPIC, NOT_EMITTING, EMITTING
+}
+
+@Component({
+  selector: 'metron-config-sensor',
+  templateUrl: 'sensor-parser-config.component.html',
+  styleUrls: ['sensor-parser-config.component.scss']
+})
+
+export class SensorParserConfigComponent implements OnInit {
+
+  sensorConfigForm: FormGroup;
+  transformsValidationForm: FormGroup;
+
+  sensorParserConfig: SensorParserConfig = new SensorParserConfig();
+  sensorEnrichmentConfig: SensorEnrichmentConfig = new SensorEnrichmentConfig();
+  indexingConfigurations: IndexingConfigurations = new IndexingConfigurations();
+
+  showGrokValidator: boolean = false;
+  showTransformsValidator: boolean = false;
+  showAdvancedParserConfiguration: boolean = false;
+  showRawJson: boolean = false;
+  showFieldSchema: boolean = false;
+  showThreatTriage: boolean = false;
+
+  configValid = false;
+  sensorNameValid = false;
+  parserClassValid = false;
+  grokStatementValid = false;
+  availableParsers = {};
+  availableParserNames = [];
+  grokStatement = '';
+  patternLabel = '';
+
+  editMode: boolean = false;
+
+  topicExists: boolean = false;
+
+  transformsValidationResult: {map: any, keys: string[]} = {map: {}, keys: []};
+  transformsValidation: SensorParserContext = new SensorParserContext();
+
+  pane = Pane;
+  openPane: Pane = null;
+
+  kafkaStatus = KafkaStatus;
+  currentKafkaStatus = null;
+
+  @ViewChild(SensorFieldSchemaComponent) sensorFieldSchema: SensorFieldSchemaComponent;
+  @ViewChild(SensorRawJsonComponent) sensorRawJson: SensorRawJsonComponent;
+
+  constructor(private sensorParserConfigService: SensorParserConfigService, private metronAlerts: MetronAlerts,
+              private sensorEnrichmentConfigService: SensorEnrichmentConfigService, private route: ActivatedRoute,
+              private sensorIndexingConfigService: SensorIndexingConfigService, private grokValidationService: GrokValidationService,
+              private router: Router, private kafkaService: KafkaService, private hdfsService: HdfsService) {
+    this.sensorParserConfig.parserConfig = {};
+  }
+
+
+  init(id: string): void {
+    if (id !== 'new') {
+      this.editMode = true;
+      this.sensorParserConfigService.get(id).subscribe((results: SensorParserConfig) => {
+        this.sensorParserConfig = results;
+        this.sensorNameValid = true;
+        this.getKafkaStatus();
+        if (Object.keys(this.sensorParserConfig.parserConfig).length > 0) {
+          this.showAdvancedParserConfiguration = true;
+        }
+        if (this.isGrokParser(this.sensorParserConfig)) {
+          let path = this.sensorParserConfig.parserConfig['grokPath'];
+          if (path) {
+            this.hdfsService.read(path).subscribe(contents => {
+              this.grokStatement = contents;
+            }, (hdfsError: RestError) => {
+              this.grokValidationService.getStatement(path).subscribe(contents => {
+                this.grokStatement = contents;
+              }, (grokError: RestError) => {
+                this.metronAlerts.showErrorMessage('Could not find grok statement in HDFS or classpath at ' + path);
+              });
+            });
+          }
+          let patternLabel = this.sensorParserConfig.parserConfig['patternLabel'];
+          if (patternLabel) {
+            this.patternLabel = patternLabel;
+          }
+      }});
+
+      this.sensorEnrichmentConfigService.get(id).subscribe((result: SensorEnrichmentConfig) => {
+        this.sensorEnrichmentConfig = result;
+      }, (error: RestError) => {
+        if (error.responseCode !== 404) {
+          this.metronAlerts.showErrorMessage(error.message);
+        }
+      });
+
+      this.sensorIndexingConfigService.get(id).subscribe((result: IndexingConfigurations) => {
+            this.indexingConfigurations = result;
+      }, (error: RestError) => {
+        if (error.responseCode !== 404) {
+          this.metronAlerts.showErrorMessage(error.message);
+        }
+      });
+    } else {
+      this.sensorParserConfig = new SensorParserConfig();
+      this.sensorParserConfig.parserClassName = 'org.apache.metron.parsers.GrokParser';
+    }
+  }
+
+  ngOnInit() {
+    this.route.params.subscribe(params => {
+      let id = params['id'];
+      this.init(id);
+    });
+    this.createForms();
+    this.getAvailableParsers();
+  }
+
+  createSensorConfigForm(): FormGroup {
+    let group: any = {};
+
+    group['sensorTopic'] = new FormControl(this.sensorParserConfig.sensorTopic, Validators.required);
+    group['parserClassName'] = new FormControl(this.sensorParserConfig.parserClassName, Validators.required);
+    group['grokStatement'] = new FormControl(this.grokStatement);
+    group['transforms'] = new FormControl(this.sensorParserConfig['transforms']);
+    group['stellar'] = new FormControl(this.sensorParserConfig);
+    group['threatTriage'] = new FormControl(this.sensorEnrichmentConfig);
+    group['hdfsIndex'] = new FormControl(this.indexingConfigurations.hdfs.index, Validators.required);
+    group['hdfsBatchSize'] = new FormControl(this.indexingConfigurations.hdfs.batchSize, Validators.required);
+    group['hdfsEnabled'] = new FormControl(this.indexingConfigurations.hdfs.enabled, Validators.required);
+    group['elasticsearchIndex'] = new FormControl(this.indexingConfigurations.elasticsearch.index, Validators.required);
+    group['elasticsearchBatchSize'] = new FormControl(this.indexingConfigurations.elasticsearch.batchSize, Validators.required);
+    group['elasticsearchEnabled'] = new FormControl(this.indexingConfigurations.elasticsearch.enabled, Validators.required);
+    group['solrIndex'] = new FormControl(this.indexingConfigurations.solr.index, Validators.required);
+    group['solrBatchSize'] = new FormControl(this.indexingConfigurations.solr.batchSize, Validators.required);
+    group['solrEnabled'] = new FormControl(this.indexingConfigurations.solr.enabled, Validators.required);
+
+    return new FormGroup(group);
+  }
+
+  createTransformsValidationForm(): FormGroup {
+    let group: any = {};
+
+    group['sampleData'] = new FormControl(this.transformsValidation.sampleData, Validators.required);
+    group['sensorParserConfig'] = new FormControl(this.transformsValidation.sensorParserConfig, Validators.required);
+
+    return new FormGroup(group);
+  }
+
+  createForms() {
+    this.sensorConfigForm = this.createSensorConfigForm();
+    this.transformsValidationForm = this.createTransformsValidationForm();
+    if (Object.keys(this.sensorParserConfig.parserConfig).length > 0) {
+      this.showAdvancedParserConfiguration = true;
+    }
+  }
+
+  getAvailableParsers() {
+    this.sensorParserConfigService.getAvailableParsers().subscribe(
+      availableParsers => {
+        this.availableParsers = availableParsers;
+        this.availableParserNames = Object.keys(availableParsers);
+      }
+    );
+  }
+
+  getMessagePrefix(): string {
+    return this.editMode ? 'Modified' : 'Created';
+  }
+
+  onSetSensorName(): void {
+    this.sensorNameValid = this.sensorParserConfig.sensorTopic !== undefined &&
+        this.sensorParserConfig.sensorTopic.length > 0;
+    if (this.sensorNameValid) {
+      this.getKafkaStatus();
+    }
+    this.isConfigValid();
+  }
+
+  onParserTypeChange(): void {
+    this.parserClassValid = this.sensorParserConfig.parserClassName !== undefined &&
+        this.sensorParserConfig.parserClassName.length > 0;
+    if (this.parserClassValid) {
+      if (this.isGrokParser(this.sensorParserConfig)) {
+      } else {
+        this.hidePane(Pane.GROK);
+      }
+    }
+    this.isConfigValid();
+  }
+
+  onGrokStatementChange(): void {
+    this.grokStatementValid = this.grokStatement !== undefined &&
+        this.grokStatement.length > 0;
+    this.isConfigValid();
+  }
+
+  isConfigValid() {
+    let isGrokParser = this.isGrokParser(this.sensorParserConfig);
+    this.configValid = this.sensorNameValid && this.parserClassValid && (!isGrokParser || this.grokStatementValid);
+  }
+
+  getKafkaStatus() {
+    if (!this.sensorParserConfig.sensorTopic || this.sensorParserConfig.sensorTopic.length === 0) {
+      this.currentKafkaStatus = null;
+      return;
+    }
+
+    this.kafkaService.get(this.sensorParserConfig.sensorTopic).subscribe(kafkaTopic => {
+      this.kafkaService.sample(this.sensorParserConfig.sensorTopic).subscribe((sampleData: string) => {
+        this.currentKafkaStatus = (sampleData && sampleData.length > 0) ? KafkaStatus.EMITTING : KafkaStatus.NOT_EMITTING;
+      },
+      error => {
+        this.currentKafkaStatus = KafkaStatus.NOT_EMITTING;
+      });
+    },
+    error => {
+      this.currentKafkaStatus = KafkaStatus.NO_TOPIC;
+    });
+
+  }
+
+  getTransforms(): string {
+    let count = 0;
+    if (this.sensorParserConfig.fieldTransformations) {
+      for (let tranforms of this.sensorParserConfig.fieldTransformations) {
+        if (tranforms.output) {
+          count += tranforms.output.length;
+        }
+      }
+    }
+
+    return count + ' Transformations Applied';
+  }
+
+  goBack() {
+    this.router.navigateByUrl('/sensors');
+    return false;
+  }
+
+  onSaveGrokStatement(grokStatement: string) {
+    this.grokStatement = grokStatement;
+    let grokPath = this.sensorParserConfig.parserConfig['grokPath'];
+    if (!grokPath || grokPath.indexOf('/patterns') === 0) {
+      this.sensorParserConfig.parserConfig['grokPath'] = '/apps/metron/patterns/' + this.sensorParserConfig.sensorTopic;
+    }
+  }
+
+  onSavePatternLabel(patternLabel: string) {
+    this.patternLabel = patternLabel;
+    this.sensorParserConfig.parserConfig['patternLabel'] = patternLabel;
+  }
+
+  onSave() {
+    let sensorParserConfigSave: SensorParserConfig = new SensorParserConfig();
+    sensorParserConfigSave.parserConfig = {};
+    sensorParserConfigSave.sensorTopic = this.sensorParserConfig.sensorTopic;
+    sensorParserConfigSave.parserClassName = this.sensorParserConfig.parserClassName;
+    sensorParserConfigSave.parserConfig = this.sensorParserConfig.parserConfig;
+    sensorParserConfigSave.fieldTransformations = this.sensorParserConfig.fieldTransformations;
+
+    if (!this.indexingConfigurations.hdfs.index) {
+      this.indexingConfigurations.hdfs.index = this.sensorParserConfig.sensorTopic;
+    }
+    if (!this.indexingConfigurations.elasticsearch.index) {
+      this.indexingConfigurations.elasticsearch.index = this.sensorParserConfig.sensorTopic;
+    }
+    if (!this.indexingConfigurations.solr.index) {
+      this.indexingConfigurations.solr.index = this.sensorParserConfig.sensorTopic;
+    }
+    this.sensorParserConfigService.post(sensorParserConfigSave).subscribe(
+      sensorParserConfig => {
+        if (this.isGrokParser(sensorParserConfig)) {
+            this.hdfsService.post(this.sensorParserConfig.parserConfig['grokPath'], this.grokStatement).subscribe(
+                response => {}, (error: RestError) => this.metronAlerts.showErrorMessage(error.message));
+        }
+        this.sensorEnrichmentConfigService.post(sensorParserConfig.sensorTopic, this.sensorEnrichmentConfig).subscribe(
+            (sensorEnrichmentConfig: SensorEnrichmentConfig) => {
+        }, (error: RestError) => {
+              let msg = ' Sensor parser config but unable to save enrichment configuration: ';
+              this.metronAlerts.showErrorMessage(this.getMessagePrefix() + msg + error.message);
+        });
+        this.sensorIndexingConfigService.post(sensorParserConfig.sensorTopic, this.indexingConfigurations).subscribe(
+            (indexingConfigurations: IndexingConfigurations) => {
+        }, (error: RestError) => {
+              let msg = ' Sensor parser config but unable to save indexing configuration: ';
+              this.metronAlerts.showErrorMessage(this.getMessagePrefix() + msg + error.message);
+        });
+        this.metronAlerts.showSuccessMessage(this.getMessagePrefix() + ' Sensor ' + sensorParserConfig.sensorTopic);
+        this.sensorParserConfigService.dataChangedSource.next([sensorParserConfigSave]);
+        this.goBack();
+      }, (error: RestError) => {
+        this.metronAlerts.showErrorMessage('Unable to save sensor config: ' + error.message);
+      });
+  }
+
+  isGrokParser(sensorParserConfig: SensorParserConfig): boolean {
+    if (sensorParserConfig && sensorParserConfig.parserClassName) {
+      return sensorParserConfig.parserClassName === 'org.apache.metron.parsers.GrokParser';
+    }
+    return false;
+  }
+
+  getTransformationCount(): number {
+    let stellarTransformations = this.sensorParserConfig.fieldTransformations.filter(fieldTransformer =>
+      fieldTransformer.transformation === 'STELLAR');
+    if (stellarTransformations.length > 0 && stellarTransformations[0].config) {
+      return Object.keys(stellarTransformations[0].config).length;
+    } else {
+      return 0;
+    }
+  }
+
+  getEnrichmentCount(): number {
+    let count = 0;
+    if (this.sensorEnrichmentConfig.enrichment.fieldMap) {
+      for (let enrichment in this.sensorEnrichmentConfig.enrichment.fieldMap) {
+        if (enrichment !== 'hbaseEnrichment' && enrichment !== 'stellar') {
+          count += this.sensorEnrichmentConfig.enrichment.fieldMap[enrichment].length;
+        }
+      }
+    }
+    if (this.sensorEnrichmentConfig.enrichment.fieldToTypeMap) {
+      for (let fieldName in this.sensorEnrichmentConfig.enrichment.fieldToTypeMap) {
+        if (this.sensorEnrichmentConfig.enrichment.fieldToTypeMap.hasOwnProperty(fieldName)) {
+          count += this.sensorEnrichmentConfig.enrichment.fieldToTypeMap[fieldName].length;
+        }
+      }
+    }
+    return count;
+  }
+
+  getThreatIntelCount(): number {
+    let count = 0;
+    if (this.sensorEnrichmentConfig.threatIntel.fieldToTypeMap) {
+      for (let fieldName in this.sensorEnrichmentConfig.threatIntel.fieldToTypeMap) {
+        if (this.sensorEnrichmentConfig.threatIntel.fieldToTypeMap.hasOwnProperty(fieldName)) {
+          count += this.sensorEnrichmentConfig.threatIntel.fieldToTypeMap[fieldName].length;
+        }
+      }
+    }
+    return count;
+  }
+
+  getRuleCount(): number {
+    let count = 0;
+    if (this.sensorEnrichmentConfig.threatIntel.triageConfig.riskLevelRules) {
+      count = Object.keys(this.sensorEnrichmentConfig.threatIntel.triageConfig.riskLevelRules).length;
+    }
+    return count;
+  }
+
+  onShowGrokPane() {
+    if (!this.patternLabel) {
+      this.patternLabel = this.sensorParserConfig.sensorTopic.toUpperCase();
+    }
+    this.showPane(this.pane.GROK);
+  }
+
+  showPane(pane: Pane) {
+    this.setPaneVisibility(pane, true);
+  }
+
+  hidePane(pane: Pane) {
+    this.setPaneVisibility(pane, false);
+  }
+
+  setPaneVisibility(pane: Pane, visibilty: boolean) {
+    this.showGrokValidator = (pane === Pane.GROK) ? visibilty : false;
+    this.showFieldSchema = (pane === Pane.FIELDSCHEMA) ? visibilty : false;
+    this.showRawJson = (pane ===  Pane.RAWJSON) ? visibilty : false;
+    this.showThreatTriage = (pane ===  Pane.THREATTRIAGE) ? visibilty : false;
+  }
+
+  onRawJsonChanged(): void {
+    this.sensorFieldSchema.createFieldSchemaRows();
+  }
+
+  onAdvancedConfigFormClose(): void {
+    this.showAdvancedParserConfiguration = false;
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/sensors/sensor-parser-config/sensor-parser-config.module.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/sensors/sensor-parser-config/sensor-parser-config.module.ts b/metron-interface/metron-config/src/app/sensors/sensor-parser-config/sensor-parser-config.module.ts
new file mode 100644
index 0000000..68e22f8
--- /dev/null
+++ b/metron-interface/metron-config/src/app/sensors/sensor-parser-config/sensor-parser-config.module.ts
@@ -0,0 +1,35 @@
+/**
+ * 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 {ReactiveFormsModule} from '@angular/forms';
+import {routing} from './sensor-parser-config.routing';
+import {SensorParserConfigComponent} from './sensor-parser-config.component';
+import {SharedModule} from '../../shared/shared.module';
+import {NumberSpinnerModule} from '../../shared/number-spinner/number-spinner.module';
+import {AdvancedConfigFormModule} from '../../shared/advanced-config-form/advanced-config-form.module';
+import {SensorGrokModule} from '../sensor-grok/sensor-grok.module';
+import {SensorFieldSchemaModule} from '../sensor-field-schema/sensor-field-schema.module';
+import {SensorRawJsonModule} from '../sensor-raw-json/sensor-raw-json.module';
+import {SensorThreatTriageModule} from '../sensor-threat-triage/sensor-threat-triage.module';
+
+@NgModule ({
+  imports: [ routing, ReactiveFormsModule, SharedModule, NumberSpinnerModule, AdvancedConfigFormModule,
+                SensorGrokModule, SensorFieldSchemaModule, SensorRawJsonModule, SensorThreatTriageModule ],
+  declarations: [ SensorParserConfigComponent ]
+})
+export class SensorParserConfigModule { }

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/sensors/sensor-parser-config/sensor-parser-config.routing.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/sensors/sensor-parser-config/sensor-parser-config.routing.ts b/metron-interface/metron-config/src/app/sensors/sensor-parser-config/sensor-parser-config.routing.ts
new file mode 100644
index 0000000..e54b7b5
--- /dev/null
+++ b/metron-interface/metron-config/src/app/sensors/sensor-parser-config/sensor-parser-config.routing.ts
@@ -0,0 +1,27 @@
+/**
+ * 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 { ModuleWithProviders }  from '@angular/core';
+import { Routes, RouterModule } from '@angular/router';
+import {SensorParserConfigComponent} from './sensor-parser-config.component';
+import {AuthGuard} from '../../shared/auth-guard';
+
+const routes: Routes = [
+  { path: 'sensors-config/:id', component: SensorParserConfigComponent, canActivate: [AuthGuard], outlet: 'dialog'}
+];
+
+export const routing: ModuleWithProviders = RouterModule.forChild(routes);

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/sensors/sensor-parser-list/index.ts
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/sensors/sensor-parser-list/index.ts b/metron-interface/metron-config/src/app/sensors/sensor-parser-list/index.ts
new file mode 100644
index 0000000..7a80f38
--- /dev/null
+++ b/metron-interface/metron-config/src/app/sensors/sensor-parser-list/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-parser-list.component';

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/sensors/sensor-parser-list/sensor-parser-list.component.html
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/sensors/sensor-parser-list/sensor-parser-list.component.html b/metron-interface/metron-config/src/app/sensors/sensor-parser-list/sensor-parser-list.component.html
new file mode 100644
index 0000000..bd0b1fd
--- /dev/null
+++ b/metron-interface/metron-config/src/app/sensors/sensor-parser-list/sensor-parser-list.component.html
@@ -0,0 +1,85 @@
+<!--
+  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="sensors details-pane-padding">
+
+  <div class="container-fluid">
+    <div class="row">
+      <div class="col-lg-10 px-0">
+        <div class="metron-title"> {{componentName}} ({{count}}) </div>
+      </div>
+      <div class="col-lg-2">
+        <div class="dropdown pull-right">
+          <button class="btn btn-primary dropdown-toggle" type="button" id="dropdownMenu1" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
+            ACTIONS
+          </button>
+          <div class="dropdown-menu dropdown-menu-right metron-bg-inverse" aria-labelledby="dropdownMenu1">
+            <span class="dropdown-item" data-action="Delete" (click)="onDeleteSensor()" [class.disabled]="selectedSensors.length == 0">Delete</span>
+            <span class="dropdown-item" data-action="Enable" (click)="onEnableSensors()" [class.disabled]="selectedSensors.length == 0">Enable</span>
+            <span class="dropdown-item" data-action="Disable" (click)="onDisableSensors()" [class.disabled]="selectedSensors.length == 0">Disable</span>
+            <span class="dropdown-item" data-action="Start" (click)="onStartSensors()" [class.disabled]="selectedSensors.length == 0">Start</span>
+            <span class="dropdown-item" data-action="Stop" (click)="onStopSensors()" [class.disabled]="selectedSensors.length == 0">Stop</span>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+
+  <table class="table card-deck" metron-config-table #table (onSort)="onSort($event)">
+    <thead>
+    <tr>
+      <th> <metron-config-sorter [sortBy]="'sensorTopic'"> Name </metron-config-sorter> </th>
+      <th> <metron-config-sorter [sortBy]="'parserClassName'"> Parser </metron-config-sorter> </th>
+      <th> <metron-config-sorter [sortBy]="'status'"> Status </metron-config-sorter> </th>
+      <th> <metron-config-sorter [sortBy]="'latency'"> Latency </metron-config-sorter> </th>
+      <th> <metron-config-sorter [sortBy]="'throughput'"> Throughput </metron-config-sorter> </th>
+      <th> <metron-config-sorter [sortBy]="'modifiedByDate'"> Last Updated </metron-config-sorter> </th>
+      <th> <metron-config-sorter [sortBy]="'modifiedBy'"> Last Editor </metron-config-sorter> </th>
+      <th style="width:100px"></th>
+      <th style="width:50px"><input id="select-deselect-all" class="fontawesome-checkbox" type="checkbox" (click)="onSelectDeselectAll($event)"><label for="select-deselect-all"></label></th>
+    </tr>
+    </thead>
+    <tbody>
+    <tr *ngFor="let sensor of sensors;" (click)="onSensorRowSelect(sensor.config, $event)" [ngClass]="{'active': (selectedSensors.indexOf(sensor) != -1 || sensorParserConfigService.getSelectedSensor() == sensor.config)}">
+      <td>{{ sensor.config.sensorTopic }}</td>
+      <td>{{ getParserType(sensor.config) }}</td>
+      <td [ngClass]="{'warning-text': (sensor.status == 'Stopped' || sensor.status == 'Disabled')}">{{ sensor.status }}</td>
+      <td>{{ sensor.latency }}</td>
+      <td>{{ sensor.throughput }}</td>
+      <td>{{ sensor.modifiedByDate }}</td>
+      <td>{{ sensor.modifiedBy }}</td>
+      <td class="icon-container">
+          <i  data-toggle="tooltip" title="Operation in progress" class="fa fa-circle-o-notch fa-spin fa-lg fa-fw" [hidden]="!sensor.config['startStopInProgress']"></i>
+
+          <i  data-toggle="tooltip" title="Stop parser topology" class="fa fa-stop fa-lg" aria-hidden="true" [hidden]="((sensor.status != 'Running' && sensor.status != 'Disabled') || sensor.config['startStopInProgress'])" (click)="onStopSensor(sensor, $event)"></i>
+          <i  data-toggle="tooltip" title="Disable parser topology" class="fa fa-ban fa-lg" aria-hidden="true" [hidden]="(sensor.status != 'Running'  || sensor.config['startStopInProgress'])" (click)="onDisableSensor(sensor, $event)"></i>
+
+          <i  data-toggle="tooltip" title="Start parser topology" class="fa fa-play fa-lg" aria-hidden="true" [hidden]="(sensor.status != 'Stopped' || sensor.config['startStopInProgress'])" (click)="onStartSensor(sensor, $event)"></i>
+          <i  data-toggle="tooltip" title="Enable parser topology" class="fa fa-check-circle-o fa-lg" aria-hidden="true" [hidden]="(sensor.status != 'Disabled' || sensor.config['startStopInProgress'])" (click)="onEnableSensor(sensor, $event)"></i>
+
+          <i  data-toggle="tooltip" title="Edit parser topology" class="fa fa-pencil fa-lg" aria-hidden="true" (click)="navigateToSensorEdit(sensor.config, $event)"></i>
+
+          <i  data-toggle="tooltip" title="Delete parser configuration" class="fa fa-trash-o fa-lg" aria-hidden="true" (click)="deleteSensor($event, [sensor.config])"></i>
+      </td>
+      <td><input id="{{ sensor.config.sensorTopic }}" class="fontawesome-checkbox" type="checkbox" name="{{sensor.config.sensorTopic}}" (click)="onRowSelected(sensor, $event)"><label attr.for="{{ sensor.config.sensorTopic }}"></label></td>
+    </tr>
+    </tbody>
+  </table>
+
+    <div class="metron-add-button hexa-button" (click)="addAddSensor()">
+      <i class="fa fa-plus"></i>
+    </div>
+
+</div>

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/1ef8cd8f/metron-interface/metron-config/src/app/sensors/sensor-parser-list/sensor-parser-list.component.scss
----------------------------------------------------------------------
diff --git a/metron-interface/metron-config/src/app/sensors/sensor-parser-list/sensor-parser-list.component.scss b/metron-interface/metron-config/src/app/sensors/sensor-parser-list/sensor-parser-list.component.scss
new file mode 100644
index 0000000..5d5f1e3
--- /dev/null
+++ b/metron-interface/metron-config/src/app/sensors/sensor-parser-list/sensor-parser-list.component.scss
@@ -0,0 +1,17 @@
+/**
+ * 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.
+ */


Mime
View raw message