Skip to content
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
390af80
add initial Scout Osquery tests
csr Mar 19, 2026
89ead7f
use correct matcher
csr Mar 19, 2026
3f966b9
fix spacing
csr Mar 19, 2026
1c3bedb
capitalize Osquery
csr Mar 19, 2026
05032cd
use `apiServices` instead of `kbnClient` or `apiClient` for setup and…
csr Mar 19, 2026
f89c521
add API tests that test permission bounderies (shoul return unauthori…
csr Mar 19, 2026
bc87674
split monolithic API test into three focused tests
csr Mar 19, 2026
b26ca4d
define common constants file
csr Mar 19, 2026
58d18b8
split sequential test into multiple parallel tests + global setup hook
csr Mar 19, 2026
d457e10
minor tweaks to UI tests
csr Mar 19, 2026
b643cf5
remove UI tests (will create a separate follow-up PR)
csr Mar 19, 2026
5bbca9c
remove FTR tests that were migrated to Scout
csr Mar 19, 2026
f82900d
Merge branch 'main' into scout-osquery-first-steps
csr Mar 19, 2026
a7ed131
Merge branch 'main' into scout-osquery-first-steps
csr Mar 20, 2026
e383d22
Fix `createdBy` query parameter encoding for MKI compatibility
csr Mar 20, 2026
10796be
Changes from node scripts/eslint_all_files --no-cache --fix
kibanamachine Mar 20, 2026
7ae9d3e
Merge branch 'main' into scout-osquery-first-steps
csr Mar 24, 2026
e207506
check for 404 not found response
csr Mar 24, 2026
c14bfc2
temporarily exclude test suites from ECH test runs
csr Mar 24, 2026
ab4cfdb
replace `deploymentAgnostic` tags with Security-specific tags
csr Mar 24, 2026
3d191fd
add `tsconfig.json`
csr Mar 24, 2026
5cdeabb
re-add FTR test suite (without tests that are already migrated)
csr Mar 25, 2026
88ba38c
filter pack by `enabled` status (both `true` and `false`)
csr Mar 25, 2026
e300d7e
update comments
csr Mar 25, 2026
c9e22d6
update tags
csr Mar 25, 2026
455163d
update comments
csr Mar 25, 2026
e381663
remove `tsconfig.json`
csr Mar 25, 2026
7680bf8
add TODO comment
csr Mar 25, 2026
4d281f4
update plugin's `tsconfig.json`
csr Mar 25, 2026
288bb06
Changes from node scripts/lint_ts_projects --fix
kibanamachine Mar 25, 2026
99b381c
Changes from node scripts/regenerate_moon_projects.js --update
kibanamachine Mar 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .buildkite/scout_ci_config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ plugins:
- navigation
- observability
- observability_onboarding
- osquery

Copy link
Copy Markdown
Member Author

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.

- painless_lab
- profiling
- search_getting_started
Expand Down
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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Low tests/packs_admin.spec.ts:55

findResponse.body.data[0] on line 57 throws TypeError when the search returns an empty array. The check on line 56 only verifies data is defined, not that it contains elements. If the pack isn't found due to eventual consistency, data[0] is undefined and 'created_by_profile_uid' in undefined throws. Consider asserting data.length > 0 before the element access, or use optional chaining with a fallback assertion.

      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:
In file x-pack/platform/plugins/shared/osquery/test/scout/api/tests/packs_admin.spec.ts around lines 55-58:

`findResponse.body.data[0]` on line 57 throws `TypeError` when the search returns an empty array. The check on line 56 only verifies `data` is defined, not that it contains elements. If the pack isn't found due to eventual consistency, `data[0]` is `undefined` and `'created_by_profile_uid' in undefined` throws. Consider asserting `data.length > 0` before the element access, or use optional chaining with a fallback assertion.

Evidence trail:
x-pack/platform/plugins/shared/osquery/test/scout/api/tests/packs_admin.spec.ts lines 56-57 at REVIEWED_COMMIT - shows `expect(findResponse.body.data).toBeDefined()` followed by `expect('created_by_profile_uid' in findResponse.body.data[0]).toBe(true)` with no length check between them.

}
);
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 }) => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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 ?enabled=true to verify the filter works. The FTR equivalent does this and asserts only 1 result comes back. Might be worth adding since it's in the test name.

@csr csr Mar 25, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. We now filter packs by both enabled=true (existing coverage) and false (new coverage) in 88ba38c.

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);
});
}
);
Loading
Loading