@oneuptime/common 12.0.22 → 12.0.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (345) hide show
  1. package/Models/DatabaseModels/DetectionRule.ts +55 -0
  2. package/Models/DatabaseModels/Index.ts +2 -0
  3. package/Models/DatabaseModels/NetworkDevice.ts +44 -0
  4. package/Models/DatabaseModels/NetworkDeviceAutoImportRule.ts +584 -0
  5. package/Models/DatabaseModels/NetworkDeviceDiscoveryScan.ts +105 -0
  6. package/Models/DatabaseModels/Project.ts +68 -0
  7. package/Models/DatabaseModels/User.ts +68 -0
  8. package/Server/API/TelemetryAPI.ts +29 -0
  9. package/Server/Infrastructure/Postgres/SchemaMigrations/1789100000000-AddNetworkDeviceAutoImportRule.ts +60 -0
  10. package/Server/Infrastructure/Postgres/SchemaMigrations/1789200000000-AddSysObjectIdPatternToAutoImportRule.ts +26 -0
  11. package/Server/Infrastructure/Postgres/SchemaMigrations/1789300000000-AddAutoApplyVendorHealthTemplate.ts +26 -0
  12. package/Server/Infrastructure/Postgres/SchemaMigrations/1789400000000-AddNameToNetworkDeviceDiscoveryScan.ts +28 -0
  13. package/Server/Infrastructure/Postgres/SchemaMigrations/1789500000000-AddCampaignUtmFields.ts +49 -0
  14. package/Server/Infrastructure/Postgres/SchemaMigrations/1789512000000-AddDetectionRuleDistinctCountColumns.ts +33 -0
  15. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +12 -0
  16. package/Server/Middleware/GzipRequestBody.ts +311 -0
  17. package/Server/Middleware/MultipartFormData.ts +87 -3
  18. package/Server/Services/DetectionRuleService.ts +73 -6
  19. package/Server/Services/Index.ts +207 -0
  20. package/Server/Services/NetworkDeviceAutoImportRuleEngineService.ts +917 -0
  21. package/Server/Services/NetworkDeviceAutoImportRuleService.ts +181 -0
  22. package/Server/Services/NetworkDeviceDiscoveryScanService.ts +59 -0
  23. package/Server/Services/NetworkDeviceService.ts +148 -0
  24. package/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.ts +65 -0
  25. package/Server/Services/OnCallDutyPolicyFeedService.ts +1 -1
  26. package/Server/Services/ProjectService.ts +29 -19
  27. package/Server/Services/SecurityEventService.ts +90 -13
  28. package/Server/Services/StatusPageResourceService.ts +223 -0
  29. package/Server/Services/TelemetryAttributeService.ts +17 -0
  30. package/Server/Services/UserService.ts +4 -6
  31. package/Server/Types/Markdown.ts +161 -67
  32. package/Server/Utils/AI/Toolbox/OnCallTools.ts +12 -4
  33. package/Server/Utils/AnalyticsDatabase/Statement.ts +13 -1
  34. package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +297 -6
  35. package/Server/Utils/Attribution.ts +4 -0
  36. package/Server/Utils/Marketing/MarketingEventUtil.ts +45 -7
  37. package/Server/Utils/Monitor/NetworkInventoryUtil.ts +39 -1
  38. package/Server/Utils/SecurityEvent/DetectionRuleEvaluator.ts +55 -3
  39. package/Server/Utils/SecurityEvent/Sigma/SigmaClickhouseCompiler.ts +21 -0
  40. package/Server/Utils/StartServer.ts +12 -32
  41. package/Server/Utils/Telemetry/LlmCostBudgetEvaluator.ts +101 -4
  42. package/Server/Utils/Telemetry/LlmMetricSpend.ts +130 -0
  43. package/Server/Views/Partials/AnalyticsConsent.ejs +159 -102
  44. package/Server/Views/Partials/Head.ejs +2 -1
  45. package/Tests/App/Dashboard/AdminUserNotificationMethodsPage.test.tsx +776 -38
  46. package/Tests/App/Dashboard/CorrelateFilterBuilder.test.tsx +359 -0
  47. package/Tests/App/Dashboard/CorrelateGraph.test.tsx +865 -0
  48. package/Tests/App/Dashboard/CorrelationGraph.test.ts +346 -0
  49. package/Tests/App/Dashboard/CriteriaFilterConditionDropdown.test.tsx +256 -0
  50. package/Tests/App/Dashboard/CriteriaFilterDefaults.test.ts +738 -0
  51. package/Tests/App/Dashboard/CriteriaFilterMonitorTypeChange.test.ts +1122 -0
  52. package/Tests/App/Dashboard/DiscoveryScanWizardValidation.test.tsx +283 -0
  53. package/Tests/App/Dashboard/ExternalStatusPageCriteriaFilter.test.ts +407 -0
  54. package/Tests/App/Dashboard/MetricSeriesInvestigateMenu.test.tsx +5 -14
  55. package/Tests/App/Dashboard/MonitorCriteriaActionToggles.test.tsx +426 -0
  56. package/Tests/App/Dashboard/MonitorStepsCriteriaAlignmentWiring.test.tsx +352 -0
  57. package/Tests/App/Dashboard/OnCallLayerCardOverrides.test.tsx +278 -0
  58. package/Tests/App/Dashboard/OnCallLayerShiftPreviewOverrides.test.ts +237 -0
  59. package/Tests/App/Dashboard/OnCallReadinessSurfaces.test.tsx +2 -2
  60. package/Tests/App/Dashboard/OnCallSchedulePreviewOverrides.test.tsx +460 -0
  61. package/Tests/App/Dashboard/SecurityEventAttributeUtil.test.ts +171 -0
  62. package/Tests/App/Dashboard/SecurityEventCorrelation.test.ts +857 -0
  63. package/Tests/App/Dashboard/SecurityEventDetailObservables.test.tsx +194 -0
  64. package/Tests/App/Dashboard/SecurityEventsDetectionRulesPage.test.tsx +124 -0
  65. package/Tests/App/Dashboard/SecurityEventsMonitorStepForm.test.tsx +79 -19
  66. package/Tests/App/Dashboard/SecurityEventsTableColumns.test.tsx +382 -0
  67. package/Tests/App/StatusPage/BulkAddStatusPageMonitors.test.ts +216 -0
  68. package/Tests/App/StatusPage/BulkAddStatusPageMonitorsModal.test.tsx +590 -11
  69. package/Tests/App/StatusPage/StatusPageResourceExplorer.test.tsx +757 -0
  70. package/Tests/Models/DiscoveryScanNameColumn.test.ts +293 -0
  71. package/Tests/Server/API/SecurityEventAttributesAPI.test.ts +564 -0
  72. package/Tests/Server/Infrastructure/Postgres/SchemaMigrationsOrdering.test.ts +120 -4
  73. package/Tests/Server/Middleware/GzipRequestBody.test.ts +725 -0
  74. package/Tests/Server/Middleware/MultipartFormData.test.ts +298 -0
  75. package/Tests/Server/Services/AddDetectionRuleDistinctCountColumnsMigration.test.ts +311 -0
  76. package/Tests/Server/Services/AutoImportScanCredentialSelect.test.ts +205 -0
  77. package/Tests/Server/Services/DetectionRuleService.test.ts +134 -2
  78. package/Tests/Server/Services/DiscoveryScanClaimHookFreeSafety.test.ts +7 -1
  79. package/Tests/Server/Services/FeedRetentionConsistency.test.ts +131 -0
  80. package/Tests/Server/Services/NetworkDeviceAutoImportRuleEngineService.test.ts +933 -0
  81. package/Tests/Server/Services/NetworkDeviceDiscoveryScanService.test.ts +194 -0
  82. package/Tests/Server/Services/NetworkDeviceMonitorStatusStamp.test.ts +800 -0
  83. package/Tests/Server/Services/OnCallScheduleAttachRosterRefresh.test.ts +244 -0
  84. package/Tests/Server/Services/SecurityEventService.test.ts +376 -0
  85. package/Tests/Server/Services/StatusPageResourceDuplicates.test.ts +703 -0
  86. package/Tests/Server/Services/TelemetryAttributeServiceSecurityEvent.test.ts +256 -0
  87. package/Tests/Server/Types/Workflow/Components/ComponentRegistryParity.test.ts +417 -0
  88. package/Tests/Server/Utils/AnalyticsDatabase/StatementGeneratorWhereOperators.test.ts +677 -0
  89. package/Tests/Server/Utils/Marketing/MarketingEventUtil.test.ts +43 -0
  90. package/Tests/Server/Utils/Monitor/Criteria/ProfileMonitorCriteria.test.ts +15 -11
  91. package/Tests/Server/Utils/Monitor/NetworkInventoryUtil.test.ts +97 -1
  92. package/Tests/Server/Utils/SecurityEvent/DetectionRuleEvaluator.test.ts +369 -8
  93. package/Tests/Server/Utils/SecurityEvent/SigmaClickhouseCompiler.test.ts +35 -0
  94. package/Tests/Server/Utils/Telemetry/LlmCostBudgetEvaluator.test.ts +368 -4
  95. package/Tests/Server/Utils/Telemetry/LlmMetricSpend.test.ts +245 -0
  96. package/Tests/Server/Views/AnalyticsConsentPartial.test.ts +48 -5
  97. package/Tests/Types/Code/YamlSyntax.test.ts +396 -0
  98. package/Tests/Types/Monitor/MetricAndProfileDefaultCriteria.test.ts +957 -0
  99. package/Tests/Types/Monitor/MonitorCriteriaActionValidation.test.ts +598 -0
  100. package/Tests/Types/Monitor/MonitorCriteriaInstance.test.ts +49 -0
  101. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationNotificationMode.test.ts +6 -6
  102. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationSeverityMapper.test.ts +4 -4
  103. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationUtil.test.ts +4 -3
  104. package/Tests/Types/Telemetry/LlmMetricConventions.test.ts +193 -0
  105. package/Tests/UI/Components/AiInvestigationSettingsCard.test.tsx +0 -12
  106. package/Tests/UI/Components/CardModelDetailEdit.test.tsx +1 -17
  107. package/Tests/UI/Components/CodeEditor.test.tsx +474 -10
  108. package/Tests/UI/Components/Detail/YamlField.test.tsx +105 -0
  109. package/Tests/UI/Components/EntityDropdownLabelsBulkAdd.test.tsx +319 -0
  110. package/Tests/UI/Components/Forms/FormFieldYaml.test.tsx +231 -0
  111. package/Tests/UI/Components/Forms/Utils/FormFieldSchemaTypeUtil.test.ts +1 -0
  112. package/Tests/UI/Components/Forms/ValidationYAML.test.ts +234 -0
  113. package/Tests/UI/Components/ModelTable/AttributeColumns.test.tsx +573 -0
  114. package/Tests/UI/Components/ModelTable/BaseModelTableAttributeColumns.test.tsx +824 -0
  115. package/Tests/UI/Components/ModelTable/ColumnCustomizationModalAddableColumns.test.tsx +707 -0
  116. package/Tests/UI/Components/SideOver.test.tsx +1 -1
  117. package/Tests/UI/Components/StatusPage/ResourceGridSelection.test.tsx +294 -0
  118. package/Tests/UI/Components/StatusPage/ResourceListSelection.test.tsx +436 -0
  119. package/Tests/UI/Components/TableBulkCsvExport.test.tsx +6 -2
  120. package/Tests/UI/Components/Toggle.test.tsx +25 -0
  121. package/Tests/UI/Components/YamlEditor.test.tsx +847 -0
  122. package/Tests/Utils/NetworkDevice/DeviceReachabilityUtil.test.ts +252 -0
  123. package/Tests/Utils/NetworkDiscovery/AutoImportRuleMatcher.test.ts +516 -0
  124. package/Tests/Utils/NetworkDiscovery/DiscoveredDeviceBuilder.test.ts +272 -0
  125. package/Tests/Utils/NetworkDiscovery/DiscoveredHostUtil.test.ts +221 -0
  126. package/Tests/Utils/NetworkDiscovery/ScanNameUtil.test.ts +312 -0
  127. package/Tests/Utils/NetworkDiscovery/ScanTargetUtil.test.ts +164 -0
  128. package/Tests/Utils/StatusPage/ResourceExplorer.test.ts +468 -0
  129. package/Tests/Utils/Telemetry/LlmMetricQuery.test.ts +472 -0
  130. package/Types/Code/YamlSyntax.ts +396 -0
  131. package/Types/Exception/ExceptionCode.ts +1 -0
  132. package/Types/Exception/PayloadTooLargeException.ts +8 -0
  133. package/Types/Marketing/Attribution.ts +10 -0
  134. package/Types/Marketing/MarketingEvent.ts +4 -0
  135. package/Types/Monitor/MonitorCriteriaInstance.ts +116 -56
  136. package/Types/Monitor/Recommendation/MonitorRecommendationSeverityMapper.ts +2 -1
  137. package/Types/NetworkAutomation/RuleRunResult.ts +84 -0
  138. package/Types/Permission.ts +47 -0
  139. package/Types/SecurityEvent/DetectionFindingConstants.ts +11 -0
  140. package/Types/Telemetry/LlmMetricConventions.ts +124 -0
  141. package/UI/Components/AutocompleteTextInput/AutocompleteTextInput.tsx +3 -0
  142. package/UI/Components/BulkUpdate/BulkUpdateForm.tsx +10 -1
  143. package/UI/Components/CodeEditor/CodeEditor.tsx +241 -79
  144. package/UI/Components/CodeEditor/YamlEditor.tsx +369 -0
  145. package/UI/Components/Detail/Detail.tsx +10 -0
  146. package/UI/Components/EntityDropdown/EntityDropdown.tsx +33 -0
  147. package/UI/Components/Forms/Fields/FormField.tsx +31 -0
  148. package/UI/Components/Forms/Types/Field.ts +8 -0
  149. package/UI/Components/Forms/Types/FormFieldSchemaType.ts +1 -0
  150. package/UI/Components/Forms/Utils/FormFieldSchemaTypeUtil.ts +2 -0
  151. package/UI/Components/Forms/Validation.ts +46 -0
  152. package/UI/Components/Input/Input.tsx +3 -0
  153. package/UI/Components/ModelTable/AttributeColumns.tsx +321 -0
  154. package/UI/Components/ModelTable/BaseModelTable.tsx +263 -14
  155. package/UI/Components/ModelTable/Column.ts +8 -0
  156. package/UI/Components/ModelTable/ColumnCustomizationModal.tsx +310 -0
  157. package/UI/Components/ModelTable/ColumnPreference.ts +13 -1
  158. package/UI/Components/MonitorTemplateVariables/TemplateVariablesModal.tsx +2 -2
  159. package/UI/Components/SideOver/SideOver.tsx +5 -5
  160. package/UI/Components/StatusPage/ResourceGrid.tsx +82 -2
  161. package/UI/Components/StatusPage/ResourceList.tsx +197 -38
  162. package/UI/Components/Toggle/Toggle.tsx +9 -1
  163. package/UI/Components/Types/FieldType.ts +1 -0
  164. package/Utils/NetworkDevice/DeviceReachabilityUtil.ts +73 -1
  165. package/Utils/NetworkDiscovery/AutoImportRuleMatcher.ts +257 -0
  166. package/Utils/NetworkDiscovery/DiscoveredDeviceBuilder.ts +183 -0
  167. package/Utils/NetworkDiscovery/DiscoveredHostUtil.ts +95 -0
  168. package/Utils/NetworkDiscovery/DiscoveryImportEligibility.ts +66 -0
  169. package/Utils/NetworkDiscovery/ScanNameUtil.ts +181 -0
  170. package/Utils/NetworkDiscovery/ScanTargetUtil.ts +82 -0
  171. package/Utils/StatusPage/ResourceExplorer.ts +258 -0
  172. package/Utils/Telemetry/LlmMetricQuery.ts +204 -0
  173. package/build/dist/Models/DatabaseModels/DetectionRule.js +57 -0
  174. package/build/dist/Models/DatabaseModels/DetectionRule.js.map +1 -1
  175. package/build/dist/Models/DatabaseModels/Index.js +2 -0
  176. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  177. package/build/dist/Models/DatabaseModels/NetworkDevice.js +45 -0
  178. package/build/dist/Models/DatabaseModels/NetworkDevice.js.map +1 -1
  179. package/build/dist/Models/DatabaseModels/NetworkDeviceAutoImportRule.js +609 -0
  180. package/build/dist/Models/DatabaseModels/NetworkDeviceAutoImportRule.js.map +1 -0
  181. package/build/dist/Models/DatabaseModels/NetworkDeviceDiscoveryScan.js +97 -0
  182. package/build/dist/Models/DatabaseModels/NetworkDeviceDiscoveryScan.js.map +1 -1
  183. package/build/dist/Models/DatabaseModels/Project.js +76 -0
  184. package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
  185. package/build/dist/Models/DatabaseModels/User.js +76 -0
  186. package/build/dist/Models/DatabaseModels/User.js.map +1 -1
  187. package/build/dist/Server/API/TelemetryAPI.js +17 -0
  188. package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
  189. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789100000000-AddNetworkDeviceAutoImportRule.js +35 -0
  190. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789100000000-AddNetworkDeviceAutoImportRule.js.map +1 -0
  191. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789200000000-AddSysObjectIdPatternToAutoImportRule.js +19 -0
  192. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789200000000-AddSysObjectIdPatternToAutoImportRule.js.map +1 -0
  193. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789300000000-AddAutoApplyVendorHealthTemplate.js +19 -0
  194. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789300000000-AddAutoApplyVendorHealthTemplate.js.map +1 -0
  195. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789400000000-AddNameToNetworkDeviceDiscoveryScan.js +21 -0
  196. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789400000000-AddNameToNetworkDeviceDiscoveryScan.js.map +1 -0
  197. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789500000000-AddCampaignUtmFields.js +42 -0
  198. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789500000000-AddCampaignUtmFields.js.map +1 -0
  199. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789512000000-AddDetectionRuleDistinctCountColumns.js +22 -0
  200. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789512000000-AddDetectionRuleDistinctCountColumns.js.map +1 -0
  201. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +12 -0
  202. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  203. package/build/dist/Server/Middleware/GzipRequestBody.js +224 -0
  204. package/build/dist/Server/Middleware/GzipRequestBody.js.map +1 -0
  205. package/build/dist/Server/Middleware/MultipartFormData.js +71 -2
  206. package/build/dist/Server/Middleware/MultipartFormData.js.map +1 -1
  207. package/build/dist/Server/Services/DetectionRuleService.js +49 -4
  208. package/build/dist/Server/Services/DetectionRuleService.js.map +1 -1
  209. package/build/dist/Server/Services/Index.js +206 -0
  210. package/build/dist/Server/Services/Index.js.map +1 -1
  211. package/build/dist/Server/Services/NetworkDeviceAutoImportRuleEngineService.js +711 -0
  212. package/build/dist/Server/Services/NetworkDeviceAutoImportRuleEngineService.js.map +1 -0
  213. package/build/dist/Server/Services/NetworkDeviceAutoImportRuleService.js +155 -0
  214. package/build/dist/Server/Services/NetworkDeviceAutoImportRuleService.js.map +1 -0
  215. package/build/dist/Server/Services/NetworkDeviceDiscoveryScanService.js +50 -0
  216. package/build/dist/Server/Services/NetworkDeviceDiscoveryScanService.js.map +1 -1
  217. package/build/dist/Server/Services/NetworkDeviceService.js +124 -0
  218. package/build/dist/Server/Services/NetworkDeviceService.js.map +1 -1
  219. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js +68 -13
  220. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js.map +1 -1
  221. package/build/dist/Server/Services/OnCallDutyPolicyFeedService.js +1 -1
  222. package/build/dist/Server/Services/OnCallDutyPolicyFeedService.js.map +1 -1
  223. package/build/dist/Server/Services/ProjectService.js +25 -45
  224. package/build/dist/Server/Services/ProjectService.js.map +1 -1
  225. package/build/dist/Server/Services/SecurityEventService.js +57 -5
  226. package/build/dist/Server/Services/SecurityEventService.js.map +1 -1
  227. package/build/dist/Server/Services/StatusPageResourceService.js +140 -0
  228. package/build/dist/Server/Services/StatusPageResourceService.js.map +1 -1
  229. package/build/dist/Server/Services/TelemetryAttributeService.js +17 -0
  230. package/build/dist/Server/Services/TelemetryAttributeService.js.map +1 -1
  231. package/build/dist/Server/Services/UserService.js +2 -12
  232. package/build/dist/Server/Services/UserService.js.map +1 -1
  233. package/build/dist/Server/Types/Markdown.js +147 -61
  234. package/build/dist/Server/Types/Markdown.js.map +1 -1
  235. package/build/dist/Server/Utils/AI/Toolbox/OnCallTools.js +12 -4
  236. package/build/dist/Server/Utils/AI/Toolbox/OnCallTools.js.map +1 -1
  237. package/build/dist/Server/Utils/AnalyticsDatabase/Statement.js +12 -1
  238. package/build/dist/Server/Utils/AnalyticsDatabase/Statement.js.map +1 -1
  239. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +246 -9
  240. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js.map +1 -1
  241. package/build/dist/Server/Utils/Attribution.js.map +1 -1
  242. package/build/dist/Server/Utils/Marketing/MarketingEventUtil.js +27 -7
  243. package/build/dist/Server/Utils/Marketing/MarketingEventUtil.js.map +1 -1
  244. package/build/dist/Server/Utils/Monitor/NetworkInventoryUtil.js +26 -0
  245. package/build/dist/Server/Utils/Monitor/NetworkInventoryUtil.js.map +1 -1
  246. package/build/dist/Server/Utils/SecurityEvent/DetectionRuleEvaluator.js +42 -7
  247. package/build/dist/Server/Utils/SecurityEvent/DetectionRuleEvaluator.js.map +1 -1
  248. package/build/dist/Server/Utils/SecurityEvent/Sigma/SigmaClickhouseCompiler.js +18 -0
  249. package/build/dist/Server/Utils/SecurityEvent/Sigma/SigmaClickhouseCompiler.js.map +1 -1
  250. package/build/dist/Server/Utils/StartServer.js +12 -21
  251. package/build/dist/Server/Utils/StartServer.js.map +1 -1
  252. package/build/dist/Server/Utils/Telemetry/LlmCostBudgetEvaluator.js +76 -5
  253. package/build/dist/Server/Utils/Telemetry/LlmCostBudgetEvaluator.js.map +1 -1
  254. package/build/dist/Server/Utils/Telemetry/LlmMetricSpend.js +102 -0
  255. package/build/dist/Server/Utils/Telemetry/LlmMetricSpend.js.map +1 -0
  256. package/build/dist/Types/Code/YamlSyntax.js +212 -0
  257. package/build/dist/Types/Code/YamlSyntax.js.map +1 -0
  258. package/build/dist/Types/Exception/ExceptionCode.js +1 -0
  259. package/build/dist/Types/Exception/ExceptionCode.js.map +1 -1
  260. package/build/dist/Types/Exception/PayloadTooLargeException.js +8 -0
  261. package/build/dist/Types/Exception/PayloadTooLargeException.js.map +1 -0
  262. package/build/dist/Types/Marketing/Attribution.js +10 -0
  263. package/build/dist/Types/Marketing/Attribution.js.map +1 -1
  264. package/build/dist/Types/Monitor/MonitorCriteriaInstance.js +112 -51
  265. package/build/dist/Types/Monitor/MonitorCriteriaInstance.js.map +1 -1
  266. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationSeverityMapper.js +2 -1
  267. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationSeverityMapper.js.map +1 -1
  268. package/build/dist/Types/NetworkAutomation/RuleRunResult.js +24 -0
  269. package/build/dist/Types/NetworkAutomation/RuleRunResult.js.map +1 -1
  270. package/build/dist/Types/Permission.js +41 -0
  271. package/build/dist/Types/Permission.js.map +1 -1
  272. package/build/dist/Types/SecurityEvent/DetectionFindingConstants.js +9 -0
  273. package/build/dist/Types/SecurityEvent/DetectionFindingConstants.js.map +1 -1
  274. package/build/dist/Types/Telemetry/LlmMetricConventions.js +98 -0
  275. package/build/dist/Types/Telemetry/LlmMetricConventions.js.map +1 -0
  276. package/build/dist/UI/Components/AutocompleteTextInput/AutocompleteTextInput.js +1 -1
  277. package/build/dist/UI/Components/AutocompleteTextInput/AutocompleteTextInput.js.map +1 -1
  278. package/build/dist/UI/Components/BulkUpdate/BulkUpdateForm.js +5 -2
  279. package/build/dist/UI/Components/BulkUpdate/BulkUpdateForm.js.map +1 -1
  280. package/build/dist/UI/Components/CodeEditor/CodeEditor.js +206 -76
  281. package/build/dist/UI/Components/CodeEditor/CodeEditor.js.map +1 -1
  282. package/build/dist/UI/Components/CodeEditor/YamlEditor.js +201 -0
  283. package/build/dist/UI/Components/CodeEditor/YamlEditor.js.map +1 -0
  284. package/build/dist/UI/Components/Detail/Detail.js +9 -0
  285. package/build/dist/UI/Components/Detail/Detail.js.map +1 -1
  286. package/build/dist/UI/Components/EntityDropdown/EntityDropdown.js +16 -1
  287. package/build/dist/UI/Components/EntityDropdown/EntityDropdown.js.map +1 -1
  288. package/build/dist/UI/Components/Forms/Fields/FormField.js +14 -1
  289. package/build/dist/UI/Components/Forms/Fields/FormField.js.map +1 -1
  290. package/build/dist/UI/Components/Forms/Types/Field.js.map +1 -1
  291. package/build/dist/UI/Components/Forms/Types/FormFieldSchemaType.js +1 -0
  292. package/build/dist/UI/Components/Forms/Types/FormFieldSchemaType.js.map +1 -1
  293. package/build/dist/UI/Components/Forms/Utils/FormFieldSchemaTypeUtil.js +2 -0
  294. package/build/dist/UI/Components/Forms/Utils/FormFieldSchemaTypeUtil.js.map +1 -1
  295. package/build/dist/UI/Components/Forms/Validation.js +27 -0
  296. package/build/dist/UI/Components/Forms/Validation.js.map +1 -1
  297. package/build/dist/UI/Components/Input/Input.js +1 -1
  298. package/build/dist/UI/Components/Input/Input.js.map +1 -1
  299. package/build/dist/UI/Components/ModelTable/AttributeColumns.js +159 -0
  300. package/build/dist/UI/Components/ModelTable/AttributeColumns.js.map +1 -0
  301. package/build/dist/UI/Components/ModelTable/BaseModelTable.js +179 -17
  302. package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
  303. package/build/dist/UI/Components/ModelTable/ColumnCustomizationModal.js +133 -2
  304. package/build/dist/UI/Components/ModelTable/ColumnCustomizationModal.js.map +1 -1
  305. package/build/dist/UI/Components/ModelTable/ColumnPreference.js +7 -1
  306. package/build/dist/UI/Components/ModelTable/ColumnPreference.js.map +1 -1
  307. package/build/dist/UI/Components/MonitorTemplateVariables/TemplateVariablesModal.js +2 -2
  308. package/build/dist/UI/Components/SideOver/SideOver.js +5 -5
  309. package/build/dist/UI/Components/StatusPage/ResourceGrid.js +28 -2
  310. package/build/dist/UI/Components/StatusPage/ResourceGrid.js.map +1 -1
  311. package/build/dist/UI/Components/StatusPage/ResourceList.js +87 -20
  312. package/build/dist/UI/Components/StatusPage/ResourceList.js.map +1 -1
  313. package/build/dist/UI/Components/Toggle/Toggle.js +9 -1
  314. package/build/dist/UI/Components/Toggle/Toggle.js.map +1 -1
  315. package/build/dist/UI/Components/Types/FieldType.js +1 -0
  316. package/build/dist/UI/Components/Types/FieldType.js.map +1 -1
  317. package/build/dist/Utils/NetworkDevice/DeviceReachabilityUtil.js +37 -1
  318. package/build/dist/Utils/NetworkDevice/DeviceReachabilityUtil.js.map +1 -1
  319. package/build/dist/Utils/NetworkDiscovery/AutoImportRuleMatcher.js +149 -0
  320. package/build/dist/Utils/NetworkDiscovery/AutoImportRuleMatcher.js.map +1 -0
  321. package/build/dist/Utils/NetworkDiscovery/DiscoveredDeviceBuilder.js +121 -0
  322. package/build/dist/Utils/NetworkDiscovery/DiscoveredDeviceBuilder.js.map +1 -0
  323. package/build/dist/Utils/NetworkDiscovery/DiscoveredHostUtil.js +75 -0
  324. package/build/dist/Utils/NetworkDiscovery/DiscoveredHostUtil.js.map +1 -0
  325. package/build/dist/Utils/NetworkDiscovery/DiscoveryImportEligibility.js +59 -0
  326. package/build/dist/Utils/NetworkDiscovery/DiscoveryImportEligibility.js.map +1 -0
  327. package/build/dist/Utils/NetworkDiscovery/ScanNameUtil.js +120 -0
  328. package/build/dist/Utils/NetworkDiscovery/ScanNameUtil.js.map +1 -0
  329. package/build/dist/Utils/NetworkDiscovery/ScanTargetUtil.js +60 -0
  330. package/build/dist/Utils/NetworkDiscovery/ScanTargetUtil.js.map +1 -1
  331. package/build/dist/Utils/StatusPage/ResourceExplorer.js +171 -0
  332. package/build/dist/Utils/StatusPage/ResourceExplorer.js.map +1 -1
  333. package/build/dist/Utils/Telemetry/LlmMetricQuery.js +119 -0
  334. package/build/dist/Utils/Telemetry/LlmMetricQuery.js.map +1 -0
  335. package/jest.config.json +2 -0
  336. package/package.json +1 -1
  337. package/tsconfig.json +2 -1
  338. package/Tests/UI/Components/Toast.test.tsx +0 -197
  339. package/Tests/UI/Components/ToastStacking.test.tsx +0 -198
  340. package/UI/Components/Toast/Toast.tsx +0 -189
  341. package/UI/Components/Toast/ToastInit.tsx +0 -110
  342. package/build/dist/UI/Components/Toast/Toast.js +0 -123
  343. package/build/dist/UI/Components/Toast/Toast.js.map +0 -1
  344. package/build/dist/UI/Components/Toast/ToastInit.js +0 -53
  345. package/build/dist/UI/Components/Toast/ToastInit.js.map +0 -1
@@ -0,0 +1,917 @@
1
+ import NetworkDevice from "../../Models/DatabaseModels/NetworkDevice";
2
+ import NetworkDeviceAutoImportRule from "../../Models/DatabaseModels/NetworkDeviceAutoImportRule";
3
+ import NetworkDeviceDiscoveryScan, {
4
+ DiscoveredNetworkDevice,
5
+ } from "../../Models/DatabaseModels/NetworkDeviceDiscoveryScan";
6
+ import NetworkDeviceAutoImportRuleService from "./NetworkDeviceAutoImportRuleService";
7
+ import NetworkDeviceDiscoveryScanService from "./NetworkDeviceDiscoveryScanService";
8
+ import NetworkDeviceService from "./NetworkDeviceService";
9
+ import BadDataException from "../../Types/Exception/BadDataException";
10
+ import { JSONObject } from "../../Types/JSON";
11
+ import LIMIT_MAX from "../../Types/Database/LimitMax";
12
+ import ObjectID from "../../Types/ObjectID";
13
+ import OneUptimeDate from "../../Types/Date";
14
+ import QueryDeepPartialEntity from "../../Types/Database/PartialEntity";
15
+ import SortOrder from "../../Types/BaseDatabase/SortOrder";
16
+ import {
17
+ AutoImportRuleRunResult,
18
+ MAX_MATCHED_IP_SAMPLE,
19
+ } from "../../Types/NetworkAutomation/RuleRunResult";
20
+ import AutoImportRuleMatcher, {
21
+ AutoImportHostEvaluation,
22
+ } from "../../Utils/NetworkDiscovery/AutoImportRuleMatcher";
23
+ import {
24
+ buildFallbackDeviceName,
25
+ buildNetworkDeviceFromDiscoveredHost,
26
+ } from "../../Utils/NetworkDiscovery/DiscoveredDeviceBuilder";
27
+ import { normalizeDiscoveredHosts } from "../../Utils/NetworkDiscovery/DiscoveredHostUtil";
28
+ import Semaphore, { SemaphoreMutex } from "../Infrastructure/Semaphore";
29
+ import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
30
+ import logger, { LogAttributes } from "../Utils/Logger";
31
+
32
+ /*
33
+ * The engine behind network device auto-import rules (issue #3378): reads a
34
+ * completed discovery scan's results, asks the project's rules which hosts
35
+ * import, and creates the Network Devices the manual Review dialog would
36
+ * have — through the same builder, so a rule-imported host and a
37
+ * hand-imported host are the same device. Site assignment, owners and labels
38
+ * then apply themselves through NetworkDeviceService.onCreateSuccess's
39
+ * existing rule chain, exactly as they do for a manual import.
40
+ *
41
+ * Two entry points, mirroring the label-rule engine:
42
+ *
43
+ * - processCompletedScan: the automatic path, called by the
44
+ * NetworkDeviceDiscovery worker for each Completed scan whose
45
+ * autoImportProcessedAt marker is NULL.
46
+ * - applyRuleToCompletedScans: the manual "Run Now" (and its dry run),
47
+ * applying ONE rule to the project's existing completed scans.
48
+ *
49
+ * Everything here is idempotent by construction: a device exists per
50
+ * (project, address), recurring scans re-report the same hosts every
51
+ * interval, and both paths skip hosts whose address is already registered —
52
+ * so seeing the same results twice creates nothing twice.
53
+ */
54
+
55
+ /*
56
+ * The scan columns an import copies onto an SNMP device — the same set the
57
+ * probe claim endpoint selects, and for the same reason: an unselected
58
+ * credential column arrives undefined and would be silently NOT copied,
59
+ * leaving the device unable to poll forever. Exported so a test can pin this
60
+ * list against the builder's DiscoveredDeviceScanSource shape.
61
+ */
62
+ export const AUTO_IMPORT_SCAN_CREDENTIAL_SELECT: {
63
+ [key: string]: boolean;
64
+ } = {
65
+ probeId: true,
66
+ snmpVersion: true,
67
+ snmpCommunityString: true,
68
+ snmpPort: true,
69
+ snmpV3SecurityLevel: true,
70
+ snmpV3Username: true,
71
+ snmpV3AuthProtocol: true,
72
+ snmpV3AuthKey: true,
73
+ snmpV3PrivProtocol: true,
74
+ snmpV3PrivKey: true,
75
+ };
76
+
77
+ /*
78
+ * Devices one pass will create from one scan. Deliberately conservative:
79
+ * every create runs the full service pipeline plus a detached
80
+ * label/owner/site rule chain, so hundreds per pass is already a burst of
81
+ * thousands of queries. The cap loses nothing — a truncated pass leaves the
82
+ * scan's marker unstamped, the next worker tick resumes, and idempotency
83
+ * skips what this pass already created — but it turns "a typo'd rule meets a
84
+ * /16" from an outage into a few paced ticks an operator can catch.
85
+ */
86
+ export const MAX_DEVICES_PER_AUTO_IMPORT_RUN: number = 500;
87
+
88
+ // Scans one manual "Run Now" will read, oldest results last.
89
+ export const MAX_SCANS_PER_AUTO_IMPORT_RULE_RUN: number = 100;
90
+
91
+ /*
92
+ * Results older than this are stamped processed WITHOUT importing. A late
93
+ * result can land on a reaper-Failed scan hours after its sweep ran (the
94
+ * ingest endpoint accepts it as the truth about that run), and a worker
95
+ * outage can leave a backlog — in both cases silently auto-importing an
96
+ * hours-old host list is the wrong surprise. The operator can still reach
97
+ * old results deliberately, through "Run Now".
98
+ */
99
+ export const MAX_RESULT_AGE_IN_HOURS: number = 24;
100
+
101
+ /*
102
+ * The Redis lock every path that CREATES devices from scan results runs
103
+ * under — the worker sweep and the manual Run Now alike. The engine's
104
+ * idempotency is check-then-create with no DB backstop (device names are
105
+ * app-level unique, addresses not unique at all), so two concurrent passes
106
+ * over the same results can both pass the check and both insert. The worker
107
+ * holds this lock for its whole sweep; a real (non-dry) manual run takes
108
+ * the SAME lock, so manual-vs-sweep and manual-vs-manual can never
109
+ * interleave — which also means the scan jsonb write-back has exactly one
110
+ * engine writer at a time. Defined here, imported by the worker, so the two
111
+ * can never drift onto different locks.
112
+ */
113
+ export const AUTO_IMPORT_SWEEP_LOCK_KEY: string =
114
+ "NetworkDeviceDiscovery:ProcessAutoImportRules";
115
+ export const AUTO_IMPORT_SWEEP_LOCK_NAMESPACE: string = "Workers.Cron";
116
+ export const AUTO_IMPORT_SWEEP_LOCK_TIMEOUT_MS: number = 11 * 60 * 1000;
117
+
118
+ /*
119
+ * Hostname sets shared across one worker sweep, keyed by project id. One
120
+ * sweep can process several scans of the same project (overlapping subnets
121
+ * on different probes are normal), and a device created from scan A must
122
+ * read as registered when scan B reports the same address seconds later —
123
+ * before any re-read of the device table would show it.
124
+ */
125
+ export type ExistingHostnamesByProjectId = Map<string, Set<string>>;
126
+
127
+ /*
128
+ * How many imports one run has attempted, shared across every scan the run
129
+ * touches. Real runs count creates and failures; dry runs count the imports
130
+ * they WOULD have attempted — so a dry run over a /16 is bounded by the same
131
+ * cap as the real run it predicts, instead of walking unbounded work inside
132
+ * an API request.
133
+ */
134
+ interface ImportAttemptBudget {
135
+ count: number;
136
+ }
137
+
138
+ class NetworkDeviceAutoImportRuleEngineServiceClass {
139
+ /**
140
+ * The automatic path: apply the project's enabled rules to one Completed
141
+ * scan, then stamp the scan's autoImportProcessedAt marker.
142
+ *
143
+ * Returns null when there was nothing to do (scan gone, superseded,
144
+ * already processed, no import rules, or results too old) and the run's
145
+ * counters otherwise. Throws only on unexpected failures — the caller
146
+ * (the worker sweep) isolates those per scan.
147
+ *
148
+ * The marker protocol, and why every stamp is a compare-and-set on
149
+ * (status, completedAt): the ingest endpoint clears the marker in the same
150
+ * write that stores new results, so "marker is NULL" always means "the
151
+ * results now on the row have not been processed". If new results land
152
+ * while this pass is reading the old ones, the CAS misses, the marker
153
+ * stays NULL, and the next tick processes the new upload — the stamp can
154
+ * never retire results it did not see, and the jsonb write-back can never
155
+ * clobber a newer host list. A truncated pass that created something
156
+ * stamps nothing on purpose: the NULL marker is what makes the next tick
157
+ * resume. (A truncated pass where every create FAILED stamps anyway — see
158
+ * the zero-progress note at the stamp below.)
159
+ */
160
+ @CaptureSpan()
161
+ public async processCompletedScan(data: {
162
+ scanId: ObjectID;
163
+ existingHostnamesByProjectId: ExistingHostnamesByProjectId;
164
+ }): Promise<AutoImportRuleRunResult | null> {
165
+ const scan: NetworkDeviceDiscoveryScan | null =
166
+ await NetworkDeviceDiscoveryScanService.findOneBy({
167
+ query: {
168
+ _id: data.scanId,
169
+ status: "Completed",
170
+ },
171
+ select: {
172
+ _id: true,
173
+ projectId: true,
174
+ status: true,
175
+ completedAt: true,
176
+ autoImportProcessedAt: true,
177
+ discoveredDevices: true,
178
+ ...AUTO_IMPORT_SCAN_CREDENTIAL_SELECT,
179
+ },
180
+ props: {
181
+ isRoot: true,
182
+ },
183
+ });
184
+
185
+ /*
186
+ * Re-checked on this fresh read even though the sweep query already
187
+ * filtered: the sweep's snapshot ages while earlier scans in it are
188
+ * processed, and a scan can complete a new run (or be re-queued) in
189
+ * that window.
190
+ */
191
+ if (!scan || !scan.projectId || scan.autoImportProcessedAt) {
192
+ return null;
193
+ }
194
+
195
+ const projectId: ObjectID = scan.projectId;
196
+
197
+ const rules: Array<NetworkDeviceAutoImportRule> =
198
+ await this.loadEnabledRules(projectId);
199
+
200
+ const hasImportRules: boolean = rules.some(
201
+ (rule: NetworkDeviceAutoImportRule) => {
202
+ return !rule.isExclusion;
203
+ },
204
+ );
205
+
206
+ if (!hasImportRules) {
207
+ // Nothing can import; retire these results so they never accumulate.
208
+ await this.stampScan({ scan: scan, createdIpAddresses: [] });
209
+ return null;
210
+ }
211
+
212
+ if (this.isTooOldToAutoImport(scan)) {
213
+ logger.warn(
214
+ `Auto-import: scan ${scan.id?.toString()} completed more than ${MAX_RESULT_AGE_IN_HOURS} hours ago; stamping it processed without importing. Use the rule's Run Now to import old results deliberately.`,
215
+ { projectId: projectId.toString() } as LogAttributes,
216
+ );
217
+ await this.stampScan({ scan: scan, createdIpAddresses: [] });
218
+ return null;
219
+ }
220
+
221
+ const existingHostnames: Set<string> = await this.getExistingHostnames({
222
+ projectId: projectId,
223
+ cache: data.existingHostnamesByProjectId,
224
+ });
225
+
226
+ const result: AutoImportRuleRunResult = this.emptyResult(false);
227
+ const attempts: ImportAttemptBudget = { count: 0 };
228
+
229
+ const createdIpAddresses: Array<string> = await this.importHostsFromScan({
230
+ scan: scan,
231
+ rules: rules,
232
+ existingHostnames: existingHostnames,
233
+ result: result,
234
+ attempts: attempts,
235
+ isDryRun: false,
236
+ });
237
+
238
+ /*
239
+ * A truncated pass that made progress leaves the marker NULL so the
240
+ * next tick resumes where the cap stopped it. A truncated pass that
241
+ * created NOTHING — every attempt failed — is stamped anyway: resuming
242
+ * it would repeat the identical doomed pass every minute forever, and
243
+ * the sub-cap case already retires all-failing hosts un-imported in one
244
+ * pass, so stamping keeps the two consistent. Run Now remains the
245
+ * deliberate way back into a stamped scan.
246
+ */
247
+ const shouldResume: boolean =
248
+ result.isTruncated && result.devicesCreated > 0;
249
+
250
+ if (result.isTruncated && result.devicesCreated === 0) {
251
+ logger.error(
252
+ `Auto-import: scan ${scan.id?.toString()} hit the per-pass cap with every create failing (${result.devicesFailed} failure(s)); stamping it processed so the sweep does not retry it forever. Inspect the failures and use the rule's Run Now to retry deliberately.`,
253
+ { projectId: projectId.toString() } as LogAttributes,
254
+ );
255
+ }
256
+
257
+ await this.stampScan({
258
+ scan: scan,
259
+ createdIpAddresses: createdIpAddresses,
260
+ leaveMarkerUnstamped: shouldResume,
261
+ });
262
+
263
+ return result;
264
+ }
265
+
266
+ /*
267
+ * The manual path: apply ONE rule to the project's completed scans, newest
268
+ * first — or, as a dry run, evaluate everything and write nothing. The
269
+ * project's enabled exclusion rules always ride along: "Run Now" must not
270
+ * be a way around a veto.
271
+ *
272
+ * Markers are deliberately not stamped here. They belong to the automatic
273
+ * path's bookkeeping, and a manual run of one rule has not done what the
274
+ * full rule set would.
275
+ */
276
+ @CaptureSpan()
277
+ public async applyRuleToCompletedScans(data: {
278
+ ruleId: ObjectID;
279
+ projectId: ObjectID;
280
+ isDryRun: boolean;
281
+ }): Promise<AutoImportRuleRunResult> {
282
+ const rule: NetworkDeviceAutoImportRule | null =
283
+ await NetworkDeviceAutoImportRuleService.findOneBy({
284
+ query: {
285
+ _id: data.ruleId,
286
+ projectId: data.projectId,
287
+ },
288
+ select: {
289
+ _id: true,
290
+ name: true,
291
+ isEnabled: true,
292
+ isExclusion: true,
293
+ ipMatchTarget: true,
294
+ sysNamePattern: true,
295
+ sysDescrPattern: true,
296
+ sysObjectIdPattern: true,
297
+ includePingOnlyHosts: true,
298
+ },
299
+ props: { isRoot: true },
300
+ });
301
+
302
+ if (!rule) {
303
+ throw new BadDataException("Auto-import rule not found.");
304
+ }
305
+
306
+ /*
307
+ * A DISABLED rule can still be dry-run — that is the point of the dry
308
+ * run: answer "what would this rule import" BEFORE enabling it against
309
+ * live scans. Only a real run of a disabled rule contradicts the toggle
310
+ * the user can see next to the button.
311
+ */
312
+ if (!rule.isEnabled && !data.isDryRun) {
313
+ throw new BadDataException(
314
+ "This auto-import rule is disabled. Enable it before running it, or use Dry Run to preview what it would import.",
315
+ );
316
+ }
317
+
318
+ if (rule.isExclusion) {
319
+ throw new BadDataException(
320
+ "An exclusion rule only vetoes other rules and imports nothing by itself, so running it alone would do nothing. Run one of the import rules it applies to instead.",
321
+ );
322
+ }
323
+
324
+ /*
325
+ * A real run creates devices, so it must hold the same lock the worker
326
+ * sweep holds (see AUTO_IMPORT_SWEEP_LOCK_KEY): without it, a Run Now
327
+ * clicked while the every-minute sweep is processing the same
328
+ * marker-NULL scan races the check-then-create idempotency and both
329
+ * paths insert the same host. A dry run writes nothing and skips the
330
+ * lock.
331
+ */
332
+ let mutex: SemaphoreMutex | null = null;
333
+
334
+ if (!data.isDryRun) {
335
+ try {
336
+ mutex = await Semaphore.lock({
337
+ key: AUTO_IMPORT_SWEEP_LOCK_KEY,
338
+ namespace: AUTO_IMPORT_SWEEP_LOCK_NAMESPACE,
339
+ lockTimeout: AUTO_IMPORT_SWEEP_LOCK_TIMEOUT_MS,
340
+ acquireAttemptsLimit: 3,
341
+ });
342
+ } catch (err) {
343
+ logger.debug(
344
+ `Auto-import Run Now: could not acquire the sweep lock: ${err}`,
345
+ );
346
+ throw new BadDataException(
347
+ "An automatic import sweep is currently running. Try again in a minute — hosts it imports are skipped by your run anyway.",
348
+ );
349
+ }
350
+ }
351
+
352
+ try {
353
+ return await this.runRuleAgainstCompletedScans({
354
+ rule: rule,
355
+ projectId: data.projectId,
356
+ isDryRun: data.isDryRun,
357
+ });
358
+ } finally {
359
+ if (mutex) {
360
+ try {
361
+ await Semaphore.release(mutex);
362
+ } catch (err) {
363
+ logger.error(
364
+ `Auto-import Run Now: error releasing the sweep lock: ${err}`,
365
+ );
366
+ }
367
+ }
368
+ }
369
+ }
370
+
371
+ private async runRuleAgainstCompletedScans(data: {
372
+ rule: NetworkDeviceAutoImportRule;
373
+ projectId: ObjectID;
374
+ isDryRun: boolean;
375
+ }): Promise<AutoImportRuleRunResult> {
376
+ const exclusionRules: Array<NetworkDeviceAutoImportRule> = (
377
+ await this.loadEnabledRules(data.projectId)
378
+ ).filter((candidate: NetworkDeviceAutoImportRule) => {
379
+ return Boolean(candidate.isExclusion);
380
+ });
381
+
382
+ const rules: Array<NetworkDeviceAutoImportRule> = [
383
+ data.rule,
384
+ ...exclusionRules,
385
+ ];
386
+
387
+ const existingHostnames: Set<string> = await this.loadExistingHostnames(
388
+ data.projectId,
389
+ );
390
+
391
+ const result: AutoImportRuleRunResult = this.emptyResult(data.isDryRun);
392
+ const attempts: ImportAttemptBudget = { count: 0 };
393
+
394
+ /*
395
+ * Newest results first: a manual run is "bring the estate up to date",
396
+ * and the newest scan of a subnet supersedes its older runs — with
397
+ * idempotency, whichever result mentions a host first imports it and
398
+ * later mentions skip. Minimal columns only — the full row, with its
399
+ * multi-megabyte discoveredDevices jsonb, is re-read one scan at a time
400
+ * inside the loop so only one result set is ever resident (the same
401
+ * two-phase shape the worker sweep uses, for the same reason).
402
+ */
403
+ const scanStubs: Array<NetworkDeviceDiscoveryScan> =
404
+ await NetworkDeviceDiscoveryScanService.findBy({
405
+ query: {
406
+ projectId: data.projectId,
407
+ status: "Completed",
408
+ },
409
+ select: {
410
+ _id: true,
411
+ },
412
+ sort: {
413
+ completedAt: SortOrder.Descending,
414
+ },
415
+ limit: MAX_SCANS_PER_AUTO_IMPORT_RULE_RUN + 1,
416
+ skip: 0,
417
+ props: { isRoot: true },
418
+ });
419
+
420
+ // One extra row settles "were there more scans" honestly.
421
+ result.hasMoreScans = scanStubs.length > MAX_SCANS_PER_AUTO_IMPORT_RULE_RUN;
422
+
423
+ if (result.hasMoreScans) {
424
+ scanStubs.pop();
425
+ }
426
+
427
+ for (const scanStub of scanStubs) {
428
+ const scan: NetworkDeviceDiscoveryScan | null =
429
+ await NetworkDeviceDiscoveryScanService.findOneBy({
430
+ query: {
431
+ _id: scanStub.id!,
432
+ projectId: data.projectId,
433
+ status: "Completed",
434
+ },
435
+ select: {
436
+ _id: true,
437
+ projectId: true,
438
+ status: true,
439
+ completedAt: true,
440
+ discoveredDevices: true,
441
+ ...AUTO_IMPORT_SCAN_CREDENTIAL_SELECT,
442
+ },
443
+ props: { isRoot: true },
444
+ });
445
+
446
+ // Re-queued or deleted since the stub query — nothing to evaluate.
447
+ if (!scan) {
448
+ continue;
449
+ }
450
+
451
+ const createdIpAddresses: Array<string> = await this.importHostsFromScan({
452
+ scan: scan,
453
+ rules: rules,
454
+ existingHostnames: existingHostnames,
455
+ result: result,
456
+ attempts: attempts,
457
+ isDryRun: data.isDryRun,
458
+ });
459
+
460
+ if (createdIpAddresses.length > 0) {
461
+ /*
462
+ * Same CAS write-back as the automatic path, so the Review dialog
463
+ * does not re-offer (pre-checked!) hosts this run just imported. A
464
+ * miss only means fresher results arrived, which recomputed the
465
+ * flags anyway. No concurrent engine writer can clobber this: the
466
+ * write happens only on real runs, which hold the sweep lock.
467
+ */
468
+ await this.stampScan({
469
+ scan: scan,
470
+ createdIpAddresses: createdIpAddresses,
471
+ leaveMarkerUnstamped: true,
472
+ });
473
+ }
474
+
475
+ // The device cap is shared across the whole run, dry or real.
476
+ if (result.isTruncated) {
477
+ break;
478
+ }
479
+
480
+ /*
481
+ * Yield the event loop between scans: this runs inside an API request
482
+ * handler, and evaluating scan after scan of jsonb back-to-back would
483
+ * starve every other request the process is serving. setTimeout
484
+ * rather than setImmediate because the latter is a bare Node global
485
+ * that the jsdom-based test environment does not define.
486
+ */
487
+ await new Promise<void>((resolve: () => void) => {
488
+ setTimeout(resolve, 0);
489
+ });
490
+ }
491
+
492
+ return result;
493
+ }
494
+
495
+ /*
496
+ * ------------------------------------------------------------------
497
+ * Internals
498
+ * ------------------------------------------------------------------
499
+ */
500
+
501
+ private emptyResult(isDryRun: boolean): AutoImportRuleRunResult {
502
+ return {
503
+ hostsEvaluated: 0,
504
+ hostsMatched: 0,
505
+ hostsExcluded: 0,
506
+ hostsSkippedAlreadyRegistered: 0,
507
+ devicesCreated: 0,
508
+ devicesFailed: 0,
509
+ isTruncated: false,
510
+ hasMoreScans: false,
511
+ isDryRun: isDryRun,
512
+ matchedIpAddressSample: [],
513
+ };
514
+ }
515
+
516
+ /*
517
+ * ALL enabled rules of the project, paged to completeness — never a capped
518
+ * subset. The label/owner engines cap their fetch at 100 and a dropped
519
+ * rule there merely adds nothing; here a dropped EXCLUSION rule fails
520
+ * OPEN, importing the exact hosts the operator vetoed, nondeterministically
521
+ * (an uncapped unsorted fetch returns Postgres's choice of rows). Rule
522
+ * rows are a handful of scalar columns, so completeness is one query in
523
+ * any realistic project.
524
+ */
525
+ private async loadEnabledRules(
526
+ projectId: ObjectID,
527
+ ): Promise<Array<NetworkDeviceAutoImportRule>> {
528
+ const rules: Array<NetworkDeviceAutoImportRule> = [];
529
+
530
+ for (let skip: number = 0; ; skip += LIMIT_MAX) {
531
+ const page: Array<NetworkDeviceAutoImportRule> =
532
+ await NetworkDeviceAutoImportRuleService.findBy({
533
+ query: {
534
+ projectId: projectId,
535
+ isEnabled: true,
536
+ },
537
+ select: {
538
+ _id: true,
539
+ name: true,
540
+ isExclusion: true,
541
+ ipMatchTarget: true,
542
+ sysNamePattern: true,
543
+ sysDescrPattern: true,
544
+ sysObjectIdPattern: true,
545
+ includePingOnlyHosts: true,
546
+ },
547
+ sort: {
548
+ createdAt: SortOrder.Ascending,
549
+ },
550
+ limit: LIMIT_MAX,
551
+ skip: skip,
552
+ props: { isRoot: true },
553
+ });
554
+
555
+ rules.push(...page);
556
+
557
+ if (page.length < LIMIT_MAX) {
558
+ break;
559
+ }
560
+ }
561
+
562
+ return rules;
563
+ }
564
+
565
+ private isTooOldToAutoImport(scan: NetworkDeviceDiscoveryScan): boolean {
566
+ if (!scan.completedAt) {
567
+ return false;
568
+ }
569
+
570
+ return OneUptimeDate.isBefore(
571
+ OneUptimeDate.fromString(scan.completedAt),
572
+ OneUptimeDate.getSomeHoursAgo(MAX_RESULT_AGE_IN_HOURS),
573
+ );
574
+ }
575
+
576
+ private async getExistingHostnames(data: {
577
+ projectId: ObjectID;
578
+ cache: ExistingHostnamesByProjectId;
579
+ }): Promise<Set<string>> {
580
+ const key: string = data.projectId.toString();
581
+
582
+ const cached: Set<string> | undefined = data.cache.get(key);
583
+
584
+ if (cached) {
585
+ return cached;
586
+ }
587
+
588
+ const loaded: Set<string> = await this.loadExistingHostnames(
589
+ data.projectId,
590
+ );
591
+ data.cache.set(key, loaded);
592
+
593
+ return loaded;
594
+ }
595
+
596
+ /*
597
+ * Every device address in the project — the same paged walk the probe
598
+ * ingest endpoint does to stamp isAlreadyRegistered, for the same reason:
599
+ * a truncated answer produces duplicate devices, which is worse than a
600
+ * slow answer. Sorted for stable paging.
601
+ */
602
+ private async loadExistingHostnames(
603
+ projectId: ObjectID,
604
+ ): Promise<Set<string>> {
605
+ const existingHostnames: Set<string> = new Set<string>();
606
+
607
+ for (let skip: number = 0; ; skip += LIMIT_MAX) {
608
+ const existing: Array<NetworkDevice> = await NetworkDeviceService.findBy({
609
+ query: {
610
+ projectId: projectId,
611
+ },
612
+ select: {
613
+ hostname: true,
614
+ },
615
+ sort: {
616
+ createdAt: SortOrder.Ascending,
617
+ },
618
+ limit: LIMIT_MAX,
619
+ skip: skip,
620
+ props: {
621
+ isRoot: true,
622
+ },
623
+ });
624
+
625
+ for (const device of existing) {
626
+ if (device.hostname) {
627
+ existingHostnames.add(device.hostname);
628
+ }
629
+ }
630
+
631
+ if (existing.length < LIMIT_MAX) {
632
+ break;
633
+ }
634
+ }
635
+
636
+ return existingHostnames;
637
+ }
638
+
639
+ /*
640
+ * The core loop: evaluate every discovered host of one scan against the
641
+ * rule set, create what should import, and account for every host in the
642
+ * shared result. Returns the addresses actually created (for the jsonb
643
+ * write-back). Mutates `existingHostnames` as it creates, which is what
644
+ * makes duplicate rows, overlapping scans in one sweep, and repeat runs
645
+ * idempotent.
646
+ */
647
+ private async importHostsFromScan(data: {
648
+ scan: NetworkDeviceDiscoveryScan;
649
+ rules: Array<NetworkDeviceAutoImportRule>;
650
+ existingHostnames: Set<string>;
651
+ result: AutoImportRuleRunResult;
652
+ attempts: ImportAttemptBudget;
653
+ isDryRun: boolean;
654
+ }): Promise<Array<string>> {
655
+ const scan: NetworkDeviceDiscoveryScan = data.scan;
656
+ const result: AutoImportRuleRunResult = data.result;
657
+
658
+ if (!scan.projectId) {
659
+ return [];
660
+ }
661
+
662
+ /*
663
+ * The jsonb is the probe's payload stored verbatim; normalisation fixes
664
+ * the null rows, non-string addresses and duplicate-row flags the
665
+ * Review dialog already learned to survive. See DiscoveredHostUtil.
666
+ */
667
+ const hosts: Array<DiscoveredNetworkDevice> = normalizeDiscoveredHosts(
668
+ (scan.discoveredDevices as Array<DiscoveredNetworkDevice>) || [],
669
+ );
670
+
671
+ const createdIpAddresses: Array<string> = [];
672
+
673
+ for (const host of hosts) {
674
+ /*
675
+ * The address becomes the device's varchar(100) hostname, on which
676
+ * the create THROWS rather than truncates — and no real address is
677
+ * anywhere near that long, so an over-long "address" is junk that
678
+ * would fail identically on every pass. Refusing it here (instead of
679
+ * counting a devicesFailed per pass forever) is what keeps a
680
+ * pathological scan from failing the same way every worker tick.
681
+ */
682
+ if (!host.ipAddress || host.ipAddress.length > 100) {
683
+ continue;
684
+ }
685
+
686
+ result.hostsEvaluated++;
687
+
688
+ const evaluation: AutoImportHostEvaluation =
689
+ AutoImportRuleMatcher.evaluateHost(data.rules, host);
690
+
691
+ if (evaluation.excludedByRule) {
692
+ result.hostsExcluded++;
693
+ continue;
694
+ }
695
+
696
+ if (!evaluation.shouldImport) {
697
+ continue;
698
+ }
699
+
700
+ result.hostsMatched++;
701
+
702
+ /*
703
+ * The frozen isAlreadyRegistered flag AND the live set: the flag is a
704
+ * point-in-time answer from the last upload, the set covers devices
705
+ * created since — including by this very run, which is what collapses
706
+ * duplicate rows and overlapping scans into one device.
707
+ */
708
+ if (
709
+ host.isAlreadyRegistered ||
710
+ data.existingHostnames.has(host.ipAddress)
711
+ ) {
712
+ result.hostsSkippedAlreadyRegistered++;
713
+ continue;
714
+ }
715
+
716
+ if (data.attempts.count >= MAX_DEVICES_PER_AUTO_IMPORT_RUN) {
717
+ result.isTruncated = true;
718
+ break;
719
+ }
720
+
721
+ if (result.matchedIpAddressSample.length < MAX_MATCHED_IP_SAMPLE) {
722
+ result.matchedIpAddressSample.push(host.ipAddress);
723
+ }
724
+
725
+ if (data.isDryRun) {
726
+ /*
727
+ * A dry run reports the device as "created" in no counter at all —
728
+ * hostsMatched minus hostsSkippedAlreadyRegistered is exactly what a
729
+ * real run would attempt, and the sample above says which hosts.
730
+ * The address still joins the set, simulating the create's dedupe:
731
+ * without this, a host on duplicate rows (or in two overlapping
732
+ * scans) would be counted as importable once per appearance, and
733
+ * the dry run would promise more than the real run does.
734
+ */
735
+ data.attempts.count++;
736
+ data.existingHostnames.add(host.ipAddress);
737
+ continue;
738
+ }
739
+
740
+ const wasCreated: boolean = await this.createDeviceForHost({
741
+ projectId: scan.projectId,
742
+ scan: scan,
743
+ host: host,
744
+ existingHostnames: data.existingHostnames,
745
+ result: result,
746
+ attempts: data.attempts,
747
+ });
748
+
749
+ if (wasCreated) {
750
+ createdIpAddresses.push(host.ipAddress);
751
+ }
752
+ }
753
+
754
+ return createdIpAddresses;
755
+ }
756
+
757
+ /*
758
+ * One host -> one device, with the collision protocol the estate needs:
759
+ *
760
+ * Device names are unique per project (an app-level check, not a DB
761
+ * constraint) while addresses are not unique at all, so the failure modes
762
+ * split. A create that fails re-checks the ADDRESS first — losing a race
763
+ * to a concurrent import of the same host must read as "already
764
+ * registered", not spawn a renamed twin. Only when the address is still
765
+ * free is the name the problem (two hosts legitimately sharing a sysName),
766
+ * and one retry under the address-suffixed fallback name settles it.
767
+ */
768
+ private async createDeviceForHost(data: {
769
+ projectId: ObjectID;
770
+ scan: NetworkDeviceDiscoveryScan;
771
+ host: DiscoveredNetworkDevice;
772
+ existingHostnames: Set<string>;
773
+ result: AutoImportRuleRunResult;
774
+ attempts: ImportAttemptBudget;
775
+ }): Promise<boolean> {
776
+ data.attempts.count++;
777
+
778
+ const attemptCreate: (name?: string) => Promise<void> = async (
779
+ name?: string,
780
+ ): Promise<void> => {
781
+ const device: NetworkDevice = buildNetworkDeviceFromDiscoveredHost({
782
+ projectId: data.projectId,
783
+ host: data.host,
784
+ scan: data.scan,
785
+ name: name,
786
+ /*
787
+ * Zero-touch all the way down: nobody reviews an auto-imported
788
+ * device, so nobody would ever click the vendor-template banner.
789
+ * The first poll that fingerprints the vendor seeds the Health
790
+ * OIDs instead (NetworkInventoryUtil).
791
+ */
792
+ autoApplyVendorHealthTemplate: true,
793
+ });
794
+
795
+ await NetworkDeviceService.create({
796
+ data: device,
797
+ props: {
798
+ isRoot: true,
799
+ },
800
+ });
801
+ };
802
+
803
+ try {
804
+ await attemptCreate();
805
+ } catch (firstError) {
806
+ const registeredMeanwhile: NetworkDevice | null =
807
+ await NetworkDeviceService.findOneBy({
808
+ query: {
809
+ projectId: data.projectId,
810
+ hostname: data.host.ipAddress,
811
+ },
812
+ select: { _id: true },
813
+ props: { isRoot: true },
814
+ });
815
+
816
+ if (registeredMeanwhile) {
817
+ // Skipped is a sub-bucket of matched, same as the other run results.
818
+ data.result.hostsSkippedAlreadyRegistered++;
819
+ data.existingHostnames.add(data.host.ipAddress);
820
+ return false;
821
+ }
822
+
823
+ try {
824
+ await attemptCreate(buildFallbackDeviceName(data.host));
825
+ } catch (secondError) {
826
+ data.result.devicesFailed++;
827
+ logger.error(
828
+ `Auto-import: could not create a device for ${data.host.ipAddress} (scan ${data.scan.id?.toString()}): ${firstError} / retry: ${secondError}`,
829
+ { projectId: data.projectId.toString() } as LogAttributes,
830
+ );
831
+ return false;
832
+ }
833
+ }
834
+
835
+ data.result.devicesCreated++;
836
+ data.existingHostnames.add(data.host.ipAddress);
837
+
838
+ return true;
839
+ }
840
+
841
+ /*
842
+ * The one write this engine makes to the scan row: retire the consumed
843
+ * hosts in the stored jsonb (so the Review dialog stops offering them) and
844
+ * stamp the processed marker — in a single hook-free compare-and-set on
845
+ * (status, completedAt), for the reasons on processCompletedScan. The
846
+ * claim endpoint's hook-free contract is why this must never go through
847
+ * updateOneById: the scan service deliberately has no update-success hooks
848
+ * to piggyback on, and this write must not fire the full pipeline on every
849
+ * worker tick.
850
+ *
851
+ * The CAS defends against exactly one concurrent writer — the ingest
852
+ * endpoint, which rewrites completedAt with every upload. It cannot
853
+ * defend against another ENGINE write (which changes neither status nor
854
+ * completedAt), and does not need to: every path that reaches this method
855
+ * holds the sweep lock (the worker for its whole sweep, Run Now for a
856
+ * real run; dry runs never write), so the full-array replace below always
857
+ * has the row's only engine writer. Weaken that invariant and this
858
+ * becomes last-writer-wins on the isAlreadyRegistered flips.
859
+ */
860
+ private async stampScan(data: {
861
+ scan: NetworkDeviceDiscoveryScan;
862
+ createdIpAddresses: Array<string>;
863
+ leaveMarkerUnstamped?: boolean;
864
+ }): Promise<void> {
865
+ const payload: JSONObject = {};
866
+
867
+ if (!data.leaveMarkerUnstamped) {
868
+ payload["autoImportProcessedAt"] = OneUptimeDate.getCurrentDate();
869
+ }
870
+
871
+ if (data.createdIpAddresses.length > 0) {
872
+ const createdSet: Set<string> = new Set<string>(data.createdIpAddresses);
873
+
874
+ /*
875
+ * Mapped over the RAW stored rows, not the normalised copies: the
876
+ * write-back must preserve whatever shape the probe sent, only
877
+ * flipping isAlreadyRegistered on rows whose (trimmed) address was
878
+ * imported. Junk rows pass through untouched.
879
+ */
880
+ const restamped: Array<unknown> = (
881
+ (data.scan.discoveredDevices as Array<unknown>) || []
882
+ ).map((row: unknown): unknown => {
883
+ if (!row || typeof row !== "object") {
884
+ return row;
885
+ }
886
+
887
+ const hostRow: DiscoveredNetworkDevice = row as DiscoveredNetworkDevice;
888
+
889
+ const ipAddress: string = String(hostRow.ipAddress ?? "").trim();
890
+
891
+ if (!ipAddress || !createdSet.has(ipAddress)) {
892
+ return row;
893
+ }
894
+
895
+ return { ...hostRow, isAlreadyRegistered: true };
896
+ });
897
+
898
+ payload["discoveredDevices"] = restamped as unknown as Array<JSONObject>;
899
+ }
900
+
901
+ if (Object.keys(payload).length === 0) {
902
+ return;
903
+ }
904
+
905
+ await NetworkDeviceDiscoveryScanService.updateColumnsByIdWithoutHooks({
906
+ id: data.scan.id!,
907
+ // Cast: the model's JSON column makes DeepPartial recursion blow up.
908
+ data: payload as unknown as QueryDeepPartialEntity<NetworkDeviceDiscoveryScan>,
909
+ expectedData: {
910
+ status: "Completed",
911
+ completedAt: data.scan.completedAt || null,
912
+ } as unknown as QueryDeepPartialEntity<NetworkDeviceDiscoveryScan>,
913
+ });
914
+ }
915
+ }
916
+
917
+ export default new NetworkDeviceAutoImportRuleEngineServiceClass();