@oneuptime/common 12.0.27 → 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 (204) hide show
  1. package/Models/AnalyticsModels/RumSessionChunk.ts +52 -0
  2. package/Models/DatabaseModels/CloudResource.ts +17 -1
  3. package/Models/DatabaseModels/DashboardDomain.ts +43 -0
  4. package/Models/DatabaseModels/MonitorTemplate.ts +15 -3
  5. package/Models/DatabaseModels/RumApplication.ts +35 -2
  6. package/Models/DatabaseModels/StatusPageDomain.ts +43 -0
  7. package/Server/API/DashboardDomainAPI.ts +74 -0
  8. package/Server/API/MicrosoftTeamsAPI.ts +81 -180
  9. package/Server/API/StatusPageDomainAPI.ts +74 -0
  10. package/Server/API/TelemetryAPI.ts +104 -15
  11. package/Server/Infrastructure/Postgres/SchemaMigrations/1790200000000-AllowNullMonitorNameOnMonitorTemplate.ts +43 -0
  12. package/Server/Infrastructure/Postgres/SchemaMigrations/1790300000000-AddCertificateReissueRequestedAtToDomains.ts +40 -0
  13. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
  14. package/Server/Middleware/ProjectAuthorization.ts +6 -1
  15. package/Server/Services/DashboardDomainService.ts +131 -0
  16. package/Server/Services/LogPipelineProcessorService.ts +88 -0
  17. package/Server/Services/MonitorTemplateService.ts +70 -11
  18. package/Server/Services/StatusPageDomainService.ts +163 -3
  19. package/Server/Services/StatusPagePrivateUserSessionService.ts +197 -56
  20. package/Server/Types/Domain.ts +17 -0
  21. package/Server/Utils/APIKey/AccessPermission.ts +24 -0
  22. package/Server/Utils/Greenlock/Greenlock.ts +156 -58
  23. package/Server/Utils/LogPipelineProcessorValidation.ts +112 -0
  24. package/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsClient.ts +1094 -45
  25. package/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsPoller.ts +153 -17
  26. package/Server/Utils/SessionReplay/SessionReplayGateCache.ts +12 -5
  27. package/Server/Utils/SessionReplay/SessionReplayIdentity.ts +102 -0
  28. package/Server/Utils/SessionReplay/SessionReplayReadService.ts +11 -2
  29. package/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.ts +23 -11
  30. package/Server/Utils/Workspace/MicrosoftTeams/Actions/Auth.ts +0 -170
  31. package/Server/Utils/Workspace/MicrosoftTeams/Actions/Authorization.ts +158 -0
  32. package/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.ts +23 -11
  33. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +35 -13
  34. package/Tests/App/Dashboard/MonitorTemplateColumnAndFacet.test.tsx +505 -0
  35. package/Tests/App/StatusPage/StatusPageLoginCodeBootstrap.test.ts +106 -0
  36. package/Tests/App/StatusPage/StatusPageLoginCodeUtil.test.ts +84 -0
  37. package/Tests/App/StatusPage/StatusPageRedirectSafety.test.ts +119 -0
  38. package/Tests/Models/DatabaseModels/SessionReplayModels.test.ts +81 -0
  39. package/Tests/Models/MonitorTemplateMonitorNameColumn.test.ts +258 -0
  40. package/Tests/Server/API/CustomDomainReissueSslAPI.test.ts +469 -0
  41. package/Tests/Server/API/MicrosoftTeamsBotMessagesEndpoint.test.ts +729 -0
  42. package/Tests/Server/API/MicrosoftTeamsBotTestEndpoint.test.ts +26 -0
  43. package/Tests/Server/API/MicrosoftTeamsLegacyWebhookRemoval.test.ts +402 -0
  44. package/Tests/Server/API/SessionReplayAPI.test.ts +232 -0
  45. package/Tests/Server/Middleware/ProjectAuthorizationApiKeyMiddleware.test.ts +4 -0
  46. package/Tests/Server/Services/AddCertificateReissueRequestedAtToDomainsMigration.test.ts +287 -0
  47. package/Tests/Server/Services/CustomDomainCertificateReissue.test.ts +477 -0
  48. package/Tests/Server/Services/LogPipelineProcessorSaveValidation.test.ts +249 -0
  49. package/Tests/Server/Services/MonitorTemplateServiceBulkSync.test.ts +549 -0
  50. package/Tests/Server/Services/MonitorTemplateServiceNetworkDeviceSync.test.ts +16 -3
  51. package/Tests/Server/Services/NetworkDeviceAutoImportRuleEngineService.test.ts +57 -0
  52. package/Tests/Server/Services/SecurityEventLastErrorColumnWidth.test.ts +151 -15
  53. package/Tests/Server/Services/StatusPagePrivateUserSessionService.test.ts +587 -0
  54. package/Tests/Server/Types/Database/Permissions/ApiKeyFileAccess.test.ts +286 -0
  55. package/Tests/Server/Types/Database/Permissions/MonitorTemplateColumnAccess.test.ts +328 -0
  56. package/Tests/Server/Types/Database/QueryHelperFindWithSameTextAndIsNull.test.ts +83 -0
  57. package/Tests/Server/Utils/AI/SRE/ConfidenceSignal.test.ts +377 -0
  58. package/Tests/Server/Utils/APIKey/AccessPermission.test.ts +16 -1
  59. package/Tests/Server/Utils/Greenlock/CertificateRenewalSchedule.test.ts +250 -0
  60. package/Tests/Server/Utils/LogPipelineProcessorValidation.test.ts +198 -0
  61. package/Tests/Server/Utils/SecurityEvent/ConnectorErrorMessage.test.ts +48 -9
  62. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsAuth.test.ts +726 -0
  63. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsClient.test.ts +74 -11
  64. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsErrorHandling.test.ts +1107 -0
  65. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsPoller.test.ts +20 -3
  66. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsPollerFailureTaxonomy.test.ts +51 -11
  67. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsPollerHardening.test.ts +1246 -0
  68. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsRequestContract.test.ts +961 -0
  69. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsResponseParsing.test.ts +653 -0
  70. package/Tests/Server/Utils/Workspace/MicrosoftTeamsActionAuthorization.test.ts +880 -0
  71. package/Tests/Types/API/HTTPResponse.test.ts +175 -0
  72. package/Tests/Types/API/Route.test.ts +36 -4
  73. package/Tests/Types/Database/DatabaseProperty.test.ts +171 -0
  74. package/Tests/Types/DateLocalShortDateTimeString.test.ts +518 -0
  75. package/Tests/Types/DateLocalTimeString.test.ts +259 -0
  76. package/Tests/Types/DateUserPrefers12HourFormat.test.ts +316 -0
  77. package/Tests/UI/Components/LogsHistogram.test.tsx +41 -2
  78. package/Tests/UI/Components/LogsViewerEmptyStateTimeRange.test.tsx +212 -0
  79. package/Tests/UI/Components/TelemetryChartClockFormat.test.tsx +359 -0
  80. package/Tests/UI/Components/TimeRangePickerClockFormat.test.tsx +422 -0
  81. package/Tests/UI/Components/TimeRangePickerDropdown.test.tsx +146 -8
  82. package/Tests/UI/MicrosoftTeams/MicrosoftTeamsMessagingEndpointGuidance.test.tsx +520 -0
  83. package/Tests/UI/Utils/Navigation.test.ts +82 -0
  84. package/Tests/Utils/Array.test.ts +100 -0
  85. package/Tests/Utils/CertificateReissue.test.ts +225 -0
  86. package/Tests/Utils/Grok/Grok.test.ts +492 -0
  87. package/Tests/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.test.ts +181 -13
  88. package/Types/API/Route.ts +20 -5
  89. package/Types/Date.ts +143 -13
  90. package/Types/Rum/SessionReplay.ts +76 -1
  91. package/UI/Components/Date/TimeRangePickerDropdown.tsx +9 -5
  92. package/UI/Components/LogsViewer/LogsViewer.tsx +12 -7
  93. package/UI/Components/LogsViewer/components/HistogramTooltip.tsx +18 -17
  94. package/UI/Components/LogsViewer/components/LogsAnalyticsView.tsx +21 -28
  95. package/UI/Components/LogsViewer/components/LogsHistogram.tsx +2 -4
  96. package/UI/Components/TelemetryViewer/components/TelemetryHistogram.tsx +2 -4
  97. package/UI/Components/TelemetryViewer/components/TelemetryHistogramTooltip.tsx +17 -16
  98. package/UI/Utils/Navigation.ts +42 -0
  99. package/Utils/Array.ts +37 -0
  100. package/Utils/CertificateReissue.ts +128 -0
  101. package/Utils/Grok/Grok.ts +461 -0
  102. package/Utils/Grok/GrokPatterns.ts +118 -0
  103. package/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.ts +58 -6
  104. package/build/dist/Models/AnalyticsModels/RumSessionChunk.js +48 -0
  105. package/build/dist/Models/AnalyticsModels/RumSessionChunk.js.map +1 -1
  106. package/build/dist/Models/DatabaseModels/CloudResource.js +19 -2
  107. package/build/dist/Models/DatabaseModels/CloudResource.js.map +1 -1
  108. package/build/dist/Models/DatabaseModels/DashboardDomain.js +44 -0
  109. package/build/dist/Models/DatabaseModels/DashboardDomain.js.map +1 -1
  110. package/build/dist/Models/DatabaseModels/MonitorTemplate.js +17 -4
  111. package/build/dist/Models/DatabaseModels/MonitorTemplate.js.map +1 -1
  112. package/build/dist/Models/DatabaseModels/RumApplication.js +37 -3
  113. package/build/dist/Models/DatabaseModels/RumApplication.js.map +1 -1
  114. package/build/dist/Models/DatabaseModels/StatusPageDomain.js +44 -0
  115. package/build/dist/Models/DatabaseModels/StatusPageDomain.js.map +1 -1
  116. package/build/dist/Server/API/DashboardDomainAPI.js +43 -1
  117. package/build/dist/Server/API/DashboardDomainAPI.js.map +1 -1
  118. package/build/dist/Server/API/MicrosoftTeamsAPI.js +74 -118
  119. package/build/dist/Server/API/MicrosoftTeamsAPI.js.map +1 -1
  120. package/build/dist/Server/API/StatusPageDomainAPI.js +43 -1
  121. package/build/dist/Server/API/StatusPageDomainAPI.js.map +1 -1
  122. package/build/dist/Server/API/TelemetryAPI.js +67 -14
  123. package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
  124. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790200000000-AllowNullMonitorNameOnMonitorTemplate.js +34 -0
  125. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790200000000-AllowNullMonitorNameOnMonitorTemplate.js.map +1 -0
  126. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790300000000-AddCertificateReissueRequestedAtToDomains.js +28 -0
  127. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790300000000-AddCertificateReissueRequestedAtToDomains.js.map +1 -0
  128. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
  129. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  130. package/build/dist/Server/Middleware/ProjectAuthorization.js +6 -1
  131. package/build/dist/Server/Middleware/ProjectAuthorization.js.map +1 -1
  132. package/build/dist/Server/Services/DashboardDomainService.js +104 -0
  133. package/build/dist/Server/Services/DashboardDomainService.js.map +1 -1
  134. package/build/dist/Server/Services/LogPipelineProcessorService.js +65 -0
  135. package/build/dist/Server/Services/LogPipelineProcessorService.js.map +1 -1
  136. package/build/dist/Server/Services/MonitorTemplateService.js +57 -11
  137. package/build/dist/Server/Services/MonitorTemplateService.js.map +1 -1
  138. package/build/dist/Server/Services/StatusPageDomainService.js +132 -3
  139. package/build/dist/Server/Services/StatusPageDomainService.js.map +1 -1
  140. package/build/dist/Server/Services/StatusPagePrivateUserSessionService.js +112 -32
  141. package/build/dist/Server/Services/StatusPagePrivateUserSessionService.js.map +1 -1
  142. package/build/dist/Server/Types/Domain.js +16 -0
  143. package/build/dist/Server/Types/Domain.js.map +1 -1
  144. package/build/dist/Server/Utils/APIKey/AccessPermission.js +24 -0
  145. package/build/dist/Server/Utils/APIKey/AccessPermission.js.map +1 -1
  146. package/build/dist/Server/Utils/Greenlock/Greenlock.js +110 -35
  147. package/build/dist/Server/Utils/Greenlock/Greenlock.js.map +1 -1
  148. package/build/dist/Server/Utils/LogPipelineProcessorValidation.js +77 -0
  149. package/build/dist/Server/Utils/LogPipelineProcessorValidation.js.map +1 -0
  150. package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsClient.js +777 -36
  151. package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsClient.js.map +1 -1
  152. package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsPoller.js +108 -17
  153. package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsPoller.js.map +1 -1
  154. package/build/dist/Server/Utils/SessionReplay/SessionReplayGateCache.js.map +1 -1
  155. package/build/dist/Server/Utils/SessionReplay/SessionReplayIdentity.js +59 -0
  156. package/build/dist/Server/Utils/SessionReplay/SessionReplayIdentity.js.map +1 -0
  157. package/build/dist/Server/Utils/SessionReplay/SessionReplayReadService.js +9 -2
  158. package/build/dist/Server/Utils/SessionReplay/SessionReplayReadService.js.map +1 -1
  159. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.js +17 -6
  160. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.js.map +1 -1
  161. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Auth.js +0 -159
  162. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Auth.js.map +1 -1
  163. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Authorization.js +128 -0
  164. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Authorization.js.map +1 -0
  165. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.js +17 -6
  166. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.js.map +1 -1
  167. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +25 -13
  168. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
  169. package/build/dist/Types/API/Route.js +16 -5
  170. package/build/dist/Types/API/Route.js.map +1 -1
  171. package/build/dist/Types/Date.js +115 -13
  172. package/build/dist/Types/Date.js.map +1 -1
  173. package/build/dist/Types/Rum/SessionReplay.js +38 -0
  174. package/build/dist/Types/Rum/SessionReplay.js.map +1 -1
  175. package/build/dist/UI/Components/Date/TimeRangePickerDropdown.js +9 -5
  176. package/build/dist/UI/Components/Date/TimeRangePickerDropdown.js.map +1 -1
  177. package/build/dist/UI/Components/LogsViewer/LogsViewer.js +12 -7
  178. package/build/dist/UI/Components/LogsViewer/LogsViewer.js.map +1 -1
  179. package/build/dist/UI/Components/LogsViewer/components/HistogramTooltip.js +15 -14
  180. package/build/dist/UI/Components/LogsViewer/components/HistogramTooltip.js.map +1 -1
  181. package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js +17 -22
  182. package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js.map +1 -1
  183. package/build/dist/UI/Components/LogsViewer/components/LogsHistogram.js +2 -4
  184. package/build/dist/UI/Components/LogsViewer/components/LogsHistogram.js.map +1 -1
  185. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js +2 -4
  186. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js.map +1 -1
  187. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogramTooltip.js +15 -14
  188. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogramTooltip.js.map +1 -1
  189. package/build/dist/UI/Utils/Navigation.js +30 -0
  190. package/build/dist/UI/Utils/Navigation.js.map +1 -1
  191. package/build/dist/Utils/Array.js +25 -0
  192. package/build/dist/Utils/Array.js.map +1 -1
  193. package/build/dist/Utils/CertificateReissue.js +97 -0
  194. package/build/dist/Utils/CertificateReissue.js.map +1 -0
  195. package/build/dist/Utils/Grok/Grok.js +324 -0
  196. package/build/dist/Utils/Grok/Grok.js.map +1 -0
  197. package/build/dist/Utils/Grok/GrokPatterns.js +112 -0
  198. package/build/dist/Utils/Grok/GrokPatterns.js.map +1 -0
  199. package/build/dist/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.js +63 -3
  200. package/build/dist/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.js.map +1 -1
  201. package/package.json +1 -1
  202. package/UI/Utils/JsonWebToken.ts +0 -14
  203. package/build/dist/UI/Utils/JsonWebToken.js +0 -10
  204. package/build/dist/UI/Utils/JsonWebToken.js.map +0 -1
@@ -16,7 +16,7 @@ import CaptureSpan from "../../Telemetry/CaptureSpan";
16
16
  import ConnectorErrorMessage from "../ConnectorErrorMessage";
17
17
  import { buildSecurityEventDbRow } from "../SecurityEventRow";
18
18
  import ThreatIntelEnricher from "../ThreatIntel/ThreatIntelEnricher";
19
- import GoogleSecOpsClient from "./GoogleSecOpsClient";
19
+ import GoogleSecOpsClient, { FetchAlertsResult } from "./GoogleSecOpsClient";
20
20
 
21
21
  const SECOPS_SERVICE_NAME: string = "Google SecOps";
22
22
 
@@ -31,6 +31,10 @@ const MAX_LOOKBACK_IN_MINUTES: number = 24 * 60;
31
31
  /*
32
32
  * Overlap between windows so boundary alerts are never missed; the
33
33
  * eventUid content hash keeps redelivered alerts deduplicatable.
34
+ *
35
+ * Still open (out of scope here): the overlap does produce genuine
36
+ * duplicate rows, because SecurityEvent is a plain MergeTree with
37
+ * eventUid outside the sort key, so nothing collapses them at write time.
34
38
  */
35
39
  const WINDOW_OVERLAP_IN_MINUTES: number = 1;
36
40
 
@@ -80,7 +84,18 @@ export default class GoogleSecOpsPoller {
80
84
  }
81
85
 
82
86
  try {
83
- await this.pollConnection(connection);
87
+ const ingested: number = await this.pollConnection(connection);
88
+
89
+ /*
90
+ * The count used to be returned and dropped on the floor, which
91
+ * left "polling healthy but quiet" and "polling silently broken"
92
+ * looking identical from outside the process. At info rather than
93
+ * debug for that reason — a number nobody sees at the default log
94
+ * level answers the question no better than not having it.
95
+ */
96
+ logger.info(
97
+ `GoogleSecOpsPoller: connection ${connection.id?.toString()} ingested ${ingested} security events.`,
98
+ );
84
99
  } catch (error) {
85
100
  logger.error(
86
101
  `GoogleSecOpsPoller: error polling connection ${connection.id?.toString()}:`,
@@ -134,6 +149,8 @@ export default class GoogleSecOpsPoller {
134
149
  );
135
150
  }
136
151
 
152
+ const connectionId: string = connection.id.toString();
153
+
137
154
  const client: GoogleSecOpsClient =
138
155
  clientOverride ||
139
156
  new GoogleSecOpsClient({
@@ -149,26 +166,96 @@ export default class GoogleSecOpsPoller {
149
166
  -MAX_LOOKBACK_IN_MINUTES,
150
167
  );
151
168
 
152
- let startTime: Date = connection.cursor
153
- ? OneUptimeDate.addRemoveMinutes(
154
- new Date(connection.cursor),
169
+ const defaultStartTime: Date = OneUptimeDate.addRemoveMinutes(
170
+ endTime,
171
+ -DEFAULT_LOOKBACK_IN_MINUTES,
172
+ );
173
+
174
+ let startTime: Date = defaultStartTime;
175
+
176
+ if (connection.cursor) {
177
+ const cursorTime: Date = new Date(connection.cursor);
178
+
179
+ if (Number.isNaN(cursorTime.getTime())) {
180
+ /*
181
+ * An unreadable cursor used to share a branch with a stale one and
182
+ * open the full 24 hour window — the widest possible blast radius
183
+ * for the least trustworthy input. It means "no usable cursor",
184
+ * which is what a first poll means too.
185
+ */
186
+ logger.warn(
187
+ `GoogleSecOpsPoller: connection ${connectionId} has an unreadable cursor ${JSON.stringify(
188
+ connection.cursor,
189
+ )}; polling the default ${DEFAULT_LOOKBACK_IN_MINUTES} minute window instead.`,
190
+ );
191
+ } else {
192
+ startTime = OneUptimeDate.addRemoveMinutes(
193
+ cursorTime,
155
194
  -WINDOW_OVERLAP_IN_MINUTES,
156
- )
157
- : OneUptimeDate.addRemoveMinutes(endTime, -DEFAULT_LOOKBACK_IN_MINUTES);
195
+ );
196
+ }
197
+ }
198
+
199
+ if (OneUptimeDate.isBefore(startTime, earliestAllowed)) {
200
+ /*
201
+ * The alerts between the cursor and the cap are skipped and will
202
+ * never be fetched again, so the gap has to be stated. Silently
203
+ * truncating it is how a connector comes back from an outage
204
+ * looking healthy while a day of detections is simply missing.
205
+ */
206
+ logger.warn(
207
+ `GoogleSecOpsPoller: connection ${connectionId} would poll from ${startTime.toISOString()}, ${OneUptimeDate.getMinutesBetweenTwoDates(
208
+ startTime,
209
+ endTime,
210
+ )} minutes back and past the ${MAX_LOOKBACK_IN_MINUTES} minute cap; alerts before ${earliestAllowed.toISOString()} are skipped and will not be fetched again.`,
211
+ );
158
212
 
159
- if (
160
- Number.isNaN(startTime.getTime()) ||
161
- OneUptimeDate.isBefore(startTime, earliestAllowed)
162
- ) {
163
213
  startTime = earliestAllowed;
164
214
  }
165
215
 
166
- const alerts: Array<JSONObject> = await client.fetchDetectionAlerts({
216
+ if (!OneUptimeDate.isBefore(startTime, endTime)) {
217
+ /*
218
+ * A cursor in the future — clock skew on the writer, or a restored
219
+ * backup — used to send Chronicle an inverted timeRange, which is
220
+ * neither an error the operator sees nor a window that returns
221
+ * anything.
222
+ */
223
+ logger.warn(
224
+ `GoogleSecOpsPoller: connection ${connectionId} cursor resolves to ${startTime.toISOString()}, at or after the current time; polling the default ${DEFAULT_LOOKBACK_IN_MINUTES} minute window instead.`,
225
+ );
226
+
227
+ startTime = defaultStartTime;
228
+ }
229
+
230
+ const fetched: FetchAlertsResult = await client.fetchDetectionAlerts({
167
231
  startTime,
168
232
  endTime,
169
233
  });
170
234
 
171
- let ingested: number = 0;
235
+ if (fetched.truncatedByCount) {
236
+ logger.warn(
237
+ `GoogleSecOpsPoller: connection ${connectionId} window ${startTime.toISOString()} to ${endTime.toISOString()} matched more alerts than Chronicle will return, and this endpoint has no pagination — alerts in this window were dropped. Lower pollIntervalInMinutes.`,
238
+ );
239
+ }
240
+
241
+ if (fetched.truncatedByBytes) {
242
+ logger.warn(
243
+ `GoogleSecOpsPoller: connection ${connectionId} window ${startTime.toISOString()} to ${endTime.toISOString()} was truncated by Chronicle's memory limit — alerts in this window were dropped. Lower pollIntervalInMinutes.`,
244
+ );
245
+ }
246
+
247
+ const alerts: Array<JSONObject> = fetched.alerts;
248
+
249
+ const normalizedAlerts: Array<NormalizedSecurityEvent> = [];
250
+
251
+ /*
252
+ * Objects the alert gate refused, and objects that blew up on the way
253
+ * through it. They are kept apart because they mean opposite things
254
+ * for the cursor: a refusal is deterministic and re-fetching it would
255
+ * refuse it again forever, a failure may well succeed next time.
256
+ */
257
+ let rejectedCount: number = 0;
258
+ let failedCount: number = 0;
172
259
 
173
260
  if (alerts.length > 0) {
174
261
  const serviceMetadata: TelemetryServiceMetadata =
@@ -185,17 +272,34 @@ export default class GoogleSecOpsPoller {
185
272
  projectRetentionInDays: serviceMetadata.projectRetentionInDays,
186
273
  });
187
274
 
188
- const normalizedAlerts: Array<NormalizedSecurityEvent> = [];
189
-
190
275
  for (const alert of alerts) {
191
276
  try {
277
+ /*
278
+ * The normalizer is a total function whose class, category,
279
+ * vendor and product are constants, so anything handed to it
280
+ * comes back out as a plausible Detection Finding row. The gate
281
+ * is what keeps a stray envelope chunk from being stored as a
282
+ * severity-Unknown detection nobody can tell from a real one.
283
+ */
284
+ if (!GoogleSecOpsAlertNormalizer.isGoogleSecOpsAlert(alert)) {
285
+ rejectedCount++;
286
+ continue;
287
+ }
288
+
192
289
  normalizedAlerts.push(GoogleSecOpsAlertNormalizer.normalize(alert));
193
290
  } catch (normalizeError) {
291
+ failedCount++;
194
292
  logger.error("GoogleSecOpsPoller: error normalizing alert");
195
293
  logger.error(normalizeError);
196
294
  }
197
295
  }
198
296
 
297
+ if (rejectedCount > 0) {
298
+ logger.warn(
299
+ `GoogleSecOpsPoller: connection ${connectionId} discarded ${rejectedCount} of ${alerts.length} fetched objects that do not look like Google SecOps alerts.`,
300
+ );
301
+ }
302
+
199
303
  /*
200
304
  * Threat-intel enrichment before row building, same seam as the
201
305
  * HTTP ingest path — polled alerts get the same threat.* stamps.
@@ -218,15 +322,43 @@ export default class GoogleSecOpsPoller {
218
322
 
219
323
  if (rows.length > 0) {
220
324
  await SecurityEventService.insertJsonRows(rows);
221
- ingested = rows.length;
222
325
  }
223
326
  }
224
327
 
328
+ const ingested: number = normalizedAlerts.length;
329
+
330
+ /*
331
+ * Nothing storable came out of a window that did contain alerts, and
332
+ * the reason was a failure rather than a refusal — so the alerts still
333
+ * exist at Chronicle and the window has to be re-fetched. Advancing
334
+ * here is what turned a normalizer fault into permanent, silent data
335
+ * loss: the insert-failure path leaves the cursor alone by throwing,
336
+ * and this path did not.
337
+ */
338
+ const advanceCursor: boolean = ingested > 0 || failedCount === 0;
339
+
340
+ if (!advanceCursor) {
341
+ logger.warn(
342
+ `GoogleSecOpsPoller: connection ${connectionId} normalized none of its ${alerts.length} fetched alerts; holding the cursor so the window is polled again rather than skipped.`,
343
+ );
344
+ }
345
+
346
+ const cursorUpdate: { cursor?: string } = advanceCursor
347
+ ? {
348
+ /*
349
+ * Still open (out of scope here): this stores the end of the
350
+ * window rather than the newest detection timestamp actually
351
+ * ingested, which is what the column documents itself as.
352
+ */
353
+ cursor: endTime.toISOString(),
354
+ }
355
+ : {};
356
+
225
357
  await GoogleSecOpsConnectionService.updateOneById({
226
358
  id: connection.id,
227
359
  data: {
228
360
  lastPolledAt: endTime,
229
- cursor: endTime.toISOString(),
361
+ ...cursorUpdate,
230
362
  lastError: null as unknown as string,
231
363
  },
232
364
  props: {
@@ -234,6 +366,10 @@ export default class GoogleSecOpsPoller {
234
366
  },
235
367
  });
236
368
 
369
+ logger.debug(
370
+ `GoogleSecOpsPoller: connection ${connectionId} fetched ${alerts.length} alerts and ingested ${ingested} security events for ${startTime.toISOString()} to ${endTime.toISOString()}.`,
371
+ );
372
+
237
373
  return ingested;
238
374
  }
239
375
  }
@@ -105,11 +105,18 @@ export interface SessionReplayGatePolicy {
105
105
  isAppEnabled: boolean;
106
106
 
107
107
  /*
108
- * Empty means REFUSED, not allow-all. This is the only anti-forgery
109
- * control available: a TelemetryIngestionKey has no expiry, no scope and
110
- * no origin binding, and the docs tell customers to paste it into
111
- * browser JavaScript, so anyone who scrapes the key could otherwise
112
- * write recordings into the victim's project.
108
+ * Empty means ANY ORIGIN, which is the shipped default (the column
109
+ * defaults to '[]') and what isOriginAllowed below actually implements -
110
+ * this doc used to claim the opposite ("empty means REFUSED"), in the one
111
+ * direction where being wrong matters, since a reader would conclude the
112
+ * feature was locked down out of the box.
113
+ *
114
+ * Filling it in is the only anti-forgery control available: a
115
+ * TelemetryIngestionKey has no expiry, no scope and no origin binding, and
116
+ * the docs tell customers to paste it into browser JavaScript, so anyone
117
+ * who scrapes the key can write recordings into the victim's project until
118
+ * this list names the customer's own domains. The installation-test panel
119
+ * flags an empty list as a warning for exactly that reason.
113
120
  */
114
121
  allowedOrigins: Array<string>;
115
122
 
@@ -0,0 +1,102 @@
1
+ import crypto from "crypto";
2
+ import { EncryptionSecret } from "../../EnvironmentConfig";
3
+ import ObjectID from "../../../Types/ObjectID";
4
+ import { SESSION_REPLAY_MAX_USER_REF_LENGTH } from "../../../Types/Rum/SessionReplay";
5
+
6
+ /*
7
+ * "Whose session is this?"
8
+ *
9
+ * A page that has an end-user reference hands it to the recorder
10
+ * (data-oneuptime-user-ref, or the init global). When the application has
11
+ * identity capture switched on, the recorder puts that reference on the
12
+ * chunk envelope's meta, and this module turns it into the two columns the
13
+ * session header carries:
14
+ *
15
+ * identifiedUserKey HMAC(EncryptionSecret, "<projectId>:<userRef>")
16
+ * identifiedUserLabel the reference itself, behind its own column ACL
17
+ *
18
+ * The key is what makes a reference SEARCHABLE and ERASABLE without storing
19
+ * it in a lookup-friendly form: a support engineer filtering by user, and a
20
+ * right-to-erasure request naming one, both resolve to the same digest. The
21
+ * label is what makes a session READABLE - "jane@example.com" rather than a
22
+ * 64-character hash - and is why it sits behind a narrower ACL than the rest
23
+ * of the session metadata.
24
+ *
25
+ * Scoped by project and NOT by application, deliberately. A project-wide
26
+ * erasure request (ProcessSessionErasureRequests filters on projectId with
27
+ * the application clause optional) has to be able to reach every session for
28
+ * that person, including ones recorded by sibling applications. An
29
+ * application-scoped digest would make that request silently under-delete,
30
+ * which is the failure mode with legal consequences.
31
+ *
32
+ * Deliberately NOT reusing SessionReplayTargeting.buildTargetKey: that one
33
+ * is application-scoped and prefixed for a Redis keyspace. Both are HMACs of
34
+ * a user reference, and conflating them would tie an erasure lookup to the
35
+ * shape of a Redis key.
36
+ */
37
+
38
+ export interface SessionReplayUserKeyInput {
39
+ projectId: ObjectID;
40
+ userRef: string;
41
+ }
42
+
43
+ export default class SessionReplayIdentity {
44
+ /*
45
+ * Usable when non-empty and within the shared cap. The cap matters on both
46
+ * sides of the comparison: the recorder slices the reference to the same
47
+ * length before sending it, so anything longer could never match a stored
48
+ * key anyway, and hashing an unbounded string here would be the only
49
+ * unbounded work on this path.
50
+ */
51
+ public static isUsableUserRef(userRef: unknown): userRef is string {
52
+ return (
53
+ typeof userRef === "string" &&
54
+ userRef.trim().length > 0 &&
55
+ userRef.length <= SESSION_REPLAY_MAX_USER_REF_LENGTH
56
+ );
57
+ }
58
+
59
+ /*
60
+ * The project scope lives in the KEY, not in the message.
61
+ *
62
+ * HMAC takes arbitrary key material, so deriving a per-project key from
63
+ * the instance secret and the project id gives real domain separation:
64
+ * two projects cannot produce the same digest for the same person even
65
+ * if one of them learns the other's project id, which prefixing the
66
+ * message only achieves by convention. It is also what makes the
67
+ * "per-project" wording on the identity columns literally true.
68
+ *
69
+ * The reference is trimmed but NOT lower-cased. End-user references are
70
+ * opaque to us - "U-1000" and "u-1000" may well be two different
71
+ * customers in the host application's own database - so folding case here
72
+ * would merge two people's recordings under one key, and an erasure for
73
+ * one would delete the other's. Targeting lower-cases the APPLICATION
74
+ * identifier for the same reason in reverse: that one is ours and is
75
+ * case-insensitive.
76
+ *
77
+ * A slow KDF (bcrypt / scrypt / argon2) is deliberately NOT used and
78
+ * would not help. This is a keyed pseudonym, not a stored password: it
79
+ * has to be DETERMINISTIC so the session filter and a right-to-erasure
80
+ * request months later resolve the same digest, and those algorithms are
81
+ * salted per invocation. What defends the digest is the key - without
82
+ * EncryptionSecret an attacker holding the column cannot test candidate
83
+ * references at all, at any work factor.
84
+ */
85
+ public static buildUserKey(data: SessionReplayUserKeyInput): string {
86
+ const projectKey: string = `${EncryptionSecret.toString()}:${data.projectId.toString()}`;
87
+
88
+ return crypto
89
+ .createHmac("sha256", projectKey)
90
+ .update(data.userRef.trim())
91
+ .digest("hex");
92
+ }
93
+
94
+ /*
95
+ * The label as stored. Trimmed and capped to the same length the recorder
96
+ * enforces, so the column can never hold something the wire could not have
97
+ * carried.
98
+ */
99
+ public static buildUserLabel(userRef: string): string {
100
+ return userRef.trim().slice(0, SESSION_REPLAY_MAX_USER_REF_LENGTH);
101
+ }
102
+ }
@@ -1341,13 +1341,22 @@ export default class SessionReplayReadService {
1341
1341
  );
1342
1342
  }
1343
1343
 
1344
- if (filters.hasFrustration === true) {
1344
+ if (filters.hasFrustration !== undefined) {
1345
1345
  /*
1346
1346
  * Over the argMax aliases, like every HAVING predicate here — the
1347
1347
  * raw columns would sum across ReplacingMergeTree versions.
1348
+ *
1349
+ * `!== undefined` rather than `=== true`, so `false` means "sessions
1350
+ * with NO frustration signals" instead of being silently dropped. The
1351
+ * route admits any boolean, and hasError / isFinalized beside it both
1352
+ * honour false, so accepting the value and ignoring it returned the
1353
+ * whole unfiltered list with a 200 and no indication why.
1348
1354
  */
1355
+ const total: string =
1356
+ "(aggRageClickCount + aggDeadClickCount + aggErrorClickCount + aggRefreshRageCount)";
1357
+
1349
1358
  statement.append(
1350
- " AND (aggRageClickCount + aggDeadClickCount + aggErrorClickCount + aggRefreshRageCount) > 0",
1359
+ filters.hasFrustration ? ` AND ${total} > 0` : ` AND ${total} = 0`,
1351
1360
  );
1352
1361
  }
1353
1362
 
@@ -18,6 +18,8 @@ import AlertStateService from "../../../../Services/AlertStateService";
18
18
  import UserNotificationEventType from "../../../../../Types/UserNotification/UserNotificationEventType";
19
19
  import OnCallDutyPolicy from "../../../../../Models/DatabaseModels/OnCallDutyPolicy";
20
20
  import AlertState from "../../../../../Models/DatabaseModels/AlertState";
21
+ import DatabaseCommonInteractionProps from "../../../../../Types/BaseDatabase/DatabaseCommonInteractionProps";
22
+ import MicrosoftTeamsActionAuthorization from "./Authorization";
21
23
 
22
24
  export default class MicrosoftTeamsAlertActions {
23
25
  @CaptureSpan()
@@ -255,6 +257,7 @@ export default class MicrosoftTeamsAlertActions {
255
257
  value: JSONObject;
256
258
  projectId: ObjectID;
257
259
  oneUptimeUserId: ObjectID;
260
+ databaseProps: DatabaseCommonInteractionProps;
258
261
  turnContext: TurnContext;
259
262
  }): Promise<void> {
260
263
  const {
@@ -263,6 +266,7 @@ export default class MicrosoftTeamsAlertActions {
263
266
  value,
264
267
  projectId,
265
268
  oneUptimeUserId,
269
+ databaseProps,
266
270
  turnContext,
267
271
  } = data;
268
272
 
@@ -274,10 +278,15 @@ export default class MicrosoftTeamsAlertActions {
274
278
  return;
275
279
  }
276
280
 
277
- await AlertService.acknowledgeAlert(
278
- new ObjectID(actionValue),
279
- oneUptimeUserId,
280
- );
281
+ const alertId: ObjectID = new ObjectID(actionValue);
282
+
283
+ await MicrosoftTeamsActionAuthorization.assertCanUpdateAlert({
284
+ alertId: alertId,
285
+ projectId: projectId,
286
+ props: databaseProps,
287
+ });
288
+
289
+ await AlertService.acknowledgeAlert(alertId, oneUptimeUserId);
281
290
  await turnContext.sendActivity("✅ Alert acknowledged.");
282
291
  return;
283
292
  }
@@ -288,10 +297,15 @@ export default class MicrosoftTeamsAlertActions {
288
297
  return;
289
298
  }
290
299
 
291
- await AlertService.resolveAlert(
292
- new ObjectID(actionValue),
293
- oneUptimeUserId,
294
- );
300
+ const alertId: ObjectID = new ObjectID(actionValue);
301
+
302
+ await MicrosoftTeamsActionAuthorization.assertCanUpdateAlert({
303
+ alertId: alertId,
304
+ projectId: projectId,
305
+ props: databaseProps,
306
+ });
307
+
308
+ await AlertService.resolveAlert(alertId, oneUptimeUserId);
295
309
  await turnContext.sendActivity("✅ Alert resolved.");
296
310
  return;
297
311
  }
@@ -506,9 +520,7 @@ export default class MicrosoftTeamsAlertActions {
506
520
  data: {
507
521
  currentAlertStateId: new ObjectID(alertStateId.toString()),
508
522
  },
509
- props: {
510
- isRoot: true,
511
- },
523
+ props: databaseProps,
512
524
  });
513
525
 
514
526
  await turnContext.sendActivity("✅ Alert state changed successfully.");
@@ -1,8 +1,5 @@
1
- import { ExpressRequest } from "../../../Express";
2
1
  import { JSONObject } from "../../../../../Types/JSON";
3
2
  import ObjectID from "../../../../../Types/ObjectID";
4
- import WorkspaceProjectAuthToken from "../../../../../Models/DatabaseModels/WorkspaceProjectAuthToken";
5
- import WorkspaceProjectAuthTokenService from "../../../../Services/WorkspaceProjectAuthTokenService";
6
3
  import WorkspaceType from "../../../../../Types/Workspace/WorkspaceType";
7
4
  import logger from "../../../Logger";
8
5
  import CaptureSpan from "../../../Telemetry/CaptureSpan";
@@ -30,173 +27,6 @@ export interface MicrosoftTeamsRequest {
30
27
  }
31
28
 
32
29
  export default class MicrosoftTeamsAuthAction {
33
- @CaptureSpan()
34
- public static async isAuthorized(data: {
35
- req: ExpressRequest;
36
- }): Promise<MicrosoftTeamsRequest> {
37
- const req: ExpressRequest = data.req;
38
- let payload: JSONObject = {};
39
- let tenantId: string = "";
40
-
41
- try {
42
- payload = req.body as JSONObject;
43
- logger.debug("Microsoft Teams payload received:");
44
- logger.debug(payload);
45
- } catch (error) {
46
- logger.debug("Failed to parse Microsoft Teams payload:");
47
- logger.debug(error);
48
- return {
49
- isAuthorized: false,
50
- projectId: new ObjectID(""),
51
- authToken: "",
52
- payloadType: "unknown",
53
- tenantId: tenantId,
54
- };
55
- }
56
-
57
- // Microsoft Teams sends different types of activities
58
- const activityType: string = payload["type"] as string;
59
- const from: JSONObject = payload["from"] as JSONObject;
60
- const conversation: JSONObject = payload["conversation"] as JSONObject;
61
- const channelData: JSONObject = payload["channelData"] as JSONObject;
62
-
63
- let teamId: string = "";
64
- let channelId: string = "";
65
- let userId: string = "";
66
-
67
- // Extract team and channel information
68
- if (channelData && channelData["team"]) {
69
- teamId = (channelData["team"] as JSONObject)["id"] as string;
70
- }
71
-
72
- if (conversation && conversation["id"]) {
73
- // For channel messages, conversation ID contains channel info
74
- channelId = conversation["id"] as string;
75
- }
76
-
77
- if (from && from["id"]) {
78
- userId = from["id"] as string;
79
- }
80
-
81
- if (channelData && channelData["tenant"]) {
82
- tenantId = (channelData["tenant"] as JSONObject)["id"] as string;
83
- }
84
-
85
- // Handle different activity types
86
- if (activityType === "installationUpdate") {
87
- // Bot was installed or uninstalled
88
- const action: string = payload["action"] as string;
89
- if (action === "remove") {
90
- return {
91
- isAuthorized: true,
92
- projectId: new ObjectID(""), // We'll need to find the project
93
- authToken: "",
94
- payloadType: "app_uninstall",
95
- teamId: teamId,
96
- tenantId: tenantId,
97
- };
98
- }
99
- }
100
-
101
- // Find the project associated with this team
102
- if (!teamId) {
103
- logger.debug("No team ID found in payload", {
104
- tenantId: tenantId,
105
- });
106
- return {
107
- isAuthorized: false,
108
- projectId: new ObjectID(""),
109
- authToken: "",
110
- payloadType: activityType,
111
- tenantId: tenantId,
112
- };
113
- }
114
-
115
- let projectAuthToken: WorkspaceProjectAuthToken | null = null;
116
-
117
- try {
118
- // Find project auth token by checking if miscData contains the teamId
119
- projectAuthToken = await WorkspaceProjectAuthTokenService.findOneBy({
120
- query: {
121
- workspaceType: WorkspaceType.MicrosoftTeams,
122
- workspaceProjectId: tenantId,
123
- },
124
- select: {
125
- _id: true,
126
- projectId: true,
127
- authToken: true,
128
- miscData: true,
129
- },
130
- props: {
131
- isRoot: true,
132
- },
133
- });
134
- } catch (error) {
135
- logger.debug("Error finding project auth token:", {
136
- teamId: teamId,
137
- tenantId: tenantId,
138
- });
139
- logger.debug(error);
140
- return {
141
- isAuthorized: false,
142
- projectId: new ObjectID(""),
143
- authToken: "",
144
- payloadType: activityType,
145
- tenantId: tenantId,
146
- };
147
- }
148
-
149
- if (!projectAuthToken) {
150
- logger.debug(`No project auth token found for team: ${teamId}`, {
151
- teamId: teamId,
152
- tenantId: tenantId,
153
- });
154
- return {
155
- isAuthorized: false,
156
- projectId: new ObjectID(""),
157
- authToken: "",
158
- payloadType: activityType,
159
- tenantId: tenantId,
160
- };
161
- }
162
-
163
- // Parse actions from the payload
164
- const actions: Array<MicrosoftTeamsAction> = [];
165
-
166
- if (activityType === "invoke") {
167
- // Handle adaptive card button clicks
168
- const value: JSONObject = payload["value"] as JSONObject;
169
- if (value && value["action"]) {
170
- actions.push({
171
- actionType: value["action"] as string,
172
- actionValue: value["actionValue"] as string,
173
- });
174
- }
175
- } else if (activityType === "message") {
176
- // Handle text messages to the bot
177
- const text: string = (payload["text"] as string) || "";
178
- if (text.toLowerCase().includes("help")) {
179
- actions.push({
180
- actionType: "help",
181
- });
182
- }
183
- }
184
-
185
- return {
186
- isAuthorized: true,
187
- projectId: projectAuthToken.projectId!,
188
- authToken: projectAuthToken.authToken!,
189
- payloadType: activityType,
190
- actions: actions,
191
- userId: userId,
192
- teamId: teamId,
193
- tenantId: tenantId,
194
- channelId: channelId,
195
- messageId: payload["id"] as string,
196
- payload: payload,
197
- };
198
- }
199
-
200
30
  @CaptureSpan()
201
31
  public static async getOneUptimeUserIdFromTeamsUserId(data: {
202
32
  teamsUserId: string;