@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
@@ -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
  }
@@ -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;
@@ -0,0 +1,158 @@
1
+ import Alert from "../../../../../Models/DatabaseModels/Alert";
2
+ import Incident from "../../../../../Models/DatabaseModels/Incident";
3
+ import DatabaseCommonInteractionProps from "../../../../../Types/BaseDatabase/DatabaseCommonInteractionProps";
4
+ import NotAuthorizedException from "../../../../../Types/Exception/NotAuthorizedException";
5
+ import ObjectID from "../../../../../Types/ObjectID";
6
+ import AlertService from "../../../../Services/AlertService";
7
+ import IncidentService from "../../../../Services/IncidentService";
8
+ import ModelPermission from "../../../../Types/Database/Permissions/Index";
9
+ import Query from "../../../../Types/Database/Query";
10
+ import { applyAlertSelfPrivacyFilter } from "../../../Alert/AlertPrivacyFilter";
11
+ import { applyIncidentSelfPrivacyFilter } from "../../../Incident/IncidentPrivacyFilter";
12
+ import CaptureSpan from "../../../Telemetry/CaptureSpan";
13
+
14
+ export default class MicrosoftTeamsActionAuthorization {
15
+ @CaptureSpan()
16
+ public static async assertCanUpdateIncident(data: {
17
+ incidentId: ObjectID;
18
+ projectId: ObjectID;
19
+ props: DatabaseCommonInteractionProps;
20
+ }): Promise<void> {
21
+ const incident: Incident | null = await IncidentService.findOneBy({
22
+ query: {
23
+ _id: data.incidentId,
24
+ projectId: data.projectId,
25
+ },
26
+ select: {
27
+ _id: true,
28
+ labels: {
29
+ _id: true,
30
+ name: true,
31
+ },
32
+ },
33
+ props: {
34
+ isRoot: true,
35
+ },
36
+ });
37
+
38
+ if (!incident) {
39
+ throw new NotAuthorizedException(
40
+ "You do not have permission to update this incident.",
41
+ );
42
+ }
43
+
44
+ await ModelPermission.checkUpdatePermissionByModel({
45
+ modelType: Incident,
46
+ fetchModelWithAccessControlIds: async (): Promise<Incident> => {
47
+ return incident;
48
+ },
49
+ props: data.props,
50
+ });
51
+
52
+ const permittedQuery: Query<Incident> =
53
+ await ModelPermission.checkUpdateQueryPermissions(
54
+ Incident,
55
+ {
56
+ _id: data.incidentId,
57
+ projectId: data.projectId,
58
+ },
59
+ {
60
+ currentIncidentStateId: ObjectID.getZeroObjectID(),
61
+ },
62
+ data.props,
63
+ );
64
+
65
+ const privacyScopedQuery: Query<Incident> = applyIncidentSelfPrivacyFilter(
66
+ permittedQuery,
67
+ data.props,
68
+ );
69
+
70
+ const permittedIncident: Incident | null = await IncidentService.findOneBy({
71
+ query: privacyScopedQuery,
72
+ select: {
73
+ _id: true,
74
+ },
75
+ props: {
76
+ isRoot: true,
77
+ },
78
+ });
79
+
80
+ if (!permittedIncident) {
81
+ throw new NotAuthorizedException(
82
+ "You do not have permission to update this incident.",
83
+ );
84
+ }
85
+ }
86
+
87
+ @CaptureSpan()
88
+ public static async assertCanUpdateAlert(data: {
89
+ alertId: ObjectID;
90
+ projectId: ObjectID;
91
+ props: DatabaseCommonInteractionProps;
92
+ }): Promise<void> {
93
+ const alert: Alert | null = await AlertService.findOneBy({
94
+ query: {
95
+ _id: data.alertId,
96
+ projectId: data.projectId,
97
+ },
98
+ select: {
99
+ _id: true,
100
+ labels: {
101
+ _id: true,
102
+ name: true,
103
+ },
104
+ },
105
+ props: {
106
+ isRoot: true,
107
+ },
108
+ });
109
+
110
+ if (!alert) {
111
+ throw new NotAuthorizedException(
112
+ "You do not have permission to update this alert.",
113
+ );
114
+ }
115
+
116
+ await ModelPermission.checkUpdatePermissionByModel({
117
+ modelType: Alert,
118
+ fetchModelWithAccessControlIds: async (): Promise<Alert> => {
119
+ return alert;
120
+ },
121
+ props: data.props,
122
+ });
123
+
124
+ const permittedQuery: Query<Alert> =
125
+ await ModelPermission.checkUpdateQueryPermissions(
126
+ Alert,
127
+ {
128
+ _id: data.alertId,
129
+ projectId: data.projectId,
130
+ },
131
+ {
132
+ currentAlertStateId: ObjectID.getZeroObjectID(),
133
+ },
134
+ data.props,
135
+ );
136
+
137
+ const privacyScopedQuery: Query<Alert> = applyAlertSelfPrivacyFilter(
138
+ permittedQuery,
139
+ data.props,
140
+ );
141
+
142
+ const permittedAlert: Alert | null = await AlertService.findOneBy({
143
+ query: privacyScopedQuery,
144
+ select: {
145
+ _id: true,
146
+ },
147
+ props: {
148
+ isRoot: true,
149
+ },
150
+ });
151
+
152
+ if (!permittedAlert) {
153
+ throw new NotAuthorizedException(
154
+ "You do not have permission to update this alert.",
155
+ );
156
+ }
157
+ }
158
+ }