@oneuptime/common 13.0.3 → 13.0.5

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 (687) hide show
  1. package/Models/DatabaseModels/Index.ts +4 -0
  2. package/Models/DatabaseModels/RumApplication.ts +1 -1
  3. package/Models/DatabaseModels/SecurityEventConnection.ts +457 -0
  4. package/Models/DatabaseModels/SecurityEventConnectionRun.ts +263 -0
  5. package/Models/DatabaseModels/TelemetryIngestionKey.ts +2 -2
  6. package/Server/API/AIInvestigationAPI.ts +484 -24
  7. package/Server/API/GoogleSecOpsConnectionAPI.ts +249 -8
  8. package/Server/API/SecurityEventConnectionAPI.ts +250 -0
  9. package/Server/API/TelemetryAPI.ts +402 -265
  10. package/Server/Infrastructure/Postgres/SchemaMigrations/1792700000000-AddSecurityEventConnections.ts +99 -0
  11. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
  12. package/Server/Middleware/TelemetryIngest.ts +101 -6
  13. package/Server/Services/ExceptionAggregationService.ts +7 -16
  14. package/Server/Services/GoogleSecOpsConnectionService.ts +15 -0
  15. package/Server/Services/Index.ts +4 -0
  16. package/Server/Services/InventoryItemService.ts +28 -0
  17. package/Server/Services/LogAggregationService.ts +17 -23
  18. package/Server/Services/MetricAggregationService.ts +4 -14
  19. package/Server/Services/SecurityEventConnectionRunService.ts +10 -0
  20. package/Server/Services/SecurityEventConnectionService.ts +538 -0
  21. package/Server/Services/TraceAggregationService.ts +77 -28
  22. package/Server/Types/AnalyticsDatabase/ModelPermission.ts +9 -0
  23. package/Server/Utils/AI/Chat/ObservabilityAssistant.ts +13 -1
  24. package/Server/Utils/AI/SRE/AIInvestigationEngine.ts +15 -1
  25. package/Server/Utils/AI/SRE/InvestigationEvidence.ts +658 -0
  26. package/Server/Utils/AI/SRE/InvestigationReferences.ts +324 -0
  27. package/Server/Utils/AI/Toolbox/Index.ts +38 -0
  28. package/Server/Utils/AI/Toolbox/TimelineTools.ts +9 -2
  29. package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +80 -0
  30. package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +5 -3
  31. package/Server/Utils/SecurityEvent/Connectors/AwsSecurityHub/AwsSecurityHubClient.ts +1108 -0
  32. package/Server/Utils/SecurityEvent/Connectors/AwsSecurityHub/AwsSecurityHubConnector.ts +732 -0
  33. package/Server/Utils/SecurityEvent/Connectors/ConnectorPlatformHealth.ts +495 -0
  34. package/Server/Utils/SecurityEvent/Connectors/CrowdStrikeFalcon/CrowdStrikeFalconClient.ts +689 -0
  35. package/Server/Utils/SecurityEvent/Connectors/CrowdStrikeFalcon/CrowdStrikeFalconConnector.ts +767 -0
  36. package/Server/Utils/SecurityEvent/Connectors/ElasticSecurity/ElasticSecurityClient.ts +571 -0
  37. package/Server/Utils/SecurityEvent/Connectors/ElasticSecurity/ElasticSecurityConnector.ts +827 -0
  38. package/Server/Utils/SecurityEvent/Connectors/MicrosoftDefenderXdr/MicrosoftDefenderXdrClient.ts +610 -0
  39. package/Server/Utils/SecurityEvent/Connectors/MicrosoftDefenderXdr/MicrosoftDefenderXdrConnector.ts +549 -0
  40. package/Server/Utils/SecurityEvent/Connectors/MicrosoftSentinel/MicrosoftSentinelClient.ts +830 -0
  41. package/Server/Utils/SecurityEvent/Connectors/MicrosoftSentinel/MicrosoftSentinelConnector.ts +684 -0
  42. package/Server/Utils/SecurityEvent/Connectors/Okta/OktaClient.ts +487 -0
  43. package/Server/Utils/SecurityEvent/Connectors/Okta/OktaConnector.ts +778 -0
  44. package/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectionPoller.ts +1002 -0
  45. package/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectionRunExecutor.ts +556 -0
  46. package/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectionTester.ts +275 -0
  47. package/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectorRegistry.ts +53 -0
  48. package/Server/Utils/SecurityEvent/Connectors/Splunk/SplunkClient.ts +709 -0
  49. package/Server/Utils/SecurityEvent/Connectors/Splunk/SplunkConnector.ts +596 -0
  50. package/Server/Utils/SecurityEvent/Connectors/Types.ts +191 -0
  51. package/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsClient.ts +353 -65
  52. package/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsConnectionTester.ts +777 -0
  53. package/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsPoller.ts +747 -148
  54. package/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsRunExecutor.ts +48 -2
  55. package/Server/Utils/SecurityEvent/SecurityEventDedupe.ts +81 -0
  56. package/Server/Utils/SessionReplay/SessionReplayReadService.ts +741 -40
  57. package/Server/Utils/StartServer.ts +10 -0
  58. package/Server/Utils/Telemetry/EntityRegistry.ts +8 -0
  59. package/Server/Utils/Telemetry/KubernetesResourceAttributes.ts +108 -0
  60. package/Server/Utils/Telemetry/ResourceEntityFilter.ts +174 -93
  61. package/Server/Utils/Telemetry/ResourceFacetPlanner.ts +251 -0
  62. package/Server/Utils/Telemetry/ResourceFacetResolver.ts +248 -413
  63. package/Server/Utils/Telemetry/ServiceDependencyDiscovery.ts +668 -0
  64. package/Server/Utils/Telemetry/TraceContextPropagation.ts +348 -0
  65. package/Server/Utils/Telemetry.ts +18 -0
  66. package/Tests/App/Dashboard/AIInvestigationHeaderStatus.test.tsx +413 -0
  67. package/Tests/App/Dashboard/AIRootCauseFeedItem.test.tsx +285 -0
  68. package/Tests/App/Dashboard/AffectedResourcesDisplay.test.tsx +242 -0
  69. package/Tests/App/Dashboard/AlertEpisodeViewFields.test.tsx +601 -15
  70. package/Tests/App/Dashboard/ChangeAlertStateAIHeader.test.tsx +835 -0
  71. package/Tests/App/Dashboard/CloudResourceTelemetryScope.test.ts +41 -0
  72. package/Tests/App/Dashboard/ConnectionTestModal.test.tsx +604 -0
  73. package/Tests/App/Dashboard/ConnectorTestReportView.test.tsx +738 -0
  74. package/Tests/App/Dashboard/DashboardLazyPages.test.tsx +153 -6
  75. package/Tests/App/Dashboard/EntityDetailPanel.test.tsx +165 -0
  76. package/Tests/App/Dashboard/EpisodeChangeState.test.tsx +1069 -0
  77. package/Tests/App/Dashboard/EpisodeMembersCard.test.tsx +772 -0
  78. package/Tests/App/Dashboard/EventOverviewPages.test.tsx +2354 -0
  79. package/Tests/App/Dashboard/EventOverviewSkeleton.test.tsx +210 -0
  80. package/Tests/App/Dashboard/EventStatBar.test.tsx +392 -0
  81. package/Tests/App/Dashboard/EventStatusPanel.test.tsx +247 -0
  82. package/Tests/App/Dashboard/ExceptionAIAssistance.test.tsx +606 -0
  83. package/Tests/App/Dashboard/ExceptionBreadcrumbTimeline.test.tsx +273 -0
  84. package/Tests/App/Dashboard/ExceptionDetailSideMenu.test.tsx +176 -0
  85. package/Tests/App/Dashboard/ExceptionOverviewCards.test.tsx +366 -0
  86. package/Tests/App/Dashboard/ExceptionSignalPages.test.tsx +371 -0
  87. package/Tests/App/Dashboard/ExceptionStackFrameViewer.test.tsx +440 -0
  88. package/Tests/App/Dashboard/ExceptionSummary.test.tsx +381 -0
  89. package/Tests/App/Dashboard/ExceptionTriage.test.tsx +306 -0
  90. package/Tests/App/Dashboard/ExceptionsResourceFacets.test.tsx +604 -0
  91. package/Tests/App/Dashboard/ExceptionsViewerEntityChips.test.tsx +706 -0
  92. package/Tests/App/Dashboard/FeedOrdering.test.ts +32 -0
  93. package/Tests/App/Dashboard/GoogleSecOpsConnectionForm.test.tsx +838 -0
  94. package/Tests/App/Dashboard/GoogleSecOpsConnectionsErrors.test.tsx +18 -0
  95. package/Tests/App/Dashboard/GoogleSecOpsDiagnostics.test.tsx +297 -20
  96. package/Tests/App/Dashboard/IncidentEpisodeViewFields.test.tsx +618 -13
  97. package/Tests/App/Dashboard/InfrastructureExplorer.test.tsx +254 -207
  98. package/Tests/App/Dashboard/InfrastructureGraph.test.tsx +303 -135
  99. package/Tests/App/Dashboard/InvestigationEvidenceList.test.tsx +1251 -0
  100. package/Tests/App/Dashboard/InvestigationFeedRefresh.test.tsx +303 -0
  101. package/Tests/App/Dashboard/InvestigationPanel.test.tsx +876 -8
  102. package/Tests/App/Dashboard/InvestigationPanelStatus.test.tsx +35 -0
  103. package/Tests/App/Dashboard/InvestigationReferenceLink.test.tsx +177 -0
  104. package/Tests/App/Dashboard/InvestigationReportView.test.tsx +1295 -0
  105. package/Tests/App/Dashboard/KubernetesImageReferenceView.test.tsx +114 -0
  106. package/Tests/App/Dashboard/KubernetesLogsTabChips.test.tsx +285 -0
  107. package/Tests/App/Dashboard/LogsEntityChipNames.test.tsx +316 -0
  108. package/Tests/App/Dashboard/LogsResourceFacetChips.test.tsx +361 -0
  109. package/Tests/App/Dashboard/ProfileTableEntityNames.test.tsx +432 -0
  110. package/Tests/App/Dashboard/ProfileViewEntityName.test.tsx +250 -0
  111. package/Tests/App/Dashboard/ResourceArchiveSettings.test.tsx +10 -0
  112. package/Tests/App/Dashboard/ResourceLogsTabsBareLayout.test.tsx +700 -0
  113. package/Tests/App/Dashboard/ResourcePageEntityNameProps.test.tsx +803 -0
  114. package/Tests/App/Dashboard/RumSessionReplaySideMenu.test.tsx +11 -1
  115. package/Tests/App/Dashboard/ScheduledMaintenanceChangeState.test.tsx +1026 -0
  116. package/Tests/App/Dashboard/ScheduledMaintenanceFeedRefresh.test.tsx +436 -0
  117. package/Tests/App/Dashboard/ScheduledMaintenanceOverviewPage.test.tsx +1195 -0
  118. package/Tests/App/Dashboard/SecurityEventConnectionFormModal.test.tsx +1022 -0
  119. package/Tests/App/Dashboard/SecurityEventConnectionsTable.test.tsx +905 -0
  120. package/Tests/App/Dashboard/SecurityEventsSideMenu.test.tsx +247 -0
  121. package/Tests/App/Dashboard/ServiceMapGraph.test.tsx +375 -274
  122. package/Tests/App/Dashboard/SessionReplayAuditTable.test.tsx +410 -0
  123. package/Tests/App/Dashboard/TelemetryResourceRetentionSettings.test.tsx +774 -0
  124. package/Tests/App/Dashboard/TopErrorsPanelResourceNames.test.tsx +316 -0
  125. package/Tests/App/Dashboard/TopologyDataLoading.test.tsx +30 -0
  126. package/Tests/App/Dashboard/TopologyPageNavigation.test.tsx +88 -24
  127. package/Tests/App/Dashboard/TraceExplorer.test.tsx +1590 -0
  128. package/Tests/App/Dashboard/TraceWaterfall.test.tsx +488 -0
  129. package/Tests/App/Dashboard/TracesAnalyticsSeriesLabels.test.tsx +367 -0
  130. package/Tests/App/Dashboard/TracesEntityNames.test.tsx +585 -0
  131. package/Tests/App/Dashboard/TracesResourceFacets.test.tsx +613 -0
  132. package/Tests/App/Dashboard/UseServiceNames.test.tsx +359 -0
  133. package/Tests/Models/DatabaseModels/DomainRoleTierCoverage.test.ts +7 -0
  134. package/Tests/Models/InventoryItemNaming.test.ts +132 -16
  135. package/Tests/Models/SecurityDomainAccessControl.test.ts +61 -17
  136. package/Tests/Server/API/AIInvestigationAPI.test.ts +611 -2
  137. package/Tests/Server/API/AIInvestigationEvidenceAPI.test.ts +1098 -0
  138. package/Tests/Server/API/GoogleSecOpsConnectionAPI.test.ts +409 -0
  139. package/Tests/Server/API/SecurityEventConnectionAPI.test.ts +856 -0
  140. package/Tests/Server/API/SessionReplayAPI.test.ts +528 -1
  141. package/Tests/Server/API/TelemetryFacetsAPI.test.ts +810 -0
  142. package/Tests/Server/Infrastructure/E2EClickHouseFixtureAccess.test.ts +240 -0
  143. package/Tests/Server/Middleware/TelemetryIngestBrowserKey.test.ts +229 -0
  144. package/Tests/Server/Services/InventoryItemDisplayName.test.ts +66 -0
  145. package/Tests/Server/Services/OpenTelemetryResourceRetentionMemo.test.ts +28 -1
  146. package/Tests/Server/Services/SecurityEventConnectionService.test.ts +1233 -0
  147. package/Tests/Server/Services/TelemetryIngestionKeyValidation.test.ts +53 -0
  148. package/Tests/Server/Services/TelemetryResourceFacetCounting.test.ts +218 -0
  149. package/Tests/Server/Services/TelemetryResourceFacetFilters.test.ts +155 -1
  150. package/Tests/Server/Services/TraceAggregationExceptionScope.test.ts +142 -0
  151. package/Tests/Server/Services/TraceAnalyticsResourceDisplayNames.test.ts +85 -24
  152. package/Tests/Server/Types/Database/Permissions/SecurityRoleAccess.test.ts +33 -0
  153. package/Tests/Server/Utils/AI/InvestigationEvidence.test.ts +1395 -0
  154. package/Tests/Server/Utils/AI/InvestigationReferences.test.ts +616 -0
  155. package/Tests/Server/Utils/AI/TimelineTools.test.ts +119 -0
  156. package/Tests/Server/Utils/AI/Toolbox/AIToolbox.test.ts +151 -0
  157. package/Tests/Server/Utils/AnalyticsDatabase/StatementGenerator.test.ts +65 -0
  158. package/Tests/Server/Utils/AnalyticsDatabase/StatementGeneratorExceptionScope.test.ts +224 -0
  159. package/Tests/Server/Utils/LoggerCore.test.ts +597 -0
  160. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorPerSeriesFanout.test.ts +55 -2
  161. package/Tests/Server/Utils/Monitor/MonitorTemplateUtil.test.ts +1109 -0
  162. package/Tests/Server/Utils/SecurityEvent/Connectors/AwsSecurityHub/AwsSecurityHubClient.test.ts +1240 -0
  163. package/Tests/Server/Utils/SecurityEvent/Connectors/AwsSecurityHub/AwsSecurityHubConnector.test.ts +1853 -0
  164. package/Tests/Server/Utils/SecurityEvent/Connectors/ConnectorPlatformHealth.test.ts +1129 -0
  165. package/Tests/Server/Utils/SecurityEvent/Connectors/CrowdStrikeFalcon/CrowdStrikeFalconClient.test.ts +1066 -0
  166. package/Tests/Server/Utils/SecurityEvent/Connectors/CrowdStrikeFalcon/CrowdStrikeFalconConnector.test.ts +1568 -0
  167. package/Tests/Server/Utils/SecurityEvent/Connectors/ElasticSecurity/ElasticSecurityClient.test.ts +930 -0
  168. package/Tests/Server/Utils/SecurityEvent/Connectors/ElasticSecurity/ElasticSecurityConnector.test.ts +1409 -0
  169. package/Tests/Server/Utils/SecurityEvent/Connectors/MicrosoftDefenderXdr/MicrosoftDefenderXdrClient.test.ts +763 -0
  170. package/Tests/Server/Utils/SecurityEvent/Connectors/MicrosoftDefenderXdr/MicrosoftDefenderXdrConnector.test.ts +1296 -0
  171. package/Tests/Server/Utils/SecurityEvent/Connectors/MicrosoftSentinel/MicrosoftSentinelClient.test.ts +1102 -0
  172. package/Tests/Server/Utils/SecurityEvent/Connectors/MicrosoftSentinel/MicrosoftSentinelConnector.test.ts +1174 -0
  173. package/Tests/Server/Utils/SecurityEvent/Connectors/Okta/OktaClient.test.ts +1032 -0
  174. package/Tests/Server/Utils/SecurityEvent/Connectors/Okta/OktaConnector.test.ts +1558 -0
  175. package/Tests/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectionPoller.test.ts +3041 -0
  176. package/Tests/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectionRunExecutor.test.ts +1168 -0
  177. package/Tests/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectionTester.test.ts +906 -0
  178. package/Tests/Server/Utils/SecurityEvent/Connectors/Splunk/SplunkClient.test.ts +1068 -0
  179. package/Tests/Server/Utils/SecurityEvent/Connectors/Splunk/SplunkConnector.test.ts +1004 -0
  180. package/Tests/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsConnectionTester.test.ts +784 -0
  181. package/Tests/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsRunExecutor.test.ts +113 -1
  182. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsClient.test.ts +5 -1
  183. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsDiagnostics.test.ts +93 -13
  184. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsHttpIntegration.test.ts +23 -1
  185. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsPoller.test.ts +25 -3
  186. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsPollerFailureTaxonomy.test.ts +22 -8
  187. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsPollerHardening.test.ts +82 -16
  188. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsSearchDetections.test.ts +730 -0
  189. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsThreePassPoller.test.ts +1577 -0
  190. package/Tests/Server/Utils/SessionReplay/SessionReplayOriginAllowlist.test.ts +41 -0
  191. package/Tests/Server/Utils/SessionReplay/SessionReplayReadServiceQueries.test.ts +1453 -18
  192. package/Tests/Server/Utils/Telemetry/EntityRegistry.test.ts +711 -0
  193. package/Tests/Server/Utils/Telemetry/KubernetesResourceAttributes.test.ts +162 -0
  194. package/Tests/Server/Utils/Telemetry/ResourceEntityFilter.test.ts +460 -13
  195. package/Tests/Server/Utils/Telemetry/ResourceFacetPlanner.test.ts +678 -0
  196. package/Tests/Server/Utils/Telemetry/ResourceFacetResolver.test.ts +868 -0
  197. package/Tests/Server/Utils/Telemetry/ServiceDependencyDiscovery.test.ts +525 -0
  198. package/Tests/Server/Utils/Telemetry/TraceContextPropagation.test.ts +420 -0
  199. package/Tests/Types/OnCallDutyPolicy/LayerUtilCorePaths.test.ts +1014 -0
  200. package/Tests/Types/OnCallDutyPolicy/LayerUtilExhaustiveRotation.test.ts +9 -6
  201. package/Tests/Types/Rules/RuleCriteriaFieldRegistry.test.ts +276 -0
  202. package/Tests/Types/Rum/SessionReplayApiContracts.test.ts +73 -0
  203. package/Tests/Types/Rum/SessionReplayTransportContracts.test.ts +95 -0
  204. package/Tests/Types/SecurityEvent/Connectors/ConnectorDiagnostics.test.ts +82 -0
  205. package/Tests/Types/SecurityEvent/Connectors/SecurityEventConnectorCatalog.test.ts +314 -0
  206. package/Tests/Types/Telemetry/ExceptionSpanScope.test.ts +89 -0
  207. package/Tests/Types/Telemetry/LlmConventions.test.ts +495 -0
  208. package/Tests/Types/Telemetry/ResourceEntityFacet.test.ts +96 -0
  209. package/Tests/Types/Telemetry/ResourceFacetCatalog.test.ts +304 -0
  210. package/Tests/UI/Components/ActiveFilterChipsLockedFilters.test.tsx +357 -0
  211. package/Tests/UI/Components/Card.test.tsx +312 -1
  212. package/Tests/UI/Components/CardModelDetailHeaderLayout.test.tsx +253 -0
  213. package/Tests/UI/Components/CardModelDetailRefresher.test.tsx +342 -0
  214. package/Tests/UI/Components/Checkbox.test.tsx +79 -0
  215. package/Tests/UI/Components/CopyTextButton.test.tsx +142 -0
  216. package/Tests/UI/Components/DetailCompactStyle.test.tsx +621 -0
  217. package/Tests/UI/Components/ErrorBoundary.test.tsx +60 -0
  218. package/Tests/UI/Components/FacetSections.test.tsx +581 -0
  219. package/Tests/UI/Components/FacetSidebarControls.test.tsx +812 -0
  220. package/Tests/UI/Components/FacetVisibility.test.ts +429 -0
  221. package/Tests/UI/Components/LockedFilterActions.test.tsx +396 -0
  222. package/Tests/UI/Components/LockedFilterChip.test.tsx +651 -0
  223. package/Tests/UI/Components/LogsEntityNames.test.ts +1769 -0
  224. package/Tests/UI/Components/LogsEntityNamesComponents.test.tsx +696 -0
  225. package/Tests/UI/Components/LogsFacetSidebarHiddenFacets.test.tsx +600 -0
  226. package/Tests/UI/Components/LogsViewerEntityNames.test.tsx +713 -0
  227. package/Tests/UI/Components/MarkdownInlineReferences.test.tsx +2079 -0
  228. package/Tests/UI/Components/MemberRoleAssignment.test.tsx +446 -0
  229. package/Tests/UI/Components/ModelDetailRefetch.test.tsx +397 -0
  230. package/Tests/UI/Components/ModelListRefetch.test.tsx +233 -0
  231. package/Tests/UI/Components/ModelPageModelChange.test.tsx +409 -0
  232. package/Tests/UI/Components/PaginationIntegration.test.tsx +25 -0
  233. package/Tests/UI/Components/ResourceFacetConfigs.test.ts +172 -0
  234. package/Tests/UI/Components/SavedViewsSidebarIntegration.test.tsx +32 -14
  235. package/Tests/UI/Components/TableSortAndMobileColumns.test.tsx +44 -1
  236. package/Tests/UI/Components/TelemetryActiveFilterChipsLockedFilters.test.tsx +304 -0
  237. package/Tests/UI/Components/TelemetryFacetSidebarHiddenFacets.test.tsx +718 -0
  238. package/Tests/UI/Monitor/EvaluationLogList.test.tsx +303 -0
  239. package/Tests/UI/Monitor/MonitorSummarySnapshotRender.test.tsx +55 -1
  240. package/Tests/UI/Rum/FidelityNotices.test.ts +22 -0
  241. package/Tests/UI/Rum/RecordingHealthCard.test.tsx +120 -136
  242. package/Tests/UI/Rum/RecordingHealthDashboard.test.tsx +988 -0
  243. package/Tests/UI/Rum/ReplayCard.test.tsx +32 -0
  244. package/Tests/UI/Rum/ReplayCorrelationPanel.test.tsx +709 -12
  245. package/Tests/UI/Rum/ReplayHeader.test.tsx +50 -1
  246. package/Tests/UI/Rum/ReplayPinControl.test.tsx +67 -10
  247. package/Tests/UI/Rum/ReplayPlayerChrome.test.tsx +41 -12
  248. package/Tests/UI/Rum/ReplayStage.test.tsx +663 -1
  249. package/Tests/UI/Rum/ReplayStageOverlays.test.tsx +173 -0
  250. package/Tests/UI/Rum/ReplayUi.test.tsx +189 -2
  251. package/Tests/UI/Rum/ReplayUserSessionsMenu.test.tsx +91 -0
  252. package/Tests/UI/Rum/SessionReplayEmptyState.test.tsx +1 -1
  253. package/Tests/UI/Rum/SessionReplaySettingsPolicyLoading.test.tsx +585 -0
  254. package/Tests/UI/Rum/SessionReplayTable.test.tsx +844 -51
  255. package/Tests/UI/Rum/SessionReplayUsersTable.test.tsx +29 -1
  256. package/Tests/UI/Telemetry/TelemetryDetailPanel.test.tsx +288 -0
  257. package/Tests/UI/Utils/Breadcrumb/fixtures/RealBreadcrumbTrails.ts +31 -1
  258. package/Tests/UI/Utils/Breadcrumb/fixtures/RealRoutePatterns.ts +6 -0
  259. package/Tests/UI/Utils/Clipboard.test.ts +154 -0
  260. package/Tests/UI/Utils/Navigation.test.ts +24 -0
  261. package/Tests/UI/Utils/Telemetry/TelemetryEntityNames.test.ts +2205 -0
  262. package/Tests/UI/Utils/Telemetry/UseTelemetryEntityNames.test.tsx +844 -0
  263. package/Tests/Utils/AI/InvestigationReport.test.ts +2430 -0
  264. package/Tests/Utils/APIOutgoingRequestTracer.test.ts +137 -0
  265. package/Tests/Utils/Rum/SessionReplayHealth.test.ts +884 -0
  266. package/Tests/Utils/Rum/SessionReplayHealthDiagnosis.test.ts +40 -0
  267. package/Tests/Utils/Rum/SessionReplayRecordingEnded.test.ts +619 -0
  268. package/Tests/Utils/SecurityEvent/Connectors/AwsSecurityHubNormalizer.test.ts +631 -0
  269. package/Tests/Utils/SecurityEvent/Connectors/CrowdStrikeFalconNormalizer.test.ts +479 -0
  270. package/Tests/Utils/SecurityEvent/Connectors/ElasticSecurityNormalizer.test.ts +507 -0
  271. package/Tests/Utils/SecurityEvent/Connectors/MicrosoftDefenderXdrNormalizer.test.ts +686 -0
  272. package/Tests/Utils/SecurityEvent/Connectors/MicrosoftSentinelNormalizer.test.ts +474 -0
  273. package/Tests/Utils/SecurityEvent/Connectors/OktaNormalizer.test.ts +736 -0
  274. package/Tests/Utils/SecurityEvent/Connectors/SplunkNormalizer.test.ts +407 -0
  275. package/Tests/Utils/SecurityEvent/GoogleSecOpsAlertNormalizer.test.ts +129 -0
  276. package/Tests/Utils/Telemetry/CrossSignalScope.test.ts +57 -1
  277. package/Tests/Utils/Telemetry/EntityRelationship.test.ts +36 -0
  278. package/Tests/Utils/Telemetry/LockedFilterSearch.test.ts +600 -0
  279. package/Tests/Utils/Telemetry/NetworkHost.test.ts +48 -0
  280. package/Tests/Utils/Telemetry/OriginAllowList.test.ts +154 -0
  281. package/Tests/Utils/Traces/CriticalPath.test.ts +25 -0
  282. package/Types/AI/AIChatTypes.ts +8 -0
  283. package/Types/AI/InvestigationEvidence.ts +96 -0
  284. package/Types/Monitor/MonitorEvaluationSummary.ts +5 -0
  285. package/Types/OnCallDutyPolicy/Layer.ts +12 -0
  286. package/Types/Permission.ts +2 -2
  287. package/Types/Rules/RuleCriteriaFieldRegistry.ts +12 -0
  288. package/Types/Rum/SessionReplay.ts +109 -0
  289. package/Types/Rum/SessionReplayApi.ts +31 -0
  290. package/Types/SecurityEvent/Connectors/ConnectorDiagnostics.ts +111 -0
  291. package/Types/SecurityEvent/Connectors/SecurityEventConnectionDiagnostics.ts +87 -0
  292. package/Types/SecurityEvent/Connectors/SecurityEventConnectorCatalog.ts +492 -0
  293. package/Types/SecurityEvent/Connectors/SecurityEventConnectorProvider.ts +41 -0
  294. package/Types/SecurityEvent/GoogleSecOpsDiagnostics.ts +56 -1
  295. package/Types/SecurityEvent/GoogleSecOpsRegion.ts +34 -0
  296. package/Types/Telemetry/EntityType.ts +14 -0
  297. package/Types/Telemetry/ExceptionSpanScope.ts +83 -0
  298. package/Types/Telemetry/LockedFilterDetail.ts +46 -0
  299. package/Types/Telemetry/ResourceEntityFacet.ts +17 -10
  300. package/Types/Telemetry/ResourceFacetCatalog.ts +171 -0
  301. package/UI/Components/Card/Card.tsx +118 -83
  302. package/UI/Components/Checkbox/Checkbox.tsx +8 -5
  303. package/UI/Components/CopyTextButton/CopyTextButton.tsx +7 -1
  304. package/UI/Components/CustomFields/CustomFieldsDetail.tsx +4 -1
  305. package/UI/Components/Detail/Detail.tsx +137 -22
  306. package/UI/Components/ErrorBoundary.tsx +38 -0
  307. package/UI/Components/LogsViewer/LogsEntityNames.ts +912 -0
  308. package/UI/Components/LogsViewer/LogsViewer.tsx +156 -77
  309. package/UI/Components/LogsViewer/components/ActiveFilterChips.tsx +28 -11
  310. package/UI/Components/LogsViewer/components/FacetSection.tsx +66 -79
  311. package/UI/Components/LogsViewer/components/FacetValueRow.tsx +17 -2
  312. package/UI/Components/LogsViewer/components/LogDetailsPanel.tsx +45 -5
  313. package/UI/Components/LogsViewer/components/LogsAnalyticsView.tsx +100 -19
  314. package/UI/Components/LogsViewer/components/LogsFacetSidebar.tsx +156 -39
  315. package/UI/Components/LogsViewer/components/LogsTable.tsx +30 -8
  316. package/UI/Components/LogsViewer/types.ts +6 -0
  317. package/UI/Components/Markdown.tsx/InlineReferences.tsx +628 -0
  318. package/UI/Components/Markdown.tsx/MarkdownViewer.tsx +385 -312
  319. package/UI/Components/MemberRoleAssignment/MemberRoleAssignment.tsx +50 -22
  320. package/UI/Components/ModelDetail/CardModelDetail.tsx +15 -0
  321. package/UI/Components/ModelDetail/ModelDetail.tsx +64 -19
  322. package/UI/Components/ModelList/ModelList.tsx +33 -8
  323. package/UI/Components/Page/ModelPage.tsx +119 -54
  324. package/UI/Components/Table/Table.tsx +8 -0
  325. package/UI/Components/Table/TableRow.tsx +13 -5
  326. package/UI/Components/TelemetryViewer/FacetVisibility.ts +229 -0
  327. package/UI/Components/TelemetryViewer/ResourceFacetConfigs.ts +46 -0
  328. package/UI/Components/TelemetryViewer/TelemetryViewer.tsx +11 -0
  329. package/UI/Components/TelemetryViewer/components/FacetSearchInput.tsx +79 -0
  330. package/UI/Components/TelemetryViewer/components/FacetSectionHeader.tsx +69 -0
  331. package/UI/Components/TelemetryViewer/components/FacetShowMoreButton.tsx +36 -0
  332. package/UI/Components/TelemetryViewer/components/HiddenFacetsFooter.tsx +88 -0
  333. package/UI/Components/TelemetryViewer/components/LockedFilterActions.tsx +205 -0
  334. package/UI/Components/TelemetryViewer/components/LockedFilterChip.tsx +496 -0
  335. package/UI/Components/TelemetryViewer/components/TelemetryActiveFilterChips.tsx +30 -10
  336. package/UI/Components/TelemetryViewer/components/TelemetryDetailPanel.tsx +203 -10
  337. package/UI/Components/TelemetryViewer/components/TelemetryFacetSection.tsx +60 -73
  338. package/UI/Components/TelemetryViewer/components/TelemetryFacetSidebar.tsx +94 -7
  339. package/UI/Components/TelemetryViewer/components/TelemetryFacetValueRow.tsx +17 -2
  340. package/UI/Components/TelemetryViewer/types.ts +19 -0
  341. package/UI/Components/TelemetryViewer/useFacetSearchExemptions.ts +158 -0
  342. package/UI/Components/TelemetryViewer/useFacetSectionSearch.ts +102 -0
  343. package/UI/Utils/Clipboard.ts +68 -2
  344. package/UI/Utils/Navigation.ts +17 -2
  345. package/UI/Utils/Telemetry/TelemetryEntityNames.ts +620 -0
  346. package/UI/Utils/Telemetry/UseTelemetryEntityNames.ts +151 -0
  347. package/Utils/AI/InvestigationReport.ts +1789 -0
  348. package/Utils/API.ts +90 -1
  349. package/Utils/Rum/SessionReplayHealth.ts +11 -0
  350. package/Utils/Rum/SessionReplayRecordingEnded.ts +134 -0
  351. package/Utils/SecurityEvent/Connectors/AwsSecurityHubNormalizer.ts +427 -0
  352. package/Utils/SecurityEvent/Connectors/CrowdStrikeFalconNormalizer.ts +197 -0
  353. package/Utils/SecurityEvent/Connectors/ElasticSecurityNormalizer.ts +464 -0
  354. package/Utils/SecurityEvent/Connectors/MicrosoftDefenderXdrNormalizer.ts +567 -0
  355. package/Utils/SecurityEvent/Connectors/MicrosoftSentinelNormalizer.ts +299 -0
  356. package/Utils/SecurityEvent/Connectors/OktaNormalizer.ts +467 -0
  357. package/Utils/SecurityEvent/Connectors/SplunkNormalizer.ts +439 -0
  358. package/Utils/SecurityEvent/GoogleSecOpsAlertNormalizer.ts +124 -12
  359. package/Utils/Telemetry/CrossSignalScope.ts +22 -8
  360. package/Utils/Telemetry/EntityRelationship.ts +8 -0
  361. package/Utils/Telemetry/LockedFilterSearch.ts +412 -0
  362. package/Utils/Telemetry/NetworkHost.ts +43 -0
  363. package/Utils/Telemetry/OriginAllowList.ts +139 -4
  364. package/Utils/Traces/CriticalPath.ts +10 -4
  365. package/build/dist/Models/DatabaseModels/Index.js +4 -0
  366. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  367. package/build/dist/Models/DatabaseModels/RumApplication.js +1 -1
  368. package/build/dist/Models/DatabaseModels/RumApplication.js.map +1 -1
  369. package/build/dist/Models/DatabaseModels/SecurityEventConnection.js +490 -0
  370. package/build/dist/Models/DatabaseModels/SecurityEventConnection.js.map +1 -0
  371. package/build/dist/Models/DatabaseModels/SecurityEventConnectionRun.js +287 -0
  372. package/build/dist/Models/DatabaseModels/SecurityEventConnectionRun.js.map +1 -0
  373. package/build/dist/Models/DatabaseModels/TelemetryIngestionKey.js +2 -2
  374. package/build/dist/Models/DatabaseModels/TelemetryIngestionKey.js.map +1 -1
  375. package/build/dist/Server/API/AIInvestigationAPI.js +336 -27
  376. package/build/dist/Server/API/AIInvestigationAPI.js.map +1 -1
  377. package/build/dist/Server/API/GoogleSecOpsConnectionAPI.js +182 -7
  378. package/build/dist/Server/API/GoogleSecOpsConnectionAPI.js.map +1 -1
  379. package/build/dist/Server/API/SecurityEventConnectionAPI.js +171 -0
  380. package/build/dist/Server/API/SecurityEventConnectionAPI.js.map +1 -0
  381. package/build/dist/Server/API/TelemetryAPI.js +248 -170
  382. package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
  383. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1792700000000-AddSecurityEventConnections.js +40 -0
  384. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1792700000000-AddSecurityEventConnections.js.map +1 -0
  385. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
  386. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  387. package/build/dist/Server/Middleware/TelemetryIngest.js +57 -4
  388. package/build/dist/Server/Middleware/TelemetryIngest.js.map +1 -1
  389. package/build/dist/Server/Services/ExceptionAggregationService.js +7 -16
  390. package/build/dist/Server/Services/ExceptionAggregationService.js.map +1 -1
  391. package/build/dist/Server/Services/GoogleSecOpsConnectionService.js +8 -0
  392. package/build/dist/Server/Services/GoogleSecOpsConnectionService.js.map +1 -1
  393. package/build/dist/Server/Services/Index.js +4 -0
  394. package/build/dist/Server/Services/Index.js.map +1 -1
  395. package/build/dist/Server/Services/InventoryItemService.js +22 -0
  396. package/build/dist/Server/Services/InventoryItemService.js.map +1 -1
  397. package/build/dist/Server/Services/LogAggregationService.js +8 -17
  398. package/build/dist/Server/Services/LogAggregationService.js.map +1 -1
  399. package/build/dist/Server/Services/MetricAggregationService.js +4 -14
  400. package/build/dist/Server/Services/MetricAggregationService.js.map +1 -1
  401. package/build/dist/Server/Services/SecurityEventConnectionRunService.js +9 -0
  402. package/build/dist/Server/Services/SecurityEventConnectionRunService.js.map +1 -0
  403. package/build/dist/Server/Services/SecurityEventConnectionService.js +356 -0
  404. package/build/dist/Server/Services/SecurityEventConnectionService.js.map +1 -0
  405. package/build/dist/Server/Services/TraceAggregationService.js +51 -25
  406. package/build/dist/Server/Services/TraceAggregationService.js.map +1 -1
  407. package/build/dist/Server/Types/AnalyticsDatabase/ModelPermission.js +9 -0
  408. package/build/dist/Server/Types/AnalyticsDatabase/ModelPermission.js.map +1 -1
  409. package/build/dist/Server/Utils/AI/Chat/ObservabilityAssistant.js +3 -0
  410. package/build/dist/Server/Utils/AI/Chat/ObservabilityAssistant.js.map +1 -1
  411. package/build/dist/Server/Utils/AI/SRE/AIInvestigationEngine.js +14 -7
  412. package/build/dist/Server/Utils/AI/SRE/AIInvestigationEngine.js.map +1 -1
  413. package/build/dist/Server/Utils/AI/SRE/InvestigationEvidence.js +468 -0
  414. package/build/dist/Server/Utils/AI/SRE/InvestigationEvidence.js.map +1 -0
  415. package/build/dist/Server/Utils/AI/SRE/InvestigationReferences.js +230 -0
  416. package/build/dist/Server/Utils/AI/SRE/InvestigationReferences.js.map +1 -0
  417. package/build/dist/Server/Utils/AI/Toolbox/Index.js +32 -0
  418. package/build/dist/Server/Utils/AI/Toolbox/Index.js.map +1 -1
  419. package/build/dist/Server/Utils/AI/Toolbox/TimelineTools.js +6 -2
  420. package/build/dist/Server/Utils/AI/Toolbox/TimelineTools.js.map +1 -1
  421. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +59 -0
  422. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js.map +1 -1
  423. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +5 -3
  424. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
  425. package/build/dist/Server/Utils/SecurityEvent/Connectors/AwsSecurityHub/AwsSecurityHubClient.js +752 -0
  426. package/build/dist/Server/Utils/SecurityEvent/Connectors/AwsSecurityHub/AwsSecurityHubClient.js.map +1 -0
  427. package/build/dist/Server/Utils/SecurityEvent/Connectors/AwsSecurityHub/AwsSecurityHubConnector.js +493 -0
  428. package/build/dist/Server/Utils/SecurityEvent/Connectors/AwsSecurityHub/AwsSecurityHubConnector.js.map +1 -0
  429. package/build/dist/Server/Utils/SecurityEvent/Connectors/ConnectorPlatformHealth.js +352 -0
  430. package/build/dist/Server/Utils/SecurityEvent/Connectors/ConnectorPlatformHealth.js.map +1 -0
  431. package/build/dist/Server/Utils/SecurityEvent/Connectors/CrowdStrikeFalcon/CrowdStrikeFalconClient.js +405 -0
  432. package/build/dist/Server/Utils/SecurityEvent/Connectors/CrowdStrikeFalcon/CrowdStrikeFalconClient.js.map +1 -0
  433. package/build/dist/Server/Utils/SecurityEvent/Connectors/CrowdStrikeFalcon/CrowdStrikeFalconConnector.js +466 -0
  434. package/build/dist/Server/Utils/SecurityEvent/Connectors/CrowdStrikeFalcon/CrowdStrikeFalconConnector.js.map +1 -0
  435. package/build/dist/Server/Utils/SecurityEvent/Connectors/ElasticSecurity/ElasticSecurityClient.js +344 -0
  436. package/build/dist/Server/Utils/SecurityEvent/Connectors/ElasticSecurity/ElasticSecurityClient.js.map +1 -0
  437. package/build/dist/Server/Utils/SecurityEvent/Connectors/ElasticSecurity/ElasticSecurityConnector.js +511 -0
  438. package/build/dist/Server/Utils/SecurityEvent/Connectors/ElasticSecurity/ElasticSecurityConnector.js.map +1 -0
  439. package/build/dist/Server/Utils/SecurityEvent/Connectors/MicrosoftDefenderXdr/MicrosoftDefenderXdrClient.js +406 -0
  440. package/build/dist/Server/Utils/SecurityEvent/Connectors/MicrosoftDefenderXdr/MicrosoftDefenderXdrClient.js.map +1 -0
  441. package/build/dist/Server/Utils/SecurityEvent/Connectors/MicrosoftDefenderXdr/MicrosoftDefenderXdrConnector.js +336 -0
  442. package/build/dist/Server/Utils/SecurityEvent/Connectors/MicrosoftDefenderXdr/MicrosoftDefenderXdrConnector.js.map +1 -0
  443. package/build/dist/Server/Utils/SecurityEvent/Connectors/MicrosoftSentinel/MicrosoftSentinelClient.js +474 -0
  444. package/build/dist/Server/Utils/SecurityEvent/Connectors/MicrosoftSentinel/MicrosoftSentinelClient.js.map +1 -0
  445. package/build/dist/Server/Utils/SecurityEvent/Connectors/MicrosoftSentinel/MicrosoftSentinelConnector.js +417 -0
  446. package/build/dist/Server/Utils/SecurityEvent/Connectors/MicrosoftSentinel/MicrosoftSentinelConnector.js.map +1 -0
  447. package/build/dist/Server/Utils/SecurityEvent/Connectors/Okta/OktaClient.js +314 -0
  448. package/build/dist/Server/Utils/SecurityEvent/Connectors/Okta/OktaClient.js.map +1 -0
  449. package/build/dist/Server/Utils/SecurityEvent/Connectors/Okta/OktaConnector.js +494 -0
  450. package/build/dist/Server/Utils/SecurityEvent/Connectors/Okta/OktaConnector.js.map +1 -0
  451. package/build/dist/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectionPoller.js +719 -0
  452. package/build/dist/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectionPoller.js.map +1 -0
  453. package/build/dist/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectionRunExecutor.js +404 -0
  454. package/build/dist/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectionRunExecutor.js.map +1 -0
  455. package/build/dist/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectionTester.js +209 -0
  456. package/build/dist/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectionTester.js.map +1 -0
  457. package/build/dist/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectorRegistry.js +40 -0
  458. package/build/dist/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectorRegistry.js.map +1 -0
  459. package/build/dist/Server/Utils/SecurityEvent/Connectors/Splunk/SplunkClient.js +494 -0
  460. package/build/dist/Server/Utils/SecurityEvent/Connectors/Splunk/SplunkClient.js.map +1 -0
  461. package/build/dist/Server/Utils/SecurityEvent/Connectors/Splunk/SplunkConnector.js +379 -0
  462. package/build/dist/Server/Utils/SecurityEvent/Connectors/Splunk/SplunkConnector.js.map +1 -0
  463. package/build/dist/Server/Utils/SecurityEvent/Connectors/Types.js +27 -0
  464. package/build/dist/Server/Utils/SecurityEvent/Connectors/Types.js.map +1 -0
  465. package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsClient.js +234 -61
  466. package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsClient.js.map +1 -1
  467. package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsConnectionTester.js +537 -0
  468. package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsConnectionTester.js.map +1 -0
  469. package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsPoller.js +572 -115
  470. package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsPoller.js.map +1 -1
  471. package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsRunExecutor.js +35 -1
  472. package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsRunExecutor.js.map +1 -1
  473. package/build/dist/Server/Utils/SecurityEvent/SecurityEventDedupe.js +52 -0
  474. package/build/dist/Server/Utils/SecurityEvent/SecurityEventDedupe.js.map +1 -0
  475. package/build/dist/Server/Utils/SessionReplay/SessionReplayReadService.js +521 -32
  476. package/build/dist/Server/Utils/SessionReplay/SessionReplayReadService.js.map +1 -1
  477. package/build/dist/Server/Utils/StartServer.js +10 -0
  478. package/build/dist/Server/Utils/StartServer.js.map +1 -1
  479. package/build/dist/Server/Utils/Telemetry/EntityRegistry.js +8 -0
  480. package/build/dist/Server/Utils/Telemetry/EntityRegistry.js.map +1 -1
  481. package/build/dist/Server/Utils/Telemetry/KubernetesResourceAttributes.js +80 -0
  482. package/build/dist/Server/Utils/Telemetry/KubernetesResourceAttributes.js.map +1 -0
  483. package/build/dist/Server/Utils/Telemetry/ResourceEntityFilter.js +122 -67
  484. package/build/dist/Server/Utils/Telemetry/ResourceEntityFilter.js.map +1 -1
  485. package/build/dist/Server/Utils/Telemetry/ResourceFacetPlanner.js +146 -0
  486. package/build/dist/Server/Utils/Telemetry/ResourceFacetPlanner.js.map +1 -0
  487. package/build/dist/Server/Utils/Telemetry/ResourceFacetResolver.js +174 -248
  488. package/build/dist/Server/Utils/Telemetry/ResourceFacetResolver.js.map +1 -1
  489. package/build/dist/Server/Utils/Telemetry/ServiceDependencyDiscovery.js +492 -0
  490. package/build/dist/Server/Utils/Telemetry/ServiceDependencyDiscovery.js.map +1 -0
  491. package/build/dist/Server/Utils/Telemetry/TraceContextPropagation.js +259 -0
  492. package/build/dist/Server/Utils/Telemetry/TraceContextPropagation.js.map +1 -0
  493. package/build/dist/Server/Utils/Telemetry.js +11 -5
  494. package/build/dist/Server/Utils/Telemetry.js.map +1 -1
  495. package/build/dist/Types/AI/InvestigationEvidence.js +2 -0
  496. package/build/dist/Types/AI/InvestigationEvidence.js.map +1 -0
  497. package/build/dist/Types/OnCallDutyPolicy/Layer.js +11 -0
  498. package/build/dist/Types/OnCallDutyPolicy/Layer.js.map +1 -1
  499. package/build/dist/Types/Permission.js +2 -2
  500. package/build/dist/Types/Permission.js.map +1 -1
  501. package/build/dist/Types/Rules/RuleCriteriaFieldRegistry.js +9 -0
  502. package/build/dist/Types/Rules/RuleCriteriaFieldRegistry.js.map +1 -1
  503. package/build/dist/Types/Rum/SessionReplay.js +95 -0
  504. package/build/dist/Types/Rum/SessionReplay.js.map +1 -1
  505. package/build/dist/Types/Rum/SessionReplayApi.js.map +1 -1
  506. package/build/dist/Types/SecurityEvent/Connectors/ConnectorDiagnostics.js +18 -0
  507. package/build/dist/Types/SecurityEvent/Connectors/ConnectorDiagnostics.js.map +1 -0
  508. package/build/dist/Types/SecurityEvent/Connectors/SecurityEventConnectionDiagnostics.js +2 -0
  509. package/build/dist/Types/SecurityEvent/Connectors/SecurityEventConnectionDiagnostics.js.map +1 -0
  510. package/build/dist/Types/SecurityEvent/Connectors/SecurityEventConnectorCatalog.js +374 -0
  511. package/build/dist/Types/SecurityEvent/Connectors/SecurityEventConnectorCatalog.js.map +1 -0
  512. package/build/dist/Types/SecurityEvent/Connectors/SecurityEventConnectorProvider.js +35 -0
  513. package/build/dist/Types/SecurityEvent/Connectors/SecurityEventConnectorProvider.js.map +1 -0
  514. package/build/dist/Types/SecurityEvent/GoogleSecOpsRegion.js +35 -0
  515. package/build/dist/Types/SecurityEvent/GoogleSecOpsRegion.js.map +1 -0
  516. package/build/dist/Types/Telemetry/EntityType.js +14 -0
  517. package/build/dist/Types/Telemetry/EntityType.js.map +1 -1
  518. package/build/dist/Types/Telemetry/ExceptionSpanScope.js +59 -0
  519. package/build/dist/Types/Telemetry/ExceptionSpanScope.js.map +1 -0
  520. package/build/dist/Types/Telemetry/LockedFilterDetail.js +16 -0
  521. package/build/dist/Types/Telemetry/LockedFilterDetail.js.map +1 -0
  522. package/build/dist/Types/Telemetry/ResourceEntityFacet.js +16 -10
  523. package/build/dist/Types/Telemetry/ResourceEntityFacet.js.map +1 -1
  524. package/build/dist/Types/Telemetry/ResourceFacetCatalog.js +119 -0
  525. package/build/dist/Types/Telemetry/ResourceFacetCatalog.js.map +1 -0
  526. package/build/dist/UI/Components/Card/Card.js +34 -17
  527. package/build/dist/UI/Components/Card/Card.js.map +1 -1
  528. package/build/dist/UI/Components/Checkbox/Checkbox.js +6 -4
  529. package/build/dist/UI/Components/Checkbox/Checkbox.js.map +1 -1
  530. package/build/dist/UI/Components/CopyTextButton/CopyTextButton.js +5 -1
  531. package/build/dist/UI/Components/CopyTextButton/CopyTextButton.js.map +1 -1
  532. package/build/dist/UI/Components/CustomFields/CustomFieldsDetail.js +1 -1
  533. package/build/dist/UI/Components/CustomFields/CustomFieldsDetail.js.map +1 -1
  534. package/build/dist/UI/Components/Detail/Detail.js +63 -13
  535. package/build/dist/UI/Components/Detail/Detail.js.map +1 -1
  536. package/build/dist/UI/Components/ErrorBoundary.js +34 -0
  537. package/build/dist/UI/Components/ErrorBoundary.js.map +1 -1
  538. package/build/dist/UI/Components/LogsViewer/LogsEntityNames.js +535 -0
  539. package/build/dist/UI/Components/LogsViewer/LogsEntityNames.js.map +1 -0
  540. package/build/dist/UI/Components/LogsViewer/LogsViewer.js +117 -57
  541. package/build/dist/UI/Components/LogsViewer/LogsViewer.js.map +1 -1
  542. package/build/dist/UI/Components/LogsViewer/components/ActiveFilterChips.js +5 -7
  543. package/build/dist/UI/Components/LogsViewer/components/ActiveFilterChips.js.map +1 -1
  544. package/build/dist/UI/Components/LogsViewer/components/FacetSection.js +31 -44
  545. package/build/dist/UI/Components/LogsViewer/components/FacetSection.js.map +1 -1
  546. package/build/dist/UI/Components/LogsViewer/components/FacetValueRow.js +17 -3
  547. package/build/dist/UI/Components/LogsViewer/components/FacetValueRow.js.map +1 -1
  548. package/build/dist/UI/Components/LogsViewer/components/LogDetailsPanel.js +19 -3
  549. package/build/dist/UI/Components/LogsViewer/components/LogDetailsPanel.js.map +1 -1
  550. package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js +64 -18
  551. package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js.map +1 -1
  552. package/build/dist/UI/Components/LogsViewer/components/LogsFacetSidebar.js +100 -41
  553. package/build/dist/UI/Components/LogsViewer/components/LogsFacetSidebar.js.map +1 -1
  554. package/build/dist/UI/Components/LogsViewer/components/LogsTable.js +13 -5
  555. package/build/dist/UI/Components/LogsViewer/components/LogsTable.js.map +1 -1
  556. package/build/dist/UI/Components/LogsViewer/types.js.map +1 -1
  557. package/build/dist/UI/Components/Markdown.tsx/InlineReferences.js +403 -0
  558. package/build/dist/UI/Components/Markdown.tsx/InlineReferences.js.map +1 -0
  559. package/build/dist/UI/Components/Markdown.tsx/MarkdownViewer.js +197 -179
  560. package/build/dist/UI/Components/Markdown.tsx/MarkdownViewer.js.map +1 -1
  561. package/build/dist/UI/Components/MemberRoleAssignment/MemberRoleAssignment.js +30 -21
  562. package/build/dist/UI/Components/MemberRoleAssignment/MemberRoleAssignment.js.map +1 -1
  563. package/build/dist/UI/Components/ModelDetail/CardModelDetail.js +12 -0
  564. package/build/dist/UI/Components/ModelDetail/CardModelDetail.js.map +1 -1
  565. package/build/dist/UI/Components/ModelDetail/ModelDetail.js +46 -12
  566. package/build/dist/UI/Components/ModelDetail/ModelDetail.js.map +1 -1
  567. package/build/dist/UI/Components/ModelList/ModelList.js +26 -7
  568. package/build/dist/UI/Components/ModelList/ModelList.js.map +1 -1
  569. package/build/dist/UI/Components/Page/ModelPage.js +62 -34
  570. package/build/dist/UI/Components/Page/ModelPage.js.map +1 -1
  571. package/build/dist/UI/Components/Table/Table.js +3 -1
  572. package/build/dist/UI/Components/Table/Table.js.map +1 -1
  573. package/build/dist/UI/Components/Table/TableRow.js +8 -1
  574. package/build/dist/UI/Components/Table/TableRow.js.map +1 -1
  575. package/build/dist/UI/Components/TelemetryViewer/FacetVisibility.js +111 -0
  576. package/build/dist/UI/Components/TelemetryViewer/FacetVisibility.js.map +1 -0
  577. package/build/dist/UI/Components/TelemetryViewer/ResourceFacetConfigs.js +22 -0
  578. package/build/dist/UI/Components/TelemetryViewer/ResourceFacetConfigs.js.map +1 -0
  579. package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js +1 -1
  580. package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js.map +1 -1
  581. package/build/dist/UI/Components/TelemetryViewer/components/FacetSearchInput.js +36 -0
  582. package/build/dist/UI/Components/TelemetryViewer/components/FacetSearchInput.js.map +1 -0
  583. package/build/dist/UI/Components/TelemetryViewer/components/FacetSectionHeader.js +21 -0
  584. package/build/dist/UI/Components/TelemetryViewer/components/FacetSectionHeader.js.map +1 -0
  585. package/build/dist/UI/Components/TelemetryViewer/components/FacetShowMoreButton.js +14 -0
  586. package/build/dist/UI/Components/TelemetryViewer/components/FacetShowMoreButton.js.map +1 -0
  587. package/build/dist/UI/Components/TelemetryViewer/components/HiddenFacetsFooter.js +27 -0
  588. package/build/dist/UI/Components/TelemetryViewer/components/HiddenFacetsFooter.js.map +1 -0
  589. package/build/dist/UI/Components/TelemetryViewer/components/LockedFilterActions.js +88 -0
  590. package/build/dist/UI/Components/TelemetryViewer/components/LockedFilterActions.js.map +1 -0
  591. package/build/dist/UI/Components/TelemetryViewer/components/LockedFilterChip.js +228 -0
  592. package/build/dist/UI/Components/TelemetryViewer/components/LockedFilterChip.js.map +1 -0
  593. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryActiveFilterChips.js +6 -6
  594. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryActiveFilterChips.js.map +1 -1
  595. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryDetailPanel.js +111 -12
  596. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryDetailPanel.js.map +1 -1
  597. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetSection.js +27 -40
  598. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetSection.js.map +1 -1
  599. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetSidebar.js +70 -10
  600. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetSidebar.js.map +1 -1
  601. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetValueRow.js +17 -3
  602. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetValueRow.js.map +1 -1
  603. package/build/dist/UI/Components/TelemetryViewer/types.js +0 -4
  604. package/build/dist/UI/Components/TelemetryViewer/types.js.map +1 -1
  605. package/build/dist/UI/Components/TelemetryViewer/useFacetSearchExemptions.js +70 -0
  606. package/build/dist/UI/Components/TelemetryViewer/useFacetSearchExemptions.js.map +1 -0
  607. package/build/dist/UI/Components/TelemetryViewer/useFacetSectionSearch.js +52 -0
  608. package/build/dist/UI/Components/TelemetryViewer/useFacetSectionSearch.js.map +1 -0
  609. package/build/dist/UI/Utils/Clipboard.js +59 -2
  610. package/build/dist/UI/Utils/Clipboard.js.map +1 -1
  611. package/build/dist/UI/Utils/Navigation.js +11 -2
  612. package/build/dist/UI/Utils/Navigation.js.map +1 -1
  613. package/build/dist/UI/Utils/Telemetry/TelemetryEntityNames.js +471 -0
  614. package/build/dist/UI/Utils/Telemetry/TelemetryEntityNames.js.map +1 -0
  615. package/build/dist/UI/Utils/Telemetry/UseTelemetryEntityNames.js +113 -0
  616. package/build/dist/UI/Utils/Telemetry/UseTelemetryEntityNames.js.map +1 -0
  617. package/build/dist/Utils/AI/InvestigationReport.js +1241 -0
  618. package/build/dist/Utils/AI/InvestigationReport.js.map +1 -0
  619. package/build/dist/Utils/API.js +25 -0
  620. package/build/dist/Utils/API.js.map +1 -1
  621. package/build/dist/Utils/Rum/SessionReplayHealth.js +10 -0
  622. package/build/dist/Utils/Rum/SessionReplayHealth.js.map +1 -1
  623. package/build/dist/Utils/Rum/SessionReplayRecordingEnded.js +54 -0
  624. package/build/dist/Utils/Rum/SessionReplayRecordingEnded.js.map +1 -0
  625. package/build/dist/Utils/SecurityEvent/Connectors/AwsSecurityHubNormalizer.js +311 -0
  626. package/build/dist/Utils/SecurityEvent/Connectors/AwsSecurityHubNormalizer.js.map +1 -0
  627. package/build/dist/Utils/SecurityEvent/Connectors/CrowdStrikeFalconNormalizer.js +150 -0
  628. package/build/dist/Utils/SecurityEvent/Connectors/CrowdStrikeFalconNormalizer.js.map +1 -0
  629. package/build/dist/Utils/SecurityEvent/Connectors/ElasticSecurityNormalizer.js +328 -0
  630. package/build/dist/Utils/SecurityEvent/Connectors/ElasticSecurityNormalizer.js.map +1 -0
  631. package/build/dist/Utils/SecurityEvent/Connectors/MicrosoftDefenderXdrNormalizer.js +405 -0
  632. package/build/dist/Utils/SecurityEvent/Connectors/MicrosoftDefenderXdrNormalizer.js.map +1 -0
  633. package/build/dist/Utils/SecurityEvent/Connectors/MicrosoftSentinelNormalizer.js +212 -0
  634. package/build/dist/Utils/SecurityEvent/Connectors/MicrosoftSentinelNormalizer.js.map +1 -0
  635. package/build/dist/Utils/SecurityEvent/Connectors/OktaNormalizer.js +351 -0
  636. package/build/dist/Utils/SecurityEvent/Connectors/OktaNormalizer.js.map +1 -0
  637. package/build/dist/Utils/SecurityEvent/Connectors/SplunkNormalizer.js +348 -0
  638. package/build/dist/Utils/SecurityEvent/Connectors/SplunkNormalizer.js.map +1 -0
  639. package/build/dist/Utils/SecurityEvent/GoogleSecOpsAlertNormalizer.js +103 -10
  640. package/build/dist/Utils/SecurityEvent/GoogleSecOpsAlertNormalizer.js.map +1 -1
  641. package/build/dist/Utils/Telemetry/CrossSignalScope.js +16 -5
  642. package/build/dist/Utils/Telemetry/CrossSignalScope.js.map +1 -1
  643. package/build/dist/Utils/Telemetry/EntityRelationship.js +7 -0
  644. package/build/dist/Utils/Telemetry/EntityRelationship.js.map +1 -1
  645. package/build/dist/Utils/Telemetry/LockedFilterSearch.js +277 -0
  646. package/build/dist/Utils/Telemetry/LockedFilterSearch.js.map +1 -0
  647. package/build/dist/Utils/Telemetry/NetworkHost.js +39 -0
  648. package/build/dist/Utils/Telemetry/NetworkHost.js.map +1 -0
  649. package/build/dist/Utils/Telemetry/OriginAllowList.js +105 -4
  650. package/build/dist/Utils/Telemetry/OriginAllowList.js.map +1 -1
  651. package/build/dist/Utils/Traces/CriticalPath.js +9 -4
  652. package/build/dist/Utils/Traces/CriticalPath.js.map +1 -1
  653. package/package.json +1 -1
  654. package/Tests/UI/Rum/RecordingHealthStrip.test.tsx +0 -600
  655. package/UI/Components/GanttChart/Bar/BarLabel.tsx +0 -17
  656. package/UI/Components/GanttChart/Bar/Index.tsx +0 -149
  657. package/UI/Components/GanttChart/ChartContainer.tsx +0 -50
  658. package/UI/Components/GanttChart/Index.tsx +0 -90
  659. package/UI/Components/GanttChart/Row/Index.tsx +0 -22
  660. package/UI/Components/GanttChart/Row/Row.tsx +0 -231
  661. package/UI/Components/GanttChart/Row/RowLabel.tsx +0 -30
  662. package/UI/Components/GanttChart/Rows.tsx +0 -59
  663. package/UI/Components/GanttChart/Timeline/Index.tsx +0 -46
  664. package/UI/Components/GanttChart/Timeline/TimelineInterval.tsx +0 -33
  665. package/UI/Components/GanttChart/Timeline/TimelineIntervalMarks.tsx +0 -43
  666. package/build/dist/UI/Components/GanttChart/Bar/BarLabel.js +0 -8
  667. package/build/dist/UI/Components/GanttChart/Bar/BarLabel.js.map +0 -1
  668. package/build/dist/UI/Components/GanttChart/Bar/Index.js +0 -77
  669. package/build/dist/UI/Components/GanttChart/Bar/Index.js.map +0 -1
  670. package/build/dist/UI/Components/GanttChart/ChartContainer.js +0 -33
  671. package/build/dist/UI/Components/GanttChart/ChartContainer.js.map +0 -1
  672. package/build/dist/UI/Components/GanttChart/Index.js +0 -39
  673. package/build/dist/UI/Components/GanttChart/Index.js.map +0 -1
  674. package/build/dist/UI/Components/GanttChart/Row/Index.js +0 -7
  675. package/build/dist/UI/Components/GanttChart/Row/Index.js.map +0 -1
  676. package/build/dist/UI/Components/GanttChart/Row/Row.js +0 -106
  677. package/build/dist/UI/Components/GanttChart/Row/Row.js.map +0 -1
  678. package/build/dist/UI/Components/GanttChart/Row/RowLabel.js +0 -10
  679. package/build/dist/UI/Components/GanttChart/Row/RowLabel.js.map +0 -1
  680. package/build/dist/UI/Components/GanttChart/Rows.js +0 -19
  681. package/build/dist/UI/Components/GanttChart/Rows.js.map +0 -1
  682. package/build/dist/UI/Components/GanttChart/Timeline/Index.js +0 -16
  683. package/build/dist/UI/Components/GanttChart/Timeline/Index.js.map +0 -1
  684. package/build/dist/UI/Components/GanttChart/Timeline/TimelineInterval.js +0 -13
  685. package/build/dist/UI/Components/GanttChart/Timeline/TimelineInterval.js.map +0 -1
  686. package/build/dist/UI/Components/GanttChart/Timeline/TimelineIntervalMarks.js +0 -13
  687. package/build/dist/UI/Components/GanttChart/Timeline/TimelineIntervalMarks.js.map +0 -1
@@ -0,0 +1,1577 @@
1
+ import GoogleSecOpsConnection from "../../../../Models/DatabaseModels/GoogleSecOpsConnection";
2
+ import Semaphore, {
3
+ SemaphoreLockTimeoutError,
4
+ } from "../../../../Server/Infrastructure/Semaphore";
5
+ import GoogleSecOpsConnectionService from "../../../../Server/Services/GoogleSecOpsConnectionService";
6
+ import OTelIngestService from "../../../../Server/Services/OpenTelemetryIngestService";
7
+ import SecurityEventService from "../../../../Server/Services/SecurityEventService";
8
+ import logger from "../../../../Server/Utils/Logger";
9
+ import GoogleSecOpsClient, {
10
+ FetchAlertsResult,
11
+ GoogleSecOpsListBasis,
12
+ SearchDetectionsResult,
13
+ } from "../../../../Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsClient";
14
+ import GoogleSecOpsPoller from "../../../../Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsPoller";
15
+ import SecurityEventDedupe from "../../../../Server/Utils/SecurityEvent/SecurityEventDedupe";
16
+ import ThreatIntelEnricher from "../../../../Server/Utils/SecurityEvent/ThreatIntel/ThreatIntelEnricher";
17
+ import APIException from "../../../../Types/Exception/ApiException";
18
+ import OneUptimeDate from "../../../../Types/Date";
19
+ import { JSONObject } from "../../../../Types/JSON";
20
+ import ObjectID from "../../../../Types/ObjectID";
21
+ import {
22
+ GoogleSecOpsDiagnosticCheck,
23
+ GoogleSecOpsRunResult,
24
+ } from "../../../../Types/SecurityEvent/GoogleSecOpsDiagnostics";
25
+ import ServiceType from "../../../../Types/Telemetry/ServiceType";
26
+ import { getJestSpyOn } from "../../../Spy";
27
+ import {
28
+ afterEach,
29
+ beforeEach,
30
+ describe,
31
+ expect,
32
+ jest,
33
+ test,
34
+ } from "@jest/globals";
35
+
36
+ /*
37
+ * The three-pass fetch. A scheduled poll reads one window three ways —
38
+ * rule detections by CREATED time, curated rule detections by created
39
+ * time, and the alerts view by detection time — and unions the results by
40
+ * Collection.id. These tests pin the request contract of each pass as
41
+ * seen from the poller, the per-pass request budgets, the union, the
42
+ * per-pass counts, the curated-rule degradation, the 24-hour first
43
+ * window, the creation-lag statistics, and the two bookkeeping rules that
44
+ * changed with it: a rejected object no longer holds the cursor, and a
45
+ * busy source lock is reported in words rather than as a Redis error.
46
+ */
47
+
48
+ const NOW: Date = new Date("2026-09-14T12:00:00.000Z");
49
+ const PROJECT_ID: ObjectID = new ObjectID(
50
+ "11111111-1111-4111-8111-111111111111",
51
+ );
52
+ const CONNECTION_ID: ObjectID = new ObjectID(
53
+ "22222222-2222-4222-8222-222222222222",
54
+ );
55
+
56
+ interface SearchCall {
57
+ startTime: Date;
58
+ endTime: Date;
59
+ listBasis: GoogleSecOpsListBasis;
60
+ alertingOnly: boolean;
61
+ pageSize?: number | undefined;
62
+ pageToken?: string | undefined;
63
+ curated?: boolean | undefined;
64
+ }
65
+
66
+ interface AlertsCall {
67
+ startTime: Date;
68
+ endTime: Date;
69
+ maxAlerts?: number | undefined;
70
+ includeNonAlertingDetections?: boolean | undefined;
71
+ }
72
+
73
+ interface FakeClient {
74
+ client: GoogleSecOpsClient;
75
+ searchCalls: Array<SearchCall>;
76
+ alertsCalls: Array<AlertsCall>;
77
+ }
78
+
79
+ type SearchAnswer =
80
+ | SearchDetectionsResult
81
+ | Error
82
+ | ((call: SearchCall) => SearchDetectionsResult);
83
+
84
+ function page(
85
+ detections: Array<JSONObject>,
86
+ changes: Partial<SearchDetectionsResult> = {},
87
+ ): SearchDetectionsResult {
88
+ return { detections, nextPageToken: null, truncated: false, ...changes };
89
+ }
90
+
91
+ function fetched(
92
+ alerts: Array<JSONObject>,
93
+ changes: Partial<FetchAlertsResult> = {},
94
+ ): FetchAlertsResult {
95
+ return {
96
+ alerts,
97
+ complete: true,
98
+ progress: 1,
99
+ truncatedByCount: false,
100
+ truncatedByBytes: false,
101
+ baselineAlertsCount: alerts.length,
102
+ filteredAlertsCount: alerts.length,
103
+ chunkCount: 1,
104
+ ...changes,
105
+ };
106
+ }
107
+
108
+ function detection(id: string, changes: JSONObject = {}): JSONObject {
109
+ return {
110
+ id,
111
+ type: "RULE_DETECTION",
112
+ detectionTime: "2026-09-14T11:00:00.000Z",
113
+ createdTime: "2026-09-14T11:02:00.000Z",
114
+ detection: [
115
+ { ruleName: `Rule for ${id}`, alertState: "ALERTING", severity: "HIGH" },
116
+ ],
117
+ ...changes,
118
+ };
119
+ }
120
+
121
+ /*
122
+ * Answers are queued per (curated, basis) pair so a test can script the
123
+ * rule pass and the curated pass independently; each queue repeats its
124
+ * last entry, so one entry means "answer this way every time".
125
+ */
126
+ function makeClient(data: {
127
+ rule?: Array<SearchAnswer> | undefined;
128
+ curated?: Array<SearchAnswer> | undefined;
129
+ alerts?: Array<FetchAlertsResult | Error> | undefined;
130
+ }): FakeClient {
131
+ const searchCalls: Array<SearchCall> = [];
132
+ const alertsCalls: Array<AlertsCall> = [];
133
+ const indexes: Map<string, number> = new Map();
134
+ let alertsIndex: number = 0;
135
+
136
+ const client: GoogleSecOpsClient = {
137
+ testAuthentication: jest.fn(async (): Promise<void> => {}),
138
+ searchDetections: jest.fn(
139
+ async (call: SearchCall): Promise<SearchDetectionsResult> => {
140
+ searchCalls.push(call);
141
+ const queue: Array<SearchAnswer> = (call.curated
142
+ ? data.curated
143
+ : data.rule) || [page([])];
144
+ const key: string = `${call.curated ? "curated" : "rule"}:${call.listBasis}`;
145
+ const index: number = indexes.get(key) || 0;
146
+ indexes.set(key, index + 1);
147
+ const answer: SearchAnswer = queue[
148
+ Math.min(index, queue.length - 1)
149
+ ] as SearchAnswer;
150
+
151
+ if (answer instanceof Error) {
152
+ throw answer;
153
+ }
154
+
155
+ return typeof answer === "function" ? answer(call) : answer;
156
+ },
157
+ ),
158
+ fetchDetectionAlerts: jest.fn(
159
+ async (call: AlertsCall): Promise<FetchAlertsResult> => {
160
+ alertsCalls.push(call);
161
+ const queue: Array<FetchAlertsResult | Error> = data.alerts || [
162
+ fetched([]),
163
+ ];
164
+ const answer: FetchAlertsResult | Error = queue[
165
+ Math.min(alertsIndex++, queue.length - 1)
166
+ ] as FetchAlertsResult | Error;
167
+
168
+ if (answer instanceof Error) {
169
+ throw answer;
170
+ }
171
+
172
+ return answer;
173
+ },
174
+ ),
175
+ } as unknown as GoogleSecOpsClient;
176
+
177
+ return { client, searchCalls, alertsCalls };
178
+ }
179
+
180
+ function connection(
181
+ changes: Partial<GoogleSecOpsConnection> = {},
182
+ ): GoogleSecOpsConnection {
183
+ const item: GoogleSecOpsConnection = new GoogleSecOpsConnection();
184
+ item._id = CONNECTION_ID.toString();
185
+ item.projectId = PROJECT_ID;
186
+ item.region = "us";
187
+ item.instanceResourceName = "projects/p/locations/us/instances/i";
188
+ item.serviceAccountJson = "{}";
189
+ item.pollIntervalInMinutes = 5;
190
+ Object.assign(item, changes);
191
+ return item;
192
+ }
193
+
194
+ function lastUpdate(): JSONObject {
195
+ const calls: Array<Array<unknown>> = getJestSpyOn(
196
+ GoogleSecOpsConnectionService,
197
+ "updateOneById",
198
+ ).mock.calls;
199
+ return (calls[calls.length - 1]![0] as { data: JSONObject }).data;
200
+ }
201
+
202
+ function checkNamed(
203
+ result: GoogleSecOpsRunResult,
204
+ name: string,
205
+ ): GoogleSecOpsDiagnosticCheck | undefined {
206
+ return result.checks.find((check: GoogleSecOpsDiagnosticCheck): boolean => {
207
+ return check.name === name;
208
+ });
209
+ }
210
+
211
+ describe("GoogleSecOpsPoller three-pass fetch", () => {
212
+ let insertedRows: Array<JSONObject>;
213
+
214
+ beforeEach(() => {
215
+ insertedRows = [];
216
+ getJestSpyOn(OneUptimeDate, "getCurrentDate").mockReturnValue(NOW);
217
+ getJestSpyOn(Semaphore, "lock").mockResolvedValue({});
218
+ getJestSpyOn(Semaphore, "release").mockResolvedValue(undefined);
219
+ getJestSpyOn(GoogleSecOpsPoller, "findExistingEventUids").mockResolvedValue(
220
+ new Set(),
221
+ );
222
+ getJestSpyOn(SecurityEventService, "insertJsonRows").mockImplementation(((
223
+ rows: Array<JSONObject>,
224
+ ): Promise<void> => {
225
+ insertedRows.push(...rows);
226
+ return Promise.resolve();
227
+ }) as never);
228
+ getJestSpyOn(
229
+ OTelIngestService,
230
+ "telemetryServiceFromName",
231
+ ).mockResolvedValue({
232
+ serviceName: "Google SecOps",
233
+ primaryEntityId: PROJECT_ID,
234
+ primaryEntityType: ServiceType.OpenTelemetry,
235
+ dataRententionInDays: 15,
236
+ serviceRetentionConfig: null,
237
+ serviceRetentionInDays: null,
238
+ projectRetentionConfig: null,
239
+ projectRetentionInDays: 15,
240
+ });
241
+ getJestSpyOn(
242
+ ThreatIntelEnricher,
243
+ "enrichNormalizedEvents",
244
+ ).mockResolvedValue({ eventsMatched: 0, valuesLookedUp: 0 });
245
+ getJestSpyOn(
246
+ GoogleSecOpsConnectionService,
247
+ "updateOneById",
248
+ ).mockResolvedValue(undefined);
249
+ getJestSpyOn(logger, "warn").mockImplementation((): void => {});
250
+ getJestSpyOn(logger, "error").mockImplementation((): void => {});
251
+ });
252
+
253
+ afterEach(() => {
254
+ jest.restoreAllMocks();
255
+ });
256
+
257
+ test("a poll reads three passes over one window and unions them by Collection.id", async () => {
258
+ const fake: FakeClient = makeClient({
259
+ rule: [page([detection("a"), detection("b")])],
260
+ curated: [page([detection("b"), detection("c")])],
261
+ alerts: [fetched([detection("c"), detection("d")])],
262
+ });
263
+
264
+ const result: GoogleSecOpsRunResult =
265
+ await GoogleSecOpsPoller.executeConnection(
266
+ connection(),
267
+ { type: "poll" },
268
+ fake.client,
269
+ );
270
+
271
+ expect(result).toMatchObject({
272
+ status: "success",
273
+ complete: true,
274
+ basis: "created-time",
275
+ fetchedCount: 4,
276
+ ingestedCount: 4,
277
+ requestCount: 3,
278
+ sourceCounts: { ruleDetections: 2, curatedDetections: 2, alertsView: 2 },
279
+ });
280
+ expect(
281
+ insertedRows
282
+ .map((row: JSONObject): unknown => {
283
+ return row["eventUid"];
284
+ })
285
+ .sort(),
286
+ ).toEqual(["a", "b", "c", "d"]);
287
+ expect(
288
+ result.checks.map((check: GoogleSecOpsDiagnosticCheck): string => {
289
+ return `${check.name}:${check.status}`;
290
+ }),
291
+ ).toEqual([
292
+ "Validate configuration:success",
293
+ "Read rule detections by created time:success",
294
+ "Read curated rule detections by created time:success",
295
+ "Read alerts view by detection time:success",
296
+ "Normalize detections:success",
297
+ "Import detections:success",
298
+ ]);
299
+ expect(lastUpdate()["cursor"]).toBe(NOW.toISOString());
300
+ });
301
+
302
+ test("the search passes ask for created time, the saved scope, full pages and the right route", async () => {
303
+ const fake: FakeClient = makeClient({});
304
+
305
+ await GoogleSecOpsPoller.executeConnection(
306
+ connection(),
307
+ { type: "poll" },
308
+ fake.client,
309
+ );
310
+
311
+ expect(fake.searchCalls).toHaveLength(2);
312
+ expect(fake.searchCalls[0]).toMatchObject({
313
+ listBasis: "CREATED_TIME",
314
+ alertingOnly: true,
315
+ pageSize: 1000,
316
+ curated: false,
317
+ });
318
+ expect(fake.searchCalls[0]!.pageToken).toBeUndefined();
319
+ expect(fake.searchCalls[1]).toMatchObject({
320
+ listBasis: "CREATED_TIME",
321
+ alertingOnly: true,
322
+ pageSize: 1000,
323
+ curated: true,
324
+ });
325
+ // All three passes cover exactly the same window.
326
+ for (const call of fake.searchCalls) {
327
+ expect(call.startTime).toEqual(fake.alertsCalls[0]!.startTime);
328
+ expect(call.endTime).toEqual(fake.alertsCalls[0]!.endTime);
329
+ }
330
+ expect(fake.alertsCalls[0]).toMatchObject({
331
+ maxAlerts: 1000,
332
+ includeNonAlertingDetections: false,
333
+ });
334
+ });
335
+
336
+ test("Alerts and detections drops alertState from the searches and widens the alerts view", async () => {
337
+ const fake: FakeClient = makeClient({});
338
+
339
+ await GoogleSecOpsPoller.executeConnection(
340
+ connection({ includeNonAlertingDetections: true }),
341
+ { type: "poll" },
342
+ fake.client,
343
+ );
344
+
345
+ expect(
346
+ fake.searchCalls.every((call: SearchCall): boolean => {
347
+ return call.alertingOnly === false;
348
+ }),
349
+ ).toBe(true);
350
+ expect(fake.alertsCalls[0]!.includeNonAlertingDetections).toBe(true);
351
+ });
352
+
353
+ test("the first poll of a new connection looks back 24 hours by created time", async () => {
354
+ const fake: FakeClient = makeClient({});
355
+
356
+ const result: GoogleSecOpsRunResult =
357
+ await GoogleSecOpsPoller.executeConnection(
358
+ connection(),
359
+ { type: "poll" },
360
+ fake.client,
361
+ );
362
+
363
+ expect(result.windowStart).toBe("2026-09-13T12:00:00.000Z");
364
+ expect(result.windowEnd).toBe(NOW.toISOString());
365
+ expect(fake.searchCalls[0]!.startTime.toISOString()).toBe(
366
+ "2026-09-13T12:00:00.000Z",
367
+ );
368
+ });
369
+
370
+ test("a saved cursor starts the window one minute earlier and ends now", async () => {
371
+ const fake: FakeClient = makeClient({});
372
+
373
+ const result: GoogleSecOpsRunResult =
374
+ await GoogleSecOpsPoller.executeConnection(
375
+ connection({ cursor: "2026-09-14T11:55:00.000Z" }),
376
+ { type: "poll" },
377
+ fake.client,
378
+ );
379
+
380
+ expect(result.windowStart).toBe("2026-09-14T11:54:00.000Z");
381
+ expect(result.windowEnd).toBe(NOW.toISOString());
382
+ expect(lastUpdate()["cursor"]).toBe(NOW.toISOString());
383
+ });
384
+
385
+ test("a stale cursor is caught up in 24 hour chunks", async () => {
386
+ const fake: FakeClient = makeClient({});
387
+
388
+ const result: GoogleSecOpsRunResult =
389
+ await GoogleSecOpsPoller.executeConnection(
390
+ connection({ cursor: "2026-09-10T12:00:00.000Z" }),
391
+ { type: "poll" },
392
+ fake.client,
393
+ );
394
+
395
+ expect(result.windowStart).toBe("2026-09-10T11:59:00.000Z");
396
+ /*
397
+ * Review finding alerts-view-budget-pins-cursor-forever (F1): the chunk
398
+ * is measured from the cursor, not from the overlapped start, so a full
399
+ * 24 hour chunk ends 24 hours after the cursor (it used to end at 11:59).
400
+ */
401
+ expect(result.windowEnd).toBe("2026-09-11T12:00:00.000Z");
402
+ expect(result.chunkMinutes).toBe(24 * 60);
403
+ expect(result.warnings.join(" ")).toMatch(/24 hour windows/);
404
+ });
405
+
406
+ test.each([
407
+ [90, "2026-09-10T13:30:00.000Z"],
408
+ [1, "2026-09-10T12:01:00.000Z"],
409
+ [0, "2026-09-11T12:00:00.000Z"],
410
+ [24 * 60 + 1, "2026-09-11T12:00:00.000Z"],
411
+ [2.5, "2026-09-11T12:00:00.000Z"],
412
+ ["90", "2026-09-11T12:00:00.000Z"],
413
+ [null, "2026-09-11T12:00:00.000Z"],
414
+ ])(
415
+ "a stored nextChunkMinutes of %j sets the chunk only when it is a whole number of minutes in range",
416
+ async (stored: unknown, windowEnd: string) => {
417
+ const fake: FakeClient = makeClient({});
418
+
419
+ const result: GoogleSecOpsRunResult =
420
+ await GoogleSecOpsPoller.executeConnection(
421
+ connection({
422
+ cursor: "2026-09-10T12:00:00.000Z",
423
+ lastPollResult: {
424
+ type: "poll",
425
+ nextChunkMinutes: stored,
426
+ } as unknown as JSONObject,
427
+ }),
428
+ { type: "poll" },
429
+ fake.client,
430
+ );
431
+
432
+ expect(result.windowStart).toBe("2026-09-10T11:59:00.000Z");
433
+ expect(result.windowEnd).toBe(windowEnd);
434
+ expect(lastUpdate()["cursor"]).toBe(windowEnd);
435
+ },
436
+ );
437
+
438
+ test("nextPageToken is followed and the token is forwarded", async () => {
439
+ const fake: FakeClient = makeClient({
440
+ rule: [
441
+ page([detection("p1")], { nextPageToken: "t1" }),
442
+ page([detection("p2")], { nextPageToken: "t2" }),
443
+ page([detection("p3")]),
444
+ ],
445
+ });
446
+
447
+ const result: GoogleSecOpsRunResult =
448
+ await GoogleSecOpsPoller.executeConnection(
449
+ connection(),
450
+ { type: "poll" },
451
+ fake.client,
452
+ );
453
+
454
+ const ruleCalls: Array<SearchCall> = fake.searchCalls.filter(
455
+ (call: SearchCall): boolean => {
456
+ return call.curated !== true;
457
+ },
458
+ );
459
+ expect(
460
+ ruleCalls.map((call: SearchCall): string | undefined => {
461
+ return call.pageToken;
462
+ }),
463
+ ).toEqual([undefined, "t1", "t2"]);
464
+ expect(result).toMatchObject({
465
+ complete: true,
466
+ fetchedCount: 3,
467
+ requestCount: 5,
468
+ sourceCounts: { ruleDetections: 3, curatedDetections: 0, alertsView: 0 },
469
+ });
470
+ });
471
+
472
+ /*
473
+ * Review findings alerts-view-budget-pins-cursor-forever and
474
+ * budget-skipped-passes-reported-success (F1, F1b). The three passes used
475
+ * to share twelve requests, a stopped poll held the cursor on the same
476
+ * window forever with the advice "Narrow the time range", and a curated
477
+ * pass or alerts view that never ran still reported success.
478
+ */
479
+ test("the search passes share a 20 page budget, the alerts view keeps its own, and a stopped poll narrows the next chunk", async () => {
480
+ const fake: FakeClient = makeClient({
481
+ rule: [page([detection("endless")], { nextPageToken: "again" })],
482
+ });
483
+
484
+ const result: GoogleSecOpsRunResult =
485
+ await GoogleSecOpsPoller.executeConnection(
486
+ connection({ cursor: "2026-09-14T11:55:00.000Z" }),
487
+ { type: "poll" },
488
+ fake.client,
489
+ );
490
+
491
+ expect(fake.searchCalls).toHaveLength(20);
492
+ expect(
493
+ fake.searchCalls.every((call: SearchCall): boolean => {
494
+ return call.curated === false;
495
+ }),
496
+ ).toBe(true);
497
+ // The alerts view still ran on its own budget.
498
+ expect(fake.alertsCalls).toHaveLength(1);
499
+ expect(result.requestCount).toBe(21);
500
+ expect(result.status).toBe("partial");
501
+ expect(result.complete).toBe(false);
502
+
503
+ const rule: GoogleSecOpsDiagnosticCheck | undefined = checkNamed(
504
+ result,
505
+ "Read rule detections by created time",
506
+ );
507
+ expect(rule?.status).toBe("warn");
508
+ expect(rule?.message).toContain(
509
+ "stopped by the request budget after 20 requests",
510
+ );
511
+ const curated: GoogleSecOpsDiagnosticCheck | undefined = checkNamed(
512
+ result,
513
+ "Read curated rule detections by created time",
514
+ );
515
+ expect(curated?.status).toBe("warn");
516
+ expect(curated?.message).toContain(
517
+ "stopped by the request budget after 0 requests",
518
+ );
519
+ expect(
520
+ checkNamed(result, "Read alerts view by detection time")?.status,
521
+ ).toBe("success");
522
+
523
+ // Five minutes past the cursor could not be read, so the next poll reads two.
524
+ expect(result.chunkMinutes).toBe(5);
525
+ expect(result.nextChunkMinutes).toBe(2);
526
+ expect(result.forcedAdvance).toBeUndefined();
527
+ expect(result.warnings).toContain(
528
+ "This window holds more records than one poll can read; the next poll reads a 2 minute window from the same starting point.",
529
+ );
530
+ expect(result.warnings.join(" ")).not.toMatch(/recovery request limit/);
531
+ expect(lastUpdate()).not.toHaveProperty("cursor");
532
+ expect(lastUpdate()["lastError"]).toContain(
533
+ "the next poll reads a 2 minute window",
534
+ );
535
+ });
536
+
537
+ test("a pass the poll time budget never let start is a warning, not a success", async () => {
538
+ const realNow: number = Date.now();
539
+ let elapsedMs: number = 0;
540
+ getJestSpyOn(Date, "now").mockImplementation((): number => {
541
+ return realNow + elapsedMs;
542
+ });
543
+ const fake: FakeClient = makeClient({
544
+ rule: [
545
+ (): SearchDetectionsResult => {
546
+ // The rule pass is slow enough to spend the whole four minutes.
547
+ elapsedMs = 5 * 60 * 1000;
548
+ return page([detection("slow")]);
549
+ },
550
+ ],
551
+ });
552
+
553
+ const result: GoogleSecOpsRunResult =
554
+ await GoogleSecOpsPoller.executeConnection(
555
+ connection({ cursor: "2026-09-14T11:55:00.000Z" }),
556
+ { type: "poll" },
557
+ fake.client,
558
+ );
559
+
560
+ expect(fake.searchCalls).toHaveLength(1);
561
+ expect(fake.alertsCalls).toHaveLength(0);
562
+ expect(
563
+ checkNamed(result, "Read rule detections by created time")?.status,
564
+ ).toBe("success");
565
+ for (const name of [
566
+ "Read curated rule detections by created time",
567
+ "Read alerts view by detection time",
568
+ ]) {
569
+ expect(checkNamed(result, name)).toMatchObject({
570
+ status: "warn",
571
+ message: "Not run: the poll time budget was spent.",
572
+ });
573
+ }
574
+ expect(result.warnings).toContain(
575
+ "Read alerts view by detection time was not run: the poll time budget was spent.",
576
+ );
577
+ expect(result.status).toBe("partial");
578
+ // What was read is still imported; the cursor waits for a shorter window.
579
+ expect(result.ingestedCount).toBe(1);
580
+ expect(result.nextChunkMinutes).toBe(2);
581
+ expect(lastUpdate()).not.toHaveProperty("cursor");
582
+ });
583
+
584
+ test("a pass the poll time budget stops part way names how far it got", async () => {
585
+ const realNow: number = Date.now();
586
+ let elapsedMs: number = 0;
587
+ getJestSpyOn(Date, "now").mockImplementation((): number => {
588
+ return realNow + elapsedMs;
589
+ });
590
+ const fake: FakeClient = makeClient({
591
+ rule: [
592
+ page([detection("first")], { nextPageToken: "t1" }),
593
+ (): SearchDetectionsResult => {
594
+ elapsedMs = 5 * 60 * 1000;
595
+ return page([detection("second")], { nextPageToken: "t2" });
596
+ },
597
+ ],
598
+ });
599
+
600
+ const result: GoogleSecOpsRunResult =
601
+ await GoogleSecOpsPoller.executeConnection(
602
+ connection({ cursor: "2026-09-14T11:55:00.000Z" }),
603
+ { type: "poll" },
604
+ fake.client,
605
+ );
606
+
607
+ expect(fake.searchCalls).toHaveLength(2);
608
+ expect(
609
+ checkNamed(result, "Read rule detections by created time"),
610
+ ).toMatchObject({
611
+ status: "warn",
612
+ message:
613
+ "2 rule detections returned for the window by created time. The pass was stopped by the poll time budget after 2 requests.",
614
+ });
615
+ expect(result.status).toBe("partial");
616
+ });
617
+
618
+ test("a page truncated by size holds the cursor and says so", async () => {
619
+ const fake: FakeClient = makeClient({
620
+ rule: [page([detection("t")], { truncated: true })],
621
+ });
622
+
623
+ const result: GoogleSecOpsRunResult =
624
+ await GoogleSecOpsPoller.executeConnection(
625
+ connection({ cursor: "2026-09-14T11:55:00.000Z" }),
626
+ { type: "poll" },
627
+ fake.client,
628
+ );
629
+
630
+ expect(result.status).toBe("partial");
631
+ expect(result.ingestedCount).toBe(1);
632
+ expect(result.warnings.join(" ")).toMatch(/truncated a page/);
633
+ expect(lastUpdate()).not.toHaveProperty("cursor");
634
+ });
635
+
636
+ test.each([400, 403, 404])(
637
+ "a curated pass answering HTTP %s is a warning, not a failed poll",
638
+ async (status: number) => {
639
+ const fake: FakeClient = makeClient({
640
+ rule: [page([detection("r")])],
641
+ curated: [
642
+ new APIException(
643
+ `Google SecOps detections search failed (HTTP ${status}): {"error":{"code":${status}}}`,
644
+ ),
645
+ ],
646
+ });
647
+
648
+ const result: GoogleSecOpsRunResult =
649
+ await GoogleSecOpsPoller.executeConnection(
650
+ connection(),
651
+ { type: "poll" },
652
+ fake.client,
653
+ );
654
+
655
+ expect(result.status).toBe("success");
656
+ expect(result.complete).toBe(true);
657
+ expect(result.ingestedCount).toBe(1);
658
+ expect(
659
+ checkNamed(result, "Read curated rule detections by created time")
660
+ ?.status,
661
+ ).toBe("warn");
662
+ expect(result.warnings.join(" ")).toContain(`(HTTP ${status})`);
663
+ expect(fake.alertsCalls).toHaveLength(1);
664
+ expect(lastUpdate()["cursor"]).toBe(NOW.toISOString());
665
+ },
666
+ );
667
+
668
+ test.each([401, 429, 500])(
669
+ "a curated pass answering HTTP %s fails the run and holds the cursor",
670
+ async (status: number) => {
671
+ const fake: FakeClient = makeClient({
672
+ rule: [page([detection("r")])],
673
+ curated: [
674
+ new APIException(
675
+ `Google SecOps detections search failed (HTTP ${status}): {"error":{"code":${status}}}`,
676
+ ),
677
+ ],
678
+ });
679
+
680
+ const result: GoogleSecOpsRunResult =
681
+ await GoogleSecOpsPoller.executeConnection(
682
+ connection({ cursor: "2026-09-14T11:55:00.000Z" }),
683
+ { type: "poll" },
684
+ fake.client,
685
+ );
686
+
687
+ expect(result.status).toBe("failed");
688
+ expect(result.error).toContain(`(HTTP ${status})`);
689
+ expect(
690
+ checkNamed(result, "Read curated rule detections by created time")
691
+ ?.status,
692
+ ).toBe("failed");
693
+ expect(fake.alertsCalls).toHaveLength(0);
694
+ expect(insertedRows).toHaveLength(0);
695
+ expect(lastUpdate()).not.toHaveProperty("cursor");
696
+ },
697
+ );
698
+
699
+ test("a curated pass timing out or answering unreadably fails the run like any other pass", async () => {
700
+ const fake: FakeClient = makeClient({
701
+ curated: [
702
+ new APIException(
703
+ "Google SecOps detections search returned a non-JSON body.",
704
+ ),
705
+ ],
706
+ });
707
+
708
+ const result: GoogleSecOpsRunResult =
709
+ await GoogleSecOpsPoller.executeConnection(
710
+ connection(),
711
+ { type: "poll" },
712
+ fake.client,
713
+ );
714
+
715
+ expect(result.status).toBe("failed");
716
+ expect(result.error).toBe(
717
+ "Google SecOps detections search returned a non-JSON body.",
718
+ );
719
+ });
720
+
721
+ test("a rule pass failure names its own step", async () => {
722
+ const fake: FakeClient = makeClient({
723
+ rule: [
724
+ new APIException(
725
+ "Google SecOps detections search failed (HTTP 403): denied",
726
+ ),
727
+ ],
728
+ });
729
+
730
+ const result: GoogleSecOpsRunResult =
731
+ await GoogleSecOpsPoller.executeConnection(
732
+ connection(),
733
+ { type: "poll" },
734
+ fake.client,
735
+ );
736
+
737
+ expect(result.status).toBe("failed");
738
+ expect(result.checks[result.checks.length - 1]).toMatchObject({
739
+ name: "Read rule detections by created time",
740
+ status: "failed",
741
+ });
742
+ expect(fake.searchCalls).toHaveLength(1);
743
+ expect(fake.alertsCalls).toHaveLength(0);
744
+ });
745
+
746
+ test("rejected objects are counted and warned but no longer hold the cursor", async () => {
747
+ const fake: FakeClient = makeClient({
748
+ rule: [page([{ arbitrary: "envelope" }, detection("ok")])],
749
+ });
750
+
751
+ const result: GoogleSecOpsRunResult =
752
+ await GoogleSecOpsPoller.executeConnection(
753
+ connection({ cursor: "2026-09-14T11:55:00.000Z" }),
754
+ { type: "poll" },
755
+ fake.client,
756
+ );
757
+
758
+ expect(result).toMatchObject({
759
+ status: "success",
760
+ complete: true,
761
+ rejectedCount: 1,
762
+ ingestedCount: 1,
763
+ });
764
+ expect(checkNamed(result, "Normalize detections")?.status).toBe("warn");
765
+ expect(result.warnings.join(" ")).toMatch(/do not hold the poll cursor/);
766
+ expect(lastUpdate()["cursor"]).toBe(NOW.toISOString());
767
+ expect(lastUpdate()["lastError"]).toBeNull();
768
+ });
769
+
770
+ test("a normalization failure still holds the cursor", async () => {
771
+ const poison: JSONObject = { id: "poison" };
772
+ Object.defineProperty(poison, "detection", {
773
+ get: (): never => {
774
+ throw new Error("poison");
775
+ },
776
+ enumerable: true,
777
+ });
778
+ const fake: FakeClient = makeClient({
779
+ rule: [page([poison, detection("ok")])],
780
+ });
781
+
782
+ const result: GoogleSecOpsRunResult =
783
+ await GoogleSecOpsPoller.executeConnection(
784
+ connection({ cursor: "2026-09-14T11:55:00.000Z" }),
785
+ { type: "poll" },
786
+ fake.client,
787
+ );
788
+
789
+ expect(result).toMatchObject({
790
+ status: "partial",
791
+ failedCount: 1,
792
+ ingestedCount: 1,
793
+ complete: false,
794
+ });
795
+ expect(checkNamed(result, "Normalize detections")?.status).toBe("failed");
796
+ expect(lastUpdate()).not.toHaveProperty("cursor");
797
+ });
798
+
799
+ test("creation lag is measured and a lag beyond the poll interval explains the created-time basis", async () => {
800
+ const fake: FakeClient = makeClient({
801
+ rule: [
802
+ page([
803
+ detection("late", {
804
+ detectionTime: "2026-09-14T08:00:00.000Z",
805
+ createdTime: "2026-09-14T11:00:00.000Z",
806
+ }),
807
+ detection("prompt", {
808
+ detectionTime: "2026-09-14T11:00:00.000Z",
809
+ createdTime: "2026-09-14T11:02:00.000Z",
810
+ }),
811
+ detection("no-created", { createdTime: undefined }),
812
+ ]),
813
+ ],
814
+ });
815
+
816
+ const result: GoogleSecOpsRunResult =
817
+ await GoogleSecOpsPoller.executeConnection(
818
+ connection({ pollIntervalInMinutes: 5 }),
819
+ { type: "poll" },
820
+ fake.client,
821
+ );
822
+
823
+ expect(result.creationLag).toEqual({
824
+ measured: 2,
825
+ lateCount: 1,
826
+ maxLagMinutes: 180,
827
+ });
828
+ expect(result.warnings.join(" ")).toMatch(
829
+ /1 of 2 detections were created more than 6 minutes after their detection time \(up to 180 minutes\)\. This is why the connector polls by created time/,
830
+ );
831
+ // Informational: the run is complete and the cursor moves.
832
+ expect(result.status).toBe("success");
833
+ expect(lastUpdate()["cursor"]).toBe(NOW.toISOString());
834
+ });
835
+
836
+ test("creation lag within the interval produces statistics but no warning", async () => {
837
+ const fake: FakeClient = makeClient({
838
+ rule: [page([detection("prompt")])],
839
+ });
840
+
841
+ const result: GoogleSecOpsRunResult =
842
+ await GoogleSecOpsPoller.executeConnection(
843
+ connection(),
844
+ { type: "poll" },
845
+ fake.client,
846
+ );
847
+
848
+ expect(result.creationLag).toEqual({
849
+ measured: 1,
850
+ lateCount: 0,
851
+ maxLagMinutes: 2,
852
+ });
853
+ expect(result.warnings).toEqual([]);
854
+ });
855
+
856
+ test("preview reads the search passes by both bases and reports the detection-time basis", async () => {
857
+ const fake: FakeClient = makeClient({
858
+ rule: [page([detection("x")])],
859
+ });
860
+
861
+ const result: GoogleSecOpsRunResult =
862
+ await GoogleSecOpsPoller.executeConnection(
863
+ connection(),
864
+ {
865
+ type: "preview",
866
+ startTime: "2026-09-13T00:00:00.000Z",
867
+ endTime: NOW.toISOString(),
868
+ },
869
+ fake.client,
870
+ );
871
+
872
+ expect(
873
+ fake.searchCalls.map((call: SearchCall): string => {
874
+ return `${call.curated ? "curated" : "rule"}:${call.listBasis}`;
875
+ }),
876
+ ).toEqual([
877
+ "rule:CREATED_TIME",
878
+ "rule:DETECTION_TIME",
879
+ "curated:CREATED_TIME",
880
+ "curated:DETECTION_TIME",
881
+ ]);
882
+ expect(result.basis).toBe("detection-time");
883
+ expect(result.sourceCounts).toEqual({
884
+ ruleDetections: 2,
885
+ curatedDetections: 0,
886
+ alertsView: 0,
887
+ });
888
+ // The same record read by both bases is one record.
889
+ expect(result.fetchedCount).toBe(1);
890
+ expect(
891
+ checkNamed(result, "Read rule detections by created and detection time"),
892
+ ).toBeDefined();
893
+ expect(insertedRows).toHaveLength(0);
894
+ expect(GoogleSecOpsConnectionService.updateOneById).not.toHaveBeenCalled();
895
+ });
896
+
897
+ test("a connection test reads at most one record per pass and never paginates", async () => {
898
+ const fake: FakeClient = makeClient({
899
+ rule: [page([detection("one")], { nextPageToken: "more" })],
900
+ curated: [page([detection("two")], { nextPageToken: "more" })],
901
+ alerts: [fetched([detection("three")])],
902
+ });
903
+
904
+ const result: GoogleSecOpsRunResult =
905
+ await GoogleSecOpsPoller.executeConnection(
906
+ connection(),
907
+ { type: "test" },
908
+ fake.client,
909
+ );
910
+
911
+ expect(result.status).toBe("success");
912
+ expect(fake.searchCalls).toHaveLength(2);
913
+ expect(
914
+ fake.searchCalls.every((call: SearchCall): boolean => {
915
+ return call.pageSize === 1;
916
+ }),
917
+ ).toBe(true);
918
+ expect(fake.alertsCalls[0]!.maxAlerts).toBe(1);
919
+ expect(result.requestCount).toBe(3);
920
+ expect(insertedRows).toHaveLength(0);
921
+ expect(GoogleSecOpsConnectionService.updateOneById).not.toHaveBeenCalled();
922
+ expect(Semaphore.lock).not.toHaveBeenCalled();
923
+ });
924
+
925
+ test("a busy source lock is reported in words and does not read Google", async () => {
926
+ getJestSpyOn(Semaphore, "lock").mockRejectedValue(
927
+ new SemaphoreLockTimeoutError("Acquire GoogleSecOpsSource lock timeout"),
928
+ );
929
+ const fake: FakeClient = makeClient({});
930
+
931
+ await expect(
932
+ GoogleSecOpsPoller.executeConnection(
933
+ connection(),
934
+ { type: "poll" },
935
+ fake.client,
936
+ ),
937
+ ).rejects.toThrow(
938
+ "Another poll or import for this source is still running in this project. This run was skipped; polling continues on the next scheduled tick.",
939
+ );
940
+ expect(fake.searchCalls).toHaveLength(0);
941
+ expect(fake.alertsCalls).toHaveLength(0);
942
+ expect(GoogleSecOpsConnectionService.updateOneById).not.toHaveBeenCalled();
943
+ });
944
+
945
+ test("any other lock failure is passed through unchanged", async () => {
946
+ getJestSpyOn(Semaphore, "lock").mockRejectedValue(
947
+ new Error("Redis client is not connected"),
948
+ );
949
+
950
+ await expect(
951
+ GoogleSecOpsPoller.executeConnection(
952
+ connection(),
953
+ { type: "poll" },
954
+ makeClient({}).client,
955
+ ),
956
+ ).rejects.toThrow("Redis client is not connected");
957
+ });
958
+
959
+ test("the dedupe lookup delegates to the shared SecurityEventDedupe with the Google source names", async () => {
960
+ getJestSpyOn(GoogleSecOpsPoller, "findExistingEventUids").mockRestore();
961
+ const shared: ReturnType<typeof getJestSpyOn> = getJestSpyOn(
962
+ SecurityEventDedupe,
963
+ "findExistingEventUids",
964
+ ).mockResolvedValue(new Set(["already"]));
965
+
966
+ const found: Set<string> = await GoogleSecOpsPoller.findExistingEventUids(
967
+ PROJECT_ID,
968
+ ["already", "new"],
969
+ );
970
+
971
+ expect(found).toEqual(new Set(["already"]));
972
+ expect(shared).toHaveBeenCalledWith({
973
+ projectId: PROJECT_ID,
974
+ vendorName: "Google",
975
+ productName: "Google SecOps",
976
+ ids: ["already", "new"],
977
+ });
978
+ });
979
+ });
980
+
981
+ /*
982
+ * Adaptive catch-up across many polls. Review finding
983
+ * alerts-view-budget-pins-cursor-forever (F1, F1b): a window holding more
984
+ * than one poll could read used to be re-read on every tick with the cursor
985
+ * held, so nothing created after the first poll was ever imported.
986
+ *
987
+ * Every test here drives a fake tenant that answers the way Google
988
+ * documents: the detection searches filter on created time and page newest
989
+ * first; the alerts view filters on detection time, returns at most
990
+ * maxAlerts and reports how many matched in baselineAlertsCount. A
991
+ * simulated connection row carries cursor and lastPollResult from one poll
992
+ * to the next the way the database does.
993
+ */
994
+ const MINUTE_MS: number = 60 * 1000;
995
+
996
+ interface TenantDetection {
997
+ id: string;
998
+ createdMs: number;
999
+ detectionMs: number;
1000
+ }
1001
+
1002
+ interface Tenant {
1003
+ detections: Array<TenantDetection>;
1004
+ /*
1005
+ * Google may return fewer detections than the pageSize asked for. A
1006
+ * smaller page keeps the volume needed to overflow the page budget small.
1007
+ */
1008
+ searchPageSize: number;
1009
+ searchFailure?: Error | undefined;
1010
+ }
1011
+
1012
+ interface TenantCalls {
1013
+ search: number;
1014
+ alerts: number;
1015
+ }
1016
+
1017
+ interface ConnectionRow {
1018
+ cursor?: string | undefined;
1019
+ lastPollResult?: JSONObject | undefined;
1020
+ }
1021
+
1022
+ interface PollRecord {
1023
+ nowMs: number;
1024
+ cursorBefore: string | undefined;
1025
+ result: GoogleSecOpsRunResult;
1026
+ update: JSONObject;
1027
+ calls: TenantCalls;
1028
+ }
1029
+
1030
+ function tenantCollection(item: TenantDetection): JSONObject {
1031
+ return {
1032
+ id: item.id,
1033
+ type: "RULE_DETECTION",
1034
+ detectionTime: new Date(item.detectionMs).toISOString(),
1035
+ createdTime: new Date(item.createdMs).toISOString(),
1036
+ detection: [
1037
+ { ruleName: "Burst rule", alertState: "ALERTING", severity: "HIGH" },
1038
+ ],
1039
+ };
1040
+ }
1041
+
1042
+ function inRange(timeMs: number, startTime: Date, endTime: Date): boolean {
1043
+ return timeMs >= startTime.getTime() && timeMs < endTime.getTime();
1044
+ }
1045
+
1046
+ function tenantClient(tenant: Tenant, calls: TenantCalls): GoogleSecOpsClient {
1047
+ return {
1048
+ testAuthentication: jest.fn(async (): Promise<void> => {}),
1049
+ searchDetections: jest.fn(
1050
+ async (call: SearchCall): Promise<SearchDetectionsResult> => {
1051
+ calls.search++;
1052
+ if (tenant.searchFailure) {
1053
+ throw tenant.searchFailure;
1054
+ }
1055
+ if (call.curated) {
1056
+ return page([]);
1057
+ }
1058
+ const matched: Array<TenantDetection> = tenant.detections
1059
+ .filter((item: TenantDetection): boolean => {
1060
+ return inRange(item.createdMs, call.startTime, call.endTime);
1061
+ })
1062
+ .sort((a: TenantDetection, b: TenantDetection): number => {
1063
+ return b.createdMs - a.createdMs;
1064
+ });
1065
+ const offset: number = call.pageToken ? Number(call.pageToken) : 0;
1066
+ const next: number =
1067
+ offset + Math.min(call.pageSize || 1000, tenant.searchPageSize);
1068
+ return page(matched.slice(offset, next).map(tenantCollection), {
1069
+ nextPageToken: next < matched.length ? String(next) : null,
1070
+ });
1071
+ },
1072
+ ),
1073
+ fetchDetectionAlerts: jest.fn(
1074
+ async (call: AlertsCall): Promise<FetchAlertsResult> => {
1075
+ calls.alerts++;
1076
+ const matched: Array<TenantDetection> = tenant.detections
1077
+ .filter((item: TenantDetection): boolean => {
1078
+ return inRange(item.detectionMs, call.startTime, call.endTime);
1079
+ })
1080
+ .sort((a: TenantDetection, b: TenantDetection): number => {
1081
+ return b.detectionMs - a.detectionMs;
1082
+ });
1083
+ const returned: Array<TenantDetection> = matched.slice(
1084
+ 0,
1085
+ call.maxAlerts || 1000,
1086
+ );
1087
+ return fetched(returned.map(tenantCollection), {
1088
+ truncatedByCount: matched.length > returned.length,
1089
+ baselineAlertsCount: matched.length,
1090
+ filteredAlertsCount: matched.length,
1091
+ });
1092
+ },
1093
+ ),
1094
+ } as unknown as GoogleSecOpsClient;
1095
+ }
1096
+
1097
+ describe("GoogleSecOpsPoller adaptive catch-up across polls", () => {
1098
+ let stored: Set<string>;
1099
+
1100
+ beforeEach(() => {
1101
+ stored = new Set();
1102
+ getJestSpyOn(OneUptimeDate, "getCurrentDate").mockReturnValue(NOW);
1103
+ getJestSpyOn(Semaphore, "lock").mockResolvedValue({});
1104
+ getJestSpyOn(Semaphore, "release").mockResolvedValue(undefined);
1105
+ // Dedupe against what earlier polls stored, as ClickHouse would.
1106
+ getJestSpyOn(
1107
+ GoogleSecOpsPoller,
1108
+ "findExistingEventUids",
1109
+ ).mockImplementation(
1110
+ async (
1111
+ _projectId: ObjectID,
1112
+ ids: Array<string>,
1113
+ ): Promise<Set<string>> => {
1114
+ return new Set(
1115
+ ids.filter((id: string): boolean => {
1116
+ return stored.has(id);
1117
+ }),
1118
+ );
1119
+ },
1120
+ );
1121
+ getJestSpyOn(SecurityEventService, "insertJsonRows").mockImplementation(((
1122
+ rows: Array<JSONObject>,
1123
+ ): Promise<void> => {
1124
+ for (const row of rows) {
1125
+ stored.add(String(row["eventUid"]));
1126
+ }
1127
+ return Promise.resolve();
1128
+ }) as never);
1129
+ getJestSpyOn(
1130
+ OTelIngestService,
1131
+ "telemetryServiceFromName",
1132
+ ).mockResolvedValue({
1133
+ serviceName: "Google SecOps",
1134
+ primaryEntityId: PROJECT_ID,
1135
+ primaryEntityType: ServiceType.OpenTelemetry,
1136
+ dataRententionInDays: 15,
1137
+ serviceRetentionConfig: null,
1138
+ serviceRetentionInDays: null,
1139
+ projectRetentionConfig: null,
1140
+ projectRetentionInDays: 15,
1141
+ });
1142
+ getJestSpyOn(
1143
+ ThreatIntelEnricher,
1144
+ "enrichNormalizedEvents",
1145
+ ).mockResolvedValue({ eventsMatched: 0, valuesLookedUp: 0 });
1146
+ getJestSpyOn(
1147
+ GoogleSecOpsConnectionService,
1148
+ "updateOneById",
1149
+ ).mockResolvedValue(undefined);
1150
+ getJestSpyOn(logger, "warn").mockImplementation((): void => {});
1151
+ getJestSpyOn(logger, "error").mockImplementation((): void => {});
1152
+ getJestSpyOn(logger, "debug").mockImplementation((): void => {});
1153
+ });
1154
+
1155
+ afterEach(() => {
1156
+ jest.restoreAllMocks();
1157
+ });
1158
+
1159
+ /*
1160
+ * Runs scheduled polls one after another, carrying the row forward, and
1161
+ * checks the two properties every poll must keep: a written cursor only
1162
+ * moves forward, and no pass goes past its request budget.
1163
+ */
1164
+ async function runPolls(data: {
1165
+ tenant: Tenant;
1166
+ row: ConnectionRow;
1167
+ startMs: number;
1168
+ stepMinutes: number;
1169
+ maxPolls: number;
1170
+ beforePoll?: ((index: number, nowMs: number) => void) | undefined;
1171
+ until?: ((record: PollRecord) => boolean) | undefined;
1172
+ }): Promise<Array<PollRecord>> {
1173
+ const records: Array<PollRecord> = [];
1174
+ for (let index: number = 0; index < data.maxPolls; index++) {
1175
+ const nowMs: number = data.startMs + index * data.stepMinutes * MINUTE_MS;
1176
+ getJestSpyOn(OneUptimeDate, "getCurrentDate").mockReturnValue(
1177
+ new Date(nowMs),
1178
+ );
1179
+ if (data.beforePoll) {
1180
+ data.beforePoll(index, nowMs);
1181
+ }
1182
+ const calls: TenantCalls = { search: 0, alerts: 0 };
1183
+ const cursorBefore: string | undefined = data.row.cursor;
1184
+ const result: GoogleSecOpsRunResult =
1185
+ await GoogleSecOpsPoller.executeConnection(
1186
+ connection({
1187
+ ...(data.row.cursor ? { cursor: data.row.cursor } : {}),
1188
+ ...(data.row.lastPollResult
1189
+ ? { lastPollResult: data.row.lastPollResult }
1190
+ : {}),
1191
+ }),
1192
+ { type: "poll" },
1193
+ tenantClient(data.tenant, calls),
1194
+ );
1195
+ const update: JSONObject = lastUpdate();
1196
+ // The row keeps a JSON copy, not the live result object.
1197
+ data.row.lastPollResult = JSON.parse(
1198
+ JSON.stringify(update["lastPollResult"]),
1199
+ ) as JSONObject;
1200
+ const written: unknown = update["cursor"];
1201
+ if (typeof written === "string") {
1202
+ if (cursorBefore) {
1203
+ expect(Date.parse(written)).toBeGreaterThan(Date.parse(cursorBefore));
1204
+ }
1205
+ data.row.cursor = written;
1206
+ }
1207
+ expect(calls.search).toBeLessThanOrEqual(20);
1208
+ expect(calls.alerts).toBeLessThanOrEqual(16);
1209
+ const record: PollRecord = {
1210
+ nowMs,
1211
+ cursorBefore,
1212
+ result,
1213
+ update,
1214
+ calls,
1215
+ };
1216
+ records.push(record);
1217
+ if (data.until && data.until(record)) {
1218
+ break;
1219
+ }
1220
+ }
1221
+ return records;
1222
+ }
1223
+
1224
+ function chunks(
1225
+ records: Array<PollRecord>,
1226
+ key: "chunkMinutes" | "nextChunkMinutes",
1227
+ ): Array<number | undefined> {
1228
+ return records.map((record: PollRecord): number | undefined => {
1229
+ return record.result[key];
1230
+ });
1231
+ }
1232
+
1233
+ test("Case A: 1,200 detections created in one hour of the first 24 hour window no longer pin the cursor", async () => {
1234
+ const tenant: Tenant = { detections: [], searchPageSize: 1000 };
1235
+ const burstStartMs: number = NOW.getTime() - 6 * 60 * MINUTE_MS;
1236
+ for (let index: number = 0; index < 1200; index++) {
1237
+ const detectionMs: number =
1238
+ burstStartMs + Math.floor((index * 60 * MINUTE_MS) / 1200);
1239
+ tenant.detections.push({
1240
+ id: `burst-${index}`,
1241
+ detectionMs,
1242
+ createdMs: detectionMs + MINUTE_MS,
1243
+ });
1244
+ }
1245
+ const createdAfterFirstPollMs: number = NOW.getTime() + 3 * MINUTE_MS;
1246
+ const row: ConnectionRow = {};
1247
+
1248
+ const records: Array<PollRecord> = await runPolls({
1249
+ tenant,
1250
+ row,
1251
+ startMs: NOW.getTime(),
1252
+ stepMinutes: 5,
1253
+ maxPolls: 4,
1254
+ beforePoll: (index: number): void => {
1255
+ if (index === 1) {
1256
+ tenant.detections.push({
1257
+ id: "created-after-first-poll",
1258
+ detectionMs: createdAfterFirstPollMs - MINUTE_MS,
1259
+ createdMs: createdAfterFirstPollMs,
1260
+ });
1261
+ }
1262
+ },
1263
+ });
1264
+
1265
+ const first: PollRecord = records[0]!;
1266
+ /*
1267
+ * The alerts view splits the day around the burst. With the old shared
1268
+ * budget of twelve requests the two search pages and the curated page
1269
+ * left nine for it, which was not enough; on its own budget it finishes.
1270
+ */
1271
+ expect(first.calls.search).toBe(3);
1272
+ expect(first.calls.search + first.calls.alerts).toBeGreaterThan(12);
1273
+ expect(first.result).toMatchObject({
1274
+ windowStart: "2026-09-13T12:00:00.000Z",
1275
+ windowEnd: NOW.toISOString(),
1276
+ status: "success",
1277
+ complete: true,
1278
+ ingestedCount: 1200,
1279
+ chunkMinutes: 24 * 60,
1280
+ nextChunkMinutes: 24 * 60,
1281
+ });
1282
+ expect(first.update["cursor"]).toBe(NOW.toISOString());
1283
+
1284
+ // The next tick reads forward from the cursor and imports the new detection.
1285
+ const second: PollRecord = records[1]!;
1286
+ expect(second.result.windowStart).toBe("2026-09-14T11:59:00.000Z");
1287
+ expect(second.result.ingestedCount).toBe(1);
1288
+ expect(stored.has("created-after-first-poll")).toBe(true);
1289
+ expect(stored.size).toBe(1201);
1290
+ expect(
1291
+ records.every((record: PollRecord): boolean => {
1292
+ return record.result.complete;
1293
+ }),
1294
+ ).toBe(true);
1295
+ });
1296
+
1297
+ test("Case B: 1,001 detections sharing one detection time force one reported advance and newer detections still arrive", async () => {
1298
+ const tenant: Tenant = { detections: [], searchPageSize: 1000 };
1299
+ const sharedDetectionMs: number = NOW.getTime() - 3 * MINUTE_MS;
1300
+ for (let index: number = 0; index < 1001; index++) {
1301
+ tenant.detections.push({
1302
+ id: `shared-${index}`,
1303
+ detectionMs: sharedDetectionMs,
1304
+ createdMs: sharedDetectionMs + 30 * 1000,
1305
+ });
1306
+ }
1307
+ let laterCreatedMs: number = 0;
1308
+ const row: ConnectionRow = { cursor: "2026-09-14T11:55:00.000Z" };
1309
+
1310
+ const records: Array<PollRecord> = await runPolls({
1311
+ tenant,
1312
+ row,
1313
+ startMs: NOW.getTime(),
1314
+ stepMinutes: 5,
1315
+ maxPolls: 10,
1316
+ beforePoll: (index: number, nowMs: number): void => {
1317
+ if (index === 2) {
1318
+ laterCreatedMs = nowMs - 30 * 1000;
1319
+ tenant.detections.push({
1320
+ id: "later",
1321
+ detectionMs: nowMs - MINUTE_MS,
1322
+ createdMs: laterCreatedMs,
1323
+ });
1324
+ }
1325
+ },
1326
+ until: (record: PollRecord): boolean => {
1327
+ return stored.has("later") && record.result.complete;
1328
+ },
1329
+ });
1330
+
1331
+ // The created-time pass read all of them on the first poll.
1332
+ const first: PollRecord = records[0]!;
1333
+ expect(first.result.sourceCounts?.ruleDetections).toBe(1001);
1334
+ expect(stored.size).toBeGreaterThanOrEqual(1001);
1335
+ expect(first.result.status).toBe("partial");
1336
+ expect(
1337
+ checkNamed(first.result, "Read alerts view by detection time"),
1338
+ ).toMatchObject({ status: "warn" });
1339
+ expect(
1340
+ checkNamed(first.result, "Read alerts view by detection time")?.message,
1341
+ ).toContain("stopped by the request budget after 16 requests");
1342
+
1343
+ // The alerts view can never split one detection time, so one minute is skipped and reported.
1344
+ const forced: Array<PollRecord> = records.filter(
1345
+ (record: PollRecord): boolean => {
1346
+ return record.result.forcedAdvance === true;
1347
+ },
1348
+ );
1349
+ expect(forced).toHaveLength(1);
1350
+ const forcedWarning: string =
1351
+ "More records were created in the one minute from 2026-09-14T11:57:00.000Z to 2026-09-14T11:58:00.000Z than one poll can read. Polling moved past this minute so newer records keep arriving; use Import this time range in Diagnostics on this minute to recover what one run can read.";
1352
+ expect(forced[0]!.result.warnings[0]).toBe(forcedWarning);
1353
+ expect(
1354
+ String(forced[0]!.update["lastError"]).startsWith(forcedWarning),
1355
+ ).toBe(true);
1356
+ expect(forced[0]!.update["cursor"]).toBe("2026-09-14T11:58:00.000Z");
1357
+
1358
+ // The poll after it starts at the skipped minute's end instead of overflowing on it again.
1359
+ const afterForced: PollRecord = records[records.indexOf(forced[0]!) + 1]!;
1360
+ expect(afterForced.result.windowStart).toBe("2026-09-14T11:58:00.000Z");
1361
+ expect(afterForced.result.complete).toBe(true);
1362
+
1363
+ // Bounded: the detection created after the burst is imported within ten polls.
1364
+ expect(stored.has("later")).toBe(true);
1365
+ expect(records.length).toBeLessThanOrEqual(10);
1366
+ expect(Date.parse(row.cursor!)).toBeGreaterThan(laterCreatedMs);
1367
+ });
1368
+
1369
+ test("narrowing halves the chunk until a window fits, then doubles it back", async () => {
1370
+ const cursorMs: number = Date.parse("2026-09-12T12:00:00.000Z");
1371
+ const tenant: Tenant = { detections: [], searchPageSize: 100 };
1372
+ // 2,500 detections created in one hour: more than 20 pages of 100.
1373
+ for (let index: number = 0; index < 2500; index++) {
1374
+ const createdMs: number =
1375
+ cursorMs + 60 * MINUTE_MS + Math.floor((index * 60 * MINUTE_MS) / 2500);
1376
+ tenant.detections.push({
1377
+ id: `hour-${index}`,
1378
+ createdMs,
1379
+ // Detected long ago, so only the created-time searches see them.
1380
+ detectionMs: createdMs - 30 * 24 * 60 * MINUTE_MS,
1381
+ });
1382
+ }
1383
+ const row: ConnectionRow = { cursor: new Date(cursorMs).toISOString() };
1384
+
1385
+ const records: Array<PollRecord> = await runPolls({
1386
+ tenant,
1387
+ row,
1388
+ startMs: NOW.getTime(),
1389
+ stepMinutes: 5,
1390
+ maxPolls: 8,
1391
+ });
1392
+
1393
+ expect(chunks(records, "chunkMinutes")).toEqual([
1394
+ 1440, 720, 360, 180, 90, 180, 360, 720,
1395
+ ]);
1396
+ expect(chunks(records, "nextChunkMinutes")).toEqual([
1397
+ 720, 360, 180, 90, 180, 360, 720, 1440,
1398
+ ]);
1399
+ expect(
1400
+ records.map((record: PollRecord): boolean => {
1401
+ return record.result.complete;
1402
+ }),
1403
+ ).toEqual([false, false, false, false, true, true, true, true]);
1404
+ for (const record of records.slice(0, 4)) {
1405
+ expect(record.update).not.toHaveProperty("cursor");
1406
+ expect(record.result.windowStart).toBe("2026-09-12T11:59:00.000Z");
1407
+ expect(
1408
+ checkNamed(record.result, "Read rule detections by created time")
1409
+ ?.message,
1410
+ ).toContain("stopped by the request budget after 20 requests");
1411
+ expect(record.result.warnings).toContain(
1412
+ `This window holds more records than one poll can read; the next poll reads a ${record.result.nextChunkMinutes} minute window from the same starting point.`,
1413
+ );
1414
+ }
1415
+ expect(
1416
+ records.slice(4).map((record: PollRecord): unknown => {
1417
+ return record.update["cursor"];
1418
+ }),
1419
+ ).toEqual([
1420
+ "2026-09-12T13:30:00.000Z",
1421
+ "2026-09-12T16:30:00.000Z",
1422
+ "2026-09-12T22:30:00.000Z",
1423
+ "2026-09-13T10:30:00.000Z",
1424
+ ]);
1425
+ expect(stored.size).toBe(2500);
1426
+ });
1427
+
1428
+ test("a one-minute window that still overflows forces an advance with the warning in lastError", async () => {
1429
+ const minuteMs: number = Date.parse("2026-09-14T11:00:00.000Z");
1430
+ const tenant: Tenant = { detections: [], searchPageSize: 100 };
1431
+ for (let index: number = 0; index < 2500; index++) {
1432
+ const createdMs: number =
1433
+ minuteMs + Math.floor((index * MINUTE_MS) / 2500);
1434
+ tenant.detections.push({
1435
+ id: `minute-${index}`,
1436
+ createdMs,
1437
+ detectionMs: createdMs - 30 * 24 * 60 * MINUTE_MS,
1438
+ });
1439
+ }
1440
+ const row: ConnectionRow = {
1441
+ cursor: "2026-09-14T11:00:00.000Z",
1442
+ lastPollResult: { type: "poll", nextChunkMinutes: 4 },
1443
+ };
1444
+ const outage: APIException = new APIException(
1445
+ 'Google SecOps detections search failed (HTTP 503): {"error":{"code":503}}',
1446
+ );
1447
+
1448
+ const records: Array<PollRecord> = await runPolls({
1449
+ tenant,
1450
+ row,
1451
+ startMs: NOW.getTime(),
1452
+ stepMinutes: 5,
1453
+ maxPolls: 6,
1454
+ beforePoll: (index: number): void => {
1455
+ // The poll right after the forced advance fails once.
1456
+ tenant.searchFailure = index === 3 ? outage : undefined;
1457
+ },
1458
+ });
1459
+
1460
+ expect(chunks(records, "chunkMinutes")).toEqual([4, 2, 1, 1, 1, 2]);
1461
+ expect(chunks(records, "nextChunkMinutes")).toEqual([2, 1, 1, 1, 2, 4]);
1462
+ expect(
1463
+ records.map((record: PollRecord): string => {
1464
+ return record.result.status;
1465
+ }),
1466
+ ).toEqual(["partial", "partial", "partial", "failed", "empty", "empty"]);
1467
+
1468
+ const forced: PollRecord = records[2]!;
1469
+ const warning: string =
1470
+ "More records were created in the one minute from 2026-09-14T11:00:00.000Z to 2026-09-14T11:01:00.000Z than one poll can read. Polling moved past this minute so newer records keep arriving; use Import this time range in Diagnostics on this minute to recover what one run can read.";
1471
+ expect(forced.result.forcedAdvance).toBe(true);
1472
+ expect(forced.result.warnings[0]).toBe(warning);
1473
+ expect(forced.update["cursor"]).toBe("2026-09-14T11:01:00.000Z");
1474
+ expect(String(forced.update["lastError"]).startsWith(warning)).toBe(true);
1475
+ expect(forced.update).not.toHaveProperty("lastSuccessfulPollAt");
1476
+
1477
+ // A failure keeps the cursor and the chunk, and the retry still starts past the skipped minute.
1478
+ expect(records[3]!.update).not.toHaveProperty("cursor");
1479
+ expect(records[3]!.result.windowStart).toBe("2026-09-14T11:01:00.000Z");
1480
+ expect(records[3]!.update["lastError"]).toContain("HTTP 503");
1481
+ expect(records[4]!.result.windowStart).toBe("2026-09-14T11:01:00.000Z");
1482
+ expect(records[4]!.update["cursor"]).toBe("2026-09-14T11:02:00.000Z");
1483
+ expect(records[4]!.update["lastError"]).toBeNull();
1484
+ // Once past it, the usual one minute overlap is back.
1485
+ expect(records[5]!.result.windowStart).toBe("2026-09-14T11:01:00.000Z");
1486
+ expect(records[5]!.result.windowEnd).toBe("2026-09-14T11:04:00.000Z");
1487
+ });
1488
+
1489
+ test("a caught-up poll keeps the chunk it was given, so the poll after a failure or a late tick still reaches the present", async () => {
1490
+ const tenant: Tenant = {
1491
+ detections: [],
1492
+ searchPageSize: 1000,
1493
+ searchFailure: new APIException(
1494
+ 'Google SecOps detections search failed (HTTP 500): {"error":{"code":500}}',
1495
+ ),
1496
+ };
1497
+ const row: ConnectionRow = { cursor: "2026-09-14T11:55:00.000Z" };
1498
+
1499
+ const records: Array<PollRecord> = await runPolls({
1500
+ tenant,
1501
+ row,
1502
+ startMs: NOW.getTime(),
1503
+ stepMinutes: 30,
1504
+ maxPolls: 3,
1505
+ beforePoll: (index: number): void => {
1506
+ if (index === 1) {
1507
+ tenant.searchFailure = undefined;
1508
+ }
1509
+ },
1510
+ });
1511
+
1512
+ /*
1513
+ * These windows reach five and thirty-five minutes past the cursor only
1514
+ * because they end at the present. That length says nothing about
1515
+ * volume, so it must not become the next chunk and leave later polls
1516
+ * behind.
1517
+ */
1518
+ expect(records[0]!.result).toMatchObject({
1519
+ status: "failed",
1520
+ chunkMinutes: 5,
1521
+ nextChunkMinutes: 24 * 60,
1522
+ });
1523
+ expect(records[1]!.result).toMatchObject({
1524
+ status: "empty",
1525
+ windowStart: "2026-09-14T11:54:00.000Z",
1526
+ windowEnd: "2026-09-14T12:30:00.000Z",
1527
+ chunkMinutes: 35,
1528
+ nextChunkMinutes: 24 * 60,
1529
+ });
1530
+ expect(records[2]!.result.windowEnd).toBe("2026-09-14T13:00:00.000Z");
1531
+ });
1532
+
1533
+ test("a failed poll keeps its chunk; only a finished or overflowing poll changes it", async () => {
1534
+ const tenant: Tenant = {
1535
+ detections: [],
1536
+ searchPageSize: 1000,
1537
+ searchFailure: new APIException(
1538
+ 'Google SecOps detections search failed (HTTP 500): {"error":{"code":500}}',
1539
+ ),
1540
+ };
1541
+ const row: ConnectionRow = {
1542
+ cursor: "2026-09-14T10:00:00.000Z",
1543
+ lastPollResult: { type: "poll", nextChunkMinutes: 30 },
1544
+ };
1545
+
1546
+ const records: Array<PollRecord> = await runPolls({
1547
+ tenant,
1548
+ row,
1549
+ startMs: NOW.getTime(),
1550
+ stepMinutes: 5,
1551
+ maxPolls: 3,
1552
+ beforePoll: (index: number): void => {
1553
+ if (index === 2) {
1554
+ tenant.searchFailure = undefined;
1555
+ }
1556
+ },
1557
+ });
1558
+
1559
+ for (const record of records.slice(0, 2)) {
1560
+ expect(record.result).toMatchObject({
1561
+ status: "failed",
1562
+ windowStart: "2026-09-14T09:59:00.000Z",
1563
+ windowEnd: "2026-09-14T10:30:00.000Z",
1564
+ chunkMinutes: 30,
1565
+ nextChunkMinutes: 30,
1566
+ });
1567
+ expect(record.update).not.toHaveProperty("cursor");
1568
+ }
1569
+ expect(records[2]!.result).toMatchObject({
1570
+ status: "empty",
1571
+ windowEnd: "2026-09-14T10:30:00.000Z",
1572
+ chunkMinutes: 30,
1573
+ nextChunkMinutes: 60,
1574
+ });
1575
+ expect(records[2]!.update["cursor"]).toBe("2026-09-14T10:30:00.000Z");
1576
+ });
1577
+ });