@oneuptime/common 12.0.23 → 12.0.25

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 (323) hide show
  1. package/Models/AnalyticsModels/Span.ts +101 -0
  2. package/Models/DatabaseModels/DetectionRule.ts +55 -0
  3. package/Models/DatabaseModels/NetworkDeviceAutoImportRule.ts +27 -7
  4. package/Models/DatabaseModels/NetworkDeviceDiscoveryScan.ts +67 -0
  5. package/Models/DatabaseModels/Project.ts +68 -0
  6. package/Models/DatabaseModels/User.ts +68 -0
  7. package/Server/API/BaseAPI.ts +0 -24
  8. package/Server/API/SlackAPI.ts +0 -2
  9. package/Server/API/TelemetryAPI.ts +29 -0
  10. package/Server/Infrastructure/Postgres/SchemaMigrations/1789400000000-AddNameToNetworkDeviceDiscoveryScan.ts +28 -0
  11. package/Server/Infrastructure/Postgres/SchemaMigrations/1789500000000-AddCampaignUtmFields.ts +49 -0
  12. package/Server/Infrastructure/Postgres/SchemaMigrations/1789512000000-AddDetectionRuleDistinctCountColumns.ts +33 -0
  13. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +6 -0
  14. package/Server/Middleware/GzipRequestBody.ts +311 -0
  15. package/Server/Middleware/MultipartFormData.ts +87 -3
  16. package/Server/Middleware/SlackAuthorization.ts +96 -18
  17. package/Server/Services/DetectionRuleService.ts +73 -6
  18. package/Server/Services/Index.ts +205 -0
  19. package/Server/Services/NetworkDeviceDiscoveryScanService.ts +59 -0
  20. package/Server/Services/NetworkDeviceService.ts +148 -0
  21. package/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.ts +65 -0
  22. package/Server/Services/OnCallDutyPolicyFeedService.ts +1 -1
  23. package/Server/Services/ProjectService.ts +29 -19
  24. package/Server/Services/SecurityEventService.ts +90 -13
  25. package/Server/Services/StatusPageResourceService.ts +223 -0
  26. package/Server/Services/TelemetryAttributeService.ts +17 -0
  27. package/Server/Services/UserService.ts +4 -6
  28. package/Server/Types/Markdown.ts +161 -67
  29. package/Server/Utils/AI/Toolbox/OnCallTools.ts +12 -4
  30. package/Server/Utils/AnalyticsDatabase/Statement.ts +13 -1
  31. package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +297 -6
  32. package/Server/Utils/Attribution.ts +4 -0
  33. package/Server/Utils/Marketing/MarketingEventUtil.ts +45 -7
  34. package/Server/Utils/SecurityEvent/DetectionRuleEvaluator.ts +55 -3
  35. package/Server/Utils/SecurityEvent/Sigma/SigmaClickhouseCompiler.ts +21 -0
  36. package/Server/Utils/StartServer.ts +12 -32
  37. package/Server/Utils/Telemetry/LlmCostBudgetEvaluator.ts +101 -4
  38. package/Server/Utils/Telemetry/LlmMetricSpend.ts +181 -0
  39. package/Server/Utils/Telemetry/LlmSpan.ts +46 -0
  40. package/Server/Utils/Workspace/Slack/Actions/Auth.ts +0 -12
  41. package/Server/Views/Partials/AnalyticsConsent.ejs +159 -102
  42. package/Server/Views/Partials/Head.ejs +2 -1
  43. package/Tests/App/Dashboard/AdminUserNotificationMethodsPage.test.tsx +776 -38
  44. package/Tests/App/Dashboard/CorrelateFilterBuilder.test.tsx +359 -0
  45. package/Tests/App/Dashboard/CorrelateGraph.test.tsx +865 -0
  46. package/Tests/App/Dashboard/CorrelationGraph.test.ts +346 -0
  47. package/Tests/App/Dashboard/CriteriaFilterConditionDropdown.test.tsx +256 -0
  48. package/Tests/App/Dashboard/CriteriaFilterDefaults.test.ts +738 -0
  49. package/Tests/App/Dashboard/CriteriaFilterMonitorTypeChange.test.ts +1122 -0
  50. package/Tests/App/Dashboard/DiscoveryScanWizardValidation.test.tsx +283 -0
  51. package/Tests/App/Dashboard/ExternalStatusPageCriteriaFilter.test.ts +407 -0
  52. package/Tests/App/Dashboard/LlmCallsTableIdentity.test.tsx +322 -0
  53. package/Tests/App/Dashboard/LlmOverview.test.tsx +335 -0
  54. package/Tests/App/Dashboard/LlmSpanDisplay.test.ts +391 -0
  55. package/Tests/App/Dashboard/LlmUsageBreakdown.test.tsx +1007 -0
  56. package/Tests/App/Dashboard/MetricSeriesInvestigateMenu.test.tsx +5 -14
  57. package/Tests/App/Dashboard/MonitorCriteriaActionToggles.test.tsx +426 -0
  58. package/Tests/App/Dashboard/MonitorStepsCriteriaAlignmentWiring.test.tsx +352 -0
  59. package/Tests/App/Dashboard/OnCallLayerCardOverrides.test.tsx +278 -0
  60. package/Tests/App/Dashboard/OnCallLayerShiftPreviewOverrides.test.ts +237 -0
  61. package/Tests/App/Dashboard/OnCallReadinessSurfaces.test.tsx +2 -2
  62. package/Tests/App/Dashboard/OnCallSchedulePreviewOverrides.test.tsx +460 -0
  63. package/Tests/App/Dashboard/SecurityEventAttributeUtil.test.ts +171 -0
  64. package/Tests/App/Dashboard/SecurityEventCorrelation.test.ts +857 -0
  65. package/Tests/App/Dashboard/SecurityEventDetailObservables.test.tsx +194 -0
  66. package/Tests/App/Dashboard/SecurityEventsDetectionRulesPage.test.tsx +124 -0
  67. package/Tests/App/Dashboard/SecurityEventsMonitorStepForm.test.tsx +79 -19
  68. package/Tests/App/Dashboard/SecurityEventsTableColumns.test.tsx +382 -0
  69. package/Tests/App/StatusPage/BulkAddStatusPageMonitors.test.ts +216 -0
  70. package/Tests/App/StatusPage/BulkAddStatusPageMonitorsModal.test.tsx +590 -11
  71. package/Tests/App/StatusPage/StatusPageResourceExplorer.test.tsx +757 -0
  72. package/Tests/Models/DiscoveryScanNameColumn.test.ts +293 -0
  73. package/Tests/Server/API/BaseAPI.test.ts +41 -0
  74. package/Tests/Server/API/BaseAPIUpdatePayloadValidation.test.ts +9 -16
  75. package/Tests/Server/API/SecurityEventAttributesAPI.test.ts +564 -0
  76. package/Tests/Server/Infrastructure/Postgres/SchemaMigrationsOrdering.test.ts +120 -4
  77. package/Tests/Server/Middleware/GzipRequestBody.test.ts +725 -0
  78. package/Tests/Server/Middleware/MultipartFormData.test.ts +298 -0
  79. package/Tests/Server/Middleware/SlackAuthorization.test.ts +262 -5
  80. package/Tests/Server/Services/AddDetectionRuleDistinctCountColumnsMigration.test.ts +311 -0
  81. package/Tests/Server/Services/DetectionRuleService.test.ts +134 -2
  82. package/Tests/Server/Services/DiscoveryScanClaimHookFreeSafety.test.ts +7 -1
  83. package/Tests/Server/Services/FeedRetentionConsistency.test.ts +131 -0
  84. package/Tests/Server/Services/NetworkDeviceDiscoveryScanService.test.ts +194 -0
  85. package/Tests/Server/Services/NetworkDeviceMonitorStatusStamp.test.ts +800 -0
  86. package/Tests/Server/Services/OnCallScheduleAttachRosterRefresh.test.ts +244 -0
  87. package/Tests/Server/Services/SecurityEventService.test.ts +376 -0
  88. package/Tests/Server/Services/StatusPageResourceDuplicates.test.ts +703 -0
  89. package/Tests/Server/Services/TelemetryAttributeServiceSecurityEvent.test.ts +256 -0
  90. package/Tests/Server/Types/Workflow/Components/ComponentRegistryParity.test.ts +417 -0
  91. package/Tests/Server/Utils/AnalyticsDatabase/StatementGeneratorWhereOperators.test.ts +677 -0
  92. package/Tests/Server/Utils/Marketing/MarketingEventUtil.test.ts +43 -0
  93. package/Tests/Server/Utils/Monitor/Criteria/ProfileMonitorCriteria.test.ts +15 -11
  94. package/Tests/Server/Utils/SecurityEvent/DetectionRuleEvaluator.test.ts +369 -8
  95. package/Tests/Server/Utils/SecurityEvent/SigmaClickhouseCompiler.test.ts +35 -0
  96. package/Tests/Server/Utils/Telemetry/LlmCostBudgetEvaluator.test.ts +387 -4
  97. package/Tests/Server/Utils/Telemetry/LlmMetricSpend.test.ts +386 -0
  98. package/Tests/Server/Utils/Telemetry/LlmSpan.test.ts +804 -0
  99. package/Tests/Server/Views/AnalyticsConsentPartial.test.ts +48 -5
  100. package/Tests/Types/Code/YamlSyntax.test.ts +396 -0
  101. package/Tests/Types/Monitor/MetricAndProfileDefaultCriteria.test.ts +957 -0
  102. package/Tests/Types/Monitor/MonitorCriteriaActionValidation.test.ts +598 -0
  103. package/Tests/Types/Monitor/MonitorCriteriaInstance.test.ts +49 -0
  104. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationNotificationMode.test.ts +6 -6
  105. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationSeverityMapper.test.ts +4 -4
  106. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationUtil.test.ts +4 -3
  107. package/Tests/Types/Telemetry/LlmMetricConventions.test.ts +584 -0
  108. package/Tests/UI/Components/AiInvestigationSettingsCard.test.tsx +0 -12
  109. package/Tests/UI/Components/CardModelDetailEdit.test.tsx +1 -17
  110. package/Tests/UI/Components/CodeEditor.test.tsx +474 -10
  111. package/Tests/UI/Components/Detail/YamlField.test.tsx +105 -0
  112. package/Tests/UI/Components/EntityDropdownLabelsBulkAdd.test.tsx +319 -0
  113. package/Tests/UI/Components/Forms/FormFieldYaml.test.tsx +231 -0
  114. package/Tests/UI/Components/Forms/Utils/FormFieldSchemaTypeUtil.test.ts +1 -0
  115. package/Tests/UI/Components/Forms/ValidationYAML.test.ts +234 -0
  116. package/Tests/UI/Components/ModelTable/AttributeColumns.test.tsx +573 -0
  117. package/Tests/UI/Components/ModelTable/BaseModelTableAttributeColumns.test.tsx +824 -0
  118. package/Tests/UI/Components/ModelTable/ColumnCustomizationModalAddableColumns.test.tsx +707 -0
  119. package/Tests/UI/Components/SideOver.test.tsx +1 -1
  120. package/Tests/UI/Components/StatusPage/ResourceGridSelection.test.tsx +294 -0
  121. package/Tests/UI/Components/StatusPage/ResourceListSelection.test.tsx +436 -0
  122. package/Tests/UI/Components/TableBulkCsvExport.test.tsx +6 -2
  123. package/Tests/UI/Components/Toggle.test.tsx +25 -0
  124. package/Tests/UI/Components/YamlEditor.test.tsx +847 -0
  125. package/Tests/Utils/NetworkDevice/DeviceReachabilityUtil.test.ts +252 -0
  126. package/Tests/Utils/NetworkDiscovery/DiscoveredHostUtil.test.ts +221 -0
  127. package/Tests/Utils/NetworkDiscovery/ScanNameUtil.test.ts +312 -0
  128. package/Tests/Utils/StatusPage/ResourceExplorer.test.ts +468 -0
  129. package/Tests/Utils/Telemetry/LlmMetricQuery.test.ts +770 -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/SecurityEvent/DetectionFindingConstants.ts +11 -0
  138. package/Types/Telemetry/LlmConventions.ts +255 -0
  139. package/Types/Telemetry/LlmMetricConventions.ts +329 -0
  140. package/UI/Components/AutocompleteTextInput/AutocompleteTextInput.tsx +3 -0
  141. package/UI/Components/BulkUpdate/BulkUpdateForm.tsx +10 -1
  142. package/UI/Components/CodeEditor/CodeEditor.tsx +241 -79
  143. package/UI/Components/CodeEditor/YamlEditor.tsx +369 -0
  144. package/UI/Components/Detail/Detail.tsx +10 -0
  145. package/UI/Components/EntityDropdown/EntityDropdown.tsx +33 -0
  146. package/UI/Components/Forms/Fields/FormField.tsx +31 -0
  147. package/UI/Components/Forms/Types/Field.ts +8 -0
  148. package/UI/Components/Forms/Types/FormFieldSchemaType.ts +1 -0
  149. package/UI/Components/Forms/Utils/FormFieldSchemaTypeUtil.ts +2 -0
  150. package/UI/Components/Forms/Validation.ts +46 -0
  151. package/UI/Components/Input/Input.tsx +3 -0
  152. package/UI/Components/ModelTable/AttributeColumns.tsx +321 -0
  153. package/UI/Components/ModelTable/BaseModelTable.tsx +263 -14
  154. package/UI/Components/ModelTable/Column.ts +8 -0
  155. package/UI/Components/ModelTable/ColumnCustomizationModal.tsx +310 -0
  156. package/UI/Components/ModelTable/ColumnPreference.ts +13 -1
  157. package/UI/Components/MonitorTemplateVariables/TemplateVariablesModal.tsx +2 -2
  158. package/UI/Components/SideOver/SideOver.tsx +5 -5
  159. package/UI/Components/StatusPage/ResourceGrid.tsx +82 -2
  160. package/UI/Components/StatusPage/ResourceList.tsx +197 -38
  161. package/UI/Components/Toggle/Toggle.tsx +9 -1
  162. package/UI/Components/Types/FieldType.ts +1 -0
  163. package/Utils/NetworkDevice/DeviceReachabilityUtil.ts +73 -1
  164. package/Utils/NetworkDiscovery/ScanNameUtil.ts +181 -0
  165. package/Utils/StatusPage/ResourceExplorer.ts +258 -0
  166. package/Utils/Telemetry/LlmMetricQuery.ts +287 -0
  167. package/build/dist/Models/AnalyticsModels/Span.js +89 -0
  168. package/build/dist/Models/AnalyticsModels/Span.js.map +1 -1
  169. package/build/dist/Models/DatabaseModels/DetectionRule.js +57 -0
  170. package/build/dist/Models/DatabaseModels/DetectionRule.js.map +1 -1
  171. package/build/dist/Models/DatabaseModels/NetworkDeviceAutoImportRule.js +27 -7
  172. package/build/dist/Models/DatabaseModels/NetworkDeviceAutoImportRule.js.map +1 -1
  173. package/build/dist/Models/DatabaseModels/NetworkDeviceDiscoveryScan.js +69 -0
  174. package/build/dist/Models/DatabaseModels/NetworkDeviceDiscoveryScan.js.map +1 -1
  175. package/build/dist/Models/DatabaseModels/Project.js +76 -0
  176. package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
  177. package/build/dist/Models/DatabaseModels/User.js +76 -0
  178. package/build/dist/Models/DatabaseModels/User.js.map +1 -1
  179. package/build/dist/Server/API/BaseAPI.js +3 -19
  180. package/build/dist/Server/API/BaseAPI.js.map +1 -1
  181. package/build/dist/Server/API/SlackAPI.js +0 -2
  182. package/build/dist/Server/API/SlackAPI.js.map +1 -1
  183. package/build/dist/Server/API/TelemetryAPI.js +17 -0
  184. package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
  185. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789400000000-AddNameToNetworkDeviceDiscoveryScan.js +21 -0
  186. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789400000000-AddNameToNetworkDeviceDiscoveryScan.js.map +1 -0
  187. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789500000000-AddCampaignUtmFields.js +42 -0
  188. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789500000000-AddCampaignUtmFields.js.map +1 -0
  189. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789512000000-AddDetectionRuleDistinctCountColumns.js +22 -0
  190. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789512000000-AddDetectionRuleDistinctCountColumns.js.map +1 -0
  191. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +6 -0
  192. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  193. package/build/dist/Server/Middleware/GzipRequestBody.js +224 -0
  194. package/build/dist/Server/Middleware/GzipRequestBody.js.map +1 -0
  195. package/build/dist/Server/Middleware/MultipartFormData.js +71 -2
  196. package/build/dist/Server/Middleware/MultipartFormData.js.map +1 -1
  197. package/build/dist/Server/Middleware/SlackAuthorization.js +58 -7
  198. package/build/dist/Server/Middleware/SlackAuthorization.js.map +1 -1
  199. package/build/dist/Server/Services/DetectionRuleService.js +49 -4
  200. package/build/dist/Server/Services/DetectionRuleService.js.map +1 -1
  201. package/build/dist/Server/Services/Index.js +204 -0
  202. package/build/dist/Server/Services/Index.js.map +1 -1
  203. package/build/dist/Server/Services/NetworkDeviceDiscoveryScanService.js +50 -0
  204. package/build/dist/Server/Services/NetworkDeviceDiscoveryScanService.js.map +1 -1
  205. package/build/dist/Server/Services/NetworkDeviceService.js +124 -0
  206. package/build/dist/Server/Services/NetworkDeviceService.js.map +1 -1
  207. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js +68 -13
  208. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js.map +1 -1
  209. package/build/dist/Server/Services/OnCallDutyPolicyFeedService.js +1 -1
  210. package/build/dist/Server/Services/OnCallDutyPolicyFeedService.js.map +1 -1
  211. package/build/dist/Server/Services/ProjectService.js +25 -45
  212. package/build/dist/Server/Services/ProjectService.js.map +1 -1
  213. package/build/dist/Server/Services/SecurityEventService.js +57 -5
  214. package/build/dist/Server/Services/SecurityEventService.js.map +1 -1
  215. package/build/dist/Server/Services/StatusPageResourceService.js +140 -0
  216. package/build/dist/Server/Services/StatusPageResourceService.js.map +1 -1
  217. package/build/dist/Server/Services/TelemetryAttributeService.js +17 -0
  218. package/build/dist/Server/Services/TelemetryAttributeService.js.map +1 -1
  219. package/build/dist/Server/Services/UserService.js +2 -12
  220. package/build/dist/Server/Services/UserService.js.map +1 -1
  221. package/build/dist/Server/Types/Markdown.js +147 -61
  222. package/build/dist/Server/Types/Markdown.js.map +1 -1
  223. package/build/dist/Server/Utils/AI/Toolbox/OnCallTools.js +12 -4
  224. package/build/dist/Server/Utils/AI/Toolbox/OnCallTools.js.map +1 -1
  225. package/build/dist/Server/Utils/AnalyticsDatabase/Statement.js +12 -1
  226. package/build/dist/Server/Utils/AnalyticsDatabase/Statement.js.map +1 -1
  227. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +246 -9
  228. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js.map +1 -1
  229. package/build/dist/Server/Utils/Attribution.js.map +1 -1
  230. package/build/dist/Server/Utils/Marketing/MarketingEventUtil.js +27 -7
  231. package/build/dist/Server/Utils/Marketing/MarketingEventUtil.js.map +1 -1
  232. package/build/dist/Server/Utils/SecurityEvent/DetectionRuleEvaluator.js +42 -7
  233. package/build/dist/Server/Utils/SecurityEvent/DetectionRuleEvaluator.js.map +1 -1
  234. package/build/dist/Server/Utils/SecurityEvent/Sigma/SigmaClickhouseCompiler.js +18 -0
  235. package/build/dist/Server/Utils/SecurityEvent/Sigma/SigmaClickhouseCompiler.js.map +1 -1
  236. package/build/dist/Server/Utils/StartServer.js +12 -21
  237. package/build/dist/Server/Utils/StartServer.js.map +1 -1
  238. package/build/dist/Server/Utils/Telemetry/LlmCostBudgetEvaluator.js +76 -5
  239. package/build/dist/Server/Utils/Telemetry/LlmCostBudgetEvaluator.js.map +1 -1
  240. package/build/dist/Server/Utils/Telemetry/LlmMetricSpend.js +151 -0
  241. package/build/dist/Server/Utils/Telemetry/LlmMetricSpend.js.map +1 -0
  242. package/build/dist/Server/Utils/Telemetry/LlmSpan.js +24 -1
  243. package/build/dist/Server/Utils/Telemetry/LlmSpan.js.map +1 -1
  244. package/build/dist/Server/Utils/Workspace/Slack/Actions/Auth.js +0 -10
  245. package/build/dist/Server/Utils/Workspace/Slack/Actions/Auth.js.map +1 -1
  246. package/build/dist/Types/Code/YamlSyntax.js +212 -0
  247. package/build/dist/Types/Code/YamlSyntax.js.map +1 -0
  248. package/build/dist/Types/Exception/ExceptionCode.js +1 -0
  249. package/build/dist/Types/Exception/ExceptionCode.js.map +1 -1
  250. package/build/dist/Types/Exception/PayloadTooLargeException.js +8 -0
  251. package/build/dist/Types/Exception/PayloadTooLargeException.js.map +1 -0
  252. package/build/dist/Types/Marketing/Attribution.js +10 -0
  253. package/build/dist/Types/Marketing/Attribution.js.map +1 -1
  254. package/build/dist/Types/Monitor/MonitorCriteriaInstance.js +112 -51
  255. package/build/dist/Types/Monitor/MonitorCriteriaInstance.js.map +1 -1
  256. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationSeverityMapper.js +2 -1
  257. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationSeverityMapper.js.map +1 -1
  258. package/build/dist/Types/SecurityEvent/DetectionFindingConstants.js +9 -0
  259. package/build/dist/Types/SecurityEvent/DetectionFindingConstants.js.map +1 -1
  260. package/build/dist/Types/Telemetry/LlmConventions.js +236 -0
  261. package/build/dist/Types/Telemetry/LlmConventions.js.map +1 -1
  262. package/build/dist/Types/Telemetry/LlmMetricConventions.js +290 -0
  263. package/build/dist/Types/Telemetry/LlmMetricConventions.js.map +1 -0
  264. package/build/dist/UI/Components/AutocompleteTextInput/AutocompleteTextInput.js +1 -1
  265. package/build/dist/UI/Components/AutocompleteTextInput/AutocompleteTextInput.js.map +1 -1
  266. package/build/dist/UI/Components/BulkUpdate/BulkUpdateForm.js +5 -2
  267. package/build/dist/UI/Components/BulkUpdate/BulkUpdateForm.js.map +1 -1
  268. package/build/dist/UI/Components/CodeEditor/CodeEditor.js +206 -76
  269. package/build/dist/UI/Components/CodeEditor/CodeEditor.js.map +1 -1
  270. package/build/dist/UI/Components/CodeEditor/YamlEditor.js +201 -0
  271. package/build/dist/UI/Components/CodeEditor/YamlEditor.js.map +1 -0
  272. package/build/dist/UI/Components/Detail/Detail.js +9 -0
  273. package/build/dist/UI/Components/Detail/Detail.js.map +1 -1
  274. package/build/dist/UI/Components/EntityDropdown/EntityDropdown.js +16 -1
  275. package/build/dist/UI/Components/EntityDropdown/EntityDropdown.js.map +1 -1
  276. package/build/dist/UI/Components/Forms/Fields/FormField.js +14 -1
  277. package/build/dist/UI/Components/Forms/Fields/FormField.js.map +1 -1
  278. package/build/dist/UI/Components/Forms/Types/Field.js.map +1 -1
  279. package/build/dist/UI/Components/Forms/Types/FormFieldSchemaType.js +1 -0
  280. package/build/dist/UI/Components/Forms/Types/FormFieldSchemaType.js.map +1 -1
  281. package/build/dist/UI/Components/Forms/Utils/FormFieldSchemaTypeUtil.js +2 -0
  282. package/build/dist/UI/Components/Forms/Utils/FormFieldSchemaTypeUtil.js.map +1 -1
  283. package/build/dist/UI/Components/Forms/Validation.js +27 -0
  284. package/build/dist/UI/Components/Forms/Validation.js.map +1 -1
  285. package/build/dist/UI/Components/Input/Input.js +1 -1
  286. package/build/dist/UI/Components/Input/Input.js.map +1 -1
  287. package/build/dist/UI/Components/ModelTable/AttributeColumns.js +159 -0
  288. package/build/dist/UI/Components/ModelTable/AttributeColumns.js.map +1 -0
  289. package/build/dist/UI/Components/ModelTable/BaseModelTable.js +179 -17
  290. package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
  291. package/build/dist/UI/Components/ModelTable/ColumnCustomizationModal.js +133 -2
  292. package/build/dist/UI/Components/ModelTable/ColumnCustomizationModal.js.map +1 -1
  293. package/build/dist/UI/Components/ModelTable/ColumnPreference.js +7 -1
  294. package/build/dist/UI/Components/ModelTable/ColumnPreference.js.map +1 -1
  295. package/build/dist/UI/Components/MonitorTemplateVariables/TemplateVariablesModal.js +2 -2
  296. package/build/dist/UI/Components/SideOver/SideOver.js +5 -5
  297. package/build/dist/UI/Components/StatusPage/ResourceGrid.js +28 -2
  298. package/build/dist/UI/Components/StatusPage/ResourceGrid.js.map +1 -1
  299. package/build/dist/UI/Components/StatusPage/ResourceList.js +87 -20
  300. package/build/dist/UI/Components/StatusPage/ResourceList.js.map +1 -1
  301. package/build/dist/UI/Components/Toggle/Toggle.js +9 -1
  302. package/build/dist/UI/Components/Toggle/Toggle.js.map +1 -1
  303. package/build/dist/UI/Components/Types/FieldType.js +1 -0
  304. package/build/dist/UI/Components/Types/FieldType.js.map +1 -1
  305. package/build/dist/Utils/NetworkDevice/DeviceReachabilityUtil.js +37 -1
  306. package/build/dist/Utils/NetworkDevice/DeviceReachabilityUtil.js.map +1 -1
  307. package/build/dist/Utils/NetworkDiscovery/ScanNameUtil.js +120 -0
  308. package/build/dist/Utils/NetworkDiscovery/ScanNameUtil.js.map +1 -0
  309. package/build/dist/Utils/StatusPage/ResourceExplorer.js +171 -0
  310. package/build/dist/Utils/StatusPage/ResourceExplorer.js.map +1 -1
  311. package/build/dist/Utils/Telemetry/LlmMetricQuery.js +175 -0
  312. package/build/dist/Utils/Telemetry/LlmMetricQuery.js.map +1 -0
  313. package/jest.config.json +2 -0
  314. package/package.json +1 -1
  315. package/tsconfig.json +2 -1
  316. package/Tests/UI/Components/Toast.test.tsx +0 -197
  317. package/Tests/UI/Components/ToastStacking.test.tsx +0 -198
  318. package/UI/Components/Toast/Toast.tsx +0 -189
  319. package/UI/Components/Toast/ToastInit.tsx +0 -110
  320. package/build/dist/UI/Components/Toast/Toast.js +0 -123
  321. package/build/dist/UI/Components/Toast/Toast.js.map +0 -1
  322. package/build/dist/UI/Components/Toast/ToastInit.js +0 -53
  323. package/build/dist/UI/Components/Toast/ToastInit.js.map +0 -1
@@ -0,0 +1,865 @@
1
+ import "@testing-library/jest-dom";
2
+ import {
3
+ cleanup,
4
+ fireEvent,
5
+ render,
6
+ screen,
7
+ waitFor,
8
+ within,
9
+ } from "@testing-library/react";
10
+ import React from "react";
11
+ import {
12
+ afterEach,
13
+ beforeEach,
14
+ describe,
15
+ expect,
16
+ jest,
17
+ test,
18
+ } from "@jest/globals";
19
+ import getJestMockFunction, { MockFunction } from "../../MockType";
20
+
21
+ /*
22
+ * Security Events → Correlate (issue #3395): the quick single-observable
23
+ * search stays as shorthand, a condition builder adds AND/OR chains, the
24
+ * applied filter shows as removable chips and lives in the URL, AND chains
25
+ * compile into ONE query, OR chains fan out and union by event id, class
26
+ * nodes drill down into their events, and observable nodes offer
27
+ * focus/add/exclude pivots. Everything the component promises the issue is
28
+ * pinned here against a mocked AnalyticsModelAPI and a recording ReactFlow
29
+ * stand-in.
30
+ */
31
+
32
+ const getListMock: MockFunction = getJestMockFunction();
33
+ const mockQueryParams: Record<string, string | null> = {};
34
+ const setQueryStringMock: MockFunction = getJestMockFunction();
35
+ const navigateMock: MockFunction = getJestMockFunction();
36
+
37
+ const PROJECT_ID_STRING: string = "11111111-1111-4111-8111-111111111111";
38
+
39
+ /*
40
+ * The arrow wrappers are load bearing: jest.mock is hoisted above the
41
+ * compiled requires, so naming the mocks directly would capture them before
42
+ * their initializers have run.
43
+ */
44
+ jest.mock("../../../UI/Utils/AnalyticsModelAPI/AnalyticsModelAPI", () => {
45
+ return {
46
+ __esModule: true,
47
+ default: {
48
+ getList: (...args: Array<any>) => {
49
+ return getListMock(...args);
50
+ },
51
+ },
52
+ };
53
+ });
54
+
55
+ jest.mock("../../../UI/Utils/Project", () => {
56
+ return {
57
+ __esModule: true,
58
+ default: {
59
+ getCurrentProjectId: () => {
60
+ return new ObjectID("11111111-1111-4111-8111-111111111111");
61
+ },
62
+ },
63
+ };
64
+ });
65
+
66
+ jest.mock("../../../UI/Utils/Navigation", () => {
67
+ return {
68
+ __esModule: true,
69
+ default: {
70
+ getQueryStringByName: (name: string) => {
71
+ return mockQueryParams[name] ?? null;
72
+ },
73
+ setQueryString: (...args: Array<any>) => {
74
+ return setQueryStringMock(...args);
75
+ },
76
+ navigate: (...args: Array<any>) => {
77
+ return navigateMock(...args);
78
+ },
79
+ },
80
+ };
81
+ });
82
+
83
+ jest.mock("react-i18next", () => {
84
+ return {
85
+ useTranslation: () => {
86
+ return {
87
+ t: (key: string): string => {
88
+ return key;
89
+ },
90
+ };
91
+ },
92
+ };
93
+ });
94
+
95
+ /*
96
+ * ReactFlow renders nothing measurable in jsdom — replace it with a stand-in
97
+ * that lists every node as a clickable button so node-click pivots are
98
+ * testable, and forwards onNodeClick exactly like the real component.
99
+ */
100
+ jest.mock("reactflow", () => {
101
+ return {
102
+ __esModule: true,
103
+ default: (props: any) => {
104
+ return React.createElement(
105
+ "div",
106
+ { "data-testid": "mock-react-flow" },
107
+ (props.nodes || []).map((node: any) => {
108
+ return React.createElement(
109
+ "button",
110
+ {
111
+ key: node.id,
112
+ type: "button",
113
+ "data-testid": `flow-node-${node.id}`,
114
+ onClick: (event: any) => {
115
+ if (props.onNodeClick) {
116
+ props.onNodeClick(event, node);
117
+ }
118
+ },
119
+ },
120
+ typeof node.data?.label === "string" ? node.data.label : node.id,
121
+ );
122
+ }),
123
+ );
124
+ },
125
+ Background: () => {
126
+ return null;
127
+ },
128
+ Controls: () => {
129
+ return null;
130
+ },
131
+ BackgroundVariant: { Dots: "dots" },
132
+ MarkerType: { ArrowClosed: "arrowclosed" },
133
+ };
134
+ });
135
+
136
+ const detailPivotObservable: string = "pivot-observable";
137
+
138
+ jest.mock(
139
+ "../../../../App/FeatureSet/Dashboard/src/Components/SecurityEvents/SecurityEventDetail",
140
+ () => {
141
+ return {
142
+ __esModule: true,
143
+ default: (props: any) => {
144
+ return React.createElement(
145
+ "div",
146
+ { "data-testid": "mock-event-detail" },
147
+ React.createElement(
148
+ "span",
149
+ { "data-testid": "mock-event-detail-message" },
150
+ props.securityEvent?.message || "",
151
+ ),
152
+ React.createElement(
153
+ "button",
154
+ {
155
+ type: "button",
156
+ "data-testid": "mock-event-detail-pivot",
157
+ onClick: () => {
158
+ if (props.onCorrelateObservable) {
159
+ props.onCorrelateObservable("pivot-observable");
160
+ }
161
+ },
162
+ },
163
+ "pivot",
164
+ ),
165
+ );
166
+ },
167
+ };
168
+ },
169
+ );
170
+
171
+ /*
172
+ * The component import comes LAST: requiring it is what instantiates the
173
+ * mock factories above, and those factories reference these imports (e.g.
174
+ * ObjectID) — so everything else must be initialized first.
175
+ */
176
+ import SecurityEvent from "../../../Models/AnalyticsModels/SecurityEvent";
177
+ import ObjectID from "../../../Types/ObjectID";
178
+ import OcsfSeverity from "../../../Types/SecurityEvent/OcsfSeverity";
179
+ import InBetween from "../../../Types/BaseDatabase/InBetween";
180
+ import Includes from "../../../Types/BaseDatabase/Includes";
181
+ import IncludesNone from "../../../Types/BaseDatabase/IncludesNone";
182
+ import Search from "../../../Types/BaseDatabase/Search";
183
+ import {
184
+ CorrelationFilter,
185
+ serializeCorrelationFilter,
186
+ } from "../../../../App/FeatureSet/Dashboard/src/Utils/SecurityEventCorrelation";
187
+ import CorrelateGraph from "../../../../App/FeatureSet/Dashboard/src/Components/SecurityEvents/CorrelateGraph";
188
+
189
+ interface EventInput {
190
+ id: string;
191
+ className?: string;
192
+ severityName?: OcsfSeverity;
193
+ message?: string;
194
+ observables?: Array<string>;
195
+ principalUser?: string;
196
+ }
197
+
198
+ function buildEvent(input: EventInput): SecurityEvent {
199
+ const event: SecurityEvent = new SecurityEvent();
200
+ event._id = new ObjectID(input.id);
201
+ event.time = new Date("2026-08-25T10:00:00.000Z");
202
+ if (input.className) {
203
+ event.className = input.className;
204
+ }
205
+ if (input.severityName) {
206
+ event.severityName = input.severityName;
207
+ }
208
+ if (input.message) {
209
+ event.message = input.message;
210
+ }
211
+ if (input.principalUser) {
212
+ event.principalUser = input.principalUser;
213
+ }
214
+ event.observables = input.observables || [];
215
+ return event;
216
+ }
217
+
218
+ function listResult(events: Array<SecurityEvent>): {
219
+ data: Array<SecurityEvent>;
220
+ count: number;
221
+ skip: number;
222
+ limit: number;
223
+ } {
224
+ return { data: events, count: events.length, skip: 0, limit: 200 };
225
+ }
226
+
227
+ type QueryRecord = Record<string, unknown>;
228
+
229
+ function queryOfCall(callIndex: number): QueryRecord {
230
+ const callArg: { query: QueryRecord } = getListMock.mock.calls[
231
+ callIndex
232
+ ]?.[0] as { query: QueryRecord };
233
+ return callArg.query;
234
+ }
235
+
236
+ function runQuickSearch(observable: string): void {
237
+ fireEvent.change(screen.getByTestId("security-events-correlate-observable"), {
238
+ target: { value: observable },
239
+ });
240
+ fireEvent.click(screen.getByTestId("security-events-correlate-button"));
241
+ }
242
+
243
+ beforeEach(() => {
244
+ getListMock.mockReset();
245
+ getListMock.mockResolvedValue(listResult([]));
246
+ setQueryStringMock.mockReset();
247
+ navigateMock.mockReset();
248
+ for (const key of Object.keys(mockQueryParams)) {
249
+ delete mockQueryParams[key];
250
+ }
251
+ });
252
+
253
+ afterEach(() => {
254
+ cleanup();
255
+ });
256
+
257
+ describe("CorrelateGraph", () => {
258
+ test("starts on the empty state without fetching", () => {
259
+ render(<CorrelateGraph />);
260
+ expect(screen.getByText("Correlate security events")).toBeInTheDocument();
261
+ expect(getListMock).not.toHaveBeenCalled();
262
+ });
263
+
264
+ test("quick search compiles to ONE hasAny query with project and time scope", async () => {
265
+ getListMock.mockResolvedValue(
266
+ listResult([
267
+ buildEvent({
268
+ id: "22222222-2222-4222-8222-222222222222",
269
+ className: "Authentication",
270
+ observables: ["wb-ubuntu-03", "alice"],
271
+ }),
272
+ buildEvent({
273
+ id: "33333333-3333-4333-8333-333333333333",
274
+ className: "Process Activity",
275
+ observables: ["wb-ubuntu-03"],
276
+ }),
277
+ ]),
278
+ );
279
+
280
+ render(<CorrelateGraph />);
281
+ runQuickSearch("wb-ubuntu-03");
282
+
283
+ await waitFor(() => {
284
+ expect(getListMock).toHaveBeenCalledTimes(1);
285
+ });
286
+
287
+ const query: QueryRecord = queryOfCall(0);
288
+ expect((query["projectId"] as ObjectID).toString()).toBe(PROJECT_ID_STRING);
289
+ expect(query["time"]).toBeInstanceOf(InBetween);
290
+ expect(query["observables"]).toBeInstanceOf(Includes);
291
+ expect((query["observables"] as Includes).values).toEqual(["wb-ubuntu-03"]);
292
+
293
+ const callArg: { limit: number; sort: Record<string, unknown> } =
294
+ getListMock.mock.calls[0]?.[0] as {
295
+ limit: number;
296
+ sort: Record<string, unknown>;
297
+ };
298
+ expect(callArg.limit).toBe(200);
299
+
300
+ await waitFor(() => {
301
+ expect(screen.getByText(/2 matching events\./)).toBeInTheDocument();
302
+ });
303
+
304
+ // The applied filter shows as a chip and lands in the URL.
305
+ expect(screen.getByTestId("correlate-filter-chip-0")).toHaveTextContent(
306
+ "wb-ubuntu-03",
307
+ );
308
+ expect(setQueryStringMock).toHaveBeenCalledWith(
309
+ expect.objectContaining({
310
+ q: expect.stringContaining("wb-ubuntu-03"),
311
+ observable: null,
312
+ }),
313
+ );
314
+
315
+ // Class nodes made it into the graph.
316
+ expect(
317
+ screen.getByTestId("flow-node-class:Authentication"),
318
+ ).toHaveTextContent("Authentication (1)");
319
+ });
320
+
321
+ test("a ?observable= deep link auto-correlates on mount", async () => {
322
+ mockQueryParams["observable"] = "wb-ubuntu-03";
323
+ render(<CorrelateGraph />);
324
+
325
+ await waitFor(() => {
326
+ expect(getListMock).toHaveBeenCalledTimes(1);
327
+ });
328
+ expect((queryOfCall(0)["observables"] as Includes).values).toEqual([
329
+ "wb-ubuntu-03",
330
+ ]);
331
+ });
332
+
333
+ test("a ?q= deep link restores a multi-condition AND filter into one query and opens the builder", async () => {
334
+ const filter: CorrelationFilter = {
335
+ conditions: [
336
+ { field: "principalIp", operator: "equals", value: "192.168.1.20" },
337
+ { field: "principalHost", operator: "equals", value: "wb-ubuntu-03" },
338
+ ],
339
+ connector: "and",
340
+ } as CorrelationFilter;
341
+ mockQueryParams["q"] = serializeCorrelationFilter(filter);
342
+ mockQueryParams["hours"] = "168";
343
+
344
+ render(<CorrelateGraph />);
345
+
346
+ await waitFor(() => {
347
+ expect(getListMock).toHaveBeenCalledTimes(1);
348
+ });
349
+ const query: QueryRecord = queryOfCall(0);
350
+ expect(query["principalIp"]).toBe("192.168.1.20");
351
+ expect(query["principalHost"]).toBe("wb-ubuntu-03");
352
+
353
+ // Both chips visible, builder auto-opened for the chain.
354
+ expect(screen.getByTestId("correlate-filter-chip-1")).toBeInTheDocument();
355
+ expect(screen.getByTestId("correlate-filter-builder")).toBeInTheDocument();
356
+
357
+ // The seeded time range makes it into the query window (168h ≈ 7 days).
358
+ const timeWindow: InBetween<Date> = query["time"] as InBetween<Date>;
359
+ const windowMs: number =
360
+ new Date(timeWindow.endValue).getTime() -
361
+ new Date(timeWindow.startValue).getTime();
362
+ expect(Math.round(windowMs / (1000 * 60 * 60))).toBe(168);
363
+ });
364
+
365
+ test("OR filters fan out into one query per condition and union by event id", async () => {
366
+ const filter: CorrelationFilter = {
367
+ conditions: [
368
+ { field: "observable", operator: "equals", value: "wb-ubuntu-03" },
369
+ { field: "message", operator: "contains", value: "failed" },
370
+ ],
371
+ connector: "or",
372
+ } as CorrelationFilter;
373
+ mockQueryParams["q"] = serializeCorrelationFilter(filter);
374
+
375
+ const sharedEvent: SecurityEvent = buildEvent({
376
+ id: "44444444-4444-4444-8444-444444444444",
377
+ className: "Authentication",
378
+ message: "failed password for alice",
379
+ observables: ["wb-ubuntu-03"],
380
+ });
381
+
382
+ getListMock.mockImplementation((args: any) => {
383
+ const query: QueryRecord = args.query as QueryRecord;
384
+ if (query["observables"]) {
385
+ return Promise.resolve(
386
+ listResult([
387
+ sharedEvent,
388
+ buildEvent({
389
+ id: "55555555-5555-4555-8555-555555555555",
390
+ className: "Authentication",
391
+ observables: ["wb-ubuntu-03"],
392
+ }),
393
+ ]),
394
+ );
395
+ }
396
+ return Promise.resolve(
397
+ listResult([
398
+ sharedEvent,
399
+ buildEvent({
400
+ id: "66666666-6666-4666-8666-666666666666",
401
+ className: "Detection Finding",
402
+ message: "failed logon burst",
403
+ observables: [],
404
+ }),
405
+ ]),
406
+ );
407
+ });
408
+
409
+ render(<CorrelateGraph />);
410
+
411
+ await waitFor(() => {
412
+ expect(getListMock).toHaveBeenCalledTimes(2);
413
+ });
414
+
415
+ // 2 + 2 rows with one shared id → 3 unique events.
416
+ await waitFor(() => {
417
+ expect(screen.getByText(/3 matching events\./)).toBeInTheDocument();
418
+ });
419
+
420
+ const queries: Array<QueryRecord> = [queryOfCall(0), queryOfCall(1)];
421
+ const observableQuery: QueryRecord | undefined = queries.find(
422
+ (query: QueryRecord) => {
423
+ return Boolean(query["observables"]);
424
+ },
425
+ );
426
+ const messageQuery: QueryRecord | undefined = queries.find(
427
+ (query: QueryRecord) => {
428
+ return Boolean(query["message"]);
429
+ },
430
+ );
431
+ expect(observableQuery).toBeTruthy();
432
+ expect(messageQuery?.["message"]).toBeInstanceOf(Search);
433
+ });
434
+
435
+ test("an impossible AND chain surfaces the friendly compiler error and fetches nothing", async () => {
436
+ const filter: CorrelationFilter = {
437
+ conditions: [
438
+ { field: "principalHost", operator: "equals", value: "host-a" },
439
+ { field: "principalHost", operator: "equals", value: "host-b" },
440
+ ],
441
+ connector: "and",
442
+ } as CorrelationFilter;
443
+ mockQueryParams["q"] = serializeCorrelationFilter(filter);
444
+
445
+ render(<CorrelateGraph />);
446
+
447
+ await waitFor(() => {
448
+ expect(screen.getByText(/can never match/)).toBeInTheDocument();
449
+ });
450
+ expect(getListMock).not.toHaveBeenCalled();
451
+ });
452
+
453
+ test("hitting the per-query cap marks counts as lower bounds", async () => {
454
+ const manyEvents: Array<SecurityEvent> = [];
455
+ for (let eventIndex: number = 0; eventIndex < 200; eventIndex++) {
456
+ manyEvents.push(
457
+ buildEvent({
458
+ id: `77777777-7777-4777-8777-${String(eventIndex).padStart(12, "0")}`,
459
+ className: "Authentication",
460
+ observables: ["wb-ubuntu-03"],
461
+ }),
462
+ );
463
+ }
464
+ getListMock.mockResolvedValue(listResult(manyEvents));
465
+
466
+ render(<CorrelateGraph />);
467
+ runQuickSearch("wb-ubuntu-03");
468
+
469
+ await waitFor(() => {
470
+ expect(
471
+ screen.getByText(/treat counts as lower bounds/),
472
+ ).toBeInTheDocument();
473
+ });
474
+ });
475
+
476
+ test("removing a chip re-correlates with the remaining conditions", async () => {
477
+ const filter: CorrelationFilter = {
478
+ conditions: [
479
+ { field: "principalIp", operator: "equals", value: "192.168.1.20" },
480
+ { field: "principalHost", operator: "equals", value: "wb-ubuntu-03" },
481
+ ],
482
+ connector: "and",
483
+ } as CorrelationFilter;
484
+ mockQueryParams["q"] = serializeCorrelationFilter(filter);
485
+
486
+ render(<CorrelateGraph />);
487
+ await waitFor(() => {
488
+ expect(getListMock).toHaveBeenCalledTimes(1);
489
+ });
490
+
491
+ fireEvent.click(screen.getByTestId("correlate-filter-chip-remove-0"));
492
+
493
+ await waitFor(() => {
494
+ expect(getListMock).toHaveBeenCalledTimes(2);
495
+ });
496
+ const query: QueryRecord = queryOfCall(1);
497
+ expect(query["principalIp"]).toBeUndefined();
498
+ expect(query["principalHost"]).toBe("wb-ubuntu-03");
499
+ expect(screen.queryByTestId("correlate-filter-chip-1")).toBeNull();
500
+ });
501
+
502
+ test("clearing every condition returns to the empty state and clears the URL", async () => {
503
+ const filter: CorrelationFilter = {
504
+ conditions: [
505
+ { field: "observable", operator: "equals", value: "wb-ubuntu-03" },
506
+ { field: "observable", operator: "equals", value: "192.168.1.20" },
507
+ ],
508
+ connector: "or",
509
+ } as CorrelationFilter;
510
+ mockQueryParams["q"] = serializeCorrelationFilter(filter);
511
+
512
+ render(<CorrelateGraph />);
513
+ await waitFor(() => {
514
+ expect(getListMock).toHaveBeenCalled();
515
+ });
516
+
517
+ fireEvent.click(screen.getByTestId("correlate-filter-clear-all"));
518
+
519
+ await waitFor(() => {
520
+ expect(screen.getByText("Correlate security events")).toBeInTheDocument();
521
+ });
522
+ expect(setQueryStringMock).toHaveBeenLastCalledWith(
523
+ expect.objectContaining({ q: null, hours: null }),
524
+ );
525
+ });
526
+
527
+ test("clicking a class node opens the drill-down and a row opens the event detail; the detail pivot re-correlates", async () => {
528
+ getListMock.mockResolvedValue(
529
+ listResult([
530
+ buildEvent({
531
+ id: "88888888-8888-4888-8888-888888888888",
532
+ className: "Authentication",
533
+ severityName: OcsfSeverity.High,
534
+ message: "failed password for alice",
535
+ principalUser: "alice",
536
+ observables: ["wb-ubuntu-03", "alice"],
537
+ }),
538
+ buildEvent({
539
+ id: "99999999-9999-4999-8999-999999999999",
540
+ className: "Process Activity",
541
+ message: "suspicious process",
542
+ observables: ["wb-ubuntu-03"],
543
+ }),
544
+ ]),
545
+ );
546
+
547
+ render(<CorrelateGraph />);
548
+ runQuickSearch("wb-ubuntu-03");
549
+
550
+ await waitFor(() => {
551
+ expect(
552
+ screen.getByTestId("flow-node-class:Authentication"),
553
+ ).toBeInTheDocument();
554
+ });
555
+
556
+ fireEvent.click(screen.getByTestId("flow-node-class:Authentication"));
557
+
558
+ const drillDown: HTMLElement = screen.getByTestId("correlate-drilldown");
559
+ expect(within(drillDown).getByText(/1 matching event/)).toBeInTheDocument();
560
+ expect(
561
+ within(drillDown).getByText("failed password for alice"),
562
+ ).toBeInTheDocument();
563
+ // Only Authentication events — not the Process Activity row.
564
+ expect(within(drillDown).queryByText("suspicious process")).toBeNull();
565
+
566
+ fireEvent.click(screen.getByTestId("correlate-drilldown-event-0"));
567
+ expect(screen.getByTestId("mock-event-detail")).toBeInTheDocument();
568
+ expect(screen.getByTestId("mock-event-detail-message")).toHaveTextContent(
569
+ "failed password for alice",
570
+ );
571
+
572
+ fireEvent.click(screen.getByTestId("mock-event-detail-pivot"));
573
+ await waitFor(() => {
574
+ expect(getListMock).toHaveBeenCalledTimes(2);
575
+ });
576
+ expect((queryOfCall(1)["observables"] as Includes).values).toEqual([
577
+ detailPivotObservable,
578
+ ]);
579
+ });
580
+
581
+ test("clicking an observable node offers Focus / Add / Exclude pivots", async () => {
582
+ getListMock.mockResolvedValue(
583
+ listResult([
584
+ buildEvent({
585
+ id: "22222222-2222-4222-8222-222222222222",
586
+ className: "Authentication",
587
+ observables: ["wb-ubuntu-03", "alice"],
588
+ }),
589
+ ]),
590
+ );
591
+
592
+ render(<CorrelateGraph />);
593
+ runQuickSearch("wb-ubuntu-03");
594
+
595
+ await waitFor(() => {
596
+ expect(
597
+ screen.getByTestId("flow-node-observable:alice"),
598
+ ).toBeInTheDocument();
599
+ });
600
+
601
+ fireEvent.click(screen.getByTestId("flow-node-observable:alice"));
602
+ const actionBar: HTMLElement = screen.getByTestId(
603
+ "correlate-observable-actions",
604
+ );
605
+ expect(within(actionBar).getByText("alice")).toBeInTheDocument();
606
+
607
+ // Exclude appends "Observable is not alice" alongside the equality.
608
+ fireEvent.click(screen.getByTestId("correlate-action-exclude"));
609
+ await waitFor(() => {
610
+ expect(getListMock).toHaveBeenCalledTimes(2);
611
+ });
612
+ const operators: Array<unknown> = queryOfCall(1)[
613
+ "observables"
614
+ ] as Array<unknown>;
615
+ expect(Array.isArray(operators)).toBe(true);
616
+ expect(operators[0]).toBeInstanceOf(Includes);
617
+ expect((operators[0] as Includes).values).toEqual(["wb-ubuntu-03"]);
618
+ expect(operators[1]).toBeInstanceOf(IncludesNone);
619
+ expect((operators[1] as IncludesNone).values).toEqual(["alice"]);
620
+ });
621
+
622
+ test("Focus replaces the whole filter with the selected observable", async () => {
623
+ getListMock.mockResolvedValue(
624
+ listResult([
625
+ buildEvent({
626
+ id: "22222222-2222-4222-8222-222222222222",
627
+ className: "Authentication",
628
+ observables: ["wb-ubuntu-03", "alice"],
629
+ }),
630
+ ]),
631
+ );
632
+
633
+ render(<CorrelateGraph />);
634
+ runQuickSearch("wb-ubuntu-03");
635
+
636
+ await waitFor(() => {
637
+ expect(
638
+ screen.getByTestId("flow-node-observable:alice"),
639
+ ).toBeInTheDocument();
640
+ });
641
+
642
+ fireEvent.click(screen.getByTestId("flow-node-observable:alice"));
643
+ fireEvent.click(screen.getByTestId("correlate-action-focus"));
644
+
645
+ await waitFor(() => {
646
+ expect(getListMock).toHaveBeenCalledTimes(2);
647
+ });
648
+ expect((queryOfCall(1)["observables"] as Includes).values).toEqual([
649
+ "alice",
650
+ ]);
651
+ // The quick input follows a single-observable filter.
652
+ expect(
653
+ screen.getByTestId("security-events-correlate-observable"),
654
+ ).toHaveValue("alice");
655
+ });
656
+
657
+ test("searched observables never appear as co-occurring nodes", async () => {
658
+ getListMock.mockResolvedValue(
659
+ listResult([
660
+ buildEvent({
661
+ id: "22222222-2222-4222-8222-222222222222",
662
+ className: "Authentication",
663
+ observables: ["wb-ubuntu-03", "alice"],
664
+ }),
665
+ ]),
666
+ );
667
+
668
+ render(<CorrelateGraph />);
669
+ runQuickSearch("wb-ubuntu-03");
670
+
671
+ await waitFor(() => {
672
+ expect(
673
+ screen.getByTestId("flow-node-observable:alice"),
674
+ ).toBeInTheDocument();
675
+ });
676
+ expect(
677
+ screen.queryByTestId("flow-node-observable:wb-ubuntu-03"),
678
+ ).toBeNull();
679
+ });
680
+
681
+ test("an API failure surfaces as an error message", async () => {
682
+ getListMock.mockRejectedValue(new Error("ClickHouse is down"));
683
+
684
+ render(<CorrelateGraph />);
685
+ runQuickSearch("wb-ubuntu-03");
686
+
687
+ await waitFor(() => {
688
+ expect(screen.getByText(/ClickHouse is down/)).toBeInTheDocument();
689
+ });
690
+ });
691
+
692
+ test("changing the time range refetches with the new window and syncs the URL", async () => {
693
+ getListMock.mockResolvedValue(
694
+ listResult([
695
+ buildEvent({
696
+ id: "22222222-2222-4222-8222-222222222222",
697
+ className: "Authentication",
698
+ observables: ["wb-ubuntu-03"],
699
+ }),
700
+ ]),
701
+ );
702
+
703
+ render(<CorrelateGraph />);
704
+ runQuickSearch("wb-ubuntu-03");
705
+ await waitFor(() => {
706
+ expect(getListMock).toHaveBeenCalledTimes(1);
707
+ });
708
+
709
+ const timeRangeCombobox: HTMLElement = within(
710
+ screen.getByTestId("security-events-correlate-time-range"),
711
+ ).getByRole("combobox");
712
+ fireEvent.keyDown(timeRangeCombobox, { key: "ArrowDown" });
713
+ const option: HTMLElement = screen.getByText("Last 7 days");
714
+ fireEvent.mouseDown(option);
715
+ fireEvent.click(option);
716
+
717
+ await waitFor(() => {
718
+ expect(getListMock).toHaveBeenCalledTimes(2);
719
+ });
720
+ const timeWindow: InBetween<Date> = queryOfCall(1)[
721
+ "time"
722
+ ] as InBetween<Date>;
723
+ const windowMs: number =
724
+ new Date(timeWindow.endValue).getTime() -
725
+ new Date(timeWindow.startValue).getTime();
726
+ expect(Math.round(windowMs / (1000 * 60 * 60))).toBe(168);
727
+ expect(setQueryStringMock).toHaveBeenLastCalledWith(
728
+ expect.objectContaining({ hours: "168" }),
729
+ );
730
+ });
731
+
732
+ test.each<[string]>([["999"], ["abc"], ["-5"]])(
733
+ "an invalid ?hours=%s falls back to the 24-hour default",
734
+ async (rawHours: string) => {
735
+ mockQueryParams["observable"] = "wb-ubuntu-03";
736
+ mockQueryParams["hours"] = rawHours;
737
+
738
+ render(<CorrelateGraph />);
739
+ await waitFor(() => {
740
+ expect(getListMock).toHaveBeenCalledTimes(1);
741
+ });
742
+
743
+ const timeWindow: InBetween<Date> = queryOfCall(0)[
744
+ "time"
745
+ ] as InBetween<Date>;
746
+ const windowMs: number =
747
+ new Date(timeWindow.endValue).getTime() -
748
+ new Date(timeWindow.startValue).getTime();
749
+ expect(Math.round(windowMs / (1000 * 60 * 60))).toBe(24);
750
+ },
751
+ );
752
+
753
+ test("OR mode hides the Exclude pivot and the drill-down class filter", async () => {
754
+ const filter: CorrelationFilter = {
755
+ conditions: [
756
+ { field: "observable", operator: "equals", value: "wb-ubuntu-03" },
757
+ { field: "observable", operator: "equals", value: "192.168.1.20" },
758
+ ],
759
+ connector: "or",
760
+ } as CorrelationFilter;
761
+ mockQueryParams["q"] = serializeCorrelationFilter(filter);
762
+
763
+ getListMock.mockResolvedValue(
764
+ listResult([
765
+ buildEvent({
766
+ id: "22222222-2222-4222-8222-222222222222",
767
+ className: "Authentication",
768
+ observables: ["wb-ubuntu-03", "alice"],
769
+ }),
770
+ ]),
771
+ );
772
+
773
+ render(<CorrelateGraph />);
774
+ await waitFor(() => {
775
+ expect(
776
+ screen.getByTestId("flow-node-observable:alice"),
777
+ ).toBeInTheDocument();
778
+ });
779
+
780
+ fireEvent.click(screen.getByTestId("flow-node-observable:alice"));
781
+ expect(
782
+ screen.getByTestId("correlate-observable-actions"),
783
+ ).toBeInTheDocument();
784
+ // "is not X" only narrows under AND — the pivot hides in OR mode.
785
+ expect(screen.queryByTestId("correlate-action-exclude")).toBeNull();
786
+
787
+ fireEvent.click(screen.getByTestId("flow-node-class:Authentication"));
788
+ expect(screen.getByTestId("correlate-drilldown")).toBeInTheDocument();
789
+ expect(screen.queryByTestId("correlate-drilldown-filter-class")).toBeNull();
790
+ });
791
+
792
+ test("the drill-down's Filter-to-this-class appends a className condition in AND mode", async () => {
793
+ getListMock.mockResolvedValue(
794
+ listResult([
795
+ buildEvent({
796
+ id: "22222222-2222-4222-8222-222222222222",
797
+ className: "Authentication",
798
+ observables: ["wb-ubuntu-03"],
799
+ }),
800
+ ]),
801
+ );
802
+
803
+ render(<CorrelateGraph />);
804
+ runQuickSearch("wb-ubuntu-03");
805
+ await waitFor(() => {
806
+ expect(
807
+ screen.getByTestId("flow-node-class:Authentication"),
808
+ ).toBeInTheDocument();
809
+ });
810
+
811
+ fireEvent.click(screen.getByTestId("flow-node-class:Authentication"));
812
+ fireEvent.click(screen.getByTestId("correlate-drilldown-filter-class"));
813
+
814
+ await waitFor(() => {
815
+ expect(getListMock).toHaveBeenCalledTimes(2);
816
+ });
817
+ const query: QueryRecord = queryOfCall(1);
818
+ expect((query["observables"] as Includes).values).toEqual(["wb-ubuntu-03"]);
819
+ expect(query["className"]).toBe("Authentication");
820
+ });
821
+
822
+ test("Correlate stays disabled while a builder row has no value", () => {
823
+ render(<CorrelateGraph />);
824
+
825
+ // Open the builder — it seeds one default (empty-valued) row.
826
+ fireEvent.click(
827
+ screen.getByTestId("security-events-correlate-toggle-builder"),
828
+ );
829
+ expect(screen.getByTestId("correlate-filter-builder")).toBeInTheDocument();
830
+ expect(
831
+ screen.getByTestId("security-events-correlate-button"),
832
+ ).toBeDisabled();
833
+
834
+ fireEvent.change(screen.getByTestId("correlate-condition-value-0"), {
835
+ target: { value: "wb-ubuntu-03" },
836
+ });
837
+ expect(
838
+ screen.getByTestId("security-events-correlate-button"),
839
+ ).not.toBeDisabled();
840
+ });
841
+
842
+ test("opening the builder seeds it from freshly typed (unapplied) quick text", async () => {
843
+ getListMock.mockResolvedValue(listResult([]));
844
+
845
+ render(<CorrelateGraph />);
846
+ // Apply one search, then type something NEW without applying it.
847
+ runQuickSearch("wb-ubuntu-03");
848
+ await waitFor(() => {
849
+ expect(getListMock).toHaveBeenCalledTimes(1);
850
+ });
851
+ fireEvent.change(
852
+ screen.getByTestId("security-events-correlate-observable"),
853
+ { target: { value: "freshly-typed" } },
854
+ );
855
+
856
+ fireEvent.click(
857
+ screen.getByTestId("security-events-correlate-toggle-builder"),
858
+ );
859
+
860
+ // The new text wins over the stale draft from the applied search.
861
+ expect(screen.getByTestId("correlate-condition-value-0")).toHaveValue(
862
+ "freshly-typed",
863
+ );
864
+ });
865
+ });