[backend] make poster filtering with OR work (#12701)

This commit is contained in:
Jeremy Cloarec
2025-12-08 16:00:07 +01:00
parent 23e924ec6d
commit 60d73f0977
3 changed files with 339 additions and 122 deletions

View File

@@ -139,7 +139,7 @@ import {
RELATION_TYPE_SUBFILTER,
TYPE_FILTER,
} from '../utils/filtering/filtering-constants';
import { type FilterGroup, FilterMode, FilterOperator } from '../generated/graphql';
import { type Filter, type FilterGroup, FilterMode, FilterOperator } from '../generated/graphql';
import {
type AttributeDefinition,
authorizedMembers,
@@ -173,7 +173,7 @@ import { ENTITY_IPV4_ADDR, ENTITY_IPV6_ADDR, isStixCyberObservable } from '../sc
import { lockResources } from '../lock/master-lock';
import { DRAFT_OPERATION_CREATE, DRAFT_OPERATION_DELETE, DRAFT_OPERATION_DELETE_LINKED, DRAFT_OPERATION_UPDATE_LINKED } from '../modules/draftWorkspace/draftOperations';
import { RELATION_SAMPLE } from '../modules/malwareAnalysis/malwareAnalysis-types';
import { asyncFilter, asyncMap } from '../utils/data-processing';
import { asyncMap } from '../utils/data-processing';
import { doYield } from '../utils/eventloop-utils';
import { RELATION_COVERED } from '../modules/securityCoverage/securityCoverage-types';
import type { AuthContext, AuthUser } from '../types/user';
@@ -191,7 +191,7 @@ import type {
StoreRelation,
} from '../types/store';
import type { BasicStoreSettings } from '../types/settings';
import { completeSpecialFilterKeys } from '../utils/filtering/filtering-completeSpecialFilterKeys';
import { completeSpecialFilterKeys, type TaggedFilter, type TaggedFilterGroup } from '../utils/filtering/filtering-completeSpecialFilterKeys';
import { IDS_ATTRIBUTES } from '../domain/attribute-utils';
const ELK_ENGINE = 'elk';
@@ -621,7 +621,6 @@ export const buildDataRestrictions = async (
opts: { includeAuthorities?: boolean | null } | null | undefined = {},
): Promise<{ must: any[]; must_not: any[] }> => {
const must: any[] = [];
const must_not: any[] = [];
// If internal users of the system, we cancel rights checking
if (INTERNAL_USERS[user.id]) {
@@ -2542,6 +2541,70 @@ const buildSubQueryForFilterGroup = (
}
return null;
};
const buildSubQueryForTaggedFilterGroup = (
context: AuthContext,
user: AuthUser,
inputFilters: any,
): { subQuery: any; postFiltersTags: Set<string> } => {
const { mode = 'and', filters = [], filterGroups = [] } = inputFilters;
const localMustFilters: any = [];
const localSubQueries: { subQuery: any; associatedTags: Set<string> }[] = [];
const localPostFilterTags: Set<string> = new Set<string>();
// Handle filterGroups
for (let index = 0; index < filterGroups.length; index += 1) {
const group = filterGroups[index];
if (isFilterGroupNotEmpty(group)) {
const { subQuery, postFiltersTags } = buildSubQueryForTaggedFilterGroup(context, user, group);
if (subQuery) { // can be null
localSubQueries.push({ subQuery, associatedTags: postFiltersTags });
}
if (postFiltersTags) {
postFiltersTags.forEach((t: string) => localPostFilterTags.add(t));
}
}
}
// Handle filters
for (let index = 0; index < filters.length; index += 1) {
const filter = filters[index];
const isValidFilter = filter?.values || filter?.nested?.length > 0;
if (isValidFilter) {
const localMustFilter = buildLocalMustFilter(filter);
if (filter?.postFilteringTag) {
const associatedTag = filter?.postFilteringTag as string;
localPostFilterTags.add(associatedTag);
const associatedTags = new Set(associatedTag);
localSubQueries.push({ subQuery: localMustFilter, associatedTags });
} else {
localSubQueries.push({ subQuery: localMustFilter, associatedTags: new Set<string>() });
}
}
}
// Wrap every subquery in a bool must tagged with name equal to local filter tags
for (let i = 0; i < localSubQueries.length; i++) {
const { subQuery, associatedTags } = localSubQueries[i];
const tagsToApply = Array.from(localPostFilterTags).filter((t: string) => !associatedTags.has(t));
const localMustFilter: any = {
bool: {
must: [subQuery],
},
};
if (mode === 'or' && tagsToApply.length > 0) {
localMustFilter.bool['_name'] = tagsToApply.join(POST_FILTER_TAG_SEPARATOR);
}
localMustFilters.push(localMustFilter);
}
if (localMustFilters.length > 0) {
const currentSubQuery = {
bool: {
should: localMustFilters,
minimum_should_match: mode === 'or' ? 1 : localMustFilters.length,
},
};
return { subQuery: currentSubQuery, postFiltersTags: localPostFilterTags };
}
return { subQuery: null, postFiltersTags: localPostFilterTags };
};
const getRuntimeEntities = async (context: AuthContext, user: AuthUser, entityType: string) => {
const elements = await elPaginate<BasicStoreEntity>(context, user, READ_INDEX_STIX_DOMAIN_OBJECTS, {
types: [entityType],
@@ -2734,12 +2797,13 @@ type QueryBodyBuilderOpts = ProcessSearchArgs & BuildDraftFilterOpts & {
first?: number | null;
types?: string[] | null;
search?: string | null;
filters?: FilterGroup | null;
filters?: TaggedFilterGroup | FilterGroup | null;
noFiltersChecking?: boolean;
startDate?: any;
endDate?: any;
dateAttribute?: string | null;
includeAuthorities?: boolean | null;
handlePostFiltering?: boolean;
};
const elQueryBodyBuilder = async (context: AuthContext, user: AuthUser, options: QueryBodyBuilderOpts) => {
const {
@@ -2761,6 +2825,7 @@ const elQueryBodyBuilder = async (context: AuthContext, user: AuthUser, options:
endDate = null,
dateAttribute = null,
includeAuthorities = false,
handlePostFiltering = false,
} = options;
const elFindByIdsToMap = async (c: AuthContext, u: AuthUser, i: string[], o: any) => {
return elFindByIds<BasicStoreObject>(c, u, i, { ...o, toMap: true }) as Promise<Record<string, BasicStoreObject>>;
@@ -2797,9 +2862,16 @@ const elQueryBodyBuilder = async (context: AuthContext, user: AuthUser, options:
// Handle filters
if (isFilterGroupNotEmpty(completeFilters)) {
const finalFilters = await completeSpecialFilterKeys(context, user, completeFilters);
const filtersSubQuery = buildSubQueryForFilterGroup(context, user, finalFilters);
if (filtersSubQuery) {
mustFilters.push(filtersSubQuery);
if (handlePostFiltering) {
const { subQuery: filtersSubQuery } = buildSubQueryForTaggedFilterGroup(context, user, finalFilters);
if (filtersSubQuery) {
mustFilters.push(filtersSubQuery);
}
} else {
const filtersSubQuery = buildSubQueryForFilterGroup(context, user, finalFilters);
if (filtersSubQuery) {
mustFilters.push(filtersSubQuery);
}
}
}
// Handle search
@@ -2899,6 +2971,106 @@ const buildSearchResult = <T extends BasicStoreBase>(
}
return elements;
};
const POST_FILTER_TAG_SEPARATOR = ';';
type PostFiltersTagMaps = {
tagToFilterMap: Map<string, Filter>; // Is used during hit processing to know which post filtering application to exclude based on tag
filterToTagMap: Map<Filter, string>; // Is used during applyTagToFilters to get tag to apply based on filter
};
const buildPostFiltersTagMaps = (
filters: FilterGroup | undefined | null,
): PostFiltersTagMaps => {
const tagToFilterMap = new Map<string, Filter>();
const filterToTagMap = new Map<Filter, string>();
const result = { tagToFilterMap, filterToTagMap };
if (isNotEmptyField(filters)) {
const definedFilters = filters as FilterGroup;
const postFilteringFilters = extractFiltersFromGroup(definedFilters, [INSTANCE_REGARDING_OF, INSTANCE_DYNAMIC_REGARDING_OF])
.filter((filter) => isEmptyField(filter.operator) || filter.operator === 'eq');
for (let i = 0; i < postFilteringFilters.length; i++) {
const currentPostFilter: Filter = postFilteringFilters[i];
tagToFilterMap.set(String(i), currentPostFilter);
filterToTagMap.set(currentPostFilter, String(i));
}
}
return result;
};
const applyTagToFilters = (
filters: FilterGroup | undefined | null,
postFiltersTagsMaps: PostFiltersTagMaps,
): TaggedFilterGroup | undefined | null => {
if (!filters || postFiltersTagsMaps.filterToTagMap.size <= 0) {
return filters;
}
let newFilterGroups: TaggedFilterGroup[] = filters.filterGroups;
if (newFilterGroups.length > 0) {
newFilterGroups = [];
for (let i = 0; i < filters.filterGroups.length; i++) {
const filterGroupToTag = filters.filterGroups[i];
const taggedFilterGroup = applyTagToFilters(filterGroupToTag, postFiltersTagsMaps);
if (taggedFilterGroup) {
newFilterGroups.push(taggedFilterGroup);
}
}
}
let newFilters: TaggedFilter[] = filters.filters;
if (newFilters.length > 0) {
newFilters = [];
for (let j = 0; j < filters.filters.length; j++) {
const currentFilter = filters.filters[j];
if (postFiltersTagsMaps.filterToTagMap.has(currentFilter)) {
const filterTag = postFiltersTagsMaps.filterToTagMap.get(currentFilter) as string;
const taggedFilter = { ...currentFilter, postFilteringTag: filterTag };
newFilters.push(taggedFilter);
} else {
newFilters.push(currentFilter);
}
}
}
return {
mode: filters.mode,
filters: newFilters,
filterGroups: newFilterGroups,
};
};
const applyPostFilteringToElements = async <T extends BasicStoreBase>(
context: AuthContext,
user: AuthUser,
elements: { element: T; tagsToIgnoreSet: Set<string> }[],
postFiltersMaps?: PostFiltersTagMaps,
): Promise<T[]> => {
let filteredElements: T[] = [];
const rawElements = elements.map((e) => e.element);
if (elements.length <= 0 || !postFiltersMaps || postFiltersMaps.filterToTagMap.size <= 0) {
filteredElements = rawElements;
} else {
const allPostFilteringFilters = Array.from(postFiltersMaps.filterToTagMap.entries());
const tagToPostFilterMap = new Map<string, any>();
for (let i = 0; i < allPostFilteringFilters.length; i++) {
const [currentPostFilter, tag] = allPostFilteringFilters[i];
const postFilterCheckFunction = await buildRegardingOfFilter<T>(context, user, rawElements, currentPostFilter);
tagToPostFilterMap.set(tag, postFilterCheckFunction);
}
const allTags = Array.from(tagToPostFilterMap.keys());
for (let j = 0; j < elements.length; j++) {
const { element: currentElement, tagsToIgnoreSet } = elements[j];
let keepCurrentElement = true;
const postFilterTagsToIgnore: string[] = Array.from(tagsToIgnoreSet);
const tagsToApply = allTags.filter((t) => !postFilterTagsToIgnore.includes(t));
for (let k = 0; k < tagsToApply.length; k++) {
const tagToApply = tagsToApply[k];
const filterCheckToApply = tagToPostFilterMap.get(tagToApply);
keepCurrentElement = keepCurrentElement && filterCheckToApply(currentElement);
}
if (keepCurrentElement) {
filteredElements.push(currentElement);
}
}
}
return filteredElements;
};
export type PaginateOpts = QueryBodyBuilderOpts & {
baseData?: boolean;
baseFields?: string[];
@@ -2933,7 +3105,9 @@ export const elPaginate = async <T extends BasicStoreBase>(
filters,
connectionFormat = true,
} = options;
const body = await elQueryBodyBuilder(context, user, options);
const postFiltersMaps: PostFiltersTagMaps = buildPostFiltersTagMaps(filters);
const tagAppliedFilters = applyTagToFilters(filters, postFiltersMaps);
const body = await elQueryBodyBuilder(context, user, { ...options, filters: tagAppliedFilters, handlePostFiltering: true });
if (body.size > ES_MAX_PAGINATION && !bypassSizeLimit) {
logApp.info('[SEARCH] Pagination limited to max result config', { size: body.size, max: ES_MAX_PAGINATION });
body.size = ES_MAX_PAGINATION;
@@ -2955,12 +3129,30 @@ export const elPaginate = async <T extends BasicStoreBase>(
const data = await elRawSearch(context, user, types !== null ? types : 'Any', query);
const globalCount = data.hits.total.value;
const elements = await elConvertHits<T>(data.hits.hits);
// If filters contains an "in regards of" filter a post-security filtering is needed
const regardingOfFilter = elements.length === 0 ? undefined : await buildRegardingOfFilter<T>(context, user, elements, filters);
const filteredElements = regardingOfFilter ? await asyncFilter(elements, regardingOfFilter) : elements;
const filterCount = elements.length - filteredElements.length;
const result = buildSearchResult(filteredElements, first, body.search_after, globalCount, filterCount, connectionFormat);
let finalElements = elements;
if (finalElements.length > 0 && postFiltersMaps.filterToTagMap.size > 0) {
const elementsWithTags: { element: T; tagsToIgnoreSet: Set<string> }[] = [];
for (let i = 0; i < data.hits.hits.length; i++) {
const element = elements[i];
const dataHit = data.hits.hits[i];
const tagsToIgnoreSet = new Set<string>();
if (dataHit.matched_queries) {
for (let j = 0; j < dataHit.matched_queries.length; j++) {
const currentMatchedQuery = dataHit.matched_queries[j];
const matchedTags = currentMatchedQuery.split(POST_FILTER_TAG_SEPARATOR);
for (let k = 0; k < matchedTags.length; k++) {
const matchedTag = matchedTags[k];
tagsToIgnoreSet.add(matchedTag);
}
}
}
elementsWithTags.push({ element, tagsToIgnoreSet });
}
// Since filters contains filters requiring post filterting (regardingOf, dynamicRegardingOf), a post-security filtering is needed
finalElements = await applyPostFilteringToElements(context, user, elementsWithTags, postFiltersMaps);
}
const filterCount = elements.length - finalElements.length;
const result = buildSearchResult(finalElements, first, body.search_after, globalCount, filterCount, connectionFormat);
if (withResultMeta) {
const lastProcessedSort = R.last(elements)?.sort;
const endCursor = lastProcessedSort ? offsetToCursor(lastProcessedSort) : null;
@@ -3485,98 +3677,87 @@ const buildRegardingOfFilter = async <T extends BasicStoreBase> (
context: AuthContext,
user: AuthUser,
elements: T[],
filters: FilterGroup | undefined | null,
filter: Filter,
) => {
// First check if there is an "in regards of" filter
// If its case we need to ensure elements are filtered according to denormalization rights.
if (isNotEmptyField(filters)) {
const definedFilters = filters as FilterGroup;
const extractedFilters = extractFiltersFromGroup(definedFilters, [INSTANCE_REGARDING_OF, INSTANCE_DYNAMIC_REGARDING_OF])
.filter((filter) => isEmptyField(filter.operator) || filter.operator === 'eq');
if (extractedFilters.length > 0) {
const targetValidatedIds = new Set();
const sideIdManualInferred = new Map();
for (let i = 0; i < extractedFilters.length; i += 1) {
const { values } = extractedFilters[i];
const ids = values.filter((v) => v.key === ID_SUBFILTER).map((f) => f.values).flat();
const types = values.filter((v) => v.key === RELATION_TYPE_SUBFILTER).map((f) => f.values).flat();
const inferredParameterValues = values.filter((v) => v.key === RELATION_INFERRED_SUBFILTER).map((f) => f.values).flat();
const directionForced = R.head(values.filter((v) => v.key === INSTANCE_REGARDING_OF_DIRECTION_FORCED).map((f) => f.values).flat()) ?? false;
const directionReverse = R.head(values.filter((v) => v.key === INSTANCE_REGARDING_OF_DIRECTION_REVERSE).map((f) => f.values).flat()) ?? false;
// resolve all relationships that target the id values, forcing the type is available
const paginateArgs: RepaginateOpts<BasicStoreRelation> = { baseData: true, types };
const elementIds = elements.map(({ id }) => id);
if (directionForced) {
// If a direction is forced, build the filter in the correct direction
const directedFilters = [];
if (directionReverse) {
directedFilters.push({ key: ['fromId'], values: elementIds });
if (ids.length > 0) { // Ids can be empty if nothing configured by the user
directedFilters.push({ key: ['toId'], values: ids });
}
} else {
directedFilters.push({ key: ['toId'], values: elementIds });
if (ids.length > 0) { // Ids can be empty if nothing configured by the user
directedFilters.push({ key: ['fromId'], values: ids });
}
}
paginateArgs.filters = { mode: FilterMode.And, filters: directedFilters, filterGroups: [] };
} else {
// If no direction is setup, create the filter group for both directions
const filterTo = [{ key: ['fromId'], values: elementIds }];
const filterFrom = [{ key: ['toId'], values: elementIds }];
if (ids.length > 0) { // Ids can be empty if nothing configured by the user
filterTo.push({ key: ['toId'], values: ids });
filterFrom.push({ key: ['fromId'], values: ids });
}
paginateArgs.filters = {
mode: FilterMode.Or,
filters: [],
filterGroups: [
{ mode: FilterMode.And, filterGroups: [], filters: filterTo },
{ mode: FilterMode.And, filterGroups: [], filters: filterFrom }],
};
}
let relationshipIndices = READ_RELATIONSHIPS_INDICES;
if (inferredParameterValues.length > 0) {
if (inferredParameterValues.includes('true')) {
relationshipIndices = [READ_INDEX_INFERRED_RELATIONSHIPS];
} else if (inferredParameterValues.includes('false')) {
relationshipIndices = READ_RELATIONSHIPS_INDICES_WITHOUT_INFERRED;
};
};
const relationships = await elList<BasicStoreRelation>(context, user, relationshipIndices, paginateArgs);
// compute side ids
const addTypeSide = (sideId: string, sideType: string) => {
targetValidatedIds.add(sideId);
if (sideIdManualInferred.has(sideId)) {
const toTypes = sideIdManualInferred.get(sideId);
toTypes.add(sideType);
sideIdManualInferred.set(sideId, toTypes);
} else {
const toTypes = new Set();
toTypes.add(sideType);
sideIdManualInferred.set(sideId, toTypes);
}
};
for (let relIndex = 0; relIndex < relationships.length; relIndex += 1) {
await doYield();
const relation = relationships[relIndex];
const relType = isInferredIndex(relation._index) ? 'inferred' : 'manual';
addTypeSide(relation.fromId, relType);
addTypeSide(relation.toId, relType);
}
// We need to ensure elements are filtered according to denormalization rights.
const targetValidatedIds = new Set();
const sideIdManualInferred = new Map();
const { values } = filter;
const ids = values.filter((v) => v.key === ID_SUBFILTER).map((f) => f.values).flat();
const types = values.filter((v) => v.key === RELATION_TYPE_SUBFILTER).map((f) => f.values).flat();
const inferredParameterValues = values.filter((v) => v.key === RELATION_INFERRED_SUBFILTER).map((f) => f.values).flat();
const directionForced = R.head(values.filter((v) => v.key === INSTANCE_REGARDING_OF_DIRECTION_FORCED).map((f) => f.values).flat()) ?? false;
const directionReverse = R.head(values.filter((v) => v.key === INSTANCE_REGARDING_OF_DIRECTION_REVERSE).map((f) => f.values).flat()) ?? false;
// resolve all relationships that target the id values, forcing the type is available
const paginateArgs: RepaginateOpts<BasicStoreRelation> = { baseData: true, types };
const elementIds = elements.map(({ id }) => id);
if (directionForced) {
// If a direction is forced, build the filter in the correct direction
const directedFilters = [];
if (directionReverse) {
directedFilters.push({ key: ['fromId'], values: elementIds });
if (ids.length > 0) { // Ids can be empty if nothing configured by the user
directedFilters.push({ key: ['toId'], values: ids });
}
return (element: (T & { regardingOfTypes?: string })) => {
const accepted = targetValidatedIds.has(element.id);
if (accepted) {
element.regardingOfTypes = sideIdManualInferred.get(element.id);
}
return accepted;
};
} else {
directedFilters.push({ key: ['toId'], values: elementIds });
if (ids.length > 0) { // Ids can be empty if nothing configured by the user
directedFilters.push({ key: ['fromId'], values: ids });
}
}
paginateArgs.filters = { mode: FilterMode.And, filters: directedFilters, filterGroups: [] };
} else {
// If no direction is setup, create the filter group for both directions
const filterTo = [{ key: ['fromId'], values: elementIds }];
const filterFrom = [{ key: ['toId'], values: elementIds }];
if (ids.length > 0) { // Ids can be empty if nothing configured by the user
filterTo.push({ key: ['toId'], values: ids });
filterFrom.push({ key: ['fromId'], values: ids });
}
paginateArgs.filters = {
mode: FilterMode.Or,
filters: [],
filterGroups: [
{ mode: FilterMode.And, filterGroups: [], filters: filterTo },
{ mode: FilterMode.And, filterGroups: [], filters: filterFrom }],
};
}
let relationshipIndices = READ_RELATIONSHIPS_INDICES;
if (inferredParameterValues.length > 0) {
if (inferredParameterValues.includes('true')) {
relationshipIndices = [READ_INDEX_INFERRED_RELATIONSHIPS];
} else if (inferredParameterValues.includes('false')) {
relationshipIndices = READ_RELATIONSHIPS_INDICES_WITHOUT_INFERRED;
}
}
return undefined;
const relationships = await elList<BasicStoreRelation>(context, user, relationshipIndices, paginateArgs);
// compute side ids
const addTypeSide = (sideId: string, sideType: string) => {
targetValidatedIds.add(sideId);
if (sideIdManualInferred.has(sideId)) {
const toTypes = sideIdManualInferred.get(sideId);
toTypes.add(sideType);
sideIdManualInferred.set(sideId, toTypes);
} else {
const toTypes = new Set();
toTypes.add(sideType);
sideIdManualInferred.set(sideId, toTypes);
}
};
for (let relIndex = 0; relIndex < relationships.length; relIndex += 1) {
await doYield();
const relation = relationships[relIndex];
const relType = isInferredIndex(relation._index) ? 'inferred' : 'manual';
addTypeSide(relation.fromId, relType);
addTypeSide(relation.toId, relType);
}
return (element: (T & { regardingOfTypes?: string })) => {
const accepted = targetValidatedIds.has(element.id);
if (accepted) {
element.regardingOfTypes = sideIdManualInferred.get(element.id);
}
return accepted;
};
};
type AttributeValues = {
orderMode?: string | null;

View File

@@ -55,8 +55,8 @@ import { getEntitiesListFromCache } from '../../database/cache';
import { ENTITY_TYPE_STATUS } from '../../schema/internalObject';
import { IDS_ATTRIBUTES } from '../../domain/attribute-utils';
export const adaptFilterToRegardingOfFilterKey = async (context: AuthContext, user: AuthUser, filter: Filter) => {
const { key: filterKey } = filter;
export const adaptFilterToRegardingOfFilterKey = async (context: AuthContext, user: AuthUser, filter: TaggedFilter) => {
const { key: filterKey, postFilteringTag } = filter;
const regardingFilters = [];
const idParameter = filter.values.find((i) => i.key === ID_SUBFILTER);
const typeParameter = filter.values.find((i) => i.key === RELATION_TYPE_SUBFILTER);
@@ -130,13 +130,13 @@ export const adaptFilterToRegardingOfFilterKey = async (context: AuthContext, us
? buildRefRelationKey('*', '*')
: types.map((t: string) => buildRefRelationKey(t, '*'));
keys.forEach((relKey: string) => {
regardingFilters.push({ key: [relKey], operator: filter.operator, values: ['EXISTS'] });
regardingFilters.push({ key: [relKey], operator: filter.operator, values: ['EXISTS'], postFilteringTag });
});
} else {
const keys = isEmptyField(types)
? buildRefRelationKey('*', '*')
: types.flatMap((t: string) => [buildRefRelationKey(t, ID_INTERNAL), buildRefRelationKey(t, ID_INFERRED)]);
regardingFilters.push({ key: keys, operator: filter.operator, mode, values: ids });
regardingFilters.push({ key: keys, operator: filter.operator, mode, values: ids, postFilteringTag });
}
return { newFilterGroup: { mode, filters: regardingFilters, filterGroups: [] } };
};
@@ -683,6 +683,12 @@ const adaptFilterToComputedReliabilityFilterKey = async (context: AuthContext, u
return { newFilterGroup };
};
export type TaggedFilter = Filter & { postFilteringTag?: string };
export type TaggedFilterGroup = {
mode: FilterMode;
filters: TaggedFilter[];
filterGroups: TaggedFilterGroup[];
};
/**
* Complete the filter if needed for several special filter keys
* Some keys need this preprocessing before building the query:
@@ -696,11 +702,11 @@ const adaptFilterToComputedReliabilityFilterKey = async (context: AuthContext, u
export const completeSpecialFilterKeys = async (
context: AuthContext,
user: AuthUser,
inputFilters: FilterGroup,
): Promise<FilterGroup> => {
inputFilters: TaggedFilterGroup,
): Promise<TaggedFilterGroup> => {
const { filters = [], filterGroups = [] } = inputFilters;
const finalFilters = [];
const finalFilterGroups: FilterGroup[] = [];
const finalFilterGroups: TaggedFilterGroup[] = [];
for (let index = 0; index < filterGroups.length; index += 1) {
const filterGroup = filterGroups[index];
const newFilterGroup = await completeSpecialFilterKeys(context, user, filterGroup);

View File

@@ -2455,9 +2455,9 @@ describe('Complex filters combinations for elastic queries', () => {
describe('Complex filters regarding of for elastic queries', () => {
it('should list entities using basic regarding of filter', async () => {
const generateFilters = (withRegardingOf = true, regardingOfOperator = 'eq') => {
const generateFilters = (withRegardingOf = true, regardingOfOperator = 'eq', mainMode = 'and') => {
return {
mode: 'and',
mode: mainMode,
filters: [
{
key: 'entity_type',
@@ -2494,15 +2494,24 @@ describe('Complex filters regarding of for elastic queries', () => {
expect(eqQueryResult.data.globalSearch.edges.length).toEqual(2);
expect(eqQueryResult.data.globalSearch.edges[0].node.standard_id).toEqual('intrusion-set--d12c5319-f308-5fef-9336-20484af42084');
expect(eqQueryResult.data.globalSearch.edges[1].node.standard_id).toEqual('malware--21c45dbe-54ec-5bb7-b8cd-9f27cc518714');
const eqOrQueryResult = await queryAsAdmin({ query: LIST_QUERY, variables: { filters: generateFilters(true, 'eq', 'or') } });
expect(eqOrQueryResult.data.globalSearch.edges.length).toEqual(5);
expect(eqOrQueryResult.data.globalSearch.edges[0].node.standard_id).toEqual('attack-pattern--a01046cc-192f-5d52-8e75-6e447fae3890');
expect(eqOrQueryResult.data.globalSearch.edges[1].node.standard_id).toEqual('attack-pattern--b5c4784e-6ecc-5347-a231-c9739e077dd8');
expect(eqOrQueryResult.data.globalSearch.edges[2].node.standard_id).toEqual('intrusion-set--d12c5319-f308-5fef-9336-20484af42084');
expect(eqOrQueryResult.data.globalSearch.edges[3].node.standard_id).toEqual('malware--21c45dbe-54ec-5bb7-b8cd-9f27cc518714');
expect(eqOrQueryResult.data.globalSearch.edges[4].node.standard_id).toEqual('malware--8a4b5aef-e4a7-524c-92f9-a61c08d1cd85');
const notEqQueryResult = await queryAsAdmin({ query: LIST_QUERY, variables: { filters: generateFilters(true, 'not_eq') } });
expect(notEqQueryResult.data.globalSearch.edges.length).toEqual(1);
expect(notEqQueryResult.data.globalSearch.edges[0].node.standard_id).toEqual('malware--8a4b5aef-e4a7-524c-92f9-a61c08d1cd85');
const notEqOrQueryResult = await queryAsAdmin({ query: LIST_QUERY, variables: { filters: generateFilters(true, 'not_eq', 'or') } });
expect(notEqOrQueryResult.data.globalSearch.edges.length).toEqual(38);
});
it('should list entities using complex regarding of filter', async () => {
const attackPattern = await storeLoadById(testContext, ADMIN_USER, 'attack-pattern--2fc04aa5-48c1-49ec-919a-b88241ef1d17', ENTITY_TYPE_ATTACK_PATTERN);
const generateFilters = (withRegardingOf = true, regardingOfOperator = 'eq') => {
const generateFilters = (withRegardingOf = true, regardingOfOperator = 'eq', mainMode = 'and') => {
return {
mode: 'and',
mode: mainMode,
filters: [
{
key: 'entity_type',
@@ -2537,16 +2546,20 @@ describe('Complex filters regarding of for elastic queries', () => {
const queryResult = await queryAsAdmin({ query: LIST_QUERY, variables: { filters: generateFilters(true, 'eq') } });
expect(queryResult.data.globalSearch.edges.length).toEqual(1);
expect(queryResult.data.globalSearch.edges[0].node.standard_id).toEqual('malware--21c45dbe-54ec-5bb7-b8cd-9f27cc518714');
const queryEqOrResult = await queryAsAdmin({ query: LIST_QUERY, variables: { filters: generateFilters(true, 'eq', 'or') } });
expect(queryEqOrResult.data.globalSearch.edges.length).toEqual(3);
const notEqQueryResult = await queryAsAdmin({ query: LIST_QUERY, variables: { filters: generateFilters(true, 'not_eq') } });
expect(notEqQueryResult.data.globalSearch.edges.length).toEqual(2);
expect(notEqQueryResult.data.globalSearch.edges[0].node.standard_id).toEqual('intrusion-set--d12c5319-f308-5fef-9336-20484af42084');
expect(notEqQueryResult.data.globalSearch.edges[1].node.standard_id).toEqual('malware--8a4b5aef-e4a7-524c-92f9-a61c08d1cd85');
const notEqOrQueryResult = await queryAsAdmin({ query: LIST_QUERY, variables: { filters: generateFilters(true, 'not_eq', 'or') } });
expect(notEqOrQueryResult.data.globalSearch.edges.length).toEqual(40);
});
it('should list entities using complex regarding of filter with force direction', async () => {
const attackPattern = await storeLoadById(testContext, ADMIN_USER, 'attack-pattern--2fc04aa5-48c1-49ec-919a-b88241ef1d17', ENTITY_TYPE_ATTACK_PATTERN);
const generateFilters = (reverse, withRegardingOf = true, regardingOfOperator = 'eq') => {
const generateFilters = (reverse, withRegardingOf = true, regardingOfOperator = 'eq', mainMode = 'and') => {
return {
mode: 'and',
mode: mainMode,
filters: [
{
key: 'entity_type',
@@ -2583,6 +2596,8 @@ describe('Complex filters regarding of for elastic queries', () => {
const queryResultReverse = await queryAsAdmin({ query: LIST_QUERY, variables: { filters: generateFilters(true, true, 'eq') } });
expect(queryResultReverse.data.globalSearch.edges.length).toEqual(1);
expect(queryResultReverse.data.globalSearch.edges[0].node.standard_id).toEqual('malware--21c45dbe-54ec-5bb7-b8cd-9f27cc518714');
const queryResultReverseOr = await queryAsAdmin({ query: LIST_QUERY, variables: { filters: generateFilters(true, true, 'eq', 'or') } });
expect(queryResultReverseOr.data.globalSearch.edges.length).toEqual(3);
const notEqQueryResultReverse = await queryAsAdmin({ query: LIST_QUERY, variables: { filters: generateFilters(true, true, 'not_eq') } });
expect(notEqQueryResultReverse.data.globalSearch.edges.length).toEqual(2);
expect(notEqQueryResultReverse.data.globalSearch.edges[0].node.standard_id).toEqual('intrusion-set--d12c5319-f308-5fef-9336-20484af42084');
@@ -2590,15 +2605,17 @@ describe('Complex filters regarding of for elastic queries', () => {
// reverse = false
const queryResult = await queryAsAdmin({ query: LIST_QUERY, variables: { filters: generateFilters(false, true, 'eq') } });
expect(queryResult.data.globalSearch.edges.length).toEqual(0); // 0 instead of 1 because of the reverse that implies a post filtering
const queryResultOr = await queryAsAdmin({ query: LIST_QUERY, variables: { filters: generateFilters(false, true, 'eq', 'or') } });
expect(queryResultOr.data.globalSearch.edges.length).toEqual(3); // 0 instead of 1 because of the reverse that implies a post filtering
const notEqQueryResult = await queryAsAdmin({ query: LIST_QUERY, variables: { filters: generateFilters(false, true, 'not_eq') } });
expect(notEqQueryResult.data.globalSearch.edges.length).toEqual(2); // 2 here as post filtering is not applied on not_eq
expect(notEqQueryResult.data.globalSearch.edges[0].node.standard_id).toEqual('intrusion-set--d12c5319-f308-5fef-9336-20484af42084');
expect(notEqQueryResult.data.globalSearch.edges[1].node.standard_id).toEqual('malware--8a4b5aef-e4a7-524c-92f9-a61c08d1cd85');
});
it('should list entities using basic regarding of dynamic filter', async () => {
const generateFilters = (withRegardingOf = true, regardingOfOperator = 'eq') => {
const generateFilters = (withRegardingOf = true, regardingOfOperator = 'eq', mainMode = 'and') => {
return {
mode: 'and',
mode: mainMode,
filters: [
{
key: 'entity_type',
@@ -2651,14 +2668,18 @@ describe('Complex filters regarding of for elastic queries', () => {
const queryResult = await queryAsAdmin({ query: LIST_QUERY, variables: { filters: generateFilters(true, 'eq') } });
expect(queryResult.data.globalSearch.edges.length).toEqual(1);
expect(queryResult.data.globalSearch.edges[0].node.standard_id).toEqual('malware--21c45dbe-54ec-5bb7-b8cd-9f27cc518714');
const queryResultOr = await queryAsAdmin({ query: LIST_QUERY, variables: { filters: generateFilters(true, 'eq', 'or') } });
expect(queryResultOr.data.globalSearch.edges.length).toEqual(2);
const notEqQueryResult = await queryAsAdmin({ query: LIST_QUERY, variables: { filters: generateFilters(true, 'not_eq') } });
expect(notEqQueryResult.data.globalSearch.edges.length).toEqual(1);
expect(notEqQueryResult.data.globalSearch.edges[0].node.standard_id).toEqual('malware--8a4b5aef-e4a7-524c-92f9-a61c08d1cd85');
const notEqOrQueryResult = await queryAsAdmin({ query: LIST_QUERY, variables: { filters: generateFilters(true, 'not_eq', 'or') } });
expect(notEqOrQueryResult.data.globalSearch.edges.length).toEqual(40);
});
it('should list entities using multi relationships regarding of filter', async () => {
const generateFilters = (withRegardingOf = true, relationships = [], regardingOfOperator = 'eq') => {
const generateFilters = (withRegardingOf = true, relationships = [], regardingOfOperator = 'eq', mainMode = 'and') => {
return {
mode: 'and',
mode: mainMode,
filters: [
{
key: 'entity_type',
@@ -2699,6 +2720,13 @@ describe('Complex filters regarding of for elastic queries', () => {
// - related-to > malware-analysis--8fd6fcd4-81a9-4937-92b8-4e1cbe68f263"
let eqQueryResult = await queryAsAdmin({ query: LIST_QUERY, variables: { filters: generateFilters(true, ['uses'], 'eq') } });
expect(eqQueryResult.data.globalSearch.edges.length).toEqual(1);
const eqOrQueryResult = await queryAsAdmin({ query: LIST_QUERY, variables: { filters: generateFilters(true, ['uses'], 'eq', 'or') } });
expect(eqOrQueryResult.data.globalSearch.edges.length).toEqual(5);
expect(eqOrQueryResult.data.globalSearch.edges[0].node.standard_id).toEqual('attack-pattern--a01046cc-192f-5d52-8e75-6e447fae3890');
expect(eqOrQueryResult.data.globalSearch.edges[1].node.standard_id).toEqual('attack-pattern--b5c4784e-6ecc-5347-a231-c9739e077dd8');
expect(eqOrQueryResult.data.globalSearch.edges[2].node.standard_id).toEqual('intrusion-set--d12c5319-f308-5fef-9336-20484af42084');
expect(eqOrQueryResult.data.globalSearch.edges[3].node.standard_id).toEqual('malware--21c45dbe-54ec-5bb7-b8cd-9f27cc518714');
expect(eqOrQueryResult.data.globalSearch.edges[4].node.standard_id).toEqual('malware--8a4b5aef-e4a7-524c-92f9-a61c08d1cd85');
eqQueryResult = await queryAsAdmin({ query: LIST_QUERY, variables: { filters: generateFilters(true, ['uses', 'related-to'], 'eq') } });
expect(eqQueryResult.data.globalSearch.edges.length).toEqual(2);
expect(eqQueryResult.data.globalSearch.edges[0].node.representative.main).toEqual('Paradise Ransomware');
@@ -2712,9 +2740,9 @@ describe('Complex filters regarding of for elastic queries', () => {
expect(notEqQueryResult.data.globalSearch.edges[0].node.standard_id).toEqual('malware--8a4b5aef-e4a7-524c-92f9-a61c08d1cd85');
});
it('should list entities using multi ids regarding of filter', async () => {
const generateFilters = (withRegardingOf = true, ids = [], regardingOfOperator = 'eq') => {
const generateFilters = (withRegardingOf = true, ids = [], regardingOfOperator = 'eq', mainMode = 'and') => {
return {
mode: 'and',
mode: mainMode,
filters: [
{
key: 'entity_type',
@@ -2765,6 +2793,8 @@ describe('Complex filters regarding of for elastic queries', () => {
expect(eqQueryResult.data.globalSearch.edges[0].node.standard_id).toEqual('malware--21c45dbe-54ec-5bb7-b8cd-9f27cc518714');
expect(eqQueryResult.data.globalSearch.edges[1].node.representative.main).toEqual('Spelevo EK');
expect(eqQueryResult.data.globalSearch.edges[1].node.standard_id).toEqual('malware--8a4b5aef-e4a7-524c-92f9-a61c08d1cd85');
const eqOrQueryResult = await queryAsAdmin({ query: LIST_QUERY, variables: { filters: generateFilters(true, [targetAttack.internal_id, malwareAnalysis.internal_id], 'eq', 'or') } });
expect(eqOrQueryResult.data.globalSearch.edges.length).toEqual(4);
let notEqQueryResult = await queryAsAdmin({ query: LIST_QUERY, variables: { filters: generateFilters(true, [targetAttack.internal_id], 'not_eq') } });
expect(notEqQueryResult.data.globalSearch.edges.length).toEqual(1);
notEqQueryResult = await queryAsAdmin({ query: LIST_QUERY, variables: { filters: generateFilters(true, [targetAttack.internal_id, malwareAnalysis.internal_id], 'not_eq') } });