Skip to content

Updated field mapping UX; disabled windows run for cypress #307

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jan 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 2 additions & 15 deletions .github/workflows/cypress-workflow.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,8 @@ jobs:
name: Run Cypress E2E tests
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
os: [ubuntu-latest]
include:
- os: windows-latest
cypress_cache_folder: ~/AppData/Local/Cypress/Cache
- os: ubuntu-latest
cypress_cache_folder: ~/.cache/Cypress
runs-on: ${{ matrix.os }}
Expand All @@ -34,10 +32,6 @@ jobs:
# TODO: Parse this from security analytics plugin (https://github.com/opensearch-project/security-analytics/issues/170)
java-version: 11

- name: Enable longer filenames
if: ${{ matrix.os == 'windows-latest' }}
run: git config --system core.longpaths true

- name: Checkout security analytics
uses: actions/checkout@v2
with:
Expand Down Expand Up @@ -93,14 +87,7 @@ jobs:
yarn start --no-base-path --no-watch &
shell: bash

# Window is slow so wait longer
- name: Sleep until OSD server starts - windows
if: ${{ matrix.os == 'windows-latest' }}
run: Start-Sleep -s 400
shell: powershell

- name: Sleep until OSD server starts - non-windows
if: ${{ matrix.os != 'windows-latest' }}
- name: Sleep until OSD server starts
run: sleep 300
shell: bash

Expand Down
2 changes: 1 addition & 1 deletion cypress/integration/1_detectors.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ describe('Detectors', () => {
cy.get('button').contains('Next').click({ force: true });

// Check that correct page now showing
cy.contains('Required field mappings');
cy.contains('Configure field mapping');

// Select appropriate names to map fields to
for (let field_name in sample_field_mappings.properties) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,20 @@

import React, { Component } from 'react';
import { RouteComponentProps } from 'react-router-dom';
import { EuiSpacer, EuiTitle, EuiText } from '@elastic/eui';
import {
EuiSpacer,
EuiTitle,
EuiText,
EuiCallOut,
EuiAccordion,
EuiHorizontalRule,
EuiPanel,
} from '@elastic/eui';
import FieldMappingsTable from '../components/RequiredFieldMapping';
import { createDetectorSteps } from '../../../utils/constants';
import { ContentPanel } from '../../../../../components/ContentPanel';
import { Detector, FieldMapping } from '../../../../../../models/interfaces';
import { EMPTY_FIELD_MAPPINGS } from '../utils/constants';
import { EMPTY_FIELD_MAPPINGS_VIEW } from '../utils/constants';
import { DetectorCreationStep } from '../../../models/types';
import { GetFieldMappingViewResponse } from '../../../../../../server/models/interfaces';
import FieldMappingService from '../../../../../services/FieldMappingService';
Expand Down Expand Up @@ -49,7 +57,7 @@ export default class ConfigureFieldMapping extends Component<
});
this.state = {
loading: props.loading || false,
mappingsData: EMPTY_FIELD_MAPPINGS,
mappingsData: EMPTY_FIELD_MAPPINGS_VIEW,
createdMappings,
invalidMappingFieldNames: [],
};
Expand All @@ -70,17 +78,21 @@ export default class ConfigureFieldMapping extends Component<
Object.keys(mappingsView.response.properties).forEach((ruleFieldName) => {
existingMappings[ruleFieldName] = mappingsView.response.properties[ruleFieldName].path;
});
this.setState({ createdMappings: existingMappings, mappingsData: mappingsView.response });
this.setState({
createdMappings: existingMappings,
mappingsData: {
...mappingsView.response,
unmapped_field_aliases: [
'timestamp',
...(mappingsView.response.unmapped_field_aliases || []),
],
},
});
this.updateMappingSharedState(existingMappings);
}
this.setState({ loading: false });
};

validateMappings(mappings: ruleFieldToIndexFieldMap): boolean {
// TODO: Implement validation
return true; //allFieldsMapped; // && allAliasesUnique;
}

/**
* Returns the fieldName(s) that have duplicate alias assigned to them
*/
Expand Down Expand Up @@ -110,8 +122,7 @@ export default class ConfigureFieldMapping extends Component<
invalidMappingFieldNames: invalidMappingFieldNames,
});
this.updateMappingSharedState(newMappings);
const mappingsValid = this.validateMappings(newMappings);
this.props.updateDataValidState(DetectorCreationStep.CONFIGURE_FIELD_MAPPING, mappingsValid);
this.props.updateDataValidState(DetectorCreationStep.CONFIGURE_FIELD_MAPPING, true);
};

updateMappingSharedState = (createdMappings: ruleFieldToIndexFieldMap) => {
Expand All @@ -126,42 +137,52 @@ export default class ConfigureFieldMapping extends Component<
};

render() {
const { isEdit } = this.props;
const { loading, mappingsData, createdMappings, invalidMappingFieldNames } = this.state;
const existingMappings: ruleFieldToIndexFieldMap = {
...createdMappings,
};
const ruleFields = [...(mappingsData.unmapped_field_aliases || [])];
const indexFields = [...(mappingsData.unmapped_index_fields || [])];

// read only data
const mappedRuleFields: string[] = [];
const mappedLogFields: string[] = [];
Object.keys(mappingsData.properties).forEach((ruleFieldName) => {
existingMappings[ruleFieldName] = mappingsData.properties[ruleFieldName].path;
ruleFields.unshift(ruleFieldName);
indexFields.unshift(mappingsData.properties[ruleFieldName].path);
mappedRuleFields.unshift(ruleFieldName);
mappedLogFields.unshift(mappingsData.properties[ruleFieldName].path);
});

// edit data
const ruleFields = [...(mappingsData.unmapped_field_aliases || [])];
const indexFields = [...(mappingsData.unmapped_index_fields || [])];

return (
<div>
{!isEdit && (
<>
<EuiTitle size={'m'}>
<h3>{createDetectorSteps[DetectorCreationStep.CONFIGURE_FIELD_MAPPING].title}</h3>
</EuiTitle>
<EuiTitle size={'m'}>
<h3>{createDetectorSteps[DetectorCreationStep.CONFIGURE_FIELD_MAPPING].title}</h3>
</EuiTitle>

<EuiText size="s" color="subdued">
To perform threat detection, known field names from your log data source are
automatically mapped to rule field names. Additional fields that may require manual
mapping will be shown below.
</EuiText>
<EuiText size="s" color="subdued">
To perform threat detection, known field names from your log data source are automatically
mapped to rule field names. Additional fields that may require manual mapping will be
shown below.
</EuiText>

<EuiSpacer size={'m'} />
</>
)}
<EuiSpacer size={'m'} />

{ruleFields.length > 0 && (
{ruleFields.length > 0 ? (
<>
<ContentPanel title={`Required field mappings (${ruleFields.length})`} titleSize={'m'}>
<EuiCallOut
title={`${ruleFields.length} rule fields may need manual mapping`}
color={'warning'}
>
<p>
To generate accurate findings, we recommend mapping the following security rules
fields with the log field from your data source.
</p>
</EuiCallOut>
<EuiSpacer size={'m'} />
<ContentPanel title={`Manual field mappings (${ruleFields.length})`} titleSize={'m'}>
<FieldMappingsTable<MappingViewType.Edit>
{...this.props}
loading={loading}
ruleFields={ruleFields}
indexFields={indexFields}
Expand All @@ -171,12 +192,48 @@ export default class ConfigureFieldMapping extends Component<
invalidMappingFieldNames,
onMappingCreation: this.onMappingCreation,
}}
{...this.props}
/>
</ContentPanel>
<EuiSpacer size={'m'} />
</>
) : (
<>
<EuiCallOut title={'We have automatically mapped all fields'} color={'success'}>
<p>
Your data source(s) have been mapped with all security rule fields. No action is
needed.
</p>
</EuiCallOut>
<EuiSpacer size={'m'} />
</>
)}

<EuiPanel>
<EuiAccordion
buttonContent={
<div data-test-subj="mapped-fields-btn">
<EuiTitle>
<h4>{`View mapped fields (${mappedRuleFields.length})`}</h4>
</EuiTitle>
</div>
}
buttonProps={{ style: { paddingLeft: '10px', paddingRight: '10px' } }}
id={'mappedFieldsAccordion'}
initialIsOpen={false}
>
<EuiHorizontalRule margin={'xs'} />
<FieldMappingsTable<MappingViewType.Readonly>
{...this.props}
loading={loading}
ruleFields={mappedRuleFields}
indexFields={mappedLogFields}
mappingProps={{
type: MappingViewType.Readonly,
}}
/>
</EuiAccordion>
</EuiPanel>
<EuiSpacer size={'m'} />
</div>
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,22 @@
* SPDX-License-Identifier: Apache-2.0
*/

import { GetFieldMappingViewResponse } from '../../../../../../server/models/interfaces';
import {
FieldMappingPropertyMap,
GetFieldMappingViewResponse,
} from '../../../../../../server/models/interfaces';

export const STATUS_ICON_PROPS = {
unmapped: { type: 'alert', color: 'danger' },
mapped: { type: 'checkInCircleFilled', color: 'success' },
};

export const EMPTY_FIELD_MAPPINGS: GetFieldMappingViewResponse = {
export const EMPTY_FIELD_MAPPINGS_VIEW: GetFieldMappingViewResponse = {
properties: {},
unmapped_field_aliases: [],
unmapped_index_fields: [],
};

export const EMPTY_FIELD_MAPPINGS: FieldMappingPropertyMap = {
properties: {},
};
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,10 @@ export const FieldMappingsView: React.FC<FieldMappingsViewProps> = ({
async (indexName: string) => {
const getMappingRes = await services?.fieldMappingService.getMappings(indexName);
if (getMappingRes?.ok) {
const mappings = getMappingRes.response[detector.detector_type.toLowerCase()];
if (mappings) {
const mappingsData = getMappingRes.response[indexName];
if (mappingsData) {
let items: FieldMappingsTableItem[] = [];
Object.entries(mappings.mappings.properties).forEach((entry) => {
Object.entries(mappingsData.mappings.properties).forEach((entry) => {
items.push({
ruleFieldName: entry[0],
logFieldName: entry[1].path,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
import React, { Component } from 'react';
import { RouteComponentProps } from 'react-router-dom';
import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiTitle } from '@elastic/eui';
import ConfigureFieldMapping from '../../../CreateDetector/components/ConfigureFieldMapping';
import { Detector, FieldMapping } from '../../../../../models/interfaces';
import FieldMappingService from '../../../../services/FieldMappingService';
import { DetectorHit, SearchDetectorsResponse } from '../../../../../server/models/interfaces';
Expand All @@ -15,6 +14,7 @@ import { DetectorsService } from '../../../../services';
import { ServerResponse } from '../../../../../server/models/types';
import { NotificationsStart } from 'opensearch-dashboards/public';
import { errorNotificationToast, successNotificationToast } from '../../../../utils/helpers';
import EditFieldMappings from '../../containers/FieldMappings/EditFieldMapping';

export interface UpdateFieldMappingsProps
extends RouteComponentProps<any, any, { detectorHit: DetectorHit }> {
Expand Down Expand Up @@ -159,14 +159,12 @@ export default class UpdateFieldMappings extends Component<
<EuiSpacer size={'xxl'} />

{!loading && (
<ConfigureFieldMapping
<EditFieldMappings
{...this.props}
isEdit={true}
detector={detector}
fieldMappings={fieldMappings}
filedMappingService={filedMappingService}
replaceFieldMappings={this.replaceFieldMappings}
updateDataValidState={() => {}}
loading={loading}
/>
)}
Expand Down
Loading