Skip to content

Commit e70319f

Browse files
amsiglanAWSHurneyt
authored andcommitted
[Backport 2.x] Field mapping changes from opensearch-project#307 (opensearch-project#311)
* Updated field mapping UX; disabled windows run for cypress (opensearch-project#307) * updated field mapping UX; disabled windows run for cypress Signed-off-by: Amardeepsingh Siglani <[email protected]> * updated workflow file Signed-off-by: Amardeepsingh Siglani <[email protected]> * added timestamp field to list of unmapped fields Signed-off-by: Amardeepsingh Siglani <[email protected]> * updated cypress test Signed-off-by: Amardeepsingh Siglani <[email protected]> Signed-off-by: Amardeepsingh Siglani <[email protected]> * updated cypress workflow Signed-off-by: Amardeepsingh Siglani <[email protected]> Signed-off-by: Amardeepsingh Siglani <[email protected]> Signed-off-by: AWSHurneyt <[email protected]>
1 parent 5181cc6 commit e70319f

File tree

7 files changed

+316
-55
lines changed

7 files changed

+316
-55
lines changed

.github/workflows/cypress-workflow.yml

Lines changed: 54 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,13 @@ env:
1313
jobs:
1414
tests:
1515
name: Run Cypress E2E tests
16-
runs-on: ubuntu-latest
16+
strategy:
17+
matrix:
18+
os: [ubuntu-latest]
19+
include:
20+
- os: ubuntu-latest
21+
cypress_cache_folder: ~/.cache/Cypress
22+
runs-on: ${{ matrix.os }}
1723
env:
1824
# prevents extra Cypress installation progress messages
1925
CI: 1
@@ -25,55 +31,87 @@ jobs:
2531
with:
2632
# TODO: Parse this from security analytics plugin (https://github.com/opensearch-project/security-analytics/issues/170)
2733
java-version: 11
34+
2835
- name: Checkout security analytics
2936
uses: actions/checkout@v2
3037
with:
3138
path: security-analytics
3239
repository: opensearch-project/security-analytics
3340
ref: ${{ env.SECURITY_ANALYTICS_BRANCH }}
41+
3442
- name: Run opensearch with plugin
3543
run: |
3644
cd security-analytics
3745
./gradlew run -Dopensearch.version=${{ env.OPENSEARCH_VERSION }} &
3846
sleep 300
39-
# timeout 300 bash -c 'while [[ "$(curl -s -o /dev/null -w ''%{http_code}'' localhost:9200)" != "200" ]]; do sleep 5; done'
40-
- name: Checkout Security Analytics Dashboards plugin
41-
uses: actions/checkout@v2
42-
with:
43-
path: security-analytics-dashboards-plugin
47+
shell: bash
48+
4449
- name: Checkout OpenSearch-Dashboards
4550
uses: actions/checkout@v2
4651
with:
4752
repository: opensearch-project/OpenSearch-Dashboards
4853
path: OpenSearch-Dashboards
4954
ref: ${{ env.OPENSEARCH_DASHBOARDS_VERSION }}
55+
56+
- name: Checkout Security Analytics Dashboards plugin
57+
uses: actions/checkout@v2
58+
with:
59+
path: OpenSearch-Dashboards/plugins/security-analytics-dashboards-plugin
60+
5061
- name: Get node and yarn versions
5162
id: versions
5263
run: |
5364
echo "::set-output name=node_version::$(node -p "(require('./OpenSearch-Dashboards/package.json').engines.node).match(/[.0-9]+/)[0]")"
5465
echo "::set-output name=yarn_version::$(node -p "(require('./OpenSearch-Dashboards/package.json').engines.yarn).match(/[.0-9]+/)[0]")"
66+
5567
- name: Setup node
5668
uses: actions/setup-node@v1
5769
with:
5870
node-version: ${{ steps.versions.outputs.node_version }}
5971
registry-url: 'https://registry.npmjs.org'
72+
6073
- name: Install correct yarn version for OpenSearch-Dashboards
6174
run: |
6275
npm uninstall -g yarn
6376
echo "Installing yarn ${{ steps.versions_step.outputs.yarn_version }}"
6477
npm i -g yarn@${{ steps.versions.outputs.yarn_version }}
78+
6579
- name: Bootstrap plugin/OpenSearch-Dashboards
6680
run: |
67-
mkdir -p OpenSearch-Dashboards/plugins
68-
mv security-analytics-dashboards-plugin OpenSearch-Dashboards/plugins
6981
cd OpenSearch-Dashboards/plugins/security-analytics-dashboards-plugin
7082
yarn osd bootstrap
83+
7184
- name: Run OpenSearch-Dashboards server
7285
run: |
7386
cd OpenSearch-Dashboards
7487
yarn start --no-base-path --no-watch &
75-
sleep 300
76-
# timeout 300 bash -c 'while [[ "$(curl -s localhost:5601/api/status | jq -r '.status.overall.state')" != "green" ]]; do sleep 5; done'
88+
shell: bash
89+
90+
- name: Sleep until OSD server starts
91+
run: sleep 300
92+
shell: bash
93+
94+
- name: Install Cypress
95+
run: |
96+
cd OpenSearch-Dashboards/plugins/security-analytics-dashboards-plugin
97+
# This will install Cypress in case the binary is missing which can happen on Windows and Mac
98+
# If the binary exists, this will exit quickly so it should not be an expensive operation
99+
npx cypress install
100+
shell: bash
101+
102+
- name: Get Cypress version
103+
id: cypress_version
104+
run: |
105+
cd OpenSearch-Dashboards/plugins/security-analytics-dashboards-plugin
106+
echo "::set-output name=cypress_version::$(cat ./package.json | jq '.dependencies.cypress' | tr -d '"')"
107+
108+
- name: Cache Cypress
109+
id: cache-cypress
110+
uses: actions/cache@v2
111+
with:
112+
path: ${{ matrix.cypress_cache_folder }}
113+
key: cypress-cache-v2-${{ runner.os }}-${{ hashFiles('**/package.json') }}
114+
77115
# for now just chrome, use matrix to do all browsers later
78116
- name: Cypress tests
79117
uses: cypress-io/github-action@v2
@@ -82,15 +120,19 @@ jobs:
82120
command: yarn run cypress run
83121
wait-on: 'http://localhost:5601'
84122
browser: chrome
123+
env:
124+
CYPRESS_CACHE_FOLDER: ${{ matrix.cypress_cache_folder }}
125+
85126
# Screenshots are only captured on failure, will change this once we do visual regression tests
86127
- uses: actions/upload-artifact@v1
87128
if: failure()
88129
with:
89-
name: cypress-screenshots
130+
name: cypress-screenshots-${{ matrix.os }}
90131
path: OpenSearch-Dashboards/plugins/security-analytics-dashboards-plugin/cypress/screenshots
132+
91133
# Test run video was always captured, so this action uses "always()" condition
92134
- uses: actions/upload-artifact@v1
93135
if: always()
94136
with:
95-
name: cypress-videos
137+
name: cypress-videos-${{ matrix.os }}
96138
path: OpenSearch-Dashboards/plugins/security-analytics-dashboards-plugin/cypress/videos

cypress/integration/1_detectors.spec.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ describe('Detectors', () => {
6969
cy.get('button').contains('Next').click({ force: true });
7070

7171
// Check that correct page now showing
72-
cy.contains('Required field mappings');
72+
cy.contains('Configure field mapping');
7373

7474
// Select appropriate names to map fields to
7575
for (let field_name in sample_field_mappings.properties) {

public/pages/CreateDetector/components/ConfigureFieldMapping/containers/ConfigureFieldMapping.tsx

Lines changed: 90 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,20 @@
55

66
import React, { Component } from 'react';
77
import { RouteComponentProps } from 'react-router-dom';
8-
import { EuiSpacer, EuiTitle, EuiText } from '@elastic/eui';
8+
import {
9+
EuiSpacer,
10+
EuiTitle,
11+
EuiText,
12+
EuiCallOut,
13+
EuiAccordion,
14+
EuiHorizontalRule,
15+
EuiPanel,
16+
} from '@elastic/eui';
917
import FieldMappingsTable from '../components/RequiredFieldMapping';
1018
import { createDetectorSteps } from '../../../utils/constants';
1119
import { ContentPanel } from '../../../../../components/ContentPanel';
1220
import { Detector, FieldMapping } from '../../../../../../models/interfaces';
13-
import { EMPTY_FIELD_MAPPINGS } from '../utils/constants';
21+
import { EMPTY_FIELD_MAPPINGS_VIEW } from '../utils/constants';
1422
import { DetectorCreationStep } from '../../../models/types';
1523
import { GetFieldMappingViewResponse } from '../../../../../../server/models/interfaces';
1624
import FieldMappingService from '../../../../../services/FieldMappingService';
@@ -49,7 +57,7 @@ export default class ConfigureFieldMapping extends Component<
4957
});
5058
this.state = {
5159
loading: props.loading || false,
52-
mappingsData: EMPTY_FIELD_MAPPINGS,
60+
mappingsData: EMPTY_FIELD_MAPPINGS_VIEW,
5361
createdMappings,
5462
invalidMappingFieldNames: [],
5563
};
@@ -70,17 +78,21 @@ export default class ConfigureFieldMapping extends Component<
7078
Object.keys(mappingsView.response.properties).forEach((ruleFieldName) => {
7179
existingMappings[ruleFieldName] = mappingsView.response.properties[ruleFieldName].path;
7280
});
73-
this.setState({ createdMappings: existingMappings, mappingsData: mappingsView.response });
81+
this.setState({
82+
createdMappings: existingMappings,
83+
mappingsData: {
84+
...mappingsView.response,
85+
unmapped_field_aliases: [
86+
'timestamp',
87+
...(mappingsView.response.unmapped_field_aliases || []),
88+
],
89+
},
90+
});
7491
this.updateMappingSharedState(existingMappings);
7592
}
7693
this.setState({ loading: false });
7794
};
7895

79-
validateMappings(mappings: ruleFieldToIndexFieldMap): boolean {
80-
// TODO: Implement validation
81-
return true; //allFieldsMapped; // && allAliasesUnique;
82-
}
83-
8496
/**
8597
* Returns the fieldName(s) that have duplicate alias assigned to them
8698
*/
@@ -110,8 +122,7 @@ export default class ConfigureFieldMapping extends Component<
110122
invalidMappingFieldNames: invalidMappingFieldNames,
111123
});
112124
this.updateMappingSharedState(newMappings);
113-
const mappingsValid = this.validateMappings(newMappings);
114-
this.props.updateDataValidState(DetectorCreationStep.CONFIGURE_FIELD_MAPPING, mappingsValid);
125+
this.props.updateDataValidState(DetectorCreationStep.CONFIGURE_FIELD_MAPPING, true);
115126
};
116127

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

128139
render() {
129-
const { isEdit } = this.props;
130140
const { loading, mappingsData, createdMappings, invalidMappingFieldNames } = this.state;
131141
const existingMappings: ruleFieldToIndexFieldMap = {
132142
...createdMappings,
133143
};
134-
const ruleFields = [...(mappingsData.unmapped_field_aliases || [])];
135-
const indexFields = [...(mappingsData.unmapped_index_fields || [])];
136144

145+
// read only data
146+
const mappedRuleFields: string[] = [];
147+
const mappedLogFields: string[] = [];
137148
Object.keys(mappingsData.properties).forEach((ruleFieldName) => {
138-
existingMappings[ruleFieldName] = mappingsData.properties[ruleFieldName].path;
139-
ruleFields.unshift(ruleFieldName);
140-
indexFields.unshift(mappingsData.properties[ruleFieldName].path);
149+
mappedRuleFields.unshift(ruleFieldName);
150+
mappedLogFields.unshift(mappingsData.properties[ruleFieldName].path);
141151
});
142152

153+
// edit data
154+
const ruleFields = [...(mappingsData.unmapped_field_aliases || [])];
155+
const indexFields = [...(mappingsData.unmapped_index_fields || [])];
156+
143157
return (
144158
<div>
145-
{!isEdit && (
146-
<>
147-
<EuiTitle size={'m'}>
148-
<h3>{createDetectorSteps[DetectorCreationStep.CONFIGURE_FIELD_MAPPING].title}</h3>
149-
</EuiTitle>
159+
<EuiTitle size={'m'}>
160+
<h3>{createDetectorSteps[DetectorCreationStep.CONFIGURE_FIELD_MAPPING].title}</h3>
161+
</EuiTitle>
150162

151-
<EuiText size="s" color="subdued">
152-
To perform threat detection, known field names from your log data source are
153-
automatically mapped to rule field names. Additional fields that may require manual
154-
mapping will be shown below.
155-
</EuiText>
163+
<EuiText size="s" color="subdued">
164+
To perform threat detection, known field names from your log data source are automatically
165+
mapped to rule field names. Additional fields that may require manual mapping will be
166+
shown below.
167+
</EuiText>
156168

157-
<EuiSpacer size={'m'} />
158-
</>
159-
)}
169+
<EuiSpacer size={'m'} />
160170

161-
{ruleFields.length > 0 && (
171+
{ruleFields.length > 0 ? (
162172
<>
163-
<ContentPanel title={`Required field mappings (${ruleFields.length})`} titleSize={'m'}>
173+
<EuiCallOut
174+
title={`${ruleFields.length} rule fields may need manual mapping`}
175+
color={'warning'}
176+
>
177+
<p>
178+
To generate accurate findings, we recommend mapping the following security rules
179+
fields with the log field from your data source.
180+
</p>
181+
</EuiCallOut>
182+
<EuiSpacer size={'m'} />
183+
<ContentPanel title={`Manual field mappings (${ruleFields.length})`} titleSize={'m'}>
164184
<FieldMappingsTable<MappingViewType.Edit>
185+
{...this.props}
165186
loading={loading}
166187
ruleFields={ruleFields}
167188
indexFields={indexFields}
@@ -171,12 +192,48 @@ export default class ConfigureFieldMapping extends Component<
171192
invalidMappingFieldNames,
172193
onMappingCreation: this.onMappingCreation,
173194
}}
174-
{...this.props}
175195
/>
176196
</ContentPanel>
177197
<EuiSpacer size={'m'} />
178198
</>
199+
) : (
200+
<>
201+
<EuiCallOut title={'We have automatically mapped all fields'} color={'success'}>
202+
<p>
203+
Your data source(s) have been mapped with all security rule fields. No action is
204+
needed.
205+
</p>
206+
</EuiCallOut>
207+
<EuiSpacer size={'m'} />
208+
</>
179209
)}
210+
211+
<EuiPanel>
212+
<EuiAccordion
213+
buttonContent={
214+
<div data-test-subj="mapped-fields-btn">
215+
<EuiTitle>
216+
<h4>{`View mapped fields (${mappedRuleFields.length})`}</h4>
217+
</EuiTitle>
218+
</div>
219+
}
220+
buttonProps={{ style: { paddingLeft: '10px', paddingRight: '10px' } }}
221+
id={'mappedFieldsAccordion'}
222+
initialIsOpen={false}
223+
>
224+
<EuiHorizontalRule margin={'xs'} />
225+
<FieldMappingsTable<MappingViewType.Readonly>
226+
{...this.props}
227+
loading={loading}
228+
ruleFields={mappedRuleFields}
229+
indexFields={mappedLogFields}
230+
mappingProps={{
231+
type: MappingViewType.Readonly,
232+
}}
233+
/>
234+
</EuiAccordion>
235+
</EuiPanel>
236+
<EuiSpacer size={'m'} />
180237
</div>
181238
);
182239
}

public/pages/CreateDetector/components/ConfigureFieldMapping/utils/constants.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,22 @@
33
* SPDX-License-Identifier: Apache-2.0
44
*/
55

6-
import { GetFieldMappingViewResponse } from '../../../../../../server/models/interfaces';
6+
import {
7+
FieldMappingPropertyMap,
8+
GetFieldMappingViewResponse,
9+
} from '../../../../../../server/models/interfaces';
710

811
export const STATUS_ICON_PROPS = {
912
unmapped: { type: 'alert', color: 'danger' },
1013
mapped: { type: 'checkInCircleFilled', color: 'success' },
1114
};
1215

13-
export const EMPTY_FIELD_MAPPINGS: GetFieldMappingViewResponse = {
16+
export const EMPTY_FIELD_MAPPINGS_VIEW: GetFieldMappingViewResponse = {
1417
properties: {},
1518
unmapped_field_aliases: [],
1619
unmapped_index_fields: [],
1720
};
21+
22+
export const EMPTY_FIELD_MAPPINGS: FieldMappingPropertyMap = {
23+
properties: {},
24+
};

public/pages/Detectors/components/FieldMappingsView/FieldMappingsView.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,10 +52,10 @@ export const FieldMappingsView: React.FC<FieldMappingsViewProps> = ({
5252
async (indexName: string) => {
5353
const getMappingRes = await services?.fieldMappingService.getMappings(indexName);
5454
if (getMappingRes?.ok) {
55-
const mappings = getMappingRes.response[detector.detector_type.toLowerCase()];
56-
if (mappings) {
55+
const mappingsData = getMappingRes.response[indexName];
56+
if (mappingsData) {
5757
let items: FieldMappingsTableItem[] = [];
58-
Object.entries(mappings.mappings.properties).forEach((entry) => {
58+
Object.entries(mappingsData.mappings.properties).forEach((entry) => {
5959
items.push({
6060
ruleFieldName: entry[0],
6161
logFieldName: entry[1].path,

0 commit comments

Comments
 (0)