@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,311 @@
1
+ import { AddDetectionRuleDistinctCountColumns1789512000000 } from "../../../Server/Infrastructure/Postgres/SchemaMigrations/1789512000000-AddDetectionRuleDistinctCountColumns";
2
+ import SchemaMigrations from "../../../Server/Infrastructure/Postgres/SchemaMigrations/Index";
3
+ import DetectionRule from "../../../Models/DatabaseModels/DetectionRule";
4
+ import ColumnLength from "../../../Types/Database/ColumnLength";
5
+ import ColumnType from "../../../Types/Database/ColumnType";
6
+ import { TableColumnMetadata } from "../../../Types/Database/TableColumn";
7
+ import TableColumnType from "../../../Types/Database/TableColumnType";
8
+ import {
9
+ MigrationInterface,
10
+ QueryRunner,
11
+ getMetadataArgsStorage,
12
+ } from "typeorm";
13
+ import type { ColumnMetadataArgs } from "typeorm/metadata-args/ColumnMetadataArgs";
14
+ import fs from "fs";
15
+ import path from "path";
16
+ import { describe, expect, test } from "@jest/globals";
17
+
18
+ /*
19
+ * The migration behind detection-rule correlation semantics (issue #3398).
20
+ *
21
+ * DetectionRule gains distinctCountField (what a rule counts: unique
22
+ * values of a field instead of raw matching events) and
23
+ * matchCountThreshold (how many of that count a group needs in one
24
+ * evaluation window before the rule fires). Three things have to hold or
25
+ * a deployed instance breaks on upgrade:
26
+ *
27
+ * 1. up() adds exactly those two columns — distinctCountField nullable
28
+ * with NO default (NULL means "count raw events", the behavior every
29
+ * existing rule was saved with), matchCountThreshold NOT NULL
30
+ * DEFAULT '1' so every existing row keeps fire-on-any-match without
31
+ * a backfill;
32
+ * 2. down() removes exactly what up() added;
33
+ * 3. the migration is registered in SchemaMigrations/Index.ts — an
34
+ * unregistered migration never runs on boot, so the evaluator's
35
+ * SELECT of the two columns would 500 on every evaluation cycle.
36
+ *
37
+ * Pure SQL-contract assertions against a fake QueryRunner. No Postgres.
38
+ * The registry-wide ordering guard lives in
39
+ * SchemaMigrationsOrdering.test.ts and is not repeated here; what is
40
+ * asserted here is about THIS migration and stays true forever.
41
+ */
42
+
43
+ const MIGRATION_DIRECTORY: string = path.join(
44
+ __dirname,
45
+ "../../../Server/Infrastructure/Postgres/SchemaMigrations",
46
+ );
47
+
48
+ const MIGRATION_TIMESTAMP: string = "1789512000000";
49
+
50
+ const MIGRATION_NAME: string = `AddDetectionRuleDistinctCountColumns${MIGRATION_TIMESTAMP}`;
51
+
52
+ type MakeQueryRunnerResult = {
53
+ runner: QueryRunner;
54
+ statements: Array<string>;
55
+ };
56
+
57
+ type MakeQueryRunnerFunction = () => MakeQueryRunnerResult;
58
+
59
+ const makeQueryRunner: MakeQueryRunnerFunction = (): MakeQueryRunnerResult => {
60
+ const statements: Array<string> = [];
61
+
62
+ const query: (...args: Array<unknown>) => Promise<undefined> = (
63
+ ...args: Array<unknown>
64
+ ): Promise<undefined> => {
65
+ statements.push(String(args[0]));
66
+ return Promise.resolve(undefined);
67
+ };
68
+
69
+ return {
70
+ runner: { query } as unknown as QueryRunner,
71
+ statements,
72
+ };
73
+ };
74
+
75
+ describe("AddDetectionRuleDistinctCountColumns1789512000000 — SQL contract", () => {
76
+ test("up() adds exactly the two columns with their exact types and defaults", async () => {
77
+ const { runner, statements } = makeQueryRunner();
78
+
79
+ await new AddDetectionRuleDistinctCountColumns1789512000000().up(runner);
80
+
81
+ expect(statements).toEqual([
82
+ `ALTER TABLE "DetectionRule" ADD "distinctCountField" character varying(100)`,
83
+ `ALTER TABLE "DetectionRule" ADD "matchCountThreshold" integer NOT NULL DEFAULT '1'`,
84
+ ]);
85
+ });
86
+
87
+ /*
88
+ * NULL is a meaning on distinctCountField: it is what the evaluator
89
+ * reads as "threshold the raw match count", i.e. the semantics every
90
+ * rule saved before this migration was created with. A default would
91
+ * silently switch existing rules to distinct counting.
92
+ */
93
+ test("distinctCountField is nullable with no default", async () => {
94
+ const { runner, statements } = makeQueryRunner();
95
+
96
+ await new AddDetectionRuleDistinctCountColumns1789512000000().up(runner);
97
+
98
+ const addDistinctCountField: string = statements.find((sql: string) => {
99
+ return sql.includes(`ADD "distinctCountField"`);
100
+ })!;
101
+
102
+ expect(addDistinctCountField).not.toContain("NOT NULL");
103
+ expect(addDistinctCountField).not.toContain("DEFAULT");
104
+ });
105
+
106
+ /*
107
+ * The opposite for matchCountThreshold: the DB default IS the
108
+ * compatibility story. Every pre-existing row gets threshold 1 —
109
+ * fire-on-any-match — the instant the column appears, with no backfill
110
+ * statement to forget.
111
+ */
112
+ test("matchCountThreshold is NOT NULL with DEFAULT '1'", async () => {
113
+ const { runner, statements } = makeQueryRunner();
114
+
115
+ await new AddDetectionRuleDistinctCountColumns1789512000000().up(runner);
116
+
117
+ const addMatchCountThreshold: string = statements.find((sql: string) => {
118
+ return sql.includes(`ADD "matchCountThreshold"`);
119
+ })!;
120
+
121
+ expect(addMatchCountThreshold).toContain("integer NOT NULL DEFAULT '1'");
122
+ });
123
+
124
+ test("down() drops exactly what up() added, in reverse order", async () => {
125
+ const { runner, statements } = makeQueryRunner();
126
+
127
+ await new AddDetectionRuleDistinctCountColumns1789512000000().down(runner);
128
+
129
+ expect(statements).toEqual([
130
+ `ALTER TABLE "DetectionRule" DROP COLUMN "matchCountThreshold"`,
131
+ `ALTER TABLE "DetectionRule" DROP COLUMN "distinctCountField"`,
132
+ ]);
133
+ });
134
+
135
+ /*
136
+ * Not just "same count": the exact column names, derived from the two
137
+ * statement sets rather than restated, so adding a column to up()
138
+ * without adding it to down() fails here.
139
+ */
140
+ test("up() and down() name the same columns", async () => {
141
+ const { runner: upRunner, statements: upStatements } = makeQueryRunner();
142
+ const { runner: downRunner, statements: downStatements } =
143
+ makeQueryRunner();
144
+
145
+ await new AddDetectionRuleDistinctCountColumns1789512000000().up(upRunner);
146
+ await new AddDetectionRuleDistinctCountColumns1789512000000().down(
147
+ downRunner,
148
+ );
149
+
150
+ const columnsIn: (
151
+ statements: Array<string>,
152
+ keyword: string,
153
+ ) => Array<string> = (
154
+ statements: Array<string>,
155
+ keyword: string,
156
+ ): Array<string> => {
157
+ return statements
158
+ .filter((sql: string) => {
159
+ return sql.includes(keyword);
160
+ })
161
+ .map((sql: string) => {
162
+ return sql.split(`${keyword} "`)[1]!.split('"')[0]!;
163
+ })
164
+ .sort();
165
+ };
166
+
167
+ expect(columnsIn(downStatements, "DROP COLUMN")).toEqual(
168
+ columnsIn(upStatements, "ADD"),
169
+ );
170
+ });
171
+ });
172
+
173
+ describe("registration", () => {
174
+ /*
175
+ * The step AGENTS.md calls out and the one that fails silently: a
176
+ * migration that is not in this array never runs, so the columns are
177
+ * missing in a real deployment while the evaluator's rule SELECT and
178
+ * the dashboard form both reference them.
179
+ */
180
+ test("the migration is registered so it runs on boot", () => {
181
+ const names: Array<string> = SchemaMigrations.map(
182
+ (migration: new () => MigrationInterface): string => {
183
+ return migration.name;
184
+ },
185
+ );
186
+
187
+ expect(names).toContain(MIGRATION_NAME);
188
+ });
189
+
190
+ test("it is registered exactly once", () => {
191
+ const occurrences: number = SchemaMigrations.filter(
192
+ (migration: new () => MigrationInterface): boolean => {
193
+ return migration.name === MIGRATION_NAME;
194
+ },
195
+ ).length;
196
+
197
+ expect(occurrences).toBe(1);
198
+ });
199
+
200
+ /*
201
+ * TypeORM records applied migrations by the `name` property, not the
202
+ * file name. A mismatch re-runs the migration on every boot.
203
+ */
204
+ test("its declared name matches its class name", () => {
205
+ expect(new AddDetectionRuleDistinctCountColumns1789512000000().name).toBe(
206
+ MIGRATION_NAME,
207
+ );
208
+ expect(AddDetectionRuleDistinctCountColumns1789512000000.name).toBe(
209
+ MIGRATION_NAME,
210
+ );
211
+ });
212
+ });
213
+
214
+ describe("identity on disk", () => {
215
+ /*
216
+ * The timestamp in the class name is the one on disk. A class renamed
217
+ * without renaming its file — or two migrations landing on the same
218
+ * timestamp — makes the registered ordering describe something that is
219
+ * not what actually runs.
220
+ */
221
+ test("exactly one file on disk carries its timestamp, and it is this one", () => {
222
+ const matching: Array<string> = fs
223
+ .readdirSync(MIGRATION_DIRECTORY)
224
+ .filter((file: string): boolean => {
225
+ return file.startsWith(`${MIGRATION_TIMESTAMP}-`);
226
+ });
227
+
228
+ expect(matching).toEqual([
229
+ `${MIGRATION_TIMESTAMP}-AddDetectionRuleDistinctCountColumns.ts`,
230
+ ]);
231
+ });
232
+ });
233
+
234
+ /*
235
+ * The model side of the same contract. TypeORM generates the schema from
236
+ * these decorators, so a column that exists in the migration but not on
237
+ * the model (or vice versa, or with different nullability/defaults) is
238
+ * exactly the drift the Postgres Schema Drift workflow fails on.
239
+ */
240
+ describe("the DetectionRule model carries the columns the migration adds", () => {
241
+ const columnArgs: (propertyName: string) => ColumnMetadataArgs = (
242
+ propertyName: string,
243
+ ): ColumnMetadataArgs => {
244
+ const args: ColumnMetadataArgs | undefined = getMetadataArgsStorage()
245
+ .columns.filter((candidate: ColumnMetadataArgs) => {
246
+ return candidate.target === DetectionRule;
247
+ })
248
+ .find((candidate: ColumnMetadataArgs) => {
249
+ return candidate.propertyName === propertyName;
250
+ });
251
+
252
+ if (!args) {
253
+ throw new Error(
254
+ `DetectionRule.${propertyName} has no TypeORM @Column metadata`,
255
+ );
256
+ }
257
+
258
+ return args;
259
+ };
260
+
261
+ test("distinctCountField is a nullable ShortText varchar(100), matching the migration", () => {
262
+ const args: ColumnMetadataArgs = columnArgs("distinctCountField");
263
+
264
+ expect(args.options.type).toBe(ColumnType.ShortText);
265
+ expect(args.options.length).toBe(ColumnLength.ShortText);
266
+ // character varying(100) in the migration.
267
+ expect(ColumnLength.ShortText).toBe(100);
268
+ expect(args.options.nullable).toBe(true);
269
+ expect(args.options.default).toBeUndefined();
270
+ });
271
+
272
+ test("matchCountThreshold is a NOT NULL number defaulting to 1, matching the migration", () => {
273
+ const args: ColumnMetadataArgs = columnArgs("matchCountThreshold");
274
+
275
+ expect(args.options.type).toBe(ColumnType.Number);
276
+ expect(args.options.nullable).toBe(false);
277
+ expect(args.options.default).toBe(1);
278
+ });
279
+
280
+ test("distinctCountField's TableColumn is an optional ShortText", () => {
281
+ const metadata: TableColumnMetadata =
282
+ new DetectionRule().getTableColumnMetadata("distinctCountField");
283
+
284
+ expect(metadata.type).toBe(TableColumnType.ShortText);
285
+ expect(metadata.required).toBe(false);
286
+ });
287
+
288
+ /*
289
+ * isDefaultValueColumn is the API-side half of the DB default: without
290
+ * it, checkRequiredFields 400s every create payload written before the
291
+ * column existed and the DEFAULT '1' can never apply.
292
+ */
293
+ test("matchCountThreshold's TableColumn is required with default 1 as a default-value column", () => {
294
+ const metadata: TableColumnMetadata =
295
+ new DetectionRule().getTableColumnMetadata("matchCountThreshold");
296
+
297
+ expect(metadata.type).toBe(TableColumnType.Number);
298
+ expect(metadata.required).toBe(true);
299
+ expect(metadata.defaultValue).toBe(1);
300
+ expect(metadata.isDefaultValueColumn).toBe(true);
301
+ });
302
+
303
+ test("both columns default to undefined on a fresh model instance", () => {
304
+ const rule: DetectionRule = new DetectionRule();
305
+
306
+ expect("distinctCountField" in rule).toBe(true);
307
+ expect("matchCountThreshold" in rule).toBe(true);
308
+ expect(rule.distinctCountField).toBeUndefined();
309
+ expect(rule.matchCountThreshold).toBeUndefined();
310
+ });
311
+ });
@@ -13,8 +13,10 @@ import { describe, expect, test } from "@jest/globals";
13
13
  * typo must surface to the person editing the rule, not as a cron-side
14
14
  * lastError hours later. These tests drive the create/update hooks
15
15
  * directly: invalid or aggregation-based Sigma is rejected, the rule name
16
- * defaults from the Sigma title, and the evaluation interval is clamped to
17
- * whole minutes between 1 and 1440.
16
+ * defaults from the Sigma title, the evaluation interval is clamped to
17
+ * whole minutes between 1 and 1440, and the match count threshold must be
18
+ * a whole number between 1 and 1000000 (or omitted — the column defaults
19
+ * to 1, so old API clients that never send it keep working).
18
20
  */
19
21
 
20
22
  const PROJECT_ID: ObjectID = new ObjectID(
@@ -52,6 +54,7 @@ function buildRule(
52
54
  name?: string;
53
55
  sigmaRuleYaml?: string;
54
56
  evaluationIntervalInMinutes?: number;
57
+ matchCountThreshold?: number;
55
58
  } = {},
56
59
  ): DetectionRule {
57
60
  const rule: DetectionRule = new DetectionRule();
@@ -66,6 +69,10 @@ function buildRule(
66
69
  rule.evaluationIntervalInMinutes = options.evaluationIntervalInMinutes;
67
70
  }
68
71
 
72
+ if (options.matchCountThreshold !== undefined) {
73
+ rule.matchCountThreshold = options.matchCountThreshold;
74
+ }
75
+
69
76
  return rule;
70
77
  }
71
78
 
@@ -219,6 +226,58 @@ detection:
219
226
  ).resolves.toBeDefined();
220
227
  });
221
228
  });
229
+
230
+ describe("match count threshold validation", () => {
231
+ const THRESHOLD_ERROR: string =
232
+ "Match count threshold must be a whole number between 1 and 1000000.";
233
+
234
+ test.each([1, 5, 1000000])("%d is accepted", async (threshold: number) => {
235
+ const rule: DetectionRule = buildRule({
236
+ name: "Thresholded",
237
+ matchCountThreshold: threshold,
238
+ });
239
+
240
+ await expect(
241
+ service.onBeforeCreate(buildCreateBy(rule)),
242
+ ).resolves.toBeDefined();
243
+ });
244
+
245
+ test.each([0, -1, 1.5, 1000001])(
246
+ "%d is rejected with the exact message",
247
+ async (threshold: number) => {
248
+ const rule: DetectionRule = buildRule({
249
+ name: "Bad Threshold",
250
+ matchCountThreshold: threshold,
251
+ });
252
+
253
+ await expect(
254
+ service.onBeforeCreate(buildCreateBy(rule)),
255
+ ).rejects.toThrow(THRESHOLD_ERROR);
256
+ },
257
+ );
258
+
259
+ test("an omitted threshold passes — the column defaults to 1, so old API clients that never send it still create rules", async () => {
260
+ const rule: DetectionRule = buildRule({ name: "No Threshold" });
261
+
262
+ expect(rule.matchCountThreshold).toBeUndefined();
263
+
264
+ await expect(
265
+ service.onBeforeCreate(buildCreateBy(rule)),
266
+ ).resolves.toBeDefined();
267
+ });
268
+
269
+ test("a valid threshold does not bypass the Sigma validation that runs before it", async () => {
270
+ const rule: DetectionRule = buildRule({
271
+ name: "Still Broken YAML",
272
+ sigmaRuleYaml: "title: [unclosed",
273
+ matchCountThreshold: 5,
274
+ });
275
+
276
+ await expect(service.onBeforeCreate(buildCreateBy(rule))).rejects.toThrow(
277
+ BadDataException,
278
+ );
279
+ });
280
+ });
222
281
  });
223
282
 
224
283
  describe("DetectionRuleService.onBeforeUpdate", () => {
@@ -251,4 +310,77 @@ describe("DetectionRuleService.onBeforeUpdate", () => {
251
310
  ),
252
311
  ).rejects.toThrow(BadDataException);
253
312
  });
313
+
314
+ test("an update with an invalid matchCountThreshold is rejected with the exact message", async () => {
315
+ await expect(
316
+ service.onBeforeUpdate(buildUpdateBy({ matchCountThreshold: 0 })),
317
+ ).rejects.toThrow(
318
+ "Match count threshold must be a whole number between 1 and 1000000.",
319
+ );
320
+ });
321
+
322
+ test("an update with a valid matchCountThreshold passes", async () => {
323
+ await expect(
324
+ service.onBeforeUpdate(buildUpdateBy({ matchCountThreshold: 42 })),
325
+ ).resolves.toBeDefined();
326
+ });
327
+
328
+ test("an update that omits matchCountThreshold skips the threshold validation", async () => {
329
+ await expect(
330
+ service.onBeforeUpdate(buildUpdateBy({ description: "No threshold" })),
331
+ ).resolves.toBeDefined();
332
+ });
333
+ });
334
+
335
+ describe("DetectionRuleService field-name trimming", () => {
336
+ /*
337
+ * groupByField / distinctCountField are looked up verbatim by the
338
+ * evaluator — an unknown name silently becomes an attributes[] lookup
339
+ * yielding '' on every row, and for a distinct-count rule that is a
340
+ * permanent zero-fire outage with no error anywhere. Trimming at save
341
+ * time kills the whitespace-from-a-paste class of that failure.
342
+ */
343
+ test("create trims whitespace from groupByField and distinctCountField", async () => {
344
+ const rule: DetectionRule = buildRule();
345
+ rule.groupByField = " principalIp ";
346
+ rule.distinctCountField = "principalUser\t";
347
+
348
+ const result: OnCreate<DetectionRule> = await service.onBeforeCreate(
349
+ buildCreateBy(rule),
350
+ );
351
+
352
+ expect(result.createBy.data.groupByField).toBe("principalIp");
353
+ expect(result.createBy.data.distinctCountField).toBe("principalUser");
354
+ });
355
+
356
+ test("create leaves omitted field names undefined", async () => {
357
+ const result: OnCreate<DetectionRule> = await service.onBeforeCreate(
358
+ buildCreateBy(buildRule()),
359
+ );
360
+
361
+ expect(result.createBy.data.groupByField).toBeUndefined();
362
+ expect(result.createBy.data.distinctCountField).toBeUndefined();
363
+ });
364
+
365
+ test("update trims field names and collapses whitespace-only to the cleared value", async () => {
366
+ const result: OnUpdate<DetectionRule> = await service.onBeforeUpdate(
367
+ buildUpdateBy({
368
+ groupByField: " ",
369
+ distinctCountField: " targetUser ",
370
+ }),
371
+ );
372
+
373
+ // '' is the documented "feature off" value, so clearing still works.
374
+ expect(result.updateBy.data.groupByField).toBe("");
375
+ expect(result.updateBy.data.distinctCountField).toBe("targetUser");
376
+ });
377
+
378
+ test("an update that omits the field names does not add them", async () => {
379
+ const result: OnUpdate<DetectionRule> = await service.onBeforeUpdate(
380
+ buildUpdateBy({ description: "No field names" }),
381
+ );
382
+
383
+ expect("groupByField" in result.updateBy.data).toBe(false);
384
+ expect("distinctCountField" in result.updateBy.data).toBe(false);
385
+ });
254
386
  });
@@ -140,7 +140,13 @@ describe("discovery-scan claim hookless write safety preconditions", () => {
140
140
  "startedAt",
141
141
  "statusMessage",
142
142
  ];
143
- const columnsValidatedByHook: Array<string> = ["cidr"];
143
+ /*
144
+ * `name` joined `cidr` here when discovery scans became nameable
145
+ * (issue #3391): the hook validates and normalizes it on any update that
146
+ * carries it. The claim payload does not, which is what keeps the
147
+ * hookless write honest.
148
+ */
149
+ const columnsValidatedByHook: Array<string> = ["cidr", "name"];
144
150
 
145
151
  for (const column of claimWriteColumns) {
146
152
  expect(columnsValidatedByHook).not.toContain(column);
@@ -0,0 +1,131 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+
4
+ /*
5
+ * Retention windows are declared per service via hardDeleteItemsOlderThanInDays,
6
+ * but they only take effect once the service is listed in
7
+ * Server/Services/Index.ts - that array is what
8
+ * App/FeatureSet/Workers/Jobs/HardDelete/HardDeleteItemsInDatabase.ts iterates.
9
+ * A window on an unregistered service is dead configuration, and registering
10
+ * the service later silently arms it.
11
+ *
12
+ * That is not hypothetical: OnCallDutyPolicyFeedService sat unregistered
13
+ * carrying a 90 day window while every other feed kept 3 years. Commit
14
+ * 6bbf2f866c ("retain items for 3 years instead of 120 days") swept the feeds
15
+ * from 120 to 3 * 365 and missed this one precisely because it held 90 rather
16
+ * than 120, and nothing noticed because the service was not registered.
17
+ * Registering it - which the workflow component registry fix had to do, since
18
+ * OnCallDutyPolicyFeed is workflow-enabled - would have started hard deleting
19
+ * on-call policy history at 90 days on billing-enabled deployments.
20
+ *
21
+ * Feed tables are the same kind of data as each other: a user-visible activity
22
+ * timeline hung off a parent entity. They should expire together, whether or
23
+ * not they happen to be registered yet.
24
+ */
25
+
26
+ const SERVICES_DIRECTORY: string = path.join(
27
+ __dirname,
28
+ "..",
29
+ "..",
30
+ "..",
31
+ "Server",
32
+ "Services",
33
+ );
34
+
35
+ const RETENTION_PATTERN: RegExp =
36
+ /hardDeleteItemsOlderThanInDays\(\s*"(\w+)"\s*,\s*([^)]+?)\s*\)/;
37
+
38
+ interface FeedRetention {
39
+ serviceName: string;
40
+ columnName: string;
41
+ window: string;
42
+ }
43
+
44
+ const feedServiceNames: Array<string> = fs
45
+ .readdirSync(SERVICES_DIRECTORY)
46
+ .sort()
47
+ .filter((fileName: string) => {
48
+ return fileName.endsWith("FeedService.ts");
49
+ })
50
+ .map((fileName: string) => {
51
+ return fileName.replace(/\.ts$/, "");
52
+ });
53
+
54
+ const feedRetentions: Array<FeedRetention> = [];
55
+
56
+ for (const serviceName of feedServiceNames) {
57
+ const source: string = fs.readFileSync(
58
+ path.join(SERVICES_DIRECTORY, `${serviceName}.ts`),
59
+ "utf8",
60
+ );
61
+
62
+ const match: RegExpMatchArray | null = source.match(RETENTION_PATTERN);
63
+
64
+ if (!match) {
65
+ // Deliberately not skipped - see "declares a retention window at all" below.
66
+ continue;
67
+ }
68
+
69
+ feedRetentions.push({
70
+ serviceName: serviceName,
71
+ columnName: match[1]!,
72
+ window: match[2]!.trim(),
73
+ });
74
+ }
75
+
76
+ describe("Feed service retention windows", () => {
77
+ test("the feed services were actually found", () => {
78
+ // Guards the assertions below against a silent pass on an empty list.
79
+ expect(feedServiceNames.length).toBeGreaterThan(0);
80
+ });
81
+
82
+ test("every feed service declares a retention window at all", () => {
83
+ /*
84
+ * The window/column assertions below can only speak for services this file
85
+ * managed to parse. Comparing against the filenames on disk - rather than a
86
+ * hardcoded count - is what stops a feed service that declares no retention,
87
+ * or declares one in a shape the pattern misses, from being dropped from
88
+ * those assertions and passing in silence.
89
+ */
90
+ const parsed: Array<string> = feedRetentions.map((entry: FeedRetention) => {
91
+ return entry.serviceName;
92
+ });
93
+
94
+ expect(parsed).toEqual(feedServiceNames);
95
+ });
96
+
97
+ test("every feed service expires on the same window", () => {
98
+ const windows: Array<string> = [
99
+ ...new Set(
100
+ feedRetentions.map((entry: FeedRetention) => {
101
+ return entry.window;
102
+ }),
103
+ ),
104
+ ];
105
+
106
+ // Reported with the owning service names so a divergent one is obvious.
107
+ expect({
108
+ windows,
109
+ services: feedRetentions.map((entry: FeedRetention) => {
110
+ return `${entry.serviceName}=${entry.window}`;
111
+ }),
112
+ }).toEqual({
113
+ windows: ["3 * 365"],
114
+ services: feedRetentions.map((entry: FeedRetention) => {
115
+ return `${entry.serviceName}=3 * 365`;
116
+ }),
117
+ });
118
+ });
119
+
120
+ test("every feed service expires on the same column", () => {
121
+ const columns: Array<string> = [
122
+ ...new Set(
123
+ feedRetentions.map((entry: FeedRetention) => {
124
+ return entry.columnName;
125
+ }),
126
+ ),
127
+ ];
128
+
129
+ expect(columns).toEqual(["createdAt"]);
130
+ });
131
+ });