Skip to content

Commit 56b5b1e

Browse files
[Security Solution][Attacks/Alerts][Setup and miscellaneous] Attacks indices RBAC (elastic#243079) (elastic#244667)
## Summary Related to elastic#243079 Here we introduce a new internal attack discovery endpoint that returns missing index privileges for the users. There are two indices user should have access to in order to be able to work with attack discovery feature: `.alerts-security.attack.discovery.alerts-*` and `.adhoc.alerts-security.attack.discovery.alerts`. The required privileges are `read`, `write`, `view_index_metadata` and `maintenance`. The endpoint checks whether any of those required privileges are missing for those two indices and returns the information about it in a form like this: ``` [ { indexName: '.alerts-security.attack.discovery.alerts-default', privileges: ['read', 'write', 'view_index_metadata', 'maintenance'], }, { indexName: '.adhoc.alerts-security.attack.discovery.alerts-default', privileges: ['read', 'write', 'view_index_metadata', 'maintenance'], }, ] ``` ## Testing 1. Create a role which has attack discovery kibana privileges and lacks index privileges to one of the indices: `.alerts-security.attack.discovery.alerts-*` and/or `.adhoc.alerts-security.attack.discovery.alerts`. 2. Use this cURL request to get missing privileges information ``` curl --location 'http://localhost:5601/internal/elastic_assistant/attack_discovery/_missing_privileges' \ --header 'kbn-xsrf: true' \ --header 'elastic-api-version: 1' \ --header 'x-elastic-internal-origin: security-solution' ``` **NOTE**: You need to add credentials of the user with the created role rights in this request. --------- Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
1 parent 2f4cfb1 commit 56b5b1e

30 files changed

Lines changed: 1439 additions & 5 deletions

File tree

.buildkite/ftr_security_serverless_configs.yml

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -111,13 +111,11 @@ enabled:
111111
- x-pack/solutions/security/test/security_solution_api_integration/test_suites/genai/nlp_cleanup_task/trial_license_complete_tier/configs/serverless.config.ts
112112
- x-pack/solutions/security/test/security_solution_api_integration/test_suites/genai/nlp_cleanup_task/basic_license_essentials_tier/configs/serverless.config.ts
113113
- x-pack/solutions/security/test/security_solution_api_integration/test_suites/genai/startup/trial_license_complete_tier/configs/serverless.config.ts
114-
- x-pack/solutions/security/test/security_solution_api_integration/test_suites/genai/knowledge_base/entries/trial_license_complete_tier/configs/ess.config.ts
115114
- x-pack/solutions/security/test/security_solution_api_integration/test_suites/genai/knowledge_base/entries/trial_license_complete_tier/configs/serverless.config.ts
116-
- x-pack/solutions/security/test/security_solution_api_integration/test_suites/genai/attack_discovery/schedules/trial_license_complete_tier/configs/ess.config.ts
115+
- x-pack/solutions/security/test/security_solution_api_integration/test_suites/genai/attack_discovery/privileges/trial_license_complete_tier/configs/serverless.config.ts
116+
- x-pack/solutions/security/test/security_solution_api_integration/test_suites/genai/attack_discovery/privileges/basic_license_essentials_tier/configs/serverless.config.ts
117117
- x-pack/solutions/security/test/security_solution_api_integration/test_suites/genai/attack_discovery/schedules/trial_license_complete_tier/configs/serverless.config.ts
118-
- x-pack/solutions/security/test/security_solution_api_integration/test_suites/genai/attack_discovery/schedules/basic_license_essentials_tier/configs/ess.config.ts
119118
- x-pack/solutions/security/test/security_solution_api_integration/test_suites/genai/attack_discovery/schedules/basic_license_essentials_tier/configs/serverless.config.ts
120-
- x-pack/solutions/security/test/security_solution_api_integration/test_suites/genai/conversations/trial_license_complete_tier/configs/ess.config.ts
121119
- x-pack/solutions/security/test/security_solution_api_integration/test_suites/genai/conversations/trial_license_complete_tier/configs/serverless.config.ts
122120
- x-pack/solutions/security/test/security_solution_api_integration/test_suites/entity_analytics/risk_engine/trial_license_complete_tier/configs/serverless.config.ts
123121
- x-pack/solutions/security/test/security_solution_api_integration/test_suites/entity_analytics/risk_engine/basic_license_essentials_tier/configs/serverless.config.ts

.buildkite/ftr_security_stateful_configs.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,12 @@ enabled:
9595
- x-pack/solutions/security/test/security_solution_api_integration/test_suites/detections_response/telemetry/trial_license_complete_tier/configs/ess.config.ts
9696
- x-pack/solutions/security/test/security_solution_api_integration/test_suites/telemetry/configs/ess.config.ts
9797
- x-pack/solutions/security/test/security_solution_api_integration/test_suites/detections_response/user_roles/trial_license_complete_tier/configs/ess.config.ts
98+
- x-pack/solutions/security/test/security_solution_api_integration/test_suites/genai/knowledge_base/entries/trial_license_complete_tier/configs/ess.config.ts
99+
- x-pack/solutions/security/test/security_solution_api_integration/test_suites/genai/attack_discovery/privileges/trial_license_complete_tier/configs/ess.config.ts
100+
- x-pack/solutions/security/test/security_solution_api_integration/test_suites/genai/attack_discovery/privileges/basic_license_essentials_tier/configs/ess.config.ts
101+
- x-pack/solutions/security/test/security_solution_api_integration/test_suites/genai/attack_discovery/schedules/trial_license_complete_tier/configs/ess.config.ts
102+
- x-pack/solutions/security/test/security_solution_api_integration/test_suites/genai/attack_discovery/schedules/basic_license_essentials_tier/configs/ess.config.ts
103+
- x-pack/solutions/security/test/security_solution_api_integration/test_suites/genai/conversations/trial_license_complete_tier/configs/ess.config.ts
98104
- x-pack/solutions/security/test/security_solution_api_integration/test_suites/entity_analytics/risk_engine/trial_license_complete_tier/configs/ess.config.ts
99105
- x-pack/solutions/security/test/security_solution_api_integration/test_suites/entity_analytics/risk_engine/basic_license_essentials_tier/configs/ess.config.ts
100106
- x-pack/solutions/security/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/configs/ess.config.ts

x-pack/platform/packages/shared/kbn-elastic-assistant-common/constants.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,9 +105,13 @@ export const ATTACK_DISCOVERY_SCHEDULES_BY_ID_DISABLE =
105105
`${ATTACK_DISCOVERY_SCHEDULES}/{id}/_disable` as const;
106106
export const ATTACK_DISCOVERY_SCHEDULES_FIND = `${ATTACK_DISCOVERY_SCHEDULES}/_find` as const;
107107

108-
// Attack discovery internal API (depreciated)
108+
// Attack discovery internal API
109109
export const ATTACK_DISCOVERY_INTERNAL =
110110
`${ELASTIC_AI_ASSISTANT_INTERNAL_URL}/attack_discovery` as const;
111+
export const ATTACK_DISCOVERY_INTERNAL_MISSING_PRIVILEGES =
112+
`${ATTACK_DISCOVERY_INTERNAL}/_missing_privileges` as const;
113+
114+
// Attack discovery internal API (depreciated)
111115
export const ATTACK_DISCOVERY_INTERNAL_BULK = `${ATTACK_DISCOVERY_INTERNAL}/_bulk` as const;
112116
export const ATTACK_DISCOVERY_INTERNAL_FIND = `${ATTACK_DISCOVERY_INTERNAL}/_find` as const;
113117
export const ATTACK_DISCOVERY_GENERATIONS_INTERNAL =
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
/*
9+
* NOTICE: Do not edit this file manually.
10+
* This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator.
11+
*
12+
* info:
13+
* title: Get Attack discovery indices missing privileges - Internal API endpoint
14+
* version: 1
15+
*/
16+
17+
import { z } from '@kbn/zod';
18+
19+
export type AttackDiscoveryMissingPrivileges = z.infer<typeof AttackDiscoveryMissingPrivileges>;
20+
export const AttackDiscoveryMissingPrivileges = z.object({
21+
/**
22+
* The index name of the privilege missing
23+
*/
24+
index_name: z.string(),
25+
/**
26+
* The index privileges level missing
27+
*/
28+
privileges: z.array(z.string()),
29+
});
30+
31+
/**
32+
* The missing index privileges required for Attack discovery
33+
*/
34+
export type GetAttackDiscoveryMissingPrivilegesInternalResponse = z.infer<
35+
typeof GetAttackDiscoveryMissingPrivilegesInternalResponse
36+
>;
37+
export const GetAttackDiscoveryMissingPrivilegesInternalResponse = z.array(
38+
AttackDiscoveryMissingPrivileges
39+
);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
openapi: 3.0.0
2+
info:
3+
title: Get Attack discovery indices missing privileges - Internal API endpoint
4+
version: '1'
5+
paths:
6+
/internal/elastic_assistant/attack_discovery/_missing_privileges:
7+
get:
8+
x-codegen-enabled: true
9+
x-labels: [ess, serverless]
10+
operationId: GetAttackDiscoveryMissingPrivilegesInternal
11+
description: Identifies the privileges required for Attack discovery and returns the missing privileges
12+
summary: Retrieves the missing privileges for Attack discovery
13+
deprecated: true
14+
tags:
15+
- attack_discovery
16+
- attack_discovery_missing_privileges
17+
responses:
18+
200:
19+
description: Indicates privileges have been retrieved correctly.
20+
content:
21+
application/json:
22+
schema:
23+
type: array
24+
description: The missing index privileges required for Attack discovery
25+
items:
26+
$ref: '#/components/schemas/AttackDiscoveryMissingPrivileges'
27+
400:
28+
description: Generic Error
29+
content:
30+
application/json:
31+
schema:
32+
type: object
33+
properties:
34+
statusCode:
35+
type: number
36+
error:
37+
type: string
38+
message:
39+
type: string
40+
41+
components:
42+
schemas:
43+
AttackDiscoveryMissingPrivileges:
44+
type: object
45+
required:
46+
- index_name
47+
- privileges
48+
properties:
49+
index_name:
50+
type: string
51+
description: The index name of the privilege missing
52+
privileges:
53+
type: array
54+
items:
55+
type: string
56+
description: The index privileges level missing

x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/schemas/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ export * from './attack_discovery/routes/internal/get/get_attack_discovery_gener
3232
export * from './attack_discovery/routes/internal/post/post_attack_discovery_generations_dismiss.route.gen';
3333
export * from './attack_discovery/routes/internal/schedules/find_attack_discovery_schedules_route.gen';
3434
export * from './attack_discovery/routes/internal/schedules/schedules.gen';
35+
export * from './attack_discovery/routes/internal/privileges/get_missing_privileges.gen';
3536

3637
export { AttackDiscoveryApiSchedule } from './attack_discovery/routes/public/schedules/schedules_api.gen';
3738

x-pack/solutions/security/plugins/elastic_assistant/server/__mocks__/request.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ import {
5252
ELASTIC_AI_ASSISTANT_PROMPTS_URL_BULK_ACTION,
5353
ELASTIC_AI_ASSISTANT_PROMPTS_URL_FIND,
5454
ELASTIC_AI_ASSISTANT_SECURITY_AI_PROMPTS_URL_FIND,
55+
ATTACK_DISCOVERY_INTERNAL_MISSING_PRIVILEGES,
5556
} from '@kbn/elastic-assistant-common';
5657
import {
5758
getAppendConversationMessagesSchemaMock,
@@ -332,6 +333,12 @@ export const postDefendInsightsRequest = (body: DefendInsightsPostRequestBody) =
332333
body,
333334
});
334335

336+
export const getAttackDiscoveryMissingPrivilegesRequest = () =>
337+
requestMock.create({
338+
method: 'get',
339+
path: ATTACK_DISCOVERY_INTERNAL_MISSING_PRIVILEGES,
340+
});
341+
335342
export const findAttackDiscoverySchedulesRequest = () =>
336343
requestMock.create({
337344
method: 'get',
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
import type { KibanaRequest } from '@kbn/core-http-server';
9+
import { httpServiceMock, httpServerMock } from '@kbn/core-http-server-mocks';
10+
import type { SecurityHasPrivilegesResponse } from '@elastic/elasticsearch/lib/api/types';
11+
12+
import { getMissingIndexPrivilegesInternalRoute } from './get_missing_privileges';
13+
import * as helpers from '../../helpers';
14+
import type { AttackDiscoveryDataClient } from '../../../lib/attack_discovery/persistence';
15+
import { mockAuthenticatedUser } from '../../../__mocks__/mock_authenticated_user';
16+
import { requestContextMock } from '../../../__mocks__/request_context';
17+
18+
const { context: mockContext } = requestContextMock.createTools();
19+
20+
describe('getMissingIndexPrivilegesInternalRoute', () => {
21+
let router: ReturnType<typeof httpServiceMock.createRouter>;
22+
let mockRequest: Partial<KibanaRequest<unknown, unknown, unknown>>;
23+
let mockResponse: ReturnType<typeof httpServerMock.createResponseFactory>;
24+
let mockDataClient: {
25+
spaceId: string;
26+
getAdHocAlertsIndexPattern: jest.Mock;
27+
};
28+
let addVersionMock: jest.Mock;
29+
let getHandler: (ctx: unknown, req: unknown, res: unknown) => Promise<unknown>;
30+
let mockEsClient: {
31+
security: {
32+
hasPrivileges: jest.Mock;
33+
};
34+
};
35+
36+
const scheduledIndexPattern = '.alerts-security.attack.discovery.alerts-default';
37+
const adhocIndexPattern = '.internal.alerts-security.alerts-attack-discovery-adhoc-default';
38+
39+
beforeEach(() => {
40+
jest.clearAllMocks();
41+
router = httpServiceMock.createRouter();
42+
mockDataClient = {
43+
spaceId: 'default',
44+
getAdHocAlertsIndexPattern: jest.fn().mockReturnValue(adhocIndexPattern),
45+
};
46+
mockContext.elasticAssistant.getAttackDiscoveryDataClient.mockResolvedValue(
47+
mockDataClient as unknown as AttackDiscoveryDataClient
48+
);
49+
mockRequest = {};
50+
mockResponse = httpServerMock.createResponseFactory();
51+
jest
52+
.spyOn(helpers, 'performChecks')
53+
.mockResolvedValue({ isSuccess: true, currentUser: mockAuthenticatedUser });
54+
55+
mockEsClient = {
56+
security: {
57+
hasPrivileges: jest.fn(),
58+
},
59+
};
60+
(mockContext.core.elasticsearch.client.asCurrentUser as unknown) = mockEsClient;
61+
62+
addVersionMock = jest.fn();
63+
(router.versioned.get as jest.Mock).mockReturnValue({ addVersion: addVersionMock });
64+
getMissingIndexPrivilegesInternalRoute(router as never);
65+
getHandler = addVersionMock.mock.calls[0][1];
66+
});
67+
68+
it('returns 200 and an empty array when all privileges are present', async () => {
69+
const mockPrivilegesResponse: SecurityHasPrivilegesResponse = {
70+
username: 'elastic',
71+
has_all_requested: true,
72+
cluster: {},
73+
index: {
74+
[scheduledIndexPattern]: {
75+
read: true,
76+
write: true,
77+
view_index_metadata: true,
78+
maintenance: true,
79+
},
80+
[adhocIndexPattern]: {
81+
read: true,
82+
write: true,
83+
view_index_metadata: true,
84+
maintenance: true,
85+
},
86+
},
87+
application: {},
88+
};
89+
mockEsClient.security.hasPrivileges.mockResolvedValue({ body: mockPrivilegesResponse });
90+
91+
await getHandler(mockContext, mockRequest, mockResponse);
92+
93+
expect(mockResponse.ok).toHaveBeenCalledWith({
94+
body: [],
95+
});
96+
});
97+
98+
it('returns 200 and missing privileges for the scheduled index', async () => {
99+
const mockPrivilegesResponse: SecurityHasPrivilegesResponse = {
100+
username: 'elastic',
101+
has_all_requested: false,
102+
cluster: {},
103+
index: {
104+
[scheduledIndexPattern]: {
105+
read: true,
106+
write: false,
107+
view_index_metadata: true,
108+
maintenance: false,
109+
},
110+
[adhocIndexPattern]: {
111+
read: true,
112+
write: true,
113+
view_index_metadata: true,
114+
maintenance: true,
115+
},
116+
},
117+
application: {},
118+
};
119+
mockEsClient.security.hasPrivileges.mockResolvedValue({ body: mockPrivilegesResponse });
120+
121+
await getHandler(mockContext, mockRequest, mockResponse);
122+
123+
expect(mockResponse.ok).toHaveBeenCalledWith({
124+
body: [
125+
{
126+
index_name: scheduledIndexPattern,
127+
privileges: ['write', 'maintenance'],
128+
},
129+
],
130+
});
131+
});
132+
133+
it('returns 200 and missing privileges for both indices', async () => {
134+
const mockPrivilegesResponse: SecurityHasPrivilegesResponse = {
135+
username: 'elastic',
136+
has_all_requested: false,
137+
cluster: {},
138+
index: {
139+
[scheduledIndexPattern]: {
140+
read: false,
141+
write: false,
142+
view_index_metadata: true,
143+
maintenance: true,
144+
},
145+
[adhocIndexPattern]: {
146+
read: true,
147+
write: true,
148+
view_index_metadata: false,
149+
maintenance: false,
150+
},
151+
},
152+
application: {},
153+
};
154+
mockEsClient.security.hasPrivileges.mockResolvedValue({ body: mockPrivilegesResponse });
155+
156+
await getHandler(mockContext, mockRequest, mockResponse);
157+
158+
expect(mockResponse.ok).toHaveBeenCalledWith({
159+
body: [
160+
{
161+
index_name: scheduledIndexPattern,
162+
privileges: ['read', 'write'],
163+
},
164+
{
165+
index_name: adhocIndexPattern,
166+
privileges: ['view_index_metadata', 'maintenance'],
167+
},
168+
],
169+
});
170+
});
171+
172+
it('returns 500 if the data client is not initialized', async () => {
173+
mockContext.elasticAssistant.getAttackDiscoveryDataClient.mockResolvedValueOnce(null);
174+
175+
await getHandler(mockContext, mockRequest, mockResponse);
176+
177+
expect(mockResponse.custom).toHaveBeenCalledWith({
178+
body: Buffer.from(
179+
JSON.stringify({
180+
message: 'Attack discovery data client not initialized',
181+
status_code: 500,
182+
})
183+
),
184+
headers: expect.any(Object),
185+
statusCode: 500,
186+
});
187+
});
188+
189+
it('returns an error when performChecks fails', async () => {
190+
(helpers.performChecks as jest.Mock).mockResolvedValueOnce({
191+
isSuccess: false,
192+
response: { status: 403, payload: { message: 'Forbidden' } },
193+
});
194+
195+
const result = await getHandler(mockContext, mockRequest, mockResponse);
196+
197+
expect(result).toEqual({ status: 403, payload: { message: 'Forbidden' } });
198+
});
199+
200+
describe('when hasPrivileges throws', () => {
201+
const thrownError = new Error('fail!');
202+
203+
beforeEach(() => {
204+
mockEsClient.security.hasPrivileges.mockRejectedValueOnce(thrownError);
205+
});
206+
207+
it('includes the error message in the response body', async () => {
208+
await getHandler(mockContext, mockRequest, mockResponse);
209+
const customCall = mockResponse.custom?.mock.calls[0]?.[0];
210+
const bodyString = customCall && customCall.body ? customCall.body.toString() : '';
211+
212+
expect(bodyString).toContain(thrownError.message);
213+
});
214+
215+
it('returns status code 500', async () => {
216+
await getHandler(mockContext, mockRequest, mockResponse);
217+
const customCall = mockResponse.custom?.mock.calls[0]?.[0];
218+
219+
expect(customCall.statusCode).toBe(500);
220+
});
221+
});
222+
});

0 commit comments

Comments
 (0)