Skip to content

Commit

Permalink
[Osquery] Fix osquery response actions validations (#144994)
Browse files Browse the repository at this point in the history
  • Loading branch information
tomsonpl authored Nov 28, 2022
1 parent bc2eeda commit 1cb49bb
Show file tree
Hide file tree
Showing 19 changed files with 408 additions and 279 deletions.
91 changes: 84 additions & 7 deletions x-pack/plugins/osquery/cypress/e2e/all/alerts.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,20 @@
* 2.0.
*/

import {
RESPONSE_ACTIONS_ITEM_0,
RESPONSE_ACTIONS_ITEM_1,
RESPONSE_ACTIONS_ITEM_2,
OSQUERY_RESPONSE_ACTION_ADD_BUTTON,
} from '../../tasks/response_actions';
import { ArchiverMethod, runKbnArchiverScript } from '../../tasks/archiver';
import { login } from '../../tasks/login';
import {
findAndClickButton,
findFormFieldByRowsLabelAndType,
inputQuery,
submitQuery,
typeInECSFieldInput,
} from '../../tasks/live_query';
import { preparePack } from '../../tasks/packs';
import { closeModalIfVisible } from '../../tasks/integrations';
Expand Down Expand Up @@ -60,26 +67,96 @@ describe('Alert Event Details', () => {
cy.getBySel('ruleSwitch').should('have.attr', 'aria-checked', 'true');
});

it('enables to add detection action with osquery', () => {
it('adds response actations with osquery with proper validation and form values', () => {
cy.visit('/app/security/rules');
cy.contains(RULE_NAME).click();
cy.contains('Edit rule settings').click();
cy.getBySel('edit-rule-actions-tab').wait(500).click();
cy.contains('Perform no actions').get('select').select('On each rule execution');
cy.contains('Response actions are run on each rule execution');
cy.getBySel('.osquery-ResponseActionTypeSelectOption').click();
cy.get(LIVE_QUERY_EDITOR);
cy.getBySel(OSQUERY_RESPONSE_ACTION_ADD_BUTTON).click();
cy.getBySel(RESPONSE_ACTIONS_ITEM_0).within(() => {
cy.get(LIVE_QUERY_EDITOR);
});
cy.contains('Save changes').click();
cy.contains('Query is a required field');
inputQuery('select * from uptime');
cy.wait(1000); // wait for the validation to trigger - cypress is way faster than users ;)
cy.getBySel(RESPONSE_ACTIONS_ITEM_0).within(() => {
cy.contains('Query is a required field');
inputQuery('select * from uptime1');
});

cy.getBySel(OSQUERY_RESPONSE_ACTION_ADD_BUTTON).click();

cy.getBySel(RESPONSE_ACTIONS_ITEM_1).within(() => {
cy.contains('Run a set of queries in a pack').click();
});
cy.contains('Save changes').click();
cy.getBySel('response-actions-error')
.within(() => {
cy.contains(' Pack is a required field');
})
.should('exist');
cy.contains('Pack is a required field');
cy.getBySel(RESPONSE_ACTIONS_ITEM_1).within(() => {
cy.getBySel('comboBoxInput').type('testpack{downArrow}{enter}');
});

cy.getBySel(OSQUERY_RESPONSE_ACTION_ADD_BUTTON).click();

cy.getBySel(RESPONSE_ACTIONS_ITEM_2).within(() => {
cy.get(LIVE_QUERY_EDITOR);
cy.contains('Query is a required field');
inputQuery('select * from uptime');
cy.contains('Advanced').click();
typeInECSFieldInput('message{downArrow}{enter}');
cy.getBySel('osqueryColumnValueSelect').type('days{downArrow}{enter}');
cy.wait(1000); // wait for the validation to trigger - cypress is way faster than users ;)
});

// getSavedQueriesDropdown().type(`users{downArrow}{enter}`);
cy.contains('Save changes').click();
cy.contains(`${RULE_NAME} was saved`).should('exist');
cy.getBySel('toastCloseButton').click();
cy.contains('Edit rule settings').click();
cy.getBySel('edit-rule-actions-tab').wait(500).click();
cy.contains('select * from uptime');
cy.getBySel(RESPONSE_ACTIONS_ITEM_0).within(() => {
cy.contains('select * from uptime1');
});
cy.getBySel(RESPONSE_ACTIONS_ITEM_2).within(() => {
cy.contains('select * from uptime');
cy.contains('Log message optimized for viewing in a log viewer');
cy.contains('Days of uptime');
});
cy.getBySel(RESPONSE_ACTIONS_ITEM_1).within(() => {
cy.contains('testpack');
cy.getBySel('comboBoxInput').type('{backspace}{enter}');
});
cy.getBySel(RESPONSE_ACTIONS_ITEM_0).within(() => {
cy.contains('select * from uptime1');
cy.getBySel('remove-response-action').click();
});
cy.getBySel(RESPONSE_ACTIONS_ITEM_0).within(() => {
cy.contains('Search for a pack to run');
cy.contains('Pack is a required field');
cy.getBySel('comboBoxInput').type('testpack{downArrow}{enter}');
});
cy.getBySel(RESPONSE_ACTIONS_ITEM_1).within(() => {
cy.contains('select * from uptime');
cy.contains('Log message optimized for viewing in a log viewer');
cy.contains('Days of uptime');
});
cy.contains('Save changes').click();
cy.contains(`${RULE_NAME} was saved`).should('exist');
cy.getBySel('toastCloseButton').click();
cy.contains('Edit rule settings').click();
cy.getBySel('edit-rule-actions-tab').wait(500).click();
cy.getBySel(RESPONSE_ACTIONS_ITEM_0).within(() => {
cy.contains('testpack');
});
cy.getBySel(RESPONSE_ACTIONS_ITEM_1).within(() => {
cy.contains('select * from uptime');
cy.contains('Log message optimized for viewing in a log viewer');
cy.contains('Days of uptime');
});
});

it('should be able to run live query and add to timeline (-depending on the previous test)', () => {
Expand Down
12 changes: 12 additions & 0 deletions x-pack/plugins/osquery/cypress/tasks/response_actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

export const RESPONSE_ACTIONS_ITEM_0 = 'response-actions-list-item-0';
export const RESPONSE_ACTIONS_ITEM_1 = 'response-actions-list-item-1';
export const RESPONSE_ACTIONS_ITEM_2 = 'response-actions-list-item-2';

export const OSQUERY_RESPONSE_ACTION_ADD_BUTTON = 'osquery-response-action-type-selection-option';
16 changes: 6 additions & 10 deletions x-pack/plugins/osquery/public/live_queries/form/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export interface LiveQueryFormFields {
savedQueryId?: string | null;
ecs_mapping: ECSMapping;
packId: string[];
queryType: 'query' | 'pack';
}

interface DefaultLiveQueryFormFields {
Expand Down Expand Up @@ -95,14 +96,14 @@ const LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({
);

const [showSavedQueryFlyout, setShowSavedQueryFlyout] = useState(false);
const [queryType, setQueryType] = useState<string>('query');
const [isLive, setIsLive] = useState(false);

const queryState = getFieldState('query');
const watchedValues = watch();
const handleShowSaveQueryFlyout = useCallback(() => setShowSavedQueryFlyout(true), []);
const handleCloseSaveQueryFlyout = useCallback(() => setShowSavedQueryFlyout(false), []);

const { queryType } = watchedValues;
const {
data,
isLoading,
Expand Down Expand Up @@ -241,7 +242,7 @@ const LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({
}

if (defaultValue?.packId && canRunPacks) {
setQueryType('pack');
setValue('queryType', 'pack');

if (!isPackDataFetched) return;
const selectedPackOption = find(packsData?.data, ['id', defaultValue.packId]);
Expand All @@ -261,11 +262,11 @@ const LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({
}

if (canRunSingleQuery) {
return setQueryType('query');
return setValue('queryType', 'query');
}

if (canRunPacks) {
return setQueryType('pack');
return setValue('queryType', 'pack');
}
}
}, [canRunPacks, canRunSingleQuery, defaultValue, isPackDataFetched, packsData?.data, setValue]);
Expand Down Expand Up @@ -293,12 +294,7 @@ const LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({
<FormProvider {...hooksForm}>
<EuiFlexGroup direction="column" css={groupStyles}>
{queryField && (
<QueryPackSelectable
queryType={queryType}
setQueryType={setQueryType}
canRunPacks={canRunPacks}
canRunSingleQuery={canRunSingleQuery}
/>
<QueryPackSelectable canRunPacks={canRunPacks} canRunSingleQuery={canRunSingleQuery} />
)}
{!hideAgentsField && (
<EuiFlexItem>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import { isEmpty } from 'lodash';
import type { EuiAccordionProps } from '@elastic/eui';
import { EuiCodeBlock, EuiFormRow, EuiAccordion, EuiSpacer } from '@elastic/eui';
import React, { useCallback, useMemo, useState } from 'react';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import styled from 'styled-components';
import { useController, useFormContext } from 'react-hook-form';
import { i18n } from '@kbn/i18n';
Expand All @@ -30,8 +30,8 @@ const StyledEuiCodeBlock = styled(EuiCodeBlock)`
`;

export interface LiveQueryQueryFieldProps {
disabled?: boolean;
handleSubmitForm?: () => void;
disabled?: boolean;
}

const LiveQueryQueryFieldComponent: React.FC<LiveQueryQueryFieldProps> = ({
Expand All @@ -42,7 +42,7 @@ const LiveQueryQueryFieldComponent: React.FC<LiveQueryQueryFieldProps> = ({
const [advancedContentState, setAdvancedContentState] =
useState<EuiAccordionProps['forceState']>('closed');
const permissions = useKibana().services.application.capabilities.osquery;
const queryType = watch('queryType', 'query');
const [ecsMapping, queryType] = watch(['ecs_mapping', 'queryType']);

const {
field: { onChange, value },
Expand All @@ -60,6 +60,12 @@ const LiveQueryQueryFieldComponent: React.FC<LiveQueryQueryFieldProps> = ({
defaultValue: '',
});

useEffect(() => {
if (!isEmpty(ecsMapping) && advancedContentState === 'closed') {
setAdvancedContentState('open');
}
}, [advancedContentState, ecsMapping]);

const handleSavedQueryChange: SavedQueriesDropdownProps['onChange'] = useCallback(
(savedQuery) => {
if (savedQuery) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { EuiCard, EuiFlexGroup, EuiFlexItem, EuiFormRow } from '@elastic/eui';
import React, { useCallback, useMemo } from 'react';
import { i18n } from '@kbn/i18n';
import styled from 'styled-components';
import { useController } from 'react-hook-form';

const StyledEuiCard = styled(EuiCard)`
padding: 16px 92px 16px 16px !important;
Expand Down Expand Up @@ -49,28 +50,29 @@ const StyledEuiCard = styled(EuiCard)`
`;

interface QueryPackSelectableProps {
queryType: string;
setQueryType: (type: string) => void;
canRunSingleQuery: boolean;
canRunPacks: boolean;
resetFormFields?: () => void;
}

export const QueryPackSelectable = ({
queryType,
setQueryType,
canRunSingleQuery,
canRunPacks,
resetFormFields,
}: QueryPackSelectableProps) => {
const {
field: { value: queryType, onChange: setQueryType },
} = useController({
name: 'queryType',
defaultValue: 'query',
rules: {
deps: ['packId', 'query'],
},
});

const handleChange = useCallback(
(type) => {
setQueryType(type);
if (resetFormFields) {
resetFormFields();
}
},
[resetFormFields, setQueryType]
[setQueryType]
);
const queryCardSelectable = useMemo(
() => ({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import {
reduce,
trim,
get,
reject,
} from 'lodash';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import type { EuiComboBoxProps, EuiComboBoxOptionOption } from '@elastic/eui';
Expand All @@ -40,7 +39,7 @@ import { i18n } from '@kbn/i18n';
import styled from 'styled-components';
import deepEqual from 'fast-deep-equal';

import type { InternalFieldErrors, UseFieldArrayRemove, UseFormReturn } from 'react-hook-form';
import type { FieldErrors, UseFieldArrayRemove, UseFormReturn } from 'react-hook-form';
import { useForm, useController, useFieldArray, useFormContext } from 'react-hook-form';
import type { ECSMapping } from '@kbn/osquery-io-ts-types';

Expand Down Expand Up @@ -594,6 +593,7 @@ const OsqueryColumnFieldComponent: React.FC<OsqueryColumnFieldProps> = ({
idAria={idAria}
helpText={selectedOptions[0]?.value?.description}
{...euiFieldProps}
data-test-subj="osqueryColumnValueSelect"
options={(resultTypeField.value === 'field' && euiFieldProps.options) || EMPTY_ARRAY}
/>
</EuiFlexItem>
Expand Down Expand Up @@ -731,12 +731,14 @@ interface OsqueryColumn {

export const ECSMappingEditorField = React.memo(({ euiFieldProps }: ECSMappingEditorFieldProps) => {
const {
setError,
clearErrors,
watch: watchRoot,
register: registerRoot,
setValue: setValueRoot,
formState: { errors: errorsRoot },
} = useFormContext<{ query: string; ecs_mapping: ECSMapping }>();

const latestErrors = useRef<FieldErrors<ECSMappingArray> | undefined>(undefined);
const [query, ecsMapping] = watchRoot(['query', 'ecs_mapping']);
const { control, trigger, watch, formState, resetField, getFieldState } = useForm<{
ecsMappingArray: ECSMappingArray;
Expand All @@ -759,14 +761,20 @@ export const ECSMappingEditorField = React.memo(({ euiFieldProps }: ECSMappingEd
const [osquerySchemaOptions, setOsquerySchemaOptions] = useState<OsquerySchemaOption[]>([]);

useEffect(() => {
registerRoot('ecs_mapping', {
validate: () => {
const nonEmptyErrors = reject(ecsMappingArrayState.error, isEmpty) as InternalFieldErrors[];
registerRoot('ecs_mapping');
}, [registerRoot]);

return !nonEmptyErrors.length;
},
});
}, [ecsMappingArrayState.error, errorsRoot, registerRoot]);
useEffect(() => {
if (!deepEqual(latestErrors.current, formState.errors.ecsMappingArray)) {
// @ts-expect-error update types
latestErrors.current = formState.errors.ecsMappingArray;
if (formState.errors.ecsMappingArray?.length && formState.errors.ecsMappingArray[0]?.key) {
setError('ecs_mapping', formState.errors.ecsMappingArray[0].key);
} else {
clearErrors('ecs_mapping');
}
}
}, [formState, setError, clearErrors]);

useEffect(() => {
const subscription = watchRoot((data, payload) => {
Expand Down
Loading

0 comments on commit 1cb49bb

Please sign in to comment.