forked from elastic/kibana
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
adding finding route in backend (elastic#61)
* adding finding a route in backend
- Loading branch information
Showing
6 changed files
with
227 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
75 changes: 75 additions & 0 deletions
75
x-pack/plugins/cloud_security_posture/server/routes/findings/findings.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
/* | ||
* 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 { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mocks'; | ||
|
||
import { getLatestCycleIds } from './get_latest_cycle_ids'; | ||
|
||
const mockEsClient = elasticsearchClientMock.createClusterClient().asScoped().asInternalUser; | ||
|
||
afterEach(() => { | ||
mockEsClient.search.mockClear(); | ||
mockEsClient.count.mockClear(); | ||
}); | ||
|
||
describe('get latest cycle ids', () => { | ||
it('expect for empty response from client and get undefined', async () => { | ||
const response = await getLatestCycleIds(mockEsClient); | ||
expect(response).toEqual(undefined); | ||
}); | ||
|
||
it('expect to find empty bucket', async () => { | ||
mockEsClient.search.mockResolvedValueOnce( | ||
// @ts-expect-error @elastic/elasticsearch Aggregate only allows unknown values | ||
elasticsearchClientMock.createSuccessTransportRequestPromise({ | ||
aggregations: { | ||
group: { | ||
buckets: [{}], | ||
}, | ||
}, | ||
}) | ||
); | ||
const response = await getLatestCycleIds(mockEsClient); | ||
expect(response).toEqual(undefined); | ||
}); | ||
|
||
it('expect to find 1 cycle id', async () => { | ||
mockEsClient.search.mockResolvedValueOnce( | ||
// @ts-expect-error @elastic/elasticsearch Aggregate only allows unknown values | ||
elasticsearchClientMock.createSuccessTransportRequestPromise({ | ||
aggregations: { | ||
group: { | ||
buckets: [ | ||
{ group_docs: { hits: { hits: [{ fields: { 'run_id.keyword': ['randomId1'] } }] } } }, | ||
], | ||
}, | ||
}, | ||
}) | ||
); | ||
const response = await getLatestCycleIds(mockEsClient); | ||
expect(response).toEqual(expect.arrayContaining(['randomId1'])); | ||
}); | ||
|
||
it('expect to find mutiple cycle ids', async () => { | ||
mockEsClient.search.mockResolvedValueOnce( | ||
// @ts-expect-error @elastic/elasticsearch Aggregate only allows unknown values | ||
elasticsearchClientMock.createSuccessTransportRequestPromise({ | ||
aggregations: { | ||
group: { | ||
buckets: [ | ||
{ group_docs: { hits: { hits: [{ fields: { 'run_id.keyword': ['randomId1'] } }] } } }, | ||
{ group_docs: { hits: { hits: [{ fields: { 'run_id.keyword': ['randomId2'] } }] } } }, | ||
{ group_docs: { hits: { hits: [{ fields: { 'run_id.keyword': ['randomId3'] } }] } } }, | ||
], | ||
}, | ||
}, | ||
}) | ||
); | ||
const response = await getLatestCycleIds(mockEsClient); | ||
expect(response).toEqual(expect.arrayContaining(['randomId1', 'randomId2', 'randomId3'])); | ||
}); | ||
}); |
79 changes: 79 additions & 0 deletions
79
x-pack/plugins/cloud_security_posture/server/routes/findings/findings.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
/* | ||
* 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 { SearchRequest, QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; | ||
|
||
import { schema as rt, TypeOf } from '@kbn/config-schema'; | ||
import type { ElasticsearchClient } from 'src/core/server'; | ||
import type { IRouter } from 'src/core/server'; | ||
import { getLatestCycleIds } from './get_latest_cycle_ids'; | ||
import { CSP_KUBEBEAT_INDEX_PATTERN, FINDINGS_ROUTH_PATH } from '../../../common/constants'; | ||
export const DEFAULT_FINDINGS_PER_PAGE = 20; | ||
type FindingsQuerySchema = TypeOf<typeof schema>; | ||
|
||
const buildQueryFilter = async ( | ||
esClient: ElasticsearchClient, | ||
queryParams: FindingsQuerySchema | ||
): Promise<QueryDslQueryContainer> => { | ||
if (queryParams.latest_cycle) { | ||
const latestCycleIds = await getLatestCycleIds(esClient); | ||
if (!!latestCycleIds) { | ||
const filter = latestCycleIds.map((latestCycleId) => ({ | ||
term: { 'run_id.keyword': latestCycleId }, | ||
})); | ||
|
||
return { | ||
bool: { filter }, | ||
}; | ||
} | ||
} | ||
return { | ||
match_all: {}, | ||
}; | ||
}; | ||
|
||
const getFindingsEsQuery = async ( | ||
esClient: ElasticsearchClient, | ||
queryParams: FindingsQuerySchema | ||
): Promise<SearchRequest> => { | ||
const query = await buildQueryFilter(esClient, queryParams); | ||
return { | ||
index: CSP_KUBEBEAT_INDEX_PATTERN, | ||
query, | ||
size: queryParams.per_page, | ||
from: | ||
queryParams.page <= 1 | ||
? 0 | ||
: queryParams.page * queryParams.per_page - queryParams.per_page + 1, | ||
}; | ||
}; | ||
|
||
export const defineFindingsIndexRoute = (router: IRouter): void => | ||
router.get( | ||
{ | ||
path: FINDINGS_ROUTH_PATH, | ||
validate: { query: schema }, | ||
}, | ||
async (context, request, response) => { | ||
try { | ||
const esClient = context.core.elasticsearch.client.asCurrentUser; | ||
const { query } = request; | ||
const esQuery = await getFindingsEsQuery(esClient, query); | ||
const findings = await esClient.search(esQuery); | ||
const hits = findings.body.hits.hits; | ||
return response.ok({ body: hits }); | ||
} catch (err) { | ||
return response.customError({ body: { message: err }, statusCode: 500 }); // TODO: research error handling | ||
} | ||
} | ||
); | ||
|
||
const schema = rt.object({ | ||
latest_cycle: rt.maybe(rt.boolean()), | ||
page: rt.number({ defaultValue: 1, min: 0 }), // TODO: research for pagintaion best practice | ||
per_page: rt.number({ defaultValue: DEFAULT_FINDINGS_PER_PAGE, min: 0 }), | ||
}); |
59 changes: 59 additions & 0 deletions
59
x-pack/plugins/cloud_security_posture/server/routes/findings/get_latest_cycle_ids.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
/* | ||
* 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 { AggregationsFiltersAggregate, SearchRequest } from '@elastic/elasticsearch/lib/api/types'; | ||
import type { ElasticsearchClient } from 'src/core/server'; | ||
import { AGENT_LOGS_INDEX } from '../../../common/constants'; | ||
|
||
const getAgentLogsEsQuery = (): SearchRequest => ({ | ||
index: AGENT_LOGS_INDEX, | ||
size: 0, | ||
// query: { | ||
// bool: { | ||
// filter: [ | ||
// { term: { 'event_status.keyword': 'end' } }, // TODO: commment out when updateing agent to dend logs | ||
// ], | ||
// }, | ||
// }, | ||
aggs: { | ||
group: { | ||
terms: { field: 'agent.id.keyword' }, | ||
aggs: { | ||
group_docs: { | ||
top_hits: { | ||
size: 1, | ||
sort: [{ '@timestamp': { order: 'desc' } }], | ||
}, | ||
}, | ||
}, | ||
}, | ||
}, | ||
fields: ['run_id.keyword', 'agent.id.keyword'], | ||
_source: false, | ||
}); | ||
|
||
const getCycleId = (v: any): string => v.group_docs.hits.hits?.[0]?.fields['run_id.keyword'][0]; | ||
|
||
export const getLatestCycleIds = async ( | ||
esClient: ElasticsearchClient | ||
): Promise<string[] | undefined> => { | ||
try { | ||
const agentLogs = await esClient.search(getAgentLogsEsQuery()); | ||
const aggregations = agentLogs.body.aggregations; | ||
if (!aggregations) { | ||
return; | ||
} | ||
const buckets = (aggregations.group as Record<string, AggregationsFiltersAggregate>).buckets; | ||
if (!Array.isArray(buckets)) { | ||
return; | ||
} | ||
return buckets.map(getCycleId); | ||
} catch (err) { | ||
// TODO: return meaningful error message | ||
return; | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters