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

[Osquery] Add to timeline button #128596

Merged
merged 30 commits into from
Apr 29, 2022
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
9a0ea9f
[wip]
tomsonpl Mar 28, 2022
ef1bcd4
fix timeline rerender and change action_id
tomsonpl Mar 28, 2022
635af17
Merge branch 'main' into osquery-add-to-timeline
kibanamachine Mar 28, 2022
e483a17
fix zindex issue
tomsonpl Mar 28, 2022
2d4b13b
Merge branch 'osquery-add-to-timeline' of github.com:tomsonpl/kibana …
tomsonpl Mar 28, 2022
0b3d977
fix eslint
tomsonpl Mar 28, 2022
b06a58f
add timeline e2e test to alerts
tomsonpl Mar 29, 2022
9b33c21
Merge branch 'main' into osquery-add-to-timeline
kibanamachine Mar 29, 2022
2bdb838
Merge branch 'main' into osquery-add-to-timeline
kibanamachine Mar 31, 2022
d7cccf6
Merge branch 'main' into osquery-add-to-timeline
kibanamachine Apr 4, 2022
2650b51
Add timeline to row (by _id)
tomsonpl Apr 6, 2022
24649a5
Merge branch 'main' into osquery-add-to-timeline
tomsonpl Apr 25, 2022
1e86af8
Resolev conflicts - remove hideFullscreen
tomsonpl Apr 25, 2022
99bdaeb
[CI] Auto-commit changed files from 'node scripts/eslint --no-cache -…
kibanamachine Apr 25, 2022
cb087ec
fix tests
tomsonpl Apr 25, 2022
1a00d81
Merge remote-tracking branch 'origin/osquery-add-to-timeline' into os…
tomsonpl Apr 25, 2022
b77290c
fix tests
tomsonpl Apr 25, 2022
5caef62
Merge branch 'main' into osquery-add-to-timeline
tomsonpl Apr 25, 2022
afc5980
Merge branch 'main' into osquery-add-to-timeline
tomsonpl Apr 26, 2022
3c66905
refactor payload destructuring
tomsonpl Apr 28, 2022
bcf8613
Merge branch 'main' into osquery-add-to-timeline
tomsonpl Apr 28, 2022
0742331
fix after merge
tomsonpl Apr 28, 2022
dbe1048
fix tests
tomsonpl Apr 28, 2022
faaaced
Merge branch 'main' into osquery-add-to-timeline
kibanamachine Apr 28, 2022
6717b51
Merge branch 'main' into osquery-add-to-timeline
kibanamachine Apr 28, 2022
7cbda37
remove go back button from osquery flyout
tomsonpl Apr 28, 2022
81ebe56
Merge remote-tracking branch 'origin/osquery-add-to-timeline' into os…
tomsonpl Apr 28, 2022
8cead6b
Merge branch 'main' into osquery-add-to-timeline
kibanamachine Apr 28, 2022
ad9c19c
fix tests
tomsonpl Apr 28, 2022
dae6719
Merge remote-tracking branch 'origin/osquery-add-to-timeline' into os…
tomsonpl Apr 28, 2022
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
14 changes: 10 additions & 4 deletions x-pack/plugins/osquery/cypress/integration/all/alerts.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ describe('Alert Event Details', () => {
it('should be able to run live query', () => {
const PACK_NAME = 'testpack';
const RULE_NAME = 'Test-rule';
const TIMELINE_NAME = 'Untitled timeline';
navigateTo('/app/osquery/packs');
preparePack(PACK_NAME);
findAndClickButton('Edit');
Expand All @@ -57,19 +58,24 @@ describe('Alert Event Details', () => {
cy.getBySel('ruleSwitch').click();
cy.getBySel('ruleSwitch').should('have.attr', 'aria-checked', 'true');
cy.visit('/app/security/alerts');
cy.wait(500);
cy.getBySel('expand-event').first().click();
cy.getBySel('take-action-dropdown-btn').click();
cy.getBySel('osquery-action-item').click();
cy.contains('1 agent selected.');
inputQuery('select * from uptime;');
submitQuery();
checkResults();

cy.contains('Save for later').click();
cy.contains('Save query');
cy.get('.euiButtonEmpty--flushLeft').contains('Cancel').click();
cy.getBySel('add-to-timeline').first().click();
cy.getBySel('globalToastList').contains('Added');
cy.getBySel(RESULTS_TABLE).within(() => {
cy.getBySel(RESULTS_TABLE_BUTTON).should('not.exist');
});
cy.contains('Save for later').click();
cy.contains('Save query');
cy.contains(/^Save$/);
cy.contains('Cancel').click();
cy.contains(TIMELINE_NAME).click();
cy.getBySel('draggableWrapperKeyboardHandler').contains('action_id: "');
});
});
13 changes: 11 additions & 2 deletions x-pack/plugins/osquery/cypress/integration/all/live_query.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,18 @@ describe('ALL - Live Query', () => {
});
cy.react(RESULTS_TABLE_CELL_WRRAPER, {
props: { id: 'message', index: 1 },
});
}).should('exist');
cy.wait(500);
cy.react(RESULTS_TABLE_CELL_WRRAPER, {
props: { id: 'osquery.days.number', index: 2 },
}).should('exist');

cy.react(RESULTS_TABLE_CELL_WRRAPER, {
props: { id: 'osquery.days.number', index: 2 },
}).react('EuiIconIndexMapping');
}).within(() => {
cy.get('.euiToolTipAnchor').within(() => {
cy.get('svg').should('exist');
});
});
});
});
8 changes: 4 additions & 4 deletions x-pack/plugins/osquery/public/live_queries/form/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ interface LiveQueryFormProps {
ecsMappingField?: boolean;
formType?: FormType;
enabled?: boolean;
hideFullscreen?: true;
addToTimeline?: (payload: { query: [string, string]; isIcon?: true }) => React.ReactElement;
}

const LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({
Expand All @@ -73,7 +73,7 @@ const LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({
ecsMappingField = true,
formType = 'steps',
enabled = true,
hideFullscreen,
addToTimeline,
}) => {
const ecsFieldRef = useRef<ECSMappingEditorFieldRef>();
const permissions = useKibana().services.application.capabilities.osquery;
Expand Down Expand Up @@ -393,10 +393,10 @@ const LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({
actionId={actionId}
endDate={data?.actions[0].expiration}
agentIds={agentIds}
hideFullscreen={hideFullscreen}
addToTimeline={addToTimeline}
/>
) : null,
[actionId, agentIds, data?.actions, hideFullscreen]
[actionId, agentIds, data?.actions, addToTimeline]
);

const formSteps: EuiContainedStepProps[] = useMemo(
Expand Down
6 changes: 3 additions & 3 deletions x-pack/plugins/osquery/public/live_queries/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ interface LiveQueryProps {
ecsMappingField?: boolean;
enabled?: boolean;
formType?: 'steps' | 'simple';
hideFullscreen?: true;
addToTimeline?: (payload: { query: [string, string]; isIcon?: true }) => React.ReactElement;
}

const LiveQueryComponent: React.FC<LiveQueryProps> = ({
Expand All @@ -45,7 +45,7 @@ const LiveQueryComponent: React.FC<LiveQueryProps> = ({
ecsMappingField,
formType,
enabled,
hideFullscreen,
addToTimeline,
}) => {
const { data: hasActionResultsPrivileges, isLoading } = useActionResultsPrivileges();

Expand Down Expand Up @@ -115,7 +115,7 @@ const LiveQueryComponent: React.FC<LiveQueryProps> = ({
onSuccess={onSuccess}
formType={formType}
enabled={enabled}
hideFullscreen={hideFullscreen}
addToTimeline={addToTimeline}
/>
);
};
Expand Down
32 changes: 28 additions & 4 deletions x-pack/plugins/osquery/public/results/results_table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import {
EuiProgress,
EuiSpacer,
EuiIconTip,
EuiDataGridCellValueElementProps,
EuiDataGridControlColumn,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
Expand Down Expand Up @@ -46,15 +48,15 @@ interface ResultsTableComponentProps {
agentIds?: string[];
endDate?: string;
startDate?: string;
hideFullscreen?: true;
addToTimeline?: (payload: { query: [string, string]; isIcon?: true }) => React.ReactElement;
}

const ResultsTableComponent: React.FC<ResultsTableComponentProps> = ({
actionId,
agentIds,
startDate,
endDate,
hideFullscreen,
addToTimeline,
}) => {
const [isLive, setIsLive] = useState(true);
const { data: hasActionResultsPrivileges } = useActionResultsPrivileges();
Expand Down Expand Up @@ -309,10 +311,30 @@ const ResultsTableComponent: React.FC<ResultsTableComponentProps> = ({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [allResultsData?.columns.length, ecsMappingColumns, getHeaderDisplay]);

const leadingControlColumns: EuiDataGridControlColumn[] = useMemo(() => {
const data = allResultsData?.edges;
if (addToTimeline && data) {
return [
{
id: 'timeline',
width: 38,
headerCellRender: () => null,
rowCellRender: (actionProps: EuiDataGridCellValueElementProps) => {
const eventId = data[actionProps.rowIndex]._id;

return addToTimeline({ query: ['_id', eventId], isIcon: true });
},
},
];
}

return [];
}, [addToTimeline, allResultsData?.edges]);

const toolbarVisibility = useMemo(
() => ({
showDisplaySelector: false,
showFullScreenSelector: !hideFullscreen,
showFullScreenSelector: !addToTimeline,
additionalControls: (
<>
<ViewResultsInDiscoverAction
Expand All @@ -327,10 +349,11 @@ const ResultsTableComponent: React.FC<ResultsTableComponentProps> = ({
endDate={endDate}
startDate={startDate}
/>
{addToTimeline && addToTimeline({ query: ['action_id', actionId] })}
</>
),
}),
[actionId, endDate, startDate, hideFullscreen]
[actionId, addToTimeline, endDate, startDate]
);

useEffect(
Expand Down Expand Up @@ -380,6 +403,7 @@ const ResultsTableComponent: React.FC<ResultsTableComponentProps> = ({
columnVisibility={columnVisibility}
rowCount={allResultsData?.totalCount ?? 0}
renderCellValue={renderCellValue}
leadingControlColumns={leadingControlColumns}
sorting={tableSorting}
pagination={tablePagination}
height="500px"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,15 @@ interface ResultTabsProps {
agentIds?: string[];
startDate?: string;
endDate?: string;
hideFullscreen?: true;
addToTimeline?: (payload: { query: [string, string]; isIcon?: true }) => React.ReactElement;
}

const ResultTabsComponent: React.FC<ResultTabsProps> = ({
actionId,
agentIds,
endDate,
startDate,
hideFullscreen,
addToTimeline,
}) => {
const tabs = useMemo(
() => [
Expand All @@ -40,7 +40,7 @@ const ResultTabsComponent: React.FC<ResultTabsProps> = ({
agentIds={agentIds}
startDate={startDate}
endDate={endDate}
hideFullscreen={hideFullscreen}
addToTimeline={addToTimeline}
/>
</>
),
Expand All @@ -60,7 +60,7 @@ const ResultTabsComponent: React.FC<ResultTabsProps> = ({
),
},
],
[actionId, agentIds, endDate, startDate, hideFullscreen]
[actionId, agentIds, endDate, startDate, addToTimeline]
);

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,13 @@ import { useIsOsqueryAvailable } from './use_is_osquery_available';
interface OsqueryActionProps {
agentId?: string;
formType: 'steps' | 'simple';
hideFullscreen?: true;
addToTimeline: (payload: { query: [string, string]; isIcon?: true }) => React.ReactElement;
}

const OsqueryActionComponent: React.FC<OsqueryActionProps> = ({
agentId,
formType = 'simple',
hideFullscreen,
addToTimeline,
}) => {
const permissions = useKibana().services.application.capabilities.osquery;

Expand Down Expand Up @@ -139,18 +139,18 @@ const OsqueryActionComponent: React.FC<OsqueryActionProps> = ({
);
}

return <LiveQuery formType={formType} agentId={agentId} hideFullscreen={hideFullscreen} />;
return <LiveQuery formType={formType} agentId={agentId} addToTimeline={addToTimeline} />;
};

const OsqueryAction = React.memo(OsqueryActionComponent);

// @ts-expect-error update types
const OsqueryActionWrapperComponent = ({ services, agentId, formType, hideFullscreen }) => (
const OsqueryActionWrapperComponent = ({ services, agentId, formType, addToTimeline }) => (
<KibanaThemeProvider theme$={services.theme.theme$}>
<KibanaContextProvider services={services}>
<EuiErrorBoundary>
<QueryClientProvider client={queryClient}>
<OsqueryAction agentId={agentId} formType={formType} hideFullscreen={hideFullscreen} />
<OsqueryAction agentId={agentId} formType={formType} addToTimeline={addToTimeline} />
</QueryClientProvider>
</EuiErrorBoundary>
</KibanaContextProvider>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,20 @@
* 2.0.
*/

import React from 'react';
import React, { useCallback } from 'react';
import styled from 'styled-components';
import { EuiFlyout, EuiFlyoutFooter, EuiFlyoutBody, EuiFlyoutHeader } from '@elastic/eui';
import {
EuiFlyout,
EuiFlyoutFooter,
EuiFlyoutBody,
EuiFlyoutHeader,
EuiButtonEmpty,
} from '@elastic/eui';
import { useKibana } from '../../../common/lib/kibana';
import { OsqueryEventDetailsFooter } from './osquery_flyout_footer';
import { OsqueryEventDetailsHeader } from './osquery_flyout_header';
import { ACTION_OSQUERY } from './translations';
import { DataProvider } from '../../../timelines/components/timeline/data_providers/data_provider';

const OsqueryActionWrapper = styled.div`
padding: 8px;
Expand All @@ -22,11 +29,44 @@ export interface OsqueryFlyoutProps {
onClose: () => void;
}

export const OsqueryFlyout: React.FC<OsqueryFlyoutProps> = ({ agentId, onClose }) => {
const TimelineComponent = React.memo((props) => {
return <EuiButtonEmpty {...props} size="xs" />;
});
TimelineComponent.displayName = 'TimelineComponent';

export const OsqueryFlyoutComponent: React.FC<OsqueryFlyoutProps> = ({ agentId, onClose }) => {
const {
services: { osquery },
services: { osquery, timelines },
} = useKibana();

const { getAddToTimelineButton } = timelines.getHoverActions();

const handleAddToTimeline = useCallback(
(payload: { query: [string, string]; isIcon?: true }) => {
const [field, value] = payload.query;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: could change this to const { query: [field, value], isIcon } = payload so it's more clear that isIcon is used below

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

const providerA: DataProvider = {
and: [],
enabled: true,
excluded: false,
id: value,
kqlQuery: '',
name: value,
queryMatch: {
field,
value,
operator: ':',
},
};

return getAddToTimelineButton({
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should have at least a unit test to check the flyout is rendered correctly according to the props and then check if add to timeline was executed with the right options.

dataProvider: providerA,
field: value,
ownFocus: false,
...(payload.isIcon ? { showTooltip: true } : { Component: TimelineComponent }),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this ever called with isIcon false?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, never. It's either true or undefined

});
},
[getAddToTimelineButton]
);
// @ts-expect-error
const { OsqueryAction } = osquery;
return (
Expand All @@ -45,7 +85,7 @@ export const OsqueryFlyout: React.FC<OsqueryFlyoutProps> = ({ agentId, onClose }
</EuiFlyoutHeader>
<EuiFlyoutBody>
<OsqueryActionWrapper data-test-subj="flyout-body-osquery">
<OsqueryAction agentId={agentId} formType="steps" hideFullscreen={true} />
<OsqueryAction agentId={agentId} formType="steps" addToTimeline={handleAddToTimeline} />
</OsqueryActionWrapper>
</EuiFlyoutBody>
<EuiFlyoutFooter>
Expand All @@ -55,4 +95,4 @@ export const OsqueryFlyout: React.FC<OsqueryFlyoutProps> = ({ agentId, onClose }
);
};

OsqueryFlyout.displayName = 'OsqueryFlyout';
export const OsqueryFlyout = React.memo(OsqueryFlyoutComponent);