forked from elastic/kibana
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathfeature.ts
More file actions
166 lines (145 loc) · 5.66 KB
/
Copy pathfeature.ts
File metadata and controls
166 lines (145 loc) · 5.66 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { z } from '@kbn/zod/v4';
import { isEqual, uniq } from 'lodash';
import { conditionSchema, type Condition } from '@kbn/streamlang';
export const DATASET_ANALYSIS_FEATURE_TYPE = 'dataset_analysis' as const;
export const LOG_SAMPLES_FEATURE_TYPE = 'log_samples' as const;
export const LOG_PATTERNS_FEATURE_TYPE = 'log_patterns' as const;
export const ERROR_LOGS_FEATURE_TYPE = 'error_logs' as const;
export const COMPUTED_FEATURE_TYPES = [
DATASET_ANALYSIS_FEATURE_TYPE,
LOG_SAMPLES_FEATURE_TYPE,
LOG_PATTERNS_FEATURE_TYPE,
ERROR_LOGS_FEATURE_TYPE,
] as const;
export const INFERRED_FEATURE_TYPES = [
'entity',
'infrastructure',
'technology',
'dependency',
'schema',
] as const;
export const baseFeatureSchema = z.object({
id: z.string(),
stream_name: z.string(),
type: z.string(),
subtype: z.string().optional(),
title: z.string().optional(),
description: z.string(),
properties: z.record(z.string(), z.unknown()),
confidence: z.number().min(0).max(100),
evidence: z.array(z.string()).optional(),
evidence_doc_ids: z.array(z.string()).optional(),
tags: z.array(z.string()).optional(),
filter: conditionSchema.optional(),
meta: z.record(z.string(), z.unknown()).optional(),
});
export type BaseFeature = z.infer<typeof baseFeatureSchema>;
// Stricter schema for LLM-identified features — makes subtype, title, evidence, tags required
export const identifiedFeatureSchema = baseFeatureSchema
.omit({ subtype: true, title: true, evidence: true, tags: true })
.and(
z.object({
subtype: z.string(),
title: z.string(),
evidence: z.array(z.string()),
tags: z.array(z.string()),
})
);
export type IdentifiedFeature = z.infer<typeof identifiedFeatureSchema>;
export const ignoredFeatureSchema = z.object({
feature_id: z.string(),
feature_title: z.string(),
excluded_feature_id: z.string(),
reason: z.string(),
});
export type IgnoredFeature = z.infer<typeof ignoredFeatureSchema>;
/**
* Server-side feature shape on the unified knowledge indicators data stream.
*
* Note: as part of the unified KI data stream migration, the legacy
* `uuid`, `status`, `last_seen`, and `expires_at` fields have been removed.
* Identity is now `(stream.name, type, id)` and revisions are append-only.
* `updated_at` is read-only at the domain layer — it is derived from the
* latest revision's `@timestamp` when reading and is not a property of the
* write payload (`BaseFeature`).
*
* `excluded` is a root-level marker on the storage doc surfaced here so the
* UI can split active vs. excluded features when the caller opts into
* `include_excluded=true`. Soft deletes use tombstone revisions (`deleted:
* true` on identity-only docs), not this field.
*/
export const featureSchema = baseFeatureSchema.and(
z.object({
run_id: z.string().optional(),
excluded: z.boolean().optional(),
updated_at: z.string().optional(),
})
);
export type Feature = z.infer<typeof featureSchema>;
export type FeatureWithFilter = Feature & { filter: Condition };
export function isFeature(feature: unknown): feature is Feature {
return featureSchema.safeParse(feature).success;
}
export function isFeatureWithFilter(feature: unknown): feature is FeatureWithFilter {
const result = featureSchema.safeParse(feature);
return result.success && Boolean(result.data.filter);
}
export function isComputedFeature(feature: BaseFeature): boolean {
return (COMPUTED_FEATURE_TYPES as unknown as string[]).includes(feature.type);
}
export function hasSameFingerprint(feature: BaseFeature, other: BaseFeature): boolean {
return (
feature.type === other.type &&
feature.subtype === other.subtype &&
isEqual(feature.properties, other.properties)
);
}
export function isDuplicateFeature(feature: BaseFeature, other: BaseFeature): boolean {
return feature.id.toLowerCase() === other.id.toLowerCase() || hasSameFingerprint(feature, other);
}
const mergeArrays = (a: string[] | undefined, b: string[] | undefined): string[] | undefined => {
const merged = uniq([...(a ?? []), ...(b ?? [])]);
return merged.length > 0 ? merged : undefined;
};
export function toBaseFeature(feature: Feature): BaseFeature {
return {
id: feature.id,
stream_name: feature.stream_name,
type: feature.type,
subtype: feature.subtype,
title: feature.title,
description: feature.description,
properties: feature.properties,
confidence: feature.confidence,
evidence: feature.evidence,
evidence_doc_ids: feature.evidence_doc_ids,
tags: feature.tags,
filter: feature.filter,
meta: feature.meta,
};
}
export function mergeFeature(existing: BaseFeature, incoming: BaseFeature): BaseFeature {
const mergedMeta = { ...(existing.meta ?? {}), ...(incoming.meta ?? {}) };
const mergedProperties = { ...(existing.properties ?? {}), ...(incoming.properties ?? {}) };
return {
id: existing.id,
stream_name: existing.stream_name,
type: existing.type,
subtype: existing.subtype,
title: incoming.title,
description: incoming.description,
properties: mergedProperties,
confidence: Math.round((existing.confidence + incoming.confidence) / 2),
evidence: mergeArrays(existing.evidence, incoming.evidence),
evidence_doc_ids: mergeArrays(existing.evidence_doc_ids, incoming.evidence_doc_ids),
tags: mergeArrays(existing.tags, incoming.tags),
filter: incoming.filter ?? existing.filter,
meta: Object.keys(mergedMeta).length > 0 ? mergedMeta : undefined,
};
}