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 all 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: "');
});
});
5 changes: 4 additions & 1 deletion x-pack/plugins/osquery/cypress/tasks/live_query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@ export const DEFAULT_QUERY = 'select * from processes;';
export const BIG_QUERY = 'select * from processes, users;';

export const selectAllAgents = () => {
cy.react('EuiComboBox', { props: { placeholder: 'Select agents or groups' } }).type('All agents');
cy.react('EuiComboBox', { props: { placeholder: 'Select agents or groups' } })
.find('input')
.should('not.be.disabled');
cy.react('EuiComboBox', { props: { placeholder: 'Select agents or groups' } }).click();
cy.react('EuiFilterSelectItem').contains('All agents').should('exist');
cy.react('EuiComboBox', { props: { placeholder: 'Select agents or groups' } }).type(
'{downArrow}{enter}{esc}'
Expand Down
10 changes: 5 additions & 5 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;
isExternal?: 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,
isExternal,
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}
isExternal={isExternal}
addToTimeline={addToTimeline}
/>
) : null,
[actionId, agentIds, data?.actions, isExternal]
[actionId, agentIds, data?.actions, addToTimeline]
);

const formSteps: EuiContainedStepProps[] = useMemo(
Expand Down Expand Up @@ -467,7 +467,7 @@ const LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({
</Form>
{showSavedQueryFlyout ? (
<SavedQueryFlyout
isExternal={isExternal}
isExternal={!!addToTimeline}
onClose={handleCloseSaveQueryFlout}
defaultValue={flyoutFormDefaultValue}
/>
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';
isExternal?: 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,
isExternal,
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}
isExternal={isExternal}
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;
isExternal?: true;
addToTimeline?: (payload: { query: [string, string]; isIcon?: true }) => React.ReactElement;
}

const ResultsTableComponent: React.FC<ResultsTableComponentProps> = ({
actionId,
agentIds,
startDate,
endDate,
isExternal,
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: !isExternal,
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, isExternal]
[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;
isExternal?: true;
addToTimeline?: (payload: { query: [string, string]; isIcon?: true }) => React.ReactElement;
}

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

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import { useCreateSavedQuery } from './use_create_saved_query';
interface AddQueryFlyoutProps {
defaultValue: unknown;
onClose: () => void;
isExternal?: true;
isExternal?: boolean;
}

const additionalZIndexStyle = { style: 'z-index: 6000' };
Expand Down Expand Up @@ -60,7 +60,7 @@ const SavedQueryFlyoutComponent: React.FC<AddQueryFlyoutProps> = ({
ownFocus
onClose={onClose}
aria-labelledby="flyoutTitle"
maskProps={isExternal && additionalZIndexStyle} // For an edge case to display above the alerts flyout
maskProps={isExternal ? additionalZIndexStyle : undefined} // For an edge case to display above the alerts flyout
>
<EuiFlyoutHeader hasBorder>
<EuiTitle size="s">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,14 @@ import { useIsOsqueryAvailable } from './use_is_osquery_available';
interface OsqueryActionProps {
agentId?: string;
formType: 'steps' | 'simple';
isExternal?: true;
addToTimeline?: (payload: { query: [string, string]; isIcon?: true }) => React.ReactElement;
}

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

const emptyPrompt = useMemo(
Expand Down Expand Up @@ -99,18 +103,18 @@ const OsqueryActionComponent: React.FC<OsqueryActionProps> = ({ agentId, formTyp
);
}

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

export const OsqueryAction = React.memo(OsqueryActionComponent);

// @ts-expect-error update types
const OsqueryActionWrapperComponent = ({ services, agentId, formType, isExternal }) => (
const OsqueryActionWrapperComponent = ({ services, agentId, formType, addToTimeline }) => (
<KibanaThemeProvider theme$={services.theme.theme$}>
<KibanaContextProvider services={services}>
<EuiErrorBoundary>
<QueryClientProvider client={queryClient}>
<OsqueryAction agentId={agentId} formType={formType} isExternal={isExternal} />
<OsqueryAction agentId={agentId} formType={formType} addToTimeline={addToTimeline} />
</QueryClientProvider>
</EuiErrorBoundary>
</KibanaContextProvider>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,35 +78,27 @@ describe('Osquery Action', () => {
spyOsquery();
mockKibana();

const { getByText } = renderWithContext(
<OsqueryAction agentId={'test'} formType={'steps'} isExternal={true} />
);
const { getByText } = renderWithContext(<OsqueryAction agentId={'test'} formType={'steps'} />);
expect(getByText(EMPTY_PROMPT)).toBeInTheDocument();
});
it('should return empty prompt when no agentId', async () => {
spyOsquery();
mockKibana();

const { getByText } = renderWithContext(
<OsqueryAction agentId={''} formType={'steps'} isExternal={true} />
);
const { getByText } = renderWithContext(<OsqueryAction agentId={''} formType={'steps'} />);
expect(getByText(EMPTY_PROMPT)).toBeInTheDocument();
});
it('should return permission denied when agentFetched and agentData available', async () => {
spyOsquery({ agentData: {} });
mockKibana();

const { getByText } = renderWithContext(
<OsqueryAction agentId={'test'} formType={'steps'} isExternal={true} />
);
const { getByText } = renderWithContext(<OsqueryAction agentId={'test'} formType={'steps'} />);
expect(getByText(PERMISSION_DENIED)).toBeInTheDocument();
});
it('should return agent status error when permissions are ok and agent status is wrong', async () => {
spyOsquery({ agentData: {} });
mockKibana(properPermissions);
const { getByText } = renderWithContext(
<OsqueryAction agentId={'test'} formType={'steps'} isExternal={true} />
);
const { getByText } = renderWithContext(<OsqueryAction agentId={'test'} formType={'steps'} />);
expect(getByText(AGENT_STATUS_ERROR)).toBeInTheDocument();
});
it('should return permission denied if just one permission (runSavedQueries) is available', async () => {
Expand All @@ -116,9 +108,7 @@ describe('Osquery Action', () => {
runSavedQueries: true,
},
});
const { getByText } = renderWithContext(
<OsqueryAction agentId={'test'} formType={'steps'} isExternal={true} />
);
const { getByText } = renderWithContext(<OsqueryAction agentId={'test'} formType={'steps'} />);
expect(getByText(PERMISSION_DENIED)).toBeInTheDocument();
});
it('should return permission denied if just one permission (readSavedQueries) is available', async () => {
Expand All @@ -128,9 +118,7 @@ describe('Osquery Action', () => {
readSavedQueries: true,
},
});
const { getByText } = renderWithContext(
<OsqueryAction agentId={'test'} formType={'steps'} isExternal={true} />
);
const { getByText } = renderWithContext(<OsqueryAction agentId={'test'} formType={'steps'} />);
expect(getByText(PERMISSION_DENIED)).toBeInTheDocument();
});
it('should return permission denied if no writeLiveQueries', async () => {
Expand All @@ -140,9 +128,7 @@ describe('Osquery Action', () => {
writeLiveQueries: true,
},
});
const { getByText } = renderWithContext(
<OsqueryAction agentId={'test'} formType={'steps'} isExternal={true} />
);
const { getByText } = renderWithContext(<OsqueryAction agentId={'test'} formType={'steps'} />);
expect(getByText(AGENT_STATUS_ERROR)).toBeInTheDocument();
});
it('should return not available prompt if osquery is not available', async () => {
Expand All @@ -152,17 +138,15 @@ describe('Osquery Action', () => {
writeLiveQueries: true,
},
});
const { getByText } = renderWithContext(
<OsqueryAction agentId={'test'} formType={'steps'} isExternal={true} />
);
const { getByText } = renderWithContext(<OsqueryAction agentId={'test'} formType={'steps'} />);
expect(getByText(NOT_AVAILABLE)).toBeInTheDocument();
});
it('should not return any errors when all data is ok', async () => {
spyOsquery({ agentData: { status: 'online' } });
mockKibana(properPermissions);

const { queryByText } = renderWithContext(
<OsqueryAction agentId={'test'} formType={'steps'} isExternal={true} />
<OsqueryAction agentId={'test'} formType={'steps'} />
);
expect(queryByText(EMPTY_PROMPT)).not.toBeInTheDocument();
expect(queryByText(PERMISSION_DENIED)).not.toBeInTheDocument();
Expand Down
Loading