Skip to content

Commit edf00b0

Browse files
[Entity Store v2] Add CRUD API (elastic#252052)
Closes elastic#245018 ## TODO: - [x] Add the 3 API routes - [x] Add Entity Manager to handle the operations - [x] Rebase onto single index code - [ ] ~~Update Entity schema with missing fields~~ We decided Entity v2 schema changes will get their own PR - [x] Support bulk async - [x] Update with Unique ID generation - [x] Add Scout tests - [ ] ~~Update documentation~~ Separate PR --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
1 parent bc6d47b commit edf00b0

28 files changed

Lines changed: 2205 additions & 278 deletions

x-pack/solutions/security/plugins/entity_store/common/domain/definitions/common_fields.ts

Lines changed: 20 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
import type { EntityType, EntityField } from './entity_schema';
99
import { oldestValue, newestValue } from './field_retention_operations';
1010

11+
export const ENTITY_ID_FIELD = 'entity.id';
12+
1113
// Copied from x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/entity_store/entity_definitions/entity_descriptions/common.ts
1214

1315
export const getCommonFieldDescriptions = (
@@ -53,57 +55,57 @@ export const getEntityFieldsDescriptions = (rootField?: EntityType) => {
5355
newestValue({ source: `${prefix}.url`, destination: 'entity.url' }),
5456

5557
newestValue({
56-
source: `${prefix}.attributes.Privileged`,
57-
destination: 'entity.attributes.Privileged',
58+
source: `${prefix}.attributes.privileged`,
59+
destination: 'entity.attributes.privileged',
5860
mapping: { type: 'boolean' },
5961
allowAPIUpdate: true,
6062
}),
6163
newestValue({
62-
source: `${prefix}.attributes.Asset`,
63-
destination: 'entity.attributes.Asset',
64+
source: `${prefix}.attributes.asset`,
65+
destination: 'entity.attributes.asset',
6466
mapping: { type: 'boolean' },
6567
allowAPIUpdate: true,
6668
}),
6769
newestValue({
68-
source: `${prefix}.attributes.Managed`,
69-
destination: 'entity.attributes.Managed',
70+
source: `${prefix}.attributes.managed`,
71+
destination: 'entity.attributes.managed',
7072
mapping: { type: 'boolean' },
7173
allowAPIUpdate: true,
7274
}),
7375
newestValue({
74-
source: `${prefix}.attributes.Mfa_enabled`,
75-
destination: 'entity.attributes.Mfa_enabled',
76+
source: `${prefix}.attributes.mfa_enabled`,
77+
destination: 'entity.attributes.mfa_enabled',
7678
mapping: { type: 'boolean' },
7779
allowAPIUpdate: true,
7880
}),
7981

8082
/* Lifecycle fields should not allow update via the API */
8183
newestValue({
82-
source: `${prefix}.lifecycle.First_seen`,
83-
destination: 'entity.lifecycle.First_seen',
84+
source: `${prefix}.lifecycle.first_seen`,
85+
destination: 'entity.lifecycle.first_seen',
8486
mapping: { type: 'date' },
8587
}),
8688
newestValue({
87-
source: `${prefix}.lifecycle.Last_activity`,
88-
destination: 'entity.lifecycle.Last_activity',
89+
source: `${prefix}.lifecycle.last_activity`,
90+
destination: 'entity.lifecycle.last_activity',
8991
mapping: { type: 'date' },
9092
}),
9193

9294
newestValue({
93-
source: `${prefix}.behaviors.Brute_force_victim`,
94-
destination: 'entity.behaviors.Brute_force_victim',
95+
source: `${prefix}.behaviors.brute_force_victim`,
96+
destination: 'entity.behaviors.brute_force_victim',
9597
mapping: { type: 'boolean' },
9698
allowAPIUpdate: true,
9799
}),
98100
newestValue({
99-
source: `${prefix}.behaviors.New_country_login`,
100-
destination: 'entity.behaviors.New_country_login',
101+
source: `${prefix}.behaviors.new_country_login`,
102+
destination: 'entity.behaviors.new_country_login',
101103
mapping: { type: 'boolean' },
102104
allowAPIUpdate: true,
103105
}),
104106
newestValue({
105-
source: `${prefix}.behaviors.Used_usb_device`,
106-
destination: 'entity.behaviors.Used_usb_device',
107+
source: `${prefix}.behaviors.used_usb_device`,
108+
destination: 'entity.behaviors.used_usb_device',
107109
mapping: { type: 'boolean' },
108110
allowAPIUpdate: true,
109111
}),
Lines changed: 310 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,310 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
/*
9+
* NOTICE: Do not edit this file manually.
10+
* This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator.
11+
*
12+
* info:
13+
* title: Common Entities Schemas
14+
* version: 1
15+
*/
16+
17+
import { z } from '@kbn/zod';
18+
19+
export type EngineMetadata = z.infer<typeof EngineMetadata>;
20+
export const EngineMetadata = z
21+
.object({
22+
Type: z.string(),
23+
})
24+
.strict();
25+
26+
export type EntityRiskLevels = z.infer<typeof EntityRiskLevels>;
27+
export const EntityRiskLevels = z.enum(['Unknown', 'Low', 'Moderate', 'High', 'Critical']);
28+
export type EntityRiskLevelsEnum = typeof EntityRiskLevels.enum;
29+
export const EntityRiskLevelsEnum = EntityRiskLevels.enum;
30+
31+
export type EntityField = z.infer<typeof EntityField>;
32+
export const EntityField = z
33+
.object({
34+
id: z.string(),
35+
name: z.string().optional(),
36+
type: z.string().optional(),
37+
sub_type: z.string().optional(),
38+
source: z.string().optional(),
39+
EngineMetadata: EngineMetadata.optional(),
40+
attributes: z
41+
.object({
42+
privileged: z.boolean().optional(),
43+
asset: z.boolean().optional(),
44+
managed: z.boolean().optional(),
45+
mfa_enabled: z.boolean().optional(),
46+
})
47+
.strict()
48+
.optional(),
49+
behaviors: z
50+
.object({
51+
brute_force_victim: z.boolean().optional(),
52+
new_country_login: z.boolean().optional(),
53+
used_usb_device: z.boolean().optional(),
54+
})
55+
.strict()
56+
.optional(),
57+
lifecycle: z
58+
.object({
59+
first_seen: z.string().datetime().optional(),
60+
last_activity: z.string().datetime().optional(),
61+
})
62+
.strict()
63+
.optional(),
64+
relationships: z
65+
.object({
66+
communicates_with: z.array(z.string()).optional(),
67+
depends_on: z.array(z.string()).optional(),
68+
dependent_of: z.array(z.string()).optional(),
69+
owns: z.array(z.string()).optional(),
70+
owned_by: z.array(z.string()).optional(),
71+
accesses_frequently: z.array(z.string()).optional(),
72+
accessed_frequently_by: z.array(z.string()).optional(),
73+
supervises: z.array(z.string()).optional(),
74+
supervised_by: z.array(z.string()).optional(),
75+
})
76+
.strict()
77+
.optional(),
78+
risk: z
79+
.object({
80+
/**
81+
* Lexical description of the entity's risk.
82+
*/
83+
calculated_level: EntityRiskLevels.optional(),
84+
/**
85+
* The raw numeric value of the given entity's risk score.
86+
*/
87+
calculated_score: z.number().optional(),
88+
/**
89+
* The normalized numeric value of the given entity's risk score. Useful for comparing with other entities.
90+
*/
91+
calculated_score_norm: z.number().min(0).max(100).optional(),
92+
})
93+
.strict()
94+
.optional(),
95+
})
96+
.strict();
97+
98+
/**
99+
* The criticality level of the asset.
100+
*/
101+
export type AssetCriticalityLevel = z.infer<typeof AssetCriticalityLevel>;
102+
export const AssetCriticalityLevel = z.enum([
103+
'low_impact',
104+
'medium_impact',
105+
'high_impact',
106+
'extreme_impact',
107+
]);
108+
export type AssetCriticalityLevelEnum = typeof AssetCriticalityLevel.enum;
109+
export const AssetCriticalityLevelEnum = AssetCriticalityLevel.enum;
110+
111+
export type Asset = z.infer<typeof Asset>;
112+
export const Asset = z
113+
.object({
114+
id: z.string().optional(),
115+
name: z.string().optional(),
116+
owner: z.string().optional(),
117+
serial_number: z.string().optional(),
118+
model: z.string().optional(),
119+
vendor: z.string().optional(),
120+
environment: z.string().optional(),
121+
criticality: AssetCriticalityLevel.optional(),
122+
business_unit: z.string().optional(),
123+
})
124+
.strict();
125+
126+
/**
127+
* A generic representation of a document contributing to a Risk Score.
128+
*/
129+
export type RiskScoreInput = z.infer<typeof RiskScoreInput>;
130+
export const RiskScoreInput = z.object({
131+
/**
132+
* The unique identifier (`_id`) of the original source document
133+
*/
134+
id: z.string(),
135+
/**
136+
* The unique index (`_index`) of the original source document
137+
*/
138+
index: z.string(),
139+
/**
140+
* The risk category of the risk input document.
141+
*/
142+
category: z.string(),
143+
/**
144+
* A human-readable description of the risk input document.
145+
*/
146+
description: z.string(),
147+
/**
148+
* The weighted risk score of the risk input document.
149+
*/
150+
risk_score: z.number().min(0).max(100).optional(),
151+
/**
152+
* The @timestamp of the risk input document.
153+
*/
154+
timestamp: z.string().optional(),
155+
contribution_score: z.number().optional(),
156+
});
157+
158+
export type EntityRiskScoreRecord = z.infer<typeof EntityRiskScoreRecord>;
159+
export const EntityRiskScoreRecord = z.object({
160+
/**
161+
* The time at which the risk score was calculated.
162+
*/
163+
'@timestamp': z.string().datetime(),
164+
/**
165+
* The identifier field defining this risk score. Coupled with `id_value`, uniquely identifies the entity being scored.
166+
*/
167+
id_field: z.string(),
168+
/**
169+
* The identifier value defining this risk score. Coupled with `id_field`, uniquely identifies the entity being scored.
170+
*/
171+
id_value: z.string(),
172+
/**
173+
* Lexical description of the entity's risk.
174+
*/
175+
calculated_level: EntityRiskLevels,
176+
/**
177+
* The raw numeric value of the given entity's risk score.
178+
*/
179+
calculated_score: z.number(),
180+
/**
181+
* The normalized numeric value of the given entity's risk score. Useful for comparing with other entities.
182+
*/
183+
calculated_score_norm: z.number().min(0).max(100),
184+
/**
185+
* The contribution of Category 1 to the overall risk score (`calculated_score`). Category 1 contains Detection Engine Alerts.
186+
*/
187+
category_1_score: z.number(),
188+
/**
189+
* The number of risk input documents that contributed to the Category 1 score (`category_1_score`).
190+
*/
191+
category_1_count: z.number().int(),
192+
/**
193+
* A list of the highest-risk documents contributing to this risk score. Useful for investigative purposes.
194+
*/
195+
inputs: z.array(RiskScoreInput),
196+
category_2_score: z.number().optional(),
197+
category_2_count: z.number().int().optional(),
198+
notes: z.array(z.string()),
199+
criticality_modifier: z.number().optional(),
200+
criticality_level: AssetCriticalityLevel.optional(),
201+
/**
202+
* A list of modifiers that were applied to the risk score calculation.
203+
*/
204+
modifiers: z
205+
.array(
206+
z.object({
207+
type: z.string(),
208+
subtype: z.string().optional(),
209+
modifier_value: z.number().optional(),
210+
contribution: z.number(),
211+
metadata: z.object({}).catchall(z.unknown()).optional(),
212+
})
213+
)
214+
.optional(),
215+
});
216+
217+
export type UserEntity = z.infer<typeof UserEntity>;
218+
export const UserEntity = z
219+
.object({
220+
'@timestamp': z.string().datetime().optional(),
221+
entity: EntityField,
222+
user: z
223+
.object({
224+
full_name: z.array(z.string()).optional(),
225+
domain: z.array(z.string()).optional(),
226+
roles: z.array(z.string()).optional(),
227+
name: z.string(),
228+
id: z.array(z.string()).optional(),
229+
email: z.array(z.string()).optional(),
230+
hash: z.array(z.string()).optional(),
231+
risk: EntityRiskScoreRecord.optional(),
232+
})
233+
.strict()
234+
.optional(),
235+
asset: Asset.optional(),
236+
event: z
237+
.object({
238+
ingested: z.string().datetime().optional(),
239+
})
240+
.strict()
241+
.optional(),
242+
})
243+
.strict();
244+
245+
export type HostEntity = z.infer<typeof HostEntity>;
246+
export const HostEntity = z
247+
.object({
248+
'@timestamp': z.string().datetime().optional(),
249+
entity: EntityField,
250+
host: z
251+
.object({
252+
hostname: z.array(z.string()).optional(),
253+
domain: z.array(z.string()).optional(),
254+
ip: z.array(z.string()).optional(),
255+
name: z.string(),
256+
id: z.array(z.string()).optional(),
257+
type: z.array(z.string()).optional(),
258+
mac: z.array(z.string()).optional(),
259+
architecture: z.array(z.string()).optional(),
260+
risk: EntityRiskScoreRecord.optional(),
261+
entity: EntityField.optional(),
262+
})
263+
.strict()
264+
.optional(),
265+
asset: Asset.optional(),
266+
event: z
267+
.object({
268+
ingested: z.string().datetime().optional(),
269+
})
270+
.strict()
271+
.optional(),
272+
})
273+
.strict();
274+
275+
export type ServiceEntity = z.infer<typeof ServiceEntity>;
276+
export const ServiceEntity = z
277+
.object({
278+
'@timestamp': z.string().datetime().optional(),
279+
entity: EntityField,
280+
service: z
281+
.object({
282+
name: z.string(),
283+
risk: EntityRiskScoreRecord.optional(),
284+
entity: EntityField.optional(),
285+
})
286+
.strict()
287+
.optional(),
288+
asset: Asset.optional(),
289+
event: z
290+
.object({
291+
ingested: z.string().datetime().optional(),
292+
})
293+
.strict()
294+
.optional(),
295+
})
296+
.strict();
297+
298+
export type GenericEntity = z.infer<typeof GenericEntity>;
299+
export const GenericEntity = z
300+
.object({
301+
'@timestamp': z.string().datetime().optional(),
302+
entity: EntityField,
303+
asset: Asset.optional(),
304+
})
305+
.strict();
306+
307+
export const EntityInternal = z.union([UserEntity, HostEntity, ServiceEntity, GenericEntity]);
308+
309+
export type Entity = z.infer<typeof EntityInternal>;
310+
export const Entity = EntityInternal as z.ZodType<Entity>;

0 commit comments

Comments
 (0)