@oneuptime/common 12.0.28 → 12.0.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (182) hide show
  1. package/Models/DatabaseModels/DashboardDomain.ts +43 -0
  2. package/Models/DatabaseModels/MonitorTemplate.ts +15 -3
  3. package/Models/DatabaseModels/StatusPageDomain.ts +43 -0
  4. package/Server/API/DashboardDomainAPI.ts +74 -0
  5. package/Server/API/MicrosoftTeamsAPI.ts +81 -180
  6. package/Server/API/StatusPageDomainAPI.ts +74 -0
  7. package/Server/Infrastructure/Postgres/SchemaMigrations/1790200000000-AllowNullMonitorNameOnMonitorTemplate.ts +43 -0
  8. package/Server/Infrastructure/Postgres/SchemaMigrations/1790300000000-AddCertificateReissueRequestedAtToDomains.ts +40 -0
  9. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
  10. package/Server/Middleware/ProjectAuthorization.ts +6 -1
  11. package/Server/Services/DashboardDomainService.ts +131 -0
  12. package/Server/Services/LogPipelineProcessorService.ts +88 -0
  13. package/Server/Services/MonitorTemplateService.ts +70 -11
  14. package/Server/Services/StatusPageDomainService.ts +163 -3
  15. package/Server/Services/StatusPagePrivateUserSessionService.ts +197 -56
  16. package/Server/Types/Domain.ts +17 -0
  17. package/Server/Utils/APIKey/AccessPermission.ts +24 -0
  18. package/Server/Utils/Greenlock/Greenlock.ts +156 -58
  19. package/Server/Utils/LogPipelineProcessorValidation.ts +112 -0
  20. package/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsClient.ts +1094 -45
  21. package/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsPoller.ts +153 -17
  22. package/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.ts +23 -11
  23. package/Server/Utils/Workspace/MicrosoftTeams/Actions/Auth.ts +0 -170
  24. package/Server/Utils/Workspace/MicrosoftTeams/Actions/Authorization.ts +158 -0
  25. package/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.ts +23 -11
  26. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +35 -13
  27. package/Tests/App/Dashboard/MonitorTemplateColumnAndFacet.test.tsx +505 -0
  28. package/Tests/App/StatusPage/StatusPageLoginCodeBootstrap.test.ts +106 -0
  29. package/Tests/App/StatusPage/StatusPageLoginCodeUtil.test.ts +84 -0
  30. package/Tests/App/StatusPage/StatusPageRedirectSafety.test.ts +119 -0
  31. package/Tests/Models/MonitorTemplateMonitorNameColumn.test.ts +258 -0
  32. package/Tests/Server/API/CustomDomainReissueSslAPI.test.ts +469 -0
  33. package/Tests/Server/API/MicrosoftTeamsBotMessagesEndpoint.test.ts +729 -0
  34. package/Tests/Server/API/MicrosoftTeamsBotTestEndpoint.test.ts +26 -0
  35. package/Tests/Server/API/MicrosoftTeamsLegacyWebhookRemoval.test.ts +402 -0
  36. package/Tests/Server/Middleware/ProjectAuthorizationApiKeyMiddleware.test.ts +4 -0
  37. package/Tests/Server/Services/AddCertificateReissueRequestedAtToDomainsMigration.test.ts +287 -0
  38. package/Tests/Server/Services/CustomDomainCertificateReissue.test.ts +477 -0
  39. package/Tests/Server/Services/LogPipelineProcessorSaveValidation.test.ts +249 -0
  40. package/Tests/Server/Services/MonitorTemplateServiceBulkSync.test.ts +549 -0
  41. package/Tests/Server/Services/MonitorTemplateServiceNetworkDeviceSync.test.ts +16 -3
  42. package/Tests/Server/Services/NetworkDeviceAutoImportRuleEngineService.test.ts +57 -0
  43. package/Tests/Server/Services/SecurityEventLastErrorColumnWidth.test.ts +151 -15
  44. package/Tests/Server/Services/StatusPagePrivateUserSessionService.test.ts +587 -0
  45. package/Tests/Server/Types/Database/Permissions/ApiKeyFileAccess.test.ts +286 -0
  46. package/Tests/Server/Types/Database/Permissions/MonitorTemplateColumnAccess.test.ts +328 -0
  47. package/Tests/Server/Types/Database/QueryHelperFindWithSameTextAndIsNull.test.ts +83 -0
  48. package/Tests/Server/Utils/AI/SRE/ConfidenceSignal.test.ts +377 -0
  49. package/Tests/Server/Utils/APIKey/AccessPermission.test.ts +16 -1
  50. package/Tests/Server/Utils/Greenlock/CertificateRenewalSchedule.test.ts +250 -0
  51. package/Tests/Server/Utils/LogPipelineProcessorValidation.test.ts +198 -0
  52. package/Tests/Server/Utils/SecurityEvent/ConnectorErrorMessage.test.ts +48 -9
  53. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsAuth.test.ts +726 -0
  54. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsClient.test.ts +74 -11
  55. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsErrorHandling.test.ts +1107 -0
  56. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsPoller.test.ts +20 -3
  57. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsPollerFailureTaxonomy.test.ts +51 -11
  58. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsPollerHardening.test.ts +1246 -0
  59. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsRequestContract.test.ts +961 -0
  60. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsResponseParsing.test.ts +653 -0
  61. package/Tests/Server/Utils/Workspace/MicrosoftTeamsActionAuthorization.test.ts +880 -0
  62. package/Tests/Types/API/HTTPResponse.test.ts +175 -0
  63. package/Tests/Types/API/Route.test.ts +36 -4
  64. package/Tests/Types/Database/DatabaseProperty.test.ts +171 -0
  65. package/Tests/Types/DateLocalShortDateTimeString.test.ts +518 -0
  66. package/Tests/Types/DateLocalTimeString.test.ts +259 -0
  67. package/Tests/Types/DateUserPrefers12HourFormat.test.ts +316 -0
  68. package/Tests/UI/Components/LogsHistogram.test.tsx +41 -2
  69. package/Tests/UI/Components/LogsViewerEmptyStateTimeRange.test.tsx +212 -0
  70. package/Tests/UI/Components/TelemetryChartClockFormat.test.tsx +359 -0
  71. package/Tests/UI/Components/TimeRangePickerClockFormat.test.tsx +422 -0
  72. package/Tests/UI/Components/TimeRangePickerDropdown.test.tsx +146 -8
  73. package/Tests/UI/MicrosoftTeams/MicrosoftTeamsMessagingEndpointGuidance.test.tsx +520 -0
  74. package/Tests/UI/Utils/Navigation.test.ts +82 -0
  75. package/Tests/Utils/Array.test.ts +100 -0
  76. package/Tests/Utils/CertificateReissue.test.ts +225 -0
  77. package/Tests/Utils/Grok/Grok.test.ts +492 -0
  78. package/Tests/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.test.ts +181 -13
  79. package/Types/API/Route.ts +20 -5
  80. package/Types/Date.ts +143 -13
  81. package/Types/Rum/SessionReplay.ts +62 -1
  82. package/UI/Components/Date/TimeRangePickerDropdown.tsx +9 -5
  83. package/UI/Components/LogsViewer/LogsViewer.tsx +12 -7
  84. package/UI/Components/LogsViewer/components/HistogramTooltip.tsx +18 -17
  85. package/UI/Components/LogsViewer/components/LogsAnalyticsView.tsx +21 -28
  86. package/UI/Components/LogsViewer/components/LogsHistogram.tsx +2 -4
  87. package/UI/Components/TelemetryViewer/components/TelemetryHistogram.tsx +2 -4
  88. package/UI/Components/TelemetryViewer/components/TelemetryHistogramTooltip.tsx +17 -16
  89. package/UI/Utils/Navigation.ts +42 -0
  90. package/Utils/Array.ts +37 -0
  91. package/Utils/CertificateReissue.ts +128 -0
  92. package/Utils/Grok/Grok.ts +461 -0
  93. package/Utils/Grok/GrokPatterns.ts +118 -0
  94. package/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.ts +58 -6
  95. package/build/dist/Models/DatabaseModels/DashboardDomain.js +44 -0
  96. package/build/dist/Models/DatabaseModels/DashboardDomain.js.map +1 -1
  97. package/build/dist/Models/DatabaseModels/MonitorTemplate.js +17 -4
  98. package/build/dist/Models/DatabaseModels/MonitorTemplate.js.map +1 -1
  99. package/build/dist/Models/DatabaseModels/StatusPageDomain.js +44 -0
  100. package/build/dist/Models/DatabaseModels/StatusPageDomain.js.map +1 -1
  101. package/build/dist/Server/API/DashboardDomainAPI.js +43 -1
  102. package/build/dist/Server/API/DashboardDomainAPI.js.map +1 -1
  103. package/build/dist/Server/API/MicrosoftTeamsAPI.js +74 -118
  104. package/build/dist/Server/API/MicrosoftTeamsAPI.js.map +1 -1
  105. package/build/dist/Server/API/StatusPageDomainAPI.js +43 -1
  106. package/build/dist/Server/API/StatusPageDomainAPI.js.map +1 -1
  107. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790200000000-AllowNullMonitorNameOnMonitorTemplate.js +34 -0
  108. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790200000000-AllowNullMonitorNameOnMonitorTemplate.js.map +1 -0
  109. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790300000000-AddCertificateReissueRequestedAtToDomains.js +28 -0
  110. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790300000000-AddCertificateReissueRequestedAtToDomains.js.map +1 -0
  111. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
  112. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  113. package/build/dist/Server/Middleware/ProjectAuthorization.js +6 -1
  114. package/build/dist/Server/Middleware/ProjectAuthorization.js.map +1 -1
  115. package/build/dist/Server/Services/DashboardDomainService.js +104 -0
  116. package/build/dist/Server/Services/DashboardDomainService.js.map +1 -1
  117. package/build/dist/Server/Services/LogPipelineProcessorService.js +65 -0
  118. package/build/dist/Server/Services/LogPipelineProcessorService.js.map +1 -1
  119. package/build/dist/Server/Services/MonitorTemplateService.js +57 -11
  120. package/build/dist/Server/Services/MonitorTemplateService.js.map +1 -1
  121. package/build/dist/Server/Services/StatusPageDomainService.js +132 -3
  122. package/build/dist/Server/Services/StatusPageDomainService.js.map +1 -1
  123. package/build/dist/Server/Services/StatusPagePrivateUserSessionService.js +112 -32
  124. package/build/dist/Server/Services/StatusPagePrivateUserSessionService.js.map +1 -1
  125. package/build/dist/Server/Types/Domain.js +16 -0
  126. package/build/dist/Server/Types/Domain.js.map +1 -1
  127. package/build/dist/Server/Utils/APIKey/AccessPermission.js +24 -0
  128. package/build/dist/Server/Utils/APIKey/AccessPermission.js.map +1 -1
  129. package/build/dist/Server/Utils/Greenlock/Greenlock.js +110 -35
  130. package/build/dist/Server/Utils/Greenlock/Greenlock.js.map +1 -1
  131. package/build/dist/Server/Utils/LogPipelineProcessorValidation.js +77 -0
  132. package/build/dist/Server/Utils/LogPipelineProcessorValidation.js.map +1 -0
  133. package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsClient.js +777 -36
  134. package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsClient.js.map +1 -1
  135. package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsPoller.js +108 -17
  136. package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsPoller.js.map +1 -1
  137. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.js +17 -6
  138. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.js.map +1 -1
  139. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Auth.js +0 -159
  140. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Auth.js.map +1 -1
  141. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Authorization.js +128 -0
  142. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Authorization.js.map +1 -0
  143. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.js +17 -6
  144. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.js.map +1 -1
  145. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +25 -13
  146. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
  147. package/build/dist/Types/API/Route.js +16 -5
  148. package/build/dist/Types/API/Route.js.map +1 -1
  149. package/build/dist/Types/Date.js +115 -13
  150. package/build/dist/Types/Date.js.map +1 -1
  151. package/build/dist/Types/Rum/SessionReplay.js +38 -0
  152. package/build/dist/Types/Rum/SessionReplay.js.map +1 -1
  153. package/build/dist/UI/Components/Date/TimeRangePickerDropdown.js +9 -5
  154. package/build/dist/UI/Components/Date/TimeRangePickerDropdown.js.map +1 -1
  155. package/build/dist/UI/Components/LogsViewer/LogsViewer.js +12 -7
  156. package/build/dist/UI/Components/LogsViewer/LogsViewer.js.map +1 -1
  157. package/build/dist/UI/Components/LogsViewer/components/HistogramTooltip.js +15 -14
  158. package/build/dist/UI/Components/LogsViewer/components/HistogramTooltip.js.map +1 -1
  159. package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js +17 -22
  160. package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js.map +1 -1
  161. package/build/dist/UI/Components/LogsViewer/components/LogsHistogram.js +2 -4
  162. package/build/dist/UI/Components/LogsViewer/components/LogsHistogram.js.map +1 -1
  163. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js +2 -4
  164. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js.map +1 -1
  165. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogramTooltip.js +15 -14
  166. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogramTooltip.js.map +1 -1
  167. package/build/dist/UI/Utils/Navigation.js +30 -0
  168. package/build/dist/UI/Utils/Navigation.js.map +1 -1
  169. package/build/dist/Utils/Array.js +25 -0
  170. package/build/dist/Utils/Array.js.map +1 -1
  171. package/build/dist/Utils/CertificateReissue.js +97 -0
  172. package/build/dist/Utils/CertificateReissue.js.map +1 -0
  173. package/build/dist/Utils/Grok/Grok.js +324 -0
  174. package/build/dist/Utils/Grok/Grok.js.map +1 -0
  175. package/build/dist/Utils/Grok/GrokPatterns.js +112 -0
  176. package/build/dist/Utils/Grok/GrokPatterns.js.map +1 -0
  177. package/build/dist/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.js +63 -3
  178. package/build/dist/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.js.map +1 -1
  179. package/package.json +1 -1
  180. package/UI/Utils/JsonWebToken.ts +0 -14
  181. package/build/dist/UI/Utils/JsonWebToken.js +0 -10
  182. package/build/dist/UI/Utils/JsonWebToken.js.map +0 -1
@@ -0,0 +1,461 @@
1
+ import BadDataException from "../../Types/Exception/BadDataException";
2
+ import GrokPatterns from "./GrokPatterns";
3
+
4
+ /*
5
+ * Grok engine.
6
+ *
7
+ * A grok pattern is a regex with named references into a pattern
8
+ * library: `%{IPV4:client_ip} - %{WORD:verb}` means "the IPv4 regex,
9
+ * captured as client_ip, then a literal ' - ', then a word captured as
10
+ * verb". Anything that is not a `%{...}` reference is passed through to
11
+ * the regex verbatim, so raw regex and grok references can be mixed.
12
+ *
13
+ * Supported reference forms:
14
+ *
15
+ * %{NAME} match, capture nothing
16
+ * %{NAME:field} capture into `field` as a string
17
+ * %{NAME:field:type} capture into `field`, coerced (int/float/boolean)
18
+ *
19
+ * Compilation expands references recursively into one JavaScript RegExp
20
+ * and records, for each captured field, the internal group name that
21
+ * holds it. Only the groups this engine injects capture — every library
22
+ * definition uses `(?:...)` — so extraction is a `match.groups` lookup
23
+ * with no index arithmetic.
24
+ *
25
+ * Matching is deliberately UNANCHORED, like Logstash: a pattern
26
+ * describes a fragment of the line unless the author anchors it with
27
+ * `^`/`$` themselves.
28
+ *
29
+ * This runs once per ingested log record on the telemetry hot path
30
+ * against text the customer's users control, so the compiler enforces
31
+ * hard ceilings on expansion (depth, count, final source length) and
32
+ * the matcher refuses inputs above MAX_GROK_INPUT_LENGTH rather than
33
+ * handing an unbounded string to a backtracking engine.
34
+ */
35
+
36
+ export enum GrokFieldType {
37
+ String = "string",
38
+ Integer = "integer",
39
+ Float = "float",
40
+ Boolean = "boolean",
41
+ }
42
+
43
+ export type GrokValue = string | number | boolean;
44
+
45
+ export interface GrokCapture {
46
+ /** Internal RegExp group name, e.g. "oug0". Never surfaced to users. */
47
+ groupName: string;
48
+ /** Attribute name the user asked for, e.g. "client_ip". */
49
+ fieldName: string;
50
+ fieldType: GrokFieldType;
51
+ }
52
+
53
+ export interface CompiledGrokPattern {
54
+ regex: RegExp;
55
+ captures: Array<GrokCapture>;
56
+ /** Expanded regex source. Useful for debugging and for the UI tester. */
57
+ source: string;
58
+ }
59
+
60
+ /** Longest raw pattern a user may save. */
61
+ export const MAX_GROK_PATTERN_LENGTH: number = 4000;
62
+
63
+ /** Longest expanded regex source a pattern may compile to. */
64
+ export const MAX_GROK_SOURCE_LENGTH: number = 20000;
65
+
66
+ /** How deep `%{A}` -> `%{B}` -> `%{C}` nesting may go. */
67
+ export const MAX_GROK_EXPANSION_DEPTH: number = 20;
68
+
69
+ /** Total number of references expanded while compiling one pattern. */
70
+ export const MAX_GROK_EXPANSIONS: number = 500;
71
+
72
+ /** Fields one pattern may capture. */
73
+ export const MAX_GROK_CAPTURES: number = 100;
74
+
75
+ /*
76
+ * Inputs longer than this are not parsed. A backtracking regex cannot be
77
+ * interrupted once started, so the only bound available on the ingest
78
+ * hot path is the length of what we feed it.
79
+ */
80
+ export const MAX_GROK_INPUT_LENGTH: number = 32768;
81
+
82
+ /*
83
+ * `%{NAME}`, `%{NAME:field}` or `%{NAME:field:type}`. The field and type
84
+ * segments exclude `{`, `}` and `:` so a malformed reference fails to
85
+ * match here and is reported as unparsable rather than being silently
86
+ * split at the wrong colon.
87
+ */
88
+ const GROK_REFERENCE_SOURCE: string =
89
+ "%\\{([A-Za-z0-9_]+)(?::([^:{}]*))?(?::([^:{}]*))?\\}";
90
+
91
+ /*
92
+ * Field names become log attribute keys. Dots are allowed because OTel
93
+ * semantic-convention keys use them (`http.request.method`).
94
+ */
95
+ const FIELD_NAME_REGEX: RegExp = /^[A-Za-z_][A-Za-z0-9_.@-]*$/;
96
+
97
+ interface CompileContext {
98
+ patterns: Record<string, string>;
99
+ captures: Array<GrokCapture>;
100
+ /** Names currently being expanded — used to reject circular references. */
101
+ stack: Array<string>;
102
+ expansions: number;
103
+ }
104
+
105
+ function parseFieldType(rawType: string | undefined): GrokFieldType {
106
+ if (!rawType) {
107
+ return GrokFieldType.String;
108
+ }
109
+
110
+ switch (rawType.trim().toLowerCase()) {
111
+ case "int":
112
+ case "integer":
113
+ case "long":
114
+ return GrokFieldType.Integer;
115
+ case "float":
116
+ case "double":
117
+ case "number":
118
+ return GrokFieldType.Float;
119
+ case "bool":
120
+ case "boolean":
121
+ return GrokFieldType.Boolean;
122
+ case "string":
123
+ case "text":
124
+ return GrokFieldType.String;
125
+ default:
126
+ throw new BadDataException(
127
+ `Unknown grok field type "${rawType}". Supported types are: int, long, float, double, boolean, string.`,
128
+ );
129
+ }
130
+ }
131
+
132
+ function resolveReference(
133
+ match: RegExpExecArray,
134
+ context: CompileContext,
135
+ ): string {
136
+ const patternName: string = match[1] as string;
137
+ const rawFieldName: string | undefined = match[2];
138
+ const rawFieldType: string | undefined = match[3];
139
+
140
+ context.expansions++;
141
+
142
+ if (context.expansions > MAX_GROK_EXPANSIONS) {
143
+ throw new BadDataException(
144
+ `This grok pattern expands to more than ${MAX_GROK_EXPANSIONS} sub-patterns. Please simplify it.`,
145
+ );
146
+ }
147
+
148
+ const definition: string | undefined = context.patterns[patternName];
149
+
150
+ if (definition === undefined) {
151
+ throw new BadDataException(
152
+ `Unknown grok pattern "%{${patternName}}". Check the list of supported patterns.`,
153
+ );
154
+ }
155
+
156
+ if (context.stack.includes(patternName)) {
157
+ throw new BadDataException(
158
+ `Grok pattern "%{${patternName}}" refers to itself. Circular pattern references are not supported.`,
159
+ );
160
+ }
161
+
162
+ if (context.stack.length >= MAX_GROK_EXPANSION_DEPTH) {
163
+ throw new BadDataException(
164
+ `Grok pattern "%{${patternName}}" nests more than ${MAX_GROK_EXPANSION_DEPTH} levels deep. Please simplify it.`,
165
+ );
166
+ }
167
+
168
+ context.stack.push(patternName);
169
+ const expanded: string = expandPattern(definition, context);
170
+ context.stack.pop();
171
+
172
+ if (rawFieldName === undefined) {
173
+ return `(?:${expanded})`;
174
+ }
175
+
176
+ const fieldName: string = rawFieldName.trim();
177
+
178
+ if (!FIELD_NAME_REGEX.test(fieldName)) {
179
+ throw new BadDataException(
180
+ `"${rawFieldName}" is not a valid grok field name. Use letters, digits, and . _ - @ (starting with a letter or underscore).`,
181
+ );
182
+ }
183
+
184
+ if (context.captures.length >= MAX_GROK_CAPTURES) {
185
+ throw new BadDataException(
186
+ `This grok pattern captures more than ${MAX_GROK_CAPTURES} fields. Please capture fewer fields.`,
187
+ );
188
+ }
189
+
190
+ const groupName: string = `oug${context.captures.length}`;
191
+
192
+ context.captures.push({
193
+ groupName: groupName,
194
+ fieldName: fieldName,
195
+ fieldType: parseFieldType(rawFieldType),
196
+ });
197
+
198
+ return `(?<${groupName}>${expanded})`;
199
+ }
200
+
201
+ function expandPattern(source: string, context: CompileContext): string {
202
+ /*
203
+ * A fresh RegExp per call: `lastIndex` is per-object state and
204
+ * expansion recurses, so a shared instance would have nested calls
205
+ * stepping on each other's scan position.
206
+ */
207
+ const referenceRegex: RegExp = new RegExp(GROK_REFERENCE_SOURCE, "g");
208
+
209
+ let output: string = "";
210
+ let cursor: number = 0;
211
+ let match: RegExpExecArray | null = referenceRegex.exec(source);
212
+
213
+ while (match !== null) {
214
+ output += source.slice(cursor, match.index);
215
+ output += resolveReference(match, context);
216
+ cursor = match.index + match[0].length;
217
+
218
+ if (output.length > MAX_GROK_SOURCE_LENGTH) {
219
+ throw new BadDataException(
220
+ `This grok pattern expands to more than ${MAX_GROK_SOURCE_LENGTH} characters of regex. Please simplify it.`,
221
+ );
222
+ }
223
+
224
+ match = referenceRegex.exec(source);
225
+ }
226
+
227
+ output += source.slice(cursor);
228
+
229
+ return output;
230
+ }
231
+
232
+ /*
233
+ * Compile a grok pattern. Throws BadDataException with a message meant
234
+ * for the person who typed the pattern — this is what save-time
235
+ * validation and the pattern tester in the dashboard show.
236
+ */
237
+ export function compileGrokPattern(
238
+ pattern: string,
239
+ extraPatterns?: Record<string, string> | undefined,
240
+ ): CompiledGrokPattern {
241
+ if (typeof pattern !== "string" || pattern.trim().length === 0) {
242
+ throw new BadDataException("Grok pattern cannot be empty.");
243
+ }
244
+
245
+ if (pattern.length > MAX_GROK_PATTERN_LENGTH) {
246
+ throw new BadDataException(
247
+ `Grok pattern cannot be longer than ${MAX_GROK_PATTERN_LENGTH} characters.`,
248
+ );
249
+ }
250
+
251
+ const context: CompileContext = {
252
+ patterns: extraPatterns
253
+ ? { ...GrokPatterns, ...extraPatterns }
254
+ : GrokPatterns,
255
+ captures: [],
256
+ stack: [],
257
+ expansions: 0,
258
+ };
259
+
260
+ const source: string = expandPattern(pattern, context);
261
+
262
+ if (source.length > MAX_GROK_SOURCE_LENGTH) {
263
+ throw new BadDataException(
264
+ `This grok pattern expands to more than ${MAX_GROK_SOURCE_LENGTH} characters of regex. Please simplify it.`,
265
+ );
266
+ }
267
+
268
+ let regex: RegExp;
269
+
270
+ try {
271
+ /*
272
+ * No `g`/`y` flag on purpose: those carry `lastIndex` between calls
273
+ * and a compiled pattern is reused for every record.
274
+ */
275
+ regex = new RegExp(source);
276
+ } catch (err) {
277
+ throw new BadDataException(
278
+ `This grok pattern is not valid: ${err instanceof Error ? err.message : String(err)}`,
279
+ );
280
+ }
281
+
282
+ return {
283
+ regex: regex,
284
+ captures: context.captures,
285
+ source: source,
286
+ };
287
+ }
288
+
289
+ function coerceValue(raw: string, fieldType: GrokFieldType): GrokValue {
290
+ if (fieldType === GrokFieldType.Integer) {
291
+ const parsed: number = parseInt(raw, 10);
292
+ return Number.isFinite(parsed) ? parsed : raw;
293
+ }
294
+
295
+ if (fieldType === GrokFieldType.Float) {
296
+ const parsed: number = parseFloat(raw);
297
+ return Number.isFinite(parsed) ? parsed : raw;
298
+ }
299
+
300
+ if (fieldType === GrokFieldType.Boolean) {
301
+ const normalized: string = raw.trim().toLowerCase();
302
+
303
+ if (
304
+ normalized === "true" ||
305
+ normalized === "yes" ||
306
+ normalized === "y" ||
307
+ normalized === "1"
308
+ ) {
309
+ return true;
310
+ }
311
+
312
+ if (
313
+ normalized === "false" ||
314
+ normalized === "no" ||
315
+ normalized === "n" ||
316
+ normalized === "0"
317
+ ) {
318
+ return false;
319
+ }
320
+
321
+ /*
322
+ * Not recognisably a boolean — keep the raw text rather than
323
+ * inventing a `false` the log never said.
324
+ */
325
+ return raw;
326
+ }
327
+
328
+ return raw;
329
+ }
330
+
331
+ /*
332
+ * Run a compiled pattern against one string. Returns null when the
333
+ * pattern does not match; returns the captured fields otherwise (which
334
+ * is an empty object for a pattern that matches but captures nothing).
335
+ *
336
+ * Empty captures are dropped, matching Logstash's default
337
+ * `keep_empty_captures => false`: an optional group that did not
338
+ * participate should not write a blank attribute onto the log.
339
+ */
340
+ export function matchGrokPattern(
341
+ compiled: CompiledGrokPattern,
342
+ input: string,
343
+ ): Record<string, GrokValue> | null {
344
+ if (typeof input !== "string" || input.length === 0) {
345
+ return null;
346
+ }
347
+
348
+ if (input.length > MAX_GROK_INPUT_LENGTH) {
349
+ return null;
350
+ }
351
+
352
+ const match: RegExpExecArray | null = compiled.regex.exec(input);
353
+
354
+ if (!match) {
355
+ return null;
356
+ }
357
+
358
+ const groups: Record<string, string | undefined> = match.groups || {};
359
+ const extracted: Record<string, GrokValue> = {};
360
+
361
+ for (const capture of compiled.captures) {
362
+ const raw: string | undefined = groups[capture.groupName];
363
+
364
+ if (raw === undefined || raw === "") {
365
+ continue;
366
+ }
367
+
368
+ /*
369
+ * The same field name can legitimately appear twice (alternation
370
+ * branches, or a library pattern that already names it). Whichever
371
+ * group actually matched first wins, so a later branch that did not
372
+ * participate cannot blank out a real value.
373
+ */
374
+ if (extracted[capture.fieldName] !== undefined) {
375
+ continue;
376
+ }
377
+
378
+ extracted[capture.fieldName] = coerceValue(raw, capture.fieldType);
379
+ }
380
+
381
+ return extracted;
382
+ }
383
+
384
+ /*
385
+ * Process-wide compile cache for the ingest path.
386
+ *
387
+ * Compiling walks the pattern library and builds a RegExp; doing that
388
+ * per log record would dominate the cost of the processor. Compiled
389
+ * patterns are immutable and carry no `lastIndex` state (no `g` flag),
390
+ * so one instance is safely shared by every project using the same
391
+ * pattern text.
392
+ *
393
+ * Failures are cached too. An invalid pattern that reaches ingest would
394
+ * otherwise be recompiled — and rejected — once per record.
395
+ */
396
+ const compileCache: Map<string, CompiledGrokPattern | BadDataException> =
397
+ new Map<string, CompiledGrokPattern | BadDataException>();
398
+
399
+ export const MAX_CACHED_GROK_PATTERNS: number = 500;
400
+
401
+ export function compileGrokPatternCached(pattern: string): CompiledGrokPattern {
402
+ const cached: CompiledGrokPattern | BadDataException | undefined =
403
+ compileCache.get(pattern);
404
+
405
+ if (cached !== undefined) {
406
+ if (cached instanceof BadDataException) {
407
+ throw cached;
408
+ }
409
+
410
+ return cached;
411
+ }
412
+
413
+ let result: CompiledGrokPattern | BadDataException;
414
+
415
+ try {
416
+ result = compileGrokPattern(pattern);
417
+ } catch (err) {
418
+ result =
419
+ err instanceof BadDataException ? err : new BadDataException(String(err));
420
+ }
421
+
422
+ if (compileCache.size >= MAX_CACHED_GROK_PATTERNS) {
423
+ // Insertion-ordered: drop the oldest entry to bound the cache.
424
+ const oldestKey: string | undefined = compileCache.keys().next().value;
425
+
426
+ if (oldestKey !== undefined) {
427
+ compileCache.delete(oldestKey);
428
+ }
429
+ }
430
+
431
+ compileCache.set(pattern, result);
432
+
433
+ if (result instanceof BadDataException) {
434
+ throw result;
435
+ }
436
+
437
+ return result;
438
+ }
439
+
440
+ /** Test seam: forget every cached compilation. */
441
+ export function clearGrokCompileCache(): void {
442
+ compileCache.clear();
443
+ }
444
+
445
+ /*
446
+ * Compile + match in one call. Convenience for the dashboard's pattern
447
+ * tester and for tests; ingest compiles once and reuses the result.
448
+ */
449
+ export function parseWithGrokPattern(
450
+ pattern: string,
451
+ input: string,
452
+ ): Record<string, GrokValue> | null {
453
+ return matchGrokPattern(compileGrokPattern(pattern), input);
454
+ }
455
+
456
+ export default {
457
+ compileGrokPattern,
458
+ compileGrokPatternCached,
459
+ matchGrokPattern,
460
+ parseWithGrokPattern,
461
+ };
@@ -0,0 +1,118 @@
1
+ /*
2
+ * Built-in grok pattern library.
3
+ *
4
+ * Grok is regex with names: `%{IPV4:client_ip}` is just the IPv4 regex
5
+ * plus "put what it matched in client_ip". This file holds the named
6
+ * regex halves; Grok.ts does the naming.
7
+ *
8
+ * Two rules every entry here must follow:
9
+ *
10
+ * 1. NO capturing groups. Use `(?:...)`. Grok.ts injects its own
11
+ * named groups around the pattern it is asked to capture and reads
12
+ * results back through `match.groups`, so a stray `(...)` in a
13
+ * definition would silently shift nothing but still cost a capture
14
+ * slot on every record.
15
+ *
16
+ * 2. JavaScript-compatible syntax only. The upstream Logstash
17
+ * definitions are Oniguruma and use possessive quantifiers (`++`)
18
+ * and atomic groups (`(?>...)`), neither of which V8 accepts —
19
+ * those have been rewritten here rather than copied.
20
+ *
21
+ * Every repetition of a group consumes at least one character, which is
22
+ * what keeps `(?:/[\w]*)+`-shaped patterns linear instead of
23
+ * exponential. Keep it that way when adding patterns: these run once
24
+ * per ingested log record against attacker-supplied text.
25
+ */
26
+
27
+ const GrokPatterns: Record<string, string> = {
28
+ /* --- Primitives --- */
29
+ USERNAME: String.raw`[a-zA-Z0-9._-]+`,
30
+ USER: String.raw`%{USERNAME}`,
31
+ INT: String.raw`(?:[+-]?(?:[0-9]+))`,
32
+ BASE10NUM: String.raw`(?:[+-]?(?:[0-9]+(?:\.[0-9]+)?|\.[0-9]+))`,
33
+ NUMBER: String.raw`(?:%{BASE10NUM})`,
34
+ BASE16NUM: String.raw`(?:0[xX])?(?:[0-9A-Fa-f]+)`,
35
+ POSINT: String.raw`\b(?:[1-9][0-9]*)\b`,
36
+ NONNEGINT: String.raw`\b(?:[0-9]+)\b`,
37
+ WORD: String.raw`\b\w+\b`,
38
+ NOTSPACE: String.raw`\S+`,
39
+ SPACE: String.raw`\s*`,
40
+ DATA: String.raw`.*?`,
41
+ GREEDYDATA: String.raw`.*`,
42
+ QUOTEDSTRING: String.raw`(?:"(?:\\.|[^\\"])*"|'(?:\\.|[^\\'])*')`,
43
+ QS: String.raw`%{QUOTEDSTRING}`,
44
+ UUID: String.raw`[A-Fa-f0-9]{8}-(?:[A-Fa-f0-9]{4}-){3}[A-Fa-f0-9]{12}`,
45
+
46
+ /* --- Network --- */
47
+ IPV4: String.raw`(?<![0-9])(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(?![0-9])`,
48
+ /*
49
+ * Alternation is first-match, not longest-match, so order carries
50
+ * meaning here: the IPv4-embedded forms come first (a generic hex form
51
+ * would otherwise stop before the dotted quad), and the "::"-elision
52
+ * forms are ordered so a shape that can only match a prefix never sits
53
+ * ahead of one that matches the whole address.
54
+ */
55
+ IPV6: String.raw`(?:::(?:[Ff]{4}(?::0{1,4})?:)?%{IPV4}|(?:[0-9A-Fa-f]{1,4}:){1,4}:%{IPV4}|(?:[0-9A-Fa-f]{1,4}:){7}[0-9A-Fa-f]{1,4}|[0-9A-Fa-f]{1,4}:(?::[0-9A-Fa-f]{1,4}){1,6}|(?:[0-9A-Fa-f]{1,4}:){1,2}(?::[0-9A-Fa-f]{1,4}){1,5}|(?:[0-9A-Fa-f]{1,4}:){1,3}(?::[0-9A-Fa-f]{1,4}){1,4}|(?:[0-9A-Fa-f]{1,4}:){1,4}(?::[0-9A-Fa-f]{1,4}){1,3}|(?:[0-9A-Fa-f]{1,4}:){1,5}(?::[0-9A-Fa-f]{1,4}){1,2}|(?:[0-9A-Fa-f]{1,4}:){1,6}:[0-9A-Fa-f]{1,4}|(?:[0-9A-Fa-f]{1,4}:){1,7}:|:(?:(?::[0-9A-Fa-f]{1,4}){1,7}|:))`,
56
+ IP: String.raw`(?:%{IPV6}|%{IPV4})`,
57
+ HOSTNAME: String.raw`\b(?:[0-9A-Za-z][0-9A-Za-z-]{0,62})(?:\.(?:[0-9A-Za-z][0-9A-Za-z-]{0,62}))*\.?`,
58
+ IPORHOST: String.raw`(?:%{IP}|%{HOSTNAME})`,
59
+ HOSTPORT: String.raw`%{IPORHOST}:%{POSINT}`,
60
+ COMMONMAC: String.raw`(?:(?:[A-Fa-f0-9]{2}:){5}[A-Fa-f0-9]{2})`,
61
+ CISCOMAC: String.raw`(?:(?:[A-Fa-f0-9]{4}\.){2}[A-Fa-f0-9]{4})`,
62
+ WINDOWSMAC: String.raw`(?:(?:[A-Fa-f0-9]{2}-){5}[A-Fa-f0-9]{2})`,
63
+ MAC: String.raw`(?:%{CISCOMAC}|%{WINDOWSMAC}|%{COMMONMAC})`,
64
+ EMAILLOCALPART: String.raw`[a-zA-Z0-9!#$%&'*+\-/=?^_{|}~]{1,64}(?:\.[a-zA-Z0-9!#$%&'*+\-/=?^_{|}~]{1,62}){0,63}`,
65
+ EMAILADDRESS: String.raw`%{EMAILLOCALPART}@%{HOSTNAME}`,
66
+
67
+ /* --- Paths and URIs --- */
68
+ UNIXPATH: String.raw`(?:/[\w_%!$@:.,+~-]*)+`,
69
+ WINPATH: String.raw`(?:[A-Za-z]+:|\\)(?:\\[^\\?*]*)+`,
70
+ PATH: String.raw`(?:%{UNIXPATH}|%{WINPATH})`,
71
+ URIPROTO: String.raw`[A-Za-z][A-Za-z0-9+.-]+`,
72
+ URIHOST: String.raw`%{IPORHOST}(?::%{POSINT})?`,
73
+ URIPATH: String.raw`(?:/[A-Za-z0-9$.+!*'(){},~:;=@#%&_-]*)+`,
74
+ URIPARAM: String.raw`\?[A-Za-z0-9$.+!*'|(){},~@#%&/=:;_?<>\[\]-]*`,
75
+ URIPATHPARAM: String.raw`%{URIPATH}(?:%{URIPARAM})?`,
76
+ URI: String.raw`%{URIPROTO}://(?:%{USER}(?::[^@]*)?@)?(?:%{URIHOST})?(?:%{URIPATHPARAM})?`,
77
+
78
+ /* --- Dates and times --- */
79
+ MONTH: String.raw`\b(?:Jan(?:uary)?|Feb(?:ruary)?|Mar(?:ch)?|Apr(?:il)?|May|Jun(?:e)?|Jul(?:y)?|Aug(?:ust)?|Sep(?:tember)?|Oct(?:ober)?|Nov(?:ember)?|Dec(?:ember)?)\b`,
80
+ MONTHNUM: String.raw`(?:0?[1-9]|1[0-2])`,
81
+ MONTHNUM2: String.raw`(?:0[1-9]|1[0-2])`,
82
+ MONTHDAY: String.raw`(?:0[1-9]|[12][0-9]|3[01]|[1-9])`,
83
+ DAY: String.raw`(?:Mon(?:day)?|Tue(?:sday)?|Wed(?:nesday)?|Thu(?:rsday)?|Fri(?:day)?|Sat(?:urday)?|Sun(?:day)?)`,
84
+ YEAR: String.raw`(?:\d\d){1,2}`,
85
+ HOUR: String.raw`(?:2[0123]|[01]?[0-9])`,
86
+ MINUTE: String.raw`(?:[0-5][0-9])`,
87
+ SECOND: String.raw`(?:(?:[0-5]?[0-9]|60)(?:[:.,][0-9]+)?)`,
88
+ TIME: String.raw`(?:%{HOUR}:%{MINUTE}(?::%{SECOND})?)`,
89
+ DATE_US: String.raw`%{MONTHNUM}[/-]%{MONTHDAY}[/-]%{YEAR}`,
90
+ DATE_EU: String.raw`%{MONTHDAY}[./-]%{MONTHNUM}[./-]%{YEAR}`,
91
+ DATE: String.raw`(?:%{DATE_US}|%{DATE_EU})`,
92
+ ISO8601_TIMEZONE: String.raw`(?:Z|[+-]%{HOUR}(?::?%{MINUTE})?)`,
93
+ TIMESTAMP_ISO8601: String.raw`%{YEAR}-%{MONTHNUM2}-%{MONTHDAY}[T ]%{HOUR}:?%{MINUTE}(?::?%{SECOND})?(?:%{ISO8601_TIMEZONE})?`,
94
+ DATESTAMP: String.raw`%{DATE}[- ]%{TIME}`,
95
+ TZ: String.raw`(?:[APMCE][SD]T|UTC|GMT)`,
96
+ HTTPDATE: String.raw`%{MONTHDAY}/%{MONTH}/%{YEAR}:%{TIME} %{INT}`,
97
+ SYSLOGTIMESTAMP: String.raw`%{MONTH} +%{MONTHDAY} %{TIME}`,
98
+
99
+ /* --- Log shapes --- */
100
+ LOGLEVEL: String.raw`(?:[Aa]lert|ALERT|[Tt]race|TRACE|[Dd]ebug|DEBUG|[Nn]otice|NOTICE|[Ii]nfo(?:rmation)?|INFO(?:RMATION)?|[Ww]arn(?:ing)?|WARN(?:ING)?|[Ee]rr(?:or)?|ERR(?:OR)?|[Cc]rit(?:ical)?|CRIT(?:ICAL)?|[Ff]atal|FATAL|[Ss]evere|SEVERE|[Ee]merg(?:ency)?|EMERG(?:ENCY)?)`,
101
+ PROG: String.raw`[\x21-\x5a\x5c\x5e-\x7e]+`,
102
+ SYSLOGPROG: String.raw`%{PROG:program}(?:\[%{POSINT:pid}\])?`,
103
+ SYSLOGFACILITY: String.raw`<%{NONNEGINT:facility}\.%{NONNEGINT:priority}>`,
104
+ SYSLOGBASE: String.raw`%{SYSLOGTIMESTAMP:timestamp} (?:%{SYSLOGFACILITY} )?%{IPORHOST:logsource} %{SYSLOGPROG}:`,
105
+ JAVACLASS: String.raw`(?:[a-zA-Z$_][a-zA-Z$_0-9]*\.)*[a-zA-Z$_][a-zA-Z$_0-9]*`,
106
+ COMMONAPACHELOG: String.raw`%{IPORHOST:clientip} %{USER:ident} %{USER:auth} \[%{HTTPDATE:timestamp}\] "(?:%{WORD:verb} %{NOTSPACE:request}(?: HTTP/%{NUMBER:httpversion})?|%{DATA:rawrequest})" %{NUMBER:response} (?:%{NUMBER:bytes}|-)`,
107
+ COMBINEDAPACHELOG: String.raw`%{COMMONAPACHELOG} %{QS:referrer} %{QS:agent}`,
108
+ };
109
+
110
+ /*
111
+ * Names only, sorted — the pattern picker in the processor form lists
112
+ * these so a user can see what is available without leaving the page.
113
+ */
114
+ export function getGrokPatternNames(): Array<string> {
115
+ return Object.keys(GrokPatterns).sort();
116
+ }
117
+
118
+ export default GrokPatterns;
@@ -1,7 +1,6 @@
1
1
  import Monitor from "../../Models/DatabaseModels/Monitor";
2
2
  import MonitorTemplate from "../../Models/DatabaseModels/MonitorTemplate";
3
3
  import NetworkDevice from "../../Models/DatabaseModels/NetworkDevice";
4
- import ColumnLength from "../../Types/Database/ColumnLength";
5
4
  import BadDataException from "../../Types/Exception/BadDataException";
6
5
  import { JSONObject } from "../../Types/JSON";
7
6
  import JSONFunctions from "../../Types/JSONFunctions";
@@ -25,6 +24,29 @@ import ObjectID from "../../Types/ObjectID";
25
24
  * rule-engine, and audit hooks still run.
26
25
  */
27
26
 
27
+ /*
28
+ * The widest name a provisioned monitor may carry.
29
+ *
30
+ * Monitor.name is varchar(100), but the real ceiling is the SLUG: Monitor is
31
+ * @SlugifyColumn("name", "slug") and Slug.getSlug appends a dash plus ten
32
+ * random digits into its own varchar(100), and the create path THROWS on
33
+ * overflow rather than truncating. A name that fills the name column
34
+ * therefore produces a 111-character slug and fails the insert — the monitor
35
+ * is counted into `monitorsFailed` and the device silently never gets one.
36
+ *
37
+ * 88 leaves exactly the slug's 11-character tail, the same cap
38
+ * MAX_PING_MONITOR_NAME_LENGTH uses on the other provisioning path. Device
39
+ * names arrive clamped to 80 by DiscoveredDeviceBuilder, so this only bites a
40
+ * device renamed by hand or a long template suffix.
41
+ */
42
+ export const MAX_PROVISIONED_MONITOR_NAME_LENGTH: number = 88;
43
+
44
+ /*
45
+ * What sits between the device's own identity and the template's default
46
+ * monitor name when the template supplies one.
47
+ */
48
+ const MONITOR_NAME_SEPARATOR: string = " - ";
49
+
28
50
  type NetworkDeviceId = ObjectID | string | null | undefined;
29
51
 
30
52
  export interface RebindNetworkDeviceMonitorStepsData {
@@ -300,12 +322,42 @@ export default class NetworkDeviceMonitorTemplateUtil {
300
322
  data.networkDevice.hostname?.trim() ||
301
323
  `Network Device ${networkDeviceId}`;
302
324
  const templateMonitorName: string =
303
- data.monitorTemplate.monitorName?.trim() || "Monitor";
304
-
305
- return this.truncateWithoutSplittingSurrogatePair(
306
- `${deviceIdentity} - ${templateMonitorName}`,
307
- ColumnLength.ShortText,
325
+ data.monitorTemplate.monitorName?.trim() || "";
326
+
327
+ /*
328
+ * The suffix is OPT-IN (issue #3486). A template with no default monitor
329
+ * name names its monitors after the device and nothing else, because the
330
+ * alternative — the old `|| "Monitor"` fallback — was an invented suffix
331
+ * on an estate the operator never asked to have suffixed. A template that
332
+ * DOES carry a name keeps the composed form, which is what tells two
333
+ * templates apart on the same device.
334
+ */
335
+ const composedName: string = templateMonitorName
336
+ ? `${deviceIdentity}${MONITOR_NAME_SEPARATOR}${templateMonitorName}`
337
+ : deviceIdentity;
338
+
339
+ const truncatedName: string = this.truncateWithoutSplittingSurrogatePair(
340
+ composedName,
341
+ MAX_PROVISIONED_MONITOR_NAME_LENGTH,
308
342
  );
343
+
344
+ /*
345
+ * The device identity leads, so a cap reached inside the separator leaves
346
+ * the name trailing " ", " -" or " - " — a suffix that is visibly half
347
+ * there. Everything that survived such a cut IS the device identity, so
348
+ * return that rather than the remnant. Checked by LENGTH rather than by
349
+ * trailing characters, so a device genuinely named "Router A -" keeps its
350
+ * own trailing hyphen when nothing was cut at all.
351
+ */
352
+ if (
353
+ truncatedName.length > deviceIdentity.length &&
354
+ truncatedName.length <=
355
+ deviceIdentity.length + MONITOR_NAME_SEPARATOR.length
356
+ ) {
357
+ return deviceIdentity;
358
+ }
359
+
360
+ return truncatedName;
309
361
  }
310
362
 
311
363
  private static truncateWithoutSplittingSurrogatePair(