Skip to content

Commit

Permalink
[8.x] [Entity Store] [Asset Inventory] Universal entity definition (e…
Browse files Browse the repository at this point in the history
…lastic#202888) (elastic#205536)

# Backport

This will backport the following commits from `main` to `8.x`:
- [[Entity Store] [Asset Inventory] Universal entity definition
(elastic#202888)](elastic#202888)

<!--- Backport version: 8.9.8 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT [{"author":{"name":"Tiago Vila
Verde","email":"[email protected]"},"sourceCommit":{"committedDate":"2025-01-03T16:43:16Z","message":"[Entity
Store] [Asset Inventory] Universal entity definition (elastic#202888)\n\n##
Summary\r\n\r\nThis PR adds a universal entity definition.\r\nA
universal entity uses `related.entity` as an identifier field
and\r\nincludes an extra processor step that parses the
field\r\n`entities.keyword` and extracts all the entities in said field
(whose\r\noriginal data comes from `related.entities`).\r\n\r\nSee
this\r\n[doc](https://docs.google.com/document/d/1D8xDtn3HHP65i1Y3eIButacD6ZizyjZZRJB7mxlXzQY/edit?tab=t.0#heading=h.9fz3qtlfzjg7)\r\nfor
more details.\r\n\r\nTo accomplish this, we need to allow describing an
entity along with\r\nextra entity store resources required for that
entity's engine.\r\nThis PR reworks the current entity store by
introducing an `Entity\r\nDescription`, which has all that required
information. From it, we can\r\nbuild an `EntityEngineDescription` which
adds all the needed data that\r\nmust be computed (as opposed to
hardcoded) and is then used to generate\r\nall the resources needed for
that Entity's engine (entity definition,\r\npipeline, enrich policy,
index mappings, etc).\r\n\r\n<img width=\"3776\"
alt=\"EntityDescriptions\"\r\nsrc=\"https://github.com/user-attachments/assets/bdf7915f-1981-47e6-a815-31163f24ad03\">\r\n\r\nThis
required a refactoring of the current `UnitedEntityDefinition`,\r\nwhich
has now been removed in favour of more contextual functions for\r\nall
the different parts.\r\nThe intention is to decouple the Entity
Description schema from the\r\nschemas required for field retention,
entity manager and pipeline. We\r\ncan then freely expand on our Entity
Description as required, and simply\r\nalter the conversion functions
when needed.\r\n\r\n## How to test\r\n\r\n1. On a fresh ES cluster, add
some entity data\r\n* For hosts and user, use the [security
documents\r\ngenerator](https://github.com/elastic/security-documents-generator)\r\n
* For universal, there are a few more steps:\r\n 1. Create the
`entity.keyword` builder pipeline\r\n 2. Add it to a index template\r\n
3. Post some docs to the corresponding index \r\n2. Initialise the
universal entity engine via:
`POST\r\nkbn:/api/entity_store/engines/universal/init {}`\r\n* Note that
using the UI does not work, as we've specifically removed\r\nthe
Universal engine from the normal Entity Store workflow\r\n3. Check the
status of the store is `running` via
`GET\r\nkbn:/api/entity_store/status`\r\n4. Once the transform runs, you
can query `GET entities*/_search` to see\r\nthe created
entities\r\n\r\nNote that universal entities do not show up in the
dashboard Entities\r\nList.\r\n\r\n\r\n### Code to ingest
data\r\n<details>\r\n<summary>Pipeline</summary>\r\n\r\n```js\r\nPUT
_ingest/pipeline/entities-keyword-builder\r\n{\r\n
\"description\":\"Serialize entities.metadata into a keyword
field\",\r\n \"processors\":[\r\n {\r\n \"script\":{\r\n
\"lang\":\"painless\",\r\n \"source\":\"\"\"\r\nString jsonFromMap(Map
map) {\r\n StringBuilder json = new StringBuilder(\"{\");\r\n boolean
first = true;\r\n\r\n for (entry in map.entrySet()) {\r\n if (!first)
{\r\n json.append(\",\");\r\n }\r\n first = false;\r\n\r\n String key =
entry.getKey().replace(\"\\\"\", \"\\\\\\\"\");\r\n Object value =
entry.getValue();\r\n\r\n
json.append(\"\\\"\").append(key).append(\"\\\":\");\r\n\r\n if (value
instanceof String) {\r\n String escapedValue = ((String)
value).replace(\"\\\"\", \"\\\\\\\"\").replace(\"=\", \":\");\r\n
json.append(\"\\\"\").append(escapedValue).append(\"\\\"\");\r\n } else
if (value instanceof Map) {\r\n json.append(jsonFromMap((Map)
value));\r\n } else if (value instanceof List) {\r\n
json.append(jsonFromList((List) value));\r\n } else if (value instanceof
Boolean || value instanceof Number) {\r\n
json.append(value.toString());\r\n } else {\r\n // For other types,
treat as string\r\n String escapedValue =
value.toString().replace(\"\\\"\", \"\\\\\\\"\").replace(\"=\",
\":\");\r\n
json.append(\"\\\"\").append(escapedValue).append(\"\\\"\");\r\n }\r\n
}\r\n\r\n json.append(\"}\");\r\n return
json.toString();\r\n}\r\n\r\nString jsonFromList(List list) {\r\n\r\n
StringBuilder json = new StringBuilder(\"[\");\r\n boolean first =
true;\r\n\r\n for (item in list) {\r\n if (!first) {\r\n
json.append(\",\");\r\n }\r\n first = false;\r\n\r\n if (item instanceof
String) {\r\n String escapedItem = ((String) item).replace(\"\\\"\",
\"\\\\\\\"\").replace(\"=\", \":\");\r\n
json.append(\"\\\"\").append(escapedItem).append(\"\\\"\");\r\n } else
if (item instanceof Map) {\r\n json.append(jsonFromMap((Map) item));\r\n
} else if (item instanceof List) {\r\n json.append(jsonFromList((List)
item));\r\n } else if (item instanceof Boolean || item instanceof
Number) {\r\n json.append(item.toString());\r\n } else {\r\n // For
other types, treat as string\r\n String escapedItem =
item.toString().replace(\"\\\"\", \"\\\\\\\"\").replace(\"=\",
\":\");\r\n
json.append(\"\\\"\").append(escapedItem).append(\"\\\"\");\r\n }\r\n
}\r\n\r\n json.append(\"]\");\r\n return
json.toString();\r\n}\r\n\r\ndef metadata =
jsonFromMap(ctx['entities']['metadata']);\r\nctx['entities']['keyword']
= metadata;\r\n\"\"\"\r\n\r\n }\r\n }\r\n
]\r\n}\r\n```\r\n</details>\r\n\r\n<details>\r\n<summary>Index
template</summary>\r\n\r\n```js\r\nPUT
/_index_template/entity_store_index_template\r\n{\r\n
\"index_patterns\":[\r\n \"logs-store\"\r\n ],\r\n \"template\":{\r\n
\"settings\":{\r\n \"index\":{\r\n
\"default_pipeline\":\"entities-keyword-builder\"\r\n }\r\n },\r\n
\"mappings\":{\r\n \"properties\":{\r\n \"@timestamp\":{\r\n
\"type\":\"date\"\r\n },\r\n \"message\":{\r\n \"type\":\"text\"\r\n
},\r\n \"event\":{\r\n \"properties\":{\r\n \"action\":{\r\n
\"type\":\"keyword\"\r\n },\r\n \"category\":{\r\n
\"type\":\"keyword\"\r\n },\r\n \"type\":{\r\n \"type\":\"keyword\"\r\n
},\r\n \"outcome\":{\r\n \"type\":\"keyword\"\r\n },\r\n
\"provider\":{\r\n \"type\":\"keyword\"\r\n },\r\n \"ingested\":{\r\n
\"type\": \"date\"\r\n }\r\n }\r\n },\r\n \"related\":{\r\n
\"properties\":{\r\n \"entity\":{\r\n \"type\":\"keyword\"\r\n }\r\n
}\r\n },\r\n \"entities\":{\r\n \"properties\":{\r\n \"metadata\":{\r\n
\"type\":\"flattened\"\r\n },\r\n \"keyword\":{\r\n
\"type\":\"keyword\"\r\n }\r\n }\r\n }\r\n }\r\n }\r\n
}\r\n}\r\n```\r\n</details>\r\n\r\n<details>\r\n<summary>Example source
doc</summary>\r\n\r\n```js\r\nPOST /logs-store/_doc/\r\n{\r\n
\"@timestamp\":\"2024-11-29T10:01:00Z\",\r\n \"message\":\"Eddie\",\r\n
\"event\": {\r\n \"type\":[\r\n \"creation\"\r\n ],\r\n \"ingested\":
\"2024-12-03T10:01:00Z\"\r\n },\r\n \"related\":{\r\n \"entity\":[\r\n
\"AKIAI44QH8DHBEXAMPLE\"\r\n ]\r\n },\r\n \"entities\":{\r\n
\"metadata\":{\r\n \"AKIAI44QH8DHBEXAMPLE\":{\r\n \"entity\":{\r\n
\"id\":\"AKIAI44QH8DHBEXAMPLE\",\r\n \"category\":\"Access
Management\",\r\n \"type\":\"AWS IAM Access Key\"\r\n },\r\n
\"cloud\":{\r\n \"account\":{\r\n \"id\":\"444455556666\"\r\n }\r\n
}\r\n }\r\n }\r\n }\r\n}\r\n```\r\n</details>\r\n\r\n### To do\r\n\r\n-
[x] Add/Improve [Unit or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\n-
[x] Feature flag\r\n\r\n\r\n----\r\n#### Update:\r\n\r\nAdded
`assetInventoryStoreEnabled` Feature Flag. It is disabled by\r\ndefault
and even when enabled, the `/api/entity_store/enable` route does\r\nnot
initialize the Universal Entity
Engine.\r\n`/api/entity_store/engines/universal/init` needs to be
manually called\r\nto initialize
it\r\n\r\n---------\r\n\r\nCo-authored-by: kibanamachine
<[email protected]>\r\nCo-authored-by:
Rômulo Farias <[email protected]>\r\nCo-authored-by:
jaredburgettelastic <[email protected]>\r\nCo-authored-by:
Elastic Machine
<[email protected]>","sha":"c6b0a31d8ec6d423a8071f50a22e55acedd0dee0","branchLabelMapping":{"^v9.0.0$":"main","^v8.18.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","v9.0.0","Team:Cloud
Security","ci:cloud-deploy","Theme: entity_analytics","Team:Entity
Analytics","backport:version","v8.18.0"],"number":202888,"url":"https://github.com/elastic/kibana/pull/202888","mergeCommit":{"message":"[Entity
Store] [Asset Inventory] Universal entity definition (elastic#202888)\n\n##
Summary\r\n\r\nThis PR adds a universal entity definition.\r\nA
universal entity uses `related.entity` as an identifier field
and\r\nincludes an extra processor step that parses the
field\r\n`entities.keyword` and extracts all the entities in said field
(whose\r\noriginal data comes from `related.entities`).\r\n\r\nSee
this\r\n[doc](https://docs.google.com/document/d/1D8xDtn3HHP65i1Y3eIButacD6ZizyjZZRJB7mxlXzQY/edit?tab=t.0#heading=h.9fz3qtlfzjg7)\r\nfor
more details.\r\n\r\nTo accomplish this, we need to allow describing an
entity along with\r\nextra entity store resources required for that
entity's engine.\r\nThis PR reworks the current entity store by
introducing an `Entity\r\nDescription`, which has all that required
information. From it, we can\r\nbuild an `EntityEngineDescription` which
adds all the needed data that\r\nmust be computed (as opposed to
hardcoded) and is then used to generate\r\nall the resources needed for
that Entity's engine (entity definition,\r\npipeline, enrich policy,
index mappings, etc).\r\n\r\n<img width=\"3776\"
alt=\"EntityDescriptions\"\r\nsrc=\"https://github.com/user-attachments/assets/bdf7915f-1981-47e6-a815-31163f24ad03\">\r\n\r\nThis
required a refactoring of the current `UnitedEntityDefinition`,\r\nwhich
has now been removed in favour of more contextual functions for\r\nall
the different parts.\r\nThe intention is to decouple the Entity
Description schema from the\r\nschemas required for field retention,
entity manager and pipeline. We\r\ncan then freely expand on our Entity
Description as required, and simply\r\nalter the conversion functions
when needed.\r\n\r\n## How to test\r\n\r\n1. On a fresh ES cluster, add
some entity data\r\n* For hosts and user, use the [security
documents\r\ngenerator](https://github.com/elastic/security-documents-generator)\r\n
* For universal, there are a few more steps:\r\n 1. Create the
`entity.keyword` builder pipeline\r\n 2. Add it to a index template\r\n
3. Post some docs to the corresponding index \r\n2. Initialise the
universal entity engine via:
`POST\r\nkbn:/api/entity_store/engines/universal/init {}`\r\n* Note that
using the UI does not work, as we've specifically removed\r\nthe
Universal engine from the normal Entity Store workflow\r\n3. Check the
status of the store is `running` via
`GET\r\nkbn:/api/entity_store/status`\r\n4. Once the transform runs, you
can query `GET entities*/_search` to see\r\nthe created
entities\r\n\r\nNote that universal entities do not show up in the
dashboard Entities\r\nList.\r\n\r\n\r\n### Code to ingest
data\r\n<details>\r\n<summary>Pipeline</summary>\r\n\r\n```js\r\nPUT
_ingest/pipeline/entities-keyword-builder\r\n{\r\n
\"description\":\"Serialize entities.metadata into a keyword
field\",\r\n \"processors\":[\r\n {\r\n \"script\":{\r\n
\"lang\":\"painless\",\r\n \"source\":\"\"\"\r\nString jsonFromMap(Map
map) {\r\n StringBuilder json = new StringBuilder(\"{\");\r\n boolean
first = true;\r\n\r\n for (entry in map.entrySet()) {\r\n if (!first)
{\r\n json.append(\",\");\r\n }\r\n first = false;\r\n\r\n String key =
entry.getKey().replace(\"\\\"\", \"\\\\\\\"\");\r\n Object value =
entry.getValue();\r\n\r\n
json.append(\"\\\"\").append(key).append(\"\\\":\");\r\n\r\n if (value
instanceof String) {\r\n String escapedValue = ((String)
value).replace(\"\\\"\", \"\\\\\\\"\").replace(\"=\", \":\");\r\n
json.append(\"\\\"\").append(escapedValue).append(\"\\\"\");\r\n } else
if (value instanceof Map) {\r\n json.append(jsonFromMap((Map)
value));\r\n } else if (value instanceof List) {\r\n
json.append(jsonFromList((List) value));\r\n } else if (value instanceof
Boolean || value instanceof Number) {\r\n
json.append(value.toString());\r\n } else {\r\n // For other types,
treat as string\r\n String escapedValue =
value.toString().replace(\"\\\"\", \"\\\\\\\"\").replace(\"=\",
\":\");\r\n
json.append(\"\\\"\").append(escapedValue).append(\"\\\"\");\r\n }\r\n
}\r\n\r\n json.append(\"}\");\r\n return
json.toString();\r\n}\r\n\r\nString jsonFromList(List list) {\r\n\r\n
StringBuilder json = new StringBuilder(\"[\");\r\n boolean first =
true;\r\n\r\n for (item in list) {\r\n if (!first) {\r\n
json.append(\",\");\r\n }\r\n first = false;\r\n\r\n if (item instanceof
String) {\r\n String escapedItem = ((String) item).replace(\"\\\"\",
\"\\\\\\\"\").replace(\"=\", \":\");\r\n
json.append(\"\\\"\").append(escapedItem).append(\"\\\"\");\r\n } else
if (item instanceof Map) {\r\n json.append(jsonFromMap((Map) item));\r\n
} else if (item instanceof List) {\r\n json.append(jsonFromList((List)
item));\r\n } else if (item instanceof Boolean || item instanceof
Number) {\r\n json.append(item.toString());\r\n } else {\r\n // For
other types, treat as string\r\n String escapedItem =
item.toString().replace(\"\\\"\", \"\\\\\\\"\").replace(\"=\",
\":\");\r\n
json.append(\"\\\"\").append(escapedItem).append(\"\\\"\");\r\n }\r\n
}\r\n\r\n json.append(\"]\");\r\n return
json.toString();\r\n}\r\n\r\ndef metadata =
jsonFromMap(ctx['entities']['metadata']);\r\nctx['entities']['keyword']
= metadata;\r\n\"\"\"\r\n\r\n }\r\n }\r\n
]\r\n}\r\n```\r\n</details>\r\n\r\n<details>\r\n<summary>Index
template</summary>\r\n\r\n```js\r\nPUT
/_index_template/entity_store_index_template\r\n{\r\n
\"index_patterns\":[\r\n \"logs-store\"\r\n ],\r\n \"template\":{\r\n
\"settings\":{\r\n \"index\":{\r\n
\"default_pipeline\":\"entities-keyword-builder\"\r\n }\r\n },\r\n
\"mappings\":{\r\n \"properties\":{\r\n \"@timestamp\":{\r\n
\"type\":\"date\"\r\n },\r\n \"message\":{\r\n \"type\":\"text\"\r\n
},\r\n \"event\":{\r\n \"properties\":{\r\n \"action\":{\r\n
\"type\":\"keyword\"\r\n },\r\n \"category\":{\r\n
\"type\":\"keyword\"\r\n },\r\n \"type\":{\r\n \"type\":\"keyword\"\r\n
},\r\n \"outcome\":{\r\n \"type\":\"keyword\"\r\n },\r\n
\"provider\":{\r\n \"type\":\"keyword\"\r\n },\r\n \"ingested\":{\r\n
\"type\": \"date\"\r\n }\r\n }\r\n },\r\n \"related\":{\r\n
\"properties\":{\r\n \"entity\":{\r\n \"type\":\"keyword\"\r\n }\r\n
}\r\n },\r\n \"entities\":{\r\n \"properties\":{\r\n \"metadata\":{\r\n
\"type\":\"flattened\"\r\n },\r\n \"keyword\":{\r\n
\"type\":\"keyword\"\r\n }\r\n }\r\n }\r\n }\r\n }\r\n
}\r\n}\r\n```\r\n</details>\r\n\r\n<details>\r\n<summary>Example source
doc</summary>\r\n\r\n```js\r\nPOST /logs-store/_doc/\r\n{\r\n
\"@timestamp\":\"2024-11-29T10:01:00Z\",\r\n \"message\":\"Eddie\",\r\n
\"event\": {\r\n \"type\":[\r\n \"creation\"\r\n ],\r\n \"ingested\":
\"2024-12-03T10:01:00Z\"\r\n },\r\n \"related\":{\r\n \"entity\":[\r\n
\"AKIAI44QH8DHBEXAMPLE\"\r\n ]\r\n },\r\n \"entities\":{\r\n
\"metadata\":{\r\n \"AKIAI44QH8DHBEXAMPLE\":{\r\n \"entity\":{\r\n
\"id\":\"AKIAI44QH8DHBEXAMPLE\",\r\n \"category\":\"Access
Management\",\r\n \"type\":\"AWS IAM Access Key\"\r\n },\r\n
\"cloud\":{\r\n \"account\":{\r\n \"id\":\"444455556666\"\r\n }\r\n
}\r\n }\r\n }\r\n }\r\n}\r\n```\r\n</details>\r\n\r\n### To do\r\n\r\n-
[x] Add/Improve [Unit or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\n-
[x] Feature flag\r\n\r\n\r\n----\r\n#### Update:\r\n\r\nAdded
`assetInventoryStoreEnabled` Feature Flag. It is disabled by\r\ndefault
and even when enabled, the `/api/entity_store/enable` route does\r\nnot
initialize the Universal Entity
Engine.\r\n`/api/entity_store/engines/universal/init` needs to be
manually called\r\nto initialize
it\r\n\r\n---------\r\n\r\nCo-authored-by: kibanamachine
<[email protected]>\r\nCo-authored-by:
Rômulo Farias <[email protected]>\r\nCo-authored-by:
jaredburgettelastic <[email protected]>\r\nCo-authored-by:
Elastic Machine
<[email protected]>","sha":"c6b0a31d8ec6d423a8071f50a22e55acedd0dee0"}},"sourceBranch":"main","suggestedTargetBranches":["8.x"],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","labelRegex":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/202888","number":202888,"mergeCommit":{"message":"[Entity
Store] [Asset Inventory] Universal entity definition (elastic#202888)\n\n##
Summary\r\n\r\nThis PR adds a universal entity definition.\r\nA
universal entity uses `related.entity` as an identifier field
and\r\nincludes an extra processor step that parses the
field\r\n`entities.keyword` and extracts all the entities in said field
(whose\r\noriginal data comes from `related.entities`).\r\n\r\nSee
this\r\n[doc](https://docs.google.com/document/d/1D8xDtn3HHP65i1Y3eIButacD6ZizyjZZRJB7mxlXzQY/edit?tab=t.0#heading=h.9fz3qtlfzjg7)\r\nfor
more details.\r\n\r\nTo accomplish this, we need to allow describing an
entity along with\r\nextra entity store resources required for that
entity's engine.\r\nThis PR reworks the current entity store by
introducing an `Entity\r\nDescription`, which has all that required
information. From it, we can\r\nbuild an `EntityEngineDescription` which
adds all the needed data that\r\nmust be computed (as opposed to
hardcoded) and is then used to generate\r\nall the resources needed for
that Entity's engine (entity definition,\r\npipeline, enrich policy,
index mappings, etc).\r\n\r\n<img width=\"3776\"
alt=\"EntityDescriptions\"\r\nsrc=\"https://github.com/user-attachments/assets/bdf7915f-1981-47e6-a815-31163f24ad03\">\r\n\r\nThis
required a refactoring of the current `UnitedEntityDefinition`,\r\nwhich
has now been removed in favour of more contextual functions for\r\nall
the different parts.\r\nThe intention is to decouple the Entity
Description schema from the\r\nschemas required for field retention,
entity manager and pipeline. We\r\ncan then freely expand on our Entity
Description as required, and simply\r\nalter the conversion functions
when needed.\r\n\r\n## How to test\r\n\r\n1. On a fresh ES cluster, add
some entity data\r\n* For hosts and user, use the [security
documents\r\ngenerator](https://github.com/elastic/security-documents-generator)\r\n
* For universal, there are a few more steps:\r\n 1. Create the
`entity.keyword` builder pipeline\r\n 2. Add it to a index template\r\n
3. Post some docs to the corresponding index \r\n2. Initialise the
universal entity engine via:
`POST\r\nkbn:/api/entity_store/engines/universal/init {}`\r\n* Note that
using the UI does not work, as we've specifically removed\r\nthe
Universal engine from the normal Entity Store workflow\r\n3. Check the
status of the store is `running` via
`GET\r\nkbn:/api/entity_store/status`\r\n4. Once the transform runs, you
can query `GET entities*/_search` to see\r\nthe created
entities\r\n\r\nNote that universal entities do not show up in the
dashboard Entities\r\nList.\r\n\r\n\r\n### Code to ingest
data\r\n<details>\r\n<summary>Pipeline</summary>\r\n\r\n```js\r\nPUT
_ingest/pipeline/entities-keyword-builder\r\n{\r\n
\"description\":\"Serialize entities.metadata into a keyword
field\",\r\n \"processors\":[\r\n {\r\n \"script\":{\r\n
\"lang\":\"painless\",\r\n \"source\":\"\"\"\r\nString jsonFromMap(Map
map) {\r\n StringBuilder json = new StringBuilder(\"{\");\r\n boolean
first = true;\r\n\r\n for (entry in map.entrySet()) {\r\n if (!first)
{\r\n json.append(\",\");\r\n }\r\n first = false;\r\n\r\n String key =
entry.getKey().replace(\"\\\"\", \"\\\\\\\"\");\r\n Object value =
entry.getValue();\r\n\r\n
json.append(\"\\\"\").append(key).append(\"\\\":\");\r\n\r\n if (value
instanceof String) {\r\n String escapedValue = ((String)
value).replace(\"\\\"\", \"\\\\\\\"\").replace(\"=\", \":\");\r\n
json.append(\"\\\"\").append(escapedValue).append(\"\\\"\");\r\n } else
if (value instanceof Map) {\r\n json.append(jsonFromMap((Map)
value));\r\n } else if (value instanceof List) {\r\n
json.append(jsonFromList((List) value));\r\n } else if (value instanceof
Boolean || value instanceof Number) {\r\n
json.append(value.toString());\r\n } else {\r\n // For other types,
treat as string\r\n String escapedValue =
value.toString().replace(\"\\\"\", \"\\\\\\\"\").replace(\"=\",
\":\");\r\n
json.append(\"\\\"\").append(escapedValue).append(\"\\\"\");\r\n }\r\n
}\r\n\r\n json.append(\"}\");\r\n return
json.toString();\r\n}\r\n\r\nString jsonFromList(List list) {\r\n\r\n
StringBuilder json = new StringBuilder(\"[\");\r\n boolean first =
true;\r\n\r\n for (item in list) {\r\n if (!first) {\r\n
json.append(\",\");\r\n }\r\n first = false;\r\n\r\n if (item instanceof
String) {\r\n String escapedItem = ((String) item).replace(\"\\\"\",
\"\\\\\\\"\").replace(\"=\", \":\");\r\n
json.append(\"\\\"\").append(escapedItem).append(\"\\\"\");\r\n } else
if (item instanceof Map) {\r\n json.append(jsonFromMap((Map) item));\r\n
} else if (item instanceof List) {\r\n json.append(jsonFromList((List)
item));\r\n } else if (item instanceof Boolean || item instanceof
Number) {\r\n json.append(item.toString());\r\n } else {\r\n // For
other types, treat as string\r\n String escapedItem =
item.toString().replace(\"\\\"\", \"\\\\\\\"\").replace(\"=\",
\":\");\r\n
json.append(\"\\\"\").append(escapedItem).append(\"\\\"\");\r\n }\r\n
}\r\n\r\n json.append(\"]\");\r\n return
json.toString();\r\n}\r\n\r\ndef metadata =
jsonFromMap(ctx['entities']['metadata']);\r\nctx['entities']['keyword']
= metadata;\r\n\"\"\"\r\n\r\n }\r\n }\r\n
]\r\n}\r\n```\r\n</details>\r\n\r\n<details>\r\n<summary>Index
template</summary>\r\n\r\n```js\r\nPUT
/_index_template/entity_store_index_template\r\n{\r\n
\"index_patterns\":[\r\n \"logs-store\"\r\n ],\r\n \"template\":{\r\n
\"settings\":{\r\n \"index\":{\r\n
\"default_pipeline\":\"entities-keyword-builder\"\r\n }\r\n },\r\n
\"mappings\":{\r\n \"properties\":{\r\n \"@timestamp\":{\r\n
\"type\":\"date\"\r\n },\r\n \"message\":{\r\n \"type\":\"text\"\r\n
},\r\n \"event\":{\r\n \"properties\":{\r\n \"action\":{\r\n
\"type\":\"keyword\"\r\n },\r\n \"category\":{\r\n
\"type\":\"keyword\"\r\n },\r\n \"type\":{\r\n \"type\":\"keyword\"\r\n
},\r\n \"outcome\":{\r\n \"type\":\"keyword\"\r\n },\r\n
\"provider\":{\r\n \"type\":\"keyword\"\r\n },\r\n \"ingested\":{\r\n
\"type\": \"date\"\r\n }\r\n }\r\n },\r\n \"related\":{\r\n
\"properties\":{\r\n \"entity\":{\r\n \"type\":\"keyword\"\r\n }\r\n
}\r\n },\r\n \"entities\":{\r\n \"properties\":{\r\n \"metadata\":{\r\n
\"type\":\"flattened\"\r\n },\r\n \"keyword\":{\r\n
\"type\":\"keyword\"\r\n }\r\n }\r\n }\r\n }\r\n }\r\n
}\r\n}\r\n```\r\n</details>\r\n\r\n<details>\r\n<summary>Example source
doc</summary>\r\n\r\n```js\r\nPOST /logs-store/_doc/\r\n{\r\n
\"@timestamp\":\"2024-11-29T10:01:00Z\",\r\n \"message\":\"Eddie\",\r\n
\"event\": {\r\n \"type\":[\r\n \"creation\"\r\n ],\r\n \"ingested\":
\"2024-12-03T10:01:00Z\"\r\n },\r\n \"related\":{\r\n \"entity\":[\r\n
\"AKIAI44QH8DHBEXAMPLE\"\r\n ]\r\n },\r\n \"entities\":{\r\n
\"metadata\":{\r\n \"AKIAI44QH8DHBEXAMPLE\":{\r\n \"entity\":{\r\n
\"id\":\"AKIAI44QH8DHBEXAMPLE\",\r\n \"category\":\"Access
Management\",\r\n \"type\":\"AWS IAM Access Key\"\r\n },\r\n
\"cloud\":{\r\n \"account\":{\r\n \"id\":\"444455556666\"\r\n }\r\n
}\r\n }\r\n }\r\n }\r\n}\r\n```\r\n</details>\r\n\r\n### To do\r\n\r\n-
[x] Add/Improve [Unit or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\n-
[x] Feature flag\r\n\r\n\r\n----\r\n#### Update:\r\n\r\nAdded
`assetInventoryStoreEnabled` Feature Flag. It is disabled by\r\ndefault
and even when enabled, the `/api/entity_store/enable` route does\r\nnot
initialize the Universal Entity
Engine.\r\n`/api/entity_store/engines/universal/init` needs to be
manually called\r\nto initialize
it\r\n\r\n---------\r\n\r\nCo-authored-by: kibanamachine
<[email protected]>\r\nCo-authored-by:
Rômulo Farias <[email protected]>\r\nCo-authored-by:
jaredburgettelastic <[email protected]>\r\nCo-authored-by:
Elastic Machine
<[email protected]>","sha":"c6b0a31d8ec6d423a8071f50a22e55acedd0dee0"}},{"branch":"8.x","label":"v8.18.0","labelRegex":"^v8.18.0$","isSourceBranch":false,"state":"NOT_CREATED"}]}]
BACKPORT-->

Co-authored-by: Tiago Vila Verde <[email protected]>
  • Loading branch information
jaredburgettelastic and tiansivive authored Jan 3, 2025
1 parent 25ede9b commit 4994699
Show file tree
Hide file tree
Showing 54 changed files with 1,130 additions and 1,130 deletions.
5 changes: 4 additions & 1 deletion oas_docs/output/kibana.serverless.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -45909,6 +45909,8 @@ components:
enum:
- user
- host
- service
- universal
type: string
Security_Entity_Analytics_API_HostEntity:
type: object
Expand Down Expand Up @@ -45979,6 +45981,7 @@ components:
- host.name
- user.name
- service.name
- related.entity
type: string
Security_Entity_Analytics_API_IndexPattern:
type: string
Expand Down Expand Up @@ -50085,7 +50088,7 @@ components:
items:
type: string
description: |
A list of "carbon copy" email addresses. Addresses can be specified in `user@host-name` format or in name `<user@host-name>` format
A list of "carbon copy" email addresses. Addresses can be specified in `user@host-name` format or in name `<user@host-name>` format
message:
type: string
description: The email message text. Markdown format is supported.
Expand Down
2 changes: 2 additions & 0 deletions oas_docs/output/kibana.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -35059,6 +35059,7 @@ components:
- user
- host
- service
- universal
type: string
Security_Entity_Analytics_API_HostEntity:
type: object
Expand Down Expand Up @@ -35129,6 +35130,7 @@ components:
- host.name
- user.name
- service.name
- related.entity
type: string
Security_Entity_Analytics_API_IndexPattern:
type: string
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
import { z } from '@kbn/zod';

export type IdField = z.infer<typeof IdField>;
export const IdField = z.enum(['host.name', 'user.name', 'service.name']);
export const IdField = z.enum(['host.name', 'user.name', 'service.name', 'related.entity']);
export type IdFieldEnum = typeof IdField.enum;
export const IdFieldEnum = IdField.enum;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ components:
- 'host.name'
- 'user.name'
- 'service.name'
- 'related.entity'
AssetCriticalityRecordIdParts:
type: object
properties:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
import { z } from '@kbn/zod';

export type EntityType = z.infer<typeof EntityType>;
export const EntityType = z.enum(['user', 'host', 'service']);
export const EntityType = z.enum(['user', 'host', 'service', 'universal']);
export type EntityTypeEnum = typeof EntityType.enum;
export const EntityTypeEnum = EntityType.enum;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ components:
- user
- host
- service
- universal

EngineDescriptor:
type: object
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ describe('parseAssetCriticalityCsvRow', () => {

// @ts-ignore result can now only be InvalidRecord
expect(result.error).toMatchInlineSnapshot(
`"Invalid entity type \\"invalid\\", expected to be one of: user, host, service"`
`"Invalid entity type \\"invalid\\", expected to be one of: user, host, service, universal"`
);
});

Expand All @@ -57,7 +57,7 @@ describe('parseAssetCriticalityCsvRow', () => {

// @ts-ignore result can now only be InvalidRecord
expect(result.error).toMatchInlineSnapshot(
`"Invalid entity type \\"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx...\\", expected to be one of: user, host, service"`
`"Invalid entity type \\"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx...\\", expected to be one of: user, host, service, universal"`
);
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export const IDENTITY_FIELD_MAP: Record<EntityType, IdField> = {
[EntityTypeEnum.host]: 'host.name',
[EntityTypeEnum.user]: 'user.name',
[EntityTypeEnum.service]: 'service.name',
[EntityTypeEnum.universal]: 'related.entity',
};

export const getAvailableEntityTypes = (): EntityType[] =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -266,8 +266,13 @@ export const allowedExperimentalValues = Object.freeze({
/**
* Enables CrowdStrike's RunScript RTR command
*/

crowdstrikeRunScriptEnabled: false,

/**
* Enables the Asset Inventory Entity Store feature.
* Allows initializing the Universal Entity Store via the API.
*/
assetInventoryStoreEnabled: false,
});

type ExperimentalConfigKeys = Array<keyof ExperimentalFeatures>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1002,6 +1002,7 @@ components:
- user
- host
- service
- universal
type: string
HostEntity:
type: object
Expand Down Expand Up @@ -1072,6 +1073,7 @@ components:
- host.name
- user.name
- service.name
- related.entity
type: string
IndexPattern:
type: string
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1002,6 +1002,7 @@ components:
- user
- host
- service
- universal
type: string
HostEntity:
type: object
Expand Down Expand Up @@ -1072,6 +1073,7 @@ components:
- host.name
- user.name
- service.name
- related.entity
type: string
IndexPattern:
type: string
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ const entityTypeByIdField = {
'host.name': 'host',
'user.name': 'user',
'service.name': 'service',
'related.entity': 'universal',
} as const;

export const getImplicitEntityFields = (record: AssetCriticalityUpsertWithDeleted) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,24 @@ import {
EngineComponentResourceEnum,
type EngineComponentStatus,
} from '../../../../../common/api/entity_analytics';
import type { UnitedEntityDefinition } from '../united_entity_definitions';
import type { EntityEngineInstallationDescriptor } from '../installation/types';

const getComponentTemplateName = (definitionId: string) => `${definitionId}-latest@platform`;

interface Options {
unitedDefinition: UnitedEntityDefinition;
/**
* The entity engine description id
**/
id: string;
esClient: ElasticsearchClient;
}

export const createEntityIndexComponentTemplate = ({ unitedDefinition, esClient }: Options) => {
const { entityManagerDefinition, indexMappings } = unitedDefinition;
const name = getComponentTemplateName(entityManagerDefinition.id);
export const createEntityIndexComponentTemplate = (
description: EntityEngineInstallationDescriptor,
esClient: ElasticsearchClient
) => {
const { id, indexMappings } = description;
const name = getComponentTemplateName(id);
return esClient.cluster.putComponentTemplate({
name,
body: {
Expand All @@ -35,9 +41,8 @@ export const createEntityIndexComponentTemplate = ({ unitedDefinition, esClient
});
};

export const deleteEntityIndexComponentTemplate = ({ unitedDefinition, esClient }: Options) => {
const { entityManagerDefinition } = unitedDefinition;
const name = getComponentTemplateName(entityManagerDefinition.id);
export const deleteEntityIndexComponentTemplate = ({ id, esClient }: Options) => {
const name = getComponentTemplateName(id);
return esClient.cluster.deleteComponentTemplate(
{ name },
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@
*/

import type { ElasticsearchClient, Logger } from '@kbn/core/server';
import type { EnrichPutPolicyRequest } from '@elastic/elasticsearch/lib/api/types';
import type { EntityType } from '../../../../../common/api/entity_analytics';
import { EngineComponentResourceEnum } from '../../../../../common/api/entity_analytics';
import { getEntitiesIndexName } from '../utils';
import type { UnitedEntityDefinition } from '../united_entity_definitions';
import type { EntityEngineInstallationDescriptor } from '../installation/types';

type DefinitionMetadata = Pick<UnitedEntityDefinition, 'namespace' | 'entityType' | 'version'>;
type DefinitionMetadata = Pick<EntityEngineInstallationDescriptor, 'entityType' | 'version'> & {
namespace: string;
};

export const getFieldRetentionEnrichPolicyName = ({
namespace,
Expand All @@ -21,69 +23,79 @@ export const getFieldRetentionEnrichPolicyName = ({
return `entity_store_field_retention_${entityType}_${namespace}_v${version}`;
};

const getFieldRetentionEnrichPolicy = (
unitedDefinition: UnitedEntityDefinition
): EnrichPutPolicyRequest => {
const { namespace, entityType, fieldRetentionDefinition } = unitedDefinition;
return {
name: getFieldRetentionEnrichPolicyName(unitedDefinition),
match: {
indices: getEntitiesIndexName(entityType, namespace),
match_field: fieldRetentionDefinition.matchField,
enrich_fields: fieldRetentionDefinition.fields.map(({ field }) => field),
},
};
};

export const createFieldRetentionEnrichPolicy = async ({
esClient,
unitedDefinition,
description,
options,
}: {
esClient: ElasticsearchClient;
unitedDefinition: UnitedEntityDefinition;
description: EntityEngineInstallationDescriptor;
options: { namespace: string };
}) => {
const policy = getFieldRetentionEnrichPolicy(unitedDefinition);
return esClient.enrich.putPolicy(policy);
return esClient.enrich.putPolicy({
name: getFieldRetentionEnrichPolicyName({
namespace: options.namespace,
entityType: description.entityType,
version: description.version,
}),
match: {
indices: getEntitiesIndexName(description.entityType, options.namespace),
match_field: description.identityField,
enrich_fields: description.fields.map(({ destination }) => destination),
},
});
};

export const executeFieldRetentionEnrichPolicy = async ({
esClient,
unitedDefinition,
entityType,
version,
logger,
options,
}: {
unitedDefinition: DefinitionMetadata;
entityType: EntityType;
version: string;
esClient: ElasticsearchClient;
logger: Logger;
options: { namespace: string };
}): Promise<{ executed: boolean }> => {
const name = getFieldRetentionEnrichPolicyName(unitedDefinition);
const name = getFieldRetentionEnrichPolicyName({
namespace: options.namespace,
entityType,
version,
});
try {
await esClient.enrich.executePolicy({ name });
return { executed: true };
} catch (e) {
if (e.statusCode === 404) {
return { executed: false };
}
logger.error(
`Error executing field retention enrich policy for ${unitedDefinition.entityType}: ${e.message}`
);
logger.error(`Error executing field retention enrich policy for ${entityType}: ${e.message}`);
throw e;
}
};

export const deleteFieldRetentionEnrichPolicy = async ({
unitedDefinition,
description,
options,
esClient,
logger,
attempts = 5,
delayMs = 2000,
}: {
unitedDefinition: DefinitionMetadata;
description: EntityEngineInstallationDescriptor;
options: { namespace: string };
esClient: ElasticsearchClient;
logger: Logger;
attempts?: number;
delayMs?: number;
}) => {
const name = getFieldRetentionEnrichPolicyName(unitedDefinition);
const name = getFieldRetentionEnrichPolicyName({
namespace: options.namespace,
entityType: description.entityType,
version: description.version,
});
let currentAttempt = 1;
while (currentAttempt <= attempts) {
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
*/

import type { ElasticsearchClient, Logger } from '@kbn/core/server';
import type { MappingTypeMapping } from '@elastic/elasticsearch/lib/api/types';
import {
EngineComponentResourceEnum,
type EngineComponentStatus,
Expand All @@ -14,6 +15,8 @@ import {
import { getEntitiesIndexName } from '../utils';
import { createOrUpdateIndex } from '../../utils/create_or_update_index';

import type { EntityEngineInstallationDescriptor } from '../installation/types';

interface Options {
entityType: EntityType;
esClient: ElasticsearchClient;
Expand Down Expand Up @@ -58,3 +61,51 @@ export const getEntityIndexStatus = async ({

return { id: index, installed: exists, resource: EngineComponentResourceEnum.index };
};

export type MappingProperties = NonNullable<MappingTypeMapping['properties']>;

export const generateIndexMappings = (
description: EntityEngineInstallationDescriptor
): MappingTypeMapping => {
const identityFieldMappings: MappingProperties = {
[description.identityField]: {
type: 'keyword',
fields: {
text: {
type: 'match_only_text',
},
},
},
};

const otherFieldMappings = description.fields
.filter(({ mapping }) => mapping)
.reduce((acc, { destination, mapping }) => {
acc[destination] = mapping;
return acc;
}, {} as MappingProperties);

return {
properties: { ...BASE_ENTITY_INDEX_MAPPING, ...identityFieldMappings, ...otherFieldMappings },
};
};

export const BASE_ENTITY_INDEX_MAPPING: MappingProperties = {
'@timestamp': {
type: 'date',
},
'asset.criticality': {
type: 'keyword',
},
'entity.name': {
type: 'keyword',
fields: {
text: {
type: 'match_only_text',
},
},
},
'entity.source': {
type: 'keyword',
},
};
Loading

0 comments on commit 4994699

Please sign in to comment.