Skip to content
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

[8.x] [Security Solution] [Attack discovery] Alerts filtering (#205070) #205137

Merged
merged 2 commits into from
Dec 24, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
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 @@ -13,6 +13,7 @@ describe('getOpenAndAcknowledgedAlertsQuery', () => {
const anonymizationFields = [
{ id: 'field1', field: 'field1', allowed: true, anonymized: false },
{ id: 'field2', field: 'field2', allowed: true, anonymized: false },
{ id: 'field3', field: 'field3', allowed: false, anonymized: false },
];
const size = 10;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,34 @@

import type { AnonymizationFieldResponse } from '../../schemas/anonymization_fields/bulk_crud_anonymization_fields_route.gen';

export const DEFAULT_END = 'now';
export const DEFAULT_START = 'now-24h';

interface GetOpenAndAcknowledgedAlertsQuery {
allow_no_indices: boolean;
body: {
fields: Array<{
field: string;
include_unmapped: boolean;
}>;
query: {
bool: {
filter: Array<Record<string, unknown>>;
};
};
runtime_mappings: Record<string, unknown>;
size: number;
sort: Array<{
[key: string]: {
order: string;
};
}>;
_source: boolean;
};
ignore_unavailable: boolean;
index: string[];
}

/**
* This query returns open and acknowledged (non-building block) alerts in the last 24 hours.
*
Expand All @@ -15,12 +43,18 @@ import type { AnonymizationFieldResponse } from '../../schemas/anonymization_fie
export const getOpenAndAcknowledgedAlertsQuery = ({
alertsIndexPattern,
anonymizationFields,
end,
filter,
size,
start,
}: {
alertsIndexPattern: string;
anonymizationFields: AnonymizationFieldResponse[];
end?: string | null;
filter?: Record<string, unknown> | null;
size: number;
}) => ({
start?: string | null;
}): GetOpenAndAcknowledgedAlertsQuery => ({
allow_no_indices: true,
body: {
fields: anonymizationFields
Expand Down Expand Up @@ -53,11 +87,12 @@ export const getOpenAndAcknowledgedAlertsQuery = ({
minimum_should_match: 1,
},
},
...(filter != null ? [filter] : []),
{
range: {
'@timestamp': {
gte: 'now-24h',
lte: 'now',
gte: start != null ? start : DEFAULT_START,
lte: end != null ? end : DEFAULT_END,
format: 'strict_date_optional_time',
},
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,6 @@ export type AssistantFeatureKey = keyof AssistantFeatures;
*/
export const defaultAssistantFeatures = Object.freeze({
assistantModelEvaluation: false,
attackDiscoveryAlertFiltering: false,
defendInsights: false,
});
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,14 @@ export const AttackDiscoveryPostRequestBody = z.object({
* LLM API configuration.
*/
apiConfig: ApiConfig,
end: z.string().optional(),
filter: z.object({}).catchall(z.unknown()).optional(),
langSmithProject: z.string().optional(),
langSmithApiKey: z.string().optional(),
model: z.string().optional(),
replacements: Replacements.optional(),
size: z.number(),
start: z.string().optional(),
subAction: z.enum(['invokeAI', 'invokeStream']),
});
export type AttackDiscoveryPostRequestBodyInput = z.input<typeof AttackDiscoveryPostRequestBody>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@ paths:
apiConfig:
$ref: '../conversations/common_attributes.schema.yaml#/components/schemas/ApiConfig'
description: LLM API configuration.
end:
type: string
filter:
type: object
additionalProperties: true
langSmithProject:
type: string
langSmithApiKey:
Expand All @@ -48,6 +53,8 @@ paths:
$ref: '../conversations/common_attributes.schema.yaml#/components/schemas/Replacements'
size:
type: number
start:
type: string
subAction:
type: string
enum:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,6 @@ import { z } from '@kbn/zod';
export type GetCapabilitiesResponse = z.infer<typeof GetCapabilitiesResponse>;
export const GetCapabilitiesResponse = z.object({
assistantModelEvaluation: z.boolean(),
attackDiscoveryAlertFiltering: z.boolean(),
defendInsights: z.boolean(),
});
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,13 @@ paths:
properties:
assistantModelEvaluation:
type: boolean
attackDiscoveryAlertFiltering:
type: boolean
defendInsights:
type: boolean
required:
- assistantModelEvaluation
- attackDiscoveryAlertFiltering
- defendInsights
'400':
description: Generic Error
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,10 @@ export { getRawDataOrDefault } from './impl/alerts/helpers/get_raw_data_or_defau

/** Return true if the provided size is out of range */
export { sizeIsOutOfRange } from './impl/alerts/helpers/size_is_out_of_range';

export {
/** The default (relative) end of the date range (i.e. `now`) */
DEFAULT_END,
/** The default (relative) start of the date range (i.e. `now-24h`) */
DEFAULT_START,
} from './impl/alerts/get_open_and_acknowledged_alerts_query';
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,14 @@ import { KnowledgeBaseConfig } from '../assistant/types';
export const ATTACK_DISCOVERY_STORAGE_KEY = 'attackDiscovery';
export const DEFEND_INSIGHTS_STORAGE_KEY = 'defendInsights';
export const DEFAULT_ASSISTANT_NAMESPACE = 'elasticAssistantDefault';
export const END_LOCAL_STORAGE_KEY = 'end';
export const LAST_CONVERSATION_ID_LOCAL_STORAGE_KEY = 'lastConversationId';
export const FILTERS_LOCAL_STORAGE_KEY = 'filters';
export const MAX_ALERTS_LOCAL_STORAGE_KEY = 'maxAlerts';
export const KNOWLEDGE_BASE_LOCAL_STORAGE_KEY = 'knowledgeBase';
export const QUERY_LOCAL_STORAGE_KEY = 'query';
export const SHOW_SETTINGS_TOUR_LOCAL_STORAGE_KEY = 'showSettingsTour';
export const START_LOCAL_STORAGE_KEY = 'start';
export const STREAMING_LOCAL_STORAGE_KEY = 'streaming';
export const TRACE_OPTIONS_SESSION_STORAGE_KEY = 'traceOptions';
export const CONVERSATION_TABLE_SESSION_STORAGE_KEY = 'conversationTable';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,11 +84,19 @@ export {
DEFAULT_ATTACK_DISCOVERY_MAX_ALERTS,
DEFAULT_LATEST_ALERTS,
DEFEND_INSIGHTS_STORAGE_KEY,
/** The end of the date range of alerts, sent as context to the LLM */
END_LOCAL_STORAGE_KEY,
/** Search bar filters that apply to the alerts sent as context to the LLM */
FILTERS_LOCAL_STORAGE_KEY,
KNOWLEDGE_BASE_LOCAL_STORAGE_KEY,
/** The local storage key that specifies the maximum number of alerts to send as context */
MAX_ALERTS_LOCAL_STORAGE_KEY,
/** Search bar query that apply to the alerts sent as context to the LLM */
QUERY_LOCAL_STORAGE_KEY,
/** The local storage key that specifies whether the settings tour should be shown */
SHOW_SETTINGS_TOUR_LOCAL_STORAGE_KEY,
/** The start of the date range of alerts, sent as context to the LLM */
START_LOCAL_STORAGE_KEY,
} from './impl/assistant_context/constants';

export { useLoadConnectors } from './impl/connectorland/use_load_connectors';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,15 @@ import type { GraphState } from './types';
export interface GetDefaultAttackDiscoveryGraphParams {
alertsIndexPattern?: string;
anonymizationFields: AnonymizationFieldResponse[];
end?: string;
esClient: ElasticsearchClient;
filter?: Record<string, unknown>;
llm: ActionsClientLlm;
logger?: Logger;
onNewReplacements?: (replacements: Replacements) => void;
replacements?: Replacements;
size: number;
start?: string;
}

export type DefaultAttackDiscoveryGraph = ReturnType<typeof getDefaultAttackDiscoveryGraph>;
Expand All @@ -46,19 +49,22 @@ export type DefaultAttackDiscoveryGraph = ReturnType<typeof getDefaultAttackDisc
export const getDefaultAttackDiscoveryGraph = ({
alertsIndexPattern,
anonymizationFields,
end,
esClient,
filter,
llm,
logger,
onNewReplacements,
replacements,
size,
start,
}: GetDefaultAttackDiscoveryGraphParams): CompiledStateGraph<
GraphState,
Partial<GraphState>,
'generate' | 'refine' | 'retrieve_anonymized_alerts' | '__start__'
> => {
try {
const graphState = getDefaultGraphState();
const graphState = getDefaultGraphState({ end, filter, start });

// get nodes:
const retrieveAnonymizedAlertsNode = getRetrieveAnonymizedAlertsNode({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,36 +21,48 @@ export class AnonymizedAlertsRetriever extends BaseRetriever {

#alertsIndexPattern?: string;
#anonymizationFields?: AnonymizationFieldResponse[];
#end?: string | null;
#esClient: ElasticsearchClient;
#filter?: Record<string, unknown> | null;
#onNewReplacements?: (newReplacements: Replacements) => void;
#replacements?: Replacements;
#size?: number;
#start?: string | null;

constructor({
alertsIndexPattern,
anonymizationFields,
fields,
end,
esClient,
filter,
onNewReplacements,
replacements,
size,
start,
}: {
alertsIndexPattern?: string;
anonymizationFields?: AnonymizationFieldResponse[];
fields?: CustomRetrieverInput;
end?: string | null;
esClient: ElasticsearchClient;
fields?: CustomRetrieverInput;
filter?: Record<string, unknown> | null;
onNewReplacements?: (newReplacements: Replacements) => void;
replacements?: Replacements;
size?: number;
start?: string | null;
}) {
super(fields);

this.#alertsIndexPattern = alertsIndexPattern;
this.#anonymizationFields = anonymizationFields;
this.#end = end;
this.#esClient = esClient;
this.#filter = filter;
this.#onNewReplacements = onNewReplacements;
this.#replacements = replacements;
this.#size = size;
this.#start = start;
}

async _getRelevantDocuments(
Expand All @@ -60,10 +72,13 @@ export class AnonymizedAlertsRetriever extends BaseRetriever {
const anonymizedAlerts = await getAnonymizedAlerts({
alertsIndexPattern: this.#alertsIndexPattern,
anonymizationFields: this.#anonymizationFields,
end: this.#end,
esClient: this.#esClient,
filter: this.#filter,
onNewReplacements: this.#onNewReplacements,
replacements: this.#replacements,
size: this.#size,
start: this.#start,
});

return anonymizedAlerts.map((alert) => ({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,23 @@ import { AnonymizationFieldResponse } from '@kbn/elastic-assistant-common/impl/s
export const getAnonymizedAlerts = async ({
alertsIndexPattern,
anonymizationFields,
end,
esClient,
filter,
onNewReplacements,
replacements,
size,
start,
}: {
alertsIndexPattern?: string;
anonymizationFields?: AnonymizationFieldResponse[];
end?: string | null;
esClient: ElasticsearchClient;
filter?: Record<string, unknown> | null;
onNewReplacements?: (replacements: Replacements) => void;
replacements?: Replacements;
size?: number;
start?: string | null;
}): Promise<string[]> => {
if (alertsIndexPattern == null || size == null || sizeIsOutOfRange(size)) {
return [];
Expand All @@ -40,7 +46,10 @@ export const getAnonymizedAlerts = async ({
const query = getOpenAndAcknowledgedAlertsQuery({
alertsIndexPattern,
anonymizationFields: anonymizationFields ?? [],
end,
filter,
size,
start,
});

const result = await esClient.search<SearchResponse>(query);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,17 +36,23 @@ export const getRetrieveAnonymizedAlertsNode = ({
onNewReplacements?.(localReplacements); // invoke the callback with the latest replacements
};

const retriever = new AnonymizedAlertsRetriever({
alertsIndexPattern,
anonymizationFields,
esClient,
onNewReplacements: localOnNewReplacements,
replacements,
size,
});

const retrieveAnonymizedAlerts = async (state: GraphState): Promise<GraphState> => {
logger?.debug(() => '---RETRIEVE ANONYMIZED ALERTS---');

const { end, filter, start } = state;

const retriever = new AnonymizedAlertsRetriever({
alertsIndexPattern,
anonymizationFields,
end,
esClient,
filter,
onNewReplacements: localOnNewReplacements,
replacements,
size,
start,
});

const documents = await retriever
.withConfig({ runName: 'runAnonymizedAlertsRetriever' })
.invoke('');
Expand Down
Loading
Loading