mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
Co-authored-by: Patryk Kopycinski <contact@patrykkopycinski.com>
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
(cherry picked from commit 01113b265b
)
Co-authored-by: Tomasz Ciecierski <tomasz.ciecierski@elastic.co>
Co-authored-by: Patryk Kopyciński <contact@patrykkopycinski.com>
This commit is contained in:
parent
4453816724
commit
cd91e866ae
25 changed files with 364 additions and 117 deletions
|
@ -8,7 +8,12 @@
|
|||
import { ROLES } from '../../test';
|
||||
import { ArchiverMethod, runKbnArchiverScript } from '../../tasks/archiver';
|
||||
import { login } from '../../tasks/login';
|
||||
import { findAndClickButton, findFormFieldByRowsLabelAndType } from '../../tasks/live_query';
|
||||
import {
|
||||
checkResults,
|
||||
findAndClickButton,
|
||||
findFormFieldByRowsLabelAndType,
|
||||
submitQuery,
|
||||
} from '../../tasks/live_query';
|
||||
import { preparePack } from '../../tasks/packs';
|
||||
import { closeModalIfVisible } from '../../tasks/integrations';
|
||||
import { navigateTo } from '../../tasks/navigation';
|
||||
|
@ -18,43 +23,76 @@ describe('Alert_Test', () => {
|
|||
runKbnArchiverScript(ArchiverMethod.LOAD, 'pack');
|
||||
runKbnArchiverScript(ArchiverMethod.LOAD, 'rule');
|
||||
});
|
||||
beforeEach(() => {
|
||||
login(ROLES.alert_test);
|
||||
});
|
||||
|
||||
after(() => {
|
||||
runKbnArchiverScript(ArchiverMethod.UNLOAD, 'pack');
|
||||
runKbnArchiverScript(ArchiverMethod.UNLOAD, 'rule');
|
||||
});
|
||||
|
||||
it('should be able to run live query', () => {
|
||||
const PACK_NAME = 'testpack';
|
||||
const RULE_NAME = 'Test-rule';
|
||||
navigateTo('/app/osquery');
|
||||
preparePack(PACK_NAME);
|
||||
findAndClickButton('Edit');
|
||||
cy.contains(`Edit ${PACK_NAME}`);
|
||||
findFormFieldByRowsLabelAndType(
|
||||
'Scheduled agent policies (optional)',
|
||||
'fleet server {downArrow}{enter}'
|
||||
);
|
||||
findAndClickButton('Update pack');
|
||||
closeModalIfVisible();
|
||||
cy.contains(PACK_NAME);
|
||||
cy.visit('/app/security/rules');
|
||||
cy.contains(RULE_NAME).click();
|
||||
cy.wait(2000);
|
||||
cy.getBySel('ruleSwitch').should('have.attr', 'aria-checked', 'true');
|
||||
cy.getBySel('ruleSwitch').click();
|
||||
cy.getBySel('ruleSwitch').should('have.attr', 'aria-checked', 'false');
|
||||
cy.getBySel('ruleSwitch').click();
|
||||
cy.getBySel('ruleSwitch').should('have.attr', 'aria-checked', 'true');
|
||||
cy.visit('/app/security/alerts');
|
||||
cy.getBySel('expand-event').first().click();
|
||||
cy.getBySel('take-action-dropdown-btn').click();
|
||||
cy.getBySel('osquery-action-item').click();
|
||||
describe('alert_test role', () => {
|
||||
it('should not be able to run live query', () => {
|
||||
login(ROLES.alert_test);
|
||||
|
||||
cy.contains('Run Osquery');
|
||||
cy.contains('Permission denied');
|
||||
const PACK_NAME = 'testpack';
|
||||
const RULE_NAME = 'Test-rule';
|
||||
navigateTo('/app/osquery');
|
||||
preparePack(PACK_NAME);
|
||||
findAndClickButton('Edit');
|
||||
cy.contains(`Edit ${PACK_NAME}`);
|
||||
findFormFieldByRowsLabelAndType(
|
||||
'Scheduled agent policies (optional)',
|
||||
'fleet server {downArrow}{enter}'
|
||||
);
|
||||
findAndClickButton('Update pack');
|
||||
closeModalIfVisible();
|
||||
cy.contains(PACK_NAME);
|
||||
cy.visit('/app/security/rules');
|
||||
cy.contains(RULE_NAME).click();
|
||||
cy.wait(2000);
|
||||
cy.getBySel('ruleSwitch').should('have.attr', 'aria-checked', 'true');
|
||||
cy.getBySel('ruleSwitch').click();
|
||||
cy.getBySel('ruleSwitch').should('have.attr', 'aria-checked', 'false');
|
||||
cy.getBySel('ruleSwitch').click();
|
||||
cy.getBySel('ruleSwitch').should('have.attr', 'aria-checked', 'true');
|
||||
cy.visit('/app/security/alerts');
|
||||
cy.getBySel('expand-event').first().click();
|
||||
cy.getBySel('take-action-dropdown-btn').click();
|
||||
cy.getBySel('osquery-action-item').click();
|
||||
|
||||
cy.contains('Run Osquery');
|
||||
cy.contains('Permission denied');
|
||||
});
|
||||
});
|
||||
|
||||
describe('t1_analyst role', () => {
|
||||
it('should be able to run rule investigation guide query', () => {
|
||||
login(ROLES.t1_analyst);
|
||||
|
||||
navigateTo('/app/osquery');
|
||||
|
||||
cy.visit('/app/security/alerts');
|
||||
cy.getBySel('expand-event').first().click();
|
||||
|
||||
cy.contains('Get processes').click();
|
||||
submitQuery();
|
||||
checkResults();
|
||||
});
|
||||
|
||||
it('should not be able to run custom query', () => {
|
||||
login(ROLES.t1_analyst);
|
||||
|
||||
navigateTo('/app/osquery');
|
||||
|
||||
cy.visit('/app/security/alerts');
|
||||
cy.getBySel('expand-event').first().click();
|
||||
|
||||
cy.contains('Get processes').click();
|
||||
|
||||
cy.intercept('POST', '/api/osquery/live_queries', (req) => {
|
||||
req.body.query = 'select * from processes limit 10';
|
||||
});
|
||||
submitQuery();
|
||||
cy.contains('Forbidden');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -50,7 +50,7 @@ describe('T1 Analyst - READ + runSavedQueries ', () => {
|
|||
cy.contains('New live query').should('not.be.disabled');
|
||||
cy.contains('select * from uptime');
|
||||
cy.wait(1000);
|
||||
cy.react('EuiTableBody').first().react('DefaultItemAction').first().click();
|
||||
cy.react('EuiTableBody').first().react('CustomItemAction').first().click();
|
||||
cy.contains(SAVED_QUERY_ID);
|
||||
submitQuery();
|
||||
checkResults();
|
||||
|
|
|
@ -47,7 +47,8 @@
|
|||
"winlogbeat-*"
|
||||
],
|
||||
"query": "_id:*",
|
||||
"filters": []
|
||||
"filters": [],
|
||||
"note": "!{osquery{\"query\":\"SELECT * FROM processes;\",\"label\":\"Get processes\",\"ecs_mapping\":{\"process.pid\":{\"field\":\"pid\"},\"process.name\":{\"field\":\"name\"},\"process.executable\":{\"field\":\"path\"},\"process.args\":{\"field\":\"cmdline\"},\"process.working_directory\":{\"field\":\"cwd\"},\"user.id\":{\"field\":\"uid\"},\"group.id\":{\"field\":\"gid\"},\"process.parent.pid\":{\"field\":\"parent\"},\"process.pgid\":{\"field\":\"pgroup\"}}}}\n\n!{osquery{\"query\":\"select * from users;\",\"label\":\"Get users\"}}"
|
||||
},
|
||||
"schedule": {
|
||||
"interval": "5m"
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
"navigation",
|
||||
"taskManager",
|
||||
"triggersActionsUi",
|
||||
"ruleRegistry",
|
||||
"security"
|
||||
],
|
||||
"server": true,
|
||||
|
|
|
@ -15,6 +15,7 @@ import {
|
|||
EuiIcon,
|
||||
EuiFlexItem,
|
||||
EuiFlexGroup,
|
||||
EuiToolTip,
|
||||
} from '@elastic/eui';
|
||||
import React, { useState, useCallback, useMemo } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
|
@ -34,7 +35,18 @@ interface ActionTableResultsButtonProps {
|
|||
const ActionTableResultsButton: React.FC<ActionTableResultsButtonProps> = ({ actionId }) => {
|
||||
const navProps = useRouterNavigate(`live_queries/${actionId}`);
|
||||
|
||||
return <EuiButtonIcon iconType="visTable" {...navProps} />;
|
||||
const detailsText = i18n.translate(
|
||||
'xpack.osquery.liveQueryActions.table.viewDetailsActionButton',
|
||||
{
|
||||
defaultMessage: 'Details',
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiToolTip position="top" content={detailsText}>
|
||||
<EuiButtonIcon iconType="visTable" {...navProps} aria-label={detailsText} />
|
||||
</EuiToolTip>
|
||||
);
|
||||
};
|
||||
|
||||
ActionTableResultsButton.displayName = 'ActionTableResultsButton';
|
||||
|
@ -100,7 +112,7 @@ const ActionsTableComponent = () => {
|
|||
);
|
||||
|
||||
const handlePlayClick = useCallback(
|
||||
(item) => {
|
||||
(item) => () => {
|
||||
const packId = item._source.pack_id;
|
||||
|
||||
if (packId) {
|
||||
|
@ -139,6 +151,25 @@ const ActionsTableComponent = () => {
|
|||
},
|
||||
[push]
|
||||
);
|
||||
const renderPlayButton = useCallback(
|
||||
(item, enabled) => {
|
||||
const playText = i18n.translate('xpack.osquery.liveQueryActions.table.runActionAriaLabel', {
|
||||
defaultMessage: 'Run query',
|
||||
});
|
||||
|
||||
return (
|
||||
<EuiToolTip position="top" content={playText}>
|
||||
<EuiButtonIcon
|
||||
iconType="play"
|
||||
onClick={handlePlayClick(item)}
|
||||
isDisabled={!enabled}
|
||||
aria-label={playText}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
);
|
||||
},
|
||||
[handlePlayClick]
|
||||
);
|
||||
|
||||
const existingPackIds = useMemo(() => map(packsData?.data ?? [], 'id'), [packsData]);
|
||||
|
||||
|
@ -197,10 +228,8 @@ const ActionsTableComponent = () => {
|
|||
}),
|
||||
actions: [
|
||||
{
|
||||
type: 'icon',
|
||||
icon: 'play',
|
||||
onClick: handlePlayClick,
|
||||
available: isPlayButtonAvailable,
|
||||
render: renderPlayButton,
|
||||
},
|
||||
{
|
||||
render: renderActionsColumn,
|
||||
|
@ -209,11 +238,11 @@ const ActionsTableComponent = () => {
|
|||
},
|
||||
],
|
||||
[
|
||||
handlePlayClick,
|
||||
isPlayButtonAvailable,
|
||||
renderActionsColumn,
|
||||
renderAgentsColumn,
|
||||
renderCreatedByColumn,
|
||||
renderPlayButton,
|
||||
renderQueryColumn,
|
||||
renderTimestampColumn,
|
||||
]
|
||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -29,6 +29,7 @@ import { savedQueryDataSerializer } from '../../saved_queries/form/use_saved_que
|
|||
import { PackFieldWrapper } from '../../shared_components/osquery_response_action_type/pack_field_wrapper';
|
||||
|
||||
export interface LiveQueryFormFields {
|
||||
alertIds?: string[];
|
||||
query?: string;
|
||||
agentSelection: AgentSelection;
|
||||
savedQueryId?: string | null;
|
||||
|
@ -39,6 +40,7 @@ export interface LiveQueryFormFields {
|
|||
interface DefaultLiveQueryFormFields {
|
||||
query?: string;
|
||||
agentSelection?: AgentSelection;
|
||||
alertIds?: string[];
|
||||
savedQueryId?: string | null;
|
||||
ecs_mapping?: ECSMapping;
|
||||
packId?: string;
|
||||
|
@ -119,6 +121,7 @@ const LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({
|
|||
|
||||
useEffect(() => {
|
||||
register('savedQueryId');
|
||||
register('alertIds');
|
||||
}, [register]);
|
||||
|
||||
const queryStatus = useMemo(() => {
|
||||
|
@ -135,19 +138,20 @@ const LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({
|
|||
);
|
||||
|
||||
const onSubmit = useCallback(
|
||||
(values: LiveQueryFormFields) => {
|
||||
async (values: LiveQueryFormFields) => {
|
||||
const serializedData = pickBy(
|
||||
{
|
||||
agentSelection: values.agentSelection,
|
||||
saved_query_id: values.savedQueryId,
|
||||
query: values.query,
|
||||
alert_ids: values.alertIds,
|
||||
pack_id: values?.packId?.length ? values?.packId[0] : undefined,
|
||||
ecs_mapping: values.ecs_mapping,
|
||||
},
|
||||
(value) => !isEmpty(value)
|
||||
) as unknown as LiveQueryFormFields;
|
||||
|
||||
mutateAsync(serializedData);
|
||||
await mutateAsync(serializedData);
|
||||
},
|
||||
[mutateAsync]
|
||||
);
|
||||
|
@ -159,8 +163,6 @@ const LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({
|
|||
|
||||
const { data: packsData, isFetched: isPackDataFetched } = usePacks({});
|
||||
|
||||
const handleSubmitForm = useMemo(() => handleSubmit(onSubmit), [handleSubmit, onSubmit]);
|
||||
|
||||
const submitButtonContent = useMemo(
|
||||
() => (
|
||||
<EuiFlexItem>
|
||||
|
@ -181,8 +183,9 @@ const LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({
|
|||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
id="submit-button"
|
||||
disabled={!enabled || isSubmitting}
|
||||
onClick={handleSubmitForm}
|
||||
disabled={!enabled}
|
||||
isLoading={isSubmitting}
|
||||
onClick={handleSubmit(onSubmit)}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.liveQueryForm.form.submitButtonLabel"
|
||||
|
@ -201,7 +204,8 @@ const LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({
|
|||
handleShowSaveQueryFlyout,
|
||||
enabled,
|
||||
isSubmitting,
|
||||
handleSubmitForm,
|
||||
handleSubmit,
|
||||
onSubmit,
|
||||
]
|
||||
);
|
||||
|
||||
|
@ -256,6 +260,10 @@ const LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({
|
|||
setValue('agentSelection', defaultValue.agentSelection);
|
||||
}
|
||||
|
||||
if (defaultValue?.alertIds?.length) {
|
||||
setValue('alertIds', defaultValue.alertIds);
|
||||
}
|
||||
|
||||
if (defaultValue?.packId && canRunPacks) {
|
||||
setQueryType('pack');
|
||||
|
||||
|
@ -297,6 +305,7 @@ const LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({
|
|||
resetField('query');
|
||||
resetField('ecs_mapping');
|
||||
resetField('savedQueryId');
|
||||
resetField('alertIds');
|
||||
clearErrors();
|
||||
}
|
||||
}, [queryType, cleanupLiveQuery, resetField, setValue, clearErrors, defaultValue]);
|
||||
|
@ -329,7 +338,7 @@ const LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({
|
|||
) : (
|
||||
<>
|
||||
<EuiFlexItem>
|
||||
<LiveQueryQueryField handleSubmitForm={handleSubmitForm} />
|
||||
<LiveQueryQueryField handleSubmitForm={handleSubmit(onSubmit)} />
|
||||
</EuiFlexItem>
|
||||
{submitButtonContent}
|
||||
<EuiFlexItem>{resultsStepContent}</EuiFlexItem>
|
||||
|
|
|
@ -21,6 +21,7 @@ import type { AgentSelection } from '../agents/types';
|
|||
interface LiveQueryProps {
|
||||
agentId?: string;
|
||||
agentIds?: string[];
|
||||
alertIds?: string[];
|
||||
agentPolicyIds?: string[];
|
||||
onSuccess?: () => void;
|
||||
query?: string;
|
||||
|
@ -40,6 +41,7 @@ interface LiveQueryProps {
|
|||
const LiveQueryComponent: React.FC<LiveQueryProps> = ({
|
||||
agentId,
|
||||
agentIds,
|
||||
alertIds,
|
||||
agentPolicyIds,
|
||||
onSuccess,
|
||||
query,
|
||||
|
@ -77,6 +79,7 @@ const LiveQueryComponent: React.FC<LiveQueryProps> = ({
|
|||
const defaultValue = useMemo(() => {
|
||||
const initialValue = {
|
||||
...(initialAgentSelection ? { agentSelection: initialAgentSelection } : {}),
|
||||
alertIds,
|
||||
query,
|
||||
savedQueryId,
|
||||
ecs_mapping,
|
||||
|
@ -84,7 +87,7 @@ const LiveQueryComponent: React.FC<LiveQueryProps> = ({
|
|||
};
|
||||
|
||||
return !isEmpty(pickBy(initialValue, (value) => !isEmpty(value))) ? initialValue : undefined;
|
||||
}, [ecs_mapping, initialAgentSelection, packId, query, savedQueryId]);
|
||||
}, [alertIds, ecs_mapping, initialAgentSelection, packId, query, savedQueryId]);
|
||||
|
||||
if (isLoading) {
|
||||
return <EuiLoadingContent lines={10} />;
|
||||
|
|
|
@ -126,9 +126,20 @@ const PacksTableComponent = () => {
|
|||
);
|
||||
|
||||
const renderPlayAction = useCallback(
|
||||
(item, enabled) => (
|
||||
<EuiButtonIcon iconType="play" onClick={handlePlayClick(item)} isDisabled={!enabled} />
|
||||
),
|
||||
(item, enabled) => {
|
||||
const playText = i18n.translate('xpack.osquery.packs.table.runActionAriaLabel', {
|
||||
defaultMessage: 'Run {packName}',
|
||||
values: {
|
||||
packName: item.attributes.name,
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<EuiToolTip position="top" content={playText}>
|
||||
<EuiButtonIcon iconType="play" onClick={handlePlayClick(item)} isDisabled={!enabled} />
|
||||
</EuiToolTip>
|
||||
);
|
||||
},
|
||||
[handlePlayClick]
|
||||
);
|
||||
|
||||
|
|
|
@ -49,7 +49,7 @@ import {
|
|||
convertECSMappingToArray,
|
||||
convertECSMappingToObject,
|
||||
} from '../../../common/schemas/common/utils';
|
||||
import ECSSchema from '../../common/schemas/ecs/v8.4.0.json';
|
||||
import ECSSchema from '../../common/schemas/ecs/v8.5.0.json';
|
||||
import osquerySchema from '../../common/schemas/osquery/v5.4.0.json';
|
||||
|
||||
import { FieldIcon } from '../../common/lib/kibana';
|
||||
|
@ -728,19 +728,13 @@ 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 }>();
|
||||
|
||||
useEffect(() => {
|
||||
registerRoot('ecs_mapping');
|
||||
}, [registerRoot]);
|
||||
|
||||
const [query, ecsMapping] = watchRoot(['query', 'ecs_mapping'], { ecs_mapping: {} });
|
||||
const [query, ecsMapping] = watchRoot(['query', 'ecs_mapping']);
|
||||
const { control, trigger, watch, formState, resetField, getFieldState } = useForm<{
|
||||
ecsMappingArray: ECSMappingArray;
|
||||
}>({
|
||||
|
@ -761,6 +755,16 @@ export const ECSMappingEditorField = React.memo(({ euiFieldProps }: ECSMappingEd
|
|||
const ecsMappingArrayState = getFieldState('ecsMappingArray', formState);
|
||||
const [osquerySchemaOptions, setOsquerySchemaOptions] = useState<OsquerySchemaOption[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
registerRoot('ecs_mapping', {
|
||||
validate: () => {
|
||||
const nonEmptyErrors = reject(ecsMappingArrayState.error, isEmpty) as InternalFieldErrors[];
|
||||
|
||||
return !nonEmptyErrors.length;
|
||||
},
|
||||
});
|
||||
}, [ecsMappingArrayState.error, errorsRoot, registerRoot]);
|
||||
|
||||
useEffect(() => {
|
||||
const subscription = watchRoot((data, payload) => {
|
||||
if (payload.name === 'ecs_mapping') {
|
||||
|
@ -1019,10 +1023,16 @@ export const ECSMappingEditorField = React.memo(({ euiFieldProps }: ECSMappingEd
|
|||
orderBy(suggestions, ['value.suggestion_label', 'value.tableOrder'], ['asc', 'desc']),
|
||||
'label'
|
||||
);
|
||||
setOsquerySchemaOptions((prevValue) =>
|
||||
!deepEqual(prevValue, newOptions) ? newOptions : prevValue
|
||||
);
|
||||
}, [query]);
|
||||
setOsquerySchemaOptions((prevValue) => {
|
||||
if (!deepEqual(prevValue, newOptions)) {
|
||||
trigger();
|
||||
|
||||
return newOptions;
|
||||
}
|
||||
|
||||
return prevValue;
|
||||
});
|
||||
}, [query, trigger]);
|
||||
|
||||
useEffect(() => {
|
||||
const parsedMapping = convertECSMappingToObject(formValue.ecsMappingArray);
|
||||
|
@ -1033,27 +1043,6 @@ export const ECSMappingEditorField = React.memo(({ euiFieldProps }: ECSMappingEd
|
|||
}
|
||||
}, [setValueRoot, formValue, ecsMappingArrayState.isDirty, ecsMapping]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!formState.isValid) {
|
||||
const nonEmptyErrors = reject(ecsMappingArrayState.error, isEmpty) as InternalFieldErrors[];
|
||||
if (nonEmptyErrors.length) {
|
||||
setError('ecs_mapping', {
|
||||
type: nonEmptyErrors[0].key?.type ?? 'custom',
|
||||
message: nonEmptyErrors[0].key?.message ?? '',
|
||||
});
|
||||
}
|
||||
} else {
|
||||
clearErrors('ecs_mapping');
|
||||
}
|
||||
}, [
|
||||
errorsRoot,
|
||||
clearErrors,
|
||||
formState.isValid,
|
||||
formState.errors,
|
||||
setError,
|
||||
ecsMappingArrayState.error,
|
||||
]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiFlexGroup>
|
||||
|
|
|
@ -62,9 +62,9 @@ const QueryFlyoutComponent: React.FC<QueryFlyoutProps> = ({
|
|||
formState: { isSubmitting },
|
||||
resetField,
|
||||
} = hooksForm;
|
||||
const onSubmit = (payload: PackQueryFormData) => {
|
||||
const onSubmit = async (payload: PackQueryFormData) => {
|
||||
const serializedData: PackSOQueryFormData = serializer(payload);
|
||||
onSave(serializedData);
|
||||
await onSave(serializedData);
|
||||
onClose();
|
||||
};
|
||||
|
||||
|
|
|
@ -49,10 +49,10 @@ const EditSavedQueryFormComponent: React.FC<EditSavedQueryFormProps> = ({
|
|||
formState: { isSubmitting },
|
||||
} = hooksForm;
|
||||
|
||||
const onSubmit = (payload: SavedQueryFormData) => {
|
||||
const onSubmit = async (payload: SavedQueryFormData) => {
|
||||
const serializedData = serializer(payload);
|
||||
try {
|
||||
handleSubmit(serializedData);
|
||||
await handleSubmit(serializedData);
|
||||
// eslint-disable-next-line no-empty
|
||||
} catch (e) {}
|
||||
};
|
||||
|
|
|
@ -61,19 +61,27 @@ const PlayButtonComponent: React.FC<PlayButtonProps> = ({ disabled = false, save
|
|||
[push, savedQuery]
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiButtonIcon
|
||||
color="primary"
|
||||
iconType="play"
|
||||
isDisabled={disabled}
|
||||
onClick={handlePlayClick}
|
||||
aria-label={i18n.translate('xpack.osquery.savedQueryList.queriesTable.runActionAriaLabel', {
|
||||
const playText = useMemo(
|
||||
() =>
|
||||
i18n.translate('xpack.osquery.savedQueryList.queriesTable.runActionAriaLabel', {
|
||||
defaultMessage: 'Run {savedQueryName}',
|
||||
values: {
|
||||
savedQueryName: savedQuery.attributes.name,
|
||||
savedQueryName: savedQuery.attributes.id,
|
||||
},
|
||||
})}
|
||||
/>
|
||||
}),
|
||||
[savedQuery]
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiToolTip position="top" content={playText}>
|
||||
<EuiButtonIcon
|
||||
color="primary"
|
||||
iconType="play"
|
||||
isDisabled={disabled}
|
||||
onClick={handlePlayClick}
|
||||
aria-label={playText}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -92,19 +100,27 @@ const EditButtonComponent: React.FC<EditButtonProps> = ({
|
|||
}) => {
|
||||
const buttonProps = useRouterNavigate(`saved_queries/${savedQueryId}`);
|
||||
|
||||
return (
|
||||
<EuiButtonIcon
|
||||
color="primary"
|
||||
{...buttonProps}
|
||||
iconType="pencil"
|
||||
isDisabled={disabled}
|
||||
aria-label={i18n.translate('xpack.osquery.savedQueryList.queriesTable.editActionAriaLabel', {
|
||||
const editText = useMemo(
|
||||
() =>
|
||||
i18n.translate('xpack.osquery.savedQueryList.queriesTable.editActionAriaLabel', {
|
||||
defaultMessage: 'Edit {savedQueryName}',
|
||||
values: {
|
||||
savedQueryName,
|
||||
},
|
||||
})}
|
||||
/>
|
||||
}),
|
||||
[savedQueryName]
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiToolTip position="top" content={editText}>
|
||||
<EuiButtonIcon
|
||||
color="primary"
|
||||
{...buttonProps}
|
||||
iconType="pencil"
|
||||
isDisabled={disabled}
|
||||
aria-label={editText}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -124,7 +140,7 @@ const SavedQueriesPageComponent = () => {
|
|||
|
||||
const renderEditAction = useCallback(
|
||||
(item: SavedQuerySO) => (
|
||||
<EditButton savedQueryId={item.id} savedQueryName={item.attributes.name} />
|
||||
<EditButton savedQueryId={item.id} savedQueryName={item.attributes.id} />
|
||||
),
|
||||
[]
|
||||
);
|
||||
|
|
|
@ -47,9 +47,9 @@ const NewSavedQueryFormComponent: React.FC<NewSavedQueryFormProps> = ({
|
|||
formState: { isSubmitting, errors },
|
||||
} = hooksForm;
|
||||
|
||||
const onSubmit = (payload: SavedQueryFormData) => {
|
||||
const onSubmit = async (payload: SavedQueryFormData) => {
|
||||
const serializedData = serializer(payload);
|
||||
handleSubmit(serializedData);
|
||||
await handleSubmit(serializedData);
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
|
@ -1,6 +1,15 @@
|
|||
{
|
||||
"elasticsearch": {
|
||||
"cluster": ["manage"],
|
||||
"indices": [
|
||||
{
|
||||
"names": [".items-*", ".lists-*", ".alerts-security.alerts-*", ".siem-signals-*"],
|
||||
"privileges": ["manage", "read", "write", "view_index_metadata", "maintenance"]
|
||||
},
|
||||
{
|
||||
"names": ["*"],
|
||||
"privileges": ["read"]
|
||||
},
|
||||
{
|
||||
"names": ["logs-osquery_manager*"],
|
||||
"privileges": ["read"]
|
||||
|
@ -10,6 +19,7 @@
|
|||
"kibana": [
|
||||
{
|
||||
"feature": {
|
||||
"siem": ["all"],
|
||||
"osquery": ["read", "run_saved_queries" ]
|
||||
},
|
||||
"spaces": ["*"]
|
||||
|
|
|
@ -40,7 +40,7 @@ const RESTRICTED_FIELDS = [
|
|||
|
||||
run(
|
||||
async ({ flags }) => {
|
||||
const schemaPath = path.resolve(`../../public/common/schemas/ecs/`);
|
||||
const schemaPath = path.resolve(`./public/common/schemas/ecs/`);
|
||||
const schemaFile = path.join(schemaPath, flags.schema_version as string);
|
||||
const schemaData = await require(schemaFile);
|
||||
|
||||
|
|
|
@ -14,6 +14,7 @@ import type {
|
|||
AgentPolicyServiceInterface,
|
||||
PackagePolicyClient,
|
||||
} from '@kbn/fleet-plugin/server';
|
||||
import type { RuleRegistryPluginStartContract } from '@kbn/rule-registry-plugin/server';
|
||||
import type { ConfigType } from '../../common/config';
|
||||
import type { TelemetryEventsSender } from './telemetry/sender';
|
||||
|
||||
|
@ -26,6 +27,7 @@ export type OsqueryAppContextServiceStartContract = Partial<
|
|||
logger: Logger;
|
||||
config: ConfigType;
|
||||
registerIngestCallback?: FleetStartContract['registerExternalCallback'];
|
||||
ruleRegistryService?: RuleRegistryPluginStartContract;
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -37,12 +39,14 @@ export class OsqueryAppContextService {
|
|||
private packageService: PackageService | undefined;
|
||||
private packagePolicyService: PackagePolicyClient | undefined;
|
||||
private agentPolicyService: AgentPolicyServiceInterface | undefined;
|
||||
private ruleRegistryService: RuleRegistryPluginStartContract | undefined;
|
||||
|
||||
public start(dependencies: OsqueryAppContextServiceStartContract) {
|
||||
this.agentService = dependencies.agentService;
|
||||
this.packageService = dependencies.packageService;
|
||||
this.packagePolicyService = dependencies.packagePolicyService;
|
||||
this.agentPolicyService = dependencies.agentPolicyService;
|
||||
this.ruleRegistryService = dependencies.ruleRegistryService;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
|
@ -63,6 +67,10 @@ export class OsqueryAppContextService {
|
|||
public getAgentPolicyService(): AgentPolicyServiceInterface | undefined {
|
||||
return this.agentPolicyService;
|
||||
}
|
||||
|
||||
public getRuleRegistryService(): RuleRegistryPluginStartContract | undefined {
|
||||
return this.ruleRegistryService;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -100,6 +100,7 @@ export class OsqueryPlugin implements Plugin<OsqueryPluginSetup, OsqueryPluginSt
|
|||
|
||||
this.osqueryAppContextService.start({
|
||||
...plugins.fleet,
|
||||
ruleRegistryService: plugins.ruleRegistry,
|
||||
// @ts-expect-error update types
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
config: this.config!,
|
||||
|
|
|
@ -6,12 +6,18 @@
|
|||
*/
|
||||
|
||||
import type { IRouter } from '@kbn/core/server';
|
||||
import unified from 'unified';
|
||||
import markdown from 'remark-parse';
|
||||
import { some, filter } from 'lodash';
|
||||
import deepEqual from 'fast-deep-equal';
|
||||
|
||||
import type { ECSMappingOrUndefined } from '@kbn/osquery-io-ts-types';
|
||||
import { createLiveQueryRequestBodySchema } from '../../../common/schemas/routes/live_query';
|
||||
import type { CreateLiveQueryRequestBodySchema } from '../../../common/schemas/routes/live_query';
|
||||
import { buildRouteValidation } from '../../utils/build_validation/route_validation';
|
||||
import type { OsqueryAppContext } from '../../lib/osquery_app_context_services';
|
||||
import { createActionHandler } from '../../handlers';
|
||||
import { parser as OsqueryParser } from './osquery_parser';
|
||||
|
||||
export const createLiveQueryRoute = (router: IRouter, osqueryContext: OsqueryAppContext) => {
|
||||
router.post(
|
||||
|
@ -37,7 +43,41 @@ export const createLiveQueryRoute = (router: IRouter, osqueryContext: OsqueryApp
|
|||
);
|
||||
|
||||
if (isInvalid) {
|
||||
return response.forbidden();
|
||||
if (request.body.alert_ids?.length) {
|
||||
try {
|
||||
const client = await osqueryContext.service
|
||||
.getRuleRegistryService()
|
||||
?.getRacClientWithRequest(request);
|
||||
|
||||
const alertData = await client?.get({ id: request.body.alert_ids[0] });
|
||||
|
||||
if (alertData?.['kibana.alert.rule.note']) {
|
||||
const parsedAlertInvestigationGuide = unified()
|
||||
.use([[markdown, {}], OsqueryParser])
|
||||
.parse(alertData?.['kibana.alert.rule.note']);
|
||||
|
||||
const osqueryQueries = filter(parsedAlertInvestigationGuide?.children as object, [
|
||||
'type',
|
||||
'osquery',
|
||||
]);
|
||||
|
||||
const requestQueryExistsInTheInvestigationGuide = some(
|
||||
osqueryQueries,
|
||||
(payload: {
|
||||
configuration: { query: string; ecs_mapping: ECSMappingOrUndefined };
|
||||
}) =>
|
||||
payload?.configuration?.query === request.body.query &&
|
||||
deepEqual(payload?.configuration?.ecs_mapping, request.body.ecs_mapping)
|
||||
);
|
||||
|
||||
if (!requestQueryExistsInTheInvestigationGuide) throw new Error();
|
||||
}
|
||||
} catch (error) {
|
||||
return response.forbidden();
|
||||
}
|
||||
} else {
|
||||
return response.forbidden();
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
|
|
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import type { RemarkTokenizer } from '@elastic/eui';
|
||||
import type { Plugin } from 'unified';
|
||||
|
||||
export const parser: Plugin = function () {
|
||||
const Parser = this.Parser;
|
||||
const tokenizers = Parser.prototype.blockTokenizers;
|
||||
const methods = Parser.prototype.blockMethods;
|
||||
|
||||
const tokenizeOsquery: RemarkTokenizer = function (eat, value, silent) {
|
||||
if (value.startsWith('!{osquery') === false) return false;
|
||||
|
||||
const nextChar = value[9];
|
||||
|
||||
if (nextChar !== '{' && nextChar !== '}') return false; // this isn't actually a osquery
|
||||
|
||||
if (silent) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// is there a configuration?
|
||||
const hasConfiguration = nextChar === '{';
|
||||
|
||||
let match = '!{osquery';
|
||||
let configuration = {};
|
||||
|
||||
if (hasConfiguration) {
|
||||
let configurationString = '';
|
||||
|
||||
let openObjects = 0;
|
||||
|
||||
for (let i = 9; i < value.length; i++) {
|
||||
const char = value[i];
|
||||
if (char === '{') {
|
||||
openObjects++;
|
||||
configurationString += char;
|
||||
} else if (char === '}') {
|
||||
openObjects--;
|
||||
if (openObjects === -1) {
|
||||
break;
|
||||
}
|
||||
|
||||
configurationString += char;
|
||||
} else {
|
||||
configurationString += char;
|
||||
}
|
||||
}
|
||||
|
||||
match += configurationString;
|
||||
try {
|
||||
configuration = JSON.parse(configurationString);
|
||||
} catch (e) {
|
||||
const now = eat.now();
|
||||
this.file.fail(`Unable to parse osquery JSON configuration: ${e}`, {
|
||||
line: now.line,
|
||||
column: now.column + 9,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
match += '}';
|
||||
|
||||
return eat(match)({
|
||||
type: 'osquery',
|
||||
configuration,
|
||||
});
|
||||
};
|
||||
|
||||
tokenizers.osquery = tokenizeOsquery;
|
||||
methods.splice(methods.indexOf('text'), 0, 'osquery');
|
||||
};
|
|
@ -20,6 +20,7 @@ import type {
|
|||
TaskManagerStartContract as TaskManagerPluginStart,
|
||||
} from '@kbn/task-manager-plugin/server';
|
||||
import type { PluginStart as DataViewsPluginStart } from '@kbn/data-views-plugin/server';
|
||||
import type { RuleRegistryPluginStartContract } from '@kbn/rule-registry-plugin/server';
|
||||
import type { CreateLiveQueryRequestBodySchema } from '../common/schemas/routes/live_query';
|
||||
|
||||
export interface OsqueryPluginSetup {
|
||||
|
@ -46,4 +47,5 @@ export interface StartPlugins {
|
|||
fleet?: FleetStartContract;
|
||||
taskManager?: TaskManagerPluginStart;
|
||||
telemetry?: TelemetryPluginStart;
|
||||
ruleRegistry?: RuleRegistryPluginStartContract;
|
||||
}
|
||||
|
|
|
@ -261,7 +261,7 @@ const RunOsqueryButtonRenderer = ({
|
|||
};
|
||||
}) => {
|
||||
const [showFlyout, setShowFlyout] = useState(false);
|
||||
const { agentId } = useContext(BasicAlertDataContext);
|
||||
const { agentId, alertId } = useContext(BasicAlertDataContext);
|
||||
|
||||
const handleOpen = useCallback(() => setShowFlyout(true), [setShowFlyout]);
|
||||
|
||||
|
@ -278,6 +278,7 @@ const RunOsqueryButtonRenderer = ({
|
|||
{showFlyout && (
|
||||
<OsqueryFlyout
|
||||
defaultValues={{
|
||||
...(alertId ? { alertIds: [alertId] } : {}),
|
||||
query: configuration.query,
|
||||
ecs_mapping: configuration.ecs_mapping,
|
||||
queryField: false,
|
||||
|
|
|
@ -72,6 +72,8 @@ const AlertContextMenuComponent: React.FC<AlertContextMenuProps & PropsFromRedux
|
|||
const onMenuItemClick = useCallback(() => {
|
||||
setPopover(false);
|
||||
}, []);
|
||||
|
||||
const alertId = ecsRowData?.kibana?.alert ? ecsRowData?._id : null;
|
||||
const ruleId = get(0, ecsRowData?.kibana?.alert?.rule?.uuid);
|
||||
const ruleName = get(0, ecsRowData?.kibana?.alert?.rule?.name);
|
||||
|
||||
|
@ -264,7 +266,11 @@ const AlertContextMenuComponent: React.FC<AlertContextMenuProps & PropsFromRedux
|
|||
<EventFiltersFlyout data={ecsRowData} onCancel={closeAddEventFilterModal} />
|
||||
)}
|
||||
{isOsqueryFlyoutOpen && agentId && ecsRowData != null && (
|
||||
<OsqueryFlyout agentId={agentId} onClose={handleOnOsqueryClick} />
|
||||
<OsqueryFlyout
|
||||
agentId={agentId}
|
||||
defaultValues={alertId ? { alertIds: [alertId] } : undefined}
|
||||
onClose={handleOnOsqueryClick}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -63,6 +63,7 @@ export const FlyoutFooterComponent = React.memo(
|
|||
timelineQuery,
|
||||
refetchFlyoutData,
|
||||
}: FlyoutFooterProps & PropsFromRedux) => {
|
||||
const alertId = detailsEcsData?.kibana?.alert ? detailsEcsData?._id : null;
|
||||
const ruleIndex = useMemo(
|
||||
() =>
|
||||
find({ category: 'signal', field: 'signal.rule.index' }, detailsData)?.values ??
|
||||
|
@ -173,7 +174,11 @@ export const FlyoutFooterComponent = React.memo(
|
|||
/>
|
||||
)}
|
||||
{isOsqueryFlyoutOpenWithAgentId && detailsEcsData != null && (
|
||||
<OsqueryFlyout agentId={isOsqueryFlyoutOpenWithAgentId} onClose={closeOsqueryFlyout} />
|
||||
<OsqueryFlyout
|
||||
agentId={isOsqueryFlyoutOpenWithAgentId}
|
||||
defaultValues={alertId ? { alertIds: [alertId] } : undefined}
|
||||
onClose={closeOsqueryFlyout}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue