@oneuptime/common 12.0.22 → 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 (345) hide show
  1. package/Models/DatabaseModels/DetectionRule.ts +55 -0
  2. package/Models/DatabaseModels/Index.ts +2 -0
  3. package/Models/DatabaseModels/NetworkDevice.ts +44 -0
  4. package/Models/DatabaseModels/NetworkDeviceAutoImportRule.ts +584 -0
  5. package/Models/DatabaseModels/NetworkDeviceDiscoveryScan.ts +105 -0
  6. package/Models/DatabaseModels/Project.ts +68 -0
  7. package/Models/DatabaseModels/User.ts +68 -0
  8. package/Server/API/TelemetryAPI.ts +29 -0
  9. package/Server/Infrastructure/Postgres/SchemaMigrations/1789100000000-AddNetworkDeviceAutoImportRule.ts +60 -0
  10. package/Server/Infrastructure/Postgres/SchemaMigrations/1789200000000-AddSysObjectIdPatternToAutoImportRule.ts +26 -0
  11. package/Server/Infrastructure/Postgres/SchemaMigrations/1789300000000-AddAutoApplyVendorHealthTemplate.ts +26 -0
  12. package/Server/Infrastructure/Postgres/SchemaMigrations/1789400000000-AddNameToNetworkDeviceDiscoveryScan.ts +28 -0
  13. package/Server/Infrastructure/Postgres/SchemaMigrations/1789500000000-AddCampaignUtmFields.ts +49 -0
  14. package/Server/Infrastructure/Postgres/SchemaMigrations/1789512000000-AddDetectionRuleDistinctCountColumns.ts +33 -0
  15. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +12 -0
  16. package/Server/Middleware/GzipRequestBody.ts +311 -0
  17. package/Server/Middleware/MultipartFormData.ts +87 -3
  18. package/Server/Services/DetectionRuleService.ts +73 -6
  19. package/Server/Services/Index.ts +207 -0
  20. package/Server/Services/NetworkDeviceAutoImportRuleEngineService.ts +917 -0
  21. package/Server/Services/NetworkDeviceAutoImportRuleService.ts +181 -0
  22. package/Server/Services/NetworkDeviceDiscoveryScanService.ts +59 -0
  23. package/Server/Services/NetworkDeviceService.ts +148 -0
  24. package/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.ts +65 -0
  25. package/Server/Services/OnCallDutyPolicyFeedService.ts +1 -1
  26. package/Server/Services/ProjectService.ts +29 -19
  27. package/Server/Services/SecurityEventService.ts +90 -13
  28. package/Server/Services/StatusPageResourceService.ts +223 -0
  29. package/Server/Services/TelemetryAttributeService.ts +17 -0
  30. package/Server/Services/UserService.ts +4 -6
  31. package/Server/Types/Markdown.ts +161 -67
  32. package/Server/Utils/AI/Toolbox/OnCallTools.ts +12 -4
  33. package/Server/Utils/AnalyticsDatabase/Statement.ts +13 -1
  34. package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +297 -6
  35. package/Server/Utils/Attribution.ts +4 -0
  36. package/Server/Utils/Marketing/MarketingEventUtil.ts +45 -7
  37. package/Server/Utils/Monitor/NetworkInventoryUtil.ts +39 -1
  38. package/Server/Utils/SecurityEvent/DetectionRuleEvaluator.ts +55 -3
  39. package/Server/Utils/SecurityEvent/Sigma/SigmaClickhouseCompiler.ts +21 -0
  40. package/Server/Utils/StartServer.ts +12 -32
  41. package/Server/Utils/Telemetry/LlmCostBudgetEvaluator.ts +101 -4
  42. package/Server/Utils/Telemetry/LlmMetricSpend.ts +130 -0
  43. package/Server/Views/Partials/AnalyticsConsent.ejs +159 -102
  44. package/Server/Views/Partials/Head.ejs +2 -1
  45. package/Tests/App/Dashboard/AdminUserNotificationMethodsPage.test.tsx +776 -38
  46. package/Tests/App/Dashboard/CorrelateFilterBuilder.test.tsx +359 -0
  47. package/Tests/App/Dashboard/CorrelateGraph.test.tsx +865 -0
  48. package/Tests/App/Dashboard/CorrelationGraph.test.ts +346 -0
  49. package/Tests/App/Dashboard/CriteriaFilterConditionDropdown.test.tsx +256 -0
  50. package/Tests/App/Dashboard/CriteriaFilterDefaults.test.ts +738 -0
  51. package/Tests/App/Dashboard/CriteriaFilterMonitorTypeChange.test.ts +1122 -0
  52. package/Tests/App/Dashboard/DiscoveryScanWizardValidation.test.tsx +283 -0
  53. package/Tests/App/Dashboard/ExternalStatusPageCriteriaFilter.test.ts +407 -0
  54. package/Tests/App/Dashboard/MetricSeriesInvestigateMenu.test.tsx +5 -14
  55. package/Tests/App/Dashboard/MonitorCriteriaActionToggles.test.tsx +426 -0
  56. package/Tests/App/Dashboard/MonitorStepsCriteriaAlignmentWiring.test.tsx +352 -0
  57. package/Tests/App/Dashboard/OnCallLayerCardOverrides.test.tsx +278 -0
  58. package/Tests/App/Dashboard/OnCallLayerShiftPreviewOverrides.test.ts +237 -0
  59. package/Tests/App/Dashboard/OnCallReadinessSurfaces.test.tsx +2 -2
  60. package/Tests/App/Dashboard/OnCallSchedulePreviewOverrides.test.tsx +460 -0
  61. package/Tests/App/Dashboard/SecurityEventAttributeUtil.test.ts +171 -0
  62. package/Tests/App/Dashboard/SecurityEventCorrelation.test.ts +857 -0
  63. package/Tests/App/Dashboard/SecurityEventDetailObservables.test.tsx +194 -0
  64. package/Tests/App/Dashboard/SecurityEventsDetectionRulesPage.test.tsx +124 -0
  65. package/Tests/App/Dashboard/SecurityEventsMonitorStepForm.test.tsx +79 -19
  66. package/Tests/App/Dashboard/SecurityEventsTableColumns.test.tsx +382 -0
  67. package/Tests/App/StatusPage/BulkAddStatusPageMonitors.test.ts +216 -0
  68. package/Tests/App/StatusPage/BulkAddStatusPageMonitorsModal.test.tsx +590 -11
  69. package/Tests/App/StatusPage/StatusPageResourceExplorer.test.tsx +757 -0
  70. package/Tests/Models/DiscoveryScanNameColumn.test.ts +293 -0
  71. package/Tests/Server/API/SecurityEventAttributesAPI.test.ts +564 -0
  72. package/Tests/Server/Infrastructure/Postgres/SchemaMigrationsOrdering.test.ts +120 -4
  73. package/Tests/Server/Middleware/GzipRequestBody.test.ts +725 -0
  74. package/Tests/Server/Middleware/MultipartFormData.test.ts +298 -0
  75. package/Tests/Server/Services/AddDetectionRuleDistinctCountColumnsMigration.test.ts +311 -0
  76. package/Tests/Server/Services/AutoImportScanCredentialSelect.test.ts +205 -0
  77. package/Tests/Server/Services/DetectionRuleService.test.ts +134 -2
  78. package/Tests/Server/Services/DiscoveryScanClaimHookFreeSafety.test.ts +7 -1
  79. package/Tests/Server/Services/FeedRetentionConsistency.test.ts +131 -0
  80. package/Tests/Server/Services/NetworkDeviceAutoImportRuleEngineService.test.ts +933 -0
  81. package/Tests/Server/Services/NetworkDeviceDiscoveryScanService.test.ts +194 -0
  82. package/Tests/Server/Services/NetworkDeviceMonitorStatusStamp.test.ts +800 -0
  83. package/Tests/Server/Services/OnCallScheduleAttachRosterRefresh.test.ts +244 -0
  84. package/Tests/Server/Services/SecurityEventService.test.ts +376 -0
  85. package/Tests/Server/Services/StatusPageResourceDuplicates.test.ts +703 -0
  86. package/Tests/Server/Services/TelemetryAttributeServiceSecurityEvent.test.ts +256 -0
  87. package/Tests/Server/Types/Workflow/Components/ComponentRegistryParity.test.ts +417 -0
  88. package/Tests/Server/Utils/AnalyticsDatabase/StatementGeneratorWhereOperators.test.ts +677 -0
  89. package/Tests/Server/Utils/Marketing/MarketingEventUtil.test.ts +43 -0
  90. package/Tests/Server/Utils/Monitor/Criteria/ProfileMonitorCriteria.test.ts +15 -11
  91. package/Tests/Server/Utils/Monitor/NetworkInventoryUtil.test.ts +97 -1
  92. package/Tests/Server/Utils/SecurityEvent/DetectionRuleEvaluator.test.ts +369 -8
  93. package/Tests/Server/Utils/SecurityEvent/SigmaClickhouseCompiler.test.ts +35 -0
  94. package/Tests/Server/Utils/Telemetry/LlmCostBudgetEvaluator.test.ts +368 -4
  95. package/Tests/Server/Utils/Telemetry/LlmMetricSpend.test.ts +245 -0
  96. package/Tests/Server/Views/AnalyticsConsentPartial.test.ts +48 -5
  97. package/Tests/Types/Code/YamlSyntax.test.ts +396 -0
  98. package/Tests/Types/Monitor/MetricAndProfileDefaultCriteria.test.ts +957 -0
  99. package/Tests/Types/Monitor/MonitorCriteriaActionValidation.test.ts +598 -0
  100. package/Tests/Types/Monitor/MonitorCriteriaInstance.test.ts +49 -0
  101. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationNotificationMode.test.ts +6 -6
  102. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationSeverityMapper.test.ts +4 -4
  103. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationUtil.test.ts +4 -3
  104. package/Tests/Types/Telemetry/LlmMetricConventions.test.ts +193 -0
  105. package/Tests/UI/Components/AiInvestigationSettingsCard.test.tsx +0 -12
  106. package/Tests/UI/Components/CardModelDetailEdit.test.tsx +1 -17
  107. package/Tests/UI/Components/CodeEditor.test.tsx +474 -10
  108. package/Tests/UI/Components/Detail/YamlField.test.tsx +105 -0
  109. package/Tests/UI/Components/EntityDropdownLabelsBulkAdd.test.tsx +319 -0
  110. package/Tests/UI/Components/Forms/FormFieldYaml.test.tsx +231 -0
  111. package/Tests/UI/Components/Forms/Utils/FormFieldSchemaTypeUtil.test.ts +1 -0
  112. package/Tests/UI/Components/Forms/ValidationYAML.test.ts +234 -0
  113. package/Tests/UI/Components/ModelTable/AttributeColumns.test.tsx +573 -0
  114. package/Tests/UI/Components/ModelTable/BaseModelTableAttributeColumns.test.tsx +824 -0
  115. package/Tests/UI/Components/ModelTable/ColumnCustomizationModalAddableColumns.test.tsx +707 -0
  116. package/Tests/UI/Components/SideOver.test.tsx +1 -1
  117. package/Tests/UI/Components/StatusPage/ResourceGridSelection.test.tsx +294 -0
  118. package/Tests/UI/Components/StatusPage/ResourceListSelection.test.tsx +436 -0
  119. package/Tests/UI/Components/TableBulkCsvExport.test.tsx +6 -2
  120. package/Tests/UI/Components/Toggle.test.tsx +25 -0
  121. package/Tests/UI/Components/YamlEditor.test.tsx +847 -0
  122. package/Tests/Utils/NetworkDevice/DeviceReachabilityUtil.test.ts +252 -0
  123. package/Tests/Utils/NetworkDiscovery/AutoImportRuleMatcher.test.ts +516 -0
  124. package/Tests/Utils/NetworkDiscovery/DiscoveredDeviceBuilder.test.ts +272 -0
  125. package/Tests/Utils/NetworkDiscovery/DiscoveredHostUtil.test.ts +221 -0
  126. package/Tests/Utils/NetworkDiscovery/ScanNameUtil.test.ts +312 -0
  127. package/Tests/Utils/NetworkDiscovery/ScanTargetUtil.test.ts +164 -0
  128. package/Tests/Utils/StatusPage/ResourceExplorer.test.ts +468 -0
  129. package/Tests/Utils/Telemetry/LlmMetricQuery.test.ts +472 -0
  130. package/Types/Code/YamlSyntax.ts +396 -0
  131. package/Types/Exception/ExceptionCode.ts +1 -0
  132. package/Types/Exception/PayloadTooLargeException.ts +8 -0
  133. package/Types/Marketing/Attribution.ts +10 -0
  134. package/Types/Marketing/MarketingEvent.ts +4 -0
  135. package/Types/Monitor/MonitorCriteriaInstance.ts +116 -56
  136. package/Types/Monitor/Recommendation/MonitorRecommendationSeverityMapper.ts +2 -1
  137. package/Types/NetworkAutomation/RuleRunResult.ts +84 -0
  138. package/Types/Permission.ts +47 -0
  139. package/Types/SecurityEvent/DetectionFindingConstants.ts +11 -0
  140. package/Types/Telemetry/LlmMetricConventions.ts +124 -0
  141. package/UI/Components/AutocompleteTextInput/AutocompleteTextInput.tsx +3 -0
  142. package/UI/Components/BulkUpdate/BulkUpdateForm.tsx +10 -1
  143. package/UI/Components/CodeEditor/CodeEditor.tsx +241 -79
  144. package/UI/Components/CodeEditor/YamlEditor.tsx +369 -0
  145. package/UI/Components/Detail/Detail.tsx +10 -0
  146. package/UI/Components/EntityDropdown/EntityDropdown.tsx +33 -0
  147. package/UI/Components/Forms/Fields/FormField.tsx +31 -0
  148. package/UI/Components/Forms/Types/Field.ts +8 -0
  149. package/UI/Components/Forms/Types/FormFieldSchemaType.ts +1 -0
  150. package/UI/Components/Forms/Utils/FormFieldSchemaTypeUtil.ts +2 -0
  151. package/UI/Components/Forms/Validation.ts +46 -0
  152. package/UI/Components/Input/Input.tsx +3 -0
  153. package/UI/Components/ModelTable/AttributeColumns.tsx +321 -0
  154. package/UI/Components/ModelTable/BaseModelTable.tsx +263 -14
  155. package/UI/Components/ModelTable/Column.ts +8 -0
  156. package/UI/Components/ModelTable/ColumnCustomizationModal.tsx +310 -0
  157. package/UI/Components/ModelTable/ColumnPreference.ts +13 -1
  158. package/UI/Components/MonitorTemplateVariables/TemplateVariablesModal.tsx +2 -2
  159. package/UI/Components/SideOver/SideOver.tsx +5 -5
  160. package/UI/Components/StatusPage/ResourceGrid.tsx +82 -2
  161. package/UI/Components/StatusPage/ResourceList.tsx +197 -38
  162. package/UI/Components/Toggle/Toggle.tsx +9 -1
  163. package/UI/Components/Types/FieldType.ts +1 -0
  164. package/Utils/NetworkDevice/DeviceReachabilityUtil.ts +73 -1
  165. package/Utils/NetworkDiscovery/AutoImportRuleMatcher.ts +257 -0
  166. package/Utils/NetworkDiscovery/DiscoveredDeviceBuilder.ts +183 -0
  167. package/Utils/NetworkDiscovery/DiscoveredHostUtil.ts +95 -0
  168. package/Utils/NetworkDiscovery/DiscoveryImportEligibility.ts +66 -0
  169. package/Utils/NetworkDiscovery/ScanNameUtil.ts +181 -0
  170. package/Utils/NetworkDiscovery/ScanTargetUtil.ts +82 -0
  171. package/Utils/StatusPage/ResourceExplorer.ts +258 -0
  172. package/Utils/Telemetry/LlmMetricQuery.ts +204 -0
  173. package/build/dist/Models/DatabaseModels/DetectionRule.js +57 -0
  174. package/build/dist/Models/DatabaseModels/DetectionRule.js.map +1 -1
  175. package/build/dist/Models/DatabaseModels/Index.js +2 -0
  176. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  177. package/build/dist/Models/DatabaseModels/NetworkDevice.js +45 -0
  178. package/build/dist/Models/DatabaseModels/NetworkDevice.js.map +1 -1
  179. package/build/dist/Models/DatabaseModels/NetworkDeviceAutoImportRule.js +609 -0
  180. package/build/dist/Models/DatabaseModels/NetworkDeviceAutoImportRule.js.map +1 -0
  181. package/build/dist/Models/DatabaseModels/NetworkDeviceDiscoveryScan.js +97 -0
  182. package/build/dist/Models/DatabaseModels/NetworkDeviceDiscoveryScan.js.map +1 -1
  183. package/build/dist/Models/DatabaseModels/Project.js +76 -0
  184. package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
  185. package/build/dist/Models/DatabaseModels/User.js +76 -0
  186. package/build/dist/Models/DatabaseModels/User.js.map +1 -1
  187. package/build/dist/Server/API/TelemetryAPI.js +17 -0
  188. package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
  189. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789100000000-AddNetworkDeviceAutoImportRule.js +35 -0
  190. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789100000000-AddNetworkDeviceAutoImportRule.js.map +1 -0
  191. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789200000000-AddSysObjectIdPatternToAutoImportRule.js +19 -0
  192. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789200000000-AddSysObjectIdPatternToAutoImportRule.js.map +1 -0
  193. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789300000000-AddAutoApplyVendorHealthTemplate.js +19 -0
  194. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789300000000-AddAutoApplyVendorHealthTemplate.js.map +1 -0
  195. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789400000000-AddNameToNetworkDeviceDiscoveryScan.js +21 -0
  196. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789400000000-AddNameToNetworkDeviceDiscoveryScan.js.map +1 -0
  197. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789500000000-AddCampaignUtmFields.js +42 -0
  198. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789500000000-AddCampaignUtmFields.js.map +1 -0
  199. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789512000000-AddDetectionRuleDistinctCountColumns.js +22 -0
  200. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789512000000-AddDetectionRuleDistinctCountColumns.js.map +1 -0
  201. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +12 -0
  202. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  203. package/build/dist/Server/Middleware/GzipRequestBody.js +224 -0
  204. package/build/dist/Server/Middleware/GzipRequestBody.js.map +1 -0
  205. package/build/dist/Server/Middleware/MultipartFormData.js +71 -2
  206. package/build/dist/Server/Middleware/MultipartFormData.js.map +1 -1
  207. package/build/dist/Server/Services/DetectionRuleService.js +49 -4
  208. package/build/dist/Server/Services/DetectionRuleService.js.map +1 -1
  209. package/build/dist/Server/Services/Index.js +206 -0
  210. package/build/dist/Server/Services/Index.js.map +1 -1
  211. package/build/dist/Server/Services/NetworkDeviceAutoImportRuleEngineService.js +711 -0
  212. package/build/dist/Server/Services/NetworkDeviceAutoImportRuleEngineService.js.map +1 -0
  213. package/build/dist/Server/Services/NetworkDeviceAutoImportRuleService.js +155 -0
  214. package/build/dist/Server/Services/NetworkDeviceAutoImportRuleService.js.map +1 -0
  215. package/build/dist/Server/Services/NetworkDeviceDiscoveryScanService.js +50 -0
  216. package/build/dist/Server/Services/NetworkDeviceDiscoveryScanService.js.map +1 -1
  217. package/build/dist/Server/Services/NetworkDeviceService.js +124 -0
  218. package/build/dist/Server/Services/NetworkDeviceService.js.map +1 -1
  219. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js +68 -13
  220. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js.map +1 -1
  221. package/build/dist/Server/Services/OnCallDutyPolicyFeedService.js +1 -1
  222. package/build/dist/Server/Services/OnCallDutyPolicyFeedService.js.map +1 -1
  223. package/build/dist/Server/Services/ProjectService.js +25 -45
  224. package/build/dist/Server/Services/ProjectService.js.map +1 -1
  225. package/build/dist/Server/Services/SecurityEventService.js +57 -5
  226. package/build/dist/Server/Services/SecurityEventService.js.map +1 -1
  227. package/build/dist/Server/Services/StatusPageResourceService.js +140 -0
  228. package/build/dist/Server/Services/StatusPageResourceService.js.map +1 -1
  229. package/build/dist/Server/Services/TelemetryAttributeService.js +17 -0
  230. package/build/dist/Server/Services/TelemetryAttributeService.js.map +1 -1
  231. package/build/dist/Server/Services/UserService.js +2 -12
  232. package/build/dist/Server/Services/UserService.js.map +1 -1
  233. package/build/dist/Server/Types/Markdown.js +147 -61
  234. package/build/dist/Server/Types/Markdown.js.map +1 -1
  235. package/build/dist/Server/Utils/AI/Toolbox/OnCallTools.js +12 -4
  236. package/build/dist/Server/Utils/AI/Toolbox/OnCallTools.js.map +1 -1
  237. package/build/dist/Server/Utils/AnalyticsDatabase/Statement.js +12 -1
  238. package/build/dist/Server/Utils/AnalyticsDatabase/Statement.js.map +1 -1
  239. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +246 -9
  240. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js.map +1 -1
  241. package/build/dist/Server/Utils/Attribution.js.map +1 -1
  242. package/build/dist/Server/Utils/Marketing/MarketingEventUtil.js +27 -7
  243. package/build/dist/Server/Utils/Marketing/MarketingEventUtil.js.map +1 -1
  244. package/build/dist/Server/Utils/Monitor/NetworkInventoryUtil.js +26 -0
  245. package/build/dist/Server/Utils/Monitor/NetworkInventoryUtil.js.map +1 -1
  246. package/build/dist/Server/Utils/SecurityEvent/DetectionRuleEvaluator.js +42 -7
  247. package/build/dist/Server/Utils/SecurityEvent/DetectionRuleEvaluator.js.map +1 -1
  248. package/build/dist/Server/Utils/SecurityEvent/Sigma/SigmaClickhouseCompiler.js +18 -0
  249. package/build/dist/Server/Utils/SecurityEvent/Sigma/SigmaClickhouseCompiler.js.map +1 -1
  250. package/build/dist/Server/Utils/StartServer.js +12 -21
  251. package/build/dist/Server/Utils/StartServer.js.map +1 -1
  252. package/build/dist/Server/Utils/Telemetry/LlmCostBudgetEvaluator.js +76 -5
  253. package/build/dist/Server/Utils/Telemetry/LlmCostBudgetEvaluator.js.map +1 -1
  254. package/build/dist/Server/Utils/Telemetry/LlmMetricSpend.js +102 -0
  255. package/build/dist/Server/Utils/Telemetry/LlmMetricSpend.js.map +1 -0
  256. package/build/dist/Types/Code/YamlSyntax.js +212 -0
  257. package/build/dist/Types/Code/YamlSyntax.js.map +1 -0
  258. package/build/dist/Types/Exception/ExceptionCode.js +1 -0
  259. package/build/dist/Types/Exception/ExceptionCode.js.map +1 -1
  260. package/build/dist/Types/Exception/PayloadTooLargeException.js +8 -0
  261. package/build/dist/Types/Exception/PayloadTooLargeException.js.map +1 -0
  262. package/build/dist/Types/Marketing/Attribution.js +10 -0
  263. package/build/dist/Types/Marketing/Attribution.js.map +1 -1
  264. package/build/dist/Types/Monitor/MonitorCriteriaInstance.js +112 -51
  265. package/build/dist/Types/Monitor/MonitorCriteriaInstance.js.map +1 -1
  266. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationSeverityMapper.js +2 -1
  267. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationSeverityMapper.js.map +1 -1
  268. package/build/dist/Types/NetworkAutomation/RuleRunResult.js +24 -0
  269. package/build/dist/Types/NetworkAutomation/RuleRunResult.js.map +1 -1
  270. package/build/dist/Types/Permission.js +41 -0
  271. package/build/dist/Types/Permission.js.map +1 -1
  272. package/build/dist/Types/SecurityEvent/DetectionFindingConstants.js +9 -0
  273. package/build/dist/Types/SecurityEvent/DetectionFindingConstants.js.map +1 -1
  274. package/build/dist/Types/Telemetry/LlmMetricConventions.js +98 -0
  275. package/build/dist/Types/Telemetry/LlmMetricConventions.js.map +1 -0
  276. package/build/dist/UI/Components/AutocompleteTextInput/AutocompleteTextInput.js +1 -1
  277. package/build/dist/UI/Components/AutocompleteTextInput/AutocompleteTextInput.js.map +1 -1
  278. package/build/dist/UI/Components/BulkUpdate/BulkUpdateForm.js +5 -2
  279. package/build/dist/UI/Components/BulkUpdate/BulkUpdateForm.js.map +1 -1
  280. package/build/dist/UI/Components/CodeEditor/CodeEditor.js +206 -76
  281. package/build/dist/UI/Components/CodeEditor/CodeEditor.js.map +1 -1
  282. package/build/dist/UI/Components/CodeEditor/YamlEditor.js +201 -0
  283. package/build/dist/UI/Components/CodeEditor/YamlEditor.js.map +1 -0
  284. package/build/dist/UI/Components/Detail/Detail.js +9 -0
  285. package/build/dist/UI/Components/Detail/Detail.js.map +1 -1
  286. package/build/dist/UI/Components/EntityDropdown/EntityDropdown.js +16 -1
  287. package/build/dist/UI/Components/EntityDropdown/EntityDropdown.js.map +1 -1
  288. package/build/dist/UI/Components/Forms/Fields/FormField.js +14 -1
  289. package/build/dist/UI/Components/Forms/Fields/FormField.js.map +1 -1
  290. package/build/dist/UI/Components/Forms/Types/Field.js.map +1 -1
  291. package/build/dist/UI/Components/Forms/Types/FormFieldSchemaType.js +1 -0
  292. package/build/dist/UI/Components/Forms/Types/FormFieldSchemaType.js.map +1 -1
  293. package/build/dist/UI/Components/Forms/Utils/FormFieldSchemaTypeUtil.js +2 -0
  294. package/build/dist/UI/Components/Forms/Utils/FormFieldSchemaTypeUtil.js.map +1 -1
  295. package/build/dist/UI/Components/Forms/Validation.js +27 -0
  296. package/build/dist/UI/Components/Forms/Validation.js.map +1 -1
  297. package/build/dist/UI/Components/Input/Input.js +1 -1
  298. package/build/dist/UI/Components/Input/Input.js.map +1 -1
  299. package/build/dist/UI/Components/ModelTable/AttributeColumns.js +159 -0
  300. package/build/dist/UI/Components/ModelTable/AttributeColumns.js.map +1 -0
  301. package/build/dist/UI/Components/ModelTable/BaseModelTable.js +179 -17
  302. package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
  303. package/build/dist/UI/Components/ModelTable/ColumnCustomizationModal.js +133 -2
  304. package/build/dist/UI/Components/ModelTable/ColumnCustomizationModal.js.map +1 -1
  305. package/build/dist/UI/Components/ModelTable/ColumnPreference.js +7 -1
  306. package/build/dist/UI/Components/ModelTable/ColumnPreference.js.map +1 -1
  307. package/build/dist/UI/Components/MonitorTemplateVariables/TemplateVariablesModal.js +2 -2
  308. package/build/dist/UI/Components/SideOver/SideOver.js +5 -5
  309. package/build/dist/UI/Components/StatusPage/ResourceGrid.js +28 -2
  310. package/build/dist/UI/Components/StatusPage/ResourceGrid.js.map +1 -1
  311. package/build/dist/UI/Components/StatusPage/ResourceList.js +87 -20
  312. package/build/dist/UI/Components/StatusPage/ResourceList.js.map +1 -1
  313. package/build/dist/UI/Components/Toggle/Toggle.js +9 -1
  314. package/build/dist/UI/Components/Toggle/Toggle.js.map +1 -1
  315. package/build/dist/UI/Components/Types/FieldType.js +1 -0
  316. package/build/dist/UI/Components/Types/FieldType.js.map +1 -1
  317. package/build/dist/Utils/NetworkDevice/DeviceReachabilityUtil.js +37 -1
  318. package/build/dist/Utils/NetworkDevice/DeviceReachabilityUtil.js.map +1 -1
  319. package/build/dist/Utils/NetworkDiscovery/AutoImportRuleMatcher.js +149 -0
  320. package/build/dist/Utils/NetworkDiscovery/AutoImportRuleMatcher.js.map +1 -0
  321. package/build/dist/Utils/NetworkDiscovery/DiscoveredDeviceBuilder.js +121 -0
  322. package/build/dist/Utils/NetworkDiscovery/DiscoveredDeviceBuilder.js.map +1 -0
  323. package/build/dist/Utils/NetworkDiscovery/DiscoveredHostUtil.js +75 -0
  324. package/build/dist/Utils/NetworkDiscovery/DiscoveredHostUtil.js.map +1 -0
  325. package/build/dist/Utils/NetworkDiscovery/DiscoveryImportEligibility.js +59 -0
  326. package/build/dist/Utils/NetworkDiscovery/DiscoveryImportEligibility.js.map +1 -0
  327. package/build/dist/Utils/NetworkDiscovery/ScanNameUtil.js +120 -0
  328. package/build/dist/Utils/NetworkDiscovery/ScanNameUtil.js.map +1 -0
  329. package/build/dist/Utils/NetworkDiscovery/ScanTargetUtil.js +60 -0
  330. package/build/dist/Utils/NetworkDiscovery/ScanTargetUtil.js.map +1 -1
  331. package/build/dist/Utils/StatusPage/ResourceExplorer.js +171 -0
  332. package/build/dist/Utils/StatusPage/ResourceExplorer.js.map +1 -1
  333. package/build/dist/Utils/Telemetry/LlmMetricQuery.js +119 -0
  334. package/build/dist/Utils/Telemetry/LlmMetricQuery.js.map +1 -0
  335. package/jest.config.json +2 -0
  336. package/package.json +1 -1
  337. package/tsconfig.json +2 -1
  338. package/Tests/UI/Components/Toast.test.tsx +0 -197
  339. package/Tests/UI/Components/ToastStacking.test.tsx +0 -198
  340. package/UI/Components/Toast/Toast.tsx +0 -189
  341. package/UI/Components/Toast/ToastInit.tsx +0 -110
  342. package/build/dist/UI/Components/Toast/Toast.js +0 -123
  343. package/build/dist/UI/Components/Toast/Toast.js.map +0 -1
  344. package/build/dist/UI/Components/Toast/ToastInit.js +0 -53
  345. package/build/dist/UI/Components/Toast/ToastInit.js.map +0 -1
@@ -40,6 +40,7 @@ jest.mock("../../../../Server/Infrastructure/Queue", () => {
40
40
 
41
41
  import MarketingEventUtil from "../../../../Server/Utils/Marketing/MarketingEventUtil";
42
42
  import Attribution from "../../../../Server/Utils/Attribution";
43
+ import { UtmPropertyKeys } from "../../../../Types/Marketing/Attribution";
43
44
  import {
44
45
  MarketingEvent,
45
46
  MarketingEventType,
@@ -120,12 +121,54 @@ describe("MarketingEventUtil", () => {
120
121
  utmCampaign: "brand",
121
122
  utmTerm: "oneuptime",
122
123
  utmContent: "hero-b",
124
+ utmId: undefined,
125
+ utmSourcePlatform: undefined,
126
+ utmCreativeFormat: undefined,
127
+ utmMarketingTactic: undefined,
123
128
  utmUrl: "https://oneuptime.com/?gclid=x",
124
129
  clickIds: { gclid: "x" },
125
130
  firstTouch: { utmSource: "linkedin" },
126
131
  });
127
132
  });
128
133
 
134
+ /*
135
+ * THE CONTRACT, ASSERTED MECHANICALLY.
136
+ *
137
+ * The browser captures every key in UtmWireKeyToPropertyKey, the signup
138
+ * door stores them and the Cal embed sends them — and for four of them the
139
+ * outbound conversion carried nothing at all, because this payload was
140
+ * hand-listed and nobody updated it. Nothing errored; conversions kept
141
+ * arriving, just unattributable on those dimensions.
142
+ *
143
+ * Built from the contract rather than a fixture, so a key added to
144
+ * Common/Types/Marketing/Attribution.ts is exercised here automatically
145
+ * instead of quietly going missing on the wire again.
146
+ */
147
+ test("carries every UTM key the shared contract names", () => {
148
+ const source: Record<string, string> = {};
149
+
150
+ for (const propertyKey of UtmPropertyKeys) {
151
+ source[propertyKey] = `${propertyKey}-value`;
152
+ }
153
+
154
+ const event: MarketingEvent = MarketingEventUtil.buildEvent({
155
+ eventType: MarketingEventType.SignUp,
156
+ eventId: "sign_up:u1",
157
+ occurredAt: new Date(),
158
+ attributionSource: source,
159
+ });
160
+
161
+ const attribution: Record<string, unknown> =
162
+ event.attribution as unknown as Record<string, unknown>;
163
+
164
+ // The fixture has to exercise the keys for this to mean anything.
165
+ expect(UtmPropertyKeys.length).toBeGreaterThanOrEqual(9);
166
+
167
+ for (const propertyKey of UtmPropertyKeys) {
168
+ expect(attribution[propertyKey]).toBe(`${propertyKey}-value`);
169
+ }
170
+ });
171
+
129
172
  /*
130
173
  * A conversion carrying no attribution is still a conversion. The two
131
174
  * container fields default to empty objects rather than undefined so a
@@ -200,30 +200,34 @@ describe("ProfileMonitorCriteria.isMonitorInstanceCriteriaFilterMet", () => {
200
200
  });
201
201
 
202
202
  /*
203
- * The exact production shape of a "profile presence" alert: criteria
204
- * "Profile Count >= 1" flips the monitor Offline the moment a matching
205
- * profile is ingested. Mirrors the log-presence offline criteria and keeps
206
- * the count-based criteria in lock-step.
203
+ * A hand-written inversion of the shipped default, not the default itself.
204
+ * Profiles ships the Logs polarity - profiles arriving is the sign of life,
205
+ * so the default offline criteria is "Profile Count == 0" (covered above)
206
+ * and the online one is "Profile Count > 0". Someone monitoring for the
207
+ * appearance of profiles rather than their absence can write "Profile
208
+ * Count >= 1" and point it at the offline status instead; these tests pin
209
+ * that the evaluator decides that rule the same way, since nothing in
210
+ * ProfileMonitorCriteria knows or cares which status a criteria carries.
207
211
  */
208
- describe("profile-presence offline criteria (Profile Count >= 1)", () => {
209
- const offlineCriteria: CriteriaFilter = {
212
+ describe("a user-authored profile-presence criteria (Profile Count >= 1)", () => {
213
+ const presenceCriteria: CriteriaFilter = {
210
214
  checkOn: CheckOn.ProfileCount,
211
215
  filterType: FilterType.GreaterThanOrEqualTo,
212
216
  value: 1,
213
217
  };
214
218
 
215
- test("one matching profile → met (monitor goes offline)", async () => {
216
- const result: string | null = await evaluate(1, offlineCriteria);
219
+ test("one matching profile → met", async () => {
220
+ const result: string | null = await evaluate(1, presenceCriteria);
217
221
  expect(result).toBeTruthy();
218
222
  expect(result).toContain("Profile Count");
219
223
  });
220
224
 
221
225
  test("many matching profiles → met", async () => {
222
- expect(await evaluate(6718284, offlineCriteria)).toBeTruthy();
226
+ expect(await evaluate(6718284, presenceCriteria)).toBeTruthy();
223
227
  });
224
228
 
225
- test("zero matching profiles → not met (monitor stays operational)", async () => {
226
- expect(await evaluate(0, offlineCriteria)).toBeNull();
229
+ test("zero matching profiles → not met", async () => {
230
+ expect(await evaluate(0, presenceCriteria)).toBeNull();
227
231
  });
228
232
  });
229
233
 
@@ -11,6 +11,10 @@ import SnmpInterface from "../../../../Types/Monitor/SnmpMonitor/SnmpInterface";
11
11
  import SnmpMonitorResponse from "../../../../Types/Monitor/SnmpMonitor/SnmpMonitorResponse";
12
12
  import LldpNeighbor from "../../../../Types/Monitor/SnmpMonitor/LldpNeighbor";
13
13
  import CdpNeighbor from "../../../../Types/Monitor/SnmpMonitor/CdpNeighbor";
14
+ import SnmpOid from "../../../../Types/Monitor/SnmpMonitor/SnmpOid";
15
+ import SnmpVendorTemplateUtil, {
16
+ SnmpVendorTemplate,
17
+ } from "../../../../Types/Monitor/SnmpMonitor/SnmpVendorTemplate";
14
18
 
15
19
  /*
16
20
  * NetworkInventoryUtil.updateFromWalk is the single writer that keeps the
@@ -40,14 +44,21 @@ let interfaceUpdateSpy: jest.SpyInstance;
40
44
  let interfaceCreateSpy: jest.SpyInstance;
41
45
  let endpointUpsertSpy: jest.SpyInstance;
42
46
 
43
- function mockServices(existingInterfaces: Array<NetworkInterface> = []): void {
47
+ function mockServices(
48
+ existingInterfaces: Array<NetworkInterface> = [],
49
+ deviceOverrides: Partial<NetworkDevice> = {},
50
+ ): void {
44
51
  /*
45
52
  * The project-membership guard resolves the device (scoped to the given
46
53
  * project) before any write; return a matching device so the write path
47
54
  * runs. The cross-project-refusal case overrides this to null.
55
+ * deviceOverrides seeds the columns the vendor-template auto-apply reads
56
+ * (autoApplyVendorHealthTemplate, snmpOids) — absent by default, exactly
57
+ * like a device that never opted in.
48
58
  */
49
59
  const ownedDevice: NetworkDevice = new NetworkDevice();
50
60
  ownedDevice.id = new ObjectID(DEVICE_ID);
61
+ Object.assign(ownedDevice, deviceOverrides);
51
62
  deviceFindSpy = jest
52
63
  .spyOn(NetworkDeviceService, "findOneBy")
53
64
  .mockResolvedValue(ownedDevice);
@@ -867,3 +878,88 @@ describe("NetworkInventoryUtil.updateFromWalk — endpoint discovery", () => {
867
878
  ]);
868
879
  });
869
880
  });
881
+
882
+ describe("NetworkInventoryUtil.updateFromWalk — vendor health template auto-apply", () => {
883
+ /*
884
+ * The automatic counterpart of the dashboard's vendor-template banner:
885
+ * a device that opted in (auto-imported devices do) gets its EMPTY Health
886
+ * OID list seeded from the vendor template its sysObjectID fingerprints,
887
+ * on the first poll that learns the vendor. An existing list — however it
888
+ * got there — is the operator's and is never touched.
889
+ */
890
+ test("an opted-in device with no health OIDs is seeded from the fingerprinted vendor template", async () => {
891
+ mockServices([], { autoApplyVendorHealthTemplate: true });
892
+
893
+ await runWalk({
894
+ systemInfo: {
895
+ sysObjectId: CISCO_SYS_OBJECT_ID,
896
+ },
897
+ });
898
+
899
+ const update: DeviceUpdatePayload = deviceUpdatePayload();
900
+ const seeded: Array<SnmpOid> = update["snmpOids"] as Array<SnmpOid>;
901
+
902
+ const ciscoTemplate: SnmpVendorTemplate | undefined =
903
+ SnmpVendorTemplateUtil.matchBySysObjectId(CISCO_SYS_OBJECT_ID);
904
+
905
+ expect(ciscoTemplate).toBeDefined();
906
+ expect(seeded).toEqual(ciscoTemplate!.oids);
907
+ expect(seeded.length).toBeGreaterThan(0);
908
+ });
909
+
910
+ test("a device that never opted in is not seeded", async () => {
911
+ mockServices();
912
+
913
+ await runWalk({
914
+ systemInfo: {
915
+ sysObjectId: CISCO_SYS_OBJECT_ID,
916
+ },
917
+ });
918
+
919
+ expect(deviceUpdatePayload()).not.toHaveProperty("snmpOids");
920
+ });
921
+
922
+ test("an existing health OID list is never touched, even with the toggle on", async () => {
923
+ const handPickedOids: Array<SnmpOid> = [
924
+ { oid: "1.3.6.1.4.1.9.9.109.1.1.1.1.7.1", name: "CPU 5min" },
925
+ ];
926
+
927
+ mockServices([], {
928
+ autoApplyVendorHealthTemplate: true,
929
+ snmpOids: handPickedOids,
930
+ });
931
+
932
+ await runWalk({
933
+ systemInfo: {
934
+ sysObjectId: CISCO_SYS_OBJECT_ID,
935
+ },
936
+ });
937
+
938
+ expect(deviceUpdatePayload()).not.toHaveProperty("snmpOids");
939
+ });
940
+
941
+ test("a walk that learned no sysObjectID seeds nothing", async () => {
942
+ mockServices([], { autoApplyVendorHealthTemplate: true });
943
+
944
+ await runWalk({
945
+ systemInfo: {
946
+ sysName: "core-sw-01",
947
+ },
948
+ });
949
+
950
+ expect(deviceUpdatePayload()).not.toHaveProperty("snmpOids");
951
+ });
952
+
953
+ test("an enterprise with no vendor template seeds nothing", async () => {
954
+ mockServices([], { autoApplyVendorHealthTemplate: true });
955
+
956
+ // Enterprise 99999 has no entry in ENTERPRISE_TEMPLATE_IDS.
957
+ await runWalk({
958
+ systemInfo: {
959
+ sysObjectId: "1.3.6.1.4.1.99999.1.1",
960
+ },
961
+ });
962
+
963
+ expect(deviceUpdatePayload()).not.toHaveProperty("snmpOids");
964
+ });
965
+ });
@@ -36,11 +36,13 @@ import IncidentSeverityService from "../../../../Server/Services/IncidentSeverit
36
36
  import DetectionRuleService from "../../../../Server/Services/DetectionRuleService";
37
37
  import SecurityEventService, {
38
38
  DetectionMatchGroup,
39
+ FindDetectionMatchesData,
39
40
  } from "../../../../Server/Services/SecurityEventService";
40
41
  import OTelIngestService, {
41
42
  TelemetryServiceMetadata,
42
43
  } from "../../../../Server/Services/OpenTelemetryIngestService";
43
44
  import logger from "../../../../Server/Utils/Logger";
45
+ import { Statement } from "../../../../Server/Utils/AnalyticsDatabase/Statement";
44
46
  import MetricSeriesFingerprint from "../../../../Utils/Metrics/MetricSeriesFingerprint";
45
47
  import ObjectID from "../../../../Types/ObjectID";
46
48
  import OneUptimeDate from "../../../../Types/Date";
@@ -107,6 +109,9 @@ function buildRule(
107
109
  lastEvaluatedAt?: Date;
108
110
  evaluationIntervalInMinutes?: number;
109
111
  id?: ObjectID;
112
+ groupByField?: string;
113
+ distinctCountField?: string;
114
+ matchCountThreshold?: number;
110
115
  } = {},
111
116
  ): DetectionRule {
112
117
  const rule: DetectionRule = new DetectionRule();
@@ -117,7 +122,15 @@ function buildRule(
117
122
  rule.sigmaRuleYaml =
118
123
  options.sigmaRuleYaml ?? sigmaYaml(options.level || "high");
119
124
  rule.evaluationIntervalInMinutes = options.evaluationIntervalInMinutes ?? 5;
120
- rule.groupByField = "principalUser";
125
+ rule.groupByField = options.groupByField ?? "principalUser";
126
+
127
+ if (options.distinctCountField !== undefined) {
128
+ rule.distinctCountField = options.distinctCountField;
129
+ }
130
+
131
+ if (options.matchCountThreshold !== undefined) {
132
+ rule.matchCountThreshold = options.matchCountThreshold;
133
+ }
121
134
 
122
135
  if (options.shouldCreateAlert !== undefined) {
123
136
  rule.shouldCreateAlert = options.shouldCreateAlert;
@@ -156,10 +169,12 @@ function buildSeverity(idSuffix: string, name: string): AlertSeverity {
156
169
  function buildGroup(
157
170
  groupValue: string,
158
171
  matchCount: number,
172
+ distinctCount: number = 0,
159
173
  ): DetectionMatchGroup {
160
174
  return {
161
175
  groupValue,
162
176
  matchCount,
177
+ distinctCount,
163
178
  sampleMessage: `sample event for ${groupValue || "all"}`,
164
179
  sampleObservables: [groupValue, "10.0.0.9"].filter(
165
180
  (observable: string): boolean => {
@@ -297,15 +312,24 @@ function createdIncident(spies: EvaluationSpies, index: number): Incident {
297
312
  return (call as { data: Incident }).data;
298
313
  }
299
314
 
315
+ function findMatchesArg(spies: EvaluationSpies): FindDetectionMatchesData {
316
+ return spies.findDetectionMatches.mock
317
+ .calls[0]?.[0] as FindDetectionMatchesData;
318
+ }
319
+
320
+ // The evaluator logs expected failures; keep test output clean.
321
+ function silenceLogger(): void {
322
+ getJestSpyOn(logger, "error").mockImplementation((() => {
323
+ return undefined;
324
+ }) as never);
325
+ getJestSpyOn(logger, "warn").mockImplementation((() => {
326
+ return undefined;
327
+ }) as never);
328
+ }
329
+
300
330
  describe("DetectionRuleEvaluator", () => {
301
331
  beforeEach(() => {
302
- // The evaluator logs expected failures; keep test output clean.
303
- getJestSpyOn(logger, "error").mockImplementation((() => {
304
- return undefined;
305
- }) as never);
306
- getJestSpyOn(logger, "warn").mockImplementation((() => {
307
- return undefined;
308
- }) as never);
332
+ silenceLogger();
309
333
  });
310
334
 
311
335
  afterEach(() => {
@@ -1042,6 +1066,29 @@ describe("DetectionRuleEvaluator", () => {
1042
1066
  expect(select["shouldCreateAlert"]).toBe(true);
1043
1067
  expect(select["alertSeverityId"]).toBe(true);
1044
1068
  });
1069
+
1070
+ test("selects the distinct-count columns so thresholds do not silently read as 1", async () => {
1071
+ /*
1072
+ * Same drift as the incident columns: a rule fetched WITHOUT
1073
+ * matchCountThreshold in the select falls back to the
1074
+ * fire-on-any-match default, and one fetched without
1075
+ * distinctCountField silently thresholds the raw count instead of
1076
+ * the distinct count — neither failure trips any other assertion.
1077
+ */
1078
+ const rulesFindBy: Spy = getJestSpyOn(
1079
+ DetectionRuleService,
1080
+ "findBy",
1081
+ ).mockResolvedValue([] as never);
1082
+
1083
+ await DetectionRuleEvaluator.evaluateAllDueRules();
1084
+
1085
+ const select: JSONObject = (
1086
+ rulesFindBy.mock.calls[0]?.[0] as { select: JSONObject }
1087
+ ).select;
1088
+
1089
+ expect(select["distinctCountField"]).toBe(true);
1090
+ expect(select["matchCountThreshold"]).toBe(true);
1091
+ });
1045
1092
  });
1046
1093
 
1047
1094
  describe("rule bookkeeping", () => {
@@ -1095,4 +1142,318 @@ describe("DetectionRuleEvaluator", () => {
1095
1142
  ).toBe(false);
1096
1143
  });
1097
1144
  });
1145
+
1146
+ describe("distinct count and match count threshold", () => {
1147
+ /*
1148
+ * matchCountThreshold holds a rule back until a group reaches N
1149
+ * events, and distinctCountField switches what N counts: raw
1150
+ * matches, or uniqExact of one field. The ClickHouse HAVING clause
1151
+ * already enforces the threshold server-side, but the evaluator
1152
+ * re-filters so the firing contract does not depend on which side
1153
+ * built the rows — these tests pin that in-process filter plus the
1154
+ * exact query contract handed to SecurityEventService.
1155
+ */
1156
+ test("threshold on the raw count path: only groups at/above it fire and totalMatches sums firing groups only", async () => {
1157
+ const rule: DetectionRule = buildRule({
1158
+ matchCountThreshold: 3,
1159
+ shouldWriteDetectionFinding: false,
1160
+ });
1161
+
1162
+ const spies: EvaluationSpies = installSpies({
1163
+ groups: [
1164
+ buildGroup("alice", 5),
1165
+ buildGroup("bob", 2), // below threshold — must not fire
1166
+ buildGroup("carol", 3), // exactly at threshold — fires
1167
+ ],
1168
+ severities: [buildSeverity("0001", "Default")],
1169
+ });
1170
+
1171
+ const result: DetectionRuleEvaluationResult =
1172
+ await DetectionRuleEvaluator.evaluateRule(rule);
1173
+
1174
+ expect(result.matchedGroups).toBe(2);
1175
+ // 5 + 3; bob's 2 sub-threshold events are not "matches" at all.
1176
+ expect(result.totalMatches).toBe(8);
1177
+ expect(result.alertsCreated).toBe(2);
1178
+
1179
+ const alertFingerprints: Array<string | undefined> = [
1180
+ createdAlert(spies, 0).seriesFingerprint,
1181
+ createdAlert(spies, 1).seriesFingerprint,
1182
+ ];
1183
+
1184
+ expect(alertFingerprints).toContain(expectedFingerprint("alice"));
1185
+ expect(alertFingerprints).toContain(expectedFingerprint("carol"));
1186
+ expect(alertFingerprints).not.toContain(expectedFingerprint("bob"));
1187
+ });
1188
+
1189
+ test("an unset threshold behaves as 1 — pre-threshold rules keep firing on any match", async () => {
1190
+ const rule: DetectionRule = buildRule({
1191
+ shouldWriteDetectionFinding: false,
1192
+ });
1193
+
1194
+ const spies: EvaluationSpies = installSpies({
1195
+ groups: [buildGroup("alice", 1)],
1196
+ severities: [buildSeverity("0001", "Default")],
1197
+ });
1198
+
1199
+ const result: DetectionRuleEvaluationResult =
1200
+ await DetectionRuleEvaluator.evaluateRule(rule);
1201
+
1202
+ expect(result.matchedGroups).toBe(1);
1203
+ expect(result.alertsCreated).toBe(1);
1204
+ expect(findMatchesArg(spies).minMatchCount).toBe(1);
1205
+ });
1206
+
1207
+ test("a threshold of 0 behaves as 1, not as fire-on-nothing", async () => {
1208
+ const rule: DetectionRule = buildRule({
1209
+ matchCountThreshold: 0,
1210
+ shouldWriteDetectionFinding: false,
1211
+ });
1212
+
1213
+ const spies: EvaluationSpies = installSpies({
1214
+ groups: [buildGroup("alice", 1)],
1215
+ severities: [buildSeverity("0001", "Default")],
1216
+ });
1217
+
1218
+ const result: DetectionRuleEvaluationResult =
1219
+ await DetectionRuleEvaluator.evaluateRule(rule);
1220
+
1221
+ expect(result.matchedGroups).toBe(1);
1222
+ expect(result.alertsCreated).toBe(1);
1223
+ // The clamp happens before the query, not just in the re-filter.
1224
+ expect(findMatchesArg(spies).minMatchCount).toBe(1);
1225
+ });
1226
+
1227
+ test("a distinct-count rule fires on distinctCount, not matchCount", async () => {
1228
+ const rule: DetectionRule = buildRule({
1229
+ distinctCountField: "principalIp",
1230
+ matchCountThreshold: 5,
1231
+ shouldWriteDetectionFinding: false,
1232
+ });
1233
+
1234
+ const spies: EvaluationSpies = installSpies({
1235
+ groups: [
1236
+ // 50 raw matches from only 2 IPs — noisy, not distributed.
1237
+ buildGroup("alice", 50, 2),
1238
+ // 6 raw matches from 5 IPs — under on raw count, fires on distinct.
1239
+ buildGroup("bob", 6, 5),
1240
+ ],
1241
+ severities: [buildSeverity("0001", "Default")],
1242
+ });
1243
+
1244
+ const result: DetectionRuleEvaluationResult =
1245
+ await DetectionRuleEvaluator.evaluateRule(rule);
1246
+
1247
+ expect(result.matchedGroups).toBe(1);
1248
+ expect(result.totalMatches).toBe(6);
1249
+ expect(result.alertsCreated).toBe(1);
1250
+ expect(createdAlert(spies, 0).seriesFingerprint).toBe(
1251
+ expectedFingerprint("bob"),
1252
+ );
1253
+ });
1254
+
1255
+ test("a group with zero matches never fires even when distinctCount clears the threshold", async () => {
1256
+ const rule: DetectionRule = buildRule({
1257
+ distinctCountField: "principalIp",
1258
+ matchCountThreshold: 5,
1259
+ shouldWriteDetectionFinding: false,
1260
+ });
1261
+
1262
+ const spies: EvaluationSpies = installSpies({
1263
+ // Nonsense a buggy query could emit: distinct 7 over 0 rows.
1264
+ groups: [buildGroup("alice", 0, 7)],
1265
+ severities: [buildSeverity("0001", "Default")],
1266
+ });
1267
+
1268
+ const result: DetectionRuleEvaluationResult =
1269
+ await DetectionRuleEvaluator.evaluateRule(rule);
1270
+
1271
+ expect(result.matchedGroups).toBe(0);
1272
+ expect(result.totalMatches).toBe(0);
1273
+ expect(spies.alertCreate).not.toHaveBeenCalled();
1274
+ });
1275
+
1276
+ test("a plain rule queries with a null distinct expression, its threshold, and the existing group contract", async () => {
1277
+ const rule: DetectionRule = buildRule({
1278
+ matchCountThreshold: 4,
1279
+ shouldWriteDetectionFinding: false,
1280
+ });
1281
+
1282
+ const spies: EvaluationSpies = installSpies();
1283
+
1284
+ await DetectionRuleEvaluator.evaluateRule(rule);
1285
+
1286
+ const args: FindDetectionMatchesData = findMatchesArg(spies);
1287
+
1288
+ expect(args.projectId.toString()).toBe(PROJECT_ID.toString());
1289
+ expect(args.minMatchCount).toBe(4);
1290
+ expect(args.distinctCountExpression).toBeNull();
1291
+ // groupByField still compiles to an expression; maxGroups is intact.
1292
+ expect(args.groupByExpression).toBeInstanceOf(Statement);
1293
+ expect(args.maxGroups).toBe(100);
1294
+ });
1295
+
1296
+ test("a distinct-count rule queries with a compiled Statement for the distinct field", async () => {
1297
+ const rule: DetectionRule = buildRule({
1298
+ distinctCountField: "principalIp",
1299
+ matchCountThreshold: 5,
1300
+ shouldWriteDetectionFinding: false,
1301
+ });
1302
+
1303
+ const spies: EvaluationSpies = installSpies();
1304
+
1305
+ await DetectionRuleEvaluator.evaluateRule(rule);
1306
+
1307
+ const args: FindDetectionMatchesData = findMatchesArg(spies);
1308
+
1309
+ expect(args.distinctCountExpression).toBeInstanceOf(Statement);
1310
+ expect(args.minMatchCount).toBe(5);
1311
+ expect(args.groupByExpression).toBeInstanceOf(Statement);
1312
+ });
1313
+
1314
+ test("a distinct rule's alert description explains the distinct count and threshold", async () => {
1315
+ const rule: DetectionRule = buildRule({
1316
+ distinctCountField: "principalIp",
1317
+ matchCountThreshold: 5,
1318
+ shouldWriteDetectionFinding: false,
1319
+ });
1320
+
1321
+ const spies: EvaluationSpies = installSpies({
1322
+ groups: [buildGroup("alice", 6, 5)],
1323
+ severities: [buildSeverity("0001", "Default")],
1324
+ });
1325
+
1326
+ await DetectionRuleEvaluator.evaluateRule(rule);
1327
+
1328
+ expect(createdAlert(spies, 0).description).toContain(
1329
+ "5 distinct principalIp values across these events (rule threshold: 5).",
1330
+ );
1331
+ });
1332
+
1333
+ test("a plain rule's alert description never says distinct", async () => {
1334
+ const rule: DetectionRule = buildRule({
1335
+ shouldWriteDetectionFinding: false,
1336
+ });
1337
+
1338
+ const spies: EvaluationSpies = installSpies({
1339
+ groups: [buildGroup("alice", 6)],
1340
+ severities: [buildSeverity("0001", "Default")],
1341
+ });
1342
+
1343
+ await DetectionRuleEvaluator.evaluateRule(rule);
1344
+
1345
+ expect(createdAlert(spies, 0).description).not.toContain("distinct");
1346
+ });
1347
+
1348
+ test("a distinct rule's findings carry the distinct-count attribute and message suffix", async () => {
1349
+ const rule: DetectionRule = buildRule({
1350
+ distinctCountField: "principalIp",
1351
+ matchCountThreshold: 5,
1352
+ shouldCreateAlert: false,
1353
+ });
1354
+
1355
+ const spies: EvaluationSpies = installSpies({
1356
+ groups: [buildGroup("alice", 6, 5)],
1357
+ });
1358
+
1359
+ await DetectionRuleEvaluator.evaluateRule(rule);
1360
+
1361
+ const row: JSONObject = (
1362
+ spies.insertJsonRows.mock.calls[0]?.[0] as Array<JSONObject>
1363
+ )[0]!;
1364
+
1365
+ expect(
1366
+ (row["attributes"] as JSONObject)["oneuptime.detection.distinct_count"],
1367
+ ).toBe("5");
1368
+ expect(row["message"]).toContain("(6 events, 5 distinct principalIp)");
1369
+ });
1370
+
1371
+ test("a plain rule's findings carry no distinct-count attribute or message suffix", async () => {
1372
+ const rule: DetectionRule = buildRule({
1373
+ shouldCreateAlert: false,
1374
+ });
1375
+
1376
+ const spies: EvaluationSpies = installSpies({
1377
+ groups: [buildGroup("alice", 6)],
1378
+ });
1379
+
1380
+ await DetectionRuleEvaluator.evaluateRule(rule);
1381
+
1382
+ const row: JSONObject = (
1383
+ spies.insertJsonRows.mock.calls[0]?.[0] as Array<JSONObject>
1384
+ )[0]!;
1385
+
1386
+ /*
1387
+ * Absent, not "0": consumers filter on attribute presence, and a
1388
+ * plain rule has no distinct semantics to report.
1389
+ */
1390
+ expect(
1391
+ Object.prototype.hasOwnProperty.call(
1392
+ row["attributes"] as JSONObject,
1393
+ "oneuptime.detection.distinct_count",
1394
+ ),
1395
+ ).toBe(false);
1396
+ expect(row["message"]).not.toContain("distinct");
1397
+ });
1398
+
1399
+ test("flagship: distinct usernames per source IP — one qualifying group, one alert, one finding", async () => {
1400
+ /*
1401
+ * The scenario the feature was built for: password spraying.
1402
+ * Group by attacker IP, count distinct usernames tried, fire at 5.
1403
+ * One IP hammered a single account 40 times (lockout noise, not a
1404
+ * spray); another tried 6 accounts across 9 events.
1405
+ */
1406
+ const rule: DetectionRule = buildRule({
1407
+ groupByField: "principalIp",
1408
+ distinctCountField: "principalUser",
1409
+ matchCountThreshold: 5,
1410
+ });
1411
+
1412
+ const spies: EvaluationSpies = installSpies({
1413
+ groups: [
1414
+ buildGroup("203.0.113.7", 9, 6),
1415
+ buildGroup("198.51.100.4", 40, 1),
1416
+ ],
1417
+ severities: [buildSeverity("0001", "High")],
1418
+ });
1419
+
1420
+ const result: DetectionRuleEvaluationResult =
1421
+ await DetectionRuleEvaluator.evaluateRule(rule);
1422
+
1423
+ expect(result.matchedGroups).toBe(1);
1424
+ expect(result.totalMatches).toBe(9);
1425
+ expect(result.alertsCreated).toBe(1);
1426
+ expect(result.findingsWritten).toBe(1);
1427
+ expect(result.error).toBeNull();
1428
+
1429
+ // The query was built from the rule's three knobs.
1430
+ const args: FindDetectionMatchesData = findMatchesArg(spies);
1431
+ expect(args.groupByExpression).toBeInstanceOf(Statement);
1432
+ expect(args.distinctCountExpression).toBeInstanceOf(Statement);
1433
+ expect(args.minMatchCount).toBe(5);
1434
+
1435
+ const alert: Alert = createdAlert(spies, 0);
1436
+
1437
+ expect(alert.title).toContain("203.0.113.7");
1438
+ expect(alert.seriesFingerprint).toBe(expectedFingerprint("203.0.113.7"));
1439
+ expect(alert.description).toContain(
1440
+ "6 distinct principalUser values across these events (rule threshold: 5).",
1441
+ );
1442
+
1443
+ const rows: Array<JSONObject> = spies.insertJsonRows.mock
1444
+ .calls[0]?.[0] as Array<JSONObject>;
1445
+
1446
+ expect(rows).toHaveLength(1);
1447
+
1448
+ const row: JSONObject = rows[0]!;
1449
+ const attributes: JSONObject = row["attributes"] as JSONObject;
1450
+
1451
+ expect(attributes["oneuptime.detection.group_value"]).toBe("203.0.113.7");
1452
+ expect(attributes["oneuptime.detection.match_count"]).toBe("9");
1453
+ expect(attributes["oneuptime.detection.distinct_count"]).toBe("6");
1454
+ expect(row["message"]).toContain(
1455
+ "Detection: Brute Force Watch — 203.0.113.7 (9 events, 6 distinct principalUser)",
1456
+ );
1457
+ });
1458
+ });
1098
1459
  });