@oneuptime/common 12.0.23 → 12.0.24

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 (294) hide show
  1. package/Models/DatabaseModels/DetectionRule.ts +55 -0
  2. package/Models/DatabaseModels/NetworkDeviceAutoImportRule.ts +27 -7
  3. package/Models/DatabaseModels/NetworkDeviceDiscoveryScan.ts +67 -0
  4. package/Models/DatabaseModels/Project.ts +68 -0
  5. package/Models/DatabaseModels/User.ts +68 -0
  6. package/Server/API/TelemetryAPI.ts +29 -0
  7. package/Server/Infrastructure/Postgres/SchemaMigrations/1789400000000-AddNameToNetworkDeviceDiscoveryScan.ts +28 -0
  8. package/Server/Infrastructure/Postgres/SchemaMigrations/1789500000000-AddCampaignUtmFields.ts +49 -0
  9. package/Server/Infrastructure/Postgres/SchemaMigrations/1789512000000-AddDetectionRuleDistinctCountColumns.ts +33 -0
  10. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +6 -0
  11. package/Server/Middleware/GzipRequestBody.ts +311 -0
  12. package/Server/Middleware/MultipartFormData.ts +87 -3
  13. package/Server/Services/DetectionRuleService.ts +73 -6
  14. package/Server/Services/Index.ts +205 -0
  15. package/Server/Services/NetworkDeviceDiscoveryScanService.ts +59 -0
  16. package/Server/Services/NetworkDeviceService.ts +148 -0
  17. package/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.ts +65 -0
  18. package/Server/Services/OnCallDutyPolicyFeedService.ts +1 -1
  19. package/Server/Services/ProjectService.ts +29 -19
  20. package/Server/Services/SecurityEventService.ts +90 -13
  21. package/Server/Services/StatusPageResourceService.ts +223 -0
  22. package/Server/Services/TelemetryAttributeService.ts +17 -0
  23. package/Server/Services/UserService.ts +4 -6
  24. package/Server/Types/Markdown.ts +161 -67
  25. package/Server/Utils/AI/Toolbox/OnCallTools.ts +12 -4
  26. package/Server/Utils/AnalyticsDatabase/Statement.ts +13 -1
  27. package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +297 -6
  28. package/Server/Utils/Attribution.ts +4 -0
  29. package/Server/Utils/Marketing/MarketingEventUtil.ts +45 -7
  30. package/Server/Utils/SecurityEvent/DetectionRuleEvaluator.ts +55 -3
  31. package/Server/Utils/SecurityEvent/Sigma/SigmaClickhouseCompiler.ts +21 -0
  32. package/Server/Utils/StartServer.ts +12 -32
  33. package/Server/Utils/Telemetry/LlmCostBudgetEvaluator.ts +101 -4
  34. package/Server/Utils/Telemetry/LlmMetricSpend.ts +130 -0
  35. package/Server/Views/Partials/AnalyticsConsent.ejs +159 -102
  36. package/Server/Views/Partials/Head.ejs +2 -1
  37. package/Tests/App/Dashboard/AdminUserNotificationMethodsPage.test.tsx +776 -38
  38. package/Tests/App/Dashboard/CorrelateFilterBuilder.test.tsx +359 -0
  39. package/Tests/App/Dashboard/CorrelateGraph.test.tsx +865 -0
  40. package/Tests/App/Dashboard/CorrelationGraph.test.ts +346 -0
  41. package/Tests/App/Dashboard/CriteriaFilterConditionDropdown.test.tsx +256 -0
  42. package/Tests/App/Dashboard/CriteriaFilterDefaults.test.ts +738 -0
  43. package/Tests/App/Dashboard/CriteriaFilterMonitorTypeChange.test.ts +1122 -0
  44. package/Tests/App/Dashboard/DiscoveryScanWizardValidation.test.tsx +283 -0
  45. package/Tests/App/Dashboard/ExternalStatusPageCriteriaFilter.test.ts +407 -0
  46. package/Tests/App/Dashboard/MetricSeriesInvestigateMenu.test.tsx +5 -14
  47. package/Tests/App/Dashboard/MonitorCriteriaActionToggles.test.tsx +426 -0
  48. package/Tests/App/Dashboard/MonitorStepsCriteriaAlignmentWiring.test.tsx +352 -0
  49. package/Tests/App/Dashboard/OnCallLayerCardOverrides.test.tsx +278 -0
  50. package/Tests/App/Dashboard/OnCallLayerShiftPreviewOverrides.test.ts +237 -0
  51. package/Tests/App/Dashboard/OnCallReadinessSurfaces.test.tsx +2 -2
  52. package/Tests/App/Dashboard/OnCallSchedulePreviewOverrides.test.tsx +460 -0
  53. package/Tests/App/Dashboard/SecurityEventAttributeUtil.test.ts +171 -0
  54. package/Tests/App/Dashboard/SecurityEventCorrelation.test.ts +857 -0
  55. package/Tests/App/Dashboard/SecurityEventDetailObservables.test.tsx +194 -0
  56. package/Tests/App/Dashboard/SecurityEventsDetectionRulesPage.test.tsx +124 -0
  57. package/Tests/App/Dashboard/SecurityEventsMonitorStepForm.test.tsx +79 -19
  58. package/Tests/App/Dashboard/SecurityEventsTableColumns.test.tsx +382 -0
  59. package/Tests/App/StatusPage/BulkAddStatusPageMonitors.test.ts +216 -0
  60. package/Tests/App/StatusPage/BulkAddStatusPageMonitorsModal.test.tsx +590 -11
  61. package/Tests/App/StatusPage/StatusPageResourceExplorer.test.tsx +757 -0
  62. package/Tests/Models/DiscoveryScanNameColumn.test.ts +293 -0
  63. package/Tests/Server/API/SecurityEventAttributesAPI.test.ts +564 -0
  64. package/Tests/Server/Infrastructure/Postgres/SchemaMigrationsOrdering.test.ts +120 -4
  65. package/Tests/Server/Middleware/GzipRequestBody.test.ts +725 -0
  66. package/Tests/Server/Middleware/MultipartFormData.test.ts +298 -0
  67. package/Tests/Server/Services/AddDetectionRuleDistinctCountColumnsMigration.test.ts +311 -0
  68. package/Tests/Server/Services/DetectionRuleService.test.ts +134 -2
  69. package/Tests/Server/Services/DiscoveryScanClaimHookFreeSafety.test.ts +7 -1
  70. package/Tests/Server/Services/FeedRetentionConsistency.test.ts +131 -0
  71. package/Tests/Server/Services/NetworkDeviceDiscoveryScanService.test.ts +194 -0
  72. package/Tests/Server/Services/NetworkDeviceMonitorStatusStamp.test.ts +800 -0
  73. package/Tests/Server/Services/OnCallScheduleAttachRosterRefresh.test.ts +244 -0
  74. package/Tests/Server/Services/SecurityEventService.test.ts +376 -0
  75. package/Tests/Server/Services/StatusPageResourceDuplicates.test.ts +703 -0
  76. package/Tests/Server/Services/TelemetryAttributeServiceSecurityEvent.test.ts +256 -0
  77. package/Tests/Server/Types/Workflow/Components/ComponentRegistryParity.test.ts +417 -0
  78. package/Tests/Server/Utils/AnalyticsDatabase/StatementGeneratorWhereOperators.test.ts +677 -0
  79. package/Tests/Server/Utils/Marketing/MarketingEventUtil.test.ts +43 -0
  80. package/Tests/Server/Utils/Monitor/Criteria/ProfileMonitorCriteria.test.ts +15 -11
  81. package/Tests/Server/Utils/SecurityEvent/DetectionRuleEvaluator.test.ts +369 -8
  82. package/Tests/Server/Utils/SecurityEvent/SigmaClickhouseCompiler.test.ts +35 -0
  83. package/Tests/Server/Utils/Telemetry/LlmCostBudgetEvaluator.test.ts +368 -4
  84. package/Tests/Server/Utils/Telemetry/LlmMetricSpend.test.ts +245 -0
  85. package/Tests/Server/Views/AnalyticsConsentPartial.test.ts +48 -5
  86. package/Tests/Types/Code/YamlSyntax.test.ts +396 -0
  87. package/Tests/Types/Monitor/MetricAndProfileDefaultCriteria.test.ts +957 -0
  88. package/Tests/Types/Monitor/MonitorCriteriaActionValidation.test.ts +598 -0
  89. package/Tests/Types/Monitor/MonitorCriteriaInstance.test.ts +49 -0
  90. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationNotificationMode.test.ts +6 -6
  91. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationSeverityMapper.test.ts +4 -4
  92. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationUtil.test.ts +4 -3
  93. package/Tests/Types/Telemetry/LlmMetricConventions.test.ts +193 -0
  94. package/Tests/UI/Components/AiInvestigationSettingsCard.test.tsx +0 -12
  95. package/Tests/UI/Components/CardModelDetailEdit.test.tsx +1 -17
  96. package/Tests/UI/Components/CodeEditor.test.tsx +474 -10
  97. package/Tests/UI/Components/Detail/YamlField.test.tsx +105 -0
  98. package/Tests/UI/Components/EntityDropdownLabelsBulkAdd.test.tsx +319 -0
  99. package/Tests/UI/Components/Forms/FormFieldYaml.test.tsx +231 -0
  100. package/Tests/UI/Components/Forms/Utils/FormFieldSchemaTypeUtil.test.ts +1 -0
  101. package/Tests/UI/Components/Forms/ValidationYAML.test.ts +234 -0
  102. package/Tests/UI/Components/ModelTable/AttributeColumns.test.tsx +573 -0
  103. package/Tests/UI/Components/ModelTable/BaseModelTableAttributeColumns.test.tsx +824 -0
  104. package/Tests/UI/Components/ModelTable/ColumnCustomizationModalAddableColumns.test.tsx +707 -0
  105. package/Tests/UI/Components/SideOver.test.tsx +1 -1
  106. package/Tests/UI/Components/StatusPage/ResourceGridSelection.test.tsx +294 -0
  107. package/Tests/UI/Components/StatusPage/ResourceListSelection.test.tsx +436 -0
  108. package/Tests/UI/Components/TableBulkCsvExport.test.tsx +6 -2
  109. package/Tests/UI/Components/Toggle.test.tsx +25 -0
  110. package/Tests/UI/Components/YamlEditor.test.tsx +847 -0
  111. package/Tests/Utils/NetworkDevice/DeviceReachabilityUtil.test.ts +252 -0
  112. package/Tests/Utils/NetworkDiscovery/DiscoveredHostUtil.test.ts +221 -0
  113. package/Tests/Utils/NetworkDiscovery/ScanNameUtil.test.ts +312 -0
  114. package/Tests/Utils/StatusPage/ResourceExplorer.test.ts +468 -0
  115. package/Tests/Utils/Telemetry/LlmMetricQuery.test.ts +472 -0
  116. package/Types/Code/YamlSyntax.ts +396 -0
  117. package/Types/Exception/ExceptionCode.ts +1 -0
  118. package/Types/Exception/PayloadTooLargeException.ts +8 -0
  119. package/Types/Marketing/Attribution.ts +10 -0
  120. package/Types/Marketing/MarketingEvent.ts +4 -0
  121. package/Types/Monitor/MonitorCriteriaInstance.ts +116 -56
  122. package/Types/Monitor/Recommendation/MonitorRecommendationSeverityMapper.ts +2 -1
  123. package/Types/SecurityEvent/DetectionFindingConstants.ts +11 -0
  124. package/Types/Telemetry/LlmMetricConventions.ts +124 -0
  125. package/UI/Components/AutocompleteTextInput/AutocompleteTextInput.tsx +3 -0
  126. package/UI/Components/BulkUpdate/BulkUpdateForm.tsx +10 -1
  127. package/UI/Components/CodeEditor/CodeEditor.tsx +241 -79
  128. package/UI/Components/CodeEditor/YamlEditor.tsx +369 -0
  129. package/UI/Components/Detail/Detail.tsx +10 -0
  130. package/UI/Components/EntityDropdown/EntityDropdown.tsx +33 -0
  131. package/UI/Components/Forms/Fields/FormField.tsx +31 -0
  132. package/UI/Components/Forms/Types/Field.ts +8 -0
  133. package/UI/Components/Forms/Types/FormFieldSchemaType.ts +1 -0
  134. package/UI/Components/Forms/Utils/FormFieldSchemaTypeUtil.ts +2 -0
  135. package/UI/Components/Forms/Validation.ts +46 -0
  136. package/UI/Components/Input/Input.tsx +3 -0
  137. package/UI/Components/ModelTable/AttributeColumns.tsx +321 -0
  138. package/UI/Components/ModelTable/BaseModelTable.tsx +263 -14
  139. package/UI/Components/ModelTable/Column.ts +8 -0
  140. package/UI/Components/ModelTable/ColumnCustomizationModal.tsx +310 -0
  141. package/UI/Components/ModelTable/ColumnPreference.ts +13 -1
  142. package/UI/Components/MonitorTemplateVariables/TemplateVariablesModal.tsx +2 -2
  143. package/UI/Components/SideOver/SideOver.tsx +5 -5
  144. package/UI/Components/StatusPage/ResourceGrid.tsx +82 -2
  145. package/UI/Components/StatusPage/ResourceList.tsx +197 -38
  146. package/UI/Components/Toggle/Toggle.tsx +9 -1
  147. package/UI/Components/Types/FieldType.ts +1 -0
  148. package/Utils/NetworkDevice/DeviceReachabilityUtil.ts +73 -1
  149. package/Utils/NetworkDiscovery/ScanNameUtil.ts +181 -0
  150. package/Utils/StatusPage/ResourceExplorer.ts +258 -0
  151. package/Utils/Telemetry/LlmMetricQuery.ts +204 -0
  152. package/build/dist/Models/DatabaseModels/DetectionRule.js +57 -0
  153. package/build/dist/Models/DatabaseModels/DetectionRule.js.map +1 -1
  154. package/build/dist/Models/DatabaseModels/NetworkDeviceAutoImportRule.js +27 -7
  155. package/build/dist/Models/DatabaseModels/NetworkDeviceAutoImportRule.js.map +1 -1
  156. package/build/dist/Models/DatabaseModels/NetworkDeviceDiscoveryScan.js +69 -0
  157. package/build/dist/Models/DatabaseModels/NetworkDeviceDiscoveryScan.js.map +1 -1
  158. package/build/dist/Models/DatabaseModels/Project.js +76 -0
  159. package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
  160. package/build/dist/Models/DatabaseModels/User.js +76 -0
  161. package/build/dist/Models/DatabaseModels/User.js.map +1 -1
  162. package/build/dist/Server/API/TelemetryAPI.js +17 -0
  163. package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
  164. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789400000000-AddNameToNetworkDeviceDiscoveryScan.js +21 -0
  165. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789400000000-AddNameToNetworkDeviceDiscoveryScan.js.map +1 -0
  166. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789500000000-AddCampaignUtmFields.js +42 -0
  167. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789500000000-AddCampaignUtmFields.js.map +1 -0
  168. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789512000000-AddDetectionRuleDistinctCountColumns.js +22 -0
  169. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789512000000-AddDetectionRuleDistinctCountColumns.js.map +1 -0
  170. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +6 -0
  171. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  172. package/build/dist/Server/Middleware/GzipRequestBody.js +224 -0
  173. package/build/dist/Server/Middleware/GzipRequestBody.js.map +1 -0
  174. package/build/dist/Server/Middleware/MultipartFormData.js +71 -2
  175. package/build/dist/Server/Middleware/MultipartFormData.js.map +1 -1
  176. package/build/dist/Server/Services/DetectionRuleService.js +49 -4
  177. package/build/dist/Server/Services/DetectionRuleService.js.map +1 -1
  178. package/build/dist/Server/Services/Index.js +204 -0
  179. package/build/dist/Server/Services/Index.js.map +1 -1
  180. package/build/dist/Server/Services/NetworkDeviceDiscoveryScanService.js +50 -0
  181. package/build/dist/Server/Services/NetworkDeviceDiscoveryScanService.js.map +1 -1
  182. package/build/dist/Server/Services/NetworkDeviceService.js +124 -0
  183. package/build/dist/Server/Services/NetworkDeviceService.js.map +1 -1
  184. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js +68 -13
  185. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js.map +1 -1
  186. package/build/dist/Server/Services/OnCallDutyPolicyFeedService.js +1 -1
  187. package/build/dist/Server/Services/OnCallDutyPolicyFeedService.js.map +1 -1
  188. package/build/dist/Server/Services/ProjectService.js +25 -45
  189. package/build/dist/Server/Services/ProjectService.js.map +1 -1
  190. package/build/dist/Server/Services/SecurityEventService.js +57 -5
  191. package/build/dist/Server/Services/SecurityEventService.js.map +1 -1
  192. package/build/dist/Server/Services/StatusPageResourceService.js +140 -0
  193. package/build/dist/Server/Services/StatusPageResourceService.js.map +1 -1
  194. package/build/dist/Server/Services/TelemetryAttributeService.js +17 -0
  195. package/build/dist/Server/Services/TelemetryAttributeService.js.map +1 -1
  196. package/build/dist/Server/Services/UserService.js +2 -12
  197. package/build/dist/Server/Services/UserService.js.map +1 -1
  198. package/build/dist/Server/Types/Markdown.js +147 -61
  199. package/build/dist/Server/Types/Markdown.js.map +1 -1
  200. package/build/dist/Server/Utils/AI/Toolbox/OnCallTools.js +12 -4
  201. package/build/dist/Server/Utils/AI/Toolbox/OnCallTools.js.map +1 -1
  202. package/build/dist/Server/Utils/AnalyticsDatabase/Statement.js +12 -1
  203. package/build/dist/Server/Utils/AnalyticsDatabase/Statement.js.map +1 -1
  204. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +246 -9
  205. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js.map +1 -1
  206. package/build/dist/Server/Utils/Attribution.js.map +1 -1
  207. package/build/dist/Server/Utils/Marketing/MarketingEventUtil.js +27 -7
  208. package/build/dist/Server/Utils/Marketing/MarketingEventUtil.js.map +1 -1
  209. package/build/dist/Server/Utils/SecurityEvent/DetectionRuleEvaluator.js +42 -7
  210. package/build/dist/Server/Utils/SecurityEvent/DetectionRuleEvaluator.js.map +1 -1
  211. package/build/dist/Server/Utils/SecurityEvent/Sigma/SigmaClickhouseCompiler.js +18 -0
  212. package/build/dist/Server/Utils/SecurityEvent/Sigma/SigmaClickhouseCompiler.js.map +1 -1
  213. package/build/dist/Server/Utils/StartServer.js +12 -21
  214. package/build/dist/Server/Utils/StartServer.js.map +1 -1
  215. package/build/dist/Server/Utils/Telemetry/LlmCostBudgetEvaluator.js +76 -5
  216. package/build/dist/Server/Utils/Telemetry/LlmCostBudgetEvaluator.js.map +1 -1
  217. package/build/dist/Server/Utils/Telemetry/LlmMetricSpend.js +102 -0
  218. package/build/dist/Server/Utils/Telemetry/LlmMetricSpend.js.map +1 -0
  219. package/build/dist/Types/Code/YamlSyntax.js +212 -0
  220. package/build/dist/Types/Code/YamlSyntax.js.map +1 -0
  221. package/build/dist/Types/Exception/ExceptionCode.js +1 -0
  222. package/build/dist/Types/Exception/ExceptionCode.js.map +1 -1
  223. package/build/dist/Types/Exception/PayloadTooLargeException.js +8 -0
  224. package/build/dist/Types/Exception/PayloadTooLargeException.js.map +1 -0
  225. package/build/dist/Types/Marketing/Attribution.js +10 -0
  226. package/build/dist/Types/Marketing/Attribution.js.map +1 -1
  227. package/build/dist/Types/Monitor/MonitorCriteriaInstance.js +112 -51
  228. package/build/dist/Types/Monitor/MonitorCriteriaInstance.js.map +1 -1
  229. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationSeverityMapper.js +2 -1
  230. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationSeverityMapper.js.map +1 -1
  231. package/build/dist/Types/SecurityEvent/DetectionFindingConstants.js +9 -0
  232. package/build/dist/Types/SecurityEvent/DetectionFindingConstants.js.map +1 -1
  233. package/build/dist/Types/Telemetry/LlmMetricConventions.js +98 -0
  234. package/build/dist/Types/Telemetry/LlmMetricConventions.js.map +1 -0
  235. package/build/dist/UI/Components/AutocompleteTextInput/AutocompleteTextInput.js +1 -1
  236. package/build/dist/UI/Components/AutocompleteTextInput/AutocompleteTextInput.js.map +1 -1
  237. package/build/dist/UI/Components/BulkUpdate/BulkUpdateForm.js +5 -2
  238. package/build/dist/UI/Components/BulkUpdate/BulkUpdateForm.js.map +1 -1
  239. package/build/dist/UI/Components/CodeEditor/CodeEditor.js +206 -76
  240. package/build/dist/UI/Components/CodeEditor/CodeEditor.js.map +1 -1
  241. package/build/dist/UI/Components/CodeEditor/YamlEditor.js +201 -0
  242. package/build/dist/UI/Components/CodeEditor/YamlEditor.js.map +1 -0
  243. package/build/dist/UI/Components/Detail/Detail.js +9 -0
  244. package/build/dist/UI/Components/Detail/Detail.js.map +1 -1
  245. package/build/dist/UI/Components/EntityDropdown/EntityDropdown.js +16 -1
  246. package/build/dist/UI/Components/EntityDropdown/EntityDropdown.js.map +1 -1
  247. package/build/dist/UI/Components/Forms/Fields/FormField.js +14 -1
  248. package/build/dist/UI/Components/Forms/Fields/FormField.js.map +1 -1
  249. package/build/dist/UI/Components/Forms/Types/Field.js.map +1 -1
  250. package/build/dist/UI/Components/Forms/Types/FormFieldSchemaType.js +1 -0
  251. package/build/dist/UI/Components/Forms/Types/FormFieldSchemaType.js.map +1 -1
  252. package/build/dist/UI/Components/Forms/Utils/FormFieldSchemaTypeUtil.js +2 -0
  253. package/build/dist/UI/Components/Forms/Utils/FormFieldSchemaTypeUtil.js.map +1 -1
  254. package/build/dist/UI/Components/Forms/Validation.js +27 -0
  255. package/build/dist/UI/Components/Forms/Validation.js.map +1 -1
  256. package/build/dist/UI/Components/Input/Input.js +1 -1
  257. package/build/dist/UI/Components/Input/Input.js.map +1 -1
  258. package/build/dist/UI/Components/ModelTable/AttributeColumns.js +159 -0
  259. package/build/dist/UI/Components/ModelTable/AttributeColumns.js.map +1 -0
  260. package/build/dist/UI/Components/ModelTable/BaseModelTable.js +179 -17
  261. package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
  262. package/build/dist/UI/Components/ModelTable/ColumnCustomizationModal.js +133 -2
  263. package/build/dist/UI/Components/ModelTable/ColumnCustomizationModal.js.map +1 -1
  264. package/build/dist/UI/Components/ModelTable/ColumnPreference.js +7 -1
  265. package/build/dist/UI/Components/ModelTable/ColumnPreference.js.map +1 -1
  266. package/build/dist/UI/Components/MonitorTemplateVariables/TemplateVariablesModal.js +2 -2
  267. package/build/dist/UI/Components/SideOver/SideOver.js +5 -5
  268. package/build/dist/UI/Components/StatusPage/ResourceGrid.js +28 -2
  269. package/build/dist/UI/Components/StatusPage/ResourceGrid.js.map +1 -1
  270. package/build/dist/UI/Components/StatusPage/ResourceList.js +87 -20
  271. package/build/dist/UI/Components/StatusPage/ResourceList.js.map +1 -1
  272. package/build/dist/UI/Components/Toggle/Toggle.js +9 -1
  273. package/build/dist/UI/Components/Toggle/Toggle.js.map +1 -1
  274. package/build/dist/UI/Components/Types/FieldType.js +1 -0
  275. package/build/dist/UI/Components/Types/FieldType.js.map +1 -1
  276. package/build/dist/Utils/NetworkDevice/DeviceReachabilityUtil.js +37 -1
  277. package/build/dist/Utils/NetworkDevice/DeviceReachabilityUtil.js.map +1 -1
  278. package/build/dist/Utils/NetworkDiscovery/ScanNameUtil.js +120 -0
  279. package/build/dist/Utils/NetworkDiscovery/ScanNameUtil.js.map +1 -0
  280. package/build/dist/Utils/StatusPage/ResourceExplorer.js +171 -0
  281. package/build/dist/Utils/StatusPage/ResourceExplorer.js.map +1 -1
  282. package/build/dist/Utils/Telemetry/LlmMetricQuery.js +119 -0
  283. package/build/dist/Utils/Telemetry/LlmMetricQuery.js.map +1 -0
  284. package/jest.config.json +2 -0
  285. package/package.json +1 -1
  286. package/tsconfig.json +2 -1
  287. package/Tests/UI/Components/Toast.test.tsx +0 -197
  288. package/Tests/UI/Components/ToastStacking.test.tsx +0 -198
  289. package/UI/Components/Toast/Toast.tsx +0 -189
  290. package/UI/Components/Toast/ToastInit.tsx +0 -110
  291. package/build/dist/UI/Components/Toast/Toast.js +0 -123
  292. package/build/dist/UI/Components/Toast/Toast.js.map +0 -1
  293. package/build/dist/UI/Components/Toast/ToastInit.js +0 -53
  294. package/build/dist/UI/Components/Toast/ToastInit.js.map +0 -1
@@ -0,0 +1,244 @@
1
+ import OnCallDutyPolicyEscalationRuleScheduleService from "../../../Server/Services/OnCallDutyPolicyEscalationRuleScheduleService";
2
+ import OnCallDutyPolicyScheduleService from "../../../Server/Services/OnCallDutyPolicyScheduleService";
3
+ import OnCallDutyPolicyFeedService from "../../../Server/Services/OnCallDutyPolicyFeedService";
4
+ import OnCallDutyPolicyTimeLogService from "../../../Server/Services/OnCallDutyPolicyTimeLogService";
5
+ import logger from "../../../Server/Utils/Logger";
6
+ import ObjectID from "../../../Types/ObjectID";
7
+ import { afterEach, describe, expect, jest, test } from "@jest/globals";
8
+
9
+ /*
10
+ * https://github.com/OneUptime/oneuptime/issues/3411
11
+ *
12
+ * The persisted roster columns on OnCallDutyPolicySchedule (currentUserIdOnRoster
13
+ * and friends) are derived state, and one of their inputs is WHICH on-call
14
+ * policies escalate to the schedule: getSingleAttachedPolicyId resolves the
15
+ * roster in a policy's context only when exactly one policy is attached, which
16
+ * is what makes that policy's POLICY-SCOPED user overrides count.
17
+ *
18
+ * Attaching or detaching a schedule therefore changes the correct answer. Every
19
+ * other input - layers, layer users, overrides - already refreshes the roster on
20
+ * change; this one did not, so after attaching a schedule to its first policy
21
+ * the roster kept the pre-attach, policy-blind answer until the next natural
22
+ * hand-off. The schedule page's "X is currently on the roster" banner and the
23
+ * "On Call Now" column then named the originally-scheduled user for the whole
24
+ * override window while alert routing - which always resolves live, with the
25
+ * policy id - paged the substitute.
26
+ *
27
+ * The hooks are protected, so they are invoked through `as any`, the same way
28
+ * OnCallDutyPolicyUserOverrideEdit.test.ts does.
29
+ */
30
+
31
+ const PROJECT_ID: ObjectID = new ObjectID("project-1");
32
+ const SCHEDULE_ID: ObjectID = new ObjectID("schedule-1");
33
+ const OTHER_SCHEDULE_ID: ObjectID = new ObjectID("schedule-2");
34
+ const POLICY_ID: ObjectID = new ObjectID("policy-1");
35
+ const RULE_ID: ObjectID = new ObjectID("rule-1");
36
+ const LINK_ID: ObjectID = new ObjectID("link-1");
37
+
38
+ function silenceLoggerError(): void {
39
+ jest.spyOn(logger, "error").mockImplementation((): void => {
40
+ return undefined;
41
+ });
42
+ }
43
+
44
+ /*
45
+ * Everything the hooks touch besides the roster refresh: feed items, time logs
46
+ * and the live roster lookup. Stubbed so each test asserts one thing - which
47
+ * schedules got their roster re-resolved.
48
+ */
49
+ function stubCollaborators(options?: {
50
+ currentUserInSchedule?: ObjectID | null;
51
+ }): void {
52
+ jest
53
+ .spyOn(OnCallDutyPolicyScheduleService, "getCurrentUserIdInSchedule")
54
+ .mockResolvedValue(
55
+ options?.currentUserInSchedule === undefined
56
+ ? null
57
+ : options.currentUserInSchedule,
58
+ );
59
+
60
+ jest
61
+ .spyOn(OnCallDutyPolicyFeedService, "createOnCallDutyPolicyFeedItem")
62
+ .mockResolvedValue(undefined as never);
63
+
64
+ jest
65
+ .spyOn(OnCallDutyPolicyTimeLogService, "startTimeLogForUser")
66
+ .mockResolvedValue(undefined as never);
67
+
68
+ jest
69
+ .spyOn(OnCallDutyPolicyTimeLogService, "endTimeLogForUser")
70
+ .mockResolvedValue(undefined as never);
71
+ }
72
+
73
+ /*
74
+ * Typed loosely on purpose: jest.SpiedFunction is invariant in its call
75
+ * signature, so the precise spy type does not widen to SpiedFunction<any> and
76
+ * every call site would need the exact generic restated.
77
+ */
78
+ function spyOnRosterRefresh(): any {
79
+ return jest
80
+ .spyOn(
81
+ OnCallDutyPolicyScheduleService,
82
+ "refreshCurrentUserIdAndHandoffTimeInSchedule",
83
+ )
84
+ .mockResolvedValue({
85
+ currentUserId: null,
86
+ handOffTimeAt: null,
87
+ nextUserId: null,
88
+ nextHandOffTimeAt: null,
89
+ rosterStartAt: null,
90
+ nextRosterStartAt: null,
91
+ });
92
+ }
93
+
94
+ function refreshedScheduleIds(spy: any): Array<string> {
95
+ return spy.mock.calls.map((call: Array<unknown>): string => {
96
+ return (call[0] as ObjectID).toString();
97
+ });
98
+ }
99
+
100
+ function linkRow(scheduleId: ObjectID): any {
101
+ return {
102
+ id: LINK_ID,
103
+ projectId: PROJECT_ID,
104
+ onCallDutyPolicyScheduleId: scheduleId,
105
+ onCallDutyPolicySchedule: { name: "Primary schedule" },
106
+ onCallDutyPolicyEscalationRule: { name: "Rule 1", id: RULE_ID, order: 1 },
107
+ onCallDutyPolicy: { name: "Payments", id: POLICY_ID },
108
+ createdByUserId: null,
109
+ };
110
+ }
111
+
112
+ describe("Attaching / detaching a schedule re-resolves its persisted roster (issue #3411)", () => {
113
+ afterEach(() => {
114
+ jest.restoreAllMocks();
115
+ });
116
+
117
+ test("onCreateSuccess refreshes the attached schedule's roster", async () => {
118
+ silenceLoggerError();
119
+ stubCollaborators();
120
+
121
+ jest
122
+ .spyOn(OnCallDutyPolicyEscalationRuleScheduleService, "findOneById")
123
+ .mockResolvedValue(linkRow(SCHEDULE_ID));
124
+
125
+ const refresh: any = spyOnRosterRefresh();
126
+
127
+ await (
128
+ OnCallDutyPolicyEscalationRuleScheduleService as any
129
+ ).onCreateSuccess({}, { id: LINK_ID });
130
+
131
+ expect(refreshedScheduleIds(refresh)).toEqual([SCHEDULE_ID.toString()]);
132
+ });
133
+
134
+ /*
135
+ * The refresh has to happen even when the schedule currently pages nobody:
136
+ * that is precisely the state where the roster is most misleading, and the
137
+ * notification/time-log code below the refresh returns early there. Placing
138
+ * the refresh after those early returns would have made this case a silent
139
+ * no-op.
140
+ */
141
+ test("onCreateSuccess refreshes even when nobody is on call in the schedule", async () => {
142
+ silenceLoggerError();
143
+ stubCollaborators({ currentUserInSchedule: null });
144
+
145
+ jest
146
+ .spyOn(OnCallDutyPolicyEscalationRuleScheduleService, "findOneById")
147
+ .mockResolvedValue(linkRow(SCHEDULE_ID));
148
+
149
+ const refresh: any = spyOnRosterRefresh();
150
+
151
+ await (
152
+ OnCallDutyPolicyEscalationRuleScheduleService as any
153
+ ).onCreateSuccess({}, { id: LINK_ID });
154
+
155
+ expect(refresh).toHaveBeenCalledTimes(1);
156
+ });
157
+
158
+ test("a failing roster refresh does not fail the attach", async () => {
159
+ silenceLoggerError();
160
+ stubCollaborators();
161
+
162
+ jest
163
+ .spyOn(OnCallDutyPolicyEscalationRuleScheduleService, "findOneById")
164
+ .mockResolvedValue(linkRow(SCHEDULE_ID));
165
+
166
+ jest
167
+ .spyOn(
168
+ OnCallDutyPolicyScheduleService,
169
+ "refreshCurrentUserIdAndHandoffTimeInSchedule",
170
+ )
171
+ .mockRejectedValue(new Error("redis down"));
172
+
173
+ await expect(
174
+ (OnCallDutyPolicyEscalationRuleScheduleService as any).onCreateSuccess(
175
+ {},
176
+ { id: LINK_ID },
177
+ ),
178
+ ).resolves.toBeDefined();
179
+ });
180
+
181
+ test("onDeleteSuccess refreshes the detached schedule's roster", async () => {
182
+ silenceLoggerError();
183
+ stubCollaborators();
184
+
185
+ const refresh: any = spyOnRosterRefresh();
186
+
187
+ await (
188
+ OnCallDutyPolicyEscalationRuleScheduleService as any
189
+ ).onDeleteSuccess(
190
+ { carryForward: { deletedItems: [linkRow(SCHEDULE_ID)] } },
191
+ [LINK_ID],
192
+ );
193
+
194
+ expect(refreshedScheduleIds(refresh)).toEqual([SCHEDULE_ID.toString()]);
195
+ });
196
+
197
+ /*
198
+ * Removing one schedule from several escalation rules of the same policy is
199
+ * ONE context change. Refreshing per row would re-diff the same roster
200
+ * repeatedly, and each diff can emit a hand-off page - so a tidy-up that
201
+ * removes three rules must not ring the same phone three times.
202
+ */
203
+ test("onDeleteSuccess refreshes each affected schedule exactly once", async () => {
204
+ silenceLoggerError();
205
+ stubCollaborators();
206
+
207
+ const refresh: any = spyOnRosterRefresh();
208
+
209
+ await (
210
+ OnCallDutyPolicyEscalationRuleScheduleService as any
211
+ ).onDeleteSuccess(
212
+ {
213
+ carryForward: {
214
+ deletedItems: [
215
+ linkRow(SCHEDULE_ID),
216
+ linkRow(SCHEDULE_ID),
217
+ linkRow(OTHER_SCHEDULE_ID),
218
+ ],
219
+ },
220
+ },
221
+ [LINK_ID],
222
+ );
223
+
224
+ expect(refreshedScheduleIds(refresh).sort()).toEqual(
225
+ [OTHER_SCHEDULE_ID.toString(), SCHEDULE_ID.toString()].sort(),
226
+ );
227
+ });
228
+
229
+ test("onDeleteSuccess refreshes before the per-item early return for an uncovered schedule", async () => {
230
+ silenceLoggerError();
231
+ stubCollaborators({ currentUserInSchedule: null });
232
+
233
+ const refresh: any = spyOnRosterRefresh();
234
+
235
+ await (
236
+ OnCallDutyPolicyEscalationRuleScheduleService as any
237
+ ).onDeleteSuccess(
238
+ { carryForward: { deletedItems: [linkRow(SCHEDULE_ID)] } },
239
+ [LINK_ID],
240
+ );
241
+
242
+ expect(refresh).toHaveBeenCalledTimes(1);
243
+ });
244
+ });
@@ -0,0 +1,376 @@
1
+ import SecurityEventService, {
2
+ DetectionMatchGroup,
3
+ FindDetectionMatchesData,
4
+ } from "../../../Server/Services/SecurityEventService";
5
+ import { Results } from "../../../Server/Services/AnalyticsDatabaseService";
6
+ import { getQuerySettings } from "../../../Server/Utils/AnalyticsDatabase/QuerySettingsHelper";
7
+ import {
8
+ SQL,
9
+ Statement,
10
+ } from "../../../Server/Utils/AnalyticsDatabase/Statement";
11
+ import TableColumnType from "../../../Types/AnalyticsDatabase/TableColumnType";
12
+ import { JSONObject } from "../../../Types/JSON";
13
+ import ObjectID from "../../../Types/ObjectID";
14
+ import OneUptimeDate from "../../../Types/Date";
15
+ import { getJestSpyOn } from "../../Spy";
16
+ import { afterEach, describe, expect, jest, test } from "@jest/globals";
17
+
18
+ /*
19
+ * The SQL contract of SecurityEventService.findDetectionMatches — the one
20
+ * query the detection engine runs per due rule (issue #3398 added the
21
+ * distinct-count semantics).
22
+ *
23
+ * Everything asserted here is invisible from the TypeScript call site and
24
+ * only observable as query text / bound params, so this file pins the
25
+ * rendered Statement:
26
+ *
27
+ * - a rule WITHOUT a distinct-count field selects a constant
28
+ * `0 AS distinctCount` (never an aggregate), gets NO HAVING clause at
29
+ * threshold 1, and orders by matchCount;
30
+ * - a raw-count threshold > 1 becomes `HAVING matchCount >= {bound}`;
31
+ * - a distinct-count rule counts `uniqExact(nullIf(<expr>, ''))` — empty
32
+ * values excluded — thresholds and orders on distinctCount, and gets
33
+ * the HAVING clause EVEN at threshold 1, because a group whose matches
34
+ * all lack the counted field has distinctCount 0 and must not fire;
35
+ * - the threshold is clamped to >= 1 before it is bound;
36
+ * - projectId / time-window predicates and the maxGroups LIMIT stay
37
+ * bound parameters, whatever the threshold shape;
38
+ * - result rows map distinctCount (default 0 when the column is absent)
39
+ * without disturbing the existing field mapping.
40
+ *
41
+ * Every service boundary is stubbed: executeQuery captures the Statement,
42
+ * and the database handle only supplies the schema name. No ClickHouse.
43
+ */
44
+
45
+ type Spy = ReturnType<typeof getJestSpyOn>;
46
+
47
+ const PROJECT_ID: ObjectID = new ObjectID(
48
+ "22222222-2222-4222-8222-222222222222",
49
+ );
50
+ const START_TIME: Date = new Date("2026-08-25T10:00:00.000Z");
51
+ const END_TIME: Date = new Date("2026-08-25T10:05:00.000Z");
52
+ const DATABASE_NAME: string = "oneuptime";
53
+ const MAX_GROUPS: number = 50;
54
+
55
+ // The trailing SETTINGS clause is shared verbatim by every variant below.
56
+ const SETTINGS_SUFFIX: string = getQuerySettings({
57
+ maxExecutionTimeInSeconds: 60,
58
+ });
59
+
60
+ /*
61
+ * Captures the Statement handed to executeQuery, returning canned rows.
62
+ * `rows: undefined` simulates a response body with no `data` key at all.
63
+ */
64
+ function stubExecuteQuery(rows?: Array<JSONObject>): Spy {
65
+ getJestSpyOn(
66
+ SecurityEventService.database,
67
+ "getDatasourceOptions",
68
+ ).mockReturnValue({ database: DATABASE_NAME } as never);
69
+
70
+ const fakeResult: Results = {
71
+ json: (): Promise<unknown> => {
72
+ return Promise.resolve(rows === undefined ? {} : { data: rows });
73
+ },
74
+ } as unknown as Results;
75
+
76
+ return getJestSpyOn(SecurityEventService, "executeQuery").mockResolvedValue(
77
+ fakeResult as never,
78
+ );
79
+ }
80
+
81
+ /*
82
+ * Fragments the way SigmaClickhouseCompiler hands them over: every
83
+ * rule-controlled value is a bound parameter, field references are bound
84
+ * Identifiers.
85
+ */
86
+ function buildWhereFragment(): Statement {
87
+ return SQL`statusName = ${{
88
+ type: TableColumnType.Text,
89
+ value: "Failure",
90
+ }}`;
91
+ }
92
+
93
+ function buildFieldExpression(field: string): Statement {
94
+ return SQL`${field}`;
95
+ }
96
+
97
+ async function callFindDetectionMatches(
98
+ overrides: Partial<FindDetectionMatchesData> = {},
99
+ ): Promise<Array<DetectionMatchGroup>> {
100
+ return SecurityEventService.findDetectionMatches({
101
+ projectId: PROJECT_ID,
102
+ startTime: START_TIME,
103
+ endTime: END_TIME,
104
+ whereFragment: buildWhereFragment(),
105
+ groupByExpression: null,
106
+ distinctCountExpression: null,
107
+ minMatchCount: 1,
108
+ maxGroups: MAX_GROUPS,
109
+ ...overrides,
110
+ });
111
+ }
112
+
113
+ function capturedStatement(spy: Spy): Statement {
114
+ expect(spy).toHaveBeenCalledTimes(1);
115
+ return spy.mock.calls[0]![0] as Statement;
116
+ }
117
+
118
+ describe("SecurityEventService.findDetectionMatches — SQL contract", () => {
119
+ afterEach(() => {
120
+ jest.restoreAllMocks();
121
+ });
122
+
123
+ describe("no distinct-count expression, threshold 1 (the pre-#3398 shape)", () => {
124
+ test("selects a constant 0 distinctCount, emits no HAVING, and orders by matchCount", async () => {
125
+ const spy: Spy = stubExecuteQuery([]);
126
+
127
+ await callFindDetectionMatches();
128
+
129
+ const statement: Statement = capturedStatement(spy);
130
+
131
+ /*
132
+ * Full-text equality: this is the exact query every existing rule
133
+ * (threshold 1, no distinct field) runs, so nothing about it may
134
+ * drift as a side effect of the new parameters.
135
+ */
136
+ expect(statement.query).toBe(
137
+ "SELECT '' AS groupValue, count() AS matchCount, 0 AS distinctCount, " +
138
+ "any(message) AS sampleMessage, " +
139
+ "arrayDistinct(arrayFlatten(groupArray(20)(observables))) AS sampleObservables " +
140
+ `FROM ${DATABASE_NAME}.SecurityEventItemV1 ` +
141
+ "WHERE projectId = {p0:String} AND time >= {p1:DateTime64(9)} AND time < {p2:DateTime64(9)} " +
142
+ "AND (statusName = {p3:String}) " +
143
+ "GROUP BY groupValue ORDER BY matchCount DESC LIMIT {p4:Int32}" +
144
+ SETTINGS_SUFFIX,
145
+ );
146
+
147
+ expect(statement.query).not.toContain("HAVING");
148
+ expect(statement.query).not.toContain("uniqExact");
149
+
150
+ expect(statement.query_params).toStrictEqual({
151
+ p0: PROJECT_ID.toString(),
152
+ p1: OneUptimeDate.toClickhouseDateTime64(START_TIME),
153
+ p2: OneUptimeDate.toClickhouseDateTime64(END_TIME),
154
+ p3: "Failure",
155
+ p4: MAX_GROUPS,
156
+ });
157
+ });
158
+ });
159
+
160
+ describe("raw match-count threshold above 1", () => {
161
+ test("threshold 5 becomes HAVING matchCount >= a bound param and every other binding survives the renumbering", async () => {
162
+ const spy: Spy = stubExecuteQuery([]);
163
+
164
+ await callFindDetectionMatches({ minMatchCount: 5 });
165
+
166
+ const statement: Statement = capturedStatement(spy);
167
+
168
+ expect(statement.query).toContain(
169
+ "GROUP BY groupValue HAVING matchCount >= {p4:Int32} ORDER BY matchCount DESC LIMIT {p5:Int32}",
170
+ );
171
+ // The threshold thresholds the RAW count — never the constant 0.
172
+ expect(statement.query).not.toContain("distinctCount >=");
173
+ expect(statement.query).not.toContain("ORDER BY distinctCount");
174
+
175
+ /*
176
+ * HAVING is appended mid-statement, which renumbers the params after
177
+ * it — the tenant/time/limit bindings must survive that renumbering
178
+ * with their values intact.
179
+ */
180
+ expect(statement.query).toContain(
181
+ "WHERE projectId = {p0:String} AND time >= {p1:DateTime64(9)} AND time < {p2:DateTime64(9)} AND (statusName = {p3:String})",
182
+ );
183
+
184
+ expect(statement.query_params).toStrictEqual({
185
+ p0: PROJECT_ID.toString(),
186
+ p1: OneUptimeDate.toClickhouseDateTime64(START_TIME),
187
+ p2: OneUptimeDate.toClickhouseDateTime64(END_TIME),
188
+ p3: "Failure",
189
+ p4: 5,
190
+ p5: MAX_GROUPS,
191
+ });
192
+ });
193
+ });
194
+
195
+ describe("distinct-count expression", () => {
196
+ test("aggregates uniqExact(nullIf(expr, '')) and gets HAVING distinctCount EVEN at threshold 1", async () => {
197
+ const spy: Spy = stubExecuteQuery([]);
198
+
199
+ await callFindDetectionMatches({
200
+ distinctCountExpression: buildFieldExpression("targetUser"),
201
+ minMatchCount: 1,
202
+ });
203
+
204
+ const statement: Statement = capturedStatement(spy);
205
+
206
+ /*
207
+ * nullIf(expr, '') keeps empty values out of the distinct count —
208
+ * "5 distinct usernames" must never count rows carrying no
209
+ * username. And the HAVING clause exists at threshold 1 because a
210
+ * group whose matches all lack the field has distinctCount 0.
211
+ */
212
+ expect(statement.query).toContain(
213
+ "count() AS matchCount, uniqExact(nullIf({p0:Identifier}, '')) AS distinctCount",
214
+ );
215
+ expect(statement.query).toContain(
216
+ "GROUP BY groupValue HAVING distinctCount >= {p5:Int32} ORDER BY distinctCount DESC LIMIT {p6:Int32}",
217
+ );
218
+ expect(statement.query).not.toContain("matchCount >=");
219
+
220
+ expect(statement.query_params["p0"]).toBe("targetUser");
221
+ expect(statement.query_params["p5"]).toBe(1);
222
+ expect(statement.query_params["p6"]).toBe(MAX_GROUPS);
223
+ });
224
+
225
+ test("group-by and distinct expressions bind as separate identifiers, threshold on distinctCount", async () => {
226
+ const spy: Spy = stubExecuteQuery([]);
227
+
228
+ await callFindDetectionMatches({
229
+ groupByExpression: buildFieldExpression("principalUser"),
230
+ distinctCountExpression: buildFieldExpression("targetUser"),
231
+ minMatchCount: 3,
232
+ });
233
+
234
+ const statement: Statement = capturedStatement(spy);
235
+
236
+ expect(statement.query).toContain(
237
+ "SELECT {p0:Identifier} AS groupValue, count() AS matchCount, uniqExact(nullIf({p1:Identifier}, '')) AS distinctCount",
238
+ );
239
+ expect(statement.query).toContain(
240
+ "HAVING distinctCount >= {p6:Int32} ORDER BY distinctCount DESC LIMIT {p7:Int32}",
241
+ );
242
+
243
+ expect(statement.query_params["p0"]).toBe("principalUser");
244
+ expect(statement.query_params["p1"]).toBe("targetUser");
245
+ expect(statement.query_params["p6"]).toBe(3);
246
+ expect(statement.query_params["p7"]).toBe(MAX_GROUPS);
247
+ });
248
+ });
249
+
250
+ describe("threshold clamping", () => {
251
+ test("minMatchCount 0 clamps to 1 — no HAVING on a raw-count rule", async () => {
252
+ const spy: Spy = stubExecuteQuery([]);
253
+
254
+ await callFindDetectionMatches({ minMatchCount: 0 });
255
+
256
+ const statement: Statement = capturedStatement(spy);
257
+
258
+ /*
259
+ * Clamped-to-1 raw-count rules keep the HAVING-free shape: emitting
260
+ * `HAVING matchCount >= 1` would be harmless but is pinned away so
261
+ * the pre-#3398 query stays byte-identical for existing rules.
262
+ */
263
+ expect(statement.query).not.toContain("HAVING");
264
+ expect(statement.query).toContain("ORDER BY matchCount DESC");
265
+ expect(statement.query_params["p4"]).toBe(MAX_GROUPS);
266
+ });
267
+
268
+ test("a negative minMatchCount binds 1, not the negative value, on a distinct-count rule", async () => {
269
+ const spy: Spy = stubExecuteQuery([]);
270
+
271
+ await callFindDetectionMatches({
272
+ distinctCountExpression: buildFieldExpression("principalIp"),
273
+ minMatchCount: -5,
274
+ });
275
+
276
+ const statement: Statement = capturedStatement(spy);
277
+
278
+ expect(statement.query).toContain("HAVING distinctCount >= {p5:Int32}");
279
+ expect(statement.query_params["p5"]).toBe(1);
280
+ // The clamp must never leak a value <= 0 into any bound param.
281
+ expect(Object.values(statement.query_params)).not.toContain(-5);
282
+ });
283
+ });
284
+
285
+ describe("row mapping", () => {
286
+ test("maps every column, parsing ClickHouse's stringly counts", async () => {
287
+ /*
288
+ * ClickHouse's JSON format renders UInt64 aggregates as strings —
289
+ * matchCount and distinctCount must come back as numbers anyway.
290
+ */
291
+ stubExecuteQuery([
292
+ {
293
+ groupValue: "alice",
294
+ matchCount: "7",
295
+ distinctCount: "3",
296
+ sampleMessage: "failed login for alice",
297
+ sampleObservables: ["alice", "10.0.0.9"],
298
+ },
299
+ ]);
300
+
301
+ const groups: Array<DetectionMatchGroup> = await callFindDetectionMatches(
302
+ {
303
+ distinctCountExpression: buildFieldExpression("targetUser"),
304
+ },
305
+ );
306
+
307
+ expect(groups).toStrictEqual([
308
+ {
309
+ groupValue: "alice",
310
+ matchCount: 7,
311
+ distinctCount: 3,
312
+ sampleMessage: "failed login for alice",
313
+ sampleObservables: ["alice", "10.0.0.9"],
314
+ },
315
+ ]);
316
+ });
317
+
318
+ test("distinctCount defaults to 0 when the column is missing or null", async () => {
319
+ stubExecuteQuery([
320
+ {
321
+ // A row from before the column existed — no distinctCount key.
322
+ groupValue: "bob",
323
+ matchCount: 2,
324
+ sampleMessage: "m",
325
+ sampleObservables: [],
326
+ },
327
+ {
328
+ groupValue: "eve",
329
+ matchCount: 4,
330
+ distinctCount: null,
331
+ sampleMessage: "n",
332
+ sampleObservables: [],
333
+ },
334
+ ]);
335
+
336
+ const groups: Array<DetectionMatchGroup> =
337
+ await callFindDetectionMatches();
338
+
339
+ expect(groups[0]!.distinctCount).toBe(0);
340
+ expect(groups[1]!.distinctCount).toBe(0);
341
+ // ...without disturbing the fields that were there.
342
+ expect(groups[0]!.matchCount).toBe(2);
343
+ expect(groups[1]!.matchCount).toBe(4);
344
+ });
345
+
346
+ test("degenerate rows fall back field-by-field instead of throwing", async () => {
347
+ stubExecuteQuery([
348
+ {
349
+ sampleObservables: "not-an-array",
350
+ },
351
+ ]);
352
+
353
+ const groups: Array<DetectionMatchGroup> =
354
+ await callFindDetectionMatches();
355
+
356
+ expect(groups).toStrictEqual([
357
+ {
358
+ groupValue: "",
359
+ matchCount: 0,
360
+ distinctCount: 0,
361
+ sampleMessage: "",
362
+ sampleObservables: [],
363
+ },
364
+ ]);
365
+ });
366
+
367
+ test("a response with no data key maps to an empty group list", async () => {
368
+ stubExecuteQuery(undefined);
369
+
370
+ const groups: Array<DetectionMatchGroup> =
371
+ await callFindDetectionMatches();
372
+
373
+ expect(groups).toStrictEqual([]);
374
+ });
375
+ });
376
+ });