-
Notifications
You must be signed in to change notification settings - Fork 8.6k
[Osquery] Add Scout API tests for saved queries, packs, and response actions #258534
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
Changes from 21 commits
390af80
89ead7f
3f966b9
1c3bedb
05032cd
f89c521
bc87674
b26ca4d
58d18b8
d457e10
b643cf5
5bbca9c
f82900d
a7ed131
e383d22
10796be
7ae9d3e
e207506
c14bfc2
ab4cfdb
3d191fd
5cdeabb
88ba38c
e300d7e
c9e22d6
455163d
e381663
7680bf8
4d281f4
288bb06
99b381c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,62 @@ | ||
| /* | ||
| * 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 { OSQUERY_API_VERSION } from '../../common/constants'; | ||
|
|
||
| export { OSQUERY_API_VERSION }; | ||
|
|
||
| export const COMMON_HEADERS = { | ||
| 'kbn-xsrf': 'some-xsrf-token', | ||
| 'x-elastic-internal-origin': 'kibana', | ||
| 'Content-Type': 'application/json;charset=UTF-8', | ||
| 'elastic-api-version': OSQUERY_API_VERSION, | ||
| } as const; | ||
|
|
||
| export const API_PATHS = { | ||
| DETECTION_RULES: 'api/detection_engine/rules', | ||
| OSQUERY_SAVED_QUERIES: 'api/osquery/saved_queries', | ||
| OSQUERY_PACKS: 'api/osquery/packs', | ||
| } as const; | ||
|
|
||
| const uniqueId = () => `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; | ||
|
|
||
| export const getMinimalRule = (overrides: Record<string, unknown> = {}) => ({ | ||
| type: 'query', | ||
| index: ['auditbeat-*'], | ||
| language: 'kuery', | ||
| query: '_id:*', | ||
| name: `Test rule ${uniqueId()}`, | ||
| description: 'Test rule for Osquery response actions', | ||
| risk_score: 21, | ||
| severity: 'low', | ||
| interval: '5m', | ||
| from: 'now-360s', | ||
| to: 'now', | ||
| enabled: false, | ||
| ...overrides, | ||
| }); | ||
|
|
||
| export const getMinimalPack = (overrides: Record<string, unknown> = {}) => ({ | ||
| name: `test-pack-${uniqueId()}`, | ||
| description: 'Test pack for Osquery Scout tests', | ||
| enabled: true, | ||
| queries: { | ||
| testQuery: { | ||
| query: 'select * from uptime;', | ||
| interval: 3600, | ||
| }, | ||
| }, | ||
| shards: {}, | ||
| ...overrides, | ||
| }); | ||
|
|
||
| export const getMinimalSavedQuery = (overrides: Record<string, unknown> = {}) => ({ | ||
| id: `test-saved-query-${uniqueId()}`, | ||
| query: 'select 1;', | ||
| interval: '3600', | ||
| ...overrides, | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| /* | ||
| * 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 { ApiServicesFixture, ScoutTestFixtures, ScoutWorkerFixtures } from '@kbn/scout'; | ||
| import { apiTest as baseApiTest } from '@kbn/scout'; | ||
| import { | ||
| getOsqueryApiService, | ||
| type OsqueryApiService, | ||
| } from '../../common/services/osquery_api_service'; | ||
|
|
||
| export interface OsqueryApiServicesFixture extends ApiServicesFixture { | ||
| osquery: OsqueryApiService; | ||
| } | ||
|
|
||
| export const apiTest = baseApiTest.extend< | ||
| ScoutTestFixtures, | ||
| { apiServices: OsqueryApiServicesFixture } | ||
| >({ | ||
| apiServices: [ | ||
| async ( | ||
| { | ||
| apiServices, | ||
| kbnClient, | ||
| log, | ||
| }: { | ||
| apiServices: ApiServicesFixture; | ||
| kbnClient: ScoutWorkerFixtures['kbnClient']; | ||
| log: ScoutWorkerFixtures['log']; | ||
| }, | ||
| use: (extendedApiServices: OsqueryApiServicesFixture) => Promise<void> | ||
| ) => { | ||
| const extendedApiServices = apiServices as OsqueryApiServicesFixture; | ||
| extendedApiServices.osquery = getOsqueryApiService({ kbnClient, log }); | ||
| await use(extendedApiServices); | ||
| }, | ||
| { scope: 'worker' }, | ||
| ], | ||
| }); | ||
|
|
||
| export * as testData from './constants'; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| /* | ||
| * 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 { createPlaywrightConfig } from '@kbn/scout'; | ||
|
|
||
| export default createPlaywrightConfig({ | ||
| testDir: './tests', | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| /* | ||
| * 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 { RoleApiCredentials } from '@kbn/scout'; | ||
| import { tags } from '@kbn/scout'; | ||
| import { expect } from '@kbn/scout/api'; | ||
| import { apiTest, testData } from '../fixtures'; | ||
|
|
||
| // TODO: replace '@local-stateful-classic' with tags.stateful.classic once #258866 is released | ||
| apiTest.describe( | ||
| 'Osquery packs - admin', | ||
| { tag: ['@local-stateful-classic', ...tags.serverless.security.complete] }, | ||
| () => { | ||
| let adminCredentials: RoleApiCredentials; | ||
| const createdPackIds: string[] = []; | ||
|
|
||
| apiTest.beforeAll(async ({ requestAuth }) => { | ||
| adminCredentials = await requestAuth.getApiKeyForAdmin(); | ||
| }); | ||
|
|
||
| apiTest.afterAll(async ({ apiServices }) => { | ||
| for (const packId of createdPackIds) { | ||
| await apiServices.osquery.packs.delete(packId); | ||
| } | ||
| }); | ||
|
|
||
| apiTest('includes profile_uid fields on create and find', async ({ apiClient }) => { | ||
| const createResponse = await apiClient.post(testData.API_PATHS.OSQUERY_PACKS, { | ||
| headers: { ...testData.COMMON_HEADERS, ...adminCredentials.apiKeyHeader }, | ||
| body: testData.getMinimalPack(), | ||
| responseType: 'json', | ||
| }); | ||
| expect(createResponse).toHaveStatusCode(200); | ||
| expect(createResponse.body.data).toBeDefined(); | ||
| createdPackIds.push(createResponse.body.data.saved_object_id); | ||
|
|
||
| expect('created_by_profile_uid' in createResponse.body.data).toBe(true); | ||
| expect('updated_by_profile_uid' in createResponse.body.data).toBe(true); | ||
| expect(createResponse.body.data.created_by).toBeDefined(); | ||
|
|
||
| const findResponse = await apiClient.get( | ||
| `${testData.API_PATHS.OSQUERY_PACKS}?search=${createResponse.body.data.name}`, | ||
| { | ||
| headers: { ...testData.COMMON_HEADERS, ...adminCredentials.apiKeyHeader }, | ||
| responseType: 'json', | ||
| } | ||
| ); | ||
| expect(findResponse).toHaveStatusCode(200); | ||
| expect(findResponse.body.data).toBeDefined(); | ||
| expect('created_by_profile_uid' in findResponse.body.data[0]).toBe(true); | ||
| }); | ||
|
Comment on lines
+56
to
+59
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟢 Low
expect(findResponse).toHaveStatusCode(200);
expect(findResponse.body.data).toBeDefined();
- expect('created_by_profile_uid' in findResponse.body.data[0]).toBe(true);
+ expect(findResponse.body.data.length).toBeGreaterThan(0);
+ expect('created_by_profile_uid' in findResponse.body.data[0]).toBe(true);🤖 Copy this AI Prompt to have your agent fix this: |
||
| } | ||
| ); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,209 @@ | ||
| /* | ||
| * 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 { RoleApiCredentials } from '@kbn/scout'; | ||
| import { tags } from '@kbn/scout'; | ||
| import { expect } from '@kbn/scout/api'; | ||
| import { apiTest, testData } from '../fixtures'; | ||
|
|
||
| // TODO: replace '@local-stateful-classic' with tags.stateful.classic once #258883 and #259343 are released | ||
| apiTest.describe( | ||
| 'Osquery packs - editor', | ||
| { tag: ['@local-stateful-classic', ...tags.serverless.security.complete] }, | ||
| () => { | ||
| let editorCredentials: RoleApiCredentials; | ||
| const createdPackIds: string[] = []; | ||
|
|
||
| apiTest.beforeAll(async ({ requestAuth }) => { | ||
| editorCredentials = await requestAuth.getApiKeyForPrivilegedUser(); | ||
| }); | ||
|
|
||
| apiTest.afterAll(async ({ apiServices }) => { | ||
| for (const packId of createdPackIds) { | ||
| await apiServices.osquery.packs.delete(packId); | ||
| } | ||
| }); | ||
|
|
||
| apiTest('creates and reads a pack', async ({ apiClient }) => { | ||
| const packBody = testData.getMinimalPack(); | ||
| const createResponse = await apiClient.post(testData.API_PATHS.OSQUERY_PACKS, { | ||
| headers: { ...testData.COMMON_HEADERS, ...editorCredentials.apiKeyHeader }, | ||
| body: packBody, | ||
| responseType: 'json', | ||
| }); | ||
| expect(createResponse).toHaveStatusCode(200); | ||
| expect(createResponse.body.data).toBeDefined(); | ||
| const packId = createResponse.body.data.saved_object_id; | ||
| createdPackIds.push(packId); | ||
|
|
||
| const readResponse = await apiClient.get(`${testData.API_PATHS.OSQUERY_PACKS}/${packId}`, { | ||
| headers: { ...testData.COMMON_HEADERS, ...editorCredentials.apiKeyHeader }, | ||
| responseType: 'json', | ||
| }); | ||
| expect(readResponse).toHaveStatusCode(200); | ||
| expect(readResponse.body.data).toBeDefined(); | ||
| expect(readResponse.body.data.name).toBe(packBody.name); | ||
| expect(readResponse.body.data.queries.testQuery.query).toBe('select * from uptime;'); | ||
| }); | ||
|
|
||
| apiTest('creates a pack with multiple queries', async ({ apiClient }) => { | ||
| const packBody = testData.getMinimalPack({ | ||
| queries: { | ||
| memoryInfo: { | ||
| query: 'SELECT * FROM memory_info;', | ||
| interval: 3600, | ||
| platform: 'linux', | ||
| }, | ||
| systemInfo: { | ||
| query: 'SELECT * FROM system_info;', | ||
| interval: 600, | ||
| }, | ||
| uptimeQuery: { | ||
| query: 'select * from uptime;', | ||
| interval: 900, | ||
| platform: 'darwin', | ||
| }, | ||
| }, | ||
| }); | ||
|
|
||
| const createResponse = await apiClient.post(testData.API_PATHS.OSQUERY_PACKS, { | ||
| headers: { ...testData.COMMON_HEADERS, ...editorCredentials.apiKeyHeader }, | ||
| body: packBody, | ||
| responseType: 'json', | ||
| }); | ||
| expect(createResponse).toHaveStatusCode(200); | ||
| expect(createResponse.body.data).toBeDefined(); | ||
| createdPackIds.push(createResponse.body.data.saved_object_id); | ||
|
|
||
| const { queries } = createResponse.body.data; | ||
| expect(Object.keys(queries)).toHaveLength(3); | ||
| expect(queries.memoryInfo.query).toBe('SELECT * FROM memory_info;'); | ||
| expect(queries.memoryInfo.platform).toBe('linux'); | ||
| expect(queries.systemInfo.interval).toBe(600); | ||
| }); | ||
|
|
||
| apiTest('updates a pack', async ({ apiClient }) => { | ||
| const packBody = testData.getMinimalPack(); | ||
| const createResponse = await apiClient.post(testData.API_PATHS.OSQUERY_PACKS, { | ||
| headers: { ...testData.COMMON_HEADERS, ...editorCredentials.apiKeyHeader }, | ||
| body: packBody, | ||
| responseType: 'json', | ||
| }); | ||
| expect(createResponse).toHaveStatusCode(200); | ||
| expect(createResponse.body.data).toBeDefined(); | ||
| const packId = createResponse.body.data.saved_object_id; | ||
| createdPackIds.push(packId); | ||
|
|
||
| const updatedName = `${packBody.name}-updated`; | ||
| const updateResponse = await apiClient.put(`${testData.API_PATHS.OSQUERY_PACKS}/${packId}`, { | ||
| headers: { ...testData.COMMON_HEADERS, ...editorCredentials.apiKeyHeader }, | ||
| body: { | ||
| ...packBody, | ||
| name: updatedName, | ||
| enabled: false, | ||
| queries: { | ||
| ...packBody.queries, | ||
| newQuery: { query: 'select * from processes;', interval: 1800 }, | ||
| }, | ||
| }, | ||
| responseType: 'json', | ||
| }); | ||
| expect(updateResponse).toHaveStatusCode(200); | ||
|
|
||
| const readResponse = await apiClient.get(`${testData.API_PATHS.OSQUERY_PACKS}/${packId}`, { | ||
| headers: { ...testData.COMMON_HEADERS, ...editorCredentials.apiKeyHeader }, | ||
| responseType: 'json', | ||
| }); | ||
| expect(readResponse).toHaveStatusCode(200); | ||
| expect(readResponse.body.data).toBeDefined(); | ||
| expect(readResponse.body.data.name).toBe(updatedName); | ||
| expect(readResponse.body.data.enabled).toBe(false); | ||
| expect(Object.keys(readResponse.body.data.queries)).toHaveLength(2); | ||
| }); | ||
|
|
||
| apiTest('deletes a pack', async ({ apiClient }) => { | ||
| const createResponse = await apiClient.post(testData.API_PATHS.OSQUERY_PACKS, { | ||
| headers: { ...testData.COMMON_HEADERS, ...editorCredentials.apiKeyHeader }, | ||
| body: testData.getMinimalPack(), | ||
| responseType: 'json', | ||
| }); | ||
| expect(createResponse).toHaveStatusCode(200); | ||
| expect(createResponse.body.data).toBeDefined(); | ||
| const packId = createResponse.body.data.saved_object_id; | ||
|
|
||
| const deleteResponse = await apiClient.delete( | ||
| `${testData.API_PATHS.OSQUERY_PACKS}/${packId}`, | ||
| { | ||
| headers: { ...testData.COMMON_HEADERS, ...editorCredentials.apiKeyHeader }, | ||
| responseType: 'json', | ||
| } | ||
| ); | ||
| expect(deleteResponse).toHaveStatusCode(200); | ||
|
|
||
| const readResponse = await apiClient.get(`${testData.API_PATHS.OSQUERY_PACKS}/${packId}`, { | ||
| headers: { ...testData.COMMON_HEADERS, ...editorCredentials.apiKeyHeader }, | ||
| responseType: 'json', | ||
| }); | ||
|
|
||
| expect(readResponse).toHaveStatusCode(404); | ||
| }); | ||
|
|
||
| apiTest('finds packs with search and enabled filters', async ({ apiClient }) => { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Small thing: this test creates alpha (enabled=true) and beta (enabled=false) but never actually queries with
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good point. We now filter packs by both |
||
| const uniquePrefix = `findtest-${Date.now()}`; | ||
| let createdByUser: string | undefined; | ||
|
|
||
| for (const [suffix, enabled] of [ | ||
| ['alpha', true], | ||
| ['beta', false], | ||
| ] as const) { | ||
| const createResponse = await apiClient.post(testData.API_PATHS.OSQUERY_PACKS, { | ||
| headers: { ...testData.COMMON_HEADERS, ...editorCredentials.apiKeyHeader }, | ||
| body: testData.getMinimalPack({ name: `${uniquePrefix}-${suffix}`, enabled }), | ||
| responseType: 'json', | ||
| }); | ||
| expect(createResponse).toHaveStatusCode(200); | ||
| expect(createResponse.body.data).toBeDefined(); | ||
| createdPackIds.push(createResponse.body.data.saved_object_id); | ||
| createdByUser ??= createResponse.body.data.created_by; | ||
| } | ||
|
|
||
| expect(createdByUser).toBeDefined(); | ||
|
|
||
| const searchResponse = await apiClient.get( | ||
| `${testData.API_PATHS.OSQUERY_PACKS}?search=${encodeURIComponent(uniquePrefix)}`, | ||
| { | ||
| headers: { ...testData.COMMON_HEADERS, ...editorCredentials.apiKeyHeader }, | ||
| responseType: 'json', | ||
| } | ||
| ); | ||
| expect(searchResponse).toHaveStatusCode(200); | ||
| expect(searchResponse.body.total).toBe(2); | ||
|
|
||
| const noMatchResponse = await apiClient.get( | ||
| `${testData.API_PATHS.OSQUERY_PACKS}?search=zzzznonexistent999`, | ||
| { | ||
| headers: { ...testData.COMMON_HEADERS, ...editorCredentials.apiKeyHeader }, | ||
| responseType: 'json', | ||
| } | ||
| ); | ||
| expect(noMatchResponse).toHaveStatusCode(200); | ||
| expect(noMatchResponse.body.total).toBe(0); | ||
|
|
||
| const createdByResponse = await apiClient.get( | ||
| `${testData.API_PATHS.OSQUERY_PACKS}?search=${encodeURIComponent( | ||
| uniquePrefix | ||
| )}&createdBy=${encodeURIComponent(createdByUser!)}`, | ||
| { | ||
| headers: { ...testData.COMMON_HEADERS, ...editorCredentials.apiKeyHeader }, | ||
| responseType: 'json', | ||
| } | ||
| ); | ||
| expect(createdByResponse).toHaveStatusCode(200); | ||
| expect(createdByResponse.body.total).toBe(2); | ||
| }); | ||
| } | ||
| ); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This first batch of tests can run in the CI without any Scout customs servers config setup.