[Osquery] Another batch of small fixes (#142193) (#142543)

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:
Kibana Machine 2022-10-04 03:46:59 -06:00 committed by GitHub
parent 4453816724
commit cd91e866ae
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 364 additions and 117 deletions

View file

@ -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');
});
});
});

View file

@ -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();

View file

@ -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"

View file

@ -19,6 +19,7 @@
"navigation",
"taskManager",
"triggersActionsUi",
"ruleRegistry",
"security"
],
"server": true,

View file

@ -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

View file

@ -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>

View file

@ -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} />;

View file

@ -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]
);

View file

@ -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>

View file

@ -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();
};

View file

@ -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) {}
};

View file

@ -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} />
),
[]
);

View file

@ -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 (

View file

@ -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": ["*"]

View file

@ -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);

View file

@ -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;
}
}
/**

View file

@ -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!,

View file

@ -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 {

View file

@ -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');
};

View file

@ -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;
}

View file

@ -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,

View file

@ -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}
/>
)}
</>
);

View file

@ -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}
/>
)}
</>
);