[Security Solution][Detections] Fixes bulk alert status update to only update alerts within the defined daterange (#82071)

## Summary

Addresses https://github.com/elastic/kibana/issues/82004 where updating an alerts status would result in all alerts being updated as the request is sent without the necessary daterange filter.

##### Before:
<details><summary>Query</summary>
<p>

``` json
  {
    "conflicts": "proceed",
    "status": "open",
    "query": {
      "bool": {
        "must": [],
        "filter": [
          { "match_all": {} },
          { "term": { "signal.status": "closed" } }
        ],
        "should": [],
        "must_not": [
          { "exists": { "field": "signal.rule.building_block_type" } }
        ]
      }
    }
  }
```
</p>
</details>
<p align="center">
  <img width="500" src="https://user-images.githubusercontent.com/2946766/97628470-5db73580-19f2-11eb-8e51-61e428e7804f.gif" />
</p>


##### After:
<details><summary>Query</summary>
<p>

``` json
  {
    "conflicts": "proceed",
    "status": "closed",
    "query": {
      "bool": {
        "must": [],
        "filter": [
          {
            "bool": {
              "filter": [
                {
                  "bool": {
                    "should": [
                      {
                        "range": {
                          "@timestamp": { "gte": "2020-10-29T20:17:23.357Z" }
                        }
                      }
                    ],
                    "minimum_should_match": 1
                  }
                },
                {
                  "bool": {
                    "should": [
                      {
                        "range": {
                          "@timestamp": { "lte": "2020-10-29T20:17:40.097Z" }
                        }
                      }
                    ],
                    "minimum_should_match": 1
                  }
                }
              ]
            }
          },
          { "term": { "signal.status": "open" } }
        ],
        "should": [],
        "must_not": [
          { "exists": { "field": "signal.rule.building_block_type" } }
        ]
      }
    }
  }
```
</p>
</details>
<p align="center">
  <img width="500" src="https://user-images.githubusercontent.com/2946766/97628955-0796c200-19f3-11eb-9ec3-2a6ace17160b.gif" />
</p>



### Checklist

Delete any items that are not applicable to this PR.

- [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
This commit is contained in:
Garrett Spong 2020-11-03 16:34:07 -07:00 committed by GitHub
parent 4323357ef8
commit 74463a42f1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 79 additions and 56 deletions

View file

@ -177,8 +177,6 @@ const EventsViewerComponent: React.FC<Props> = ({
filters,
kqlQuery: query,
kqlMode,
start,
end,
isEventViewer: true,
});

View file

@ -117,7 +117,6 @@ const StatefulTopNComponent: React.FC<Props> = ({
browserFields,
config: esQuery.getEsQueryConfig(kibana.services.uiSettings),
dataProviders,
end: activeTimelineTo,
filters: activeTimelineFilters,
indexPattern,
kqlMode,
@ -125,7 +124,6 @@ const StatefulTopNComponent: React.FC<Props> = ({
language: 'kuery',
query: activeTimelineKqlQueryExpression ?? '',
},
start: activeTimelineFrom,
})?.filterQuery
: undefined
}

View file

@ -195,6 +195,8 @@ const AlertsUtilityBarComponent: React.FC<AlertsUtilityBarProps> = ({
</UtilityBarAction>
<UtilityBarAction
aria-label="selectAllAlerts"
dataTestSubj="selectAllAlertsButton"
iconType={showClearSelection ? 'cross' : 'pagesSelect'}
onClick={() => {
if (!showClearSelection) {

View file

@ -5,7 +5,7 @@
*/
import { TimelineType } from '../../../../common/types/timeline';
import { Filter } from '../../../../../../../src/plugins/data/public';
import { esFilters, Filter } from '../../../../../../../src/plugins/data/public';
import {
DataProvider,
DataProviderType,
@ -17,6 +17,7 @@ import {
replaceTemplateFieldFromQuery,
replaceTemplateFieldFromMatchFilters,
reformatDataProviderWithNewValue,
buildTimeRangeFilter,
} from './helpers';
import { mockTimelineDetails } from '../../../common/mock';
@ -530,4 +531,38 @@ describe('helpers', () => {
});
});
});
describe('buildTimeRangeFilter', () => {
test('time range filter is created with from and to', () => {
const from = '2020-10-29T21:06:10.192Z';
const to = '2020-10-29T21:07:38.774Z';
const timeRangeFilter = buildTimeRangeFilter(from, to);
expect(timeRangeFilter).toEqual([
{
range: {
'@timestamp': {
gte: '2020-10-29T21:06:10.192Z',
lt: '2020-10-29T21:07:38.774Z',
format: 'strict_date_optional_time',
},
},
meta: {
type: 'range',
disabled: false,
negate: false,
alias: null,
key: '@timestamp',
params: {
gte: from,
lt: to,
format: 'strict_date_optional_time',
},
},
$state: {
store: esFilters.FilterStateStore.APP_STATE,
},
},
]);
});
});
});

View file

@ -5,7 +5,12 @@
*/
import { isEmpty } from 'lodash/fp';
import { Filter, esKuery, KueryNode } from '../../../../../../../src/plugins/data/public';
import {
Filter,
esKuery,
KueryNode,
esFilters,
} from '../../../../../../../src/plugins/data/public';
import {
DataProvider,
DataProviderType,
@ -214,3 +219,30 @@ export const replaceTemplateFieldFromDataProviders = (
}
return newDataProvider;
});
export const buildTimeRangeFilter = (from: string, to: string): Filter[] => [
{
range: {
'@timestamp': {
gte: from,
lt: to,
format: 'strict_date_optional_time',
},
},
meta: {
type: 'range',
disabled: false,
negate: false,
alias: null,
key: '@timestamp',
params: {
gte: from,
lt: to,
format: 'strict_date_optional_time',
},
},
$state: {
store: esFilters.FilterStateStore.APP_STATE,
},
} as Filter,
];

View file

@ -46,6 +46,7 @@ import {
} from '../../../common/components/toasters';
import { SourcererScopeName } from '../../../common/store/sourcerer/model';
import { useSourcererScope } from '../../../common/containers/sourcerer';
import { buildTimeRangeFilter } from './helpers';
interface OwnProps {
timelineId: TimelineIdLiteral;
@ -105,13 +106,14 @@ export const AlertsTableComponent: React.FC<AlertsTableComponentProps> = ({
dataProviders: [],
indexPattern: indexPatterns,
browserFields,
filters: isEmpty(defaultFilters)
? [...globalFilters, ...customFilters]
: [...(defaultFilters ?? []), ...globalFilters, ...customFilters],
filters: [
...(defaultFilters ?? []),
...globalFilters,
...customFilters,
...buildTimeRangeFilter(from, to),
],
kqlQuery: globalQuery,
kqlMode: globalQuery.language,
start: from,
end: to,
isEventViewer: true,
});
}

View file

@ -14,8 +14,6 @@ import { mockBrowserFields } from '../../../common/containers/source/mock';
import { EsQueryConfig, Filter, esFilters } from '../../../../../../../src/plugins/data/public';
const cleanUpKqlQuery = (str: string) => str.replace(/\n/g, '').replace(/\s\s+/g, ' ');
const startDate = '2018-03-23T18:49:23.132Z';
const endDate = '2018-03-24T03:33:52.253Z';
describe('Build KQL Query', () => {
test('Build KQL query with one data provider', () => {
@ -238,8 +236,6 @@ describe('Combined Queries', () => {
filters: [],
kqlQuery: { query: '', language: 'kuery' },
kqlMode: 'search',
start: startDate,
end: endDate,
})
).toBeNull();
});
@ -255,8 +251,6 @@ describe('Combined Queries', () => {
filters: [],
kqlQuery: { query: '', language: 'kuery' },
kqlMode: 'search',
start: startDate,
end: endDate,
isEventViewer,
})
).toEqual({
@ -300,8 +294,6 @@ describe('Combined Queries', () => {
],
kqlQuery: { query: '', language: 'kuery' },
kqlMode: 'search',
start: startDate,
end: endDate,
isEventViewer,
})
).toEqual({
@ -320,8 +312,6 @@ describe('Combined Queries', () => {
filters: [],
kqlQuery: { query: '', language: 'kuery' },
kqlMode: 'search',
start: startDate,
end: endDate,
})!;
expect(filterQuery).toEqual(
'{"bool":{"must":[],"filter":[{"bool":{"should":[{"match_phrase":{"name":"Provider 1"}}],"minimum_should_match":1}}],"should":[],"must_not":[]}}'
@ -340,8 +330,6 @@ describe('Combined Queries', () => {
filters: [],
kqlQuery: { query: '', language: 'kuery' },
kqlMode: 'search',
start: startDate,
end: endDate,
})!;
expect(filterQuery).toEqual(
'{"bool":{"must":[],"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1521848183232,"lte":1521848183232}}}],"minimum_should_match":1}}],"should":[],"must_not":[]}}'
@ -360,8 +348,6 @@ describe('Combined Queries', () => {
filters: [],
kqlQuery: { query: '', language: 'kuery' },
kqlMode: 'search',
start: startDate,
end: endDate,
})!;
expect(filterQuery).toEqual(
'{"bool":{"must":[],"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1521848183232,"lte":1521848183232}}}],"minimum_should_match":1}}],"should":[],"must_not":[]}}'
@ -380,8 +366,6 @@ describe('Combined Queries', () => {
filters: [],
kqlQuery: { query: '', language: 'kuery' },
kqlMode: 'search',
start: startDate,
end: endDate,
})!;
expect(filterQuery).toEqual(
'{"bool":{"must":[],"filter":[{"bool":{"should":[{"match":{"event.end":1521848183232}}],"minimum_should_match":1}}],"should":[],"must_not":[]}}'
@ -400,8 +384,6 @@ describe('Combined Queries', () => {
filters: [],
kqlQuery: { query: '', language: 'kuery' },
kqlMode: 'search',
start: startDate,
end: endDate,
})!;
expect(filterQuery).toEqual(
'{"bool":{"must":[],"filter":[{"bool":{"should":[{"match":{"event.end":1521848183232}}],"minimum_should_match":1}}],"should":[],"must_not":[]}}'
@ -417,8 +399,6 @@ describe('Combined Queries', () => {
filters: [],
kqlQuery: { query: 'host.name: "host-1"', language: 'kuery' },
kqlMode: 'search',
start: startDate,
end: endDate,
})!;
expect(filterQuery).toEqual(
'{"bool":{"must":[],"filter":[{"bool":{"should":[{"match_phrase":{"host.name":"host-1"}}],"minimum_should_match":1}}],"should":[],"must_not":[]}}'
@ -435,8 +415,6 @@ describe('Combined Queries', () => {
filters: [],
kqlQuery: { query: 'host.name: "host-1"', language: 'kuery' },
kqlMode: 'search',
start: startDate,
end: endDate,
})!;
expect(filterQuery).toEqual(
'{"bool":{"must":[],"filter":[{"bool":{"should":[{"bool":{"should":[{"match_phrase":{"name":"Provider 1"}}],"minimum_should_match":1}},{"bool":{"should":[{"match_phrase":{"host.name":"host-1"}}],"minimum_should_match":1}}],"minimum_should_match":1}}],"should":[],"must_not":[]}}'
@ -453,8 +431,6 @@ describe('Combined Queries', () => {
filters: [],
kqlQuery: { query: 'host.name: "host-1"', language: 'kuery' },
kqlMode: 'filter',
start: startDate,
end: endDate,
})!;
expect(filterQuery).toEqual(
'{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"name":"Provider 1"}}],"minimum_should_match":1}},{"bool":{"should":[{"match_phrase":{"host.name":"host-1"}}],"minimum_should_match":1}}]}}],"should":[],"must_not":[]}}'
@ -473,8 +449,6 @@ describe('Combined Queries', () => {
filters: [],
kqlQuery: { query: 'host.name: "host-1"', language: 'kuery' },
kqlMode: 'search',
start: startDate,
end: endDate,
})!;
expect(filterQuery).toEqual(
'{"bool":{"must":[],"filter":[{"bool":{"should":[{"bool":{"should":[{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"name":"Provider 1"}}],"minimum_should_match":1}},{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"name":"Provider 3"}}],"minimum_should_match":1}},{"bool":{"should":[{"match_phrase":{"name":"Provider 4"}}],"minimum_should_match":1}}]}}]}},{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"name":"Provider 2"}}],"minimum_should_match":1}},{"bool":{"should":[{"match_phrase":{"name":"Provider 5"}}],"minimum_should_match":1}}]}}],"minimum_should_match":1}},{"bool":{"should":[{"match_phrase":{"host.name":"host-1"}}],"minimum_should_match":1}}],"minimum_should_match":1}}],"should":[],"must_not":[]}}'
@ -493,8 +467,6 @@ describe('Combined Queries', () => {
filters: [],
kqlQuery: { query: 'host.name: "host-1"', language: 'kuery' },
kqlMode: 'filter',
start: startDate,
end: endDate,
})!;
expect(filterQuery).toEqual(
'{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"should":[{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"name":"Provider 1"}}],"minimum_should_match":1}},{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"name":"Provider 3"}}],"minimum_should_match":1}},{"bool":{"should":[{"match_phrase":{"name":"Provider 4"}}],"minimum_should_match":1}}]}}]}},{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"name":"Provider 2"}}],"minimum_should_match":1}},{"bool":{"should":[{"match_phrase":{"name":"Provider 5"}}],"minimum_should_match":1}}]}}],"minimum_should_match":1}},{"bool":{"should":[{"match_phrase":{"host.name":"host-1"}}],"minimum_should_match":1}}]}}],"should":[],"must_not":[]}}'

View file

@ -104,8 +104,6 @@ export const combineQueries = ({
filters = [],
kqlQuery,
kqlMode,
start,
end,
isEventViewer,
}: {
config: EsQueryConfig;
@ -115,8 +113,6 @@ export const combineQueries = ({
filters: Filter[];
kqlQuery: Query;
kqlMode: string;
start: string;
end: string;
isEventViewer?: boolean;
}): { filterQuery: string } | null => {
const kuery: Query = { query: '', language: kqlQuery.language };

View file

@ -164,20 +164,8 @@ export const TimelineComponent: React.FC<Props> = ({
filters,
kqlQuery,
kqlMode,
start,
end,
}),
[
browserFields,
dataProviders,
esQueryConfig,
start,
end,
filters,
indexPattern,
kqlMode,
kqlQuery,
]
[browserFields, dataProviders, esQueryConfig, filters, indexPattern, kqlMode, kqlQuery]
);
const canQueryTimeline = useMemo(