diff --git a/.github/workflows/cypress-workflow.yml b/.github/workflows/cypress-workflow.yml
index 02b36849f..ea311bd20 100644
--- a/.github/workflows/cypress-workflow.yml
+++ b/.github/workflows/cypress-workflow.yml
@@ -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 }}
@@ -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:
@@ -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
diff --git a/cypress/integration/1_detectors.spec.js b/cypress/integration/1_detectors.spec.js
index 01ba7a96f..41cfb7f67 100644
--- a/cypress/integration/1_detectors.spec.js
+++ b/cypress/integration/1_detectors.spec.js
@@ -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) {
diff --git a/public/pages/CreateDetector/components/ConfigureFieldMapping/containers/ConfigureFieldMapping.tsx b/public/pages/CreateDetector/components/ConfigureFieldMapping/containers/ConfigureFieldMapping.tsx
index 4340d12e8..8d5df4ec8 100644
--- a/public/pages/CreateDetector/components/ConfigureFieldMapping/containers/ConfigureFieldMapping.tsx
+++ b/public/pages/CreateDetector/components/ConfigureFieldMapping/containers/ConfigureFieldMapping.tsx
@@ -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';
@@ -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: [],
};
@@ -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
*/
@@ -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) => {
@@ -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 (
- {!isEdit && (
- <>
-
- {createDetectorSteps[DetectorCreationStep.CONFIGURE_FIELD_MAPPING].title}
-
+
+ {createDetectorSteps[DetectorCreationStep.CONFIGURE_FIELD_MAPPING].title}
+
-
- 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.
-
+
+ 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.
+
-
- >
- )}
+
- {ruleFields.length > 0 && (
+ {ruleFields.length > 0 ? (
<>
-
+
+
+ To generate accurate findings, we recommend mapping the following security rules
+ fields with the log field from your data source.
+
+
+
+
+ {...this.props}
loading={loading}
ruleFields={ruleFields}
indexFields={indexFields}
@@ -171,12 +192,48 @@ export default class ConfigureFieldMapping extends Component<
invalidMappingFieldNames,
onMappingCreation: this.onMappingCreation,
}}
- {...this.props}
/>
>
+ ) : (
+ <>
+
+
+ Your data source(s) have been mapped with all security rule fields. No action is
+ needed.
+
+
+
+ >
)}
+
+
+
+
+ {`View mapped fields (${mappedRuleFields.length})`}
+
+
+ }
+ buttonProps={{ style: { paddingLeft: '10px', paddingRight: '10px' } }}
+ id={'mappedFieldsAccordion'}
+ initialIsOpen={false}
+ >
+
+
+ {...this.props}
+ loading={loading}
+ ruleFields={mappedRuleFields}
+ indexFields={mappedLogFields}
+ mappingProps={{
+ type: MappingViewType.Readonly,
+ }}
+ />
+
+
+
);
}
diff --git a/public/pages/CreateDetector/components/ConfigureFieldMapping/utils/constants.ts b/public/pages/CreateDetector/components/ConfigureFieldMapping/utils/constants.ts
index e1245b54e..cd7e111fc 100644
--- a/public/pages/CreateDetector/components/ConfigureFieldMapping/utils/constants.ts
+++ b/public/pages/CreateDetector/components/ConfigureFieldMapping/utils/constants.ts
@@ -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: {},
+};
diff --git a/public/pages/Detectors/components/FieldMappingsView/FieldMappingsView.tsx b/public/pages/Detectors/components/FieldMappingsView/FieldMappingsView.tsx
index 3d8ea5cb4..06c782874 100644
--- a/public/pages/Detectors/components/FieldMappingsView/FieldMappingsView.tsx
+++ b/public/pages/Detectors/components/FieldMappingsView/FieldMappingsView.tsx
@@ -52,10 +52,10 @@ export const FieldMappingsView: React.FC = ({
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,
diff --git a/public/pages/Detectors/components/UpdateFieldMappings/UpdateFieldMappings.tsx b/public/pages/Detectors/components/UpdateFieldMappings/UpdateFieldMappings.tsx
index eb5a6f5b5..1ee5ec8af 100644
--- a/public/pages/Detectors/components/UpdateFieldMappings/UpdateFieldMappings.tsx
+++ b/public/pages/Detectors/components/UpdateFieldMappings/UpdateFieldMappings.tsx
@@ -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';
@@ -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 {
@@ -159,14 +159,12 @@ export default class UpdateFieldMappings extends Component<
{!loading && (
- {}}
loading={loading}
/>
)}
diff --git a/public/pages/Detectors/containers/FieldMappings/EditFieldMapping.tsx b/public/pages/Detectors/containers/FieldMappings/EditFieldMapping.tsx
new file mode 100644
index 000000000..a99c089c5
--- /dev/null
+++ b/public/pages/Detectors/containers/FieldMappings/EditFieldMapping.tsx
@@ -0,0 +1,157 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React, { Component } from 'react';
+import { RouteComponentProps } from 'react-router-dom';
+import { EuiSpacer } from '@elastic/eui';
+import FieldMappingsTable from '../../../CreateDetector/components/ConfigureFieldMapping/components/RequiredFieldMapping';
+import { ContentPanel } from '../../../../components/ContentPanel';
+import { Detector, FieldMapping } from '../../../../../models/interfaces';
+import { EMPTY_FIELD_MAPPINGS } from '../../../CreateDetector/components/ConfigureFieldMapping/utils/constants';
+import { FieldMappingPropertyMap } from '../../../../../server/models/interfaces';
+import FieldMappingService from '../../../../services/FieldMappingService';
+import { MappingViewType } from '../../../CreateDetector/components/ConfigureFieldMapping/components/RequiredFieldMapping/FieldMappingsTable';
+
+export interface ruleFieldToIndexFieldMap {
+ [fieldName: string]: string;
+}
+
+interface EditFieldMappingsProps extends RouteComponentProps {
+ detector: Detector;
+ filedMappingService: FieldMappingService;
+ fieldMappings: FieldMapping[];
+ loading: boolean;
+ replaceFieldMappings: (mappings: FieldMapping[]) => void;
+}
+
+interface EditFieldMappingsState {
+ loading: boolean;
+ mappingsData: FieldMappingPropertyMap;
+ createdMappings: ruleFieldToIndexFieldMap;
+ invalidMappingFieldNames: string[];
+}
+
+export default class EditFieldMappings extends Component<
+ EditFieldMappingsProps,
+ EditFieldMappingsState
+> {
+ constructor(props: EditFieldMappingsProps) {
+ super(props);
+ const createdMappings: ruleFieldToIndexFieldMap = {};
+ props.fieldMappings.forEach((mapping) => {
+ createdMappings[mapping.ruleFieldName] = mapping.indexFieldName;
+ });
+ this.state = {
+ loading: props.loading || false,
+ mappingsData: EMPTY_FIELD_MAPPINGS,
+ createdMappings,
+ invalidMappingFieldNames: [],
+ };
+ }
+
+ componentDidMount = async () => {
+ this.getAllMappings();
+ };
+
+ getAllMappings = async () => {
+ this.setState({ loading: true });
+ const indexName = this.props.detector.inputs[0].detector_input.indices[0];
+ const mappingsView = await this.props.filedMappingService.getMappings(indexName);
+ if (mappingsView.ok) {
+ const existingMappings = { ...this.state.createdMappings };
+ const properties = mappingsView.response[indexName]?.mappings.properties;
+
+ if (properties) {
+ Object.keys(properties).forEach((ruleFieldName) => {
+ existingMappings[ruleFieldName] = properties[ruleFieldName].path;
+ });
+ this.setState({ createdMappings: existingMappings, mappingsData: { properties } });
+ this.updateMappingSharedState(existingMappings);
+ }
+ }
+ this.setState({ loading: false });
+ };
+
+ /**
+ * Returns the fieldName(s) that have duplicate alias assigned to them
+ */
+ getInvalidMappingFieldNames(mappings: ruleFieldToIndexFieldMap): string[] {
+ const seenAliases = new Set();
+ const invalidFields: string[] = [];
+
+ Object.entries(mappings).forEach((entry) => {
+ if (seenAliases.has(entry[1])) {
+ invalidFields.push(entry[0]);
+ }
+
+ seenAliases.add(entry[1]);
+ });
+
+ return invalidFields;
+ }
+
+ onMappingCreation = (ruleFieldName: string, indxFieldName: string): void => {
+ const newMappings: ruleFieldToIndexFieldMap = {
+ ...this.state.createdMappings,
+ [ruleFieldName]: indxFieldName,
+ };
+ const invalidMappingFieldNames = this.getInvalidMappingFieldNames(newMappings);
+ this.setState({
+ createdMappings: newMappings,
+ invalidMappingFieldNames: invalidMappingFieldNames,
+ });
+ this.updateMappingSharedState(newMappings);
+ };
+
+ updateMappingSharedState = (createdMappings: ruleFieldToIndexFieldMap) => {
+ this.props.replaceFieldMappings(
+ Object.entries(createdMappings).map((entry) => {
+ return {
+ ruleFieldName: entry[0],
+ indexFieldName: entry[1],
+ };
+ })
+ );
+ };
+
+ render() {
+ const { loading, mappingsData, createdMappings, invalidMappingFieldNames } = this.state;
+ const existingMappings: ruleFieldToIndexFieldMap = {
+ ...createdMappings,
+ };
+ const ruleFields: string[] = [];
+ const indexFields: string[] = [];
+
+ Object.keys(mappingsData.properties).forEach((ruleFieldName) => {
+ existingMappings[ruleFieldName] = mappingsData.properties[ruleFieldName].path;
+ ruleFields.unshift(ruleFieldName);
+ indexFields.unshift(mappingsData.properties[ruleFieldName].path);
+ });
+
+ return (
+
+ {ruleFields.length > 0 && (
+ <>
+
+
+ {...this.props}
+ loading={loading}
+ ruleFields={ruleFields}
+ indexFields={indexFields}
+ mappingProps={{
+ type: MappingViewType.Edit,
+ existingMappings,
+ invalidMappingFieldNames,
+ onMappingCreation: this.onMappingCreation,
+ }}
+ />
+
+
+ >
+ )}
+
+ );
+ }
+}