Filters format migration for OpenCTI 5.12
The version 5.12 of OpenCTI introduces breaking changes to the filters format used in the API. This documentation describes how you can migrate your scripts or programs that call the OpenCTI API, when updating from a version of OpenCTI inferior to 5.12.
Why this migration?
Before OpenCTI 5.12, it was not possible to construct complex filters combinations: we couldn't embed filters within filters, used different boolean modes (and/or), filter on all available attributes or relations for a given entity type, or even test for empty fields of any sort.
Legacy of years of development, the former format and filtering mechanics were not adapted for such task, and a profound refactoring was necessary to make it happen.
Here are the main pain points we identified beforehand:
- The filters frontend and backend formats were very different, requiring careful conversions.
- The filter were static lists of keys, depending on each given entity type and maintained by hands.
- The operator (
eq
,not_eq
, etc.) was inside the key (e.g.entity_type_not_eq
), limiting operator combination and requiring error-prone parsing. - The frontend format imposed a unique form of combination (
and
between filters,or
between values inside each filter, and nothing else possible). - The flat list structure made impossible filter imbrication by nature.
- Filters and query options were mixed in GQL queries for the same purpose (for instance, option
types
analog to a filter on keyentity_type
).
// filter formats in OpenCTI < 5.12
type Filter = {
key: string, // a key in the list of the available filter keys for given the entity type
values: string[],
operator: string,
filterMode: 'and' | 'or',
}
// "give me Reports labelled with labelX or labelY"
const filters = [
{
"key": "entity_type",
"values": ["Report"],
"operator": "eq",
"filterMode": "or"
},
{
"key": "labelledBy",
"values": ["<id-for-labelX>", "<id-for-labelY>"],
"operator": "eq",
"filterMode": "or"
},
]
The new format brings a lot of short-term benefits and is compatible with our long-term vision of the filtering capabilities in OpenCTI. We chose a simple recursive structure that allow complex combination of any sort with respect to basic boolean logic.
The list of operator is fixed and can be extended during future developments.
// filter formats in OpenCTI >= 5.12
type FilterGroup = {
mode: 'and' | 'or'
filters: Filter[]
filterGroups: FilterGroup[] // recursive definition
}
type Filter = {
key: string[]
values: string[]
operator: 'eq' | 'not_eq' | 'gt' // ... and more
mode: 'and' | 'or',
}
// "give me Reports and RFIs, not marked TLP:RED with labelX or no label"
const filters = {
mode: 'and',
filters: [
{ key: 'entity_type', values: ['Report', 'Case-Rfi'], operator: 'eq', mode: 'or', },
{ key: 'objectMarking', values: ['<id-for-TLP:RED>'], operator: 'not_eq', mode: 'or', },
],
filterGroups: [{
mode: 'or',
filters: [
{ key: 'objectLabel', values: ["<id-for-labelX>"], operator: 'eq', mode: 'or', },
{ key: 'objectLabel', values: [], operator: 'nil', mode: 'or', },
],
filterGroups: [],
}],
};
Because changing filters format impacts almost everything in the platform, we decided to do a complete refactoring once and for all. We want this migration process to be clear and easy.
What has been changed
The new filter implementation bring major changes in the way filters are processed and executed.
-
We change the filters formats (see
FilterGroup
type above):- In the frontend, an operator and a mode are stored for each key.
- The new format enables filters imbrication thanks to the new attribute 'filterGroups'.
- The keys are of type string (no more static list of enums).
- The 'values' attribute can no longer contain null values (use the
nil
operator instead).
-
We also renamed some filter keys, to be consistent with the entities schema definitions.
- We implemented the handling of the different operators and modes in the backend.
- We introduced new void operators (
nil
/not_nil
) to test the presence or absence of value in any field.
How to migrate your own filters
We wrote a migration script to convert all stored filters created prior to version 5.12. These filters will thus be migrated automatically when starting your updated platform.
However, you might have your own connectors, queries, or python scripts that use the graphql API or the python client. If this is the case, you must change the filter format if you want to run the code against OpenCTI >= 5.12.
Filter conversion
To convert filters prior to version 5.12 in the new format:
- update the
key
field if it has been changed in 5.12 (see the conversion table below), - rename the field
filterMode
inmode
- if
values
containsnull
, see below for conversion.
Now you can build your new FilterGroup
object with:
mode: 'and'
filters
= array of converted filters (following previous steps),filterGroups: []
const oldFilter = {
key: 'old_key',
values: ['value1', 'value2'],
operator: 'XX',
filterMode: 'XX',
}
const convertedFilter = {
key: 'converted_key_if_necessary',
values: ['value1', 'value2'],
operator: 'XX',
mode: 'XX',
}
const newFilters = {
mode: 'and',
filters: [convertedFilter1, convertedFilter2...], // array with all converted filters
filterGroups: [],
}
If values
contains a null
value, you need to convert the filter by using the new nil
/ not_nil
operators. Here's the procedure:
-
Extract one filter dedicated to
null
- if operator was
'eq'
, switch tooperator: 'nil'
/ if operator wasnot_eq
, switch tooperator = 'not_nil'
values = []
- if operator was
-
Extract another filter for all the other values.
// "Must have a label that is not Label1 or Label2"
const oldFilter = {
key: 'labelledBy',
values: [null, 'id-for-Label1', 'id-for-Label2'],
operator: 'not_eq',
filterMode: 'and',
}
const newFilters = {
mode: 'and',
filters: [
{
key: 'objectLabel',
values: ['id-label-1', 'id-for-Label2'],
operator: 'not_eq',
mode: 'and',
},
{
key: 'objectLabel',
values: [],
operator: 'not_nil',
mode: 'and',
},
],
filterGroups: [],
}
Switch to nested filter to preserve logic
To preserve the logic of your old filter you might need to compose nested filter groups. This could happen for instance when using eq
operator with null
values for one filter, combined in and
mode with other filters.
Filter keys conversion table
Make sure you address all the filter keys that require conversion, following the map below:
// array of [oldKey, newKey] for the renamed keys
const keyConversion = [
['labelledBy', 'objectLabel'],
['markedBy', 'objectMarking'],
['objectContains', 'objects'],
['killChainPhase', 'killChainPhases'],
['assigneeTo', 'objectAssignee'],
['participant', 'objectParticipant'],
['creator', 'creator_id'],
['hasExternalReference', 'externalReferences'],
['hashes_MD5', 'hashes.MD5'],
['hashes_SHA1', 'hashes.SHA-1'],
['hashes_SHA256', 'hashes.SHA-256'],
['hashes_SHA512', 'hashes.SHA-512']
]
Examples
Simple example
Let's start with a simple case:
All Reports with label "label1" or "label2"
(entity_type = Report) AND (label = label1 OR label2)
const oldBackendFilter = [
{
key: 'entity_type',
values: ['Report'],
operator: 'eq',
filterMode: 'or',
},
{
key: 'labelledBy',
values: [label1_id, label2_id],
operator: 'eq',
filterMode: 'or',
},
];
const newFilters = {
mode: 'and',
filters: [
{
key: 'entity_type',
values: ['Report'],
operator: 'eq',
mode: 'or',
},
{
key: 'objectLabel',
values: [label1_id, label2_id],
operator: 'eq',
mode: 'or',
}
],
filterGroups: [],
};
Complex example
Now let's see a more complex case involving filter group nesting:
"All Reports that have either the label "label1" or "label2", or no label at all"
(entity_type = Report) AND (label = "No label" OR label1 OR label2)
const oldBackendFilter = [
{
key: 'labelledBy',
values: [label1_id, label2_id, null],
operator: 'eq',
filterMode: 'or',
},
{
key: 'entity_type',
values: ['Report'],
operator: 'eq',
filterMode: 'or',
},
];
const newFilters = {
mode: 'and',
filters: [
{
key: 'entity_type',
values: ['Report'],
operator: 'eq',
mode: 'or',
},
],
// the combination mode/operator in this case requires nested filter groups
filterGroups: [
{
mode: 'or',
filters: [
{
key: 'objectLabel',
values: [label1_id, label2_id],
operator: 'eq',
mode: 'or',
},
{
key: 'objectLabel',
operator: 'nil',
values: [], // empty, does not matter
mode: 'or', // and/or, does not matter
},
],
filterGroups: [],
}
],
};
Residual dynamic filters
Dynamic filters are not stored in the database, they enable to filter view in the UI, e.g. filters in entities list, investigations, knowledge graphs. They are saved as URL parameters, and can be saved in local storage.
These filters are not migrated automatically and are lost when moving to 5.12. This concerns the filters saved for each view, that are restored when coming back to the same view. You will need to reconstruct the filters by hand in the UI; these new filters will be properly saved and restored afterward.
Also, when going to an url with filters in the old format, OpenCTI will display a warning and remove the filter parameters. Only URLs built by OpenCTI 5.12 are compatible with it, so you will need to reconstruct the filters by hand and save / share your updated links.