mirror of
https://github.com/elastic/kibana.git
synced 2025-06-28 19:13:14 -04:00
## Summary Enhances the graph API to support filtering by bool query. Graph API is an internal API that hasn't been released yet to ESS, and is not available yet on serverless (behind a feature-flag in kibana.config) due to the above I don't consider it a breaking change. Previous API request body: ```js query: schema.object({ actorIds: schema.arrayOf(schema.string()), eventIds: schema.arrayOf(schema.string()), // TODO: use zod for range validation instead of config schema start: schema.oneOf([schema.number(), schema.string()]), end: schema.oneOf([schema.number(), schema.string()]), ``` New API request body: ```js nodesLimit: schema.maybe(schema.number()), // Maximum number of nodes in the graph (currently the graph doesn't handle very well graph with over 100 nodes) showUnknownTarget: schema.maybe(schema.boolean()), // Whether or not to return events that miss target.entity.id query: schema.object({ eventIds: schema.arrayOf(schema.string()), // Event ids that triggered the alert, would be marked in red // TODO: use zod for range validation instead of config schema start: schema.oneOf([schema.number(), schema.string()]), end: schema.oneOf([schema.number(), schema.string()]), esQuery: schema.maybe( // elasticsearch's dsl bool query schema.object({ bool: schema.object({ filter: schema.maybe(schema.arrayOf(schema.object({}, { unknowns: 'allow' }))), must: schema.maybe(schema.arrayOf(schema.object({}, { unknowns: 'allow' }))), should: schema.maybe(schema.arrayOf(schema.object({}, { unknowns: 'allow' }))), must_not: schema.maybe(schema.arrayOf(schema.object({}, { unknowns: 'allow' }))), }), }) ``` New field to the graph API response (pseudo): ```js messages?: ApiMessageCode[] enum ApiMessageCode { ReachedNodesLimit = 'REACHED_NODES_LIMIT', } ``` ### How to test Toggle feature flag in kibana.dev.yml ```yaml xpack.securitySolution.enableExperimental: ['graphVisualizationInFlyoutEnabled'] ``` To test through the UI you can use the mocked data ```bash node scripts/es_archiver load x-pack/test/cloud_security_posture_functional/es_archives/logs_gcp_audit \ --es-url http://elastic:changeme@localhost:9200 \ --kibana-url http://elastic:changeme@localhost:5601 node scripts/es_archiver load x-pack/test/cloud_security_posture_functional/es_archives/security_alerts \ --es-url http://elastic:changeme@localhost:9200 \ --kibana-url http://elastic:changeme@localhost:5601 ``` 1. Go to the alerts page 2. Change the query time range to show alerts from the 13th of October 2024 (**IMPORTANT**) 3. Open the alerts flyout 5. Scroll to see the graph visualization : D To test **only** the API you can use the mocked data ```bash node scripts/es_archiver load x-pack/test/cloud_security_posture_api/es_archives/logs_gcp_audit \ --es-url http://elastic:changeme@localhost:9200 \ --kibana-url http://elastic:changeme@localhost:5601 ``` And through dev tools: ``` POST kbn:/internal/cloud_security_posture/graph?apiVersion=1 { "query": { "eventIds": [], "start": "now-1y/y", "end": "now/d", "esQuery": { "bool": { "filter": [ { "match_phrase": { "actor.entity.id": "admin@example.com" } } ] } } } } ``` ### Related PRs - https://github.com/elastic/kibana/pull/196034 - https://github.com/elastic/kibana/pull/195307 ### Checklist - [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 --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
93 lines
2.8 KiB
TypeScript
93 lines
2.8 KiB
TypeScript
/*
|
|
* 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 { RetryService } from '@kbn/ftr-common-functional-services';
|
|
import type { Agent } from 'supertest';
|
|
import type { ToolingLog } from '@kbn/tooling-log';
|
|
import type { Client as EsClient } from '@elastic/elasticsearch';
|
|
import type { CallbackHandler, Response } from 'superagent';
|
|
import expect from '@kbn/expect';
|
|
import { ELASTIC_HTTP_VERSION_HEADER } from '@kbn/core-http-common';
|
|
|
|
/**
|
|
* Checks if plugin initialization was done
|
|
* Required before indexing findings
|
|
*/
|
|
export const waitForPluginInitialized = ({
|
|
retry,
|
|
logger,
|
|
supertest,
|
|
}: {
|
|
retry: RetryService;
|
|
logger: ToolingLog;
|
|
supertest: Pick<Agent, 'get'>;
|
|
}): Promise<void> =>
|
|
retry.try(async () => {
|
|
logger.debug('Check CSP plugin is initialized');
|
|
const response = await supertest
|
|
.get('/internal/cloud_security_posture/status?check=init')
|
|
.set(ELASTIC_HTTP_VERSION_HEADER, '1')
|
|
.expect(200);
|
|
expect(response.body).to.eql({ isPluginInitialized: true });
|
|
logger.debug('CSP plugin is initialized');
|
|
});
|
|
|
|
export function result(status: number, logger?: ToolingLog): CallbackHandler {
|
|
return (err: any, res: Response) => {
|
|
if ((res?.status || err.status) !== status) {
|
|
throw new Error(
|
|
`Expected ${status} ,got ${res?.status || err.status} resp: ${
|
|
res?.body ? JSON.stringify(res.body) : err.text
|
|
}`
|
|
);
|
|
} else if (err) {
|
|
logger?.warning(`Error result ${err.text}`);
|
|
}
|
|
};
|
|
}
|
|
|
|
export class EsIndexDataProvider {
|
|
private es: EsClient;
|
|
private readonly index: string;
|
|
|
|
constructor(es: EsClient, index: string) {
|
|
this.es = es;
|
|
this.index = index;
|
|
}
|
|
|
|
async addBulk(docs: Array<Record<string, any>>, overrideTimestamp = true) {
|
|
const operations = docs.flatMap((doc) => [
|
|
{ create: { _index: this.index } },
|
|
{ ...doc, ...(overrideTimestamp ? { '@timestamp': new Date().toISOString() } : {}) },
|
|
]);
|
|
|
|
const resp = await this.es.bulk({ refresh: 'wait_for', index: this.index, operations });
|
|
expect(resp.errors).eql(false, `Error in bulk indexing: ${JSON.stringify(resp)}`);
|
|
|
|
return resp;
|
|
}
|
|
|
|
async deleteAll() {
|
|
const indexExists = await this.es.indices.exists({ index: this.index });
|
|
|
|
if (indexExists) {
|
|
return this.es.deleteByQuery({
|
|
index: this.index,
|
|
query: { match_all: {} },
|
|
refresh: true,
|
|
});
|
|
}
|
|
}
|
|
|
|
async destroyIndex() {
|
|
const indexExists = await this.es.indices.exists({ index: this.index });
|
|
|
|
if (indexExists) {
|
|
return this.es.indices.delete({ index: this.index });
|
|
}
|
|
}
|
|
}
|