[AI4DSOC] Fix issue with filtering by integrations (#216574)

## Summary

This PR fixes an issue with the logic implemented in [this previous
PR](https://github.com/elastic/kibana/pull/215586):
_In the AI for SOC effort, each integration is bundled with a single
rule. This means that deselecting a source from the Source filter button
is equivalent to adding a filter to the search bar to exclude all alerts
with the kibana.alert.rule.name property having the value of that
integration._

The problem with the previous logic above is the value in the
`kibana.alert.rule.name` field can be overridden (see `Rule name
override
[here](https://www.elastic.co/guide/en/security/current/rules-ui-create.html)).
Therefore filtering alerts by this value does not guarantee that all the
alerts generated by the rule will be correctly filtered out.

The new logic uses the `rule.id` instead of the `rule.name`, which we
then use to filter using the `signal.rule.id` field instead of
`kibana.alert.rule.name`

### Example:

 There are following 2 integrations installed:
```typescript
[
  {
    id: 'splunk',
    name: 'splunk',
    status: installationStatuses.Installed,
    title: 'Splunk',
    version: '',
  },
  {
    id: 'google_secops',
    name: 'google_secops',
    status: installationStatuses.Installed,
    title: 'Google SecOps',
    version: '',
  },
]
```

This means that - in theory - there are the following 2 rules installed
and running:
```typescript
[
  {
    related_integrations: [{ package: 'splunk' }],
    id: 'splunk_rule_id',
  },
  {
    related_integrations: [{ package: 'google_secops' }],
    id: 'google_secops_rule_id',
  },
]
```

In this case, the `Sources` button would show 2 entries, as follow:
```typescript
[
  {
    checked: 'on',
    key: 'splunk_rule_id',
    label: 'Splunk',
  },
  {
    checked: 'on',
    key: 'google_secops_rule_id',
    label: 'Splunk',
  },
]
```

This PR also fixes a small miss in [the prior
PR](https://github.com/elastic/kibana/pull/215585) that implemented the
KPI section, where I had forgotten to pass the KQL filters to the
charts.

#### Before


https://github.com/user-attachments/assets/77e583c6-718f-46d9-96b4-42ee9976161b

#### After


https://github.com/user-attachments/assets/50e8e541-5798-4906-b7cc-4f9756dbdefc

## How to test

This needs to be ran in Serverless:
- `yarn es serverless --projectType security`
- `yarn serverless-security --no-base-path`

You also need to enable the AI for SOC tier, by adding the following to
your `serverless.security.dev.yaml` file:
```
xpack.securitySolutionServerless.productTypes:
  [
    { product_line: 'ai_soc', product_tier: 'search_ai_lake' },
  ]
```

Use one of these Serverless users:
- `platform_engineer`
- `endpoint_operations_analyst`
- `endpoint_policy_manager`
- `admin`
- `system_indices_superuser`

### Notes

- generate data: `yarn test:generate:serverless-dev`
- create 4 catch all rules, each with a name of a AI for SOC integration
(`google_secops`, `microsoft_sentinel`,, `sentinel_one` and
`crowdstrike`)
- change [this
line](https://github.com/elastic/kibana/blob/main/x-pack/solutions/security/plugins/security_solution/public/detections/hooks/alert_summary/use_fetch_integrations.ts#L73)
to `installedPackages: availablePackages` to force having some packages
installed
- change [this
line](https://github.com/elastic/kibana/blob/main/x-pack/solutions/security/plugins/security_solution/public/detections/hooks/alert_summary/use_integrations.ts#L63)
to `r.name === p.name` to make sure there will be matches between
integrations and rules

### Checklist

- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios

https://github.com/elastic/security-team/issues/11956
This commit is contained in:
Philippe Oberti 2025-04-04 20:59:38 +02:00 committed by GitHub
parent 3c2b4daf97
commit 2ed4266ea5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 31 additions and 17 deletions

View file

@ -35,10 +35,14 @@ export const KPIsSection = memo(({ dataView }: KPIsSectionProps) => {
const getGlobalQuerySelector = useMemo(() => inputsSelectors.globalQuerySelector(), []);
const query = useDeepEqualSelector(getGlobalQuerySelector);
const getGlobalFiltersSelector = useMemo(() => inputsSelectors.globalFiltersQuerySelector(), []);
const filters = useDeepEqualSelector(getGlobalFiltersSelector);
return (
<EuiFlexGroup data-test-subj={KPIS_SECTION}>
<EuiFlexItem>
<SeverityLevelPanel
filters={filters}
signalIndexName={signalIndexName}
query={query}
showCellActions={false}
@ -46,13 +50,18 @@ export const KPIsSection = memo(({ dataView }: KPIsSectionProps) => {
</EuiFlexItem>
<EuiFlexItem>
<AlertsByRulePanel
filters={filters}
signalIndexName={signalIndexName}
query={query}
showCellActions={false}
/>
</EuiFlexItem>
<EuiFlexItem>
<AlertsProgressBarByHostNamePanel signalIndexName={signalIndexName} query={query} />
<AlertsProgressBarByHostNamePanel
filters={filters}
signalIndexName={signalIndexName}
query={query}
/>
</EuiFlexItem>
</EuiFlexGroup>
);

View file

@ -9,6 +9,7 @@ import React from 'react';
import { act, render } from '@testing-library/react';
import { useKibana } from '../../../../common/lib/kibana';
import {
FILTER_KEY,
INTEGRATION_BUTTON_TEST_ID,
IntegrationFilterButton,
INTEGRATIONS_LIST_TEST_ID,
@ -74,12 +75,12 @@ describe('<IntegrationFilterButton />', () => {
alias: null,
disabled: false,
index: undefined,
key: 'kibana.alert.rule.name',
key: FILTER_KEY,
negate: true,
params: { query: 'firstKey' },
type: 'phrase',
},
query: { match_phrase: { 'kibana.alert.rule.name': 'firstKey' } },
query: { match_phrase: { [FILTER_KEY]: 'firstKey' } },
},
]);
});
@ -92,12 +93,12 @@ describe('<IntegrationFilterButton />', () => {
alias: null,
disabled: false,
index: undefined,
key: 'kibana.alert.rule.name',
key: FILTER_KEY,
negate: true,
params: { query: 'secondKey' },
type: 'phrase',
},
query: { match_phrase: { 'kibana.alert.rule.name': 'secondKey' } },
query: { match_phrase: { [FILTER_KEY]: 'secondKey' } },
},
]);
const setFilters = jest.fn();

View file

@ -32,7 +32,7 @@ const INTEGRATIONS_BUTTON = i18n.translate(
}
);
export const FILTER_KEY = 'kibana.alert.rule.name';
export const FILTER_KEY = 'signal.rule.id';
export interface IntegrationFilterButtonProps {
/**
@ -75,13 +75,13 @@ export const IntegrationFilterButton = memo(({ integrations }: IntegrationFilter
) => {
setItems(options);
const ruleName = changedOption.key;
if (ruleName) {
const ruleId = changedOption.key;
if (ruleId) {
const existingFilters = filterManager.getFilters();
const newFilters: Filter[] = updateFiltersArray(
existingFilters,
FILTER_KEY,
ruleName,
ruleId,
changedOption.checked === 'on'
);
filterManager.setFilters(newFilters);

View file

@ -39,7 +39,7 @@ describe('useIntegrations', () => {
rules: [
{
related_integrations: [{ package: 'splunk' }],
name: 'SplunkRuleName',
id: 'SplunkRuleId',
},
],
total: 0,
@ -64,7 +64,7 @@ describe('useIntegrations', () => {
{
checked: 'on',
'data-test-subj': 'alert-summary-integration-option-Splunk',
key: 'SplunkRuleName',
key: 'SplunkRuleId',
label: 'Splunk',
},
],
@ -85,9 +85,9 @@ describe('useIntegrations', () => {
disabled: false,
type: 'phrase',
key: FILTER_KEY,
params: { query: 'Splunk' },
params: { query: 'SplunkRuleId' },
},
query: { match_phrase: { [FILTER_KEY]: 'Splunk' } },
query: { match_phrase: { [FILTER_KEY]: 'SplunkRuleId' } },
},
]),
},
@ -101,7 +101,7 @@ describe('useIntegrations', () => {
rules: [
{
related_integrations: [{ package: 'splunk' }],
name: 'SplunkRuleName',
id: 'SplunkRuleId',
},
],
total: 0,
@ -125,7 +125,7 @@ describe('useIntegrations', () => {
integrations: [
{
'data-test-subj': 'alert-summary-integration-option-Splunk',
key: 'SplunkRuleName',
key: 'SplunkRuleId',
label: 'Splunk',
},
],

View file

@ -65,13 +65,17 @@ export const useIntegrations = ({ packages }: UseIntegrationsParams): UseIntegra
if (matchingRule) {
// Retrieves the filter from the key/value pair
const currentFilter = filterExistsInFiltersArray(currentFilters, FILTER_KEY, p.title);
const currentFilter = filterExistsInFiltersArray(
currentFilters,
FILTER_KEY,
matchingRule.id
);
// A EuiSelectableOption is checked only if there is no matching filter for that rule
const integration = {
'data-test-subj': `${INTEGRATION_OPTION_TEST_ID}${p.title}`,
...(!currentFilter && { checked: 'on' as EuiSelectableOptionCheckedType }),
key: matchingRule?.name,
key: matchingRule?.id, // we save the rule id that we will match again the signal.rule.id field on the alerts
label: p.title,
};
result.push(integration);