Skip to content
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

Support more cases for detection; use code editor for condition #783

Merged
merged 3 commits into from
Nov 21, 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
4 changes: 2 additions & 2 deletions babel.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ module.exports = {
],
plugins: [
[require('@babel/plugin-transform-runtime'), { regenerator: true }],
require('@babel/plugin-proposal-class-properties'),
require('@babel/plugin-proposal-object-rest-spread'),
require('@babel/plugin-transform-class-properties'),
require('@babel/plugin-transform-object-rest-spread'),
],
};
5 changes: 3 additions & 2 deletions cypress/integration/1_detectors.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const cypressIndexWindows = 'cypress-index-windows';
const detectorName = 'test detector';
const cypressLogTypeDns = 'dns';
const sampleNotificationChannel = 'sample_chime_channel';
const creationFailedMessage = 'Create detector failed.';

const cypressDNSRule = dns_name_rule_data.title;

Expand Down Expand Up @@ -381,12 +382,12 @@ describe('Detectors', () => {

it('...can fail creation', () => {
createDetector(`${detectorName}_fail`, '.kibana_1', true);
cy.getElementByText('.euiCallOut', 'Create detector failed.');
cy.getElementByText('.euiCallOut', creationFailedMessage);
});

it('...can be created', () => {
createDetector(detectorName, cypressIndexDns, false);
cy.getElementByText('.euiCallOut', 'Detector created successfully');
cy.contains(creationFailedMessage).should('not.exist');
});

it('...basic details can be edited', () => {
Expand Down
20 changes: 5 additions & 15 deletions cypress/integration/2_rules.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -176,8 +176,6 @@ const fillCreateForm = () => {
getMapValueField().type('FieldValue');
});

getConditionAddButton().click({ force: true });

// rule additional details
SAMPLE_RULE.tags.forEach((tag, idx) => {
getTagField(idx).type(tag);
Expand Down Expand Up @@ -336,7 +334,7 @@ describe('Rules', () => {
getMapKeyField()
.parentsUntil('.euiFormRow__fieldWrapper')
.siblings()
.contains('Key name is required');
.contains('Invalid key name');

getMapKeyField().type('FieldKey');
getMapKeyField()
Expand Down Expand Up @@ -388,13 +386,10 @@ describe('Rules', () => {
getConditionField().scrollIntoView();
getConditionField().find('.euiFormErrorText').should('not.exist');
getRuleSubmitButton().click({ force: true });
getConditionField().parents('.euiFormRow__fieldWrapper').contains('Condition is required');

getConditionAddButton().click({ force: true });
getConditionField().find('.euiFormErrorText').should('not.exist');

getConditionRemoveButton(0).click({ force: true });
getConditionField().parents('.euiFormRow__fieldWrapper').contains('Condition is required');
getConditionField()
.parents('.euiFormRow__fieldWrapper')
.contains('Condition is required')
.should('not.exist');
});

it('...should validate tag field', () => {
Expand Down Expand Up @@ -472,11 +467,6 @@ describe('Rules', () => {
getMapListField().type('FieldValue');
});

// condition field
getConditionRemoveButton(0).click({ force: true });
toastShouldExist();
getConditionAddButton().click({ force: true });

// tags field
getTagField(0).clearValue().type('wrong.tag');
toastShouldExist();
Expand Down
7 changes: 4 additions & 3 deletions cypress/integration/3_alerts.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -112,12 +112,12 @@ describe('Alerts', () => {

// Wait for the findings table to finish loading
cy.contains('Findings (1)');
cy.contains('Cypress USB Rule');
cy.contains('Detection rules');

// Confirm alert findings contain expected values
cy.get('tbody > tr').should(($tr) => {
expect($tr, `timestamp`).to.contain(date);
expect($tr, `rule name`).to.contain('Cypress USB Rule');
expect($tr, `detection`).to.contain('Detection rules');
expect($tr, `detector name`).to.contain(testDetector.name);
expect($tr, `log type`).to.contain(
`System Activity: ${getLogTypeLabel(testDetector.detector_type)}`
Expand All @@ -143,7 +143,8 @@ describe('Alerts', () => {

cy.get('[data-test-subj="alert-details-flyout"]').within(() => {
// Wait for findings table to finish loading
cy.contains('Cypress USB Rule');
cy.wait(3000);
cy.contains('Detection rules');

// Click the details button for the first finding
cy.get('tbody > tr')
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@
"@cypress/request": "^3.0.0"
},
"devDependencies": {
"@babel/plugin-transform-class-properties": "^7.22.9",
"@babel/plugin-transform-object-rest-spread": "^7.22.9",
"@elastic/elastic-eslint-config-kibana": "link:../../packages/opensearch-eslint-config-opensearch-dashboards",
"@elastic/eslint-import-resolver-kibana": "link:../../packages/osd-eslint-import-resolver-opensearch-dashboards",
"@testing-library/dom": "^8.11.3",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,10 @@ import {
EuiFilePicker,
EuiButtonEmpty,
EuiCallOut,
EuiCodeEditor,
} from '@elastic/eui';
import _ from 'lodash';
import { validateCondition, validateDetectionFieldName } from '../../../../utils/validation';
import { SelectionExpField } from './components/SelectionExpField';

export interface DetectionVisualEditorProps {
detectionYml: string;
Expand Down Expand Up @@ -90,7 +90,7 @@ const detectionModifierOptions = [
];

const defaultDetectionObj: DetectionObject = {
condition: '',
condition: 'Selection_1',
selections: [
{
name: 'Selection_1',
Expand Down Expand Up @@ -178,7 +178,18 @@ export class DetectionVisualEditor extends React.Component<
const selectionMapJSON = detectionJSON[selectionKey];
const selectionDataEntries: SelectionData[] = [];

if (typeof selectionMapJSON === 'object') {
if (Array.isArray(selectionMapJSON)) {
selectionDataEntries.push({
field: '',
modifier: 'all',
values: selectionMapJSON,
selectedRadioId: `${
selectionMapJSON.length <= 1
? SelectionMapValueRadioId.VALUE
: SelectionMapValueRadioId.LIST
}-${selectionIdx}-0`,
});
} else if (typeof selectionMapJSON === 'object') {
Object.keys(selectionMapJSON).forEach((fieldKey, dataIdx) => {
const [field, modifier] = fieldKey.split('|');
const val = selectionMapJSON[fieldKey];
Expand Down Expand Up @@ -212,11 +223,15 @@ export class DetectionVisualEditor extends React.Component<
};

selections.forEach((selection) => {
const selectionMaps: any = {};
let selectionMaps: any = {};

selection.data.forEach((datum) => {
const key = `${datum.field}${datum.modifier ? `|${datum.modifier}` : ''}`;
selectionMaps[key] = datum.values;
if (datum.field) {
const key = `${datum.field}${datum.modifier ? `|${datum.modifier}` : ''}`;
selectionMaps[key] = datum.values;
} else {
selectionMaps = datum.values;
}
});

compiledDetection[selection.name] = selectionMaps;
Expand All @@ -228,21 +243,16 @@ export class DetectionVisualEditor extends React.Component<
private validateData = (selections: Selection[]) => {
const { errors } = this.state;
selections.map((selection, selIdx) => {
const fieldNames = new Set<string>();
selection.data.map((data, idx) => {
if ('field' in data) {
const fieldName = `field_${selIdx}_${idx}`;
delete errors.fields[fieldName];
if (!data.field) {
errors.fields[fieldName] = 'Key name is required';
} else if (fieldNames.has(data.field)) {
errors.fields[fieldName] = 'Key name already used';
} else {
fieldNames.add(data.field);
if (!validateDetectionFieldName(data.field)) {
errors.fields[fieldName] = 'Invalid key name.';
}

if (!validateDetectionFieldName(data.field)) {
errors.fields[fieldName] =
'Invalid key name. Valid characters are a-z, A-Z, 0-9, hyphens, dots, and underscores';
}

errors.touched[fieldName] = true;
}

Expand Down Expand Up @@ -343,29 +353,14 @@ export class DetectionVisualEditor extends React.Component<
};

private validateCondition = (value: string) => {
const {
errors,
detectionObj: { selections },
} = this.state;
const { errors } = this.state;
value = value.trim();
delete errors.fields['condition'];
if (!value) {
errors.fields['condition'] = 'Condition is required';
} else {
if (!validateCondition(value)) {
errors.fields['condition'] = 'Invalid condition.';
} else {
const selectionNames = _.map(selections, 'name');
const conditions = _.pull(value.split(' '), ...['and', 'or', 'not']);
conditions.map((selection) => {
if (_.indexOf(selectionNames, selection) === -1) {
errors.fields[
'condition'
] = `Invalid selection name ${selection}. Allowed names: "${selectionNames.join(
', '
)}"`;
}
});
}
}

Expand All @@ -376,8 +371,6 @@ export class DetectionVisualEditor extends React.Component<
};

private updateCondition = (value: string) => {
value = value.trim();

const detectionObj: DetectionObject = { ...this.state.detectionObj, condition: value };
this.setState(
{
Expand Down Expand Up @@ -729,6 +722,7 @@ export class DetectionVisualEditor extends React.Component<
<EuiButton
style={{ width: '70%' }}
iconType="plusInCircle"
disabled={!selection.data.at(-1)?.field}
onClick={() => {
const newData = [
...selection.data,
Expand All @@ -754,14 +748,15 @@ export class DetectionVisualEditor extends React.Component<
fullWidth
iconType={'plusInCircle'}
onClick={() => {
const selectionName = `Selection_${selections.length + 1}`;
this.setState({
detectionObj: {
condition,
condition: `${condition} and ${selectionName}`,
selections: [
...selections,
{
...defaultDetectionObj.selections[0],
name: `Selection_${selections.length + 1}`,
name: selectionName,
},
],
},
Expand Down Expand Up @@ -796,11 +791,16 @@ export class DetectionVisualEditor extends React.Component<
</>
}
>
<SelectionExpField
selections={this.state.detectionObj.selections}
<EuiCodeEditor
mode="yaml"
width="600px"
height="50px"
value={this.state.detectionObj.condition}
onChange={this.updateCondition}
dataTestSubj={'rule_detection_field'}
onChange={(value) => this.updateCondition(value)}
onBlur={(e) => {
this.updateCondition(this.state.detectionObj.condition);
}}
data-test-subj={'rule_detection_field'}
/>
</EuiFormRow>

Expand Down
Loading
Loading