@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
@@ -5,7 +5,12 @@ import Select from "../../Types/AnalyticsDatabase/Select";
5
5
  import Sort from "../../Types/AnalyticsDatabase/Sort";
6
6
  import UpdateBy from "../../Types/AnalyticsDatabase/UpdateBy";
7
7
  import logger from "../Logger";
8
- import { QualifiedColumn, SQL, Statement } from "./Statement";
8
+ import {
9
+ QualifiedColumn,
10
+ SQL,
11
+ Statement,
12
+ escapeIlikePattern,
13
+ } from "./Statement";
9
14
  import {
10
15
  adaptTableSettingsForStorage,
11
16
  getDistributedEngine,
@@ -25,10 +30,12 @@ import AnalyticsTableColumn, {
25
30
  } from "../../../Types/AnalyticsDatabase/TableColumn";
26
31
  import TableColumnType from "../../../Types/AnalyticsDatabase/TableColumnType";
27
32
  import EqualTo from "../../../Types/BaseDatabase/EqualTo";
33
+ import EqualToOrNull from "../../../Types/BaseDatabase/EqualToOrNull";
28
34
  import GreaterThan from "../../../Types/BaseDatabase/GreaterThan";
29
35
  import GreaterThanOrEqual from "../../../Types/BaseDatabase/GreaterThanOrEqual";
30
36
  import InBetween from "../../../Types/BaseDatabase/InBetween";
31
37
  import Includes from "../../../Types/BaseDatabase/Includes";
38
+ import IncludesAll from "../../../Types/BaseDatabase/IncludesAll";
32
39
  import IncludesNone from "../../../Types/BaseDatabase/IncludesNone";
33
40
  import ObjectID from "../../../Types/ObjectID";
34
41
  import IsNull from "../../../Types/BaseDatabase/IsNull";
@@ -39,6 +46,8 @@ import LessThanOrNull from "../../../Types/BaseDatabase/LessThanOrNull";
39
46
  import NotEqual from "../../../Types/BaseDatabase/NotEqual";
40
47
  import NotContains from "../../../Types/BaseDatabase/NotContains";
41
48
  import NotNull from "../../../Types/BaseDatabase/NotNull";
49
+ import QueryOperator from "../../../Types/BaseDatabase/QueryOperator";
50
+ import GenericObject from "../../../Types/GenericObject";
42
51
  import Search from "../../../Types/BaseDatabase/Search";
43
52
  import MultiSearch from "../../../Types/BaseDatabase/MultiSearch";
44
53
  import StartsWith from "../../../Types/BaseDatabase/StartsWith";
@@ -108,6 +117,28 @@ export default class StatementGenerator<TBaseModel extends AnalyticsBaseModel> {
108
117
  this.database = data.database;
109
118
  }
110
119
 
120
+ /*
121
+ * Map(String,String) and JSON columns have per-sub-key handling further
122
+ * down the operator chain; a scalar operator applied to the whole column
123
+ * would compile to SQL ClickHouse rejects with a cryptic type error.
124
+ * Fail with the same loud BadDataException the fallback uses.
125
+ */
126
+ private throwIfMapOrJsonColumn(input: {
127
+ operator: QueryOperator<GenericObject | number | string>;
128
+ tableColumn: AnalyticsTableColumn;
129
+ key: string;
130
+ }): void {
131
+ if (
132
+ input.tableColumn.type === TableColumnType.MapStringString ||
133
+ input.tableColumn.type === TableColumnType.JSON ||
134
+ input.tableColumn.type === TableColumnType.JSONArray
135
+ ) {
136
+ throw new BadDataException(
137
+ `Unsupported query operator ${input.operator.constructor.name} on column: ${input.key}`,
138
+ );
139
+ }
140
+ }
141
+
111
142
  private appendMapKeyPresenceFilter(input: {
112
143
  whereStatement: Statement;
113
144
  mapColumn: AnalyticsTableColumn;
@@ -755,13 +786,86 @@ export default class StatementGenerator<TBaseModel extends AnalyticsBaseModel> {
755
786
  whereStatement.append(SQL` `);
756
787
  }
757
788
 
758
- if (value instanceof Search) {
789
+ /*
790
+ * Several operators AND-ed onto ONE column — e.g.
791
+ * `observables: [Includes([x]), IncludesNone([y])]` for "mentions x
792
+ * AND does not mention y". The flat query map has a single slot per
793
+ * column, so without this an AND of two predicates on the same column
794
+ * is inexpressible (the Security Events correlation builder needs
795
+ * it). Each element compiles through a single-key recursive call, so
796
+ * every per-operator branch below applies unchanged; elements whose
797
+ * branch drops the predicate (e.g. an empty Includes) contribute
798
+ * nothing. A bare value array (exact-array equality on ArrayText)
799
+ * has non-operator elements and falls through untouched.
800
+ */
801
+ if (
802
+ Array.isArray(value) &&
803
+ value.length > 0 &&
804
+ value.every((element: unknown) => {
805
+ return element instanceof QueryOperator;
806
+ })
807
+ ) {
808
+ let isFirstFragment: boolean = true;
809
+ for (const operator of value) {
810
+ const fragment: Statement = this.toWhereStatement(
811
+ { [key]: operator } as Query<TBaseModel>,
812
+ options,
813
+ );
814
+ if (!fragment.query) {
815
+ continue;
816
+ }
817
+ if (isFirstFragment) {
818
+ isFirstFragment = false;
819
+ } else {
820
+ whereStatement.append(SQL` `);
821
+ }
822
+ whereStatement.append(fragment);
823
+ }
824
+ continue;
825
+ }
826
+
827
+ if (
828
+ value instanceof Search &&
829
+ tableColumn.type === TableColumnType.ArrayText
830
+ ) {
831
+ /*
832
+ * Substring search over an Array(String) column ("any element
833
+ * contains v"): `arrayExists(x -> x ILIKE '%v%', col)`. The scalar
834
+ * `col ILIKE ...` form below would declare the bound parameter as
835
+ * Array(String) while carrying a single pattern string, which
836
+ * ClickHouse rejects at parameter-parse time — so array columns get
837
+ * their own per-element form. This is what makes a plain text filter
838
+ * on `observables` (Security Events) work at all.
839
+ */
840
+ whereStatement.append(
841
+ SQL`AND arrayExists(x -> x ILIKE ${{
842
+ value: value,
843
+ type: TableColumnType.Text,
844
+ }}, ${columnRef(key)})`,
845
+ );
846
+ } else if (value instanceof Search) {
759
847
  whereStatement.append(
760
848
  SQL`AND ${columnRef(key)} ILIKE ${{
761
849
  value: value,
762
850
  type: tableColumn.type,
763
851
  }}`,
764
852
  );
853
+ } else if (
854
+ value instanceof NotEqual &&
855
+ tableColumn.type === TableColumnType.ArrayText
856
+ ) {
857
+ /*
858
+ * "not equal to v" on an Array(String) column means "does not
859
+ * mention v" — the scalar `col != v` form would bind a single
860
+ * string against an Array(String) parameter and fail at
861
+ * ClickHouse parameter-parse time.
862
+ */
863
+ whereStatement.append(
864
+ SQL`AND NOT has(${columnRef(key)}, ${{
865
+ value: String((value as NotEqual<string>).value ?? ""),
866
+ type: TableColumnType.Text,
867
+ }})`,
868
+ );
765
869
  } else if (value instanceof NotEqual) {
766
870
  whereStatement.append(
767
871
  SQL`AND ${columnRef(key)} != ${{
@@ -887,13 +991,183 @@ export default class StatementGenerator<TBaseModel extends AnalyticsBaseModel> {
887
991
  );
888
992
  }
889
993
  } else if (value instanceof IsNull) {
890
- if (tableColumn.type === TableColumnType.Text) {
994
+ if (tableColumn.type === TableColumnType.ArrayText) {
995
+ /*
996
+ * Array(String) columns are non-Nullable — `IS NULL` would be
997
+ * constant-false. "Not set" for an array is the empty array.
998
+ */
999
+ whereStatement.append(SQL`AND empty(${columnRef(key)})`);
1000
+ } else if (tableColumn.type === TableColumnType.Text) {
891
1001
  whereStatement.append(
892
1002
  SQL`AND (${columnRef(key)} IS NULL OR ${columnRef(key)} = '')`,
893
1003
  );
894
1004
  } else {
895
1005
  whereStatement.append(SQL`AND ${columnRef(key)} IS NULL`);
896
1006
  }
1007
+ } else if (value instanceof NotNull) {
1008
+ /*
1009
+ * Mirror of IsNull above: Text columns store '' as their "not set"
1010
+ * default (they are non-nullable Strings unless optional), so a
1011
+ * present-value check must reject the empty string too; Array
1012
+ * columns are non-Nullable, so `IS NOT NULL` would be constant-true
1013
+ * — presence for an array means "has at least one element".
1014
+ */
1015
+ if (tableColumn.type === TableColumnType.ArrayText) {
1016
+ whereStatement.append(SQL`AND notEmpty(${columnRef(key)})`);
1017
+ } else if (tableColumn.type === TableColumnType.Text) {
1018
+ whereStatement.append(
1019
+ SQL`AND (${columnRef(key)} IS NOT NULL AND ${columnRef(key)} != '')`,
1020
+ );
1021
+ } else {
1022
+ whereStatement.append(SQL`AND ${columnRef(key)} IS NOT NULL`);
1023
+ }
1024
+ } else if (value instanceof EqualTo) {
1025
+ /*
1026
+ * Explicit equality wrapper. Before this branch existed an EqualTo
1027
+ * instance fell through to the bare-value fallback, which bound the
1028
+ * operator OBJECT as the parameter — a silent match-nothing filter.
1029
+ * Scalars bind the wrapped value exactly like a bare value; on an
1030
+ * Array(String) column "equals v" means membership (`has`), which
1031
+ * is what a table filter's Equal To on `observables` intends. Map
1032
+ * and JSON columns have no scalar equality — fail loudly instead
1033
+ * of emitting SQL ClickHouse will reject with a cryptic error.
1034
+ */
1035
+ this.throwIfMapOrJsonColumn({ operator: value, tableColumn, key });
1036
+ if (tableColumn.type === TableColumnType.ArrayText) {
1037
+ whereStatement.append(
1038
+ SQL`AND has(${columnRef(key)}, ${{
1039
+ value: String((value as EqualTo<string>).value ?? ""),
1040
+ type: TableColumnType.Text,
1041
+ }})`,
1042
+ );
1043
+ } else {
1044
+ whereStatement.append(
1045
+ SQL`AND ${columnRef(key)} = ${{
1046
+ value: (value as EqualTo<string>).value,
1047
+ type: tableColumn.type,
1048
+ }}`,
1049
+ );
1050
+ }
1051
+ } else if (value instanceof EqualToOrNull) {
1052
+ this.throwIfMapOrJsonColumn({ operator: value, tableColumn, key });
1053
+ if (tableColumn.type === TableColumnType.ArrayText) {
1054
+ throw new BadDataException(
1055
+ `Unsupported query operator EqualToOrNull on column: ${key}`,
1056
+ );
1057
+ }
1058
+ if (tableColumn.type === TableColumnType.Text) {
1059
+ whereStatement.append(
1060
+ SQL`AND (${columnRef(key)} = ${{
1061
+ value: (value as EqualToOrNull<string>).value,
1062
+ type: tableColumn.type,
1063
+ }} OR ${columnRef(key)} IS NULL OR ${columnRef(key)} = '')`,
1064
+ );
1065
+ } else {
1066
+ whereStatement.append(
1067
+ SQL`AND (${columnRef(key)} = ${{
1068
+ value: (value as EqualToOrNull<string>).value,
1069
+ type: tableColumn.type,
1070
+ }} OR ${columnRef(key)} IS NULL)`,
1071
+ );
1072
+ }
1073
+ } else if (value instanceof StartsWith) {
1074
+ /*
1075
+ * Prefix / suffix / negated-substring matching, previously only
1076
+ * implemented for map (attributes) sub-keys. Array(String) columns
1077
+ * get the per-element arrayExists form (see the Search branch
1078
+ * above); scalars get a plain ILIKE. Patterns bind as Text because
1079
+ * the pattern itself is a string whatever the column type is, and
1080
+ * the user value is escaped so it matches literally rather than as
1081
+ * wildcard syntax.
1082
+ */
1083
+ this.throwIfMapOrJsonColumn({ operator: value, tableColumn, key });
1084
+ const startsWithPattern: string = `${escapeIlikePattern(
1085
+ (value.value as string) || "",
1086
+ )}%`;
1087
+ if (tableColumn.type === TableColumnType.ArrayText) {
1088
+ whereStatement.append(
1089
+ SQL`AND arrayExists(x -> x ILIKE ${{
1090
+ value: startsWithPattern,
1091
+ type: TableColumnType.Text,
1092
+ }}, ${columnRef(key)})`,
1093
+ );
1094
+ } else {
1095
+ whereStatement.append(
1096
+ SQL`AND ${columnRef(key)} ILIKE ${{
1097
+ value: startsWithPattern,
1098
+ type: TableColumnType.Text,
1099
+ }}`,
1100
+ );
1101
+ }
1102
+ } else if (value instanceof EndsWith) {
1103
+ this.throwIfMapOrJsonColumn({ operator: value, tableColumn, key });
1104
+ const endsWithPattern: string = `%${escapeIlikePattern(
1105
+ (value.value as string) || "",
1106
+ )}`;
1107
+ if (tableColumn.type === TableColumnType.ArrayText) {
1108
+ whereStatement.append(
1109
+ SQL`AND arrayExists(x -> x ILIKE ${{
1110
+ value: endsWithPattern,
1111
+ type: TableColumnType.Text,
1112
+ }}, ${columnRef(key)})`,
1113
+ );
1114
+ } else {
1115
+ whereStatement.append(
1116
+ SQL`AND ${columnRef(key)} ILIKE ${{
1117
+ value: endsWithPattern,
1118
+ type: TableColumnType.Text,
1119
+ }}`,
1120
+ );
1121
+ }
1122
+ } else if (value instanceof NotContains) {
1123
+ /*
1124
+ * On a nullable scalar, `NOT (NULL ILIKE ...)` is NULL and would
1125
+ * filter the row out — but a row with no value at all trivially
1126
+ * "does not contain" the needle, so NULL rows are let through
1127
+ * explicitly (mirrors how missing map keys pass the map-branch
1128
+ * NotContains).
1129
+ */
1130
+ this.throwIfMapOrJsonColumn({ operator: value, tableColumn, key });
1131
+ const notContainsPattern: string = `%${escapeIlikePattern(
1132
+ (value.value as string) || "",
1133
+ )}%`;
1134
+ if (tableColumn.type === TableColumnType.ArrayText) {
1135
+ whereStatement.append(
1136
+ SQL`AND NOT arrayExists(x -> x ILIKE ${{
1137
+ value: notContainsPattern,
1138
+ type: TableColumnType.Text,
1139
+ }}, ${columnRef(key)})`,
1140
+ );
1141
+ } else {
1142
+ whereStatement.append(
1143
+ SQL`AND (NOT (${columnRef(key)} ILIKE ${{
1144
+ value: notContainsPattern,
1145
+ type: TableColumnType.Text,
1146
+ }}) OR ${columnRef(key)} IS NULL)`,
1147
+ );
1148
+ }
1149
+ } else if (
1150
+ value instanceof IncludesAll &&
1151
+ tableColumn.type === TableColumnType.ArrayText
1152
+ ) {
1153
+ /*
1154
+ * Array(String) conjunction ("mentions ALL of these"): `hasAll`,
1155
+ * the AND-counterpart of the Includes/hasAny branch above. This is
1156
+ * what a correlation like "events naming host X AND ip Y" compiles
1157
+ * to on the `observables` column. An empty IncludesAll constrains
1158
+ * nothing — drop the predicate (hasAll(col, []) is vacuously true
1159
+ * anyway, but skipping keeps parity with the empty-Includes path).
1160
+ */
1161
+ const arrayAllValues: Array<string> =
1162
+ ((value as IncludesAll).values as Array<string>) || [];
1163
+ if (arrayAllValues.length > 0) {
1164
+ whereStatement.append(
1165
+ SQL`AND hasAll(${columnRef(key)}, ${{
1166
+ value: arrayAllValues,
1167
+ type: TableColumnType.ArrayText,
1168
+ }})`,
1169
+ );
1170
+ }
897
1171
  } else if (
898
1172
  tableColumn.type === TableColumnType.MapStringString &&
899
1173
  typeof value === "object"
@@ -985,7 +1259,9 @@ export default class StatementGenerator<TBaseModel extends AnalyticsBaseModel> {
985
1259
  }
986
1260
 
987
1261
  if (mapEntry instanceof NotContains) {
988
- const literalValue: string = `%${(mapEntry.value as string) || ""}%`;
1262
+ const literalValue: string = `%${escapeIlikePattern(
1263
+ (mapEntry.value as string) || "",
1264
+ )}%`;
989
1265
  whereStatement.append(
990
1266
  SQL`AND NOT arrayExists((k, v) -> lowerUTF8(k) = lowerUTF8(${{
991
1267
  value: mapKey,
@@ -999,7 +1275,9 @@ export default class StatementGenerator<TBaseModel extends AnalyticsBaseModel> {
999
1275
  }
1000
1276
 
1001
1277
  if (mapEntry instanceof StartsWith) {
1002
- const literalValue: string = `${(mapEntry.value as string) || ""}%`;
1278
+ const literalValue: string = `${escapeIlikePattern(
1279
+ (mapEntry.value as string) || "",
1280
+ )}%`;
1003
1281
  whereStatement.append(
1004
1282
  SQL`AND arrayExists((k, v) -> lowerUTF8(k) = lowerUTF8(${{
1005
1283
  value: mapKey,
@@ -1013,7 +1291,9 @@ export default class StatementGenerator<TBaseModel extends AnalyticsBaseModel> {
1013
1291
  }
1014
1292
 
1015
1293
  if (mapEntry instanceof EndsWith) {
1016
- const literalValue: string = `%${(mapEntry.value as string) || ""}`;
1294
+ const literalValue: string = `%${escapeIlikePattern(
1295
+ (mapEntry.value as string) || "",
1296
+ )}`;
1017
1297
  whereStatement.append(
1018
1298
  SQL`AND arrayExists((k, v) -> lowerUTF8(k) = lowerUTF8(${{
1019
1299
  value: mapKey,
@@ -1290,6 +1570,17 @@ export default class StatementGenerator<TBaseModel extends AnalyticsBaseModel> {
1290
1570
  }
1291
1571
  }
1292
1572
  } else {
1573
+ /*
1574
+ * Bare-value equality. A query operator that reaches this point has
1575
+ * no branch for this column type (e.g. IncludesAll on a scalar) —
1576
+ * binding the operator object itself would produce a silent
1577
+ * match-nothing filter, so fail loudly instead.
1578
+ */
1579
+ if (value instanceof QueryOperator) {
1580
+ throw new BadDataException(
1581
+ `Unsupported query operator ${value.constructor.name} on column: ${key}`,
1582
+ );
1583
+ }
1293
1584
  whereStatement.append(
1294
1585
  SQL`AND ${columnRef(key)} = ${{ value, type: tableColumn.type }}`,
1295
1586
  );
@@ -46,6 +46,10 @@ export interface UtmAttribution {
46
46
  utmCampaign?: string | undefined;
47
47
  utmTerm?: string | undefined;
48
48
  utmContent?: string | undefined;
49
+ utmId?: string | undefined;
50
+ utmSourcePlatform?: string | undefined;
51
+ utmCreativeFormat?: string | undefined;
52
+ utmMarketingTactic?: string | undefined;
49
53
  utmUrl?: string | undefined;
50
54
  }
51
55
 
@@ -1,5 +1,10 @@
1
1
  import Attribution from "../Attribution";
2
2
  import MarketingEventWebhook from "./MarketingEventWebhook";
3
+ import {
4
+ UtmPropertyKeys,
5
+ UtmUrlPropertyKey,
6
+ UtmWireKeyToPropertyKey,
7
+ } from "../../../Types/Marketing/Attribution";
3
8
  import Queue, { QueueName } from "../../Infrastructure/Queue";
4
9
  import { JSONObject } from "../../../Types/JSON";
5
10
  import {
@@ -34,25 +39,58 @@ export interface AttributionSource {
34
39
  utmCampaign?: string | undefined;
35
40
  utmTerm?: string | undefined;
36
41
  utmContent?: string | undefined;
42
+ utmId?: string | undefined;
43
+ utmSourcePlatform?: string | undefined;
44
+ utmCreativeFormat?: string | undefined;
45
+ utmMarketingTactic?: string | undefined;
37
46
  utmUrl?: string | undefined;
38
47
  clickIds?: JSONObject | undefined;
39
48
  firstTouchAttribution?: JSONObject | undefined;
40
49
  }
41
50
 
51
+ /*
52
+ * The campaign columns as snake_case analytics properties, driven from the
53
+ * shared contract so a key added there cannot be reported by the browser and
54
+ * the receiver while silently missing from PostHog.
55
+ */
56
+ export function utmAnalyticsProperties(row: {
57
+ [key: string]: unknown;
58
+ }): JSONObject {
59
+ const properties: JSONObject = {};
60
+
61
+ for (const [wireKey, propertyKey] of Object.entries(
62
+ UtmWireKeyToPropertyKey,
63
+ )) {
64
+ properties[wireKey] = (row[propertyKey] as string | undefined) || "";
65
+ }
66
+
67
+ return properties;
68
+ }
69
+
42
70
  export default class MarketingEventUtil {
71
+ /*
72
+ * Driven from UtmPropertyKeys rather than hand-listed. Hand-listing is how
73
+ * utm_id and its three GA4 siblings reached the browser, the User row and
74
+ * the Cal booking metadata while silently never reaching a receiver.
75
+ *
76
+ * Additive only, so the schema version does not move
77
+ * (Docs/analytics/marketing-event-webhooks.md).
78
+ */
43
79
  public static buildAttribution(
44
80
  source: AttributionSource | undefined,
45
81
  ): MarketingEventAttribution {
46
- return {
47
- utmSource: source?.utmSource,
48
- utmMedium: source?.utmMedium,
49
- utmCampaign: source?.utmCampaign,
50
- utmTerm: source?.utmTerm,
51
- utmContent: source?.utmContent,
52
- utmUrl: source?.utmUrl,
82
+ const sourceRow: JSONObject = (source || {}) as unknown as JSONObject;
83
+
84
+ const attribution: MarketingEventAttribution = {
53
85
  clickIds: source?.clickIds || {},
54
86
  firstTouch: source?.firstTouchAttribution || {},
55
87
  };
88
+
89
+ for (const propertyKey of [...UtmPropertyKeys, UtmUrlPropertyKey]) {
90
+ attribution[propertyKey] = sourceRow[propertyKey];
91
+ }
92
+
93
+ return attribution;
56
94
  }
57
95
 
58
96
  public static buildEvent(data: {
@@ -16,6 +16,7 @@ import OcsfSeverity, {
16
16
  import { ocsfCategoryForClassUid } from "../../../Types/SecurityEvent/OcsfEventClass";
17
17
  import SigmaRule, { SigmaLevel } from "../../../Types/SecurityEvent/SigmaRule";
18
18
  import {
19
+ DETECTION_DISTINCT_COUNT_ATTRIBUTE,
19
20
  DETECTION_FINDING_CLASS_NAME,
20
21
  DETECTION_FINDING_CLASS_UID,
21
22
  DETECTION_GROUP_VALUE_ATTRIBUTE,
@@ -36,12 +37,14 @@ import OTelIngestService, {
36
37
  } from "../../Services/OpenTelemetryIngestService";
37
38
  import SecurityEventService, {
38
39
  DetectionMatchGroup,
40
+ doesGroupMeetDetectionThreshold,
39
41
  } from "../../Services/SecurityEventService";
40
42
  import { resolveTelemetryRetentionInDays } from "../../../Types/Telemetry/TelemetryRetentionConfig";
41
43
  import logger from "../Logger";
42
44
  import CaptureSpan from "../Telemetry/CaptureSpan";
43
45
  import { Statement } from "../AnalyticsDatabase/Statement";
44
46
  import SigmaClickhouseCompiler, {
47
+ buildSigmaDistinctCountExpression,
45
48
  buildSigmaFieldExpression,
46
49
  } from "./Sigma/SigmaClickhouseCompiler";
47
50
  import { buildSecurityEventDbRow } from "./SecurityEventRow";
@@ -96,6 +99,8 @@ export default class DetectionRuleEvaluator {
96
99
  sigmaRuleYaml: true,
97
100
  evaluationIntervalInMinutes: true,
98
101
  groupByField: true,
102
+ distinctCountField: true,
103
+ matchCountThreshold: true,
99
104
  shouldCreateAlert: true,
100
105
  shouldWriteDetectionFinding: true,
101
106
  shouldCreateIncident: true,
@@ -188,6 +193,12 @@ export default class DetectionRuleEvaluator {
188
193
  ? buildSigmaFieldExpression(rule.groupByField)
189
194
  : null;
190
195
 
196
+ const distinctCountExpression: Statement | null = rule.distinctCountField
197
+ ? buildSigmaDistinctCountExpression(rule.distinctCountField)
198
+ : null;
199
+
200
+ const matchCountThreshold: number = this.effectiveThreshold(rule);
201
+
191
202
  const groups: Array<DetectionMatchGroup> =
192
203
  await SecurityEventService.findDetectionMatches({
193
204
  projectId: rule.projectId,
@@ -195,12 +206,24 @@ export default class DetectionRuleEvaluator {
195
206
  endTime,
196
207
  whereFragment,
197
208
  groupByExpression,
209
+ distinctCountExpression,
210
+ minMatchCount: matchCountThreshold,
198
211
  maxGroups: MAX_GROUPS_PER_EVALUATION,
199
212
  });
200
213
 
214
+ /*
215
+ * The query's HAVING clause already enforces the threshold — this
216
+ * re-applies it (via the predicate exported next to that HAVING
217
+ * builder) so the firing contract does not depend on which side
218
+ * built the rows.
219
+ */
201
220
  const matchedGroups: Array<DetectionMatchGroup> = groups.filter(
202
221
  (row: DetectionMatchGroup): boolean => {
203
- return row.matchCount > 0;
222
+ return doesGroupMeetDetectionThreshold({
223
+ group: row,
224
+ usesDistinctCount: Boolean(rule.distinctCountField),
225
+ minMatchCount: matchCountThreshold,
226
+ });
204
227
  },
205
228
  );
206
229
 
@@ -546,6 +569,16 @@ export default class DetectionRuleEvaluator {
546
569
  )} and ${OneUptimeDate.getDateAsFormattedString(data.endTime)}.`,
547
570
  );
548
571
 
572
+ if (rule.distinctCountField) {
573
+ descriptionParts.push(
574
+ `${matchGroup.distinctCount} distinct ${rule.distinctCountField} value${
575
+ matchGroup.distinctCount === 1 ? "" : "s"
576
+ } across these events (rule threshold: ${this.effectiveThreshold(
577
+ rule,
578
+ )}).`,
579
+ );
580
+ }
581
+
549
582
  if (matchGroup.sampleMessage) {
550
583
  descriptionParts.push(`Sample event: ${matchGroup.sampleMessage}`);
551
584
  }
@@ -590,6 +623,10 @@ export default class DetectionRuleEvaluator {
590
623
 
591
624
  const rows: Array<JSONObject> = matchedGroups.map(
592
625
  (matchGroup: DetectionMatchGroup): JSONObject => {
626
+ const distinctSuffix: string = rule.distinctCountField
627
+ ? `, ${matchGroup.distinctCount} distinct ${rule.distinctCountField}`
628
+ : "";
629
+
593
630
  const normalized: NormalizedSecurityEvent = {
594
631
  time: OneUptimeDate.getCurrentDate(),
595
632
  eventUid: `detection:${rule.id!.toString()}:${this.buildFingerprint(
@@ -605,8 +642,8 @@ export default class DetectionRuleEvaluator {
605
642
  severityName,
606
643
  statusName: "New",
607
644
  message: matchGroup.groupValue
608
- ? `Detection: ${rule.name} — ${matchGroup.groupValue} (${matchGroup.matchCount} events)`
609
- : `Detection: ${rule.name} (${matchGroup.matchCount} events)`,
645
+ ? `Detection: ${rule.name} — ${matchGroup.groupValue} (${matchGroup.matchCount} events${distinctSuffix})`
646
+ : `Detection: ${rule.name} (${matchGroup.matchCount} events${distinctSuffix})`,
610
647
  vendorName: "OneUptime",
611
648
  productName: "OneUptime Detections",
612
649
  ruleId: rule.id!.toString(),
@@ -636,6 +673,13 @@ export default class DetectionRuleEvaluator {
636
673
  [DETECTION_RULE_ID_ATTRIBUTE]: rule.id!.toString(),
637
674
  [DETECTION_RULE_NAME_ATTRIBUTE]: rule.name || parsedRule.title,
638
675
  [DETECTION_MATCH_COUNT_ATTRIBUTE]: String(matchGroup.matchCount),
676
+ ...(rule.distinctCountField
677
+ ? {
678
+ [DETECTION_DISTINCT_COUNT_ATTRIBUTE]: String(
679
+ matchGroup.distinctCount,
680
+ ),
681
+ }
682
+ : {}),
639
683
  ...(matchGroup.groupValue
640
684
  ? { [DETECTION_GROUP_VALUE_ATTRIBUTE]: matchGroup.groupValue }
641
685
  : {}),
@@ -659,6 +703,14 @@ export default class DetectionRuleEvaluator {
659
703
  return rows.length;
660
704
  }
661
705
 
706
+ /*
707
+ * Unset and 0 both read as 1 so every rule saved before the threshold
708
+ * column existed keeps its fire-on-any-match behavior.
709
+ */
710
+ private static effectiveThreshold(rule: DetectionRule): number {
711
+ return Math.max(1, rule.matchCountThreshold || 1);
712
+ }
713
+
662
714
  private static buildFingerprint(
663
715
  ruleId: ObjectID,
664
716
  groupValue: string,
@@ -242,6 +242,27 @@ export function buildSigmaFieldExpression(field: string): Statement {
242
242
  return new Statement([resolved.column], []);
243
243
  }
244
244
 
245
+ /*
246
+ * buildSigmaFieldExpression with the column's "unset" sentinel normalized
247
+ * away, for distinct counting. Text and attribute lookups yield '' when
248
+ * absent, which the caller's nullIf(expr, '') already excludes — but the
249
+ * number columns are non-Nullable with 0 as their documented
250
+ * not-applicable value, and toString(0) is '0', not ''. Mapping 0 to NULL
251
+ * here, where the column type is known, keeps a portless event from
252
+ * counting as distinct port "0" in a port-scan rule. severityId 0 (OCSF
253
+ * Unknown) is deliberately treated as missing too, consistent with the
254
+ * empty-string rule for text.
255
+ */
256
+ export function buildSigmaDistinctCountExpression(field: string): Statement {
257
+ const resolved: ResolvedField = resolveSigmaField(field);
258
+
259
+ if (resolved.kind === "numberColumn") {
260
+ return new Statement([`toString(nullIf(${resolved.column}, 0))`], []);
261
+ }
262
+
263
+ return buildSigmaFieldExpression(field);
264
+ }
265
+
245
266
  export default class SigmaClickhouseCompiler {
246
267
  /*
247
268
  * Compile a rule (or raw YAML) into a boolean SQL expression. The