Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
ef1e988
feat(data-source-catalog): scaffold @kbn/data-source-catalog package
patrykkopycinski Mar 26, 2026
47f2663
feat(data-source-catalog): add types and constants
patrykkopycinski Mar 26, 2026
bba67ec
feat(data-source-catalog): add ES index mapping definition
patrykkopycinski Mar 26, 2026
34e95ba
feat(data-source-catalog): add CatalogClient for index CRUD
patrykkopycinski Mar 26, 2026
61f62c5
feat(data-source-catalog): add CatalogQuery with filter, search, fiel…
patrykkopycinski Mar 26, 2026
93ee5f6
feat(data-source-catalog): add IndexMetadataProvider
patrykkopycinski Mar 26, 2026
b5038c5
feat(data-source-catalog): add IntegrationProvider (Fleet package met…
patrykkopycinski Mar 26, 2026
2ec6beb
feat(data-source-catalog): add CatalogRefresh orchestration
patrykkopycinski Mar 26, 2026
5af0f83
feat(data-source-catalog): add SecurityCatalogService with lifecycle …
patrykkopycinski Mar 26, 2026
11c7d16
feat(data-source-catalog): add IndexStatsProvider (doc counts, freshn…
patrykkopycinski Mar 26, 2026
57be439
feat(data-source-catalog): wire IndexStatsProvider into refresh pipeline
patrykkopycinski Mar 26, 2026
e6f6508
feat(data-source-catalog): add TaskManager periodic stats refresh (6h…
patrykkopycinski Mar 26, 2026
0e0eb36
feat(data-source-catalog): add formatCatalogContextForPrompt helper
patrykkopycinski Mar 26, 2026
cf7144e
feat(data-source-catalog): add dataSourceContext parameter to Attack …
patrykkopycinski Mar 26, 2026
8e4b6eb
feat(data-source-catalog): add DataSourceCatalogTool for AI Assistant
patrykkopycinski Mar 26, 2026
7222604
feat(data-source-catalog): add DISCOVER_DATA_SOURCES node to AI Rule …
patrykkopycinski Mar 26, 2026
aabaa93
chore(data-source-catalog): format fixes
patrykkopycinski Mar 26, 2026
c31afd6
fix(data-source-catalog): update to ES client v8 API (remove body wra…
patrykkopycinski Mar 29, 2026
bf56399
fix(data-source-catalog): fix Fleet getInstalledPackages response sha…
patrykkopycinski Mar 29, 2026
06ab56d
feat(data-source-catalog): add Tier 3 semantic layer with heuristic s…
patrykkopycinski Mar 29, 2026
0b22392
feat(data-source-catalog): add CatalogIntegrationEnricher for SIEM Mi…
patrykkopycinski Mar 29, 2026
a4ecc6b
feat(data-source-catalog): add Rule Creation UI catalog data source h…
patrykkopycinski Mar 29, 2026
dfd2420
feat(data-source-catalog): add required_fields advisory validation ag…
patrykkopycinski Mar 29, 2026
cfce166
feat(data-source-catalog): add kNN vector search to CatalogQuery for …
patrykkopycinski Mar 29, 2026
30dcb8d
feat(data-source-catalog): wire CatalogSuggestions into Rule Creation…
patrykkopycinski Mar 29, 2026
aadd21b
feat(data-source-catalog): add RequiredFieldsCatalogWarnings UI compo…
patrykkopycinski Mar 29, 2026
b795f70
feat(data-source-catalog): pre-populate required_fields and related_i…
patrykkopycinski Mar 29, 2026
96e0d74
feat(data-source-catalog): make catalog refresh interval and index pa…
patrykkopycinski Mar 29, 2026
ecc66bb
Merge remote-tracking branch 'upstream/main' into feat/data-source-ca…
patrykkopycinski Mar 29, 2026
40cc5ac
fix(data-source-catalog): use shared formatter in tool, add telemetry…
patrykkopycinski Mar 29, 2026
c60fcbc
fix(data-source-catalog): address code review findings (ReDoS, race c…
patrykkopycinski Mar 29, 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 package.json
Original file line number Diff line number Diff line change
Expand Up @@ -545,6 +545,7 @@
"@kbn/data-search-plugin": "link:src/platform/test/plugin_functional/plugins/data_search",
"@kbn/data-service": "link:src/platform/packages/shared/kbn-data-service",
"@kbn/data-service-server": "link:src/platform/packages/shared/kbn-data-service-server",
"@kbn/data-source-catalog": "link:x-pack/platform/packages/shared/kbn-data-source-catalog",
"@kbn/data-sources-plugin": "link:x-pack/platform/plugins/shared/data_sources",
"@kbn/data-stream-adapter": "link:x-pack/solutions/security/packages/data-stream-adapter",
"@kbn/data-streams": "link:src/platform/packages/private/kbn-data-streams",
Expand Down
2 changes: 2 additions & 0 deletions tsconfig.base.json
Original file line number Diff line number Diff line change
Expand Up @@ -924,6 +924,8 @@
"@kbn/data-service/*": ["src/platform/packages/shared/kbn-data-service/*"],
"@kbn/data-service-server": ["src/platform/packages/shared/kbn-data-service-server"],
"@kbn/data-service-server/*": ["src/platform/packages/shared/kbn-data-service-server/*"],
"@kbn/data-source-catalog": ["x-pack/platform/packages/shared/kbn-data-source-catalog"],
"@kbn/data-source-catalog/*": ["x-pack/platform/packages/shared/kbn-data-source-catalog/*"],
"@kbn/data-sources-plugin": ["x-pack/platform/plugins/shared/data_sources"],
"@kbn/data-sources-plugin/*": ["x-pack/platform/plugins/shared/data_sources/*"],
"@kbn/data-stream-adapter": ["x-pack/solutions/security/packages/data-stream-adapter"],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# @kbn/data-source-catalog

Empty package generated by @kbn/generate
34 changes: 34 additions & 0 deletions x-pack/platform/packages/shared/kbn-data-source-catalog/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* 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.
*/

export type {
DataSourceEntry,
DataSourceType,
DataSourceStats,
DataSourceSemantic,
FreshnessCategory,
FieldMetadata,
IntegrationMetadata,
CatalogQueryParams,
CatalogQueryResult,
} from './src/types';

export {
CATALOG_INDEX_NAME,
DEFAULT_FIELD_LIMIT,
DEFAULT_SECURITY_PATTERNS,
FRESHNESS_THRESHOLDS,
CATALOG_VERSION,
} from './src/constants';

export { catalogIndexMapping } from './src/index_mapping';

export { CatalogClient } from './src/catalog_client';
export { CatalogQuery } from './src/catalog_query';
export { refreshCatalog } from './src/catalog_refresh';
export type { PackageClientLike } from './src/providers/integration_provider';
export { generateHeuristicSummary } from './src/providers/heuristic_summary_provider';
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.
*/

module.exports = {
preset: '@kbn/test/jest_node',
rootDir: '../../../../..',
roots: ['<rootDir>/x-pack/platform/packages/shared/kbn-data-source-catalog'],
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"type": "shared-server",
"id": "@kbn/data-source-catalog",
"owner": [
"@elastic/security-detection-engine"
],
"group": "platform",
"visibility": "shared"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"name": "@kbn/data-source-catalog",
"private": true,
"version": "1.0.0",
"license": "Elastic License 2.0 OR AGPL-3.0-only OR SSPL-1.0",
"sideEffects": false
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
/*
* 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 { elasticsearchServiceMock } from '@kbn/core/server/mocks';
import { CatalogClient } from './catalog_client';
import { CATALOG_INDEX_NAME } from './constants';
import { catalogIndexMapping } from './index_mapping';
import type { DataSourceEntry } from './types';

const makeEntry = (id: string): DataSourceEntry => ({
id,
name: `entry-${id}`,
type: 'index',
mapping: {
fields: [],
total_field_count: 0,
ecs_field_count: 0,
ecs_field_coverage: 0,
},
catalog_version: 1,
refreshed_at: '2024-01-01T00:00:00.000Z',
});

describe('CatalogClient', () => {
let esClient: ReturnType<typeof elasticsearchServiceMock.createElasticsearchClient>;
let catalogClient: CatalogClient;

beforeEach(() => {
esClient = elasticsearchServiceMock.createElasticsearchClient();
catalogClient = new CatalogClient(esClient);
});

describe('ensureIndex', () => {
it('creates the index if it does not exist', async () => {
esClient.indices.exists.mockResolvedValue(false);

await catalogClient.ensureIndex();

expect(esClient.indices.exists).toHaveBeenCalledWith({ index: CATALOG_INDEX_NAME });
expect(esClient.indices.create).toHaveBeenCalledWith({
index: CATALOG_INDEX_NAME,
mappings: catalogIndexMapping,
settings: {
number_of_shards: 1,
auto_expand_replicas: '0-1',
},
});
});

it('skips creation if index already exists', async () => {
esClient.indices.exists.mockResolvedValue(true);

await catalogClient.ensureIndex();

expect(esClient.indices.exists).toHaveBeenCalledWith({ index: CATALOG_INDEX_NAME });
expect(esClient.indices.create).not.toHaveBeenCalled();
});

it('does not throw when another node created the index concurrently', async () => {
esClient.indices.exists.mockResolvedValue(false);
const raceError = Object.assign(new Error('resource_already_exists_exception'), {
meta: { body: { error: { type: 'resource_already_exists_exception' } } },
});
esClient.indices.create.mockRejectedValue(raceError);

await expect(catalogClient.ensureIndex()).resolves.toBeUndefined();
});

it('re-throws non-race-condition errors from index creation', async () => {
esClient.indices.exists.mockResolvedValue(false);
const unexpectedError = new Error('unknown_error');
esClient.indices.create.mockRejectedValue(unexpectedError);

await expect(catalogClient.ensureIndex()).rejects.toThrow('unknown_error');
});
});

describe('bulkUpsert', () => {
it('indexes entries via bulk API with correct _id and _index', async () => {
esClient.bulk.mockResolvedValue({ errors: false, took: 1, items: [] });

const entries = [makeEntry('a'), makeEntry('b')];
await catalogClient.bulkUpsert(entries);

expect(esClient.bulk).toHaveBeenCalledWith({
operations: [
{ index: { _index: CATALOG_INDEX_NAME, _id: 'a' } },
entries[0],
{ index: { _index: CATALOG_INDEX_NAME, _id: 'b' } },
entries[1],
],
refresh: 'wait_for',
});
});

it('throws when bulk response has errors', async () => {
esClient.bulk.mockResolvedValue({
errors: true,
took: 1,
items: [
{ index: { _index: CATALOG_INDEX_NAME, _id: 'a', status: 400, error: { type: 'mapper_parsing_exception', reason: 'mapping error' } } },
],
});

await expect(catalogClient.bulkUpsert([makeEntry('a')])).rejects.toThrow(
'Bulk upsert had errors: mapping error'
);
});

it('returns early for empty array without calling bulk', async () => {
await catalogClient.bulkUpsert([]);

expect(esClient.bulk).not.toHaveBeenCalled();
});
});

describe('deleteAll', () => {
it('calls deleteByQuery with match_all on the catalog index', async () => {
await catalogClient.deleteAll();

expect(esClient.deleteByQuery).toHaveBeenCalledWith({
index: CATALOG_INDEX_NAME,
query: { match_all: {} },
refresh: true,
});
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
* 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 { ElasticsearchClient } from '@kbn/core-elasticsearch-server';
import { CATALOG_INDEX_NAME } from './constants';
import { catalogIndexMapping } from './index_mapping';
import type { DataSourceEntry } from './types';

export class CatalogClient {
constructor(private readonly esClient: ElasticsearchClient) {}

async ensureIndex(): Promise<void> {
const exists = await this.esClient.indices.exists({ index: CATALOG_INDEX_NAME });
if (exists) {
return;
}
try {
await this.esClient.indices.create({
index: CATALOG_INDEX_NAME,
mappings: catalogIndexMapping,
settings: {
number_of_shards: 1,
auto_expand_replicas: '0-1',
},
});
} catch (error: unknown) {
// Handle race condition: another Kibana node may have created the index
const isAlreadyExists =
error instanceof Error &&
'meta' in error &&
(error as { meta?: { body?: { error?: { type?: string } } } }).meta?.body?.error?.type ===
'resource_already_exists_exception';
if (!isAlreadyExists) {
throw error;
}
}
}

async bulkUpsert(entries: DataSourceEntry[]): Promise<void> {
if (entries.length === 0) {
return;
}

const operations = entries.flatMap((entry) => [
{ index: { _index: CATALOG_INDEX_NAME, _id: entry.id } },
entry,
]);

const result = await this.esClient.bulk({ operations, refresh: 'wait_for' });

if (result.errors) {
const errorItems = result.items
.filter((item) => item.index?.error)
.map((item) => item.index?.error?.reason)
.slice(0, 5);
throw new Error(`Bulk upsert had errors: ${errorItems.join('; ')}`);
}
}

async deleteAll(): Promise<void> {
await this.esClient.deleteByQuery({
index: CATALOG_INDEX_NAME,
query: { match_all: {} },
refresh: true,
});
}
}
Loading
Loading