Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
73cd65e
[Entity Analytics][Leads generation] Add API routes, LeadDataClient, …
abhishekbhatia1710 Mar 27, 2026
15ee065
refactor: remove dead schemas, unused params, and deprecated alias
abhishekbhatia1710 Mar 27, 2026
460e231
Merge branch 'main' of github.com:elastic/kibana into ea-15953-api-ro…
abhishekbhatia1710 Mar 27, 2026
8e6568a
Merge branch 'main' of github.com:elastic/kibana into ea-15953-api-ro…
abhishekbhatia1710 Mar 27, 2026
4723219
Merge remote-tracking branch 'upstream/main' into ea-15953-api-routes
abhishekbhatia1710 Mar 27, 2026
d0f8fb8
Address review feedback: simplify staleness, fix module naming, harde…
abhishekbhatia1710 Mar 27, 2026
f226e76
Address second round of review feedback
abhishekbhatia1710 Mar 28, 2026
e626b8f
Switch entity retrieval from fetchAllUnifiedLatestEntities to CRUDCli…
abhishekbhatia1710 Mar 28, 2026
77670e8
Delete an unwanted file
abhishekbhatia1710 Mar 28, 2026
b143f03
Merge remote-tracking branch 'upstream/main' into ea-15953-api-routes
abhishekbhatia1710 Mar 28, 2026
d7e6e67
Merge branch 'ea-15953-api-routes' of github.com:abhishekbhatia1710/k…
abhishekbhatia1710 Mar 28, 2026
bb8b1b9
Changes from node scripts/eslint_all_files --no-cache --fix
kibanamachine Mar 28, 2026
e5dd25e
Merge branch 'main' into ea-15953-api-routes
abhishekbhatia1710 Mar 30, 2026
51071a6
Merge branch 'main' into ea-15953-api-routes
jaredburgettelastic Mar 30, 2026
4cbba9f
Merge branch 'main' into ea-15953-api-routes
jaredburgettelastic Mar 30, 2026
6fc763b
Fixed merge conflict issue
jaredburgettelastic Mar 30, 2026
68eda28
Fix ES term query failures on dynamically-mapped text fields
abhishekbhatia1710 Mar 31, 2026
b686df4
Merge branch 'main' into ea-15953-api-routes
abhishekbhatia1710 Mar 31, 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
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,12 @@
export const LEAD_GENERATION_URL = '/internal/entity_analytics/leads' as const;
export const GENERATE_LEADS_URL = `${LEAD_GENERATION_URL}/generate` as const;
export const GET_LEADS_URL = LEAD_GENERATION_URL as string;
export const GET_LEAD_BY_ID_URL = `${LEAD_GENERATION_URL}/{id}` as const;
export const LEAD_GENERATION_STATUS_URL = `${LEAD_GENERATION_URL}/status` as const;
export const DISMISS_LEAD_URL = `${LEAD_GENERATION_URL}/{id}/_dismiss` as const;
export const BULK_UPDATE_LEADS_URL = `${LEAD_GENERATION_URL}/bulk_update` as const;
export const ENABLE_LEAD_GENERATION_URL = `${LEAD_GENERATION_URL}/enable` as const;
export const DISABLE_LEAD_GENERATION_URL = `${LEAD_GENERATION_URL}/disable` as const;

export type LeadGenerationMode = 'adhoc' | 'scheduled';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,6 @@ export const leadEntitySchema = z.object({
name: z.string(),
});

export type LeadEntity = z.infer<typeof leadEntitySchema>;

// ---------------------------------------------------------------------------
// Lead
// ---------------------------------------------------------------------------
Expand All @@ -74,52 +72,31 @@ export const leadSchema = z.object({

export type Lead = z.infer<typeof leadSchema>;

// ---------------------------------------------------------------------------
// Engine configuration
// ---------------------------------------------------------------------------

export const leadGenerationEngineConfigSchema = z.object({
minObservations: z.number().int().min(0).default(1),
maxLeads: z.number().int().min(1).default(10),
});

export type LeadGenerationEngineConfig = z.infer<typeof leadGenerationEngineConfigSchema>;

// ---------------------------------------------------------------------------
// API request / response schemas
// ---------------------------------------------------------------------------

export const generateLeadsRequestSchema = z.object({
maxLeads: z.number().int().min(1).max(50).optional(),
connectorId: z.string().optional(),
});

export type GenerateLeadsRequest = z.infer<typeof generateLeadsRequestSchema>;

export const findLeadsRequestSchema = z.object({
page: z.number().int().min(1).optional().default(1),
perPage: z.number().int().min(1).max(100).optional().default(20),
page: z.coerce.number().int().min(1).optional().default(1),
perPage: z.coerce.number().int().min(1).max(100).optional().default(20),
sortField: z.enum(['priority', 'timestamp']).optional().default('priority'),
sortOrder: z.enum(['asc', 'desc']).optional().default('desc'),
status: LeadStatusEnum.optional(),
});

export type FindLeadsRequest = z.infer<typeof findLeadsRequestSchema>;

export const findLeadsResponseSchema = z.object({
leads: z.array(leadSchema),
total: z.number(),
page: z.number(),
perPage: z.number(),
export const getLeadByIdRequestSchema = z.object({
id: z.string().min(1),
});

export type FindLeadsResponse = z.infer<typeof findLeadsResponseSchema>;

export const leadGenerationStatusSchema = z.object({
isEnabled: z.boolean(),
indexExists: z.boolean(),
totalLeads: z.number(),
lastRun: z.string().datetime().nullable(),
export const dismissLeadRequestSchema = z.object({
id: z.string().min(1),
});

export type LeadGenerationStatus = z.infer<typeof leadGenerationStatusSchema>;
export const bulkUpdateLeadsRequestSchema = z.object({
ids: z.array(z.string().min(1)).min(1).max(100),
status: LeadStatusEnum,
});
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import type {
Observation,
ObservationModule,
} from '../types';
import { DEFAULT_ENGINE_CONFIG, computeStaleness } from '../types';
import { DEFAULT_ENGINE_CONFIG } from '../types';
import { entityToKey } from '../observation_modules/utils';
import { llmSynthesizeLeadContent } from './llm_synthesize';

Expand Down Expand Up @@ -236,7 +236,7 @@ const groupIntoLeads = async (
priority: maxPriority,
chatRecommendations: recommendations,
timestamp: now.toISOString(),
staleness: computeStaleness(now, now),
staleness: 'fresh',
observations: allObservations,
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ Respond ONLY with a valid JSON object (no markdown fences, no extra text) matchi
{{
"title": "string - MAXIMUM 4 WORDS. A short threat label, not a sentence. Good: 'Anomalous behavior', 'Credential harvesting', 'Lateral movement detected', 'Privilege escalation'. Bad: 'Suspected Multi-Tactic Attack Targeting DevOps User with Container Escape'",
"description": "string - a narrative paragraph (plain text, NO markdown, NO bold/italic markers) connecting the evidence, referencing specific data points (scores, alert counts, escalation deltas), explaining why this matters and what the attacker may be doing. Do NOT use asterisks or markdown formatting.",
"tags": ["string array - 3 to 6 tags. Use human-readable technique or rule names, NOT numeric IDs. Good: 'Container Escape Attempt', 'Remote Service Execution', 'Credential Access via Brute Force'. Bad: 'T1075', 'T1078'. Also include short contextual tags like 'Privilege Escalation', 'Lateral Movement'."],
"tags": ["string array - 3 to 6 tags. Use human-readable technique or rule names, NOT numeric IDs. Only use rule names that appear explicitly in the observation data below; do not invent or guess rule names. Good: 'Container Escape Attempt', 'Remote Service Execution', 'Credential Access via Brute Force'. Bad: 'T1075', 'T1078'. Also include short contextual tags like 'Privilege Escalation', 'Lateral Movement'."],
"recommendations": ["string - a single chat message an analyst can paste into an AI chat assistant to start investigating. It must be a direct request or question the analyst would type. Example: 'Show me the critical/high severity alerts for user \\"jsmith\\" from the last 7 days, grouped by detection rule name, and correlate with the risk score trend over the last 30 days'. Do NOT write generic advice like 'Isolate the account' or 'Review logs'. Write an actual chat prompt."]
}}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,54 @@
* 2.0.
*/

import type { EntityStoreCRUDClient } from '@kbn/entity-store/server';
import type { Entity } from '../../../../common/api/entity_analytics/entity_store/entities/common.gen';
import type { LeadEntity } from './types';

const ENTITY_PAGE_SIZE = 1000;

/**
* Convert an Entity Store V2 record into a LeadEntity, extracting the
* convenience `type` and `name` fields from the nested `entity` object.
* Falls back to `entity.id` (EUID) when `entity.name` is absent.
*
* Accepts `Record<string, unknown>` so it works with Entity types from
* both the security_solution and entity_store plugins (structurally
* equivalent but separate Zod-generated types).
*/
export const entityRecordToLeadEntity = (record: Entity): LeadEntity => {
const entityField = (record as Record<string, unknown>).entity as
| { name?: string; type?: string; id?: string }
export const entityRecordToLeadEntity = (record: Record<string, unknown>): LeadEntity => {
const entityField = record.entity as
| { name?: string; type?: string; id?: string; EngineMetadata?: { Type?: string } }
| undefined;
return {
record,
type: entityField?.type ?? 'unknown',
record: record as Entity,
type: entityField?.EngineMetadata?.Type ?? entityField?.type ?? 'unknown',
name: entityField?.name ?? entityField?.id ?? 'unknown',
};
};

/**
* Paginate through all entities in the V2 unified index via
* `CRUDClient.listEntities()`, accumulating results across pages.
*/
export const fetchAllLeadEntities = async (
Comment thread
abhishekbhatia1710 marked this conversation as resolved.
crudClient: EntityStoreCRUDClient
): Promise<LeadEntity[]> => {
const allEntities: LeadEntity[] = [];
let searchAfter: Array<string | number> | undefined;

do {
const { entities, nextSearchAfter } = await crudClient.listEntities({
size: ENTITY_PAGE_SIZE,
searchAfter,
});

for (const entity of entities) {
allEntities.push(entityRecordToLeadEntity(entity));
}

searchAfter = nextSearchAfter;
} while (searchAfter !== undefined);

return allEntities;
};
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,19 @@
*/

export { createLeadIndexService, type LeadIndexService } from './indices';
export {
createLeadDataClient,
type LeadDataClient,
type LeadDataClientDeps,
} from './lead_data_client';
export { ObservationModuleRegistry, type ObservationEntity } from './observation_modules';
export { createLeadGenerationEngine } from './engine';
export {
createRiskScoreModule,
createTemporalStateModule,
createBehavioralAnalysisModule,
} from './observation_modules';
export { entityRecordToLeadEntity } from './entity_conversion';
export { entityRecordToLeadEntity, fetchAllLeadEntities } from './entity_conversion';
export { createLeadGenerationService } from './services/lead_generation_service';
export type {
Lead,
Expand Down
Loading
Loading