[Cloud Security] Fixed failing FTR (#199683)

## Summary

fixes: https://github.com/elastic/kibana/issues/198632

FTR failed due to the usage of relative start and end when querying for
the graph.

Together with @tinnytintin10 we decided it is safe to assume the time to
query would be 30 minutes before and after the alert.

**Some enhancements and fixes:**
- Disabled re-fetch /graph API when window focus returned

### Checklist

Delete any items that are not applicable to this PR.

- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [ ] [Flaky Test
Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was
used on any tests changed
This commit is contained in:
Kfir Peled 2024-11-13 01:43:58 +00:00 committed by GitHub
parent 43a5022362
commit 5add2c82db
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 169 additions and 79 deletions

View file

@ -28,15 +28,6 @@ jest.mock('../hooks/use_fetch_graph_data', () => ({
}));
const mockUseFetchGraphData = useFetchGraphData as jest.Mock;
const mockUseUiSetting = jest.fn().mockReturnValue([false]);
jest.mock('@kbn/kibana-react-plugin/public', () => {
const original = jest.requireActual('@kbn/kibana-react-plugin/public');
return {
...original,
useUiSetting$: () => mockUseUiSetting(),
};
});
const mockGraph = () => <div data-test-subj={GRAPH_PREVIEW_TEST_ID} />;
jest.mock('@kbn/cloud-security-posture-graph', () => {
@ -64,7 +55,11 @@ describe('<GraphPreviewContainer />', () => {
data: { nodes: [], edges: [] },
});
const timestamp = new Date().toISOString();
(useGraphPreview as jest.Mock).mockReturnValue({
timestamp,
eventIds: [],
isAuditLog: true,
});
@ -87,9 +82,23 @@ describe('<GraphPreviewContainer />', () => {
expect(
getByTestId(EXPANDABLE_PANEL_HEADER_TITLE_TEXT_TEST_ID(GRAPH_PREVIEW_TEST_ID))
).toBeInTheDocument();
expect(mockUseFetchGraphData).toHaveBeenCalled();
expect(mockUseFetchGraphData.mock.calls[0][0]).toEqual({
req: {
query: {
eventIds: [],
start: `${timestamp}||-30m`,
end: `${timestamp}||+30m`,
},
},
options: {
enabled: true,
refetchOnWindowFocus: false,
},
});
});
it('should render error message and text in header', () => {
it('should not render when graph data is not available', () => {
mockUseFetchGraphData.mockReturnValue({
isLoading: false,
isError: false,
@ -100,10 +109,10 @@ describe('<GraphPreviewContainer />', () => {
isAuditLog: false,
});
const { getByTestId } = renderGraphPreview();
const { queryByTestId } = renderGraphPreview();
expect(
getByTestId(EXPANDABLE_PANEL_HEADER_TITLE_TEXT_TEST_ID(GRAPH_PREVIEW_TEST_ID))
).toBeInTheDocument();
queryByTestId(EXPANDABLE_PANEL_HEADER_TITLE_TEXT_TEST_ID(GRAPH_PREVIEW_TEST_ID))
).not.toBeInTheDocument();
});
});

View file

@ -14,57 +14,60 @@ import { useFetchGraphData } from '../hooks/use_fetch_graph_data';
import { useGraphPreview } from '../hooks/use_graph_preview';
import { ExpandablePanel } from '../../../shared/components/expandable_panel';
const DEFAULT_FROM = 'now-60d/d';
const DEFAULT_TO = 'now/d';
/**
* Graph preview under Overview, Visualizations. It shows a graph representation of entities.
*/
export const GraphPreviewContainer: React.FC = () => {
const { dataAsNestedObject, getFieldsData } = useDocumentDetailsContext();
const { eventIds } = useGraphPreview({
const {
eventIds,
timestamp = new Date().toISOString(),
isAuditLog,
} = useGraphPreview({
getFieldsData,
ecsData: dataAsNestedObject,
});
// TODO: default start and end might not capture the original event
const graphFetchQuery = useFetchGraphData({
const { isLoading, isError, data } = useFetchGraphData({
req: {
query: {
eventIds,
start: DEFAULT_FROM,
end: DEFAULT_TO,
start: `${timestamp}||-30m`,
end: `${timestamp}||+30m`,
},
},
options: {
enabled: isAuditLog,
refetchOnWindowFocus: false,
},
});
return (
<ExpandablePanel
header={{
title: (
<FormattedMessage
id="xpack.securitySolution.flyout.right.visualizations.graphPreview.graphPreviewTitle"
defaultMessage="Graph preview"
/>
),
iconType: 'indexMapping',
}}
data-test-subj={GRAPH_PREVIEW_TEST_ID}
content={
!graphFetchQuery.isLoading && !graphFetchQuery.isError
? {
paddingSize: 'none',
}
: undefined
}
>
<GraphPreview
isLoading={graphFetchQuery.isLoading}
isError={graphFetchQuery.isError}
data={graphFetchQuery.data}
/>
</ExpandablePanel>
isAuditLog && (
<ExpandablePanel
header={{
title: (
<FormattedMessage
id="xpack.securitySolution.flyout.right.visualizations.graphPreview.graphPreviewTitle"
defaultMessage="Graph preview"
/>
),
iconType: 'indexMapping',
}}
data-test-subj={GRAPH_PREVIEW_TEST_ID}
content={
!isLoading && !isError
? {
paddingSize: 'none',
}
: undefined
}
>
<GraphPreview isLoading={isLoading} isError={isError} data={data} />
</ExpandablePanel>
)
);
};

View file

@ -18,7 +18,7 @@ describe('useGraphPreview', () => {
it(`should return false when missing actor`, () => {
const getFieldsData: GetFieldsData = (field: string) => {
if (field === 'kibana.alert.original_event.id') {
return field;
return 'eventId';
}
return mockFieldData[field];
};
@ -35,7 +35,12 @@ describe('useGraphPreview', () => {
},
});
expect(hookResult.result.current.isAuditLog).toEqual(false);
const { isAuditLog, timestamp, eventIds, actorIds, action } = hookResult.result.current;
expect(isAuditLog).toEqual(false);
expect(timestamp).toEqual(mockFieldData['@timestamp'][0]);
expect(eventIds).toEqual(['eventId']);
expect(actorIds).toEqual([]);
expect(action).toEqual(['action']);
});
it(`should return false when missing event.action`, () => {
@ -57,7 +62,12 @@ describe('useGraphPreview', () => {
},
});
expect(hookResult.result.current.isAuditLog).toEqual(false);
const { isAuditLog, timestamp, eventIds, actorIds, action } = hookResult.result.current;
expect(isAuditLog).toEqual(false);
expect(timestamp).toEqual(mockFieldData['@timestamp'][0]);
expect(eventIds).toEqual(['eventId']);
expect(actorIds).toEqual(['actorId']);
expect(action).toEqual(undefined);
});
it(`should return false when missing original_event.id`, () => {
@ -80,7 +90,45 @@ describe('useGraphPreview', () => {
},
});
expect(hookResult.result.current.isAuditLog).toEqual(false);
const { isAuditLog, timestamp, eventIds, actorIds, action } = hookResult.result.current;
expect(isAuditLog).toEqual(false);
expect(timestamp).toEqual(mockFieldData['@timestamp'][0]);
expect(eventIds).toEqual([]);
expect(actorIds).toEqual(['actorId']);
expect(action).toEqual(['action']);
});
it(`should return false when timestamp is missing`, () => {
const getFieldsData: GetFieldsData = (field: string) => {
if (field === '@timestamp') {
return;
} else if (field === 'kibana.alert.original_event.id') {
return 'eventId';
} else if (field === 'actor.entity.id') {
return 'actorId';
}
return mockFieldData[field];
};
hookResult = renderHook((props: UseGraphPreviewParams) => useGraphPreview(props), {
initialProps: {
getFieldsData,
ecsData: {
_id: 'id',
event: {
action: ['action'],
},
},
},
});
const { isAuditLog, timestamp, eventIds, actorIds, action } = hookResult.result.current;
expect(isAuditLog).toEqual(false);
expect(timestamp).toEqual(null);
expect(eventIds).toEqual(['eventId']);
expect(actorIds).toEqual(['actorId']);
expect(action).toEqual(['action']);
});
it(`should return true when alert is has graph preview`, () => {
@ -106,7 +154,12 @@ describe('useGraphPreview', () => {
},
});
expect(hookResult.result.current.isAuditLog).toEqual(true);
const { isAuditLog, timestamp, eventIds, actorIds, action } = hookResult.result.current;
expect(isAuditLog).toEqual(true);
expect(timestamp).toEqual(mockFieldData['@timestamp'][0]);
expect(eventIds).toEqual(['eventId']);
expect(actorIds).toEqual(['actorId']);
expect(action).toEqual(['action']);
});
it(`should return true when alert is has graph preview with multiple values`, () => {
@ -132,6 +185,11 @@ describe('useGraphPreview', () => {
},
});
expect(hookResult.result.current.isAuditLog).toEqual(true);
const { isAuditLog, timestamp, eventIds, actorIds, action } = hookResult.result.current;
expect(isAuditLog).toEqual(true);
expect(timestamp).toEqual(mockFieldData['@timestamp'][0]);
expect(eventIds).toEqual(['id1', 'id2']);
expect(actorIds).toEqual(['actorId1', 'actorId2']);
expect(action).toEqual(['action1', 'action2']);
});
});

View file

@ -8,7 +8,7 @@
import type { EcsSecurityExtension as Ecs } from '@kbn/securitysolution-ecs';
import { get } from 'lodash/fp';
import type { GetFieldsData } from '../../shared/hooks/use_get_fields_data';
import { getFieldArray } from '../../shared/utils';
import { getField, getFieldArray } from '../../shared/utils';
export interface UseGraphPreviewParams {
/**
@ -25,6 +25,11 @@ export interface UseGraphPreviewParams {
* Interface for the result of the useGraphPreview hook
*/
export interface UseGraphPreviewResult {
/**
* The timestamp of the event
*/
timestamp: string | null;
/**
* Array of event IDs associated with the alert
*/
@ -38,7 +43,7 @@ export interface UseGraphPreviewResult {
/**
* Action associated with the event
*/
action: string | undefined;
action?: string[];
/**
* Boolean indicating if the event is an audit log (contains event ids, actor ids and action)
@ -53,13 +58,15 @@ export const useGraphPreview = ({
getFieldsData,
ecsData,
}: UseGraphPreviewParams): UseGraphPreviewResult => {
const timestamp = getField(getFieldsData('@timestamp'));
const originalEventId = getFieldsData('kibana.alert.original_event.id');
const eventId = getFieldsData('event.id');
const eventIds = originalEventId ? getFieldArray(originalEventId) : getFieldArray(eventId);
const actorIds = getFieldArray(getFieldsData('actor.entity.id'));
const action = get(['event', 'action'], ecsData);
const isAuditLog = actorIds.length > 0 && action?.length > 0 && eventIds.length > 0;
const action: string[] | undefined = get(['event', 'action'], ecsData);
const isAuditLog =
Boolean(timestamp) && actorIds.length > 0 && Boolean(action?.length) && eventIds.length > 0;
return { eventIds, actorIds, action, isAuditLog };
return { timestamp, eventIds, actorIds, action, isAuditLog };
};

View file

@ -399,7 +399,7 @@ export default function (providerContext: FtrProviderContext) {
});
});
it('Should filter unknown targets', async () => {
it('should filter unknown targets', async () => {
const response = await postGraph(supertest, {
query: {
eventIds: [],
@ -424,7 +424,7 @@ export default function (providerContext: FtrProviderContext) {
expect(response.body).not.to.have.property('messages');
});
it('Should return unknown targets', async () => {
it('should return unknown targets', async () => {
const response = await postGraph(supertest, {
showUnknownTarget: true,
query: {
@ -450,7 +450,7 @@ export default function (providerContext: FtrProviderContext) {
expect(response.body).not.to.have.property('messages');
});
it('Should limit number of nodes', async () => {
it('should limit number of nodes', async () => {
const response = await postGraph(supertest, {
nodesLimit: 1,
query: {
@ -476,6 +476,20 @@ export default function (providerContext: FtrProviderContext) {
expect(response.body).to.have.property('messages').length(1);
expect(response.body.messages[0]).equal(ApiMessageCode.ReachedNodesLimit);
});
it('should support date math', async () => {
const response = await postGraph(supertest, {
query: {
eventIds: ['kabcd1234efgh5678'],
start: '2024-09-01T12:30:00.000Z||-30m',
end: '2024-09-01T12:30:00.000Z||+30m',
},
}).expect(result(200));
expect(response.body).to.have.property('nodes').length(3);
expect(response.body).to.have.property('edges').length(2);
expect(response.body).not.to.have.property('messages');
});
});
});
}

View file

@ -4,7 +4,7 @@
"id": "589e086d7ceec7d4b353340578bd607e96fbac7eab9e2926f110990be15122f1",
"index": ".internal.alerts-security.alerts-default-000001",
"source": {
"@timestamp": "2024-09-01T20:44:02.109Z",
"@timestamp": "2024-09-01T12:44:02.109Z",
"actor": {
"entity": {
"id": "admin@example.com"
@ -34,7 +34,7 @@
],
"dataset": "gcp.audit",
"id": "kabcd1234efgh5678",
"ingested": "2024-09-01T20:40:17Z",
"ingested": "2024-09-01T12:40:17Z",
"module": "gcp",
"outcome": "success",
"provider": "activity",
@ -95,8 +95,8 @@
}
],
"kibana.alert.depth": 1,
"kibana.alert.intended_timestamp": "2024-09-01T20:44:02.117Z",
"kibana.alert.last_detected": "2024-09-01T20:44:02.117Z",
"kibana.alert.intended_timestamp": "2024-09-01T12:44:02.117Z",
"kibana.alert.last_detected": "2024-09-01T12:44:02.117Z",
"kibana.alert.original_event.action": "google.iam.admin.v1.CreateRole",
"kibana.alert.original_event.agent_id_status": "missing",
"kibana.alert.original_event.category": [
@ -106,7 +106,7 @@
],
"kibana.alert.original_event.dataset": "gcp.audit",
"kibana.alert.original_event.id": "kabcd1234efgh5678",
"kibana.alert.original_event.ingested": "2024-09-01T20:40:17Z",
"kibana.alert.original_event.ingested": "2024-09-01T12:40:17Z",
"kibana.alert.original_event.kind": "event",
"kibana.alert.original_event.module": "gcp",
"kibana.alert.original_event.outcome": "success",
@ -126,13 +126,13 @@
],
"kibana.alert.rule.category": "Custom Query Rule",
"kibana.alert.rule.consumer": "siem",
"kibana.alert.rule.created_at": "2024-09-01T20:38:49.650Z",
"kibana.alert.rule.created_at": "2024-09-01T12:38:49.650Z",
"kibana.alert.rule.created_by": "elastic",
"kibana.alert.rule.description": "Identifies an Identity and Access Management (IAM) custom role creation in Google Cloud Platform (GCP). Custom roles are user-defined, and allow for the bundling of one or more supported permissions to meet specific needs. Custom roles will not be updated automatically and could lead to privilege creep if not carefully scrutinized.",
"kibana.alert.rule.enabled": true,
"kibana.alert.rule.exceptions_list": [
],
"kibana.alert.rule.execution.timestamp": "2024-09-01T20:44:02.117Z",
"kibana.alert.rule.execution.timestamp": "2024-09-01T12:44:02.117Z",
"kibana.alert.rule.execution.uuid": "a440f349-1900-4087-b507-f2b98c6cfa79",
"kibana.alert.rule.false_positives": [
"Custom role creations may be done by a system or network administrator. Verify whether the user email, resource name, and/or hostname should be making changes in your environment. Role creations by unfamiliar users or hosts should be investigated. If known behavior is causing false positives, it can be exempted from the rule."
@ -300,12 +300,12 @@
"kibana.alert.rule.timestamp_override": "event.ingested",
"kibana.alert.rule.to": "now",
"kibana.alert.rule.type": "query",
"kibana.alert.rule.updated_at": "2024-09-01T20:39:00.099Z",
"kibana.alert.rule.updated_at": "2024-09-01T12:39:00.099Z",
"kibana.alert.rule.updated_by": "elastic",
"kibana.alert.rule.uuid": "c6f64115-5941-46ef-bfa3-61a4ecb4f3ba",
"kibana.alert.rule.version": 104,
"kibana.alert.severity": "medium",
"kibana.alert.start": "2024-09-01T20:44:02.117Z",
"kibana.alert.start": "2024-09-01T12:44:02.117Z",
"kibana.alert.status": "active",
"kibana.alert.uuid": "589e086d7ceec7d4b353340578bd607e96fbac7eab9e2926f110990be15122f1",
"kibana.alert.workflow_assignee_ids": [
@ -361,7 +361,7 @@
"id": "838ea37ab43ab7d2754d007fbe8191be53d7d637bea62f6189f8db1503c0e250",
"index": ".internal.alerts-security.alerts-default-000001",
"source": {
"@timestamp": "2024-09-01T20:39:03.646Z",
"@timestamp": "2024-09-01T12:39:03.646Z",
"actor": {
"entity": {
"id": "admin@example.com"
@ -391,7 +391,7 @@
],
"dataset": "gcp.audit",
"id": "kabcd1234efgh5678",
"ingested": "2024-09-01T20:38:13Z",
"ingested": "2024-09-01T12:38:13Z",
"module": "gcp",
"outcome": "success",
"provider": "activity",
@ -452,8 +452,8 @@
}
],
"kibana.alert.depth": 1,
"kibana.alert.intended_timestamp": "2024-09-01T20:39:03.657Z",
"kibana.alert.last_detected": "2024-09-01T20:39:03.657Z",
"kibana.alert.intended_timestamp": "2024-09-01T12:39:03.657Z",
"kibana.alert.last_detected": "2024-09-01T12:39:03.657Z",
"kibana.alert.original_event.action": "google.iam.admin.v1.CreateRole",
"kibana.alert.original_event.agent_id_status": "missing",
"kibana.alert.original_event.category": [
@ -463,7 +463,7 @@
],
"kibana.alert.original_event.dataset": "gcp.audit",
"kibana.alert.original_event.id": "kabcd1234efgh5678",
"kibana.alert.original_event.ingested": "2024-09-01T20:38:13Z",
"kibana.alert.original_event.ingested": "2024-09-01T12:38:13Z",
"kibana.alert.original_event.kind": "event",
"kibana.alert.original_event.module": "gcp",
"kibana.alert.original_event.outcome": "success",
@ -483,13 +483,13 @@
],
"kibana.alert.rule.category": "Custom Query Rule",
"kibana.alert.rule.consumer": "siem",
"kibana.alert.rule.created_at": "2024-09-01T20:38:49.650Z",
"kibana.alert.rule.created_at": "2024-09-01T12:38:49.650Z",
"kibana.alert.rule.created_by": "elastic",
"kibana.alert.rule.description": "Identifies an Identity and Access Management (IAM) custom role creation in Google Cloud Platform (GCP). Custom roles are user-defined, and allow for the bundling of one or more supported permissions to meet specific needs. Custom roles will not be updated automatically and could lead to privilege creep if not carefully scrutinized.",
"kibana.alert.rule.enabled": true,
"kibana.alert.rule.exceptions_list": [
],
"kibana.alert.rule.execution.timestamp": "2024-09-01T20:39:03.657Z",
"kibana.alert.rule.execution.timestamp": "2024-09-01T12:39:03.657Z",
"kibana.alert.rule.execution.uuid": "939d34e1-1e74-480d-90ae-24079d9b40d3",
"kibana.alert.rule.false_positives": [
"Custom role creations may be done by a system or network administrator. Verify whether the user email, resource name, and/or hostname should be making changes in your environment. Role creations by unfamiliar users or hosts should be investigated. If known behavior is causing false positives, it can be exempted from the rule."
@ -657,12 +657,12 @@
"kibana.alert.rule.timestamp_override": "event.ingested",
"kibana.alert.rule.to": "now",
"kibana.alert.rule.type": "query",
"kibana.alert.rule.updated_at": "2024-09-01T20:39:00.099Z",
"kibana.alert.rule.updated_at": "2024-09-01T12:39:00.099Z",
"kibana.alert.rule.updated_by": "elastic",
"kibana.alert.rule.uuid": "c6f64115-5941-46ef-bfa3-61a4ecb4f3ba",
"kibana.alert.rule.version": 104,
"kibana.alert.severity": "medium",
"kibana.alert.start": "2024-09-01T20:39:03.657Z",
"kibana.alert.start": "2024-09-01T12:39:03.657Z",
"kibana.alert.status": "active",
"kibana.alert.uuid": "838ea37ab43ab7d2754d007fbe8191be53d7d637bea62f6189f8db1503c0e250",
"kibana.alert.workflow_assignee_ids": [

View file

@ -17,8 +17,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
const pageObjects = getPageObjects(['common', 'header', 'alerts']);
const alertsPage = pageObjects.alerts;
// Failing: See https://github.com/elastic/kibana/issues/198632
describe.skip('Security Alerts Page - Graph visualization', function () {
describe('Security Alerts Page - Graph visualization', function () {
this.tags(['cloud_security_posture_graph_viz']);
before(async () => {